diff --git a/CHANGELOG.md b/CHANGELOG.md index c6f8ab99..f32e5b5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ All notable changes to this project are documented here. The format follows ### Changed +- **Statusline, cold-cache guard, and cheaper `/compact` are off by default.** `setup` / + `ensure` no longer first-install them. New machines get `/sub` (and routed subagents) + only. An already-owned status line or guard is still refreshed on upgrade; compact + model lists already in `config.toml` still apply. Opt in with `llmtrim statusline + install`, `llmtrim guard install`, or `llmtrim compact models haiku sonnet`. + - **Status watch SVGs show the 0.13 Sub tab.** The README hero (`status-watch-dark.svg` / `status-watch-light.svg`) still rendered the 0.12 Always→Codex / kimi / grok presets. Tab 4 now matches CLIProxyAPI: diff --git a/INSTALL.md b/INSTALL.md index deb75e73..928c66a8 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -100,20 +100,16 @@ curl and the npm one-liner above run this for you. After Cargo, Homebrew, Scoop, `LLMTRIM_NO_SETUP=1`, run it yourself: ```bash -llmtrim setup # CA + shell env + autostart + Claude Code integrations + daemon +llmtrim setup # CA + shell env + autostart + Claude Code /sub + daemon llmtrim status # live savings dashboard ``` Open a new terminal so tools inherit `HTTPS_PROXY`. Re-running `setup` is safe (idempotent). -When Claude Code is present (`~/.claude`), `setup` also enables: - -- status line -- cold-cache guard -- window-local `/sub` -- cheaper `/compact` model chain - -No separate install steps for those. Later upgrades refresh them via `update` / `ensure`. +When Claude Code is present (`~/.claude`), `setup` also enables window-local `/sub`. +Status line, cold-cache guard, and cheaper `/compact` are not installed (deprecated; +opt in with `llmtrim statusline install`, `llmtrim guard install`, `llmtrim compact models …`). +Existing installs of those keep working; `ensure` refreshes an already-owned status line or guard. llmtrim is a local MITM proxy (plus optional Claude Code hooks). `llmtrim uninstall` reverses it. How traffic reaches tools: [README](README.md#what-it-does). diff --git a/README.md b/README.md index 2f66b005..4bf845a2 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,8 @@

- Using Claude Code? One install also gets you a live status line, - a cold-cache guard, cheaper /compact, - and /sub to serve it through CLIProxyAPI. + Using Claude Code? One install also gets you /sub + to serve it through CLIProxyAPI.

