@hegemonart/get-design-done 1.19.6 → 1.20.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 (93) hide show
  1. package/.claude-plugin/marketplace.json +4 -4
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/CHANGELOG.md +60 -0
  4. package/README.md +12 -0
  5. package/agents/design-reflector.md +13 -0
  6. package/connections/connections.md +3 -0
  7. package/connections/figma.md +2 -0
  8. package/connections/gdd-state.md +186 -0
  9. package/hooks/budget-enforcer.ts +716 -0
  10. package/hooks/context-exhaustion.ts +251 -0
  11. package/hooks/gdd-read-injection-scanner.ts +172 -0
  12. package/hooks/hooks.json +3 -3
  13. package/package.json +19 -6
  14. package/reference/config-schema.md +2 -2
  15. package/reference/error-recovery.md +58 -0
  16. package/reference/registry.json +7 -0
  17. package/reference/schemas/budget.schema.json +42 -0
  18. package/reference/schemas/events.schema.json +55 -0
  19. package/reference/schemas/generated.d.ts +419 -0
  20. package/reference/schemas/iteration-budget.schema.json +36 -0
  21. package/reference/schemas/mcp-gdd-state-tools.schema.json +89 -0
  22. package/reference/schemas/rate-limits.schema.json +31 -0
  23. package/scripts/aggregate-agent-metrics.ts +282 -0
  24. package/scripts/codegen-schema-types.ts +149 -0
  25. package/scripts/lib/error-classifier.cjs +232 -0
  26. package/scripts/lib/error-classifier.d.cts +44 -0
  27. package/scripts/lib/event-stream/emitter.ts +88 -0
  28. package/scripts/lib/event-stream/index.ts +154 -0
  29. package/scripts/lib/event-stream/types.ts +127 -0
  30. package/scripts/lib/event-stream/writer.ts +154 -0
  31. package/scripts/lib/gdd-errors/classification.ts +124 -0
  32. package/scripts/lib/gdd-errors/index.ts +218 -0
  33. package/scripts/lib/gdd-state/gates.ts +216 -0
  34. package/scripts/lib/gdd-state/index.ts +167 -0
  35. package/scripts/lib/gdd-state/lockfile.ts +232 -0
  36. package/scripts/lib/gdd-state/mutator.ts +574 -0
  37. package/scripts/lib/gdd-state/parser.ts +523 -0
  38. package/scripts/lib/gdd-state/types.ts +179 -0
  39. package/scripts/lib/iteration-budget.cjs +205 -0
  40. package/scripts/lib/iteration-budget.d.cts +32 -0
  41. package/scripts/lib/jittered-backoff.cjs +112 -0
  42. package/scripts/lib/jittered-backoff.d.cts +38 -0
  43. package/scripts/lib/lockfile.cjs +177 -0
  44. package/scripts/lib/lockfile.d.cts +21 -0
  45. package/scripts/lib/prompt-sanitizer/index.ts +435 -0
  46. package/scripts/lib/prompt-sanitizer/patterns.ts +173 -0
  47. package/scripts/lib/rate-guard.cjs +365 -0
  48. package/scripts/lib/rate-guard.d.cts +38 -0
  49. package/scripts/mcp-servers/gdd-state/schemas/add_blocker.schema.json +67 -0
  50. package/scripts/mcp-servers/gdd-state/schemas/add_decision.schema.json +68 -0
  51. package/scripts/mcp-servers/gdd-state/schemas/add_must_have.schema.json +68 -0
  52. package/scripts/mcp-servers/gdd-state/schemas/checkpoint.schema.json +51 -0
  53. package/scripts/mcp-servers/gdd-state/schemas/frontmatter_update.schema.json +62 -0
  54. package/scripts/mcp-servers/gdd-state/schemas/get.schema.json +51 -0
  55. package/scripts/mcp-servers/gdd-state/schemas/probe_connections.schema.json +75 -0
  56. package/scripts/mcp-servers/gdd-state/schemas/resolve_blocker.schema.json +66 -0
  57. package/scripts/mcp-servers/gdd-state/schemas/set_status.schema.json +47 -0
  58. package/scripts/mcp-servers/gdd-state/schemas/transition_stage.schema.json +70 -0
  59. package/scripts/mcp-servers/gdd-state/schemas/update_progress.schema.json +58 -0
  60. package/scripts/mcp-servers/gdd-state/server.ts +288 -0
  61. package/scripts/mcp-servers/gdd-state/tools/add_blocker.ts +72 -0
  62. package/scripts/mcp-servers/gdd-state/tools/add_decision.ts +89 -0
  63. package/scripts/mcp-servers/gdd-state/tools/add_must_have.ts +113 -0
  64. package/scripts/mcp-servers/gdd-state/tools/checkpoint.ts +60 -0
  65. package/scripts/mcp-servers/gdd-state/tools/frontmatter_update.ts +91 -0
  66. package/scripts/mcp-servers/gdd-state/tools/get.ts +51 -0
  67. package/scripts/mcp-servers/gdd-state/tools/index.ts +51 -0
  68. package/scripts/mcp-servers/gdd-state/tools/probe_connections.ts +73 -0
  69. package/scripts/mcp-servers/gdd-state/tools/resolve_blocker.ts +84 -0
  70. package/scripts/mcp-servers/gdd-state/tools/set_status.ts +54 -0
  71. package/scripts/mcp-servers/gdd-state/tools/shared.ts +194 -0
  72. package/scripts/mcp-servers/gdd-state/tools/transition_stage.ts +80 -0
  73. package/scripts/mcp-servers/gdd-state/tools/update_progress.ts +81 -0
  74. package/scripts/validate-frontmatter.ts +114 -0
  75. package/scripts/validate-schemas.ts +401 -0
  76. package/skills/brief/SKILL.md +15 -6
  77. package/skills/design/SKILL.md +31 -13
  78. package/skills/explore/SKILL.md +41 -17
  79. package/skills/health/SKILL.md +15 -4
  80. package/skills/optimize/SKILL.md +3 -3
  81. package/skills/pause/SKILL.md +16 -10
  82. package/skills/plan/SKILL.md +33 -17
  83. package/skills/progress/SKILL.md +15 -11
  84. package/skills/resume/SKILL.md +19 -10
  85. package/skills/settings/SKILL.md +11 -3
  86. package/skills/todo/SKILL.md +12 -3
  87. package/skills/verify/SKILL.md +65 -29
  88. package/hooks/budget-enforcer.js +0 -329
  89. package/hooks/context-exhaustion.js +0 -127
  90. package/hooks/gdd-read-injection-scanner.js +0 -39
  91. package/scripts/aggregate-agent-metrics.js +0 -173
  92. package/scripts/validate-frontmatter.cjs +0 -68
  93. package/scripts/validate-schemas.cjs +0 -242
