@hegemonart/get-design-done 1.31.0 → 1.32.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 (180) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +75 -0
  4. package/NOTICE +262 -0
  5. package/README.md +13 -1
  6. package/SKILL.md +4 -0
  7. package/agents/design-authority-watcher.md +1 -1
  8. package/agents/perf-analyzer.md +2 -2
  9. package/bin/gdd-mcp +78 -0
  10. package/bin/gdd-sdk +34 -24
  11. package/bin/gdd-state-mcp +78 -0
  12. package/{README.de.md → docs/i18n/README.de.md} +1 -1
  13. package/{README.fr.md → docs/i18n/README.fr.md} +1 -1
  14. package/{README.it.md → docs/i18n/README.it.md} +1 -1
  15. package/{README.ja.md → docs/i18n/README.ja.md} +1 -1
  16. package/{README.ko.md → docs/i18n/README.ko.md} +1 -1
  17. package/{README.zh-CN.md → docs/i18n/README.zh-CN.md} +1 -1
  18. package/hooks/_hook-emit.js +1 -1
  19. package/hooks/budget-enforcer.ts +5 -5
  20. package/hooks/context-exhaustion.ts +2 -2
  21. package/hooks/gdd-precompact-snapshot.js +3 -3
  22. package/hooks/gdd-read-injection-scanner.ts +2 -2
  23. package/hooks/gdd-sessionstart-recap.js +1 -1
  24. package/hooks/gdd-turn-closeout.js +1 -1
  25. package/hooks/hooks.json +9 -0
  26. package/hooks/inject-using-gdd.sh +72 -0
  27. package/hooks/run-hook.cmd +35 -0
  28. package/package.json +20 -9
  29. package/recipes/.gitkeep +0 -0
  30. package/reference/schemas/events.schema.json +63 -1
  31. package/reference/schemas/recipe.schema.json +33 -0
  32. package/scripts/cli/gdd-events.mjs +5 -5
  33. package/scripts/lib/cache/gdd-cache-manager.cjs +1 -1
  34. package/scripts/lib/cli/index.ts +22 -160
  35. package/scripts/lib/connection-probe/index.cjs +1 -1
  36. package/scripts/lib/discuss-parallel-runner/aggregator.ts +1 -1
  37. package/scripts/lib/discuss-parallel-runner/index.ts +1 -1
  38. package/scripts/lib/error-classifier.cjs +24 -227
  39. package/scripts/lib/event-stream/index.ts +25 -193
  40. package/scripts/lib/gdd-errors/index.ts +24 -213
  41. package/scripts/lib/gdd-state/index.ts +23 -161
  42. package/scripts/lib/health-mirror/index.cjs +79 -1
  43. package/scripts/lib/iteration-budget.cjs +23 -199
  44. package/scripts/lib/jittered-backoff.cjs +24 -107
  45. package/scripts/lib/lockfile.cjs +23 -195
  46. package/scripts/lib/logger/index.ts +1 -1
  47. package/scripts/lib/parallelism-engine/concurrency-tuner.cjs +1 -1
  48. package/scripts/lib/perf-analyzer/index.cjs +1 -1
  49. package/scripts/lib/pipeline-runner/index.ts +4 -4
  50. package/scripts/lib/pipeline-runner/state-machine.ts +1 -1
  51. package/scripts/lib/prompt-dedup/index.cjs +1 -1
  52. package/scripts/lib/rate-guard.cjs +2 -2
  53. package/scripts/lib/recipe-loader.cjs +142 -0
  54. package/scripts/lib/session-runner/errors.ts +3 -3
  55. package/scripts/lib/session-runner/index.ts +3 -3
  56. package/scripts/lib/session-runner/transcript.ts +1 -1
  57. package/scripts/lib/tool-scoping/index.ts +1 -1
  58. package/scripts/mcp-servers/gdd-mcp/server.ts +29 -311
  59. package/scripts/mcp-servers/gdd-state/server.ts +28 -282
  60. package/sdk/README.md +45 -0
  61. package/{scripts/lib → sdk}/cli/commands/audit.ts +3 -3
  62. package/{scripts/lib → sdk}/cli/commands/init.ts +3 -3
  63. package/{scripts/lib → sdk}/cli/commands/query.ts +4 -4
  64. package/{scripts/lib → sdk}/cli/commands/run.ts +5 -5
  65. package/{scripts/lib → sdk}/cli/commands/stage.ts +5 -5
  66. package/sdk/cli/index.js +8091 -0
  67. package/sdk/cli/index.ts +172 -0
  68. package/{scripts/lib → sdk}/cli/parse-args.ts +2 -2
  69. package/{scripts/lib/gdd-errors → sdk/errors}/classification.ts +1 -1
  70. package/sdk/errors/index.ts +218 -0
  71. package/{scripts/lib → sdk}/event-stream/emitter.ts +1 -1
  72. package/sdk/event-stream/index.ts +197 -0
  73. package/{scripts/lib → sdk}/event-stream/reader.ts +1 -1
  74. package/{scripts/lib → sdk}/event-stream/types.ts +2 -2
  75. package/{scripts/lib → sdk}/event-stream/writer.ts +1 -1
  76. package/sdk/index.ts +19 -0
  77. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/README.md +3 -3
  78. package/sdk/mcp/gdd-mcp/server.js +1966 -0
  79. package/sdk/mcp/gdd-mcp/server.ts +325 -0
  80. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_cycle_recap.ts +3 -3
  81. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_decisions_list.ts +2 -2
  82. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_events_tail.ts +3 -3
  83. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_health.ts +2 -2
  84. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_intel_get.ts +2 -2
  85. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_learnings_digest.ts +2 -2
  86. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_phase_current.ts +2 -2
  87. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_phases_list.ts +2 -2
  88. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_plans_list.ts +2 -2
  89. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_reflections_latest.ts +2 -2
  90. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_status.ts +3 -3
  91. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_telemetry_query.ts +3 -3
  92. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/index.ts +2 -2
  93. package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/shared.ts +3 -3
  94. package/sdk/mcp/gdd-state/server.js +2790 -0
  95. package/sdk/mcp/gdd-state/server.ts +294 -0
  96. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/add_blocker.ts +3 -3
  97. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/add_decision.ts +3 -3
  98. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/add_must_have.ts +3 -3
  99. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/checkpoint.ts +2 -2
  100. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/frontmatter_update.ts +2 -2
  101. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/get.ts +3 -3
  102. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/index.ts +1 -1
  103. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/probe_connections.ts +3 -3
  104. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/resolve_blocker.ts +3 -3
  105. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/set_status.ts +2 -2
  106. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/shared.ts +8 -8
  107. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/transition_stage.ts +4 -4
  108. package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/update_progress.ts +2 -2
  109. package/sdk/primitives/error-classifier.cjs +232 -0
  110. package/sdk/primitives/iteration-budget.cjs +205 -0
  111. package/sdk/primitives/jittered-backoff.cjs +112 -0
  112. package/sdk/primitives/lockfile.cjs +201 -0
  113. package/{scripts/lib/gdd-state → sdk/state}/gates.ts +1 -1
  114. package/sdk/state/index.ts +167 -0
  115. package/{scripts/lib/gdd-state → sdk/state}/lockfile.ts +1 -1
  116. package/{scripts/lib/gdd-state → sdk/state}/mutator.ts +1 -1
  117. package/{scripts/lib/gdd-state → sdk/state}/parser.ts +1 -1
  118. package/{scripts/lib/gdd-state → sdk/state}/types.ts +4 -4
  119. package/skills/audit/SKILL.md +13 -0
  120. package/skills/brief/SKILL.md +25 -0
  121. package/skills/design/SKILL.md +17 -0
  122. package/skills/discuss/SKILL.md +13 -0
  123. package/skills/explore/SKILL.md +17 -0
  124. package/skills/health/SKILL.md +6 -0
  125. package/skills/plan/SKILL.md +25 -0
  126. package/skills/quality-gate/SKILL.md +2 -2
  127. package/skills/router/SKILL.md +4 -0
  128. package/skills/router/router-pick-emitter.md +78 -0
  129. package/skills/using-gdd/SKILL.md +78 -0
  130. package/skills/verify/SKILL.md +17 -0
  131. package/scripts/aggregate-agent-metrics.ts +0 -282
  132. package/scripts/bootstrap-manifest.txt +0 -3
  133. package/scripts/bootstrap.sh +0 -80
  134. package/scripts/build-distribution-bundles.cjs +0 -549
  135. package/scripts/build-intel.cjs +0 -486
  136. package/scripts/codegen-schema-types.ts +0 -149
  137. package/scripts/detect-stale-refs.cjs +0 -107
  138. package/scripts/e2e/run-headless.ts +0 -514
  139. package/scripts/extract-changelog-section.cjs +0 -58
  140. package/scripts/gsd-cleanup-incubator.cjs +0 -367
  141. package/scripts/injection-patterns.cjs +0 -58
  142. package/scripts/lint-agentskills-spec.cjs +0 -457
  143. package/scripts/release-smoke-test.cjs +0 -200
  144. package/scripts/rollback-release.sh +0 -42
  145. package/scripts/run-injection-scanner-ci.cjs +0 -83
  146. package/scripts/tests/test-authority-rejected-kinds.sh +0 -58
  147. package/scripts/tests/test-authority-watcher-diff.sh +0 -113
  148. package/scripts/tests/test-motion-provenance.sh +0 -64
  149. package/scripts/validate-frontmatter.ts +0 -409
  150. package/scripts/validate-incubator-scope.cjs +0 -133
  151. package/scripts/validate-schemas.ts +0 -401
  152. package/scripts/validate-skill-length.cjs +0 -283
  153. package/scripts/verify-version-sync.cjs +0 -30
  154. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_cycle_recap.schema.json +0 -0
  155. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_decisions_list.schema.json +0 -0
  156. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_events_tail.schema.json +0 -0
  157. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_health.schema.json +0 -0
  158. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_intel_get.schema.json +0 -0
  159. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_learnings_digest.schema.json +0 -0
  160. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_phase_current.schema.json +0 -0
  161. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_phases_list.schema.json +0 -0
  162. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_plans_list.schema.json +0 -0
  163. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_reflections_latest.schema.json +0 -0
  164. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_status.schema.json +0 -0
  165. /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_telemetry_query.schema.json +0 -0
  166. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/add_blocker.schema.json +0 -0
  167. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/add_decision.schema.json +0 -0
  168. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/add_must_have.schema.json +0 -0
  169. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/checkpoint.schema.json +0 -0
  170. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/frontmatter_update.schema.json +0 -0
  171. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/get.schema.json +0 -0
  172. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/probe_connections.schema.json +0 -0
  173. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/resolve_blocker.schema.json +0 -0
  174. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/set_status.schema.json +0 -0
  175. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/transition_stage.schema.json +0 -0
  176. /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/update_progress.schema.json +0 -0
  177. /package/{scripts/lib → sdk/primitives}/error-classifier.d.cts +0 -0
  178. /package/{scripts/lib → sdk/primitives}/iteration-budget.d.cts +0 -0
  179. /package/{scripts/lib → sdk/primitives}/jittered-backoff.d.cts +0 -0
  180. /package/{scripts/lib → sdk/primitives}/lockfile.d.cts +0 -0
