From c04131b94f614395e92ca42818c99f9691579be1 Mon Sep 17 00:00:00 2001 From: Adam Scerra Date: Thu, 25 Jun 2026 18:11:10 -0400 Subject: [PATCH] fix(dispatch): add label-based handoffs for bot-to-bot dispatch paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR 0054's is_event_actor_authorized check blocks GitHub App bot accounts (which return empty role_name from the collaborator API) from triggering dispatch via issues.opened and pull_request_target.opened. This breaks two agent handoff paths: - retro → triage: post-retro.sh creates bare issues, bot fails auth - code → review: post-code.sh creates PRs, bot fails auth Fix by adding label-based handoffs consistent with how triage → code already works (via ready-to-code label). Label application requires write access, serving as an implicit authorization gate per ADR 0054. Changes: - Add ready-for-triage label trigger to dispatch routing - post-retro.sh applies ready-for-triage on issue creation - post-code.sh applies ready-for-review after PR creation - Update glossary, ADR 0054 annotation, and agent docs with ready-for-triage - retro.md: document ready-for-triage as a control label - triage.md: document ready-for-triage as an input trigger Closes #2636 Closes #2669 Signed-off-by: Adam Scerra Co-authored-by: Cursor --- .github/workflows/reusable-dispatch.yml | 4 +++- ...quire-authorization-on-all-agent-dispatch-paths.md | 8 ++++---- docs/agents/retro.md | 4 +++- docs/agents/triage.md | 8 +++++--- docs/glossary.md | 2 +- .../fullsend-repo/.github/workflows/dispatch.yml | 6 ++++-- internal/scaffold/fullsend-repo/scripts/post-code.sh | 11 +++++++++++ .../scaffold/fullsend-repo/scripts/post-retro-test.sh | 5 +++++ internal/scaffold/fullsend-repo/scripts/post-retro.sh | 3 ++- internal/scaffold/scaffold_test.go | 1 + 10 files changed, 39 insertions(+), 13 deletions(-) diff --git a/.github/workflows/reusable-dispatch.yml b/.github/workflows/reusable-dispatch.yml index 4195d1d5e3..71c2468a6d 100644 --- a/.github/workflows/reusable-dispatch.yml +++ b/.github/workflows/reusable-dispatch.yml @@ -241,7 +241,9 @@ jobs: STAGE="triage" fi elif [[ "${EVENT_ACTION}" == "labeled" ]]; then - if [[ "${TRIGGERING_LABEL}" == "ready-to-code" ]]; then + if [[ "${TRIGGERING_LABEL}" == "ready-for-triage" ]]; then + STAGE="triage" + elif [[ "${TRIGGERING_LABEL}" == "ready-to-code" ]]; then STAGE="code" elif [[ "${TRIGGERING_LABEL}" == "ready-for-review" ]]; then if [[ "${ISSUE_IS_PR}" == "true" ]]; then diff --git a/docs/ADRs/0054-require-authorization-on-all-agent-dispatch-paths.md b/docs/ADRs/0054-require-authorization-on-all-agent-dispatch-paths.md index 40d18dc955..012c2f2866 100644 --- a/docs/ADRs/0054-require-authorization-on-all-agent-dispatch-paths.md +++ b/docs/ADRs/0054-require-authorization-on-all-agent-dispatch-paths.md @@ -140,10 +140,10 @@ inference compute on them automatically. Agent-to-agent handoffs use label-based triggers, not slash commands. When one agent completes a stage, its post-script applies a label -(e.g., `ready-to-code`, `ready-for-review`) which triggers the next -stage via the `issues.labeled` dispatch path. Label application requires -write access — an implicit authorization gate — so no explicit -`is_authorized` check is needed on that path. +(e.g., `ready-for-triage`, `ready-to-code`, `ready-for-review`) which +triggers the next stage via the `issues.labeled` dispatch path. Label +application requires write access — an implicit authorization gate — so +no explicit `is_authorized` check is needed on that path. The `COMMENT_USER_TYPE != "Bot"` check in the slash command guard means bot accounts cannot invoke slash commands at all (the condition diff --git a/docs/agents/retro.md b/docs/agents/retro.md index d7ff0f79e9..98843646ad 100644 --- a/docs/agents/retro.md +++ b/docs/agents/retro.md @@ -41,7 +41,9 @@ The retro agent also runs automatically when a PR is closed (merged or not). ## Control labels -The retro agent does not apply or consume control labels. +| Label | Meaning | +|-------|---------| +| `ready-for-triage` | Applied by the post-script to proposal issues so they enter the [triage](triage.md) pipeline automatically. | ## Configuration and extension diff --git a/docs/agents/triage.md b/docs/agents/triage.md index 664faf1312..5db79134e0 100644 --- a/docs/agents/triage.md +++ b/docs/agents/triage.md @@ -28,9 +28,11 @@ The `/fs-triage` command does not accept arguments — it re-evaluates the issue using current content, comments, and any prior triage analysis. Triage also runs automatically when a new issue is opened or edited by a -repository owner, member, or collaborator, and when someone comments on an -issue labeled `needs-info` (to re-evaluate after the reporter provides -clarification). +repository owner, member, or collaborator, when the `ready-for-triage` +label is applied to an issue (used by the [retro agent](retro.md) to +route proposal issues into the triage pipeline), and when someone +comments on an issue labeled `needs-info` (to re-evaluate after the +reporter provides clarification). ## Control labels diff --git a/docs/glossary.md b/docs/glossary.md index 695f063740..b9d96110c4 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -83,7 +83,7 @@ See [architecture.md](architecture.md) and [agent-architecture.md](problems/agen ### Label State Machine -The set of valid label transitions on issues and PRs that encode workflow state. Labels like `ready-to-code` drive agent dispatch; others such as `ready-for-merge` and `requires-manual-review` encode review outcomes. In per-repo installs, `ready-for-review` on a PR also triggers review; applying it to a standalone issue does not. Per-org installs still accept legacy issue-side review triggers pending a follow-up. The label state machine guard validates that transitions are legal and enforces mutual exclusion — for example, starting a triage run clears downstream labels so stale state does not carry forward. +The set of valid label transitions on issues and PRs that encode workflow state. Labels like `ready-for-triage`, `ready-to-code`, and `ready-for-review` drive agent dispatch; others such as `ready-for-merge` and `requires-manual-review` encode review outcomes. In per-repo installs, `ready-for-review` on a PR also triggers review; applying it to a standalone issue does not. Per-org installs still accept legacy issue-side review triggers pending a follow-up. The label state machine guard validates that transitions are legal and enforces mutual exclusion — for example, starting a triage run clears downstream labels so stale state does not carry forward. See [ADR 0002](ADRs/0002-initial-fullsend-design.md) building block 3. ## M diff --git a/internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml b/internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml index eb132f011c..39dd443c2a 100644 --- a/internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml +++ b/internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml @@ -1,5 +1,5 @@ --- -# lint-workflow-size: max-lines=470 +# lint-workflow-size: max-lines=475 # Dispatcher workflow that routes events to agent workflows based on stage. # Routing logic determines the stage from event context — the shim only # forwards the raw event. Adding a new stage requires only a case branch @@ -185,7 +185,9 @@ jobs: STAGE="triage" fi elif [[ "${EVENT_ACTION}" == "labeled" ]]; then - if [[ "${TRIGGERING_LABEL}" == "ready-to-code" ]]; then + if [[ "${TRIGGERING_LABEL}" == "ready-for-triage" ]]; then + STAGE="triage" + elif [[ "${TRIGGERING_LABEL}" == "ready-to-code" ]]; then STAGE="code" elif [[ "${TRIGGERING_LABEL}" == "ready-for-review" ]]; then if [[ "${ISSUE_HAS_PR}" == "true" ]]; then diff --git a/internal/scaffold/fullsend-repo/scripts/post-code.sh b/internal/scaffold/fullsend-repo/scripts/post-code.sh index 05a8826c4f..d48abb1d40 100755 --- a/internal/scaffold/fullsend-repo/scripts/post-code.sh +++ b/internal/scaffold/fullsend-repo/scripts/post-code.sh @@ -456,3 +456,14 @@ rm -f "${PR_CREATE_STDERR}" echo "PR created: ${PR_URL}" echo "pr_url=${PR_URL}" >> "${GITHUB_OUTPUT:-/dev/null}" + +# Apply ready-for-review label so the review agent is dispatched via the +# issues.labeled path. The pull_request_target.opened event requires the PR +# author to pass is_event_actor_authorized, which fails for bot accounts +# (GitHub App bots have no collaborator role). The label-based path has no +# explicit auth gate — label application itself requires write access. +PR_NUMBER_FROM_URL="${PR_URL##*/}" +gh issue edit "${PR_NUMBER_FROM_URL}" \ + --repo "${REPO_FULL_NAME}" \ + --add-label "ready-for-review" 2>/dev/null || \ + echo "::warning::Failed to apply ready-for-review label to PR #${PR_NUMBER_FROM_URL}" diff --git a/internal/scaffold/fullsend-repo/scripts/post-retro-test.sh b/internal/scaffold/fullsend-repo/scripts/post-retro-test.sh index 9f5c0b1e6f..08dfd94b8a 100644 --- a/internal/scaffold/fullsend-repo/scripts/post-retro-test.sh +++ b/internal/scaffold/fullsend-repo/scripts/post-retro-test.sh @@ -214,6 +214,11 @@ run_test "happy-path-issue-created" \ "${FIXTURE_ONE_PROPOSAL}" \ "gh issue create" +# Verify that the happy-path applied the ready-for-triage label. +run_test "happy-path-triage-label" \ + "${FIXTURE_ONE_PROPOSAL}" \ + "ready-for-triage" + # Happy path: no proposals, comment posted successfully. run_test "happy-path-no-proposals" \ "${FIXTURE_NO_PROPOSALS}" \ diff --git a/internal/scaffold/fullsend-repo/scripts/post-retro.sh b/internal/scaffold/fullsend-repo/scripts/post-retro.sh index 28e284bed6..46452c408a 100755 --- a/internal/scaffold/fullsend-repo/scripts/post-retro.sh +++ b/internal/scaffold/fullsend-repo/scripts/post-retro.sh @@ -97,7 +97,8 @@ for i in $(seq 0 $((PROPOSAL_COUNT - 1))); do if ! ISSUE_URL=$(gh issue create \ --repo "${TARGET_REPO}" \ --title "${TITLE}" \ - --body "${BODY}" 2>&1); then + --body "${BODY}" \ + --label "ready-for-triage" 2>&1); then echo "ERROR: failed to create issue in ${TARGET_REPO} (gh issue create --repo ${TARGET_REPO}): ${ISSUE_URL}" >&2 exit 1 fi diff --git a/internal/scaffold/scaffold_test.go b/internal/scaffold/scaffold_test.go index 3f8672620c..8f0902c5c1 100644 --- a/internal/scaffold/scaffold_test.go +++ b/internal/scaffold/scaffold_test.go @@ -209,6 +209,7 @@ func TestDispatchWorkflowContent(t *testing.T) { assert.Contains(t, s, "/fs-fix") assert.Contains(t, s, "/fs-retro") assert.Contains(t, s, "/fs-prioritize") + assert.Contains(t, s, "ready-for-triage") assert.Contains(t, s, "ready-to-code") assert.Contains(t, s, "ready-for-review") assert.Contains(t, s, "TRIGGERING_LABEL")