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.
Files changed (135) hide show
  1. package/dist/bundled-resource-path.d.ts +8 -0
  2. package/dist/bundled-resource-path.js +14 -0
  3. package/dist/headless-query.js +6 -6
  4. package/dist/resources/extensions/gsd/auto/session.js +27 -32
  5. package/dist/resources/extensions/gsd/auto-dashboard.js +29 -109
  6. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +6 -1
  7. package/dist/resources/extensions/gsd/auto-dispatch.js +52 -81
  8. package/dist/resources/extensions/gsd/auto-loop.js +956 -0
  9. package/dist/resources/extensions/gsd/auto-observability.js +4 -2
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +75 -185
  11. package/dist/resources/extensions/gsd/auto-prompts.js +133 -101
  12. package/dist/resources/extensions/gsd/auto-recovery.js +59 -97
  13. package/dist/resources/extensions/gsd/auto-start.js +330 -309
  14. package/dist/resources/extensions/gsd/auto-supervisor.js +5 -11
  15. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +7 -7
  16. package/dist/resources/extensions/gsd/auto-timers.js +3 -4
  17. package/dist/resources/extensions/gsd/auto-verification.js +35 -73
  18. package/dist/resources/extensions/gsd/auto-worktree-sync.js +167 -0
  19. package/dist/resources/extensions/gsd/auto-worktree.js +291 -126
  20. package/dist/resources/extensions/gsd/auto.js +283 -1013
  21. package/dist/resources/extensions/gsd/captures.js +10 -4
  22. package/dist/resources/extensions/gsd/dispatch-guard.js +7 -8
  23. package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -18
  24. package/dist/resources/extensions/gsd/doctor-checks.js +3 -4
  25. package/dist/resources/extensions/gsd/git-service.js +1 -1
  26. package/dist/resources/extensions/gsd/gsd-db.js +296 -151
  27. package/dist/resources/extensions/gsd/index.js +92 -228
  28. package/dist/resources/extensions/gsd/post-unit-hooks.js +13 -13
  29. package/dist/resources/extensions/gsd/progress-score.js +61 -156
  30. package/dist/resources/extensions/gsd/quick.js +98 -122
  31. package/dist/resources/extensions/gsd/session-lock.js +13 -0
  32. package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
  33. package/dist/resources/extensions/gsd/undo.js +43 -48
  34. package/dist/resources/extensions/gsd/unit-runtime.js +16 -15
  35. package/dist/resources/extensions/gsd/verification-evidence.js +0 -1
  36. package/dist/resources/extensions/gsd/verification-gate.js +6 -35
  37. package/dist/resources/extensions/gsd/worktree-command.js +30 -24
  38. package/dist/resources/extensions/gsd/worktree-manager.js +2 -3
  39. package/dist/resources/extensions/gsd/worktree-resolver.js +344 -0
  40. package/dist/resources/extensions/gsd/worktree.js +7 -44
  41. package/dist/tool-bootstrap.js +59 -11
  42. package/dist/worktree-cli.js +7 -7
  43. package/package.json +1 -1
  44. package/packages/pi-ai/dist/models.generated.d.ts +3630 -5483
  45. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  46. package/packages/pi-ai/dist/models.generated.js +735 -2588
  47. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  48. package/packages/pi-ai/src/models.generated.ts +1039 -2892
  49. package/packages/pi-coding-agent/package.json +1 -1
  50. package/pkg/package.json +1 -1
  51. package/src/resources/extensions/gsd/auto/session.ts +47 -30
  52. package/src/resources/extensions/gsd/auto-dashboard.ts +28 -131
  53. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +6 -1
  54. package/src/resources/extensions/gsd/auto-dispatch.ts +135 -91
  55. package/src/resources/extensions/gsd/auto-loop.ts +1665 -0
  56. package/src/resources/extensions/gsd/auto-observability.ts +4 -2
  57. package/src/resources/extensions/gsd/auto-post-unit.ts +85 -228
  58. package/src/resources/extensions/gsd/auto-prompts.ts +138 -109
  59. package/src/resources/extensions/gsd/auto-recovery.ts +124 -118
  60. package/src/resources/extensions/gsd/auto-start.ts +440 -354
  61. package/src/resources/extensions/gsd/auto-supervisor.ts +5 -12
  62. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +8 -8
  63. package/src/resources/extensions/gsd/auto-timers.ts +3 -4
  64. package/src/resources/extensions/gsd/auto-verification.ts +76 -90
  65. package/src/resources/extensions/gsd/auto-worktree-sync.ts +204 -0
  66. package/src/resources/extensions/gsd/auto-worktree.ts +389 -141
  67. package/src/resources/extensions/gsd/auto.ts +515 -1199
  68. package/src/resources/extensions/gsd/captures.ts +10 -4
  69. package/src/resources/extensions/gsd/dispatch-guard.ts +13 -9
  70. package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -18
  71. package/src/resources/extensions/gsd/doctor-checks.ts +3 -4
  72. package/src/resources/extensions/gsd/git-service.ts +8 -1
  73. package/src/resources/extensions/gsd/gitignore.ts +4 -2
  74. package/src/resources/extensions/gsd/gsd-db.ts +375 -180
  75. package/src/resources/extensions/gsd/index.ts +104 -263
  76. package/src/resources/extensions/gsd/post-unit-hooks.ts +13 -13
  77. package/src/resources/extensions/gsd/progress-score.ts +65 -200
  78. package/src/resources/extensions/gsd/quick.ts +121 -125
  79. package/src/resources/extensions/gsd/session-lock.ts +11 -0
  80. package/src/resources/extensions/gsd/templates/preferences.md +1 -0
  81. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +32 -59
  82. package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +75 -27
  83. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
  84. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +37 -0
  85. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1458 -0
  86. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +8 -162
  87. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -108
  88. package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +1 -3
  89. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +0 -3
  90. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  91. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -55
  92. package/src/resources/extensions/gsd/tests/headless-query.test.ts +22 -0
  93. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +8 -11
  94. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +4 -6
  95. package/src/resources/extensions/gsd/tests/run-uat.test.ts +3 -3
  96. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +64 -0
  97. package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +181 -0
  98. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +0 -3
  99. package/src/resources/extensions/gsd/tests/token-profile.test.ts +6 -6
  100. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -6
  101. package/src/resources/extensions/gsd/tests/undo.test.ts +6 -0
  102. package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +24 -26
  103. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +7 -201
  104. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  105. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  106. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +0 -3
  107. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +705 -0
  108. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +57 -106
  109. package/src/resources/extensions/gsd/tests/worktree.test.ts +5 -1
  110. package/src/resources/extensions/gsd/tests/write-gate.test.ts +43 -132
  111. package/src/resources/extensions/gsd/types.ts +90 -81
  112. package/src/resources/extensions/gsd/undo.ts +42 -46
  113. package/src/resources/extensions/gsd/unit-runtime.ts +14 -18
  114. package/src/resources/extensions/gsd/verification-evidence.ts +1 -3
  115. package/src/resources/extensions/gsd/verification-gate.ts +6 -39
  116. package/src/resources/extensions/gsd/worktree-command.ts +36 -24
  117. package/src/resources/extensions/gsd/worktree-manager.ts +2 -3
  118. package/src/resources/extensions/gsd/worktree-resolver.ts +485 -0
  119. package/src/resources/extensions/gsd/worktree.ts +7 -44
  120. package/dist/resources/extensions/gsd/auto-constants.js +0 -5
  121. package/dist/resources/extensions/gsd/auto-idempotency.js +0 -106
  122. package/dist/resources/extensions/gsd/auto-stuck-detection.js +0 -165
  123. package/dist/resources/extensions/gsd/mechanical-completion.js +0 -351
  124. package/src/resources/extensions/gsd/auto-constants.ts +0 -6
  125. package/src/resources/extensions/gsd/auto-idempotency.ts +0 -151
  126. package/src/resources/extensions/gsd/auto-stuck-detection.ts +0 -221
  127. package/src/resources/extensions/gsd/mechanical-completion.ts +0 -430
  128. package/src/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +0 -691
  129. package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +0 -127
  130. package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +0 -123
  131. package/src/resources/extensions/gsd/tests/dispatch-stall-guard.test.ts +0 -126
  132. package/src/resources/extensions/gsd/tests/loop-regression.test.ts +0 -874
  133. package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +0 -356
  134. package/src/resources/extensions/gsd/tests/progress-score.test.ts +0 -206
  135. package/src/resources/extensions/gsd/tests/session-lock.test.ts +0 -434
