@stackmemoryai/stackmemory 1.10.5 → 1.12.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.
Files changed (92) hide show
  1. package/README.md +104 -23
  2. package/dist/src/cli/claude-sm.js +266 -84
  3. package/dist/src/cli/codex-sm.js +185 -33
  4. package/dist/src/cli/commands/bench.js +209 -2
  5. package/dist/src/cli/commands/cache.js +126 -0
  6. package/dist/src/cli/commands/daemon.js +41 -0
  7. package/dist/src/cli/commands/handoff.js +40 -9
  8. package/dist/src/cli/commands/onboard.js +70 -3
  9. package/dist/src/cli/commands/optimize.js +117 -0
  10. package/dist/src/cli/commands/orchestrate.js +230 -5
  11. package/dist/src/cli/commands/orchestrator.js +312 -24
  12. package/dist/src/cli/commands/pack.js +322 -0
  13. package/dist/src/cli/commands/search.js +40 -1
  14. package/dist/src/cli/commands/setup.js +177 -7
  15. package/dist/src/cli/commands/skills.js +10 -1
  16. package/dist/src/cli/commands/state.js +265 -0
  17. package/dist/src/cli/gemini-sm.js +19 -29
  18. package/dist/src/cli/index.js +90 -29
  19. package/dist/src/cli/opencode-sm.js +38 -21
  20. package/dist/src/cli/utils/determinism-watcher.js +66 -0
  21. package/dist/src/cli/utils/real-cli-bin.js +44 -0
  22. package/dist/src/core/cache/content-cache.js +238 -0
  23. package/dist/src/core/cache/index.js +11 -0
  24. package/dist/src/core/cache/token-estimator.js +16 -0
  25. package/dist/src/core/context/frame-database.js +38 -30
  26. package/dist/src/core/cross-search/cross-project-search.js +269 -0
  27. package/dist/src/core/{merge → cross-search}/index.js +6 -4
  28. package/dist/src/core/database/sqlite-adapter.js +0 -83
  29. package/dist/src/core/extensions/provider-adapter.js +5 -0
  30. package/dist/src/core/models/model-router.js +22 -2
  31. package/dist/src/core/monitoring/logger.js +2 -1
  32. package/dist/src/core/optimization/trace-optimizer.js +413 -0
  33. package/dist/src/core/provenance/confidence-scorer.js +128 -0
  34. package/dist/src/core/provenance/index.js +40 -0
  35. package/dist/src/core/provenance/provenance-store.js +194 -0
  36. package/dist/src/core/provenance/types.js +82 -0
  37. package/dist/src/core/session/project-handoff.js +64 -0
  38. package/dist/src/core/session/session-manager.js +28 -0
  39. package/dist/src/core/shared-state/canonical-store.js +564 -0
  40. package/dist/src/core/skill-packs/index.js +18 -0
  41. package/dist/src/core/skill-packs/parser.js +42 -0
  42. package/dist/src/core/skill-packs/registry.js +224 -0
  43. package/dist/src/core/skill-packs/types.js +66 -0
  44. package/dist/src/core/trace/trace-event-store.js +282 -0
  45. package/dist/src/core/trace/trace-event.js +4 -0
  46. package/dist/src/daemon/daemon-config.js +7 -0
  47. package/dist/src/daemon/services/github-service.js +126 -0
  48. package/dist/src/daemon/unified-daemon.js +30 -0
  49. package/dist/src/features/sweep/pty-wrapper.js +13 -5
  50. package/dist/src/hooks/schemas.js +2 -0
  51. package/dist/src/integrations/claude-code/subagent-client.js +89 -0
  52. package/dist/src/integrations/github/pr-state.js +158 -0
  53. package/dist/src/integrations/linear/client.js +4 -1
  54. package/dist/src/integrations/mcp/handlers/cross-search-handlers.js +188 -0
  55. package/dist/src/integrations/mcp/handlers/index.js +40 -59
  56. package/dist/src/integrations/mcp/server.js +425 -311
  57. package/dist/src/integrations/mcp/tool-alias-registry.js +370 -0
  58. package/dist/src/integrations/mcp/tool-definitions.js +98 -229
  59. package/dist/src/integrations/ralph/context/stackmemory-context-loader.js +3 -40
  60. package/dist/src/integrations/ralph/learning/pattern-learner.js +1 -20
  61. package/dist/src/integrations/ralph/swarm/swarm-coordinator.js +0 -2
  62. package/dist/src/mcp/stackmemory-mcp-server.js +315 -0
  63. package/dist/src/orchestrators/multimodal/determinism.js +243 -0
  64. package/dist/src/orchestrators/multimodal/harness.js +147 -77
  65. package/dist/src/orchestrators/multimodal/providers.js +44 -3
  66. package/dist/src/utils/hook-installer.js +0 -8
  67. package/package.json +10 -1
  68. package/packs/coding/python-fastapi/instructions.md +60 -0
  69. package/packs/coding/python-fastapi/pack.yaml +28 -0
  70. package/packs/coding/typescript-react/instructions.md +47 -0
  71. package/packs/coding/typescript-react/pack.yaml +28 -0
  72. package/packs/core/commands/capture.md +32 -0
  73. package/packs/core/commands/learn.md +73 -0
  74. package/packs/core/commands/next.md +36 -0
  75. package/packs/core/commands/restart.md +58 -0
  76. package/packs/core/commands/restore.md +29 -0
  77. package/packs/core/commands/start.md +57 -0
  78. package/packs/core/commands/stop.md +65 -0
  79. package/packs/core/commands/summary.md +40 -0
  80. package/packs/core/manifest.json +24 -0
  81. package/packs/ops/decision-recovery/instructions.md +65 -0
  82. package/packs/ops/decision-recovery/pack.yaml +89 -0
  83. package/dist/src/cli/commands/team.js +0 -168
  84. package/dist/src/core/context/shared-context-layer.js +0 -620
  85. package/dist/src/core/context/stack-merge-resolver.js +0 -748
  86. package/dist/src/core/merge/conflict-detector.js +0 -430
  87. package/dist/src/core/merge/resolution-engine.js +0 -557
  88. package/dist/src/core/merge/stack-diff.js +0 -531
  89. package/dist/src/core/merge/unified-merge-resolver.js +0 -302
  90. package/dist/src/integrations/mcp/handlers/cord-handlers.js +0 -397
  91. package/dist/src/integrations/mcp/handlers/team-handlers.js +0 -211
  92. /package/dist/src/core/{merge → cache}/types.js +0 -0
@@ -107,6 +107,7 @@ The daemon provides:
107
107
  const services = [];
108
108
  if (newStatus.services.context.enabled) services.push("context");
109
109
  if (newStatus.services.linear.enabled) services.push("linear");
110
+ if (newStatus.services.github?.enabled) services.push("github");
110
111
  if (newStatus.services.maintenance?.enabled)
111
112
  services.push("maintenance");
112
113
  if (newStatus.services.memory?.enabled) services.push("memory");
@@ -232,6 +233,25 @@ The daemon provides:
232
233
  console.log(chalk.gray(` Syncs: ${lin.syncCount}`));
233
234
  }
234
235
  }
236
+ const gh = status.services.github;
237
+ if (gh) {
238
+ console.log(
239
+ ` GitHub: ${gh.enabled ? chalk.green("Enabled") : chalk.gray("Disabled")}`
240
+ );
241
+ if (gh.enabled) {
242
+ console.log(
243
+ chalk.gray(` Interval: ${config.github.interval} min`)
244
+ );
245
+ if (gh.syncCount) {
246
+ console.log(chalk.gray(` Refreshes: ${gh.syncCount}`));
247
+ }
248
+ if (gh.lastProjectionState) {
249
+ console.log(
250
+ chalk.gray(` Last PR state: ${gh.lastProjectionState}`)
251
+ );
252
+ }
253
+ }
254
+ }
235
255
  const maint = status.services.maintenance;
236
256
  if (maint) {
237
257
  console.log(
@@ -605,6 +625,22 @@ function getServiceHealthChecks(status, config) {
605
625
  detail: "Disabled"
606
626
  });
607
627
  }
628
+ const gh = status.services.github;
629
+ if (gh?.enabled) {
630
+ const intervalMs = config.github.interval * 6e4;
631
+ const overdue = gh.lastRun ? Date.now() - gh.lastRun > intervalMs * 2 : false;
632
+ checks.push({
633
+ name: "GitHub Service",
634
+ status: overdue ? "warn" : "ok",
635
+ detail: gh.lastRun ? `Last refresh: ${formatTimeAgo(gh.lastRun)} | Refreshes: ${gh.syncCount ?? 0}${gh.lastProjectionState ? ` | Last PR state: ${gh.lastProjectionState}` : ""}` : `Enabled (interval: ${config.github.interval}m) | No refreshes yet`
636
+ });
637
+ } else {
638
+ checks.push({
639
+ name: "GitHub Service",
640
+ status: "ok",
641
+ detail: "Disabled"
642
+ });
643
+ }
608
644
  const maint = status.services.maintenance;
