baldart 4.34.2 → 4.36.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,48 @@ 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.36.0] - 2026-06-13
9
+
10
+ **`/new` security-domain fixes are now applied by `security-reviewer`, not `coder` — the v4.26.1 canonical writer map, finally propagated from `new2` to `/new`.** Auditing the `new2` lessons for guards/logic missing on `/new` surfaced one real gap (the others — args-string guard, JS router clamp, no-self-judge + specialist-owned lane, relevance-gated fan-out — were already present on `/new`). `new2-resolve.js` routes security fixes to `security-reviewer` (`fixerAgent = {doc:'doc-reviewer', ui:'ui-expert', security:'security-reviewer'}[domain] || 'coder'`), but the canonical writer map was never propagated to `/new`'s SSOT: the `Domain-Override Domains` table (SKILL.md) and every fix-routing site still sent `security` → `coder`. A coder applying a one-line RLS/permission/auth fix lacks the security-invariant contract that lives in `security-reviewer`'s system prompt — the same class of error as "wrong agent for the card", and a direct violation of the user's standing strict-specialization principle. **MINOR** (changes which agent applies security fixes across `/new`; backwards-compatible — `migration` stays `coder`, no install/layout change, no `baldart.config.yml` key ⇒ schema-propagation rule N/A).
11
+
12
+ ### Changed
13
+
14
+ - **`framework/.claude/skills/new/SKILL.md`** — `Domain-Override Domains` table: `security` owning agent `coder` → **`security-reviewer`** (write mode), plus a new "Why `security` is owned by `security-reviewer`" rationale mirroring the `doc` one. The sequential-mode overview line aligned too.
15
+ - **`framework/.claude/skills/new/references/review-cycle.md`**, **`final-review.md`**, **`team-mode.md`**, **`codex-gate.md`** — every security-fix routing site (Phase 2.55 Domain-Override delegation, the delegated-workflow residual routing, the Final FULL merge-blocking partition, the Phase 3.7 codex fix sub-loop, and the doc-drift→bug security path) now routes `security` → `security-reviewer` and runs it before the `coder` pass. `migration` stays `coder`.
16
+ - **`framework/.claude/workflows/new-card-review.js`** — the Fix phase no longer folds security into the single coder pass. It partitions `VERIFIED` findings into a `security-reviewer` pass (domain `security`) and a `coder` pass (`code`/`perf`/`migration`/`test`/`simplify`), run sequentially (security first) over the disjoint-by-ownership editable set so shared-file edits never conflict; a FAIL in either pass fails the wave. `new-final-review.js` needed no change (it is read-only — the calling skill applies fixes — and its `domainVerifier` already routes security verification to `security-reviewer`).
17
+ - **`framework/.claude/agents/security-reviewer.md`** — new "Dual mode — review vs. apply" Behavior Rule: by default it audits and proposes (read-only), but when invoked as the security domain writer (by `/new`/`new2`/the codex fix loop) it APPLIES the remediation directly via Edit/Write and re-verifies — security fixes are owned by it, never deferred to a coder.
18
+
19
+ ## [4.35.1] - 2026-06-13
20
+
21
+ **`/new` workflow delegation no longer degrades to a silent no-op when `args` arrives as a JSON string.** A live `/new FEAT-0027 -full` team-mode run delegated its per-wave review cluster to the `new-card-review` workflow and got back a degenerate result (`cards:0`, 0 agents, ~24ms) — the orchestrator correctly fell back to the inline cluster, but the delegation (the single biggest context-economy win in team mode) was wasted on every wave. Root cause: the `Workflow` tool sometimes serializes a structured `args` object to a JSON **string**; `new-card-review.js` and `new-final-review.js` read `args.cards` / `args.reviewScopeFiles` directly, so a string `args` left those `undefined` → empty scope → the early-return guard fired. The `new2` family (`new2.js`, `new2-resolve.js`) had already been hardened against exactly this (`F-001/F-004` parse-or-default guard), but the fix was **never propagated** to the two `/new` workflows — a parallel-location miss. **PATCH** (bugfix to shipped workflow payload, no behaviour change to install, no config key ⇒ schema-propagation rule N/A).
22
+
23
+ ### Fixed
24
+
25
+ - **`framework/.claude/workflows/new-card-review.js`**, **`framework/.claude/workflows/new-final-review.js`** — added the same defensive `if (typeof a === 'string') { try { a = JSON.parse(a) } catch (_) { a = {} } }` guard already present in `new2.js`/`new2-resolve.js`. All four workflows now tolerate `args` delivered as a JSON string, so `/new`'s delegated review cluster and Final Review fan-out run as intended instead of no-op'ing into the inline fallback.
26
+
27
+ ## [4.35.0] - 2026-06-13
28
+
29
+ **Card-baseline standardization — every backlog card, any prefix/origin, conforms to one profile-aware SSOT; `/new` normalizes foreign cards at ingestion.** A real `CHORE-0007` (consumer repo `mayo`) reached `/new` **without `review_profile`** (and without `scope`/`scope_boundaries`/`canonical_docs`): it was hand/ad-hoc authored after a graph-align finding, never by the canonical writer. `/new` and `/new2` consume cards **type-blind** — they scale per-card review depth on `review_profile` and run the same pipeline regardless of prefix — so an off-baseline card silently degrades the pipeline. Root cause: the baseline was scattered across `card-template.yml` + `prd-card-writer`'s Required-Fields + Rule C with **no single SSOT and no validator**; `prd-card-writer` only documented the `/prd` epic+children flow (its standalone single-card mode, used by `new2-resolve`, was undocumented); and three writers diverged — `/prd`/`new2`/`new2-resolve` emit the full baseline, but the `/new` AC-deferral stub (`completeness.md`) and `/issue-review` (`issue-review.md`) wrote partial cards.
30
+
31
+ The fix is **profile-aware** by design (an adversarial review caught that a flat "every field required, non-empty" rule would wrongly reject valid cards — an epic legitimately has `owner_agent: ""` and `files_likely_touched: []`; a standalone CHORE legitimately omits `scope_boundaries`/`links.prd`). A new SSOT module defines per-profile (epic/child/standalone) field states (REQUIRED / may-be-empty / conditional). Every creation path now delegates to `prd-card-writer` (the canonical writer), and `/new`/`/new2` ingestion **normalizes** a non-conformant card: HALT only on truly non-derivable fields (`scope`/`requirements`/`acceptance_criteria`/`files_likely_touched`), **back-fill + persist** the derivable ones (`review_profile` via Rule C, `owner_agent`→`coder`) to the card on disk in the **main repo** (check-and-skip idempotent; committed with the `merge-cleanup.md` Phase 6b COMMIT_LOCK discipline), WARN on the rest. A shipped validator (profile-aware) + CI gate would have failed `CHORE-0007` at authoring time. **MINOR** (new protocol module + new agent capability + new validator/CI gate; backwards-compatible — conformant cards still validate, back-fill only *adds* fields; `review_profile`/baseline are not `baldart.config.yml` keys ⇒ schema-propagation rule N/A).
32
+
33
+ ### Added
34
+
35
+ - **`framework/agents/card-schema.md`** — Atomic Card Baseline Schema: the universal, profile-aware (epic/child/standalone) field-state matrix every backlog card satisfies, plus the consumer HALT/BACK-FILL/WARN contract. Cites Rule A (`owner_agent` enum, REGISTRY.md) and Rule C (`review_profile` criteria) — never copies them. This is the missing peer of the `agents/index.md` module manifest ("if you create/mutate a card, read card-schema.md"). 23rd protocol module.
36
+ - **`framework/scripts/validate-card-baseline.js`** — profile-aware validator. Parses the field-state matrix from `card-schema.md` and the enums from `REGISTRY.md` (no embedded copy → no drift), detects the card profile, and asserts each field's state + enum validity. Exit 1 + per-field errors. Module API (`validateCard`/`detectProfile`/`loadSchema`/`loadEnums`) for reuse.
37
+ - **`scripts/check-card-baseline.js`** + new step in **`.github/workflows/check-reference-integrity.yml`** — CI gate: self-tests the validator against fixtures (valid epic/child/standalone + a broken CHORE-0007 shape + invalid enums) and asserts `card-template.yml` ↔ `card-schema.md` do not drift.
38
+ - **`framework/templates/ci/check-card-baseline.yml`** — opt-in consumer CI template: fails a PR when a hand-authored `backlog/*.yml` drifts off the baseline (catches manual cards at PR time, not only at `/new` time).
39
+
40
+ ### Changed
41
+
42
+ - **`framework/.claude/agents/prd-card-writer.md`** — new § "Standalone Single-Card Mode" (documents the no-epic single-card path used by `/new`/`new2`/`new2-resolve`/`/issue-review`: relaxes epic+children and conditional fields, but still emits the full STANDALONE baseline — *standalone ≠ minimal*). "Required Fields Per Card" now cites `card-schema.md` instead of being the implicit SSOT; `description:` frontmatter acknowledges the standalone invocation.
43
+ - **`framework/.claude/skills/new/references/setup.md`** — pre-flight step 1b restructured into **1b-i** (HALT on non-derivable fields, incl. `scope`; `scope_boundaries` is NOT a HALT field), **1b-ii** (back-fill `review_profile`/`owner_agent`, persist to `$MAIN`, check-and-skip, dedicated `[BACKFILL]` commit), **1b-iii** (run the profile-aware validator).
44
+ - **`framework/.claude/skills/new/SKILL.md`** — QA Profile Selector simplified to a pure read (the compute-from-Rule-C fallback is consolidated into setup.md 1b-ii, which persists the value — no second copy of the criteria).
45
+ - **`framework/.claude/skills/new/references/completeness.md`** — AC-deferral follow-up now delegates to `prd-card-writer` Standalone Mode (full baseline) instead of writing a 3-field stub; minimal stub only on total agent outage (self-heals on first ingestion).
46
+ - **`framework/.claude/commands/issue-review.md`** — backlog-card creation delegates to `prd-card-writer` (standalone for one-off issues, epic+children for new features) instead of hand-filling the template.
47
+ - **`framework/.claude/skills/new2/SKILL.md`** — offline reconciliation cites `card-schema.md` + the standalone mode; notes the crisis stub self-heals via the `/new` back-fill.
48
+ - **`framework/agents/index.md`**, **`framework/.claude/skills/prd/assets/card-template.yml`** — routing rule + template header pointer to `card-schema.md` as the field SSOT.
49
+
8
50
  ## [4.34.2] - 2026-06-13
9
51
 
10
52
  **`/new` workflow-delegation gates hardened to BINARY/imperative — close the "ran inline by judgment" escape hatch.** A real `/new` run (mayo, single-card CHORE-0007, BALDART 4.34.1 with both workflows linked + the `Workflow` tool available) ran **both** review-delegation gates inline instead of delegating: the orchestrator invented escape criteria absent from the gate ("single-card → marginal context-economy benefit", "memory note on workflow degeneration") and mis-cited the `feedback_dynamic_workflows_fit` guidance, which actually names the Final Review fan-out as the canonical workflow fit. The degeneration reports it conflated concern the `new2`/`new2-resolve` whole-batch host — a different workflow. Net effect: the run validated only the inline fallback and gave **zero** signal on the `new-card-review` (v4.34.0) path. Both gates were already worded as `IF tool+script → delegate / ELSE inline`, but the prose left enough softness for the model to insert discretion. **PATCH** (gate-prose hardening; no behavior change to the install layout or the workflows themselves — the delegated path was always the intended one).
