Skip to content

feat(dispatch): let a repository preserve the agent run already in flight - #7007

Open
waynesun09 wants to merge 9 commits into
mainfrom
preserve-runs
Open

feat(dispatch): let a repository preserve the agent run already in flight#7007
waynesun09 wants to merge 9 commits into
mainfrom
preserve-runs

Conversation

@waynesun09

@waynesun09 waynesun09 commented Sep 4, 2026

Copy link
Copy Markdown
Member

Summary

Every agent stage job cancels the run working on a work item as soon as a second event arrives for it. This lets a repository choose otherwise: the run in flight finishes, and the newer event waits as the single pending run GitHub Actions allows, which then works from the item's current state.

Default behaviour is unchanged. FULLSEND_PRESERVE_RUNS unset, which is everywhere today, still cancels.

Scope

In: the concurrency expression on the seven reusable-dispatch.yml stage jobs, and the two run facts an agent needs to tell what changed underneath it (FULLSEND_RUN_HEAD_SHA, FULLSEND_RUN_STARTED_AT), exported through bootstrapEnv after .env.d is sourced so a harness env file cannot replace them. Note they are not fully protected: reservedSandboxKeys guards env.sandbox only, and an unvalidated host_files destination can still overwrite the runner's env file. That gap is pre-existing and repo-wide — FULLSEND_ROLE and FULLSEND_SLUG sit above the same line — and is tracked separately in #7010 rather than fixed here.

Not in, deliberately: anything that talks to a running agent. No watcher, no receipt, no /fs-steer, no runtime changes. When nothing steers, the run queued behind simply does the work, so there is nothing to skip and no receipt to get wrong. That half is #6959, which is stacked on this branch and is not a prerequisite for this one.

The other half of the bargain is fullsend-ai/agents#1163: once the run in flight is preserved, it can outlive the state it was dispatched on, so the agent re-checks the work item once before writing its result. It skips that check when these variables are empty, so the two merge in either order.

Why

A restart is not only tokens. It pays sandbox provisioning and bootstrap before the model reads anything, then re-reads the whole item from cold. On #6513, six force-pushes produced five completed reviews of commits that were superseded within minutes, none of which led to a change.

