@stackmemoryai/stackmemory 1.10.4 → 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 (95) 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/commands/wiki.js +33 -0
  18. package/dist/src/cli/gemini-sm.js +19 -29
  19. package/dist/src/cli/index.js +90 -29
  20. package/dist/src/cli/opencode-sm.js +38 -21
  21. package/dist/src/cli/utils/determinism-watcher.js +66 -0
  22. package/dist/src/cli/utils/real-cli-bin.js +44 -0
  23. package/dist/src/core/cache/content-cache.js +238 -0
  24. package/dist/src/core/cache/index.js +11 -0
  25. package/dist/src/core/cache/token-estimator.js +16 -0
  26. package/dist/src/core/context/frame-database.js +38 -30
  27. package/dist/src/core/cross-search/cross-project-search.js +269 -0
  28. package/dist/src/core/{merge → cross-search}/index.js +6 -4
  29. package/dist/src/core/database/sqlite-adapter.js +0 -83
  30. package/dist/src/core/extensions/provider-adapter.js +5 -0
  31. package/dist/src/core/models/model-router.js +22 -2
  32. package/dist/src/core/monitoring/logger.js +2 -1
  33. package/dist/src/core/optimization/trace-optimizer.js +413 -0
  34. package/dist/src/core/provenance/confidence-scorer.js +128 -0
  35. package/dist/src/core/provenance/index.js +40 -0
  36. package/dist/src/core/provenance/provenance-store.js +194 -0
  37. package/dist/src/core/provenance/types.js +82 -0
  38. package/dist/src/core/session/project-handoff.js +64 -0
  39. package/dist/src/core/session/session-manager.js +28 -0
  40. package/dist/src/core/shared-state/canonical-store.js +564 -0
  41. package/dist/src/core/skill-packs/index.js +18 -0
  42. package/dist/src/core/skill-packs/parser.js +42 -0
  43. package/dist/src/core/skill-packs/registry.js +224 -0
  44. package/dist/src/core/skill-packs/types.js +66 -0
  45. package/dist/src/core/trace/trace-event-store.js +282 -0
  46. package/dist/src/core/trace/trace-event.js +4 -0
  47. package/dist/src/core/wiki/wiki-compiler.js +219 -0
  48. package/dist/src/daemon/daemon-config.js +7 -0
  49. package/dist/src/daemon/services/github-service.js +126 -0
  50. package/dist/src/daemon/unified-daemon.js +30 -0
  51. package/dist/src/features/sweep/pty-wrapper.js +13 -5
  52. package/dist/src/hooks/schemas.js +2 -0
  53. package/dist/src/integrations/claude-code/subagent-client.js +89 -0
  54. package/dist/src/integrations/github/pr-state.js +158 -0
  55. package/dist/src/integrations/linear/client.js +4 -1
  56. package/dist/src/integrations/mcp/handlers/cross-search-handlers.js +188 -0
  57. package/dist/src/integrations/mcp/handlers/index.js +40 -59
  58. package/dist/src/integrations/mcp/server.js +425 -311
  59. package/dist/src/integrations/mcp/tool-alias-registry.js +370 -0
  60. package/dist/src/integrations/mcp/tool-definitions.js +98 -229
  61. package/dist/src/integrations/ralph/context/stackmemory-context-loader.js +3 -40
  62. package/dist/src/integrations/ralph/learning/pattern-learner.js +1 -20
  63. package/dist/src/integrations/ralph/swarm/swarm-coordinator.js +0 -2
  64. package/dist/src/mcp/stackmemory-mcp-server.js +315 -0
  65. package/dist/src/orchestrators/multimodal/determinism.js +243 -0
  66. package/dist/src/orchestrators/multimodal/harness.js +147 -77
  67. package/dist/src/orchestrators/multimodal/providers.js +44 -3
  68. package/dist/src/utils/hook-installer.js +8 -8
  69. package/package.json +10 -1
  70. package/packs/coding/python-fastapi/instructions.md +60 -0
  71. package/packs/coding/python-fastapi/pack.yaml +28 -0
  72. package/packs/coding/typescript-react/instructions.md +47 -0
  73. package/packs/coding/typescript-react/pack.yaml +28 -0
  74. package/packs/core/commands/capture.md +32 -0
  75. package/packs/core/commands/learn.md +73 -0
  76. package/packs/core/commands/next.md +36 -0
  77. package/packs/core/commands/restart.md +58 -0
  78. package/packs/core/commands/restore.md +29 -0
  79. package/packs/core/commands/start.md +57 -0
  80. package/packs/core/commands/stop.md +65 -0
  81. package/packs/core/commands/summary.md +40 -0
  82. package/packs/core/manifest.json +24 -0
  83. package/packs/ops/decision-recovery/instructions.md +65 -0
  84. package/packs/ops/decision-recovery/pack.yaml +89 -0
  85. package/templates/claude-hooks/doc-ingest.js +76 -0
  86. package/dist/src/cli/commands/team.js +0 -168
  87. package/dist/src/core/context/shared-context-layer.js +0 -620
  88. package/dist/src/core/context/stack-merge-resolver.js +0 -748
  89. package/dist/src/core/merge/conflict-detector.js +0 -430
  90. package/dist/src/core/merge/resolution-engine.js +0 -557
  91. package/dist/src/core/merge/stack-diff.js +0 -531
  92. package/dist/src/core/merge/unified-merge-resolver.js +0 -302
  93. package/dist/src/integrations/mcp/handlers/cord-handlers.js +0 -397
  94. package/dist/src/integrations/mcp/handlers/team-handlers.js +0 -211
  95. /package/dist/src/core/{merge → cache}/types.js +0 -0
