watcher correctness + perf: graceful shutdown, mtime-cached scans, single-row UPDATEs, pcr_dir fail-loud#86
Merged
Conversation
Mirrors the truncate-on-char-boundary fix in `util::text` and the recent diff-truncate fix. Two spots still byte-sliced on text we didn't allocate ourselves: - `commands/log.rs::short_sha` (`sha[..7]`) — composer / commit shas are hex in practice but `short_sha` is also called on the `manual-*` sentinels, and any future non-ASCII tag would have panicked. - `sources/cursor/session_state_watcher.rs` short-id log line (`&row.composer_id[..len.min(8)]`) — composer ids are UUIDs but the byte-slice would have paniced if Cursor ever shipped a Unicode session identifier. Both now use `chars().take(N).collect::<String>()`, same shape as the existing `truncate` helper. No public behavior change for ASCII inputs (length still 7 / 8). Co-authored-by: Cursor <cursoragent@cursor.com>
`process_file` used to `state.set(file_path, line_count)` BEFORE calling `parse_claude_code_session`. If the parser returned an empty session — corrupt JSONL, mid-write truncation, schema drift, anything — the cursor still moved forward, so on the next scan none of those unprocessed lines would ever be retried (the watcher short-circuits on `line_count <= prev_count`). A full rescan never happens in steady state, so the loss was permanent. Move `state.set` to *after* `session.prompts.is_empty()` so the cursor only advances when parse produced something usable. Tests: - New `claudecode_state_ordering` integration test asserts that a three-line corrupt-JSONL file leaves the cursor at 0, then rewriting the same path with a valid user/assistant turn moves the cursor forward. A second call on identical bytes is idempotent (no regression). Co-authored-by: Cursor <cursoragent@cursor.com>
`UPDATE drafts SET response_text = ? WHERE session_id = ? AND
prompt_text = ?` overwrote every row with that (session, prompt)
pair. Claude Code and Cursor both let users re-send identical
prompt text inside a single session ("go", "continue", "yes"),
and we already keep those as distinct rows — the v2 content hash
folds `captured_at` in so they survive the `content_hash UNIQUE`
constraint. Both rows were getting the second turn's response
text written onto them, which produced wrong transcripts on the
older row.
Switch to a select-then-update shape so we always target a
single id:
1. `SELECT id ... ORDER BY captured_at DESC, id DESC LIMIT 1` —
the watcher only ever enriches "the newest draft for this
(session, prompt) pair", so anchor on that.
2. `UPDATE ... WHERE id = ? AND (response_text IS NULL OR
LENGTH < LENGTH(?))` — the don't-shrink guard moves to the
UPDATE so a stale "newer reply" call doesn't get fall back
to an older row that happens to qualify.
`LIMIT 1` in the UPDATE itself isn't an option here: rusqlite's
bundled SQLite isn't built with `SQLITE_ENABLE_UPDATE_DELETE_LIMIT`
(verified — `UPDATE ... LIMIT 1` errors with a syntax error).
Tests:
- New `drafts_update_response_scoped` integration test creates
two drafts with identical (session_id, prompt_text) and
asserts only the newer row receives the response across three
successive `update_draft_response` calls (initial, idempotent,
extended). The older row is verified untouched at every step.
Co-authored-by: Cursor <cursoragent@cursor.com>
`pcr start` previously had two correctness gaps on SIGINT:
1. The Ctrl-C handler was installed inside `wait_for_shutdown`,
AFTER PID file write and AFTER `spawn_all_sources`. If SIGINT
landed in that window, the default signal handler killed the
process with `W_TERMSIG(SIGINT)` (exit status 130) and the PID
file leaked behind.
2. Watcher scan loops (`cursor::watcher::start`,
`session_state_watcher::run_blocking`, `diff_tracker::run_blocking`)
were bare `loop { sleep(N s); poll() }` with no shutdown channel,
so Ctrl-C tore them down mid-scan rather than letting them
unwind at an iteration boundary.
Both fixes share a single process-wide flag in the new
`crate::shutdown` module:
- `request_shutdown()` is wired to the `ctrlc` handler installed
at the very top of `commands::start::run` — before PID file
write, before watcher spawn — so the window where the default
SIGINT handler could kill us is gone.
- `is_shutting_down()` is cheap; `sleep_unless_shutdown(d)` slices
long sleeps into 200 ms chunks so each watcher reacts within
~one chunk of Ctrl-C instead of waiting up to its full tick.
- The three scan loops above now `while sleep_unless_shutdown(d) {
... }` so they exit cleanly. PID file cleanup runs through the
existing `PidFileGuard::drop` (already idempotent via
`let _ = remove_file`).
Tests:
- `crate::shutdown::tests` covers the early-exit and
full-completion paths of `sleep_unless_shutdown` with a 50 ms
background thread that flips the flag.
- New `pcr-cli/tests/start_graceful_shutdown.rs` (Unix only)
spawns the real `pcr` binary, waits for the PID file, sends
SIGINT via `libc::kill(2)`, and asserts (a) the process exits
with status 0, (b) the PID file is gone. Without the fix this
test fails with `W_TERMSIG(SIGINT)` and a leaked PID file.
Co-authored-by: Cursor <cursoragent@cursor.com>
The periodic safety-net loop ran a full recursive WalkDir over `~/.cursor/projects/` every 20 s and re-processed every transcript on every pass — relying on the in-process `seen` dedup to no-op already-saved bubbles. At scale (hundreds of long-lived projects × thousands of bubbles each) this dominates CPU and disk seek budget for a watcher that's supposed to be invisible. Three changes: 1. **Fast-path skip on quiet polls.** Track the top-level dir mtime + a `notify_event_pending` flag set by the fsnotify loop. The next periodic tick checks both: when neither indicates new activity, return None immediately and don't walk anything. New project subdirs change the top-level mtime; in-session bubble writes are caught by the notify flag (they don't bump the parent's mtime). 2. **Per-file mtime cache.** When a walk does run, snapshot each transcript's mtime and skip `process_session` for any path whose mtime equals the previously-cached value. Already-processed bubbles were no-ops via dedup anyway, but the open + JSON-fetch + DB-lookup cost is what the cache actually saves. 3. **Bump periodic interval from 20 s to 60 s.** Notify is the primary signal (sub-second after debounce); the periodic loop is purely a safety net for notify-misses, so 60 s matches the audit's "treat as safety-net every 60-120 s" guidance without changing pickup latency for the notify path (still ~600 ms end-to-end via the existing 500 ms debounce + 100 ms checker tick). Initial-scan path passes `force = true` to bypass the fast-path skip — the caches start empty so the first walk has to populate them regardless. Tests: - `collect_pending_initial_walk_returns_all_transcripts` — force walk surfaces every transcript and populates the cache. - `collect_pending_fast_path_skips_when_nothing_changed` — second poll with no signals returns None (no walk). - `collect_pending_walks_when_notify_event_signalled` — notify flag forces a walk even when the dir mtime is stale; flag is consumed. - `collect_pending_returns_only_files_whose_mtime_moved` — poke one transcript's mtime, assert exactly that path is pending (other transcript is filtered out). - `collect_pending_force_runs_walk_even_without_signals` — matches the start() initial-scan invariant. Co-authored-by: Cursor <cursoragent@cursor.com>
\`gc_orphaned\` shelled out \`git cat-file -e <sha>\` once per unpushed prompt_commit row to test whether the commit still exists. With N unpushed commits per project this was N git processes per GC pass. At scale (hundreds of stale unpushed commits across long-lived projects) the fork/exec overhead dominates the actual GC work. Replace the loop with a single \`git cat-file --batch-check\` invocation: pipe every SHA on stdin, parse \`<sha> missing\` / \`<sha> <type>\` lines on stdout. One process per repo, regardless of N. Same orphan-detection semantics — a SHA is classified orphan iff git can't resolve the object (rebased away, branch deleted, repo re-cloned). The audit suggested \`git for-each-ref refs/heads/\`, but for-each-ref walks named refs forward (and its committerdate-by-default output isn't a SHA-existence test); \`cat-file --batch-check\` is the purpose-built primitive for "does this exact object exist". Documented inline. Failure modes preserved conservatively: if git is missing / the repo is broken / the subprocess can't be spawned, return an empty missing set so the caller doesn't reclassify unpushed work as orphan because the user's git binary is mis-installed. Tests: - \`batch_check_marks_only_unknown_shas_as_missing\` builds a one-commit repo with a TempDir, queries both the real HEAD sha and a 40-zeros sha, asserts only the bogus one shows up in the missing set. Skips gracefully if \`git --version\` isn't on PATH. - \`batch_check_returns_empty_when_input_is_empty\` — no work, no subprocess. - \`batch_check_returns_empty_on_git_failure\` — a non-git directory must NOT cause every queried SHA to be reported missing (would wrongly GC unpushed work). Co-authored-by: Cursor <cursoragent@cursor.com>
…/tmp
\`config::pcr_dir()\` previously did
\`dirs::home_dir().unwrap_or_else(env::temp_dir).join(".pcr-dev")\`.
The silent fallback meant that on any machine where \`$HOME\` /
\`%USERPROFILE%\` couldn't be resolved (sandboxes, locked-down
containers, cron-like environments) the CLI quietly wrote the
SQLite store, auth.json, watcher PID, and per-source state
files under \`/tmp/.pcr-dev/\` — where they evaporated on
reboot. The user's login disappeared and every watcher
re-emitted every prompt on next start because its state cursor
was gone.
Change the signature to \`anyhow::Result<PathBuf>\` and update
each caller:
- \`commands/start.rs::pid_file_path\` propagates Result; the
CLI entry point surfaces the error via \`display::print_error\`
and returns \`GenericError\` before spawning any watchers.
- \`commands/hook.rs::run\` treats Err as "no live watcher to
signal" and exits 0 — that's the only branch a hook should
ever take.
- \`auth.rs\`: \`auth_file_path\` returns Result. \`load\` falls
through to None (best-effort by design). \`save\` propagates
via \`?\`. \`clear\` no-ops since there's nothing to clear if
the path can't be resolved.
- \`projects.rs::file_path\` returns Result. \`load\` (hot path
called from every watcher tick) degrades to an empty list,
matching its existing best-effort behaviour on a missing
projects.json. \`save\` propagates via \`?\`.
- \`sources/cursor/diff_tracker.rs::state_path\` returns
\`Option<PathBuf>\` — load_state and save_state are
soft-state operations and already swallow IO errors; making
them Option lets them skip persistence cleanly when \`$HOME\`
is unavailable.
- \`store/db.rs::db_path\` returns Result; \`open()\` keeps its
panic-on-failure semantics (the SQLite singleton is built
with \`OnceLock\` and already \`.expect\`s on schema errors)
but the panic message now explicitly names the missing
\$HOME instead of returning a \`/tmp\` path.
- \`sources/shared/state.rs::FileState::new\` keeps its
\`-> Self\` signature so the constraint not to touch the
recent vscode/watcher dedup work doesn't bleed into this
diff. Instead it \`.expect\`s with the same clear "refusing
to put state under /tmp" message — failing fast at watcher
construction is strictly better than silent fallback.
Tests:
- \`config::tests::pcr_dir_returns_err_when_home_is_unresolvable\`
drives the error branch by removing HOME + USERPROFILE,
calls \`pcr_dir()\`, and asserts the message mentions
\`\$HOME\` so users can self-diagnose. Skips gracefully on
Unix platforms where \`dirs::home_dir()\` falls through to
\`getpwuid_r\` (the regression is still gone, the test just
can't observe it from there). Env is restored before any
assertion can panic so the rest of the test binary stays
sane.
Diff: 177 lines added / 34 removed across 8 files. Per the
audit's expectation, this is a >100-line correctness fix worth
the scale.
Co-authored-by: Cursor <cursoragent@cursor.com>
4 tasks
KaluJo
added a commit
that referenced
this pull request
May 18, 2026
Adds a best-effort background update check that prints a soft "X is available — run: brew upgrade pcr" notice at the end of every interactive `pcr` command when a newer `pcr-dev` ships on npm. Modeled on the `update-notifier` npm package and `cargo`'s own behaviour: - Background thread fires off a 3-second-timeout GET against https://registry.npmjs.org/pcr-dev/latest at the *start* of the command, runs concurrently with the command itself, and writes the result to `~/.pcr-dev/update-check.json` regardless of whether the foreground command has exited. The thread is intentionally not joined — network failures, captive portals, slow DNS never delay the user's primary signal. - At the *end* of the command, the cached file is read and a one-line notice is printed to stderr if (a) the cached version is greater than `CARGO_PKG_VERSION` under naive `major.minor.patch` semver, and (b) we haven't shown the notice in the last hour (so back-to- back `pcr log; pcr show` doesn't double-print). - Suggested upgrade command is install-method-aware: inspects `current_exe()` for `/Cellar/`, `/opt/homebrew/`, or `/node_modules/` and prints `brew upgrade pcr`, `npm i -g pcr-dev@latest`, or a generic `https://pcr.dev/install` link respectively. - Hard skips: `--json` output, the hidden `hook` + `mcp` subcommands (they're stdio JSON-RPC / Stop-hook channels), `CI=*` env, and `PCR_NO_UPDATE_CHECK=1` for users who want to opt out. 6 new unit tests cover semver comparison (including prerelease suffixes), forward-compatible cache deserialisation (older payloads without `last_notice_unix` decode cleanly), and the quiet-subcommand skip list. Workspace tests: 134 passed (was 128 on this main baseline). Also adds CHANGELOG.md (none previously existed in this repo) seeded with an Unreleased section that catalogues this feature plus the in-flight fixes from PRs #85 (vscode dual-watch dedup) and #86 (watcher correctness + perf) so they have a single grep-able home when those PRs merge. Independent of PR #85 / #86 — touches `lib.rs`, `entry.rs`, and a new file. Safe to merge in any order. Made with [Cursor](https://cursor.com) Co-authored-by: Cursor <cursoragent@cursor.com>
KaluJo
added a commit
that referenced
this pull request
May 18, 2026
PR #86 changed `config::pcr_dir()` to return `Result<PathBuf>` so that auth + SQLite + watcher state never silently fall back to `/tmp` when neither `$HOME` nor `%USERPROFILE%` resolves. The update-notifier is best-effort, so it absorbs the Err the same way it absorbs every other failure: collapse to `None` and silently skip the cache operation. The foreground command never sees the error. Cache helper signatures: cache_path() -> PathBuf // before cache_path() -> Option<PathBuf> // after load_cache + save_cache add a `let-else` guard at the top. Tests unchanged — all 6 update_check::tests still pass, workspace 153 passing / 0 failing. Co-authored-by: Cursor <cursoragent@cursor.com>
KaluJo
added a commit
that referenced
this pull request
May 18, 2026
…md (#87) * feat: "newer version available" notice on pcr runs + start CHANGELOG.md Adds a best-effort background update check that prints a soft "X is available — run: brew upgrade pcr" notice at the end of every interactive `pcr` command when a newer `pcr-dev` ships on npm. Modeled on the `update-notifier` npm package and `cargo`'s own behaviour: - Background thread fires off a 3-second-timeout GET against https://registry.npmjs.org/pcr-dev/latest at the *start* of the command, runs concurrently with the command itself, and writes the result to `~/.pcr-dev/update-check.json` regardless of whether the foreground command has exited. The thread is intentionally not joined — network failures, captive portals, slow DNS never delay the user's primary signal. - At the *end* of the command, the cached file is read and a one-line notice is printed to stderr if (a) the cached version is greater than `CARGO_PKG_VERSION` under naive `major.minor.patch` semver, and (b) we haven't shown the notice in the last hour (so back-to- back `pcr log; pcr show` doesn't double-print). - Suggested upgrade command is install-method-aware: inspects `current_exe()` for `/Cellar/`, `/opt/homebrew/`, or `/node_modules/` and prints `brew upgrade pcr`, `npm i -g pcr-dev@latest`, or a generic `https://pcr.dev/install` link respectively. - Hard skips: `--json` output, the hidden `hook` + `mcp` subcommands (they're stdio JSON-RPC / Stop-hook channels), `CI=*` env, and `PCR_NO_UPDATE_CHECK=1` for users who want to opt out. 6 new unit tests cover semver comparison (including prerelease suffixes), forward-compatible cache deserialisation (older payloads without `last_notice_unix` decode cleanly), and the quiet-subcommand skip list. Workspace tests: 134 passed (was 128 on this main baseline). Also adds CHANGELOG.md (none previously existed in this repo) seeded with an Unreleased section that catalogues this feature plus the in-flight fixes from PRs #85 (vscode dual-watch dedup) and #86 (watcher correctness + perf) so they have a single grep-able home when those PRs merge. Independent of PR #85 / #86 — touches `lib.rs`, `entry.rs`, and a new file. Safe to merge in any order. Made with [Cursor](https://cursor.com) Co-authored-by: Cursor <cursoragent@cursor.com> * update_check: handle pcr_dir() -> Result<PathBuf> from #86 PR #86 changed `config::pcr_dir()` to return `Result<PathBuf>` so that auth + SQLite + watcher state never silently fall back to `/tmp` when neither `$HOME` nor `%USERPROFILE%` resolves. The update-notifier is best-effort, so it absorbs the Err the same way it absorbs every other failure: collapse to `None` and silently skip the cache operation. The foreground command never sees the error. Cache helper signatures: cache_path() -> PathBuf // before cache_path() -> Option<PathBuf> // after load_cache + save_cache add a `let-else` guard at the top. Tests unchanged — all 6 update_check::tests still pass, workspace 153 passing / 0 failing. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
Merged
KaluJo
added a commit
that referenced
this pull request
May 18, 2026
Bumps the workspace to 0.3.0 — the 0.x minor (rather than 0.2.10 patch) is motivated by the breaking signature change to `pcr_core::config::pcr_dir()` from #86 (returns `Result<PathBuf>` instead of `PathBuf`). The CLI surface (`pcr <cmd>` flags / exit codes / output format) is unchanged. Version touchpoints: * `Cargo.toml` workspace.package.version → 0.3.0 * `crates/pcr-napi/package.json` version + all 4 optionalDependencies * `crates/pcr-napi/npm/{darwin-arm64,darwin-x64,linux-x64-gnu, win32-x64-msvc}/package.json` versions * `README.md` TUI mock version stamp * `CHANGELOG.md` `[Unreleased]` promoted to `[0.3.0] — 2026-05-18` with full release notes catalogued by PR (#85, #86, #87, #88) and grouped Added / Changed / Fixed / Tests. Workspace verification: * `cargo fmt --all --check` clean * `cargo clippy --workspace --all-targets -- -D warnings` clean * `cargo test --workspace` — 153 passing, 0 failing (was 128 on the v0.2.9 baseline; +25 across the 4 merged PRs) * `cargo build -p pcr-cli --release` → `pcr 0.3.0 (rust)` After this lands on `main`, the release commit is tagged `v0.3.0` locally and pushed; that triggers the release workflow which publishes npm + builds binaries + dispatches the homebrew formula update. Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Lands the high-impact BUG / PERF / CORRECTNESS items from the recent
CLI audit. Each fix is one commit; commits stand alone so they can
be reverted independently if needed. 132 → 143 tests (every fix
added new coverage).
BUG
commands/start.rsgraceful shutdown. The Ctrl-C handlerwas installed inside
wait_for_shutdown, after PID filewrite and after
spawn_all_sources. SIGINT in that windowkilled the process with
W_TERMSIG(SIGINT)and leaked the PIDfile. Watcher scan loops were bare
loop { sleep; scan() }withno shutdown channel either. New
crate::shutdownmodule withrequest_shutdown/is_shutting_down/sleep_unless_shutdown;the
ctrlchandler is wired at the very top ofcommands::start::runbefore any setup work; the three long-running scan loops
(
cursor::watcher,session_state_watcher,diff_tracker) nowcooperatively poll the flag at every iteration boundary.
update_draft_responseupdated multiple rows. Theunscoped
UPDATE drafts WHERE session_id=? AND prompt_text=?wasoverwriting every row with that (session, prompt) pair. In
practice Claude Code and Cursor both let users re-send identical
prompt text inside one session ("go", "continue", "yes"), so the
older row had its response stomped by the newer turn's. Replaced
with
SELECT id ORDER BY captured_at DESC, id DESC LIMIT 1→UPDATE WHERE id = ? AND don't-shrink-guard.UPDATE ... LIMIT 1isn't an option here — rusqlite's bundled SQLite is built
without
SQLITE_ENABLE_UPDATE_DELETE_LIMIT.PERF
WalkDirevery 20 s was CPU/disk-heavy at scale and re-processed every
transcript on every pass. Now: track top-level dir mtime + a
notify_event_pendingflag; on the periodic tick, skip the walkentirely when neither signal indicates new activity. When a walk
does run, per-file mtime cache skips
process_sessionfor pathswhose mtime hasn't moved. Bumped periodic interval from 20 s to
60 s — notify (with its existing 500 ms debounce) is the primary
signal; the periodic loop is purely a safety net for notify-misses.
Pickup latency is unchanged for the notify path (~600 ms
end-to-end).
gc::orphanedbatchedgit cat-file. Was spawning onegit cat-file -e <sha>per unpushed commit (O(N) processes perGC pass). Now a single
git cat-file --batch-checkinvocation perrepo: pipe every SHA on stdin, parse
<sha> missing/<sha> <type>on stdout. The audit suggestedgit for-each-ref,but that walks named refs forward and isn't a SHA-existence test;
--batch-checkis git's purpose-built primitive — documentedinline. Failure modes are conservative: missing git → empty
missing-set → no unpushed work gets reclassified as orphan.
CORRECTNESS
pcr_dir()returnsResult. Previously diddirs::home_dir().unwrap_or_else(env::temp_dir), silently writingauth + SQLite + watcher state under
/tmpwhenever$HOME/%USERPROFILE%couldn't be resolved (sandboxes, locked-downcontainers). Drafts and login evaporated on reboot. Now returns
anyhow::Result<PathBuf>with a clear "set HOME and re-run"message. Every direct caller updated: CLI entry points surface via
display::print_error+ non-zero exit; library wrappers propagatevia
?(or.expect()at hard fatal boundaries like the SQLitesingleton, where the panic message names the missing
$HOMEinstead of returning a
/tmppath).claudecode::process_filestate cursor ordering.state.set(file_path, lines)ran before the parse. Atransient parse failure (corrupt JSONL, mid-write truncation,
schema drift) advanced the cursor without saving anything, so the
unprocessed lines were lost until a full re-scan — which never
happens in steady state. Moved
state.setto after theprompts.is_empty()check.commands/log.rs::short_shaand
cursor::session_state_watchershort-id log line) stillbyte-sliced on text we didn't allocate — composer IDs are UUIDs
in practice but any future non-ASCII tag would have panicked.
Switched to
chars().take(N).collect::<String>(), matching thetruncate_difffix from PR vscode: stop dual-watch dupes from chatSessions + transcripts #85.Branching
Off
mainrather than stacked onfix/vscode-dual-watch-dedup—recent PRs (#80–#85) are all single feature branches against
main, none stacked. Conflict-free either way; these files don'toverlap with the dedup work.
Test plan
cargo fmt --allclean.cargo check --workspace --all-targetsclean.cargo clippy --workspace --all-targets -- -D warningsclean.cargo test --workspace— 143 passed across 13 binaries; 0 failures (was 128 on thismainbaseline; the new tests are the 11+ added across the 7 fixes — most notably the SIGINT integration test, the mtime-cache unit tests, theupdate_draft_responsemulti-row regression test, the claudecode state-ordering test, the gc--batch-checktest, and thepcr_direrror-branch test).pcr startsmoke test on a real~/.cursor/projects/: watcherstarts, captures prompts, Ctrl-C produces exit code 0 and removes
the PID file (covered by
pcr-cli/tests/start_graceful_shutdown.rs).Deviations from the audit description
git cat-file --batch-checkrather than the audit'sliteral
git for-each-ref refs/heads/suggestion —for-each-refwalks named refs, which isn't the right primitive for "does this
exact SHA exist". Documented inline in the new helper. Same
one-subprocess-per-repo cost shape the audit asked for.
FileState::newreturningSelf(with a fail-fast.expect) rather thanResult<Self>. The strict constraint notto touch
vscode/watcher.rs(recent dedup work) means propagatingResultthroughFileState::newwould have either bled intothat file or required a Result-shaped facade around it. The
fail-fast panic message is unambiguous and the audit's correctness
intent (no silent
/tmpfallback) is fully preserved.Not in scope
rx.recv()main loop and 250 ms debouncepump don't yet observe
crate::shutdown. Those threads die whenthe process exits (which is fine — they have no in-flight DB
writes when blocking on notify), but wiring them through the
shared flag would tighten the shutdown story further. Leaving
for a follow-up so this PR stays focused on the audit's
explicit items.
Made with Cursor