agentleFS
ThalisuThalisu/SKILLS/skills/testing-policy/SKILL.md

testing-policy

Skill1 starsChanged 43 days agoMITView on GitHub

Install, migrate or refresh the canonical Testing Policy (Definition of Done) in the current project: the marked CLAUDE.md section, the generated reference file that carries the full rules, the unit-test-author and e2e-test-author agents, the inline test-author skill, the duplication scan and an optional hook that blocks new skip markers in test files.

  • Reads credentials
  • Installs packages

1 other project uses a version of this.

Adopted by

Most recently taken up by

  • Other versions1 project uses a version of this.

    Word for word · 0

    None found.

    Edited · 0

    None found.

    Wrote their own version · 1

    1. precise-alloy/ritus★ 1same name

    How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.

  • In this repository15 other files it gives its agents.

The file

We split this skill into the rules it gives an agent

6 rules. About 0 are common, 2 are distinctive, and 4 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 4

    Very specific

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

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

    Run bash <skill-dir>/scripts/verify-policy.sh <project>.

    line 30
  2. Very specific:

    Read package.json scripts (or Makefile / justfile / pyproject) for: unit full-suite and single-file commands, mandatory flags and preload …

    line 52
  3. Distinctive:

    Keep the counts that decided it: the option's description in step 3 quotes them.

    line 78
  4. Very specific:

    install: after the section documenting test commands when one exists, before conventions.

    line 106
  5. Distinctive:

    Run every command written into Project facts and the Project maps once more.

    line 149
  6. Very specific:

    Must hold after any mode. Lines marked ✓ are checked mechanically by verify-policy.sh.

    line 155

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill 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
SKILL.md

Install, migrate or refresh the canonical Testing Policy in this project.

Sources and what they become

Source (next to this file)Installed as
POLICY.md, rendered by scripts/render-policy.sh <surface>the marked ## Testing Policy (Definition of Done) section in CLAUDE.md: the short core and Project facts
POLICY.md, rendered by scripts/render-policy.sh <surface> --reference.claude/testing-policy/policy.md: the reference, the full rules under the section titles the core names
AGENT-UNIT.md, rendered by scripts/render-agent.sh unit.claude/agents/unit-test-author.md
AGENT-E2E.md, rendered by scripts/render-agent.sh e2e.claude/agents/e2e-test-author.md: native and mixed surfaces only
SKILL-TEST-AUTHOR.md, rendered by scripts/render-agent.sh test-author.claude/skills/test-author/SKILL.md: the inline entry point for writers without the Agent tool
scripts/scan-test-assets.sh.claude/testing-policy/scan-test-assets.sh: discovery and duplication scan
scripts/skip-patterns.sh.claude/testing-policy/skip-patterns.sh: the skip markers and test-file classes shared by the scan and the hook; project additions live in skip-patterns.local.sh next to it
scripts/forbid-test-skips.sh.claude/testing-policy/forbid-test-skips.sh + a PreToolUse entry in .claude/settings.json; optional, offered in step 6
scripts/verify-policy.shnot installed; run it from here in steps 0 and 8
(no source, created on demand).claude/testing-policy/capture/: the home for anything captured from this project, committed with it like every other installed file

Generated vs preserved. Everything rendered from a template or copied from scripts/ is generated: overwritten on refresh, never hand-edited. That includes the reference, .claude/testing-policy/policy.md: it has no slots and holds no project fact, so nothing in it is the project's to keep, and a rule the project wants to add goes to Project facts. Preserved on refresh: Project facts, the agents' frontmatter (the author tier included) and Project map, skip-patterns.local.sh, .claude/settings.json. One addition is allowed in a preserved part: a line the template gained since the installed version (verify-policy.sh names it in agent_*_map_missing= or policy_facts_missing=; 2.2 added System boundaries and 2.4 added Partial test data to the unit map, and 2.5 added Post-feature gate to Project facts) is appended at its template position, System boundaries filled from this run's discovery and Partial test data from the outcome of step 5's offer, since discovery reads eligibility for that one and never a value, and Post-feature gate from the step-3 answer, since it is a choice no file in the project records. Otherwise existing lines stay verbatim, with two exceptions. The first: a Partial test data line the map already carries is rewritten when the offer was accepted this run and the add succeeded, the one case in step 5 where a value the project wrote moves. The second is a rename, on a unit surface only: 3.0 reworded two Project-facts lines for the single agent that surface installs, so the label **Why the agents run in place** becomes **Why the agent runs in place** where it stands, its filled text kept, and "the agents' Project map" on the Duplication scan line becomes "the agent's Project map". verify-policy.sh names the new label in policy_facts_missing= because it cannot tell a rename from a gain, and appending it would leave two labels for one fact. The report names the appended line and the renamed one. The template version lives once, in POLICY.md; every rendered file carries it (v= on the section's start marker and on the reference's reference-start marker, <!-- testing-policy:agent v=… --> / <!-- testing-policy:skill v=… --> after an agent's or the skill's frontmatter).

Three invariants. Every {{slot}} is filled from what exists in the project; never invent a path, a command or an example. Every command written into Project facts or a Project map was run once during this install and returned output. And nothing captured from a project is written outside it. Inside it, it is written to be shared: material that quotes real paths, service names, boundaries or debt goes to the agents' Project map, to Project facts, or to .claude/testing-policy/capture/, and is committed there, so a teammate reads it from git instead of re-running this install on their own machine. A Project map is scoped to the repo the install ran in; it never becomes a shared artifact of this skill, whose directory is common to every project and is public. That is the one absolute: this skill's own directory holds nothing captured from any project, committed or not.

Step 0: detect state

Run bash <skill-dir>/scripts/verify-policy.sh <project>. policy= picks the mode:

  • none → install
  • legacy (a Testing Policy section without markers; legacy_lines= gives its extent, EOF when it closes the file) → migrate
  • stale (marked, older version) or drifted (core hand-edited) → refresh
  • current → nothing to do unless another line is not ok / n/a / none or the user asked to re-map: a stale or drifted agent is refreshed alone (step 5), a wired-missing hook re-copied (step 6), a missing or drifted policy_reference rewritten alone from the render with the section untouched (step 4, item 3), and a label named in policy_facts_missing appended alone at its template position with the core untouched (step 4; the unit label **Why the agent runs in place** is renamed in place instead), Post-feature gate with the answer step 3 asks for as it does on a refresh. Otherwise say so and stop.

Keep the rest of the output; steps 2-7 use it: policy_reference (printed only when a marked section exists: missing · drifted = any byte differing from render-policy.sh <surface> --reference · ok; written in step 4, and missing is the expected reading on a section installed before 3.0), agent_unit / agent_e2e (missing · unmarked = hand-written, ask · stale = older template, v2 marker style included · drifted = current version, core hand-edited · ok), agent_unit_map_missing / agent_e2e_map_missing (Project-map lines the template gained, appended in step 5), agent_unit_tier / agent_e2e_tier (missing = no model: line, the question never reached it · invalid = a value the harness does not accept · ok; asked in step 3, written in step 5), policy_facts_missing (Project-facts lines the template gained, appended in step 4, Post-feature gate asked in step 3), skill_test_author (missing · stale · ok), scan_script, skip_patterns, hook (missing · script-only · wired · wired-missing = wired in settings but the script is gone), gitignored, capture_legacy, partial_data_helper (n/a = not TypeScript · absent · installed, read in step 2).

Step 1: the surface, native, consumer, mixed or unit

Infer before asking. Native evidence: a UI tree (app/, pages/, screens/, src/app/) and an E2E tool with flows in the repo (playwright.config.*, .maestro/, cypress/, e2e*/). Consumer evidence: an HTTP/queue/library entry point, no UI tree, no E2E tool, CLAUDE.md or docs naming the repos that call it. Mixed: both. Unit evidence: no user-facing surface of its own and no consumer repo whose E2E covers it, or the user saying the repo has no E2E tier; its unit suite is the whole proof.

  • native → no question here.
  • unit → no question here; step 2 skips every E2E read and step 5 installs no e2e-test-author.
  • consumer or mixed → one AskUserQuestion (multiSelect) for the consumer repos: candidates from CLAUDE.md, docs, sibling directories and an existing Project facts consumer list, plus Other. When the inference itself is not conclusive, confirm the surface in the same call.

