baldart 3.12.0 → 3.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,74 @@ All notable changes to BALDART will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.14.0] - 2026-05-23
9
+
10
+ BALDART closes the **LSP install completeness gap** AND adds a long-overdue **`.framework/` gitignore self-heal** so install / update / push never trip over a stale or pre-existing ignore rule.
11
+
12
+ ### LSP install completeness
13
+
14
+ Since v3.10.0, opting into `features.has_lsp_layer: true` installed the language-server binary (typescript-language-server, pyright, gopls, rust-analyzer, ruby-lsp) into the consumer repo — but the matching native Claude Code plugin (`<lang>-lsp@claude-plugins-official`) was left for the user to install manually via `/plugin install`. Almost nobody did, so Claude itself never invoked the LSP and code-exploration silently fell back to Grep. The "opt-in to LSP" UX was effectively broken: users thought they had it, agents behaved as if they didn't. v3.14.0 wires the two layers together — when the user opts into LSP, BALDART now installs the binary AND enables the native plugin in the same `configure` step, idempotently and gated on `claude` being in `tools.enabled`.
15
+
16
+ ### Added
17
+
18
+ - **`claudePluginId()` returns real plugin ids** in every LSP adapter (`src/utils/lsp-adapters/{typescript,python,go,rust,ruby}.js`). Previously a `null` placeholder; now resolves to `typescript-lsp@claude-plugins-official`, `pyright-lsp@claude-plugins-official`, `gopls-lsp@claude-plugins-official`, `rust-analyzer-lsp@claude-plugins-official`, `ruby-lsp@claude-plugins-official` respectively. All plugins live in the Anthropic-published [`anthropics/claude-plugins-official`](https://github.com/anthropics/claude-plugins-official) marketplace.
19
+ - **`LspInstaller.installClaudePlugins(serverNames, opts)`** in `src/utils/lsp-installer.js` — high-level installer that (1) verifies the `claude` CLI is on PATH, (2) idempotently registers the `claude-plugins-official` marketplace via `claude plugin marketplace add anthropics/claude-plugins-official --scope user` (no-op when already registered — list is parsed first), (3) reads `claude plugin list --json` to skip plugins already installed at any scope, (4) runs `claude plugin install <id> --scope user` per missing plugin. Defaults to `--scope user` so a single install is reused across every project on the machine (LSP plugins are machine-wide useful — project scope would create N copies). Always returns `{ installed, skipped, manual, marketplace }`; never throws.
20
+ - **`LspInstaller.ensureClaudeMarketplace()`, `isClaudeCliAvailable()`, `listInstalledClaudePlugins()`, `verifyClaudePlugins(serverNames)`** — supporting primitives, all idempotent and degrade-open (parsing failure returns empty set → next install is attempted rather than incorrectly skipped).
21
+ - **`baldart configure` companion-plugin step** — after installing the language servers, the configure flow now calls `installClaudePlugins(merged.lsp.installed_servers)`. Surfaces clear messaging for each branch: success (✓ Installed plugin ids), idempotent skip ("Already installed"), Claude CLI missing (manual install commands printed verbatim per plugin), or `claude` opted out of `tools.enabled` (silent skip — Codex-only users don't want Claude marketplace mutation).
22
+ - **`baldart doctor` companion-plugin diagnostic** — `detectState` now calls `verifyClaudePlugins` when LSP is enabled and the `claude` CLI is available, populating `state.lspMissingClaudePlugins`. The `LSP layer` row in the diagnostic table reports `N server(s) verified, M Claude plugin(s) missing (…)` when the layer is half-installed. Severity `warn`.
23
+ - **`lsp-install-claude-plugins` action** in the doctor planner — `autoOk: true` (user-scope plugin install is idempotent and fully reversible via `claude plugin uninstall`). Backfills missing plugins for installs predating v3.14.0 and recovers from manual plugin uninstalls. Pairs with other actions; never short-circuits.
24
+
25
+ ### Changed
26
+
27
+ - **LSP opt-in is now end-to-end.** Pre-v3.14.0: `features.has_lsp_layer: true` installed the binary but the integration was dormant until the user manually enabled the plugin. Post-v3.14.0: opting in installs both layers in the same step. The framework's "always-ask, never-assume" contract still applies — when `claude` is not in `tools.enabled` OR the `claude` CLI is missing, the install gracefully degrades (no error, clear hint, deferred until the prerequisites are in place). `baldart doctor` then surfaces the half-install state on every subsequent invocation.
28
+
29
+ ### Notes
30
+
31
+ - **No new `baldart.config.yml` keys.** The plugin install reuses `features.has_lsp_layer`, `lsp.installed_servers`, and `tools.enabled` already in place — so the [schema-change propagation rule](../.claude/projects/-Users-antoniobaldassarre-BALDART/memory/feedback_schema_change_propagation.md) does not require a new template / configure prompt / update detector for this release. Scope is hard-coded to `user` (machine-wide LSP plugins) rather than exposed as `lsp.claude_plugin_scope` — the decision is opinionated and consistent with how the official marketplace tracks its own installs. If a future release needs per-project plugin scope, *that* would trigger the propagation rule (template + prompt + update detector + this changelog).
32
+ - **Marketplace and plugins live at `--scope user`, not project.** Three reasons: (1) LSP plugins are machine-wide useful — typescript-lsp installed once at user scope is reused by every TS project on the machine, not duplicated N times; (2) marketplace install at user scope means subsequent `baldart configure` runs in sibling projects skip the marketplace add entirely (idempotent); (3) project-scope plugins live under `.claude/` in the consumer repo, which would either need committing (noisy diffs) or `.gitignore` adjustments (extra protocol burden). User scope avoids all three.
33
+ - **Idempotency is checked twice.** Marketplace add reads `claude plugin marketplace list --json` first; plugin install reads `claude plugin list --json` first. Re-running `baldart configure` (or `baldart doctor` with the action accepted) is a no-op when everything is in place — no spurious install commands, no error noise.
34
+ - **Gate on `claude` in `tools.enabled`.** When the consumer has `tools.enabled: [codex]` (Codex-only), the plugin install is a silent no-op — Codex has no equivalent plugin model and Claude marketplace mutation would be off-target. Doctor diagnostics for missing plugins are similarly gated.
35
+ - **No framework payload changes.** All work is inside `src/` — adapters, installer, configure command, doctor command. No framework agent/skill/command/protocol module touched, so existing consumers see the new behavior the next time they run `baldart configure` or `baldart doctor` after upgrading the CLI (`npm i -g baldart@latest`).
36
+
37
+ ### `.framework/` gitignore self-heal
38
+
39
+ `.framework/` is a git subtree — it MUST be tracked. If anything in the gitignore chain (project `.gitignore`, `.git/info/exclude`, global `core.excludesFile`) matches `.framework`, every subsequent `git add` on files under that directory fails with `paths are ignored by one of your .gitignore files` — and `git subtree pull` itself errors out. Pre-v3.14.0 we relied on the user noticing and removing the rule manually; v3.14.0 detects and self-heals.
40
+
41
+ ### Added
42
+
43
+ - **`GitUtils.checkFrameworkIgnored()` and `ensureFrameworkNotIgnored()`** in `src/utils/git.js` — `check-ignore -v --no-index .framework` to detect the offending source / line / pattern, then idempotently strip standalone `.framework` rules from the project `.gitignore` and append a one-time `!.framework` + `!.framework/**` negation block marked with `# BALDART: .framework/ is a git subtree — never ignore (auto-managed)`. Re-checks after writing — if the rule lives in a location we cannot rewrite from here (parent-dir `.gitignore` outside the repo, non-standard `core.excludesFile`), surfaces the leftover state via `{ stillIgnored: true, recheck }` so the caller can hard-fail with actionable output.
44
+ - **Self-heal call in `baldart add`** (before `git subtree add`), **`baldart update`** (before `git subtree pull`), **`baldart push`** (before any `git add`). All three flows degrade gracefully — if `check-ignore` itself fails, the heal step prints a warning and continues; if the rule persists after heal, the command exits 1 with the resolution path printed.
45
+
46
+ ### Notes (gitignore)
47
+
48
+ - **Idempotent across re-runs.** The negation marker is checked before appending; the strip step is a `filter` over current lines. Running `add` / `update` / `push` repeatedly on a healthy repo is a no-op.
49
+ - **No false positives on .framework-related paths.** The strip filter matches only the six exact forms (`.framework`, `.framework/`, `/.framework`, `/.framework/`, `.framework/*`, `/.framework/*`) — `.frameworks/` or `framework/something.bak` patterns are preserved untouched.
50
+
51
+ ## [3.13.0] - 2026-05-23
52
+
53
+ BALDART closes **two drift visibility gaps in one release**. (1) The CLI binary (installed globally via `npm i -g baldart`) and the framework payload (`.framework/` inside each consumer repo, updated by `baldart update`) drifted silently — users on an old CLI never learned that new flags, new doctor checks, or new adapters had shipped. (2) The framework payload on disk and the `.baldart/state.json` ledger could drift silently when the ledger write failed (the catch in `update.js` was silent until now), when an update flow exited mid-way, or when a manual commit excluded `.baldart/state.json` — `baldart doctor` had no way to detect or fix this. Both gaps are now closed: a **non-invasive CLI self-update notifier** for the first, and **ledger drift detection + self-heal** in the doctor for the second. The unifying theme is "no silent drift" — every layer that can fall out of sync now reports it and offers a one-step fix.
54
+
55
+ ### Added
56
+
57
+ - **`src/utils/update-notifier.js`** — best-effort CLI self-update check. HTTPS GET to `https://registry.npmjs.org/baldart/latest` with a 1.5s timeout, cached at `~/.baldart/cli-update-check.json` (TTL 24h). Banner is printed from the cache on subsequent invocations (the very first run shows nothing — that is intentional; the check never blocks the command). The background refresh fire-and-forget: its timer is `unref()`'d and the underlying socket dies with the process, so the check can never hold the event loop open. Suppression: `BALDART_NO_UPDATE_CHECK=1`, `CI=true`, `NODE_ENV=test`, non-TTY stdout, or `--offline` on `baldart` / `baldart doctor` / `baldart version`.
58
+ - **Top-of-run banner in `bin/baldart.js`** — when a newer baldart npm package has been detected on a previous run, every command prints a 3-line hint at startup (yellow arrow + bold version, then two gray lines): `↑ baldart X.Y.Z available (you have A.B.C)` + `Update with: npm i -g baldart@latest` + `Suppress with: BALDART_NO_UPDATE_CHECK=1`. Zero-latency (reads cache only); no banner if cache is missing or up-to-date.
59
+ - **`CLI` row in `baldart doctor` diagnostic table** — always shown (independent of git/framework state), surfaces drift inline alongside framework version / config / overlays / hooks so the doctor remains the single pane of glass. Severity `warn` when an update is available, `ok` otherwise.
60
+ - **CLI drift in `baldart version` box** — the existing `CLI: vX.Y.Z` line now appends `→ vA.B.C available (npm i -g baldart@latest)` when a newer version was detected.
61
+ - **`State.reconcileInstalledVersion({ to, reason })` in `src/utils/state.js`** — new ledger reconciliation helper. Idempotent (no-op when `from === to`), audit-trailed (writes a `ledger-reconcile` history entry distinct from real `update` events), and reversible (pure local file write). Used by the doctor's new `reconcile-state-ledger` action and available for direct call from future recovery flows.
62
+ - **Ledger drift detection in `baldart doctor`** — `detectState` now compares `.framework/VERSION` against `.baldart/state.json:installed_version` and surfaces a `stateLedgerDrift` state field when they diverge. The action planner inserts a `reconcile-state-ledger` action (`autoOk: true`, idempotent, no remote effects) that one-shot fixes the drift via `State.reconcileInstalledVersion()`. Drift is non-blocking — it pairs with other proposed actions instead of short-circuiting them.
63
+
64
+ ### Changed
65
+
66
+ - **`src/commands/update.js`** — replaced the silent `try { State.recordUpdate(...) } catch (_) {}` on the ledger write with a visible warning + actionable next step. When the write fails or returns an unexpected `installed_version`, the user now sees a yellow warning explaining the drift (framework on disk is at vNEW but ledger still records vOLD) and is told to run `baldart doctor` to reconcile. This is the root cause of most "my doctor says I'm on the wrong version" reports — silent catches were hiding write failures (permissions, EBUSY) for weeks. New behavior makes the failure immediately visible and self-healable.
67
+
68
+ ### Notes
69
+
70
+ - **CLI vs framework — two independent layers.** `npm i -g baldart@latest` updates the binary on disk (this package); `baldart update` updates the *framework payload* inside the consumer repo (the `.framework/` subtree). They are *independent* by design — a consumer may want to update the framework without touching the global CLI (or vice versa). The notifier surfaces drift on the *CLI* layer; the existing `Remote` row in doctor (and the `Remote:` line in `version`) surfaces drift on the *framework* layer. Both layers are now visible at a glance from the same diagnostic surface.
71
+ - **Why not auto-install.** Three reasons we deliberately reject `npx baldart` auto-running `npm i -g`: (1) blast radius — global install impacts *every* project on the user's machine, not just the one they invoked from; (2) permissions — `npm i -g` often needs sudo (without nvm/volta), and a no-args `baldart` cannot trigger sudo prompts silently; (3) reproducibility — `--auto` runs in CI must be deterministic, so auto-updating the CLI mid-run would change behavior unpredictably. The notifier is the right level: visible, dismissible, never invasive.
72
+ - **No new `baldart.config.yml` keys.** The notifier cache lives in `~/.baldart/cli-update-check.json` (per-user, not per-project), so the schema-change propagation rule (per `feedback_schema_change_propagation`) does not apply to this release. Suppression is via environment variable (`BALDART_NO_UPDATE_CHECK=1`) which is intentionally global — disabling the notifier in one project but enabling it in another is not a use case worth modeling.
73
+ - **Backwards-compatible.** Pre-3.13 consumers see the new banner on their next `baldart` run after the npm package upgrades. Consumers that never want the check set `BALDART_NO_UPDATE_CHECK=1` in their shell rc; CI runs are auto-suppressed.
74
+ - **No framework payload changes.** All work is inside `src/` and `bin/` — the framework agents/skills/commands/protocols are unchanged. This is purely a CLI-layer improvement; no consumer files are touched by `baldart update`.
75
+
8
76
  ## [3.12.0] - 2026-05-23
9
77
 
10
78
  BALDART completes the **UI excellence loop**: the `ui-expert` agent is upgraded from a "good but generic" baseline to a world-class UI/UX reviewer/designer, and — crucially — the registry-first discipline gains a **post-intervention coherence gate** so visual coherence is enforced not only when work begins (BLOCKING reads) but also when work ends (BLOCKING completion check). The asymmetry of v3.11.0 (registry read before, no verification after) is closed: every UI change introduced by `ui-expert`, the `ui-design` skill, the `frontend-design` skill, or caught by `code-reviewer` must now reconcile the registry (INDEX + per-component specs + tokens-reference) in the **same change**, never deferring drift to the weekly `ds-drift` routine. This release contains zero schema changes — the cascade reuses `features.has_design_system` + `paths.*` already in place.
package/README.md CHANGED
@@ -156,6 +156,30 @@ last-push info. Use `--offline` to skip the upstream fetch.
156
156
 
157
157
  State updates happen automatically — no manual bookkeeping required.
158
158
 
159
+ ### CLI self-update notifier (new in v3.13.0)
160
+
161
+ The CLI binary (`baldart`, installed globally via `npm i -g baldart`) and the
162
+ framework payload (`.framework/` inside each repo, updated by `baldart update`)
163
+ are two independent layers. Until now the CLI drifted silently — users on an
164
+ old CLI never learned about new flags or new doctor checks.
165
+
166
+ Now every `baldart` run does a best-effort check against `registry.npmjs.org`
167
+ (cached at `~/.baldart/cli-update-check.json` with a 24h TTL, 1.5s network
168
+ timeout, fire-and-forget). When a newer version is available, a one-shot
169
+ banner appears at the top of the run:
170
+
171
+ ```
172
+ ↑ baldart 3.13.0 available (you have 3.12.0)
173
+ Update with: npm i -g baldart@latest
174
+ Suppress with: BALDART_NO_UPDATE_CHECK=1
175
+ ```
176
+
177
+ The notifier never installs anything automatically — global installs can
178
+ require sudo and affect every other project on your machine, so the choice
179
+ stays with you. CLI drift also appears inline in `baldart doctor` (new `CLI`
180
+ row) and in `baldart version`. Auto-suppressed in CI, in non-TTY runs, with
181
+ `--offline`, with `NODE_ENV=test`, or via `BALDART_NO_UPDATE_CHECK=1`.
182
+
159
183
  ### Project Configuration (new in v3.0.0)
160
184
 
161
185
  BALDART skills are **portable across projects**. Instead of hard-coding paths
package/VERSION CHANGED
@@ -1 +1 @@
1
- 3.12.0
1
+ 3.14.0
package/bin/baldart.js CHANGED
@@ -3,6 +3,20 @@
3
3
  const { Command } = require('commander');
4
4
  const chalk = require('chalk');
5
5
  const packageJson = require('../package.json');
6
+ const updateNotifier = require('../src/utils/update-notifier');
7
+
8
+ // CLI self-update notifier (v3.13.0+). Best-effort; non-blocking; suppressed
9
+ // in CI / non-TTY / via BALDART_NO_UPDATE_CHECK=1 / when --offline is passed.
10
+ // Prints a one-shot hint at the top of the run if a newer npm version was
11
+ // detected on a previous invocation, then kicks off a background refresh.
12
+ // First-ever run shows nothing — that is intentional; we never block the
13
+ // command on a network call.
14
+ const rawArgsForNotifier = process.argv.slice(2);
15
+ const offlineForNotifier = rawArgsForNotifier.includes('--offline');
16
+ updateNotifier.announceAndRefresh({
17
+ currentVersion: packageJson.version,
18
+ offline: offlineForNotifier,
19
+ });
6
20
 
7
21
  const program = new Command();
8
22
 
@@ -45,7 +45,7 @@ so existing behavior is preserved).
45
45
 
46
46
  ---
47
47
 
48
- ## 2. The four moving parts
48
+ ## 2. The five moving parts
49
49
 
50
50
  ```
51
51
  ┌─────────────────────────────────────────────────────────────────────┐
@@ -53,17 +53,30 @@ so existing behavior is preserved).
53
53
  │ features.has_lsp_layer: true │
54
54
  │ lsp.installed_servers: [typescript, python] │
55
55
  │ lsp.auto_verify: true │
56
+ │ tools.enabled: [claude] ← gates native plugin install │
56
57
  └──────────────┬──────────────────────────────────────────────────────┘
57
58
  │ read by
58
59
 
59
60
  ┌─────────────────────────────────────────────────────────────────────┐
60
61
  │ Plumbing layer (CLI, this repo) │
61
62
  │ src/utils/lsp-adapters/<lang>.js ← per-language recipes │
62
- │ src/utils/lsp-installer.js ← high-level install+verify
63
+ │ src/utils/lsp-installer.js ← server + plugin installer
63
64
  │ src/commands/configure.js (prompt + install on opt-in) │
64
- │ src/commands/doctor.js (verify + lsp-fix action)
65
+ │ src/commands/doctor.js (verify + lsp-fix + plugin-fix)
65
66
  └──────────────┬──────────────────────────────────────────────────────┘
66
- │ writes installed_servers; surfaces broken servers
67
+ │ writes installed_servers; runs `claude plugin install`
68
+
69
+ ┌─────────────────────────────────────────────────────────────────────┐
70
+ │ Native Claude Code integration (since v3.14.0) │
71
+ │ Marketplace: anthropics/claude-plugins-official (user scope) │
72
+ │ Plugins: │
73
+ │ typescript-lsp@claude-plugins-official │
74
+ │ pyright-lsp@claude-plugins-official │
75
+ │ gopls-lsp@claude-plugins-official │
76
+ │ rust-analyzer-lsp@claude-plugins-official │
77
+ │ ruby-lsp@claude-plugins-official │
78
+ └──────────────┬──────────────────────────────────────────────────────┘
79
+ │ surfaces LSP to Claude Code at runtime
67
80
 
68
81
  ┌─────────────────────────────────────────────────────────────────────┐
69
82
  │ Protocol layer (framework/) │
@@ -85,9 +98,13 @@ so existing behavior is preserved).
85
98
  ```
86
99
 
87
100
  Each layer is independently testable and individually replaceable. The
88
- adapters never speak LSP themselves they just describe *how to install*
89
- the language server. Actual symbol resolution at runtime happens through
90
- whatever Claude Code plugin / MCP server the consumer has loaded.
101
+ adapters describe *how to install* the language server AND *which native
102
+ plugin* surfaces it to Claude. Pre-v3.14.0 the second half was a manual
103
+ step the user almost always skipped opting into LSP put a binary on
104
+ disk that Claude never invoked. v3.14.0 closes that gap: the configure
105
+ flow installs both halves in one step, gated on `claude` being in
106
+ `tools.enabled` (Codex-only consumers are unaffected — the gate degrades
107
+ to a silent no-op).
91
108
 
92
109
  ---
93
110
 
@@ -123,6 +140,23 @@ When the user confirms, for each detected language:
123
140
  `baldart doctor` re-verifies on the next run and flags the mismatch if the
124
141
  user hasn't run the printed command yet.
125
142
 
143
+ **Then (v3.14.0+) the native plugin install runs.** For every server in
144
+ `installed_servers` whose adapter declares a `claudePluginId()`, configure:
145
+
146
+ 1. Verifies the `claude` CLI is on PATH (skipped silently if not — manual
147
+ install commands are printed instead).
148
+ 2. Idempotently registers the `claude-plugins-official` marketplace at user
149
+ scope (`claude plugin marketplace add anthropics/claude-plugins-official
150
+ --scope user` — no-op when already registered).
151
+ 3. Reads `claude plugin list --json` to skip plugins already installed at
152
+ any scope.
153
+ 4. Runs `claude plugin install <id> --scope user` for each missing plugin
154
+ (e.g. `typescript-lsp@claude-plugins-official`). User scope = a single
155
+ install is reused by every project on the machine.
156
+
157
+ Gated on `claude` being in `tools.enabled` — Codex-only consumers see a
158
+ silent skip, not a failure.
159
+
126
160
  ### 3.2 Update from pre-v3.10
127
161
 
128
162
  ```bash
@@ -238,7 +272,7 @@ Every adapter is a class exporting:
238
272
  | `npmPackage` | string (npm-dev only) | What goes after `npm install --save-dev` |
239
273
  | `installCommand()` | string | The exact shell command (printed or executed) |
240
274
  | `verifyCommand()` | string | A no-side-effect probe (e.g. `--version`) |
241
- | `claudePluginId()` | string \| null | Reserved: id of a Claude Code code-intelligence plugin |
275
+ | `claudePluginId()` | string \| null | Native Claude Code plugin id (e.g. `typescript-lsp@claude-plugins-official`) — installed by the configure flow alongside the language server (v3.14.0+). `null` = no companion plugin |
242
276
  | `static detect(cwd)` | boolean | Pure: does the language live in this repo? |
243
277
 
244
278
  The contract is enforced by convention, not by an interface check. When you
@@ -249,14 +283,37 @@ to the REGISTRY in `lsp-adapters/index.js`.
249
283
 
250
284
  `LspInstaller` exposes:
251
285
 
286
+ **Language-server layer (since v3.10.0):**
287
+
252
288
  - `detectLanguages()` → string[] — pure, no I/O beyond filesystem reads
253
289
  - `recommend(langs?)` → entries with install metadata for UI consumption
254
290
  - `installServers(names, { interactive=true, dryRun=false })` →
255
291
  `{ installed, skipped, manual }` — returns rather than throws
256
292
  - `verifyServers(names)` → `[{ name, ok, output|error }]` — never throws
257
293
 
294
+ **Native Claude Code plugin layer (since v3.14.0):**
295
+
296
+ - `isClaudeCliAvailable()` → boolean — `claude --version` probe with 5s timeout
297
+ - `ensureClaudeMarketplace()` → `{ ok, alreadyPresent, error? }` — idempotently
298
+ registers `anthropics/claude-plugins-official` at user scope; reads the
299
+ existing marketplace list first so re-runs are no-ops
300
+ - `listInstalledClaudePlugins()` → `Set<string>` of `<name>@<marketplace>` ids
301
+ pulled from `claude plugin list --json`; empty set on any failure (degrade
302
+ open — we'd rather attempt a redundant install than incorrectly skip)
303
+ - `installClaudePlugins(names, { toolsEnabled=['claude'], scope='user', interactive=false })`
304
+ → `{ installed, skipped, manual, marketplace, reason? }` — gated on
305
+ `claude` being in `toolsEnabled` AND the CLI being available; per-plugin
306
+ idempotency check; defaults to `--scope user` so a single install is reused
307
+ across every project on the machine
308
+ - `verifyClaudePlugins(names)` → `[{ name, pluginId, installed }]` —
309
+ consumed by `baldart doctor` to surface the half-installed state where the
310
+ language-server binary is on PATH but the matching plugin is missing
311
+
258
312
  All side-effecting methods are gated by `interactive` and a per-server
259
- confirm; doctor passes `interactive: true` so the user retains veto power.
313
+ confirm; doctor passes `interactive: true` for the server layer and
314
+ `interactive: false` for the plugin layer (the plugin install is idempotent
315
+ and user-scope, so the outer doctor prompt + per-action `autoOk: true` is
316
+ sufficient).
260
317
 
261
318
  ---
262
319
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "3.12.0",
3
+ "version": "3.14.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"
@@ -79,6 +79,30 @@ async function add(repo, options) {
79
79
  // Step 4: Download framework
80
80
  UI.header('STEP 4/5: Download Framework');
81
81
 
82
+ // `.framework/` MUST be trackable — `git subtree add` itself fails if a
83
+ // pre-existing `.gitignore` rule matches the path. Heal proactively.
84
+ // (since v3.14.0)
85
+ try {
86
+ const heal = await git.ensureFrameworkNotIgnored();
87
+ if (heal.wasFixed) {
88
+ UI.warning(`.framework/ was ignored by ${heal.source || 'a gitignore'} — auto-healed before subtree add.`);
89
+ if (heal.removedLines > 0) {
90
+ UI.info(` Removed ${heal.removedLines} stale .framework line(s) from .gitignore.`);
91
+ }
92
+ if (heal.appendedNegation) {
93
+ UI.info(' Appended `!.framework` negation block to .gitignore (idempotent).');
94
+ }
95
+ if (heal.stillIgnored) {
96
+ UI.error('.framework/ is STILL ignored after auto-heal — install would fail.');
97
+ UI.info(`Source: ${heal.recheck.source}:${heal.recheck.line} (${heal.recheck.pattern})`);
98
+ UI.info('Resolve by removing that rule, then re-run `baldart add`.');
99
+ process.exit(1);
100
+ }
101
+ }
102
+ } catch (err) {
103
+ UI.warning(`Could not check .gitignore for .framework: ${err.message}`);
104
+ }
105
+
82
106
  const downloadSpinner = UI.spinner(`Downloading from ${repo}...`).start();
83
107
 
84
108
  try {
@@ -607,6 +607,50 @@ async function interactivePrompts(merged, detected) {
607
607
  UI.warning(`Skipped: ${skipped.map(s => `${s.name} (${s.reason})`).join(', ')}`);
608
608
  }
609
609
  }
610
+
611
+ // ---- Native Claude Code companion plugins (since v3.14.0) ----------
612
+ // Install the official LSP plugin alongside each language server. Without
613
+ // this step, opting into the LSP layer only puts the binary on disk —
614
+ // Claude itself never sees it and agents fall back to Grep silently.
615
+ // Gated on `claude` being in `tools.enabled` and the `claude` CLI being
616
+ // on PATH; degrades gracefully otherwise.
617
+ const allLspServers = Array.from(new Set([
618
+ ...(merged.lsp.installed_servers || [])
619
+ ]));
620
+ if (allLspServers.length) {
621
+ const pluginResult = await installer.installClaudePlugins(allLspServers, {
622
+ toolsEnabled: merged.tools.enabled,
623
+ interactive: false,
624
+ scope: 'user'
625
+ });
626
+ if (pluginResult.reason === 'claude not in tools.enabled') {
627
+ UI.info('Skipping native Claude plugin install (claude not in tools.enabled).');
628
+ } else if (pluginResult.reason === 'claude CLI not on PATH') {
629
+ UI.warning('`claude` CLI not on PATH — native LSP plugins not installed.');
630
+ UI.info('Install them manually after adding the CLI:');
631
+ for (const m of pluginResult.skipped) {
632
+ // We don't have manual[] entries here (no targets resolved past the
633
+ // gate), so synthesize the command from the adapter directly.
634
+ try {
635
+ const a = lspAdapters.getAdapter(m.name, process.cwd());
636
+ const pid = a.claudePluginId();
637
+ if (pid) UI.code(`claude plugin install ${pid} --scope user`, m.name);
638
+ } catch { /* unknown adapter */ }
639
+ }
640
+ } else {
641
+ if (pluginResult.installed.length) {
642
+ UI.success(`Installed Claude LSP plugin(s): ${pluginResult.installed.map(i => i.pluginId).join(', ')}`);
643
+ }
644
+ const alreadyOnes = pluginResult.skipped.filter(s => s.reason === 'plugin already installed');
645
+ if (alreadyOnes.length) {
646
+ UI.info(`Already installed: ${alreadyOnes.map(s => s.name).join(', ')}`);
647
+ }
648
+ for (const m of pluginResult.manual) {
649
+ UI.warning(`Could not auto-install ${m.label} plugin — run manually:`);
650
+ UI.code(m.command, '');
651
+ }
652
+ }
653
+ }
610
654
  }
611
655
  } else {
612
656
  // Feature disabled — wipe the state so `installed_servers` doesn't lie.
@@ -31,6 +31,8 @@ const UI = require('../utils/ui');
31
31
  const State = require('../utils/state');
32
32
  const Hooks = require('../utils/hooks');
33
33
  const LspInstaller = require('../utils/lsp-installer');
34
+ const UpdateNotifier = require('../utils/update-notifier');
35
+ const cliPackageJson = require('../../package.json');
34
36
 
35
37
  const FRAMEWORK_DIR = '.framework';
36
38
  const REPO_DEFAULT = 'antbald/BALDART';
@@ -146,6 +148,8 @@ async function detectState(cwd, opts = {}) {
146
148
  const git = new GitUtils(cwd);
147
149
  const state = {
148
150
  cwd,
151
+ cliVersion: cliPackageJson.version,
152
+ cliUpdate: UpdateNotifier.hint({ currentVersion: cliPackageJson.version }),
149
153
  isGitRepo: await git.isGitRepo(),
150
154
  frameworkPresent: await git.frameworkExists(),
151
155
  legacyLayout: false,
@@ -188,6 +192,19 @@ async function detectState(cwd, opts = {}) {
188
192
  ? `v${ledger.last_pushed_version} (${(ledger.last_push_date || '').slice(0, 10) || '—'})`
189
193
  : null;
190
194
 
195
+ // Ledger drift detection (v3.13.0+): the framework on disk says one thing,
196
+ // the ledger says another. Caused by interrupted updates, manual commits
197
+ // that excluded state.json, or filesystem write failures the old silent
198
+ // catch in update.js used to hide. Self-healable via `reconcile-state-ledger`.
199
+ state.stateLedgerDrift = null;
200
+ if (state.frameworkVersion && ledger.installed_version &&
201
+ state.frameworkVersion !== ledger.installed_version) {
202
+ state.stateLedgerDrift = {
203
+ payload: state.frameworkVersion,
204
+ ledger: ledger.installed_version
205
+ };
206
+ }
207
+
191
208
  state.remote = await describeRemote(git, ledger.framework_repo || REPO_DEFAULT, !!opts.offline);
192
209
  state.commitsAhead = await commitsAheadOfRemote(git);
193
210
  state.workingTreeDirty = await localFrameworkChanges(git);
@@ -198,10 +215,18 @@ async function detectState(cwd, opts = {}) {
198
215
  state.editGateRegistered = false;
199
216
  try { state.editGateRegistered = Hooks.isRegistered(cwd); } catch (_) {}
200
217
 
218
+ // `.framework/` is a git subtree — gitignore matching it breaks every
219
+ // future `git add` under that path (the failure mode that hit v3.13.0
220
+ // consumers during push). Detect any gitignore rule that catches the
221
+ // path so the planner can heal it before any flow needs to stage files.
222
+ state.frameworkIgnored = { ignored: false };
223
+ try { state.frameworkIgnored = await git.checkFrameworkIgnored(); } catch (_) {}
224
+
201
225
  // ---- LSP layer (since v3.10.0) -------------------------------------
202
226
  state.lspEnabled = false;
203
227
  state.lspInstalled = [];
204
228
  state.lspBroken = [];
229
+ state.lspMissingClaudePlugins = []; // [{ name, pluginId }] — since v3.14.0
205
230
  try {
206
231
  // Reuse the `config` already parsed above instead of re-reading the file.
207
232
  if (config && !config.__malformed && config.features && config.features.has_lsp_layer === true) {
@@ -213,6 +238,20 @@ async function detectState(cwd, opts = {}) {
213
238
  const installer = new LspInstaller(cwd);
214
239
  const results = installer.verifyServers(installed);
215
240
  state.lspBroken = results.filter(r => !r.ok).map(r => r.name);
241
+
242
+ // Companion Claude plugin check (v3.14.0+). Gated on `claude` being
243
+ // in tools.enabled AND the `claude` CLI being available; otherwise
244
+ // we don't report missing plugins (manual install hint would be
245
+ // misleading without the CLI).
246
+ const toolsEnabled = (config.tools && Array.isArray(config.tools.enabled))
247
+ ? config.tools.enabled
248
+ : ['claude'];
249
+ if (toolsEnabled.includes('claude') && installer.isClaudeCliAvailable()) {
250
+ const checks = installer.verifyClaudePlugins(installed);
251
+ state.lspMissingClaudePlugins = checks
252
+ .filter(c => c.installed === false)
253
+ .map(c => ({ name: c.name, pluginId: c.pluginId }));
254
+ }
216
255
  }
217
256
  }
218
257
  } catch (_) { /* never block doctor on LSP probe */ }
@@ -313,6 +352,64 @@ function planActions(state) {
313
352
  // Drift is non-blocking — keep checking for other actions after.
314
353
  }
315
354
 
355
+ if (state.stateLedgerDrift) {
356
+ actions.push({
357
+ key: 'reconcile-state-ledger',
358
+ label: `Reconcile state ledger (${state.stateLedgerDrift.ledger} → ${state.stateLedgerDrift.payload})`,
359
+ why: `.framework/VERSION is v${state.stateLedgerDrift.payload} but .baldart/state.json records installed_version=${state.stateLedgerDrift.ledger}. Typical causes: an update flow that exited mid-way (configure declined, stash conflict), a manual commit that excluded .baldart/state.json, or a write failure that the old silent catch in update.js used to hide (fixed in v3.13.0).`,
360
+ autoOk: true, // pure local file write, idempotent, fully reversible
361
+ run: () => {
362
+ const res = State.reconcileInstalledVersion({
363
+ to: state.stateLedgerDrift.payload,
364
+ reason: 'doctor reconcile — payload/ledger drift'
365
+ });
366
+ if (res.changed) {
367
+ UI.success(`Ledger updated: installed_version ${res.from} → ${res.to}`);
368
+ UI.info('History entry "ledger-reconcile" recorded for audit. Commit .baldart/state.json when ready.');
369
+ } else {
370
+ UI.info('No change needed — ledger already matches payload.');
371
+ }
372
+ },
373
+ });
374
+ // Drift is non-blocking — keep checking for other actions after.
375
+ }
376
+
377
+ // Heal `.gitignore` BEFORE any other action that may stage files under
378
+ // `.framework/`. Without this, every downstream `git add` (push, auto-commit
379
+ // during update, lsp-install commits, …) would fail with "paths are ignored
380
+ // by one of your .gitignore files". (since v3.14.0)
381
+ if (state.frameworkIgnored && state.frameworkIgnored.ignored) {
382
+ actions.push({
383
+ key: 'unignore-framework',
384
+ label: 'Heal .gitignore so .framework/ is trackable',
385
+ why: `.framework/ is a git subtree managed by BALDART — it MUST be tracked. Currently ignored by ${state.frameworkIgnored.source}:${state.frameworkIgnored.line} (pattern: ${state.frameworkIgnored.pattern}). Any subsequent \`git add\` under that path would fail.`,
386
+ autoOk: true, // pure local file rewrite, idempotent, fully reversible
387
+ run: async () => {
388
+ const git = new GitUtils(state.cwd);
389
+ const heal = await git.ensureFrameworkNotIgnored();
390
+ if (!heal.wasFixed) {
391
+ UI.info('.gitignore already healthy — nothing to do.');
392
+ return;
393
+ }
394
+ UI.success('.gitignore healed:');
395
+ if (heal.removedLines > 0) {
396
+ UI.info(` Removed ${heal.removedLines} stale .framework line(s).`);
397
+ }
398
+ if (heal.appendedNegation) {
399
+ UI.info(' Appended `!.framework` negation block (idempotent).');
400
+ }
401
+ if (heal.stillIgnored) {
402
+ UI.warning(' .framework/ is STILL ignored — likely a parent-dir .gitignore outside this repo.');
403
+ UI.info(` Source: ${heal.recheck.source}:${heal.recheck.line} (${heal.recheck.pattern})`);
404
+ UI.info(' Resolve by removing that rule manually.');
405
+ } else {
406
+ UI.info(' Commit the .gitignore change when you next push.');
407
+ }
408
+ },
409
+ });
410
+ // Non-blocking — keep looking for other actions to run in the same pass.
411
+ }
412
+
316
413
  if (state.editGateRegistered === false) {
317
414
  actions.push({
318
415
  key: 'register-edit-gate',
@@ -347,6 +444,40 @@ function planActions(state) {
347
444
  });
348
445
  }
349
446
 
447
+ // Native Claude LSP plugins (v3.14.0+). The server binary alone is not
448
+ // enough — without the matching `<lang>-lsp@claude-plugins-official` plugin
449
+ // installed, Claude Code never invokes the LSP and agents silently fall back
450
+ // to Grep. This action backfills the plugins for installs predating v3.14.0
451
+ // and recovers from manual plugin uninstalls.
452
+ if (state.lspEnabled && state.lspMissingClaudePlugins && state.lspMissingClaudePlugins.length > 0) {
453
+ const missing = state.lspMissingClaudePlugins;
454
+ actions.push({
455
+ key: 'lsp-install-claude-plugins',
456
+ label: `Install ${missing.length} native Claude LSP plugin(s)`,
457
+ why: `LSP layer is enabled and the language server(s) are present, but the matching Claude Code plugin(s) are not installed: ${missing.map(m => m.pluginId).join(', ')}. Without them Claude never sees the LSP and the layer is effectively dead. Standard since v3.14.0; missing on installs predating that version.`,
458
+ autoOk: true, // user-scope plugin install, idempotent, fully reversible via `claude plugin uninstall`
459
+ run: async () => {
460
+ const installer = new LspInstaller(state.cwd);
461
+ const res = await installer.installClaudePlugins(missing.map(m => m.name), {
462
+ toolsEnabled: ['claude'], // we already checked tools.enabled in detectState
463
+ interactive: false,
464
+ scope: 'user',
465
+ });
466
+ if (res.installed.length) {
467
+ UI.success(`Installed: ${res.installed.map(i => i.pluginId).join(', ')}`);
468
+ }
469
+ for (const m of res.manual || []) {
470
+ UI.warning(`Could not auto-install ${m.label}:`);
471
+ console.log(` ${m.command}`);
472
+ }
473
+ if (res.skipped.length) {
474
+ const real = res.skipped.filter(s => s.reason !== 'plugin already installed');
475
+ if (real.length) UI.warning(`Skipped: ${real.map(s => `${s.name} (${s.reason})`).join(', ')}`);
476
+ }
477
+ },
478
+ });
479
+ }
480
+
350
481
  const remoteAhead = state.remote.fetched && state.remote.behind > 0;
351
482
  const hasLocalWork = state.workingTreeDirty > 0 || state.commitsAhead > 0;
352
483
 
@@ -394,6 +525,17 @@ function renderDiagnostic(state) {
394
525
  console.log(`Repository: ${state.cwd}`);
395
526
  console.log();
396
527
 
528
+ // CLI version line — always shown (independent of git repo state). When a
529
+ // newer baldart npm package was detected on a previous run, surface it here
530
+ // so the doctor table is the single pane of glass for "what's stale".
531
+ console.log(statusLine(
532
+ 'CLI',
533
+ state.cliUpdate
534
+ ? `v${state.cliVersion} → v${state.cliUpdate.latest} available (npm i -g baldart@latest)`
535
+ : `v${state.cliVersion}`,
536
+ state.cliUpdate ? 'warn' : 'ok'
537
+ ));
538
+
397
539
  console.log(statusLine('Git repo', state.isGitRepo ? 'yes' : 'no', state.isGitRepo ? 'ok' : 'err'));
398
540
 
399
541
  if (!state.isGitRepo) return;
@@ -429,9 +571,11 @@ function renderDiagnostic(state) {
429
571
  console.log(statusLine(
430
572
  'State ledger',
431
573
  state.statePresent
432
- ? `installed v${state.stateInstalled} (${fmtDate(state.stateInstallDate)})`
574
+ ? (state.stateLedgerDrift
575
+ ? `drift: ledger=v${state.stateLedgerDrift.ledger}, payload=v${state.stateLedgerDrift.payload}`
576
+ : `installed v${state.stateInstalled} (${fmtDate(state.stateInstallDate)})`)
433
577
  : 'not seeded',
434
- state.statePresent ? 'ok' : 'warn'
578
+ state.stateLedgerDrift ? 'warn' : (state.statePresent ? 'ok' : 'warn')
435
579
  ));
436
580
 
437
581
  if (state.stateLastUpdate) {
@@ -478,15 +622,29 @@ function renderDiagnostic(state) {
478
622
  state.editGateRegistered ? 'ok' : 'warn'
479
623
  ));
480
624
 
625
+ if (state.frameworkIgnored && state.frameworkIgnored.ignored) {
626
+ console.log(statusLine(
627
+ '.gitignore',
628
+ `IGNORES .framework/ (${state.frameworkIgnored.source}:${state.frameworkIgnored.line}) — will be auto-healed`,
629
+ 'warn'
630
+ ));
631
+ }
632
+
481
633
  if (state.lspEnabled) {
482
634
  const total = state.lspInstalled.length;
483
635
  const broken = state.lspBroken.length;
484
- const value = total === 0
485
- ? 'enabled but no servers installed'
486
- : broken === 0
487
- ? `${total} server(s) verified (${state.lspInstalled.join(', ')})`
488
- : `${broken}/${total} broken (${state.lspBroken.join(', ')})`;
489
- const severity = total === 0 || broken > 0 ? 'warn' : 'ok';
636
+ const missingPlugins = (state.lspMissingClaudePlugins || []).length;
637
+ let value;
638
+ if (total === 0) {
639
+ value = 'enabled but no servers installed';
640
+ } else if (broken > 0) {
641
+ value = `${broken}/${total} broken (${state.lspBroken.join(', ')})`;
642
+ } else if (missingPlugins > 0) {
643
+ value = `${total} server(s) verified, ${missingPlugins} Claude plugin(s) missing (${state.lspMissingClaudePlugins.map(m => m.pluginId).join(', ')})`;
644
+ } else {
645
+ value = `${total} server(s) verified (${state.lspInstalled.join(', ')})`;
646
+ }
647
+ const severity = total === 0 || broken > 0 || missingPlugins > 0 ? 'warn' : 'ok';
490
648
  console.log(statusLine('LSP layer', value, severity));
491
649
  } else {
492
650
  console.log(statusLine('LSP layer', 'disabled', 'ok'));
Binary file