Summary
The reviewer agent posts a review marker with id=reviewer:<loopID> (loop UUID only), but the daemon's verifyAgentNativeReviewMarker expects id=reviewer:<loopID>:<headSHA> (loop UUID + : + head SHA). The marker verification always fails, the reviewer loop never reaches completed, and it stays backing_off with "no matching GitHub review marker was found" — even though the review IS posted with real findings.
Evidence
Review posted on PR (ankaboot-source/m3llm#180)
The reviewer agent posted a review with this marker in the body:
<!-- looper:review id=reviewer:739a4c36-9ad9-4600-a475-7d72feb0f6bd head=d36b61c2b2a65bc2c77997ea5e4cd39c0d590131 outcome=actionable -->
Note: id=reviewer:739a4c36-9ad9-4600-a475-7d72feb0f6bd — loop UUID only, no :headSHA suffix.
Daemon expects
agentNativeReviewID (reviewer/runner.go:6731-6733) constructs the expected id as reviewer:<loopID>:<headSHA>:
func agentNativeReviewID(loopID string, headSHA string) string {
return fmt.Sprintf("reviewer:%s:%s", loopID, headSHA)
}
agentNativeReviewMarker (reviewer/runner.go:6735-6740) builds the expected marker with that idempotencyKey:
func agentNativeReviewMarker(loopID string, headSHA string, idempotencyKey string) string {
if idempotencyKey == "" {
idempotencyKey = fmt.Sprintf("reviewer:%s:%s", loopID, headSHA)
}
return fmt.Sprintf("looper:review id=%s head=%s", idempotencyKey, headSHA)
}
Prompt instructs the agent correctly
The prompt (reviewer/runner.go:6526) tells the agent:
Review idempotency marker prefix: <!-- looper:review id=reviewer:<loopID>:<headSHA> head=<headSHA> outcome=clean|non_blocking|blocking -->
So the prompt DOES include the :headSHA suffix in the id field. But the agent (opencode in our case) posts id=reviewer:<loopID> — dropping the :headSHA suffix.
Verification fails
verifyAgentNativeReviewMarker (reviewer/runner.go:3317-3322) calls agentNativeReviewMarker to build the expected marker, then FindReviewMarker (gateway.go:2322) → findAllowedReviewMarker (gateway.go:2363). The matches function (gateway.go:2514-2528) compares id=reviewer:loopID:headSHA (expected) vs m.ID=reviewer:loopID (actual) → no match.
The fallback agentNativeLoopReviewMarker (line 6742-6744) uses id_prefix=reviewer:loopID: but strings.HasPrefix("reviewer:loopID", "reviewer:loopID:") → false (actual ID lacks the trailing :).
Loop status
status = backing_off
last_error = Reviewer agent completed but no matching GitHub review marker was found
The review IS posted with real findings (1 blocking + 3 non-blocking), the fixer IS auto-triggered by the review comments, but the reviewer loop never converges to completed.
Root cause
The agent does not faithfully reproduce the marker format from the prompt. The prompt shows id=reviewer:<loopID>:<headSHA> but the agent posts id=reviewer:<loopID>. This may be because:
- The agent interprets the
id field as a stable identifier (loop UUID) and treats the :headSHA as redundant since head=<headSHA> is already a separate field.
- The marker format in the prompt is a "prefix" template, and the agent may not realize the
:headSHA is a required part of the id value (not just the prefix example).
This is distinct from #225 (closed by #231): #225 addressed marker verification missing valid posted reviews that DID have the correct id=reviewer:loopID:headSHA format. Here the agent posts a DIFFERENT format (id=reviewer:loopID without the suffix) that the verification cannot match even with the #231 tolerance fixes.
Expected behavior
The daemon should tolerate the id=reviewer:<loopID> format (without :headSHA suffix) since:
- The
head=<headSHA> field is already present and verified separately.
- The loop UUID alone is sufficient for idempotency within a single loop.
- Agents cannot be guaranteed to faithfully reproduce the exact
id format from the prompt template.
Proposed fix
Two non-exclusive options:
Option A — Tolerant matching in findAllowedReviewMarker: When the exact id=reviewer:loopID:headSHA match fails, fall back to matching id=reviewer:loopID (loop UUID only) as long as head=<headSHA> matches. This is a ~5-line change in matches (gateway.go:2514-2528).
Option B — Simplify the prompt template: Change the prompt to show id=reviewer:<loopID> (loop UUID only) and drop the :headSHA from the id field, since head=<headSHA> is already a separate field. Update agentNativeReviewID and agentNativeReviewMarker accordingly. This is a larger change but makes the marker format simpler and less error-prone for agents.
Environment
Reproduction
- Configure looper with opencode as agent vendor
- Label an issue
looper:plan, let planner open a spec PR
- Promote to
looper:worker-ready, let worker open an impl PR
- Run
looper review <repo>#<pr> to trigger reviewer
- Reviewer posts review with
id=reviewer:<loopID> (no :headSHA)
- Loop stays
backing_off — "no matching GitHub review marker was found"
Context
Summary
The reviewer agent posts a review marker with
id=reviewer:<loopID>(loop UUID only), but the daemon'sverifyAgentNativeReviewMarkerexpectsid=reviewer:<loopID>:<headSHA>(loop UUID +:+ head SHA). The marker verification always fails, the reviewer loop never reachescompleted, and it staysbacking_offwith "no matching GitHub review marker was found" — even though the review IS posted with real findings.Evidence
Review posted on PR (ankaboot-source/m3llm#180)
The reviewer agent posted a review with this marker in the body:
<!-- looper:review id=reviewer:739a4c36-9ad9-4600-a475-7d72feb0f6bd head=d36b61c2b2a65bc2c77997ea5e4cd39c0d590131 outcome=actionable -->Note:
id=reviewer:739a4c36-9ad9-4600-a475-7d72feb0f6bd— loop UUID only, no:headSHAsuffix.Daemon expects
agentNativeReviewID(reviewer/runner.go:6731-6733) constructs the expected id asreviewer:<loopID>:<headSHA>:agentNativeReviewMarker(reviewer/runner.go:6735-6740) builds the expected marker with that idempotencyKey:Prompt instructs the agent correctly
The prompt (reviewer/runner.go:6526) tells the agent:
So the prompt DOES include the
:headSHAsuffix in theidfield. But the agent (opencode in our case) postsid=reviewer:<loopID>— dropping the:headSHAsuffix.Verification fails
verifyAgentNativeReviewMarker(reviewer/runner.go:3317-3322) callsagentNativeReviewMarkerto build the expected marker, thenFindReviewMarker(gateway.go:2322) →findAllowedReviewMarker(gateway.go:2363). Thematchesfunction (gateway.go:2514-2528) comparesid=reviewer:loopID:headSHA(expected) vsm.ID=reviewer:loopID(actual) → no match.The fallback
agentNativeLoopReviewMarker(line 6742-6744) usesid_prefix=reviewer:loopID:butstrings.HasPrefix("reviewer:loopID", "reviewer:loopID:")→ false (actual ID lacks the trailing:).Loop status
The review IS posted with real findings (1 blocking + 3 non-blocking), the fixer IS auto-triggered by the review comments, but the reviewer loop never converges to
completed.Root cause
The agent does not faithfully reproduce the marker format from the prompt. The prompt shows
id=reviewer:<loopID>:<headSHA>but the agent postsid=reviewer:<loopID>. This may be because:idfield as a stable identifier (loop UUID) and treats the:headSHAas redundant sincehead=<headSHA>is already a separate field.:headSHAis a required part of theidvalue (not just the prefix example).This is distinct from #225 (closed by #231): #225 addressed marker verification missing valid posted reviews that DID have the correct
id=reviewer:loopID:headSHAformat. Here the agent posts a DIFFERENT format (id=reviewer:loopIDwithout the suffix) that the verification cannot match even with the #231 tolerance fixes.Expected behavior
The daemon should tolerate the
id=reviewer:<loopID>format (without:headSHAsuffix) since:head=<headSHA>field is already present and verified separately.idformat from the prompt template.Proposed fix
Two non-exclusive options:
Option A — Tolerant matching in
findAllowedReviewMarker: When the exactid=reviewer:loopID:headSHAmatch fails, fall back to matchingid=reviewer:loopID(loop UUID only) as long ashead=<headSHA>matches. This is a ~5-line change inmatches(gateway.go:2514-2528).Option B — Simplify the prompt template: Change the prompt to show
id=reviewer:<loopID>(loop UUID only) and drop the:headSHAfrom theidfield, sincehead=<headSHA>is already a separate field. UpdateagentNativeReviewIDandagentNativeReviewMarkeraccordingly. This is a larger change but makes the marker format simpler and less error-prone for agents.Environment
Reproduction
looper:plan, let planner open a spec PRlooper:worker-ready, let worker open an impl PRlooper review <repo>#<pr>to trigger reviewerid=reviewer:<loopID>(no:headSHA)backing_off— "no matching GitHub review marker was found"Context
looper review submit— separate bug, worked around with dev build