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
@@ -4,8 +4,10 @@
4
4
  //
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
- import { createRequire } from 'node:module';
8
- import { GSDError, GSD_STALE_STATE } from './errors.js';
7
+ import { createRequire } from "node:module";
8
+ import { existsSync, copyFileSync, mkdirSync } from "node:fs";
9
+ import { dirname } from "node:path";
10
+ import { GSDError, GSD_STALE_STATE } from "./errors.js";
9
11
  // Create a require function for loading native modules in ESM context
10
12
  const _require = createRequire(import.meta.url);
11
13
  let providerName = null;
@@ -19,14 +21,14 @@ function suppressSqliteWarning() {
19
21
  const origEmit = process.emit;
20
22
  // @ts-expect-error — overriding process.emit with filtered version
21
23
  process.emit = function (event, ...args) {
22
- if (event === 'warning' &&
24
+ if (event === "warning" &&
23
25
  args[0] &&
24
- typeof args[0] === 'object' &&
25
- 'name' in args[0] &&
26
- args[0].name === 'ExperimentalWarning' &&
27
- 'message' in args[0] &&
28
- typeof args[0].message === 'string' &&
29
- args[0].message.includes('SQLite')) {
26
+ typeof args[0] === "object" &&
27
+ "name" in args[0] &&
28
+ args[0].name === "ExperimentalWarning" &&
29
+ "message" in args[0] &&
30
+ typeof args[0].message === "string" &&
31
+ args[0].message.includes("SQLite")) {
30
32
  return false;
31
33
  }
32
34
  return origEmit.apply(process, [event, ...args]);
@@ -39,10 +41,10 @@ function loadProvider() {
39
41
  // Try node:sqlite first
40
42
  try {
41
43
  suppressSqliteWarning();
42
- const mod = _require('node:sqlite');
44
+ const mod = _require("node:sqlite");
43
45
  if (mod.DatabaseSync) {
44
46
  providerModule = mod;
45
- providerName = 'node:sqlite';
47
+ providerName = "node:sqlite";
46
48
  return;
47
49
  }
48
50
  }
@@ -51,17 +53,17 @@ function loadProvider() {
51
53
  }
52
54
  // Try better-sqlite3
53
55
  try {
54
- const mod = _require('better-sqlite3');
55
- if (typeof mod === 'function' || (mod && mod.default)) {
56
+ const mod = _require("better-sqlite3");
57
+ if (typeof mod === "function" || (mod && mod.default)) {
56
58
  providerModule = mod.default || mod;
57
- providerName = 'better-sqlite3';
59
+ providerName = "better-sqlite3";
58
60
  return;
59
61
  }
60
62
  }
61
63
  catch {
62
64
  // better-sqlite3 not available
63
65
  }
64
- process.stderr.write('gsd-db: No SQLite provider available (tried node:sqlite, better-sqlite3)\n');
66
+ process.stderr.write("gsd-db: No SQLite provider available (tried node:sqlite, better-sqlite3)\n");
65
67
  }
66
68
  // ─── Database Adapter ──────────────────────────────────────────────────────
67
69
  /**
@@ -76,7 +78,7 @@ function normalizeRow(row) {
76
78
  return row;
77
79
  }
78
80
  function normalizeRows(rows) {
79
- return rows.map(r => normalizeRow(r));
81
+ return rows.map((r) => normalizeRow(r));
80
82
  }
81
83
  function createAdapter(rawDb) {
82
84
  const db = rawDb;
@@ -88,7 +90,7 @@ function createAdapter(rawDb) {
88
90
  const stmt = db.prepare(sql);
89
91
  return {
90
92
  run(...params) {
91
- stmt.run(...params);
93
+ return stmt.run(...params);
92
94
  },
93
95
  get(...params) {
94
96
  return normalizeRow(stmt.get(...params));
@@ -107,7 +109,7 @@ function openRawDb(path) {
107
109
  loadProvider();
108
110
  if (!providerModule || !providerName)
109
111
  return null;
110
- if (providerName === 'node:sqlite') {
112
+ if (providerName === "node:sqlite") {
111
113
  const { DatabaseSync } = providerModule;
112
114
  return new DatabaseSync(path);
113
115
  }
@@ -120,9 +122,9 @@ const SCHEMA_VERSION = 3;
120
122
  function initSchema(db, fileBacked) {
121
123
  // WAL mode for file-backed databases (must be outside transaction)
122
124
  if (fileBacked) {
123
- db.exec('PRAGMA journal_mode=WAL');
125
+ db.exec("PRAGMA journal_mode=WAL");
124
126
  }
125
- db.exec('BEGIN');
127
+ db.exec("BEGIN");
126
128
  try {
127
129
  db.exec(`
128
130
  CREATE TABLE IF NOT EXISTS schema_version (
@@ -192,20 +194,25 @@ function initSchema(db, fileBacked) {
192
194
  processed_at TEXT NOT NULL
193
195
  )
194
196
  `);
195
- db.exec('CREATE INDEX IF NOT EXISTS idx_memories_active ON memories(superseded_by)');
197
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memories_active ON memories(superseded_by)");
196
198
  // Views — DROP + CREATE since CREATE VIEW IF NOT EXISTS doesn't update definitions
197
199
  db.exec(`CREATE VIEW IF NOT EXISTS active_decisions AS SELECT * FROM decisions WHERE superseded_by IS NULL`);
198
200
  db.exec(`CREATE VIEW IF NOT EXISTS active_requirements AS SELECT * FROM requirements WHERE superseded_by IS NULL`);
199
201
  db.exec(`CREATE VIEW IF NOT EXISTS active_memories AS SELECT * FROM memories WHERE superseded_by IS NULL`);
200
202
  // Insert schema version if not already present
201
- const existing = db.prepare('SELECT count(*) as cnt FROM schema_version').get();
202
- if (existing && existing['cnt'] === 0) {
203
- db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)').run({ ':version': SCHEMA_VERSION, ':applied_at': new Date().toISOString() });
203
+ const existing = db
204
+ .prepare("SELECT count(*) as cnt FROM schema_version")
205
+ .get();
206
+ if (existing && existing["cnt"] === 0) {
207
+ db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)").run({
208
+ ":version": SCHEMA_VERSION,
209
+ ":applied_at": new Date().toISOString(),
210
+ });
204
211
  }
205
- db.exec('COMMIT');
212
+ db.exec("COMMIT");
206
213
  }
207
214
  catch (err) {
208
- db.exec('ROLLBACK');
215
+ db.exec("ROLLBACK");
209
216
  throw err;
210
217
  }
211
218
  // Run incremental migrations for existing databases
@@ -216,11 +223,11 @@ function initSchema(db, fileBacked) {
216
223
  * and applies DDL for each version step up to SCHEMA_VERSION.
217
224
  */
218
225
  function migrateSchema(db) {
219
- const row = db.prepare('SELECT MAX(version) as v FROM schema_version').get();
220
- const currentVersion = row ? row['v'] : 0;
226
+ const row = db.prepare("SELECT MAX(version) as v FROM schema_version").get();
227
+ const currentVersion = row ? row["v"] : 0;
221
228
  if (currentVersion >= SCHEMA_VERSION)
222
229
  return;
223
- db.exec('BEGIN');
230
+ db.exec("BEGIN");
224
231
  try {
225
232
  // v1 → v2: add artifacts table
226
233
  if (currentVersion < 2) {
@@ -235,7 +242,7 @@ function migrateSchema(db) {
235
242
  imported_at TEXT NOT NULL DEFAULT ''
236
243
  )
237
244
  `);
238
- db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)').run({ ':version': 2, ':applied_at': new Date().toISOString() });
245
+ db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)").run({ ":version": 2, ":applied_at": new Date().toISOString() });
239
246
  }
240
247
  // v2 → v3: add memories + memory_processed_units tables
241
248
  if (currentVersion < 3) {
@@ -261,15 +268,15 @@ function migrateSchema(db) {
261
268
  processed_at TEXT NOT NULL
262
269
  )
263
270
  `);
264
- db.exec('CREATE INDEX IF NOT EXISTS idx_memories_active ON memories(superseded_by)');
265
- db.exec('DROP VIEW IF EXISTS active_memories');
266
- db.exec('CREATE VIEW active_memories AS SELECT * FROM memories WHERE superseded_by IS NULL');
267
- db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)').run({ ':version': 3, ':applied_at': new Date().toISOString() });
271
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memories_active ON memories(superseded_by)");
272
+ db.exec("DROP VIEW IF EXISTS active_memories");
273
+ db.exec("CREATE VIEW active_memories AS SELECT * FROM memories WHERE superseded_by IS NULL");
274
+ db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)").run({ ":version": 3, ":applied_at": new Date().toISOString() });
268
275
  }
269
- db.exec('COMMIT');
276
+ db.exec("COMMIT");
270
277
  }
271
278
  catch (err) {
272
- db.exec('ROLLBACK');
279
+ db.exec("ROLLBACK");
273
280
  throw err;
274
281
  }
275
282
  }
@@ -309,7 +316,7 @@ export function openDatabase(path) {
309
316
  if (!rawDb)
310
317
  return false;
311
318
  const adapter = createAdapter(rawDb);
312
- const fileBacked = path !== ':memory:';
319
+ const fileBacked = path !== ":memory:";
313
320
  try {
314
321
  initSchema(adapter, fileBacked);
315
322
  }
@@ -317,7 +324,9 @@ export function openDatabase(path) {
317
324
  try {
318
325
  adapter.close();
319
326
  }
320
- catch { /* swallow */ }
327
+ catch {
328
+ /* swallow */
329
+ }
321
330
  throw err;
322
331
  }
323
332
  currentDb = adapter;
@@ -346,15 +355,15 @@ export function closeDatabase() {
346
355
  */
347
356
  export function transaction(fn) {
348
357
  if (!currentDb)
349
- throw new GSDError(GSD_STALE_STATE, 'gsd-db: No database open');
350
- currentDb.exec('BEGIN');
358
+ throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
359
+ currentDb.exec("BEGIN");
351
360
  try {
352
361
  const result = fn();
353
- currentDb.exec('COMMIT');
362
+ currentDb.exec("COMMIT");
354
363
  return result;
355
364
  }
356
365
  catch (err) {
357
- currentDb.exec('ROLLBACK');
366
+ currentDb.exec("ROLLBACK");
358
367
  throw err;
359
368
  }
360
369
  }
@@ -364,17 +373,19 @@ export function transaction(fn) {
364
373
  */
365
374
  export function insertDecision(d) {
366
375
  if (!currentDb)
367
- throw new GSDError(GSD_STALE_STATE, 'gsd-db: No database open');
368
- currentDb.prepare(`INSERT INTO decisions (id, when_context, scope, decision, choice, rationale, revisable, superseded_by)
369
- VALUES (:id, :when_context, :scope, :decision, :choice, :rationale, :revisable, :superseded_by)`).run({
370
- ':id': d.id,
371
- ':when_context': d.when_context,
372
- ':scope': d.scope,
373
- ':decision': d.decision,
374
- ':choice': d.choice,
375
- ':rationale': d.rationale,
376
- ':revisable': d.revisable,
377
- ':superseded_by': d.superseded_by,
376
+ throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
377
+ currentDb
378
+ .prepare(`INSERT INTO decisions (id, when_context, scope, decision, choice, rationale, revisable, superseded_by)
379
+ VALUES (:id, :when_context, :scope, :decision, :choice, :rationale, :revisable, :superseded_by)`)
380
+ .run({
381
+ ":id": d.id,
382
+ ":when_context": d.when_context,
383
+ ":scope": d.scope,
384
+ ":decision": d.decision,
385
+ ":choice": d.choice,
386
+ ":rationale": d.rationale,
387
+ ":revisable": d.revisable,
388
+ ":superseded_by": d.superseded_by,
378
389
  });
379
390
  }
380
391
  /**
@@ -383,19 +394,19 @@ export function insertDecision(d) {
383
394
  export function getDecisionById(id) {
384
395
  if (!currentDb)
385
396
  return null;
386
- const row = currentDb.prepare('SELECT * FROM decisions WHERE id = ?').get(id);
397
+ const row = currentDb.prepare("SELECT * FROM decisions WHERE id = ?").get(id);
387
398
  if (!row)
388
399
  return null;
389
400
  return {
390
- seq: row['seq'],
391
- id: row['id'],
392
- when_context: row['when_context'],
393
- scope: row['scope'],
394
- decision: row['decision'],
395
- choice: row['choice'],
396
- rationale: row['rationale'],
397
- revisable: row['revisable'],
398
- superseded_by: row['superseded_by'] ?? null,
401
+ seq: row["seq"],
402
+ id: row["id"],
403
+ when_context: row["when_context"],
404
+ scope: row["scope"],
405
+ decision: row["decision"],
406
+ choice: row["choice"],
407
+ rationale: row["rationale"],
408
+ revisable: row["revisable"],
409
+ superseded_by: row["superseded_by"] ?? null,
399
410
  };
400
411
  }
401
412
  /**
@@ -404,16 +415,16 @@ export function getDecisionById(id) {
404
415
  export function getActiveDecisions() {
405
416
  if (!currentDb)
406
417
  return [];
407
- const rows = currentDb.prepare('SELECT * FROM active_decisions').all();
408
- return rows.map(row => ({
409
- seq: row['seq'],
410
- id: row['id'],
411
- when_context: row['when_context'],
412
- scope: row['scope'],
413
- decision: row['decision'],
414
- choice: row['choice'],
415
- rationale: row['rationale'],
416
- revisable: row['revisable'],
418
+ const rows = currentDb.prepare("SELECT * FROM active_decisions").all();
419
+ return rows.map((row) => ({
420
+ seq: row["seq"],
421
+ id: row["id"],
422
+ when_context: row["when_context"],
423
+ scope: row["scope"],
424
+ decision: row["decision"],
425
+ choice: row["choice"],
426
+ rationale: row["rationale"],
427
+ revisable: row["revisable"],
417
428
  superseded_by: null,
418
429
  }));
419
430
  }
@@ -423,21 +434,23 @@ export function getActiveDecisions() {
423
434
  */
424
435
  export function insertRequirement(r) {
425
436
  if (!currentDb)
426
- throw new GSDError(GSD_STALE_STATE, 'gsd-db: No database open');
427
- currentDb.prepare(`INSERT INTO requirements (id, class, status, description, why, source, primary_owner, supporting_slices, validation, notes, full_content, superseded_by)
428
- VALUES (:id, :class, :status, :description, :why, :source, :primary_owner, :supporting_slices, :validation, :notes, :full_content, :superseded_by)`).run({
429
- ':id': r.id,
430
- ':class': r.class,
431
- ':status': r.status,
432
- ':description': r.description,
433
- ':why': r.why,
434
- ':source': r.source,
435
- ':primary_owner': r.primary_owner,
436
- ':supporting_slices': r.supporting_slices,
437
- ':validation': r.validation,
438
- ':notes': r.notes,
439
- ':full_content': r.full_content,
440
- ':superseded_by': r.superseded_by,
437
+ throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
438
+ currentDb
439
+ .prepare(`INSERT INTO requirements (id, class, status, description, why, source, primary_owner, supporting_slices, validation, notes, full_content, superseded_by)
440
+ VALUES (:id, :class, :status, :description, :why, :source, :primary_owner, :supporting_slices, :validation, :notes, :full_content, :superseded_by)`)
441
+ .run({
442
+ ":id": r.id,
443
+ ":class": r.class,
444
+ ":status": r.status,
445
+ ":description": r.description,
446
+ ":why": r.why,
447
+ ":source": r.source,
448
+ ":primary_owner": r.primary_owner,
449
+ ":supporting_slices": r.supporting_slices,
450
+ ":validation": r.validation,
451
+ ":notes": r.notes,
452
+ ":full_content": r.full_content,
453
+ ":superseded_by": r.superseded_by,
441
454
  });
442
455
  }
443
456
  /**
@@ -446,22 +459,24 @@ export function insertRequirement(r) {
446
459
  export function getRequirementById(id) {
447
460
  if (!currentDb)
448
461
  return null;
449
- const row = currentDb.prepare('SELECT * FROM requirements WHERE id = ?').get(id);
462
+ const row = currentDb
463
+ .prepare("SELECT * FROM requirements WHERE id = ?")
464
+ .get(id);
450
465
  if (!row)
451
466
  return null;
452
467
  return {
453
- id: row['id'],
454
- class: row['class'],
455
- status: row['status'],
456
- description: row['description'],
457
- why: row['why'],
458
- source: row['source'],
459
- primary_owner: row['primary_owner'],
460
- supporting_slices: row['supporting_slices'],
461
- validation: row['validation'],
462
- notes: row['notes'],
463
- full_content: row['full_content'],
464
- superseded_by: row['superseded_by'] ?? null,
468
+ id: row["id"],
469
+ class: row["class"],
470
+ status: row["status"],
471
+ description: row["description"],
472
+ why: row["why"],
473
+ source: row["source"],
474
+ primary_owner: row["primary_owner"],
475
+ supporting_slices: row["supporting_slices"],
476
+ validation: row["validation"],
477
+ notes: row["notes"],
478
+ full_content: row["full_content"],
479
+ superseded_by: row["superseded_by"] ?? null,
465
480
  };
466
481
  }
467
482
  /**
@@ -470,19 +485,19 @@ export function getRequirementById(id) {
470
485
  export function getActiveRequirements() {
471
486
  if (!currentDb)
472
487
  return [];
473
- const rows = currentDb.prepare('SELECT * FROM active_requirements').all();
474
- return rows.map(row => ({
475
- id: row['id'],
476
- class: row['class'],
477
- status: row['status'],
478
- description: row['description'],
479
- why: row['why'],
480
- source: row['source'],
481
- primary_owner: row['primary_owner'],
482
- supporting_slices: row['supporting_slices'],
483
- validation: row['validation'],
484
- notes: row['notes'],
485
- full_content: row['full_content'],
488
+ const rows = currentDb.prepare("SELECT * FROM active_requirements").all();
489
+ return rows.map((row) => ({
490
+ id: row["id"],
491
+ class: row["class"],
492
+ status: row["status"],
493
+ description: row["description"],
494
+ why: row["why"],
495
+ source: row["source"],
496
+ primary_owner: row["primary_owner"],
497
+ supporting_slices: row["supporting_slices"],
498
+ validation: row["validation"],
499
+ notes: row["notes"],
500
+ full_content: row["full_content"],
486
501
  superseded_by: null,
487
502
  }));
488
503
  }
@@ -520,17 +535,19 @@ export function _resetProvider() {
520
535
  */
521
536
  export function upsertDecision(d) {
522
537
  if (!currentDb)
523
- throw new GSDError(GSD_STALE_STATE, 'gsd-db: No database open');
524
- currentDb.prepare(`INSERT OR REPLACE INTO decisions (id, when_context, scope, decision, choice, rationale, revisable, superseded_by)
525
- VALUES (:id, :when_context, :scope, :decision, :choice, :rationale, :revisable, :superseded_by)`).run({
526
- ':id': d.id,
527
- ':when_context': d.when_context,
528
- ':scope': d.scope,
529
- ':decision': d.decision,
530
- ':choice': d.choice,
531
- ':rationale': d.rationale,
532
- ':revisable': d.revisable,
533
- ':superseded_by': d.superseded_by ?? null,
538
+ throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
539
+ currentDb
540
+ .prepare(`INSERT OR REPLACE INTO decisions (id, when_context, scope, decision, choice, rationale, revisable, superseded_by)
541
+ VALUES (:id, :when_context, :scope, :decision, :choice, :rationale, :revisable, :superseded_by)`)
542
+ .run({
543
+ ":id": d.id,
544
+ ":when_context": d.when_context,
545
+ ":scope": d.scope,
546
+ ":decision": d.decision,
547
+ ":choice": d.choice,
548
+ ":rationale": d.rationale,
549
+ ":revisable": d.revisable,
550
+ ":superseded_by": d.superseded_by ?? null,
534
551
  });
535
552
  }
536
553
  /**
@@ -538,21 +555,23 @@ export function upsertDecision(d) {
538
555
  */
539
556
  export function upsertRequirement(r) {
540
557
  if (!currentDb)
541
- throw new GSDError(GSD_STALE_STATE, 'gsd-db: No database open');
542
- currentDb.prepare(`INSERT OR REPLACE INTO requirements (id, class, status, description, why, source, primary_owner, supporting_slices, validation, notes, full_content, superseded_by)
543
- VALUES (:id, :class, :status, :description, :why, :source, :primary_owner, :supporting_slices, :validation, :notes, :full_content, :superseded_by)`).run({
544
- ':id': r.id,
545
- ':class': r.class,
546
- ':status': r.status,
547
- ':description': r.description,
548
- ':why': r.why,
549
- ':source': r.source,
550
- ':primary_owner': r.primary_owner,
551
- ':supporting_slices': r.supporting_slices,
552
- ':validation': r.validation,
553
- ':notes': r.notes,
554
- ':full_content': r.full_content,
555
- ':superseded_by': r.superseded_by ?? null,
558
+ throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
559
+ currentDb
560
+ .prepare(`INSERT OR REPLACE INTO requirements (id, class, status, description, why, source, primary_owner, supporting_slices, validation, notes, full_content, superseded_by)
561
+ VALUES (:id, :class, :status, :description, :why, :source, :primary_owner, :supporting_slices, :validation, :notes, :full_content, :superseded_by)`)
562
+ .run({
563
+ ":id": r.id,
564
+ ":class": r.class,
565
+ ":status": r.status,
566
+ ":description": r.description,
567
+ ":why": r.why,
568
+ ":source": r.source,
569
+ ":primary_owner": r.primary_owner,
570
+ ":supporting_slices": r.supporting_slices,
571
+ ":validation": r.validation,
572
+ ":notes": r.notes,
573
+ ":full_content": r.full_content,
574
+ ":superseded_by": r.superseded_by ?? null,
556
575
  });
557
576
  }
558
577
  /**
@@ -568,7 +587,7 @@ export function clearArtifacts() {
568
587
  if (!currentDb)
569
588
  return;
570
589
  try {
571
- currentDb.exec('DELETE FROM artifacts');
590
+ currentDb.exec("DELETE FROM artifacts");
572
591
  }
573
592
  catch {
574
593
  // Clearing a cache should never be fatal
@@ -576,15 +595,141 @@ export function clearArtifacts() {
576
595
  }
577
596
  export function insertArtifact(a) {
578
597
  if (!currentDb)
579
- throw new GSDError(GSD_STALE_STATE, 'gsd-db: No database open');
580
- currentDb.prepare(`INSERT OR REPLACE INTO artifacts (path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at)
581
- VALUES (:path, :artifact_type, :milestone_id, :slice_id, :task_id, :full_content, :imported_at)`).run({
582
- ':path': a.path,
583
- ':artifact_type': a.artifact_type,
584
- ':milestone_id': a.milestone_id,
585
- ':slice_id': a.slice_id,
586
- ':task_id': a.task_id,
587
- ':full_content': a.full_content,
588
- ':imported_at': new Date().toISOString(),
598
+ throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
599
+ currentDb
600
+ .prepare(`INSERT OR REPLACE INTO artifacts (path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at)
601
+ VALUES (:path, :artifact_type, :milestone_id, :slice_id, :task_id, :full_content, :imported_at)`)
602
+ .run({
603
+ ":path": a.path,
604
+ ":artifact_type": a.artifact_type,
605
+ ":milestone_id": a.milestone_id,
606
+ ":slice_id": a.slice_id,
607
+ ":task_id": a.task_id,
608
+ ":full_content": a.full_content,
609
+ ":imported_at": new Date().toISOString(),
589
610
  });
590
611
  }
612
+ // ─── Worktree DB Helpers ──────────────────────────────────────────────────
613
+ export function copyWorktreeDb(srcDbPath, destDbPath) {
614
+ try {
615
+ if (!existsSync(srcDbPath))
616
+ return false;
617
+ const destDir = dirname(destDbPath);
618
+ mkdirSync(destDir, { recursive: true });
619
+ copyFileSync(srcDbPath, destDbPath);
620
+ return true;
621
+ }
622
+ catch (err) {
623
+ process.stderr.write(`gsd-db: failed to copy DB to worktree: ${err.message}\n`);
624
+ return false;
625
+ }
626
+ }
627
+ export function reconcileWorktreeDb(mainDbPath, worktreeDbPath) {
628
+ const zero = {
629
+ decisions: 0,
630
+ requirements: 0,
631
+ artifacts: 0,
632
+ conflicts: [],
633
+ };
634
+ if (!existsSync(worktreeDbPath))
635
+ return zero;
636
+ if (worktreeDbPath.includes("'")) {
637
+ process.stderr.write(`gsd-db: worktree DB reconciliation failed: path contains unsafe characters\n`);
638
+ return zero;
639
+ }
640
+ if (!currentDb) {
641
+ const opened = openDatabase(mainDbPath);
642
+ if (!opened) {
643
+ process.stderr.write(`gsd-db: worktree DB reconciliation failed: cannot open main DB\n`);
644
+ return zero;
645
+ }
646
+ }
647
+ const adapter = currentDb;
648
+ const conflicts = [];
649
+ try {
650
+ adapter.exec(`ATTACH DATABASE '${worktreeDbPath}' AS wt`);
651
+ try {
652
+ const decConf = adapter
653
+ .prepare(`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`)
654
+ .all();
655
+ for (const row of decConf)
656
+ conflicts.push(`decision ${row["id"]}: modified in both`);
657
+ const reqConf = adapter
658
+ .prepare(`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`)
659
+ .all();
660
+ for (const row of reqConf)
661
+ conflicts.push(`requirement ${row["id"]}: modified in both`);
662
+ const merged = { decisions: 0, requirements: 0, artifacts: 0 };
663
+ adapter.exec("BEGIN");
664
+ try {
665
+ const dR = adapter
666
+ .prepare(`
667
+ INSERT OR REPLACE INTO decisions (
668
+ id, when_context, scope, decision, choice, rationale, revisable, superseded_by
669
+ )
670
+ SELECT
671
+ id, when_context, scope, decision, choice, rationale, revisable, superseded_by
672
+ FROM wt.decisions
673
+ `)
674
+ .run();
675
+ merged.decisions =
676
+ typeof dR === "object" && dR !== null
677
+ ? (dR.changes ?? 0)
678
+ : 0;
679
+ const rR = adapter
680
+ .prepare(`
681
+ INSERT OR REPLACE INTO requirements (
682
+ id, class, status, description, why, source, primary_owner,
683
+ supporting_slices, validation, notes, full_content, superseded_by
684
+ )
685
+ SELECT
686
+ id, class, status, description, why, source, primary_owner,
687
+ supporting_slices, validation, notes, full_content, superseded_by
688
+ FROM wt.requirements
689
+ `)
690
+ .run();
691
+ merged.requirements =
692
+ typeof rR === "object" && rR !== null
693
+ ? (rR.changes ?? 0)
694
+ : 0;
695
+ const aR = adapter
696
+ .prepare(`
697
+ INSERT OR REPLACE INTO artifacts (
698
+ path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at
699
+ )
700
+ SELECT
701
+ path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at
702
+ FROM wt.artifacts
703
+ `)
704
+ .run();
705
+ merged.artifacts =
706
+ typeof aR === "object" && aR !== null
707
+ ? (aR.changes ?? 0)
708
+ : 0;
709
+ adapter.exec("COMMIT");
710
+ }
711
+ catch (txErr) {
712
+ try {
713
+ adapter.exec("ROLLBACK");
714
+ }
715
+ catch {
716
+ /* best-effort */
717
+ }
718
+ throw txErr;
719
+ }
720
+ return { ...merged, conflicts };
721
+ }
722
+ finally {
723
+ try {
724
+ adapter.exec("DETACH DATABASE wt");
725
+ }
726
+ catch {
727
+ /* best-effort */
728
+ }
729
+ }
730
+ }
731
+ catch (err) {
732
+ process.stderr.write(`gsd-db: worktree DB reconciliation failed: ${err.message}\n`);
733
+ return { ...zero, conflicts };
734
+ }
735
+ }