Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 142 additions & 8 deletions .github/workflows/opencode-review-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5623,8 +5623,86 @@ jobs:
exit 0
}

retry_inline_comments_one_at_a_time() {
local batch_payload_file="$1" review_body="$2" refused_locations_file="$3"
local attached_locations_file="${4:-}"
local deferred_locations_file="${5:-}"
local split_dir comment_file wrapped_file error_file response_file
local attached=0
local found=0
local -a split_args

: >"$refused_locations_file"
if [ -n "$attached_locations_file" ]; then
: >"$attached_locations_file"
fi
split_dir="$(mktemp -d)"
split_args=(
python3 "$GITHUB_WORKSPACE/scripts/ci/opencode_inline_comment_fallback.py"
--split-payload "$batch_payload_file"
--output-dir "$split_dir"
--retry-limit "${OPENCODE_INLINE_COMMENT_RETRY_LIMIT:-20}"
)
if [ -n "$deferred_locations_file" ]; then
: >"$deferred_locations_file"
split_args+=(--deferred-locations "$deferred_locations_file")
fi
if ! "${split_args[@]}"; then
rm -rf "$split_dir"
return 1
fi
for comment_file in "$split_dir"/comment-*.json; do
[ -f "$comment_file" ] || continue
found=1
wrapped_file="$(mktemp)"
error_file="$(mktemp)"
response_file="$(mktemp)"
if [ "$attached" -eq 0 ]; then
jq --arg body "$review_body" --arg event "REQUEST_CHANGES" \
'.event = $event | .body = $body' "$comment_file" >"$wrapped_file"
else
cp "$comment_file" "$wrapped_file"
fi
if post_pull_review_with_retry \
"inline review one-at-a-time" \
"$review_write_token" \
"$wrapped_file" \
"$error_file" \
"$response_file"; then
attached=1
if [ -n "$attached_locations_file" ]; then
python3 "$GITHUB_WORKSPACE/scripts/ci/opencode_inline_comment_fallback.py" \
--record-attach \
--attached-locations "$attached_locations_file" \
--comment-file "$comment_file" || true
fi
else
python3 "$GITHUB_WORKSPACE/scripts/ci/opencode_inline_comment_fallback.py" \
--record-refusal \
--refused-locations "$refused_locations_file" \
--comment-file "$comment_file" \
--error-file "$error_file" || true
if [ -s "$error_file" ]; then
cat "$error_file" >>"${refused_locations_file}.errors"
fi
fi
rm -f "$wrapped_file" "$error_file" "$response_file"
if [ "${REVIEW_PUBLICATION_STALE_HEAD:-}" = "1" ]; then
rm -rf "$split_dir"
return 1
fi
done
rm -rf "$split_dir"
if [ "$found" -eq 0 ] || [ "$attached" -eq 0 ]; then
return 1
fi
return 0
}