@@ -2,18 +2,18 @@
2
2
  name: gdd-settings
3
3
  description: "Manage .design/config.json settings. Subcommands: profile, parallelism, cleanup, show."
4
4
  argument-hint: "<profile <name>|parallelism <key> <value>|cleanup|show>"
5
- tools: Read, Write, AskUserQuestion, Bash
5
+ tools: Read, Write, AskUserQuestion, Bash, mcp__gdd_state__get, mcp__gdd_state__frontmatter_update
6
6
  ---
7
7
 
8
8
  # gdd-settings
9
9
 
10
- Manages `.design/config.json` — the per-project config for model profile and parallelism. See `reference/config-schema.md` for the full schema.
10
+ Manages `.design/config.json` — the per-project config for model profile and parallelism. See `reference/config-schema.md` for the full schema. This skill also supports patching non-stage STATE.md frontmatter keys (`cycle`, `wave`, custom keys) via `mcp__gdd_state__frontmatter_update`. See **STATE.md frontmatter** below.
11
11
 
12
12
  ## Subcommands
13
13
 
14
14
  ### `show`
15
15
 
16
- Print the current `.design/config.json` contents, nicely formatted. If the file is missing, print the defaults with a note that no config exists yet.
16
+ Print the current `.design/config.json` contents, nicely formatted. If the file is missing, print the defaults with a note that no config exists yet. Also call `mcp__gdd_state__get` to print the current STATE.md frontmatter keys (cycle, wave, model_profile) alongside config.json for a unified view.
17
17
 
18
18
  ### `profile <name>`
19
19
 
@@ -50,6 +50,14 @@ Always:
50
50
  2. Merge the single field being changed — never overwrite unrelated fields.
51
51
  3. Write back as pretty JSON (2-space indent, trailing newline).
52
52
 
53
+ ## STATE.md frontmatter
54
+
55
+ For any STATE.md frontmatter patch (cycle, wave, or project-custom keys), call `mcp__gdd_state__frontmatter_update({ patch: { <key>: <value> } })`. Do not `Edit` or `Write` STATE.md directly.
56
+
57
+ **Stage-patch guard:** this skill cannot patch `stage`. If the user attempts to set `stage` here, reject with: "Use /gdd:brief, /gdd:explore, etc. for stage transitions. The settings skill is for non-stage frontmatter only." The MCP tool itself rejects `stage` patches with a VALIDATION error (surfaced by `mcp__gdd_state__frontmatter_update`), which this prose surfaces up-front so the user gets a clear message before the tool round-trip.
58
+
59
+ This surface is STATE.md-only. `.design/config.json` mutations continue to use `Read` + `Write` directly (out of scope for the 11-tool MCP catalog).
60
+
53
61
  ## Default Config
54
62
 
55
63
  If `.design/config.json` does not exist, create it with:
@@ -2,12 +2,12 @@
2
2
  name: gdd-todo
3
3
  description: "Design backlog — add/list/pick design tasks. Writes to .design/TODO.md."
4
4
  argument-hint: "<add|list|pick> [text]"
5
- tools: Read, Write, AskUserQuestion
5
+ tools: Read, Write, AskUserQuestion, mcp__gdd_state__get, mcp__gdd_state__add_decision, mcp__gdd_state__add_must_have
6
6
  ---
7
7
 
8
8
  # /gdd:todo
9
9
 
10
- **Role:** Design todo list. Three subcommands: `add`, `list`, `pick`. Backing store: `.design/TODO.md`.
10
+ **Role:** Design todo list. Three subcommands: `add`, `list`, `pick`. Backing store: `.design/TODO.md`. For items that are pipeline-level decisions or must-haves (not free-form backlog), route through the `gdd-state` MCP tools instead — see **Pipeline-linked items** below.
11
11
 
12
12
  ## File format
13
13
 
@@ -37,7 +37,8 @@ If text omitted, use `AskUserQuestion`: "What todo item? (include priority P0-P3
37
37
  Create TODO.md from the template above if missing.
38
38
 
39
39
  ### list
40
- Read `.design/TODO.md`. Print all `- [ ]` and `- [-]` items grouped by priority section, with index numbers.
40
+ 1. Call `mcp__gdd_state__get` pipeline-level decisions + must-haves (shown as context at the top).
41
+ 2. Read `.design/TODO.md` (file outside the MCP catalog). Print all `- [ ]` and `- [-]` items grouped by priority section, with index numbers.
41
42
 
42
43
  ### pick
43
44
  Read `.design/TODO.md`. Collect unchecked items. Use `AskUserQuestion` to let the user pick one. Rewrite the chosen line as:
