@hegemonart/get-design-done 1.53.0 → 1.55.0

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 (56) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +88 -0
  4. package/README.md +4 -0
  5. package/SKILL.md +2 -1
  6. package/agents/component-taxonomy-mapper.md +3 -0
  7. package/agents/motion-mapper.md +1 -0
  8. package/agents/token-mapper.md +3 -0
  9. package/bin/gdd-dashboard +91 -0
  10. package/dist/claude-code/.claude/skills/new-addendum/SKILL.md +81 -0
  11. package/package.json +2 -1
  12. package/reference/frameworks/astro.md +43 -0
  13. package/reference/frameworks/nextjs.md +44 -0
  14. package/reference/frameworks/remix.md +44 -0
  15. package/reference/frameworks/storybook.md +44 -0
  16. package/reference/frameworks/sveltekit.md +43 -0
  17. package/reference/frameworks/vite-react.md +43 -0
  18. package/reference/interaction.md +1 -0
  19. package/reference/motion/framer-motion.md +45 -0
  20. package/reference/motion/gsap.md +45 -0
  21. package/reference/motion/motion-one.md +44 -0
  22. package/reference/motion/react-spring.md +44 -0
  23. package/reference/motion.md +1 -0
  24. package/reference/registry.json +163 -1
  25. package/reference/registry.schema.json +18 -1
  26. package/reference/skill-graph.md +2 -1
  27. package/reference/systems/chakra.md +44 -0
  28. package/reference/systems/css-modules.md +44 -0
  29. package/reference/systems/mui.md +44 -0
  30. package/reference/systems/radix-themes.md +43 -0
  31. package/reference/systems/shadcn.md +45 -0
  32. package/reference/systems/styled-components.md +44 -0
  33. package/reference/systems/tailwind.md +44 -0
  34. package/reference/systems/vanilla-extract.md +44 -0
  35. package/scripts/lib/dashboard/graph-html.cjs +0 -0
  36. package/scripts/lib/detect/stack.cjs +455 -0
  37. package/scripts/lib/detect/stack.d.cts +44 -0
  38. package/scripts/lib/explore-parallel-runner/index.ts +138 -1
  39. package/scripts/lib/explore-parallel-runner/types.ts +27 -0
  40. package/scripts/lib/health-mirror/index.cjs +218 -1
  41. package/scripts/lib/manifest/skills.json +8 -0
  42. package/scripts/lib/mapper-spawn.cjs +257 -0
  43. package/scripts/lib/mapper-spawn.d.cts +60 -0
  44. package/scripts/lib/new-addendum.cjs +204 -0
  45. package/sdk/cli/commands/dashboard.ts +419 -0
  46. package/sdk/cli/index.js +1388 -3
  47. package/sdk/cli/index.ts +7 -0
  48. package/sdk/dashboard/data/_pkg-root.cjs +92 -0
  49. package/sdk/dashboard/data/cost-aggregator.cjs +187 -0
  50. package/sdk/dashboard/data/discovery.cjs +297 -0
  51. package/sdk/dashboard/data/risk-surface.cjs +136 -0
  52. package/sdk/dashboard/data/source.cjs +576 -0
  53. package/sdk/dashboard/tui/ansi.cjs +355 -0
  54. package/sdk/dashboard/tui/index.cjs +778 -0
  55. package/sdk/mcp/gdd-mcp/server.js +1117 -0
  56. package/skills/new-addendum/SKILL.md +81 -0
