Skip to content

fix(stella-cli): the settings scope merge silently drops Stop hooks and upstream_pin (#3243 Phase 0) - #3275

Open
macanderson wants to merge 3 commits into
mainfrom
feat/3243-p0-settings-overlay-safety
Open

fix(stella-cli): the settings scope merge silently drops Stop hooks and upstream_pin (#3243 Phase 0)#3275
macanderson wants to merge 3 commits into
mainfrom
feat/3243-p0-settings-overlay-safety

Conversation

@macanderson

@macanderson macanderson commented Aug 14, 2026

Copy link
Copy Markdown
Owner

Phase 0 of #3243 — "make the flag safe to add", plus the three live defects it turned up. No new behavior; three keys that were supposed to work now do.

The defect class

A settings key is four edits in lockstep: the struct field, Settings::overlay_scope, unknown::ROOT_FIELDS, and the TOML document. Miss the overlay and the key parses in all three scopes, merges to the default, and configures nothing — no parse error, no warning, and no failing test, because a field's accessor tests call it on a directly-deserialized Settings and never on a merged one.

enable_recap shipped inert for exactly this reason; the apology is still in merge.rs, alongside six comments asking future authors to remember. Prose is not a guard. It did not stop the next three.

What was broken

1. Stop and PreCompact hooks were unreachable from configuration. concat_hooks joined three of Hooks' five events. Every scope parsed a Stop matcher and the merge discarded it, so stop_hook_feedback's completion gate — the mechanism #3246's whole P1 rests on — could not be switched on from any settings file. The #2684 witnesses did not catch it because they build Hooks with serde_json::from_str and never merge a scope: the identical blind spot that let enable_recap ship.

2. providers.<id>.upstream_pin merged to None in all three scopes. ProviderSettings::overlay copied eight fields and omitted it, so the gateway pin that keeps two benchmark runs comparable was silently unset — the failure mode being that a head-to-head looks pinned and isn't.

3. The same key warned as a typo. unknown::PROVIDER_FIELDS omitted it too, so spelling it correctly earned "unrecognized key ignored — check the spelling".

All three are still present on main at the tip this branch rebases onto; see the witness section.

The guard

crates/stella-cli/src/settings/completeness.rs. The ledgers destructure Settings, Hooks, and ProviderSettings exhaustively, with no .. rest pattern, so adding a field stops the file compiling until its author declares what the merge does with it — a compile error, not a red test, which is the discipline stella-protocol's event-consumer table already uses (E0004).

Each field declares a posture:

  • Mergedoverlay_scope must carry it. Dropped ⇒ the enable_recap defect.
  • ManagedOnly — assigned by merge_captured_scopes from the managed snapshot alone. enterprise_telemetry is the only one, and the test asserts overlay_scope does not carry it, so a lower scope cannot forge an org enrollment.
  • Computed#[serde(skip)], derived by load.

The unrecognized-key vocabulary is checked both ways: a missing entry reports a correct key as a typo, and a stale entry silences a real one.

The scope document (EVERY_KEY) is deserialized rather than struct-literal'd on purpose — it exercises the path a real settings.json takes, and the populated assertion keeps it honest: a key set to its own default fails as a test bug rather than passing vacuously.

Witness

Each test fails on main and passes here. Verified against the current tip (eb427c2da), not just the tip I started from:

$ cargo test -p stella-cli --bin stella settings::completeness   # on main's behavior
every_hook_event_survives_the_scope_merge      FAILED
  `hooks.Stop` is dropped by `concat_hooks`, so a matcher declared in any
  settings scope never reaches the runtime.
every_provider_field_survives_the_entry_overlay FAILED
  `providers.<id>.upstream_pin` is dropped by `ProviderSettings::overlay`
the_unknown_key_vocabulary_matches_the_structs  FAILED
  `ProviderSettings` has a field `upstream_pin` that the vocabulary does not list

and confirmed the defects are on the tip, independent of the tests:

$ git show origin/main:crates/stella-cli/src/settings/merge.rs | rg "join\(&mut target"
join(&mut target.session_start, ...)   # three lines, no stop, no pre_compact
$ git show origin/main:crates/stella-cli/src/settings/unknown.rs | rg upstream_pin
(absent)

After the fix: cargo test -p stella-cli --bin stella settings::117 passed, 0 failed.

Note the whole-Settings test passes on base while the per-event one fails — hooks is merged, just incompletely. That asymmetry is why the per-event ledger exists as its own test rather than a row in the first.

Trust-side decision (#3243 Phase 0's third item)

Recorded in merge.rs rather than left to the next reader: context may carry the steering switch, because every knob in that block selects among sources already gated one tier down (context_providers is trust-restored, custom tools ride project_custom_tools_allowed, hooks ride trust.hooks) — so a knob deciding whether to consult them grants no authority the repository does not already have. The comment also names the condition that flips the answer: a steering source whose selection is the act of execution, i.e. the plugin source #3246 sequences last. That move belongs in the PR that adds plugins, not in a comment written in advance.

Scope

stella-cli only; no production code path changes shape. Ran cargo test -p stella-cli --bin stella settings:: and cargo fmt. I did not run the workspace suite locally — CI is the check for that.

No tests deleted or renamed.

Refs #3243, #3246

Summary by Sourcery

Enforce completeness of stella-cli settings merging and unknown-key handling, fixing previously inert configuration keys and adding a compile-time guard for future fields.

Bug Fixes:

  • Ensure Stop and PreCompact hook events are preserved when merging hook scopes so their matchers reach runtime.
  • Ensure providers..upstream_pin participates in ProviderSettings overlays instead of silently merging to None.
  • Include upstream_pin in the provider unknown-key vocabulary so correctly spelled keys are no longer reported as typos.

Enhancements:

  • Introduce a merge completeness test module that exhaustively validates Settings, Hooks, and ProviderSettings fields against overlays and unknown-key vocabularies, turning missing wiring into compile-time/test failures.
  • Clarify trust and steering behavior for context-related settings in merge.rs comments to document the boundary for future plugin-based execution sources.

Tests:

  • Add comprehensive settings completeness tests that deserialize an EVERY_KEY document, assert all merged fields survive scope overlays, verify each hook event is concatenated across scopes, ensure provider entries overlay all fields, and confirm unknown-key vocabularies match the structs.

…nd upstream_pin

A settings key is four edits in lockstep: the struct field, the scope
overlay, the unrecognized-key vocabulary, and the TOML document. Miss the
overlay and the key parses in every scope, merges to the default, and
configures nothing — no parse error, no warning, no failing test, because a
field's accessor tests call it on a directly-deserialized `Settings` and
never on a merged one. `enable_recap` shipped inert for exactly this reason
and `merge.rs` carries six comments begging future authors to remember.

Prose is not a guard, and the six comments did not stop the next three:

- `concat_hooks` joined three of `Hooks`' five events, so a `Stop` or
  `PreCompact` matcher declared in ANY settings file never reached the
  runtime. `stop_hook_feedback`'s completion gate was unreachable from
  configuration entirely. The #2684 witnesses missed it because they build
  `Hooks` with `serde_json::from_str` and never merge a scope.
- `ProviderSettings::overlay` dropped `upstream_pin`, so
  `providers.<id>.upstream_pin` merged to `None` in all three scopes and the
  gateway pin a comparable head-to-head depends on was silently unset.
- `unknown::PROVIDER_FIELDS` omitted the same key, so spelling it correctly
  earned a "possible typo" warning.

Adds `settings::completeness`, which makes the next one a compile error
rather than a silent `None`: the ledgers destructure `Settings`, `Hooks` and
`ProviderSettings` exhaustively with no `..` rest pattern, so a new field
stops the file compiling until its author declares what the merge does with
it — the `E0004`-over-a-red-test discipline `stella-protocol`'s
event-consumer table already uses. The vocabulary is checked in both
directions, because a stale entry silences a real typo just as a missing one
invents one.

Also records the trust-side decision #3243 asked for: `context` may carry
the steering switch because every knob in it selects among sources already
gated one tier down, and names the condition that would change that answer.

Refs #3243, #3246
@vercel

vercel Bot commented Aug 14, 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 14, 2026 8:53pm

@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

@sourcery-ai

sourcery-ai Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds a compile-time completeness guard for stella-cli settings merges, fixes missing merge/unknown-key wiring for hooks and provider upstream_pin, and documents a trust decision around context steering in settings merging.

Sequence diagram for settings scope merge and hooks/provider overlay

sequenceDiagram
    actor Operator
    participant SettingsLoader
    participant Settings
    participant ProviderSettings
    participant Hooks

    Operator->>SettingsLoader: load_settings
    SettingsLoader->>Settings: load
    SettingsLoader->>Settings: overlay_scope(scope)
    activate Settings
    Settings->>ProviderSettings: overlay(entry)
    Settings->>Hooks: concat_hooks(base_hooks, scope.hooks)
    deactivate Settings

    note over ProviderSettings,Hooks: overlay now carries upstream_pin and all hook events (including stop, pre_compact)
Loading

File-Level Changes

Change Details Files
Ensure all hook events and provider fields participate correctly in settings scope merges and overlays, preventing configuration keys from silently becoming no-ops.
  • Extend concat_hooks to merge Stop and PreCompact events alongside existing hook events.
  • Expose Settings::overlay_scope within the module and document its role and failure mode when fields are omitted.
  • Update ProviderSettings::overlay to include upstream_pin in the per-field overlay process.
crates/stella-cli/src/settings/merge.rs
crates/stella-cli/src/settings.rs
Align unknown-key vocabularies with the Settings and ProviderSettings structs and expose them for testing.
  • Make ROOT_FIELDS and PROVIDER_FIELDS public within the module to be consumable by tests.
  • Add upstream_pin to PROVIDER_FIELDS so it is recognized as a valid provider key and not warned as a typo.
  • Document in comments that ROOT_FIELDS and PROVIDER_FIELDS are checked bidirectionally against the structs by the completeness tests.
crates/stella-cli/src/settings/unknown.rs
Introduce exhaustive completeness tests that enforce merge/overlay coverage and vocabulary alignment for all settings-related structs.
  • Add a new completeness test module that constructs a fully-populated Settings instance from JSON (EVERY_KEY) and verifies all Posture::Merged fields survive overlay_scope.
  • Define ledgers for Settings, Hooks, and ProviderSettings that exhaustively destructure structs without rest patterns and classify fields by posture (Merged, ManagedOnly, Computed).
  • Add tests to ensure each hook event and provider field survives merges/overlays and that unknown-key vocabularies (ROOT_FIELDS, PROVIDER_FIELDS) match the structs in both directions.
crates/stella-cli/src/settings/completeness.rs
crates/stella-cli/src/settings.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

merge.rs and unknown.rs carry intra-doc links to `super::completeness`,
but that module was gated `#[cfg(test)]` only. rustdoc does not set
`cfg(test)` while building docs (`make doc-warnings` runs
`cargo doc --document-private-items` with RUSTDOCFLAGS="-D warnings"),
so both links resolved to nothing and failed the doc-warnings gate:

  error: unresolved link to `super::completeness`
    --> crates/stella-cli/src/settings/merge.rs:46:7
    --> crates/stella-cli/src/settings/merge.rs:165:31
    --> crates/stella-cli/src/settings/unknown.rs:45:7
    --> crates/stella-cli/src/settings/unknown.rs:66:29

Gating on `cfg(any(test, doc))` instead keeps the module out of
production and release builds while making it visible to `cargo doc`,
which is the standard fix for a doc-only-visible cfg(test) item.

Also merges origin/main to pick up #3276, which independently fixed
the duplicate `use std::sync::Mutex;` import in
crates/stella-core/src/driver/restore.rs (composed from #3265 and
#3271) that was the actual cause of the "fmt + clippy + test" CI
failure on this branch.

Verified locally:
  cargo fmt --all --check
  cargo clippy -p stella-core -p stella-cli --all-targets -- -D warnings
  RUSTDOCFLAGS="-D warnings" cargo doc -p stella-cli --no-deps \
    --document-private-items --keep-going
  cargo test -p stella-core
  cargo test -p stella-cli --bin stella   (1727 passed)
  cargo test -p stella-pipeline --test cache_correctness

Filed #3312 for a pre-existing, unrelated test flake noticed while
investigating (settings::tests::enable_recap_survives_the_scope_merge
and create_worktrees_survives_the_scope_merge race on the ambient
STELLA_MANAGED_SETTINGS env var under parallel test execution; not
introduced by this branch, whose diff never touches
crates/stella-cli/src/settings/tests.rs).

Refs #3312
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