| name | ci-guardian |
| description | Never block on CI. After any push to main (or a PR branch), immediately continue other work and hand CI off to a background agent that watches the run and, if it fails, diagnoses → reproduces → fixes → re-gates → re-pushes, looping until the branch is green. Keeps main green without the main loop ever waiting on a ~12-minute CI cycle.
|
| user_invocable | true |
CI Guardian — TerranSoulApp
Standing rule (user directive 2026-07-04): never wait for CI to go green
before starting other work. The moment you git push, spin up a background
agent to own that CI run to completion and return to your real task. Blocking
the main loop on CI is the anti-pattern this skill exists to kill.
The pattern (do this after every push)
- Push.
- Spawn a background agent (
Agent tool, run_in_background: true,
subagent_type: general-purpose) with the CI-Guardian mission below,
passing the pushed commit SHA / branch.
- Immediately continue your other work. Do not monitor CI yourself.
- When the guardian reports, react only if it needs a decision; otherwise it
has already re-pushed a fix and main is green.
Only make the main loop wait on CI when a later step genuinely cannot proceed
without a confirmed-green main (e.g. cutting a release). Even then, prefer
letting the guardian finish and checking its result asynchronously.
CI-Guardian mission (the background agent's brief)
Pin the run ID first — never poll by "latest on branch." Immediately
after the push, resolve the exact run for <sha> once:
gh run list --branch <branch> --limit 5 --json databaseId,headSha,status,conclusion
and pick the entry whose headSha equals <sha> (retry briefly if the run
hasn't appeared yet — GitHub takes a few seconds to register it). Then poll
that databaseId by ID — gh run view <id> --json status,conclusion —
with a sleep, do not busy-spin, until status == "completed".
Do NOT keep re-running gh run list --branch <branch> --limit 1 as your
poll loop: if another push lands on the same branch while you watch (this
repo has multiple agents/sessions pushing concurrently), --limit 1
silently starts returning that NEWER run instead of yours, and you poll
forever waiting for a terminal state that was already reached on a run
you stopped tracking. (Observed 2026-07-06: a guardian's loop tracked a
moving "latest run" target for 60 minutes with no result; gh run view <pinned-id> immediately showed the real run had already succeeded.)
Poll in ONE BLOCKING FOREGROUND call — never background the wait. You
are a subagent, not the main loop: a subagent that backgrounds a wait
(run_in_background: true on gh run watch, or any Monitor/watcher it
expects to notify it later) reliably goes idle and never gets re-invoked —
observed 2026-07-09 on this exact skill's previous advice ("background
gh run watch <id> --exit-status"): the guardian armed it, yielded, and
the very next task-notification reported "stopped with no live background
children of its own" — the watcher was never a live child that could wake
it. Only the top-level orchestrating loop reliably resumes from a
run_in_background notification; a subagent one level down does not. Do
the wait as a single FOREGROUND Bash call that blocks until done and prints
exactly once, at the end — either gh run watch <id> --exit-status
(foreground, no run_in_background) or a silent poll loop in one call:
until gh run view <id> --json status -q .status | grep -q completed; do sleep 20; done; gh run view <id> --json conclusion -q .conclusion.
Keep the poll call's own timeout UNDER ~110000ms — do not set it to
the tool's max. An earlier version of this skill said "always pass an
explicit generous timeout (e.g. 600000ms)" to stop the Bash tool from
silently auto-backgrounding an untimed call. That guidance was itself
refined 2026-07-09: even WITH an explicit long timeout set, this
environment's Bash tool still auto-promoted a blocking poll loop
(until gh run view ...; do sleep 20; done) to a background task after
roughly 2 minutes of wall-clock — the explicit timeout parameter does
NOT reliably prevent auto-backgrounding on a long-running call, contrary
to the original assumption. The workaround that actually works: keep the
poll call's own timeout SHORT (under ~110000ms) so the tool call itself
naturally times out and returns control to you mid-turn BEFORE the
~2-minute auto-background threshold kicks in — then immediately issue
another bounded foreground call (gh run view <id> --json status,conclusion) and repeat. This is fully compatible with the
single-loop pattern below (until ...; do sleep 20; done) as long as the
OUTER timeout you pass to the Bash tool stays under ~110s — a run that
takes several minutes just means several such bounded calls in a row
within the same turn, never a single multi-minute one. If a call times
out before the run reaches a terminal state, that is fine — you are still
mid-turn, not waiting on a notification — just issue another bounded
foreground call immediately and repeat. The failure mode this skill
exists to avoid is ending your TURN while waiting; looping several
bounded foreground calls back-to-back within the same turn is always
safe. (A naive while true; do ...; echo status=$status; sleep N; done
under Monitor also spams one notification per tick — observed 2026-07-08,
16+ pings over ~10 minutes on run 28924365625 — so keep the loop body
silent regardless of which form you use.) Also note:
this environment's Bash tool has no external jq binary — use gh's own
-q/--jq query flag (e.g. gh run view <id> --json status -q .status)
instead of piping through jq.
Once resolved:
- success → report green, stop.
- failure → get the failing job + test
(
gh run view <id> --log-failed | grep -iE "FAILED|panicked|error\[|assertion|left:|right:").
Classify:
- Flake (a test that passes on rerun with no code change — RRF tie-order,
shard-engine concurrency, timing): record it in the SWE2-TEST-FLAKE
watchlist and fix at the source (never just rerun-until-green — that
hides the bug). Reproduce-first: build a sub-10s snippet / stress test that
triggers it locally (many concurrency races reproduce on any multicore even
if "CI-only" at first glance); promote it to a regression test.
- Real regression → reproduce with the smallest failing case, fix it.
- Infra/transient (runner OOM, network) → rerun once
(
gh run rerun <id> --failed); if it recurs, treat as real.
- Run the CI-exact gate locally before pushing the fix (see below), from
src-tauri/ with serial cargo jobs + CARGO_INCREMENTAL=0 (this machine
OOMs parallel rustc; per reference_cargo_oom_jobs1). Never edit .rs
files while a cargo gate compiles — keep edits and gates strictly serial.
- Commit (message ends with the repo's
Co-Authored-By trailer), push, and
loop: watch the new run until green. Escalate to the user after 3
failed fix attempts on the same failure, or on any security failure.
- Sync any durable root-cause lesson into
mcp-data/shared/memory-seed.sql.
The CI-exact gate (must pass before pushing a fix)
Mirrors .github/workflows/terransoul-ci.yml exactly:
npm run lint && npm run build && npx vitest run \
&& npm run test:marketplace-sync && npm run marketplace:sync:check \
&& cd src-tauri \
&& CARGO_INCREMENTAL=0 cargo clippy --lib --tests --features postgres -j 1 -- -D warnings \
&& CARGO_INCREMENTAL=0 cargo test --lib --features postgres --no-run -j 1 \
&& CARGO_INCREMENTAL=0 cargo test --lib -j 1
(npm run build covers vue-tsc; local npm run lint may false-flag
venv/worktree JS the CI checkout lacks — see
reference_eslint_local_vs_ci_venv_worktree.)
Never-regress
If a CI failure is a benchmark floor breach, the guardian follows
rules/bench-never-regress.md (investigate → optimize production-path → rebench
until equal-or-better), not a number edit.