Skip to content
Open
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
213 changes: 213 additions & 0 deletions .github/workflows/agent-mention-opencode-dispatch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
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:${{
github.event.client_payload.agent_invocation_key }}]

on:
repository_dispatch:
types: [agent-mention-opencode]

concurrency:
group: agent-mention-opencode-${{ github.event.client_payload.agent_invocation_key || github.run_id }}
cancel-in-progress: false
queue: max

permissions:
contents: read

jobs:
validate-and-forward:
if: github.repository == 'ContextualWisdomLab/.github'
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
actions: read
contents: write
env:
GH_TOKEN: ${{ github.token }}
REQUESTED_AGENT: "opencode-agent"
PAYLOAD_AGENT: ${{ github.event.client_payload.requested_agent || '' }}
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 || 'true' }}
REVIEW_DISPATCH_LIMIT: ${{ github.event.client_payload.review_dispatch_limit || '1' }}
ENABLE_AUTO_MERGE: ${{ github.event.client_payload.enable_auto_merge || 'false' }}
UPDATE_BRANCHES: ${{ github.event.client_payload.update_branches || 'false' }}
MERGE_MODE: ${{ github.event.client_payload.merge_mode || 'disabled' }}
steps:
- name: Validate exact invocation payload
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
echo "::error::Rejected malformed or mismatched OpenCode agent invocation payload."
exit 1
fi

python3 - <<'PYTHON'
import hashlib
import hmac
import json
import os

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",
},
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")
PYTHON

- name: Inspect exact-name Actions artifact ledger
id: ledger
run: |
set -euo pipefail
LEDGER_ARTIFACT_NAME="cwl-agent-invocation-${INVOCATION_KEY}"
export LEDGER_ARTIFACT_NAME
echo "LEDGER_ARTIFACT_NAME=$LEDGER_ARTIFACT_NAME" >>"$GITHUB_ENV"
response_file="${RUNNER_TEMP}/agent-mention-artifacts.json"
gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts" \
-X GET \
-f "name=${LEDGER_ARTIFACT_NAME}" \
-f "per_page=100" >"$response_file"
python3 - "$response_file" <<'PYTHON'
import json
import os
from pathlib import Path
import sys

response_path = Path(sys.argv[1])
payload = json.loads(response_path.read_text(encoding="utf-8"))
expected_name = os.environ["LEDGER_ARTIFACT_NAME"]
if not isinstance(payload, dict):
raise SystemExit("artifact response must be an object")
total_count = payload.get("total_count")
artifacts = payload.get("artifacts")
if type(total_count) is not int or total_count < 0:
raise SystemExit("artifact response has an invalid total_count")
if not isinstance(artifacts, list):
raise SystemExit("artifact response has an invalid artifacts collection")
if total_count != len(artifacts):
raise SystemExit("artifact response is truncated or inconsistent")
live = False
for artifact in artifacts:
if not isinstance(artifact, dict):
raise SystemExit("artifact response contains a non-object record")
artifact_id = artifact.get("id")
name = artifact.get("name")
expired = artifact.get("expired")
if type(artifact_id) is not int or artifact_id < 1:
raise SystemExit("artifact response contains an invalid artifact id")
if not isinstance(name, str) or name != expected_name:
raise SystemExit("artifact response contains a mismatched artifact name")
if type(expired) is not bool:
raise SystemExit("artifact response contains an invalid expired flag")
live = live or not expired

output_path = Path(os.environ["GITHUB_OUTPUT"])
if live:
with output_path.open("a", encoding="utf-8") as handle:
handle.write("claim=false\n")
raise SystemExit(0)

claim_dir = Path(os.environ["RUNNER_TEMP"]) / "cwl-agent-invocation-ledger"
claim_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
claim = {
"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"],
"invocation_key": os.environ["INVOCATION_KEY"],
"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_dir / "claim.json").write_text(
json.dumps(claim, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
with output_path.open("a", encoding="utf-8") as handle:
handle.write("claim=true\n")
PYTHON

- name: Claim exact invocation in the durable artifact ledger
if: steps.ledger.outputs.claim == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cwl-agent-invocation-${{ env.INVOCATION_KEY }}
path: ${{ runner.temp }}/cwl-agent-invocation-ledger/claim.json
if-no-files-found: error
retention-days: 30
compression-level: 0
overwrite: false
include-hidden-files: false

- name: Forward once to the authoritative review-only scheduler
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" \
'{
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",
}
Comment on lines +198 to +211

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check which merge-scheduler client_payload fields the consumer reads.
set -euo pipefail