@@ -5,10 +5,11 @@
5
5
  // Exposes a unified sync API for decisions and requirements storage.
6
6
  // Schema is initialized on first open with WAL mode for file-backed DBs.
7
7
 
8
- import { createRequire } from 'node:module';
9
- import { existsSync } from 'node:fs';
10
- import type { Decision, Requirement } from './types.js';
11
- import { GSDError, GSD_STALE_STATE } from './errors.js';
8
+ import { createRequire } from "node:module";
9
+ import { existsSync, copyFileSync, mkdirSync } from "node:fs";
10
+ import { dirname } from "node:path";
11
+ import type { Decision, Requirement } from "./types.js";
12
+ import { GSDError, GSD_STALE_STATE } from "./errors.js";
12
13
 
13
14
  // Create a require function for loading native modules in ESM context
14
15
  const _require = createRequire(import.meta.url);
@@ -20,7 +21,7 @@ const _require = createRequire(import.meta.url);
20
21
  * Both expose prepare().run/get/all — the adapter normalizes row objects.
21
22
  */
22
23
  interface DbStatement {
23
- run(...params: unknown[]): void;
24
+ run(...params: unknown[]): unknown;
24
25
  get(...params: unknown[]): Record<string, unknown> | undefined;
25
26
  all(...params: unknown[]): Record<string, unknown>[];
26
27
  }
@@ -31,7 +32,7 @@ interface DbAdapter {
31
32
  close(): void;
32
33
  }
33
34
 
34
- type ProviderName = 'node:sqlite' | 'better-sqlite3';
35
+ type ProviderName = "node:sqlite" | "better-sqlite3";
35
36
 
36
37
  let providerName: ProviderName | null = null;
37
38
  let providerModule: unknown = null;
