gsd-pi 2.33.1-dev.ee47f1b → 2.34.0-dev.bbb5216

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 (135) hide show
  1. package/dist/bundled-resource-path.d.ts +8 -0
  2. package/dist/bundled-resource-path.js +14 -0
  3. package/dist/headless-query.js +6 -6
  4. package/dist/resources/extensions/gsd/auto/session.js +27 -32
  5. package/dist/resources/extensions/gsd/auto-dashboard.js +29 -109
  6. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +6 -1
  7. package/dist/resources/extensions/gsd/auto-dispatch.js +52 -81
  8. package/dist/resources/extensions/gsd/auto-loop.js +956 -0
  9. package/dist/resources/extensions/gsd/auto-observability.js +4 -2
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +75 -185
  11. package/dist/resources/extensions/gsd/auto-prompts.js +133 -101
  12. package/dist/resources/extensions/gsd/auto-recovery.js +59 -97
  13. package/dist/resources/extensions/gsd/auto-start.js +330 -309
  14. package/dist/resources/extensions/gsd/auto-supervisor.js +5 -11
  15. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +7 -7
  16. package/dist/resources/extensions/gsd/auto-timers.js +3 -4
  17. package/dist/resources/extensions/gsd/auto-verification.js +35 -73
  18. package/dist/resources/extensions/gsd/auto-worktree-sync.js +167 -0
  19. package/dist/resources/extensions/gsd/auto-worktree.js +291 -126
  20. package/dist/resources/extensions/gsd/auto.js +283 -1013
  21. package/dist/resources/extensions/gsd/captures.js +10 -4
  22. package/dist/resources/extensions/gsd/dispatch-guard.js +7 -8
  23. package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -18
  24. package/dist/resources/extensions/gsd/doctor-checks.js +3 -4
  25. package/dist/resources/extensions/gsd/git-service.js +1 -1
  26. package/dist/resources/extensions/gsd/gsd-db.js +296 -151
  27. package/dist/resources/extensions/gsd/index.js +92 -228
  28. package/dist/resources/extensions/gsd/post-unit-hooks.js +13 -13
  29. package/dist/resources/extensions/gsd/progress-score.js +61 -156
  30. package/dist/resources/extensions/gsd/quick.js +98 -122
  31. package/dist/resources/extensions/gsd/session-lock.js +13 -0
  32. package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
  33. package/dist/resources/extensions/gsd/undo.js +43 -48
  34. package/dist/resources/extensions/gsd/unit-runtime.js +16 -15
  35. package/dist/resources/extensions/gsd/verification-evidence.js +0 -1
  36. package/dist/resources/extensions/gsd/verification-gate.js +6 -35
  37. package/dist/resources/extensions/gsd/worktree-command.js +30 -24
  38. package/dist/resources/extensions/gsd/worktree-manager.js +2 -3
  39. package/dist/resources/extensions/gsd/worktree-resolver.js +344 -0
  40. package/dist/resources/extensions/gsd/worktree.js +7 -44
  41. package/dist/tool-bootstrap.js +59 -11
  42. package/dist/worktree-cli.js +7 -7
  43. package/package.json +1 -1
  44. package/packages/pi-ai/dist/models.generated.d.ts +3630 -5483
  45. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  46. package/packages/pi-ai/dist/models.generated.js +735 -2588
  47. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  48. package/packages/pi-ai/src/models.generated.ts +1039 -2892
  49. package/packages/pi-coding-agent/package.json +1 -1
  50. package/pkg/package.json +1 -1
  51. package/src/resources/extensions/gsd/auto/session.ts +47 -30
  52. package/src/resources/extensions/gsd/auto-dashboard.ts +28 -131
  53. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +6 -1
  54. package/src/resources/extensions/gsd/auto-dispatch.ts +135 -91
  55. package/src/resources/extensions/gsd/auto-loop.ts +1665 -0
  56. package/src/resources/extensions/gsd/auto-observability.ts +4 -2
  57. package/src/resources/extensions/gsd/auto-post-unit.ts +85 -228
  58. package/src/resources/extensions/gsd/auto-prompts.ts +138 -109
  59. package/src/resources/extensions/gsd/auto-recovery.ts +124 -118
  60. package/src/resources/extensions/gsd/auto-start.ts +440 -354
  61. package/src/resources/extensions/gsd/auto-supervisor.ts +5 -12
  62. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +8 -8
  63. package/src/resources/extensions/gsd/auto-timers.ts +3 -4
  64. package/src/resources/extensions/gsd/auto-verification.ts +76 -90
  65. package/src/resources/extensions/gsd/auto-worktree-sync.ts +204 -0
  66. package/src/resources/extensions/gsd/auto-worktree.ts +389 -141
  67. package/src/resources/extensions/gsd/auto.ts +515 -1199
  68. package/src/resources/extensions/gsd/captures.ts +10 -4
  69. package/src/resources/extensions/gsd/dispatch-guard.ts +13 -9
  70. package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -18
  71. package/src/resources/extensions/gsd/doctor-checks.ts +3 -4
  72. package/src/resources/extensions/gsd/git-service.ts +8 -1
  73. package/src/resources/extensions/gsd/gitignore.ts +4 -2
  74. package/src/resources/extensions/gsd/gsd-db.ts +375 -180
  75. package/src/resources/extensions/gsd/index.ts +104 -263
  76. package/src/resources/extensions/gsd/post-unit-hooks.ts +13 -13
  77. package/src/resources/extensions/gsd/progress-score.ts +65 -200
  78. package/src/resources/extensions/gsd/quick.ts +121 -125
  79. package/src/resources/extensions/gsd/session-lock.ts +11 -0
  80. package/src/resources/extensions/gsd/templates/preferences.md +1 -0
  81. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +32 -59
  82. package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +75 -27
  83. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
  84. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +37 -0
  85. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1458 -0
  86. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +8 -162
  87. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -108
  88. package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +1 -3
  89. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +0 -3
  90. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  91. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -55
  92. package/src/resources/extensions/gsd/tests/headless-query.test.ts +22 -0
  93. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +8 -11
  94. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +4 -6
  95. package/src/resources/extensions/gsd/tests/run-uat.test.ts +3 -3
  96. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +64 -0
  97. package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +181 -0
  98. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +0 -3
  99. package/src/resources/extensions/gsd/tests/token-profile.test.ts +6 -6
  100. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -6
  101. package/src/resources/extensions/gsd/tests/undo.test.ts +6 -0
  102. package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +24 -26
  103. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +7 -201
  104. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  105. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  106. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +0 -3
  107. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +705 -0
  108. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +57 -106
  109. package/src/resources/extensions/gsd/tests/worktree.test.ts +5 -1
  110. package/src/resources/extensions/gsd/tests/write-gate.test.ts +43 -132
  111. package/src/resources/extensions/gsd/types.ts +90 -81
  112. package/src/resources/extensions/gsd/undo.ts +42 -46
  113. package/src/resources/extensions/gsd/unit-runtime.ts +14 -18
  114. package/src/resources/extensions/gsd/verification-evidence.ts +1 -3
  115. package/src/resources/extensions/gsd/verification-gate.ts +6 -39
  116. package/src/resources/extensions/gsd/worktree-command.ts +36 -24
  117. package/src/resources/extensions/gsd/worktree-manager.ts +2 -3
  118. package/src/resources/extensions/gsd/worktree-resolver.ts +485 -0
  119. package/src/resources/extensions/gsd/worktree.ts +7 -44
  120. package/dist/resources/extensions/gsd/auto-constants.js +0 -5
  121. package/dist/resources/extensions/gsd/auto-idempotency.js +0 -106
  122. package/dist/resources/extensions/gsd/auto-stuck-detection.js +0 -165
  123. package/dist/resources/extensions/gsd/mechanical-completion.js +0 -351
  124. package/src/resources/extensions/gsd/auto-constants.ts +0 -6
  125. package/src/resources/extensions/gsd/auto-idempotency.ts +0 -151
  126. package/src/resources/extensions/gsd/auto-stuck-detection.ts +0 -221
  127. package/src/resources/extensions/gsd/mechanical-completion.ts +0 -430
  128. package/src/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +0 -691
  129. package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +0 -127
  130. package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +0 -123
  131. package/src/resources/extensions/gsd/tests/dispatch-stall-guard.test.ts +0 -126
  132. package/src/resources/extensions/gsd/tests/loop-regression.test.ts +0 -874
  133. package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +0 -356
  134. package/src/resources/extensions/gsd/tests/progress-score.test.ts +0 -206
  135. package/src/resources/extensions/gsd/tests/session-lock.test.ts +0 -434
