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
|
@@ -11,6 +11,7 @@ import type { GSDState } from "./types.js";
|
|
|
11
11
|
import { getCurrentBranch } from "./worktree.js";
|
|
12
12
|
import { getActiveHook } from "./post-unit-hooks.js";
|
|
13
13
|
import { getLedger, getProjectTotals } from "./metrics.js";
|
|
14
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
14
15
|
import {
|
|
15
16
|
resolveMilestoneFile,
|
|
16
17
|
resolveSliceFile,
|
|
@@ -24,7 +25,11 @@ import { makeUI } from "../shared/tui.js";
|
|
|
24
25
|
import { GLYPH, INDENT } from "../shared/mod.js";
|
|
25
26
|
import { computeProgressScore } from "./progress-score.js";
|
|
26
27
|
import { getActiveWorktreeName } from "./worktree-command.js";
|
|
27
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
getGlobalGSDPreferencesPath,
|
|
30
|
+
getProjectGSDPreferencesPath,
|
|
31
|
+
parsePreferencesMarkdown,
|
|
32
|
+
} from "./preferences.js";
|
|
28
33
|
import { resolveServiceTierIcon, getEffectiveServiceTier } from "./service-tier.js";
|
|
29
34
|
import { parseUnitId } from "./unit-id.js";
|
|
30
35
|
import {
|
|
@@ -370,26 +375,74 @@ export type WidgetMode = "full" | "small" | "min" | "off";
|
|
|
370
375
|
const WIDGET_MODES: WidgetMode[] = ["full", "small", "min", "off"];
|
|
371
376
|
let widgetMode: WidgetMode = "full";
|
|
372
377
|
let widgetModeInitialized = false;
|
|
378
|
+
let widgetModePreferencePath: string | null = null;
|
|
379
|
+
|
|
380
|
+
function safeReadTextFile(path: string): string | null {
|
|
381
|
+
try {
|
|
382
|
+
if (!existsSync(path)) return null;
|
|
383
|
+
return readFileSync(path, "utf-8");
|
|
384
|
+
} catch {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function readWidgetModeFromFile(path: string): WidgetMode | undefined {
|
|
390
|
+
const raw = safeReadTextFile(path);
|
|
391
|
+
if (!raw) return undefined;
|
|
392
|
+
const prefs = parsePreferencesMarkdown(raw);
|
|
393
|
+
const saved = prefs?.widget_mode;
|
|
394
|
+
if (saved && WIDGET_MODES.includes(saved as WidgetMode)) {
|
|
395
|
+
return saved as WidgetMode;
|
|
396
|
+
}
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function resolveWidgetModePreferencePath(
|
|
401
|
+
projectPath = getProjectGSDPreferencesPath(),
|
|
402
|
+
globalPath = getGlobalGSDPreferencesPath(),
|
|
403
|
+
): string {
|
|
404
|
+
if (readWidgetModeFromFile(projectPath)) {
|
|
405
|
+
return projectPath;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (readWidgetModeFromFile(globalPath)) {
|
|
409
|
+
return globalPath;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (safeReadTextFile(projectPath) !== null) return projectPath;
|
|
413
|
+
if (safeReadTextFile(globalPath) !== null) return globalPath;
|
|
414
|
+
return getGlobalGSDPreferencesPath();
|
|
415
|
+
}
|
|
373
416
|
|
|
374
417
|
/** Load widget mode from preferences (once). */
|
|
375
|
-
function ensureWidgetModeLoaded(): void {
|
|
418
|
+
function ensureWidgetModeLoaded(projectPath?: string, globalPath?: string): void {
|
|
376
419
|
if (widgetModeInitialized) return;
|
|
377
420
|
widgetModeInitialized = true;
|
|
378
421
|
try {
|
|
379
|
-
const
|
|
380
|
-
const
|
|
422
|
+
const resolvedProjectPath = projectPath ?? getProjectGSDPreferencesPath();
|
|
423
|
+
const resolvedGlobalPath = globalPath ?? getGlobalGSDPreferencesPath();
|
|
424
|
+
const saved = readWidgetModeFromFile(resolvedProjectPath) ?? readWidgetModeFromFile(resolvedGlobalPath);
|
|
381
425
|
if (saved && WIDGET_MODES.includes(saved as WidgetMode)) {
|
|
382
426
|
widgetMode = saved as WidgetMode;
|
|
383
427
|
}
|
|
428
|
+
widgetModePreferencePath = resolveWidgetModePreferencePath(resolvedProjectPath, resolvedGlobalPath);
|
|
384
429
|
} catch (err) { /* non-fatal — use default */
|
|
385
|
-
logWarning("dashboard", `operation failed: ${
|
|
430
|
+
logWarning("dashboard", `operation failed: ${getErrorMessage(err)}`);
|
|
431
|
+
widgetModePreferencePath = getGlobalGSDPreferencesPath();
|
|
386
432
|
}
|
|
387
433
|
}
|
|
388
434
|
|
|
389
|
-
/**
|
|
390
|
-
|
|
435
|
+
/**
|
|
436
|
+
* Persist widget mode to the preference file that owns the effective value.
|
|
437
|
+
* Project-scoped widget_mode wins over global; if neither scope defines it,
|
|
438
|
+
* we prefer an existing project preferences file and otherwise fall back to
|
|
439
|
+
* the global preferences file.
|
|
440
|
+
*/
|
|
441
|
+
function persistWidgetMode(
|
|
442
|
+
mode: WidgetMode,
|
|
443
|
+
prefsPath = widgetModePreferencePath ?? resolveWidgetModePreferencePath(),
|
|
444
|
+
): void {
|
|
391
445
|
try {
|
|
392
|
-
const prefsPath = getGlobalGSDPreferencesPath();
|
|
393
446
|
let content = "";
|
|
394
447
|
if (existsSync(prefsPath)) {
|
|
395
448
|
content = readFileSync(prefsPath, "utf-8");
|
|
@@ -408,26 +461,34 @@ function persistWidgetMode(mode: WidgetMode): void {
|
|
|
408
461
|
}
|
|
409
462
|
|
|
410
463
|
/** Cycle to the next widget mode. Returns the new mode. */
|
|
411
|
-
export function cycleWidgetMode(): WidgetMode {
|
|
412
|
-
ensureWidgetModeLoaded();
|
|
464
|
+
export function cycleWidgetMode(projectPath?: string, globalPath?: string): WidgetMode {
|
|
465
|
+
ensureWidgetModeLoaded(projectPath, globalPath);
|
|
413
466
|
const idx = WIDGET_MODES.indexOf(widgetMode);
|
|
414
467
|
widgetMode = WIDGET_MODES[(idx + 1) % WIDGET_MODES.length];
|
|
415
|
-
persistWidgetMode(widgetMode);
|
|
468
|
+
persistWidgetMode(widgetMode, widgetModePreferencePath ?? resolveWidgetModePreferencePath(projectPath, globalPath));
|
|
416
469
|
return widgetMode;
|
|
417
470
|
}
|
|
418
471
|
|
|
419
472
|
/** Set widget mode directly. */
|
|
420
|
-
export function setWidgetMode(mode: WidgetMode): void {
|
|
473
|
+
export function setWidgetMode(mode: WidgetMode, projectPath?: string, globalPath?: string): void {
|
|
474
|
+
ensureWidgetModeLoaded(projectPath, globalPath);
|
|
421
475
|
widgetMode = mode;
|
|
422
|
-
persistWidgetMode(widgetMode);
|
|
476
|
+
persistWidgetMode(widgetMode, widgetModePreferencePath ?? resolveWidgetModePreferencePath(projectPath, globalPath));
|
|
423
477
|
}
|
|
424
478
|
|
|
425
479
|
/** Get current widget mode. */
|
|
426
|
-
export function getWidgetMode(): WidgetMode {
|
|
427
|
-
ensureWidgetModeLoaded();
|
|
480
|
+
export function getWidgetMode(projectPath?: string, globalPath?: string): WidgetMode {
|
|
481
|
+
ensureWidgetModeLoaded(projectPath, globalPath);
|
|
428
482
|
return widgetMode;
|
|
429
483
|
}
|
|
430
484
|
|
|
485
|
+
/** Test-only reset for widget mode caching. */
|
|
486
|
+
export function _resetWidgetModeForTests(): void {
|
|
487
|
+
widgetMode = "full";
|
|
488
|
+
widgetModeInitialized = false;
|
|
489
|
+
widgetModePreferencePath = null;
|
|
490
|
+
}
|
|
491
|
+
|
|
431
492
|
// ─── Progress Widget ──────────────────────────────────────────────────────────
|
|
432
493
|
|
|
433
494
|
/** State accessors passed to updateProgressWidget to avoid direct global access */
|
|
@@ -901,4 +962,3 @@ function padToWidth(s: string, colWidth: number): string {
|
|
|
901
962
|
if (vis >= colWidth) return truncateToWidth(s, colWidth, "…");
|
|
902
963
|
return s + " ".repeat(colWidth - vis);
|
|
903
964
|
}
|
|
904
|
-
|
|
@@ -379,40 +379,8 @@ export const DISPATCH_RULES: DispatchRule[] = [
|
|
|
379
379
|
},
|
|
380
380
|
},
|
|
381
381
|
{
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
if (state.phase !== "planning") return null;
|
|
385
|
-
// Phase skip: skip research when preference or profile says so
|
|
386
|
-
if (prefs?.phases?.skip_research || prefs?.phases?.skip_slice_research)
|
|
387
|
-
return null;
|
|
388
|
-
if (!state.activeSlice) return missingSliceStop(mid, state.phase);
|
|
389
|
-
const sid = state.activeSlice!.id;
|
|
390
|
-
const sTitle = state.activeSlice!.title;
|
|
391
|
-
const researchFile = resolveSliceFile(basePath, mid, sid, "RESEARCH");
|
|
392
|
-
if (researchFile) return null; // has research, fall through
|
|
393
|
-
// Skip slice research for S01 when milestone research already exists —
|
|
394
|
-
// the milestone research already covers the same ground for the first slice.
|
|
395
|
-
const milestoneResearchFile = resolveMilestoneFile(
|
|
396
|
-
basePath,
|
|
397
|
-
mid,
|
|
398
|
-
"RESEARCH",
|
|
399
|
-
);
|
|
400
|
-
if (milestoneResearchFile && sid === "S01") return null; // fall through to plan-slice
|
|
401
|
-
return {
|
|
402
|
-
action: "dispatch",
|
|
403
|
-
unitType: "research-slice",
|
|
404
|
-
unitId: `${mid}/${sid}`,
|
|
405
|
-
prompt: await buildResearchSlicePrompt(
|
|
406
|
-
mid,
|
|
407
|
-
midTitle,
|
|
408
|
-
sid,
|
|
409
|
-
sTitle,
|
|
410
|
-
basePath,
|
|
411
|
-
),
|
|
412
|
-
};
|
|
413
|
-
},
|
|
414
|
-
},
|
|
415
|
-
{
|
|
382
|
+
// Keep this rule before the single-slice research rule so the multi-slice
|
|
383
|
+
// path wins whenever 2+ slices are ready.
|
|
416
384
|
name: "planning (multiple slices need research) → parallel-research-slices",
|
|
417
385
|
match: async ({ state, mid, midTitle, basePath, prefs }) => {
|
|
418
386
|
if (state.phase !== "planning") return null;
|
|
@@ -459,6 +427,40 @@ export const DISPATCH_RULES: DispatchRule[] = [
|
|
|
459
427
|
};
|
|
460
428
|
},
|
|
461
429
|
},
|
|
430
|
+
{
|
|
431
|
+
name: "planning (no research, not S01) → research-slice",
|
|
432
|
+
match: async ({ state, mid, midTitle, basePath, prefs }) => {
|
|
433
|
+
if (state.phase !== "planning") return null;
|
|
434
|
+
// Phase skip: skip research when preference or profile says so
|
|
435
|
+
if (prefs?.phases?.skip_research || prefs?.phases?.skip_slice_research)
|
|
436
|
+
return null;
|
|
437
|
+
if (!state.activeSlice) return missingSliceStop(mid, state.phase);
|
|
438
|
+
const sid = state.activeSlice!.id;
|
|
439
|
+
const sTitle = state.activeSlice!.title;
|
|
440
|
+
const researchFile = resolveSliceFile(basePath, mid, sid, "RESEARCH");
|
|
441
|
+
if (researchFile) return null; // has research, fall through
|
|
442
|
+
// Skip slice research for S01 when milestone research already exists —
|
|
443
|
+
// the milestone research already covers the same ground for the first slice.
|
|
444
|
+
const milestoneResearchFile = resolveMilestoneFile(
|
|
445
|
+
basePath,
|
|
446
|
+
mid,
|
|
447
|
+
"RESEARCH",
|
|
448
|
+
);
|
|
449
|
+
if (milestoneResearchFile && sid === "S01") return null; // fall through to plan-slice
|
|
450
|
+
return {
|
|
451
|
+
action: "dispatch",
|
|
452
|
+
unitType: "research-slice",
|
|
453
|
+
unitId: `${mid}/${sid}`,
|
|
454
|
+
prompt: await buildResearchSlicePrompt(
|
|
455
|
+
mid,
|
|
456
|
+
midTitle,
|
|
457
|
+
sid,
|
|
458
|
+
sTitle,
|
|
459
|
+
basePath,
|
|
460
|
+
),
|
|
461
|
+
};
|
|
462
|
+
},
|
|
463
|
+
},
|
|
462
464
|
{
|
|
463
465
|
name: "planning → plan-slice",
|
|
464
466
|
match: async ({ state, mid, midTitle, basePath }) => {
|
|
@@ -883,4 +885,3 @@ export async function resolveDispatch(
|
|
|
883
885
|
export function getDispatchRuleNames(): string[] {
|
|
884
886
|
return DISPATCH_RULES.map((r) => r.name);
|
|
885
887
|
}
|
|
886
|
-
|
|
@@ -1592,7 +1592,7 @@ export async function buildValidateMilestonePrompt(
|
|
|
1592
1592
|
logWarning("prompt", `buildValidateMilestonePrompt verification classes lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1593
1593
|
}
|
|
1594
1594
|
|
|
1595
|
-
// Inline all slice summaries and
|
|
1595
|
+
// Inline all slice summaries and assessment results
|
|
1596
1596
|
let valSliceIds: string[] = [];
|
|
1597
1597
|
try {
|
|
1598
1598
|
const { isDbAvailable, getMilestoneSlices } = await import("./gsd-db.js");
|
|
@@ -1617,10 +1617,10 @@ export async function buildValidateMilestonePrompt(
|
|
|
1617
1617
|
const summaryRel = relSliceFile(base, mid, sid, "SUMMARY");
|
|
1618
1618
|
inlined.push(await inlineFile(summaryPath, summaryRel, `${sid} Summary`));
|
|
1619
1619
|
|
|
1620
|
-
const
|
|
1621
|
-
const
|
|
1622
|
-
const
|
|
1623
|
-
if (
|
|
1620
|
+
const assessmentPath = resolveSliceFile(base, mid, sid, "ASSESSMENT");
|
|
1621
|
+
const assessmentRel = relSliceFile(base, mid, sid, "ASSESSMENT");
|
|
1622
|
+
const assessmentInline = await inlineFileOptional(assessmentPath, assessmentRel, `${sid} Assessment`);
|
|
1623
|
+
if (assessmentInline) inlined.push(assessmentInline);
|
|
1624
1624
|
}
|
|
1625
1625
|
|
|
1626
1626
|
// Aggregate unresolved follow-ups and known limitations across slices
|
|
@@ -2150,4 +2150,3 @@ export async function buildRewriteDocsPrompt(
|
|
|
2150
2150
|
overridesPath: relGsdRootFile("OVERRIDES"),
|
|
2151
2151
|
});
|
|
2152
2152
|
}
|
|
2153
|
-
|
|
@@ -13,7 +13,7 @@ import { appendEvent } from "./workflow-events.js";
|
|
|
13
13
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
14
14
|
import { clearParseCache } from "./files.js";
|
|
15
15
|
import { parseRoadmap as parseLegacyRoadmap, parsePlan as parseLegacyPlan } from "./parsers-legacy.js";
|
|
16
|
-
import { isDbAvailable, getTask, getSlice, getSliceTasks, updateTaskStatus, updateSliceStatus } from "./gsd-db.js";
|
|
16
|
+
import { isDbAvailable, getTask, getSlice, getSliceTasks, getPendingGates, updateTaskStatus, updateSliceStatus } from "./gsd-db.js";
|
|
17
17
|
import { isValidationTerminal } from "./state.js";
|
|
18
18
|
import { getErrorMessage } from "./error-utils.js";
|
|
19
19
|
import { logWarning, logError } from "./workflow-logger.js";
|
|
@@ -248,8 +248,7 @@ export function verifyExpectedArtifact(
|
|
|
248
248
|
if (gateIds.length === 0) return true;
|
|
249
249
|
|
|
250
250
|
try {
|
|
251
|
-
const
|
|
252
|
-
const pending = getPending(mid, sid, "slice");
|
|
251
|
+
const pending = getPendingGates(mid, sid, "slice");
|
|
253
252
|
const pendingIds = new Set(pending.map((g: any) => g.gate_id));
|
|
254
253
|
// All dispatched gates must no longer be pending
|
|
255
254
|
for (const gid of gateIds) {
|
|
@@ -480,22 +479,23 @@ function abortAndResetMerge(
|
|
|
480
479
|
}
|
|
481
480
|
}
|
|
482
481
|
|
|
482
|
+
export type MergeReconcileResult = "clean" | "reconciled" | "blocked";
|
|
483
|
+
|
|
483
484
|
/**
|
|
484
485
|
* Detect leftover merge state from a prior session and reconcile it.
|
|
485
486
|
* If MERGE_HEAD or SQUASH_MSG exists, check whether conflicts are resolved.
|
|
486
|
-
* If resolved: finalize the commit. If
|
|
487
|
-
*
|
|
488
|
-
* Returns true if state was dirty and re-derivation is needed.
|
|
487
|
+
* If resolved: finalize the commit. If only .gsd conflicts remain: auto-resolve.
|
|
488
|
+
* If code conflicts remain: fail safe without modifying the worktree.
|
|
489
489
|
*/
|
|
490
490
|
export function reconcileMergeState(
|
|
491
491
|
basePath: string,
|
|
492
492
|
ctx: ExtensionContext,
|
|
493
|
-
):
|
|
493
|
+
): MergeReconcileResult {
|
|
494
494
|
const mergeHeadPath = join(basePath, ".git", "MERGE_HEAD");
|
|
495
495
|
const squashMsgPath = join(basePath, ".git", "SQUASH_MSG");
|
|
496
496
|
const hasMergeHead = existsSync(mergeHeadPath);
|
|
497
497
|
const hasSquashMsg = existsSync(squashMsgPath);
|
|
498
|
-
if (!hasMergeHead && !hasSquashMsg) return
|
|
498
|
+
if (!hasMergeHead && !hasSquashMsg) return "clean";
|
|
499
499
|
|
|
500
500
|
const conflictedFiles = nativeConflictFiles(basePath);
|
|
501
501
|
if (conflictedFiles.length === 0) {
|
|
@@ -511,7 +511,7 @@ export function reconcileMergeState(
|
|
|
511
511
|
} catch (err) {
|
|
512
512
|
const errorMessage = getErrorMessage(err);
|
|
513
513
|
ctx.ui.notify(`Failed to finalize leftover merge/squash commit: ${errorMessage}`, "error");
|
|
514
|
-
return
|
|
514
|
+
return "blocked";
|
|
515
515
|
}
|
|
516
516
|
} else {
|
|
517
517
|
// Still conflicted — try auto-resolving .gsd/ state file conflicts (#530)
|
|
@@ -551,15 +551,16 @@ export function reconcileMergeState(
|
|
|
551
551
|
);
|
|
552
552
|
}
|
|
553
553
|
} else {
|
|
554
|
-
// Code conflicts present —
|
|
555
|
-
|
|
554
|
+
// Code conflicts present — fail safe and preserve any manual resolution
|
|
555
|
+
// work instead of discarding it with merge --abort/reset --hard.
|
|
556
556
|
ctx.ui.notify(
|
|
557
|
-
"Detected leftover merge state with unresolved conflicts
|
|
558
|
-
"
|
|
557
|
+
"Detected leftover merge state with unresolved code conflicts. Auto-mode will pause without modifying the worktree so manual conflict resolution is preserved.",
|
|
558
|
+
"error",
|
|
559
559
|
);
|
|
560
|
+
return "blocked";
|
|
560
561
|
}
|
|
561
562
|
}
|
|
562
|
-
return
|
|
563
|
+
return "reconciled";
|
|
563
564
|
}
|
|
564
565
|
|
|
565
566
|
// ─── Loop Remediation ─────────────────────────────────────────────────────────
|
|
@@ -618,4 +619,3 @@ export function buildLoopRemediationSteps(
|
|
|
618
619
|
}
|
|
619
620
|
return null;
|
|
620
621
|
}
|
|
621
|
-
|
|
@@ -7,6 +7,16 @@ import { loadEffectiveGSDPreferences } from "../preferences.js";
|
|
|
7
7
|
import { ensureDbOpen } from "./dynamic-tools.js";
|
|
8
8
|
import { StringEnum } from "@gsd/pi-ai";
|
|
9
9
|
import { logError } from "../workflow-logger.js";
|
|
10
|
+
import { getErrorMessage } from "../error-utils.js";
|
|
11
|
+
import { shouldBlockContextArtifactSave } from "./write-gate.js";
|
|
12
|
+
|
|
13
|
+
const SUPPORTED_SUMMARY_ARTIFACT_TYPES = ["SUMMARY", "RESEARCH", "CONTEXT", "ASSESSMENT", "CONTEXT-DRAFT"] as const;
|
|
14
|
+
|
|
15
|
+
export function isSupportedSummaryArtifactType(
|
|
16
|
+
artifactType: string,
|
|
17
|
+
): artifactType is (typeof SUPPORTED_SUMMARY_ARTIFACT_TYPES)[number] {
|
|
18
|
+
return (SUPPORTED_SUMMARY_ARTIFACT_TYPES as readonly string[]).includes(artifactType);
|
|
19
|
+
}
|
|
10
20
|
|
|
11
21
|
/**
|
|
12
22
|
* Register an alias tool that shares the same execute function as its canonical counterpart.
|
|
@@ -283,13 +293,23 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|
|
283
293
|
details: { operation: "save_summary", error: "db_unavailable" } as any,
|
|
284
294
|
};
|
|
285
295
|
}
|
|
286
|
-
|
|
287
|
-
if (!validTypes.includes(params.artifact_type)) {
|
|
296
|
+
if (!isSupportedSummaryArtifactType(params.artifact_type)) {
|
|
288
297
|
return {
|
|
289
|
-
content: [{ type: "text" as const, text: `Error: Invalid artifact_type "${params.artifact_type}". Must be one of: ${
|
|
298
|
+
content: [{ type: "text" as const, text: `Error: Invalid artifact_type "${params.artifact_type}". Must be one of: ${SUPPORTED_SUMMARY_ARTIFACT_TYPES.join(", ")}` }],
|
|
290
299
|
details: { operation: "save_summary", error: "invalid_artifact_type" } as any,
|
|
291
300
|
};
|
|
292
301
|
}
|
|
302
|
+
const contextGuard = shouldBlockContextArtifactSave(
|
|
303
|
+
params.artifact_type,
|
|
304
|
+
params.milestone_id ?? null,
|
|
305
|
+
params.slice_id ?? null,
|
|
306
|
+
);
|
|
307
|
+
if (contextGuard.block) {
|
|
308
|
+
return {
|
|
309
|
+
content: [{ type: "text" as const, text: `Error saving artifact: ${contextGuard.reason ?? "context write blocked"}` }],
|
|
310
|
+
details: { operation: "save_summary", error: "context_write_blocked" } as any,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
293
313
|
try {
|
|
294
314
|
let relativePath: string;
|
|
295
315
|
if (params.task_id && params.slice_id) {
|
|
@@ -333,16 +353,17 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|
|
333
353
|
"Computes the file path from milestone/slice/task IDs automatically.",
|
|
334
354
|
promptSnippet: "Save a GSD artifact (summary/research/context/assessment) to DB and disk",
|
|
335
355
|
promptGuidelines: [
|
|
336
|
-
"Use gsd_summary_save to persist structured artifacts (SUMMARY, RESEARCH, CONTEXT, ASSESSMENT).",
|
|
356
|
+
"Use gsd_summary_save to persist structured artifacts (SUMMARY, RESEARCH, CONTEXT, ASSESSMENT, CONTEXT-DRAFT).",
|
|
337
357
|
"milestone_id is required. slice_id and task_id are optional — they determine the file path.",
|
|
338
358
|
"The tool computes the relative path automatically: milestones/M001/M001-SUMMARY.md, milestones/M001/slices/S01/S01-SUMMARY.md, etc.",
|
|
339
|
-
"artifact_type must be one of: SUMMARY, RESEARCH, CONTEXT, ASSESSMENT.",
|
|
359
|
+
"artifact_type must be one of: SUMMARY, RESEARCH, CONTEXT, ASSESSMENT, CONTEXT-DRAFT.",
|
|
360
|
+
"Use CONTEXT-DRAFT for incremental draft persistence; use CONTEXT for the final milestone context after depth verification.",
|
|
340
361
|
],
|
|
341
362
|
parameters: Type.Object({
|
|
342
363
|
milestone_id: Type.String({ description: "Milestone ID (e.g. M001)" }),
|
|
343
364
|
slice_id: Type.Optional(Type.String({ description: "Slice ID (e.g. S01)" })),
|
|
344
365
|
task_id: Type.Optional(Type.String({ description: "Task ID (e.g. T01)" })),
|
|
345
|
-
artifact_type: Type.String({ description: "One of: SUMMARY, RESEARCH, CONTEXT, ASSESSMENT" }),
|
|
366
|
+
artifact_type: Type.String({ description: "One of: SUMMARY, RESEARCH, CONTEXT, ASSESSMENT, CONTEXT-DRAFT" }),
|
|
346
367
|
content: Type.String({ description: "The full markdown content of the artifact" }),
|
|
347
368
|
}),
|
|
348
369
|
execute: summarySaveExecute,
|
|
@@ -6,7 +6,7 @@ import { isToolCallEventType } from "@gsd/pi-coding-agent";
|
|
|
6
6
|
import { buildMilestoneFileName, resolveMilestonePath, resolveSliceFile, resolveSlicePath } from "../paths.js";
|
|
7
7
|
import { buildBeforeAgentStartResult } from "./system-context.js";
|
|
8
8
|
import { handleAgentEnd } from "./agent-end-recovery.js";
|
|
9
|
-
import { clearDiscussionFlowState,
|
|
9
|
+
import { clearDiscussionFlowState, isDepthConfirmationAnswer, isQueuePhaseActive, markDepthVerified, resetWriteGateState, shouldBlockContextWrite, shouldBlockQueueExecution, isGateQuestionId, setPendingGate, clearPendingGate, getPendingGate, shouldBlockPendingGate, shouldBlockPendingGateBash, extractDepthVerificationMilestoneId } from "./write-gate.js";
|
|
10
10
|
import { isBlockedStateFile, isBashWriteToStateFile, BLOCKED_WRITE_ERROR } from "../write-intercept.js";
|
|
11
11
|
import { cleanupQuickBranch } from "../quick.js";
|
|
12
12
|
import { getDiscussionMilestoneId } from "../guided-flow.js";
|
|
@@ -24,6 +24,7 @@ import { logWarning as safetyLogWarning } from "../workflow-logger.js";
|
|
|
24
24
|
import { installNotifyInterceptor } from "./notify-interceptor.js";
|
|
25
25
|
import { initNotificationStore } from "../notification-store.js";
|
|
26
26
|
import { initNotificationWidget } from "../notification-widget.js";
|
|
27
|
+
import { initHealthWidget } from "../health-widget.js";
|
|
27
28
|
|
|
28
29
|
// Skip the welcome screen on the very first session_start — cli.ts already
|
|
29
30
|
// printed it before the TUI launched. Only re-print on /clear (subsequent sessions).
|
|
@@ -39,6 +40,7 @@ export function registerHooks(pi: ExtensionAPI): void {
|
|
|
39
40
|
initNotificationStore(process.cwd());
|
|
40
41
|
installNotifyInterceptor(ctx);
|
|
41
42
|
initNotificationWidget(ctx);
|
|
43
|
+
initHealthWidget(ctx);
|
|
42
44
|
resetWriteGateState();
|
|
43
45
|
resetToolCallLoopGuard();
|
|
44
46
|
resetAskUserQuestionsCache();
|
|
@@ -162,12 +164,50 @@ export function registerHooks(pi: ExtensionAPI): void {
|
|
|
162
164
|
});
|
|
163
165
|
|
|
164
166
|
pi.on("tool_call", async (event) => {
|
|
167
|
+
const discussionBasePath = process.cwd();
|
|
165
168
|
// ── Loop guard: block repeated identical tool calls ──
|
|
166
169
|
const loopCheck = checkToolCallLoop(event.toolName, event.input as Record<string, unknown>);
|
|
167
170
|
if (loopCheck.block) {
|
|
168
171
|
return { block: true, reason: loopCheck.reason };
|
|
169
172
|
}
|
|
170
173
|
|
|
174
|
+
// ── Discussion gate enforcement: track pending gate questions ─────────
|
|
175
|
+
// Only gate-shaped ask_user_questions calls should block execution.
|
|
176
|
+
// The gate stays pending until the user selects the approval option.
|
|
177
|
+
if (event.toolName === "ask_user_questions") {
|
|
178
|
+
const milestoneId = getDiscussionMilestoneId(discussionBasePath);
|
|
179
|
+
const inDiscussion = milestoneId !== null || isQueuePhaseActive();
|
|
180
|
+
if (inDiscussion) {
|
|
181
|
+
const questions: any[] = (event.input as any)?.questions ?? [];
|
|
182
|
+
const questionId = questions.find((question) => typeof question?.id === "string" && isGateQuestionId(question.id))?.id;
|
|
183
|
+
if (typeof questionId === "string") {
|
|
184
|
+
setPendingGate(questionId);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Discussion gate enforcement: block tool calls while gate is pending ──
|
|
190
|
+
// If ask_user_questions was called with a gate ID but hasn't been confirmed,
|
|
191
|
+
// block all non-read-only tool calls to prevent the model from skipping gates.
|
|
192
|
+
if (getPendingGate()) {
|
|
193
|
+
const milestoneId = getDiscussionMilestoneId(discussionBasePath);
|
|
194
|
+
if (isToolCallEventType("bash", event)) {
|
|
195
|
+
const bashGuard = shouldBlockPendingGateBash(
|
|
196
|
+
event.input.command,
|
|
197
|
+
milestoneId,
|
|
198
|
+
isQueuePhaseActive(),
|
|
199
|
+
);
|
|
200
|
+
if (bashGuard.block) return bashGuard;
|
|
201
|
+
} else {
|
|
202
|
+
const gateGuard = shouldBlockPendingGate(
|
|
203
|
+
event.toolName,
|
|
204
|
+
milestoneId,
|
|
205
|
+
isQueuePhaseActive(),
|
|
206
|
+
);
|
|
207
|
+
if (gateGuard.block) return gateGuard;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
171
211
|
// ── Queue-mode execution guard (#2545): block source-code mutations ──
|
|
172
212
|
// When /gsd queue is active, the agent should only create milestones,
|
|
173
213
|
// not execute work. Block write/edit to non-.gsd/ paths and bash commands
|
|
@@ -210,8 +250,7 @@ export function registerHooks(pi: ExtensionAPI): void {
|
|
|
210
250
|
const result = shouldBlockContextWrite(
|
|
211
251
|
event.toolName,
|
|
212
252
|
event.input.path,
|
|
213
|
-
getDiscussionMilestoneId(),
|
|
214
|
-
isDepthVerified(),
|
|
253
|
+
getDiscussionMilestoneId(discussionBasePath),
|
|
215
254
|
isQueuePhaseActive(),
|
|
216
255
|
);
|
|
217
256
|
if (result.block) return result;
|
|
@@ -239,21 +278,43 @@ export function registerHooks(pi: ExtensionAPI): void {
|
|
|
239
278
|
|
|
240
279
|
pi.on("tool_result", async (event) => {
|
|
241
280
|
if (event.toolName !== "ask_user_questions") return;
|
|
242
|
-
const milestoneId = getDiscussionMilestoneId();
|
|
281
|
+
const milestoneId = getDiscussionMilestoneId(process.cwd());
|
|
243
282
|
const queueActive = isQueuePhaseActive();
|
|
244
283
|
if (!milestoneId && !queueActive) return;
|
|
245
284
|
|
|
246
285
|
const details = event.details as any;
|
|
247
|
-
if (details?.cancelled || !details?.response) return;
|
|
248
286
|
|
|
287
|
+
// ── Discussion gate enforcement: handle gate question responses ──
|
|
288
|
+
// If the result is cancelled or has no response, the pending gate stays active
|
|
289
|
+
// so the model is blocked from non-read-only tools until it re-asks.
|
|
290
|
+
// If the user responded at all (even "needs adjustment"), clear the pending gate
|
|
291
|
+
// because the user engaged — the prompt handles the re-ask-after-adjustment flow.
|
|
249
292
|
const questions: any[] = (event.input as any)?.questions ?? [];
|
|
293
|
+
const currentPendingGate = getPendingGate();
|
|
294
|
+
if (currentPendingGate) {
|
|
295
|
+
if (details?.cancelled || !details?.response) {
|
|
296
|
+
// Gate stays pending — model will be blocked from non-read-only tools
|
|
297
|
+
// until it re-asks and gets a valid response
|
|
298
|
+
} else {
|
|
299
|
+
const pendingQuestion = questions.find((question) => question?.id === currentPendingGate);
|
|
300
|
+
if (pendingQuestion) {
|
|
301
|
+
const answer = details.response?.answers?.[currentPendingGate];
|
|
302
|
+
if (isDepthConfirmationAnswer(answer?.selected, pendingQuestion.options)) {
|
|
303
|
+
clearPendingGate();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (details?.cancelled || !details?.response) return;
|
|
310
|
+
|
|
250
311
|
for (const question of questions) {
|
|
251
312
|
if (typeof question.id === "string" && question.id.includes("depth_verification")) {
|
|
252
313
|
// Only unlock the gate if the user selected the first option (confirmation).
|
|
253
314
|
// Cross-references against the question's defined options to reject free-form "Other" text.
|
|
254
315
|
const answer = details.response?.answers?.[question.id];
|
|
255
316
|
if (isDepthConfirmationAnswer(answer?.selected, question.options)) {
|
|
256
|
-
markDepthVerified();
|
|
317
|
+
markDepthVerified(extractDepthVerificationMilestoneId(question.id) ?? milestoneId);
|
|
257
318
|
}
|
|
258
319
|
break;
|
|
259
320
|
}
|
|
@@ -7,18 +7,20 @@ import { Key } from "@gsd/pi-tui";
|
|
|
7
7
|
import { GSDDashboardOverlay } from "../dashboard-overlay.js";
|
|
8
8
|
import { GSDNotificationOverlay } from "../notification-overlay.js";
|
|
9
9
|
import { ParallelMonitorOverlay } from "../parallel-monitor-overlay.js";
|
|
10
|
+
import { projectRoot } from "../commands/context.js";
|
|
10
11
|
import { shortcutDesc } from "../../shared/mod.js";
|
|
11
12
|
|
|
12
13
|
export function registerShortcuts(pi: ExtensionAPI): void {
|
|
13
14
|
pi.registerShortcut(Key.ctrlAlt("g"), {
|
|
14
15
|
description: shortcutDesc("Open GSD dashboard", "/gsd status"),
|
|
15
16
|
handler: async (ctx) => {
|
|
16
|
-
|
|
17
|
+
const basePath = projectRoot();
|
|
18
|
+
if (!existsSync(join(basePath, ".gsd"))) {
|
|
17
19
|
ctx.ui.notify("No .gsd/ directory found. Run /gsd to start.", "info");
|
|
18
20
|
return;
|
|
19
21
|
}
|
|
20
|
-
await ctx.ui.custom<
|
|
21
|
-
(tui, theme, _kb, done) => new GSDDashboardOverlay(tui, theme, () => done()),
|
|
22
|
+
await ctx.ui.custom<boolean>(
|
|
23
|
+
(tui, theme, _kb, done) => new GSDDashboardOverlay(tui, theme, () => done(true)),
|
|
22
24
|
{
|
|
23
25
|
overlay: true,
|
|
24
26
|
overlayOptions: {
|
|
@@ -35,8 +37,8 @@ export function registerShortcuts(pi: ExtensionAPI): void {
|
|
|
35
37
|
pi.registerShortcut(Key.ctrlAlt("n"), {
|
|
36
38
|
description: shortcutDesc("Open notification history", "/gsd notifications"),
|
|
37
39
|
handler: async (ctx) => {
|
|
38
|
-
await ctx.ui.custom<
|
|
39
|
-
(tui, theme, _kb, done) => new GSDNotificationOverlay(tui, theme, () => done()),
|
|
40
|
+
await ctx.ui.custom<boolean>(
|
|
41
|
+
(tui, theme, _kb, done) => new GSDNotificationOverlay(tui, theme, () => done(true)),
|
|
40
42
|
{
|
|
41
43
|
overlay: true,
|
|
42
44
|
overlayOptions: {
|
|
@@ -54,13 +56,14 @@ export function registerShortcuts(pi: ExtensionAPI): void {
|
|
|
54
56
|
pi.registerShortcut(Key.ctrlAlt("p"), {
|
|
55
57
|
description: shortcutDesc("Open parallel worker monitor", "/gsd parallel watch"),
|
|
56
58
|
handler: async (ctx) => {
|
|
57
|
-
const
|
|
59
|
+
const basePath = projectRoot();
|
|
60
|
+
const parallelDir = join(basePath, ".gsd", "parallel");
|
|
58
61
|
if (!existsSync(parallelDir)) {
|
|
59
62
|
ctx.ui.notify("No parallel workers found. Run /gsd parallel start first.", "info");
|
|
60
63
|
return;
|
|
61
64
|
}
|
|
62
|
-
await ctx.ui.custom<
|
|
63
|
-
(tui, theme, _kb, done) => new ParallelMonitorOverlay(tui, theme, () => done()),
|
|
65
|
+
await ctx.ui.custom<boolean>(
|
|
66
|
+
(tui, theme, _kb, done) => new ParallelMonitorOverlay(tui, theme, () => done(true)),
|
|
64
67
|
{
|
|
65
68
|
overlay: true,
|
|
66
69
|
overlayOptions: {
|