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.
- package/README.md +43 -6
- package/dist/cli.js +4 -2
- package/dist/headless.d.ts +3 -0
- package/dist/headless.js +136 -8
- package/dist/help-text.js +3 -0
- package/dist/loader.js +33 -4
- package/dist/resources/extensions/bg-shell/index.ts +19 -2
- package/dist/resources/extensions/bg-shell/process-manager.ts +45 -0
- package/dist/resources/extensions/bg-shell/types.ts +21 -1
- package/dist/resources/extensions/gsd/auto/session.ts +224 -0
- package/dist/resources/extensions/gsd/auto-budget.ts +32 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +63 -10
- package/dist/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
- package/dist/resources/extensions/gsd/auto-dispatch.ts +23 -10
- package/dist/resources/extensions/gsd/auto-model-selection.ts +179 -0
- package/dist/resources/extensions/gsd/auto-observability.ts +74 -0
- package/dist/resources/extensions/gsd/auto-prompts.ts +0 -1
- package/dist/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
- package/dist/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
- package/dist/resources/extensions/gsd/auto-unit-closeout.ts +46 -0
- package/dist/resources/extensions/gsd/auto-worktree-sync.ts +207 -0
- package/dist/resources/extensions/gsd/auto.ts +977 -1551
- package/dist/resources/extensions/gsd/commands.ts +3 -3
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +47 -72
- package/dist/resources/extensions/gsd/doctor-proactive.ts +9 -4
- package/dist/resources/extensions/gsd/export-html.ts +1001 -0
- package/dist/resources/extensions/gsd/export.ts +49 -1
- package/dist/resources/extensions/gsd/git-service.ts +6 -0
- package/dist/resources/extensions/gsd/gitignore.ts +4 -1
- package/dist/resources/extensions/gsd/guided-flow.ts +24 -5
- package/dist/resources/extensions/gsd/index.ts +54 -1
- package/dist/resources/extensions/gsd/native-git-bridge.ts +30 -2
- package/dist/resources/extensions/gsd/observability-validator.ts +21 -0
- package/dist/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
- package/dist/resources/extensions/gsd/preferences.ts +62 -1
- package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -3
- package/dist/resources/extensions/gsd/prompts/system.md +1 -1
- package/dist/resources/extensions/gsd/reports.ts +510 -0
- package/dist/resources/extensions/gsd/roadmap-slices.ts +1 -1
- package/dist/resources/extensions/gsd/skills/gsd-headless/SKILL.md +178 -0
- package/dist/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +54 -0
- package/dist/resources/extensions/gsd/skills/gsd-headless/references/commands.md +59 -0
- package/dist/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +185 -0
- package/dist/resources/extensions/gsd/state.ts +30 -0
- package/dist/resources/extensions/gsd/templates/task-summary.md +9 -0
- package/dist/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
- package/dist/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
- package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
- package/dist/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
- package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
- package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
- package/dist/resources/extensions/gsd/tests/network-error-fallback.test.ts +51 -1
- package/dist/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
- package/dist/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
- package/dist/resources/extensions/gsd/tests/parallel-merge.test.ts +465 -0
- package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
- package/dist/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +71 -0
- package/dist/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
- package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +9 -9
- package/dist/resources/extensions/gsd/tests/verification-evidence.test.ts +743 -0
- package/dist/resources/extensions/gsd/tests/verification-gate.test.ts +965 -0
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
- package/dist/resources/extensions/gsd/tests/worktree.test.ts +3 -1
- package/dist/resources/extensions/gsd/types.ts +38 -0
- package/dist/resources/extensions/gsd/verification-evidence.ts +183 -0
- package/dist/resources/extensions/gsd/verification-gate.ts +567 -0
- package/dist/resources/extensions/gsd/visualizer-data.ts +25 -3
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +31 -21
- package/dist/resources/extensions/gsd/visualizer-views.ts +15 -66
- package/dist/resources/extensions/search-the-web/tool-search.ts +26 -0
- package/dist/resources/extensions/shared/format-utils.ts +85 -0
- package/dist/resources/extensions/shared/tests/format-utils.test.ts +153 -0
- package/dist/resources/extensions/subagent/index.ts +46 -1
- package/dist/resources/extensions/subagent/isolation.ts +9 -6
- package/package.json +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +7 -4
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/src/providers/openai-completions.ts +7 -4
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.js +7 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/config.js +9 -2
- package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -1
- package/packages/pi-coding-agent/src/core/lsp/client.ts +8 -0
- package/packages/pi-coding-agent/src/core/lsp/config.ts +9 -2
- package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/editor.js +1 -1
- package/packages/pi-tui/dist/components/editor.js.map +1 -1
- package/packages/pi-tui/src/components/editor.ts +3 -1
- package/scripts/link-workspace-packages.cjs +22 -6
- package/src/resources/extensions/bg-shell/index.ts +19 -2
- package/src/resources/extensions/bg-shell/process-manager.ts +45 -0
- package/src/resources/extensions/bg-shell/types.ts +21 -1
- package/src/resources/extensions/gsd/auto/session.ts +224 -0
- package/src/resources/extensions/gsd/auto-budget.ts +32 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +63 -10
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +23 -10
- package/src/resources/extensions/gsd/auto-model-selection.ts +179 -0
- package/src/resources/extensions/gsd/auto-observability.ts +74 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +0 -1
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
- package/src/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
- package/src/resources/extensions/gsd/auto-unit-closeout.ts +46 -0
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +207 -0
- package/src/resources/extensions/gsd/auto.ts +977 -1551
- package/src/resources/extensions/gsd/commands.ts +3 -3
- package/src/resources/extensions/gsd/dashboard-overlay.ts +47 -72
- package/src/resources/extensions/gsd/doctor-proactive.ts +9 -4
- package/src/resources/extensions/gsd/export-html.ts +1001 -0
- package/src/resources/extensions/gsd/export.ts +49 -1
- package/src/resources/extensions/gsd/git-service.ts +6 -0
- package/src/resources/extensions/gsd/gitignore.ts +4 -1
- package/src/resources/extensions/gsd/guided-flow.ts +24 -5
- package/src/resources/extensions/gsd/index.ts +54 -1
- package/src/resources/extensions/gsd/native-git-bridge.ts +30 -2
- package/src/resources/extensions/gsd/observability-validator.ts +21 -0
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
- package/src/resources/extensions/gsd/preferences.ts +62 -1
- package/src/resources/extensions/gsd/prompts/execute-task.md +4 -3
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- package/src/resources/extensions/gsd/reports.ts +510 -0
- package/src/resources/extensions/gsd/roadmap-slices.ts +1 -1
- package/src/resources/extensions/gsd/skills/gsd-headless/SKILL.md +178 -0
- package/src/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +54 -0
- package/src/resources/extensions/gsd/skills/gsd-headless/references/commands.md +59 -0
- package/src/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +185 -0
- package/src/resources/extensions/gsd/state.ts +30 -0
- package/src/resources/extensions/gsd/templates/task-summary.md +9 -0
- package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
- package/src/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
- package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
- package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
- package/src/resources/extensions/gsd/tests/network-error-fallback.test.ts +51 -1
- package/src/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
- package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
- package/src/resources/extensions/gsd/tests/parallel-merge.test.ts +465 -0
- package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +71 -0
- package/src/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +9 -9
- package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +743 -0
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +965 -0
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
- package/src/resources/extensions/gsd/tests/worktree.test.ts +3 -1
- package/src/resources/extensions/gsd/types.ts +38 -0
- package/src/resources/extensions/gsd/verification-evidence.ts +183 -0
- package/src/resources/extensions/gsd/verification-gate.ts +567 -0
- package/src/resources/extensions/gsd/visualizer-data.ts +25 -3
- package/src/resources/extensions/gsd/visualizer-overlay.ts +31 -21
- package/src/resources/extensions/gsd/visualizer-views.ts +15 -66
- package/src/resources/extensions/search-the-web/tool-search.ts +26 -0
- package/src/resources/extensions/shared/format-utils.ts +85 -0
- package/src/resources/extensions/shared/tests/format-utils.test.ts +153 -0
- package/src/resources/extensions/subagent/index.ts +46 -1
- 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:
|
|
254
|
-
//
|
|
255
|
-
// wrote
|
|
256
|
-
//
|
|
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: "
|
|
261
|
-
|
|
262
|
-
|
|
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,
|