@@ -46,18 +47,20 @@ function suppressSqliteWarning(): void {
46
47
  // @ts-expect-error — overriding process.emit with filtered version
47
48
  process.emit = function (event: string, ...args: unknown[]): boolean {
48
49
  if (
49
- event === 'warning' &&
50
+ event === "warning" &&
50
51
  args[0] &&
51
- typeof args[0] === 'object' &&
52
- 'name' in args[0] &&
53
- (args[0] as { name: string }).name === 'ExperimentalWarning' &&
54
- 'message' in args[0] &&
55
- typeof (args[0] as { message: string }).message === 'string' &&
56
- (args[0] as { message: string }).message.includes('SQLite')
52
+ typeof args[0] === "object" &&
53
+ "name" in args[0] &&
54
+ (args[0] as { name: string }).name === "ExperimentalWarning" &&
55
+ "message" in args[0] &&
56
+ typeof (args[0] as { message: string }).message === "string" &&
57
+ (args[0] as { message: string }).message.includes("SQLite")
57
58
  ) {
58
59
  return false;
59
60
  }
60
- return origEmit.apply(process, [event, ...args] as Parameters<typeof process.emit>) as unknown as boolean;
61
+ return origEmit.apply(process, [event, ...args] as Parameters<
62
+ typeof process.emit
63
+ >) as unknown as boolean;
61
64
  };
62
65
  }
63
66
 
