From 033cc885cddfbfeff1c7be54fca6224baa9e96fb Mon Sep 17 00:00:00 2001 From: Ralph Bean Date: Fri, 15 May 2026 15:30:56 -0400 Subject: [PATCH 1/5] docs: add design spec for silent skip of unconfigured roles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spec for fixing #973 — change the defaults.roles gate in dispatch workflows to exit 0 with a notice instead of exit 1 with an error when a stage's role is not configured. Assisted-by: Claude Opus 4.6 Signed-off-by: Ralph Bean --- ...5-silent-skip-unconfigured-roles-design.md | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-15-silent-skip-unconfigured-roles-design.md diff --git a/docs/superpowers/specs/2026-05-15-silent-skip-unconfigured-roles-design.md b/docs/superpowers/specs/2026-05-15-silent-skip-unconfigured-roles-design.md new file mode 100644 index 0000000000..86ac217100 --- /dev/null +++ b/docs/superpowers/specs/2026-05-15-silent-skip-unconfigured-roles-design.md @@ -0,0 +1,40 @@ +# Silent skip for unconfigured roles in dispatch workflows + +**Issue:** [#973](https://github.com/fullsend-ai/fullsend/issues/973) +**Date:** 2026-05-15 + +## Problem + +The shim workflow dispatches `stage=retro` on every PR close event. On orgs that have not configured the retro agent (no `fullsend` role in `defaults.roles`), the `defaults.roles` gate in `dispatch.yml` rejects the dispatch with `exit 1` and an `::error::` annotation. This produces a failed workflow run on every merged PR, creating noise in the Actions tab. + +## Design + +Change the role-not-configured check in both dispatch workflows from a hard failure to a silent skip. + +### What changes + +In the "Check role is enabled" step of both files: + +- **Before:** `::error::` annotation + `exit 1` +- **After:** `::notice::` annotation + clear the `stage` output + `exit 0` + +Clearing the `stage` output (`echo "stage=" >> "${GITHUB_OUTPUT}"`) ensures downstream jobs gated on `needs.route.outputs.stage == ''` are skipped cleanly. + +### Files to modify + +1. `internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml` (per-org mode) — lines 265-266 +2. `.github/workflows/reusable-dispatch.yml` (per-repo mode) — lines 250-251 + +### What stays the same + +- The kill switch remains `exit 1`. It is an intentional "stop everything" signal. +- The role gate still blocks dispatch for unconfigured roles. The behavior is identical — only the exit code and annotation level change. + +## Test plan + +- Update `internal/scaffold/scaffold_test.go` if it asserts on the error message text. +- Verify the new notice message text appears in the workflow output. +- Scenarios: + 1. Stage dispatched when its role is NOT in `defaults.roles` — workflow exits 0, notice annotation logged, no error. + 2. Stage dispatched when its role IS in `defaults.roles` — workflow proceeds normally. + 3. Empty `defaults.roles` — all stages skip silently. From 29698a4a0c234dcd97617119999ea82fbae0ac26 Mon Sep 17 00:00:00 2001 From: Ralph Bean Date: Fri, 15 May 2026 15:33:33 -0400 Subject: [PATCH 2/5] docs: add implementation plan for silent skip of unconfigured roles Three tasks: fix per-org dispatch, fix per-repo dispatch, verify tests. Assisted-by: Claude Opus 4.6 Signed-off-by: Ralph Bean --- ...26-05-15-silent-skip-unconfigured-roles.md | 234 ++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-15-silent-skip-unconfigured-roles.md diff --git a/docs/superpowers/plans/2026-05-15-silent-skip-unconfigured-roles.md b/docs/superpowers/plans/2026-05-15-silent-skip-unconfigured-roles.md new file mode 100644 index 0000000000..fc3c869371 --- /dev/null +++ b/docs/superpowers/plans/2026-05-15-silent-skip-unconfigured-roles.md @@ -0,0 +1,234 @@ +# Silent Skip Unconfigured Roles — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Change the `defaults.roles` gate in both dispatch workflows to silently skip (exit 0 + notice) instead of hard-fail (exit 1 + error) when a stage's role is not configured. + +**Architecture:** Give the role-check step an `id` and an output flag (`skipped=true`) when the role is missing. Downstream steps (fan-out, fork-PR check) add a condition checking that flag. This avoids restructuring the workflow while preventing dispatch of unconfigured stages. + +**Tech Stack:** GitHub Actions workflow YAML, Go scaffold tests + +--- + +### Task 1: Fix per-org dispatch (`dispatch.yml` template) + +**Files:** +- Modify: `internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml:251-267` (role-check step) +- Modify: `internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml:269-270` (fork-PR step `if`) +- Modify: `internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml:288-289` (fan-out step `if`) + +- [ ] **Step 1: Add `id` to role-check step and change exit behavior** + +In `internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml`, change lines 251-267 from: + +```yaml + - name: Check role is enabled + if: steps.route.outputs.stage != '' + env: + STAGE: ${{ steps.route.outputs.stage }} + run: | + set -euo pipefail + STAGE_ROLE="$STAGE" + case "$STAGE" in + code) STAGE_ROLE="coder" ;; + retro|prioritize) STAGE_ROLE="fullsend" ;; + esac + + ROLES=$(yq '.defaults.roles[]' config.yaml 2>/dev/null || echo "") + if [[ -n "$ROLES" ]] && ! echo "$ROLES" | grep -Fqx "$STAGE_ROLE"; then + echo "::error::Stage '$STAGE' (role: $STAGE_ROLE) is not in defaults.roles — dispatch blocked" + exit 1 + fi +``` + +To: + +```yaml + - name: Check role is enabled + id: role-check + if: steps.route.outputs.stage != '' + env: + STAGE: ${{ steps.route.outputs.stage }} + run: | + set -euo pipefail + STAGE_ROLE="$STAGE" + case "$STAGE" in + code) STAGE_ROLE="coder" ;; + retro|prioritize) STAGE_ROLE="fullsend" ;; + esac + + ROLES=$(yq '.defaults.roles[]' config.yaml 2>/dev/null || echo "") + if [[ -n "$ROLES" ]] && ! echo "$ROLES" | grep -Fqx "$STAGE_ROLE"; then + echo "::notice::Stage '$STAGE' skipped — role '$STAGE_ROLE' not in defaults.roles" + echo "skipped=true" >> "${GITHUB_OUTPUT}" + exit 0 + fi +``` + +- [ ] **Step 2: Add role-check guard to fork-PR and fan-out steps** + +On line 270, change: +```yaml + if: steps.route.outputs.stage == 'fix' && github.event.issue.pull_request +``` +To: +```yaml + if: steps.route.outputs.stage == 'fix' && steps.role-check.outputs.skipped != 'true' && github.event.issue.pull_request +``` + +On line 289, change: +```yaml + if: steps.route.outputs.stage != '' +``` +To: +```yaml + if: steps.route.outputs.stage != '' && steps.role-check.outputs.skipped != 'true' +``` + +- [ ] **Step 3: Run `make lint` to validate YAML** + +Run: `make lint` +Expected: PASS + +- [ ] **Step 4: Commit** + +```bash +git add internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml +git commit -S -s -m "fix: silent skip when role not in defaults.roles (per-org dispatch) + +Change the role-check gate to exit 0 with a notice annotation instead +of exit 1 with an error when a stage's role is not configured. This +prevents noisy failed workflow runs on orgs that haven't opted into +all agents. + +Fixes #973 + +Assisted-by: Claude Opus 4.6 " +``` + +--- + +### Task 2: Fix per-repo dispatch (`reusable-dispatch.yml`) + +**Files:** +- Modify: `.github/workflows/reusable-dispatch.yml:234-252` (role-check step) +- Modify: `.github/workflows/reusable-dispatch.yml:51-52` (job outputs) + +The per-repo dispatch uses separate jobs. The role-check step is in the `route` job, and downstream jobs gate on `needs.route.outputs.stage`. The job output on line 52 is `stage: ${{ steps.route.outputs.stage }}`. We need to override this when the role is skipped. + +- [ ] **Step 1: Add `id` to role-check step and change exit behavior** + +In `.github/workflows/reusable-dispatch.yml`, change lines 234-252 from: + +```yaml + - name: Check role is enabled + if: steps.route.outputs.stage != '' + env: + STAGE: ${{ steps.route.outputs.stage }} + run: | + set -euo pipefail + if [[ ! -f .fullsend/config.yaml ]]; then + exit 0 + fi + STAGE_ROLE="$STAGE" + case "$STAGE" in + code) STAGE_ROLE="coder" ;; + retro|prioritize) STAGE_ROLE="fullsend" ;; + esac + ROLES=$(yq '.roles[]' .fullsend/config.yaml 2>/dev/null || echo "") + if [[ -n "$ROLES" ]] && ! echo "$ROLES" | grep -Fqx "$STAGE_ROLE"; then + echo "::error::Stage '$STAGE' (role: $STAGE_ROLE) is not in configured roles — dispatch blocked" + exit 1 + fi +``` + +To: + +```yaml + - name: Check role is enabled + id: role-check + if: steps.route.outputs.stage != '' + env: + STAGE: ${{ steps.route.outputs.stage }} + run: | + set -euo pipefail + if [[ ! -f .fullsend/config.yaml ]]; then + exit 0 + fi + STAGE_ROLE="$STAGE" + case "$STAGE" in + code) STAGE_ROLE="coder" ;; + retro|prioritize) STAGE_ROLE="fullsend" ;; + esac + ROLES=$(yq '.roles[]' .fullsend/config.yaml 2>/dev/null || echo "") + if [[ -n "$ROLES" ]] && ! echo "$ROLES" | grep -Fqx "$STAGE_ROLE"; then + echo "::notice::Stage '$STAGE' skipped — role '$STAGE_ROLE' not in configured roles" + echo "skipped=true" >> "${GITHUB_OUTPUT}" + exit 0 + fi +``` + +- [ ] **Step 2: Gate the job `stage` output on role-check** + +On line 52, change: +```yaml + stage: ${{ steps.route.outputs.stage }} +``` +To: +```yaml + stage: ${{ steps.role-check.outputs.skipped == 'true' && '' || steps.route.outputs.stage }} +``` + +This clears the stage output when the role check skips, so all downstream jobs (`triage`, `code`, `review`, `fix`, `retro`) with `if: needs.route.outputs.stage == ''` are skipped. + +- [ ] **Step 3: Run `make lint` to validate YAML** + +Run: `make lint` +Expected: PASS + +- [ ] **Step 4: Commit** + +```bash +git add .github/workflows/reusable-dispatch.yml +git commit -S -s -m "fix: silent skip when role not in configured roles (per-repo dispatch) + +Same change as per-org dispatch: exit 0 with notice instead of exit 1 +with error when a stage's role is not configured. + +Fixes #973 + +Assisted-by: Claude Opus 4.6 " +``` + +--- + +### Task 3: Update scaffold test assertions + +**Files:** +- Modify: `internal/scaffold/scaffold_test.go:219-221` + +- [ ] **Step 1: Check if any test assertions reference the old error message** + +Run: `grep -n 'dispatch blocked\|not in defaults.roles\|not in configured roles' internal/scaffold/scaffold_test.go` + +If no assertions match the old error text, no changes are needed. The existing assertion on line 221 (`assert.Contains(t, s, "defaults.roles")`) still passes because the string `defaults.roles` is still present in the notice message. + +- [ ] **Step 2: Run tests** + +Run: `make go-test` +Expected: PASS + +- [ ] **Step 3: Run vet** + +Run: `make go-vet` +Expected: PASS + +- [ ] **Step 4: Commit (only if changes were needed)** + +If test assertions were updated: +```bash +git add internal/scaffold/scaffold_test.go +git commit -S -s -m "test: update scaffold assertions for notice-level role skip + +Assisted-by: Claude Opus 4.6 " +``` From 04bfcc355c9123ba838b72183227e80fe09e39dd Mon Sep 17 00:00:00 2001 From: Ralph Bean Date: Fri, 15 May 2026 15:36:06 -0400 Subject: [PATCH 3/5] fix: silent skip when role not in configured roles (per-repo dispatch) Same change as per-org dispatch: exit 0 with notice instead of exit 1 with error when a stage's role is not configured. Fixes #973 Assisted-by: Claude Opus 4.6 Signed-off-by: Ralph Bean --- .github/workflows/reusable-dispatch.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/reusable-dispatch.yml b/.github/workflows/reusable-dispatch.yml index 8026110e14..0e35f177b4 100644 --- a/.github/workflows/reusable-dispatch.yml +++ b/.github/workflows/reusable-dispatch.yml @@ -49,7 +49,7 @@ jobs: contents: read pull-requests: read outputs: - stage: ${{ steps.route.outputs.stage }} + stage: ${{ steps.role-check.outputs.skipped == 'true' && '' || steps.route.outputs.stage }} trigger_source: ${{ steps.route.outputs.trigger_source }} event_payload: ${{ steps.payload.outputs.event_payload }} steps: @@ -232,6 +232,7 @@ jobs: fi - name: Check role is enabled + id: role-check if: steps.route.outputs.stage != '' env: STAGE: ${{ steps.route.outputs.stage }} @@ -247,8 +248,9 @@ jobs: esac ROLES=$(yq '.roles[]' .fullsend/config.yaml 2>/dev/null || echo "") if [[ -n "$ROLES" ]] && ! echo "$ROLES" | grep -Fqx "$STAGE_ROLE"; then - echo "::error::Stage '$STAGE' (role: $STAGE_ROLE) is not in configured roles — dispatch blocked" - exit 1 + echo "::notice::Stage '$STAGE' skipped — role '$STAGE_ROLE' not in configured roles" + echo "skipped=true" >> "${GITHUB_OUTPUT}" + exit 0 fi - name: Block fork PRs for fix stage From 92335d47c22e663918a62e56cc191842fa9c01be Mon Sep 17 00:00:00 2001 From: Ralph Bean Date: Fri, 15 May 2026 15:36:07 -0400 Subject: [PATCH 4/5] fix: silent skip when role not in defaults.roles (per-org dispatch) Change the role-check gate to exit 0 with a notice annotation instead of exit 1 with an error when a stage's role is not configured. This prevents noisy failed workflow runs on orgs that haven't opted into all agents. Fixes #973 Assisted-by: Claude Opus 4.6 Signed-off-by: Ralph Bean --- .../fullsend-repo/.github/workflows/dispatch.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml b/internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml index 7e72a70945..1fce710763 100644 --- a/internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml +++ b/internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml @@ -249,6 +249,7 @@ jobs: fi - name: Check role is enabled + id: role-check if: steps.route.outputs.stage != '' env: STAGE: ${{ steps.route.outputs.stage }} @@ -262,12 +263,13 @@ jobs: ROLES=$(yq '.defaults.roles[]' config.yaml 2>/dev/null || echo "") if [[ -n "$ROLES" ]] && ! echo "$ROLES" | grep -Fqx "$STAGE_ROLE"; then - echo "::error::Stage '$STAGE' (role: $STAGE_ROLE) is not in defaults.roles — dispatch blocked" - exit 1 + echo "::notice::Stage '$STAGE' skipped — role '$STAGE_ROLE' not in defaults.roles" + echo "skipped=true" >> "${GITHUB_OUTPUT}" + exit 0 fi - name: Block fork PRs for fix stage - if: steps.route.outputs.stage == 'fix' && github.event.issue.pull_request + if: steps.route.outputs.stage == 'fix' && steps.role-check.outputs.skipped != 'true' && github.event.issue.pull_request env: GH_TOKEN: ${{ steps.oidc-mint.outputs.token }} SOURCE_REPO: ${{ github.repository }} @@ -286,7 +288,7 @@ jobs: fi - name: Find and trigger agent workflows for stage - if: steps.route.outputs.stage != '' + if: steps.route.outputs.stage != '' && steps.role-check.outputs.skipped != 'true' env: GH_TOKEN: ${{ steps.oidc-mint.outputs.token }} STAGE: ${{ steps.route.outputs.stage }} From dd6e9673d6650b86087852ce05b826ce09633aff Mon Sep 17 00:00:00 2001 From: Ralph Bean Date: Fri, 15 May 2026 19:49:44 -0400 Subject: [PATCH 5/5] fix(dispatch): guard fork-PR step on role-check skip in reusable workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-org dispatch.yml correctly gates the "Block fork PRs" step on steps.role-check.outputs.skipped != 'true', but the reusable workflow was missing this guard. Without it, an unconfigured fix role on a fork PR still hits exit 1 — the exact noisy failure this branch eliminates. Assisted-by: Claude Opus 4.6 Signed-off-by: Ralph Bean --- .github/workflows/reusable-dispatch.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/reusable-dispatch.yml b/.github/workflows/reusable-dispatch.yml index 0e35f177b4..d0fb664181 100644 --- a/.github/workflows/reusable-dispatch.yml +++ b/.github/workflows/reusable-dispatch.yml @@ -254,7 +254,7 @@ jobs: fi - name: Block fork PRs for fix stage - if: steps.route.outputs.stage == 'fix' && github.event.issue.pull_request + if: steps.route.outputs.stage == 'fix' && steps.role-check.outputs.skipped != 'true' && github.event.issue.pull_request env: GH_TOKEN: ${{ github.token }} SOURCE_REPO: ${{ github.repository }}