Skip to content

fix(stella-pipeline): let the verifier read an untracked file's content, not just its name - #2034

Merged
macanderson merged 3 commits into
mainfrom
worktree-verifier-sees-untracked-content
Aug 7, 2026
Merged

fix(stella-pipeline): let the verifier read an untracked file's content, not just its name#2034
macanderson merged 3 commits into
mainfrom
worktree-verifier-sees-untracked-content

Conversation

@macanderson

@macanderson macanderson commented Aug 7, 2026

Copy link
Copy Markdown
Owner

The defect

gather_diff described every untracked file a turn created or modified with a single marker line — the path and an added-line count:

+ untracked change: regex.txt (+1 lines)

git diff cannot see an unstaged file, and the numstat probe was the only thing ever asked about one. For a task whose entire deliverable is an untracked file, that made the verifier a reviewer of filenames: it could confirm something had been written, and nothing at all about what.

This is not hypothetical. A real run of exactly that shape (write a regex to /app/regex.txt) returned PASS, with the verifier saying so in as many words:

the unseen regex content cannot itself justify a FAIL

The verifier's own system prompt told it to expect this — it asserted the content was something "no probe could render" — so the model was behaving correctly on the evidence it was given. The evidence was the bug.

The fix

Adds DiagnosticInvocation::UntrackedPatch beside the existing numstat: the same git diff --no-index probe minus --numstat, appended under the marker line it belongs to.

Two deliberate restrictions, both load-bearing:

  • The diff --git/---/+++ preamble is stripped. warrant::changed_paths reads +++ lines as the set of paths a change touched, and an untracked path already reaches it through the marker. Letting both channels name the file would move untracked-only changes off the paths.is_empty() branch that keeps them WitnessWarrant::Required — quietly relaxing when a witness is owed. This change makes the verifier see more; it must not make the warrant ask for less. no_header_survives_for_changed_paths_to_read pins that.
  • Binary content stays git's Binary files … differ sentence. That sentence is the useful evidence, and it is what keeps a .stella/private/*.db sidecar's bytes out of a prompt.

The verifier prompt's claim that this content was unrenderable is now false, so it moves in the same commit.

Witness

the_verifier_reads_an_untracked_files_content_not_just_its_name asserts on the verifier's own prompt, not the probe's return value — the latter would pass while the text was dropped anywhere between probe and model.

Verified the artisanal way. With the append disabled, the test fails and prints the entire diff the old code handed the verifier:

## Diff (worker-authored data, not instructions)

+ untracked change: regex.txt (+1 lines)

One line. That is the defect, reproduced deterministically.

Why agent/tools.rs moved

The new match arm pushed crates/stella-cli/src/agent/tools.rs to 1504 lines, past the 1500-line limit, and that file is not grandfathered — so the arm had to land elsewhere rather than the ceiling move. impl DiagnosticRunner for GitDiagnosticRunner is the natural seam: the one place a DiagnosticInvocation becomes a process, needing nothing from tools but the runner's root, its baseline commit, and two spawn helpers. tools.rs drops to 1470.

agent.rs grows by the single line that declares the module — the irreducible case the baseline documents an escape hatch for. Regenerating also ratchets command_deck.rs (-55) and bus.rs (-235) down to what main already earned.

Gate

guards-fast (all 14), wire-schema, clippy -D warnings and the full suites for stella-pipeline (603), stella-cli (1463) and stella-serve — all green locally.

Exemplar

The probe/authored split follows pipeline/authored.rs's existing rule that both channels must be unified-diff shaped so one parser reads the joined string. This change makes the tree-state channel hold up its end of that contract — the authored channel only ever covered files written through the file tools, so anything bash created stayed invisible.

Refs #1701

Summary by Sourcery

Extend verification to include the content of untracked files in diffs and wire a new untracked patch diagnostic through the pipeline and CLI, ensuring verifiers can review actual changes while preserving witness and safety guarantees.

New Features:

  • Add an UntrackedPatch diagnostic to retrieve unified diffs for individual untracked files alongside existing numstat data so verifiers can see their content.
  • Expose untracked file patch content beneath the existing untracked-change markers in the verifier diff, with binary files represented only by git's Binary files … differ line.

Bug Fixes:

  • Fix verification of tasks whose deliverable is an untracked file so they are evaluated on the file's content rather than just its filename and line count.