echo "== merge-scheduler dispatch consumers =="
rg -n -C 5 'merge-scheduler' --glob '.github/workflows/*.yml' || true

echo "== scheduler references to dropped fields =="
rg -n -C 3 'agent_invocation_key|source_comment_id' --glob '*.py' --glob '*.yml' || echo "no references found"

Repository: ContextualWisdomLab/.github

Length of output: 206


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workflow files =="
git ls-files '.github/workflows/*.yml' '.github/workflows/*.yaml' | sed -n '1,200p'

echo "== target workflow candidate =="
fd -a 'agent-mention-opencode-dispatch\.yml$' . | sed -n '1,20p'

echo "== target workflow lines 150-230 =="
file="$(fd 'agent-mention-opencode-dispatch\.yml$' . | head -n1)"
if [ -n "${file:-}" ]; then
  wc -l "$file"
  sed -n '150,230p' "$file" | nl -ba -v150
fi

echo "== all client_payload / dispatch references in workflows =="
rg -n -C 4 'client_payload|agent_invocation_key|source_comment_id|event_type: "merge-scheduler"|event_type: "merge' --glob '.github/workflows/*.yml' --glob '.github/workflows/*.yaml' . || true

Repository: ContextualWisdomLab/.github

Length of output: 1255


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repository files mentioning merge-scheduler =="
rg -n -C 5 'merge-scheduler' . || true

echo "== repository files mentioning scheduler comment/invocation fields =="
rg -n -C 4 'agent_invocation_key|source_comment_id|invocation_key|comment_id|comment' . | sed -n '1,260p' || true

echo "== Python/scheduler candidate files =="
fd -e py . | sed -n '1,200p'

Repository: ContextualWisdomLab/.github

Length of output: 50385


🏁 Script executed:

#!/bin/bash
set -euo pipefail

target="$(fd 'agent-mention-opencode-dispatch\.yml$' . | head -n1 || true)"
if [ -n "${target:-}" ]; then
  echo "== file: $target =="
  wc -l "$target"
  echo "== lines 1 - 120 =="
  sed -n '1,120p' "$target" | nl -ba -v1
  echo "== lines 120 - 230 =="
  sed -n '120,230p' "$target" | nl -ba -v120
else
  echo "missing agent-mention-opencode-dispatch.yml"
fi

echo "== tracked files with 'agent_invocation_key' or 'source_comment_id' excluding docs/tests =="
rg -n -C 3 'agent_invocation_key|source_comment_id' --glob '!PR_GOVERNANCE_AUDIT.md' --glob '!CLAUDE.md' --glob '!docs/**' --glob '!tests/**' . || true

echo "== all repository files with event_type merge-scheduler ==
rg -n -C 4 '"event_type":\s*"merge-scheduler"|event_type:' --glob '.github/workflows/*.yml' --glob '.github/workflows/*.yaml' .github || true

Repository: ContextualWisdomLab/.github

Length of output: 3351


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate dispatch/workflow files =="
git ls-files | rg '(^|/)(agent-mention-opencode-dispatch|agent-mention-router|pr-review-merge-scheduler)\.(yml|yaml|py)$|(^|/)workflows/.*(yml|yaml)$|(^|/)scripts/ci/.*\.py$' | sed -n '1,220p'

echo "== merge-scheduler event consumers =="
rg -n -C 5 '"merge-scheduler"|merge-scheduler|workflow_id' .github/workflows || true

echo "== agent_invocation_key/source_comment_id references =="
rg -n -C 3 'agent_invocation_key|source_comment_id|comment_id|invocation_key' --glob '!PR_GOVERNANCE_AUDIT.md' --glob '!CLAUDE.md' --glob '!docs/**' --glob '!tests/**' . || true

echo "== merged PR file content if absent from repository =="
git rev-parse --verify HEAD >/dev/null 2>&1 || true
git show HEAD:.github/workflows/agent-mention-opencode-dispatch.yml 2>/dev/null | sed -n '160,225p' | nl -ba -v160 || true

