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.
- package/CHANGELOG.md +39 -0
- package/README.md +2 -0
- package/VERSION +1 -1
- package/framework/.claude/agents/coder.md +3 -3
- package/framework/.claude/skills/new/SKILL.md +151 -2356
- package/framework/.claude/skills/new/references/codex-gate.md +129 -0
- package/framework/.claude/skills/new/references/commit.md +57 -0
- package/framework/.claude/skills/new/references/completeness.md +284 -0
- package/framework/.claude/skills/new/references/final-review.md +312 -0
- package/framework/.claude/skills/new/references/implement.md +353 -0
- package/framework/.claude/skills/new/references/merge-cleanup.md +175 -0
- package/framework/.claude/skills/new/references/metrics.md +72 -0
- package/framework/.claude/skills/new/references/production-readiness.md +232 -0
- package/framework/.claude/skills/new/references/review-cycle.md +247 -0
- package/framework/.claude/skills/new/references/setup.md +249 -0
- package/framework/.claude/skills/new/references/team-mode.md +253 -0
- package/framework/.claude/skills/worktree-manager/SKILL.md +16 -8
- package/framework/.claude/workflows/new-final-review.js +215 -0
- package/framework/agents/coding-standards.md +5 -5
- 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
|
@@ -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/
|
|
95
|
-
> Phase 3.7
|
|
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/
|
|
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/
|
|
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
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; }
|