@@ -0,0 +1,201 @@
1
+ // scripts/lib/lockfile.cjs
2
+ //
3
+ // Plan 20-14 — PID+timestamp sibling lockfile for `.cjs` consumers.
4
+ //
5
+ // Algorithm mirrors sdk/state/lockfile.ts (Plan 20-01):
6
+ // Lock path: `${target}.lock`
7
+ // Payload: { pid: number, host: string, acquired_at: ISO8601 }
8
+ // Acquire: atomic `writeFileSync(..., { flag: 'wx' })`
9
+ // Stale rule: pid dead (ESRCH via `kill(pid, 0)`) OR `acquired_at` older
10
+ // than `staleMs` OR unparseable payload
11
+ // Release: unlink; ENOENT is not an error; idempotent
12
+ //
13
+ // Windows: AV scanners and file-indexers can hold a file briefly after
14
+ // close. `wx` create may fail with EPERM/EBUSY even when the target is
15
+ // free; we treat these as transient and loop (same code path as EEXIST).
16
+ //
17
+ // Dependency-cycle note: Plan 20-14's rate-guard + iteration-budget
18
+ // consume this module, and both are required to stay dependency-light so
19
+ // that hooks/budget-enforcer.ts can import them without dragging the
20
+ // gdd-state MCP graph along. Hence this standalone .cjs port instead of
21
+ // calling the .ts version.
22
+
23
+ 'use strict';
24
+
25
+ const fs = require('node:fs');
26
+ const os = require('node:os');
27
+
28
+ const DEFAULT_STALE_MS = 60_000;
29
+ const DEFAULT_MAX_WAIT_MS = 5_000;
30
+ const DEFAULT_POLL_MS = 50;
31
+
32
+ /**
33
+ * Acquire an advisory lock at `${path}.lock`. Returns an idempotent
34
+ * async release function.
35
+ *
36
+ * @param {string} path path being locked (we append `.lock`)
37
+ * @param {object} [opts]
38
+ * @param {number} [opts.staleMs] ms after which an existing lock is stale. Default 60_000.
39
+ * @param {number} [opts.maxWaitMs] total ms to wait before throwing. Default 5_000.
40
+ * @param {number} [opts.pollMs] ms between retry attempts. Default 50.
41
+ * @returns {Promise<() => Promise<void>>} release function
42
+ * @throws {Error} with name === 'LockAcquisitionError' when maxWaitMs elapses
43
+ */
44
+ async function acquire(path, opts) {
45
+ const o = opts || {};
46
+ const staleMs = Number.isFinite(o.staleMs) ? o.staleMs : DEFAULT_STALE_MS;
47
+ const maxWaitMs = Number.isFinite(o.maxWaitMs) ? o.maxWaitMs : DEFAULT_MAX_WAIT_MS;
48
+ const pollMs = Number.isFinite(o.pollMs) ? o.pollMs : DEFAULT_POLL_MS;
49
+
50
+ if (staleMs < 0 || maxWaitMs < 0 || pollMs < 0) {
51
+ throw new Error(
52
+ `lockfile.acquire: invalid options (staleMs=${staleMs}, maxWaitMs=${maxWaitMs}, pollMs=${pollMs})`,
53
+ );
54
+ }
55
+
56
+ const lockPath = `${path}.lock`;
57
+ const payload = JSON.stringify({
58
+ pid: process.pid,
59
+ host: os.hostname(),
60
+ acquired_at: new Date().toISOString(),
61
+ });
62
+ const startedAt = Date.now();
63
+
64
+ while (true) {
65
+ try {
66
+ fs.writeFileSync(lockPath, payload, { flag: 'wx', encoding: 'utf8' });
67
+ return makeRelease(lockPath);
68
+ } catch (err) {
69
+ const code = err && typeof err === 'object' ? err.code : undefined;
70
+ if (code !== 'EEXIST' && code !== 'EPERM' && code !== 'EBUSY') {
71
+ throw err;
72
+ }
73
+ // Try to read the current holder; if it vanished between EEXIST and
74
+ // read, loop immediately.
75
+ const existing = readLockSafe(lockPath);
76
+ if (existing === null) continue;
77
+
78
+ const parsed = parseLock(existing);
79
+ // Only clear when we're confident the lock is stale: the payload
80
+ // parses AND the PID/age check says so. An unparseable payload is
81
+ // treated as fresh — on Windows, AV/indexer can transiently deny
82
+ // reads (EACCES/EPERM/EBUSY), and clearing under that condition
83
+ // would let two writers race and lose increments.
84
+ if (parsed !== null && isStale(parsed, staleMs)) {
85
+ // Clear stale lock; race-tolerant — if it's already gone we get
86
+ // ENOENT, no-op.
87
+ try { fs.unlinkSync(lockPath); } catch { /* ignore */ }
88
+ continue;
89
+ }
90
+
91
+ if (Date.now() - startedAt >= maxWaitMs) {
92
+ const e = new Error(
93
+ `lockfile: failed to acquire ${lockPath} within ${maxWaitMs}ms (held by ${existing})`,
94
+ );
95
+ e.name = 'LockAcquisitionError';
96
+ e.lockPath = lockPath;
97
+ e.holder = existing;
98
+ e.waitedMs = Date.now() - startedAt;
99
+ throw e;
100
+ }
101
+ await sleep(pollMs);
102
+ }
103
+ }
104
+ }
105
+
106
+ function makeRelease(lockPath) {
107
+ let released = false;
108
+ return async function release() {
109
+ if (released) return;
110
+ released = true;
111
+ try {
112
+ fs.unlinkSync(lockPath);
113
+ } catch (err) {
114
+ const code = err && typeof err === 'object' ? err.code : undefined;
115
+ if (code === 'ENOENT') return; // idempotent — already gone
116
+ if (code === 'EPERM' || code === 'EBUSY') {
117
+ // Windows AV/indexer: retry once.
118
+ await sleep(50);
119
+ try {
120
+ if (fs.existsSync(lockPath)) fs.unlinkSync(lockPath);
121
+ } catch { /* give up; stale-detection will reclaim */ }
122
+ return;
123
+ }
124
+ // Any other errno: swallow. Best-effort cleanup; stale-age check
125
+ // will eventually reclaim the lock.
126
+ }
127
+ };
128
+ }
129
+
130
+ function readLockSafe(p) {
131
+ try {
132
+ return fs.readFileSync(p, 'utf8');
133
+ } catch (err) {
134
+ const code = err && typeof err === 'object' ? err.code : undefined;
135
+ if (code === 'ENOENT') return null;
136
+ return '<unreadable>';
137
+ }
138
+ }
139
+
140
+ function parseLock(raw) {
141
+ try {
142
+ const obj = JSON.parse(raw);
143
+ if (
144
+ obj && typeof obj === 'object' &&
145
+ typeof obj.pid === 'number' &&
146
+ typeof obj.host === 'string' &&
147
+ typeof obj.acquired_at === 'string'
148
+ ) {
149
+ return obj;
150
+ }
151
+ return null;
152
+ } catch {
153
+ return null;
154
+ }
155
+ }
156
+
157
+ function isStale(payload, staleMs) {
158
+ if (!isPidAlive(payload.pid, payload.host)) return true;
159
+ const t = Date.parse(payload.acquired_at);
160
+ if (!Number.isFinite(t)) return true;
161
+ return Date.now() - t > staleMs;
162
+ }
163
+
164
+ function isPidAlive(pid, host) {
165
+ if (host !== os.hostname()) return true; // can't introspect other hosts
166
+ if (pid === process.pid) return true;
167
+ try {
168
+ process.kill(pid, 0); // signal 0 = validate, don't deliver
169
+ return true;
170
+ } catch (err) {
171
+ const code = err && typeof err === 'object' ? err.code : undefined;
172
+ if (code === 'ESRCH') return false;
173
+ // EPERM / EACCES: process exists but is unsignalable; treat as alive.
174
+ return true;
175
+ }
176
+ }
177
+
178
+ function sleep(ms) {
179
+ return new Promise((resolve) => setTimeout(resolve, ms));
180
+ }
181
+
182
+ /**
183
+ * `fs.renameSync` wrapper that retries once on Windows EPERM/EBUSY/EACCES.
184
+ * AV scanners and the file-indexer can briefly hold a destination open
185
+ * after another process closed it, causing rename to fail even when the
186
+ * advisory lock is correctly held.
187
+ *
188
+ * Mirrors the inline retry in sdk/state/index.ts mutate().
189
+ */
190
+ async function renameWithRetry(from, to) {
191
+ try {
192
+ fs.renameSync(from, to);
193
+ } catch (err) {
194
+ const code = err && typeof err === 'object' ? err.code : undefined;
195
+ if (code !== 'EPERM' && code !== 'EBUSY' && code !== 'EACCES') throw err;
196
+ await sleep(50);
197
+ fs.renameSync(from, to);
198
+ }
199
+ }
200
+
201
+ module.exports = { acquire, renameWithRetry };
@@ -1,4 +1,4 @@
1
- // scripts/lib/gdd-state/gates.ts — pure transition-gate functions.
1
+ // sdk/state/gates.ts — pure transition-gate functions.
2
2
  //