Relation to ADR 0098 (#6909)

That ADR proposes preserve-and-coalesce as policy. It is open, and it and this change were written in parallel against the same feedback rather than against each other, so this is not offered as its implementation. If it is accepted, the natural follow-up is to drop the variable and make the behaviour unconditional.

Testing

  • make lint passes (pre-commit over the full range)
  • Tests added: the alignment test now asserts the exact expression every stage job carries, and runfacts_test.go covers the exports, UTC normalisation, quote escaping, the empty head on an issue run, and that GITHUB_SHA is never used as a fallback

Every commit builds and vets on its own.

Checklist

  • PR title follows Conventional Commits
  • Commits are signed off (DCO)
  • I wrote this contribution myself and can explain all changes in it

Refs #6957

Review round

A Codex gpt-5.6-sol pass (one reviewer, not the full squad) produced five findings, all confirmed and addressed:

  • Run facts were overrideable through .env.d; they now export below that line, beside the env.sandbox block that already sits there for the same reason. The general reserved-key gap is reservedSandboxKeys only guards env.sandbox, not .env.d or host_files dest #7010.
  • Opting in gives something up, and the PR now says so in three places. Preserving the run in flight removes the per-stage cancel-in-progress duplicate-dispatch mitigation that accepted ADR 0063 relies on, leaving the Jira lock and agent idempotency. The duplicate-poll case is where preserving is least defensible, because two duplicate dispatches are the same work rather than a newer state superseding an older one. All seven jobs stay consistent; an exemption for harness-run was considered and rejected, since a job quietly ignoring the variable is harder to reason about than the trade.
  • The alignment test pinned six of the seven jobs; it now pins all seven, and asserts scalar types so a literal boolean cannot silently become a quoted string, or the gate a bare boolean.
  • FULLSEND_PRESERVE_RUNS is documented for operators in the repo-variable table, including that the comparison is case-insensitive: TRUE and True preserve, while 1 and yes cancel.
  • The run start is captured before the setup work that precedes it. The honest baseline is the workflow run's server-side created_at rather than any host clock inside the process, which is noted in the code.

The export ordering is now load-bearing, so bootstrapEnv's script assembly was split into a testable function and pinned by a test, rather than being asserted only by a comment.

Rebased onto current main. functional-tests was red here for a reason outside this change: main allowlists **/claude.exe on the Vertex egress profile and this branch predated that, so OPA denied the agent binary and every triage eval case failed at zero cost. The rebase picks that fix up. The e2e red is separate and unrelated, a pool-repo race in TestAdminInstallUninstall (422 expected head sha didn't match current head ref).

Rebased onto current main after this PR went DIRTY. The conflict was #7042, which exports its own runner-owned values into the sandbox and reasons about ordering against harness-controlled entries the same way this PR does. Three conflicts, all in internal/cli/run.go: the reserved-key map took the union of both pairs of keys; bootstrapEnv's neighbourhood kept #7042's block plus this PR's runFacts type under one merged signature; and the third was that #7042's iteration-env line and this PR's run-facts export both claimed to be last, for the same stated reason — coming after every harness-controlled entry so nothing can shadow them.

They export disjoint variables, so neither shadows the other, but only one can be last. #7042's sources a file rewritten before every iteration, so its position is load-bearing; the run facts only ever needed to be after .env.d. Final order is .env.denv.sandbox → run facts → iteration env source, and both comments now state which invariant they hold rather than both asserting primacy. The ordering test asserts "after .env.d" rather than "last", so it survives the reordering while still constraining the property that matters; it was mutation-checked after the rebase, since a replay can quietly make an assertion vacuous.

@waynesun09

Copy link
Copy Markdown
Member Author

Stacked: this is the base. #6959 sits on top of it and carries the steering half. Reviewing this one alone is meaningful; reviewing #6959 without this one is not.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Optionally preserve in-flight agent stage runs

✨ Enhancement ⚙️ Configuration changes 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Lets repositories preserve active stage runs while retaining cancellation as the default.
• Exports run head and start time for detecting intervening work-item changes.
• Adds concurrency, run-fact, escaping, and forge-specific baseline coverage.
Diagram

graph TD
  A["Repository event"] --> B{"Preserve runs?"}
  B -- "No" --> C["Cancel active run"] --> D["Newest stage run"] --> G["CLI run baseline"] --> H["Agent sandbox"]
  B -- "Yes" --> E["Finish active run"] --> F["Pending stage run"] --> G
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Make preservation unconditional
  • ➕ Provides one consistent concurrency policy across repositories
  • ➕ Eliminates a long-lived configuration toggle
  • ➖ Changes existing behavior before ADR 0098 is accepted
  • ➖ Requires consumers to support state reconciliation safely
2. Add runtime steering
  • ➕ Could redirect active agents instead of waiting for a subsequent run
  • ➕ May reduce duplicated work after rapid events
  • ➖ Requires watcher, receipt, and runtime protocol changes
  • ➖ Introduces substantially more failure modes than queue coalescing
3. Retain cancel-and-restart behavior
  • ➕ Preserves established semantics without agent-side reconciliation
  • ➕ Always starts from the newest dispatch context
  • ➖ Discards completed work during event bursts
  • ➖ Repeats sandbox provisioning, bootstrap, and item analysis

Recommendation: The opt-in preserve-and-coalesce approach is best while policy and agent reconciliation support are still rolling out. It preserves compatibility, relies on GitHub Actions' existing single-pending-run semantics, and exports only the baseline facts agents need; preservation can become unconditional later if ADR 0098 is accepted.

Files changed (6) +187 / -23

Enhancement (1) +56 / -2
run.goExport immutable run baseline facts to sandboxes +56/-2

Export immutable run baseline facts to sandboxes

• Captures the run start time and resolves pull or merge request head SHAs for GitHub and GitLab. It exports both values safely into the sandbox and reserves their keys against harness overrides, deliberately excluding GITHUB_SHA as a fallback.

internal/cli/run.go

Tests (3) +106 / -11
run_test.goPass run facts through existing bootstrap tests +4/-4

Pass run facts through existing bootstrap tests

• Updates existing bootstrapEnv test calls for the new runFacts argument without changing their original assertions.

internal/cli/run_test.go

runfacts_test.goCover run baseline extraction and environment exports +85/-0

Cover run baseline extraction and environment exports

• Tests UTC normalization, empty issue heads, shell-quote escaping, GitHub and GitLab SHA sources, and exclusion of the base GITHUB_SHA. It also verifies that both exported keys remain reserved.

internal/cli/runfacts_test.go

workflow_call_alignment_test.goEnforce exact stage preservation expressions +17/-7

Enforce exact stage preservation expressions

• Changes concurrency decoding from bool to yaml.Node so tests can handle both expressions and literals. It asserts the exact preservation-gated expression on dispatch stages while retaining literal checks for reusable workflows, thin callers, and shims.

internal/scaffold/workflow_call_alignment_test.go

Documentation (1) +1 / -0
ci-workflows.mdDocument agent-stage concurrency policy +1/-0

Document agent-stage concurrency policy

• Documents the repository-level preservation option, its default behavior, and the requirement that every dispatch stage use the identical expression.

docs/contributing/ci-workflows.md

Other (1) +24 / -10
reusable-dispatch.ymlGate stage cancellation on repository preservation policy +24/-10

Gate stage cancellation on repository preservation policy

• Replaces hard-coded cancellation on all seven stage jobs with FULLSEND_PRESERVE_RUNS-based expressions. The default continues cancelling active runs, while an explicit true value preserves the active run and queues the newest event.

.github/workflows/reusable-dispatch.yml

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 4, 2026

Copy link
Copy Markdown

🤖 Review · ❌ Terminated · Started 2:21 PM UTC · Ended 3:03 PM UTC

Commit: c45f2ad · View workflow run →

@qodo-code-review

qodo-code-review Bot commented Sep 4, 2026

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. Run timestamp starts too late ✓ Resolved 🐞 Bug ≡ Correctness
Description
FULLSEND_RUN_STARTED_AT is captured only after harness resolution, token minting, environment
expansion, and validation, so work-item activity during that setup is incorrectly classified as
predating the run. A preserved run can therefore miss an update that occurred after runAgent began
and write a stale result.
Code

internal/cli/run.go[R1125-1127]

+	// The instant this run started, exported into the sandbox so the agent
+	// can tell which activity on the work item postdates it.
+	runStartedAt := time.Now().UTC()
Relevance

●●● Strong

Recent run.go correctness findings about lifecycle boundaries and preserving run state are accepted.

PR-#1682
PR-#1780

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
runAgent begins at line 483 and performs environment loading and harness resolution from lines 496
onward; token minting and environment validation also occur at lines 865-920. The new timestamp is
not captured until lines 1125-1127, despite being documented and exported as the instant the run
started.

internal/cli/run.go[483-524]
internal/cli/run.go[865-920]
internal/cli/run.go[1125-1127]
internal/cli/run.go[1811-1812]

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

## Issue description
Capture `runStartedAt` at the beginning of `runAgent`, before setup work that may take significant time, so the exported baseline covers the complete run lifecycle.

## Issue Context
The current assignment occurs after harness resolution, token minting, environment expansion, and validation. Updates arriving during those operations can be excluded from the agent's final reconciliation window.

## Fix Focus Areas
- internal/cli/run.go[483-500]
- internal/cli/run.go[1125-1127]
- internal/cli/run.go[1811-1812]

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



Remediation recommended

2. Run facts remain shadowable ✓ Resolved 🐞 Bug ≡ Correctness
Description
bootstrapEnv emits the reserved run facts before sourcing .env.d/*.env, allowing any expanded
harness host_files environment file to overwrite them. The agent can consequently receive
fabricated or stale baselines despite these keys being reserved against env.sandbox shadowing.
Code

internal/cli/run.go[R2770-2772]

+	// Expose this run's baseline so the agent can re-check, once, whether the
+	// work item moved under it before it writes its result.
+	lines = append(lines, buildRunFactsEnvLines(facts)...)
Relevance

●●● Strong

The repository accepted protections against environment shadowing and explicitly recognized .env.d
precedence risks.

PR-#2582
PR-#5837

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The reservation check only filters env.sandbox keys in buildSandboxEnvLines. Run facts are
appended at lines 2770-2772, but .env.d files are sourced afterward at lines 2812-2813; those
files are populated from harness host_files, whose contents may contain assignments or exports.

internal/cli/run.go[2645-2705]
internal/cli/run.go[2770-2772]
internal/cli/run.go[2812-2818]
internal/cli/run.go[2838-2865]

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

## Issue description
Ensure runner-owned run facts take precedence over both `.env.d` files and `env.sandbox` values by exporting them after all user-configurable environment sources.

## Issue Context
The reserved-key validation applies only to `env.sandbox`. Expanded `host_files` are sourced later from `.env.d`, so they can currently redefine either run-fact variable.

## Fix Focus Areas
- internal/cli/run.go[2645-2669]
- internal/cli/run.go[2770-2772]
- internal/cli/run.go[2812-2818]
- internal/cli/run.go[2838-2865]

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


Grey Divider

Context sources
✅ Compliance rules (platform): 67 rules
Review mode: ⚖️ Balanced: This changes GitHub Actions concurrency behavior and runtime environment exports across multiple paths, creating genuine CI and agent-state risks, but the scope remains cohesive enough for one careful review.

Grey Divider

Tip of the day
💡 Did you know, you can show, collapse, or hide each part of a finding: code, evidence, and all

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread internal/cli/run.go Outdated
Comment thread internal/cli/run.go Outdated
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

Site preview

Preview: https://19b2668e-site.fullsend-ai.workers.dev

Commit: 3ee26f777af52b6099a2c400c92ef0ef44312818

@codecov

codecov Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@fullsend-ai-review

Copy link
Copy Markdown

🤖 Finished Review · ❌ Failure (validation failed after 2 iteration(s)) · Started 2:21 PM UTC · Completed 3:03 PM UTC

Commit: c45f2ad · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $6.04

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 4, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 3:15 PM UTC · Ended 3:28 PM UTC

Commit: e56312e · View workflow run →

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 4, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 3:30 PM UTC · Completed 3:52 PM UTC

Commit: 9085c4d · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $5.53

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 4, 2026

Copy link
Copy Markdown

Risk Assessment: elevated (3/5)

Details

Re-review anchoring preserves the prior score of 3 (elevated): Tier 1 signals are unchanged (CI workflow modified, one protected path, moderate-large change size, decent test ratio), Tier 2 confirms run.go and reusable-dispatch.yml remain extreme churn hotspots under heavy multi-author contention, and Tier 3 retains a rollback-safety concern from the behavioral concurrency toggle — no articulable reason exists to deviate from the prior elevated assessment.

Previous run

Risk Assessment: elevated (3/5)

Details

Tier 1 signals are unchanged from the prior assessment (CI workflow modified, one protected path, moderate-large change size, decent test ratio); Tier 2 confirms run.go and reusable-dispatch.yml are extreme churn hotspots under heavy multi-author contention, and Tier 3 flags a rollback-safety concern from the behavioral concurrency toggle change -- all consistent with the prior score of 3 (elevated), which is preserved under re-review anchoring.

Previous run (2)

Risk Assessment: elevated (3/5)

Details

PR modifies critical workflow infrastructure (reusable-dispatch.yml) and core CLI logic (run.go) with CI workflow changes and one protected path file. Elevated by active development on run.go and the behavioral nature of the concurrency toggle change.

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 4, 2026

Copy link
Copy Markdown

Review

Findings

Medium

  • [protected-path] .github/workflows/reusable-dispatch.yml — This PR modifies a file under the .github/ protected path. The PR links to issue Steer the in-flight agent run on work-item updates instead of cancelling it #6957 and the description explains the rationale (making cancel-in-progress conditional on FULLSEND_PRESERVE_RUNS to let repositories preserve in-flight agent runs). Human approval is always required for protected-path changes, regardless of context.

  • [missing-doc] docs/guides/user/building-custom-agents.md:74 — The section "Environment variables set by the runner, present in every agent's shell" lists FULLSEND_TIMEOUT_MINUTES and FULLSEND_ITERATION_DEADLINE but omits FULLSEND_RUN_HEAD_SHA and FULLSEND_RUN_STARTED_AT, both of which are now unconditionally exported into every agent sandbox by buildRunFactsEnvLines() regardless of whether FULLSEND_PRESERVE_RUNS is set.
    Remediation: Add FULLSEND_RUN_HEAD_SHA (the PR/MR head SHA at run start; empty for issue runs) and FULLSEND_RUN_STARTED_AT (RFC 3339 UTC timestamp of run start) to the env-var list in this section.

  • [missing-doc] docs/reference/harness-reference.md:146 — The timeout_minutes field description states that FULLSEND_TIMEOUT_MINUTES and FULLSEND_ITERATION_DEADLINE are reserved env.sandbox keys. The PR adds FULLSEND_RUN_HEAD_SHA and FULLSEND_RUN_STARTED_AT to reservedSandboxKeys with the same protection, but neither is mentioned in the harness reference. Harness authors attempting to set either key via env.sandbox will have it silently dropped with no indication the name is reserved.
    Remediation: Extend the reserved-key note to include FULLSEND_RUN_HEAD_SHA and FULLSEND_RUN_STARTED_AT.

  • [missing-doc] docs/cli/run.md:130 — The "Budget and deadline" section documents only FULLSEND_TIMEOUT_MINUTES and FULLSEND_ITERATION_DEADLINE. FULLSEND_RUN_HEAD_SHA and FULLSEND_RUN_STARTED_AT are also now unconditionally set in every sandbox but are not documented in this CLI reference page.
    Remediation: Add a new subsection (e.g., "Run identity") documenting FULLSEND_RUN_HEAD_SHA and FULLSEND_RUN_STARTED_AT, noting their relevance to FULLSEND_PRESERVE_RUNS reconciliation.

Low

  • [stale-doc] docs/guides/dev/cli-internals.md:433 — The ASCII flow diagram under "bootstrapEnv() writes:" does not include FULLSEND_RUN_HEAD_SHA or FULLSEND_RUN_STARTED_AT. The PR refactors bootstrapEnv into bootstrapEnv + buildEnvScriptLines + buildRunFactsEnvLines + uploadHostFiles and inserts the run-facts export block between .env.d sourcing and the iteration-env source line; the diagram reflects none of this.
    Remediation: Update the diagram to include FULLSEND_RUN_HEAD_SHA and FULLSEND_RUN_STARTED_AT after .env.d sourcing and before iteration.env.

  • [missing-doc] docs/guides/user/bring-your-own-agent.md:162 — The paragraph enumerating runner-exported variables (FULLSEND_TIMEOUT_MINUTES, FULLSEND_ITERATION_DEADLINE) omits FULLSEND_RUN_HEAD_SHA and FULLSEND_RUN_STARTED_AT.
    Remediation: Add a brief mention alongside the budget variables.

  • [naming-convention] internal/cli/runfacts_test.go:1 — The filename runfacts_test.go does not follow the established topic-split naming convention for run.go tests. Existing peers use run_<topic>_test.go (run_models_aliases_test.go, run_openai_test.go, run_overrides_test.go) or <topic>_run_test.go (prescript_run_test.go, telemetry_run_test.go).
    Remediation: Rename to run_facts_test.go to align with the run_<topic>_test.go pattern.

  • [defense-in-depth] internal/cli/run.go:3066 — Pre-existing gap acknowledged in the PR: a host_files entry whose destination is the runner's own .env file can replace it wholesale, bypassing both reservedSandboxKeys and ordering-based protection for the new run facts. This gap is repo-wide (not introduced by this PR) and is tracked in reservedSandboxKeys only guards env.sandbox, not .env.d or host_files dest #7010.

  • [scope-authorization-premature] .github/workflows/reusable-dispatch.yml:47 — The FULLSEND_PRESERVE_RUNS opt-in ships while ADR 0098 remains open. Since the prior review, ci-workflows.md has been substantially extended with explicit guidance on what preserving gives up (ADR 0063 layer removal, duplicate-poll hazard), and the PR body explicitly disavows being ADR 0098's implementation.

  • [architecture-coherence] internal/cli/run.go:169FULLSEND_RUN_STARTED_AT captures a host-side clock value, not the workflow run's server-side created_at. The two halves of the preserve-and-reconcile system will reference different absolute timestamps. The risk is conservative — the window of undetected updates is bounded and small — but the architectural split between the two baselines is not spelled out in the issue or any ADR.


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

Medium

  • [protected-path] .github/workflows/reusable-dispatch.yml — This PR modifies a file under the .github/ protected path. The PR links to issue Steer the in-flight agent run on work-item updates instead of cancelling it #6957 and the description explains the rationale (making cancel-in-progress conditional on FULLSEND_PRESERVE_RUNS to let repositories preserve in-flight agent runs). Human approval is always required for protected-path changes, regardless of context.

  • [missing-doc] docs/guides/user/building-custom-agents.md:74 — The section "Environment variables set by the runner, present in every agent's shell" lists FULLSEND_TIMEOUT_MINUTES and FULLSEND_ITERATION_DEADLINE but omits FULLSEND_RUN_HEAD_SHA and FULLSEND_RUN_STARTED_AT, both of which are now unconditionally exported into every agent sandbox by buildRunFactsEnvLines() regardless of whether FULLSEND_PRESERVE_RUNS is set. Agent authors writing custom agents who want to implement the reconciliation check will have no documented API to discover.
    Remediation: Add FULLSEND_RUN_HEAD_SHA (the PR/MR head SHA at run start; empty for issue runs) and FULLSEND_RUN_STARTED_AT (RFC 3339 UTC timestamp of run start) to the env-var list in this section.

  • [missing-doc] docs/reference/harness-reference.md:146 — The timeout_minutes field description explicitly states "Both names are reserved: an env.sandbox entry with either name is dropped" for FULLSEND_TIMEOUT_MINUTES and FULLSEND_ITERATION_DEADLINE. The PR adds FULLSEND_RUN_HEAD_SHA and FULLSEND_RUN_STARTED_AT to reservedSandboxKeys with the same protection, but neither is mentioned in the harness reference. Harness authors attempting to set either key via env.sandbox will have it silently dropped with no indication the name is reserved.
    Remediation: Add a note that FULLSEND_RUN_HEAD_SHA and FULLSEND_RUN_STARTED_AT are also reserved.

  • [stale-doc] docs/guides/dev/cli-internals.md:433 — The ASCII flow diagram under "bootstrapEnv() writes:" lists the environment variables the function writes into the sandbox .env, ending with the iteration env sourcing. The PR refactors bootstrapEnv into buildEnvScriptLines + uploadHostFiles and adds buildRunFactsEnvLines() that writes FULLSEND_RUN_HEAD_SHA and FULLSEND_RUN_STARTED_AT between .env.d sourcing and the iteration env line. The diagram does not reflect these new entries.
    Remediation: Update the diagram to include FULLSEND_RUN_HEAD_SHA and FULLSEND_RUN_STARTED_AT between the .env.d sourcing and iteration.env lines.

Low

  • [test-adequacy] internal/scaffold/workflow_call_alignment_test.go:577TestThinCallerStageConcurrency checks CancelInProgress.Value but not CancelInProgress.Tag after the type changed from bool to yaml.Node. The analogous tests for reusable-agent workflows (line 549) and shim-labeled-event filtering (line 583) both assert Tag == "!!bool" with an explicit comment that "Node.Value cannot tell them apart" between a YAML boolean true and a YAML string "true". The thin caller test omits this Tag assertion.
    Remediation: Add assert.Equal(t, "!!bool", stageJob.Concurrency.CancelInProgress.Tag, "must be a YAML boolean, not a quoted string") to TestThinCallerStageConcurrency.

  • [scope-authorization-premature] .github/workflows/reusable-dispatch.yml:47 — The PR implements opt-in infrastructure for preserving runs (FULLSEND_PRESERVE_RUNS) while ADR 0098 is referenced as pending policy. The opt-in surface ships before its governing ADR is accepted, which could constrain the ADR outcome.
    Remediation: Consider documenting in the ADR (when it lands) that this PR's opt-in is a deliberate pre-policy escape hatch.

  • [naming-convention] internal/cli/runfacts_test.go:1 — The test file runfacts_test.go doesn't follow the established naming convention for topic-split test files covering code in run.go. The package uses run_<topic>_test.go (e.g., run_models_aliases_test.go) or <topic>_run_test.go (e.g., telemetry_run_test.go). The new file omits the run component.
    Remediation: Rename to run_facts_test.go to align with the run_<topic>_test.go pattern.

  • [defense-in-depth] internal/cli/run.go:3066 — Pre-existing gap acknowledged in the PR: a host_files entry whose destination is the runner's own .env file can replace it wholesale, bypassing both reservedSandboxKeys and ordering-based protection for the new run facts. This gap is repo-wide (not introduced by this PR) and is tracked in reservedSandboxKeys only guards env.sandbox, not .env.d or host_files dest #7010.

  • [missing-doc] docs/guides/user/bring-your-own-agent.md:162 — The paragraph enumerating runner-exported variables (FULLSEND_TIMEOUT_MINUTES, FULLSEND_ITERATION_DEADLINE) omits FULLSEND_RUN_HEAD_SHA and FULLSEND_RUN_STARTED_AT.
    Remediation: Add a brief mention alongside the budget variables.


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

Medium

  • [protected-path] .github/workflows/reusable-dispatch.yml — This PR modifies a file under the .github/ protected path. The PR links to issue Steer the in-flight agent run on work-item updates instead of cancelling it #6957 and the description explains the rationale (making cancel-in-progress conditional on FULLSEND_PRESERVE_RUNS to let repositories preserve in-flight agent runs). Human approval is always required for protected-path changes, regardless of context.

Low

  • [naming-convention] internal/cli/run.gorunFacts struct and buildRunFactsEnvLines function follow the established Go naming conventions and the build*EnvLines pattern in the file. No change needed.
  • [api-shape] internal/cli/run.gobootstrapEnv signature correctly places the required runFacts parameter before the variadic fetchEnv. No change needed.

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 4, 2026

Copy link
Copy Markdown

🤖 Finished Review · ❌ Failure (validation failed after 2 iteration(s)) · Started 7:17 PM UTC · Completed 7:59 PM UTC

Commit: c6c05f8 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high

Every stage job cancels the run working on a work item as soon as a
second event arrives for it. The replacement pays sandbox provisioning
and bootstrap before the model reads anything, then re-reads the whole
item from cold, so a burst of pushes discards finished work and buys
nothing: on #6513 six force-pushes produced five completed reviews of
commits that were superseded within minutes.

FULLSEND_PRESERVE_RUNS lets a repository choose otherwise. Unset — the
default everywhere — is exactly today's behaviour. Set to "true", the
run in flight finishes and the newer event waits as the single pending
run, which then works from the item's current state.

The agent's side of that bargain, reconciling current state rather than
the state that dispatched it, is fullsend-ai/agents#1163, and is inert
until the run facts in the next commit reach the sandbox.

Refs #6957
Assisted-by: Claude

Signed-off-by: Wayne Sun <gsun@redhat.com>

The alignment test decoded cancel-in-progress as a Go bool, which an
expression string cannot unmarshal into, so it moves to a yaml.Node and
asserts the exact expression every stage job must carry.

Signed-off-by: Wayne Sun <gsun@redhat.com>
Once a repository preserves the run in flight, that run can outlive the
state it was dispatched on, so an agent has to be able to tell what
changed underneath it before it writes its result. Export the two facts
it needs: the work item's head at run start, and the instant the run
started.

They go through bootstrapEnv rather than env.sandbox or an env/*.env
file, because .env.d files are sourced afterwards and would expand ${VAR}
host-side to an empty string, and a ${VAR} in harness env.sandbox
hard-fails ValidateRunnerEnvWith for every consumer that does not define
it. Both are reserved so a harness cannot shadow them.

An issue run exports an empty head rather than omitting the variable: the
agent side skips its re-check on an empty value, and absence and emptiness
would otherwise be indistinguishable. The consumer is
fullsend-ai/agents#1163.

Refs #6957
Assisted-by: Claude

Signed-off-by: Wayne Sun <gsun@redhat.com>
The concurrency rules describe workflow-level choices; the stage jobs in
reusable-dispatch.yml now defer that one to the repository. Say so where a
contributor reads the rules, and say that the expression must stay
identical across stage jobs, since a mixed setting would let one role
cancel while another queues on the same work item.

Refs #6957
Assisted-by: Claude

Signed-off-by: Wayne Sun <gsun@redhat.com>
reservedSandboxKeys stops an env.sandbox entry shadowing
FULLSEND_RUN_HEAD_SHA and FULLSEND_RUN_STARTED_AT, but it says nothing about
.env.d, which bootstrapEnv sources after writing them — so a harness
host_files env file could overwrite either value and hand the agent a
fabricated baseline. Position in the generated script is what actually
protects them, which is why the ADR 0055 env.sandbox block already sits
after that line for the same reason. The run facts now sit beside it.

Since the ordering is behaviour rather than formatting, the line assembly
moves into buildEnvScriptLines so a test can pin it; bootstrapEnv keeps the
file write and upload, and host_files copying moves to uploadHostFiles. No
behaviour change beyond the ordering.

This does not close every route: a host_files entry whose dest is the
runner's own .env replaces the file wholesale, and the same exposure applies
to FULLSEND_ROLE and FULLSEND_SLUG, which are written four lines above.
That gap is repo-wide and predates this change, so it is tracked separately
rather than widened into this one.

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
ADR 0063 names per-stage cancel-in-progress as the second of two layers
protecting poll dispatch from duplicate side effects. Setting
FULLSEND_PRESERVE_RUNS removes that layer, leaving the poller's own lock and
whatever idempotency the agent has — and the duplicate-poll case is where the
trade is least favourable, because two duplicate dispatches are the same work
rather than a newer state superseding an older one, so both run to completion
instead of one replacing the other.

Recorded where someone deciding will read it: the workflow's concurrency
comment, and a cross-reference note on ADR 0063 itself, which its rules allow
without rewriting an accepted decision. The default is unchanged, so that
ADR's assumption still holds wherever the variable is unset.

Deliberately documentation and not a code guard, and deliberately not an
exemption for harness-run: all seven stage jobs stay consistent, because a
job that quietly ignored the variable would be harder to reason about than
the trade itself. The repository owner makes this call; our part is to make
sure they make it knowing what they give up.

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
The concurrency test iterated six built-in stages while the workflow has
seven jobs carrying the expression, so harness-run — the matrix fan-out —
was unpinned and a change to it would not have been caught. It is in the map
now, keyed by matrix identity rather than the event payload because a poller
supplies its work item, and the failure message says that a new stage job
has to be added there or its concurrency is unguarded.

Converting CancelInProgress from bool to yaml.Node also lost a property
nobody asked to give up: Node.Value is "true" for both a YAML boolean and
the quoted string "true", and only Tag separates them, so the three literal
sites would have accepted a string and the expression site would have
accepted a bare boolean. Each now asserts Tag alongside Value.

Both were checked against tampered fixtures rather than assumed: setting
harness-run back to a literal true fails on harness-run, and quoting a
reusable workflow's cancel-in-progress fails on the type.

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
The variable was described only in the contributor CI guide. A repository
administrator deciding whether to set it reads the operations guide, which
already carries the repo-variable table it belongs in.

The value semantics are worth stating rather than leaving to be discovered:
GitHub compares strings case-insensitively, so TRUE and True also preserve,
while any other value — including 1 and yes, which someone setting a boolean
flag might reasonably try — cancels. The row points at the CI guide for what
preserving gives up.

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
runStartedAt was taken after harness resolution, base composition, token
minting and env expansion — a mint call retries over the network — so
activity on the work item during setup was exported to the agent as
predating the run, and a preserved run could miss an update it was supposed
to reconcile against.

Moving the capture to the top of runAgent shrinks that window to nothing
within this process, which is the whole fix available here. It is not the
honest baseline and the comment says so: the run was dispatched before the
process started, so the true start is the workflow run's server-side
created_at, which no host clock can reach and which costs an API call to
read. That is deliberately not added here, but it is recorded so the two
halves do not end up disagreeing about what "run start" means — the
follow-up run watcher reached the same conclusion and already uses the run
record's created_at rather than its own clock.

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Sep 5, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 10:29 PM UTC · Completed 10:54 PM UTC

Commit: 6aea370 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $8.65

fullsend-ai-review[bot]

This comment was marked as outdated.

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot removed the requires-manual-review Review requires human judgment label Sep 5, 2026
Four review findings on the base half.

**The GitLab head was empty on most merge request pipelines.** runHeadSHA
returned CI_MERGE_REQUEST_SOURCE_BRANCH_SHA with no fallback, and GitLab's
predefined-variables reference says of it: "The variable is empty in merge
request pipelines. The SHA is present only in merged results pipelines." So
an ordinary MR pipeline exported an empty head — the signal reserved for
issue runs — and the agent could not tell the branch had moved underneath it,
which is the one capability the run facts exist to provide.

CI_COMMIT_SHA fills the gap but only conditionally: in a merged results
pipeline it is the merge-result commit rather than the source head, so an
unconditional fallback would export the wrong SHA there. Guarding on
CI_PIPELINE_SOURCE is self-correcting, because in a merged results pipeline
the first variable is populated and the fallback is never reached. A pipeline
that is not a merge request still reports no head, which is correct. Tests
cover all three shapes; only the first had one.

**The ordering test did not pin the ordering.** It asserted the run facts
follow .env.d, but not that the iteration source line is last — so a change
moving iteration sourcing above .env.d would have satisfied every existing
assertion while letting a harness env file overwrite the budget and deadline
that #7042 put there. That is the conflict we resolved by hand a few hours
ago, unguarded. The whole order is now asserted, including that the final
element is iterationEnvSourceLine().

**The operator link promised an explanation that was not there.** The
operations guide sends an administrator to ci-workflows.md "for what
preserving gives up", and that page described only cancel-versus-queue. It
now names the trade: ADR 0063 counts per-stage cancellation among its
duplicate-dispatch mitigations, and preserving leaves the poller's lock and
agent idempotency.

**And one asymmetry:** the deprecated thin-caller assertion checked Value
without Tag, so quoting that literal would have escaped it while the seven
stage jobs caught it.

Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Sep 6, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 4:39 PM UTC · Completed 4:59 PM UTC

Commit: 3ee26f7 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $6.62

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

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

  • .github/workflows/reusable-dispatch.yml (file-level): Line 47 · [low] scope-authorization-premature

The FULLSEND_PRESERVE_RUNS opt-in ships while ADR 0098 remains open. Since the prior review, ci-workflows.md has been substantially extended with explicit guidance on what preserving gives up (ADR 0063 layer removal, duplicate-poll hazard), and the PR body explicitly disavows being ADR 0098's implementation.

  • internal/cli/run.go (file-level): Line 169 · [low] architecture-coherence

FULLSEND_RUN_STARTED_AT captures a host-side clock value, not the workflow run's server-side created_at. The two halves of the preserve-and-reconcile system will reference different absolute timestamps. The risk is conservative — the window of undetected updates is bounded and small — but the architectural split is not spelled out in the issue or any ADR.

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

@@ -0,0 +1,165 @@
package cli

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] naming-convention

The filename runfacts_test.go does not follow the established topic-split naming convention for run.go tests. Existing peers use run__test.go (run_models_aliases_test.go, run_openai_test.go, run_overrides_test.go) or _run_test.go (prescript_run_test.go, telemetry_run_test.go).

Suggested fix: Rename to run_facts_test.go to align with the run__test.go pattern.

Comment thread internal/cli/run.go
// After .env.d, for the same reason env.sandbox is: a file sourced from
// .env.d would otherwise overwrite these. reservedSandboxKeys stops an
// env.sandbox entry shadowing them, but it says nothing about .env.d, so
// position is what actually protects them. It does not close every route —

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] defense-in-depth

Pre-existing gap acknowledged in the PR: a host_files entry whose destination is the runner's own .env file can replace it wholesale, bypassing both reservedSandboxKeys and ordering-based protection for the new run facts. This gap is repo-wide (not introduced by this PR) and is tracked in #7010.

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

Labels

risk/elevated PR risk: elevated

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant