@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.
@@ -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 { getAgentStatusDir } from "./orchestrator.js";
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
- const b = "\x1B[1m";
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 ${w}${fmtTokens(inputTokens)}${r} ${d}|${r} Output ${w}${fmtTokens(outputTokens)}${r} ${d}|${r} Total ${w}${fmtTokens(totalTokens)}${r}`
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 ${w}${fmtTokens(tokensPerMin)}/min${r} ${d}|${r} Messages ${w}${estMessages}${r} ${d}|${r} Cache hit ${w}${cacheHitRate}%${r}`
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 agentsDir = join(homedir(), ".stackmemory", "conductor", "agents");
334
- if (!existsSync(agentsDir)) {
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 entries = readdirSync(agentsDir, { withFileTypes: true });
339
- const statuses = [];
340
- for (const entry of entries) {
341
- if (!entry.isDirectory()) continue;
342
- const statusPath = join(agentsDir, entry.name, "status.json");
343
- if (!existsSync(statusPath)) continue;
344
- try {
345
- const data = JSON.parse(readFileSync(statusPath, "utf-8"));
346
- statuses.push(data);
347
- } catch {
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
- if (statuses.length === 0) {
351
- console.log("No agent status files found");
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
- statuses.sort(
355
- (a, b) => new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime()
532
+ console.log(
533
+ `
534
+ ${c.b}Finalizing ${needsFinalize.length} agent(s)${c.r}
535
+ `
356
536
  );
357
- const header = `${"Issue".padEnd(12)}${"Phase".padEnd(16)}${"Tools".padStart(7)}${"Files".padStart(7)}${"Tokens".padStart(9)} Last Update`;
358
- console.log(header);
359
- for (const s of statuses) {
360
- const elapsed = Date.now() - new Date(s.lastUpdate).getTime();
361
- const lastUpdate = formatElapsed(elapsed);
362
- 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}`;
363
- console.log(line);
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 agentsDir = join(
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 = Date.now() - new Date(s.lastUpdate).getTime();
521
- 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)} ${formatElapsed(elapsed)}`;
522
- console.log(line);
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 = Date.now() - new Date(s.lastUpdate).getTime();
546
- const phaseColor = s.phase === "committing" ? green : s.phase === "testing" ? yellow : s.phase === "implementing" ? cyan : "";
934
+ const { elapsed, alive, stale } = enrichStatus(s);
935
+ const prog = phaseProgress(s.phase, s.toolCalls, stale, alive);
547
936
  console.log(
548
- `${b}${s.issue}${r} ${phaseColor}${s.phase}${r} ${d}${formatElapsed(elapsed)}${r}`
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(` ${d}(no file changes detected)${r}`);
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 color = "";
558
- if (status.includes("M")) color = yellow;
946
+ let col = "";
947
+ if (status.includes("M")) col = c.yellow;
559
948
  else if (status.includes("A") || status.includes("?"))
560
- color = green;
561
- else if (status.includes("D")) color = red;
562
- console.log(` ${color}${status}${r} ${path}`);
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
- const conductor = new Conductor({ repoRoot: process.cwd() });
570
- await conductor.scanUsageLogs();
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}\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550${r}`
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: ${cyan}${currentMode}${r} | Refresh: ${intervalSec}s`
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: ${cyan}${phaseFilter}${r}` : "";
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
- `${b}\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550${r}`
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();