gsd-pi 2.78.1-dev.b6a389b66 → 2.78.1-dev.d8826a445
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/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/phases.js +7 -2
- package/dist/resources/extensions/gsd/auto/session.js +3 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +3 -2
- package/dist/resources/extensions/gsd/auto-post-unit.js +7 -1
- package/dist/resources/extensions/gsd/auto-worktree.js +185 -40
- package/dist/resources/extensions/gsd/auto.js +62 -1
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +17 -16
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
- 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/gsd-db.js +194 -0
- package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
- package/dist/resources/extensions/gsd/guided-flow.js +117 -25
- 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/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 +15 -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 +10 -10
- 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 +10 -10
- 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/phases.ts +8 -2
- package/src/resources/extensions/gsd/auto/session.ts +4 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +10 -2
- package/src/resources/extensions/gsd/auto-post-unit.ts +8 -1
- package/src/resources/extensions/gsd/auto-worktree.ts +225 -47
- package/src/resources/extensions/gsd/auto.ts +79 -1
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +17 -17
- 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/db-writer.ts +113 -17
- package/src/resources/extensions/gsd/delegation-policy.ts +197 -0
- package/src/resources/extensions/gsd/gsd-db.ts +184 -0
- package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
- package/src/resources/extensions/gsd/guided-flow.ts +154 -25
- 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/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-session-scope.test.ts +331 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -0
- 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/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/workspace-collapse-integration.test.ts +371 -0
- 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/originalbase-path-comparison.test.ts +329 -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/pending-autostart-scope.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +150 -7
- package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +74 -0
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +28 -16
- package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +453 -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 +102 -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/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 +190 -0
- package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +67 -52
- 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 +16 -2
- package/src/resources/extensions/gsd/write-intercept.ts +3 -3
- /package/dist/web/standalone/.next/static/{HahrZrc_Xn4wumj0O1Ydp → AT5qi39nKXkdmQIOIoh0f}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{HahrZrc_Xn4wumj0O1Ydp → AT5qi39nKXkdmQIOIoh0f}/_ssgManifest.js +0 -0
|
@@ -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,12 +299,12 @@ 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
|
-
clearDiscussionFlowState();
|
|
302
|
+
clearDiscussionFlowState(process.cwd());
|
|
303
303
|
|
|
304
304
|
markApprovalGateVerified('depth_verification_project_confirm');
|
|
305
305
|
assert.strictEqual(
|
|
306
306
|
shouldBlockRootArtifactSaveInSnapshot(
|
|
307
|
-
loadWriteGateSnapshot(),
|
|
307
|
+
loadWriteGateSnapshot(process.cwd()),
|
|
308
308
|
'PROJECT',
|
|
309
309
|
{ requireVerifiedApproval: true },
|
|
310
310
|
).block,
|
|
@@ -312,20 +312,18 @@ test('write-gate: reopening a gate revokes its previous verified approval', () =
|
|
|
312
312
|
'precondition: verified approval unlocks the final project artifact',
|
|
313
313
|
);
|
|
314
314
|
|
|
315
|
-
setPendingGate('depth_verification_project_confirm');
|
|
316
|
-
clearPendingGate();
|
|
315
|
+
setPendingGate('depth_verification_project_confirm', process.cwd());
|
|
316
|
+
clearPendingGate(process.cwd());
|
|
317
317
|
|
|
318
318
|
assert.strictEqual(
|
|
319
319
|
shouldBlockRootArtifactSaveInSnapshot(
|
|
320
|
-
loadWriteGateSnapshot(),
|
|
320
|
+
loadWriteGateSnapshot(process.cwd()),
|
|
321
321
|
'PROJECT',
|
|
322
322
|
{ requireVerifiedApproval: true },
|
|
323
323
|
).block,
|
|
324
324
|
true,
|
|
325
325
|
'a re-asked gate must require a fresh approval',
|
|
326
326
|
);
|
|
327
|
-
|
|
328
|
-
clearDiscussionFlowState();
|
|
329
327
|
});
|
|
330
328
|
|
|
331
329
|
// ═══════════════════════════════════════════════════════════════════════
|
|
@@ -357,26 +355,26 @@ test('write-gate: isGateQuestionId recognizes all gate patterns', () => {
|
|
|
357
355
|
// ─── Scenario 20: setPendingGate / getPendingGate / clearPendingGate lifecycle ──
|
|
358
356
|
|
|
359
357
|
test('write-gate: pending gate lifecycle (set, get, clear)', () => {
|
|
360
|
-
clearDiscussionFlowState();
|
|
358
|
+
clearDiscussionFlowState(process.cwd());
|
|
361
359
|
assert.strictEqual(getPendingGate(), null, 'starts null');
|
|
362
360
|
|
|
363
|
-
setPendingGate('depth_verification');
|
|
361
|
+
setPendingGate('depth_verification', process.cwd());
|
|
364
362
|
assert.strictEqual(getPendingGate(), 'depth_verification', 'set correctly');
|
|
365
363
|
|
|
366
|
-
clearPendingGate();
|
|
364
|
+
clearPendingGate(process.cwd());
|
|
367
365
|
assert.strictEqual(getPendingGate(), null, 'cleared correctly');
|
|
368
366
|
|
|
369
367
|
// clearDiscussionFlowState also clears pending gate
|
|
370
|
-
setPendingGate('depth_verification_M002');
|
|
371
|
-
clearDiscussionFlowState();
|
|
368
|
+
setPendingGate('depth_verification_M002', process.cwd());
|
|
369
|
+
clearDiscussionFlowState(process.cwd());
|
|
372
370
|
assert.strictEqual(getPendingGate(), null, 'clearDiscussionFlowState clears pending gate');
|
|
373
371
|
});
|
|
374
372
|
|
|
375
373
|
// ─── Scenario 21: shouldBlockPendingGate blocks non-safe tools when gate is pending ──
|
|
376
374
|
|
|
377
375
|
test('write-gate: shouldBlockPendingGate blocks write/edit during pending gate', () => {
|
|
378
|
-
clearDiscussionFlowState();
|
|
379
|
-
setPendingGate('depth_verification');
|
|
376
|
+
clearDiscussionFlowState(process.cwd());
|
|
377
|
+
setPendingGate('depth_verification', process.cwd());
|
|
380
378
|
|
|
381
379
|
// write should be blocked during discussion
|
|
382
380
|
const writeResult = shouldBlockPendingGate('write', 'M001', false);
|
|
@@ -390,15 +388,13 @@ test('write-gate: shouldBlockPendingGate blocks write/edit during pending gate',
|
|
|
390
388
|
// gsd tools should be blocked
|
|
391
389
|
const gsdResult = shouldBlockPendingGate('gsd_plan_milestone', 'M001', false);
|
|
392
390
|
assert.strictEqual(gsdResult.block, true, 'gsd tools should be blocked');
|
|
393
|
-
|
|
394
|
-
clearDiscussionFlowState();
|
|
395
391
|
});
|
|
396
392
|
|
|
397
393
|
// ─── Scenario 22: shouldBlockPendingGate allows only re-asking when gate is pending ──
|
|
398
394
|
|
|
399
395
|
test('write-gate: shouldBlockPendingGate blocks read-only tools and allows ask_user_questions during pending gate', () => {
|
|
400
|
-
clearDiscussionFlowState();
|
|
401
|
-
setPendingGate('depth_verification');
|
|
396
|
+
clearDiscussionFlowState(process.cwd());
|
|
397
|
+
setPendingGate('depth_verification', process.cwd());
|
|
402
398
|
|
|
403
399
|
// ask_user_questions is always safe (model needs to re-ask)
|
|
404
400
|
assert.strictEqual(shouldBlockPendingGate('ask_user_questions', 'M001').block, false);
|
|
@@ -407,67 +403,57 @@ test('write-gate: shouldBlockPendingGate blocks read-only tools and allows ask_u
|
|
|
407
403
|
assert.strictEqual(shouldBlockPendingGate('grep', 'M001').block, true);
|
|
408
404
|
assert.strictEqual(shouldBlockPendingGate('glob', 'M001').block, true);
|
|
409
405
|
assert.strictEqual(shouldBlockPendingGate('ls', 'M001').block, true);
|
|
410
|
-
|
|
411
|
-
clearDiscussionFlowState();
|
|
412
406
|
});
|
|
413
407
|
|
|
414
408
|
// ─── Scenario 23: shouldBlockPendingGate still blocks when the session is ambiguous ──
|
|
415
409
|
|
|
416
410
|
test('write-gate: shouldBlockPendingGate blocks outside discussion when a gate is pending', () => {
|
|
417
|
-
clearDiscussionFlowState();
|
|
418
|
-
setPendingGate('depth_verification');
|
|
411
|
+
clearDiscussionFlowState(process.cwd());
|
|
412
|
+
setPendingGate('depth_verification', process.cwd());
|
|
419
413
|
|
|
420
414
|
// No milestoneId and no queue phase — still block because the gate is pending
|
|
421
415
|
const result = shouldBlockPendingGate('write', null, false);
|
|
422
416
|
assert.strictEqual(result.block, true, 'should block even when milestoneId is null');
|
|
423
|
-
|
|
424
|
-
clearDiscussionFlowState();
|
|
425
417
|
});
|
|
426
418
|
|
|
427
419
|
// ─── Scenario 24: shouldBlockPendingGate blocks in queue mode ──
|
|
428
420
|
|
|
429
421
|
test('write-gate: shouldBlockPendingGate blocks in queue mode when gate is pending', () => {
|
|
430
|
-
clearDiscussionFlowState();
|
|
431
|
-
setQueuePhaseActive(true);
|
|
432
|
-
setPendingGate('depth_verification');
|
|
422
|
+
clearDiscussionFlowState(process.cwd());
|
|
423
|
+
setQueuePhaseActive(true, process.cwd());
|
|
424
|
+
setPendingGate('depth_verification', process.cwd());
|
|
433
425
|
|
|
434
426
|
const result = shouldBlockPendingGate('write', null, true);
|
|
435
427
|
assert.strictEqual(result.block, true, 'should block in queue mode');
|
|
436
|
-
|
|
437
|
-
clearDiscussionFlowState();
|
|
438
428
|
});
|
|
439
429
|
|
|
440
430
|
// ─── Scenario 25: shouldBlockPendingGateBash blocks read-only commands ──
|
|
441
431
|
|
|
442
432
|
test('write-gate: shouldBlockPendingGateBash blocks read-only commands during pending gate', () => {
|
|
443
|
-
clearDiscussionFlowState();
|
|
444
|
-
setPendingGate('depth_verification');
|
|
433
|
+
clearDiscussionFlowState(process.cwd());
|
|
434
|
+
setPendingGate('depth_verification', process.cwd());
|
|
445
435
|
|
|
446
436
|
assert.strictEqual(shouldBlockPendingGateBash('cat file.txt', 'M001').block, true);
|
|
447
437
|
assert.strictEqual(shouldBlockPendingGateBash('git log --oneline', 'M001').block, true);
|
|
448
438
|
assert.strictEqual(shouldBlockPendingGateBash('grep -r pattern .', 'M001').block, true);
|
|
449
439
|
assert.strictEqual(shouldBlockPendingGateBash('ls -la', 'M001').block, true);
|
|
450
|
-
|
|
451
|
-
clearDiscussionFlowState();
|
|
452
440
|
});
|
|
453
441
|
|
|
454
442
|
// ─── Scenario 26: shouldBlockPendingGateBash blocks mutating commands ──
|
|
455
443
|
|
|
456
444
|
test('write-gate: shouldBlockPendingGateBash blocks mutating commands during pending gate', () => {
|
|
457
|
-
clearDiscussionFlowState();
|
|
458
|
-
setPendingGate('depth_verification');
|
|
445
|
+
clearDiscussionFlowState(process.cwd());
|
|
446
|
+
setPendingGate('depth_verification', process.cwd());
|
|
459
447
|
|
|
460
448
|
const result = shouldBlockPendingGateBash('npm run build', 'M001');
|
|
461
449
|
assert.strictEqual(result.block, true, 'mutating bash should be blocked');
|
|
462
450
|
assert.ok(result.reason!.includes('depth_verification'));
|
|
463
|
-
|
|
464
|
-
clearDiscussionFlowState();
|
|
465
451
|
});
|
|
466
452
|
|
|
467
453
|
// ─── Scenario 27: no pending gate means no blocking ──
|
|
468
454
|
|
|
469
455
|
test('write-gate: no pending gate means no blocking', () => {
|
|
470
|
-
clearDiscussionFlowState();
|
|
456
|
+
clearDiscussionFlowState(process.cwd());
|
|
471
457
|
|
|
472
458
|
assert.strictEqual(shouldBlockPendingGate('write', 'M001').block, false);
|
|
473
459
|
assert.strictEqual(shouldBlockPendingGateBash('npm run build', 'M001').block, false);
|
|
@@ -476,11 +462,40 @@ test('write-gate: no pending gate means no blocking', () => {
|
|
|
476
462
|
// ─── Scenario 28: resetWriteGateState clears pending gate ──
|
|
477
463
|
|
|
478
464
|
test('write-gate: resetWriteGateState clears pending gate', () => {
|
|
479
|
-
setPendingGate('depth_verification');
|
|
480
|
-
resetWriteGateState();
|
|
465
|
+
setPendingGate('depth_verification', process.cwd());
|
|
466
|
+
resetWriteGateState(process.cwd());
|
|
481
467
|
assert.strictEqual(getPendingGate(), null);
|
|
482
468
|
});
|
|
483
469
|
|
|
470
|
+
test('write-gate: in-memory state is scoped by basePath', () => {
|
|
471
|
+
const workspaceA = join(tmpdir(), `gsd-write-gate-isolation-a-${randomUUID()}`);
|
|
472
|
+
const workspaceB = join(tmpdir(), `gsd-write-gate-isolation-b-${randomUUID()}`);
|
|
473
|
+
|
|
474
|
+
try {
|
|
475
|
+
clearDiscussionFlowState(workspaceA);
|
|
476
|
+
clearDiscussionFlowState(workspaceB);
|
|
477
|
+
|
|
478
|
+
setPendingGate('depth_verification_M777', workspaceA);
|
|
479
|
+
assert.strictEqual(getPendingGate(workspaceA), 'depth_verification_M777', 'workspace A should see its pending gate');
|
|
480
|
+
assert.strictEqual(getPendingGate(workspaceB), null, 'workspace B should not see workspace A pending gate');
|
|
481
|
+
|
|
482
|
+
clearPendingGate(workspaceA);
|
|
483
|
+
setQueuePhaseActive(true, workspaceA);
|
|
484
|
+
assert.strictEqual(isQueuePhaseActive(workspaceA), true, 'workspace A should see queue mode active');
|
|
485
|
+
assert.strictEqual(isQueuePhaseActive(workspaceB), false, 'workspace B should not see workspace A queue mode');
|
|
486
|
+
|
|
487
|
+
markDepthVerified('M777', workspaceA);
|
|
488
|
+
assert.strictEqual(isMilestoneDepthVerified('M777', workspaceA), true, 'workspace A should see its verified milestone');
|
|
489
|
+
assert.strictEqual(isMilestoneDepthVerified('M777', workspaceB), false, 'workspace B should not see workspace A milestone verification');
|
|
490
|
+
assert.strictEqual(isDepthVerified(workspaceB), false, 'workspace B should have no verified depth state');
|
|
491
|
+
} finally {
|
|
492
|
+
clearDiscussionFlowState(workspaceA);
|
|
493
|
+
clearDiscussionFlowState(workspaceB);
|
|
494
|
+
rmSync(workspaceA, { recursive: true, force: true });
|
|
495
|
+
rmSync(workspaceB, { recursive: true, force: true });
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
484
499
|
// ─── Standard options fixture used across depth confirmation tests ──
|
|
485
500
|
|
|
486
501
|
const STANDARD_OPTIONS = [
|
|
@@ -656,7 +671,7 @@ test('write-gate: loadWriteGateSnapshot returns empty default when persist file
|
|
|
656
671
|
} else {
|
|
657
672
|
process.env.GSD_PERSIST_WRITE_GATE_STATE = originalEnv;
|
|
658
673
|
}
|
|
659
|
-
clearDiscussionFlowState();
|
|
674
|
+
clearDiscussionFlowState(base);
|
|
660
675
|
try {
|
|
661
676
|
rmSync(base, { recursive: true, force: true });
|
|
662
677
|
} 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
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// GSD-2 — WorktreeResolver: encapsulates worktree path state and merge/exit lifecycle.
|
|
1
2
|
/**
|
|
2
3
|
* WorktreeResolver — encapsulates worktree path state and merge/exit lifecycle.
|
|
3
4
|
*
|
|
@@ -23,7 +24,20 @@ import { emitJournalEvent } from "./journal.js";
|
|
|
23
24
|
import { emitWorktreeCreated, emitWorktreeMerged } from "./worktree-telemetry.js";
|
|
24
25
|
import { getCollapseCadence, getMilestoneResquash, resquashMilestoneOnMain } from "./slice-cadence.js";
|
|
25
26
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
26
|
-
import { resolveWorktreeProjectRoot } from "./worktree-root.js";
|
|
27
|
+
import { resolveWorktreeProjectRoot, normalizeWorktreePathForCompare } from "./worktree-root.js";
|
|
28
|
+
|
|
29
|
+
// ─── Path Comparison Helper ────────────────────────────────────────────────
|
|
30
|
+
/**
|
|
31
|
+
* Compare two paths for physical identity, tolerating trailing slashes,
|
|
32
|
+
* symlink differences, and case variations on case-insensitive volumes.
|
|
33
|
+
*
|
|
34
|
+
* Used in place of string `===` / `!==` wherever one operand may be
|
|
35
|
+
* realpath-normalised (e.g. from the workspace registry) and the other
|
|
36
|
+
* may not be (e.g. a raw caller-supplied basePath).
|
|
37
|
+
*/
|
|
38
|
+
function isSamePath(a: string, b: string): boolean {
|
|
39
|
+
return normalizeWorktreePathForCompare(a) === normalizeWorktreePathForCompare(b);
|
|
40
|
+
}
|
|
27
41
|
|
|
28
42
|
// ─── Dependency Interface ──────────────────────────────────────────────────
|
|
29
43
|
|
|
@@ -589,7 +603,7 @@ export class WorktreeResolver {
|
|
|
589
603
|
milestoneId,
|
|
590
604
|
"ROADMAP",
|
|
591
605
|
);
|
|
592
|
-
if (!roadmapPath && this.s.basePath
|
|
606
|
+
if (!roadmapPath && !isSamePath(this.s.basePath, originalBase)) {
|
|
593
607
|
roadmapPath = this.deps.resolveMilestoneFile(
|
|
594
608
|
this.s.basePath,
|
|
595
609
|
milestoneId,
|
|
@@ -91,9 +91,9 @@ function matchesBlockedPattern(path: string): boolean {
|
|
|
91
91
|
* Directs the agent to use engine tool calls instead.
|
|
92
92
|
*/
|
|
93
93
|
export const BLOCKED_WRITE_ERROR = `Direct writes to .gsd/STATE.md and .gsd/gsd.db are blocked. Use engine tool calls instead:
|
|
94
|
-
- To complete a task: call
|
|
95
|
-
- To complete a slice: call
|
|
96
|
-
- To save a decision: call
|
|
94
|
+
- To complete a task: call gsd_task_complete(milestone_id, slice_id, task_id, summary)
|
|
95
|
+
- To complete a slice: call gsd_slice_complete(milestone_id, slice_id, summary, uat_result)
|
|
96
|
+
- To save a decision: call gsd_decision_save(scope, decision, choice, rationale)
|
|
97
97
|
- To start a task: call gsd_start_task(milestone_id, slice_id, task_id)
|
|
98
98
|
- To record verification: call gsd_record_verification(milestone_id, slice_id, task_id, evidence)
|
|
99
99
|
- To report a blocker: call gsd_report_blocker(milestone_id, slice_id, task_id, description)`;
|
|
File without changes
|
|
File without changes
|