@stackmemoryai/stackmemory 1.5.7 → 1.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cli/commands/orchestrate.js +474 -88
- package/dist/src/cli/commands/orchestrator.js +217 -33
- package/package.json +1 -1
- package/scripts/gepa/.before-optimize.md +0 -32
- package/scripts/gepa/generations/gen-000/baseline.md +0 -32
- package/scripts/gepa/generations/gen-001/baseline.md +0 -32
- package/scripts/gepa/generations/gen-001/variant-a.md +107 -1
- package/scripts/gepa/generations/gen-001/variant-b.md +216 -1
- package/scripts/gepa/generations/gen-001/variant-c.md +83 -1
- package/scripts/gepa/generations/gen-001/variant-d.md +90 -1
- package/scripts/gepa/results/eval-1-baseline.json +78 -78
- package/scripts/gepa/results/eval-1-variant-a.json +74 -74
- package/scripts/gepa/results/eval-1-variant-b.json +78 -78
- package/scripts/gepa/results/eval-1-variant-c.json +72 -72
- package/scripts/gepa/results/eval-1-variant-d.json +80 -80
- package/scripts/gepa/state.json +17 -5
|
@@ -4,8 +4,10 @@ const __filename = __fileURLToPath(import.meta.url);
|
|
|
4
4
|
const __dirname = __pathDirname(__filename);
|
|
5
5
|
import { spawn, execSync } from "child_process";
|
|
6
6
|
import {
|
|
7
|
+
appendFileSync,
|
|
7
8
|
existsSync,
|
|
8
9
|
mkdirSync,
|
|
10
|
+
readFileSync,
|
|
9
11
|
rmSync,
|
|
10
12
|
writeFileSync,
|
|
11
13
|
readdirSync,
|
|
@@ -27,6 +29,14 @@ import {
|
|
|
27
29
|
} from "../../core/worktree/preflight.js";
|
|
28
30
|
import { ContextCapture } from "../../core/worktree/capture.js";
|
|
29
31
|
import { extractKeywords } from "../../core/utils/text.js";
|
|
32
|
+
function getOutcomesLogPath() {
|
|
33
|
+
return join(homedir(), ".stackmemory", "conductor", "outcomes.jsonl");
|
|
34
|
+
}
|
|
35
|
+
function logAgentOutcome(entry) {
|
|
36
|
+
const dir = join(homedir(), ".stackmemory", "conductor");
|
|
37
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
38
|
+
appendFileSync(getOutcomesLogPath(), JSON.stringify(entry) + "\n");
|
|
39
|
+
}
|
|
30
40
|
function findPackageRoot() {
|
|
31
41
|
const currentFile = fileURLToPath(import.meta.url);
|
|
32
42
|
let dir = dirname(currentFile);
|
|
@@ -452,6 +462,7 @@ class Conductor {
|
|
|
452
462
|
logger.info("Rate limit backoff expired, resuming dispatch");
|
|
453
463
|
console.log("[rate-limit] Backoff expired, resuming dispatch");
|
|
454
464
|
}
|
|
465
|
+
await this.checkStaleAgents();
|
|
455
466
|
await this.reconcile();
|
|
456
467
|
const available = this.config.maxConcurrent - this.running.size;
|
|
457
468
|
if (available <= 0) {
|
|
@@ -633,6 +644,18 @@ class Conductor {
|
|
|
633
644
|
await this.runAgent(issue, run);
|
|
634
645
|
run.status = "completed";
|
|
635
646
|
this.completeCount++;
|
|
647
|
+
logAgentOutcome({
|
|
648
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
649
|
+
issue: issue.identifier,
|
|
650
|
+
attempt: run.attempt,
|
|
651
|
+
outcome: "success",
|
|
652
|
+
phase: run.phase,
|
|
653
|
+
toolCalls: run.toolCalls,
|
|
654
|
+
filesModified: run.filesModified,
|
|
655
|
+
tokensUsed: run.tokensUsed,
|
|
656
|
+
durationMs: Date.now() - run.startedAt,
|
|
657
|
+
hasCommits: true
|
|
658
|
+
});
|
|
636
659
|
await this.runHook(
|
|
637
660
|
"after-run",
|
|
638
661
|
run.workspacePath,
|
|
@@ -949,6 +972,26 @@ class Conductor {
|
|
|
949
972
|
return "claude";
|
|
950
973
|
}
|
|
951
974
|
// ── Agent Execution ──
|
|
975
|
+
/**
|
|
976
|
+
* Build environment variables for an agent process.
|
|
977
|
+
* Injects PORTLESS_URL if portless is available, giving each worktree
|
|
978
|
+
* a stable named localhost URL for service discovery.
|
|
979
|
+
*/
|
|
980
|
+
buildAgentEnv(issue, run) {
|
|
981
|
+
const env = { ...process.env };
|
|
982
|
+
delete env["CLAUDECODE"];
|
|
983
|
+
delete env["ANTHROPIC_API_KEY"];
|
|
984
|
+
const wsKey = this.sanitizeIdentifier(issue.identifier);
|
|
985
|
+
return {
|
|
986
|
+
...env,
|
|
987
|
+
SYMPHONY_WORKSPACE_DIR: run.workspacePath,
|
|
988
|
+
SYMPHONY_ISSUE_ID: issue.id,
|
|
989
|
+
SYMPHONY_ISSUE_IDENTIFIER: issue.identifier,
|
|
990
|
+
SYMPHONY_ATTEMPT: String(run.attempt),
|
|
991
|
+
PORTLESS_URL: `http://${wsKey}.localhost:1355`,
|
|
992
|
+
PORTLESS_NAME: wsKey
|
|
993
|
+
};
|
|
994
|
+
}
|
|
952
995
|
async runAgent(issue, run) {
|
|
953
996
|
if (this.config.agentMode === "cli") {
|
|
954
997
|
try {
|
|
@@ -995,18 +1038,7 @@ class Conductor {
|
|
|
995
1038
|
],
|
|
996
1039
|
{
|
|
997
1040
|
cwd: run.workspacePath,
|
|
998
|
-
env: (
|
|
999
|
-
const env = { ...process.env };
|
|
1000
|
-
delete env.CLAUDECODE;
|
|
1001
|
-
delete env.ANTHROPIC_API_KEY;
|
|
1002
|
-
return {
|
|
1003
|
-
...env,
|
|
1004
|
-
SYMPHONY_WORKSPACE_DIR: run.workspacePath,
|
|
1005
|
-
SYMPHONY_ISSUE_ID: issue.id,
|
|
1006
|
-
SYMPHONY_ISSUE_IDENTIFIER: issue.identifier,
|
|
1007
|
-
SYMPHONY_ATTEMPT: String(run.attempt)
|
|
1008
|
-
};
|
|
1009
|
-
})(),
|
|
1041
|
+
env: this.buildAgentEnv(issue, run),
|
|
1010
1042
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1011
1043
|
}
|
|
1012
1044
|
);
|
|
@@ -1114,18 +1146,9 @@ class Conductor {
|
|
|
1114
1146
|
runAgentAdapter(issue, run) {
|
|
1115
1147
|
return new Promise((resolve, reject) => {
|
|
1116
1148
|
const prompt = this.buildPrompt(issue, run.attempt);
|
|
1117
|
-
const env = { ...process.env };
|
|
1118
|
-
delete env.CLAUDECODE;
|
|
1119
|
-
delete env.ANTHROPIC_API_KEY;
|
|
1120
1149
|
const proc = spawn("node", [this.config.appServerPath], {
|
|
1121
1150
|
cwd: run.workspacePath,
|
|
1122
|
-
env:
|
|
1123
|
-
...env,
|
|
1124
|
-
SYMPHONY_WORKSPACE_DIR: run.workspacePath,
|
|
1125
|
-
SYMPHONY_ISSUE_ID: issue.id,
|
|
1126
|
-
SYMPHONY_ISSUE_IDENTIFIER: issue.identifier,
|
|
1127
|
-
SYMPHONY_ATTEMPT: String(run.attempt)
|
|
1128
|
-
},
|
|
1151
|
+
env: this.buildAgentEnv(issue, run),
|
|
1129
1152
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1130
1153
|
});
|
|
1131
1154
|
run.process = proc;
|
|
@@ -1269,7 +1292,32 @@ class Conductor {
|
|
|
1269
1292
|
}
|
|
1270
1293
|
}
|
|
1271
1294
|
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Build the agent prompt. If a custom template exists at
|
|
1297
|
+
* ~/.stackmemory/conductor/prompt-template.md, use it with variable
|
|
1298
|
+
* substitution. Otherwise fall back to the default template.
|
|
1299
|
+
*
|
|
1300
|
+
* Template variables: {{ISSUE_ID}}, {{TITLE}}, {{DESCRIPTION}},
|
|
1301
|
+
* {{LABELS}}, {{PRIORITY}}, {{ATTEMPT}}, {{PRIOR_CONTEXT}}
|
|
1302
|
+
*/
|
|
1272
1303
|
buildPrompt(issue, attempt) {
|
|
1304
|
+
const templatePath = join(
|
|
1305
|
+
homedir(),
|
|
1306
|
+
".stackmemory",
|
|
1307
|
+
"conductor",
|
|
1308
|
+
"prompt-template.md"
|
|
1309
|
+
);
|
|
1310
|
+
const priority = ["None", "Urgent", "High", "Medium", "Low"][issue.priority] || "None";
|
|
1311
|
+
const labels = issue.labels.length > 0 ? issue.labels.map((l) => l.name).join(", ") : "";
|
|
1312
|
+
const priorContext = attempt > 1 ? `This is attempt ${attempt}. Check .stackmemory/conductor-context.md for context from prior attempts.` : "";
|
|
1313
|
+
if (existsSync(templatePath)) {
|
|
1314
|
+
try {
|
|
1315
|
+
let template = readFileSync(templatePath, "utf-8");
|
|
1316
|
+
template = template.replace(/\{\{ISSUE_ID\}\}/g, issue.identifier).replace(/\{\{TITLE\}\}/g, issue.title).replace(/\{\{DESCRIPTION\}\}/g, issue.description || "").replace(/\{\{LABELS\}\}/g, labels).replace(/\{\{PRIORITY\}\}/g, priority).replace(/\{\{ATTEMPT\}\}/g, String(attempt)).replace(/\{\{PRIOR_CONTEXT\}\}/g, priorContext);
|
|
1317
|
+
return template;
|
|
1318
|
+
} catch {
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1273
1321
|
const lines = [
|
|
1274
1322
|
`You are working on Linear issue ${issue.identifier}: ${issue.title}`,
|
|
1275
1323
|
""
|
|
@@ -1277,17 +1325,12 @@ class Conductor {
|
|
|
1277
1325
|
if (issue.description) {
|
|
1278
1326
|
lines.push("## Description", "", issue.description, "");
|
|
1279
1327
|
}
|
|
1280
|
-
if (
|
|
1281
|
-
lines.push(`Labels: ${
|
|
1328
|
+
if (labels) {
|
|
1329
|
+
lines.push(`Labels: ${labels}`);
|
|
1282
1330
|
}
|
|
1283
|
-
lines.push(
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
if (attempt > 1) {
|
|
1287
|
-
lines.push(
|
|
1288
|
-
"",
|
|
1289
|
-
`This is attempt ${attempt}. Check .stackmemory/conductor-context.md for context from prior attempts.`
|
|
1290
|
-
);
|
|
1331
|
+
lines.push(`Priority: ${priority}`);
|
|
1332
|
+
if (priorContext) {
|
|
1333
|
+
lines.push("", priorContext);
|
|
1291
1334
|
}
|
|
1292
1335
|
lines.push(
|
|
1293
1336
|
"",
|
|
@@ -1405,6 +1448,146 @@ class Conductor {
|
|
|
1405
1448
|
}
|
|
1406
1449
|
}
|
|
1407
1450
|
// ── Reconciliation ──
|
|
1451
|
+
/**
|
|
1452
|
+
* Check for stale agents — no status update in 1 hour.
|
|
1453
|
+
* If the process is dead, clean up. If alive but stuck, kill and free the slot.
|
|
1454
|
+
*/
|
|
1455
|
+
async checkStaleAgents() {
|
|
1456
|
+
if (this.running.size === 0) return;
|
|
1457
|
+
const staleThresholdMs = 60 * 60 * 1e3;
|
|
1458
|
+
const now = Date.now();
|
|
1459
|
+
for (const [issueId, run] of this.running) {
|
|
1460
|
+
const agentDir = getAgentStatusDir(run.issue.identifier);
|
|
1461
|
+
const statusPath = join(agentDir, "status.json");
|
|
1462
|
+
let lastUpdate = run.startedAt;
|
|
1463
|
+
try {
|
|
1464
|
+
const data = JSON.parse(readFileSync(statusPath, "utf-8"));
|
|
1465
|
+
lastUpdate = new Date(data.lastUpdate).getTime();
|
|
1466
|
+
} catch {
|
|
1467
|
+
}
|
|
1468
|
+
const elapsed = now - lastUpdate;
|
|
1469
|
+
if (elapsed < staleThresholdMs) continue;
|
|
1470
|
+
const elapsedMin = Math.round(elapsed / 6e4);
|
|
1471
|
+
const pid = run.process?.pid;
|
|
1472
|
+
let alive = false;
|
|
1473
|
+
if (pid) {
|
|
1474
|
+
try {
|
|
1475
|
+
process.kill(pid, 0);
|
|
1476
|
+
alive = true;
|
|
1477
|
+
} catch {
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
if (!alive) {
|
|
1481
|
+
logger.warn("Stale agent process is dead, cleaning up", {
|
|
1482
|
+
identifier: run.issue.identifier,
|
|
1483
|
+
pid,
|
|
1484
|
+
staleMins: elapsedMin
|
|
1485
|
+
});
|
|
1486
|
+
console.log(
|
|
1487
|
+
`[${run.issue.identifier}] Agent dead after ${elapsedMin}m \u2014 finalizing`
|
|
1488
|
+
);
|
|
1489
|
+
await this.finalizeStaleAgent(run);
|
|
1490
|
+
this.running.delete(issueId);
|
|
1491
|
+
} else {
|
|
1492
|
+
logger.warn("Stale agent still alive but stuck, killing", {
|
|
1493
|
+
identifier: run.issue.identifier,
|
|
1494
|
+
pid,
|
|
1495
|
+
staleMins: elapsedMin
|
|
1496
|
+
});
|
|
1497
|
+
console.log(
|
|
1498
|
+
`[${run.issue.identifier}] Agent stuck for ${elapsedMin}m \u2014 killing pid ${pid}`
|
|
1499
|
+
);
|
|
1500
|
+
run.process?.kill("SIGTERM");
|
|
1501
|
+
await new Promise((resolve) => setTimeout(resolve, 1e4));
|
|
1502
|
+
if (run.process && !run.process.killed) {
|
|
1503
|
+
run.process.kill("SIGKILL");
|
|
1504
|
+
}
|
|
1505
|
+
await this.finalizeStaleAgent(run);
|
|
1506
|
+
this.running.delete(issueId);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* Finalize an agent that completed or died without proper cleanup.
|
|
1512
|
+
* Checks for commits, transitions Linear issue, takes snapshot.
|
|
1513
|
+
*/
|
|
1514
|
+
async finalizeStaleAgent(run) {
|
|
1515
|
+
const wsPath = run.workspacePath;
|
|
1516
|
+
if (!wsPath || !existsSync(wsPath)) {
|
|
1517
|
+
this.completed.add(run.issue.id);
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
let hasCommits = false;
|
|
1521
|
+
try {
|
|
1522
|
+
const log = execSync(
|
|
1523
|
+
`git log origin/${this.config.baseBranch}..HEAD --oneline`,
|
|
1524
|
+
{ cwd: wsPath, encoding: "utf-8", timeout: 1e4 }
|
|
1525
|
+
);
|
|
1526
|
+
hasCommits = log.trim().length > 0;
|
|
1527
|
+
} catch {
|
|
1528
|
+
}
|
|
1529
|
+
let errorTail;
|
|
1530
|
+
if (!hasCommits) {
|
|
1531
|
+
try {
|
|
1532
|
+
const logPath = join(
|
|
1533
|
+
getAgentStatusDir(run.issue.identifier),
|
|
1534
|
+
"output.log"
|
|
1535
|
+
);
|
|
1536
|
+
if (existsSync(logPath)) {
|
|
1537
|
+
const content = readFileSync(logPath, "utf-8");
|
|
1538
|
+
const lines = content.trim().split("\n");
|
|
1539
|
+
errorTail = lines.slice(-5).join("\n");
|
|
1540
|
+
}
|
|
1541
|
+
} catch {
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
const durationMs = Date.now() - run.startedAt;
|
|
1545
|
+
logAgentOutcome({
|
|
1546
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1547
|
+
issue: run.issue.identifier,
|
|
1548
|
+
attempt: run.attempt,
|
|
1549
|
+
outcome: hasCommits ? "success" : "failure",
|
|
1550
|
+
phase: run.phase,
|
|
1551
|
+
toolCalls: run.toolCalls,
|
|
1552
|
+
filesModified: run.filesModified,
|
|
1553
|
+
tokensUsed: run.tokensUsed,
|
|
1554
|
+
durationMs,
|
|
1555
|
+
hasCommits,
|
|
1556
|
+
errorTail
|
|
1557
|
+
});
|
|
1558
|
+
if (hasCommits) {
|
|
1559
|
+
logger.info("Stale agent had commits, transitioning to In Review", {
|
|
1560
|
+
identifier: run.issue.identifier
|
|
1561
|
+
});
|
|
1562
|
+
console.log(
|
|
1563
|
+
`[${run.issue.identifier}] Found commits \u2014 moving to In Review`
|
|
1564
|
+
);
|
|
1565
|
+
this.takeSnapshot(wsPath, run.issue);
|
|
1566
|
+
await this.transitionIssue(run.issue, this.config.inReviewState).catch(
|
|
1567
|
+
(err) => {
|
|
1568
|
+
logger.error("Failed to transition stale agent issue", {
|
|
1569
|
+
identifier: run.issue.identifier,
|
|
1570
|
+
error: err.message
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
);
|
|
1574
|
+
this.completeCount++;
|
|
1575
|
+
} else {
|
|
1576
|
+
logger.info("Stale agent had no commits, marking as failed", {
|
|
1577
|
+
identifier: run.issue.identifier
|
|
1578
|
+
});
|
|
1579
|
+
console.log(
|
|
1580
|
+
`[${run.issue.identifier}] No commits found \u2014 marking failed`
|
|
1581
|
+
);
|
|
1582
|
+
this.failCount++;
|
|
1583
|
+
}
|
|
1584
|
+
this.writeAgentStatus(run.issue.identifier, {
|
|
1585
|
+
...run,
|
|
1586
|
+
status: hasCommits ? "completed" : "failed",
|
|
1587
|
+
phase: hasCommits ? "committing" : run.phase
|
|
1588
|
+
});
|
|
1589
|
+
this.completed.add(run.issue.id);
|
|
1590
|
+
}
|
|
1408
1591
|
async reconcile() {
|
|
1409
1592
|
if (!this.client || this.running.size === 0) return;
|
|
1410
1593
|
for (const [issueId, run] of this.running) {
|
|
@@ -1454,5 +1637,6 @@ class Conductor {
|
|
|
1454
1637
|
}
|
|
1455
1638
|
export {
|
|
1456
1639
|
Conductor,
|
|
1457
|
-
getAgentStatusDir
|
|
1640
|
+
getAgentStatusDir,
|
|
1641
|
+
getOutcomesLogPath
|
|
1458
1642
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackmemoryai/stackmemory",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.9",
|
|
4
4
|
"description": "Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, and automatic hooks.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.0.0",
|
|
@@ -103,38 +103,6 @@ Located at `api/app/graphql/{api,private}/v1/`
|
|
|
103
103
|
- Use `test()` instead of `it()` in tests
|
|
104
104
|
- Use `toBeCalled()` instead of `toHaveBeenCalledWith()` in jest assertions
|
|
105
105
|
|
|
106
|
-
## LSP Setup (Code Intelligence)
|
|
107
|
-
|
|
108
|
-
Claude Code uses LSP plugins for go-to-definition, find-references, and real-time diagnostics.
|
|
109
|
-
|
|
110
|
-
### Installed Plugins
|
|
111
|
-
- **vtsls** (JS/TS) - `vtsls@claude-code-lsps` from boostvolt marketplace
|
|
112
|
-
- **ruby-lsp** (Ruby) - `ruby-lsp@ruby-skills` + `ruby-skills@ruby-skills` from st0012 marketplace
|
|
113
|
-
|
|
114
|
-
### Prerequisites
|
|
115
|
-
```bash
|
|
116
|
-
# TypeScript/JS language server
|
|
117
|
-
npm install -g @vtsls/language-server
|
|
118
|
-
|
|
119
|
-
# Ruby language server (use rbenv gem, not system)
|
|
120
|
-
/Users/jwu/.rbenv/versions/3.3.5/bin/gem install ruby-lsp
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
### Known Issues
|
|
124
|
-
- Official `typescript-lsp@claude-plugins-official` is broken (missing plugin.json) — use `vtsls@claude-code-lsps` instead
|
|
125
|
-
- Ruby LSP launch script patched at `~/.claude/plugins/cache/ruby-skills/ruby-lsp/1.0.0/scripts/launch-ruby-lsp.sh` — macOS lacks GNU `timeout` which breaks the version manager detection. Patch hardcodes rbenv PATH.
|
|
126
|
-
- Plugin auto-updates may overwrite the patch — re-apply if Ruby LSP stops working
|
|
127
|
-
|
|
128
|
-
### Reinstall Commands
|
|
129
|
-
```bash
|
|
130
|
-
claude plugin marketplace add boostvolt/claude-code-lsps
|
|
131
|
-
claude plugin install vtsls@claude-code-lsps
|
|
132
|
-
|
|
133
|
-
claude plugin marketplace add st0012/ruby-skills
|
|
134
|
-
claude plugin install ruby-lsp@ruby-skills
|
|
135
|
-
claude plugin install ruby-skills@ruby-skills
|
|
136
|
-
```
|
|
137
|
-
|
|
138
106
|
## Key Configuration Files
|
|
139
107
|
|
|
140
108
|
- `api/config/database.yml` - Database connections (primary + timescale)
|
|
@@ -103,38 +103,6 @@ Located at `api/app/graphql/{api,private}/v1/`
|
|
|
103
103
|
- Use `test()` instead of `it()` in tests
|
|
104
104
|
- Use `toBeCalled()` instead of `toHaveBeenCalledWith()` in jest assertions
|
|
105
105
|
|
|
106
|
-
## LSP Setup (Code Intelligence)
|
|
107
|
-
|
|
108
|
-
Claude Code uses LSP plugins for go-to-definition, find-references, and real-time diagnostics.
|
|
109
|
-
|
|
110
|
-
### Installed Plugins
|
|
111
|
-
- **vtsls** (JS/TS) - `vtsls@claude-code-lsps` from boostvolt marketplace
|
|
112
|
-
- **ruby-lsp** (Ruby) - `ruby-lsp@ruby-skills` + `ruby-skills@ruby-skills` from st0012 marketplace
|
|
113
|
-
|
|
114
|
-
### Prerequisites
|
|
115
|
-
```bash
|
|
116
|
-
# TypeScript/JS language server
|
|
117
|
-
npm install -g @vtsls/language-server
|
|
118
|
-
|
|
119
|
-
# Ruby language server (use rbenv gem, not system)
|
|
120
|
-
/Users/jwu/.rbenv/versions/3.3.5/bin/gem install ruby-lsp
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
### Known Issues
|
|
124
|
-
- Official `typescript-lsp@claude-plugins-official` is broken (missing plugin.json) — use `vtsls@claude-code-lsps` instead
|
|
125
|
-
- Ruby LSP launch script patched at `~/.claude/plugins/cache/ruby-skills/ruby-lsp/1.0.0/scripts/launch-ruby-lsp.sh` — macOS lacks GNU `timeout` which breaks the version manager detection. Patch hardcodes rbenv PATH.
|
|
126
|
-
- Plugin auto-updates may overwrite the patch — re-apply if Ruby LSP stops working
|
|
127
|
-
|
|
128
|
-
### Reinstall Commands
|
|
129
|
-
```bash
|
|
130
|
-
claude plugin marketplace add boostvolt/claude-code-lsps
|
|
131
|
-
claude plugin install vtsls@claude-code-lsps
|
|
132
|
-
|
|
133
|
-
claude plugin marketplace add st0012/ruby-skills
|
|
134
|
-
claude plugin install ruby-lsp@ruby-skills
|
|
135
|
-
claude plugin install ruby-skills@ruby-skills
|
|
136
|
-
```
|
|
137
|
-
|
|
138
106
|
## Key Configuration Files
|
|
139
107
|
|
|
140
108
|
- `api/config/database.yml` - Database connections (primary + timescale)
|
|
@@ -103,38 +103,6 @@ Located at `api/app/graphql/{api,private}/v1/`
|
|
|
103
103
|
- Use `test()` instead of `it()` in tests
|
|
104
104
|
- Use `toBeCalled()` instead of `toHaveBeenCalledWith()` in jest assertions
|
|
105
105
|
|
|
106
|
-
## LSP Setup (Code Intelligence)
|
|
107
|
-
|
|
108
|
-
Claude Code uses LSP plugins for go-to-definition, find-references, and real-time diagnostics.
|
|
109
|
-
|
|
110
|
-
### Installed Plugins
|
|
111
|
-
- **vtsls** (JS/TS) - `vtsls@claude-code-lsps` from boostvolt marketplace
|
|
112
|
-
- **ruby-lsp** (Ruby) - `ruby-lsp@ruby-skills` + `ruby-skills@ruby-skills` from st0012 marketplace
|
|
113
|
-
|
|
114
|
-
### Prerequisites
|
|
115
|
-
```bash
|
|
116
|
-
# TypeScript/JS language server
|
|
117
|
-
npm install -g @vtsls/language-server
|
|
118
|
-
|
|
119
|
-
# Ruby language server (use rbenv gem, not system)
|
|
120
|
-
/Users/jwu/.rbenv/versions/3.3.5/bin/gem install ruby-lsp
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
### Known Issues
|
|
124
|
-
- Official `typescript-lsp@claude-plugins-official` is broken (missing plugin.json) — use `vtsls@claude-code-lsps` instead
|
|
125
|
-
- Ruby LSP launch script patched at `~/.claude/plugins/cache/ruby-skills/ruby-lsp/1.0.0/scripts/launch-ruby-lsp.sh` — macOS lacks GNU `timeout` which breaks the version manager detection. Patch hardcodes rbenv PATH.
|
|
126
|
-
- Plugin auto-updates may overwrite the patch — re-apply if Ruby LSP stops working
|
|
127
|
-
|
|
128
|
-
### Reinstall Commands
|
|
129
|
-
```bash
|
|
130
|
-
claude plugin marketplace add boostvolt/claude-code-lsps
|
|
131
|
-
claude plugin install vtsls@claude-code-lsps
|
|
132
|
-
|
|
133
|
-
claude plugin marketplace add st0012/ruby-skills
|
|
134
|
-
claude plugin install ruby-lsp@ruby-skills
|
|
135
|
-
claude plugin install ruby-skills@ruby-skills
|
|
136
|
-
```
|
|
137
|
-
|
|
138
106
|
## Key Configuration Files
|
|
139
107
|
|
|
140
108
|
- `api/config/database.yml` - Database connections (primary + timescale)
|
|
@@ -1 +1,107 @@
|
|
|
1
|
-
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Guidance for Claude Code when working in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
Sol is the **Rize** monorepo (automatic time tracking). Stack:
|
|
8
|
+
- **api/** - Rails 7.1 GraphQL backend (Ruby 3.3.5)
|
|
9
|
+
- **web/** - Next.js 14 React app (Node 22)
|
|
10
|
+
- **electron/** - Electron desktop app (Node 22)
|
|
11
|
+
- **services/** - Bun TypeScript event consumers/workers
|
|
12
|
+
- **vanity/** - Webflow marketing scripts (deprecated)
|
|
13
|
+
- **voyager/** - Marketing website (Next.js)
|
|
14
|
+
- **puppet/** - Puppeteer server for images/PDFs
|
|
15
|
+
- **chrome/** - Chrome extension
|
|
16
|
+
- **docs/** - Docusaurus site
|
|
17
|
+
- **zapier/** - Zapier integration
|
|
18
|
+
|
|
19
|
+
## Development Commands
|
|
20
|
+
|
|
21
|
+
### Start Dev Environment
|
|
22
|
+
```bash
|
|
23
|
+
./scripts/run-dev.sh # All services (requires iTerm2 on macOS)
|
|
24
|
+
|
|
25
|
+
# Individually:
|
|
26
|
+
cd api && hivemind Procfile.dev # Rails + AnyCable + Sidekiq + Clockwork
|
|
27
|
+
cd web && npm run dev # Next.js (port 3001)
|
|
28
|
+
cd electron && npm run dev # Electron with hot reload
|
|
29
|
+
cd services && hivemind Procfile.dev # Bun services
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Docker (required before api/services)
|
|
33
|
+
```bash
|
|
34
|
+
cd api && docker-compose up -d
|
|
35
|
+
# TimescaleDB: localhost:15432 | Redis: localhost:16379
|
|
36
|
+
# Kafka: localhost:9092 | MySQL: localhost:13306
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Testing
|
|
40
|
+
```bash
|
|
41
|
+
# API (RSpec)
|
|
42
|
+
cd api && bundle exec rspec
|
|
43
|
+
cd api && bundle exec rspec spec/path/to/file_spec.rb
|
|
44
|
+
cd api && bundle exec rspec spec/path/to/file_spec.rb:42
|
|
45
|
+
|
|
46
|
+
# Electron (Jest)
|
|
47
|
+
cd electron && npm test
|
|
48
|
+
cd electron && npm run test:watch
|
|
49
|
+
cd electron && npm run test:coverage
|
|
50
|
+
|
|
51
|
+
# Web: no active tests (exits 0)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Building
|
|
55
|
+
```bash
|
|
56
|
+
cd api && bundle install && rake db:migrate
|
|
57
|
+
cd web && npm run build # gql-gen + tailwind + next build
|
|
58
|
+
cd electron && npm run build # Electron Forge make
|
|
59
|
+
cd services && bun install
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### GraphQL Code Generation
|
|
63
|
+
- `cd web && npm run build` — includes gql codegen
|
|
64
|
+
- `cd electron && npm run dev` — runs gql codegen automatically
|
|
65
|
+
|
|
66
|
+
## Architecture
|
|
67
|
+
|
|
68
|
+
### GraphQL API
|
|
69
|
+
Two endpoints:
|
|
70
|
+
- **api/v1** — Public API (OAuth, Zapier) → `api/app/graphql/api/v1/`
|
|
71
|
+
- **private/v1** — Internal API (web, electron) → `api/app/graphql/private/v1/`
|
|
72
|
+
|
|
73
|
+
### Real-time
|
|
74
|
+
- AnyCable WebSocket subscriptions
|
|
75
|
+
- ActionCable channels: `api/app/channels/`
|
|
76
|
+
- Config: `api/config/cable.yml`, `api/config/anycable.yml`
|
|
77
|
+
|
|
78
|
+
### Background Jobs
|
|
79
|
+
- Sidekiq (async): `api/config/sidekiq.yml`
|
|
80
|
+
- Clockwork (scheduled): `api/config/clock.rb`
|
|
81
|
+
|
|
82
|
+
### Event Streaming
|
|
83
|
+
- Kafka publish/consume via `services/consumers/`
|
|
84
|
+
- Config: `api/config/initializers/kafka.rb`
|
|
85
|
+
|
|
86
|
+
### Databases
|
|
87
|
+
- **PostgreSQL** — primary app data
|
|
88
|
+
- **TimescaleDB** — time-series (separate connection in `database.yml`)
|
|
89
|
+
- **MySQL** — legacy integrations
|
|
90
|
+
- **Redis** — caching, ActionCable, Sidekiq
|
|
91
|
+
|
|
92
|
+
## Style Guidelines
|
|
93
|
+
|
|
94
|
+
### JavaScript/TypeScript Tests
|
|
95
|
+
- Use `test()` not `it()`
|
|
96
|
+
- Use `toBeCalled()` not `toHaveBeenCalledWith()`
|
|
97
|
+
|
|
98
|
+
## Key Config Files
|
|
99
|
+
|
|
100
|
+
| File | Purpose |
|
|
101
|
+
|------|---------|
|
|
102
|
+
| `api/config/database.yml` | DB connections (primary + timescale) |
|
|
103
|
+
| `api/config/cable.yml` | AnyCable WebSocket config |
|
|
104
|
+
| `api/Procfile.dev` | Dev processes (rails, anycable, sidekiq, clockwork) |
|
|
105
|
+
| `sol.code-workspace` | VS Code multi-folder workspace |
|
|
106
|
+
|
|
107
|
+
Each subproject needs its own `.env` file (not committed).
|