Skip to content

fix(#5074): add preflight dependency check for validation_loop scripts - #5192

Merged
waynesun09 merged 3 commits into
mainfrom
agent/5074-preflight-dep-check
Jul 24, 2026
Merged

fix(#5074): add preflight dependency check for validation_loop scripts#5192
waynesun09 merged 3 commits into
mainfrom
agent/5074-preflight-dep-check

Conversation

@fullsend-ai-coder

@fullsend-ai-coder fullsend-ai-coder Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

When a validation_loop declares a preflight_check command, the runner now executes it during the preflight phase — before sandbox creation. This catches missing host-side dependencies (e.g. python3-jsonschema) immediately instead of after the agent has already completed (~74s of wasted execution).

Scope: this covers validation_loop.preflight_check only, not pre_script/post_script. Issue #5074's "Expected Behavior" section calls for all three; extending to pre_script/post_script is tracked separately in #5568 rather than done here, since PreflightCheck's current nesting inside ValidationLoop doesn't naturally extend to the other two without a design decision on where it should live.

Changes:

  • Add PreflightCheck field to ValidationLoop struct in the
    harness schema (preflight_check YAML key)
  • Execute the check in run.go after ValidateFilesExist but
    before openshell/sandbox setup, with the same ${VAR} expansion
    and RunnerEnv merging that validation_loop.schema gets
  • Add unit tests for YAML parsing, preflight execution, and
    base composition inheritance of the new field

Not included: this PR does NOT update fullsend's own scaffold harness YAML files (triage, review, fix, prioritize, retro). An earlier revision did, but that has no effect for fullsend-ai's own agents — per fullsend-ai/.fullsend's config.yaml, those agents resolve from the pinned fullsend-ai/agents commit before ever falling back to this local scaffold. The actual fix for fullsend-ai's own production agents is tracked at fullsend-ai/agents#422 instead. This PR's Go-level schema/execution changes are generic infrastructure that any harness (including ones from fullsend-ai/agents) can use once that field is set.

Note: pre-commit could not run in this environment (network 403 on hook fetch). The post-script runs an authoritative pre-commit on the runner.

Also found during review (out of scope for this diff): the internal/cli tests that call runAgent("code", fixtureDir) (including TestRunAgent_PreflightCheck_*) aren't hermetic — with a GH_TOKEN/GITHUB_TOKEN resolvable in the environment, agent resolution can silently fetch the live harness from fullsend-ai/agents instead of using the test's local fixture. Pre-existing, not introduced by this PR (an untouched older test has the same exposure); CI only passes today because lint.yml happens to blank those env vars. Tracked at #5569.


Relates to #5074 (validation-loop leg only; see scope note above and #5568 for the rest)

Post-script verification

  • Branch is not main/master (agent/5074-preflight-dep-check)
  • Secret scan passed (gitleaks — 83397e5355462dea996845ed2a1ec019d32f30ac..HEAD)
  • Pre-commit hooks passed (authoritative run on runner)
  • Tests ran inside sandbox

@fullsend-ai-coder
fullsend-ai-coder Bot requested a review from a team as a code owner July 16, 2026 14:19
@fullsend-ai-coder fullsend-ai-coder Bot added the ready-for-review Triggers review agent dispatch label Jul 16, 2026
@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown

E2E tests are running

Authorization passed for this commit. See the E2E Tests workflow for results.

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown

Site preview

Preview: https://6d579d6a-site.fullsend-ai.workers.dev

Commit: 52c398faf7fdab72b9d66647abd74728389148fb

@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@rh-hemartin

Copy link
Copy Markdown
Member

/fs-review

@fullsend-ai-review

fullsend-ai-review Bot commented Jul 22, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 7:30 AM UTC · Completed 7:47 AM UTC
Commit: 7ada4e0 · View workflow run →

@fullsend-ai-review

fullsend-ai-review Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review

Findings

Medium

  • [logic-error] internal/harness/forge.go:137mergeForgeConfig (forge-into-harness merge) does not carry forward PreflightCheck. The PR adds carry-forward logic to both mergeBaseIntoChild (compose.go) and mergeForgeConfigInto (compose.go), but misses this third merge site. When a harness declares preflight_check on its top-level validation_loop and a forge section overrides validation_loop without setting its own preflight_check, the check is silently dropped.
    Remediation: Add the same carry-forward pattern in forge.go: before overwriting h.ValidationLoop with fc.ValidationLoop, save h.ValidationLoop.PreflightCheck and copy it to fc.ValidationLoop.PreflightCheck if empty. Add a test case analogous to TestMergeForgeConfigInto_PreflightCheckCarryForward.

Low

  • [validation-gap] internal/harness/harness.go:770ValidateResourceTypes() and ResolveRelativeTo() do not process PreflightCheck. This is intentional — PreflightCheck is a shell command string, not a file path or URL, so path resolution and local-only validation do not apply.

  • [environment-exposure] internal/cli/run.go:689PreflightCheck runs with append(os.Environ(), envToList(h.RunnerEnv)...) while pre_script and post_script use childScriptEnv(), which additionally strips and replaces TRACEPARENT. The functional difference is negligible for a dependency check.

  • [shell-injection-surface] internal/cli/run.go:689PreflightCheck uses exec.CommandContext("sh", "-c", ...) while other executable harness fields use direct path execution. The sh -c wrapper is intentionally correct for inline shell commands sourced from repo-committed YAML (same trust boundary).

  • [const-vs-var-pattern] internal/cli/run.go:70preflightCheckTimeout is a var while the analogous preflightGitHubTimeout is a const. The var choice is explicitly documented for test overridability and is a defensible design choice.

  • [numbering-inconsistency] internal/cli/run.go:681 — Step // 1d. follows // 1c. consistently as a sub-step of section 1 before section 2. Numbering is internally consistent.

  • [missing-new-feature-documentation] docs/guides/user/building-custom-agents.md:163 — The validation_loop example does not show the new preflight_check field.

  • [missing-new-feature-documentation] docs/guides/user/customizing-agents.md:43 — The validation_loop examples do not mention the new preflight_check option.

  • [missing-new-feature-documentation] docs/guides/user/bring-your-own-agent.md:215 — The validation_loop field reference does not mention the new preflight_check option.

Previous run

Review

Findings

Medium

  • [logic-error] internal/harness/forge.go:137mergeForgeConfig (forge-into-harness merge) does not carry forward PreflightCheck. The PR adds carry-forward logic to both mergeBaseIntoChild (compose.go:547) and mergeForgeConfigInto (compose.go:1254), but misses this third merge site. When a harness declares preflight_check on its top-level validation_loop and a forge section overrides validation_loop without setting its own preflight_check, the check is silently dropped — the exact bug this PR fixes in the other two merge functions.
    Remediation: Apply the same carry-forward pattern before line 138: if fc.ValidationLoop.PreflightCheck is empty and h.ValidationLoop is non-nil, copy h.ValidationLoop.PreflightCheck to fc.ValidationLoop.PreflightCheck before the replacement. Add a test case analogous to TestMergeForgeConfigInto_PreflightCheckCarryForward.

Low

  • [environment-exposure] internal/cli/run.go:682PreflightCheck runs with os.Environ() while other host-side scripts (pre_script, post_script) use childScriptEnv() which constructs a more controlled environment. A dependency check like python3 -c 'import jsonschema' does not need access to secrets that may be in the runner environment.

  • [test-adequacy] internal/cli/run_test.goTestRunAgent_PreflightCheck_Timeout does not exercise the timeout code path. The test pre-cancels the parent context (context.Canceled), but the production code checks for context.DeadlineExceeded to produce the timeout-specific error message ("timed out after 30s"). The test passes because the cancelled context causes the command to fail, but the DeadlineExceeded branch is never reached.

  • [shell-injection-surface] internal/cli/run.go:681PreflightCheck uses exec.CommandContext("sh", "-c", ...) while other executable harness fields use direct path execution. This is intentionally correct for inline commands (e.g., python3 -c '...') but widens the execution surface. Defense-in-depth concern; the value is sourced from repo-committed YAML (same trust boundary).

  • [validation-gap] internal/harness/harness.go:210ValidateResourceTypes() and ResolveRelativeTo() process other ValidationLoop fields but not PreflightCheck. Intentional since PreflightCheck is a shell command (not a file path), but the asymmetry is worth noting.

  • [missing-new-feature-documentation] docs/guides/user/building-custom-agents.md:163 — The validation_loop example does not show the new preflight_check field. Users building custom agents would not discover this option.

  • [missing-new-feature-documentation] docs/guides/user/customizing-agents.md:43 — The validation_loop examples do not mention the new preflight_check option.

Previous run

Review

Findings

Medium

  • [missing-new-feature-documentation] docs/ADRs/0024-harness-definitions.md:388 — The validation_loop schema definition does not include the new preflight_check field. Since this ADR has status Accepted, only a brief cross-reference annotation is appropriate (not a schema rewrite).
    Remediation: Add a short annotation noting that preflight_check was added later (see issue runner: preflight dependency check does not catch missing python3-jsonschema before agent execution #5074).

  • [missing-new-feature-documentation] docs/ADRs/0045-forge-portable-harness-schema.md:385 — The forge-portable harness schema does not document the new preflight_check field. ADR immutability applies; only a cross-reference annotation is appropriate.
    Remediation: Add a brief cross-reference annotation.

  • [missing-new-feature-documentation] docs/guides/user/building-custom-agents.md:163 — The validation_loop example does not show the new preflight_check field. Users building custom agents would not discover this option.
    Remediation: Add preflight_check to the validation_loop example block with a brief explanation.

  • [missing-new-feature-documentation] docs/guides/user/customizing-agents.md:43 — The validation_loop examples (3 places) do not mention the new preflight_check option.
    Remediation: Update all three validation_loop examples to include preflight_check as an optional field.

Low

  • [test-adequacy] internal/cli/run_test.go:2387 — The four new tests do not exercise the actual preflight logic in runAgent(). They construct a Harness, manually run exec.Command, and assert exit codes — testing shell behavior rather than the guard condition, error formatting, printer calls, or early-return behavior in production code.

  • [shell-injection-surface] internal/cli/run.go:679PreflightCheck is executed via exec.Command("sh", "-c", ...) while all other executable harness fields use direct path execution. The value is sourced from repo-committed YAML (same trust boundary), but the sh -c invocation enables command chaining and subshell execution that direct path execution would not. Defense-in-depth concern, not a direct vulnerability.

  • [context-cancellation] internal/cli/run.go:679 — The preflight check uses exec.Command without passing context. If the parent context is cancelled, the command will not be killed. Consistent with how other checks in this function handle context.

  • [validation-gap] internal/harness/harness.goValidateResourceTypes() and ResolveRelativeTo() process other ValidationLoop fields but not PreflightCheck. This is intentional since PreflightCheck is a shell command (not a file path), but the asymmetry is worth noting.

  • [printer-message-capitalization] internal/cli/run.go:677StepStart("Preflight: checking validation_loop dependencies") uses a colon-separated prefix style. Existing StepStart patterns use sentence case (e.g., "Checking openshell availability", "Checking gateway").

  • [future-extensibility] internal/harness/harness.go:211preflight_check is scoped to ValidationLoop. Issue runner: preflight dependency check does not catch missing python3-jsonschema before agent execution #5074 suggested extending the pattern to pre_script and post_script dependencies. The current nesting may not naturally extend to other script types.


Labels: PR modifies harness schema (ValidationLoop struct) and CLI runner (preflight check execution)

Previous run

Review

Findings

Medium

  • [logic-error] internal/harness/forge.go:137mergeForgeConfig (forge-into-harness merge) does not carry forward PreflightCheck. The PR adds carry-forward logic to both mergeBaseIntoChild (compose.go:547) and mergeForgeConfigInto (compose.go:1254), but misses this third merge site. When a harness declares preflight_check on its top-level validation_loop and a forge section overrides validation_loop without setting preflight_check, the check is silently dropped — the exact bug this PR fixes in the other two merge functions.
    Remediation: Apply the same carry-forward pattern before line 138: if fc.ValidationLoop.PreflightCheck is empty and h.ValidationLoop is non-nil, copy h.ValidationLoop.PreflightCheck to fc.ValidationLoop.PreflightCheck before the replacement. Add a test case analogous to TestMergeForgeConfigInto_PreflightCheckCarryForward.

Low

  • [environment-exposure] internal/cli/run.go:682PreflightCheck runs with os.Environ() while other host-side scripts (pre_script, post_script) use childScriptEnv() which constructs a more controlled environment. A dependency check like python3 -c 'import jsonschema' does not need access to secrets that may be in the runner environment.

  • [test-adequacy] internal/cli/run_test.goTestRunAgent_PreflightCheck_Timeout does not exercise the timeout code path. The test pre-cancels the parent context (context.Canceled), but the production code checks for context.DeadlineExceeded to produce the timeout-specific error message ("timed out after 30s"). The test passes because the cancelled context causes the command to fail, but the DeadlineExceeded branch is never reached.

  • [shell-injection-surface] internal/cli/run.go:681PreflightCheck uses exec.CommandContext("sh", "-c", ...) while other executable harness fields use direct path execution. This is intentionally correct for inline commands (e.g., python3 -c '...') but widens the execution surface. Defense-in-depth concern; the value is sourced from repo-committed YAML (same trust boundary).

  • [validation-gap] internal/harness/harness.go:210ValidateResourceTypes() and ResolveRelativeTo() process other ValidationLoop fields but not PreflightCheck. Intentional since PreflightCheck is a shell command (not a file path), but the asymmetry is worth noting.

  • [missing-new-feature-documentation] docs/guides/user/building-custom-agents.md:163 — The validation_loop example does not show the new preflight_check field. Users building custom agents would not discover this option.

  • [missing-new-feature-documentation] docs/guides/user/customizing-agents.md:43 — The validation_loop examples do not mention the new preflight_check option.

Previous run (2)

Review

Findings

Medium

  • [missing-new-feature-documentation] docs/ADRs/0024-harness-definitions.md:388 — The validation_loop schema definition does not include the new preflight_check field. Since this ADR has status Accepted, only a brief cross-reference annotation is appropriate (not a schema rewrite).
    Remediation: Add a short annotation noting that preflight_check was added later (see issue runner: preflight dependency check does not catch missing python3-jsonschema before agent execution #5074).

  • [missing-new-feature-documentation] docs/ADRs/0045-forge-portable-harness-schema.md:385 — The forge-portable harness schema does not document the new preflight_check field. ADR immutability applies; only a cross-reference annotation is appropriate.
    Remediation: Add a brief cross-reference annotation.

  • [missing-new-feature-documentation] docs/guides/user/building-custom-agents.md:163 — The validation_loop example does not show the new preflight_check field. Users building custom agents would not discover this option.
    Remediation: Add preflight_check to the validation_loop example block with a brief explanation.

  • [missing-new-feature-documentation] docs/guides/user/customizing-agents.md:43 — The validation_loop examples (3 places) do not mention the new preflight_check option.
    Remediation: Update all three validation_loop examples to include preflight_check as an optional field.

Low

  • [test-adequacy] internal/cli/run_test.go:2387 — The four new tests do not exercise the actual preflight logic in runAgent(). They construct a Harness, manually run exec.Command, and assert exit codes — testing shell behavior rather than the guard condition, error formatting, printer calls, or early-return behavior in production code.

  • [shell-injection-surface] internal/cli/run.go:679PreflightCheck is executed via exec.Command("sh", "-c", ...) while all other executable harness fields use direct path execution. The value is sourced from repo-committed YAML (same trust boundary), but the sh -c invocation enables command chaining and subshell execution that direct path execution would not. Defense-in-depth concern, not a direct vulnerability.

  • [context-cancellation] internal/cli/run.go:679 — The preflight check uses exec.Command without passing context. If the parent context is cancelled, the command will not be killed. Consistent with how other checks in this function handle context.

  • [validation-gap] internal/harness/harness.goValidateResourceTypes() and ResolveRelativeTo() process other ValidationLoop fields but not PreflightCheck. This is intentional since PreflightCheck is a shell command (not a file path), but the asymmetry is worth noting.

  • [printer-message-capitalization] internal/cli/run.go:677StepStart("Preflight: checking validation_loop dependencies") uses a colon-separated prefix style. Existing StepStart patterns use sentence case (e.g., "Checking openshell availability", "Checking gateway").

  • [future-extensibility] internal/harness/harness.go:211preflight_check is scoped to ValidationLoop. Issue runner: preflight dependency check does not catch missing python3-jsonschema before agent execution #5074 suggested extending the pattern to pre_script and post_script dependencies. The current nesting may not naturally extend to other script types.


Labels: PR modifies harness schema (ValidationLoop struct) and CLI runner (preflight check execution)

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot added requires-manual-review Review requires human judgment component/harness Agent harness, config, and skills loading component/runner Agent runner behavior and lifecycle labels Jul 22, 2026

@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.

Ran an independent multi-pass review focused on the new preflight_check mechanism. Three prior comments below were revised in place with severity changes based on further evidence (the test-coverage gap corresponds to a currently-failing codecov/patch check, not just a style note; the context-cancellation gap deviates from this repo's own established timeout conventions in tokenscope.go/preflight_github.go; the pre/post-script scope gap is explicit in issue #5074's stated "Expected Behavior"). Four new findings: a composition bug that can silently drop preflight_check when a harness overrides validation_loop, a missing ${VAR}/${FULLSEND_DIR} expansion pass, a missing RunnerEnv merge, and a discarded exec error where a reusable helper already exists. The previously flagged documentation gaps and the sh -c/capitalization notes stand as already assessed — no change there.

Comment thread internal/cli/run.go
Comment thread internal/cli/run.go Outdated
Comment thread internal/cli/run.go Outdated
Comment thread internal/cli/run.go

@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.

One additional finding below couldn't be anchored as an inline comment because internal/harness/compose.go isn't part of this PR's diff (no file/line in it to attach a review comment to).


[MEDIUM] internal/harness/compose.go:601preflight_check skips the allowlist/audit pipeline and character validation every sibling executable harness field goes through

ValidationLoop's other executable-content fields, Script and Schema, are file-path references: when inherited through base composition they're routed through resolveBaseScripts (compose.go:601-620 for base-level fields, :649-670 for forge-level), which enforces validateBaseRelPath (rejects null bytes, traversal, absolute paths, embedded URLs) and fetchBaseFile (checks the org's allowed_remote_resources allowlist, content-addresses/caches the file, and writes an auditBaseFetch log entry). PreflightCheck (harness.go:211, a raw shell-command string, not a path) never goes through compose.go at all — it only travels as part of the whole-struct copy child.ValidationLoop = base.ValidationLoop (compose.go:541-542 and 1243-1245), with no per-field check.

Separately, harness.go's Validate()/validateSecurity() apply regex character-class validation to Role, Slug, Agent (basename), Model, and Providers (harness.go:390-423), but PreflightCheck — the one field in this struct directly interpolated into sh -c "..." (run.go:677) and run unsandboxed on the host with os.Environ() before openshell/sandbox setup — gets none of that scrutiny.

This is a real, verified inconsistency, though downgraded from an initial HIGH assessment: it isn't a new class of risk (pre_script/post_script already execute host-side with full env access today, and a remote base: URL's overall content is still hash-pinned and allowlist-checked as a whole file via fetchBaseURL, so it isn't literally unauthenticated). It is, however, a genuine defense-in-depth gap relative to this struct's own established pattern — and it's exactly the mechanism the companion scaffold/agents-repo fix (see the inline finding on triage.yaml) would exercise once preflight_check entries are added to fullsend-ai/agents' harness YAMLs, which many downstream org configs consume via this same base: composition path.

Suggestion: Bring PreflightCheck into the same trust model as Script/Schema: either require it to reference a script file (subject to validateBaseRelPath + fetchBaseFile + auditBaseFetch), or, if an inline command must stay supported, route it through resolveBaseScripts (or an equivalent path) for allowlist participation, plus add basic content/length validation in Validate()/validateSecurity(), consistent with how Role/Slug/Agent/Model/Providers are already treated.

Comment thread internal/scaffold/fullsend-repo/harness/triage.yaml Outdated
@waynesun09 waynesun09 added the ok-to-test Allow e2e CI to run after maintainer review (must be re-applied after each push) label Jul 23, 2026
@waynesun09

Copy link
Copy Markdown
Member

/fs-fix

Please address the following unresolved review findings from this PR's review threads (verbatim below, most severe first). Two multi-pass reviews (bot + human) converged on these independently.


HIGH — internal/cli/run.go:675 — premature-decision: preflight_check silently dropped whenever a harness overrides validation_loop during composition

Finding: ValidationLoop is merged as a whole-struct replace during composition — internal/harness/compose.go contains if child.ValidationLoop == nil { child.ValidationLoop = base.ValidationLoop } at both the base-composition and forge-override merge sites. Since PreflightCheck now lives inside that struct, any child/wrapper harness that declares its own validation_loop: block for any reason — even just to change max_iterations, which is exactly the pattern shown as a normal customization example in this repo's own user guide — silently loses the entire inherited ValidationLoop, including preflight_check, with no error or lint warning. That reintroduces precisely the bug #5074 was filed to fix, for any harness that customizes validation_loop at all.

Suggestion: Either carry PreflightCheck forward independently when the child sets its own ValidationLoop but leaves PreflightCheck empty (at both merge call sites in compose.go), or add a Lint() diagnostic that fires when a child overrides validation_loop while its base declares a non-empty preflight_check, so the drop is visible instead of silent.


HIGH — internal/cli/run.go:679 — context-cancellation (revised from Low)

Finding: runAgent's own ctx context.Context is in scope at this point (used a few lines earlier for harness.LoadWithBase/mintAgentToken), but the preflight command uses plain exec.Command with no timeout and never observes ctx. This matters more here than a generic missing-timeout nit: cmd/fullsend/main.go builds its root context with signal.NotifyContext(..., os.Interrupt, syscall.SIGTERM), and this same package already has two established conventions for bounding exactly this kind of fast host-side check — tokenscope.go (context.WithTimeout(ctx, 5*time.Second)) and preflight_github.go (preflightGitHubTimeout = 30 * time.Second). A preflight_check that hangs (network call, interactive prompt, a bug in a user-supplied check) will not be killed by Ctrl+C/SIGTERM and blocks the run indefinitely — the opposite of this feature's own time-saving goal, since a hang now risks waiting forever instead of the ~74s it used to waste.

Suggestion: Use exec.CommandContext(ctx, "sh", "-c", ...) with a short, explicit timeout derived from ctx (mirroring preflight_github.go's preflightGitHubTimeout pattern), and surface a distinct "preflight check timed out" error so a hang is distinguishable from a normal dependency failure.


MEDIUM — internal/cli/run_test.go:2387 — test-adequacy (revised from Low)

Finding: None of the four new tests exercise runAgent's actual control flow — they re-implement exec.Command("sh", "-c", ...) + CombinedOutput() directly in the test body rather than calling into the guard at run.go:675, so the StepStart/StepFail/StepDone messaging and the error-wrapping logic added by this PR are never executed. TestPreflightCheck_NoCheckConfigured and TestPreflightCheck_NilValidationLoop only assert Go's own zero-value semantics (assert.Empty/assert.Nil on a struct literal that never set the field) and would stay green even if the entire new block in run.go were deleted. This isn't just a style nit: codecov/patch is currently failing on this PR (0% patch coverage, 12 lines missing), and the file already has an established, reusable pattern for testing exactly this kind of pre-sandbox guard — useFakeOpenshell(t) plus a direct runAgent(...) call, as used by TestRunAgent_HarnessLoadPipeline/TestRunAgent_ErrorOnMissingRole.

Suggestion: Use the useFakeOpenshell + runAgent fixture already established in this file to test the guard directly: a failing preflight_check should return before reaching the "Checking openshell availability" step with an error containing preflight_check; a passing/absent one should proceed past it. Drop or demote the two tests that only assert zero-value semantics.


MEDIUM — internal/cli/run.go:677 — premature-decision: preflight_check bypasses the ${VAR} expansion and validation its sibling field schema gets

Finding: validation_loop.schema is explicitly expanded via os.Expand(h.ValidationLoop.Schema, expander) a few dozen lines earlier in this function (so ${FULLSEND_DIR} resolves before use), and is checked for unresolved ${VAR} references by ValidateRunnerEnvWith. PreflightCheck goes through neither. ${FULLSEND_DIR} is the most commonly used harness templating variable in this repo (used in every scaffold YAML's schema field), but it isn't a real OS environment variable — it only resolves through this function's own expander/lookup closures. A harness author who naturally writes preflight_check: "test -x ${FULLSEND_DIR}/scripts/check.sh" by analogy with the neighboring schema field will have it silently expand to an empty string under sh -c, and the resulting failure will surface as "Install the missing dependency" — actively misleading, since the real issue would be this gap, not a missing dependency.

Suggestion: Route h.ValidationLoop.PreflightCheck through the same os.Expand(..., expander) call used for Schema, and add it to ValidateRunnerEnvWith's var-ref checks alongside Schema.


MEDIUM — internal/cli/run.go:678preflight_check doesn't get h.RunnerEnv/env.runner, unlike every sibling host command

Finding: preflightCmd.Env = os.Environ() uses only the raw process environment (this line is actually a functional no-op, since exec.Cmd already defaults to the parent's environment when Env is left nil). Every other host-side command in this function merges the harness's effective RunnerEnv on top of os.Environ() instead: PreScript/PostScript use childScriptEnv(h.RunnerEnv, ...), and ValidationLoop.Script uses append(os.Environ(), validationEnv(h, ...)...), where validationEnv includes envToList(h.RunnerEnv). A harness that uses runner_env/env.runner to point at a custom tool/interpreter location — a natural way to satisfy the exact kind of host dependency this feature checks for — will have the preflight probe evaluate a different environment than the real validation script it's meant to predict, producing false failures or false passes that don't match what happens later in the same run.

Suggestion: Build the preflight command's environment the same way, e.g. preflightCmd.Env = append(os.Environ(), envToList(h.RunnerEnv)...) (or reuse childScriptEnv), so the preflight probe and the real validation script always see the same environment.


MEDIUM — internal/cli/run.go:687 — Underlying exec error is discarded when the failing command produces no output

Finding: preflightErr is only used in the if preflightErr != nil guard — it's never included in the returned error. When CombinedOutput() fails with empty output (permission denied, the process killed by a signal, sh itself missing, etc. — not unusual for simple probes like test -f <path>), the returned error falls back to just the configured command string plus a generic "Install the missing dependency" hint, with no indication of what actually went wrong. This codebase already has a helper that solves exactly this, a few hundred lines away: validationFailMessage(output []byte, execErr error) string, which falls back to execErr.Error() when output is empty and already has dedicated unit tests for that branch.

Suggestion: Reuse it: return fmt.Errorf("validation_loop.preflight_check failed: %s\n%s\nInstall the missing dependency before running this agent", h.ValidationLoop.PreflightCheck, validationFailMessage(preflightOut, preflightErr)), collapsing both branches into one and preserving the underlying error text when there's no output.


MEDIUM — internal/harness/harness.go:211 — premature-decision: scope narrower than issue #5074's stated requirement (revised from Low)

Finding: Issue #5074's "Expected Behavior" section (not just the softer "Suggested Approach") states: "Dependencies required by harness scripts (pre-script, validation script, post-script) should be checked during the preflight phase." The triage comment's recommended fix and proposed test cases likewise explicitly call for pre-script/post-script coverage ("Edge case: verify preflight checks work for pre-scripts and post-scripts too"). This PR implements only the validation_loop leg, yet closes #5074 outright with no note that pre-script/post-script coverage is deferred, and no follow-up issue linked. Nesting the new field inside ValidationLoop (rather than as a top-level Harness field alongside PreScript/PostScript) also makes extending to those two script types a bigger lift later than if this had been designed for all three from the start.

Suggestion: Either extend this PR to cover pre_script/post_script, or narrow the PR description to state that only validation-script dependencies are in scope and open/link a tracked follow-up for the rest, rather than relying on "Closes #5074" to fully resolve the issue as filed.


MEDIUM — internal/harness/compose.go:601 (general PR comment, not anchored inline because compose.go isn't in this PR's diff) — preflight_check skips the allowlist/audit pipeline and character validation every sibling executable harness field goes through

ValidationLoop's other executable-content fields, Script and Schema, are file-path references: when inherited through base composition they're routed through resolveBaseScripts (compose.go:601-620 for base-level fields, :649-670 for forge-level), which enforces validateBaseRelPath (rejects null bytes, traversal, absolute paths, embedded URLs) and fetchBaseFile (checks the org's allowed_remote_resources allowlist, content-addresses/caches the file, and writes an auditBaseFetch log entry). PreflightCheck (harness.go:211, a raw shell-command string, not a path) never goes through compose.go at all — it only travels as part of the whole-struct copy child.ValidationLoop = base.ValidationLoop (compose.go:541-542 and 1243-1245), with no per-field check.

Separately, harness.go's Validate()/validateSecurity() apply regex character-class validation to Role, Slug, Agent (basename), Model, and Providers (harness.go:390-423), but PreflightCheck — the one field in this struct directly interpolated into sh -c "..." (run.go:677) and run unsandboxed on the host with os.Environ() before openshell/sandbox setup — gets none of that scrutiny.

This is a real, verified inconsistency, though downgraded from an initial HIGH assessment: it isn't a new class of risk (pre_script/post_script already execute host-side with full env access today, and a remote base: URL's overall content is still hash-pinned and allowlist-checked as a whole file via fetchBaseURL, so it isn't literally unauthenticated). It is, however, a genuine defense-in-depth gap relative to this struct's own established pattern.

Suggestion: Bring PreflightCheck into the same trust model as Script/Schema: either require it to reference a script file (subject to validateBaseRelPath + fetchBaseFile + auditBaseFetch), or, if an inline command must stay supported, route it through resolveBaseScripts (or an equivalent path) for allowlist participation, plus add basic content/length validation in Validate()/validateSecurity(), consistent with how Role/Slug/Agent/Model/Providers are already treated.


LOW — internal/cli/run.go:679 — shell-injection-surface

PreflightCheck is executed via exec.Command("sh", "-c", ...) while all other executable harness fields use direct path execution. The value is sourced from repo-committed YAML (same trust boundary), but the sh -c invocation enables command chaining and subshell execution. Defense-in-depth concern, not a direct vulnerability.


LOW — internal/cli/run.go:677 — printer-message-capitalization

StepStart("Preflight: checking validation_loop dependencies") uses a colon-separated prefix style. Existing StepStart patterns use sentence case (e.g., "Checking openshell availability", "Checking gateway").


Out of scope for this repo (not part of this fix request): the [HIGH] Scaffold YAML fix doesn't reach fullsend-ai's own production agents finding on internal/scaffold/fullsend-repo/harness/triage.yaml:32 is not actionable here — internal/scaffold/ is no longer the source of truth for these agent definitions (they now live in fullsend-ai/agents per the agent-extraction ADR). That has been filed upstream instead: fullsend-ai/agents#422. Please leave the scaffold YAML edits in this PR as-is; do not revert or further modify internal/scaffold/fullsend-repo/harness/*.yaml as part of this fix request.

@fullsend-ai-coder

fullsend-ai-coder Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Finished Fix · ✅ Success · Started 11:47 PM UTC · Completed 11:59 PM UTC
Commit: 3eb6a6e · View workflow run →

fullsend-ai-coder Bot added a commit that referenced this pull request Jul 23, 2026
- Carry forward PreflightCheck during composition when a child harness
  overrides validation_loop without setting its own preflight_check,
  preventing silent loss of inherited preflight checks (compose.go,
  both mergeBaseIntoChild and mergeForgeConfigInto merge sites)
- Use exec.CommandContext with 30s timeout (mirroring preflightGitHubTimeout
  pattern) so preflight checks respect ctx cancellation and cannot hang
  indefinitely; surface distinct "timed out" error message (run.go)
- Rewrite preflight tests to call runAgent directly via the established
  useFakeOpenshell fixture, covering StepStart/StepFail/StepDone messaging
  and error-wrapping logic; add timeout test case (run_test.go)
- Add composition tests for PreflightCheck carry-forward and child
  override semantics (compose_test.go)

Addresses review feedback on #5192
@fullsend-ai-coder

Copy link
Copy Markdown
Contributor Author

🔧 Fix agent — iteration 1 (human-triggered)

Addressed all 3 review findings: (1) fixed silent PreflightCheck loss during composition by carrying the field forward at both merge sites, (2) added context-awareness and 30s timeout to preflight command execution, (3) rewrote all preflight tests to exercise actual runAgent control flow and added a timeout test. All changes include corresponding test coverage.

Fixed (3):

  1. premature-decision: preflight_check silently dropped during composition (internal/harness/compose.go): Added PreflightCheck carry-forward in both mergeBaseIntoChild and mergeForgeConfigInto: when a child overrides validation_loop without setting its own preflight_check, the base's preflight_check is now preserved. Added composition tests (carry-forward and child-override cases) in compose_test.go.
  2. context-cancellation: preflight command ignores ctx and has no timeout (internal/cli/run.go): Replaced exec.Command with exec.CommandContext using a 30s preflightCheckTimeout constant (mirroring preflightGitHubTimeout pattern). Added distinct 'timed out' error message when context deadline is exceeded, distinguishable from normal dependency failures.
  3. test-adequacy: tests re-implement exec.Command instead of calling runAgent (internal/cli/run_test.go): Replaced all four stub tests with five tests that call runAgent directly using the established useFakeOpenshell fixture, exercising the actual StepStart/StepFail/StepDone messaging and error-wrapping logic. Added preflightTestSetup helper to create required files (agents, harness, scripts). Added new TestRunAgent_PreflightCheck_Timeout test case verifying context cancellation behavior.

Tests: passed

Decision points
  • Chose 30s timeout for preflightCheckTimeout to match preflightGitHubTimeout (alternatives: 5s like tokenscope.go, 60s for slow network checks; rationale: Preflight checks are fast host-side dependency checks (e.g. 'python3 -c import jsonschema'); 30s matches the existing preflightGitHubTimeout pattern in this package and provides ample headroom without allowing indefinite hangs)
  • Carried PreflightCheck forward independently rather than adding a Lint() diagnostic (alternatives: Lint() warning when child overrides validation_loop and base has preflight_check; rationale: Silent carry-forward matches the existing merge semantics for other scalar fields and eliminates the bug without requiring users to change their harness files; a Lint diagnostic would be a weaker fix since it only warns rather than preventing the problem)

Updated by fullsend fix agent

Comment thread internal/cli/run_test.go Outdated
@waynesun09

Copy link
Copy Markdown
Member

/fs-review

@fullsend-ai-review

fullsend-ai-review Bot commented Jul 24, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 12:30 AM UTC · Completed 12:48 AM UTC
Commit: 3eb6a6e · View workflow run →

fullsend-ai-review[bot]

This comment was marked as outdated.

@rh-hemartin

Copy link
Copy Markdown
Member

These harnesses will have no effect, open a followup issue on fullsend-ai/agents to introduce that pre-check.

@waynesun09

Copy link
Copy Markdown
Member

Agreed — removed the scaffold edits in 456a40d and filed fullsend-ai/agents#422 for the actual fix (adding preflight_check to the harness YAMLs fullsend-ai's own agents actually resolve from).

@waynesun09
waynesun09 added this pull request to the merge queue Jul 24, 2026
Merged via the queue into main with commit dd1d7a9 Jul 24, 2026
24 of 26 checks passed
@waynesun09
waynesun09 deleted the agent/5074-preflight-dep-check branch July 24, 2026 15:46
@fullsend-ai-retro

fullsend-ai-retro Bot commented Jul 24, 2026

Copy link
Copy Markdown

🤖 Finished Retro · ✅ Success · Started 3:49 PM UTC · Completed 4:09 PM UTC
Commit: 52c398f · View workflow run →

@fullsend-ai-retro

Copy link
Copy Markdown

Retro: PR #5192 — preflight dependency check for validation_loop

Timeline: Code agent created PR on 2026-07-16. Review agent ran 2026-07-22 (3 findings). Human reviewer waynesun09 conducted independent multi-pass review escalating 7 findings (2 HIGH, 5 MEDIUM). Fix agent ran 2026-07-23, addressed 3/10 findings (both HIGHs + 1 MEDIUM test-adequacy rewrite). Human committed remaining fixes 2026-07-24: ${VAR} expansion, RunnerEnv merge, exec error handling, scaffold dead-code removal, timeout test correction. Final review + approval 2026-07-24, merged same day. Total cycle: 8 days.

Fix agent effectiveness

The fix agent was given 10 explicit findings in the /fs-fix request and addressed 3 (30%), all structurally straightforward changes (composition carry-forward, exec.CommandContext swap, test fixture rewrite). It addressed 0 of 5 MEDIUMs — every finding requiring cross-cutting codebase knowledge (env var expansion pipelines, environment construction patterns, error helper reuse, deployment topology) was missed. The agent also introduced a subtle test correctness bug: its timeout test used a pre-cancelled context (yielding context.Canceled) instead of a genuine deadline expiry (DeadlineExceeded).

Review agent vs human reviewer

  • Matched findings: 5 (composition carry-forward, test adequacy, scope, shell-injection, env exposure)
  • Agent-only valid findings: 5 (context/timeout self-escalation was the standout — went from Low to HIGH across passes by finding codebase conventions)
  • Human-only findings (agent gaps): 6 — ${VAR} expansion bypass, RunnerEnv parity, discarded exec error, scaffold dead code, security pipeline bypass, DeadlineExceeded branch coverage
  • Gap rate on MEDIUM+ findings: 55% (6/11)

The meta-pattern across 4 of 6 gaps: the review agent analyzed each code site in isolation rather than asking "what pipeline does the sibling field go through, and does this new field go through the same pipeline?"

Evidence for existing issues

Autonomy assessment

This PR does not support relaxing human review requirements for harness schema + CLI runner changes. The 55% gap rate on MEDIUM+ findings, including correctness and security gaps, demonstrates the review agent cannot yet substitute for human review on cross-cutting feature PRs touching these paths. The agent's self-escalation capability (context/timeout finding) is a genuine strength but insufficient alone.

Proposals filed

waynesun09 added a commit that referenced this pull request Jul 24, 2026
The dispatch "Check for existing PRs" guard used --search "N in:title,body",
which matches any PR that mentions the issue number anywhere in its body —
including unrelated cross-references — and skipped code dispatch whenever
one existed. Its bot-authored-PR exclusion also never worked: GraphQL's
Bot.login omits the REST "[bot]" suffix, so the literal string comparison
against "fullsend-ai-coder[bot]" never matched.

Replace both with a GraphQL query for closedByPullRequestsReferences,
which only returns PRs that actually close the issue via Fixes/Closes/
Resolves keywords, and match the bot exclusion on __typename == "Bot"
instead of a REST-formatted login string. The query also returns MERGED
PRs regardless of includeClosedPrs, so filter on .state == "OPEN"
explicitly — otherwise a long-merged closer would permanently block
re-dispatch on a reopened issue. Add issues: read to both jobs'
permissions, since the query now resolves through the Issue type rather
than pull-requests-only fields. Separate stderr from the query's stdout
so an incidental warning on an otherwise-successful call can't pollute
the result and trigger a false-positive skip.

internal/scaffold/fullsend-repo/scripts/pre-code.sh had the identical
bug (same substring search, same broken bot-login comparison) and runs
as a second gate in the same code-dispatch pipeline, so the original
false-positive-skip failure mode could still recur through that path.
Apply the same fix there and correct pre-code-test.sh's mock fixtures,
which hardcoded the REST-suffixed bot login format and so validated the
wrong assumption about what the API actually returns.

Verified against production data: #5569 (false
positive: PR #5192 only mentions the issue, now correctly proceeds),
#1320 (historical: closed by now-merged PR #2373, now correctly
proceeds since the closer is no longer open), #5560 (bot-authored
closer, correctly excluded), and #5575 itself (open, human-authored PR
#5578 with "Fixes #5575" in its body, correctly detected as blocking).

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
waynesun09 added a commit that referenced this pull request Jul 29, 2026
The dispatch "Check for existing PRs" guard used --search "N in:title,body",
which matches any PR that mentions the issue number anywhere in its body —
including unrelated cross-references — and skipped code dispatch whenever
one existed. Its bot-authored-PR exclusion also never worked: GraphQL's
Bot.login omits the REST "[bot]" suffix, so the literal string comparison
against "fullsend-ai-coder[bot]" never matched.

Replace both with a GraphQL query for closedByPullRequestsReferences,
which only returns PRs that actually close the issue via Fixes/Closes/
Resolves keywords, and match the bot exclusion on __typename == "Bot"
instead of a REST-formatted login string. The query also returns MERGED
PRs regardless of includeClosedPrs, so filter on .state == "OPEN"
explicitly — otherwise a long-merged closer would permanently block
re-dispatch on a reopened issue. Add issues: read to both jobs'
permissions, since the query now resolves through the Issue type rather
than pull-requests-only fields. Separate stderr from the query's stdout
so an incidental warning on an otherwise-successful call can't pollute
the result and trigger a false-positive skip.

internal/scaffold/fullsend-repo/scripts/pre-code.sh had the identical
bug (same substring search, same broken bot-login comparison) and runs
as a second gate in the same code-dispatch pipeline, so the original
false-positive-skip failure mode could still recur through that path.
Apply the same fix there and correct pre-code-test.sh's mock fixtures,
which hardcoded the REST-suffixed bot login format and so validated the
wrong assumption about what the API actually returns.

Verified against production data: #5569 (false
positive: PR #5192 only mentions the issue, now correctly proceeds),
proceeds since the closer is no longer open), #5560 (bot-authored
closer, correctly excluded), and #5575 itself (open, human-authored PR

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
waynesun09 added a commit that referenced this pull request Jul 29, 2026
The dispatch "Check for existing PRs" guard used --search "N in:title,body",
which matches any PR that mentions the issue number anywhere in its body —
including unrelated cross-references — and skipped code dispatch whenever
one existed. Its bot-authored-PR exclusion also never worked: GraphQL's
Bot.login omits the REST "[bot]" suffix, so the literal string comparison
against "fullsend-ai-coder[bot]" never matched.

Replace both with a GraphQL query for closedByPullRequestsReferences,
which only returns PRs that actually close the issue via Fixes/Closes/
Resolves keywords, and match the bot exclusion on __typename == "Bot"
instead of a REST-formatted login string. The query also returns MERGED
PRs regardless of includeClosedPrs, so filter on .state == "OPEN"
explicitly — otherwise a long-merged closer would permanently block
re-dispatch on a reopened issue. Add issues: read to both jobs'
permissions, since the query now resolves through the Issue type rather
than pull-requests-only fields. Separate stderr from the query's stdout
so an incidental warning on an otherwise-successful call can't pollute
the result and trigger a false-positive skip.

internal/scaffold/fullsend-repo/scripts/pre-code.sh had the identical
bug (same substring search, same broken bot-login comparison) and runs
as a second gate in the same code-dispatch pipeline, so the original
false-positive-skip failure mode could still recur through that path.
Apply the same fix there and correct pre-code-test.sh's mock fixtures,
which hardcoded the REST-suffixed bot login format and so validated the
wrong assumption about what the API actually returns.

Verified against production data: #5569 (false
positive: PR #5192 only mentions the issue, now correctly proceeds),
#1320 (historical: closed by now-merged PR #2373, correctly proceeds
since the closer is no longer open), #5560 (bot-authored closer,
correctly excluded), and #5575 itself (open, human-authored PR #5578
with a "Fixes" keyword, correctly detected as blocking).

Also bump closedByPullRequestsReferences's first from 20 to 100 (the
connection's API max) at all three call sites, since a long-lived,
repeatedly-reopened issue could otherwise silently truncate past the
20th closing-PR reference; add null-safety around the nodes array and
author login so a missing field degrades gracefully instead of erroring
or printing "null"; sanitize captured stderr before interpolating it
into a ::warning:: workflow command; fix a pre-code-test.sh case that
mocked a query failure while asserting the "no linked PRs" behavior;
and document a third bot-login format returned by gh's own --json
output (app/<slug> with a separate is_bot flag).

Assisted-by: Claude (fix, review), Grok (review)
Signed-off-by: Wayne Sun <gsun@redhat.com>
waynesun09 added a commit that referenced this pull request Jul 29, 2026
The dispatch "Check for existing PRs" guard used --search "N in:title,body",
which matches any PR that mentions the issue number anywhere in its body —
including unrelated cross-references — and skipped code dispatch whenever
one existed. Its bot-authored-PR exclusion also never worked: GraphQL's
Bot.login omits the REST "[bot]" suffix, so the literal string comparison
against "fullsend-ai-coder[bot]" never matched.

Replace both with a GraphQL query for closedByPullRequestsReferences,
which only returns PRs that actually close the issue via Fixes/Closes/
Resolves keywords, and match the bot exclusion on __typename == "Bot"
instead of a REST-formatted login string. The query also returns MERGED
PRs regardless of includeClosedPrs, so filter on .state == "OPEN"
explicitly — otherwise a long-merged closer would permanently block
re-dispatch on a reopened issue. Add issues: read to both jobs'
permissions, since the query now resolves through the Issue type rather
than pull-requests-only fields. Separate stderr from the query's stdout
so an incidental warning on an otherwise-successful call can't pollute
the result and trigger a false-positive skip.

internal/scaffold/fullsend-repo/scripts/pre-code.sh had the identical
bug (same substring search, same broken bot-login comparison) and runs
as a second gate in the same code-dispatch pipeline, so the original
false-positive-skip failure mode could still recur through that path.
Apply the same fix there and correct pre-code-test.sh's mock fixtures,
which hardcoded the REST-suffixed bot login format and so validated the
wrong assumption about what the API actually returns.

Verified against production data: #5569 (false
positive: PR #5192 only mentions the issue, now correctly proceeds),
#1320 (historical: closed by now-merged PR #2373, correctly proceeds
since the closer is no longer open), #5560 (bot-authored closer,
correctly excluded), and #5575 itself (open, human-authored PR #5578
with a "Fixes" keyword, correctly detected as blocking).

