diff --git a/.github/workflows/reusable-dispatch.yml b/.github/workflows/reusable-dispatch.yml index 904e846a5..6a42217f8 100644 --- a/.github/workflows/reusable-dispatch.yml +++ b/.github/workflows/reusable-dispatch.yml @@ -86,7 +86,7 @@ jobs: COMMENT_AUTHOR_ASSOC: ${{ github.event.comment.author_association }} ISSUE_LABELS: ${{ join(github.event.issue.labels.*.name, ',') }} PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }} - ISSUE_HAS_PR: ${{ github.event.issue.pull_request && 'true' || 'false' }} + ISSUE_IS_PR: ${{ github.event.issue.pull_request && 'true' || 'false' }} ISSUE_USER_LOGIN: ${{ github.event.issue.user.login }} REVIEW_STATE: ${{ github.event.review.state }} REVIEW_USER_LOGIN: ${{ github.event.review.user.login }} @@ -134,15 +134,17 @@ jobs: STAGE="triage" ;; /fs-code) - if [[ "${ISSUE_HAS_PR}" == "false" ]]; then + if [[ "${ISSUE_IS_PR}" == "false" ]]; then STAGE="code" fi ;; /fs-review) - STAGE="review" + if [[ "${ISSUE_IS_PR}" == "true" ]]; then + STAGE="review" + fi ;; /fs-fix) - if [[ "${ISSUE_HAS_PR}" == "true" ]]; then + if [[ "${ISSUE_IS_PR}" == "true" ]]; then if [[ "${COMMENT_USER_TYPE}" != "Bot" ]] && is_authorized; then STAGE="fix" TRIGGER_SOURCE="${COMMENT_USER_LOGIN}" @@ -185,7 +187,9 @@ jobs: if [[ "${TRIGGERING_LABEL}" == "ready-to-code" ]]; then STAGE="code" elif [[ "${TRIGGERING_LABEL}" == "ready-for-review" ]]; then - STAGE="review" + if [[ "${ISSUE_IS_PR}" == "true" ]]; then + STAGE="review" + fi fi fi ;; diff --git a/docs/ADRs/0002-initial-fullsend-design.md b/docs/ADRs/0002-initial-fullsend-design.md index d4007f6ff..19202287d 100644 --- a/docs/ADRs/0002-initial-fullsend-design.md +++ b/docs/ADRs/0002-initial-fullsend-design.md @@ -160,6 +160,11 @@ It **does not** read the **issue comment thread** for intake decisions—no scan 2. **`ready-for-review`** label **added** to the issue (or linked PR—policy per repo). Available for manual dispatch. 3. **`/review`** in a comment. +> **Note (2026-06):** In per-repo mode, triggers 2 and 3 (`ready-for-review` +> label, `/fs-review` slash command) dispatch only when the issue has an +> associated pull request (`issue.pull_request` present). Per-org `dispatch.yml` +> is unchanged pending follow-up. + **When a review run starts** (initial review, **`/review`**, or **push-triggered re-review**): **remove** **`ready-for-review`** **and** **`ready-for-merge`**. A new round **supersedes** any prior merge verdict until the coordinator finishes this round—otherwise **`ready-for-merge`** could describe an **old** head after the author **pushed** new commits, which is **unsafe** for bots and humans. Reviewers evaluate the **current** PR head; the coordinator applies outcomes using the algorithm below. (**`requires-manual-review`** is **not** removed here by default—humans may still need to resolve an earlier split verdict unless **repo policy** clears it when enqueueing a new round.) **Review swarm:** diff --git a/docs/ADRs/0033-per-repo-installation-mode.md b/docs/ADRs/0033-per-repo-installation-mode.md index 7f7dcaae9..a43e2ef10 100644 --- a/docs/ADRs/0033-per-repo-installation-mode.md +++ b/docs/ADRs/0033-per-repo-installation-mode.md @@ -170,13 +170,18 @@ The org-level `.fullsend` config repo tier is skipped — the in-repo `.fullsend This is the key new artifact, published in `fullsend-ai/fullsend/.github/workflows/`. It is a reusable version of the per-org `dispatch.yml` (ADR 0034), accepting event context via `workflow_call` inputs and performing the same routing and dispatch logic. The routing logic (identical to per-org `dispatch.yml`) maps: -- `issues` + `labeled` → stage based on label name (`ready-to-code` → code, `ready-for-review` → review) +- `issues` + `labeled` → `ready-to-code` → code - `issue_comment` + slash commands → `/fs-triage`, `/fs-code`, `/fs-review`, `/fs-fix`, `/fs-retro`, `/fs-prioritize` - `issue_comment` + `needs-info` label (non-command) → auto-triage - `pull_request_target` + `opened`/`synchronize`/`ready_for_review` → review - `pull_request_target` + `closed` → retro - `pull_request_review` + `changes_requested` from review bot → fix (same-repo PRs only) +> **Note (2026-06):** Per-repo `reusable-dispatch.yml` gates review label and +> slash-command triggers on `issue.pull_request`. Per-org `dispatch.yml` unchanged +> pending follow-up. Fix dispatch was already PR-only. See +> [ADR 0034](0034-centralized-shim-routing-via-dispatch.md) routing note. + In per-org mode, `dispatch.yml` routes events and dispatches to thin callers via `workflow_call`. In per-repo mode, `reusable-dispatch.yml` routes events and dispatches to per-stage reusable workflows directly via conditional `workflow_call` jobs, keeping the entire pipeline within a single `workflow_call` chain. **Dispatch mechanism**: Per-org uses `workflow_call` to fan out to thin callers in `.fullsend`, which in turn call upstream reusable workflows via `workflow_call`. Per-repo uses conditional `workflow_call` jobs inside `reusable-dispatch.yml` to call `reusable-code.yml` etc. directly, eliminating the need for thin callers. diff --git a/docs/ADRs/0034-centralized-shim-routing-via-dispatch.md b/docs/ADRs/0034-centralized-shim-routing-via-dispatch.md index 6884b1c24..b69fe39fa 100644 --- a/docs/ADRs/0034-centralized-shim-routing-via-dispatch.md +++ b/docs/ADRs/0034-centralized-shim-routing-via-dispatch.md @@ -102,12 +102,16 @@ a stage name: - `issue_comment` with `/fs-triage`, `/fs-code`, `/fs-review`, `/fs-fix`, `/fs-retro`, `/fs-prioritize` commands → corresponding stage - `issue_comment` on `needs-info` issue from non-bot → `triage` -- `issues` + `labeled` with `ready-to-code` or `ready-for-review` → `code` - or `review` +- `issues` + `labeled` with `ready-to-code` → `code` - `pull_request_target` opened/synchronize/ready_for_review → `review` - `pull_request_target` closed → `retro` - `pull_request_review` with bot `changes_requested` → `fix` +> **Note (2026-06):** Per-repo `reusable-dispatch.yml` now requires PR context for +> review label and slash-command triggers (`ready-for-review`, `/fs-review` gated +> on `issue.pull_request`). Per-org `dispatch.yml` is unchanged pending follow-up. +> Fix dispatch was already PR-only via `pull_request_review` and PR-gated `/fs-fix`. + If no stage matches, `dispatch.yml` exits early with no fan-out. The existing kill switch, role enablement, and `# fullsend-stage:` marker scanning ([ADR 0026](0026-stage-based-dispatch-for-agent-workflow-decoupling.md)) run diff --git a/docs/agents/code.md b/docs/agents/code.md index dba86be61..ed2b22262 100644 --- a/docs/agents/code.md +++ b/docs/agents/code.md @@ -37,7 +37,7 @@ on issues (not PRs). The code agent is also triggered automatically when the | Label | Meaning | |-------|---------| | `ready-to-code` | Triggers the code agent. Applied by the [triage](triage.md) post-script for low-risk categories (bug, documentation, performance), or manually by a human for feature work after prioritization. | -| `ready-for-review` | Applied by the code agent's post-script after pushing a PR. Signals the [review agent](review.md) to evaluate the change. | +| `ready-for-review` | Applied by the code agent's post-script after pushing a PR. In per-repo installs, triggers review when applied to a PR; also marks workflow state for humans and the retro agent. | ## Configuration and extension diff --git a/docs/agents/review.md b/docs/agents/review.md index 246275010..009c5d719 100644 --- a/docs/agents/review.md +++ b/docs/agents/review.md @@ -25,11 +25,10 @@ If a prior review exists (e.g., re-review after fixes), it is injected into the | Command | Where | Effect | |---------|-------|--------| -| `/fs-review` | Issue or PR comment | Triggers a review | +| `/fs-review` | PR comment | Triggers a review on the PR (per-repo installs only; standalone issues are ignored) | -The `/fs-review` command does not accept arguments. The review agent also runs -automatically when a PR is opened, synchronized (new commits pushed), or moved -out of draft. +The `/fs-review` command does not accept arguments. The review agent also runs automatically when a PR is opened, +synchronized (new commits pushed), or moved out of draft. ## Control labels @@ -37,7 +36,7 @@ These labels are applied by the review post-script based on the review outcome. | Label | Meaning | |-------|---------| -| `ready-for-review` | Signals the review agent to evaluate the PR. Applied by the [code agent](code.md) post-script. | +| `ready-for-review` | Workflow state marker on the PR. Applied by the [code agent](code.md) post-script after pushing. In per-repo installs, triggers review when applied to a PR. | | `ready-for-merge` | The review agent approved the PR. No blocking findings. | | `requires-manual-review` | The review agent found issues that require human judgment — it could not confidently approve or reject. | | `rejected` | The review agent rejected the PR and the post-script closed it. | diff --git a/docs/glossary.md b/docs/glossary.md index 94eaaa5f4..695f06374 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`, `ready-for-review`, `ready-for-merge`, and `requires-manual-review` are control markers that drive agent dispatch and enforce ordering. 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-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. See [ADR 0002](ADRs/0002-initial-fullsend-design.md) building block 3. ## M diff --git a/docs/guides/user/bugfix-workflow.md b/docs/guides/user/bugfix-workflow.md index 38e0171dc..f38e4ed3b 100644 --- a/docs/guides/user/bugfix-workflow.md +++ b/docs/guides/user/bugfix-workflow.md @@ -46,7 +46,7 @@ These labels track where an issue is in the pipeline: | `feature` | Issue categorized as a feature request | Waits for human prioritization before coding | | `triaged` | Triage passed but not auto-promoted | Waits for human review (applies to features and uncategorized issues) | | `ready-to-code` | Triage passed (bug, docs, performance) | Code agent picks it up | -| `ready-for-review` | PR ready for review (manual trigger) | Review agents evaluate the PR | +| `ready-for-review` | PR ready for review | Per-repo: triggers review when applied to a PR; also runs automatically via PR events | | `ready-for-merge` | All reviewers unanimously approved | PR can be merged per governance policy | | `requires-manual-review` | Reviewers disagreed or flagged security concerns | Human must decide | @@ -124,7 +124,7 @@ The code agent: ### Stage 3: Review -**Triggered by:** `pull_request_target` events (PR opened, push to PR branch, or marked ready for review), `/fs-review` command, or `ready-for-review` label. +**Triggered by:** `pull_request_target` events (PR opened, push to PR branch, or marked ready for review), `/fs-review` on a PR comment, or applying `ready-for-review` to a PR (per-repo installs enforce PR context for the latter two). The review swarm: diff --git a/internal/scaffold/workflow_call_alignment_test.go b/internal/scaffold/workflow_call_alignment_test.go index 0379396e7..584ec628d 100644 --- a/internal/scaffold/workflow_call_alignment_test.go +++ b/internal/scaffold/workflow_call_alignment_test.go @@ -252,3 +252,14 @@ func TestReusableDispatchUsesFullyQualifiedPaths(t *testing.T) { }) } } + +// TestReusableDispatchRoutingContent validates PR-context gating in per-repo +// reusable-dispatch.yml routing (per-org dispatch.yml unchanged). +func TestReusableDispatchRoutingContent(t *testing.T) { + content, err := os.ReadFile(filepath.Join("..", "..", ".github", "workflows", "reusable-dispatch.yml")) + require.NoError(t, err) + s := string(content) + assert.Contains(t, s, "ISSUE_IS_PR") + assert.Regexp(t, `(?s)/fs-review\).*?ISSUE_IS_PR`, s) + assert.Regexp(t, `(?s)ready-for-review.*?ISSUE_IS_PR`, s) +}