baldart 3.19.0 → 3.21.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,315 @@
1
+ ---
2
+ name: baldart-update
3
+ description: "Guidance for updating BALDART framework to the latest version in a consumer repo. Use when the user says /baldart-update, 'aggiorna baldart', 'aggiorna framework', 'update del framework', 'pull baldart latest', or wants to bring the framework up-to-date. Replicates all 5 native CLI decision points (preview diff, working tree stash, hooks drift, schema config drift, BALDART-managed auto-commit) as chat-side confirmations before launching `npx baldart update --yes` via Bash. For non-update drift, defers to `npx baldart` (smart doctor)."
4
+ contamination_scan: skip
5
+ ---
6
+
7
+ <!--
8
+ DRIFT-CHECK
9
+ This block is read by scripts/check-update-skill-drift.js (triggered by
10
+ .github/workflows/check-update-skill-drift.yml on push tag v*.*.* and
11
+ pull_request). When any of the numbers below changes against the actual
12
+ sources, a warning is surfaced in the workflow run. The decision whether
13
+ this skill needs an update is left to a human reviewer.
14
+
15
+ update_js_prompts: 6
16
+ hook_registry_entries: 3
17
+ config_template_top_level_keys: features,git,identity,lsp,paths,stack,tools,version
18
+ last_verified: v3.21.2
19
+ -->
20
+
21
+
22
+ # /baldart-update — Update framework in this consumer
23
+
24
+ Guide the user through an update of the BALDART framework inside their
25
+ consumer repo, replicating in chat **every interactive decision point** the
26
+ CLI would normally ask in the terminal — so the CLI can be driven with
27
+ `--yes` while the user keeps full control of each choice.
28
+
29
+ ```
30
+ ╔══════════════════════════════════════════════════════════════════════╗
31
+ ║ BOOTSTRAP NOTICE ║
32
+ ║ ║
33
+ ║ IF THIS IS THE FIRST UPDATE AFTER A CONSUMER WAS ON A VERSION ║
34
+ ║ THAT PREDATES /baldart-update, THIS SKILL DOES NOT YET EXIST IN ║
35
+ ║ THE CONSUMER'S .claude/skills/. The user must run: ║
36
+ ║ ║
37
+ ║ npx baldart update ║
38
+ ║ ║
39
+ ║ directly in their terminal **once**, and the skill becomes ║
40
+ ║ available for every subsequent update. This skill is the pattern ║
41
+ ║ for follow-up updates, not the very first one. ║
42
+ ╚══════════════════════════════════════════════════════════════════════╝
43
+ ```
44
+
45
+ ## Execution model — IMPORTANT
46
+
47
+ This is an **agent-driven skill with full chat-side replication** of every
48
+ decision point the CLI would pose interactively. The flow is:
49
+
50
+ 1. Run *read-only* probes to detect the same conditions `update.js` would
51
+ detect (version drift, dirty working tree, hooks drift, schema config
52
+ drift, BALDART-managed files to auto-commit).
53
+ 2. For each detected condition, surface it in chat and ask the user the
54
+ exact same question the CLI would ask in a TTY.
55
+ 3. Once every prompt has been answered in chat, launch `npx baldart update
56
+ --yes` via `Bash` (with a long timeout — see Hard rules). The `--yes`
57
+ flag is safe here precisely because there is no native prompt left to
58
+ silence: every one of them has already been resolved above.
59
+ 4. Parse the CLI output, report the backup tag + new version, surface any
60
+ stash-pop conflicts that may have happened after the subtree pull.
61
+
62
+ This is the **intentional divergence** from `/baldart-push`, which is
63
+ pure-guidance (the user runs the CLI themselves). Here the user has asked
64
+ for an agent that drives the update, so the skill drives it — but it
65
+ replicates every confirmation so nothing is silently auto-accepted.
66
+
67
+ ## When to use
68
+
69
+ - The user invokes `/baldart-update`.
70
+ - The user says "aggiorna baldart", "aggiorna framework", "update del
71
+ framework", "pull baldart latest", "porta baldart all'ultima versione".
72
+ - The user wants to bring the framework payload in the consumer repo
73
+ in sync with upstream `main`.
74
+
75
+ ## When NOT to use
76
+
77
+ - The user wants to fix drift that is NOT an update (hooks missing, config
78
+ schema drift in isolation, broken symlinks). → `npx baldart` (smart
79
+ doctor) is the right tool — it picks the right action among ~12.
80
+ - The consumer has no `.framework/` directory yet. → `npx baldart add`.
81
+ - The user wants to contribute local framework improvements upstream.
82
+ → `/baldart-push`.
83
+ - The user is in the BALDART repo itself (i.e. `/Users/.../BALDART`),
84
+ not in a consumer. This skill is for consumers; this repo updates
85
+ via `git pull`.
86
+
87
+ ## Project Context
88
+
89
+ **Reads from `baldart.config.yml`:** none directly (the skill reads
90
+ `.framework/VERSION`, `.baldart/state.json`, and `.claude/settings.json`,
91
+ which are BALDART-owned and not gated by config keys).
92
+ **Gated by features:** none.
93
+ **Overlay:** loads `.baldart/overlays/baldart-update.md` if present.
94
+ Typical overlay content: extra pre-flight checks specific to the consumer
95
+ (e.g. "block update if a release branch is open"), or post-update
96
+ verification steps (e.g. "run `npm test` after symlink reconcile").
97
+ **On missing/empty keys:** not applicable — the skill operates on BALDART's
98
+ own state files. See [framework/agents/project-context.md](../../../agents/project-context.md)
99
+ for the protocol.
100
+
101
+ ## Hard rules
102
+
103
+ 1. **MUST NOT re-implement the logic of `update.js`.** Same rule as
104
+ `/baldart-push`: the CLI is the source of truth. The skill only
105
+ replicates prompts and parses output.
106
+ 2. **MUST replicate in chat all 5 decision points** before passing `--yes`:
107
+ preview, working-tree stash, hooks drift, schema config drift,
108
+ auto-commit of BALDART-managed files. Silencing one is dishonest — the
109
+ user thinks they confirmed everything, but they didn't see that prompt.
110
+ 3. **MUST pass `timeout: 600000` (10 minutes)** to the `Bash` call running
111
+ `npx baldart update --yes`. The default 120s is not enough for subtree
112
+ pull + symlink reconcile + post-update wizard + hooks register +
113
+ auto-commit on real-world repos.
114
+ 4. **MUST NOT pass `--auto-stash`** to the CLI: `--yes` already implies it.
115
+ Passing both is imprecise and signals a misread of the CLI.
116
+ 5. **MUST report the backup tag** generated by the CLI, so the user has a
117
+ one-line rollback path.
118
+ 6. **MUST NOT launch update if `.framework/` is missing** — defer to
119
+ `npx baldart add` and stop.
120
+ 7. **MUST NOT assume defaults when a pre-check fails.** If the skill cannot
121
+ read `.claude/settings.json` or `baldart.config.yml`, it asks the user
122
+ how to proceed or aborts. Never silence a decision point because a
123
+ probe failed.
124
+
125
+ ## The 5 decision points the skill replicates
126
+
127
+ | # | Native CLI prompt | Chat-side replication | Detect via |
128
+ |---|------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
129
+ | 1 | "Proceed with update?" | Show "Remote: N commit(s) ahead" + last N commit messages, ask "Procedo?" | `npx baldart version` (parse "Remote: …" + "Repository: <url>") → if N > 0, `git fetch <url> main && git log HEAD..FETCH_HEAD --oneline -- .framework/` |
130
+ | 2 | "Stash dirty working tree?" | List modified files, explain they will go into `baldart-pre-update-<timestamp>`, ask explicit confirmation | `git status --porcelain` in consumer root |
131
+ | 3 | Hooks drift prompt | Compare `.claude/settings.json` entries vs `HOOK_REGISTRY`; if drift, describe what backfill will change | `cat .claude/settings.json` + read `.framework/src/utils/hooks.js` for `HOOK_REGISTRY` |
132
+ | 4 | Schema config migration | Compare `baldart.config.yml` keys vs `.framework/framework/templates/baldart.config.template.yml`; if missing keys, list which + consequence | semantic diff key-by-key (NOT textual diff) |
133
+ | 5 | Auto-commit BALDART-managed | Classify `git status --porcelain` paths as BALDART-managed vs user-owned by path-prefix, show classification, confirm commit of the managed subset | path-prefix matching on `.framework/`, `.claude/agents/`, `.claude/skills/`, `.claude/commands/`, `AGENTS.md`, `agents/` |
134
+
135
+ > **IMPORTANT — do NOT use `git -C .framework fetch`.** `.framework/` is a git
136
+ > subtree, not a separate repo: git commands inside it fall back to the consumer's
137
+ > own repo and would fetch the wrong remote. The correct pattern (mirroring
138
+ > `update.js` + `src/utils/git.js`) is `git fetch <BALDART-repo-url> main` from
139
+ > the consumer root, then `git log HEAD..FETCH_HEAD -- .framework/` for the
140
+ > preview. The repo URL is exposed by `npx baldart version` in the
141
+ > `Repository:` line.
142
+
143
+ ## Workflow
144
+
145
+ ### Step 0 — Pre-flight (read-only)
146
+
147
+ ```bash
148
+ # One call gives both: installed version, behind count, and the
149
+ # BALDART repo URL. NO git operation inside .framework/ — that
150
+ # directory is a git subtree, not a separate repo.
151
+ npx baldart version
152
+ ```
153
+
154
+ Parse the output for:
155
+
156
+ - `Installed: vX.Y.Z` — current framework version in the consumer.
157
+ - `Remote: N commit(s) ahead — pull with …` — number of commits behind upstream. If this line is absent or shows 0, the consumer is up-to-date.
158
+ - `Repository: <url>` — the BALDART git URL (needed in Step 1).
159
+
160
+ If `Remote:` is absent / shows 0 → tell the user "Already up-to-date — version vX.Y.Z" and STOP. Do not launch the CLI.
161
+
162
+ If `.framework/` does not exist, `npx baldart version` will say "framework not installed" — STOP and instruct `npx baldart add`. Do not attempt to recover.
163
+
164
+ ### Step 1 — Replicate decision point #1 (preview)
165
+
166
+ ```bash
167
+ # Use the URL extracted from Step 0, not `git -C .framework`.
168
+ # This mirrors src/utils/git.js (fetch + diffWithRemote use FETCH_HEAD,
169
+ # never `git -C .framework`).
170
+ git fetch <BALDART-repo-url> main
171
+ git log HEAD..FETCH_HEAD --oneline -- .framework/
172
+ ```
173
+
174
+ Surface to the user:
175
+
176
+ - Current installed version (from Step 0).
177
+ - Number of commits behind (from Step 0).
178
+ - One-line summary of each incoming commit (from the `git log` above).
179
+
180
+ Ask: *"Procedo con l'aggiornamento?"*. If no → STOP.
181
+
182
+ ### Step 2 — Replicate decision point #2 (working tree)
183
+
184
+ ```bash
185
+ git status --porcelain
186
+ ```
187
+
188
+ If output is empty, proceed silently. If not, list the modified files in
189
+ chat, explain:
190
+
191
+ > *"Il working tree ha N file modificati. Il CLI li stasherà in un ref
192
+ > `baldart-pre-update-<timestamp>` prima del pull e li ripristinerà dopo.
193
+ > Posso procedere con lo stash automatico?"*
194
+
195
+ If the user refuses → STOP, instruct them to commit or stash manually
196
+ and re-run `/baldart-update`.
197
+
198
+ ### Step 3 — Replicate decision points #3 + #4 (hooks/schema drift)
199
+
200
+ ```bash
201
+ # Hooks drift detection
202
+ cat .claude/settings.json
203
+ # Compare entries against HOOK_REGISTRY in .framework/src/utils/hooks.js
204
+ # (the skill reads both and reports any expected entry missing
205
+ # or any installed entry with a stale config)
206
+
207
+ # Schema drift detection
208
+ # Compare top-level keys in baldart.config.yml against
209
+ # .framework/framework/templates/baldart.config.template.yml
210
+ ```
211
+
212
+ For each drift detected, describe what `update.js` would do:
213
+
214
+ - Hooks drift → "il CLI registrerà le hook mancanti in `settings.json`
215
+ (entry: `baldart-<hook-name>`). Procedo?"
216
+ - Schema drift → "il CLI segnalerà che mancano le chiavi `X.Y` da
217
+ `baldart.config.yml` e suggerirà di rilanciare `npx baldart configure`
218
+ dopo l'update. Procedo (l'update non bloccherà, ma è bene saperlo)?"
219
+
220
+ If a probe fails (e.g. `settings.json` malformed), ask the user how to
221
+ proceed — never silently assume "no drift".
222
+
223
+ ### Step 4 — Replicate decision point #5 (auto-commit classification)
224
+
225
+ ```bash
226
+ git status --porcelain
227
+ ```
228
+
229
+ Classify each path:
230
+
231
+ - **BALDART-managed**: starts with `.framework/`, `.claude/agents/`,
232
+ `.claude/skills/`, `.claude/commands/`, `AGENTS.md`, `agents/`.
233
+ - **User-owned**: everything else.
234
+
235
+ Show the user the classification:
236
+
237
+ > *"Dopo il pull, il CLI auto-committa SOLO i file BALDART-managed che
238
+ > risultano modificati dal reconcile (es. symlink ricreati, agent/command
239
+ > generati). I tuoi file user-owned non vengono toccati. Procedo?"*
240
+
241
+ If no managed files would be modified, skip the prompt silently.
242
+
243
+ ### Step 5 — Run the CLI
244
+
245
+ ```bash
246
+ npx baldart update --yes
247
+ ```
248
+
249
+ **Use the Bash tool with `timeout: 600000`** (10 minutes). Capture stdout
250
+ and stderr — both are needed for Step 6.
251
+
252
+ Do NOT pass `--auto-stash` (redundant with `--yes`).
253
+
254
+ ### Step 6 — Report
255
+
256
+ Extract the post-update facts and surface them:
257
+
258
+ ```bash
259
+ # Backup tag (most recent baldart-pre-update-* tag)
260
+ git tag -l 'baldart-pre-update-*' --sort=-creatordate | head -1
261
+
262
+ # New installed version
263
+ cat .framework/VERSION
264
+
265
+ # Stash-pop conflicts (if any)
266
+ # Grep stdout/stderr for: CONFLICT, "stash pop failed", "unmerged paths"
267
+ # If matches found, list the conflicted paths:
268
+ git status --porcelain | grep '^UU'
269
+ ```
270
+
271
+ Report to the user, in order:
272
+
273
+ 1. **Version**: `vX.Y.Z → vA.B.C` (delta).
274
+ 2. **Backup tag**: the `baldart-pre-update-<timestamp>` ref, with the
275
+ rollback command: `git reset --hard <tag>` (only if needed).
276
+ 3. **Hooks reconciled**: any hook backfilled.
277
+ 4. **Schema notes**: any missing key still flagged → suggest
278
+ `npx baldart configure`.
279
+ 5. **Stash-pop status**: clean / conflicts present (with file list).
280
+ 6. **Suggested next step**: if schema drift residual or other non-update
281
+ doctor checks fired, suggest `npx baldart` (smart doctor).
282
+
283
+ ## Failure modes
284
+
285
+ | Symptom | Cause | What you do |
286
+ |--------------------------------------------------------|----------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|
287
+ | `git -C .framework fetch` errors out | Offline / auth issue / repo URL changed | STOP. Surface the error verbatim. Ask the user to retry when network is back. Do not launch update. |
288
+ | Working tree dirty + user refuses stash | Authoring decision | STOP cleanly. Instruct: commit or stash manually, then re-run `/baldart-update`. |
289
+ | `update.js` exits non-zero (subtree pull conflict) | Upstream and local both modified the same file | Surface the CLI error verbatim. Provide the backup tag + `git reset --hard <tag>` rollback command. Do NOT attempt automatic conflict resolution. |
290
+ | Stash-pop conflicts after the subtree pull | User-modified file collides with framework change | Detected by grep on stdout/stderr (`CONFLICT`, `stash pop failed`, `unmerged paths`). List `git status --porcelain | grep '^UU'` paths. Instruct manual resolve. NEVER auto-resolve with `--hard` or `--no-verify`. |
291
+ | `.framework/` missing | Consumer never installed BALDART | STOP. Instruct `npx baldart add`. Do not improvise. |
292
+ | Bash 10-minute timeout exceeded | Very slow network / disk / huge repo | Surface the timeout. Instruct the user to run `npx baldart update` manually in their terminal to see live progress. |
293
+ | Pre-check probe fails (e.g. `settings.json` malformed) | Consumer state inconsistent | STOP. Surface the probe failure. Ask the user whether to proceed without that pre-check, or to fix manually first. Never silently assume "no drift". |
294
+
295
+ ## Why this skill exists
296
+
297
+ The smart doctor (`npx baldart`, no args) already proposes an update
298
+ when it detects remote ahead — but it does so as **one of about twelve**
299
+ possible actions (install, configure, migrate, repair symlinks, register
300
+ hooks, reconcile state ledger, …). When the user knows they want *only*
301
+ to update, going through the doctor is more friction than signal.
302
+
303
+ This skill is the direct path for that case, **and** it does what the
304
+ terminal CLI does — surface every decision point as a confirmation —
305
+ inside the chat instead of in the user's shell. The user keeps the
306
+ control they would have in a TTY, but they get it conversationally.
307
+
308
+ `/baldart-push` and `/baldart-update` together form the conversational
309
+ spine of BALDART maintenance:
310
+
311
+ - `/baldart-update` — pull from upstream.
312
+ - `/baldart-push` — push to upstream.
313
+
314
+ Both wrap the CLI without re-implementing it; both keep the user in the
315
+ loop. The CLI remains the source of truth.
@@ -94,6 +94,7 @@ Conflict steps (must follow in order):
94
94
 
