@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
|
@@ -16,7 +16,10 @@ import { homedir, tmpdir } from "os";
|
|
|
16
16
|
import Database from "better-sqlite3";
|
|
17
17
|
import { logger } from "../../core/monitoring/logger.js";
|
|
18
18
|
import { Conductor } from "./orchestrator.js";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
getAgentStatusDir,
|
|
21
|
+
getOutcomesLogPath
|
|
22
|
+
} from "./orchestrator.js";
|
|
20
23
|
function getGlobalStorePath() {
|
|
21
24
|
const dir = join(homedir(), ".stackmemory", "conductor");
|
|
22
25
|
if (!existsSync(dir)) {
|
|
@@ -73,6 +76,129 @@ function budgetBar(pct, width = 30) {
|
|
|
73
76
|
const rst = "\x1B[0m";
|
|
74
77
|
return `${color}${"\u2588".repeat(filled)}${dim}${"\u2591".repeat(empty)}${rst} ${String(pct).padStart(3)}%`;
|
|
75
78
|
}
|
|
79
|
+
const STALE_UI_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
80
|
+
const STALE_FINALIZE_THRESHOLD_MS = 60 * 60 * 1e3;
|
|
81
|
+
const c = {
|
|
82
|
+
r: "\x1B[0m",
|
|
83
|
+
// reset
|
|
84
|
+
b: "\x1B[1m",
|
|
85
|
+
// bold
|
|
86
|
+
d: "\x1B[2m",
|
|
87
|
+
// dim
|
|
88
|
+
i: "\x1B[3m",
|
|
89
|
+
// italic
|
|
90
|
+
u: "\x1B[4m",
|
|
91
|
+
// underline
|
|
92
|
+
// Linear-inspired palette
|
|
93
|
+
purple: "\x1B[38;5;141m",
|
|
94
|
+
blue: "\x1B[38;5;75m",
|
|
95
|
+
cyan: "\x1B[38;5;80m",
|
|
96
|
+
green: "\x1B[38;5;114m",
|
|
97
|
+
yellow: "\x1B[38;5;221m",
|
|
98
|
+
orange: "\x1B[38;5;215m",
|
|
99
|
+
red: "\x1B[38;5;203m",
|
|
100
|
+
pink: "\x1B[38;5;176m",
|
|
101
|
+
gray: "\x1B[38;5;245m",
|
|
102
|
+
white: "\x1B[38;5;255m",
|
|
103
|
+
bg: {
|
|
104
|
+
purple: "\x1B[48;5;53m",
|
|
105
|
+
blue: "\x1B[48;5;24m",
|
|
106
|
+
green: "\x1B[48;5;22m",
|
|
107
|
+
red: "\x1B[48;5;52m",
|
|
108
|
+
yellow: "\x1B[48;5;58m",
|
|
109
|
+
gray: "\x1B[48;5;236m"
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const phaseIcon = {
|
|
113
|
+
reading: "\u25D4",
|
|
114
|
+
planning: "\u25D1",
|
|
115
|
+
implementing: "\u25D5",
|
|
116
|
+
testing: "\u25CF",
|
|
117
|
+
committing: "\u2713"
|
|
118
|
+
};
|
|
119
|
+
const phaseColor = {
|
|
120
|
+
reading: c.cyan,
|
|
121
|
+
planning: c.blue,
|
|
122
|
+
implementing: c.yellow,
|
|
123
|
+
testing: c.pink,
|
|
124
|
+
committing: c.green
|
|
125
|
+
};
|
|
126
|
+
function isProcessAlive(pid) {
|
|
127
|
+
try {
|
|
128
|
+
process.kill(pid, 0);
|
|
129
|
+
return true;
|
|
130
|
+
} catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function phaseProgress(phase, toolCalls, stale, alive) {
|
|
135
|
+
const basePct = {
|
|
136
|
+
reading: 10,
|
|
137
|
+
planning: 25,
|
|
138
|
+
implementing: 50,
|
|
139
|
+
testing: 75,
|
|
140
|
+
committing: 90
|
|
141
|
+
};
|
|
142
|
+
let pct = basePct[phase] || 0;
|
|
143
|
+
if (phase === "implementing") {
|
|
144
|
+
pct += Math.min(Math.floor(toolCalls / 80 * 25), 25);
|
|
145
|
+
}
|
|
146
|
+
if (phase === "committing") {
|
|
147
|
+
pct = 90 + Math.min(Math.floor(toolCalls / 60 * 10), 9);
|
|
148
|
+
}
|
|
149
|
+
const labels = {
|
|
150
|
+
reading: "Reading",
|
|
151
|
+
planning: "Planning",
|
|
152
|
+
implementing: "Implementing",
|
|
153
|
+
testing: "Testing",
|
|
154
|
+
committing: "Committing"
|
|
155
|
+
};
|
|
156
|
+
let label = labels[phase] || phase;
|
|
157
|
+
let color = phaseColor[phase] || "";
|
|
158
|
+
let icon = phaseIcon[phase] || "\u25CB";
|
|
159
|
+
if (!alive) {
|
|
160
|
+
label = "Dead";
|
|
161
|
+
color = c.red;
|
|
162
|
+
icon = "\u2717";
|
|
163
|
+
} else if (stale) {
|
|
164
|
+
label = "Stalled";
|
|
165
|
+
color = c.orange;
|
|
166
|
+
icon = "\u23F8";
|
|
167
|
+
}
|
|
168
|
+
return { icon, color, pct, label };
|
|
169
|
+
}
|
|
170
|
+
function progressBar(pct, width) {
|
|
171
|
+
const filled = Math.min(Math.round(pct / 100 * width), width);
|
|
172
|
+
const empty = width - filled;
|
|
173
|
+
const col = pct >= 90 ? c.green : pct >= 50 ? c.yellow : c.cyan;
|
|
174
|
+
return `${col}${"\u2501".repeat(filled)}${c.d}${"\u254C".repeat(empty)}${c.r}`;
|
|
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
|
+
}
|
|
76
202
|
function fmtMinutes(m) {
|
|
77
203
|
if (m < 0) return "N/A";
|
|
78
204
|
if (m >= 60) return `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
@@ -89,24 +215,20 @@ function printUsageSummary(u) {
|
|
|
89
215
|
const mins5x = u.minutesRemaining5x ?? -1;
|
|
90
216
|
const mins20x = u.minutesRemaining20x ?? -1;
|
|
91
217
|
const cacheHitRate = u.cacheHitRate || 0;
|
|
92
|
-
|
|
93
|
-
const d = "\x1B[2m";
|
|
94
|
-
const w = "\x1B[37m";
|
|
95
|
-
const r = "\x1B[0m";
|
|
96
|
-
console.log(`${b}Token Usage${r}`);
|
|
218
|
+
console.log(`${c.b}Token Usage${c.r}`);
|
|
97
219
|
console.log(
|
|
98
|
-
` 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}`
|
|
99
221
|
);
|
|
100
222
|
console.log(
|
|
101
|
-
` 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}`
|
|
102
224
|
);
|
|
103
225
|
console.log("");
|
|
104
|
-
console.log(`${b}Budget (Max plan, 5h window)${r}`);
|
|
226
|
+
console.log(`${c.b}Budget (Max plan, 5h window)${c.r}`);
|
|
105
227
|
console.log(
|
|
106
|
-
` 5x (225 msgs) ${budgetBar(budgetPct5x)} ${d}~${fmtMinutes(mins5x)} left${r}`
|
|
228
|
+
` 5x (225 msgs) ${budgetBar(budgetPct5x)} ${c.d}~${fmtMinutes(mins5x)} left${c.r}`
|
|
107
229
|
);
|
|
108
230
|
console.log(
|
|
109
|
-
` 20x (900 msgs) ${budgetBar(budgetPct20x)} ${d}~${fmtMinutes(mins20x)} left${r}`
|
|
231
|
+
` 20x (900 msgs) ${budgetBar(budgetPct20x)} ${c.d}~${fmtMinutes(mins20x)} left${c.r}`
|
|
110
232
|
);
|
|
111
233
|
}
|
|
112
234
|
function createConductorCommands() {
|
|
@@ -330,37 +452,143 @@ function createConductorCommands() {
|
|
|
330
452
|
globalDb.close();
|
|
331
453
|
});
|
|
332
454
|
cmd.command("status").description("Show running agent status table").action(async () => {
|
|
333
|
-
const
|
|
334
|
-
if (
|
|
455
|
+
const statuses = scanAgentStatuses();
|
|
456
|
+
if (statuses.length === 0) {
|
|
335
457
|
console.log("No agent status files found");
|
|
336
458
|
return;
|
|
337
459
|
}
|
|
338
|
-
const
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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);
|
|
464
|
+
const healthy = active.length - stalled.length;
|
|
465
|
+
const parts = [];
|
|
466
|
+
if (healthy > 0) parts.push(`${c.green}\u25CF ${healthy}${c.r}`);
|
|
467
|
+
if (stalled.length > 0)
|
|
468
|
+
parts.push(`${c.orange}\u23F8 ${stalled.length}${c.r}`);
|
|
469
|
+
if (dead.length > 0) parts.push(`${c.red}\u2717 ${dead.length}${c.r}`);
|
|
470
|
+
console.log(`
|
|
471
|
+
${c.b}${c.white}Conductor${c.r} ${parts.join(" ")}
|
|
472
|
+
`);
|
|
473
|
+
const cols = (process.stdout.columns || 80) >= 90 ? 2 : 1;
|
|
474
|
+
const rows = [];
|
|
475
|
+
for (const s of enriched) {
|
|
476
|
+
const { icon, color, pct, label } = phaseProgress(
|
|
477
|
+
s.phase,
|
|
478
|
+
s.toolCalls,
|
|
479
|
+
s.stale,
|
|
480
|
+
s.alive
|
|
481
|
+
);
|
|
482
|
+
const bar = progressBar(pct, 8);
|
|
483
|
+
const timeColor = !s.alive ? c.red : s.stale ? c.orange : c.gray;
|
|
484
|
+
const cell = [
|
|
485
|
+
`${color}${icon}${c.r} ${c.b}${s.issue}${c.r} ${color}${label}${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}`
|
|
487
|
+
];
|
|
488
|
+
rows.push(cell);
|
|
489
|
+
}
|
|
490
|
+
if (cols === 2) {
|
|
491
|
+
for (let i = 0; i < rows.length; i += 2) {
|
|
492
|
+
const left = rows[i];
|
|
493
|
+
const right = rows[i + 1];
|
|
494
|
+
const pad = 42;
|
|
495
|
+
if (right) {
|
|
496
|
+
console.log(
|
|
497
|
+
` ${left[0].padEnd(pad + 30)}${c.gray}\u2502${c.r} ${right[0]}`
|
|
498
|
+
);
|
|
499
|
+
console.log(
|
|
500
|
+
` ${left[1].padEnd(pad + 30)}${c.gray}\u2502${c.r} ${right[1]}`
|
|
501
|
+
);
|
|
502
|
+
} else {
|
|
503
|
+
console.log(` ${left[0]}`);
|
|
504
|
+
console.log(` ${left[1]}`);
|
|
505
|
+
}
|
|
506
|
+
if (i + 2 < rows.length) {
|
|
507
|
+
console.log(` ${c.gray}${"\u254C".repeat(38)}\u253C${"\u254C".repeat(38)}${c.r}`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
} else {
|
|
511
|
+
for (let i = 0; i < rows.length; i++) {
|
|
512
|
+
console.log(` ${rows[i][0]}`);
|
|
513
|
+
console.log(` ${rows[i][1]}`);
|
|
514
|
+
if (i < rows.length - 1) {
|
|
515
|
+
console.log(` ${c.gray}${"\u254C".repeat(38)}${c.r}`);
|
|
516
|
+
}
|
|
348
517
|
}
|
|
349
518
|
}
|
|
350
|
-
|
|
351
|
-
|
|
519
|
+
console.log("");
|
|
520
|
+
});
|
|
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) => {
|
|
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;
|
|
525
|
+
});
|
|
526
|
+
if (needsFinalize.length === 0) {
|
|
527
|
+
console.log(
|
|
528
|
+
`${c.green}All agents are healthy \u2014 nothing to finalize.${c.r}`
|
|
529
|
+
);
|
|
352
530
|
return;
|
|
353
531
|
}
|
|
354
|
-
|
|
355
|
-
|
|
532
|
+
console.log(
|
|
533
|
+
`
|
|
534
|
+
${c.b}Finalizing ${needsFinalize.length} agent(s)${c.r}
|
|
535
|
+
`
|
|
356
536
|
);
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
537
|
+
for (const s of needsFinalize) {
|
|
538
|
+
const elapsedStr = formatElapsed(s.elapsed).replace(" ago", "");
|
|
539
|
+
let hasCommits = false;
|
|
540
|
+
if (s.workspacePath && existsSync(s.workspacePath)) {
|
|
541
|
+
try {
|
|
542
|
+
const log = execSync("git log origin/main..HEAD --oneline", {
|
|
543
|
+
cwd: s.workspacePath,
|
|
544
|
+
encoding: "utf-8",
|
|
545
|
+
timeout: 1e4
|
|
546
|
+
});
|
|
547
|
+
hasCommits = log.trim().length > 0;
|
|
548
|
+
} catch {
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const statusIcon = !s.alive ? `${c.red}\u2717 dead${c.r}` : `${c.orange}\u23F8 stalled ${elapsedStr}${c.r}`;
|
|
552
|
+
const commitStatus = hasCommits ? `${c.green}has commits \u2192 In Review${c.r}` : `${c.gray}no commits \u2192 mark failed${c.r}`;
|
|
553
|
+
console.log(` ${c.b}${s.issue}${c.r} ${statusIcon} ${commitStatus}`);
|
|
554
|
+
if (options.dryRun) continue;
|
|
555
|
+
if (s.alive) {
|
|
556
|
+
try {
|
|
557
|
+
process.kill(s.pid, "SIGTERM");
|
|
558
|
+
console.log(` ${c.gray}Sent SIGTERM to pid ${s.pid}${c.r}`);
|
|
559
|
+
} catch {
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const statusPath = join(agentsDir, s.dir, "status.json");
|
|
563
|
+
try {
|
|
564
|
+
const updated = { ...s };
|
|
565
|
+
delete updated["dir"];
|
|
566
|
+
writeFileSync(
|
|
567
|
+
statusPath,
|
|
568
|
+
JSON.stringify(
|
|
569
|
+
{ ...updated, lastUpdate: (/* @__PURE__ */ new Date()).toISOString() },
|
|
570
|
+
null,
|
|
571
|
+
2
|
|
572
|
+
)
|
|
573
|
+
);
|
|
574
|
+
} catch {
|
|
575
|
+
}
|
|
576
|
+
if (hasCommits) {
|
|
577
|
+
console.log(
|
|
578
|
+
` ${c.cyan}\u2192 Move ${s.issue} to "In Review" in Linear${c.r}`
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (options.dryRun) {
|
|
583
|
+
console.log(
|
|
584
|
+
`
|
|
585
|
+
${c.d}Dry run \u2014 no changes made. Remove --dry-run to execute.${c.r}`
|
|
586
|
+
);
|
|
587
|
+
} else {
|
|
588
|
+
console.log(
|
|
589
|
+
`
|
|
590
|
+
${c.green}Done.${c.r} Run ${c.cyan}conductor status${c.r} to verify.`
|
|
591
|
+
);
|
|
364
592
|
}
|
|
365
593
|
});
|
|
366
594
|
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) => {
|
|
@@ -383,6 +611,189 @@ function createConductorCommands() {
|
|
|
383
611
|
process.on("SIGTERM", forward);
|
|
384
612
|
});
|
|
385
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
|
+
});
|
|
386
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) => {
|
|
387
798
|
const statusPath = join(
|
|
388
799
|
process.cwd(),
|
|
@@ -474,35 +885,8 @@ function createConductorCommands() {
|
|
|
474
885
|
let paused = false;
|
|
475
886
|
let refreshInterval = interval;
|
|
476
887
|
let phaseFilter = options.phase || null;
|
|
477
|
-
const b = "\x1B[1m";
|
|
478
|
-
const d = "\x1B[2m";
|
|
479
|
-
const cyan = "\x1B[36m";
|
|
480
|
-
const green = "\x1B[32m";
|
|
481
|
-
const yellow = "\x1B[33m";
|
|
482
|
-
const red = "\x1B[31m";
|
|
483
|
-
const r = "\x1B[0m";
|
|
484
888
|
function readStatuses() {
|
|
485
|
-
const
|
|
486
|
-
homedir(),
|
|
487
|
-
".stackmemory",
|
|
488
|
-
"conductor",
|
|
489
|
-
"agents"
|
|
490
|
-
);
|
|
491
|
-
if (!existsSync(agentsDir)) return [];
|
|
492
|
-
const entries = readdirSync(agentsDir, { withFileTypes: true });
|
|
493
|
-
const statuses = [];
|
|
494
|
-
for (const entry of entries) {
|
|
495
|
-
if (!entry.isDirectory()) continue;
|
|
496
|
-
const statusPath = join(agentsDir, entry.name, "status.json");
|
|
497
|
-
if (!existsSync(statusPath)) continue;
|
|
498
|
-
try {
|
|
499
|
-
statuses.push(JSON.parse(readFileSync(statusPath, "utf-8")));
|
|
500
|
-
} catch {
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
statuses.sort(
|
|
504
|
-
(a, b2) => new Date(b2.lastUpdate).getTime() - new Date(a.lastUpdate).getTime()
|
|
505
|
-
);
|
|
889
|
+
const statuses = scanAgentStatuses();
|
|
506
890
|
if (phaseFilter) {
|
|
507
891
|
return statuses.filter((s) => s.phase === phaseFilter);
|
|
508
892
|
}
|
|
@@ -514,12 +898,17 @@ function createConductorCommands() {
|
|
|
514
898
|
console.log(` No active agents${filterNote}`);
|
|
515
899
|
return;
|
|
516
900
|
}
|
|
517
|
-
const header = `${"Issue".padEnd(12)}${"Phase".padEnd(16)}${"Tools".padStart(7)}${"Files".padStart(7)}${"Tokens".padStart(9)} Last Update`;
|
|
518
|
-
console.log(header);
|
|
519
901
|
for (const s of statuses) {
|
|
520
|
-
const elapsed
|
|
521
|
-
const
|
|
522
|
-
|
|
902
|
+
const { elapsed, alive, stale } = enrichStatus(s);
|
|
903
|
+
const prog = phaseProgress(s.phase, s.toolCalls, stale, alive);
|
|
904
|
+
const bar = progressBar(prog.pct, 10);
|
|
905
|
+
const timeColor = !alive ? c.red : stale ? c.orange : c.gray;
|
|
906
|
+
console.log(
|
|
907
|
+
` ${prog.color}${prog.icon}${c.r} ${c.b}${s.issue}${c.r} ${prog.color}${prog.label}${c.r} ${timeColor}${formatElapsed(elapsed)}${c.r}`
|
|
908
|
+
);
|
|
909
|
+
console.log(
|
|
910
|
+
` ${bar} ${c.d}${prog.pct}%${c.r} ${c.gray}${s.toolCalls} tools ${s.filesModified} files ${fmtTokens(s.tokensUsed)} tok${c.r}`
|
|
911
|
+
);
|
|
523
912
|
}
|
|
524
913
|
}
|
|
525
914
|
function getWorktreeFiles(workspacePath) {
|
|
@@ -542,56 +931,53 @@ function createConductorCommands() {
|
|
|
542
931
|
return;
|
|
543
932
|
}
|
|
544
933
|
for (const s of statuses) {
|
|
545
|
-
const elapsed
|
|
546
|
-
const
|
|
934
|
+
const { elapsed, alive, stale } = enrichStatus(s);
|
|
935
|
+
const prog = phaseProgress(s.phase, s.toolCalls, stale, alive);
|
|
547
936
|
console.log(
|
|
548
|
-
|
|
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}`
|
|
549
938
|
);
|
|
550
939
|
const files = getWorktreeFiles(s.workspacePath || "");
|
|
551
940
|
if (files.length === 0) {
|
|
552
|
-
console.log(`
|
|
941
|
+
console.log(` ${c.d}(no file changes)${c.r}`);
|
|
553
942
|
} else {
|
|
554
943
|
for (const f of files) {
|
|
555
944
|
const status = f.substring(0, 2);
|
|
556
945
|
const path = f.substring(3);
|
|
557
|
-
let
|
|
558
|
-
if (status.includes("M"))
|
|
946
|
+
let col = "";
|
|
947
|
+
if (status.includes("M")) col = c.yellow;
|
|
559
948
|
else if (status.includes("A") || status.includes("?"))
|
|
560
|
-
|
|
561
|
-
else if (status.includes("D"))
|
|
562
|
-
console.log(`
|
|
949
|
+
col = c.green;
|
|
950
|
+
else if (status.includes("D")) col = c.red;
|
|
951
|
+
console.log(` ${col}${status}${c.r} ${path}`);
|
|
563
952
|
}
|
|
564
953
|
}
|
|
565
954
|
console.log("");
|
|
566
955
|
}
|
|
567
956
|
}
|
|
957
|
+
const cachedConductor = new Conductor({ repoRoot: process.cwd() });
|
|
568
958
|
async function getUsage() {
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
return conductor.getUsageSummary();
|
|
959
|
+
await cachedConductor.scanUsageLogs();
|
|
960
|
+
return cachedConductor.getUsageSummary();
|
|
572
961
|
}
|
|
573
962
|
async function render() {
|
|
574
963
|
process.stdout.write("\x1B[2J\x1B[H");
|
|
575
964
|
const pauseTag = paused ? " [PAUSED]" : "";
|
|
576
965
|
const intervalSec = Math.round(refreshInterval / 1e3);
|
|
577
966
|
console.log(
|
|
578
|
-
`${b}\
|
|
579
|
-
);
|
|
580
|
-
console.log(
|
|
581
|
-
`${b} Conductor Monitor${r} ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}${pauseTag}`
|
|
967
|
+
`${c.purple}${c.b} \u2501\u2501\u2501 Conductor Monitor \u2501\u2501\u2501${c.r} ${c.gray}${(/* @__PURE__ */ new Date()).toLocaleTimeString()}${pauseTag}${c.r}`
|
|
582
968
|
);
|
|
583
969
|
console.log(
|
|
584
|
-
` Mode
|
|
970
|
+
` ${c.gray}Mode:${c.r} ${c.cyan}${currentMode}${c.r} ${c.gray}\u2502${c.r} ${c.gray}Refresh:${c.r} ${intervalSec}s`
|
|
585
971
|
);
|
|
586
|
-
const filterNote = phaseFilter ? ` Filter
|
|
972
|
+
const filterNote = phaseFilter ? ` ${c.gray}Filter:${c.r} ${c.cyan}${phaseFilter}${c.r}` : "";
|
|
587
973
|
if (interactive) {
|
|
588
974
|
console.log(
|
|
589
|
-
` ${d}[s]tatus [u]sage [f]iles [d]ashboard [j]son [l]ogs [r]efresh [p]ause [1-5]phase [0]clear [+/-] [q]uit${r}`
|
|
975
|
+
` ${c.d}[s]tatus [u]sage [f]iles [d]ashboard [j]son [l]ogs [r]efresh [p]ause [1-5]phase [0]clear [+/-] [q]uit${c.r}`
|
|
590
976
|
);
|
|
591
977
|
}
|
|
592
978
|
if (filterNote) console.log(filterNote);
|
|
593
979
|
console.log(
|
|
594
|
-
`${
|
|
980
|
+
`${c.gray} \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}`
|
|
595
981
|
);
|
|
596
982
|
console.log("");
|
|
597
983
|
const statuses = readStatuses();
|
|
@@ -622,7 +1008,7 @@ function createConductorCommands() {
|
|
|
622
1008
|
}
|
|
623
1009
|
console.log("");
|
|
624
1010
|
console.log(
|
|
625
|
-
`${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}`
|
|
626
1012
|
);
|
|
627
1013
|
}
|
|
628
1014
|
await render();
|