For each consumer, read its CLAUDE.md and scripts for: E2E tool · single-flow command · full-suite command · flow naming with 2-3 real file names · how its stack runs THIS repo from the local working tree · its known infra failures.

Step 2: discover

  1. Copy scripts/scan-test-assets.sh and scripts/skip-patterns.sh to .claude/testing-policy/ (overwrite; both are generated) and run the scan with this project's roots: --root for unit roots, --flows for E2E roots (native/mixed), --shared for the shared homes you already recognise. Keep the whole report: candidate-homes seeds the role map; duplicate-symbols, local-factories, inline-helpers, type-assertions and skip-markers are the debt list for step 8.
  2. Read package.json scripts (or Makefile / justfile / pyproject) for: unit full-suite and single-file commands, mandatory flags and preload files, formatter, E2E single-flow and full-suite commands, remote-runner conventions.
  3. Build the role map. Unit: mocks · helpers · factories · fixtures; E2E (native/mixed): page objects or shared subflows · data factories · backend/DB helpers · fixtures. Exactly one candidate → record it. Two or more, or none → collect for step 3; never guess, never list both. One directory may hold two roles (a data/ with factories and DB helpers): that is one path per role, not an ambiguity.
  4. Unit boundaries. The --section mock-targets report lists every module-mock target, package or internal, with its file count. A boundary is what the tests replace that lives outside the repo: the package targets, plus whatever the mocks home stands in for (read its files: the exported setup* / mock* / with* functions and the service, client or clock each replaces). Each becomes one System boundaries line, what → shared mock. An internal target is one of two things, and the module's own imports tell which: a module that talks to the outside, one that imports a driver, SDK or client package (ioredis, amqplib, jose, nodemailer, @supabase/supabase-js) or calls fetch, the filesystem, the clock or process.env directly, is a thin wrapper around a boundary and is listed by what it wraps; a module that only imports this repo's other modules is an internal collaborator, debt for step 8, never a boundary. A boundary mocked inline per test file with no shared mock is listed as none yet → create at <mocks home>/<name> and counted as debt too. Only a read that stays inconclusive rides the step-3 question. No mocks at all → none: pure modules.
  5. Record the idiom: identifier language in the test tree (it may legitimately differ from the code policy), mocking convention, locator strategy, base classes, and 2-3 real well-shaped shared assets with their signatures, as calibration, never a catalog.

    Calibration is scoped, never global. On refresh or migrate, the reference is this project's own installed .claude/agents/*-test-author.md: already filled, already about this repo. On a first install there is nothing to copy from, so the target is stated here instead of shipped as an example: one line per role, each naming a path that exists; boundaries written what → shared mock; 2-3 signatures rather than a catalog; counts wherever they were measured (<n> of <total> files, <n> uses vs <m>), never an adjective; a role with no home yet written none yet → <path> and carried into the debt list. A map captured from another repo is never the model: no example map ships with this skill, and none may be kept in its directory.

  6. Preflight (native/mixed): app health endpoint, required services; include anything whose absence has produced a false product-bug red before (CLAUDE.md, docs, memory).
  7. The partial data helper (unit, TypeScript only). Two facts already read decide eligibility, and both must hold: partial_data_helper from step 0 is not n/a, which is the TypeScript configuration, and the scan's layout section names at least one TypeScript test-file suffix under the roots, which is the test file. They are the whole gate: whether type-assertions found anything does not gate it, since the offer is about what the next test is written with, not about what the tree already carries. An eligible project reading absent carries the offer into step 3; one reading installed already has the helper and is not offered it. A decline leaves nothing behind: the next run reads absent again and offers again, the way the hook offer is offered again after a no. The offer says what a yes does before the answer is taken: it fetches third-party code from the public npm registry into the project, @total-typescript/shoehorn as a dev dependency of the workspace below, which every later test run then loads.

    Record for step 5 the workspace that holds the tests, the directory of the nearest package.json at or above the unit test root, and the manager that workspace implies. Every value the add takes from the project is untrusted input and none of it reaches a command line: the packageManager field, the lockfile it reads, the workspace path and the package-manager configuration that workspace carries. The manager is a name that selects one of four fixed commands and is never itself a command line: it comes from the packageManager field when present, read as its leading token up to the @ and nothing else, else from the lockfile beside the workspace or at the repo root. The four commands are written out once, here, and step 5 runs one of them whole or runs nothing:

    • pnpm-lock.yaml → pnpm add -D @total-typescript/shoehorn --ignore-scripts --ignore-pnpmfile --registry https://registry.npmjs.org/
    • yarn.lock → yarn add -D @total-typescript/shoehorn --ignore-scripts --registry https://registry.npmjs.org/
    • package-lock.json → npm install -D @total-typescript/shoehorn --ignore-scripts --registry https://registry.npmjs.org/
    • bun.lockb or bun.lock → bun add -d @total-typescript/shoehorn --ignore-scripts --registry https://registry.npmjs.org/

    The flags are the gate, not decoration: the add is the one step of this install that pulls code from outside the repo, and a clone gets to configure how its package manager behaves. --ignore-scripts keeps the fetched package's own install hooks from running as the developer, and --ignore-pnpmfile keeps pnpm from loading the .pnpmfile.cjs a repository may carry, which runs even with scripts off. The pinned --registry is the one host the add talks to and the only one its credentials go to, so a .npmrc a clone wrote can neither name that host nor expand a variable of the developer's environment into a token for it.

    A flag closes the key it names, and such a file carries keys no flag on the line covers, a proxy and a CA among them, so the file itself is refused rather than argued with: when the workspace, or a directory between it and the repo root, carries .npmrc, .yarnrc, .yarnrc.yml, .pnpmfile.cjs or bunfig.toml, the add is not run at all and step 5 records it as debt like any other add that could not run. Whatever the add does resolve against, the registry and the credentials are the developer's own, never the project's.

    A packageManager whose leading token is not pnpm, yarn, npm or bun, neither source answering, or two workspaces holding tests, is the ambiguous case step 5 records as debt; never guess a manager or a workspace, and never run a string a project file supplied.

  8. The author tier, for each author the install writes (unit; e2e on native and mixed) whose tier step 3 will ask: install, migrate, and an agent reading agent_*_tier=missing or invalid. The Recommended pair is derived from counts step 2 already measured, never from a new read. The author runs the reuse audit, decides what earns an assertion, and writes the diagnosis a handback routes on, and a wrong test is paid twice under red-first, so the Recommended model is opus and the volume of dispatches is paid down through the effort (ADR 0052):

    • unit: opus · medium; opus · high when the scan's duplicate-symbols or local-factories report debt, or the shared homes hold more than a handful of assets, since that is where the reuse audit is hard. The cheaper options step 3 lists beside it are fable · medium, offered wherever a smaller model at a higher effort would otherwise run, and held at medium because at low it calls search tools less often and the reuse audit is a search (ADR 0058), and sonnet · high.
    • e2e: opus · high on every project. When the preflight in item 6 names stateful services (a database, auth, seeds) the flows depend on, the option's description quotes them as the one reason a user may answer opus xhigh in free text, and says that step-up is unmeasured: xhigh is reserved for work where a quality gain was measured, and the model thinks noticeably more per turn there (ADR 0059).

    Keep the counts that decided it: the option's description in step 3 quotes them.