95
95
  - MUST treat `AGENTS.md` as authoritative for agent rules.
96
96
  - MUST invoke `codebase-architect` agent (via Task tool) whenever you need to understand codebase structure, existing patterns, or code architecture before planning or implementing changes; do not proceed with planning or implementation without first understanding the existing system through codebase-architect.
97
+ - MUST NOT silently fall back when an `Agent` (a.k.a. `Task`) tool call returns empty (`0 tool uses · Done`), times out, or fails with `permissionDecision: deny`. This pattern indicates a Claude Code sub-agent failure (see anthropics/claude-code#56869) or a discovery miss / blocked agent (see #20931 and BALDART's own `agent-discovery-gate` PreToolUse hook), NOT a signal to substitute another agent. STOP, report to the user verbatim: "Agent `<name>` non discoverato o fallito in questa sessione — riavvia Claude Code e verifica con `npx baldart doctor`", and wait for direction. Never auto-route to `feature-dev:*`, `general-purpose`, or any other marketplace/built-in agent as a replacement. **This rule is the ONLY defense against bug #56869 (which BALDART cannot patch from outside Claude Code); deviating from it reintroduces the exact failure mode the rule exists to prevent.**
97
98
  - MUST NOT work on files/components already claimed by another agent; multiple agents allowed if working on independent areas.
98
99
  - MUST perform a mandatory clarity analysis before fixing any issue labeled `bug`; confirm that the issue description, proposed correct behavior, and edge cases are unambiguous, document any resolved doubts, and only start fix work once every zone of uncertainty is covered.
99
100
  - MUST mark missing info as UNKNOWN and ask the user; if blocked, set `BLOCKED` with a blocker.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "3.19.0",
3
+ "version": "3.21.2",
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"
@@ -166,19 +166,24 @@ async function add(repo, options) {
166
166
  UI.success('Git aliases configured');
167
167
  }
168
168
 
169
- // Framework-edit-gate hook (since v3.3.0)
170
- // Auto-register the PreToolUse hook that prevents project-specific
171
- // tokens from being written into .framework/ skills accidentally.
169
+ // BALDART hooks (multi-hook registry since v3.18.0)
170
+ // Auto-register every hook in src/utils/hooks.js HOOK_REGISTRY into
171
+ // .claude/settings.json. Drift on an existing entry triggers an
172
+ // interactive prompt (Keep / Replace / Show diff).
172
173
  try {
173
- const res = Hooks.register();
174
- if (res.status === 'created') {
175
- UI.success('Registered framework-edit-gate hook in .claude/settings.json');
176
- } else if (res.status === 'updated') {
177
- UI.success('Added framework-edit-gate hook to existing .claude/settings.json');
178
- } else if (res.status === 'already') {
179
- UI.info('framework-edit-gate hook already registered.');
180
- } else if (res.status === 'malformed') {
174
+ const res = await Hooks.registerAll(process.cwd(), {
175
+ onDrift: Hooks.createDriftPrompt(UI),
176
+ });
177
+ if (res.malformed) {
181
178
  UI.warning('.claude/settings.json is malformed; skipped hook registration. Run `npx baldart doctor` after fixing it.');
179
+ } else {
180
+ for (const r of res.results) {
181
+ if (r.status === 'created') UI.success(`Registered hook \`${r.id}\` in .claude/settings.json`);
182
+ else if (r.status === 'legacy-migrated') UI.success(`Migrated legacy hook to \`${r.id}\` (previous: ${r.previousCommand})`);
183
+ else if (r.status === 'drift-replaced') UI.success(`Replaced drifted hook \`${r.id}\` (was: ${r.previousCommand})`);
184
+ else if (r.status === 'drift-kept') UI.info(`Kept user-customized hook \`${r.id}\` as-is.`);
185
+ else if (r.status === 'already') UI.info(`Hook \`${r.id}\` already registered.`);
186
+ }
182
187
  }
183
188
  } catch (err) {
184
189
  UI.warning(`Hook registration skipped: ${err.message}`);
@@ -212,8 +212,47 @@ async function detectState(cwd, opts = {}) {
212
212
  state.overlays = countOverlays(cwd);
213
213
  state.overlaysDrifted = overlayDrift(cwd, state.frameworkVersion);
214
214
 
215
- state.editGateRegistered = false;
216
- try { state.editGateRegistered = Hooks.isRegistered(cwd); } catch (_) {}
215
+ state.hooksStatus = { malformed: false, hooks: [] };
216
+ state.hooksMissing = [];
217
+ state.hooksDrifted = [];
218
+ state.hooksLegacy = [];
219
+ try {
220
+ const status = Hooks.getStatus(cwd);
221
+ state.hooksStatus = status;
222
+ if (!status.malformed) {
223
+ state.hooksMissing = status.hooks.filter((h) => !h.registered).map((h) => h.id);
224
+ state.hooksDrifted = status.hooks.filter((h) => h.registered && h.drift && !h.legacy).map((h) => h.id);
225
+ state.hooksLegacy = status.hooks.filter((h) => h.legacy).map((h) => h.id);
226
+ }
227
+ } catch (_) {}
228
+
229
+ // ---- Agent symlink integrity (since v3.20.0) -----------------------
230
+ // Source of truth for the BALDART agent list is the framework filesystem
231
+ // directory itself (`.framework/framework/.claude/agents/*.md`). For each
232
+ // name, the consumer must have a working `.claude/agents/<name>.md`
233
+ // (symlink to the framework copy, possibly merged with an overlay).
234
+ // A missing or broken symlink would otherwise be detected at runtime by
235
+ // `agent-discovery-gate.js`, blocking Agent calls. This probe lets the
236
+ // doctor heal it offline before the user hits the gate.
237
+ state.agentSymlinksBroken = [];
238
+ try {
239
+ if (state.frameworkPresent) {
240
+ const frameworkAgentsDir = path.join(cwd, '.framework', 'framework', '.claude', 'agents');
241
+ const consumerAgentsDir = path.join(cwd, '.claude', 'agents');
242
+ if (fs.existsSync(frameworkAgentsDir)) {
243
+ const baldartAgents = fs.readdirSync(frameworkAgentsDir)
244
+ .filter((f) => f.endsWith('.md') && f !== 'REGISTRY.md')
245
+ .map((f) => f.replace(/\.md$/, ''));
246
+ for (const name of baldartAgents) {
247
+ try {
248
+ fs.statSync(path.join(consumerAgentsDir, `${name}.md`));
249
+ } catch (_) {
250
+ state.agentSymlinksBroken.push(name);
251
+ }
252
+ }
253
+ }
254
+ }
255
+ } catch (_) { /* never block doctor on probe */ }
217
256
 
218
257
  // `.framework/` is a git subtree — gitignore matching it breaks every
219
258
  // future `git add` under that path (the failure mode that hit v3.13.0
@@ -410,15 +449,75 @@ function planActions(state) {
410
449
  // Non-blocking — keep looking for other actions to run in the same pass.
411
450
  }
412
451
 
413
- if (state.editGateRegistered === false) {
452
+ // BALDART hooks (multi-hook registry since v3.18.0).
453
+ // Three buckets, three actions: missing → propose; legacy → propose
454
+ // (silent migration to current id); drifted → require interactive prompt.
455
+ if ((state.hooksMissing && state.hooksMissing.length > 0)
456
+ || (state.hooksLegacy && state.hooksLegacy.length > 0)) {
457
+ const ids = [...(state.hooksMissing || []), ...(state.hooksLegacy || [])];
414
458
  actions.push({
415
- key: 'register-edit-gate',
416
- label: 'Register framework-edit-gate hook (.claude/settings.json)',
417
- why: 'Prevents project-specific tokens (Neo-Brutalism, merchant, …) from being written to .framework/ accidentally. Standard in v3.3.0+; missing on installs predating that version.',
459
+ key: 'register-hooks',
460
+ label: `Register/migrate BALDART hook(s) in .claude/settings.json (${ids.join(', ')})`,
461
+ why: 'BALDART hooks are missing or registered under a pre-v3.18 layout. They run on PreToolUse/Stop/SessionStart to gate edits and feed the auto-learning loop. Re-registration is idempotent and never overwrites user-customized commands without asking.',
418
462
  autoOk: true,
419
463
  run: async () => {
420
- const res = require('../utils/hooks').register();
421
- UI.success(`Hook registered: ${res.status} (${res.path})`);
464
+ const Hooks = require('../utils/hooks');
465
+ const res = await Hooks.registerAll(process.cwd(), {
466
+ onDrift: Hooks.createDriftPrompt(UI, { autoYes: state.autoMode }),
467
+ });
468
+ if (res.malformed) {
469
+ UI.warning('.claude/settings.json is malformed; fix it manually then re-run `npx baldart doctor`.');
470
+ return;
471
+ }
472
+ for (const r of res.results) {
473
+ if (r.status === 'created') UI.success(`Registered hook \`${r.id}\``);
474
+ else if (r.status === 'legacy-migrated') UI.success(`Migrated legacy hook to \`${r.id}\` (previous: ${r.previousCommand})`);
475
+ }
476
+ },
477
+ });
478
+ }
479
+ if (state.hooksDrifted && state.hooksDrifted.length > 0) {
480
+ actions.push({
481
+ key: 'reconcile-hooks-drift',
482
+ label: `Reconcile drifted BALDART hook(s) (${state.hooksDrifted.join(', ')})`,
483
+ why: 'These hooks are registered but their command differs from the BALDART default. This is typically a sign of a manual customization or a stale framework version on disk. Reconciliation prompts you per hook (Keep / Replace / Show diff).',
484
+ autoOk: false, // drift reconciliation is the kind of decision --auto must NOT make
485
+ run: async () => {
486
+ const Hooks = require('../utils/hooks');
487
+ const res = await Hooks.registerAll(process.cwd(), {
488
+ onDrift: Hooks.createDriftPrompt(UI), // always interactive here
489
+ });
490
+ for (const r of res.results) {
491
+ if (r.status === 'drift-replaced') UI.success(`Replaced \`${r.id}\` (was: ${r.previousCommand})`);
492
+ else if (r.status === 'drift-kept') UI.info(`Kept \`${r.id}\` as-is.`);
493
+ }
494
+ },
495
+ });
496
+ }
497
+
498
+ // Agent symlink integrity (since v3.20.0). Re-runs the agent merge loop to
499
+ // recreate any missing per-agent symlink. Calls `SymlinkUtils.mergeAgents`
500
+ // directly rather than dispatching to `update` — we want only the symlink
501
+ // pass, not a full update (which would also pull, merge overlays, register
502
+ // hooks, and verify config drift).
503
+ if (state.agentSymlinksBroken && state.agentSymlinksBroken.length > 0) {
504
+ actions.push({
505
+ key: 'repair-agent-symlinks',
506
+ label: `Repair ${state.agentSymlinksBroken.length} broken agent symlink(s)`,
507
+ why: `BALDART expects these agents to be reachable via .claude/agents/<name>.md but the symlinks are missing or broken: ${state.agentSymlinksBroken.join(', ')}. Without these, the agent-discovery-gate PreToolUse hook will deny Agent calls targeting these names.`,
508
+ autoOk: true,
509
+ run: async () => {
510
+ const SymlinkUtils = require('../utils/symlinks');
511
+ // Re-read the config here (not stashed on state) to pick up
512
+ // `tools.enabled`. Falls back to ['claude'] if absent/malformed.
513
+ const cfg = loadConfig(process.cwd());
514
+ const enabledTools = (cfg && !cfg.__malformed
515
+ && cfg.tools && Array.isArray(cfg.tools.enabled))
516
+ ? cfg.tools.enabled
517
+ : ['claude'];
518
+ const symlinks = new SymlinkUtils();
519
+ symlinks.mergeAgents({ tools: enabledTools });
520
+ UI.success(`Re-merged agent symlinks for tools: ${enabledTools.join(', ')}`);
422
521
  },
423
522
  });
424
523
  }
@@ -616,11 +715,36 @@ function renderDiagnostic(state) {
616
715
  console.log(` • ${o.name} targets v${o.target}, installed v${o.installed}`));
617
716
  }
618
717
 
619
- console.log(statusLine(
620
- 'Edit gate',
621
- state.editGateRegistered ? 'registered' : 'NOT registered (.claude/settings.json)',
622
- state.editGateRegistered ? 'ok' : 'warn'
623
- ));
718
+ {
719
+ const total = state.hooksStatus && state.hooksStatus.hooks ? state.hooksStatus.hooks.length : 0;
720
+ const missing = state.hooksMissing ? state.hooksMissing.length : 0;
721
+ const drifted = state.hooksDrifted ? state.hooksDrifted.length : 0;
722
+ const legacy = state.hooksLegacy ? state.hooksLegacy.length : 0;
723
+ const registered = total - missing;
724
+ let summary;
725
+ let status;
726
+ if (state.hooksStatus && state.hooksStatus.malformed) {
727
+ summary = '.claude/settings.json is malformed';
728
+ status = 'warn';
729
+ } else if (missing === 0 && drifted === 0 && legacy === 0) {
730
+ summary = `${registered}/${total} registered`;
731
+ status = 'ok';
732
+ } else {
733
+ const parts = [`${registered}/${total} registered`];
734
+ if (missing) parts.push(`${missing} missing`);
735
+ if (drifted) parts.push(`${drifted} drifted`);
736
+ if (legacy) parts.push(`${legacy} pre-v3.18`);
737
+ summary = parts.join(', ');
738
+ status = 'warn';
739
+ }
740
+ console.log(statusLine('BALDART hooks', summary, status));
741
+ if (state.hooksMissing && state.hooksMissing.length)
742
+ console.log(` • missing: ${state.hooksMissing.join(', ')}`);
743
+ if (state.hooksDrifted && state.hooksDrifted.length)
744
+ console.log(` • drifted: ${state.hooksDrifted.join(', ')}`);
745
+ if (state.hooksLegacy && state.hooksLegacy.length)
746
+ console.log(` • pre-v3.18: ${state.hooksLegacy.join(', ')}`);
747
+ }
624
748
 
625
749
  if (state.frameworkIgnored && state.frameworkIgnored.ignored) {
626
750
  console.log(statusLine(
@@ -663,6 +787,7 @@ async function doctor(opts = {}) {
663
787
  const offline = !!opts.offline;
664
788
 
665
789
  const state = await detectState(cwd, { offline });
790
+ state.autoMode = auto;
666
791
  renderDiagnostic(state);
667
792
 
668
793
  const actions = planActions(state);
@@ -400,13 +400,25 @@ async function update(options = {}) {
400
400
  UI.warning(`Routines wizard skipped: ${err.message}`);
401
401
  }
402
402
 
403
- // Framework-edit-gate hook (since v3.3.0) — backfill for consumers
404
- // installed before v3.3.0 didn't have this hook. We register it on every
405
- // update (idempotent no-op if already present).
403
+ // BALDART hooks (multi-hook registry since v3.18.0) — backfill for
404
+ // consumers that predate any of the hooks shipped in the current
405
+ // framework version. Idempotent: no-op when every hook is already in
406
+ // place. Drift on an existing entry triggers an interactive prompt
407
+ // (Keep / Replace / Show diff); on --auto / -y the default is 'keep'.
406
408
  try {
407
- const res = Hooks.register();
408
- if (res.status === 'created' || res.status === 'updated') {
409
- UI.success('Registered framework-edit-gate hook in .claude/settings.json (v3.3.0+ default).');
409
+ const res = await Hooks.registerAll(process.cwd(), {
410
+ onDrift: Hooks.createDriftPrompt(UI, { autoYes }),
411
+ });
412
+ if (res.malformed) {
413
+ UI.warning('.claude/settings.json is malformed; skipped hook registration. Run `npx baldart doctor` after fixing it.');
414
+ } else {
415
+ for (const r of res.results) {
416
+ if (r.status === 'created') UI.success(`Registered hook \`${r.id}\` in .claude/settings.json (v3.18.0+ default).`);
417
+ else if (r.status === 'legacy-migrated') UI.success(`Migrated legacy hook to \`${r.id}\` (previous: ${r.previousCommand})`);
418
+ else if (r.status === 'drift-replaced') UI.success(`Replaced drifted hook \`${r.id}\` (was: ${r.previousCommand})`);
419
+ else if (r.status === 'drift-kept') UI.info(`Kept user-customized hook \`${r.id}\` as-is.`);
420
+ // 'already' is silent on update to avoid noise
421
+ }
410
422
  }
411
423
  } catch (err) {
412
424
  UI.warning(`Hook registration skipped: ${err.message}`);