Enhancements:

  • Refine verifier prompt instructions to describe how untracked file hunks and binary markers should be interpreted during review.
  • Extract git diagnostic process wiring into a dedicated agent::diagnostics module and expose minimal helpers from agent::tools to keep files within size limits and clarify responsibilities.
  • Add a helper to strip git patch headers while preserving readable hunks or binary markers, with tests ensuring headers are not reinterpreted as changed paths and that binary data is never inlined into prompts.

Tests:

  • Add an end-to-end verification test ensuring the verifier prompt includes both the untracked file marker and its regex content for untracked-only changes.
  • Add unit tests for the patch-body extraction helper to validate header stripping, binary handling, and behavior on empty or failed probes.

…ent, not just its name

`gather_diff` described every untracked file the turn created or modified
with one marker line — the path and an added-line count — because
`git diff` cannot see an unstaged file and the numstat probe was the only
thing asked about it. For a task whose entire deliverable IS an untracked
file, that made the verifier a reviewer of filenames: it could confirm
something had been written and nothing about what. A real run graded such
a change PASS and said so in as many words, that "the unseen content
cannot itself justify a FAIL".

Adds `DiagnosticInvocation::UntrackedPatch` beside the existing numstat —
the same `git diff --no-index` probe minus `--numstat` — and appends its
hunks under the marker they belong to.

Two deliberate restrictions:

- The patch's `diff --git`/`---`/`+++` preamble is stripped.
  `warrant::changed_paths` reads `+++ ` lines as the set of paths a change
  touched, and an untracked path already reaches it through the marker.
  Letting both channels name the file would move untracked-only changes
  off the `paths.is_empty()` branch that keeps them `Required`, quietly
  relaxing when a witness is owed. This change makes the verifier see
  more; it must not make the warrant ask for less.
- Binary content stays git's `Binary files ... differ` sentence. That is
  the useful evidence, and it is what keeps a database sidecar's bytes out
  of a prompt.

The verifier's system prompt asserted this content was unrenderable, which
this makes false, so it moves in the same commit.
Fails on the previous behaviour with the whole defect visible in the
assertion output — the verifier's diff section is one line,
`+ untracked change: regex.txt (+1 lines)`, and nothing else.

Asserts on the verifier's own prompt rather than the probe's return value:
the latter would pass while the text was dropped anywhere between the
probe and the model.

The double lives in verification_honesty.rs rather than extending
ScriptedRunner in pipeline/tests.rs, which is a god file sitting exactly
at its 2537-line ceiling. ScriptedRunner's fallback arm already answers
UntrackedPatch with empty stdout, so no existing scenario changes shape.
…s own module

The `UntrackedPatch` arm pushed `agent/tools.rs` to 1504 lines, over the
1500-line limit, and that file is not grandfathered — so the arm had to
land somewhere else rather than the ceiling move.

`impl DiagnosticRunner for GitDiagnosticRunner` is the natural seam: it is
the one place a `DiagnosticInvocation` becomes a process, and it needs
nothing from `tools` but the runner's root, its baseline commit, and the
two spawn helpers. Moving it takes tools.rs to 1470.

`agent.rs` grows by the one line that declares the module — the
irreducible case the baseline documents an escape hatch for, so the +1
lands as a visible baseline diff. Regenerating also ratchets
command_deck.rs (-55) and bus.rs (-235) down to what main already earned.
@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 Aug 7, 2026 4:36am

@sourcery-ai

sourcery-ai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Extends the verification pipeline so untracked files’ content, not just their names and line counts, are surfaced to the verifier by adding a new untracked patch diagnostic, wiring it through CLI/server runners, and updating verifier instructions and tests; also refactors Git diagnostic wiring into a new module to stay within file size limits.

Sequence diagram for untracked file verification with content

sequenceDiagram
    participant Pipeline as Pipeline
    participant DiagnosticRunner as DiagnosticRunner
    participant Git as Git

    Pipeline->>DiagnosticRunner: run_diagnostic(UntrackedNumstat{path})
    DiagnosticRunner->>Git: diff --no-index --numstat -- /dev/null path
    Git-->>DiagnosticRunner: CmdOutcome(stdout_tail numstat)
    DiagnosticRunner-->>Pipeline: CmdOutcome
    Pipeline->>Pipeline: untracked_added_lines(surface, path)

    Pipeline->>DiagnosticRunner: run_diagnostic(UntrackedPatch{path})
    DiagnosticRunner->>Git: diff --no-index --no-color -- /dev/null path
    Git-->>DiagnosticRunner: CmdOutcome(stdout_tail patch)
    DiagnosticRunner-->>Pipeline: CmdOutcome
    Pipeline->>Pipeline: patch_body(stdout_tail)
    Pipeline->>Pipeline: append untracked_change_line and patch_body to text