On refresh, run the same discovery and diff it against the written Project facts / Project map: disagreements are reported, the written text stays untouched. The user decides.

Step 3: ask, once

One AskUserQuestion call, holding the pending questions in this order:

  1. The roles: one question per ambiguous or orphan role from step 2, with the real candidates found; for an orphan, a path following the project's own naming as the Recommended option, plus Other.
  2. The mock targets: one multiSelect over the internal mock targets from step 2 whose read stayed inconclusive, asking which of them wrap a boundary; the rest are debt.
  3. The legacy rules, in migrate mode only: one question per project-specific rule found in the legacy section that has no home in the template, with two options, keep it in Project facts, or drop it.
  4. The post-feature gate, the tier that runs once a feature's own tests are green. Pending on install and migrate, and on a refresh only when policy_facts_missing names Post-feature gate. Four options, the full unit suite, the full E2E suite, both, or none; two on a unit surface, the full unit suite or none. Recommended: a refresh from before 2.5 marks both, the closest to the full unit suite per change and full E2E suite per phase that version stated; a migrate marks the one the legacy section states, if any; an install marks none, since the cost of a full suite is the project's trade-off alone.
  5. The author tier from step 2, one question per author whose tier step 2 item 8 marked for asking: install, migrate, and an agent reading agent_*_tier=missing or invalid. Four options, plus the free-text Other the tool adds:

    • the Recommended pair, its description quoting the counts that decided it; an agent reading invalid marks its installed pair's correction as Recommended and names the value refused;
    • fable · medium, described as often competitive with Opus and Sonnet on cost per task while scoring higher, and held at medium because at low it searches less and the reuse audit is a search;
    • sonnet · high, described as the cheapest pair, at half the Opus price per token, and unmeasured on the reuse audit and the handback diagnosis, the two jobs a wrong test is paid twice for;
    • inherit, the session's model and effort.

    A free-text answer is read as <model> <effort> or <model> alone, and is taken only when render-agent.sh accepts it (the model one of sonnet, opus, haiku, fable, inherit; the effort one of low, medium, high, xhigh, max); the renderer's refusal is quoted and that one question asked again.

  6. The partial data helper offer from step 2, for an eligible project only.
  7. The hook offer from step 6, pending in the states that step offers it in, hook=missing and script-only.

