@stackmemoryai/stackmemory 1.5.8 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cli/commands/orchestrate.js +507 -140
- package/dist/src/cli/commands/orchestrator.js +84 -19
- package/dist/src/daemon/daemon-config.js +2 -3
- package/dist/src/daemon/session-daemon.js +4 -5
- package/dist/src/daemon/unified-daemon.js +4 -5
- package/dist/src/features/sweep/sweep-server-manager.js +3 -6
- package/dist/src/hooks/daemon.js +5 -8
- package/dist/src/utils/process-cleanup.js +9 -0
- package/package.json +1 -1
- package/scripts/gepa/.before-optimize.md +0 -32
- package/scripts/gepa/generations/gen-000/baseline.md +0 -32
- package/scripts/gepa/generations/gen-001/baseline.md +0 -32
- package/scripts/gepa/generations/gen-001/variant-a.md +107 -1
- package/scripts/gepa/generations/gen-001/variant-b.md +216 -1
- package/scripts/gepa/generations/gen-001/variant-c.md +83 -1
- package/scripts/gepa/generations/gen-001/variant-d.md +90 -1
- package/scripts/gepa/results/eval-1-baseline.json +78 -78
- package/scripts/gepa/results/eval-1-variant-a.json +74 -74
- package/scripts/gepa/results/eval-1-variant-b.json +78 -78
- package/scripts/gepa/results/eval-1-variant-c.json +72 -72
- package/scripts/gepa/results/eval-1-variant-d.json +80 -80
- package/scripts/gepa/state.json +17 -5
|
@@ -9,15 +9,18 @@ import {
|
|
|
9
9
|
mkdirSync,
|
|
10
10
|
writeFileSync,
|
|
11
11
|
readFileSync,
|
|
12
|
-
readdirSync
|
|
12
|
+
readdirSync,
|
|
13
|
+
copyFileSync
|
|
13
14
|
} from "fs";
|
|
14
15
|
import { join } from "path";
|
|
15
16
|
import { homedir, tmpdir } from "os";
|
|
16
17
|
import Database from "better-sqlite3";
|
|
17
18
|
import { logger } from "../../core/monitoring/logger.js";
|
|
19
|
+
import { isProcessAlive } from "../../utils/process-cleanup.js";
|
|
18
20
|
import { Conductor } from "./orchestrator.js";
|
|
19
21
|
import {
|
|
20
|
-
getAgentStatusDir
|
|
22
|
+
getAgentStatusDir,
|
|
23
|
+
getOutcomesLogPath
|
|
21
24
|
} from "./orchestrator.js";
|
|
22
25
|
function getGlobalStorePath() {
|
|
23
26
|
const dir = join(homedir(), ".stackmemory", "conductor");
|
|
@@ -75,6 +78,8 @@ function budgetBar(pct, width = 30) {
|
|
|
75
78
|
const rst = "\x1B[0m";
|
|
76
79
|
return `${color}${"\u2588".repeat(filled)}${dim}${"\u2591".repeat(empty)}${rst} ${String(pct).padStart(3)}%`;
|
|
77
80
|
}
|
|
81
|
+
const STALE_UI_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
82
|
+
const STALE_FINALIZE_THRESHOLD_MS = 60 * 60 * 1e3;
|
|
78
83
|
const c = {
|
|
79
84
|
r: "\x1B[0m",
|
|
80
85
|
// reset
|
|
@@ -120,14 +125,6 @@ const phaseColor = {
|
|
|
120
125
|
testing: c.pink,
|
|
121
126
|
committing: c.green
|
|
122
127
|
};
|
|
123
|
-
function isProcessAlive(pid) {
|
|
124
|
-
try {
|
|
125
|
-
process.kill(pid, 0);
|
|
126
|
-
return true;
|
|
127
|
-
} catch {
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
128
|
function phaseProgress(phase, toolCalls, stale, alive) {
|
|
132
129
|
const basePct = {
|
|
133
130
|
reading: 10,
|
|
@@ -170,6 +167,32 @@ function progressBar(pct, width) {
|
|
|
170
167
|
const col = pct >= 90 ? c.green : pct >= 50 ? c.yellow : c.cyan;
|
|
171
168
|
return `${col}${"\u2501".repeat(filled)}${c.d}${"\u254C".repeat(empty)}${c.r}`;
|
|
172
169
|
}
|
|
170
|
+
function scanAgentStatuses() {
|
|
171
|
+
const agentsDir2 = join(homedir(), ".stackmemory", "conductor", "agents");
|
|
172
|
+
if (!existsSync(agentsDir2)) return [];
|
|
173
|
+
const entries = readdirSync(agentsDir2, { withFileTypes: true });
|
|
174
|
+
const statuses = [];
|
|
175
|
+
for (const entry of entries) {
|
|
176
|
+
if (!entry.isDirectory()) continue;
|
|
177
|
+
const statusPath = join(agentsDir2, entry.name, "status.json");
|
|
178
|
+
if (!existsSync(statusPath)) continue;
|
|
179
|
+
try {
|
|
180
|
+
const data = JSON.parse(readFileSync(statusPath, "utf-8"));
|
|
181
|
+
statuses.push({ ...data, dir: entry.name });
|
|
182
|
+
} catch {
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
statuses.sort(
|
|
186
|
+
(a, b) => new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime()
|
|
187
|
+
);
|
|
188
|
+
return statuses;
|
|
189
|
+
}
|
|
190
|
+
function enrichStatus(s) {
|
|
191
|
+
const elapsed = Date.now() - new Date(s.lastUpdate).getTime();
|
|
192
|
+
const alive = isProcessAlive(s.pid);
|
|
193
|
+
const stale = alive && elapsed > STALE_UI_THRESHOLD_MS;
|
|
194
|
+
return { elapsed, alive, stale };
|
|
195
|
+
}
|
|
173
196
|
function fmtMinutes(m) {
|
|
174
197
|
if (m < 0) return "N/A";
|
|
175
198
|
if (m >= 60) return `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
@@ -186,25 +209,220 @@ function printUsageSummary(u) {
|
|
|
186
209
|
const mins5x = u.minutesRemaining5x ?? -1;
|
|
187
210
|
const mins20x = u.minutesRemaining20x ?? -1;
|
|
188
211
|
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}`);
|
|
212
|
+
console.log(`${c.b}Token Usage${c.r}`);
|
|
194
213
|
console.log(
|
|
195
|
-
` Input ${
|
|
214
|
+
` 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
215
|
);
|
|
197
216
|
console.log(
|
|
198
|
-
` Rate ${
|
|
217
|
+
` 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
218
|
);
|
|
200
219
|
console.log("");
|
|
201
|
-
console.log(`${b}Budget (Max plan, 5h window)${
|
|
220
|
+
console.log(`${c.b}Budget (Max plan, 5h window)${c.r}`);
|
|
202
221
|
console.log(
|
|
203
|
-
` 5x (225 msgs) ${budgetBar(budgetPct5x)} ${
|
|
222
|
+
` 5x (225 msgs) ${budgetBar(budgetPct5x)} ${c.d}~${fmtMinutes(mins5x)} left${c.r}`
|
|
204
223
|
);
|
|
205
224
|
console.log(
|
|
206
|
-
` 20x (900 msgs) ${budgetBar(budgetPct20x)} ${
|
|
225
|
+
` 20x (900 msgs) ${budgetBar(budgetPct20x)} ${c.d}~${fmtMinutes(mins20x)} left${c.r}`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
const DEFAULT_PROMPT_TEMPLATE = `# Agent Prompt \u2014 {{ISSUE_ID}}
|
|
229
|
+
|
|
230
|
+
You are working on Linear issue **{{ISSUE_ID}}**: {{TITLE}}
|
|
231
|
+
|
|
232
|
+
## Description
|
|
233
|
+
|
|
234
|
+
{{DESCRIPTION}}
|
|
235
|
+
|
|
236
|
+
## Context
|
|
237
|
+
|
|
238
|
+
- Priority: {{PRIORITY}}
|
|
239
|
+
- Labels: {{LABELS}}
|
|
240
|
+
{{PRIOR_CONTEXT}}
|
|
241
|
+
|
|
242
|
+
## Instructions
|
|
243
|
+
|
|
244
|
+
1. Read the issue description and related code carefully
|
|
245
|
+
2. Plan your approach before writing code
|
|
246
|
+
3. Implement the requested changes
|
|
247
|
+
4. Run \`npm run lint\` and fix any errors
|
|
248
|
+
5. Run \`npm run test:run\` and fix any failures
|
|
249
|
+
6. Commit your changes with format: \`type(scope): message\`
|
|
250
|
+
|
|
251
|
+
## Rules
|
|
252
|
+
|
|
253
|
+
- Follow existing code conventions (ESM imports with .js extension, TypeScript strict)
|
|
254
|
+
- Keep changes focused \u2014 only modify what the issue requires
|
|
255
|
+
- Write or update tests for any new functionality
|
|
256
|
+
- Do not skip pre-commit hooks
|
|
257
|
+
- If stuck, leave a comment in the code explaining the blocker
|
|
258
|
+
|
|
259
|
+
Work in the current directory. All changes will be on a dedicated branch.
|
|
260
|
+
`;
|
|
261
|
+
function ensureDefaultPromptTemplate() {
|
|
262
|
+
const templatePath = join(
|
|
263
|
+
homedir(),
|
|
264
|
+
".stackmemory",
|
|
265
|
+
"conductor",
|
|
266
|
+
"prompt-template.md"
|
|
207
267
|
);
|
|
268
|
+
if (!existsSync(templatePath)) {
|
|
269
|
+
const dir = join(homedir(), ".stackmemory", "conductor");
|
|
270
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
271
|
+
writeFileSync(templatePath, DEFAULT_PROMPT_TEMPLATE);
|
|
272
|
+
console.log(
|
|
273
|
+
` ${c.d}Created default prompt template: ${templatePath}${c.r}`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
return templatePath;
|
|
277
|
+
}
|
|
278
|
+
function spawnClaudePrint(prompt, timeoutMs = 12e4) {
|
|
279
|
+
return new Promise((resolve, reject) => {
|
|
280
|
+
const child = cpSpawn("claude", ["--print"], {
|
|
281
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
282
|
+
env: { ...process.env }
|
|
283
|
+
});
|
|
284
|
+
let stdout = "";
|
|
285
|
+
let stderr = "";
|
|
286
|
+
let killed = false;
|
|
287
|
+
const timer = setTimeout(() => {
|
|
288
|
+
killed = true;
|
|
289
|
+
child.kill("SIGTERM");
|
|
290
|
+
}, timeoutMs);
|
|
291
|
+
child.stdout.on("data", (d) => stdout += d.toString());
|
|
292
|
+
child.stderr.on("data", (d) => stderr += d.toString());
|
|
293
|
+
child.on("close", (code) => {
|
|
294
|
+
clearTimeout(timer);
|
|
295
|
+
if (killed)
|
|
296
|
+
return reject(new Error(`claude timed out after ${timeoutMs}ms`));
|
|
297
|
+
if (code !== 0 && !stdout)
|
|
298
|
+
return reject(new Error(stderr || `claude exited ${code}`));
|
|
299
|
+
resolve(stdout);
|
|
300
|
+
});
|
|
301
|
+
child.on("error", (err) => {
|
|
302
|
+
clearTimeout(timer);
|
|
303
|
+
reject(err);
|
|
304
|
+
});
|
|
305
|
+
child.stdin.write(prompt);
|
|
306
|
+
child.stdin.end();
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
async function evolvePromptTemplate(input) {
|
|
310
|
+
const {
|
|
311
|
+
templatePath,
|
|
312
|
+
successRate,
|
|
313
|
+
failures,
|
|
314
|
+
failPhases,
|
|
315
|
+
errorPatterns,
|
|
316
|
+
recs,
|
|
317
|
+
outcomes
|
|
318
|
+
} = input;
|
|
319
|
+
let currentTemplate;
|
|
320
|
+
if (existsSync(templatePath)) {
|
|
321
|
+
currentTemplate = readFileSync(templatePath, "utf-8");
|
|
322
|
+
} else {
|
|
323
|
+
currentTemplate = DEFAULT_PROMPT_TEMPLATE;
|
|
324
|
+
const dir = join(homedir(), ".stackmemory", "conductor");
|
|
325
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
326
|
+
writeFileSync(templatePath, currentTemplate);
|
|
327
|
+
}
|
|
328
|
+
const failPhaseSummary = Object.entries(failPhases).sort((a, b) => b[1] - a[1]).map(([phase, count]) => ` - ${phase}: ${count} failures`).join("\n");
|
|
329
|
+
const errorSummary = Object.entries(errorPatterns).sort((a, b) => b[1] - a[1]).map(([pattern, count]) => ` - ${pattern}: ${count} occurrences`).join("\n");
|
|
330
|
+
const failedOutcomes = outcomes.filter((o) => o.outcome === "failure" && o.errorTail).slice(-5);
|
|
331
|
+
const errorTails = failedOutcomes.map(
|
|
332
|
+
(o) => ` [${o.issue} attempt ${o.attempt}, phase: ${o.phase}]
|
|
333
|
+
${o.errorTail}`
|
|
334
|
+
).join("\n\n");
|
|
335
|
+
const mutationPrompt = `You are optimizing a prompt template for autonomous AI coding agents managed by a conductor system.
|
|
336
|
+
|
|
337
|
+
CURRENT PROMPT TEMPLATE:
|
|
338
|
+
\`\`\`markdown
|
|
339
|
+
${currentTemplate}
|
|
340
|
+
\`\`\`
|
|
341
|
+
|
|
342
|
+
PERFORMANCE DATA:
|
|
343
|
+
- Success rate: ${successRate}%
|
|
344
|
+
- Total failures: ${failures}
|
|
345
|
+
|
|
346
|
+
FAILURE PHASE BREAKDOWN:
|
|
347
|
+
${failPhaseSummary || " (none)"}
|
|
348
|
+
|
|
349
|
+
ERROR PATTERNS:
|
|
350
|
+
${errorSummary || " (none)"}
|
|
351
|
+
|
|
352
|
+
SAMPLE ERROR TAILS FROM RECENT FAILURES:
|
|
353
|
+
${errorTails || " (none)"}
|
|
354
|
+
|
|
355
|
+
RECOMMENDATIONS FROM ANALYSIS:
|
|
356
|
+
${recs.map((r) => `- ${r}`).join("\n")}
|
|
357
|
+
|
|
358
|
+
YOUR TASK:
|
|
359
|
+
Improve the prompt template to reduce failures. Focus on:
|
|
360
|
+
1. Adding specific instructions that address the most common failure modes
|
|
361
|
+
2. Making implicit requirements explicit (lint rules, test commands, commit format)
|
|
362
|
+
3. Adding guardrails for the error patterns seen (e.g., if lint failures are common, add lint-specific instructions)
|
|
363
|
+
4. Keeping the template concise \u2014 agents work better with clear, structured prompts
|
|
364
|
+
5. Preserving all {{VARIABLE}} placeholders exactly as-is
|
|
365
|
+
|
|
366
|
+
REQUIREMENTS:
|
|
367
|
+
- Output ONLY the improved markdown template
|
|
368
|
+
- Keep all {{VARIABLE}} placeholders: {{ISSUE_ID}}, {{TITLE}}, {{DESCRIPTION}}, {{LABELS}}, {{PRIORITY}}, {{ATTEMPT}}, {{PRIOR_CONTEXT}}
|
|
369
|
+
- Do not add commentary, explanations, or markdown fences around the output
|
|
370
|
+
- Target similar length to the current template (no bloat)
|
|
371
|
+
|
|
372
|
+
OUTPUT THE IMPROVED TEMPLATE:`;
|
|
373
|
+
try {
|
|
374
|
+
console.log(
|
|
375
|
+
` ${c.d}Calling Claude to generate improved template...${c.r}`
|
|
376
|
+
);
|
|
377
|
+
const evolved = await spawnClaudePrint(mutationPrompt);
|
|
378
|
+
if (!evolved.trim()) {
|
|
379
|
+
console.log(` ${c.red}Empty response from Claude \u2014 skipping.${c.r}`);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const requiredVars = ["{{ISSUE_ID}}", "{{TITLE}}", "{{DESCRIPTION}}"];
|
|
383
|
+
const missing = requiredVars.filter((v) => !evolved.includes(v));
|
|
384
|
+
if (missing.length > 0) {
|
|
385
|
+
console.log(
|
|
386
|
+
` ${c.red}Evolved template missing variables: ${missing.join(", ")} \u2014 skipping.${c.r}`
|
|
387
|
+
);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
391
|
+
const backupPath = templatePath.replace(".md", `.backup-${timestamp}.md`);
|
|
392
|
+
copyFileSync(templatePath, backupPath);
|
|
393
|
+
console.log(` ${c.d}Backed up to ${backupPath}${c.r}`);
|
|
394
|
+
writeFileSync(templatePath, evolved.trim() + "\n");
|
|
395
|
+
console.log(
|
|
396
|
+
` ${c.green}Evolved template written to ${templatePath}${c.r}`
|
|
397
|
+
);
|
|
398
|
+
const evolutionLog = join(
|
|
399
|
+
homedir(),
|
|
400
|
+
".stackmemory",
|
|
401
|
+
"conductor",
|
|
402
|
+
"evolution-log.jsonl"
|
|
403
|
+
);
|
|
404
|
+
const entry = {
|
|
405
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
406
|
+
successRate,
|
|
407
|
+
failures,
|
|
408
|
+
failPhases,
|
|
409
|
+
errorPatterns,
|
|
410
|
+
backupPath
|
|
411
|
+
};
|
|
412
|
+
const dir = join(homedir(), ".stackmemory", "conductor");
|
|
413
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
414
|
+
writeFileSync(
|
|
415
|
+
evolutionLog,
|
|
416
|
+
(existsSync(evolutionLog) ? readFileSync(evolutionLog, "utf-8") : "") + JSON.stringify(entry) + "\n"
|
|
417
|
+
);
|
|
418
|
+
console.log(` ${c.d}Evolution logged to ${evolutionLog}${c.r}`);
|
|
419
|
+
} catch (err) {
|
|
420
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
421
|
+
console.log(` ${c.red}Evolution failed: ${msg}${c.r}`);
|
|
422
|
+
console.log(
|
|
423
|
+
` ${c.d}Tip: Ensure 'claude' CLI is available and authenticated.${c.r}`
|
|
424
|
+
);
|
|
425
|
+
}
|
|
208
426
|
}
|
|
209
427
|
function createConductorCommands() {
|
|
210
428
|
const cmd = new Command("conductor");
|
|
@@ -323,14 +541,14 @@ function createConductorCommands() {
|
|
|
323
541
|
const risks = anchors.filter((a) => a.type === "RISK");
|
|
324
542
|
if (decisions.length > 0) {
|
|
325
543
|
lines.push("", "### Decisions");
|
|
326
|
-
for (const
|
|
327
|
-
lines.push(`- ${
|
|
544
|
+
for (const d of decisions.slice(0, 10)) {
|
|
545
|
+
lines.push(`- ${d.text}`);
|
|
328
546
|
}
|
|
329
547
|
}
|
|
330
548
|
if (risks.length > 0) {
|
|
331
549
|
lines.push("", "### Risks");
|
|
332
|
-
for (const
|
|
333
|
-
lines.push(`- ${
|
|
550
|
+
for (const r of risks.slice(0, 5)) {
|
|
551
|
+
lines.push(`- ${r.text}`);
|
|
334
552
|
}
|
|
335
553
|
}
|
|
336
554
|
} catch {
|
|
@@ -414,48 +632,28 @@ function createConductorCommands() {
|
|
|
414
632
|
}
|
|
415
633
|
console.log(`Found ${results.length} result(s) for "${query}":
|
|
416
634
|
`);
|
|
417
|
-
for (const
|
|
418
|
-
const date = new Date(
|
|
635
|
+
for (const r of results) {
|
|
636
|
+
const date = new Date(r.captured_at * 1e3).toISOString().slice(0, 16);
|
|
419
637
|
console.log(
|
|
420
|
-
` ${
|
|
638
|
+
` ${r.issue_id} [${r.context_type}] attempt ${r.attempt} (${date})`
|
|
421
639
|
);
|
|
422
|
-
if (
|
|
423
|
-
const snippet =
|
|
640
|
+
if (r.summary) {
|
|
641
|
+
const snippet = r.summary.slice(0, 120).replace(/\n/g, " ");
|
|
424
642
|
console.log(` ${snippet}`);
|
|
425
643
|
}
|
|
426
644
|
}
|
|
427
645
|
globalDb.close();
|
|
428
646
|
});
|
|
429
647
|
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
|
-
}
|
|
648
|
+
const statuses = scanAgentStatuses();
|
|
447
649
|
if (statuses.length === 0) {
|
|
448
650
|
console.log("No agent status files found");
|
|
449
651
|
return;
|
|
450
652
|
}
|
|
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));
|
|
653
|
+
const enriched = statuses.map((s) => ({ ...s, ...enrichStatus(s) }));
|
|
654
|
+
const active = enriched.filter((s) => s.alive);
|
|
655
|
+
const stalled = enriched.filter((s) => s.stale);
|
|
656
|
+
const dead = enriched.filter((s) => !s.alive);
|
|
459
657
|
const healthy = active.length - stalled.length;
|
|
460
658
|
const parts = [];
|
|
461
659
|
if (healthy > 0) parts.push(`${c.green}\u25CF ${healthy}${c.r}`);
|
|
@@ -467,21 +665,18 @@ function createConductorCommands() {
|
|
|
467
665
|
`);
|
|
468
666
|
const cols = (process.stdout.columns || 80) >= 90 ? 2 : 1;
|
|
469
667
|
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);
|
|
668
|
+
for (const s of enriched) {
|
|
474
669
|
const { icon, color, pct, label } = phaseProgress(
|
|
475
670
|
s.phase,
|
|
476
671
|
s.toolCalls,
|
|
477
|
-
|
|
478
|
-
alive
|
|
672
|
+
s.stale,
|
|
673
|
+
s.alive
|
|
479
674
|
);
|
|
480
675
|
const bar = progressBar(pct, 8);
|
|
481
|
-
const timeColor = !alive ? c.red :
|
|
676
|
+
const timeColor = !s.alive ? c.red : s.stale ? c.orange : c.gray;
|
|
482
677
|
const cell = [
|
|
483
678
|
`${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}`
|
|
679
|
+
` ${bar} ${c.d}${pct}%${c.r} ${c.gray}${s.toolCalls}t ${s.filesModified}f${c.r} ${timeColor}${formatElapsed(s.elapsed)}${c.r}`
|
|
485
680
|
];
|
|
486
681
|
rows.push(cell);
|
|
487
682
|
}
|
|
@@ -517,27 +712,9 @@ function createConductorCommands() {
|
|
|
517
712
|
console.log("");
|
|
518
713
|
});
|
|
519
714
|
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;
|
|
715
|
+
const statuses = scanAgentStatuses();
|
|
716
|
+
const needsFinalize = statuses.map((s) => ({ ...s, ...enrichStatus(s) })).filter((s) => {
|
|
717
|
+
return !s.alive || s.elapsed > STALE_FINALIZE_THRESHOLD_MS;
|
|
541
718
|
});
|
|
542
719
|
if (needsFinalize.length === 0) {
|
|
543
720
|
console.log(
|
|
@@ -551,26 +728,37 @@ function createConductorCommands() {
|
|
|
551
728
|
`
|
|
552
729
|
);
|
|
553
730
|
for (const s of needsFinalize) {
|
|
554
|
-
const
|
|
555
|
-
const elapsed = Date.now() - new Date(s.lastUpdate).getTime();
|
|
556
|
-
const elapsedStr = formatElapsed(elapsed).replace(" ago", "");
|
|
731
|
+
const elapsedStr = formatElapsed(s.elapsed).replace(" ago", "");
|
|
557
732
|
let hasCommits = false;
|
|
558
733
|
if (s.workspacePath && existsSync(s.workspacePath)) {
|
|
734
|
+
let baseBranch = "main";
|
|
559
735
|
try {
|
|
560
|
-
const
|
|
736
|
+
const ref = execSync("git symbolic-ref refs/remotes/origin/HEAD", {
|
|
561
737
|
cwd: s.workspacePath,
|
|
562
738
|
encoding: "utf-8",
|
|
563
|
-
timeout:
|
|
564
|
-
});
|
|
739
|
+
timeout: 5e3
|
|
740
|
+
}).trim();
|
|
741
|
+
baseBranch = ref.replace("refs/remotes/origin/", "");
|
|
742
|
+
} catch {
|
|
743
|
+
}
|
|
744
|
+
try {
|
|
745
|
+
const log = execSync(
|
|
746
|
+
`git log origin/${baseBranch}..HEAD --oneline`,
|
|
747
|
+
{
|
|
748
|
+
cwd: s.workspacePath,
|
|
749
|
+
encoding: "utf-8",
|
|
750
|
+
timeout: 1e4
|
|
751
|
+
}
|
|
752
|
+
);
|
|
565
753
|
hasCommits = log.trim().length > 0;
|
|
566
754
|
} catch {
|
|
567
755
|
}
|
|
568
756
|
}
|
|
569
|
-
const statusIcon = !alive ? `${c.red}\u2717 dead${c.r}` : `${c.orange}\u23F8 stalled ${elapsedStr}${c.r}`;
|
|
757
|
+
const statusIcon = !s.alive ? `${c.red}\u2717 dead${c.r}` : `${c.orange}\u23F8 stalled ${elapsedStr}${c.r}`;
|
|
570
758
|
const commitStatus = hasCommits ? `${c.green}has commits \u2192 In Review${c.r}` : `${c.gray}no commits \u2192 mark failed${c.r}`;
|
|
571
759
|
console.log(` ${c.b}${s.issue}${c.r} ${statusIcon} ${commitStatus}`);
|
|
572
760
|
if (options.dryRun) continue;
|
|
573
|
-
if (alive) {
|
|
761
|
+
if (s.alive) {
|
|
574
762
|
try {
|
|
575
763
|
process.kill(s.pid, "SIGTERM");
|
|
576
764
|
console.log(` ${c.gray}Sent SIGTERM to pid ${s.pid}${c.r}`);
|
|
@@ -618,17 +806,220 @@ function createConductorCommands() {
|
|
|
618
806
|
const lines = parseInt(options.lines, 10);
|
|
619
807
|
const args = options.follow ? ["-f", "-n", String(lines), logPath] : ["-n", String(lines), logPath];
|
|
620
808
|
const tail = cpSpawn("tail", args, { stdio: "inherit" });
|
|
809
|
+
const forward = () => {
|
|
810
|
+
tail.kill("SIGTERM");
|
|
811
|
+
};
|
|
812
|
+
process.on("SIGINT", forward);
|
|
813
|
+
process.on("SIGTERM", forward);
|
|
621
814
|
await new Promise((resolve) => {
|
|
622
815
|
tail.on("close", () => {
|
|
816
|
+
process.removeListener("SIGINT", forward);
|
|
817
|
+
process.removeListener("SIGTERM", forward);
|
|
623
818
|
resolve();
|
|
624
819
|
});
|
|
625
|
-
const forward = () => {
|
|
626
|
-
tail.kill("SIGTERM");
|
|
627
|
-
};
|
|
628
|
-
process.on("SIGINT", forward);
|
|
629
|
-
process.on("SIGTERM", forward);
|
|
630
820
|
});
|
|
631
821
|
});
|
|
822
|
+
cmd.command("learn").description(
|
|
823
|
+
"Analyze agent outcomes and generate improved prompt templates"
|
|
824
|
+
).option("--last <n>", "Analyze last N outcomes (default: all)", "0").option("--failures-only", "Only analyze failures", false).option("--export", "Export analysis as JSON", false).option(
|
|
825
|
+
"--evolve",
|
|
826
|
+
"Auto-mutate prompt template using GEPA-style evolution from failure data",
|
|
827
|
+
false
|
|
828
|
+
).action(async (options) => {
|
|
829
|
+
const logPath = getOutcomesLogPath();
|
|
830
|
+
if (!existsSync(logPath)) {
|
|
831
|
+
console.log(
|
|
832
|
+
`${c.yellow}No outcomes log found.${c.r} Run conductor to generate data.`
|
|
833
|
+
);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
const raw = readFileSync(logPath, "utf-8").trim().split("\n").filter((l) => l.length > 0);
|
|
837
|
+
let outcomes = raw.map(
|
|
838
|
+
(line) => JSON.parse(line)
|
|
839
|
+
);
|
|
840
|
+
if (options.failuresOnly) {
|
|
841
|
+
outcomes = outcomes.filter((o) => o.outcome === "failure");
|
|
842
|
+
}
|
|
843
|
+
const lastN = parseInt(options.last, 10);
|
|
844
|
+
if (lastN > 0) {
|
|
845
|
+
outcomes = outcomes.slice(-lastN);
|
|
846
|
+
}
|
|
847
|
+
if (outcomes.length === 0) {
|
|
848
|
+
console.log(`${c.gray}No matching outcomes to analyze.${c.r}`);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
const total = outcomes.length;
|
|
852
|
+
const successes = outcomes.filter((o) => o.outcome === "success").length;
|
|
853
|
+
const failures = outcomes.filter((o) => o.outcome === "failure").length;
|
|
854
|
+
const successRate = Math.round(successes / total * 100);
|
|
855
|
+
const avgTokens = Math.round(
|
|
856
|
+
outcomes.reduce((s, o) => s + o.tokensUsed, 0) / total
|
|
857
|
+
);
|
|
858
|
+
const avgDuration = Math.round(
|
|
859
|
+
outcomes.reduce((s, o) => s + o.durationMs, 0) / total / 6e4
|
|
860
|
+
);
|
|
861
|
+
const avgTools = Math.round(
|
|
862
|
+
outcomes.reduce((s, o) => s + o.toolCalls, 0) / total
|
|
863
|
+
);
|
|
864
|
+
const failPhases = {};
|
|
865
|
+
for (const o of outcomes.filter((o2) => o2.outcome === "failure")) {
|
|
866
|
+
failPhases[o.phase] = (failPhases[o.phase] || 0) + 1;
|
|
867
|
+
}
|
|
868
|
+
const retries = outcomes.filter((o) => o.attempt > 1);
|
|
869
|
+
const retrySuccessRate = retries.length > 0 ? Math.round(
|
|
870
|
+
retries.filter((o) => o.outcome === "success").length / retries.length * 100
|
|
871
|
+
) : 0;
|
|
872
|
+
const errorPatterns = {};
|
|
873
|
+
for (const o of outcomes.filter(
|
|
874
|
+
(o2) => o2.outcome === "failure" && o2.errorTail
|
|
875
|
+
)) {
|
|
876
|
+
const tail = o.errorTail;
|
|
877
|
+
if (tail.includes("lint"))
|
|
878
|
+
errorPatterns["lint_failure"] = (errorPatterns["lint_failure"] || 0) + 1;
|
|
879
|
+
else if (tail.includes("test"))
|
|
880
|
+
errorPatterns["test_failure"] = (errorPatterns["test_failure"] || 0) + 1;
|
|
881
|
+
else if (tail.includes("timeout") || tail.includes("timed out"))
|
|
882
|
+
errorPatterns["timeout"] = (errorPatterns["timeout"] || 0) + 1;
|
|
883
|
+
else if (tail.includes("conflict"))
|
|
884
|
+
errorPatterns["git_conflict"] = (errorPatterns["git_conflict"] || 0) + 1;
|
|
885
|
+
else if (tail.includes("429") || tail.includes("rate"))
|
|
886
|
+
errorPatterns["rate_limit"] = (errorPatterns["rate_limit"] || 0) + 1;
|
|
887
|
+
else errorPatterns["unknown"] = (errorPatterns["unknown"] || 0) + 1;
|
|
888
|
+
}
|
|
889
|
+
if (options.export) {
|
|
890
|
+
const analysis = {
|
|
891
|
+
total,
|
|
892
|
+
successes,
|
|
893
|
+
failures,
|
|
894
|
+
successRate,
|
|
895
|
+
avgTokens,
|
|
896
|
+
avgDurationMin: avgDuration,
|
|
897
|
+
avgToolCalls: avgTools,
|
|
898
|
+
failPhases,
|
|
899
|
+
retrySuccessRate,
|
|
900
|
+
errorPatterns,
|
|
901
|
+
outcomes
|
|
902
|
+
};
|
|
903
|
+
console.log(JSON.stringify(analysis, null, 2));
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
console.log(`
|
|
907
|
+
${c.b}${c.purple}Conductor Learning Report${c.r}
|
|
908
|
+
`);
|
|
909
|
+
const rateColor = successRate >= 80 ? c.green : successRate >= 50 ? c.yellow : c.red;
|
|
910
|
+
console.log(
|
|
911
|
+
` ${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`
|
|
912
|
+
);
|
|
913
|
+
console.log(
|
|
914
|
+
` ${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`
|
|
915
|
+
);
|
|
916
|
+
if (retries.length > 0) {
|
|
917
|
+
console.log(
|
|
918
|
+
` ${c.b}Retries${c.r} ${c.white}${retries.length}${c.r} attempts ${c.white}${retrySuccessRate}%${c.r} retry success rate`
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
if (failures > 0) {
|
|
922
|
+
console.log(`
|
|
923
|
+
${c.b}Failure Phases${c.r}`);
|
|
924
|
+
const sorted = Object.entries(failPhases).sort((a, b) => b[1] - a[1]);
|
|
925
|
+
for (const [phase, count] of sorted) {
|
|
926
|
+
const pct = Math.round(count / failures * 100);
|
|
927
|
+
const bar = progressBar(pct, 10);
|
|
928
|
+
console.log(
|
|
929
|
+
` ${phaseIcon[phase] || "\u25CB"} ${phase.padEnd(14)} ${bar} ${c.white}${count}${c.r} ${c.gray}(${pct}%)${c.r}`
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
if (Object.keys(errorPatterns).length > 0) {
|
|
934
|
+
console.log(`
|
|
935
|
+
${c.b}Error Patterns${c.r}`);
|
|
936
|
+
const sorted = Object.entries(errorPatterns).sort(
|
|
937
|
+
(a, b) => b[1] - a[1]
|
|
938
|
+
);
|
|
939
|
+
for (const [pattern, count] of sorted) {
|
|
940
|
+
console.log(
|
|
941
|
+
` ${c.red}\u25CF${c.r} ${pattern.padEnd(16)} ${c.white}${count}${c.r}`
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
console.log(`
|
|
946
|
+
${c.b}Recommendations${c.r}`);
|
|
947
|
+
const recs = [];
|
|
948
|
+
if (errorPatterns["lint_failure"] > 0) {
|
|
949
|
+
recs.push(
|
|
950
|
+
"Add explicit lint rules to prompt template (ESLint conventions, import style)"
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
if (errorPatterns["test_failure"] > 0) {
|
|
954
|
+
recs.push(
|
|
955
|
+
'Add "run tests before committing" emphasis, include test command in prompt'
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
if (errorPatterns["timeout"] > 0) {
|
|
959
|
+
recs.push(
|
|
960
|
+
"Reduce scope per issue or increase turnTimeoutMs in conductor config"
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
if (failPhases["implementing"] > failures * 0.5) {
|
|
964
|
+
recs.push(
|
|
965
|
+
"Agents stall during implementation \u2014 add examples or break issues smaller"
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
if (failPhases["reading"] > 0) {
|
|
969
|
+
recs.push(
|
|
970
|
+
"Agents fail during reading \u2014 improve issue descriptions or add context pointers"
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
if (retrySuccessRate < 30 && retries.length > 2) {
|
|
974
|
+
recs.push(
|
|
975
|
+
"Low retry success \u2014 consider better prior-attempt context injection"
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
if (successRate >= 80) {
|
|
979
|
+
recs.push(
|
|
980
|
+
"High success rate \u2014 current prompt template is working well"
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
if (recs.length === 0) {
|
|
984
|
+
recs.push("Collect more data for actionable recommendations");
|
|
985
|
+
}
|
|
986
|
+
for (const rec of recs) {
|
|
987
|
+
console.log(` ${c.cyan}\u2192${c.r} ${rec}`);
|
|
988
|
+
}
|
|
989
|
+
const templatePath = join(
|
|
990
|
+
homedir(),
|
|
991
|
+
".stackmemory",
|
|
992
|
+
"conductor",
|
|
993
|
+
"prompt-template.md"
|
|
994
|
+
);
|
|
995
|
+
if (!existsSync(templatePath)) {
|
|
996
|
+
console.log(
|
|
997
|
+
`
|
|
998
|
+
${c.d}Tip: Create ${templatePath} to customize agent prompts.${c.r}`
|
|
999
|
+
);
|
|
1000
|
+
console.log(
|
|
1001
|
+
` ${c.d}Variables: {{ISSUE_ID}} {{TITLE}} {{DESCRIPTION}} {{LABELS}} {{PRIORITY}} {{ATTEMPT}} {{PRIOR_CONTEXT}}${c.r}`
|
|
1002
|
+
);
|
|
1003
|
+
} else {
|
|
1004
|
+
console.log(`
|
|
1005
|
+
${c.d}Using custom template: ${templatePath}${c.r}`);
|
|
1006
|
+
}
|
|
1007
|
+
if (options.evolve) {
|
|
1008
|
+
console.log(`
|
|
1009
|
+
${c.b}${c.cyan}Evolving prompt template...${c.r}
|
|
1010
|
+
`);
|
|
1011
|
+
await evolvePromptTemplate({
|
|
1012
|
+
templatePath,
|
|
1013
|
+
successRate,
|
|
1014
|
+
failures,
|
|
1015
|
+
failPhases,
|
|
1016
|
+
errorPatterns,
|
|
1017
|
+
recs,
|
|
1018
|
+
outcomes
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
console.log("");
|
|
1022
|
+
});
|
|
632
1023
|
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
1024
|
const statusPath = join(
|
|
634
1025
|
process.cwd(),
|
|
@@ -694,6 +1085,7 @@ function createConductorCommands() {
|
|
|
694
1085
|
'Agent mode: "cli" (claude -p, session auth) or "adapter" (JSON-RPC, API key)',
|
|
695
1086
|
"cli"
|
|
696
1087
|
).action(async (options) => {
|
|
1088
|
+
ensureDefaultPromptTemplate();
|
|
697
1089
|
const conductor = new Conductor({
|
|
698
1090
|
teamId: options.team,
|
|
699
1091
|
activeStates: options.states.split(",").map((s) => s.trim()),
|
|
@@ -721,27 +1113,7 @@ function createConductorCommands() {
|
|
|
721
1113
|
let refreshInterval = interval;
|
|
722
1114
|
let phaseFilter = options.phase || null;
|
|
723
1115
|
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
|
-
);
|
|
1116
|
+
const statuses = scanAgentStatuses();
|
|
745
1117
|
if (phaseFilter) {
|
|
746
1118
|
return statuses.filter((s) => s.phase === phaseFilter);
|
|
747
1119
|
}
|
|
@@ -754,9 +1126,7 @@ function createConductorCommands() {
|
|
|
754
1126
|
return;
|
|
755
1127
|
}
|
|
756
1128
|
for (const s of statuses) {
|
|
757
|
-
const elapsed
|
|
758
|
-
const stale = elapsed > 5 * 60 * 1e3;
|
|
759
|
-
const alive = isProcessAlive(s.pid);
|
|
1129
|
+
const { elapsed, alive, stale } = enrichStatus(s);
|
|
760
1130
|
const prog = phaseProgress(s.phase, s.toolCalls, stale, alive);
|
|
761
1131
|
const bar = progressBar(prog.pct, 10);
|
|
762
1132
|
const timeColor = !alive ? c.red : stale ? c.orange : c.gray;
|
|
@@ -788,9 +1158,7 @@ function createConductorCommands() {
|
|
|
788
1158
|
return;
|
|
789
1159
|
}
|
|
790
1160
|
for (const s of statuses) {
|
|
791
|
-
const elapsed
|
|
792
|
-
const stale = elapsed > 5 * 60 * 1e3;
|
|
793
|
-
const alive = isProcessAlive(s.pid);
|
|
1161
|
+
const { elapsed, alive, stale } = enrichStatus(s);
|
|
794
1162
|
const prog = phaseProgress(s.phase, s.toolCalls, stale, alive);
|
|
795
1163
|
console.log(
|
|
796
1164
|
` ${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 +1181,10 @@ function createConductorCommands() {
|
|
|
813
1181
|
console.log("");
|
|
814
1182
|
}
|
|
815
1183
|
}
|
|
1184
|
+
const cachedConductor = new Conductor({ repoRoot: process.cwd() });
|
|
816
1185
|
async function getUsage() {
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
return conductor.getUsageSummary();
|
|
1186
|
+
await cachedConductor.scanUsageLogs();
|
|
1187
|
+
return cachedConductor.getUsageSummary();
|
|
820
1188
|
}
|
|
821
1189
|
async function render() {
|
|
822
1190
|
process.stdout.write("\x1B[2J\x1B[H");
|
|
@@ -867,7 +1235,7 @@ function createConductorCommands() {
|
|
|
867
1235
|
}
|
|
868
1236
|
console.log("");
|
|
869
1237
|
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}`
|
|
1238
|
+
`${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
1239
|
);
|
|
872
1240
|
}
|
|
873
1241
|
await render();
|
|
@@ -876,14 +1244,14 @@ function createConductorCommands() {
|
|
|
876
1244
|
if (!paused) await render();
|
|
877
1245
|
}, refreshInterval);
|
|
878
1246
|
await new Promise((resolve) => {
|
|
879
|
-
|
|
880
|
-
clearInterval(timer);
|
|
881
|
-
resolve();
|
|
882
|
-
});
|
|
883
|
-
process.on("SIGTERM", () => {
|
|
1247
|
+
const cleanup = () => {
|
|
884
1248
|
clearInterval(timer);
|
|
1249
|
+
process.removeListener("SIGINT", cleanup);
|
|
1250
|
+
process.removeListener("SIGTERM", cleanup);
|
|
885
1251
|
resolve();
|
|
886
|
-
}
|
|
1252
|
+
};
|
|
1253
|
+
process.on("SIGINT", cleanup);
|
|
1254
|
+
process.on("SIGTERM", cleanup);
|
|
887
1255
|
});
|
|
888
1256
|
return;
|
|
889
1257
|
}
|
|
@@ -996,16 +1364,15 @@ function createConductorCommands() {
|
|
|
996
1364
|
}
|
|
997
1365
|
});
|
|
998
1366
|
await new Promise((resolve) => {
|
|
999
|
-
|
|
1367
|
+
const cleanup = () => {
|
|
1000
1368
|
if (refreshTimer) clearTimeout(refreshTimer);
|
|
1001
1369
|
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
1370
|
+
process.removeListener("SIGINT", cleanup);
|
|
1371
|
+
process.removeListener("SIGTERM", cleanup);
|
|
1002
1372
|
resolve();
|
|
1003
|
-
}
|
|
1004
|
-
process.on("
|
|
1005
|
-
|
|
1006
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
1007
|
-
resolve();
|
|
1008
|
-
});
|
|
1373
|
+
};
|
|
1374
|
+
process.on("SIGINT", cleanup);
|
|
1375
|
+
process.on("SIGTERM", cleanup);
|
|
1009
1376
|
});
|
|
1010
1377
|
});
|
|
1011
1378
|
return cmd;
|