ci-cd-validation
Pre-push validation checklist (cargo fmt, clippy with zero warnings, feature flag testing, test suite), CI monitoring, merge process, and release quality gates. Use when preparing to push code, valida
SKILL.md.claude/skills/ci-cd-validation
GitHubCI/CD Validation & Merge Workflow
Validation, CI monitoring, and merge procedures for cqlite. There is one gate —
scripts/agent-gate.sh — run in a tiered loop. The old manual cargo checklist, coverage/tarpaulin
gate, and human-merge steps were M1-era fossils and are gone (issue #1855); see the pointer files in this
dir and docs/development/pm-operating-loop.md.
When to Use This Skill
Pre-push validation · preparing PRs · monitoring CI · merging · releases · troubleshooting CI failures.
Quick Validation — the tiered gate loop (issue #1821)
scripts/agent-gate.sh is THE canonical pre-PR gate (issue #719): it mirrors the enforced CI gates plus
the local smoke suite and emits a machine-checkable summary block. A claim that "the gate passed" must come
from this script's summary (between the AGENT-GATE SUMMARY markers, ending in RESULT: PASS) — ad-hoc
cargo runs do not count.
Only RESULT: PASS/RESULT: FAIL is a verdict (#3041). The gate writes
RESULT: INCOMPLETE (gate did not finish) into the summary file at launch and overwrites it on
completion, so INCOMPLETE is a liveness placeholder, not a verdict. If you poll a summary file
instead of waiting for the process to exit, the predicate is
the RECORD grammar grep -qE '^RESULT: (PASS|FAIL)([[:space:]]|$)' "$AGENT_GATE_SUMMARY_FILE" — a bare
grep -q on the bare RESULT: token fires the instant the gate starts and would accept a just-launched (or
still-queued) gate as certified, and an unanchored form matches RESULT: PASSENGER.
COMPLETION AND VERDICT ARE TWO ASSERTIONS (#3750). The record grammar above is for full and --lite
ONLY, and must keep REFUSING PARTIAL, ERROR and REFUSED. An --only <component> run demotes success to RESULT: PARTIAL, so
that grammar spins on green there. Poll --only by EXIT STATUS (3 = completed PARTIAL) where you can
observe it, else by the ONLY grammar grep -qE '^RESULT: (PASS|FAIL|PARTIAL)([[:space:]]|$)'. --delta
is a THIRD mode with a THIRD set — it alone can terminate ERROR or REFUSED (7 emit sites, all in
run_delta; REFUSED arrives via emit_summary "$(_tree_result REFUSED)", so it looks unemitted and is
not), so a --delta poller on the record grammar hangs on a terminal outcome:
grep -qE '^RESULT: (PASS|FAIL|PARTIAL|ERROR|REFUSED)([[:space:]]|$)' — gate-liveness.sh's own
enumerated terminal set, token for token, so "what is terminal" is decided in ONE place. Then read the
component's verdict SEPARATELY, from its own line:
bash scripts/gate-component-verdict.sh "$SUM" --mode only --component <name> --run-id <id> (exit 0 PASS /
1 NOT-PASS / 4 COULD-NOT-MEASURE, no verdict available whatever the reason / 64 USAGE). It is NOT a
completion probe and has no opinion about liveness — never call it in a loop: establish completion
first with the grammars above or gate-liveness.sh, which is the three-valued liveness authority and
the only one of the two that may be polled (#3750 descoped a retryability taxonomy here, because it
told a lane a LIVE gate was permanently unmeasurable and an obedient lane relaunches it). A
completed run whose component SKIPped or is absent is NOT a pass —
and a SKIPping component still exits 3, so exit 3 is completion and never a green.
Every invocation — full, lite, and --only — MUST use the summary-file redirect (#1175/#2079). The
summary block is the only gate text an agent retains; never stream raw gate stdout into a persistent
context, and never read gate.log.
# ITERATE — every fix round. --lite components (exactly, per scripts/agent-gate.sh LITE_COMPONENTS):
# file-size · fmt · clippy (PER-PACKAGE scoped, #1844 — not whole-workspace) · roborev-lints ·
# scoped-tests (blast-radius: touched package --lib + the diff's new --test targets).
# COST IS A FUNCTION OF THE DIFF (#3764): ~1-5 min is the warm NARROW-diff case (median 1.4 min); a
# cqlite-core/src/ diff measures median 20 min (to 43 min local; up to ~104 min under peer load is
# REPORTED, #3764, not measured here), and a cold clippy alone adds 16-24 min whatever the diff. CLAUDE.md's Lite row has the full model.
# Emits a DISTINCT ==== AGENT-GATE LITE SUMMARY ==== block that must NEVER be pasted as the full SUMMARY.
AGENT_GATE_SUMMARY_FILE=/tmp/lite-<N>.txt \
bash scripts/agent-gate.sh --lite > /tmp/lite-<N>.log 2>&1 < /dev/null
cat /tmp/lite-<N>.txt
# BEFORE MERGE — the FULL gate, run EXACTLY ONCE, inside the flow-closer subagent (#2084).
# Its ==== AGENT-GATE SUMMARY ==== (ending RESULT: PASS) is the only run that counts; --lite never
# replaces it.
AGENT_GATE_SUMMARY_FILE=/tmp/gate-<N>.txt \
bash scripts/agent-gate.sh > /tmp/gate-<N>.log 2>&1 < /dev/null
cat /tmp/gate-<N>.txt
# Debugging only (output marked PARTIAL, never counts as the gate):
AGENT_GATE_SUMMARY_FILE=/tmp/partial-<N>.txt \
bash scripts/agent-gate.sh --only fmt,clippy > /tmp/partial-<N>.log 2>&1 < /dev/null
- Division of labor (#1855/#2084/#2079): the implementing subagent iterates on
--lite/ targeted tests and ends at commit + push + report. The ONE full gate of record and the final roborev pass run inside the disposableflow-closersubagent, NOT in the lead — that roborev pass goes through the ONLY sanctioned invocation,bash scripts/flow/roborev-review.sh --agent <agent> --model <model>(#2964; both flags required, never a bareroborev review --branch), and the closer retains only its==== ROBOREV REVIEW SUMMARY ====block, whose header is deliberately distinct from everyAGENT-GATE *SUMMARYso neither can be pasted as the other — the lead never runs the full gate and never reads its stdout; it retains only the closer's terminal packet (verdict, PR URL, summary-file path). The closer launches the gate viaBash run_in_backgroundand reads the SUMMARY from the file: a subagent that idle-waits on a 12-25 min full gate is killed by the 600s stall watchdog and orphans its child gate process. The closer has noAgenttool, so it requests C (spec-auditor) from the lead via aNEEDS-SPAWNpacket (#2668). - Queued gate ≠ hung gate: under load the full gate may queue for a #1825 slot (prints
waiting for gate slot (N in use)…once) then run 15-20 min — total wall time can exceed 20 min. Use a long Bashtimeoutorrun_in_backgroundand check for that line before assuming a hang (the default 2-min timeout truncates a queued gate). - Gate PASS ≠ CI green: the local gate uses pre-existing datasets and a subset of
--testtargets. When a change touches a regenerate path, a fixture parser, or a fail-closed CI guard, reproduce the actual CI lane locally before relying on the gate.
CI Monitoring
gh run list --limit 10 # recent runs
gh run view <run-id> # a specific run
gh run watch # watch live
gh run view <run-id> --log-failed # failed-job logs only
Merge
Merge is autonomous on green — once local certification holds (gate PASS + (design-driven)
spec-auditor C PASS + roborev clean — a terminal RESULT: PASS from
scripts/flow/roborev-review.sh, never NOTHING-TO-REVIEW), and after the pre-merge SHA assert + HOLD
re-read, arm
gh pr merge --auto --squash --delete-branch and stop; GitHub lands the PR when the #2433 required
check goes green (#2667), then flow-finalize <N>. Never ScheduleWakeup-poll a PR's own CI — --auto
replaces the busy-wait. Merge is not a human gate; hold only for a genuine design call, a scope/product
question, an unmet requirement, or a HOLD: merge after #N order. See
merge-process.md and docs/development/pm-operating-loop.md.
References
docs/development/pm-operating-loop.md— delivery model (tiered gate, merge-on-green, telemetry)- The
flow-*skills — pipeline stages (groom → activate → implement → address → finalize; board) - validation-checklist.md · merge-process.md — pointers
- Gate contract: https://pmcfadin.github.io/cqlite/agents-developing/gate-contract/