create_pull_review_with_payload() {
local event="$1" body="$2" review_payload_file="$3" fallback_body_file="$4"
local source_body_file="${5:-}"
local control_json="${6:-}"
local gh_error_file
local rewritten_payload_file
local review_response_file
Expand All @@ -5640,6 +5718,41 @@ jobs:
emit_review_body_to_action_log "$event" "$body" "$review_payload_file"
if ! post_pull_review_with_retry "inline review" "$review_write_token" "$review_payload_file" "$gh_error_file" "$review_response_file"; then
warn_gh_publication_failure "pull review inline comments" "$gh_error_file"
if [ "${REVIEW_PUBLICATION_STALE_HEAD:-}" != "1" ] \
&& python3 "$GITHUB_WORKSPACE/scripts/ci/opencode_inline_comment_fallback.py" \
--is-unprocessable --error-file "$gh_error_file"; then
refused_locations_file="$(mktemp)"
attached_locations_file="$(mktemp)"
deferred_locations_file="$(mktemp)"
if retry_inline_comments_one_at_a_time \
"$review_payload_file" "$body" "$refused_locations_file" \
"$attached_locations_file" "$deferred_locations_file"; then
if { [ -s "$refused_locations_file" ] || [ -s "$deferred_locations_file" ]; } \
&& [ -n "$source_body_file" ] && [ -n "$control_json" ]; then
mixed_error_file="$gh_error_file"
if [ -s "${refused_locations_file}.errors" ]; then
mixed_error_file="${refused_locations_file}.errors"
fi
build_inline_comment_failure_body \
"$source_body_file" "$fallback_body_file" "$control_json" \
"$mixed_error_file" "$refused_locations_file" \
"$attached_locations_file" "$deferred_locations_file" || true
update_review_overview "$event" "$(cat "$fallback_body_file")"
else
update_review_overview "$event" "$body"
fi
rm -f "$gh_error_file" "$review_response_file" \
"$refused_locations_file" "${refused_locations_file}.errors" \
"$attached_locations_file" "$deferred_locations_file"
return 0
fi
rm -f "$refused_locations_file" "${refused_locations_file}.errors" \
"$attached_locations_file" "$deferred_locations_file"
fi
if [ -n "$source_body_file" ] && [ -n "$control_json" ]; then
build_inline_comment_failure_body \
"$source_body_file" "$fallback_body_file" "$control_json" "$gh_error_file" || true
fi
rm -f "$gh_error_file" "$review_response_file"
if [ "${REVIEW_PUBLICATION_STALE_HEAD:-}" = "1" ]; then
printf '::error::OpenCode inline review publication stopped because PR head advanced beyond %s.\n' "$HEAD_SHA"
Expand Down Expand Up @@ -5766,12 +5879,33 @@ jobs:
build_inline_comment_failure_body() {
local body_file="$1"
local output_file="$2"

{
cat "$body_file"
printf '\n## Inline comment publishing failed\n\n'
printf 'GitHub did not accept the inline review comments for the cited finding lines, so OpenCode did not copy suggested diffs into this PR-level body. Re-run the review after the findings are anchored to changed diff lines, or inspect the workflow log/control JSON and apply the changes manually.\n'
} >"$output_file"
local control_json="$3"
local error_file="${4:-}"
local refused_locations_file="${5:-}"
local attached_locations_file="${6:-}"
local deferred_locations_file="${7:-}"
local -a fallback_args

fallback_args=(
python3 "$GITHUB_WORKSPACE/scripts/ci/opencode_inline_comment_fallback.py"
--control "$control_json"
--body "$body_file"
--output "$output_file"
--retry-limit "${OPENCODE_INLINE_COMMENT_RETRY_LIMIT:-20}"
)
if [ -n "$error_file" ]; then
fallback_args+=(--error-file "$error_file")
fi
if [ -n "$refused_locations_file" ]; then
fallback_args+=(--refused-locations "$refused_locations_file")
fi
if [ -n "$attached_locations_file" ]; then
fallback_args+=(--attached-locations "$attached_locations_file")
fi
if [ -n "$deferred_locations_file" ]; then
fallback_args+=(--deferred-locations "$deferred_locations_file")
fi
"${fallback_args[@]}"
}

publish_request_changes_from_control() {
Expand All @@ -5785,8 +5919,8 @@ jobs:
fallback_body_file="$(mktemp)"
format_request_changes_body "$control_json" "$body_file"
build_request_changes_review_payload "$control_json" "$body_file" "$payload_file"
build_inline_comment_failure_body "$body_file" "$fallback_body_file"
create_pull_review_with_payload "REQUEST_CHANGES" "$(cat "$body_file")" "$payload_file" "$fallback_body_file"
build_inline_comment_failure_body "$body_file" "$fallback_body_file" "$control_json"
create_pull_review_with_payload "REQUEST_CHANGES" "$(cat "$body_file")" "$payload_file" "$fallback_body_file" "$body_file" "$control_json"
rm -f "$body_file" "$payload_file" "$fallback_body_file"
}

Expand Down
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,9 @@

<!-- CWL-ENTRY -->
> **Agents: read the master context FIRST.** Before any work, read [`docs/CWL-MASTER-CONTEXT.md`](docs/CWL-MASTER-CONTEXT.md) (mission · naruon-as-platform + inter-component UML · cross-cutting disciplines · conventions · roadmap · current state), the live **GitHub Project #1** <https://github.com/orgs/ContextualWisdomLab/projects/1> (work/roadmap source of truth), the full spec **ContextualWisdomLab/naruon#974**, and operate the Project per [`docs/agent-github-project-protocol.md`](docs/agent-github-project-protocol.md). The repo/Project — not any private agent memory — is the source of truth.
Materialize accepts only exact SHA-256 pins or a bounded relative `-r` include (no `.`/`..`); a lone `--require-hashes` directive is not trust evidence. See [`docs/doctoring/review-inline-comment-422-fallback.md`](docs/doctoring/review-inline-comment-422-fallback.md).

One-at-a-time 422 retries must keep multi-line start_line/start_side.
Attach and refusal receipts cite start_line when a multi-line suggestion rewrote line to the span end.
Leftover overview receipts sanitize path and phrase so a leftover cannot close the HTML comment or reopen a suggestion fence.
Leftover overview paths that contain `-->`, `<!--`, or a suggestion fence are omitted.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ Semantic Versioning where the repository publishes a release.

### Fixed

- Omitted leftover 422-fallback paths that contain `-->`, `<!--`, or a suggestion fence so a leftover cannot close `<!-- opencode-review-overview -->` or reopen an applyable GitHub suggestion block (CWE-116).
- Sanitized leftover overview receipt path and phrase so a leftover cannot close `<!-- opencode-review-overview -->` or reopen a GitHub suggestion fence (CWE-116).
- Materialized base Python locks only when every package line is an exact SHA-256 pin or a bounded relative `-r`/`--requirement` include. A lone `--require-hashes` directive, a dotted include such as `./lock.txt`, or `-r other-hashes.txt` no longer enters the trusted build context.
- Recorded attach and refusal `path:line` from `start_line` when a multi-line GitHub suggestion rewrote `line` to the span end, so overview receipts still match the trusted finding line.
- Kept `start_line`/`start_side` on one-at-a-time 422 retries so a multi-line GitHub suggestion still posts as one range instead of a single comment on the last line.
- Capped one-at-a-time OpenCode inline retries at 20 comments and listed attached `path:line` beside refused receipts so the overview shows both outcomes, plus any locations left untried by the cap. A `422` substring inside a SHA or issue number no longer starts that retry (CWE-1288), and receipt phrases escape backticks and HTML metacharacters.
- Kept each refused OpenCode inline comment's own GitHub 422 phrase next to its `path:line` so mixed retries do not collapse every failure into one shared error sentence.
- After a mixed one-at-a-time inline retry, listed only the refused `path:line` rows in the overview receipts so attached hunks are not reported as failed.
- After a batch GitHub 422, retried OpenCode inline comments one at a time so comments on surviving hunks still attach instead of dropping the entire review thread.
- Stored each refused OpenCode inline comment as a durable overview receipt that pairs the trusted `path:line` with the GitHub 422 error phrase from `gh api` stderr or JSON `errors[].message`.
- Named each trusted `path:line` in the OpenCode GitHub 422 inline-comment fallback so a refused attach still tells the author the exact current-head location instead of a generic “cited finding lines” sentence.
- Bounded the Strix quality self-test's deterministic timeout fixtures to 3-second process and 5-second fake-sleep budgets so exact-head policy evidence completes inside the existing job limit without changing production Strix scanner timeouts, providers, credentials, or review semantics.
- Allowed commas and ASCII parentheses in the bounded Strix changed-file path policy so legal tracked Packrat fixtures can receive exact-head security analysis, while rejecting raw `..` components before normalization and keeping controls, backslashes, whitespace ambiguity, and shell punctuation fail-closed.
- Bound each review-agent invocation key to the wrapper's complete canonical payload, including the base branch and requesting actor; altered fields with a valid-format key now fail before durable-leader election or forwarding, and wrapper write permission is job-scoped.
Expand Down
90 changes: 90 additions & 0 deletions docs/doctoring/review-inline-comment-422-fallback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# GitHub 422 inline-comment fallback cites trusted path:line

검토 기준일: **2026-08-13**

## Incident

When GitHub rejects an OpenCode `REQUEST_CHANGES` review because one or more
inline comments cannot attach, the publisher already falls back to a PR-level
body and does not copy suggested diffs into that body. The fallback sentence
said only “the cited finding lines.” Authors then had to open the workflow log
or control JSON to learn *which* `path:line` GitHub refused (GitHub, n.d.-a,
n.d.-b). That is weaker than the line-anchored review artifact modern code
review expects (Bacchelli & Bird, 2013).

## Decision

Leftover overview paths that contain `-->`, `<!--`, or a suggestion fence are omitted so a leftover cannot close the HTML comment.
Materialize accepts only exact SHA-256 pins or a bounded relative `-r` include; a lone `--require-hashes` line is not lock evidence.

Leftover overview receipts sanitize path and phrase; a leftover cannot close the HTML comment or reopen a suggestion fence.

`scripts/ci/opencode_inline_comment_fallback.py` reads the trusted control
JSON, keeps first-seen safe relative `path` plus positive integer `line`
pairs, and appends them to the fallback body as `` `path:line` `` list
items. Unsafe paths (`..`, absolute, drive, backslash) and non-positive
lines are omitted. An empty location set is stated explicitly.

After a refused attach, the publisher first checks that the failure is
HTTP 422 (not a bare `422` substring in a SHA or issue number; CWE-1288),
splits the batch `comments` array into at most 20
single-comment review payloads (`OPENCODE_INLINE_COMMENT_RETRY_LIMIT`,
default 20), and retries each with the same write helper. Comments past
that cap are recorded as not retried instead of opening unbounded `gh
api` writes. The first success uses `REQUEST_CHANGES` plus the review
body; later successes use `COMMENT`. Survivors therefore still appear on
Files changed. Remaining failures still rebuild the fallback from the
`gh api` error file and write durable receipts into the OpenCode
overview comment (`<!-- opencode-review-overview -->`). On mixed success
the overview lists attached `path:line` rows beside refused `path:line`
rows (each refused row keeps that comment's own 422 phrase) and any
locations left untried by the retry cap. JSON `errors[].message` such as
`pull_request_review_thread.path is invalid`, or the first `HTTP 422`
line, is the phrase source. A later comment's different GitHub error
does not overwrite an earlier one. URLs are stripped and each phrase is
bounded to 240 characters. Backticks and HTML metacharacters are
escaped before the phrase is written into the overview body.
When a multi-line GitHub suggestion rewrites ``line`` to the span end,
attach and refusal receipts cite ``start_line`` so the overview still
names the trusted finding (GitHub, n.d.-b).

The publisher calls this helper from `build_inline_comment_failure_body`
with the same control object used to build the inline `comments` array.
Suggested diffs stay out of the PR-level body.

## Verification contract

- `tests/test_opencode_inline_comment_fallback.py` pins safe-pair extraction,
the exact location list, GitHub JSON `errors[].message` phrases, HTTP 422
line fallback, empty-set sentence, CLI success with `--error-file`,
fail-closed unreadable control or error input, batch-to-single comment
splitting, `--is-unprocessable` classification, mixed-success
receipts that list attached path:line beside refused path:line,
per-comment 422 phrases, the 20-comment one-at-a-time retry cap, and
leftover path:line rows that were not retried.
- `tests/test_opencode_agent_contract.py` and
`scripts/ci/test_strix_quick_gate.sh` pin the workflow call with
`$control_json`.

## Rollback

If GitHub later accepts off-diff comments, keep citing the attempted
`path:line` in the fallback. Do not restore a location-free sentence.

## References (APA 7th)

MITRE. (2026). *CWE-1288: Improper validation of syntactic correctness of
input*. https://cwe.mitre.org/data/definitions/1288.html

Bacchelli, A., & Bird, C. (2013). Expectations, outcomes, and challenges of
modern code review. In *Proceedings of the 35th International Conference on
Software Engineering* (pp. 712–721). IEEE.
https://doi.org/10.1109/ICSE.2013.6606617

GitHub. (n.d.-a). *Create a review for a pull request*. GitHub Docs. Retrieved
August 13, 2026, from
https://docs.github.com/en/rest/pulls/reviews#create-a-review-for-a-pull-request

GitHub. (n.d.-b). *Create a review comment for a pull request*. GitHub Docs.
Retrieved August 13, 2026, from
https://docs.github.com/en/rest/pulls/comments#create-a-review-comment-for-a-pull-request
Loading
Loading