gsd-pi 2.26.0 → 2.27.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 (171) hide show
  1. package/README.md +43 -6
  2. package/dist/cli.js +4 -2
  3. package/dist/headless.d.ts +3 -0
  4. package/dist/headless.js +136 -8
  5. package/dist/help-text.js +3 -0
  6. package/dist/loader.js +33 -4
  7. package/dist/resources/extensions/bg-shell/index.ts +19 -2
  8. package/dist/resources/extensions/bg-shell/process-manager.ts +45 -0
  9. package/dist/resources/extensions/bg-shell/types.ts +21 -1
  10. package/dist/resources/extensions/gsd/auto/session.ts +224 -0
  11. package/dist/resources/extensions/gsd/auto-budget.ts +32 -0
  12. package/dist/resources/extensions/gsd/auto-dashboard.ts +63 -10
  13. package/dist/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
  14. package/dist/resources/extensions/gsd/auto-dispatch.ts +23 -10
  15. package/dist/resources/extensions/gsd/auto-model-selection.ts +179 -0
  16. package/dist/resources/extensions/gsd/auto-observability.ts +74 -0
  17. package/dist/resources/extensions/gsd/auto-prompts.ts +0 -1
  18. package/dist/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
  19. package/dist/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
  20. package/dist/resources/extensions/gsd/auto-unit-closeout.ts +46 -0
  21. package/dist/resources/extensions/gsd/auto-worktree-sync.ts +207 -0
  22. package/dist/resources/extensions/gsd/auto.ts +977 -1551
  23. package/dist/resources/extensions/gsd/commands.ts +3 -3
  24. package/dist/resources/extensions/gsd/dashboard-overlay.ts +47 -72
  25. package/dist/resources/extensions/gsd/doctor-proactive.ts +9 -4
  26. package/dist/resources/extensions/gsd/export-html.ts +1001 -0
  27. package/dist/resources/extensions/gsd/export.ts +49 -1
  28. package/dist/resources/extensions/gsd/git-service.ts +6 -0
  29. package/dist/resources/extensions/gsd/gitignore.ts +4 -1
  30. package/dist/resources/extensions/gsd/guided-flow.ts +24 -5
  31. package/dist/resources/extensions/gsd/index.ts +54 -1
  32. package/dist/resources/extensions/gsd/native-git-bridge.ts +30 -2
  33. package/dist/resources/extensions/gsd/observability-validator.ts +21 -0
  34. package/dist/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
  35. package/dist/resources/extensions/gsd/preferences.ts +62 -1
  36. package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -3
  37. package/dist/resources/extensions/gsd/prompts/system.md +1 -1
  38. package/dist/resources/extensions/gsd/reports.ts +510 -0
  39. package/dist/resources/extensions/gsd/roadmap-slices.ts +1 -1
  40. package/dist/resources/extensions/gsd/skills/gsd-headless/SKILL.md +178 -0
  41. package/dist/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +54 -0
  42. package/dist/resources/extensions/gsd/skills/gsd-headless/references/commands.md +59 -0
  43. package/dist/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +185 -0
  44. package/dist/resources/extensions/gsd/state.ts +30 -0
  45. package/dist/resources/extensions/gsd/templates/task-summary.md +9 -0
  46. package/dist/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
  47. package/dist/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
  48. package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
  49. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
  50. package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
  51. package/dist/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
  52. package/dist/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
  53. package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
  54. package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
  55. package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +1 -1
  56. package/dist/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
  57. package/dist/resources/extensions/gsd/tests/network-error-fallback.test.ts +51 -1
  58. package/dist/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
  59. package/dist/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
  60. package/dist/resources/extensions/gsd/tests/parallel-merge.test.ts +465 -0
  61. package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
  62. package/dist/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +71 -0
  63. package/dist/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
  64. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +9 -9
  65. package/dist/resources/extensions/gsd/tests/verification-evidence.test.ts +743 -0
  66. package/dist/resources/extensions/gsd/tests/verification-gate.test.ts +965 -0
  67. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
  68. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
  69. package/dist/resources/extensions/gsd/tests/worktree.test.ts +3 -1
  70. package/dist/resources/extensions/gsd/types.ts +38 -0
  71. package/dist/resources/extensions/gsd/verification-evidence.ts +183 -0
  72. package/dist/resources/extensions/gsd/verification-gate.ts +567 -0
  73. package/dist/resources/extensions/gsd/visualizer-data.ts +25 -3
  74. package/dist/resources/extensions/gsd/visualizer-overlay.ts +31 -21
  75. package/dist/resources/extensions/gsd/visualizer-views.ts +15 -66
  76. package/dist/resources/extensions/search-the-web/tool-search.ts +26 -0
  77. package/dist/resources/extensions/shared/format-utils.ts +85 -0
  78. package/dist/resources/extensions/shared/tests/format-utils.test.ts +153 -0
  79. package/dist/resources/extensions/subagent/index.ts +46 -1
  80. package/dist/resources/extensions/subagent/isolation.ts +9 -6
  81. package/package.json +1 -1
  82. package/packages/pi-ai/dist/providers/openai-completions.js +7 -4
  83. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  84. package/packages/pi-ai/src/providers/openai-completions.ts +7 -4
  85. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  86. package/packages/pi-coding-agent/dist/core/lsp/client.js +7 -0
  87. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  88. package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -1
  89. package/packages/pi-coding-agent/dist/core/lsp/config.js +9 -2
  90. package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -1
  91. package/packages/pi-coding-agent/src/core/lsp/client.ts +8 -0
  92. package/packages/pi-coding-agent/src/core/lsp/config.ts +9 -2
  93. package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
  94. package/packages/pi-tui/dist/components/editor.js +1 -1
  95. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  96. package/packages/pi-tui/src/components/editor.ts +3 -1
  97. package/scripts/link-workspace-packages.cjs +22 -6
  98. package/src/resources/extensions/bg-shell/index.ts +19 -2
  99. package/src/resources/extensions/bg-shell/process-manager.ts +45 -0
  100. package/src/resources/extensions/bg-shell/types.ts +21 -1
  101. package/src/resources/extensions/gsd/auto/session.ts +224 -0
  102. package/src/resources/extensions/gsd/auto-budget.ts +32 -0
  103. package/src/resources/extensions/gsd/auto-dashboard.ts +63 -10
  104. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
  105. package/src/resources/extensions/gsd/auto-dispatch.ts +23 -10
  106. package/src/resources/extensions/gsd/auto-model-selection.ts +179 -0
  107. package/src/resources/extensions/gsd/auto-observability.ts +74 -0
  108. package/src/resources/extensions/gsd/auto-prompts.ts +0 -1
  109. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
  110. package/src/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
  111. package/src/resources/extensions/gsd/auto-unit-closeout.ts +46 -0
  112. package/src/resources/extensions/gsd/auto-worktree-sync.ts +207 -0
  113. package/src/resources/extensions/gsd/auto.ts +977 -1551
  114. package/src/resources/extensions/gsd/commands.ts +3 -3
  115. package/src/resources/extensions/gsd/dashboard-overlay.ts +47 -72
  116. package/src/resources/extensions/gsd/doctor-proactive.ts +9 -4
  117. package/src/resources/extensions/gsd/export-html.ts +1001 -0
  118. package/src/resources/extensions/gsd/export.ts +49 -1
  119. package/src/resources/extensions/gsd/git-service.ts +6 -0
  120. package/src/resources/extensions/gsd/gitignore.ts +4 -1
  121. package/src/resources/extensions/gsd/guided-flow.ts +24 -5
  122. package/src/resources/extensions/gsd/index.ts +54 -1
  123. package/src/resources/extensions/gsd/native-git-bridge.ts +30 -2
  124. package/src/resources/extensions/gsd/observability-validator.ts +21 -0
  125. package/src/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
  126. package/src/resources/extensions/gsd/preferences.ts +62 -1
  127. package/src/resources/extensions/gsd/prompts/execute-task.md +4 -3
  128. package/src/resources/extensions/gsd/prompts/system.md +1 -1
  129. package/src/resources/extensions/gsd/reports.ts +510 -0
  130. package/src/resources/extensions/gsd/roadmap-slices.ts +1 -1
  131. package/src/resources/extensions/gsd/skills/gsd-headless/SKILL.md +178 -0
  132. package/src/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +54 -0
  133. package/src/resources/extensions/gsd/skills/gsd-headless/references/commands.md +59 -0
  134. package/src/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +185 -0
  135. package/src/resources/extensions/gsd/state.ts +30 -0
  136. package/src/resources/extensions/gsd/templates/task-summary.md +9 -0
  137. package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
  138. package/src/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
  139. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
  140. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
  141. package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
  142. package/src/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
  143. package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
  144. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
  145. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
  146. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +1 -1
  147. package/src/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
  148. package/src/resources/extensions/gsd/tests/network-error-fallback.test.ts +51 -1
  149. package/src/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
  150. package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
  151. package/src/resources/extensions/gsd/tests/parallel-merge.test.ts +465 -0
  152. package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
  153. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +71 -0
  154. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
  155. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +9 -9
  156. package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +743 -0
  157. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +965 -0
  158. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
  159. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
  160. package/src/resources/extensions/gsd/tests/worktree.test.ts +3 -1
  161. package/src/resources/extensions/gsd/types.ts +38 -0
  162. package/src/resources/extensions/gsd/verification-evidence.ts +183 -0
  163. package/src/resources/extensions/gsd/verification-gate.ts +567 -0
  164. package/src/resources/extensions/gsd/visualizer-data.ts +25 -3
  165. package/src/resources/extensions/gsd/visualizer-overlay.ts +31 -21
  166. package/src/resources/extensions/gsd/visualizer-views.ts +15 -66
  167. package/src/resources/extensions/search-the-web/tool-search.ts +26 -0
  168. package/src/resources/extensions/shared/format-utils.ts +85 -0
  169. package/src/resources/extensions/shared/tests/format-utils.test.ts +153 -0
  170. package/src/resources/extensions/subagent/index.ts +46 -1
  171. package/src/resources/extensions/subagent/isolation.ts +9 -6
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Direct phase dispatch — handles manual /gsd dispatch commands.
3
+ * Resolves phase name → unit type + prompt, creates a session, and sends the message.
4
+ */
5
+
6
+ import type {
7
+ ExtensionAPI,
8
+ ExtensionCommandContext,
9
+ } from "@gsd/pi-coding-agent";
10
+
11
+ import { deriveState } from "./state.js";
12
+ import { loadFile, parseRoadmap } from "./files.js";
13
+ import {
14
+ resolveMilestoneFile, resolveSliceFile, relSliceFile,
15
+ } from "./paths.js";
16
+ import {
17
+ buildResearchSlicePrompt,
18
+ buildResearchMilestonePrompt,
19
+ buildPlanSlicePrompt,
20
+ buildPlanMilestonePrompt,
21
+ buildExecuteTaskPrompt,
22
+ buildCompleteSlicePrompt,
23
+ buildCompleteMilestonePrompt,
24
+ buildReassessRoadmapPrompt,
25
+ buildRunUatPrompt,
26
+ buildReplanSlicePrompt,
27
+ } from "./auto-prompts.js";
28
+ import { loadEffectiveGSDPreferences } from "./preferences.js";
29
+ import { pauseAuto } from "./auto.js";
30
+
31
+ export async function dispatchDirectPhase(
32
+ ctx: ExtensionCommandContext,
33
+ pi: ExtensionAPI,
34
+ phase: string,
35
+ base: string,
36
+ ): Promise<void> {
37
+ const state = await deriveState(base);
38
+ const mid = state.activeMilestone?.id;
39
+ const midTitle = state.activeMilestone?.title ?? "";
40
+
41
+ if (!mid) {
42
+ ctx.ui.notify("Cannot dispatch: no active milestone.", "warning");
43
+ return;
44
+ }
45
+
46
+ const normalized = phase.toLowerCase();
47
+ let unitType: string;
48
+ let unitId: string;
49
+ let prompt: string;
50
+
51
+ switch (normalized) {
52
+ case "research":
53
+ case "research-milestone":
54
+ case "research-slice": {
55
+ const isSlice = normalized === "research-slice" || (normalized === "research" && state.phase !== "pre-planning");
56
+ if (isSlice) {
57
+ const sid = state.activeSlice?.id;
58
+ const sTitle = state.activeSlice?.title ?? "";
59
+ if (!sid) {
60
+ ctx.ui.notify("Cannot dispatch research-slice: no active slice.", "warning");
61
+ return;
62
+ }
63
+
64
+ // When require_slice_discussion is enabled, pause auto-mode before
65
+ // each new slice so the user can discuss requirements first (#789).
66
+ const sliceContextFile = resolveSliceFile(base, mid, sid, "CONTEXT");
67
+ const requireDiscussion = loadEffectiveGSDPreferences()?.preferences?.phases?.require_slice_discussion;
68
+ if (requireDiscussion && !sliceContextFile) {
69
+ ctx.ui.notify(
70
+ `Slice ${sid} requires discussion before planning. Run /gsd discuss to discuss this slice, then /gsd auto to resume.`,
71
+ "info",
72
+ );
73
+ await pauseAuto(ctx, pi);
74
+ return;
75
+ }
76
+
77
+ unitType = "research-slice";
78
+ unitId = `${mid}/${sid}`;
79
+ prompt = await buildResearchSlicePrompt(mid, midTitle, sid, sTitle, base);
80
+ } else {
81
+ unitType = "research-milestone";
82
+ unitId = mid;
83
+ prompt = await buildResearchMilestonePrompt(mid, midTitle, base);
84
+ }
85
+ break;
86
+ }
87
+
88
+ case "plan":
89
+ case "plan-milestone":
90
+ case "plan-slice": {
91
+ const isSlice = normalized === "plan-slice" || (normalized === "plan" && state.phase !== "pre-planning");
92
+ if (isSlice) {
93
+ const sid = state.activeSlice?.id;
94
+ const sTitle = state.activeSlice?.title ?? "";
95
+ if (!sid) {
96
+ ctx.ui.notify("Cannot dispatch plan-slice: no active slice.", "warning");
97
+ return;
98
+ }
99
+ unitType = "plan-slice";
100
+ unitId = `${mid}/${sid}`;
101
+ prompt = await buildPlanSlicePrompt(mid, midTitle, sid, sTitle, base);
102
+ } else {
103
+ unitType = "plan-milestone";
104
+ unitId = mid;
105
+ prompt = await buildPlanMilestonePrompt(mid, midTitle, base);
106
+ }
107
+ break;
108
+ }
109
+
110
+ case "execute":
111
+ case "execute-task": {
112
+ const sid = state.activeSlice?.id;
113
+ const sTitle = state.activeSlice?.title ?? "";
114
+ const tid = state.activeTask?.id;
115
+ const tTitle = state.activeTask?.title ?? "";
116
+ if (!sid) {
117
+ ctx.ui.notify("Cannot dispatch execute-task: no active slice.", "warning");
118
+ return;
119
+ }
120
+ if (!tid) {
121
+ ctx.ui.notify("Cannot dispatch execute-task: no active task.", "warning");
122
+ return;
123
+ }
124
+ unitType = "execute-task";
125
+ unitId = `${mid}/${sid}/${tid}`;
126
+ prompt = await buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base);
127
+ break;
128
+ }
129
+
130
+ case "complete":
131
+ case "complete-slice":
132
+ case "complete-milestone": {
133
+ const isSlice = normalized === "complete-slice" || (normalized === "complete" && state.phase === "summarizing");
134
+ if (isSlice) {
135
+ const sid = state.activeSlice?.id;
136
+ const sTitle = state.activeSlice?.title ?? "";
137
+ if (!sid) {
138
+ ctx.ui.notify("Cannot dispatch complete-slice: no active slice.", "warning");
139
+ return;
140
+ }
141
+ unitType = "complete-slice";
142
+ unitId = `${mid}/${sid}`;
143
+ prompt = await buildCompleteSlicePrompt(mid, midTitle, sid, sTitle, base);
144
+ } else {
145
+ unitType = "complete-milestone";
146
+ unitId = mid;
147
+ prompt = await buildCompleteMilestonePrompt(mid, midTitle, base);
148
+ }
149
+ break;
150
+ }
151
+
152
+ case "reassess":
153
+ case "reassess-roadmap": {
154
+ const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP");
155
+ const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
156
+ if (!roadmapContent) {
157
+ ctx.ui.notify("Cannot dispatch reassess-roadmap: no roadmap found.", "warning");
158
+ return;
159
+ }
160
+ const roadmap = parseRoadmap(roadmapContent);
161
+ const completedSlices = roadmap.slices.filter(s => s.done);
162
+ if (completedSlices.length === 0) {
163
+ ctx.ui.notify("Cannot dispatch reassess-roadmap: no completed slices.", "warning");
164
+ return;
165
+ }
166
+ const completedSliceId = completedSlices[completedSlices.length - 1].id;
167
+ unitType = "reassess-roadmap";
168
+ unitId = `${mid}/${completedSliceId}`;
169
+ prompt = await buildReassessRoadmapPrompt(mid, midTitle, completedSliceId, base);
170
+ break;
171
+ }
172
+
173
+ case "uat":
174
+ case "run-uat": {
175
+ const sid = state.activeSlice?.id;
176
+ if (!sid) {
177
+ ctx.ui.notify("Cannot dispatch run-uat: no active slice.", "warning");
178
+ return;
179
+ }
180
+ const uatFile = resolveSliceFile(base, mid, sid, "UAT");
181
+ if (!uatFile) {
182
+ ctx.ui.notify("Cannot dispatch run-uat: no UAT file found.", "warning");
183
+ return;
184
+ }
185
+ const uatContent = await loadFile(uatFile);
186
+ if (!uatContent) {
187
+ ctx.ui.notify("Cannot dispatch run-uat: UAT file is empty.", "warning");
188
+ return;
189
+ }
190
+ const uatPath = relSliceFile(base, mid, sid, "UAT");
191
+ unitType = "run-uat";
192
+ unitId = `${mid}/${sid}`;
193
+ prompt = await buildRunUatPrompt(mid, sid, uatPath, uatContent, base);
194
+ break;
195
+ }
196
+
197
+ case "replan":
198
+ case "replan-slice": {
199
+ const sid = state.activeSlice?.id;
200
+ const sTitle = state.activeSlice?.title ?? "";
201
+ if (!sid) {
202
+ ctx.ui.notify("Cannot dispatch replan-slice: no active slice.", "warning");
203
+ return;
204
+ }
205
+ unitType = "replan-slice";
206
+ unitId = `${mid}/${sid}`;
207
+ prompt = await buildReplanSlicePrompt(mid, midTitle, sid, sTitle, base);
208
+ break;
209
+ }
210
+
211
+ default:
212
+ ctx.ui.notify(
213
+ `Unknown phase "${phase}". Valid phases: research, plan, execute, complete, reassess, uat, replan.`,
214
+ "warning",
215
+ );
216
+ return;
217
+ }
218
+
219
+ ctx.ui.notify(`Dispatching ${unitType} for ${unitId}...`, "info");
220
+ const result = await ctx.newSession();
221
+ if (result.cancelled) {
222
+ ctx.ui.notify("Session creation cancelled.", "warning");
223
+ return;
224
+ }
225
+ pi.sendMessage(
226
+ { customType: "gsd-dispatch", content: prompt, display: false },
227
+ { triggerTurn: true },
228
+ );
229
+ }
@@ -242,27 +242,40 @@ const DISPATCH_RULES: DispatchRule[] = [
242
242
  },
