Skip to content

fix(store): key the review store and lock by target, not parent dir (#199) - #200

Merged
adnaan merged 1 commit into
mainfrom
issue-199
Jul 27, 2026
Merged

fix(store): key the review store and lock by target, not parent dir (#199)#200
adnaan merged 1 commit into
mainfrom
issue-199

Conversation

@adnaan

@adnaan adnaan commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Fixes #199.

The bug

A single-file review normalizes its root to the file's parent directory, and every identity hung off that directory: the store (<dir>/.prereview/), the "one server per store" pid lock inside it, and the CLI's --out resolution. So two reviews of different files that happened to share a directory were one review — the second launch was refused, or with --replace it killed the first, and both fed off one comments.csv. It fires constantly on Claude Code plan files, which all live in ~/.claude/plans/.

#171 had already noticed half of this and added a filter (session.json + inScope). A filter cannot make two servers coexist: events.jsonl / session.json / llm-status.json are single-writer, and persist() rewrites the whole CSV from one server's in-memory buffer.

The fix

  • Store keyed by target<root>/.prereview/files/<name>-<hash>/, hashed on the absolute target path (storeDirFor). Every store path already derives from filepath.Dir(csvPath), so relocating the directory relocates the whole store — including server.pid, which makes the lock per-target for free. Repo, plain-directory and --external reviews are unchanged.
  • New STORE preamble line; REPO untouched. REPO could not be repointed at the store: it is also the base the agent resolves a comment's file column against, so pointing it at the store would leave the agent unable to find the document it is reviewing — exactly the plan-file case in the issue. REPO = where the files are, STORE = where the annotations are (--out for every subcommand).
  • --out takes three forms — the STORE path, the reviewed file, or a directory. A directory anchor whose store holds only per-file stores now errors and names them, instead of hanging a stale skill's watch on an event log nothing writes.
  • Upgrade carries comments and their state — the reviewed file's rows plus the id-keyed sidecars: processed.jsonl + reenqueued.jsonl (doneness is the count of one minus the other, so both are filtered by the same id set) and the thread-reply logs. Comments alone would re-queue work the agent had already finished. It copies: the shared store may still be a directory review's live store.
  • Cross-file quiz bleedQuizResults was the last annotation surface walking every file in the store; now scoped by inScope.

Behavior change worth knowing

A single-file review and a directory review of the same directory are now separate reviews and do not see each other's comments. Documented in skill/reference.md and docs/cli.md.

Tests

  • Unit: slug stability/safety, storeDirFor, the three --out forms, the loud error, and the carry-over (filtering, idempotence, sidecars).
  • New chromedp e2e: boots two single-file reviews in one directory and asserts through a real browser that the second launch needs no --replace, the first server keeps serving, and neither page shows the other's work.
  • e2e/e2e_scope_test.go now seeds the legacy shared store, so it doubles as the upgrade test.
  • Local: go test ./... ✅ · make tmpl-check ✅ · full -tags browser sweep 145/145 ✅.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RwpqeDN9UwdseVzLC4K3GB

…199)

A single-file review normalizes its root to the file's PARENT directory, and
every identity hung off that directory: the store (<dir>/.prereview/), the
"one server per store" pid lock inside it, and the CLI's --out resolution. Two
reviews of DIFFERENT files that happened to share a directory were therefore
one review — the second launch was refused, or with --replace it killed the
first, and both fed off one comments.csv. It fires constantly on Claude Code
plan files, which all live in ~/.claude/plans/.

#171 had already noticed half of this and added a filter (session.json +
inScope), but a filter cannot make two servers coexist: events.jsonl,
session.json and llm-status.json are single-writer, and persist() rewrites the
whole CSV from one server's in-memory buffer.

- Namespace single-file stores as <root>/.prereview/files/<name>-<hash>/,
  keyed on the absolute target path (storeDirFor). Every store path already
  derives from filepath.Dir(csvPath), so relocating the directory relocates the
  whole store — including server.pid, which makes the lock per-target for free.
  Repo, plain-directory and --external reviews are unchanged.
- Print the resolved store as a new STORE preamble line. REPO could not be
  repointed at it: REPO is also the base the agent resolves a comment's `file`
  column against, so pointing it at the store would leave the agent unable to
  find the document it is reviewing.
- --out now accepts the STORE path, the reviewed file, or (as before) a
  directory. A directory anchor whose store holds only per-file stores errors
  and names them, instead of hanging a stale skill's `watch` on an event log
  nothing writes.
- Carry a pre-#199 shared store's rows for the reviewed file into its own store
  on first launch, together with the id-keyed sidecars — processed.jsonl +
  reenqueued.jsonl (doneness is the count of one minus the other, so both are
  filtered by the same ids) and the thread-reply logs. Comments alone would
  re-queue work the agent had already finished. It COPIES: the shared store may
  still be a directory review's live store.
- Scope QuizResults by inScope. It was the last annotation surface walking every
  file in the store, and the source of the cross-file quiz in the report.

Tests: unit coverage for the slug, the three --out forms, the loud error and the
carry-over (including its sidecars); a new chromedp e2e boots two single-file
reviews in one directory and asserts through a real browser that the second
launch needs no --replace, the first server keeps serving, and neither page
shows the other's work. e2e_scope_test.go now seeds the legacy shared store, so
it doubles as the upgrade test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RwpqeDN9UwdseVzLC4K3GB
@adnaan
adnaan merged commit 667e6bc into main Jul 27, 2026
11 checks passed
@adnaan
adnaan deleted the issue-199 branch July 27, 2026 05:52
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.

Single-file reviews of different files in the same directory share one store and evict each other

1 participant