Repository: ContextualWisdomLab/.github

Length of output: 21011


🌐 Web query:

GitHub Actions repository_dispatch client_payload limit 10 items documentation

💡 Result:

When using the GitHub Actions repository_dispatch event, the client_payload is subject to specific technical limitations enforced by the GitHub API [1][2]: * Maximum top-level properties: The client_payload JSON object can contain a maximum of 10 top-level properties [1][3][4]. If this limit is exceeded, the request will fail with an error such as "No more than 10 properties are allowed" [5][2]. * Payload size: The entire client_payload is limited to a maximum of 65,535 characters [1][4]. Exceeding this limit will result in a "client_payload is too large" error [5][2]. Workaround: If you need to send more than 10 properties, you can bypass the top-level property limit by nesting your data within a single top-level object [5][3]. For example, instead of sending multiple properties at the root level, structure your JSON to contain a single key that holds your entire payload [5][6]: { "data": { "key1": "value1", "key2": "value2",... "key15": "value15" } } By wrapping the content in this way, you satisfy the requirement for a maximum of 10 top-level properties while still being able to pass complex or extensive data, provided the total size remains under the 65,535-character limit [1][5].

Citations:


client_payload의 필드 수제한을 유지하면서 agent_invocation_keysource_comment_id를 전달하세요.

repository_dispatchclient_payload에 최대 10개의 top-level 키만 허용하므로, 현재 10개 항목이 채워진 payload는 새 필드를 추가할 수 없습니다. 이 두 필드는 원본 멘션 댓글 연결과 agent invocation 중복 처리에 필요하므로, 같은 키 안에서 구조화하거나 기존 불필요 필드를 정리해 전달해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/agent-mention-opencode-dispatch.yml around lines 198 -
211, Update the repository dispatch payload’s client_payload block to pass
agent_invocation_key and source_comment_id while keeping no more than 10
top-level keys. Consolidate related values under an existing nested key or
remove unnecessary fields, and preserve the values needed for original comment
correlation and duplicate agent-invocation handling.

}' \
| gh api "repos/${GITHUB_REPOSITORY}/dispatches" -X POST --input -
185 changes: 185 additions & 0 deletions .github/workflows/agent-mention-router.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
name: Review Agent Mention Router

on:
issue_comment:
types: [created]
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.
permissions:
contents: read

jobs:
route-local-agent-mention:
if: >-
github.repository == 'ContextualWisdomLab/.github'
&& github.event_name == 'issue_comment'
&& github.event.issue.pull_request
&& github.event.comment.user.type != 'Bot'
&& contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
&& (
contains(github.event.comment.body, '@cwl-noema-review')
|| contains(github.event.comment.body, '@opencode-agent')
)
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
actions: read
contents: write
issues: write
pull-requests: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GH_TOKEN: ${{ github.token }}
TARGET_REPOSITORY_TOKEN: ${{ github.token }}
AGENT_DISPATCH_TOKEN: ${{ github.token }}
OPENCODE_REPOSITORY_DISPATCH_TARGETS: ${{ vars.OPENCODE_REPOSITORY_DISPATCH_TARGETS }}
steps:
- name: Check out trusted default-branch router
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.repository.default_branch }}
persist-credentials: false

- name: Resolve immutable pull-request head
env:
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.issue.number }}
SOURCE_EVENT_PATH: ${{ github.event_path }}
run: |
set -euo pipefail
pr_json="$(gh api "repos/${REPOSITORY}/pulls/${PR_NUMBER}")"
jq \
--argjson pull_request "$pr_json" \
'. + {pull_request: $pull_request}' \
"$SOURCE_EVENT_PATH" >"${RUNNER_TEMP}/agent-mention-event.json"

- name: Route trusted local agent mention
run: >-
python3 scripts/ci/agent_mention_router.py
--event-path "${RUNNER_TEMP}/agent-mention-event.json"

