Skip to content

fix(spec): report what a review run wrote outside its spec folder - #3117

Open
mlorentedev wants to merge 2 commits into
mainfrom
fix/review-stray-writes
Open

mlorentedev wants to merge 2 commits into
mainfrom
fix/review-stray-writes

Conversation

@mlorentedev

Copy link
Copy Markdown
Owner

Part of #1649 (option 2, the floor). Option 1, running the reviewer in an isolated worktree, stays open on the issue.

Summary

A reviewer's only sanctioned output is review.md, but nothing checked that. Probes, scratch dumps and staged restores were left in the reviewed tree and found by hand five times.

  • spec deadline wraps every reviewer in both modes. When --review is given it now snapshots git status before and after the run, and names each path whose status changed outside specs/<id>/: [review] wrote outside specs/<id>/: <path>.
  • When the runner otherwise exited 0, the command exits 4. A runner failure keeps its own exit code.
  • A snapshot that fails is reported as NOT checked, never as clean.
  • The review prompt tells the reviewer to keep probes under mktemp -d, and that leftovers fail the run.

SDD skip rationale

The owner approved skip-sdd for this change. Exit 4 is the acceptance criterion the issue itself wrote: the run's verdict says so rather than exiting clean. spec deadline is hidden plumbing that only the review launcher calls, so no user-facing contract changes. A spec gated on #1649 would have to close the issue, while option 1 is still open on it. Most of the 249 lines are tests.

Verification

$ go build ./... && go vet ./... && GOOS=windows go vet ./...    # clean
$ go test ./internal/spec/ ./internal/cmd/ -skip TestEmbeddedTemplatesMatchVault -count=1
ok  	github.com/mlorentedev/dotfiles/cli/internal/spec	19.474s
ok  	github.com/mlorentedev/dotfiles/cli/internal/cmd	27.125s
$ golangci-lint run ./internal/spec/ ./internal/cmd/ ; GOOS=windows golangci-lint run ./internal/spec/ ./internal/cmd/
0 issues.
0 issues.

stray_writes_test.go has no build tag and was only run on macOS. The test (windows-latest) job is its first run on Windows. The end-to-end test in spec_deadline_stray_unix_test.go is Unix-only, because it needs a shell runner.

Knowledge

