@stackmemoryai/stackmemory 1.5.8 → 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 +251 -110
- package/dist/src/cli/commands/orchestrator.js +82 -11
- 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
|
@@ -17,7 +17,8 @@ import Database from "better-sqlite3";
|
|
|
17
17
|
import { logger } from "../../core/monitoring/logger.js";
|
|
18
18
|
import { Conductor } from "./orchestrator.js";
|
|
19
19
|
import {
|
|
20
|
-
getAgentStatusDir
|
|
20
|
+
getAgentStatusDir,
|
|
21
|
+
getOutcomesLogPath
|
|
21
22
|
} from "./orchestrator.js";
|
|
22
23
|
function getGlobalStorePath() {
|
|
23
24
|
const dir = join(homedir(), ".stackmemory", "conductor");
|
|
@@ -75,6 +76,8 @@ function budgetBar(pct, width = 30) {
|
|
|
75
76
|
const rst = "\x1B[0m";
|
|
76
77
|
return `${color}${"\u2588".repeat(filled)}${dim}${"\u2591".repeat(empty)}${rst} ${String(pct).padStart(3)}%`;
|
|
77
78
|
}
|
|
79
|
+
const STALE_UI_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
80
|
+
const STALE_FINALIZE_THRESHOLD_MS = 60 * 60 * 1e3;
|
|
78
81
|
const c = {
|
|
79
82
|
r: "\x1B[0m",
|
|
80
83
|
// reset
|
|
@@ -170,6 +173,32 @@ function progressBar(pct, width) {
|
|
|
170
173
|
const col = pct >= 90 ? c.green : pct >= 50 ? c.yellow : c.cyan;
|
|
171
174
|
return `${col}${"\u2501".repeat(filled)}${c.d}${"\u254C".repeat(empty)}${c.r}`;
|
|
172
175
|
}
|
|
176
|
+
function scanAgentStatuses() {
|
|
177
|
+
const agentsDir2 = join(homedir(), ".stackmemory", "conductor", "agents");
|
|
178
|
+
if (!existsSync(agentsDir2)) return [];
|
|
179
|
+
const entries = readdirSync(agentsDir2, { withFileTypes: true });
|
|
180
|
+
const statuses = [];
|
|
181
|
+
for (const entry of entries) {
|
|
182
|
+
if (!entry.isDirectory()) continue;
|
|
183
|
+
const statusPath = join(agentsDir2, entry.name, "status.json");
|
|
184
|
+
if (!existsSync(statusPath)) continue;
|
|
185
|
+
try {
|
|
186
|
+
const data = JSON.parse(readFileSync(statusPath, "utf-8"));
|
|
187
|
+
statuses.push({ ...data, dir: entry.name });
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
statuses.sort(
|
|
192
|
+
(a, b) => new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime()
|
|
193
|
+
);
|
|
194
|
+
return statuses;
|
|
195
|
+
}
|
|
196
|
+
function enrichStatus(s) {
|
|
197
|
+
const elapsed = Date.now() - new Date(s.lastUpdate).getTime();
|
|
198
|
+
const alive = isProcessAlive(s.pid);
|
|
199
|
+
const stale = alive && elapsed > STALE_UI_THRESHOLD_MS;
|
|
200
|
+
return { elapsed, alive, stale };
|
|
201
|
+
}
|
|
173
202
|
function fmtMinutes(m) {
|
|
174
203
|
if (m < 0) return "N/A";
|
|
175
204
|
if (m >= 60) return `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
@@ -186,24 +215,20 @@ function printUsageSummary(u) {
|
|
|
186
215
|
const mins5x = u.minutesRemaining5x ?? -1;
|
|
187
216
|
const mins20x = u.minutesRemaining20x ?? -1;
|
|
188
217
|
const cacheHitRate = u.cacheHitRate || 0;
|
|
189
|
-
|
|
190
|
-
const d2 = "\x1B[2m";
|
|
191
|
-
const w = "\x1B[37m";
|
|
192
|
-
const r2 = "\x1B[0m";
|
|
193
|
-
console.log(`${b}Token Usage${r2}`);
|
|
218
|
+
console.log(`${c.b}Token Usage${c.r}`);
|
|
194
219
|
console.log(
|
|
195
|
-
` Input ${
|
|
220
|
+
` Input ${c.white}${fmtTokens(inputTokens)}${c.r} ${c.d}|${c.r} Output ${c.white}${fmtTokens(outputTokens)}${c.r} ${c.d}|${c.r} Total ${c.white}${fmtTokens(totalTokens)}${c.r}`
|
|
196
221
|
);
|
|
197
222
|
console.log(
|
|
198
|
-
` Rate ${
|
|
223
|
+
` Rate ${c.white}${fmtTokens(tokensPerMin)}/min${c.r} ${c.d}|${c.r} Messages ${c.white}${estMessages}${c.r} ${c.d}|${c.r} Cache hit ${c.white}${cacheHitRate}%${c.r}`
|
|
199
224
|
);
|
|
200
225
|
console.log("");
|
|
201
|
-
console.log(`${b}Budget (Max plan, 5h window)${
|
|
226
|
+
console.log(`${c.b}Budget (Max plan, 5h window)${c.r}`);
|
|
202
227
|
console.log(
|
|
203
|
-
` 5x (225 msgs) ${budgetBar(budgetPct5x)} ${
|
|
228
|
+
` 5x (225 msgs) ${budgetBar(budgetPct5x)} ${c.d}~${fmtMinutes(mins5x)} left${c.r}`
|
|
204
229
|
);
|
|
205
230
|
console.log(
|
|
206
|
-
` 20x (900 msgs) ${budgetBar(budgetPct20x)} ${
|
|
231
|
+
` 20x (900 msgs) ${budgetBar(budgetPct20x)} ${c.d}~${fmtMinutes(mins20x)} left${c.r}`
|
|
207
232
|
);
|
|
208
233
|
}
|
|
209
234
|
function createConductorCommands() {
|
|
@@ -323,14 +348,14 @@ function createConductorCommands() {
|
|
|
323
348
|
const risks = anchors.filter((a) => a.type === "RISK");
|
|
324
349
|
if (decisions.length > 0) {
|
|
325
350
|
lines.push("", "### Decisions");
|
|
326
|
-
for (const
|
|
327
|
-
lines.push(`- ${
|
|
351
|
+
for (const d of decisions.slice(0, 10)) {
|
|
352
|
+
lines.push(`- ${d.text}`);
|
|
328
353
|
}
|
|
329
354
|
}
|
|
330
355
|
if (risks.length > 0) {
|
|
331
356
|
lines.push("", "### Risks");
|
|
332
|
-
for (const
|
|
333
|
-
lines.push(`- ${
|
|
357
|
+
for (const r of risks.slice(0, 5)) {
|
|
358
|
+
lines.push(`- ${r.text}`);
|
|
334
359
|
}
|
|
335
360
|
}
|
|
336
361
|
} catch {
|
|
@@ -414,48 +439,28 @@ function createConductorCommands() {
|
|
|
414
439
|
}
|
|
415
440
|
console.log(`Found ${results.length} result(s) for "${query}":
|
|
416
441
|
`);
|
|
417
|
-
for (const
|
|
418
|
-
const date = new Date(
|
|
442
|
+
for (const r of results) {
|
|
443
|
+
const date = new Date(r.captured_at * 1e3).toISOString().slice(0, 16);
|
|
419
444
|
console.log(
|
|
420
|
-
` ${
|
|
445
|
+
` ${r.issue_id} [${r.context_type}] attempt ${r.attempt} (${date})`
|
|
421
446
|
);
|
|
422
|
-
if (
|
|
423
|
-
const snippet =
|
|
447
|
+
if (r.summary) {
|
|
448
|
+
const snippet = r.summary.slice(0, 120).replace(/\n/g, " ");
|
|
424
449
|
console.log(` ${snippet}`);
|
|
425
450
|
}
|
|
426
451
|
}
|
|
427
452
|
globalDb.close();
|
|
428
453
|
});
|
|
429
454
|
cmd.command("status").description("Show running agent status table").action(async () => {
|
|
430
|
-
const
|
|
431
|
-
if (!existsSync(agentsDir)) {
|
|
432
|
-
console.log("No agent status files found");
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
const entries = readdirSync(agentsDir, { withFileTypes: true });
|
|
436
|
-
const statuses = [];
|
|
437
|
-
for (const entry of entries) {
|
|
438
|
-
if (!entry.isDirectory()) continue;
|
|
439
|
-
const statusPath = join(agentsDir, entry.name, "status.json");
|
|
440
|
-
if (!existsSync(statusPath)) continue;
|
|
441
|
-
try {
|
|
442
|
-
const data = JSON.parse(readFileSync(statusPath, "utf-8"));
|
|
443
|
-
statuses.push(data);
|
|
444
|
-
} catch {
|
|
445
|
-
}
|
|
446
|
-
}
|
|
455
|
+
const statuses = scanAgentStatuses();
|
|
447
456
|
if (statuses.length === 0) {
|
|
448
457
|
console.log("No agent status files found");
|
|
449
458
|
return;
|
|
450
459
|
}
|
|
451
|
-
statuses.
|
|
452
|
-
|
|
453
|
-
);
|
|
454
|
-
const
|
|
455
|
-
const stalled = statuses.filter(
|
|
456
|
-
(s) => isProcessAlive(s.pid) && Date.now() - new Date(s.lastUpdate).getTime() > 5 * 60 * 1e3
|
|
457
|
-
);
|
|
458
|
-
const dead = statuses.filter((s) => !isProcessAlive(s.pid));
|
|
460
|
+
const enriched = statuses.map((s) => ({ ...s, ...enrichStatus(s) }));
|
|
461
|
+
const active = enriched.filter((s) => s.alive);
|
|
462
|
+
const stalled = enriched.filter((s) => s.stale);
|
|
463
|
+
const dead = enriched.filter((s) => !s.alive);
|
|
459
464
|
const healthy = active.length - stalled.length;
|
|
460
465
|
const parts = [];
|
|
461
466
|
if (healthy > 0) parts.push(`${c.green}\u25CF ${healthy}${c.r}`);
|
|
@@ -467,21 +472,18 @@ function createConductorCommands() {
|
|
|
467
472
|
`);
|
|
468
473
|
const cols = (process.stdout.columns || 80) >= 90 ? 2 : 1;
|
|
469
474
|
const rows = [];
|
|
470
|
-
for (const s of
|
|
471
|
-
const elapsed = Date.now() - new Date(s.lastUpdate).getTime();
|
|
472
|
-
const staleFlag = elapsed > 5 * 60 * 1e3;
|
|
473
|
-
const alive = isProcessAlive(s.pid);
|
|
475
|
+
for (const s of enriched) {
|
|
474
476
|
const { icon, color, pct, label } = phaseProgress(
|
|
475
477
|
s.phase,
|
|
476
478
|
s.toolCalls,
|
|
477
|
-
|
|
478
|
-
alive
|
|
479
|
+
s.stale,
|
|
480
|
+
s.alive
|
|
479
481
|
);
|
|
480
482
|
const bar = progressBar(pct, 8);
|
|
481
|
-
const timeColor = !alive ? c.red :
|
|
483
|
+
const timeColor = !s.alive ? c.red : s.stale ? c.orange : c.gray;
|
|
482
484
|
const cell = [
|
|
483
485
|
`${color}${icon}${c.r} ${c.b}${s.issue}${c.r} ${color}${label}${c.r}`,
|
|
484
|
-
` ${bar} ${c.d}${pct}%${c.r} ${c.gray}${s.toolCalls}t ${s.filesModified}f${c.r} ${timeColor}${formatElapsed(elapsed)}${c.r}`
|
|
486
|
+
` ${bar} ${c.d}${pct}%${c.r} ${c.gray}${s.toolCalls}t ${s.filesModified}f${c.r} ${timeColor}${formatElapsed(s.elapsed)}${c.r}`
|
|
485
487
|
];
|
|
486
488
|
rows.push(cell);
|
|
487
489
|
}
|
|
@@ -517,27 +519,9 @@ function createConductorCommands() {
|
|
|
517
519
|
console.log("");
|
|
518
520
|
});
|
|
519
521
|
cmd.command("finalize").description("Clean up completed/dead agents that conductor missed").option("--dry-run", "Show what would be done without doing it", false).action(async (options) => {
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
return;
|
|
524
|
-
}
|
|
525
|
-
const entries = readdirSync(agentsDir, { withFileTypes: true });
|
|
526
|
-
const statuses = [];
|
|
527
|
-
for (const entry of entries) {
|
|
528
|
-
if (!entry.isDirectory()) continue;
|
|
529
|
-
const statusPath = join(agentsDir, entry.name, "status.json");
|
|
530
|
-
if (!existsSync(statusPath)) continue;
|
|
531
|
-
try {
|
|
532
|
-
const data = JSON.parse(readFileSync(statusPath, "utf-8"));
|
|
533
|
-
statuses.push({ ...data, dir: entry.name });
|
|
534
|
-
} catch {
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
const needsFinalize = statuses.filter((s) => {
|
|
538
|
-
const alive = isProcessAlive(s.pid);
|
|
539
|
-
const elapsed = Date.now() - new Date(s.lastUpdate).getTime();
|
|
540
|
-
return !alive || elapsed > 60 * 60 * 1e3;
|
|
522
|
+
const statuses = scanAgentStatuses();
|
|
523
|
+
const needsFinalize = statuses.map((s) => ({ ...s, ...enrichStatus(s) })).filter((s) => {
|
|
524
|
+
return !s.alive || s.elapsed > STALE_FINALIZE_THRESHOLD_MS;
|
|
541
525
|
});
|
|
542
526
|
if (needsFinalize.length === 0) {
|
|
543
527
|
console.log(
|
|
@@ -551,9 +535,7 @@ function createConductorCommands() {
|
|
|
551
535
|
`
|
|
552
536
|
);
|
|
553
537
|
for (const s of needsFinalize) {
|
|
554
|
-
const
|
|
555
|
-
const elapsed = Date.now() - new Date(s.lastUpdate).getTime();
|
|
556
|
-
const elapsedStr = formatElapsed(elapsed).replace(" ago", "");
|
|
538
|
+
const elapsedStr = formatElapsed(s.elapsed).replace(" ago", "");
|
|
557
539
|
let hasCommits = false;
|
|
558
540
|
if (s.workspacePath && existsSync(s.workspacePath)) {
|
|
559
541
|
try {
|
|
@@ -566,11 +548,11 @@ function createConductorCommands() {
|
|
|
566
548
|
} catch {
|
|
567
549
|
}
|
|
568
550
|
}
|
|
569
|
-
const statusIcon = !alive ? `${c.red}\u2717 dead${c.r}` : `${c.orange}\u23F8 stalled ${elapsedStr}${c.r}`;
|
|
551
|
+
const statusIcon = !s.alive ? `${c.red}\u2717 dead${c.r}` : `${c.orange}\u23F8 stalled ${elapsedStr}${c.r}`;
|
|
570
552
|
const commitStatus = hasCommits ? `${c.green}has commits \u2192 In Review${c.r}` : `${c.gray}no commits \u2192 mark failed${c.r}`;
|
|
571
553
|
console.log(` ${c.b}${s.issue}${c.r} ${statusIcon} ${commitStatus}`);
|
|
572
554
|
if (options.dryRun) continue;
|
|
573
|
-
if (alive) {
|
|
555
|
+
if (s.alive) {
|
|
574
556
|
try {
|
|
575
557
|
process.kill(s.pid, "SIGTERM");
|
|
576
558
|
console.log(` ${c.gray}Sent SIGTERM to pid ${s.pid}${c.r}`);
|
|
@@ -629,6 +611,189 @@ function createConductorCommands() {
|
|
|
629
611
|
process.on("SIGTERM", forward);
|
|
630
612
|
});
|
|
631
613
|
});
|
|
614
|
+
cmd.command("learn").description(
|
|
615
|
+
"Analyze agent outcomes and generate improved prompt templates"
|
|
616
|
+
).option("--last <n>", "Analyze last N outcomes (default: all)", "0").option("--failures-only", "Only analyze failures", false).option("--export", "Export analysis as JSON", false).action(async (options) => {
|
|
617
|
+
const logPath = getOutcomesLogPath();
|
|
618
|
+
if (!existsSync(logPath)) {
|
|
619
|
+
console.log(
|
|
620
|
+
`${c.yellow}No outcomes log found.${c.r} Run conductor to generate data.`
|
|
621
|
+
);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const raw = readFileSync(logPath, "utf-8").trim().split("\n").filter((l) => l.length > 0);
|
|
625
|
+
let outcomes = raw.map(
|
|
626
|
+
(line) => JSON.parse(line)
|
|
627
|
+
);
|
|
628
|
+
if (options.failuresOnly) {
|
|
629
|
+
outcomes = outcomes.filter((o) => o.outcome === "failure");
|
|
630
|
+
}
|
|
631
|
+
const lastN = parseInt(options.last, 10);
|
|
632
|
+
if (lastN > 0) {
|
|
633
|
+
outcomes = outcomes.slice(-lastN);
|
|
634
|
+
}
|
|
635
|
+
if (outcomes.length === 0) {
|
|
636
|
+
console.log(`${c.gray}No matching outcomes to analyze.${c.r}`);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
const total = outcomes.length;
|
|
640
|
+
const successes = outcomes.filter((o) => o.outcome === "success").length;
|
|
641
|
+
const failures = outcomes.filter((o) => o.outcome === "failure").length;
|
|
642
|
+
const successRate = Math.round(successes / total * 100);
|
|
643
|
+
const avgTokens = Math.round(
|
|
644
|
+
outcomes.reduce((s, o) => s + o.tokensUsed, 0) / total
|
|
645
|
+
);
|
|
646
|
+
const avgDuration = Math.round(
|
|
647
|
+
outcomes.reduce((s, o) => s + o.durationMs, 0) / total / 6e4
|
|
648
|
+
);
|
|
649
|
+
const avgTools = Math.round(
|
|
650
|
+
outcomes.reduce((s, o) => s + o.toolCalls, 0) / total
|
|
651
|
+
);
|
|
652
|
+
const failPhases = {};
|
|
653
|
+
for (const o of outcomes.filter((o2) => o2.outcome === "failure")) {
|
|
654
|
+
failPhases[o.phase] = (failPhases[o.phase] || 0) + 1;
|
|
655
|
+
}
|
|
656
|
+
const retries = outcomes.filter((o) => o.attempt > 1);
|
|
657
|
+
const retrySuccessRate = retries.length > 0 ? Math.round(
|
|
658
|
+
retries.filter((o) => o.outcome === "success").length / retries.length * 100
|
|
659
|
+
) : 0;
|
|
660
|
+
const errorPatterns = {};
|
|
661
|
+
for (const o of outcomes.filter(
|
|
662
|
+
(o2) => o2.outcome === "failure" && o2.errorTail
|
|
663
|
+
)) {
|
|
664
|
+
const tail = o.errorTail;
|
|
665
|
+
if (tail.includes("lint"))
|
|
666
|
+
errorPatterns["lint_failure"] = (errorPatterns["lint_failure"] || 0) + 1;
|
|
667
|
+
else if (tail.includes("test"))
|
|
668
|
+
errorPatterns["test_failure"] = (errorPatterns["test_failure"] || 0) + 1;
|
|
669
|
+
else if (tail.includes("timeout") || tail.includes("timed out"))
|
|
670
|
+
errorPatterns["timeout"] = (errorPatterns["timeout"] || 0) + 1;
|
|
671
|
+
else if (tail.includes("conflict"))
|
|
672
|
+
errorPatterns["git_conflict"] = (errorPatterns["git_conflict"] || 0) + 1;
|
|
673
|
+
else if (tail.includes("429") || tail.includes("rate"))
|
|
674
|
+
errorPatterns["rate_limit"] = (errorPatterns["rate_limit"] || 0) + 1;
|
|
675
|
+
else errorPatterns["unknown"] = (errorPatterns["unknown"] || 0) + 1;
|
|
676
|
+
}
|
|
677
|
+
if (options.export) {
|
|
678
|
+
const analysis = {
|
|
679
|
+
total,
|
|
680
|
+
successes,
|
|
681
|
+
failures,
|
|
682
|
+
successRate,
|
|
683
|
+
avgTokens,
|
|
684
|
+
avgDurationMin: avgDuration,
|
|
685
|
+
avgToolCalls: avgTools,
|
|
686
|
+
failPhases,
|
|
687
|
+
retrySuccessRate,
|
|
688
|
+
errorPatterns,
|
|
689
|
+
outcomes
|
|
690
|
+
};
|
|
691
|
+
console.log(JSON.stringify(analysis, null, 2));
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
console.log(`
|
|
695
|
+
${c.b}${c.purple}Conductor Learning Report${c.r}
|
|
696
|
+
`);
|
|
697
|
+
const rateColor = successRate >= 80 ? c.green : successRate >= 50 ? c.yellow : c.red;
|
|
698
|
+
console.log(
|
|
699
|
+
` ${c.b}Outcomes${c.r} ${c.white}${total}${c.r} total ${c.green}${successes}${c.r} success ${c.red}${failures}${c.r} failed ${rateColor}${successRate}%${c.r} success rate`
|
|
700
|
+
);
|
|
701
|
+
console.log(
|
|
702
|
+
` ${c.b}Averages${c.r} ${c.white}${avgDuration}m${c.r} duration ${c.white}${fmtTokens(avgTokens)}${c.r} tokens ${c.white}${avgTools}${c.r} tool calls`
|
|
703
|
+
);
|
|
704
|
+
if (retries.length > 0) {
|
|
705
|
+
console.log(
|
|
706
|
+
` ${c.b}Retries${c.r} ${c.white}${retries.length}${c.r} attempts ${c.white}${retrySuccessRate}%${c.r} retry success rate`
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
if (failures > 0) {
|
|
710
|
+
console.log(`
|
|
711
|
+
${c.b}Failure Phases${c.r}`);
|
|
712
|
+
const sorted = Object.entries(failPhases).sort((a, b) => b[1] - a[1]);
|
|
713
|
+
for (const [phase, count] of sorted) {
|
|
714
|
+
const pct = Math.round(count / failures * 100);
|
|
715
|
+
const bar = progressBar(pct, 10);
|
|
716
|
+
console.log(
|
|
717
|
+
` ${phaseIcon[phase] || "\u25CB"} ${phase.padEnd(14)} ${bar} ${c.white}${count}${c.r} ${c.gray}(${pct}%)${c.r}`
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
if (Object.keys(errorPatterns).length > 0) {
|
|
722
|
+
console.log(`
|
|
723
|
+
${c.b}Error Patterns${c.r}`);
|
|
724
|
+
const sorted = Object.entries(errorPatterns).sort(
|
|
725
|
+
(a, b) => b[1] - a[1]
|
|
726
|
+
);
|
|
727
|
+
for (const [pattern, count] of sorted) {
|
|
728
|
+
console.log(
|
|
729
|
+
` ${c.red}\u25CF${c.r} ${pattern.padEnd(16)} ${c.white}${count}${c.r}`
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
console.log(`
|
|
734
|
+
${c.b}Recommendations${c.r}`);
|
|
735
|
+
const recs = [];
|
|
736
|
+
if (errorPatterns["lint_failure"] > 0) {
|
|
737
|
+
recs.push(
|
|
738
|
+
"Add explicit lint rules to prompt template (ESLint conventions, import style)"
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
if (errorPatterns["test_failure"] > 0) {
|
|
742
|
+
recs.push(
|
|
743
|
+
'Add "run tests before committing" emphasis, include test command in prompt'
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
if (errorPatterns["timeout"] > 0) {
|
|
747
|
+
recs.push(
|
|
748
|
+
"Reduce scope per issue or increase turnTimeoutMs in conductor config"
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
if (failPhases["implementing"] > failures * 0.5) {
|
|
752
|
+
recs.push(
|
|
753
|
+
"Agents stall during implementation \u2014 add examples or break issues smaller"
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
if (failPhases["reading"] > 0) {
|
|
757
|
+
recs.push(
|
|
758
|
+
"Agents fail during reading \u2014 improve issue descriptions or add context pointers"
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
if (retrySuccessRate < 30 && retries.length > 2) {
|
|
762
|
+
recs.push(
|
|
763
|
+
"Low retry success \u2014 consider better prior-attempt context injection"
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
if (successRate >= 80) {
|
|
767
|
+
recs.push(
|
|
768
|
+
"High success rate \u2014 current prompt template is working well"
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
if (recs.length === 0) {
|
|
772
|
+
recs.push("Collect more data for actionable recommendations");
|
|
773
|
+
}
|
|
774
|
+
for (const rec of recs) {
|
|
775
|
+
console.log(` ${c.cyan}\u2192${c.r} ${rec}`);
|
|
776
|
+
}
|
|
777
|
+
const templatePath = join(
|
|
778
|
+
homedir(),
|
|
779
|
+
".stackmemory",
|
|
780
|
+
"conductor",
|
|
781
|
+
"prompt-template.md"
|
|
782
|
+
);
|
|
783
|
+
if (!existsSync(templatePath)) {
|
|
784
|
+
console.log(
|
|
785
|
+
`
|
|
786
|
+
${c.d}Tip: Create ${templatePath} to customize agent prompts.${c.r}`
|
|
787
|
+
);
|
|
788
|
+
console.log(
|
|
789
|
+
` ${c.d}Variables: {{ISSUE_ID}} {{TITLE}} {{DESCRIPTION}} {{LABELS}} {{PRIORITY}} {{ATTEMPT}} {{PRIOR_CONTEXT}}${c.r}`
|
|
790
|
+
);
|
|
791
|
+
} else {
|
|
792
|
+
console.log(`
|
|
793
|
+
${c.d}Using custom template: ${templatePath}${c.r}`);
|
|
794
|
+
}
|
|
795
|
+
console.log("");
|
|
796
|
+
});
|
|
632
797
|
cmd.command("usage").description("Show token usage, budget, and time-to-exhaustion").option("--json", "Output as JSON", false).option("--scan", "Scan Claude Code JSONL logs for historical data", false).action(async (options) => {
|
|
633
798
|
const statusPath = join(
|
|
634
799
|
process.cwd(),
|
|
@@ -721,27 +886,7 @@ function createConductorCommands() {
|
|
|
721
886
|
let refreshInterval = interval;
|
|
722
887
|
let phaseFilter = options.phase || null;
|
|
723
888
|
function readStatuses() {
|
|
724
|
-
const
|
|
725
|
-
homedir(),
|
|
726
|
-
".stackmemory",
|
|
727
|
-
"conductor",
|
|
728
|
-
"agents"
|
|
729
|
-
);
|
|
730
|
-
if (!existsSync(agentsDir)) return [];
|
|
731
|
-
const entries = readdirSync(agentsDir, { withFileTypes: true });
|
|
732
|
-
const statuses = [];
|
|
733
|
-
for (const entry of entries) {
|
|
734
|
-
if (!entry.isDirectory()) continue;
|
|
735
|
-
const statusPath = join(agentsDir, entry.name, "status.json");
|
|
736
|
-
if (!existsSync(statusPath)) continue;
|
|
737
|
-
try {
|
|
738
|
-
statuses.push(JSON.parse(readFileSync(statusPath, "utf-8")));
|
|
739
|
-
} catch {
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
statuses.sort(
|
|
743
|
-
(a, b) => new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime()
|
|
744
|
-
);
|
|
889
|
+
const statuses = scanAgentStatuses();
|
|
745
890
|
if (phaseFilter) {
|
|
746
891
|
return statuses.filter((s) => s.phase === phaseFilter);
|
|
747
892
|
}
|
|
@@ -754,9 +899,7 @@ function createConductorCommands() {
|
|
|
754
899
|
return;
|
|
755
900
|
}
|
|
756
901
|
for (const s of statuses) {
|
|
757
|
-
const elapsed
|
|
758
|
-
const stale = elapsed > 5 * 60 * 1e3;
|
|
759
|
-
const alive = isProcessAlive(s.pid);
|
|
902
|
+
const { elapsed, alive, stale } = enrichStatus(s);
|
|
760
903
|
const prog = phaseProgress(s.phase, s.toolCalls, stale, alive);
|
|
761
904
|
const bar = progressBar(prog.pct, 10);
|
|
762
905
|
const timeColor = !alive ? c.red : stale ? c.orange : c.gray;
|
|
@@ -788,9 +931,7 @@ function createConductorCommands() {
|
|
|
788
931
|
return;
|
|
789
932
|
}
|
|
790
933
|
for (const s of statuses) {
|
|
791
|
-
const elapsed
|
|
792
|
-
const stale = elapsed > 5 * 60 * 1e3;
|
|
793
|
-
const alive = isProcessAlive(s.pid);
|
|
934
|
+
const { elapsed, alive, stale } = enrichStatus(s);
|
|
794
935
|
const prog = phaseProgress(s.phase, s.toolCalls, stale, alive);
|
|
795
936
|
console.log(
|
|
796
937
|
` ${prog.color}${prog.icon}${c.r} ${c.b}${s.issue}${c.r} ${prog.color}${prog.label}${c.r} ${c.gray}${formatElapsed(elapsed)}${c.r}`
|
|
@@ -813,10 +954,10 @@ function createConductorCommands() {
|
|
|
813
954
|
console.log("");
|
|
814
955
|
}
|
|
815
956
|
}
|
|
957
|
+
const cachedConductor = new Conductor({ repoRoot: process.cwd() });
|
|
816
958
|
async function getUsage() {
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
return conductor.getUsageSummary();
|
|
959
|
+
await cachedConductor.scanUsageLogs();
|
|
960
|
+
return cachedConductor.getUsageSummary();
|
|
820
961
|
}
|
|
821
962
|
async function render() {
|
|
822
963
|
process.stdout.write("\x1B[2J\x1B[H");
|
|
@@ -867,7 +1008,7 @@ function createConductorCommands() {
|
|
|
867
1008
|
}
|
|
868
1009
|
console.log("");
|
|
869
1010
|
console.log(
|
|
870
|
-
`${d}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${r}`
|
|
1011
|
+
`${c.d}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${c.r}`
|
|
871
1012
|
);
|
|
872
1013
|
}
|
|
873
1014
|
await render();
|
|
@@ -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,
|
|
@@ -28,6 +29,14 @@ import {
|
|
|
28
29
|
} from "../../core/worktree/preflight.js";
|
|
29
30
|
import { ContextCapture } from "../../core/worktree/capture.js";
|
|
30
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
|
+
}
|
|
31
40
|
function findPackageRoot() {
|
|
32
41
|
const currentFile = fileURLToPath(import.meta.url);
|
|
33
42
|
let dir = dirname(currentFile);
|
|
@@ -635,6 +644,18 @@ class Conductor {
|
|
|
635
644
|
await this.runAgent(issue, run);
|
|
636
645
|
run.status = "completed";
|
|
637
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
|
+
});
|
|
638
659
|
await this.runHook(
|
|
639
660
|
"after-run",
|
|
640
661
|
run.workspacePath,
|
|
@@ -1271,7 +1292,32 @@ class Conductor {
|
|
|
1271
1292
|
}
|
|
1272
1293
|
}
|
|
1273
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
|
+
*/
|
|
1274
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
|
+
}
|
|
1275
1321
|
const lines = [
|
|
1276
1322
|
`You are working on Linear issue ${issue.identifier}: ${issue.title}`,
|
|
1277
1323
|
""
|
|
@@ -1279,17 +1325,12 @@ class Conductor {
|
|
|
1279
1325
|
if (issue.description) {
|
|
1280
1326
|
lines.push("## Description", "", issue.description, "");
|
|
1281
1327
|
}
|
|
1282
|
-
if (
|
|
1283
|
-
lines.push(`Labels: ${
|
|
1328
|
+
if (labels) {
|
|
1329
|
+
lines.push(`Labels: ${labels}`);
|
|
1284
1330
|
}
|
|
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
|
-
);
|
|
1331
|
+
lines.push(`Priority: ${priority}`);
|
|
1332
|
+
if (priorContext) {
|
|
1333
|
+
lines.push("", priorContext);
|
|
1293
1334
|
}
|
|
1294
1335
|
lines.push(
|
|
1295
1336
|
"",
|
|
@@ -1485,6 +1526,35 @@ class Conductor {
|
|
|
1485
1526
|
hasCommits = log.trim().length > 0;
|
|
1486
1527
|
} catch {
|
|
1487
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
|
+
});
|
|
1488
1558
|
if (hasCommits) {
|
|
1489
1559
|
logger.info("Stale agent had commits, transitioning to In Review", {
|
|
1490
1560
|
identifier: run.issue.identifier
|
|
@@ -1567,5 +1637,6 @@ class Conductor {
|
|
|
1567
1637
|
}
|
|
1568
1638
|
export {
|
|
1569
1639
|
Conductor,
|
|
1570
|
-
getAgentStatusDir
|
|
1640
|
+
getAgentStatusDir,
|
|
1641
|
+
getOutcomesLogPath
|
|
1571
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",
|