@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.
Files changed (49) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +103 -0
  4. package/README.de.md +2 -0
  5. package/README.fr.md +2 -0
  6. package/README.it.md +2 -0
  7. package/README.ja.md +2 -0
  8. package/README.ko.md +2 -0
  9. package/README.md +3 -1
  10. package/README.zh-CN.md +2 -0
  11. package/agents/design-authority-watcher.md +42 -1
  12. package/agents/design-integration-checker.md +1 -1
  13. package/agents/design-planner.md +1 -1
  14. package/agents/gdd-graph-refresh.md +90 -0
  15. package/bin/gdd-graph +261 -0
  16. package/connections/connections.md +10 -9
  17. package/connections/graphify.md +65 -54
  18. package/package.json +4 -2
  19. package/reference/capability-gap-stage-gate.md +7 -4
  20. package/reference/known-failure-modes.md +337 -1
  21. package/reference/model-tiers.md +2 -2
  22. package/reference/schemas/events.schema.json +61 -0
  23. package/reference/start-interview.md +1 -1
  24. package/scripts/detect-stale-refs.cjs +6 -0
  25. package/scripts/lib/apply-reflections/incubator-proposals.cjs +10 -3
  26. package/scripts/lib/authority-watcher/index.cjs +201 -0
  27. package/scripts/lib/failure-mode-matcher.cjs +460 -0
  28. package/scripts/lib/graph/atomic-write.mjs +68 -0
  29. package/scripts/lib/graph/build.mjs +124 -0
  30. package/scripts/lib/graph/diff.mjs +90 -0
  31. package/scripts/lib/graph/index.mjs +14 -0
  32. package/scripts/lib/graph/query.mjs +155 -0
  33. package/scripts/lib/graph/schema.json +69 -0
  34. package/scripts/lib/graph/schema.mjs +47 -0
  35. package/scripts/lib/graph/status.mjs +88 -0
  36. package/scripts/lib/graph/token-estimate.mjs +27 -0
  37. package/scripts/lib/graph/upsert.mjs +210 -0
  38. package/scripts/lib/{gsd-health-mirror → health-mirror}/index.cjs +1 -1
  39. package/scripts/lib/install/interactive.cjs +27 -2
  40. package/scripts/lib/reflector-capability-gap-aggregator.cjs +32 -0
  41. package/scripts/lib/reflector-kfm-proposer.cjs +468 -0
  42. package/scripts/mcp-servers/gdd-mcp/tools/gdd_health.ts +3 -3
  43. package/skills/apply-reflections/SKILL.md +4 -0
  44. package/skills/apply-reflections/apply-reflections-procedure.md +38 -4
  45. package/skills/connections/connections-onboarding.md +6 -6
  46. package/skills/graphify/SKILL.md +11 -10
  47. package/skills/scan/scan-procedure.md +9 -8
  48. package/agents/gdd-graphify-sync.md +0 -110
  49. /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. Read-only mirror of skills/health/SKILL.md output via
4
- // scripts/lib/gsd-health-mirror/. No subprocess spawn — pure inspection.
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/gsd-health-mirror/index.cjs';
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.2.0
5
- phase: 28.5
6
- tags: [apply-reflections, proposal, frontmatter, reference, budget, question, global-skill, incubator]
7
- last_updated: 2026-05-20
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 "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" graphify status 2>/dev/null
112
- Error or enabled:false → graphify: not_configured
113
- enabled:true check graphify-out/graph.json exists
114
- Absent → graphify: unavailable
115
- Present → graphify: available
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):
@@ -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 `.planning/config.json` via:
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 "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get graphify.enabled
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. Enable with: node gsd-tools.cjs config-set graphify.enabled true"
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 GSD tools:
33
- - build: `node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" graphify build`
34
- - query: `node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" graphify query "<term>" --budget 2000`
35
- - status: `node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" graphify status`
36
- - diff: `node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" graphify diff`
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
- - `.planning/config.json` — for `graphify.enabled` flag
42
+ - `.design/config.json` — for `graphify.enabled` flag
43
43
 
44
44
  ## Notes
45
45
 
46
- - Graphify is optional. If the binary is not installed (`pip install graphifyy`), the build subcommand will fail with an install prompt.
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 "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" graphify status
140
- -> Error or { enabled: false } -> graphify: not_configured
141
- -> { enabled: true } -> proceed to Step G2
142
-
143
- Step G2 — Graph file check:
144
- Bash: test -f graphify-out/graph.json
145
- -> Present -> graphify: available
146
- -> Absent -> graphify: unavailable
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
  ```