@@ -46,9 +47,17 @@ Read `.design/TODO.md`. Collect unchecked items. Use `AskUserQuestion` to let th
46
47
  ```
47
48
  Print "Picked: <item>".
48
49
 
50
+ ## Pipeline-linked items
51
+
52
+ When the user promotes a todo to a pipeline decision or must-have, route through MCP instead of TODO.md:
53
+
54
+ - Decision → `mcp__gdd_state__add_decision` with `{ id: "D-XX", text: "...", status: "locked"|"tentative" }`.
55
+ - Must-have → `mcp__gdd_state__add_must_have` with `{ id: "M-XX", text: "...", status: "pending" }`.
56
+
49
57
  ## Constraints
50
58
 
51
59
  - Do not modify files outside `.design/`.
52
60
  - Preserve existing sections and ordering on write.
61
+ - Do not mutate STATE.md directly — use the MCP tools above.
53
62
 
54
63
  ## TODO COMPLETE
@@ -3,6 +3,7 @@ name: verify
3
3
  description: "Stage 5 of 5 — spawns design-auditor, design-verifier, and design-integration-checker in sequence; interprets pass/gap result; handles gap-response loop with inline fix (Phase 5 will add AGENT-12 remediation agent). Thin orchestrator."
4
4
  argument-hint: "[--auto]"
5
5
  user-invocable: true
6
+ tools: mcp__gdd_state__get, mcp__gdd_state__transition_stage, mcp__gdd_state__add_must_have, mcp__gdd_state__add_blocker, mcp__gdd_state__resolve_blocker, mcp__gdd_state__update_progress, mcp__gdd_state__set_status, mcp__gdd_state__checkpoint, mcp__gdd_state__probe_connections
6
7
  ---
7
8
 
8
9
  # Get Design Done — Verify
@@ -13,11 +14,30 @@ user-invocable: true
13
14
 
14
15
  ## State Integration
15
16
 
16
- 1. Read `.design/STATE.md`.
17
- - If missing: create minimal skeleton from `reference/STATE-TEMPLATE.md` with `stage=verify`, `status=in_progress`; log warning to user: "No STATE.md found — creating minimal skeleton."
18
- - If present and `stage==verify` and `status==in_progress`: RESUME — if `.design/DESIGN-VERIFICATION.md` exists, pick up from the gap-response loop (skip re-spawning agents, go to Step 2). Otherwise re-spawn all three agents from Step 1.
19
- - Otherwise: normal transition set `stage=verify`, `status=in_progress`, `task_progress=0/3`.
20
- 2. Update `<connections>`, `last_checkpoint`. Write STATE.md.
17
+ ### Stage entry
18
+
19
+ 1. `mcp__gdd_state__transition_stage` with `to: "verify"`.
20
+ 2. `mcp__gdd_state__get` → snapshot `state`. Read `state.must_haves` — this is the verification checklist; each M-XX starts at `status: pending` and will be flipped to `pass` or `fail` as verification concludes.
21
+ 3. Resume detection (read `state.position.status` from the snapshot):
22
+ - If `status==in_progress` and `.design/DESIGN-VERIFICATION.md` exists: RESUME — skip re-spawning agents, go to Step 2 (gap-response loop).
23
+ - Otherwise: call `mcp__gdd_state__update_progress` with `task_progress: "0/3"`, `status: "in_progress"` to open the stage, then proceed to Step 1.
24
+ 4. If STATE.md is missing entirely (edge case — verify is never the entry point): block with "No STATE.md found — run /get-design-done:discover first." Do NOT attempt to create a skeleton from verify; upstream stages own bootstrap.
25
+
26
+ ---
27
+
28
+ ## Flipping a must-have status
29
+
30
+ When verification concludes that M-XX is satisfied (or failed), record the result by issuing:
31
+
32
+ `mcp__gdd_state__add_must_have` with the SAME `id` as the existing entry and the updated `status`:
33
+
34
+ ```json
35
+ { "id": "M-03", "text": "Dark mode toggle persists to localStorage", "status": "pass" }
36
+ ```
37
+
38
+ The gdd-state mutator treats an `add_must_have` with an existing id as an **update-in-place**, not a duplicate append. The entry's position in the `<must_haves>` block is preserved. This is intentional design — verify doesn't need a dedicated `update_must_have_status` tool because `add_must_have` handles both cases correctly.
39
+
40
+ Pass the original `text` verbatim when you're only flipping the status; supplying a changed `text` overwrites the prose in-place as well (useful when the M-XX description was imprecise and the verifier can restate it). Omit `text` by passing the value from the earlier `mcp__gdd_state__get` snapshot.
21
41
 
22
42
  ---
23
43
 
@@ -37,7 +57,7 @@ Step P2 — Live tool call:
37
57
  → Error containing "permission"/blocked → preview: permission_denied
38
58
  → Any other error → preview: unreachable
39
59
 
40
- Write preview status to .design/STATE.md <connections>.
60
+ Record the preview probe result via `mcp__gdd_state__probe_connections` (batched with the storybook and chromatic probes below — one call per stage, see "Batched connections write" at the end of this section).
41
61
  ```
42
62
 
43
63
  When `preview: available`, the design-verifier agent runs Phase 4B — Screenshot Evidence to resolve `? VISUAL` heuristic flags with real screenshot evidence. See `agents/design-verifier.md` Phase 4B for the screenshot evidence loop.
@@ -60,13 +80,13 @@ Step B2 — Dev server detection:
60
80
  → Returns JSON → storybook: available (compat endpoint)
61
81
  → Fails → storybook: unavailable
62
82
 
63
- Write storybook status to .design/STATE.md `<connections>`.
83
+ Record the storybook probe result for the batched `mcp__gdd_state__probe_connections` call (see below).
64
84
 
