gsd-pi 2.66.1-dev.3c26b49 → 2.66.1-dev.3cea7ac
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/dist/resources/extensions/ask-user-questions.js +79 -11
- package/dist/resources/extensions/claude-code-cli/partial-builder.js +4 -3
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +10 -3
- package/dist/resources/extensions/gsd/auto/loop.js +13 -1
- package/dist/resources/extensions/gsd/auto/phases.js +10 -4
- package/dist/resources/extensions/gsd/auto/run-unit.js +10 -2
- package/dist/resources/extensions/gsd/auto/session.js +1 -1
- package/dist/resources/extensions/gsd/auto-dashboard.js +65 -15
- package/dist/resources/extensions/gsd/auto-dispatch.js +30 -28
- package/dist/resources/extensions/gsd/auto-prompts.js +6 -6
- package/dist/resources/extensions/gsd/auto-recovery.js +11 -12
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +18 -6
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +59 -5
- package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +8 -5
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +186 -14
- package/dist/resources/extensions/gsd/codebase-generator.js +4 -0
- package/dist/resources/extensions/gsd/commands/handlers/core.js +3 -3
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +10 -4
- package/dist/resources/extensions/gsd/custom-workflow-engine.js +3 -1
- package/dist/resources/extensions/gsd/detection.js +6 -0
- package/dist/resources/extensions/gsd/files.js +19 -2
- package/dist/resources/extensions/gsd/guided-flow.js +12 -8
- package/dist/resources/extensions/gsd/index.js +1 -1
- package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +2 -0
- package/dist/resources/extensions/gsd/parsers-legacy.js +3 -1
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/discuss-prepared.md +7 -7
- package/dist/resources/extensions/gsd/prompts/discuss.md +3 -3
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -3
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
- package/dist/resources/extensions/gsd/prompts/rethink.md +6 -2
- package/dist/resources/extensions/gsd/prompts/system.md +1 -1
- package/dist/resources/extensions/gsd/prompts/triage-captures.md +1 -1
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +4 -4
- package/dist/resources/extensions/gsd/prompts/worktree-merge.md +3 -1
- package/dist/resources/extensions/gsd/safety/file-change-validator.js +2 -1
- package/dist/resources/extensions/gsd/state.js +2 -1
- package/dist/resources/extensions/gsd/visualizer-overlay.js +27 -26
- package/dist/resources/extensions/gsd/workflow-reconcile.js +46 -7
- package/dist/resources/extensions/remote-questions/manager.js +8 -0
- package/dist/resources/extensions/shared/interview-ui.js +10 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +20 -20
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +20 -20
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.js +4 -3
- package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
- package/packages/pi-ai/dist/utils/json-parse.d.ts.map +1 -1
- package/packages/pi-ai/dist/utils/json-parse.js +11 -1
- package/packages/pi-ai/dist/utils/json-parse.js.map +1 -1
- package/packages/pi-ai/dist/utils/repair-tool-json.d.ts.map +1 -1
- package/packages/pi-ai/dist/utils/repair-tool-json.js +60 -1
- package/packages/pi-ai/dist/utils/repair-tool-json.js.map +1 -1
- package/packages/pi-ai/dist/utils/tests/json-parse.test.d.ts +2 -0
- package/packages/pi-ai/dist/utils/tests/json-parse.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/utils/tests/json-parse.test.js +14 -0
- package/packages/pi-ai/dist/utils/tests/json-parse.test.js.map +1 -0
- package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js +10 -0
- package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js.map +1 -1
- package/packages/pi-ai/src/providers/anthropic-shared.ts +4 -3
- package/packages/pi-ai/src/utils/json-parse.ts +11 -1
- package/packages/pi-ai/src/utils/repair-tool-json.ts +69 -1
- package/packages/pi-ai/src/utils/tests/json-parse.test.ts +17 -0
- package/packages/pi-ai/src/utils/tests/repair-tool-json.test.ts +13 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.js +17 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +2 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +9 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +2 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/scoped-models-selector.js +2 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +2 -2
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/provider-display-name.test.ts +18 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +2 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +11 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +2 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/scoped-models-selector.ts +2 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +2 -2
- package/packages/pi-tui/dist/__tests__/autocomplete.test.js +13 -0
- package/packages/pi-tui/dist/__tests__/autocomplete.test.js.map +1 -1
- package/packages/pi-tui/dist/__tests__/stdin-buffer.test.d.ts +2 -0
- package/packages/pi-tui/dist/__tests__/stdin-buffer.test.d.ts.map +1 -0
- package/packages/pi-tui/dist/__tests__/stdin-buffer.test.js +35 -0
- package/packages/pi-tui/dist/__tests__/stdin-buffer.test.js.map +1 -0
- package/packages/pi-tui/dist/__tests__/tui.test.d.ts +2 -0
- package/packages/pi-tui/dist/__tests__/tui.test.d.ts.map +1 -0
- package/packages/pi-tui/dist/__tests__/tui.test.js +43 -0
- package/packages/pi-tui/dist/__tests__/tui.test.js.map +1 -0
- package/packages/pi-tui/dist/autocomplete.d.ts.map +1 -1
- package/packages/pi-tui/dist/autocomplete.js +9 -7
- package/packages/pi-tui/dist/autocomplete.js.map +1 -1
- package/packages/pi-tui/dist/components/__tests__/editor.test.d.ts +2 -0
- package/packages/pi-tui/dist/components/__tests__/editor.test.d.ts.map +1 -0
- package/packages/pi-tui/dist/components/__tests__/editor.test.js +54 -0
- package/packages/pi-tui/dist/components/__tests__/editor.test.js.map +1 -0
- package/packages/pi-tui/dist/components/editor.d.ts +3 -1
- package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/editor.js +14 -3
- package/packages/pi-tui/dist/components/editor.js.map +1 -1
- package/packages/pi-tui/dist/stdin-buffer.d.ts.map +1 -1
- package/packages/pi-tui/dist/stdin-buffer.js +6 -0
- package/packages/pi-tui/dist/stdin-buffer.js.map +1 -1
- package/packages/pi-tui/dist/tui.d.ts.map +1 -1
- package/packages/pi-tui/dist/tui.js +8 -0
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/src/__tests__/autocomplete.test.ts +15 -0
- package/packages/pi-tui/src/__tests__/stdin-buffer.test.ts +43 -0
- package/packages/pi-tui/src/__tests__/tui.test.ts +50 -0
- package/packages/pi-tui/src/autocomplete.ts +9 -7
- package/packages/pi-tui/src/components/__tests__/editor.test.ts +64 -0
- package/packages/pi-tui/src/components/editor.ts +14 -3
- package/packages/pi-tui/src/stdin-buffer.ts +7 -0
- package/packages/pi-tui/src/tui.ts +9 -0
- package/src/resources/extensions/ask-user-questions.ts +103 -11
- package/src/resources/extensions/claude-code-cli/partial-builder.ts +4 -3
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +12 -3
- package/src/resources/extensions/claude-code-cli/tests/partial-builder.test.ts +17 -0
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +18 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -1
- package/src/resources/extensions/gsd/auto/loop.ts +14 -1
- package/src/resources/extensions/gsd/auto/phases.ts +10 -5
- package/src/resources/extensions/gsd/auto/run-unit.ts +14 -2
- package/src/resources/extensions/gsd/auto/session.ts +1 -1
- package/src/resources/extensions/gsd/auto-dashboard.ts +76 -16
- package/src/resources/extensions/gsd/auto-dispatch.ts +36 -35
- package/src/resources/extensions/gsd/auto-prompts.ts +5 -6
- package/src/resources/extensions/gsd/auto-recovery.ts +15 -15
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +27 -6
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +67 -6
- package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +11 -8
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +209 -16
- package/src/resources/extensions/gsd/codebase-generator.ts +4 -0
- package/src/resources/extensions/gsd/commands/handlers/core.ts +6 -6
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +11 -4
- package/src/resources/extensions/gsd/custom-workflow-engine.ts +3 -1
- package/src/resources/extensions/gsd/detection.ts +6 -0
- package/src/resources/extensions/gsd/files.ts +21 -2
- package/src/resources/extensions/gsd/guided-flow.ts +15 -8
- package/src/resources/extensions/gsd/index.ts +6 -0
- package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +2 -0
- package/src/resources/extensions/gsd/parsers-legacy.ts +3 -1
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/discuss-prepared.md +7 -7
- package/src/resources/extensions/gsd/prompts/discuss.md +3 -3
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -3
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
- package/src/resources/extensions/gsd/prompts/rethink.md +6 -2
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- package/src/resources/extensions/gsd/prompts/triage-captures.md +1 -1
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +4 -4
- package/src/resources/extensions/gsd/prompts/worktree-merge.md +3 -1
- package/src/resources/extensions/gsd/safety/file-change-validator.ts +4 -1
- package/src/resources/extensions/gsd/state.ts +2 -1
- package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +52 -1
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +50 -2
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/codebase-generator.test.ts +22 -0
- package/src/resources/extensions/gsd/tests/core-overlay-fallback.test.ts +44 -0
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +7 -1
- package/src/resources/extensions/gsd/tests/custom-workflow-engine.test.ts +31 -0
- package/src/resources/extensions/gsd/tests/detection.test.ts +37 -0
- package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +34 -0
- package/src/resources/extensions/gsd/tests/health-widget.test.ts +45 -0
- package/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts +53 -13
- package/src/resources/extensions/gsd/tests/integration/state-machine-runtime-failures.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +3 -4
- package/src/resources/extensions/gsd/tests/parallel-monitor-overlay.test.ts +21 -0
- package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +71 -2
- package/src/resources/extensions/gsd/tests/parsers.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +8 -1
- package/src/resources/extensions/gsd/tests/queue-execution-guard.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/register-shortcuts.test.ts +73 -0
- package/src/resources/extensions/gsd/tests/remote-questions.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/smart-entry-complete.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/workflow-reconcile.test.ts +91 -0
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +210 -35
- package/src/resources/extensions/gsd/visualizer-overlay.ts +31 -27
- package/src/resources/extensions/gsd/workflow-reconcile.ts +59 -8
- package/src/resources/extensions/remote-questions/manager.ts +9 -0
- package/src/resources/extensions/shared/interview-ui.ts +13 -0
- /package/dist/web/standalone/.next/static/{ZzNRjwBFLOhqEu4BYCQi9 → HxFcJ8GrYNPsg9ARz7GPz}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{ZzNRjwBFLOhqEu4BYCQi9 → HxFcJ8GrYNPsg9ARz7GPz}/_ssgManifest.js +0 -0
|
@@ -58,6 +58,51 @@ export function questionSignature(questions) {
|
|
|
58
58
|
export function resetAskUserQuestionsCache() {
|
|
59
59
|
turnCache.clear();
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Race a remote channel dispatch against the local TUI. The first to produce
|
|
63
|
+
* a valid (non-error, non-timeout) result wins. The loser is cancelled via
|
|
64
|
+
* the shared AbortController.
|
|
65
|
+
*
|
|
66
|
+
* If the local TUI responds first, the remote poll is aborted (the message
|
|
67
|
+
* stays in Discord/Slack but polling stops). If remote responds first, the
|
|
68
|
+
* local TUI prompt is cancelled.
|
|
69
|
+
*
|
|
70
|
+
* Returns null only when both sides fail or are cancelled.
|
|
71
|
+
*/
|
|
72
|
+
async function raceRemoteAndLocal(startRemote, startLocal, controller, questions) {
|
|
73
|
+
// Wrap local TUI result into the same shape as remote results
|
|
74
|
+
const localPromise = startLocal().then((result) => {
|
|
75
|
+
if (!result || Object.keys(result.answers).length === 0)
|
|
76
|
+
return null;
|
|
77
|
+
return {
|
|
78
|
+
content: [{ type: "text", text: formatForLLM(result) }],
|
|
79
|
+
details: { questions, response: result, cancelled: false },
|
|
80
|
+
};
|
|
81
|
+
}).catch(() => null);
|
|
82
|
+
const remotePromise = startRemote().then((result) => {
|
|
83
|
+
if (!result)
|
|
84
|
+
return null;
|
|
85
|
+
const details = result.details;
|
|
86
|
+
// Treat timeouts and errors as non-wins — let the local TUI win instead
|
|
87
|
+
if (details?.timed_out || details?.error)
|
|
88
|
+
return null;
|
|
89
|
+
return result;
|
|
90
|
+
}).catch(() => null);
|
|
91
|
+
// Race: first non-null result wins
|
|
92
|
+
const winner = await Promise.race([
|
|
93
|
+
localPromise.then((r) => r ? { source: "local", result: r } : null),
|
|
94
|
+
remotePromise.then((r) => r ? { source: "remote", result: r } : null),
|
|
95
|
+
]);
|
|
96
|
+
if (winner) {
|
|
97
|
+
// Cancel the loser
|
|
98
|
+
controller.abort();
|
|
99
|
+
return winner.result;
|
|
100
|
+
}
|
|
101
|
+
// First to resolve was null — wait for the other
|
|
102
|
+
const [localResult, remoteResult] = await Promise.all([localPromise, remotePromise]);
|
|
103
|
+
controller.abort();
|
|
104
|
+
return localResult ?? remoteResult;
|
|
105
|
+
}
|
|
61
106
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
62
107
|
const OTHER_OPTION_LABEL = "None of the above";
|
|
63
108
|
function errorResult(message, questions = []) {
|
|
@@ -116,19 +161,42 @@ export default function AskUserQuestions(pi) {
|
|
|
116
161
|
return errorResult(`Error: ask_user_questions requires non-empty options for every question (question "${q.id}" has none)`, params.questions);
|
|
117
162
|
}
|
|
118
163
|
}
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
164
|
+
// ── Routing: race remote + local, remote-only, or local-only ────────
|
|
165
|
+
const { tryRemoteQuestions, isRemoteConfigured } = await import("./remote-questions/manager.js");
|
|
166
|
+
const hasRemote = isRemoteConfigured();
|
|
167
|
+
// Case 1: Both remote and local UI available — race them.
|
|
168
|
+
// The first response wins; the loser is cancelled via AbortController.
|
|
169
|
+
if (hasRemote && ctx.hasUI) {
|
|
170
|
+
const raceController = new AbortController();
|
|
171
|
+
// Merge the parent signal so external cancellation propagates.
|
|
172
|
+
const onParentAbort = () => raceController.abort();
|
|
173
|
+
signal?.addEventListener("abort", onParentAbort, { once: true });
|
|
174
|
+
const raceSignal = raceController.signal;
|
|
175
|
+
const raceResult = await raceRemoteAndLocal(() => tryRemoteQuestions(params.questions, raceSignal), () => showInterviewRound(params.questions, { signal: raceSignal }, ctx), raceController, params.questions);
|
|
176
|
+
signal?.removeEventListener("abort", onParentAbort);
|
|
177
|
+
if (raceResult) {
|
|
178
|
+
const details = raceResult.details;
|
|
179
|
+
if (details && !details.timed_out && !details.error && !details.cancelled) {
|
|
180
|
+
turnCache.set(sig, raceResult);
|
|
181
|
+
}
|
|
182
|
+
return { ...raceResult, details: raceResult.details };
|
|
183
|
+
}
|
|
184
|
+
// Both sides failed/cancelled — fall through to error
|
|
185
|
+
return errorResult("ask_user_questions: no response received from local UI or remote channel", params.questions);
|
|
186
|
+
}
|
|
187
|
+
// Case 2: Remote configured but no local UI (headless) — remote only.
|
|
188
|
+
if (hasRemote && !ctx.hasUI) {
|
|
189
|
+
const remoteResult = await tryRemoteQuestions(params.questions, signal);
|
|
190
|
+
if (remoteResult) {
|
|
191
|
+
const remoteDetails = remoteResult.details;
|
|
192
|
+
if (remoteDetails && !remoteDetails.timed_out && !remoteDetails.error) {
|
|
193
|
+
turnCache.set(sig, remoteResult);
|
|
194
|
+
}
|
|
195
|
+
return { ...remoteResult, details: remoteResult.details };
|
|
129
196
|
}
|
|
130
|
-
return
|
|
197
|
+
return errorResult("Error: remote channel configured but returned no result", params.questions);
|
|
131
198
|
}
|
|
199
|
+
// Case 3: No remote — local UI only.
|
|
132
200
|
if (!ctx.hasUI) {
|
|
133
201
|
return errorResult("Error: UI not available (non-interactive mode)", params.questions);
|
|
134
202
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Translates the Claude Agent SDK's `BetaRawMessageStreamEvent` sequence
|
|
5
5
|
* into GSD's `AssistantMessageEvent` deltas for incremental TUI rendering.
|
|
6
6
|
*/
|
|
7
|
-
import { repairToolJson } from "@gsd/pi-ai";
|
|
7
|
+
import { hasXmlParameterTags, repairToolJson } from "@gsd/pi-ai";
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
9
|
// Content-block mapping helpers
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
@@ -207,14 +207,15 @@ export class PartialMessageBuilder {
|
|
|
207
207
|
}
|
|
208
208
|
if (block.type === "toolCall") {
|
|
209
209
|
const jsonStr = this.toolJsonAccum.get(streamIndex) ?? "{}";
|
|
210
|
+
const jsonForParse = hasXmlParameterTags(jsonStr) ? repairToolJson(jsonStr) : jsonStr;
|
|
210
211
|
try {
|
|
211
|
-
block.arguments = JSON.parse(
|
|
212
|
+
block.arguments = JSON.parse(jsonForParse);
|
|
212
213
|
}
|
|
213
214
|
catch {
|
|
214
215
|
// JSON.parse failed — attempt repair for YAML-style bullet
|
|
215
216
|
// lists that LLMs copy from template formatting (#2660).
|
|
216
217
|
try {
|
|
217
|
-
block.arguments = JSON.parse(repairToolJson(
|
|
218
|
+
block.arguments = JSON.parse(repairToolJson(jsonForParse));
|
|
218
219
|
}
|
|
219
220
|
catch {
|
|
220
221
|
// Repair also failed — stream was truncated or garbage.
|
|
@@ -29,6 +29,15 @@ function createAssistantStream() {
|
|
|
29
29
|
// Claude binary resolution
|
|
30
30
|
// ---------------------------------------------------------------------------
|
|
31
31
|
let cachedClaudePath = null;
|
|
32
|
+
export function getClaudeLookupCommand(platform = process.platform) {
|
|
33
|
+
return platform === "win32" ? "where claude" : "which claude";
|
|
34
|
+
}
|
|
35
|
+
export function parseClaudeLookupOutput(output) {
|
|
36
|
+
return output
|
|
37
|
+
.toString()
|
|
38
|
+
.trim()
|
|
39
|
+
.split(/\r?\n/)[0] ?? "";
|
|
40
|
+
}
|
|
32
41
|
/**
|
|
33
42
|
* Resolve the path to the system-installed `claude` binary.
|
|
34
43
|
* The SDK defaults to a bundled cli.js which doesn't exist when
|
|
@@ -38,9 +47,7 @@ function getClaudePath() {
|
|
|
38
47
|
if (cachedClaudePath)
|
|
39
48
|
return cachedClaudePath;
|
|
40
49
|
try {
|
|
41
|
-
cachedClaudePath = execSync(
|
|
42
|
-
.toString()
|
|
43
|
-
.trim();
|
|
50
|
+
cachedClaudePath = parseClaudeLookupOutput(execSync(getClaudeLookupCommand(), { timeout: 5_000, stdio: "pipe" }));
|
|
44
51
|
}
|
|
45
52
|
catch {
|
|
46
53
|
cachedClaudePath = "claude"; // fall back to PATH resolution
|
|
@@ -150,7 +150,7 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
150
150
|
}
|
|
151
151
|
// Verification passed — mark step complete
|
|
152
152
|
debugLog("autoLoop", { phase: "custom-engine-reconcile", iteration, unitId: iterData.unitId });
|
|
153
|
-
await engine.reconcile(engineState, {
|
|
153
|
+
const reconcileResult = await engine.reconcile(engineState, {
|
|
154
154
|
unitType: iterData.unitType,
|
|
155
155
|
unitId: iterData.unitId,
|
|
156
156
|
startedAt: s.currentUnit?.startedAt ?? Date.now(),
|
|
@@ -161,6 +161,18 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
161
161
|
recentErrorMessages.length = 0;
|
|
162
162
|
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId, seq: nextSeq(), eventType: "iteration-end", data: { iteration } });
|
|
163
163
|
debugLog("autoLoop", { phase: "iteration-complete", iteration });
|
|
164
|
+
if (reconcileResult.outcome === "milestone-complete") {
|
|
165
|
+
await deps.stopAuto(ctx, pi, "Workflow complete");
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
if (reconcileResult.outcome === "pause") {
|
|
169
|
+
await deps.pauseAuto(ctx, pi);
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
if (reconcileResult.outcome === "stop") {
|
|
173
|
+
await deps.stopAuto(ctx, pi, reconcileResult.reason ?? "Engine stopped");
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
164
176
|
continue;
|
|
165
177
|
}
|
|
166
178
|
if (!sidecarItem) {
|
|
@@ -338,7 +338,13 @@ export async function runPreDispatch(ic, loopState) {
|
|
|
338
338
|
ctx.ui.notify(`Milestone ${mid} has no title in roadmap — using ID as fallback.`, "warning");
|
|
339
339
|
}
|
|
340
340
|
// Mid-merge safety check
|
|
341
|
-
|
|
341
|
+
const mergeReconcileResult = deps.reconcileMergeState(s.basePath, ctx);
|
|
342
|
+
if (mergeReconcileResult === "blocked") {
|
|
343
|
+
await deps.pauseAuto(ctx, pi);
|
|
344
|
+
debugLog("autoLoop", { phase: "exit", reason: "merge-reconciliation-blocked" });
|
|
345
|
+
return { action: "break", reason: "merge-reconciliation-blocked" };
|
|
346
|
+
}
|
|
347
|
+
if (mergeReconcileResult === "reconciled") {
|
|
342
348
|
deps.invalidateAllCaches();
|
|
343
349
|
state = await deps.deriveState(s.basePath);
|
|
344
350
|
mid = state.activeMilestone?.id;
|
|
@@ -948,13 +954,13 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
|
|
|
948
954
|
return { action: "break", reason: "provider-pause" };
|
|
949
955
|
}
|
|
950
956
|
// Session creation timeout (not a structural error): pause auto-mode
|
|
951
|
-
// and let the provider-error-resume timer handle recovery. This
|
|
952
|
-
// the provider-pause path — break out cleanly, don't hard-stop.
|
|
957
|
+
// and let the provider-error-resume timer handle recovery (#3767). This
|
|
958
|
+
// matches the provider-pause path — break out cleanly, don't hard-stop.
|
|
953
959
|
// Structural errors (TypeError, is not a function) are NOT transient
|
|
954
960
|
// and must hard-stop to avoid infinite retry loops.
|
|
955
961
|
if (unitResult.errorContext?.isTransient &&
|
|
956
962
|
unitResult.errorContext?.category === "timeout") {
|
|
957
|
-
ctx.ui.notify(`Session creation timed out for ${unitType} ${unitId}.
|
|
963
|
+
ctx.ui.notify(`Session creation timed out for ${unitType} ${unitId}. Pausing auto-mode (recoverable).`, "warning");
|
|
958
964
|
debugLog("autoLoop", { phase: "session-timeout-pause", unitType, unitId });
|
|
959
965
|
await deps.pauseAuto(ctx, pi);
|
|
960
966
|
return { action: "break", reason: "session-timeout" };
|
|
@@ -7,6 +7,10 @@ import { NEW_SESSION_TIMEOUT_MS } from "./session.js";
|
|
|
7
7
|
import { _setCurrentResolve, _setSessionSwitchInFlight } from "./resolve.js";
|
|
8
8
|
import { debugLog } from "../debug-logger.js";
|
|
9
9
|
import { logWarning } from "../workflow-logger.js";
|
|
10
|
+
import { resolveAutoSupervisorConfig } from "../preferences.js";
|
|
11
|
+
// Tracks the latest session-switch attempt so a late timeout settlement from an
|
|
12
|
+
// older runUnit() call cannot clear the guard for a newer one.
|
|
13
|
+
let sessionSwitchGeneration = 0;
|
|
10
14
|
/**
|
|
11
15
|
* Execute a single unit: create a new session, send the prompt, and await
|
|
12
16
|
* the agent_end promise. Returns a UnitResult describing what happened.
|
|
@@ -21,10 +25,13 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
|
|
|
21
25
|
debugLog("runUnit", { phase: "session-create", unitType, unitId });
|
|
22
26
|
let sessionResult;
|
|
23
27
|
let sessionTimeoutHandle;
|
|
28
|
+
const mySessionSwitchGeneration = ++sessionSwitchGeneration;
|
|
24
29
|
_setSessionSwitchInFlight(true);
|
|
25
30
|
try {
|
|
26
31
|
const sessionPromise = s.cmdCtx.newSession().finally(() => {
|
|
27
|
-
|
|
32
|
+
if (sessionSwitchGeneration === mySessionSwitchGeneration) {
|
|
33
|
+
_setSessionSwitchInFlight(false);
|
|
34
|
+
}
|
|
28
35
|
});
|
|
29
36
|
const timeoutPromise = new Promise((resolve) => {
|
|
30
37
|
sessionTimeoutHandle = setTimeout(() => resolve({ cancelled: true }), NEW_SESSION_TIMEOUT_MS);
|
|
@@ -83,7 +90,8 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
|
|
|
83
90
|
// If supervision fails to resolve unitPromise within 30s, treat as cancelled.
|
|
84
91
|
// Without this, a crashed agent that never emits agent_end hangs the loop (#3161).
|
|
85
92
|
debugLog("runUnit", { phase: "awaiting-agent-end", unitType, unitId });
|
|
86
|
-
const
|
|
93
|
+
const supervisor = resolveAutoSupervisorConfig();
|
|
94
|
+
const UNIT_HARD_TIMEOUT_MS = Math.max(30_000, ((supervisor.hard_timeout_minutes ?? 30) * 60 * 1000) + 30_000);
|
|
87
95
|
let unitTimeoutHandle;
|
|
88
96
|
const timeoutResult = new Promise((resolve) => {
|
|
89
97
|
unitTimeoutHandle = setTimeout(() => {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
export const MAX_UNIT_DISPATCHES = 3;
|
|
20
20
|
export const STUB_RECOVERY_THRESHOLD = 2;
|
|
21
21
|
export const MAX_LIFETIME_DISPATCHES = 6;
|
|
22
|
-
export const NEW_SESSION_TIMEOUT_MS =
|
|
22
|
+
export const NEW_SESSION_TIMEOUT_MS = 120_000;
|
|
23
23
|
// ─── AutoSession ─────────────────────────────────────────────────────────────
|
|
24
24
|
export class AutoSession {
|
|
25
25
|
// ── Lifecycle ────────────────────────────────────────────────────────────
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { getCurrentBranch } from "./worktree.js";
|
|
9
9
|
import { getActiveHook } from "./post-unit-hooks.js";
|
|
10
10
|
import { getLedger, getProjectTotals } from "./metrics.js";
|
|
11
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
11
12
|
import { isDbAvailable, getMilestoneSlices, getSliceTasks } from "./gsd-db.js";
|
|
12
13
|
import { formatShortcut } from "./files.js";
|
|
13
14
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
@@ -17,7 +18,7 @@ import { makeUI } from "../shared/tui.js";
|
|
|
17
18
|
import { GLYPH, INDENT } from "../shared/mod.js";
|
|
18
19
|
import { computeProgressScore } from "./progress-score.js";
|
|
19
20
|
import { getActiveWorktreeName } from "./worktree-command.js";
|
|
20
|
-
import {
|
|
21
|
+
import { getGlobalGSDPreferencesPath, getProjectGSDPreferencesPath, parsePreferencesMarkdown, } from "./preferences.js";
|
|
21
22
|
import { resolveServiceTierIcon, getEffectiveServiceTier } from "./service-tier.js";
|
|
22
23
|
import { parseUnitId } from "./unit-id.js";
|
|
23
24
|
import { formatRtkSavingsLabel, getRtkSessionSavings, } from "../shared/rtk-session-stats.js";
|
|
@@ -293,26 +294,68 @@ export const hideFooter = () => ({
|
|
|
293
294
|
const WIDGET_MODES = ["full", "small", "min", "off"];
|
|
294
295
|
let widgetMode = "full";
|
|
295
296
|
let widgetModeInitialized = false;
|
|
297
|
+
let widgetModePreferencePath = null;
|
|
298
|
+
function safeReadTextFile(path) {
|
|
299
|
+
try {
|
|
300
|
+
if (!existsSync(path))
|
|
301
|
+
return null;
|
|
302
|
+
return readFileSync(path, "utf-8");
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function readWidgetModeFromFile(path) {
|
|
309
|
+
const raw = safeReadTextFile(path);
|
|
310
|
+
if (!raw)
|
|
311
|
+
return undefined;
|
|
312
|
+
const prefs = parsePreferencesMarkdown(raw);
|
|
313
|
+
const saved = prefs?.widget_mode;
|
|
314
|
+
if (saved && WIDGET_MODES.includes(saved)) {
|
|
315
|
+
return saved;
|
|
316
|
+
}
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
function resolveWidgetModePreferencePath(projectPath = getProjectGSDPreferencesPath(), globalPath = getGlobalGSDPreferencesPath()) {
|
|
320
|
+
if (readWidgetModeFromFile(projectPath)) {
|
|
321
|
+
return projectPath;
|
|
322
|
+
}
|
|
323
|
+
if (readWidgetModeFromFile(globalPath)) {
|
|
324
|
+
return globalPath;
|
|
325
|
+
}
|
|
326
|
+
if (safeReadTextFile(projectPath) !== null)
|
|
327
|
+
return projectPath;
|
|
328
|
+
if (safeReadTextFile(globalPath) !== null)
|
|
329
|
+
return globalPath;
|
|
330
|
+
return getGlobalGSDPreferencesPath();
|
|
331
|
+
}
|
|
296
332
|
/** Load widget mode from preferences (once). */
|
|
297
|
-
function ensureWidgetModeLoaded() {
|
|
333
|
+
function ensureWidgetModeLoaded(projectPath, globalPath) {
|
|
298
334
|
if (widgetModeInitialized)
|
|
299
335
|
return;
|
|
300
336
|
widgetModeInitialized = true;
|
|
301
337
|
try {
|
|
302
|
-
const
|
|
303
|
-
const
|
|
338
|
+
const resolvedProjectPath = projectPath ?? getProjectGSDPreferencesPath();
|
|
339
|
+
const resolvedGlobalPath = globalPath ?? getGlobalGSDPreferencesPath();
|
|
340
|
+
const saved = readWidgetModeFromFile(resolvedProjectPath) ?? readWidgetModeFromFile(resolvedGlobalPath);
|
|
304
341
|
if (saved && WIDGET_MODES.includes(saved)) {
|
|
305
342
|
widgetMode = saved;
|
|
306
343
|
}
|
|
344
|
+
widgetModePreferencePath = resolveWidgetModePreferencePath(resolvedProjectPath, resolvedGlobalPath);
|
|
307
345
|
}
|
|
308
346
|
catch (err) { /* non-fatal — use default */
|
|
309
|
-
logWarning("dashboard", `operation failed: ${
|
|
347
|
+
logWarning("dashboard", `operation failed: ${getErrorMessage(err)}`);
|
|
348
|
+
widgetModePreferencePath = getGlobalGSDPreferencesPath();
|
|
310
349
|
}
|
|
311
350
|
}
|
|
312
|
-
/**
|
|
313
|
-
|
|
351
|
+
/**
|
|
352
|
+
* Persist widget mode to the preference file that owns the effective value.
|
|
353
|
+
* Project-scoped widget_mode wins over global; if neither scope defines it,
|
|
354
|
+
* we prefer an existing project preferences file and otherwise fall back to
|
|
355
|
+
* the global preferences file.
|
|
356
|
+
*/
|
|
357
|
+
function persistWidgetMode(mode, prefsPath = widgetModePreferencePath ?? resolveWidgetModePreferencePath()) {
|
|
314
358
|
try {
|
|
315
|
-
const prefsPath = getGlobalGSDPreferencesPath();
|
|
316
359
|
let content = "";
|
|
317
360
|
if (existsSync(prefsPath)) {
|
|
318
361
|
content = readFileSync(prefsPath, "utf-8");
|
|
@@ -332,23 +375,30 @@ function persistWidgetMode(mode) {
|
|
|
332
375
|
}
|
|
333
376
|
}
|
|
334
377
|
/** Cycle to the next widget mode. Returns the new mode. */
|
|
335
|
-
export function cycleWidgetMode() {
|
|
336
|
-
ensureWidgetModeLoaded();
|
|
378
|
+
export function cycleWidgetMode(projectPath, globalPath) {
|
|
379
|
+
ensureWidgetModeLoaded(projectPath, globalPath);
|
|
337
380
|
const idx = WIDGET_MODES.indexOf(widgetMode);
|
|
338
381
|
widgetMode = WIDGET_MODES[(idx + 1) % WIDGET_MODES.length];
|
|
339
|
-
persistWidgetMode(widgetMode);
|
|
382
|
+
persistWidgetMode(widgetMode, widgetModePreferencePath ?? resolveWidgetModePreferencePath(projectPath, globalPath));
|
|
340
383
|
return widgetMode;
|
|
341
384
|
}
|
|
342
385
|
/** Set widget mode directly. */
|
|
343
|
-
export function setWidgetMode(mode) {
|
|
386
|
+
export function setWidgetMode(mode, projectPath, globalPath) {
|
|
387
|
+
ensureWidgetModeLoaded(projectPath, globalPath);
|
|
344
388
|
widgetMode = mode;
|
|
345
|
-
persistWidgetMode(widgetMode);
|
|
389
|
+
persistWidgetMode(widgetMode, widgetModePreferencePath ?? resolveWidgetModePreferencePath(projectPath, globalPath));
|
|
346
390
|
}
|
|
347
391
|
/** Get current widget mode. */
|
|
348
|
-
export function getWidgetMode() {
|
|
349
|
-
ensureWidgetModeLoaded();
|
|
392
|
+
export function getWidgetMode(projectPath, globalPath) {
|
|
393
|
+
ensureWidgetModeLoaded(projectPath, globalPath);
|
|
350
394
|
return widgetMode;
|
|
351
395
|
}
|
|
396
|
+
/** Test-only reset for widget mode caching. */
|
|
397
|
+
export function _resetWidgetModeForTests() {
|
|
398
|
+
widgetMode = "full";
|
|
399
|
+
widgetModeInitialized = false;
|
|
400
|
+
widgetModePreferencePath = null;
|
|
401
|
+
}
|
|
352
402
|
export function updateProgressWidget(ctx, unitType, unitId, state, accessors, tierBadge) {
|
|
353
403
|
if (!ctx.hasUI)
|
|
354
404
|
return;
|
|
@@ -291,34 +291,8 @@ export const DISPATCH_RULES = [
|
|
|
291
291
|
},
|
|
292
292
|
},
|
|
293
293
|
{
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
if (state.phase !== "planning")
|
|
297
|
-
return null;
|
|
298
|
-
// Phase skip: skip research when preference or profile says so
|
|
299
|
-
if (prefs?.phases?.skip_research || prefs?.phases?.skip_slice_research)
|
|
300
|
-
return null;
|
|
301
|
-
if (!state.activeSlice)
|
|
302
|
-
return missingSliceStop(mid, state.phase);
|
|
303
|
-
const sid = state.activeSlice.id;
|
|
304
|
-
const sTitle = state.activeSlice.title;
|
|
305
|
-
const researchFile = resolveSliceFile(basePath, mid, sid, "RESEARCH");
|
|
306
|
-
if (researchFile)
|
|
307
|
-
return null; // has research, fall through
|
|
308
|
-
// Skip slice research for S01 when milestone research already exists —
|
|
309
|
-
// the milestone research already covers the same ground for the first slice.
|
|
310
|
-
const milestoneResearchFile = resolveMilestoneFile(basePath, mid, "RESEARCH");
|
|
311
|
-
if (milestoneResearchFile && sid === "S01")
|
|
312
|
-
return null; // fall through to plan-slice
|
|
313
|
-
return {
|
|
314
|
-
action: "dispatch",
|
|
315
|
-
unitType: "research-slice",
|
|
316
|
-
unitId: `${mid}/${sid}`,
|
|
317
|
-
prompt: await buildResearchSlicePrompt(mid, midTitle, sid, sTitle, basePath),
|
|
318
|
-
};
|
|
319
|
-
},
|
|
320
|
-
},
|
|
321
|
-
{
|
|
294
|
+
// Keep this rule before the single-slice research rule so the multi-slice
|
|
295
|
+
// path wins whenever 2+ slices are ready.
|
|
322
296
|
name: "planning (multiple slices need research) → parallel-research-slices",
|
|
323
297
|
match: async ({ state, mid, midTitle, basePath, prefs }) => {
|
|
324
298
|
if (state.phase !== "planning")
|
|
@@ -360,6 +334,34 @@ export const DISPATCH_RULES = [
|
|
|
360
334
|
};
|
|
361
335
|
},
|
|
362
336
|
},
|
|
337
|
+
{
|
|
338
|
+
name: "planning (no research, not S01) → research-slice",
|
|
339
|
+
match: async ({ state, mid, midTitle, basePath, prefs }) => {
|
|
340
|
+
if (state.phase !== "planning")
|
|
341
|
+
return null;
|
|
342
|
+
// Phase skip: skip research when preference or profile says so
|
|
343
|
+
if (prefs?.phases?.skip_research || prefs?.phases?.skip_slice_research)
|
|
344
|
+
return null;
|
|
345
|
+
if (!state.activeSlice)
|
|
346
|
+
return missingSliceStop(mid, state.phase);
|
|
347
|
+
const sid = state.activeSlice.id;
|
|
348
|
+
const sTitle = state.activeSlice.title;
|
|
349
|
+
const researchFile = resolveSliceFile(basePath, mid, sid, "RESEARCH");
|
|
350
|
+
if (researchFile)
|
|
351
|
+
return null; // has research, fall through
|
|
352
|
+
// Skip slice research for S01 when milestone research already exists —
|
|
353
|
+
// the milestone research already covers the same ground for the first slice.
|
|
354
|
+
const milestoneResearchFile = resolveMilestoneFile(basePath, mid, "RESEARCH");
|
|
355
|
+
if (milestoneResearchFile && sid === "S01")
|
|
356
|
+
return null; // fall through to plan-slice
|
|
357
|
+
return {
|
|
358
|
+
action: "dispatch",
|
|
359
|
+
unitType: "research-slice",
|
|
360
|
+
unitId: `${mid}/${sid}`,
|
|
361
|
+
prompt: await buildResearchSlicePrompt(mid, midTitle, sid, sTitle, basePath),
|
|
362
|
+
};
|
|
363
|
+
},
|
|
364
|
+
},
|
|
363
365
|
{
|
|
364
366
|
name: "planning → plan-slice",
|
|
365
367
|
match: async ({ state, mid, midTitle, basePath }) => {
|
|
@@ -1410,7 +1410,7 @@ export async function buildValidateMilestonePrompt(mid, midTitle, base, level) {
|
|
|
1410
1410
|
catch (err) {
|
|
1411
1411
|
logWarning("prompt", `buildValidateMilestonePrompt verification classes lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1412
1412
|
}
|
|
1413
|
-
// Inline all slice summaries and
|
|
1413
|
+
// Inline all slice summaries and assessment results
|
|
1414
1414
|
let valSliceIds = [];
|
|
1415
1415
|
try {
|
|
1416
1416
|
const { isDbAvailable, getMilestoneSlices } = await import("./gsd-db.js");
|
|
@@ -1436,11 +1436,11 @@ export async function buildValidateMilestonePrompt(mid, midTitle, base, level) {
|
|
|
1436
1436
|
const summaryPath = resolveSliceFile(base, mid, sid, "SUMMARY");
|
|
1437
1437
|
const summaryRel = relSliceFile(base, mid, sid, "SUMMARY");
|
|
1438
1438
|
inlined.push(await inlineFile(summaryPath, summaryRel, `${sid} Summary`));
|
|
1439
|
-
const
|
|
1440
|
-
const
|
|
1441
|
-
const
|
|
1442
|
-
if (
|
|
1443
|
-
inlined.push(
|
|
1439
|
+
const assessmentPath = resolveSliceFile(base, mid, sid, "ASSESSMENT");
|
|
1440
|
+
const assessmentRel = relSliceFile(base, mid, sid, "ASSESSMENT");
|
|
1441
|
+
const assessmentInline = await inlineFileOptional(assessmentPath, assessmentRel, `${sid} Assessment`);
|
|
1442
|
+
if (assessmentInline)
|
|
1443
|
+
inlined.push(assessmentInline);
|
|
1444
1444
|
}
|
|
1445
1445
|
// Aggregate unresolved follow-ups and known limitations across slices
|
|
1446
1446
|
const outstandingItems = [];
|
|
@@ -10,7 +10,7 @@ import { parseUnitId } from "./unit-id.js";
|
|
|
10
10
|
import { appendEvent } from "./workflow-events.js";
|
|
11
11
|
import { clearParseCache } from "./files.js";
|
|
12
12
|
import { parseRoadmap as parseLegacyRoadmap, parsePlan as parseLegacyPlan } from "./parsers-legacy.js";
|
|
13
|
-
import { isDbAvailable, getTask, getSlice, getSliceTasks, updateTaskStatus, updateSliceStatus } from "./gsd-db.js";
|
|
13
|
+
import { isDbAvailable, getTask, getSlice, getSliceTasks, getPendingGates, updateTaskStatus, updateSliceStatus } from "./gsd-db.js";
|
|
14
14
|
import { isValidationTerminal } from "./state.js";
|
|
15
15
|
import { getErrorMessage } from "./error-utils.js";
|
|
16
16
|
import { logWarning, logError } from "./workflow-logger.js";
|
|
@@ -198,8 +198,7 @@ export function verifyExpectedArtifact(unitType, unitId, base) {
|
|
|
198
198
|
if (gateIds.length === 0)
|
|
199
199
|
return true;
|
|
200
200
|
try {
|
|
201
|
-
const
|
|
202
|
-
const pending = getPending(mid, sid, "slice");
|
|
201
|
+
const pending = getPendingGates(mid, sid, "slice");
|
|
203
202
|
const pendingIds = new Set(pending.map((g) => g.gate_id));
|
|
204
203
|
// All dispatched gates must no longer be pending
|
|
205
204
|
for (const gid of gateIds) {
|
|
@@ -454,9 +453,8 @@ function abortAndResetMerge(basePath, hasMergeHead, squashMsgPath) {
|
|
|
454
453
|
/**
|
|
455
454
|
* Detect leftover merge state from a prior session and reconcile it.
|
|
456
455
|
* If MERGE_HEAD or SQUASH_MSG exists, check whether conflicts are resolved.
|
|
457
|
-
* If resolved: finalize the commit. If
|
|
458
|
-
*
|
|
459
|
-
* Returns true if state was dirty and re-derivation is needed.
|
|
456
|
+
* If resolved: finalize the commit. If only .gsd conflicts remain: auto-resolve.
|
|
457
|
+
* If code conflicts remain: fail safe without modifying the worktree.
|
|
460
458
|
*/
|
|
461
459
|
export function reconcileMergeState(basePath, ctx) {
|
|
462
460
|
const mergeHeadPath = join(basePath, ".git", "MERGE_HEAD");
|
|
@@ -464,7 +462,7 @@ export function reconcileMergeState(basePath, ctx) {
|
|
|
464
462
|
const hasMergeHead = existsSync(mergeHeadPath);
|
|
465
463
|
const hasSquashMsg = existsSync(squashMsgPath);
|
|
466
464
|
if (!hasMergeHead && !hasSquashMsg)
|
|
467
|
-
return
|
|
465
|
+
return "clean";
|
|
468
466
|
const conflictedFiles = nativeConflictFiles(basePath);
|
|
469
467
|
if (conflictedFiles.length === 0) {
|
|
470
468
|
// All conflicts resolved — finalize the merge/squash commit
|
|
@@ -481,7 +479,7 @@ export function reconcileMergeState(basePath, ctx) {
|
|
|
481
479
|
catch (err) {
|
|
482
480
|
const errorMessage = getErrorMessage(err);
|
|
483
481
|
ctx.ui.notify(`Failed to finalize leftover merge/squash commit: ${errorMessage}`, "error");
|
|
484
|
-
return
|
|
482
|
+
return "blocked";
|
|
485
483
|
}
|
|
486
484
|
}
|
|
487
485
|
else {
|
|
@@ -515,12 +513,13 @@ export function reconcileMergeState(basePath, ctx) {
|
|
|
515
513
|
}
|
|
516
514
|
}
|
|
517
515
|
else {
|
|
518
|
-
// Code conflicts present —
|
|
519
|
-
|
|
520
|
-
ctx.ui.notify("Detected leftover merge state with unresolved conflicts
|
|
516
|
+
// Code conflicts present — fail safe and preserve any manual resolution
|
|
517
|
+
// work instead of discarding it with merge --abort/reset --hard.
|
|
518
|
+
ctx.ui.notify("Detected leftover merge state with unresolved code conflicts. Auto-mode will pause without modifying the worktree so manual conflict resolution is preserved.", "error");
|
|
519
|
+
return "blocked";
|
|
521
520
|
}
|
|
522
521
|
}
|
|
523
|
-
return
|
|
522
|
+
return "reconciled";
|
|
524
523
|
}
|
|
525
524
|
// ─── Loop Remediation ─────────────────────────────────────────────────────────
|
|
526
525
|
/**
|
|
@@ -5,6 +5,11 @@ import { loadEffectiveGSDPreferences } from "../preferences.js";
|
|
|
5
5
|
import { ensureDbOpen } from "./dynamic-tools.js";
|
|
6
6
|
import { StringEnum } from "@gsd/pi-ai";
|
|
7
7
|
import { logError } from "../workflow-logger.js";
|
|
8
|
+
import { shouldBlockContextArtifactSave } from "./write-gate.js";
|
|
9
|
+
const SUPPORTED_SUMMARY_ARTIFACT_TYPES = ["SUMMARY", "RESEARCH", "CONTEXT", "ASSESSMENT", "CONTEXT-DRAFT"];
|
|
10
|
+
export function isSupportedSummaryArtifactType(artifactType) {
|
|
11
|
+
return SUPPORTED_SUMMARY_ARTIFACT_TYPES.includes(artifactType);
|
|
12
|
+
}
|
|
8
13
|
/**
|
|
9
14
|
* Register an alias tool that shares the same execute function as its canonical counterpart.
|
|
10
15
|
* The alias description and promptGuidelines direct the LLM to prefer the canonical name.
|
|
@@ -274,13 +279,19 @@ export function registerDbTools(pi) {
|
|
|
274
279
|
details: { operation: "save_summary", error: "db_unavailable" },
|
|
275
280
|
};
|
|
276
281
|
}
|
|
277
|
-
|
|
278
|
-
if (!validTypes.includes(params.artifact_type)) {
|
|
282
|
+
if (!isSupportedSummaryArtifactType(params.artifact_type)) {
|
|
279
283
|
return {
|
|
280
|
-
content: [{ type: "text", text: `Error: Invalid artifact_type "${params.artifact_type}". Must be one of: ${
|
|
284
|
+
content: [{ type: "text", text: `Error: Invalid artifact_type "${params.artifact_type}". Must be one of: ${SUPPORTED_SUMMARY_ARTIFACT_TYPES.join(", ")}` }],
|
|
281
285
|
details: { operation: "save_summary", error: "invalid_artifact_type" },
|
|
282
286
|
};
|
|
283
287
|
}
|
|
288
|
+
const contextGuard = shouldBlockContextArtifactSave(params.artifact_type, params.milestone_id ?? null, params.slice_id ?? null);
|
|
289
|
+
if (contextGuard.block) {
|
|
290
|
+
return {
|
|
291
|
+
content: [{ type: "text", text: `Error saving artifact: ${contextGuard.reason ?? "context write blocked"}` }],
|
|
292
|
+
details: { operation: "save_summary", error: "context_write_blocked" },
|
|
293
|
+
};
|
|
294
|
+
}
|
|
284
295
|
try {
|
|
285
296
|
let relativePath;
|
|
286
297
|
if (params.task_id && params.slice_id) {
|
|
@@ -322,16 +333,17 @@ export function registerDbTools(pi) {
|
|
|
322
333
|
"Computes the file path from milestone/slice/task IDs automatically.",
|
|
323
334
|
promptSnippet: "Save a GSD artifact (summary/research/context/assessment) to DB and disk",
|
|
324
335
|
promptGuidelines: [
|
|
325
|
-
"Use gsd_summary_save to persist structured artifacts (SUMMARY, RESEARCH, CONTEXT, ASSESSMENT).",
|
|
336
|
+
"Use gsd_summary_save to persist structured artifacts (SUMMARY, RESEARCH, CONTEXT, ASSESSMENT, CONTEXT-DRAFT).",
|
|
326
337
|
"milestone_id is required. slice_id and task_id are optional — they determine the file path.",
|
|
327
338
|
"The tool computes the relative path automatically: milestones/M001/M001-SUMMARY.md, milestones/M001/slices/S01/S01-SUMMARY.md, etc.",
|
|
328
|
-
"artifact_type must be one of: SUMMARY, RESEARCH, CONTEXT, ASSESSMENT.",
|
|
339
|
+
"artifact_type must be one of: SUMMARY, RESEARCH, CONTEXT, ASSESSMENT, CONTEXT-DRAFT.",
|
|
340
|
+
"Use CONTEXT-DRAFT for incremental draft persistence; use CONTEXT for the final milestone context after depth verification.",
|
|
329
341
|
],
|
|
330
342
|
parameters: Type.Object({
|
|
331
343
|
milestone_id: Type.String({ description: "Milestone ID (e.g. M001)" }),
|
|
332
344
|
slice_id: Type.Optional(Type.String({ description: "Slice ID (e.g. S01)" })),
|
|
333
345
|
task_id: Type.Optional(Type.String({ description: "Task ID (e.g. T01)" })),
|
|
334
|
-
artifact_type: Type.String({ description: "One of: SUMMARY, RESEARCH, CONTEXT, ASSESSMENT" }),
|
|
346
|
+
artifact_type: Type.String({ description: "One of: SUMMARY, RESEARCH, CONTEXT, ASSESSMENT, CONTEXT-DRAFT" }),
|
|
335
347
|
content: Type.String({ description: "The full markdown content of the artifact" }),
|
|
336
348
|
}),
|
|
337
349
|
execute: summarySaveExecute,
|