@@ -68,10 +71,10 @@ function loadProvider(): void {
68
71
  // Try node:sqlite first
69
72
  try {
70
73
  suppressSqliteWarning();
71
- const mod = _require('node:sqlite');
74
+ const mod = _require("node:sqlite");
72
75
  if (mod.DatabaseSync) {
73
76
  providerModule = mod;
74
- providerName = 'node:sqlite';
77
+ providerName = "node:sqlite";
75
78
  return;
76
79
  }
77
80
  } catch {
@@ -80,17 +83,19 @@ function loadProvider(): void {
80
83
 
81
84
  // Try better-sqlite3
82
85
  try {
83
- const mod = _require('better-sqlite3');
84
- if (typeof mod === 'function' || (mod && mod.default)) {
86
+ const mod = _require("better-sqlite3");
87
+ if (typeof mod === "function" || (mod && mod.default)) {
85
88
  providerModule = mod.default || mod;
86
- providerName = 'better-sqlite3';
89
+ providerName = "better-sqlite3";
87
90
  return;
88
91
  }
89
92
  } catch {
90
93
  // better-sqlite3 not available
91
94
  }
92
95
 
93
- process.stderr.write('gsd-db: No SQLite provider available (tried node:sqlite, better-sqlite3)\n');
96
+ process.stderr.write(
97
+ "gsd-db: No SQLite provider available (tried node:sqlite, better-sqlite3)\n",
98
+ );
94
99
  }
95
100
 
96
101
  // ─── Database Adapter ──────────────────────────────────────────────────────
@@ -101,13 +106,13 @@ function loadProvider(): void {
101
106
  function normalizeRow(row: unknown): Record<string, unknown> | undefined {
102
107
  if (row == null) return undefined;
103
108
  if (Object.getPrototypeOf(row) === null) {
104
- return { ...row as Record<string, unknown> };
109
+ return { ...(row as Record<string, unknown>) };
105
110
  }
106
111
  return row as Record<string, unknown>;
107
112
  }
108
113
 
109
114
  function normalizeRows(rows: unknown[]): Record<string, unknown>[] {
110
- return rows.map(r => normalizeRow(r)!);
115
+ return rows.map((r) => normalizeRow(r)!);
111
116
  }
112
117
 
113
118
  function createAdapter(rawDb: unknown): DbAdapter {
@@ -128,8 +133,8 @@ function createAdapter(rawDb: unknown): DbAdapter {
128
133
  prepare(sql: string): DbStatement {
129
134
  const stmt = db.prepare(sql);
130
135
  return {
131
- run(...params: unknown[]): void {
132
- stmt.run(...params);
136
+ run(...params: unknown[]): unknown {
137
+ return stmt.run(...params);
133
138
  },
134
139
  get(...params: unknown[]): Record<string, unknown> | undefined {
135
140
  return normalizeRow(stmt.get(...params));
@@ -149,8 +154,10 @@ function openRawDb(path: string): unknown {
149
154
  loadProvider();
150
155
  if (!providerModule || !providerName) return null;
151
156
 
152
- if (providerName === 'node:sqlite') {
153
- const { DatabaseSync } = providerModule as { DatabaseSync: new (path: string) => unknown };
157
+ if (providerName === "node:sqlite") {
158
+ const { DatabaseSync } = providerModule as {
159
+ DatabaseSync: new (path: string) => unknown;
160
+ };
154
161
  return new DatabaseSync(path);
155
162
  }
156
163
 
@@ -166,10 +173,10 @@ const SCHEMA_VERSION = 3;
166
173
  function initSchema(db: DbAdapter, fileBacked: boolean): void {
167
174
  // WAL mode for file-backed databases (must be outside transaction)
168
175
  if (fileBacked) {
169
- db.exec('PRAGMA journal_mode=WAL');
176
+ db.exec("PRAGMA journal_mode=WAL");
170
177
  }
171
178
 
172
- db.exec('BEGIN');
179
+ db.exec("BEGIN");
173
180
  try {
174
181
  db.exec(`
175
182
  CREATE TABLE IF NOT EXISTS schema_version (
@@ -245,24 +252,37 @@ function initSchema(db: DbAdapter, fileBacked: boolean): void {
245
252
  )
246
253
  `);
247
254
 
248
- db.exec('CREATE INDEX IF NOT EXISTS idx_memories_active ON memories(superseded_by)');
255
+ db.exec(
256
+ "CREATE INDEX IF NOT EXISTS idx_memories_active ON memories(superseded_by)",
257
+ );
249
258
 
250
259
  // Views — DROP + CREATE since CREATE VIEW IF NOT EXISTS doesn't update definitions
251
- db.exec(`CREATE VIEW IF NOT EXISTS active_decisions AS SELECT * FROM decisions WHERE superseded_by IS NULL`);
252
- db.exec(`CREATE VIEW IF NOT EXISTS active_requirements AS SELECT * FROM requirements WHERE superseded_by IS NULL`);
253
- db.exec(`CREATE VIEW IF NOT EXISTS active_memories AS SELECT * FROM memories WHERE superseded_by IS NULL`);
260
+ db.exec(
261
+ `CREATE VIEW IF NOT EXISTS active_decisions AS SELECT * FROM decisions WHERE superseded_by IS NULL`,
262
+ );
263
+ db.exec(
264
+ `CREATE VIEW IF NOT EXISTS active_requirements AS SELECT * FROM requirements WHERE superseded_by IS NULL`,
265
+ );
266
+ db.exec(
267
+ `CREATE VIEW IF NOT EXISTS active_memories AS SELECT * FROM memories WHERE superseded_by IS NULL`,
268
+ );
254
269
 
255
270
  // Insert schema version if not already present
256
- const existing = db.prepare('SELECT count(*) as cnt FROM schema_version').get();
257
- if (existing && (existing['cnt'] as number) === 0) {
258
- db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)').run(
259
- { ':version': SCHEMA_VERSION, ':applied_at': new Date().toISOString() },
260
- );
271
+ const existing = db
272
+ .prepare("SELECT count(*) as cnt FROM schema_version")
273
+ .get();
274
+ if (existing && (existing["cnt"] as number) === 0) {
275
+ db.prepare(
276
+ "INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)",
277
+ ).run({
278
+ ":version": SCHEMA_VERSION,
279
+ ":applied_at": new Date().toISOString(),
280
+ });
261
281
  }
262
282
 
263
- db.exec('COMMIT');
283
+ db.exec("COMMIT");
264
284
  } catch (err) {
265
- db.exec('ROLLBACK');
285
+ db.exec("ROLLBACK");
266
286
  throw err;
267
287
  }
268
288
 
@@ -275,12 +295,12 @@ function initSchema(db: DbAdapter, fileBacked: boolean): void {
275
295
  * and applies DDL for each version step up to SCHEMA_VERSION.
276
296
  */
277
297
  function migrateSchema(db: DbAdapter): void {
278
- const row = db.prepare('SELECT MAX(version) as v FROM schema_version').get();
279
- const currentVersion = row ? (row['v'] as number) : 0;
298
+ const row = db.prepare("SELECT MAX(version) as v FROM schema_version").get();
299
+ const currentVersion = row ? (row["v"] as number) : 0;
280
300
 
281
301
  if (currentVersion >= SCHEMA_VERSION) return;
282
302
 
283
- db.exec('BEGIN');
303
+ db.exec("BEGIN");
284
304
  try {
285
305
  // v1 → v2: add artifacts table
286
306
  if (currentVersion < 2) {
@@ -296,9 +316,9 @@ function migrateSchema(db: DbAdapter): void {
296
316
  )
297
317
  `);
298
318
 
299
- db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)').run(
300
- { ':version': 2, ':applied_at': new Date().toISOString() },
301
- );
319
+ db.prepare(
320
+ "INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)",
321
+ ).run({ ":version": 2, ":applied_at": new Date().toISOString() });
302
322
  }
303
323
 
304
324
  // v2 → v3: add memories + memory_processed_units tables
@@ -327,18 +347,22 @@ function migrateSchema(db: DbAdapter): void {
327
347
  )
328
348
  `);
329
349
 
330
- db.exec('CREATE INDEX IF NOT EXISTS idx_memories_active ON memories(superseded_by)');
331
- db.exec('DROP VIEW IF EXISTS active_memories');
332
- db.exec('CREATE VIEW active_memories AS SELECT * FROM memories WHERE superseded_by IS NULL');
333
-
334
- db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)').run(
335
- { ':version': 3, ':applied_at': new Date().toISOString() },
350
+ db.exec(
351
+ "CREATE INDEX IF NOT EXISTS idx_memories_active ON memories(superseded_by)",
336
352
  );
353
+ db.exec("DROP VIEW IF EXISTS active_memories");
354
+ db.exec(
355
+ "CREATE VIEW active_memories AS SELECT * FROM memories WHERE superseded_by IS NULL",
356
+ );
357
+
358
+ db.prepare(
359
+ "INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)",
360
+ ).run({ ":version": 3, ":applied_at": new Date().toISOString() });
337
361
  }
338
362
 
339
- db.exec('COMMIT');
363
+ db.exec("COMMIT");
340
364
  } catch (err) {
341
- db.exec('ROLLBACK');
365
+ db.exec("ROLLBACK");
342
366
  throw err;
343
367
  }
344
368
  }
@@ -385,12 +409,16 @@ export function openDatabase(path: string): boolean {
385
409
  if (!rawDb) return false;
386
410
 
387
411
  const adapter = createAdapter(rawDb);
388
- const fileBacked = path !== ':memory:';
412
+ const fileBacked = path !== ":memory:";
389
413
 
390
414
  try {
391
415
  initSchema(adapter, fileBacked);
392
416
  } catch (err) {
393
- try { adapter.close(); } catch { /* swallow */ }
417
+ try {
418
+ adapter.close();
419
+ } catch {
420
+ /* swallow */
421
+ }
394
422
  throw err;
395
423
  }
396
424
 
@@ -420,14 +448,15 @@ export function closeDatabase(): void {
420
448
  * Runs a function inside a transaction. Rolls back on error.
421
449
  */
422
450
  export function transaction<T>(fn: () => T): T {
423
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, 'gsd-db: No database open');
424
- currentDb.exec('BEGIN');
451
+ if (!currentDb)
452
+ throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
453
+ currentDb.exec("BEGIN");
425
454
  try {
426
455
  const result = fn();
427
- currentDb.exec('COMMIT');
456
+ currentDb.exec("COMMIT");
428
457
  return result;
429
458
  } catch (err) {
430
- currentDb.exec('ROLLBACK');
459
+ currentDb.exec("ROLLBACK");
431
460
  throw err;
432
461
  }
433
462
  }
@@ -437,21 +466,24 @@ export function transaction<T>(fn: () => T): T {
437
466
  /**
438
467
  * Insert a decision. The `seq` field is auto-generated.
439
468
  */
440
- export function insertDecision(d: Omit<Decision, 'seq'>): void {
441
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, 'gsd-db: No database open');
442
- currentDb.prepare(
443
- `INSERT INTO decisions (id, when_context, scope, decision, choice, rationale, revisable, superseded_by)
469
+ export function insertDecision(d: Omit<Decision, "seq">): void {
470
+ if (!currentDb)
471
+ throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
472
+ currentDb
473
+ .prepare(
474
+ `INSERT INTO decisions (id, when_context, scope, decision, choice, rationale, revisable, superseded_by)
444
475
  VALUES (:id, :when_context, :scope, :decision, :choice, :rationale, :revisable, :superseded_by)`,
445
- ).run({
446
- ':id': d.id,
447
- ':when_context': d.when_context,
448
- ':scope': d.scope,
449
- ':decision': d.decision,
450
- ':choice': d.choice,
451
- ':rationale': d.rationale,
452
- ':revisable': d.revisable,
453
- ':superseded_by': d.superseded_by,
454
- });
476
+ )
477
+ .run({
478
+ ":id": d.id,
479
+ ":when_context": d.when_context,
480
+ ":scope": d.scope,
481
+ ":decision": d.decision,
482
+ ":choice": d.choice,
483
+ ":rationale": d.rationale,
484
+ ":revisable": d.revisable,
485
+ ":superseded_by": d.superseded_by,
486
+ });
455
487
  }
456
488
 
457
489
  /**
@@ -459,18 +491,18 @@ export function insertDecision(d: Omit<Decision, 'seq'>): void {
459
491
  */
460
492
  export function getDecisionById(id: string): Decision | null {
461
493
  if (!currentDb) return null;
462
- const row = currentDb.prepare('SELECT * FROM decisions WHERE id = ?').get(id);
494
+ const row = currentDb.prepare("SELECT * FROM decisions WHERE id = ?").get(id);
463
495
  if (!row) return null;
464
496
  return {
465
- seq: row['seq'] as number,
466
- id: row['id'] as string,
467
- when_context: row['when_context'] as string,
468
- scope: row['scope'] as string,
469
- decision: row['decision'] as string,
470
- choice: row['choice'] as string,
471
- rationale: row['rationale'] as string,
472
- revisable: row['revisable'] as string,
473
- superseded_by: (row['superseded_by'] as string) ?? null,
497
+ seq: row["seq"] as number,
498
+ id: row["id"] as string,
499
+ when_context: row["when_context"] as string,
500
+ scope: row["scope"] as string,
501
+ decision: row["decision"] as string,
502
+ choice: row["choice"] as string,
503
+ rationale: row["rationale"] as string,
504
+ revisable: row["revisable"] as string,
505
+ superseded_by: (row["superseded_by"] as string) ?? null,
474
506
  };
475
507
  }
476
508
 
@@ -479,16 +511,16 @@ export function getDecisionById(id: string): Decision | null {
479
511
  */
480
512
  export function getActiveDecisions(): Decision[] {
481
513
  if (!currentDb) return [];
482
- const rows = currentDb.prepare('SELECT * FROM active_decisions').all();
483
- return rows.map(row => ({
484
- seq: row['seq'] as number,
485
- id: row['id'] as string,
486
- when_context: row['when_context'] as string,
487
- scope: row['scope'] as string,
488
- decision: row['decision'] as string,
489
- choice: row['choice'] as string,
490
- rationale: row['rationale'] as string,
491
- revisable: row['revisable'] as string,
514
+ const rows = currentDb.prepare("SELECT * FROM active_decisions").all();
515
+ return rows.map((row) => ({
516
+ seq: row["seq"] as number,
517
+ id: row["id"] as string,
518
+ when_context: row["when_context"] as string,
519
+ scope: row["scope"] as string,
520
+ decision: row["decision"] as string,
521
+ choice: row["choice"] as string,
522
+ rationale: row["rationale"] as string,
523
+ revisable: row["revisable"] as string,
492
524
  superseded_by: null,
493
525
  }));
494
526
  }
@@ -499,24 +531,27 @@ export function getActiveDecisions(): Decision[] {
499
531
  * Insert a requirement.
500
532
  */
501
533
  export function insertRequirement(r: Requirement): void {
502
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, 'gsd-db: No database open');
503
- currentDb.prepare(
504
- `INSERT INTO requirements (id, class, status, description, why, source, primary_owner, supporting_slices, validation, notes, full_content, superseded_by)
534
+ if (!currentDb)
535
+ throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
536
+ currentDb
537
+ .prepare(
538
+ `INSERT INTO requirements (id, class, status, description, why, source, primary_owner, supporting_slices, validation, notes, full_content, superseded_by)
505
539
  VALUES (:id, :class, :status, :description, :why, :source, :primary_owner, :supporting_slices, :validation, :notes, :full_content, :superseded_by)`,
506
- ).run({
507
- ':id': r.id,
508
- ':class': r.class,
509
- ':status': r.status,
510
- ':description': r.description,
511
- ':why': r.why,
512
- ':source': r.source,
513
- ':primary_owner': r.primary_owner,
514
- ':supporting_slices': r.supporting_slices,
515
- ':validation': r.validation,
516
- ':notes': r.notes,
517
- ':full_content': r.full_content,
518
- ':superseded_by': r.superseded_by,
519
- });
540
+ )
541
+ .run({
542
+ ":id": r.id,
543
+ ":class": r.class,
544
+ ":status": r.status,
545
+ ":description": r.description,
546
+ ":why": r.why,
547
+ ":source": r.source,
548
+ ":primary_owner": r.primary_owner,
549
+ ":supporting_slices": r.supporting_slices,
550
+ ":validation": r.validation,
551
+ ":notes": r.notes,
552
+ ":full_content": r.full_content,
553
+ ":superseded_by": r.superseded_by,
554
+ });
520
555
  }
521
556
 
522
557
  /**
@@ -524,21 +559,23 @@ export function insertRequirement(r: Requirement): void {
524
559
  */
525
560
  export function getRequirementById(id: string): Requirement | null {
526
561
  if (!currentDb) return null;
527
- const row = currentDb.prepare('SELECT * FROM requirements WHERE id = ?').get(id);
562
+ const row = currentDb
563
+ .prepare("SELECT * FROM requirements WHERE id = ?")
564
+ .get(id);
528
565
  if (!row) return null;
529
566
  return {
530
- id: row['id'] as string,
531
- class: row['class'] as string,
532
- status: row['status'] as string,
533
- description: row['description'] as string,
534
- why: row['why'] as string,
535
- source: row['source'] as string,
536
- primary_owner: row['primary_owner'] as string,
537
- supporting_slices: row['supporting_slices'] as string,
538
- validation: row['validation'] as string,
539
- notes: row['notes'] as string,
540
- full_content: row['full_content'] as string,
541
- superseded_by: (row['superseded_by'] as string) ?? null,
567
+ id: row["id"] as string,
568
+ class: row["class"] as string,
569
+ status: row["status"] as string,
570
+ description: row["description"] as string,
571
+ why: row["why"] as string,
572
+ source: row["source"] as string,
573
+ primary_owner: row["primary_owner"] as string,
574
+ supporting_slices: row["supporting_slices"] as string,
575
+ validation: row["validation"] as string,
576
+ notes: row["notes"] as string,
577
+ full_content: row["full_content"] as string,
578
+ superseded_by: (row["superseded_by"] as string) ?? null,
542
579
  };
543
580
  }
544
581
 
@@ -547,19 +584,19 @@ export function getRequirementById(id: string): Requirement | null {
547
584
  */
548
585
  export function getActiveRequirements(): Requirement[] {
549
586
  if (!currentDb) return [];
550
- const rows = currentDb.prepare('SELECT * FROM active_requirements').all();
551
- return rows.map(row => ({
552
- id: row['id'] as string,
553
- class: row['class'] as string,
554
- status: row['status'] as string,
555
- description: row['description'] as string,
556
- why: row['why'] as string,
557
- source: row['source'] as string,
558
- primary_owner: row['primary_owner'] as string,
559
- supporting_slices: row['supporting_slices'] as string,
560
- validation: row['validation'] as string,
561
- notes: row['notes'] as string,
562
- full_content: row['full_content'] as string,
587
+ const rows = currentDb.prepare("SELECT * FROM active_requirements").all();
588
+ return rows.map((row) => ({
589
+ id: row["id"] as string,
590
+ class: row["class"] as string,
591
+ status: row["status"] as string,
592
+ description: row["description"] as string,
593
+ why: row["why"] as string,
594
+ source: row["source"] as string,
595
+ primary_owner: row["primary_owner"] as string,
596
+ supporting_slices: row["supporting_slices"] as string,
597
+ validation: row["validation"] as string,
598
+ notes: row["notes"] as string,
599
+ full_content: row["full_content"] as string,
563
600
  superseded_by: null,
564
601
  }));
565
602
  }
@@ -602,45 +639,51 @@ export function _resetProvider(): void {
602
639
  /**
603
640
  * Insert or replace a decision. Uses the `id` UNIQUE constraint for idempotency.
604
641
  */
605
- export function upsertDecision(d: Omit<Decision, 'seq'>): void {
606
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, 'gsd-db: No database open');
607
- currentDb.prepare(
608
- `INSERT OR REPLACE INTO decisions (id, when_context, scope, decision, choice, rationale, revisable, superseded_by)
642
+ export function upsertDecision(d: Omit<Decision, "seq">): void {
643
+ if (!currentDb)
644
+ throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
645
+ currentDb
646
+ .prepare(
647
+ `INSERT OR REPLACE INTO decisions (id, when_context, scope, decision, choice, rationale, revisable, superseded_by)
609
648
  VALUES (:id, :when_context, :scope, :decision, :choice, :rationale, :revisable, :superseded_by)`,
610
- ).run({
611
- ':id': d.id,
612
- ':when_context': d.when_context,
613
- ':scope': d.scope,
614
- ':decision': d.decision,
615
- ':choice': d.choice,
616
- ':rationale': d.rationale,
617
- ':revisable': d.revisable,
618
- ':superseded_by': d.superseded_by ?? null,
619
- });
649
+ )
650
+ .run({
651
+ ":id": d.id,
652
+ ":when_context": d.when_context,
653
+ ":scope": d.scope,
654
+ ":decision": d.decision,
655
+ ":choice": d.choice,
656
+ ":rationale": d.rationale,
657
+ ":revisable": d.revisable,
658
+ ":superseded_by": d.superseded_by ?? null,
659
+ });
620
660
  }
621
661
 
622
662
  /**
623
663
  * Insert or replace a requirement. Uses the `id` PK for idempotency.
624
664
  */
625
665
  export function upsertRequirement(r: Requirement): void {
626
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, 'gsd-db: No database open');
627
- currentDb.prepare(
628
- `INSERT OR REPLACE INTO requirements (id, class, status, description, why, source, primary_owner, supporting_slices, validation, notes, full_content, superseded_by)
666
+ if (!currentDb)
667
+ throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
668
+ currentDb
669
+ .prepare(
670
+ `INSERT OR REPLACE INTO requirements (id, class, status, description, why, source, primary_owner, supporting_slices, validation, notes, full_content, superseded_by)
629
671
  VALUES (:id, :class, :status, :description, :why, :source, :primary_owner, :supporting_slices, :validation, :notes, :full_content, :superseded_by)`,
630
- ).run({
631
- ':id': r.id,
632
- ':class': r.class,
633
- ':status': r.status,
634
- ':description': r.description,
635
- ':why': r.why,
636
- ':source': r.source,
637
- ':primary_owner': r.primary_owner,
638
- ':supporting_slices': r.supporting_slices,
639
- ':validation': r.validation,
640
- ':notes': r.notes,
641
- ':full_content': r.full_content,
642
- ':superseded_by': r.superseded_by ?? null,
643
- });
672
+ )
673
+ .run({
674
+ ":id": r.id,
675
+ ":class": r.class,
676
+ ":status": r.status,
677
+ ":description": r.description,
678
+ ":why": r.why,
679
+ ":source": r.source,
680
+ ":primary_owner": r.primary_owner,
681
+ ":supporting_slices": r.supporting_slices,
682
+ ":validation": r.validation,
683
+ ":notes": r.notes,
684
+ ":full_content": r.full_content,
685
+ ":superseded_by": r.superseded_by ?? null,
686
+ });
644
687
  }
645
688
 
646
689
  /**
@@ -655,7 +698,7 @@ export function upsertRequirement(r: Requirement): void {
655
698
  export function clearArtifacts(): void {
656
699
  if (!currentDb) return;
657
700
  try {
658
- currentDb.exec('DELETE FROM artifacts');
701
+ currentDb.exec("DELETE FROM artifacts");
659
702
  } catch {
660
703
  // Clearing a cache should never be fatal
661
704
  }
@@ -669,17 +712,169 @@ export function insertArtifact(a: {
669
712
  task_id: string | null;
670
713
  full_content: string;
671
714
  }): void {
672
- if (!currentDb) throw new GSDError(GSD_STALE_STATE, 'gsd-db: No database open');
673
- currentDb.prepare(
674
- `INSERT OR REPLACE INTO artifacts (path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at)
715
+ if (!currentDb)
716
+ throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
717
+ currentDb
718
+ .prepare(
719
+ `INSERT OR REPLACE INTO artifacts (path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at)
675
720
  VALUES (:path, :artifact_type, :milestone_id, :slice_id, :task_id, :full_content, :imported_at)`,
676
- ).run({
677
- ':path': a.path,
678
- ':artifact_type': a.artifact_type,
679
- ':milestone_id': a.milestone_id,
680
- ':slice_id': a.slice_id,
681
- ':task_id': a.task_id,
682
- ':full_content': a.full_content,
683
- ':imported_at': new Date().toISOString(),
684
- });
721
+ )
722
+ .run({
723
+ ":path": a.path,
724
+ ":artifact_type": a.artifact_type,
725
+ ":milestone_id": a.milestone_id,
726
+ ":slice_id": a.slice_id,
727
+ ":task_id": a.task_id,
728
+ ":full_content": a.full_content,
729
+ ":imported_at": new Date().toISOString(),
730
+ });
731
+ }
732
+
733
+ // ─── Worktree DB Helpers ──────────────────────────────────────────────────
734
+
735
+ export function copyWorktreeDb(srcDbPath: string, destDbPath: string): boolean {
736
+ try {
737
+ if (!existsSync(srcDbPath)) return false;
738
+ const destDir = dirname(destDbPath);
739
+ mkdirSync(destDir, { recursive: true });
740
+ copyFileSync(srcDbPath, destDbPath);
741
+ return true;
742
+ } catch (err) {
743
+ process.stderr.write(
744
+ `gsd-db: failed to copy DB to worktree: ${(err as Error).message}\n`,
745
+ );
746
+ return false;
747
+ }
748
+ }
749
+
750
+ export function reconcileWorktreeDb(
751
+ mainDbPath: string,
752
+ worktreeDbPath: string,
753
+ ): {
754
+ decisions: number;
755
+ requirements: number;
756
+ artifacts: number;
757
+ conflicts: string[];
758
+ } {
759
+ const zero = {
760
+ decisions: 0,
761
+ requirements: 0,
762
+ artifacts: 0,
763
+ conflicts: [] as string[],
764
+ };
765
+ if (!existsSync(worktreeDbPath)) return zero;
766
+ if (worktreeDbPath.includes("'")) {
767
+ process.stderr.write(
768
+ `gsd-db: worktree DB reconciliation failed: path contains unsafe characters\n`,
769
+ );
770
+ return zero;
771
+ }
772
+ if (!currentDb) {
773
+ const opened = openDatabase(mainDbPath);
774
+ if (!opened) {
775
+ process.stderr.write(
776
+ `gsd-db: worktree DB reconciliation failed: cannot open main DB\n`,
777
+ );
778
+ return zero;
779
+ }
780
+ }
781
+ const adapter = currentDb!;
782
+ const conflicts: string[] = [];
783
+ try {
784
+ adapter.exec(`ATTACH DATABASE '${worktreeDbPath}' AS wt`);
785
+ try {
786
+ const decConf = adapter
787
+ .prepare(
788
+ `SELECT m.id FROM decisions m INNER JOIN wt.decisions w ON m.id = w.id WHERE m.decision != w.decision OR m.choice != w.choice OR m.rationale != w.rationale OR m.superseded_by IS NOT w.superseded_by`,
789
+ )
790
+ .all();
791
+ for (const row of decConf)
792
+ conflicts.push(
793
+ `decision ${(row as Record<string, unknown>)["id"]}: modified in both`,
794
+ );
795
+ const reqConf = adapter
796
+ .prepare(
797
+ `SELECT m.id FROM requirements m INNER JOIN wt.requirements w ON m.id = w.id WHERE m.description != w.description OR m.status != w.status OR m.notes != w.notes OR m.superseded_by IS NOT w.superseded_by`,
798
+ )
799
+ .all();
800
+ for (const row of reqConf)
801
+ conflicts.push(
802
+ `requirement ${(row as Record<string, unknown>)["id"]}: modified in both`,
803
+ );
804
+ const merged = { decisions: 0, requirements: 0, artifacts: 0 };
805
+ adapter.exec("BEGIN");
806
+ try {
807
+ const dR = adapter
808
+ .prepare(
809
+ `
810
+ INSERT OR REPLACE INTO decisions (
811
+ id, when_context, scope, decision, choice, rationale, revisable, superseded_by
812
+ )
813
+ SELECT
814
+ id, when_context, scope, decision, choice, rationale, revisable, superseded_by
815
+ FROM wt.decisions
816
+ `,
817
+ )
818
+ .run();
819
+ merged.decisions =
820
+ typeof dR === "object" && dR !== null
821
+ ? ((dR as { changes?: number }).changes ?? 0)
822
+ : 0;
823
+ const rR = adapter
824
+ .prepare(
825
+ `
826
+ INSERT OR REPLACE INTO requirements (
827
+ id, class, status, description, why, source, primary_owner,
828
+ supporting_slices, validation, notes, full_content, superseded_by
829
+ )
830
+ SELECT
831
+ id, class, status, description, why, source, primary_owner,
832
+ supporting_slices, validation, notes, full_content, superseded_by
833
+ FROM wt.requirements
834
+ `,
835
+ )
836
+ .run();
837
+ merged.requirements =
838
+ typeof rR === "object" && rR !== null
839
+ ? ((rR as { changes?: number }).changes ?? 0)
840
+ : 0;
841
+ const aR = adapter
842
+ .prepare(
843
+ `
844
+ INSERT OR REPLACE INTO artifacts (
845
+ path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at
846
+ )
847
+ SELECT
848
+ path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at
849
+ FROM wt.artifacts
850
+ `,
851
+ )
852
+ .run();
853
+ merged.artifacts =
854
+ typeof aR === "object" && aR !== null
855
+ ? ((aR as { changes?: number }).changes ?? 0)
856
+ : 0;
857
+ adapter.exec("COMMIT");
858
+ } catch (txErr) {
859
+ try {
860
+ adapter.exec("ROLLBACK");
861
+ } catch {
862
+ /* best-effort */
863
+ }
864
+ throw txErr;
865
+ }
866
+ return { ...merged, conflicts };
867
+ } finally {
868
+ try {
869
+ adapter.exec("DETACH DATABASE wt");
870
+ } catch {
871
+ /* best-effort */
872
+ }
873
+ }
874
+ } catch (err) {
875
+ process.stderr.write(
876
+ `gsd-db: worktree DB reconciliation failed: ${(err as Error).message}\n`,
877
+ );
878
+ return { ...zero, conflicts };
879
+ }
685
880
  }