@hegemonart/get-design-done 1.24.2 → 1.25.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.
@@ -0,0 +1,126 @@
1
+ 'use strict';
2
+
3
+ // scripts/lib/quality-gate-detect.cjs — quality-gate detection chain.
4
+ //
5
+ // Phase 25 Plan 25-09: promotes the doc-only auto-detection logic from
6
+ // skills/quality-gate/SKILL.md (Step 1, D-06) into a small testable
7
+ // JS module. Pure function, no I/O, no clock.
8
+ //
9
+ // The 3-tier resolution order (D-06):
10
+ //
11
+ // Tier 1 — Authoritative config:
12
+ // If the (already-loaded) `.design/config.json#quality_gate.commands`
13
+ // array is non-empty, return it verbatim. Skip all later tiers.
14
+ //
15
+ // Tier 2 — Auto-detect from package.json#scripts:
16
+ // If the (already-loaded) package.json#scripts object exists and is
17
+ // non-empty, intersect its keys with the canonical allowlist and
18
+ // emit `npm run <script>` for each match. The allowlist (case-
19
+ // sensitive, exact match):
20
+ //
21
+ // lint
22
+ // typecheck (or `tsc` as a substitute when `typecheck` is absent)
23
+ // test
24
+ // chromatic
25
+ // test:visual
26
+ //
27
+ // Hard exclusions (never included even if present):
28
+ //
29
+ // test:e2e (too slow for a Stage 4.5 gate)
30
+ // test:integration (only excluded when a separate `test` exists)
31
+ //
32
+ // Tier 3 — Skip with notice:
33
+ // Returns an empty array. Caller emits a `quality_gate_skipped`
34
+ // event and writes a `<run/>` with status="skipped".
35
+ //
36
+ // Mirrors the table in skills/quality-gate/SKILL.md verbatim. When the
37
+ // SKILL.md prose changes, change this module in lockstep — the SKILL is
38
+ // the design intent, this is the executable encoding consumers can test
39
+ // against.
40
+
41
+ /**
42
+ * Allowlisted script names (case-sensitive, exact match unless noted).
43
+ * Order matters: it determines the canonical command-list ordering, which
44
+ * in turn drives the deterministic `commands_run` field in events.jsonl
45
+ * and the STATE.md <run/> entry.
46
+ */
47
+ const ALLOWLIST = Object.freeze([
48
+ 'lint',
49
+ 'typecheck',
50
+ 'test',
51
+ 'chromatic',
52
+ 'test:visual',
53
+ ]);
54
+
55
+ /**
56
+ * Hard exclusions. Even when present in package.json#scripts, these are
57
+ * never run by the quality gate — they are too slow / orthogonal to the
58
+ * gate's purpose. Excluding `test:integration` only matters when a
59
+ * separate `test` script exists; we encode that invariant in detect().
60
+ */
61
+ const ALWAYS_EXCLUDED = Object.freeze(['test:e2e']);
62
+
63
+ /**
64
+ * Detection chain.
65
+ *
66
+ * @param {object} inputs
67
+ * @param {string[]|null|undefined} inputs.configCommands
68
+ * Value of `.design/config.json#quality_gate.commands`. `null` or
69
+ * empty array means "no config-side override; fall through to
70
+ * auto-detect". The caller is responsible for reading the file.
71
+ * @param {Record<string, string>|null|undefined} inputs.scripts
72
+ * Value of `package.json#scripts`. `null` means "no package.json".
73
+ * @returns {{commands: string[], tier: 1|2|3, reason?: string}}
74
+ * Detection result. `tier` is the tier that produced the
75
+ * commands (1 / 2 / 3 — see top of file). `reason` is populated
76
+ * on tier 3 only ("no commands resolved").
77
+ */
78
+ function detect(inputs) {
79
+ const configCommands = inputs && inputs.configCommands;
80
+ const scripts = inputs && inputs.scripts;
81
+
82
+ // --- Tier 1: authoritative config wins. ---
83
+ if (Array.isArray(configCommands) && configCommands.length > 0) {
84
+ return { commands: configCommands.slice(), tier: 1 };
85
+ }
86
+
87
+ // --- Tier 2: auto-detect from package.json#scripts. ---
88
+ if (scripts && typeof scripts === 'object') {
89
+ const detected = autoDetect(scripts);
90
+ if (detected.length > 0) {
91
+ return { commands: detected, tier: 2 };
92
+ }
93
+ }
94
+
95
+ // --- Tier 3: nothing resolved → skip with notice. ---
96
+ return { commands: [], tier: 3, reason: 'no commands resolved' };
97
+ }
98
+
99
+ /**
100
+ * Apply the allowlist to a `scripts` map. Pure.
101
+ */
102
+ function autoDetect(scripts) {
103
+ const out = [];
104
+ for (const name of ALLOWLIST) {
105
+ if (name === 'typecheck') {
106
+ // `typecheck` preferred; fall through to `tsc` only if absent.
107
+ if (Object.prototype.hasOwnProperty.call(scripts, 'typecheck')) {
108
+ out.push('npm run typecheck');
109
+ } else if (Object.prototype.hasOwnProperty.call(scripts, 'tsc')) {
110
+ out.push('npm run tsc');
111
+ }
112
+ continue;
113
+ }
114
+ if (Object.prototype.hasOwnProperty.call(scripts, name) && !ALWAYS_EXCLUDED.includes(name)) {
115
+ out.push(`npm run ${name}`);
116
+ }
117
+ }
118
+ return out;
119
+ }
120
+
121
+ module.exports = {
122
+ ALLOWLIST,
123
+ ALWAYS_EXCLUDED,
124
+ detect,
125
+ autoDetect,
126
+ };
@@ -0,0 +1,222 @@
1
+ ---
2
+ name: quality-gate
3
+ description: "Stage 4.5 of the pipeline. Detects, runs, and classifies project quality commands (lint / typecheck / test / visual-regression) between /gdd:design and /gdd:verify; writes the most recent run to STATE.md <quality_gate>. Non-blocking on timeout (warn + proceed); failures spawn design-fixer until the loop converges or max_iters is reached."
4
+ tools: Read, Write, Edit, Bash, Grep, Glob, Task
5
+ color: amber
6
+ model: inherit
7
+ default-tier: haiku
8
+ tier-rationale: "Orchestration of pre-detected commands and a downstream Haiku classifier. The skill itself does no synthesis — Bash runs do all the work, the classifier agent owns the routing decision."
9
+ size_budget: M
10
+ parallel-safe: conditional-on-touches
11
+ typical-duration-seconds: 180
12
+ reads-only: false
13
+ writes:
14
+ - ".design/STATE.md"
15
+ - ".design/events.jsonl"
16
+ ---
17
+
18
+ @reference/shared-preamble.md
19
+
20
+ # quality-gate
21
+
22
+ ## Role
23
+
24
+ You are the Stage 4.5 gate that runs between `/gdd:design` and `/gdd:verify`. You answer one question: *does this project's own quality tooling pass against the current working tree?*
25
+
26
+ You are NOT a design checker, an a11y checker, or a verifier. You are a thin façade over the project's existing `lint` / `typecheck` / `test` / visual-regression scripts. You exist so that the verify stage can refuse entry when those scripts fail (and so that the fix loop can be bounded and observable).
27
+
28
+ You write exactly two artifacts:
29
+ 1. The `<quality_gate>` block in `.design/STATE.md` (one most-recent `<run/>` element).
30
+ 2. Lifecycle events in `.design/events.jsonl` (per Step 6 below).
31
+
32
+ You never block on timeout. You never block on a "skipped" detection result. You only mark `status="fail"` when the fix loop reaches `max_iters` without converging — and even then it is the verify stage's job to refuse entry; YOU exit successfully so the user sees the report regardless.
33
+
34
+ ## Configuration Surface
35
+
36
+ Read once at start, from `.design/config.json` (all keys optional; defaults documented):
37
+
38
+ | Key | Default | Purpose |
39
+ |-----|---------|---------|
40
+ | `quality_gate.commands` | `null` | Authoritative list of commands. When provided, skips auto-detection. Each entry is a string the shell can run (e.g. `"npm run lint"`). |
41
+ | `quality_gate.timeout_seconds` | `600` | Total wall-clock budget for Step 2. On timeout: warn + proceed (D-07). |
42
+ | `quality_gate.max_iters` | `3` | Hard cap on Step 4 fix-loop iterations. |
43
+
44
+ Missing config file is not an error — defaults apply.
45
+
46
+ ## Step 1 — Detection chain
47
+
48
+ Per D-06, resolve the active command list with this 3-tier fallback. Stop at the first tier that produces ≥ 1 command:
49
+
50
+ ### Tier 1 — Authoritative config
51
+
52
+ If `.design/config.json` carries `quality_gate.commands` and the array is non-empty, use it verbatim. Skip Tier 2 and Tier 3.
53
+
54
+ ### Tier 2 — Auto-detect from `package.json#scripts`
55
+
56
+ If `package.json` exists at the project root, read its `scripts` object. Match script names against the following allowlist (case-sensitive, exact match unless noted):
57
+
58
+ | Script name | Notes |
59
+ |-------------|-------|
60
+ | `lint` | Always include if present. |
61
+ | `typecheck` | Always include if present. |
62
+ | `tsc` | Include if `typecheck` is absent (substitute, not duplicate). |
63
+ | `test` | Include if present. |
64
+ | `chromatic` | Include if present (visual-regression). |
65
+ | `test:visual` | Include if present (visual-regression). |
66
+
67
+ **Excluded by name** (intentionally — too slow for a Stage 4.5 gate):
68
+ - `test:e2e`
69
+ - `test:integration` (only if a separate `test` exists)
70
+ - Any script whose name starts with `dev:`, `build:`, `start:`.
71
+
72
+ For each matched script, the command to run is `npm run <script-name>` (use `pnpm run` or `yarn` only if the project's root carries a corresponding lockfile and the user's `.design/config.json` lists `quality_gate.package_manager`; otherwise default to `npm run` for portability).
73
+
74
+ If `package.json` does not exist, or `scripts` is empty, or no allowlisted name matches, advance to Tier 3.
75
+
76
+ ### Tier 3 — Skip with notice
77
+
78
+ Emit a `quality_gate_skipped` event with `reason: "no commands resolved"` (Step 6). Write a `<run/>` element with `status="skipped"`, `commands_run=""`, `iteration=0`, `started_at` and `completed_at` set to the same timestamp. Exit successfully with status `skipped`. The verify-entry gate (Plan 25-07 territory) does NOT block on `skipped`.
79
+
80
+ ## Step 2 — Parallel run
81
+
82
+ Open Step 2 by emitting `quality_gate_started` with the resolved command list (Step 6).
83
+
84
+ For each command produced by Step 1, spawn a **separate** `Bash` invocation; collect `{command, exit_code, stdout, stderr}` for each. Run them concurrently — the gate's wall-clock budget is the slowest command, not their sum.
85
+
86
+ The combined wall-clock budget is `quality_gate.timeout_seconds` (default 600). If the budget elapses before all commands complete:
87
+
88
+ 1. Emit `quality_gate_timeout` with the names of commands that did not finish.
89
+ 2. Mark `status="timeout"`, `commands_run=<comma-joined attempted names>`, and treat unfinished commands as having no failure to classify.
90
+ 3. Skip Step 3 / Step 4 (no fix loop on timeout — it would just compound the slowness).
91
+ 4. Proceed to Step 5 (STATE write) and Step 6 (final event).
92
+ 5. **Exit successfully.** Verify entry treats `timeout` as a warn, not a block.
93
+
94
+ If all commands complete within budget, advance to Step 3.
95
+
96
+ ## Step 3 — Classification
97
+
98
+ Spawn the `quality-gate-runner` agent via the `Task` tool. Pass an input payload of the shape:
99
+
100
+ ```json
101
+ {
102
+ "outputs": [
103
+ {"command": "npm run lint", "exit_code": 0, "stderr": ""},
104
+ {"command": "npm run typecheck", "exit_code": 1, "stderr": "<verbatim stderr>"},
105
+ {"command": "npm run test", "exit_code": 0, "stderr": ""}
106
+ ]
107
+ }
108
+ ```
109
+
110
+ The agent emits a single JSON object on stdout (see `agents/quality-gate-runner.md`):
111
+
112
+ ```json
113
+ {
114
+ "status": "pass" | "fail",
115
+ "classified_failures": {
116
+ "lint": ["…"],
117
+ "type": ["…"],
118
+ "test": ["…"],
119
+ "visual": ["…"]
120
+ }
121
+ }
122
+ ```
123
+
124
+ When `status === "pass"`, advance directly to Step 5 with `iteration` equal to the current loop counter (starts at `1` on the first pass).
125
+
126
+ When `status === "fail"`, advance to Step 4.
127
+
128
+ ## Step 4 — Fix loop (D-08)
129
+
130
+ If `iteration >= quality_gate.max_iters` (default 3), the loop is exhausted:
131
+ - Emit `quality_gate_fail` with the final classified failures.
132
+ - Mark `status="fail"`, persist the final `iteration`, and proceed to Step 5.
133
+ - **Exit successfully.** Verify entry refuses on `status="fail"`; YOU do not throw.
134
+
135
+ Otherwise, increment `iteration` and emit `quality_gate_iteration` with the current value. Spawn the existing `design-fixer` agent (Phase 5) via `Task` with classified failures as context — pass the same shape produced by Step 3 plus the original `outputs[]` for verbatim error context. After the fixer returns, restart from Step 2 (re-run all commands; do not prune to "only the previously failing ones" — fixes can introduce regressions in formerly-clean commands).
136
+
137
+ The loop terminates when either Step 3 returns `status="pass"` or `iteration` reaches `max_iters`.
138
+
139
+ ## Step 5 — STATE write
140
+
141
+ Open `.design/STATE.md`. Mutate the parsed state's `quality_gate` field to:
142
+
143
+ ```ts
144
+ {
145
+ run: {
146
+ started_at: <ISO 8601 — captured at Step 2 entry>,
147
+ completed_at: <ISO 8601 — now>,
148
+ status: <"pass" | "fail" | "timeout" | "skipped">,
149
+ iteration: <final loop counter>,
150
+ commands_run: <comma-joined names of commands that completed>,
151
+ extra_attrs: {},
152
+ },
153
+ }
154
+ ```
155
+
156
+ Persist via `mcp__gdd_state__set_quality_gate` (the underlying mutator wiring is named in this contract; the SDK MCP layer wraps every mutator method, so the surface inherits free from the parser/mutator extension landed in this plan). Until the MCP tool exists (Plan 25-07 surfaces it in the verify-stage integration), use the `apply()` mutator from `scripts/lib/gdd-state/mutator.ts` directly:
157
+
158
+ ```ts
159
+ apply(raw, (state) => {
160
+ state.quality_gate = { run };
161
+ return state;
162
+ });
163
+ ```
164
+
165
+ Either path is acceptable. The on-disk shape is identical.
166
+
167
+ ## Step 6 — Event emission (D-09)
168
+
169
+ Emit lifecycle events to `.design/events.jsonl` via the existing `appendEvent()` surface exported from `scripts/lib/event-stream/index.ts` — the same module Phase 22 telemetry, the budget-enforcer, the read-injection scanner, and the gdd-state MCP server already write through. Do not roll a bespoke writer; the singleton in `event-stream/index.ts` is persist-first / broadcast-second and never throws on the persist path, which is the contract this skill relies on.
170
+
171
+ Import shape:
172
+
173
+ ```ts
174
+ import { appendEvent } from '../../scripts/lib/event-stream/index.ts';
175
+ ```
176
+
177
+ Each emission is a single `appendEvent({...})` call with `type` set to one of the six names in the table below. Pass the event-specific payload fields verbatim — `appendEvent` stamps `_meta` (pid, host, source) and the JSONL writer captures the canonical `ts` from the writer surface. The `cycle` and `stage` fields are stamped by the same path used elsewhere in Phase 22+ (consumers match on `type`; treat `ts`, `cycle`, `stage` as injected, not caller-supplied).
178
+
179
+ One event per JSONL line. Schema and lifecycle map:
180
+
181
+ | Event | When (lifecycle position) | Required fields |
182
+ |-------|---------------------------|-----------------|
183
+ | `quality_gate_started` | Step 2 entry — fired ONCE per skill invocation, immediately before any `Bash` spawn. Carries the resolved command list from Step 1 so downstream telemetry can correlate `started` → terminal event. | `commands` (string[]), `timeout_seconds` (number), `max_iters` (number) |
184
+ | `quality_gate_iteration` | Step 4 entry — fired ONCE per retry, with `iteration` set to the new (post-increment) loop counter. The first run is implicit (covered by `started`); only retries `≥ 2` emit `iteration`. | `iteration` (int ≥ 2) |
185
+ | `quality_gate_pass` | Step 3 returned `status: "pass"` — terminal happy path. Fires before Step 5 (STATE write) so a consumer tailing the stream sees the verdict before the on-disk run record. | `iteration` (final loop counter), `commands_run` (string[]) |
186
+ | `quality_gate_fail` | Step 4 reached `max_iters` without convergence — terminal failure path. The verify-entry gate (Step 2.5 of `skills/verify/SKILL.md`) is the sole consumer that *acts* on this; this skill exits successfully regardless. | `iteration` (final loop counter, equal to `max_iters`), `classified_failures` (object — same shape as `quality-gate-runner` agent output) |
187
+ | `quality_gate_timeout` | Step 2 wall-clock budget elapsed — terminal warn path (per D-07 verify treats this as a warning, not a block). Fires before Step 5 STATE write, same ordering as `pass`/`fail`. | `unfinished_commands` (string[]) |
188
+ | `quality_gate_skipped` | Step 1 Tier 3 fired (no commands resolved) — terminal no-op path. Fires before the synthetic `<run/>` is written to STATE.md. | `reason` (string — e.g. `"no commands resolved"`) |
189
+
190
+ All six events carry the standard `ts`, `cycle`, `stage` fields injected by `appendEvent` / the writer. Do not invent additional event names — the verify-entry gate, reflector, and Phase 22 telemetry consumers match on this exact list. Do not emit any of these names from any path other than the lifecycle positions above (e.g., do not emit `quality_gate_started` again on a Step 4 retry — that's what `quality_gate_iteration` is for).
191
+
192
+ **Failure-mode contract:** `appendEvent()` swallows persist failures internally. If the writer cannot open `.design/events.jsonl`, the skill MUST still proceed — the event stream is observability, not correctness. The STATE.md write in Step 5 is the durable record consumers MUST rely on; events.jsonl is the supplementary timeline.
193
+
194
+ ## Output Contract
195
+
196
+ Emit a single JSON object on stdout summarizing the run for the caller:
197
+
198
+ ```json
199
+ {
200
+ "status": "pass",
201
+ "iteration": 1,
202
+ "commands_run": ["npm run lint", "npm run typecheck", "npm run test"],
203
+ "started_at": "2026-04-29T10:00:00Z",
204
+ "completed_at": "2026-04-29T10:01:42Z"
205
+ }
206
+ ```
207
+
208
+ Schema:
209
+ - `status` — `pass | fail | timeout | skipped`.
210
+ - `iteration` — final loop counter; `0` for `skipped`.
211
+ - `commands_run` — array of command strings actually executed.
212
+ - `started_at` / `completed_at` — ISO 8601, copied from the STATE write.
213
+
214
+ The skill exits with shell exit code `0` on every terminal status — including `fail`. The verify-entry gate is the sole consumer of the `fail` status; this skill never throws to the orchestrator.
215
+
216
+ ## Constraints
217
+
218
+ - **Do not** prune the command list across iterations — always re-run everything in Step 2.
219
+ - **Do not** spawn `quality-gate-runner` more than once per loop iteration. Spawn `design-fixer` more than once if and only if the loop iterates.
220
+ - **Do not** read or write any STATE block other than `<quality_gate>` and `<position>` (the latter only as required by the standard write contract; the gate is a checkpoint, not a stage transition, so `<position>` updates are limited to `last_checkpoint`).
221
+ - **Do not** invoke verify or design — Stage 4.5 sits strictly between them.
222
+ - Treat exit codes via the standard convention: `0` = clean; non-zero = failure to be classified. Do not interpret stderr content for the pass/fail decision — the agent does that classification, you do not.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: gdd-router
3
- description: "Routes a /gdd command to fast|quick|full path and returns {path, model_tier_overrides, estimated_cost_usd, cache_hits}. Deterministic — no model call. Invoked once at command entry before any Agent spawn. Read by hooks/budget-enforcer.js."
3
+ description: "Routes a /gdd command to fast|quick|full path + S|M|L|XL complexity_class and returns {path, complexity_class, model_tier_overrides, estimated_cost_usd, cache_hits}. Deterministic — no model call. Invoked once at command entry before any Agent spawn. Read by hooks/budget-enforcer.js."
4
4
  argument-hint: "<intent-string> [<target-artifacts-csv>]"
5
5
  tools: Read, Bash, Grep
6
6
  ---
@@ -18,25 +18,45 @@ You are a deterministic routing skill. You do not spawn agents. You read `.desig
18
18
  ```json
19
19
  {
20
20
  "path": "fast",
21
+ "complexity_class": "M",
21
22
  "model_tier_overrides": {"design-verifier": "haiku"},
22
23
  "estimated_cost_usd": 0.034,
23
24
  "cache_hits": ["design-context-builder:abc123"]
24
25
  }
25
26
  ```
26
- - `path` enum: `fast` (single Haiku + no checkers), `quick` (Sonnet mappers + Haiku verify), `full` (Opus planners + full quality gates).
27
+ - `path` enum: `fast` (single Haiku + no checkers), `quick` (Sonnet mappers + Haiku verify), `full` (Opus planners + full quality gates). Stays unchanged for back-compat per D-05.
28
+ - `complexity_class` enum: `S | M | L | XL` (Phase 25 / D-04, D-05). Additive to `path` — existing consumers reading only `path` keep working. Mapping is documented in the Path Selection Heuristic table below.
27
29
  - `model_tier_overrides` merges agent frontmatter `default-tier` with `.design/budget.json.tier_overrides` — budget.json wins per D-04.
28
30
  - `estimated_cost_usd` is the sum of per-spawn estimates using the D-06 formula and `reference/model-prices.md`.
29
31
  - `cache_hits` is a list of `{agent}:{input-hash}` strings that exist in `.design/cache-manifest.json` and are within TTL; emitting a hit lets the hook short-circuit that spawn per D-05.
30
32
 
31
33
  ## Path Selection Heuristic
32
34
 
33
- | Signal | path |
34
- |--------|------|
35
- | Command is `/gdd:scan`, `/gdd:stats`, `/gdd:health`, `/gdd:help` | `fast` |
36
- | Command spawns exactly one agent (no orchestration) | `fast` |
37
- | Command spawns parallel mappers but no planners/auditors (`/gdd:discover` in `--auto` mode) | `quick` |
38
- | Command spawns planners, auditors, verifiers, or integration-checkers (`/gdd:plan`, `/gdd:verify`, `/gdd:audit`) | `full` |
39
- | `--dry-run` flag present on any command | downgrade one tier (fast↔quick↔full) |
35
+ The router emits both `path` (legacy 3-tier enum) and `complexity_class` (Phase 25 4-tier enum). The canonical mapping is:
36
+
37
+ | complexity_class | path | Behavior |
38
+ |------------------|------|----------|
39
+ | `S` | `fast` (short-circuited) | Skip router itself, skip cache-manager, skip telemetry write. Deterministic no-op decision. |
40
+ | `M` | `fast` | Single Haiku + no checkers. |
41
+ | `L` | `quick` | Sonnet mappers + Haiku verify. |
42
+ | `XL` | `full` | Opus planners + full quality gates. Recommends worktree-isolation default + mandatory inter-stage checkpoint + reflector auto-spawn. |
43
+
44
+ Bucket assignment:
45
+
46
+ | Signal | complexity_class | path |
47
+ |--------|------------------|------|
48
+ | Command is `/gdd:help`, `/gdd:stats`, `/gdd:note`, `/gdd:health`, single-Haiku skill | `S` | `fast` (short-circuited — see below) |
49
+ | Command is `/gdd:scan`, `/gdd:brief`, `/gdd:sketch`, `/gdd:spike`, `/gdd:fast` | `M` | `fast` |
50
+ | Command spawns exactly one agent (no orchestration), not in S list | `M` | `fast` |
51
+ | Command is `/gdd:explore`, `/gdd:discover`, standalone `/gdd:verify`, standalone `/gdd:plan` | `L` | `quick` |
52
+ | Command spawns parallel mappers but no planners/auditors (`/gdd:discover` in `--auto` mode) | `L` | `quick` |
53
+ | Command is `/gdd:next`, `/gdd:do`, `/gdd:autonomous`, end-to-end Brief→Verify, anything spawning planners + auditors + verifiers in series | `XL` | `full` |
54
+ | Command spawns planners, auditors, verifiers, or integration-checkers (`/gdd:plan`, `/gdd:verify`, `/gdd:audit`) and is not standalone | `XL` | `full` |
55
+ | `--dry-run` flag present on any command | downgrade one tier (XL→L→M→S; `path` follows the mapping table) |
56
+
57
+ ### S-class short-circuit
58
+
59
+ When `complexity_class` would be `S`, the router itself **does not run** for that invocation — the deterministic skip list is encoded in the `/gdd:*` SKILL.md entry of the matching command. The budget-enforcer hook treats "no router decision payload + matching command name" as the S-class signal and skips enforcement entirely (no telemetry row, no cache lookup, no event emission). When the router *is* invoked explicitly (e.g., debugging) it still emits `complexity_class: "S"` in the JSON for observability, but the runtime path is the no-op.
40
60
 
41
61
  ## Cost Estimation Algorithm
42
62
 
@@ -62,11 +62,54 @@ Write `.design/sketches/<slug>/WINNER.md`:
62
62
  **Project skill written to**: ./.claude/skills/design-<area>-conventions.md
63
63
  ```
64
64
 
65
- ## Step 7 — Update sketches SUMMARY.md
65
+ ## Step 7 — Append D-XX + `<prototyping>` outcome to STATE.md
66
+
67
+ Two coupled writes to `.design/STATE.md`. Both must succeed together so the
68
+ sketch resolution is discoverable from both `<decisions>` (read by all
69
+ downstream stages) and `<prototyping>` (read by planner-specific context via
70
+ the decision-injector).
71
+
72
+ Compute `D-XX` as the highest existing `D-NN` in `<decisions>` plus 1
73
+ (scan `<decisions>` for `D-\d+:` entries and take `max + 1`, zero-padded
74
+ to two digits — e.g. existing `D-07` → new entry is `D-08`). Use the same
75
+ `D-XX` value in both writes below.
76
+
77
+ **Write 1 — append a numbered decision under `<decisions>`:**
78
+ ```
79
+ D-XX: sketch/<slug> — winner: variant-N — <one-line rationale> (locked)
80
+ Source: .design/sketches/<slug>/WINNER.md
81
+ ```
82
+
83
+ **Write 2 — append a `<sketch>` child element under `<prototyping>`:**
84
+ ```
85
+ <sketch slug="<slug>" cycle="<cycle>" decision="D-XX" status="resolved"/>
86
+ ```
87
+
88
+ `<cycle>` is the current cycle id from `.design/STATE.md` frontmatter
89
+ (`cycle:` field; empty string is valid for Wave A single-cycle projects).
90
+
91
+ If a `<prototyping>` block does not yet exist in STATE.md, materialize it
92
+ between `<must_haves>` and `<connections>` per the STATE template, then
93
+ append the `<sketch …/>` line as its first child. The block is omitted on
94
+ fresh files and only appears once the first sketch / spike / skipped entry
95
+ lands.
96
+
97
+ If MCP `gdd_state` tools are available, prefer the typed mutators (these
98
+ wrap `scripts/lib/gdd-state/mutator.ts` and emit byte-identical output to
99
+ manual edits):
100
+ ```
101
+ - mcp__gdd_state__add_decision({id: "D-XX", text: "sketch/<slug> — winner: variant-N — <rationale>", status: "locked"})
102
+ - mcp__gdd_state__add_prototyping({type: "sketch", slug: "<slug>", cycle: "<cycle>", decision: "D-XX", status: "resolved"})
103
+ ```
104
+
105
+ Without MCP, edit `.design/STATE.md` directly with `Read` + `Write`,
106
+ inserting the two lines into the correct blocks.
107
+
108
+ ## Step 8 — Update sketches SUMMARY.md
66
109
 
67
110
  Append entry to `.design/sketches/SUMMARY.md` (create if missing):
68
111
  ```markdown
69
- - <slug> (YYYY-MM-DD) — winner: variant-N — area: <area> — <one-line rationale>
112
+ - <slug> (YYYY-MM-DD) — winner: variant-N — area: <area> — D-XX — <one-line rationale>
70
113
  ```
71
114
 
72
115
  ## After writing
@@ -76,6 +119,8 @@ Append entry to `.design/sketches/SUMMARY.md` (create if missing):
76
119
  Slug: <slug>
77
120
  Winner: variant-N
78
121
  Area: <area>
122
+ Decision recorded: D-XX
123
+ Prototyping entry: <sketch slug="<slug>" cycle="<cycle>" decision="D-XX" status="resolved"/>
79
124
  Project skill: ./.claude/skills/design-<area>-conventions.md
80
125
  ━━━━━━━━━━━━━━━━━━━━━
81
126
  ```
@@ -53,9 +53,47 @@ D-XX: spike/<slug> — <verdict> — <recommendation>
53
53
  Source: .design/spikes/<slug>/FINDINGS.md
54
54
  ```
55
55
 
56
- (Increment D-XX from the highest existing number.)
56
+ (Increment D-XX from the highest existing number — scan `<decisions>` for
57
+ `D-\d+:` entries and take `max + 1`, zero-padded to two digits.)
57
58
 
58
- ## Step 6 Update spikes SUMMARY.md
59
+ If MCP `gdd_state` tools are available, prefer the typed mutator:
60
+ ```
61
+ - mcp__gdd_state__add_decision({id: "D-XX", text: "spike/<slug> — <verdict> — <recommendation> — <one-line rationale>", status: "locked"})
62
+ ```
63
+
64
+ ## Step 6 — Append `<prototyping>` outcome to STATE.md
65
+
66
+ Coupled with the Step 5 decision write — both must succeed together so the
67
+ spike resolution is discoverable from both `<decisions>` (read by all
68
+ downstream stages) and `<prototyping>` (read by planner-specific context via
69
+ the decision-injector). Use the **same `D-XX`** as Step 5.
70
+
71
+ Append a `<spike>` child element under `<prototyping>` in `.design/STATE.md`:
72
+ ```
73
+ <spike slug="<slug>" cycle="<cycle>" decision="D-XX" verdict="yes|no|partial" status="resolved"/>
74
+ ```
75
+
76
+ `<cycle>` is the current cycle id from `.design/STATE.md` frontmatter
77
+ (`cycle:` field; empty string is valid for Wave A single-cycle projects).
78
+ `verdict` is the answer from Step 3 (`yes` / `no` / `partial`).
79
+
80
+ If a `<prototyping>` block does not yet exist in STATE.md, materialize it
81
+ between `<must_haves>` and `<connections>` per the STATE template, then
82
+ append the `<spike …/>` line as its first child. The block is omitted on
83
+ fresh files and only appears once the first sketch / spike / skipped entry
84
+ lands.
85
+
86
+ If MCP `gdd_state` tools are available, prefer the typed mutator (it wraps
87
+ `scripts/lib/gdd-state/mutator.ts` and emits byte-identical output to manual
88
+ edits):
89
+ ```
90
+ - mcp__gdd_state__add_prototyping({type: "spike", slug: "<slug>", cycle: "<cycle>", decision: "D-XX", verdict: "<verdict>", status: "resolved"})
91
+ ```
92
+
93
+ Without MCP, edit `.design/STATE.md` directly with `Read` + `Write`,
94
+ inserting the line into the `<prototyping>` block.
95
+
96
+ ## Step 7 — Update spikes SUMMARY.md
59
97
 
60
98
  Append entry to `.design/spikes/SUMMARY.md` (create if missing):
61
99
  ```markdown
@@ -69,6 +107,7 @@ Append entry to `.design/spikes/SUMMARY.md` (create if missing):
69
107
  Slug: <slug>
70
108
  Verdict: <verdict>
71
109
  Decision recorded: D-XX
110
+ Prototyping entry: <spike slug="<slug>" cycle="<cycle>" decision="D-XX" verdict="<verdict>" status="resolved"/>
72
111
  FINDINGS.md written.
73
112
  ━━━━━━━━━━━━━━━━━━━━
74
113
  ```
@@ -0,0 +1,115 @@
1
+ ---
2
+ name: gdd-turn-closeout
3
+ description: "Portable mirror of the gdd-turn-closeout Stop hook (D-11). Closes the events.jsonl gap at turn-end and surfaces a stage-completion or paused-mid-task nudge. Tail-called by orchestrator skills (/gdd:next, /gdd:design, /gdd:verify) at exit on the 13 non-Claude runtimes that lack a Stop hook surface. Idempotent, non-blocking, ≤10ms typical."
4
+ argument-hint: "(none — reads .design/STATE.md and .design/telemetry/events.jsonl from cwd)"
5
+ tools: Read, Bash
6
+ ---
7
+
8
+ # gdd-turn-closeout
9
+
10
+ ## Role
11
+
12
+ You are a deterministic **closeout** skill. You close the per-turn telemetry gap on runtimes that don't expose a Stop event (codex, gemini, and 11 others). You are a code-level mirror of `hooks/gdd-turn-closeout.js` (D-10): same conditions, same idempotence, same emitted event shape. The only difference: the JS hook emits the nudge as `additionalContext` via the harness; this skill prints the nudge directly to the user.
13
+
14
+ **When to invoke:** orchestrator skills (`/gdd:next`, `/gdd:design`, `/gdd:verify`) tail-call this skill as their final step before returning, so the user sees a closing nudge that matches what Claude Code users see via the Stop hook. Adoption is incremental — each orchestrator can wire the tail-call independently; the skill exists as a stable, callable surface today.
15
+
16
+ ## Invocation Contract
17
+
18
+ - **Input**: none. Operates on `.design/STATE.md` and `.design/telemetry/events.jsonl` in the current working directory.
19
+ - **Output**: at most one printed line — the nudge — or silent return.
20
+ - **Latency budget**: ≤10ms typical (matches D-10). Read **only** STATE.md and the tail of events.jsonl; never load the full event stream.
21
+ - **Idempotence**: if the most recent event line is already a `turn_end` for the current `(stage, task_progress)` tuple, skip the append but still print the nudge.
22
+ - **Non-blocking**: any I/O failure → silent return. This skill must never gate the user.
23
+
24
+ ## Algorithm
25
+
26
+ Execute these steps **in order** and stop at the first early-return.
27
+
28
+ ### Step 1 — Try to read STATE.md
29
+
30
+ Read `.design/STATE.md`. If the file is missing or unreadable: **return silently** (no print, no append). Mirrors the JS hook's "missing STATE.md" branch.
31
+
32
+ ### Step 2 — Parse the `<position>` block
33
+
34
+ Lightweight-parse only the `<position>…</position>` block (the rest of STATE.md is irrelevant here). Extract `stage`, `status`, `task_progress`. A regex pass (`/<position>([\s\S]*?)<\/position>/` then per-line `key: value`) is sufficient — do **not** invoke the full STATE parser (cost overhead).
35
+
36
+ If `status != "in_progress"`: **return silently**. The pipeline is either initialized, completed, or blocked — no turn-end gap to close.
37
+
38
+ ### Step 3 — Tail the last event line
39
+
40
+ Read **only the last 8 KiB** of `.design/telemetry/events.jsonl` (a single event line is ≪64 KiB). Treat all of these as "stale by definition":
41
+ - The file is missing.
42
+ - The file is empty.
43
+ - The last line fails to parse as JSON.
44
+ - The last line's `timestamp` is missing or unparseable.
45
+
46
+ Otherwise compute `now - last_event.timestamp`. If the gap is **<60 seconds**, the user is actively mid-turn — **return silently** (the next real event will close the gap naturally).
47
+
48
+ A reasonable Bash one-liner for the tail when running this skill in a runtime that lacks a Read-tail primitive: `tail -n 1 .design/telemetry/events.jsonl 2>/dev/null`.
49
+
50
+ ### Step 4 — Idempotence check, then append
51
+
52
+ If the last event is already shaped `{type: "turn_end", stage: <same>, payload: {task_progress: <same>}}` for the **exact** `(stage, task_progress)` tuple from Step 2: **skip the append** but proceed to Step 5.
53
+
54
+ Otherwise append a single JSONL line to `.design/telemetry/events.jsonl`:
55
+
56
+ ```json
57
+ {"type":"turn_end","timestamp":"<ISO 8601 now>","sessionId":"<session-id-or-'turn-closeout'>","stage":"<stage>","payload":{"task_progress":"<N/M>"},"_meta":{"source":"gdd-turn-closeout-skill"}}
58
+ ```
59
+
60
+ Create `.design/telemetry/` if missing. The append must be a single `appendFile`-equivalent call (the writer assumes append-atomicity per Plan 20-06).
61
+
62
+ ### Step 5 — Print the nudge
63
+
64
+ Match `task_progress` against `^(\d+)/(\d+)$`:
65
+
66
+ - **Numerator equals denominator and denominator > 0** (e.g. `5/5`, stage-complete):
67
+
68
+ > Stage `<stage>` complete — run `/gdd:next` or `/gdd:reflect`
69
+
70
+ - **Otherwise** (mid-task, e.g. `3/7`, `0/0`, malformed):
71
+
72
+ > Stage `<stage>` paused mid-task — resume with `/gdd:resume`
73
+
74
+ Print exactly one of these two lines. No additional commentary, no explanations of what the skill did — the nudge is the user-facing surface.
75
+
76
+ ## Failure Modes
77
+
78
+ Every step above has an explicit silent-return on failure. The skill must remain non-blocking under all conditions:
79
+
80
+ | Condition | Behavior |
81
+ |-----------|----------|
82
+ | `.design/STATE.md` missing or unreadable | Silent return |
83
+ | `<position>` block absent or malformed | Silent return |
84
+ | `status != "in_progress"` | Silent return |
85
+ | `.design/telemetry/events.jsonl` missing | Treat as stale → fall through to append + nudge |
86
+ | Last event line unparseable | Treat as stale → fall through |
87
+ | Last event timestamp <60s old | Silent return |
88
+ | Append fails (permission, disk full) | Print the nudge anyway; do not surface the I/O error |
89
+ | Any uncaught throw at any step | Silent return |
90
+
91
+ ## Equivalence with the JS hook
92
+
93
+ This skill and `hooks/gdd-turn-closeout.js` MUST stay code-level equivalent. Specifically:
94
+
95
+ - Same four early-return branches (no STATE / not in_progress / fresh event / no-op).
96
+ - Same staleness threshold: **60 seconds**.
97
+ - Same idempotence guard: `(type=turn_end, stage, payload.task_progress)` triple.
98
+ - Same emitted event shape (only `_meta.source` differs: `gdd-turn-closeout` vs `gdd-turn-closeout-skill`, so reflector telemetry can distinguish hook-driven vs skill-driven turn-ends).
99
+ - Same nudge wording for both `N/N` and mid-task cases.
100
+
101
+ If you change one, change the other in the same plan. Plan 25-09's `tests/turn-closeout-hook.test.cjs` covers the JS hook; the parallel coverage for this skill rides on Plan 25-09's Phase 25 baseline.
102
+
103
+ ## Non-Goals
104
+
105
+ - **Not a state writer.** This skill never edits STATE.md. The events.jsonl append is the only side effect.
106
+ - **Not a stage transition.** A `turn_end` event is a within-stage observation, not a state-machine move; downstream tools that gate on stage transitions ignore it.
107
+ - **Not a Stop-event harness.** Cross-runtime Stop-event support at the harness level is explicit out-of-scope for Phase 25 (see CONTEXT.md OOS section).
108
+
109
+ ## Integration Point
110
+
111
+ The canonical tail-call sites (per D-11) are `/gdd:next`, `/gdd:design`, `/gdd:verify`. Each orchestrator's final step, immediately before returning to the user, should be:
112
+
113
+ > Invoke skill `gdd-turn-closeout`.
114
+
115
+ Tail-call wiring is intentionally not part of v1.25 (Plan 25-04 ships only the callable surface). Each orchestrator can adopt the wiring independently in a follow-up.