Skip to content

fix: repair main — duplicate MemoryCmd broke the build, plus an obsolete baseline entry#739

Merged
macanderson merged 2 commits into
mainfrom
fix/main-ci-obsolete-baseline
Jul 26, 2026
Merged

fix: repair main — duplicate MemoryCmd broke the build, plus an obsolete baseline entry#739
macanderson merged 2 commits into
mainfrom
fix/main-ci-obsolete-baseline

Conversation

@macanderson

@macanderson macanderson commented Jul 26, 2026

Copy link
Copy Markdown
Owner

main is red, and in two ways. The second one is the real problem.

1. main does not compile

cargo check -p stella-cli fails with nine E0308s:

error[E0308]: mismatched types
    --> stella-cli/src/main.rs:1140:17
     = note: `MemoryCmd` and `memory_cmd::MemoryCmd` have similar names, but are actually distinct types

Cause: two PRs that each merged cleanly and are each individually correct.

Git merged both without a textual conflict. The result declares the enum twice: Command::Memory holds a memory_cmd::MemoryCmd, while the dispatch arms match on the stale crate::MemoryCmd.

This is the failure mode a textual merge is worst at — neither side touched the same lines, so nothing conflicted, and the incompatibility surfaces as a type error two files away from either change.

Fix, in the direction #733 intended: move #735's Compact and Index variants onto memory_cmd::MemoryCmd with their doc comments intact, delete the stale duplicate from main.rs, and qualify the dispatch arms.

2. An obsolete baseline entry

This is what CI reported first, and it masked the compile error — check-file-size runs before clippy and exits first.

#733 deleted main.rs's baseline entry when the file fell to 1337 lines; #735 re-added it at 1816. The file is now 1421 — simultaneously under the limit and exempted from it, which the ratchet rejects by design, since an exemption must not outlive the problem it covered.

Verification

Unblocks #736 and #738, which cannot get a meaningful CI signal against a main that does not build.

`main` is red. `scripts/check-file-size.sh` rejects a baseline entry for a
file that no longer needs one — "an exemption must not outlive the problem it
covered" — and `stella-cli/src/main.rs` is now 1421 lines with a 1816 ceiling
still on the books.

This is an interaction between two PRs that were in flight at the same time,
not a mistake in either. #733 moved the storage/scripts/graph commands out of
main.rs, took it to 1337, and *deleted* its baseline entry because the file no
longer crossed the limit. #735 landed separately, grew main.rs back to 1421,
and carried a baseline entry for it. Both were individually correct; together
they produce a file that is under the limit and exempted from it, which the
ratchet treats as an error by design.

Dropping the entry is the fix the script itself prescribes. main.rs stays
covered by the ordinary 1500-line limit with 79 lines of headroom.

check-file-size: OK — 456 Rust files, none over 1500 lines except 27
grandfathered (none grew).
@vercel

vercel Bot commented Jul 26, 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 Jul 26, 2026 10:46pm

@sourcery-ai

sourcery-ai Bot commented Jul 26, 2026

Copy link
Copy Markdown

🧙 Sourcery has finished reviewing your pull request!


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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

…ling

`main` does not build. `cargo check -p stella-cli` fails with nine E0308s:
"`MemoryCmd` and `memory_cmd::MemoryCmd` have similar names, but are actually
distinct types".

Cause: two PRs that each merged cleanly and are individually correct. #733
*moved* `MemoryCmd` into `memory_cmd.rs` to get `main.rs` under its size
ratchet. #735 was cut from a `main` that predated that move, so it carried its
own copy of the enum and added two new variants — `Compact` and `Index` — to
that copy. Git merged both without a textual conflict, and the result declares
the enum twice: `Command::Memory` holds a `memory_cmd::MemoryCmd`, while the
dispatch arms match on the stale `crate::MemoryCmd`.

This is the failure mode a textual merge is worst at: neither side touched the
same lines, so nothing conflicted, and the incompatibility is a type error two
files away from either change.

Fix, in the direction #733 intended:

- Move #735's `Compact` and `Index` variants onto `memory_cmd::MemoryCmd`,
  doc comments intact, with their arg types qualified through `crate::`.
- Delete the stale duplicate from `main.rs`.
- Qualify the dispatch arms.