65
85
  ---
66
86
 
67
87
  ### Storybook A11y Loop (when storybook: available)
68
88
 
69
- If `storybook: available` in STATE.md `<connections>`:
89
+ If `state.connections.storybook === "available"` (from the earlier `mcp__gdd_state__get` snapshot):
70
90
  1. Run: Bash: npx storybook test --ci 2>&1 | tee .design/storybook-a11y-report.txt
71
91
  2. Read .design/storybook-a11y-report.txt — pass to design-verifier as additional a11y evidence
72
92
  3. design-verifier reads this file in its a11y gap analysis section and annotates DESIGN-VERIFICATION.md with per-story violations
@@ -91,7 +111,21 @@ Step C2 — Token check:
91
111
  → false → chromatic: unavailable
92
112
 
93
113
  Also check: if storybook: not_configured → chromatic effectively unavailable (emit note, do not run).
94
- Write chromatic status to .design/STATE.md <connections>.
114
+ Record the chromatic probe result for the batched `mcp__gdd_state__probe_connections` call below.
115
+
116
+ ### Batched connections write
117
+
118
+ After all three probes (preview, storybook, chromatic) have a verdict, call `mcp__gdd_state__probe_connections` ONCE with `probe_results` = an array of `{ name, status }` entries — one per probed connection. Example:
119
+
120
+ ```json
121
+ [
122
+ { "name": "preview", "status": "available" },
123
+ { "name": "storybook", "status": "unavailable" },
124
+ { "name": "chromatic", "status": "not_configured" }
125
+ ]
126
+ ```
127
+
128
+ Unspecified connections keep their existing value. Do NOT issue multiple `probe_connections` calls — the tool is designed for a single batch write per stage.
95
129
 
96
130
  ### Chromatic Visual Delta (when chromatic: available)
97
131
 
@@ -135,7 +169,7 @@ Also pass post-handoff context to design-auditor: auditor skips DESIGN-PLAN.md r
135
169
  - Read `.design/config.json` `parallelism` (or defaults from `reference/config-schema.md`).
136
170
  - Apply rules from `reference/parallelism-rules.md`.
137
171
  - `design-verifier` depends on `design-auditor` output (rule 1) → serial between those two. `design-integration-checker` is independent of the auditor's *file* output but runs after verifier in the current sequence; if config opts in, `design-auditor` and `design-integration-checker` can parallelize (disjoint writes). Default: serial.
138
- - Write `<parallelism_decision>` to STATE.md before spawning.
172
+ - Record `<parallelism_decision>` via `mcp__gdd_state__set_status` (e.g., `status: "verify_parallelism_decided: <serial|parallel>"`) before spawning. Do not write STATE.md directly.
139
173
 
140
174
  ## Step 1 — Spawn Auditor + Verifier + Integration Checker
141
175
 
@@ -169,7 +203,7 @@ Emit `## AUDIT COMPLETE` when done.
169
203
  """)
170
204
  ```
171
205
 
172
- Wait for `## AUDIT COMPLETE` in the agent response. Once detected, update STATE.md `task_progress=1/3`.
206
+ Wait for `## AUDIT COMPLETE` in the agent response. Once detected, call `mcp__gdd_state__update_progress` with `task_progress: "1/3"` and a short `status` summary (e.g., `status: "audit_done"`).
173
207
 
174
208
  ### 1b-gate. Lazy gate — should design-verifier run?
175
209
 
@@ -193,7 +227,7 @@ Spawn the cheap Haiku gate before the expensive verifier:
193
227
 
194
228
  Wait for `## GATE COMPLETE`. Parse the JSON:
195
229
 
196
- - `spawn: false` → append pending telemetry row `{ts, agent: "design-verifier", tier: "skipped", tokens_in: 0, tokens_out: 0, cache_hit: false, est_cost_usd: 0, lazy_skipped: true, gate_rationale: "<from gate>", cycle, phase}` (PreToolUse hook from 10.1-01 flushes on next tool use; orchestrator MAY stub-append directly to `.design/telemetry/costs.jsonl` until 10.1-05 lands). Skip 1b. Set `task_progress=2/3`. Emit `design-verifier skipped — gate rationale: <rationale>`.
230
+ - `spawn: false` → append pending telemetry row `{ts, agent: "design-verifier", tier: "skipped", tokens_in: 0, tokens_out: 0, cache_hit: false, est_cost_usd: 0, lazy_skipped: true, gate_rationale: "<from gate>", cycle, phase}` (PreToolUse hook from 10.1-01 flushes on next tool use; orchestrator MAY stub-append directly to `.design/telemetry/costs.jsonl` until 10.1-05 lands). Skip 1b. Call `mcp__gdd_state__update_progress` with `task_progress: "2/3"` and `status: "verifier_gate_skipped"`. Emit `design-verifier skipped — gate rationale: <rationale>`.
197
231
  - `spawn: true` → proceed to 1b as currently written.
198
232
 
199
233
  ### 1b. Run design-verifier (reads auditor output as additional input)
@@ -231,7 +265,7 @@ by structured gap list, then `## VERIFICATION COMPLETE`. If no gaps, just emit `
231
265
  """)
232
266
  ```
233
267
 
234
- Wait for `## VERIFICATION COMPLETE` in the agent response. Once detected, update STATE.md `task_progress=2/3`.
268
+ Wait for `## VERIFICATION COMPLETE` in the agent response. Once detected, call `mcp__gdd_state__update_progress` with `task_progress: "2/3"` and a short `status` summary (e.g., `status: "verifier_done"`).
235
269
 
236
270
  ### 1c-gate. Lazy gate — should design-integration-checker run?
237
271
 
@@ -255,7 +289,7 @@ Same pattern as 1b-gate:
255
289
 
256
290
  Wait for `## GATE COMPLETE`. Parse JSON:
257
291
 
258
- - `spawn: false` → append `lazy_skipped: true` telemetry row (same shape), skip 1c, set `task_progress=3/3`, emit `design-integration-checker skipped — gate rationale: <rationale>`.
292
+ - `spawn: false` → append `lazy_skipped: true` telemetry row (same shape), skip 1c, call `mcp__gdd_state__update_progress` with `task_progress: "3/3"` and `status: "integration_checker_gate_skipped"`, emit `design-integration-checker skipped — gate rationale: <rationale>`.
259
293
  - `spawn: true` → proceed to 1c as currently written.
260
294
 
261
295
  ### 1c. Run design-integration-checker (post-verification decision wiring check)
@@ -282,7 +316,7 @@ Emit `## INTEGRATION CHECK COMPLETE` when done.
282
316
  """)
283
317
  ```
284
318
 
285
- Wait for `## INTEGRATION CHECK COMPLETE` in the agent response. Once detected, update STATE.md `task_progress=3/3`.
319
+ Wait for `## INTEGRATION CHECK COMPLETE` in the agent response. Once detected, call `mcp__gdd_state__update_progress` with `task_progress: "3/3"` and a short `status` summary (e.g., `status: "integration_check_done"`).
286
320
 
287
321
  **Note:** Integration-checker findings (Orphaned and Missing decisions) are treated as additional gaps and fed into the gap-response loop in Step 2 alongside verifier gaps.
288
322
 
@@ -300,14 +334,14 @@ Merge verifier gaps (G-NN entries) and integration-checker gaps (Orphaned/Missin
300
334
 
301
335
  ### If NO gaps from either source (PASS):
302
336
 
303
- - Update STATE.md `<must_haves>`: set each M-XX `status=pass`.
304
- - Go to **State Update (exit)** with status=completed.
337
+ - For each M-XX from the earlier `mcp__gdd_state__get` snapshot (`state.must_haves`): call `mcp__gdd_state__add_must_have` with the same `id`, the same `text`, and `status: "pass"`. The mutator updates in-place (see "Flipping a must-have status" above).
338
+ - Go to **Stage exit** with status=completed.
305
339
 
306
340
  ### If GAPS FOUND (from either source):
307
341
 
308
342
  - Parse all gaps (verifier + integration-checker combined).
309
343
  - Count gaps by severity (BLOCKER, MAJOR, MINOR, COSMETIC).
310
- - If `auto_mode=true`: preserve DESIGN-VERIFICATION.md, update STATE.md `status=blocked`, append `<blockers>` entry: "[verify] [ISO date]: N blockers found — see .design/DESIGN-VERIFICATION.md and integration-checker output". Exit with message:
344
+ - If `auto_mode=true`: preserve DESIGN-VERIFICATION.md, call `mcp__gdd_state__set_status` with `status: "blocked"`, then call `mcp__gdd_state__add_blocker` with `stage: "verify"` and `text: "N blockers found — see .design/DESIGN-VERIFICATION.md and integration-checker output"` (the mutator stamps the ISO date automatically). Exit with message:
311
345
  ```
312
346
  Verification failed — N gaps found (X blockers, Y majors, Z minors, W cosmetics).
313
347
  Report: .design/DESIGN-VERIFICATION.md
@@ -340,9 +374,9 @@ Choose:
340
374
  ### If user chose [2] Save and exit:
341
375
 
342
376
  - Preserve DESIGN-VERIFICATION.md.
343
- - Update STATE.md: `<position> status=blocked`.
344
- - Append `<blockers>`: "[verify] [ISO date]: N gaps outstanding — see .design/DESIGN-VERIFICATION.md".
345
- - Write STATE.md.
377
+ - Call `mcp__gdd_state__set_status` with `status: "blocked"`.
378
+ - Call `mcp__gdd_state__add_blocker` with `stage: "verify"` and `text: "N gaps outstanding — see .design/DESIGN-VERIFICATION.md"` (ISO date stamped by the mutator).
379
+ - Call `mcp__gdd_state__checkpoint` to record the save-and-exit checkpoint.
346
380
  - Exit:
347
381
  ```
348
382
  Gaps saved. Resume with: /get-design-done:verify
@@ -351,9 +385,9 @@ Choose:
351
385
 
352
386
  ### If user chose [3] Accept as-is:
353
387
 
354
- - Update STATE.md `<must_haves>`: set `status=fail` for each unmet must-have, but proceed to exit.
355
- - Append `<blockers>`: "[verify] [ISO date]: accepted with N unresolved gaps".
356
- - Go to **State Update (exit)** with status=completed.
388
+ - For each unmet must-have (from the earlier snapshot, comparing against verifier gaps): call `mcp__gdd_state__add_must_have` with the same `id`, the same `text`, and `status: "fail"` (update-in-place idiom). Then proceed to exit.
389
+ - Call `mcp__gdd_state__add_blocker` with `stage: "verify"` and `text: "accepted with N unresolved gaps"`.
390
+ - Go to **Stage exit** with status=completed.
357
391
 
358
392
  ### If user chose [1] Fix now:
359
393
 
@@ -381,7 +415,7 @@ Context:
381
415
  auto_mode: <true|false>
382
416
 
383
417
  Emit ## FIX COMPLETE when all in-scope gaps have been attempted (partial success is still ## FIX COMPLETE).
384
- Write a <blocker> entry to .design/STATE.md for any gap that could not be fixed.
418
+ Record any gap that could not be fixed via mcp__gdd_state__add_blocker with stage: "verify".
385
419
  """)
386
420
 
387
421
  Wait for `## FIX COMPLETE` in the agent response before continuing.