@@ -11,10 +11,15 @@
11
11
 
12
12
  import type { GSDState } from "./types.js";
13
13
  import type { GSDPreferences } from "./preferences.js";
14
- import { loadFile, loadActiveOverrides, parseRoadmap } from "./files.js";
14
+ import type { UatType } from "./files.js";
15
+ import { loadFile, extractUatType, loadActiveOverrides } from "./files.js";
15
16
  import {
16
- resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveTaskFile,
17
- relSliceFile, buildMilestoneFileName,
17
+ resolveMilestoneFile,
18
+ resolveMilestonePath,
19
+ resolveSliceFile,
20
+ resolveTaskFile,
21
+ relSliceFile,
22
+ buildMilestoneFileName,
18
23
  } from "./paths.js";
19
24
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
20
25
  import { join } from "node:path";
@@ -38,7 +43,13 @@ import {
38
43
  // ─── Types ────────────────────────────────────────────────────────────────
39
44
 
40
45
  export type DispatchAction =
41
- | { action: "dispatch"; unitType: string; unitId: string; prompt: string }
46
+ | {
47
+ action: "dispatch";
48
+ unitType: string;
49
+ unitId: string;
50
+ prompt: string;
51
+ pauseAfterDispatch?: boolean;
52
+ }
42
53
  | { action: "stop"; reason: string; level: "info" | "warning" | "error" }
43
54
  | { action: "skip" };
44
55
 
@@ -57,6 +68,14 @@ interface DispatchRule {
57
68
  match: (ctx: DispatchContext) => Promise<DispatchAction | null>;
58
69
  }
59
70
 
71
+ function missingSliceStop(mid: string, phase: string): DispatchAction {
72
+ return {
73
+ action: "stop",
74
+ reason: `${mid}: phase "${phase}" has no active slice — run /gsd doctor.`,
75
+ level: "error",
76
+ };
77
+ }
78
+
60
79
  // ─── Rewrite Circuit Breaker ──────────────────────────────────────────────
61
80
 
62
81
  const MAX_REWRITE_ATTEMPTS = 3;
@@ -65,28 +84,6 @@ export function resetRewriteCircuitBreaker(): void {
65
84
  rewriteAttemptCount = 0;
66
85
  }
67
86
 
68
- /**
69
- * Guard for accessing activeSlice/activeTask in dispatch rules.
70
- * Returns a stop action if the expected ref is null (corrupt state).
71
- */
72
- function requireSlice(state: GSDState): { sid: string; sTitle: string } | DispatchAction {
73
- if (!state.activeSlice) {
74
- return { action: "stop", reason: `Phase "${state.phase}" but no active slice — run /gsd doctor.`, level: "error" };
75
- }
76
- return { sid: state.activeSlice.id, sTitle: state.activeSlice.title };
77
- }
78
-
79
- function requireTask(state: GSDState): { sid: string; sTitle: string; tid: string; tTitle: string } | DispatchAction {
80
- if (!state.activeSlice || !state.activeTask) {
81
- return { action: "stop", reason: `Phase "${state.phase}" but no active slice/task — run /gsd doctor.`, level: "error" };
82
- }
83
- return { sid: state.activeSlice.id, sTitle: state.activeSlice.title, tid: state.activeTask.id, tTitle: state.activeTask.title };
84
- }
85
-
86
- function isStopAction(v: unknown): v is DispatchAction {
87
- return typeof v === "object" && v !== null && "action" in v;
88
- }
89
-
90
87
  // ─── Rules ────────────────────────────────────────────────────────────────
91
88
 
92
89
  const DISPATCH_RULES: DispatchRule[] = [
@@ -107,7 +104,13 @@ const DISPATCH_RULES: DispatchRule[] = [
107
104
  action: "dispatch",
108
105
  unitType: "rewrite-docs",
109
106
  unitId,
110
- prompt: await buildRewriteDocsPrompt(mid, midTitle, state.activeSlice, basePath, pendingOverrides),
107
+ prompt: await buildRewriteDocsPrompt(
108
+ mid,
109
+ midTitle,
110
+ state.activeSlice,
111
+ basePath,
112
+ pendingOverrides,
113
+ ),
111
114
  };
112
115
  },
113
116
  },