The tool takes at most four questions per call, so the pending questions go in the order above, four per call: the first four ride in this call, the next four in a second call right after it, and so on, each question asked once. Both offers take their place in that order like any other question. Roles that mapped cleanly are never asked, and a project step 2 found ineligible never sees the offer and never sees the package name, so a Python or Maestro project is never asked about a TypeScript package. Skip the call entirely when nothing is pending.

Step 4: render and place the policy

  1. bash scripts/render-policy.sh <surface> and fill the {{slots}} in Project facts only; the core has none, and neither has the reference of item 3. SCAN_COMMAND is the exact invocation from step 2. WHY_IN_PLACE states the project's real reason (the E2E stack serves the primary checkout; the unit runner's path-ignore for agent worktrees; ...). POST_FEATURE_GATE is the step-3 answer as given, with the full-suite command of each suite it names.
  2. Place it:
    • install: after the section documenting test commands when one exists, before conventions. Never inside a block another skill or generator owns (a marked <!-- <name>:*-start --> ... <!-- <name>:*-end --> region, or similar); put it between blocks, so regeneration cannot overwrite it.
    • migrate: replace the legacy section (legacy_lines= from step 0) in place, same position. Slot values come from the legacy text where it named them (commands, flow examples, infra failures); the project-specific rules the user kept go into Project facts.
    • surface change (the user asks for another surface, or discovery disagrees with surface= and the user confirms): render the new surface, replace the core, set surface= on the start marker, rewrite the reference for the new surface (item 3; its marker carries surface= too and its sections differ per surface), and drop the Project-facts lines the new surface does not render; every other facts line stays verbatim, and a Post-feature gate naming a suite the new surface lacks is asked again in step 3. The two unit wordings in the refresh bullet below follow the surface: a change into unit applies them, a change out of it reverses them, each in place with the filled text kept. An e2e-test-author left behind on a unit or consumer surface is deleted with the user's yes.
    • refresh: replace only core-start .. core-end with render-policy.sh <surface> --core-only, and set v= on the start marker to the template version. Project facts are preserved verbatim, except a label named in policy_facts_missing, which is appended at its template position: Post-feature gate takes the step-3 answer. Then rewrite the reference (item 3). From 2.10 that is the whole path to 3.0: the core between the markers becomes the short one, the start marker reads v=3.0, the reference is written for the first time (policy_reference=missing beside policy=stale is the expected reading before it), and Project facts stay verbatim, since the full rules that left the section are in the reference under the same titles and no project value was among them. A unit surface takes two more edits, both in Project facts and both in place: rename the label **Why the agents run in place** to **Why the agent runs in place**, keeping the text the project filled after it, and change "the agents' Project map" to "the agent's Project map" on the Duplication scan line. policy_facts_missing names the new label there; it is this rename, never a line to append.
  3. Write the reference, in every mode: bash scripts/render-policy.sh <surface> --reference > <project>/.claude/testing-policy/policy.md, the whole output verbatim, the reference-start and reference-end markers included, overwriting the file when one is there. Nothing is filled in and nothing is edited afterwards: the reference is generated and slot-free, verify-policy.sh compares it byte for byte with that render, and every refresh overwrites it. The same write is the whole repair when step 0 read policy=current with policy_reference=missing or drifted: rewrite the file from the render, never patch it by hand, and leave the section alone. On drifted, say in the report that the file was rewritten, since a hand edit in it is lost; a project-specific rule belongs in Project facts.

Step 5: agents and the inline skill

  • unit-test-author always; e2e-test-author on native and mixed; the test-author skill always (TEST_AUTHOR_E2E_LINE names the consumer repos on a consumer surface, and says the repo has no E2E flows on a unit surface).
  • Render with bash scripts/render-agent.sh <unit|e2e|test-author>, an agent with its step-3 tier: --model <m> --effort <e>, or --model inherit alone, which writes model: inherit and no effort: line, since omitting it is how an agent inherits the session's effort. What happens next follows the state from step 0:
    • missing → write the whole render, Project map slots filled from the role map. The Discovery block is the step-2 scan invocation (--section duplicate-symbols, plus --section local-factories for unit / --section inline-helpers for E2E) and 1-2 targeted greps over the shared homes, every line run once.
    • stale or drifted → replace only the core: from the line that starts with <!-- testing-policy:core-start (in the v2 style that comment spans two lines; replace through its -->) up to <!-- testing-policy:core-end -->, with render-agent.sh <unit|e2e> --core-only; add <!-- testing-policy:agent v=<version> --> right after the frontmatter when absent. Frontmatter and Project map stay verbatim, except a line named in agent_*_map_missing (for 2.2, System boundaries in the unit map; for 2.4, Partial test data in the unit map), which is appended at its template position with every other line left as the project wrote it. System boundaries is filled from step 2, and Partial test data from the outcome of the offer below, since step 2 reads eligibility for it and never a value. A map that already carries Partial test data is never named there, so nothing is appended to it: that line is the one exception below, and every other installed line stays verbatim. Report discovery disagreements (step 2).
    • An agent whose frontmatter lacks the tier or carries an invalid one (agent_*_tier=missing / invalid), in any state that keeps the frontmatter, gets its model: and effort: lines written from the step-3 answer, replacing any it had, with every other frontmatter line left as the project wrote it. On ok the tier is the project's and is never asked or rewritten, unless the user asks to change it.
    • unmarked (hand-written, no markers) → ask before touching it; never overwrite silently.
    • ok → nothing.
    • The test-author skill has no preserved part: missing or stale → write the whole render with its one slot filled.
  • The unit map's Partial test data line is written here, because its value is the result of the add. On missing, and on a stale or drifted agent whose map lacks the label and has it appended, the line is written from the outcome below, whatever that outcome is. On a stale or drifted agent whose map already carries the label, the line is rewritten only when the offer was accepted this run and the add succeeded, and otherwise stays exactly as the project wrote it on every run that added nothing, so a refresh never overwrites a value the project set. On a yes at step 3, print the exact command line first, then run the add once from the workspace step 2 recorded: the whole fixed command that workspace's manager selected in step 2, its flags included, and nothing else. Nothing is appended to that line and no flag is dropped from it, since the flags are what keeps the add from running code the repository chose; the line is the fixed command a manager's name selected rather than any string a project file spelled. On success write the line as fromPartial() for partial data that still type checks and fromAny() for data that is wrong on purpose, from that package. Otherwise write none yet → <that command>, a pointer and not a command that ran: on a no, on an eligible project that was never offered, and on an add that failed or could not run at all (no network, the manager or the workspace unanswered, two workspaces holding tests, a workspace carrying package-manager configuration of its own). A failed add never stops the install; it is one more line of debt, and step 8 reports it with its reason. A project reading partial_data_helper=n/a at step 0 gets n/a on the line, so the label is filled in every project and in every mode.

Step 6: enforcement hook

forbid-test-skips.sh is a PreToolUse hook on Edit|Write|MultiEdit that blocks an edit introducing a skip marker into a test file; edits that keep or remove existing markers pass. Markers and test-file classes come from skip-patterns.sh (step 2); built-in coverage: JS/TS (jest, vitest, bun, mocha, jasmine, node:test, Playwright, Cypress: .skip/.only/.todo/.fixme/.fail/.fails/.failing/.skipIf/.runIf, xit/fit and friends), Python (pytest.mark.skip|skipif|xfail, pytest.skip(), unittest.skip*/expectedFailure), Maestro (optional: true). It needs jq (command -v jq; if absent, say so and skip the offer). It sees only Claude's edits; the user's editor is untouched.

  • hook=missing → offer it (the question rides in step 3). On yes: copy the script to .claude/testing-policy/, then merge this entry into .claude/settings.json → hooks.PreToolUse (append to the existing array; never overwrite other hooks; create the file if absent):

    {"matcher": "Edit|Write|MultiEdit", "hooks": [{"type": "command", "command": "bash \"$CLAUDE_PROJECT_DIR\"/.claude/testing-policy/forbid-test-skips.sh"}]}
  • script-only → offer only the settings entry. wired → no offer; re-copy the script (generated) so it matches the installed skip-patterns.sh. wired-missing → re-copy the script and say so.
  • When Project facts name a skip mechanism or a test-file pattern the built-ins do not cover, write it into .claude/testing-policy/skip-patterns.local.sh (preserved; sourced last): append to a SKIP_PAT_* variable, or define skip_pattern_local <path> setting SKIP_PAT / SKIP_KIND for the extra files. Prove it once: pipe a synthetic Edit payload that introduces the marker through the hook and confirm exit 2.

Step 7: git visibility

One rule in the project, one rule outside it, and verify-policy.sh reports the first.

Everything this install writes into the project is tracked, the capture folder included. gitignored= lists installed paths git is ignoring, and every one of them is a defect. Two reasons, and they point the same way: agent worktrees, CI, remote runners and the PreToolUse hook only see tracked files, so an ignored agent, test-author skill, policy.md (the reference the core sends every reader to), scan-test-assets.sh, skip-patterns.sh, forbid-test-skips.sh or settings.json is a broken install; and everything this install learned about the repo (the Project maps, Project facts, and whatever sits in .claude/testing-policy/capture/: a kept scan report, a filled example map, a note quoting real paths, boundaries or debt) is knowledge the team reads from git, not something each teammate re-derives by running the install on their own machine. Offer the fix; apply it only with the user's yes. Gotcha: git cannot re-include a path under an excluded parent, so a .claude or .claude/ line must become .claude/* before !.claude/agents/, !.claude/skills/, !.claude/testing-policy/ and !.claude/settings.json take effect. Show the resulting .gitignore lines.

capture_legacy= means an earlier install left a gitignored .claude/testing-policy/local/ behind. Offer to move its files into capture/ (plain mv, since an ignored file is untracked and git mv would fail on it), drop the .gitignore line that hid them, then git add and commit: the material is about this repo and belongs to whoever clones it. Never delete those files; the user decides.

