@hegemonart/get-design-done 1.56.0 → 1.57.1
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 +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +73 -0
- package/README.md +2 -0
- package/SKILL.md +1 -0
- package/dist/claude-code/.claude/skills/state/SKILL.md +106 -0
- package/hooks/budget-enforcer.ts +5 -4
- package/hooks/gdd-fact-force.js +92 -3
- package/hooks/gdd-protected-paths.js +25 -2
- package/hooks/gdd-read-injection-scanner.ts +9 -1
- package/hooks/gdd-risk-gate.js +17 -7
- package/package.json +1 -1
- package/reference/skill-graph.md +2 -1
- package/scripts/lib/design-search.cjs +20 -2
- package/scripts/lib/manifest/skills.json +8 -0
- package/scripts/lib/state/migrate-to-sqlite.cjs +680 -0
- package/scripts/lib/state/query-surface.cjs +403 -0
- package/scripts/lib/state/render-markdown.cjs +729 -0
- package/scripts/lib/state/state-backend.cjs +345 -0
- package/scripts/lib/state/state-store.cjs +766 -0
- package/sdk/cli/index.js +199 -96
- package/sdk/dashboard/data/_pkg-root.cjs +4 -4
- package/sdk/dashboard/data/risk-surface.cjs +54 -19
- package/sdk/dashboard/data/source.cjs +44 -5
- package/sdk/dashboard/tui/index.cjs +28 -2
- package/sdk/mcp/gdd-state/server.js +133 -30
- package/sdk/mcp/gdd-state/tools/get.ts +8 -0
- package/sdk/state/index.ts +277 -13
- package/sdk/state/lockfile.ts +48 -0
- package/sdk/state/schema.sql +218 -0
- package/skills/state/SKILL.md +106 -0
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* resolve a cross-tree sibling via a fixed `__dirname`-relative `../../..`
|
|
7
7
|
* jump, because that breaks the moment a file is copied/moved or the layout
|
|
8
8
|
* shifts. Instead, walk UP from this file's directory until we find the GDD
|
|
9
|
-
* package.json (identified by `name === 'get-design-done'`), and
|
|
10
|
-
* in-repo siblings relative to that root.
|
|
9
|
+
* package.json (identified by `name === '@hegemonart/get-design-done'`), and
|
|
10
|
+
* resolve all in-repo siblings relative to that root.
|
|
11
11
|
*
|
|
12
12
|
* Even though these dashboard `.cjs` files are NOT esbuild-bundled (R8 — the
|
|
13
13
|
* bin trampoline runs them directly so the Phase 53 __dirname-rewrite trap
|
|
@@ -23,7 +23,7 @@ let _cachedRoot = null;
|
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Walk up from `startDir` looking for the GDD package root. The GDD root is
|
|
26
|
-
* the first ancestor whose package.json declares `name: "get-design-done"`;
|
|
26
|
+
* the first ancestor whose package.json declares `name: "@hegemonart/get-design-done"`;
|
|
27
27
|
* if no such marker is found (e.g. running from an unusual layout), fall back
|
|
28
28
|
* to the FIRST ancestor that has any package.json, then to `startDir`.
|
|
29
29
|
*
|
|
@@ -44,7 +44,7 @@ function findPackageRoot(startDir) {
|
|
|
44
44
|
}
|
|
45
45
|
if (pkg) {
|
|
46
46
|
if (firstWithPkg === null) firstWithPkg = dir;
|
|
47
|
-
if (pkg.name === 'get-design-done') return dir;
|
|
47
|
+
if (pkg.name === '@hegemonart/get-design-done') return dir;
|
|
48
48
|
}
|
|
49
49
|
const parent = path.dirname(dir);
|
|
50
50
|
if (parent === dir) break;
|
|
@@ -33,15 +33,20 @@
|
|
|
33
33
|
* (absent / unknown) -> default
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Canonical action -> color map. Keys are lowercase (matching the emitter's
|
|
38
|
+
* suggested_action values from events.schema.json: allow/review/
|
|
39
|
+
* require_confirmation/block). The display label is separate from the key so
|
|
40
|
+
* the map can do a single case-insensitive lookup.
|
|
41
|
+
*/
|
|
37
42
|
const ACTION_COLOR = Object.freeze({
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
allow: 'green',
|
|
44
|
+
review: 'yellow',
|
|
45
|
+
require_confirmation: 'orange',
|
|
46
|
+
block: 'red',
|
|
42
47
|
});
|
|
43
48
|
|
|
44
|
-
/** The set of recognized suggested-action values (Phase 56 vocabulary). */
|
|
49
|
+
/** The set of recognized suggested-action values (Phase 56 vocabulary, lowercase). */
|
|
45
50
|
const VALID_ACTIONS = Object.freeze(Object.keys(ACTION_COLOR));
|
|
46
51
|
|
|
47
52
|
/** The blank placeholder row emitted pre-56 (or for malformed/absent input). */
|
|
@@ -66,34 +71,64 @@ function finiteOrNull(v) {
|
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
/**
|
|
69
|
-
*
|
|
74
|
+
* Canonicalize a suggested_action to its lowercase snake_case key for lookup.
|
|
75
|
+
* Handles the emitter's lowercase values (allow/review/require_confirmation/block)
|
|
76
|
+
* AND legacy CamelCase (Allow/Review/RequireConfirmation/Block) - the CamelCase
|
|
77
|
+
* 'RequireConfirmation' lowercases to 'requireconfirmation' with no separator, so
|
|
78
|
+
* map that explicitly to 'require_confirmation'. Returns null for non-strings.
|
|
79
|
+
* @param {*} action
|
|
80
|
+
* @returns {string|null}
|
|
81
|
+
*/
|
|
82
|
+
function canonAction(action) {
|
|
83
|
+
if (typeof action !== 'string') return null;
|
|
84
|
+
let s = action.trim().toLowerCase().replace(/[\s-]+/g, '_');
|
|
85
|
+
if (s === 'requireconfirmation') s = 'require_confirmation';
|
|
86
|
+
return s;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Map a suggested_action to its display color. Case-insensitive so both
|
|
91
|
+
* 'allow' (emitter) and 'Allow' (legacy) resolve correctly. Unknown / absent -> 'default'.
|
|
70
92
|
* @param {*} action
|
|
71
93
|
* @returns {'green'|'yellow'|'orange'|'red'|'default'}
|
|
72
94
|
*/
|
|
73
95
|
function colorForAction(action) {
|
|
74
|
-
|
|
75
|
-
|
|
96
|
+
const canon = canonAction(action);
|
|
97
|
+
if (canon !== null && Object.prototype.hasOwnProperty.call(ACTION_COLOR, canon)) {
|
|
98
|
+
return ACTION_COLOR[canon];
|
|
76
99
|
}
|
|
77
100
|
return 'default';
|
|
78
101
|
}
|
|
79
102
|
|
|
80
103
|
/**
|
|
81
|
-
* Surface the risk fields on ONE event/finding.
|
|
82
|
-
*
|
|
83
|
-
*
|
|
104
|
+
* Surface the risk fields on ONE event/finding. Accepts either a raw
|
|
105
|
+
* risk_assessment envelope (with a `.payload` sub-object) OR a bare payload
|
|
106
|
+
* object. When `.payload` is present it is used as the source of risk fields;
|
|
107
|
+
* otherwise `item` itself is inspected (bare-payload / legacy path).
|
|
84
108
|
*
|
|
85
|
-
*
|
|
109
|
+
* Reads `risk_score`, `confidence`, `suggested_action` WHEN PRESENT; otherwise
|
|
110
|
+
* returns the blank placeholder. PURE; NEVER throws.
|
|
111
|
+
*
|
|
112
|
+
* @param {*} item an event envelope or bare payload (may be missing risk fields pre-56)
|
|
86
113
|
* @returns {{risk_score:number|null, confidence:number|null,
|
|
87
114
|
* suggested_action:string|null, color:string}}
|
|
88
115
|
*/
|
|
89
116
|
function surfaceRiskOne(item) {
|
|
90
117
|
if (!item || typeof item !== 'object') return blankRow();
|
|
91
118
|
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
119
|
+
// Normalize: if the caller passed a full event envelope (with .payload), use the payload.
|
|
120
|
+
const src = (item.payload && typeof item.payload === 'object') ? item.payload : item;
|
|
121
|
+
|
|
122
|
+
const risk_score = finiteOrNull(src.risk_score);
|
|
123
|
+
const confidence = finiteOrNull(src.confidence);
|
|
124
|
+
const rawAction = src.suggested_action;
|
|
125
|
+
const canon = canonAction(rawAction);
|
|
126
|
+
const recognized =
|
|
127
|
+
canon !== null && Object.prototype.hasOwnProperty.call(ACTION_COLOR, canon);
|
|
128
|
+
// Preserve the action VERBATIM when recognized: the emitter sends lowercase
|
|
129
|
+
// (allow/review/require_confirmation/block), legacy callers may send CamelCase,
|
|
130
|
+
// and either is echoed back unchanged. Unrecognized / absent -> null.
|
|
131
|
+
const suggested_action = recognized ? rawAction : null;
|
|
97
132
|
|
|
98
133
|
// Pre-56: when NONE of the risk fields are present, emit the blank placeholder
|
|
99
134
|
// verbatim (color 'default') so the column reads as "not yet scored".
|
|
@@ -105,7 +140,7 @@ function surfaceRiskOne(item) {
|
|
|
105
140
|
risk_score,
|
|
106
141
|
confidence,
|
|
107
142
|
suggested_action,
|
|
108
|
-
color:
|
|
143
|
+
color: recognized ? ACTION_COLOR[canon] : 'default',
|
|
109
144
|
};
|
|
110
145
|
}
|
|
111
146
|
|
|
@@ -58,6 +58,10 @@ function tryRequire(relPath) {
|
|
|
58
58
|
const designContextQuery = tryRequire('scripts/lib/design-context-query.cjs');
|
|
59
59
|
const eventChain = tryRequire('scripts/lib/event-chain.cjs');
|
|
60
60
|
const healthMirror = tryRequire('scripts/lib/health-mirror/index.cjs');
|
|
61
|
+
// Phase 57 (Round 3-E): state-store provides backendName() so the dashboard
|
|
62
|
+
// model can surface whether it read from SQLite or markdown. Soft-loaded so
|
|
63
|
+
// a missing module degrades without crashing. Never throws.
|
|
64
|
+
const stateStore = tryRequire('scripts/lib/state/state-store.cjs');
|
|
61
65
|
|
|
62
66
|
// ---------------------------------------------------------------------------
|
|
63
67
|
// .ts shared libs — dynamic import(pathToFileURL), memoized.
|
|
@@ -211,23 +215,54 @@ function scrapeEventsFile(eventsPath) {
|
|
|
211
215
|
|
|
212
216
|
/**
|
|
213
217
|
* Load STATE.md-derived fields: status, phase(stage), cycle, decisions[],
|
|
214
|
-
* blockers[]. Tries the typed sdk/state read() first
|
|
218
|
+
* blockers[], backend. Tries the typed sdk/state read() first (which is
|
|
219
|
+
* already migration-active-aware for Phase 57: when BACKEND==='sqlite' and
|
|
220
|
+
* a sibling state.sqlite exists, the dual-write path ensures STATE.md is
|
|
221
|
+
* byte-equal with SQLite so read() returns the canonical view), then the
|
|
222
|
+
* file-scrape fallback. Never throws.
|
|
223
|
+
*
|
|
224
|
+
* The `backend` field reflects the active state-store backend:
|
|
225
|
+
* 'sqlite' — better-sqlite3 + FTS5 available and migration active
|
|
226
|
+
* 'markdown' — markdown floor (the universal default and CI surface)
|
|
215
227
|
*
|
|
216
228
|
* @param {string} root
|
|
217
229
|
* @param {string[]} degraded
|
|
218
230
|
* @returns {Promise<{status:string|null, phase:string|null, cycle:string|null,
|
|
219
|
-
* decisions:Array, blockers:Array}>}
|
|
231
|
+
* decisions:Array, blockers:Array, backend:'sqlite'|'markdown'}>}
|
|
220
232
|
*/
|
|
221
233
|
async function loadState(root, degraded) {
|
|
222
234
|
const statePath = path.join(root, '.design', 'STATE.md');
|
|
223
|
-
const empty = {
|
|
235
|
+
const empty = {
|
|
236
|
+
status: null, phase: null, cycle: null,
|
|
237
|
+
decisions: [], blockers: [],
|
|
238
|
+
backend: /** @type {'sqlite'|'markdown'} */ ('markdown'),
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// Determine the active backend for this specific state path (Phase 57 R3-E).
|
|
242
|
+
// The migration-active gate is:
|
|
243
|
+
// BACKEND==='sqlite' AND existsSync(<statePath-sibling>/state.sqlite)
|
|
244
|
+
//
|
|
245
|
+
// We use state-store.backendName() to check the global probe result, then
|
|
246
|
+
// confirm by checking whether a sibling state.sqlite exists next to STATE.md.
|
|
247
|
+
// This mirrors the migrationActive() logic in sdk/state/index.ts exactly.
|
|
248
|
+
const globalBackend =
|
|
249
|
+
(stateStore && typeof stateStore.backendName === 'function')
|
|
250
|
+
? /** @type {'sqlite'|'markdown'} */ (stateStore.backendName())
|
|
251
|
+
: 'markdown';
|
|
252
|
+
const sqliteSibling = path.join(root, '.design', 'state.sqlite');
|
|
253
|
+
const activeBackend = /** @type {'sqlite'|'markdown'} */ (
|
|
254
|
+
globalBackend === 'sqlite' && fs.existsSync(sqliteSibling) ? 'sqlite' : 'markdown'
|
|
255
|
+
);
|
|
224
256
|
|
|
225
257
|
if (!fs.existsSync(statePath)) {
|
|
226
258
|
degraded.push('state: .design/STATE.md not found');
|
|
227
|
-
return empty;
|
|
259
|
+
return { ...empty, backend: activeBackend };
|
|
228
260
|
}
|
|
229
261
|
|
|
230
262
|
// 1) Typed lib read() — the in-process shared surface (R1).
|
|
263
|
+
// Phase 57: sdk/state read() is already migration-active-aware; when
|
|
264
|
+
// BACKEND==='sqlite' and state.sqlite sibling exists, STATE.md is kept
|
|
265
|
+
// byte-equal by the dual-write path, so no separate SQLite read needed.
|
|
231
266
|
try {
|
|
232
267
|
const stateMod = await importState();
|
|
233
268
|
if (stateMod && typeof stateMod.read === 'function') {
|
|
@@ -239,6 +274,7 @@ async function loadState(root, degraded) {
|
|
|
239
274
|
cycle: (parsed.frontmatter && parsed.frontmatter.cycle) || null,
|
|
240
275
|
decisions: Array.isArray(parsed.decisions) ? parsed.decisions : [],
|
|
241
276
|
blockers: Array.isArray(parsed.blockers) ? parsed.blockers : [],
|
|
277
|
+
backend: activeBackend,
|
|
242
278
|
};
|
|
243
279
|
}
|
|
244
280
|
degraded.push('state: sdk/state import unavailable — using file scrape');
|
|
@@ -246,7 +282,7 @@ async function loadState(root, degraded) {
|
|
|
246
282
|
degraded.push(`state: typed read failed (${errMsg(err)}) — using file scrape`);
|
|
247
283
|
}
|
|
248
284
|
|
|
249
|
-
// 2) File-scrape fallback.
|
|
285
|
+
// 2) File-scrape fallback (ultimate fallback; never throws).
|
|
250
286
|
const scraped = scrapeStateFile(statePath);
|
|
251
287
|
if (scraped) {
|
|
252
288
|
return {
|
|
@@ -255,6 +291,7 @@ async function loadState(root, degraded) {
|
|
|
255
291
|
cycle: scraped.cycle,
|
|
256
292
|
decisions: scraped.decisions,
|
|
257
293
|
blockers: scraped.blockers,
|
|
294
|
+
backend: 'markdown',
|
|
258
295
|
};
|
|
259
296
|
}
|
|
260
297
|
degraded.push('state: scrape fallback failed');
|
|
@@ -508,6 +545,7 @@ function errMsg(err) {
|
|
|
508
545
|
* sessions: Array,
|
|
509
546
|
* degraded: string[],
|
|
510
547
|
* root: string,
|
|
548
|
+
* backend: 'sqlite'|'markdown',
|
|
511
549
|
* }>}
|
|
512
550
|
*/
|
|
513
551
|
async function loadDashboardModel(opts = {}) {
|
|
@@ -564,6 +602,7 @@ async function loadDashboardModel(opts = {}) {
|
|
|
564
602
|
sessions,
|
|
565
603
|
degraded,
|
|
566
604
|
root,
|
|
605
|
+
backend: stateRes.backend,
|
|
567
606
|
};
|
|
568
607
|
}
|
|
569
608
|
|
|
@@ -37,6 +37,21 @@ const readline = require('node:readline');
|
|
|
37
37
|
|
|
38
38
|
const ansi = require('./ansi.cjs');
|
|
39
39
|
|
|
40
|
+
// Lazily require the risk-surface helper (same dep-free constraint as the data plane).
|
|
41
|
+
let _surfaceRisk = null;
|
|
42
|
+
function getSurfaceRisk() {
|
|
43
|
+
if (_surfaceRisk === null) {
|
|
44
|
+
try {
|
|
45
|
+
({ surfaceRisk: _surfaceRisk } = require('../data/risk-surface.cjs'));
|
|
46
|
+
} catch {
|
|
47
|
+
// If the module is unavailable for any reason, fall back to a no-op that returns
|
|
48
|
+
// blank placeholder rows so the column still renders cleanly.
|
|
49
|
+
_surfaceRisk = () => ({ risk_score: null, confidence: null, suggested_action: null, color: 'default' });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return _surfaceRisk;
|
|
53
|
+
}
|
|
54
|
+
|
|
40
55
|
// Lazily require the data plane so `renderFrame` (the pure path) can be imported + unit-tested
|
|
41
56
|
// without paying for the data module's transitive requires. `run` resolves it on demand.
|
|
42
57
|
let _loadDashboardModel = null;
|
|
@@ -351,11 +366,22 @@ function bodyFindings(model, inner, scroll) {
|
|
|
351
366
|
if (tail.length === 0) {
|
|
352
367
|
lines.push(' ' + ansi.color('no events', { dim: true }));
|
|
353
368
|
} else {
|
|
369
|
+
const surfaceRisk = getSurfaceRisk();
|
|
354
370
|
for (const ev of tail) {
|
|
355
371
|
const name = (ev && (ev.event || ev.type || ev.kind)) || 'event';
|
|
356
|
-
//
|
|
372
|
+
// Phase-56+: surface risk/confidence from risk_assessment events.
|
|
373
|
+
// For pre-56 events that lack risk fields, surfaceRiskOne returns the blank placeholder.
|
|
374
|
+
const surfaced = (ev && ev.type === 'risk_assessment')
|
|
375
|
+
? surfaceRisk(ev)
|
|
376
|
+
: { risk_score: null, confidence: null, suggested_action: null, color: 'default' };
|
|
377
|
+
const riskText = surfaced.risk_score !== null
|
|
378
|
+
? ansi.color(surfaced.risk_score.toFixed(2), { fg: surfaced.color !== 'default' ? surfaced.color : undefined })
|
|
379
|
+
: ansi.color('·', { dim: true });
|
|
380
|
+
const confText = surfaced.confidence !== null
|
|
381
|
+
? String(surfaced.confidence.toFixed(2))
|
|
382
|
+
: ansi.color('·', { dim: true });
|
|
357
383
|
lines.push(ansi.columns(
|
|
358
|
-
[String(name),
|
|
384
|
+
[String(name), riskText, confText],
|
|
359
385
|
[Math.max(8, inner - 14), 5, 5],
|
|
360
386
|
));
|
|
361
387
|
}
|
|
@@ -36,7 +36,7 @@ __export(server_exports, {
|
|
|
36
36
|
});
|
|
37
37
|
module.exports = __toCommonJS(server_exports);
|
|
38
38
|
var import_node_fs5 = require("node:fs");
|
|
39
|
-
var
|
|
39
|
+
var import_node_path4 = require("node:path");
|
|
40
40
|
var import_server = require("@modelcontextprotocol/sdk/server/index.js");
|
|
41
41
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
42
42
|
var import_types8 = require("@modelcontextprotocol/sdk/types.js");
|
|
@@ -196,6 +196,7 @@ __export(get_exports, {
|
|
|
196
196
|
|
|
197
197
|
// sdk/state/index.ts
|
|
198
198
|
var import_node_fs2 = require("node:fs");
|
|
199
|
+
var import_node_path = require("node:path");
|
|
199
200
|
|
|
200
201
|
// sdk/state/lockfile.ts
|
|
201
202
|
var import_node_fs = require("node:fs");
|
|
@@ -349,7 +350,10 @@ function getErrnoCode(err) {
|
|
|
349
350
|
return void 0;
|
|
350
351
|
}
|
|
351
352
|
function sleep(ms) {
|
|
352
|
-
return new Promise((
|
|
353
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
354
|
+
}
|
|
355
|
+
async function acquireSqliteLock(sqlitePath, opts = {}) {
|
|
356
|
+
return acquire(sqlitePath, opts);
|
|
353
357
|
}
|
|
354
358
|
|
|
355
359
|
// sdk/state/parser.ts
|
|
@@ -1727,11 +1731,70 @@ function gateFor(from, to) {
|
|
|
1727
1731
|
}
|
|
1728
1732
|
|
|
1729
1733
|
// sdk/state/index.ts
|
|
1734
|
+
function _findPackageRoot(startDir) {
|
|
1735
|
+
let dir = (0, import_node_path.resolve)(startDir);
|
|
1736
|
+
let firstWithPkg = null;
|
|
1737
|
+
for (let i = 0; i < 12; i++) {
|
|
1738
|
+
const pkgPath = (0, import_node_path.join)(dir, "package.json");
|
|
1739
|
+
if ((0, import_node_fs2.existsSync)(pkgPath)) {
|
|
1740
|
+
try {
|
|
1741
|
+
const pkg = require(pkgPath);
|
|
1742
|
+
if (firstWithPkg === null) firstWithPkg = dir;
|
|
1743
|
+
if (pkg.name === "@hegemonart/get-design-done") return dir;
|
|
1744
|
+
} catch {
|
|
1745
|
+
if (firstWithPkg === null) firstWithPkg = dir;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
const parent = (0, import_node_path.dirname)(dir);
|
|
1749
|
+
if (parent === dir) break;
|
|
1750
|
+
dir = parent;
|
|
1751
|
+
}
|
|
1752
|
+
return firstWithPkg;
|
|
1753
|
+
}
|
|
1754
|
+
var _backendCache = null;
|
|
1755
|
+
function _loadBackend() {
|
|
1756
|
+
if (_backendCache !== null) return _backendCache === false ? null : _backendCache;
|
|
1757
|
+
try {
|
|
1758
|
+
const pkgRoot = _findPackageRoot(__dirname);
|
|
1759
|
+
if (pkgRoot === null) {
|
|
1760
|
+
_backendCache = false;
|
|
1761
|
+
return null;
|
|
1762
|
+
}
|
|
1763
|
+
const backendPath = (0, import_node_path.join)(pkgRoot, "scripts", "lib", "state", "state-backend.cjs");
|
|
1764
|
+
if (!(0, import_node_fs2.existsSync)(backendPath)) {
|
|
1765
|
+
_backendCache = false;
|
|
1766
|
+
return null;
|
|
1767
|
+
}
|
|
1768
|
+
_backendCache = require(backendPath);
|
|
1769
|
+
return _backendCache;
|
|
1770
|
+
} catch {
|
|
1771
|
+
_backendCache = false;
|
|
1772
|
+
return null;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
function migrationActive(statePath) {
|
|
1776
|
+
const backend = _loadBackend();
|
|
1777
|
+
if (backend === null || backend.BACKEND !== "sqlite") return false;
|
|
1778
|
+
const sqliteSibling = (0, import_node_path.join)((0, import_node_path.dirname)(statePath), "state.sqlite");
|
|
1779
|
+
if (!(0, import_node_fs2.existsSync)(sqliteSibling)) return false;
|
|
1780
|
+
try {
|
|
1781
|
+
if ((0, import_node_fs2.statSync)(sqliteSibling).isDirectory()) return false;
|
|
1782
|
+
} catch {
|
|
1783
|
+
return false;
|
|
1784
|
+
}
|
|
1785
|
+
return true;
|
|
1786
|
+
}
|
|
1730
1787
|
async function read(path2) {
|
|
1731
1788
|
const raw = (0, import_node_fs2.readFileSync)(path2, "utf8");
|
|
1732
1789
|
return parse(raw).state;
|
|
1733
1790
|
}
|
|
1734
1791
|
async function mutate(path2, fn) {
|
|
1792
|
+
if (migrationActive(path2)) {
|
|
1793
|
+
return _mutateSqliteActive(path2, fn);
|
|
1794
|
+
}
|
|
1795
|
+
return _mutateMarkdown(path2, fn);
|
|
1796
|
+
}
|
|
1797
|
+
async function _mutateMarkdown(path2, fn) {
|
|
1735
1798
|
const release = await acquire(path2);
|
|
1736
1799
|
const tmpPath = `${path2}.tmp`;
|
|
1737
1800
|
try {
|
|
@@ -1768,6 +1831,46 @@ async function mutate(path2, fn) {
|
|
|
1768
1831
|
await release();
|
|
1769
1832
|
}
|
|
1770
1833
|
}
|
|
1834
|
+
async function _mutateSqliteActive(path2, fn) {
|
|
1835
|
+
const sqlitePath = (0, import_node_path.join)((0, import_node_path.dirname)(path2), "state.sqlite");
|
|
1836
|
+
const releaseSqliteLock = await acquireSqliteLock(sqlitePath);
|
|
1837
|
+
const release = await acquire(path2);
|
|
1838
|
+
const tmpPath = `${path2}.tmp`;
|
|
1839
|
+
try {
|
|
1840
|
+
const raw = (0, import_node_fs2.readFileSync)(path2, "utf8");
|
|
1841
|
+
const { state, raw_bodies, raw_frontmatter, block_gaps, line_ending } = parse(raw);
|
|
1842
|
+
const clone = structuredClone(state);
|
|
1843
|
+
const next = fn(clone);
|
|
1844
|
+
const out = serialize(next, {
|
|
1845
|
+
raw_frontmatter,
|
|
1846
|
+
raw_bodies,
|
|
1847
|
+
block_gaps,
|
|
1848
|
+
line_ending
|
|
1849
|
+
});
|
|
1850
|
+
(0, import_node_fs2.writeFileSync)(tmpPath, out, "utf8");
|
|
1851
|
+
try {
|
|
1852
|
+
(0, import_node_fs2.renameSync)(tmpPath, path2);
|
|
1853
|
+
} catch (err) {
|
|
1854
|
+
const code = typeof err === "object" && err !== null && "code" in err ? err.code : void 0;
|
|
1855
|
+
if (code === "EPERM" || code === "EBUSY") {
|
|
1856
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1857
|
+
(0, import_node_fs2.renameSync)(tmpPath, path2);
|
|
1858
|
+
} else {
|
|
1859
|
+
throw err;
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
return next;
|
|
1863
|
+
} catch (err) {
|
|
1864
|
+
try {
|
|
1865
|
+
if ((0, import_node_fs2.existsSync)(tmpPath)) (0, import_node_fs2.unlinkSync)(tmpPath);
|
|
1866
|
+
} catch {
|
|
1867
|
+
}
|
|
1868
|
+
throw err;
|
|
1869
|
+
} finally {
|
|
1870
|
+
await release();
|
|
1871
|
+
await releaseSqliteLock();
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1771
1874
|
async function transition(path2, toStage) {
|
|
1772
1875
|
const beforeMutate = await read(path2);
|
|
1773
1876
|
const from = beforeMutate.position.stage;
|
|
@@ -1798,7 +1901,7 @@ async function transition(path2, toStage) {
|
|
|
1798
1901
|
}
|
|
1799
1902
|
|
|
1800
1903
|
// sdk/mcp/gdd-state/tools/shared.ts
|
|
1801
|
-
var
|
|
1904
|
+
var import_node_path3 = __toESM(require("node:path"));
|
|
1802
1905
|
var import_node_fs4 = require("node:fs");
|
|
1803
1906
|
var import_node_module2 = require("node:module");
|
|
1804
1907
|
|
|
@@ -1848,13 +1951,13 @@ var EventBus = class extends import_node_events.EventEmitter {
|
|
|
1848
1951
|
|
|
1849
1952
|
// sdk/event-stream/writer.ts
|
|
1850
1953
|
var import_node_fs3 = require("node:fs");
|
|
1851
|
-
var
|
|
1954
|
+
var import_node_path2 = require("node:path");
|
|
1852
1955
|
var import_node_module = require("node:module");
|
|
1853
1956
|
function _findRepoRoot() {
|
|
1854
1957
|
let dir = process.cwd();
|
|
1855
1958
|
for (let i = 0; i < 8; i++) {
|
|
1856
|
-
if ((0, import_node_fs3.existsSync)((0,
|
|
1857
|
-
const parent = (0,
|
|
1959
|
+
if ((0, import_node_fs3.existsSync)((0, import_node_path2.join)(dir, "package.json"))) return dir;
|
|
1960
|
+
const parent = (0, import_node_path2.dirname)(dir);
|
|
1858
1961
|
if (parent === dir) break;
|
|
1859
1962
|
dir = parent;
|
|
1860
1963
|
}
|
|
@@ -1863,16 +1966,16 @@ function _findRepoRoot() {
|
|
|
1863
1966
|
var _redact;
|
|
1864
1967
|
try {
|
|
1865
1968
|
const _root = _findRepoRoot();
|
|
1866
|
-
const _candidate = (0,
|
|
1969
|
+
const _candidate = (0, import_node_path2.resolve)(_root, "scripts/lib/redact.cjs");
|
|
1867
1970
|
if ((0, import_node_fs3.existsSync)(_candidate)) {
|
|
1868
|
-
const _redactRequire = (0, import_node_module.createRequire)((0,
|
|
1971
|
+
const _redactRequire = (0, import_node_module.createRequire)((0, import_node_path2.join)(_root, "package.json"));
|
|
1869
1972
|
const _mod = _redactRequire(_candidate);
|
|
1870
1973
|
_redact = _mod.redact;
|
|
1871
1974
|
} else {
|
|
1872
|
-
const _altRoot = (0,
|
|
1873
|
-
const _altCandidate = (0,
|
|
1975
|
+
const _altRoot = (0, import_node_path2.resolve)(_root, "..", "..");
|
|
1976
|
+
const _altCandidate = (0, import_node_path2.resolve)(_altRoot, "scripts/lib/redact.cjs");
|
|
1874
1977
|
if ((0, import_node_fs3.existsSync)(_altCandidate)) {
|
|
1875
|
-
const _altRequire = (0, import_node_module.createRequire)((0,
|
|
1978
|
+
const _altRequire = (0, import_node_module.createRequire)((0, import_node_path2.join)(_altRoot, "package.json"));
|
|
1876
1979
|
const _altMod = _altRequire(_altCandidate);
|
|
1877
1980
|
_redact = _altMod.redact;
|
|
1878
1981
|
} else {
|
|
@@ -1898,7 +2001,7 @@ var EventWriter = class {
|
|
|
1898
2001
|
directoryEnsured = false;
|
|
1899
2002
|
constructor(opts = {}) {
|
|
1900
2003
|
const rawPath = opts.path ?? DEFAULT_EVENTS_PATH;
|
|
1901
|
-
this.path = (0,
|
|
2004
|
+
this.path = (0, import_node_path2.isAbsolute)(rawPath) ? rawPath : (0, import_node_path2.resolve)(process.cwd(), rawPath);
|
|
1902
2005
|
this.maxLineBytes = opts.maxLineBytes ?? DEFAULT_MAX_LINE_BYTES;
|
|
1903
2006
|
}
|
|
1904
2007
|
/**
|
|
@@ -1959,7 +2062,7 @@ var EventWriter = class {
|
|
|
1959
2062
|
*/
|
|
1960
2063
|
ensureDirectory() {
|
|
1961
2064
|
if (this.directoryEnsured) return;
|
|
1962
|
-
(0, import_node_fs3.mkdirSync)((0,
|
|
2065
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path2.dirname)(this.path), { recursive: true });
|
|
1963
2066
|
this.directoryEnsured = true;
|
|
1964
2067
|
}
|
|
1965
2068
|
};
|
|
@@ -2017,8 +2120,8 @@ function getSessionId() {
|
|
|
2017
2120
|
function _findRepoRoot2() {
|
|
2018
2121
|
let dir = process.cwd();
|
|
2019
2122
|
for (let i = 0; i < 8; i++) {
|
|
2020
|
-
if ((0, import_node_fs4.existsSync)(
|
|
2021
|
-
const parent =
|
|
2123
|
+
if ((0, import_node_fs4.existsSync)(import_node_path3.default.join(dir, "package.json"))) return dir;
|
|
2124
|
+
const parent = import_node_path3.default.dirname(dir);
|
|
2022
2125
|
if (parent === dir) break;
|
|
2023
2126
|
dir = parent;
|
|
2024
2127
|
}
|
|
@@ -2027,9 +2130,9 @@ function _findRepoRoot2() {
|
|
|
2027
2130
|
var _worktree = (() => {
|
|
2028
2131
|
try {
|
|
2029
2132
|
const root = _findRepoRoot2();
|
|
2030
|
-
const candidate =
|
|
2133
|
+
const candidate = import_node_path3.default.resolve(root, "scripts/lib/worktree-resolve.cjs");
|
|
2031
2134
|
if (!(0, import_node_fs4.existsSync)(candidate)) return null;
|
|
2032
|
-
const req = (0, import_node_module2.createRequire)(
|
|
2135
|
+
const req = (0, import_node_module2.createRequire)(import_node_path3.default.join(root, "package.json"));
|
|
2033
2136
|
return req(candidate);
|
|
2034
2137
|
} catch {
|
|
2035
2138
|
return null;
|
|
@@ -2042,20 +2145,20 @@ function resolveStatePath() {
|
|
|
2042
2145
|
try {
|
|
2043
2146
|
const designRoot = _worktree.resolveDesignRoot();
|
|
2044
2147
|
if (_worktree.isWorktree()) {
|
|
2045
|
-
_worktree.noticeOnce(
|
|
2148
|
+
_worktree.noticeOnce(import_node_path3.default.dirname(designRoot));
|
|
2046
2149
|
}
|
|
2047
|
-
return
|
|
2150
|
+
return import_node_path3.default.join(designRoot, "STATE.md");
|
|
2048
2151
|
} catch {
|
|
2049
2152
|
}
|
|
2050
2153
|
}
|
|
2051
2154
|
return ".design/STATE.md";
|
|
2052
2155
|
}
|
|
2053
|
-
if (
|
|
2054
|
-
return
|
|
2156
|
+
if (import_node_path3.default.isAbsolute(override)) {
|
|
2157
|
+
return import_node_path3.default.resolve(override);
|
|
2055
2158
|
}
|
|
2056
|
-
const root =
|
|
2057
|
-
const resolved =
|
|
2058
|
-
const withSep = root.endsWith(
|
|
2159
|
+
const root = import_node_path3.default.resolve(process.cwd());
|
|
2160
|
+
const resolved = import_node_path3.default.resolve(root, override);
|
|
2161
|
+
const withSep = root.endsWith(import_node_path3.default.sep) ? root : root + import_node_path3.default.sep;
|
|
2059
2162
|
if (resolved !== root && !resolved.startsWith(withSep)) {
|
|
2060
2163
|
throwValidation(
|
|
2061
2164
|
"STATE_PATH_ESCAPE",
|
|
@@ -2763,16 +2866,16 @@ var TOOL_COUNT = TOOL_MODULES.length;
|
|
|
2763
2866
|
var SERVER_NAME = "gdd-state";
|
|
2764
2867
|
var SERVER_VERSION = "1.20.0";
|
|
2765
2868
|
function here() {
|
|
2766
|
-
const expectedRel = (0,
|
|
2869
|
+
const expectedRel = (0, import_node_path4.join)("sdk", "mcp", "gdd-state");
|
|
2767
2870
|
const entry = process.argv[1];
|
|
2768
2871
|
if (typeof entry === "string" && entry.length > 0) {
|
|
2769
|
-
const entryDir = (0,
|
|
2770
|
-
if ((0, import_node_fs5.existsSync)((0,
|
|
2872
|
+
const entryDir = (0, import_node_path4.dirname)((0, import_node_path4.resolve)(entry));
|
|
2873
|
+
if ((0, import_node_fs5.existsSync)((0, import_node_path4.join)(entryDir, "tools", "index.ts"))) {
|
|
2771
2874
|
return entryDir;
|
|
2772
2875
|
}
|
|
2773
2876
|
}
|
|
2774
|
-
const candidate = (0,
|
|
2775
|
-
if ((0, import_node_fs5.existsSync)((0,
|
|
2877
|
+
const candidate = (0, import_node_path4.resolve)(process.cwd(), expectedRel);
|
|
2878
|
+
if ((0, import_node_fs5.existsSync)((0, import_node_path4.join)(candidate, "tools", "index.ts"))) {
|
|
2776
2879
|
return candidate;
|
|
2777
2880
|
}
|
|
2778
2881
|
return candidate;
|
|
@@ -2780,7 +2883,7 @@ function here() {
|
|
|
2780
2883
|
function loadTools() {
|
|
2781
2884
|
const baseDir = here();
|
|
2782
2885
|
return TOOL_MODULES.map((m) => {
|
|
2783
|
-
const absPath = (0,
|
|
2886
|
+
const absPath = (0, import_node_path4.join)(baseDir, "tools", m.schemaPath);
|
|
2784
2887
|
const raw = (0, import_node_fs5.readFileSync)(absPath, "utf8");
|
|
2785
2888
|
const parsed = JSON.parse(raw);
|
|
2786
2889
|
const rawInput = parsed.properties?.input;
|
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
// event. Optionally projects a subset of fields when `input.fields` is
|
|
6
6
|
// provided; unknown field names are silently ignored so callers can pass
|
|
7
7
|
// a broad list without pre-flight knowledge of ParsedState shape.
|
|
8
|
+
//
|
|
9
|
+
// Phase 57 (Round 3-E): this tool already benefits from the SQLite read
|
|
10
|
+
// path transparently. `sdk/state/index.ts` `read()` delegates through the
|
|
11
|
+
// migration-active gate: when BACKEND==='sqlite' and a sibling state.sqlite
|
|
12
|
+
// exists the dual-write path keeps STATE.md byte-equal with SQLite state,
|
|
13
|
+
// so reading the on-disk STATE.md returns the canonical view regardless of
|
|
14
|
+
// which backend is active. No behavioral change is required here and the
|
|
15
|
+
// tool input/output SCHEMA IS UNCHANGED (no mcp-tools-manifest hash drift).
|
|
8
16
|
|
|
9
17
|
import { read } from '../../../state/index.ts';
|
|
10
18
|
import type { ParsedState } from '../../../state/types.ts';
|