@@ -115,74 +118,63 @@ const DISPATCH_RULES: DispatchRule[] = [
115
118
  name: "summarizing → complete-slice",
116
119
  match: async ({ state, mid, midTitle, basePath }) => {
117
120
  if (state.phase !== "summarizing") return null;
118
- const sliceRef = requireSlice(state);
119
- if (isStopAction(sliceRef)) return sliceRef as DispatchAction;
120
- const { sid, sTitle } = sliceRef;
121
+ if (!state.activeSlice) return missingSliceStop(mid, state.phase);
122
+ const sid = state.activeSlice!.id;
123
+ const sTitle = state.activeSlice!.title;
121
124
  return {
122
125
  action: "dispatch",
123
126
  unitType: "complete-slice",
124
127
  unitId: `${mid}/${sid}`,
125
- prompt: await buildCompleteSlicePrompt(mid, midTitle, sid, sTitle, basePath),
128
+ prompt: await buildCompleteSlicePrompt(
129
+ mid,
130
+ midTitle,
131
+ sid,
132
+ sTitle,
133
+ basePath,
134
+ ),
126
135
  };
127
136
  },
128
137
  },
129
- {
130
- name: "uat-verdict-gate (non-PASS blocks progression)",
131
- match: async ({ mid, basePath, prefs }) => {
132
- // Only applies when UAT dispatch is enabled
133
- if (!prefs?.uat_dispatch) return null;
134
-
135
- const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
136
- const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
137
- if (!roadmapContent) return null;
138
-
139
- const roadmap = parseRoadmap(roadmapContent);
140
- for (const slice of roadmap.slices.filter(s => s.done)) {
141
- const resultFile = resolveSliceFile(basePath, mid, slice.id, "UAT-RESULT");
142
- if (!resultFile) continue;
143
- const content = await loadFile(resultFile);
144
- if (!content) continue;
145
- const verdictMatch = content.match(/verdict:\s*([\w-]+)/i);
146
- const verdict = verdictMatch?.[1]?.toLowerCase();
147
- if (verdict && verdict !== "pass" && verdict !== "passed") {
148
- return {
149
- action: "stop" as const,
150
- reason: `UAT verdict for ${slice.id} is "${verdict}" — blocking progression until resolved.\nReview the UAT result and update the verdict to PASS, or re-run /gsd auto after fixing.`,
151
- level: "warning" as const,
152
- };
153
- }
154
- }
155
- return null;
156
- },
157
- },
158
138
  {
159
139
  name: "run-uat (post-completion)",
160
140
  match: async ({ state, mid, basePath, prefs }) => {
161
141
  const needsRunUat = await checkNeedsRunUat(basePath, mid, state, prefs);
162
142
  if (!needsRunUat) return null;
163
- const { sliceId } = needsRunUat;
143
+ const { sliceId, uatType } = needsRunUat;
144
+ const uatFile = resolveSliceFile(basePath, mid, sliceId, "UAT")!;
145
+ const uatContent = await loadFile(uatFile);
164
146
  return {
165
147
  action: "dispatch",
166
148
  unitType: "run-uat",
167
149
  unitId: `${mid}/${sliceId}`,
168
150
  prompt: await buildRunUatPrompt(
169
- mid, sliceId, relSliceFile(basePath, mid, sliceId, "UAT"), basePath,
151
+ mid,
152
+ sliceId,
153
+ relSliceFile(basePath, mid, sliceId, "UAT"),
154
+ uatContent ?? "",
155
+ basePath,
170
156
  ),
157
+ pauseAfterDispatch: uatType !== "artifact-driven",
171
158
  };
172
159
  },
173
160
  },
