Repo-specific PR/CI conventions for an agent subscribed to PR activity (loopdive/js2wasm). Read this BEFORE acting on a CI or review event. Overrides the generic PR-steward defaults where this repo's merge queue, gates and hooks make them wrong — most importantly: agents NEVER enqueue, a bot `hold` is a real regression, and close+reopen is the sanctioned way to DEQUEUE for a push.
Install
npx skillscat add loopdive/js2/steward Install via the SkillsCat registry.
/steward — acting on PR events in this repo
Read this before acting on a CI or review event on a PR you opened or drive.
It does not relax anything the generic rules state as "never" (no skipping
or disabling tests; no rewriting someone else's history; no approving or
merging). It states the repo conventions the generic defaults get wrong here,
and each one below has bitten a real lane.
Authoritative sources this file summarises: `docs/ci-policy.md`
and CLAUDE.md. Where they disagree with this file, they win — re-verify rather
than trusting this summary's dates.
1. NEVER enqueue. The server enqueues.
.github/workflows/auto-enqueue.yml is the single enqueuer (#2786), triggered
on required-check completion. A dev/agent must not enqueue, and must never
re-enqueue: re-adding a PR that is in the in-flight merge group rebuilds
that group and cancels its run. A re-enqueue loop on the queue head cost
~3.5h of cancellation churn on 2026-06-20.
Your job on a green PR is to make it enqueueable and confirm it was taken —
not to push it into the queue.
2. Judge readiness from the real signals, not from mergeStateStatus
A SKIPPED required check SATISFIES branch protection. Jobs skipped by
their ownif:/path filter still publish a check run with conclusionskipped. On a docs-only PRequivalence-gateskips and the PR is stillCLEAN. Do not decide readiness by counting sixSUCCESSconclusions —
you will not find them and will wrongly conclude the PR is not ready. Judge
each REQUIRED check as green when its conclusion issuccessORskipped.Do not read
mergeStateStatusas the enqueue gate at all. Since #4094enqueueEligibility()inscripts/enqueue-green-prs.mjsdecides from four
real signals only — not draft, no hold label,mergeable === "MERGEABLE"
(the field, not the status string), andclassifyChecks()green. The
function's own comment is explicit: "mergeStateStatusis deliberately NOT
a parameter. It cannot be consulted even by accident." The oldENQUEUEABLE = {CLEAN, HAS_HOOKS}set is vestigial — read lines 631 and
1276, both of which say "Was!ENQUEUEABLE.has(...)".Being behind main does not block enqueue.
mergeStateStatusis also
stale — measured on the same PR minutes apart with no push, and a PR 4
commits behind readingCLEANwhile one 0 commits behind readUNSTABLE.
Diagnose from the check runs andmergeable, never from the summary string.A red NON-REQUIRED check DOES block enqueue. This file claimed the
opposite until 2026-08-29, and it is the costlier error of the two: it tells
you to stand down on the one signal that will strand the PR forever, because
nothing retries a non-required lane on its own.classifyChecks()applies its zero-FAILURE rule before the required-name
filter, so the required list never gets consulted for a failing row:// Zero-FAILURE rule: applies to EVERY check, required or not (#3878/#3904). if (!PASSING_CHECK_STATES.has(state) && !PENDING_CHECK_STATES.has(state)) { failures.push(`${name}: ${state}`); continue; // ← returns green:false regardless of `required` } if (!required.has(name)) continue; // ← only reached by passing/pending rowsPASSING_CHECK_STATESis{pass, skipping}andPENDING_CHECK_STATESis{pending, queued, in_progress}— every other state is a failure, whoever
published it.Observed on PR #5260 (2026-08-29): all six required checks green or skipped,
mergeable: true, nohold— and theauto-enqueuerun logged- #5260 skip (failing-checks: quickjs eval-engine lane (non-required): fail)
on every sweep. Read theauto-enqueuerun's own log when a PR looks ready
and is not moving; it names the reason verbatim.So a red non-required lane is work, not noise. Treat it like any other CI
red under §CI red: rule out that it is not this PR's, re-run once if you have
the means, and if you do not, say so and keep the PR watched — it will not
clear itself.BEHIND→ do NOTHING. Let the PR's CI finish. This is the one that looks
most like a call to action and is not. Behind-ness does not block enqueue:scripts/enqueue-green-prs.mjsderives eligibility from checks + draft +
hold-labels and says so outright — "mergeStateStatusis deliberately NOT a
parameter. It cannot be consulted." (#4094) — and the merge queue builds the
group against main anyway.Merging main into a
BEHINDPR restarts its ~10-minute PR CI from zero.
On a busy day main merges faster than that, so the checks never complete and
the PR becomes permanently un-enqueueable. #4520 cycled 70+ minutes this way,
andauto-refresh-prs.ymlskips in-flight heads specifically to avoid it.
A steward that "helpfully" merges main on everyBEHINDcheck-in is the
livelock. If a refresh is genuinely wanted, that cron owns it — it runs every
20 min and already knows when to hold off.DIRTY→ a real conflict; resolve it (see §5). On a PR whose own files
conflict, first suspect a duplicate merge — another lane landing the same
mechanism — rather than ordinary drift.linear-testsis not a required check (#3934), despite older docs.
Verify the required list rather than trusting any doc:
gh api repos/loopdive/js2wasm/rules/branches/main \
--jq '[.[]|select(.type=="required_status_checks")|.parameters.required_status_checks[].context]'Enforcement is a repo ruleset, not classic branch protection — the classic
endpoint answers 404 Branch not protected.
3. Two required checks are DESIGNED green no-ops at PR level
check for test262 regressions and merge shard reports green-skip onpull_request (SHARDS_RAN: false) — the heavy shard matrix ismerge_group-only. Green there is not conformance evidence. The real
regression gates run in the merge_group re-validation on the merged state,
which is why a fully-green PR can still fail the queue.
4. A bot hold label is a REAL regression, not noise
When github-actions[bot] adds hold plus anauto-park-bot:merge-group-failure comment (#2547), it caught a merged-baseline
regression that PR-level checks cannot catch.
- Never remove a bot park-hold without first diagnosing the cited run.
- A bot park-hold is not your own manual
hold. Confusing the two re-admits a
regressing PR — a dev did exactly this on #1960. - Re-enqueue at most once, and only after a confirmed fix or a confirmed
flake established from the regressed-test delta. - A held PR is skipped by the auto-enqueue backstop, so it strands until
someone resolves it. The cron will not rescue it.
5. The merge queue LOCKS the branch — close+reopen is how you dequeue
A queued PR rejects pushes with GH006: … queued for merging cannot be updated.
Draft does not dequeue. Closing and reopening the PR does.
The generic rule "never close and reopen the PR to kick CI" still stands and is
not what this is. Kicking CI is forbidden; dequeuing so you can push a real
change is the sanctioned mechanism, and it is the only one. State plainly in
your report that you dequeued and why, so it is not mistaken for a CI kick.
git merge is blocked in the repo root by .claude/hooks/check-cwd.sh for
non-tech-lead agents. git pull --no-rebase --no-edit origin main is not
blocked and does the same job. Server-side, theupdate_pull_request_branch API also works and avoids the hook entirely.
Never rebase and never force-push. Public main is append-only; fix bad
commits forward with a revert PR.
6. Gates and hooks that will reject your push
- Never pass
--no-verifytogit commitorgit push(project-lead
order, 2026-08-22). If a hook is slow useSKIP_SLOW_PRECOMMIT=1, which
still runs the fast checks, and run the heavy gates by hand. - Run all five source-ratchet gates before committing, chained so a failure
blocks:
Never pipe a gate whose exit status you need —node scripts/check-loc-budget.mjs && node scripts/check-func-budget.mjs \ && node scripts/check-coercion-sites.mjs && npm run -s check:oracle-ratchet \ && npm run -s check:dead-exportsgate | tailreportstail's status, so a red gate reads as green. - CI diffs the merge preview, not your fork point, so a gate can pass
locally and still failquality. Simulate it:LOC_GATE_BASE=$(git rev-parse <upstream-main-tip>) node scripts/check-loc-budget.mjs - Growth allowances go in the PR's own
plan/issues/*.mdYAML frontmatter with
a dated rationale. Never editscripts/*-baseline.json— main is its sole
writer. - Your commit message must end with a line containing just
✓— the
pre-commit hook requires that checklist sign-off. - The commit author must not match
claude|anthropic
(.husky/commit-msg, project-lead order 2026-08-09). Claude belongs only in aCo-Authored-By:trailer. If your harness asks you to setuser.email noreply@anthropic.com, that conflicts with this hook: do not
bypass it — report the conflict. - New codegen needing type info must use
ctx.oracle, not the rawts.TypeChecker— rawchecker.getTypeAtLocation/ctx.checkertrips the
oracle-ratchet gate.
7. Conventions for what you write
- PR bodies link issues to the website page, never bare
#NNNN— PR and
issue numbers share one sequence, so a bare#NNNNautolinks to an unrelated
PR. Use[#NNNN](https://js2wasm.loopdive.com/dashboard/issue.html?slug=<file-basename-without-.md>).
Commit messages keep plain#NNNN(tooling greps them). - Docs-only changes go in ONE open PR. Push docs commits onto the open docs
PR's branch instead of opening a second. An implementation PR still carries
its own issue-file status edit. - Ad-hoc probe/debug/repro files go in
.tmp/— gitignored, invisible to
vitest.
8. Verifying a test262 claim before you assert it
Judging a row by "it compiled" is how #4764 shipped a regression. Use the
runner's own verdict:
npx tsx scripts/run-test262-paths.mts <file-of-paths> --isolate--isolate is mandatory for any family that mutates a shared intrinsic.
Every row otherwise runs in one realm, and the class/dstr/*-array-prototype.js
group replaces Array.prototype[Symbol.iterator], which breaks for…of inside
the runner itself — the run dies after doing all the work and every row past
the first poisoner is meaningless. Measured difference on one 8-row slice:
in-process pass 1, error 7; --isolate pass 6, fail 1, error 1.
Keep slices small. Devs do not run full local test262; CI validates
conformance.
9. Related skills
Prefer these over improvising: `/dev-self-merge`
(green-PR self-check), `/test-and-merge`,
`/analyze-regression` (diff two runs),
`/handle-regression` (single fix),
`/regression-triage` (bulk-classify),
`/pr-conflict-refresh`.