@@ -76,7 +75,7 @@ llmtrim sits on your machine as a local proxy, trims the waste, and forwards a s Compression cannot raise your bill or break a request; worst case is zero savings. Everything runs locally, nothing is sent to us. [In action →](#in-action) -For Claude Code the same install goes further: a status line with live trim % and rate limits, a guard that warns before an expired prompt cache re-bills your whole context, `/compact` on a cheaper model, and `/sub` to route sessions through another subscription. [Details →](#claude-code) +For Claude Code the same install also wires `/sub` to route sessions through another subscription. [Details →](#claude-code) --- @@ -88,7 +87,7 @@ npm install -g @llmtrim/cli@latest && llmtrim setup llmtrim status ``` -That's it. `setup` starts a local proxy, wires your shell, and enables recoverable tool-output shaping. When Claude Code is present, it also turns on the status line, cold-cache guard, `/sub`, and cheaper `/compact`. You do not run a separate install for each of those. +That's it. `setup` starts a local proxy, wires your shell, and enables recoverable tool-output shaping. When Claude Code is present, it also turns on `/sub`. You do not run a separate install for that. | You want | Run | |---|---| @@ -165,7 +164,7 @@ llmtrim ensure # match the recommended install state on this machine | Force one session through llmtrim | `llmtrim wrap claude` | | Remove everything | `llmtrim uninstall` | -After `setup`, `update`, or `ensure`, owned Claude Code pieces (status line, guard, `/sub`, compact defaults) stay in sync with the binary. You should not need `statusline install` or similar after an upgrade. +After `setup`, `update`, or `ensure`, owned Claude Code `/sub` stays in sync with the binary. Time series: `llmtrim status --daily` · `--weekly` · `--monthly` · `--json` · `--csv`. @@ -242,69 +241,12 @@ Default `auto` enables each stage only where it pays. `safe` is lossless-only. [ ## Claude Code -When `~/.claude` exists, `setup`, `update`, and `ensure` wire these. No separate install commands. +When `~/.claude` exists, `setup`, `update`, and `ensure` wire `/sub` (and routed subagents). No separate install command for that. | Feature | What you get | |---|---| -| Status line | Model, context gauge, trim %, rate limits, cache warm/cold | -| Guard | Blocks one turn if a cold-cache resume would rewrite a huge context (and bill for it) | -| `/compact` models | Prefer Haiku → Sonnet before your selected model, but only when the prompt cache is cold | | `/sub` | Per-window: `/sub on [optional:codex\|kimi\|grok]` · `/sub off` · `/sub status` | -```text -◆ Opus→gpt-5.6-terra ▓▓▓▓▓░░░ 142k ✂ 6.8% ◔ 3h·24% · 4d·12% ♻ 63% cached -``` - -
-Status line details - -Claude Code [custom status line](https://code.claude.com/docs/en/statusline). The arrow is the backend that answered the last turn (not merely what is configured). In `sub` fallback mode it stays off while Anthropic serves and shows up when a chain provider does. - -- `✂`: trim for this session (`✂ –` until something is saved) -- `◔`: rate-limit windows (time left · % used) — Claude.ai when Anthropic is serving; under `/sub` the active plan's windows (Codex today: weekly always, 5h when the plan reports one). Kimi/Grok fill in when their usage APIs are wired -- Context gauge: fill of the serving model's real window (green under 40%, orange 40-65%, red above) -- `♻`: prompt-cache reuse; becomes `♻ cache cold` after the cache TTL - -Owned settings rewrite themselves when the binary path or payload changes. To opt out, leave your own status line in place, or uninstall ours (`llmtrim statusline uninstall`). - -
- -
-Cold-cache guard - -Resuming a large session after the prompt-cache TTL rewrites the whole context at cache-write rates (often a few dollars) with no warning at the prompt. - -Guard is a free `UserPromptSubmit` hook. It blocks one turn, prints idle time, context size, estimated cost, and the draft you typed (Claude Code clears the input box and would otherwise drop it), then lets a resend through. If you type something else next, the blocked text is also reinjected as model context. `/compact` pays that cold write too, because it has to read the full context to summarize. Local-only slash commands that never hit the model (today: `/sub`) pass through without acking the gap, so the next real prompt still warns. - -Opt out: `llmtrim guard uninstall`. `ensure` remembers that choice. - -```text -Idle 6h 19m, 347k tokens of context. The prompt cache has expired, so the next turn -rewrites the whole context (about $3.47 before any work happens). -``` - -
- -
-Cheaper `/compact` - -```bash -llmtrim compact models haiku sonnet # setup already sets this by default -llmtrim compact status -llmtrim compact off -``` - -```toml -[compact] -models = ["haiku", "sonnet"] -``` - -Candidates run in order when they fit the compressed request. Claude's selected model is always the last fallback (do not put it in the list). Empty `models = []` records opt-out. - -The redirect only fires once the prompt cache has gone cold. `/compact` re-sends the conversation Claude Code has been caching against your selected model, so while that cache is warm a cache-read there costs less than a cold read on a smaller model, and the compact stays home. After the cache expires the cheaper model wins, so that is when the redirect takes over. - -
-
Subscription reroute (`sub`) (opt-in; may conflict with provider ToS) @@ -540,8 +482,7 @@ These knobs are orthogonal to compression. Each resolves env-first, then from th
-Claude Code options (compact models, subscription reroute) are under -[Claude Code](#claude-code). +Claude Code options (subscription reroute) are under [Claude Code](#claude-code).
Upstream proxy (corporate egress or chaining local tools) diff --git a/crates/llmtrim-cli/src/compact.rs b/crates/llmtrim-cli/src/compact.rs index 0d594b23..b2cc6892 100644 --- a/crates/llmtrim-cli/src/compact.rs +++ b/crates/llmtrim-cli/src/compact.rs @@ -1,5 +1,6 @@ //! Claude Code compaction request detection and ordered model planning. //! +//! Opt-in via `[compact].models` / `llmtrim compact models` — `setup` does not write defaults. //! Claude Code handles `/compact` locally and sends a normal Anthropic Messages request. The //! internal summarization prompt is the protocol marker; ordinary user text mentioning `/compact` //! must never trigger model substitution. diff --git a/crates/llmtrim-cli/src/ensure.rs b/crates/llmtrim-cli/src/ensure.rs index 8740bb67..dc13a71c 100644 --- a/crates/llmtrim-cli/src/ensure.rs +++ b/crates/llmtrim-cli/src/ensure.rs @@ -2,8 +2,10 @@ //! //! Happy path: `llmtrim setup` / `llmtrim update` / `llmtrim doctor --fix` / status `f` all //! land here. Power-user install/uninstall commands remain as escape hatches; humans should not -//! need them after a release. Owned Claude Code hooks and the status line are rewritten in place -//! when the binary path or feature payload changes so changelogs never say "re-run install". +//! need them after a release. Owned `/sub` hooks (and a previously installed status line or +//! guard) are rewritten in place when the binary path or feature payload changes so changelogs +//! never say "re-run install". Statusline, guard, and cheaper `/compact` are deprecated: new +//! installs do not wire them. use std::path::{Path, PathBuf}; @@ -124,7 +126,7 @@ fn save_at(path: &Path, state: &State) -> Result<()> { /// How ensure was invoked — controls prompting and how loud the report is. #[derive(Debug, Clone, Copy)] pub struct Options { - /// May prompt on a TTY (first-run compact/tray choices). Non-interactive defaults to yes + /// May prompt on a TTY (first-run tray choice). Non-interactive defaults to yes /// for recommended items, no for optional network downloads. pub interactive: bool, /// Skip the success panel (daemon start / quiet migrations). @@ -134,7 +136,8 @@ pub struct Options { /// Allow downloading the Linux tray binary when missing (interactive confirm, or forced). pub download_tray: bool, /// When false (quiet auto-heal), only refresh *owned* stale integrations and daemon skew — - /// never first-install statusline/guard//sub/compact or enable tray autostart. + /// never first-install `/sub` / subagents or enable tray autostart. Statusline, guard, and + /// compact are never first-installed even when this is true (deprecated). pub install_missing: bool, } @@ -185,36 +188,33 @@ fn probe_with(state: &State) -> Vec { let mut gaps = Vec::new(); let claude = crate::statusline::claude_code_present(); - if claude && !state.opt_out.statusline { - match crate::statusline::owned_status() { - crate::statusline::OwnedStatus::Missing => gaps.push(Gap { - id: "statusline", - label: "Statusline".into(), - detail: "not installed — recommended for Claude Code".into(), - }), - crate::statusline::OwnedStatus::Stale => gaps.push(Gap { - id: "statusline", - label: "Statusline".into(), - detail: "stale (binary path or refresh settings)".into(), - }), - crate::statusline::OwnedStatus::Current | crate::statusline::OwnedStatus::Foreign => {} - } + if claude + && !state.opt_out.statusline + && matches!( + crate::statusline::owned_status(), + crate::statusline::OwnedStatus::Stale + ) + { + // Deprecated: missing is not a gap. Refresh only an already-owned stale line. + gaps.push(Gap { + id: "statusline", + label: "Statusline".into(), + detail: "stale (binary path or refresh settings)".into(), + }); } - if claude && !state.opt_out.guard { - match crate::guard::owned_status() { - crate::guard::OwnedStatus::Missing => gaps.push(Gap { - id: "guard", - label: "Guard".into(), - detail: "not installed — warns before cold-cache resumes".into(), - }), - crate::guard::OwnedStatus::Stale => gaps.push(Gap { - id: "guard", - label: "Guard".into(), - detail: "stale (binary path)".into(), - }), - crate::guard::OwnedStatus::Current => {} - } + if claude + && !state.opt_out.guard + && matches!( + crate::guard::owned_status(), + crate::guard::OwnedStatus::Stale + ) + { + gaps.push(Gap { + id: "guard", + label: "Guard".into(), + detail: "stale (binary path)".into(), + }); } if claude && !state.opt_out.window_sub { @@ -250,14 +250,6 @@ fn probe_with(state: &State) -> Vec { } } - if claude && !state.opt_out.compact && !llmtrim_core::config::compact_models_configured() { - gaps.push(Gap { - id: "compact", - label: "Compact".into(), - detail: "cheaper /compact models not configured".into(), - }); - } - // Version skew: daemon older than this binary. if let Some(d) = crate::daemon::running() && let Some(v) = d.version.as_deref() @@ -316,8 +308,8 @@ pub fn apply(opts: Options) -> Result { if claude && !state.opt_out.statusline { let status = crate::statusline::owned_status(); - let skip_missing = - !opts.install_missing && matches!(status, crate::statusline::OwnedStatus::Missing); + // Deprecated: never first-install. Refresh an already-owned (or foreign-skip) line. + let skip_missing = matches!(status, crate::statusline::OwnedStatus::Missing); if !skip_missing { match crate::statusline::sync_owned() { Ok(crate::statusline::SyncOutcome::Installed) => { @@ -363,8 +355,7 @@ pub fn apply(opts: Options) -> Result { if claude && !state.opt_out.guard { let gstatus = crate::guard::owned_status(); - let skip_missing = - !opts.install_missing && matches!(gstatus, crate::guard::OwnedStatus::Missing); + let skip_missing = matches!(gstatus, crate::guard::OwnedStatus::Missing); if !skip_missing { match crate::guard::sync_owned() { Ok(true) => { @@ -469,43 +460,8 @@ pub fn apply(opts: Options) -> Result { .push((ui::NOTE, "Subagents".into(), "opted out".into())); } - if claude - && opts.install_missing - && !state.opt_out.compact - && !llmtrim_core::config::compact_models_configured() - { - let want = if opts.interactive { - confirm_default_yes("Use cheaper models for Claude Code /compact (Haiku → Sonnet)?") - } else { - true - }; - if want { - match llmtrim_core::config::write_compact_models(&["haiku".into(), "sonnet".into()]) { - Ok(()) => { - report.applied.push("compact"); - report.rows.push(( - ui::OK, - "Compact".into(), - "Haiku → Sonnet → original model".into(), - )); - } - Err(e) => { - report - .rows - .push((ui::WARN, "Compact".into(), format!("not configured: {e:#}"))) - } - } - } else { - // Remember opt-out as empty models list + flag. - let _ = llmtrim_core::config::write_compact_models(&[]); - state.opt_out.compact = true; - report.rows.push(( - ui::OK, - "Compact".into(), - "original model only (remembered)".into(), - )); - } - } else if claude && llmtrim_core::config::compact_models_configured() { + // Deprecated: never write `[compact].models` on setup/ensure. Leave an existing list alone. + if claude && llmtrim_core::config::compact_models_configured() { report .rows .push((ui::OK, "Compact".into(), "configured".into())); @@ -700,7 +656,8 @@ pub fn maybe_auto() -> Result { .iter() .any(|g| g.id == "daemon" || g.detail.contains("stale")); // Quiet path never first-installs missing integrations — only refresh owned stale - // pieces / daemon skew, or stamp the version after a binary bump. + // pieces / daemon skew, or stamp the version after a binary bump. Statusline/guard/compact + // are never first-installed on any path. if !version_mismatch && !needs_refresh { return Ok(false); } @@ -1006,6 +963,15 @@ mod tests { assert!(!s.opt_out.window_sub); } + #[test] + fn probe_never_flags_unconfigured_compact() { + let gaps = probe_with(&State::default()); + assert!( + gaps.iter().all(|g| g.id != "compact"), + "compact is deprecated and must not be a recommended gap: {gaps:?}" + ); + } + #[cfg(feature = "intercept")] #[test] fn route_agents_opt_out_is_sidecar_not_optout_field() { diff --git a/crates/llmtrim-cli/src/guard.rs b/crates/llmtrim-cli/src/guard.rs index beb8316e..c4d6562a 100644 --- a/crates/llmtrim-cli/src/guard.rs +++ b/crates/llmtrim-cli/src/guard.rs @@ -544,7 +544,8 @@ pub enum OwnedStatus { Current, } -/// Whether ensure should wire / refresh the cold-cache guard. +/// Ownership of the guard hook relative to this binary. `ensure` refreshes Stale only; +/// it does not first-install Missing (deprecated). pub fn owned_status() -> OwnedStatus { let Ok(path) = claude_settings_path() else { return OwnedStatus::Missing; diff --git a/crates/llmtrim-cli/src/main.rs b/crates/llmtrim-cli/src/main.rs index e5ad6b98..d0d94cc9 100644 --- a/crates/llmtrim-cli/src/main.rs +++ b/crates/llmtrim-cli/src/main.rs @@ -132,11 +132,11 @@ enum Commands { #[command(subcommand)] action: AgentsCmd, }, - /// Configure cheaper models for Claude Code `/compact` + /// Configure cheaper models for Claude Code `/compact` (deprecated; opt-in) /// - /// Configured alternatives are tried in order when they fit the request. The model Claude Code - /// originally requested is always the implicit final fallback. Prefer `llmtrim ensure` / - /// `setup` for the recommended defaults — this is the power-user escape hatch. + /// Not installed by `setup` / `ensure`. Configured alternatives are tried in order when they + /// fit the request. The model Claude Code originally requested is always the implicit final + /// fallback. #[command(hide = true)] Compact { #[command(subcommand)] @@ -167,8 +167,8 @@ enum Commands { /// /// The fastest path from install to compressing: ensures the local CA, sets /// HTTPS_PROXY + CA trust in your environment (shell profile on POSIX, - /// HKCU\Environment on Windows), enables run-at-login, wires Claude Code - /// integrations (statusline, guard, /sub, compact), and starts the interceptor. + /// HKCU\Environment on Windows), enables run-at-login, wires Claude Code `/sub`, + /// and starts the interceptor. /// Idempotent — re-running reuses the same port and won't restart a healthy daemon. /// No sudo. Setup { @@ -187,9 +187,10 @@ enum Commands { }, /// Bring this machine to the recommended current state /// - /// Idempotent: rewrites owned Claude Code hooks/statusline when stale, installs - /// recommended integrations you have not opted out of, and restarts a version-skewed - /// daemon. Same engine `setup` and `update` use — the one verb after a release. + /// Idempotent: rewrites owned Claude Code `/sub` (and a previously installed statusline + /// or guard) when stale, installs `/sub` / subagents you have not opted out of, and + /// restarts a version-skewed daemon. Same engine `setup` and `update` use — the one verb + /// after a release. Does not install statusline, guard, or cheaper `/compact`. Ensure { /// Suppress the summary panel (for scripts / postinstall hooks). #[arg(long, short = 'q')] @@ -307,20 +308,20 @@ enum Commands { #[command(subcommand)] action: Option, }, - /// Render Claude Code's custom status line (hook entrypoint; prefer `llmtrim ensure`) + /// Render Claude Code's custom status line (deprecated; opt-in) /// /// With no subcommand, reads Claude Code's JSON session blob on stdin and prints one - /// elegant line. `install` / `uninstall` are power-user escape hatches — `setup`, - /// `update`, and `ensure` keep the status line current automatically. + /// elegant line. Not installed by `setup` / `ensure`. `install` wires it; `ensure` only + /// refreshes an already-owned line. #[command(hide = true)] Statusline { #[command(subcommand)] action: Option, }, - /// Cold-cache resume warning hook (prefer `llmtrim ensure`) + /// Cold-cache resume warning hook (deprecated; opt-in) /// - /// With no subcommand, acts as Claude Code's `UserPromptSubmit` hook. `install` / - /// `uninstall` are escape hatches — `setup` / `update` / `ensure` wire this for you. + /// With no subcommand, acts as Claude Code's `UserPromptSubmit` hook. Not installed by + /// `setup` / `ensure`. `install` wires it; `ensure` only refreshes an already-owned hook. #[command(hide = true)] Guard { #[command(subcommand)] diff --git a/crates/llmtrim-cli/src/setup.rs b/crates/llmtrim-cli/src/setup.rs index bc033b96..e67a83c6 100644 --- a/crates/llmtrim-cli/src/setup.rs +++ b/crates/llmtrim-cli/src/setup.rs @@ -540,9 +540,10 @@ pub fn run(requested: Option, force: bool) -> Result<()> { Err(e) => rows.push((ui::WARN, "Autostart".into(), format!("not enabled: {e}"))), } - // 3b. Claude Code integrations + tray: one ensure pass (statusline, guard, /sub, compact, - // tray autostart). Idempotent; honors remembered opt-outs. `--force` / non-TTY take - // recommended defaults without asking. + // 3b. Claude Code integrations + tray: one ensure pass (`/sub`, subagents, tray autostart). + // Statusline, guard, and cheaper `/compact` are not first-installed (deprecated). + // Idempotent; honors remembered opt-outs. `--force` / non-TTY take recommended + // defaults without asking. let ensure_report = crate::ensure::apply(crate::ensure::Options { interactive: !force && std::io::IsTerminal::is_terminal(&std::io::stdin()), quiet: true, @@ -662,8 +663,7 @@ pub fn run(requested: Option, force: bool) -> Result<()> { " {} llmtrim status", ui::paint(color, Tone::Dim, "watch savings") ); - // Claude Code integrations (statusline, guard, /sub, compact) are applied by ensure above — - // no separate install homework. + // Claude Code `/sub` is applied by ensure above — no separate install homework. #[cfg(windows)] println!( "{}", diff --git a/crates/llmtrim-cli/src/statusline.rs b/crates/llmtrim-cli/src/statusline.rs index bdab5d4d..e256df31 100644 --- a/crates/llmtrim-cli/src/statusline.rs +++ b/crates/llmtrim-cli/src/statusline.rs @@ -898,8 +898,7 @@ pub fn run() -> Result<()> { // ── install / uninstall (wire ~/.claude/settings.json) ──────────────────────────── /// Whether Claude Code appears to be installed (its `~/.claude` config dir exists). Used by -/// `setup` to hint at the status line for Claude Code users only, not users of other agents — -/// setup itself is client-agnostic and never writes this file. +/// `ensure` to decide whether to wire `/sub` (and to refresh a previously owned status line). pub fn claude_code_present() -> bool { claude_settings_path() .ok() @@ -1095,7 +1094,7 @@ pub enum OwnedStatus { Foreign, } -/// Whether ensure should install / refresh the status line. +/// Whether this binary owns the current `statusLine` (Missing is not first-installed by ensure). pub fn owned_status() -> OwnedStatus { let Ok(path) = claude_settings_path() else { return OwnedStatus::Missing; diff --git a/crates/llmtrim-core/src/config.rs b/crates/llmtrim-core/src/config.rs index 99d0ec15..62032a60 100644 --- a/crates/llmtrim-core/src/config.rs +++ b/crates/llmtrim-core/src/config.rs @@ -1496,7 +1496,7 @@ pub fn write_compact_models(models: &[String]) -> Result<()> { } /// Whether the config explicitly contains `[compact].models`, including an empty list used to -/// remember that setup's recommendation was declined. +/// remember a prior opt-out. `setup` / `ensure` no longer write this key. pub fn compact_models_configured() -> bool { let Some(path) = config_path() else { return false; diff --git a/scripts/test-ensure-sandboxes.sh b/scripts/test-ensure-sandboxes.sh index da3e96fe..1f2dfe5e 100755 --- a/scripts/test-ensure-sandboxes.sh +++ b/scripts/test-ensure-sandboxes.sh @@ -102,7 +102,7 @@ with_sandbox s1 with-claude -- bash -c ' exit 0 ' && ok "s1 ensure + opt-out" || bad "s1 ensure + opt-out" -# ── Step 2a: setup wires integrations ──────────────────────────────────────── +# ── Step 2a: setup wires /sub, not deprecated extras ──────────────────────── with_sandbox s2a with-claude -- bash -c ' set -e '"$BIN"' setup --force /tmp/llmtrim-setup-out.$$ 2>&1 || { @@ -111,27 +111,26 @@ with_sandbox s2a with-claude -- bash -c ' } rm -f /tmp/llmtrim-setup-out.$$ test -f "$LLMTRIM_HOME/integrations.json" - jq -e ".statusLine.command | test(\"statusline\")" "$HOME/.claude/settings.json" >/dev/null - jq -e ".statusLine.refreshInterval == 300" "$HOME/.claude/settings.json" >/dev/null - jq -e ".. | strings | select(test(\"guard\"))" "$HOME/.claude/settings.json" >/dev/null + jq -e "has(\"statusLine\") | not" "$HOME/.claude/settings.json" >/dev/null + if jq -e ".. | strings | select(test(\" guard$\"))" "$HOME/.claude/settings.json" >/dev/null 2>&1; then + exit 1 + fi test -f "$HOME/.claude/skills/sub/SKILL.md" grep -q "llmtrim-owned-window-sub" "$HOME/.claude/skills/sub/SKILL.md" - # compact configured somewhere under XDG found=0 while IFS= read -r -d "" f; do if grep -q "\[compact\]" "$f" 2>/dev/null; then found=1; break; fi done < <(find "$XDG_CONFIG_HOME" -name "config.toml" -print0 2>/dev/null || true) - # also check HOME/.config while IFS= read -r -d "" f; do if grep -q "\[compact\]" "$f" 2>/dev/null; then found=1; break; fi done < <(find "$HOME" -name "config.toml" -print0 2>/dev/null || true) - test "$found" = 1 -' && ok "s2a setup wires integrations" || bad "s2a setup wires integrations" + test "$found" = 0 +' && ok "s2a setup wires /sub only" || bad "s2a setup wires /sub only" -# ── Step 2b: stale statusline rewrite ──────────────────────────────────────── +# ── Step 2b: stale statusline rewrite (already owned) ──────────────────────── with_sandbox s2b with-claude -- bash -c ' set -e - '"$BIN"' ensure -q /dev/null jq ".statusLine = { \"type\": \"command\", \"command\": \"/old/cellar/llmtrim statusline\", @@ -144,14 +143,13 @@ with_sandbox s2b with-claude -- bash -c ' jq -e ".statusLine.refreshInterval == 300" "$HOME/.claude/settings.json" >/dev/null ' && ok "s2b stale statusline rewrite" || bad "s2b stale statusline rewrite" -# ── Step 3: ensure installs statusline; foreign left alone ─────────────────── +# ── Step 3: ensure does not first-install statusline; foreign left alone ───── with_sandbox s3a with-claude -- bash -c ' set -e echo "{}" >"$HOME/.claude/settings.json" '"$BIN"' ensure -q /dev/null - jq -e ".statusLine.command | test(\"statusline\")" "$HOME/.claude/settings.json" >/dev/null -' && ok "s3a statusline install" || bad "s3a statusline install" + jq -e "has(\"statusLine\") | not" "$HOME/.claude/settings.json" >/dev/null +' && ok "s3a statusline not first-installed" || bad "s3a statusline not first-installed" with_sandbox s3b with-claude -- bash -c ' set -e @@ -172,7 +170,7 @@ with_sandbox s4 with-claude -- bash -c ' set -e test "$d1" -ne 0 '"$BIN"' doctor --fix /dev/null 2>&1 || true - jq -e ".statusLine.command | test(\"statusline\")" "$HOME/.claude/settings.json" >/dev/null + jq -e "has(\"statusLine\") | not" "$HOME/.claude/settings.json" >/dev/null test -f "$HOME/.claude/skills/sub/SKILL.md" ' && ok "s4 doctor --fix" || bad "s4 doctor --fix" @@ -226,8 +224,10 @@ with_sandbox s8a no-claude -- bash -c ' with_sandbox s8b with-claude -- bash -c ' set -e '"$BIN"' ensure /dev/null - jq -e ".statusLine.refreshInterval == 300" "$HOME/.claude/settings.json" >/dev/null - jq -e ".. | strings | select(test(\"guard\"))" "$HOME/.claude/settings.json" >/dev/null + jq -e "has(\"statusLine\") | not" "$HOME/.claude/settings.json" >/dev/null + if jq -e ".. | strings | select(test(\" guard$\"))" "$HOME/.claude/settings.json" >/dev/null 2>&1; then + exit 1 + fi test -f "$HOME/.claude/skills/sub/SKILL.md" test -f "$LLMTRIM_HOME/integrations.json" # update reminder path (package channel) should not crash