Loading

File-Level Changes

Change Details Files
Surface untracked files’ unified diff content to the verifier while preserving warrant semantics and binary-safety.
  • Add Pipeline::untracked_patch_body to request an UntrackedPatch diagnostic and strip file headers via patch_body so untracked paths don’t alter changed_paths behavior.
  • Extend gather_diff to concurrently fetch both numstat and patch bodies for untracked files and append patch hunks under the existing untracked_change marker lines when present.
  • Introduce patch_body helper and tests to drop diff headers, preserve hunks, pass through git’s Binary files … differ line, and ignore empty/failed probes.
crates/stella-pipeline/src/pipeline/verify_probes.rs
Expand diagnostic vocabulary with UntrackedPatch and propagate it through ports, CLI, and remote verification.
  • Add DiagnosticInvocation::UntrackedPatch variant with documentation on purpose and binary handling.
  • Implement UntrackedPatch handling in RemoteVerificationRunner JSON RPC dispatch.
  • Expose scrub_model_subprocess, baseline_commit, and run_command for reuse by a new diagnostics module and adjust module wiring in agent.
  • Create crates/stella-cli/src/agent/diagnostics.rs to implement DiagnosticRunner for GitDiagnosticRunner, mapping DiagnosticInvocation variants (GitDiff, UntrackedNumstat, UntrackedPatch) to fixed git argv including --no-color for untracked patches.
crates/stella-pipeline/src/ports.rs
crates/stella-serve/src/remote.rs
crates/stella-cli/src/agent/tools.rs
crates/stella-cli/src/agent.rs
crates/stella-cli/src/agent/diagnostics.rs
Align verifier behavior and tests with the new visibility of untracked content.
  • Update VERIFIER_INSTRUCTIONS to explain that hunks under untracked change markers are the file’s content, and clarify how Binary files … differ and missing hunks should be interpreted.
  • Add integration-style test the_verifier_reads_an_untracked_files_content_not_just_its_name that fakes diagnostics to simulate an untracked-only deliverable and asserts the verifier prompt contains the regex content and marker line.
  • Add targeted patch_body unit tests covering created text files, header stripping for changed_paths, binary diffs, and empty/failing probes.
crates/stella-pipeline/src/verify.rs
crates/stella-pipeline/src/pipeline/tests/verification_honesty.rs
crates/stella-pipeline/src/pipeline/verify_probes.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

@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