3
3
  // Plan 20-02 (SDK-03): the typed, single-source-of-truth implementation
4
4
  // of "can this pipeline advance?" that replaces prose-encoded guards in
@@ -0,0 +1,167 @@
1
+ // sdk/state/index.ts — public API for the gdd-state module.
2
+ //
3
+ // This is the ONLY file consumers should import from. The module exposes
4
+ // exactly five surface-level names:
5
+ // * read(path) — parse STATE.md from disk
6
+ // * mutate(path, fn) — atomic read-modify-write under a lock
7
+ // * transition(path, toStage) — gate + stage-advance helper
8
+ // * ParsedState (type) — consumer-visible shape
9
+ // * Stage (type) — stage enum
10
+ //
11
+ // Plan 20-02 wired the real transition gates in via `gateFor(from, to)`
12
+ // imported from `./gates.ts`. Plan 20-04 migrated the error classes
13
+ // (TransitionGateFailed, LockAcquisitionError, ParseError) to the
14
+ // unified `gdd-errors` taxonomy — `types.ts` re-exports them verbatim
15
+ // so consumers of `gdd-state` need no changes.
16
+
17
+ import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync } from 'node:fs';
18
+
19
+ import { acquire } from './lockfile.ts';
20
+ import { parse } from './parser.ts';
21
+ import { serialize } from './mutator.ts';
22
+ import { gateFor } from './gates.ts';
23
+ import {
24
+ TransitionGateFailed,
25
+ isStage,
26
+ type ParsedState,
27
+ type Stage,
28
+ type TransitionResult,
29
+ } from './types.ts';
30
+
31
+ export type { ParsedState, Stage } from './types.ts';
32
+ export { TransitionGateFailed, LockAcquisitionError, ParseError } from './types.ts';
33
+
34
+ /**
35
+ * Read STATE.md from disk and return the parsed state.
36
+ *
37
+ * Shared-read: no lock is taken. Reads are snapshot-safe for markdown
38
+ * (the OS guarantees a coherent view even if a writer is mid-rename —
39
+ * we either see the old file or the new file, never a torn write,
40
+ * because `mutate()` uses atomic rename).
41
+ */
42
+ export async function read(path: string): Promise<ParsedState> {
43
+ const raw: string = readFileSync(path, 'utf8');
44
+ return parse(raw).state;
45
+ }
46
+
47
+ /**
48
+ * Atomic read-modify-write on STATE.md.
49
+ *
50
+ * Flow:
51
+ * 1. Acquire sibling `.lock` file (PID+timestamp advisory lock).
52
+ * 2. Read current contents.
53
+ * 3. Apply `fn`.
54
+ * 4. Serialize to a `.tmp` file next to `path`.
55
+ * 5. `renameSync(.tmp, path)` — POSIX-atomic; on Windows EPERM means
56
+ * a scanner held it briefly, retry once.
57
+ * 6. Release the lock (in `finally` — released even on mid-fn throw).
58
+ *
59
+ * Crash between write and rename is benign: STATE.md is untouched; the
60
+ * `.tmp` file is orphaned (cleaned up on the next acquire by the caller).
61
+ */
62
+ export async function mutate(
63
+ path: string,
64
+ fn: (s: ParsedState) => ParsedState,
65
+ ): Promise<ParsedState> {
66
+ const release = await acquire(path);
67
+ const tmpPath: string = `${path}.tmp`;
68
+ try {
69
+ const raw: string = readFileSync(path, 'utf8');
70
+ const { state, raw_bodies, raw_frontmatter, block_gaps, line_ending } =
71
+ parse(raw);
72
+ // Deep-clone so the consumer's fn cannot mutate the state we just
73
+ // parsed (defensive — apply() does this too for pure callers).
74
+ const clone = structuredClone(state);
75
+ const next = fn(clone);
76
+ const out = serialize(next, {
77
+ raw_frontmatter,
78
+ raw_bodies,
79
+ block_gaps,
80
+ line_ending,
81
+ });
82
+ writeFileSync(tmpPath, out, 'utf8');
83
+ try {
84
+ renameSync(tmpPath, path);
85
+ } catch (err) {
86
+ // Windows EPERM retry — AV / indexer holding STATE.md briefly.
87
+ const code =
88
+ typeof err === 'object' && err !== null && 'code' in err
89
+ ? (err as { code?: unknown }).code
90
+ : undefined;
91
+ if (code === 'EPERM' || code === 'EBUSY') {
92
+ await new Promise((r) => setTimeout(r, 50));
93
+ renameSync(tmpPath, path);
94
+ } else {
95
+ throw err;
96
+ }
97
+ }
98
+ return next;
99
+ } catch (err) {
100
+ // Clean up the orphaned tmp file on failure so we don't pollute.
101
+ try {
102
+ if (existsSync(tmpPath)) unlinkSync(tmpPath);
103
+ } catch {
104
+ // best-effort; a leftover tmp file does not corrupt STATE.md.
105
+ }
106
+ throw err;
107
+ } finally {
108
+ await release();
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Advance to `toStage` under the locked RMW protocol.
114
+ *
115
+ * Steps:
116
+ * 1. Read current state (outside the lock) to pass to the gate.
117
+ * 2. Resolve the gate via `gateFor(position.stage, toStage)`.
118
+ * - `null` → TransitionGateFailed "Invalid transition" (skip-stage,
119
+ * backward, same-stage, or from outside the Stage union).
120
+ * 3. Invoke the gate. If `pass: false`, throw TransitionGateFailed with
121
+ * the gate's blockers verbatim.
122
+ * 4. If `pass: true`, mutate STATE.md under the lock:
123
+ * - frontmatter.stage = toStage
124
+ * - position.stage = toStage
125
+ * - frontmatter.last_checkpoint = now (ISO)
126
+ * - timestamps[`${toStage}_started_at`] = now (ISO)
127
+ *
128
+ * Returns the updated state plus the gate response (for callers that
129
+ * want to log blockers — on pass, `blockers` is always `[]`).
130
+ */
131
+ export async function transition(
132
+ path: string,
133
+ toStage: Stage,
134
+ ): Promise<TransitionResult> {
135
+ // Read (outside the lock) to pass current state to the gate — the
136
+ // mutate() below will re-read under the lock before applying changes.
137
+ // This two-phase pattern matches the GSD reference implementation.
138
+ const beforeMutate = await read(path);
139
+ const from: string = beforeMutate.position.stage;
140
+ // `position.stage` is typed as `string` in ParsedState (parser tolerates
141
+ // `scan` and other pre-brief values). Narrow it to `Stage` before asking
142
+ // the gate registry — anything outside the union is an invalid FROM.
143
+ if (!isStage(from)) {
144
+ throw new TransitionGateFailed(toStage, [
145
+ `Invalid transition: from="${from}" is not a recognized Stage`,
146
+ ]);
147
+ }
148
+ const gate = gateFor(from, toStage);
149
+ if (gate === null) {
150
+ throw new TransitionGateFailed(toStage, [
151
+ `Invalid transition: ${from} → ${toStage}`,
152
+ ]);
153
+ }
154
+ const gateResult = gate(beforeMutate);
155
+ if (!gateResult.pass) {
156
+ throw new TransitionGateFailed(toStage, gateResult.blockers);
157
+ }
158
+ const nowIso: string = new Date().toISOString();
159
+ const nextState = await mutate(path, (s): ParsedState => {
160
+ s.frontmatter.stage = toStage;
161
+ s.frontmatter.last_checkpoint = nowIso;
162
+ s.position.stage = toStage;
163
+ s.timestamps[`${toStage}_started_at`] = nowIso;
164
+ return s;
165
+ });
166
+ return { pass: true, blockers: gateResult.blockers, state: nextState };
167
+ }
@@ -1,4 +1,4 @@
1
- // scripts/lib/gdd-state/lockfile.ts — PID+timestamp sibling lockfile.
1
+ // sdk/state/lockfile.ts — PID+timestamp sibling lockfile.
2
2
  //
3
3
  // Pattern (from GSD state-mutation.ts D2):
4
4
  // Lock file at `${target}.lock` holds JSON
@@ -1,4 +1,4 @@
1
- // scripts/lib/gdd-state/mutator.ts — serializer + `apply(raw, fn)` mutator.
1
+ // sdk/state/mutator.ts — serializer + `apply(raw, fn)` mutator.
2
2
  //
3
3
  // Two guarantees:
4
4
  // 1. `serialize(parse(raw).state, parse(raw).raw_bodies)` === `raw`
@@ -1,4 +1,4 @@
1
- // scripts/lib/gdd-state/parser.ts — turns STATE.md text into ParsedState.
1
+ // sdk/state/parser.ts — turns STATE.md text into ParsedState.
2
2
  //
3
3
  // Design constraints:
4
4
  // 1. Pure function (no I/O). `read()` in index.ts supplies the file.
@@ -1,4 +1,4 @@
1
- // scripts/lib/gdd-state/types.ts — typed shape of a parsed STATE.md.
1
+ // sdk/state/types.ts — typed shape of a parsed STATE.md.
2
2
  //
3
3
  // Plan 20-01 (SDK-01/02): canonical type surface consumed by the parser,
4
4
  // mutator, and public read/mutate/transition API. Everything here is
@@ -241,18 +241,18 @@ export interface TransitionResult extends GateResult {
241
241
 
242
242
  // Error classes migrated to the unified GDDError taxonomy in Plan 20-04.
243
243
  // Re-exported here so existing consumers (tests, downstream modules) keep
244
- // importing from `gdd-state/types.ts` unchanged.
244
+ // importing from `sdk/state/types.ts` unchanged.
245
245
  //
246
246
  // * TransitionGateFailed — StateConflictError subclass; retryable
247
247
  // * LockAcquisitionError — StateConflictError subclass; retryable
248
248
  // * ParseError — ValidationError subclass; fix your STATE.md
249
249
  //
250
- // See `scripts/lib/gdd-errors/index.ts` for the taxonomy definition.
250
+ // See `sdk/errors/index.ts` for the taxonomy definition.
251
251
  export {
252
252
  TransitionGateFailed,
253
253
  LockAcquisitionError,
254
254
  ParseError,
255
- } from '../gdd-errors/index.ts';
255
+ } from '../errors/index.ts';
256
256
 
257
257
  /** Type-guard for `Stage`. */
258
258
  export function isStage(value: unknown): value is Stage {
@@ -63,4 +63,17 @@ After the consolidated audit summary has been printed (and any reflection-propos
63
63
 
64
64
  Written by `hooks/update-check.sh`; suppressed mid-pipeline and when the latest release is dismissed.
65
65
 
66
+ ## Rationalizations — Thought to Reality
67
+
68
+ The excuses an agent reaches for to skip or thin out an audit, and the drift each one misses:
69
+
70
+ | Thought | Reality |
71
+ |---------|---------|
72
+ | "The audit passed last cycle, I can skip it this cycle." | Per-cycle audit catches drift the prior pass couldn't see; a skipped review is exactly where regressions accumulate unnoticed. |
73
+ | "`--quick` is fine, integration isn't the concern here." | Dropping the integration-checker hides orphaned decisions — wiring breaks even when the 6-pillar score looks healthy. |
74
+ | "I can eyeball the scores instead of spawning the auditor." | The auditor's rubric scores six pillars consistently; an eyeballed review drifts toward whatever the agent already believes. |
75
+ | "Reflection proposals are optional polish, skip the reflector." | The reflector turns this cycle's learnings into next-cycle improvements; skipping it lets the same mistakes repeat. |
76
+ | "I'll modify the source while I'm in here fixing findings." | Audit is read-only by contract; editing source mid-audit invalidates the very scores you're producing. |
77
+ | "Retroactive mode is overkill for a finished cycle." | Retroactive verification is the only check on tasks that shipped without per-task verify — skipping it leaves a completed cycle unaudited. |
78
+
66
79
  ## AUDIT COMPLETE
@@ -92,4 +92,29 @@ Next: @get-design-done explore
92
92
  ━━━━━━━━━━━━━━━━━━━━━━━
93
93
  ```
94
94
 
95
+ ## Spec self-review (before transition)
96
+
97
+ Run this final spec-quality pass over `.design/BRIEF.md` before the brief→explore transition:
98
+ - Placeholder scan: no TBD / TODO / `<placeholder>` / lorem left in the artifact.
99
+ - Internal consistency: sections don't contradict each other.
100
+ - Scope check: nothing in the artifact exceeds (or silently drops) the agreed scope.
101
+ - Ambiguity check: every requirement/decision is specific enough to act on without a follow-up question.
102
+
103
+ <HARD-GATE>
104
+ Do NOT transition to explore (or invoke `/gdd:explore`) until the brief artifact (default `.design/BRIEF.md`) is committed AND the user has approved it. If this project uses a custom `.design` location, read the artifact path from `.design/STATE.md` rather than assuming the default.
105
+ </HARD-GATE>
106
+
107
+ ## Rationalizations — Thought to Reality
108
+
109
+ The excuses an agent invents to skip or shortcut the brief, and what each one actually costs the cycle:
110
+
111
+ | Thought | Reality |
112
+ |---------|---------|
113
+ | "This brief is too simple to need a problem statement." | Skip the brief = guess at requirements, then redesign mid-design when the real problem surfaces. |
114
+ | "The user told me what to build, I can skip the interview." | Unasked constraints (a11y, brand, stack) become rework — the five questions exist because each one has blown a past cycle. |
115
+ | "I'll capture success metrics later in verify." | Verify has nothing to check against; an un-metricked brief produces an un-verifiable cycle. |
116
+ | "Scope is obvious, I don't need an in/out line." | Undeclared scope is scope creep waiting to happen — the explore scan widens to fill the vacuum. |
117
+ | "I can answer all five questions for the user from context." | AskUserQuestion one-at-a-time exists because batched/assumed answers smuggle in wrong premises that compound downstream. |
118
+ | "STATE.md bootstrap can wait." | Every later MCP mutation requires STATE.md to exist; skipping the bootstrap hard-blocks explore on entry. |
119
+
95
120
  ## BRIEF COMPLETE
@@ -78,4 +78,21 @@ Print the `=== Design stage complete ===` summary (tasks complete/total, deviati
78
78
 
79
79
  After all tasks finish, if STATE.md `<connections>` has `figma: available`, offer the user the figma-write opt-in prompt (modes: annotate / tokenize / mappings, with optional `--dry-run`). Spawn `design-figma-writer` with the selected mode on "yes"; skip silently on "no". NEVER auto-run without confirmation. Full prompt + dispatch logic: `./design-procedure.md` §Figma Write Dispatch.
80
80
 
81
+ <HARD-GATE>
82
+ Do NOT transition to verify (or invoke `/gdd:verify`) until `.design/DESIGN-SUMMARY.md` is committed. If this project uses a custom `.design` location, read the artifact path from `.design/STATE.md` rather than assuming the default.
83
+ </HARD-GATE>
84
+
85
+ ## Rationalizations — Thought to Reality
86
+
87
+ The excuses an agent uses to cut corners during design implementation, and the cost of each:
88
+
89
+ | Thought | Reality |
90
+ |---------|---------|
91
+ | "I can skip planning for this small task and just implement it." | Plan-skipped tasks blow scope per cycle telemetry; the gate is for the typical case, not the exception. |
92
+ | "These two tasks touch nearby files but I'll run them in parallel anyway." | Overlapping `Touches:` in a parallel batch produce merge conflicts that silently drop one task's work — split into sequential sub-waves. |
93
+ | "Hardcoding this value is faster than wiring the token." | A hardcoded value is a stub the verifier catches as drift from the design tokens; you pay for it twice. |
94
+ | "I'll emit the `.stories.tsx` stub later when Storybook is back up." | The CSF stub must land with the component or the next cycle's visual-regression scope misses it entirely. |
95
+ | "This deviation is minor, I won't record a blocker." | An unrecorded deviation can't be resolved by a follow-up task, so it leaks into verify as an unexplained gap. |
96
+ | "Auto-mode means I can ignore the wave checkpoints." | Auto-mode skips prompts, not the wave structure; ignoring wave order still corrupts dependent-task ordering. |
97
+
81
98
  ## DESIGN COMPLETE
@@ -80,4 +80,17 @@ Cycle: <name or "default">
80
80
  - Do not run the interview yourself — always spawn the agent.
81
81
  - Do not touch files outside `.design/`.
82
82
 
83
+ ## Rationalizations — Thought to Reality
84
+
85
+ The shortcuts an agent takes during a discuss session, and what each one costs the decision record:
86
+
87
+ | Thought | Reality |
88
+ |---------|---------|
89
+ | "I'll ask all eight questions at once to save time." | Batched questions overwhelm the user; one-at-a-time keeps each decision clean and prevents coupled answers. |
90
+ | "I can run the interview inline instead of spawning the discussant." | The skill's contract is to always spawn the agent — running it yourself skips the discussant's mode handling and D-XX numbering. |
91
+ | "This answer is good enough, I'll record it as a decision without follow-up." | A vague answer ("modern", "clean") recorded as a D-XX locks in an undecided premise; reject and re-ask once. |
92
+ | "I'll batch all the new D-XX entries into STATE.md at the end." | Decisions written atomically per answer survive an interrupted session; batching loses everything if the session drops. |
93
+ | "The glossary term can wait until I write the summary." | CONTEXT.md is written immediately per term — a deferred glossary entry is a naming inconsistency the next cycle inherits. |
94
+ | "Every decision this session is worth an ADR." | ADRs require all three criteria (hard-to-reverse, surprising, real-tradeoff); auto-promoting routine choices buries the genuinely load-bearing ones. |
95
+
83
96
  ## DISCUSS COMMAND COMPLETE
@@ -85,4 +85,21 @@ Full interview protocol + JSON line schema: `./explore-procedure.md` §Step 3.
85
85
 
86
86
  Print: "=== Explore complete ===\nSaved: .design/DESIGN.md, .design/DESIGN-DEBT.md, .design/DESIGN-CONTEXT.md\nNext: @get-design-done plan".
87
87
 
88
+ <HARD-GATE>
89
+ Do NOT transition to plan (or invoke `/gdd:plan`) until BOTH `.design/DESIGN.md` AND `.design/DESIGN-CONTEXT.md` are committed AND the user has approved them. If this project uses a custom `.design` location, read the artifact paths from `.design/STATE.md` rather than assuming the default.
90
+ </HARD-GATE>
91
+
92
+ ## Rationalizations — Thought to Reality
93
+
94
+ The shortcut excuses an agent reaches for during explore, and the drift each one introduces:
95
+
96
+ | Thought | Reality |
97
+ |---------|---------|
98
+ | "I already know this codebase, I can skip the inventory scan." | An unscanned codebase hides the tokens/components you'll duplicate — the grep pass exists to stop you reinventing what's there. |
99
+ | "The six connection probes are noise, I'll assume Figma is off." | A skipped probe means a wrong connection assumption silently breaks the design stage's tool dispatch. |
100
+ | "`--skip-interview` is fine, the brief covered it." | The interview locks the gray areas the brief left fuzzy; skipping it ships undecided D-XX into planning. |
101
+ | "I'll batch all the interview questions to save round-trips." | Batched questions overwhelm the user and smuggle in coupled assumptions — one-at-a-time keeps each decision clean. |
102
+ | "DESIGN-DEBT.md is optional, the scan was clean enough." | Unrecorded debt resurfaces as an unexplained constraint three stages later with no provenance. |
103
+ | "Prior sketches and project conventions don't apply this cycle." | Ignored conventions get overridden by defaults, producing inconsistency the audit will flag against the rest of the system. |
104
+
88
105
  ## EXPLORE COMPLETE
@@ -63,6 +63,12 @@ After the health table, the `gdd_health` MCP surface (`scripts/lib/health-mirror
63
63
 
64
64
  Token PRESENCE only is detected (D-10) — the token value is never read, logged, or shown. The Free-tier signal is read from the local raw-pull cache only; no network call is made.
65
65
 
66
+ ## Skill-discipline bootstrap (skill_discipline)
67
+
68
+ The `gdd_health` MCP surface also reports a `skill_discipline` check (Phase 32) confirming the using-gdd SessionStart bootstrap is live — detail is one of three exact strings:
69
+ - `skill-discipline: ready` — `skills/using-gdd/SKILL.md` exists AND `hooks/hooks.json` SessionStart wires `inject-using-gdd.sh` (status `ok`).
70
+ - `skill-discipline: missing using-gdd` (skill absent) or `skill-discipline: hook not wired` (skill present, no SessionStart inject) — both `warn`.
71
+
66
72
  ## Check MCP registration (gdd-mcp)
67
73
 
68
74
  After the health table, inspect whether `gdd-mcp` (Phase 27.7+) is registered with any installed harness and render a one-line status row. Dismissable via `.design/config.json#mcp_nudge=false`. Non-blocking: failure paths render `MCP server: unknown` rather than crash. Full detection procedure (dismissal check, detection via `scripts/lib/install/mcp-register.cjs`, row rendering for claude/codex/both/neither, fallback) lives in `./health-mcp-detection.md`.
@@ -77,4 +77,29 @@ The next stage (design) calls `mcp__gdd_state__transition_stage` on entry — th
77
77
 
78
78
  Print: plan tasks (N waves, M total tasks), files written (`.design/DESIGN-PLAN.md`, plus `.design/DESIGN-RESEARCH.md` if research ran), next step `/get-design-done:design`.
79
79
 
80
+ ## Spec self-review (before transition)
81
+
82
+ Run this final spec-quality pass over `.design/DESIGN-PLAN.md` before the plan→design transition:
83
+ - Placeholder scan: no TBD / TODO / `<placeholder>` / lorem left in the artifact.
84
+ - Internal consistency: sections don't contradict each other.
85
+ - Scope check: nothing in the artifact exceeds (or silently drops) the agreed scope.
86
+ - Ambiguity check: every requirement/decision is specific enough to act on without a follow-up question.
87
+
88
+ <HARD-GATE>
89
+ Do NOT transition to design (or invoke `/gdd:design`) until `.design/DESIGN-PLAN.md` is committed AND the user has approved it. If this project uses a custom `.design` location, read the artifact path from `.design/STATE.md` rather than assuming the default.
90
+ </HARD-GATE>
91
+
92
+ ## Rationalizations — Thought to Reality
93
+
94
+ The reasons an agent gives to skip planning or rush DESIGN-PLAN.md, and what each one costs:
95
+
96
+ | Thought | Reality |
97
+ |---------|---------|
98
+ | "This change is small, I can design straight from DESIGN-CONTEXT.md." | Plan-skipped tasks blow scope per cycle telemetry; the plan gate is for the typical case, not the exception you think you're in. |
99
+ | "Pattern mapping is brownfield ceremony, I'll skip it." | Step 1.5 is mandatory because an unmapped brownfield is where the executor silently re-implements an existing pattern. |
100
+ | "The plan-checker will just rubber-stamp it, skip the spawn." | The checker's 5 dimensions (coverage, wave order, must-have derivation) catch the gaps you can't see in your own plan. |
101
+ | "I'll let the planner infer wave ordering at design time." | Unordered waves serialize work that could parallelize — or worse, run dependent tasks concurrently and corrupt the tree. |
102
+ | "Research is overkill for this scope." | The complexity heuristic exists precisely because agents under-estimate scope; skipping research on a 3+-scope domain guarantees a mid-design surprise. |
103
+ | "I can record decisions in DESIGN-PLAN.md prose instead of D-XX." | Prose decisions never reach STATE.md, so verify's integration-checker can't trace them and flags them orphaned. |
104
+
80
105
  ## PLAN COMPLETE
@@ -58,11 +58,11 @@ Else: increment `iteration`, emit `quality_gate_iteration`, spawn `design-fixer`
58
58
 
59
59
  ## Step 5 — STATE write
60
60
 
61
- Mutate `state.quality_gate.run` to `{started_at, completed_at, status, iteration, commands_run, extra_attrs:{}}`. Persist via `mcp__gdd_state__set_quality_gate` or `apply()` mutator from `scripts/lib/gdd-state/mutator.ts` — identical on-disk shape.
61
+ Mutate `state.quality_gate.run` to `{started_at, completed_at, status, iteration, commands_run, extra_attrs:{}}`. Persist via `mcp__gdd_state__set_quality_gate` or `apply()` mutator from `sdk/state/mutator.ts` — identical on-disk shape.
62
62
 
63
63
  ## Step 6 — Event emission (D-09)
64
64
 
65
- Use `appendEvent` from `scripts/lib/event-stream/index.ts` — persist-first / broadcast-second; never throws on persist path. `ts` / `cycle` / `stage` are stamped by the writer. Six event types (one per lifecycle position):
65
+ Use `appendEvent` from `sdk/event-stream/index.ts` — persist-first / broadcast-second; never throws on persist path. `ts` / `cycle` / `stage` are stamped by the writer. Six event types (one per lifecycle position):
66
66
 
67
67
  | Event | When | Payload |
68
68
  |-------|------|---------|
@@ -79,6 +79,10 @@ If `.design/budget.json` is missing, assume defaults from `reference/config-sche
79
79
 
80
80
  When the router cannot resolve `intent-string` to a known agent (no `description` match, no `default-tier` rule, no path-selection fallback), emit ONE `capability_gap` event with `source: "router"` before returning the conservative-fallback JSON. Feeds Phase 29 Stage-0 telemetry — see `./capability-gap-emitter.md` for the synchronous Node snippet, semantic notes (suggested_kind = `"agent"`, MCP-probe exclusion per D-08, back-compat invariant on router output), and the opaque-extras payload routing through `appendChainEvent`.
81
81
 
82
+ ## Emitting router_pick on a resolved pick
83
+
84
+ When the router DID resolve a pick — it has the `path`/`complexity_class`/`resolved_models` decision and is about to return the decision JSON — emit ONE `router_pick` event (`source: "router"`) recording which skill/agent was auto-picked, as the last step before returning. Side-effect only; the output JSON contract is UNCHANGED. Feeds the D-02 under-reached-skill instrument (Phase 33 baselines per-skill pick rates) — see `./router-pick-emitter.md` for the synchronous Node snippet, the 7-field no-PII payload (context_hash only — never the raw prompt), and the opaque-extras routing through `appendChainEvent`.
85
+
82
86
  ## Non-Goals
83
87
 
84
88
  The router does not: (a) make a model call, (b) write files, (c) enforce budget caps (that's the hook's job), (d) learn from history (Phase 11 reflector territory per D-07).