feat(annotate): add strict atomic result output#1091
Open
rNoz wants to merge 2 commits into
Open
Conversation
This was referenced Jul 20, 2026
rNoz
marked this pull request as ready for review
July 20, 2026 13:22
Adopt the grep convention for the strict annotate gate's exit codes: 0 = approved, 1 = negative human outcome (annotated/dismissed under --require-approval), 2 = the gate itself was misconfigured or could not start/deliver a decision. Previously all usage/startup/validation failures shared exit 1 with "reviewer did not approve", so callers could not tell a denied review from a broken gate. - parseStrictAnnotateOptions failures (bad flag combos, strict flags outside annotate --gate --json) now exit 2 - --result-file preflight failures (missing parent, pre-existing or dangling-symlink destination) now exit 2 - post-decision publication failures (destination raced into existence, hard links unavailable, stdout write failure) now exit 2: they deliver no decision record at all, so the code's own fail-closed handling presents them as environment errors, never as a reviewer outcome -- and never approval, since only 0 means approved - decision outcomes keep 0/1 exactly as before; signal deaths keep 128+n - document the contract in AGENTS.md and the annotate-gates guide Claude-Session: https://claude.ai/code/session_01YXkgsNucxDwAL4GdR4XYRk
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
Direct
plannotator annotategates now support optional fail-closed approval status and atomic structured-result files.--require-approvalmakes annotated and dismissed decisions exit nonzero after their JSON is published.--result-file <path>writes the exact newline-terminated stdout record through a private same-directory temporary file and atomically publishes it without overwriting an existing destination.Existing plaintext, JSON, and hook-native paths still use the legacy emitter and retain their current output and zero exit status.
Implements #1090
Why
Structured stdout identifies each annotate decision, but every legacy decision exits successfully. Automation must parse stdout before it can distinguish approval from requested changes.
Detached callers also need an atomic readiness boundary. Shell redirection can expose an empty or partial file, while a pre-existing or concurrently created destination can be mistaken for the current result. Atomic no-clobber publication makes one complete JSON record the only successful file outcome.
This is not a crash-durability guarantee: file data is synced before hard-link publication, but the parent directory is not synced.
Changes
--require-approvaland--result-file <path>.annotate --gate --json, reject--hook, reject non-annotate commands, and reject missing or duplicate values.--require-approval.0600same-directory temporary file, write, file sync, close, atomic hard link, and temporary unlink.Automated validation
At
rnoz/feat-annotate-strict-result5097c557, based onupstream/main2594b374:bun test apps/hook/server/cli.test.ts apps/hook/server/annotate-command.test.ts apps/hook/server/strict-annotate-result.test.ts: 35 passed, 0 failed, 120 assertions.bun run typecheck: passed.bun run build:marketing: passed; Astro built 40 pages.bun run --cwd apps/review build && bun run build:hook: passed; Vite transformed 3,511 review modules and 5,581 hook modules.git diff --check: passed.Evaluated along with other in-flight PRs I am pushing, for the final E2E tests in real sessions (see disclaimer note for the PRs mentioned):
Live strict direct-gate validation
These are ad hoc E2E tests I have manually performed for this branch, but remember that I am using the integration branch with all these contributions in real projects. The ad hoc tests where specifically done with low reasoning models more like a threshold "check". I am using all this on a daily basis, flawlessly.
All file-review runs used stable project paths so the reviewed source could be revised and reopened:
1. The exact same source path was revised and reopened with history/diff, then approved with exit0and noteThanks. I confirm I see the diff.dismissedand exit1. The same path was reopened and then approved with exit0. This run used the then-current local integration, which included the now-deferred close-signal prototype; it proves strict dismissed serialization and exit pairing, not ownership or correctness of browser-close detection.0and noteSome fake note, and consumed the authoritative result. Earlier Copilot attempts exposed private adapter instructions and were harness failures, not product failures.1, emittedResult file parent does not existon stderr, and created no result file.validation/live/approval-ui-v2.stdout.logandvalidation/live/approval-ui-v2.result.jsoncontain matching JSON lines and feedback. No final-UI process exit code was captured. Automated coordinator tests verify approved exit0and exact stdout/result byte equality. Approval-copy details belong to the separate structured-approval UX change.Manual testing gaps
Compatibility
Strict behavior is opt-in. Without
--require-approvalor--result-file, annotate still delegates to the existing plaintext, JSON, or hook-native emitter and exits0.The new flags are parsed only by the direct binary. They are not added to the shared OpenCode or Pi slash-command parser.
--hookremains unchanged and rejects either strict flag.Result-file publication never copies, truncates, or overwrites the requested destination. The hard link is the only publication point, so a destination created after startup validation causes the command to fail closed.