@@ -19,6 +19,7 @@ import { createReadStream } from "fs";
19
19
  import { createInterface } from "readline";
20
20
  import { fileURLToPath } from "url";
21
21
  import { Transform } from "stream";
22
+ import { createHash } from "crypto";
22
23
  import { logger } from "../../core/monitoring/logger.js";
23
24
  import { isProcessAlive } from "../../utils/process-cleanup.js";
24
25
  import {
@@ -34,6 +35,59 @@ import {
34
35
  TraceCollector,
35
36
  stringifyEventTruncated
36
37
  } from "./conductor-traces.js";
38
+ const PROMPT_PHASES = [
39
+ "system",
40
+ "understand",
41
+ "implement",
42
+ "validate",
43
+ "deliver"
44
+ ];
45
+ function buildPromptFromPhases(variables) {
46
+ const promptsDir = join(homedir(), ".stackmemory", "conductor", "prompts");
47
+ const systemPath = join(promptsDir, "system.md");
48
+ if (!existsSync(systemPath)) return null;
49
+ const versions = {};
50
+ const parts = [];
51
+ for (const phase of PROMPT_PHASES) {
52
+ const phasePath = join(promptsDir, `${phase}.md`);
53
+ if (!existsSync(phasePath)) continue;
54
+ let content = readFileSync(phasePath, "utf-8");
55
+ for (const [key, value] of Object.entries(variables)) {
56
+ content = content.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
57
+ }
58
+ parts.push(content);
59
+ const hash = createHash("sha256").update(readFileSync(phasePath, "utf-8")).digest("hex").slice(0, 8);
60
+ versions[phase] = hash;
61
+ }
62
+ if (parts.length === 0) return null;
63
+ const dspyPath = join(
64
+ homedir(),
65
+ ".stackmemory",
66
+ "dspy",
67
+ "optimized_state.json"
68
+ );
69
+ if (existsSync(dspyPath)) {
70
+ try {
71
+ const state = JSON.parse(readFileSync(dspyPath, "utf-8"));
72
+ for (const phase of PROMPT_PHASES) {
73
+ const sig = state[phase];
74
+ if (sig?.fewShotExamples?.length) {
75
+ const examples = sig.fewShotExamples.slice(0, 3).map(
76
+ (ex) => `<example>
77
+ Input: ${JSON.stringify(ex.input)}
78
+ Output: ${JSON.stringify(ex.output)}
79
+ </example>`
80
+ ).join("\n");
81
+ parts.push(`
82
+ ## Optimized Examples (${phase}):
83
+ ${examples}`);
84
+ }
85
+ }
86
+ } catch {
87
+ }
88
+ }
89
+ return { prompt: parts.join("\n\n"), versions };
90
+ }
37
91
  function getOutcomesLogPath() {
38
92
  return join(homedir(), ".stackmemory", "conductor", "outcomes.jsonl");
39
93
  }
@@ -44,11 +98,6 @@ function logAgentOutcome(entry) {
44
98
  }
45
99
  function createPullRequest(opts) {
46
100
  try {
47
- execSync(`git push -u origin "${opts.branch}"`, {
48
- cwd: opts.workspacePath,
49
- stdio: "pipe",
50
- timeout: 6e4
51
- });
52
101
  const prTitle = `feat(conductor): ${opts.issueId} \u2014 ${opts.title}`;
53
102
  const prBody = [
54
103
  "## Summary",
@@ -60,6 +109,33 @@ function createPullRequest(opts) {
60
109
  "",
61
110
  "_This PR was auto-created by StackMemory Conductor._"
62
111
  ].join("\n");
112
+ if (opts.useGitButler) {
113
+ execSync(`but push --branch "${opts.branch}"`, {
114
+ cwd: opts.workspacePath,
115
+ stdio: "pipe",
116
+ timeout: 6e4
117
+ });
118
+ const result2 = execSync(
119
+ `but pr create --branch "${opts.branch}" --title "${prTitle.replace(/"/g, '\\"')}" --body "${prBody.replace(/"/g, '\\"')}"`,
120
+ {
121
+ cwd: opts.workspacePath,
122
+ encoding: "utf-8",
123
+ stdio: ["pipe", "pipe", "pipe"],
124
+ timeout: 3e4
125
+ }
126
+ );
127
+ const prUrl2 = result2.trim();
128
+ logger.info("Created PR via GitButler", {
129
+ issueId: opts.issueId,
130
+ prUrl: prUrl2
131
+ });
132
+ return prUrl2;
133
+ }
134
+ execSync(`git push -u origin "${opts.branch}"`, {
135
+ cwd: opts.workspacePath,
136
+ stdio: "pipe",
137
+ timeout: 6e4
138
+ });
63
139
  const result = execSync(
64
140
  `gh pr create --base "${opts.baseBranch}" --head "${opts.branch}" --title "${prTitle.replace(/"/g, '\\"')}" --body "${prBody.replace(/"/g, '\\"')}"`,
65
141
  {
@@ -158,8 +234,74 @@ function getRetryStrategy(issue, outcomes) {
158
234
  );
159
235
  }
160
236
  }
237
+ const promptsDir = join(homedir(), ".stackmemory", "conductor", "prompts");
238
+ if (lastFailure?.phase && existsSync(join(promptsDir, "system.md"))) {
239
+ const phaseAssertions = getPhaseAssertions(
240
+ lastFailure.phase,
241
+ lastFailure.errorTail || ""
242
+ );
243
+ adjustments.push(...phaseAssertions);
244
+ }
161
245
  return { shouldRetry: true, adjustments };
162
246
  }
247
+ function getPhaseAssertions(phase, error) {
248
+ const assertions = [];
249
+ switch (phase) {
250
+ case "reading":
251
+ case "planning":
252
+ assertions.push(
253
+ "ASSERTION: Re-read the issue description completely before planning.",
254
+ "ASSERTION: List ALL files you plan to modify before starting implementation."
255
+ );
256
+ break;
257
+ case "implementing":
258
+ if (/scope|unrelated|refactor/i.test(error)) {
259
+ assertions.push(
260
+ "ASSERTION: Only modify files directly required by the issue. Do NOT refactor surrounding code."
261
+ );
262
+ }
263
+ if (/import|module|ESM/i.test(error)) {
264
+ assertions.push(
265
+ "ASSERTION: Every relative import MUST end with .js extension. Check ALL new imports."
266
+ );
267
+ }
268
+ assertions.push(
269
+ "ASSERTION: After implementing, review your diff \u2014 if any change is not required by the issue, revert it."
270
+ );
271
+ break;
272
+ case "testing":
273
+ case "linting":
274
+ case "building":
275
+ if (/lint|eslint/i.test(error)) {
276
+ assertions.push(
277
+ "ASSERTION: Run `npm run lint` IMMEDIATELY. Fix every error. Do NOT proceed until lint passes.",
278
+ "ASSERTION: Common lint fixes \u2014 catch {} not catch (_err) {}, remove unused imports, add .js to relative imports."
279
+ );
280
+ }
281
+ if (/test|vitest|jest|FAIL/i.test(error)) {
282
+ assertions.push(
283
+ "ASSERTION: Read the FULL test error output. Identify which assertion fails and why.",
284
+ "ASSERTION: If vi.clearAllMocks() is in beforeEach, re-set any mockReturnValue calls after it."
285
+ );
286
+ }
287
+ if (/build|tsc|type/i.test(error)) {
288
+ assertions.push(
289
+ "ASSERTION: Run `npm run build` and fix ALL TypeScript errors before committing."
290
+ );
291
+ }
292
+ assertions.push(
293
+ "ASSERTION: Do NOT use --no-verify to bypass pre-commit hooks. Fix the underlying issue."
294
+ );
295
+ break;
296
+ case "committing":
297
+ assertions.push(
298
+ "ASSERTION: Commit message must follow format: type(scope): description",
299
+ "ASSERTION: If pre-commit hook fails, fix the issue and create a NEW commit \u2014 do NOT amend."
300
+ );
301
+ break;
302
+ }
303
+ return assertions;
304
+ }
163
305
  function findPackageRoot() {
164
306
  const currentFile = fileURLToPath(import.meta.url);
165
307
  let dir = dirname(currentFile);
@@ -340,6 +482,8 @@ class Conductor {
340
482
  stateCache = /* @__PURE__ */ new Map();
341
483
  activeStatesLower;
342
484
  terminalStatesLower;
485
+ /** Whether to use GitButler virtual branches instead of git worktrees */
486
+ useGitButler = false;
343
487
  /** Global rate limit backoff state */
344
488
  rateLimit = {
345
489
  inBackoff: false,
@@ -397,21 +541,79 @@ class Conductor {
397
541
  );
398
542
  }
399
543
  }
400
- if (!existsSync(this.config.workspaceRoot)) {
544
+ const wsMode = this.config.workspaceMode || "auto";
545
+ const laneMode = Boolean(this.config.laneBranch);
546
+ if (laneMode && wsMode === "gitbutler") {
547
+ throw new Error(
548
+ "--lane is only supported with git worktrees. Use --workspace-mode worktree or omit --workspace-mode."
549
+ );
550
+ }
551
+ if (!laneMode && (wsMode === "gitbutler" || wsMode === "auto")) {
552
+ try {
553
+ const butVersion = execSync("but --version", {
554
+ cwd: this.config.repoRoot,
555
+ encoding: "utf-8",
556
+ stdio: ["pipe", "pipe", "pipe"],
557
+ timeout: 5e3
558
+ }).trim();
559
+ const gbDir = join(this.config.repoRoot, ".git", "gitbutler");
560
+ if (wsMode === "gitbutler" || existsSync(gbDir)) {
561
+ this.useGitButler = true;
562
+ logger.info("Using GitButler virtual branches", {
563
+ version: butVersion
564
+ });
565
+ console.log(`[conductor] GitButler mode (${butVersion})`);
566
+ }
567
+ } catch {
568
+ if (wsMode === "gitbutler") {
569
+ throw new Error(
570
+ "GitButler CLI (but) not found. Install: brew install --cask gitbutler"
571
+ );
572
+ }
573
+ }
574
+ } else if (laneMode) {
575
+ logger.info("Lane mode enabled; using git worktrees", {
576
+ laneBranch: this.config.laneBranch
577
+ });
578
+ }
579
+ if (!this.useGitButler && !existsSync(this.config.workspaceRoot)) {
401
580
  mkdirSync(this.config.workspaceRoot, { recursive: true });
402
581
  }
403
582
  this.client = await this.createLinearClient();
404
- if (!this.config.teamId && this.client) {
583
+ if (this.client) {
405
584
  try {
406
- const team = await this.client.getTeam();
407
- this.config.teamId = team.id;
408
- logger.info("Auto-detected Linear team", {
409
- id: team.id,
410
- name: team.name,
411
- key: team.key
412
- });
585
+ if (this.config.teamId) {
586
+ const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-/i.test(this.config.teamId);
587
+ if (!isUuid) {
588
+ const teams = await this.client.getTeams();
589
+ const needle = this.config.teamId.toLowerCase();
590
+ const match = teams.find(
591
+ (t) => t.name.toLowerCase() === needle || t.key.toLowerCase() === needle
592
+ );
593
+ if (match) {
594
+ logger.info("Resolved team name to ID", {
595
+ name: this.config.teamId,
596
+ id: match.id,
597
+ key: match.key
598
+ });
599
+ this.config.teamId = match.id;
600
+ } else {
601
+ logger.warn("Team not found by name/key, will try as ID", {
602
+ team: this.config.teamId
603
+ });
604
+ }
605
+ }
606
+ } else {
607
+ const team = await this.client.getTeam();
608
+ this.config.teamId = team.id;
609
+ logger.info("Auto-detected Linear team", {
610
+ id: team.id,
611
+ name: team.name,
612
+ key: team.key
613
+ });
614
+ }
413
615
  } catch (err) {
414
- logger.warn("Failed to auto-detect team", {
616
+ logger.warn("Failed to resolve team", {
415
617
  error: err.message
416
618
  });
417
619
  }
@@ -433,7 +635,9 @@ class Conductor {
433
635
  try {
434
636
  await this.poll();
435
637
  } catch (err) {
436
- logger.error("Initial poll failed", { error: err.message });
638
+ const e = err;
639
+ logger.error("Initial poll failed", { error: e.message, stack: e.stack });
640
+ console.error(`[conductor] Initial poll failed: ${e.message}`);
437
641
  }
438
642
  await this.schedulePoll();
439
643
  }
@@ -691,6 +895,7 @@ class Conductor {
691
895
  const allCandidates = [];
692
896
  const issues = await this.client.getIssues({
693
897
  teamId: this.config.teamId,
898
+ stateType: "unstarted",
694
899
  limit: 50
695
900
  });
696
901
  for (const issue of issues) {
@@ -824,7 +1029,8 @@ class Conductor {
824
1029
  title: issue.title,
825
1030
  filesModified: run.filesModified,
826
1031
  toolCalls: run.toolCalls,
827
- workspacePath: run.workspacePath
1032
+ workspacePath: run.workspacePath,
1033
+ useGitButler: this.useGitButler
828
1034
  });
829
1035
  if (url) {
830
1036
  prUrl = url;
@@ -843,6 +1049,7 @@ class Conductor {
843
1049
  durationMs: Date.now() - run.startedAt,
844
1050
  hasCommits: true,
845
1051
  labels: issue.labels.map((l) => l.name),
1052
+ promptVersions: this.lastPromptVersions,
846
1053
  prUrl
847
1054
  });
848
1055
  await this.runHook(
@@ -878,6 +1085,7 @@ class Conductor {
878
1085
  durationMs: Date.now() - run.startedAt,
879
1086
  hasCommits: false,
880
1087
  labels: issue.labels.map((l) => l.name),
1088
+ promptVersions: this.lastPromptVersions,
881
1089
  errorTail: run.error?.slice(-500)
882
1090
  });
883
1091
  if (this.handleRateLimitError(run.error, issue.identifier)) {
@@ -1102,6 +1310,45 @@ class Conductor {
1102
1310
  // ── Workspace Management ──
1103
1311
  async createWorkspace(issue) {
1104
1312
  const wsKey = this.sanitizeIdentifier(issue.identifier);
1313
+ if (this.useGitButler) {
1314
+ return this.createGitButlerBranch(issue, wsKey);
1315
+ }
1316
+ return this.createWorktree(issue, wsKey);
1317
+ }
1318
+ createGitButlerBranch(issue, wsKey) {
1319
+ if (this.config.laneBranch) {
1320
+ throw new Error(
1321
+ "Lane mode requires git worktrees; GitButler virtual branches are not supported."
1322
+ );
1323
+ }
1324
+ const branchName = `conductor/${wsKey}`;
1325
+ try {
1326
+ execSync("but pull", {
1327
+ cwd: this.config.repoRoot,
1328
+ stdio: "pipe",
1329
+ timeout: 3e4
1330
+ });
1331
+ } catch {
1332
+ }
1333
+ try {
1334
+ execSync(`but branch new "${branchName}"`, {
1335
+ cwd: this.config.repoRoot,
1336
+ stdio: "pipe",
1337
+ timeout: 1e4
1338
+ });
1339
+ logger.info("Created GitButler virtual branch", {
1340
+ identifier: issue.identifier,
1341
+ branch: branchName
1342
+ });
1343
+ } catch {
1344
+ logger.info("GitButler branch may already exist, reusing", {
1345
+ identifier: issue.identifier,
1346
+ branch: branchName
1347
+ });
1348
+ }
1349
+ return this.config.repoRoot;
1350
+ }
1351
+ createWorktree(issue, wsKey) {
1105
1352
  const wsPath = join(this.config.workspaceRoot, wsKey);
1106
1353
  if (existsSync(wsPath)) {
1107
1354
  logger.info("Reusing existing workspace", {
@@ -1110,7 +1357,9 @@ class Conductor {
1110
1357
  });
1111
1358
  return wsPath;
1112
1359
  }
1113
- const branchName = `conductor/${wsKey}`;
1360
+ const lane = this.config.laneBranch;
1361
+ const branchName = lane ? `worktree-agent-${wsKey}` : `conductor/${wsKey}`;
1362
+ const startPoint = lane ? lane : `origin/${this.config.baseBranch}`;
1114
1363
  try {
1115
1364
  execSync("git fetch origin", {
1116
1365
  cwd: this.config.repoRoot,
@@ -1118,7 +1367,7 @@ class Conductor {
1118
1367
  timeout: 3e4
1119
1368
  });
1120
1369
  execSync(
1121
- `git worktree add "${wsPath}" -b "${branchName}" "origin/${this.config.baseBranch}"`,
1370
+ `git worktree add "${wsPath}" -b "${branchName}" "${startPoint}"`,
1122
1371
  {
1123
1372
  cwd: this.config.repoRoot,
1124
1373
  stdio: "pipe",
@@ -1128,7 +1377,8 @@ class Conductor {
1128
1377
  logger.info("Created workspace", {
1129
1378
  identifier: issue.identifier,
1130
1379
  path: wsPath,
1131
- branch: branchName
1380
+ branch: branchName,
1381
+ lane: lane || null
1132
1382
  });
1133
1383
  } catch (err) {
1134
1384
  try {
@@ -1147,6 +1397,25 @@ class Conductor {
1147
1397
  }
1148
1398
  async removeWorkspace(issue) {
1149
1399
  const wsKey = this.sanitizeIdentifier(issue.identifier);
1400
+ const branchName = `conductor/${wsKey}`;
1401
+ if (this.useGitButler) {
1402
+ await this.runHook("before-remove", this.config.repoRoot, issue).catch(
1403
+ () => {
1404
+ }
1405
+ );
1406
+ try {
1407
+ execSync(`but unapply "${branchName}"`, {
1408
+ cwd: this.config.repoRoot,
1409
+ stdio: "pipe",
1410
+ timeout: 1e4
1411
+ });
1412
+ } catch {
1413
+ logger.debug("GitButler branch already unapplied", {
1414
+ identifier: issue.identifier
1415
+ });
1416
+ }
1417
+ return;
1418
+ }
1150
1419
  const wsPath = join(this.config.workspaceRoot, wsKey);
1151
1420
  if (!existsSync(wsPath)) return;
1152
1421
  await this.runHook("before-remove", wsPath, issue).catch(() => {
@@ -1562,10 +1831,12 @@ class Conductor {
1562
1831
  }
1563
1832
  }
1564
1833
  }
1834
+ /** Last prompt version hashes — set by buildPrompt, read by outcome logging */
1835
+ lastPromptVersions = {};
1565
1836
  /**
1566
- * Build the agent prompt. If a custom template exists at
1567
- * ~/.stackmemory/conductor/prompt-template.md, use it with variable
1568
- * substitution. Otherwise fall back to the default template.
1837
+ * Build the agent prompt. Tries decomposed phase files first
1838
+ * (~/.stackmemory/conductor/prompts/*.md), then typed templates,
1839
+ * then custom prompt-template.md, then default.
1569
1840
  *
1570
1841
  * Template variables: {{ISSUE_ID}}, {{TITLE}}, {{DESCRIPTION}},
1571
1842
  * {{LABELS}}, {{PRIORITY}}, {{ATTEMPT}}, {{PRIOR_CONTEXT}}
@@ -1593,6 +1864,21 @@ class Conductor {
1593
1864
  );
1594
1865
  }
1595
1866
  const priorContext = contextParts.join("\n");
1867
+ const variables = {
1868
+ ISSUE_ID: issue.identifier,
1869
+ TITLE: issue.title,
1870
+ DESCRIPTION: issue.description || "",
1871
+ LABELS: labels,
1872
+ PRIORITY: priority,
1873
+ SCOPE: issue.identifier.toLowerCase().replace(/-\d+$/, ""),
1874
+ ATTEMPT: String(attempt),
1875
+ PRIOR_CONTEXT: priorContext
1876
+ };
1877
+ const phaseResult = buildPromptFromPhases(variables);
1878
+ if (phaseResult) {
1879
+ this.lastPromptVersions = phaseResult.versions;
1880
+ return phaseResult.prompt;
1881
+ }
1596
1882
  const templateDir = join(
1597
1883
  __dirname,
1598
1884
  "..",
@@ -1850,7 +2136,8 @@ class Conductor {
1850
2136
  title: run.issue.title,
1851
2137
  filesModified: run.filesModified,
1852
2138
  toolCalls: run.toolCalls,
1853
- workspacePath: wsPath
2139
+ workspacePath: wsPath,
2140
+ useGitButler: this.useGitButler
1854
2141
  });
1855
2142
  if (url) {
1856
2143
  prUrl = url;
@@ -1870,6 +2157,7 @@ class Conductor {
1870
2157
  durationMs,
1871
2158
  hasCommits,
1872
2159
  labels: run.issue.labels.map((l) => l.name),
2160
+ promptVersions: this.lastPromptVersions,
1873
2161
  errorTail,
1874
2162
  prUrl
1875
2163
  });