baldart 4.13.0 → 4.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,7 +5,31 @@ 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.13.0] - 2026-06-04
8
+ ## [4.14.1] - 2026-06-04
9
+
10
+ **Il gate Simplify del Team Mode di `/new` (D.3b) ora skippa solo le card davvero trivial (`TRIVIAL_CARDS`), non tutte quelle con `review_profile == skip` — chiude una divergenza col path sequential (Phase 2.55 `IS_TRIVIAL`) e con la stessa D.1.5 di team-mode.** La pipeline per-card di `/new` vive in due modalità (sequential e team) che ri-enunciano alcuni gate; due predicati gemelli erano davvero divergenti. **Drift 1 (bug, fixato)**: il sequential Phase 2.55 ri-conferma `IS_TRIVIAL` sul diff ACTUAL committato (tutte e 3 le condizioni, incl. il check non-source) → una card `review_profile=skip` con un file SORGENTE nel diff reale NON è trivial → ESEGUE Simplify; il team D.3b invece skippava sul solo `review_profile == skip`, ignorando il diff reale → la saltava. Aggravante: team-mode era incoerente con sé stesso — a D.1.5 calcola GIÀ `TRIVIAL_CARDS` col check non-source completo (e la sua nota SSOT dichiara «D.3b already skipped for trivial»), ma il gate D.3b gateava sul set più lasco. Blast radius: solo qualità (la card prendeva comunque D.2 code-review + il Final FULL gate), ma in team mode si perdeva la difesa contro la deviazione del coder che il sequential ha. **Drift 2 (intenzionale, documentato)**: lo scope per-GRUPPO della QA gate (D.4) vs per-CARD del sequential (21b) — stesso predicato (`deep`/risk → FULL, altrimenti defer al Final), granularità per necessità del modello a wave (qa-sentinel gira una volta sul diff combinato del gruppo); è il lato deliberatamente più conservativo (una card rischiosa → tutto il gruppo FULL), nessun gap di correttezza → comportamento invariato, aggiunta solo una frase di chiarimento. **PATCH** (bugfix di un falso-skip in team mode; nessuna capability, nessuna chiave `baldart.config.yml`).
11
+
12
+ > **Why.** I gate ri-enunciati nei due path di `/new` devono restare predicato-identici, o le due modalità divergono silenziosamente. Qui il fix è un puro allineamento: D.3b passa dal set lasco (`review_profile == skip`) al set già calcolato a monte a D.1.5 (`TRIVIAL_CARDS` = `review_profile==skip` AND 0 Step-A triggers AND non-source diff), che è esattamente l'`IS_TRIVIAL` del sequential 2.55. Nessuna nuova logica, nessun ricalcolo: si consuma il set che D.1.5 dichiarava già di pilotare. Il Drift 2 si lascia perché la granularità per-gruppo è una necessità del modello a wave (un solo qa-sentinel sul diff combinato), non una svista, ed è il lato più sicuro — ma ora è documentato come intenzionale per non farlo rifixare in futuro.
13
+
14
+ ### Changed
15
+
16
+ - **`framework/.claude/skills/new/references/team-mode.md`** — gate D.3b (Simplify) riscritto da «SKIP per `review_profile == skip`» a «SKIP per card in `TRIVIAL_CARDS`» (il set già computato a D.1.5), con log-line allineata al sequential (`simplify: SKIPPED (trivial — non-source diff)`); una card `skip` con file sorgente nel diff ora esegue Simplify come nel sequential 2.55. D.4 (QA gate) — aggiunta una frase che documenta lo scope per-gruppo come INTENZIONALE (stesso predicato del sequential 21b, valutato a livello di gruppo perché qa-sentinel gira una volta sul diff combinato; lato deliberatamente più conservativo). Nessun cambio di logica su D.4.
17
+
18
+ ## [4.14.0] - 2026-06-04
19
+
20
+ **Nuovo tipo di payload `.claude/workflows/` (dynamic workflow di Claude Code) + il primo workflow `new-final-review`: la Final Review F.2–F.4 di `/new` può girare come script di orchestrazione deterministico, opt-in e con fallback inline.** I dynamic workflow (research-preview di Claude Code) spostano l'orchestrazione fan-out fuori dal context-window dell'orchestrator, nel codice. Una review avversariale ha **refutato** l'idea di farne il motore *per-card* di `/new` (rompe la portabilità Codex — sono Claude-only; dipende da una preview come breaking change; duplica la logica di gate col team-mode; introduce attrito-permessi e regressione di recovery proprio dove `/new` è più intrecciata con worktree/git/stato). Il bersaglio scelto è invece la parte dove i rischi spariscono e il valore resta: la **Final Review F.1–F.6** è **read-only** (gli agenti leggono solo il diff committato), ha **definizione SSOT unica** condivisa da sequential e team mode (→ nessun twin), e ha l'**adversarial-verify già nativo** (FP-check di Codex in F.3, cross-validation `code-reviewer` a confidence<80 in F.4). **MINOR** (nuova capability additiva: nuovo payload type + un workflow; nessun breaking change, nessuna chiave `baldart.config.yml` — il gating è runtime via presenza del tool `Workflow`; i consumer senza workflow o Codex-only eseguono la Final Review inline come prima).
21
+
22
+ > **Why.** La Final Review è l'unica fase di `/new` con una definizione singola condivisa da entrambe le modalità e senza ownership di stato git o gate-utente: estrarla in un workflow non duplica nulla e non regredisce nulla. Il confine è netto — il workflow incapsula **F.2 (baseline) + F.3 (fan-out Codex ‖ doc-reviewer ‖ api-perf-cost-auditor ‖ qa-sentinel) + F.4 (consolidamento + verifica)** e ritorna i finding già classificati (`VERIFIED`/`NEEDS_MANUAL_CONFIRMATION`, `FALSE_POSITIVE` scartati) + la gate table; **F.1 (scope), F.5 (apply fix + build + `AskUserQuestion`) e F.6 (wrap-up) restano nella skill**, perché un workflow non può eseguire git né chiedere all'utente. La prosa di `final-review.md` resta SSOT e fallback sempre-disponibile; il workflow ne mirrora solo la *forma di orchestrazione* (i suoi agent-brief citano il modulo per la *semantica* di review). Distribuzione: symlink per-item come skill/agent/command, ma **senza overlay** (l'overlay-merger è per `.md`; un `.js` non ha sezioni markdown) e **solo per Claude** (`codex.js` ritorna `workflowsDir()=null` → mai linkato in un albero Codex).
23
+
24
+ ### Added
25
+
26
+ - **`framework/.claude/workflows/new-final-review.js`** (nuovo) — workflow `meta.name: 'new-final-review'`, phases `Baseline`/`Review`/`Verify`. Fan-out F.2–F.4 con fallback Codex→`code-reviewer` come branch JS, schema sui finding (`VERIFIED`/`FALSE_POSITIVE`/`NEEDS_MANUAL_CONFIRMATION`), `api-perf-cost-auditor` saltato via branch quando non ci sono file API/data. Tutto da `args` (portabile, anti-contaminazione).
27
+ - **Payload type `.claude/workflows/`** — `src/utils/tool-adapters/claude.js` espone `workflowsDir()='.claude/workflows'`; `codex.js` `workflowsDir()=null` (Claude-only). `src/utils/symlinks.js`: `_mergeBulkDir` generalizzato con mappa per-kind (`MERGE_KINDS`: estensione + overlay on/off), nuovo `mergeWorkflows()`, chiamato in `createAllSymlinks()` e nel reconcile di `update.js`; `verifySymlinks()` esteso a `workflows`. `BALDART_MANAGED_PATTERNS` (`update.js`) include `workflows`. `doctor.js`: probe + repair advisory dei workflow non linkati (Claude-only, non bloccante).
28
+ - **`framework/docs/WORKFLOWS.md`** (nuovo) — cos'è il payload `.claude/workflows/`, il modello opt-in + degradazione, il perché Claude-only e no-overlay.
29
+
30
+ ### Changed
31
+
32
+ - **`framework/.claude/skills/new/references/final-review.md`** — nuovo **Step F.1.5 (workflow delegation gate)**: se il tool `Workflow` è disponibile e `new-final-review.js` è linkato → delega F.2–F.4 al workflow e porta i finding/gate-table a F.5; altrimenti inline come prima (prosa = SSOT/fallback). Drift-note per i maintainer. `SKILL.md` § Routing annota la delega.
9
33
 
10
34
  **Il SKILL.md di `/new` scende da 61k a ~10k token (−82%) — il dettaglio passo-passo di ogni fase è uscito dal corpo skill (prefisso di sistema, ri-letto a ogni turno) in moduli `references/*.md` caricati on-demand.** Diagnosi data-driven su una run reale (`/new` su ~4 card → 480k token di contesto): forensics sul transcript dell'orchestrator → il **floor statico è 152k token PRIMA che parta una card**, e il colpevole #1 è il SKILL.md monolitico da **2766 righe / 61k token**, residente nel prefisso di sistema cached e ri-validato a ogni turno (362 turni nella run osservata). Una prima ipotesi ovvia (comprimere la *prosa* degli agent, stile "caveman") è stata **misurata e scartata**: la prosa è ~8k = 2% del totale. La cura replica 1:1 il pattern già in prod su `/prd` (core snello + reference modules + routing table): il core tiene solo gli invarianti cross-fase (Context Tracking, Progress Visibility, § Context economy, Agent Routing, QA Profile, Trivial fast-lane, Risk-signal detector, Fix Application Log) + una **§ Routing** che mappa fase→modulo→skip; ogni fase carica il suo modulo via Read quando ci entra. Contenuto spostato **verbatim** (zero cambi comportamentali; le stesse fasi/gate, nello stesso ordine). **MINOR** (refactor capability-neutral + nuovo contratto interno di module-loading on-demand; install-contract e invocazione invariati, consumer auto-aggiornati via directory-symlink; nessuna chiave `baldart.config.yml`, nessuna modifica CLI).
11
35
 
package/README.md CHANGED
@@ -339,6 +339,7 @@ your-project/
339
339
  │ ├── agents/ # Symlink → .framework/.claude/agents/
340
340
  │ ├── commands/ # Symlink → .framework/.claude/commands/
341
341
  │ ├── skills/ # Per-item merge dir (v2.1.1+): framework symlinks alongside your own skills
342
+ │ ├── workflows/ # Per-item symlinks of framework dynamic workflows (v4.14.0+, Claude-only)
342
343
  │ └── hooks/ # Customizable copies
343
344
  ├── baldart.config.yml # Project context: paths/identity/stack/features (v3.0.0+)
344
345
  ├── .baldart/
@@ -427,6 +428,7 @@ Files with symlinks auto-update when framework updates:
427
428
  - `agents/`
428
429
  - `.claude/agents/`
429
430
  - `.claude/commands/`
431
+ - `.claude/workflows/` (v4.14.0+) — framework dynamic workflows, per-item symlinks (Claude-only; no Codex equivalent)
430
432
 
431
433
  Files managed by the CLI:
432
434
 
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.13.0
1
+ 4.14.1
@@ -268,7 +268,7 @@ mappa di navigazione.
268
268
  | 2.55 / 2.6 / 3 / 3.5 — Simplify + E2E + Doc + QA | dopo AC-Closure | [`references/review-cycle.md`](references/review-cycle.md) | `IS_TRIVIAL`→skip 2.55+3.5; `has_e2e_review:false`/backend-only→skip 2.6; `light`+no-doc-diff→Doc a Final; `balanced`→QA a Final |
269
269
  | 3.7 — Pre-Merge Codex Review Gate | pre-commit | [`references/codex-gate.md`](references/codex-gate.md) | solo `IS_TRIVIAL` (altrimenti unconditional; `light`/`full` è DEPTH, non skip) |
270
270
  | 4-5 — Commit + Context Clean | fine card | [`references/commit.md`](references/commit.md) | — |
271
- | Final review F.1-F.6 | dopo l'ultima card | [`references/final-review.md`](references/final-review.md) | mai (FULL gate unconditional) |
271
+ | Final review F.1-F.6 | dopo l'ultima card | [`references/final-review.md`](references/final-review.md) | mai (FULL gate unconditional). **F.2–F.4 delegate al workflow `new-final-review` se il tool `Workflow` è disponibile (Step F.1.5); altrimenti inline.** F.5/F.6 sempre nella skill. |
272
272
  | 6 / 6b / 6c — Merge & cleanup | dopo Final | [`references/merge-cleanup.md`](references/merge-cleanup.md) | mai (BLOCKING) |
273
273
  | 7 — Production Readiness | dopo il merge | [`references/production-readiness.md`](references/production-readiness.md) | — |
274
274
  | 8 — Metrics Log | ultimo | [`references/metrics.md`](references/metrics.md) | mai (non-blocking, best-effort) |
@@ -46,6 +46,51 @@ Once ALL cards are committed in the worktree:
46
46
  batch diff, so cards reviewed at `light` in Phase 3.7 still get a guaranteed full review before
47
47
  merge. Record the union in the tracker; downstream F.3 reviews it in full.
48
48
 
49
+ ### Step F.1.5 — Workflow delegation gate (v4.14.0 — opt-in, additive)
50
+
51
+ The fan-out of F.2–F.4 (architecture baseline + multi-agent review + finding
52
+ consolidation/verification) is a **read-only** pass — it reads the committed batch
53
+ diff and emits findings, it touches no git state and asks the user nothing. That
54
+ makes it a clean fit for a **dynamic workflow**, which runs the fan-out as a
55
+ deterministic script outside this orchestrator's context window.
56
+
57
+ **Branch (decide once, here):**
58
+
59
+ - **IF** the `Workflow` tool is available to you **AND** the script
60
+ `.claude/workflows/new-final-review.js` is present (linked by the framework on
61
+ Claude-enabled installs) → **delegate F.2–F.4** to it:
62
+
63
+ ```
64
+ Workflow({ name: 'new-final-review', args: {
65
+ firstCardId, worktreePath, baseBranch, // from Phase 0 / tracker
66
+ cardPaths, // backlog YAML paths
67
+ reviewScopeFiles, // the FULL union from F.1
68
+ archBaselinePaths, // per-card baselines if ALL present (F.2 dedup), else null
69
+ hasApiDataFiles, // true unless NO scope file falls under paths.api_* / data-model
70
+ config // the parsed baldart.config.yml
71
+ }})
72
+ ```
73
+
74
+ The workflow returns `{ codexEngine, findings, gateTable, summary }` where
75
+ `findings` are already consolidated and classified (`VERIFIED` /
76
+ `NEEDS_MANUAL_CONFIRMATION`; `FALSE_POSITIVE` already dropped) and `gateTable`
77
+ is the qa-sentinel PASS/FAIL/SKIP table. **Skip the inline F.2/F.3/F.4 below**
78
+ and carry those results straight into **Step F.5** (fix application + final
79
+ build + the `AskUserQuestion` gates — which stay HERE, in the skill, because a
80
+ workflow cannot prompt the user). Log `f.final: delegated to new-final-review
81
+ workflow (engine: <codexEngine>)` in the tracker.
82
+
83
+ - **ELSE** (no `Workflow` tool — older Claude Code, `disableWorkflows`, or a
84
+ Codex install where workflows have no equivalent) → run the **inline F.2–F.4
85
+ below exactly as written**. This prose is the SSOT and the always-available
86
+ fallback; the workflow is only an accelerator that mirrors it.
87
+
88
+ > **Drift note (maintainers):** `new-final-review.js` encodes only the
89
+ > *orchestration shape* of F.2–F.4; its agent briefs cite THIS module for the
90
+ > review *semantics*, so the SSOT for what each reviewer checks stays here. When
91
+ > you change the F.2–F.4 reviewer set, gates, or classification rules, update the
92
+ > workflow's phase/agent wiring to match.
93
+
49
94
  ### Step F.2 — Architecture baseline
50
95
 
51
96
  5. **Reuse per-card baselines before re-spawning (dedup).** Phase 1 step 5b already persisted one
@@ -173,13 +173,13 @@ After ALL agents in the group complete successfully:
173
173
  3a. **D.3a — Phase 2.5b AC-Closure Gate (per-card, BLOCKING — non-skippable)** — For EACH card in the group, **sequentially**, invoke the full Phase 2.5b gate as documented in `### Phase 2.5b — AC-Closure Gate (BLOCKING — Scope Closure Discipline)`. This includes: build the AC Closure Ledger from the card YAML, run the rationalization scan, invoke `AskUserQuestion` one-per-deferred-AC, run the `implementation_notes` deferral audit, and persist the ledger in the tracker. Until EVERY card in the group exits PASS, do NOT proceed to D.3b. Cards exiting with `not_implemented` ACs that the user routes to "Implementa adesso" must finish their fix-coder loop and re-pass the gate before D.3b starts for the next card. Log under `## AC Closure Ledger — <CARD-ID>` per card.
174
174
 
175
175
  3b. **D.3b — Phase 2.55 Simplify (per-card, FANNED OUT across the group)** — The Simplify agents are **read-only analysis on file-disjoint per-card diffs** (the orchestrator applies the fixes afterward), so there is NO reason to run them one card at a time. **Spawn the per-card Simplify analysis for ALL eligible cards in PARALLEL** — in a SINGLE message, fire each card's Phase 2.55 trio (Reuse / Quality / Efficiency) against that card's diff captured to `/tmp/diff-<CARD-ID>.txt` (per Phase 2.55 step 2 — pass each trio the **path**, scoped to the card's File Ownership Map; never inline the diff). Per-card (not group-aggregate) so findings stay attributable. When all analyses return, **apply fixes per card** (file-disjoint → no write conflict), then re-run `npm run lint` and `npx tsc --noEmit` on the worktree ONCE for the whole group (redirect to disk per § "Context economy"). (Concurrency is capped by the platform; passing N cards is safe — excess agents queue.)
176
- - **Gate (enumerated, `review_profile`-driven)**: SKIP D.3b for a card whose `review_profile == skip` (trivial / non-implementable card Simplify is quality-only, so there is no merge-gate coverage to lose, and there is no substantive diff to simplify). Log `simplify: SKIPPED (review_profile=skip)`. For `light`/`balanced`/`deep` cards D.3b runs unchanged. This is the ONLY enumerated skip — never skip D.3b "for time" on a `light`+ card. (Skipped cards are simply omitted from the parallel fan-out.)
176
+ - **Gate (enumerated, `TRIVIAL_CARDS`-driven)**: SKIP D.3b for a card in **`TRIVIAL_CARDS`** (the set already computed at D.1.5 — `review_profile == skip` AND 0 Step-A triggers AND **non-source diff**), aligning team mode with sequential Phase 2.55's `IS_TRIVIAL` re-confirmation on the ACTUAL diff and with team-mode's own D.1.5 SSOT. A trivial card has no substantive diff to simplify, and Simplify is quality-only (no merge-gate coverage to lose). Log `simplify: SKIPPED (trivial — non-source diff)`. **A card with `review_profile == skip` whose committed diff DID touch a source file is NOT in `TRIVIAL_CARDS` → run D.3b for it** (exactly as sequential 2.55 does — `skip` is the floor, the real diff is the deciding check). For `light`/`balanced`/`deep` cards D.3b runs unchanged. This is the ONLY enumerated skip — never skip D.3b "for time" on a non-trivial card. (Skipped cards are simply omitted from the parallel fan-out.)
177
177
 
178
178
  3c. **D.3c — Phase 2.6 E2E-Review (per-card)** — First, evaluate the existing Gate table for EVERY card at once (skip when `features.has_e2e_review: false`, backend-only diff per the diff predicate documented in Phase 2.6, or card type in the Phase 2.6 skip set — `backend`/`api`/`db`/`infra`/`docs`/`chore`/`config`). In practice most cards in a group skip this gate (backend/db/api), so the eligible set is usually 0–1. For the cards that PASS the gate, invoke `/e2e-review` in programmatic mode with that card's payload. Each `/e2e-review` keeps its own isolated state dir (`.baldart/e2e-review/<CARD-ID>/`), so multiple runs do not clobber each other's artifacts.
179
179
  - **Parallel-when-safe**: if **two or more** cards pass the gate, you MAY fan them out in parallel ONLY when they hit **disjoint routes/pages** — the genuinely shared resource is the worktree's single dev-server port, and concurrent Playwright sessions against one server are fine for disjoint routes. If the eligible cards touch overlapping routes (or `/e2e-review` spins its own server and a port clash is possible), run those **sequentially** to avoid a flaky cross-test. Default to parallel for the common disjoint case; fall back to sequential on any contention.
180
180
  - BLOCKING per-card: if `/e2e-review` returns `"blocked"` or `"error"`, surface to the user via the same `AskUserQuestion` documented in Phase 2.6 — do NOT proceed to D.4 with an unresolved card. Skips are logged with the documented gate reason, never with `"time budget"` or similar.
181
181
 
182
- 4. **D.4 — QA gate (group; since v4.7.0 — runs only for `deep` / risk-escalation, else deferred to Final)** — Read each card's `review_profile` field and the D.1.5 Step-A trigger result. **Run the group qa-sentinel (once, at FULL) iff** the group's MAX profile is `deep` **OR** any card in the group had a Step-A risk escalation. **Otherwise (max profile ≤ `balanced`, no escalation) → DEFER the group QA to the Final Review F.3 qa-sentinel** (FULL suite + build + audit over the entire batch — the unconditional merge gate); log `D.4 QA: DEFERRED to Final FULL gate (group max=balanced, no risk escalation)`. This mirrors sequential step 21b. When it DOES run, invoke qa-sentinel at FULL (the combined group diff must be validated together) using the same prompt contract as Phase 3.5 step 22. ⚠️ A balanced-only group runs its first suite at the Final gate; merge safety is preserved because Final is FULL over the whole batch.
182
+ 4. **D.4 — QA gate (group; since v4.7.0 — runs only for `deep` / risk-escalation, else deferred to Final)** — Read each card's `review_profile` field and the D.1.5 Step-A trigger result. **Run the group qa-sentinel (once, at FULL) iff** the group's MAX profile is `deep` **OR** any card in the group had a Step-A risk escalation. **Otherwise (max profile ≤ `balanced`, no escalation) → DEFER the group QA to the Final Review F.3 qa-sentinel** (FULL suite + build + audit over the entire batch — the unconditional merge gate); log `D.4 QA: DEFERRED to Final FULL gate (group max=balanced, no risk escalation)`. This mirrors sequential step 21b. When it DOES run, invoke qa-sentinel at FULL (the combined group diff must be validated together) using the same prompt contract as Phase 3.5 step 22. **Granularity (INTENTIONAL — not a drift)**: the predicate is identical to sequential 21b (`deep`/risk-escalation → FULL, else defer to the Final FULL gate), but it is evaluated **per-GROUP** here whereas sequential evaluates it per-CARD. This is by design — qa-sentinel runs once over the group's combined diff (the wave-batched model gives it no per-card granularity), so the gate is deliberately the more conservative side: a single `deep`/risk card escalates the WHOLE group to FULL rather than just itself. No correctness gap — every card still reaches a FULL suite no later than the unconditional Final gate. ⚠️ A balanced-only group runs its first suite at the Final gate; merge safety is preserved because Final is FULL over the whole batch.
183
183
 
184
184
  4a. **D.4a — Per-card doc gate (consumes D.2, since v3.35.0; doc-reviewer-applied since v3.40.0)** — Do NOT re-run the doc AUDIT (D.2 already produced per-card-attributed findings). If any card in the group has doc findings, invoke the **doc-reviewer once in write mode** over the group, passing the per-card-attributed findings from D.2, to APPLY all doc fixes in a single pass (it is now alone — code-reviewer is done — so the D.2 parallel-safety constraint no longer applies). Do **NOT** spawn a fix-coder for doc findings: `doc` is owned by `doc-reviewer` (see "Domain-Override Domains"); a code-oriented coder lacks the doc-invariant contract. The only exception is a doc-drift→bug finding rooted in CODE — that follows the D.4b code fix path. (Previously D.4a spawned a fix-coder per card; the audit was already collapsed to a single attributed D.2 pass. D.4b's per-card `/codexreview` also skips its doc-reviewer via the lean contract, so the doc AUDIT runs once per group while the doc FIXES run once per group via doc-reviewer.) **Telemetry** — append one row per applied doc finding to `## Fix Application Log`: `D.4a | doc | est_lines=<n> | decision=doc-reviewer | applied_by=doc-reviewer | card=<ID> | finding=<1-line>`. **Phase-8 producer (named counter)** — ALSO record per card the `doc_gaps: found=<N> fixed=<M>` line in that card's `## Completed Cards` entry (same named counter as sequential Phase 3 step 15), so Phase 8 reads `doc_gaps_found`/`doc_gaps_fixed` from a structured field, not a free-form row.
185
185
 
@@ -0,0 +1,215 @@
1
+ export const meta = {
2
+ name: 'new-final-review',
3
+ description:
4
+ "Cross-batch final code review for /new. Fans out a multi-agent review (Codex primary + doc-reviewer + api-perf-cost-auditor + qa-sentinel gates) over the WHOLE batch diff, then adversarially verifies low-confidence findings. Read-only: it returns classified findings and a gate table, and applies NO fixes (the calling skill owns fix application + user gates). Maps to references/final-review.md steps F.2–F.4.",
5
+ phases: [
6
+ { title: 'Baseline', detail: 'architecture grounding for the batch scope (F.2)' },
7
+ { title: 'Review', detail: 'parallel multi-agent review of the batch diff (F.3)' },
8
+ { title: 'Verify', detail: 'adversarial validation of low-confidence findings (F.4)' },
9
+ ],
10
+ }
11
+
12
+ // ───────────────────────────────────────────────────────────────────────────
13
+ // args contract — supplied by the /new skill at F.1 (it owns git + tracker):
14
+ // firstCardId string first card id, for labels/ids
15
+ // worktreePath string the batch worktree
16
+ // baseBranch string trunk the diff is taken against
17
+ // cardPaths string[] backlog YAML paths (agents Read them)
18
+ // reviewScopeFiles string[] FULL union of touched files (F.1)
19
+ // archBaselinePaths string[] | null per-card baselines; null → spawn architect (F.2)
20
+ // hasApiDataFiles boolean gates api-perf-cost-auditor (default true)
21
+ // config object resolved baldart.config.yml (paths.*, …)
22
+ //
23
+ // Return value (consumed by the skill at F.5):
24
+ // { codexEngine, findings:[…classified, FALSE_POSITIVE dropped], gateTable, summary }
25
+ // ───────────────────────────────────────────────────────────────────────────
26
+
27
+ const a = args || {}
28
+ const scope = Array.isArray(a.reviewScopeFiles) ? a.reviewScopeFiles : []
29
+ const cards = Array.isArray(a.cardPaths) ? a.cardPaths : []
30
+ const cfg = a.config || {}
31
+ const highRisk = (cfg.paths && cfg.paths.high_risk_modules) || [] // security-domain hint
32
+ const protocolRef = '.claude/skills/new/references/final-review.md'
33
+
34
+ if (!scope.length) {
35
+ log('new-final-review: empty review scope — nothing to review.')
36
+ return { codexEngine: 'none', findings: [], gateTable: [], summary: emptySummary() }
37
+ }
38
+
39
+ // ---- Schemas ----------------------------------------------------------------
40
+ const FINDING = {
41
+ type: 'object',
42
+ required: ['finding_id', 'title', 'severity', 'confidence', 'evidence', 'minimal_fix_direction', 'domain'],
43
+ additionalProperties: false,
44
+ properties: {
45
+ finding_id: { type: 'string', description: '<CARD-ID>-F### or <agent>-F###' },
46
+ title: { type: 'string' },
47
+ severity: { enum: ['BLOCKER', 'HIGH', 'MEDIUM', 'LOW'] },
48
+ confidence: { type: 'number', description: '0-100' },
49
+ evidence: { type: 'string', description: 'exact file:line + code quote' },
50
+ minimal_fix_direction: { type: 'string' },
51
+ domain: { enum: ['doc', 'security', 'migration', 'code', 'perf', 'test'], description: 'Domain-Override routing bucket' },
52
+ },
53
+ }
54
+ const FINDINGS_SCHEMA = {
55
+ type: 'object', required: ['findings'], additionalProperties: false,
56
+ properties: { findings: { type: 'array', items: FINDING }, note: { type: 'string' } },
57
+ }
58
+ const CODEX_SCHEMA = {
59
+ type: 'object', required: ['codexAvailable', 'findings'], additionalProperties: false,
60
+ properties: {
61
+ codexAvailable: { type: 'boolean', description: 'false if CODEX_NOT_FOUND / TIMED_OUT' },
62
+ findings: { type: 'array', items: FINDING },
63
+ note: { type: 'string' },
64
+ },
65
+ }
66
+ const GATES_SCHEMA = {
67
+ type: 'object', required: ['gates'], additionalProperties: false,
68
+ properties: {
69
+ gates: {
70
+ type: 'array',
71
+ items: {
72
+ type: 'object', required: ['gate', 'status'], additionalProperties: false,
73
+ properties: { gate: { type: 'string' }, status: { enum: ['PASS', 'FAIL', 'SKIP'] }, detail: { type: 'string' } },
74
+ },
75
+ },
76
+ },
77
+ }
78
+ const VERDICT_SCHEMA = {
79
+ type: 'object', required: ['classification'], additionalProperties: false,
80
+ properties: {
81
+ classification: { enum: ['VERIFIED', 'FALSE_POSITIVE', 'NEEDS_MANUAL_CONFIRMATION'] },
82
+ rationale: { type: 'string' },
83
+ },
84
+ }
85
+
86
+ // ---- Shared brief fragments -------------------------------------------------
87
+ const scopeBrief = [
88
+ `Worktree: ${a.worktreePath || '(cwd)'}`,
89
+ `Base branch for the diff: ${a.baseBranch || '(trunk)'}`,
90
+ `Backlog cards (Read each for acceptance_criteria + entrypoints):\n${cards.join('\n') || '(none)'}`,
91
+ `Files changed — review ALL of these in full:\n${scope.join('\n')}`,
92
+ ].join('\n\n')
93
+
94
+ // ───────────────────────────────────────────────────────────────────────────
95
+ // Phase Baseline (F.2) — reuse per-card baselines when present, else architect ×1
96
+ // ───────────────────────────────────────────────────────────────────────────
97
+ phase('Baseline')
98
+ let baselineBrief
99
+ const baselinePaths = Array.isArray(a.archBaselinePaths) ? a.archBaselinePaths.filter(Boolean) : []
100
+ if (baselinePaths.length) {
101
+ // Pass-by-path (context economy): agents Read these on demand.
102
+ baselineBrief = `Architecture baseline files (Read each — file paths, type signatures, patterns, high-risk paths):\n${baselinePaths.join('\n')}`
103
+ log(`Baseline: reusing ${baselinePaths.length} per-card baseline file(s).`)
104
+ } else {
105
+ const arch = await agent(
106
+ `You are grounding a post-implementation code review. Map the EXISTING architecture, critical patterns, and high-risk code paths relevant to this batch's changed files, so downstream reviewers can spot regressions.\n\n${scopeBrief}\n\nReturn a concise baseline (the key modules, their contracts, and the regression-prone seams touched by this diff). Do not review the changes yet.`,
107
+ { label: 'arch-baseline', phase: 'Baseline', agentType: 'codebase-architect',
108
+ schema: { type: 'object', required: ['baseline'], additionalProperties: false, properties: { baseline: { type: 'string' } } } }
109
+ )
110
+ baselineBrief = `Architecture baseline (from codebase-architect):\n${(arch && arch.baseline) || '(unavailable)'}`
111
+ log('Baseline: spawned codebase-architect (no per-card baselines supplied).')
112
+ }
113
+
114
+ // ───────────────────────────────────────────────────────────────────────────
115
+ // Phase Review (F.3) — Codex primary ‖ doc-reviewer ‖ api-perf-cost-auditor ‖ qa-sentinel
116
+ // ───────────────────────────────────────────────────────────────────────────
117
+ phase('Review')
118
+
119
+ const codexPrompt =
120
+ `Run a deep, cross-model code review over this batch diff, following the /codexreview protocol summarized in ${protocolRef} (Step F.3). The code is already written and committed — find bugs, regressions, security issues, and quality problems.\n\n` +
121
+ `Resolve and invoke the Codex companion (a non-Anthropic frontier model) via Bash:\n` +
122
+ " CODEX_SCRIPT=\"$(ls -d ~/.claude/plugins/marketplaces/openai-codex/plugins/codex/scripts/codex-companion.mjs ~/.claude/plugins/cache/openai-codex/codex/*/scripts/codex-companion.mjs 2>/dev/null | sort -V | tail -1)\"\n" +
123
+ `If no companion script is found, OR the review does not complete, return { "codexAvailable": false, "findings": [] } — do NOT fabricate findings.\n\n` +
124
+ `${scopeBrief}\n\n${baselineBrief}\n\n` +
125
+ `For each finding return: finding_id, title, severity (BLOCKER|HIGH|MEDIUM|LOW), confidence (0-100), evidence (exact file:line + code quote), minimal_fix_direction, and domain (doc|security|migration|code|perf|test). ` +
126
+ `Run the mandatory false-positive check on every finding and suppress the unconvincing ones (your findings are treated as already FP-validated). Set codexAvailable:true when the review ran.`
127
+
128
+ const docPrompt =
129
+ `Cross-card documentation + SSOT-registry review over the batch diff, per ${protocolRef} Step F.3 (doc-reviewer row). Check doc consistency, ssot-registry completeness, and invariants across the changed files.\n\n${scopeBrief}\n\n${baselineBrief}\n\nReturn findings (domain almost always "doc"). Use the finding schema fields.`
130
+
131
+ const apiPrompt =
132
+ `API / data-model / performance / cost defect review over the batch diff, per ${protocolRef} Step F.3 (api-perf-cost-auditor row). Look for unbounded reads, N+1, missing pagination, contract drift, and cost regressions.\n\n${scopeBrief}\n\n${baselineBrief}\n\nReturn findings with domain in {code, perf, migration, security}. Use the finding schema fields.`
133
+
134
+ const qaPrompt =
135
+ `Run MECHANICAL GATES ONLY over the batch scope, per ${protocolRef} Step F.3 (qa-sentinel row): lint, type-check, the full test suite, build, dependency audit, and markdownlint as applicable to this project. Do NOT read source for code findings, do NOT emit severities — return only a PASS/FAIL/SKIP gate table.\n\nWorktree: ${a.worktreePath || '(cwd)'}\nChanged files:\n${scope.join('\n')}`
136
+
137
+ const reviewThunks = [
138
+ () => agent(codexPrompt, { label: 'codex', phase: 'Review', schema: CODEX_SCHEMA }).then((r) => ({ kind: 'codex', r })),
139
+ () => agent(docPrompt, { label: 'doc-reviewer', phase: 'Review', agentType: 'doc-reviewer', schema: FINDINGS_SCHEMA }).then((r) => ({ kind: 'doc', r })),
140
+ () => agent(qaPrompt, { label: 'qa-sentinel', phase: 'Review', agentType: 'qa-sentinel', schema: GATES_SCHEMA }).then((r) => ({ kind: 'qa', r })),
141
+ ]
142
+ // api-perf-cost-auditor is skipped when the batch touches no API/data files (F.3 note).
143
+ if (a.hasApiDataFiles !== false) {
144
+ reviewThunks.push(() => agent(apiPrompt, { label: 'api-perf-cost-auditor', phase: 'Review', agentType: 'api-perf-cost-auditor', schema: FINDINGS_SCHEMA }).then((r) => ({ kind: 'api', r })))
145
+ } else {
146
+ log('Review: api-perf-cost-auditor skipped (no API/data files in scope).')
147
+ }
148
+
149
+ const reviewResults = (await parallel(reviewThunks)).filter(Boolean)
150
+
151
+ // ---- Fan-in (F.4 step 9): collect findings + Codex fallback branch ----------
152
+ let raw = []
153
+ let gateTable = []
154
+ let codexEngine = 'codex'
155
+ for (const item of reviewResults) {
156
+ if (item.kind === 'codex') {
157
+ if (item.r && item.r.codexAvailable && Array.isArray(item.r.findings)) {
158
+ raw.push(...item.r.findings.map((f) => ({ ...f, source: 'codex', preValidated: true })))
159
+ } else {
160
+ // CODEX_NOT_FOUND / TIMED_OUT → Claude code-reviewer fallback (F.4 step 8).
161
+ codexEngine = 'code-reviewer (fallback)'
162
+ log('Review: Codex unavailable — falling back to code-reviewer for the primary code review.')
163
+ const fb = await agent(
164
+ `Codex was unavailable for the batch final review. Run the FULL code review yourself over the batch diff, per ${protocolRef} Step F.3.\n\n${scopeBrief}\n\n${baselineBrief}\n\nReturn findings using the schema fields, with a self false-positive check applied.`,
165
+ { label: 'code-reviewer (fallback)', phase: 'Review', agentType: 'code-reviewer', schema: FINDINGS_SCHEMA }
166
+ )
167
+ if (fb && Array.isArray(fb.findings)) raw.push(...fb.findings.map((f) => ({ ...f, source: 'code-reviewer', preValidated: false })))
168
+ }
169
+ } else if (item.kind === 'qa') {
170
+ gateTable = (item.r && item.r.gates) || []
171
+ } else if (item.r && Array.isArray(item.r.findings)) {
172
+ raw.push(...item.r.findings.map((f) => ({ ...f, source: item.kind, preValidated: false })))
173
+ }
174
+ }
175
+
176
+ // ───────────────────────────────────────────────────────────────────────────
177
+ // Phase Verify (F.4) — adversarial validation of low-confidence findings
178
+ // Codex findings are pre-FP-validated → VERIFIED. Claude-agent findings with
179
+ // confidence < 80 get a code-reviewer static validator; disagreement →
180
+ // NEEDS_MANUAL_CONFIRMATION (never silently dropped). High-confidence Claude
181
+ // findings pass through as VERIFIED.
182
+ // ───────────────────────────────────────────────────────────────────────────
183
+ phase('Verify')
184
+ const classified = (await parallel(raw.map((f) => () => verifyFinding(f)))).filter(Boolean)
185
+
186
+ async function verifyFinding(f) {
187
+ if (f.preValidated || (typeof f.confidence === 'number' && f.confidence >= 80)) {
188
+ return { ...f, classification: 'VERIFIED' }
189
+ }
190
+ const v = await agent(
191
+ `Adversarially validate this code-review finding as a static reviewer over the cited file:line. Default to FALSE_POSITIVE if the evidence does not hold; use NEEDS_MANUAL_CONFIRMATION only when you genuinely cannot decide from the code.\n\n` +
192
+ `finding_id: ${f.finding_id}\nseverity: ${f.severity}\ntitle: ${f.title}\nevidence: ${f.evidence}\ndomain: ${f.domain}\nsecurity-sensitive paths: ${highRisk.join(', ') || '(none configured)'}`,
193
+ { label: `verify:${f.finding_id}`, phase: 'Verify', agentType: 'code-reviewer', schema: VERDICT_SCHEMA }
194
+ )
195
+ return { ...f, classification: (v && v.classification) || 'NEEDS_MANUAL_CONFIRMATION' }
196
+ }
197
+
198
+ // ---- Result: drop FALSE_POSITIVE, keep VERIFIED + NEEDS_MANUAL (F.4 step 9) --
199
+ const findings = classified.filter((f) => f.classification !== 'FALSE_POSITIVE')
200
+ const summary = {
201
+ total: raw.length,
202
+ verified: classified.filter((f) => f.classification === 'VERIFIED').length,
203
+ falsePositive: classified.filter((f) => f.classification === 'FALSE_POSITIVE').length,
204
+ needsManual: classified.filter((f) => f.classification === 'NEEDS_MANUAL_CONFIRMATION').length,
205
+ blockers: classified.filter((f) => f.classification === 'VERIFIED' && f.severity === 'BLOCKER').length,
206
+ highs: classified.filter((f) => f.classification === 'VERIFIED' && f.severity === 'HIGH').length,
207
+ failingGates: gateTable.filter((g) => g.status === 'FAIL').map((g) => g.gate),
208
+ }
209
+ log(`Final review done: ${summary.verified} verified (${summary.blockers} BLOCKER / ${summary.highs} HIGH), ${summary.needsManual} needs-manual, ${summary.failingGates.length} failing gate(s). Engine: ${codexEngine}.`)
210
+
211
+ return { codexEngine, findings, gateTable, summary }
212
+
213
+ function emptySummary() {
214
+ return { total: 0, verified: 0, falsePositive: 0, needsManual: 0, blockers: 0, highs: 0, failingGates: [] }
215
+ }
@@ -0,0 +1,62 @@
1
+ # Dynamic Workflows (`.claude/workflows/`)
2
+
3
+ **Since v4.14.0.** BALDART can ship Claude Code **dynamic workflows** — JavaScript
4
+ orchestration scripts that fan out subagents at scale, with the plan held in code
5
+ (deterministic branches/loops) instead of in the orchestrator's context window.
6
+ They are distributed as a new per-item payload under `.claude/workflows/`.
7
+
8
+ This is an **opt-in accelerator**, never a hard dependency. Every skill that
9
+ delegates to a workflow keeps a complete inline fallback, so a repo where
10
+ workflows are unavailable behaves exactly as before.
11
+
12
+ ## What ships today
13
+
14
+ | Workflow | Used by | What it does |
15
+ | :--- | :--- | :--- |
16
+ | `new-final-review` | `/new` Final Review (Step F.1.5) | Runs the read-only cross-batch review fan-out — architecture baseline + Codex ‖ doc-reviewer ‖ api-perf-cost-auditor ‖ qa-sentinel — then adversarially verifies low-confidence findings and returns them classified. Applies no fixes (the skill owns fix application + user gates). |
17
+
18
+ ## How distribution works
19
+
20
+ - **Per-item symlink**, same model as `.claude/skills/`, `.claude/agents/`,
21
+ `.claude/commands/`: each framework `<name>.js` under
22
+ `.framework/framework/.claude/workflows/` is symlinked into the consumer's
23
+ `.claude/workflows/<name>.js`. `baldart add` / `update` / `doctor` create and
24
+ reconcile these links.
25
+ - **Claude-only.** Dynamic workflows are a Claude Code runtime feature with no
26
+ OpenAI Codex equivalent. The Codex tool adapter returns `workflowsDir() = null`,
27
+ so workflow scripts are never linked into a Codex tree. Codex consumers (and any
28
+ Claude consumer with workflows disabled) run the delegating skill's inline path.
29
+ - **No overlay.** Overlays are a markdown construct (`## [OVERRIDE]` sections + an
30
+ HTML-comment marker) and do not apply to `.js`. Workflows are distributed as
31
+ plain symlinks; customise the *delegation decision* in the skill/overlay layer,
32
+ not the script.
33
+ - **Contamination-scanned.** The `framework-edit-gate` hook scans workflow `.js`
34
+ content by path (not extension), so a hardcoded project path or identity token
35
+ in a workflow is blocked exactly like in any other framework file. Workflows
36
+ must read all project-specific facts from their `args` (passed by the skill from
37
+ `baldart.config.yml`), never hardcode them.
38
+
39
+ ## The opt-in / degradation contract
40
+
41
+ A delegating skill decides **at runtime**, with no new `baldart.config.yml` key:
42
+
43
+ 1. **IF** the `Workflow` tool is available to the model **AND** the workflow
44
+ script is linked → call `Workflow({ name, args })` and use its return value.
45
+ 2. **ELSE** → run the skill's inline fallback (the prose remains the SSOT).
46
+
47
+ There is deliberately no `features.use_workflows` flag: workflow availability is a
48
+ property of the Claude Code session (research-preview, plan tier, the native
49
+ `disableWorkflows` setting), not a fact the repo can record. Force-disabling is the
50
+ native Claude Code toggle; the skill simply detects and degrades.
51
+
52
+ ## Authoring notes (maintainers)
53
+
54
+ - A workflow script begins with a pure-literal `export const meta = { name,
55
+ description, phases }`, then a body using `agent()` / `parallel()` / `pipeline()`
56
+ / `phase()` / `log()` and the `args` / `budget` globals.
57
+ - The script cannot run bash/git/fs — only its agents can. Keep anything that
58
+ needs git, shared state, or a user prompt in the calling skill; give the
59
+ workflow the read-only fan-out.
60
+ - Single-source the *semantics*: a workflow's agent briefs should cite the skill's
61
+ reference module for what each agent checks, so the workflow encodes only the
62
+ *orchestration shape*. Carry a drift note in the reference module.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "4.13.0",
3
+ "version": "4.14.1",
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"
@@ -292,6 +292,37 @@ async function detectState(cwd, opts = {}) {
292
292
  }
293
293
  } catch (_) { /* never block doctor on probe */ }
294
294
 
295
+ // ---- Workflow symlink integrity (since v4.14.0) --------------------
296
+ // Dynamic workflows (.js) are Claude-only and per-item symlinked into
297
+ // `.claude/workflows/`. Unlike agents there is NO hard discovery gate —
298
+ // a missing workflow just makes the consuming skill fall back to its
299
+ // inline path (e.g. /new's Final Review) — so this is advisory self-heal,
300
+ // not a blocker. Gated on `claude` being enabled (workflows have no Codex
301
+ // equivalent, so a Codex-only install must not be flagged).
302
+ state.workflowSymlinksBroken = [];
303
+ try {
304
+ if (state.frameworkPresent) {
305
+ const toolsEnabled = (config && !config.__malformed && config.tools && Array.isArray(config.tools.enabled))
306
+ ? config.tools.enabled : ['claude'];
307
+ if (toolsEnabled.includes('claude')) {
308
+ const frameworkWorkflowsDir = path.join(cwd, '.framework', 'framework', '.claude', 'workflows');
309
+ const consumerWorkflowsDir = path.join(cwd, '.claude', 'workflows');
310
+ if (fs.existsSync(frameworkWorkflowsDir)) {
311
+ const baldartWorkflows = fs.readdirSync(frameworkWorkflowsDir)
312
+ .filter((f) => f.endsWith('.js') && !f.startsWith('.'))
313
+ .map((f) => f.replace(/\.js$/, ''));
314
+ for (const name of baldartWorkflows) {
315
+ try {
316
+ fs.statSync(path.join(consumerWorkflowsDir, `${name}.js`));
317
+ } catch (_) {
318
+ state.workflowSymlinksBroken.push(name);
319
+ }
320
+ }
321
+ }
322
+ }
323
+ }
324
+ } catch (_) { /* never block doctor on probe */ }
325
+
295
326
  // `.framework/` is a git subtree — gitignore matching it breaks every
296
327
  // future `git add` under that path (the failure mode that hit v3.13.0
297
328
  // consumers during push). Detect any gitignore rule that catches the
@@ -584,6 +615,29 @@ function planActions(state) {
584
615
  });
585
616
  }
