baldart 4.13.0 → 4.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,7 +5,21 @@ 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.0] - 2026-06-04
9
+
10
+ **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).
11
+
12
+ > **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).
13
+
14
+ ### Added
15
+
16
+ - **`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).
17
+ - **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).
18
+ - **`framework/docs/WORKFLOWS.md`** (nuovo) — cos'è il payload `.claude/workflows/`, il modello opt-in + degradazione, il perché Claude-only e no-overlay.
19
+
20
+ ### Changed
21
+
22
+ - **`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
23
 
10
24
  **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
25
 
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.0
@@ -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
@@ -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.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"
@@ -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; }