Skip to content

fix(review): reject completed tasks with empty reviewer results#1550

Merged
Alan-TheGentleman merged 18 commits into
Gentleman-Programming:mainfrom
MataM15:fix/1455-reject-empty-reviewer-result
Jul 22, 2026
Merged

fix(review): reject completed tasks with empty reviewer results#1550
Alan-TheGentleman merged 18 commits into
Gentleman-Programming:mainfrom
MataM15:fix/1455-reject-empty-reviewer-result

Conversation

@MataM15

@MataM15 MataM15 commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

🔗 Linked Issues

Closes #1454
Closes #1555
Closes #1575
Closes #1579
Closes #1259
Closes #1264
Closes #1609

Adds regression proof for #1215 and preserves the original contributor work from already-closed #1455.


🏷️ PR Type

  • type:bug
  • type:feature
  • type:docs
  • type:refactor
  • type:chore
  • type:breaking-change

📝 Summary

  • Introduces one provider-owned artifact subject and admission boundary for reviewer, correction, and validator evidence.
  • Rejects unavailable inspection, stale/out-of-scope findings, unsupported causal claims, malformed extraction, and mismatched candidate bindings before consuming immutable result slots.
  • Adds exact-revision review reopen-results recovery that retains valid admitted slots and quarantines unusable artifacts without resetting lineage, scope, risk, lenses, or budget.
  • Reuses predecessor correction evidence only when tranche-6 relation proof and exact candidate/validation identities compose.
  • Makes restart STATUS sufficient to relaunch missing reviewers with exact frozen subject, diff, and path/mode manifest, completing fix(review): expose immutable candidate diff and changed-path manifest from START #1454.

📂 Changes

Area Change
Artifact identity Canonical ArtifactSubject, admitted-result envelope, raw/canonical digests, exact lens/order/revision/target/correction bindings.
Admission Bounded extraction, full inspection proof, repository-bound finding locations, causal classification, strict IDs, pathless diagnostics.
Recovery review reopen-results with maintainer authorization, store-lock re-admission, selective retention/quarantine, exact replay and mixed-slot refinalization.
Evidence composition Narrow accounting-only predecessor evidence import through tranche-6 relation proof and provider targeted-validation request.
Restart flow STATUS/next-transition and managed OpenCode receive immutable subject, candidate diff, and complete path/mode manifest without a new review budget.
Contracts Additive capabilities v1.3, START/status/result-artifact v2 and subject/admitted-result v1 schemas/fixtures while preserving strict v1/v1.2 bytes.
Portability Windows-safe Git fixture configuration and cross-platform contract/build coverage.

🧪 Test Plan

  • go test ./... -count=1
  • Focused/repeated/race suites for reviewtransaction, CLI, update and upgrade
  • go vet ./...
  • go run ./internal/gofmtcheck
  • Review contract and capabilities scripts
  • OpenCode plugin dynamic probe/build
  • 34 JSON contract documents and 34 golden suites
  • Linux, Darwin and Windows cross-builds plus Windows test compilation
  • Independent final review: 0 blocker, 0 critical, 0 warning, 0 suggestion

✅ Contributor Checklist

  • Every linked open issue has status:approved
  • Exactly one type:* label
  • Maintainer size:exception applied because admission, recovery and versioned contracts share one authority invariant
  • All original community commits and authorship preserved without rebase or force-push
  • Conventional commits with no AI attribution
  • Documentation, plugin, schemas, fixtures and goldens updated together

Review Evidence

  • HEAD: bfdffbca31c3d13bd8653a416ccebb329d5ee4f3
  • Tree: 47daaa50483481968fd3e93519cab66a7630d8ce
  • Parents: 1f9af714 + 56720ef0
  • Diff against current main: 80 files, +6192/-279
  • Merge composition: zero overlapping paths and empty remerge diff

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds durable reviewer-slot acquisition, acquisition-bound capture and disposal, distinct empty-result and nested-envelope classifications, class-aware incident artifacts, and recovery tests covering validation, idempotence, retries, and escalation.

Changes

Bound acquisition and classified incident recovery

Layer / File(s) Summary
Durable acquisition records and binding validation
internal/reviewtransaction/..., internal/cli/review_artifact.go, internal/cli/review_artifact_test.go
Adds immutable acquisition records and exact binding checks, exposes review acquire-result, and requires acquisition IDs for capture operations.
Classified incident preservation
internal/reviewtransaction/compact.go, internal/cli/review_incident.go, internal/cli/review_incident_test.go
Defines incident classes, validates and records --class, generates class-aware filenames, and tests duplicate and interrupted recovery.
Plugin acquisition and classification propagation
internal/assets/opencode/plugins/review-result-artifacts.ts, internal/assets/assets_test.go
Injects acquisition manifests, separates empty and nested result failures, and forwards classifications to incident preservation.
Class-aware disposal and recovery binding
internal/reviewtransaction/compact_result_disposition.go, internal/cli/review_dispose_result.go, internal/cli/review_dispose_result_test.go
Resolves class-less or class-suffixed artifacts, validates acquisition bindings, and verifies escalation of preserved incidents.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Plugin as OpenCode plugin
  participant Acquire as review acquire-result
  participant Capture as review capture-result
  participant Preserve as review preserve-result
  participant Dispose as review dispose-result
  participant Store as CompactStore
  Plugin->>Acquire: reserve reviewer slot
  Acquire->>Store: persist acquisition
  Plugin->>Capture: submit result with acquisition ID
  Capture->>Store: validate exact binding
  Plugin->>Preserve: preserve classified failure
  Preserve->>Store: write incident artifact
  Dispose->>Store: resolve artifact and validate acquisition
  Store-->>Dispose: escalation disposition
Loading

Possibly related issues

Possibly related PRs

Suggested labels: type:bug, size:exception, priority:high

Suggested reviewers: alan-thegentleman

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy #1455 by separating empty vs nested failures, preserving raw evidence, and adding atomic slot-bound recovery plus tests.
Out of Scope Changes check ✅ Passed No clear out-of-scope changes; the acquisition, classification, CLI, and plugin updates all support the linked review workflow.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches a real part of the change: rejecting empty reviewer results, even though it omits the new acquisition and nested-envelope work.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

Comment @coderabbitai help to get the list of available commands.

@dnlrsls dnlrsls left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Request changes: this currently proves idempotent result publication, not exactly-once recovery acquisition.

tool.execute.before only runs the non-mutating capture preflight. The slot is not acquired or durably marked until CaptureReviewerResult, which runs after the reviewer has already executed. Two concurrent recoveries can therefore both pass preflight and launch the same lens, and a crash after launch but before capture leaves the slot immediately re-runnable. Even after a successful capture, another preflight still passes because it does not check or reserve the captured slot.

The duplicate/interrupted recovery tests start at RunReviewCaptureResult, so they do not exercise duplicate reviewer launches. This conflicts with #1455's explicit requirement that concurrent or repeated recovery attempts cannot acquire the slot twice, and with its warning that retry is unsafe once execution may have occurred.

Please add an atomic, durable acquisition/unknown-outcome transition before task launch, bind capture or disposition to that acquisition, reject concurrent/repeated acquisition, and test the launch and crash boundaries. The incident classification and raw-preservation work otherwise looks solid.

@MataM15

MataM15 commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review — you were right, and this is fixed.

What changed

Added durable pre-launch acquisition, per your request:

  • New review acquire-result command — atomically and durably reserves the exact (lineage, target, lens, order) slot under the store lock before the reviewer task launches, recording an immutable unknown_outcome state (internal/reviewtransaction/compact_result_acquisition.go). A concurrent or repeated acquisition attempt for the same slot is now rejected outright — only one launch can ever win it.
  • capture-result and dispose-result now require --acquisition <ID>, validated with exact live-store binding equality — both terminal outcomes are bound to the specific acquisition that authorized the launch, not just to the lineage/target/lens/order tuple.
  • preserve-result requires --acquisition too, but only format-validates it (not against the live store) — this is a deliberate, narrow deviation: it must still durably preserve evidence even when called from a repository that can't resolve the reviewing lineage's live store (an existing, pinned test relies on that). Capture and dispose — the two operations that can actually consume a slot or terminally escalate a lineage — enforce the full live check.
  • Plugin (review-result-artifacts.ts): tool.execute.before now calls acquire-result instead of the old read-only --preflight, fails closed on any error (no more graceful degrade on version skew — an old binary without acquire-result now blocks the launch instead of silently bypassing acquisition), and threads the acquisition ID through as GENTLE_AI_REVIEW_ACQUISITION beside the existing GENTLE_AI_REVIEW_BINDING.