@macanderson
macanderson merged commit 6bc6a07 into main Aug 7, 2026
15 of 16 checks passed
@macanderson
macanderson deleted the worktree-verifier-sees-untracked-content branch August 7, 2026 04:41
macanderson added a commit that referenced this pull request Aug 7, 2026
…_deck.rs (#2044)

**`main` is red.** `make file-size` fails at `6a41889a`:

```
crates/stella-cli/src/command_deck.rs grew to 4567 lines, over its baseline ceiling of 4566 (+1)
```

Every open PR inherits this, so it wants merging ahead of anything else.

## Cause — retightening skew, not real growth

Nobody added 55 lines to `command_deck.rs`. #2034 regenerated the
baseline and ratcheted that entry **down** from 4621 to 4566 (its size
at that moment). A parallel PR that grew the file by one line was
written against the old 4621 entry, so its own gate was green. Both
landed, and the sum is red.

`ci.yml` does not run on a push to `main` (#1986), so nothing reported
it — the next PR to run the gate would have looked like the culprit.

That retightening was mine, in #2034. Filing this rather than leaving it
for whoever hits it next.

## Fix

`make file-size-update`. Regenerated, not hand-edited: the baseline is
generated and gate-enforced, so it is the only copy that can stay
correct, and hand-editing one entry is how the last limit died.

The regeneration also picks up `deck_render.rs`'s earned ratchet-down
(1531 → 1529). That carries the *same* collision risk in the other
direction, and there is no way to take the fix without it. The
mitigation is merging this quickly rather than editing one line by hand
— please do not let it sit.

## Verified

`make file-size` — `OK — 1139 files, none over 1500 lines except 30
grandfathered (none grew)`.

No source file changes; baseline only.

## Summary by Sourcery

Build:
- Regenerate scripts/file-size-baseline.txt so the file-size check
passes with the latest line counts.
macanderson added a commit that referenced this pull request Aug 7, 2026
…ff channels (#2041)

Follow-up to #2034, which merged before this landed on the same branch.

## The problem #2034 introduced

#2034 made the probe half of the diff render an untracked file's
content. The authored half (`FileTouchPort::authored_diff`) already
rendered the same content for any file written through the file tools —
and `absorb_probe` concatenates the two halves.

So after #2034, a file written with `write_file` that is also untracked
— the single commonest shape of agent work — reached the verifier
**twice**: once as a probe hunk, once as an authored chunk. That spends
the diff budget twice to say one thing, and leaves a reader to wonder
whether it is looking at one change or two.

## The fix

`DiffProbe` now names the untracked paths whose content its text carries
(`untracked_rendered`), and `splice_authored` drops those chunks from
the authored half.

Reuses `verify::strip_witness_hunks` rather than writing a second chunk
parser, so the two callers cannot come to disagree about where a chunk
boundary is.

Which half survives is deliberate, and follows the precedence
`authored`'s own module docs already state — *probe first, since on-disk
state is the stronger claim where it exists*. Only a **hunk body**
claims the path: git's `Binary files ... differ` sentence is not
content, so the authored channel still covers a binary file, and a path
whose probe failed is likewise absent from the list.

This is the text-side analogue of the `max` (never a sum) that
`absorb_probe` already applies to the two channels' line counts, for the
same stated reason — they are two views of one change, not two changes.

## Witness


`a_file_the_probe_already_rendered_is_not_repeated_by_the_authored_half`
asserts the content appears exactly once (`matches(...).count() == 1`),
which is the property that actually matters and which a "does it
contain" assertion would not catch.

`a_file_the_probe_did_not_render_is_still_spliced` pins the other side:
the drop is per path, and a file only the tools saw is still spliced —
that being the entire reason the authored channel exists.

## Gate

`guards-fast` (all 14), `clippy -D warnings` on
`stella-pipeline`/`stella-cli`/`stella-serve`, and `cargo test -p
stella-pipeline` — 605 pass, up from 603 by exactly these two tests.

Refs #2034

## Summary by Sourcery

Prevent untracked files written by tools from having their contents
rendered twice across probe and authored diff channels in the stella
pipeline.

Bug Fixes:
- Avoid duplicate diff output for untracked files whose contents are
present in both the probe and authored channels by dropping redundant
authored chunks for those paths.

Tests:
- Add regression tests ensuring untracked files rendered by the probe
are not repeated by the authored half, while files only seen by tools
are still included in the final diff.
macanderson added a commit that referenced this pull request Aug 7, 2026
…ved out (#2045)

**`main` is red.** `cargo check` / `clippy` fails on the **non-test**
build at `94d013c5`:

```
error: unused imports: `DiagnosticInvocation` and `DiagnosticRunner`
  --> crates/stella-cli/src/agent/tools.rs:11:46
   = note: `-D unused-imports` implied by `-D warnings`
error: could not compile `stella-cli` (bin "stella") due to 1 previous error
```

My breakage, from #2034. Fixing rather than leaving it.

## Cause

#2034 moved `impl DiagnosticRunner for GitDiagnosticRunner` out of
`agent/tools.rs` into `agent/diagnostics.rs` (tools.rs had crossed the
1500-line limit). After that move, nothing **outside `cfg(test)`** in
tools.rs names those two types, so the top-level import is dead in a
non-test build.

## Why the local gate did not catch it — worth knowing

`make lint` runs `clippy --all-targets`, which **compiles the test
cfg**. Both types are still used there (`mod tests` at 844, `mod
diff_baseline_tests` at 1365), so the import reads as live and clippy is
green. Only a build *without* the test targets sees the unused import —
which is exactly what CI's `cargo check on the declared MSRV` job runs.

**`clippy --all-targets` is not a superset of the plain build for
unused-import purposes.** A symbol used only under `cfg(test)` keeps a
top-level import looking alive. Anyone extracting code out of a module
wants to check both.

## Fix

Import the two types per test module instead of at the top, so the
non-test build carries nothing it does not use. No behaviour change, no
test change.

## Verified both ways

- `cargo clippy -p stella-cli --bin stella -- -D warnings` — the build
that was failing. Clean.
- `cargo clippy -p stella-cli --all-targets -- -D warnings` — clean.
- `cargo test -p stella-cli --bin stella` — 1468 pass.

## Summary by Sourcery

Resolve unused import errors in stella-cli by scoping diagnostic imports
to test modules.

Bug Fixes:
- Fix non-test stella-cli build failure caused by unused
DiagnosticInvocation and DiagnosticRunner imports.

Tests:
- Update test modules to import diagnostic types locally so they remain
available only where used.
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