Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
826c6ee
fix(actions): bound agent mention dispatch envelope
seonghobae Aug 8, 2026
2fe27c0
test(agent-mention): require complete quality gate scope
seonghobae Aug 8, 2026
7bffba7
fix(agent-mention): cover scheduler quality surfaces
seonghobae Aug 8, 2026
e408117
test(agent-mention): cover wrapper validation branches
seonghobae Aug 8, 2026
ba93f53
fix(actions): address remaining exact-head review findings
seonghobae Aug 8, 2026
04c68a9
test(agent-mention): cover fix scheduler quality surface
seonghobae Aug 9, 2026
97f5277
docs(actions): cite SC-23 mention-dispatch envelope authenticity
seonghobae Aug 13, 2026
db8d0cb
fix(coverage): accept only bounded relative requirement includes
seonghobae Aug 13, 2026
486fbb2
test(ci): reproduce flattened requirement include failure
seonghobae Aug 14, 2026
9737cc2
fix(ci): preserve bounded requirement include closure
seonghobae Aug 14, 2026
1c506ae
test(ci): cover requirement include fail-closed branches
seonghobae Aug 14, 2026
9c7805c
ci: gate materialized requirement include regression
seonghobae Aug 14, 2026
f0bf38f
test(ci): require include regression in materializer gate
seonghobae Aug 14, 2026
85b7fe3
docs(ci): record requirement include materialization contract
seonghobae Aug 14, 2026
979f5d1
docs(ci): record flattened include repair
seonghobae Aug 14, 2026
c4184d1
fix(actions): preserve queued reviews when reactions fail
seonghobae Aug 14, 2026
79bff20
test(actions): cover reaction-denied review dispatch
seonghobae Aug 14, 2026
4c9421b
test(actions): align idempotency with best-effort reactions
seonghobae Aug 14, 2026
79ef99b
test(actions): pin complete dispatch receipt contracts
seonghobae Aug 15, 2026
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
199 changes: 122 additions & 77 deletions .github/workflows/agent-mention-opencode-dispatch.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Agent Mention OpenCode Dispatch
run-name: >-
Agent Mention OpenCode ${{ github.event.client_payload.target_repository }}#${{
github.event.client_payload.pr_number }} [cwl-agent-invocation:${{
Agent Mention OpenCode ${{ github.event.client_payload.claim.repository }}#${{
github.event.client_payload.claim.pr_number }} [cwl-agent-invocation:${{
github.event.client_payload.agent_invocation_key }}]

on:
Expand All @@ -26,40 +26,15 @@ jobs:
contents: write
env:
GH_TOKEN: ${{ github.token }}
REQUESTED_AGENT: "opencode-agent"
PAYLOAD_AGENT: ${{ github.event.client_payload.requested_agent || '' }}
CLIENT_PAYLOAD_JSON: ${{ toJSON(github.event.client_payload) }}
PAYLOAD_SCHEMA: ${{ github.event.client_payload.schema || '' }}
INVOCATION_KEY: ${{ github.event.client_payload.agent_invocation_key || '' }}
TARGET_REPOSITORY: ${{ github.event.client_payload.target_repository || '' }}
PR_NUMBER: ${{ github.event.client_payload.pr_number || '' }}
PR_HEAD_SHA: ${{ github.event.client_payload.pr_head_sha || '' }}
PR_BASE_SHA: ${{ github.event.client_payload.pr_base_sha || '' }}
BASE_BRANCH: ${{ github.event.client_payload.base_branch || '' }}
REQUESTED_BY: ${{ github.event.client_payload.requested_by || '' }}
SOURCE_COMMENT_ID: ${{ github.event.client_payload.source_comment_id || '' }}
TRIGGER_REVIEWS: ${{ github.event.client_payload.trigger_reviews }}
REVIEW_DISPATCH_LIMIT: ${{ github.event.client_payload.review_dispatch_limit || '' }}
ENABLE_AUTO_MERGE: ${{ github.event.client_payload.enable_auto_merge }}
UPDATE_BRANCHES: ${{ github.event.client_payload.update_branches }}
MERGE_MODE: ${{ github.event.client_payload.merge_mode || '' }}
steps:
- name: Validate exact invocation payload
- name: Validate exact invocation payload and prepare scheduler request
run: |
set -euo pipefail
if [ "$PAYLOAD_AGENT" != "$REQUESTED_AGENT" ] ||
! [[ "$INVOCATION_KEY" =~ ^[0-9a-f]{64}$ ]] ||
! [[ "$TARGET_REPOSITORY" =~ ^ContextualWisdomLab/[A-Za-z0-9_.-]+$ ]] ||
! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]] ||
! [[ "$PR_HEAD_SHA" =~ ^[0-9a-f]{40}$ ]] ||
! [[ "$PR_BASE_SHA" =~ ^[0-9a-f]{40}$ ]] ||
! [[ "$BASE_BRANCH" =~ ^[A-Za-z0-9._/-]+$ ]] ||
[[ "$BASE_BRANCH" == -* ]] ||
! [[ "$SOURCE_COMMENT_ID" =~ ^[1-9][0-9]*$ ]] ||
[ "$TRIGGER_REVIEWS" != "true" ] ||
[ "$REVIEW_DISPATCH_LIMIT" != "1" ] ||
[ "$ENABLE_AUTO_MERGE" != "false" ] ||
[ "$UPDATE_BRANCHES" != "false" ] ||
[ "$MERGE_MODE" != "disabled" ] ||
! [[ "$REQUESTED_BY" =~ ^[A-Za-z0-9-]+$ ]]; then
if [ "$PAYLOAD_SCHEMA" != "cwl.agent-invocation/v2" ] ||
! [[ "$INVOCATION_KEY" =~ ^[0-9a-f]{64}$ ]]; then
echo "::error::Rejected malformed or mismatched OpenCode agent invocation payload."
exit 1
fi
Expand All @@ -69,30 +44,127 @@ jobs:
import hmac
import json
import os
from pathlib import Path
import re