@@ -417,11 +451,13 @@ Write updated .design/DESIGN-VERIFICATION.md. Emit ## GAPS FOUND (if any), then
417
451
 
418
452
  ---
419
453
 
420
- ## State Update (exit)
454
+ ## Stage exit
421
455
 
422
- 1. `<position> status=completed` (or `blocked` for save-and-exit).
423
- 2. `<timestamps> verify_completed_at=<ISO date now>`.
424
- 3. Update `last_checkpoint`. Write STATE.md.
456
+ 1. Call `mcp__gdd_state__update_progress` with `task_progress: "<verified>/<total>"` (the total is `state.must_haves.length` from the entry snapshot; verified is the count set to `pass`) and `status: "verify_complete"`.
457
+ 2. Call `mcp__gdd_state__set_status` with one of:
458
+ - `status: "pipeline_complete"` all must-haves passed and no outstanding gaps.
459
+ - `status: "verify_failed_requires_loop"` — gaps remain (save-and-exit, accept-as-is with fails, or auto-mode blocker).
460
+ 3. Call `mcp__gdd_state__checkpoint` — stamps `frontmatter.last_checkpoint` and appends a `verify_completed_at` timestamp entry. No direct STATE.md writes; the checkpoint tool owns the final persist.
425
461
 
426
462
  ---
427
463
 
