Skip to content
14 changes: 9 additions & 5 deletions .github/workflows/reusable-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -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)
Comment thread
ifireball marked this conversation as resolved.
Comment thread
ifireball marked this conversation as resolved.
Comment thread
ifireball marked this conversation as resolved.
Comment thread
ifireball marked this conversation as resolved.
Comment thread
ifireball marked this conversation as resolved.
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}"
Expand Down Expand Up @@ -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
;;
Expand Down
5 changes: 5 additions & 0 deletions docs/ADRs/0002-initial-fullsend-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:**
Expand Down
7 changes: 6 additions & 1 deletion docs/ADRs/0033-per-repo-installation-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 6 additions & 2 deletions docs/ADRs/0034-centralized-shim-routing-via-dispatch.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Comment thread
ifireball marked this conversation as resolved.
> **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
Expand Down
2 changes: 1 addition & 1 deletion docs/agents/code.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 4 additions & 5 deletions docs/agents/review.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,18 @@ 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

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. |
Expand Down
2 changes: 1 addition & 1 deletion docs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/guides/user/bugfix-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down Expand Up @@ -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:

Expand Down
11 changes: 11 additions & 0 deletions internal/scaffold/workflow_call_alignment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Loading