Net effect: two concurrent recoveries can no longer both launch the same lens, and a crash after launch but before capture leaves an auditable unknown_outcome record instead of a silently re-runnable slot.

Size

This landed as 3 separate commits instead of amending the original 6 — the full remediation is 747 changed lines (685 additions, 62 deletions), over the 400-line single-PR budget, so it's delivered as a commit-per-unit chain on the same branch/PR rather than forced into one oversized diff:

  • 885c954 — durable pre-launch reviewer-result acquisition (domain + store, 316 lines)
  • 68cb28f — require acquisition binding on capture, preserve, and dispose (278 lines)
  • 74a38f3 — acquire before launch in the review-result plugin (153 lines)

Test plan

  • go test ./internal/reviewtransaction/... — green, including new acquisition tests (first acquisition, exactly-one-winner concurrency, stale-target no-write, captured-slot rejection, crash/repeat-after-acquisition retry refusal)
  • go test ./internal/cli/... — green, including retrofit of existing capture/preserve/dispose tests to acquire the slot first, plus new terminal-binding tests
  • go test ./internal/assets/... — green, plugin contract tests updated for GENTLE_AI_REVIEW_ACQUISITION propagation and fail-closed version skew
  • go test ./... — full repo green, zero regressions
  • go vet ./... — clean

Happy to split the preserve-result format-only validation differently if you'd rather it fail without live-store access too — wanted to flag that trade-off explicitly rather than bury it.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/reviewtransaction/compact_result_disposition.go (1)

114-134: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Avoid globbing the full incidents path. CompactIncidentsDir is derived from the repo’s on-disk Git common dir, so a checkout path containing [ or ] can make filepath.Glob(filepath.Join(incidents, ...)) miss every artifact. Match basenames from os.ReadDir, or escape the parent path before globbing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/reviewtransaction/compact_result_disposition.go` around lines 114 -
134, Update the artifact lookup around filepath.Glob in the compact-result
disposition function to avoid interpreting the incidents directory path as a
glob pattern. Enumerate entries with os.ReadDir and match candidate basenames
against the existing order/lens/digest naming pattern, preserving the zero-,
one-, and multiple-match behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@internal/reviewtransaction/compact_result_disposition.go`:
- Around line 114-134: Update the artifact lookup around filepath.Glob in the
compact-result disposition function to avoid interpreting the incidents
directory path as a glob pattern. Enumerate entries with os.ReadDir and match
candidate basenames against the existing order/lens/digest naming pattern,
preserving the zero-, one-, and multiple-match behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d049e750-3b86-4a35-a4d7-aced9d5ac9ed

📥 Commits

Reviewing files that changed from the base of the PR and between feaa78c and 74a38f3.

📒 Files selected for processing (16)
  • internal/assets/assets_test.go
  • internal/assets/opencode/plugins/review-result-artifacts.ts
  • internal/cli/review_artifact.go
  • internal/cli/review_artifact_test.go
  • internal/cli/review_capabilities.go
  • internal/cli/review_dispose_result.go
  • internal/cli/review_dispose_result_test.go
  • internal/cli/review_facade.go
  • internal/cli/review_incident.go
  • internal/cli/review_incident_test.go
  • internal/reviewtransaction/compact_reclaim.go
  • internal/reviewtransaction/compact_result_acquisition.go
  • internal/reviewtransaction/compact_result_acquisition_test.go
  • internal/reviewtransaction/compact_result_disposition.go
  • internal/reviewtransaction/compact_result_disposition_test.go
  • internal/reviewtransaction/compact_store.go

@MataM15

MataM15 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Flagging a process overlap rather than acting on it unilaterally — this needs a maintainer call, not something I should decide from this side.

#1455 was closed by #1549 merging (decode2, merged 2026-07-20T16:49:27Z). That PR only distinguishes empty_result/nested_envelope inside the Go capture path — it does not propagate the classification through the plugin, does not add class-aware preservation/disposition, and does not acquire the (lineage, target, lens, order) slot before the reviewer task runs.

@dnlrsls left CHANGES_REQUESTED on #1549 as well, with essentially the same objection raised here:

Request changes: this is a useful payload-validation improvement, but it does not implement the complete recovery contract required by #1455... Because this PR declares Closes #1455, merging it as-is would close an approved high-priority issue while leaving its atomic-recovery, raw-evidence, and duplicate/interrupted-recovery requirements unresolved. Please either expand it to satisfy the full contract with launch-boundary tests, or narrow its issue linkage so it is treated as a partial prerequisite rather than the complete fix.