package/README.md CHANGED
@@ -82,7 +82,7 @@ No additional activation steps needed — once installed, Claude Code (and Codex
82
82
  ### Core Protocol
83
83
 
84
84
  - **AGENTS.md**: Mandatory coordination rules (MUST/SHOULD/OPTIONAL)
85
- - **agents/**: 17 domain modules (architecture, workflows, testing, security, etc.)
85
+ - **agents/**: 23 domain modules (architecture, workflows, testing, security, card-schema, etc.)
86
86
  - **Routing**: If you touch X, read Y - minimize context loading
87
87
 
88
88
  ### AI Agents (28 specialized agents)
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.34.2
1
+ 4.36.0
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: prd-card-writer
3
- description: "Generates atomic backlog cards from an approved PRD. Handles card YAML writing, dependency graph computation, parallel group assignment, and traceability matrices (FR/NFR, ISA, UI). Invoked by the /prd skill at Step 5 to offload the heaviest mechanical phase from the main context."
3
+ description: "Generates atomic backlog cards from an approved PRD. Handles card YAML writing, dependency graph computation, parallel group assignment, and traceability matrices (FR/NFR, ISA, UI). Invoked by the /prd skill at Step 5 to offload the heaviest mechanical phase from the main context. Also runs in Standalone Single-Card Mode to author ONE baseline-conformant follow-up/CHORE/BUG/issue card (no epic+children) — invoked by /new (AC-deferral), new2/new2-resolve, and /issue-review. The canonical card writer: every card-creation path delegates here rather than hand-writing a partial stub."
4
4
  model: opus
5
5
  effort: high
6
6
  color: amber
@@ -364,6 +364,38 @@ error-prone.
364
364
 
365
365
  Mirror their structure when generating new epic+child sets.
366
366
 
367
+ ## Standalone Single-Card Mode (since v4.35.0)
368
+
369
+ The epic+children structure above is the **PRD-batch** contract. You are ALSO the canonical
370
+ writer for a **single** card with no PRD/epic lineage — a deferred AC, a graph-align finding, a
371
+ CHORE/BUG follow-up, a GitHub-issue card. Callers that invoke you this way: `/new` AC-deferral
372
+ (`new/references/completeness.md`), `new2-resolve` / `new2` offline reconciliation, and
373
+ `/issue-review`. This mode exists so those cards are **first-class**, not minimal stubs.
374
+
375
+ **Trigger:** the caller passes a single-card brief (a residual / AC / finding / issue) instead of
376
+ a PRD path + state file, typically with `MODE: standalone`. When there is no PRD/epic context in
377
+ the prompt, you are in standalone mode.
378
+
379
+ **RELAXED in standalone mode:**
380
+ - The "1 epic + N children" mandate and all epic-only Tier-2 fields (`execution_strategy`,
381
+ `parallel_group` roll-up, AC-EPIC, the `-00` epic file). A standalone card has **no epic**;
382
+ set `group.parent` to a tracking slug or the originating card id and omit `group.sequence`.
383
+ - `scope_boundaries` — omit when the card has no siblings (the usual standalone case).
384
+ - `canonical_docs` / `links.prd` — when the card has no originating PRD, emit them best-effort or
385
+ omit with a one-line `[NO-PRD]` note in your response; do NOT fabricate a PRD path.
386
+
387
+ **STILL MANDATORY in standalone mode (zero tolerance) — the STANDALONE column of
388
+ `framework/agents/card-schema.md`:** `owner_agent` (Rule A enum, routed to the card's domain),
389
+ `review_profile` (Rule C), `scope`, `requirements`, `acceptance_criteria`,
390
+ `definition_of_done`, `files_likely_touched` (≥1), `data_sources` (≥`[]`), plus the
391
+ identity/meta fields. **Standalone ≠ minimal** — a one-off CHORE carries the same baseline as a
392
+ PRD child. The card MUST pass the `/new` pre-flight field check and the
393
+ `validate-card-baseline.js` validator.
394
+
395
+ **Filename:** `<PREFIX>-NNNN-<slug>.yml` or `<parent-id>-followup-<gate>.yml`. Reserve the id
396
+ with the same prefix-parametric allocator (`allocate-id.sh reserve <PREFIX> <slug>`). Return the
397
+ created card id.
398
+
367
399
  ## Pre-Generation Checklist (MANDATORY — execute BEFORE writing any card)
368
400
 
369
401
  Before writing the first YAML file, confirm and report to the caller:
@@ -385,6 +417,12 @@ children): HALT and report the issue. Do NOT generate flat cards as fallback.
385
417
 
386
418
  ## Required Fields Per Card
387
419
 
420
+ The **universal, profile-aware baseline** (which field is required / may-be-empty / conditional
421
+ for EPIC vs CHILD vs STANDALONE) is the field-state matrix in
422
+ [`framework/agents/card-schema.md`](../../agents/card-schema.md) — the SSOT. The list below is
423
+ the CHILD-profile expansion with the concrete population rules; consult card-schema.md for the
424
+ epic/standalone deltas and never let the two disagree.
425
+
388
426
  Every card MUST include ALL fields from the template:
389
427
 
390
428
  - `id`, `title`, `status` (TODO), `priority`, `owner_agent`, `execution_mode`
@@ -50,6 +50,7 @@ Before reviewing:
50
50
 
51
51
  ## Behavior Rules
52
52
 
53
+ - **Dual mode — review vs. apply.** By default you AUDIT and *propose* remediations (read-only). But when invoked as the **security domain writer** (e.g. by `/new` / `new2` / the Phase 3.7 codex fix loop, whose brief tells you to "apply the verified security findings" in write mode), you ARE the fixer: APPLY the minimal remediation directly with the Edit/Write tools, then re-verify (lint/tsc/build as instructed). Security fixes are owned by you — never deferred to a coder — because the auth/permission/RLS/multi-tenant-isolation invariants live in YOUR system prompt, not the coder's. Stay within the files the brief's ownership map allows; if a fix needs a file outside that scope, report it as residual rather than expanding scope.
53
54
  - Be extremely critical, thorough, and skeptical. Optimize for correctness and security, not politeness.
54
55
  - Do NOT assume the developer did things safely unless proven by code evidence.
55
56
  - Treat ALL external input as hostile.
@@ -14,7 +14,7 @@ If the user mentions `/issue-review` without a number, prompt for the GitHub iss
14
14
 
15
15
  ## Phase 2 — Plan
16
16
 
17
- 4. Create or update a backlog card from `templates/feature-card.template.yml`. Populate `execution_mode`, `git_strategy`, and `claimed_paths` fields.
17
+ 4. Create or update the backlog card by **delegating to the `prd-card-writer` agent** (do NOT hand-write a partial card from the template). For a one-off issue use **Standalone Single-Card Mode** (`MODE: standalone`); when the issue is a new feature that goes through the `prd` agent (Phase 3), let `prd-card-writer` produce the epic+children as usual. Pass the triage findings, `execution_mode`, `git_strategy`, and `claimed_paths` as INPUT to the writer — it returns a card carrying the full baseline of `framework/agents/card-schema.md` (incl. `owner_agent` via Rule A and `review_profile` via Rule C), not just those three fields.
18
18
  5. Ask the user which git strategy to use (main vs feature branch) per AGENTS.md and record it in `git_strategy`.
19
19
  6. Route to specialist agents based on issue type:
20
20
  - UI/UX issues → invoke `ui-expert` agent.
@@ -291,7 +291,7 @@ per-card nei sub-step D.x (mai aggregate). Caricalo quando Pre-flight seleziona
291
291
  ### Sequential mode (default for small batches)
292
292
 
293
293
  - Cards execute one at a time through the full per-card pipeline (Phases 1-5).
294
- - Code review and doc review for the same card run as **parallel read-only audits**, then fixes are applied by domain owner: **doc findings → `doc-reviewer` (write mode)**, code/security/migration findings → `coder`. (Sequential Phase 3 is even simpler — doc-reviewer runs alone, so it audits AND applies in one invocation.)
294
+ - Code review and doc review for the same card run as **parallel read-only audits**, then fixes are applied by domain owner (see § "Domain-Override Domains"): **doc findings → `doc-reviewer` (write mode)**, **security findings → `security-reviewer` (write mode)**, code/perf/migration findings → `coder`. (Sequential Phase 3 is even simpler — doc-reviewer runs alone, so it audits AND applies in one invocation.)
295
295
  - This mode is unchanged from the original behavior.
296
296
 
297
297
  ### Team mode (for complex batches)
@@ -377,13 +377,14 @@ They are deliberately NOT reproduced here. If the card has a `review_profile`
377
377
  `light`, `balanced`, `deep`}, **use it verbatim** — do NOT recompute. Log `profile=<x> (from card)`
378
378
  in the tracker.
379
379
 
380
- **Fallback path (legacy cards only — no `review_profile` field):** cards authored before v3.38.0
381
- (or manually) lack the field. Only then, compute the profile by reading the decision criteria
382
- from the SSOT: **`review_profile` is computed by prd-card-writer (Rule C); see
383
- `framework/.claude/agents/prd-card-writer.md` for the criteria.** Apply those criteria here,
384
- then log `profile=<x> (computed review_profile absent)`. There is no second copy of the
385
- criteria in this file keeping one authority avoids the drift that previously let the two tables
386
- disagree.
380
+ **By this point `review_profile` is always present.** Pre-flight step 1b-ii (`setup.md`) already
381
+ **normalized** every ingested card a legacy/manual/foreign card that lacked the field had it
382
+ back-filled via `prd-card-writer.md § Rule C` and **persisted to disk** (with a `[BACKFILL]` log),
383
+ so this selector is a pure read. The compute-from-Rule-C logic lives ONLY at 1b-ii there is no
384
+ second copy of the criteria here, which avoids the drift that previously let the two tables
385
+ disagree. Defensive one-liner only: if a card somehow still lacks the field (validator skipped in
386
+ this install), compute it via Rule C and log `profile=<x> (computed — review_profile absent)` —
387
+ but the canonical write happened at 1b-ii.
387
388
 
388
389
  > Note: the qa-sentinel agent keeps its OWN, different mapping (`profile → SCOPED/FULL` test tier),
389
390
  > which is legitimately qa-sentinel's; that is not the same thing as this profile-selection criteria.
@@ -540,11 +541,13 @@ Enumerated exhaustively:
540
541
  | Domain | Owning agent | Match rule |
541
542
  |---|---|---|
542
543
  | `doc` | **`doc-reviewer`** (write mode) | File path matching `*.md` under `${paths.references_dir}`, `${paths.prd_dir}`, project root `CHANGELOG.md`, or any `ssot-registry.md`. |
543
- | `security` | `coder` | File path matching any entry in `paths.high_risk_modules` (`baldart.config.yml`) — the same auth/permission/payment-class paths the Phase 3.7 Step A detector reads. Also any SQL migration whose content matches `CREATE POLICY|ALTER POLICY|DROP POLICY` (RLS policy mutations). If `paths.high_risk_modules` is absent, the security match rule emits a one-line diagnostic and matches nothing (no hardcoded default). |
544
+ | `security` | **`security-reviewer`** (write mode) | File path matching any entry in `paths.high_risk_modules` (`baldart.config.yml`) — the same auth/permission/payment-class paths the Phase 3.7 Step A detector reads. Also any SQL migration whose content matches `CREATE POLICY|ALTER POLICY|DROP POLICY` (RLS policy mutations). If `paths.high_risk_modules` is absent, the security match rule emits a one-line diagnostic and matches nothing (no hardcoded default). |
544
545
  | `migration` | `coder` | File path matching `${paths.migrations_dir}/*.sql` if `paths.migrations_dir` is defined in `baldart.config.yml`; otherwise the project's migrations dir per convention (`migrations/`, `db/migrate/`, `supabase/migrations/`, `prisma/migrations/`). |
545
546
 
546
547
  **Why `doc` is owned by `doc-reviewer`, not `coder` (since v3.40.0)** — the doc invariants the orchestrator must not break (freshness markers, linking protocol, frontmatter standard, tabular formatting, SSOT/registry coverage, dependency-topological order, SCIP/code refs) are encoded in the **`doc-reviewer`** system prompt, NOT the coder's. The coder is a code-oriented agent that lacks the doc-invariant contract — routing doc fixes to it is the wrong agent doing work the auditing agent already has full context for. The agent that *audits* the docs is also the agent that *fixes* them (`doc-reviewer.md` § Constraints: "WRITE missing docs directly. You are fully responsible — do not defer to other agents"). NEVER route a `doc`-domain fix to `coder`.
547
548
 
549
+ **Why `security` is owned by `security-reviewer`, not `coder` (since v4.36.0)** — the same logic as `doc`, applied to the security domain (canonical writer map v4.26.1; user principle "il codice lo scrive solo coder, la security solo security-reviewer"). The auth/permission/RLS/multi-tenant-isolation invariants live in the **`security-reviewer`** system prompt, not the coder's; a coder applying a one-line RLS or permission fix without that contract is the same class of error as the "wrong agent for the card". `security-reviewer` is the writer for security-domain fixes — it audits AND applies. `migration` stays `coder` (SQL authoring is the coder's lane; a migration's security-policy content matching the RLS rule above is classified `security`, not `migration`). NEVER route a `security`-domain fix to `coder`.
550
+
548
551
  **Edge case explicit** — a mechanical append-a-row update to `CHANGELOG.md` or `ssot-registry.md` is still classified `doc` and still goes through `doc-reviewer`, never inline and never `coder`. The uniformity of the rule matters more than the cost of the individual spawn.
549
552
 
550
553
  Domains NOT listed here remain governed by the per-phase rules of the corresponding phase (e.g. `simplify-*` follows Phase 2.55 inline rule).
@@ -128,9 +128,9 @@ For EVERY card (no conditional skip — the gate ALWAYS runs; only its DEPTH var
128
128
 
129
129
  4. **Apply fix sub-loop** (mirror of Phase 3.5 retry pattern):
130
130
  - If 0 BLOCKER and 0 HIGH → log `verdict: PASS — proceeding to Phase 4` in tracker. Done. (MEDIUM/LOW findings are advisory at this per-card gate; they are not silently lost — the post-batch **Final-review FULL gate** applies every VERIFIED finding ≥ MEDIUM. Log the MEDIUM count in the tracker so it is visible.)
131
- - If 1+ BLOCKER OR 1+ HIGH → spawn `coder` agent with the report path + list of VERIFIED bugs. **At `full` profile** the report contains Codex-suggested inline patches: pass them and have the coder **apply the suggested patches** with the right system prompt (project conventions, naming, testing patterns) — it does NOT re-do the analysis or re-grep (since v3.28.3), BUT it MUST first confirm each patch still applies against the current file state (prior fix-loop iterations may have shifted line offsets); if a patch no longer applies cleanly, the coder re-locates the target by content and applies the equivalent edit rather than a stale-offset verbatim paste. **At `light` profile** (since v4.18.0) the findings come from **Codex** (the sole finder) — the report carries Codex's `minimal_fix_direction`; brief the coder to apply it (treat it like the `full`-profile Codex fix direction). **On the Codex-unavailable fallback** the `light` findings come from `code-reviewer` instead — brief the coder to apply the `code-reviewer` fix direction (no Codex patches to paste). After coder fixes, **re-write the lean contract `/tmp/codexreview-lean-<CARD-ID>.json` (it is consumed-once and deleted by `/codexreview`)** and re-invoke `/codexreview` via the Skill tool with `args: <CARD-ID>` (NOT a bare prose mention — the card ID MUST be passed so the retry reviews THIS card, not an inferred one). Repeat **max 2 times**.
131
+ - If 1+ BLOCKER OR 1+ HIGH → spawn the **domain writer** with the report path + list of VERIFIED bugs (canonical writer map v4.26.1 — see SKILL.md § "Domain-Override Domains"): **`security`-domain findings** (touching `paths.high_risk_modules` or RLS-policy SQL — the same `security` match rule) → **`security-reviewer`** in write mode (it owns the security-invariant contract a coder lacks; never route a security fix to `coder`); **all other findings** (`correctness`/code/perf/`other`) → **`coder`**. Run security-reviewer first, then coder (skip either if its partition is empty). **At `full` profile** the report contains Codex-suggested inline patches: pass them and have the coder **apply the suggested patches** with the right system prompt (project conventions, naming, testing patterns) — it does NOT re-do the analysis or re-grep (since v3.28.3), BUT it MUST first confirm each patch still applies against the current file state (prior fix-loop iterations may have shifted line offsets); if a patch no longer applies cleanly, the coder re-locates the target by content and applies the equivalent edit rather than a stale-offset verbatim paste. **At `light` profile** (since v4.18.0) the findings come from **Codex** (the sole finder) — the report carries Codex's `minimal_fix_direction`; brief the coder to apply it (treat it like the `full`-profile Codex fix direction). **On the Codex-unavailable fallback** the `light` findings come from `code-reviewer` instead — brief the coder to apply the `code-reviewer` fix direction (no Codex patches to paste). After coder fixes, **re-write the lean contract `/tmp/codexreview-lean-<CARD-ID>.json` (it is consumed-once and deleted by `/codexreview`)** and re-invoke `/codexreview` via the Skill tool with `args: <CARD-ID>` (NOT a bare prose mention — the card ID MUST be passed so the retry reviews THIS card, not an inferred one). Repeat **max 2 times**.
132
132
  - If still BLOCKER/HIGH after 2 retries → log in `## Issues & Flags` and **ask the user** whether to proceed, escalate, or stop. The Phase 4 commit MUST NOT happen until the Pre-Merge Codex Review verdict is PASS or user explicitly overrides.
133
- - **Telemetry** — for EVERY codex finding processed (verified BLOCKER, verified HIGH, or false-positive-filtered), append one row to `## Fix Application Log`: `3.7 | codex-<security|correctness|other> | est_lines=<n> | decision=<coder|skipped> | applied_by=<coder|-> | severity=<BLOCKER|HIGH|FALSE-POSITIVE> | retry=<n>`. Classify domain: `security` for findings touching RLS / auth / permissions / payments; `correctness` for logic / data integrity / race conditions; `other` for everything else.
133
+ - **Telemetry** — for EVERY codex finding processed (verified BLOCKER, verified HIGH, or false-positive-filtered), append one row to `## Fix Application Log`: `3.7 | codex-<security|correctness|other> | est_lines=<n> | decision=<security-reviewer|coder|skipped> | applied_by=<security-reviewer|coder|-> | severity=<BLOCKER|HIGH|FALSE-POSITIVE> | retry=<n>`. (`security`-domain fixes are applied by `security-reviewer`, all others by `coder`.) Classify domain: `security` for findings touching RLS / auth / permissions / payments; `correctness` for logic / data integrity / race conditions; `other` for everything else.
134
134
 
135
135
  5. **Update tracker**: phase = `3.7-codexgate DONE` (the gate runs unconditionally for every card — the legacy `3.7-highrisk` name implied it only fired on high-risk cards, which is no longer true), log final verdict, retry count, list of fixed findings, and the report path.
136
136
 
@@ -251,7 +251,7 @@ This gate enforces `framework/agents/workflows.md § Scope Closure Discipline` a
251
251
  - Options (max 4):
252
252
  1. **"Implementa adesso"** — spawn a targeted fix `coder` agent scoped to this card's MAY EDIT files (from `## File Ownership Map`) with the instruction "Implement ONLY AC-<N>: '<AC text>'. Do not refactor or expand scope." Re-run Phase 2.5 verification on the resulting diff — **including the mandatory sub-steps 5b (API contract), 5c (alias-mutation), 5d (caller-pattern test)** if the fix touched a route, an exported helper, or a call site (a single-AC fix can still introduce these). **Hard cap: 2 attempts on the same AC.** On the 2nd failure, do NOT re-offer "Implementa adesso" — re-invoke `AskUserQuestion` with only options 2/3/4 (approve deferral / follow-up card / stop), so the loop cannot recur unbounded.
253
253
  2. **"Approva il deferral"** — record `[USER-APPROVED DEFERRAL] <today>: <user-supplied reason>` on its own line in the card's `implementation_notes`. Mark the row `User-approved? yes`.
254
- 3. **"Sposta su follow-up card"** — create a backlog stub at `${paths.backlog_dir}/<CARD-ID>-followup-AC<N>.yml` with `status: TODO` AND the minimum fields the Pre-flight gate (step 1b) requires, so a future `/new` run can pick it up without halting: a non-empty `requirements` (≥1, derived from the AC), a non-empty `acceptance_criteria` (≥1, the verbatim AC text), and a non-empty `files_likely_touched` (≥1, carried from the parent card's ownership map for this AC). Mark the row `User-approved? yes (follow-up: <new-card-id>)`. Do NOT proceed to Phase 2.55 until the follow-up file exists on disk and passes the step-1b field check.
254
+ 3. **"Sposta su follow-up card"** — create the follow-up card by **delegating to the `prd-card-writer` agent in Standalone Single-Card Mode** (NOT a hand-written stub backlog cards are owned by `prd-card-writer`, same discipline as `new2`/`new2-resolve`). Brief it: `MODE: standalone`, write `${paths.backlog_dir}/<CARD-ID>-followup-AC<N>.yml` with `status: TODO`, derived from the deferred AC a non-empty `requirements` (≥1, derived from the AC), `acceptance_criteria` = the verbatim AC text (≥1), `files_likely_touched` (≥1, carried from the parent card's ownership map for this AC), `owner_agent` routed to the AC's domain, and `review_profile` per Rule C — i.e. the full STANDALONE baseline of `framework/agents/card-schema.md`. If `prd-card-writer` is unavailable (total outage), fall back to a minimal valid stub carrying at least the step-1b HALT fields (`requirements`/`acceptance_criteria`/`files_likely_touched`/`scope`); the consumer back-fill (step 1b-ii) then fills `review_profile`/`owner_agent` on first ingestion. Mark the row `User-approved? yes (follow-up: <new-card-id>)`. Do NOT proceed to Phase 2.55 until the follow-up file exists on disk and passes the step-1b field check.
255
255
  4. **"Ferma il batch"** — halt the orchestrator, leave the worktree intact, log the reason in `## Issues & Flags`. Do NOT commit.
256
256
 
257
257
  Do NOT batch the question across multiple ACs — one `AskUserQuestion` per AC, so the user sees each AC's text in isolation. Issue the questions sequentially.
@@ -220,9 +220,9 @@ that is a **gate violation**: log it as
220
220
  10. **Persist verified findings** to `/tmp/batch-final-review-<FIRST-CARD-ID>.md`.
221
221
  11. **Merge-blocking gate (mirrors the per-card Phase 3.7 gate this final pass backstops):** if any VERIFIED **BLOCKER or HIGH** finding exists, it MUST be resolved before Phase 6 merge. Apply fixes by **domain owner** (since v3.40.0 — same Domain-Override routing as the per-card phases), then re-verify; if a BLOCKER/HIGH cannot be resolved in a single apply + one retry, log it in `## Issues & Flags` and invoke `AskUserQuestion` (override with reason / escalate to a follow-up card / halt) — do NOT proceed to Phase 6 with an unresolved BLOCKER or HIGH. VERIFIED findings of severity MEDIUM are also applied (advisory below that). Partition the verified findings by the **Domain-Override match rules** ("Domain-Override Domains"):
222
222
  - **`doc`-domain findings** (file path matching the `doc` match rule — `*.md` under `${paths.references_dir}`/`${paths.prd_dir}`, `CHANGELOG.md`, `ssot-registry.md`) → invoke the **doc-reviewer** agent once in write mode to apply them. NEVER route doc fixes to coder.
223
- - **`security`-domain findings** (path in `paths.high_risk_modules`, or RLS-policy SQL) and **`migration`-domain findings** (SQL under the migrations dir) → route to **coder**, but apply the Sub-agent failure protocol's STOP-on-crash rule for these domains (never inline-fallback on a security/migration fix). These are NOT collapsed into a generic "everything else" bucket.
223
+ - **`security`-domain findings** (path in `paths.high_risk_modules`, or RLS-policy SQL) route to **security-reviewer** in write mode (canonical writer map v4.26.1 — it owns the security-invariant contract a coder lacks; NEVER route security fixes to coder). **`migration`-domain findings** (SQL under the migrations dir) → route to **coder**. For both, apply the Sub-agent failure protocol's STOP-on-crash rule (never inline-fallback on a security/migration fix). These are NOT collapsed into a generic "everything else" bucket.
224
224
  - **All remaining findings** (other code, perf, test) → invoke the **coder** agent once to apply them in a single pass.
225
- Run in the order doc-reviewer → coder (or skip either if its partition is empty). Pass only the verified findings, not false positives.
225
+ Run in the order doc-reviewer → security-reviewer → coder (skip any whose partition is empty). Pass only the verified findings, not false positives.
226
226
  12. Run final build: `npm run lint && npx tsc --noEmit && npm run build` (redirect each to `/tmp/final-<gate>.txt` per § "Context economy"; surface only exit code + bounded extract on failure).
227
227
  If any check fails, apply self-healing retry loop (up to 3 times).
228
228
  13. **Update tracker** with final review results:
@@ -51,8 +51,10 @@ so it surfaces in telemetry.
51
51
  ```
52
52
 
53
53
  The workflow runs Simplify + Codex (agent-launched, code-reviewer fallback) + qa-sentinel + security,
54
- FP-checks each specialist's own findings, then **one coder applies all VERIFIED
55
- code/perf/security/simplify findings in a single pass** and re-verifies lint/tsc/build. It returns
54
+ FP-checks each specialist's own findings, then the **domain writer applies its VERIFIED findings**
55
+ (canonical writer map v4.26.1: `security` `security-reviewer`; `code`/`perf`/`migration`/`test`/
56
+ `simplify` → `coder`) — security-reviewer pass first, then the coder pass — and re-verifies
57
+ lint/tsc/build. It returns
56
58
  `{ codexEngine, perCard: { <CARD-ID>: { fixesApplied, residual } }, gateTable, summary }`.
57
59
  **Skip the inline Phase 2.55 + Phase 3.5 below AND the Phase 3.7 gate in `codex-gate.md`** (all three
58
60
  are now done), then handle the workflow output HERE in the skill. **Process each `residual` finding by
@@ -61,7 +63,9 @@ so it surfaces in telemetry.
61
63
  - `classification == NEEDS_MANUAL_CONFIRMATION` (any domain) → `AskUserQuestion` — the human gate the
62
64
  workflow cannot run. (`summary.needsManual` counts these, doc included.)
63
65
  - else `domain == doc` residual → carry into **Phase 3** (the doc-reviewer runs there, post-E2E, on final code).
64
- - else `code`/`perf`/`security`/`migration` residual (a fix the coder could not converge in its 2 retries)
66
+ - else `security` residual (a fix not converged in 2 retries) → spawn a targeted `security-reviewer`
67
+ now over this card's `editableFiles` (it owns the security-invariant contract — never a coder).
68
+ - else `code`/`perf`/`migration` residual (a fix the coder could not converge in its 2 retries)
65
69
  → spawn a targeted `coder` now over this card's `editableFiles`.
66
70
  - **QA gate (BLOCKING — mirror of inline Phase 3.5 step 24)**: if `gateTable` has any `status:"FAIL"`
67
71
  **OR** `summary.checksFailed` is true, the merge gate is NOT satisfied. Spawn a `coder` on the
@@ -107,7 +111,7 @@ After completeness is verified, clean up the implementation before it reaches re
107
111
  - **Efficiency agent** — flag unnecessary work (redundant computations, duplicate API calls, N+1), missed concurrency, hot-path bloat, recurring no-op updates without change-detection guards, TOCTOU existence checks, memory issues (unbounded structures, missing cleanup), overly broad operations.
108
112
 
109
113
  4. Aggregate findings from all three agents. For each finding:
110
- - **Valid AND in a Domain-Override domain** (the finding's target file matches the `doc`, `security`, or `migration` match rule in "Domain-Override Domains") → do NOT apply inline. Delegate to the domain owner: `doc` → `doc-reviewer` (write mode), `security`/`migration` → `coder`. Even a one-line efficiency fix in `paths.high_risk_modules` or a migration file goes to the owning agent — the orchestrator lacks that domain's invariant contract.
114
+ - **Valid AND in a Domain-Override domain** (the finding's target file matches the `doc`, `security`, or `migration` match rule in "Domain-Override Domains") → do NOT apply inline. Delegate to the domain **writer** (canonical writer map v4.26.1): `doc` → `doc-reviewer` (write mode), `security` → `security-reviewer` (write mode — it owns the security-invariant contract a coder lacks), `migration` → `coder`. Even a one-line efficiency fix in `paths.high_risk_modules` (security) or a migration file goes to the owning agent — the orchestrator lacks that domain's invariant contract.
111
115
  - **Valid AND not in a Domain-Override domain** → fix directly (apply edits inline).
112
116
  - **False positive / not worth addressing** → skip, BUT record it (see telemetry). If the skip rests on a "covered by X" / "redundant" / "not needed" rationalization (the same family the AC-Closure Gate guards against), do NOT discard silently — verify the rationale by reading `X`, and if it does not hold, treat the finding as valid.
113
117
 
@@ -279,9 +283,9 @@ skill's Phase 1 falls back to deriving Gherkin scenarios from
279
283
  per-card Phase 3.7 gate now skips that duplicate (lean mode), so THIS pass MUST carry it.
280
284
  A doc-drift→bug finding whose root cause is in CODE (not the doc) is the ONE thing
281
285
  doc-reviewer does NOT fix itself — report it with the conflicting code location + the doc
282
- it violates, and the orchestrator routes it to the `security`/code fix path as appropriate.
286
+ it violates, and the orchestrator routes it to the `security` (→ security-reviewer) / code (→ coder) fix path as appropriate.
283
287
  ```
284
- Doc-reviewer applies all doc-domain fixes itself. The orchestrator does NOT spawn a coder for doc fixes (since v3.40.0 — `doc` is owned by `doc-reviewer`, see "Domain-Override Domains"). The only doc-reviewer output that leaves this phase unfixed is a **doc-drift→bug finding rooted in CODE** (the implementation contradicts a documented contract). Route it explicitly: if the conflicting code file matches the `security` Domain-Override match rule (`paths.high_risk_modules`) → spawn `coder` with the finding now, in this phase (a security-class code fix is not deferrable to a `light` Phase 3.7); otherwise carry the finding into the Phase 3.7 `/codexreview` input as a known code-drift bug and let the Phase 3.7 fix sub-loop apply it. Either way, append a Fix Application Log row with `domain=codex-correctness` (NOT `doc`) so telemetry attributes it as a code fix. Do NOT leave it accumulating in the tracker with no fix owner.
288
+ Doc-reviewer applies all doc-domain fixes itself. The orchestrator does NOT spawn a coder for doc fixes (since v3.40.0 — `doc` is owned by `doc-reviewer`, see "Domain-Override Domains"). The only doc-reviewer output that leaves this phase unfixed is a **doc-drift→bug finding rooted in CODE** (the implementation contradicts a documented contract). Route it explicitly: if the conflicting code file matches the `security` Domain-Override match rule (`paths.high_risk_modules`) → spawn `security-reviewer` with the finding now, in this phase (a security-class code fix is not deferrable to a `light` Phase 3.7, and security is owned by `security-reviewer` — never a coder); otherwise carry the finding into the Phase 3.7 `/codexreview` input as a known code-drift bug and let the Phase 3.7 fix sub-loop apply it. Either way, append a Fix Application Log row with `domain=codex-correctness` (NOT `doc`) so telemetry attributes it as a code fix. Do NOT leave it accumulating in the tracker with no fix owner.
285
289
  14. **Knowledge-corpus sync (OPTIONAL — only if the project ships a corpus-sync agent)**: There is NO shipped `obsidian-sync` agent — do NOT dispatch one (a hard dispatch to a non-existent subagent fails silently). Only when the project provides its own knowledge-corpus sync agent (declared in `.baldart/overlays/new.md`) AND doc-reviewer's findings indicate a corpus impact, invoke that agent with the listed paths after the doc fixes are applied. Otherwise skip with a one-line notice (`knowledge-corpus sync: skipped (no corpus-sync agent configured)`). Non-blocking either way.
286
290
  15. **Telemetry** — after doc-reviewer returns, append one row per doc finding to `## Fix Application Log`: `3 | doc | est_lines=<n> | decision=doc-reviewer | applied_by=doc-reviewer | finding=<1-line>`. If 0 findings, append one row: `3 | doc | est_lines=0 | decision=skipped | applied_by=- | reason=no-findings`. **Phase-8 producer (named counter)** — ALSO record the per-card doc-gap counts as a structured line in `## Current Card` (carried into `## Completed Cards` at Phase 5): `doc_gaps: found=<N> fixed=<M>` where `N` = total doc findings doc-reviewer raised and `M` = those it applied. This is the single named producer for Phase 8's `doc_gaps_found` / `doc_gaps_fixed` fields — without it those fields have no upstream write and Phase 8 would hard-code zeros. (D.4a is the team-mode producer of the same counter — see Phase 7 § D.4a.)
287
291
  16. Run `npm run lint` and `npx tsc --noEmit` (when `stack.language` includes typescript) to verify nothing broke (redirect to disk per § "Context economy"). If doc-reviewer touched any source-adjacent file (a `.ts`/`.tsx` helper, a co-located doc export), also run `npm run build`. If any check fails, apply the self-healing retry loop (up to 3 times, no user prompt). **If still failing after 3 retries**: do NOT fall through silently to Phase 3.5 — log `[DOC-PHASE-REGRESSION]` in `## Issues & Flags` and invoke `AskUserQuestion` (revert the doc-phase edits that broke the build / keep and fix manually / stop the card).
@@ -127,12 +127,23 @@
127
127
  ## Pre-flight (once)
128
128
 
129
129
  1. Read each backlog card from `${paths.backlog_dir}/*.yml` to understand scope and dependencies. Also read the project's canonical-docs registry — typically `${paths.references_dir}/ssot-registry.md` plus any linking-protocol guide — to understand which canonical docs exist for the feature area being implemented. Exact filenames are listed in `.baldart/overlays/new.md`; skip when absent.
130
- 1b. **Validate card fields (pre-flight gate)** — for each card, verify it has the minimum required fields before queuing it:
131
- - `requirements` — must be a non-empty list (>=1 item)
132
- - `acceptance_criteria` must be a non-empty list (>=1 item)
133
- - `files_likely_touched` must be a non-empty list (>=1 file)
130
+ 1b. **Normalize & validate card baseline (pre-flight gate)** — `/new` is **type-blind**: it runs the same pipeline on `FEAT`/`CHORE`/`BUG`/`DOC`/`PERF`. The universal, profile-aware baseline is the SSOT in [`framework/agents/card-schema.md`](../../../agents/card-schema.md). Detect each card's profile (epic / child / standalone) per that module, then run three stages. (Epic-parent cards are excluded from the per-card loop skip 1b-i/ii for them; the validator at 1b-iii still checks their EPIC column.)
131
+
132
+ **1b-iHALT on non-derivable fields (ask the user).** Verify the fields that cannot be safely synthesized are present and non-empty:
133
+ - `requirements` (>=1), `acceptance_criteria` (>=1), `files_likely_touched` (>=1 file), and `scope`.
134
+ - `scope_boundaries` is **NOT** in this set (it is conditional — legitimately omitted for standalone cards with no siblings; see card-schema.md).
134
135
  If any card fails: log the specific missing fields in `## Issues & Flags`, ask the user to fill them in before proceeding with that card, and continue pre-flight for any remaining valid cards.
135
136
 
137
+ **1b-ii — Back-fill deterministically-computable fields (compute → persist → log).** For each non-epic card missing a derivable field, compute it and **write it back to the card on disk in `$MAIN/${paths.backlog_dir}`** (the main repo path resolved at Phase 0 step 1 — NOT the worktree copy; same F-040 discipline `new2` uses for follow-ups):
138
+ - `review_profile` absent → compute via `prd-card-writer.md § Rule C` (the SSOT), write it.
139
+ - `owner_agent` absent/empty/`claude` → default `coder`, write it. (Never back-fill an epic's `""`.)
140
+ - **Check-and-skip (idempotent):** write ONLY when the field is genuinely absent. A card that already has the field is untouched — so a pre-flight re-entry after compaction never double-writes.
141
+ - Log each write on its own line in `## Issues & Flags`: `[BACKFILL] <CARD-ID>: <field>=<value> (Rule C / coder-default)`.
142
+ - **Commit discipline:** after all cards are processed, if ANY back-fill was written, commit them together in `$MAIN` with the COMMIT_LOCK + doc-freshness precautions of `merge-cleanup.md` Phase 6b (clear stale `COMMIT_LOCK`; `git add` only the back-filled card YAMLs; if the doc-freshness hook blocks, stage `ssot-registry.md`; ≤2 retries): `git commit -m "chore(backlog): normalise card baseline [BACKFILL]"`. Do NOT leave `$MAIN` dirty for the batch duration — an uncommitted backlog edit would surface as noise in the Phase 3/4 git-diff gates.
143
+ - `canonical_docs` / `links.prd` absent on a standalone/non-PRD card → **WARN** only (log, do not block, do not back-fill).
144
+
145
+ **1b-iii — Conformance validation.** Run the framework validator `node .framework/framework/scripts/validate-card-baseline.js <card-yaml-path>` (or the path resolved for your install) against each card. It is profile-aware (epic/child/standalone) and catches what 1b-i/1b-ii could not — invalid enum values (`review_profile` ∉ `{skip,light,balanced,deep}`, `owner_agent` ∉ Rule A enum), or a REQUIRED field still missing. Exit 1 → display the per-field errors in `## Issues & Flags` and HALT that card (continue others). If the validator is not reachable in this install, skip with a one-line note (the 1b-i/1b-ii gates still applied).
146
+
136
147
  1c. **Field Registry Validation (pre-flight gate)** — for each card with a `data_fields` block, run the project's field-validation tool if available (path listed in `.baldart/overlays/new.md`; typically `python3 tools/validate-card-fields.py <card-yaml-path>`).
137
148
  - If exit 1: display the field errors in `## Issues & Flags` and HALT — ask the user to fix the card before proceeding. Do not start implementation until the card passes validation.
138
149
  - If the card has DB-index signals (`db_indexes` — or legacy `firestore_indexes` — or `data.collections`/`data.tables`) but NO `data_fields` block: log WARNING in `## Issues & Flags` — "Card `<ID>` touches the persistence layer but has no `data_fields` block. Field/column names are unvalidated."
@@ -184,13 +184,15 @@ After ALL agents in the group complete successfully:
184
184
  }})
185
185
  ```
186
186
  The workflow fans out the finders per card, runs ONE Codex pass + ONE qa-sentinel (group max tier)
187
- over the union, and **one coder applies all VERIFIED code/perf/security/simplify fixes for the
188
- whole group in a single pass** (files disjoint by ownership no conflict, same as D.3). It returns
189
- `{ codexEngine, perCard, gateTable, summary }`. **Skip the inline D.2 (code portion), D.3, D.3b,
187
+ over the union, and the **domain writer applies all VERIFIED fixes for the whole group** (canonical
188
+ writer map v4.26.1: `security``security-reviewer`, then `code`/`perf`/`migration`/`test`/`simplify`
189
+ `coder`; the two passes run sequentially over disjoint-by-ownership files no conflict, same as D.3).
190
+ It returns `{ codexEngine, perCard, gateTable, summary }`. **Skip the inline D.2 (code portion), D.3, D.3b,
190
191
  D.4, D.4b** below. Then per card handle `perCard[<id>].residual` exactly as the sequential gate does
191
192
  (`references/review-cycle.md` § Phase 2.5x — **by classification first**: `NEEDS_MANUAL_CONFIRMATION`
192
- any-domain → `AskUserQuestion`; else doc residual → the post-E2E doc step; else unconverged
193
- code/perf/security residual → targeted `coder`). Apply the **same BLOCKING QA-gate consumption**:
193
+ any-domain → `AskUserQuestion`; else doc residual → the post-E2E doc step; else unconverged `security`
194
+ residual → targeted `security-reviewer`; else unconverged code/perf residual → targeted `coder`).
195
+ Apply the **same BLOCKING QA-gate consumption**:
194
196
  `gateTable` with any `status:"FAIL"` OR `summary.checksFailed` → coder fix (≤2 retries) then
195
197
  `AskUserQuestion`; **D.5 commit MUST NOT happen until `gateTable` is PASS/SKIP and `checksFailed` is
196
198
  false** (a delegated QA FAIL blocks exactly as inline D.4 / Phase 3.5 would — `gateTable` is
@@ -186,12 +186,15 @@ returns when the batch is done. It returns:
186
186
  "the workflow attempted a write", never proof on disk. So for **every** `residuals[]` entry
187
187
  (regardless of the `materialized` flag), check whether a matching follow-up card actually exists
188
188
  on disk under `${paths.backlog_dir}` in the **MAIN repo** (`<card>-followup-*.yml`). If it is
189
- absent, create it by **delegating to the `prd-card-writer` agent** the same owner the workflow
190
- uses (card-template, Rule C `review_profile`, `owner_agent` routed to the residual's domain,
191
- traceability) derived from the residual (≥1 requirement; `acceptance_criteria` = the verbatim
192
- residual; `files_likely_touched` from the card's ownership). Do NOT hand-write a minimal stub —
193
- the offline path must match agent-path quality (F-039); it MUST pass the `/new` pre-flight field
194
- check. If `prd-card-writer` is unavailable (total outage), fall back to a minimal valid stub. This
189
+ absent, create it by **delegating to the `prd-card-writer` agent in Standalone Single-Card Mode**
190
+ the same owner the workflow uses (full STANDALONE baseline of `framework/agents/card-schema.md`:
191
+ Rule C `review_profile`, `owner_agent` routed to the residual's domain, `scope`, traceability)
192
+ derived from the residual (≥1 requirement; `acceptance_criteria` = the verbatim residual;
193
+ `files_likely_touched` from the card's ownership). Do NOT hand-write a minimal stub the offline
194
+ path must match agent-path quality (F-039); it MUST pass the `/new` pre-flight field check. If
195
+ `prd-card-writer` is unavailable (total outage), fall back to a minimal valid stub carrying the
196
+ step-1b HALT fields — the `/new` pre-flight back-fill (step 1b-ii) then normalizes its
197
+ `review_profile`/`owner_agent` on first ingestion, so even the crisis stub self-heals. This
195
198
  main-repo, **disk-verified** write is the SSOT — nothing is dropped even on a non-merged batch.
196
199
  2. **Mark deferred cards DONE — only after their follow-up exists AND every deferral class allows
197
200
  it (F-040/H + A3).** Some committed cards were intentionally left **NON-DONE** because they carry
@@ -1,6 +1,11 @@
1
1
  # =============================================================================
2
2
  # {{FEAT-ID}} — {{Short title}}
3
3
  # =============================================================================
4
+ # Field SSOT: framework/agents/card-schema.md — the universal, profile-aware
5
+ # (epic/child/standalone) baseline every card satisfies. This template is the
6
+ # CHILD profile; consult card-schema.md for the epic/standalone deltas. The
7
+ # review_profile criteria live in prd-card-writer.md § Rule C; the owner_agent
8
+ # enum in REGISTRY.md — never re-listed here.
4
9
 
5
10
  id: {{FEAT-ID}}
6
11
  title: "{{Full descriptive title}}"
@@ -28,7 +28,11 @@ export const meta = {
28
28
  // gateTable, summary }
29
29
  // ───────────────────────────────────────────────────────────────────────────
30
30
 
31
- const a = args || {}
31
+ // Tolerate args delivered as a JSON string (parse-or-default) — the Workflow tool
32
+ // sometimes serializes a structured `args` object to a string; without this guard
33
+ // `a.cards` is undefined → empty `cards` → degenerate no-op return (cards:0, 0 agents).
34
+ let a = args || {}
35
+ if (typeof a === 'string') { try { a = JSON.parse(a) } catch (_) { a = {} } }
32
36
  const cards = (Array.isArray(a.cards) ? a.cards : []).filter((c) => c && c.cardId)
33
37
  const cfg = a.config || {}
34
38
  const highRisk = (cfg.paths && cfg.paths.high_risk_modules) || [] // security-domain hint
@@ -298,59 +302,83 @@ const surviving = classified
298
302
  .map((f) => ({ ...f, card: attributeCard(f, fileToCard, cards) }))
299
303
 
300
304
  // ───────────────────────────────────────────────────────────────────────────
301
- // Phase Fix — ONE coder applies all VERIFIED code/perf/security/simplify findings.
302
- // doc findingsresidual (the skill runs doc-reviewer post-E2E on final code).
303
- // NEEDS_MANUAL_CONFIRMATION residual (human gate, owned by the skill).
305
+ // Phase Fix — the DOMAIN WRITER applies its verified findings (canonical writer
306
+ // map v4.26.1): security security-reviewer (owns the security-invariant
307
+ // contract never folded into the coder pass); code/perf/migration/test/simplify
308
+ // → coder. doc findings → residual (the skill runs doc-reviewer post-E2E on final
309
+ // code). NEEDS_MANUAL_CONFIRMATION → residual (human gate, owned by the skill).
304
310
  // ───────────────────────────────────────────────────────────────────────────
305
311
  phase('Fix')
306
312
  const isDoc = (f) => /doc|wiki|ssot|readme/.test(String(f.domain).toLowerCase())
313
+ // 'security' domain → security-reviewer. migration STAYS coder (canonical writer map: code/perf/
314
+ // migration/test → coder), so match the exact 'security' domain, not the broader verifier regex.
315
+ const isSecurity = (f) => String(f.domain).toLowerCase() === 'security'
307
316
  const isManual = (f) => f.classification === 'NEEDS_MANUAL_CONFIRMATION'
308
317
  // Partition `surviving` (= VERIFIED + NEEDS_MANUAL; FALSE_POSITIVE already dropped) with NO overlap:
309
- // actionable = VERIFIED non-doc → the coder fixes these.
318
+ // securityFix = VERIFIED security security-reviewer applies (it owns the security invariants).
319
+ // actionable = VERIFIED non-doc non-security → the coder fixes these.
310
320
  // docResidual = VERIFIED doc → the skill runs doc-reviewer post-E2E on final code.
311
321
  // manualResidual= NEEDS_MANUAL any → human gate, owned by the skill (a doc-manual must NOT be
312
322
  // silently auto-re-reviewed: it carries its needs-manual classification out).
313
- const actionable = surviving.filter((f) => f.classification === 'VERIFIED' && !isDoc(f))
323
+ const securityFix = surviving.filter((f) => f.classification === 'VERIFIED' && !isDoc(f) && isSecurity(f))
324
+ const actionable = surviving.filter((f) => f.classification === 'VERIFIED' && !isDoc(f) && !isSecurity(f))
314
325
  const docResidual = surviving.filter((f) => f.classification === 'VERIFIED' && isDoc(f))
315
326
  const manualResidual = surviving.filter(isManual)
316
327
 
317
328
  const SKIP_CHECKS = { lint: 'SKIP', tsc: 'SKIP', build: 'SKIP' }
318
- let fixResult = { applied: [], unresolved: [], checks: { ...SKIP_CHECKS } }
319
- if (actionable.length && unionEditable.length) {
329
+
330
+ // One fix pass: the domain WRITER applies its verified findings to the worktree, then re-verifies.
331
+ // Passes run SEQUENTIALLY (security-reviewer before coder) so edits on shared files never conflict
332
+ // without having to partition the ownership map; the last pass to run carries the build it verified.
333
+ async function applyFixPass(findings, writer, label, role) {
334
+ if (!findings.length) return { applied: [], unresolved: [], checks: { ...SKIP_CHECKS }, ran: false }
335
+ if (!unionEditable.length) {
336
+ log(`Fix: ${findings.length} ${label} finding(s) but no editable files in scope — returned as residual (${writer} skipped).`)
337
+ return { applied: [], unresolved: findings.map((f) => f.finding_id), checks: { ...SKIP_CHECKS }, ran: false }
338
+ }
320
339
  const fixBrief =
321
- `Apply ALL of the verified review findings below to the worktree, then verify the build. You are the SINGLE fix pass for this wave.\n\n` +
340
+ `Apply ALL of the verified ${role} review findings below to the worktree, then verify the build. You are the ${writer} fix pass for this wave.\n\n` +
322
341
  `Worktree: ${a.worktreePath || '(cwd)'} — cd into it.\n` +
323
342
  `You MAY edit ONLY these files (ownership map — touching anything else is a violation):\n${unionEditable.join('\n')}\n\n` +
324
- `Findings to fix (grouped — fix the code, not the tests unless a test itself is wrong; do NOT expand scope beyond the finding):\n` +
325
- actionable.map((f) => `- [${f.finding_id}] (${f.card || '?'} / ${f.domain} / ${f.severity}) ${f.title}\n evidence: ${f.evidence}\n direction: ${f.minimal_fix_direction}`).join('\n') +
343
+ `Findings to fix (fix the code, not the tests unless a test itself is wrong; do NOT expand scope beyond the finding):\n` +
344
+ findings.map((f) => `- [${f.finding_id}] (${f.card || '?'} / ${f.domain} / ${f.severity}) ${f.title}\n evidence: ${f.evidence}\n direction: ${f.minimal_fix_direction}`).join('\n') +
326
345
  `\n\nAfter applying: run \`npm run lint\` and (when the project uses typescript) \`npx tsc --noEmit\` and \`npm run build\` in the worktree. If a check fails because of an edit you made, fix the regression — at most 2 retries — staying within the allowed files. ` +
327
346
  `Do NOT commit. Do NOT git stash (refs/stash is shared across worktrees). ` +
328
347
  `Return: applied (finding_ids you fixed), unresolved (finding_ids you could NOT fix within the allowed files / 2 retries), and checks (PASS/FAIL/SKIP for lint, tsc, build).`
329
- const r = await agent(fixBrief, { label: 'fix-coder', phase: 'Fix', agentType: 'coder', schema: FIX_SCHEMA })
330
- // Normalize: the coder may die (null) or return a truthy object missing fields.
331
- fixResult = (r && typeof r === 'object') ? r : { applied: [], unresolved: actionable.map((f) => f.finding_id), checks: { ...SKIP_CHECKS } }
332
- if (!Array.isArray(fixResult.applied)) fixResult.applied = []
333
- if (!Array.isArray(fixResult.unresolved)) fixResult.unresolved = []
334
- if (!fixResult.checks || typeof fixResult.checks !== 'object') fixResult.checks = { ...SKIP_CHECKS }
335
- log(`Fix: coder applied ${fixResult.applied.length}/${actionable.length} finding(s); checks lint=${fixResult.checks.lint} tsc=${fixResult.checks.tsc} build=${fixResult.checks.build}.`)
336
- } else if (actionable.length) {
337
- // Actionable findings exist but NO editable files are mapped → cannot fix; return all as residual
338
- // (no wasted coder spawn — the skill will route them to a targeted coder with a proper ownership scope).
339
- fixResult = { applied: [], unresolved: actionable.map((f) => f.finding_id), checks: { ...SKIP_CHECKS } }
340
- log(`Fix: ${actionable.length} actionable finding(s) but no editable files in scope — returned as residual (coder skipped).`)
341
- } else {
342
- log('Fix: no actionable code/perf/security/simplify findings — coder skipped.')
348
+ const r = await agent(fixBrief, { label, phase: 'Fix', agentType: writer, schema: FIX_SCHEMA })
349
+ // Normalize: the agent may die (null) or return a truthy object missing fields.
350
+ const res = (r && typeof r === 'object') ? r : { applied: [], unresolved: findings.map((f) => f.finding_id), checks: { ...SKIP_CHECKS } }
351
+ if (!Array.isArray(res.applied)) res.applied = []
352
+ if (!Array.isArray(res.unresolved)) res.unresolved = []
353
+ if (!res.checks || typeof res.checks !== 'object') res.checks = { ...SKIP_CHECKS }
354
+ res.ran = true
355
+ log(`Fix: ${writer} applied ${res.applied.length}/${findings.length} ${label} finding(s); checks lint=${res.checks.lint} tsc=${res.checks.tsc} build=${res.checks.build}.`)
356
+ return res
343
357
  }
344
358
 
359
+ // Security writer FIRST (owns the security-invariant contract), then the coder. Sequential → no
360
+ // edit conflict on shared files; the coder pass (when it runs) carries the authoritative build.
361
+ const secResult = await applyFixPass(securityFix, 'security-reviewer', 'fix-security', 'security')
362
+ const codeFixResult = await applyFixPass(actionable, 'coder', 'fix-coder', 'code/perf/simplify')
363
+ if (!securityFix.length && !actionable.length) log('Fix: no actionable code/perf/security/simplify findings — fixers skipped.')
364
+
365
+ // Merge the two passes. A FAIL in EITHER pass fails the wave; PASS only when a pass actually ran it.
366
+ const fixPasses = [secResult, codeFixResult]
367
+ const allActionable = [...securityFix, ...actionable]
368
+ const appliedIds = new Set(fixPasses.flatMap((p) => (p.applied || []).map((x) => x.finding_id)))
369
+ const unresolvedIds = new Set(fixPasses.flatMap((p) => p.unresolved || []))
370
+ const ranChecks = fixPasses.filter((p) => p.ran).map((p) => p.checks)
371
+ const mergedChecks = ['lint', 'tsc', 'build'].reduce((acc, k) => {
372
+ acc[k] = ranChecks.some((c) => c[k] === 'FAIL') ? 'FAIL' : (ranChecks.some((c) => c[k] === 'PASS') ? 'PASS' : 'SKIP')
373
+ return acc
374
+ }, {})
345
375
  // Unfixed actionable findings become residual (human/coder follow-up owned by the skill).
346
- const appliedIds = new Set((fixResult.applied || []).map((x) => x.finding_id))
347
- const unresolvedIds = new Set(fixResult.unresolved || [])
348
- const codeResidual = actionable.filter((f) => !appliedIds.has(f.finding_id) || unresolvedIds.has(f.finding_id))
349
- const checksFailed = ['lint', 'tsc', 'build'].some((k) => fixResult.checks && fixResult.checks[k] === 'FAIL')
376
+ const codeResidual = allActionable.filter((f) => !appliedIds.has(f.finding_id) || unresolvedIds.has(f.finding_id))
377
+ const checksFailed = ['lint', 'tsc', 'build'].some((k) => mergedChecks[k] === 'FAIL')
350
378
 
351
379
  // ---- Assemble per-card result ----------------------------------------------
352
380
  function bucket(cardId) { return perCard[cardId] || (perCard[cardId] = { fixesApplied: [], residual: [] }) }
353
- for (const f of actionable) {
381
+ for (const f of allActionable) {
354
382
  if (appliedIds.has(f.finding_id) && !unresolvedIds.has(f.finding_id)) {
355
383
  bucket(f.card || cards[0].cardId).fixesApplied.push(`[${f.finding_id}] ${f.title}`)
356
384
  }
@@ -24,7 +24,11 @@ export const meta = {
24
24
  // { codexEngine, findings:[…classified, FALSE_POSITIVE dropped], gateTable, summary }
25
25
  // ───────────────────────────────────────────────────────────────────────────
26
26
 
27
- const a = args || {}
27
+ // Tolerate args delivered as a JSON string (parse-or-default) — the Workflow tool
28
+ // sometimes serializes a structured `args` object to a string; without this guard
29
+ // `a.reviewScopeFiles`/`a.cardPaths` are undefined → empty scope → degenerate no-op return.
30
+ let a = args || {}
31
+ if (typeof a === 'string') { try { a = JSON.parse(a) } catch (_) { a = {} } }
28
32
  const scope = Array.isArray(a.reviewScopeFiles) ? a.reviewScopeFiles : []
29
33
  const cards = Array.isArray(a.cardPaths) ? a.cardPaths : []
30
34
  const cfg = a.config || {}
@@ -0,0 +1,112 @@
1
+ # Atomic Card Baseline Schema
2
+
3
+ ## Purpose
4
+
5
+ Define the **universal baseline** every backlog card MUST satisfy — for ANY prefix
6
+ (`FEAT`/`CHORE`/`BUG`/`DOC`/`PERF`/`UI`) and ANY origin (a `/prd` run, a follow-up, a
7
+ GitHub-issue triage, a manual edit). `/new` and `/new2` consume cards **type-blind**: they
8
+ scale per-card review depth on `review_profile` and run the same pipeline regardless of prefix.
9
+ A card that drifts off this baseline (e.g. a `CHORE` with no `review_profile` or no `scope`)
10
+ silently degrades that pipeline. This module is the **single source of truth** for *which
11
+ fields a card needs* and *in what state*; every writer and every consumer cites it.
12
+
13
+ ## Scope
14
+
15
+ **In**: the per-profile field-state matrix, the profile-detection rule, and the
16
+ consumer contract (HALT / BACK-FILL / WARN) that `/new` ingestion enforces.
17
+ **Out**: the *value* decisions — `owner_agent` routing lives in `prd-card-writer.md § Rule A`
18
+ (enum SSOT: `.claude/agents/REGISTRY.md`); `review_profile` criteria live in
19
+ `prd-card-writer.md § Rule C`. This module says a card MUST carry those fields and in what
20
+ state; it **never copies** the Rule A enum or the Rule C decision table (doing so would
21
+ re-introduce the drift those SSOTs exist to prevent).
22
+
23
+ ## The three card profiles
24
+
25
+ Baseline requirements are **profile-aware** — a flat "every field required and non-empty"
26
+ rule would wrongly reject valid cards (an epic legitimately has `owner_agent: ""` and
27
+ `files_likely_touched: []`; a standalone CHORE legitimately has no `scope_boundaries` or
28
+ `links.prd`). Detect the profile first, then apply its column:
29
+
30
+ - **EPIC** — filename matches `*-epic.yml`, OR `group.sequence == 0`, OR `review_profile: skip`
31
+ with `owner_agent: ""`. A tracker, never implemented. `/new` excludes it from the per-card
32
+ loop.
33
+ - **STANDALONE** — a single card with no PRD/epic lineage: no `group.parent` pointing at a
34
+ `-00` epic AND no `links.prd`. Typical: a follow-up (`<id>-followup-<gate>.yml`), a
35
+ graph-align CHORE, an ad-hoc BUG.
36
+ - **CHILD** — a card produced under a PRD epic (`group.parent` → a `-00` epic id). The default
37
+ for `/prd`-generated work.
38
+
39
+ ## Field-state matrix
40
+
41
+ Legend: **R** = required, present **and non-empty** · **E** = required key, value MAY be empty
42
+ (`[]` / `""`) · **C** = conditional, omittable per a documented carve-out (never a HALT) ·
43
+ **—** = not applicable for this profile.
44
+
45
+ | Field | EPIC | CHILD | STANDALONE | Notes |
46
+ |---|---|---|---|---|
47
+ | `id` | R | R | R | |
48
+ | `title` | R | R | R | |
49
+ | `status` | R | R | R | enum `TODO`/`IN_PROGRESS`/`DONE` |
50
+ | `priority` | R | R | R | enum `HIGH`/`MEDIUM`/`LOW` |
51
+ | `owner_agent` | E (`""`) | R | R | CHILD/STANDALONE ∈ Rule A enum; EPIC is `""` (tracker) |
52
+ | `execution_mode` | R | R | R | |
53
+ | `group` | R (`sequence: 0`) | R (`parent`+`sequence`) | C | STANDALONE may set `group.parent` to a tracking slug / originating id; `sequence` omittable |
54
+ | `git_strategy` | R | R | R | branch/base/target |
55
+ | `context` | R | R | R | |
56
+ | `scope` | R | R | R | **never omittable** — encodes intent |
57
+ | `scope_boundaries` | R | C | C | "Omit for standalone cards with no siblings" (`prd-card-writer.md`) |
58
+ | `requirements` | C | R | R | EPIC tracks via AC-EPIC, not `requirements` |
59
+ | `acceptance_criteria` | R (AC-EPIC) | R | R | |
60
+ | `definition_of_done` | R | R | R | |
61
+ | `depends_on` / `blocks` | E (`[]`) | E | E | |
62
+ | `estimated_complexity` | C | R | R | |
63
+ | `review_profile` | R (`skip`) | R | R | ∈ `{skip,light,balanced,deep}`; CHILD/STANDALONE via Rule C |
64
+ | `parallel_group` | R (`0`) | R | C | |
65
+ | `files_likely_touched` | E (`[]`, never code) | R | R | EPIC tracks docs only |
66
+ | `links.prd` | C | R | C | STANDALONE/non-PRD cards legitimately lack a PRD |
67
+ | `links.design` | C | C | C | required when the card has UI scope |
68
+ | `canonical_docs` | R | R | C (WARN) | "REQUIRED for all cards generated from a PRD"; a manual CHORE may lack it |
69
+ | `data_sources` | E (`[]`) | E (`[]` if no data) | E | key always present, value may be `[]` |
70
+ | `documentation_impact` | R | C | C | |
71
+ | `execution_strategy` | R | — | — | epic-only orchestration block |
72
+ | `data_fields` | — | C | C | only if it reads/writes persistence |
73
+ | `db_indexes` | — | C | C | only if it adds a compound query |
74
+ | `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) |
75
+ | `assumptions` · `unknowns` · `notes` | C | C | C | |
76
+
77
+ ## Consumer contract — HALT / BACK-FILL / WARN
78
+
79
+ `/new` and `/new2` ingestion (pre-flight) enforce this matrix against the detected profile.
80
+ The three classes are deliberately distinct so a thin card is fixed where it can be and only
81
+ blocks where intent is genuinely missing:
82
+
83
+ - **HALT (non-derivable — ask the user):** a **R** field that cannot be safely synthesized.
84
+ The canonical HALT set is `requirements`, `acceptance_criteria`, `files_likely_touched`,
85
+ and `scope` (CHILD/STANDALONE). `scope_boundaries` is **NOT** in the HALT set — it is **C**.
86
+ - **BACK-FILL (deterministically computable — compute, persist, log):** `review_profile`
87
+ (compute via `prd-card-writer.md § Rule C`) and `owner_agent` (default `coder` for a
88
+ non-epic; **never** back-fill an epic's `""`). Write the value to the card on disk in the
89
+ **main repo** with a `[BACKFILL] <id>: <field>=<value>` log. **Check-and-skip**: only write
90
+ when the field is genuinely absent (idempotent across pre-flight re-entry).
91
+ - **WARN (expected but tolerable absent):** `canonical_docs` / `links.prd` on a STANDALONE /
92
+ non-PRD card. Log, do not block.
93
+
94
+ A back-fill never rescues a card missing a HALT field: a card without `scope` blocks regardless
95
+ of how many derivable fields were filled.
96
+
97
+ ## Writer contract
98
+
99
+ The canonical writer is the `prd-card-writer` agent. It emits the full baseline for the
100
+ relevant profile — including its **Standalone Single-Card Mode** for follow-up / CHORE / BUG /
101
+ issue cards (see `prd-card-writer.md`). **Every** card-creation path delegates to it rather
102
+ than hand-writing a partial stub; the only exception is a documented crisis fallback on total
103
+ agent outage, and even that stub is normalized by the consumer contract above on first
104
+ ingestion. New writers MUST cite this module, not re-list fields.
105
+
106
+ ## Validator
107
+
108
+ `framework/scripts/validate-card-baseline.js` (and the CI sibling
109
+ `scripts/check-card-baseline.js`) parse this matrix, detect the card's profile, and assert
110
+ each field's state (R present+non-empty, E key present, C ignored, enums valid). They read the
111
+ field list from THIS file — never an embedded copy — so the schema and the validator cannot
112
+ drift.
@@ -23,6 +23,7 @@ Route agents to the right module with minimal reading.
23
23
  - If touching design-review workflows or UI guidelines -> read `agents/design-review.md` and project-specific UI guidelines.
24
24
  - If touching architecture, auth, or tech stack -> read `agents/architecture.md`.
25
25
  - If touching workflow/process/commits/backlog -> read `agents/workflows.md`.
26
+ - If CREATING or MUTATING a backlog card (any prefix — `FEAT`/`CHORE`/`BUG`/`DOC`/`PERF`/`UI`), or consuming one type-blind (`/new`, `/new2`) -> read `agents/card-schema.md` (the universal, profile-aware baseline) before writing/validating fields.
26
27
  - If touching testing or QA issues -> read `agents/testing.md` (also documents the scope-aware,
27
28
  profile-driven test-selection strategy consumed by `qa-sentinel`).
28
29
  - If touching GitHub issues or issue workflow -> read `agents/github-issue-subagent.md`.
@@ -65,6 +66,7 @@ When adding or updating agents, update REGISTRY.md — not this file.
65
66
  - `agents/code-search-protocol.md` — Retrieval hierarchy for code search: LSP → Grep → Git (since v3.10.0, gated on `features.has_lsp_layer`)
66
67
  - `agents/code-graph-protocol.md` — Structural/relational retrieval via the Graphify code knowledge graph (since v4.21.0, gated on `features.has_code_graph`)
67
68
  - `agents/design-system-protocol.md` — Registry-first discipline for UI work: BLOCKING cascade on `INDEX.md` + `tokens-reference.md` + `components/<Name>.md` (since v3.11.0, gated on `features.has_design_system`)
69
+ - `agents/card-schema.md` — Atomic Card Baseline Schema: the universal, profile-aware (epic/child/standalone) field contract every backlog card satisfies, plus the consumer HALT/BACK-FILL/WARN contract (since v4.35.0)
68
70
 
69
71
  ## Where to Document (Decision Tree)
70
72
 
@@ -0,0 +1,263 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * validate-card-baseline.js — profile-aware validator for backlog cards.
4
+ *
5
+ * Why this exists (v4.35.0): `/new` and `/new2` consume cards type-blind and scale
6
+ * per-card review depth on `review_profile`. A card that drifts off the universal
7
+ * baseline (e.g. a CHORE with no `review_profile` or no `scope`) silently degrades
8
+ * that pipeline. This validator is the machine-checkable enforcement of the SSOT
9
+ * field-state matrix in `framework/agents/card-schema.md`.
10
+ *
11
+ * It is PROFILE-AWARE — a flat "every field required, non-empty" rule would wrongly
12
+ * reject valid cards (an epic has `owner_agent: ""` and `files_likely_touched: []`;
13
+ * a standalone CHORE has no `scope_boundaries`/`links.prd`). It detects epic / child /
14
+ * standalone and applies that profile's column.
15
+ *
16
+ * SSOT, never embedded: the field-state matrix is PARSED from card-schema.md and the
17
+ * `owner_agent`/`status` enums from REGISTRY.md, so the validator cannot drift from
18
+ * the docs it enforces.
19
+ *
20
+ * Usage:
21
+ * node framework/scripts/validate-card-baseline.js <card.yml> [<card2.yml> ...]
22
+ * Exit 0 = all valid; exit 1 = at least one card has errors (printed per-field).
23
+ *
24
+ * Module API (for CI self-tests in scripts/check-card-baseline.js):
25
+ * const { validateCard, detectProfile, loadSchema, loadEnums } = require(...)
26
+ */
27
+ 'use strict';
28
+
29
+ const fs = require('fs');
30
+ const path = require('path');
31
+ const yaml = require('js-yaml');
32
+
33
+ const SCRIPT_DIR = __dirname;
34
+ const SCHEMA_MD = path.join(SCRIPT_DIR, '..', 'agents', 'card-schema.md');
35
+ const REGISTRY_MD = path.join(SCRIPT_DIR, '..', '.claude', 'agents', 'REGISTRY.md');
36
+
37
+ const STATIC_ENUMS = {
38
+ priority: ['HIGH', 'MEDIUM', 'LOW'],
39
+ execution_mode: ['local', 'cloud'],
40
+ review_profile: ['skip', 'light', 'balanced', 'deep'],
41
+ };
42
+
43
+ // --- SSOT parsers -----------------------------------------------------------
44
+
45
+ /**
46
+ * Parse the field-state matrix from card-schema.md.
47
+ * Returns { field: { EPIC, CHILD, STANDALONE } } where each value is R|E|C|NA.
48
+ */
49
+ function loadSchema(schemaPath = SCHEMA_MD) {
50
+ const text = fs.readFileSync(schemaPath, 'utf8');
51
+ const lines = text.split('\n');
52
+ const matrix = {};
53
+ let inTable = false;
54
+ let cols = null; // index map after the header
55
+ for (const raw of lines) {
56
+ const line = raw.trim();
57
+ const isRow = line.startsWith('|') && line.endsWith('|');
58
+ if (!inTable) {
59
+ if (isRow && /\|\s*Field\s*\|/i.test(line) && /EPIC/i.test(line) && /CHILD/i.test(line)) {
60
+ // header row found — map column order
61
+ const headers = splitRow(line).map((h) => h.toUpperCase());
62
+ cols = {
63
+ EPIC: headers.indexOf('EPIC'),
64
+ CHILD: headers.indexOf('CHILD'),
65
+ STANDALONE: headers.indexOf('STANDALONE'),
66
+ };
67
+ inTable = true;
68
+ }
69
+ continue;
70
+ }
71
+ if (!isRow) break; // table ended
72
+ const cells = splitRow(line);
73
+ // skip the markdown separator row (|---|---|)
74
+ if (cells.every((c) => /^:?-+:?$/.test(c))) continue;
75
+ const fields = extractBackticked(cells[0]);
76
+ if (!fields.length) continue;
77
+ const state = (cell) => parseState(cells[cell]);
78
+ const entry = { EPIC: state(cols.EPIC), CHILD: state(cols.CHILD), STANDALONE: state(cols.STANDALONE) };
79
+ for (const f of fields) matrix[f] = entry;
80
+ }
81
+ if (!Object.keys(matrix).length) {
82
+ throw new Error(`validate-card-baseline: could not parse field-state matrix from ${schemaPath}`);
83
+ }
84
+ return matrix;
85
+ }
86
+
87
+ function splitRow(line) {
88
+ // drop leading/trailing pipe, split on |, trim
89
+ return line.replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim());
90
+ }
91
+
92
+ function extractBackticked(cell) {
93
+ const out = [];
94
+ const re = /`([^`]+)`/g;
95
+ let m;
96
+ while ((m = re.exec(cell)) !== null) {
97
+ // keep dotted paths (links.prd) and plain identifiers; drop anything with spaces/values
98
+ const tok = m[1].trim();
99
+ if (/^[A-Za-z_][A-Za-z0-9_.]*$/.test(tok)) out.push(tok);
100
+ }
101
+ return out;
102
+ }
103
+
104
+ function parseState(cell) {
105
+ if (cell == null) return 'NA';
106
+ const c = cell.trim();
107
+ if (c === '' || c.startsWith('—') || c.startsWith('-')) return 'NA';
108
+ const ch = c[0].toUpperCase();
109
+ if (ch === 'R') return 'R';
110
+ if (ch === 'E') return 'E';
111
+ if (ch === 'C') return 'C';
112
+ return 'NA';
113
+ }
114
+
115
+ /** Parse the owner_agent + status enums from REGISTRY.md (fenced `- value` lists). */
116
+ function loadEnums(registryPath = REGISTRY_MD) {
117
+ const fallback = {
118
+ owner_agent: ['coder', 'ui-expert', 'plan', 'visual-designer', 'motion-expert'],
119
+ status: ['TODO', 'READY', 'IN_PROGRESS', 'BLOCKED', 'DONE'],
120
+ };
121
+ let text;
122
+ try { text = fs.readFileSync(registryPath, 'utf8'); } catch { return fallback; }
123
+ return {
124
+ owner_agent: parseEnumBlock(text, 'owner_agent enum') || fallback.owner_agent,
125
+ status: parseEnumBlock(text, 'status enum') || fallback.status,
126
+ };
127
+ }
128
+
129
+ function parseEnumBlock(text, header) {
130
+ const idx = text.indexOf(`${header}:`);
131
+ if (idx === -1) return null;
132
+ const rest = text.slice(idx);
133
+ const vals = [];
134
+ const lines = rest.split('\n').slice(1); // after the `<header>:` line
135
+ for (const line of lines) {
136
+ const m = line.match(/^\s*-\s*([A-Za-z0-9_-]+)/);
137
+ if (m) { vals.push(m[1]); continue; }
138
+ if (line.trim().startsWith('```')) break; // end of fenced block
139
+ if (line.trim() === '' && vals.length) break;
140
+ }
141
+ return vals.length ? vals : null;
142
+ }
143
+
144
+ // --- profile detection + validation ----------------------------------------
145
+
146
+ function detectProfile(card, filename = '') {
147
+ const isEpic =
148
+ /-epic\.ya?ml$/i.test(filename) ||
149
+ card.group?.sequence === 0 ||
150
+ (card.review_profile === 'skip' && card.owner_agent === '');
151
+ if (isEpic) return 'EPIC';
152
+ const hasPrd = !!(card.links && card.links.prd);
153
+ const parent = card.group && card.group.parent;
154
+ const parentIsEpic = typeof parent === 'string' && /-0*0$/.test(parent.trim());
155
+ const isStandalone = !hasPrd && !parentIsEpic;
156
+ return isStandalone ? 'STANDALONE' : 'CHILD';
157
+ }
158
+
159
+ function getPath(obj, dotted) {
160
+ return dotted.split('.').reduce((o, k) => (o == null ? undefined : o[k]), obj);
161
+ }
162
+
163
+ function present(v) {
164
+ return v !== undefined && v !== null;
165
+ }
166
+
167
+ function nonEmpty(v) {
168
+ if (!present(v)) return false;
169
+ if (typeof v === 'string') return v.trim() !== '';
170
+ if (Array.isArray(v)) return v.length > 0;
171
+ if (typeof v === 'object') return Object.keys(v).length > 0;
172
+ return true; // numbers, booleans
173
+ }
174
+
175
+ /**
176
+ * Validate a parsed card object against the schema matrix + enums.
177
+ * Returns { profile, errors: string[] }.
178
+ */
179
+ function validateCard(card, { matrix, enums, filename = '' } = {}) {
180
+ matrix = matrix || loadSchema();
181
+ enums = enums || loadEnums();
182
+ const profile = detectProfile(card, filename);
183
+ const errors = [];
184
+
185
+ for (const [field, states] of Object.entries(matrix)) {
186
+ const state = states[profile];
187
+ if (state === 'NA' || state === 'C') continue;
188
+ const val = field.includes('.') ? getPath(card, field) : card[field];
189
+ if (state === 'R' && !nonEmpty(val)) {
190
+ errors.push(`REQUIRED field '${field}' is missing or empty (profile ${profile})`);
191
+ } else if (state === 'E' && !present(field.includes('.') ? getPath(card, field) : card[field]) && !(field in card)) {
192
+ errors.push(`field '${field}' must be present as a key (value may be empty) (profile ${profile})`);
193
+ }
194
+ }
195
+
196
+ // Enum checks (only when the value is present and non-empty).
197
+ const enumChecks = [
198
+ ['status', enums.status],
199
+ ['priority', STATIC_ENUMS.priority],
200
+ ['execution_mode', STATIC_ENUMS.execution_mode],
201
+ ['review_profile', STATIC_ENUMS.review_profile],
202
+ ];
203
+ for (const [field, allowed] of enumChecks) {
204
+ const v = card[field];
205
+ if (nonEmpty(v) && !allowed.includes(String(v))) {
206
+ errors.push(`'${field}' = '${v}' is not in enum {${allowed.join(', ')}}`);
207
+ }
208
+ }
209
+ // owner_agent: '' is legal ONLY for epics; otherwise must be in the Rule A enum.
210
+ const oa = card.owner_agent;
211
+ if (profile === 'EPIC') {
212
+ if (oa !== '' && oa != null && !enums.owner_agent.includes(String(oa))) {
213
+ errors.push(`epic 'owner_agent' must be '' or in {${enums.owner_agent.join(', ')}} (got '${oa}')`);
214
+ }
215
+ } else if (nonEmpty(oa) && !enums.owner_agent.includes(String(oa))) {
216
+ errors.push(`'owner_agent' = '${oa}' is not in enum {${enums.owner_agent.join(', ')}}`);
217
+ }
218
+
219
+ return { profile, errors };
220
+ }
221
+
222
+ // --- CLI --------------------------------------------------------------------
223
+
224
+ function main(argv) {
225
+ const files = argv.slice(2);
226
+ if (!files.length) {
227
+ process.stderr.write('Usage: validate-card-baseline.js <card.yml> [<card2.yml> ...]\n');
228
+ return 2;
229
+ }
230
+ const matrix = loadSchema();
231
+ const enums = loadEnums();
232
+ let failed = 0;
233
+ for (const file of files) {
234
+ let card;
235
+ try {
236
+ card = yaml.load(fs.readFileSync(file, 'utf8'));
237
+ } catch (e) {
238
+ process.stdout.write(`✖ ${file}: YAML parse error — ${e.message}\n`);
239
+ failed++;
240
+ continue;
241
+ }
242
+ if (!card || typeof card !== 'object') {
243
+ process.stdout.write(`✖ ${file}: not a YAML mapping\n`);
244
+ failed++;
245
+ continue;
246
+ }
247
+ const { profile, errors } = validateCard(card, { matrix, enums, filename: path.basename(file) });
248
+ if (errors.length) {
249
+ failed++;
250
+ process.stdout.write(`✖ ${file} [${profile}] — ${errors.length} issue(s):\n`);
251
+ for (const e of errors) process.stdout.write(` - ${e}\n`);
252
+ } else {
253
+ process.stdout.write(`✓ ${file} [${profile}]\n`);
254
+ }
255
+ }
256
+ return failed ? 1 : 0;
257
+ }
258
+
259
+ if (require.main === module) {
260
+ process.exit(main(process.argv));
261
+ }
262
+
263
+ module.exports = { validateCard, detectProfile, loadSchema, loadEnums, nonEmpty };
@@ -0,0 +1,48 @@
1
+ # Backlog card-baseline gate (BALDART v4.35.0) — OPT-IN consumer template.
2
+ #
3
+ # Copy this file to .github/workflows/ in your repo to fail a PR when a backlog
4
+ # card drifts off the universal, profile-aware baseline (the SSOT field-state
5
+ # matrix in .framework/framework/agents/card-schema.md). This catches a manually
6
+ # or ad-hoc authored card (e.g. a CHORE with no review_profile or no scope) at
7
+ # PR time — not only when /new ingests it.
8
+ #
9
+ # The validator is profile-aware (epic / child / standalone) and ships with the
10
+ # framework subtree; if it is absent the gate no-ops (run `baldart update`).
11
+
12
+ name: Backlog card baseline
13
+
14
+ on:
15
+ pull_request:
16
+ paths:
17
+ - 'backlog/**'
18
+
19
+ jobs:
20
+ card-baseline:
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - name: Checkout
24
+ uses: actions/checkout@v4
25
+
26
+ - name: Setup Node
27
+ uses: actions/setup-node@v4
28
+ with:
29
+ node-version: '20'
30
+
31
+ - name: Install js-yaml (validator dependency)
32
+ run: npm i js-yaml --no-save
33
+
34
+ - name: Validate backlog card baseline
35
+ run: |
36
+ VALIDATOR=.framework/framework/scripts/validate-card-baseline.js
37
+ if [ ! -f "$VALIDATOR" ]; then
38
+ echo "card-baseline validator not found at $VALIDATOR — run 'baldart update'. Skipping."
39
+ exit 0
40
+ fi
41
+ # Adjust the glob if your cards live elsewhere (baldart.config.yml paths.backlog_dir).
42
+ shopt -s nullglob
43
+ CARDS=(backlog/*.yml backlog/*.yaml)
44
+ if [ ${#CARDS[@]} -eq 0 ]; then
45
+ echo "No backlog cards to validate."
46
+ exit 0
47
+ fi
48
+ node "$VALIDATOR" "${CARDS[@]}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "4.34.2",
3
+ "version": "4.36.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"