@@ -1,329 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * budget-enforcer.js — PreToolUse hook (matcher: Agent)
4
- *
5
- * Intercepts every Agent tool spawn. Consults:
6
- * (a) router decision (from tool_input.context.router_decision if supplied)
7
- * (b) .design/cache-manifest.json for short-circuit cached answers (D-05)
8
- * (c) .design/budget.json for tier_overrides + caps (D-01, D-04, D-10)
9
- *
10
- * Enforcement (D-02, D-03, D-11):
11
- * - enforcement_mode: "enforce" + 100% cap → block with actionable error
12
- * - enforcement_mode: "enforce" + 80% soft-threshold + auto_downgrade_on_cap → rewrite tier to haiku
13
- * - enforcement_mode: "warn" → log warning, allow spawn
14
- * - enforcement_mode: "log" → advisory only
15
- *
16
- * Logs every decision to .design/telemetry/costs.jsonl (OPT-09 schema).
17
- * Every telemetry write fires a detached child aggregator (scripts/aggregate-agent-metrics.js)
18
- * that rebuilds .design/agent-metrics.json incrementally.
19
- *
20
- * Hook type: PreToolUse
21
- * Input: JSON on stdin { tool_name, tool_input }
22
- * Output: JSON on stdout { continue, suppressOutput, message, modified_tool_input? }
23
- */
24
-
25
- 'use strict';
26
-
27
- const fs = require('fs');
28
- const path = require('path');
29
- const readline = require('readline');
30
- const { spawn } = require('child_process');
31
-
32
- const BUDGET_PATH = path.join(process.cwd(), '.design', 'budget.json');
33
- const MANIFEST_PATH = path.join(process.cwd(), '.design', 'cache-manifest.json');
34
- const TELEMETRY_PATH = path.join(process.cwd(), '.design', 'telemetry', 'costs.jsonl');
35
- const PHASE_TOTALS_PATH = path.join(process.cwd(), '.design', 'telemetry', 'phase-totals.json');
36
- const STATE_PATH = path.join(process.cwd(), '.design', 'STATE.md');
37
-
38
- // ---- budget.json loader with defaults per D-12 ----
39
- function loadBudget() {
40
- const defaults = {
41
- per_task_cap_usd: 2.00,
42
- per_phase_cap_usd: 20.00,
43
- tier_overrides: {},
44
- auto_downgrade_on_cap: true,
45
- cache_ttl_seconds: 3600,
46
- enforcement_mode: 'enforce'
47
- };
48
- if (!fs.existsSync(BUDGET_PATH)) return defaults;
49
- try { return { ...defaults, ...JSON.parse(fs.readFileSync(BUDGET_PATH, 'utf8')) }; }
50
- catch { return defaults; }
51
- }
52
-
53
- // ---- cumulative phase spend (WR-02) ----
54
- // Reads from the lightweight phase-totals.json written by aggregate-agent-metrics.js
55
- // instead of replaying the full costs.jsonl on every hook invocation.
56
- // Falls back to 0 when the file doesn't exist yet (early in a session).
57
- function currentPhaseSpend(phase) {
58
- if (fs.existsSync(PHASE_TOTALS_PATH)) {
59
- try {
60
- const data = JSON.parse(fs.readFileSync(PHASE_TOTALS_PATH, 'utf8'));
61
- return Number(data.totals?.[phase] || 0);
62
- } catch { /* fall through */ }
63
- }
64
- // Fallback: replay JSONL when phase-totals.json not yet written (first spawn of session).
65
- if (!fs.existsSync(TELEMETRY_PATH)) return 0;
66
- const lines = fs.readFileSync(TELEMETRY_PATH, 'utf8').split(/\r?\n/).filter(Boolean);
67
- let sum = 0;
68
- for (const line of lines) {
69
- try {
70
- const row = JSON.parse(line);
71
- if (row.phase === phase) sum += Number(row.est_cost_usd || 0);
72
- } catch { /* tolerant */ }
73
- }
74
- return sum;
75
- }
76
-
77
- // ---- cycle + phase reader (STATE.md frontmatter) ----
78
- function readCycleAndPhase() {
79
- const defaults = { cycle: 'unknown', phase: 'unknown' };
80
- if (!fs.existsSync(STATE_PATH)) return defaults;
81
- try {
82
- const content = fs.readFileSync(STATE_PATH, 'utf8');
83
- // Match the first frontmatter block: between opening '---' and next '---'
84
- const fm = content.match(/^---\s*\n([\s\S]*?)\n---/);
85
- const body = fm ? fm[1] : content;
86
- const cycleMatch = body.match(/^cycle:\s*"?([^"\n]+)"?/m);
87
- const phaseMatch = body.match(/^phase:\s*"?([^"\n]+)"?/m);
88
- return {
89
- cycle: cycleMatch ? cycleMatch[1].trim() : 'unknown',
90
- phase: phaseMatch ? phaseMatch[1].trim() : 'unknown',
91
- };
92
- } catch {
93
- return defaults;
94
- }
95
- }
96
-
97
- // Deprecated alias for plan 01 callers (and any other hook/script that imports this).
98
- // Returns only the phase string; prefer readCycleAndPhase() for new code.
99
- function currentPhase() {
100
- return readCycleAndPhase().phase;
101
- }
102
-
103
- // ---- cache short-circuit (D-05) ----
104
- function cacheLookup(agent, inputHash) {
105
- if (!fs.existsSync(MANIFEST_PATH)) return null;
106
- try {
107
- const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8'));
108
- const entry = manifest.entries?.[`${agent}:${inputHash}`];
109
- if (!entry) return null;
110
- const age = Date.now() / 1000 - entry.ts_unix;
111
- if (age > (manifest.ttl_seconds || 3600)) return null;
112
- return entry.result; // cached blob
113
- } catch { return null; }
114
- }
115
-
116
- // ---- tier resolution (D-04) ----
117
- function resolveTier(agent, agentDefaultTier, overrides) {
118
- return overrides?.[agent] || agentDefaultTier || 'sonnet';
119
- }
120
-
121
- // ---- detached aggregator invocation ----
122
- // Fire-and-forget: do not block the hook. The aggregator reads costs.jsonl tail
123
- // and rewrites .design/agent-metrics.json atomically.
124
- function spawnAggregator() {
125
- try {
126
- const aggregatorPath = path.join(process.cwd(), 'scripts', 'aggregate-agent-metrics.js');
127
- if (!fs.existsSync(aggregatorPath)) return; // script not installed — fail open
128
- const child = spawn('node', [aggregatorPath], {
129
- cwd: process.cwd(),
130
- detached: true,
131
- stdio: 'ignore',
132
- env: { PATH: process.env.PATH }, // IN-02: minimal env; aggregator needs no secrets
133
- });
134
- child.unref();
135
- } catch {
136
- // Aggregator failures are non-fatal to the hook.
137
- }
138
- }
139
-
140
- // ---- locked-schema row builder (OPT-09) ----
141
- function buildTelemetryRow(partial) {
142
- const { cycle, phase } = partial._cyclePhase || readCycleAndPhase();
143
- // The nine mandatory fields per OPT-09, always in this order.
144
- const row = {
145
- ts: partial.ts || new Date().toISOString(),
146
- agent: String(partial.agent || 'unknown'),
147
- tier: String(partial.tier || 'unknown'),
148
- tokens_in: Number(partial.tokens_in || 0),
149
- tokens_out: Number(partial.tokens_out || 0),
150
- cache_hit: Boolean(partial.cache_hit),
151
- est_cost_usd: Number(partial.est_cost_usd || 0),
152
- cycle: partial.cycle || cycle,
153
- phase: partial.phase || phase,
154
- };
155
- // Optional diagnostic fields (Phase 11 reflector ignores unknown fields gracefully).
156
- if (partial.tier_downgraded !== undefined) row.tier_downgraded = Boolean(partial.tier_downgraded);
157
- if (partial.enforcement_mode !== undefined) row.enforcement_mode = String(partial.enforcement_mode);
158
- if (partial.lazy_skipped !== undefined) row.lazy_skipped = Boolean(partial.lazy_skipped);
159
- if (partial.block_reason !== undefined) row.block_reason = String(partial.block_reason);
160
- return row;
161
- }
162
-
163
- // ---- telemetry writer: append one JSON row to costs.jsonl ----
164
- function writeTelemetry(partial) {
165
- const dir = path.dirname(TELEMETRY_PATH);
166
- try {
167
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
168
- const row = buildTelemetryRow(partial);
169
- fs.appendFileSync(TELEMETRY_PATH, JSON.stringify(row) + '\n', 'utf8');
170
- // Fire-and-forget aggregator — rebuilds .design/agent-metrics.json incrementally.
171
- spawnAggregator();
172
- } catch {
173
- // Fail open: telemetry must never block the hook.
174
- }
175
- }
176
-
177
- // Backward-compat alias (plan 01 called it appendTelemetry; keep it working).
178
- const appendTelemetry = writeTelemetry;
179
-
180
- // ---- main ----
181
- async function main() {
182
- const rl = readline.createInterface({ input: process.stdin });
183
- let inputData = '';
184
- for await (const line of rl) inputData += line + '\n';
185
-
186
- let parsed;
187
- try { parsed = JSON.parse(inputData); } catch { process.exit(0); }
188
-
189
- if (parsed.tool_name !== 'Agent') process.exit(0); // only guard Agent spawns
190
-
191
- const toolInput = parsed.tool_input || {};
192
- const agent = toolInput.subagent_type || toolInput.agent || 'unknown';
193
- const inputHash = toolInput._input_hash || null; // supplied by orchestrator if cache-manager pre-computed it
194
-
195
- // Resolve cycle + phase once so every branch can stamp consistent values.
196
- const { cycle, phase } = readCycleAndPhase();
197
- const cyclePhase = { cycle, phase };
198
-
199
- // Branch A: lazy-gate signal from plan 10.1-04 agents (design-verifier-gate, etc.).
200
- // Gate agents set tool_input.lazy_skipped === true when the heuristic declines
201
- // to spawn the full checker. We log a zero-cost row and pass through.
202
- if (toolInput.lazy_skipped === true) {
203
- writeTelemetry({
204
- agent,
205
- tier: 'gate',
206
- tokens_in: 0,
207
- tokens_out: 0,
208
- cache_hit: false,
209
- est_cost_usd: 0,
210
- lazy_skipped: true,
211
- _cyclePhase: cyclePhase,
212
- });
213
- const response = { continue: true, suppressOutput: true };
214
- process.stdout.write(JSON.stringify(response));
215
- return;
216
- }
217
-
218
- const budget = loadBudget();
219
-
220
- // Branch B: cache short-circuit (D-05)
221
- if (inputHash) {
222
- const cached = cacheLookup(agent, inputHash);
223
- if (cached !== null) {
224
- writeTelemetry({
225
- agent,
226
- tier: 'cache',
227
- tokens_in: 0,
228
- tokens_out: 0,
229
- cache_hit: true,
230
- est_cost_usd: 0,
231
- _cyclePhase: cyclePhase,
232
- });
233
- const response = {
234
- continue: false, // block the real spawn; orchestrator reads suppressOutput.message for cached blob
235
- suppressOutput: false,
236
- message: `gdd-budget-enforcer: SkippedCached — returning cached result for ${agent}:${inputHash}`,
237
- cached_result: cached,
238
- };
239
- process.stdout.write(JSON.stringify(response));
240
- return;
241
- }
242
- }
243
-
244
- // Layer B: cap checks (D-02)
245
- const estCost = Number(toolInput._est_cost_usd || 0);
246
- const phaseSpend = currentPhaseSpend(phase);
247
-
248
- if (budget.enforcement_mode === 'enforce') {
249
- // Branch C: 100% per_task cap (hard block)
250
- if (estCost >= budget.per_task_cap_usd) {
251
- writeTelemetry({
252
- agent,
253
- tier: toolInput._tier_override || toolInput._default_tier || 'sonnet',
254
- tokens_in: Number(toolInput._tokens_in_est || 0),
255
- tokens_out: Number(toolInput._tokens_out_est || 0),
256
- cache_hit: false,
257
- est_cost_usd: estCost,
258
- enforcement_mode: budget.enforcement_mode,
259
- block_reason: 'per_task_cap',
260
- _cyclePhase: cyclePhase,
261
- });
262
- const response = {
263
- continue: false,
264
- suppressOutput: false,
265
- message: `Budget cap reached for per-task. Estimated: $${estCost.toFixed(4)}, cap: $${budget.per_task_cap_usd.toFixed(2)}. Raise cap in .design/budget.json or retry after next task.`,
266
- };
267
- process.stdout.write(JSON.stringify(response));
268
- return;
269
- }
270
- // Branch D: 100% per_phase cap (hard block)
271
- if (phaseSpend + estCost >= budget.per_phase_cap_usd) {
272
- writeTelemetry({
273
- agent,
274
- tier: toolInput._tier_override || toolInput._default_tier || 'sonnet',
275
- tokens_in: Number(toolInput._tokens_in_est || 0),
276
- tokens_out: Number(toolInput._tokens_out_est || 0),
277
- cache_hit: false,
278
- est_cost_usd: estCost,
279
- enforcement_mode: budget.enforcement_mode,
280
- block_reason: 'per_phase_cap',
281
- _cyclePhase: cyclePhase,
282
- });
283
- const response = {
284
- continue: false,
285
- suppressOutput: false,
286
- message: `Budget cap reached for per-phase (${phase}). Cumulative: $${(phaseSpend + estCost).toFixed(4)}, cap: $${budget.per_phase_cap_usd.toFixed(2)}. Raise cap in .design/budget.json or retry after next phase.`,
287
- };
288
- process.stdout.write(JSON.stringify(response));
289
- return;
290
- }
291
- // 80% soft-threshold downgrade (D-03): task-scoped, per reference/model-tiers.md
292
- if (budget.auto_downgrade_on_cap && estCost >= (0.80 * budget.per_task_cap_usd)) {
293
- toolInput._tier_override = 'haiku';
294
- toolInput._tier_downgraded = true;
295
- }
296
- } else if (budget.enforcement_mode === 'warn') {
297
- if (estCost >= budget.per_task_cap_usd) {
298
- process.stderr.write(`gdd-budget-enforcer WARN: per-task cap will be exceeded ($${estCost.toFixed(4)} >= $${budget.per_task_cap_usd})\n`);
299
- }
300
- }
301
- // enforcement_mode === 'log': no blocking, just telemetry
302
-
303
- // D-04: tier_overrides rewrite
304
- if (budget.tier_overrides[agent]) {
305
- toolInput._tier_override = budget.tier_overrides[agent];
306
- }
307
-
308
- // Branch E: standard spawn-allowed (includes tier-downgraded path D-03)
309
- writeTelemetry({
310
- agent,
311
- tier: toolInput._tier_override || toolInput._default_tier || 'sonnet',
312
- tokens_in: Number(toolInput._tokens_in_est || 0),
313
- tokens_out: Number(toolInput._tokens_out_est || 0),
314
- cache_hit: false,
315
- est_cost_usd: estCost,
316
- tier_downgraded: !!toolInput._tier_downgraded,
317
- enforcement_mode: budget.enforcement_mode,
318
- _cyclePhase: cyclePhase,
319
- });
320
-
321
- const response = {
322
- continue: true,
323
- suppressOutput: true,
324
- modified_tool_input: toolInput
325
- };
326
- process.stdout.write(JSON.stringify(response));
327
- }
328
-
329
- main().catch(err => { console.error('budget-enforcer hook error:', err); process.exit(0); });