Zdůvodnění
Dne 2026-10-09 CI dvakrát odmítlo release 0.19.0, pokaždé kvůli něčemu, co lokální běh najde za pár sekund. Prvním byla strukturní kontrola, economics-claims, za 6 s a always: true. Druhým případ sady, 08, za 1 s. Ani jedno před pushem neběželo. Pre-push hook spustil finish --check, který hlásí požadované kontroly jako nikdy nenahlášené a žádnou z nich nespouští, a případy se vybíraly odhadem. Každé odmítnutí stálo okruh přes CI delší než hodinu, pak další derive za zámkem stroje, a to u jediné změny, na kterou vlastník čekal.
Celá úloha structure odpovědí není. Trvá asi 20 minut, sahá na API GitHubu a z jednoho stroje pushuje devět relací. Co push dluží, je ta část CI, která je levná, lokální a jistě selže stejným způsobem, vybraná podle toho, co CI měřilo.
Version 1 refused the push on every failed gate, and on a loaded machine that made the hook
the slowest part of the work: on 2026-10-20 a push of a rescue ref spent 3490 s in the step at
load 70 to 90, and a commit hook started a cold cargo build. On 2026-10-21 the owner relaxed
the cooperation model (rule project.the-cooperation-model-is-the-owners-word, version 2,
I3824): local gates are informative and CI holds the verdict. A finding CI will reach again
is therefore reported here, not refused (I3830). A check CI cannot reach, or reaches only
after a merge or a deploy it would break, still refuses: it takes seconds and nothing else
would say it.
Požadované chování
.githooks/pre-pushcallsscripts/ci/run-plan --before-pushafterfinish --check. The policy declares it (enforcement: structure-gates-on-push, wired bygit-hook:pre-push), sodoctorfinds a clone whose hook stops calling it: the commit hook reports that finding and CI's core-check, which runs doctor again, refuses it.- The step computes the push's plan, as CI does, from the commits the push sends: the
merge base up to
HEAD(scripts/ci-plan --head HEAD), never the working tree. An untracked file is not a change a push carries; on 2026-10-22 git's.merge_file_*temporaries, left at a checkout's root by an interrupted merge, put a push that only deleted remote refs into the full plan for more than 15 minutes (#2372). The step refuses the push (exit 12) when the plan cannot be computed.scripts/ci-planrefuses a base or head that names no commit. It used to pipe a failinggit diffintosort, whose status hid the failure, and so returned a plan computed from nothing with exit 0. - Kontroly. Spouští vybrané strukturní kontroly plánu, které nejsou v
network: trueoznačeny.ai/repo/ci/gates.yamla jejichž zaznamenané sekundy nejsou víc nežMJ_PUSH_GATE_SECONDS(výchozí 15). Sekundy pocházejí z.ai/repo/ci/structure-durations.tsv, kteréscripts/ci-baseline --structure-durationszapisuje z artefaktůci-metrics-structure, jež CI nahrává. Bere je od nejlevnější, dokud jejich součet zůstává vMJ_PUSH_GATE_BUDGET(výchozí 120). - Skipped gates. Every gate left out prints one line naming it and why: it reaches the network, it has no recorded duration, it is over the per-gate limit, or it is past the budget. A missing durations file refuses the push (exit 12).
- Případy. Spouští případy, které
.ai/repo/ci/suite-durations.tsvzaznamenal nejvýšeMJ_PUSH_CASE_SECONDS(výchozí 5), po čtyřech zároveň. Jmenovitě vynechává případ, který deklarujemajordomus-exclusivenebo spouští server (serve_up,mcp_shared,serve ensure|start, launcher MCP). Rychlé je to, co zaznamenalo CI. Případ se serverem měří časování a porty a na stroji, který sdílí devět relací, selže kvůli zátěži, ne kvůli změně: pět jich tak selhalo při prvním běhu. - Bound. It reports its measured time against
MJ_PUSH_BOUND(default 180). An overrun names the slowest steps and never refuses the push: a busy machine must not fail it, or people route around the step. - Verdict. A failed gate or case exits 1 with the tail of its output and a last line that
names it and says CI holds the verdict. The hook prints
reported, not refused: CI holds the verdictand lets the push through; CI's validate refuses the same head. Any other non-zero exit of the step, 12 above all, refuses the push: what cannot be measured is not passed. - Stdin. Každá kontrola a každý případ běží se zavřeným stdin. Stdin hooku je seznam refů.
test/run.shdal každému případu stdin runneru, protože úloha na pozadí pod řízením úloh si jej ponechává, takže případ, který četl stdin, čekal věčně na otevřené rouře: případ 54, 39 minut, téhož dne. - Deletions. A push whose every ref has an all-zero local sha carries no commit, so the hook
exits 0 before
finish --checkand the plan, as it does forrefs/majordomus/*(I4008: 16 deletions waited 2400 s on the build lock, 2026-10-22); one ref that carries a commit and the whole hook applies.
What refuses and what reports
| Hook | Kontrola | Local verdict | Proč |
|---|---|---|---|
| pre-push | bin/majordomus finish --check | refuses | the task's scope and contract exist only on this machine |
| pre-push | scripts/case-numbers check | refuses | each branch is clean alone; the pair exists only after a merge |
| pre-push | scripts/ci/run-plan --before-push, exit 12 | refuses | a plan that cannot be computed is not a pass |
| pre-push | scripts/ci/run-plan --before-push, exit 1 | reports | CI's structure job and suite run the same gates and cases |
| pre-push | scripts/rust-check --doc | reports | CI's rustdoc job refuses the same head |
| pre-commit | bin/majordomus doctor | reports | CI's core-check runs doctor and refuses |
| pre-commit | bin/majordomus-cli worktree guard | refuses | CI checks out a fresh clone and never sees which worktree a commit came from (rule project.worktree-topology) |
| pre-commit | scripts/pages current | refuses | a stale derive breaks the deploy for whoever lands next |
| pre-commit | scripts/ci/providers-check --permissions-only | refuses | the owner-authorized SSH permissions of every native client |
A reported finding prints one line on stderr, <hook>: <check> failed (exit <n>); reported, not refused: CI holds the verdict, after the check's own output. The hooks carry the
report function themselves; there is no sourced helper.
Chování při selhání
A refusing check exits the hook with its own code, naming what failed. A reporting check
never changes the hook's exit. There is no flag that skips any step, as there is no
--no-verify.
Ověření
test/cases/1021_a_push_runs_what_ci_would_find_first.sh řídí krok nad plánem a trváními, které zapisuje. Dokazuje:
- a cheap gate runs, and a cheap failing gate exits 1 with its output and a line that names it and says CI holds the verdict;
- každý druh vynechané kontroly je jmenován s důvodem: síť, žádné zaznamenané trvání, přes limit na kontrolu a za rozpočtem;
- a missing durations file, or a plan that cannot be computed (an unknown base), exits 12;
- hook volá krok, politika deklaruje vynucování a plán nese síťový příznak modelu;
- případ, který čte stdin v rouře, jež se nikdy nezavře, už runner nezablokuje.
test/cases/4008_a_push_that_only_deletes_plans_nothing.sh: a deletion-only push runs no
finish, case-numbers or run-plan, and a mixed push still runs all three.
test/cases/3948_a_local_gate_reports_and_ci_decides.sh runs the real hooks in a fixture
whose checks are stubs with chosen exit codes. It proves:
- a push whose step exits 1 exits 0, with the gate's line and the hook's report line;
- a push whose step exits 12, whose
finish --checkfails or whose case numbers collide is refused with that code; - a push that changes the crate and whose rustdoc fails exits 0 with a report line;
- a commit whose doctor fails exits 0 with a report line, and one whose
pages current, worktree guard or SSH permissions check fails is refused with that code; - each call is still on a line doctor's wiring check matches.
První běh kroku na vlastní branchi tu branch odmítl kvůli nepřipnutému sort (order-check), dřív než ji viděla jakákoli CI.