It was merged anyway, closing #1455.

This PR (#1550), including the remediation just pushed, implements the full contract that objection describes: durable pre-launch acquisition, exactly-once launch enforcement, and the crash/duplicate-recovery boundary tests. None of that landed in #1549.

Rather than guess at the right outcome, flagging the options for you to pick:

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
internal/cli/review_artifact.go (2)

56-59: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Read errors are masked behind a generic "required" message.

If readFacadeBytes fails for a real reason (permission denied, path not found), the error is discarded and replaced with "final verification evidence is required," hiding the actual cause from callers/automation.

🐛 Proposed fix to preserve the underlying error
 	payload, err := readFacadeBytes(*input)
-	if err != nil || len(payload) == 0 || len(payload) > reviewResultArtifactLimit {
-		return reviewPreflightError(errors.New("final verification evidence is required"))
-	}
+	if err != nil {
+		return reviewPreflightError(fmt.Errorf("read final verification evidence: %w", err))
+	}
+	if len(payload) == 0 || len(payload) > reviewResultArtifactLimit {
+		return reviewPreflightError(errors.New("final verification evidence is required"))
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/review_artifact.go` around lines 56 - 59, Update the validation
around readFacadeBytes in the review artifact preflight flow to preserve and
return the underlying read error when err is non-nil, while retaining the
existing generic required-evidence error for empty or oversized payloads. Keep
the current reviewPreflightError wrapping behavior and size checks unchanged.

371-395: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Leaked .capture-digest-* temp file on error paths.

Unlike the sibling temp-file patterns in this same file (lines 72-77, 329-330), persistReviewerArtifactDigest never registers a cleanup for its temp file. If Chmod, WriteString, Sync, or Close fails, the .capture-digest-* file is left on disk permanently (only the fd is closed via the deferred temp.Close()).

🐛 Proposed fix matching the sibling cleanup pattern
 	temp, err := os.CreateTemp(dir, ".capture-digest-*")
 	if err != nil {
 		return err
 	}
+	owned, _ := temp.Stat()
+	defer removeOwnedArtifact(temp.Name(), owned)
 	defer temp.Close()
 	if err := temp.Chmod(0o600); err != nil {
 		return err
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/review_artifact.go` around lines 371 - 395, Update
persistReviewerArtifactDigest to register cleanup for the temporary file
immediately after os.CreateTemp succeeds, using the existing sibling temp-file
cleanup pattern. Ensure the cleanup removes temp.Name() on Chmod, WriteString,
Sync, Close, and publish error paths while preserving the successful publish
flow.
internal/reviewtransaction/compact_reclaim.go (1)

203-235: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Close the remaining quarantine-root swap window. ensureCanonicalReviewQuarantineRoot still leaves a path-based TOCTOU gap between the last check and os.MkdirTemp(quarantineRoot, ...); a symlink swap there can redirect where the quarantine directory is created. Use os.Root/os.OpenInRoot to make the quarantine operations fd-scoped.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/reviewtransaction/compact_reclaim.go` around lines 203 - 235,
Replace the path-based quarantine creation and subsequent operations guarded by
ensureCanonicalReviewQuarantineRoot with fd-scoped operations using os.Root and
os.OpenInRoot, rooted at the validated review-transactions authority directory.
Ensure os.MkdirTemp and quarantine inspection/rename operations resolve through
the opened root so a symlink swap cannot redirect them, while preserving the
existing canonical-root validation behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/cli/review_artifact.go`:
- Line 233: Update the upfront validation in the review artifact command to
require a non-empty acquisition flag whenever preflight is disabled, matching
the contract declared by the acquisition flag help text. Add this check
alongside the existing input validation before store.ReadResultAcquisition,
while preserving the current preflight exception and specific required-flag
error behavior.

---

Outside diff comments:
In `@internal/cli/review_artifact.go`:
- Around line 56-59: Update the validation around readFacadeBytes in the review
artifact preflight flow to preserve and return the underlying read error when
err is non-nil, while retaining the existing generic required-evidence error for
empty or oversized payloads. Keep the current reviewPreflightError wrapping
behavior and size checks unchanged.
- Around line 371-395: Update persistReviewerArtifactDigest to register cleanup
for the temporary file immediately after os.CreateTemp succeeds, using the
existing sibling temp-file cleanup pattern. Ensure the cleanup removes
temp.Name() on Chmod, WriteString, Sync, Close, and publish error paths while
preserving the successful publish flow.

In `@internal/reviewtransaction/compact_reclaim.go`:
- Around line 203-235: Replace the path-based quarantine creation and subsequent
operations guarded by ensureCanonicalReviewQuarantineRoot with fd-scoped
operations using os.Root and os.OpenInRoot, rooted at the validated
review-transactions authority directory. Ensure os.MkdirTemp and quarantine
inspection/rename operations resolve through the opened root so a symlink swap
cannot redirect them, while preserving the existing canonical-root validation
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f4347045-e17d-4043-bbd3-ed3591f423ac

📥 Commits

Reviewing files that changed from the base of the PR and between 74a38f3 and 01ece33.

📒 Files selected for processing (6)
  • internal/assets/assets_test.go
  • internal/cli/review_artifact.go
  • internal/cli/review_artifact_test.go
  • internal/cli/review_capabilities.go
  • internal/cli/review_facade.go
  • internal/reviewtransaction/compact_reclaim.go

Comment thread internal/cli/review_artifact.go Outdated
MataM15 added a commit to MataM15/gentle-ai that referenced this pull request Jul 21, 2026
Match the flag's own documented "required unless --preflight"
contract with an explicit check, instead of relying on
ReadResultAcquisition's internal empty-ID error deep in the function.
Checked only after binding/repository validation so a wrong --cwd or
stale lineage/target/lens/order still surfaces its own actionable
error first (CodeRabbit finding on PR Gentleman-Programming#1550).
@MataM15

MataM15 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Verified each finding against current code before touching anything:

  • review_artifact.go:233 (--acquisition upfront) — valid, fixed in 6802c39. Added an explicit check matching the flag's own "required unless --preflight" contract, placed after binding/repository validation (not before) so a wrong --cwd or stale lineage/target/lens/order still gets its own actionable error first — the existing TestReviewCaptureResultNestedRepositoryFailsActionablyAndStaysRetriable pins that ordering.
  • review_artifact.go:56-59, 371-395, and compact_reclaim.go:203-235 — all three are pre-existing code from main (commits fe7e5c81 and ddd1944a, confirmed via git blame), not introduced by this PR. Out of scope here; left untouched to keep this PR's diff to what it's actually responsible for.

go build, go vet, and go test ./... all green after the fix.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/cli/review_artifact.go (1)

175-222: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Add a recovery path for failed acquisition publication.
review acquire-result reserves the slot before encodeReviewJSON writes the ID. If stdout closes or the write fails, the durable acquisition stays consumed but the caller never gets its ID, and a retry is rejected as a duplicate. Surface the existing acquisition through a read/replay path or another durable handoff, and add a broken-stdout test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/review_artifact.go` around lines 175 - 222, Update
RunReviewAcquireResult so a failure from encodeReviewJSON does not leave the
acquired slot inaccessible: retain the acquisition ID and expose an existing
read/replay or durable handoff path that callers can use to recover it, while
preserving duplicate protection. Add a test covering broken stdout and verifying
the acquisition remains recoverable through that path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@internal/cli/review_artifact.go`:
- Around line 175-222: Update RunReviewAcquireResult so a failure from
encodeReviewJSON does not leave the acquired slot inaccessible: retain the
acquisition ID and expose an existing read/replay or durable handoff path that
callers can use to recover it, while preserving duplicate protection. Add a test
covering broken stdout and verifying the acquisition remains recoverable through
that path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1cc83d19-33c9-4575-a7f9-0cf6d27ae63e

📥 Commits

Reviewing files that changed from the base of the PR and between 01ece33 and b0b0914.

📒 Files selected for processing (2)
  • internal/cli/review_artifact.go
  • internal/cli/review_artifact_test.go

MataM15 added 13 commits July 21, 2026 11:49
Add a dedicated ResultIncidentClass type (empty_result, nested_envelope)
distinct from ResultDispositionClass, since disposition classes judge
candidate-inapplicability of a decodable payload while incident classes
describe why the plugin could never extract a payload at all.
Add the optional --class flag to review preserve-result and reject any
value outside the closed ResultIncidentClass enum (or empty) before any
filepath.Join or incident-directory write, so a bogus or path-traversal
class (e.g. --class ../evil) is rejected without touching disk.
Thread --class into preserveIncidentArtifact and reviewIncidentArtifact.Class,
and extend the content-addressed incident filename from
order-lens-digest12 to order-lens-class-digest12 when a class is present.
Omitting --class keeps the old filename shape and empty Class, so existing
callers stay backward-compatible.
Replace the single combined throw in reviewerResult() with two classified
errors (reviewClass: empty_result | nested_envelope), add an
extractionClass(cause) reader, and thread the class through preserveResult
into --class so preservedCaptureFailure forwards it on extraction failure.
Assert the old conflated message ("reviewer task result is empty or
contains a nested envelope") is absent from the plugin source, so a
future edit cannot silently regress the two classified throws back into
one indistinguishable free-text error.
…rios

Add regression coverage for AC scenarios 6 and 7: a duplicate recovery
capture for the identical slot is idempotent with no revision bump, and a
recovery capture interrupted by a rejected payload leaves the slot cleanly
retryable through the existing store-lock + CAS in CaptureReviewerResult.
Both pass against the existing atomicity with no new primitive.
preserveIncidentArtifact writes incident filenames with a class suffix
(order-lens-class-digest12) whenever --class is non-empty, but
CompactPreservedResultPath used by DisposeUnreplayablePreservedResult
only reconstructed the old class-less shape, causing review
dispose-result to fail to locate any incident preserved with
empty_result/nested_envelope. The lookup now checks the class-less
path first, then falls back to a glob over the incidents directory to
resolve the class-suffixed shape without needing the class as an
input, preserving backward compatibility with existing class-less
incidents.

Found by native bounded review (lineage review-083dd7c132820384),
confirmed by review-resilience and review-reliability lenses.
review acquire-result's launch slot needs an exactly-once authority: the
existing capture-result --preflight only verifies a binding read-only after
the reviewer already ran, so a crash or duplicate recovery attempt could
relaunch the identical bound task with no durable trace.

CompactStore.AcquireReviewerResult atomically publishes an immutable
review-acquisitions/NN-lens.json unknown_outcome record under the store lock
after revalidating the exact lineage, target, lens, and order. Existence of
that record consumes the slot: a stale binding is refused before any write,
an already-captured slot is refused, and a duplicate or post-crash retry of
the identical binding is refused while leaving the original record readable.
ReadResultAcquisition gives terminal callers (capture, disposal) a lock-free
exact-match lookup by ID, lineage, target, lens, and order.
…ispose

Exposes review acquire-result as a new CLI subcommand and makes it the
exactly-once pre-launch authority: capture-result now requires --acquisition
(exempt only for the existing read-only --preflight verification) and
refuses unless it exactly matches the live acquisition record for the bound
lineage, target, lens, and order. dispose-result requires the same exact
binding equality via the acquisition record before it will terminally
escalate a lineage. preserve-result requires --acquisition in the correct
generated-ID shape, but validates it as a format guard only (not against a
live store), since it must keep durably saving evidence even from a
repository that cannot resolve the reviewing lineage's compact store (for
example a nested-worktree misconfiguration) -- only the store-bound terminal
operations (capture-result, dispose-result) prove exact binding equality
against the live acquisition.

Existing capture-result, preserve-result, and dispose-result tests are
updated to acquire the exact slot before exercising a real success path;
error-path tests that fail on an earlier binding check are unaffected, since
the acquisition check runs after binding validation and before payload
handling.
Replaces the read-only preflightCapture check in tool.execute.before with an
atomic review acquire-result call: it durably reserves the launch slot
before the bound reviewer task runs, and any failure -- including an older
gentle-ai binary that does not support acquire-result -- now fails closed
instead of degrading gracefully into a launch with no pre-launch authority.
The acquired ID is injected as GENTLE_AI_REVIEW_ACQUISITION on its own line
immediately after the unchanged GENTLE_AI_REVIEW_BINDING, so the launched
prompt carries it through to tool.execute.after, which recovers it and
threads it into capture-result and preserve-result via --acquisition. A
missing or malformed acquisition surviving launch fails closed with the raw
output embedded in the error rather than silently discarding it.
Match the flag's own documented "required unless --preflight"
contract with an explicit check, instead of relying on
ReadResultAcquisition's internal empty-ID error deep in the function.
Checked only after binding/repository validation so a wrong --cwd or
stale lineage/target/lens/order still surfaces its own actionable
error first (CodeRabbit finding on PR Gentleman-Programming#1550).
… rejection

These two tests were fixed to pass --acquisition after the origin/main
merge (capture-result now requires it before payload validation), but
the fix was left in the working tree only — the merge commit captured
the pre-fix version of this file from its own auto-merge staging.
Tests already ran green against the working tree; this just makes the
history match what was actually tested.
RunReviewAcquireResult durably records the pre-launch acquisition before
encoding its ID to stdout. If that final write fails (broken pipe, crashed
caller), the slot is consumed but the caller never learns its ID, and a
retry over the identical binding is correctly rejected as a duplicate —
leaving the slot permanently unbindable.

Add CompactStore.ReadResultAcquisitionByBinding, a lock-free read that
recovers an existing acquisition by lineage/target/lens/order alone (no ID
required), and wire it behind a new `--replay` flag on `review
acquire-result`. Replay never creates or mutates an acquisition and never
authorizes a new launch: a genuine acquire attempt over an already-acquired
binding still refuses identically, with or without a prior replay.
@MataM15
MataM15 force-pushed the fix/1455-reject-empty-reviewer-result branch from b0b0914 to 362c272 Compare July 21, 2026 11:59

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/cli/review_incident.go (1)

117-135: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Filename-shape contract is duplicated with the reader in compact_result_disposition.go.

preserveIncidentArtifact hard-codes the two incident filename shapes (%02d-%s-%s.raw and %02d-%s-%s-%s.raw) that CompactPreservedResultPath in internal/reviewtransaction/compact_result_disposition.go must independently reconstruct via os.Stat/filepath.Glob to locate the same artifact later. Nothing enforces these two format strings stay in sync across packages; a future edit to one side (e.g. reordering fields, changing the separator) would silently break dispose-result lookups with no compile-time signal, only a runtime "no such file" error. Consider hoisting a single shared helper (e.g. in reviewtransaction, since it already owns CompactIncidentsDir) that both builds and matches the filename, so cli only calls a documented API instead of duplicating the shape.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/review_incident.go` around lines 117 - 135, The incident
artifact filename contract is duplicated between preserveIncidentArtifact and
CompactPreservedResultPath. Add a shared reviewtransaction helper that builds
and/or matches incident artifact filenames, then update preserveIncidentArtifact
and CompactPreservedResultPath to use that API instead of hard-coded format
strings or independent matching logic, preserving both class and non-class
filename shapes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@internal/cli/review_incident.go`:
- Around line 117-135: The incident artifact filename contract is duplicated
between preserveIncidentArtifact and CompactPreservedResultPath. Add a shared
reviewtransaction helper that builds and/or matches incident artifact filenames,
then update preserveIncidentArtifact and CompactPreservedResultPath to use that
API instead of hard-coded format strings or independent matching logic,
preserving both class and non-class filename shapes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: af8c853f-3aa5-44ed-85aa-3a1b7cf88e55

📥 Commits

Reviewing files that changed from the base of the PR and between b0b0914 and 362c272.

📒 Files selected for processing (17)
  • internal/assets/assets_test.go
  • internal/assets/opencode/plugins/review-result-artifacts.ts
  • internal/cli/review_artifact.go
  • internal/cli/review_artifact_test.go
  • internal/cli/review_capabilities.go
  • internal/cli/review_dispose_result.go
  • internal/cli/review_dispose_result_test.go
  • internal/cli/review_facade.go
  • internal/cli/review_incident.go
  • internal/cli/review_incident_test.go
  • internal/reviewtransaction/compact.go
  • internal/reviewtransaction/compact_reclaim.go
  • internal/reviewtransaction/compact_result_acquisition.go
  • internal/reviewtransaction/compact_result_acquisition_test.go
  • internal/reviewtransaction/compact_result_disposition.go
  • internal/reviewtransaction/compact_result_disposition_test.go
  • internal/reviewtransaction/compact_store.go

@MataM15
MataM15 requested a review from dnlrsls July 21, 2026 12:10
# Conflicts:
#	internal/assets/assets_test.go
#	internal/assets/opencode/plugins/review-result-artifacts.ts
#	internal/cli/review_facade.go
#	internal/cli/review_incident.go
Bind reviewer artifacts to provider-owned subjects and admit only completed, in-scope inspections. Add exact validating-result reopen and narrowly gated correction-evidence recovery while preserving strict legacy contracts through versioned schemas.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment