@stackmemoryai/stackmemory 1.5.8 → 1.6.0
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 +507 -140
- package/dist/src/cli/commands/orchestrator.js +84 -19
- package/dist/src/daemon/daemon-config.js +2 -3
- package/dist/src/daemon/session-daemon.js +4 -5
- package/dist/src/daemon/unified-daemon.js +4 -5
- package/dist/src/features/sweep/sweep-server-manager.js +3 -6
- package/dist/src/hooks/daemon.js +5 -8
- package/dist/src/utils/process-cleanup.js +9 -0
- 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,6 +4,7 @@ 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,
|
|
9
10
|
readFileSync,
|
|
@@ -19,6 +20,7 @@ import { createInterface } from "readline";
|
|
|
19
20
|
import { fileURLToPath } from "url";
|
|
20
21
|
import { Transform } from "stream";
|
|
21
22
|
import { logger } from "../../core/monitoring/logger.js";
|
|
23
|
+
import { isProcessAlive } from "../../utils/process-cleanup.js";
|
|
22
24
|
import {
|
|
23
25
|
LinearClient
|
|
24
26
|
} from "../../integrations/linear/client.js";
|
|
@@ -28,6 +30,14 @@ import {
|
|
|
28
30
|
} from "../../core/worktree/preflight.js";
|
|
29
31
|
import { ContextCapture } from "../../core/worktree/capture.js";
|
|
30
32
|
import { extractKeywords } from "../../core/utils/text.js";
|
|
33
|
+
function getOutcomesLogPath() {
|
|
34
|
+
return join(homedir(), ".stackmemory", "conductor", "outcomes.jsonl");
|
|
35
|
+
}
|
|
36
|
+
function logAgentOutcome(entry) {
|
|
37
|
+
const dir = join(homedir(), ".stackmemory", "conductor");
|
|
38
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
39
|
+
appendFileSync(getOutcomesLogPath(), JSON.stringify(entry) + "\n");
|
|
40
|
+
}
|
|
31
41
|
function findPackageRoot() {
|
|
32
42
|
const currentFile = fileURLToPath(import.meta.url);
|
|
33
43
|
let dir = dirname(currentFile);
|
|
@@ -635,6 +645,18 @@ class Conductor {
|
|
|
635
645
|
await this.runAgent(issue, run);
|
|
636
646
|
run.status = "completed";
|
|
637
647
|
this.completeCount++;
|
|
648
|
+
logAgentOutcome({
|
|
649
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
650
|
+
issue: issue.identifier,
|
|
651
|
+
attempt: run.attempt,
|
|
652
|
+
outcome: "success",
|
|
653
|
+
phase: run.phase,
|
|
654
|
+
toolCalls: run.toolCalls,
|
|
655
|
+
filesModified: run.filesModified,
|
|
656
|
+
tokensUsed: run.tokensUsed,
|
|
657
|
+
durationMs: Date.now() - run.startedAt,
|
|
658
|
+
hasCommits: true
|
|
659
|
+
});
|
|
638
660
|
await this.runHook(
|
|
639
661
|
"after-run",
|
|
640
662
|
run.workspacePath,
|
|
@@ -1271,7 +1293,32 @@ class Conductor {
|
|
|
1271
1293
|
}
|
|
1272
1294
|
}
|
|
1273
1295
|
}
|
|
1296
|
+
/**
|
|
1297
|
+
* Build the agent prompt. If a custom template exists at
|
|
1298
|
+
* ~/.stackmemory/conductor/prompt-template.md, use it with variable
|
|
1299
|
+
* substitution. Otherwise fall back to the default template.
|
|
1300
|
+
*
|
|
1301
|
+
* Template variables: {{ISSUE_ID}}, {{TITLE}}, {{DESCRIPTION}},
|
|
1302
|
+
* {{LABELS}}, {{PRIORITY}}, {{ATTEMPT}}, {{PRIOR_CONTEXT}}
|
|
1303
|
+
*/
|
|
1274
1304
|
buildPrompt(issue, attempt) {
|
|
1305
|
+
const templatePath = join(
|
|
1306
|
+
homedir(),
|
|
1307
|
+
".stackmemory",
|
|
1308
|
+
"conductor",
|
|
1309
|
+
"prompt-template.md"
|
|
1310
|
+
);
|
|
1311
|
+
const priority = ["None", "Urgent", "High", "Medium", "Low"][issue.priority] || "None";
|
|
1312
|
+
const labels = issue.labels.length > 0 ? issue.labels.map((l) => l.name).join(", ") : "";
|
|
1313
|
+
const priorContext = attempt > 1 ? `This is attempt ${attempt}. Check .stackmemory/conductor-context.md for context from prior attempts.` : "";
|
|
1314
|
+
if (existsSync(templatePath)) {
|
|
1315
|
+
try {
|
|
1316
|
+
let template = readFileSync(templatePath, "utf-8");
|
|
1317
|
+
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);
|
|
1318
|
+
return template;
|
|
1319
|
+
} catch {
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1275
1322
|
const lines = [
|
|
1276
1323
|
`You are working on Linear issue ${issue.identifier}: ${issue.title}`,
|
|
1277
1324
|
""
|
|
@@ -1279,17 +1326,12 @@ class Conductor {
|
|
|
1279
1326
|
if (issue.description) {
|
|
1280
1327
|
lines.push("## Description", "", issue.description, "");
|
|
1281
1328
|
}
|
|
1282
|
-
if (
|
|
1283
|
-
lines.push(`Labels: ${
|
|
1329
|
+
if (labels) {
|
|
1330
|
+
lines.push(`Labels: ${labels}`);
|
|
1284
1331
|
}
|
|
1285
|
-
lines.push(
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
if (attempt > 1) {
|
|
1289
|
-
lines.push(
|
|
1290
|
-
"",
|
|
1291
|
-
`This is attempt ${attempt}. Check .stackmemory/conductor-context.md for context from prior attempts.`
|
|
1292
|
-
);
|
|
1332
|
+
lines.push(`Priority: ${priority}`);
|
|
1333
|
+
if (priorContext) {
|
|
1334
|
+
lines.push("", priorContext);
|
|
1293
1335
|
}
|
|
1294
1336
|
lines.push(
|
|
1295
1337
|
"",
|
|
@@ -1428,14 +1470,7 @@ class Conductor {
|
|
|
1428
1470
|
if (elapsed < staleThresholdMs) continue;
|
|
1429
1471
|
const elapsedMin = Math.round(elapsed / 6e4);
|
|
1430
1472
|
const pid = run.process?.pid;
|
|
1431
|
-
|
|
1432
|
-
if (pid) {
|
|
1433
|
-
try {
|
|
1434
|
-
process.kill(pid, 0);
|
|
1435
|
-
alive = true;
|
|
1436
|
-
} catch {
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1473
|
+
const alive = pid ? isProcessAlive(pid) : false;
|
|
1439
1474
|
if (!alive) {
|
|
1440
1475
|
logger.warn("Stale agent process is dead, cleaning up", {
|
|
1441
1476
|
identifier: run.issue.identifier,
|
|
@@ -1485,6 +1520,35 @@ class Conductor {
|
|
|
1485
1520
|
hasCommits = log.trim().length > 0;
|
|
1486
1521
|
} catch {
|
|
1487
1522
|
}
|
|
1523
|
+
let errorTail;
|
|
1524
|
+
if (!hasCommits) {
|
|
1525
|
+
try {
|
|
1526
|
+
const logPath = join(
|
|
1527
|
+
getAgentStatusDir(run.issue.identifier),
|
|
1528
|
+
"output.log"
|
|
1529
|
+
);
|
|
1530
|
+
if (existsSync(logPath)) {
|
|
1531
|
+
const content = readFileSync(logPath, "utf-8");
|
|
1532
|
+
const lines = content.trim().split("\n");
|
|
1533
|
+
errorTail = lines.slice(-5).join("\n");
|
|
1534
|
+
}
|
|
1535
|
+
} catch {
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
const durationMs = Date.now() - run.startedAt;
|
|
1539
|
+
logAgentOutcome({
|
|
1540
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1541
|
+
issue: run.issue.identifier,
|
|
1542
|
+
attempt: run.attempt,
|
|
1543
|
+
outcome: hasCommits ? "success" : "failure",
|
|
1544
|
+
phase: run.phase,
|
|
1545
|
+
toolCalls: run.toolCalls,
|
|
1546
|
+
filesModified: run.filesModified,
|
|
1547
|
+
tokensUsed: run.tokensUsed,
|
|
1548
|
+
durationMs,
|
|
1549
|
+
hasCommits,
|
|
1550
|
+
errorTail
|
|
1551
|
+
});
|
|
1488
1552
|
if (hasCommits) {
|
|
1489
1553
|
logger.info("Stale agent had commits, transitioning to In Review", {
|
|
1490
1554
|
identifier: run.issue.identifier
|
|
@@ -1567,5 +1631,6 @@ class Conductor {
|
|
|
1567
1631
|
}
|
|
1568
1632
|
export {
|
|
1569
1633
|
Conductor,
|
|
1570
|
-
getAgentStatusDir
|
|
1634
|
+
getAgentStatusDir,
|
|
1635
|
+
getOutcomesLogPath
|
|
1571
1636
|
};
|
|
@@ -5,6 +5,7 @@ const __dirname = __pathDirname(__filename);
|
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
6
6
|
import { join } from "path";
|
|
7
7
|
import { homedir } from "os";
|
|
8
|
+
import { isProcessAlive } from "../utils/process-cleanup.js";
|
|
8
9
|
const DEFAULT_DAEMON_CONFIG = {
|
|
9
10
|
version: "1.0.0",
|
|
10
11
|
context: {
|
|
@@ -139,9 +140,7 @@ function readDaemonStatus() {
|
|
|
139
140
|
try {
|
|
140
141
|
const pidContent = readFileSync(pidFile, "utf8").trim();
|
|
141
142
|
const pid = parseInt(pidContent, 10);
|
|
142
|
-
|
|
143
|
-
process.kill(pid, 0);
|
|
144
|
-
} catch {
|
|
143
|
+
if (!isProcessAlive(pid)) {
|
|
145
144
|
return defaultStatus;
|
|
146
145
|
}
|
|
147
146
|
if (!existsSync(statusFile)) {
|
|
@@ -6,6 +6,7 @@ const __dirname = __pathDirname(__filename);
|
|
|
6
6
|
import * as fs from "fs";
|
|
7
7
|
import * as path from "path";
|
|
8
8
|
import { execSync } from "child_process";
|
|
9
|
+
import { isProcessAlive } from "../utils/process-cleanup.js";
|
|
9
10
|
class SessionDaemon {
|
|
10
11
|
config;
|
|
11
12
|
state;
|
|
@@ -69,16 +70,14 @@ class SessionDaemon {
|
|
|
69
70
|
try {
|
|
70
71
|
const existingPid = fs.readFileSync(this.pidFile, "utf8").trim();
|
|
71
72
|
const pid = parseInt(existingPid, 10);
|
|
72
|
-
|
|
73
|
-
process.kill(pid, 0);
|
|
73
|
+
if (isProcessAlive(pid)) {
|
|
74
74
|
this.log("WARN", "Daemon already running for this session", {
|
|
75
75
|
existingPid: pid
|
|
76
76
|
});
|
|
77
77
|
return false;
|
|
78
|
-
} catch {
|
|
79
|
-
this.log("INFO", "Cleaning up stale PID file", { stalePid: pid });
|
|
80
|
-
fs.unlinkSync(this.pidFile);
|
|
81
78
|
}
|
|
79
|
+
this.log("INFO", "Cleaning up stale PID file", { stalePid: pid });
|
|
80
|
+
fs.unlinkSync(this.pidFile);
|
|
82
81
|
} catch {
|
|
83
82
|
try {
|
|
84
83
|
fs.unlinkSync(this.pidFile);
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
appendFileSync,
|
|
11
11
|
readFileSync
|
|
12
12
|
} from "fs";
|
|
13
|
+
import { isProcessAlive } from "../utils/process-cleanup.js";
|
|
13
14
|
import {
|
|
14
15
|
loadDaemonConfig,
|
|
15
16
|
getDaemonPaths,
|
|
@@ -75,14 +76,12 @@ class UnifiedDaemon {
|
|
|
75
76
|
try {
|
|
76
77
|
const existingPid = readFileSync(this.paths.pidFile, "utf8").trim();
|
|
77
78
|
const pid = parseInt(existingPid, 10);
|
|
78
|
-
|
|
79
|
-
process.kill(pid, 0);
|
|
79
|
+
if (isProcessAlive(pid)) {
|
|
80
80
|
this.log("WARN", "daemon", "Daemon already running", { pid });
|
|
81
81
|
return false;
|
|
82
|
-
} catch {
|
|
83
|
-
this.log("INFO", "daemon", "Cleaning stale PID file", { pid });
|
|
84
|
-
unlinkSync(this.paths.pidFile);
|
|
85
82
|
}
|
|
83
|
+
this.log("INFO", "daemon", "Cleaning stale PID file", { pid });
|
|
84
|
+
unlinkSync(this.paths.pidFile);
|
|
86
85
|
} catch {
|
|
87
86
|
try {
|
|
88
87
|
unlinkSync(this.paths.pidFile);
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "./types.js";
|
|
11
11
|
import { createPredictionClient } from "./prediction-client.js";
|
|
12
12
|
import { logger } from "../../core/monitoring/logger.js";
|
|
13
|
+
import { isProcessAlive } from "../../utils/process-cleanup.js";
|
|
13
14
|
const HOME = process.env["HOME"] || "/tmp";
|
|
14
15
|
const PID_FILE = join(HOME, ".stackmemory", "sweep", "server.pid");
|
|
15
16
|
const LOG_FILE = join(HOME, ".stackmemory", "sweep", "server.log");
|
|
@@ -149,9 +150,7 @@ Download with: huggingface-cli download sweepai/sweep-next-edit-1.5B sweep-next-
|
|
|
149
150
|
process.kill(status.pid, "SIGTERM");
|
|
150
151
|
await new Promise((resolve) => {
|
|
151
152
|
const checkInterval = setInterval(() => {
|
|
152
|
-
|
|
153
|
-
process.kill(status.pid, 0);
|
|
154
|
-
} catch {
|
|
153
|
+
if (!isProcessAlive(status.pid)) {
|
|
155
154
|
clearInterval(checkInterval);
|
|
156
155
|
resolve();
|
|
157
156
|
}
|
|
@@ -186,9 +185,7 @@ Download with: huggingface-cli download sweepai/sweep-next-edit-1.5B sweep-next-
|
|
|
186
185
|
try {
|
|
187
186
|
const data = JSON.parse(readFileSync(PID_FILE, "utf-8"));
|
|
188
187
|
const { pid, port, host, modelPath, startedAt } = data;
|
|
189
|
-
|
|
190
|
-
process.kill(pid, 0);
|
|
191
|
-
} catch {
|
|
188
|
+
if (!isProcessAlive(pid)) {
|
|
192
189
|
return { running: false };
|
|
193
190
|
}
|
|
194
191
|
const client = createPredictionClient({ port, host });
|
package/dist/src/hooks/daemon.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { join, extname, relative } from "path";
|
|
14
14
|
import { spawn } from "child_process";
|
|
15
15
|
import { loadConfig } from "./config.js";
|
|
16
|
+
import { isProcessAlive } from "../utils/process-cleanup.js";
|
|
16
17
|
import {
|
|
17
18
|
hookEmitter
|
|
18
19
|
} from "./events.js";
|
|
@@ -51,13 +52,11 @@ async function startDaemon(options = {}) {
|
|
|
51
52
|
const pidFile = config.daemon.pid_file;
|
|
52
53
|
if (existsSync(pidFile)) {
|
|
53
54
|
const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
|
|
54
|
-
|
|
55
|
-
process.kill(pid, 0);
|
|
55
|
+
if (isProcessAlive(pid)) {
|
|
56
56
|
log("warn", "Daemon already running", { pid });
|
|
57
57
|
return;
|
|
58
|
-
} catch {
|
|
59
|
-
unlinkSync(pidFile);
|
|
60
58
|
}
|
|
59
|
+
unlinkSync(pidFile);
|
|
61
60
|
}
|
|
62
61
|
if (!options.foreground) {
|
|
63
62
|
const child = spawn(
|
|
@@ -117,17 +116,15 @@ function getDaemonStatus() {
|
|
|
117
116
|
return { running: false };
|
|
118
117
|
}
|
|
119
118
|
const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
|
|
120
|
-
|
|
121
|
-
process.kill(pid, 0);
|
|
119
|
+
if (isProcessAlive(pid)) {
|
|
122
120
|
return {
|
|
123
121
|
running: true,
|
|
124
122
|
pid,
|
|
125
123
|
uptime: state.running ? Date.now() - state.startTime : void 0,
|
|
126
124
|
eventsProcessed: state.eventsProcessed
|
|
127
125
|
};
|
|
128
|
-
} catch {
|
|
129
|
-
return { running: false };
|
|
130
126
|
}
|
|
127
|
+
return { running: false };
|
|
131
128
|
}
|
|
132
129
|
function setupLogStream() {
|
|
133
130
|
const logFile = config.daemon.log_file;
|
|
@@ -7,6 +7,14 @@ import * as fs from "fs";
|
|
|
7
7
|
import * as path from "path";
|
|
8
8
|
import * as os from "os";
|
|
9
9
|
import { logger } from "../core/monitoring/logger.js";
|
|
10
|
+
function isProcessAlive(pid) {
|
|
11
|
+
try {
|
|
12
|
+
process.kill(pid, 0);
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
10
18
|
const STACKMEMORY_PROCESS_PATTERNS = [
|
|
11
19
|
"stackmemory",
|
|
12
20
|
"ralph orchestrate",
|
|
@@ -131,5 +139,6 @@ export {
|
|
|
131
139
|
cleanupStaleProcesses,
|
|
132
140
|
findStaleProcesses,
|
|
133
141
|
getStackmemoryProcesses,
|
|
142
|
+
isProcessAlive,
|
|
134
143
|
killStaleProcesses
|
|
135
144
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackmemoryai/stackmemory",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
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).
|