envelope = json.loads(os.environ["CLIENT_PAYLOAD_JSON"])
envelope_keys = {"schema", "claim", "agent_invocation_key"}
claim_keys = {
"actor",
"agent",
"base_branch",
"base_sha",
"comment_id",
"enable_auto_merge",
"head_sha",
"merge_mode",
"pr_number",
"repository",
"review_dispatch_limit",
"trigger_reviews",
"update_branches",
}
if not isinstance(envelope, dict) or set(envelope) != envelope_keys:
raise SystemExit("invalid OpenCode invocation envelope")
claim = envelope["claim"]
if not isinstance(claim, dict):
raise SystemExit("OpenCode invocation claim must be an object")
if envelope["schema"] != "cwl.agent-invocation/v2":
raise SystemExit("unsupported OpenCode invocation schema")
if envelope["schema"] != os.environ["PAYLOAD_SCHEMA"]:
raise SystemExit("OpenCode invocation schema context mismatch")
if envelope["agent_invocation_key"] != os.environ["INVOCATION_KEY"]:
raise SystemExit("OpenCode invocation key context mismatch")
if set(claim) != claim_keys:
raise SystemExit("invalid OpenCode invocation claim fields")

text_fields = {
"actor",
"agent",
"base_branch",
"base_sha",
"head_sha",
"merge_mode",
"repository",
"review_dispatch_limit",
}
if any(not isinstance(claim[field], str) for field in text_fields):
raise SystemExit("OpenCode invocation claim has a non-string text field")
if type(claim["comment_id"]) is not int or claim["comment_id"] < 1:
raise SystemExit("OpenCode invocation claim has an invalid comment id")
if type(claim["pr_number"]) is not int or claim["pr_number"] < 1:
raise SystemExit("OpenCode invocation claim has an invalid pull request number")
for field in ("enable_auto_merge", "trigger_reviews", "update_branches"):
if type(claim[field]) is not bool:
raise SystemExit(f"OpenCode invocation claim has an invalid {field} flag")
if not re.fullmatch(r"ContextualWisdomLab/[A-Za-z0-9_.-]+", claim["repository"]):
raise SystemExit("OpenCode invocation claim has an invalid repository")
if not re.fullmatch(r"[0-9a-f]{40}", claim["head_sha"]):
raise SystemExit("OpenCode invocation claim has an invalid head SHA")
if not re.fullmatch(r"[0-9a-f]{40}", claim["base_sha"]):
raise SystemExit("OpenCode invocation claim has an invalid base SHA")
if (
not re.fullmatch(r"[A-Za-z0-9._/-]+", claim["base_branch"])
or claim["base_branch"].startswith("-")
):
raise SystemExit("OpenCode invocation claim has an invalid base branch")
if not re.fullmatch(r"[A-Za-z0-9-]+", claim["actor"]):
raise SystemExit("OpenCode invocation claim has an invalid actor")
expected_policy = {
"agent": "opencode-agent",
"enable_auto_merge": False,
"merge_mode": "disabled",
"review_dispatch_limit": "1",
"trigger_reviews": True,
"update_branches": False,
}
if any(claim[field] != value for field, value in expected_policy.items()):
raise SystemExit("OpenCode invocation claim violates review-only policy")

canonical = json.dumps(
{
"actor": os.environ["REQUESTED_BY"],
"agent": os.environ["REQUESTED_AGENT"],
"base_branch": os.environ["BASE_BRANCH"],
"base_sha": os.environ["PR_BASE_SHA"],
"comment_id": int(os.environ["SOURCE_COMMENT_ID"]),
"enable_auto_merge": os.environ["ENABLE_AUTO_MERGE"] == "true",
"head_sha": os.environ["PR_HEAD_SHA"],
"merge_mode": os.environ["MERGE_MODE"],
"pr_number": int(os.environ["PR_NUMBER"]),
"repository": os.environ["TARGET_REPOSITORY"],
"review_dispatch_limit": os.environ["REVIEW_DISPATCH_LIMIT"],
"trigger_reviews": os.environ["TRIGGER_REVIEWS"] == "true",
"update_branches": os.environ["UPDATE_BRANCHES"] == "true",
},
claim,
ensure_ascii=True,
separators=(",", ":"),
sort_keys=True,
).encode("utf-8")
expected = hashlib.sha256(canonical).hexdigest()
if not hmac.compare_digest(expected, os.environ["INVOCATION_KEY"]):
raise SystemExit("invocation key does not match canonical payload")

scheduler_request = {
"event_type": "merge-scheduler-agent-review-v2",
"client_payload": envelope,
}
encoded_request = json.dumps(
scheduler_request,
ensure_ascii=True,
separators=(",", ":"),
sort_keys=True,
).encode("utf-8")
if len(envelope) > 10 or len(encoded_request) > 65_535:
raise SystemExit("scheduler repository dispatch exceeds GitHub limits")

request_path = Path(os.environ["RUNNER_TEMP"]) / "agent-review-scheduler-request.json"
request_path.write_bytes(encoded_request)
exports = {
"BASE_BRANCH": claim["base_branch"],
"ENABLE_AUTO_MERGE": str(claim["enable_auto_merge"]).lower(),
"MERGE_MODE": claim["merge_mode"],
"PR_BASE_SHA": claim["base_sha"],
"PR_HEAD_SHA": claim["head_sha"],
"PR_NUMBER": str(claim["pr_number"]),
"REQUESTED_AGENT": claim["agent"],
"REQUESTED_BY": claim["actor"],
"REVIEW_DISPATCH_LIMIT": claim["review_dispatch_limit"],
"SCHEDULER_REQUEST_FILE": str(request_path),
"SOURCE_COMMENT_ID": str(claim["comment_id"]),
"TARGET_REPOSITORY": claim["repository"],
"TRIGGER_REVIEWS": str(claim["trigger_reviews"]).lower(),
"UPDATE_BRANCHES": str(claim["update_branches"]).lower(),
}
with Path(os.environ["GITHUB_ENV"]).open("a", encoding="utf-8") as handle:
for name, value in exports.items():
handle.write(f"{name}={value}\n")
PYTHON

- name: Inspect exact-name Actions artifact ledger
Expand Down Expand Up @@ -189,33 +261,6 @@ jobs:
if: steps.ledger.outputs.claim == 'true'
run: |
set -euo pipefail
jq -n \
--arg target_repository "$TARGET_REPOSITORY" \
--argjson pr_number "$PR_NUMBER" \
--arg pr_head_sha "$PR_HEAD_SHA" \
--arg pr_base_sha "$PR_BASE_SHA" \
--arg base_branch "$BASE_BRANCH" \
--arg requested_agent "$REQUESTED_AGENT" \
--arg agent_invocation_key "$INVOCATION_KEY" \
--arg requested_by "$REQUESTED_BY" \
--argjson source_comment_id "$SOURCE_COMMENT_ID" \
'{
event_type: "merge-scheduler",
client_payload: {
target_repository: $target_repository,
pr_number: $pr_number,
pr_head_sha: $pr_head_sha,
pr_base_sha: $pr_base_sha,
base_branch: $base_branch,
trigger_reviews: true,
review_dispatch_limit: "1",
enable_auto_merge: false,
update_branches: false,
merge_mode: "disabled",
requested_agent: $requested_agent,
agent_invocation_key: $agent_invocation_key,
requested_by: $requested_by,
source_comment_id: $source_comment_id
}
}' \
| gh api "repos/${GITHUB_REPOSITORY}/dispatches" -X POST --input -
gh api "repos/${GITHUB_REPOSITORY}/dispatches" \
-X POST \
--input "$SCHEDULER_REQUEST_FILE"
26 changes: 25 additions & 1 deletion .github/workflows/agent-mention-router-quality-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,21 @@ on:
- ".github/workflows/agent-mention-router-quality-ci.yml"
- ".github/workflows/agent-mention-noema-dispatch.yml"
- ".github/workflows/agent-mention-opencode-dispatch.yml"
- ".github/workflows/opencode-review-dispatch.yml"
- ".github/workflows/pr-review-merge-scheduler.yml"
- ".github/workflows/strix.yml"
- "docs/automation/review-agent-comment-invocation.md"
- "scripts/ci/agent_mention_router.py"
- "scripts/ci/agent_mention_sweep.py"
- "scripts/ci/pr_review_fix_scheduler.py"
- "scripts/ci/pr_review_merge_scheduler.py"
- "scripts/ci/test_strix_quick_gate.sh"
- "tests/test_agent_mention_*.py"
- "tests/test_opencode_agent_contract.py"
- "tests/test_opencode_workflow_shell_syntax.py"
- "tests/test_pr_review_merge_scheduler.py"
- "tests/test_pr_review_fix_scheduler_coverage.py"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- "tests/test_required_workflow_queue_contract.py"
- "requirements-opencode-review-ci-hashes.txt"
push:
branches: [main]
Expand All @@ -21,11 +31,21 @@ on:
- ".github/workflows/agent-mention-router-quality-ci.yml"
- ".github/workflows/agent-mention-noema-dispatch.yml"
- ".github/workflows/agent-mention-opencode-dispatch.yml"
- ".github/workflows/opencode-review-dispatch.yml"
- ".github/workflows/pr-review-merge-scheduler.yml"
- ".github/workflows/strix.yml"
- "docs/automation/review-agent-comment-invocation.md"
- "scripts/ci/agent_mention_router.py"
- "scripts/ci/agent_mention_sweep.py"
- "scripts/ci/pr_review_fix_scheduler.py"
- "scripts/ci/pr_review_merge_scheduler.py"
- "scripts/ci/test_strix_quick_gate.sh"
- "tests/test_agent_mention_*.py"
- "tests/test_opencode_agent_contract.py"
- "tests/test_opencode_workflow_shell_syntax.py"
- "tests/test_pr_review_merge_scheduler.py"
- "tests/test_pr_review_fix_scheduler_coverage.py"
- "tests/test_required_workflow_queue_contract.py"
- "requirements-opencode-review-ci-hashes.txt"

concurrency:
Expand Down Expand Up @@ -99,6 +119,8 @@ jobs:
include =
scripts/ci/agent_mention_router.py
scripts/ci/agent_mention_sweep.py
scripts/ci/pr_review_fix_scheduler.py
scripts/ci/pr_review_merge_scheduler.py
[report]
fail_under = 100
show_missing = True
Expand All @@ -109,6 +131,8 @@ jobs:
python -m coverage report --fail-under=100
python -m interrogate --fail-under=100 \
scripts/ci/agent_mention_router.py \
scripts/ci/agent_mention_sweep.py
scripts/ci/agent_mention_sweep.py \
scripts/ci/pr_review_fix_scheduler.py \
scripts/ci/pr_review_merge_scheduler.py
python -m compileall -q scripts/ci tests
git diff --check "$CHANGE_DIFF_RANGE"
18 changes: 13 additions & 5 deletions .github/workflows/opencode-review-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,21 @@ on:
types: [opencode-review]

concurrency:
# PR-number scope keeps stale dispatches replaced for the current head.
# Serialize valid dispatches per sender and target PR without dropping an
# already-pending newer head when a delayed stale event arrives. queue:max
# preserves pending runs; payloads missing queue-key fields get isolated by run
# id, and cancel:false lets live metadata validation reject stale snapshots.
group: >-
opencode-review-repository-dispatch-${{
github.event.client_payload.target_repository || github.repository }}-${{
github.event.client_payload.pr_number && format('pr-{0}', github.event.client_payload.pr_number) ||
github.run_id }}
cancel-in-progress: true
github.event.sender.id &&
github.event.client_payload.target_repository &&
github.event.client_payload.pr_number &&
format('sender-{0}-{1}-pr-{2}', github.event.sender.id,
github.event.client_payload.target_repository,
github.event.client_payload.pr_number) ||
format('invalid-{0}', github.run_id) }}
cancel-in-progress: false
queue: max

permissions:
contents: read
Expand Down
Loading
Loading