Generated from
cat-harness/skills/sdlc/sdlc-core/platform-gates.md— do not edit here.
The platform’s own gates — run what CI will run
One command:
bun run cat gates # the fast set
bun run cat gates --all # plus the jobs that need a browser
bun run cat gates:list # see them without running them
Run it before you push, not after CI tells you.
Why this skill does not contain a list of gates
Because a list here would be wrong within a week, and a wrong list is worse than none: it reads as authoritative, so nobody checks it.
bun run cat gates derives the set from .github/workflows/code-quality-gates.yml
at the moment you run it. Add a gate to CI and it is in your next local sweep
with nothing to update here.
This is not a hypothetical drift. The agent who wrote this skill had spent
that session running 17 gates by hand, repeatedly, and reporting them as
“the gate sweep” in commit messages and PR bodies. The workflow runs about
thirty. The hand-list was a guess that read as coverage — and the
difference included check:tools, check:voices, kg:schema:check,
readme:audit and agent-memory:check, none of which had been run once.
Why the CI workflow is the authority, and package.json is not
The failure being prevented is specifically green here, red in CI. So the question is not “what checks exist” — it is “what will CI run against my change”. Only the workflow knows that.
package.json over-answers it. Measured 2026-09-19: of 33 check: / :check
scripts, 21 appeared in no workflow at all. Running everything registered
would fail you on things CI does not gate. The workflow’s own comments name
six it deliberately excludes, each with a reason — check:ci-health is a
report rather than a gate and describes main rather than your diff;
check:corpus-gate needs a folio the platform does not carry;
check:upstream-pins runs weekly on its own schedule.
Those reasons live in the workflow, next to the exclusion. Read them there.
bun test green is not the gates green
They come apart in practice, and the gap is the whole reason this exists. Measured 2026-09-19 on one change:
| what ran | result |
|---|---|
bun test | 2891 pass, 0 fail |
tsc --noEmit | failed — a readonly array from as const |
check:schema-nodes | failed — a new module with no @graphNode tag |
The third also broke a QA sidecar comparison, so one cause produced two symptoms in different gates. An agent running only the test suite would have pushed all three and learned about them from a red PR.
…and the gates green is not the PUBLISHED PAGE green
The section above is one step of a ladder, and the step after it is the one that ships a broken site. Measured 2026-09-20 on a single change:
| what ran | result |
|---|---|
bun test | 3517 pass, 0 fail |
eslint, tsc --noEmit | clean |
twelve *:check gates | all green |
| e2e and accessibility suites | pass |
the actual published index.html | 3 escaped <article> tags, 0 real |
A Liquid {% endif -%} in docs/_includes/landing.html right-stripped the
newline before the next attribute. The emitted tag had two attributes with no
separator between them, index.md is markdown, so Kramdown refused the block as
HTML and escaped the whole of it — the live landing page printed
<article class="fa-sticky …" as visible words and every sticky rule was
dead.
Not one of those gates was wrong. They all read a source, and the source was valid HTML. The defect exists only in the markdown converter’s output, so nothing readable from a checkout could see it.
Two things follow, and the second is the one to act on:
- Gate coverage is bounded by what a gate can look at. A repository can have a dense wall of green checks and no check at all on the artefact a reader loads. Ask what the gates read, not how many there are.
- When your change alters a rendered page, build the page. The site build is
not in the fast loop and there is no
*:checktwin for “the output is well-formed” —check:escaped-markupnow asks one narrow version of that question, and only indocs-site.yml, where_site/exists. If the real build will not run locally (thejust-the-docsremote theme 403s through the agent proxy), stub the theme and build anyway; never substitute the thing under test.continual-progress§”A template is not a page” carries the discipline.
…and a green gate is not a VERIFIED FACT
The step after that one. The section above is about a gate that could not see the artefact; this is about a gate that saw a different question and answered it correctly.
Measured 2026-09-22 on the work-plan consolidation’s own first commit
(issue #956). A session wrote three delegated claims, each citing bean k59d —
milestone critical paths go stale here — and each instructing its reader to
re-verify the path before acting on it. It then copied p5wm’s critical
path into one of them verbatim, through four completed beans.
| claim | carried the stale content | check:stale-paths |
|---|---|---|
10uc | yes, as a numbered path | ✗ failed the push |
w0cr | yes, as prose | ✓ passed |
Two lessons, and they are different failures that look identical from the outside.
1. A rule that lives only in prose has a compliance ceiling nobody measures.
Knowing about k59d — citing it, quoting it, instructing three other agents
about it — did not prevent reproducing k59d in the same commit. The gate
worked and the discipline did not. When you find yourself writing a rule into
prose, ask what would fail if somebody ignored it; if the answer is nothing,
you have written a wish. The measurable form of the same rule is a check, a
schema field, or a test — and bean-blocking’s status: blocked is the worked
example of the other outcome: a prescribed form the tool refuses to store has a
0 % ceiling, and across 99 in-progress beans it was honoured exactly 0 times.
2. A gate’s silence is bounded by its stated scope, and w0cr is why. check:stale-paths judges two shapes — an id adjacent to an arrow, and a
numbered step under a path heading — and declines everything else, on
purpose and in writing. Prose that merely mentions a closed bean is outside it.
So w0cr passed while carrying the same defect, and a reader who stopped at the
tick was entitled to believe it.
Green over a narrow rule is worse than red, because red is examined. This is
1xhc’s “a gate that does not fire is indistinguishable from one that passed”,
and it is the same shape as check:ci-health reporting eight workflows green
beside 31 further workflow files that produced no run at all — which it
labels unjudged, not green. Anything summarising that as green CI is the
defect.
So, before quoting a tick as evidence:
What does this gate say it judges, and is my claim inside it?
Read the check’s own header — every one here states its scope and its declined cases — rather than inferring the scope from the fact that it ran.
Fast versus full, and why the split is not a judgement call
It is job membership in the workflow, read at run time.
- fast — the
typescriptjob. No browser. This is the inner loop. --all— adds the jobs that install Chromium.render:bpmn:checkis there because bpmn-js renders through a browser.
That last one is worth knowing about: it once passed “locally” only because a browser had been staged earlier in the same session. Green on that machine, red on a clean runner — the exact gap these gates close, inside the gate tooling itself.
Running “the gates that relate to my change” is not running the gates
Pick the gate set by JOB MEMBERSHIP, never by which gates you judge
relevant. bun run cat gates --all is the whole point of there being a command;
choosing a subset re-introduces exactly the gap the command closes.
Measured 2026-09-20, PR #526. A change added two collectors to
kg-export.ts. Before pushing, the author ran bun test (3902 pass), tsc,
eslint, and then four gates chosen by hand as the ones the change touched —
check:instance-render, check:declared-assets, kg:audit:check,
check:harness-dirs. All green. CI failed on ns:check, a gate not in
that list, and the failure was a direct consequence of the change:
2 minted term(s) have no definition:
· Asset
· assetRole
The reasoning that produced the subset was sound and that is the trap: every gate in it really was related, and the one that failed was related in a way the author had not thought of. A subset chosen by an author tests the author’s model of the change, and CI tests the change. Those differ precisely where the bug is.
The cost is asymmetric and small. gates --all takes minutes on one machine;
a red CI run costs a push cycle, the reviewer’s trust, and an event that wakes
somebody.
And the follow-up push failed too, for the adjacent reason
The fix for the above ran gates --all and reported 60 of 60. CI then
failed again, on kg:audit:check:
1 sidecar(s) are stale. Run `bun run cat kg:audit` and commit:
· test/results/kg-qa/skills/sdlc/sdlc-core/platform-gates.kg-qa.json
The gates run was launched, and while it ran this very file was edited to record the first lesson. So the run verified a tree that did not contain the change being pushed, and its “60 of 60” was quoted as though it did.
A verification names a TREE, not a branch. Anything edited after the run started is unverified, however green the run was.
bun run cat gates reads the WORKING TREE, not the commit, which is the
mechanism behind that sentence and worth knowing on its own. A third instance,
2026-09-20: a splice script corrupted <name>.json while a run was in
flight, and 40 of 61 gates failed reading a file that was invalid for about
a minute. Every one of the five failures the summary named passed when
re-run by hand. So a mid-run edit does not merely leave the change unverified
— it can make the run report failures that were never in any commit, which
costs more time than the run saved.
This is the harder half to notice, because nothing looks wrong: the command really did pass, the output really does say 60 of 60, and the number is simply about a different tree than the one that got pushed. Run the gates after the last edit, not alongside it — and if you must edit while they run, the run is spent and needs doing again.
Editing a SKILL is the case most likely to produce this, because a skill body has two committed artefacts generated from it, and both go stale the moment the body changes. A skill edit is three files:
| file | regenerate with |
|---|---|
the body, skills/<pkg>/<name>.md | — you edit it |
test/results/kg-qa/.../<name>.kg-qa.json | bun run cat kg:audit |
docs/reference/skill-instructions/<name>.md | bun run cat-harness-tools/scripts/gen-skill-docs.ts |
The kg-qa row is a derived QA result: its record is the commit-keyed entry
the CI job qa-publish stores on the orphan qa-reports branch (arc 3fva),
and the committed copy goes when bean 5hox removes it. Until then
kg:audit:check still compares against it, so it is still regenerated here —
and bun run cat skill:register does all of it in one command.
This paragraph said “at least two files” and was wrong on its first
commit — it named the sidecar and missed the generated docs, and
gen-skill-docs --check duly failed, 1 of 60. Which is the lesson about
incomplete regeneration being itself incompletely regenerated, so it is left
recorded rather than tidied away: “at least” is what a writer reaches for
when they have not counted, and a gate counts.
The specific rule that was missed
termIri("X") MINTS a term, and a minted term needs a gloss. Adding a new
@type or a new property in kg-export.ts is therefore a TWO-file change:
the emitter, and schemas/vocabulary.ts (CLASS_GLOSSES for a type,
PROPERTY_GLOSSES for a property). ns:check is what refuses the half of it
that was done alone, and it says so in one line.
Note this is a different gate from the export’s own undeclared-term report,
which the same change had already satisfied. Getting past that one reads like
having satisfied the vocabulary, and does not: one checks that the JSON-LD
@context binds the term, the other checks that the namespace DEFINES it.
Both must pass, and passing the first is not evidence about the second.
What a *:check failure is telling you
Most of these gates compare a committed artefact against what its generator would produce now. A red one almost never means “the artefact is wrong”. It means you changed an input and did not regenerate.
So the fix is to run the generator and commit its output — never to hand-edit the artefact into agreement. Hand-editing makes this run green and the next regeneration noisy, and it silently detaches the artefact from its source.
QA gates are moving to a different shape, and their failure means something
else. Arc 3fva takes derived QA results off main, so a gate over the
qa graph cannot compare against a committed file for long. Those gates now
compute and judge: --check writes nothing, and fails only on a finding
that is NEW against a baseline — the committed working copy for now,
--against <ref> on the qa-reports branch after. A missing baseline is
UNKNOWN, printed and never a pass, never a fail. The four judge states are
ok (0), finding (1), unknown and error (2) — JUDGEMENT_EXIT in
scripts/qa-results.ts. A red one of THOSE is a real finding your change
introduced; regenerating and committing does not clear it, fixing the subject
does. Which gates have moved is in each script’s own docblock; do not infer
it from this paragraph.
Each *:check script has a generator beside it under the same stem; bun run
gates:list shows the check, and the script it checks is named in the check’s
own output when it fails.
…and a green gate set is not a CLEAN RUN
bun run cat gates snapshots the tree before the first gate and after every one, so
every gate can pass and the run still exit 1:
✗ every gate passed, and the run is NOT clean — 1 gate(s) changed the tree.
The gates after the mutation were handed the repaired tree, so their verdicts
describe a state you have not committed. The discipline is in the skill, not
here — gate-tree-mutation carries the two causes, the
one command that tells them apart, and the question that settles the second: does
the churning field describe the subject the artefact is about, or the run
that produced it? Only the first is a reason to write. Bean 3ozg is its worked
example — an engine_version counted as substantive rewrote 72 committed sidecars
on every branch — and the skill also carries why discarding such a churn is a
workaround whose habit is the defect.
Three things this skill will not do for you
It does not replace reading the failure. The runner names the failing command and the CI step it belongs to, so a local failure and a red PR are findable by the same string. Read what the gate printed.
It does not tell you a gate is wrong. A gate whose purpose you cannot state gets “fixed” by being made to pass, which is how a check becomes decoration. Every gate in that workflow carries a comment saying what it caught and when. If you think one is wrong, that comment is what you are arguing with.
It does not make --all optional before a push that touches a diagram.
Anything under processes/ changes a rendered SVG, and the check for
that is in the browser job.
If bun run cat gates reports no gates
It fails rather than exiting clean, and that is deliberate. An empty sweep
that exits 0 is indistinguishable from a passing one — the defect this
repository has paid for three separate times: a grep over zero Lean files
printing OK, a ruff scan of missing paths reporting a clean baseline it had
never computed, and readme:sync:check passing over a README with no markers.
A filter over nothing passes. If you see that refusal, the workflow was renamed or restructured and the reader needs fixing — not the gate list.
Related
.github/workflows/code-quality-gates.yml | the authority, with a comment per gate saying what it caught |
scripts/gates.ts | the reader, and the vacuity guard |
continual-progress | commit early, open the PR at the first commit — this is the step before the push |
prepare-merge | the pre-merge recipe, which runs these plus content-type gates |
ci-health | whether the workflows themselves are working — a different question |
gate-tree-mutation | reading NOT clean — a green gate set over a tree a gate repaired |
Processes that run this skill
| process | step(s) that name it |
|---|---|
| Code change and review | Run the platform’s own gates |
| The gates a change must pass before it can merge | Verdict reuse: tree already green? (NOT A GATE); Import hygiene (HARD) and advisories (WARN-ONLY); TypeScript: lint, types and tests (HARD); End-to-end + accessibility (HARD); Repository gates (HARD); Registered gates that never run (HARD); Skill-registration chain (UNMASKED) |
| A merge train | Run the gate set on the train (calls a sub-process) |