agentleFS
scottcrosby-securebinescottcrosby-securebine/doctrine-skills/CLAUDE.md

doctrine-skills

CLAUDE.md0 starsChanged 9 days agoMITView on GitHub

A Claude Code plugin ("doctrine") distributing fourteen skills as markdown, plus a small amount of code that is not part of any skill — there is no build or lint step. The "pure markdown" description was true until 2026-08-30 and is not any more: hooks/ ships a Claude Code hook set (issue #17) that gives each dispatched seat a live herdr pane while it runs — stacked beside the session up to a cap of six, its own sidebar-listed tab…

  • Installs packages

The file

We split this CLAUDE.md into the rules it gives an agent

1 rule. About 0 are common, 0 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Keep SKILL.md in step with what the code actually does — it claims specific behaviour (four widths, lazy-load scrolling, overlay hiding, …

    line 144

≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
CLAUDE.md

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this repo is

A Claude Code plugin ("doctrine") distributing fourteen skills as markdown, plus a small amount of code that is not part of any skill — there is no build or lint step. The "pure markdown" description was true until 2026-08-30 and is not any more: hooks/ ships a Claude Code hook set (issue #17) that gives each dispatched seat a live herdr pane while it runs — stacked beside the session up to a cap of six, its own sidebar-listed tab beyond that (Scott's ruling 2026-08-31: side-pane seats get no sidebar row). The pane closes on the seat's SubagentStop, with one exception since 2.1.0: a codex:codex-rescue seat's wrapper returns while the job it started runs on, so its Stop keeps the pane and the marker and runs the hook's own --codex-tail mode in it, which follows the job record codexJobRecords() in dctr-state.mjs finds (this workspaceRoot, created since the seat started, newest wins) and relabels the pane when the status turns terminal (Scott's ruling 2026-09-07: relabel and stay). That marker stays until the next codex seat is placed (Scott's ruling 2026-09-07): that placement, inside the same lock and before it counts the column, closes every finished, unfocused codex pane and removes its marker, so the slot is free for the wave that follows. A focused pane still stays, and a job whose record cannot be read is not finished, matching the watcher, which keeps waiting on one. A tab seat's focus is read from the TAB, never from its recorded root pane (Scott's ruling 2026-09-08): the thing being closed is the tab, and reading the pane meant a user who split the tab and closed its root had the focused sibling destroyed on the next codex placement. Until another codex seat arrives, nothing sweeps and the pane still counts against the cap. Every seat pane is named from the harness's agent-<id>.meta.json description, read through readMeta(), which is the same title the status line shows and why the name in both places is identical. In a herdr session (HERDR_ENV=1) it panes the seat directly; with neither that nor the contained variable set it stands down in its first lines, so an installer without herdr pays one process spawn per hook event and nothing else. That is not one per seat, and the description said so through two releases. Claude Code fires SubagentStop for its own forked queries too (a prompt suggestion after most turns, memory extraction periodically), each carrying an agent_id and an empty agent_type; one session logged 62 of them against four seats, and a session that dispatched nothing logged eight in eleven turns. They cannot be filtered out in hooks.json: the dispatcher runs every matcher when the match query is empty (read from the 2.1.261 bundle, 2026-09-05). dctr-lib.mjs says which half of the seat test they fail. Since 1.54.0 a session running inside a container uses pane containment: when DCTR_VIEW_REQUEST_DIR is set (an allowed mount) the hook takes that path BEFORE the HERDR_ENV gate, writes a view-request file into that directory naming the seat's agent_id, and makes no herdr call at all — a contained agent must reach nothing on the host, so a host-side watcher reads the request and renders the seat inside the container. The pane a herdr server spawns is a host process, which is why a contained agent never drives one (Scott's containment ruling, 2026-09-01). Since 2.5.0 (E8's e8-restore phase, 2026-09-23) the hook set also has a SessionStart entry with matcher clear, dctr-restore.mjs: after a /clear in a repo whose SESSION_MEMORY.md kickoff names a handoff whose record: line names a record with a last state line of Open or Blocked, it injects four facts as additionalContext (the phase name, the record path, the raw last state line, the wrapper), under 2,000 characters, and nothing on any other source, on a subagent's event, or for any other state; it calls no herdr and its pure decisions sit in dctr-lib.mjs. Since 2.6.0 (E8's e8-gauge phase) the set also has a PostToolBatch entry, dctr-gauge.mjs, the context gauge: on every tool batch of the main session (any input carrying agent_id stands down first) it follows the same kickoff chain as the restore hook to the record, and only where that record is Open or Blocked and its last auto-cycle line is on does it read the transcript's last usage entry, the tier from that line, the window from the statusline bridge's per-session file (dctr-bridge.mjs, installed by its own install command as a copy under the Claude config dir and named in settings.json), and, once per session id, append auto-cycle: warned to the record and hand the session the facts as additionalContext; a missing, unparseable or stale reading is unknown and never low use, and three in a row warn with unknown. It calls no herdr. Since visibility-62 the set also has a PreToolUse entry (matcher Bash|Write|Edit), dctr-roundguard.mjs, the round guard: where dctr-token.mjs would publish, it refuses once per round per record the main session's tool call that writes a new round line without running dctr-token.mjs, or whose record or text names a gate transcript that resolves, relative to the call's cwd as one literal cd earlier in the command moves it and through any symlink, under the temp directory, as roundGuardDecision in dctr-lib.mjs decides, and calls no herdr. Beside it dctr-record.mjs is the parser for the record line forms hub step 5 defines (wave, round, finding raised and cleared, ruling, alarm, question, state, the auto-cycle lines and the wrapper: line), the reader the later E8 phases build on. E8's e8-autocycle phase adds the auto-cycle entries: Stop, Notification (matcher permission_prompt|idle_prompt), StopFailure and UserPromptSubmit all run dctr-cycle.mjs, which stands down, as it does on any subagent's event, unless the record the same kickoff chain reaches is Open or Blocked and its last auto-cycle on/off line is on. Past that, a Stop does what cycleDecision decides: it appends one auto-cycle paused: line naming its reason, waits for live work, does nothing, or launches dctr-typer.mjs detached, which no hook entry names and which types /clear and then the /doctrine:doctrine-resume command into the session's herdr pane once focus moves off it, each poll doing what typerStep decides. A Notification or StopFailure appends the paused line notifyDecision returns, if any, and UserPromptSubmit only records where the session's turn ended. UserPromptSubmit also runs dctr-promptgate.mjs, the prompt gate (E10 S1), on every prompt whatever the record says: a prompt that does not carry the auto-cycle's marker is never touched and costs one process spawn, and a marked one runs only in the session this pane's restore file names, so the typed resume line never runs in a session its /clear did not start; any other marked prompt is blocked and the pane's gated marker written, which the Codex typer reads. Past its marker check the gate runs inside one catch that prints the block, because both hosts let a prompt through when a hook crashes, prints malformed JSON or times out. Those three functions sit in dctr-lib.mjs and own the order and every condition. A Stop whose decision reaches the pane lookup outside a herdr pane (no HERDR_PANE_ID) pauses with could not type into the pane: this session is not in a herdr pane, and with DCTR_VIEW_REQUEST_DIR set the hook and the typer call no herdr. hooks/ also carries scripts no hook event runs directly, the typer among them; one is dctr-bridge.mjs, the statusLine command the installer step copies, which Claude Code runs to draw the statusline and not on a hook event. dctr-token.mjs (issue #19) is an orchestrator-run one-liner the hub's step 5 names, publishing a run's round and gate counters as sidebar tokens, and since 2.3.0 the $epic and $phase tokens and the ·owed suffix doctrine-project names — same stand-down rule, and dark until the user's herdr config renders them. dctr-doctor.mjs is run by hand after a Codex, Claude Code or herdr update: it drives both installed hosts in scratch homes under its own herdr server and reports, by stable code, whether each host signal the hooks read still holds, and which hook areas it does not exercise. dctr-project.mjs (2026-09-13) is the format check and status command doctrine-project names for a repo carrying docs/PROJECT.md: check reads the project file and epic records offline and exits 1 on any finding, and status prints what those files and the current posture show, unknown wherever a source cannot be read. dctr-gate.mjs (2026-09-05) is the launcher the hub's step 3 names for every gate, however short. Before issue #62 it named only the Bash tool's ten-minute ceiling, which left an orchestrator that had already decided to background something reading the rule as advice about a problem it believed it had solved; #62 added backgrounding as a second trigger, which still left a short foreground gate to each orchestrator's choice, so it now names every gate. inside herdr it runs the check in a pane placed under the seat rules, cap and lock (it registers a dctr-gate-N marker beside the seats, so a wave arriving mid-gate stacks next to it), outside herdr, or in a contained session, it runs the check detached and writes no view request, since a pane is a host process and a gate has no transcript to render; either way the result file <out>.result, which the launcher alone writes and only at completion, is the completion signal and its first line exit=N the record; the transcript <out> holds the check's bytes and nothing else (Scott's ruling 2026-09-11, after three review rounds each found a new defect in the escaping that had kept the verdict inside the transcript). The pane it splits is named from the launcher's label the moment it is placed and carries elapsed time while the check runs, refreshed on an interval DCTR_ELAPSED_MS overrides; before 2026-09-07 the split path passed no label and never renamed, so a side-column gate pane was nameless for its whole run, the completion rename firing only on a focused pane. Elapsed counts up rather than down because the launcher cannot know a check's total without imposing an output format on every check it runs. It exists because a live run pushed a 60-minute mutation gate into setsid nohup after the tool killed it at 48 rows, and the user then had nothing to watch. dctr-pane.mjs (2026-09-06) is the interactive-session launcher doctrine-pane names: it runs a login under script(1) for a real PTY — the gate launcher's piped stdio cannot serve one — in a pane the user watches and can take over. It enforces and never interprets: an earlier revision parsed the terminal to decide what a prompt was and whether a command had finished, and real hardware refuted both within two commands, so every such judgement belongs to the agent reading the bytes. Its markers live in a session-independent directory precisely so the SessionEnd sweep cannot reach them, which is why interactivePanes() in dctr-state.mjs is a separate reader from liveSeats(): placement counts these panes against the same six-pane cap, and nothing in the stop or SessionEnd path can see them. Counting and destroying need different readers, and sharing one would put the panes back under the sweep they were moved out of. The shared on-disk state and the herdr call moved to dctr-state.mjs so the hook and the launcher place panes under one lock; the decisions stay pure in dctr-lib.mjs. CI runs only the gates that need nothing installed (.github/workflows/gates.yml), which is every command in the block below and never the two that matter most. The machine-validated files are .claude-plugin/marketplace.json, .claude-plugin/plugin.json and the root plugin.json, plus doctrine-gauntlet's harness code, whose gates are manual and run by hand: the syntax gate and tamper fixtures that workflow.md and floor.md define. node tools/doc-check.mjs is a separate gate over every skill's prose, not part of that harness. hooks/dctr-mutations.mjs is a gate over the other gates: it reverts each named repair in a copy of the tree and fails if no selftest clause goes red. It exists because the recurring worst finding in the pane launcher's review rounds was a check that certifies a path it does not take — a clause asserting a variable it never read, a fixture whose record shape no code writes, a fixture whose pane id the workspace filter excluded so the branch under test never ran once. A mutation whose anchor does not occur EXACTLY ONCE fails rather than skipping, in both directions: a missing anchor because a renamed line would otherwise retire the mutation guarding it, and a duplicated one because replace() takes the first hit and leaves the rest unguarded while reporting the same "ok" — each is the same defect one level up. It runs the mutations in parallel (mapPool in dctr-lib.mjs, DCTR_JOBS workers, defaulting to 8 or the host's core count, whichever is smaller), each on its own copy of the tree, and prints results in input order as they settle rather than in completion order, so a run reads the same twice. What that promises is the RESULT lines and their attribution, never the transcript's bytes: since issue #62 the run also prints a progress line naming how many have settled and which item has been in flight longest, because input order means a slow early item holds every finished line behind it and a pane showing one line for two minutes reads as hung. Those lines carry a · and never an ok/FAIL/ERROR shape, so what a reader greps for is unchanged. The suite list is ordered by measured cost and the comment saying so is a claim to re-check: some stops at the first suite that notices, so a mutation pays for every suite ahead of the one that catches it, and the list once read cheapest-first while a 439ms suite sat behind a 19s one. Since 2.17.0 (E11) hooks/hooks.json also names a hooks module under modules, hooks/band/register.tsx, the doctrine band: on Claude Code only, one line above the prompt drawn while the record the kickoff chain reaches is Open or Blocked, its pure decisions in hooks/band/band.ts, which reads through dctr-record.mjs and copies no reader (E11-D14 is why kickoffHandoff, handoffHeader and restoreState live there and not in dctr-lib.mjs, which a hooks module cannot load because it imports node:*). The plugin's userConfig field band is its /config row. Its tests run under claude plugin test ., which needs Claude Code installed, so neither CI nor the mutation gate runs them; the gate's copies carry band/band.ts only because the Codex install suite edits it. Bump version in .claude-plugin/plugin.json and in the root plugin.json (the Codex manifest) together when shipping a change.

Skills were validated by running fresh-context agents through each wrapper against realistic scenarios and patching every ambiguity they hit — when changing a skill's flow, that's the test to rerun. That test carries a standing debt. The hub's step 5 was driven once, on 2026-08-27, by four fresh-context agents each executing a phase (docs, code, debug, gauntlet) on a synthetic project against 9a96069; that run found three defects fifteen reading rounds had missed, and the rewrites of step 5 that followed it were read, not driven, until the 2026-09-05 convergence change: its step 5 was driven once on a synthetic phase before it shipped, the drive reopened the phase with two blocking findings six seat passes had missed, and the reset-trigger wording was then re-aligned twice on seat findings after the drive, so the shipping text is audited past the point it was driven. The seven wrappers have not been driven since the original validation, and all seven have been rewritten since. What they now say has been audited, not driven. doctrine-project, the eighth wrapper, postdates that validation and has been driven: fresh-context agents drove four fixtures on 2026-09-13, re-driven twice, the last re-drive finding nothing blocking on db0582b. Those drives are what found nine release-blocking defects the reading rounds had passed, and the first re-drive found one more, a recurrence of a defect an earlier repair had fixed only in the case it named, so read the coverage as of db0582b and not of anything changed since. The two find different things — an audit reads for contradiction against other text, a scenario run finds the instruction an agent cannot act on — so do not read a clean audit as coverage of this.

One standing regression suite exists, and it is manual: fixtures/. Three generality cases — bare.html (no design system, no repo, no package.json), fixtures/shadcn.sh (a recipe that builds a stock Next + shadcn install on demand; the app is deliberately not vendored, so a rebuild resolves fresh — fixtures/README.md records what that recipe actually holds still and what it lets float, which is what decides whether a rebuild's diff is signal or weather), and a real bound system — exist because doctrine-gauntlet grew up against one design system, which is how a skill acquires rules that only make sense for that system without anyone noticing. Run a gauntlet or harness change against all three; each proves something the other two cannot, and fixtures/README.md says what. The sharpest case is the theme probe, and the lesson is about the probe, not about any line it currently prints: the same probe output is a false alarm on one fixture and a true defect on another, because what decides it is who owns theming outside the specimen — which is exactly what the probe cannot see. One fixture can therefore never tell you which you are looking at. Don't record the probe's current wording here; it moves whenever the harness does, and per-fixture expectations belong in fixtures/README.md where a harness change is forced to walk past them.

Commands

Each invocation is stated here once, or pointed at where it is stated once. A green CI tick covers every command in the block below, and nothing under it.

node tools/doc-check.mjs              # prose gate over every .md under skills/; exit 0 clean, 1 on any finding, 2 if --selftest found no sidecar
node tools/doc-check.mjs --selftest   # its three-clause tamper test, which lives in tools/doc-check.selftest.mjs
python3 -m json.tool .claude-plugin/plugin.json       # after editing any manifest
python3 -m json.tool .claude-plugin/marketplace.json
python3 -m json.tool plugin.json                      # the Codex (Agent Plugins) manifest; "Files that must stay in sync" says what it must match
node tools/herdr-lint.mjs           # the ONE class this repo kept re-finding by review round: a herdr reply whose failure OR emptiness is read as an answer that authorizes destruction; exit 0 clean, 1 on any finding
node tools/herdr-lint.mjs --selftest  # its three-clause tamper test, which lives in tools/herdr-lint.selftest.mjs; exit 2 if the sidecar is missing
node hooks/dctr-seat.selftest.mjs   # three-clause tamper test for the seat hook's pure decisions
node hooks/dctr-seat.contained.selftest.mjs   # contained posture calls herdr zero times (tripwire on PATH)
node hooks/dctr-gate.selftest.mjs   # gate launcher: the check runs, its transcript is its bytes alone, the result file says exit=N, the no-herdr path calls herdr zero times
node hooks/dctr-pane.selftest.mjs   # interactive launcher: every refusal trips AND calls herdr zero times, a valid session is not refused, the fixtures are proved defective without the checker, and BOTH script(1) invocations are asserted so the macOS argument shape is checked without a Mac
node hooks/dctr-seat.teardown.selftest.mjs   # the seat hook's destructive paths, plus the one placement path that fails the same way: a close that FAILED keeps its record while one that answered `pane_not_found` (or `tab_not_found`, a different code) does not, a lookup that failed never becomes a close, an unreadable marker does not abandon the readable seats, a healthy teardown still removes everything, and a reservation that cannot be made stands down instead of spinning
node hooks/dctr-project.selftest.mjs   # project format check and status: each rule trips on a broken fixture, a known-good project stays quiet, the fixtures are proved defective without the checker, and a failed read prints unknown
node hooks/dctr-project.history.selftest.mjs   # project lifecycle histories: check after every step, legal steps clean, illegal steps named, and each fix a finding prescribes leaves zero findings
node hooks/dctr-record.selftest.mjs   # record line parser: every form hub step 5 defines is returned with its fields and no prose line is, the last state line and the wrapper token are read, and the fixture is proved to carry each form without the parser
node hooks/dctr-restore.selftest.mjs   # SessionStart restore hook on /clear: an Open or Blocked record (in-repo or sibling repo) injects the four facts under 2,000 characters; startup, resume, compact, a subagent's clear, an Exited record and every missing input inject nothing and say why; herdr is called zero times (tripwire on PATH)
node hooks/dctr-gauge.selftest.mjs   # PostToolBatch context gauge: the pure decisions (skip on agent_id, unknown readings, tier forms and errors, the per-session latch, three unknowns), the hook end to end on a fixture tree (warning once per session id, warned line appended, seat batch skipped with the latch untouched, every stand-down), herdr called zero times (tripwire on PATH), and the fixtures proved defective without the gauge
node hooks/dctr-roundguard.selftest.mjs   # PreToolUse round guard: the pure decision with each refusal beside its known-good twin, the hook end to end with herdr called zero times (tripwire on PATH), and clause 3 proving fixtures without the decision, each clause's label saying what it proves, with a clause that fails, at run time, naming every key of every fixture table in its `TABLES` list that no clause-3 proof obtained through `prove()` (a fixture written inline in a clause, or a table left out of `TABLES`, is not checked); needs bash
node hooks/dctr-bridge.selftest.mjs   # statusline bridge: the wrapped command's bytes and exit code pass through unchanged, concurrent sessions each leave a complete file, another session's file is ignored, install on every settings shape (absent, command, already wrapped, refused type), and the fixtures proved without the bridge
node hooks/dctr-cycle.selftest.mjs   # auto-cycle hook: the pure decisions (auto-cycle active, the Stop steps, the cycle count and progress, the Notification reasons, every pause reason verbatim), the tree hash over git repos, the hook end to end with one fixture per Stop precondition, each paused line alerted once, Notification and StopFailure, zero herdr calls contained (logging shim on PATH), and the fixtures proved without the hook
node hooks/dctr-typer.selftest.mjs   # auto-cycle typer: the step decision per observation, and the typer end to end with a herdr shim playing the pane, asserting the /clear and resume sends per case (claim, stop file, focus, session, idle, transcript growth, restore file, first turn), the cycle line only once the /clear is seen to take (the restore file, or herdr naming another session) and never for one that did not, the pause past the restore wait by what herdr reports (R18 on the old session, R14 on another, R17 on no reading), and zero herdr calls contained
node hooks/dctr-codex.seams.selftest.mjs   # Codex host readers and decisions (transcripts, usage, the typer's pane check, the watcher's observations) on real Codex 0.156.1 captures
node hooks/dctr-codex.hooks.selftest.mjs   # restore, gauge, auto-cycle, typer and watcher end to end on Codex payloads and rollouts, logging herdr shim on PATH
node hooks/dctr-seat.codex.selftest.mjs    # the seat hook, renderer, gate session and /clear sweep on Codex payloads
node hooks/dctr-codex.selftest.mjs         # Codex install on scratch homes: entries, trust hashes, network access, foreign entries byte-identical, rerun a no-op; needs python3 3.11+
node hooks/dctr-notify.selftest.mjs        # the orchestrator's pause notification on Codex: exact title, no body, the stand-downs
node hooks/dctr-doctor.selftest.mjs        # the doctor's verdict on its live captures from both hosts: each tampered capture drifts exactly the signals named for it, the captures as taken drift nothing, and each tamper is proved without the verdict; no file under hooks/ or skills/ carries an account identifier, --save writes only into the scratch dir, and a doctor interrupted under a stub herdr stops what it started and removes its scratch dir, with /proc and without; on macOS it reads what is left running from the pids the stub recorded, and the orphan outside the server's tree, which only the /proc scan finds, prints a named skip that the Linux run drives
node skills/doctrine-gauntlet/harness/round.codex.selftest.mjs   # the gauntlet round on Codex: every tamper fixture through the runner meets its expect, an altered expect and a removed check are caught, and the CLI refuses what it must and runs through a symlinked plugin root
node hooks/dctr-mutations.mjs       # reverts each named repair and FAILS if no clause notices — the check that answers whether the other checks pin anything; a suite that renders no verdict (a failed spawn, a signal, or an exit with no FAIL line) is retried once and then reported as unjudged with the first line it printed, never as a repair nothing pins
python3 -m json.tool hooks/hooks.json

To run the floor against fixtures/ from this repo, a browser is needed here. The install is deliberately untracked — node_modules/, package.json and package-lock.json are all in .gitignore, because a tracked package.json would ship a build shape to every installer. That reason survives the arrival of hooks/: a hook the harness runs is not a build step, and nothing here is compiled, bundled or installed before use. The two commands:

npm i -D playwright-core axe-core
node node_modules/playwright-core/cli.js install chromium

The second is not npx playwright install. That belongs to the full playwright package and refuses when only playwright-core is present, pointing you at @playwright/test. The README carried the wrong command from the day it was written until 2026-08-29, when running it found out.

node floor.mjs <url-or-file.html> <outPrefix> [dark|light|both] [--fragment] [--single-theme] [--theme-class=NAME] [--crop=SELECTOR] [--expect=TEXT] — run from the target project, not from here, so it resolves that project's Playwright and axe. It rejects an unknown flag rather than ignoring one. floor.md is the manual.

The round script's syntax gate is not node --check (see "The one skill with code in it" for why); the one-liner is written down in skills/doctrine-gauntlet/workflow.md and is deliberately not copied here, because a copy forks the first time the original is corrected. Its tamper fixtures (harness/round.tamper.json) run through the Workflow tool, per the same file, and through the Codex runner's selftest in the block above, each seat a stand-in.

claude plugin test . runs the band's tests (hooks/band/*.test.ts) and is manual: it needs Claude Code installed, so it sits outside the block and CI does not run it.

There is no single command that runs all of these. .github/workflows/gates.yml runs the ones that need no install — every command in the block above, plus the workflow script's syntax gate — on every push. What it cannot run is the part with the most defects in it: the floor against fixtures/, because package.json is gitignored on purpose and CI has nothing to install from; harness/round.tamper.json through the Workflow tool (CI runs its fixtures only through the Codex runner with stand-in seats, which checks the script's counting and not the Workflow tool); and the driven fresh-context runs, which are agents rather than a command. So a green tick means the prose gate and the parsers are clean, and says nothing about the harness or the skills. Read it as the cheap end of the suite, never as the suite. No proportion is written down: it would rot the next time a gate is added, which is what just happened to the one that used to sit here.

Architecture

Layer, not fork. The core design rule: these skills wrap other authors' skills (Matt Pocock's engineering skills, superpowers, codex, ponytail, writing-clearly-and-concisely) by invoking them by name at runtime, never copying or restating their content, so upstream updates flow through. The one sanctioned exception is matts-code-review — a renamed copy of Matt's code-review, because the original name collides with Claude Code's native /code-review (documented in README Requirements).

Hub and spokes. skills/doctrine/SKILL.md is the hub: the seven-step posture (ask questions first, phases and waves of parallel agents, native checks, red team, loop to one clean pass, simplify, deliver) and the fallback table for missing prerequisites. Thirteen of the fourteen skills are doctrine-*; the eight wrappers each begin with "REQUIRED BACKGROUND: read the doctrine skill first" and supply only the task shape: which external skill is the core discipline, how phases map onto it, and which review skill the phase gate uses. Don't restate posture steps inside a wrapper — reference them ("doctrine step 4").

A hoist that is deliberately not done yet, apart from one half that moved. Two stretches of doctrine-gauntlet — "The docket and the arbiter" in full, plus the counter table and the rationale around it in "Modes" — are task-shape-agnostic and conceptually belong to the hub. They stay in the wrapper because no second wrapper needs them, and hoisting early makes every wrapper carry machinery one uses. A second candidate now exists and is deliberately not hoisted either: doctrine-write and doctrine-gauntlet both build a claim ledger, and the gauntlet's version cross-references the write one rather than redefining it — the predecessor-is-evidence rule is genuinely gauntlet-only, so two definitions would be wrong but one shared section is premature at two users. The trigger is a second wrapper needing an escalation budget or PROPOSED/RULED discipline — at that point move them up. It is a re-layering, not a cut: nothing gets deleted, and expect to generalize on the way, since the counters are written against gauntlet's two modes and the docket destinations assume a bound design system. One half moved when doctrine-project landed, and it moved on the second trigger below, not this one (Scott's Q8 ruling, 2026-09-12). The binding half of PROPOSED/RULED, that an item put to the user for a ruling is never a constraint, a baseline or authority until it is ruled, now sits in hub steps 1, 2 and 4: the hub's own step 1 sentence for a repo carrying docs/PROJECT.md sends a phase into ruled Done means, so the hub would otherwise depend on a rule only a dependent states. The docket, the arbiter, the escalation budget and the counter table stayed in doctrine-gauntlet, because doctrine-project needs an unruled item to bind nothing, and needs no docket, destinations or counters, so for those the first trigger has still not fired. The gauntlet's builder-brief statement of the rule stayed too, since a builder's prompt is assembled from that brief and cannot follow a pointer; only the docket section's builder sentence became a pointer to the hub. No size is recorded here on purpose, and neither is a certification that one was measured. A word count in this file rots without announcing it and is then cited as fact, and "already measured, don't re-derive" is a stop-looking sign hung on the claim most likely to have rotted — the same mechanism as the "ten" in "Some rules only look redundant" that survived until an audit found roughly forty. Measure it yourself if the size bears on your decision; what this paragraph is for is the why.

A second trigger exists, and it has already fired — on a third candidate neither of the two above. The hub used the term "the anchor" in its own steps 4 and 5 and defined it nowhere; only doctrine-write and doctrine-research defined it, so the hub was instructing the other five wrappers to hand a red team an artifact nothing in their flow produced. The definition was hoisted into hub step 1 as the genus — the user's own words for what the work is for and how it will be judged — with the two wrapper definitions left in place as species, and no wrapper edited, because silence inherits the fix and a local echo forks it. The next hoist, the orchestrator-only carve-out in 9a96069, did edit three wrappers — but with a pointer clause each ("and whatever else doctrine step 5 puts in that section"), never a restatement. A pointer inherits a correction the way silence does; it exists only because each of those wrappers enumerated its record's contents as a closed list, which silence cannot amend. So there are two triggers, pointing opposite ways: a second wrapper needing shared machinery, and the hub already depending on a term only its dependents define. The first trigger looks down the dependency edge and would never have caught the second.

Every external reference needs a fallback. Any skill a wrapper invokes must have a graceful-degradation path, either in the doctrine skill's Fallbacks table or stated inline in the wrapper. A reference with no fallback is a bug.

Gate semantics are deliberate design. The one-clean-pass exit gate (a pass certified by a context that did not write the fix, on a revision whose real-environment run is on record, with nothing outstanding from any earlier pass), the three-bucket definition of a "finding" (blocking = someone acting on the deliverable as it stands would do the wrong thing, with the instances listed after the test and never before it, while non-blocking improvements, declined nitpicks, findings the user rules closed, and red-team claims verified false from source never count against a pass, and wording that changes neither what the code does nor what it is for is not a finding at all, and the 2026-09-05 convergence run found three live repos' seats filing every imprecise sentence as a false claim because their briefs carried only the list of instances), the repair rule (a certified revision stays certified through a repair to wording, a comment, a test, a gate instrument or a steering document, unless that repair loosens or removes a check, which needs its own clean pass like any repair to shipping code, and a repair to a non-comment line of shipping code, or to a claim about it, needs its own clean pass, judged from the diff and never from a label), the repair-side rule (a repair writes no counts, coverage claims or gate history into shipping artifacts, and documentation waits until after exit, since half of 696 recorded blockers sat inside the previous round's repair), the prose-deliverable exit (a named replacement the prose wrappers adopt by name, keeping only its alarm half: where no pass has come clean by the round alarm's first firing, one closing round scoped to the diff since the last fully reviewed revision, or to the whole deliverable where no revision was fully reviewed, then Shipped at an escalation with the punch list), the two alarms (the round alarm, at every Nth round that found a blocker, N the user's figure in the anchor and 4 by default, and the time alarm, when a phase runs past the user's time budget, two hours by default, without a round closing, either of which stops the loop until a ruling given after it fires), the five terminal states a phase can end in (only one of them clean, and Unable is the one where resume is false), the class question asked of every blocking finding with its fix-the-class rule (deliberately not a label naming the cause: causation is usually unprovable and a guessed label gets counted as a fact by the diagnosis), the build-the-check-first rule, the diagnostic form of an alarm's escalation, which leads with ship risk and only then explains the loop, and the nine invariants from the 2026-08-28 three-session field audit (a pass certified against one frozen revision, only a post-firing ruling restarts an alarm, seat deadlines with liveness read from the job's own record, simplification before the certifying pass, non-mutating reviewers, the pre-delivery gate statement, the record authoritative on resume, the deleted no-path-resolves rationale, and the unresolved-template-marker rule from issue #15, which was a Red flags bullet until the hub's Red flags section went and now sits in step 2 beside the prompt-assembly rule, each one a failure observed in production transcripts) are all specified in this repo's skills/doctrine/SKILL.md step 5, which the README summarises and points at. Don't loosen them casually when editing skills. The terminal states, the class question and the diagnostic escalation were added after a six-round run in this repo whose rounds 5 and 6 found no code defect and kept finding prose the earlier rounds' own fixes had made stale: the gate was right to count each one, and wrong only in that nothing cheaper than a review round could see them. The class question shipped first as a fresh/fallout label, in 9a96069, and was cut back to a question the same day, because a cross-model review pointed out that it made the orchestrator assert a cause it could rarely show. Two more came from a driven 13-round run on 2026-08-28 (a prose-heavy two-repo deliverable): the pass after a repair is pointed at the repair, and an alarm's diagnosis says whether the exit condition is reachable at all and may propose a narrowed blocking definition for the user to rule on. Both are additive and neither loosens the gate: the first adds a target to a pass that was already running, the second adds a question to a diagnosis and a proposal the user rules on. The evidence for the first is that eight of that run's thirteen rounds found their worst blocking finding inside the previous round's fix. The evidence for the second is that the run reached round 13 without one clean pass, not because the artifact was bad but because a false claim is blocking by definition and three adversarial seats over several hundred lines of prose will find one every round, and the findings fell from state-loss defects to comment accuracy while no pass came clean. That run is also the first time the hub's own step 5 was driven end to end rather than read, which was this repo's top-ranked next move.

Some rules only look redundant. doctrine-gauntlet records a running list of pairs that read as the same rule stated twice and are not — each survives because a run can comply with one and violate the other, and several exist because the two halves reach two different agents' prompts. The register that recorded them with their distinctions was never tracked in this repo: no file under skills/doctrine-gauntlet/ has ever been deleted, so nothing in history holds it (checked 2026-09-07, after this file claimed for a week that it was recoverable). Do not write a count of them into this file: it said "ten" until an audit found roughly forty, and any number here is a defect waiting to be cited as fact. Before proposing that two rules there are one, make the argument yourself against the current text: show a run that can comply with one and violate the other, and check whether the two halves reach two different agents' prompts, which is why several of them exist.

Files that must stay in sync

Adding, renaming, or rescoping a skill touches all of:

  • skills/<name>/SKILL.md (frontmatter name: must match the directory)
  • Every count in README.md, not just the obvious one: the opening line, the skill table heading, the wrapper count in the doctrine row, the install line, and the Requirements lead. Grep the file for the old number rather than trusting this bullet to have listed every site.
  • The wrapper list in skills/doctrine/SKILL.md's intro
  • Descriptions in .claude-plugin/plugin.json, marketplace.json and the root plugin.json, whose name, version and description match .claude-plugin/plugin.json; its extensions.com.openai.hooks: {} is what keeps Codex from loading the Claude Code hooks, so it stays
  • tools/doc-check.mjs, whose corpus is every .md under skills/. A new skill enters that corpus the moment it exists, so run it; its header is a contract and a claim in it that a change makes false is a defect like any other.
  • This file. CLAUDE.md states the skill count and the wrapper count in more than one place, and it is the only file no other file's checklist points at — grep it for the old numbers. This is not hypothetical: the round-18 change to the definition of a "finding" was synced into README.md and missed here, and the commit that fixed it says so.
  • If the new wrapper brings a new external dependency: a fallback, and a bullet in README.md's Requirements section. The fallback goes wherever the law above allows — a row in the Fallbacks table in skills/doctrine/SKILL.md, or stated inline in the wrapper; several of the current prerequisites take the inline form and are correct, so a checklist demanding a table row would be failed by this repo on the day it was written. No number is written here on purpose, for the reason this file gives twice elsewhere: a count rots silently and is then cited as fact. The README bullet has no such alternative: Requirements is the only list a user reads before installing, and a dependency missing from it is invisible. Its optional entries live in docs/requirements.md, a page a reader of the README never opens unprompted, so a dependency added there is invisible unless it is also named in the visible lead.

The one skill with code in it

doctrine-gauntlet is the only skill with bundled code; doctrine-project also bundles a sidecar, formats.md, its format reference loaded on demand under the split rule below. The gauntlet's bundled files: harness/floor.mjs (the technical floor its critics depend on), floor.md (how to drive that harness — flags, exit codes, render honesty), harness/round.workflow.mjs (one fused-gate round as a Workflow-tool script: structure and counters only, never a brief), harness/round.codex.mjs and its selftest (the same round on Codex, and on Claude Code wherever the other-family red-team seat is present: it runs the script unchanged and hands each agent call to a seat through a journal, the red-team# call to the other model family's seat), workflow.md (how to invoke it and its tamper test), tools.md (Codex and Claude Design invocation). The sidecars exist so SKILL.md carries decision content and gate law while operating manuals and review history load on demand. The split rule: if it changes what an agent decides, it belongs in SKILL.md; if it changes how a tool is invoked or records a call already made, it belongs in a sidecar. A third kind existed and no longer ships: a record of rulings already made, read by a maintainer and never assembled into a prompt. Two of those were removed on 2026-08-29 because this repo is used by people other than its author and internal review history is not theirs to download. harness/floor.mjs, harness/round.workflow.mjs and harness/round.codex.mjs with its selftest are the only code any skill ships; the repo also tracks tools/doc-check.mjs, its sidecar tools/doc-check.selftest.mjs, tools/herdr-lint.mjs and its sidecar tools/herdr-lint.selftest.mjs, fixtures/shadcn.sh, the fixture phase under fixtures/e9-phase/ and the files under hooks/, none of which a skill loads. One qualification since issue #19: the hub's steps 3 and 5 name hooks/dctr-gate.mjs and hooks/dctr-token.mjs, doctrine-pane names hooks/dctr-pane.mjs, and doctrine-project names hooks/dctr-project.mjs and hooks/dctr-token.mjs, as commands a run invokes — invocation, not loading; no skill carries its content and the harness files stay the only code any skill ships. The hooks are loaded by the harness rather than by a skill, which is why the layer-not-fork law does not reach them and why they need their own gate. The floor is run as node floor.mjs rather than executed; the workflow script is run by round.codex.mjs on Codex and wherever the other-family red-team seat is present, otherwise by the Workflow tool, each wrapping its body in an async function — so node --check rejects its top-level return and the syntax gate is new Function around the body, as workflow.md records. git ls-files -s shows exactly one mode-100755 file in the repo, and it is fixtures/shadcn.sh. The workflow script carries no brief text, for the reasons and with the disclosed exceptions workflow.md gives, and its tamper test is the same three-clause law as the harness's — the fixtures ship as harness/round.tamper.json and run through the Workflow tool and through round.codex.selftest.mjs. The harness's portability is the point: it resolves Playwright and axe from whatever the host project has, and reports [UNMEASURED] rather than passing something it could not check. Never hardcode a path into it.

Keep SKILL.md in step with what the code actually does — it claims specific behaviour (four widths, lazy-load scrolling, overlay hiding, the flag set) that the code must still perform.

Two width constants, deliberately distinct. WIDEST (2560) is the top of the ladder; DESKTOP (1440) is the reference for "enough room available" judgements. The inner-clip discriminator must stay anchored to DESKTOP — a component clipping at 1440 and fitting at 2560 is still a defect, and anchoring it to WIDEST silently re-rules every such case as a deliberate responsive scroller. 2560 earns its two extra configurations per run because a whole defect class lives above 1440 and no number of rounds below it can see the class at all: seven gauntlet rounds passed a page whose nav was 13px at every resolution up to 4K.

Every new check ships with a tamper test, and it has three clauses — break the thing and confirm the check trips; run it against a known-good artifact and confirm it stays quiet; and prove the broken fixture really carries the defect, independently of the check. A check that silently measures nothing prints exactly what a passing check prints. The second clause is not optional: it is what distinguished a real layout defect from a design system's deliberate responsive scrollers, which look identical to a check that has only seen the broken case. The third is the one to reach for when you are editing this repo, because it is what the first two cannot do. The reduced-motion check passed both of them and was blind anyway: it read each element's own computed opacity, which does not inherit but composites, so opacity: 0 on a reveal wrapper left every child computing 1 and nothing painted — and the fixture it was built against happened to put the zero where the check could see it, so both halves behaved. Reading the render is what settled it: an h1 and nothing else under TECHNICAL FLOOR: PASS. The same read condemned the fixture — its content was opacity: 0 in both renders, 7,596 non-white pixels in each, so the "correct failure" clause one certified was the check flagging permanently hidden content, not a motion defect. A change that makes the harness quieter needs the same proof as one that makes it louder: the visibility filter silently stopped reporting a bound system's checkboxes — correct, as it turned out, since the label is the real target — but "a finding disappeared" and "a blind spot appeared" print exactly the same thing, which is nothing.

"No fixture reaches it" is a missing fixture, not a dead branch. Before deleting a guard whose absence no check can show, establish whether a fixture could reach it. Those are two different claims and this repo conflated them once, at cost: a reviewer showed the pane_not_found branch of the focus lookup was unreachable by every fixture, it was deleted, and a second reviewer then showed the deletion leaks a finished codex tab's marker for an entire session, because staleSideSeats never judges a tab seat. Both reviewers were right. Unreachable by the fixtures and unnecessary to the code are separate findings, and only the second one licenses a deletion. Where the first holds and the second does not, write the fixture. This rule had no written statement until 2026-09-08 and was carried instead by five code comments asserting it as repo law, which is why the deletion looked correct to the reviewer who made it.

Target size is split across the gate and [JUDGE] on purpose. WCAG 2.5.8's exceptions are real — an isolated undersized target and an inline-in-a-sentence link are both spec-compliant — so only a crowded undersized target gates, and the exempt ones print for a critic. Do not "simplify" this into a flat 24x24 gate: that invents a rule the spec does not carry, and the harness's whole claim is that it never passes or fails something it did not actually measure. Two flags exist because without them the floor can never close on ordinary projects: --theme-class=NAME for class-based theming (Tailwind's dark), and --single-theme for a site that genuinely ships one. --crop=SELECTOR exists because a full-page screenshot is read scaled to fit, so nothing else lets a reviewer see a figure at the size it ships. Any new theme-application site must go through the shared applyTheme() helper; a second call site setting data-theme directly is how the reduced-motion pass silently measured the wrong theme.

The harness applies the theme, so it must not certify one nobody can reach. The reachability handoff prints [JUDGE], never [UNMEASURED], on every two-theme run that is not --single-theme or --fragment; what the probe found (a media query, a toggle, a script, weak signals, or nothing) changes only the message, not whether it prints. That is deliberate: both themes really were rendered, so nothing went unmeasured — the possible defect is that one of them ships to nobody, and only a critic can rule on that. The separate dead-switch branch, where the two renders come back identical, pushes a real [UNMEASURED] and a [JUDGE]. The probe is load-bearing, not a nicety: an instrument that creates the state it measures will otherwise pass a dead palette exactly like a working one. Keep the probe honest if you touch theming — it is the one check whose absence is invisible in the output.

The prose has a gate now, and it exists because it did not. tools/doc-check.mjs reads every .md under skills/ against doctrine-gauntlet's fixture file and fails on a stale fixture name in a document that names a live hyphenated one, on three or more fixture names listed inside one sentence, on the same long claim appearing in two places, on a fixture the documents' own derivation rule cannot classify, on an attributed citation that no longer resolves — against the document it names where it names one, against the corpus where it names only a section — on a sentence about claims in a rewrite that omits the refute exemption another sentence in the same document carries, and on a span-copy instruction (from, a quoted marker, through) whose marker first occurs inside such an instruction or does not occur exactly once outside them. Every one of those rules is narrower than that sentence makes it sound, and the tool's own header is where each narrowing is written down. Its header states what it does not catch, and that list is longer than what it does — read it before trusting a clean run. It sits outside skills/, so no skill loads it and the harness files are still the only code any skill ships; it is tracked and would be distributed with the repo like fixtures/shadcn.sh. Run it with --selftest for its tamper test, whose third clause asserts the synthetic inputs really carry their defects without calling the checker at all.

None of its rules is arbitrary — each is a failure that actually shipped here. A review round found a document pointing at an enumeration deleted in the same batch. Review rounds repeatedly found a roster that had gone stale because a fixture was added elsewhere; a roster rots whenever any other fixture changes, while a claim about one named fixture rots only when that fixture does, which is why the check counts names per sentence and not per paragraph. And a rule copied into seven fixture notes forked from the original the first time the original was corrected — the copies were found by five expensive review rounds and then, after a fix that replaced one identical sentence with another identical sentence, by this script in under a second. Prefer stating a convention once and pointing at it. Silence inherits a correction; an echo blocks it.

Capability and accuracy outrank size. A missing rule costs a defect in shipped work; a present one costs tokens, and those are not comparable prices. Don't compress for its own sake, don't report length as a concern, and don't cut a rule because a file feels long. The test is whether a rule fires, and whether it reaches the prompt of the agent who must obey it — a rule nobody has tripped, in a prompt nobody assembles, is the thing to cut. Sidecars are still right when they change who loads what, and wrong when they exist to make a number smaller.

Session state, and one tracked thing that is not the plugin

SESSION_MEMORY.md at the root is session state, not documentation, and it is untracked: a working record of where the last session stopped, what it decided and what it left open, rewritten wholesale rather than appended. It is in .gitignore, so it is local to this machine and does not ship to anyone installing the plugin. Read it to resume; never cite it as law, and never let a claim in it outrank the code. It was never tracked: the 2026-08-28 commit that added it to .gitignore touched only that file and this one, and no commit on any branch has carried it (checked 2026-09-05 against every commit's file list, after this file claimed 34 such commits for a week). A fresh clone starts with no memory file at all rather than with another machine's.

docs/research/ held dated dossiers from investigations whose results shaped the skills, one of them a negative result that rejected all four rules it proposed. It was removed on 2026-08-29 for the same reason and is recoverable from history. Nothing in the repo depends on it: a dossier was always evidence about the day it was written, never a description of how anything currently works.

Conventions

  • SKILL.md frontmatter description: follows the "Use when …" trigger-phrase style; it is what makes the skill fire, so write it for matching, not marketing.
  • Wrappers end with a short "Red flags" section: concrete failure modes, not generic advice.
  • Push to the remote only when explicitly told to.
  • Project and epic tracking for this repo lives in the private companion repo scottcrosby-securebine/doctrine-skills-project, not here, so rulings and returns never ship in the plugin tree. A phase in this repo follows doctrine-project's lifecycle procedure against that repo's docs/PROJECT.md, and runs check and status from its root.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.