baldart 4.34.1 → 4.35.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 +32 -0
- package/README.md +1 -1
- package/VERSION +1 -1
- package/framework/.claude/agents/prd-card-writer.md +39 -1
- package/framework/.claude/commands/issue-review.md +1 -1
- package/framework/.claude/skills/new/SKILL.md +8 -7
- package/framework/.claude/skills/new/references/completeness.md +1 -1
- package/framework/.claude/skills/new/references/final-review.md +13 -2
- package/framework/.claude/skills/new/references/review-cycle.md +15 -2
- package/framework/.claude/skills/new/references/setup.md +15 -4
- package/framework/.claude/skills/new2/SKILL.md +9 -6
- package/framework/.claude/skills/prd/assets/card-template.yml +5 -0
- package/framework/agents/card-schema.md +112 -0
- package/framework/agents/index.md +2 -0
- package/framework/scripts/validate-card-baseline.js +263 -0
- package/framework/templates/ci/check-card-baseline.yml +48 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,38 @@ 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.35.0] - 2026-06-13
|
|
9
|
+
|
|
10
|
+
**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.
|
|
11
|
+
|
|
12
|
+
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).
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`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.
|
|
17
|
+
- **`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.
|
|
18
|
+
- **`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.
|
|
19
|
+
- **`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).
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **`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.
|
|
24
|
+
- **`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).
|
|
25
|
+
- **`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).
|
|
26
|
+
- **`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).
|
|
27
|
+
- **`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.
|
|
28
|
+
- **`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.
|
|
29
|
+
- **`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.
|
|
30
|
+
|
|
31
|
+
## [4.34.2] - 2026-06-13
|
|
32
|
+
|
|
33
|
+
**`/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).
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **`framework/.claude/skills/new/references/review-cycle.md`** — Phase 2.5x branch reframed as a "MECHANICAL, BINARY decision" with an explicit **no-discretion** rule: the soft factors (N=1, small diff, marginal benefit, "a workflow once degenerated", "inline is safer") are named and declared **NOT inputs to the gate**; the `new2` degeneration conflation is corrected inline; running inline while the conditions held is now defined as a **gate violation** with a mandated telemetry log line.
|
|
38
|
+
- **`framework/.claude/skills/new/references/final-review.md`** — Step F.1.5 branch given the same imperative treatment (MUST-delegate, no-discretion rule, `new2`-conflation correction, gate-violation log line).
|
|
39
|
+
|
|
8
40
|
## [4.34.1] - 2026-06-12
|
|
9
41
|
|
|
10
42
|
**`reference-integrity` CI gate: recognize the plumbing carve-out.** v4.33.2 introduced a legitimate `subagent_type: general-purpose` dispatch for the mechanical file-revert agent (the REGISTRY.md plumbing carve-out — mechanical git/file ops, never code authoring), but the CI `reference-integrity` gate's R11 anti-fallback rule banned `general-purpose` **unconditionally**, so `main` went red on the v4.33.2 / v4.34.0 tags (the npm publish workflow is independent and succeeded — both versions are live). The gate now allows `subagent_type: general-purpose` **only** on a dispatch line explicitly marked `plumbing carve-out`; every other `general-purpose` dispatch stays banned (R11 intact, intentionally narrow so it can't be abused as a generic fallback). **PATCH** (CI gate fix; no behavior change to `/new`).
|
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/**:
|
|
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.
|
|
1
|
+
4.35.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`
|
|
@@ -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
|
|
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.
|
|
@@ -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
|
-
**
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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.
|
|
@@ -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
|
|
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.
|
|
@@ -54,11 +54,22 @@ diff and emits findings, it touches no git state and asks the user nothing. That
|
|
|
54
54
|
makes it a clean fit for a **dynamic workflow**, which runs the fan-out as a
|
|
55
55
|
deterministic script outside this orchestrator's context window.
|
|
56
56
|
|
|
57
|
-
**Branch (decide once, here)
|
|
57
|
+
**Branch (decide once, here).** This is a **MECHANICAL, BINARY decision** — check the two
|
|
58
|
+
conditions and act on the result. You have **NO discretion**: if both hold you **MUST** delegate.
|
|
59
|
+
A single-card (N=1) batch, a small diff, "the context-economy benefit is marginal here", "a workflow
|
|
60
|
+
once degenerated on this project", or "inline is safer / I want full control" are **NOT inputs to this
|
|
61
|
+
gate** and **MUST NOT** override it. (Any workflow-degeneration reports concern the `new2`/`new2-resolve`
|
|
62
|
+
whole-batch host — a *different* workflow; the canonical guidance is that this read-only F.2–F.4 fan-out
|
|
63
|
+
is **exactly** where a workflow fits.) Delegating IS the SSOT-sanctioned path — the workflow mirrors this
|
|
64
|
+
prose; the inline branch below exists **only** for installs where the `Workflow` tool is genuinely
|
|
65
|
+
absent, never as a judgment-call alternative. If you run inline while the delegation conditions were met,
|
|
66
|
+
that is a **gate violation**: log it as
|
|
67
|
+
`f.final: GATE VIOLATION — ran inline despite Workflow available + new-final-review.js linked`.
|
|
58
68
|
|
|
59
69
|
- **IF** the `Workflow` tool is available to you **AND** the script
|
|
60
70
|
`.claude/workflows/new-final-review.js` is present (linked by the framework on
|
|
61
|
-
Claude-enabled installs) → **delegate F.2–F.4** to it
|
|
71
|
+
Claude-enabled installs) → you **MUST delegate F.2–F.4** to it (not optional — see the
|
|
72
|
+
no-discretion rule above):
|
|
62
73
|
|
|
63
74
|
```
|
|
64
75
|
Workflow({ name: 'new-final-review', args: {
|
|
@@ -12,13 +12,26 @@ state outside the worktree and asks the user nothing on the happy path. That mak
|
|
|
12
12
|
biggest source of prefix growth on long epics). E2E (Phase 2.6, human-gated + nests a skill) and the
|
|
13
13
|
doc-review (Phase 3, write-mode, must see the FINAL code) stay in the skill.
|
|
14
14
|
|
|
15
|
-
**Branch (decide once, here — at the top of the review cluster, AFTER Phase 2.5b AC-Closure)
|
|
15
|
+
**Branch (decide once, here — at the top of the review cluster, AFTER Phase 2.5b AC-Closure).**
|
|
16
|
+
This is a **MECHANICAL, BINARY decision** — evaluate the three conditions below and act on the result.
|
|
17
|
+
You have **NO discretion**: if the delegation conditions hold you **MUST** delegate. The factors that
|
|
18
|
+
feel like reasons to "just run it inline" — a single-card batch, a small/well-scoped diff, "the
|
|
19
|
+
context-economy benefit is marginal here", "a workflow once degenerated on this project", "inline is
|
|
20
|
+
safer / I have full control" — are **NOT inputs to this gate** and **MUST NOT** override it. (Those
|
|
21
|
+
degeneration reports concern the `new2`/`new2-resolve` whole-batch host, a *different* workflow; the
|
|
22
|
+
canonical guidance is that this read-then-fix fan-out is exactly where a workflow fits.) Delegating IS
|
|
23
|
+
the SSOT-sanctioned path: the workflow mirrors this prose, and the inline fallback exists **only** for
|
|
24
|
+
installs where the `Workflow` tool is genuinely absent — not as a judgment-call alternative. If you ever
|
|
25
|
+
run inline while the delegation conditions were met, that is a **gate violation**: log it explicitly as
|
|
26
|
+
`review-cluster: GATE VIOLATION — ran inline despite Workflow available + new-card-review.js linked + non-trivial`
|
|
27
|
+
so it surfaces in telemetry.
|
|
16
28
|
|
|
17
29
|
- **IF `IS_TRIVIAL`** (§ "Trivial-card fast-lane", re-confirmed on the committed diff) → do NOT delegate.
|
|
18
30
|
Follow the trivial fast-lane in Phase 2.55 below (inline mechanical gates → Phase 3 doc → commit).
|
|
19
31
|
|
|
20
32
|
- **ELSE IF** the `Workflow` tool is available **AND** `.claude/workflows/new-card-review.js` is present
|
|
21
|
-
(linked by the framework on Claude-enabled installs) → **delegate the cluster** to it
|
|
33
|
+
(linked by the framework on Claude-enabled installs) → you **MUST delegate the cluster** to it (this
|
|
34
|
+
branch is not optional — see the no-discretion rule above). First build the
|
|
22
35
|
inputs (reusing the existing deterministic logic — do NOT re-implement it):
|
|
23
36
|
- `qaTier` ← the **Phase 3.5 profile selection** (step 19-21b below: `review_profile` floor →
|
|
24
37
|
`skip`/`light` ⇒ `qaTier:"light"` (qa-sentinel deferred to Final), `deep` **or any Phase 3.7 Step-A
|
|
@@ -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. **
|
|
131
|
-
|
|
132
|
-
-
|
|
133
|
-
- `
|
|
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-i — HALT 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."
|
|
@@ -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
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
the
|
|
194
|
-
|
|
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}}"
|
|
@@ -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[@]}"
|