gsd-pi 2.66.1-dev.0df32ec → 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 +17 -17
- 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 +17 -17
- 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/{Zw5aZFHFtOwjJSOsINh1m → HxFcJ8GrYNPsg9ARz7GPz}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{Zw5aZFHFtOwjJSOsINh1m → HxFcJ8GrYNPsg9ARz7GPz}/_ssgManifest.js +0 -0
|
@@ -9,6 +9,7 @@ import { deriveState, isValidationTerminal } from "../state.ts";
|
|
|
9
9
|
import { resolveExpectedArtifactPath, diagnoseExpectedArtifact } from "../auto-artifact-paths.ts";
|
|
10
10
|
import { verifyExpectedArtifact, buildLoopRemediationSteps } from "../auto-recovery.ts";
|
|
11
11
|
import { resolveDispatch, type DispatchContext } from "../auto-dispatch.ts";
|
|
12
|
+
import { buildValidateMilestonePrompt } from "../auto-prompts.ts";
|
|
12
13
|
import type { GSDState } from "../types.ts";
|
|
13
14
|
import { clearPathCache } from "../paths.ts";
|
|
14
15
|
import { clearParseCache } from "../files.ts";
|
|
@@ -57,6 +58,12 @@ function writeSliceSummary(base: string, mid: string, sid: string, content: stri
|
|
|
57
58
|
writeFileSync(join(dir, `${sid}-SUMMARY.md`), content);
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
function writeSliceAssessment(base: string, mid: string, sid: string, content: string): void {
|
|
62
|
+
const dir = join(base, ".gsd", "milestones", mid, "slices", sid);
|
|
63
|
+
mkdirSync(dir, { recursive: true });
|
|
64
|
+
writeFileSync(join(dir, `${sid}-ASSESSMENT.md`), content);
|
|
65
|
+
}
|
|
66
|
+
|
|
60
67
|
const ALL_DONE_ROADMAP = `# M001: Test Milestone
|
|
61
68
|
|
|
62
69
|
## Vision
|
|
@@ -192,6 +199,25 @@ test("deriveState returns complete when both VALIDATION and SUMMARY exist", asyn
|
|
|
192
199
|
}
|
|
193
200
|
});
|
|
194
201
|
|
|
202
|
+
test("buildValidateMilestonePrompt inlines ASSESSMENT evidence instead of UAT spec", async () => {
|
|
203
|
+
const base = makeTmpBase();
|
|
204
|
+
try {
|
|
205
|
+
writeRoadmap(base, "M001", ALL_DONE_ROADMAP);
|
|
206
|
+
const dir = join(base, ".gsd", "milestones", "M001");
|
|
207
|
+
writeFileSync(join(dir, "M001-CONTEXT.md"), CONTEXT_FILE);
|
|
208
|
+
writeSliceSummary(base, "M001", "S01", "# S01 Summary\nDelivered.");
|
|
209
|
+
writeFileSync(join(dir, "slices", "S01", "S01-UAT.md"), "# UAT Spec\nDo the thing.\n");
|
|
210
|
+
writeSliceAssessment(base, "M001", "S01", "---\nverdict: PASS\n---\n# Assessment\nEvidence captured.");
|
|
211
|
+
|
|
212
|
+
const prompt = await buildValidateMilestonePrompt("M001", "Test Milestone", base);
|
|
213
|
+
assert.match(prompt, /S01 Assessment/i, "prompt should inline assessment evidence");
|
|
214
|
+
assert.match(prompt, /verdict: PASS/i, "prompt should include the assessment verdict");
|
|
215
|
+
assert.doesNotMatch(prompt, /UAT Spec/i, "prompt should not inline the raw UAT spec as evidence");
|
|
216
|
+
} finally {
|
|
217
|
+
cleanup(base);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
195
221
|
// ─── Dispatch rule ────────────────────────────────────────────────────────
|
|
196
222
|
|
|
197
223
|
test("dispatch rule matches validating-milestone phase", async () => {
|
|
@@ -233,3 +233,62 @@ assert.ok(
|
|
|
233
233
|
overlaySrc.includes('from "../shared/mod.js"'),
|
|
234
234
|
"imports from shared barrel",
|
|
235
235
|
);
|
|
236
|
+
|
|
237
|
+
test("visualizer overlay closes on escape in filter and help submodes", async () => {
|
|
238
|
+
const mod = await import("../visualizer-overlay.js");
|
|
239
|
+
|
|
240
|
+
const mockTui = { requestRender: () => {} };
|
|
241
|
+
const mockTheme = {
|
|
242
|
+
fg: (_color: string, text: string) => text,
|
|
243
|
+
bold: (text: string) => text,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
let closedFilter = false;
|
|
247
|
+
const filterOverlay = new mod.GSDVisualizerOverlay(
|
|
248
|
+
mockTui,
|
|
249
|
+
mockTheme as any,
|
|
250
|
+
() => { closedFilter = true; },
|
|
251
|
+
);
|
|
252
|
+
filterOverlay.filterMode = true;
|
|
253
|
+
filterOverlay.handleInput("\u0003");
|
|
254
|
+
assert.equal(closedFilter, true, "Ctrl+C closes while filter mode is active");
|
|
255
|
+
filterOverlay.dispose();
|
|
256
|
+
|
|
257
|
+
let closedHelp = false;
|
|
258
|
+
const helpOverlay = new mod.GSDVisualizerOverlay(
|
|
259
|
+
mockTui,
|
|
260
|
+
mockTheme as any,
|
|
261
|
+
() => { closedHelp = true; },
|
|
262
|
+
);
|
|
263
|
+
helpOverlay.showHelp = true;
|
|
264
|
+
helpOverlay.handleInput("\u001b");
|
|
265
|
+
assert.equal(closedHelp, true, "Escape closes while help overlay is visible");
|
|
266
|
+
helpOverlay.dispose();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("visualizer overlay tab hitboxes include rendered badges", async () => {
|
|
270
|
+
const mod = await import("../visualizer-overlay.js");
|
|
271
|
+
|
|
272
|
+
const mockTui = { requestRender: () => {} };
|
|
273
|
+
const mockTheme = {
|
|
274
|
+
fg: (_color: string, text: string) => text,
|
|
275
|
+
bold: (text: string) => text,
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const overlay = new mod.GSDVisualizerOverlay(
|
|
279
|
+
mockTui,
|
|
280
|
+
mockTheme as any,
|
|
281
|
+
() => {},
|
|
282
|
+
);
|
|
283
|
+
overlay.loading = true;
|
|
284
|
+
overlay.data = { captures: { pendingCount: 3 } } as any;
|
|
285
|
+
|
|
286
|
+
const lines = overlay.render(120);
|
|
287
|
+
const tabLine = lines.find((line: string) => line.includes("Captures") && line.includes("(3)"));
|
|
288
|
+
assert.ok(tabLine, "rendered tab bar includes captures badge");
|
|
289
|
+
const plain = tabLine!.replace(/\x1b\[[0-9;]*m/g, "");
|
|
290
|
+
const badgeColumn = plain.indexOf("(3)") + 2;
|
|
291
|
+
overlay.handleInput(`\x1b[<0;${badgeColumn};2M`);
|
|
292
|
+
assert.equal(overlay.activeTab, 8, "clicking the badge area selects the captures tab");
|
|
293
|
+
overlay.dispose();
|
|
294
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import test, { afterEach } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, mkdirSync, rmSync, existsSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
|
|
7
|
+
import { appendEvent, readEvents } from "../workflow-events.ts";
|
|
8
|
+
import { listConflicts, reconcileWorktreeLogs, resolveConflict } from "../workflow-reconcile.ts";
|
|
9
|
+
import { closeDatabase } from "../gsd-db.ts";
|
|
10
|
+
|
|
11
|
+
const tmpDirs: string[] = [];
|
|
12
|
+
|
|
13
|
+
function makeTmpRepo(): { main: string; worktree: string } {
|
|
14
|
+
const root = mkdtempSync(join(tmpdir(), "workflow-reconcile-"));
|
|
15
|
+
const main = join(root, "main");
|
|
16
|
+
const worktree = join(root, "worktree");
|
|
17
|
+
mkdirSync(main, { recursive: true });
|
|
18
|
+
mkdirSync(worktree, { recursive: true });
|
|
19
|
+
tmpDirs.push(root);
|
|
20
|
+
return { main, worktree };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
closeDatabase();
|
|
25
|
+
for (const dir of tmpDirs) {
|
|
26
|
+
try {
|
|
27
|
+
rmSync(dir, { recursive: true, force: true });
|
|
28
|
+
} catch {
|
|
29
|
+
// Best-effort cleanup on platforms that keep files open briefly.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
tmpDirs.length = 0;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("resolveConflict(pick=main) rewrites the worktree log durably", () => {
|
|
36
|
+
const { main, worktree } = makeTmpRepo();
|
|
37
|
+
|
|
38
|
+
appendEvent(main, {
|
|
39
|
+
cmd: "plan_milestone",
|
|
40
|
+
params: { milestoneId: "M001", title: "Base Milestone" },
|
|
41
|
+
ts: "2026-01-01T00:00:00.000Z",
|
|
42
|
+
actor: "agent",
|
|
43
|
+
});
|
|
44
|
+
appendEvent(worktree, {
|
|
45
|
+
cmd: "plan_milestone",
|
|
46
|
+
params: { milestoneId: "M001", title: "Base Milestone" },
|
|
47
|
+
ts: "2026-01-01T00:00:00.000Z",
|
|
48
|
+
actor: "agent",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
appendEvent(main, {
|
|
52
|
+
cmd: "plan_milestone",
|
|
53
|
+
params: { milestoneId: "M001", title: "Main Choice" },
|
|
54
|
+
ts: "2026-01-01T00:01:00.000Z",
|
|
55
|
+
actor: "agent",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
appendEvent(worktree, {
|
|
59
|
+
cmd: "plan_milestone",
|
|
60
|
+
params: { milestoneId: "M001", title: "Worktree Choice" },
|
|
61
|
+
ts: "2026-01-01T00:01:00.000Z",
|
|
62
|
+
actor: "agent",
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const initial = reconcileWorktreeLogs(main, worktree);
|
|
66
|
+
assert.equal(initial.conflicts.length, 1, "expected one conflict before resolution");
|
|
67
|
+
assert.ok(listConflicts(main).length === 1, "CONFLICTS.md should exist after detection");
|
|
68
|
+
|
|
69
|
+
resolveConflict(main, worktree, "milestone:M001", "main");
|
|
70
|
+
|
|
71
|
+
assert.equal(listConflicts(main).length, 0, "conflict file should be cleared after resolving main");
|
|
72
|
+
const conflictsPath = join(main, ".gsd", "CONFLICTS.md");
|
|
73
|
+
assert.equal(
|
|
74
|
+
existsSync(conflictsPath),
|
|
75
|
+
false,
|
|
76
|
+
"CONFLICTS.md should be removed after the last conflict is resolved",
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const wtEvents = readEvents(join(worktree, ".gsd", "event-log.jsonl"));
|
|
80
|
+
assert.ok(
|
|
81
|
+
wtEvents.some((e) => e.cmd === "plan_milestone" && e.params.title === "Main Choice"),
|
|
82
|
+
"worktree log should be rewritten to the main-side resolution",
|
|
83
|
+
);
|
|
84
|
+
assert.ok(
|
|
85
|
+
!wtEvents.some((e) => e.cmd === "plan_milestone" && e.params.title === "Worktree Choice"),
|
|
86
|
+
"worktree log should no longer contain the discarded conflict event",
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const second = reconcileWorktreeLogs(main, worktree);
|
|
90
|
+
assert.equal(second.conflicts.length, 0, "reconcile should stay clean after choosing main");
|
|
91
|
+
});
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Exercises shouldBlockContextWrite() — a pure function that implements:
|
|
5
5
|
* (a) toolName !== "write" → pass
|
|
6
|
-
* (b)
|
|
6
|
+
* (b) milestone context must resolve to a verified milestone
|
|
7
7
|
* (c) path doesn't match /M\d+-CONTEXT\.md$/ → pass
|
|
8
|
-
* (d)
|
|
8
|
+
* (d) non-context files → pass
|
|
9
9
|
* (e) else → block with actionable reason
|
|
10
10
|
*/
|
|
11
11
|
|
|
@@ -14,12 +14,12 @@ import assert from 'node:assert/strict';
|
|
|
14
14
|
import {
|
|
15
15
|
isDepthConfirmationAnswer,
|
|
16
16
|
shouldBlockContextWrite,
|
|
17
|
-
isDepthVerified,
|
|
18
|
-
isQueuePhaseActive,
|
|
19
17
|
setQueuePhaseActive,
|
|
20
18
|
} from '../index.ts';
|
|
21
19
|
import {
|
|
22
20
|
markDepthVerified,
|
|
21
|
+
isMilestoneDepthVerified,
|
|
22
|
+
shouldBlockContextArtifactSave,
|
|
23
23
|
clearDiscussionFlowState,
|
|
24
24
|
resetWriteGateState,
|
|
25
25
|
} from '../bootstrap/write-gate.ts';
|
|
@@ -53,26 +53,27 @@ test('write-gate: blocks CONTEXT.md write during discussion without depth verifi
|
|
|
53
53
|
// ─── Scenario 3: Allows CONTEXT.md write after depth verification ──
|
|
54
54
|
|
|
55
55
|
test('write-gate: allows CONTEXT.md write after depth verification', () => {
|
|
56
|
+
clearDiscussionFlowState();
|
|
57
|
+
markDepthVerified('M001');
|
|
56
58
|
const result = shouldBlockContextWrite(
|
|
57
59
|
'write',
|
|
58
60
|
'/Users/dev/project/.gsd/milestones/M001/M001-CONTEXT.md',
|
|
59
61
|
'M001',
|
|
60
|
-
true,
|
|
61
62
|
);
|
|
62
63
|
assert.strictEqual(result.block, false, 'should not block after depth verification');
|
|
63
64
|
assert.strictEqual(result.reason, undefined, 'should have no reason');
|
|
65
|
+
clearDiscussionFlowState();
|
|
64
66
|
});
|
|
65
67
|
|
|
66
|
-
// ─── Scenario 4:
|
|
68
|
+
// ─── Scenario 4: Ambiguous session context no longer bypasses the gate ──
|
|
67
69
|
|
|
68
|
-
test('write-gate:
|
|
70
|
+
test('write-gate: blocks CONTEXT.md write when milestoneId is ambiguous', () => {
|
|
69
71
|
const result = shouldBlockContextWrite(
|
|
70
72
|
'write',
|
|
71
73
|
'.gsd/milestones/M001/M001-CONTEXT.md',
|
|
72
74
|
null,
|
|
73
|
-
false,
|
|
74
75
|
);
|
|
75
|
-
assert.strictEqual(result.block,
|
|
76
|
+
assert.strictEqual(result.block, true, 'should block when milestone context is ambiguous');
|
|
76
77
|
});
|
|
77
78
|
|
|
78
79
|
// ─── Scenario 5: Allows non-CONTEXT.md writes during discussion ──
|
|
@@ -83,7 +84,6 @@ test('write-gate: allows non-CONTEXT.md writes during discussion', () => {
|
|
|
83
84
|
'write',
|
|
84
85
|
'.gsd/milestones/M001/M001-DISCUSSION.md',
|
|
85
86
|
'M001',
|
|
86
|
-
false,
|
|
87
87
|
);
|
|
88
88
|
assert.strictEqual(r1.block, false, 'DISCUSSION.md should pass');
|
|
89
89
|
|
|
@@ -92,7 +92,6 @@ test('write-gate: allows non-CONTEXT.md writes during discussion', () => {
|
|
|
92
92
|
'write',
|
|
93
93
|
'.gsd/milestones/M001/slices/S01/S01-PLAN.md',
|
|
94
94
|
'M001',
|
|
95
|
-
false,
|
|
96
95
|
);
|
|
97
96
|
assert.strictEqual(r2.block, false, 'slice plan should pass');
|
|
98
97
|
|
|
@@ -101,7 +100,6 @@ test('write-gate: allows non-CONTEXT.md writes during discussion', () => {
|
|
|
101
100
|
'write',
|
|
102
101
|
'src/index.ts',
|
|
103
102
|
'M001',
|
|
104
|
-
false,
|
|
105
103
|
);
|
|
106
104
|
assert.strictEqual(r3.block, false, 'regular code file should pass');
|
|
107
105
|
});
|
|
@@ -113,7 +111,6 @@ test('write-gate: regex does not match slice context files (S01-CONTEXT.md)', ()
|
|
|
113
111
|
'write',
|
|
114
112
|
'.gsd/milestones/M001/slices/S01/S01-CONTEXT.md',
|
|
115
113
|
'M001',
|
|
116
|
-
false,
|
|
117
114
|
);
|
|
118
115
|
assert.strictEqual(result.block, false, 'S01-CONTEXT.md should not be blocked');
|
|
119
116
|
});
|
|
@@ -125,7 +122,6 @@ test('write-gate: blocked reason contains depth_verification keyword and anti-by
|
|
|
125
122
|
'write',
|
|
126
123
|
'.gsd/milestones/M999/M999-CONTEXT.md',
|
|
127
124
|
'M999',
|
|
128
|
-
false,
|
|
129
125
|
);
|
|
130
126
|
assert.strictEqual(result.block, true);
|
|
131
127
|
assert.ok(result.reason!.includes('depth_verification'), 'reason should mention depth_verification question id');
|
|
@@ -141,7 +137,6 @@ test('write-gate: blocks CONTEXT.md write in queue mode without depth verificati
|
|
|
141
137
|
'write',
|
|
142
138
|
'.gsd/milestones/M001/M001-CONTEXT.md',
|
|
143
139
|
null, // no milestoneId in queue mode
|
|
144
|
-
false, // not depth-verified
|
|
145
140
|
true, // queue phase active
|
|
146
141
|
);
|
|
147
142
|
assert.strictEqual(result.block, true, 'should block in queue mode without depth verification');
|
|
@@ -151,48 +146,228 @@ test('write-gate: blocks CONTEXT.md write in queue mode without depth verificati
|
|
|
151
146
|
// ─── Scenario 9: Queue mode allows CONTEXT.md write after depth verification ──
|
|
152
147
|
|
|
153
148
|
test('write-gate: allows CONTEXT.md write in queue mode after depth verification', () => {
|
|
149
|
+
clearDiscussionFlowState();
|
|
150
|
+
markDepthVerified('M001');
|
|
154
151
|
const result = shouldBlockContextWrite(
|
|
155
152
|
'write',
|
|
156
153
|
'.gsd/milestones/M001/M001-CONTEXT.md',
|
|
157
154
|
null, // no milestoneId in queue mode
|
|
158
|
-
true, // depth-verified
|
|
159
155
|
true, // queue phase active
|
|
160
156
|
);
|
|
161
157
|
assert.strictEqual(result.block, false, 'should not block in queue mode after depth verification');
|
|
158
|
+
clearDiscussionFlowState();
|
|
162
159
|
});
|
|
163
160
|
|
|
164
|
-
// ─── Scenario 10:
|
|
165
|
-
// This is the core regression for #1812: in queue mode, the tool_result handler
|
|
166
|
-
// must call markDepthVerified() even when getDiscussionMilestoneId() is null.
|
|
161
|
+
// ─── Scenario 10: depth verification is scoped per milestone, not global ──
|
|
167
162
|
|
|
168
|
-
test('write-gate: markDepthVerified
|
|
163
|
+
test('write-gate: markDepthVerified unlocks only the matching milestone', () => {
|
|
169
164
|
clearDiscussionFlowState();
|
|
170
|
-
|
|
165
|
+
markDepthVerified('M001');
|
|
171
166
|
|
|
172
|
-
|
|
173
|
-
const blocked = shouldBlockContextWrite(
|
|
167
|
+
const allowed = shouldBlockContextWrite(
|
|
174
168
|
'write',
|
|
175
169
|
'.gsd/milestones/M001/M001-CONTEXT.md',
|
|
176
170
|
null,
|
|
177
|
-
isDepthVerified(),
|
|
178
|
-
isQueuePhaseActive(),
|
|
179
171
|
);
|
|
180
|
-
assert.strictEqual(
|
|
172
|
+
assert.strictEqual(allowed.block, false, 'should allow the verified milestone');
|
|
181
173
|
|
|
182
|
-
|
|
183
|
-
markDepthVerified();
|
|
184
|
-
|
|
185
|
-
// After marking: should pass
|
|
186
|
-
const allowed = shouldBlockContextWrite(
|
|
174
|
+
const blockedOther = shouldBlockContextWrite(
|
|
187
175
|
'write',
|
|
188
|
-
'.gsd/milestones/
|
|
176
|
+
'.gsd/milestones/M002/M002-CONTEXT.md',
|
|
189
177
|
null,
|
|
190
|
-
isDepthVerified(),
|
|
191
|
-
isQueuePhaseActive(),
|
|
192
178
|
);
|
|
193
|
-
assert.strictEqual(
|
|
179
|
+
assert.strictEqual(blockedOther.block, true, 'other milestones should remain blocked');
|
|
180
|
+
assert.strictEqual(isMilestoneDepthVerified('M001'), true);
|
|
181
|
+
assert.strictEqual(isMilestoneDepthVerified('M002'), false);
|
|
182
|
+
|
|
183
|
+
clearDiscussionFlowState();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ─── Scenario 11: gsd_summary_save CONTEXT contract is milestone-scoped ──
|
|
187
|
+
|
|
188
|
+
test('write-gate: gsd_summary_save only blocks final milestone CONTEXT writes', () => {
|
|
189
|
+
clearDiscussionFlowState();
|
|
190
|
+
|
|
191
|
+
assert.strictEqual(
|
|
192
|
+
shouldBlockContextArtifactSave('CONTEXT-DRAFT', 'M001').block,
|
|
193
|
+
false,
|
|
194
|
+
'draft CONTEXT should be allowed',
|
|
195
|
+
);
|
|
196
|
+
assert.strictEqual(
|
|
197
|
+
shouldBlockContextArtifactSave('CONTEXT', 'M001', 'S01').block,
|
|
198
|
+
false,
|
|
199
|
+
'slice CONTEXT should be allowed',
|
|
200
|
+
);
|
|
201
|
+
assert.strictEqual(
|
|
202
|
+
shouldBlockContextArtifactSave('CONTEXT', 'M001').block,
|
|
203
|
+
true,
|
|
204
|
+
'final milestone CONTEXT should block before verification',
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
markDepthVerified('M001');
|
|
208
|
+
assert.strictEqual(
|
|
209
|
+
shouldBlockContextArtifactSave('CONTEXT', 'M001').block,
|
|
210
|
+
false,
|
|
211
|
+
'final milestone CONTEXT should pass after verification',
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
clearDiscussionFlowState();
|
|
215
|
+
});
|
|
194
216
|
|
|
217
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
218
|
+
// Discussion gate enforcement tests (pending gate mechanism)
|
|
219
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
220
|
+
|
|
221
|
+
import {
|
|
222
|
+
isGateQuestionId,
|
|
223
|
+
shouldBlockPendingGate,
|
|
224
|
+
shouldBlockPendingGateBash,
|
|
225
|
+
setPendingGate,
|
|
226
|
+
clearPendingGate,
|
|
227
|
+
getPendingGate,
|
|
228
|
+
} from '../bootstrap/write-gate.ts';
|
|
229
|
+
|
|
230
|
+
// ─── Scenario 19: isGateQuestionId recognizes all gate patterns ──
|
|
231
|
+
|
|
232
|
+
test('write-gate: isGateQuestionId recognizes all gate patterns', () => {
|
|
233
|
+
assert.strictEqual(isGateQuestionId('layer1_scope_gate'), true);
|
|
234
|
+
assert.strictEqual(isGateQuestionId('layer2_architecture_gate'), true);
|
|
235
|
+
assert.strictEqual(isGateQuestionId('layer3_error_gate'), true);
|
|
236
|
+
assert.strictEqual(isGateQuestionId('layer4_quality_gate'), true);
|
|
237
|
+
assert.strictEqual(isGateQuestionId('depth_verification'), true);
|
|
238
|
+
assert.strictEqual(isGateQuestionId('depth_verification_M002'), true);
|
|
239
|
+
assert.strictEqual(isGateQuestionId('my_layer1_scope_gate_question'), true);
|
|
240
|
+
// Non-gate question IDs
|
|
241
|
+
assert.strictEqual(isGateQuestionId('project_intent'), false);
|
|
242
|
+
assert.strictEqual(isGateQuestionId('feature_priority'), false);
|
|
243
|
+
assert.strictEqual(isGateQuestionId(''), false);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// ─── Scenario 20: setPendingGate / getPendingGate / clearPendingGate lifecycle ──
|
|
247
|
+
|
|
248
|
+
test('write-gate: pending gate lifecycle (set, get, clear)', () => {
|
|
195
249
|
clearDiscussionFlowState();
|
|
250
|
+
assert.strictEqual(getPendingGate(), null, 'starts null');
|
|
251
|
+
|
|
252
|
+
setPendingGate('layer1_scope_gate');
|
|
253
|
+
assert.strictEqual(getPendingGate(), 'layer1_scope_gate', 'set correctly');
|
|
254
|
+
|
|
255
|
+
clearPendingGate();
|
|
256
|
+
assert.strictEqual(getPendingGate(), null, 'cleared correctly');
|
|
257
|
+
|
|
258
|
+
// clearDiscussionFlowState also clears pending gate
|
|
259
|
+
setPendingGate('layer2_architecture_gate');
|
|
260
|
+
clearDiscussionFlowState();
|
|
261
|
+
assert.strictEqual(getPendingGate(), null, 'clearDiscussionFlowState clears pending gate');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// ─── Scenario 21: shouldBlockPendingGate blocks non-safe tools when gate is pending ──
|
|
265
|
+
|
|
266
|
+
test('write-gate: shouldBlockPendingGate blocks write/edit during pending gate', () => {
|
|
267
|
+
clearDiscussionFlowState();
|
|
268
|
+
setPendingGate('layer1_scope_gate');
|
|
269
|
+
|
|
270
|
+
// write should be blocked during discussion
|
|
271
|
+
const writeResult = shouldBlockPendingGate('write', 'M001', false);
|
|
272
|
+
assert.strictEqual(writeResult.block, true, 'write should be blocked');
|
|
273
|
+
assert.ok(writeResult.reason!.includes('layer1_scope_gate'), 'reason mentions the gate');
|
|
274
|
+
|
|
275
|
+
// edit should be blocked
|
|
276
|
+
const editResult = shouldBlockPendingGate('edit', 'M001', false);
|
|
277
|
+
assert.strictEqual(editResult.block, true, 'edit should be blocked');
|
|
278
|
+
|
|
279
|
+
// gsd tools should be blocked
|
|
280
|
+
const gsdResult = shouldBlockPendingGate('gsd_plan_milestone', 'M001', false);
|
|
281
|
+
assert.strictEqual(gsdResult.block, true, 'gsd tools should be blocked');
|
|
282
|
+
|
|
283
|
+
clearDiscussionFlowState();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// ─── Scenario 22: shouldBlockPendingGate allows safe tools when gate is pending ──
|
|
287
|
+
|
|
288
|
+
test('write-gate: shouldBlockPendingGate allows read-only and ask_user_questions during pending gate', () => {
|
|
289
|
+
clearDiscussionFlowState();
|
|
290
|
+
setPendingGate('layer1_scope_gate');
|
|
291
|
+
|
|
292
|
+
// ask_user_questions is always safe (model needs to re-ask)
|
|
293
|
+
assert.strictEqual(shouldBlockPendingGate('ask_user_questions', 'M001').block, false);
|
|
294
|
+
// read-only tools are safe
|
|
295
|
+
assert.strictEqual(shouldBlockPendingGate('read', 'M001').block, false);
|
|
296
|
+
assert.strictEqual(shouldBlockPendingGate('grep', 'M001').block, false);
|
|
297
|
+
assert.strictEqual(shouldBlockPendingGate('glob', 'M001').block, false);
|
|
298
|
+
assert.strictEqual(shouldBlockPendingGate('ls', 'M001').block, false);
|
|
299
|
+
|
|
300
|
+
clearDiscussionFlowState();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// ─── Scenario 23: shouldBlockPendingGate still blocks when the session is ambiguous ──
|
|
304
|
+
|
|
305
|
+
test('write-gate: shouldBlockPendingGate blocks outside discussion when a gate is pending', () => {
|
|
306
|
+
clearDiscussionFlowState();
|
|
307
|
+
setPendingGate('layer1_scope_gate');
|
|
308
|
+
|
|
309
|
+
// No milestoneId and no queue phase — still block because the gate is pending
|
|
310
|
+
const result = shouldBlockPendingGate('write', null, false);
|
|
311
|
+
assert.strictEqual(result.block, true, 'should block even when milestoneId is null');
|
|
312
|
+
|
|
313
|
+
clearDiscussionFlowState();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// ─── Scenario 24: shouldBlockPendingGate blocks in queue mode ──
|
|
317
|
+
|
|
318
|
+
test('write-gate: shouldBlockPendingGate blocks in queue mode when gate is pending', () => {
|
|
319
|
+
clearDiscussionFlowState();
|
|
320
|
+
setQueuePhaseActive(true);
|
|
321
|
+
setPendingGate('depth_verification');
|
|
322
|
+
|
|
323
|
+
const result = shouldBlockPendingGate('write', null, true);
|
|
324
|
+
assert.strictEqual(result.block, true, 'should block in queue mode');
|
|
325
|
+
|
|
326
|
+
clearDiscussionFlowState();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// ─── Scenario 25: shouldBlockPendingGateBash allows read-only commands ──
|
|
330
|
+
|
|
331
|
+
test('write-gate: shouldBlockPendingGateBash allows read-only commands during pending gate', () => {
|
|
332
|
+
clearDiscussionFlowState();
|
|
333
|
+
setPendingGate('layer2_architecture_gate');
|
|
334
|
+
|
|
335
|
+
assert.strictEqual(shouldBlockPendingGateBash('cat file.txt', 'M001').block, false);
|
|
336
|
+
assert.strictEqual(shouldBlockPendingGateBash('git log --oneline', 'M001').block, false);
|
|
337
|
+
assert.strictEqual(shouldBlockPendingGateBash('grep -r pattern .', 'M001').block, false);
|
|
338
|
+
assert.strictEqual(shouldBlockPendingGateBash('ls -la', 'M001').block, false);
|
|
339
|
+
|
|
340
|
+
clearDiscussionFlowState();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// ─── Scenario 26: shouldBlockPendingGateBash blocks mutating commands ──
|
|
344
|
+
|
|
345
|
+
test('write-gate: shouldBlockPendingGateBash blocks mutating commands during pending gate', () => {
|
|
346
|
+
clearDiscussionFlowState();
|
|
347
|
+
setPendingGate('layer2_architecture_gate');
|
|
348
|
+
|
|
349
|
+
const result = shouldBlockPendingGateBash('npm run build', 'M001');
|
|
350
|
+
assert.strictEqual(result.block, true, 'mutating bash should be blocked');
|
|
351
|
+
assert.ok(result.reason!.includes('layer2_architecture_gate'));
|
|
352
|
+
|
|
353
|
+
clearDiscussionFlowState();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// ─── Scenario 27: no pending gate means no blocking ──
|
|
357
|
+
|
|
358
|
+
test('write-gate: no pending gate means no blocking', () => {
|
|
359
|
+
clearDiscussionFlowState();
|
|
360
|
+
|
|
361
|
+
assert.strictEqual(shouldBlockPendingGate('write', 'M001').block, false);
|
|
362
|
+
assert.strictEqual(shouldBlockPendingGateBash('npm run build', 'M001').block, false);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// ─── Scenario 28: resetWriteGateState clears pending gate ──
|
|
366
|
+
|
|
367
|
+
test('write-gate: resetWriteGateState clears pending gate', () => {
|
|
368
|
+
setPendingGate('layer3_error_gate');
|
|
369
|
+
resetWriteGateState();
|
|
370
|
+
assert.strictEqual(getPendingGate(), null);
|
|
196
371
|
});
|
|
197
372
|
|
|
198
373
|
// ─── Standard options fixture used across depth confirmation tests ──
|
|
@@ -34,6 +34,24 @@ const TAB_LABELS = [
|
|
|
34
34
|
"0 Export",
|
|
35
35
|
];
|
|
36
36
|
|
|
37
|
+
type TabBarEntry = { label: string; width: number };
|
|
38
|
+
|
|
39
|
+
function buildTabBarEntries(activeTab: number, filterText: string, capturesPendingCount?: number): TabBarEntry[] {
|
|
40
|
+
return TAB_LABELS.map((label, i) => {
|
|
41
|
+
let displayLabel = label;
|
|
42
|
+
if (i === activeTab && filterText) {
|
|
43
|
+
displayLabel += " \u2731";
|
|
44
|
+
}
|
|
45
|
+
if (i === 8 && capturesPendingCount) {
|
|
46
|
+
displayLabel += ` (${capturesPendingCount})`;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
label: displayLabel,
|
|
50
|
+
width: visibleWidth(displayLabel) + 2,
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
37
55
|
export class GSDVisualizerOverlay {
|
|
38
56
|
private tui: { requestRender: () => void };
|
|
39
57
|
private theme: Theme;
|
|
@@ -116,15 +134,14 @@ export class GSDVisualizerOverlay {
|
|
|
116
134
|
}
|
|
117
135
|
|
|
118
136
|
handleInput(data: string): void {
|
|
137
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
|
|
138
|
+
this.dispose();
|
|
139
|
+
this.onClose();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
119
143
|
// Filter mode input routing
|
|
120
144
|
if (this.filterMode) {
|
|
121
|
-
if (matchesKey(data, Key.escape)) {
|
|
122
|
-
this.filterMode = false;
|
|
123
|
-
this.filterText = "";
|
|
124
|
-
this.invalidate();
|
|
125
|
-
this.tui.requestRender();
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
145
|
if (matchesKey(data, Key.enter)) {
|
|
129
146
|
this.filterMode = false;
|
|
130
147
|
this.invalidate();
|
|
@@ -179,8 +196,9 @@ export class GSDVisualizerOverlay {
|
|
|
179
196
|
// Left click — check if on tab bar row
|
|
180
197
|
if (mouse.y === 2) {
|
|
181
198
|
let xPos = 3;
|
|
182
|
-
|
|
183
|
-
|
|
199
|
+
const tabs = buildTabBarEntries(this.activeTab, this.filterText, this.data?.captures?.pendingCount);
|
|
200
|
+
for (let i = 0; i < tabs.length; i++) {
|
|
201
|
+
const tabWidth = tabs[i]!.width;
|
|
184
202
|
if (mouse.x >= xPos && mouse.x < xPos + tabWidth) {
|
|
185
203
|
this.activeTab = i;
|
|
186
204
|
this.invalidate();
|
|
@@ -194,12 +212,6 @@ export class GSDVisualizerOverlay {
|
|
|
194
212
|
return;
|
|
195
213
|
}
|
|
196
214
|
|
|
197
|
-
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
|
|
198
|
-
this.dispose();
|
|
199
|
-
this.onClose();
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
215
|
if (matchesKey(data, Key.shift("tab"))) {
|
|
204
216
|
this.activeTab = (this.activeTab - 1 + TAB_COUNT) % TAB_COUNT;
|
|
205
217
|
this.invalidate();
|
|
@@ -442,20 +454,12 @@ export class GSDVisualizerOverlay {
|
|
|
442
454
|
const content: string[] = [];
|
|
443
455
|
|
|
444
456
|
// Tab bar
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
// Show filter indicator on active tab with filter
|
|
448
|
-
if (i === this.activeTab && this.filterText) {
|
|
449
|
-
displayLabel += " \u2731";
|
|
450
|
-
}
|
|
451
|
-
// Show captures badge
|
|
452
|
-
if (i === 8 && this.data?.captures?.pendingCount) {
|
|
453
|
-
displayLabel += ` (${this.data.captures.pendingCount})`;
|
|
454
|
-
}
|
|
457
|
+
const tabEntries = buildTabBarEntries(this.activeTab, this.filterText, this.data?.captures?.pendingCount);
|
|
458
|
+
const tabs = tabEntries.map((entry, i) => {
|
|
455
459
|
if (i === this.activeTab) {
|
|
456
|
-
return th.fg("accent", `[${
|
|
460
|
+
return th.fg("accent", `[${entry.label}]`);
|
|
457
461
|
}
|
|
458
|
-
return th.fg("dim", `[${
|
|
462
|
+
return th.fg("dim", `[${entry.label}]`);
|
|
459
463
|
});
|
|
460
464
|
content.push(" " + tabs.join(" "));
|
|
461
465
|
content.push("");
|