609
645
  if (maint?.enabled) {
610
646
  const intervalMs = config.maintenance.interval * 6e4;
@@ -701,6 +737,11 @@ function buildHealthReport(status, config, paths) {
701
737
  enabled: status.services.linear.enabled,
702
738
  lastRun: status.services.linear.lastRun
703
739
  },
740
+ {
741
+ key: "github",
742
+ enabled: status.services.github?.enabled ?? false,
743
+ lastRun: status.services.github?.lastRun
744
+ },
704
745
  {
705
746
  key: "maintenance",
706
747
  enabled: status.services.maintenance?.enabled ?? false,
@@ -20,6 +20,10 @@ import { FrameManager } from "../../core/context/index.js";
20
20
  import { LinearTaskManager } from "../../features/tasks/linear-task-manager.js";
21
21
  import { logger } from "../../core/monitoring/logger.js";
22
22
  import { EnhancedHandoffGenerator } from "../../core/session/handoff.js";
23
+ import {
24
+ getProjectHandoffPaths,
25
+ loadProjectHandoff
26
+ } from "../../core/session/project-handoff.js";
23
27
  const countTokens = (text) => Math.ceil(text.length / 3.5);
24
28
  const MAX_HANDOFF_VERSIONS = 10;
25
29
  function saveVersionedHandoff(projectRoot, branch, content) {
@@ -273,7 +277,7 @@ Generated by stackmemory capture at ${timestamp}
273
277
  if (!existsSync(stackmemoryDir)) {
274
278
  mkdirSync(stackmemoryDir, { recursive: true });
275
279
  }
276
- const handoffPath = join(stackmemoryDir, "last-handoff.md");
280
+ const { handoffPath, metadataPath } = getProjectHandoffPaths(projectRoot);
277
281
  writeFileSync(handoffPath, handoffPrompt);
278
282
  let branch = "unknown";
279
283
  try {
@@ -289,6 +293,18 @@ Generated by stackmemory capture at ${timestamp}
289
293
  branch,
290
294
  handoffPrompt
291
295
  );
296
+ writeFileSync(
297
+ metadataPath,
298
+ JSON.stringify(
299
+ {
300
+ branch,
301
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
302
+ projectRoot
303
+ },
304
+ null,
305
+ 2
306
+ )
307
+ );
292
308
  console.log(
293
309
  `Versioned: ${versionedPath.split("/").slice(-2).join("/")}`
294
310
  );
@@ -331,26 +347,41 @@ Generated by stackmemory capture at ${timestamp}
331
347
  }
332
348
  function createRestoreCommand() {
333
349
  const cmd = new Command("restore");
334
- cmd.description("Restore context from last handoff").option("--no-copy", "Do not copy prompt to clipboard").action(async (options) => {
350
+ cmd.description("Restore context from last handoff").option("--no-copy", "Do not copy prompt to clipboard").option("--force", "Restore even if the handoff branch does not match").action(async (options) => {
335
351
  try {
336
352
  const projectRoot = process.cwd();
337
- const handoffPath = join(
338
- projectRoot,
339
- ".stackmemory",
340
- "last-handoff.md"
341
- );
342
353
  const metaPath = join(
343
354
  process.env["HOME"] || "~",
344
355
  ".stackmemory",
345
356
  "handoffs",
346
357
  "last-handoff-meta.json"
347
358
  );
348
- if (!existsSync(handoffPath)) {
359
+ const currentBranch = (() => {
360
+ try {
361
+ return execSync("git rev-parse --abbrev-ref HEAD", {
362
+ encoding: "utf-8",
363
+ cwd: projectRoot,
364
+ stdio: ["pipe", "pipe", "pipe"]
365
+ }).trim();
366
+ } catch {
367
+ return void 0;
368
+ }
369
+ })();
370
+ const handoff = loadProjectHandoff(projectRoot, currentBranch);
371
+ if (!handoff) {
349
372
  console.log("\u274C No handoff found in this project");
350
373
  console.log('\u{1F4A1} Run "stackmemory capture" to create one');
351
374
  return;
352
375
  }
353
- const handoffPrompt = readFileSync(handoffPath, "utf-8");
376
+ if (!handoff.compatible && !options.force) {
377
+ console.log("\u26A0\uFE0F Skipping stale handoff");
378
+ console.log(` ${handoff.mismatchReason}`);
379
+ console.log(
380
+ ' Run "stackmemory restore --force" to inspect it anyway'
381
+ );
382
+ return;
383
+ }
384
+ const handoffPrompt = handoff.content;
354
385
  console.log("\n" + "=".repeat(60));
355
386
  console.log("\u{1F4CB} RESTORED HANDOFF");
356
387
  console.log("=".repeat(60));
@@ -383,6 +383,67 @@ DATABASE_URL=${config.databaseUrl}
383
383
  console.log(chalk.yellow(" \u26A0 Could not write hosted DB env file"));
384
384
  }
385
385
  }
386
+ {
387
+ const claudeCommandsDir = join(homedir(), ".claude", "commands");
388
+ const packsDir = join(__dirname, "..", "..", "..", "packs");
389
+ const coreManifest = join(packsDir, "core", "manifest.json");
390
+ if (existsSync(coreManifest)) {
391
+ const { installCommands } = await inquirer.prompt([
392
+ {
393
+ type: "confirm",
394
+ name: "installCommands",
395
+ message: "Install session commands for Claude Code? (/start, /stop, /restart, /next)",
396
+ default: true
397
+ }
398
+ ]);
399
+ if (installCommands) {
400
+ console.log(chalk.gray("Installing command pack: core..."));
401
+ try {
402
+ execFileSync(
403
+ process.execPath,
404
+ [process.argv[1], "setup-commands", "--force"],
405
+ { stdio: "pipe" }
406
+ );
407
+ console.log(
408
+ chalk.green(" \u2713 Installed /start, /stop, /restart, /next")
409
+ );
410
+ } catch {
411
+ try {
412
+ if (!existsSync(claudeCommandsDir)) {
413
+ mkdirSync(claudeCommandsDir, { recursive: true });
414
+ }
415
+ const manifest = JSON.parse(
416
+ (await import("fs")).readFileSync(coreManifest, "utf8")
417
+ );
418
+ const allCmds = [
419
+ ...manifest.commands?.public || [],
420
+ ...manifest.commands?.internal || []
421
+ ];
422
+ let count = 0;
423
+ for (const cmd of allCmds) {
424
+ const src = join(packsDir, "core", cmd.file);
425
+ const dst = join(claudeCommandsDir, `${cmd.name}.md`);
426
+ if (existsSync(src) && !existsSync(dst)) {
427
+ execFileSync("ln", ["-s", src, dst]);
428
+ count++;
429
+ }
430
+ }
431
+ if (count > 0) {
432
+ console.log(
433
+ chalk.green(` \u2713 Installed ${count} command(s)`)
434
+ );
435
+ }
436
+ } catch {
437
+ console.log(
438
+ chalk.yellow(
439
+ " \u26A0 Could not install commands. Run: stackmemory setup-commands"
440
+ )
441
+ );
442
+ }
443
+ }
444
+ }
445
+ }
446
+ }
386
447
  const binPath = "/usr/local/bin/claude-sm";
387
448
  const sourcePath = join(configPath, "bin", "stackmemory");
388
449
  try {
@@ -531,12 +592,18 @@ function showNextSteps(config) {
531
592
  )
532
593
  );
533
594
  }
534
- console.log("3. Use with Claude:");
595
+ console.log("3. Install session commands for Claude Code:");
596
+ console.log(
597
+ chalk.gray(
598
+ " stackmemory setup-commands # Installs /start, /stop, /restart, /next\n"
599
+ )
600
+ );
601
+ console.log("4. Use with Claude:");
535
602
  console.log(chalk.gray(" claude-sm # Or use stackmemory directly\n"));
536
- console.log("4. Use with Codex:");
603
+ console.log("5. Use with Codex:");
537
604
  console.log(chalk.gray(" codex-sm # Codex + StackMemory integration\n"));
538
605
  if (config.enableLinear) {
539
- console.log("5. Sync with Linear:");
606
+ console.log("6. Sync with Linear:");
540
607
  console.log(chalk.gray(" stackmemory linear sync\n"));
541
608
  }
542
609
  console.log("For more help:");
@@ -0,0 +1,117 @@
1
+ import { fileURLToPath as __fileURLToPath } from 'url';
2
+ import { dirname as __pathDirname } from 'path';
3
+ const __filename = __fileURLToPath(import.meta.url);
4
+ const __dirname = __pathDirname(__filename);
5
+ import { Command } from "commander";
6
+ import Database from "better-sqlite3";
7
+ import { existsSync } from "fs";
8
+ import { join } from "path";
9
+ import chalk from "chalk";
10
+ import { TraceStore } from "../../core/trace/trace-store.js";
11
+ import { TraceOptimizer } from "../../core/optimization/trace-optimizer.js";
12
+ function createOptimizeCommand() {
13
+ const optimize = new Command("optimize").description(
14
+ "Offline optimizers for harnesses, traces, and prompts"
15
+ );
16
+ optimize.command("traces").description(
17
+ "Analyze stored traces and generate HALO-like offline optimizer recommendations"
18
+ ).option("-d, --days <n>", "Only analyze traces from the last N days", "30").option(
19
+ "-m, --min-occurrences <n>",
20
+ "Minimum repeated occurrences before surfacing a pattern",
21
+ "2"
22
+ ).option("--json", "Print machine-readable JSON").option(
23
+ "--no-write",
24
+ "Do not persist report files under .stackmemory/build"
25
+ ).action(async (options) => {
26
+ const projectRoot = process.cwd();
27
+ const dbPath = join(projectRoot, ".stackmemory", "context.db");
28
+ if (!existsSync(dbPath)) {
29
+ console.log(
30
+ chalk.red("StackMemory not initialized in this directory.")
31
+ );
32
+ console.log(chalk.gray('Run "stackmemory init" first.'));
33
+ return;
34
+ }
35
+ const db = new Database(dbPath);
36
+ try {
37
+ const traceStore = new TraceStore(db);
38
+ const optimizer = new TraceOptimizer(traceStore);
39
+ const report = optimizer.analyze({
40
+ lookbackDays: parseInt(options.days, 10) || 30,
41
+ minOccurrences: parseInt(options.minOccurrences, 10) || 2
42
+ });
43
+ const persisted = options.write ? optimizer.persistReport(projectRoot, report) : null;
44
+ if (options.json) {
45
+ console.log(
46
+ JSON.stringify(
47
+ {
48
+ ...report,
49
+ persisted
50
+ },
51
+ null,
52
+ 2
53
+ )
54
+ );
55
+ return;
56
+ }
57
+ console.log(chalk.blue("\nTrace Optimizer Report\n"));
58
+ console.log(`Lookback window: ${report.lookbackDays} day(s)`);
59
+ console.log(`Traces analyzed: ${report.totalTracesAnalyzed}`);
60
+ console.log(`Traces with errors: ${report.tracesWithErrors}`);
61
+ console.log(`Causal traces: ${report.causalTraces}`);
62
+ console.log(
63
+ `Average tools/trace: ${report.averageToolsPerTrace.toFixed(2)}`
64
+ );
65
+ console.log(
66
+ `Average trace score: ${report.averageTraceScore.toFixed(2)}`
67
+ );
68
+ if (report.recommendations.length === 0) {
69
+ console.log(
70
+ chalk.yellow(
71
+ "\nNo repeated patterns crossed the threshold. Lower --min-occurrences or collect more traces."
72
+ )
73
+ );
74
+ } else {
75
+ console.log(chalk.blue("\nRecommendations:\n"));
76
+ for (const recommendation of report.recommendations) {
77
+ const badge = recommendation.priority === "high" ? chalk.red("[high]") : chalk.yellow("[medium]");
78
+ console.log(
79
+ `${badge} ${chalk.white(recommendation.title)} (${recommendation.confidence.toFixed(2)} confidence)`
80
+ );
81
+ console.log(` ${recommendation.summary}`);
82
+ console.log(` Targets: ${recommendation.targetAreas.join(", ")}`);
83
+ console.log(` Actions: ${recommendation.actions.join(" | ")}`);
84
+ console.log(
85
+ ` Validate: ${recommendation.validations.join(" | ")}`
86
+ );
87
+ console.log("");
88
+ }
89
+ }
90
+ if (report.clusters.length > 0) {
91
+ console.log(chalk.blue("Detected clusters:\n"));
92
+ for (const cluster of report.clusters) {
93
+ console.log(
94
+ `- ${cluster.label} (${cluster.occurrences} traces, ${cluster.kind})`
95
+ );
96
+ if (cluster.toolPatterns.length > 0) {
97
+ console.log(` Tools: ${cluster.toolPatterns.join(", ")}`);
98
+ }
99
+ if (cluster.sampleSummaries.length > 0) {
100
+ console.log(` Examples: ${cluster.sampleSummaries.join(" | ")}`);
101
+ }
102
+ }
103
+ console.log("");
104
+ }
105
+ if (persisted) {
106
+ console.log(chalk.gray(`Saved JSON: ${persisted.jsonPath}`));
107
+ console.log(chalk.gray(`Saved Markdown: ${persisted.markdownPath}`));
108
+ }
109
+ } finally {
110
+ db.close();
111
+ }
112
+ });
113
+ return optimize;
114
+ }
115
+ export {
116
+ createOptimizeCommand
117
+ };
@@ -189,13 +189,13 @@ function progressBar(pct, width) {
189
189
  return `${col}${"\u2501".repeat(filled)}${c.d}${"\u254C".repeat(empty)}${c.r}`;
190
190
  }
191
191
  function scanAgentStatuses() {
192
- const agentsDir2 = join(homedir(), ".stackmemory", "conductor", "agents");
193
- if (!existsSync(agentsDir2)) return [];
194
- const entries = readdirSync(agentsDir2, { withFileTypes: true });
192
+ const agentsDir = join(homedir(), ".stackmemory", "conductor", "agents");
193
+ if (!existsSync(agentsDir)) return [];
194
+ const entries = readdirSync(agentsDir, { withFileTypes: true });
195
195
  const statuses = [];
196
196
  for (const entry of entries) {
197
197
  if (!entry.isDirectory()) continue;
198
- const statusPath = join(agentsDir2, entry.name, "status.json");
198
+ const statusPath = join(agentsDir, entry.name, "status.json");
199
199
  if (!existsSync(statusPath)) continue;
200
200
  try {
201
201
  const data = JSON.parse(readFileSync(statusPath, "utf-8"));
@@ -953,6 +953,7 @@ function createConductorCommands() {
953
953
  console.log("");
954
954
  });
955
955
  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) => {
956
+ const agentsDir = join(homedir(), ".stackmemory", "conductor", "agents");
956
957
  const statuses = scanAgentStatuses();
957
958
  const needsFinalize = statuses.map((s) => ({ ...s, ...enrichStatus(s) })).filter((s) => {
958
959
  return !s.alive || s.elapsed > STALE_FINALIZE_THRESHOLD_MS;
@@ -1038,6 +1039,221 @@ function createConductorCommands() {
1038
1039
  );
1039
1040
  }
1040
1041
  });
1042
+ const isMerged = (repo, branch, lane) => {
1043
+ try {
1044
+ execSync(`git merge-base --is-ancestor "${branch}" "${lane}"`, {
1045
+ cwd: repo,
1046
+ stdio: "pipe",
1047
+ timeout: 5e3
1048
+ });
1049
+ return true;
1050
+ } catch (err) {
1051
+ const status = err.status;
1052
+ if (status !== 1) throw err;
1053
+ }
1054
+ try {
1055
+ const out = execSync(`git cherry "${lane}" "${branch}"`, {
1056
+ cwd: repo,
1057
+ encoding: "utf-8",
1058
+ timeout: 1e4
1059
+ }).trim();
1060
+ if (!out) return true;
1061
+ const lines = out.split("\n").filter(Boolean);
1062
+ return lines.every((l) => l.startsWith("-"));
1063
+ } catch {
1064
+ return false;
1065
+ }
1066
+ };
1067
+ const bucketLaneBranches = (repo, lane) => {
1068
+ let all = [];
1069
+ try {
1070
+ all = execSync(`git branch --list 'worktree-agent-*'`, {
1071
+ cwd: repo,
1072
+ encoding: "utf-8",
1073
+ timeout: 1e4
1074
+ }).split("\n").map((s) => s.trim().replace(/^[*+]\s*/, "")).filter(Boolean);
1075
+ } catch {
1076
+ }
1077
+ const merged = [];
1078
+ const unmerged = [];
1079
+ for (const b of all) {
1080
+ (isMerged(repo, b, lane) ? merged : unmerged).push(b);
1081
+ }
1082
+ return { merged, unmerged };
1083
+ };
1084
+ const getWorktreeCleanliness = (repo, wtPath) => {
1085
+ try {
1086
+ const out = execSync(`git -C "${wtPath}" status --short`, {
1087
+ cwd: repo,
1088
+ encoding: "utf-8",
1089
+ timeout: 5e3
1090
+ });
1091
+ return out.trim().length > 0 ? "dirty" : "clean";
1092
+ } catch {
1093
+ return "unknown";
1094
+ }
1095
+ };
1096
+ const laneCmd = cmd.command("lane").description(
1097
+ "Inspect or clean up worktree-agent-* branches against a lane"
1098
+ );
1099
+ laneCmd.command("status").description(
1100
+ "List worktree-agent-* branches and their merge state vs the lane"
1101
+ ).option(
1102
+ "--lane <branch>",
1103
+ "Lane branch to compare against (default: current branch)"
1104
+ ).option("--repo <path>", "Git repo root", process.cwd()).action((options) => {
1105
+ const repo = options.repo;
1106
+ const lane = options.lane || execSync("git branch --show-current", {
1107
+ cwd: repo,
1108
+ encoding: "utf-8"
1109
+ }).trim();
1110
+ if (!lane) {
1111
+ console.error(
1112
+ `${c.red}Could not resolve lane branch.${c.r} Pass --lane.`
1113
+ );
1114
+ process.exit(1);
1115
+ }
1116
+ const { merged, unmerged } = bucketLaneBranches(repo, lane);
1117
+ console.log(`
1118
+ ${c.b}Lane:${c.r} ${c.cyan}${lane}${c.r}
1119
+ `);
1120
+ if (merged.length) {
1121
+ console.log(` ${c.green}merged (${merged.length}):${c.r}`);
1122
+ for (const b of merged) console.log(` ${c.gray}\u2713${c.r} ${b}`);
1123
+ }
1124
+ if (unmerged.length) {
1125
+ console.log(`
1126
+ ${c.orange}unmerged (${unmerged.length}):${c.r}`);
1127
+ for (const b of unmerged) console.log(` ${c.orange}\u2022${c.r} ${b}`);
1128
+ }
1129
+ if (!merged.length && !unmerged.length) {
1130
+ console.log(` ${c.gray}No worktree-agent-* branches found.${c.r}`);
1131
+ }
1132
+ });
1133
+ laneCmd.command("cleanup").description(
1134
+ "Remove worktree-agent-* branches and worktrees already merged into the lane"
1135
+ ).option(
1136
+ "--lane <branch>",
1137
+ "Lane branch to compare against (default: current branch)"
1138
+ ).option("--repo <path>", "Git repo root", process.cwd()).option("--dry-run", "Show what would be removed without doing it", false).option(
1139
+ "--force",
1140
+ "Remove worktrees even if they have uncommitted changes",
1141
+ false
1142
+ ).action((options) => {
1143
+ const repo = options.repo;
1144
+ const lane = options.lane || execSync("git branch --show-current", {
1145
+ cwd: repo,
1146
+ encoding: "utf-8"
1147
+ }).trim();
1148
+ if (!lane) {
1149
+ console.error(
1150
+ `${c.red}Could not resolve lane branch.${c.r} Pass --lane.`
1151
+ );
1152
+ process.exit(1);
1153
+ }
1154
+ const { merged } = bucketLaneBranches(repo, lane);
1155
+ if (merged.length === 0) {
1156
+ console.log(
1157
+ `${c.green}Nothing to clean up for lane ${c.cyan}${lane}${c.r}.`
1158
+ );
1159
+ return;
1160
+ }
1161
+ const worktreeMap = /* @__PURE__ */ new Map();
1162
+ try {
1163
+ const wt = execSync("git worktree list --porcelain", {
1164
+ cwd: repo,
1165
+ encoding: "utf-8",
1166
+ timeout: 1e4
1167
+ });
1168
+ let currentPath = "";
1169
+ for (const line of wt.split("\n")) {
1170
+ if (line.startsWith("worktree ")) {
1171
+ currentPath = line.slice("worktree ".length).trim();
1172
+ } else if (line.startsWith("branch ")) {
1173
+ const ref = line.slice("branch ".length).trim();
1174
+ const b = ref.replace(/^refs\/heads\//, "");
1175
+ if (currentPath) worktreeMap.set(b, currentPath);
1176
+ }
1177
+ }
1178
+ } catch {
1179
+ }
1180
+ console.log(
1181
+ `
1182
+ ${c.b}Lane:${c.r} ${c.cyan}${lane}${c.r} ${c.d}(${merged.length} merged branches)${c.r}
1183
+ `
1184
+ );
1185
+ let skippedDirty = 0;
1186
+ let skippedUnknown = 0;
1187
+ for (const branch of merged) {
1188
+ const wtPath = worktreeMap.get(branch);
1189
+ const cleanliness = wtPath ? getWorktreeCleanliness(repo, wtPath) : "unknown";
1190
+ const dirty = cleanliness === "dirty";
1191
+ if (dirty && !options.force) {
1192
+ skippedDirty++;
1193
+ console.log(
1194
+ ` ${c.orange}\u26A0${c.r} ${c.orange}dirty${c.r} ${wtPath} ${c.d}(skipping ${branch} \u2014 pass --force to remove)${c.r}`
1195
+ );
1196
+ continue;
1197
+ }
1198
+ if (wtPath && cleanliness === "unknown" && !options.force) {
1199
+ skippedUnknown++;
1200
+ console.log(
1201
+ ` ${c.orange}\u26A0${c.r} ${c.orange}unknown${c.r} ${wtPath} ${c.d}(skipping ${branch} \u2014 could not verify clean state; pass --force to remove)${c.r}`
1202
+ );
1203
+ continue;
1204
+ }
1205
+ const dirtyTag = dirty ? ` ${c.orange}[dirty]${c.r}` : "";
1206
+ const unknownTag = cleanliness === "unknown" ? ` ${c.orange}[unknown]${c.r}` : "";
1207
+ const action = wtPath ? `${c.gray}worktree remove${c.r} ${wtPath}${dirtyTag}${unknownTag} + ${c.gray}branch -D${c.r} ${branch}` : `${c.gray}branch -D${c.r} ${branch}`;
1208
+ console.log(` ${c.green}\u2713${c.r} ${action}`);
1209
+ if (options.dryRun) continue;
1210
+ if (wtPath) {
1211
+ try {
1212
+ execSync(`git worktree remove "${wtPath}" --force`, {
1213
+ cwd: repo,
1214
+ stdio: "pipe",
1215
+ timeout: 15e3
1216
+ });
1217
+ } catch (err) {
1218
+ console.log(
1219
+ ` ${c.red}worktree remove failed:${c.r} ${err.message}`
1220
+ );
1221
+ }
1222
+ }
1223
+ try {
1224
+ execSync(`git branch -D "${branch}"`, {
1225
+ cwd: repo,
1226
+ stdio: "pipe",
1227
+ timeout: 5e3
1228
+ });
1229
+ } catch (err) {
1230
+ console.log(
1231
+ ` ${c.red}branch -D failed:${c.r} ${err.message}`
1232
+ );
1233
+ }
1234
+ }
1235
+ if (!options.dryRun) {
1236
+ try {
1237
+ execSync("git worktree prune", {
1238
+ cwd: repo,
1239
+ stdio: "pipe",
1240
+ timeout: 1e4
1241
+ });
1242
+ } catch {
1243
+ }
1244
+ const skipNote = skippedDirty ? ` ${c.orange}(${skippedDirty} dirty skipped \u2014 pass --force)${c.r}` : "";
1245
+ const unknownNote = skippedUnknown ? ` ${c.orange}(${skippedUnknown} unknown skipped \u2014 pass --force)${c.r}` : "";
1246
+ console.log(`
1247
+ ${c.green}Done.${c.r}${skipNote}${unknownNote}`);
1248
+ } else {
1249
+ const skipNote = skippedDirty ? ` ${c.orange}(${skippedDirty} dirty would be skipped \u2014 pass --force to include)${c.r}` : "";
1250
+ const unknownNote = skippedUnknown ? ` ${c.orange}(${skippedUnknown} unknown would be skipped \u2014 pass --force to include)${c.r}` : "";
1251
+ console.log(
1252
+ `
1253
+ ${c.d}Dry run \u2014 no changes made. Remove --dry-run to execute.${c.r}${skipNote}${unknownNote}`
1254
+ );
1255
+ }
1256
+ });
1041
1257
  cmd.command("logs").description("Tail agent output log").argument("<issue-id>", "Issue identifier (e.g., STA-499)").option("-f, --follow", "Follow the log (tail -f)", false).option("-n, --lines <n>", "Number of lines to show", "50").action(async (issueId, options) => {
1042
1258
  const logPath = join(getAgentStatusDir(issueId), "output.log");
1043
1259
  if (!existsSync(logPath)) {
@@ -1521,6 +1737,13 @@ function createConductorCommands() {
1521
1737
  ).option(
1522
1738
  "--no-pr",
1523
1739
  "Disable automatic GitHub PR creation after agent success"
1740
+ ).option(
1741
+ "--workspace-mode <mode>",
1742
+ 'Workspace mode: "auto" (detect GitButler), "gitbutler", or "worktree"',
1743
+ "auto"
1744
+ ).option(
1745
+ "--lane <branch>",
1746
+ "Optional lane branch. When set, conductor uses git worktrees rooted at the lane as worktree-agent-<id> and suppresses PRs (lane is human-curated)."
1524
1747
  ).action(async (options) => {
1525
1748
  ensureDefaultPromptTemplate();
1526
1749
  const conductor = new Conductor({
@@ -1537,7 +1760,9 @@ function createConductorCommands() {
1537
1760
  turnTimeoutMs: parseInt(options.turnTimeout, 10),
1538
1761
  agentMode: options.mode === "adapter" ? "adapter" : "cli",
1539
1762
  model: options.model,
1540
- autoPR: options.pr
1763
+ autoPR: options.lane ? false : options.pr,
1764
+ workspaceMode: options.workspaceMode,
1765
+ laneBranch: options.lane
1541
1766
  });
1542
1767
  await conductor.start();
1543
1768
  });