Outside the project, the rule is absolute: this skill's own directory holds no capture at all, not a map, not a scan report, not a note, committed or gitignored. That directory is shared by every project that installs the skill and is public, so anything project-derived in it is global by construction, which is exactly what a Project map must not be. A capture worth keeping goes to its own project and is committed there; one found in the skill directory is deleted, not gitignored.

Step 8: verify and report

  1. Run every command written into Project facts and the Project maps once more; a command that errors or returns nothing is a wrong slot; fix it before finishing.
  2. bash scripts/verify-policy.sh <project> must print policy=current, policy_reference=ok, every agent_* ok (or n/a), every agent_*_tier ok, no agent_*_map_missing, skill_test_author=ok, scan_script=ok, skip_patterns=ok, no policy_missing, no policy_facts_missing, no policy_unfilled_slots, and exit 0 (hook=missing is fine when the offer was declined, and partial_data_helper=absent is fine when the offer was declined or the add failed: the key is report only and never moves the exit code).
  3. Report: the state transition (legacy → current v<template version>, ...); a diff-level summary per file, the reference named as written or rewritten; Project-map disagreements, appended lines and the renamed unit label (refresh); the duplication, skip-marker, type-assertion and internal-mock debt from the scan, which is reported and not fixed: the second-use rule pays the duplication organically (the next author that needs one of those assets consolidates first), an internal mock is replaced by a seam the next time its test is touched, and a type assertion is replaced by the helper the unit map names the next time its test file is touched, so the install rewrites no test file; the partial data helper: added with the command that ran, already present, declined, or not added with the reason the add failed; the tier each author runs on, and whether this run asked it or kept the installed one; hook installed, re-copied or declined; gitignore status. Do not commit unless asked.

Post-install checklist

Must hold after any mode. Lines marked ✓ are checked mechanically by verify-policy.sh; check the rest by reading.

  • ✓ testing-policy:start v=<template version> surface=<surface> and testing-policy:end around the section; core-start / core-end inside; the core byte-identical to render-policy.sh <surface> --core-only.
  • ✓ .claude/testing-policy/policy.md byte-identical to render-policy.sh <surface> --reference, its reference-start marker carrying the same v= and surface= as the section.
  • ✓ Every heading that render-policy.sh <surface> emits is present in the section, as a full line.
  • ✓ Every Project-facts label that render-policy.sh <surface> emits is present in the section.
  • ✓ No {{slot}} left in the section.
  • ✓ Agents: <!-- testing-policy:agent v=<template version> --> after the frontmatter, bare core-start / core-end markers, core byte-identical to render-agent.sh <unit|e2e> --core-only; every Project-map label of the template present; test-author skill carries <!-- testing-policy:skill v=<template version> -->; scan-test-assets.sh and skip-patterns.sh present.
  • ✓ Each installed author's frontmatter carries a model: line the harness accepts, and an effort: line, when present, with a value the harness accepts.
  • ✓ Nothing this install wrote into the project is gitignored (gitignored=none), the capture folder included; the team gets the maps and captures from git.
  • Nothing captured from this project was written outside it, this skill's own directory first of all.
  • The Project map is scoped to this repo: every path, command and signature in it was found here, and none was carried over from an example or another project.
  • Project facts name real commands that ran; consumer lines (consumer/mixed) carry the "run against this repo's local build" recipe.
  • Post-feature gate in Project facts reads one of the four answers the project gave: the full unit suite, the full E2E suite, both, or none.
  • Partial test data in the unit map carries the helper and its two functions when the project has the package, n/a when it is not TypeScript, and none yet → <the command that would add it> otherwise; partial_data_helper= in the verify output agrees with it, and a disagreement is a project that dropped the package after adopting it.
  • System boundaries in the unit map names only things outside the repo (packages, services, the clock, the filesystem) or the repo's thin wrappers around them, each with its shared mock; internal collaborators found mocked are in the report as debt, never in the map.
  • Legacy project-specific rules the user kept survived into Project facts; nothing of the old section remains outside the markers.
  • skip-patterns.local.sh exists exactly when Project facts name a mechanism outside the built-ins, and a synthetic payload proved it blocks.

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.