From 730508a9ebb129779788770d9f97521d67bf2d83 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 17 Aug 2026 20:43:55 +0000 Subject: [PATCH 1/4] fix(automation): keep mention dispatch payloads within GitHub's 10-key limit @opencode-agent mentions could not enqueue because both the first-hop wrapper payload and the merge-scheduler forwarder sent 14 client_payload keys. GitHub's repository_dispatch API allows 10 and returns HTTP 422. Keep identity on both hops, bind review-only flags in the invocation claim, hardcode those flags in the wrapper, and fail closed if a payload grows past 10 keys. Co-authored-by: Seongho Bae --- .../agent-mention-opencode-dispatch.yml | 16 +- .../review-agent-comment-invocation.md | 2 +- scripts/ci/agent_mention_router.py | 53 +++++-- ..._agent_mention_complete_payload_binding.py | 11 ++ ...st_agent_mention_dispatch_payload_limit.py | 141 ++++++++++++++++++ tests/test_agent_mention_router.py | 10 +- 6 files changed, 203 insertions(+), 30 deletions(-) create mode 100644 tests/test_agent_mention_dispatch_payload_limit.py diff --git a/.github/workflows/agent-mention-opencode-dispatch.yml b/.github/workflows/agent-mention-opencode-dispatch.yml index 160b4723d..02a3f6f08 100644 --- a/.github/workflows/agent-mention-opencode-dispatch.yml +++ b/.github/workflows/agent-mention-opencode-dispatch.yml @@ -36,11 +36,11 @@ jobs: 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 || '' }} + TRIGGER_REVIEWS: "true" + REVIEW_DISPATCH_LIMIT: "1" + ENABLE_AUTO_MERGE: "false" + UPDATE_BRANCHES: "false" + MERGE_MODE: "disabled" steps: - name: Validate exact invocation payload run: | @@ -195,9 +195,7 @@ jobs: --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", @@ -207,14 +205,10 @@ jobs: 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 } }' \ diff --git a/docs/automation/review-agent-comment-invocation.md b/docs/automation/review-agent-comment-invocation.md index 51c84dcde..3d2ca496d 100644 --- a/docs/automation/review-agent-comment-invocation.md +++ b/docs/automation/review-agent-comment-invocation.md @@ -45,7 +45,7 @@ This preserves the central MSA boundary without copying privileged workflow code - `contents: write` is intentionally retained only on jobs that call GitHub's create-repository-dispatch endpoint. GitHub documents that endpoint as requiring Contents repository permission at write level. Removing it would disable the bounded central dispatch path; broad workflow-default write access is not granted. - The organization sweep uses the established cross-repository credential chain for reading target comments, while the central repository's own short-lived job token dispatches the central workflows. - OpenCode dispatch is restricted to the exact `OPENCODE_REPOSITORY_DISPATCH_TARGETS` allowlist. -- An invocation cannot merge: `enable_auto_merge=false`, `update_branches=false`, and `merge_mode=disabled` are explicit in the dispatch payload. +- An invocation cannot merge: `enable_auto_merge=false`, `update_branches=false`, and `merge_mode=disabled` are bound into the OpenCode invocation claim and hardcoded in the wrapper. GitHub's create-repository-dispatch endpoint allows at most 10 top-level `client_payload` properties (HTTP 422 otherwise), so those review-only constants are not copied onto the first-hop mention payload. The wrapper's merge-scheduler forward keeps the three flags that override scheduler defaults, together with repository, PR, head/base SHA, base branch, invocation key, and source comment identity. - Every dispatch is bound to live PR number, current head SHA, base branch, source comment, requested agent, and requesting actor metadata fetched or validated immediately before dispatch. - Router jobs use the fixed `ubuntu-24.04` runner and an immutable `actions/checkout` v7.0.1 commit pin; checkout credentials are not persisted. - A branch-selectable `workflow_dispatch` trigger is intentionally absent. This prevents a repository writer from choosing an unreviewed branch version of the central router while the job holds dispatch permissions. diff --git a/scripts/ci/agent_mention_router.py b/scripts/ci/agent_mention_router.py index bdb8ac3db..2b5453139 100644 --- a/scripts/ci/agent_mention_router.py +++ b/scripts/ci/agent_mention_router.py @@ -33,6 +33,7 @@ BASE_BRANCH_RE = re.compile(r"^(?!-)[A-Za-z0-9._/-]+$") ACTOR_RE = re.compile(r"^[A-Za-z0-9-]+$") RECEIPT_RE = re.compile(r"") +REPOSITORY_DISPATCH_CLIENT_PAYLOAD_MAX_KEYS = 10 @dataclass(frozen=True) @@ -369,13 +370,36 @@ def dispatched_agents( return frozenset(observed) +def repository_dispatch_body( + event_type: str, + client_payload: dict[str, Any], +) -> dict[str, Any]: + """Return a repository_dispatch body within GitHub's 10-key payload limit. + + GitHub's create-repository-dispatch endpoint accepts at most 10 top-level + ``client_payload`` properties. A larger object is rejected with HTTP 422, + so mention routing cannot enqueue a review. + """ + + if len(client_payload) > REPOSITORY_DISPATCH_CLIENT_PAYLOAD_MAX_KEYS: + raise ValueError( + "repository_dispatch client_payload has " + f"{len(client_payload)} keys; GitHub allows at most " + f"{REPOSITORY_DISPATCH_CLIENT_PAYLOAD_MAX_KEYS}" + ) + return { + "event_type": event_type, + "client_payload": client_payload, + } + + def noema_payload(request: MentionRequest) -> dict[str, Any]: """Return the durable Noema wrapper dispatch request body.""" agent = "cwl-noema-review" - return { - "event_type": "agent-mention-noema", - "client_payload": { + return repository_dispatch_body( + "agent-mention-noema", + { "target_repository": request.repository, "pr_number": request.pull_request_number, "pr_head_sha": request.pull_request_head_sha, @@ -386,33 +410,32 @@ def noema_payload(request: MentionRequest) -> dict[str, Any]: "requested_by": request.actor, "source_comment_id": request.comment_id, }, - } + ) def opencode_payload(request: MentionRequest) -> dict[str, Any]: - """Return the durable review-only OpenCode wrapper dispatch body.""" + """Return the durable review-only OpenCode wrapper dispatch body. + + Review-only behavior flags stay in the invocation claim and are hardcoded + by the wrapper. Copying them onto this first hop exceeds GitHub's 10-key + ``client_payload`` limit and prevents mention pings from enqueueing. + """ agent = "opencode-agent" - claim = agent_invocation_claim(request, agent) - return { - "event_type": "agent-mention-opencode", - "client_payload": { + return repository_dispatch_body( + "agent-mention-opencode", + { "target_repository": request.repository, "pr_number": request.pull_request_number, "pr_head_sha": request.pull_request_head_sha, "pr_base_sha": request.pull_request_base_sha, "base_branch": request.pull_request_base_branch, - "trigger_reviews": claim["trigger_reviews"], - "review_dispatch_limit": claim["review_dispatch_limit"], - "enable_auto_merge": claim["enable_auto_merge"], - "update_branches": claim["update_branches"], - "merge_mode": claim["merge_mode"], "requested_agent": agent, "agent_invocation_key": agent_invocation_key(request, agent), "requested_by": request.actor, "source_comment_id": request.comment_id, }, - } + ) def dispatch_request( diff --git a/tests/test_agent_mention_complete_payload_binding.py b/tests/test_agent_mention_complete_payload_binding.py index 04562e93f..c07025407 100644 --- a/tests/test_agent_mention_complete_payload_binding.py +++ b/tests/test_agent_mention_complete_payload_binding.py @@ -162,6 +162,17 @@ def test_wrappers_recompute_complete_claim_before_ledger_access() -> None: assert "--arg pr_base_sha \"$PR_BASE_SHA\"" in workflow assert "pr_base_sha: $pr_base_sha" in workflow + assert "github.event.client_payload.trigger_reviews" not in opencode + assert "github.event.client_payload.review_dispatch_limit" not in opencode + assert "github.event.client_payload.enable_auto_merge" not in opencode + assert "github.event.client_payload.update_branches" not in opencode + assert "github.event.client_payload.merge_mode" not in opencode + assert 'TRIGGER_REVIEWS: "true"' in opencode + assert 'REVIEW_DISPATCH_LIMIT: "1"' in opencode + assert 'ENABLE_AUTO_MERGE: "false"' in opencode + assert 'UPDATE_BRANCHES: "false"' in opencode + assert 'MERGE_MODE: "disabled"' in opencode + for field in ( '"trigger_reviews": os.environ["TRIGGER_REVIEWS"] == "true"', '"review_dispatch_limit": os.environ["REVIEW_DISPATCH_LIMIT"]', diff --git a/tests/test_agent_mention_dispatch_payload_limit.py b/tests/test_agent_mention_dispatch_payload_limit.py new file mode 100644 index 000000000..87ad68d8d --- /dev/null +++ b/tests/test_agent_mention_dispatch_payload_limit.py @@ -0,0 +1,141 @@ +"""Contract: mention repository_dispatch payloads stay within GitHub's 10-key limit.""" + +from __future__ import annotations + +import importlib.util +import re +import sys +from pathlib import Path +from types import ModuleType + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +ROUTER_PATH = ROOT / "scripts" / "ci" / "agent_mention_router.py" +NOEMA_WORKFLOW = ROOT / ".github" / "workflows" / "agent-mention-noema-dispatch.yml" +OPENCODE_WORKFLOW = ROOT / ".github" / "workflows" / "agent-mention-opencode-dispatch.yml" +GITHUB_DOCS = ( + "https://docs.github.com/en/rest/repos/repos#create-a-repository-dispatch-event" +) +WRAPPER_CLIENT_PAYLOAD_RE = re.compile( + r"client_payload:\s*\{(?P.*?)^\s+\}", + re.MULTILINE | re.DOTALL, +) +WRAPPER_PAYLOAD_KEY_RE = re.compile(r"^\s+([A-Za-z_][A-Za-z0-9_]*):", re.MULTILINE) +REQUIRED_IDENTITY_KEYS = frozenset( + { + "target_repository", + "pr_number", + "pr_head_sha", + "source_comment_id", + } +) +OPENCODE_FORWARD_SAFETY_KEYS = frozenset( + { + "enable_auto_merge", + "update_branches", + "merge_mode", + } +) + + +def _load_router() -> ModuleType: + """Load the router module from the pull-request source tree.""" + + module_name = "agent_mention_dispatch_payload_limit" + spec = importlib.util.spec_from_file_location(module_name, ROUTER_PATH) + assert spec and spec.loader + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + spec.loader.exec_module(module) + return module + + +def _request(module: ModuleType): + """Return one complete trusted mention request.""" + + return module.MentionRequest( + "ContextualWisdomLab/example", + 17, + "a" * 40, + "main", + 91, + "maintainer", + ("cwl-noema-review", "opencode-agent"), + pull_request_base_sha="b" * 40, + ) + + +def _wrapper_forward_payload_keys(workflow_text: str) -> tuple[str, ...]: + """Extract top-level client_payload keys from one wrapper forwarder.""" + + match = WRAPPER_CLIENT_PAYLOAD_RE.search(workflow_text) + assert match is not None + keys = tuple(WRAPPER_PAYLOAD_KEY_RE.findall(match.group("body"))) + assert keys + assert len(keys) == len(set(keys)) + return keys + + +def test_github_repository_dispatch_limit_is_ten_top_level_keys() -> None: + """The router constant matches GitHub's documented client_payload cap.""" + + router = _load_router() + assert router.REPOSITORY_DISPATCH_CLIENT_PAYLOAD_MAX_KEYS == 10 + assert GITHUB_DOCS in ( + ROOT / "docs" / "automation" / "review-agent-comment-invocation.md" + ).read_text(encoding="utf-8") + + +def test_mention_router_payloads_stay_within_github_key_limit() -> None: + """Both first-hop mention dispatches keep identity without exceeding 10 keys.""" + + router = _load_router() + request = _request(router) + noema = router.noema_payload(request)["client_payload"] + opencode = router.opencode_payload(request)["client_payload"] + limit = router.REPOSITORY_DISPATCH_CLIENT_PAYLOAD_MAX_KEYS + + assert len(noema) <= limit + assert len(opencode) <= limit + assert REQUIRED_IDENTITY_KEYS <= noema.keys() + assert REQUIRED_IDENTITY_KEYS <= opencode.keys() + assert { + "trigger_reviews", + "review_dispatch_limit", + "enable_auto_merge", + "update_branches", + "merge_mode", + }.isdisjoint(opencode.keys()) + + +def test_wrapper_forwarders_stay_within_github_key_limit() -> None: + """Mention-forwarder jq payloads also stay at or under 10 top-level keys.""" + + router = _load_router() + limit = router.REPOSITORY_DISPATCH_CLIENT_PAYLOAD_MAX_KEYS + noema_keys = _wrapper_forward_payload_keys( + NOEMA_WORKFLOW.read_text(encoding="utf-8") + ) + opencode_keys = _wrapper_forward_payload_keys( + OPENCODE_WORKFLOW.read_text(encoding="utf-8") + ) + + assert len(noema_keys) <= limit + assert len(opencode_keys) <= limit + assert REQUIRED_IDENTITY_KEYS <= set(noema_keys) + assert REQUIRED_IDENTITY_KEYS <= set(opencode_keys) + assert OPENCODE_FORWARD_SAFETY_KEYS <= set(opencode_keys) + assert "trigger_reviews" not in opencode_keys + assert "review_dispatch_limit" not in opencode_keys + assert "requested_agent" not in opencode_keys + assert "requested_by" not in opencode_keys + + +def test_repository_dispatch_body_rejects_more_than_ten_keys() -> None: + """An oversized client_payload fails closed before GitHub returns HTTP 422.""" + + router = _load_router() + oversized = {f"field_{index}": index for index in range(11)} + with pytest.raises(ValueError, match="GitHub allows at most 10"): + router.repository_dispatch_body("agent-mention-opencode", oversized) diff --git a/tests/test_agent_mention_router.py b/tests/test_agent_mention_router.py index 4509d43f0..874a79e4f 100644 --- a/tests/test_agent_mention_router.py +++ b/tests/test_agent_mention_router.py @@ -222,9 +222,13 @@ def test_eligible_agents_and_payloads() -> None: assert opencode["event_type"] == "agent-mention-opencode" assert opencode["client_payload"]["base_branch"] == "develop" assert opencode["client_payload"]["pr_base_sha"] == "b" * 40 - assert opencode["client_payload"]["merge_mode"] == "disabled" - assert opencode["client_payload"]["enable_auto_merge"] is False - assert opencode["client_payload"]["update_branches"] is False + assert "merge_mode" not in opencode["client_payload"] + assert "enable_auto_merge" not in opencode["client_payload"] + assert "update_branches" not in opencode["client_payload"] + claim = module.agent_invocation_claim(request, "opencode-agent") + assert claim["merge_mode"] == "disabled" + assert claim["enable_auto_merge"] is False + assert claim["update_branches"] is False def test_dispatch_uses_central_events_and_acknowledges() -> None: From ee7761c1bbab4cb3cba72ccc0f499f7d9305c965 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 16:26:55 -0700 Subject: [PATCH 2/4] test(automation): reproduce dropped pending agent mentions --- tests/test_agent_mention_queue_isolation.py | 71 +++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 tests/test_agent_mention_queue_isolation.py diff --git a/tests/test_agent_mention_queue_isolation.py b/tests/test_agent_mention_queue_isolation.py new file mode 100644 index 000000000..8af11e04a --- /dev/null +++ b/tests/test_agent_mention_queue_isolation.py @@ -0,0 +1,71 @@ +"""Regression contracts for isolated review-agent mention queues.""" + +from __future__ import annotations + +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +WORKFLOW = ROOT / ".github" / "workflows" / "agent-mention-router.yml" + + +def _job_block(workflow: str, job_name: str, next_job_name: str | None) -> str: + """Return one top-level workflow job bounded by the following job.""" + + jobs = workflow.split("\njobs:\n", 1)[1] + start = jobs.index(f" {job_name}:\n") + if next_job_name is None: + return jobs[start:] + end = jobs.index(f"\n {next_job_name}:\n", start) + return jobs[start:end] + + +def _concurrency_block(job: str) -> str: + """Return the job-scoped concurrency mapping before ``runs-on``.""" + + start = job.index(" concurrency:\n") + end = job.index("\n runs-on:", start) + return job[start:end] + + +def test_interactive_mentions_and_sweeps_use_independent_queues() -> None: + """A scheduled sweep cannot replace a pending trusted mention request.""" + + workflow = WORKFLOW.read_text(encoding="utf-8") + header = workflow.split("\njobs:\n", 1)[0] + local_job = _job_block( + workflow, + "route-local-agent-mention", + "sweep-organization-agent-mentions", + ) + sweep_job = _job_block( + workflow, + "sweep-organization-agent-mentions", + None, + ) + + assert not any(line.startswith("concurrency:") for line in header.splitlines()) + assert _concurrency_block(local_job) == ( + " concurrency:\n" + " group: review-agent-mention-router-local-${{ github.repository }}\n" + " queue: max" + ) + assert _concurrency_block(sweep_job) == ( + " concurrency:\n" + " group: review-agent-mention-router-sweep-${{ github.repository }}\n" + " cancel-in-progress: false" + ) + + +def test_interactive_queue_retains_pending_requests_without_cancellation() -> None: + """The bounded interactive queue retains work and never cancels in progress.""" + + workflow = WORKFLOW.read_text(encoding="utf-8") + local_job = _job_block( + workflow, + "route-local-agent-mention", + "sweep-organization-agent-mentions", + ) + concurrency = _concurrency_block(local_job) + + assert "queue: max" in concurrency + assert "cancel-in-progress: true" not in concurrency From 6b398ded1de4bb448c24783855bda1e36bfad29e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 16:27:36 -0700 Subject: [PATCH 3/4] fix(automation): isolate interactive agent mention queue --- .github/workflows/agent-mention-router.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/agent-mention-router.yml b/.github/workflows/agent-mention-router.yml index f14667a93..b922ba5ab 100644 --- a/.github/workflows/agent-mention-router.yml +++ b/.github/workflows/agent-mention-router.yml @@ -6,10 +6,6 @@ on: schedule: - cron: "*/5 * * * *" -concurrency: - group: review-agent-mention-router-${{ github.repository }} - cancel-in-progress: false - # Organization required-workflow rules do not propagate issue_comment events # into sibling repositories. Keep the workflow default read-only; each bounded # job declares only the writes it actually needs. @@ -28,6 +24,9 @@ jobs: contains(github.event.comment.body, '@cwl-noema-review') || contains(github.event.comment.body, '@opencode-agent') ) + concurrency: + group: review-agent-mention-router-local-${{ github.repository }} + queue: max runs-on: ubuntu-24.04 timeout-minutes: 5 permissions: @@ -70,6 +69,9 @@ jobs: if: >- github.repository == 'ContextualWisdomLab/.github' && github.event_name == 'schedule' + concurrency: + group: review-agent-mention-router-sweep-${{ github.repository }} + cancel-in-progress: false runs-on: ubuntu-24.04 timeout-minutes: 15 permissions: From 099faef0f942afe88de417921214afdf30c15ea5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 16:28:17 -0700 Subject: [PATCH 4/4] docs(automation): record mention routing reliability boundary --- .../agent-mention-concurrency-isolation.md | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 docs/doctoring/agent-mention-concurrency-isolation.md diff --git a/docs/doctoring/agent-mention-concurrency-isolation.md b/docs/doctoring/agent-mention-concurrency-isolation.md new file mode 100644 index 000000000..163a5bc85 --- /dev/null +++ b/docs/doctoring/agent-mention-concurrency-isolation.md @@ -0,0 +1,94 @@ +# Review-agent mention routing reliability + +Review date: **2026-08-19** + +## Incident + +Trusted `@opencode-agent` comments could remain unacknowledged and fail to start the existing OpenCode review path. Two independent control-plane defects produced the same operator-visible symptom before model execution. + +1. The OpenCode `repository_dispatch.client_payload` exceeded GitHub's ten-property limit, so GitHub rejected the request with HTTP 422 before the trusted wrapper started. +2. Interactive `issue_comment` routing and the five-minute organization sweep shared one workflow-level concurrency group. Under the default single-pending contract, a newly queued sweep could replace a pending interactive mention before exact-head resolution, durable claim creation, dispatch, or acknowledgement. + +Neither defect is evidence that the requesting maintainer, model, repository allowlist, or final review result is invalid. + +## Test-first repair + +The permanent regression contracts were committed before their corresponding production changes. + +- `tests/test_agent_mention_dispatch_payload_limit.py` requires both dispatch hops to stay at or below ten top-level payload properties and requires the router to reject an oversized payload before GitHub does. +- `tests/test_agent_mention_queue_isolation.py` requires the interactive route and scheduled sweep to use different job-level concurrency groups, with `queue: max` on the interactive route and no cancellation of in-progress interactive work. + +## Decision + +### Bounded dispatch envelope + +The router-to-wrapper OpenCode payload carries nine identity and provenance fields. Review-only behavior remains bound into the canonical invocation hash and is reconstructed by the trusted wrapper: + +```text +trigger_reviews=true +review_dispatch_limit=1 +enable_auto_merge=false +update_branches=false +merge_mode=disabled +``` + +The wrapper-to-scheduler payload carries exactly ten fields, including the three values that override unsafe scheduler defaults. The wrapper therefore remains review-only and cannot merge or update a branch. + +### Isolated concurrency queues + +Concurrency is scoped to each job rather than the whole workflow: + +```yaml +route-local-agent-mention: + concurrency: + group: review-agent-mention-router-local-${{ github.repository }} + queue: max + +sweep-organization-agent-mentions: + concurrency: + group: review-agent-mention-router-sweep-${{ github.repository }} + cancel-in-progress: false +``` + +GitHub documents that `queue: max` permits up to 100 pending jobs or workflow runs in one concurrency group and cannot be combined with `cancel-in-progress: true`. The interactive queue therefore retains bounded pending requests instead of replacing the previous pending request. Scheduled sweeps retain coalescing behavior in a separate group and cannot displace interactive work. + +Concurrency is not the idempotency authority. Duplicate forwarding remains governed by the complete canonical invocation key, exact-key downstream concurrency, and the immutable exact-name Actions artifact ledger. + +## Preserved boundaries + +- No model provider, reviewer identity, repository allowlist, token name, credential scope, or branch-protection rule changes. +- `COPILOT_GITHUB_TOKEN` remains unused. +- Workflow-default permissions remain read-only; existing bounded jobs keep only their required writes. +- Only trusted non-bot `OWNER`, `MEMBER`, or `COLLABORATOR` comments on open pull requests are eligible. +- Pull request number, exact head and base SHAs, base branch, source comment, requested agent, and requesting actor remain bound to the invocation key. +- Mention routing remains unable to approve, merge, update branches, publish, or release. + +## Operational acceptance + +After protected integration: + +1. submit a fresh trusted `@opencode-agent` comment on an open pull request; +2. require the hidden receipt marker, acknowledgement comment, or durable exact-name artifact for the source comment; +3. require the trusted OpenCode wrapper and review-only scheduler dispatch to start for the same repository, pull request, and exact head; +4. verify that a scheduled sweep cannot cancel or replace the interactive route; +5. distinguish downstream provider or review failure from routing failure rather than treating every missing verdict as the same incident. + +A receipt proves routing and durable claim processing. It is not an approval and never substitutes for exact-head checks or branch protection. + +## Rollback prohibition + +Do not restore either defective boundary: + +- do not increase the first- or second-hop payload beyond GitHub's limit; +- do not move local and scheduled work back into one workflow-level concurrency group; +- do not replace `queue: max` with the default single-pending interactive queue unless another independently reviewed durable queue preserves every eligible request. + +A safe emergency degradation may suspend the scheduled sweep while retaining the isolated interactive route. + +## References + +GitHub. (n.d.). *Control the concurrency of workflows and jobs*. GitHub Docs. Retrieved August 19, 2026, from https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency + +GitHub. (n.d.). *REST API endpoints for repositories: Create a repository dispatch event*. GitHub Docs. Retrieved August 19, 2026, from https://docs.github.com/en/rest/repos/repos#create-a-repository-dispatch-event + +GitHub. (n.d.). *Store and share data with workflow artifacts*. GitHub Docs. Retrieved August 19, 2026, from https://docs.github.com/en/actions/tutorials/store-and-share-data