baldart 5.6.0 → 5.8.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/VERSION +1 -1
  3. package/framework/.claude/agents/prd-card-writer.md +23 -0
  4. package/framework/.claude/skills/capture/CHANGELOG.md +4 -0
  5. package/framework/.claude/skills/capture/SKILL.md +2 -1
  6. package/framework/.claude/skills/design-sync/CHANGELOG.md +4 -0
  7. package/framework/.claude/skills/design-sync/SKILL.md +2 -1
  8. package/framework/.claude/skills/ds-edit/CHANGELOG.md +5 -0
  9. package/framework/.claude/skills/ds-edit/SKILL.md +9 -10
  10. package/framework/.claude/skills/ds-handoff/CHANGELOG.md +5 -0
  11. package/framework/.claude/skills/ds-handoff/SKILL.md +3 -2
  12. package/framework/.claude/skills/ds-new/CHANGELOG.md +4 -0
  13. package/framework/.claude/skills/ds-new/SKILL.md +2 -1
  14. package/framework/.claude/skills/ds-render/CHANGELOG.md +4 -0
  15. package/framework/.claude/skills/ds-render/SKILL.md +2 -1
  16. package/framework/.claude/skills/e2e-review/CHANGELOG.md +4 -0
  17. package/framework/.claude/skills/e2e-review/SKILL.md +2 -1
  18. package/framework/.claude/skills/gamification-design/CHANGELOG.md +4 -0
  19. package/framework/.claude/skills/gamification-design/SKILL.md +2 -1
  20. package/framework/.claude/skills/graph-align/CHANGELOG.md +4 -0
  21. package/framework/.claude/skills/graph-align/SKILL.md +2 -1
  22. package/framework/.claude/skills/i18n/CHANGELOG.md +4 -0
  23. package/framework/.claude/skills/i18n/SKILL.md +2 -1
  24. package/framework/.claude/skills/motion-design/CHANGELOG.md +4 -0
  25. package/framework/.claude/skills/motion-design/SKILL.md +2 -1
  26. package/framework/.claude/skills/new/CHANGELOG.md +19 -0
  27. package/framework/.claude/skills/new/SKILL.md +2 -1
  28. package/framework/.claude/skills/new/references/final-review.md +36 -1
  29. package/framework/.claude/skills/new2/CHANGELOG.md +15 -0
  30. package/framework/.claude/skills/new2/SKILL.md +2 -1
  31. package/framework/.claude/skills/prd/CHANGELOG.md +32 -0
  32. package/framework/.claude/skills/prd/SKILL.md +2 -1
  33. package/framework/.claude/skills/prd/assets/epic-template.yml +6 -0
  34. package/framework/.claude/skills/prd/references/backlog-phase.md +36 -0
  35. package/framework/.claude/skills/prd/references/validation-phase.md +30 -0
  36. package/framework/.claude/skills/prd-add/CHANGELOG.md +4 -0
  37. package/framework/.claude/skills/prd-add/SKILL.md +2 -1
  38. package/framework/.claude/skills/skill-creator/references/skill-structure.md +31 -2
  39. package/framework/.claude/skills/skill-creator/scripts/quick_validate.py +25 -3
  40. package/framework/.claude/skills/ui-design/CHANGELOG.md +4 -0
  41. package/framework/.claude/skills/ui-design/SKILL.md +2 -1
  42. package/framework/.claude/skills/ui-implement/CHANGELOG.md +4 -0
  43. package/framework/.claude/skills/ui-implement/SKILL.md +2 -1
  44. package/framework/.claude/workflows/new2.js +24 -3
  45. package/framework/agents/card-schema.md +50 -0
  46. package/package.json +1 -1
  47. package/src/commands/configure.js +9 -2
  48. package/src/commands/doctor.js +65 -0
  49. package/src/utils/skill-gate.js +139 -0
  50. package/src/utils/symlinks.js +72 -7
@@ -290,6 +290,42 @@ The `prd-card-writer` agent owns the entire card writing pipeline:
290
290
  - Presenting results to the user (the agent returns summary, main context displays it)
291
291
  - Proceeding to Step 6 (validation phase)
292
292
 
