gsd-pi 2.33.0-dev.69bff0f → 2.33.0-dev.bafba33

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.
Files changed (26) hide show
  1. package/README.md +18 -13
  2. package/dist/resources/extensions/gsd/auto-supervisor.ts +5 -10
  3. package/dist/resources/extensions/gsd/auto-worktree.ts +1 -135
  4. package/dist/resources/extensions/gsd/commands.ts +2 -14
  5. package/dist/resources/extensions/gsd/session-lock.ts +16 -80
  6. package/dist/resources/extensions/gsd/tests/loop-regression.test.ts +1 -39
  7. package/dist/resources/extensions/gsd/tests/session-lock.test.ts +0 -119
  8. package/dist/resources/extensions/mcporter/extension-manifest.json +12 -0
  9. package/package.json +1 -1
  10. package/src/resources/extensions/gsd/auto-supervisor.ts +5 -10
  11. package/src/resources/extensions/gsd/auto-worktree.ts +1 -135
  12. package/src/resources/extensions/gsd/commands.ts +2 -14
  13. package/src/resources/extensions/gsd/session-lock.ts +16 -80
  14. package/src/resources/extensions/gsd/tests/loop-regression.test.ts +1 -39
  15. package/src/resources/extensions/gsd/tests/session-lock.test.ts +0 -119
  16. package/src/resources/extensions/mcporter/extension-manifest.json +12 -0
  17. package/dist/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +0 -691
  18. package/dist/resources/extensions/gsd/tests/cache-staleness-regression.test.ts +0 -317
  19. package/dist/resources/extensions/gsd/tests/roadmap-parse-regression.test.ts +0 -358
  20. package/dist/resources/extensions/gsd/tests/session-lock-regression.test.ts +0 -216
  21. package/dist/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +0 -206
  22. package/src/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +0 -691
  23. package/src/resources/extensions/gsd/tests/cache-staleness-regression.test.ts +0 -317
  24. package/src/resources/extensions/gsd/tests/roadmap-parse-regression.test.ts +0 -358
  25. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +0 -216
  26. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +0 -206
@@ -1,216 +0,0 @@
1
- /**
2
- * session-lock-regression.test.ts — Regression tests for session lock lifecycle.
3
- *
4
- * Regression coverage for:
5
- * #1257 False-positive "Session lock lost" during auto-mode
6
- * #1245 Stranded .gsd.lock/ directory preventing new sessions
7
- * #1251 Same root cause as #1245
8
- *
9
- * Tests the acquire → validate → release lifecycle and edge cases
10
- * without requiring concurrent processes.
11
- */
12
-
13
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, readFileSync } from 'node:fs';
14
- import { join } from 'node:path';
15
- import { tmpdir } from 'node:os';
16
-
17
- import {
18
- acquireSessionLock,
19
- validateSessionLock,
20
- releaseSessionLock,
21
- readSessionLockData,
22
- updateSessionLock,
23
- isSessionLockHeld,
24
- } from '../session-lock.ts';
25
- import { gsdRoot } from '../paths.ts';
26
- import { createTestContext } from './test-helpers.ts';
27
-
28
- const { assertEq, assertTrue, report } = createTestContext();
29
-
30
- async function main(): Promise<void> {
31
-
32
- // ─── 1. Basic acquire/release lifecycle ───────────────────────────────
33
- console.log('\n=== 1. acquire → validate → release lifecycle ===');
34
- {
35
- const base = mkdtempSync(join(tmpdir(), 'gsd-session-lock-'));
36
- mkdirSync(join(base, '.gsd'), { recursive: true });
37
-
38
- try {
39
- const result = acquireSessionLock(base);
40
- assertTrue(result.acquired, 'lock acquired successfully');
41
-
42
- const valid = validateSessionLock(base);
43
- assertTrue(valid, 'lock validates after acquisition');
44
-
45
- assertTrue(isSessionLockHeld(base), 'isSessionLockHeld returns true');
46
-
47
- releaseSessionLock(base);
48
-
49
- // After release, the lock file should be cleaned up
50
- const lockFile = join(gsdRoot(base), 'auto.lock');
51
- assertTrue(!existsSync(lockFile), 'lock file removed after release');
52
-
53
- // The .gsd.lock/ directory should be cleaned up
54
- const lockDir = gsdRoot(base) + '.lock';
55
- assertTrue(!existsSync(lockDir), '.gsd.lock/ directory removed after release (#1245)');
56
- } finally {
57
- rmSync(base, { recursive: true, force: true });
58
- }
59
- }
60
-
61
- // ─── 2. Double release is safe ────────────────────────────────────────
62
- console.log('\n=== 2. double release does not throw ===');
63
- {
64
- const base = mkdtempSync(join(tmpdir(), 'gsd-session-lock-'));
65
- mkdirSync(join(base, '.gsd'), { recursive: true });
66
-
67
- try {
68
- acquireSessionLock(base);
69
- releaseSessionLock(base);
70
- // Second release should not throw
71
- let threw = false;
72
- try {
73
- releaseSessionLock(base);
74
- } catch {
75
- threw = true;
76
- }
77
- assertTrue(!threw, 'double release does not throw');
78
- } finally {
79
- rmSync(base, { recursive: true, force: true });
80
- }
81
- }
82
-
83
- // ─── 3. updateSessionLock preserves lock data ─────────────────────────
84
- console.log('\n=== 3. updateSessionLock writes metadata ===');
85
- {
86
- const base = mkdtempSync(join(tmpdir(), 'gsd-session-lock-'));
87
- mkdirSync(join(base, '.gsd'), { recursive: true });
88
-
89
- try {
90
- acquireSessionLock(base);
91
-
92
- updateSessionLock(base, 'execute-task', 'M001/S01/T01', 5, '/tmp/session.json');
93
-
94
- const data = readSessionLockData(base);
95
- assertTrue(data !== null, 'lock data readable after update');
96
- if (data) {
97
- assertEq(data.pid, process.pid, 'lock data has correct PID');
98
- assertEq(data.unitType, 'execute-task', 'lock data has correct unit type');
99
- assertEq(data.unitId, 'M001/S01/T01', 'lock data has correct unit ID');
100
- assertEq(data.completedUnits, 5, 'lock data has correct completed count');
101
- assertEq(data.sessionFile, '/tmp/session.json', 'lock data has session file');
102
- }
103
-
104
- releaseSessionLock(base);
105
- } finally {
106
- rmSync(base, { recursive: true, force: true });
107
- }
108
- }
109
-
110
- // ─── 4. Stale lock from dead PID → re-acquirable (#1245) ─────────────
111
- console.log('\n=== 4. stale lock from dead PID → re-acquirable ===');
112
- {
113
- const base = mkdtempSync(join(tmpdir(), 'gsd-session-lock-'));
114
- mkdirSync(join(base, '.gsd'), { recursive: true });
115
-
116
- try {
117
- // Write a lock file with a definitely-dead PID
118
- const lockFile = join(gsdRoot(base), 'auto.lock');
119
- const staleLock = {
120
- pid: 99999999, // extremely unlikely to be alive
121
- startedAt: new Date(Date.now() - 3600000).toISOString(),
122
- unitType: 'execute-task',
123
- unitId: 'M001/S01/T01',
124
- unitStartedAt: new Date(Date.now() - 3600000).toISOString(),
125
- completedUnits: 3,
126
- };
127
- writeFileSync(lockFile, JSON.stringify(staleLock, null, 2));
128
-
129
- // Should be able to acquire despite the stale lock
130
- const result = acquireSessionLock(base);
131
- assertTrue(result.acquired, '#1245: stale lock from dead PID → re-acquirable');
132
-
133
- releaseSessionLock(base);
134
- } finally {
135
- rmSync(base, { recursive: true, force: true });
136
- }
137
- }
138
-
139
- // ─── 5. readSessionLockData with no lock → null ───────────────────────
140
- console.log('\n=== 5. readSessionLockData with no lock → null ===');
141
- {
142
- const base = mkdtempSync(join(tmpdir(), 'gsd-session-lock-'));
143
- mkdirSync(join(base, '.gsd'), { recursive: true });
144
-
145
- try {
146
- const data = readSessionLockData(base);
147
- assertEq(data, null, 'no lock file → null');
148
- } finally {
149
- rmSync(base, { recursive: true, force: true });
150
- }
151
- }
152
-
153
- // ─── 6. validateSessionLock after own acquisition → true ──────────────
154
- console.log('\n=== 6. validateSessionLock after own acquisition → true ===');
155
- {
156
- const base = mkdtempSync(join(tmpdir(), 'gsd-session-lock-'));
157
- mkdirSync(join(base, '.gsd'), { recursive: true });
158
-
159
- try {
160
- acquireSessionLock(base);
161
-
162
- // Multiple validations should all return true (regression for #1257)
163
- for (let i = 0; i < 5; i++) {
164
- const valid = validateSessionLock(base);
165
- assertTrue(valid, `#1257: validation ${i + 1} returns true for own lock`);
166
- }
167
-
168
- releaseSessionLock(base);
169
- } finally {
170
- rmSync(base, { recursive: true, force: true });
171
- }
172
- }
173
-
174
- // ─── 7. readSessionLockData with corrupt JSON → null ──────────────────
175
- console.log('\n=== 7. corrupt lock file → null ===');
176
- {
177
- const base = mkdtempSync(join(tmpdir(), 'gsd-session-lock-'));
178
- mkdirSync(join(base, '.gsd'), { recursive: true });
179
-
180
- try {
181
- const lockFile = join(gsdRoot(base), 'auto.lock');
182
- writeFileSync(lockFile, 'NOT VALID JSON {{{');
183
-
184
- const data = readSessionLockData(base);
185
- assertEq(data, null, 'corrupt JSON → null');
186
- } finally {
187
- rmSync(base, { recursive: true, force: true });
188
- }
189
- }
190
-
191
- // ─── 8. Acquire after release is possible ─────────────────────────────
192
- console.log('\n=== 8. acquire after release → re-acquirable ===');
193
- {
194
- const base = mkdtempSync(join(tmpdir(), 'gsd-session-lock-'));
195
- mkdirSync(join(base, '.gsd'), { recursive: true });
196
-
197
- try {
198
- const r1 = acquireSessionLock(base);
199
- assertTrue(r1.acquired, 'first acquisition');
200
- releaseSessionLock(base);
201
-
202
- const r2 = acquireSessionLock(base);
203
- assertTrue(r2.acquired, 're-acquisition after release');
204
- releaseSessionLock(base);
205
- } finally {
206
- rmSync(base, { recursive: true, force: true });
207
- }
208
- }
209
-
210
- report();
211
- }
212
-
213
- main().catch((error) => {
214
- console.error(error);
215
- process.exit(1);
216
- });
@@ -1,206 +0,0 @@
1
- /**
2
- * worktree-sync-milestones.test.ts — Regression test for #1311.
3
- *
4
- * Verifies that syncGsdStateToWorktree copies missing milestones,
5
- * milestone files, and slice directories from the main repo's .gsd/
6
- * into the worktree's .gsd/.
7
- *
8
- * Covers:
9
- * - Entirely missing milestone directory
10
- * - Milestone exists but missing CONTEXT/ROADMAP files
11
- * - Missing slices within an existing milestone
12
- * - No-op when directories are identical (symlinked)
13
- * - Root-level files (DECISIONS, REQUIREMENTS, etc.)
14
- */
15
-
16
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, readFileSync, symlinkSync, realpathSync } from 'node:fs';
17
- import { join } from 'node:path';
18
- import { tmpdir } from 'node:os';
19
-
20
- import { syncGsdStateToWorktree } from '../auto-worktree.ts';
21
- import { createTestContext } from './test-helpers.ts';
22
-
23
- const { assertEq, assertTrue, report } = createTestContext();
24
-
25
- function createBase(name: string): string {
26
- const base = mkdtempSync(join(tmpdir(), `gsd-wt-sync-${name}-`));
27
- mkdirSync(join(base, '.gsd', 'milestones'), { recursive: true });
28
- return base;
29
- }
30
-
31
- function cleanup(base: string): void {
32
- rmSync(base, { recursive: true, force: true });
33
- }
34
-
35
- async function main(): Promise<void> {
36
-
37
- // ─── 1. Missing milestone directory is synced ─────────────────────────
38
- console.log('\n=== 1. missing milestone directory is copied from main ===');
39
- {
40
- const mainBase = createBase('main');
41
- const wtBase = createBase('wt');
42
-
43
- try {
44
- // Main repo has M001 and M002
45
- const m001Dir = join(mainBase, '.gsd', 'milestones', 'M001');
46
- mkdirSync(m001Dir, { recursive: true });
47
- writeFileSync(join(m001Dir, 'M001-CONTEXT.md'), '# M001\nDone.');
48
- writeFileSync(join(m001Dir, 'M001-ROADMAP.md'), '# Roadmap');
49
-
50
- const m002Dir = join(mainBase, '.gsd', 'milestones', 'M002');
51
- mkdirSync(m002Dir, { recursive: true });
52
- writeFileSync(join(m002Dir, 'M002-CONTEXT.md'), '# M002\nNew milestone.');
53
- writeFileSync(join(m002Dir, 'M002-ROADMAP.md'), '# Roadmap');
54
-
55
- // Worktree only has M001
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');
62
-
63
- const result = syncGsdStateToWorktree(mainBase, wtBase);
64
-
65
- assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M002')), '#1311: M002 synced to worktree');
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');
69
- } finally {
70
- cleanup(mainBase);
71
- cleanup(wtBase);
72
- }
73
- }
74
-
75
- // ─── 2. Missing files within existing milestone ───────────────────────
76
- console.log('\n=== 2. missing files within existing milestone are synced ===');
77
- {
78
- const mainBase = createBase('main');
79
- const wtBase = createBase('wt');
80
-
81
- try {
82
- // Main repo M001 has CONTEXT, ROADMAP, RESEARCH
83
- const m001Dir = join(mainBase, '.gsd', 'milestones', 'M001');
84
- mkdirSync(m001Dir, { recursive: true });
85
- writeFileSync(join(m001Dir, 'M001-CONTEXT.md'), '# M001 Context');
86
- writeFileSync(join(m001Dir, 'M001-ROADMAP.md'), '# M001 Roadmap');
87
- writeFileSync(join(m001Dir, 'M001-RESEARCH.md'), '# M001 Research');
88
-
89
- // Worktree M001 only has CONTEXT (stale snapshot)
90
- const wtM001Dir = join(wtBase, '.gsd', 'milestones', 'M001');
91
- mkdirSync(wtM001Dir, { recursive: true });
92
- writeFileSync(join(wtM001Dir, 'M001-CONTEXT.md'), '# M001 Context');
93
-
94
- const result = syncGsdStateToWorktree(mainBase, wtBase);
95
-
96
- assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'M001-ROADMAP.md')), 'ROADMAP synced');
97
- assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'M001-RESEARCH.md')), 'RESEARCH synced');
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
- );
104
- } finally {
105
- cleanup(mainBase);
106
- cleanup(wtBase);
107
- }
108
- }
109
-
110
- // ─── 3. Missing slices directory synced ───────────────────────────────
111
- console.log('\n=== 3. missing slices directory synced ===');
112
- {
113
- const mainBase = createBase('main');
114
- const wtBase = createBase('wt');
115
-
116
- try {
117
- // Main repo has M001 with slices S01–S03
118
- const m001Dir = join(mainBase, '.gsd', 'milestones', 'M001');
119
- mkdirSync(join(m001Dir, 'slices', 'S01'), { recursive: true });
120
- mkdirSync(join(m001Dir, 'slices', 'S02'), { recursive: true });
121
- mkdirSync(join(m001Dir, 'slices', 'S03'), { recursive: true });
122
- 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
-
127
- // Worktree M001 has slices S01–S02 only (S03 missing)
128
- const wtM001Dir = join(wtBase, '.gsd', 'milestones', 'M001');
129
- mkdirSync(join(wtM001Dir, 'slices', 'S01'), { recursive: true });
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');
134
-
135
- assertTrue(!existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'slices', 'S03')), 'S03 missing before sync');
136
-
137
- syncGsdStateToWorktree(mainBase, wtBase);
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');
141
- } finally {
142
- cleanup(mainBase);
143
- cleanup(wtBase);
144
- }
145
- }
146
-
147
- // ─── 4. No-op when both resolve to same directory (symlink) ───────────
148
- console.log('\n=== 4. no-op when .gsd/ resolves to same path (symlinked) ===');
149
- {
150
- const sharedDir = createBase('shared');
151
- const mainBase = mkdtempSync(join(tmpdir(), 'gsd-wt-sync-main-'));
152
- const wtBase = mkdtempSync(join(tmpdir(), 'gsd-wt-sync-wt-'));
153
-
154
- try {
155
- // Both main and worktree symlink to the same shared directory
156
- writeFileSync(join(sharedDir, '.gsd', 'milestones', 'keep'), '');
157
- symlinkSync(join(sharedDir, '.gsd'), join(mainBase, '.gsd'));
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');
162
- } finally {
163
- cleanup(sharedDir);
164
- rmSync(mainBase, { recursive: true, force: true });
165
- rmSync(wtBase, { recursive: true, force: true });
166
- }
167
- }
168
-
169
- // ─── 5. Root-level .gsd/ files synced ─────────────────────────────────
170
- console.log('\n=== 5. root-level .gsd/ files synced ===');
171
- {
172
- const mainBase = createBase('main');
173
- const wtBase = createBase('wt');
174
-
175
- try {
176
- writeFileSync(join(mainBase, '.gsd', 'DECISIONS.md'), '# Decisions');
177
- writeFileSync(join(mainBase, '.gsd', 'REQUIREMENTS.md'), '# Requirements');
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');
187
- } finally {
188
- cleanup(mainBase);
189
- cleanup(wtBase);
190
- }
191
- }
192
-
193
- // ─── 6. Non-existent directories handled gracefully ───────────────────
194
- console.log('\n=== 6. non-existent directories → no-op ===');
195
- {
196
- const result = syncGsdStateToWorktree('/tmp/does-not-exist-main', '/tmp/does-not-exist-wt');
197
- assertEq(result.synced.length, 0, 'no crash on missing directories');
198
- }
199
-
200
- report();
201
- }
202
-
203
- main().catch((error) => {
204
- console.error(error);
205
- process.exit(1);
206
- });