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 +15 -1
- package/README.md +2 -0
- package/VERSION +1 -1
- package/framework/.claude/skills/new/SKILL.md +1 -1
- package/framework/.claude/skills/new/references/final-review.md +45 -0
- package/framework/.claude/workflows/new-final-review.js +215 -0
- package/framework/docs/WORKFLOWS.md +62 -0
- package/package.json +1 -1
- package/src/commands/doctor.js +54 -0
- package/src/commands/update.js +2 -1
- package/src/utils/symlinks.js +41 -12
- package/src/utils/tool-adapters/claude.js +4 -0
- package/src/utils/tool-adapters/codex.js +6 -0
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.
|
|
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.
|
|
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
package/src/commands/doctor.js
CHANGED
|
@@ -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',
|
package/src/commands/update.js
CHANGED
|
@@ -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
|
package/src/utils/symlinks.js
CHANGED
|
@@ -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
|
|
346
|
-
* adapter declares a target directory
|
|
347
|
-
*
|
|
348
|
-
*
|
|
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(
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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; }
|