293
+ ## Launch-wave partition (orchestrator judgment — YOU compute it, not the card-writer)
294
+
295
+ After the card-writer returns the Parallel Execution Map, **you** (the `/prd` orchestrator)
296
+ decide how the epic should be **launched** across `/new` runs — a judgment the mechanical
297
+ card-writer cannot make because it needs the discovery context you hold (Tier, review-profile
298
+ mix, migration, UI/server split). This is the durable half of the "Piano di implementazione
299
+ suggerito" the final message renders (validation-phase.md § Final output).
300
+
301
+ **Decide single-run vs waves (scoped — default single-run).** Keep the epic as ONE run unless
302
+ at least one split trigger fires; if none fires, do NOT tag any wave (single-run is the explicit
303
+ else branch — never fabricate a wave grouping just to have one):
304
+ - **Large batch** — many children over several dependency levels (a long single run drifts the
305
+ trunk mid-batch → merge conflicts instead of fast-forward; keep this rationale generic, do not
306
+ name a stack).
307
+ - **Migration / schema card present** — land it in an EARLY wave so its migration/deploy gate runs
308
+ once and later waves build on the real schema.
309
+ - **Review pressure** — many `review_profile: deep` children; splitting keeps the orchestrator
310
+ context lucid on exactly the cards that need lucid review.
311
+ - **Clear server↔UI cut** — server/data foundation vs UI surfaces is a natural wave boundary
312
+ (UI/E2E cards exercise the server, so they belong in the later wave).
313
+ - **Cross-domain file conflicts** — `execution_strategy.file_conflicts` that straddle a domain
314
+ boundary force sequencing.
315
+
316
+ **Assign waves over the EXISTING groups (one partition, no re-listing of card IDs).** Waves are a
317
+ contiguous grouping of `execution_strategy.groups[]` in `level` order. For each group in a wave,
318
+ set `wave` (1-based, ascending), `wave_name` (a short, unique, human-memorable name derived from
319
+ the wave's dominant domain/role — e.g. "Fondamenta server", "Superfici UI & closer"), and
320
+ `wave_rationale` (one line). Persist them by **editing the epic card** (`backlog/<slug>-00.yml`)
321
+ in the worktree — a targeted Edit adding the three sub-keys to the matching `groups[]` entries.
322
+ Do NOT re-spawn the card-writer; do NOT create a parallel `waves[]` block. Schema:
323
+ `framework/agents/card-schema.md` § "`execution_strategy.groups[].wave*`".
324
+
325
+ **Do NOT persist the engine (`/new` vs `/new2`) or the hard rules** — those are render-time,
326
+ runtime-aware (validation-phase.md § Final output). On a single-run epic, skip this step entirely
327
+ (no wave keys written).
328
+
293
329
  ## Present and Proceed
294
330
 
295
331
  Present the plan to the user (traceability matrix + parallel execution map) for
@@ -410,4 +410,34 @@ A run that prints "local `git.trunk_branch` did not fast-forward, handle the fil
410
410
  yourself and then pull" has FAILED Step 7.5 — go back and resolve it before
411
411
  printing the summary.
412
412
 
413
+ Then, ALWAYS, append the launch plan:
414
+
415
+ ### 🚀 Piano di implementazione suggerito (MANDATORY — always render)
416
+
417
+ Derive it from the epic card's `execution_strategy` (the wave partition you persisted in
418
+ backlog-phase.md § "Launch-wave partition"); nothing here is recomputed from scratch. Rules +
419
+ schema: `framework/agents/card-schema.md` § "`execution_strategy.groups[].wave*`" (Launch waves).
420
+
421
+ - **Single-run** (≤1 distinct `wave_name`, e.g. a light-lane 1-child epic): render ONE line —
422
+ `**Lancia in un'unica run:** \`/new <all card IDs>\`` — no table. Stop here.
423
+ - **Waves** (≥2 distinct `wave_name`): render a table, one row per wave in `wave` order:
424
+
425
+ | Wave | Nome | Card | Contenuto |
426
+ |------|------|------|-----------|
427
+ | 1 | Fondamenta server | FEAT-XXXX-01…06 | <wave_rationale> |
428
+ | 2 | Superfici UI & closer | FEAT-XXXX-07…13 | <wave_rationale> |
429
+
430
+ Then, below the table:
431
+ - **Engine (runtime-aware — computed here, never read from the card).** On **Claude** (the
432
+ `Workflow` tool is available) recommend `/new2` for a high-volume wave (off-context batch, no
433
+ interactive gates) and `/new` (classic) for a wave that wants human gates (migration / risky
434
+ review) — a hybrid (`/new` on the risky early wave, `/new2` on the rest) is a valid call. On
435
+ **Codex** recommend `/new` only — `/new2` needs the `Workflow` tool and does not exist there
436
+ (`framework/agents/runtime-portability-protocol.md` § "Workflow acceleration"). Give the exact
437
+ command per wave, e.g. `/new2 FEAT-XXXX-01-FEAT-XXXX-06`.
438
+ - **Hard rule:** niente altro lavoro in parallelo sugli stessi file tra una wave e l'altra.
439
+ - **Post-wave gates:** if `.baldart/overlays/prd.md` declares project-specific between-wave
440
+ cautions (e.g. a migration/deploy gate, a compile check, a stash check), surface them after the
441
+ relevant wave. Omit this line when the overlay declares none — never invent stack-specific steps.
442
+
413
443
  Display completed Progress Bar.
@@ -2,6 +2,10 @@
2
2
 