Also bump closedByPullRequestsReferences's first from 20 to 100 (the
connection's API max) at all three call sites, since a long-lived,
repeatedly-reopened issue could otherwise silently truncate past the
20th closing-PR reference; add null-safety around the nodes array and
author login so a missing field degrades gracefully instead of erroring
or printing "null"; sanitize captured stderr before interpolating it
into a ::warning:: workflow command; fix a pre-code-test.sh case that
mocked a query failure while asserting the "no linked PRs" behavior;
and document a third bot-login format returned by gh's own --json
output (app/<slug> with a separate is_bot flag).

Assisted-by: Claude (fix, review), Grok (review)
Signed-off-by: Wayne Sun <gsun@redhat.com>
waynesun09 added a commit that referenced this pull request Aug 7, 2026
The dispatch "Check for existing PRs" guard used --search "N in:title,body",
which matches any PR that mentions the issue number anywhere in its body —
including unrelated cross-references — and skipped code dispatch whenever
one existed. Its bot-authored-PR exclusion also never worked: GraphQL's
Bot.login omits the REST "[bot]" suffix, so the literal string comparison
against "fullsend-ai-coder[bot]" never matched.

Replace both with a GraphQL query for closedByPullRequestsReferences,
which only returns PRs that actually close the issue via Fixes/Closes/
Resolves keywords, and match the bot exclusion on __typename == "Bot"
instead of a REST-formatted login string. The query also returns MERGED
PRs regardless of includeClosedPrs, so filter on .state == "OPEN"
explicitly — otherwise a long-merged closer would permanently block
re-dispatch on a reopened issue. Add issues: read to both jobs'
permissions, since the query now resolves through the Issue type rather
than pull-requests-only fields. Separate stderr from the query's stdout
so an incidental warning on an otherwise-successful call can't pollute
the result and trigger a false-positive skip.

