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 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.70.0
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, call:
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
- The screenshot path is consumed by the visual-fidelity-verifier in Phase 4 — do not change it.
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[]` (one per route in the scenario)
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 this route entirely.
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, region)`.
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/*/${CLAUDE_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_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").
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` + an implementation_note (`"DONE post-run (new2) AC deferred to follow-up
216
- <id>"`) in `${paths.backlog_dir}/<card>.yml`, and fold all of them into ONE reconciliation
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "4.70.0",
3
+ "version": "4.72.0",
4
4
  "description": "Claude Agent Framework - Reusable framework for coordinating AI agents and humans in software projects",
5
5
  "bin": {
6
6
  "baldart": "./bin/baldart.js"