baldart 4.99.0 → 5.0.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 +75 -0
- package/README.md +1 -0
- package/VERSION +1 -1
- package/framework/.claude/agents/CHANGELOG.md +82 -0
- package/framework/.claude/agents/REGISTRY.md +3 -1
- package/framework/.claude/agents/code-reviewer.md +84 -400
- package/framework/.claude/agents/codebase-architect.md +71 -356
- package/framework/.claude/agents/coder.md +131 -291
- package/framework/.claude/agents/doc-reviewer.md +126 -450
- package/framework/.claude/agents/plan-auditor.md +137 -436
- package/framework/.claude/agents/qa-sentinel.md +59 -313
- package/framework/.claude/agents/skill-improver.md +60 -4
- package/framework/.claude/agents/ui-expert.md +104 -591
- package/framework/.claude/hooks/agent-discovery-gate.js +2 -2
- package/framework/.claude/hooks/agent-discovery-info.sh +1 -0
- package/framework/.claude/skills/new/CHANGELOG.md +11 -0
- package/framework/.claude/skills/new/SKILL.md +1 -1
- package/framework/.claude/skills/new/references/codex-gate.md +25 -11
- package/framework/.claude/skills/new/references/implement.md +43 -3
- package/framework/.claude/skills/new/references/review-cycle.md +16 -4
- package/framework/.claude/skills/new/scripts/build-review-bundle.mjs +104 -0
- package/framework/.claude/skills/new/scripts/doc-invariants.mjs +163 -0
- package/framework/.claude/skills/new2/CHANGELOG.md +4 -0
- package/framework/.claude/skills/new2/SKILL.md +5 -1
- package/framework/.claude/workflows/new2-resolve.js +10 -3
- package/framework/.claude/workflows/new2.js +51 -3
- package/framework/agents/agent-operating-protocol.md +152 -0
- package/framework/agents/doc-audit-protocol.md +227 -0
- package/framework/agents/index.md +4 -0
- package/framework/agents/review-protocol.md +207 -0
- package/framework/routines/finding-mine.routine.yml +56 -0
- package/framework/routines/index.yml +11 -0
- package/package.json +1 -1
- package/src/commands/configure.js +15 -0
- package/src/commands/doctor.js +69 -2
- package/src/utils/agent-slots.js +109 -0
- package/src/utils/overlay-merger.js +17 -8
- package/src/utils/symlinks.js +93 -33
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Review Protocol — the shared verification engine for BALDART reviewers
|
|
2
|
+
|
|
3
|
+
**Purpose**: the anti-hallucination and calibration passes every reviewer runs
|
|
4
|
+
— Challenge, Actionability, Simulation, Chain-of-Verification, quantified risk
|
|
5
|
+
scoring, specialist-spawn discipline — used to be duplicated (and drifting)
|
|
6
|
+
across `code-reviewer` and `plan-auditor`, with partial copies in
|
|
7
|
+
`security-reviewer` / `api-perf-cost-auditor`. This module is their **single
|
|
8
|
+
SSOT** (v5.0.0). Each reviewer keeps a 1-line BINDING version of every pass
|
|
9
|
+
inline and cites the matching section here for the full procedure. On
|
|
10
|
+
divergence, **this module wins**.
|
|
11
|
+
|
|
12
|
+
**What stays in the agent bodies, never here**: the pooled findings YAML
|
|
13
|
+
schema, verdict-line formats, severity never-demote lists, per-domain
|
|
14
|
+
checklists, spawn MATRICES (which specialist for which signal), and report
|
|
15
|
+
caps — those are I/O contracts and domain knowledge, not procedure.
|
|
16
|
+
|
|
17
|
+
**Consumers**: `code-reviewer`, `plan-auditor`, `security-reviewer`,
|
|
18
|
+
`api-perf-cost-auditor` (body citations); `doc-reviewer` cites
|
|
19
|
+
`SECTION=challenge` for its findings hygiene.
|
|
20
|
+
|
|
21
|
+
## Contract
|
|
22
|
+
|
|
23
|
+
- **Dispatch**: agents cite `agents/review-protocol.md SECTION=<challenge|simulation|cove|risk-scoring|specialist-spawn>`.
|
|
24
|
+
Grep for `### SECTION: <name>` and Read ONLY that section.
|
|
25
|
+
- **Order of passes** (normative): generate findings → **challenge** →
|
|
26
|
+
**simulation** (already-generated `simulation_failure` hypotheses included) →
|
|
27
|
+
**cove** on every survivor → **risk-scoring** → report. Specialist spawns
|
|
28
|
+
happen early (parallel with your own review); their findings enter YOUR
|
|
29
|
+
challenge + CoVe passes like your own.
|
|
30
|
+
- **Degrade-safe**: skipping a pass means less rigor, never a malformed report
|
|
31
|
+
— the report shape is defined in the agent body.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
### SECTION: challenge
|
|
36
|
+
|
|
37
|
+
**Challenge Pass** (before reporting). For EACH HIGH and MEDIUM finding ask:
|
|
38
|
+
|
|
39
|
+
> "What is the strongest argument that this is a false positive?"
|
|
40
|
+
|
|
41
|
+
Consider:
|
|
42
|
+
- Is this already handled elsewhere in the codebase?
|
|
43
|
+
- Is this a project convention I'm unfamiliar with (check the agent memory
|
|
44
|
+
false-positive list)?
|
|
45
|
+
- Is the issue intentionally deferred to a later card per `notes` / card scope?
|
|
46
|
+
- Am I applying a generic best practice that doesn't fit this context?
|
|
47
|
+
|
|
48
|
+
**Suppress** the finding if the FP argument is convincing, and record it:
|
|
49
|
+
|
|
50
|
+
```markdown
|
|
51
|
+
<details>
|
|
52
|
+
<summary>Suppressed findings (N items — challenge pass)</summary>
|
|
53
|
+
- **Finding title** — FP argument: <why suppressed>
|
|
54
|
+
</details>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Never-demote guard**: items on the agent's never-demote list (each reviewer
|
|
58
|
+
declares its own inline) are NEVER false positives — do not suppress them,
|
|
59
|
+
regardless of how convincing the FP argument sounds.
|
|
60
|
+
|
|
61
|
+
**Actionability Pass** (after the FP challenge). A finding that survives the
|
|
62
|
+
challenge can still need NO change — the code/plan is fine as-is and the only
|
|
63
|
+
honest "fix" is "acceptable as-is / verified safe". That is a **cleared
|
|
64
|
+
concern, not a finding**: a downstream fixer spawned on it would no-op. Either
|
|
65
|
+
omit it, or — when the pooled schema exposes `requires_action` — set
|
|
66
|
+
`requires_action: false` (recorded, never routed to a writer). Record cleared
|
|
67
|
+
concerns separately:
|
|
68
|
+
|
|
69
|
+
```markdown
|
|
70
|
+
<details>
|
|
71
|
+
<summary>Verified, no action (N items)</summary>
|
|
72
|
+
- **Title** — why it is safe as-is
|
|
73
|
+
</details>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Guard: actionability applies to MEDIUM/LOW only — a BLOCKER/HIGH is never
|
|
77
|
+
"no action". And if a change IS needed but is out of your scope, that is NOT
|
|
78
|
+
no-action: report it as a real finding (it surfaces as residual).
|
|
79
|
+
|
|
80
|
+
### SECTION: simulation
|
|
81
|
+
|
|
82
|
+
Walk the artifact as if you were the runtime (diff) or the implementing
|
|
83
|
+
engineer (plan). Emit findings of type `simulation_failure` with the exact
|
|
84
|
+
breaking point and the broken invariant.
|
|
85
|
+
|
|
86
|
+
**Diff variant** (code review) — for each non-trivial changed
|
|
87
|
+
function/handler:
|
|
88
|
+
1. **Input boundary**: feed it the messiest realistic input (empty, null,
|
|
89
|
+
malformed JSON, oversized payload, malicious script, expired token,
|
|
90
|
+
concurrent request from the same user). Where does it break first?
|
|
91
|
+
2. **State machine consistency**: if the change mutates persistent state (DB,
|
|
92
|
+
session/local storage, cookies), trace the state at each branch — does any
|
|
93
|
+
branch leave inconsistent state?
|
|
94
|
+
3. **Reversibility**: if the function fails mid-execution, can the partial
|
|
95
|
+
side effects be rolled back? A transactional/atomic write is OK; a sequence
|
|
96
|
+
of independent writes without a transaction wrapper is NOT.
|
|
97
|
+
4. **Concurrent runs**: two parallel requests on the same resource — where do
|
|
98
|
+
they collide?
|
|
99
|
+
5. **Loop boundary**: any `for`/`map` over unbounded data (collection without
|
|
100
|
+
limit, user-provided array)? Where does it explode?
|
|
101
|
+
|
|
102
|
+
**Plan variant** (plan audit) — for each plan step:
|
|
103
|
+
1. **Preconditions**: are all prerequisites from prior steps actually
|
|
104
|
+
satisfied at this point?
|
|
105
|
+
2. **State machine consistency**: what is the shared state (record, env var,
|
|
106
|
+
feature flag) at this step — consistent with later steps' assumptions?
|
|
107
|
+
3. **Reversibility**: if step N fails, can steps 1..N-1 roll back cleanly? If
|
|
108
|
+
not → `irreversible_step_without_safety_net`.
|
|
109
|
+
4. **Concurrent runs**: two instances of this plan running simultaneously
|
|
110
|
+
(parallel cards, retry, multiple environments) — where do they collide?
|
|
111
|
+
5. **External dependency clock**: async propagation (index build, DNS, CDN
|
|
112
|
+
purge, deploy) — is wait time accounted for?
|
|
113
|
+
|
|
114
|
+
**Hypothesis discipline (BINDING)**: simulation is an LLM mental walk, NOT
|
|
115
|
+
execution — every `simulation_failure` is a *hypothesis* until grounded.
|
|
116
|
+
Each one MUST survive the CoVe pass below (grep/read the actual code path);
|
|
117
|
+
set `cove_verified: true` only when the breaking branch is confirmed against
|
|
118
|
+
real code. Drop any simulation claim you cannot ground — never ship an
|
|
119
|
+
unverified "the runtime will break here" as HIGH.
|
|
120
|
+
|
|
121
|
+
### SECTION: cove
|
|
122
|
+
|
|
123
|
+
**Chain-of-Verification** (for every surviving HIGH/MEDIUM finding, after
|
|
124
|
+
Challenge + Simulation). Generate 2–3 verification questions per finding and
|
|
125
|
+
EXECUTE them via grep/read — this forces grounding of every citation in actual
|
|
126
|
+
evidence.
|
|
127
|
+
|
|
128
|
+
Example — finding "auth wrapper missing on POST handler at `<route-file>:45`"
|
|
129
|
+
(`<auth-wrapper>` = the project's auth guard, resolve from
|
|
130
|
+
`${paths.high_risk_modules}`):
|
|
131
|
+
1. Does the file exist? → `test -f <route-file>`
|
|
132
|
+
2. Is there really no auth import/wrapper? → `rg -n "<auth-wrapper>" <route-file>`
|
|
133
|
+
3. Is the route actually public per docs? → `rg -l "<route-path>" ${paths.references_dir}/api/`
|
|
134
|
+
|
|
135
|
+
Drop findings whose verification fails (the finding itself was wrong) and
|
|
136
|
+
record them:
|
|
137
|
+
|
|
138
|
+
```markdown
|
|
139
|
+
### Hallucinated Findings Dropped (CoVe)
|
|
140
|
+
- **Finding title** — Verification: `<command>` → `<result>` → dropped because <reason>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
A finding whose evidence (file/line/quote) cannot be re-located verbatim is
|
|
144
|
+
dropped, not "approximately relocated".
|
|
145
|
+
|
|
146
|
+
### SECTION: risk-scoring
|
|
147
|
+
|
|
148
|
+
**Quantified risk** (mandatory on every HIGH finding, and on risk-register
|
|
149
|
+
rows):
|
|
150
|
+
- **Impact** (1–5): 1 = cosmetic, 5 = data loss / security breach / production
|
|
151
|
+
outage.
|
|
152
|
+
- **Likelihood** (1–5): 1 = theoretical only, 5 = will hit on first run.
|
|
153
|
+
- **Priority** = Impact × Likelihood (1–25).
|
|
154
|
+
|
|
155
|
+
Thresholds:
|
|
156
|
+
- Priority ≥ 16 → automatic **BLOCKER**.
|
|
157
|
+
- Priority 9–15 → confirms **HIGH**.
|
|
158
|
+
- Priority < 9 → demote to MEDIUM — unless the item is on the agent's
|
|
159
|
+
never-demote list.
|
|
160
|
+
|
|
161
|
+
**Severity calibration (BINDING)**: severity is **ABSOLUTE, per finding,
|
|
162
|
+
anchored to its evidenced consequence** — never a relative ranking, never a
|
|
163
|
+
quota. Positional bands ("top 20% → HIGH") manufacture mandatory findings on
|
|
164
|
+
clean artifacts and demote real defects on large batches; both are calibration
|
|
165
|
+
failures. For each survivor ask: *"what happens if this ships / is implemented
|
|
166
|
+
exactly as written?"* A HIGH MUST cite its evidence (file/field/line + the
|
|
167
|
+
code path that proves it); no evidence citation → cap at MEDIUM. **Zero HIGH —
|
|
168
|
+
or zero findings at all — on a clean artifact is a legitimate, reportable
|
|
169
|
+
outcome.** Do not stretch severities to fill bands.
|
|
170
|
+
|
|
171
|
+
### SECTION: specialist-spawn
|
|
172
|
+
|
|
173
|
+
Procedure for the specialist auto-spawn matrices (each reviewer keeps its OWN
|
|
174
|
+
matrix — signal → agent — inline; this section is the shared discipline):
|
|
175
|
+
|
|
176
|
+
1. **Orchestrated-mode suppression (BINDING — check FIRST).** If your spawn
|
|
177
|
+
prompt or lean contract carries a `specialist_dispatch` block
|
|
178
|
+
(`{"suppress": [...], "owner": ...}`) — or the legacy
|
|
179
|
+
`skip_doc_reviewer: true` flag — you MUST NOT spawn any agent listed in
|
|
180
|
+
`suppress`. The orchestrator owns specialist routing on that run (it runs
|
|
181
|
+
those reviews itself in dedicated phases); a nested spawn would duplicate
|
|
182
|
+
the review, untracked. Record what you would have routed as a normal
|
|
183
|
+
finding tagged `dispatch_deferred: <agent>` and let the orchestrator route
|
|
184
|
+
it. **Standalone invocations (no dispatch block): the matrix applies
|
|
185
|
+
normally.**
|
|
186
|
+
2. **At most once per specialist.** If a coordinating team has independently
|
|
187
|
+
spawned (or will spawn) a matched specialist on the same artifact, consume
|
|
188
|
+
its findings instead of re-spawning.
|
|
189
|
+
3. **Parallel, single message** — spawn all matched specialists in one message
|
|
190
|
+
with multiple Task calls.
|
|
191
|
+
4. **Structured returns**: instruct each specialist to return its structured
|
|
192
|
+
(Mode A / pooled YAML) findings schema, never free markdown — that is what
|
|
193
|
+
makes the merge lossless.
|
|
194
|
+
5. **Merge with provenance**: fold their findings into YOUR findings output
|
|
195
|
+
with their `source: <agent>` preserved. Fill any schema field the
|
|
196
|
+
specialist did not populate from their evidence, or set `N/A` — never drop
|
|
197
|
+
a real finding because a field is missing.
|
|
198
|
+
6. **Specialist findings pass through YOUR Challenge + CoVe** like your own.
|
|
199
|
+
7. **Deduplicate ownership**: when a specialist owns a domain's depth (e.g.
|
|
200
|
+
security-reviewer on an auth diff), do not emit your own duplicate findings
|
|
201
|
+
for the same issues — cover only what the specialist did NOT raise. On a
|
|
202
|
+
severity conflict, the specialist's rating wins.
|
|
203
|
+
8. **No generic substitution**: if a named specialist is unavailable, do NOT
|
|
204
|
+
substitute `general-purpose` or any generic agent — surface
|
|
205
|
+
`CAPABILITY_UNAVAILABLE: <agent>` in your verdict context and cover that
|
|
206
|
+
domain with your own persona checklist.
|
|
207
|
+
9. Zero specialist signals → no spawn; declare it in the verdict context.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
name: finding-mine
|
|
2
|
+
description: Monthly deep mining of pooled findings, QA reports, batch trackers and reviewer memories into classified improvement proposals — the anticipatory loop behind the weekly skill-improve.
|
|
3
|
+
since_version: 5.0.0
|
|
4
|
+
|
|
5
|
+
schedule:
|
|
6
|
+
cron: "0 3 1 * *" # 1st of the month, 03:00 UTC (an hour after a weekly skill-improve slot)
|
|
7
|
+
timezone: UTC
|
|
8
|
+
jitter_minutes: 0
|
|
9
|
+
cadence_label: monthly
|
|
10
|
+
|
|
11
|
+
agent: skill-improver
|
|
12
|
+
|
|
13
|
+
prompt: |
|
|
14
|
+
MODE: miner
|
|
15
|
+
|
|
16
|
+
Run the monthly deep mining pass per `skill-improver.md § MODE: miner`:
|
|
17
|
+
|
|
18
|
+
1. Read the past 30 days of evidence — routine reports under `docs/reports/`
|
|
19
|
+
(including the month's weekly `*-skill-improve.md` and their Upstream
|
|
20
|
+
Candidates), `qa/*.md`, the `/new` batch trackers
|
|
21
|
+
(`$(git rev-parse --git-common-dir)/baldart/run/batch-tracker-*.md`,
|
|
22
|
+
mining `## Fix Application Log` incl. the `model=` fix-pass A/B outcomes
|
|
23
|
+
and `## Lessons Learned`), and the reviewer agent memories
|
|
24
|
+
(`.claude/agent-memory/{code-reviewer,plan-auditor,qa-sentinel}/MEMORY.md`).
|
|
25
|
+
Absent sources are skipped with a one-line note.
|
|
26
|
+
2. Produce CLASSIFIED proposals (each with ≥2 independent occurrences cited):
|
|
27
|
+
`author-time-coder-rule` | `never-demote-code-reviewer` |
|
|
28
|
+
`deterministic-gate-candidate` | `card-schema-field-proposal` (proposal
|
|
29
|
+
ONLY — never applied) | `upstream-candidate` (consolidated + deduplicated
|
|
30
|
+
from the weeklies; a weekly overlay fix that KEPT recurring escalates here
|
|
31
|
+
with the failed-fix evidence).
|
|
32
|
+
3. Apply at most 3 overlay edits yourself (the strongest, evidence-backed) —
|
|
33
|
+
write surface identical to the weekly pass: `.baldart/overlays/` + `docs/`
|
|
34
|
+
only, commits prefixed `[FINDING-MINE]`. Everything else stays a proposal.
|
|
35
|
+
4. Emit `docs/reports/{{YYYYMMDD}}-finding-mine.md` — proposals grouped by
|
|
36
|
+
class with evidence citations, confidence, and effort estimate.
|
|
37
|
+
|
|
38
|
+
Graceful degradation: no recurring pattern in the month → write a short
|
|
39
|
+
"no proposals" report and exit cleanly. Never edit framework files directly
|
|
40
|
+
(the framework-edit-gate denies it — the overlay is the write surface).
|
|
41
|
+
|
|
42
|
+
output:
|
|
43
|
+
path: docs/reports/{{YYYYMMDD}}-finding-mine.md
|
|
44
|
+
commit:
|
|
45
|
+
enabled: true
|
|
46
|
+
prefix: "[FINDING-MINE]"
|
|
47
|
+
branch: main
|
|
48
|
+
|
|
49
|
+
required_artifacts:
|
|
50
|
+
- docs/reports/
|
|
51
|
+
- .claude/agents/skill-improver.md
|
|
52
|
+
|
|
53
|
+
backend_hints:
|
|
54
|
+
- claude-code-cloud
|
|
55
|
+
- github-actions
|
|
56
|
+
- cron
|
|
@@ -89,3 +89,14 @@ routines:
|
|
|
89
89
|
registry context, runs lint, and commits directly to the trunk (no PR).
|
|
90
90
|
Optional (only runs when features.has_i18n is true and target languages
|
|
91
91
|
are configured).
|
|
92
|
+
|
|
93
|
+
- name: finding-mine
|
|
94
|
+
file: finding-mine.routine.yml
|
|
95
|
+
cadence: monthly
|
|
96
|
+
since_version: 5.0.0
|
|
97
|
+
summary: |
|
|
98
|
+
Monthly deep mining pass (skill-improver MODE: miner) — 30 days of
|
|
99
|
+
pooled findings, QA reports, batch trackers and reviewer memories
|
|
100
|
+
distilled into classified proposals (author-time rules, never-demote
|
|
101
|
+
rows, deterministic-gate candidates, card-schema proposals, upstream
|
|
102
|
+
candidates). The anticipatory loop behind the weekly skill-improve.
|
package/package.json
CHANGED
|
@@ -1487,6 +1487,21 @@ async function configure(opts = {}) {
|
|
|
1487
1487
|
fs.writeFileSync(outPath, serialize(merged), 'utf8');
|
|
1488
1488
|
UI.success(`Wrote ${CONFIG_FILE}`);
|
|
1489
1489
|
|
|
1490
|
+
// v5.0.0 — slot-generated agents depend on the flags just written (features.*,
|
|
1491
|
+
// stack.database, …): re-run the agent merge so `.claude/agents/` reflects the
|
|
1492
|
+
// new config immediately (closes the "config changed without update" gap).
|
|
1493
|
+
// Fail-safe: configure must never die on a merge hiccup — doctor heals later.
|
|
1494
|
+
try {
|
|
1495
|
+
const SymlinkUtils = require('../utils/symlinks');
|
|
1496
|
+
const enabledTools = (merged.tools && Array.isArray(merged.tools.enabled) && merged.tools.enabled.length)
|
|
1497
|
+
? merged.tools.enabled : ['claude'];
|
|
1498
|
+
if (fs.existsSync(path.join(cwd, '.framework'))) {
|
|
1499
|
+
new SymlinkUtils(cwd).mergeAgents({ tools: enabledTools });
|
|
1500
|
+
}
|
|
1501
|
+
} catch (err) {
|
|
1502
|
+
UI.warning(`Agent re-merge after configure failed (${err.message}) — run \`npx baldart doctor\` to heal.`);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1490
1505
|
// Ensure overlays dir exists (user-owned)
|
|
1491
1506
|
const overlaysAbs = path.join(cwd, OVERLAYS_DIR);
|
|
1492
1507
|
if (!fs.existsSync(overlaysAbs)) {
|
package/src/commands/doctor.js
CHANGED
|
@@ -324,7 +324,7 @@ async function detectState(cwd, opts = {}) {
|
|
|
324
324
|
const consumerAgentsDir = path.join(cwd, '.claude', 'agents');
|
|
325
325
|
if (fs.existsSync(frameworkAgentsDir)) {
|
|
326
326
|
const baldartAgents = fs.readdirSync(frameworkAgentsDir)
|
|
327
|
-
.filter((f) => f.endsWith('.md') && f !== 'REGISTRY.md')
|
|
327
|
+
.filter((f) => f.endsWith('.md') && f !== 'REGISTRY.md' && f !== 'CHANGELOG.md')
|
|
328
328
|
.map((f) => f.replace(/\.md$/, ''));
|
|
329
329
|
for (const name of baldartAgents) {
|
|
330
330
|
try {
|
|
@@ -337,6 +337,47 @@ async function detectState(cwd, opts = {}) {
|
|
|
337
337
|
}
|
|
338
338
|
} catch (_) { /* never block doctor on probe */ }
|
|
339
339
|
|
|
340
|
+
// ---- Slot-generated agents integrity (since v5.0.0) -----------------
|
|
341
|
+
// An agent whose framework source carries `{{#flag}}` conditionals is
|
|
342
|
+
// installed as a GENERATED file (.baldart/generated/agents/<name>.md behind
|
|
343
|
+
// a symlink), stamped with base_sha (RAW framework source), config_sha
|
|
344
|
+
// (resolved flag set) and overlay_sha. Stale = any of the three drifted,
|
|
345
|
+
// or the agent is still a plain pre-5.0 symlink (slots never resolved).
|
|
346
|
+
// Heal = `regenerate-agents` action (delegates to SymlinkUtils.mergeAgents
|
|
347
|
+
// — the doctor never duplicates merge logic). Fully fail-safe.
|
|
348
|
+
state.generatedAgentsStale = [];
|
|
349
|
+
try {
|
|
350
|
+
if (state.frameworkPresent) {
|
|
351
|
+
const slots = require('../utils/agent-slots');
|
|
352
|
+
const om = require('../utils/overlay-merger');
|
|
353
|
+
const flags = slots.resolveAgentFlags((config && !config.__malformed) ? config : {});
|
|
354
|
+
const wantConfigSha = slots.computeAgentConfigSha(flags);
|
|
355
|
+
const frameworkAgentsDir = path.join(cwd, '.framework', 'framework', '.claude', 'agents');
|
|
356
|
+
if (fs.existsSync(frameworkAgentsDir)) {
|
|
357
|
+
for (const f of fs.readdirSync(frameworkAgentsDir)) {
|
|
358
|
+
if (!f.endsWith('.md') || f === 'REGISTRY.md' || f === 'CHANGELOG.md') continue;
|
|
359
|
+
const raw = fs.readFileSync(path.join(frameworkAgentsDir, f), 'utf8');
|
|
360
|
+
if (!slots.hasAgentSlots(raw)) continue;
|
|
361
|
+
const name = f.replace(/\.md$/, '');
|
|
362
|
+
let reason = null;
|
|
363
|
+
try {
|
|
364
|
+
const content = fs.readFileSync(path.join(cwd, '.claude', 'agents', f), 'utf8'); // follows the symlink
|
|
365
|
+
const marker = om.readMarker(content);
|
|
366
|
+
if (!marker || marker.kind !== 'agent') reason = 'slots not resolved (plain pre-5.0 symlink)';
|
|
367
|
+
else if (marker.baseSha !== om.shortSha(raw)) reason = 'framework source changed';
|
|
368
|
+
else if (marker.configSha !== wantConfigSha) reason = 'baldart.config.yml flags changed';
|
|
369
|
+
else {
|
|
370
|
+
const overlayAbs = path.join(cwd, '.baldart', 'overlays', 'agents', f);
|
|
371
|
+
const wantOverlaySha = fs.existsSync(overlayAbs) ? om.shortSha(fs.readFileSync(overlayAbs, 'utf8')) : 'none';
|
|
372
|
+
if (marker.overlaySha !== wantOverlaySha) reason = 'overlay changed';
|
|
373
|
+
}
|
|
374
|
+
} catch (_) { reason = 'not installed'; }
|
|
375
|
+
if (reason) state.generatedAgentsStale.push({ name, reason });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
} catch (_) { /* never block doctor on probe */ }
|
|
380
|
+
|
|
340
381
|
// ---- Codex agent transpile integrity (S6) --------------------------
|
|
341
382
|
// When Codex is an enabled tool, every framework agent (.md) must have been
|
|
342
383
|
// transpiled to `.codex/agents/<name>.toml` (generated on install/update).
|
|
@@ -357,7 +398,7 @@ async function detectState(cwd, opts = {}) {
|
|
|
357
398
|
const codexAgentsDir = path.join(cwd, '.codex', 'agents');
|
|
358
399
|
if (fs.existsSync(frameworkAgentsDir)) {
|
|
359
400
|
const baldartAgents = fs.readdirSync(frameworkAgentsDir)
|
|
360
|
-
.filter((f) => f.endsWith('.md') && f !== 'REGISTRY.md')
|
|
401
|
+
.filter((f) => f.endsWith('.md') && f !== 'REGISTRY.md' && f !== 'CHANGELOG.md')
|
|
361
402
|
.map((f) => f.replace(/\.md$/, ''));
|
|
362
403
|
for (const name of baldartAgents) {
|
|
363
404
|
try { fs.statSync(path.join(codexAgentsDir, `${name}.toml`)); }
|
|
@@ -963,6 +1004,32 @@ function planActions(state) {
|
|
|
963
1004
|
});
|
|
964
1005
|
}
|
|
965
1006
|
|
|
1007
|
+
// Slot-generated agents integrity (since v5.0.0). Regenerates any agent whose
|
|
1008
|
+
// installed copy drifted from source/config/overlay, or that a pre-5.0 install
|
|
1009
|
+
// left as a plain symlink (slot conditionals never resolved — the model would
|
|
1010
|
+
// read ALL variants). Delegates to `SymlinkUtils.mergeAgents`, same rationale
|
|
1011
|
+
// as `repair-agent-symlinks`: only the merge pass, never a full update.
|
|
1012
|
+
if (state.generatedAgentsStale && state.generatedAgentsStale.length > 0) {
|
|
1013
|
+
const detail = state.generatedAgentsStale.map((a) => `${a.name}: ${a.reason}`).join('; ');
|
|
1014
|
+
actions.push({
|
|
1015
|
+
key: 'regenerate-agents',
|
|
1016
|
+
label: `Regenerate ${state.generatedAgentsStale.length} slot-generated agent(s)`,
|
|
1017
|
+
why: `These agents are generated per-consumer from their framework source + baldart.config.yml flags (+ overlay), and the installed copy is out of date (${detail}). They are BALDART-generated — edit .baldart/overlays/agents/<name>.md, not the file itself.`,
|
|
1018
|
+
autoOk: true,
|
|
1019
|
+
run: async () => {
|
|
1020
|
+
const SymlinkUtils = require('../utils/symlinks');
|
|
1021
|
+
const cfg = loadConfig(process.cwd());
|
|
1022
|
+
const enabledTools = (cfg && !cfg.__malformed
|
|
1023
|
+
&& cfg.tools && Array.isArray(cfg.tools.enabled))
|
|
1024
|
+
? cfg.tools.enabled
|
|
1025
|
+
: ['claude'];
|
|
1026
|
+
const symlinks = new SymlinkUtils();
|
|
1027
|
+
symlinks.mergeAgents({ tools: enabledTools });
|
|
1028
|
+
UI.success(`Regenerated slot-generated agents for tools: ${enabledTools.join(', ')}`);
|
|
1029
|
+
},
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
|
|
966
1033
|
// Workflow symlink integrity (since v4.14.0). Advisory — a missing workflow
|
|
967
1034
|
// symlink degrades the consuming skill to its inline path, it does not block.
|
|
968
1035
|
// Re-runs only the workflow merge pass (same rationale as the agent repair).
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-slots.js — install-time slot resolution for agent definitions (v5.0.0).
|
|
3
|
+
*
|
|
4
|
+
* Agent sources under framework/.claude/agents/ may carry `{{#flag}}…{{/flag}}`
|
|
5
|
+
* conditionals (SLOT syntax — flags ONLY: no `{{ scalar }}`, no `{{> partial}}`;
|
|
6
|
+
* `${paths.*}` references stay runtime-resolved prose, unchanged). At install/
|
|
7
|
+
* update time the conditionals are resolved from the consumer's
|
|
8
|
+
* `baldart.config.yml`, so the installed agent contains ONLY the variant that
|
|
9
|
+
* matches this project (e.g. one `stack.database` block instead of four).
|
|
10
|
+
*
|
|
11
|
+
* Every flag derives from EXISTING config keys — this module introduces no new
|
|
12
|
+
* `baldart.config.yml` surface (schema-change propagation rule: N/A).
|
|
13
|
+
*
|
|
14
|
+
* Consumed by SymlinkUtils._mergeBulkDir (kind `agent`, MERGE_KINDS.slots) and
|
|
15
|
+
* by the doctor staleness probe. Pure functions except loadAgentConfig (fs read).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const yaml = require('js-yaml');
|
|
21
|
+
const { shortSha } = require('./overlay-merger');
|
|
22
|
+
|
|
23
|
+
/** Parse baldart.config.yml; {} when absent/malformed (fail-open: no flags → all conditionals drop). */
|
|
24
|
+
function loadAgentConfig(cwd) {
|
|
25
|
+
try {
|
|
26
|
+
return yaml.load(fs.readFileSync(path.join(cwd, 'baldart.config.yml'), 'utf8')) || {};
|
|
27
|
+
} catch (_) {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Derive the agent-slot flag set from the config. Keep this list in sync with
|
|
34
|
+
* the flags actually used by framework/.claude/agents/*.md sources — doctor's
|
|
35
|
+
* staleness probe hashes exactly this object (config_sha).
|
|
36
|
+
*/
|
|
37
|
+
function resolveAgentFlags(cfg) {
|
|
38
|
+
const features = (cfg && cfg.features) || {};
|
|
39
|
+
const stack = (cfg && cfg.stack) || {};
|
|
40
|
+
const db = String(stack.database || 'none').toLowerCase();
|
|
41
|
+
const relational = ['postgres', 'postgresql', 'supabase', 'mysql', 'sqlite'].includes(db);
|
|
42
|
+
return {
|
|
43
|
+
// features.* (existing keys only)
|
|
44
|
+
has_backlog: features.has_backlog === true,
|
|
45
|
+
has_adrs: features.has_adrs === true,
|
|
46
|
+
has_i18n: features.has_i18n === true,
|
|
47
|
+
has_design_system: features.has_design_system === true,
|
|
48
|
+
has_toolchain: features.has_toolchain === true,
|
|
49
|
+
has_lsp_layer: features.has_lsp_layer === true,
|
|
50
|
+
has_code_graph: features.has_code_graph === true,
|
|
51
|
+
has_wiki_overlay: features.has_wiki_overlay === true,
|
|
52
|
+
multi_tenant_theming: features.multi_tenant_theming === true,
|
|
53
|
+
// stack.database → exactly one db_* true (db_none when unset/none)
|
|
54
|
+
db_firestore: db === 'firestore',
|
|
55
|
+
db_relational: relational,
|
|
56
|
+
db_mongodb: db === 'mongodb',
|
|
57
|
+
db_dynamodb: db === 'dynamodb',
|
|
58
|
+
db_none: !relational && !['firestore', 'mongodb', 'dynamodb'].includes(db),
|
|
59
|
+
// stack.* accents
|
|
60
|
+
fw_nextjs: String(stack.framework || '').toLowerCase() === 'nextjs',
|
|
61
|
+
deploy_vercel: String(stack.deployment || '').toLowerCase() === 'vercel',
|
|
62
|
+
e2e_playwright: String((stack.testing && stack.testing.e2e) || '').toLowerCase() === 'playwright',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** True when the content carries slot syntax (flag conditionals only). */
|
|
67
|
+
function hasAgentSlots(content) {
|
|
68
|
+
return /\{\{#/.test(content);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Resolve `{{#flag}}…{{/flag}}` conditionals against the flag set. Returns
|
|
73
|
+
* { content, residual } where residual lists any `{{…}}` still present after
|
|
74
|
+
* the fill — always an authoring error (unknown flag / malformed block), never
|
|
75
|
+
* silently stripped, so the caller can surface it.
|
|
76
|
+
*
|
|
77
|
+
* Deliberately NOT root-primitives' fillTemplate: that engine also substitutes
|
|
78
|
+
* scalars/partials and blanks unknown `{{ x }}` tokens — for agents a leftover
|
|
79
|
+
* token must be a loud warning, not an empty string.
|
|
80
|
+
*/
|
|
81
|
+
function fillAgentSlots(content, flags) {
|
|
82
|
+
// Iterate to a fixpoint (bounded) so NESTED conditionals resolve too: a kept
|
|
83
|
+
// outer block re-exposes its inner `{{#flag}}` blocks on the next pass; a
|
|
84
|
+
// dropped outer block removes its inner blocks with it (correct semantics).
|
|
85
|
+
let out = content;
|
|
86
|
+
for (let i = 0; i < 5; i++) {
|
|
87
|
+
const next = out.replace(/\{\{#([\w.]+)\}\}\n?([\s\S]*?)\{\{\/\1\}\}\n?/g, (_, key, body) =>
|
|
88
|
+
(flags[key] ? body : ''));
|
|
89
|
+
if (next === out) break;
|
|
90
|
+
out = next;
|
|
91
|
+
}
|
|
92
|
+
const residual = [...new Set((out.match(/\{\{[^}]*\}\}/g) || []))];
|
|
93
|
+
// collapse blank runs left by dropped blocks; keep everything else verbatim
|
|
94
|
+
const cleaned = out.replace(/\n{3,}/g, '\n\n');
|
|
95
|
+
return { content: cleaned, residual };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Stable hash of the resolved flag set — stamped as config_sha in the marker. */
|
|
99
|
+
function computeAgentConfigSha(flags) {
|
|
100
|
+
return shortSha(JSON.stringify(flags));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = {
|
|
104
|
+
loadAgentConfig,
|
|
105
|
+
resolveAgentFlags,
|
|
106
|
+
hasAgentSlots,
|
|
107
|
+
fillAgentSlots,
|
|
108
|
+
computeAgentConfigSha,
|
|
109
|
+
};
|
|
@@ -133,9 +133,13 @@ function refreshOverlayVersionPin(overlayPath, kind, frameworkVersion, currentBa
|
|
|
133
133
|
return { changed: true, reason: 'refreshed' };
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
function buildMarker({ kind, name, baseSha, overlaySha }) {
|
|
136
|
+
function buildMarker({ kind, name, baseSha, overlaySha, configSha }) {
|
|
137
137
|
const baseShaField = baseSha ? ` base_sha=${baseSha}` : '';
|
|
138
|
-
|
|
138
|
+
// config_sha (v5.0.0, slot-generated agents) is a TRAILING field on purpose:
|
|
139
|
+
// readMarker and the edit-gate's GENERATED_MARKER_RE match a prefix ending at
|
|
140
|
+
// overlay_sha, so older readers tolerate it transparently.
|
|
141
|
+
const configShaField = configSha ? ` config_sha=${configSha}` : '';
|
|
142
|
+
return `${MARKER_PREFIX} kind=${kind} name=${name}${baseShaField} overlay_sha=${overlaySha}${configShaField}
|
|
139
143
|
DO NOT EDIT MANUALLY. Edit .baldart/overlays/${kind}s/${name}.md instead;
|
|
140
144
|
this file is regenerated by \`npx baldart update\`. -->`;
|
|
141
145
|
}
|
|
@@ -162,9 +166,9 @@ function readMarker(content) {
|
|
|
162
166
|
const block = content.slice(idx, idx + 4000);
|
|
163
167
|
const end = block.indexOf('-->');
|
|
164
168
|
if (end === -1) return null;
|
|
165
|
-
const m = block.slice(0, end).match(/kind=(\S+)\s+name=(\S+)(?:\s+base_version=(\S+))?(?:\s+base_sha=(\S+))?\s+overlay_sha=(\S+)
|
|
169
|
+
const m = block.slice(0, end).match(/kind=(\S+)\s+name=(\S+)(?:\s+base_version=(\S+))?(?:\s+base_sha=(\S+))?\s+overlay_sha=(\S+)(?:\s+config_sha=(\S+))?/);
|
|
166
170
|
if (!m) return null;
|
|
167
|
-
return { kind: m[1], name: m[2], baseVersion: m[3] || null, baseSha: m[4] || null, overlaySha: m[5] };
|
|
171
|
+
return { kind: m[1], name: m[2], baseVersion: m[3] || null, baseSha: m[4] || null, overlaySha: m[5], configSha: m[6] || null };
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
/**
|
|
@@ -234,7 +238,7 @@ function rebuildSections(sections) {
|
|
|
234
238
|
* @param {string} args.frameworkVersion - the installed framework VERSION
|
|
235
239
|
* @returns {{ generated: string, warnings: string[], baseVersionTargeted: string, overlayMode: string }}
|
|
236
240
|
*/
|
|
237
|
-
function mergeOverlay({ kind, name, baseContent, overlayContent, frameworkVersion }) {
|
|
241
|
+
function mergeOverlay({ kind, name, baseContent, overlayContent, frameworkVersion, configSha, baseShaOverride }) {
|
|
238
242
|
const warnings = [];
|
|
239
243
|
|
|
240
244
|
// Parse overlay frontmatter for metadata.
|
|
@@ -293,9 +297,14 @@ function mergeOverlay({ kind, name, baseContent, overlayContent, frameworkVersio
|
|
|
293
297
|
? '\n' + trailing.map((s) => `## ${s.heading}\n${s.body}`).join('\n').replace(/\n+$/, '\n')
|
|
294
298
|
: '';
|
|
295
299
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const
|
|
300
|
+
// An absent overlay (slot-only generation, v5.0.0) is stamped `none`, not
|
|
301
|
+
// sha-of-empty-string — truthful and stable for byte-identity comparisons.
|
|
302
|
+
const overlaySha = overlayContent ? shortSha(overlayContent) : 'none';
|
|
303
|
+
// baseShaOverride: slot-generated agents pin the RAW framework source here
|
|
304
|
+
// (upstream-drift signal), not the filled content (which changes with config
|
|
305
|
+
// — that drift is config_sha's job).
|
|
306
|
+
const baseSha = baseShaOverride || shortSha(baseContent);
|
|
307
|
+
const marker = buildMarker({ kind, name, baseSha, overlaySha, configSha });
|
|
299
308
|
|
|
300
309
|
const fmBlock = baseParsed.frontmatter
|
|
301
310
|
? `---\n${baseParsed.frontmatter}\n---\n`
|