A standalone, per-repo CI/CD fighter that drives a stuck pipeline to a legitimate green — or improves red→yellow, or fails and hands the human a structured report. It wins the gate; it never cheats past it.
|
Note
|
Status — v0.1 in active development (updated 2026-10-28). The engine and
its formal invariant are real and verified (see below); live gate fetch via gh
landed in #6; squabble fight (classify each red as self-win | escalate |
assign-owner), the squabble-app loopback HTTP backend, and the boj-server
expert summoning (--summon, feature boj) landed in #13. squabble fight
--apply now enacts the first appliable self-win — stripping an on..paths
trigger filter that strands a required check — by writing the workflow file
(propose remains the default; --apply never commits, pushes, or re-runs CI).
The fetch reads *both gate surfaces — branch rulesets AND classic branch
protection (#100) — and probes the repository’s Actions permissions whenever a
required context refuses to start, so a STARTUP_FAILURE is attributed to the
posture that caused it rather than to the reusable’s owner or a security scan
(#15). Still open: applying the other self-wins (required-context reconcile,
reusable-workflow re-pin — these need the GitHub ruleset API / a target SHA) and
the GitHub App webhook leg (pending the owner’s hosting decision). Zero-job
startup_failure runs — no job, therefore no check run, therefore nothing in a
PR check list — are now first-class: squabble zero-job attempts to attribute
each observed run to a moved coordinate or an allow-list gap, and reports
Unattributed — with the probe failure quoted, and exit 4 — when the evidence
does not support a class; squabble chains compares
canonical upstream names through REST so a move is named before a run proves
it. Both are diagnosis only; squabble ≠ bypass still holds. See
docs/CHARTER.adoc for the design and open owner decisions.
|
- Chains: cross-repo CI dependencies (read-only)
- Zero-job startup failures: the CI that never reports
- Rust deductive verification (Creusot)
- Downstream integration contracts (pinned, immutable revisions)
- Core principle: squabble ≠ bypass
- What’s in the box
- Verify it yourself
- The v0.1 fight playbook (gate-deadlock class only)
- Licensing
squabble chains maps which repos' CI depends on which, through reusable
workflows and pinned actions. Nothing is written anywhere.
squabble chains . ../standards gh:hyperpolymath/typed-wasm --follow 1-
Blocking (exit 4): dependency cycles, and dead upstreams. A deleted action repo produces no check run at all, so the board reads green.
-
Warnings: diamonds (one repo’s CI receives an upstream at two pins, one of them via a reusable workflow), pin skew across repos, tag/branch pins, and moved upstreams (see below).
-
It also prints an upstreams-first landing order and the longest chain.
Every upstream coordinate is resolved through REST with redirects followed, and
its canonical full_name is compared with the name as written — names, never
pins. A difference is reported as a moved upstream: a reusable coordinate under
a renamed/transferred owner can die as a zero-job startup_failure ("workflow
was not found") that no PR check list shows, so it is named before CI goes
quiet. It stays a warning while the redirect still works (that is a risk, not an
observed failure); squabble zero-job is what turns an observed failure into a
blocking finding. The same canonicalisation groups pin skew, so the old and new
spellings of one upstream cannot hide a split. --no-canonical skips the REST
reads and says so — a deliberate blind spot, not a clean bill.
An upstream that was never probed is listed on the frontier, not assumed
healthy. See docs/proposals/squabble-modes-and-app-layer.adoc for where
this is heading: upstream conformance, custodian rulesets, and the app-aware
layer.
Some workflows fail before any job exists. GitHub creates no check run, the PR check list stays tidy, and a required context is simply absent. Two first-class classes are modelled, each with its own move:
| Class | Move | What it means |
|---|---|---|
moved coordinate |
|
A reusable coordinate names an owner/repo that was renamed or transferred. Repoint to the canonical name carrying the same pin — repointing is not re-pinning, and no pin is ever resolved on the way. |
allow-list admission required |
|
The coordinate (or its canonical target) is refused by the caller’s
|
squabble zero-job . --limit 30 # attribute the latest zero-job runs
squabble zero-job . --limit 30 --json # the same, machine-readablePer run it reports the repo, the workflow, the coordinate as written, the
canonical target, and the required allow-list pattern — plus the sequencing: fix
the repoint first, then admit the canonical pattern if the run is still refused.
Exit 4 means zero-job runs were observed, attributed or not: a run it cannot
attribute is still a run that never reported. Exit 0 speaks only for the
scanned window.
zero-job is outside squabble-contract/1.0.0: that revision’s surface stays
at four subcommands, and nothing here adds meaning to it. Like chains, the
command is read-only and writes nothing, so its absence from the pinned
contract costs a consumer nothing.
Both moves only let a check start. Neither drops a required context, weakens a
check, nor edits settings: squabble ≠ bypass holds, and nothing here is a
green. A missing or skipped check cannot be made green by a diagnosis.
The estate requires its Rust to carry verification obligations, the twin of the
SPARK proofs. verification/squabble-creusot states the engine’s load-bearing
invariants — acceptance ⇔ every requirement satisfied and the set non-empty, a
skip is not evidence, no diagnosis is a green, admission is refused exactly when
it is required, a repoint carries the pin verbatim — to
Creusot, and mirrors them as ordinary
`#[test]`s against the real code so drift is caught on stable Rust.
just verify-status # which toolchain and prover are present
just verify-install # stable + Creusot's pinned nightly + Creusot itself
just verify-mirror # stable Rust: the model vs the real engine
just verify-creusot # the prover: discharge the obligationsStated, not proved. No obligation has been discharged, and this repository’s
own Rust CI currently ends as a zero-job startup_failure, so CI has not
compiled the workspace either. .github/workflows/creusot.yml calls the estate’s
creusot-gate.yml, which fails a claim of proof with no contracts behind it and
reports no proof performed rather than a verdict when the prover cannot be
shown to work. Details and the licence note: docs/CREUSOT.adoc.
contracts/ publishes two contracts that other repositories integrate against.
Both are consumed by revision id plus a sha256 manifest, never by a branch:
|
The read-only CI invocation: which subcommands are in contract, the six read
permissions the gate needs (and the |
|
The evidence format a Rust repository submits to Proven-Tests: real executed tests with a log digest, example/property/proof told apart, coverage derived only from executed non-debt passes, the required Idris2 toolchain for the framework itself (not for submitting), and a minimal downstream Rust fixture. |
just contract-test # both conformance suites
just contract-pin squabble 1.0.0 # verify a pin as a consumer does
bash contracts/bin/verify-pin.sh proven-tests 1.0.0Nothing in either contract writes: no merges, no re-runs, no edits, no admin
overrides. The consumer audit refuses a workflow that grants a write scope, pins
to a moving ref, masks a gate step, or calls fight --apply.
Every "green" the squabbler declares is backed by the gate’s own required
check passing legitimately — never by an admin override, an enforce_admins
toggle, or removal/rename-away of a required context. An admin-merge cheats past
the gate; a squabble wins it.
This is not a code-review promise. It is a machine-checked invariant: the
SPARK package in spark/ proves that the only transition into Green is "a
required check actually ran and passed" — dropping the required set, renaming a
check away, or any override are provably not paths to green.
|
The fight engine. Pure, detachable, no hypatia/estate dependency — |
|
The shared host brain: a2ml ecosystem-context reader + workflow ground-truth + the fight planner. One planner for the CLI and the App, so they cannot drift. |
|
|
|
The loopback HTTP backend (axum, |
|
Forge access: GitHub GraphQL via |
|
The formal heart. |
|
The Rust twin of |
just test # 7 engine tests pass
just prove # gnatprove: postcondition proved (squabble ≠ bypass)
just demo # diagnose a stale-required-context deadlockMined from the 2026-06-22 rsr-template-repo lockdown (effectively a manual
squabbler run). Every move satisfies the gate:
-
Stale required-context — reconcile the required name to the real emitted name.
-
Path-filter deadlock — inject the documented job-level pass-through.
-
Reusable-workflow pin drift — re-pin a stale/broken SHA to current.
-
Modify/delete & rebase conflicts — resolve and re-run.
-
Check-name ground-truthing — the shared substrate for moves 1 and 2.
-
Licence-policy drift — doctrine-constrained: adding a missing canonical
LICENSES/text is authoring (may apply); rewriting an existing file’s SPDX header is relicensing (flag, owner-only — never edit). -
Non-functional gate — the required check’s own script fails unconditionally (cannot be won from the PR side; mined from
hypatia#566): recognise, stop re-diagnosing, escalate with evidence. -
Actions-policy
startup_failuredeadlock (issue #15) — the repository’s own posture refuses to start a required check: an empty/lackingselectedallowlist (ReconcileActionsPolicy/SetActionsAllowedAll— estate posture of record isallowed_actions=allwithsha_pinning_required=trueKEPT), or a tag-pinneduses:under enforced SHA pinning (PinWorkflowActions). The fetch probesactions/permissionswhenever a required context resolves toSTARTUP_FAILURE, so the fight proposes the settings/pin move instead of mis-attributing to the reusable’s owner or a security scan. These moves only let a required check start; none weakens the gate. -
Zero-job
startup_failure— a workflow that dies before any job exists, so no check run is created and no PR check list shows it. Two distinct classes: a moved coordinate (reusable owner renamed/transferred) →RepointToCanonicalwith the same pin; and allow-list admission required (canonical coordinate refused by the caller’sselectedallow-list) →AllowListAdmissionRequirednaming the exactowner/repo@*pattern.squabble chainsresolves every upstream’s canonicalfull_nameand flags moved upstreams and canonical-grouped pin skew before a run proves it;squabble zero-jobreports attribution per observed zero-job run. Diagnosis and sequencing only — neither move makes anything green.
Red→green code fixes are deliberately out of scope for v0.1: "fixing" a failing test by weakening it would violate squabble ≠ bypass.
