gsd-pi 2.33.1-dev.ee47f1b → 2.34.0-dev.bbb5216
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/bundled-resource-path.d.ts +8 -0
- package/dist/bundled-resource-path.js +14 -0
- package/dist/headless-query.js +6 -6
- package/dist/resources/extensions/gsd/auto/session.js +27 -32
- package/dist/resources/extensions/gsd/auto-dashboard.js +29 -109
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +6 -1
- package/dist/resources/extensions/gsd/auto-dispatch.js +52 -81
- package/dist/resources/extensions/gsd/auto-loop.js +956 -0
- package/dist/resources/extensions/gsd/auto-observability.js +4 -2
- package/dist/resources/extensions/gsd/auto-post-unit.js +75 -185
- package/dist/resources/extensions/gsd/auto-prompts.js +133 -101
- package/dist/resources/extensions/gsd/auto-recovery.js +59 -97
- package/dist/resources/extensions/gsd/auto-start.js +330 -309
- package/dist/resources/extensions/gsd/auto-supervisor.js +5 -11
- package/dist/resources/extensions/gsd/auto-timeout-recovery.js +7 -7
- package/dist/resources/extensions/gsd/auto-timers.js +3 -4
- package/dist/resources/extensions/gsd/auto-verification.js +35 -73
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +167 -0
- package/dist/resources/extensions/gsd/auto-worktree.js +291 -126
- package/dist/resources/extensions/gsd/auto.js +283 -1013
- package/dist/resources/extensions/gsd/captures.js +10 -4
- package/dist/resources/extensions/gsd/dispatch-guard.js +7 -8
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -18
- package/dist/resources/extensions/gsd/doctor-checks.js +3 -4
- package/dist/resources/extensions/gsd/git-service.js +1 -1
- package/dist/resources/extensions/gsd/gsd-db.js +296 -151
- package/dist/resources/extensions/gsd/index.js +92 -228
- package/dist/resources/extensions/gsd/post-unit-hooks.js +13 -13
- package/dist/resources/extensions/gsd/progress-score.js +61 -156
- package/dist/resources/extensions/gsd/quick.js +98 -122
- package/dist/resources/extensions/gsd/session-lock.js +13 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
- package/dist/resources/extensions/gsd/undo.js +43 -48
- package/dist/resources/extensions/gsd/unit-runtime.js +16 -15
- package/dist/resources/extensions/gsd/verification-evidence.js +0 -1
- package/dist/resources/extensions/gsd/verification-gate.js +6 -35
- package/dist/resources/extensions/gsd/worktree-command.js +30 -24
- package/dist/resources/extensions/gsd/worktree-manager.js +2 -3
- package/dist/resources/extensions/gsd/worktree-resolver.js +344 -0
- package/dist/resources/extensions/gsd/worktree.js +7 -44
- package/dist/tool-bootstrap.js +59 -11
- package/dist/worktree-cli.js +7 -7
- package/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +3630 -5483
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +735 -2588
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/src/models.generated.ts +1039 -2892
- package/packages/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/session.ts +47 -30
- package/src/resources/extensions/gsd/auto-dashboard.ts +28 -131
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +6 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +135 -91
- package/src/resources/extensions/gsd/auto-loop.ts +1665 -0
- package/src/resources/extensions/gsd/auto-observability.ts +4 -2
- package/src/resources/extensions/gsd/auto-post-unit.ts +85 -228
- package/src/resources/extensions/gsd/auto-prompts.ts +138 -109
- package/src/resources/extensions/gsd/auto-recovery.ts +124 -118
- package/src/resources/extensions/gsd/auto-start.ts +440 -354
- package/src/resources/extensions/gsd/auto-supervisor.ts +5 -12
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +8 -8
- package/src/resources/extensions/gsd/auto-timers.ts +3 -4
- package/src/resources/extensions/gsd/auto-verification.ts +76 -90
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +204 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +389 -141
- package/src/resources/extensions/gsd/auto.ts +515 -1199
- package/src/resources/extensions/gsd/captures.ts +10 -4
- package/src/resources/extensions/gsd/dispatch-guard.ts +13 -9
- package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -18
- package/src/resources/extensions/gsd/doctor-checks.ts +3 -4
- package/src/resources/extensions/gsd/git-service.ts +8 -1
- package/src/resources/extensions/gsd/gitignore.ts +4 -2
- package/src/resources/extensions/gsd/gsd-db.ts +375 -180
- package/src/resources/extensions/gsd/index.ts +104 -263
- package/src/resources/extensions/gsd/post-unit-hooks.ts +13 -13
- package/src/resources/extensions/gsd/progress-score.ts +65 -200
- package/src/resources/extensions/gsd/quick.ts +121 -125
- package/src/resources/extensions/gsd/session-lock.ts +11 -0
- package/src/resources/extensions/gsd/templates/preferences.md +1 -0
- package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +32 -59
- package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +75 -27
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +37 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1458 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +8 -162
- package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -108
- package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +1 -3
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -55
- package/src/resources/extensions/gsd/tests/headless-query.test.ts +22 -0
- package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +8 -11
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +4 -6
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +181 -0
- package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +6 -6
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -6
- package/src/resources/extensions/gsd/tests/undo.test.ts +6 -0
- package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +24 -26
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +7 -201
- package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
- package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
- package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +705 -0
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +57 -106
- package/src/resources/extensions/gsd/tests/worktree.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +43 -132
- package/src/resources/extensions/gsd/types.ts +90 -81
- package/src/resources/extensions/gsd/undo.ts +42 -46
- package/src/resources/extensions/gsd/unit-runtime.ts +14 -18
- package/src/resources/extensions/gsd/verification-evidence.ts +1 -3
- package/src/resources/extensions/gsd/verification-gate.ts +6 -39
- package/src/resources/extensions/gsd/worktree-command.ts +36 -24
- package/src/resources/extensions/gsd/worktree-manager.ts +2 -3
- package/src/resources/extensions/gsd/worktree-resolver.ts +485 -0
- package/src/resources/extensions/gsd/worktree.ts +7 -44
- package/dist/resources/extensions/gsd/auto-constants.js +0 -5
- package/dist/resources/extensions/gsd/auto-idempotency.js +0 -106
- package/dist/resources/extensions/gsd/auto-stuck-detection.js +0 -165
- package/dist/resources/extensions/gsd/mechanical-completion.js +0 -351
- package/src/resources/extensions/gsd/auto-constants.ts +0 -6
- package/src/resources/extensions/gsd/auto-idempotency.ts +0 -151
- package/src/resources/extensions/gsd/auto-stuck-detection.ts +0 -221
- package/src/resources/extensions/gsd/mechanical-completion.ts +0 -430
- package/src/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +0 -691
- package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +0 -127
- package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +0 -123
- package/src/resources/extensions/gsd/tests/dispatch-stall-guard.test.ts +0 -126
- package/src/resources/extensions/gsd/tests/loop-regression.test.ts +0 -874
- package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +0 -356
- package/src/resources/extensions/gsd/tests/progress-score.test.ts +0 -206
- package/src/resources/extensions/gsd/tests/session-lock.test.ts +0 -434
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* worktree-sync-milestones.test.ts — Regression test for #1311.
|
|
3
3
|
*
|
|
4
|
-
* Verifies that
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Verifies that syncProjectRootToWorktree copies milestone artifacts
|
|
5
|
+
* from the main repo's .gsd/ into the worktree's .gsd/ for the
|
|
6
|
+
* specified milestone, and deletes gsd.db so it rebuilds from fresh state.
|
|
7
7
|
*
|
|
8
8
|
* Covers:
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
12
|
-
* - No-op when
|
|
13
|
-
* -
|
|
9
|
+
* - Milestone directory synced from main to worktree
|
|
10
|
+
* - Missing slices within a milestone are synced
|
|
11
|
+
* - gsd.db deleted in worktree after sync
|
|
12
|
+
* - No-op when paths are equal
|
|
13
|
+
* - No-op when milestoneId is null
|
|
14
|
+
* - Non-existent directories handled gracefully
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
|
-
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync
|
|
17
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
|
|
17
18
|
import { join } from 'node:path';
|
|
18
19
|
import { tmpdir } from 'node:os';
|
|
19
20
|
|
|
20
|
-
import {
|
|
21
|
+
import { syncProjectRootToWorktree } from '../auto-worktree-sync.ts';
|
|
21
22
|
import { createTestContext } from './test-helpers.ts';
|
|
22
23
|
|
|
23
|
-
const {
|
|
24
|
+
const { assertTrue, report } = createTestContext();
|
|
24
25
|
|
|
25
26
|
function createBase(name: string): string {
|
|
26
27
|
const base = mkdtempSync(join(tmpdir(), `gsd-wt-sync-${name}-`));
|
|
@@ -34,156 +35,106 @@ function cleanup(base: string): void {
|
|
|
34
35
|
|
|
35
36
|
async function main(): Promise<void> {
|
|
36
37
|
|
|
37
|
-
// ─── 1.
|
|
38
|
-
console.log('\n=== 1.
|
|
38
|
+
// ─── 1. Milestone directory synced from main to worktree ──────────────
|
|
39
|
+
console.log('\n=== 1. milestone directory synced from main to worktree ===');
|
|
39
40
|
{
|
|
40
41
|
const mainBase = createBase('main');
|
|
41
42
|
const wtBase = createBase('wt');
|
|
42
43
|
|
|
43
44
|
try {
|
|
44
|
-
// Main repo has M001 and M002
|
|
45
45
|
const m001Dir = join(mainBase, '.gsd', 'milestones', 'M001');
|
|
46
46
|
mkdirSync(m001Dir, { recursive: true });
|
|
47
|
-
writeFileSync(join(m001Dir, 'M001-CONTEXT.md'), '# M001\
|
|
47
|
+
writeFileSync(join(m001Dir, 'M001-CONTEXT.md'), '# M001\nContext.');
|
|
48
48
|
writeFileSync(join(m001Dir, 'M001-ROADMAP.md'), '# Roadmap');
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
writeFileSync(join(m002Dir, 'M002-CONTEXT.md'), '# M002\nNew milestone.');
|
|
53
|
-
writeFileSync(join(m002Dir, 'M002-ROADMAP.md'), '# Roadmap');
|
|
50
|
+
// Worktree has no M001
|
|
51
|
+
assertTrue(!existsSync(join(wtBase, '.gsd', 'milestones', 'M001')), 'M001 missing before sync');
|
|
54
52
|
|
|
55
|
-
|
|
56
|
-
const wtM001Dir = join(wtBase, '.gsd', 'milestones', 'M001');
|
|
57
|
-
mkdirSync(wtM001Dir, { recursive: true });
|
|
58
|
-
writeFileSync(join(wtM001Dir, 'M001-CONTEXT.md'), '# M001\nDone.');
|
|
59
|
-
|
|
60
|
-
// M002 is missing from worktree
|
|
61
|
-
assertTrue(!existsSync(join(wtBase, '.gsd', 'milestones', 'M002')), 'M002 missing before sync');
|
|
53
|
+
syncProjectRootToWorktree(mainBase, wtBase, 'M001');
|
|
62
54
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', '
|
|
66
|
-
assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M002', 'M002-CONTEXT.md')), 'M002 CONTEXT synced');
|
|
67
|
-
assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M002', 'M002-ROADMAP.md')), 'M002 ROADMAP synced');
|
|
68
|
-
assertTrue(result.synced.length > 0, 'sync reported files');
|
|
55
|
+
assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M001')), '#1311: M001 synced to worktree');
|
|
56
|
+
assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'M001-CONTEXT.md')), 'M001 CONTEXT synced');
|
|
57
|
+
assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'M001-ROADMAP.md')), 'M001 ROADMAP synced');
|
|
69
58
|
} finally {
|
|
70
59
|
cleanup(mainBase);
|
|
71
60
|
cleanup(wtBase);
|
|
72
61
|
}
|
|
73
62
|
}
|
|
74
63
|
|
|
75
|
-
// ─── 2. Missing
|
|
76
|
-
console.log('\n=== 2. missing
|
|
64
|
+
// ─── 2. Missing slices synced ──────────────────────────────────────────
|
|
65
|
+
console.log('\n=== 2. missing slices within milestone are synced ===');
|
|
77
66
|
{
|
|
78
67
|
const mainBase = createBase('main');
|
|
79
68
|
const wtBase = createBase('wt');
|
|
80
69
|
|
|
81
70
|
try {
|
|
82
|
-
// Main repo M001 has CONTEXT, ROADMAP, RESEARCH
|
|
83
71
|
const m001Dir = join(mainBase, '.gsd', 'milestones', 'M001');
|
|
84
|
-
mkdirSync(m001Dir, { recursive: true });
|
|
85
|
-
|
|
86
|
-
writeFileSync(join(m001Dir, 'M001-ROADMAP.md'), '#
|
|
87
|
-
writeFileSync(join(m001Dir, '
|
|
72
|
+
mkdirSync(join(m001Dir, 'slices', 'S01'), { recursive: true });
|
|
73
|
+
mkdirSync(join(m001Dir, 'slices', 'S02'), { recursive: true });
|
|
74
|
+
writeFileSync(join(m001Dir, 'M001-ROADMAP.md'), '# Roadmap');
|
|
75
|
+
writeFileSync(join(m001Dir, 'slices', 'S01', 'S01-PLAN.md'), '# S01 Plan');
|
|
76
|
+
writeFileSync(join(m001Dir, 'slices', 'S02', 'S02-PLAN.md'), '# S02 Plan');
|
|
88
77
|
|
|
89
|
-
// Worktree
|
|
78
|
+
// Worktree only has S01
|
|
90
79
|
const wtM001Dir = join(wtBase, '.gsd', 'milestones', 'M001');
|
|
91
|
-
mkdirSync(wtM001Dir, { recursive: true });
|
|
92
|
-
writeFileSync(join(wtM001Dir, '
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', '
|
|
97
|
-
assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', '
|
|
98
|
-
// Existing file should NOT be overwritten
|
|
99
|
-
assertEq(
|
|
100
|
-
readFileSync(join(wtBase, '.gsd', 'milestones', 'M001', 'M001-CONTEXT.md'), 'utf-8'),
|
|
101
|
-
'# M001 Context',
|
|
102
|
-
'existing CONTEXT not overwritten',
|
|
103
|
-
);
|
|
80
|
+
mkdirSync(join(wtM001Dir, 'slices', 'S01'), { recursive: true });
|
|
81
|
+
writeFileSync(join(wtM001Dir, 'slices', 'S01', 'S01-PLAN.md'), '# S01 Plan');
|
|
82
|
+
|
|
83
|
+
syncProjectRootToWorktree(mainBase, wtBase, 'M001');
|
|
84
|
+
|
|
85
|
+
assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'slices', 'S02')), '#1311: S02 synced');
|
|
86
|
+
assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'slices', 'S02', 'S02-PLAN.md')), 'S02 PLAN synced');
|
|
104
87
|
} finally {
|
|
105
88
|
cleanup(mainBase);
|
|
106
89
|
cleanup(wtBase);
|
|
107
90
|
}
|
|
108
91
|
}
|
|
109
92
|
|
|
110
|
-
// ─── 3.
|
|
111
|
-
console.log('\n=== 3.
|
|
93
|
+
// ─── 3. gsd.db deleted in worktree after sync ─────────────────────────
|
|
94
|
+
console.log('\n=== 3. gsd.db deleted in worktree after sync ===');
|
|
112
95
|
{
|
|
113
96
|
const mainBase = createBase('main');
|
|
114
97
|
const wtBase = createBase('wt');
|
|
115
98
|
|
|
116
99
|
try {
|
|
117
|
-
// Main repo has M001 with slices S01–S03
|
|
118
100
|
const m001Dir = join(mainBase, '.gsd', 'milestones', 'M001');
|
|
119
|
-
mkdirSync(
|
|
120
|
-
mkdirSync(join(m001Dir, 'slices', 'S02'), { recursive: true });
|
|
121
|
-
mkdirSync(join(m001Dir, 'slices', 'S03'), { recursive: true });
|
|
101
|
+
mkdirSync(m001Dir, { recursive: true });
|
|
122
102
|
writeFileSync(join(m001Dir, 'M001-ROADMAP.md'), '# Roadmap');
|
|
123
|
-
writeFileSync(join(m001Dir, 'slices', 'S01', 'S01-PLAN.md'), '# S01 Plan');
|
|
124
|
-
writeFileSync(join(m001Dir, 'slices', 'S02', 'S02-PLAN.md'), '# S02 Plan');
|
|
125
|
-
writeFileSync(join(m001Dir, 'slices', 'S03', 'S03-PLAN.md'), '# S03 Plan');
|
|
126
103
|
|
|
127
|
-
// Worktree
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
mkdirSync(join(wtM001Dir, 'slices', 'S02'), { recursive: true });
|
|
131
|
-
writeFileSync(join(wtM001Dir, 'M001-ROADMAP.md'), '# Roadmap');
|
|
132
|
-
writeFileSync(join(wtM001Dir, 'slices', 'S01', 'S01-PLAN.md'), '# S01 Plan');
|
|
133
|
-
writeFileSync(join(wtM001Dir, 'slices', 'S02', 'S02-PLAN.md'), '# S02 Plan');
|
|
104
|
+
// Worktree has a stale gsd.db
|
|
105
|
+
writeFileSync(join(wtBase, '.gsd', 'gsd.db'), 'stale data');
|
|
106
|
+
assertTrue(existsSync(join(wtBase, '.gsd', 'gsd.db')), 'gsd.db exists before sync');
|
|
134
107
|
|
|
135
|
-
|
|
108
|
+
syncProjectRootToWorktree(mainBase, wtBase, 'M001');
|
|
136
109
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'slices', 'S03')), '#1311: S03 synced');
|
|
140
|
-
assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'slices', 'S03', 'S03-PLAN.md')), 'S03 PLAN synced');
|
|
110
|
+
assertTrue(!existsSync(join(wtBase, '.gsd', 'gsd.db')), '#853: gsd.db deleted after sync');
|
|
141
111
|
} finally {
|
|
142
112
|
cleanup(mainBase);
|
|
143
113
|
cleanup(wtBase);
|
|
144
114
|
}
|
|
145
115
|
}
|
|
146
116
|
|
|
147
|
-
// ─── 4. No-op when
|
|
148
|
-
console.log('\n=== 4. no-op when
|
|
117
|
+
// ─── 4. No-op when paths are equal ────────────────────────────────────
|
|
118
|
+
console.log('\n=== 4. no-op when paths are equal ===');
|
|
149
119
|
{
|
|
150
|
-
const
|
|
151
|
-
const mainBase = mkdtempSync(join(tmpdir(), 'gsd-wt-sync-main-'));
|
|
152
|
-
const wtBase = mkdtempSync(join(tmpdir(), 'gsd-wt-sync-wt-'));
|
|
153
|
-
|
|
120
|
+
const base = createBase('same');
|
|
154
121
|
try {
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
symlinkSync(join(sharedDir, '.gsd'), join(wtBase, '.gsd'));
|
|
159
|
-
|
|
160
|
-
const result = syncGsdStateToWorktree(mainBase, wtBase);
|
|
161
|
-
assertEq(result.synced.length, 0, 'no files synced when both point to same dir');
|
|
122
|
+
// Should not throw
|
|
123
|
+
syncProjectRootToWorktree(base, base, 'M001');
|
|
124
|
+
assertTrue(true, 'no crash when paths are equal');
|
|
162
125
|
} finally {
|
|
163
|
-
cleanup(
|
|
164
|
-
rmSync(mainBase, { recursive: true, force: true });
|
|
165
|
-
rmSync(wtBase, { recursive: true, force: true });
|
|
126
|
+
cleanup(base);
|
|
166
127
|
}
|
|
167
128
|
}
|
|
168
129
|
|
|
169
|
-
// ─── 5.
|
|
170
|
-
console.log('\n=== 5.
|
|
130
|
+
// ─── 5. No-op when milestoneId is null ────────────────────────────────
|
|
131
|
+
console.log('\n=== 5. no-op when milestoneId is null ===');
|
|
171
132
|
{
|
|
172
133
|
const mainBase = createBase('main');
|
|
173
134
|
const wtBase = createBase('wt');
|
|
174
|
-
|
|
175
135
|
try {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
writeFileSync(join(mainBase, '.gsd', 'PROJECT.md'), '# Project');
|
|
179
|
-
|
|
180
|
-
// Worktree has none of these
|
|
181
|
-
const result = syncGsdStateToWorktree(mainBase, wtBase);
|
|
182
|
-
|
|
183
|
-
assertTrue(existsSync(join(wtBase, '.gsd', 'DECISIONS.md')), 'DECISIONS.md synced');
|
|
184
|
-
assertTrue(existsSync(join(wtBase, '.gsd', 'REQUIREMENTS.md')), 'REQUIREMENTS.md synced');
|
|
185
|
-
assertTrue(existsSync(join(wtBase, '.gsd', 'PROJECT.md')), 'PROJECT.md synced');
|
|
186
|
-
assertTrue(result.synced.length >= 3, 'at least 3 files synced');
|
|
136
|
+
syncProjectRootToWorktree(mainBase, wtBase, null);
|
|
137
|
+
assertTrue(true, 'no crash when milestoneId is null');
|
|
187
138
|
} finally {
|
|
188
139
|
cleanup(mainBase);
|
|
189
140
|
cleanup(wtBase);
|
|
@@ -193,8 +144,8 @@ async function main(): Promise<void> {
|
|
|
193
144
|
// ─── 6. Non-existent directories handled gracefully ───────────────────
|
|
194
145
|
console.log('\n=== 6. non-existent directories → no-op ===');
|
|
195
146
|
{
|
|
196
|
-
|
|
197
|
-
|
|
147
|
+
syncProjectRootToWorktree('/tmp/does-not-exist-main', '/tmp/does-not-exist-wt', 'M001');
|
|
148
|
+
assertTrue(true, 'no crash on missing directories');
|
|
198
149
|
}
|
|
199
150
|
|
|
200
151
|
report();
|
|
@@ -104,11 +104,15 @@ async function main(): Promise<void> {
|
|
|
104
104
|
run("git checkout -b f-123-thing", repo);
|
|
105
105
|
assertEq(getCurrentBranch(repo), "f-123-thing", "on feature branch");
|
|
106
106
|
|
|
107
|
+
const commitsBefore = run("git rev-list --count HEAD", repo);
|
|
107
108
|
captureIntegrationBranch(repo, "M001");
|
|
108
109
|
assertEq(readIntegrationBranch(repo, "M001"), "f-123-thing",
|
|
109
110
|
"captureIntegrationBranch records the current branch");
|
|
110
111
|
|
|
111
|
-
//
|
|
112
|
+
// Metadata is stored in external state, not committed to git.
|
|
113
|
+
const commitsAfter = run("git rev-list --count HEAD", repo);
|
|
114
|
+
assertEq(commitsAfter, commitsBefore, "captureIntegrationBranch does not create a git commit");
|
|
115
|
+
|
|
112
116
|
rmSync(repo, { recursive: true, force: true });
|
|
113
117
|
}
|
|
114
118
|
|
|
@@ -1,31 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Unit tests for the CONTEXT.md write-gate.
|
|
2
|
+
* Unit tests for the CONTEXT.md write-gate (D031 guard chain).
|
|
3
3
|
*
|
|
4
4
|
* Exercises shouldBlockContextWrite() — a pure function that implements:
|
|
5
5
|
* (a) toolName !== "write" → pass
|
|
6
|
-
* (b) milestoneId null
|
|
6
|
+
* (b) milestoneId null → pass (not in discussion)
|
|
7
7
|
* (c) path doesn't match /M\d+-CONTEXT\.md$/ → pass
|
|
8
|
-
* (d) depthVerified → pass
|
|
9
|
-
* (e)
|
|
10
|
-
* (f) queuePhaseActive + not verified → block
|
|
11
|
-
* (g) else → block with actionable reason
|
|
12
|
-
*
|
|
13
|
-
* Also exercises per-milestone verification helpers:
|
|
14
|
-
* markDepthVerified(), isDepthVerifiedFor()
|
|
8
|
+
* (d) depthVerified → pass
|
|
9
|
+
* (e) else → block with actionable reason
|
|
15
10
|
*/
|
|
16
11
|
|
|
17
12
|
import test from 'node:test';
|
|
18
13
|
import assert from 'node:assert/strict';
|
|
19
|
-
import {
|
|
20
|
-
shouldBlockContextWrite,
|
|
21
|
-
markDepthVerified,
|
|
22
|
-
isDepthVerifiedFor,
|
|
23
|
-
isDepthVerified,
|
|
24
|
-
} from '../index.ts';
|
|
14
|
+
import { shouldBlockContextWrite } from '../index.ts';
|
|
25
15
|
|
|
26
|
-
//
|
|
27
|
-
// Discussion flow tests (backward compatibility)
|
|
28
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
16
|
+
// ─── Scenario 1: Blocks CONTEXT.md write during discussion without depth verification (absolute path) ──
|
|
29
17
|
|
|
30
18
|
test('write-gate: blocks CONTEXT.md write during discussion without depth verification (absolute path)', () => {
|
|
31
19
|
const result = shouldBlockContextWrite(
|
|
@@ -38,6 +26,8 @@ test('write-gate: blocks CONTEXT.md write during discussion without depth verifi
|
|
|
38
26
|
assert.ok(result.reason, 'should provide a reason');
|
|
39
27
|
});
|
|
40
28
|
|
|
29
|
+
// ─── Scenario 2: Blocks CONTEXT.md write during discussion without depth verification (relative path) ──
|
|
30
|
+
|
|
41
31
|
test('write-gate: blocks CONTEXT.md write during discussion without depth verification (relative path)', () => {
|
|
42
32
|
const result = shouldBlockContextWrite(
|
|
43
33
|
'write',
|
|
@@ -49,7 +39,9 @@ test('write-gate: blocks CONTEXT.md write during discussion without depth verifi
|
|
|
49
39
|
assert.ok(result.reason, 'should provide a reason');
|
|
50
40
|
});
|
|
51
41
|
|
|
52
|
-
|
|
42
|
+
// ─── Scenario 3: Allows CONTEXT.md write after depth verification ──
|
|
43
|
+
|
|
44
|
+
test('write-gate: allows CONTEXT.md write after depth verification', () => {
|
|
53
45
|
const result = shouldBlockContextWrite(
|
|
54
46
|
'write',
|
|
55
47
|
'/Users/dev/project/.gsd/milestones/M001/M001-CONTEXT.md',
|
|
@@ -60,152 +52,71 @@ test('write-gate: allows CONTEXT.md write after depth verification (discussion f
|
|
|
60
52
|
assert.strictEqual(result.reason, undefined, 'should have no reason');
|
|
61
53
|
});
|
|
62
54
|
|
|
63
|
-
|
|
55
|
+
// ─── Scenario 4: Allows CONTEXT.md write outside discussion phase (milestoneId null) ──
|
|
56
|
+
|
|
57
|
+
test('write-gate: allows CONTEXT.md write outside discussion phase', () => {
|
|
64
58
|
const result = shouldBlockContextWrite(
|
|
65
59
|
'write',
|
|
66
60
|
'.gsd/milestones/M001/M001-CONTEXT.md',
|
|
67
61
|
null,
|
|
68
62
|
false,
|
|
69
|
-
false,
|
|
70
63
|
);
|
|
71
|
-
assert.strictEqual(result.block, false, 'should not block outside
|
|
64
|
+
assert.strictEqual(result.block, false, 'should not block outside discussion phase');
|
|
72
65
|
});
|
|
73
66
|
|
|
74
|
-
|
|
75
|
-
const r1 = shouldBlockContextWrite('write', '.gsd/milestones/M001/M001-DISCUSSION.md', 'M001', false);
|
|
76
|
-
assert.strictEqual(r1.block, false, 'DISCUSSION.md should pass');
|
|
77
|
-
|
|
78
|
-
const r2 = shouldBlockContextWrite('write', '.gsd/milestones/M001/slices/S01/S01-PLAN.md', 'M001', false);
|
|
79
|
-
assert.strictEqual(r2.block, false, 'slice plan should pass');
|
|
80
|
-
|
|
81
|
-
const r3 = shouldBlockContextWrite('write', 'src/index.ts', 'M001', false);
|
|
82
|
-
assert.strictEqual(r3.block, false, 'regular code file should pass');
|
|
83
|
-
});
|
|
67
|
+
// ─── Scenario 5: Allows non-CONTEXT.md writes during discussion ──
|
|
84
68
|
|
|
85
|
-
test('write-gate:
|
|
86
|
-
|
|
69
|
+
test('write-gate: allows non-CONTEXT.md writes during discussion', () => {
|
|
70
|
+
// DISCUSSION.md
|
|
71
|
+
const r1 = shouldBlockContextWrite(
|
|
87
72
|
'write',
|
|
88
|
-
'.gsd/milestones/M001/
|
|
73
|
+
'.gsd/milestones/M001/M001-DISCUSSION.md',
|
|
89
74
|
'M001',
|
|
90
75
|
false,
|
|
91
76
|
);
|
|
92
|
-
assert.strictEqual(
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test('write-gate: blocked reason contains actionable instructions', () => {
|
|
96
|
-
const result = shouldBlockContextWrite(
|
|
97
|
-
'write',
|
|
98
|
-
'.gsd/milestones/M999/M999-CONTEXT.md',
|
|
99
|
-
'M999',
|
|
100
|
-
false,
|
|
101
|
-
);
|
|
102
|
-
assert.strictEqual(result.block, true);
|
|
103
|
-
assert.ok(result.reason!.includes('depth_verification'), 'reason should mention depth_verification');
|
|
104
|
-
assert.ok(result.reason!.includes('ask_user_questions'), 'reason should mention ask_user_questions');
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
108
|
-
// Queue flow tests (NEW — enforces write-gate during /gsd queue)
|
|
109
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
110
|
-
|
|
111
|
-
test('write-gate: blocks CONTEXT.md write during queue flow without verification', () => {
|
|
112
|
-
const result = shouldBlockContextWrite(
|
|
113
|
-
'write',
|
|
114
|
-
'.gsd/milestones/M010-3ym37m/M010-3ym37m-CONTEXT.md',
|
|
115
|
-
null, // queue flows have no pendingAutoStart → milestoneId is null
|
|
116
|
-
false,
|
|
117
|
-
true, // but queuePhaseActive is true
|
|
118
|
-
);
|
|
119
|
-
assert.strictEqual(result.block, true, 'should block during queue flow without verification');
|
|
120
|
-
assert.ok(result.reason!.includes('multi-milestone'), 'reason should mention multi-milestone');
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test('write-gate: allows CONTEXT.md write during queue flow AFTER per-milestone verification', () => {
|
|
124
|
-
// Simulate: depth_verification_M010-3ym37m was answered
|
|
125
|
-
markDepthVerified('M010-3ym37m');
|
|
77
|
+
assert.strictEqual(r1.block, false, 'DISCUSSION.md should pass');
|
|
126
78
|
|
|
127
|
-
|
|
79
|
+
// Slice file
|
|
80
|
+
const r2 = shouldBlockContextWrite(
|
|
128
81
|
'write',
|
|
129
|
-
'.gsd/milestones/
|
|
130
|
-
|
|
82
|
+
'.gsd/milestones/M001/slices/S01/S01-PLAN.md',
|
|
83
|
+
'M001',
|
|
131
84
|
false,
|
|
132
|
-
true,
|
|
133
85
|
);
|
|
134
|
-
assert.strictEqual(
|
|
135
|
-
});
|
|
86
|
+
assert.strictEqual(r2.block, false, 'slice plan should pass');
|
|
136
87
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const result = shouldBlockContextWrite(
|
|
88
|
+
// Regular code file
|
|
89
|
+
const r3 = shouldBlockContextWrite(
|
|
140
90
|
'write',
|
|
141
|
-
'
|
|
142
|
-
|
|
91
|
+
'src/index.ts',
|
|
92
|
+
'M001',
|
|
143
93
|
false,
|
|
144
|
-
true,
|
|
145
94
|
);
|
|
146
|
-
assert.strictEqual(
|
|
95
|
+
assert.strictEqual(r3.block, false, 'regular code file should pass');
|
|
147
96
|
});
|
|
148
97
|
|
|
149
|
-
|
|
150
|
-
markDepthVerified('*');
|
|
151
|
-
|
|
152
|
-
const r1 = shouldBlockContextWrite(
|
|
153
|
-
'write',
|
|
154
|
-
'.gsd/milestones/M099/M099-CONTEXT.md',
|
|
155
|
-
null,
|
|
156
|
-
false,
|
|
157
|
-
true,
|
|
158
|
-
);
|
|
159
|
-
assert.strictEqual(r1.block, false, 'wildcard should pass any milestone');
|
|
160
|
-
});
|
|
98
|
+
// ─── Scenario 6: Regex specificity — doesn't match S01-CONTEXT.md ──
|
|
161
99
|
|
|
162
|
-
test('write-gate:
|
|
100
|
+
test('write-gate: regex does not match slice context files (S01-CONTEXT.md)', () => {
|
|
163
101
|
const result = shouldBlockContextWrite(
|
|
164
102
|
'write',
|
|
165
|
-
'.gsd/
|
|
166
|
-
|
|
103
|
+
'.gsd/milestones/M001/slices/S01/S01-CONTEXT.md',
|
|
104
|
+
'M001',
|
|
167
105
|
false,
|
|
168
|
-
true,
|
|
169
106
|
);
|
|
170
|
-
assert.strictEqual(result.block, false, '
|
|
107
|
+
assert.strictEqual(result.block, false, 'S01-CONTEXT.md should not be blocked');
|
|
171
108
|
});
|
|
172
109
|
|
|
173
|
-
//
|
|
174
|
-
// Unique milestone ID format tests
|
|
175
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
176
|
-
|
|
177
|
-
test('write-gate: matches unique milestone ID format (M010-3ym37m)', () => {
|
|
178
|
-
const result = shouldBlockContextWrite(
|
|
179
|
-
'write',
|
|
180
|
-
'.gsd/milestones/M010-3ym37m/M010-3ym37m-CONTEXT.md',
|
|
181
|
-
'M010-3ym37m',
|
|
182
|
-
false,
|
|
183
|
-
);
|
|
184
|
-
assert.strictEqual(result.block, true, 'should match unique milestone ID format');
|
|
185
|
-
});
|
|
110
|
+
// ─── Scenario 7: Error message contains actionable instruction ──
|
|
186
111
|
|
|
187
|
-
test('write-gate:
|
|
112
|
+
test('write-gate: blocked reason contains depth_verification keyword', () => {
|
|
188
113
|
const result = shouldBlockContextWrite(
|
|
189
114
|
'write',
|
|
190
|
-
'.gsd/milestones/
|
|
191
|
-
'
|
|
115
|
+
'.gsd/milestones/M999/M999-CONTEXT.md',
|
|
116
|
+
'M999',
|
|
192
117
|
false,
|
|
193
118
|
);
|
|
194
|
-
assert.strictEqual(result.block, true
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
198
|
-
// Per-milestone depth verification helpers
|
|
199
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
200
|
-
|
|
201
|
-
test('isDepthVerifiedFor: returns false for unknown milestone', () => {
|
|
202
|
-
assert.strictEqual(isDepthVerifiedFor('M999-xxxxxx'), true,
|
|
203
|
-
'returns true because wildcard * was set in earlier test');
|
|
204
|
-
// Note: test isolation would require clearing state, but these tests
|
|
205
|
-
// exercise the module as a singleton (matching production behavior)
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
test('isDepthVerified: returns true when any milestone verified', () => {
|
|
209
|
-
// At this point M010-3ym37m and * are verified from earlier tests
|
|
210
|
-
assert.strictEqual(isDepthVerified(), true);
|
|
119
|
+
assert.strictEqual(result.block, true);
|
|
120
|
+
assert.ok(result.reason!.includes('depth_verification'), 'reason should mention depth_verification question id');
|
|
121
|
+
assert.ok(result.reason!.includes('ask_user_questions'), 'reason should mention ask_user_questions tool');
|
|
211
122
|
});
|