feat(stella-cli): /reload, and a SETTINGS save that applies without a restart - #1992
Conversation
A new /reload deck command re-reads the settings scope chain (user + project, managed ceiling folded in) via Config::reload_from_disk and re-applies everything it derives — engine posture, tool policy, authority, recap/trace/reward/worktree switches — to the live session. Provider/model/credential resolution is deliberately untouched. Saving from the SETTINGS tab or the tools overlay now reloads the live config immediately, closing the save-then-restart surprise.
…and_deck.rs, witness /reload command_deck.rs is closed to growth and the /reload feature put it 37 lines over its retightened ceiling; move handle_engine_config_input and handle_tools_input to command_deck/settings_io.rs (the skills.rs / authoring.rs pattern). Add the witness test for Config::reload_from_disk: a settings edit made after the Config was resolved flips the recap toggle and the bash tool switch without a restart.
…ghten the file-size baseline reload_from_disk pushed config.rs (1498 on main) over the 1500 ceiling, and the baseline accepts no new entries — the mutation gets its own submodule instead. Regenerating the baseline also drops the stale stella-protocol/src/event.rs entry (its tests moved to event/tests.rs on main, leaving the file at 1454) and retightens command_deck.rs and the pipeline entries; AGENTS.md's god-file table and the stella-protocol README follow the baseline, as check-god-files requires.
… cfg mutably Threading `&mut Config` into the deck did not compile: the in-turn recv site sits in the same select as the turn coroutine, which holds `&Config` and reads the very fields a reload rewrites (tool policy, authority, engine posture). Rust was right — reloading there tears config out from under a running turn. The handlers now report `stale` and the caller re-derives at a safe boundary, the discipline `/budget` already follows with pending_budget: immediately at the idle site, and after the turn ends mid-turn. Both panels re-read the scope chain from disk, so the delay is invisible in the UI; only subsequent turns depend on the live Config. Also fixes the witness, which read the developer's real ~/.stella because UserPaths::test_default keeps the ambient home — it now uses the thread-local paths seam (#1139).
command_deck.rs is closed to growth: the parked-reload plumbing pushed it back over its ceiling, so the command body follows the panel handlers into the submodule. Net against main the file shrinks 4621 -> 4566, which is the single line the regenerated baseline now carries.
# Conflicts: # crates/stella-protocol/README.md
There was a problem hiding this comment.
Sorry @macanderson, you have reached your weekly rate limit of 500000 diff characters.
Please try again later or upgrade to continue using Sourcery
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Reviewer's Guide/reload slash command and SETTINGS saves now reload the live Config safely at turn boundaries, with settings I/O split out of command_deck into a new settings_io module and a dedicated Config::reload_from_disk implementation. Sequence diagram for SETTINGS save and deferred Config reload at turn boundariessequenceDiagram
actor User
participant Deck as run_deck_session
participant SettingsIO as settings_io
participant Config
participant TUI as Inbound
User->>Deck: WorkspaceInput::EngineConfigSave
Deck->>SettingsIO: handle_engine_config_input(input, cfg, stale, in_tx)
SettingsIO->>SettingsIO: engine.save_to(path)
SettingsIO->>TUI: engine_config_inbound(cfg, Some(status))
SettingsIO-->>Deck: stale = true
alt [no turn in flight]
Deck->>Config: apply_pending_reload(cfg, in_tx)
Config->>Config: reload_from_disk()
Config->>TUI: engine_config_inbound(cfg, None)
else [turn in flight]
Deck->>Deck: pending_settings_reload = true
Deck-->>User: continue turn with old cfg
Deck->>Deck: turn ends
Deck->>Deck: if pending_settings_reload
Deck->>Config: apply_pending_reload(cfg, in_tx)
Config->>Config: reload_from_disk()
Config->>TUI: chrome_note(...)
end
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Heads-up: the merge conflict on this PR is already resolved — What is still blocking it is not this PR's diff. The This branch is byte-identical to git diff origin/main HEAD -- scripts/file-size-baseline.txt # empty
git diff origin/main HEAD -- crates/stella-core/src/driver.rs \
crates/stella-pipeline/src/pipeline/tests.rs # emptyand the same guard fails on a branch cut straight from Fix is up in #2003 (regenerated baseline; One thing worth checking when you next touch this branch: the original diff on |
reload_from_disk assigned six Config fields before settings.reward_policy()? could fail, so a scope chain that parsed but did not resolve left the live session on a posture no settings file ever produced — new tool policy, old authority — while both callers in command_deck::settings_io told the user the reload had failed and the previous values were kept. Split into derive-then-commit: every fallible call now runs into a local before self is touched, and the commit block is infallible. Witness: config::tests::a_failed_reload_leaves_every_field_untouched writes a well-formed settings.json whose verifier_weight outranks the deterministic weight (the one fallible step downstream of the load) and asserts the recap toggle and bash switch are unmoved. Fails on the interleaved body, passes on the split. Reported by the Vercel review bot on #1992.
…orktree-pr1992-feedback
…kew, a dead binding, and a broken doc link (#2003) ## What & why `main` is red on **three independent gate steps**, so every open PR inherits failures it did not cause. This PR is the smallest reviewable change that turns all three green. None of them is reachable from the others, which is why they are here together rather than in three PRs — landing one still leaves `main` red. | Step | Failure | Cause | |---|---|---| | `file size ratchet` | 2 god files +1 over ceiling | baseline skew between #1979 and #1962 | | `cargo clippy -D warnings` | dead `spend` binding | leftover from the per-iteration reborrow refactor | | `cargo doc -D warnings` | unresolved intra-doc link | item re-exported at the crate root, link unqualified | ### 1. `file size ratchet` — a parallel-merge baseline skew ``` crates/stella-core/src/driver.rs grew to 2572 lines, over its baseline ceiling of 2571 (+1) crates/stella-pipeline/src/pipeline/tests.rs grew to 2537 lines, over its baseline ceiling of 2536 (+1) ``` **Neither contributing PR did anything wrong.** #1979 added three lines to `driver.rs` and #1962 added a line to `pipeline/tests.rs`, and each regenerated `scripts/file-size-baseline.txt` on top of a `main` that did not yet carry the other's growth. Both were green on their own merge commits; the *composition* is red. This is the repository's most common cause of a red `main`, and it is why the baseline is generated rather than edited — hand-patching the two visible numbers fixes today's symptom and leaves the next skew just as invisible. Regenerated with `make file-size-update` on top of current `main` — the whole file, not the failing lines — so the result is reproducible by re-running the command rather than a set of numbers someone picked. The diff moves in **both** directions, and the tightenings are the larger half: | File | Before | After | Δ | |---|---|---|---| | `crates/stella-core/src/bus.rs` | 2126 | 1891 | **−235** | | `crates/stella-pipeline/src/pipeline.rs` | 3451 | 3181 | **−270** | | `crates/stella-core/src/driver.rs` | 2571 | 2572 | +1 | | `crates/stella-pipeline/src/pipeline/tests.rs` | 2536 | 2537 | +1 | The two reductions are #1994's `bus/names.rs` split and the `pipeline.rs` extraction finally reaching the ledger — work the same skew had been hiding. The ratchet had been holding those two ceilings **505 lines looser than the tree actually needs**, which is the direction that quietly readmits bloat. The two raises are **+1 apiece against code already on `main` behind a review** — precisely the irreducible case the escape hatch is documented for (AGENTS.md § "God files"). No new baseline entry, and no file grandfathered that was not already. No entry dropped below the 1500-line limit either, so the god-file tables in `AGENTS.md` and the crate READMEs are untouched. ### 2. `cargo clippy -D warnings` — a dead binding ``` error: variable does not need to be mutable error: unused variable: `spend` --> crates/stella-pipeline/src/pipeline/scope_stage.rs:34:13 ``` `let mut spend = Spend { budget, total };` is the binding's **only** occurrence in the file. The loop immediately below deliberately constructs a *fresh* reborrow on each iteration, and its own comment says why: > Reborrowed per iteration: the loop replans after a rejected scope card, and a > moved `Spend` could not be handed to the next attempt. So the outer binding is leftover from that refactor, not a value the loop shadows. **Deleted rather than underscore-prefixed** — `_spend` would keep a dead constructor alive and read as deliberate to the next person. ### 3. `cargo doc -D warnings` — an unqualified intra-doc link ``` error: unresolved link to `CompactionRewrite` --> crates/stella-protocol/src/event.rs:545:53 ``` The type is re-exported at the crate root (`lib.rs:75`) but is not in the `event` module's scope. The field two lines below the doc comment already spells it `crate::CompactionRewrite`; only the link was unqualified. Qualified it to match, so the link and the field now name the same item by the same path. ## The witness - [ ] This PR includes a witness test Not applicable, and deliberately so — this is a build-artifact regeneration plus two one-line corrections to already-reviewed code, with no behavior change: the deleted binding had no reader, and a doc link is not code. **The gate steps are the test**, and all three flip fail → pass. The ratchet flip was verified locally on a branch cut straight from `origin/main` with no other change applied: ``` $ bash scripts/check-file-size.sh # before check-file-size: FAILED crates/stella-core/src/driver.rs grew to 2572 lines, over its baseline ceiling of 2571 (+1) crates/stella-pipeline/src/pipeline/tests.rs grew to 2537 lines, over its baseline ceiling of 2536 (+1) $ bash scripts/check-file-size.sh # after check-file-size: OK — 1129 Rust/Python/shell files, none over 1500 lines except 30 grandfathered (none grew). $ bash scripts/check-god-files.sh check-god-files: OK — 22 god file(s) across 7 crate(s), named identically in AGENTS.md and every crate README. ``` That same probe branch is how the ratchet failure was isolated from PR #1992's diff — #1992 is byte-identical to `main` for both the baseline and both failing files, so the red was `main`'s, not its. **The clippy and rustdoc fixes were verified in CI, not locally, and that was a deliberate choice.** Three Terminal-Bench runs are live on this machine right now, including a Stella-vs-Claude-Code head-to-head. A full workspace clippy + rustdoc compile would have contended for CPU with a measured benchmark and skewed its wall-clock numbers. Per CLAUDE.md's "measure honestly" rule, a slower verification path is the correct trade against corrupting a benchmark this project reports in public. The cheap guards above compile nothing, which is why they were safe to run. ## Ground-rule check - [x] No new dependencies; no I/O added to `stella-core` - [x] No new outbound network calls - [x] Baseline **regenerated**, never hand-edited - [x] No new baseline entry; no ceiling raised beyond already-merged code - [x] No `#[allow]` used to silence either lint — both were real defects - [x] No behavior change, so no serde round-trip or parity-matrix impact ## Nothing left behind - [x] Filed: #2004 **#2004** — the ratchet has no defense against this skew, and this is its **third occurrence** (#1761, #1782, now this). Two PRs can each regenerate the baseline correctly against different snapshots of `main` and compose into a red tree; nothing detects it until the next push pays for it. The proposal is to make the guard judge *the change* rather than *the tree* — a file already over its ceiling at the merge base must not fail a PR that did not grow it. AGENTS.md already rejects the shared-cell design for `GATE_STEPS` counts (#1883) for exactly this reason; the baseline has the same shape and never got the same treatment. This PR deliberately does **not** attempt that fix. `main` is red right now, and an unbreak should be the smallest reviewable thing that turns it green. Related to #1986 — `ci.yml` does not run on a push to `main`, which is why all three of these survived on `main` rather than being caught at merge time. ## Anything reviewers should know? **This unblocks #1992**, the only other open PR, which is `MERGEABLE/BLOCKED` solely on these checks. Its own merge conflict is already resolved (`origin/main` is an ancestor of its head); GitHub had simply not recomputed the stale `CONFLICTING` flag. It will need `main` merged in after this lands.
What
Adds a
/reloaddeck command, and makes a SETTINGS-tab save take effect in therunning session instead of waiting for a restart.
Config::reload_from_diskre-reads the settings scope chain (user + project,managed ceiling folded in) and re-applies everything
load_with_settingsderives from it — engine posture, tool policy, authority, and the
recap/trace/reward/worktree switches — to the live
Config.Provider/model/credential resolution is deliberately not re-run: it needs
the full startup chain (interactive prompt included), and swapping provider
mid-session is a much larger step than a config refresh.
/modeland theSETTINGS tab remain the seam for that.
The interesting part: a reload cannot happen mid-turn
The first cut threaded
&mut Configdown to the deck's overlay handlers andreloaded inline. That does not compile, and the borrow checker was right on the
substance: the deck's in-turn recv site sits in the same
select!as the turncoroutine, which holds
&Configand is actively reading the very fields areload rewrites (tool policy, authority, engine posture). Reloading there tears
config out from under a running turn.
So the handlers no longer reload. They report
stale, and the callerre-derives at a safe boundary — the discipline
/budgetalready follows withpending_budget:parked
/budgetcap.The delay is invisible in the UI:
engine_config_inboundandtool_policy_inboundboth re-read the scope chain from disk already, so thepanels show what the files say regardless. Only subsequent turns depend on
the live
Config.Exemplar for the shape: this is the same "park the mutation, apply it at the
safe boundary" pattern
pending_budgetuses a few lines above, which in turnmirrors AGENTS.md invariant #6 ("budget aborts at safe boundaries only").
Witness test
config::tests::reload_from_disk_reapplies_the_settings_scope_chain— writes{"enable_recap": "on", "tools": {"bash": "off"}}to the user scope afterthe
Configis built, callsreload_from_disk, and asserts both the recaptoggle and the
bashswitch flipped.Verified the artisanal way: with
reload_from_disk's body replaced byOk(()), the test fails (reload must re-derive the recap toggle from the scope chain on disk); with the real body it passes.It redirects the user scope through the thread-local paths seam
(
paths::test_user_home, #1139) rather than$HOME— no env mutation, nounsafe, no cross-thread race. Worth noting for anyone writing a similar test:UserPaths::test_default()keeps the developer's real home(
..Self::from_environment()), so an earlier draft of this test was silentlyreading my own
~/.stella/settings.json.File-size guard
command_deck.rsis a god file closed to growth, so none of this landed in it.The SETTINGS overlay handlers and the
/reloadbody moved out tocommand_deck/settings_io.rs(theskills.rs/authoring.rspattern), andreload_from_disklives inconfig/reload.rsrather than pushingconfig.rs(1498 on main) over the ceiling.
Net effect:
command_deck.rsshrinks 4621 → 4566, which is the single linethe regenerated baseline carries.
Review feedback: a failed reload was not all-or-nothing
The Vercel review bot caught a real defect, now fixed.
reload_from_diskassigned sixselffields beforesettings.reward_policy()?— the only fallible step downstream of the load — could fail. That falsified an
invariant this PR itself documents on
apply_pending_reload: "A failed reloadleaves the session on its previous (still coherent) values."
The failure mode is worse than a torn write because it is silent. Both
callers tell the user the reload failed and the previous values were kept, while
the next turn actually runs under a hybrid posture — tool policy re-derived from
disk, authority and reward weights from session start — that no scope chain ever
produced.
The repair is a derive-then-commit split: every fallible call now runs into a
local before
selfis touched, and the commit block is infallible, so?canonly fire while
selfis still pristine. A phase comment states the rule, so afuture fallible getter lands above the commit block instead of rediscovering the
hazard.
apply_pending_reload's doc now names where its coherence claim isactually guaranteed, rather than assuming it.
Second witness —
config::tests::a_failed_reload_leaves_every_field_untouchedwrites awell-formed
settings.jsonwhoseverifier_weight: 2.0outranks thedeterministic weight (
reward_policy()refuses by name rather than clamping),then asserts the recap toggle and the
bashswitch are unmoved. Checked theartisanal way: against the old interleaved body it fails on the first assertion
(
a failed reload must not leave the recap toggle applied); against the splitit passes.
Both reload witnesses now share a
reload_fixturehelper, so the redirecteduser home and the all-defaults
Configare built once.Not in this PR
mainis red on two gates this branch does not touch, and four unbreakPRs are already open for them, so I deliberately did not add a fifth:
stella-core/src/driver.rs(2572 vs a ceiling of2571) and
stella-pipeline/src/pipeline/tests.rs(2537 vs 2536) are overthe baseline on
origin/mainitself, the parallel-merge skew. Covered byfix(repo,stella-pipeline,stella-protocol): unbreak main — file-size skew, a dead binding, and a broken doc link #2003, fix(stella-pipeline,scripts): unbreak main — two unbreak PRs fixed the same three things, plus the file-size ratchet blocking every PR #2008, fix(stella-pipeline): unbreak main — duplicated unbreak halves and baseline skew #2009.
spendlocal instella-pipeline/src/pipeline/scope_stage.rs. Covered by fix(stella-pipeline,stella-protocol,repo): unbreak main — five breaks stacked behind one clippy error #2000.Both are inherited:
cargo clippy -p stella-cli --all-targets -- -D warningsreports zero findings in a
stella-clifile, andcheck-file-sizenames onlythe two files above, neither of them this PR's.
This PR's earlier CI red was a stale base: the run tested a merge against
43402ae4, wherestella-pipeline's tests did not compile(
PassingShell/shell_call_resultmissing,ModelCallRole::Researchuncovered).
mainhas since repaired all three; the branch is merged up to6c345532.An open TOOLS panel keeps a stale render after
/reload(and after/model,pre-existing) — filed as Deck: an open TOOLS panel keeps a stale render after /reload and /model #1990 with the suggested
DeckCommandapproach,because an accurate row list needs the MCP-inclusive live stack that
run_deck_commanddoes not hold.Verification
cargo test -p stella-cli— 1463 + 12 integration targets, all passed, 0 failed.cargo clippy -p stella-cli --all-targets -- -D warnings— zero findings instella-cli; the only errors arestella-pipeline's pre-existing deadspendlocal (fix(stella-pipeline,stella-protocol,repo): unbreak main — five breaks stacked behind one clippy error #2000).cargo fmt -p stella-cli -- --check— clean.check-god-files,check-left-behind— OK.check-file-sizefails only onthe two inherited files named above.
genuinely flips fail → pass.
Refs #1990
Summary by Sourcery
Add live settings reload support, including a /reload deck command and automatic application of SETTINGS tab changes without restarting.
New Features:
Enhancements:
Documentation:
Tests: