@hegemonart/get-design-done 1.30.0 → 1.30.6
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 +103 -0
- package/README.de.md +2 -0
- package/README.fr.md +2 -0
- package/README.it.md +2 -0
- package/README.ja.md +2 -0
- package/README.ko.md +2 -0
- package/README.md +3 -1
- package/README.zh-CN.md +2 -0
- package/agents/design-authority-watcher.md +42 -1
- package/agents/design-integration-checker.md +1 -1
- package/agents/design-planner.md +1 -1
- package/agents/gdd-graph-refresh.md +90 -0
- package/bin/gdd-graph +261 -0
- package/connections/connections.md +10 -9
- package/connections/graphify.md +65 -54
- package/package.json +4 -2
- package/reference/capability-gap-stage-gate.md +7 -4
- package/reference/known-failure-modes.md +337 -1
- package/reference/model-tiers.md +2 -2
- package/reference/schemas/events.schema.json +61 -0
- package/reference/start-interview.md +1 -1
- package/scripts/detect-stale-refs.cjs +6 -0
- package/scripts/lib/apply-reflections/incubator-proposals.cjs +10 -3
- package/scripts/lib/authority-watcher/index.cjs +201 -0
- package/scripts/lib/failure-mode-matcher.cjs +460 -0
- package/scripts/lib/graph/atomic-write.mjs +68 -0
- package/scripts/lib/graph/build.mjs +124 -0
- package/scripts/lib/graph/diff.mjs +90 -0
- package/scripts/lib/graph/index.mjs +14 -0
- package/scripts/lib/graph/query.mjs +155 -0
- package/scripts/lib/graph/schema.json +69 -0
- package/scripts/lib/graph/schema.mjs +47 -0
- package/scripts/lib/graph/status.mjs +88 -0
- package/scripts/lib/graph/token-estimate.mjs +27 -0
- package/scripts/lib/graph/upsert.mjs +210 -0
- package/scripts/lib/{gsd-health-mirror → health-mirror}/index.cjs +1 -1
- package/scripts/lib/install/interactive.cjs +27 -2
- package/scripts/lib/reflector-capability-gap-aggregator.cjs +32 -0
- package/scripts/lib/reflector-kfm-proposer.cjs +468 -0
- package/scripts/mcp-servers/gdd-mcp/tools/gdd_health.ts +3 -3
- package/skills/apply-reflections/SKILL.md +4 -0
- package/skills/apply-reflections/apply-reflections-procedure.md +38 -4
- package/skills/connections/connections-onboarding.md +6 -6
- package/skills/graphify/SKILL.md +11 -10
- package/skills/scan/scan-procedure.md +9 -8
- package/agents/gdd-graphify-sync.md +0 -110
- /package/scripts/lib/{gsd-health-mirror → health-mirror}/index.d.cts +0 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scripts/lib/reflector-kfm-proposer.cjs — Plan 30.5-03 Task 1.
|
|
3
|
+
*
|
|
4
|
+
* Reflector KFM proposer: when a capability_gap cluster recurs ≥3 times
|
|
5
|
+
* with NO matching entry in `reference/known-failure-modes.md`, this
|
|
6
|
+
* module drops a draft catalogue entry into
|
|
7
|
+
* `.design/reflections/incubator/kfm-<slug>/CATALOGUE-ENTRY.md`. The
|
|
8
|
+
* draft is STRICTLY proposal-only — promotion to the canonical catalogue
|
|
9
|
+
* is gated through `applyAccept()` (the apply-reflections accept action,
|
|
10
|
+
* Plan 30.5-03 Task 1 step 5).
|
|
11
|
+
*
|
|
12
|
+
* Decisions honored:
|
|
13
|
+
* * D-05 — Reflector follows Phase 29 incubator-author on-disk pattern
|
|
14
|
+
* (drafts in `.design/reflections/incubator/<slug>/`). User reviews
|
|
15
|
+
* via `/gdd:apply-reflections`.
|
|
16
|
+
* * D-06 — Same draft surface consumed by Task 2's authority-watcher
|
|
17
|
+
* `kfm-candidate` event. One unified review path, not two.
|
|
18
|
+
* * 30.5 D-07/D-08 — Re-uses `failure-mode-matcher.match()` for
|
|
19
|
+
* existing-entry detection. Threshold is the matcher's default 0.4
|
|
20
|
+
* unless overridden via options.matcherThreshold.
|
|
21
|
+
* * Phase 29 SC-8 — Nothing the reflector authors auto-ships. Drafts
|
|
22
|
+
* sit in the incubator until the user accepts them.
|
|
23
|
+
*
|
|
24
|
+
* Public API:
|
|
25
|
+
* proposeKfmDraft(input, options) → Result
|
|
26
|
+
* shouldPropose(cluster, options) → boolean
|
|
27
|
+
* applyAccept(draftPath, options) → { action: 'accepted', promotedModeId }
|
|
28
|
+
* applyReject(draftPath, options) → { action: 'rejected' }
|
|
29
|
+
* applyDefer(draftPath, options) → { action: 'deferred' }
|
|
30
|
+
* applyEdit(draftPath, options) → { action: 'edited', path }
|
|
31
|
+
*
|
|
32
|
+
* Input shape (capability_gap cluster shape from Plan 29-03):
|
|
33
|
+
* { cluster_id, size, intent_summary, symptom?, suggested_kind,
|
|
34
|
+
* posterior?, parent_event_ids?, sources?, ... }
|
|
35
|
+
*
|
|
36
|
+
* Alternate input shape (kfm-candidate event from Task 2):
|
|
37
|
+
* { event_type: 'kfm-candidate', event_id, article_url, article_title,
|
|
38
|
+
* suggested_symptom, suggested_pattern_hint, raw_excerpt, ... }
|
|
39
|
+
*
|
|
40
|
+
* Both shapes are merged into the same `kfm-<slug>/CATALOGUE-ENTRY.md`
|
|
41
|
+
* draft surface (D-06).
|
|
42
|
+
*
|
|
43
|
+
* Pure CommonJS, deps = node:fs + node:path. No npm dependencies.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
'use strict';
|
|
47
|
+
|
|
48
|
+
const fs = require('node:fs');
|
|
49
|
+
const path = require('node:path');
|
|
50
|
+
|
|
51
|
+
const matcher = require('./failure-mode-matcher.cjs');
|
|
52
|
+
|
|
53
|
+
// -------------------------------------------------------------------
|
|
54
|
+
// Constants
|
|
55
|
+
// -------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
const DEFAULT_STABILITY_K = 3;
|
|
58
|
+
const DEFAULT_MATCHER_THRESHOLD = 0.4; // matches failure-mode-matcher default
|
|
59
|
+
const INCUBATOR_PREFIX = 'kfm-';
|
|
60
|
+
|
|
61
|
+
// Phase 30.5 schema v2 fields. The two un-inferable ones (`pattern`,
|
|
62
|
+
// `fix`) get `TODO:` placeholders — user fills them via the apply-
|
|
63
|
+
// reflections edit action.
|
|
64
|
+
const REQUIRED_SCHEMA_FIELDS = Object.freeze([
|
|
65
|
+
'id',
|
|
66
|
+
'pattern',
|
|
67
|
+
'diagnosis',
|
|
68
|
+
'remedy',
|
|
69
|
+
'severity',
|
|
70
|
+
'propose_report',
|
|
71
|
+
'symptom',
|
|
72
|
+
'root_cause',
|
|
73
|
+
'fix',
|
|
74
|
+
'related_phases',
|
|
75
|
+
'first_observed_cycle',
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
// -------------------------------------------------------------------
|
|
79
|
+
// Helpers
|
|
80
|
+
// -------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
function findRepoRoot(startDir) {
|
|
83
|
+
let dir = startDir || __dirname;
|
|
84
|
+
for (let i = 0; i < 12; i++) {
|
|
85
|
+
if (fs.existsSync(path.join(dir, 'package.json'))) return dir;
|
|
86
|
+
const parent = path.dirname(dir);
|
|
87
|
+
if (parent === dir) break;
|
|
88
|
+
dir = parent;
|
|
89
|
+
}
|
|
90
|
+
return path.resolve(__dirname, '..', '..');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Kebab-case slug from a free-text symptom (mirrors incubator-author
|
|
95
|
+
* deriveSlug semantics — ASCII-only, dash-collapsed, ≤40 chars).
|
|
96
|
+
*/
|
|
97
|
+
function deriveSlug(text) {
|
|
98
|
+
const raw = typeof text === 'string' ? text : '';
|
|
99
|
+
let s = raw.toLowerCase();
|
|
100
|
+
s = s.replace(/[^\x20-\x7e]+/g, '');
|
|
101
|
+
s = s.replace(/[^a-z0-9]+/g, '-');
|
|
102
|
+
s = s.replace(/-+/g, '-');
|
|
103
|
+
s = s.replace(/^-+|-+$/g, '');
|
|
104
|
+
if (s.length > 40) s = s.slice(0, 40);
|
|
105
|
+
s = s.replace(/-+$/g, '');
|
|
106
|
+
return s || 'unnamed';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Quote a YAML scalar — single-quote shape with `''` escape (matches the
|
|
111
|
+
* catalogue's serialization).
|
|
112
|
+
*/
|
|
113
|
+
function quoteYaml(s) {
|
|
114
|
+
if (s === undefined || s === null) return "''";
|
|
115
|
+
const str = String(s);
|
|
116
|
+
// Use single quotes with `''` escape if value contains : # or starts/ends with whitespace.
|
|
117
|
+
if (/[:#'"\\\n\r]|^\s|\s$/.test(str) || str === '') {
|
|
118
|
+
return `'${str.replace(/'/g, "''")}'`;
|
|
119
|
+
}
|
|
120
|
+
return str;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Build a YAML block string from a fields object.
|
|
125
|
+
* Order: REQUIRED_SCHEMA_FIELDS, then any extras alphabetised.
|
|
126
|
+
*/
|
|
127
|
+
function serializeYaml(fields) {
|
|
128
|
+
const out = [];
|
|
129
|
+
const extras = Object.keys(fields)
|
|
130
|
+
.filter((k) => !REQUIRED_SCHEMA_FIELDS.includes(k))
|
|
131
|
+
.sort();
|
|
132
|
+
for (const k of [...REQUIRED_SCHEMA_FIELDS, ...extras]) {
|
|
133
|
+
if (!(k in fields)) continue;
|
|
134
|
+
const v = fields[k];
|
|
135
|
+
if (Array.isArray(v)) {
|
|
136
|
+
out.push(`${k}: [${v.join(', ')}]`);
|
|
137
|
+
} else if (typeof v === 'boolean') {
|
|
138
|
+
out.push(`${k}: ${v}`);
|
|
139
|
+
} else if (typeof v === 'number') {
|
|
140
|
+
out.push(`${k}: ${v}`);
|
|
141
|
+
} else {
|
|
142
|
+
out.push(`${k}: ${quoteYaml(v)}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return out.join('\n');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Compute the next available `KFM-NNN` numeric id from the catalogue.
|
|
150
|
+
* Returns the modeId string.
|
|
151
|
+
*/
|
|
152
|
+
function nextKfmId(cataloguePath) {
|
|
153
|
+
let max = 0;
|
|
154
|
+
try {
|
|
155
|
+
const text = fs.readFileSync(cataloguePath, 'utf8');
|
|
156
|
+
const ids = text.match(/id:\s*KFM-(\d+)/g) || [];
|
|
157
|
+
for (const m of ids) {
|
|
158
|
+
const n = parseInt(m.replace(/[^0-9]/g, ''), 10);
|
|
159
|
+
if (Number.isFinite(n) && n > max) max = n;
|
|
160
|
+
}
|
|
161
|
+
} catch (_e) {
|
|
162
|
+
// Catalogue missing — start from 1.
|
|
163
|
+
}
|
|
164
|
+
return `KFM-${String(max + 1).padStart(3, '0')}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// -------------------------------------------------------------------
|
|
168
|
+
// Input shape normalisation (cluster OR kfm-candidate event)
|
|
169
|
+
// -------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Normalise either a capability_gap cluster OR a kfm-candidate event
|
|
173
|
+
* into a uniform `{ symptom, slug, size, sourceLabel, articleUrl?,
|
|
174
|
+
* articleTitle?, suggestedPatternHint?, rawExcerpt? }` shape.
|
|
175
|
+
*/
|
|
176
|
+
function normaliseInput(input) {
|
|
177
|
+
if (!input || typeof input !== 'object') return null;
|
|
178
|
+
|
|
179
|
+
// kfm-candidate event shape (Task 2, D-06).
|
|
180
|
+
if (input.event_type === 'kfm-candidate' ||
|
|
181
|
+
(input.source === 'authority_watcher' && input.suggested_symptom)) {
|
|
182
|
+
const symptom = String(input.suggested_symptom || '').trim();
|
|
183
|
+
if (!symptom) return null;
|
|
184
|
+
return {
|
|
185
|
+
symptom,
|
|
186
|
+
slug: deriveSlug(symptom),
|
|
187
|
+
size: 1, // single-event source; bypasses ≥3 gate (D-06: authority signal is a 1-shot whitelist match).
|
|
188
|
+
sourceLabel: 'authority_watcher',
|
|
189
|
+
articleUrl: input.article_url,
|
|
190
|
+
articleTitle: input.article_title,
|
|
191
|
+
suggestedPatternHint: input.suggested_pattern_hint,
|
|
192
|
+
rawExcerpt: input.raw_excerpt,
|
|
193
|
+
via: 'kfm-candidate',
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// capability_gap cluster shape (Task 1, D-05).
|
|
198
|
+
const symptom = String(
|
|
199
|
+
input.symptom || input.intent_summary || ''
|
|
200
|
+
).trim();
|
|
201
|
+
if (!symptom) return null;
|
|
202
|
+
return {
|
|
203
|
+
symptom,
|
|
204
|
+
slug: deriveSlug(symptom),
|
|
205
|
+
size: Number(input.size) || 0,
|
|
206
|
+
sourceLabel: 'reflector_capability_gap',
|
|
207
|
+
parentEventIds: Array.isArray(input.parent_event_ids) ? input.parent_event_ids : [],
|
|
208
|
+
suggestedKind: input.suggested_kind,
|
|
209
|
+
posterior: input.posterior,
|
|
210
|
+
sources: input.sources,
|
|
211
|
+
cycles: input.cycles_observed,
|
|
212
|
+
via: 'capability_gap',
|
|
213
|
+
intentSummary: input.intent_summary,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// -------------------------------------------------------------------
|
|
218
|
+
// Public API
|
|
219
|
+
// -------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Decide whether a normalised input qualifies for proposal.
|
|
223
|
+
* Returns { ok: boolean, reason?: string, matchedModeId?: string }.
|
|
224
|
+
*/
|
|
225
|
+
function shouldPropose(input, options) {
|
|
226
|
+
const opts = options || {};
|
|
227
|
+
const repoRoot = opts.repoRoot || findRepoRoot();
|
|
228
|
+
const cataloguePath = path.join(repoRoot, 'reference', 'known-failure-modes.md');
|
|
229
|
+
const threshold = Number.isFinite(opts.matcherThreshold)
|
|
230
|
+
? opts.matcherThreshold
|
|
231
|
+
: DEFAULT_MATCHER_THRESHOLD;
|
|
232
|
+
const stabilityK = Number.isFinite(opts.stabilityK)
|
|
233
|
+
? opts.stabilityK
|
|
234
|
+
: DEFAULT_STABILITY_K;
|
|
235
|
+
|
|
236
|
+
const norm = normaliseInput(input);
|
|
237
|
+
if (!norm) return { ok: false, reason: 'invalid_input' };
|
|
238
|
+
|
|
239
|
+
// kfm-candidate events bypass ≥K gate (D-06 — authority-watcher is a
|
|
240
|
+
// human-curated whitelist hit, treated as 1-shot signal).
|
|
241
|
+
if (norm.via !== 'kfm-candidate' && norm.size < stabilityK) {
|
|
242
|
+
return { ok: false, reason: 'below_stability_k' };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Existing-entry check via failure-mode-matcher.
|
|
246
|
+
const matches = matcher.match(
|
|
247
|
+
{ message: norm.symptom, stack: '' },
|
|
248
|
+
{ cataloguePath, threshold, topN: 1 }
|
|
249
|
+
);
|
|
250
|
+
if (Array.isArray(matches) && matches.length >= 1 && matches[0].confidence >= threshold) {
|
|
251
|
+
return { ok: false, reason: 'matched_existing', matchedModeId: matches[0].modeId };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return { ok: true };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Propose a KFM draft for a capability_gap cluster OR a kfm-candidate
|
|
259
|
+
* event. Returns:
|
|
260
|
+
* { action: 'drafted', path, slug, proposed_id }
|
|
261
|
+
* { action: 'skipped', reason, matchedModeId? }
|
|
262
|
+
*/
|
|
263
|
+
function proposeKfmDraft(input, options) {
|
|
264
|
+
const opts = options || {};
|
|
265
|
+
const repoRoot = opts.repoRoot || findRepoRoot();
|
|
266
|
+
const incubatorRoot = path.join(repoRoot, '.design', 'reflections', 'incubator');
|
|
267
|
+
const cataloguePath = path.join(repoRoot, 'reference', 'known-failure-modes.md');
|
|
268
|
+
const now = opts.now || new Date().toISOString().slice(0, 10);
|
|
269
|
+
const cycleSlug = opts.cycleSlug || `cycle-${now.slice(0, 7)}`; // cycle-YYYY-MM
|
|
270
|
+
|
|
271
|
+
const gate = shouldPropose(input, opts);
|
|
272
|
+
if (!gate.ok) {
|
|
273
|
+
return { action: 'skipped', reason: gate.reason, matchedModeId: gate.matchedModeId };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const norm = normaliseInput(input);
|
|
277
|
+
const slug = norm.slug;
|
|
278
|
+
const proposedId = opts.proposedId || nextKfmId(cataloguePath);
|
|
279
|
+
|
|
280
|
+
// Provisional schema fields — `pattern` and `fix` MUST be placeholders
|
|
281
|
+
// per Plan 30.5-03 Task 1 step 3 (reflector can't infer these).
|
|
282
|
+
const fields = {
|
|
283
|
+
id: proposedId,
|
|
284
|
+
pattern: 'TODO: <regex against error.message + error.stack>',
|
|
285
|
+
diagnosis: norm.symptom.length > 0 ? norm.symptom.split('\n')[0].slice(0, 240) : 'TODO: <one-sentence root cause>',
|
|
286
|
+
remedy: 'TODO: <user-runnable one-liner>',
|
|
287
|
+
severity: 'medium',
|
|
288
|
+
propose_report: false,
|
|
289
|
+
symptom: norm.symptom,
|
|
290
|
+
root_cause: norm.suggestedPatternHint || 'TODO: <technical explanation>',
|
|
291
|
+
fix: 'TODO: <step-by-step user-runnable remedy>',
|
|
292
|
+
related_phases: [],
|
|
293
|
+
first_observed_cycle: cycleSlug,
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const draftDir = path.join(incubatorRoot, `${INCUBATOR_PREFIX}${slug}`);
|
|
297
|
+
fs.mkdirSync(draftDir, { recursive: true });
|
|
298
|
+
const draftPath = path.join(draftDir, 'CATALOGUE-ENTRY.md');
|
|
299
|
+
|
|
300
|
+
const originHeader = [
|
|
301
|
+
`# KFM proposal — ${proposedId}`,
|
|
302
|
+
'',
|
|
303
|
+
`**Source:** ${norm.sourceLabel}`,
|
|
304
|
+
`**Via:** ${norm.via}`,
|
|
305
|
+
norm.parentEventIds ? `**Parent event ids:** ${norm.parentEventIds.join(', ') || '(none)'}` : null,
|
|
306
|
+
norm.articleUrl ? `**Article URL:** ${norm.articleUrl}` : null,
|
|
307
|
+
norm.articleTitle ? `**Article title:** ${norm.articleTitle}` : null,
|
|
308
|
+
norm.rawExcerpt ? `**Excerpt:** ${norm.rawExcerpt.replace(/\n/g, ' ').slice(0, 500)}` : null,
|
|
309
|
+
'',
|
|
310
|
+
`Drafted ${now}. Review via \`/gdd:apply-reflections\` → [KFM-CANDIDATE] proposal class.`,
|
|
311
|
+
'',
|
|
312
|
+
'Fill the `TODO:` placeholders before accepting. The `pattern` regex is matched against',
|
|
313
|
+
'`[error.message, error.stack].filter(Boolean).join("\\n")` — keep it conservative so',
|
|
314
|
+
'first-match-wins (Phase 30 D-13) does not steal traffic from other entries.',
|
|
315
|
+
'',
|
|
316
|
+
'## Proposed YAML',
|
|
317
|
+
'',
|
|
318
|
+
'```yaml',
|
|
319
|
+
serializeYaml(fields),
|
|
320
|
+
'```',
|
|
321
|
+
'',
|
|
322
|
+
].filter((line) => line !== null).join('\n');
|
|
323
|
+
|
|
324
|
+
fs.writeFileSync(draftPath, originHeader);
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
action: 'drafted',
|
|
328
|
+
path: draftPath,
|
|
329
|
+
slug: `${INCUBATOR_PREFIX}${slug}`,
|
|
330
|
+
proposed_id: proposedId,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// -------------------------------------------------------------------
|
|
335
|
+
// Apply-reflections actions: accept / reject / defer / edit
|
|
336
|
+
// -------------------------------------------------------------------
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Promote a draft → canonical catalogue + registry.json.
|
|
340
|
+
* Returns { action: 'accepted', promotedModeId }.
|
|
341
|
+
*/
|
|
342
|
+
function applyAccept(draftPath, options) {
|
|
343
|
+
const opts = options || {};
|
|
344
|
+
const repoRoot = opts.repoRoot || findRepoRoot();
|
|
345
|
+
const cataloguePath = path.join(repoRoot, 'reference', 'known-failure-modes.md');
|
|
346
|
+
const registryPath = path.join(repoRoot, 'reference', 'registry.json');
|
|
347
|
+
|
|
348
|
+
if (!fs.existsSync(draftPath)) {
|
|
349
|
+
throw new Error(`KFM draft not found: ${draftPath}`);
|
|
350
|
+
}
|
|
351
|
+
const draftText = fs.readFileSync(draftPath, 'utf8');
|
|
352
|
+
const yamlMatch = draftText.match(/```yaml\s*\n([\s\S]*?)\n```/);
|
|
353
|
+
if (!yamlMatch) {
|
|
354
|
+
throw new Error(`KFM draft missing yaml block: ${draftPath}`);
|
|
355
|
+
}
|
|
356
|
+
let yamlBody = yamlMatch[1];
|
|
357
|
+
|
|
358
|
+
// Re-stamp id to next available — the proposed id may have collided
|
|
359
|
+
// with intervening promotions on shared incubator surfaces.
|
|
360
|
+
const finalId = opts.finalId || nextKfmId(cataloguePath);
|
|
361
|
+
yamlBody = yamlBody.replace(/^id:\s*KFM-\d+/m, `id: ${finalId}`);
|
|
362
|
+
|
|
363
|
+
// Extract the symptom for the catalogue heading.
|
|
364
|
+
const symptomMatch = yamlBody.match(/^symptom:\s*'?(.+?)'?$/m);
|
|
365
|
+
const symptomHeading = symptomMatch ? symptomMatch[1].slice(0, 80) : finalId;
|
|
366
|
+
|
|
367
|
+
// Append to catalogue.
|
|
368
|
+
const block = `\n### ${finalId} — ${symptomHeading}\n\nPromoted from incubator KFM proposal.\n\n\`\`\`yaml\n${yamlBody}\n\`\`\`\n`;
|
|
369
|
+
fs.appendFileSync(cataloguePath, block);
|
|
370
|
+
|
|
371
|
+
// Register in registry.json.
|
|
372
|
+
let registry;
|
|
373
|
+
try {
|
|
374
|
+
registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
375
|
+
} catch (_e) {
|
|
376
|
+
registry = { version: 1, entries: [] };
|
|
377
|
+
}
|
|
378
|
+
if (!Array.isArray(registry.entries)) registry.entries = [];
|
|
379
|
+
registry.entries.push({
|
|
380
|
+
name: `known-failure-modes/${finalId.toLowerCase()}`,
|
|
381
|
+
path: 'reference/known-failure-modes.md',
|
|
382
|
+
type: 'failure-mode',
|
|
383
|
+
phase: 30.5,
|
|
384
|
+
description: `${finalId} — ${symptomHeading}`,
|
|
385
|
+
origin: 'incubator-kfm',
|
|
386
|
+
added: new Date().toISOString().slice(0, 10),
|
|
387
|
+
});
|
|
388
|
+
fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
|
|
389
|
+
|
|
390
|
+
// Remove incubator dir LAST (T-29.05-04 — partial failure leaves draft retryable).
|
|
391
|
+
const incubatorDir = path.dirname(draftPath);
|
|
392
|
+
try {
|
|
393
|
+
for (const f of fs.readdirSync(incubatorDir)) {
|
|
394
|
+
fs.unlinkSync(path.join(incubatorDir, f));
|
|
395
|
+
}
|
|
396
|
+
fs.rmdirSync(incubatorDir);
|
|
397
|
+
} catch (_e) {
|
|
398
|
+
// Best-effort; the catalogue + registry promotions already landed.
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return { action: 'accepted', promotedModeId: finalId };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Remove the incubator draft directory.
|
|
406
|
+
*/
|
|
407
|
+
function applyReject(draftPath, _options) {
|
|
408
|
+
if (!fs.existsSync(draftPath)) {
|
|
409
|
+
return { action: 'rejected', noop: true };
|
|
410
|
+
}
|
|
411
|
+
const dir = path.dirname(draftPath);
|
|
412
|
+
try {
|
|
413
|
+
for (const f of fs.readdirSync(dir)) {
|
|
414
|
+
fs.unlinkSync(path.join(dir, f));
|
|
415
|
+
}
|
|
416
|
+
fs.rmdirSync(dir);
|
|
417
|
+
} catch (_e) {
|
|
418
|
+
// Best-effort.
|
|
419
|
+
}
|
|
420
|
+
return { action: 'rejected' };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Stamp `deferred_until` into the draft body. Draft remains in place.
|
|
425
|
+
*/
|
|
426
|
+
function applyDefer(draftPath, options) {
|
|
427
|
+
const opts = options || {};
|
|
428
|
+
if (!fs.existsSync(draftPath)) {
|
|
429
|
+
throw new Error(`KFM draft not found: ${draftPath}`);
|
|
430
|
+
}
|
|
431
|
+
const deferredUntil = opts.deferredUntil || new Date(Date.now() + 30 * 86_400_000).toISOString().slice(0, 10);
|
|
432
|
+
const orig = fs.readFileSync(draftPath, 'utf8');
|
|
433
|
+
let updated;
|
|
434
|
+
if (/^deferred_until:/m.test(orig)) {
|
|
435
|
+
updated = orig.replace(/^deferred_until:.*$/m, `deferred_until: ${deferredUntil}`);
|
|
436
|
+
} else {
|
|
437
|
+
updated = `${orig}\ndeferred_until: ${deferredUntil}\n`;
|
|
438
|
+
}
|
|
439
|
+
fs.writeFileSync(draftPath, updated);
|
|
440
|
+
return { action: 'deferred', deferredUntil };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Edit hook — returns the draft path so the caller can open `$EDITOR`.
|
|
445
|
+
* Caller re-renders the proposal after edit, per Phase 29-05 semantics.
|
|
446
|
+
*/
|
|
447
|
+
function applyEdit(draftPath, _options) {
|
|
448
|
+
if (!fs.existsSync(draftPath)) {
|
|
449
|
+
throw new Error(`KFM draft not found: ${draftPath}`);
|
|
450
|
+
}
|
|
451
|
+
return { action: 'edited', path: draftPath };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
module.exports = {
|
|
455
|
+
proposeKfmDraft,
|
|
456
|
+
shouldPropose,
|
|
457
|
+
applyAccept,
|
|
458
|
+
applyReject,
|
|
459
|
+
applyDefer,
|
|
460
|
+
applyEdit,
|
|
461
|
+
// Exposed for tests / higher-level integration.
|
|
462
|
+
_deriveSlug: deriveSlug,
|
|
463
|
+
_nextKfmId: nextKfmId,
|
|
464
|
+
_normaliseInput: normaliseInput,
|
|
465
|
+
_REQUIRED_SCHEMA_FIELDS: REQUIRED_SCHEMA_FIELDS,
|
|
466
|
+
_DEFAULT_STABILITY_K: DEFAULT_STABILITY_K,
|
|
467
|
+
_DEFAULT_MATCHER_THRESHOLD: DEFAULT_MATCHER_THRESHOLD,
|
|
468
|
+
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// scripts/mcp-servers/gdd-mcp/tools/gdd_health.ts
|
|
2
2
|
//
|
|
3
|
-
// Plan 27.7-02
|
|
4
|
-
//
|
|
3
|
+
// Plan 27.7-02 (lib renamed to health-mirror in Phase 30.6-08 per D-10).
|
|
4
|
+
// Read-only mirror of skills/health/SKILL.md output. No subprocess spawn — pure inspection.
|
|
5
5
|
|
|
6
|
-
import { getHealthChecks } from '../../../lib/
|
|
6
|
+
import { getHealthChecks } from '../../../lib/health-mirror/index.cjs';
|
|
7
7
|
import { errorResponse, okResponse, resolveProjectRoot, type ToolResponse } from './shared.ts';
|
|
8
8
|
|
|
9
9
|
export const name = 'gdd_health';
|
|
@@ -80,6 +80,10 @@ Incubator drafts authored by `scripts/lib/incubator-author.cjs` (Phase 29-04) ap
|
|
|
80
80
|
|
|
81
81
|
**Stage-1 gate.** At session start, call `checkStage1Gate()`. If `thresholdMet && !optInRecorded`, display the opt-in prompt once. NEVER auto-flip per D-01 — recording opt-in requires explicit user confirmation via `recordOptIn()`. Full procedure: `./apply-reflections-procedure.md` §[INCUBATOR].
|
|
82
82
|
|
|
83
|
+
## [KFM-CANDIDATE]
|
|
84
|
+
|
|
85
|
+
KFM-catalogue proposals authored by `scripts/lib/reflector-kfm-proposer.cjs` (Phase 30.5-03 D-05) appear as a 6th proposal class. Drafts at `.design/reflections/incubator/kfm-<slug>/CATALOGUE-ENTRY.md`; pre-filled 11-field schema with `TODO:` placeholders for `pattern` + `fix`. Two upstream signals share the surface (D-06): `capability_gap` clusters (≥3, no existing match) + `kfm-candidate` events (whitelist-matched articles, 1-shot). User chooses **accept** | **reject** | **defer** | **edit**. `applyAccept` appends to `reference/known-failure-modes.md` + `reference/registry.json` (`origin: incubator-kfm`); `applyReject` removes the incubator subdir; `applyDefer` stamps `deferred_until`; `applyEdit` returns the draft path for `$EDITOR`. Full procedure: `./apply-reflections-procedure.md` §[KFM-CANDIDATE].
|
|
86
|
+
|
|
83
87
|
## Do Not
|
|
84
88
|
|
|
85
89
|
- Do not apply any proposal without the user explicitly choosing `a` or `e`.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: apply-reflections-procedure
|
|
3
3
|
type: heuristic
|
|
4
|
-
version: 1.
|
|
5
|
-
phase:
|
|
6
|
-
tags: [apply-reflections, proposal, frontmatter, reference, budget, question, global-skill, incubator]
|
|
7
|
-
last_updated: 2026-05-
|
|
4
|
+
version: 1.3.0
|
|
5
|
+
phase: 30.5
|
|
6
|
+
tags: [apply-reflections, proposal, frontmatter, reference, budget, question, global-skill, incubator, kfm-candidate]
|
|
7
|
+
last_updated: 2026-05-21
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Apply-Reflections — Per-Type Procedure
|
|
@@ -134,3 +134,37 @@ bandit.updateWithDelegate({
|
|
|
134
134
|
```
|
|
135
135
|
|
|
136
136
|
Omitting `prior_class` reverts to Phase 23.5 informed-prior bootstrap (non-breaking). The reward math is unchanged — `prior_class` only affects bootstrap.
|
|
137
|
+
|
|
138
|
+
### [KFM-CANDIDATE]
|
|
139
|
+
|
|
140
|
+
Known-failure-mode catalogue proposals come from `scripts/lib/reflector-kfm-proposer.cjs` (Phase 30.5-03 D-05). They live at `.design/reflections/incubator/kfm-<slug>/CATALOGUE-ENTRY.md` and contain a single fenced ```yaml block pre-filled with the Phase 30.5 schema-v2 11-field shape (`id` + `pattern` + `diagnosis` + `remedy` + `severity` + `propose_report` + `symptom` + `root_cause` + `fix` + `related_phases` + `first_observed_cycle`). Two of those — `pattern` and `fix` — are `TODO:` placeholders the reflector cannot infer; the user fills them via the **edit** action before accepting.
|
|
141
|
+
|
|
142
|
+
Two upstream signals share this draft surface (D-06):
|
|
143
|
+
- `capability_gap` clusters of size ≥3 with no existing-entry match (Phase 29-03 aggregator + `failure-mode-matcher.match()`).
|
|
144
|
+
- `kfm-candidate` events from the Phase 30.5-03 Task 2 authority-watcher whitelist (D-06 — single events bypass the ≥3 gate).
|
|
145
|
+
|
|
146
|
+
Use `scripts/lib/reflector-kfm-proposer.cjs` for all actions:
|
|
147
|
+
|
|
148
|
+
**Discovery + render** (once per cycle):
|
|
149
|
+
|
|
150
|
+
1. Glob `.design/reflections/incubator/kfm-*/CATALOGUE-ENTRY.md` → list pending KFM drafts.
|
|
151
|
+
2. For each draft: read the body, show the origin header (source, parent event ids OR article url) + the proposed yaml block.
|
|
152
|
+
3. Prompt: `(a) accept (r) reject (d) defer (e) edit (q) quit`.
|
|
153
|
+
|
|
154
|
+
**Per-action behavior:**
|
|
155
|
+
|
|
156
|
+
1. **accept** — call `applyAccept(draftPath, { repoRoot })`.
|
|
157
|
+
- The helper re-stamps the proposed `id` with the next available `KFM-NNN` from the catalogue (avoids collisions when multiple drafts promote in the same run).
|
|
158
|
+
- Appends a `### KFM-NNN — <symptom heading>` section into `reference/known-failure-modes.md` with the yaml block intact.
|
|
159
|
+
- Appends a `reference/registry.json` entry: `{ name: 'known-failure-modes/kfm-NNN', path: 'reference/known-failure-modes.md', type: 'failure-mode', phase: 30.5, origin: 'incubator-kfm', added: '<ISO date>' }`.
|
|
160
|
+
- Removes the incubator subdir LAST (partial-failure leaves the draft retryable).
|
|
161
|
+
- Print: "Accepted — promoted to KFM-NNN in reference/known-failure-modes.md."
|
|
162
|
+
- Append `**Applied**: <date>` to the proposal entry (when surfaced from a reflections file).
|
|
163
|
+
|
|
164
|
+
2. **reject** — call `applyReject(draftPath)`. Only the incubator subdir is removed; catalogue + registry untouched. Print: "Rejected — draft removed."
|
|
165
|
+
|
|
166
|
+
3. **defer** — call `applyDefer(draftPath, { deferredUntil })` where `deferredUntil` is an ISO date (default: today + 30d). The helper stamps `deferred_until: <ISO>` into the draft body. Print: "Deferred — draft re-surfaces next run."
|
|
167
|
+
|
|
168
|
+
4. **edit** — call `applyEdit(draftPath)` which returns the draft path. The caller opens `$EDITOR` on the path; on clean exit, re-discover the draft and re-prompt. Typical edits: replace `pattern: 'TODO: ...'` with a conservative regex, replace `fix: 'TODO: ...'` with a step-by-step user-runnable remedy, set `severity` if `medium` default is wrong.
|
|
169
|
+
|
|
170
|
+
**Why this is gated.** `reference/known-failure-modes.md` feeds Phase 30's `triage-matcher.cjs` BEFORE the consent prompt — a bad entry could mute legitimate issue reports. The user-review gate is non-negotiable (D-05). The proposer is strictly proposal-only; the canonical catalogue only changes via the accept action.
|
|
@@ -106,13 +106,13 @@ Bash: command -v chromatic >/dev/null 2>&1 || npx --yes chromatic --version 2>/d
|
|
|
106
106
|
Set → chromatic: available
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
-
**graphify** (CLI + file):
|
|
109
|
+
**graphify** (native CLI + file):
|
|
110
110
|
```
|
|
111
|
-
Bash: node "
|
|
112
|
-
→
|
|
113
|
-
→
|
|
114
|
-
|
|
115
|
-
|
|
111
|
+
Bash: node -e "try{const c=JSON.parse(require('fs').readFileSync('.design/config.json','utf8'));process.stdout.write(String(c.graphify?.enabled===true))}catch{process.stdout.write('false')}"
|
|
112
|
+
→ false → graphify: not_configured
|
|
113
|
+
→ true → Bash: node bin/gdd-graph status --format json
|
|
114
|
+
→ { configured: true, exists: false } → graphify: unavailable
|
|
115
|
+
→ { configured: true, exists: true } → graphify: available
|
|
116
116
|
```
|
|
117
117
|
|
|
118
118
|
**pencil-dev** (file probe):
|
package/skills/graphify/SKILL.md
CHANGED
|
@@ -19,30 +19,31 @@ Thin command wrapper around the GSD graphify tools integration.
|
|
|
19
19
|
## Behavior
|
|
20
20
|
|
|
21
21
|
1. Read `.design/STATE.md` to check `graphify` status in `<connections>`.
|
|
22
|
-
2. Check `graphify.enabled` in `.
|
|
22
|
+
2. Check `graphify.enabled` in `.design/config.json` via a direct file read (per D-09 — no `config-get` CLI subcommand):
|
|
23
23
|
```
|
|
24
|
-
node
|
|
24
|
+
node -e "try{const c=JSON.parse(require('fs').readFileSync('.design/config.json','utf8'));process.stdout.write(String(c.graphify?.enabled===true))}catch{process.stdout.write('false')}"
|
|
25
25
|
```
|
|
26
26
|
3. If not enabled, print:
|
|
27
27
|
```
|
|
28
|
-
"Graphify is not enabled.
|
|
28
|
+
"Graphify is not enabled. Edit `.design/config.json` to set `graphify.enabled: true`."
|
|
29
29
|
"Then run /gdd:graphify build to generate the knowledge graph."
|
|
30
30
|
```
|
|
31
31
|
STOP.
|
|
32
|
-
4. Execute the requested subcommand via
|
|
33
|
-
- build: `node
|
|
34
|
-
- query: `node
|
|
35
|
-
- status: `node
|
|
36
|
-
- diff: `node
|
|
32
|
+
4. Execute the requested subcommand via the native CLI:
|
|
33
|
+
- build: `node bin/gdd-graph build`
|
|
34
|
+
- query: `node bin/gdd-graph query "<term>" --budget 2000`
|
|
35
|
+
- status: `node bin/gdd-graph status`
|
|
36
|
+
- diff: `node bin/gdd-graph diff`
|
|
37
37
|
5. After `build` completes, update `.design/STATE.md` `<connections>`: `graphify: available`
|
|
38
38
|
|
|
39
39
|
## Required Reading
|
|
40
40
|
|
|
41
41
|
- `.design/STATE.md` — for graphify status in `<connections>`
|
|
42
|
-
- `.
|
|
42
|
+
- `.design/config.json` — for `graphify.enabled` flag
|
|
43
43
|
|
|
44
44
|
## Notes
|
|
45
45
|
|
|
46
|
-
- Graphify is optional.
|
|
46
|
+
- Graphify is optional. The native CLI ships in this repo at `bin/gdd-graph` (no external install — Node only).
|
|
47
|
+
- Graph is stored at `.design/graph/graph.json` (Ajv-validated against `scripts/lib/graph/schema.json`).
|
|
47
48
|
- Graph covers source code (`src/`, `components/`). It does NOT index `.design/` artifacts by default.
|
|
48
49
|
- Use `query` with node IDs from the graph schema: `component:<name>`, `token:color/<name>`, `decision:D-<nn>`, etc.
|
|
@@ -136,14 +136,15 @@ Write: chromatic: <status> to STATE.md <connections>
|
|
|
136
136
|
|
|
137
137
|
```
|
|
138
138
|
Step G1 — Config check:
|
|
139
|
-
Bash: node "
|
|
140
|
-
->
|
|
141
|
-
->
|
|
142
|
-
|
|
143
|
-
Step G2 — Graph file check:
|
|
144
|
-
Bash:
|
|
145
|
-
->
|
|
146
|
-
->
|
|
139
|
+
Bash: node -e "try{const c=JSON.parse(require('fs').readFileSync('.design/config.json','utf8'));process.stdout.write(String(c.graphify?.enabled===true))}catch{process.stdout.write('false')}"
|
|
140
|
+
-> false -> graphify: not_configured
|
|
141
|
+
-> true -> proceed to Step G2
|
|
142
|
+
|
|
143
|
+
Step G2 — Graph file check (status JSON):
|
|
144
|
+
Bash: node bin/gdd-graph status --format json
|
|
145
|
+
-> { configured: true, exists: true, ... } -> graphify: available
|
|
146
|
+
-> { configured: false, exists: false } or -> graphify: unavailable
|
|
147
|
+
{ exists: false }
|
|
147
148
|
|
|
148
149
|
Write: graphify: <status> to STATE.md <connections>
|
|
149
150
|
```
|