gsd-pi 2.11.0 → 2.13.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 (165) hide show
  1. package/dist/cli.js +18 -1
  2. package/dist/onboarding.js +3 -0
  3. package/dist/resource-loader.d.ts +2 -0
  4. package/dist/resource-loader.js +36 -1
  5. package/dist/resources/extensions/bg-shell/index.ts +51 -7
  6. package/dist/resources/extensions/gsd/auto-worktree.ts +509 -0
  7. package/dist/resources/extensions/gsd/auto.ts +381 -13
  8. package/dist/resources/extensions/gsd/commands.ts +9 -3
  9. package/dist/resources/extensions/gsd/doctor.ts +254 -3
  10. package/dist/resources/extensions/gsd/git-self-heal.ts +198 -0
  11. package/dist/resources/extensions/gsd/git-service.ts +11 -0
  12. package/dist/resources/extensions/gsd/guided-flow.ts +81 -9
  13. package/dist/resources/extensions/gsd/post-unit-hooks.ts +449 -0
  14. package/dist/resources/extensions/gsd/preferences.ts +209 -1
  15. package/dist/resources/extensions/gsd/prompt-loader.ts +28 -1
  16. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  17. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -3
  18. package/dist/resources/extensions/gsd/prompts/discuss.md +10 -8
  19. package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -2
  20. package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +3 -1
  21. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -1
  22. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
  23. package/dist/resources/extensions/gsd/prompts/guided-execute-task.md +3 -1
  24. package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +4 -2
  25. package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +3 -1
  26. package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +3 -1
  27. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +9 -12
  28. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -3
  29. package/dist/resources/extensions/gsd/prompts/queue.md +3 -1
  30. package/dist/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  31. package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
  32. package/dist/resources/extensions/gsd/prompts/system.md +32 -29
  33. package/dist/resources/extensions/gsd/templates/context.md +1 -1
  34. package/dist/resources/extensions/gsd/templates/state.md +3 -3
  35. package/dist/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +282 -0
  36. package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +259 -0
  37. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +147 -0
  38. package/dist/resources/extensions/gsd/tests/doctor-git.test.ts +246 -0
  39. package/dist/resources/extensions/gsd/tests/doctor.test.ts +115 -1
  40. package/dist/resources/extensions/gsd/tests/git-self-heal.test.ts +234 -0
  41. package/dist/resources/extensions/gsd/tests/isolation-resolver.test.ts +107 -0
  42. package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +297 -0
  43. package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +88 -0
  44. package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +226 -0
  45. package/dist/resources/extensions/gsd/tests/regex-hardening.test.ts +12 -0
  46. package/dist/resources/extensions/gsd/tests/worktree-e2e.test.ts +315 -0
  47. package/dist/resources/extensions/gsd/types.ts +109 -0
  48. package/dist/resources/extensions/gsd/worktree-manager.ts +6 -4
  49. package/dist/resources/extensions/search-the-web/command-search-provider.ts +8 -4
  50. package/dist/resources/extensions/search-the-web/native-search.ts +15 -10
  51. package/dist/resources/extensions/search-the-web/provider.ts +19 -2
  52. package/dist/resources/extensions/search-the-web/tool-fetch-page.ts +62 -0
  53. package/dist/resources/extensions/search-the-web/tool-llm-context.ts +62 -3
  54. package/dist/resources/extensions/search-the-web/tool-search.ts +62 -3
  55. package/dist/wizard.js +1 -0
  56. package/package.json +1 -1
  57. package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
  58. package/packages/pi-agent-core/dist/agent-loop.js +169 -55
  59. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  60. package/packages/pi-agent-core/dist/agent.d.ts +13 -1
  61. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  62. package/packages/pi-agent-core/dist/agent.js +16 -0
  63. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  64. package/packages/pi-agent-core/dist/types.d.ts +91 -1
  65. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  66. package/packages/pi-agent-core/dist/types.js.map +1 -1
  67. package/packages/pi-agent-core/src/agent-loop.ts +273 -63
  68. package/packages/pi-agent-core/src/agent.ts +24 -0
  69. package/packages/pi-agent-core/src/types.ts +98 -0
  70. package/packages/pi-ai/dist/env-api-keys.js +1 -0
  71. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  72. package/packages/pi-ai/dist/models.generated.d.ts +314 -0
  73. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  74. package/packages/pi-ai/dist/models.generated.js +236 -0
  75. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  76. package/packages/pi-ai/dist/types.d.ts +1 -1
  77. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  78. package/packages/pi-ai/dist/types.js.map +1 -1
  79. package/packages/pi-ai/src/env-api-keys.ts +1 -0
  80. package/packages/pi-ai/src/models.generated.ts +236 -0
  81. package/packages/pi-ai/src/types.ts +2 -1
  82. package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
  83. package/packages/pi-coding-agent/dist/cli/args.js +2 -1
  84. package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
  85. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +10 -0
  86. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  87. package/packages/pi-coding-agent/dist/core/agent-session.js +69 -8
  88. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  89. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +4 -1
  90. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  91. package/packages/pi-coding-agent/dist/core/extensions/runner.js +2 -1
  92. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  93. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +5 -0
  94. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  95. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  96. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  97. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  98. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  99. package/packages/pi-coding-agent/dist/core/sdk.js +3 -3
  100. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  102. package/packages/pi-coding-agent/dist/core/system-prompt.js +6 -0
  103. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  104. package/packages/pi-coding-agent/src/cli/args.ts +2 -1
  105. package/packages/pi-coding-agent/src/core/agent-session.ts +76 -7
  106. package/packages/pi-coding-agent/src/core/extensions/runner.ts +2 -1
  107. package/packages/pi-coding-agent/src/core/extensions/types.ts +2 -0
  108. package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
  109. package/packages/pi-coding-agent/src/core/sdk.ts +3 -3
  110. package/packages/pi-coding-agent/src/core/system-prompt.ts +9 -0
  111. package/packages/pi-tui/dist/components/editor.d.ts +11 -0
  112. package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
  113. package/packages/pi-tui/dist/components/editor.js +64 -6
  114. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  115. package/packages/pi-tui/src/components/editor.ts +71 -6
  116. package/src/resources/extensions/bg-shell/index.ts +51 -7
  117. package/src/resources/extensions/gsd/auto-worktree.ts +509 -0
  118. package/src/resources/extensions/gsd/auto.ts +381 -13
  119. package/src/resources/extensions/gsd/commands.ts +9 -3
  120. package/src/resources/extensions/gsd/doctor.ts +254 -3
  121. package/src/resources/extensions/gsd/git-self-heal.ts +198 -0
  122. package/src/resources/extensions/gsd/git-service.ts +11 -0
  123. package/src/resources/extensions/gsd/guided-flow.ts +81 -9
  124. package/src/resources/extensions/gsd/post-unit-hooks.ts +449 -0
  125. package/src/resources/extensions/gsd/preferences.ts +209 -1
  126. package/src/resources/extensions/gsd/prompt-loader.ts +28 -1
  127. package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  128. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -3
  129. package/src/resources/extensions/gsd/prompts/discuss.md +10 -8
  130. package/src/resources/extensions/gsd/prompts/execute-task.md +4 -2
  131. package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +3 -1
  132. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -1
  133. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
  134. package/src/resources/extensions/gsd/prompts/guided-execute-task.md +3 -1
  135. package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +4 -2
  136. package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +3 -1
  137. package/src/resources/extensions/gsd/prompts/guided-research-slice.md +3 -1
  138. package/src/resources/extensions/gsd/prompts/plan-milestone.md +9 -12
  139. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -3
  140. package/src/resources/extensions/gsd/prompts/queue.md +3 -1
  141. package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  142. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  143. package/src/resources/extensions/gsd/prompts/system.md +32 -29
  144. package/src/resources/extensions/gsd/templates/context.md +1 -1
  145. package/src/resources/extensions/gsd/templates/state.md +3 -3
  146. package/src/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +282 -0
  147. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +259 -0
  148. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +147 -0
  149. package/src/resources/extensions/gsd/tests/doctor-git.test.ts +246 -0
  150. package/src/resources/extensions/gsd/tests/doctor.test.ts +115 -1
  151. package/src/resources/extensions/gsd/tests/git-self-heal.test.ts +234 -0
  152. package/src/resources/extensions/gsd/tests/isolation-resolver.test.ts +107 -0
  153. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +297 -0
  154. package/src/resources/extensions/gsd/tests/preferences-git.test.ts +88 -0
  155. package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +226 -0
  156. package/src/resources/extensions/gsd/tests/regex-hardening.test.ts +12 -0
  157. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +315 -0
  158. package/src/resources/extensions/gsd/types.ts +109 -0
  159. package/src/resources/extensions/gsd/worktree-manager.ts +6 -4
  160. package/src/resources/extensions/search-the-web/command-search-provider.ts +8 -4
  161. package/src/resources/extensions/search-the-web/native-search.ts +15 -10
  162. package/src/resources/extensions/search-the-web/provider.ts +19 -2
  163. package/src/resources/extensions/search-the-web/tool-fetch-page.ts +62 -0
  164. package/src/resources/extensions/search-the-web/tool-llm-context.ts +62 -3
  165. package/src/resources/extensions/search-the-web/tool-search.ts +62 -3