Also drops the obsolete `main.rs` baseline entry, which is what CI reported
first: #733 deleted it when the file fell to 1337 lines, #735 re-added it at
1816, and the file is now 1421 — under the limit and exempted from it, which
the ratchet rejects by design. That check masked the compile error, because
file-size runs before clippy and exits first.

Verified beyond the build: `stella memory --help` lists all nine subcommands,
so #735's `compact` and `index` survive the move.

`make gate` green: exit 0, 3670 tests, check-file-size OK.
@macanderson macanderson changed the title fix(ci): drop the obsolete main.rs baseline entry failing main fix: repair main — duplicate MemoryCmd broke the build, plus an obsolete baseline entry Jul 26, 2026
@macanderson
macanderson merged commit 76d3d43 into main Jul 26, 2026
9 checks passed
@macanderson
macanderson deleted the fix/main-ci-obsolete-baseline branch July 26, 2026 22:51
macanderson added a commit that referenced this pull request Jul 26, 2026
… list, not the filesystem (#742)

Closes #737.

## The bug

Auto-created skills could silently overwrite a user's hand-edited skill
file. No prompt, no backup, no message beyond the ordinary "new skill
auto-created" line — and the file is not versioned by the app, so the
loss is unrecoverable.

Spec §8 (`docs/design/adaptive-context.md`) lists **"never clobbering a
hand-edited file"** among the five guarantees the adaptive loop must
preserve. It did not hold.

## Root cause: a list is not the filesystem

`stella-core/src/skills.rs` `decide_auto_creation` asks whether the
target path is in `existing_paths`. `stella-cli` built that list from
`self.load_skills()` — the **loaded, filtered** skill list — and then
did an unconditional `std::fs::write`.

So the guard answered *"is this a skill I successfully loaded and
kept?"*, never *"does this file exist?"*. `AutoCreateSkip::FileExists`
was misnamed: it never checked existence. **Loaded-skill paths and
on-disk paths are different sets, and the guarantee is about the
latter.**

Three things drop a real on-disk file out of the loaded list, all in
`load_workspace_skills_with_authority`:

1. `include_workspace_skills == false` — the workspace dir is passed as
`String::new()`, so nothing from it loads;
2. `filesystem_settings_disabled()` — returns an empty `LoadedSkills`;
3. `retain_enabled(...)` — a skill disabled from the SKILLS tab is
dropped, and the comment directly above it says *"its file stays on
disk"*.

**(3) is the reachable one** and needs no unusual configuration. Because
mined identity is a deterministic `{slug}-{hash8}`, a recurring lesson
cluster reliably re-targets the exact path the user edited: the property
that makes mining idempotent is what aims it at their work.

## The fix

**`stella-core` stays pure.** It is "the engine — no I/O, fastest to
test" and has essentially no filesystem dependency; putting
`path.exists()` in `decide_auto_creation` would trade one design defect
for another and make the function much harder to test. Its behavior is
unchanged here — **the caller now hands it accurate input**, which is
the same shape `rules::decide_promotion(approve, file_exists)` already
uses.

- `stella-cli` gains `skill_paths_on_disk`, which enumerates the `*.md`
files **actually present** in the target directory, rebuilding path
strings the same way the guard builds its target so they compare
exactly.
- The loaded paths are still unioned in. They cost nothing, they carry
the "this is a known skill" signal, and they keep the guard armed for
already-loaded skills if the directory read ever fails.
- The parameter is renamed `existing_paths` → `occupied_paths` and
documented as a filesystem fact. The old name is what invited the
confusion, and the rename is line-neutral — `stella-core/src/skills.rs`
sits exactly on its 1502-line baseline ceiling, so no `mod tests`
extraction was needed.

Structurally this closes all three triggers at once: the guard no longer
depends on `load_skills()` at all.

## Authority incoherence

`auto_create_skills` computed its write target from
`workspace_skills_dir()` unconditionally, so a session forbidden from
*reading* project prompts still *wrote* mined prose into that directory
— where the same flag guaranteed it would never be loaded back.

Auto-creation is now **skipped entirely** when
`include_workspace_skills` is false. Reading and writing that directory
are one authority. The narrower alternative — redirect creation to the
user-global dir — is worse: it would escalate prose mined under an
untrusted workspace into the scope that applies to *every other*
workspace.

## What the tests prove

The bug's whole character is that it is silent, so the tests are the
deliverable. They live in `stella-cli/src/memory/tests.rs`, which is the
only place this seam is reachable — `stella-core` only ever sees the
list this crate hands it.

Both regression tests **fail on the unfixed code**, verified by
reverting the fix:

- `a_disabled_hand_edited_skill_is_never_overwritten_by_auto_creation` —
trigger (3), end to end: mine a skill, hand-edit it, disable it from the
SKILLS tab, re-mine the same cluster. Before the fix this fails by
finding the generated markdown where the user's text was. It asserts the
preconditions too (file on disk, absent from the loaded list), so it
cannot silently stop testing the thing.
-
`auto_creation_never_writes_into_a_skills_dir_the_session_may_not_read`
— the authority fix, with a control proving the same log *does* create a
skill once the workspace scope is allowed.

Existing guarantees pinned so this change cannot regress them:

- `auto_creation_still_caps_each_session_and_spares_loaded_skills` — the
per-session cap of 2 holds, stays capped within a session, resets next
session, and the ordinary no-clobber-of-a-loaded-skill case still holds.
- `a_forgotten_lesson_still_cannot_return_as_an_auto_created_skill` —
tombstone filtering still gates auto-creation.

`make gate` green: **3674 tests passed, 0 failed**.

## Notes for reviewers

- **Stacked on #739.** `main` does not currently compile (a duplicate
`MemoryCmd` from #733/#735 merging cleanly but incompatibly), so this
branch sits on `fix/main-ci-obsolete-baseline`. The diff will reduce to
my five files once #739 merges. Nothing here depends on it beyond being
able to build.
- **#736 contains a test that deliberately pins the current buggy
behavior**, written to prove a migration was behavior-compatible. This
fix invalidates that pin. Expected and already flagged — whoever rebases
#736 should update it to assert the file survives rather than that it is
overwritten.
- `stella-core`'s contract is enforced by documentation
("`occupied_paths` MUST be the paths present on disk") rather than by
the type system. Making it unforgeable would mean either I/O in the
engine or a port/closure parameter; given the 1502-line ceiling on that
file and the "keep it tight" bar for a data-loss fix, the doc plus the
CLI-side tests seemed the right trade. Happy to revisit.
macanderson added a commit that referenced this pull request Jul 26, 2026
…ader (#745)

**`main` does not compile.**

```
error[E0432]: unresolved import `tuning::session_lifecycle_enabled`
  --> stella-cli/src/memory.rs:78:9
```

## Cause

Phase 2 (#738) and Phase 3 (#736) were built in parallel, and each
independently needed to read `context.lifecycle.enabled`. Neither could
see the other, so both wrote the reader — with a **byte-identical body**
and the same fail-closed reasoning, differing only in name and
visibility:

| | |
|---|---|
| Phase 3 | `pub(super) fn lifecycle_enabled(..)` |
| Phase 2 | `pub fn session_lifecycle_enabled(..)` |

Phase 3 merged first. Phase 2 merged second **from a rebase that
predated it**, so `tuning.rs` kept Phase 3's function while `memory.rs`
kept Phase 2's re-export of a name that no longer exists.

Neither PR was wrong, and neither conflicted textually with the `main`
that existed when it merged.

## Fix

The resolution both branches were converging on anyway: one reader,
`pub` because Phase 2's engine-config builder calls it from outside the
module, documented with both phases' reasoning, and one call site
updated.

## The pattern, since this is the third repair this week

- #739#733 moved `MemoryCmd`; #735 was cut from an older main and
re-added it. Zero textual conflict, nine type errors.
- This one — same shape, one level down.

**Parallel branches that merge cleanly are not thereby compatible.** A
branch that lands second is verified against the main it was *rebased
onto*, not the main it *lands on*, and nothing in the tooling notices
the difference. Re-verifying the second branch against the actual
post-merge tree is the step that catches this, and it is far cheaper
than a red main.

## Gate

`make gate` green: **exit 0, 3801 tests**, `check-file-size: OK — 475
Rust files, none over 1500 lines except 25 grandfathered (none grew).`
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