174
161
  {
175
162
  name: "reassess-roadmap (post-completion)",
176
163
  match: async ({ state, mid, midTitle, basePath, prefs }) => {
177
- // Reassess is opt-in: only fire when explicitly enabled
178
- if (!prefs?.phases?.reassess_after_slice) return null;
164
+ if (prefs?.phases?.skip_reassess || !prefs?.phases?.reassess_after_slice)
165
+ return null;
179
166
  const needsReassess = await checkNeedsReassessment(basePath, mid, state);
180
167
  if (!needsReassess) return null;
181
168
  return {
182
169
  action: "dispatch",
183
170
  unitType: "reassess-roadmap",
184
171
  unitId: `${mid}/${needsReassess.sliceId}`,
185
- prompt: await buildReassessRoadmapPrompt(mid, midTitle, needsReassess.sliceId, basePath),
172
+ prompt: await buildReassessRoadmapPrompt(
173
+ mid,
174
+ midTitle,
175
+ needsReassess.sliceId,
176
+ basePath,
177
+ ),
186
178
  };
187
179
  },
188
180
  },
@@ -202,7 +194,7 @@ const DISPATCH_RULES: DispatchRule[] = [
202
194
  match: async ({ state, mid, basePath }) => {
203
195
  if (state.phase !== "pre-planning") return null;
204
196
  const contextFile = resolveMilestoneFile(basePath, mid, "CONTEXT");
205
- const hasContext = !!(contextFile && await loadFile(contextFile));
197
+ const hasContext = !!(contextFile && (await loadFile(contextFile)));
206
198
  if (hasContext) return null; // fall through to next rule
207
199
  return {
208
200
  action: "stop",
@@ -244,21 +236,32 @@ const DISPATCH_RULES: DispatchRule[] = [
244
236
  match: async ({ state, mid, midTitle, basePath, prefs }) => {
245
237
  if (state.phase !== "planning") return null;
246
238
  // Phase skip: skip research when preference or profile says so
247
- if (prefs?.phases?.skip_research || prefs?.phases?.skip_slice_research) return null;
248
- const sliceRef = requireSlice(state);
249
- if (isStopAction(sliceRef)) return sliceRef as DispatchAction;
250
- const { sid, sTitle } = sliceRef;
239
+ if (prefs?.phases?.skip_research || prefs?.phases?.skip_slice_research)
240
+ return null;
241
+ if (!state.activeSlice) return missingSliceStop(mid, state.phase);
242
+ const sid = state.activeSlice!.id;
243
+ const sTitle = state.activeSlice!.title;
251
244
  const researchFile = resolveSliceFile(basePath, mid, sid, "RESEARCH");
252
245
  if (researchFile) return null; // has research, fall through
253
246
  // Skip slice research for S01 when milestone research already exists —
254
247
  // the milestone research already covers the same ground for the first slice.
255
- const milestoneResearchFile = resolveMilestoneFile(basePath, mid, "RESEARCH");
248
+ const milestoneResearchFile = resolveMilestoneFile(
249
+ basePath,
250
+ mid,
251
+ "RESEARCH",
252
+ );
256
253
  if (milestoneResearchFile && sid === "S01") return null; // fall through to plan-slice
257
254
  return {
258
255
  action: "dispatch",
259
256
  unitType: "research-slice",
260
257
  unitId: `${mid}/${sid}`,
261
- prompt: await buildResearchSlicePrompt(mid, midTitle, sid, sTitle, basePath),
258
+ prompt: await buildResearchSlicePrompt(
259
+ mid,
260
+ midTitle,
261
+ sid,
262
+ sTitle,
263
+ basePath,
264
+ ),
262
265
  };
263
266
  },
264
267
  },
@@ -266,14 +269,20 @@ const DISPATCH_RULES: DispatchRule[] = [
266
269
  name: "planning → plan-slice",
267
270
  match: async ({ state, mid, midTitle, basePath }) => {
268
271
  if (state.phase !== "planning") return null;
269
- const sliceRef = requireSlice(state);
270
- if (isStopAction(sliceRef)) return sliceRef as DispatchAction;
271
- const { sid, sTitle } = sliceRef;
272
+ if (!state.activeSlice) return missingSliceStop(mid, state.phase);
273
+ const sid = state.activeSlice!.id;
274
+ const sTitle = state.activeSlice!.title;
272
275
  return {
273
276
  action: "dispatch",
274
277
  unitType: "plan-slice",
275
278
  unitId: `${mid}/${sid}`,
276
- prompt: await buildPlanSlicePrompt(mid, midTitle, sid, sTitle, basePath),
279
+ prompt: await buildPlanSlicePrompt(
280
+ mid,
281
+ midTitle,
282
+ sid,
283
+ sTitle,
284
+ basePath,
285
+ ),
277
286
  };
278
287
  },
279
288
  },
@@ -281,14 +290,20 @@ const DISPATCH_RULES: DispatchRule[] = [
281
290
  name: "replanning-slice → replan-slice",
282
291
  match: async ({ state, mid, midTitle, basePath }) => {
283
292
  if (state.phase !== "replanning-slice") return null;
284
- const sliceRef = requireSlice(state);
285
- if (isStopAction(sliceRef)) return sliceRef as DispatchAction;
286
- const { sid, sTitle } = sliceRef;
293
+ if (!state.activeSlice) return missingSliceStop(mid, state.phase);
294
+ const sid = state.activeSlice!.id;
295
+ const sTitle = state.activeSlice!.title;
287
296
  return {
288
297
  action: "dispatch",
289
298
  unitType: "replan-slice",
290
299
  unitId: `${mid}/${sid}`,
291
- prompt: await buildReplanSlicePrompt(mid, midTitle, sid, sTitle, basePath),
300
+ prompt: await buildReplanSlicePrompt(
301
+ mid,
302
+ midTitle,
303
+ sid,
304
+ sTitle,
305
+ basePath,
306
+ ),
292
307
  };
293
308
  },
294
309
  },
@@ -296,9 +311,9 @@ const DISPATCH_RULES: DispatchRule[] = [
296
311
  name: "executing → execute-task (recover missing task plan → plan-slice)",
297
312
  match: async ({ state, mid, midTitle, basePath }) => {
298
313
  if (state.phase !== "executing" || !state.activeTask) return null;
299
- const sliceRef = requireSlice(state);
300
- if (isStopAction(sliceRef)) return sliceRef as DispatchAction;
301
- const { sid, sTitle } = sliceRef;
314
+ if (!state.activeSlice) return missingSliceStop(mid, state.phase);
315
+ const sid = state.activeSlice!.id;
316
+ const sTitle = state.activeSlice!.title;
302
317
  const tid = state.activeTask.id;
303
318
 
304
319
  // Guard: if the slice plan exists but the individual task plan files are
@@ -312,7 +327,13 @@ const DISPATCH_RULES: DispatchRule[] = [
312
327
  action: "dispatch",
313
328
  unitType: "plan-slice",
314
329
  unitId: `${mid}/${sid}`,
315
- prompt: await buildPlanSlicePrompt(mid, midTitle, sid, sTitle, basePath),
330
+ prompt: await buildPlanSlicePrompt(
331
+ mid,
332
+ midTitle,
333
+ sid,
334
+ sTitle,
335
+ basePath,
336
+ ),
316
337
  };
317
338
  }
318
339
 
@@ -323,9 +344,9 @@ const DISPATCH_RULES: DispatchRule[] = [
323
344
  name: "executing → execute-task",
324
345
  match: async ({ state, mid, basePath }) => {
325
346
  if (state.phase !== "executing" || !state.activeTask) return null;
326
- const sliceRef = requireSlice(state);
327
- if (isStopAction(sliceRef)) return sliceRef as DispatchAction;
328
- const { sid, sTitle } = sliceRef;
347
+ if (!state.activeSlice) return missingSliceStop(mid, state.phase);
348
+ const sid = state.activeSlice!.id;
349
+ const sTitle = state.activeSlice!.title;
329
350
  const tid = state.activeTask.id;
330
351
  const tTitle = state.activeTask.title;
331
352
 
@@ -333,7 +354,14 @@ const DISPATCH_RULES: DispatchRule[] = [
333
354
  action: "dispatch",
334
355
  unitType: "execute-task",
335
356
  unitId: `${mid}/${sid}/${tid}`,
336
- prompt: await buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, basePath),
357
+ prompt: await buildExecuteTaskPrompt(
358
+ mid,
359
+ sid,
360
+ sTitle,
361
+ tid,
362
+ tTitle,
363
+ basePath,
364
+ ),
337
365
  };
338
366
  },
339
367
  },
@@ -346,7 +374,10 @@ const DISPATCH_RULES: DispatchRule[] = [
346
374
  const mDir = resolveMilestonePath(basePath, mid);
347
375
  if (mDir) {
348
376
  if (!existsSync(mDir)) mkdirSync(mDir, { recursive: true });
349
- const validationPath = join(mDir, buildMilestoneFileName(mid, "VALIDATION"));
377
+ const validationPath = join(
378
+ mDir,
379
+ buildMilestoneFileName(mid, "VALIDATION"),
380
+ );
350
381
  const content = [
351
382
  "---",
352
383
  "verdict: pass",
@@ -381,6 +412,17 @@ const DISPATCH_RULES: DispatchRule[] = [
381
412
  };
382
413
  },
383
414
  },
415
+ {
416
+ name: "complete → stop",
417
+ match: async ({ state }) => {
418
+ if (state.phase !== "complete") return null;
419
+ return {
420
+ action: "stop",
421
+ reason: "All milestones complete.",
422
+ level: "info",
423
+ };
424
+ },
425
+ },
384
426
  ];
385
427
 
386
428
  // ─── Resolver ─────────────────────────────────────────────────────────────
@@ -389,7 +431,9 @@ const DISPATCH_RULES: DispatchRule[] = [
389
431
  * Evaluate dispatch rules in order. Returns the first matching action,
390
432
  * or a "stop" action if no rule matches (unhandled phase).
391
433
  */
392
- export async function resolveDispatch(ctx: DispatchContext): Promise<DispatchAction> {
434
+ export async function resolveDispatch(
435
+ ctx: DispatchContext,
436
+ ): Promise<DispatchAction> {
393
437
  for (const rule of DISPATCH_RULES) {
394
438
  const result = await rule.match(ctx);
395
439
  if (result) return result;
@@ -405,5 +449,5 @@ export async function resolveDispatch(ctx: DispatchContext): Promise<DispatchAct
405
449
 
406
450
  /** Exposed for testing — returns the rule names in evaluation order. */
407
451
  export function getDispatchRuleNames(): string[] {
408
- return DISPATCH_RULES.map(r => r.name);
452
+ return DISPATCH_RULES.map((r) => r.name);
409
453
  }