A reviewer's only sanctioned output is review.md, but nothing checked it:
probes, scratch dumps and staged restores were left in the reviewed tree
and found by hand five times (#1649). spec deadline, which wraps every
reviewer in both modes, now snapshots git status before and after the run
when --review is given, names each path changed outside specs/<id>/, and
exits 4 when the run otherwise exited 0. A snapshot that fails is reported
as unchecked, never as clean. The prompt tells the reviewer to keep probes
under mktemp -d and that leftovers fail the run.
@mlorentedev mlorentedev added the skip-sdd SDD spec-gate escape hatch (requires '## SDD skip rationale' in PR body) label Nov 3, 2026
@mlorentedev
mlorentedev marked this pull request as ready for review November 3, 2026 05:51
@coderabbitai

coderabbitai Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 56 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between 1a6a8b0 and 2cc1a63.

Files selected for processing: 5
  • cli/internal/cmd/spec.go
  • cli/internal/cmd/spec_deadline_stray_unix_test.go
  • cli/internal/spec/review_launch.go
  • cli/internal/spec/stray_writes.go
  • cli/internal/spec/stray_writes_test.go
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.

Comment on lines +55 to +64
func (before TreeSnapshot) StrayWrites(after TreeSnapshot) []string {
var stray []string
for path, code := range after.entries {
if before.entries[path] != code {
stray = append(stray, path)
}
}
sort.Strings(stray)
return stray
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Blind spot: deletions of pre-existing untracked files

Class: THEORETICAL
StrayWrites iterates only after.entries, so a path present in the before
snapshot but absent after is never reported. If the reviewer deletes an
untracked file that already existed before the run (an operator's scratch
file, a .env.local, an untracked config), the tree changed outside the
spec folder yet the run exits 0 with no mention. Same for a file moved
from outside the spec folder into it, which silently relocates operator
content under specs//. The doc comment acknowledges state-vs-content
comparison but not this deletion direction. Fix: also walk before.entries
and report paths missing from after.

Comment thread cli/internal/cmd/spec.go
Comment on lines +613 to +630
func reportStrayWrites(errw io.Writer, review string, before spec.TreeSnapshot, snapErr error) bool {
if review == "" {
return false
}
after, err := before, snapErr
if err == nil {
after, err = spec.SnapshotTree(review)
}
if err != nil {
_, _ = fmt.Fprintf(errw, "[review] writes outside the spec folder were NOT checked: %v\n", err)
return false
}
stray := before.StrayWrites(after)
for _, p := range stray {
_, _ = fmt.Fprintf(errw, "[review] wrote outside %s: %s\n", before.Folder(), p)
}
return len(stray) > 0
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Snapshot failure exits clean

Class: THEORETICAL
When the snapshot cannot be taken (snapErr non-nil, e.g. --review points
outside a git repository), reportStrayWrites prints "NOT checked" and
returns false, so a run whose reviewer left strays exits 0. The issue's
acceptance says the verdict "says so rather than exiting clean"; a stderr
line satisfies the letter of the PR description, but an operator scripting
on exit status treats an unchecked run as clean. Consider a distinct exit
code or at least confirming this asymmetry is intended.

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

1649 - Partially compliant

Compliant requirements:

  • Stray writes outside the spec folder are named on stderr with exact paths, and a clean-exiting run then exits 4
  • Mechanism is git-status-based, portable to both arms (no sandbox flag dependency)
  • End-to-end test uses a shell runner that creates files outside the spec folder and asserts exit 4 and the reported paths
  • --add-dir reachability untouched; only post-run reporting added

Non-compliant requirements:

  • "git status --porcelain is empty after a run" is weakened to "changes are reported and exit 4"; the tree is not cleaned or isolated (option 1 stays open, acknowledged in the PR body)

Requires further human verification:

  • Whether the review launcher always invokes spec deadline with --review in both arms, so the check is actually reachable in production
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified

⚙️ Agent run details
  • Model: openai/glm5.3-flash
  • Tokens: 19,691 in / 1,967 out / 21,658 total
  • Time cost: 43.8s
  • AI calls: 1

A path only the before snapshot names was dirty and is now clean or
gone: an operator's untracked file the reviewer deleted left no status
line afterwards and passed unreported.
@mlorentedev

Copy link
Copy Markdown
Owner Author

Review triage

Item Disposition Reason
cli/internal/spec/stray_writes.go:64: deletions of pre-existing untracked files apply Real gap. A path only the before snapshot named was never reported. Fixed in 2cc1a63a: StrayWrites also walks the before side. TestStrayWritesSeeAnUntrackedFileRemoved failed before the fix ([], want ["notes.txt"]).
cli/internal/cmd/spec.go:630: snapshot failure exits clean skip Intended. Exit 4 means "the reviewer wrote outside its folder". A run whose snapshot failed has not shown that, so exit 4 would misattribute it, and failing the run would discard a valid review.md over a git error. The failure is printed as NOT checked, never as clean. A distinct exit code would be a new contract, beyond this issue.
Ticket compliance: "tree is not cleaned or isolated" skip Acknowledged in the PR body. That is option 1, and it stays open on #1649.
Ticket compliance: is --review always passed in both arms? skip Verified. reviewerArgv has one caller (cmd/spec.go:204), shared by the tmux and the foreground arm. It always passes specDir/review.md, and deadlineWrap adds --review whenever that is non-empty.

Reviewer output dispositioned: github-actions (PR-Agent), 2026-11-03T06:30:22Z. CodeRabbit did not review (rate limited). The PR author did this triage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Review effort 3/5 skip-sdd SDD spec-gate escape hatch (requires '## SDD skip rationale' in PR body)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant