Shared skills, stage-gate tooling, and learnings for Mendix migration and development projects.
Serves four entry modes (see skills/conversion-runbook.md → "Entry Modes"): migration (legacy source, all stages), requirements-driven (specs/SME input, no legacy code, stages 1–6), greenfield (stage 5 onward), and change an existing app (a live .mpr, a slice being added or altered — stages P, 0–6 per slice, skills/existing-app-change.md). Plus à-la-carte use with no pipeline at all — auditing or regression/e2e-testing an existing app routes straight to skills/existing-app-assurance.md, skipping intake/stages/gates entirely.
-
CONVERSION-RUNBOOK.md(root) — thin "how to start" pointer. -
toolkit-guide.html(root) — the visual onboarding page. First-touch rule — this is the single source of the rule; every other mention defers here:[ -e "<project-root>/.claude/.guide-shown" ] && DO NOT OPENOpen it in the user's browser (
open toolkit-guide.html/xdg-open) only when that sentinel is absent, and immediatelytouchit so no later session repeats.bin/init-project.shopens it at scaffold time and writes the sentinel then. The user may also ask for it by name at any time — that's always allowed and does not consult the sentinel.Do not re-open it on a hunch that the user "seems unsure" — a session cannot tell a first-timer from someone who has seen it fifty times, and every session that guesses guesses "yes". The sentinel is the only test. (Real incident, 2026-08-12: this rule was stated conditionally here and in
README.mdbut unconditionally inskills/conversion-runbook.md,CONVERSION-RUNBOOK.md, and insidetoolkit-guide.htmlitself. Because the runbook is the one file every session is required to read first, every pipeline session opened the guide, and the user got a browser tab per session.)It's also the shared CSS shell/tokens for every stage HTML surface — that use has nothing to do with opening it.
-
skills/conversion-runbook.md— the spine: 9-stage matrix, interview protocol, gates, entry modes. Start here when unsure what stage anything is in. -
bin/init-project.sh <project-root>— the one-command install:intake.md,PROJECT.md,CLAUDE.local.md(runbook wiring), all six agent stubs,index.htmldashboard; opens the guide. Idempotent. -
bin/gate-check.sh <project-dir> [stage]— mechanical stage gates; regenerates the project dashboard from real files. Verdicts arePASS/PENDING(nothing there yet) /FAIL(there and wrong) /WAIVED(declared out of scope in the register, with a reason, via--adopt/--waive) /MANUAL. Never infer a project's position — read the register line, or ask. It also runs the obligation check (bin/lib/obligations.tsv+bin/lib/obligation-check.sh): per-module passes that owe a mark — LOOK, wiring sweep, journeys, coherence — each with the artifact that proves it and the denominator that artifact must state. A pass nobody performed reportsPENDING/FAULT, never green-by-absence;--waive look/<Module> --reason "..."records a pass deliberately not performed, and obligations below an--adoptpoint reportADOPTED. Alongside it runs the artifact check (bin/lib/artifact-manifest.tsv+bin/lib/artifact-check.sh): the producer-side mirror — one row per artifact a stage's "Agent produces" row promises, each row citing the consumer that reads it, so a stage that never emitted its artifact reportsPENDINGout loud instead of being found by a human reading a failed project. File presence only, never content quality; waivers are register lines (Waived artifact <id>: reason, or the stage-level waiver covering its artifacts wholesale). Third, the source ledger (bin/source-ledger.sh, over the every-file inventorybin/source-sufficiency.sh initwrites): each source file must name the artifact that consumed it — text and embedded images/pages — or carry a--waive source/<file>register line; this one blocks Stages 1–2, because "the triage already used the deck" is a grep, not a conversation (real incident, 2026-09-02). -
bin/sync-project.sh <project-root>— run after every toolkitgit pull: refreshes the artifacts that were copied into the project (new intake questions, untouched agent stubs) and flags stale baseline routing. Referenced skills need no sync — they update with the pull.
Every stage and module build follows the Live Checklist Protocol (skills/conversion-runbook.md §1b): post the stage's checklist in chat at start, update with ✅/🔄/⬜/❌/⏭ marks as items land, full repost before every gate. No stage works silently between gates. This is not build-phase-only.
All gate decisions land in the consuming project's PROJECT.md, marked CONFIRMED or ASSUMED. The skills/checkpoints/ CAC files are the packaged mechanism that runs the runbook's interview protocol at the six busiest transitions — they write to PROJECT.md, never to a separate state file.
Ask, then stop. Gate questions are actually asked in chat — on Claude Code via AskUserQuestion (clickable options), on any other harness as plain numbered markdown with "something else" always available — and either way the agent ends its turn and waits for the answer. AskUserQuestion is a Claude Code built-in, not something a Copilot/Cursor/Windsurf session can install; the rendering is tool-specific, the asking and the stopping are not. skills/interview-protocol.md §3 → "Asking on a non-Claude agent" is the single source of that rule; every other mention defers there. ASSUMED is earned by asking (user said "you decide"), never by skipping the question — finding the answer in the source justifies the recommendation, not silence. Unattended runs are opt-in only (Interview mode: auto in PROJECT.md, alias unattended, at the user's explicit request; modes in skills/interview-protocol.md).
Load skill files on demand when the task calls for it — not all upfront. Full routing table: README.md → "When to use which skill". The always-on set (README.md → "Baseline routing"): query-the-model.md, learned-mdl-preflight.md (the mdl helper instead runs bin/mdl-rules.sh for the statement kinds it writes; learned-microflow-patterns.md is on demand), and bin/bug-lookup.sh (a CE code, BUG-n, keyword or the whole pasted error message → the matching ledger entries; bug-logs/mxcli-bugs.md itself is read on demand — it outgrew the always-on budget).
| Task | Read this file |
|---|---|
| Any conversion/build — what stage, what gate, who owns it | skills/conversion-runbook.md |
| Audit / lint / regression-test an existing app (no pipeline) | skills/existing-app-assurance.md |
| Deciding what source answers a question, before asking the user | skills/query-the-model.md |
| Running a stage-transition checkpoint (2+1 questions) | skills/checkpoints/checkpoint-*.md |
| Extract-vs-not, extractor coverage, scoping a large source | skills/source-triage.md |
| Building/validating a new extractor for an uncovered stack | skills/extractor-quality-loop.md |
| Running or explaining the extraction pipeline | skills/migration-pipeline.md |
Mendix module boundaries (Stage 3, before create module) |
skills/modularize-domain.md |
| Architecture blueprint: diagrams, fit-gap, marketplace, security, NFRs, integrations | skills/architecture-blueprint.md |
| UI/brand layer: design system, wireframes, branding interview | skills/design-artifacts.md |
| BRDs + architecture → ordered build plan | skills/brd-to-build-plan.md |
| Per-module build discipline (gates, coverage checklist) | skills/iterative-build-loop.md |
| Writing/validating/enriching BRD JSON | skills/brd-generation.md, skills/brd-validation.md |
| Document folder scan / Excel-Word-PDF extraction | skills/document-discovery.md, skills/kb-generation.md |
| OS XML structure | skills/source-os11.md + skills/os-xml-schema.md |
| MDL microflow scripting patterns | skills/mdl-cookbook-microflows.md |
| Page/snippet pre-flight (wireframe → tokens → StyleGallery) | skills/ui-preflight-pages.md, skills/learned-stylegallery.md |
| Diagnosing a mxcli CLI error | bug-logs/mxcli-bugs.md |
| Generating a new project's CLAUDE.md | skills/bootstrap-project.md (run mxcli init FIRST — init overwrites, bootstrap merges) |
| Setting up dev-process subagents (ba/architect/mdl/gate/test/review) | skills/agent-roles.md |
| Past process decisions | process/process-learnings.md |
Migration assessment (assess-migration) is bundled with mxcli (.ai-context/skills/); skills/assess-migration.md here is a pointer plus toolkit-specific deltas only — the toolkit never duplicates bundled skills.
| Source platform | Pipeline | Run |
|---|---|---|
| OutSystems | pipelines/outsystems/ |
cd pipelines/outsystems/pipeline && npm install — see its README.md / pipeline-guide.html |
| Java + Angular / Spring Boot | pipelines/java-angular/ |
cd pipelines/java-angular/pipeline && npm install — see its README.md |
| Node/Express + React | pipelines/node-express-react/ — regex-based, proven on one source shape only; read its README's "Known gap" sections first |
cd pipelines/node-express-react/pipeline && npm install |
Mendix (an existing .mpr, rehost or move a module) |
pipelines/mendix-mpr/ — build-green on a small app only; read its README's "Field runs" first |
MXCLI=/path/to/mxcli pipelines/mendix-mpr/rehost-extract.sh <source.mpr> <target.mpr> <out-dir> — Python 3 stdlib, no npm install; see its README.md |
node_modules/ is gitignored — npm install locally per pipeline. Set local paths in pipelines/<x>/pipeline/config.json; never commit real local paths. Curated sample outputs live under each pipeline.
Clone once, point projects at it — no copies, no drift:
git clone https://github.com/MendixMau/mxcli-project-toolkit.git ~/Mendix/mxcli-project-toolkit
Each project scaffolded with bin/init-project.sh gets the Baseline routing table rendered into its CLAUDE.local.md (kept current by bin/sync-project.sh); its CLAUDE.md carries only a short pointer to that file, never a copy of the table (skills/bootstrap-project.md Step 2). Only a project with no CLAUDE.local.md convention copies the table from README.md into CLAUDE.md directly. For a self-contained handoff, use a git submodule.
Company-specific material never lives here either — own conventions, client-named learnings, house lint rules, approved MPKs go to a company brain, a private repo instantiated from templates/company-brain/ (skills/company-brain.md). The public toolkit never cites one: bin/check-no-private-citations.sh runs in CI.
Project output never lives here — analysis/, sources/, knowledge-base/, *.mpr are gitignored. A project's build plan, PROJECT.md, and session notes live in that project's own repo; promote reusable patterns into skills/learned-*.md instead of accumulating project docs here.
Authoring rule. This section governs work on this repo. It is not shipped guidance:
consuming projects load their own generated CLAUDE.local.md, and the per-tool files
(AGENTS.md, .github/copilot-instructions.md, .cursorrules, .windsurfrules) are
POINTERS, never copies — see project-bin/check-root-clean.sh.
Build it generic. Source-specific work stops at the extractor. Everything downstream of the BRD must work for any input — extracted code, documents, SME interview, or requirements typed by hand — and therefore lives in
bin/, not inpipelines/<platform>/.
The BRD is the platform boundary. Upstream of it a source is OutSystems, Java, or a folder of epics. Downstream of it there are only entities, microflows, pages and use cases. This repo serves four entry modes (migration, requirements-driven, greenfield, change an existing app) plus à-la-carte use, and only the first of those runs a pipeline at all.
Where things go.
| Kind of thing | Home | Why |
|---|---|---|
| Reading a specific source shape | pipelines/<platform>/extractors/ |
genuinely per-stack; extractor-quality-loop.md governs building one for a new stack |
| Anything reading BRDs | bin/, via bin/lib/discover-brds.sh |
BRDs arrive from extractors, kb-generation.md, SME interviews, or by hand — the reader must not care |
| Judgement, verdicts, gates | skills/ |
skills-over-scripts.md is the governing rule and it wins over this one |
Specialisation is the consumer's job, and it is already supported — extractor-quality-loop.md
(any stack: interface, validator contract, run.sh, 6 scored dimensions, 95% gate) plus three
worked pipelines to copy. Do not add a scaffold that stamps those files out; a prose spec and
worked examples are the correct form here.
Why this is written down (2026-08-19). generate-enrichment-report.js — the Stage 2 surface
— was built three times, once inside each pipeline, because nothing said it shouldn't be. The
three copies drifted onto three different BRD shapes (module vs modules[], attributeCount
vs attributes[], a.to vs target), so a BRD written by hand per brd-generation.md rendered
a page of undefined and zeros that looked complete. Requirements-driven and greenfield projects
got no Stage 2 surface at all, though conversion-runbook.md promises one unconditionally. The
same disease had already been diagnosed one layer down a week earlier — see the header of
bin/lib/discover-brds.sh: "That is what duplication costs: the fix does not travel."
Before adding any file under pipelines/, ask: does this read a source, or does it read a
BRD? If it reads a BRD, it is in the wrong directory.
Every defect in the F-042 cluster (2026-08-28) shipped the same way: an instrument written
against imagined input and merged without ever executing against real input. The
selftests all passed — selftests prove the logic, and the logic was fine; what was wrong was
the assumptions. So, before a new or materially changed instrument under bin/,
project-bin/ or project-tests/ merges:
- Golden input is captured, never hand-written. A parser of tool output (
SHOW PAGES,SHOW MODULES, mxbuild logs) gets a verbatim capture from a real model committed beside its fixture, with the parsed counts asserted. Hand-written test input encodes the same assumption the code makes — F-042's header-row bug (6 real pages counted as 13) was invisible to anything but real output, because real output had columns nobody imagined. - Both layouts. Paths resolve through
_common.sh/ config-derived roots, probed on single-tree AND two-tree (.mprunderapp/) checkouts. F-020 and F-042 are the same bug found twice, three weeks apart, in different files. - Both platforms. Bash runs under Git Bash on Windows, where MSYS maps
foo→foo.exetransparently — but Node does not: neverexecFileSynca bare wrapper name without an.exe/.cmdprobe or env override. Guard mac-only tools (sips,/usr/libexec/java_home) behindmxtk_platform/process.platform. - One field run, cited. The commit that ships an instrument names the real project it ran against and what it measured there. An instrument nobody has run is a claim, not a check.
- A producer for every consumer. If the instrument reads an artifact
(
page-scope.json,stack.env), say in its header which mandatory chain step produces that artifact — "documented in a skill" is how design-audit.js sat installed and never ran for an entire build. If no mandatory step produces it, wire one before shipping the consumer.
For an instrument that blocks (a guard, a refusing gate), two extra rules — learned when a context guard blocked a session that had done everything right except use the guard's own checkpoint script, and an expert user's reaction was "I can't even compact?":
- A blocking guard must accept evidence it did not itself create — a recent commit, a recently modified handoff doc — never only its own stamp file.
- A guard must never block the action that resolves it. Blocking the remedy is perverse: warn once, then let it through.
A merge go from the maintainer is not a review. Before any PR merges, an agent that did not write it reviews the diff and returns a verdict. The builder's own report and green CI never count: the builder grades its own work, and CI proves only the tests the builder wrote.
- Who reviews, and with what. A fresh agent given the PR diff, the linked issues and the PR body's description of what changed — never the builder's verdict, risk assessment or "all checks pass" claims. One reviewer per PR, plus one composition check when several PRs in a batch touch the same files. The reviewer may run the fixtures the PR adds or that target its changed files, in a detached worktree — that is part of the review the maintainer asked for, not a free-standing test run (see "Testing this toolkit").
- What it answers, each with file:line. Value: does it fix the issue as reported, and is
anything overbuilt? Regression risk: what changes for an existing project after
sync-project.sh(new FAIL / BLIND / refusals, exit codes, output another tool parses, gate verdicts that flip), across both layouts and macOS / Git Bash? Correctness: real bugs, tests that do not test what they claim, hand-written golden input where a capture was required. Context cost: words added to always-loaded skill files — re-measured by the reviewer, not taken from the PR body. - Verdict.
MERGE,MERGE-AFTER-FIX(exact fixes listed; the fixes are pushed and re-reviewed before merge) orHOLD(goes back to the maintainer with the reason). OnlyMERGEmerges. - Record the verdict durably. The squash commit body carries a line
Review: MERGE — <one-line top finding or "no findings">, and the merge announcement repeats it next to CI. A merge with noReview:line was not reviewed. - No exceptions for size. A one-line fix, a docs-only PR and an outside contributor's PR all
get the same review. "Small", "only skill text" and "CI is green" are how the exceptions start.
The one exception is guard rule 7's: a fix whose only purpose is to unblock master (a broken CI
check, a leak guard that blocks every commit) may merge first, with its review within 24 hours
and a
Review: PENDING (unblock)line; the follow-up verdict is recorded as a comment on the merged PR (and a follow-up fix PR if it finds anything), since a squash commit cannot be edited.
Why (real incident, 2026-10-13). Nine PRs merged in one day on the builders' own reports and green CI, each carrying its builder's "all checks pass". When the maintainer asked whether value and regression risk had been checked, the honest answer was no: nobody who had not written the code had read it. The after-the-fact reviews found, among others, a journey preflight that made every synced project's journey rung FAULT, and an exec lock that let three writers in at once. The same day showed the cost of unread diffs in another form — the always-loaded skill text hit its 80,000-word cap because each PR added words that nobody outside the PR weighed.
bin/render-routing.sh --check caps the always-loaded (baseline) skill set at
MXTK_BASELINE_BUDGET_WORDS — a ratchet, explained in that script's header: lowered as text moves
out, never raised to admit growth. Room is made by moving text, never by deleting the text
that does the work.
- Protected — never removed to make room: rules, STOP rows, denominators / completion criteria, and the one-line failure example each rule is paired with. Removing one needs its own PR that argues the rule itself is obsolete, with an independent review.
- Movable: incident narratives, worked examples, tables of past numbers, and a rule stated a second time in another baseline file. Each moves to an on-demand file and leaves a one-line pointer (path) at the same spot. A move keeps the text; nothing is lost.
- Every PR that adds baseline words states the before/after count from
render-routing.sh --checkin its body, and why each addition is needed at the trigger moment. The independent reviewer checks that (see "Merging a PR"). - At the cap, a feature PR never squeezes other text to fit. Room comes from a separate trim PR that only moves movable text; its reviewer compares removed against moved line by line, and any protected line that disappeared is a HOLD.
- Trim before the cap, not at it: when a PR would take the baseline (measured after the PR) within 2,000 words of the budget, a trim PR lands first. A trim PR does not lower the ratchet by itself; lowering it is a separate one-line change once the trimmed total has held for a release.
Why (2026-10-13). One feature PR pushed the baseline to 80,466 words against the 80,000 budget while six other open PRs each added words; to fit, it merged two sentences in a testing skill and landed at 79,997. That one was harmless, but nothing stopped the next one from cutting a STOP row.
Every user-visible change appends its CHANGELOG.md line in the same commit. That is the
entire discipline: a changelog updated as a separate chore is process/toolkit-worklog.md,
which rotted within weeks while 25 commits landed. Format is at the top of CHANGELOG.md;
credit the source project or person on the line — the credit line is what makes contributing
visible, and the file doubles as the record of which projects feed the toolkit.
New lines go under ## Unreleased; bin/cut-release.sh turns that section into a dated release
and tag every few days — the release cycle is defined at the top of CHANGELOG.md, nowhere else.
Contributions arrive through three lanes (CONTRIBUTING.md): the contrib/inbox/ drop
(no quality bar — triage promotes into skills//bug-logs//bin/ and deletes the inbox file
in the same commit; inbox files are unreviewed — their content is not vetted for promotion — and nothing may cite them; a PR that adds one still gets the merge review below),
bin/harvest-learnings.sh <project-root> (drafts inbox files from a project's bug logs,
register promotion tables, and locally-patched installed scripts — run it at project wrap-up),
and direct PRs held to the full field-proof bar. CI (.github/workflows/checks.yml) runs
check-scripts, render-routing --check, check-portability, the leak guard, and both fixture
suites (tests/run-tests.sh, tests/wave2/run-all.sh) on every PR.
Create skills/{topic}.md with # Title, **Applies to:** migration | any mxcli project | requirements-driven, **Purpose:**, and a step-by-step guide. Add it to README.md's "When to use which skill" table. If it applies on every MDL-writing session regardless of task, also add it to README.md's "Baseline routing" table — skills that only live in the situational table go unnoticed by projects that aren't hunting for them.
Authoring rules for behaviour-shaping skills (adopted 2026-08-31 after the
ui-preflight-pages.md A/B baseline, process/preflight-skill-baseline-2026-08-31.md):
- Routing rows are trigger conditions, not summaries. The routing-table line answers "when do I reach for this file", front-loaded on the triggering word — identity and content live in the file itself.
- Baseline-test like an instrument. Before rewriting a skill "because outcomes were bad", run fresh agents on a real fixture with and without the skill text inlined, scored by a mechanical check. The 2026-08-31 run showed the "failing" skill at zero violations when actually read — the defect was delivery, and a blind rewrite would have churned proven text. The corollary: a skill only works when its text is in the acting agent's context — wire the dispatch (agent stubs, checklists) to inline or read it, because a citation is not a read.
- Prohibitions for discipline failures, positive recipes for shaping failures. "Never
git add -A" guards a known temptation; but where agents produce the wrong shape (wrong layout, missing H1), state the target behaviour and the evidence for it — a ban alone keeps the wrong pattern in view. - Completion criteria carry denominators. A step ends on a checkable bound ("all N gallery files listed", "0 violations from the shell check", "NOT RUN is legal, silence is not") — a step ending in prose nobody can fail is the unfalsifiable-checklist defect the obligation check exists to prevent.
- No caches of another project's environment. A class table, file inventory, or path list
copied into a skill goes stale the day the next project scaffolds differently; instruct
"list/grep the actual file" instead (both stale tables in
ui-preflight-pages.mdwere flagged mid-run by the very agents following them). - Write for the trigger, then stop. A skill is retrieved mid-task by an agent with a specific trigger; everything not needed at that moment is dilution — one skill, one situation. (The arm-C verification of the 2026-08-31 baseline saw a possible dilution effect from ~800 words of growth in a single revision.)
- Show the failure, not just the rule. Pair each rule with the concrete bad output it
prevents — a before/after or a verbatim wrong answer. This repo does it consistently in
CLAUDE.mdincident notes and inconsistently insideskills/*.md; the skills are where the acting agent reads. - Test retrieval, not just content. A skill can be perfect and unreachable: periodically
check that its routing line actually fires for the phrasings users and agents really
produce (rule 1 makes the row a trigger condition; this rule is the check that the trigger
works). The coverage audit in
render-routing.sh --checkcatches the unrouted case mechanically; wrong-trigger routing it cannot see.tests/advisory/authoring-audit.py routingis that check (BM25 over trigger text vs the labelledrouting-phrasings.tsv); itsdupeslists rules stated twice. Advisory, never a gate.
(Rules 6–8 adapted 2026-08-31 from public skill-authoring guidance in obra/superpowers
writing-skills and Matt Pocock's writing-for-agents — patterns only. Their fourth
point, "degrade to a pointer", is already this repo's "single source of the rule; every
other mention defers here" convention plus rule 5 — one principle, stated there.)
Several Claude sessions routinely have this clone open at once, in one working tree and therefore one git index. So:
Stage and commit in the same breath, or pass explicit paths:
git commit -- <paths>. Nevergit add -A. Before resetting a commit, look at what else is in the index.
Why (real incident, 2026-08-21). A session ran git commit with no pathspec while another
session had 29 unrelated files staged. Git takes the whole index, so that work landed inside a
commit titled "Log BUG-95: …". The resulting git reset restored the 29 files — and silently
took the committer's own bug-log entry out of the working tree with it, where it survived
only in a dangling commit. Authorship is not readable from mtimes either: eight files were
mislabelled as a peer's until someone read the diffs.
Same root cause as the testing rule below — a shared, moving tree makes any whole-tree
operation untrustworthy. Coordinate with ListAgents / SendMessage before committing, and
if a peer's untracked scratch file trips the pre-commit leak guard
(bin/check-no-client-data.sh scans untracked files too), ask them to fix it. Do not gitignore
it and do not narrow the denylist.
tests/wave2/run-all.sh runs every fixture in the suite. Do not run it as a reflex "did I
break anything" check after a normal edit. Run only the fixture(s) that exercise the file(s)
you actually touched (e.g. changed bin/open-questions.sh → run test-open-questions.sh;
changed bin/gate-check.sh → also run test-bug03-gates.sh, test-source-sufficiency-gate.sh, test-source-ledger.sh,
whatever else targets it). Grep tests/wave2/ for the filename you changed if you're unsure
which fixtures cover it.
There are two separate suites. tests/run-tests.sh [-v] is self-contained (guard-script
fixtures with no external argument). Each tests/wave2/test-*.sh fixture instead takes its
subject-under-test as $1 — read its own usage: comment rather than assuming a fixed target;
several take something other than gate-check.sh (exec.sh, sync-project.sh,
check-docs-numbering.sh, a skill file). Once you have permission for a single fixture (see
below), run it as e.g. bash tests/wave2/test-bug02-register.sh bin/gate-check.sh.
Why: this repo is routinely edited by more than one agent session at once (real incident, 2026-08-19 — a peer session mid-edit reported known, unrelated failures already present in the shared working tree). A full-suite run over a moving target mixes someone else's in-flight breakage into your result and tells you nothing trustworthy about your own change — it looks like verification but isn't. It is also strictly slower for no more signal than the scoped run, since the untouched fixtures were never at risk.
Run the full suite only when the user explicitly asks for it in that message. There is no
other trigger, and none of the following is one: about to open a PR, closing out a multi-file
change, or a change that looks like it had wide blast radius. Those read as licence to
self-authorize, and they were — the rule said "ask first" and then handed out three reasons not
to. Wide blast radius is a reason to pick the right scoped check, not to run everything: for
bin/lib/skill-routing.tsv and its re-render (README.md, ROUTING.md, every agent template, and
gate-check.sh's stage map at once) that check is bin/render-routing.sh --check.
One standing exception: the independent reviewer of a PR (see "Merging a PR") may run the fixtures the PR adds or that target its changed files, without asking, in a detached worktree of the PR branch. That isolation is what avoids the shared-tree problem above; the reviewer still never runs the full suites.
Outside that reviewer exception, asking is also required for a single fixture — name it, say what it will tell us, and wait.
Where a fixture's assertions are greps over a file you changed, prefer verifying by inspection: read the fixture, replicate its greps against the changed file, and report that. Same signal, no suite run, no interference with whatever another session has in the tree right now.