Repository navigation
chore(skills): add the repository's agent skills - #1143
Merged
Merged
Conversation
Reviews and fixes in this repository spend much of their time rediscovering how work here is verified: which probe answers a claim, which controls a differential needs, which reader of a construct a change missed. The lessons exist only in reviewers' heads and in the history of PRs that fixed defects after their first push. Add four skills under .claude/skills, which coding agents read in place: - morphic-verifying-changes: a table from claim to probe, rules for planting defects, differentials and misleading toolchain behaviour, and scripts that plant one defect and restore it (mutate.sh), compare two builds of cmd/morphic over a corpus (sweep.sh), and list the packages a change can affect (affected-packages.sh). - morphic-reviewing-prs: the review passes, the probes that found what earlier rounds missed, and scripts that set a PR up in its own worktree (review-setup.sh) and scan what it ships for attribution, session words and machine paths (artifact-scan.sh). - morphic-openapi-compiler: where the compiler's rules, library behaviours and tests live, and its recurring traps. - morphic-curating-skills: where a new lesson goes, the limits a skill keeps, and skills-lint.sh, which holds every path, test, section and keyed-doc entry a skill names to the tree. Each rule keeps the PR or commit that showed it. Skills link to the normative documents rather than restating them, name nothing specific to one machine, and their scripts run under bash 3.2 and on Linux. .gitignore keeps the rest of .claude/ ignored as agent scratch space; only skills/ is tracked.
A final pass over the skills against the review procedure and the lessons they were drawn from found gaps: - The review skill had no pass for the CLAUDE.md invariants and the Go style rules no linter enforces, and did not say that a regression is never a limitation, that an oddity is checked for intent before it is called a defect, or that a review posts nothing unless asked. - Nine lessons were missing: fuzzing a rewritten reader against the old one, query differentials, the failure form of re-attributed records, re-rooting generated sweeps, a test two mechanisms keep green, library mutants through -modfile, table entries before tidying fixtures, instrumenting with -v, validating a source reader against history, and probing the external-document reader by counting fetches. - One rule still named scripts/check-coverage.sh, removed in #828. skills-lint.sh checked only paths in backticks; it now checks paths written in prose too, which is how it was found. - review-setup.sh assumed the PR lives on origin; REMOTE overrides it for a clone of a fork. CLAUDE.md now says where the skills are, for agents that do not discover them on their own.
The six skill scripts were bash, with perl for in-place edits and time limits, and the differences between macOS and Linux shells kept turning into bugs: awk record separators, pipefail ending a script on an expected non-zero status, bash 3.2's handling of empty arrays. Python's standard library covers what the scripts do on both systems, and keeps the skills' tooling apart from the Go codebase and its gate. Each script is now Python 3.9, standard library only, with a unittest file beside it. skills_lint.py holds the scripts to that form: each parses as Python 3.9, has its test beside it, and, where the running interpreter lists its standard library, imports nothing outside it. It also reports a bad link target once rather than twice. sweep.py now compiles specs in parallel.
- skills_lint.py checked paths only in single-word spans, so a path
inside a command (python3 scripts/x.py ...) went unchecked. It now
checks each path-like word of a command, leaving placeholders such as
<spec> and {owner} aside and reading ./x/... as the package x. That
found a rule naming a ./cmd/foo that does not exist.
- affected_packages.py compares real paths, so git and go spelling the
checkout through different links cannot empty the list, and lists
everything when a changed Go file maps to no package rather than too
little.
- sweep_test's timeout case timed both stand-ins under a one-second
limit, so a slow interpreter start could kill the healthy one; it now
times only the hanging one.
- sweep.py documents -j, and mutate.py says its verdict is read from
go test output.
- Every script and test now wraps at 100 columns.
- sweep.py clears a base worktree a crashed run left in its output directory, which made a rerun into the same directory fail. - affected_packages.py says on stderr when no Go package reads the changed files, rather than printing nothing for go test to read as the current directory. - A rule's probe named a function that never existed; it now names the check it meant, byte identity. - The scripts are executable, matching their shebang lines.
Each script gives exit code 1 a meaning (SURVIVED, specs differ, hits found, findings), and an uncaught Python exception also exits 1. A test command that cannot start made mutate.py report SURVIVED, and a missing go made sweep.py report differences and affected_packages.py print nothing for go test to read as the current directory. Every script's entry point now prints the traceback and exits 2, which never means a verdict; a test per affected script runs it with the tool missing. review_setup.py reads the PR before creating its worktree, so a failing gh leaves none behind. A rule told readers to rescan after every rebase of a change; this repository merges main into a branch instead, and the rule now says so.
mutate.py judged one mutant per call and never ran the test command on the clean tree, so a suite that already failed read every mutant as killed, and a set of mutants cost one invocation each. --batch reads a JSON-lines file of mutants and refuses the whole batch if any entry is malformed, missing, ambiguous or a no-op, so nothing runs on a typo. It then requires the command to pass on the clean tree, plants each mutant in turn through the existing restore-always path, and prints one line per mutant (the tests that killed it, or why there is no verdict) and a tally. Exit 0 only when every mutant was killed. The tests now capture what main prints, so a run leaves the console clean instead of printing verdict text that read like a failure.
The references converted from the review catalog and those written from past lessons followed two formats. The converted rules carried truncated "Seen in" quotes and "Also" bullets, many ran past the 150-word cap the curating skill sets, and several restated a lesson another file already held. Nothing checked the format, though the curating skill ranks a check above prose. - Every rule is a heading, a key, and Rule, Why and an optional Probe. Each Why cites the PRs or issues that showed the failure; the quotes are gone, their PR numbers kept. - Rules teaching the same lesson are merged into one, with the evidence of each: untaken guards, bounds lowered by one, instrument checks, mutation claims in PR bodies, scheduling flakes, the merged-tree review, writers of a mechanism, doc inventories, library-mirror caps and one report per malformed construct. - Rules move to the skill whose trigger covers the moment they apply. Counting and sweeping rules move from reviewing-prs to verifying-changes, which fires on counts and every/never claims and which reviews load too. Rules about verifiers, lint and archtest rules, codecs and loaders form checkers.md there, out of the OpenAPI skill whose trigger never fires on them. OpenAPI-specific sweep rules move the other way. spec-and-grammar.md is split between the two. - skills_lint.py holds each rule to the format: a key unique across the skills, the bullets in order, a citation in Why, the word cap. - verifying-changes now also fires on writing a verifier or a lint or archtest rule, and on fixing a defect.
The review skill's cost probe, a regression sweep and an issue's priority all measure against the golden spec (CLAUDE.md, "Priority"), and each fetched it by hand. golden_spec.py fetches it once per commit of cloudflare/api-schemas into a cache outside the repository, resolves HEAD to the commit measured, refuses to cache a response that is not JSON, and prints the file's path or how many objects carry each key. Its counts agree with jq's on the spec at 1a7b2e42.
A skill cites a rule by its key in a code span, but nothing checked that the key exists, so renaming or merging a rule would leave its citations pointing nowhere. The lint now reads a code span of lowercase words joined by hyphens as a rule key, other than a skill name, and reports one that no reference defines. The curating skill states the rule format as the references use it: a heading is the rule in one line, not necessarily an imperative; a Probe is left out when the Rule is itself one; a Why may carry the counts its citation pins. It also closes two gaps in where a lesson goes: a check that is filed rather than written keeps a rule naming its issue until it lands, and a lesson no skill's trigger reaches widens a description or adds a skill rather than sitting where nothing loads it. Paths are relative to the repository root bar those inside a skill's own folder, and commands run from the root. The caps read "within", as the lint measures them.
review_setup.py checked out the PR head as pushed, so a review of a branch behind main compared main with a tree that also undid main's newer commits, and the merged state was left to a last pass. It now checks out the head merged into main when they merge cleanly, the head itself when it already contains main, and the head alone, reported, when they conflict. The merge commit is made under a fixed identity, so a clone with no configured user can make it. The skill's passes follow: the gate is run in that worktree unless CI already ran the same tree, behaviour is compared with origin/main, and new commits on either side mean rerunning the setup. Findings ranked nit or suggestion read as optional, against the repository's bar that every known defect is fixed or justified in writing before merge. The rank now orders the work only; every issue and nit is fixed or justified. process.md measured the title against a three-digit PR number, one character short for four-digit ones; review_setup.py measures it with the PR's own number, and the rule points there. The skill pointed to process.md for closing keywords, which held no rule on them; it now does, from #948 and #1016.
The rule that a new bound in the load scan warns and lets the compile go on described a convention main no longer has: the reference-chain bounds that warned are gone, and nesting past the scan's bound is refused. A new bound now takes its outcome from the rules entry for its kind, and joins that entry. The conformance-case rule names the package of the -update run it asks for, defers to CONTRIBUTING.md's recipe rather than restating part of it, and names the tests a careless case fails. The skill's traps cite the rule that holds each.
mutation.md had become the home of rules that are not about planting defects: subtests, fixture order, cut alignment, crash oracles, -v instrumentation and typed testify calls. An agent writing a subtest or a differential would never open it. Each moves to the reference about its subject, and the two that repeated a rule already there are merged into it. A cancellation claim moves from the toolchain traps to the claims. A Probe that only read code now runs it, a Probe that restated the script is dropped, a coverage command gains the -race the Makefile uses, and the order-oracle rule names orderInvariant. The skill's guard probe forces each side of a guard rather than one, and its description fires before a change is called done.
The paragraph pointing at .claude/skills said it held procedures for working in "a compiler", where one skill covers the OpenAPI compiler alone.
The lint read every hyphenated code span as a rule key. In this repository's skills that is too wide: conformance case names, rules-entry keys, validation classes and x- extensions are hyphenated too, and the OpenAPI skill is where they will be written. A bare key also leaves the reader to search for the file that holds the rule. A rule is now cited as references/<file>.md#<key>, from another skill with its path from the repository root, and the lint holds each citation to a rule in that file, so a rule renamed or moved to another file leaves no citation pointing nowhere. A bare span naming an existing key is reported with a request for its file. A code span holding a placeholder such as <file> names no path.
Four probes measured something other than what their rules claimed: - The two-order rule said to compare diagnostics by their message, "as internal/harness does". The harness leaves the message out on purpose, since some messages list keywords in source order; comparing it reports order dependence that is not there. The rule now keys the multiset as diagnosticSet does. - The test-move rule diffed `go test -list . -v` for subtest names, but -list names top-level tests alone, so a dropped table row never shows. It now diffs a verbose run's PASS lines. - The golden rule reverted a fix with git stash; it now reverts through mutate.py, which restores whatever happens, as the restore rule asks. - The OpenAPI skill compiled without --verify, which checks each stage's IR against its invariants and is meant for developing a stage. The lexical file-reference rule now cites the references entry it applies and keeps only what an agent does with it, and a Why that stated a success states the failure the probe prevents.
The restore rule sends instrumentation and measurement switches through mutate.py, but mutate.py printed only its verdict, so a planted println or a timing printed nothing anyone could read, and an UNVERIFIED verdict showed only the first lines of why. Each run's whole output is now kept in a file whose path is printed; a batch keeps the baseline's and one file per mutant, numbered in batch order. The rule says the same, and that a scratch file added for a probe is deleted before a commit.
internal/keydoc reads a citation of a keyed document's entry on every Markdown line and in every Go or YAML comment, and the OpenAPI e2e and canary tests hold each one to its document. A change adding such a citation outside the packages those tests import was missed: a comment citing docs/compilers/openapi.md#no-such-rule in cmd/morphic-covergate drew only that package and internal/archtest, while the e2e test fails on it. When the change adds a citation, the script now lists the packages whose tests call keydoc.Citations or keydoc.Walk, found by grep rather than kept by hand. It also says why `go test` with no package fails when nothing reads the change.
CLAUDE.md asks a PR whose title marks a break with ! for a Breaking section in its body. review_setup.py checked for Summary and Test plan alone; it now reports a missing Breaking section under a ! title, and a Breaking section under a title that marks no break. The review skill fires for a branch or a commit range, but its setup takes only a PR number; it now says to build the same worktree by hand for those. The body was tightened to stay within its cap.
Every change ends in commits, a PR body and usually a merge of main, and the lessons for that step lived in two wrong places: the review skill's process.md, which nothing loads while an author writes a commit, and notes outside the repository. morphic-shipping-prs loads when committing, writing a commit message or PR text, opening or updating a PR, merging main in, or splitting a change into commits. Its commits-and-prs.md takes process.md's author rules (message form, closing keywords, scope, recorded decisions, stated limitations, the coverage gate) and the toolchain rule about what a commit may hold, and adds rules for gating a commit on exit status, marking an exported API change as breaking, testing each commit of a split, editing a posted body from its live copy, and re-reading prose after a redesign. merging-main.md holds what a merge of main can lose. process.md's review rule moves to review-probes.md, and the review skill reads the commit rules from their new home. preflight.py runs before a push: each subject, the title measured with the number GitHub appends, the body's sections against the title's !, the exported functions, methods and types the branch removes or changes, untracked files, the merge with main, and the artifact scan. It reuses the review scripts' checks, so the lint now accepts an import of another skill's script. On #551's commit it lists the changed compile.NewTypes, and on a pure refactor it lists nothing.
An IR change meets things ir-design does not spell out: another open PR claiming the same IRVersion, which merges silently on the constant; classification tests that a new field, ID class or sum kind must pass, and the one gap among them, a stale entry that passes; guarantees a map held that a list does not; and predicates that name a struct's fields one by one. morphic-changing-the-ir loads for work under ir/ or pass/, the IR and emitter design documents, and IRVersion. It links ir-design §2.1 and architecture §2.5 for the bump and landing rules rather than restating them, and names the bump no test sees: a change of meaning with no change of shape. The four checker rules about the IR's own verifier and codecs move here from verifying-changes. irversion_peers.py lists the open PRs that claim an IRVersion past main's and reports a collision or a claim that trails main; with the working tree set to 0.22.0 it reports #1144's claim of the same.
Recent work is cost-heavy, and its lessons were specific to this repository: count work rather than time it, assert marginal growth, count allocations in the instrumented build the gate runs, give an optimization a counting seam, and quote instructions retired rather than wall time on a shared machine. morphic-measuring-performance loads for a speed or memory change, a cost, allocation or scaling test, a benchmark, or a claim that a change is faster, cheaper, linear or allocation-free. Its two references hold the tests that pin a cost and the places cost hides; the two cost rules in verifying-changes move into them. Each cited test, helper and seam was checked against main, and the rules name today's ones. compare_cost.py builds a command at a base revision and at the working tree, runs them alternately and prints the median, range and ratio of instructions retired, CPU time, peak memory and wall time; on a spec with no change between the builds every ratio came out within 4% of 1. The review's price-the-common-path probe uses it, and review_setup.py names the IR and performance skills for the PRs they apply to.
The curating skill listed the skills by subject and CLAUDE.md named three of them; both drift each time a skill is added. Each skill's description already says when it loads, so the curating skill gives the command that prints them, and CLAUDE.md names two procedures as examples.
A review of the shipping skill found rules that said more, or other, than they meant: - It said never to rebase the branch; CLAUDE.md forbids rebasing a pushed one. It now says so, and cites the merge rule rather than repeating its steps. - The body-edit rule applied a body with `gh api --input <file>`, which sends the file as the whole request: a Markdown file fails. It now uses `-F body=@<file>`, which reads one field from the file, as gh's own help gives it. - The coverage rule's "every new branch ships with its test" meant a code path but read as a git branch here, and it required make coverage where CI's run of the same tree serves as well. - The merge rule asked for a merge of main before any push following a change on main, beyond what CLAUDE.md asks; it now asks for one before a review round and before calling the branch ready. The rule pairing a normative-text check with reusing what main landed is two rules. preflight.py ignored a body given without a title. It now judges the body's Summary and Test plan, leaving the Breaking section to the title.
The hand-list note pointed at "what none does" for the one gap no classification test closes; it now names it, a stale entry left for a deleted field. The breaking note said a shape change breaks every consumer, where ir-design §2.1 says every bump does, a change of meaning included. irversion_peers.py listed a PR whose head declares no IRVersion as claiming "(none)"; such a head claims nothing and is left out.
compare_cost.py and sweep.py compare a build of origin/main with the working tree, and on a branch that does not hold main the difference also counts what main gained since: the mistake that read a speedup on main as a regression on a branch (#1131). Both now warn when HEAD does not hold the base, compare_cost.py through sweep.py's function rather than a copy, and the performance skill holds the lesson as check-the-base-build, taking it from the shipping skill's merge rule. compare_cost.py also left its two builds, about 55 MB, in the temporary directory after every run; it now removes them. The cost-test rule for renders says to measure one visitor per call, since on main a visitor's pruning is every visitor's, and the performance table cites both rules a linear claim rests on.
sweep.py and compare_cost.py each resolved a base commit, warned when HEAD did not hold it, built a command at the working tree, built it again in a detached worktree of the base, and removed the worktree: the same steps written twice, each copy missing what the other did. sweep.py cleared a worktree a crashed run left behind; compare_cost.py captured the build's output, so a failed build said why. sweep.py now holds resolve_base and build_pair, which do both, and compare_cost.py imports them as it already imported the warning. A missing package now reports go build's own error from either script. compare_cost.py's table also computes each side's median once rather than twice.
preflight.py defined a run() helper identical to the one in artifact_scan.py, which it already imports; it now uses that one. The performance skill's table restated the review probe for pricing the common path, input for input; it now cites the probe.
skills_lint.py read no argument at all, so --help ran the whole lint and an option it does not have, such as --strict, exited 0 as a clean lint that had never applied it. It now parses its arguments: --help prints the usage, and any argument is refused with exit 2 before anything is linted, as the other scripts refuse theirs.
After listing the specs that differ, sweep.py suggested diffing their diagnostics files, which show nothing for a spec whose IR alone differs, so following it showed a real difference as none. It now names the IR pair and the diagnostics pair beside it. Its spec detector reads a version key first on a line, indented or not, which is how it finds a JSON spec a parser refuses; the docstrings said a key starting a line.
gh issue list and gh label list stop at a default number of results, and outside a terminal they say nothing of what they left out. The commands CLAUDE.md quotes for searching the tracker, reading the area prefixes in use and reading the label vocabulary took that default: the label list read 30 of the 34 labels and left out priority:P1 to priority:P4, which every issue must carry, and the prefixes came from 30 of 250 open issues. The search for a recorded decision also read open issues only, so for ir.Naming it found 2 of 16, without #184, the closed issue its own example cites. Each now passes --limit 1000, the decision search also --state all, and the tracker-search bullet says why. docs/micro-compiler-plan.md's status line, which calls gh issue list --state open the truth about what remains, gets the same limit.
A rule's Why cited the PR or issue that showed its failure, and the skills asked that each item show what its clause says. Many process lessons came from the work on a PR rather than from its record: #878's record never mentions the fold that reverted #870 while it was prepared, nor #966's or #475's the stash a pop applied, nor #591's the backup restored from the wrong copy. Their records show the fix but not the slip behind it, so whether a record shows a clause has no crisp answer for most of them, and marking each would rest on judgments a later comment could undo. A citation now names the work a failure showed in, which its record may not describe, and a rule is checked by its Probe. The curating skill states it in the rule format and its upkeep step asks that each item be the work its clause describes, which still catches a citation naming the wrong item. CLAUDE.md says it beside the skills, where every reader of a rule meets it.
derive-never-maintain says never to put a count beside the list it counts, since the count goes stale when the list changes, and four rules did: classify-ir-fields said "the other two tables" after naming them, marginal-growth-on-one-path said "all three sizes" after n, 2n and 4n, test-co-declared-keywords-together said "all four documents" after listing the compiles, and transform-differential said "two suffixes" before naming them. Each now names the members or reads "each" and "the". A scan of every number in the skills' prose outside the Why lines, where a count is evidence its citation pins, found no other: the rest are limits, parameters, sections, steps, versions and pairs such as before and after.
mutate.py held the original only in memory and wrote it back with one truncating write. When the disk filled during the test run, which writes to the go build cache, that write failed partway and left the file empty: on a 2 MB disk image the test command filled, a 220 KB file was left at 0 bytes and the script ended in a traceback, so an uncommitted edit in it was gone. Its own rule cites a full disk that stopped a restore mid-batch. The original is now copied beside the kept output before anything is planted, and nothing is planted when that copy cannot be written. The restore runs with SIGINT, SIGTERM and SIGHUP ignored, so a second interrupt cannot cut it short, and drops the copy once the file reads back as the original. When it does not, the run ends with exit 2 and names the copy, and a batch stops there rather than judge later mutants on a tree that still holds one. go heads a build failure with "# <package>", and one in a test file with "# <package> [<package>.test]". mutate.py took any line starting "# " for such a header, so a failing test whose code printed a heading or a YAML comment read as a package that did not build rather than as a kill. It now matches the header forms alone. It still reads them, since a pipe can cut the "[build failed]" line that follows.
skills_lint.py checked a path, link or skill that a skill names against the disk, so a file git ignores passed: with an ignored file on disk under docs/superpowers/, a code span and a link naming it read clean, though no clone holds it. Bare file names were already held to git's listing, untracked files included and ignored ones not, so the two checks disagreed. Every path, link target, skill or command name and cited reference is now held to the files git lists, untracked ones included, and to the directories above them. A path on disk that git ignores is reported as such, and one outside the repository as missing.
The breaking-change rule called any method added to an exported interface breaking. Its own probe disagrees: the pinned apidiff reports a method added to an interface that also declares an unexported method as a compatible change, since no type outside the package can declare that method, and reports the same addition to an open interface as incompatible. Each sealed sum in ir is such an interface, so the rule asked for a ! that no caller or implementation outside the repository needed. The rule and the shipping skill now say an exported interface with no unexported method. The probe's sentence restating that an incompatible change needs ! is dropped, keeping the rule within its word limit.
skills_lint.py held a code span naming .git/info/exclude to the tree. Once the lint held paths to git's listing, it called that file ignored by git and held by no clone, though every clone has it; in a worktree, where .git is a file, it called the file missing. Skills can name it: a clone whose info/exclude lists .claude/ hides new skill files. A first segment of .git now marks git's own files, which no listing holds and a worktree keeps elsewhere, as origin marks a remote's branch.
The test of the restore's signal handling read the handlers during the write, though its name says no signal can cut the restore short. It now sends SIGINT, SIGTERM and SIGHUP to the process as the restore starts writing, and checks that the file comes back and that the handler is put back. Leaving all three live, SIGINT alone or SIGHUP alone, or not putting the handlers back, each fails it.
skills_lint.py read git ls-files line by line. Git quotes and escapes a name holding a non-ASCII letter, so a tracked file named that way was missing from the tree the lint holds names to, its tests went unseen and a skill naming it read as citing a file git ignores. The lint now reads the list with -z. The suite now reaches the paths no test ran under trace: a skill with no SKILL.md, a frontmatter that never opens or never closes, the skips for URL links and __pycache__, the wiring that hands the real tree to the checks, a tree declaring no test, each exit status of main and an unexpected error, which exits 2 rather than as a finding. A defect planted at each turns the suite red.
Run under trace, the scripts' suites left decision paths that no test executed, so no test could notice a defect there; seven planted first passed every suite. sweep.py could stop reporting exit-code differences or passing compile flags to the build, print no warning for a base the branch lacks, keep its base worktree, and exit 0 when it could not compare. preflight.py could stop marking a conflicting merge or an artifact as a fix, mark a held or cleanly merging base as one, scan no commit message, measure a draft title without the next number, or exit 0 or 1 wrongly. artifact_scan.py's exit statuses had no test, nor had mutate.py's Go build check, usage errors, interrupts, a unittest run without OK, the reason it gives a mutant with no verdict, or an unexpected error in three of the five scripts. Each has a test now, and a defect planted at each turns its suite red.
sweep.py decided what a run meant: a spec that exited 2, hit the time limit or was killed on both sides was listed as not compared, and the sweep then exited 2 even when every compiled spec agreed. That reading belongs to whoever reads the sweep, alongside morphic's own exit codes. The sweep now prints each side's exit codes and IR bytes, each spec whose exit code, IR or diagnostics differ, and a row per spec in results.tsv: its number, both exit codes, what differs and its path. It exits 0 when no spec differs, 1 when one does and 2 when it cannot run. The differentials reference now says what morphic's codes mean, and that a spec agreeing on 2, 137 or a negative code was compiled by neither build.
mutate.py, preflight.py and artifact_scan.py each ran a tool and parsed its output into a verdict, and that parsing was where review kept finding defects: a test's printed heading read as a build failure, a pipe hid a failing test, a title was judged by the words of its own report. Reading that output is what the agent does well, so the skills now give the commands and say how to read what they print. A defect is planted in a throwaway worktree made with git stash create, which holds uncommitted work and leaves the stash list alone, so no restore can fail in the tree being worked in, and mutation.md says what counts as a kill. The shipping skill's push checklist gives the subject, title, status, conflict and artifact checks as commands, with the push chained on the skills lint, and a review runs it on the PR's tree. The curating skill keeps a script only to hold an invariant through a crash, count or compare exactly, or check every member of a large set, and has it print facts.
The test of results.tsv expected each spec's path relative to its temporary directory. Under macOS's default /tmp, a symlink, the sweep's resolved spec paths are not relative to it and the table writes them whole, so the test failed wherever TMPDIR was left unset, and a comment-only control planted in sweep.py failed with it. The test now keys the table on each spec's name.
The push checklist measured the PR title's length alone, where the preflight script it replaced also held the title to the Conventional Commits form. The title is the one subject that lands on main, since the squash takes it, so a prose title passed every line of the checklist. The subject check now reads the title too, with the number the squash appends, and prints it as it prints a commit subject: past 72 characters, off the form, or ending in a period. The message-form rule's probe says so.
The differentials reference said morphic exits 1 when a spec has findings. It exits 1 when a diagnostic reaches --fail-on, errors by default, or when the spec lowered to no document, so a spec with only warnings exits 0, as compile --help says.
sweep.py --help listed its options without saying what each does, how compile flags reach the build or what the exit codes mean, so an agent running it learned only the option letters. It now prints the docstring, which says all three, under option names that match it, and the verifying skill points at --help rather than at the source.
The report test checked the exit-code distributions and the hint, so a sweep that printed no IR bytes, read both totals from one side, dropped the list of differing specs or wrote absolute paths still passed. It now compares the whole report and every results.tsv row. The spec paths are relative again: the test resolves its root as find_specs resolves each spec. An earlier fix for macOS, where /tmp is a symlink, had keyed the table on spec names and so stopped checking the path.
The lint holds a span's path to the tree up to a #entry, a :line or a glob, and the real skills rely on the entry and the glob: without those cuts the lint reports 11 and 2 false findings. No test held any of the three, and none held the skip of a tracked test file deleted from disk, without which the lint stops with exit 2 in the middle of a change that removes one. The clean fixture now names each form, and the tree test deletes a tracked test file.
The planting rule credited #1141 with a full disk emptying a file mid-restore. On #1141 the full disk failed the restore and left a mutant planted; the emptied file was a probe on #1143. The rule on sweeping every site cited #1143 with no clause saying what its work showed: a review's fix that missed a second file.
A rule line in the differentials reference and a line of sweep.py ran past the 100 characters the skills' prose and scripts keep to.
Two Whys named no failure: one said only that both shapes survived every corpus sweep, the other that two regressions in #1016 were invisible to every corpus. Each now says what went wrong. On #1009 a file got two labels and a shared request body was named by whichever operation lowered first; on #1016 readers followed references the library never resolved and read a component by its YAML shape.
The review skill's setup said to probe "only there" after naming both the worktree and the domain skill, and its findings section ranked "each issue:" with no noun; it now says to probe only in the worktree and ranks each finding. The curating skill's script step ended on an "otherwise" with no clear antecedent, and its sentence on descriptions ran three predicates off one subject. Both are reworded, and the rules they state are unchanged.
sweep.py's docstring, which --help prints, said every corpus file was read, where only .yaml, .yml and .json files are. The lint's docstring gave every finding a line number, where one about a whole file has none and a summary line follows, and missing() named only the glob among the cuts it makes before holding a path, leaving out a #entry and a :line.
The paragraph that says where the skills are named "the scripts both use" after a list of three things, and began a sentence "Their scripts" after one whose subject was a single skill. It now says "the scripts they use" and "The scripts".
The usage line sweep.py's --help prints read "python3.14 sweep.py", which fails from the repository root, where the skills run every command. It now names the script by its path from the root, as the lint's usage line does.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Reviews and fixes here spend much of their time rediscovering how work in this repository is verified: which probe answers a claim, which controls a differential needs, which reader of a construct a change missed. Those lessons live only in reviewers' heads and in the history of PRs that fixed defects after their first push. This adds them as skills under
.claude/skills, which coding agents load when a task matches. Each skill's body loads on demand and its reference files load one at a time.morphic-verifying-changesmain, stating a countmainshares fixed or filed, a fix at the type that owns the broken state); claim → probe table; rules for claims, counts that drift, sweeping every site, tests, assertions, checks that reach what they claim, planting defects in a throwaway worktree and reading the test command's own output, differentials, toolchain traps;sweep.py, which prints facts and leaves their meaning to the readermorphic-reviewing-prsmorphic-shipping-prsmainin, splitting a change into commitsmaincan lose, and the review's first round run by the author before the first push; the push checklist of commands, which a review also runsmorphic-changing-the-irir/orpass/, the IR and emitter design documents,IRVersionmorphic-measuring-performancemorphic-openapi-compilercompilers/openapi, its corpus or its keyed docsmorphic-curating-skills.claude/skillsskills_lint.py, which checks bothThe rules come from two sources: defects past PRs fixed after their first push, and probes that later review rounds found effective. Every rule has one format (a heading, a stable key, then Rule, Why and an optional Probe), stays within 150 words, and cites in its Why the PRs or issues whose work showed the failure, which an item's record may not describe. A lesson has one home: rules teaching the same thing from both sources are merged, and each sits in the skill whose trigger fires when it applies, so the counting and sweeping rules an author needs live in verifying-changes, and the commit and PR rules in shipping-prs, which reviews read by path. A skill links to
CLAUDE.md,docs/compilers/openapi.mdanddocs/parser-library.mdrather than restating them; parser-library behaviours stay in that page.CLAUDE.mdgains two paragraphs. One says where the skills are, for agents that do not discover them on their own. The other says they are experimental: an agent treats what a skill says as a claim to check, confirming what a script prints with a probe of its own and a rule's fact about the code onmain, says after using a skill how it served, and files each flaw as askills:issue under the repository's filing rules. The tracker and label commandsCLAUDE.mdanddocs/micro-compiler-plan.mdquote now pass--limit 1000, as the skills' copies do:ghstops at a default number of results and, outside a terminal, says nothing of the rest.Decisions to look at
.claude/. refactor: remove duplicated code and scaffolding across the repository #97 ignored.claude/as agent scratch space, and repo: static coverage badge and internal gitignore entries #91 asked that.gitignorelist only project content. Skills are reviewed project content that agents read in place, so.gitignorenow ignores.claude/*and re-includes.claude/skills/; everything else there stays ignored. A clone whose own.git/info/excludelists.claude/has to narrow it to.claude/*, because an excluded directory is never searched for the negation: such a clone keeps the tracked skills, but a new skill file is missing fromgit statusand refused bygit add. The comment beside the rule in.gitignoresays so..claude/skills: archtest's walks and the keyed-doc citation checks skip dot-directories, and the scripts are not Go.skills_lint.pyholds every path in a code span, test,§section and#keya skill names to the tree, and the scripts to their form;morphic-curating-skillssays to run it after every edit, and how to run each skill's script tests. The push checklist chains each push on the same lint, and a review runs it on the PR's tree, so a code change that renames a test or path a skill names meets it before its push and in its review.main, with the commands it gives, so every probe and the comparison withmainsee what merging would produce, not a branch that also undoesmain's newer commits. A head that already containsmainmerges as a no-op, and CI's run on it covers that tree; a head that conflicts is reviewed alone and the conflict reported.sweep-every-site-of-the-mechanismnow triggers on a review's finding as on a fix, and the shipping skill has the author run that round before the first push.#Nnames the work a failure showed in instead, which still exposes a citation naming the wrong item, and a rule is checked by its Probe; the curating skill states it in the rule format, andCLAUDE.mdbeside the skills.issue:,nit:,suggestion:andquestion:to order the work, not to decide what may ship: asCLAUDE.mdrequires, everyissue:andnit:is fixed or justified in writing before merge.apidiff. Whether a change needs!turns on the exported API it removes or changes. The breaking-change rule's probe runs the Go team'sapidiff, pinned to a version, on export data written at the merge base and at the head: it type-checks both sides and applies Go's compatibility rules, so it sees interface methods, fields, grouped members and declarations moved to another package. It misses a struct tag, whichTestIRVersion_PinsTheEncodedShapeholds inir. A script that parsedgo docinstead took 283 lines, and review found it crashing on a valid signature layout; the command replaced it. The rule calls a method added to an exported interface breaking only when the interface has no unexported method:apidiffreports the same addition compatible on an interface sealed by an unexported method, as each sum iniris, and incompatible on an open one, where the rule had called every such addition breaking.sweep.py, which builds two binaries and compiles a corpus with each in parallel, comparing their outputs byte for byte, andskills_lint.py, which holds every name the skills cite to the tree. Ten more were dropped. Seven, about 1,400 lines with 1,150 lines of tests atee6fdd2f, gave confident wrong answers where a command or an existing tool did the job. Three,mutate.py,preflight.pyandartifact_scan.py, about 650 lines with 900 lines of tests at8beb0309, each ran a tool and parsed its output into a verdict, and that parsing was where review kept finding defects; an agent reading the output itself does better. A defect is now planted in a throwaway worktree, which makes unnecessary the restoremutate.pyexisted to guarantee, and the push checklist and the artifact grep are commands in the shipping skill: the checklist holds each commit subject and the title, as the squash lands it, to the form and the cap, aspreflight.pydid, and the body's sections, which it also checked, are a rule the shipping skill states. The rules that named the dropped scripts give the commands instead, and the curating skill holds any new script to the same bar.requires-python = ">=3.14"in a PEP 723 script block, whichuv runandpipx runread and the lint requires, and every command runs it aspython3.14. Each is also executable and starts with#!/usr/bin/env python3.14, which the lint requires too, so one run as./script.pytakes the same interpreter. An olderpython3, such as the 3.9 macOS ships with its developer tools, fails at import with exit status 1 or 2, and 1 is the status each script gives a finding or a difference, so no command leaves the interpreter to chance; a missingpython3.14exits 127. Python is not amongCONTRIBUTING.md's prerequisites;CLAUDE.mdnames 3.14 beside the skills. A first version in bash kept hitting differences between macOS and Linux shells (sed -i,timeout, awk record separators, bash 3.2 arrays); the standard library covers the same work on both. Each script has aunittestfile beside it. The Agent Skills specification's own validator,skills-ref, needs third-party packages, so the lint checks the specification's frontmatter rules itself.Test plan
skills_lint.pyis clean, and catches each planted defect: a missing path in backticks or in prose, a missing test,§section,#keyand skill; a machine path; a memory-style link; a reference file noSKILL.mdnames; a script that is not Python 3.14, has no test beside it, declares norequires-python = ">=3.14"script block, has no#!/usr/bin/env python3.14first line, or is not executable; and an import outside the standard library. Frontmatter is held to the Agent Skills specification and to what a YAML parser reads whole: a name of 1-64 lowercase letters and digits joined by single hyphens, only the specification's fields and each once, and no plain value holding an unquoted:, a trailing:or#, which a YAML parser refuses or reads as a comment that cuts the value; ten planted defects in those checks each turn the suite red, and a:planted in a real skill's description is reported at its line. A quoted name or description is read as YAML reads it, so a description quoted as the lint advises passes, where the lint had read the quotes raw and reported it as not starting "Use when"; a quoted value is matched as YAML closes it, so one ending in an escaped quote, which YAML cannot parse, is refused, and a comment may follow the closing quote. A block scalar is refused with the advice to write the value as one plain or quoted line, and the body's word limit is still checked. Ten planted defects in that reading each turn the suite red. A bad link target is reported once. A file named without its directory, such assweep.py, must be a file the tree holds, and each script must be named in itsSKILL.md; that check once found a script named in no skill. Paths inside commands are checked too, placeholders such as<spec>and{owner}aside. A POSIX bracket class is not taken for a link. A rule is cited asreferences/<file>.md#<key>: a citation whose file holds no such rule is reported, so is a bare span naming a rule key without its file, and renaming a key or citing the wrong file on the real tree is caught at the citing line. Each rule is held to the format: a heading with no key, a key used twice across the skills, an extra or missing bullet, a stray line, a Why citing no PR or issue, and a rule over 150 words are each reported, and planting a defect in each of those checks turns the suite red. The prose check found one rule still namingscripts/check-coverage.sh, removed in build: judge the coverage profile with golang.org/x/tools/cover #828. A code span a line break splits is reported once, outside fenced blocks, because every check reads one line at a time and never reads such a span: the check found one, agh issue listcommand no sweep of the skills' commands had read. It reads a fence as CommonMark does (backticks or tildes, indented up to three spaces, opened by no backtick run whose info string holds a backtick, and closed only by a bare run of the same character at least as long), and three planted defects in the check and six in its reading of fences each turn the suite red. A§section citation holds spaces a wrap can fall on, so its check reads on into the next line and reports a citation once, at the line it starts on; none is split today. Two planted defects in it each turn the suite red, and a third,continuefor itsbreak, survives because it changes nothing. A path in a code span or a command is held to the tree whatever its first segment: withbarrier/renamed toguard/the rule citingbarrier/text.goread clean, and so did a misspeltcompiler/openapi/..., and both are now reported. What a span holds that names no path is told apart by its form (a pointer, flag, pathspec magic, pattern, URL or placeholder) or by a named first segment (a remote's branch, a Go package, a diagnostic code), and seventeen planted defects in that each turn the suite red. A§that follows no document's name, as the second of "ir-design §15 ... and §16" did, is reported, an RFC counting as a document; three planted defects in it each turn the suite red. A description over 500 characters, the limit the curating skill now states with its reason, is reported at 501 and not at 500; the 1,024-character frontmatter cap, which no source gave and no test reached, is gone. The lint takes no argument:--helpprints its usage and any argument is refused with exit 2, where--helphad run the whole lint and an option it lacks, such as--strict, had exited 0 as a clean lint; two planted defects in that refusal each turn its suite red. A path, link target, skill or command name and cited reference is held to the files git lists, untracked ones included, and to the directories above them, where the lint had read the disk: with an ignored file on disk underdocs/superpowers/, a code span and a link naming it had read clean, and both are now reported as ignored by git, and a link out of the repository as missing. Ten planted defects in that holding, a fallback to the disk at each of its six sites among them, each turn its suite red. A span under.git/names git's own files, which every clone has and a worktree keeps elsewhere, and is left out of that holding: the lint had called.git/info/excludeignored by git in a clone and missing in a worktree, and dropping the exemption turns its suite red. Git's file list is read whole, withls-files -z: git quotes and escapes a name holding a non-ASCII letter, so a tracked file named that way had been missing from the tree, and dropping-zturns its suite red. The lint holds a path in a span up to its#entry,:lineor glob, and the real skills rely on the entry and the glob, without which it reports 11 and 2 false findings; no test held the three cuts, nor the skip of a tracked test file deleted from disk, without which the lint exits 2 in the middle of a change that removes one. The clean fixture now names each form and the tree test deletes a tracked test file, and a planted defect in each of the four turns the suite red.sweep.pyovertestdata(213 specs, compiled in parallel): with base equal to head, nothing differs, it exits 0, and both sides' exit-code distributions and IR byte totals print. WithIRVersionplanted at 9.9.9 in a throwaway worktree, 210 of them differ and it exits 1; the other 3, refused alike on both sides, agree, andresults.tsvshows their exit codes. A document written as JSON, even one saved as.yamlor.yml, is judged by its top-level keys, and any other by a version key ending at its colon: the sweep had taken the 56 conformance goldens for specs, since their coordinate keys such as"openapi:schema"read as version keys, and counted them among 269, and it missed a minified spec in a corpus of about two thousand published ones. Over that corpus it now gains that spec, drops eight files that are not specs, and keeps a JSON spec saved as.yaml. A spec, YAML or JSON, saved with a byte-order mark is found too, as the compiler reads past the mark; six planted defects in the detector each turn its suite red. A-tor-junder 1 is a usage error, and five planted defects in that logic each turn its suite red. The sweep judges nothing: under an unknown compile flag every spec exits 2 on both sides, and it reports that no spec differs and exits 0, while the distributions it prints, 2 for all 213 and no IR bytes, show that neither build compiled anything; the differentials reference gives morphic's exit codes ascompile --helpstates them, where it had said a spec with findings exits 1, though one with only warnings exits 0, and says that agreement on 2, 137 or a negative code says nothing. A spec file given as the corpus is read as one document, where it had been searched as a directory and drawn "no OpenAPI or Swagger documents", and dropping that read turns its suite red; run fromir/on one conformance spec at-b HEAD, the sweep reads no difference. Its hint for a spec that differs names the IR pair beside the diagnostics, where it had named the diagnostics alone, which show nothing for a difference in the IR only; reverting the hint turns its suite red. Its report is now pinned whole: four planted defects, printing no IR bytes, reading both totals from one side, dropping the list of differing specs and writing absolute paths, had passed its suite, whose test ofresults.tsvkeyed on spec names, and each now turns it red, the test resolving its root as the sweep resolves each spec.--helpprints the docstring, whose usage line runs from the repository root and which says what each option does and which files are read, where it had listed the option letters alone. The base worktree is removed afterwards.tests-that-bite.md, run on fix(cmd/morphic)!: report a stop only for a run it stopped #1136's fix, reverts its code alone in a detached worktree, keeping its tests and fixtures, and sixcmd/morphictests go red.mainrather than read: whereharness.Checkstops, how the canary classifies a child's crash and quiets the collector, how theTypeDefkind lists find members, whatOnVerifywraps, howcostAroundaverages, what the shape pin prints, and that no conformance spec holds an alias all hold. One did not: a stale entry is refused inidPositionsandstringListsand passes only inintegerFields, which the IR skill now says, where it had said no table notices one. Each Why changed since the citation reading was checked against its item's work: one credited feat(ir)!: flatten composition with defined order and override #1141 with a full disk emptying a file, where feat(ir)!: flatten composition with defined order and override #1141's full disk left a mutant planted and the emptied file was a probe on chore(skills): add the repository's agent skills #1143, and one cited chore(skills): add the repository's agent skills #1143 without saying what its work showed; both now say it. Two more named no failure at all, for feat!: own I/O in the engine and lower each file of a split spec #1009 and feat(compilers/openapi)!: resolve schema $refs in the compiler #1016, and now say what each showed. irverify: a stale integerFields entry for a deleted field passes #1148 tracks makingintegerFieldsrefuse a stale entry too, and the rule cites it until that lands.go testandgo buildtemplates that named no package failed there with "no Go files", and now name./<pkg>; the version-claim loop reads every open PR rather thangh's default 30; and theapidiffcomparison is the pinnedgo runcommand, which reports feat(ir)!: type the inferred-marker list and verify its form #1142's changes toir, where it had named anapidiffbinary no setup installs.recorded-base's probe reads from the merge base: in a scratch repository, a two-dot diff againstorigin/mainlistedmain's newer file after a correct fold, while the three-dot diff lists the branch's file alone there and the reverted file after a fold onto a movedmain.count-work-not-wall-clockleaves its assertion tomarginal-growth-on-one-path: it had prescribedwork(2n) <= 2*work(n) + c, while the test it cites asserts a ratio of totals and the next rule says such a ratio lets a quadratic term hide behind fixed cost.perf-guards-measure-the-defecthad a cost test time the function under test, andnondeterministic-tests-proven-stablesaid how to build a timing ratio, where the measuring skill pins a cost by counting work and no test onmaintimes a ratio; a benchmark now times and a cost test counts, and the timing sentence points at the counting rule. The tracker searches the skills quote pass--limit 1000: outside a terminalghlists 30 with no sign of the cut, while a search forUnmodeledmatches 131 issues.main(both planted defects inbarrier's cut fail a test, a file referenced as./a.yamland./x/../a.yamlis one source, and load refuses an alias cycle asopenapi/cyclic-ref).acyclic-treesnow cites the parser-library entry it rests on,environment-independent-testscovers every platform branch, two hedges became statements, and a new rule,fix-at-the-invariants-owner, records openapi: a $ref back into a schema still being lowered crashes the compiler #749's lesson.verifying-changesand the OpenAPI skill now open with the bar their work is held to. No rule puts a count beside the list it counts, asderive-never-maintainasks: four did ("the other two tables", "all three sizes", "all four documents", "two suffixes"), and a scan of every number in the skills' prose outside the Why lines found no other..claude/skills,CLAUDE.md, a brief description and a golden generated with-update, does not match its own text, and finds nothing in this PR's commit messages, added lines, title or body. Run as the shipping skill gives it under bash and zsh on this branch, the checklist prints only the lint's clean line. Its subject check reads the title too, with the number the squash appends, where the checklist had measured the title's length alone: a prose title, one of 65 characters before the number and one ending in a period each print, and one of 64 does not. It passes--no-merges, since a first version flagged the branch's merges ofmain, whose subjects no form fits.gh pr diffloop for version claims (it lists feat(ir)!: scope servers to operations by ID, home wire facts #1144 among the five open PRs, and names a PR whose diff did not load, tried with a number no PR has), and theapidiffcommands on fix(compilers/compile)!: give a shared primitive no source #551's commit (NewTypes: changed from func(int) *Types to func() *Types), on fix: close the losslessness gaps and the guards that missed them #185's, which movedCanonicalWordsintoir(CanonicalWords: removed), and on refactor(compilers/openapi): extract the lowering context #218's, which retyped the grouped constantsGroupByPathPrefixandGroupByTagsincompilers/openapionto an internal type, each reported. After the cut the lint is clean and each kept script's suite passes.sweep.pywithgitmissing and the lint with an unexpected error, and dropping either guard turns its suite red.load_treeinskills_lint.py) each turn a suite red, as do four planted in the lint's 3.14 checks (the script block, the standard library, the rule bounds and the parse target) and four in its first-line and mode checks (either check removed, the first line matched anywhere in the file, and the shebang namingpython3); a script made not executable on the real tree is reported. Both scripts were run on the final tree fromir/, both aspython3.14 <script>and through their own first line; the runs above that predate the 3.14 requirement were made under 3.9.trace, and every decision path it left unexecuted gained a test: inskills_lint.py, a skill with noSKILL.md, a frontmatter that never opens or never closes, the skips for URL links and__pycache__, the wiring that hands the real tree to the checks, a tree declaring no test, each exit status and an unexpected error; insweep.py, an exit-code difference, flags after--reaching each compile, the warning for a base the branch lacks, the run that builds both sides and removes the base worktree, and each exit 2. Re-planted on the final tree in a throwaway worktree, with each suite's own output read, thirty-five planted defects, twenty-four inskills_lint.pyand eleven insweep.py, each turn a suite red, and a comment-only control in each passes. That control first failed in the sweep's suite: a test expected spec paths relative to its temporary directory, which under macOS's default/tmp, a symlink, they are not, and it now resolves that directory as the sweep resolves each spec. Fifteen more planted on the final tree each turn a suite red, seven inskills_lint.pyand eight insweep.py, among them the eight that the first two bullets name as having passed; two survive that are equivalent: a relative import, which fails in any script run as__main__, and-j, which changes no output. Whattracestill lists as unexecuted runs only in the suites' subprocesses.ghcommandsCLAUDE.mdquotes were run with and without a limit.gh label listread 30 of the 34 labels and left outpriority:P1topriority:P4, which every issue carries;gh issue list --state open --json titleread 30 of 250 open issues; and the decision search, which read open issues only, found 2 of the 16 issues forir.Naming, without compilers/openapi: the webhooks group is named as if the source declared it #184, the closed issue its own example cites. With--limit 1000, and--state allfor the decision search, each reads every result, compilers/openapi: the webhooks group is named as if the source declared it #184 included.make gatepassed on the first commit; no step reads the new files. The branch's later changes touch only.claude/skills,CLAUDE.md, the status line ofdocs/micro-compiler-plan.mdand the comment on the.clauderule in.gitignore; of those,internal/archtestreads the two documents, andinternal/keydocandTestLoweringSpecthe citations in them, and each passes on the final head.mainis merged in, and the lint and the artifact grep are clean on the merged tree.