Skip to content

feat(stella-cli): /reload, and a SETTINGS save that applies without a restart - #1992

Merged
macanderson merged 14 commits into
mainfrom
feat/deck-reload-command
Aug 7, 2026
Merged

feat(stella-cli): /reload, and a SETTINGS save that applies without a restart#1992
macanderson merged 14 commits into
mainfrom
feat/deck-reload-command

Conversation

@macanderson

@macanderson macanderson commented Aug 7, 2026

Copy link
Copy Markdown
Owner

What

Adds a /reload deck command, and makes a SETTINGS-tab save take effect in the
running session instead of waiting for a restart.

Config::reload_from_disk re-reads the settings scope chain (user + project,
managed ceiling folded in) and re-applies everything load_with_settings
derives 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. /model and the
SETTINGS tab remain the seam for that.

The interesting part: a reload cannot happen mid-turn

The first cut threaded &mut Config down to the deck's overlay handlers and
reloaded 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 turn
coroutine, which holds &Config and is actively reading the very fields a
reload 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 caller
re-derives at a safe boundary — the discipline /budget already follows with
pending_budget:

  • idle site — reload immediately; the next prompt sees it.
  • in-turn site — park it, and apply after the turn ends, right beside the
    parked /budget cap.

The delay is invisible in the UI: engine_config_inbound and
tool_policy_inbound both re-read the scope chain from disk already, so the
panels 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_budget uses a few lines above, which in turn
mirrors 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 after
the Config is built, calls reload_from_disk, and asserts both the recap
toggle and the bash switch flipped.

Verified the artisanal way: with reload_from_disk's body replaced by
Ok(()), 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, no
unsafe, 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 silently
reading my own ~/.stella/settings.json.

File-size guard

command_deck.rs is a god file closed to growth, so none of this landed in it.
The SETTINGS overlay handlers and the /reload body moved out to
command_deck/settings_io.rs (the skills.rs / authoring.rs pattern), and
reload_from_disk lives in config/reload.rs rather than pushing config.rs
(1498 on main) over the ceiling.

Net effect: command_deck.rs shrinks 4621 → 4566, which is the single line
the 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_disk assigned six self fields before settings.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 reload
leaves 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 self is touched, and the commit block is infallible, so ? can
only fire while self is still pristine. A phase comment states the rule, so a
future fallible getter lands above the commit block instead of rediscovering the
hazard. apply_pending_reload's doc now names where its coherence claim is
actually guaranteed, rather than assuming it.

Second witness
config::tests::a_failed_reload_leaves_every_field_untouched writes a
well-formed settings.json whose verifier_weight: 2.0 outranks the
deterministic weight (reward_policy() refuses by name rather than clamping),
then asserts the recap toggle and the bash switch are unmoved. Checked the
artisanal way: against the old interleaved body it fails on the first assertion
(a failed reload must not leave the recap toggle applied); against the split
it passes.

Both reload witnesses now share a reload_fixture helper, so the redirected
user home and the all-defaults Config are built once.

