@hegemonart/get-design-done 1.57.0 → 1.57.2
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/.claude-plugin/marketplace.json +26 -41
- package/.claude-plugin/plugin.json +23 -48
- package/CHANGELOG.md +119 -0
- package/README.md +166 -511
- package/SKILL.md +2 -0
- package/agents/README.md +33 -36
- package/agents/a11y-mapper.md +3 -3
- package/agents/component-benchmark-harvester.md +6 -6
- package/agents/component-benchmark-synthesizer.md +3 -3
- package/agents/compose-executor.md +3 -3
- package/agents/cost-forecaster.md +2 -2
- package/agents/design-auditor.md +7 -7
- package/agents/design-authority-watcher.md +15 -15
- package/agents/design-context-builder.md +4 -4
- package/agents/design-context-checker-gate.md +1 -1
- package/agents/design-discussant.md +2 -2
- package/agents/design-doc-writer.md +1 -1
- package/agents/design-executor.md +2 -2
- package/agents/design-figma-writer.md +2 -2
- package/agents/design-fixer.md +7 -7
- package/agents/design-integration-checker-gate.md +1 -1
- package/agents/design-integration-checker.md +1 -1
- package/agents/design-paper-writer.md +3 -3
- package/agents/design-pencil-writer.md +1 -1
- package/agents/design-planner.md +21 -0
- package/agents/design-reflector.md +39 -39
- package/agents/design-research-synthesizer.md +1 -0
- package/agents/design-start-writer.md +1 -1
- package/agents/design-update-checker.md +5 -5
- package/agents/design-verifier-gate.md +1 -1
- package/agents/design-verifier.md +52 -48
- package/agents/ds-generator.md +2 -2
- package/agents/ds-migration-planner.md +4 -4
- package/agents/email-executor.md +9 -9
- package/agents/experiment-result-ingester.md +3 -3
- package/agents/flutter-executor.md +5 -5
- package/agents/gdd-graph-refresh.md +3 -3
- package/agents/gdd-intel-updater.md +2 -2
- package/agents/motion-mapper.md +2 -2
- package/agents/motion-verifier.md +4 -4
- package/agents/pdf-executor.md +8 -8
- package/agents/perf-analyzer.md +17 -17
- package/agents/pr-commenter.md +9 -9
- package/agents/prototype-gate.md +2 -2
- package/agents/quality-gate-runner.md +1 -1
- package/agents/rollout-coordinator.md +3 -3
- package/agents/swift-executor.md +4 -4
- package/agents/ticket-sync-agent.md +6 -6
- package/agents/user-research-synthesizer.md +2 -2
- package/connections/connections.md +44 -45
- package/connections/cursor.md +73 -0
- package/connections/preview.md +3 -3
- package/dist/claude-code/.claude/skills/cache-manager/SKILL.md +3 -3
- package/dist/claude-code/.claude/skills/cache-manager/cache-policy.md +1 -1
- package/dist/claude-code/.claude/skills/design/SKILL.md +19 -0
- package/dist/claude-code/.claude/skills/explore/SKILL.md +11 -0
- package/dist/claude-code/.claude/skills/figma-write/SKILL.md +13 -2
- package/dist/claude-code/.claude/skills/paper-write/SKILL.md +54 -0
- package/dist/claude-code/.claude/skills/pencil-write/SKILL.md +54 -0
- package/dist/claude-code/.claude/skills/report-issue/SKILL.md +2 -2
- package/dist/claude-code/.claude/skills/router/SKILL.md +2 -2
- package/dist/claude-code/.claude/skills/verify/verify-procedure.md +10 -11
- package/dist/claude-code/.claude/skills/warm-cache/SKILL.md +1 -1
- package/hooks/budget-enforcer.ts +5 -4
- package/hooks/first-run-nudge.cjs +171 -0
- package/hooks/gdd-intel-trigger.js +243 -0
- package/hooks/gdd-mcp-circuit-breaker.js +62 -7
- package/hooks/gdd-precompact-snapshot.js +50 -29
- package/hooks/gdd-protected-paths.js +175 -20
- package/hooks/gdd-read-injection-scanner.ts +9 -1
- package/hooks/gdd-risk-gate.js +110 -8
- package/hooks/gdd-sessionstart-recap.js +59 -24
- package/hooks/hooks.json +13 -4
- package/hooks/inject-using-gdd.cjs +188 -0
- package/hooks/update-check.cjs +511 -0
- package/package.json +9 -2
- package/reference/STATE-TEMPLATE.md +10 -13
- package/reference/audit-scoring.md +1 -1
- package/reference/cache-tier-doctrine.md +46 -0
- package/reference/config-schema.md +9 -9
- package/reference/i18n.md +1 -1
- package/reference/intel-schema.md +37 -2
- package/reference/meta-rules.md +4 -4
- package/reference/model-tiers.md +2 -2
- package/reference/registry.json +101 -94
- package/reference/runtime-models.md +11 -1
- package/reference/shared-preamble.md +13 -14
- package/reference/skill-graph.md +24 -1
- package/scripts/bootstrap.cjs +373 -0
- package/scripts/injection-patterns.cjs +58 -0
- package/scripts/lib/apply-reflections/incubator-proposals.cjs +57 -26
- package/scripts/lib/design-search.cjs +20 -2
- package/scripts/lib/install/converters/codex-plugin.cjs +5 -2
- package/scripts/lib/install/converters/cursor.cjs +20 -0
- package/scripts/lib/issue-reporter/report-flow.cjs +1 -1
- package/scripts/lib/manifest/skills.json +80 -13
- package/scripts/lib/state/migrate-to-sqlite.cjs +23 -7
- package/scripts/lib/state/query-surface.cjs +86 -16
- package/scripts/lib/state/render-markdown.cjs +26 -14
- package/scripts/lib/state/state-store.cjs +141 -68
- package/sdk/cli/commands/stage.ts +17 -0
- package/sdk/cli/index.js +21 -1
- package/sdk/dashboard/data/_pkg-root.cjs +4 -4
- package/sdk/dashboard/data/risk-surface.cjs +54 -19
- package/sdk/dashboard/tui/index.cjs +28 -2
- package/sdk/mcp/gdd-state/server.js +7 -1
- package/sdk/state/index.ts +11 -1
- package/skills/cache-manager/SKILL.md +3 -3
- package/skills/cache-manager/cache-policy.md +1 -1
- package/skills/design/SKILL.md +19 -0
- package/skills/explore/SKILL.md +11 -0
- package/skills/figma-write/SKILL.md +13 -2
- package/skills/paper-write/SKILL.md +54 -0
- package/skills/pencil-write/SKILL.md +54 -0
- package/skills/report-issue/SKILL.md +2 -2
- package/skills/router/SKILL.md +2 -2
- package/skills/verify/verify-procedure.md +10 -11
- package/skills/warm-cache/SKILL.md +1 -1
- package/hooks/first-run-nudge.sh +0 -82
- package/hooks/inject-using-gdd.sh +0 -72
- package/hooks/update-check.sh +0 -251
- package/scripts/lib/audit-aggregator/index.cjs +0 -219
- package/scripts/lib/hedge-ensemble.cjs +0 -217
|
@@ -46,7 +46,8 @@ function _findPackageRoot(startDir) {
|
|
|
46
46
|
const PKG_ROOT = _findPackageRoot(__dirname);
|
|
47
47
|
|
|
48
48
|
// ---------------------------------------------------------------------------
|
|
49
|
-
// Lazy-require state-backend.cjs (
|
|
49
|
+
// Lazy-require state-backend.cjs (loaded on first call so optional better-sqlite3
|
|
50
|
+
// binding does not crash module load).
|
|
50
51
|
// ---------------------------------------------------------------------------
|
|
51
52
|
let _backend = null;
|
|
52
53
|
function _requireBackend() {
|
|
@@ -65,7 +66,7 @@ function _requireBackend() {
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
// ---------------------------------------------------------------------------
|
|
68
|
-
// Lazy-require migrate-to-sqlite.cjs
|
|
69
|
+
// Lazy-require migrate-to-sqlite.cjs - used by recover().
|
|
69
70
|
// ---------------------------------------------------------------------------
|
|
70
71
|
let _migrate = null;
|
|
71
72
|
function _requireMigrate() {
|
|
@@ -128,8 +129,13 @@ function _firstToken(sql) {
|
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
/**
|
|
131
|
-
* Assert that the SQL query is a safe readonly SELECT.
|
|
132
|
-
* Throws with a descriptive message when the first token is denied or not SELECT.
|
|
132
|
+
* Assert that the SQL query is a safe readonly SELECT (or CTE: WITH ... SELECT).
|
|
133
|
+
* Throws with a descriptive message when the first token is denied or not SELECT/WITH.
|
|
134
|
+
*
|
|
135
|
+
* BUG-11: allow a leading WITH token for CTEs (WITH ... SELECT ...).
|
|
136
|
+
* The engine-level readonly connection already blocks any write CTE.
|
|
137
|
+
* We keep blocking all other non-SELECT first tokens.
|
|
138
|
+
*
|
|
133
139
|
* @param {string} sql
|
|
134
140
|
*/
|
|
135
141
|
function _assertReadonly(sql) {
|
|
@@ -142,7 +148,8 @@ function _assertReadonly(sql) {
|
|
|
142
148
|
`query-surface: statement type '${token}' is not allowed (denylist). Only SELECT is permitted.`
|
|
143
149
|
);
|
|
144
150
|
}
|
|
145
|
-
|
|
151
|
+
// Allow WITH for CTEs (WITH ... SELECT ...) — engine readonly blocks any write CTE.
|
|
152
|
+
if (token !== 'SELECT' && token !== 'WITH') {
|
|
146
153
|
throw new Error(
|
|
147
154
|
`query-surface: first token '${token}' is not SELECT. Only SELECT queries are permitted.`
|
|
148
155
|
);
|
|
@@ -196,6 +203,42 @@ function query(sql, opts = {}) {
|
|
|
196
203
|
}
|
|
197
204
|
}
|
|
198
205
|
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// _safeBackup(srcPath, bakPath) - H5 backup-guard.
|
|
208
|
+
//
|
|
209
|
+
// Copy srcPath to bakPath, then verify the backup exists AND is non-empty
|
|
210
|
+
// AFTER the copy. Returns true only when the backup is a faithful non-empty
|
|
211
|
+
// copy of the source. Callers MUST check the return value before unlinking
|
|
212
|
+
// the source - the dangerous pattern is `copy → unconditional unlink`, where
|
|
213
|
+
// a silent copy failure (or zero-byte destination) means the unlink deletes
|
|
214
|
+
// the only remaining data.
|
|
215
|
+
//
|
|
216
|
+
// Defensive: never throws. Returns false on any error or empty backup.
|
|
217
|
+
//
|
|
218
|
+
// @param {string} srcPath path to source file (must exist)
|
|
219
|
+
// @param {string} bakPath path for the backup (created/overwritten)
|
|
220
|
+
// @returns {boolean} true iff bakPath exists and is non-empty after copy
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
function _safeBackup(srcPath, bakPath) {
|
|
224
|
+
try {
|
|
225
|
+
fs.copyFileSync(srcPath, bakPath);
|
|
226
|
+
} catch {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
// Post-copy verification: the backup must EXIST and be NON-EMPTY.
|
|
230
|
+
// copyFileSync can silently produce a 0-byte file in some failure modes
|
|
231
|
+
// (interrupted IO, full disk after open). An empty backup is not a backup;
|
|
232
|
+
// unlinking the source after one would destroy the data.
|
|
233
|
+
try {
|
|
234
|
+
const st = fs.statSync(bakPath);
|
|
235
|
+
if (!st.isFile() || st.size === 0) return false;
|
|
236
|
+
return true;
|
|
237
|
+
} catch {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
199
242
|
// ---------------------------------------------------------------------------
|
|
200
243
|
// rotateBak(dbPath) - shift .bak.0..9, cap at 10.
|
|
201
244
|
// ---------------------------------------------------------------------------
|
|
@@ -241,12 +284,10 @@ function backupCycle(opts = {}) {
|
|
|
241
284
|
}
|
|
242
285
|
rotateBak(dbPath);
|
|
243
286
|
const bak0 = `${dbPath}.bak.0`;
|
|
244
|
-
|
|
245
|
-
fs.copyFileSync(dbPath, bak0);
|
|
287
|
+
if (_safeBackup(dbPath, bak0)) {
|
|
246
288
|
return { backed_up: true, path: bak0 };
|
|
247
|
-
} catch (err) {
|
|
248
|
-
return { backed_up: false, message: `backupCycle: copy failed: ${err.message}` };
|
|
249
289
|
}
|
|
290
|
+
return { backed_up: false, message: `backupCycle: copy failed or backup is empty at ${bak0}` };
|
|
250
291
|
}
|
|
251
292
|
|
|
252
293
|
// ---------------------------------------------------------------------------
|
|
@@ -276,9 +317,18 @@ function demigrate(opts = {}) {
|
|
|
276
317
|
};
|
|
277
318
|
}
|
|
278
319
|
// Take a backup before removing.
|
|
320
|
+
// H5 backup-guard: only unlink when the backup is a faithful non-empty copy.
|
|
321
|
+
// If the copy failed (or produced a 0-byte file), refuse to unlink the source -
|
|
322
|
+
// we'd be deleting the only remaining data.
|
|
279
323
|
rotateBak(dbPath);
|
|
280
324
|
const bak0 = `${dbPath}.bak.0`;
|
|
281
|
-
|
|
325
|
+
if (!_safeBackup(dbPath, bak0)) {
|
|
326
|
+
return {
|
|
327
|
+
demigrated: false,
|
|
328
|
+
message: `demigrate: refusing to remove ${dbPath} - backup at ${bak0} ` +
|
|
329
|
+
`is missing or empty after copyFileSync (would lose data).`,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
282
332
|
try {
|
|
283
333
|
fs.unlinkSync(dbPath);
|
|
284
334
|
} catch (err) {
|
|
@@ -305,10 +355,13 @@ function demigrate(opts = {}) {
|
|
|
305
355
|
* 3. Invoke migrate-to-sqlite with force:true to rebuild from markdown.
|
|
306
356
|
* 4. Run integrity_check on the new database.
|
|
307
357
|
*
|
|
358
|
+
* BUG-03/08: recover() is now async — it awaits migrateToSqlite() so that
|
|
359
|
+
* state.sqlite actually exists before the integrity check is run.
|
|
360
|
+
*
|
|
308
361
|
* @param {{ projectRoot?: string, dbPath?: string }} [opts]
|
|
309
|
-
* @returns {{ recovered: boolean, message: string, integrity?: boolean }}
|
|
362
|
+
* @returns {Promise<{ recovered: boolean, message: string, integrity?: boolean }>}
|
|
310
363
|
*/
|
|
311
|
-
function recover(opts = {}) {
|
|
364
|
+
async function recover(opts = {}) {
|
|
312
365
|
const backend = _requireBackend();
|
|
313
366
|
if (!backend || backend.BACKEND !== 'sqlite') {
|
|
314
367
|
return {
|
|
@@ -321,10 +374,23 @@ function recover(opts = {}) {
|
|
|
321
374
|
const dbPath = opts.dbPath || backend.sqlitePath(opts.projectRoot || process.cwd());
|
|
322
375
|
|
|
323
376
|
// Step 1: Rotate existing (possibly corrupt) file to .bak.0.
|
|
377
|
+
// H5 backup-guard: only unlink the source after a verified non-empty backup.
|
|
378
|
+
// For recover() the source MAY already be corrupt - so an empty/failed backup
|
|
379
|
+
// is still significant signal. We refuse to unlink when the backup is missing
|
|
380
|
+
// OR zero bytes, so the operator retains a copy of the corrupt file for
|
|
381
|
+
// diagnostics. The caller can manually delete and retry once the backup
|
|
382
|
+
// location is writable.
|
|
324
383
|
if (fs.existsSync(dbPath)) {
|
|
325
384
|
rotateBak(dbPath);
|
|
326
385
|
const bak0 = `${dbPath}.bak.0`;
|
|
327
|
-
|
|
386
|
+
if (!_safeBackup(dbPath, bak0)) {
|
|
387
|
+
return {
|
|
388
|
+
recovered: false,
|
|
389
|
+
message: `recover: refusing to remove ${dbPath} - backup at ${bak0} ` +
|
|
390
|
+
`is missing or empty after copyFileSync (would lose corrupt file ` +
|
|
391
|
+
`before rebuild). Resolve disk/permission issues and retry.`,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
328
394
|
try { fs.unlinkSync(dbPath); } catch (err) {
|
|
329
395
|
return { recovered: false, message: `recover: could not remove corrupt ${dbPath}: ${err.message}` };
|
|
330
396
|
}
|
|
@@ -335,7 +401,7 @@ function recover(opts = {}) {
|
|
|
335
401
|
if (!migrate) {
|
|
336
402
|
return {
|
|
337
403
|
recovered: false,
|
|
338
|
-
message: 'recover: migrate-to-sqlite.cjs not
|
|
404
|
+
message: 'recover: migrate-to-sqlite.cjs could not be loaded (require failed). ' +
|
|
339
405
|
'Cannot rebuild SQLite from markdown.',
|
|
340
406
|
};
|
|
341
407
|
}
|
|
@@ -343,13 +409,16 @@ function recover(opts = {}) {
|
|
|
343
409
|
let migrateResult = null;
|
|
344
410
|
try {
|
|
345
411
|
if (typeof migrate.migrateToSqlite === 'function') {
|
|
346
|
-
|
|
412
|
+
// BUG-03/08: await the async migrateToSqlite so state.sqlite is written before
|
|
413
|
+
// the integrity check below. Without await, recover() returned before the DB
|
|
414
|
+
// existed, causing integrity:false on every call.
|
|
415
|
+
migrateResult = await migrate.migrateToSqlite({
|
|
347
416
|
projectRoot: opts.projectRoot,
|
|
348
417
|
dbPath,
|
|
349
418
|
force: true,
|
|
350
419
|
});
|
|
351
420
|
} else if (typeof migrate.migrate === 'function') {
|
|
352
|
-
migrateResult = migrate.migrate({ projectRoot: opts.projectRoot, dbPath, force: true });
|
|
421
|
+
migrateResult = await migrate.migrate({ projectRoot: opts.projectRoot, dbPath, force: true });
|
|
353
422
|
} else {
|
|
354
423
|
return { recovered: false, message: 'recover: migrate-to-sqlite.cjs has no recognized export.' };
|
|
355
424
|
}
|
|
@@ -387,5 +456,6 @@ module.exports = {
|
|
|
387
456
|
// Expose internals for testing.
|
|
388
457
|
_assertReadonly,
|
|
389
458
|
_firstToken,
|
|
459
|
+
_safeBackup,
|
|
390
460
|
DENIED_TOKENS,
|
|
391
461
|
};
|
|
@@ -376,21 +376,33 @@ function renderStateMarkdown(db, cycle_id, sdk) {
|
|
|
376
376
|
}
|
|
377
377
|
|
|
378
378
|
// --- blockers raw_body ---
|
|
379
|
-
//
|
|
379
|
+
// activeBlockers is used both for raw_bodies reconstruction and the blockers state array below.
|
|
380
380
|
const activeBlockers = blockerRows.filter((r) => !r.resolved_at);
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
} else
|
|
393
|
-
|
|
381
|
+
|
|
382
|
+
// BUG-09: when _block_meta stores a non-null raw_body for 'blockers', emit it
|
|
383
|
+
// verbatim (like unstructured blocks). This preserves comment lines inside
|
|
384
|
+
// <blockers> that would otherwise be silently dropped when rebuilding from rows.
|
|
385
|
+
//
|
|
386
|
+
// Fall through to row-reconstruction only when raw_body is absent (null), which
|
|
387
|
+
// happens after an appendBlocker() call that doesn't update _block_meta.raw_body.
|
|
388
|
+
const blockersRawBody = blockRawBodies['blockers'];
|
|
389
|
+
if (blockersRawBody !== undefined && blockersRawBody !== null) {
|
|
390
|
+
// Verbatim round-trip: emit stored raw_body (preserves comments).
|
|
391
|
+
raw_bodies.blockers = blockersRawBody;
|
|
392
|
+
} else {
|
|
393
|
+
// Reconstruct from rows (no stored raw_body).
|
|
394
|
+
// Only unresolved blockers go in the STATE.md <blockers> block.
|
|
395
|
+
if (activeBlockers.length > 0) {
|
|
396
|
+
const lines = activeBlockers.map((row) => {
|
|
397
|
+
// ALWAYS prefer raw_line for blockers (date-format hazard).
|
|
398
|
+
if (row.raw_line) return row.raw_line;
|
|
399
|
+
return canonicalBlocker({ stage: row.stage || '', date: row.date || '', text: row.body_md || '' });
|
|
400
|
+
});
|
|
401
|
+
raw_bodies.blockers = lines.join('\n');
|
|
402
|
+
} else if ('blockers' in blockGaps) {
|
|
403
|
+
raw_bodies.blockers = '';
|
|
404
|
+
}
|
|
405
|
+
// If no blockGaps entry for blockers, raw_bodies.blockers stays null (block omitted).
|
|
394
406
|
}
|
|
395
407
|
|
|
396
408
|
// --- unstructured blocks: verbatim from _block_meta.raw_body ---
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* setPosition(position, opts) - upsert state_position
|
|
16
16
|
* getPosition(opts) - return current position
|
|
17
17
|
* queryDecisions(ftsQuery, opts) - FTS5 search over decisions (or JS fallback)
|
|
18
|
-
* migrate(migrateOpts) - lazy-require migrate-to-sqlite.cjs
|
|
19
|
-
* render(projectRoot) - lazy-require render-markdown.cjs
|
|
18
|
+
* migrate(migrateOpts) - lazy-require migrate-to-sqlite.cjs
|
|
19
|
+
* render(projectRoot) - lazy-require render-markdown.cjs
|
|
20
20
|
* backendName() - return BACKEND string
|
|
21
21
|
*
|
|
22
22
|
* R7 dual-write: every SQLite MUTATION wraps writeStructured() + renderMarkdown()
|
|
@@ -56,7 +56,7 @@ function findPackageRoot(startDir) {
|
|
|
56
56
|
try { pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); } catch { pkg = null; }
|
|
57
57
|
if (pkg) {
|
|
58
58
|
if (firstWithPkg === null) firstWithPkg = dir;
|
|
59
|
-
if (pkg.name === 'get-design-done') return dir;
|
|
59
|
+
if (pkg.name === '@hegemonart/get-design-done') return dir;
|
|
60
60
|
}
|
|
61
61
|
const parent = path.dirname(dir);
|
|
62
62
|
if (parent === dir) break;
|
|
@@ -103,18 +103,18 @@ async function loadSdk() {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
// ---------------------------------------------------------------------------
|
|
106
|
-
// Lazy-require helpers for
|
|
107
|
-
//
|
|
106
|
+
// Lazy-require helpers for the migrate and render sibling modules (PINNED names).
|
|
107
|
+
// Loaded on first call so a missing better-sqlite3 binding does not crash module load.
|
|
108
108
|
// ---------------------------------------------------------------------------
|
|
109
109
|
|
|
110
|
-
/** @type {any|null} Cached migrate-to-sqlite module
|
|
110
|
+
/** @type {any|null} Cached migrate-to-sqlite module. */
|
|
111
111
|
let _migrateModule = null;
|
|
112
|
-
/** @type {any|null} Cached render-markdown module
|
|
112
|
+
/** @type {any|null} Cached render-markdown module. */
|
|
113
113
|
let _renderModule = null;
|
|
114
114
|
|
|
115
115
|
/**
|
|
116
|
-
* Lazy-require ./migrate-to-sqlite.cjs
|
|
117
|
-
* Returns null if
|
|
116
|
+
* Lazy-require ./migrate-to-sqlite.cjs.
|
|
117
|
+
* Returns null if the require fails (e.g. better-sqlite3 missing).
|
|
118
118
|
* @returns {any|null}
|
|
119
119
|
*/
|
|
120
120
|
function _requireMigrate() {
|
|
@@ -128,8 +128,8 @@ function _requireMigrate() {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
|
-
* Lazy-require ./render-markdown.cjs
|
|
132
|
-
* Returns null if
|
|
131
|
+
* Lazy-require ./render-markdown.cjs.
|
|
132
|
+
* Returns null if the require fails.
|
|
133
133
|
* @returns {any|null}
|
|
134
134
|
*/
|
|
135
135
|
function _requireRender() {
|
|
@@ -198,12 +198,12 @@ function _onDiskSha(statePath) {
|
|
|
198
198
|
* If they differ, the user has hand-edited STATE.md since the last SQLite write.
|
|
199
199
|
* In that case, run a mini-migration (upsert SQLite from markdown) before proceeding.
|
|
200
200
|
*
|
|
201
|
-
* This is
|
|
201
|
+
* This is ASYNC - must be awaited before entering the db.transaction().
|
|
202
202
|
*
|
|
203
203
|
* @param {import('better-sqlite3').Database} db
|
|
204
204
|
* @param {string} statePath absolute path to STATE.md
|
|
205
205
|
*/
|
|
206
|
-
function _applyFreshnessGuard(db, statePath) {
|
|
206
|
+
async function _applyFreshnessGuard(db, statePath) {
|
|
207
207
|
try {
|
|
208
208
|
const onDisk = _onDiskSha(statePath);
|
|
209
209
|
if (onDisk === null) return; // STATE.md doesn't exist yet - skip
|
|
@@ -211,20 +211,21 @@ function _applyFreshnessGuard(db, statePath) {
|
|
|
211
211
|
const stored = metaRow ? metaRow.value : null;
|
|
212
212
|
if (stored === onDisk) return; // No drift - proceed normally
|
|
213
213
|
// Drift detected: user hand-edited STATE.md.
|
|
214
|
-
// Run mini-migration to
|
|
214
|
+
// Run mini-migration (upsertOnly) to fold the hand-edit into SQLite before
|
|
215
|
+
// proceeding with the intended mutation.
|
|
215
216
|
const migrate = _requireMigrate();
|
|
216
217
|
if (migrate && typeof migrate.migrateToSqlite === 'function') {
|
|
217
218
|
try {
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
// The mini-migration is best-effort.
|
|
219
|
+
// Close db first if needed - migrateToSqlite opens its own connection.
|
|
220
|
+
// We pass the resolved projectRoot (dirname of .design/STATE.md's parent).
|
|
221
|
+
const projectRoot = path.resolve(path.dirname(statePath), '..');
|
|
222
|
+
await migrate.migrateToSqlite({ statePath, projectRoot, force: true, upsertOnly: true });
|
|
223
223
|
} catch {
|
|
224
224
|
// Migration failed - log and continue rather than blocking the write.
|
|
225
|
+
// Still update the sha to prevent infinite re-triggering.
|
|
225
226
|
}
|
|
226
227
|
}
|
|
227
|
-
// Update stored sha to
|
|
228
|
+
// Update stored sha to reflect the current on-disk content.
|
|
228
229
|
db.prepare('INSERT OR REPLACE INTO _meta(key, value) VALUES (?, ?)').run('last_render_sha256', onDisk);
|
|
229
230
|
} catch {
|
|
230
231
|
/* Freshness guard must never break a write path */
|
|
@@ -279,7 +280,7 @@ function _dualWrite(db, statePath, cycleId, writeStructured, sdk) {
|
|
|
279
280
|
db.prepare('INSERT OR REPLACE INTO _meta(key, value) VALUES (?, ?)').run('last_render_sha256', newSha);
|
|
280
281
|
}
|
|
281
282
|
// If render module not available or sdk not loaded, still write the structured data.
|
|
282
|
-
// STATE.md will be stale until
|
|
283
|
+
// STATE.md will be stale until the render module loads successfully.
|
|
283
284
|
});
|
|
284
285
|
txn();
|
|
285
286
|
}
|
|
@@ -326,8 +327,12 @@ async function appendDecision(decision, opts = {}) {
|
|
|
326
327
|
const statePath = _statePath(opts.projectRoot);
|
|
327
328
|
const db = openStateDb(dbPath);
|
|
328
329
|
try {
|
|
329
|
-
_applyFreshnessGuard(db, statePath);
|
|
330
|
+
await _applyFreshnessGuard(db, statePath);
|
|
330
331
|
const cycleId = decision.cycleId || _currentCycleId(db);
|
|
332
|
+
// BUG-10: if no cycle is active, skip rather than inserting cycle_id=null (NOT NULL throw).
|
|
333
|
+
if (!cycleId) {
|
|
334
|
+
return { backend: 'sqlite', skipped: true, reason: 'no active cycle_id - call setPosition first' };
|
|
335
|
+
}
|
|
331
336
|
_dualWrite(db, statePath, cycleId, () => {
|
|
332
337
|
db.prepare(`
|
|
333
338
|
INSERT INTO decisions
|
|
@@ -349,6 +354,17 @@ async function appendDecision(decision, opts = {}) {
|
|
|
349
354
|
decision.rawLine || null,
|
|
350
355
|
decision.createdAt || new Date().toISOString(),
|
|
351
356
|
);
|
|
357
|
+
// BUG-05: populate decisions_fts so FTS5 queries return hits.
|
|
358
|
+
// FTS5 virtual tables do not support ON CONFLICT — use DELETE + INSERT pattern.
|
|
359
|
+
// Guard: if FTS5 tables are absent (no-fts5 build), skip without throwing.
|
|
360
|
+
try {
|
|
361
|
+
db.prepare(`DELETE FROM decisions_fts WHERE id = ?`).run(decision.id);
|
|
362
|
+
db.prepare(`INSERT INTO decisions_fts (id, body_md, tags) VALUES (?, ?, ?)`).run(
|
|
363
|
+
decision.id,
|
|
364
|
+
decision.bodyMd || '',
|
|
365
|
+
decision.tags ? JSON.stringify(decision.tags) : null,
|
|
366
|
+
);
|
|
367
|
+
} catch { /* FTS5 table absent in no-fts5 build - skip */ }
|
|
352
368
|
}, sdk);
|
|
353
369
|
return { backend: 'sqlite', id: decision.id };
|
|
354
370
|
} finally {
|
|
@@ -373,16 +389,21 @@ function getDecisions(opts = {}) {
|
|
|
373
389
|
if (BACKEND !== 'sqlite') {
|
|
374
390
|
return [];
|
|
375
391
|
}
|
|
376
|
-
|
|
377
|
-
const db = openStateDb(dbPath, { readonly: true });
|
|
392
|
+
// BUG-06: wrap in try/catch — openStateDb(readonly) throws on a missing file.
|
|
378
393
|
try {
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
394
|
+
const dbPath = _resolveDbPath(opts);
|
|
395
|
+
const db = openStateDb(dbPath, { readonly: true });
|
|
396
|
+
try {
|
|
397
|
+
const cycleId = opts.cycleId || _currentCycleId(db);
|
|
398
|
+
if (!cycleId) return [];
|
|
399
|
+
return db.prepare(
|
|
400
|
+
'SELECT * FROM decisions WHERE cycle_id = ? ORDER BY ordinal ASC'
|
|
401
|
+
).all(cycleId);
|
|
402
|
+
} finally {
|
|
403
|
+
db.close();
|
|
404
|
+
}
|
|
405
|
+
} catch {
|
|
406
|
+
return [];
|
|
386
407
|
}
|
|
387
408
|
}
|
|
388
409
|
|
|
@@ -408,7 +429,7 @@ async function appendBlocker(blocker, opts = {}) {
|
|
|
408
429
|
const statePath = _statePath(opts.projectRoot);
|
|
409
430
|
const db = openStateDb(dbPath);
|
|
410
431
|
try {
|
|
411
|
-
_applyFreshnessGuard(db, statePath);
|
|
432
|
+
await _applyFreshnessGuard(db, statePath);
|
|
412
433
|
const cycleId = blocker.cycleId || _currentCycleId(db);
|
|
413
434
|
let rowid = null;
|
|
414
435
|
_dualWrite(db, statePath, cycleId, () => {
|
|
@@ -447,21 +468,26 @@ function getBlockers(opts = {}) {
|
|
|
447
468
|
if (BACKEND !== 'sqlite') {
|
|
448
469
|
return [];
|
|
449
470
|
}
|
|
450
|
-
|
|
451
|
-
const db = openStateDb(dbPath, { readonly: true });
|
|
471
|
+
// BUG-06: wrap in try/catch — openStateDb(readonly) throws on a missing file.
|
|
452
472
|
try {
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
473
|
+
const dbPath = _resolveDbPath(opts);
|
|
474
|
+
const db = openStateDb(dbPath, { readonly: true });
|
|
475
|
+
try {
|
|
476
|
+
const cycleId = opts.cycleId || _currentCycleId(db);
|
|
477
|
+
if (!cycleId) return [];
|
|
478
|
+
if (opts.includeResolved) {
|
|
479
|
+
return db.prepare(
|
|
480
|
+
'SELECT * FROM blockers WHERE cycle_id = ? ORDER BY ordinal ASC'
|
|
481
|
+
).all(cycleId);
|
|
482
|
+
}
|
|
456
483
|
return db.prepare(
|
|
457
|
-
'SELECT * FROM blockers WHERE cycle_id = ? ORDER BY ordinal ASC'
|
|
484
|
+
'SELECT * FROM blockers WHERE cycle_id = ? AND resolved_at IS NULL ORDER BY ordinal ASC'
|
|
458
485
|
).all(cycleId);
|
|
486
|
+
} finally {
|
|
487
|
+
db.close();
|
|
459
488
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
).all(cycleId);
|
|
463
|
-
} finally {
|
|
464
|
-
db.close();
|
|
489
|
+
} catch {
|
|
490
|
+
return [];
|
|
465
491
|
}
|
|
466
492
|
}
|
|
467
493
|
|
|
@@ -488,7 +514,7 @@ async function setPosition(position, opts = {}) {
|
|
|
488
514
|
const statePath = _statePath(opts.projectRoot);
|
|
489
515
|
const db = openStateDb(dbPath);
|
|
490
516
|
try {
|
|
491
|
-
_applyFreshnessGuard(db, statePath);
|
|
517
|
+
await _applyFreshnessGuard(db, statePath);
|
|
492
518
|
_dualWrite(db, statePath, position.cycleId, () => {
|
|
493
519
|
db.prepare(`
|
|
494
520
|
INSERT INTO state_position
|
|
@@ -535,17 +561,22 @@ function getPosition(opts = {}) {
|
|
|
535
561
|
if (BACKEND !== 'sqlite') {
|
|
536
562
|
return null;
|
|
537
563
|
}
|
|
538
|
-
|
|
539
|
-
const db = openStateDb(dbPath, { readonly: true });
|
|
564
|
+
// BUG-06: wrap in try/catch — openStateDb(readonly) throws on a missing file.
|
|
540
565
|
try {
|
|
541
|
-
|
|
542
|
-
|
|
566
|
+
const dbPath = _resolveDbPath(opts);
|
|
567
|
+
const db = openStateDb(dbPath, { readonly: true });
|
|
568
|
+
try {
|
|
569
|
+
if (opts.cycleId) {
|
|
570
|
+
return db.prepare('SELECT * FROM state_position WHERE cycle_id = ?').get(opts.cycleId) || null;
|
|
571
|
+
}
|
|
572
|
+
return db.prepare(
|
|
573
|
+
'SELECT * FROM state_position ORDER BY updated_at DESC LIMIT 1'
|
|
574
|
+
).get() || null;
|
|
575
|
+
} finally {
|
|
576
|
+
db.close();
|
|
543
577
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
).get() || null;
|
|
547
|
-
} finally {
|
|
548
|
-
db.close();
|
|
578
|
+
} catch {
|
|
579
|
+
return null;
|
|
549
580
|
}
|
|
550
581
|
}
|
|
551
582
|
|
|
@@ -623,19 +654,60 @@ function _currentCycleId(db) {
|
|
|
623
654
|
}
|
|
624
655
|
|
|
625
656
|
// ---------------------------------------------------------------------------
|
|
626
|
-
// migrate - lazy delegate to migrate-to-sqlite.cjs
|
|
657
|
+
// migrate - async lazy delegate to migrate-to-sqlite.cjs.
|
|
627
658
|
// ---------------------------------------------------------------------------
|
|
628
659
|
|
|
629
660
|
/**
|
|
630
|
-
* Run migration from markdown STATE.md to SQLite.
|
|
631
|
-
*
|
|
632
|
-
*
|
|
661
|
+
* Run migration from markdown `.design/STATE.md` to the SQLite state database.
|
|
662
|
+
*
|
|
663
|
+
* This function is the public store-level entry point for the
|
|
664
|
+
* `--migrate-state` flow. It lazy-loads `./migrate-to-sqlite.cjs` on first call
|
|
665
|
+
* and delegates to whichever export it exposes (`migrateToSqlite`,
|
|
666
|
+
* `migrate`, or `migrateState`), in priority order. The underlying
|
|
667
|
+
* `migrateToSqlite` is itself async (it dynamically imports
|
|
668
|
+
* `sdk/state/parser.ts` and uses node:fs/promises for IO), so this wrapper
|
|
669
|
+
* is async and always returns a Promise.
|
|
670
|
+
*
|
|
671
|
+
* The function NEVER throws on infrastructure failures:
|
|
672
|
+
* - `BACKEND === 'markdown'` (no better-sqlite3) → resolves with
|
|
673
|
+
* `{ migrated: false, backend: 'markdown', message: ... }`.
|
|
674
|
+
* - `require('./migrate-to-sqlite.cjs')` failed → resolves with
|
|
675
|
+
* `{ migrated: false, backend: 'sqlite', message: ... }`.
|
|
676
|
+
* - The delegate module loaded but exposes no recognized export → resolves
|
|
677
|
+
* with `{ migrated: false, backend: 'sqlite', message: ... }`.
|
|
678
|
+
*
|
|
679
|
+
* Errors thrown by the delegate (parser failure, schema mismatch, etc.) are
|
|
680
|
+
* propagated as a rejected Promise - callers should `await` and handle.
|
|
681
|
+
*
|
|
682
|
+
* Idempotent: calling `migrate()` repeatedly on a clean database is safe
|
|
683
|
+
* (the underlying migration uses `INSERT ... ON CONFLICT ... DO UPDATE`).
|
|
684
|
+
* Migration is opt-in: the delegate refuses to write unless `force:true` or
|
|
685
|
+
* the CLI `--migrate-state` flag is set (a notice is returned instead).
|
|
686
|
+
*
|
|
687
|
+
* Dual-channel result shapes:
|
|
688
|
+
* - markdown floor: { migrated: false, backend: 'markdown', message }
|
|
689
|
+
* - sqlite path: { migrated: boolean, tables: {...counts}, dryRun,
|
|
690
|
+
* skipped, reason }
|
|
691
|
+
* The caller MUST inspect `migrated` (the boolean) — never `backend` alone —
|
|
692
|
+
* to decide whether the operation actually performed writes.
|
|
633
693
|
*
|
|
634
|
-
* @
|
|
635
|
-
*
|
|
636
|
-
* @
|
|
694
|
+
* @async
|
|
695
|
+
* @param {object} [migrateOpts] options forwarded to the delegate
|
|
696
|
+
* @param {string} [migrateOpts.statePath] explicit path to STATE.md
|
|
697
|
+
* @param {string} [migrateOpts.dbPath] explicit path to state.sqlite
|
|
698
|
+
* @param {string} [migrateOpts.projectRoot] project root for path lookup
|
|
699
|
+
* @param {boolean} [migrateOpts.dryRun] wrap writes in BEGIN/ROLLBACK
|
|
700
|
+
* @param {boolean} [migrateOpts.force] same as `--migrate-state` flag
|
|
701
|
+
* @param {boolean} [migrateOpts.upsertOnly] re-parse + UPSERT without wiping
|
|
702
|
+
* unrelated rows (used by the R8
|
|
703
|
+
* freshness guard)
|
|
704
|
+
* @returns {Promise<{ migrated: boolean, backend?: string, message?: string,
|
|
705
|
+
* tables?: object, dryRun?: boolean, skipped?: boolean, reason?: string }>}
|
|
706
|
+
* Resolves with the migration result; rejects only on delegate exceptions.
|
|
707
|
+
* @see migrate-to-sqlite.cjs for the underlying transactional implementation
|
|
708
|
+
* @see render for the reverse direction (SQLite → STATE.md)
|
|
637
709
|
*/
|
|
638
|
-
function migrate(migrateOpts = {}) {
|
|
710
|
+
async function migrate(migrateOpts = {}) {
|
|
639
711
|
if (BACKEND !== 'sqlite') {
|
|
640
712
|
return {
|
|
641
713
|
migrated: false,
|
|
@@ -648,17 +720,18 @@ function migrate(migrateOpts = {}) {
|
|
|
648
720
|
return {
|
|
649
721
|
migrated: false,
|
|
650
722
|
backend: 'sqlite',
|
|
651
|
-
message: 'migrate-to-sqlite.cjs not
|
|
723
|
+
message: 'migrate-to-sqlite.cjs could not be loaded (require failed)',
|
|
652
724
|
};
|
|
653
725
|
}
|
|
726
|
+
// Delegate is async; await so callers see the resolved result, not a Promise.
|
|
654
727
|
if (typeof mod.migrateToSqlite === 'function') {
|
|
655
|
-
return mod.migrateToSqlite(migrateOpts);
|
|
728
|
+
return await mod.migrateToSqlite(migrateOpts);
|
|
656
729
|
}
|
|
657
730
|
if (typeof mod.migrate === 'function') {
|
|
658
|
-
return mod.migrate(migrateOpts);
|
|
731
|
+
return await mod.migrate(migrateOpts);
|
|
659
732
|
}
|
|
660
733
|
if (typeof mod.migrateState === 'function') {
|
|
661
|
-
return mod.migrateState(migrateOpts);
|
|
734
|
+
return await mod.migrateState(migrateOpts);
|
|
662
735
|
}
|
|
663
736
|
return {
|
|
664
737
|
migrated: false,
|
|
@@ -668,13 +741,13 @@ function migrate(migrateOpts = {}) {
|
|
|
668
741
|
}
|
|
669
742
|
|
|
670
743
|
// ---------------------------------------------------------------------------
|
|
671
|
-
// render - lazy delegate to render-markdown.cjs
|
|
744
|
+
// render - lazy delegate to render-markdown.cjs.
|
|
672
745
|
// ---------------------------------------------------------------------------
|
|
673
746
|
|
|
674
747
|
/**
|
|
675
748
|
* Re-render STATE.md from SQLite state (reverse of migration).
|
|
676
|
-
* Delegates to ./render-markdown.cjs
|
|
677
|
-
* If that module
|
|
749
|
+
* Delegates to ./render-markdown.cjs.
|
|
750
|
+
* If that module cannot be loaded, returns null silently.
|
|
678
751
|
*
|
|
679
752
|
* @param {string} [projectRoot]
|
|
680
753
|
* @returns {Promise<string|null>} rendered markdown string, or null if unavailable
|
|
@@ -110,6 +110,18 @@ const VALID_STAGE_NAMES = new Set<string>([
|
|
|
110
110
|
'discuss',
|
|
111
111
|
]);
|
|
112
112
|
|
|
113
|
+
// Names of top-level `gdd-sdk` subcommands that users sometimes try to invoke
|
|
114
|
+
// as stages (e.g. `gdd-sdk stage audit`). When a stage name collides with one,
|
|
115
|
+
// surface a "did you mean" hint pointing at the real invocation.
|
|
116
|
+
const TOP_LEVEL_COMMANDS_OFTEN_CONFUSED_FOR_STAGES = new Set<string>([
|
|
117
|
+
'audit',
|
|
118
|
+
'build',
|
|
119
|
+
'dashboard',
|
|
120
|
+
'init',
|
|
121
|
+
'query',
|
|
122
|
+
'run',
|
|
123
|
+
]);
|
|
124
|
+
|
|
113
125
|
export async function stageCommand(
|
|
114
126
|
args: ParsedArgs,
|
|
115
127
|
deps: StageCommandDeps = {},
|
|
@@ -134,6 +146,11 @@ export async function stageCommand(
|
|
|
134
146
|
stderr.write(
|
|
135
147
|
`gdd-sdk stage: "${stageName}" is not one of brief|explore|plan|design|verify|discuss\n`,
|
|
136
148
|
);
|
|
149
|
+
if (TOP_LEVEL_COMMANDS_OFTEN_CONFUSED_FOR_STAGES.has(stageName)) {
|
|
150
|
+
stderr.write(
|
|
151
|
+
`gdd-sdk stage: did you mean \`gdd-sdk ${stageName}\`? "${stageName}" is a top-level subcommand, not a pipeline stage.\n`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
137
154
|
return 3;
|
|
138
155
|
}
|
|
139
156
|
|