baldart 4.12.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.
@@ -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
+ }
@@ -91,8 +91,8 @@ if (error) {
91
91
  > **SSOT.** This section is the canonical policy for reference-aliasing mutation
92
92
  > hazards. It is cited as the source of truth by `coder.md`
93
93
  > (§ Reference-Aliasing Mutation Hazards), `code-reviewer.md` (review checklist),
94
- > and `.claude/skills/new/SKILL.md` (Phase 2.5 step 5c deterministic detector +
95
- > Phase 3.7 trigger #6). When those files cite "§ Reference-Aliasing Mutation
94
+ > and `.claude/skills/new/references/{completeness,codex-gate}.md` (Phase 2.5 step 5c
95
+ > deterministic detector + Phase 3.7 gate). When those files cite "§ Reference-Aliasing Mutation
96
96
  > Patterns", they mean this section.
97
97
 
98
98
  ### The hazard
@@ -165,10 +165,10 @@ the helper's JSDoc so future editors find the contract.
165
165
  - **This section** — full policy + JSDoc contract + remediation menu.
166
166
  - `coder.md § Reference-Aliasing Mutation Hazards` — pre-implementation check.
167
167
  - `code-reviewer.md` review checklist — flags un-guarded patterns at review time.
168
- - `.claude/skills/new/SKILL.md § Phase 2.5 step 5c` — deterministic detector
169
- that flags un-guarded patterns as `[ALIAS-MUTATION] BLOCKER` in
168
+ - `.claude/skills/new/references/completeness.md § Phase 2.5 step 5c` — deterministic
169
+ detector that flags un-guarded patterns as `[ALIAS-MUTATION] BLOCKER` in
170
170
  `## Issues & Flags`.
171
- - `.claude/skills/new/SKILL.md § Phase 3.7 trigger #6` — a mutation-after-helper
171
+ - `.claude/skills/new/references/codex-gate.md § Phase 3.7` — a mutation-after-helper
172
172
  call invokes a per-card adversarial cross-model review before commit.
173
173
 
174
174
  ## Commit Message Format
@@ -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.12.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; }