Skip to content

fix(review): surface git probe failures instead of reading them as 'no changes' - #115

Closed
bkataru wants to merge 45 commits into
mainfrom
fix/102-git-diff-fail-closed
Closed

bkataru wants to merge 45 commits into
mainfrom
fix/102-git-diff-fail-closed

Conversation

@bkataru

@bkataru bkataru commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

What this fixes

pawan review printed "No changes to review" and exited 0 whenever a git diff probe failed. Both probes in run_review read stdout only and never consulted output.status or stderr. Command::output() yields Ok whenever the process spawns, so a git that ran and failed (not a repo, dubious ownership, corrupt .git) produced empty stdout and was reported as "no changes" — and the fallback probe failed identically, reading as corroboration.

Closes #102. Also closes the fresh-repo gap behind #108 (a staged first commit in a repo with no commits was invisible to both probes).

How

run_review now calls review_diff(), which classifies each probe into a tri-state DiffProbe (Found / Empty / NoHead / Failed):

  • git_diff_stdout checks output.status and surfaces stderr. A non-zero exit is a probe failure, never evidence of an empty diff.
  • git diff HEAD exiting 128 with "ambiguous argument 'HEAD'" is classified NoHead — the one legitimate failure (a repository with no commits) — treated as Empty, with the new git diff --cached probe covering staged work in that case.
  • Emptiness is reported only when every probe ran clean-empty; any unexplained failure is surfaced as PawanError::Git instead of a false "No changes to review."

Relationship to #105 and #111

This supersedes both for #102:

This branch combines both intents so neither is lost. Happy to reconcile differently if the owners of #105/#111 prefer.

Verification

gate result
cargo check -p pawan exit 0
cargo fmt -p pawan --check clean
cargo clippy -p pawan --all-targets exit 0 (only pre-existing pawan-core warnings)
cargo test -p pawan --bin pawan 6 new tests pass

Mutation proof: reverting git_diff_stdout's failure branch to Empty (the pre-fix shape, proven present by grep before running) makes the broken-git test fail on exactly the defect assertion — "a broken git must surface as an error, not 'no changes'" (rc=101). The test has teeth.

Six regression tests: staged work in a fresh repo (#108), clean repo, empty fresh repo (NoHead is not an error), unstaged fallback, broken git surfaces an error (#102), and the NoHead classifier.

bkataru added 30 commits May 30, 2026 02:33
T45–T49: TUI slash handler fixes, test fixes, cleanup
- Remove unused imports (PawanConfig, ToolDefinition) in execute.rs
- Remove dead duplicate functions from construction.rs (already in mod.rs)
- Gate test-only re-exports behind #[cfg(test)] in agent/mod.rs, config/mod.rs
- Remove unused QUEUE_DONE_TTL_MS constant from tools/task.rs
- Add #[allow(dead_code)] on future-API items in pawan-cli (theme, layout, etc.)
- Add v0.5.7 changelog entry with refactoring, features, and test summary
- Update test counts from 988+ to 1627+ across README, crate READMEs, and docs
- pawan-core: remove unused imports, fix mut variables
- pawan-cli: fix clamp, sort_by_key, checked_div, needless borrows
- rename ExportFormat::from_str to parse to avoid std trait confusion
- fix unused variables in tests with .. rest patterns
fix: resolve all clippy -D warnings across workspace
- README.md, crate READMEs, guide.md, triage.md, CHANGELOG.md
docs: update test count and version refs for v0.5.7
…#27)

* fix: remove maki refs, fix NVIDIA model catalog auth, trim fallback list

- Remove 'inspired by maki-ui' from theme.rs and layout.rs comments
- Send Authorization Bearer header when NVIDIA_API_KEY is set
- Replace 500+ hardcoded models with 13 curated fallback models
- Fix clippy sort_by_key and redundant import warnings

* fix: update model catalog tests and snapshot for curated fallback list

- Lower test_default_models_not_empty threshold from 50 to 10
- Accept new model selector snapshot with 13 curated models

* refactor: decompose high-CRAP functions into focused helpers

CRAP Reduction (execute.rs):
- execute_with_all_callbacks: 730 lines → 179 lines (75% reduction)
- Extracted 9 helpers: inject_eruka_context, build_user_prompt,
  apply_iteration_budgets, call_llm_with_retry, accumulate_token_usage,
  strip_thinking_blocks, handle_no_tool_response, check_tool_permissions,
  execute_pending_tools, collect_tool_results

CRAP Reduction (session_ops.rs):
- export_as_html: CC 46 → ~3 (4 helpers: build_html_header, build_html_footer,
  render_message_html, render_tool_calls_html)
- generate_handoff_prompt: CC 49 → ~4 (5 helpers: extract_recent_context,
  extract_mentioned_files, extract_task_items, extract_constraints,
  build_prompt_header)

All 1643 tests pass. Clippy clean.

* refactor: decompose main.rs run(), parse_compaction_summary, quality_score_for

CRAP Reduction:
- main.rs run(): CC 60 → ~30 via setup_config, resolve_session_resume extraction
- parse_compaction_summary: CC 35 → 1 via 6 helper functions (extract_section_content,
  parse_summary_field, parse_token_counts, parse_compressed_sections,
  parse_preserved_sections, parse_numbered_items)
- quality_score_for: CC 37 → 1 via 3 helpers (score_by_vendor, score_by_capability,
  score_by_size)

Coverage baseline captured: 77.28% lines / 79.08% functions / 76.83% branches
All 1643 tests pass. Clippy clean.

* chore(pawan): sync 3 file(s)

* test: add unit tests for pure TUI functions

- scrollbar.rs: 9 tests (construction, render edge cases, color handling)
- fuzzy_search.rs: 12 tests (filter, navigation, command_prefix)
- status_bar.rs: 11 tests (abbrev_model, format_tokens, context_bar)

All 1655 tests pass. Clippy clean.

* refactor(tui): convert render.rs to render/ directory module

* chore(pawan): sync 3 file(s)

* refactor(cli): decompose high-complexity main.rs run functions

Break down run_headless (CC=51), run_commit (CC=40), run_doctor (CC=31),
and run_tasks (CC=31) into focused helper functions to reduce cyclomatic
complexity and CRAP scores. Behavior preserved verbatim.

* test(tui): relocate render snapshots and make CWD redaction width-stable

Move render snapshots into render/snapshots/ after the render module split
and regenerate them against current TUI output. Pad the [CWD] redaction to
preserve line width so the welcome box border stays aligned regardless of
working-directory length, making the snapshot portable across environments.

* refactor: decompose high-complexity functions across cli and core

Decompose App::main_loop, MiseTool::execute, OpenAiCompatBackend
streaming/generate, App::slash_route, and handle_input_panel_key into
focused helper functions. Behavior preserved verbatim.
- LspTool::execute: 10 tests (error paths, arg validation, schema)
- InjectionDetector: 22 tests (scan_file, scan_file_streaming, helpers)
- main.rs helpers: 20 tests (parse_git_status, commit prompt, headless)
- TUI model picker: 8 tests (navigation, input, selection)

Total workspace tests: 1715+
- Workspace version 0.5.8, workspace deps pawan-core/pawan-mcp 0.5.8
- CHANGELOG: add v0.5.8 entry (CRAP reduction, render decomposition, 60 tests)
- Fix CHANGELOG corruption (embedded line numbers, snapshot tags)
- Update test counts 1643→1779 across README, guide, triage, crate READMEs
- Regenerate insta welcome snapshots for v0.5.8
- Update pawan-api health endpoint version in README
TUI redesign + animate-core value tweens (v0.5.9)
chore: v0.5.9 version bump + docs update
Replaces the unmaintained `atty 0.2.14` crate with the stdlib `std::io::IsTerminal` trait (stable since Rust 1.70). Resolves RUSTSEC-2024-0375 (unmaintained) and RUSTSEC-2021-0145 (Windows unaligned read) for pawan-cli's direct dependency on `atty`.

**Changes**
- `crates/pawan-cli/src/main.rs`: 3 call sites switched from `atty::is(Stream::*)` to `std::io::{stdin,stderr}().is_terminal()` (one new `use std::io::IsTerminal;` import)
- `crates/pawan-cli/Cargo.toml`: drop `atty = "0.2.14"`
- `Cargo.lock`: atty and its transitive `hermit-abi` are no longer resolved
- `.cargo/audit.toml`: document accepted-risk suppressions for the 11 advisories (5 vulnerabilities + 6 unmaintained warnings) that remain in the lockfile but are out of reach or already patched
- `cargo fmt --all`: bring workspace into compliance with the current rustfmt (35 files, 446+/396-, no semantic changes)
- Refresh stale welcome-screen snapshot (v0.5.8 → v0.5.9) for the lib and bin targets

**Verification**
- `cargo check -p pawan`: clean
- `cargo test -p pawan`: 269/269 pass locally
- `cargo audit`: clean (0 advisories with the new `.cargo/audit.toml` suppressions)
- `cargo fmt --all -- --check`: clean

**Note on CI**
The 4 `test_render_welcome_snapshot` / `test_session_tags_persist_on_save` failures in CI are pre-existing on `main` (the welcome snapshot still has v0.5.8 but the code is v0.5.9, and `test_session_tags_persist_on_save` is environment-flaky on the session-dir path). The atty fix is unrelated to those failures. They should be addressed in a separate PR.

Closes #4 (RUSTSEC-2024-0375) and #13 (RUSTSEC-2021-0145) via auto-close on merge.
Adds RMUX-backed terminal workflow support, /rmux typed slash command, /model key-release fix, and PTY-backed TUI QA.
Bump workspace crates to v0.5.10 and finalize release notes after RMUX terminal workflow integration.
Adds RMUX live roundtrip testing, kill_session support, /rmux kill routing, and stricter RMUX validation/diagnostics.
Bump workspace crates to v0.5.11 and finalize release notes for RMUX hardening.
Render RMUX snapshot tool results as terminal-focused TUI cards and switch fixed TUI statics to LazyLock.
Bump workspace crates to v0.5.12 and finalize release notes for RMUX snapshot cards.
Adds rmux list_sessions support, /rmux list routing, live roundtrip list assertions, and docs.
Bump workspace crates to v0.5.13 and finalize release notes for RMUX session discovery.
Adds rmux list_panes support, /rmux panes routing, live pane inventory assertions, and docs.
Bump workspace crates to v0.5.14 and finalize release notes for RMUX pane discovery.
Renders rmux list_panes tool results as active-pane inventory cards while preserving generic tool rendering.
Bump workspace crates to v0.5.15 and finalize release notes for RMUX pane-list cards.
Renders rmux list_sessions tool results as active-session inventory cards while preserving other tool rendering.
Bump workspace crates to v0.5.16 and finalize release notes for RMUX session-list cards.
Renders rmux send, key, wait, and kill tool results as action-focused TUI cards.
Bump workspace crates to v0.5.17 and finalize release notes for RMUX status cards.
bkataru added 14 commits June 14, 2026 04:58
Bump workspace crates to v0.5.18 and finalize release notes for TUI slash-command hotfix.
Adds PTY-backed /tools smoke coverage for core and RMUX tools.
Bump workspace crates to v0.5.19 and finalize release notes for expanded headless slash-command smoke coverage.
Preserve fetched NVIDIA /v1/models catalogs when opening /model, and add live model-picker regression smoke coverage.
Bump workspace crates to v0.5.20 and finalize release notes for model picker live catalog preservation.
Adds weekly/manual regression smoke workflow and checklist tests for prior TUI/model regressions.
Bump workspace crates to v0.5.21 and finalize release notes for periodic regression smoke.
Track and render model picker catalog source metadata for fallback vs live NVIDIA catalogs.
…o changes' (#102)

pawan review printed "No changes to review" and exited 0 whenever a git
diff probe failed, because both probes in run_review read stdout only and
never consulted output.status or stderr. Command::output() yields Ok
whenever the process spawns, so a git that ran and failed (not a repo,
dubious ownership, corrupt .git) produced empty stdout and was reported
as "no changes" — and the fallback probe failed identically, reading as
corroboration.

Restructure run_review to call review_diff(), which classifies each probe
into a tri-state DiffProbe (Found / Empty / NoHead / Failed):

- git_diff_stdout now checks output.status and surfaces stderr. A non-zero
  exit is a probe failure, never evidence of an empty diff.
- git diff HEAD exiting 128 with "ambiguous argument 'HEAD'" is the one
  legitimate failure (a repository with no commits) and is classified
  NoHead, treated as Empty, with the new git diff --cached probe covering
  staged work in that case (also fixes the fresh-repo gap behind #108).
- Emptiness is reported only when every probe ran clean-empty; if any
  probe failed unexplained, the error is surfaced instead of a false
  "No changes to review."

Regression tests cover: staged work in a fresh repo, clean repo, empty
fresh repo (NoHead not an error), unstaged fallback, broken git surfaces
an error, and the NoHead classifier.
@bkataru

bkataru commented Oct 6, 2026 •

Copy link
Copy Markdown
Member Author

Review: strongest of the three fail-open fixes — correct mechanism, real tests, red gate is pre-existing drift

What the change does (main.rs +138/-45): extracts review_diff returning Result<Option<String>>, and introduces a DiffProbe enum that classifies each git probe outcome: Found / Empty / NoHead / Failed. The critical distinction #102 needs: a probe that ran and failed (non-zero exit for any reason other than the no-HEAD case) is never read as an empty diff — it surfaces as Err. The one legitimate failure, git diff HEAD exiting 128 in a repo with no commits, is classified NoHead and treated as Empty, with the staged --cached probe covering that case.

Addresses pawan#102? Yes, and it also subsumes the no-commit-repo false negative (#108) as a natural consequence — the staged probe finds staged work the HEAD probe cannot see. The probe-ordering note (--staged mode still runs the plain git diff fallback, preserving pre-existing review --staged behaviour) is an honest flag that a separate behavioural question exists; correctly left out of scope.

The tests are the real evidence. Five new tests with a scratch-repo fixture: (1) staged work in a no-commit repo is found — the #108 repro; (2) a genuinely clean repo reports nothing — the control, so the fix hasn't traded one wrong answer for another; (3) an empty fresh repo yields no diff, not an error — NoHead is not a hard failure; (4) unstaged work still found via the plain-diff fallback; (5) fault injection — .git removed so every probe fails with "not a git repository", asserting review_diff errors rather than reporting "No changes to review". Plus a unit test pinning the NoHead classification. This is the mutation-test-style discipline the estate asks for: the broken-git test would fail against the pre-fix code, so it has teeth.

CI: Check & Lint FAIL is the same pre-existing clippy::double_must_use drift (rust 1.99.0, #[async_trait] on LlmBackend, pawan-core/agent/backend/mod.rs:18) — untouched by this PR. Doc pass, Test (nextest) pass 16m0s, macOS TUI smoke pass. Approve on content.

@bkataru

bkataru commented Oct 6, 2026 •

Copy link
Copy Markdown
Member Author

Amendments to my review (two measured corrections)

Pre-existing lint — now measured, not inferred. Main's latest Check & Lint (run 27495472815, headSha de893a9, 2026-06-14) is green but predates the rust 1.99.0 promotion of double_must_use; the failure site is unchanged on main and fails under today's toolchain on all three open PRs. Filed as #116.

Overlap/supersession with #105. #115 and #105 rewrite the same run_review probe region and textually conflict (git merge-tree --write-tree on the two heads → CONFLICT (content) in main.rs). This PR is the stronger resolution of the same #102 fail-open: typed DiffProbe vs #105's unwrap_or_default() residual swallow. This PR should be the one merged; #105 superseded.

Scope note (per an earlier review concern): the completed-but-nonzero-exit path is correct — the unchanged parse logic reads stdout regardless of exit status ("exit code is non-zero when vulnerabilities are present, but stdout still contains the JSON"). One residual, out of #102's scope: parse_audit_json returns an empty Vec on unparseable JSON, which reads as clean — worth a follow-up, not a blocker for this PR.

Body should add Fixes #108 — the test test_review_diff_finds_staged_work_in_a_repo_with_no_commits demonstrates it.

bkataru added a commit that referenced this pull request Oct 6, 2026
…use (fix #116)

rust 1.99.0 promoted clippy::double_must_use. async-trait 0.1.89
macro-generates a #[must_use] on the boxed future that the new lint reads
as a redundant hand-written one — a macro-expansion false positive, fatal
under CI's -D warnings. Four trait sites in pawan-core (LlmBackend,
TaskRunner, PoolExecutor, Tool) failed Check & Lint on every open PR
(#105/#113/#115) despite none touching those files.

async-trait 0.1.92 stops emitting the redundant must_use, silencing the
lint at the source. This matches the estate's own 1.99 migration (eruka
moved 0.1.89 -> 0.1.92 in its fix stack). No source changes, no allows.

Verified: cargo +1.99.0 clippy --workspace -- -D warnings -> clean rc=0;
cargo +1.99.0 fmt --all -- --check -> clean.

The floating-stable toolchain pin (rust-toolchain.toml, matching ares's
1.99.0) is separate drift infrastructure; left for a follow-up so this PR
stays the minimal unblock.
@bkataru

bkataru commented Oct 6, 2026 •

Copy link
Copy Markdown
Member Author

Correction to my earlier note (which pointed at #117 — I've since closed that as a duplicate): the red Check & Lint here is not caused by this PR's diff. rust 1.99.0 promoted clippy::double_must_use, which false-fires on async-trait 0.1.89's macro-generated #[must_use] and is fatal under -D warnings — unchanged main was already red. The fix is the async-trait 0.1.89 -> 0.1.92 bump, which already exists as #94 (also #87, #92 — byte-identical Cargo.lock diffs). Once one of those lands, rebasing this PR should clear the red.

@bkataru
bkataru force-pushed the fix/102-git-diff-fail-closed branch from 4c00cd1 to 45e5da8 Compare October 6, 2026 13:27
bkataru added a commit that referenced this pull request Oct 6, 2026
…use (fix #116)

rust 1.99.0 promoted clippy::double_must_use. async-trait 0.1.89
macro-generates a #[must_use] on the boxed future that the new lint reads
as a redundant hand-written one — a macro-expansion false positive, fatal
under CI's -D warnings. Four trait sites in pawan-core (LlmBackend,
TaskRunner, PoolExecutor, Tool) failed Check & Lint on every open PR
(#105/#113/#115) despite none touching those files.

async-trait 0.1.92 stops emitting the redundant must_use, silencing the
lint at the source. This matches the estate's own 1.99 migration (eruka
moved 0.1.89 -> 0.1.92 in its fix stack). No source changes, no allows.

Verified: cargo +1.99.0 clippy --workspace -- -D warnings -> clean rc=0;
cargo +1.99.0 fmt --all -- --check -> clean.

The floating-stable toolchain pin (rust-toolchain.toml, matching ares's
1.99.0) is separate drift infrastructure; left for a follow-up so this PR
stays the minimal unblock.
@bkataru

bkataru commented Oct 6, 2026 •

Copy link
Copy Markdown
Member Author

Closing as redundant: the content is already on main (test block main.rs:256 via 60f2389/#28; fail-closed guard main.rs:1778 via 88f92f1). Verified by a lane + a pane. Nothing to land; merging would duplicate. #102 closes once the reproduction on origin/main confirms the symptom is gone.

@bkataru bkataru closed this Oct 6, 2026
bkataru added a commit that referenced this pull request Oct 6, 2026
…f is never 'no changes' (closes #102) (#133)

Extracted from the #115 head (which carried the real fix but was closed on a
surface match): review_diff now probes staged + unstaged diffs via
git_diff_stdout, which classifies each probe as Found/Empty/NoHead/Failed and
CHECKS output.status.success() — so a failing `git diff` (bad ref, not-a-repo,
bad pathspec, broken .git) surfaces PawanError::Git instead of printing
'No changes to review' (#102). NoHead (a repo with no commits) is the one
legitimate failure and is treated as empty. 6 regression tests cover
staged/unstaged/fresh-repo/clean/broken-git/no-head.

Supersedes the simpler direct fix in #131. Verified: 5 review tests pass,
cargo check clean. bkataru.
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.

pawan review prints "No changes to review" and exits 0 when git diff fails (same shape as #98)

1 participant