@@ -18,17 +18,18 @@ import type {
18
18
 
19
19
  import { deriveState, invalidateStateCache } from "./state.js";
20
20
  import type { GSDState } from "./types.js";
21
- import { loadFile, parseContinue, parsePlan, parseRoadmap, parseSummary, extractUatType, inlinePriorMilestoneSummary, getManifestStatus } from "./files.js";
21
+ import { loadFile, parseContinue, parsePlan, parseRoadmap, parseSummary, extractUatType, inlinePriorMilestoneSummary, getManifestStatus, clearParseCache } from "./files.js";
22
22
  export { inlinePriorMilestoneSummary };
23
23
  import type { UatType } from "./files.js";
24
24
  import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
25
- import { loadPrompt } from "./prompt-loader.js";
25
+ import { loadPrompt, inlineTemplate } from "./prompt-loader.js";
26
26
  import {
27
27
  gsdRoot, resolveMilestoneFile, resolveSliceFile, resolveSlicePath,
28
28
  resolveMilestonePath, resolveDir, resolveTasksDir, resolveTaskFiles, resolveTaskFile,
29
29
  relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relMilestonePath,
30
30
  milestonesDir, resolveGsdRootFile, relGsdRootFile,
31
31
  buildMilestoneFileName, buildSliceFileName, buildTaskFileName,
32
+ clearPathCache,
32
33
  } from "./paths.js";
33
34
  import { saveActivityLog } from "./activity-log.js";
34
35
  import { synthesizeCrashRecovery, getDeepDiagnostic } from "./session-forensics.js";
@@ -42,6 +43,18 @@ import {
42
43
  } from "./unit-runtime.js";
43
44
  import { resolveAutoSupervisorConfig, resolveModelForUnit, resolveModelWithFallbacksForUnit, resolveSkillDiscoveryMode, loadEffectiveGSDPreferences } from "./preferences.js";
44
45
  import type { GSDPreferences } from "./preferences.js";
46
+ import {
47
+ checkPostUnitHooks,
48
+ getActiveHook,
49
+ resetHookState,
50
+ isRetryPending,
51
+ consumeRetryTrigger,
52
+ runPreDispatchHooks,
53
+ persistHookState,
54
+ restoreHookState,
55
+ clearPersistedHookState,
56
+ formatHookStatus,
57
+ } from "./post-unit-hooks.js";
45
58
  import {
46
59
  validatePlanBoundary,
47
60
  validateExecuteBoundary,
@@ -73,6 +86,19 @@ import {
73
86
  import { GitServiceImpl, runGit } from "./git-service.js";
74
87
  import { nativeCommitCountBetween } from "./native-git-bridge.js";
75
88
  import { getPriorSliceCompletionBlocker } from "./dispatch-guard.js";
89
+ import { formatGitError } from "./git-self-heal.js";
90
+ import {
91
+ createAutoWorktree,
92
+ enterAutoWorktree,
93
+ teardownAutoWorktree,
94
+ isInAutoWorktree,
95
+ getAutoWorktreePath,
96
+ getAutoWorktreeOriginalBase,
97
+ mergeSliceToMilestone,
98
+ mergeMilestoneToMain,
99
+ shouldUseWorktreeIsolation,
100
+ getMergeToMainMode,
101
+ } from "./auto-worktree.js";
76
102
  import type { GitPreferences } from "./git-service.js";
77
103
  import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
78
104
  import { makeUI, GLYPH, INDENT } from "../shared/ui.js";
@@ -131,6 +157,7 @@ let stepMode = false;
131
157
  let verbose = false;
132
158
  let cmdCtx: ExtensionCommandContext | null = null;
133
159
  let basePath = "";
160
+ let originalBasePath = "";
134
161
  let gitService: GitServiceImpl | null = null;
135
162
 
136
163
  /** Track total dispatches per unit to detect stuck loops (catches A→B→A→B patterns) */
@@ -138,6 +165,12 @@ const unitDispatchCount = new Map<string, number>();
138
165
  const MAX_UNIT_DISPATCHES = 3;
139
166
  /** Retry index at which a stub summary placeholder is written when the summary is still absent. */
140
167
  const STUB_RECOVERY_THRESHOLD = 2;
168
+ /** Hard cap on total dispatches per unit across ALL reconciliation cycles.
169
+ * unitDispatchCount can be reset by loop-recovery/self-repair paths, but this
170
+ * counter is never reset — it catches infinite reconciliation loops where
171
+ * artifacts exist but deriveState keeps returning the same unit. */
172
+ const unitLifetimeDispatches = new Map<string, number>();
173
+ const MAX_LIFETIME_DISPATCHES = 6;
141
174
 
142
175
  /** Tracks recovery attempt count per unit for backoff and diagnostics. */
143
176
  const unitRecoveryCount = new Map<string, number>();
@@ -330,6 +363,27 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
330
363
 
331
364
  // Remove SIGTERM handler registered at auto-mode start
332
365
  deregisterSigtermHandler();
366
+
367
+ // ── Auto-worktree: exit worktree and reset basePath on stop ──
368
+ if (currentMilestoneId && isInAutoWorktree(basePath)) {
369
+ try {
370
+ teardownAutoWorktree(originalBasePath, currentMilestoneId);
371
+ basePath = originalBasePath;
372
+ gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
373
+ ctx?.ui.notify("Exited auto-worktree.", "info");
374
+ } catch (err) {
375
+ ctx?.ui.notify(
376
+ `Auto-worktree teardown failed: ${err instanceof Error ? err.message : String(err)}`,
377
+ "warning",
378
+ );
379
+ // Force basePath back to original even if teardown failed
380
+ if (originalBasePath) {
381
+ basePath = originalBasePath;
382
+ try { process.chdir(basePath); } catch { /* best-effort */ }
383
+ }
384
+ }
385
+ }
386
+
333
387
  const ledger = getLedger();
334
388
  if (ledger && ledger.units.length > 0) {
335
389
  const totals = getProjectTotals(ledger.units);
@@ -347,13 +401,17 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
347
401
  }
348
402
 
349
403
  resetMetrics();
404
+ resetHookState();
405
+ if (basePath) clearPersistedHookState(basePath);
350
406
  active = false;
351
407
  paused = false;
352
408
  stepMode = false;
353
409
  unitDispatchCount.clear();
354
410
  unitRecoveryCount.clear();
411
+ unitLifetimeDispatches.clear();
355
412
  currentUnit = null;
356
413
  currentMilestoneId = null;
414
+ originalBasePath = "";
357
415
  cachedSliceProgress = null;
358
416
  pendingCrashRecovery = null;
359
417
  _handlingAgentEnd = false;
@@ -503,10 +561,17 @@ async function mergeOrphanedSliceBranches(
503
561
  "info",
504
562
  );
505
563
  try {
506
- switchToMain(base);
507
- const mergeResult = mergeSliceToMain(
508
- base, milestoneId, sliceId, sliceEntry.title || sliceId,
509
- );
564
+ let mergeResult;
565
+ if (isInAutoWorktree(base) && getMergeToMainMode() !== "slice") {
566
+ mergeResult = mergeSliceToMilestone(
567
+ base, milestoneId, sliceId, sliceEntry.title || sliceId,
568
+ );
569
+ } else {
570
+ switchToMain(base);
571
+ mergeResult = mergeSliceToMain(
572
+ base, milestoneId, sliceId, sliceEntry.title || sliceId,
573
+ );
574
+ }
510
575
  ctx.ui.notify(
511
576
  `Merged orphaned branch ${mergeResult.branch} → ${mainBranch}.`,
512
577
  "info",
@@ -553,17 +618,44 @@ export async function startAuto(
553
618
  cmdCtx = ctx;
554
619
  basePath = base;
555
620
  unitDispatchCount.clear();
621
+ unitLifetimeDispatches.clear();
556
622
  // Re-initialize metrics in case ledger was lost during pause
557
623
  if (!getLedger()) initMetrics(base);
558
624
  // Ensure milestone ID is set on git service for integration branch resolution
559
625
  if (currentMilestoneId) setActiveMilestoneId(base, currentMilestoneId);
560
626
 
627
+ // ── Auto-worktree: re-enter worktree on resume if not already inside ──
628
+ if (currentMilestoneId && originalBasePath && !isInAutoWorktree(basePath) && shouldUseWorktreeIsolation(originalBasePath)) {
629
+ try {
630
+ const existingWtPath = getAutoWorktreePath(originalBasePath, currentMilestoneId);
631
+ if (existingWtPath) {
632
+ const wtPath = enterAutoWorktree(originalBasePath, currentMilestoneId);
633
+ basePath = wtPath;
634
+ gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
635
+ ctx.ui.notify(`Re-entered auto-worktree at ${wtPath}`, "info");
636
+ } else {
637
+ // Worktree was deleted while paused — recreate it.
638
+ const wtPath = createAutoWorktree(originalBasePath, currentMilestoneId);
639
+ basePath = wtPath;
640
+ gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
641
+ ctx.ui.notify(`Recreated auto-worktree at ${wtPath}`, "info");
642
+ }
643
+ } catch (err) {
644
+ ctx.ui.notify(
645
+ `Auto-worktree re-entry failed: ${err instanceof Error ? err.message : String(err)}. Continuing at current path.`,
646
+ "warning",
647
+ );
648
+ }
649
+ }
650
+
561
651
  // Re-register SIGTERM handler for the resumed session
562
- registerSigtermHandler(base);
652
+ registerSigtermHandler(basePath);
563
653
 
564
654
  ctx.ui.setStatus("gsd-auto", stepMode ? "next" : "auto");
565
655
  ctx.ui.setFooter(hideFooter);
566
656
  ctx.ui.notify(stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info");
657
+ // Restore hook state from disk in case session was interrupted
658
+ restoreHookState(base);
567
659
  // Rebuild disk state before resuming — user interaction during pause may have changed files
568
660
  try { await rebuildState(base); } catch { /* non-fatal */ }
569
661
  try {
@@ -575,6 +667,8 @@ export async function startAuto(
575
667
  // Self-heal: clear stale runtime records where artifacts already exist
576
668
  await selfHealRuntimeRecords(base, ctx);
577
669
  invalidateStateCache();
670
+ clearParseCache();
671
+ clearPathCache();
578
672
  await dispatchNextUnit(ctx, pi);
579
673
  return;
580
674
  }
@@ -668,8 +762,11 @@ export async function startAuto(
668
762
  basePath = base;
669
763
  unitDispatchCount.clear();
670
764
  unitRecoveryCount.clear();
765
+ unitLifetimeDispatches.clear();
671
766
  completedKeySet.clear();
672
767
  loadPersistedKeys(base, completedKeySet);
768
+ resetHookState();
769
+ restoreHookState(base);
673
770
  autoStartTime = Date.now();
674
771
  completedUnits = [];
675
772
  currentUnit = null;
@@ -689,6 +786,36 @@ export async function startAuto(
689
786
  setActiveMilestoneId(base, currentMilestoneId);
690
787
  }
691
788
 
789
+ // ── Auto-worktree: create or enter worktree for the active milestone ──
790
+ // Store the original project root before any chdir so we can restore on stop.
791
+ originalBasePath = base;
792
+ if (currentMilestoneId && shouldUseWorktreeIsolation(base)) {
793
+ try {
794
+ const existingWtPath = getAutoWorktreePath(base, currentMilestoneId);
795
+ if (existingWtPath) {
796
+ // Worktree already exists (e.g., previous session created it) — enter it.
797
+ const wtPath = enterAutoWorktree(base, currentMilestoneId);
798
+ basePath = wtPath;
799
+ gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
800
+ ctx.ui.notify(`Entered auto-worktree at ${wtPath}`, "info");
801
+ } else {
802
+ // Fresh start — create worktree and enter it.
803
+ const wtPath = createAutoWorktree(base, currentMilestoneId);
804
+ basePath = wtPath;
805
+ gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
806
+ ctx.ui.notify(`Created auto-worktree at ${wtPath}`, "info");
807
+ }
808
+ // Re-register SIGTERM handler with the new basePath
809
+ registerSigtermHandler(basePath);
810
+ } catch (err) {
811
+ // Worktree creation is non-fatal — continue in the project root.
812
+ ctx.ui.notify(
813
+ `Auto-worktree setup failed: ${err instanceof Error ? err.message : String(err)}. Continuing in project root.`,
814
+ "warning",
815
+ );
816
+ }
817
+ }
818
+
692
819
  // Initialize metrics — loads existing ledger from disk
693
820
  initMetrics(base);
694
821
 
@@ -767,6 +894,8 @@ export async function handleAgentEnd(
767
894
  // Invalidate deriveState() cache — the unit just completed and may have
768
895
  // written planning files (task summaries, roadmap checkboxes, etc.)
769
896
  invalidateStateCache();
897
+ clearParseCache();
898
+ clearPathCache();
770
899
 
771
900
  // Small delay to let files settle (git commits, file writes)
772
901
  await new Promise(r => setTimeout(r, 500));
@@ -804,6 +933,97 @@ export async function handleAgentEnd(
804
933
  } catch {
805
934
  // Non-fatal
806
935
  }
936
+
937
+ // ── Path A fix: verify artifact and persist completion before re-entering dispatch ──
938
+ // After doctor + rebuildState, check whether the just-completed unit actually
939
+ // produced its expected artifact. If so, persist the completion key now so the
940
+ // idempotency check at the top of dispatchNextUnit() skips it — even if
941
+ // deriveState() still returns this unit as active (e.g. branch mismatch).
942
+ try {
943
+ if (verifyExpectedArtifact(currentUnit.type, currentUnit.id, basePath)) {
944
+ const completionKey = `${currentUnit.type}/${currentUnit.id}`;
945
+ if (!completedKeySet.has(completionKey)) {
946
+ persistCompletedKey(basePath, completionKey);
947
+ completedKeySet.add(completionKey);
948
+ }
949
+ invalidateStateCache();
950
+ }
951
+ } catch {
952
+ // Non-fatal — worst case we fall through to normal dispatch which has its own checks
953
+ }
954
+ }
955
+
956
+ // ── Post-unit hooks: check if a configured hook should run before normal dispatch ──
957
+ if (currentUnit && !stepMode) {
958
+ const hookUnit = checkPostUnitHooks(currentUnit.type, currentUnit.id, basePath);
959
+ if (hookUnit) {
960
+ // Dispatch the hook unit instead of normal flow
961
+ const hookStartedAt = Date.now();
962
+ if (currentUnit) {
963
+ const modelId = ctx.model?.id ?? "unknown";
964
+ snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
965
+ saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
966
+ }
967
+ currentUnit = { type: hookUnit.unitType, id: hookUnit.unitId, startedAt: hookStartedAt };
968
+ writeUnitRuntimeRecord(basePath, hookUnit.unitType, hookUnit.unitId, hookStartedAt, {
969
+ phase: "dispatched",
970
+ wrapupWarningSent: false,
971
+ timeoutAt: null,
972
+ lastProgressAt: hookStartedAt,
973
+ progressCount: 0,
974
+ lastProgressKind: "dispatch",
975
+ });
976
+
977
+ const state = await deriveState(basePath);
978
+ updateProgressWidget(ctx, hookUnit.unitType, hookUnit.unitId, state);
979
+ const hookState = getActiveHook();
980
+ ctx.ui.notify(
981
+ `Running post-unit hook: ${hookUnit.hookName} (cycle ${hookState?.cycle ?? 1})`,
982
+ "info",
983
+ );
984
+
985
+ // Switch model if the hook specifies one
986
+ if (hookUnit.model) {
987
+ const availableModels = ctx.modelRegistry.getAvailable();
988
+ const match = availableModels.find(m =>
989
+ m.id === hookUnit.model || `${m.provider}/${m.id}` === hookUnit.model,
990
+ );
991
+ if (match) {
992
+ try {
993
+ await pi.setModel(match);
994
+ } catch { /* non-fatal — use current model */ }
995
+ }
996
+ }
997
+
998
+ const result = await cmdCtx!.newSession();
999
+ if (result.cancelled) {
1000
+ resetHookState();
1001
+ await stopAuto(ctx, pi);
1002
+ return;
1003
+ }
1004
+ const sessionFile = ctx.sessionManager.getSessionFile();
1005
+ writeLock(basePath, hookUnit.unitType, hookUnit.unitId, completedUnits.length, sessionFile);
1006
+ // Persist hook state so cycle counts survive crashes
1007
+ persistHookState(basePath);
1008
+ pi.sendMessage(
1009
+ { customType: "gsd-auto", content: hookUnit.prompt, display: verbose },
1010
+ { triggerTurn: true },
1011
+ );
1012
+ return; // handleAgentEnd will fire again when hook session completes
1013
+ }
1014
+
1015
+ // Check if a hook requested a retry of the trigger unit
1016
+ if (isRetryPending()) {
1017
+ const trigger = consumeRetryTrigger();
1018
+ if (trigger) {
1019
+ ctx.ui.notify(
1020
+ `Hook requested retry of ${trigger.unitType} ${trigger.unitId}.`,
1021
+ "info",
1022
+ );
1023
+ // Fall through to normal dispatchNextUnit — state derivation will
1024
+ // re-select the same unit since it hasn't been marked complete
1025
+ }
1026
+ }
807
1027
  }
808
1028
 
809
1029
  // In step mode, pause and show a wizard instead of immediately dispatching
@@ -949,6 +1169,7 @@ export function describeNextUnit(state: GSDState): { label: string; description:
949
1169
  // ─── Progress Widget ──────────────────────────────────────────────────────
950
1170
 
951
1171
  function unitVerb(unitType: string): string {
1172
+ if (unitType.startsWith("hook/")) return `hook: ${unitType.slice(5)}`;
952
1173
  switch (unitType) {
953
1174
  case "research-milestone":
954
1175
  case "research-slice": return "researching";
@@ -965,6 +1186,7 @@ function unitVerb(unitType: string): string {
965
1186
  }
966
1187
 
967
1188
  function unitPhaseLabel(unitType: string): string {
1189
+ if (unitType.startsWith("hook/")) return "HOOK";
968
1190
  switch (unitType) {
969
1191
  case "research-milestone": return "RESEARCH";
970
1192
  case "research-slice": return "RESEARCH";
@@ -981,7 +1203,14 @@ function unitPhaseLabel(unitType: string): string {
981
1203
  }
982
1204
 
983
1205
  function peekNext(unitType: string, state: GSDState): string {
1206
+ // Show active hook info in progress display
1207
+ const activeHookState = getActiveHook();
1208
+ if (activeHookState) {
1209
+ return `hook: ${activeHookState.hookName} (cycle ${activeHookState.cycle})`;
1210
+ }
1211
+
984
1212
  const sid = state.activeSlice?.id ?? "";
1213
+ if (unitType.startsWith("hook/")) return `continue ${sid}`;
985
1214
  switch (unitType) {
986
1215
  case "research-milestone": return "plan milestone roadmap";
987
1216
  case "plan-milestone": return "plan or execute first slice";
@@ -1275,6 +1504,12 @@ async function dispatchNextUnit(
1275
1504
  return;
1276
1505
  }
1277
1506
 
1507
+ // Clear stale directory listing cache so deriveState sees fresh disk state (#431)
1508
+ clearPathCache();
1509
+ // Clear parsed roadmap/plan cache — doctor may have re-populated it with
1510
+ // stale data between handleAgentEnd and this dispatch call (Path B fix).
1511
+ clearParseCache();
1512
+
1278
1513
  let state = await deriveState(basePath);
1279
1514
  let mid = state.activeMilestone?.id;
1280
1515
  let midTitle = state.activeMilestone?.title;
@@ -1288,8 +1523,9 @@ async function dispatchNextUnit(
1288
1523
  // Reset stuck detection for new milestone
1289
1524
  unitDispatchCount.clear();
1290
1525
  unitRecoveryCount.clear();
1526
+ unitLifetimeDispatches.clear();
1291
1527
  // Capture integration branch for the new milestone and update git service
1292
- captureIntegrationBranch(basePath, mid);
1528
+ captureIntegrationBranch(originalBasePath || basePath, mid);
1293
1529
  }
1294
1530
  if (mid) {
1295
1531
  currentMilestoneId = mid;
@@ -1338,6 +1574,8 @@ async function dispatchNextUnit(
1338
1574
  }
1339
1575
  // Re-derive state from the now-merged working tree
1340
1576
  invalidateStateCache();
1577
+ clearParseCache();
1578
+ clearPathCache();
1341
1579
  state = await deriveState(basePath);
1342
1580
  mid = state.activeMilestone?.id;
1343
1581
  midTitle = state.activeMilestone?.title;
@@ -1392,10 +1630,17 @@ async function dispatchNextUnit(
1392
1630
  if (sliceEntry?.done) {
1393
1631
  try {
1394
1632
  const sliceTitleForMerge = sliceEntry.title || branchSid;
1395
- switchToMain(basePath);
1396
- const mergeResult = mergeSliceToMain(
1397
- basePath, branchMid, branchSid, sliceTitleForMerge,
1398
- );
1633
+ let mergeResult;
1634
+ if (isInAutoWorktree(basePath) && getMergeToMainMode() !== "slice") {
1635
+ mergeResult = mergeSliceToMilestone(
1636
+ basePath, branchMid, branchSid, sliceTitleForMerge,
1637
+ );
1638
+ } else {
1639
+ switchToMain(basePath);
1640
+ mergeResult = mergeSliceToMain(
1641
+ basePath, branchMid, branchSid, sliceTitleForMerge,
1642
+ );
1643
+ }
1399
1644
  const targetBranch = getMainBranch(basePath);
1400
1645
  ctx.ui.notify(
1401
1646
  `Merged ${mergeResult.branch} → ${targetBranch}.`,
@@ -1403,6 +1648,8 @@ async function dispatchNextUnit(
1403
1648
  );
1404
1649
  // Re-derive state from main so downstream logic sees merged state
1405
1650
  invalidateStateCache();
1651
+ clearParseCache();
1652
+ clearPathCache();
1406
1653
  state = await deriveState(basePath);
1407
1654
  mid = state.activeMilestone?.id;
1408
1655
  midTitle = state.activeMilestone?.title;
@@ -1457,7 +1704,7 @@ async function dispatchNextUnit(
1457
1704
  }
1458
1705
 
1459
1706
  // Non-conflict errors: reset and stop
1460
- const message = error instanceof Error ? error.message : String(error);
1707
+ const message = formatGitError(error instanceof Error ? error : String(error));
1461
1708
  try {
1462
1709
  const status = runGit(basePath, ["status", "--porcelain"], { allowFailure: true });
1463
1710
  if (status && (status.includes("UU ") || status.includes("AA ") || status.includes("UD "))) {
@@ -1514,6 +1761,27 @@ async function dispatchNextUnit(
1514
1761
  if (existsSync(file)) writeFileSync(file, JSON.stringify([]), "utf-8");
1515
1762
  completedKeySet.clear();
1516
1763
  } catch { /* non-fatal */ }
1764
+
1765
+ // ── Milestone merge: squash-merge milestone branch to main before stopping ──
1766
+ if (currentMilestoneId && isInAutoWorktree(basePath) && originalBasePath && getMergeToMainMode() === "milestone") {
1767
+ try {
1768
+ const roadmapPath = resolveMilestoneFile(originalBasePath, currentMilestoneId, "ROADMAP");
1769
+ const roadmapContent = readFileSync(roadmapPath, "utf-8");
1770
+ const mergeResult = mergeMilestoneToMain(originalBasePath, currentMilestoneId, roadmapContent);
1771
+ basePath = originalBasePath;
1772
+ gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1773
+ ctx.ui.notify(
1774
+ `Milestone ${currentMilestoneId} merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
1775
+ "info",
1776
+ );
1777
+ } catch (err) {
1778
+ ctx.ui.notify(
1779
+ `Milestone merge failed: ${err instanceof Error ? err.message : String(err)}`,
1780
+ "warning",
1781
+ );
1782
+ }
1783
+ }
1784
+
1517
1785
  await stopAuto(ctx, pi);
1518
1786
  return;
1519
1787
  }
@@ -1715,6 +1983,28 @@ async function dispatchNextUnit(
1715
1983
  }
1716
1984
  }
1717
1985
 
1986
+ // ── Pre-dispatch hooks: modify, skip, or replace the unit before dispatch ──
1987
+ const preDispatchResult = runPreDispatchHooks(unitType, unitId, prompt, basePath);
1988
+ if (preDispatchResult.firedHooks.length > 0) {
1989
+ ctx.ui.notify(
1990
+ `Pre-dispatch hook${preDispatchResult.firedHooks.length > 1 ? "s" : ""}: ${preDispatchResult.firedHooks.join(", ")}`,
1991
+ "info",
1992
+ );
1993
+ }
1994
+ if (preDispatchResult.action === "skip") {
1995
+ ctx.ui.notify(`Skipping ${unitType} ${unitId} (pre-dispatch hook).`, "info");
1996
+ // Yield then re-dispatch to advance to next unit
1997
+ await new Promise(r => setImmediate(r));
1998
+ await dispatchNextUnit(ctx, pi);
1999
+ return;
2000
+ }
2001
+ if (preDispatchResult.action === "replace") {
2002
+ prompt = preDispatchResult.prompt ?? prompt;
2003
+ if (preDispatchResult.unitType) unitType = preDispatchResult.unitType;
2004
+ } else if (preDispatchResult.prompt) {
2005
+ prompt = preDispatchResult.prompt;
2006
+ }
2007
+
1718
2008
  const priorSliceBlocker = getPriorSliceCompletionBlocker(basePath, getMainBranch(basePath), unitType, unitId);
1719
2009
  if (priorSliceBlocker) {
1720
2010
  await stopAuto(ctx, pi);
@@ -1754,6 +2044,26 @@ async function dispatchNextUnit(
1754
2044
  // Pattern A→B→A→B would reset retryCount every time; this map catches it.
1755
2045
  const dispatchKey = `${unitType}/${unitId}`;
1756
2046
  const prevCount = unitDispatchCount.get(dispatchKey) ?? 0;
2047
+
2048
+ // Hard lifetime cap — survives counter resets from loop-recovery/self-repair.
2049
+ // Catches the case where reconciliation "succeeds" (artifacts exist) but
2050
+ // deriveState keeps returning the same unit, creating an infinite cycle.
2051
+ const lifetimeCount = (unitLifetimeDispatches.get(dispatchKey) ?? 0) + 1;
2052
+ unitLifetimeDispatches.set(dispatchKey, lifetimeCount);
2053
+ if (lifetimeCount > MAX_LIFETIME_DISPATCHES) {
2054
+ if (currentUnit) {
2055
+ const modelId = ctx.model?.id ?? "unknown";
2056
+ snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
2057
+ }
2058
+ saveActivityLog(ctx, basePath, unitType, unitId);
2059
+ const expected = diagnoseExpectedArtifact(unitType, unitId, basePath);
2060
+ await stopAuto(ctx, pi);
2061
+ ctx.ui.notify(
2062
+ `Hard loop detected: ${unitType} ${unitId} dispatched ${lifetimeCount} times total (across reconciliation cycles). Stopping.${expected ? `\n Expected artifact: ${expected}` : ""}\n This may indicate deriveState() keeps returning the same unit despite artifacts existing.\n Check .gsd/completed-units.json and the slice plan checkbox state.`,
2063
+ "error",
2064
+ );
2065
+ return;
2066
+ }
1757
2067
  if (prevCount >= MAX_UNIT_DISPATCHES) {
1758
2068
  if (currentUnit) {
1759
2069
  const modelId = ctx.model?.id ?? "unknown";
@@ -1778,7 +2088,13 @@ async function dispatchNextUnit(
1778
2088
  `Loop recovery: ${unitId} reconciled after ${prevCount + 1} dispatches — blocker artifacts written, pipeline advancing.\n Review ${status.summaryPath} and replace the placeholder with real work.`,
1779
2089
  "warning",
1780
2090
  );
2091
+ // Persist completion so idempotency check prevents re-dispatch
2092
+ // if deriveState keeps returning this unit (#462).
2093
+ const reconciledKey = `${unitType}/${unitId}`;
2094
+ persistCompletedKey(basePath, reconciledKey);
2095
+ completedKeySet.add(reconciledKey);
1781
2096
  unitDispatchCount.delete(dispatchKey);
2097
+ invalidateStateCache();
1782
2098
  await new Promise(r => setImmediate(r));
1783
2099
  await dispatchNextUnit(ctx, pi);
1784
2100
  return;
@@ -1787,6 +2103,30 @@ async function dispatchNextUnit(
1787
2103
  }
1788
2104
  }
1789
2105
 
2106
+ // General reconciliation: if the last attempt DID produce the expected
2107
+ // artifact on disk, clear the counter and advance instead of stopping.
2108
+ // The execute-task path above handles its special case (writing placeholder
2109
+ // summaries). This catch-all covers complete-slice, plan-slice,
2110
+ // research-slice, and all other unit types where the Nth attempt at the
2111
+ // dispatch limit succeeded but the counter check fires before anyone
2112
+ // verifies disk state. Without this, a successful final attempt is
2113
+ // indistinguishable from a failed one.
2114
+ if (verifyExpectedArtifact(unitType, unitId, basePath)) {
2115
+ ctx.ui.notify(
2116
+ `Loop recovery: ${unitType} ${unitId} — artifact verified after ${prevCount + 1} dispatches. Advancing.`,
2117
+ "info",
2118
+ );
2119
+ // Persist completion so the idempotency check prevents re-dispatch
2120
+ // if deriveState keeps returning this unit (see #462).
2121
+ persistCompletedKey(basePath, dispatchKey);
2122
+ completedKeySet.add(dispatchKey);
2123
+ unitDispatchCount.delete(dispatchKey);
2124
+ invalidateStateCache();
2125
+ await new Promise(r => setImmediate(r));
2126
+ await dispatchNextUnit(ctx, pi);
2127
+ return;
2128
+ }
2129
+
1790
2130
  const expected = diagnoseExpectedArtifact(unitType, unitId, basePath);
1791
2131
  const remediation = buildLoopRemediationSteps(unitType, unitId, basePath);
1792
2132
  await stopAuto(ctx, pi);
@@ -1813,7 +2153,12 @@ async function dispatchNextUnit(
1813
2153
  `Self-repaired ${unitId}: summary existed but checkbox was unmarked. Marked [x] and advancing.`,
1814
2154
  "warning",
1815
2155
  );
2156
+ // Persist completion so idempotency check prevents re-dispatch (#462).
2157
+ const repairedKey = `${unitType}/${unitId}`;
2158
+ persistCompletedKey(basePath, repairedKey);
2159
+ completedKeySet.add(repairedKey);
1816
2160
  unitDispatchCount.delete(dispatchKey);
2161
+ invalidateStateCache();
1817
2162
  await new Promise(r => setImmediate(r));
1818
2163
  await dispatchNextUnit(ctx, pi);
1819
2164
  return;
@@ -2285,6 +2630,7 @@ async function buildResearchMilestonePrompt(mid: string, midTitle: string, base:
2285
2630
  if (requirementsInline) inlined.push(requirementsInline);
2286
2631
  const decisionsInline = await inlineGsdRootFile(base, "decisions.md", "Decisions");
2287
2632
  if (decisionsInline) inlined.push(decisionsInline);
2633
+ inlined.push(inlineTemplate("research", "Research"));
2288
2634
 
2289
2635
  const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
2290
2636
 
@@ -2317,6 +2663,11 @@ async function buildPlanMilestonePrompt(mid: string, midTitle: string, base: str
2317
2663
  if (requirementsInline) inlined.push(requirementsInline);
2318
2664
  const decisionsInline = await inlineGsdRootFile(base, "decisions.md", "Decisions");
2319
2665
  if (decisionsInline) inlined.push(decisionsInline);
2666
+ inlined.push(inlineTemplate("roadmap", "Roadmap"));
2667
+ inlined.push(inlineTemplate("decisions", "Decisions"));
2668
+ inlined.push(inlineTemplate("plan", "Slice Plan"));
2669
+ inlined.push(inlineTemplate("task-plan", "Task Plan"));
2670
+ inlined.push(inlineTemplate("secrets-manifest", "Secrets Manifest"));
2320
2671
 
2321
2672
  const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
2322
2673
 
@@ -2353,6 +2704,7 @@ async function buildResearchSlicePrompt(
2353
2704
  if (decisionsInline) inlined.push(decisionsInline);
2354
2705
  const requirementsInline = await inlineGsdRootFile(base, "requirements.md", "Requirements");
2355
2706
  if (requirementsInline) inlined.push(requirementsInline);
2707
+ inlined.push(inlineTemplate("research", "Research"));
2356
2708
 
2357
2709
  const depContent = await inlineDependencySummaries(mid, sid, base);
2358
2710
 
@@ -2388,6 +2740,8 @@ async function buildPlanSlicePrompt(
2388
2740
  if (decisionsInline) inlined.push(decisionsInline);
2389
2741
  const requirementsInline = await inlineGsdRootFile(base, "requirements.md", "Requirements");
2390
2742
  if (requirementsInline) inlined.push(requirementsInline);
2743
+ inlined.push(inlineTemplate("plan", "Slice Plan"));
2744
+ inlined.push(inlineTemplate("task-plan", "Task Plan"));
2391
2745
 
2392
2746
  const depContent = await inlineDependencySummaries(mid, sid, base);
2393
2747
 
@@ -2449,6 +2803,10 @@ async function buildExecuteTaskPrompt(
2449
2803
  );
2450
2804
 
2451
2805
  const carryForwardSection = await buildCarryForwardSection(priorSummaries, base);
2806
+ const inlinedTemplates = [
2807
+ inlineTemplate("task-summary", "Task Summary"),
2808
+ inlineTemplate("decisions", "Decisions"),
2809
+ ].join("\n\n---\n\n");
2452
2810
 
2453
2811
  const taskSummaryPath = `${relSlicePath(base, mid, sid)}/tasks/${tid}-SUMMARY.md`;
2454
2812
 
@@ -2463,6 +2821,7 @@ async function buildExecuteTaskPrompt(
2463
2821
  resumeSection,
2464
2822
  priorTaskLines: priorLines,
2465
2823
  taskSummaryPath,
2824
+ inlinedTemplates,
2466
2825
  });
2467
2826
  }
2468
2827
 
@@ -2495,6 +2854,8 @@ async function buildCompleteSlicePrompt(
2495
2854
  }
2496
2855
  }
2497
2856
  }
2857
+ inlined.push(inlineTemplate("slice-summary", "Slice Summary"));
2858
+ inlined.push(inlineTemplate("uat", "UAT"));
2498
2859
 
2499
2860
  const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
2500
2861
 
@@ -2547,6 +2908,7 @@ async function buildCompleteMilestonePrompt(
2547
2908
  const contextRel = relMilestoneFile(base, mid, "CONTEXT");
2548
2909
  const contextInline = await inlineFileOptional(contextPath, contextRel, "Milestone Context");
2549
2910
  if (contextInline) inlined.push(contextInline);
2911
+ inlined.push(inlineTemplate("milestone-summary", "Milestone Summary"));
2550
2912
 
2551
2913
  const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
2552
2914
 
@@ -2993,6 +3355,9 @@ async function collectObservabilityWarnings(
2993
3355
  unitType: string,
2994
3356
  unitId: string,
2995
3357
  ): Promise<import("./observability-validator.ts").ValidationIssue[]> {
3358
+ // Hook units have custom artifacts — skip standard observability checks
3359
+ if (unitType.startsWith("hook/")) return [];
3360
+
2996
3361
  const parts = unitId.split("/");
2997
3362
  const mid = parts[0];
2998
3363
  const sid = parts[1];
@@ -3404,6 +3769,9 @@ export function resolveExpectedArtifactPath(unitType: string, unitId: string, ba
3404
3769
  * skipped writing the UAT file (see #176).
3405
3770
  */
3406
3771
  export function verifyExpectedArtifact(unitType: string, unitId: string, base: string): boolean {
3772
+ // Clear stale directory listing cache so artifact checks see fresh disk state (#431)
3773
+ clearPathCache();
3774
+
3407
3775
  // fix-merge has no file artifact — verify by checking git state
3408
3776
  if (unitType === "fix-merge") {
3409
3777
  const unmerged = runGit(base, ["diff", "--name-only", "--diff-filter=U"], { allowFailure: true });