The seventeen repairs from the seven-seat review of 0.4.15 (spec: docs/specs/2026-09-03-release-0.5.0.md, issue #58). Subsections land per slice below.
The Action says why the base run was not made, and gate: "true" fails a pull request that judged nothing
On actions/checkout's default depth-1 clone the Action's base step made no run, verify
judged the checkout against its own run (an empty diff), the comment said verify passed,
and gate: "true" exited 0 on a pull request that exits 6 at full depth. The base step now
writes the reason to crapkit-base.reason on every failure path: shallow clone does not hold the fork point of <sha>; set fetch-depth: 0 on the checkout, no usable crapkit.toml at the fork point <sha>: ..., or lane failed at the fork point <sha>: ... with the lane's
first error line. The comment renders **verify judged no changed function:** the base run was not made (<reason>) in place of verify passed, with no base commit as the reason on
a push event and under delta: "false". With gate: "true" the exit step exits 1 when the
base run was attempted on a pull request and not made, printing the reason; a push and
delta: "false" never attempt it and keep verify's own code. The renderer stays git-free:
the sha and the reason reach tools/action/comment.py as --base-sha and --base-reason
files. Moved contracts: the README's gate and delta input rows, and the "What the
verdict line covers" passage that said none of the three failures fails the job.
The Action does not ask verify for a verdict over a failed coverage
The verdict step ran crapkit verify --json --reuse-artifacts whatever crapkit coverage
had exited. On a runner that keeps its workspace between jobs (clean: false), a lane that
stopped writing its artifact was refused by coverage (exit 5) and then verify read the
artifact that lane had left from an earlier run, passed over it, and runs list showed
that run as the trusted baseline. The checkout step now records coverage's exit, the verdict
step reads it first and does not call verify when it is non-zero, and the comment says
**no verdict: `crapkit coverage` exited 5 (lane 'py' failed: <first line of the lane failure>); verify did not run.**, quoting the error object's message when coverage --json
died before a summary, or pointing at the job log when every lane failed and nothing was
printed. gate: "true" then exits with coverage's code. The renderer takes the code as
--coverage-exit.
The pull-request comment names the function and the rule that failed the check
On exit 6 the comment read 1 gate violation, 0 ratchet regressions, ... over a table in
which the pull request's own untested route() and an untouched ratchet-marked
legacy_router() were two identical rows, and on exit 9 the ceiling and the uncovered lines
were only in the job log. The verdict now opens with the rule the exit code stands for,
**verify failed, exit 6: complexity gate.** (7 ratchet regressions, 8 new test failures,
9 diff-coverage ceiling 3, the ceiling read from the receipt's diff_uncovered_max), then
one bullet per finding: - gate: `app/calc.py:34` `route( a , b , c , d )` ccn 8, cov 0%, crap 72.0 -> decompose, - ratchet: `app/calc.py` `legacy_router( ... )` 72.0 -> 80.5 (recorded -> fresh), - new test failure: `tests/test_calc.py::test_route`, and the first
twenty uncovered changed lines as - uncovered lines in `app/calc.py`: 35, 36, ... with one
bullet per file and - and N more uncovered changed lines for the rest. The counts line
closes the block unchanged. In the table, a row whose function the committed ratchet carries
a mark for (the worklist row's ratchet_mark) reads decompose (accepted debt), and the
rows a finding names come first, ahead of the top cap. Moved contract: the README's
rendered comment is now the byte-for-byte render of the payloads under
tests/fixtures/action_comment/ (a failing example), pinned by the unit suite.
The comment's scored line names the ceiling, a failed lane's first line, or the error
The first line of the pull-request comment read 153 over target with no number, while the
scopes carried ceilings 4, 6 and 12, and a coverage --json that died before printing a
summary left the comment with wrote no run summary and the sentence naming the fix in the
job log. The line now reads 2 over ceiling 6 or 2 over their ceilings (6; reports 12, util 4) from the summary's ceilings, appends ; lane 'js' failed: <first line> for each
entry of lane_failures, and, when the payload is the one-object error --json prints on
a crapkit error, reads `crapkit coverage` exited 5: <message>. A 0.4.x payload without
ceilings reads over the ceiling. The verdict line reads the same error object from
verify --json (a missing baseline commit, exit 4) as **`crapkit verify` exited 4 and wrote no verdict: <message>.** instead of counting it as a verdict with no findings.
Moved contract: the README's rendered comment is regenerated with the new first line.
The MCP server survives a bad call
A tools/call with a missing positional, an undeclared key or a wrong type answers a tool result with isError: true in the tool's own words (brief needs name (see inputSchema.required), worklist does not take 'bogus'; accepted: repo, top, scope, top must be an integer (got "three")) before any CLI spawns, and the session continues; on 0.4.15 a missing positional killed the server and every later request read end of file. params: null and arguments: null are refusals, not crashes, and a positional sent as null is a missing positional (brief needs path (see inputSchema.required)), not a spawned CLI's stderr. tools/list declares required from each tool's positionals. ping answers an empty result instead of -32601. An exception escaping the server answers a JSON-RPC -32603 reply and the loop reads on. ADR 0001 records why the refusals are tool results and not the protocol's -32602.
explain and doctor answer JSON over MCP
Both tools shell to their --json form, so all nine tools return one shape and carry structuredContent whenever the CLI exits 0. explain takes history and tests (booleans; true adds commits and tests to each function, the CLI's --history and --tests). A doctor that finds a FAIL exits 1 and answers its JSON text with isError: true and no structuredContent. Moved contracts: the two MCP e2e asserts that read no problems found from the doctor tool now read problems: []; the two "plain text" rows leave the MCP tables in docs/agent-json.md and AGENTS.md, and the agents guide's initialize reports protocol 2024-11-05 line, stale since 0.4.13, names the negotiated revisions.
worklist and next_item take a scope over MCP
Both tools accept scope, an array of declared scope names, one --scope each, so a large repository is partitioned before top applies; the CLI's answer to the flags comes back as the tool's result.
mutate never mutates a test
crapkit mutate placed mutants in every file the diff touched, tests included: on one review run 6 of 9 mutants landed in tests/test_tax.py and the survivor was an assertion. The diff's file list, and the files --files names, now pass through the corpus predicate scoring uses (scopes, excludes, the test-file cut and max_file_bytes) before a mutant is placed. A file outside the corpus is named on stderr as not mutating <path>: outside the scored corpus, --json lists it under outside_corpus, and a diff with nothing left prints mutation: nothing to mutate; outside the scored corpus (scopes, excludes, test files, max_file_bytes): <paths> at exit 0 without starting the suite. A scope declaring paths = ["."] claims nothing in scoring and now claims nothing for mutate either; declare the files or directories by name, as doctor already asks.
A Python row's nesting is a depth
nesting on a Python function is the deepest the cognitive pass's nesting stack gets, one level per if, elif, else, for, while, except and comprehension for, none for with, try, finally, match, case or a nested def: a flat function of seven ifs reads 1 and a three-deep one reads 3. Until now the column read lizard's ND extension, which counts nesting structures for Python rather than depth, so the flat function read 7 and looked seven levels deep next to the same number for a function that was. The same pass now reads which function owns a token after lizard has, so the first token of the line that leaves a Python function is no longer charged to it: an outer function whose blocks follow a nested helper keeps its own cognitive score and depth instead of handing the first of them to the helper, and the last function of a module no longer pays for the if __name__ == "__main__": or the module-level call that follows it (six of the 5,258 rows in crapkit's own tree move, by one point each); ccn does not move. Brace languages keep lizard's column. The analysis version moves to 9, so the first inventory or coverage after upgrading runs the analysis cache cold and re-measures the corpus once; the nesting row of docs/agent-json.md names the source per language and what opens a level. (#64)
verify says why it refused an override
verify --override on a run holding a ratchet regression or a new test failure used to
exit 6 with no line about the override at all: no OVERRIDDEN, no refusal, an empty
crapkit overrides. It now prints one stderr line naming the cause and the escape,
override refused: 1 ratchet regression (app/m.py pick( a ) 240.0 -> 380.0) never qualifies for an override; raise the mark by hand and commit it, both causes on the one line when a
run holds both. The exit code is unchanged and --json stdout stays one object.
docs/ratchet.md states the rule: a mark never rises through verify.
verify says what it did to the marks file, and touches it only when something moved
A green run rewrote crapkit-ratchet.tsv on every pass, so a clean checkout ended with an
untracked marks file holding a stamp, a header and no rows, and a repo with marks got a
dirty file with nothing on the OK line to say why. The file is now written only when its
text would change and never created to hold zero marks. When it is written, the OK line
ends with ratchet: 6 dropped, 1 tightened -> git add crapkit-ratchet.tsv, and the JSON
receipt carries the same counts as ratchet_changes (null when the tighten wrote nothing).
A file written before stamping is rewritten once to gain its stamp line, and the OK line
says ratchet: restamped -> git add crapkit-ratchet.tsv for that rewrite. An override that
applied writes its grant to the same file, so its OK line ends with ratchet: 1 mark granted -> git add crapkit-ratchet.tsv; ratchet_changes stays null, the grant being listed
under overridden.
A shallow clone is named when the baseline commit is missing
verify on a depth-1 checkout said is not an ancestor of HEAD (rebase or amend rewrote history) and sent the reader after a fresh baseline when nothing was rewritten. When
git rev-parse --is-shallow-repository answers true the line now reads baseline commit a74260f321f is not an ancestor of HEAD in this shallow clone, which does not hold it; set fetch-depth: 0 on the checkout or run git fetch --unshallow. Exit 4 and the rewrite message
on a full clone are unchanged; the README transcript shows the new line.
The verify receipt carries the diff-coverage ceiling
verify --json adds diff_uncovered_max, the configured ceiling diff_uncovered_count is
judged against, null when the repo set none. Additive; schema stays 1.
init writes a scoped-test command that collects a test, and doctor repeats its lane probe
A python scope whose own paths hold no test file gets the whole-suite form,
python -m pytest tests -q -p no:cacheprovider, naming the repo's test directory unless
pytest's testpaths already collects it, in which case the positional is omitted;
{files} stays only where the tests live under the scope's paths. Before, every python
scope got {files}, and on the ordinary pkg/ + tests/ layout crapkit test-scoped pkg/x.py
handed pytest a source file to collect from and exited 5. An npm workspace scope with a
test script gets npm run test -w <dir>, written live; a root JavaScript scope gets the
runner's related-tests mode keyed by what package.json names (npx vitest related --run {files}, npx jest --findRelatedTests {files}) instead of a vitest command for every
language, and the placeholder when nothing names a runner; one comment line above each
entry names the form chosen. init also says when two workspaces name a runner and no js
lane was written. doctor re-runs init's first-run note for every coverage.py lane, so a
lane whose python cannot import pytest-cov now fails doctor with the same sentence instead
of the first crapkit coverage; a healthy lane prints
ok lane 'py': python -> <path> (pytest X, pytest-cov Y), with a WARN when that python is
not the one running doctor; a lane an environment manager heads (uv run python -m pytest --cov) prints a note that its interpreter and pytest-cov were not probed, so a lane doctor
did not ask never reads as one it found healthy; and a {files} template on a scope that
holds no test file fails, naming the whole-suite form as the fix.
One exclude glob reaches the repo root and every nested copy
A leading **/ in an [exclude] glob matches zero or more directories, so **/dist/**
excludes a repo-root dist/ as well as web/dist/, and src/distro/ stays in. Under
fnmatch alone the prefix demanded a directory in front, which is why 0.4.12's "init and
doctor agree about the root and the dot-directories" wrote the root form beside every
nested form; that rationale is reversed here and the duplicates are gone. The default set
gains **/generated/**, **/__generated__/** and **/*.generated.*, so a generated
client is never the first next-item, and crapkit init writes the list one glob per
line under a two-line comment instead of a 405-character line. A hand-written root form
such as dist/** still matches the root and nothing below it. A committed config carrying
only **/dist/**, **/conftest.py or **/*.test.* now also excludes the root copy: run
crapkit doctor after upgrading and read the per-scope file counts.
A failed lane's old artifact is refused on reuse
A lane that ran and did not rewrite its artifact was refused by coverage (exit 5) and then
scored by the next coverage --reuse-artifacts and passed by verify --reuse-artifacts,
which wrote the dead lane's old numbers in as the trusted baseline. The failed attempt now
records the modification time of the file it left behind in .crapkit/artifacts.json, and
reuse refuses the file while that time still matches: lane 'py' wrote no artifact on its last attempt — the .crapkit/cov/py.json on disk predates it and is the previous run's, which --reuse-artifacts will not score, exit 5 for coverage and verify cannot conclude with failed lanes for verify. A real run or a rewrite of the file clears it, so a coverage JSON
combined by hand from a killed run's shards still reuses. A lane refused before it ran (the
container guard) records nothing, and --reuse-unchanged reruns a lane whose last attempt
wrote nothing instead of trusting its stamp commit. The 0.4.12 entry's "--reuse-artifacts
is untouched" no longer holds; see it below.
The full-suite guard knows pytest's testpaths
python -m pytest tests --cov=app beside testpaths = ["tests"] collects the whole suite,
and the guard refused it (positional argument 'tests' narrows a full-suite coverage run,
exit 3) from every command that loads the configuration, and the advisory hook stayed silent
in that repo. The loader now takes the repository root, reads testpaths from the file
pytest would pick where the lane runs (pytest.ini and .pytest.ini decide when present,
even empty; pyproject.toml, tox.ini and setup.cfg when they hold a pytest section) and
accepts the positionals when together they name every configured entry. One entry of
several, or a positional the testpaths do not name, is refused as before, and a lane
without a positional reads no file. doctor, coverage, digest, ratchet seed and the
hook all load such a lane.
Every worklist row carries its CRAP score and coverage
The ranking view of a CRAP scorer printed risk, ccn, the standard-only ccn, churn and the
recency weight, and never the score; the HTML report sent its reader to crapkit explain
per row. A row now reads risk 14.0 ccn 14 crap 38.5 cov 50% 1c/1a calc/grade.py:7 classify( ... ): (N std) and w 0.00 leave the text, and --json keeps ccn_std and
weight beside the new crap and cov, both null on an inventory-only run and each
row's own where two functions share a name in one file. The header counts the active rows
against their total, 50 of 3980 active (worklist_top 50), or (--top N) when the flag
set the cap, and --json carries active_total, so a capped list never reads as the whole
repo. The report page renders CRAP and Cov columns and drops the footer sentence that
claimed no payload carried them. The demo recording is re-rendered, and the demo generator
now folds the interpreter path python -m crapkit prints in its next steps back to
crapkit instead of refusing the frame.
An unknown --scope is a configuration error
worklist --scope frontend on a repo whose scopes are api and web printed 0 active, 0 dormant at exit 0, which a CI step reads as a clean pass, and next-item --scope biling
answered empty: true with every reason at 0, the payload an agent reads as a finished
scope. Both now exit 3 with no scope named 'frontend'; declared: api, web before the
store is opened, the same class the loader raises for a lane naming an undeclared scope.
Worklist rows say which functions are accepted debt
Every worklist --json row carries ratchet_mark: the committed mark's value, or null
when the function carries no mark or the repo has no marks file. The mark is read under
the function's own ratchet key, counted over the whole run, so the second of two f( )
in one file reports the mark on f( )#2 and never its twin's.
A one-commit repository ranks by complexity
Every row on a fresh repo read risk 0.0 with weight 0.0 and commits 1, because a
log with one timestamp has no range to weight against and the recency logistic rounded
every commit to nothing. A commit in such a log now counts once, the same degrade an
untimestamped log already got, so the first worklist ranks by ccn times one; the hot
promotion is off when every file weighs the same, since a top 10% of equal weights would
be every file. Repositories with two or more commit times are unchanged.
One ceiling rule, and a coverage summary that says what shape the run is
Config.ceiling_of(scope) is the one spelling of "a scope's own target, else the repo's"
for every command that holds a Config (next-item, brief's packet, the hook's file ceilings,
the coverage summary, digest); the pure modules (score, verify, ratchet, packet, sarif,
worklist, store) keep taking the (target, scope_targets) pair. digest now counts each row against its
scope's ceiling like trend does, so the two agree on the same run pair: on the mini fixture
with src at 200 and tangled (crap 72) added under it, digest said over target 0 -> 1
and new over target: src/extra.ts tangled while trend --json read [0, 0]; it now says
over ceiling 0 -> 0. Its lines read over ceiling A -> B and new over ceiling: ....
The coverage summary carries the run's shape on every path: --json gains kind (coverage
or partial), unmeasured_scopes and ceilings ({"default": 6, "reports": 12}) beside
lane_failures, and over_target and grade are counted over the measured scopes only, so a
--lane web run no longer books the other scope's no-lane functions as this run's debt
(by_scope still carries them). The plain form reads run 1 @ fae4db93108: 2 functions scored: 2 measured, 1 over ceiling 6, CRAP load 41.0, grade F (zero buckets dropped, the
ceiling labelled, or over their ceilings (6; reports 12, util 4)), then -> next: crapkit worklist; a partial run opens with partial run (lane web; api unmeasured; not a baseline)
and ends with -> rerun changed lanes: crapkit coverage --reuse-unchanged, which the --lane
help now names. The report page collects its worklist through the same shaping worklist --json prints, so its rows carry ratchet_mark. Moved contracts: the coverage line in
tests/unit/test_cli_scoring_inproc.py and the docs regex in
tests/unit/test_docs_claims_contract.py; new over target in tests/e2e/test_inventory_e2e.py
and tests/unit/test_store_prune.py; build_digest takes ceiling_of (tests/unit/test_digest.py,
tests/unit/test_narrow_reads.py); every pasted summary line in README.md, docs/lanes.md and
docs/ratchet.md.
--json prints one error object when a command dies
A crapkit error escaping a command under --json used to leave stdout empty: coverage --json with pytest-cov missing exited 5 with 0 bytes, so the Action's comment read "wrote no
run summary" while the sentence naming the fix stayed in the job log. stdout now carries
{"error": {"exit": 5, "kind": "tool", "message": "every lane failed (2 of 2); the errors are above"}, "schema": 1}, with kind one of state (exit 1), config (3), git (4) or tool
(5); the stderr line and the exit code are unchanged, and without --json stdout stays
empty. docs/agent-json.md gains an Errors section.
rescore --gate carries its verdict, and a tenth MCP tool hands it to agents
rescore --gate --json adds a gate block: ok, judged (the functions the working tree
changed since HEAD, untracked files in full), ceilings per rescored file, breaches (path,
function, start, ccn, cov, crap, remedy, key_name, ceiling) and untracked; exit 6 on a
breach is unchanged. The text form prints gate: 2 changed function(s) judged, 0 over ceiling 6 when the gate passes, so the exit code is no longer the only signal. A tenth MCP tool,
gate, maps path to rescore PATH --gate --json with the same read-only annotations; a
breach comes back as a result with gate.ok false and structuredContent, while exits 3, 4
and 5 stay tool errors. The server's instructions, AGENTS.md, both MCP tables and the registry
manifest (server.json) count ten tools.
stdin is read as UTF-8 on Windows, so a non-ASCII path reaches the hook and the MCP server
On Windows a piped stdin arrived in the locale code page while Claude Code and MCP clients
write UTF-8, so the PostToolUse advisory for a ccn-8 edit in pkg/café.py exited 0 with no
output, and brief over MCP answered no function named 'f' in pkg/café.py ... it holds: nothing. crapkit now reconfigures a non-tty stdin to UTF-8 with replacement, the same rule
stdout and stderr already had; a tty keeps its native encoding. Under
PYTHONIOENCODING=cp1252 the same payload now exits 2 with the advisory naming
pkg/café.py, and the MCP call answers the function.
A configuration or marks file saved with a BOM reads as the same file; a UTF-16 one names the fix
PowerShell 5.1's Out-File -Encoding utf8 writes a byte-order mark, which tomllib read as
crapkit.toml does not parse: Invalid statement (at line 1, column 1) and the marks reader
as line 1 has 1 fields, expected 3 behind a carries no metric stamp warning; a bare
Out-File writes UTF-16, which died as a raw UnicodeDecodeError traceback at exit 1. One
reader, crapkit.repotext, now decodes crapkit.toml, the marks file and a portable baseline
with utf-8-sig, and a decode error is a configuration error, exit 3: crapkit.toml is not UTF-8 (first bytes ff fe = UTF-16, the PowerShell 5.1 Out-File default); save it as UTF-8,
or (byte e9 at offset 15) when the mark is not the cause. Every configuration read
(watch, doctor's raw pass and the advisory hook included), ratchet seed, prune,
move, merge (the git merge driver, which refused a BOM side as ours is [unstamped] and
died on a UTF-16 one), explain, brief, rescore --gate, verify's stamp guard, its
marks compare, the marks read behind --override and --baseline-tsv go through it; no
second copy of the decode exists. verify and ratchet merge write a marks file they
rewrite without the mark.
doctor warns on a hook file git cannot spawn
A pre-commit hook written with Out-File starts with a byte-order mark, git answers every
commit with cannot spawn .git/hooks/pre-commit and lets it through ungated, and doctor
passed the file. It now reads the hook git would run (git rev-parse --git-path hooks/pre-commit, so core.hooksPath and a linked worktree are honored) and WARNs
.git/hooks/pre-commit starts with a UTF-8 byte-order mark (ef bb bf), which git cannot spawn; rewrite it as ASCII (PowerShell: Set-Content -Encoding ascii), naming a UTF-16 byte-order mark (ff fe, the PowerShell 5.1 Out-File default) for the other. The README's
Route 1 gains the PowerShell form, Set-Content -Encoding ascii with the interpreter quoted
and forward-slashed.
The lines a shell captures are ASCII
$x = crapkit worklist under code page 437 captured ΓÇö where the header's em dash was.
The six one-liners a script reads back now use -: the worklist header (`... churn 12mo)
- 1 of 3 active (worklist_top 50), 0 dormant
),init's next step (... own files: py -
next: run ...), theratchet seedandpruneline (... added 1, tightened 0 - 1 mark(s)
vs run 1 ...), thewatchbanner (watching 12 tracked files every 2.0s - ctrl-c to stop), with the coverage summary and the verify OK line ASCII already. The same move on the four lines the earlier slices handed over: doctor'scannot import pytest_cov - run ...note, the(rebase or amend rewrote history) - run ...refusal, the MCPno crapkit.toml in -
nothing measured here.result and the CLI'sno crapkit.toml at - nothing to analyze(the Action's base step quotes that line verbatim and needs no change).trend's text line saysN over ceilinglikedigest. Moved contracts: the worklist header suffix in tests/e2e/test_two_view_queue_e2e.py, the watch banner in tests/unit/test_watch_shell.py,over targetin tests/e2e/test_cli_report_gaps.py and the AGENTS.mdbelow_floor` row it
pins, and every README, AGENTS.md, docs/lanes.md, docs/ratchet.md, docs/agent-json.md and
plugin skill transcript that pastes one of those lines (docs/demo.svg is re-rendered at
release).
Configuration is found upward, the way git finds .git
Every command read crapkit.toml from the working directory only, so from a monorepo
workspace crapkit worklist exited 3 with no crapkit.toml at .../mono/web while the root
configuration one level up claimed web/, and crapkit init there wrote a second
configuration claiming the same files. Without --repo, every command, crapkit mcp and
each MCP tool's repo argument now walk up from the working directory to the nearest
crapkit.toml, the way the advisory hook always did (ADR 0002); a .git entry without one
stops the walk, so a linked worktree or a nested repository never borrows a parent's
configuration or store. When the root found is not the working directory one stderr line
says crapkit: using crapkit.toml at /repo, and a relative path argument to brief,
explain, rescore, test-scoped or mutate --files is read from where you stand, so
crapkit brief src/grade.ts classify typed in web/ names web/src/grade.ts; a path that
climbs out of the root is refused with is outside the repo at /repo. A given --repo names
an exact root and walks nowhere, on mcp as on every other subcommand, and reads a relative
path argument against that root as before; the flag now defaults to nothing rather than ..
The MCP server runs each tool's command at the root it found, so a tool's repo-relative
path holds from a server a global client started in a workspace, and a repo argument
naming no directory gets the no-config answer instead of an ancestor's data. init writes
where you stand and exits 3 with crapkit.toml at /repo already claims web (scope 'web'); edit that configuration instead when an ancestor's scope path claims the directory, and
writes no nested .gitignore line when a .gitignore above already ignores .crapkit/; a
nested repository, whose top git consults nothing above, still gets its own line. A stray
crapkit.toml in a non-git ancestor, such as a home directory, is adopted with that stderr
line as the only warning. Moved contracts: --repo's default of
. (tests/unit/test_report_command.py), the lanes page's "no monorepo mode" sentence, the
README's two --repo default lines, the agents page's server line and AGENTS.md's default.
(#69)
Upgrading from 0.4.x
- Run
crapkit doctorfirst. Three defaults changed under committed configs, and doctor
names each one where it applies: an[exclude]glob with a leading**/now also matches the
repo-root copy (**/dist/**excludesdist/too), so read the per-scope file counts; a
coverage.py lane whose python cannot import pytest-cov now fails doctor instead of the first
crapkit coverage; and a{files}scoped-test template on a scope that holds no test file
fails, naming the whole-suite form as the fix. - The first
inventoryorcoverageruns the analysis cache cold. The analysis version is 9:
a Python function'snestingis now a depth (a flat chain of sevenifs reads 1, not 7), and
the cognitive pass no longer charges the first token after a nested helper to that helper.ccn
does not move, so no ratchet mark moves; six of crapkit's own 5,258 rows shiftcognitiveby one. worklisttext rows changed shape.(N std)andw 0.00left the line;crapandcovjoined
it, and the header reads50 of 3980 active (worklist_top 50). A script that parsed the text
should readworklist --json, which keepsccn_stdandweightand addscrap,cov,
ratchet_markandactive_total(schema stays 1).- The coverage summary line changed shape. It reads
run 1 @ <sha>: N functions scored: N measured, 1 over ceiling 6, CRAP load 41.0, grade F, drops zero buckets, and a partial run
opens withpartial run (...);over_targetandgradeon a partial run count the measured
scopes only.digestsaysover ceiling, and so doestrend. - Under
--json, a command that dies prints one error object on stdout ({"error": {"exit", "kind", "message"}, "schema": 1}) instead of nothing; stderr and the exit code are unchanged.
Over MCP the same object is the tool'sisErrortext for the--jsontools. - An unknown
--scopeexits 3 (no scope named 'x'; declared: api, web) where it printed an
empty list at exit 0. --reuse-artifactsrefuses the artifact of a lane whose last attempt wrote nothing (exit 5
forcoverage,cannot concludeforverify). A real run or a rewrite of the file clears it.- A lane positional that names pytest's
testpathsnow loads.python -m pytest tests --cov
besidetestpaths = ["tests"]was refused at exit 3 from every command; it is accepted when the
positionals together name every configured entry.initomits the positional in that case. mutatenever places a mutant in a test file, and a scope declaringpaths = ["."]selects
no file formutate, as it already selected none for scoring: declare the files by name.- The Action fails a pull request it could not judge. With
gate: "true", a base run that was
attempted and not made (a depth-1 checkout, nocrapkit.tomlat the fork point, a lane failing
there) exits 1 and the comment says why; setfetch-depth: 0onactions/checkout. A failed
coveragenow skipsverifyand exits with coverage's code. The comment's verdict names the
rule and lists one bullet per finding. verifytouchescrapkit-ratchet.tsvonly when something moved, never creates it empty, and
its OK line says what it wrote (ratchet: 6 dropped, 1 tightened -> git add crapkit-ratchet.tsv).
A refused--overridenow prints one stderr line naming the cause.- A one-commit repository ranks by complexity instead of reading
risk 0.0on every row. - The MCP server has ten tools.
gatemaps a path torescore PATH --gate --json;explain
anddoctoranswer JSON;worklistandnext_itemtakescope; a bad call answersisError
instead of ending the session. - Windows reads and prints plainly. stdin is read as UTF-8 even when it is a pipe, so the hook
and the MCP server no longer see mojibake; acrapkit.tomlor marks file saved as UTF-16 (the
PowerShell 5.1Out-Filedefault) exits 3 withcrapkit.toml is not UTF-8 (first bytes ff fe = UTF-16 ...); save it as UTF-8where it was a traceback, and a UTF-8 BOM on either file is
tolerated;doctorWARNs on a pre-commit hook that starts with a BOM, which git cannot spawn.
The six shell-captured one-liners (the worklist header, the ratchet seed line, the coverage
nextline, the watch line and two more) use-where they used an em dash, so a script
matching them by that character must change;trendandbrieftext sayover ceiling. - Every command finds
crapkit.tomlby walking up.--reponow defaults to the walk instead of
.: a command run below a root uses the nearest configuration above it and prints
crapkit: using crapkit.toml at <root>on stderr (stdout is untouched,--jsonstays one
object); a.gitentry holding no configuration stops the walk, so a linked worktree or a nested
repository never borrows a parent's store; an explicit--reponames an exact root and walks
nowhere, so the Action and every script that passes it are unchanged. When the walk found the
root, a relative path argument names the file where the user stands, and one climbing out of
the root exits 3; under an explicit--repothe argument stays root-relative as before.initexits 3 under a directory an ancestor configuration already claims through a
scope path, and skips its.gitignoreappend when an ancestor.gitignoreup to the repository
top already ignores.crapkit/. A straycrapkit.tomlin a non-git ancestor, a home directory
say, is adopted with that stderr line as the only warning (ADR 0002). Runcrapkit doctorfrom
the directory you work in and read the root it names.