586
617
 
618
+ // Workflow symlink integrity (since v4.14.0). Advisory — a missing workflow
619
+ // symlink degrades the consuming skill to its inline path, it does not block.
620
+ // Re-runs only the workflow merge pass (same rationale as the agent repair).
621
+ if (state.workflowSymlinksBroken && state.workflowSymlinksBroken.length > 0) {
622
+ actions.push({
623
+ key: 'repair-workflow-symlinks',
624
+ label: `Link ${state.workflowSymlinksBroken.length} framework workflow(s)`,
625
+ why: `These framework dynamic workflows are not linked into .claude/workflows/: ${state.workflowSymlinksBroken.join(', ')}. Skills that delegate to them (e.g. /new's Final Review) fall back to their inline path until the symlinks exist — not a failure, but you lose the workflow acceleration.`,
626
+ autoOk: true,
627
+ run: async () => {
628
+ const SymlinkUtils = require('../utils/symlinks');
629
+ const cfg = loadConfig(process.cwd());
630
+ const enabledTools = (cfg && !cfg.__malformed
631
+ && cfg.tools && Array.isArray(cfg.tools.enabled))
632
+ ? cfg.tools.enabled
633
+ : ['claude'];
634
+ const symlinks = new SymlinkUtils();
635
+ symlinks.mergeWorkflows({ tools: enabledTools });
636
+ UI.success(`Linked framework workflows for tools: ${enabledTools.join(', ')}`);
637
+ },
638
+ });
639
+ }
640
+
587
641
  if (state.lspEnabled && state.lspBroken && state.lspBroken.length > 0) {
588
642
  actions.push({
589
643
  key: 'lsp-fix',
@@ -103,7 +103,7 @@ const BALDART_MANAGED_PATTERNS = [
103
103
  /^\.framework(\/|$)/,
104
104
  /^AGENTS\.md$/,
105
105
  /^agents(\.backup)?(\/|$)/,
106
- /^\.claude\/(agents|commands|skills|hooks|routines|settings\.json)(\/|$)/,
106
+ /^\.claude\/(agents|commands|skills|workflows|hooks|routines|settings\.json)(\/|$)/,
107
107
  /^\.baldart(\/|$)/,
108
108
  /^baldart\.config\.yml$/,
109
109
  /^docs\/references\/(ui-guidelines\.template\.md|brand-guidelines\.md)$/,
@@ -1180,6 +1180,7 @@ async function update(options = {}, unknownArgs = []) {
1180
1180
  symlinks.mergeSkills({ tools: enabledTools });
1181
1181
  symlinks.mergeAgents({ tools: enabledTools });
1182
1182
  symlinks.mergeCommands({ tools: enabledTools });
1183
+ symlinks.mergeWorkflows({ tools: enabledTools });
1183
1184
  }
1184
1185
 
1185
1186
  // Routines wizard (since v2.1.0) — surfaces routines added in the new framework version
@@ -12,6 +12,22 @@ const FRAMEWORK_DIR = '.framework';
12
12
  const FRAMEWORK_PAYLOAD = path.join(FRAMEWORK_DIR, 'framework');
13
13
  const CONFLICT_LOG = path.join('.baldart', 'skill-conflicts.json');
14
14
 
15
+ // Per-item merge kinds whose framework source is a flat directory of single
16
+ // files under `.claude/<kind>s/`. Each entry declares:
17
+ // - dirGetter: the adapter method returning the consumer-side target dir
18
+ // (null when the tool doesn't support the kind, e.g. Codex → workflows).
19
+ // - ext: the file extension of the source items.
20
+ // - overlay: whether the base+overlay generation path applies. Overlays
21
+ // are a markdown construct (`## [OVERRIDE]` sections + HTML-comment
22
+ // marker), so they only make sense for `.md` kinds. Workflows are `.js`
23
+ // and are distributed as plain per-item symlinks — no overlay.
24
+ // - exclude: filenames in the source dir that are NOT installable items.
25
+ const MERGE_KINDS = {
26
+ agent: { dirGetter: 'agentsDir', ext: '.md', overlay: true, exclude: new Set(['REGISTRY.md']) },
27
+ command: { dirGetter: 'commandsDir', ext: '.md', overlay: true, exclude: new Set(['REGISTRY.md']) },
28
+ workflow: { dirGetter: 'workflowsDir', ext: '.js', overlay: false, exclude: new Set() },
29
+ };
30
+
15
31
  /**
16
32
  * Symlink management for BALDART.
17
33
  *
@@ -325,6 +341,10 @@ class SymlinkUtils {
325
341
  UI.section('Merging Framework Commands');
326
342
  this.mergeCommands({ tools: opts.tools });
327
343
 
344
+ UI.newline();
345
+ UI.section('Merging Framework Workflows');
346
+ this.mergeWorkflows({ tools: opts.tools });
347
+
328
348
  UI.newline();
329
349
  }
330
350
 
@@ -339,15 +359,20 @@ class SymlinkUtils {
339
359
 
340
360
  mergeAgents(opts = {}) { return this._mergeBulkDir('agent', opts); }
341
361
  mergeCommands(opts = {}) { return this._mergeBulkDir('command', opts); }
362
+ mergeWorkflows(opts = {}) { return this._mergeBulkDir('workflow', opts); }
342
363
 
343
364
  /**
344
365
  * Generic per-item merge for kinds whose framework source is a directory
345
- * of single .md files (agents, commands). For each enabled tool whose
346
- * adapter declares a target directory (claude.agentsDir(), etc.), this
347
- * either symlinks each base file or — when an overlay exists — writes a
348
- * generated real file with the overlay applied.
366
+ * of single files (agents/commands → .md, workflows → .js). For each
367
+ * enabled tool whose adapter declares a target directory
368
+ * (claude.agentsDir(), claude.workflowsDir(), …), this symlinks each base
369
+ * file or for overlay-capable `.md` kinds with an overlay present —
370
+ * writes a generated real file with the overlay applied. Workflows are
371
+ * `.js` and overlay-free, so they always take the plain-symlink path.
349
372
  */
350
373
  _mergeBulkDir(kind, opts = {}) {
374
+ const cfg = MERGE_KINDS[kind];
375
+ if (!cfg) throw new Error(`_mergeBulkDir: unknown kind '${kind}'`);
351
376
  const tools = (opts.tools && opts.tools.length) ? opts.tools : ['claude'];
352
377
  const aggregate = { linked: [], generated: [], skipped: [], conflicts: [], warnings: [] };
353
378
  const { getAdapter } = require('./tool-adapters');
@@ -359,20 +384,20 @@ class SymlinkUtils {
359
384
  }
360
385
 
361
386
  const fwItems = fs.readdirSync(fwSourceDir)
362
- .filter((n) => n.endsWith('.md') && !n.startsWith('.') && n !== 'REGISTRY.md');
387
+ .filter((n) => n.endsWith(cfg.ext) && !n.startsWith('.') && !cfg.exclude.has(n));
363
388
 
364
389
  const frameworkVersion = this._readFrameworkVersion();
365
390
 
366
391
  for (const tool of tools) {
367
392
  const adapter = getAdapter(tool, this.cwd);
368
- const dirGetter = kind === 'agent' ? adapter.agentsDir : adapter.commandsDir;
369
- const targetRel = dirGetter ? dirGetter.call(adapter) : null;
370
- if (!targetRel) continue; // tool doesn't support this kind (e.g. Codex)
393
+ const dirGetter = adapter[cfg.dirGetter];
394
+ const targetRel = (typeof dirGetter === 'function') ? dirGetter.call(adapter) : null;
395
+ if (!targetRel) continue; // tool doesn't support this kind (e.g. Codex → workflows)
371
396
 
372
397
  this.ensureDirectory(targetRel);
373
398
 
374
399
  for (const filename of fwItems) {
375
- const baseName = filename.replace(/\.md$/, '');
400
+ const baseName = filename.slice(0, -cfg.ext.length);
376
401
  const linkPath = path.join(this.cwd, targetRel, filename);
377
402
  const fwAbsolute = path.join(fwSourceDir, filename);
378
403
 
@@ -384,7 +409,7 @@ class SymlinkUtils {
384
409
 
385
410
  const overlayRel = path.join('.baldart', 'overlays', `${kind}s`, filename);
386
411
  const overlayAbs = path.join(this.cwd, overlayRel);
387
- const hasOverlay = fs.existsSync(overlayAbs);
412
+ const hasOverlay = cfg.overlay && fs.existsSync(overlayAbs);
388
413
 
389
414
  // Inspect what currently lives at the link path (if anything).
390
415
  let lstat = null;
@@ -474,7 +499,10 @@ class SymlinkUtils {
474
499
  aggregate.linked.push({ tool: adapter.name, kind, name: baseName });
475
500
  return;
476
501
  }
477
- UI.warning(`[${adapter.label}] ${kind} name conflict (user file at ${path.join(targetRel, filename)}). Framework version NOT installed. Move user content to .baldart/overlays/${kind}s/${filename} to specialize without losing upstream updates.`);
502
+ const specializeHint = MERGE_KINDS[kind] && MERGE_KINDS[kind].overlay
503
+ ? ` Move user content to .baldart/overlays/${kind}s/${filename} to specialize without losing upstream updates.`
504
+ : ` Rename your local ${kind} to keep both.`;
505
+ UI.warning(`[${adapter.label}] ${kind} name conflict (user file at ${path.join(targetRel, filename)}). Framework version NOT installed.${specializeHint}`);
478
506
  aggregate.conflicts.push({
479
507
  tool: adapter.name, kind, name: baseName,
480
508
  local_path: path.join(targetRel, filename),
@@ -647,7 +675,8 @@ class SymlinkUtils {
647
675
  }
648
676
 
649
677
  // v3.8.0+: agents and commands also use per-item merge with overlay support.
650
- for (const [kindGetter, kindLabel] of [['agentsDir', 'agents'], ['commandsDir', 'commands']]) {
678
+ // v4.14.0+: workflows (.js) use the same per-item merge, overlay-free.
679
+ for (const [kindGetter, kindLabel] of [['agentsDir', 'agents'], ['commandsDir', 'commands'], ['workflowsDir', 'workflows']]) {
651
680
  if (typeof adapter[kindGetter] !== 'function') continue;
652
681
  const rel = adapter[kindGetter]();
653
682
  if (!rel) continue;
@@ -5,6 +5,7 @@
5
5
  * - `.claude/skills/<skill>/SKILL.md` (skills, per-item symlinks)
6
6
  * - `.claude/agents/<agent>.md` (subagents, bulk symlink)
7
7
  * - `.claude/commands/<command>.md` (slash commands, bulk symlink)
8
+ * - `.claude/workflows/<workflow>.js` (dynamic workflows, per-item symlinks)
8
9
  * - `.claude/settings.json` (hooks, ide config)
9
10
  * - `AGENTS.md` (root coordination protocol)
10
11
  *
@@ -27,6 +28,9 @@ class ClaudeAdapter {
27
28
  /** Where this tool reads slash commands from (null if unsupported). */
28
29
  commandsDir() { return '.claude/commands'; }
29
30
 
31
+ /** Where this tool reads dynamic workflow scripts from (null if unsupported). */
32
+ workflowsDir() { return '.claude/workflows'; }
33
+
30
34
  /** Whether this tool consumes subagent definitions (`.claude/agents/`). */
31
35
  supportsSubagents() { return true; }
32
36
 
@@ -40,6 +40,12 @@ class CodexAdapter {
40
40
  agentsDir() { return null; }
41
41
  commandsDir() { return null; }
42
42
 
43
+ // Codex has no equivalent of Claude Code dynamic workflows (the JS
44
+ // orchestration runtime is Claude-only). Returning null disables the
45
+ // per-item workflow merge for Codex — the same source workflow scripts are
46
+ // simply never linked into a Codex tree.
47
+ workflowsDir() { return null; }
48
+
43
49
  supportsSubagents() { return false; }
44
50
  supportsSlashCommands() { return false; }
45
51
  supportsHooks() { return false; }