Not in this PR

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 in
    stella-cli; the only errors are stella-pipeline's pre-existing dead
    spend local (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-size fails only on
    the two inherited files named above.
  • Both reload witnesses re-run against the pre-fix body to confirm each one
    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:

  • Introduce a /reload deck command that re-reads settings from disk and reapplies them to the running session.
  • Allow SETTINGS tab saves for engine configuration and tool switches to take effect in the current session via deferred reloads at safe boundaries.

Enhancements:

  • Refactor SETTINGS overlay I/O handlers into a new command_deck::settings_io module to keep command_deck.rs within size limits.
  • Add Config::reload_from_disk as a focused mutation API for reapplying the settings scope chain to an existing configuration.

Documentation:

  • Document the new /reload command in the chat command reference, clarifying its effect and relationship to SETTINGS and model changes.

Tests:

  • Add a config reload test verifying that post-construction settings edits are reapplied to enable recap and disable tools as specified on disk.

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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @macanderson, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
stella-cli-docs Ignored Ignored Preview Aug 7, 2026 3:43am

@sourcery-ai

sourcery-ai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

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 boundaries

sequenceDiagram
    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
Loading

File-Level Changes

Change Details Files
Make deck sessions and commands operate on a mutable Config so settings can be reloaded in-place.
  • Changed run_deck_session signature and all call sites to take &mut Config instead of &Config.
  • Updated run_deck_command to accept &mut Config and pass it through where needed.
  • Ensured mutability is only used at safe boundaries (idle loop, post-turn) to avoid mutating Config during an in-flight turn.
crates/stella-cli/src/command_deck.rs
crates/stella-cli/src/main.rs
crates/stella-cli/src/command_deck/skills.rs
Introduce Config::reload_from_disk to re-derive engine, tools, authority, and related settings from the current scope chain without restarting.
  • Added config/reload.rs with Config::reload_from_disk implementation using Settings::load and trusted_engine_config_override.
  • Hooked reload module into config.rs via a new mod reload declaration.
  • Ensured reload_from_disk leaves provider/model/credential resolution unchanged and reports load/parse errors via String.
crates/stella-cli/src/config/reload.rs
crates/stella-cli/src/config.rs
Refactor SETTINGS tab engine/tools handlers into a new settings_io module and add a safe, deferred reload flow for SETTINGS saves.
  • Moved handle_engine_config_input and handle_tools_input from command_deck.rs into command_deck/settings_io.rs, keeping inbound builders in the parent module.
  • Extended handlers to accept a mutable stale flag that is set on successful saves, while still responding with refreshed inbound snapshots.
  • Introduced apply_pending_reload to perform Config::reload_from_disk at a shared site and report failures via chrome_note, and wired it at idle and post-turn boundaries using pending_settings_reload/settings_stale flags.
crates/stella-cli/src/command_deck.rs
crates/stella-cli/src/command_deck/settings_io.rs
Add a /reload deck command and document it, ensuring SETTINGS saves and manual reloads keep the UI in sync.
  • Registered /reload in DECK_BUILTINS with a brief description.
  • Implemented settings_io::reload_command to invoke Config::reload_from_disk, refresh the ENGINE overlay snapshot, and return a user-facing status line.
  • Added /reload documentation to the chat command docs describing what is reloaded and what is intentionally left untouched.
crates/stella-cli/src/command_deck.rs
crates/stella-cli/src/command_deck/skills.rs
website/content/docs/commands/chat.mdx
Add a witness test proving reload_from_disk re-applies the settings scope chain using test-local user paths.
  • Added reload_from_disk_reapplies_the_settings_scope_chain test constructing a Config, writing a temporary user settings.json, and asserting recap and tool policy changes after reload.
  • Used crate::paths::test_user_home and crate::test_env::lock to isolate test settings and env from the developer’s real home and other tests.
  • Cleaned up temporary directories after the test run.
crates/stella-cli/src/config/tests.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Comment thread crates/stella-cli/src/config/reload.rs Outdated
@macanderson

Copy link
Copy Markdown
Owner Author

Heads-up: the merge conflict on this PR is already resolvedda28ab19
merged current main (6c345532), and origin/main is now an ancestor of this
head. GitHub reported CONFLICTING for a while only because it had not
recomputed after #1994 merged; it now reads MERGEABLE.

What is still blocking it is not this PR's diff. The file size ratchet
check fails here:

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)

This branch is byte-identical to main for scripts/file-size-baseline.txt
and for both of those files:

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                      # empty

and the same guard fails on a branch cut straight from origin/main with no
other change applied. So main itself is red, from a baseline skew between #1979
and #1962 — each regenerated the baseline against a snapshot of main lacking
the other's growth.

Fix is up in #2003 (regenerated baseline; file size ratchet passes there).
Once that lands, merge main in here and this check should clear. The structural
cause — the baseline being one shared cell every growing PR must write — is filed
as #2004.

One thing worth checking when you next touch this branch: the original diff on
this PR included a scripts/file-size-baseline.txt edit (1 insertion, 1 deletion)
and the merge dropped it, presumably resolved in main's favour. That is
harmlesscommand_deck.rs shrank 4621 → 4566 from the settings_io.rs
extraction, and the guard only enforces the ceiling direction, so a shrink never
fails. Worth knowing it was dropped rather than assuming it survived.

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.
@macanderson
macanderson merged commit 49315b9 into main Aug 7, 2026
11 of 12 checks passed
@macanderson
macanderson deleted the feat/deck-reload-command branch August 7, 2026 03:43
macanderson added a commit that referenced this pull request Aug 7, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant