gsd-pi 2.33.1-dev.ee47f1b → 2.34.0-dev.bbb5216
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundled-resource-path.d.ts +8 -0
- package/dist/bundled-resource-path.js +14 -0
- package/dist/headless-query.js +6 -6
- package/dist/resources/extensions/gsd/auto/session.js +27 -32
- package/dist/resources/extensions/gsd/auto-dashboard.js +29 -109
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +6 -1
- package/dist/resources/extensions/gsd/auto-dispatch.js +52 -81
- package/dist/resources/extensions/gsd/auto-loop.js +956 -0
- package/dist/resources/extensions/gsd/auto-observability.js +4 -2
- package/dist/resources/extensions/gsd/auto-post-unit.js +75 -185
- package/dist/resources/extensions/gsd/auto-prompts.js +133 -101
- package/dist/resources/extensions/gsd/auto-recovery.js +59 -97
- package/dist/resources/extensions/gsd/auto-start.js +330 -309
- package/dist/resources/extensions/gsd/auto-supervisor.js +5 -11
- package/dist/resources/extensions/gsd/auto-timeout-recovery.js +7 -7
- package/dist/resources/extensions/gsd/auto-timers.js +3 -4
- package/dist/resources/extensions/gsd/auto-verification.js +35 -73
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +167 -0
- package/dist/resources/extensions/gsd/auto-worktree.js +291 -126
- package/dist/resources/extensions/gsd/auto.js +283 -1013
- package/dist/resources/extensions/gsd/captures.js +10 -4
- package/dist/resources/extensions/gsd/dispatch-guard.js +7 -8
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -18
- package/dist/resources/extensions/gsd/doctor-checks.js +3 -4
- package/dist/resources/extensions/gsd/git-service.js +1 -1
- package/dist/resources/extensions/gsd/gsd-db.js +296 -151
- package/dist/resources/extensions/gsd/index.js +92 -228
- package/dist/resources/extensions/gsd/post-unit-hooks.js +13 -13
- package/dist/resources/extensions/gsd/progress-score.js +61 -156
- package/dist/resources/extensions/gsd/quick.js +98 -122
- package/dist/resources/extensions/gsd/session-lock.js +13 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
- package/dist/resources/extensions/gsd/undo.js +43 -48
- package/dist/resources/extensions/gsd/unit-runtime.js +16 -15
- package/dist/resources/extensions/gsd/verification-evidence.js +0 -1
- package/dist/resources/extensions/gsd/verification-gate.js +6 -35
- package/dist/resources/extensions/gsd/worktree-command.js +30 -24
- package/dist/resources/extensions/gsd/worktree-manager.js +2 -3
- package/dist/resources/extensions/gsd/worktree-resolver.js +344 -0
- package/dist/resources/extensions/gsd/worktree.js +7 -44
- package/dist/tool-bootstrap.js +59 -11
- package/dist/worktree-cli.js +7 -7
- package/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +3630 -5483
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +735 -2588
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/src/models.generated.ts +1039 -2892
- package/packages/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/session.ts +47 -30
- package/src/resources/extensions/gsd/auto-dashboard.ts +28 -131
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +6 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +135 -91
- package/src/resources/extensions/gsd/auto-loop.ts +1665 -0
- package/src/resources/extensions/gsd/auto-observability.ts +4 -2
- package/src/resources/extensions/gsd/auto-post-unit.ts +85 -228
- package/src/resources/extensions/gsd/auto-prompts.ts +138 -109
- package/src/resources/extensions/gsd/auto-recovery.ts +124 -118
- package/src/resources/extensions/gsd/auto-start.ts +440 -354
- package/src/resources/extensions/gsd/auto-supervisor.ts +5 -12
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +8 -8
- package/src/resources/extensions/gsd/auto-timers.ts +3 -4
- package/src/resources/extensions/gsd/auto-verification.ts +76 -90
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +204 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +389 -141
- package/src/resources/extensions/gsd/auto.ts +515 -1199
- package/src/resources/extensions/gsd/captures.ts +10 -4
- package/src/resources/extensions/gsd/dispatch-guard.ts +13 -9
- package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -18
- package/src/resources/extensions/gsd/doctor-checks.ts +3 -4
- package/src/resources/extensions/gsd/git-service.ts +8 -1
- package/src/resources/extensions/gsd/gitignore.ts +4 -2
- package/src/resources/extensions/gsd/gsd-db.ts +375 -180
- package/src/resources/extensions/gsd/index.ts +104 -263
- package/src/resources/extensions/gsd/post-unit-hooks.ts +13 -13
- package/src/resources/extensions/gsd/progress-score.ts +65 -200
- package/src/resources/extensions/gsd/quick.ts +121 -125
- package/src/resources/extensions/gsd/session-lock.ts +11 -0
- package/src/resources/extensions/gsd/templates/preferences.md +1 -0
- package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +32 -59
- package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +75 -27
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +37 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1458 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +8 -162
- package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -108
- package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +1 -3
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -55
- package/src/resources/extensions/gsd/tests/headless-query.test.ts +22 -0
- package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +8 -11
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +4 -6
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +181 -0
- package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +6 -6
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -6
- package/src/resources/extensions/gsd/tests/undo.test.ts +6 -0
- package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +24 -26
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +7 -201
- package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
- package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
- package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +705 -0
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +57 -106
- package/src/resources/extensions/gsd/tests/worktree.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +43 -132
- package/src/resources/extensions/gsd/types.ts +90 -81
- package/src/resources/extensions/gsd/undo.ts +42 -46
- package/src/resources/extensions/gsd/unit-runtime.ts +14 -18
- package/src/resources/extensions/gsd/verification-evidence.ts +1 -3
- package/src/resources/extensions/gsd/verification-gate.ts +6 -39
- package/src/resources/extensions/gsd/worktree-command.ts +36 -24
- package/src/resources/extensions/gsd/worktree-manager.ts +2 -3
- package/src/resources/extensions/gsd/worktree-resolver.ts +485 -0
- package/src/resources/extensions/gsd/worktree.ts +7 -44
- package/dist/resources/extensions/gsd/auto-constants.js +0 -5
- package/dist/resources/extensions/gsd/auto-idempotency.js +0 -106
- package/dist/resources/extensions/gsd/auto-stuck-detection.js +0 -165
- package/dist/resources/extensions/gsd/mechanical-completion.js +0 -351
- package/src/resources/extensions/gsd/auto-constants.ts +0 -6
- package/src/resources/extensions/gsd/auto-idempotency.ts +0 -151
- package/src/resources/extensions/gsd/auto-stuck-detection.ts +0 -221
- package/src/resources/extensions/gsd/mechanical-completion.ts +0 -430
- package/src/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +0 -691
- package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +0 -127
- package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +0 -123
- package/src/resources/extensions/gsd/tests/dispatch-stall-guard.test.ts +0 -126
- package/src/resources/extensions/gsd/tests/loop-regression.test.ts +0 -874
- package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +0 -356
- package/src/resources/extensions/gsd/tests/progress-score.test.ts +0 -206
- package/src/resources/extensions/gsd/tests/session-lock.test.ts +0 -434
|
@@ -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
|
|
9
|
-
import { existsSync } from
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
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[]):
|
|
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 =
|
|
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 ===
|
|
50
|
+
event === "warning" &&
|
|
50
51
|
args[0] &&
|
|
51
|
-
typeof args[0] ===
|
|
52
|
-
|
|
53
|
-
(args[0] as { name: string }).name ===
|
|
54
|
-
|
|
55
|
-
typeof (args[0] as { message: string }).message ===
|
|
56
|
-
(args[0] as { message: string }).message.includes(
|
|
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<
|
|
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(
|
|
74
|
+
const mod = _require("node:sqlite");
|
|
72
75
|
if (mod.DatabaseSync) {
|
|
73
76
|
providerModule = mod;
|
|
74
|
-
providerName =
|
|
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(
|
|
84
|
-
if (typeof mod ===
|
|
86
|
+
const mod = _require("better-sqlite3");
|
|
87
|
+
if (typeof mod === "function" || (mod && mod.default)) {
|
|
85
88
|
providerModule = mod.default || mod;
|
|
86
|
-
providerName =
|
|
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(
|
|
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[]):
|
|
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 ===
|
|
153
|
-
const { DatabaseSync } = providerModule as {
|
|
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(
|
|
176
|
+
db.exec("PRAGMA journal_mode=WAL");
|
|
170
177
|
}
|
|
171
178
|
|
|
172
|
-
db.exec(
|
|
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(
|
|
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(
|
|
252
|
-
|
|
253
|
-
|
|
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
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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(
|
|
283
|
+
db.exec("COMMIT");
|
|
264
284
|
} catch (err) {
|
|
265
|
-
db.exec(
|
|
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(
|
|
279
|
-
const currentVersion = row ? (row[
|
|
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(
|
|
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(
|
|
300
|
-
|
|
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(
|
|
331
|
-
|
|
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(
|
|
363
|
+
db.exec("COMMIT");
|
|
340
364
|
} catch (err) {
|
|
341
|
-
db.exec(
|
|
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 !==
|
|
412
|
+
const fileBacked = path !== ":memory:";
|
|
389
413
|
|
|
390
414
|
try {
|
|
391
415
|
initSchema(adapter, fileBacked);
|
|
392
416
|
} catch (err) {
|
|
393
|
-
try {
|
|
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)
|
|
424
|
-
|
|
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(
|
|
456
|
+
currentDb.exec("COMMIT");
|
|
428
457
|
return result;
|
|
429
458
|
} catch (err) {
|
|
430
|
-
currentDb.exec(
|
|
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,
|
|
441
|
-
if (!currentDb)
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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(
|
|
494
|
+
const row = currentDb.prepare("SELECT * FROM decisions WHERE id = ?").get(id);
|
|
463
495
|
if (!row) return null;
|
|
464
496
|
return {
|
|
465
|
-
seq: row[
|
|
466
|
-
id: row[
|
|
467
|
-
when_context: row[
|
|
468
|
-
scope: row[
|
|
469
|
-
decision: row[
|
|
470
|
-
choice: row[
|
|
471
|
-
rationale: row[
|
|
472
|
-
revisable: row[
|
|
473
|
-
superseded_by: (row[
|
|
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(
|
|
483
|
-
return rows.map(row => ({
|
|
484
|
-
seq: row[
|
|
485
|
-
id: row[
|
|
486
|
-
when_context: row[
|
|
487
|
-
scope: row[
|
|
488
|
-
decision: row[
|
|
489
|
-
choice: row[
|
|
490
|
-
rationale: row[
|
|
491
|
-
revisable: row[
|
|
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)
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
|
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[
|
|
531
|
-
class: row[
|
|
532
|
-
status: row[
|
|
533
|
-
description: row[
|
|
534
|
-
why: row[
|
|
535
|
-
source: row[
|
|
536
|
-
primary_owner: row[
|
|
537
|
-
supporting_slices: row[
|
|
538
|
-
validation: row[
|
|
539
|
-
notes: row[
|
|
540
|
-
full_content: row[
|
|
541
|
-
superseded_by: (row[
|
|
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(
|
|
551
|
-
return rows.map(row => ({
|
|
552
|
-
id: row[
|
|
553
|
-
class: row[
|
|
554
|
-
status: row[
|
|
555
|
-
description: row[
|
|
556
|
-
why: row[
|
|
557
|
-
source: row[
|
|
558
|
-
primary_owner: row[
|
|
559
|
-
supporting_slices: row[
|
|
560
|
-
validation: row[
|
|
561
|
-
notes: row[
|
|
562
|
-
full_content: row[
|
|
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,
|
|
606
|
-
if (!currentDb)
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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)
|
|
627
|
-
|
|
628
|
-
|
|
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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(
|
|
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)
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
}
|