baldart 4.70.0 → 4.72.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.
- package/CHANGELOG.md +35 -0
- package/VERSION +1 -1
- package/framework/.claude/agents/code-reviewer.md +9 -0
- package/framework/.claude/agents/prd-card-writer.md +20 -0
- package/framework/.claude/agents/visual-fidelity-verifier.md +17 -1
- package/framework/.claude/skills/e2e-review/SKILL.md +58 -6
- package/framework/.claude/skills/new/references/commit.md +1 -1
- package/framework/.claude/skills/new/references/metrics.md +2 -2
- package/framework/.claude/skills/new/references/team-mode.md +2 -2
- package/framework/.claude/skills/new2/SKILL.md +11 -2
- package/framework/.claude/skills/prd/assets/card-template.yml +11 -0
- package/framework/.claude/skills/prd/assets/prd-template.md +4 -0
- package/framework/.claude/skills/prd/references/backlog-phase.md +11 -0
- package/framework/.claude/skills/prd/references/prd-writing-phase.md +5 -0
- package/framework/.claude/workflows/new2.js +10 -2
- package/framework/agents/card-schema.md +29 -0
- package/framework/agents/design-review.md +3 -0
- package/framework/agents/design-system-protocol.md +31 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,41 @@ All notable changes to BALDART will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [4.72.0] - 2026-06-25
|
|
9
|
+
|
|
10
|
+
**Session-id provenance on PRDs and cards — trace an implementation bug back to the chat history that produced it.** When `/new` ships a card that turns out wrong, there was no link from the card back to *which Claude Code session* planned it (`/prd`) or implemented it (`/new`/`/new2`) — so you couldn't reopen the transcript to see the steps the session took (or skipped). Now every card carries an optional `provenance` block recording the session UUID for each lifecycle stage, and the PRD frontmatter records its authoring session. Resolve the chat history at `~/.claude/projects/*/<session-id>.jsonl` (the UUID is globally unique — no project slug needed). This turns "this feature is broken, why?" into a concrete pointer at the planning/implementation conversation, for improving the framework from real runs.
|
|
11
|
+
|
|
12
|
+
The enabling fact (verified against a live `env`): the session id IS available at runtime as **`CLAUDE_CODE_SESSION_ID`** — NOT `CLAUDE_SESSION_ID` (which does not exist). The capture discipline matters: the value is read by the **orchestrator** (`/prd`, `/new`, the `new2` skill) in its own context and passed down — never read inside a writer/commit subagent, whose own session id differs (`CLAUDE_CODE_CHILD_SESSION=1`).
|
|
13
|
+
|
|
14
|
+
**MINOR** — adds a traceability capability; **NOT a `baldart.config.yml` key** (it is a card-schema field + a native env var), so the schema-change propagation rule does NOT apply. The `provenance` field is **optional (`C`)** in every card profile — the validator ignores it and legacy cards never break (verified by running `validate-card-baseline.js` on cards with and without the block). Sub-fields are omitted when the env var is unset; an EPIC card (a tracker) carries `planning_session` only.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`provenance` card field** (`framework/agents/card-schema.md` — the schema SSOT) — `{ planning_session, implementation_session }`, both optional session UUIDs, added to the field-state matrix as `C`/`C`/`C` with a dedicated `§ provenance` section defining ownership-by-lifecycle and the `CLAUDE_CODE_SESSION_ID` resolution rule. Mirrored in the card template (`framework/.claude/skills/prd/assets/card-template.yml`) and a `planning_session` frontmatter line in the PRD template (`framework/.claude/skills/prd/assets/prd-template.md`).
|
|
19
|
+
- **`session_id` on the `/new` telemetry row** (`framework/.claude/skills/new/references/metrics.md`) — `skill-runs.jsonl` records now carry the run's session UUID so the metrics row also resolves to its transcript.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **`/prd` stamps the planning session** — `prd-card-writer` (`framework/.claude/agents/prd-card-writer.md`) sets `provenance.planning_session` from a new `PLANNING_SESSION_ID` field the `/prd` orchestrator passes (captured in the orchestrator's own context, `backlog-phase.md`), and `/prd` writes the same id into the PRD frontmatter (`prd-writing-phase.md`). The writer never reads the env var itself (subagent session ≠ conversation session).
|
|
24
|
+
- **`/new` + `/new2` stamp the implementation session** when a card reaches `status: DONE` — `/new` at `commit.md` Step 28 (`framework/.claude/skills/new/references/commit.md`); `/new2` via a new `sessionId` workflow arg captured by the skill and stamped by `new2.js` at the per-card commit step, the cross-card integration commit, and the skill's post-run deferred-card reconciliation — always merging into the existing `provenance` block, never overwriting `planning_session`.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **`team-mode.md` used the non-existent `CLAUDE_SESSION_ID`** (`framework/.claude/skills/new/references/team-mode.md`) — the subagent-transcript locator (`ls ~/.claude/projects/*/${CLAUDE_SESSION_ID:-*}/subagents`) always fell through to the wildcard fallback because the variable name was wrong. Corrected to `CLAUDE_CODE_SESSION_ID` so it resolves the live session's `subagents/` dir directly.
|
|
29
|
+
|
|
30
|
+
## [4.71.0] - 2026-06-25
|
|
31
|
+
|
|
32
|
+
**Responsive-awareness in UI review — catch a viewport-scoped edit that regresses the *other* viewport.** When an agent works on one viewport (fix the mobile header, tweak the desktop table), nothing today catches it silently shifting the viewport it was *not* editing. The obvious fix — cloning the i18n ESLint gate into a `responsive-gate` that bans raw `matchMedia`/breakpoint literals — was **refuted** by an adversarial pass: that gate is blind to where responsiveness actually lives (Tailwind `md:` classes, CSS, CSS-in-JS — not `window.matchMedia`), so it is mayo-specific opinion, not a distributable mechanism. The real driver is served by **reactivating machinery that already exists and was inert**: `visual-fidelity-verifier` has had a `responsiveness-break` finding all along, but `/e2e-review` only ever screenshotted **one** viewport (1440px), so it could never fire.
|
|
33
|
+
|
|
34
|
+
**MINOR** — adds review capability; **no new config key** (rides on `features.has_e2e_review` + `features.has_design_system`), no schema-change propagation. The mobile viewport is fixed at 375px (the canonical small-screen audit width). Degrades to no-op on consumers without e2e-review. The per-route cost was **measured** on a real verifier run before shipping default-on: ~one extra verifier call per renderable route (~0.3% of a card run — the 375px screenshot is lighter than the 1440px one, and only renderable routes incur it).
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- **`/e2e-review` captures both viewports + runs a mobile responsive-integrity pass** (`framework/.claude/skills/e2e-review/SKILL.md`). Phase 3 now captures `<route-slug>.png` (desktop 1440×900, **unchanged path** — all existing consumers intact) AND `<route-slug>@mobile.png` (375×812), setting the viewport before navigating so SSR/responsive hooks render for that width. Phase 4 adds a second verifier invocation per route with `viewport_role: "mobile"` (step 2b), and the `skip`-level pre-filter no longer suppresses it (the mobile pass needs no mockup). Findings aggregate with `viewport_role` in the dedup key so a mobile `responsiveness-break` stays distinct and the Phase 5 gate / self-heal report show which viewport broke.
|
|
39
|
+
- **`visual-fidelity-verifier` gains a `viewport_role: "mobile"` responsive-integrity mode** (`framework/.claude/agents/visual-fidelity-verifier.md`). When set, it does NOT diff against the (desktop) mockup — the mobile layout reflows by design — and emits ONLY genuine breakage findings (`responsiveness-break`, `unreachable-action`, `layout-break`), conservatively. Reuses the existing taxonomy + output schema; no new finding codes.
|
|
40
|
+
- **`design-system-protocol.md` adds a "Responsive Scope Discipline" section** — the textual SSOT: (1) a viewport-scoped edit MUST NOT alter the other viewport's branch; (2) a structural/heavy component MUST NOT be swapped across viewports via `display:none`/`visibility`/`hidden`/`@media` CSS hiding (mounts both branches, runs both effect/query sets, ships dead DOM). Stated abstractly — concrete primitive names / breakpoints / any project-local gate live in the consumer's own files, never in the framework. Enforced as a **review check** (the CSS-hidden case is invisible to a JS AST rule), not a lint.
|
|
41
|
+
- **`code-reviewer` enforces it** (rule 9) — structural `display:none` swap = HIGH; viewport-scoped edit that mutates the other branch = MEDIUM. **`design-review.md`** checklist points to the same section. This is the surviving, generalized slice of the responsive work; everything stack-specific (a project's `eslint.responsive.config.mjs`, primitive names) stays a consumer-side project file, never in `.framework/`.
|
|
42
|
+
|
|
8
43
|
## [4.70.0] - 2026-06-25
|
|
9
44
|
|
|
10
45
|
**`/ds-edit` — the deliberate-edit twin of `/ds-new`, completing the single-element lifecycle.** `/ds-new` creates (its reuse-first step STOPS if the element exists); there was no symmetric guided entry to *change* an existing canonical element. "Update a component" decomposes into three cases — two were already covered: the **mechanical doc-resync** after a code change is automatic (serializer regenerates the HEAD from source, preserving agentic + prose; the BLOCKING `DS_COMPONENT_STALE` gate enforces it), and **agentic re-curation** is `doc-reviewer`'s domain. The gap was the **deliberate change with a decision** — add a variant/prop, a breaking change, or re-governing a closed-set role.
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.
|
|
1
|
+
4.72.0
|
|
@@ -115,6 +115,15 @@ Concrete enforcement contract for the review pass:
|
|
|
115
115
|
HEAD is NOT `DS_COMPONENT_STALE` merely for that; empty deterministic HEAD
|
|
116
116
|
fields (non-TS stack, unresolved prop) are advisory, never blocking. The HEAD
|
|
117
117
|
is an upgrade target — see `component-manifest-schema.md` § Transition leniency.
|
|
118
|
+
9. **Responsive Scope Discipline** — per [`design-system-protocol.md`](../../agents/design-system-protocol.md)
|
|
119
|
+
§ "Responsive Scope Discipline": (a) a structural/heavy component swapped
|
|
120
|
+
across viewports via `display:none` / `visibility:hidden` / `hidden` / `@media`
|
|
121
|
+
CSS hiding (mounts both branches, runs both effect/query sets, ships dead DOM)
|
|
122
|
+
is **HIGH**; (b) a viewport-scoped edit that also mutates the *other* viewport's
|
|
123
|
+
branch (e.g. a "mobile only" change that shifts the desktop layout) is
|
|
124
|
+
**MEDIUM**. Token-level per-breakpoint restyling of a single element is fine —
|
|
125
|
+
only whole-tree `display:none` swaps trip (a). This is a review judgement, not a
|
|
126
|
+
lint (the CSS-hidden case is invisible to a JS AST rule).
|
|
118
127
|
|
|
119
128
|
When `features.has_design_system: false`, the registry reads are skipped but
|
|
120
129
|
the review still flags hardcoded values and inconsistencies against
|
|
@@ -42,6 +42,26 @@ predating v3.22.0): fall back to the consumer root (whatever Bash `pwd`
|
|
|
42
42
|
returns). Log a `[WORKING-DIR-FALLBACK]` warning in your response so the
|
|
43
43
|
caller can detect it and migrate.
|
|
44
44
|
|
|
45
|
+
## Session provenance (the caller passes `PLANNING_SESSION_ID`)
|
|
46
|
+
|
|
47
|
+
When invoked by `/prd` Step 5 the caller ALSO passes a **`PLANNING_SESSION_ID`**
|
|
48
|
+
field — the Claude Code session UUID of the `/prd` run (the orchestrator's
|
|
49
|
+
`$CLAUDE_CODE_SESSION_ID`). On EVERY card you write, set:
|
|
50
|
+
|
|
51
|
+
```yaml
|
|
52
|
+
provenance:
|
|
53
|
+
planning_session: "<PLANNING_SESSION_ID>"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This makes a card traceable back to the chat history that authored it
|
|
57
|
+
(`~/.claude/projects/*/<id>.jsonl`). **Do NOT read `CLAUDE_CODE_SESSION_ID`
|
|
58
|
+
yourself** — you run as a subagent, so your own session id differs from the
|
|
59
|
+
`/prd` conversation the user sees; only the value the caller passed is correct.
|
|
60
|
+
If the caller did NOT pass `PLANNING_SESSION_ID` (legacy call site, or the env
|
|
61
|
+
var was unset), **omit the `provenance` block entirely** — never write a
|
|
62
|
+
placeholder. Do NOT set `implementation_session` — `/new`/`/new2` add that when
|
|
63
|
+
the card reaches `status: DONE`.
|
|
64
|
+
|
|
45
65
|
---
|
|
46
66
|
|
|
47
67
|
## Field Grounding Rule (HARD RULE — zero tolerance)
|
|
@@ -63,6 +63,7 @@ The orchestrator (`/e2e-review`) invokes you with this JSON payload:
|
|
|
63
63
|
"card_id": "FEAT-XXXX",
|
|
64
64
|
"route": "/dashboard",
|
|
65
65
|
"viewport": { "width": 1440, "height": 900 },
|
|
66
|
+
"viewport_role": "desktop" | "mobile", // optional, default "desktop"
|
|
66
67
|
"implementation_screenshot_path": "/abs/path/to/impl.png",
|
|
67
68
|
"mockup_source": {
|
|
68
69
|
"level": "figma" | "local" | "compliance-only" | "skip",
|
|
@@ -81,7 +82,22 @@ The orchestrator (`/e2e-review`) invokes you with this JSON payload:
|
|
|
81
82
|
```
|
|
82
83
|
|
|
83
84
|
If `mockup_source.level == "skip"`, return immediately with
|
|
84
|
-
`{ "status": "skipped", "reason": "no_mockup_available", "findings": [] }
|
|
85
|
+
`{ "status": "skipped", "reason": "no_mockup_available", "findings": [] }` —
|
|
86
|
+
**unless** `viewport_role == "mobile"`, in which case run the responsive-integrity
|
|
87
|
+
pass below (it needs no mockup).
|
|
88
|
+
|
|
89
|
+
**`viewport_role: "mobile"` — responsive-integrity pass.** When set, you are
|
|
90
|
+
looking at the **mobile render** (a narrow viewport, typically 375px). Mobile
|
|
91
|
+
mockups are rarely provided, and the mobile layout *correctly* reflows away from
|
|
92
|
+
any desktop mockup — so in this mode you **do NOT diff against a mockup** and you
|
|
93
|
+
do **NOT** emit fidelity findings (`element-order`, `spacing-off-scale`,
|
|
94
|
+
`typography-*`, `color-*` relative to a desktop design). Evaluate the mobile
|
|
95
|
+
screenshot on its own and flag **only genuine viewport breakage**:
|
|
96
|
+
`responsiveness-break` (horizontal scroll past the viewport, content cut off,
|
|
97
|
+
elements overlapping/unreadable), `unreachable-action` (primary CTA off-screen or
|
|
98
|
+
behind a fixed overlay), and `layout-break` (a container that genuinely collapsed,
|
|
99
|
+
not an intended stack). Be conservative: a dense-but-legible layout is **not** a
|
|
100
|
+
break — flag only hard failures a user would hit. Return the same output schema.
|
|
85
101
|
|
|
86
102
|
**`resolved_orphans` carve-out (Functional Traceability Gate).** The mockup is a
|
|
87
103
|
fidelity target, not a requirements oracle: it may contain interactive chrome that
|
|
@@ -273,14 +273,27 @@ capture the result + screenshots per route.
|
|
|
273
273
|
- Use `page.getByRole()`, `page.getByText()`, `page.getByTestId()` —
|
|
274
274
|
never CSS selectors that depend on Tailwind class strings.
|
|
275
275
|
- Use `expect(locator)` with auto-waiting.
|
|
276
|
-
- For each route in the scenario,
|
|
276
|
+
- For each route in the scenario, capture BOTH viewports (set the viewport
|
|
277
|
+
BEFORE navigating so SSR / responsive hooks render for that width):
|
|
278
|
+
// Desktop — existing path, consumed by Phase 4 / 4b. DO NOT change it.
|
|
279
|
+
await page.setViewportSize({ width: 1440, height: 900 });
|
|
277
280
|
await page.goto(route);
|
|
278
281
|
await page.waitForLoadState("networkidle");
|
|
279
282
|
await page.screenshot({
|
|
280
283
|
path: ".baldart/e2e-review/<CARD-ID>/screenshots/<route-slug>.png",
|
|
281
284
|
fullPage: true
|
|
282
285
|
});
|
|
283
|
-
|
|
286
|
+
// Mobile — additive, enables the responsiveness-break check (Phase 4).
|
|
287
|
+
await page.setViewportSize({ width: 375, height: 812 });
|
|
288
|
+
await page.goto(route);
|
|
289
|
+
await page.waitForLoadState("networkidle");
|
|
290
|
+
await page.screenshot({
|
|
291
|
+
path: ".baldart/e2e-review/<CARD-ID>/screenshots/<route-slug>@mobile.png",
|
|
292
|
+
fullPage: true
|
|
293
|
+
});
|
|
294
|
+
Both screenshot paths are consumed by the visual-fidelity-verifier in
|
|
295
|
+
Phase 4 — do not change them. 375px is the canonical small-screen audit
|
|
296
|
+
width (design-system-protocol Reference Tables); it is fixed, not a config key.
|
|
284
297
|
- For OTP-driven personas (test_plan.test_credentials.auth_method == "otp"),
|
|
285
298
|
use `await page.pause()` between phone-entry and code-entry. Document
|
|
286
299
|
the manual step in a test.step block.
|
|
@@ -309,7 +322,8 @@ capture the result + screenshots per route.
|
|
|
309
322
|
- `status`: `passed` | `failed` | `flaky` | `skipped`
|
|
310
323
|
- `duration_ms`
|
|
311
324
|
- `failure_reason` (when failed, the first assertion error + stack hint)
|
|
312
|
-
- `screenshot_paths[]` (
|
|
325
|
+
- `screenshot_paths[]` (per route: the desktop `<route-slug>.png` AND the
|
|
326
|
+
mobile `<route-slug>@mobile.png`)
|
|
313
327
|
5. **Functional findings**: every `failed` or `flaky` scenario becomes a
|
|
314
328
|
functional finding in the aggregate report (Phase 5). Severity:
|
|
315
329
|
- Auth / payments / data-mutation scenario failed → `critical`
|
|
@@ -336,7 +350,11 @@ For each `route` in `plan.routes[]`:
|
|
|
336
350
|
latency / cost saver — most routes pass pixel diff cleanly.
|
|
337
351
|
- When `mockup_source.level == "compliance-only"`, skip pre-filter (no
|
|
338
352
|
image to compare against) and proceed directly to step 2.
|
|
339
|
-
- When `mockup_source.level == "skip"`, skip
|
|
353
|
+
- When `mockup_source.level == "skip"`, skip the **desktop fidelity diff**
|
|
354
|
+
(steps 2) for this route — but still run the **mobile responsive pass**
|
|
355
|
+
(step 2b) if a `<route-slug>@mobile.png` screenshot was captured, since it
|
|
356
|
+
needs no mockup. Skip the route entirely only when no screenshot exists
|
|
357
|
+
(route unrenderable).
|
|
340
358
|
2. **Invoke `visual-fidelity-verifier`** with the input contract documented
|
|
341
359
|
in [`visual-fidelity-verifier.md`](../../agents/visual-fidelity-verifier.md):
|
|
342
360
|
|
|
@@ -365,12 +383,46 @@ For each `route` in `plan.routes[]`:
|
|
|
365
383
|
verifier from flagging a deliberately-removed orphan affordance as
|
|
366
384
|
`component-missing`. Omit the field (or pass `[]`) when the inventory has no
|
|
367
385
|
resolved orphans.
|
|
386
|
+
2b. **Mobile responsive-integrity pass.** Invoke the verifier a **second** time
|
|
387
|
+
for the same route with the mobile screenshot, so its `responsiveness-break`
|
|
388
|
+
check (which never fires on a desktop-only screenshot) can run. This is what
|
|
389
|
+
catches a viewport-scoped edit that regresses the *other* viewport:
|
|
390
|
+
|
|
391
|
+
```
|
|
392
|
+
subagent_type: "visual-fidelity-verifier"
|
|
393
|
+
prompt: |
|
|
394
|
+
Compare the implementation against the mockup and return JSON per your
|
|
395
|
+
output schema.
|
|
396
|
+
|
|
397
|
+
Input payload:
|
|
398
|
+
{
|
|
399
|
+
"card_id": "FEAT-XXXX",
|
|
400
|
+
"route": "/dashboard",
|
|
401
|
+
"viewport": { "width": 375, "height": 812 },
|
|
402
|
+
"viewport_role": "mobile",
|
|
403
|
+
"implementation_screenshot_path": ".baldart/e2e-review/FEAT-XXXX/screenshots/<route-slug>@mobile.png",
|
|
404
|
+
"mockup_source": { "level": "skip" },
|
|
405
|
+
"tolerance": "<from config>"
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
`viewport_role: "mobile"` puts the verifier in responsive-integrity mode: it
|
|
409
|
+
does NOT diff against the desktop mockup (the mobile layout reflows by design)
|
|
410
|
+
and emits ONLY genuine breakage findings (`responsiveness-break`,
|
|
411
|
+
`unreachable-action`, `layout-break`). Run this pass on every renderable route
|
|
412
|
+
(a desktop screenshot exists); skip it only when the route is explicitly marked
|
|
413
|
+
desktop-only in the plan. Cost is ~one extra verifier call per route — measured
|
|
414
|
+
marginal (~0.3% of a card run) because the 375px screenshot is lighter than the
|
|
415
|
+
1440px one and only renderable routes incur it.
|
|
368
416
|
3. **Parse the agent's JSON response** strictly. If the response is malformed
|
|
369
417
|
or contains prose outside the JSON, log
|
|
370
418
|
`verifier_protocol_violation` in `gaps[]` and treat the route's findings
|
|
371
419
|
as empty (do not invent findings from prose).
|
|
372
|
-
4. **Aggregate** visual findings across all routes into a single
|
|
373
|
-
`visual_findings[]` array, deduplicating by `(route, category,
|
|
420
|
+
4. **Aggregate** visual findings from BOTH passes across all routes into a single
|
|
421
|
+
`visual_findings[]` array, deduplicating by `(route, viewport_role, category,
|
|
422
|
+
region)` — `viewport_role` keeps a mobile `responsiveness-break` distinct from
|
|
423
|
+
any desktop finding on the same route. Tag each mobile-pass finding with
|
|
424
|
+
`viewport_role: "mobile"` so the Phase 5 gate and self-heal report show which
|
|
425
|
+
viewport broke.
|
|
374
426
|
|
|
375
427
|
---
|
|
376
428
|
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
3. **Retry cap + escalation** — retry the commit at most **2 times**. If it still fails (e.g. a pre-commit / doc-freshness rule that cannot be satisfied without human input), log the failure in `## Issues & Flags` as `[COMMIT-BLOCKED] <error>` and invoke `AskUserQuestion`: (a) fix the blocking condition manually and let me retry, (b) hand off the worktree intact, (c) abandon this card and continue the batch. Do NOT loop indefinitely and do NOT silently proceed to step 28 with an uncommitted card. **When AUTONOMOUS, apply § AUTONOMOUS RESOLUTION RULE (SKILL.md): category=blocker; recommended=none safe (a hook rejection needs human input) → materialize a follow-up card carrying the blocking condition and leave the worktree intact (NEVER force-commit, NEVER mark the card DONE).**
|
|
31
31
|
- The Claude Code pre-commit hook automatically skips for worktree commits (Husky handles checks natively in the worktree).
|
|
32
32
|
28. **Mark card DONE (MANDATORY — do NOT skip)**:
|
|
33
|
-
a. Edit the backlog YAML (`${paths.backlog_dir}/<CARD-ID>.yml`): set `status: DONE`, add `completed_date: <today
|
|
33
|
+
a. Edit the backlog YAML (`${paths.backlog_dir}/<CARD-ID>.yml`): set `status: DONE`, add `completed_date: <today>`, and add `provenance.implementation_session: <SESSION-ID>` (this run's session UUID — get it with `Bash(echo "${CLAUDE_CODE_SESSION_ID:-}")`; if empty, omit the field). Merge into any existing `provenance:` block written by `/prd` — do NOT overwrite `planning_session`. This makes an implementation bug traceable back to the session that produced it (`~/.claude/projects/*/<id>.jsonl`).
|
|
34
34
|
b. Add implementation notes summarizing what was built.
|
|
35
35
|
c. **Update `${paths.references_dir}/ssot-registry.md`** — add/update the entry for this card's feature area. The pre-commit doc-freshness hook BLOCKS commits that touch `${paths.backlog_dir}/` without a corresponding ssot-registry update. Always include ssot-registry.md in the same commit as the backlog YAML.
|
|
36
36
|
d. **Verify the write**: re-read the YAML file and confirm `status: DONE` is present. If not, retry the edit.
|
|
@@ -41,10 +41,10 @@ has no blocking consequence the honest label is "monitoring signal".
|
|
|
41
41
|
3. **Assemble the full JSONL record in memory** (single atomic append — JSONL is append-only, so the row must be COMPLETE before it touches the file). If `STATS=true` (step 6), the `"cost"` key is part of THIS record — do NOT write a baseline row now and rewrite it later (a mid-file rewrite corrupts the JSONL; there is no `sed -i`/`jq` rewrite step in this skill). When `STATS=true`, defer the single append to step 6 after the cost object is computed; when `STATS=false`, append now. The record shape:
|
|
42
42
|
|
|
43
43
|
```json
|
|
44
|
-
{"ts":"<ISO-8601-UTC>","skill":"new","run_id":"batch-<FIRST-CARD-ID>","cards":["FEAT-XXX"],"total_cards":N,"first_attempt_success_rate":0.0,"mean_fix_cycles":0.0,"qa_profiles":{"skip":0,"light":0,"balanced":0,"deep":0},"qa_pass_first_attempt_rate":0.0,"findings_total":0,"findings_verified":0,"actionability_rate":0.0,"severity_p0_pct":0.0,"doc_gaps_found":0,"doc_gaps_fixed":0,"cycle_time_mins":0,"worktree_branch":"","merge_commit":""}
|
|
44
|
+
{"ts":"<ISO-8601-UTC>","skill":"new","run_id":"batch-<FIRST-CARD-ID>","session_id":"<$CLAUDE_CODE_SESSION_ID, '' if unset>","cards":["FEAT-XXX"],"total_cards":N,"first_attempt_success_rate":0.0,"mean_fix_cycles":0.0,"qa_profiles":{"skip":0,"light":0,"balanced":0,"deep":0},"qa_pass_first_attempt_rate":0.0,"findings_total":0,"findings_verified":0,"actionability_rate":0.0,"severity_p0_pct":0.0,"doc_gaps_found":0,"doc_gaps_fixed":0,"cycle_time_mins":0,"worktree_branch":"","merge_commit":""}
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
Use `date -u +%Y-%m-%dT%H:%M:%SZ` for the timestamp. Append the assembled line ONCE with `echo '<full-record>' >> "$METRICS/skill-runs.jsonl"` via Bash — never append a partial row then mutate it.
|
|
47
|
+
Use `date -u +%Y-%m-%dT%H:%M:%SZ` for the timestamp. Fill `session_id` with `echo "${CLAUDE_CODE_SESSION_ID:-}"` (this run's session UUID — the same one stamped as `provenance.implementation_session` on each card; `""` if unset) so the telemetry row resolves to the chat transcript at `~/.claude/projects/*/<id>.jsonl`. Append the assembled line ONCE with `echo '<full-record>' >> "$METRICS/skill-runs.jsonl"` via Bash — never append a partial row then mutate it.
|
|
48
48
|
|
|
49
49
|
4. Copy the batch tracker to archive:
|
|
50
50
|
|
|
@@ -153,8 +153,8 @@ For each completed agent:
|
|
|
153
153
|
- A genuinely legitimate no-op card (its AC turned out already satisfied) is the rare exception: confirm it from the report's per-requirement `evidence` pointing at PRE-EXISTING code, and log it explicitly as `no-op (AC pre-satisfied)` — never assume zero-diff means success.
|
|
154
154
|
**Classify the CAUSE before re-spawning — you MUST read the rested agent's LAST event, never infer the cause from "no report" alone.** A background teammate killed mid-flight dies WITHOUT delivering anything to you, so its failure signature lives ONLY in its on-disk transcript — "no report + no diff" is the SAME observable for a rate-limit death and a genuine empty-result, and they need OPPOSITE remedies. Before choosing one, locate and tail that transcript:
|
|
155
155
|
- Subagent transcripts live at `<session-subagents-dir>/agent-*.jsonl`, each with a sibling `agent-*.meta.json` whose `agentType` equals the teammate label. Resolve the file and read its tail, e.g.:
|
|
156
|
-
`D="$(ls -dt ~/.claude/projects/*/${
|
|
157
|
-
(if `$
|
|
156
|
+
`D="$(ls -dt ~/.claude/projects/*/${CLAUDE_CODE_SESSION_ID:-*}/subagents 2>/dev/null | head -1)"; f="$(grep -l '"<teammate-label>"' "$D"/*.meta.json 2>/dev/null | head -1 | sed 's/\.meta\.json$/.jsonl/')"; tail -c 4000 "$f"`
|
|
157
|
+
(if `$CLAUDE_CODE_SESSION_ID` is unset, the most recent `subagents/` dir under this project is the live session's.) If the transcript truly cannot be located, fall back to whatever the `Agent` tool result surfaced — but do NOT default to "genuine empty-result" without having looked. **State the classification you reached + the evidence in your narration** (e.g. "03 last event = `Rate limited` → transient").
|
|
158
158
|
- **Transient infra failure (the common cause of a silent empty rest in a WIDE parallel wave).** The agent's last event is an API error — signatures: `isApiErrorMessage:true`, `Rate limited`, `Server is temporarily limiting requests`, `overload`, `(not your usage limit)` — i.e. the background teammate was **killed mid-flight by API rate-limiting** (it had done real work — reads, a plan — then died), NOT a model fabrication or "decided nothing to do". The runtime does NOT auto-resume a dead teammate, so it rests with no report + no diff. **Re-spawn it, but STAGGERED with backoff — do NOT re-fire several transient-failed agents in the same parallel burst** (that re-saturates the API and kills them again). Space them out (one at a time, or after a brief pause). This still costs a fresh run (the killed agent's reads are lost — the runtime cannot resume it), so **prevention beats recovery**: if a wave repeatedly hits rate limits, NARROW the parallel fan-out width for that wave (spawn fewer coders at once) rather than re-firing the full wave. A transient failure does NOT consume the Step-B genuine-failure budget. **(Workflow twin, since v4.66.0:** inside the `new-card-review` / `new-final-review` dynamic workflows the same transient-vs-terminal discipline is enforced in code — `agentSafe` retries a transient-killed reviewer in-workflow, and a reviewer that stays dead is recorded in `summary.degradedReviewers` rather than silently dropped by `filter(Boolean)`; a dead **qa-sentinel** specifically injects a synthetic `FAIL` gate so the merge is blocked, never passed on UNKNOWN mechanical gates. Same SSOT, two surfaces: this prose for orchestrator-spawned teammates, the workflow JS for in-workflow fan-out.)**
|
|
159
159
|
- **Genuine empty-result / fabrication.** The agent rested CLEANLY (no error in its last event), with no completion report and no diff. THIS is the model-fault case: take the one Step-B re-spawn below (same cap), re-briefing the coder with an explicit "write your ownership files and confirm them on disk before reporting done" mandate. A second empty result → `AskUserQuestion` (skip/abandon) — do not re-spawn a third time. **When AUTONOMOUS, apply § AUTONOMOUS RESOLUTION RULE (SKILL.md): category=blocker; recommended=none safe (a card whose coder produced nothing twice has no implementation) → skip the card AND materialize a follow-up card carrying its spec (NEVER mark it DONE / no-op).** (Classify the cause first — a transient rate-limit death is re-spawned staggered, not counted against this budget.)
|
|
160
160
|
|
|
@@ -152,6 +152,7 @@ Workflow({ name: 'new2', args: {
|
|
|
152
152
|
refModulesBase, // .claude/skills/new/references (semantic SSOT)
|
|
153
153
|
config, // the parsed baldart.config.yml (paths.*/stack.*/features.*/git.*)
|
|
154
154
|
ts, // ISO timestamp NOW — the workflow has no clock (Date.now() unavailable there)
|
|
155
|
+
sessionId, // this run's CLAUDE_CODE_SESSION_ID — stamped as provenance.implementation_session on each DONE card
|
|
155
156
|
migration, // Step-3.5 manifest: { status:'none'|'applied'|'skipped'|'degraded', modality?, summary?, artifacts?, affects_cards? }
|
|
156
157
|
flags: { stats, effort, full }
|
|
157
158
|
}})
|
|
@@ -160,6 +161,12 @@ Workflow({ name: 'new2', args: {
|
|
|
160
161
|
Record the kickoff time (`ts`) before the call — Step 5 uses it to stamp the
|
|
161
162
|
real wall-clock duration (the workflow cannot read a clock).
|
|
162
163
|
|
|
164
|
+
Capture `sessionId` the same way, in THIS skill's own context (not a subagent):
|
|
165
|
+
`Bash(echo "${CLAUDE_CODE_SESSION_ID:-}")`. The workflow stamps it as
|
|
166
|
+
`provenance.implementation_session` on each card it marks DONE, so a batch is
|
|
167
|
+
traceable back to the chat history that ran it (`~/.claude/projects/*/<id>.jsonl`).
|
|
168
|
+
If the echo is empty, pass `sessionId: ''` and the workflow omits the field.
|
|
169
|
+
|
|
163
170
|
The workflow runs the entire batch in the background. **Do not poll it** — it
|
|
164
171
|
returns when the batch is done. It returns:
|
|
165
172
|
|
|
@@ -212,8 +219,10 @@ returns when the batch is done. It returns:
|
|
|
212
219
|
- **Every class ∈ {`owner-gated`, `not-a-code-defect`, `policy-deferred-ac`}** → the card's own
|
|
213
220
|
code is complete; the residual is an external/infra step. Now that step 1 guaranteed its
|
|
214
221
|
deferral's follow-up exists on disk in the main repo, set the card `status: DONE` +
|
|
215
|
-
`completed_date` +
|
|
216
|
-
|
|
222
|
+
`completed_date` + `provenance.implementation_session: <sessionId>` (the same id captured
|
|
223
|
+
for the workflow args — merge into any existing provenance block, do NOT touch
|
|
224
|
+
`planning_session`; omit if empty) + an implementation_note (`"DONE post-run (new2) — AC
|
|
225
|
+
deferred to follow-up <id>"`) in `${paths.backlog_dir}/<card>.yml`, and fold all of them into ONE reconciliation
|
|
217
226
|
commit in the MAIN repo.
|
|
218
227
|
- **ANY class ∈ {`unresolved`, `out-of-ownership`, `outage`} (or missing)** → the card's DoD is
|
|
219
228
|
genuinely NOT met (an AC the workflow tried and failed to implement). Leave it **IN_PROGRESS**
|
|
@@ -284,3 +284,14 @@ assumptions:
|
|
|
284
284
|
unknowns: []
|
|
285
285
|
|
|
286
286
|
notes: []
|
|
287
|
+
|
|
288
|
+
# --- Provenance (OPTIONAL — SKILL-written, never authored by hand) ---
|
|
289
|
+
# Records which Claude Code session produced this card, so an implementation error
|
|
290
|
+
# is traceable back to the chat history that planned/implemented it.
|
|
291
|
+
# Resolve the transcript at ~/.claude/projects/*/<session-id>.jsonl (UUID is globally unique).
|
|
292
|
+
# planning_session: written by prd-card-writer (value passed in by the /prd orchestrator as
|
|
293
|
+
# PLANNING_SESSION_ID — the orchestrator's CLAUDE_CODE_SESSION_ID, NOT the writer subagent's).
|
|
294
|
+
# implementation_session: added by /new (commit.md Step 28) or /new2 when status → DONE.
|
|
295
|
+
# Omit a sub-field when its session id is unavailable; an EPIC card has planning_session only.
|
|
296
|
+
provenance:
|
|
297
|
+
planning_session: "{{CLAUDE_CODE_SESSION_ID of the /prd run — omit if unset}}"
|
|
@@ -5,6 +5,10 @@ canonicality: canonical
|
|
|
5
5
|
owner: prd
|
|
6
6
|
status: draft
|
|
7
7
|
freshness_status: fresh
|
|
8
|
+
# Session that authored this PRD — resolve the chat history at
|
|
9
|
+
# ~/.claude/projects/*/<id>.jsonl. Value = $CLAUDE_CODE_SESSION_ID of the /prd run.
|
|
10
|
+
# Omit the line if the env var is unset.
|
|
11
|
+
planning_session: {{CLAUDE_CODE_SESSION_ID of the /prd run}}
|
|
8
12
|
---
|
|
9
13
|
|
|
10
14
|
# {{Feature Name}} PRD
|
|
@@ -194,6 +194,10 @@ Agent(
|
|
|
194
194
|
directory, NOT into the consumer's main repo. See your "Working Directory"
|
|
195
195
|
section for the rule and sanity check.)
|
|
196
196
|
|
|
197
|
+
PLANNING_SESSION_ID: <$PLANNING_SESSION_ID>
|
|
198
|
+
(The /prd run's session UUID — set provenance.planning_session on every card.
|
|
199
|
+
See your "Session provenance" section. Omit the field if this value is empty.)
|
|
200
|
+
|
|
197
201
|
PRD path: <$WORKTREE_PATH>/<prd_path>
|
|
198
202
|
State file path: <$WORKTREE_PATH>/<state_file_path>
|
|
199
203
|
Card template: <$WORKTREE_PATH>/.claude/skills/prd/assets/card-template.yml
|
|
@@ -223,6 +227,13 @@ in the state file's `## Worktree` section BEFORE invoking the agent. Do NOT
|
|
|
223
227
|
pass the literal string `$WORKTREE_PATH` — the subagent has no shell context
|
|
224
228
|
to expand it.
|
|
225
229
|
|
|
230
|
+
The same applies to `<$PLANNING_SESSION_ID>`: BEFORE invoking the agent, the
|
|
231
|
+
`/prd` skill captures its OWN session id with `Bash(echo "${CLAUDE_CODE_SESSION_ID:-}")`
|
|
232
|
+
and substitutes the value (a UUID) into the prompt. The capture MUST run in the
|
|
233
|
+
orchestrator's own context (this skill), NOT inside the writer subagent — a
|
|
234
|
+
subagent's `CLAUDE_CODE_SESSION_ID` is its own, not the `/prd` conversation's.
|
|
235
|
+
If the echo prints empty, pass an empty value and the writer omits the field.
|
|
236
|
+
|
|
226
237
|
### What the agent handles
|
|
227
238
|
|
|
228
239
|
The `prd-card-writer` agent owns the entire card writing pipeline:
|
|
@@ -97,6 +97,11 @@ Mark task 3 as `in_progress`.
|
|
|
97
97
|
- Include a "Research-Informed Decisions" subsection listing decisions influenced
|
|
98
98
|
by research, with brief rationale and source reference.
|
|
99
99
|
5. Write `${paths.prd_dir}/<slug>/PRD.md` using the template from `assets/prd-template.md`.
|
|
100
|
+
Fill the frontmatter `planning_session` with this run's session id —
|
|
101
|
+
`Bash(echo "${CLAUDE_CODE_SESSION_ID:-}")` (run it in this skill's own context,
|
|
102
|
+
not a subagent). If the echo is empty, drop the `planning_session:` line. This is
|
|
103
|
+
the same UUID passed to `prd-card-writer` as `PLANNING_SESSION_ID`, so the PRD and
|
|
104
|
+
its cards share one traceable session pointer.
|
|
100
105
|
|
|
101
106
|
### PRD sections checklist
|
|
102
107
|
|
|
@@ -29,6 +29,8 @@ export const meta = {
|
|
|
29
29
|
// affected cards' apply-AC is NOT owner-gated-deferred (F-016 exception).
|
|
30
30
|
// flags { stats, effort, full }
|
|
31
31
|
// ts string ISO timestamp injected by the skill (Date.now() is unavailable here)
|
|
32
|
+
// sessionId string this run's CLAUDE_CODE_SESSION_ID (skill-captured) — stamped as
|
|
33
|
+
// provenance.implementation_session on each card marked DONE ('' → omit)
|
|
32
34
|
//
|
|
33
35
|
// return (consumed by the skill at Step 5):
|
|
34
36
|
// { report, perCardResults, gateLedger, residualFollowups, telemetry, degraded, degradationReasons, residuals }
|
|
@@ -52,6 +54,12 @@ const METRICS = a.metricsDir || 'docs/metrics'
|
|
|
52
54
|
const REF = (a.refModulesBase || '.claude/skills/new/references').replace(/\/$/, '')
|
|
53
55
|
const FLAGS = a.flags || {}
|
|
54
56
|
const TS = a.ts || ''
|
|
57
|
+
// Session provenance — stamped on each DONE card so an implementation bug is traceable to the
|
|
58
|
+
// chat history that produced it. '' (env var unset) → the commit agent omits the field.
|
|
59
|
+
const SESSION_ID = a.sessionId || ''
|
|
60
|
+
const provLine = SESSION_ID
|
|
61
|
+
? ` Also set provenance.implementation_session: "${SESSION_ID}" (merge into any existing provenance block from /prd — do NOT touch planning_session).`
|
|
62
|
+
: ''
|
|
55
63
|
// Migration Gate manifest (skill Step 3.5 — front-loaded BEFORE this workflow ran). When
|
|
56
64
|
// status:'applied', the declared schema is LIVE on the active DB, so its apply-AC must NOT be
|
|
57
65
|
// re-classified as an owner-gated policy-deferred AC (F-016) — the downstream cards verify for real.
|
|
@@ -775,7 +783,7 @@ async function runCard(cardId, cardPath) {
|
|
|
775
783
|
// main repo (so a card is never DONE with a silently-dropped requirement — F-029).
|
|
776
784
|
const doneStep = deferredOpen
|
|
777
785
|
? `(4) DO NOT mark the card DONE: it has an OPEN owner-gated/policy-deferred AC. Keep status IN_PROGRESS and add an implementation_note "deferred — DONE pending follow-up (new2 skill reconciles post-run)". STILL add the ssot-registry row for the committed code.`
|
|
778
|
-
: `(4) mark the card DONE in its YAML + add the ssot-registry row
|
|
786
|
+
: `(4) mark the card DONE in its YAML + add the ssot-registry row.${provLine}`
|
|
779
787
|
let commitRes
|
|
780
788
|
try {
|
|
781
789
|
commitRes = await agentSafe(
|
|
@@ -968,7 +976,7 @@ async function integrateCrossCard() {
|
|
|
968
976
|
try {
|
|
969
977
|
await agentSafe(
|
|
970
978
|
`Commit the cross-card integration fixes in worktree ${sharedCtx.worktreePath}. MECHANICAL — do NOT modify source beyond what is already changed on disk. Steps:\n` +
|
|
971
|
-
`(1) \`git status --porcelain\`; (2) stage ONLY dirty files within the batch union ${JSON.stringify(batchUnion)} — NEVER \`git add -A\`, NEVER \`git stash\`; (3) for each fully-freed card YAML [${fullyFreed.join(', ') || '(none)'}] set status to DONE and add implementation_note "cross-card integration: deferred AC resolved with batch-union ownership (new2 v4.30.0)"; (4) commit message \`[integrate] cross-card residual fixes${fullyFreed.length ? ' (' + fullyFreed.join(', ') + ')' : ''}\`; (5) 'nothing to commit' = the resolve already committed (record HEAD).\n` +
|
|
979
|
+
`(1) \`git status --porcelain\`; (2) stage ONLY dirty files within the batch union ${JSON.stringify(batchUnion)} — NEVER \`git add -A\`, NEVER \`git stash\`; (3) for each fully-freed card YAML [${fullyFreed.join(', ') || '(none)'}] set status to DONE and add implementation_note "cross-card integration: deferred AC resolved with batch-union ownership (new2 v4.30.0)"${SESSION_ID ? ' and set provenance.implementation_session: "' + SESSION_ID + '" (do NOT touch planning_session)' : ''}; (4) commit message \`[integrate] cross-card residual fixes${fullyFreed.length ? ' (' + fullyFreed.join(', ') + ')' : ''}\`; (5) 'nothing to commit' = the resolve already committed (record HEAD).\n` +
|
|
972
980
|
`Return: { committed, commit, filesChanged }`,
|
|
973
981
|
{ label: 'integrate:commit', phase: 'Integrate', agentType: 'general-purpose', model: 'haiku',
|
|
974
982
|
schema: { type: 'object', required: ['committed'], additionalProperties: true, properties: { committed: { type: 'boolean' }, commit: { type: 'string' }, filesChanged: { type: 'array', items: { type: 'string' } } } } }
|
|
@@ -74,6 +74,7 @@ Legend: **R** = required, present **and non-empty** · **E** = required key, val
|
|
|
74
74
|
| `db_indexes` | — | C | C | only if it adds a compound query |
|
|
75
75
|
| `test_plan` · `integration_points` · `existing_patterns` · `anti_patterns` · `reuse_analysis` · `input_output_examples` · `error_handling` | C | C | C | populate when applicable (see `prd-card-writer.md` Required Fields) |
|
|
76
76
|
| `assumptions` · `unknowns` · `notes` | C | C | C | |
|
|
77
|
+
| `provenance` | C | C | C | session-id traceability — written by the SKILL, never the user; see § `provenance` below |
|
|
77
78
|
|
|
78
79
|
## Consumer contract — HALT / BACK-FILL / WARN
|
|
79
80
|
|
|
@@ -158,6 +159,34 @@ that finds a `component_bindings` entry MUST honor it over any contrary reading
|
|
|
158
159
|
the mockup; a UI card that went through `/prd` with `features.has_design_system:
|
|
159
160
|
true` but lacks this field is under-specified (the reconciliation did not run).
|
|
160
161
|
|
|
162
|
+
### `provenance` (session-id traceability — optional, skill-written)
|
|
163
|
+
|
|
164
|
+
A small block that records **which Claude Code session produced the card**, so a card
|
|
165
|
+
that later turns out wrong is traceable back to the chat history that planned or
|
|
166
|
+
implemented it (read the transcript at `~/.claude/projects/*/<session-id>.jsonl`).
|
|
167
|
+
|
|
168
|
+
```yaml
|
|
169
|
+
provenance:
|
|
170
|
+
planning_session: "fd8a0411-8946-45b6-a671-c7f747ca67e6" # /prd — the session that AUTHORED this card
|
|
171
|
+
implementation_session: "9c2e17b0-1a3d-4f88-bb20-7e0c4a9d51fe" # /new|/new2 — the session that took it to status: DONE
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Rules:
|
|
175
|
+
- **Optional (`C`)** in every profile — legacy cards lack it and never HALT; the validator
|
|
176
|
+
ignores it. It is **never** authored by a human; the skills fill it.
|
|
177
|
+
- The value is the canonical session UUID from the env var **`CLAUDE_CODE_SESSION_ID`**
|
|
178
|
+
(NOT `CLAUDE_SESSION_ID`, which does not exist). Resolve a transcript with
|
|
179
|
+
`ls ~/.claude/projects/*/<uuid>.jsonl` — the UUID is globally unique, so the project slug
|
|
180
|
+
is not needed.
|
|
181
|
+
- **Ownership is split by lifecycle**: `prd-card-writer` writes `planning_session` at creation
|
|
182
|
+
(the value is **passed in by the `/prd` orchestrator** as `PLANNING_SESSION_ID` — never read
|
|
183
|
+
from inside the writer subagent, whose own session id differs). `/new` (`commit.md` Step 28)
|
|
184
|
+
and `/new2` (`new2.js` commit step) add `implementation_session` when they set `status: DONE`.
|
|
185
|
+
- An **EPIC** card is a tracker, never implemented → it carries `planning_session` only, never
|
|
186
|
+
`implementation_session`.
|
|
187
|
+
- Omit any sub-field whose session id is unavailable (env var unset) rather than writing a
|
|
188
|
+
placeholder.
|
|
189
|
+
|
|
161
190
|
## Writer contract
|
|
162
191
|
|
|
163
192
|
The canonical writer is the `prd-card-writer` agent. It emits the full baseline for the
|
|
@@ -73,6 +73,9 @@ recommend `/design-system-init`.
|
|
|
73
73
|
- [ ] No horizontal scroll on mobile
|
|
74
74
|
- [ ] Touch targets minimum 44x44px
|
|
75
75
|
- [ ] Content hierarchy clear
|
|
76
|
+
- [ ] Responsive Scope Discipline (design-system-protocol.md § "Responsive Scope
|
|
77
|
+
Discipline"): no structural component swapped via `display:none`; a
|
|
78
|
+
viewport-scoped change does not regress the other viewport
|
|
76
79
|
|
|
77
80
|
### Interaction Design
|
|
78
81
|
|
|
@@ -545,6 +545,37 @@ implemented (or is about to be) without a traceable function. HIGH severity:
|
|
|
545
545
|
it ships dead code and erodes the minimalism contract. The fix is always one of
|
|
546
546
|
the three outcomes above, never "leave it, it matches the mockup".
|
|
547
547
|
|
|
548
|
+
## Responsive Scope Discipline
|
|
549
|
+
|
|
550
|
+
A UI surface usually renders across viewports (mobile / tablet / desktop). Two
|
|
551
|
+
rules keep responsive work from regressing the viewport you are *not* editing —
|
|
552
|
+
the framework states them abstractly; the concrete primitive names, breakpoint
|
|
553
|
+
values and any project-specific gate live in the consumer's own files (CLAUDE.md,
|
|
554
|
+
overlays, a project-local linter), never here.
|
|
555
|
+
|
|
556
|
+
1. **Viewport-scoped edits stay scoped.** When a change targets one viewport
|
|
557
|
+
(e.g. "fix the mobile header"), it MUST NOT alter the other viewport's branch.
|
|
558
|
+
Editing the mobile path and silently shifting the desktop layout — or vice
|
|
559
|
+
versa — is a regression even when the targeted viewport looks correct. Verify
|
|
560
|
+
both viewports before declaring the task done (`/e2e-review` renders at 1440px
|
|
561
|
+
and 375px and runs the `responsiveness-break` check at each).
|
|
562
|
+
|
|
563
|
+
2. **No structural component swapped via `display:none`.** A DOM-structural or
|
|
564
|
+
heavy component (one that mounts a non-trivial subtree, fetches data, or has
|
|
565
|
+
expensive children) MUST NOT be shown/hidden across viewports via
|
|
566
|
+
`display:none` / `visibility:hidden` / a `hidden` attribute / `@media` CSS
|
|
567
|
+
hiding. Mounting both branches and hiding one ships dead DOM, runs *both*
|
|
568
|
+
components' effects and queries, and doubles the work on every render. Render
|
|
569
|
+
only the active branch (a viewport hook / conditional render) instead.
|
|
570
|
+
Token-level responsive styling — a single element restyled per breakpoint
|
|
571
|
+
(padding, font-size, direction) — is fine; it is *swapping whole component
|
|
572
|
+
trees* via `display:none` that this rule rejects.
|
|
573
|
+
|
|
574
|
+
Enforcement is a **review check** (`code-reviewer`, `/design-review`), not a lint:
|
|
575
|
+
the most damaging case (a CSS-hidden heavy subtree) is invisible to a JS AST rule.
|
|
576
|
+
Rule 2 violations are HIGH; a viewport-scoped edit that mutates the other branch
|
|
577
|
+
(rule 1) is MEDIUM.
|
|
578
|
+
|
|
548
579
|
## Post-Intervention Coherence Check (BLOCKING completion gate)
|
|
549
580
|
|
|
550
581
|
The BLOCKING pre-work above enforces *reads* before any UI change. This
|