@@ -0,0 +1,44 @@
1
+ // scripts/lib/detect/stack.d.cts — types for detect/stack.cjs (Phase 54 DETECT-01).
2
+
3
+ export interface StackEvidence {
4
+ ds?: string;
5
+ framework?: string;
6
+ motion?: string[];
7
+ note?: string;
8
+ }
9
+
10
+ export interface DetectedStack {
11
+ /** Detected design-system id (tailwind / shadcn / mui / ...), or null. */
12
+ ds: string | null;
13
+ /** Detected framework id (nextjs / remix / vite-react / ...), or null. */
14
+ framework: string | null;
15
+ /** Detected motion library ids (framer-motion / gsap / ...). Possibly empty. */
16
+ motion_libs: string[];
17
+ /** Per-category evidence strings (why each value was chosen). */
18
+ evidence: StackEvidence;
19
+ }
20
+
21
+ export interface ReadDepsResult {
22
+ deps: Record<string, string>;
23
+ present: boolean;
24
+ error: string | null;
25
+ }
26
+
27
+ /**
28
+ * Fingerprint a project's design-system / framework / motion stack. Pure,
29
+ * dependency-free, NEVER throws — an absent/malformed package.json yields
30
+ * all-null with an evidence note.
31
+ * @param root project directory (defaults to process.cwd()).
32
+ */
33
+ export function detectStack(root?: string): DetectedStack;
34
+
35
+ export function readDeps(root: string): ReadDepsResult;
36
+ export function hasDep(deps: Record<string, string>, name: string): boolean;
37
+ export function hasDepPrefix(deps: Record<string, string>, prefix: string): boolean;
38
+ export function detectDs(root: string, deps: Record<string, string>): { ds: string | null; evidence: string };
39
+ export function detectFramework(root: string, deps: Record<string, string>): { framework: string | null; evidence: string };
40
+ export function detectMotion(deps: Record<string, string>): { motion_libs: string[]; evidence: string[] };
41
+ export function main(argv: string[], io?: { cwd?: string; log?: (s: string) => void; err?: (s: string) => void }): number;
42
+ export function parseArgs(argv: string[]): { root: string | null; json: boolean; pretty: boolean; help: boolean };
43
+ export const HELP: string;
44
+ export const SKIP_DIRS: ReadonlySet<string> | string[];
@@ -30,6 +30,12 @@ import { resolveConcurrency } from '../parallelism-engine/concurrency-tuner.cjs'
30
30
  // way as concurrency-tuner.cjs above. Only invoked when opts.incremental.graph
31
31
  // is supplied — the default explore path never loads its ESM/TS dependencies.
32
32
  import { planIncremental } from '../mappers/incremental-discover.cjs';
33
+ // Phase 54 (REG-01): stack detection + addendum composition. Both CJS, imported
34
+ // the same way. The pre-spawn step (composeMapperSpecs below) calls detectStack
35
+ // ONCE and applyAddendums per spec; both are wrapped in try/catch so a failure
36
+ // degrades to the unmodified Phase-21 spec roster.
37
+ import { detectStack } from '../detect/stack.cjs';
38
+ import { applyAddendums } from '../mapper-spawn.cjs';
33
39
 
34
40
  import {
35
41
  isParallelismSafe,
@@ -105,6 +111,129 @@ export const DEFAULT_MAPPERS: readonly MapperSpec[] = Object.freeze([
105
111
  }),
106
112
  ]);
107
113
 
114
+ // ---------------------------------------------------------------------------
115
+ // Phase 54 (REG-01) — pre-spawn stack-addendum composition
116
+ // ---------------------------------------------------------------------------
117
+
118
+ /** Derive the agent name an addendum's composes_into list matches against. */
119
+ function agentNameOf(spec: MapperSpec): string {
120
+ // Addendum composes_into uses the AGENT filename (token-mapper,
121
+ // component-taxonomy-mapper, motion-mapper, visual-hierarchy-mapper), not the
122
+ // short MapperSpec.name (token, component-taxonomy, ...). Derive it from the
123
+ // agentPath basename so the registry match keys line up.
124
+ const base = spec.agentPath
125
+ .replace(/\\/g, '/')
126
+ .split('/')
127
+ .pop()
128
+ ?.replace(/\.md$/i, '');
129
+ return base && base.length > 0 ? base : spec.name;
130
+ }
131
+
132
+ /**
133
+ * Compose stack-specific guidance into each mapper spec BEFORE spawn.
134
+ *
135
+ * Detects the project stack ONCE (detect/stack.cjs#detectStack) and, for each
136
+ * spec, appends the matching `type:"stack-addendum"` reference bodies to the
137
+ * prompt (mapper-spawn.cjs#applyAddendums, cap 1 system + 1 framework + 1
138
+ * motion). The match is keyed by the spec's AGENT name against the registry
139
+ * `composes_into` list.
140
+ *
141
+ * Contract:
142
+ * * ADDITIVE + BACKWARD-COMPATIBLE: a spec with no matching addendum (or no
143
+ * detected stack) is returned with a byte-for-byte unchanged prompt. When
144
+ * NOTHING matches across all specs, the original `specs` array is returned
145
+ * unchanged (same reference), so the Phase-21 path is identical.
146
+ * * NEVER THROWS: detection / registry / file-read failures degrade to the
147
+ * unmodified roster. Dispatch is never blocked by this step.
148
+ * * The frozen DEFAULT_MAPPERS entries are never mutated — a spec that gains
149
+ * an addendum is returned as a fresh object.
150
+ *
151
+ * @returns `{ specs, missingByMapper }` — the (possibly) recomposed roster +
152
+ * a per-agent-name list of detected stack values that had NO addendum
153
+ * for that mapper (drives the R6 fallback flag / health coverage row).
154
+ */
155
+ function composeMapperSpecs(
156
+ specs: readonly MapperSpec[],
157
+ cwd: string,
158
+ addendumOpts: ExploreRunnerOptions['addendums'],
159
+ logger: ReturnType<typeof getLogger>,
160
+ ): { specs: readonly MapperSpec[]; missingByMapper: Record<string, string[]> } {
161
+ const missingByMapper: Record<string, string[]> = {};
162
+ const opt = addendumOpts ?? {};
163
+ if (opt.enabled === false) return { specs, missingByMapper };
164
+
165
+ try {
166
+ const root: string = typeof opt.root === 'string' ? opt.root : cwd;
167
+ const detect = typeof opt.detectStack === 'function' ? opt.detectStack : detectStack;
168
+ const stack = detect(root) as {
169
+ ds?: string | null;
170
+ framework?: string | null;
171
+ motion_libs?: string[];
172
+ } | null;
173
+
174
+ // Resolve the registry + refDir. Defaults read the shipped registry and the
175
+ // repo reference/ dir; tests inject both. A missing registry simply yields
176
+ // no matches (applyAddendums degrades to an empty block).
177
+ let registry: unknown = opt.registry;
178
+ const refDir: string =
179
+ typeof opt.refDir === 'string'
180
+ ? opt.refDir
181
+ : resolvePath(cwd, 'reference');
182
+ if (registry === undefined) {
183
+ try {
184
+ // Lazy require so the default path only touches disk when enabled.
185
+ const { loadRegistry } = require('../reference-registry.cjs') as {
186
+ loadRegistry: (o: { cwd?: string }) => unknown;
187
+ };
188
+ registry = loadRegistry({ cwd });
189
+ } catch {
190
+ registry = undefined; // no registry ⇒ no addendums (unchanged prompts)
191
+ }
192
+ }
193
+
194
+ let anyChanged = false;
195
+ const recomposed: MapperSpec[] = specs.map((spec) => {
196
+ const agentName = agentNameOf(spec);
197
+ // applyAddendums mutates a spec-shaped object's `.prompt` in place; we feed
198
+ // it a throwaway carrying the AGENT name so the registry match keys align,
199
+ // then copy the (possibly) augmented prompt back onto a fresh spec.
200
+ const carrier = { name: agentName, prompt: spec.prompt };
201
+ const { block, missing } = applyAddendums(carrier, stack, {
202
+ registry,
203
+ refDir,
204
+ }) as { block: string; used: string[]; missing: string[] };
205
+
206
+ if (Array.isArray(missing) && missing.length > 0) {
207
+ missingByMapper[agentName] = missing;
208
+ }
209
+ if (block && block.length > 0 && carrier.prompt !== spec.prompt) {
210
+ anyChanged = true;
211
+ return Object.freeze({ ...spec, prompt: carrier.prompt });
212
+ }
213
+ return spec;
214
+ });
215
+
216
+ if (anyChanged) {
217
+ logger.info('explore.runner.addendums_composed', {
218
+ mappers_augmented: recomposed.filter((s, i) => s !== specs[i]).length,
219
+ ds: stack && stack.ds ? stack.ds : null,
220
+ framework: stack && stack.framework ? stack.framework : null,
221
+ motion_libs:
222
+ stack && Array.isArray(stack.motion_libs) ? stack.motion_libs.length : 0,
223
+ });
224
+ return { specs: Object.freeze(recomposed), missingByMapper };
225
+ }
226
+ // Nothing matched — return the original roster reference unchanged.
227
+ return { specs, missingByMapper };
228
+ } catch (err) {
229
+ // The addendum step must NEVER break dispatch. Degrade to the unmodified
230
+ // roster + surface a warn for observability.
231
+ const message: string = err instanceof Error ? err.message : String(err);
232
+ logger.warn('explore.runner.addendums_failed', { message });
233
+ return { specs, missingByMapper };
234
+ }
235
+ }
236
+
108
237
  // ---------------------------------------------------------------------------
109
238
  // run — main orchestrator
110
239
  // ---------------------------------------------------------------------------
@@ -123,7 +252,7 @@ export const DEFAULT_MAPPERS: readonly MapperSpec[] = Object.freeze([
123
252
  export async function run(
124
253
  opts: ExploreRunnerOptions,
125
254
  ): Promise<ExploreRunnerResult> {
126
- const specs: readonly MapperSpec[] = opts.mappers ?? DEFAULT_MAPPERS;
255
+ const baseSpecs: readonly MapperSpec[] = opts.mappers ?? DEFAULT_MAPPERS;
127
256
  const cwd: string = opts.cwd ?? process.cwd();
128
257
  // Phase 27.6 D-07: data-driven concurrency default. Falls back to
129
258
  // min(cpu-1, 8) when no `parallelism.verdict` events exist in
@@ -132,6 +261,14 @@ export async function run(
132
261
 
133
262
  const logger = getLogger().child('explore.runner');
134
263
 
264
+ // --- Phase 54 (REG-01): compose stack addendums into mapper prompts -------
265
+ //
266
+ // Fingerprint the project ONCE and append the matching stack-addendum bodies
267
+ // to each mapper's prompt BEFORE partitioning / spawn. ADDITIVE +
268
+ // backward-compatible: no detected stack / no matching addendum ⇒ `specs` is
269
+ // the unchanged roster reference. NEVER throws (composeMapperSpecs guards).
270
+ const { specs } = composeMapperSpecs(baseSpecs, cwd, opts.addendums, logger);
271
+
135
272
  const outputPath: string = resolvePath(cwd, '.design/DESIGN-PATTERNS.md');
136
273
 
137
274
  // --- Phase 53 (DISC-01): incremental batching ----------------------------
@@ -138,6 +138,33 @@ export interface ExploreRunnerOptions {
138
138
  /** Forwarded into classify's projectStats.thresholds. */
139
139
  readonly thresholds?: unknown;
140
140
  };
141
+ /**
142
+ * Phase 54 (REG-01) — OPTIONAL composable stack addendums. Before spawning,
143
+ * the runner fingerprints the project (`scripts/lib/detect/stack.cjs#detectStack`)
144
+ * ONCE and appends the matching `type:"stack-addendum"` reference bodies to
145
+ * each mapper's prompt via `scripts/lib/mapper-spawn.cjs#applyAddendums`
146
+ * (cap 1 design-system + 1 framework + 1 motion per spawn). The addendum is
147
+ * selected by the mapper's AGENT name (e.g. token-mapper) against the
148
+ * registry `composes_into` list.
149
+ *
150
+ * BACKWARD-COMPATIBLE + ADDITIVE: defaults ON, but a project with no
151
+ * detected stack (or no matching addendum) gets a byte-for-byte unchanged
152
+ * prompt. The whole step is wrapped in try/catch so a detection/registry
153
+ * failure NEVER aborts dispatch. Set `enabled: false` to opt out entirely.
154
+ * The `detectStack` / `registry` / `refDir` fields are test-injection seams.
155
+ */
156
+ readonly addendums?: {
157
+ /** Opt out of stack-addendum composition entirely. Default: enabled. */
158
+ readonly enabled?: boolean;
159
+ /** Detection root. Defaults to `cwd`. */
160
+ readonly root?: string;
161
+ /** Injected detectStack (tests). Defaults to detect/stack.cjs#detectStack. */
162
+ readonly detectStack?: (root: string) => unknown;
163
+ /** Injected registry object (tests). Defaults to reference/registry.json. */
164
+ readonly registry?: unknown;
165
+ /** Reference dir addendum paths resolve against. Defaults to repo reference/. */
166
+ readonly refDir?: string;
167
+ };
141
168
  }
142
169
 
143
170
  /**
@@ -8,7 +8,7 @@
8
8
  // Surface:
9
9
  // async getHealthChecks(rootDir) → { checks: HealthCheck[] }
10
10
  //
11
- // The 7 checks (in stable order) are:
11
+ // The 10 checks (in stable order) are:
12
12
  // 1. claude_md — CLAUDE.md presence
13
13
  // 2. planning_dir — .planning/ presence
14
14
  // 3. design_dir — .design/ presence
@@ -16,6 +16,9 @@
16
16
  // 5. issue_reporter — kill-switch state (Plan 30-06 / D-08)
17
17
  // 6. figma_extract — extract readiness + Free-tier signal (Plan 31-09)
18
18
  // 7. skill_discipline — using-gdd bootstrap + SessionStart inject (Plan 32-07)
19
+ // 8. harness_freshness — per-harness last_verified age (Phase 44)
20
+ // 9. stack_addendums — Phase 54 coverage: N/M detected stacks have addendums
21
+ // 10. dashboard_reachable — Phase 55: bin/gdd-dashboard on disk + data plane loads
19
22
  //
20
23
  // Check 5 was added in Plan 30-06 — surfaces the report-issue kill-switch
21
24
  // (env or config disable) so users can verify why the command is
@@ -46,6 +49,22 @@
46
49
  // inject-using-gdd entry)
47
50
  // status: 'ok' when ready, 'warn' otherwise. PURE read-only (rootDir-relative
48
51
  // file + JSON inspection only) — NEVER throws, NEVER networks.
52
+ //
53
+ // Check 10 was added in Phase 55 — surfaces whether the GDD dashboard is
54
+ // reachable so a user running /gdd:health knows the `gdd dashboard` entrypoint
55
+ // is wired. GRACEFUL-ABSENT by design (D-8 risk surfacing precedent): the
56
+ // dashboard is an opt-in, read-only surface that also works via file-scrape, so
57
+ // a missing bin or absent data plane is a 'warn' (actionable note), NEVER a
58
+ // hard 'fail'. The status is 'ok' when BOTH the bin/gdd-dashboard trampoline
59
+ // resolves on disk (located via a package-root walk-up — the Phase 53/54 lesson,
60
+ // NEVER a fixed __dirname jump) AND the dashboard data plane module
61
+ // (sdk/dashboard/data/source.cjs) loads + exposes loadDashboardModel. The detail
62
+ // line is one of:
63
+ // - "dashboard: bin/gdd-dashboard present; data plane ok"
64
+ // - "dashboard: bin missing" (trampoline not on disk)
65
+ // - "dashboard: data plane unavailable" (bin present, source.cjs absent)
66
+ // - "dashboard: bin missing; data plane unavailable"
67
+ // PURE read-only (fs.statSync + a wrapped require) — NEVER throws, NEVER networks.
49
68
 
50
69
  const fs = require('node:fs');
51
70
  const path = require('node:path');
@@ -238,6 +257,109 @@ async function getHealthChecks(rootDir) {
238
257
  checks.push({ name: 'harness_freshness', status, detail });
239
258
  }
240
259
 
260
+ // 9. stack_addendums — Phase 54 coverage row. Fingerprints the project
261
+ // (detect/stack.cjs) and reports how many DETECTED stacks (design-system +
262
+ // framework + motion libs) have a registered type:"stack-addendum" entry in
263
+ // reference/registry.json. PURE read-only (detection reads files but never
264
+ // networks; registry is a local JSON read). GRACEFUL-ABSENT: no detected
265
+ // stack -> "no stacks detected"; an unreadable registry -> "registry
266
+ // unavailable". status is 'ok' on full coverage or nothing-to-cover,
267
+ // otherwise 'warn'. NEVER throws.
268
+ {
269
+ let status = 'ok';
270
+ let detail;
271
+ try {
272
+ const { detectStack } = require('../detect/stack.cjs');
273
+ const stack = detectStack(rootDir) || { ds: null, framework: null, motion_libs: [] };
274
+
275
+ // The detected stack values to cover, paired with the category an
276
+ // addendum must classify into to count as coverage.
277
+ const wanted = [];
278
+ if (typeof stack.ds === 'string' && stack.ds) wanted.push({ category: 'system', key: stack.ds });
279
+ if (typeof stack.framework === 'string' && stack.framework) {
280
+ wanted.push({ category: 'framework', key: stack.framework });
281
+ }
282
+ if (Array.isArray(stack.motion_libs)) {
283
+ for (const m of stack.motion_libs) {
284
+ if (typeof m === 'string' && m) wanted.push({ category: 'motion', key: m });
285
+ }
286
+ }
287
+
288
+ if (wanted.length === 0) {
289
+ detail = 'stack addendums: no stacks detected';
290
+ status = 'ok';
291
+ } else {
292
+ // Load the registry + classify its stack-addendum entries. A missing or
293
+ // malformed registry counts as zero coverage (warn), never a throw.
294
+ let entries = [];
295
+ try {
296
+ const reg = JSON.parse(
297
+ fs.readFileSync(path.join(rootDir, 'reference', 'registry.json'), 'utf8')
298
+ );
299
+ entries = Array.isArray(reg.entries) ? reg.entries : [];
300
+ } catch {
301
+ entries = null; // distinguish "registry unavailable" from "zero coverage"
302
+ }
303
+
304
+ if (entries === null) {
305
+ detail = 'stack addendums: registry unavailable';
306
+ status = 'warn';
307
+ } else {
308
+ const { classifyEntry } = require('../mapper-spawn.cjs');
309
+ // Build the set of {category|key} an addendum exists for.
310
+ const covered = new Set();
311
+ for (const e of entries) {
312
+ if (!e || e.type !== 'stack-addendum') continue;
313
+ const { category, key } = classifyEntry(e);
314
+ if (category && key) covered.add(category + '|' + key);
315
+ }
316
+ const have = wanted.filter((w) => covered.has(w.category + '|' + String(w.key).toLowerCase())).length;
317
+ const total = wanted.length;
318
+ status = have >= total ? 'ok' : 'warn';
319
+ detail = `stack addendums: ${have}/${total} detected stacks have addendums`;
320
+ }
321
+ }
322
+ } catch {
323
+ // Absolute safety net — the health probe must never crash on this check.
324
+ status = 'warn';
325
+ detail = 'stack addendums: unavailable';
326
+ }
327
+ checks.push({ name: 'stack_addendums', status, detail });
328
+ }
329
+
330
+ // 10. dashboard_reachable — Phase 55. GRACEFUL-ABSENT: reports whether the
331
+ // GDD dashboard entrypoint is wired (bin/gdd-dashboard on disk) AND its data
332
+ // plane module loads. NEVER 'fail' — a missing bin is a 'warn' note because
333
+ // the dashboard is opt-in and also works via file-scrape. PURE read-only
334
+ // (fs.statSync + a wrapped require); NEVER throws, NEVER networks.
335
+ {
336
+ let status;
337
+ let detail;
338
+ try {
339
+ const gddRoot = resolveDashboardRoot(rootDir);
340
+ const binPresent = dashboardBinResolves(gddRoot);
341
+ const dataPlaneOk = dashboardDataPlaneLoads(gddRoot);
342
+ if (binPresent && dataPlaneOk) {
343
+ status = 'ok';
344
+ detail = 'dashboard: bin/gdd-dashboard present; data plane ok';
345
+ } else {
346
+ status = 'warn';
347
+ if (!binPresent && !dataPlaneOk) {
348
+ detail = 'dashboard: bin missing; data plane unavailable';
349
+ } else if (!binPresent) {
350
+ detail = 'dashboard: bin missing';
351
+ } else {
352
+ detail = 'dashboard: data plane unavailable';
353
+ }
354
+ }
355
+ } catch {
356
+ // Absolute safety net — the health probe must never crash on this check.
357
+ status = 'warn';
358
+ detail = 'dashboard: unavailable';
359
+ }
360
+ checks.push({ name: 'dashboard_reachable', status, detail });
361
+ }
362
+
241
363
  return { checks };
242
364
  }
243
365
 
@@ -330,4 +452,99 @@ function figmaVariablesBlockedLocally(rootDir) {
330
452
  }
331
453
  }
332
454
 
455
+ /**
456
+ * Walk UP from `startDir` to the GDD package root (the first ancestor whose
457
+ * package.json `name` is the GDD package). This mirrors the Phase 53/54 lesson
458
+ * (sdk/dashboard/data/_pkg-root.cjs): NEVER resolve a cross-tree sibling via a
459
+ * fixed __dirname-relative jump. The shipped package name is scoped
460
+ * ("@hegemonart/get-design-done"); dev/self-host/fixture roots may use the bare
461
+ * "get-design-done" — both match. Bounded climb; defensive. Returns null if no
462
+ * GDD root marker is found.
463
+ *
464
+ * @param {string} startDir
465
+ * @returns {string|null} absolute package-root dir, or null
466
+ */
467
+ function findGddPackageRoot(startDir) {
468
+ try {
469
+ let dir = path.resolve(startDir);
470
+ for (let i = 0; i < 12; i++) {
471
+ try {
472
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
473
+ if (pkg && typeof pkg.name === 'string') {
474
+ if (pkg.name === 'get-design-done' || /\/get-design-done$/.test(pkg.name)) {
475
+ return dir;
476
+ }
477
+ }
478
+ } catch {
479
+ // no/garbage package.json at this level — keep climbing
480
+ }
481
+ const parent = path.dirname(dir);
482
+ if (parent === dir) break;
483
+ dir = parent;
484
+ }
485
+ return null;
486
+ } catch {
487
+ return null;
488
+ }
489
+ }
490
+
491
+ /**
492
+ * Resolve the AUTHORITATIVE GDD package root for the dashboard probe, given the
493
+ * project root being health-checked. Resolution (D-7 walk-up, never a fixed
494
+ * __dirname jump):
495
+ * - If `rootDir` itself sits inside a GDD checkout (dev / self-host / a
496
+ * hermetic fixture that declares the GDD name), THAT root is authoritative —
497
+ * its own bin/ + sdk/dashboard/ are the truth. No cross-root fallback (so a
498
+ * fixture is hermetic + deterministic regardless of the shipped tree).
499
+ * - Otherwise `rootDir` is an unrelated CONSUMER project (no GDD marker); the
500
+ * dashboard ships alongside THIS module, so walk up from __dirname.
501
+ * Returns null only if neither resolves (degrades the check to 'warn').
502
+ *
503
+ * @param {string} rootDir project root passed to getHealthChecks
504
+ * @returns {string|null}
505
+ */
506
+ function resolveDashboardRoot(rootDir) {
507
+ const fromRoot = findGddPackageRoot(rootDir);
508
+ if (fromRoot) return fromRoot;
509
+ return findGddPackageRoot(__dirname);
510
+ }
511
+
512
+ /**
513
+ * Does the bin/gdd-dashboard trampoline resolve on disk under the authoritative
514
+ * GDD root? (Phase 55, check 10.) `fs.statSync` follows symlinks, so an npm
515
+ * bin-linked trampoline (a symlink resolving to a file) counts as present. PURE
516
+ * read-only; NEVER throws.
517
+ *
518
+ * @param {string} gddRoot authoritative GDD root (or null)
519
+ * @returns {boolean} true iff bin/gdd-dashboard is present on disk
520
+ */
521
+ function dashboardBinResolves(gddRoot) {
522
+ if (!gddRoot) return false;
523
+ try {
524
+ return fs.statSync(path.join(gddRoot, 'bin', 'gdd-dashboard')).isFile();
525
+ } catch {
526
+ return false;
527
+ }
528
+ }
529
+
530
+ /**
531
+ * Does the dashboard data plane module load + expose loadDashboardModel under
532
+ * the authoritative GDD root? (Phase 55, check 10.) The data plane is
533
+ * sdk/dashboard/data/source.cjs (R1 — the in-process shared-lib read surface the
534
+ * dashboard renders). A missing module or a require error degrades to false
535
+ * (→ 'warn'), NEVER throws.
536
+ *
537
+ * @param {string} gddRoot authoritative GDD root (or null)
538
+ * @returns {boolean} true iff source.cjs loads and exports loadDashboardModel
539
+ */
540
+ function dashboardDataPlaneLoads(gddRoot) {
541
+ if (!gddRoot) return false;
542
+ try {
543
+ const mod = require(path.join(gddRoot, 'sdk', 'dashboard', 'data', 'source.cjs'));
544
+ return !!(mod && typeof mod.loadDashboardModel === 'function');
545
+ } catch {
546
+ return false;
547
+ }
548
+ }
549
+
333
550
  module.exports = { getHealthChecks };
@@ -260,6 +260,14 @@
260
260
  "user_invocable": true,
261
261
  "registered_in_phase": "52"
262
262
  },
263
+ {
264
+ "name": "new-addendum",
265
+ "description": "Scaffolds a new Phase-54 composable reference addendum for a design-system, framework, or motion library: validates the kind and the slug, defaults composes_into by kind, and writes a 4-section skeleton at reference/{systems|frameworks|motion}/<name>.md from the pure generator. Use when adding stack-specific guidance that an explore mapper should compose at spawn time and you want the frontmatter, the composes_into wiring, and the mandatory sections correct from the first commit. Activates for requests involving authoring a reference addendum, adding stack-specific mapper guidance, scaffolding a systems or frameworks or motion doc, or registering a new design-system.",
266
+ "argument_hint": "<kind> <name>",
267
+ "tools": "Read, Write, Bash, AskUserQuestion",
268
+ "user_invocable": true,
269
+ "registered_in_phase": "54"
270
+ },
263
271
  {
264
272
  "name": "new-cycle",
265
273
  "description": "Start a new design cycle. Creates cycle scope in STATE.md, initializes .design/CYCLES.md entry. Each cycle has its own goal and tracks its own decisions/tasks/pipeline runs.",