gsd-pi 2.66.1 → 2.67.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/dist/claude-cli-check.d.ts +8 -0
- package/dist/claude-cli-check.js +36 -0
- package/dist/cli.js +40 -0
- package/dist/onboarding.js +19 -2
- 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/readiness.js +63 -12
- 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 +22 -3
- 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-model-selection.js +12 -3
- package/dist/resources/extensions/gsd/auto-prompts.js +173 -25
- package/dist/resources/extensions/gsd/auto-recovery.js +11 -12
- package/dist/resources/extensions/gsd/auto.js +13 -1
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +32 -1
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +18 -6
- package/dist/resources/extensions/gsd/bootstrap/provider-error-resume.js +5 -0
- 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/context-store.js +134 -2
- 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/preferences.js +6 -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 +12 -12
- 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/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 +12 -12
- 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/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/core/agent-session.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts +16 -0
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.js +58 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.test.js +58 -0
- package/packages/pi-coding-agent/dist/core/retry-handler.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +1 -0
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- 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/package.json +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +4 -0
- package/packages/pi-coding-agent/src/core/retry-handler.test.ts +69 -0
- package/packages/pi-coding-agent/src/core/retry-handler.ts +66 -1
- package/packages/pi-coding-agent/src/core/sdk.ts +5 -0
- 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/pkg/package.json +1 -1
- 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/readiness.ts +67 -12
- 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 +27 -4
- 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-model-selection.ts +12 -3
- package/src/resources/extensions/gsd/auto-prompts.ts +195 -25
- package/src/resources/extensions/gsd/auto-recovery.ts +15 -15
- package/src/resources/extensions/gsd/auto.ts +12 -1
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +34 -1
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +27 -6
- package/src/resources/extensions/gsd/bootstrap/provider-error-resume.ts +6 -0
- 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/context-store.ts +167 -2
- 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/preferences.ts +6 -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-model-selection.test.ts +21 -7
- 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/context-store.test.ts +176 -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/decision-scope-cascade.test.ts +370 -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 +3 -3
- package/src/resources/extensions/gsd/tests/measurement.test.ts +531 -0
- 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/preferences.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +8 -1
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +60 -0
- 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/{y5P0reMrCMs-4-gswdawm → DFZllMYDbO0OwyS6FSvm5}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{y5P0reMrCMs-4-gswdawm → DFZllMYDbO0OwyS6FSvm5}/_ssgManifest.js +0 -0
|
@@ -10,7 +10,16 @@ import { resumeAutoAfterProviderDelay } from "./provider-error-resume.js";
|
|
|
10
10
|
import { classifyError, createRetryState, resetRetryState, isTransient, } from "../error-classifier.js";
|
|
11
11
|
const retryState = createRetryState();
|
|
12
12
|
const MAX_NETWORK_RETRIES = 2;
|
|
13
|
-
const MAX_TRANSIENT_AUTO_RESUMES =
|
|
13
|
+
const MAX_TRANSIENT_AUTO_RESUMES = 8;
|
|
14
|
+
/**
|
|
15
|
+
* Reset the module-level retry state so a resumed auto-session starts fresh.
|
|
16
|
+
* Called by provider-error-resume.ts before startAuto() — without this, the
|
|
17
|
+
* consecutiveTransientCount accumulates across pause/resume cycles and locks
|
|
18
|
+
* out auto-resume after MAX_TRANSIENT_AUTO_RESUMES total (not consecutive) errors.
|
|
19
|
+
*/
|
|
20
|
+
export function resetTransientRetryState() {
|
|
21
|
+
resetRetryState(retryState);
|
|
22
|
+
}
|
|
14
23
|
async function pauseTransientWithBackoff(cls, pi, ctx, errorDetail, isRateLimit) {
|
|
15
24
|
retryState.consecutiveTransientCount += 1;
|
|
16
25
|
const baseRetryAfterMs = "retryAfterMs" in cls ? cls.retryAfterMs : 15_000;
|
|
@@ -97,6 +106,28 @@ export async function handleAgentEnd(pi, event, ctx) {
|
|
|
97
106
|
const explicitRetryAfterMs = ("retryAfterMs" in lastMsg && typeof lastMsg.retryAfterMs === "number") ? lastMsg.retryAfterMs : undefined;
|
|
98
107
|
// ── 1. Classify using rawErrorMsg to avoid prose false-positives ────
|
|
99
108
|
const cls = classifyError(rawErrorMsg, explicitRetryAfterMs);
|
|
109
|
+
// ── 1b. Defer to Core RetryHandler for transient errors ─────────────
|
|
110
|
+
// The Core RetryHandler (agent-session.ts) processes retryable errors
|
|
111
|
+
// AFTER this extension handler, in the same _processAgentEvent() call.
|
|
112
|
+
// For transient errors (overloaded, rate limit, server), the Core will
|
|
113
|
+
// retry in-context — same session, same conversation — which is strictly
|
|
114
|
+
// better than our Layer 2 pause+resume (which creates a new session).
|
|
115
|
+
//
|
|
116
|
+
// If we react here AND the Core also retries, we race: pauseAuto tears
|
|
117
|
+
// down the session while agent.continue() starts a new turn.
|
|
118
|
+
//
|
|
119
|
+
// Solution: Do nothing for transient errors. The Core RetryHandler
|
|
120
|
+
// runs next in _processAgentEvent and will either:
|
|
121
|
+
// a) Retry successfully → new agent_end (success) → we see it next time
|
|
122
|
+
// b) Exhaust retries → the agent stays idle, autoLoop's unit timeout
|
|
123
|
+
// or stuck detection handles it
|
|
124
|
+
//
|
|
125
|
+
// We do NOT call resolveAgentEnd here — that would unblock autoLoop
|
|
126
|
+
// prematurely while the Core is still retrying in the same session.
|
|
127
|
+
// We do NOT call pauseAuto — that would tear down the session.
|
|
128
|
+
if (isTransient(cls)) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
100
131
|
// Cap rate-limit backoff for CLI-style providers (openai-codex, google-gemini-cli)
|
|
101
132
|
// which use per-user quotas with shorter windows (#2922).
|
|
102
133
|
if (cls.kind === "rate-limit") {
|
|
@@ -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,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getAutoDashboardData, startAuto } from "../auto.js";
|
|
2
|
+
import { resetTransientRetryState } from "./agent-end-recovery.js";
|
|
2
3
|
const defaultDeps = {
|
|
3
4
|
getSnapshot: () => getAutoDashboardData(),
|
|
4
5
|
startAuto,
|
|
@@ -13,6 +14,10 @@ export async function resumeAutoAfterProviderDelay(pi, ctx, deps = defaultDeps)
|
|
|
13
14
|
ctx.ui.notify("Provider error recovery delay elapsed, but no paused auto-mode base path was available. Leaving auto-mode paused.", "warning");
|
|
14
15
|
return "missing-base";
|
|
15
16
|
}
|
|
17
|
+
// Reset the transient retry counter before restarting — without this,
|
|
18
|
+
// consecutiveTransientCount accumulates across pause/resume cycles and
|
|
19
|
+
// permanently locks out auto-resume after MAX_TRANSIENT_AUTO_RESUMES errors.
|
|
20
|
+
resetTransientRetryState();
|
|
16
21
|
await deps.startAuto(ctx, pi, snapshot.basePath, false, { step: snapshot.stepMode });
|
|
17
22
|
return "resumed";
|
|
18
23
|
}
|
|
@@ -3,7 +3,7 @@ import { isToolCallEventType } from "@gsd/pi-coding-agent";
|
|
|
3
3
|
import { buildMilestoneFileName, resolveMilestonePath, resolveSliceFile, resolveSlicePath } from "../paths.js";
|
|
4
4
|
import { buildBeforeAgentStartResult } from "./system-context.js";
|
|
5
5
|
import { handleAgentEnd } from "./agent-end-recovery.js";
|
|
6
|
-
import { clearDiscussionFlowState,
|
|
6
|
+
import { clearDiscussionFlowState, isDepthConfirmationAnswer, isQueuePhaseActive, markDepthVerified, resetWriteGateState, shouldBlockContextWrite, shouldBlockQueueExecution, isGateQuestionId, setPendingGate, clearPendingGate, getPendingGate, shouldBlockPendingGate, shouldBlockPendingGateBash, extractDepthVerificationMilestoneId } from "./write-gate.js";
|
|
7
7
|
import { isBlockedStateFile, isBashWriteToStateFile, BLOCKED_WRITE_ERROR } from "../write-intercept.js";
|
|
8
8
|
import { cleanupQuickBranch } from "../quick.js";
|
|
9
9
|
import { getDiscussionMilestoneId } from "../guided-flow.js";
|
|
@@ -21,6 +21,7 @@ import { logWarning as safetyLogWarning } from "../workflow-logger.js";
|
|
|
21
21
|
import { installNotifyInterceptor } from "./notify-interceptor.js";
|
|
22
22
|
import { initNotificationStore } from "../notification-store.js";
|
|
23
23
|
import { initNotificationWidget } from "../notification-widget.js";
|
|
24
|
+
import { initHealthWidget } from "../health-widget.js";
|
|
24
25
|
// Skip the welcome screen on the very first session_start — cli.ts already
|
|
25
26
|
// printed it before the TUI launched. Only re-print on /clear (subsequent sessions).
|
|
26
27
|
let isFirstSession = true;
|
|
@@ -33,6 +34,7 @@ export function registerHooks(pi) {
|
|
|
33
34
|
initNotificationStore(process.cwd());
|
|
34
35
|
installNotifyInterceptor(ctx);
|
|
35
36
|
initNotificationWidget(ctx);
|
|
37
|
+
initHealthWidget(ctx);
|
|
36
38
|
resetWriteGateState();
|
|
37
39
|
resetToolCallLoopGuard();
|
|
38
40
|
resetAskUserQuestionsCache();
|
|
@@ -154,11 +156,42 @@ export function registerHooks(pi) {
|
|
|
154
156
|
}
|
|
155
157
|
});
|
|
156
158
|
pi.on("tool_call", async (event) => {
|
|
159
|
+
const discussionBasePath = process.cwd();
|
|
157
160
|
// ── Loop guard: block repeated identical tool calls ──
|
|
158
161
|
const loopCheck = checkToolCallLoop(event.toolName, event.input);
|
|
159
162
|
if (loopCheck.block) {
|
|
160
163
|
return { block: true, reason: loopCheck.reason };
|
|
161
164
|
}
|
|
165
|
+
// ── Discussion gate enforcement: track pending gate questions ─────────
|
|
166
|
+
// Only gate-shaped ask_user_questions calls should block execution.
|
|
167
|
+
// The gate stays pending until the user selects the approval option.
|
|
168
|
+
if (event.toolName === "ask_user_questions") {
|
|
169
|
+
const milestoneId = getDiscussionMilestoneId(discussionBasePath);
|
|
170
|
+
const inDiscussion = milestoneId !== null || isQueuePhaseActive();
|
|
171
|
+
if (inDiscussion) {
|
|
172
|
+
const questions = event.input?.questions ?? [];
|
|
173
|
+
const questionId = questions.find((question) => typeof question?.id === "string" && isGateQuestionId(question.id))?.id;
|
|
174
|
+
if (typeof questionId === "string") {
|
|
175
|
+
setPendingGate(questionId);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// ── Discussion gate enforcement: block tool calls while gate is pending ──
|
|
180
|
+
// If ask_user_questions was called with a gate ID but hasn't been confirmed,
|
|
181
|
+
// block all non-read-only tool calls to prevent the model from skipping gates.
|
|
182
|
+
if (getPendingGate()) {
|
|
183
|
+
const milestoneId = getDiscussionMilestoneId(discussionBasePath);
|
|
184
|
+
if (isToolCallEventType("bash", event)) {
|
|
185
|
+
const bashGuard = shouldBlockPendingGateBash(event.input.command, milestoneId, isQueuePhaseActive());
|
|
186
|
+
if (bashGuard.block)
|
|
187
|
+
return bashGuard;
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
const gateGuard = shouldBlockPendingGate(event.toolName, milestoneId, isQueuePhaseActive());
|
|
191
|
+
if (gateGuard.block)
|
|
192
|
+
return gateGuard;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
162
195
|
// ── Queue-mode execution guard (#2545): block source-code mutations ──
|
|
163
196
|
// When /gsd queue is active, the agent should only create milestones,
|
|
164
197
|
// not execute work. Block write/edit to non-.gsd/ paths and bash commands
|
|
@@ -197,7 +230,7 @@ export function registerHooks(pi) {
|
|
|
197
230
|
}
|
|
198
231
|
if (!isToolCallEventType("write", event))
|
|
199
232
|
return;
|
|
200
|
-
const result = shouldBlockContextWrite(event.toolName, event.input.path, getDiscussionMilestoneId(),
|
|
233
|
+
const result = shouldBlockContextWrite(event.toolName, event.input.path, getDiscussionMilestoneId(discussionBasePath), isQueuePhaseActive());
|
|
201
234
|
if (result.block)
|
|
202
235
|
return result;
|
|
203
236
|
});
|
|
@@ -220,21 +253,42 @@ export function registerHooks(pi) {
|
|
|
220
253
|
pi.on("tool_result", async (event) => {
|
|
221
254
|
if (event.toolName !== "ask_user_questions")
|
|
222
255
|
return;
|
|
223
|
-
const milestoneId = getDiscussionMilestoneId();
|
|
256
|
+
const milestoneId = getDiscussionMilestoneId(process.cwd());
|
|
224
257
|
const queueActive = isQueuePhaseActive();
|
|
225
258
|
if (!milestoneId && !queueActive)
|
|
226
259
|
return;
|
|
227
260
|
const details = event.details;
|
|
261
|
+
// ── Discussion gate enforcement: handle gate question responses ──
|
|
262
|
+
// If the result is cancelled or has no response, the pending gate stays active
|
|
263
|
+
// so the model is blocked from non-read-only tools until it re-asks.
|
|
264
|
+
// If the user responded at all (even "needs adjustment"), clear the pending gate
|
|
265
|
+
// because the user engaged — the prompt handles the re-ask-after-adjustment flow.
|
|
266
|
+
const questions = event.input?.questions ?? [];
|
|
267
|
+
const currentPendingGate = getPendingGate();
|
|
268
|
+
if (currentPendingGate) {
|
|
269
|
+
if (details?.cancelled || !details?.response) {
|
|
270
|
+
// Gate stays pending — model will be blocked from non-read-only tools
|
|
271
|
+
// until it re-asks and gets a valid response
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
const pendingQuestion = questions.find((question) => question?.id === currentPendingGate);
|
|
275
|
+
if (pendingQuestion) {
|
|
276
|
+
const answer = details.response?.answers?.[currentPendingGate];
|
|
277
|
+
if (isDepthConfirmationAnswer(answer?.selected, pendingQuestion.options)) {
|
|
278
|
+
clearPendingGate();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
228
283
|
if (details?.cancelled || !details?.response)
|
|
229
284
|
return;
|
|
230
|
-
const questions = event.input?.questions ?? [];
|
|
231
285
|
for (const question of questions) {
|
|
232
286
|
if (typeof question.id === "string" && question.id.includes("depth_verification")) {
|
|
233
287
|
// Only unlock the gate if the user selected the first option (confirmation).
|
|
234
288
|
// Cross-references against the question's defined options to reject free-form "Other" text.
|
|
235
289
|
const answer = details.response?.answers?.[question.id];
|
|
236
290
|
if (isDepthConfirmationAnswer(answer?.selected, question.options)) {
|
|
237
|
-
markDepthVerified();
|
|
291
|
+
markDepthVerified(extractDepthVerificationMilestoneId(question.id) ?? milestoneId);
|
|
238
292
|
}
|
|
239
293
|
break;
|
|
240
294
|
}
|
|
@@ -4,16 +4,18 @@ import { Key } from "@gsd/pi-tui";
|
|
|
4
4
|
import { GSDDashboardOverlay } from "../dashboard-overlay.js";
|
|
5
5
|
import { GSDNotificationOverlay } from "../notification-overlay.js";
|
|
6
6
|
import { ParallelMonitorOverlay } from "../parallel-monitor-overlay.js";
|
|
7
|
+
import { projectRoot } from "../commands/context.js";
|
|
7
8
|
import { shortcutDesc } from "../../shared/mod.js";
|
|
8
9
|
export function registerShortcuts(pi) {
|
|
9
10
|
pi.registerShortcut(Key.ctrlAlt("g"), {
|
|
10
11
|
description: shortcutDesc("Open GSD dashboard", "/gsd status"),
|
|
11
12
|
handler: async (ctx) => {
|
|
12
|
-
|
|
13
|
+
const basePath = projectRoot();
|
|
14
|
+
if (!existsSync(join(basePath, ".gsd"))) {
|
|
13
15
|
ctx.ui.notify("No .gsd/ directory found. Run /gsd to start.", "info");
|
|
14
16
|
return;
|
|
15
17
|
}
|
|
16
|
-
await ctx.ui.custom((tui, theme, _kb, done) => new GSDDashboardOverlay(tui, theme, () => done()), {
|
|
18
|
+
await ctx.ui.custom((tui, theme, _kb, done) => new GSDDashboardOverlay(tui, theme, () => done(true)), {
|
|
17
19
|
overlay: true,
|
|
18
20
|
overlayOptions: {
|
|
19
21
|
width: "90%",
|
|
@@ -27,7 +29,7 @@ export function registerShortcuts(pi) {
|
|
|
27
29
|
pi.registerShortcut(Key.ctrlAlt("n"), {
|
|
28
30
|
description: shortcutDesc("Open notification history", "/gsd notifications"),
|
|
29
31
|
handler: async (ctx) => {
|
|
30
|
-
await ctx.ui.custom((tui, theme, _kb, done) => new GSDNotificationOverlay(tui, theme, () => done()), {
|
|
32
|
+
await ctx.ui.custom((tui, theme, _kb, done) => new GSDNotificationOverlay(tui, theme, () => done(true)), {
|
|
31
33
|
overlay: true,
|
|
32
34
|
overlayOptions: {
|
|
33
35
|
width: "80%",
|
|
@@ -42,12 +44,13 @@ export function registerShortcuts(pi) {
|
|
|
42
44
|
pi.registerShortcut(Key.ctrlAlt("p"), {
|
|
43
45
|
description: shortcutDesc("Open parallel worker monitor", "/gsd parallel watch"),
|
|
44
46
|
handler: async (ctx) => {
|
|
45
|
-
const
|
|
47
|
+
const basePath = projectRoot();
|
|
48
|
+
const parallelDir = join(basePath, ".gsd", "parallel");
|
|
46
49
|
if (!existsSync(parallelDir)) {
|
|
47
50
|
ctx.ui.notify("No parallel workers found. Run /gsd parallel start first.", "info");
|
|
48
51
|
return;
|
|
49
52
|
}
|
|
50
|
-
await ctx.ui.custom((tui, theme, _kb, done) => new ParallelMonitorOverlay(tui, theme, () => done()), {
|
|
53
|
+
await ctx.ui.custom((tui, theme, _kb, done) => new ParallelMonitorOverlay(tui, theme, () => done(true)), {
|
|
51
54
|
overlay: true,
|
|
52
55
|
overlayOptions: {
|
|
53
56
|
width: "90%",
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const MILESTONE_CONTEXT_RE = /M\d+(?:-[a-z0-9]{6})?-CONTEXT\.md$/;
|
|
2
|
+
const CONTEXT_MILESTONE_RE = /(?:^|[/\\])(M\d+(?:-[a-z0-9]{6})?)-CONTEXT\.md$/i;
|
|
3
|
+
const DEPTH_VERIFICATION_MILESTONE_RE = /depth_verification[_-](M\d+(?:-[a-z0-9]{6})?)/i;
|
|
2
4
|
/**
|
|
3
5
|
* Path segment that identifies .gsd/ planning artifacts.
|
|
4
6
|
* Writes to these paths are allowed during queue mode.
|
|
@@ -22,10 +24,49 @@ const QUEUE_SAFE_TOOLS = new Set([
|
|
|
22
24
|
* Matches the leading command in a bash invocation.
|
|
23
25
|
*/
|
|
24
26
|
const BASH_READ_ONLY_RE = /^\s*(cat|head|tail|less|more|wc|file|stat|du|df|which|type|echo|printf|ls|find|grep|rg|awk|sed\b(?!.*-i)|sort|uniq|diff|comm|tr|cut|tee\s+-a\s+\/dev\/null|git\s+(log|show|diff|status|branch|tag|remote|rev-parse|ls-files|blame|shortlog|describe|stash\s+list|config\s+--get|cat-file)|gh\s+(issue|pr|api|repo|release)\s+(view|list|diff|status|checks)|mkdir\s+-p\s+\.gsd|rtk\s)/;
|
|
25
|
-
|
|
27
|
+
const verifiedDepthMilestones = new Set();
|
|
26
28
|
let activeQueuePhase = false;
|
|
29
|
+
/**
|
|
30
|
+
* Discussion gate enforcement state.
|
|
31
|
+
*
|
|
32
|
+
* When ask_user_questions is called with a recognized gate question ID,
|
|
33
|
+
* we track the pending gate. Until the gate is confirmed (user selects the
|
|
34
|
+
* first/recommended option), all non-read-only tool calls are blocked.
|
|
35
|
+
* This mechanically prevents the model from rationalizing past failed or
|
|
36
|
+
* cancelled gate questions.
|
|
37
|
+
*/
|
|
38
|
+
let pendingGateId = null;
|
|
39
|
+
/**
|
|
40
|
+
* Recognized gate question ID patterns.
|
|
41
|
+
* These appear in both discuss-prepared.md (4-layer) and discuss.md (depth/requirements/roadmap).
|
|
42
|
+
*/
|
|
43
|
+
const GATE_QUESTION_PATTERNS = [
|
|
44
|
+
"layer1_scope_gate",
|
|
45
|
+
"layer2_architecture_gate",
|
|
46
|
+
"layer3_error_gate",
|
|
47
|
+
"layer4_quality_gate",
|
|
48
|
+
"depth_verification",
|
|
49
|
+
];
|
|
50
|
+
/**
|
|
51
|
+
* Tools that are safe to call while a gate is pending.
|
|
52
|
+
* Includes read-only tools and ask_user_questions itself (so the model can re-ask).
|
|
53
|
+
*/
|
|
54
|
+
const GATE_SAFE_TOOLS = new Set([
|
|
55
|
+
"ask_user_questions",
|
|
56
|
+
"read", "grep", "find", "ls", "glob",
|
|
57
|
+
"search-the-web", "resolve_library", "get_library_docs", "fetch_page",
|
|
58
|
+
"search_and_read",
|
|
59
|
+
]);
|
|
27
60
|
export function isDepthVerified() {
|
|
28
|
-
return
|
|
61
|
+
return verifiedDepthMilestones.size > 0;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Check whether a specific milestone has passed depth verification.
|
|
65
|
+
*/
|
|
66
|
+
export function isMilestoneDepthVerified(milestoneId) {
|
|
67
|
+
if (!milestoneId)
|
|
68
|
+
return false;
|
|
69
|
+
return verifiedDepthMilestones.has(milestoneId);
|
|
29
70
|
}
|
|
30
71
|
export function isQueuePhaseActive() {
|
|
31
72
|
return activeQueuePhase;
|
|
@@ -34,14 +75,103 @@ export function setQueuePhaseActive(active) {
|
|
|
34
75
|
activeQueuePhase = active;
|
|
35
76
|
}
|
|
36
77
|
export function resetWriteGateState() {
|
|
37
|
-
|
|
78
|
+
verifiedDepthMilestones.clear();
|
|
79
|
+
pendingGateId = null;
|
|
38
80
|
}
|
|
39
81
|
export function clearDiscussionFlowState() {
|
|
40
|
-
|
|
82
|
+
verifiedDepthMilestones.clear();
|
|
41
83
|
activeQueuePhase = false;
|
|
84
|
+
pendingGateId = null;
|
|
85
|
+
}
|
|
86
|
+
export function markDepthVerified(milestoneId) {
|
|
87
|
+
if (!milestoneId)
|
|
88
|
+
return;
|
|
89
|
+
verifiedDepthMilestones.add(milestoneId);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Check whether a question ID matches a recognized gate pattern.
|
|
93
|
+
*/
|
|
94
|
+
export function isGateQuestionId(questionId) {
|
|
95
|
+
return GATE_QUESTION_PATTERNS.some(pattern => questionId.includes(pattern));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Extract the milestone ID embedded in a depth-verification question id.
|
|
99
|
+
* Prompts are expected to use ids like `depth_verification_M001_confirm`.
|
|
100
|
+
*/
|
|
101
|
+
export function extractDepthVerificationMilestoneId(questionId) {
|
|
102
|
+
const match = questionId.match(DEPTH_VERIFICATION_MILESTONE_RE);
|
|
103
|
+
return match?.[1] ?? null;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Extract the milestone ID from a milestone CONTEXT file path.
|
|
107
|
+
*/
|
|
108
|
+
function extractContextMilestoneId(inputPath) {
|
|
109
|
+
const match = inputPath.match(CONTEXT_MILESTONE_RE);
|
|
110
|
+
return match?.[1] ?? null;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Mark a gate as pending (called when ask_user_questions is invoked with a gate ID).
|
|
114
|
+
*/
|
|
115
|
+
export function setPendingGate(gateId) {
|
|
116
|
+
pendingGateId = gateId;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Clear the pending gate (called when the user confirms).
|
|
120
|
+
*/
|
|
121
|
+
export function clearPendingGate() {
|
|
122
|
+
pendingGateId = null;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get the currently pending gate, if any.
|
|
126
|
+
*/
|
|
127
|
+
export function getPendingGate() {
|
|
128
|
+
return pendingGateId;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Check whether a tool call should be blocked because a discussion gate
|
|
132
|
+
* is pending (ask_user_questions was called but not confirmed).
|
|
133
|
+
*
|
|
134
|
+
* Returns { block: true, reason } if the tool should be blocked.
|
|
135
|
+
* Read-only tools and ask_user_questions itself are always allowed.
|
|
136
|
+
*/
|
|
137
|
+
export function shouldBlockPendingGate(toolName, _milestoneId, _queuePhaseActive) {
|
|
138
|
+
if (!pendingGateId)
|
|
139
|
+
return { block: false };
|
|
140
|
+
if (GATE_SAFE_TOOLS.has(toolName))
|
|
141
|
+
return { block: false };
|
|
142
|
+
// Bash read-only commands are also safe
|
|
143
|
+
if (toolName === "bash")
|
|
144
|
+
return { block: false }; // bash is checked separately below
|
|
145
|
+
return {
|
|
146
|
+
block: true,
|
|
147
|
+
reason: [
|
|
148
|
+
`HARD BLOCK: Discussion gate "${pendingGateId}" has not been confirmed by the user.`,
|
|
149
|
+
`You MUST re-call ask_user_questions with the gate question before making any other tool calls.`,
|
|
150
|
+
`If the previous ask_user_questions call failed, errored, was cancelled, or the user's response`,
|
|
151
|
+
`did not match a provided option, you MUST re-ask — never rationalize past the block.`,
|
|
152
|
+
`Do NOT proceed, do NOT use alternative approaches, do NOT skip the gate.`,
|
|
153
|
+
].join(" "),
|
|
154
|
+
};
|
|
42
155
|
}
|
|
43
|
-
|
|
44
|
-
|
|
156
|
+
/**
|
|
157
|
+
* Check whether a bash command should be blocked because a discussion gate is pending.
|
|
158
|
+
* Read-only bash commands are allowed; mutating commands are blocked.
|
|
159
|
+
*/
|
|
160
|
+
export function shouldBlockPendingGateBash(command, _milestoneId, _queuePhaseActive) {
|
|
161
|
+
if (!pendingGateId)
|
|
162
|
+
return { block: false };
|
|
163
|
+
// Allow read-only bash commands
|
|
164
|
+
if (BASH_READ_ONLY_RE.test(command))
|
|
165
|
+
return { block: false };
|
|
166
|
+
return {
|
|
167
|
+
block: true,
|
|
168
|
+
reason: [
|
|
169
|
+
`HARD BLOCK: Discussion gate "${pendingGateId}" has not been confirmed by the user.`,
|
|
170
|
+
`You MUST re-call ask_user_questions with the gate question before running mutating commands.`,
|
|
171
|
+
`If the previous ask_user_questions call failed, errored, was cancelled, or the user's response`,
|
|
172
|
+
`did not match a provided option, you MUST re-ask — never rationalize past the block.`,
|
|
173
|
+
].join(" "),
|
|
174
|
+
};
|
|
45
175
|
}
|
|
46
176
|
/**
|
|
47
177
|
* Check whether a depth_verification answer confirms the discussion is complete.
|
|
@@ -67,16 +197,23 @@ export function isDepthConfirmationAnswer(selected, options) {
|
|
|
67
197
|
// accept only if it contains "(Recommended)" — the prompt convention suffix.
|
|
68
198
|
return value.includes("(Recommended)");
|
|
69
199
|
}
|
|
70
|
-
export function shouldBlockContextWrite(toolName, inputPath, milestoneId,
|
|
200
|
+
export function shouldBlockContextWrite(toolName, inputPath, milestoneId, _queuePhaseActive) {
|
|
71
201
|
if (toolName !== "write")
|
|
72
202
|
return { block: false };
|
|
73
|
-
const inDiscussion = milestoneId !== null;
|
|
74
|
-
const inQueue = queuePhaseActive ?? false;
|
|
75
|
-
if (!inDiscussion && !inQueue)
|
|
76
|
-
return { block: false };
|
|
77
203
|
if (!MILESTONE_CONTEXT_RE.test(inputPath))
|
|
78
204
|
return { block: false };
|
|
79
|
-
|
|
205
|
+
const targetMilestoneId = extractContextMilestoneId(inputPath) ?? milestoneId;
|
|
206
|
+
if (!targetMilestoneId) {
|
|
207
|
+
return {
|
|
208
|
+
block: true,
|
|
209
|
+
reason: [
|
|
210
|
+
`HARD BLOCK: Cannot write milestone CONTEXT.md without knowing which milestone it belongs to.`,
|
|
211
|
+
`This is a mechanical gate — you MUST NOT proceed, retry, or rationalize past this block.`,
|
|
212
|
+
`Required action: call ask_user_questions with question id containing "depth_verification" and the milestone id.`,
|
|
213
|
+
].join(" "),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
if (isMilestoneDepthVerified(targetMilestoneId))
|
|
80
217
|
return { block: false };
|
|
81
218
|
return {
|
|
82
219
|
block: true,
|
|
@@ -89,6 +226,37 @@ export function shouldBlockContextWrite(toolName, inputPath, milestoneId, depthV
|
|
|
89
226
|
].join(" "),
|
|
90
227
|
};
|
|
91
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Check whether a gsd_summary_save CONTEXT artifact should be blocked.
|
|
231
|
+
* Slice-level CONTEXT artifacts are allowed; milestone-level CONTEXT writes
|
|
232
|
+
* require the milestone to be depth-verified first.
|
|
233
|
+
*/
|
|
234
|
+
export function shouldBlockContextArtifactSave(artifactType, milestoneId, sliceId) {
|
|
235
|
+
if (artifactType !== "CONTEXT")
|
|
236
|
+
return { block: false };
|
|
237
|
+
if (sliceId)
|
|
238
|
+
return { block: false };
|
|
239
|
+
if (!milestoneId) {
|
|
240
|
+
return {
|
|
241
|
+
block: true,
|
|
242
|
+
reason: [
|
|
243
|
+
`HARD BLOCK: Cannot save milestone CONTEXT without a milestone_id.`,
|
|
244
|
+
`This is a mechanical gate — you MUST NOT proceed, retry, or rationalize past this block.`,
|
|
245
|
+
].join(" "),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
if (isMilestoneDepthVerified(milestoneId))
|
|
249
|
+
return { block: false };
|
|
250
|
+
return {
|
|
251
|
+
block: true,
|
|
252
|
+
reason: [
|
|
253
|
+
`HARD BLOCK: Cannot save milestone CONTEXT without depth verification for ${milestoneId}.`,
|
|
254
|
+
`This is a mechanical gate — you MUST NOT proceed, retry, or rationalize past this block.`,
|
|
255
|
+
`Required action: call ask_user_questions with question id containing "depth_verification_${milestoneId}".`,
|
|
256
|
+
`The user MUST select the "(Recommended)" confirmation option to unlock this gate.`,
|
|
257
|
+
].join(" "),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
92
260
|
/**
|
|
93
261
|
* Queue-mode execution guard (#2545).
|
|
94
262
|
*
|
|
@@ -130,6 +298,10 @@ export function shouldBlockQueueExecution(toolName, input, queuePhaseActive) {
|
|
|
130
298
|
`Use read-only commands (cat, grep, git log, etc.) to investigate, then write planning artifacts.`,
|
|
131
299
|
};
|
|
132
300
|
}
|
|
133
|
-
// Unknown tools —
|
|
134
|
-
|
|
301
|
+
// Unknown tools — block by default in queue mode so custom tools cannot
|
|
302
|
+
// bypass execution restrictions.
|
|
303
|
+
return {
|
|
304
|
+
block: true,
|
|
305
|
+
reason: `Blocked: /gsd queue is a planning tool — it creates milestones, not executes work. Unknown tools are not permitted during queue mode.`,
|
|
306
|
+
};
|
|
135
307
|
}
|
|
@@ -77,7 +77,7 @@ export async function handleStatus(ctx) {
|
|
|
77
77
|
return;
|
|
78
78
|
}
|
|
79
79
|
const { GSDDashboardOverlay } = await import("../../dashboard-overlay.js");
|
|
80
|
-
const result = await ctx.ui.custom((tui, theme, _kb, done) => new GSDDashboardOverlay(tui, theme, () => done()), {
|
|
80
|
+
const result = await ctx.ui.custom((tui, theme, _kb, done) => new GSDDashboardOverlay(tui, theme, () => done(true)), {
|
|
81
81
|
overlay: true,
|
|
82
82
|
overlayOptions: {
|
|
83
83
|
width: "70%",
|
|
@@ -99,7 +99,7 @@ export async function handleVisualize(ctx) {
|
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
101
101
|
const { GSDVisualizerOverlay } = await import("../../visualizer-overlay.js");
|
|
102
|
-
const result = await ctx.ui.custom((tui, theme, _kb, done) => new GSDVisualizerOverlay(tui, theme, () => done()), {
|
|
102
|
+
const result = await ctx.ui.custom((tui, theme, _kb, done) => new GSDVisualizerOverlay(tui, theme, () => done(true)), {
|
|
103
103
|
overlay: true,
|
|
104
104
|
overlayOptions: {
|
|
105
105
|
width: "80%",
|
|
@@ -195,7 +195,7 @@ export async function handleCoreCommand(trimmed, ctx) {
|
|
|
195
195
|
}
|
|
196
196
|
if (trimmed === "show-config") {
|
|
197
197
|
const { GSDConfigOverlay, formatConfigText } = await import("../../config-overlay.js");
|
|
198
|
-
const result = await ctx.ui.custom((tui, theme, _kb, done) => new GSDConfigOverlay(tui, theme, () => done()), {
|
|
198
|
+
const result = await ctx.ui.custom((tui, theme, _kb, done) => new GSDConfigOverlay(tui, theme, () => done(true)), {
|
|
199
199
|
overlay: true,
|
|
200
200
|
overlayOptions: {
|
|
201
201
|
width: "65%",
|
|
@@ -240,10 +240,16 @@ async function configureModels(ctx, prefs) {
|
|
|
240
240
|
for (const group of byProvider.values()) {
|
|
241
241
|
group.sort((a, b) => a.id.localeCompare(b.id));
|
|
242
242
|
}
|
|
243
|
-
//
|
|
243
|
+
// Display names for providers in the preferences wizard UI.
|
|
244
|
+
const PROVIDER_DISPLAY_NAMES = { anthropic: "anthropic-api" };
|
|
245
|
+
const displayName = (p) => PROVIDER_DISPLAY_NAMES[p] ?? p;
|
|
246
|
+
// Build provider menu with model counts (display name → real name lookup)
|
|
247
|
+
const displayToReal = new Map();
|
|
244
248
|
const providerOptions = providers.map(p => {
|
|
245
249
|
const count = byProvider.get(p).length;
|
|
246
|
-
|
|
250
|
+
const label = `${displayName(p)} (${count} models)`;
|
|
251
|
+
displayToReal.set(label, p);
|
|
252
|
+
return label;
|
|
247
253
|
});
|
|
248
254
|
providerOptions.push("(keep current)", "(clear)", "(type manually)");
|
|
249
255
|
for (const phase of modelPhases) {
|
|
@@ -267,13 +273,13 @@ async function configureModels(ctx, prefs) {
|
|
|
267
273
|
continue;
|
|
268
274
|
}
|
|
269
275
|
// Step 2: pick model within provider
|
|
270
|
-
const providerName = providerChoice.replace(/ \(\d+ models?\)$/, "");
|
|
276
|
+
const providerName = displayToReal.get(providerChoice) ?? providerChoice.replace(/ \(\d+ models?\)$/, "");
|
|
271
277
|
const group = byProvider.get(providerName);
|
|
272
278
|
if (!group)
|
|
273
279
|
continue;
|
|
274
280
|
const modelOptions = group.map(m => m.id);
|
|
275
281
|
modelOptions.push("(keep current)", "(clear)");
|
|
276
|
-
const modelChoice = await ctx.ui.select(`${phaseLabel} — ${providerName}:`, modelOptions);
|
|
282
|
+
const modelChoice = await ctx.ui.select(`${phaseLabel} — ${displayName(providerName)}:`, modelOptions);
|
|
277
283
|
if (modelChoice && typeof modelChoice === "string" && modelChoice !== "(keep current)") {
|
|
278
284
|
if (modelChoice === "(clear)") {
|
|
279
285
|
delete models[phase];
|