@stackmemoryai/stackmemory 1.5.2 → 1.5.4
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/README.md +24 -1
- package/dist/src/cli/commands/dashboard.js +47 -1
- package/dist/src/cli/commands/orchestrate.js +82 -5
- package/dist/src/cli/commands/orchestrator.js +428 -61
- package/package.json +2 -2
- package/scripts/gepa/.before-optimize.md +89 -53
- package/scripts/gepa/generations/gen-000/baseline.md +8 -0
- package/scripts/gepa/generations/gen-001/baseline.md +8 -0
- package/scripts/gepa/generations/gen-001/variant-a.md +81 -81
- package/scripts/gepa/generations/gen-001/variant-b.md +78 -64
- package/scripts/gepa/generations/gen-001/variant-c.md +54 -74
- package/scripts/gepa/generations/gen-001/variant-d.md +79 -113
- package/scripts/gepa/results/eval-1-baseline.json +74 -74
- package/scripts/gepa/results/eval-1-variant-a.json +64 -64
- package/scripts/gepa/results/eval-1-variant-b.json +65 -65
- package/scripts/gepa/results/eval-1-variant-c.json +78 -58
- package/scripts/gepa/results/eval-1-variant-d.json +77 -57
- package/scripts/gepa/state.json +14 -14
package/README.md
CHANGED
|
@@ -24,7 +24,8 @@ StackMemory is a **production-ready memory runtime** for AI coding tools that pr
|
|
|
24
24
|
- **Automatic hooks** for task tracking, Linear sync, and spec progress
|
|
25
25
|
- **Snapshot capture** — post-run context snapshots for session handoff (`snapshot save`)
|
|
26
26
|
- **Pre-flight overlap check** — predict file conflicts before parallel task dispatch (`preflight`)
|
|
27
|
-
- **Conductor orchestrator** — polls Linear, creates worktrees, spawns agents with bounded concurrency
|
|
27
|
+
- **Conductor orchestrator** — polls Linear, creates worktrees, spawns agents with bounded concurrency and auto team detection
|
|
28
|
+
- **Loop/Watch command** — poll any shell command until a condition is met (monitor CI, deploys, logs)
|
|
28
29
|
- **Memory monitor daemon** with automatic capture/clear on RAM pressure
|
|
29
30
|
- **Auto-save service** for periodic context persistence
|
|
30
31
|
- **Comprehensive test coverage** across all core modules
|
|
@@ -68,6 +69,8 @@ Tools forget decisions and constraints between sessions. StackMemory makes conte
|
|
|
68
69
|
- **Safe branches**: worktree isolation with `--worktree` or `-w`
|
|
69
70
|
- **Snapshot**: post-run context capture — records what changed, commits, and decisions (`stackmemory snapshot`)
|
|
70
71
|
- **Pre-flight check**: file overlap prediction before parallel task dispatch (`stackmemory preflight`)
|
|
72
|
+
- **Loop/Watch**: poll shell commands until conditions are met — monitor CI, deploys, logs (`stackmemory loop`)
|
|
73
|
+
- **Conductor**: autonomous orchestrator — polls Linear, creates worktrees, spawns agents with auto team detection
|
|
71
74
|
- **Persistent context**: frames, anchors, decisions, retrieval
|
|
72
75
|
- **Integrations**: Linear (API key + OAuth), DiffMem, Browser MCP, log-mcp (log analysis)
|
|
73
76
|
|
|
@@ -389,6 +392,26 @@ stackmemory pf "auth work" "db migration" -f "task1:src/auth.ts;task2:src/db.ts"
|
|
|
389
392
|
stackmemory pf "task A" "task B" "task C" --json
|
|
390
393
|
```
|
|
391
394
|
|
|
395
|
+
### Loop / Watch (`loop` / `watch`)
|
|
396
|
+
|
|
397
|
+
Poll a shell command until a condition is met. Useful for monitoring CI runs, deploy logs, inboxes, or any external state.
|
|
398
|
+
|
|
399
|
+
```bash
|
|
400
|
+
# Monitor GitHub Actions until complete
|
|
401
|
+
stackmemory loop "gh run view <id> --json status -q .status" --until "completed"
|
|
402
|
+
|
|
403
|
+
# Watch deploy logs for success
|
|
404
|
+
stackmemory watch "railway logs --latest" --until "deployed" -i 5s
|
|
405
|
+
|
|
406
|
+
# Wait for health check to pass
|
|
407
|
+
stackmemory loop "curl -s http://localhost:3000/health" --until "ok" -i 3s -t 5m
|
|
408
|
+
|
|
409
|
+
# JSON output for programmatic use
|
|
410
|
+
stackmemory loop "gh pr checks 42 --json state -q '.[0].state'" --until "SUCCESS" --json
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Options: `--until`, `--until-not`, `--until-empty`, `--until-non-empty`, `--until-exit`, `-i/--interval` (default 10s), `-t/--timeout` (default 30m), `--json`, `-q/--quiet`
|
|
414
|
+
|
|
392
415
|
---
|
|
393
416
|
|
|
394
417
|
## Documentation
|
|
@@ -8,7 +8,7 @@ import Table from "cli-table3";
|
|
|
8
8
|
import { SessionManager } from "../../core/session/session-manager.js";
|
|
9
9
|
import Database from "better-sqlite3";
|
|
10
10
|
import { join } from "path";
|
|
11
|
-
import { existsSync } from "fs";
|
|
11
|
+
import { existsSync, readFileSync } from "fs";
|
|
12
12
|
import { getModelTokenLimit } from "../../core/models/model-router.js";
|
|
13
13
|
const dashboardCommand = {
|
|
14
14
|
command: "dashboard",
|
|
@@ -150,6 +150,52 @@ const dashboardCommand = {
|
|
|
150
150
|
console.log(chalk.yellow.bold("\u{1F4BE} Context Usage"));
|
|
151
151
|
console.log(`${usageBar} ${contextUsage}%`);
|
|
152
152
|
console.log();
|
|
153
|
+
const conductorStatusPath = join(
|
|
154
|
+
projectRoot,
|
|
155
|
+
".stackmemory",
|
|
156
|
+
"conductor-status.json"
|
|
157
|
+
);
|
|
158
|
+
if (existsSync(conductorStatusPath)) {
|
|
159
|
+
try {
|
|
160
|
+
const raw = readFileSync(conductorStatusPath, "utf-8");
|
|
161
|
+
const conductorStatus = JSON.parse(raw);
|
|
162
|
+
const stale = Date.now() - conductorStatus.updatedAt > 12e4;
|
|
163
|
+
const header = stale ? chalk.gray.bold("\u2699 Conductor (stale)") : chalk.yellow.bold("\u2699 Conductor");
|
|
164
|
+
console.log(header);
|
|
165
|
+
if (conductorStatus.running.length > 0) {
|
|
166
|
+
const conductorTable = new Table({
|
|
167
|
+
head: [
|
|
168
|
+
chalk.white("Issue"),
|
|
169
|
+
chalk.white("Status"),
|
|
170
|
+
chalk.white("Title"),
|
|
171
|
+
chalk.white("Runtime")
|
|
172
|
+
],
|
|
173
|
+
style: { head: [], border: [] },
|
|
174
|
+
colWidths: [12, 14, 36, 10]
|
|
175
|
+
});
|
|
176
|
+
conductorStatus.running.forEach((r) => {
|
|
177
|
+
const mins = Math.round(r.runtime / 6e4);
|
|
178
|
+
const statusColor = r.status === "running" ? chalk.green : r.status === "completed" ? chalk.cyan : chalk.red;
|
|
179
|
+
conductorTable.push([
|
|
180
|
+
r.identifier,
|
|
181
|
+
statusColor(r.status),
|
|
182
|
+
r.title.slice(0, 34),
|
|
183
|
+
`${mins}m`
|
|
184
|
+
]);
|
|
185
|
+
});
|
|
186
|
+
console.log(conductorTable.toString());
|
|
187
|
+
} else {
|
|
188
|
+
console.log(chalk.gray(" No agents running"));
|
|
189
|
+
}
|
|
190
|
+
console.log(
|
|
191
|
+
chalk.gray(
|
|
192
|
+
` Completed: ${conductorStatus.completed} Failed: ${conductorStatus.failed} Max: ${conductorStatus.maxConcurrent}`
|
|
193
|
+
)
|
|
194
|
+
);
|
|
195
|
+
console.log();
|
|
196
|
+
} catch {
|
|
197
|
+
}
|
|
198
|
+
}
|
|
153
199
|
db.close();
|
|
154
200
|
if (argv.watch) {
|
|
155
201
|
console.log(
|
|
@@ -3,13 +3,20 @@ import { dirname as __pathDirname } from 'path';
|
|
|
3
3
|
const __filename = __fileURLToPath(import.meta.url);
|
|
4
4
|
const __dirname = __pathDirname(__filename);
|
|
5
5
|
import { Command } from "commander";
|
|
6
|
-
import { execSync } from "child_process";
|
|
7
|
-
import {
|
|
6
|
+
import { execSync, spawn as cpSpawn } from "child_process";
|
|
7
|
+
import {
|
|
8
|
+
existsSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
readdirSync
|
|
13
|
+
} from "fs";
|
|
8
14
|
import { join } from "path";
|
|
9
15
|
import { homedir, tmpdir } from "os";
|
|
10
16
|
import Database from "better-sqlite3";
|
|
11
17
|
import { logger } from "../../core/monitoring/logger.js";
|
|
12
18
|
import { Conductor } from "./orchestrator.js";
|
|
19
|
+
import { getAgentStatusDir } from "./orchestrator.js";
|
|
13
20
|
function getGlobalStorePath() {
|
|
14
21
|
const dir = join(homedir(), ".stackmemory", "conductor");
|
|
15
22
|
if (!existsSync(dir)) {
|
|
@@ -43,6 +50,16 @@ function getGlobalDb() {
|
|
|
43
50
|
`);
|
|
44
51
|
return db;
|
|
45
52
|
}
|
|
53
|
+
function formatElapsed(ms) {
|
|
54
|
+
const seconds = Math.floor(ms / 1e3);
|
|
55
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
56
|
+
const minutes = Math.floor(seconds / 60);
|
|
57
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
58
|
+
const hours = Math.floor(minutes / 60);
|
|
59
|
+
if (hours < 24) return `${hours}h ago`;
|
|
60
|
+
const days = Math.floor(hours / 24);
|
|
61
|
+
return `${days}d ago`;
|
|
62
|
+
}
|
|
46
63
|
function createConductorCommands() {
|
|
47
64
|
const cmd = new Command("conductor");
|
|
48
65
|
cmd.description("Conductor \u2014 autonomous agent orchestration via Linear");
|
|
@@ -263,6 +280,60 @@ function createConductorCommands() {
|
|
|
263
280
|
}
|
|
264
281
|
globalDb.close();
|
|
265
282
|
});
|
|
283
|
+
cmd.command("status").description("Show running agent status table").action(async () => {
|
|
284
|
+
const agentsDir = join(homedir(), ".stackmemory", "conductor", "agents");
|
|
285
|
+
if (!existsSync(agentsDir)) {
|
|
286
|
+
console.log("No agent status files found");
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const entries = readdirSync(agentsDir, { withFileTypes: true });
|
|
290
|
+
const statuses = [];
|
|
291
|
+
for (const entry of entries) {
|
|
292
|
+
if (!entry.isDirectory()) continue;
|
|
293
|
+
const statusPath = join(agentsDir, entry.name, "status.json");
|
|
294
|
+
if (!existsSync(statusPath)) continue;
|
|
295
|
+
try {
|
|
296
|
+
const data = JSON.parse(readFileSync(statusPath, "utf-8"));
|
|
297
|
+
statuses.push(data);
|
|
298
|
+
} catch {
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (statuses.length === 0) {
|
|
302
|
+
console.log("No agent status files found");
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
statuses.sort(
|
|
306
|
+
(a, b) => new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime()
|
|
307
|
+
);
|
|
308
|
+
const header = `${"Issue".padEnd(12)}${"Phase".padEnd(16)}${"Tools".padStart(7)}${"Files".padStart(7)}${"Tokens".padStart(9)} Last Update`;
|
|
309
|
+
console.log(header);
|
|
310
|
+
for (const s of statuses) {
|
|
311
|
+
const elapsed = Date.now() - new Date(s.lastUpdate).getTime();
|
|
312
|
+
const lastUpdate = formatElapsed(elapsed);
|
|
313
|
+
const line = `${s.issue.padEnd(12)}${s.phase.padEnd(16)}${String(s.toolCalls).padStart(7)}${String(s.filesModified).padStart(7)}${String(s.tokensUsed).padStart(9)} ${lastUpdate}`;
|
|
314
|
+
console.log(line);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
cmd.command("logs").description("Tail agent output log").argument("<issue-id>", "Issue identifier (e.g., STA-499)").option("-f, --follow", "Follow the log (tail -f)", false).option("-n, --lines <n>", "Number of lines to show", "50").action(async (issueId, options) => {
|
|
318
|
+
const logPath = join(getAgentStatusDir(issueId), "output.log");
|
|
319
|
+
if (!existsSync(logPath)) {
|
|
320
|
+
console.error(`No log file found for ${issueId} at ${logPath}`);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const lines = parseInt(options.lines, 10);
|
|
324
|
+
const args = options.follow ? ["-f", "-n", String(lines), logPath] : ["-n", String(lines), logPath];
|
|
325
|
+
const tail = cpSpawn("tail", args, { stdio: "inherit" });
|
|
326
|
+
await new Promise((resolve) => {
|
|
327
|
+
tail.on("close", () => {
|
|
328
|
+
resolve();
|
|
329
|
+
});
|
|
330
|
+
const forward = () => {
|
|
331
|
+
tail.kill("SIGTERM");
|
|
332
|
+
};
|
|
333
|
+
process.on("SIGINT", forward);
|
|
334
|
+
process.on("SIGTERM", forward);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
266
337
|
cmd.command("start").description("Start the orchestrator daemon").option("--team <id>", "Linear team ID").option(
|
|
267
338
|
"--states <states>",
|
|
268
339
|
"Comma-separated issue states to pick up",
|
|
@@ -275,7 +346,11 @@ function createConductorCommands() {
|
|
|
275
346
|
"--in-review <state>",
|
|
276
347
|
"State name for completed review",
|
|
277
348
|
"In Review"
|
|
278
|
-
).option("--poll <ms>", "Polling interval in milliseconds", "30000").option("--concurrency <n>", "Max concurrent agents", "3").option("--workspace-root <path>", "Workspace root directory").option("--repo <path>", "Git repo root for worktrees", process.cwd()).option("--branch <name>", "Base branch for worktrees", "main").option("--retries <n>", "Max retries per issue", "1").option("--turn-timeout <ms>", "Agent turn timeout in ms", "3600000").
|
|
349
|
+
).option("--poll <ms>", "Polling interval in milliseconds", "30000").option("--concurrency <n>", "Max concurrent agents", "3").option("--workspace-root <path>", "Workspace root directory").option("--repo <path>", "Git repo root for worktrees", process.cwd()).option("--branch <name>", "Base branch for worktrees", "main").option("--retries <n>", "Max retries per issue", "1").option("--turn-timeout <ms>", "Agent turn timeout in ms", "3600000").option(
|
|
350
|
+
"--mode <mode>",
|
|
351
|
+
'Agent mode: "cli" (claude -p, session auth) or "adapter" (JSON-RPC, API key)',
|
|
352
|
+
"cli"
|
|
353
|
+
).action(async (options) => {
|
|
279
354
|
const conductor = new Conductor({
|
|
280
355
|
teamId: options.team,
|
|
281
356
|
activeStates: options.states.split(",").map((s) => s.trim()),
|
|
@@ -287,12 +362,14 @@ function createConductorCommands() {
|
|
|
287
362
|
repoRoot: options.repo,
|
|
288
363
|
baseBranch: options.branch,
|
|
289
364
|
maxRetries: parseInt(options.retries, 10),
|
|
290
|
-
turnTimeoutMs: parseInt(options.turnTimeout, 10)
|
|
365
|
+
turnTimeoutMs: parseInt(options.turnTimeout, 10),
|
|
366
|
+
agentMode: options.mode === "adapter" ? "adapter" : "cli"
|
|
291
367
|
});
|
|
292
368
|
await conductor.start();
|
|
293
369
|
});
|
|
294
370
|
return cmd;
|
|
295
371
|
}
|
|
296
372
|
export {
|
|
297
|
-
createConductorCommands
|
|
373
|
+
createConductorCommands,
|
|
374
|
+
formatElapsed
|
|
298
375
|
};
|