sweep-organization-agent-mentions:
if: >-
github.repository == 'ContextualWisdomLab/.github'
&& github.event_name == 'schedule'
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
actions: read
contents: write
id-token: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
OPENCODE_REPOSITORY_DISPATCH_TARGETS: ${{ vars.OPENCODE_REPOSITORY_DISPATCH_TARGETS }}
LOOKBACK_HOURS: ${{ vars.AGENT_MENTION_LOOKBACK_HOURS || '168' }}
MAX_DISPATCHES: ${{ vars.AGENT_MENTION_MAX_DISPATCHES || '20' }}
DRY_RUN: "false"
steps:
- name: Exchange OpenCode app token for sibling-repository comments
id: sweep_app_token
env:
OIDC_AUDIENCE: opencode-github-action
OPENCODE_API_BASE_URL: https://api.opencode.ai
USER_TOKEN_CONFIGURED: ${{ secrets.PR_REVIEW_MERGE_TOKEN != '' || secrets.OPENCODE_APPROVE_TOKEN != '' }}
run: |
set -euo pipefail
mark_unavailable() {
echo "available=false" >>"$GITHUB_OUTPUT"
}
if [ "$USER_TOKEN_CONFIGURED" = "true" ]; then
echo "A configured cross-repository user token takes precedence."
mark_unavailable
exit 0
fi
if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then
echo "OpenCode app token exchange unavailable: OIDC request environment is missing."
mark_unavailable
exit 0
fi
request_url="${ACTIONS_ID_TOKEN_REQUEST_URL}"
separator="&"
case "$request_url" in
*\?*) ;;
*) separator="?" ;;
esac
if ! oidc_response="$(
curl -fsS --connect-timeout 10 --max-time 30 \
-H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \
"${request_url}${separator}audience=${OIDC_AUDIENCE}"
)"; then
echo "OpenCode app token exchange unavailable: OIDC token request did not complete."
mark_unavailable
exit 0
fi
oidc_token="$(jq -r '.value // empty' <<<"$oidc_response")"
if [ -z "$oidc_token" ]; then
echo "OpenCode app token exchange unavailable: OIDC token response was empty."
mark_unavailable
exit 0
fi
if ! token_response="$(
curl -fsS --connect-timeout 10 --max-time 30 \
-X POST \
-H "Authorization: Bearer ${oidc_token}" \
"${OPENCODE_API_BASE_URL}/exchange_github_app_token"
)"; then
echo "OpenCode app token exchange unavailable: app token request did not complete."
mark_unavailable
exit 0
fi
app_token="$(jq -r '.token // empty' <<<"$token_response")"
if [ -z "$app_token" ]; then
echo "OpenCode app token exchange unavailable: app token response was empty."
mark_unavailable
exit 0
fi
echo "::add-mask::$app_token"
echo "available=true" >>"$GITHUB_OUTPUT"
echo "SWEEP_APP_TOKEN=$app_token" >>"$GITHUB_ENV"

- name: Check out trusted central router
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.repository.default_branch }}
persist-credentials: false

- name: Sweep recent organization PR comments
env:
PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}
OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }}
AGENT_DISPATCH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if [ -n "$PR_REVIEW_MERGE_TOKEN" ]; then
TARGET_REPOSITORY_TOKEN="$PR_REVIEW_MERGE_TOKEN"
TARGET_REPOSITORY_SOURCE="organization"
elif [ -n "$OPENCODE_APPROVE_TOKEN" ]; then
TARGET_REPOSITORY_TOKEN="$OPENCODE_APPROVE_TOKEN"
TARGET_REPOSITORY_SOURCE="organization"
else
TARGET_REPOSITORY_TOKEN="${SWEEP_APP_TOKEN:-}"
TARGET_REPOSITORY_SOURCE="${TARGET_REPOSITORY_TOKEN:+installation}"
fi
export TARGET_REPOSITORY_TOKEN
if [ -z "$TARGET_REPOSITORY_TOKEN" ] || [ -z "$TARGET_REPOSITORY_SOURCE" ]; then
echo "::error::Agent mention sweep requires PR_REVIEW_MERGE_TOKEN, OPENCODE_APPROVE_TOKEN, or the OpenCode app token exchange."
exit 1
fi
args=(
--organization ContextualWisdomLab
--repository-source "$TARGET_REPOSITORY_SOURCE"
--lookback-hours "$LOOKBACK_HOURS"
--max-dispatches "$MAX_DISPATCHES"
)
if [ "$DRY_RUN" = "true" ]; then
args+=(--dry-run)
fi
python3 scripts/ci/agent_mention_sweep.py "${args[@]}"
Loading
Loading