From cd6212977be19b00418253f3b48d650549c996f7 Mon Sep 17 00:00:00 2001 From: don-petry <36422719+don-petry@users.noreply.github.com> Date: Fri, 2 Oct 2026 17:28:19 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20implement=20issue=20#1232=20?= =?UTF-8?q?=E2=80=94=20[bug]=20Phase=204=20gate=20wiring=20ships=20to=20ad?= =?UTF-8?q?opter=20stubs=20but=20agent-rate-limit-gate.sh=20is=20absent=20?= =?UTF-8?q?at=20v1=20=E2=80=94=20gate=20is=20inert=20and=20fail-open?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/initiative-driver.yml | 50 +++++++++---- standards/agent-rate-limits.md | 17 +++++ standards/workflows/initiative-driver.yml | 50 +++++++++---- .../initiative-driver/gate-tooling.bats | 75 +++++++++++++++++++ 4 files changed, 164 insertions(+), 28 deletions(-) create mode 100644 test/workflows/initiative-driver/gate-tooling.bats diff --git a/.github/workflows/initiative-driver.yml b/.github/workflows/initiative-driver.yml index 5a58fa22f..609e6c405 100644 --- a/.github/workflows/initiative-driver.yml +++ b/.github/workflows/initiative-driver.yml @@ -32,19 +32,27 @@ # whether `initiative-driver` may dispatch right now. initiative-driver is the # canary that ENFORCES: the gate derives concurrency / cooldown / daily-budget / # consecutive-failure counters from this stub's own run history (no state -# backend), reads every threshold from standards/agent-rate-limits.json, and -# serializes a close-event + schedule burst AHEAD of the cancel-in-progress -# concurrency group so two dispatches cannot race into cancellation (#443/#402). +# backend) and reads every threshold from standards/agent-rate-limits.json. +# It throttles dispatches that DO start (cooldown / daily budget / breaker); it +# does NOT pre-empt the cancel-in-progress concurrency group below — that group +# cancels a superseded run before any step (gate included) executes (#443/#402). # A `defer` simply skips the dispatch step — never a cancel, never a job failure. # +# Gate tooling is fetched at a pinned commit SHA of petry-projects/.github (not +# a moving tag — the PAT is handed to that code). If the tooling is unavailable +# or the gate errors, the gate step FAILS LOUDLY (::error:: annotation + step +# summary, step marked failed) but the dispatch still runs (fail-open — an +# outage of the gate must never stop the fleet; standards/agent-rate-limits.md). +# # To adopt: # 1. Copy this file verbatim to .github/workflows/initiative-driver.yml in your repo. # 2. Ensure the `initiative:auto` label exists on the repo. -# 3. Confirm the org-level secret GH_PAT_WORKFLOWS is accessible **and its -# owner has write access to petry-projects/.github-private** (to dispatch -# the central workflow) **and to this repo** (the central driver applies the -# `dev-lead` label cross-repo with that PAT; a label applied with -# GITHUB_TOKEN would not trigger dev-lead). +# 3. Confirm the org-level secret GH_PAT_DON_PETRY (or the legacy fallback +# GH_PAT_WORKFLOWS) is accessible **and its owner has write access to +# petry-projects/.github-private** (to dispatch the central workflow) **and +# to this repo** (the central driver applies the `dev-lead` label +# cross-repo with that PAT; a label applied with GITHUB_TOKEN would not +# trigger dev-lead). # # Standard: https://github.com/petry-projects/.github/blob/main/standards/ci-standards.md name: Initiative Driver — Dispatch Central @@ -97,13 +105,18 @@ jobs: # dispatch — the workflow's GITHUB_TOKEN cannot read another repo — so the # stub's `permissions:` block is unchanged. # continue-on-error: a tooling-checkout outage must not stop the fleet — - # the gate step then finds no script, emits no decision, and the dispatch - # step's `!= 'defer'` guard falls through to dispatching (fail-open). + # the gate step then finds no script, reports an ::error:: (fail loudly), + # emits no decision, and the dispatch step's `!= 'defer'` guard falls + # through to dispatching (fail-open). + # ref: pinned to an immutable commit SHA of petry-projects/.github (#1232) + # — a moving tag silently lagged the gate script (v1 predates it) and + # would hand the PAT to whatever the tag next points at. Bump it in a + # reviewed PR (see standards/agent-rate-limits.md §3.1). continue-on-error: true uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: petry-projects/.github - ref: v1 + ref: cd0b16751454d2eb3486ec04477d7ee9cc96c425 # main @ 2026-10-02 (#1232) path: .arl-gate-tooling fetch-depth: 1 persist-credentials: false @@ -114,7 +127,9 @@ jobs: # no model spend, the direct #443 target — AC #5). Reads run history with # the PAT (independent of the `permissions:` block) and emits # decision=allow|defer to $GITHUB_OUTPUT; a defer is honoured by the `if:` - # on the dispatch step below. Fail-safe: the step can never fail the job. + # on the dispatch step below. Fail loud, fail open: a missing gate script + # or a gate error marks THIS step failed with an ::error:: (visible in the + # run), but continue-on-error keeps the job — and the dispatch — going. continue-on-error: true id: arl_gate env: @@ -129,12 +144,19 @@ jobs: ARL_TRACKING_ISSUE: ${{ github.event.issue.number || vars.INITIATIVE_DRIVER_TRACKING_ISSUE || '' }} run: | set -euo pipefail - bash .arl-gate-tooling/scripts/agent-rate-limit-gate.sh initiative-driver \ + ARL_GATE_SCRIPT=.arl-gate-tooling/scripts/agent-rate-limit-gate.sh + if ! [ -f "$ARL_GATE_SCRIPT" ]; then + msg="Agent rate-limit gate NOT ENFORCED: ${ARL_GATE_SCRIPT} is missing (tooling checkout failed or the pinned ref lacks the script). Dispatching ungated (fail-open)." + echo "::error::${msg}" + echo "### :warning: ${msg}" >> "$GITHUB_STEP_SUMMARY" + exit 1 + fi + bash "$ARL_GATE_SCRIPT" initiative-driver \ --mode enforce \ --workflow initiative-driver.yml \ --actor "$ARL_ACTOR" \ --tracking-repo "$ARL_TRACKING_REPO" \ - --tracking-issue "$ARL_TRACKING_ISSUE" || true + --tracking-issue "$ARL_TRACKING_ISSUE" - name: Dispatch central initiative-driver # Skipped (clean no-op) ONLY on an explicit `defer` — never a cancel, never diff --git a/standards/agent-rate-limits.md b/standards/agent-rate-limits.md index 99a6232de..6e95fef21 100644 --- a/standards/agent-rate-limits.md +++ b/standards/agent-rate-limits.md @@ -113,6 +113,23 @@ spend) and the direct target of the dispatch-race defect calls the orchestrator with `--mode enforce`; a `defer` decision simply skips the dispatch step (a clean no-op — never a cancel, never a job failure). +**Gate tooling pin ([#1232](https://github.com/petry-projects/.github/issues/1232)).** +An enrolled stub does not carry the gate, so it checks out `petry-projects/.github` +into `.arl-gate-tooling` with the org PAT. That checkout is pinned to a **full +commit SHA**, not a moving tag: the original `ref: v1` predated +`scripts/agent-rate-limit-gate.sh`, so the gate was silently inert fleet-wide, +and a moving ref hands the PAT to whatever the tag next points at. To pick up +gate or threshold changes (`agent-rate-limits.json` is read from the same +checkout), bump the SHA in `standards/workflows/initiative-driver.yml` in its +own reviewed PR, after confirming the three gate files +(`scripts/agent-rate-limit-gate.sh`, `scripts/lib/agent-rate-limit.sh`, +`standards/agent-rate-limits.json`) resolve at that SHA, then fan out via +standards-sync. If the script is missing at run time, the gate step **fails +loudly** (`::error::` annotation, step summary, step marked failed) while +`continue-on-error` keeps the dispatch fail-open. The gate throttles dispatches +that start; it does not pre-empt the stub's `cancel-in-progress` concurrency +group, which cancels a superseded run before any step executes. + **`feature-ideation` runs the gate in `--mode log-only`** (`feature-ideation-reusable.yml`): the decision is computed from run history and logged, but the emitted decision is always `allow`, so nothing is acted on. `dev-lead` and `compliance-audit` are not diff --git a/standards/workflows/initiative-driver.yml b/standards/workflows/initiative-driver.yml index 5a58fa22f..609e6c405 100644 --- a/standards/workflows/initiative-driver.yml +++ b/standards/workflows/initiative-driver.yml @@ -32,19 +32,27 @@ # whether `initiative-driver` may dispatch right now. initiative-driver is the # canary that ENFORCES: the gate derives concurrency / cooldown / daily-budget / # consecutive-failure counters from this stub's own run history (no state -# backend), reads every threshold from standards/agent-rate-limits.json, and -# serializes a close-event + schedule burst AHEAD of the cancel-in-progress -# concurrency group so two dispatches cannot race into cancellation (#443/#402). +# backend) and reads every threshold from standards/agent-rate-limits.json. +# It throttles dispatches that DO start (cooldown / daily budget / breaker); it +# does NOT pre-empt the cancel-in-progress concurrency group below — that group +# cancels a superseded run before any step (gate included) executes (#443/#402). # A `defer` simply skips the dispatch step — never a cancel, never a job failure. # +# Gate tooling is fetched at a pinned commit SHA of petry-projects/.github (not +# a moving tag — the PAT is handed to that code). If the tooling is unavailable +# or the gate errors, the gate step FAILS LOUDLY (::error:: annotation + step +# summary, step marked failed) but the dispatch still runs (fail-open — an +# outage of the gate must never stop the fleet; standards/agent-rate-limits.md). +# # To adopt: # 1. Copy this file verbatim to .github/workflows/initiative-driver.yml in your repo. # 2. Ensure the `initiative:auto` label exists on the repo. -# 3. Confirm the org-level secret GH_PAT_WORKFLOWS is accessible **and its -# owner has write access to petry-projects/.github-private** (to dispatch -# the central workflow) **and to this repo** (the central driver applies the -# `dev-lead` label cross-repo with that PAT; a label applied with -# GITHUB_TOKEN would not trigger dev-lead). +# 3. Confirm the org-level secret GH_PAT_DON_PETRY (or the legacy fallback +# GH_PAT_WORKFLOWS) is accessible **and its owner has write access to +# petry-projects/.github-private** (to dispatch the central workflow) **and +# to this repo** (the central driver applies the `dev-lead` label +# cross-repo with that PAT; a label applied with GITHUB_TOKEN would not +# trigger dev-lead). # # Standard: https://github.com/petry-projects/.github/blob/main/standards/ci-standards.md name: Initiative Driver — Dispatch Central @@ -97,13 +105,18 @@ jobs: # dispatch — the workflow's GITHUB_TOKEN cannot read another repo — so the # stub's `permissions:` block is unchanged. # continue-on-error: a tooling-checkout outage must not stop the fleet — - # the gate step then finds no script, emits no decision, and the dispatch - # step's `!= 'defer'` guard falls through to dispatching (fail-open). + # the gate step then finds no script, reports an ::error:: (fail loudly), + # emits no decision, and the dispatch step's `!= 'defer'` guard falls + # through to dispatching (fail-open). + # ref: pinned to an immutable commit SHA of petry-projects/.github (#1232) + # — a moving tag silently lagged the gate script (v1 predates it) and + # would hand the PAT to whatever the tag next points at. Bump it in a + # reviewed PR (see standards/agent-rate-limits.md §3.1). continue-on-error: true uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: petry-projects/.github - ref: v1 + ref: cd0b16751454d2eb3486ec04477d7ee9cc96c425 # main @ 2026-10-02 (#1232) path: .arl-gate-tooling fetch-depth: 1 persist-credentials: false @@ -114,7 +127,9 @@ jobs: # no model spend, the direct #443 target — AC #5). Reads run history with # the PAT (independent of the `permissions:` block) and emits # decision=allow|defer to $GITHUB_OUTPUT; a defer is honoured by the `if:` - # on the dispatch step below. Fail-safe: the step can never fail the job. + # on the dispatch step below. Fail loud, fail open: a missing gate script + # or a gate error marks THIS step failed with an ::error:: (visible in the + # run), but continue-on-error keeps the job — and the dispatch — going. continue-on-error: true id: arl_gate env: @@ -129,12 +144,19 @@ jobs: ARL_TRACKING_ISSUE: ${{ github.event.issue.number || vars.INITIATIVE_DRIVER_TRACKING_ISSUE || '' }} run: | set -euo pipefail - bash .arl-gate-tooling/scripts/agent-rate-limit-gate.sh initiative-driver \ + ARL_GATE_SCRIPT=.arl-gate-tooling/scripts/agent-rate-limit-gate.sh + if ! [ -f "$ARL_GATE_SCRIPT" ]; then + msg="Agent rate-limit gate NOT ENFORCED: ${ARL_GATE_SCRIPT} is missing (tooling checkout failed or the pinned ref lacks the script). Dispatching ungated (fail-open)." + echo "::error::${msg}" + echo "### :warning: ${msg}" >> "$GITHUB_STEP_SUMMARY" + exit 1 + fi + bash "$ARL_GATE_SCRIPT" initiative-driver \ --mode enforce \ --workflow initiative-driver.yml \ --actor "$ARL_ACTOR" \ --tracking-repo "$ARL_TRACKING_REPO" \ - --tracking-issue "$ARL_TRACKING_ISSUE" || true + --tracking-issue "$ARL_TRACKING_ISSUE" - name: Dispatch central initiative-driver # Skipped (clean no-op) ONLY on an explicit `defer` — never a cancel, never diff --git a/test/workflows/initiative-driver/gate-tooling.bats b/test/workflows/initiative-driver/gate-tooling.bats new file mode 100644 index 000000000..d3fba4bcf --- /dev/null +++ b/test/workflows/initiative-driver/gate-tooling.bats @@ -0,0 +1,75 @@ +#!/usr/bin/env bats +# Tests for the agent-rate-limit gate wiring of the canonical caller stub +# standards/workflows/initiative-driver.yml (issue #1232). +# +# The Phase-4 stub (#640) fetched the gate tooling at the moving tag `v1`, which +# predates scripts/agent-rate-limit-gate.sh — the gate was silently inert. The +# tooling checkout must be pinned to an immutable commit SHA, a missing gate +# script must be reported loudly (not swallowed), and the header must describe +# what the stub actually does. + +TT_REPO_ROOT="$(cd -- "$(dirname -- "${BATS_TEST_DIRNAME}")/../.." && pwd)" +STUB="${TT_REPO_ROOT}/standards/workflows/initiative-driver.yml" +LIVE="${TT_REPO_ROOT}/.github/workflows/initiative-driver.yml" + +CHECKOUT_STEP='Checkout agent-rate-limit gate tooling' +GATE_STEP='Agent rate-limit admission gate (enforcing)' + +@test "stub: gate tooling checkout is pinned to a full commit SHA" { + run yq -r ".jobs.dispatch.steps[] | select(.name == \"${CHECKOUT_STEP}\") | .with.ref" "$STUB" + [ "$status" -eq 0 ] + [[ "$output" =~ ^[0-9a-f]{40}$ ]] +} + +@test "stub: gate tooling checkout still targets petry-projects/.github" { + run yq -r ".jobs.dispatch.steps[] | select(.name == \"${CHECKOUT_STEP}\") | .with.repository" "$STUB" + [ "$status" -eq 0 ] + [ "$output" = 'petry-projects/.github' ] +} + +@test "stub: gate step detects a missing gate script and reports an ::error::" { + run yq -r ".jobs.dispatch.steps[] | select(.name == \"${GATE_STEP}\") | .run" "$STUB" + [ "$status" -eq 0 ] + echo "$output" | grep -qE '\[ -f "?\$\{?ARL_GATE_SCRIPT\}?"? \]' + echo "$output" | grep -q '::error::' + echo "$output" | grep -q 'GITHUB_STEP_SUMMARY' +} + +@test "stub: gate invocation is not masked by '|| true'" { + run yq -r ".jobs.dispatch.steps[] | select(.name == \"${GATE_STEP}\") | .run" "$STUB" + [ "$status" -eq 0 ] + ! echo "$output" | grep -qF '|| true' +} + +@test "stub: missing gate script exits non-zero (step marked failed, job continues)" { + run yq -r ".jobs.dispatch.steps[] | select(.name == \"${GATE_STEP}\") | .run" "$STUB" + [ "$status" -eq 0 ] + local script="$output" + local tmp + tmp="$(mktemp -d)" + run env -C "$tmp" GITHUB_STEP_SUMMARY="$tmp/summary" GITHUB_OUTPUT="$tmp/out" \ + ARL_ACTOR=a ARL_TRACKING_REPO=o/r ARL_TRACKING_ISSUE= bash -c "$script" + [ "$status" -ne 0 ] + [[ "$output" == *"::error::"* ]] + grep -q 'agent-rate-limit-gate.sh' "$tmp/summary" + # No defer is emitted — the dispatch step's fail-open guard still dispatches. + ! grep -q 'decision=defer' "$tmp/out" 2>/dev/null + rm -rf "$tmp" + + run yq -r ".jobs.dispatch.steps[] | select(.name == \"${GATE_STEP}\") | .\"continue-on-error\"" "$STUB" + [ "$output" = 'true' ] +} + +@test "stub: header no longer claims the gate runs AHEAD of the concurrency group" { + ! grep -qiE 'AHEAD of the cancel-in-progress' "$STUB" +} + +@test "stub: adoption step names the canonical GH_PAT_DON_PETRY secret" { + run grep -nE '^# 3\..*GH_PAT_DON_PETRY' "$STUB" + [ "$status" -eq 0 ] +} + +@test "live copy matches the standard verbatim" { + run diff "$STUB" "$LIVE" + [ "$status" -eq 0 ] +} From 8b8759dcd7a449971582286dba48b9d8aee7d302 Mon Sep 17 00:00:00 2001 From: don-petry <36422719+don-petry@users.noreply.github.com> Date: Fri, 2 Oct 2026 17:50:27 +0000 Subject: [PATCH 2/2] fix(reviews): address review comments [skip ci-relay] --- .github/workflows/initiative-driver.yml | 8 +++++- standards/agent-rate-limits.md | 2 +- standards/workflows/initiative-driver.yml | 8 +++++- .../initiative-driver/gate-tooling.bats | 26 +++++++++++++++---- 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/.github/workflows/initiative-driver.yml b/.github/workflows/initiative-driver.yml index 609e6c405..1c7c765b2 100644 --- a/.github/workflows/initiative-driver.yml +++ b/.github/workflows/initiative-driver.yml @@ -152,11 +152,17 @@ jobs: exit 1 fi bash "$ARL_GATE_SCRIPT" initiative-driver \ + --repo "$ARL_TRACKING_REPO" \ --mode enforce \ --workflow initiative-driver.yml \ --actor "$ARL_ACTOR" \ --tracking-repo "$ARL_TRACKING_REPO" \ - --tracking-issue "$ARL_TRACKING_ISSUE" + --tracking-issue "$ARL_TRACKING_ISSUE" || { + rc=$? + echo "::error::Agent rate-limit gate errored (exit ${rc}) — dispatching ungated (fail-open)." + echo "### :warning: Agent rate-limit gate errored (exit ${rc}) — dispatching ungated (fail-open)." >> "$GITHUB_STEP_SUMMARY" + exit "$rc" + } - name: Dispatch central initiative-driver # Skipped (clean no-op) ONLY on an explicit `defer` — never a cancel, never diff --git a/standards/agent-rate-limits.md b/standards/agent-rate-limits.md index 6e95fef21..cc598af9c 100644 --- a/standards/agent-rate-limits.md +++ b/standards/agent-rate-limits.md @@ -128,7 +128,7 @@ standards-sync. If the script is missing at run time, the gate step **fails loudly** (`::error::` annotation, step summary, step marked failed) while `continue-on-error` keeps the dispatch fail-open. The gate throttles dispatches that start; it does not pre-empt the stub's `cancel-in-progress` concurrency -group, which cancels a superseded run before any step executes. +group, which may cancel a superseded run after its steps have started. **`feature-ideation` runs the gate in `--mode log-only`** (`feature-ideation-reusable.yml`): the decision is computed from run history and logged, but the emitted decision is diff --git a/standards/workflows/initiative-driver.yml b/standards/workflows/initiative-driver.yml index 609e6c405..1c7c765b2 100644 --- a/standards/workflows/initiative-driver.yml +++ b/standards/workflows/initiative-driver.yml @@ -152,11 +152,17 @@ jobs: exit 1 fi bash "$ARL_GATE_SCRIPT" initiative-driver \ + --repo "$ARL_TRACKING_REPO" \ --mode enforce \ --workflow initiative-driver.yml \ --actor "$ARL_ACTOR" \ --tracking-repo "$ARL_TRACKING_REPO" \ - --tracking-issue "$ARL_TRACKING_ISSUE" + --tracking-issue "$ARL_TRACKING_ISSUE" || { + rc=$? + echo "::error::Agent rate-limit gate errored (exit ${rc}) — dispatching ungated (fail-open)." + echo "### :warning: Agent rate-limit gate errored (exit ${rc}) — dispatching ungated (fail-open)." >> "$GITHUB_STEP_SUMMARY" + exit "$rc" + } - name: Dispatch central initiative-driver # Skipped (clean no-op) ONLY on an explicit `defer` — never a cancel, never diff --git a/test/workflows/initiative-driver/gate-tooling.bats b/test/workflows/initiative-driver/gate-tooling.bats index d3fba4bcf..872f9973e 100644 --- a/test/workflows/initiative-driver/gate-tooling.bats +++ b/test/workflows/initiative-driver/gate-tooling.bats @@ -18,7 +18,7 @@ GATE_STEP='Agent rate-limit admission gate (enforcing)' @test "stub: gate tooling checkout is pinned to a full commit SHA" { run yq -r ".jobs.dispatch.steps[] | select(.name == \"${CHECKOUT_STEP}\") | .with.ref" "$STUB" [ "$status" -eq 0 ] - [[ "$output" =~ ^[0-9a-f]{40}$ ]] + [ "$output" = 'cd0b16751454d2eb3486ec04477d7ee9cc96c425' ] } @test "stub: gate tooling checkout still targets petry-projects/.github" { @@ -38,7 +38,8 @@ GATE_STEP='Agent rate-limit admission gate (enforcing)' @test "stub: gate invocation is not masked by '|| true'" { run yq -r ".jobs.dispatch.steps[] | select(.name == \"${GATE_STEP}\") | .run" "$STUB" [ "$status" -eq 0 ] - ! echo "$output" | grep -qF '|| true' + run grep -qF '|| true' <<<"$output" + [ "$status" -eq 1 ] } @test "stub: missing gate script exits non-zero (step marked failed, job continues)" { @@ -49,11 +50,13 @@ GATE_STEP='Agent rate-limit admission gate (enforcing)' tmp="$(mktemp -d)" run env -C "$tmp" GITHUB_STEP_SUMMARY="$tmp/summary" GITHUB_OUTPUT="$tmp/out" \ ARL_ACTOR=a ARL_TRACKING_REPO=o/r ARL_TRACKING_ISSUE= bash -c "$script" - [ "$status" -ne 0 ] + [ "$status" -eq 1 ] [[ "$output" == *"::error::"* ]] grep -q 'agent-rate-limit-gate.sh' "$tmp/summary" # No defer is emitted — the dispatch step's fail-open guard still dispatches. - ! grep -q 'decision=defer' "$tmp/out" 2>/dev/null + touch "$tmp/out" + run grep -q 'decision=defer' "$tmp/out" + [ "$status" -eq 1 ] rm -rf "$tmp" run yq -r ".jobs.dispatch.steps[] | select(.name == \"${GATE_STEP}\") | .\"continue-on-error\"" "$STUB" @@ -61,7 +64,8 @@ GATE_STEP='Agent rate-limit admission gate (enforcing)' } @test "stub: header no longer claims the gate runs AHEAD of the concurrency group" { - ! grep -qiE 'AHEAD of the cancel-in-progress' "$STUB" + run grep -qiE 'AHEAD of the cancel-in-progress' "$STUB" + [ "$status" -eq 1 ] } @test "stub: adoption step names the canonical GH_PAT_DON_PETRY secret" { @@ -69,6 +73,18 @@ GATE_STEP='Agent rate-limit admission gate (enforcing)' [ "$status" -eq 0 ] } +@test "stub: dispatch step guard dispatches on an empty decision (fail-open)" { + run yq -r ".jobs.dispatch.steps[] | select(.name == \"Dispatch central initiative-driver\") | .if" "$STUB" + [ "$status" -eq 0 ] + [ "$output" = "steps.arl_gate.outputs.decision != 'defer'" ] +} + +@test "stub: gate invocation passes --repo so run history is the caller's" { + run yq -r ".jobs.dispatch.steps[] | select(.name == \"${GATE_STEP}\") | .run" "$STUB" + [ "$status" -eq 0 ] + [[ "$output" == *'--repo "$ARL_TRACKING_REPO"'* ]] +} + @test "live copy matches the standard verbatim" { run diff "$STUB" "$LIVE" [ "$status" -eq 0 ]