From 3e79e6ccacfc838b4e461d5ea16019435ae0b076 Mon Sep 17 00:00:00 2001 From: tend-agent <270458913+tend-agent@users.noreply.github.com> Date: Thu, 6 Aug 2026 00:17:33 +0000 Subject: [PATCH 1/3] fix(actions): report an outage when the run fails ahead of the agent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Report failure was gated on steps.claude/steps.codex outcome, so the twelve steps that run before the agent — both preflights, auth validation, the sandbox build, the adopter's sandbox_setup: — failed the job without filing or commenting on a tend-outage issue. A rate-limit preflight abort held every workflow on max-sixty/tend for 12.5 hours across 36 runs with no record anywhere. Gate on failure() instead, and correct the report-failure.sh header that documented the old contract. --- claude/action.yaml | 9 ++++++++- codex/action.yaml | 7 ++++++- shared/steps/report-failure.sh | 6 ++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/claude/action.yaml b/claude/action.yaml index 6ed13d15..f28a90fd 100644 --- a/claude/action.yaml +++ b/claude/action.yaml @@ -607,8 +607,15 @@ runs: "*Cost at API list prices — a large multiple of the effective rate on Claude Code subscriptions.*" ' <<< "$USAGE" >> "$GITHUB_STEP_SUMMARY" + # Gate on the job being red, not on the agent step specifically. A dozen + # steps run before `claude` — the two preflights, auth validation, the + # proxy and sandbox build, the adopter's `sandbox_setup:` — and any of + # them failing strands the same work, so each needs to reach the outage + # tracker. Keying on `steps.claude.outcome` meant a pre-agent failure went + # out as a red run and nothing else; a rate-limit preflight abort held + # every workflow on this repo for a full UTC day without filing a thing. - name: Report failure - if: failure() && steps.claude.outcome == 'failure' + if: failure() shell: bash run: bash "${{ github.action_path }}/../shared/steps/report-failure.sh" env: diff --git a/codex/action.yaml b/codex/action.yaml index c42895f0..7e4fe154 100644 --- a/codex/action.yaml +++ b/codex/action.yaml @@ -329,8 +329,13 @@ runs: env: GITHUB_TOKEN: ${{ inputs.github_token }} + # Gate on the job being red, not on the agent step specifically — see the + # matching note in claude/action.yaml. Every step ahead of `codex` (the + # preflights, the plugin staging, the adopter's setup) strands the same + # work when it fails, and keying on `steps.codex.outcome` kept all of them + # out of the outage tracker. - name: Report failure - if: failure() && steps.codex.outcome == 'failure' + if: failure() shell: bash run: bash "${{ github.action_path }}/../shared/steps/report-failure.sh" env: diff --git a/shared/steps/report-failure.sh b/shared/steps/report-failure.sh index e8b1c6ae..48e8051e 100755 --- a/shared/steps/report-failure.sh +++ b/shared/steps/report-failure.sh @@ -1,7 +1,9 @@ #!/usr/bin/env bash # File or append to a `tend-outage` issue when a run fails, so outages are -# tracked until resolved. Shared verbatim by both harness actions; the -# caller gates it on the agent step having failed. +# tracked until resolved. Shared verbatim by both harness actions; the caller +# gates it on the job being red, so a failure anywhere in the action — the +# preflights and sandbox build ahead of the agent as much as the agent +# itself — lands in the tracker. # # Just records the run link. Error annotations and logs are not reliably # available while the job is in_progress, so the nightly skill enriches these From adbb6ddf857807d88e43e22ff29c058f769fa95b Mon Sep 17 00:00:00 2001 From: tend-agent <270458913+tend-agent@users.noreply.github.com> Date: Thu, 6 Aug 2026 00:28:05 +0000 Subject: [PATCH 2/3] Exclude the security preflight from the widened outage gate A security-preflight failure is a persistent config refusal, not a transient outage: it stays failing until a human fixes branch protection, so under a bare `failure()` gate it would file "Bot temporarily unavailable" and append a row on every later trigger without ever naming the cause. It is also the one path where reporting would have the bot write to the repo with its PAT right after the security gate refused to let it operate there. Also correct the codex comment: that action has no `sandbox_setup` input, so the adopter's setup runs in the caller workflow before the composite action starts. --- claude/action.yaml | 15 ++++++++++++--- codex/action.yaml | 12 +++++++----- shared/steps/report-failure.sh | 7 +++++-- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/claude/action.yaml b/claude/action.yaml index f28a90fd..5fbd2236 100644 --- a/claude/action.yaml +++ b/claude/action.yaml @@ -115,6 +115,7 @@ runs: # and runs the script (reached via ../shared/steps, since this action is # one level deep). - name: Security preflight + id: security shell: bash run: bash "${{ github.action_path }}/../shared/steps/security-preflight.sh" env: @@ -608,14 +609,22 @@ runs: ' <<< "$USAGE" >> "$GITHUB_STEP_SUMMARY" # Gate on the job being red, not on the agent step specifically. A dozen - # steps run before `claude` — the two preflights, auth validation, the - # proxy and sandbox build, the adopter's `sandbox_setup:` — and any of + # steps run before `claude` — the rate-limit preflight, auth validation, + # the proxy and sandbox build, the adopter's `sandbox_setup:` — and any of # them failing strands the same work, so each needs to reach the outage # tracker. Keying on `steps.claude.outcome` meant a pre-agent failure went # out as a red run and nothing else; a rate-limit preflight abort held # every workflow on this repo for a full UTC day without filing a thing. + # + # The security preflight is the one exclusion: its failure is a config + # refusal, not an outage. It stays failing until a human fixes the repo's + # branch protection, so it would file "Bot temporarily unavailable" and + # append a row on every subsequent trigger, forever, without ever naming + # the cause. It is also the one case where reporting would have the bot + # write to the repo with its PAT right after the security gate refused to + # let it operate there. - name: Report failure - if: failure() + if: failure() && steps.security.outcome != 'failure' shell: bash run: bash "${{ github.action_path }}/../shared/steps/report-failure.sh" env: diff --git a/codex/action.yaml b/codex/action.yaml index 7e4fe154..e6566179 100644 --- a/codex/action.yaml +++ b/codex/action.yaml @@ -83,6 +83,7 @@ runs: # Preflight/teardown step bodies are shared with the Claude actions via # ../shared/steps; codex's auth, prompt staging, and run path are its own. - name: Security preflight + id: security shell: bash run: bash "${{ github.action_path }}/../shared/steps/security-preflight.sh" env: @@ -330,12 +331,13 @@ runs: GITHUB_TOKEN: ${{ inputs.github_token }} # Gate on the job being red, not on the agent step specifically — see the - # matching note in claude/action.yaml. Every step ahead of `codex` (the - # preflights, the plugin staging, the adopter's setup) strands the same - # work when it fails, and keying on `steps.codex.outcome` kept all of them - # out of the outage tracker. + # matching note in claude/action.yaml, including why the security preflight + # is excluded. Every step ahead of `codex` (the rate-limit preflight, the + # CLI and plugin installs, the AGENTS.md staging, the fork-PR pinning) + # strands the same work when it fails, and keying on `steps.codex.outcome` + # kept all of them out of the outage tracker. - name: Report failure - if: failure() + if: failure() && steps.security.outcome != 'failure' shell: bash run: bash "${{ github.action_path }}/../shared/steps/report-failure.sh" env: diff --git a/shared/steps/report-failure.sh b/shared/steps/report-failure.sh index 48e8051e..635ff9bc 100755 --- a/shared/steps/report-failure.sh +++ b/shared/steps/report-failure.sh @@ -2,8 +2,11 @@ # File or append to a `tend-outage` issue when a run fails, so outages are # tracked until resolved. Shared verbatim by both harness actions; the caller # gates it on the job being red, so a failure anywhere in the action — the -# preflights and sandbox build ahead of the agent as much as the agent -# itself — lands in the tracker. +# rate-limit preflight and sandbox build ahead of the agent as much as the +# agent itself — lands in the tracker. The one exclusion is the security +# preflight: that failure is a persistent config refusal rather than an +# outage, and the issue this files ("temporarily unavailable", closed once +# resolved) would never resolve. # # Just records the run link. Error annotations and logs are not reliably # available while the job is in_progress, so the nightly skill enriches these From 3bf1d068b4759a1d4c6192d6aa454026ae4f3ec5 Mon Sep 17 00:00:00 2001 From: tend-agent <270458913+tend-agent@users.noreply.github.com> Date: Thu, 6 Aug 2026 00:38:39 +0000 Subject: [PATCH 3/3] Name the two steps the codex comment skipped; narrow the exclusion rationale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The persistence argument doesn't single out the security preflight — `Validate auth configured` and the adopter's `sandbox_setup:` fail the same deterministic way and stay in the tracker. What actually carves it out is the PAT-write argument alone, so say that and point the repeated- append problem at #859, where a fix in report-failure.sh covers every such step without an enumerated exclusion list. --- claude/action.yaml | 18 +++++++++++------- codex/action.yaml | 9 +++++---- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/claude/action.yaml b/claude/action.yaml index 5fbd2236..747d733d 100644 --- a/claude/action.yaml +++ b/claude/action.yaml @@ -616,13 +616,17 @@ runs: # out as a red run and nothing else; a rate-limit preflight abort held # every workflow on this repo for a full UTC day without filing a thing. # - # The security preflight is the one exclusion: its failure is a config - # refusal, not an outage. It stays failing until a human fixes the repo's - # branch protection, so it would file "Bot temporarily unavailable" and - # append a row on every subsequent trigger, forever, without ever naming - # the cause. It is also the one case where reporting would have the bot - # write to the repo with its PAT right after the security gate refused to - # let it operate there. + # The security preflight is the one exclusion, and what carves it out is + # that reporting would have the bot write to the repo (`gh issue create`, + # and `gh issue close` on the reconcile path) with its PAT immediately + # after the security gate refused to let it operate there. Its failure is + # also persistent — red until a human fixes branch protection, so an issue + # titled "Bot temporarily unavailable" would collect a row per trigger — + # but that argument doesn't single it out: `Validate auth configured` and + # the adopter's `sandbox_setup:` fail the same way and stay in. Bounding + # that repeated append belongs in `report-failure.sh`, where it covers + # every such step at once (#859); enumerating more step ids here would go + # stale as steps move. - name: Report failure if: failure() && steps.security.outcome != 'failure' shell: bash diff --git a/codex/action.yaml b/codex/action.yaml index e6566179..c3c1d971 100644 --- a/codex/action.yaml +++ b/codex/action.yaml @@ -332,10 +332,11 @@ runs: # Gate on the job being red, not on the agent step specifically — see the # matching note in claude/action.yaml, including why the security preflight - # is excluded. Every step ahead of `codex` (the rate-limit preflight, the - # CLI and plugin installs, the AGENTS.md staging, the fork-PR pinning) - # strands the same work when it fails, and keying on `steps.codex.outcome` - # kept all of them out of the outage tracker. + # is excluded. Every step ahead of `codex` (the rate-limit preflight, bot-ID + # resolution, auth validation, the CLI and plugin installs, the AGENTS.md + # staging, the fork-PR pinning) strands the same work when it fails, and + # keying on `steps.codex.outcome` kept all of them out of the outage + # tracker. - name: Report failure if: failure() && steps.security.outcome != 'failure' shell: bash