gsd-pi 2.29.0-dev.2ccf3fb → 2.29.0-dev.4c155ee
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/headless.js +4 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +31 -0
- package/dist/resources/extensions/gsd/auto-dispatch.ts +32 -3
- package/dist/resources/extensions/gsd/auto-post-unit.ts +39 -10
- package/dist/resources/extensions/gsd/auto-prompts.ts +40 -17
- package/dist/resources/extensions/gsd/auto-recovery.ts +2 -1
- package/dist/resources/extensions/gsd/auto-start.ts +18 -32
- package/dist/resources/extensions/gsd/auto-worktree.ts +21 -182
- package/dist/resources/extensions/gsd/auto.ts +2 -9
- package/dist/resources/extensions/gsd/captures.ts +4 -10
- package/dist/resources/extensions/gsd/commands-handlers.ts +2 -1
- package/dist/resources/extensions/gsd/commands.ts +2 -1
- package/dist/resources/extensions/gsd/detection.ts +2 -1
- package/dist/resources/extensions/gsd/doctor-checks.ts +49 -1
- package/dist/resources/extensions/gsd/doctor-types.ts +3 -1
- package/dist/resources/extensions/gsd/forensics.ts +2 -2
- package/dist/resources/extensions/gsd/git-service.ts +3 -2
- package/dist/resources/extensions/gsd/gitignore.ts +9 -63
- package/dist/resources/extensions/gsd/gsd-db.ts +1 -165
- package/dist/resources/extensions/gsd/guided-flow.ts +8 -5
- package/dist/resources/extensions/gsd/index.ts +3 -3
- package/dist/resources/extensions/gsd/md-importer.ts +3 -2
- package/dist/resources/extensions/gsd/mechanical-completion.ts +430 -0
- package/dist/resources/extensions/gsd/migrate/command.ts +3 -2
- package/dist/resources/extensions/gsd/migrate/writer.ts +2 -1
- package/dist/resources/extensions/gsd/migrate-external.ts +123 -0
- package/dist/resources/extensions/gsd/paths.ts +24 -2
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +6 -5
- package/dist/resources/extensions/gsd/preferences-models.ts +7 -1
- package/dist/resources/extensions/gsd/preferences-validation.ts +2 -1
- package/dist/resources/extensions/gsd/preferences.ts +10 -5
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +4 -2
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +26 -2
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +15 -1
- package/dist/resources/extensions/gsd/repo-identity.ts +148 -0
- package/dist/resources/extensions/gsd/resource-version.ts +99 -0
- package/dist/resources/extensions/gsd/session-forensics.ts +4 -3
- package/dist/resources/extensions/gsd/tests/activity-log.test.ts +2 -2
- package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +3 -3
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +0 -58
- package/dist/resources/extensions/gsd/tests/doctor-runtime.test.ts +3 -4
- package/dist/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +5 -18
- package/dist/resources/extensions/gsd/tests/git-service.test.ts +10 -37
- package/dist/resources/extensions/gsd/tests/knowledge.test.ts +4 -4
- package/dist/resources/extensions/gsd/tests/mechanical-completion.test.ts +356 -0
- package/dist/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/token-profile.test.ts +14 -16
- package/dist/resources/extensions/gsd/triage-resolution.ts +2 -1
- package/dist/resources/extensions/gsd/types.ts +2 -0
- package/dist/resources/extensions/gsd/worktree-command.ts +1 -11
- package/dist/resources/extensions/gsd/worktree-manager.ts +3 -2
- package/dist/resources/extensions/gsd/worktree.ts +42 -5
- package/dist/resources/skills/react-best-practices/SKILL.md +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.js +3 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
- package/packages/pi-coding-agent/src/core/lsp/client.ts +3 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +31 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +32 -3
- package/src/resources/extensions/gsd/auto-post-unit.ts +39 -10
- package/src/resources/extensions/gsd/auto-prompts.ts +40 -17
- package/src/resources/extensions/gsd/auto-recovery.ts +2 -1
- package/src/resources/extensions/gsd/auto-start.ts +18 -32
- package/src/resources/extensions/gsd/auto-worktree.ts +21 -182
- package/src/resources/extensions/gsd/auto.ts +2 -9
- package/src/resources/extensions/gsd/captures.ts +4 -10
- package/src/resources/extensions/gsd/commands-handlers.ts +2 -1
- package/src/resources/extensions/gsd/commands.ts +2 -1
- package/src/resources/extensions/gsd/detection.ts +2 -1
- package/src/resources/extensions/gsd/doctor-checks.ts +49 -1
- package/src/resources/extensions/gsd/doctor-types.ts +3 -1
- package/src/resources/extensions/gsd/forensics.ts +2 -2
- package/src/resources/extensions/gsd/git-service.ts +3 -2
- package/src/resources/extensions/gsd/gitignore.ts +9 -63
- package/src/resources/extensions/gsd/gsd-db.ts +1 -165
- package/src/resources/extensions/gsd/guided-flow.ts +8 -5
- package/src/resources/extensions/gsd/index.ts +3 -3
- package/src/resources/extensions/gsd/md-importer.ts +3 -2
- package/src/resources/extensions/gsd/mechanical-completion.ts +430 -0
- package/src/resources/extensions/gsd/migrate/command.ts +3 -2
- package/src/resources/extensions/gsd/migrate/writer.ts +2 -1
- package/src/resources/extensions/gsd/migrate-external.ts +123 -0
- package/src/resources/extensions/gsd/paths.ts +24 -2
- package/src/resources/extensions/gsd/post-unit-hooks.ts +6 -5
- package/src/resources/extensions/gsd/preferences-models.ts +7 -1
- package/src/resources/extensions/gsd/preferences-validation.ts +2 -1
- package/src/resources/extensions/gsd/preferences.ts +10 -5
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +4 -2
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +26 -2
- package/src/resources/extensions/gsd/prompts/plan-slice.md +15 -1
- package/src/resources/extensions/gsd/repo-identity.ts +148 -0
- package/src/resources/extensions/gsd/resource-version.ts +99 -0
- package/src/resources/extensions/gsd/session-forensics.ts +4 -3
- package/src/resources/extensions/gsd/tests/activity-log.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +0 -58
- package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +3 -4
- package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +5 -18
- package/src/resources/extensions/gsd/tests/git-service.test.ts +10 -37
- package/src/resources/extensions/gsd/tests/knowledge.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +356 -0
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +14 -16
- package/src/resources/extensions/gsd/triage-resolution.ts +2 -1
- package/src/resources/extensions/gsd/types.ts +2 -0
- package/src/resources/extensions/gsd/worktree-command.ts +1 -11
- package/src/resources/extensions/gsd/worktree-manager.ts +3 -2
- package/src/resources/extensions/gsd/worktree.ts +42 -5
- package/src/resources/skills/react-best-practices/SKILL.md +1 -1
- package/dist/resources/extensions/gsd/auto-worktree-sync.ts +0 -199
- package/dist/resources/extensions/gsd/tests/worktree-db-integration.test.ts +0 -205
- package/dist/resources/extensions/gsd/tests/worktree-db.test.ts +0 -442
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +0 -199
- package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +0 -205
- package/src/resources/extensions/gsd/tests/worktree-db.test.ts +0 -442
|
@@ -1,442 +0,0 @@
|
|
|
1
|
-
import { createTestContext } from './test-helpers.ts';
|
|
2
|
-
import * as fs from 'node:fs';
|
|
3
|
-
import * as path from 'node:path';
|
|
4
|
-
import * as os from 'node:os';
|
|
5
|
-
import {
|
|
6
|
-
openDatabase,
|
|
7
|
-
closeDatabase,
|
|
8
|
-
isDbAvailable,
|
|
9
|
-
insertDecision,
|
|
10
|
-
insertRequirement,
|
|
11
|
-
insertArtifact,
|
|
12
|
-
getDecisionById,
|
|
13
|
-
getRequirementById,
|
|
14
|
-
_getAdapter,
|
|
15
|
-
copyWorktreeDb,
|
|
16
|
-
reconcileWorktreeDb,
|
|
17
|
-
} from '../gsd-db.ts';
|
|
18
|
-
|
|
19
|
-
const { assertEq, assertTrue, report } = createTestContext();
|
|
20
|
-
|
|
21
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
22
|
-
// Helpers
|
|
23
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
24
|
-
|
|
25
|
-
function tempDir(): string {
|
|
26
|
-
return fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-wt-test-'));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function cleanup(...dirs: string[]): void {
|
|
30
|
-
closeDatabase();
|
|
31
|
-
for (const dir of dirs) {
|
|
32
|
-
try {
|
|
33
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
34
|
-
} catch {
|
|
35
|
-
// best effort
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function seedMainDb(dbPath: string): void {
|
|
41
|
-
openDatabase(dbPath);
|
|
42
|
-
insertDecision({
|
|
43
|
-
id: 'D001',
|
|
44
|
-
when_context: '2025-01-01',
|
|
45
|
-
scope: 'M001/S01',
|
|
46
|
-
decision: 'Use SQLite',
|
|
47
|
-
choice: 'node:sqlite',
|
|
48
|
-
rationale: 'Built-in',
|
|
49
|
-
revisable: 'yes',
|
|
50
|
-
superseded_by: null,
|
|
51
|
-
});
|
|
52
|
-
insertRequirement({
|
|
53
|
-
id: 'R001',
|
|
54
|
-
class: 'functional',
|
|
55
|
-
status: 'active',
|
|
56
|
-
description: 'Must store decisions',
|
|
57
|
-
why: 'Core feature',
|
|
58
|
-
source: 'design',
|
|
59
|
-
primary_owner: 'S01',
|
|
60
|
-
supporting_slices: '',
|
|
61
|
-
validation: 'test',
|
|
62
|
-
notes: '',
|
|
63
|
-
full_content: 'Full requirement text',
|
|
64
|
-
superseded_by: null,
|
|
65
|
-
});
|
|
66
|
-
insertArtifact({
|
|
67
|
-
path: 'docs/arch.md',
|
|
68
|
-
artifact_type: 'plan',
|
|
69
|
-
milestone_id: 'M001',
|
|
70
|
-
slice_id: null,
|
|
71
|
-
task_id: null,
|
|
72
|
-
full_content: 'Architecture document',
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
77
|
-
// copyWorktreeDb tests
|
|
78
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
79
|
-
|
|
80
|
-
console.log('\n=== worktree-db: copyWorktreeDb ===');
|
|
81
|
-
|
|
82
|
-
// Test: copies DB file and data is queryable
|
|
83
|
-
{
|
|
84
|
-
const srcDir = tempDir();
|
|
85
|
-
const destDir = tempDir();
|
|
86
|
-
const srcDb = path.join(srcDir, 'gsd.db');
|
|
87
|
-
const destDb = path.join(destDir, 'nested', 'gsd.db');
|
|
88
|
-
|
|
89
|
-
seedMainDb(srcDb);
|
|
90
|
-
closeDatabase();
|
|
91
|
-
|
|
92
|
-
const result = copyWorktreeDb(srcDb, destDb);
|
|
93
|
-
assertTrue(result === true, 'copyWorktreeDb returns true on success');
|
|
94
|
-
assertTrue(fs.existsSync(destDb), 'dest DB file exists after copy');
|
|
95
|
-
|
|
96
|
-
// Open the copy and verify data is queryable
|
|
97
|
-
openDatabase(destDb);
|
|
98
|
-
const d = getDecisionById('D001');
|
|
99
|
-
assertTrue(d !== null, 'decision queryable in copied DB');
|
|
100
|
-
assertEq(d?.choice, 'node:sqlite', 'decision data preserved in copy');
|
|
101
|
-
|
|
102
|
-
const r = getRequirementById('R001');
|
|
103
|
-
assertTrue(r !== null, 'requirement queryable in copied DB');
|
|
104
|
-
assertEq(r?.description, 'Must store decisions', 'requirement data preserved in copy');
|
|
105
|
-
|
|
106
|
-
cleanup(srcDir, destDir);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Test: skips -wal and -shm files
|
|
110
|
-
{
|
|
111
|
-
const srcDir = tempDir();
|
|
112
|
-
const destDir = tempDir();
|
|
113
|
-
const srcDb = path.join(srcDir, 'gsd.db');
|
|
114
|
-
const destDb = path.join(destDir, 'gsd.db');
|
|
115
|
-
|
|
116
|
-
seedMainDb(srcDb);
|
|
117
|
-
closeDatabase();
|
|
118
|
-
|
|
119
|
-
// Create fake WAL/SHM files
|
|
120
|
-
fs.writeFileSync(srcDb + '-wal', 'fake wal data');
|
|
121
|
-
fs.writeFileSync(srcDb + '-shm', 'fake shm data');
|
|
122
|
-
|
|
123
|
-
copyWorktreeDb(srcDb, destDb);
|
|
124
|
-
|
|
125
|
-
assertTrue(fs.existsSync(destDb), 'DB file copied');
|
|
126
|
-
assertTrue(!fs.existsSync(destDb + '-wal'), 'WAL file NOT copied');
|
|
127
|
-
assertTrue(!fs.existsSync(destDb + '-shm'), 'SHM file NOT copied');
|
|
128
|
-
|
|
129
|
-
cleanup(srcDir, destDir);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Test: returns false when source doesn't exist (no throw)
|
|
133
|
-
{
|
|
134
|
-
const destDir = tempDir();
|
|
135
|
-
const result = copyWorktreeDb('/nonexistent/path/gsd.db', path.join(destDir, 'gsd.db'));
|
|
136
|
-
assertEq(result, false, 'returns false for missing source');
|
|
137
|
-
cleanup(destDir);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Test: creates dest directory if needed
|
|
141
|
-
{
|
|
142
|
-
const srcDir = tempDir();
|
|
143
|
-
const destDir = tempDir();
|
|
144
|
-
const srcDb = path.join(srcDir, 'gsd.db');
|
|
145
|
-
const deepDest = path.join(destDir, 'a', 'b', 'c', 'gsd.db');
|
|
146
|
-
|
|
147
|
-
seedMainDb(srcDb);
|
|
148
|
-
closeDatabase();
|
|
149
|
-
|
|
150
|
-
const result = copyWorktreeDb(srcDb, deepDest);
|
|
151
|
-
assertTrue(result === true, 'copyWorktreeDb succeeds with nested dest');
|
|
152
|
-
assertTrue(fs.existsSync(deepDest), 'DB file created at deeply nested path');
|
|
153
|
-
|
|
154
|
-
cleanup(srcDir, destDir);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
158
|
-
// reconcileWorktreeDb tests
|
|
159
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
160
|
-
|
|
161
|
-
console.log('\n=== worktree-db: reconcileWorktreeDb ===');
|
|
162
|
-
|
|
163
|
-
// Test: merges new decisions from worktree into main
|
|
164
|
-
{
|
|
165
|
-
const mainDir = tempDir();
|
|
166
|
-
const wtDir = tempDir();
|
|
167
|
-
const mainDb = path.join(mainDir, 'gsd.db');
|
|
168
|
-
const wtDb = path.join(wtDir, 'gsd.db');
|
|
169
|
-
|
|
170
|
-
// Seed main with D001
|
|
171
|
-
seedMainDb(mainDb);
|
|
172
|
-
closeDatabase();
|
|
173
|
-
|
|
174
|
-
// Copy to worktree, add D002 in worktree
|
|
175
|
-
copyWorktreeDb(mainDb, wtDb);
|
|
176
|
-
openDatabase(wtDb);
|
|
177
|
-
insertDecision({
|
|
178
|
-
id: 'D002',
|
|
179
|
-
when_context: '2025-02-01',
|
|
180
|
-
scope: 'M001/S02',
|
|
181
|
-
decision: 'Use WAL mode',
|
|
182
|
-
choice: 'WAL',
|
|
183
|
-
rationale: 'Performance',
|
|
184
|
-
revisable: 'yes',
|
|
185
|
-
superseded_by: null,
|
|
186
|
-
});
|
|
187
|
-
closeDatabase();
|
|
188
|
-
|
|
189
|
-
// Re-open main and reconcile
|
|
190
|
-
openDatabase(mainDb);
|
|
191
|
-
const result = reconcileWorktreeDb(mainDb, wtDb);
|
|
192
|
-
|
|
193
|
-
assertTrue(result.decisions > 0, 'decisions merged count > 0');
|
|
194
|
-
const d2 = getDecisionById('D002');
|
|
195
|
-
assertTrue(d2 !== null, 'D002 from worktree now in main');
|
|
196
|
-
assertEq(d2?.choice, 'WAL', 'D002 data correct after merge');
|
|
197
|
-
|
|
198
|
-
cleanup(mainDir, wtDir);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Test: merges new requirements from worktree into main
|
|
202
|
-
{
|
|
203
|
-
const mainDir = tempDir();
|
|
204
|
-
const wtDir = tempDir();
|
|
205
|
-
const mainDb = path.join(mainDir, 'gsd.db');
|
|
206
|
-
const wtDb = path.join(wtDir, 'gsd.db');
|
|
207
|
-
|
|
208
|
-
seedMainDb(mainDb);
|
|
209
|
-
closeDatabase();
|
|
210
|
-
copyWorktreeDb(mainDb, wtDb);
|
|
211
|
-
|
|
212
|
-
openDatabase(wtDb);
|
|
213
|
-
insertRequirement({
|
|
214
|
-
id: 'R002',
|
|
215
|
-
class: 'non-functional',
|
|
216
|
-
status: 'active',
|
|
217
|
-
description: 'Must be fast',
|
|
218
|
-
why: 'UX',
|
|
219
|
-
source: 'design',
|
|
220
|
-
primary_owner: 'S02',
|
|
221
|
-
supporting_slices: '',
|
|
222
|
-
validation: 'benchmark',
|
|
223
|
-
notes: '',
|
|
224
|
-
full_content: 'Performance requirement',
|
|
225
|
-
superseded_by: null,
|
|
226
|
-
});
|
|
227
|
-
closeDatabase();
|
|
228
|
-
|
|
229
|
-
openDatabase(mainDb);
|
|
230
|
-
const result = reconcileWorktreeDb(mainDb, wtDb);
|
|
231
|
-
|
|
232
|
-
assertTrue(result.requirements > 0, 'requirements merged count > 0');
|
|
233
|
-
const r2 = getRequirementById('R002');
|
|
234
|
-
assertTrue(r2 !== null, 'R002 from worktree now in main');
|
|
235
|
-
assertEq(r2?.description, 'Must be fast', 'R002 data correct after merge');
|
|
236
|
-
|
|
237
|
-
cleanup(mainDir, wtDir);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Test: merges new artifacts from worktree into main
|
|
241
|
-
{
|
|
242
|
-
const mainDir = tempDir();
|
|
243
|
-
const wtDir = tempDir();
|
|
244
|
-
const mainDb = path.join(mainDir, 'gsd.db');
|
|
245
|
-
const wtDb = path.join(wtDir, 'gsd.db');
|
|
246
|
-
|
|
247
|
-
seedMainDb(mainDb);
|
|
248
|
-
closeDatabase();
|
|
249
|
-
copyWorktreeDb(mainDb, wtDb);
|
|
250
|
-
|
|
251
|
-
openDatabase(wtDb);
|
|
252
|
-
insertArtifact({
|
|
253
|
-
path: 'docs/api.md',
|
|
254
|
-
artifact_type: 'reference',
|
|
255
|
-
milestone_id: 'M001',
|
|
256
|
-
slice_id: 'S01',
|
|
257
|
-
task_id: 'T01',
|
|
258
|
-
full_content: 'API documentation',
|
|
259
|
-
});
|
|
260
|
-
closeDatabase();
|
|
261
|
-
|
|
262
|
-
openDatabase(mainDb);
|
|
263
|
-
const result = reconcileWorktreeDb(mainDb, wtDb);
|
|
264
|
-
|
|
265
|
-
assertTrue(result.artifacts > 0, 'artifacts merged count > 0');
|
|
266
|
-
const adapter = _getAdapter()!;
|
|
267
|
-
const row = adapter.prepare('SELECT * FROM artifacts WHERE path = ?').get('docs/api.md');
|
|
268
|
-
assertTrue(row !== null, 'artifact from worktree now in main');
|
|
269
|
-
assertEq(row?.['artifact_type'], 'reference', 'artifact data correct after merge');
|
|
270
|
-
|
|
271
|
-
cleanup(mainDir, wtDir);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Test: detects conflicts (same PK, different content in both DBs)
|
|
275
|
-
{
|
|
276
|
-
const mainDir = tempDir();
|
|
277
|
-
const wtDir = tempDir();
|
|
278
|
-
const mainDb = path.join(mainDir, 'gsd.db');
|
|
279
|
-
const wtDb = path.join(wtDir, 'gsd.db');
|
|
280
|
-
|
|
281
|
-
// Seed main with D001
|
|
282
|
-
seedMainDb(mainDb);
|
|
283
|
-
closeDatabase();
|
|
284
|
-
copyWorktreeDb(mainDb, wtDb);
|
|
285
|
-
|
|
286
|
-
// Modify D001 in main
|
|
287
|
-
openDatabase(mainDb);
|
|
288
|
-
const mainAdapter = _getAdapter()!;
|
|
289
|
-
mainAdapter.prepare(
|
|
290
|
-
`UPDATE decisions SET choice = 'better-sqlite3' WHERE id = 'D001'`,
|
|
291
|
-
).run();
|
|
292
|
-
closeDatabase();
|
|
293
|
-
|
|
294
|
-
// Modify D001 in worktree differently
|
|
295
|
-
openDatabase(wtDb);
|
|
296
|
-
const wtAdapter = _getAdapter()!;
|
|
297
|
-
wtAdapter.prepare(
|
|
298
|
-
`UPDATE decisions SET choice = 'sql.js' WHERE id = 'D001'`,
|
|
299
|
-
).run();
|
|
300
|
-
closeDatabase();
|
|
301
|
-
|
|
302
|
-
// Reconcile
|
|
303
|
-
openDatabase(mainDb);
|
|
304
|
-
const result = reconcileWorktreeDb(mainDb, wtDb);
|
|
305
|
-
|
|
306
|
-
assertTrue(result.conflicts.length > 0, 'conflicts detected');
|
|
307
|
-
assertTrue(
|
|
308
|
-
result.conflicts.some(c => c.includes('D001')),
|
|
309
|
-
'conflict mentions D001',
|
|
310
|
-
);
|
|
311
|
-
|
|
312
|
-
// Worktree-wins: D001 should now have worktree's value
|
|
313
|
-
const d1 = getDecisionById('D001');
|
|
314
|
-
assertEq(d1?.choice, 'sql.js', 'worktree wins on conflict (INSERT OR REPLACE)');
|
|
315
|
-
|
|
316
|
-
cleanup(mainDir, wtDir);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// Test: handles missing worktree DB gracefully
|
|
320
|
-
{
|
|
321
|
-
const mainDir = tempDir();
|
|
322
|
-
const mainDb = path.join(mainDir, 'gsd.db');
|
|
323
|
-
|
|
324
|
-
seedMainDb(mainDb);
|
|
325
|
-
|
|
326
|
-
const result = reconcileWorktreeDb(mainDb, '/nonexistent/worktree.db');
|
|
327
|
-
assertEq(result.decisions, 0, 'no decisions merged for missing worktree DB');
|
|
328
|
-
assertEq(result.requirements, 0, 'no requirements merged for missing worktree DB');
|
|
329
|
-
assertEq(result.artifacts, 0, 'no artifacts merged for missing worktree DB');
|
|
330
|
-
assertEq(result.conflicts.length, 0, 'no conflicts for missing worktree DB');
|
|
331
|
-
|
|
332
|
-
cleanup(mainDir);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Test: path with spaces works
|
|
336
|
-
{
|
|
337
|
-
const baseDir = tempDir();
|
|
338
|
-
const mainDir = path.join(baseDir, 'main dir');
|
|
339
|
-
const wtDir = path.join(baseDir, 'worktree dir');
|
|
340
|
-
fs.mkdirSync(mainDir, { recursive: true });
|
|
341
|
-
fs.mkdirSync(wtDir, { recursive: true });
|
|
342
|
-
|
|
343
|
-
const mainDb = path.join(mainDir, 'gsd.db');
|
|
344
|
-
const wtDb = path.join(wtDir, 'gsd.db');
|
|
345
|
-
|
|
346
|
-
seedMainDb(mainDb);
|
|
347
|
-
closeDatabase();
|
|
348
|
-
copyWorktreeDb(mainDb, wtDb);
|
|
349
|
-
|
|
350
|
-
// Add a decision in worktree
|
|
351
|
-
openDatabase(wtDb);
|
|
352
|
-
insertDecision({
|
|
353
|
-
id: 'D003',
|
|
354
|
-
when_context: '2025-03-01',
|
|
355
|
-
scope: 'M001/S03',
|
|
356
|
-
decision: 'Path spaces test',
|
|
357
|
-
choice: 'yes',
|
|
358
|
-
rationale: 'Robustness',
|
|
359
|
-
revisable: 'no',
|
|
360
|
-
superseded_by: null,
|
|
361
|
-
});
|
|
362
|
-
closeDatabase();
|
|
363
|
-
|
|
364
|
-
openDatabase(mainDb);
|
|
365
|
-
const result = reconcileWorktreeDb(mainDb, wtDb);
|
|
366
|
-
assertTrue(result.decisions > 0, 'reconciliation works with spaces in path');
|
|
367
|
-
const d3 = getDecisionById('D003');
|
|
368
|
-
assertTrue(d3 !== null, 'D003 merged from worktree with spaces in path');
|
|
369
|
-
|
|
370
|
-
cleanup(baseDir);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Test: main DB is usable after reconciliation (DETACH cleanup verified)
|
|
374
|
-
{
|
|
375
|
-
const mainDir = tempDir();
|
|
376
|
-
const wtDir = tempDir();
|
|
377
|
-
const mainDb = path.join(mainDir, 'gsd.db');
|
|
378
|
-
const wtDb = path.join(wtDir, 'gsd.db');
|
|
379
|
-
|
|
380
|
-
seedMainDb(mainDb);
|
|
381
|
-
closeDatabase();
|
|
382
|
-
copyWorktreeDb(mainDb, wtDb);
|
|
383
|
-
|
|
384
|
-
openDatabase(mainDb);
|
|
385
|
-
reconcileWorktreeDb(mainDb, wtDb);
|
|
386
|
-
|
|
387
|
-
// Verify main DB is still fully usable after DETACH
|
|
388
|
-
assertTrue(isDbAvailable(), 'DB still available after reconciliation');
|
|
389
|
-
|
|
390
|
-
insertDecision({
|
|
391
|
-
id: 'D099',
|
|
392
|
-
when_context: '2025-12-01',
|
|
393
|
-
scope: 'test',
|
|
394
|
-
decision: 'Post-reconcile insert',
|
|
395
|
-
choice: 'works',
|
|
396
|
-
rationale: 'Verify DETACH cleanup',
|
|
397
|
-
revisable: 'no',
|
|
398
|
-
superseded_by: null,
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
const d99 = getDecisionById('D099');
|
|
402
|
-
assertTrue(d99 !== null, 'can insert and query after reconciliation');
|
|
403
|
-
assertEq(d99?.choice, 'works', 'post-reconcile data correct');
|
|
404
|
-
|
|
405
|
-
// Verify no "wt" database still attached
|
|
406
|
-
const adapter = _getAdapter()!;
|
|
407
|
-
let wtAccessible = false;
|
|
408
|
-
try {
|
|
409
|
-
adapter.prepare('SELECT count(*) FROM wt.decisions').get();
|
|
410
|
-
wtAccessible = true;
|
|
411
|
-
} catch {
|
|
412
|
-
// Expected — wt should be detached
|
|
413
|
-
}
|
|
414
|
-
assertTrue(!wtAccessible, 'wt database is detached after reconciliation');
|
|
415
|
-
|
|
416
|
-
cleanup(mainDir, wtDir);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// Test: reconcile with empty worktree DB (no new rows, no conflicts)
|
|
420
|
-
{
|
|
421
|
-
const mainDir = tempDir();
|
|
422
|
-
const wtDir = tempDir();
|
|
423
|
-
const mainDb = path.join(mainDir, 'gsd.db');
|
|
424
|
-
const wtDb = path.join(wtDir, 'gsd.db');
|
|
425
|
-
|
|
426
|
-
seedMainDb(mainDb);
|
|
427
|
-
closeDatabase();
|
|
428
|
-
copyWorktreeDb(mainDb, wtDb);
|
|
429
|
-
|
|
430
|
-
// Don't modify the worktree DB at all — reconcile the identical copy
|
|
431
|
-
openDatabase(mainDb);
|
|
432
|
-
const result = reconcileWorktreeDb(mainDb, wtDb);
|
|
433
|
-
|
|
434
|
-
// Should still report counts for the existing rows (INSERT OR REPLACE touches them)
|
|
435
|
-
assertTrue(result.conflicts.length === 0, 'no conflicts when DBs are identical');
|
|
436
|
-
assertTrue(isDbAvailable(), 'DB usable after no-change reconciliation');
|
|
437
|
-
|
|
438
|
-
cleanup(mainDir, wtDir);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// ─── Final Report ──────────────────────────────────────────────────────────
|
|
442
|
-
report();
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Worktree ↔ project root state synchronization for auto-mode.
|
|
3
|
-
*
|
|
4
|
-
* When auto-mode runs inside a worktree, dispatch-critical state files
|
|
5
|
-
* (.gsd/ metadata) diverge between the worktree (where work happens)
|
|
6
|
-
* and the project root (where startAutoMode reads initial state on restart).
|
|
7
|
-
* Without syncing, restarting auto-mode reads stale state from the project
|
|
8
|
-
* root and re-dispatches already-completed units.
|
|
9
|
-
*
|
|
10
|
-
* Also contains resource staleness detection and stale worktree escape.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { existsSync, mkdirSync, readFileSync, cpSync, unlinkSync, readdirSync } from "node:fs";
|
|
14
|
-
import { loadJsonFileOrNull } from "./json-persistence.js";
|
|
15
|
-
import { join, sep as pathSep } from "node:path";
|
|
16
|
-
import { homedir } from "node:os";
|
|
17
|
-
import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
|
|
18
|
-
import { atomicWriteSync } from "./atomic-write.js";
|
|
19
|
-
|
|
20
|
-
// ─── Project Root → Worktree Sync ─────────────────────────────────────────
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Sync milestone artifacts from project root INTO worktree before deriveState.
|
|
24
|
-
* Covers the case where the LLM wrote artifacts to the main repo filesystem
|
|
25
|
-
* (e.g. via absolute paths) but the worktree has stale data. Also deletes
|
|
26
|
-
* gsd.db in the worktree so it rebuilds from fresh disk state (#853).
|
|
27
|
-
* Non-fatal — sync failure should never block dispatch.
|
|
28
|
-
*/
|
|
29
|
-
export function syncProjectRootToWorktree(projectRoot: string, worktreePath: string, milestoneId: string | null): void {
|
|
30
|
-
if (!worktreePath || !projectRoot || worktreePath === projectRoot) return;
|
|
31
|
-
if (!milestoneId) return;
|
|
32
|
-
|
|
33
|
-
const prGsd = join(projectRoot, ".gsd");
|
|
34
|
-
const wtGsd = join(worktreePath, ".gsd");
|
|
35
|
-
|
|
36
|
-
// Copy milestone directory from project root to worktree if the project root
|
|
37
|
-
// has newer artifacts (e.g. slices that don't exist in the worktree yet)
|
|
38
|
-
safeCopyRecursive(join(prGsd, "milestones", milestoneId), join(wtGsd, "milestones", milestoneId))
|
|
39
|
-
|
|
40
|
-
// Copy living documents from project root to worktree so agents have the
|
|
41
|
-
// latest decisions, requirements, project state, and knowledge.
|
|
42
|
-
for (const doc of ["DECISIONS.md", "REQUIREMENTS.md", "PROJECT.md", "KNOWLEDGE.md"]) {
|
|
43
|
-
safeCopy(join(prGsd, doc), join(wtGsd, doc), { force: true });
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Delete worktree gsd.db so it rebuilds from the freshly synced files.
|
|
47
|
-
// Stale DB rows are the root cause of the infinite skip loop (#853).
|
|
48
|
-
try {
|
|
49
|
-
const wtDb = join(wtGsd, "gsd.db");
|
|
50
|
-
if (existsSync(wtDb)) {
|
|
51
|
-
unlinkSync(wtDb);
|
|
52
|
-
}
|
|
53
|
-
} catch { /* non-fatal */ }
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// ─── Worktree → Project Root Sync ─────────────────────────────────────────
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Sync dispatch-critical .gsd/ state files from worktree to project root.
|
|
60
|
-
* Only runs when inside an auto-worktree (worktreePath differs from projectRoot).
|
|
61
|
-
* Copies: STATE.md + active milestone directory (roadmap, slice plans, task summaries).
|
|
62
|
-
* Non-fatal — sync failure should never block dispatch.
|
|
63
|
-
*/
|
|
64
|
-
export function syncStateToProjectRoot(worktreePath: string, projectRoot: string, milestoneId: string | null): void {
|
|
65
|
-
if (!worktreePath || !projectRoot || worktreePath === projectRoot) return;
|
|
66
|
-
if (!milestoneId) return;
|
|
67
|
-
|
|
68
|
-
const wtGsd = join(worktreePath, ".gsd");
|
|
69
|
-
const prGsd = join(projectRoot, ".gsd");
|
|
70
|
-
|
|
71
|
-
// 1. STATE.md — the quick-glance status used by initial deriveState()
|
|
72
|
-
safeCopy(join(wtGsd, "STATE.md"), join(prGsd, "STATE.md"), { force: true })
|
|
73
|
-
|
|
74
|
-
// 2. Milestone directory — ROADMAP, slice PLANs, task summaries
|
|
75
|
-
// Copy the entire milestone .gsd subtree so deriveState reads current checkboxes
|
|
76
|
-
safeCopyRecursive(join(wtGsd, "milestones", milestoneId), join(prGsd, "milestones", milestoneId), { force: true })
|
|
77
|
-
|
|
78
|
-
// 3. Merge completed-units.json (set-union of both locations)
|
|
79
|
-
// Prevents already-completed units from being re-dispatched after crash/restart.
|
|
80
|
-
const srcKeysFile = join(wtGsd, "completed-units.json");
|
|
81
|
-
const dstKeysFile = join(prGsd, "completed-units.json");
|
|
82
|
-
if (existsSync(srcKeysFile)) {
|
|
83
|
-
try {
|
|
84
|
-
const srcKeys: string[] = JSON.parse(readFileSync(srcKeysFile, "utf8"));
|
|
85
|
-
let dstKeys: string[] = [];
|
|
86
|
-
if (existsSync(dstKeysFile)) {
|
|
87
|
-
try { dstKeys = JSON.parse(readFileSync(dstKeysFile, "utf8")); } catch { /* ignore corrupt dst */ }
|
|
88
|
-
}
|
|
89
|
-
const merged = [...new Set([...dstKeys, ...srcKeys])];
|
|
90
|
-
atomicWriteSync(dstKeysFile, JSON.stringify(merged, null, 2));
|
|
91
|
-
} catch { /* non-fatal */ }
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// 4. Runtime records — unit dispatch state used by selfHealRuntimeRecords().
|
|
95
|
-
// Without this, a crash during a unit leaves the runtime record only in the
|
|
96
|
-
// worktree. If the next session resolves basePath before worktree re-entry,
|
|
97
|
-
// selfHeal can't find or clear the stale record (#769).
|
|
98
|
-
safeCopyRecursive(join(wtGsd, "runtime", "units"), join(prGsd, "runtime", "units"), { force: true })
|
|
99
|
-
|
|
100
|
-
// 5. Living documents — decisions, requirements, project description, knowledge.
|
|
101
|
-
// Agents update these during slice execution. Without syncing, a new session
|
|
102
|
-
// reads stale copies from the project root, losing architectural decisions,
|
|
103
|
-
// requirement status updates, and accumulated knowledge (#1168).
|
|
104
|
-
for (const doc of ["DECISIONS.md", "REQUIREMENTS.md", "PROJECT.md", "KNOWLEDGE.md"]) {
|
|
105
|
-
safeCopy(join(wtGsd, doc), join(prGsd, doc), { force: true });
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ─── Resource Staleness ───────────────────────────────────────────────────
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Read the resource version (semver) from the managed-resources manifest.
|
|
113
|
-
* Uses gsdVersion instead of syncedAt so that launching a second session
|
|
114
|
-
* doesn't falsely trigger staleness (#804).
|
|
115
|
-
*/
|
|
116
|
-
function isManifestWithVersion(data: unknown): data is { gsdVersion: string } {
|
|
117
|
-
return data !== null && typeof data === "object" && "gsdVersion" in data! && typeof (data as Record<string, unknown>).gsdVersion === "string";
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function readResourceVersion(): string | null {
|
|
121
|
-
const agentDir = process.env.GSD_CODING_AGENT_DIR || join(homedir(), ".gsd", "agent");
|
|
122
|
-
const manifestPath = join(agentDir, "managed-resources.json");
|
|
123
|
-
const manifest = loadJsonFileOrNull(manifestPath, isManifestWithVersion);
|
|
124
|
-
return manifest?.gsdVersion ?? null;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Check if managed resources have been updated since session start.
|
|
129
|
-
* Returns a warning message if stale, null otherwise.
|
|
130
|
-
*/
|
|
131
|
-
export function checkResourcesStale(versionOnStart: string | null): string | null {
|
|
132
|
-
if (versionOnStart === null) return null;
|
|
133
|
-
const current = readResourceVersion();
|
|
134
|
-
if (current === null) return null;
|
|
135
|
-
if (current !== versionOnStart) {
|
|
136
|
-
return "GSD resources were updated since this session started. Restart gsd to load the new code.";
|
|
137
|
-
}
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// ─── Stale Worktree Escape ────────────────────────────────────────────────
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Detect and escape a stale worktree cwd (#608).
|
|
145
|
-
*
|
|
146
|
-
* After milestone completion + merge, the worktree directory is removed but
|
|
147
|
-
* the process cwd may still point inside `.gsd/worktrees/<MID>/`.
|
|
148
|
-
* When a new session starts, `process.cwd()` is passed as `base` to startAuto
|
|
149
|
-
* and all subsequent writes land in the wrong directory. This function detects
|
|
150
|
-
* that scenario and chdir back to the project root.
|
|
151
|
-
*
|
|
152
|
-
* Returns the corrected base path.
|
|
153
|
-
*/
|
|
154
|
-
export function escapeStaleWorktree(base: string): string {
|
|
155
|
-
const marker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
|
|
156
|
-
const idx = base.indexOf(marker);
|
|
157
|
-
if (idx === -1) return base;
|
|
158
|
-
|
|
159
|
-
// base is inside .gsd/worktrees/<something> — extract the project root
|
|
160
|
-
const projectRoot = base.slice(0, idx);
|
|
161
|
-
try {
|
|
162
|
-
process.chdir(projectRoot);
|
|
163
|
-
} catch {
|
|
164
|
-
// If chdir fails, return the original — caller will handle errors downstream
|
|
165
|
-
return base;
|
|
166
|
-
}
|
|
167
|
-
return projectRoot;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Clean stale runtime unit files for completed milestones.
|
|
172
|
-
*
|
|
173
|
-
* After restart, stale runtime/units/*.json from prior milestones can
|
|
174
|
-
* cause deriveState to resume the wrong milestone (#887). Removes files
|
|
175
|
-
* for milestones that have a SUMMARY (fully complete).
|
|
176
|
-
*/
|
|
177
|
-
export function cleanStaleRuntimeUnits(
|
|
178
|
-
gsdRootPath: string,
|
|
179
|
-
hasMilestoneSummary: (mid: string) => boolean,
|
|
180
|
-
): number {
|
|
181
|
-
const runtimeUnitsDir = join(gsdRootPath, "runtime", "units");
|
|
182
|
-
if (!existsSync(runtimeUnitsDir)) return 0;
|
|
183
|
-
|
|
184
|
-
let cleaned = 0;
|
|
185
|
-
try {
|
|
186
|
-
for (const file of readdirSync(runtimeUnitsDir)) {
|
|
187
|
-
if (!file.endsWith(".json")) continue;
|
|
188
|
-
const midMatch = file.match(/(M\d+(?:-[a-z0-9]{6})?)/);
|
|
189
|
-
if (!midMatch) continue;
|
|
190
|
-
if (hasMilestoneSummary(midMatch[1])) {
|
|
191
|
-
try {
|
|
192
|
-
unlinkSync(join(runtimeUnitsDir, file));
|
|
193
|
-
cleaned++;
|
|
194
|
-
} catch { /* non-fatal */ }
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
} catch { /* non-fatal */ }
|
|
198
|
-
return cleaned;
|
|
199
|
-
}
|