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.
Files changed (38) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/README.md +1 -0
  3. package/VERSION +1 -1
  4. package/framework/.claude/agents/CHANGELOG.md +82 -0
  5. package/framework/.claude/agents/REGISTRY.md +3 -1
  6. package/framework/.claude/agents/code-reviewer.md +84 -400
  7. package/framework/.claude/agents/codebase-architect.md +71 -356
  8. package/framework/.claude/agents/coder.md +131 -291
  9. package/framework/.claude/agents/doc-reviewer.md +126 -450
  10. package/framework/.claude/agents/plan-auditor.md +137 -436
  11. package/framework/.claude/agents/qa-sentinel.md +59 -313
  12. package/framework/.claude/agents/skill-improver.md +60 -4
  13. package/framework/.claude/agents/ui-expert.md +104 -591
  14. package/framework/.claude/hooks/agent-discovery-gate.js +2 -2
  15. package/framework/.claude/hooks/agent-discovery-info.sh +1 -0
  16. package/framework/.claude/skills/new/CHANGELOG.md +11 -0
  17. package/framework/.claude/skills/new/SKILL.md +1 -1
  18. package/framework/.claude/skills/new/references/codex-gate.md +25 -11
  19. package/framework/.claude/skills/new/references/implement.md +43 -3
  20. package/framework/.claude/skills/new/references/review-cycle.md +16 -4
  21. package/framework/.claude/skills/new/scripts/build-review-bundle.mjs +104 -0
  22. package/framework/.claude/skills/new/scripts/doc-invariants.mjs +163 -0
  23. package/framework/.claude/skills/new2/CHANGELOG.md +4 -0
  24. package/framework/.claude/skills/new2/SKILL.md +5 -1
  25. package/framework/.claude/workflows/new2-resolve.js +10 -3
  26. package/framework/.claude/workflows/new2.js +51 -3
  27. package/framework/agents/agent-operating-protocol.md +152 -0
  28. package/framework/agents/doc-audit-protocol.md +227 -0
  29. package/framework/agents/index.md +4 -0
  30. package/framework/agents/review-protocol.md +207 -0
  31. package/framework/routines/finding-mine.routine.yml +56 -0
  32. package/framework/routines/index.yml +11 -0
  33. package/package.json +1 -1
  34. package/src/commands/configure.js +15 -0
  35. package/src/commands/doctor.js +69 -2
  36. package/src/utils/agent-slots.js +109 -0
  37. package/src/utils/overlay-merger.js +17 -8
  38. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "4.99.0",
3
+ "version": "5.0.0",
4
4
  "description": "Claude Agent Framework - Reusable framework for coordinating AI agents and humans in software projects",
5
5
  "bin": {
6
6
  "baldart": "./bin/baldart.js"
@@ -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)) {
@@ -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
- return `${MARKER_PREFIX} kind=${kind} name=${name}${baseShaField} overlay_sha=${overlaySha}
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
- const overlaySha = shortSha(overlayContent);
297
- const baseSha = shortSha(baseContent);
298
- const marker = buildMarker({ kind, name, baseSha, overlaySha });
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`