Skip to content
Merged
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
16 changes: 5 additions & 11 deletions .github/workflows/agent-mention-opencode-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down Expand Up @@ -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",
Expand All @@ -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
}
}' \
Expand Down
10 changes: 6 additions & 4 deletions .github/workflows/agent-mention-router.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion docs/automation/review-agent-comment-invocation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
94 changes: 94 additions & 0 deletions docs/doctoring/agent-mention-concurrency-isolation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Review-agent mention routing reliability

Review date: **2026-08-19**
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## 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
53 changes: 38 additions & 15 deletions scripts/ci/agent_mention_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"<!-- cwl-agent-mention-receipt:(\d+) -->")
REPOSITORY_DISPATCH_CLIENT_PAYLOAD_MAX_KEYS = 10


@dataclass(frozen=True)
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand Down
11 changes: 11 additions & 0 deletions tests/test_agent_mention_complete_payload_binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]',
Expand Down
Loading
Loading