@stackmemoryai/stackmemory 1.5.9 → 1.6.1
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 +506 -33
- package/dist/src/cli/commands/orchestrator.js +208 -31
- 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 -2
- package/scripts/gepa/.before-optimize.md +0 -112
- package/scripts/gepa/README.md +0 -275
- package/scripts/gepa/config.json +0 -59
- package/scripts/gepa/evals/coding-tasks.jsonl +0 -5
- package/scripts/gepa/evals/fixtures/api-endpoint.ts +0 -31
- package/scripts/gepa/evals/fixtures/brittle-integration.ts +0 -38
- package/scripts/gepa/evals/fixtures/buggy-loop.js +0 -18
- package/scripts/gepa/evals/fixtures/callback-hell.js +0 -53
- package/scripts/gepa/evals/fixtures/fts5-triggers.sql +0 -23
- package/scripts/gepa/evals/fixtures/leaky-service.ts +0 -70
- package/scripts/gepa/evals/fixtures/mcp-dispatch-stub.ts +0 -39
- package/scripts/gepa/evals/fixtures/pr-diff.txt +0 -24
- package/scripts/gepa/evals/fixtures/unsafe-webhook.ts +0 -34
- package/scripts/gepa/evals/fixtures/unwrapped-db-op.ts +0 -42
- package/scripts/gepa/evals/stackmemory-tasks.jsonl +0 -8
- package/scripts/gepa/generations/gen-000/baseline.md +0 -112
- package/scripts/gepa/generations/gen-001/baseline.md +0 -112
- package/scripts/gepa/generations/gen-001/variant-a.md +0 -107
- package/scripts/gepa/generations/gen-001/variant-b.md +0 -216
- package/scripts/gepa/generations/gen-001/variant-c.md +0 -83
- package/scripts/gepa/generations/gen-001/variant-d.md +0 -90
- package/scripts/gepa/hooks/auto-optimize.js +0 -494
- package/scripts/gepa/hooks/eval-tracker.js +0 -203
- package/scripts/gepa/hooks/reflect.js +0 -350
- package/scripts/gepa/optimize.js +0 -853
- package/scripts/gepa/results/eval-1-baseline.json +0 -218
- package/scripts/gepa/results/eval-1-variant-a.json +0 -218
- package/scripts/gepa/results/eval-1-variant-b.json +0 -218
- package/scripts/gepa/results/eval-1-variant-c.json +0 -218
- package/scripts/gepa/results/eval-1-variant-d.json +0 -218
- package/scripts/gepa/state.json +0 -49
|
@@ -9,12 +9,14 @@ 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
22
|
getAgentStatusDir,
|
|
@@ -123,14 +125,6 @@ const phaseColor = {
|
|
|
123
125
|
testing: c.pink,
|
|
124
126
|
committing: c.green
|
|
125
127
|
};
|
|
126
|
-
function isProcessAlive(pid) {
|
|
127
|
-
try {
|
|
128
|
-
process.kill(pid, 0);
|
|
129
|
-
return true;
|
|
130
|
-
} catch {
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
128
|
function phaseProgress(phase, toolCalls, stale, alive) {
|
|
135
129
|
const basePct = {
|
|
136
130
|
reading: 10,
|
|
@@ -231,6 +225,293 @@ function printUsageSummary(u) {
|
|
|
231
225
|
` 20x (900 msgs) ${budgetBar(budgetPct20x)} ${c.d}~${fmtMinutes(mins20x)} left${c.r}`
|
|
232
226
|
);
|
|
233
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"
|
|
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 predictDifficulty(labels, description, priority, outcomes) {
|
|
279
|
+
let difficulty = "medium";
|
|
280
|
+
let confidence = 0.5;
|
|
281
|
+
const reasons = [];
|
|
282
|
+
const lowerLabels = labels.map((l) => l.toLowerCase());
|
|
283
|
+
if (outcomes.length > 0 && labels.length > 0) {
|
|
284
|
+
const matching = outcomes.filter(
|
|
285
|
+
(o) => o.labels && o.labels.some((ol) => lowerLabels.includes(ol.toLowerCase()))
|
|
286
|
+
);
|
|
287
|
+
if (matching.length >= 3) {
|
|
288
|
+
const failRate = matching.filter((o) => o.outcome === "failure").length / matching.length;
|
|
289
|
+
if (failRate > 0.6) {
|
|
290
|
+
difficulty = "hard";
|
|
291
|
+
confidence = Math.min(confidence + 0.1, 0.9);
|
|
292
|
+
reasons.push(
|
|
293
|
+
`Historical failure rate ${Math.round(failRate * 100)}% for similar labels`
|
|
294
|
+
);
|
|
295
|
+
} else if (failRate < 0.2) {
|
|
296
|
+
difficulty = "easy";
|
|
297
|
+
confidence = Math.min(confidence + 0.1, 0.9);
|
|
298
|
+
reasons.push(
|
|
299
|
+
`Historical failure rate ${Math.round(failRate * 100)}% for similar labels`
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const hasBugOrFix = lowerLabels.some(
|
|
305
|
+
(l) => l === "bug" || l === "fix" || l.includes("bugfix")
|
|
306
|
+
);
|
|
307
|
+
if (description.length < 100 && hasBugOrFix) {
|
|
308
|
+
if (difficulty !== "easy") difficulty = "easy";
|
|
309
|
+
confidence = Math.min(confidence + 0.1, 0.9);
|
|
310
|
+
reasons.push("Short description with bug/fix label suggests simple fix");
|
|
311
|
+
}
|
|
312
|
+
const hasComplexLabel = lowerLabels.some(
|
|
313
|
+
(l) => l === "feature" || l === "refactor" || l === "refactoring" || l === "architecture"
|
|
314
|
+
);
|
|
315
|
+
if (description.length > 500 || hasComplexLabel) {
|
|
316
|
+
if (difficulty !== "hard") {
|
|
317
|
+
difficulty = description.length > 500 && hasComplexLabel ? "hard" : difficulty === "easy" ? "medium" : "hard";
|
|
318
|
+
}
|
|
319
|
+
confidence = Math.min(confidence + 0.1, 0.9);
|
|
320
|
+
if (description.length > 500) {
|
|
321
|
+
reasons.push(
|
|
322
|
+
`Long description (${description.length} chars) suggests complexity`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
if (hasComplexLabel) {
|
|
326
|
+
reasons.push("Feature/refactor label suggests higher complexity");
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (priority === 1 || priority === 2) {
|
|
330
|
+
if (difficulty === "easy") difficulty = "medium";
|
|
331
|
+
else if (difficulty === "medium") difficulty = "hard";
|
|
332
|
+
confidence = Math.min(confidence + 0.1, 0.9);
|
|
333
|
+
reasons.push(
|
|
334
|
+
`Priority ${priority} (${priority === 1 ? "urgent" : "high"}) \u2014 higher difficulty expected`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
if (outcomes.length > 0 && labels.length > 0) {
|
|
338
|
+
const matching = outcomes.filter(
|
|
339
|
+
(o) => o.labels && o.labels.some((ol) => lowerLabels.includes(ol.toLowerCase()))
|
|
340
|
+
);
|
|
341
|
+
if (matching.length >= 3) {
|
|
342
|
+
const avgToolCalls = matching.reduce((s, o) => s + o.toolCalls, 0) / matching.length;
|
|
343
|
+
if (avgToolCalls > 80) {
|
|
344
|
+
difficulty = "hard";
|
|
345
|
+
confidence = Math.min(confidence + 0.1, 0.9);
|
|
346
|
+
reasons.push(
|
|
347
|
+
`Historical avg tool calls ${Math.round(avgToolCalls)} for similar labels (>80)`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (reasons.length === 0) {
|
|
353
|
+
reasons.push("No strong signals \u2014 defaulting to medium");
|
|
354
|
+
}
|
|
355
|
+
return { difficulty, confidence, reasons };
|
|
356
|
+
}
|
|
357
|
+
function loadOutcomes() {
|
|
358
|
+
const logPath = getOutcomesLogPath();
|
|
359
|
+
if (!existsSync(logPath)) return [];
|
|
360
|
+
try {
|
|
361
|
+
return readFileSync(logPath, "utf-8").trim().split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
362
|
+
} catch {
|
|
363
|
+
return [];
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function spawnClaudePrint(prompt, timeoutMs = 12e4) {
|
|
367
|
+
return new Promise((resolve, reject) => {
|
|
368
|
+
const child = cpSpawn("claude", ["--print"], {
|
|
369
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
370
|
+
env: { ...process.env }
|
|
371
|
+
});
|
|
372
|
+
let stdout = "";
|
|
373
|
+
let stderr = "";
|
|
374
|
+
let killed = false;
|
|
375
|
+
const timer = setTimeout(() => {
|
|
376
|
+
killed = true;
|
|
377
|
+
child.kill("SIGTERM");
|
|
378
|
+
}, timeoutMs);
|
|
379
|
+
child.stdout.on("data", (d) => stdout += d.toString());
|
|
380
|
+
child.stderr.on("data", (d) => stderr += d.toString());
|
|
381
|
+
child.on("close", (code) => {
|
|
382
|
+
clearTimeout(timer);
|
|
383
|
+
if (killed)
|
|
384
|
+
return reject(new Error(`claude timed out after ${timeoutMs}ms`));
|
|
385
|
+
if (code !== 0 && !stdout)
|
|
386
|
+
return reject(new Error(stderr || `claude exited ${code}`));
|
|
387
|
+
resolve(stdout);
|
|
388
|
+
});
|
|
389
|
+
child.on("error", (err) => {
|
|
390
|
+
clearTimeout(timer);
|
|
391
|
+
reject(err);
|
|
392
|
+
});
|
|
393
|
+
child.stdin.write(prompt);
|
|
394
|
+
child.stdin.end();
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
async function evolvePromptTemplate(input) {
|
|
398
|
+
const {
|
|
399
|
+
templatePath,
|
|
400
|
+
successRate,
|
|
401
|
+
failures,
|
|
402
|
+
failPhases,
|
|
403
|
+
errorPatterns,
|
|
404
|
+
recs,
|
|
405
|
+
outcomes
|
|
406
|
+
} = input;
|
|
407
|
+
let currentTemplate;
|
|
408
|
+
if (existsSync(templatePath)) {
|
|
409
|
+
currentTemplate = readFileSync(templatePath, "utf-8");
|
|
410
|
+
} else {
|
|
411
|
+
currentTemplate = DEFAULT_PROMPT_TEMPLATE;
|
|
412
|
+
const dir = join(homedir(), ".stackmemory", "conductor");
|
|
413
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
414
|
+
writeFileSync(templatePath, currentTemplate);
|
|
415
|
+
}
|
|
416
|
+
const failPhaseSummary = Object.entries(failPhases).sort((a, b) => b[1] - a[1]).map(([phase, count]) => ` - ${phase}: ${count} failures`).join("\n");
|
|
417
|
+
const errorSummary = Object.entries(errorPatterns).sort((a, b) => b[1] - a[1]).map(([pattern, count]) => ` - ${pattern}: ${count} occurrences`).join("\n");
|
|
418
|
+
const failedOutcomes = outcomes.filter((o) => o.outcome === "failure" && o.errorTail).slice(-5);
|
|
419
|
+
const errorTails = failedOutcomes.map(
|
|
420
|
+
(o) => ` [${o.issue} attempt ${o.attempt}, phase: ${o.phase}]
|
|
421
|
+
${o.errorTail}`
|
|
422
|
+
).join("\n\n");
|
|
423
|
+
const mutationPrompt = `You are optimizing a prompt template for autonomous AI coding agents managed by a conductor system.
|
|
424
|
+
|
|
425
|
+
CURRENT PROMPT TEMPLATE:
|
|
426
|
+
\`\`\`markdown
|
|
427
|
+
${currentTemplate}
|
|
428
|
+
\`\`\`
|
|
429
|
+
|
|
430
|
+
PERFORMANCE DATA:
|
|
431
|
+
- Success rate: ${successRate}%
|
|
432
|
+
- Total failures: ${failures}
|
|
433
|
+
|
|
434
|
+
FAILURE PHASE BREAKDOWN:
|
|
435
|
+
${failPhaseSummary || " (none)"}
|
|
436
|
+
|
|
437
|
+
ERROR PATTERNS:
|
|
438
|
+
${errorSummary || " (none)"}
|
|
439
|
+
|
|
440
|
+
SAMPLE ERROR TAILS FROM RECENT FAILURES:
|
|
441
|
+
${errorTails || " (none)"}
|
|
442
|
+
|
|
443
|
+
RECOMMENDATIONS FROM ANALYSIS:
|
|
444
|
+
${recs.map((r) => `- ${r}`).join("\n")}
|
|
445
|
+
|
|
446
|
+
YOUR TASK:
|
|
447
|
+
Improve the prompt template to reduce failures. Focus on:
|
|
448
|
+
1. Adding specific instructions that address the most common failure modes
|
|
449
|
+
2. Making implicit requirements explicit (lint rules, test commands, commit format)
|
|
450
|
+
3. Adding guardrails for the error patterns seen (e.g., if lint failures are common, add lint-specific instructions)
|
|
451
|
+
4. Keeping the template concise \u2014 agents work better with clear, structured prompts
|
|
452
|
+
5. Preserving all {{VARIABLE}} placeholders exactly as-is
|
|
453
|
+
|
|
454
|
+
REQUIREMENTS:
|
|
455
|
+
- Output ONLY the improved markdown template
|
|
456
|
+
- Keep all {{VARIABLE}} placeholders: {{ISSUE_ID}}, {{TITLE}}, {{DESCRIPTION}}, {{LABELS}}, {{PRIORITY}}, {{ATTEMPT}}, {{PRIOR_CONTEXT}}
|
|
457
|
+
- Do not add commentary, explanations, or markdown fences around the output
|
|
458
|
+
- Target similar length to the current template (no bloat)
|
|
459
|
+
|
|
460
|
+
OUTPUT THE IMPROVED TEMPLATE:`;
|
|
461
|
+
try {
|
|
462
|
+
console.log(
|
|
463
|
+
` ${c.d}Calling Claude to generate improved template...${c.r}`
|
|
464
|
+
);
|
|
465
|
+
const evolved = await spawnClaudePrint(mutationPrompt);
|
|
466
|
+
if (!evolved.trim()) {
|
|
467
|
+
console.log(` ${c.red}Empty response from Claude \u2014 skipping.${c.r}`);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
const requiredVars = ["{{ISSUE_ID}}", "{{TITLE}}", "{{DESCRIPTION}}"];
|
|
471
|
+
const missing = requiredVars.filter((v) => !evolved.includes(v));
|
|
472
|
+
if (missing.length > 0) {
|
|
473
|
+
console.log(
|
|
474
|
+
` ${c.red}Evolved template missing variables: ${missing.join(", ")} \u2014 skipping.${c.r}`
|
|
475
|
+
);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
479
|
+
const backupPath = templatePath.replace(".md", `.backup-${timestamp}.md`);
|
|
480
|
+
copyFileSync(templatePath, backupPath);
|
|
481
|
+
console.log(` ${c.d}Backed up to ${backupPath}${c.r}`);
|
|
482
|
+
writeFileSync(templatePath, evolved.trim() + "\n");
|
|
483
|
+
console.log(
|
|
484
|
+
` ${c.green}Evolved template written to ${templatePath}${c.r}`
|
|
485
|
+
);
|
|
486
|
+
const evolutionLog = join(
|
|
487
|
+
homedir(),
|
|
488
|
+
".stackmemory",
|
|
489
|
+
"conductor",
|
|
490
|
+
"evolution-log.jsonl"
|
|
491
|
+
);
|
|
492
|
+
const entry = {
|
|
493
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
494
|
+
successRate,
|
|
495
|
+
failures,
|
|
496
|
+
failPhases,
|
|
497
|
+
errorPatterns,
|
|
498
|
+
backupPath
|
|
499
|
+
};
|
|
500
|
+
const dir = join(homedir(), ".stackmemory", "conductor");
|
|
501
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
502
|
+
writeFileSync(
|
|
503
|
+
evolutionLog,
|
|
504
|
+
(existsSync(evolutionLog) ? readFileSync(evolutionLog, "utf-8") : "") + JSON.stringify(entry) + "\n"
|
|
505
|
+
);
|
|
506
|
+
console.log(` ${c.d}Evolution logged to ${evolutionLog}${c.r}`);
|
|
507
|
+
} catch (err) {
|
|
508
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
509
|
+
console.log(` ${c.red}Evolution failed: ${msg}${c.r}`);
|
|
510
|
+
console.log(
|
|
511
|
+
` ${c.d}Tip: Ensure 'claude' CLI is available and authenticated.${c.r}`
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
234
515
|
function createConductorCommands() {
|
|
235
516
|
const cmd = new Command("conductor");
|
|
236
517
|
cmd.description("Conductor \u2014 autonomous agent orchestration via Linear");
|
|
@@ -538,12 +819,25 @@ function createConductorCommands() {
|
|
|
538
819
|
const elapsedStr = formatElapsed(s.elapsed).replace(" ago", "");
|
|
539
820
|
let hasCommits = false;
|
|
540
821
|
if (s.workspacePath && existsSync(s.workspacePath)) {
|
|
822
|
+
let baseBranch = "main";
|
|
541
823
|
try {
|
|
542
|
-
const
|
|
824
|
+
const ref = execSync("git symbolic-ref refs/remotes/origin/HEAD", {
|
|
543
825
|
cwd: s.workspacePath,
|
|
544
826
|
encoding: "utf-8",
|
|
545
|
-
timeout:
|
|
546
|
-
});
|
|
827
|
+
timeout: 5e3
|
|
828
|
+
}).trim();
|
|
829
|
+
baseBranch = ref.replace("refs/remotes/origin/", "");
|
|
830
|
+
} catch {
|
|
831
|
+
}
|
|
832
|
+
try {
|
|
833
|
+
const log = execSync(
|
|
834
|
+
`git log origin/${baseBranch}..HEAD --oneline`,
|
|
835
|
+
{
|
|
836
|
+
cwd: s.workspacePath,
|
|
837
|
+
encoding: "utf-8",
|
|
838
|
+
timeout: 1e4
|
|
839
|
+
}
|
|
840
|
+
);
|
|
547
841
|
hasCommits = log.trim().length > 0;
|
|
548
842
|
} catch {
|
|
549
843
|
}
|
|
@@ -600,20 +894,30 @@ function createConductorCommands() {
|
|
|
600
894
|
const lines = parseInt(options.lines, 10);
|
|
601
895
|
const args = options.follow ? ["-f", "-n", String(lines), logPath] : ["-n", String(lines), logPath];
|
|
602
896
|
const tail = cpSpawn("tail", args, { stdio: "inherit" });
|
|
897
|
+
const forward = () => {
|
|
898
|
+
tail.kill("SIGTERM");
|
|
899
|
+
};
|
|
900
|
+
process.on("SIGINT", forward);
|
|
901
|
+
process.on("SIGTERM", forward);
|
|
603
902
|
await new Promise((resolve) => {
|
|
604
903
|
tail.on("close", () => {
|
|
904
|
+
process.removeListener("SIGINT", forward);
|
|
905
|
+
process.removeListener("SIGTERM", forward);
|
|
605
906
|
resolve();
|
|
606
907
|
});
|
|
607
|
-
const forward = () => {
|
|
608
|
-
tail.kill("SIGTERM");
|
|
609
|
-
};
|
|
610
|
-
process.on("SIGINT", forward);
|
|
611
|
-
process.on("SIGTERM", forward);
|
|
612
908
|
});
|
|
613
909
|
});
|
|
614
910
|
cmd.command("learn").description(
|
|
615
911
|
"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).
|
|
912
|
+
).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(
|
|
913
|
+
"--evolve",
|
|
914
|
+
"Auto-mutate prompt template using GEPA-style evolution from failure data",
|
|
915
|
+
false
|
|
916
|
+
).option(
|
|
917
|
+
"--predict",
|
|
918
|
+
"Show difficulty predictions alongside actual outcomes",
|
|
919
|
+
false
|
|
920
|
+
).action(async (options) => {
|
|
617
921
|
const logPath = getOutcomesLogPath();
|
|
618
922
|
if (!existsSync(logPath)) {
|
|
619
923
|
console.log(
|
|
@@ -792,8 +1096,171 @@ function createConductorCommands() {
|
|
|
792
1096
|
console.log(`
|
|
793
1097
|
${c.d}Using custom template: ${templatePath}${c.r}`);
|
|
794
1098
|
}
|
|
1099
|
+
if (options.evolve) {
|
|
1100
|
+
console.log(`
|
|
1101
|
+
${c.b}${c.cyan}Evolving prompt template...${c.r}
|
|
1102
|
+
`);
|
|
1103
|
+
await evolvePromptTemplate({
|
|
1104
|
+
templatePath,
|
|
1105
|
+
successRate,
|
|
1106
|
+
failures,
|
|
1107
|
+
failPhases,
|
|
1108
|
+
errorPatterns,
|
|
1109
|
+
recs,
|
|
1110
|
+
outcomes
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
if (options.predict) {
|
|
1114
|
+
console.log(
|
|
1115
|
+
`
|
|
1116
|
+
${c.b}${c.purple}Difficulty Predictions vs Actual${c.r}
|
|
1117
|
+
`
|
|
1118
|
+
);
|
|
1119
|
+
const byIssue = /* @__PURE__ */ new Map();
|
|
1120
|
+
for (const o of outcomes) {
|
|
1121
|
+
byIssue.set(o.issue, o);
|
|
1122
|
+
}
|
|
1123
|
+
const difficultyColor = {
|
|
1124
|
+
easy: c.green,
|
|
1125
|
+
medium: c.yellow,
|
|
1126
|
+
hard: c.red
|
|
1127
|
+
};
|
|
1128
|
+
for (const [issue, outcome] of byIssue) {
|
|
1129
|
+
const issueLabels = outcome.labels || [];
|
|
1130
|
+
const otherOutcomes = outcomes.filter((o) => o.issue !== issue);
|
|
1131
|
+
const pred = predictDifficulty(
|
|
1132
|
+
issueLabels,
|
|
1133
|
+
"",
|
|
1134
|
+
// no description in outcome data
|
|
1135
|
+
0,
|
|
1136
|
+
// no priority in outcome data
|
|
1137
|
+
otherOutcomes
|
|
1138
|
+
);
|
|
1139
|
+
const actualDifficulty = outcome.outcome === "success" && outcome.toolCalls < 40 ? "easy" : outcome.outcome === "failure" || outcome.toolCalls > 80 ? "hard" : "medium";
|
|
1140
|
+
const match = pred.difficulty === actualDifficulty;
|
|
1141
|
+
const matchIcon = match ? `${c.green}\u2713${c.r}` : `${c.red}\u2717${c.r}`;
|
|
1142
|
+
console.log(
|
|
1143
|
+
` ${matchIcon} ${c.white}${issue.padEnd(12)}${c.r} predicted: ${difficultyColor[pred.difficulty]}${pred.difficulty.padEnd(6)}${c.r} actual: ${difficultyColor[actualDifficulty]}${actualDifficulty.padEnd(6)}${c.r} ${c.gray}(${Math.round(pred.confidence * 100)}% conf)${c.r}`
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
const issueList = [...byIssue.values()];
|
|
1147
|
+
const correct = issueList.filter((o) => {
|
|
1148
|
+
const otherOutcomes = outcomes.filter((oo) => oo.issue !== o.issue);
|
|
1149
|
+
const pred = predictDifficulty(o.labels || [], "", 0, otherOutcomes);
|
|
1150
|
+
const actual = o.outcome === "success" && o.toolCalls < 40 ? "easy" : o.outcome === "failure" || o.toolCalls > 80 ? "hard" : "medium";
|
|
1151
|
+
return pred.difficulty === actual;
|
|
1152
|
+
}).length;
|
|
1153
|
+
const accuracy = Math.round(correct / issueList.length * 100);
|
|
1154
|
+
console.log(
|
|
1155
|
+
`
|
|
1156
|
+
${c.b}Accuracy${c.r}: ${accuracy}% (${correct}/${issueList.length})`
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
795
1159
|
console.log("");
|
|
796
1160
|
});
|
|
1161
|
+
cmd.command("predict [issue-id]").description("Predict difficulty for an issue based on historical outcomes").option("--title <title>", "Issue title (for testing without Linear)").option(
|
|
1162
|
+
"--labels <labels>",
|
|
1163
|
+
"Comma-separated labels (for testing without Linear)"
|
|
1164
|
+
).option("--priority <n>", "Priority 0-4 (for testing without Linear)", "0").option("--json", "Output as JSON", false).action(async (issueId, options) => {
|
|
1165
|
+
let title = options.title || "";
|
|
1166
|
+
let labels = options.labels ? options.labels.split(",").map((l) => l.trim()) : [];
|
|
1167
|
+
let description = "";
|
|
1168
|
+
let priority = parseInt(options.priority, 10) || 0;
|
|
1169
|
+
if (issueId && !options.title && !options.labels) {
|
|
1170
|
+
try {
|
|
1171
|
+
const { LinearClient } = await import("../../integrations/linear/client.js");
|
|
1172
|
+
const { LinearAuthManager } = await import("../../integrations/linear/auth.js");
|
|
1173
|
+
let client;
|
|
1174
|
+
try {
|
|
1175
|
+
const authManager = new LinearAuthManager(process.cwd());
|
|
1176
|
+
const token = await authManager.getValidToken();
|
|
1177
|
+
client = new LinearClient({ apiKey: token, useBearer: true });
|
|
1178
|
+
} catch {
|
|
1179
|
+
const apiKey = process.env.LINEAR_API_KEY;
|
|
1180
|
+
if (!apiKey) {
|
|
1181
|
+
console.log(
|
|
1182
|
+
`${c.red}No Linear auth found.${c.r} Use --title/--labels/--priority for testing.`
|
|
1183
|
+
);
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
client = new LinearClient({ apiKey });
|
|
1187
|
+
}
|
|
1188
|
+
const issue = await client.getIssue(issueId);
|
|
1189
|
+
if (!issue) {
|
|
1190
|
+
console.log(`${c.red}Issue ${issueId} not found.${c.r}`);
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
title = issue.title;
|
|
1194
|
+
description = issue.description || "";
|
|
1195
|
+
labels = issue.labels.map((l) => l.name);
|
|
1196
|
+
priority = issue.priority;
|
|
1197
|
+
} catch (err) {
|
|
1198
|
+
console.log(
|
|
1199
|
+
`${c.red}Failed to fetch issue:${c.r} ${err.message}`
|
|
1200
|
+
);
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
if (!issueId && !options.title) {
|
|
1205
|
+
console.log(
|
|
1206
|
+
`${c.yellow}Provide an issue ID or --title/--labels for testing.${c.r}`
|
|
1207
|
+
);
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
const outcomes = loadOutcomes();
|
|
1211
|
+
const pred = predictDifficulty(labels, description, priority, outcomes);
|
|
1212
|
+
if (options.json) {
|
|
1213
|
+
console.log(
|
|
1214
|
+
JSON.stringify(
|
|
1215
|
+
{
|
|
1216
|
+
issueId: issueId || "inline",
|
|
1217
|
+
title,
|
|
1218
|
+
labels,
|
|
1219
|
+
priority,
|
|
1220
|
+
...pred
|
|
1221
|
+
},
|
|
1222
|
+
null,
|
|
1223
|
+
2
|
|
1224
|
+
)
|
|
1225
|
+
);
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
const difficultyColor = {
|
|
1229
|
+
easy: c.green,
|
|
1230
|
+
medium: c.yellow,
|
|
1231
|
+
hard: c.red
|
|
1232
|
+
};
|
|
1233
|
+
console.log(`
|
|
1234
|
+
${c.b}${c.purple}Difficulty Prediction${c.r}
|
|
1235
|
+
`);
|
|
1236
|
+
if (title) {
|
|
1237
|
+
console.log(` ${c.b}Issue${c.r} ${issueId || "inline"}`);
|
|
1238
|
+
console.log(` ${c.b}Title${c.r} ${title}`);
|
|
1239
|
+
}
|
|
1240
|
+
if (labels.length > 0) {
|
|
1241
|
+
console.log(` ${c.b}Labels${c.r} ${labels.join(", ")}`);
|
|
1242
|
+
}
|
|
1243
|
+
if (priority > 0) {
|
|
1244
|
+
console.log(` ${c.b}Priority${c.r} ${priority}`);
|
|
1245
|
+
}
|
|
1246
|
+
console.log(
|
|
1247
|
+
`
|
|
1248
|
+
${c.b}Difficulty${c.r} ${difficultyColor[pred.difficulty]}${pred.difficulty.toUpperCase()}${c.r}`
|
|
1249
|
+
);
|
|
1250
|
+
console.log(
|
|
1251
|
+
` ${c.b}Confidence${c.r} ${Math.round(pred.confidence * 100)}%`
|
|
1252
|
+
);
|
|
1253
|
+
console.log(`
|
|
1254
|
+
${c.b}Signals${c.r}`);
|
|
1255
|
+
for (const reason of pred.reasons) {
|
|
1256
|
+
console.log(` ${c.cyan}\u2192${c.r} ${reason}`);
|
|
1257
|
+
}
|
|
1258
|
+
console.log(
|
|
1259
|
+
`
|
|
1260
|
+
${c.d}Based on ${outcomes.length} historical outcomes${c.r}
|
|
1261
|
+
`
|
|
1262
|
+
);
|
|
1263
|
+
});
|
|
797
1264
|
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) => {
|
|
798
1265
|
const statusPath = join(
|
|
799
1266
|
process.cwd(),
|
|
@@ -858,7 +1325,12 @@ function createConductorCommands() {
|
|
|
858
1325
|
"--mode <mode>",
|
|
859
1326
|
'Agent mode: "cli" (claude -p, session auth) or "adapter" (JSON-RPC, API key)',
|
|
860
1327
|
"cli"
|
|
1328
|
+
).option(
|
|
1329
|
+
"--model <model>",
|
|
1330
|
+
'Model routing: "auto" (complexity-based) or a specific model ID',
|
|
1331
|
+
"auto"
|
|
861
1332
|
).action(async (options) => {
|
|
1333
|
+
ensureDefaultPromptTemplate();
|
|
862
1334
|
const conductor = new Conductor({
|
|
863
1335
|
teamId: options.team,
|
|
864
1336
|
activeStates: options.states.split(",").map((s) => s.trim()),
|
|
@@ -871,7 +1343,8 @@ function createConductorCommands() {
|
|
|
871
1343
|
baseBranch: options.branch,
|
|
872
1344
|
maxRetries: parseInt(options.retries, 10),
|
|
873
1345
|
turnTimeoutMs: parseInt(options.turnTimeout, 10),
|
|
874
|
-
agentMode: options.mode === "adapter" ? "adapter" : "cli"
|
|
1346
|
+
agentMode: options.mode === "adapter" ? "adapter" : "cli",
|
|
1347
|
+
model: options.model
|
|
875
1348
|
});
|
|
876
1349
|
await conductor.start();
|
|
877
1350
|
});
|
|
@@ -1017,14 +1490,14 @@ function createConductorCommands() {
|
|
|
1017
1490
|
if (!paused) await render();
|
|
1018
1491
|
}, refreshInterval);
|
|
1019
1492
|
await new Promise((resolve) => {
|
|
1020
|
-
|
|
1493
|
+
const cleanup = () => {
|
|
1021
1494
|
clearInterval(timer);
|
|
1495
|
+
process.removeListener("SIGINT", cleanup);
|
|
1496
|
+
process.removeListener("SIGTERM", cleanup);
|
|
1022
1497
|
resolve();
|
|
1023
|
-
}
|
|
1024
|
-
process.on("
|
|
1025
|
-
|
|
1026
|
-
resolve();
|
|
1027
|
-
});
|
|
1498
|
+
};
|
|
1499
|
+
process.on("SIGINT", cleanup);
|
|
1500
|
+
process.on("SIGTERM", cleanup);
|
|
1028
1501
|
});
|
|
1029
1502
|
return;
|
|
1030
1503
|
}
|
|
@@ -1137,21 +1610,21 @@ function createConductorCommands() {
|
|
|
1137
1610
|
}
|
|
1138
1611
|
});
|
|
1139
1612
|
await new Promise((resolve) => {
|
|
1140
|
-
|
|
1141
|
-
if (refreshTimer) clearTimeout(refreshTimer);
|
|
1142
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
1143
|
-
resolve();
|
|
1144
|
-
});
|
|
1145
|
-
process.on("SIGTERM", () => {
|
|
1613
|
+
const cleanup = () => {
|
|
1146
1614
|
if (refreshTimer) clearTimeout(refreshTimer);
|
|
1147
1615
|
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
1616
|
+
process.removeListener("SIGINT", cleanup);
|
|
1617
|
+
process.removeListener("SIGTERM", cleanup);
|
|
1148
1618
|
resolve();
|
|
1149
|
-
}
|
|
1619
|
+
};
|
|
1620
|
+
process.on("SIGINT", cleanup);
|
|
1621
|
+
process.on("SIGTERM", cleanup);
|
|
1150
1622
|
});
|
|
1151
1623
|
});
|
|
1152
1624
|
return cmd;
|
|
1153
1625
|
}
|
|
1154
1626
|
export {
|
|
1155
1627
|
createConductorCommands,
|
|
1156
|
-
formatElapsed
|
|
1628
|
+
formatElapsed,
|
|
1629
|
+
predictDifficulty
|
|
1157
1630
|
};
|