gsd-pi 2.78.1-dev.e9d88a536 → 2.78.1-dev.eccf86e27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -7
- package/dist/help-text.js +1 -1
- package/dist/resource-loader.js +6 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/detect-stuck.js +41 -5
- package/dist/resources/extensions/gsd/auto/loop.js +235 -36
- package/dist/resources/extensions/gsd/auto/phases.js +14 -7
- package/dist/resources/extensions/gsd/auto/session.js +36 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +49 -4
- package/dist/resources/extensions/gsd/auto-post-unit.js +26 -12
- package/dist/resources/extensions/gsd/auto-worktree.js +185 -201
- package/dist/resources/extensions/gsd/auto.js +139 -49
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +26 -20
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
- package/dist/resources/extensions/gsd/crash-recovery.js +160 -47
- package/dist/resources/extensions/gsd/db/auto-workers.js +227 -0
- package/dist/resources/extensions/gsd/db/command-queue.js +105 -0
- package/dist/resources/extensions/gsd/db/milestone-leases.js +210 -0
- package/dist/resources/extensions/gsd/db/runtime-kv.js +91 -0
- package/dist/resources/extensions/gsd/db/unit-dispatches.js +322 -0
- package/dist/resources/extensions/gsd/db-writer.js +96 -16
- package/dist/resources/extensions/gsd/delegation-policy.js +155 -0
- package/dist/resources/extensions/gsd/docs/COORDINATION.md +42 -0
- package/dist/resources/extensions/gsd/doctor-proactive.js +4 -0
- package/dist/resources/extensions/gsd/doctor-runtime-checks.js +22 -6
- package/dist/resources/extensions/gsd/doctor.js +12 -2
- package/dist/resources/extensions/gsd/gsd-db.js +355 -3
- package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
- package/dist/resources/extensions/gsd/guided-flow.js +116 -26
- package/dist/resources/extensions/gsd/interrupted-session.js +18 -15
- package/dist/resources/extensions/gsd/metrics.js +287 -1
- package/dist/resources/extensions/gsd/paths.js +79 -8
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +4 -4
- package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -3
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
- package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
- package/dist/resources/extensions/gsd/state.js +21 -6
- package/dist/resources/extensions/gsd/templates/project.md +10 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
- package/dist/resources/extensions/gsd/workspace.js +59 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +79 -2
- package/dist/resources/extensions/gsd/write-intercept.js +3 -3
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
- 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 +1 -1
- 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 +14 -14
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- 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/mcp-server/README.md +2 -11
- package/packages/mcp-server/dist/remote-questions.d.ts +27 -0
- package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
- package/packages/mcp-server/dist/remote-questions.js +28 -0
- package/packages/mcp-server/dist/remote-questions.js.map +1 -1
- package/packages/mcp-server/dist/server.d.ts +28 -0
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +94 -4
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/mcp-server.test.ts +226 -0
- package/packages/mcp-server/src/remote-questions.test.ts +103 -0
- package/packages/mcp-server/src/remote-questions.ts +35 -0
- package/packages/mcp-server/src/server.ts +129 -6
- package/packages/mcp-server/src/workflow-tools.ts +1 -1
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto/detect-stuck.ts +37 -5
- package/src/resources/extensions/gsd/auto/loop.ts +263 -41
- package/src/resources/extensions/gsd/auto/phases.ts +15 -7
- package/src/resources/extensions/gsd/auto/session.ts +40 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +63 -4
- package/src/resources/extensions/gsd/auto-post-unit.ts +27 -12
- package/src/resources/extensions/gsd/auto-worktree.ts +218 -225
- package/src/resources/extensions/gsd/auto.ts +166 -43
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +26 -21
- package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +103 -0
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +80 -55
- package/src/resources/extensions/gsd/crash-recovery.ts +177 -43
- package/src/resources/extensions/gsd/db/auto-workers.ts +273 -0
- package/src/resources/extensions/gsd/db/command-queue.ts +149 -0
- package/src/resources/extensions/gsd/db/milestone-leases.ts +274 -0
- package/src/resources/extensions/gsd/db/runtime-kv.ts +127 -0
- package/src/resources/extensions/gsd/db/unit-dispatches.ts +446 -0
- package/src/resources/extensions/gsd/db-writer.ts +113 -17
- package/src/resources/extensions/gsd/delegation-policy.ts +197 -0
- package/src/resources/extensions/gsd/docs/COORDINATION.md +42 -0
- package/src/resources/extensions/gsd/doctor-proactive.ts +4 -0
- package/src/resources/extensions/gsd/doctor-runtime-checks.ts +24 -6
- package/src/resources/extensions/gsd/doctor.ts +10 -2
- package/src/resources/extensions/gsd/gsd-db.ts +354 -3
- package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
- package/src/resources/extensions/gsd/guided-flow.ts +152 -26
- package/src/resources/extensions/gsd/interrupted-session.ts +19 -12
- package/src/resources/extensions/gsd/metrics.ts +321 -1
- package/src/resources/extensions/gsd/paths.ts +67 -8
- package/src/resources/extensions/gsd/prompts/complete-slice.md +4 -4
- package/src/resources/extensions/gsd/prompts/execute-task.md +3 -3
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
- package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
- package/src/resources/extensions/gsd/state.ts +44 -6
- package/src/resources/extensions/gsd/templates/project.md +10 -0
- package/src/resources/extensions/gsd/tests/auto-discuss-milestone-deadlock-4973.test.ts +14 -14
- package/src/resources/extensions/gsd/tests/auto-loop-no-copy-artifacts.test.ts +72 -0
- package/src/resources/extensions/gsd/tests/auto-loop-symlink-worktree.test.ts +190 -0
- package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +331 -0
- package/src/resources/extensions/gsd/tests/auto-workers.test.ts +105 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -0
- package/src/resources/extensions/gsd/tests/command-queue.test.ts +141 -0
- package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +203 -0
- package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +169 -59
- package/src/resources/extensions/gsd/tests/db-writer-path-containment.test.ts +152 -0
- package/src/resources/extensions/gsd/tests/db-writer-root-artifact.test.ts +221 -0
- package/src/resources/extensions/gsd/tests/db-writer-scope.test.ts +230 -0
- package/src/resources/extensions/gsd/tests/delegation-policy.test.ts +151 -0
- package/src/resources/extensions/gsd/tests/detect-stuck-respects-retry.test.ts +173 -0
- package/src/resources/extensions/gsd/tests/dispatch-backgroundable-annotation.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +3 -23
- package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +193 -0
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +246 -0
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +218 -0
- package/src/resources/extensions/gsd/tests/gsd-db-failed-open-restore.test.ts +117 -0
- package/src/resources/extensions/gsd/tests/gsd-db-workspace-scope.test.ts +226 -0
- package/src/resources/extensions/gsd/tests/gsd-root-canonical.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/gsd-root-home-guard.test.ts +68 -5
- package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +22 -12
- package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +24 -10
- package/src/resources/extensions/gsd/tests/integration/doctor-runtime.test.ts +35 -23
- package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +369 -0
- package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +72 -25
- package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +72 -25
- package/src/resources/extensions/gsd/tests/memory-pressure-stuck-state.test.ts +9 -6
- package/src/resources/extensions/gsd/tests/metrics-atomic-merge.test.ts +222 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-hardening.test.ts +400 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-not-acquired.test.ts +141 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-retry-sleep.test.ts +287 -0
- package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +149 -0
- package/src/resources/extensions/gsd/tests/metrics-scope.test.ts +378 -0
- package/src/resources/extensions/gsd/tests/milestone-leases.test.ts +152 -0
- package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +329 -0
- package/src/resources/extensions/gsd/tests/parallel-milestone-isolation.test.ts +106 -0
- package/src/resources/extensions/gsd/tests/path-cache-decoupled.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/path-normalization-unified.test.ts +175 -0
- package/src/resources/extensions/gsd/tests/paths-cache.test.ts +170 -0
- package/src/resources/extensions/gsd/tests/paused-session-via-db.test.ts +119 -0
- package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +3 -17
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +150 -7
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +138 -16
- package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/runtime-kv.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/skipped-validation-completion.test.ts +133 -28
- package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +17 -0
- package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +134 -0
- package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +434 -0
- package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +162 -0
- package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +186 -0
- package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +247 -0
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +41 -1
- package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +239 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +9 -15
- package/src/resources/extensions/gsd/tests/workspace.test.ts +196 -0
- package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +94 -71
- package/src/resources/extensions/gsd/tests/write-intercept.test.ts +1 -1
- package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
- package/src/resources/extensions/gsd/workspace.ts +95 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +78 -2
- package/src/resources/extensions/gsd/write-intercept.ts +3 -3
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +0 -213
- package/src/resources/extensions/gsd/tests/auto-stale-lock-self-kill.test.ts +0 -87
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +0 -159
- /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_ssgManifest.js +0 -0
|
@@ -24,38 +24,38 @@ import {
|
|
|
24
24
|
// ─── shouldBlockQueueExecution ────────────────────────────────────────────
|
|
25
25
|
|
|
26
26
|
test('shouldBlockQueueExecution: queue inactive → allow write to user source', (t) => {
|
|
27
|
-
t.after(() => clearDiscussionFlowState());
|
|
28
|
-
setQueuePhaseActive(false);
|
|
27
|
+
t.after(() => clearDiscussionFlowState(process.cwd()));
|
|
28
|
+
setQueuePhaseActive(false, process.cwd());
|
|
29
29
|
const r = shouldBlockQueueExecution('write', 'src/main.ts', false);
|
|
30
30
|
assert.strictEqual(r.block, false);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
test('shouldBlockQueueExecution: queue active → block write to user source', (t) => {
|
|
34
|
-
t.after(() => clearDiscussionFlowState());
|
|
35
|
-
setQueuePhaseActive(true);
|
|
34
|
+
t.after(() => clearDiscussionFlowState(process.cwd()));
|
|
35
|
+
setQueuePhaseActive(true, process.cwd());
|
|
36
36
|
const r = shouldBlockQueueExecution('write', 'src/main.ts', true);
|
|
37
37
|
assert.strictEqual(r.block, true);
|
|
38
38
|
assert.ok(r.reason);
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
test('shouldBlockQueueExecution: queue active → allow write to .gsd/ path', (t) => {
|
|
42
|
-
t.after(() => clearDiscussionFlowState());
|
|
43
|
-
setQueuePhaseActive(true);
|
|
42
|
+
t.after(() => clearDiscussionFlowState(process.cwd()));
|
|
43
|
+
setQueuePhaseActive(true, process.cwd());
|
|
44
44
|
const r = shouldBlockQueueExecution('write', '.gsd/milestones/M001/M001-CONTEXT.md', true);
|
|
45
45
|
assert.strictEqual(r.block, false);
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
test('shouldBlockQueueExecution: queue active → block mutating bash', (t) => {
|
|
49
|
-
t.after(() => clearDiscussionFlowState());
|
|
50
|
-
setQueuePhaseActive(true);
|
|
49
|
+
t.after(() => clearDiscussionFlowState(process.cwd()));
|
|
50
|
+
setQueuePhaseActive(true, process.cwd());
|
|
51
51
|
const r = shouldBlockQueueExecution('bash', 'npm run build', true);
|
|
52
52
|
assert.strictEqual(r.block, true);
|
|
53
53
|
assert.ok(r.reason);
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
test('shouldBlockQueueExecution: queue active → allow read-only bash', (t) => {
|
|
57
|
-
t.after(() => clearDiscussionFlowState());
|
|
58
|
-
setQueuePhaseActive(true);
|
|
57
|
+
t.after(() => clearDiscussionFlowState(process.cwd()));
|
|
58
|
+
setQueuePhaseActive(true, process.cwd());
|
|
59
59
|
const r = shouldBlockQueueExecution('bash', 'git log --oneline -5', true);
|
|
60
60
|
assert.strictEqual(r.block, false);
|
|
61
61
|
});
|
|
@@ -63,30 +63,30 @@ test('shouldBlockQueueExecution: queue active → allow read-only bash', (t) =>
|
|
|
63
63
|
// ─── shouldBlockPendingGate ───────────────────────────────────────────────
|
|
64
64
|
|
|
65
65
|
test('shouldBlockPendingGate: no pending gate → allow any tool', (t) => {
|
|
66
|
-
t.after(() => clearDiscussionFlowState());
|
|
67
|
-
clearPendingGate();
|
|
66
|
+
t.after(() => clearDiscussionFlowState(process.cwd()));
|
|
67
|
+
clearPendingGate(process.cwd());
|
|
68
68
|
const r = shouldBlockPendingGate('write', 'M001');
|
|
69
69
|
assert.strictEqual(r.block, false);
|
|
70
70
|
});
|
|
71
71
|
|
|
72
72
|
test('shouldBlockPendingGate: pending gate → block write', (t) => {
|
|
73
|
-
t.after(() => clearDiscussionFlowState());
|
|
74
|
-
setPendingGate('depth_verification_M001');
|
|
73
|
+
t.after(() => clearDiscussionFlowState(process.cwd()));
|
|
74
|
+
setPendingGate('depth_verification_M001', process.cwd());
|
|
75
75
|
const r = shouldBlockPendingGate('write', 'M001');
|
|
76
76
|
assert.strictEqual(r.block, true);
|
|
77
77
|
assert.ok(r.reason?.includes('depth_verification_M001'));
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
test('shouldBlockPendingGate: pending gate → allow ask_user_questions', (t) => {
|
|
81
|
-
t.after(() => clearDiscussionFlowState());
|
|
82
|
-
setPendingGate('depth_verification_M001');
|
|
81
|
+
t.after(() => clearDiscussionFlowState(process.cwd()));
|
|
82
|
+
setPendingGate('depth_verification_M001', process.cwd());
|
|
83
83
|
const r = shouldBlockPendingGate('ask_user_questions', 'M001');
|
|
84
84
|
assert.strictEqual(r.block, false);
|
|
85
85
|
});
|
|
86
86
|
|
|
87
87
|
test('shouldBlockPendingGate: pending gate → block read so approval question stays visible', (t) => {
|
|
88
|
-
t.after(() => clearDiscussionFlowState());
|
|
89
|
-
setPendingGate('depth_verification_M001');
|
|
88
|
+
t.after(() => clearDiscussionFlowState(process.cwd()));
|
|
89
|
+
setPendingGate('depth_verification_M001', process.cwd());
|
|
90
90
|
const r = shouldBlockPendingGate('read', 'M001');
|
|
91
91
|
assert.strictEqual(r.block, true);
|
|
92
92
|
assert.ok(r.reason?.includes('already asked for user confirmation'));
|
|
@@ -95,31 +95,31 @@ test('shouldBlockPendingGate: pending gate → block read so approval question s
|
|
|
95
95
|
// ─── shouldBlockPendingGateBash ───────────────────────────────────────────
|
|
96
96
|
|
|
97
97
|
test('shouldBlockPendingGateBash: no pending gate → allow mutating bash', (t) => {
|
|
98
|
-
t.after(() => clearDiscussionFlowState());
|
|
99
|
-
clearPendingGate();
|
|
98
|
+
t.after(() => clearDiscussionFlowState(process.cwd()));
|
|
99
|
+
clearPendingGate(process.cwd());
|
|
100
100
|
const r = shouldBlockPendingGateBash('npm run build', 'M001');
|
|
101
101
|
assert.strictEqual(r.block, false);
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
test('shouldBlockPendingGateBash: pending gate → block mutating bash', (t) => {
|
|
105
|
-
t.after(() => clearDiscussionFlowState());
|
|
106
|
-
setPendingGate('depth_verification_M001');
|
|
105
|
+
t.after(() => clearDiscussionFlowState(process.cwd()));
|
|
106
|
+
setPendingGate('depth_verification_M001', process.cwd());
|
|
107
107
|
const r = shouldBlockPendingGateBash('npm run build', 'M001');
|
|
108
108
|
assert.strictEqual(r.block, true);
|
|
109
109
|
assert.ok(r.reason?.includes('depth_verification_M001'));
|
|
110
110
|
});
|
|
111
111
|
|
|
112
112
|
test('shouldBlockPendingGateBash: pending gate → block read-only bash (cat)', (t) => {
|
|
113
|
-
t.after(() => clearDiscussionFlowState());
|
|
114
|
-
setPendingGate('depth_verification_M001');
|
|
113
|
+
t.after(() => clearDiscussionFlowState(process.cwd()));
|
|
114
|
+
setPendingGate('depth_verification_M001', process.cwd());
|
|
115
115
|
const r = shouldBlockPendingGateBash('cat README.md', 'M001');
|
|
116
116
|
assert.strictEqual(r.block, true);
|
|
117
117
|
assert.ok(r.reason?.includes('already asked for user confirmation'));
|
|
118
118
|
});
|
|
119
119
|
|
|
120
120
|
test('shouldBlockPendingGateBash: pending gate → block read-only bash (git log)', (t) => {
|
|
121
|
-
t.after(() => clearDiscussionFlowState());
|
|
122
|
-
setPendingGate('depth_verification_M001');
|
|
121
|
+
t.after(() => clearDiscussionFlowState(process.cwd()));
|
|
122
|
+
setPendingGate('depth_verification_M001', process.cwd());
|
|
123
123
|
const r = shouldBlockPendingGateBash('git log --oneline -10', 'M001');
|
|
124
124
|
assert.strictEqual(r.block, true);
|
|
125
125
|
});
|
|
@@ -127,26 +127,26 @@ test('shouldBlockPendingGateBash: pending gate → block read-only bash (git log
|
|
|
127
127
|
// ─── shouldBlockContextWrite ──────────────────────────────────────────────
|
|
128
128
|
|
|
129
129
|
test('shouldBlockContextWrite: non-write tool → allow', (t) => {
|
|
130
|
-
t.after(() => clearDiscussionFlowState());
|
|
130
|
+
t.after(() => clearDiscussionFlowState(process.cwd()));
|
|
131
131
|
const r = shouldBlockContextWrite('read', '.gsd/milestones/M001/M001-CONTEXT.md', 'M001');
|
|
132
132
|
assert.strictEqual(r.block, false);
|
|
133
133
|
});
|
|
134
134
|
|
|
135
135
|
test('shouldBlockContextWrite: write to non-CONTEXT file → allow', (t) => {
|
|
136
|
-
t.after(() => clearDiscussionFlowState());
|
|
136
|
+
t.after(() => clearDiscussionFlowState(process.cwd()));
|
|
137
137
|
const r = shouldBlockContextWrite('write', 'src/index.ts', 'M001');
|
|
138
138
|
assert.strictEqual(r.block, false);
|
|
139
139
|
});
|
|
140
140
|
|
|
141
141
|
test('shouldBlockContextWrite: write to CONTEXT.md without verification → block', (t) => {
|
|
142
|
-
t.after(() => clearDiscussionFlowState());
|
|
142
|
+
t.after(() => clearDiscussionFlowState(process.cwd()));
|
|
143
143
|
const r = shouldBlockContextWrite('write', '.gsd/milestones/M007/M007-CONTEXT.md', 'M007');
|
|
144
144
|
assert.strictEqual(r.block, true);
|
|
145
145
|
assert.ok(r.reason);
|
|
146
146
|
});
|
|
147
147
|
|
|
148
148
|
test('shouldBlockContextWrite: write to CONTEXT.md after verification → allow', (t) => {
|
|
149
|
-
t.after(() => clearDiscussionFlowState());
|
|
149
|
+
t.after(() => clearDiscussionFlowState(process.cwd()));
|
|
150
150
|
markDepthVerified('M008');
|
|
151
151
|
const r = shouldBlockContextWrite('write', '.gsd/milestones/M008/M008-CONTEXT.md', 'M008');
|
|
152
152
|
assert.strictEqual(r.block, false);
|
|
@@ -155,33 +155,33 @@ test('shouldBlockContextWrite: write to CONTEXT.md after verification → allow'
|
|
|
155
155
|
// ─── shouldBlockContextArtifactSave ───────────────────────────────────────
|
|
156
156
|
|
|
157
157
|
test('shouldBlockContextArtifactSave: non-CONTEXT artifact type → allow', (t) => {
|
|
158
|
-
t.after(() => clearDiscussionFlowState());
|
|
158
|
+
t.after(() => clearDiscussionFlowState(process.cwd()));
|
|
159
159
|
const r = shouldBlockContextArtifactSave('CONTEXT-DRAFT', 'M001');
|
|
160
160
|
assert.strictEqual(r.block, false);
|
|
161
161
|
});
|
|
162
162
|
|
|
163
163
|
test('shouldBlockContextArtifactSave: slice-level CONTEXT → allow', (t) => {
|
|
164
|
-
t.after(() => clearDiscussionFlowState());
|
|
164
|
+
t.after(() => clearDiscussionFlowState(process.cwd()));
|
|
165
165
|
const r = shouldBlockContextArtifactSave('CONTEXT', 'M001', 'S01');
|
|
166
166
|
assert.strictEqual(r.block, false);
|
|
167
167
|
});
|
|
168
168
|
|
|
169
169
|
test('shouldBlockContextArtifactSave: milestone CONTEXT without verification → block', (t) => {
|
|
170
|
-
t.after(() => clearDiscussionFlowState());
|
|
170
|
+
t.after(() => clearDiscussionFlowState(process.cwd()));
|
|
171
171
|
const r = shouldBlockContextArtifactSave('CONTEXT', 'M009');
|
|
172
172
|
assert.strictEqual(r.block, true);
|
|
173
173
|
assert.ok(r.reason?.includes('M009'));
|
|
174
174
|
});
|
|
175
175
|
|
|
176
176
|
test('shouldBlockContextArtifactSave: milestone CONTEXT after verification → allow', (t) => {
|
|
177
|
-
t.after(() => clearDiscussionFlowState());
|
|
177
|
+
t.after(() => clearDiscussionFlowState(process.cwd()));
|
|
178
178
|
markDepthVerified('M010');
|
|
179
179
|
const r = shouldBlockContextArtifactSave('CONTEXT', 'M010');
|
|
180
180
|
assert.strictEqual(r.block, false);
|
|
181
181
|
});
|
|
182
182
|
|
|
183
183
|
test('shouldBlockContextArtifactSave: CONTEXT with no milestoneId → block', (t) => {
|
|
184
|
-
t.after(() => clearDiscussionFlowState());
|
|
184
|
+
t.after(() => clearDiscussionFlowState(process.cwd()));
|
|
185
185
|
const r = shouldBlockContextArtifactSave('CONTEXT', null);
|
|
186
186
|
assert.strictEqual(r.block, true);
|
|
187
187
|
assert.ok(r.reason);
|
|
@@ -9,14 +9,16 @@
|
|
|
9
9
|
* (e) else → block with actionable reason
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import test from 'node:test';
|
|
12
|
+
import test, { afterEach } from 'node:test';
|
|
13
13
|
import assert from 'node:assert/strict';
|
|
14
14
|
import { mkdirSync, writeFileSync, unlinkSync, existsSync, rmSync } from 'node:fs';
|
|
15
15
|
import { join } from 'node:path';
|
|
16
16
|
import { tmpdir } from 'node:os';
|
|
17
17
|
import { randomUUID } from 'node:crypto';
|
|
18
18
|
import {
|
|
19
|
+
isDepthVerified,
|
|
19
20
|
isDepthConfirmationAnswer,
|
|
21
|
+
isQueuePhaseActive,
|
|
20
22
|
shouldBlockContextWrite,
|
|
21
23
|
setQueuePhaseActive,
|
|
22
24
|
} from '../index.ts';
|
|
@@ -32,6 +34,10 @@ import {
|
|
|
32
34
|
loadWriteGateSnapshot,
|
|
33
35
|
} from '../bootstrap/write-gate.ts';
|
|
34
36
|
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
clearDiscussionFlowState(process.cwd());
|
|
39
|
+
});
|
|
40
|
+
|
|
35
41
|
// ─── Scenario 1: Blocks CONTEXT.md write during discussion without depth verification (absolute path) ──
|
|
36
42
|
|
|
37
43
|
test('write-gate: blocks CONTEXT.md write during discussion without depth verification (absolute path)', () => {
|
|
@@ -61,7 +67,7 @@ test('write-gate: blocks CONTEXT.md write during discussion without depth verifi
|
|
|
61
67
|
// ─── Scenario 3: Allows CONTEXT.md write after depth verification ──
|
|
62
68
|
|
|
63
69
|
test('write-gate: allows CONTEXT.md write after depth verification', () => {
|
|
64
|
-
clearDiscussionFlowState();
|
|
70
|
+
clearDiscussionFlowState(process.cwd());
|
|
65
71
|
markDepthVerified('M001');
|
|
66
72
|
const result = shouldBlockContextWrite(
|
|
67
73
|
'write',
|
|
@@ -70,7 +76,6 @@ test('write-gate: allows CONTEXT.md write after depth verification', () => {
|
|
|
70
76
|
);
|
|
71
77
|
assert.strictEqual(result.block, false, 'should not block after depth verification');
|
|
72
78
|
assert.strictEqual(result.reason, undefined, 'should have no reason');
|
|
73
|
-
clearDiscussionFlowState();
|
|
74
79
|
});
|
|
75
80
|
|
|
76
81
|
// ─── Scenario 4: Ambiguous session context no longer bypasses the gate ──
|
|
@@ -154,7 +159,7 @@ test('write-gate: blocks CONTEXT.md write in queue mode without depth verificati
|
|
|
154
159
|
// ─── Scenario 9: Queue mode allows CONTEXT.md write after depth verification ──
|
|
155
160
|
|
|
156
161
|
test('write-gate: allows CONTEXT.md write in queue mode after depth verification', () => {
|
|
157
|
-
clearDiscussionFlowState();
|
|
162
|
+
clearDiscussionFlowState(process.cwd());
|
|
158
163
|
markDepthVerified('M001');
|
|
159
164
|
const result = shouldBlockContextWrite(
|
|
160
165
|
'write',
|
|
@@ -163,13 +168,12 @@ test('write-gate: allows CONTEXT.md write in queue mode after depth verification
|
|
|
163
168
|
true, // queue phase active
|
|
164
169
|
);
|
|
165
170
|
assert.strictEqual(result.block, false, 'should not block in queue mode after depth verification');
|
|
166
|
-
clearDiscussionFlowState();
|
|
167
171
|
});
|
|
168
172
|
|
|
169
173
|
// ─── Scenario 10: depth verification is scoped per milestone, not global ──
|
|
170
174
|
|
|
171
175
|
test('write-gate: markDepthVerified unlocks only the matching milestone', () => {
|
|
172
|
-
clearDiscussionFlowState();
|
|
176
|
+
clearDiscussionFlowState(process.cwd());
|
|
173
177
|
markDepthVerified('M001');
|
|
174
178
|
|
|
175
179
|
const allowed = shouldBlockContextWrite(
|
|
@@ -187,14 +191,12 @@ test('write-gate: markDepthVerified unlocks only the matching milestone', () =>
|
|
|
187
191
|
assert.strictEqual(blockedOther.block, true, 'other milestones should remain blocked');
|
|
188
192
|
assert.strictEqual(isMilestoneDepthVerified('M001'), true);
|
|
189
193
|
assert.strictEqual(isMilestoneDepthVerified('M002'), false);
|
|
190
|
-
|
|
191
|
-
clearDiscussionFlowState();
|
|
192
194
|
});
|
|
193
195
|
|
|
194
196
|
// ─── Scenario 11: gsd_summary_save CONTEXT contract is milestone-scoped ──
|
|
195
197
|
|
|
196
198
|
test('write-gate: gsd_summary_save only blocks final milestone CONTEXT writes', () => {
|
|
197
|
-
clearDiscussionFlowState();
|
|
199
|
+
clearDiscussionFlowState(process.cwd());
|
|
198
200
|
|
|
199
201
|
assert.strictEqual(
|
|
200
202
|
shouldBlockContextArtifactSave('CONTEXT-DRAFT', 'M001').block,
|
|
@@ -218,8 +220,6 @@ test('write-gate: gsd_summary_save only blocks final milestone CONTEXT writes',
|
|
|
218
220
|
false,
|
|
219
221
|
'final milestone CONTEXT should pass after verification',
|
|
220
222
|
);
|
|
221
|
-
|
|
222
|
-
clearDiscussionFlowState();
|
|
223
223
|
});
|
|
224
224
|
|
|
225
225
|
test('write-gate: root PROJECT/REQUIREMENTS final saves block behind pending approval gate', () => {
|
|
@@ -299,33 +299,39 @@ test('write-gate: deep root PROJECT/REQUIREMENTS final saves require verified ap
|
|
|
299
299
|
});
|
|
300
300
|
|
|
301
301
|
test('write-gate: reopening a gate revokes its previous verified approval', () => {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
markApprovalGateVerified('depth_verification_project_confirm');
|
|
305
|
-
assert.strictEqual(
|
|
306
|
-
shouldBlockRootArtifactSaveInSnapshot(
|
|
307
|
-
loadWriteGateSnapshot(),
|
|
308
|
-
'PROJECT',
|
|
309
|
-
{ requireVerifiedApproval: true },
|
|
310
|
-
).block,
|
|
311
|
-
false,
|
|
312
|
-
'precondition: verified approval unlocks the final project artifact',
|
|
313
|
-
);
|
|
314
|
-
|
|
315
|
-
setPendingGate('depth_verification_project_confirm');
|
|
316
|
-
clearPendingGate();
|
|
317
|
-
|
|
318
|
-
assert.strictEqual(
|
|
319
|
-
shouldBlockRootArtifactSaveInSnapshot(
|
|
320
|
-
loadWriteGateSnapshot(),
|
|
321
|
-
'PROJECT',
|
|
322
|
-
{ requireVerifiedApproval: true },
|
|
323
|
-
).block,
|
|
324
|
-
true,
|
|
325
|
-
'a re-asked gate must require a fresh approval',
|
|
326
|
-
);
|
|
302
|
+
const base = join(tmpdir(), `gsd-write-gate-reopen-${randomUUID()}`);
|
|
303
|
+
mkdirSync(base, { recursive: true });
|
|
327
304
|
|
|
328
|
-
|
|
305
|
+
try {
|
|
306
|
+
clearDiscussionFlowState(base);
|
|
307
|
+
|
|
308
|
+
markApprovalGateVerified('depth_verification_project_confirm', base);
|
|
309
|
+
assert.strictEqual(
|
|
310
|
+
shouldBlockRootArtifactSaveInSnapshot(
|
|
311
|
+
loadWriteGateSnapshot(base),
|
|
312
|
+
'PROJECT',
|
|
313
|
+
{ requireVerifiedApproval: true },
|
|
314
|
+
).block,
|
|
315
|
+
false,
|
|
316
|
+
'precondition: verified approval unlocks the final project artifact',
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
setPendingGate('depth_verification_project_confirm', base);
|
|
320
|
+
clearPendingGate(base);
|
|
321
|
+
|
|
322
|
+
assert.strictEqual(
|
|
323
|
+
shouldBlockRootArtifactSaveInSnapshot(
|
|
324
|
+
loadWriteGateSnapshot(base),
|
|
325
|
+
'PROJECT',
|
|
326
|
+
{ requireVerifiedApproval: true },
|
|
327
|
+
).block,
|
|
328
|
+
true,
|
|
329
|
+
'a re-asked gate must require a fresh approval',
|
|
330
|
+
);
|
|
331
|
+
} finally {
|
|
332
|
+
clearDiscussionFlowState(base);
|
|
333
|
+
rmSync(base, { recursive: true, force: true });
|
|
334
|
+
}
|
|
329
335
|
});
|
|
330
336
|
|
|
331
337
|
// ═══════════════════════════════════════════════════════════════════════
|
|
@@ -357,26 +363,26 @@ test('write-gate: isGateQuestionId recognizes all gate patterns', () => {
|
|
|
357
363
|
// ─── Scenario 20: setPendingGate / getPendingGate / clearPendingGate lifecycle ──
|
|
358
364
|
|
|
359
365
|
test('write-gate: pending gate lifecycle (set, get, clear)', () => {
|
|
360
|
-
clearDiscussionFlowState();
|
|
366
|
+
clearDiscussionFlowState(process.cwd());
|
|
361
367
|
assert.strictEqual(getPendingGate(), null, 'starts null');
|
|
362
368
|
|
|
363
|
-
setPendingGate('depth_verification');
|
|
369
|
+
setPendingGate('depth_verification', process.cwd());
|
|
364
370
|
assert.strictEqual(getPendingGate(), 'depth_verification', 'set correctly');
|
|
365
371
|
|
|
366
|
-
clearPendingGate();
|
|
372
|
+
clearPendingGate(process.cwd());
|
|
367
373
|
assert.strictEqual(getPendingGate(), null, 'cleared correctly');
|
|
368
374
|
|
|
369
375
|
// clearDiscussionFlowState also clears pending gate
|
|
370
|
-
setPendingGate('depth_verification_M002');
|
|
371
|
-
clearDiscussionFlowState();
|
|
376
|
+
setPendingGate('depth_verification_M002', process.cwd());
|
|
377
|
+
clearDiscussionFlowState(process.cwd());
|
|
372
378
|
assert.strictEqual(getPendingGate(), null, 'clearDiscussionFlowState clears pending gate');
|
|
373
379
|
});
|
|
374
380
|
|
|
375
381
|
// ─── Scenario 21: shouldBlockPendingGate blocks non-safe tools when gate is pending ──
|
|
376
382
|
|
|
377
383
|
test('write-gate: shouldBlockPendingGate blocks write/edit during pending gate', () => {
|
|
378
|
-
clearDiscussionFlowState();
|
|
379
|
-
setPendingGate('depth_verification');
|
|
384
|
+
clearDiscussionFlowState(process.cwd());
|
|
385
|
+
setPendingGate('depth_verification', process.cwd());
|
|
380
386
|
|
|
381
387
|
// write should be blocked during discussion
|
|
382
388
|
const writeResult = shouldBlockPendingGate('write', 'M001', false);
|
|
@@ -390,15 +396,13 @@ test('write-gate: shouldBlockPendingGate blocks write/edit during pending gate',
|
|
|
390
396
|
// gsd tools should be blocked
|
|
391
397
|
const gsdResult = shouldBlockPendingGate('gsd_plan_milestone', 'M001', false);
|
|
392
398
|
assert.strictEqual(gsdResult.block, true, 'gsd tools should be blocked');
|
|
393
|
-
|
|
394
|
-
clearDiscussionFlowState();
|
|
395
399
|
});
|
|
396
400
|
|
|
397
401
|
// ─── Scenario 22: shouldBlockPendingGate allows only re-asking when gate is pending ──
|
|
398
402
|
|
|
399
403
|
test('write-gate: shouldBlockPendingGate blocks read-only tools and allows ask_user_questions during pending gate', () => {
|
|
400
|
-
clearDiscussionFlowState();
|
|
401
|
-
setPendingGate('depth_verification');
|
|
404
|
+
clearDiscussionFlowState(process.cwd());
|
|
405
|
+
setPendingGate('depth_verification', process.cwd());
|
|
402
406
|
|
|
403
407
|
// ask_user_questions is always safe (model needs to re-ask)
|
|
404
408
|
assert.strictEqual(shouldBlockPendingGate('ask_user_questions', 'M001').block, false);
|
|
@@ -407,67 +411,57 @@ test('write-gate: shouldBlockPendingGate blocks read-only tools and allows ask_u
|
|
|
407
411
|
assert.strictEqual(shouldBlockPendingGate('grep', 'M001').block, true);
|
|
408
412
|
assert.strictEqual(shouldBlockPendingGate('glob', 'M001').block, true);
|
|
409
413
|
assert.strictEqual(shouldBlockPendingGate('ls', 'M001').block, true);
|
|
410
|
-
|
|
411
|
-
clearDiscussionFlowState();
|
|
412
414
|
});
|
|
413
415
|
|
|
414
416
|
// ─── Scenario 23: shouldBlockPendingGate still blocks when the session is ambiguous ──
|
|
415
417
|
|
|
416
418
|
test('write-gate: shouldBlockPendingGate blocks outside discussion when a gate is pending', () => {
|
|
417
|
-
clearDiscussionFlowState();
|
|
418
|
-
setPendingGate('depth_verification');
|
|
419
|
+
clearDiscussionFlowState(process.cwd());
|
|
420
|
+
setPendingGate('depth_verification', process.cwd());
|
|
419
421
|
|
|
420
422
|
// No milestoneId and no queue phase — still block because the gate is pending
|
|
421
423
|
const result = shouldBlockPendingGate('write', null, false);
|
|
422
424
|
assert.strictEqual(result.block, true, 'should block even when milestoneId is null');
|
|
423
|
-
|
|
424
|
-
clearDiscussionFlowState();
|
|
425
425
|
});
|
|
426
426
|
|
|
427
427
|
// ─── Scenario 24: shouldBlockPendingGate blocks in queue mode ──
|
|
428
428
|
|
|
429
429
|
test('write-gate: shouldBlockPendingGate blocks in queue mode when gate is pending', () => {
|
|
430
|
-
clearDiscussionFlowState();
|
|
431
|
-
setQueuePhaseActive(true);
|
|
432
|
-
setPendingGate('depth_verification');
|
|
430
|
+
clearDiscussionFlowState(process.cwd());
|
|
431
|
+
setQueuePhaseActive(true, process.cwd());
|
|
432
|
+
setPendingGate('depth_verification', process.cwd());
|
|
433
433
|
|
|
434
434
|
const result = shouldBlockPendingGate('write', null, true);
|
|
435
435
|
assert.strictEqual(result.block, true, 'should block in queue mode');
|
|
436
|
-
|
|
437
|
-
clearDiscussionFlowState();
|
|
438
436
|
});
|
|
439
437
|
|
|
440
438
|
// ─── Scenario 25: shouldBlockPendingGateBash blocks read-only commands ──
|
|
441
439
|
|
|
442
440
|
test('write-gate: shouldBlockPendingGateBash blocks read-only commands during pending gate', () => {
|
|
443
|
-
clearDiscussionFlowState();
|
|
444
|
-
setPendingGate('depth_verification');
|
|
441
|
+
clearDiscussionFlowState(process.cwd());
|
|
442
|
+
setPendingGate('depth_verification', process.cwd());
|
|
445
443
|
|
|
446
444
|
assert.strictEqual(shouldBlockPendingGateBash('cat file.txt', 'M001').block, true);
|
|
447
445
|
assert.strictEqual(shouldBlockPendingGateBash('git log --oneline', 'M001').block, true);
|
|
448
446
|
assert.strictEqual(shouldBlockPendingGateBash('grep -r pattern .', 'M001').block, true);
|
|
449
447
|
assert.strictEqual(shouldBlockPendingGateBash('ls -la', 'M001').block, true);
|
|
450
|
-
|
|
451
|
-
clearDiscussionFlowState();
|
|
452
448
|
});
|
|
453
449
|
|
|
454
450
|
// ─── Scenario 26: shouldBlockPendingGateBash blocks mutating commands ──
|
|
455
451
|
|
|
456
452
|
test('write-gate: shouldBlockPendingGateBash blocks mutating commands during pending gate', () => {
|
|
457
|
-
clearDiscussionFlowState();
|
|
458
|
-
setPendingGate('depth_verification');
|
|
453
|
+
clearDiscussionFlowState(process.cwd());
|
|
454
|
+
setPendingGate('depth_verification', process.cwd());
|
|
459
455
|
|
|
460
456
|
const result = shouldBlockPendingGateBash('npm run build', 'M001');
|
|
461
457
|
assert.strictEqual(result.block, true, 'mutating bash should be blocked');
|
|
462
458
|
assert.ok(result.reason!.includes('depth_verification'));
|
|
463
|
-
|
|
464
|
-
clearDiscussionFlowState();
|
|
465
459
|
});
|
|
466
460
|
|
|
467
461
|
// ─── Scenario 27: no pending gate means no blocking ──
|
|
468
462
|
|
|
469
463
|
test('write-gate: no pending gate means no blocking', () => {
|
|
470
|
-
clearDiscussionFlowState();
|
|
464
|
+
clearDiscussionFlowState(process.cwd());
|
|
471
465
|
|
|
472
466
|
assert.strictEqual(shouldBlockPendingGate('write', 'M001').block, false);
|
|
473
467
|
assert.strictEqual(shouldBlockPendingGateBash('npm run build', 'M001').block, false);
|
|
@@ -476,11 +470,40 @@ test('write-gate: no pending gate means no blocking', () => {
|
|
|
476
470
|
// ─── Scenario 28: resetWriteGateState clears pending gate ──
|
|
477
471
|
|
|
478
472
|
test('write-gate: resetWriteGateState clears pending gate', () => {
|
|
479
|
-
setPendingGate('depth_verification');
|
|
480
|
-
resetWriteGateState();
|
|
473
|
+
setPendingGate('depth_verification', process.cwd());
|
|
474
|
+
resetWriteGateState(process.cwd());
|
|
481
475
|
assert.strictEqual(getPendingGate(), null);
|
|
482
476
|
});
|
|
483
477
|
|
|
478
|
+
test('write-gate: in-memory state is scoped by basePath', () => {
|
|
479
|
+
const workspaceA = join(tmpdir(), `gsd-write-gate-isolation-a-${randomUUID()}`);
|
|
480
|
+
const workspaceB = join(tmpdir(), `gsd-write-gate-isolation-b-${randomUUID()}`);
|
|
481
|
+
|
|
482
|
+
try {
|
|
483
|
+
clearDiscussionFlowState(workspaceA);
|
|
484
|
+
clearDiscussionFlowState(workspaceB);
|
|
485
|
+
|
|
486
|
+
setPendingGate('depth_verification_M777', workspaceA);
|
|
487
|
+
assert.strictEqual(getPendingGate(workspaceA), 'depth_verification_M777', 'workspace A should see its pending gate');
|
|
488
|
+
assert.strictEqual(getPendingGate(workspaceB), null, 'workspace B should not see workspace A pending gate');
|
|
489
|
+
|
|
490
|
+
clearPendingGate(workspaceA);
|
|
491
|
+
setQueuePhaseActive(true, workspaceA);
|
|
492
|
+
assert.strictEqual(isQueuePhaseActive(workspaceA), true, 'workspace A should see queue mode active');
|
|
493
|
+
assert.strictEqual(isQueuePhaseActive(workspaceB), false, 'workspace B should not see workspace A queue mode');
|
|
494
|
+
|
|
495
|
+
markDepthVerified('M777', workspaceA);
|
|
496
|
+
assert.strictEqual(isMilestoneDepthVerified('M777', workspaceA), true, 'workspace A should see its verified milestone');
|
|
497
|
+
assert.strictEqual(isMilestoneDepthVerified('M777', workspaceB), false, 'workspace B should not see workspace A milestone verification');
|
|
498
|
+
assert.strictEqual(isDepthVerified(workspaceB), false, 'workspace B should have no verified depth state');
|
|
499
|
+
} finally {
|
|
500
|
+
clearDiscussionFlowState(workspaceA);
|
|
501
|
+
clearDiscussionFlowState(workspaceB);
|
|
502
|
+
rmSync(workspaceA, { recursive: true, force: true });
|
|
503
|
+
rmSync(workspaceB, { recursive: true, force: true });
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
|
|
484
507
|
// ─── Standard options fixture used across depth confirmation tests ──
|
|
485
508
|
|
|
486
509
|
const STANDARD_OPTIONS = [
|
|
@@ -656,7 +679,7 @@ test('write-gate: loadWriteGateSnapshot returns empty default when persist file
|
|
|
656
679
|
} else {
|
|
657
680
|
process.env.GSD_PERSIST_WRITE_GATE_STATE = originalEnv;
|
|
658
681
|
}
|
|
659
|
-
clearDiscussionFlowState();
|
|
682
|
+
clearDiscussionFlowState(base);
|
|
660
683
|
try {
|
|
661
684
|
rmSync(base, { recursive: true, force: true });
|
|
662
685
|
} catch { /* swallow */ }
|
|
@@ -71,6 +71,6 @@ test('write-intercept: BLOCKED_WRITE_ERROR is a non-empty string', () => {
|
|
|
71
71
|
});
|
|
72
72
|
|
|
73
73
|
test('write-intercept: BLOCKED_WRITE_ERROR mentions engine tool calls', () => {
|
|
74
|
-
assert.ok(BLOCKED_WRITE_ERROR.includes('
|
|
74
|
+
assert.ok(BLOCKED_WRITE_ERROR.includes('gsd_task_complete'), 'should mention gsd_task_complete');
|
|
75
75
|
assert.ok(BLOCKED_WRITE_ERROR.includes('engine tool calls'), 'should mention engine tool calls');
|
|
76
76
|
});
|
|
@@ -349,9 +349,9 @@ export function getRequiredWorkflowToolsForAutoUnit(unitType: string): string[]
|
|
|
349
349
|
case "execute-task":
|
|
350
350
|
case "execute-task-simple":
|
|
351
351
|
case "reactive-execute":
|
|
352
|
-
return ["
|
|
352
|
+
return ["gsd_task_complete"];
|
|
353
353
|
case "complete-slice":
|
|
354
|
-
return ["
|
|
354
|
+
return ["gsd_slice_complete"];
|
|
355
355
|
case "replan-slice":
|
|
356
356
|
return ["gsd_replan_slice"];
|
|
357
357
|
case "reassess-roadmap":
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// GSD-2 + Workspace handle: single source of truth for path resolution per milestone
|
|
2
|
+
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { type GsdPathContract, resolveGsdPathContract, normalizeRealPath } from "./paths.js";
|
|
5
|
+
import { isGsdWorktreePath, resolveWorktreeProjectRoot } from "./worktree-root.js";
|
|
6
|
+
|
|
7
|
+
export type GsdWorkspaceMode = "project" | "worktree";
|
|
8
|
+
|
|
9
|
+
export interface GsdWorkspace {
|
|
10
|
+
readonly projectRoot: string; // realpath-normalized absolute
|
|
11
|
+
readonly worktreeRoot: string | null; // realpath-normalized absolute, null when no worktree
|
|
12
|
+
readonly mode: GsdWorkspaceMode;
|
|
13
|
+
readonly contract: GsdPathContract; // pre-resolved, frozen
|
|
14
|
+
readonly identityKey: string; // canonical key (realpath of projectRoot) for dedup/cache
|
|
15
|
+
readonly lockRoot: string; // where auto.lock and {MID}-META.json live (always projectRoot)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface MilestoneScope {
|
|
19
|
+
readonly workspace: GsdWorkspace;
|
|
20
|
+
readonly milestoneId: string;
|
|
21
|
+
// path methods:
|
|
22
|
+
readonly contextFile: () => string;
|
|
23
|
+
readonly roadmapFile: () => string;
|
|
24
|
+
readonly stateFile: () => string;
|
|
25
|
+
readonly dbPath: () => string;
|
|
26
|
+
readonly milestoneDir: () => string;
|
|
27
|
+
readonly metaJson: () => string; // {MID}-META.json on lockRoot
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function tryRealpath(p: string): string {
|
|
31
|
+
return normalizeRealPath(p);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create an immutable GsdWorkspace handle from a raw base path.
|
|
36
|
+
* Resolves both the project root and (when applicable) the worktree root,
|
|
37
|
+
* normalizes them via realpath, and freezes the result.
|
|
38
|
+
*/
|
|
39
|
+
export function createWorkspace(rawBasePath: string): GsdWorkspace {
|
|
40
|
+
const resolvedBase = resolve(rawBasePath);
|
|
41
|
+
const isWorktree = isGsdWorktreePath(resolvedBase);
|
|
42
|
+
|
|
43
|
+
const projectRootRaw = resolveWorktreeProjectRoot(resolvedBase);
|
|
44
|
+
const projectRoot = tryRealpath(resolve(projectRootRaw));
|
|
45
|
+
|
|
46
|
+
const worktreeRoot = isWorktree ? tryRealpath(resolvedBase) : null;
|
|
47
|
+
|
|
48
|
+
// Derive a canonical base from the already-realpath-normalized paths so that
|
|
49
|
+
// resolveGsdPathContract always receives a canonical path. Using the raw
|
|
50
|
+
// resolvedBase here can produce a non-canonical projectGsd when the input
|
|
51
|
+
// path contains symlinks, causing contract.projectGsd to diverge from the
|
|
52
|
+
// realpath-normalized projectRoot / identityKey.
|
|
53
|
+
const canonicalBase = isWorktree ? (worktreeRoot ?? resolvedBase) : projectRoot;
|
|
54
|
+
const contract = Object.freeze(resolveGsdPathContract(canonicalBase));
|
|
55
|
+
|
|
56
|
+
const identityKey = tryRealpath(projectRoot);
|
|
57
|
+
|
|
58
|
+
const mode: GsdWorkspaceMode = isWorktree ? "worktree" : "project";
|
|
59
|
+
|
|
60
|
+
const workspace: GsdWorkspace = Object.freeze({
|
|
61
|
+
projectRoot,
|
|
62
|
+
worktreeRoot,
|
|
63
|
+
mode,
|
|
64
|
+
contract,
|
|
65
|
+
identityKey,
|
|
66
|
+
lockRoot: projectRoot,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return workspace;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Bind a milestoneId to a workspace, producing an immutable MilestoneScope
|
|
74
|
+
* with path-returning closures that resolve via the authoritative projectGsd.
|
|
75
|
+
*
|
|
76
|
+
* All milestone-content paths route to contract.projectGsd (canonical),
|
|
77
|
+
* since that is the authoritative source of truth regardless of worktree mode.
|
|
78
|
+
*/
|
|
79
|
+
export function scopeMilestone(workspace: GsdWorkspace, milestoneId: string): MilestoneScope {
|
|
80
|
+
const { contract } = workspace;
|
|
81
|
+
const gsd = contract.projectGsd;
|
|
82
|
+
|
|
83
|
+
const scope: MilestoneScope = Object.freeze({
|
|
84
|
+
workspace,
|
|
85
|
+
milestoneId,
|
|
86
|
+
contextFile: () => join(gsd, "milestones", milestoneId, `${milestoneId}-CONTEXT.md`),
|
|
87
|
+
roadmapFile: () => join(gsd, "milestones", milestoneId, `${milestoneId}-ROADMAP.md`),
|
|
88
|
+
stateFile: () => join(gsd, "STATE.md"),
|
|
89
|
+
dbPath: () => contract.projectDb,
|
|
90
|
+
milestoneDir: () => join(gsd, "milestones", milestoneId),
|
|
91
|
+
metaJson: () => join(gsd, `${milestoneId}-META.json`),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return scope;
|
|
95
|
+
}
|