@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.
Files changed (40) hide show
  1. package/dist/src/cli/commands/orchestrate.js +506 -33
  2. package/dist/src/cli/commands/orchestrator.js +208 -31
  3. package/dist/src/daemon/daemon-config.js +2 -3
  4. package/dist/src/daemon/session-daemon.js +4 -5
  5. package/dist/src/daemon/unified-daemon.js +4 -5
  6. package/dist/src/features/sweep/sweep-server-manager.js +3 -6
  7. package/dist/src/hooks/daemon.js +5 -8
  8. package/dist/src/utils/process-cleanup.js +9 -0
  9. package/package.json +1 -2
  10. package/scripts/gepa/.before-optimize.md +0 -112
  11. package/scripts/gepa/README.md +0 -275
  12. package/scripts/gepa/config.json +0 -59
  13. package/scripts/gepa/evals/coding-tasks.jsonl +0 -5
  14. package/scripts/gepa/evals/fixtures/api-endpoint.ts +0 -31
  15. package/scripts/gepa/evals/fixtures/brittle-integration.ts +0 -38
  16. package/scripts/gepa/evals/fixtures/buggy-loop.js +0 -18
  17. package/scripts/gepa/evals/fixtures/callback-hell.js +0 -53
  18. package/scripts/gepa/evals/fixtures/fts5-triggers.sql +0 -23
  19. package/scripts/gepa/evals/fixtures/leaky-service.ts +0 -70
  20. package/scripts/gepa/evals/fixtures/mcp-dispatch-stub.ts +0 -39
  21. package/scripts/gepa/evals/fixtures/pr-diff.txt +0 -24
  22. package/scripts/gepa/evals/fixtures/unsafe-webhook.ts +0 -34
  23. package/scripts/gepa/evals/fixtures/unwrapped-db-op.ts +0 -42
  24. package/scripts/gepa/evals/stackmemory-tasks.jsonl +0 -8
  25. package/scripts/gepa/generations/gen-000/baseline.md +0 -112
  26. package/scripts/gepa/generations/gen-001/baseline.md +0 -112
  27. package/scripts/gepa/generations/gen-001/variant-a.md +0 -107
  28. package/scripts/gepa/generations/gen-001/variant-b.md +0 -216
  29. package/scripts/gepa/generations/gen-001/variant-c.md +0 -83
  30. package/scripts/gepa/generations/gen-001/variant-d.md +0 -90
  31. package/scripts/gepa/hooks/auto-optimize.js +0 -494
  32. package/scripts/gepa/hooks/eval-tracker.js +0 -203
  33. package/scripts/gepa/hooks/reflect.js +0 -350
  34. package/scripts/gepa/optimize.js +0 -853
  35. package/scripts/gepa/results/eval-1-baseline.json +0 -218
  36. package/scripts/gepa/results/eval-1-variant-a.json +0 -218
  37. package/scripts/gepa/results/eval-1-variant-b.json +0 -218
  38. package/scripts/gepa/results/eval-1-variant-c.json +0 -218
  39. package/scripts/gepa/results/eval-1-variant-d.json +0 -218
  40. 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 log = execSync("git log origin/main..HEAD --oneline", {
824
+ const ref = execSync("git symbolic-ref refs/remotes/origin/HEAD", {
543
825
  cwd: s.workspacePath,
544
826
  encoding: "utf-8",
545
- timeout: 1e4
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).action(async (options) => {
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
- process.on("SIGINT", () => {
1493
+ const cleanup = () => {
1021
1494
  clearInterval(timer);
1495
+ process.removeListener("SIGINT", cleanup);
1496
+ process.removeListener("SIGTERM", cleanup);
1022
1497
  resolve();
1023
- });
1024
- process.on("SIGTERM", () => {
1025
- clearInterval(timer);
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
- process.on("SIGINT", () => {
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
  };