internal/scaffold/fullsend-repo/scripts/pre-code.sh had the identical
bug (same substring search, same broken bot-login comparison) and runs
as a second gate in the same code-dispatch pipeline, so the original
false-positive-skip failure mode could still recur through that path.
Apply the same fix there and correct pre-code-test.sh's mock fixtures,
which hardcoded the REST-suffixed bot login format and so validated the
wrong assumption about what the API actually returns.

Verified against production data: #5569 (false
positive: PR #5192 only mentions the issue, now correctly proceeds),
proceeds since the closer is no longer open), #5560 (bot-authored
closer, correctly excluded), and #5575 itself (open, human-authored PR

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
waynesun09 added a commit that referenced this pull request Aug 7, 2026
The dispatch "Check for existing PRs" guard used --search "N in:title,body",
which matches any PR that mentions the issue number anywhere in its body —
including unrelated cross-references — and skipped code dispatch whenever
one existed. Its bot-authored-PR exclusion also never worked: GraphQL's
Bot.login omits the REST "[bot]" suffix, so the literal string comparison
against "fullsend-ai-coder[bot]" never matched.

Replace both with a GraphQL query for closedByPullRequestsReferences,
which only returns PRs that actually close the issue via Fixes/Closes/
Resolves keywords, and match the bot exclusion on __typename == "Bot"
instead of a REST-formatted login string. The query also returns MERGED
PRs regardless of includeClosedPrs, so filter on .state == "OPEN"
explicitly — otherwise a long-merged closer would permanently block
re-dispatch on a reopened issue. Add issues: read to both jobs'
permissions, since the query now resolves through the Issue type rather
than pull-requests-only fields. Separate stderr from the query's stdout
so an incidental warning on an otherwise-successful call can't pollute
the result and trigger a false-positive skip.

internal/scaffold/fullsend-repo/scripts/pre-code.sh had the identical
bug (same substring search, same broken bot-login comparison) and runs
as a second gate in the same code-dispatch pipeline, so the original
false-positive-skip failure mode could still recur through that path.
Apply the same fix there and correct pre-code-test.sh's mock fixtures,
which hardcoded the REST-suffixed bot login format and so validated the
wrong assumption about what the API actually returns.

Verified against production data: #5569 (false
positive: PR #5192 only mentions the issue, now correctly proceeds),
proceeds since the closer is no longer open), #5560 (bot-authored
closer, correctly excluded), and #5575 itself (open, human-authored PR

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component/harness Agent harness, config, and skills loading component/runner Agent runner behavior and lifecycle ok-to-test Allow e2e CI to run after maintainer review (must be re-applied after each push) ready-for-review Triggers review agent dispatch requires-manual-review Review requires human judgment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants