Skip to content

feat(#6347): add pre-computed matrix input to reusable-dispatch - #6455

Merged
ralphbean merged 8 commits into
mainfrom
reusable-dispatch-precomputed-matrix
Aug 24, 2026
Merged

feat(#6347): add pre-computed matrix input to reusable-dispatch#6455
ralphbean merged 8 commits into
mainfrom
reusable-dispatch-precomputed-matrix

Conversation

@ralphbean

Copy link
Copy Markdown
Member

Summary

Adds an optional matrix input to reusable-dispatch.yml that allows custom pollers in external repos to invoke harness agents directly without going through the routing and dispatch steps.

This approach solves #6347 while maintaining ADR 62's inlining decision - no version skew is introduced because we're not extracting workflows.

Changes

  1. New input: matrix

    • Optional pre-computed harness matrix (JSON string)
    • When provided, skips route and harness-dispatch jobs
    • Format matches fullsend dispatch --output-driver gha-matrix
  2. Modified job flow:

    • route job skips when matrix is provided
    • harness-dispatch only runs when stage == 'harness' and no matrix provided
    • harness-run uses either the computed or provided matrix
    • Made event_action optional when matrix is provided
  3. Documentation:

    • Added docs/guides/user/custom-poller-example.md with complete example
    • Updated workflow header comments to explain the new flow

How It Works

Normal flow (unchanged):

shim → reusable-dispatch → route → harness-dispatch → harness-run

Custom poller flow (new):

custom-poller → reusable-dispatch (with matrix) → harness-run

Example Usage

jobs:
  poll:
    runs-on: ubuntu-24.04
    outputs:
      matrix: ${{ steps.dispatch.outputs.matrix }}
    steps:
      - name: Poll Jira
        id: dispatch
        run: |
          MATRIX=$(fullsend poll jira --output-driver gha-matrix)
          echo "matrix=$MATRIX" >> $GITHUB_OUTPUT

  harness:
    needs: poll
    uses: fullsend-ai/fullsend/.github/workflows/reusable-dispatch.yml@v0
    with:
      matrix: ${{ needs.poll.outputs.matrix }}
      mint_url: ${{ vars.FULLSEND_MINT_URL }}
      gcp_region: ${{ vars.FULLSEND_GCP_REGION }}
    secrets:
      FULLSEND_GCP_WIF_PROVIDER: ${{ secrets.FULLSEND_GCP_WIF_PROVIDER }}
      FULLSEND_GCP_PROJECT_ID: ${{ secrets.FULLSEND_GCP_PROJECT_ID }}

ADR 62 Compliance

This does not conflict with ADR 62:

  • ✅ All stage logic remains inlined in reusable-dispatch.yml
  • ✅ No new @v0 references between workflows
  • ✅ No version skew introduced
  • ✅ The decision to inline stages is preserved

The new matrix input is an alternative entry point for external callers, not a reversal of the inlining decision.

Test plan

  • Verify existing dispatch flow still works (standard GitHub event triggers)
  • Test with custom poller providing pre-computed matrix (external repo like jira-triage-test)
  • Confirm route and harness-dispatch properly skip when matrix provided
  • Verify harness-run executes correctly with pre-computed matrix

Fixes #6347

🤖 Generated with Claude Code

Enable custom pollers in external repos to invoke harness agents directly by
providing a pre-computed matrix, bypassing the routing and dispatch steps.

Changes:
- Add optional `matrix` input to reusable-dispatch.yml
- Skip route and harness-dispatch jobs when matrix is provided
- Update harness-run to use either computed or provided matrix
- Make event_action optional when matrix is provided
- Add documentation and example workflow for custom pollers

This approach maintains ADR 62's inlining decision while enabling workflow
reuse for custom polling use cases.

Assisted-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>
@ralphbean
ralphbean requested a review from a team as a code owner August 21, 2026 16:59
@ralphbean ralphbean added the fullsend-fix Enables automatic bot-triggered fix runs on human-authored PRs label Aug 21, 2026
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add optional pre-computed matrix input to reusable-dispatch harness flow

✨ Enhancement 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add optional matrix workflow_call input to bypass route/dispatch and run harness directly.
• Make event_action optional when using a caller-provided matrix.
• Document a complete external custom-poller workflow example and required configuration.
Diagram

graph TD
  CP(["Custom poller repo"]) --> RD["reusable-dispatch.yml"] --> HR["Job: harness-run"]
  SH(["Repo shim workflow"]) --> RD --> RT["Job: route"] --> HD["Job: harness-dispatch"] --> HR
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Create a separate harness-only reusable workflow
  • ➕ Avoids conditional needs/job-skipping complexity in the central reusable-dispatch workflow
  • ➕ Clearer contract for external pollers (only harness concerns exposed)
  • ➖ Conflicts with ADR 62’s intent to keep stage logic inlined to prevent version skew
  • ➖ Adds another workflow entrypoint to maintain and document
2. Keep `route` always-running but make it a no-op when matrix is provided
  • ➕ Preserves a single dependency chain (downstream jobs can safely needs: route)
  • ➕ Reduces risk from GitHub Actions semantics around skipped dependencies and matrix evaluation
  • ➖ Adds a small amount of “plumbing” work to ensure route emits safe defaults/outputs
  • ➖ May slightly blur responsibility boundaries (route job present even when unused)
3. Introduce a lightweight “prepare-matrix” job inside reusable-dispatch
  • ➕ Moves matrix validation/normalization into the reusable workflow (consistent shape + early failure)
  • ➕ Lets downstream jobs depend on a single canonical output (computed-or-provided)
  • ➖ More YAML and expression complexity
  • ➖ Still requires careful handling to avoid evaluating fromJSON on empty inputs

Recommendation: The PR’s approach (optional matrix input that bypasses routing/dispatch) is the right direction for enabling external custom pollers while honoring ADR 62. The main thing to validate is GitHub Actions behavior around skipped needs and when matrix expressions are evaluated; if any issues arise in testing, the simplest hardening is to keep route always-running as a no-op when matrix is provided, so downstream job dependencies and outputs remain stable.

Files changed (2) +149 / -5

Enhancement (1) +27 / -5
reusable-dispatch.ymlSupport caller-provided harness matrix and skip routing/dispatch +27/-5

Support caller-provided harness matrix and skip routing/dispatch

• Adds a new optional 'matrix' input (JSON string) for workflow_call callers to provide a pre-computed harness matrix. Updates job gating so 'route' and 'harness-dispatch' are skipped when a matrix is provided, and updates 'harness-run' to select between provided vs computed matrices. Makes 'event_action' optional (default empty) to support non-GitHub-event callers.

.github/workflows/reusable-dispatch.yml

Documentation (1) +122 / -0
custom-poller-example.mdDocument external custom poller workflow using pre-computed matrix +122/-0

Document external custom poller workflow using pre-computed matrix

• Introduces a user guide showing how an external repo can poll Jira (or similar systems), emit a GHA matrix string, and invoke 'reusable-dispatch.yml' with that matrix. Documents the expected matrix schema, required vars/secrets, and notes the authorization boundary when bypassing in-repo agent enablement checks.

docs/guides/user/custom-poller-example.md

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 21, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 5:01 PM UTC · Completed 5:19 PM UTC

Commit: 7905677 · View workflow run →

@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown

Site preview

Preview: https://3f930288-site.fullsend-ai.workers.dev

Commit: abef3cf88cc76dfefd0685affe25d47316ba45d2

@qodo-code-review

qodo-code-review Bot commented Aug 21, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Action required

1. Guides index missing new entry ✓ Resolved 📜 Skill insight ⚙ Maintainability
Description
A new user guide was added under docs/guides/user/ but docs/guides/README.md was not updated to
include it. This makes the new guide undiscoverable from the guides index.
Code

docs/guides/user/custom-poller-example.md[R1-4]

+# Custom Poller Example
+
+This guide shows how to create a custom poller in your own repository that invokes fullsend harness agents directly, bypassing the standard GitHub event trigger flow.
+
Relevance

●●● Strong

Recent documentation precedent explicitly accepted updating the guides index for every new user
guide.

PR-#5778

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR adds docs/guides/user/custom-poller-example.md, but the docs/guides/README.md user guides
list does not include an entry linking to it, violating the index-update requirement.

docs/guides/user/custom-poller-example.md[1-5]
docs/guides/README.md[32-48]
Skill: writing-user-docs

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A new guide was added under `docs/guides/user/` but the guides index (`docs/guides/README.md`) was not updated.

## Issue Context
Compliance requires `docs/guides/README.md` to be updated whenever a new guide is added so readers can find it.

## Fix Focus Areas
- docs/guides/README.md[32-48]
- docs/guides/user/custom-poller-example.md[1-5]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Silent skip without event_action ✓ Resolved 🐞 Bug ≡ Correctness
Description
event_action is now optional with a default empty string, but the route job still routes
entirely based on EVENT_ACTION; when omitted (and no matrix provided), routing falls through and
the workflow silently skips dispatch. This is a regression from compile-time validation to a runtime
no-op that can break callers or new shims without a clear error.
Code

.github/workflows/reusable-dispatch.yml[R33-36]

+        description: "The event action (github.event.action) forwarded by the shim. Not required when matrix is provided."
+        required: false
        type: string
+        default: ""
Relevance

●●● Strong

Recent workflow precedent accepted preventing silent zero-dispatch success, supporting explicit
failure or feedback here.

PR-#390

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR made event_action optional/default-empty, but the routing script still branches on
EVENT_ACTION values and explicitly skips when no stage matches. With an empty action, no branch
matches and the workflow exits successfully with stage= (silent no-op).

.github/workflows/reusable-dispatch.yml[29-37]
.github/workflows/reusable-dispatch.yml[117-123]
.github/workflows/reusable-dispatch.yml[261-333]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`event_action` was changed to `required: false` with `default: ""`, but `jobs.route` still expects a real GitHub event action (opened/labeled/etc.). If a caller forgets to pass it (and does not pass `matrix`), the route logic falls through and emits `stage=` (skipping the entire workflow) instead of failing with an actionable error.

### Issue Context
The PR’s intent is: `event_action` is not needed only when `inputs.matrix` is provided. For all other calls, `event_action` is required for correct routing.

### Fix Focus Areas
- .github/workflows/reusable-dispatch.yml[32-36]
- .github/workflows/reusable-dispatch.yml[91-122]

### What to change
- Add an early guard step in the `route` job (which only runs when `inputs.matrix == ''`) to fail fast if `inputs.event_action` is empty.
 - Example: a first step in `route.steps`:
   - `if: ${{ inputs.event_action == '' }}`
   - `run: echo "::error::event_action is required when matrix is not provided"; exit 1`
- Keep `event_action` optional at the workflow_call interface to support the matrix path, but enforce it at runtime for the non-matrix path.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Guide missing Prerequisites section ✓ Resolved 📜 Skill insight ✧ Quality
Description
docs/guides/user/custom-poller-example.md contains procedural steps but does not include a clearly
labeled Prerequisites section before those steps. This violates the guide structure requirement
and makes it unclear what must be set up before following the procedure.
Code

docs/guides/user/custom-poller-example.md[R96-99]

+## Required Configuration
+
+Your external repository needs these variables and secrets configured:
+
Relevance

●●● Strong

Recent guide review accepted explicit ordered procedural structure; a clearly labeled prerequisites
section is a similarly concrete structure requirement.

PR-#5778

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The guide introduces required setup under ## Required Configuration and then presents a numbered
procedure under ## How It Works, but there is no explicitly labeled ## Prerequisites section
before step 1 as required.

docs/guides/user/custom-poller-example.md[96-118]
Skill: writing-user-docs

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new guide has procedural steps, but it does not contain a clearly labeled `## Prerequisites` section before the procedure.

## Issue Context
The guide currently uses `## Required Configuration`, but the rule requires a clearly labeled prerequisites section.

## Fix Focus Areas
- docs/guides/user/custom-poller-example.md[96-118]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Guide restates architecture inline ✓ Resolved 📜 Skill insight ⚙ Maintainability
Description
The new guide explains dispatch flow and authorization boundaries without linking to architectural
references (e.g., docs/architecture.md or ADRs). This increases drift risk as behavior evolves.
Code

docs/guides/user/custom-poller-example.md[R3-4]

+This guide shows how to create a custom poller in your own repository that invokes fullsend harness agents directly, bypassing the standard GitHub event trigger flow.
+
Relevance

●●● Strong

Recent documentation precedent accepted replacing or restoring architecture/ADR references for
security-relevant architectural claims.

PR-#6329

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The guide describes workflow architecture and security boundaries (bypassing the standard event flow
and config checks) but provides no links to architectural references, contrary to the rule.

docs/guides/user/custom-poller-example.md[3-4]
docs/guides/user/custom-poller-example.md[113-122]
Skill: writing-user-docs

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The guide includes architectural explanations (dispatch flow, bypassed checks, authorization boundary) but does not link to canonical architecture references.

## Issue Context
Guides should link to ADRs / `docs/architecture.md` instead of restating architecture inline to avoid doc drift.

## Fix Focus Areas
- docs/guides/user/custom-poller-example.md[3-4]
- docs/guides/user/custom-poller-example.md[113-122]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Jargon missing glossary links ✓ Resolved 📜 Skill insight ✧ Quality
Description
The new guide introduces domain terms (e.g., harness, matrix, WIF, OTEL/OTLP) without
linking to the glossary or defining them on first use. This reduces clarity for new users and
violates the jargon-definition requirement.
Code

docs/guides/user/custom-poller-example.md[R96-104]

+## Required Configuration
+
+Your external repository needs these variables and secrets configured:
+
+**Variables:**
+- `FULLSEND_MINT_URL` - Token mint service URL
+- `FULLSEND_GCP_REGION` - GCP region for Vertex AI
+- `OTEL_EXPORTER_OTLP_ENDPOINT` - OpenTelemetry endpoint (optional)
+- `JIRA_BASE_URL` - Jira instance URL (if using Jira agents)
Relevance

●●● Strong

Recent documentation precedent accepted defining specialized domain terms on first use and improving
glossary clarity.

PR-#5778
PR-#5532

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The guide uses specialized terms in its overview and configuration sections without any glossary
links or inline definitions, even though the repository maintains a glossary intended for newly
introduced terminology.

docs/guides/user/custom-poller-example.md[3-10]
docs/guides/user/custom-poller-example.md[96-111]
docs/glossary.md[1-6]
Skill: writing-user-docs

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The guide uses domain-specific terms (e.g., harness, matrix, WIF, OTEL/OTLP) without linking to `docs/glossary.md` or defining them inline on first use.

## Issue Context
A glossary exists and the guide should either link to it for these terms or provide a brief parenthetical definition.

## Fix Focus Areas
- docs/guides/user/custom-poller-example.md[3-10]
- docs/guides/user/custom-poller-example.md[77-90]
- docs/guides/user/custom-poller-example.md[96-111]
- docs/glossary.md[1-6]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Concurrency keys wrong repo ✓ Resolved 🐞 Bug ☼ Reliability
Description
In the new custom-poller flow, harness-run’s concurrency group uses ${{ github.repository }}
(the poller repo), not matrix.source_repo/matrix.status_repo, so runs for different target repos
can incorrectly cancel each other if agent and status_number collide. This was benign in
per-repo installs but is activated by allowing external repos to call with a precomputed matrix.
Code

.github/workflows/reusable-dispatch.yml[R1528-1534]

+    needs: [route, harness-dispatch]
+    if: |
+      ${{
+        !cancelled() && (
+          (inputs.matrix != '' && fromJSON(inputs.matrix).include[0] != null) ||
+          (needs.route.outputs.stage == 'harness' && needs.harness-dispatch.outputs.matrix != '' && fromJSON(needs.harness-dispatch.outputs.matrix).include[0] != null)
+        )
Relevance

●● Moderate

Concurrency-key concerns are subjective; historical normalization requests were rejected, but
external-target collisions add new context.

PR-#2465

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new flow explicitly supports calling from an external repository while targeting org/repo in
the matrix; meanwhile harness-run concurrency still keys on github.repository (caller repo),
which can create cross-target collisions and cancellations.

.github/workflows/reusable-dispatch.yml[1526-1541]
docs/guides/user/custom-poller-example.md[55-75]
docs/guides/user/custom-poller-example.md[81-92]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The new matrix-input path allows external (poller) repos to call `reusable-dispatch.yml` for multiple target repos. However, `harness-run`’s concurrency group is keyed by `${{ github.repository }}` (the caller/poller repo), which can cause unrelated harness runs for different `matrix.status_repo`/`matrix.source_repo` to share a concurrency group and cancel each other.

### Issue Context
In the traditional per-repo flow, `github.repository` generally matches the repo being processed, so this key was effectively unique. With the new external-caller design, `github.repository` becomes the poller repo, while the matrix contains the actual target/status repos.

### Fix Focus Areas
- .github/workflows/reusable-dispatch.yml[1526-1541]
- docs/guides/user/custom-poller-example.md[55-75]
- docs/guides/user/custom-poller-example.md[81-92]

### What to change
- Update `harness-run.concurrency.group` to include `matrix.status_repo` (or `matrix.source_repo`) instead of `github.repository`.
 - Example: `fullsend-harness-${{ matrix.agent }}-${{ matrix.status_repo }}-${{ matrix.status_number }}`
- Optionally include both `status_repo` and `source_repo` if they can diverge in practice.
- Keep the existing fields (agent/status_number) to preserve current behavior within a single repo.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

7. Scaffold dispatch missing matrix 📘 Rule violation ⚙ Maintainability
Description
.github/workflows/reusable-dispatch.yml adds a new matrix workflow input and changes the
event_action requirement, but the scaffold
internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml is not updated to match. This
breaks the required sync between dispatch workflows and risks drift across installation modes.
Code

.github/workflows/reusable-dispatch.yml[R70-74]

+      matrix:
+        description: "Pre-computed harness matrix (skips harness-dispatch if provided). Enables custom pollers to invoke harness agents directly. Format matches fullsend dispatch --output-driver gha-matrix."
+        required: false
+        type: string
+        default: ""
Relevance

● Weak

Close precedent rejected requiring scaffold parity when reusable workflow intentionally diverged;
this sync request follows that pattern.

PR-#3820

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The compliance rule requires dispatch.yml and reusable-dispatch.yml to remain in sync for
payload/routing/input threading. The PR adds matrix (and makes event_action non-required) in
reusable-dispatch.yml, but the scaffold dispatch.yml still requires event_action and does not
define matrix, demonstrating drift.

Rule 1062045: Keep jq payload, stage routing, and secret threading logic in dispatch workflows in sync
.github/workflows/reusable-dispatch.yml[32-37]
.github/workflows/reusable-dispatch.yml[70-74]
internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml[9-16]
docs/contributing/workflow-contracts.md[3-14]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`reusable-dispatch.yml` now defines a `matrix` input and relaxes `event_action`, but the scaffold `dispatch.yml` still requires `event_action` and has no `matrix` input. This violates the sync requirement between the two dispatch workflows.

## Issue Context
The repo documents that the scaffold dispatch workflow and `reusable-dispatch.yml` must remain aligned when changing routing/input contracts.

## Fix Focus Areas
- .github/workflows/reusable-dispatch.yml[32-37]
- .github/workflows/reusable-dispatch.yml[70-74]
- internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml[9-16]
- docs/contributing/workflow-contracts.md[3-14]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 58 rules

Grey Divider

Tip of the day
💡 Did you know, you can switch off images and animations for a plain-text comment

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread docs/guides/user/custom-poller-example.md
Comment thread docs/guides/user/custom-poller-example.md Outdated
Comment thread docs/guides/user/custom-poller-example.md Outdated
Comment thread docs/guides/user/custom-poller-example.md Outdated
Comment thread .github/workflows/reusable-dispatch.yml
Comment thread .github/workflows/reusable-dispatch.yml Outdated
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 21, 2026

Copy link
Copy Markdown

Review

Findings

High

  • [stale-doc] docs/.vitepress/config.ts — New file docs/guides/user/custom-poller-example.md has no VitePress sidebar entry. The User Guides section uses manual { text, link } entries (not getMarkdownFiles() auto-discovery), so the page is built but unreachable from sidebar navigation. Per AGENTS.md, all manually-configured sections require an explicit sidebar entry.
    Remediation: Add { text: "Custom Poller Example", link: "/guides/user/custom-poller-example" } to the User Guides items array in docs/.vitepress/config.ts, near the Jira Integration entry.

Medium

  • [protected-path] .github/workflows/reusable-dispatch.yml — This PR modifies a file under the .github/ protected path. The PR links to issue Extract harness-run from reusable-dispatch.yml into a reusable action #6347 and the description explains the rationale. Human approval is always required for protected-path changes regardless of context.

  • [edge-case] .github/workflows/reusable-dispatch.yml — When a caller provides the matrix input, fromJSON(inputs.matrix) is called in both the if condition and strategy.matrix expression without prior validation. Syntactically invalid JSON produces an opaque GHA expression evaluation error with no actionable message. No validation step exists for the matrix input's structure or required fields.
    Remediation: Add a validation job or step that runs when inputs.matrix != '' to validate the JSON parses correctly and contains the expected include array with required fields (agent, role, source_repo, event_payload, status_repo, status_number).

  • [authorization-bypass] .github/workflows/reusable-dispatch.yml — The matrix input bypasses the route and harness-dispatch jobs (both gated by if: inputs.matrix == ''), which perform authorization checks (collaborator permissions, role gating, agent eligibility). The mint service provides a secondary authorization layer that validates role/repo combinations, but the workflow itself no longer enforces authorization on the pre-computed matrix path. The trust boundary is the workflow_call caller (which must have access to the repo's secrets), but this trust model is not documented.
    Remediation: Document the trust model explicitly in the workflow comments: callers providing a pre-computed matrix are responsible for performing authorization upstream.

  • [scope-exceeded] .github/workflows/reusable-dispatch.yml — Issue Extract harness-run from reusable-dispatch.yml into a reusable action #6347 proposes extracting harness-run into a composite action (.github/actions/harness-run/). This PR takes a different approach (matrix input bypass). While both solve the underlying problem (enabling external callers to invoke harness-run), using Fixes #6347 will auto-close the issue without implementing its stated solution.
    Remediation: Use Relates to #6347 instead of Fixes #6347, or update the issue with a comment explaining why the matrix-bypass approach was chosen.

  • [stale-doc] docs/guides/user/jira-integration.md:255 — The "Dry-run tip" section references the removed "Dispatch agent workflows" step and gh workflow run, which no longer exist after this PR replaces the per-dispatch loop with a matrix-based approach.
    Remediation: Rewrite the Dry-run tip to reference the new flow (e.g., "run the poll step locally and inspect dispatches.json — the harness job that consumes the matrix will not run locally").

  • [stale-doc] docs/guides/user/jira-integration.md:263 — The "Dispatch record format" section describes records as "execution refs compatible with the workflow_call shim" dispatched individually via gh workflow run. They are now consumed as matrix entries via the matrix input to reusable-dispatch.yml.
    Remediation: Update the description to clarify records are matrix entries matching the fullsend dispatch --output-driver gha-matrix format.

  • [misleading-label] — PR title uses feat(#6347): but the change modifies CI infrastructure (.github/workflows/). COMMITS.md lists feat(ci) as a forbidden combination because CI changes are not user-visible features. The fullsend-fix label also conflicts with the feat prefix.
    Remediation: Change the PR title to ci(#6347): or refactor(#6347): and reconcile the label.

  • [scope-creep] .github/workflows/reusable-dispatch.yml — Jira-specific inputs and secrets (jira_base_url, JIRA_TOKEN, JIRA_USER_EMAIL) are added directly to the generic reusable-dispatch.yml workflow_call interface. Each new poll source (Linear, ServiceNow, etc.) would require its own top-level secrets, causing interface sprawl.
    Remediation: Consider whether source-specific credentials should be passed through the matrix entries or a different mechanism rather than top-level workflow_call secrets.

Low

  • [edge-case] .github/workflows/reusable-dispatch.yml!cancelled() in harness-run's if condition is more permissive than needed. !failure() would achieve the same result while gating out the failure case at the status-check level rather than relying on expression short-circuit behavior.

  • [api-contract] .github/workflows/reusable-dispatch.yml — The concurrency group now includes matrix.status_repo. If fullsend dispatch --output-driver gha-matrix does not always populate status_repo, the concurrency key changes, potentially breaking cancel-in-progress for existing dispatches. Consider a fallback: ${{ matrix.status_repo || github.repository }}.

  • [naming-convention] .github/workflows/reusable-dispatch.ymlneeds: [harness-dispatch] uses array form for a single dependency; all other jobs in the file use scalar form (needs: route).

  • [api-shape] .github/workflows/reusable-dispatch.ymlJIRA_BASE_URL uses ${{ inputs.jira_base_url || vars.JIRA_BASE_URL }} — falling back to vars. at the consumption site. This is the only input in the workflow that does this; others receive vars. values through the caller's with: block.

  • [missing-test] .github/workflows/reusable-dispatch.yml — New secrets and inputs lack test coverage in workflow_call_alignment_test.go per workflow contracts guidance for optional secret/input threading.

  • [secret-exposure] .github/workflows/reusable-dispatch.ymlJIRA_TOKEN and JIRA_USER_EMAIL are exposed in the agent sandbox environment, following the existing pattern for other secrets (OTEL headers, GCP credentials) but expanding the credential surface area.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run

Review

Findings

High

  • [authorization-bypass] .github/workflows/reusable-dispatch.yml — When inputs.matrix is provided, the route job (which enforces kill-switch, role-check, agent-check, and agent-enablement from .fullsend/config.yaml) and harness-dispatch are both skipped. harness-run executes directly from the caller-supplied matrix without any of these authorization checks. The mint service validates the calling workflow’s OIDC identity but not per-event actor permissions. An authorized calling workflow can pass arbitrary matrix entries with any role, agent, source_repo, and event_payload values, bypassing all per-event authorization.
    Remediation: Add lightweight authorization checks in harness-run that run regardless of matrix source: validate the kill switch from .fullsend/config.yaml, validate that agent/role are within the enabled set, and document the threat model explicitly.

Medium

  • [injection-via-fromJSON] .github/workflows/reusable-dispatch.ymlinputs.matrix is passed directly to fromJSON() and the parsed fields (matrix.agent, matrix.role, matrix.source_repo, etc.) flow into checkout actions, mint-token requests, env vars, and action parameters without format validation. The normal dispatch path validates stage names with regex; no equivalent validation exists for the pre-computed matrix path.
    Remediation: Add an input validation step in harness-run that validates matrix field formats before any checkout or action execution.

  • [validation-gap] .github/workflows/reusable-dispatch.yml:1391event_action changed from required: true to required: false with a validation step added to the route job. However, harness-dispatch runs independently (no needs: route) and passes event_action directly to fullsend dispatch --event-action without validation. If a caller invokes without matrix or event_action, route correctly fails but harness-dispatch proceeds with empty --event-action, and harness-run can execute via !cancelled().
    Remediation: Add a validation step to harness-dispatch that fails when inputs.event_action is empty, or add needs: route to serialize the dependency.

  • [missing-sidebar-entry] docs/.vitepress/config.ts:236 — The PR adds docs/guides/user/custom-poller-example.md but does not add a sidebar entry in config.ts. The guides/user/ section uses manual { text, link } entries (not getMarkdownFiles()), so the new page will not appear in sidebar navigation. Per AGENTS.md: “All other sections need a manual { text, link } entry.”
    Remediation: Add { text: "Custom Poller Example", link: "/guides/user/custom-poller-example" } to the guides/user/ items array.

  • [protected-path] .github/workflows/reusable-dispatch.yml — This PR modifies a file under the protected path .github/. The PR links to issue Extract harness-run from reusable-dispatch.yml into a reusable action #6347 and provides rationale for the change. Human approval is always required for protected-path changes, regardless of context.

Low

  • [concurrency-group-collision] .github/workflows/reusable-dispatch.yml:1550 — The concurrency group key includes caller-controlled matrix.status_repo and matrix.status_number with cancel-in-progress: true. A caller crafting matching values could cancel another run. Risk is bounded since callers need existing workflow access, and github.repository adds an unforgeable component.

  • [label-type-mismatch] .github/workflows/reusable-dispatch.yml — PR title uses feat(#6347) but carries the fullsend-fix label, which triggers bot-driven fix runs. This is a feature, not a fix.

  • [scope-creep] .github/workflows/reusable-dispatch.yml — Three Jira-specific inputs (jira_base_url, JIRA_TOKEN, JIRA_USER_EMAIL) are not mentioned in issue Extract harness-run from reusable-dispatch.yml into a reusable action #6347’s authorization scope. They are natural prerequisites for the custom poller use case but represent scope beyond the linked issue.

  • [naming-convention] .github/workflows/reusable-dispatch.ymljira_base_url, JIRA_TOKEN, and JIRA_USER_EMAIL use domain-specific naming that departs from the established pattern of generic names (mint_url, gcp_region) and project-scoped prefixes (FULLSEND_GCP_*, OTEL_*).

  • [concurrency-key-contract] .github/workflows/reusable-dispatch.yml:1550 — Concurrency group key adds {status_repo}, changing the key format for all callers. During rollout, in-flight runs under the old format will not be cancelled by new runs. Transient and self-resolving.

  • [job-dependency-contract] .github/workflows/reusable-dispatch.yml:1544harness-run now depends on route in addition to harness-dispatch. Minor behavioral change: route cancellation would now block harness-run.

  • [secrets-exposure] .github/workflows/reusable-dispatch.ymlJIRA_TOKEN and JIRA_USER_EMAIL are exposed to agent workloads determined by matrix.agent. When the matrix is pre-computed, the caller controls which agent accesses these secrets. Incremental risk is bounded.

  • [unnecessary-dependency] .github/workflows/reusable-dispatch.yml:1544harness-run adds route to needs but never references needs.route outputs, forcing harness-run to wait for route in the normal flow without consuming any of its outputs.

  • [description-format] .github/workflows/reusable-dispatch.yml:68matrix input description is three sentences; other inputs use one.

  • [input-classification] .github/workflows/reusable-dispatch.ymlJIRA_USER_EMAIL is not sensitive (visible in Jira profiles) but is routed as a secret. Convention routes non-sensitive config as inputs.

  • [workflow-contract-compliance] .github/workflows/reusable-dispatch.yml — Per workflow-contracts.md, new secrets/inputs should have workflow_call_alignment_test.go extended. No test updates in this PR.

  • [matrix-format-contract] .github/workflows/reusable-dispatch.yml:1548 — No schema validation on the matrix input. Malformed JSON produces runtime errors rather than clear validation failures.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (2)

Review

Findings

Critical

  • [logic-error] .github/workflows/reusable-dispatch.yml:1390 — The harness-dispatch job's new if condition requires needs.route.outputs.stage == 'harness', but the route job never sets STAGE="harness". Valid stage values are: triage, code, review, fix, retro, prioritize. On the base branch, harness-dispatch had no needs or if — it ran unconditionally and independently of route. This change breaks the existing harness dispatch path for all standard GitHub event triggers.
    Remediation: Remove the needs.route.outputs.stage == 'harness' condition. Use if: inputs.matrix == '' (bare expression, no ${{ }}) to gate on the pre-computed matrix only, or remove both needs and if to restore the base behavior and gate only the new matrix path in harness-run.

High

  • [fail-open] .github/workflows/reusable-dispatch.yml:1546 — When inputs.matrix is provided, the route job is skipped, bypassing all authorization checks: collaborator permission verification (has_repo_permission()), kill switch, role enablement, agent enablement, and the PR dedup guard. The harness-run job proceeds directly with the caller-supplied matrix via !cancelled(). The documentation acknowledges "agent-enablement checks are bypassed," but the kill switch — an emergency stop mechanism — should not be bypassable via an alternative entry point.
    Remediation: Add a validation step in harness-run (or a new intermediate job) that enforces kill switch status when inputs.matrix is provided. Document which authorization checks are intentionally delegated to the caller vs. enforced by the workflow, referencing ADR 54 and ADR 63.

  • [missing-doc] docs/.vitepress/config.ts:239 — The new docs/guides/user/custom-poller-example.md has no entry in the Vitepress sidebar config. The User Guides section uses hardcoded { text, link } entries (not getMarkdownFiles()), so the new page will not appear in sidebar navigation. AGENTS.md requires a manual sidebar entry for sections that don't use auto-discovery.
    Remediation: Add { text: "Custom Poller Example", link: "/guides/user/custom-poller-example" } to the User Guides items array near the Jira Integration entry (line 236).

Medium

  • [permission-expansion] .github/workflows/reusable-dispatch.yml:1551 — The concurrency group changed from github.repository (unforgeable, GitHub-controlled) to matrix.status_repo (fully caller-controlled when inputs.matrix is provided). A caller can set status_repo to match another legitimate run's concurrency group key, causing cancel-in-progress: true to cancel that run.
    Remediation: Include github.repository as an additional non-forgeable component in the concurrency key, e.g., fullsend-harness-${{ matrix.agent }}-${{ github.repository }}-${{ matrix.status_repo }}-${{ matrix.status_number }}.

  • [secret-exposure] .github/workflows/reusable-dispatch.yml:1694JIRA_TOKEN and JIRA_USER_EMAIL are passed as environment variables to every harness-run invocation regardless of whether the agent needs Jira access. When the pre-computed matrix path is used, the caller controls which agent runs via matrix.agent.
    Remediation: Gate Jira credentials on a matrix field (e.g., matrix.needs_jira) or document that callers should only pass JIRA_TOKEN when all agents in the matrix are trusted with Jira access.

  • [scope-deviation] .github/workflows/reusable-dispatch.yml — Issue Extract harness-run from reusable-dispatch.yml into a reusable action #6347 proposed extracting harness-run into a composite action. The PR adds an optional matrix input instead — a valid alternative approach, but the design deviation should be captured on the issue or in an ADR so the implementation and the authorization record stay aligned.
    Remediation: Update issue Extract harness-run from reusable-dispatch.yml into a reusable action #6347 to document why the composite-action approach was not taken and to authorize the matrix-input alternative.

  • [protected-path] .github/workflows/reusable-dispatch.yml — This PR modifies a file under .github/, which is a protected path. The PR links to issue Extract harness-run from reusable-dispatch.yml into a reusable action #6347 and explains the rationale. Human approval is always required for protected-path changes regardless of context.

Low

  • [error-handling-gap] .github/workflows/reusable-dispatch.yml:1546 — The fromJSON(inputs.matrix) call has no pre-validation. Malformed JSON produces a cryptic workflow evaluation error rather than a clear failure message.

  • [scope-creep] .github/workflows/reusable-dispatch.yml — Jira-specific secrets (JIRA_TOKEN, JIRA_USER_EMAIL) and input (jira_base_url) are bundled with the custom-poller feature. Consider splitting into a separate PR for cleaner review boundaries.

  • [edge-case] .github/workflows/reusable-dispatch.yml:1551 — If a custom poller omits status_repo from the matrix, the concurrency group contains an empty segment, potentially causing unrelated runs to collide.

  • [field-ordering-convention] .github/workflows/reusable-dispatch.yml:1391if: is placed before needs:, while all other stage jobs in this file place needs: first.

  • [conditional-expression-style] .github/workflows/reusable-dispatch.yml:1391${{ }} wrapper used in if: expression, while other stage jobs use bare expressions (e.g., if: needs.route.outputs.stage == 'triage').

  • [input-description-style] .github/workflows/reusable-dispatch.yml:71 — The matrix input description is multi-sentence; other inputs use terse single phrases. Move extended explanation to the header comment block.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (3)

Review

Findings

Critical

  • [backward-compatible behavior change] .github/workflows/reusable-dispatch.yml:1385 — The harness-dispatch job gains if: ${{ needs.route.outputs.stage == 'harness' && inputs.matrix == '' }} and needs: route. In the base branch, harness-dispatch had no needs and no if — it ran unconditionally and self-gated via .fullsend/config.yaml trigger detection. The route job's case statement (lines ~210–338) only outputs stages: triage, code, review, fix, retro, prioritize. There is no harness case. This means for existing callers (who don't pass matrix), needs.route.outputs.stage will never equal 'harness', so harness-dispatch will never run. The harness-run fallback branch also requires needs.route.outputs.stage == 'harness', compounding the issue. This silently breaks all repos with harness agents using CEL triggers — agents simply stop being dispatched with no error message.
    Remediation: Either (1) add a harness case to the route logic that outputs stage='harness' when harness triggers exist, or (2) remove the needs.route.outputs.stage == 'harness' guard from harness-dispatch's if and keep only inputs.matrix == '', restoring the unconditional self-gating behavior. The harness-run fallback branch must also be updated correspondingly.

Medium

  • [authorization bypass] .github/workflows/reusable-dispatch.yml — When matrix input is provided, the route job is skipped (if: ${{ inputs.matrix == '' }}). The mint-token step uses role: ${{ matrix.role }} directly from the caller-supplied matrix. If the mint service's role allowlist is broader than harness, a custom poller could request elevated roles.
    Remediation: Hardcode role to harness when inputs.matrix is provided: role: ${{ inputs.matrix != '' && 'harness' || matrix.role }}.

  • [authorization bypass] .github/workflows/reusable-dispatch.yml — The kill switch (kill_switch: true in .fullsend/config.yaml) is evaluated only in the route job, which is skipped when inputs.matrix != ''. Custom pollers bypass the kill switch entirely.
    Remediation: Add a lightweight kill-switch check in harness-run that reads .fullsend/config.yaml even when the matrix is pre-computed.

  • [missing-doc] docs/.vitepress/config.ts:223 — The new file docs/guides/user/custom-poller-example.md is added but the User Guides sidebar section uses manually listed entries (not getMarkdownFiles() auto-discovery). Without a sidebar entry, the new page is unreachable from site navigation.
    Remediation: Add { text: 'Custom Poller Example', link: '/guides/user/custom-poller-example' } to the User Guides items array in docs/.vitepress/config.ts.

  • [protected-path] .github/workflows/reusable-dispatch.yml — This file is under the .github/ protected path. The PR links to issue Extract harness-run from reusable-dispatch.yml into a reusable action #6347 and provides rationale, but human approval is always required for protected-path changes regardless of context.

Low

  • [input validation] .github/workflows/reusable-dispatch.yml:1549 — The matrix input is a free-form JSON string with no schema validation. Invalid JSON causes a GHA template expression error rather than a user-friendly message.

  • [secret exposure scope] .github/workflows/reusable-dispatch.yml:1697JIRA_TOKEN, JIRA_USER_EMAIL, and jira_base_url are exposed as environment variables in harness-run to every harness agent, regardless of whether the agent needs Jira access.

  • [scope-creep] .github/workflows/reusable-dispatch.yml — The Jira-specific inputs (jira_base_url, JIRA_TOKEN, JIRA_USER_EMAIL) are orthogonal to the matrix-input feature from Extract harness-run from reusable-dispatch.yml into a reusable action #6347. Custom pollers can pass Jira credentials via repository variables/secrets without workflow-level input threading.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (4)

Review

Findings

Critical

  • [logic error] .github/workflows/reusable-dispatch.yml:1373 — The new if condition on harness-dispatch requires needs.route.outputs.stage == 'harness', but the route job never sets stage to 'harness'. The route logic only ever assigns STAGE to triage, code, review, fix, retro, or prioritize — there is no code path that produces stage=harness. On the base branch, harness-dispatch had no needs and no if — it ran unconditionally in parallel with route. With this PR, harness-dispatch becomes dead code in the normal flow (when inputs.matrix is empty), which in turn prevents harness-run from ever executing in the normal flow. This breaks the entire harness agent pipeline for all existing callers that do not provide a pre-computed matrix.
    Remediation: Either (a) remove the needs.route.outputs.stage == 'harness' check from harness-dispatch's if condition, keeping only inputs.matrix == '' (so harness-dispatch continues to run unconditionally as before, but is skipped when a pre-computed matrix is supplied), and remove needs: route from harness-dispatch; or (b) add a 'harness' stage output path in the route job for the events that should trigger harness agents.

Medium

  • [logic error] .github/workflows/reusable-dispatch.yml:1533 — The harness-run job's if condition includes needs.route.outputs.stage == 'harness' in its second branch (the normal-flow branch). Even if harness-dispatch is fixed to run correctly, this guard would still prevent harness-run from executing because route never outputs stage == 'harness'. The original condition (needs.harness-dispatch.outputs.matrix != '' && fromJSON(...)) was sufficient and did not depend on route's stage output.
    Remediation: Change the second branch to not depend on route stage output, e.g., use needs.harness-dispatch.result == 'success' && needs.harness-dispatch.outputs.matrix != '' && ... or simply check matrix output non-emptiness as the original code did.

  • [authorization bypass] .github/workflows/reusable-dispatch.yml — When the matrix input is provided by a custom poller, the route job (which performs is_authorized permission checks per ADR 0054) and harness-dispatch (which checks .fullsend/config.yaml agent-enablement) are both skipped entirely. ADR 0054 mandates "all agent dispatch paths require authorization before dispatching." The custom poller flow creates a new dispatch path that bypasses these checks. While the workflow_call trust boundary and OIDC-based mint validation provide defense in depth, the agent-enablement layer is lost.
    Remediation: Consider adding a lightweight validation step in harness-run that checks agent enablement against .fullsend/config.yaml even when a pre-computed matrix is provided. Alternatively, document an ADR amendment for this new dispatch path that explains why the mint service alone is sufficient.

  • [missing-doc] docs/.vitepress/config.ts — New doc file docs/guides/user/custom-poller-example.md was added but is not listed in the VitePress sidebar config. The guides/user section uses manual entries (not getMarkdownFiles()), so the new page will not appear in site navigation. Per AGENTS.md: "Sections using getMarkdownFiles() are auto-discovered. All other sections need a manual { text, link } entry."
    Remediation: Add { text: "Custom Poller Example", link: "/guides/user/custom-poller-example" } to the User Guides sidebar section in docs/.vitepress/config.ts.

  • [protected-path] .github/workflows/reusable-dispatch.yml — This PR modifies a file under .github/, which is a protected path requiring human approval. The PR links to issue Extract harness-run from reusable-dispatch.yml into a reusable action #6347 and the description explains the rationale for the workflow change. Human approval is always required for protected-path changes, regardless of context.

Low

  • [edge case] .github/workflows/reusable-dispatch.yml:1541 — The matrix expression fromJSON(inputs.matrix != '' && inputs.matrix || needs.harness-dispatch.outputs.matrix) relies on GitHub Actions expression short-circuit evaluation. In edge cases where both inputs.matrix is empty and harness-dispatch was skipped (producing empty output), fromJSON('') would cause a workflow evaluation error. The if condition currently guards against this in most paths, but once the critical finding is fixed, ensure the guard remains comprehensive.

Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-coder

fullsend-ai-coder Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

🤖 Finished Fix · ❌ Failure · Started 5:21 PM UTC · Completed 5:28 PM UTC

Commit: 7905677 · View workflow run →

@fullsend-ai-coder

Copy link
Copy Markdown
Contributor

⚠️ Post-fix script failed — Pre-commit blocked (exit code 1)

The fix agent completed, but the post-fix script failed before finishing.

Workflow run: https://github.com/fullsend-ai/fullsend/actions/runs/32507571645

Details:
check toml...........................................(no files to check)Skipped
mixed line ending........................................................Passed
ruff (legacy alias)..................................(no files to check)Skipped
ruff format..........................................(no files to check)Skipped
ty check.............................................(no files to check)Skipped
bandit...............................................(no files to check)Skipped
Detect hardcoded secrets.................................................Passed
shellcheck...........................................(no files to check)Skipped
pinact (SHA-pin check)...................................................Passed
Lint GitHub Actions workflow files.......................................Failed
- hook id: actionlint
- exit code: 1

.github/workflows/reusable-dispatch.yml:1528:9: if: condition "${{\n  !cancelled() && (\n    (inputs.matrix != '' && fromJSON(inputs.matrix).include[0] != null) ||\n    (needs.harness-dispatch.outputs.matrix != '' && fromJSON(needs.harness-dispatch.outputs.matrix).include[0] != null)\n  )\n}}\n" is always evaluated to true because extra characters are around ${{ }} [if-cond]
     |
1528 |     if: |
     |         ^

gofmt................................................(no files to check)Skipped
go vet...............................................(no files to check)Skipped
lint mint embed sync.................................(no files to check)Skipped
lint interface doc sync..............................(no files to check)Skipped
lint ADR statuses....................................(no files to check)Skipped
lint ADR numbers.....................................(no files to check)Skipped
lint ADR frontmatter.................................(no files to check)Skipped
lint broken symlinks.....................................................Passed
lint docs link scope.....................................................Passed
lint markdown links......................................................Passed
lint workflow file size..............................(no files to check)Skipped
lint-staged (web)........................................................Passed

Please check the workflow logs for full details and retry with /fs-fix if appropriate.

Add optional JIRA_TOKEN, JIRA_USER_EMAIL secrets and jira_base_url input
to reusable-dispatch.yml to support Jira-based harness agents.

These are passed through to the harness-run job's environment, enabling
Jira agents to authenticate and interact with Jira Cloud instances.

JIRA_BASE_URL falls back to vars.JIRA_BASE_URL from the caller's context
if not explicitly provided as an input, following the pattern used for
OTEL variables.

Assisted-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 21, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 5:31 PM UTC · Completed 6:13 PM UTC

Commit: 61160a4 · View workflow run →

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-coder

fullsend-ai-coder Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

🤖 Finished Fix · ❌ Failure · Started 6:15 PM UTC · Completed 6:24 PM UTC

Commit: 61160a4 · View workflow run →

@fullsend-ai-coder

Copy link
Copy Markdown
Contributor

⚠️ Post-fix script failed — Pre-commit blocked (exit code 1)

The fix agent completed, but the post-fix script failed before finishing.

Workflow run: https://github.com/fullsend-ai/fullsend/actions/runs/32512193758

Details:
check toml...........................................(no files to check)Skipped
mixed line ending........................................................Passed
ruff (legacy alias)..................................(no files to check)Skipped
ruff format..........................................(no files to check)Skipped
ty check.............................................(no files to check)Skipped
bandit...............................................(no files to check)Skipped
Detect hardcoded secrets.................................................Passed
shellcheck...........................................(no files to check)Skipped
pinact (SHA-pin check)...................................................Passed
Lint GitHub Actions workflow files.......................................Failed
- hook id: actionlint
- exit code: 1

.github/workflows/reusable-dispatch.yml:1528:9: if: condition "${{\n  !cancelled() && (\n    (inputs.matrix != '' && fromJSON(inputs.matrix).include[0] != null) ||\n    (needs.harness-dispatch.outputs.matrix != '' && fromJSON(needs.harness-dispatch.outputs.matrix).include[0] != null)\n  )\n}}\n" is always evaluated to true because extra characters are around ${{ }} [if-cond]
     |
1528 |     if: |
     |         ^

gofmt................................................(no files to check)Skipped
go vet...............................................(no files to check)Skipped
lint mint embed sync.................................(no files to check)Skipped
lint interface doc sync..............................(no files to check)Skipped
lint ADR statuses....................................(no files to check)Skipped
lint ADR numbers.....................................(no files to check)Skipped
lint ADR frontmatter.................................(no files to check)Skipped
lint broken symlinks.....................................................Passed
lint docs link scope.....................................................Passed
lint markdown links......................................................Passed
lint workflow file size..............................(no files to check)Skipped
lint-staged (web)........................................................Passed

Please check the workflow logs for full details and retry with /fs-fix if appropriate.

Update jira-integration.md to use the new reusable-dispatch.yml with
pre-computed matrix input instead of the old manual gh workflow run
approach.

Update custom-poller-example.md to:
- Use top-level permissions block (required for reusable workflow calls)
- Include all required permissions (contents: write, packages: read)
- Add jira_base_url input to harness job
- Show complete poll job implementation with matrix building
- Add permissions explanation section

These docs now accurately reflect the new approach from PR #6455.

Assisted-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 21, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 6:43 PM UTC · Ended 7:13 PM UTC

Commit: 7d8d51d · View workflow run →

- Remove broken stage=='harness' checks from harness-dispatch and harness-run
  (route never outputs 'harness', breaking all existing callers)
- Fix actionlint error by inlining if condition (remove block scalar)
- Add event_action validation when matrix is not provided
- Fix concurrency key to use matrix.status_repo instead of github.repository
  (prevents cross-repo cancellation in custom poller scenarios)

Resolves critical review feedback from fullsend-ai-review and qodo-code-review.

Assisted-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>
- Add guide to docs/guides/README.md index (user guides section)
- Rename "Required Configuration" to "Prerequisites" for consistency
- Link jargon to glossary (harness, OTEL/OTLP) and external docs (matrix)
- Add architectural references (architecture.md, mint-administration.md)
- Define WIF inline (not in glossary yet)

Addresses review feedback from qodo-code-review.

Assisted-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 21, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 8:12 PM UTC · Completed 8:55 PM UTC

Commit: 9a497f9 · View workflow run →

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approving at head 9a497f92 — the design is sound and CI is fully green (behaviour, e2e, test, build, codecov/patch). One change requested below that I'd like to see before or shortly after merge; keeping it in the review body rather than an inline thread so it doesn't sit as a merge blocker.

Requested change — make the event_action validation actually gate

event_action went from required: true to required: false, default: "", which moved the contract from GitHub's own workflow_call validation (the call was refused before any job ran) to a runtime step inside route. That step currently gates nothing:

  • harness-dispatch has no needs: route — deliberately removed in f2adaa56 because route never outputs stage.
  • harness-run declares needs: [route, harness-dispatch] but guards with if: !cancelled() && .... A failed route is not a cancelled one, so it does not stop the job — and harness-run never reads needs.route at all, so route is a pure ordering edge with no effect.
  • fullsend dispatch --event-action "" does not fail either: internal/harnessdispatch/input/ghaevent.go:69-72 silently falls back to raw["action"] from the event payload.

So if a caller omits both matrix and event_action, route red-Xes, harness-dispatch still computes a real matrix from the payload fallback, and the agents run anyway. The net effect is a decorative failed job plus the loss of a previously hard contract. Either validating in harness-dispatch (which is the job that actually consumes event_action), or requiring route success on the non-matrix branch, would restore it.

On the security surface — checked, and it holds

Recording the reasoning since caller-supplied matrices look alarming at first read. The matrix controls matrix.role and matrix.source_repo, and harness-run feeds the bare name of source_repo to the mint as repos. The mint is the real authority and it binds it: internal/mintcore/repos_scope.go:93-99 allows a per-repo caller only its own bare repo name, returning errPerRepoCrossRepo → 403 otherwise, with the foreign-grant path at handler.go:310 explicitly config-gated. A forged source_repo therefore either 403s at the mint or yields a token scoped to the caller's own repo that fails at target checkout. Worth saying explicitly in the workflow comments that the safety of this input rests entirely on mint enforcement — the workflow itself trusts matrix.source_repo completely. (Org-mode .fullsend callers are deliberately more permissive, but the documented custom-poller path is per-repo, so that's fine.)

Smaller notes, none blocking

  • Malformed matrix JSON errors the if: expression at the workflow level rather than failing cleanly — ugly, not unsafe.
  • The '{"include":[]}' fallback added in 9a497f92 looks unreachable, since the if: guard rejects an empty/null include first. Harmless belt-and-braces.
  • The Jira secrets and jira_base_url (61160a46) are unrelated to #6347 — fine to land together, just noting the bundling.
  • Verified there's no stale-output hazard from !cancelled(): harness-dispatch's matrix output is set by its last step, so a failed job cannot leave a usable matrix behind.
  • Concurrency group gaining github.repository + matrix.status_repo is right for external pollers targeting other repos.

Worth remembering on merge: the agents repo consumes this file at @main (fullsend-ai/agents#874), so this dogfoods immediately rather than at the next v0 cut. That's the intent, and it's why green behaviour/e2e on this PR carries more weight than usual.

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-coder

fullsend-ai-coder Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

🤖 Finished Fix · ❌ Failure · Started 8:57 PM UTC · Completed 9:07 PM UTC

Commit: 9a497f9 · View workflow run →

…route dependency

- Add validation step to harness-dispatch that fails fast if event_action
  is empty when matrix is not provided
- Remove route from harness-run needs since it never uses route outputs

Assisted-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 21, 2026

Copy link
Copy Markdown

🤖 Review · ❌ Terminated · Started 9:07 PM UTC · Ended 9:26 PM UTC

Commit: abef3cf · View workflow run →

@fullsend-ai-coder

Copy link
Copy Markdown
Contributor

⚠️ Post-fix script failed — Push rejected (exit code 1)

The fix agent completed, but the post-fix script failed before finishing.

Workflow run: https://github.com/fullsend-ai/fullsend/actions/runs/32525978903

Details:
To https://github.com/fullsend-ai/fullsend.git
! [rejected] reusable-dispatch-precomputed-matrix -> reusable-dispatch-precomputed-matrix (fetch first)
error: failed to push some refs to 'https://github.com/fullsend-ai/fullsend.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
To https://github.com/fullsend-ai/fullsend.git
! [rejected] reusable-dispatch-precomputed-matrix -> reusable-dispatch-precomputed-matrix (stale info)
error: failed to push some refs to 'https://github.com/fullsend-ai/fullsend.git'
Please check the workflow logs for full details and retry with /fs-fix if appropriate.

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

See the review comment for full details.


Note: The following inline comments could not be posted on the diff (GitHub returned 422) and are included here instead:

  • .github/workflows/reusable-dispatch.yml:1550: [medium] edge-case

When a caller provides the matrix input, fromJSON(inputs.matrix) is called in both the if condition and strategy.matrix expression without prior validation. Syntactically invalid JSON produces an opaque GHA expression evaluation error. No validation step exists for the matrix input's structure or required fields.

Suggested fix: Add a validation job or step that runs when inputs.matrix != '' to validate the JSON parses correctly and contains the expected include array with required fields (agent, role, source_repo, event_payload, status_repo, status_number).

  • docs/guides/user/jira-integration.md (file-level): Line 255 · [medium] stale-doc

The 'Dry-run tip' section references the removed 'Dispatch agent workflows' step and 'gh workflow run', which no longer exist after this PR replaces the per-dispatch loop with a matrix-based approach.

Suggested fix: Rewrite the Dry-run tip to reference the new flow (e.g., 'run the poll step locally and inspect dispatches.json').

  • docs/guides/user/jira-integration.md (file-level): Line 263 · [medium] stale-doc

The 'Dispatch record format' section describes records as 'execution refs compatible with the workflow_call shim' dispatched individually via gh workflow run. They are now consumed as matrix entries via the matrix input.

Suggested fix: Update the description to clarify records are matrix entries matching the fullsend dispatch --output-driver gha-matrix format.

  • .github/workflows/reusable-dispatch.yml:1550: [low] edge-case

!cancelled() in harness-run's if condition is more permissive than needed. !failure() would achieve the same result while gating out the failure case at the status-check level rather than relying on expression short-circuit behavior.

Suggested fix: Replace !cancelled() with !failure().

  • .github/workflows/reusable-dispatch.yml:1553: [low] api-contract

The concurrency group now includes matrix.status_repo. If fullsend dispatch --output-driver gha-matrix does not always populate status_repo, the concurrency key changes, potentially breaking cancel-in-progress for existing dispatches.

Suggested fix: Verify fullsend dispatch always includes status_repo, or use a fallback: matrix.status_repo || github.repository.

@fullsend-ai-review

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 9:07 PM UTC · Completed 9:26 PM UTC

Commit: abef3cf · View workflow run →

@fullsend-ai-coder

fullsend-ai-coder Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

🤖 Finished Fix · ❌ Failure · Started 9:28 PM UTC · Completed 9:38 PM UTC

Commit: abef3cf · View workflow run →

@fullsend-ai-coder

Copy link
Copy Markdown
Contributor

⚠️ Post-fix script failed — Push rejected — workflows permission (exit code 1)

The fix agent completed, but the post-fix script failed before finishing.

Security boundary: the coder app intentionally lacks workflows write permission. Changes to .github/workflows/ must be made outside the agent (e.g., via a manual PR). Re-run the agent without workflow file changes, or apply those changes separately.

Workflow run: https://github.com/fullsend-ai/fullsend/actions/runs/32528536737

Details:
To https://github.com/fullsend-ai/fullsend.git
! [remote rejected] reusable-dispatch-precomputed-matrix -> reusable-dispatch-precomputed-matrix (refusing to allow a GitHub App to create or update workflow .github/workflows/reusable-dispatch.yml without workflows permission)
error: failed to push some refs to 'https://github.com/fullsend-ai/fullsend.git'
To https://github.com/fullsend-ai/fullsend.git
! [remote rejected] reusable-dispatch-precomputed-matrix -> reusable-dispatch-precomputed-matrix (refusing to allow a GitHub App to create or update workflow .github/workflows/reusable-dispatch.yml without workflows permission)
error: failed to push some refs to 'https://github.com/fullsend-ai/fullsend.git'
Please check the workflow logs for full details and retry with /fs-fix if appropriate.

@ralphbean
ralphbean added this pull request to the merge queue Aug 24, 2026
Merged via the queue into main with commit 1ad2c76 Aug 24, 2026
31 of 32 checks passed
@ralphbean
ralphbean deleted the reusable-dispatch-precomputed-matrix branch August 24, 2026 16:35
@fullsend-ai-retro

fullsend-ai-retro Bot commented Aug 24, 2026

Copy link
Copy Markdown

🤖 Finished Retro · ✅ Success · Started 4:37 PM UTC · Completed 4:52 PM UTC

Commit: abef3cf · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Cost: $4.78

@fullsend-ai-retro

Copy link
Copy Markdown

Retro: PR #6455 — Add pre-computed matrix input to reusable-dispatch

Timeline

Human-authored PR by ralphbean (8 commits, 4 files, +239/-51) adding an optional matrix input to reusable-dispatch.yml so custom pollers can invoke harness agents directly, bypassing standard routing. Linked to issue #6347.

  • Aug 21 16:59 — PR opened with initial commit.
  • Aug 21 17:19 — Review agent (run 32508451335): CHANGES_REQUESTED. Caught a critical bug — needs.route.outputs.stage == 'harness' is dead code since route never outputs 'harness'.
  • Aug 21 17:19–20:08 — Fix agent runs 1–3 all fail: runs 1–2 produce identical actionlint errors (multiline ${{ }} in if: conditions); run 3 fixes actionlint but introduces a broken markdown link.
  • Aug 21 19:06 — Author pushes 0445dc1 fixing the critical dead-code condition.
  • Aug 21 19:50waynesun09 posts a thorough human review including security analysis verifying mint service repo scoping (repos_scope.go) makes the caller-supplied matrix safe.
  • Aug 21 20:25waynesun09 APPROVES with one non-blocking note: event_action validation is decorative (doesn't actually gate downstream jobs).
  • Aug 21 20:55–21:38 — Fix agent runs 4–5 fail: run 4 loses a push race (stale ref), run 5 hits the workflows permission wall.
  • Aug 24 16:35 — PR merged after merge queue checks pass.

What went well

  • Review agent caught a real critical bug on the first review pass — the stage == 'harness' dead-code condition. This was independently confirmed by the human reviewer.
  • Human reviewer added unique security depth: waynesun09 verified the mint service enforces repo scoping, confirming the authorization-bypass concern raised by the review agent is mitigated at the infrastructure layer. This is the kind of deep trust-model analysis that currently requires human expertise.
  • Qodo review provided useful complementary coverage (mermaid diagram, alternative approaches analysis) without excessive noise.

What didn't go well

5 fix agent runs, all failed, zero value delivered. This is the headline finding. Failure breakdown:

Run Failure Category
32507571645 actionlint: multiline ${{ }} in if: Pre-commit
32512193758 Same actionlint error repeated Pre-commit
32520974420 Broken ADR link in docs Pre-commit
32525978903 Stale ref / push race Push rejected
32528536737 GitHub App lacks workflows permission Push rejected

Evidence for existing issues

  • #3627 (agent should treat workflow files as unpushable): Run 5 hit this exact wall. If Code agent should treat .github/workflows/ as unpushable when coder token lacks workflows permission #3627 were implemented, the agent would have skipped workflow changes and potentially succeeded on doc-only fixes.
  • #6494 (pre-commit in validation loop, recently closed): Runs 1–3 failed on pre-commit hooks. With an in-loop retry, the agent could have seen the actionlint or lint-md-links error and corrected within the same run.
  • #3628 (post-script should detect permission rejections): Run 5's failure was correctly identified by the harness, but the dispatch still happened.
  • #4018 (VitePress sidebar lint check): The review agent flagged the missing sidebar entry across all 5 review runs; it was never addressed before merge.
  • #1557 (review-fix race condition): Run 4 lost a push race — the author pushed between the agent's checkout and push.

Autonomy observations

The review agent matched human review on detecting the critical dead-code bug. However, the human reviewer added irreplaceable depth on the security trust model — verifying mint service enforcement in repos_scope.go to confirm the authorization-bypass concern was mitigated. For increased autonomy on security-sensitive changes, the review agent would need the ability (and instruction) to trace trust boundaries across service layers rather than flagging theoretical risks.

Proposals filed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fullsend-fix Enables automatic bot-triggered fix runs on human-authored PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Extract harness-run from reusable-dispatch.yml into a reusable action

2 participants