3
3
  Formato: [Keep a Changelog](https://keepachangelog.com/) · [SemVer](https://semver.org/).
4
4
 
5
+ ## 1.1.1 — 2026-07-06
6
+
7
+ - **Feature-gate**: added `requires_feature: has_prd_workflow` frontmatter — this skill is now linked into a consumer only when `features.has_prd_workflow` is enabled (it already refused at runtime otherwise). Trims Codex's ~2% skill-description budget on projects without the feature. Install-time gate, portable across Claude + Codex.
8
+
5
9
  ## 1.1.0 — 2026-07-02
6
10
 
7
11
  - **Analysis-profile contract (framework v4.94.0)**: PATCH-mode re-discovery passes `PROFILE=impact` to `codebase-architect` (blast-radius scoping per `framework/agents/analysis-profiles.md`).
@@ -1,7 +1,8 @@
1
1
  ---
2
2
  name: prd-add
3
3
  effort: medium
4
- version: 1.1.0
4
+ version: 1.1.1
5
+ requires_feature: has_prd_workflow
5
6
  description: "Change Request skill for active PRD sessions. Use when the user says /prd-add, 'aggiungi requisito', 'serve anche', 'manca un endpoint', or describes a new requirement that impacts an existing PRD. Also auto-triggered by /prd during discovery when the user's answer reveals a new sub-feature not covered by the original scope. Runs ICIAS impact analysis (semantic scan + structural propagation + scoring) to determine which PRD phases need SKIP/PATCH/REDO, then executes only affected phases."
6
7
  ---
7
8
 
@@ -27,13 +27,42 @@ when the skill needs them** — no empty placeholder dirs (that was upstream
27
27
  name: <kebab-case> # matches the directory name
28
28
  effort: medium # low | medium | high | xhigh | max (effort-protocol.md)
29
29
  version: 1.0.0 # the skill's OWN SemVer
30
+ requires_feature: has_x # OPTIONAL — install-time feature-gate (see below)
30
31
  description: > # triggers + what/when; NO angle brackets
31
32
  ...
32
33
  ---
33
34
  ```
34
35
 
35
- Order: `name` → `effort` → `version` → `description`. `license` / `metadata` are
36
- tolerated only on imported third-party skills; do not add other keys.
36
+ Order: `name` → `effort` → `version` → (`requires_feature`) `description`.
37
+ `license` / `metadata` are tolerated only on imported third-party skills; do not
38
+ add other keys.
39
+
40
+ ### `requires_feature` — install-time feature-gate (OPTIONAL)
41
+
42
+ A skill that is only meaningful when a `baldart.config.yml` `features.*` flag is
43
+ on (and already REFUSES at runtime otherwise) declares that dependency here, so
44
+ the installer links it into a consumer ONLY when the feature is enabled. This
45
+ trims Codex's ~2% skill-description budget on projects that don't use the feature.
46
+
47
+ ```yaml
48
+ requires_feature: has_design_system # string → link iff true
49
+ requires_feature: [has_design_system, has_e2e_review] # list = AND (ALL true)
50
+ ```
51
+
52
+ - **Omit it** for any skill that should always be linked (the default). The five
53
+ bootstrap skills that ENABLE a feature (`design-system-init`, `i18n-adopt`,
54
+ `graphify-bootstrap`, `lsp-bootstrap`, `toolchain-bootstrap`) MUST stay ungated
55
+ — otherwise the feature could never be turned on.
56
+ - The gate is **install-time only** and **portable**: both Claude (`.claude/skills/`)
57
+ and Codex (`.agents/skills/`) simply see fewer directories. Neither runtime reads
58
+ the field — it is ignored exactly like `version`/`effort`. Resolution lives in
59
+ `src/utils/skill-gate.js`; it reads `features.*` directly and **fails open** (a
60
+ missing config, or a malformed frontmatter, links the skill rather than hiding it).
61
+ - **NOT a `baldart.config.yml` key** — it consumes existing `features.*`, so the
62
+ schema-change propagation rule does NOT apply. It is skill-frontmatter metadata.
63
+ - **Invariant** (latent, for future Codex `agents/openai.yaml` metadata): a gated
64
+ skill MUST NOT be declared as a `dependencies:` target of an always-linked skill's
65
+ `openai.yaml` — pruning the gated skill would leave a dangling dependency.
37
66
 
38
67
  ## Body headers (in order, when applicable)
39
68
 
@@ -39,10 +39,22 @@ def validate_skill(skill_path):
39
39
  return False, f"Invalid YAML in frontmatter: {e}"
40
40
 
41
41
  # Define allowed properties
42
- # BALDART additions: `effort` (per-skill reasoning baseline — effort-protocol.md)
43
- # and `version` (per-skill SemVer — canonical structure, see references/skill-structure.md).
42
+ # BALDART additions: `effort` (per-skill reasoning baseline — effort-protocol.md),
43
+ # `version` (per-skill SemVer — canonical structure, see references/skill-structure.md)
44
+ # and `requires_feature` (install-time feature-gate — a skill is linked into a
45
+ # consumer only when this `features.*` flag is on; see references/skill-structure.md
46
+ # and src/utils/skill-gate.js). All three are OPTIONAL frontmatter metadata,
47
+ # ignored by the Claude/Codex skill loaders (they only read name+description).
44
48
  ALLOWED_PROPERTIES = {'name', 'description', 'license', 'allowed-tools', 'metadata',
45
- 'effort', 'version'}
49
+ 'effort', 'version', 'requires_feature'}
50
+
51
+ # Known feature flags a `requires_feature` value may reference (mirrors the
52
+ # features.* block of baldart.config.template.yml). Used for a SOFT warning
53
+ # only — an unknown value is not fatal (fail-open: the gate links the skill).
54
+ KNOWN_FEATURES = {'has_design_system', 'multi_tenant_theming', 'has_api_docs',
55
+ 'has_backlog', 'has_adrs', 'has_prd_workflow', 'has_wiki_overlay',
56
+ 'has_lsp_layer', 'has_code_graph', 'has_e2e_review',
57
+ 'has_toolchain', 'has_i18n'}
46
58
 
47
59
  # Check for unexpected properties (excluding nested keys under metadata)
48
60
  unexpected_keys = set(frontmatter.keys()) - ALLOWED_PROPERTIES
@@ -90,6 +102,16 @@ def validate_skill(skill_path):
90
102
  # `version` + a per-skill CHANGELOG.md are REQUIRED for new skills but WARNED (not
91
103
  # hard-failed) here for backwards-compat with skills authored before the convention.
92
104
  warnings = []
105
+ # Soft-validate requires_feature values against the known features.* set.
106
+ if 'requires_feature' in frontmatter:
107
+ req = frontmatter['requires_feature']
108
+ req_list = req if isinstance(req, list) else [req]
109
+ unknown = [str(f) for f in req_list if str(f) not in KNOWN_FEATURES]
110
+ if unknown:
111
+ warnings.append(
112
+ f"`requires_feature` references unknown flag(s): {', '.join(sorted(unknown))}. "
113
+ f"The gate fails open (skill still linked). Known: {', '.join(sorted(KNOWN_FEATURES))}"
114
+ )
93
115
  if 'version' not in frontmatter:
94
116
  warnings.append("missing `version:` in frontmatter (add `version: 1.0.0`)")
95
117
  if 'effort' not in frontmatter:
@@ -2,6 +2,10 @@
2
2
 
3
3
  Formato: [Keep a Changelog](https://keepachangelog.com/) · [SemVer](https://semver.org/).
4
4
 
5
+ ## 2.0.1 — 2026-07-06
6
+
7
+ - **Feature-gate**: added `requires_feature: has_design_system` frontmatter — this skill is now linked into a consumer only when `features.has_design_system` is enabled (it already refused at runtime otherwise). Trims Codex's ~2% skill-description budget on projects without the feature. Install-time gate, portable across Claude + Codex.
8
+
5
9
  ## 2.0.0 — 2026-07-03
6
10
 
7
11
  **The local design studio.** Root-level refactor (framework v5.2.0): the
@@ -1,7 +1,8 @@
1
1
  ---
2
2
  name: ui-design
3
3
  effort: medium
4
- version: 2.0.0
4
+ version: 2.0.1
5
+ requires_feature: has_design_system
5
6
  description: "The local design studio — THE framework skill for designing, prototyping and restyling UI directly in the repo (the internal twin of the Claude Design handoff). Produces context-aware, token-compliant HTML mockups in 3 distinct directions, with a locked Design Read, a deterministic anti-slop craft gate and separated generator/evaluator verification. Use when: (1) designing new pages, screens or components, (2) the /prd skill reaches its UI design phase (Step 3), (3) redesigning/restyling existing pages ('rifai la UI', 'migliora il design', 'restyle'), (4) exploring design options ('3 opzioni', 'opzioni di design', 'direzioni estetiche'), (5) prototyping a page/landing/dashboard locally ('prototipo', 'mockup', 'progetta interfaccia', 'design UI'). SUPERSEDES frontend-design (now a router) and ui-ux-pro-max for ALL design/prototyping work. Not for: implementing an already-approved mockup (→ /ui-implement), in-code UI fixes with no design decision (→ ui-expert agent), motion specs (→ /motion-design)."
6
7
  ---
7
8
 
@@ -2,6 +2,10 @@
2
2
 
3
3
  Formato: [Keep a Changelog](https://keepachangelog.com/) · [SemVer](https://semver.org/).
4
4
 
5
+ ## 1.1.2 — 2026-07-06
6
+
7
+ - **Feature-gate**: added `requires_feature: has_design_system` frontmatter — this skill is now linked into a consumer only when `features.has_design_system` is enabled (it already refused at runtime otherwise). Trims Codex's ~2% skill-description budget on projects without the feature. Install-time gate, portable across Claude + Codex.
8
+
5
9
  ## 1.1.1 — 2026-07-02
6
10
 
7
11
  - Return Contract (SKILL.md): documentati i campi `bindings` + `lane_coverage`/`lanes`
@@ -1,7 +1,8 @@
1
1
  ---
2
2
  name: ui-implement
3
3
  effort: medium
4
- version: 1.1.1
4
+ version: 1.1.2
5
+ requires_feature: has_design_system
5
6
  description: >
6
7
  Implement an ALREADY-APPROVED mockup into the codebase end-to-end and
7
8
  self-verify its fidelity. Takes a mockup in any channel — a Claude Design
@@ -219,6 +219,11 @@ const PREFLIGHT_SCHEMA = {
219
219
  // Drives the epic-closure blocklist (an epic with open scope residuals/follow-ups must
220
220
  // not close) and the Epic goal section of the final report.
221
221
  parentEpic: { type: 'string', description: "parent epic id from group.parent or parent: ('' when none)" },
222
+ // v5.8.0 — the launch-wave this card belongs to, IFF /prd persisted a wave partition on
223
+ // the epic card (optional wave_name on execution_strategy.groups[] whose .cards includes
224
+ // this id — schema: agents/card-schema.md § execution_strategy.groups[].wave*). '' when the
225
+ // epic has no wave tags (single-run / legacy). Drives the "Prossime wave" report line.
226
+ waveName: { type: 'string', description: "launch wave_name from the epic's execution_strategy.groups[] ('' when none)" },
222
227
  },
223
228
  },
224
229
  },
@@ -305,7 +310,7 @@ try {
305
310
  g3Bullet +
306
311
  `• G4 card-field validation (setup.md 1b/1c, card-schema.md consumer contract): card missing acceptance_criteria/files_likely_touched/scope → EXCLUDE (excluded[] + reason). \`requirements\` is CONDITIONAL, not an auto-exclude: if it is missing BUT acceptance_criteria + scope are both present and non-empty, FAITHFULLY DERIVE it (a concrete restatement/decomposition of the existing AC + scope — faithful, never inventing new scope) and WRITE it back to the card YAML on disk in ${MAIN}/${paths.backlog_dir || 'backlog'} (F-040: main repo, not the worktree copy; the card artifact is NOT a source/doc file, so this is allowed despite the role boundary), logging \`[BACKFILL] <id>: requirements=<N items, AC+scope-derived>\`. Only EXCLUDE for missing requirements when acceptance_criteria OR scope is ALSO empty (a genuinely thin card). Never HALT for one bad card.\n` +
307
312
  `• G5 depends-on: a card whose depends_on names a non-DONE card NOT in this batch → EXCLUDE it AND every in-batch card that transitively depends on it.\n` +
308
- `• cardGraph (REQUIRED, F-021): for every runnable card return { id, dependsOn:[IN-BATCH deps only], ownerAgent (the card's owner_agent; G25 unknown→'coder'), hasMockup (BOOLEAN — true IFF the card BUILDS the mocked UI: links.design_src non-empty, OR links.design non-empty AND (component_bindings non-empty OR files_likely_touched intersects component/style/UI source paths). A bare links.design on a docs/test/meta-only card is a POINTER to the epic's explainer, not a build target — v5.6.0, FEAT-0068: the bare test clamped a docs+ADR card to ui-expert; drives the mockup-first build override — implement.md §6a), parentEpic (the card's parent epic id from group.parent or a top-level parent: field; '' when none — drives the epic-closure blocklist + the Epic goal report section), designSrcDir (ABS path of links.design_src / the PRD mockups/_src dir; '' when absent), designHtml (ABS path when links.design is an .html file; '' otherwise), hasBindings (BOOLEAN — non-empty component_bindings), reviewProfile (the card's review_profile; default 'balanced'), policyDeferredACs, alreadyCommitted, alreadyCommittedSha, isEpic (implement.md §6b epic guard: id ends '-00' OR filename ends '-epic.yml' OR group.is_epic:true OR review_profile 'skip' with no requirements), filesLikelyTouched (verbatim from the YAML) }.\n` +
313
+ `• cardGraph (REQUIRED, F-021): for every runnable card return { id, dependsOn:[IN-BATCH deps only], ownerAgent (the card's owner_agent; G25 unknown→'coder'), hasMockup (BOOLEAN — true IFF the card BUILDS the mocked UI: links.design_src non-empty, OR links.design non-empty AND (component_bindings non-empty OR files_likely_touched intersects component/style/UI source paths). A bare links.design on a docs/test/meta-only card is a POINTER to the epic's explainer, not a build target — v5.6.0, FEAT-0068: the bare test clamped a docs+ADR card to ui-expert; drives the mockup-first build override — implement.md §6a), parentEpic (the card's parent epic id from group.parent or a top-level parent: field; '' when none — drives the epic-closure blocklist + the Epic goal report section), waveName (v5.8.0 — when the parent epic card carries a launch-wave partition, the wave_name of the execution_strategy.groups[] entry whose .cards includes this id; '' when the epic has no wave tags — single-run/legacy; drives the "Prossime wave" report line), designSrcDir (ABS path of links.design_src / the PRD mockups/_src dir; '' when absent), designHtml (ABS path when links.design is an .html file; '' otherwise), hasBindings (BOOLEAN — non-empty component_bindings), reviewProfile (the card's review_profile; default 'balanced'), policyDeferredACs, alreadyCommitted, alreadyCommittedSha, isEpic (implement.md §6b epic guard: id ends '-00' OR filename ends '-epic.yml' OR group.is_epic:true OR review_profile 'skip' with no requirements), filesLikelyTouched (verbatim from the YAML) }.\n` +
309
314
  `• B1/F-026 idempotency (per card, AFTER the worktree exists): set alreadyCommitted:true (+ alreadyCommittedSha) IFF ALL hold: (a) a commit referencing the card id exists in ${TRUNK}..HEAD of the worktree; (b) the card's validation_commands re-run GREEN right now; (c) NO open follow-up card for it exists in ${paths.backlog_dir || 'backlog'}. On a FRESH worktree ${TRUNK}..HEAD is empty → all false, zero extra work.\n` +
310
315
  `• F-016 AC↔ownership consistency: for each acceptance_criterion, derive the file(s) it requires editing. If those files are NOT a subset of the card's MAY-EDIT/files_likely_touched → add the AC to policyDeferredACs:[{n,text,owningCard|owningFile,reason}] (it will become ONE follow-up, never a resolve). Do the same for any AC whose remedy is an owner-gated infra action (remote db push / deploy / secret / DNS).\n` +
311
316
  (migrationApplied
@@ -634,7 +639,11 @@ async function runCard(cardId, cardPath) {
634
639
  : `Phase 1 fallback: the architect specialist was unavailable — do the Phase 1 claim+architect exploration yourself per ${REF}/implement.md and persist the baseline to ${baselinePath} before coding.`
635
640
  // T1.A (v4.90.0) — mockup cards: binding-compliance is verified deterministically by the
636
641
  // check-bindings.mjs script (ui-implement Step 5b is the SSOT; this is the new2 parallel).
637
- const bindingBit = node.hasMockup && node.hasBindings
642
+ // Guard on has_design_system: component_bindings only exist when a /prd design
643
+ // phase (which requires the design system) produced them, so absent-DS + bindings
644
+ // is a malformed hand-authored card — don't emit a gate whose script path could
645
+ // resolve empty. Mirrors the has_design_system ds-gate guard above.
646
+ const bindingBit = features.has_design_system && node.hasMockup && node.hasBindings
638
647
  ? `\nBINDING GATE (deterministic — ui-implement Step 5b parallel): after building, run \`node "$(ls .claude/skills/ui-implement/scripts/check-bindings.mjs .framework/framework/.claude/skills/ui-implement/scripts/check-bindings.mjs 2>/dev/null | head -1)" --bindings-json '<the card component_bindings as JSON>' --files <your touched implementation files>\`. Exit 3 = a reuse/reuse-variant binding is NOT used in the code: the map is AUTHORITATIVE — fix it before returning (one pass). Include the script's one-line JSON output as bindingCheck in your return.\n`
639
648
  : ''
640
649
  let impl
@@ -1683,7 +1692,19 @@ function buildReport(o) {
1683
1692
  const epicHigh = residuals.filter((r) => ((graphById[r.card] && graphById[r.card].parentEpic) === e || r.card === e) && (sevRank[residualSeverity(r)] || 0) >= 2)
1684
1693
  if (closedEpics.includes(e) && !closureBlockedEpics.has(e)) L.push(`- **${e}: CHIUSA** (children DONE + AC epic verificati dal merge agent)`)
1685
1694
  else if (closedEpics.includes(e)) L.push(`- **${e}: ⚠️ CHIUSA CONTRO BLOCKLIST (false DONE)** — riaprire: ${epicHigh.length} residuo/i HIGH aperti`)
1686
- else L.push(`- **${e}: APERTA** — ${reason || (closureBlockedEpics.has(e) ? 'blocklist: residui/follow-up aperti' : 'merge non eseguito o children aperti')}${epicHigh.length ? ` · ${epicHigh.length} residuo/i HIGH` : ''}`)
1695
+ else {
1696
+ L.push(`- **${e}: APERTA** — ${reason || (closureBlockedEpics.has(e) ? 'blocklist: residui/follow-up aperti' : 'merge non eseguito o children aperti')}${epicHigh.length ? ` · ${epicHigh.length} residuo/i HIGH` : ''}`)
1697
+ // v5.8.0 — remaining launch waves by NAME (parity with /new final-review F.6). Uses the
1698
+ // waveName threaded through cardGraph (buildReport has no fs access to re-read the epic).
1699
+ // Honest scope: only waves whose cards are IN THIS BATCH are visible — waves entirely
1700
+ // outside the batch aren't loaded, so this reflects the batch, not the whole epic.
1701
+ const openWaves = dedupe(
1702
+ cardGraph
1703
+ .filter((n) => (n.parentEpic || '') === e && n.waveName && (perCardResults.find((r) => r.card === n.id) || {}).status !== 'DONE')
1704
+ .map((n) => n.waveName)
1705
+ )
1706
+ if (openWaves.length) L.push(` - Prossime wave (batch): ${openWaves.map((w) => `**${w}**`).join(' · ')}`)
1707
+ }
1687
1708
  }
1688
1709
  }
1689
1710
  if (finalSummary) {
@@ -230,6 +230,56 @@ Rules:
230
230
  - Omit any sub-field whose session id is unavailable (env var unset) rather than writing a
231
231
  placeholder.
232
232
 
233
+ ### `execution_strategy.groups[].wave*` — launch-wave annotation (epic-only, optional, orchestrator-written)
234
+
235
+ The epic's `execution_strategy.groups[]` is the **single card-ID partition** consumed by `/new`
236
+ for parallelism (`groups[].level` + `.cards` — the machine schedule). A **launch wave** is a
237
+ coarser, human-facing partition layered ON TOP of those same groups so the user can sequence the
238
+ epic across multiple `/new` runs and refer to each run by a stable NAME. It is expressed as three
239
+ **optional sub-fields on the existing `groups[]` entries** — never a parallel block that re-lists
240
+ card IDs (that would be a second SSOT that drifts from `groups[].cards`):
241
+
242
+ ```yaml
243
+ execution_strategy:
244
+ recommended_mode: team | sequential # existing — scheduling shape, runtime-neutral
245
+ groups:
246
+ - level: 0
247
+ cards: [FEAT-0069-01, FEAT-0069-02] # existing — the ONLY card-ID partition
248
+ description: "Foundation/scaffold" # existing
249
+ wave: 1 # opt — 1-based wave index (contiguous over groups)
250
+ wave_name: "Fondamenta server" # opt — short, unique human NAME for the wave
251
+ wave_rationale: "migration + config + resolver + enforcement server" # opt — why this wave
252
+ ```
253
+
254
+ Rules:
255
+ - **Optional (`C`)**, epic-only. They are **sub-keys of `execution_strategy`** (already `R`), NOT
256
+ top-level fields — the field-state matrix carries **no new row** and the baseline validator (which
257
+ checks only top-level presence/non-emptiness) never sees them. Legacy epics lack them → no HALT,
258
+ no migration.
259
+ - **Ownership split (same as [`provenance`](#provenance-session-id-traceability--optional-skill-written)):**
260
+ the `/prd` **orchestrator** COMPUTES the wave partition + names + rationale (a launch judgment that
261
+ needs discovery context — migration-early, review-`deep` pressure, server↔UI cut — which the
262
+ mechanical `prd-card-writer` does not hold), and it is PERSISTED onto the epic card. The card-writer
263
+ emits these keys ONLY when handed them; otherwise it **omits** them.
264
+ - **Contiguity invariant:** waves partition the groups in `level` order — a wave is a contiguous run
265
+ of groups. `wave` numbers ascend 1,2,3…; every group in the same wave shares one `wave_name`.
266
+ - **Single-run:** ≤1 distinct `wave_name` across all groups (e.g. a light-lane 1-child epic) means
267
+ "launch in one run" — the orchestrator leaves the wave keys ABSENT rather than tagging everything
268
+ `wave: 1`.
269
+
270
+ **Launch waves (render-time — NOT persisted):** the human-facing launch plan is derived, never
271
+ stored beyond the three keys above:
272
+ - The **wave view** = group the epic's `groups[]` by `wave_name`, ordered by `wave`; each wave's
273
+ cards = the union of its groups' `.cards`. Consumers: `/prd` final message ("Piano di
274
+ implementazione suggerito") and `/new`/`/new2` final message ("prossime wave rimanenti", joining
275
+ wave membership with each card's on-disk `status`).
276
+ - The **engine recommendation** (`/new` vs `/new2`) is computed at render time and is **runtime-aware**
277
+ — `/new2` needs the Workflow tool and is **Claude-only** (see `framework/agents/runtime-portability-protocol.md`
278
+ § "Workflow acceleration" — *no Codex equivalent*). It is **never persisted** on the card: a durable,
279
+ cross-tool artifact must not carry a runtime-capability recommendation a Codex consumer cannot execute.
280
+ - The anti-parallel hard rule ("no other work on the same files between waves") and any project-specific
281
+ post-wave gates (surfaced from `.baldart/overlays/prd.md`) are render-time constants, likewise not persisted.
282
+
233
283
  ## Acceptance-criteria grammar (EARS) + per-AC oracles (since the EARS+verify wave)
234
284
 
235
285
  **Why**: a prose AC passes gates without being verifiable ("handle it correctly"), and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "5.6.0",
3
+ "version": "5.8.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"
@@ -1508,10 +1508,17 @@ async function configure(opts = {}) {
1508
1508
  const enabledTools = (merged.tools && Array.isArray(merged.tools.enabled) && merged.tools.enabled.length)
1509
1509
  ? merged.tools.enabled : ['claude'];
1510
1510
  if (fs.existsSync(path.join(cwd, '.framework'))) {
1511
- new SymlinkUtils(cwd).mergeAgents({ tools: enabledTools });
1511
+ const symlinks = new SymlinkUtils(cwd);
1512
+ symlinks.mergeAgents({ tools: enabledTools });
1513
+ // Also re-merge SKILLS: the feature-gate (src/utils/skill-gate.js) depends
1514
+ // on the `features.*` just written. On first install `mergeSkills` ran
1515
+ // fail-open BEFORE this config existed (linking everything), so this pass
1516
+ // is what actually prunes the gated-off skills. Without it, gating never
1517
+ // takes effect until the next `baldart update`.
1518
+ symlinks.mergeSkills({ tools: enabledTools });
1512
1519
  }
1513
1520
  } catch (err) {
1514
- UI.warning(`Agent re-merge after configure failed (${err.message}) — run \`npx baldart doctor\` to heal.`);
1521
+ UI.warning(`Agent/skill re-merge after configure failed (${err.message}) — run \`npx baldart doctor\` to heal.`);
1515
1522
  }
1516
1523
 
1517
1524
  // v5.1.0 — research library: create + seed ${paths.research_dir} right after
@@ -409,6 +409,47 @@ async function detectState(cwd, opts = {}) {
409
409
  }
410
410
  } catch (_) { /* never block doctor on probe */ }
411
411
 
412
+ // ---- Feature-gated skill drift (skill-gate) ------------------------
413
+ // A skill whose `requires_feature` frontmatter is no longer satisfied
414
+ // (feature flipped off in baldart.config.yml) may still be linked from a
415
+ // prior install. `configure` and `update` both re-merge (which prunes), but
416
+ // a user who edits the config and runs ONLY `doctor` should be told. The
417
+ // reverse (a feature just turned ON, gated skill not yet linked) is covered
418
+ // by the missing-skills path. Advisory only (fix = `reconcile-skills`, a
419
+ // plain re-merge); never blocks. Fully fail-safe.
420
+ state.gatedSkillsLinked = [];
421
+ try {
422
+ if (state.frameworkPresent) {
423
+ const { resolveSkillGate } = require('../utils/skill-gate');
424
+ const { getAdapter } = require('../utils/tool-adapters');
425
+ const frameworkSkillsDir = path.join(cwd, '.framework', 'framework', '.claude', 'skills');
426
+ const gate = resolveSkillGate(frameworkSkillsDir, cwd);
427
+ if (!gate.failOpen && gate.gatedOff.length) {
428
+ const toolsEnabled = (config && !config.__malformed && config.tools && Array.isArray(config.tools.enabled))
429
+ ? config.tools.enabled : ['claude'];
430
+ const seen = new Set();
431
+ for (const toolName of toolsEnabled) {
432
+ let adapter;
433
+ try { adapter = getAdapter(toolName, cwd); } catch (_) { continue; }
434
+ const skillsRel = adapter.skillsDir();
435
+ for (const name of gate.gatedOff) {
436
+ if (seen.has(name)) continue;
437
+ const linkPath = path.join(cwd, skillsRel, name);
438
+ try {
439
+ const lst = fs.lstatSync(linkPath);
440
+ if (!lst.isSymbolicLink()) continue;
441
+ const resolved = path.resolve(path.dirname(linkPath), fs.readlinkSync(linkPath));
442
+ if (resolved === path.join(frameworkSkillsDir, name)) {
443
+ seen.add(name);
444
+ state.gatedSkillsLinked.push(name);
445
+ }
446
+ } catch (_) { /* absent */ }
447
+ }
448
+ }
449
+ }
450
+ }
451
+ } catch (_) { /* never block doctor on probe */ }
452
+
412
453
  // ---- Workflow symlink integrity (since v4.14.0) --------------------
413
454
  // Dynamic workflows (.js) are Claude-only and per-item symlinked into
414
455
  // `.claude/workflows/`. Unlike agents there is NO hard discovery gate —
@@ -1086,6 +1127,30 @@ function planActions(state) {
1086
1127
  });
1087
1128
  }
1088
1129
 
1130
+ // Feature-gated skill drift (skill-gate). Advisory — these skills' gating
1131
+ // feature is now off, but they're still linked from a prior install; they'd
1132
+ // only refuse at runtime and waste the Codex skill-description budget. A plain
1133
+ // re-merge prunes them (same rationale as the agent/workflow repairs).
1134
+ if (state.gatedSkillsLinked && state.gatedSkillsLinked.length > 0) {
1135
+ actions.push({
1136
+ key: 'reconcile-skills',
1137
+ label: `Unlink ${state.gatedSkillsLinked.length} feature-gated skill(s)`,
1138
+ why: `These skills are still linked but their gating feature is now off in baldart.config.yml: ${state.gatedSkillsLinked.join(', ')}. They refuse at runtime and consume Codex's ~2% skill-description budget. Re-merging prunes them (re-enable the feature to bring them back).`,
1139
+ autoOk: true,
1140
+ run: async () => {
1141
+ const SymlinkUtils = require('../utils/symlinks');
1142
+ const cfg = loadConfig(process.cwd());
1143
+ const enabledTools = (cfg && !cfg.__malformed
1144
+ && cfg.tools && Array.isArray(cfg.tools.enabled))
1145
+ ? cfg.tools.enabled
1146
+ : ['claude'];
1147
+ const symlinks = new SymlinkUtils();
1148
+ symlinks.mergeSkills({ tools: enabledTools });
1149
+ UI.success(`Reconciled feature-gated skills for tools: ${enabledTools.join(', ')}`);
1150
+ },
1151
+ });
1152
+ }
1153
+
1089
1154
  // Root primitives integrity (since v4.83.0). Regenerates AGENTS.md / CLAUDE.md
1090
1155
  // from the versioned skeleton + config + overlay when missing, still a legacy
1091
1156
  // symlink, or drifted. Offline + idempotent. A handwritten file is NOT touched
@@ -0,0 +1,139 @@
1
+ /**
2
+ * skill-gate.js — install-time feature-gating for framework skills.
3
+ *
4
+ * BALDART ships ~40 skills; Codex reserves only ~2% of its context window for
5
+ * skill descriptions, so linking every skill (including ones whose gating
6
+ * `features.*` flag is OFF, which then refuse at runtime anyway) wastes that
7
+ * budget. This module decides, per install/update, WHICH framework skills to
8
+ * link into a consumer, based on an OPTIONAL `requires_feature` frontmatter
9
+ * field in each skill's `SKILL.md`.
10
+ *
11
+ * The gate is PURELY install-time (resolved here by the Node installer) — both
12
+ * Claude (`.claude/skills/`) and Codex (`.agents/skills/`) simply see fewer
13
+ * directories. Neither runtime interprets `requires_feature`; it is a BALDART
14
+ * authoring construct, ignored by the skill loaders exactly like `version:` /
15
+ * `effort:`. So the filter is natively portable across both tools and applied
16
+ * from a single code path (the gate is shared SEMANTICS, not runtime mechanics).
17
+ *
18
+ * Frontmatter contract (all OPTIONAL — a skill with no `requires_feature` is
19
+ * ALWAYS linked, backwards-compatible default):
20
+ * requires_feature: has_design_system # string → link iff true
21
+ * requires_feature: [has_design_system, has_e2e_review] # list = AND (all true)
22
+ *
23
+ * Fail-open everywhere (never hide a skill by accident — the safety net must
24
+ * not become a tribunal):
25
+ * - config absent / unreadable / malformed → link EVERYTHING.
26
+ * - a single skill's frontmatter malformed → link THAT skill (per-skill),
27
+ * never crash the whole merge.
28
+ *
29
+ * NOTE: we read `features.*` DIRECTLY from baldart.config.yml rather than reuse
30
+ * `agent-slots.resolveAgentFlags()` — that flag set intentionally covers only
31
+ * the keys the agent SLOT sources reference and omits `has_prd_workflow` /
32
+ * `has_e2e_review`, which ARE skill gates. Reading the raw block keeps the gate
33
+ * correct for every existing (and future) `features.*` key with no coupling.
34
+ *
35
+ * This introduces NO new `baldart.config.yml` key (it consumes existing
36
+ * `features.*`) — the schema-change propagation rule does NOT apply. It is
37
+ * skill-frontmatter metadata, like `version`/`effort`.
38
+ */
39
+
40
+ const fs = require('fs');
41
+ const path = require('path');
42
+
43
+ /**
44
+ * Load the `features` block from baldart.config.yml.
45
+ * @returns {{ features: Object|null }} `features: null` signals "config
46
+ * unavailable" → callers must fail-open (link everything).
47
+ */
48
+ function loadFeatures(cwd) {
49
+ try {
50
+ const yaml = require('js-yaml');
51
+ const cfgPath = path.join(cwd, 'baldart.config.yml');
52
+ if (!fs.existsSync(cfgPath)) return { features: null };
53
+ const cfg = yaml.load(fs.readFileSync(cfgPath, 'utf8'));
54
+ if (!cfg || typeof cfg !== 'object') return { features: null };
55
+ const features = (cfg.features && typeof cfg.features === 'object') ? cfg.features : {};
56
+ return { features };
57
+ } catch (_) {
58
+ return { features: null };
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Extract the OPTIONAL `requires_feature` from a skill's SKILL.md frontmatter.
64
+ * @returns {string[]|null} normalized list of required feature keys, or null
65
+ * when the skill is ungated / has no SKILL.md / frontmatter is unparseable
66
+ * (fail-open: null ⇒ always link).
67
+ */
68
+ function readRequiredFeatures(skillDir) {
69
+ try {
70
+ const skillMd = path.join(skillDir, 'SKILL.md');
71
+ if (!fs.existsSync(skillMd)) return null;
72
+ const raw = fs.readFileSync(skillMd, 'utf8');
73
+ if (!raw.startsWith('---')) return null;
74
+ // Frontmatter is between the first two `---` fences.
75
+ const end = raw.indexOf('\n---', 3);
76
+ if (end === -1) return null;
77
+ const fmText = raw.slice(3, end);
78
+ const yaml = require('js-yaml');
79
+ const fm = yaml.load(fmText);
80
+ if (!fm || typeof fm !== 'object') return null;
81
+ const req = fm.requires_feature;
82
+ if (req == null) return null;
83
+ const list = (Array.isArray(req) ? req : [req])
84
+ .map(x => String(x).trim())
85
+ .filter(Boolean);
86
+ return list.length ? list : null;
87
+ } catch (_) {
88
+ // Malformed frontmatter for THIS skill → fail-open (link it).
89
+ return null;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Decide which framework skills to link vs skip for this consumer.
95
+ *
96
+ * @param {string} frameworkSkillsDir absolute path to
97
+ * `.framework/framework/.claude/skills/`
98
+ * @param {string} cwd consumer repo root (where baldart.config.yml lives)
99
+ * @returns {{ toLink: string[], gatedOff: string[], failOpen: boolean }}
100
+ * `toLink` = skill dir names to link; `gatedOff` = skill dir names to skip
101
+ * (and prune if already linked); `failOpen` = true when config was
102
+ * unavailable so nothing was gated.
103
+ */
104
+ function resolveSkillGate(frameworkSkillsDir, cwd) {
105
+ const result = { toLink: [], gatedOff: [], failOpen: false };
106
+
107
+ let names = [];
108
+ try {
109
+ names = fs.readdirSync(frameworkSkillsDir).filter(name => {
110
+ if (name.startsWith('.')) return false;
111
+ try { return fs.lstatSync(path.join(frameworkSkillsDir, name)).isDirectory(); }
112
+ catch (_) { return false; }
113
+ });
114
+ } catch (_) {
115
+ return result; // no framework skills dir → empty sets, caller handles
116
+ }
117
+
118
+ const { features } = loadFeatures(cwd);
119
+
120
+ // Config unavailable → fail-open: link everything, gate nothing.
121
+ if (features === null) {
122
+ result.failOpen = true;
123
+ result.toLink = names.slice();
124
+ return result;
125
+ }
126
+
127
+ for (const name of names) {
128
+ const required = readRequiredFeatures(path.join(frameworkSkillsDir, name));
129
+ if (!required) { result.toLink.push(name); continue; }
130
+ // AND semantics: link iff EVERY required feature is explicitly true.
131
+ const allOn = required.every(f => features[f] === true);
132
+ if (allOn) result.toLink.push(name);
133
+ else result.gatedOff.push(name);
134
+ }
135
+
136
+ return result;
137
+ }
138
+
139
+ module.exports = { resolveSkillGate, readRequiredFeatures, loadFeatures };