243
243
  },
244
244
  {
245
- name: "executing → execute-task",
246
- match: async ({ state, mid, basePath }) => {
245
+ name: "executing → execute-task (recover missing task plan → plan-slice)",
246
+ match: async ({ state, mid, midTitle, basePath }) => {
247
247
  if (state.phase !== "executing" || !state.activeTask) return null;
248
248
  const sid = state.activeSlice!.id;
249
249
  const sTitle = state.activeSlice!.title;
250
250
  const tid = state.activeTask.id;
251
- const tTitle = state.activeTask.title;
252
251
 
253
- // Guard: refuse to dispatch execute-task when the task plan file is missing.
254
- // This prevents the agent from running blind after a failed plan-slice that
255
- // wrote S{sid}-PLAN.md but omitted the individual T{tid}-PLAN.md files.
256
- // (See issue #739missing task plan caused runaway execution and EPIPE crash.)
252
+ // Guard: if the slice plan exists but the individual task plan files are
253
+ // missing, the planner created S##-PLAN.md with task entries but never
254
+ // wrote the tasks/ directory files. Dispatch plan-slice to regenerate
255
+ // them rather than hard-stopping fixes the infinite-loop described in
256
+ // issue #909.
257
257
  const taskPlanPath = resolveTaskFile(basePath, mid, sid, tid, "PLAN");
258
258
  if (!taskPlanPath || !existsSync(taskPlanPath)) {
259
259
  return {
260
- action: "stop",
261
- reason: `Task plan ${tid}-PLAN.md is missing for ${mid}/${sid}/${tid}. Re-run plan-slice to regenerate task plans, or create the file manually and resume.`,
262
- level: "error",
260
+ action: "dispatch",
261
+ unitType: "plan-slice",
262
+ unitId: `${mid}/${sid}`,
263
+ prompt: await buildPlanSlicePrompt(mid, midTitle, sid, sTitle, basePath),
263
264
  };
264
265
  }
265
266
 
267
+ return null;
268
+ },
269
+ },
270
+ {
271
+ name: "executing → execute-task",
272
+ match: async ({ state, mid, basePath }) => {
273
+ if (state.phase !== "executing" || !state.activeTask) return null;
274
+ const sid = state.activeSlice!.id;
275
+ const sTitle = state.activeSlice!.title;
276
+ const tid = state.activeTask.id;
277
+ const tTitle = state.activeTask.title;
278
+
266
279
  return {
267
280
  action: "dispatch",
268
281
  unitType: "execute-task",
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Model selection and dynamic routing for auto-mode unit dispatch.
3
+ * Handles complexity-based routing, model resolution across providers,
4
+ * and fallback chains.
5
+ */
6
+
7
+ import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent";
8
+ import type { GSDPreferences } from "./preferences.js";
9
+ import { resolveModelWithFallbacksForUnit, resolveDynamicRoutingConfig } from "./preferences.js";
10
+ import { classifyUnitComplexity, tierLabel } from "./complexity-classifier.js";
11
+ import { resolveModelForComplexity } from "./model-router.js";
12
+ import { getLedger, getProjectTotals } from "./metrics.js";
13
+ import { unitPhaseLabel } from "./auto-dashboard.js";
14
+
15
+ export interface ModelSelectionResult {
16
+ /** Routing metadata for metrics recording */
17
+ routing: { tier: string; modelDowngraded: boolean } | null;
18
+ }
19
+
20
+ /**
21
+ * Select and apply the appropriate model for a unit dispatch.
22
+ * Handles: per-unit-type model preferences, dynamic complexity routing,
23
+ * provider/model resolution, fallback chains, and start-model re-application.
24
+ *
25
+ * Returns routing metadata for metrics tracking.
26
+ */
27
+ export async function selectAndApplyModel(
28
+ ctx: ExtensionContext,
29
+ pi: ExtensionAPI,
30
+ unitType: string,
31
+ unitId: string,
32
+ basePath: string,
33
+ prefs: GSDPreferences | undefined,
34
+ verbose: boolean,
35
+ autoModeStartModel: { provider: string; id: string } | null,
36
+ ): Promise<ModelSelectionResult> {
37
+ const modelConfig = resolveModelWithFallbacksForUnit(unitType);
38
+ let routing: { tier: string; modelDowngraded: boolean } | null = null;
39
+
40
+ if (modelConfig) {
41
+ const availableModels = ctx.modelRegistry.getAvailable();
42
+
43
+ // ─── Dynamic Model Routing ─────────────────────────────────────────
44
+ const routingConfig = resolveDynamicRoutingConfig();
45
+ let effectiveModelConfig = modelConfig;
46
+ let routingTierLabel = "";
47
+
48
+ if (routingConfig.enabled) {
49
+ let budgetPct: number | undefined;
50
+ if (routingConfig.budget_pressure !== false) {
51
+ const budgetCeiling = prefs?.budget_ceiling;
52
+ if (budgetCeiling !== undefined && budgetCeiling > 0) {
53
+ const currentLedger = getLedger();
54
+ const totalCost = currentLedger ? getProjectTotals(currentLedger.units).cost : 0;
55
+ budgetPct = totalCost / budgetCeiling;
56
+ }
57
+ }
58
+
59
+ const isHook = unitType.startsWith("hook/");
60
+ const shouldClassify = !isHook || routingConfig.hooks !== false;
61
+
62
+ if (shouldClassify) {
63
+ const classification = classifyUnitComplexity(unitType, unitId, basePath, budgetPct);
64
+ const availableModelIds = availableModels.map(m => m.id);
65
+ const routingResult = resolveModelForComplexity(classification, modelConfig, routingConfig, availableModelIds);
66
+
67
+ if (routingResult.wasDowngraded) {
68
+ effectiveModelConfig = {
69
+ primary: routingResult.modelId,
70
+ fallbacks: routingResult.fallbacks,
71
+ };
72
+ if (verbose) {
73
+ ctx.ui.notify(
74
+ `Dynamic routing [${tierLabel(classification.tier)}]: ${routingResult.modelId} (${classification.reason})`,
75
+ "info",
76
+ );
77
+ }
78
+ }
79
+ routingTierLabel = ` [${tierLabel(classification.tier)}]`;
80
+ routing = { tier: classification.tier, modelDowngraded: routingResult.wasDowngraded };
81
+ }
82
+ }
83
+
84
+ const modelsToTry = [effectiveModelConfig.primary, ...effectiveModelConfig.fallbacks];
85
+
86
+ for (const modelId of modelsToTry) {
87
+ const model = resolveModelId(modelId, availableModels, ctx.model?.provider);
88
+
89
+ if (!model) {
90
+ if (verbose) ctx.ui.notify(`Model ${modelId} not found, trying fallback.`, "info");
91
+ continue;
92
+ }
93
+
94
+ // Warn if the ID is ambiguous across providers
95
+ if (!modelId.includes("/")) {
96
+ const providers = availableModels.filter(m => m.id === modelId).map(m => m.provider);
97
+ if (providers.length > 1 && model.provider !== ctx.model?.provider) {
98
+ ctx.ui.notify(
99
+ `Model ID "${modelId}" exists in multiple providers (${providers.join(", ")}). ` +
100
+ `Resolved to ${model.provider}. Use "provider/model" format for explicit targeting.`,
101
+ "warning",
102
+ );
103
+ }
104
+ }
105
+
106
+ const ok = await pi.setModel(model, { persist: false });
107
+ if (ok) {
108
+ const fallbackNote = modelId === effectiveModelConfig.primary
109
+ ? ""
110
+ : ` (fallback from ${effectiveModelConfig.primary})`;
111
+ const phase = unitPhaseLabel(unitType);
112
+ ctx.ui.notify(`Model [${phase}]${routingTierLabel}: ${model.provider}/${model.id}${fallbackNote}`, "info");
113
+ break;
114
+ } else {
115
+ const nextModel = modelsToTry[modelsToTry.indexOf(modelId) + 1];
116
+ if (nextModel) {
117
+ if (verbose) ctx.ui.notify(`Failed to set model ${modelId}, trying ${nextModel}...`, "info");
118
+ } else {
119
+ ctx.ui.notify(`All preferred models unavailable for ${unitType}. Using default.`, "warning");
120
+ }
121
+ }
122
+ }
123
+ } else if (autoModeStartModel) {
124
+ // No model preference for this unit type — re-apply the model captured
125
+ // at auto-mode start to prevent bleed from shared global settings.json (#650).
126
+ const availableModels = ctx.modelRegistry.getAvailable();
127
+ const startModel = availableModels.find(
128
+ m => m.provider === autoModeStartModel.provider && m.id === autoModeStartModel.id,
129
+ );
130
+ if (startModel) {
131
+ const ok = await pi.setModel(startModel, { persist: false });
132
+ if (!ok) {
133
+ const byId = availableModels.find(m => m.id === autoModeStartModel.id);
134
+ if (byId) await pi.setModel(byId, { persist: false });
135
+ }
136
+ }
137
+ }
138
+
139
+ return { routing };
140
+ }
141
+
142
+ /**
143
+ * Resolve a model ID string to a model object from the available models list.
144
+ * Handles formats: "provider/model", "bare-id", "org/model-name" (OpenRouter).
145
+ */
146
+ function resolveModelId<T extends { id: string; provider: string }>(
147
+ modelId: string,
148
+ availableModels: T[],
149
+ currentProvider: string | undefined,
150
+ ): T | undefined {
151
+ const slashIdx = modelId.indexOf("/");
152
+
153
+ if (slashIdx !== -1) {
154
+ const maybeProvider = modelId.substring(0, slashIdx);
155
+ const id = modelId.substring(slashIdx + 1);
156
+
157
+ const knownProviders = new Set(availableModels.map(m => m.provider.toLowerCase()));
158
+ if (knownProviders.has(maybeProvider.toLowerCase())) {
159
+ const match = availableModels.find(
160
+ m => m.provider.toLowerCase() === maybeProvider.toLowerCase()
161
+ && m.id.toLowerCase() === id.toLowerCase(),
162
+ );
163
+ if (match) return match;
164
+ }
165
+
166
+ // Try matching the full string as a model ID (OpenRouter-style)
167
+ const lower = modelId.toLowerCase();
168
+ return availableModels.find(
169
+ m => m.id.toLowerCase() === lower
170
+ || `${m.provider}/${m.id}`.toLowerCase() === lower,
171
+ );
172
+ }
173
+
174
+ // Bare ID — prefer current provider, then first available
175
+ const exactProviderMatch = availableModels.find(
176
+ m => m.id === modelId && m.provider === currentProvider,
177
+ );
178
+ return exactProviderMatch ?? availableModels.find(m => m.id === modelId);
179
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Pre-dispatch observability checks for auto-mode units.
3
+ * Validates plan/summary file quality and builds repair instructions
4
+ * for the agent to fix gaps before proceeding with the unit.
5
+ */
6
+
7
+ import type { ExtensionContext } from "@gsd/pi-coding-agent";
8
+ import {
9
+ validatePlanBoundary,
10
+ validateExecuteBoundary,
11
+ validateCompleteBoundary,
12
+ formatValidationIssues,
13
+ } from "./observability-validator.js";
14
+ import type { ValidationIssue } from "./observability-validator.js";
15
+
16
+ export async function collectObservabilityWarnings(
17
+ ctx: ExtensionContext,
18
+ basePath: string,
19
+ unitType: string,
20
+ unitId: string,
21
+ ): Promise<ValidationIssue[]> {
22
+ // Hook units have custom artifacts — skip standard observability checks
23
+ if (unitType.startsWith("hook/")) return [];
24
+
25
+ const parts = unitId.split("/");
26
+ const mid = parts[0];
27
+ const sid = parts[1];
28
+ const tid = parts[2];
29
+
30
+ if (!mid || !sid) return [];
31
+
32
+ let issues = [] as Awaited<ReturnType<typeof validatePlanBoundary>>;
33
+
34
+ if (unitType === "plan-slice") {
35
+ issues = await validatePlanBoundary(basePath, mid, sid);
36
+ } else if (unitType === "execute-task" && tid) {
37
+ issues = await validateExecuteBoundary(basePath, mid, sid, tid);
38
+ } else if (unitType === "complete-slice") {
39
+ issues = await validateCompleteBoundary(basePath, mid, sid);
40
+ }
41
+
42
+ if (issues.length > 0) {
43
+ ctx.ui.notify(
44
+ `Observability check (${unitType}) found ${issues.length} warning${issues.length === 1 ? "" : "s"}:\n${formatValidationIssues(issues)}`,
45
+ "warning",
46
+ );
47
+ }
48
+
49
+ return issues;
50
+ }
51
+
52
+ export function buildObservabilityRepairBlock(issues: ValidationIssue[]): string {
53
+ if (issues.length === 0) return "";
54
+ const items = issues.map(issue => {
55
+ const fileName = issue.file.split("/").pop() || issue.file;
56
+ let line = `- **${fileName}**: ${issue.message}`;
57
+ if (issue.suggestion) line += ` → ${issue.suggestion}`;
58
+ return line;
59
+ });
60
+ return [
61
+ "",
62
+ "---",
63
+ "",
64
+ "## Pre-flight: Observability gaps to fix FIRST",
65
+ "",
66
+ "The following issues were detected in plan/summary files for this unit.",
67
+ "**Read each flagged file, apply the fix described, then proceed with the unit.**",
68
+ "",
69
+ ...items,
70
+ "",
71
+ "---",
72
+ "",
73
+ ].join("\n");
74
+ }
@@ -642,7 +642,6 @@ export async function buildPlanSlicePrompt(
642
642
  const commitInstruction = commitDocsEnabled
643
643
  ? `Commit: \`docs(${sid}): add slice plan\``
644
644
  : "Do not commit — planning docs are not tracked in git for this project.";
645
-
646
645
  return loadPrompt("plan-slice", {
647
646
  workingDirectory: base,
648
647
  milestoneId: mid, sliceId: sid, sliceTitle: sTitle,