Skip to content

Releases: ichabodcole/agent-cli-conformance

0.1.15 — the census names the shared flag set, and its fields are promised

Choose a tag to compare

@github-actions github-actions released this 09 Sep 06:56
6edc89d

Nothing in the stable column changed meaning, so nothing a CI gate binds to moves.

The census names a shared flag set

When four or more recorded paths in a --recorded-surfaces batch enumerate one identical flag set, the JSON report carries recordedSurfaces.sharedEnumeration as { paths, flags }, two numbers, and the text census prints one line under the readings:

5 of 5 recorded paths enumerate the same 3 flags; if each flag belongs to one verb, up to 15 flag/path pairs are accepted where they do not belong — a declaration says which; acc show how-to-derive-your-surface-from-one-registry

With --declaration supplied the line ends instead with the census below counts the ones that do not, as accepted-not-declared, and the pairs that do not belong are among the accepted-not-declared findings beneath it.

What the field means, for a parser:

  • paths × flags is an upper bound, not a finding; only a declaration says which verb owns a flag.
  • When several groups of paths share different sets, the field reports the group with the largest paths × flags. A tie goes to the group whose records come first in the batch file.
  • Below four paths the field is absent, not zero. A path whose unknown-flag rejection named an empty accepted set does not count toward it.

Four census fields are promised

declaration.formatVersion, declaration.status, declaration.checkedCommands and declaration.findings[].{kind,path,subject} join the README's stable column. Renaming one, changing a value of status, or withdrawing a finding kind is now a breaking change. A CI ratchet may bind to them by name:

  • status is "checked" when at least one path was compared and "not-checked" when none was. The root counts even when the declaration does not name it, so "checked" with checkedCommands of 0 is a legal state.
  • checkedCommands is the number of declared command paths that were compared. Compared paths the declaration does not name are counted in checkedUndeclared, which is not promised.
  • each finding carries kind, today one of declared-not-accepted, accepted-not-declared, refused-but-enumerated, self-description-not-declared; path, the command path as an array, [] for the root; and subject, the flag or verb. Withdrawing a kind is a break; adding one is an append within the major, so tolerate a kind you do not know.

The exit code still does not move on the declaration diff. A ratchet reads status == "checked" and an empty findings.

The README's advice for JSON consumers now reads: pin a tag or a commit SHA and accept unknown keys, since fields are added within a major.

Decided, not built

Three asks were decided without code:

  • A --fail-on-disagreement flag, or any exit code that moves on the declaration diff, is not built; it waits for a second adopter to ask.
  • An alias field in the declaration is not built. List both spellings of a flag in the accepted set your unknown-flag rejection names, and declare both as rows.
  • A closed-set comparison is not built. The declaration parser accepts values on a flag and nothing compares it; on a positional it is refused as an unknown key.

0.1.15 (2026-09-09)

Features

  • census: name the shared flag set a recorded batch shows, and promise the census fields a ratchet reads (e0bcc7e)
  • census: the census names the shape magpie had, as a bound with its condition (3facef1)

0.1.14 — every artifact names its format, and - reads stdin

Choose a tag to compare

@github-actions github-actions released this 08 Sep 19:58
cdf0d95

Every report now carries formatVersion. It is the first field under data, its value is the string "0", and it names the shape of the report document, as kitVersion names the kit that wrote it. When a run read a declaration or a recorded batch, the report echoes that document's own formatVersion as declaration.formatVersion and recordedSurfaces.formatVersion; both documents have always required the field, so the echo is present whenever the block is. A parser that refuses unknown keys breaks on these three additions. Fields are appended within a major and never renamed or removed, so tolerate unknown keys rather than pin to a key list.

acc report and acc compare refuse a report whose formatVersion is a major they do not know. The refusal is a usage error at exit 2, with details.reason unknown-format-major and two strings, details.formatVersion and details.understood, naming the document's major and this kit's. Nothing of the refused document reaches stdout. A report written before this release has no formatVersion and is still read, because every such file was written under major 0; the text prelude of acc report says format: not recorded for it and format: 0 for a report that carries the field.

The promised surface gains a row. formatVersion on the report, the declaration and a recorded batch, and a reader's refusal of a major it does not know, are on the stable side of the README's table; bumping a major is a breaking change. The unstable side is the shape within a major. The decision is the wiki page docs/wiki/decisions/every-artifact-names-its-format.md.

- names stdin on every file option:

  • acc check --declaration - and acc check --recorded-surfaces -, one of them per run.
  • acc probe-plan --declaration - and acc probe-plan --paths -. A source read from stdin is not in the tree, so the harness excludes no source file from its dirt check.
  • acc report -. The rendering's source: line says /dev/stdin.
  • acc compare - other.json, one of the reports. Its label in the comparison is stdin.

Each is refused when stdin is a terminal, at exit 2, reason stdin-is-a-terminal, before anything is read. Naming - twice in one invocation of check or compare is refused the same way, reason stdin-named-twice, also before anything is read. An invocation that passed a file literally named - now reads stdin instead; name that file ./-. The --help text of each option says that - reads stdin.

A /dev/fd/N path that does not exist gets a different hint. Under bunx, a descriptor opened by process substitution, --declaration <(your-cli schema), does not reach the process. The not_found error at exit 5 is unchanged; its hint now says the descriptor never arrived and names the two ways out: pipe the document and pass -, or run the installed bin, ./node_modules/.bin/acc.

The guide "How to establish your target is safe to check" has a fourth question: where the tool finds the tree it works on. A tool that resolves its root from its own install location, a marker file or a build-time path is not moved by the temporary working directory the probes run in, and the guide gives a hand run against the real tree for that case. The acc skill counts four questions with it.

The guide "How to derive your surface from one registry" changes its round trip to your-cli schema | acc check ./your-cli --declaration - and says what bunx does with a substituted descriptor. Its trap section adds that a help row declared and left permissive is listed as NOT COMPARED while the disagreement count stays 0, so the fix for such a help goes in the tool, not the declaration.

The pdocs adopter report, docs/reports/2026-09-07-the-pdocs-adopter-report.md, records the four findings from issue #49 and what was done about each.

0.1.14 (2026-09-08)

Features

  • report: every artifact names its format, and a reader refuses a major it does not know (b478a4e)
  • report: every artifact the kit writes names its format, and - reads a document from stdin (139b561)

Bug Fixes

  • cli: a descriptor that never arrived is named as one, and - reads stdin (cdd138d)
  • compare: compare reads - from stdin too, so the descriptor hint is true everywhere (d05b455)

0.1.13 — the report says what it reached, and A7 says what clears it

Choose a tag to compare

@github-actions github-actions released this 07 Sep 16:49
cc86861

No verdict, exit code, or JSON field changes in this release. What changes is text: one new line in every text report, two longer report sentences, and the acc skill stopping to ask after the first check. If you snapshot text reports in CI, re-record them once; JSON snapshots change only in the A7 finding's detail and coverageGaps strings named below.

Report output

  • Every text report from acc check and acc report carries a scope: line directly under the config: line. It has one of these shapes:
    • root advertises verbs: scope: the root only — the root advertises 3 verbs (open state tail) and none of them was probed; a batch handed back with --recorded-surfaces covers them
    • a batch was supplied: scope: the root, probed by the kit, plus 2 recorded paths from /abs/path/batch.json; nothing below the root was run by the kit, where the path is the batch file's absolute path
    • no verb set could be read from the root: scope: the root only — no verb set could be asserted at the root, so how many paths sit below it is not known; a batch handed back with --recorded-surfaces covers them
    • acc report over a JSON written by an older kit: scope: the root only — the advertised-verb comparison was not recorded by acc <version>, so how many paths sit below the root is not known, where the kit fills in the version that wrote the JSON
  • The A7 unverified detail, in the text report and in the finding's JSON detail field, is now: both spellings exited non-zero without naming the offending value, so neither refusal can be attributed to the value rather than to the missing verb; a refusal that names the value clears this even where the verb is also missing, so if this root checks placement before values, check the value before the placement.
  • The remedy line for a modelled declaration whose target did not enumerate is now: Remedy: nothing you can write in this file changes this — it becomes checkable when the target enumerates at the root, or when you record its rejections below the root and hand them back with --recorded-surfaces. Text report only.
  • One A7 coverage gap gained a missing word. It was only the first flag whose set help advertises is probed so a target declaring several sets is checked on one of them and is now only the first flag whose set the help advertises is probed so a target declaring several sets is checked on one of them, in the text report and the finding's JSON coverageGaps.

The acc skill

  • The skill stops after step 4, the first check and its two adoptions, and reports before covering subcommands (step 5) or restructuring to one registry (step 6). If the request that invoked the skill already said to continue past the first check, the agent continues; otherwise it asks.
  • Step 5 now says that acc probe-plan needs --paths or --declaration, an existing requirement, and that a hand-written path list or a modelled declaration at that step is scaffolding to replace in step 6, not to commit.

Guides

Documentation only; nothing here changes what the kit reports for a declaration or batch that passed before.

  • Surfaces guide: the invocation named by selfDescription must also be a declared row, and a declaration normally declares the root as path: []; both were already how the declaration comparison behaved.
  • One-registry guide: the root's own verbs such as help go undeclared by a walk over the command table, as its flags do, and once one table feeds both the declaration and the path list, 0 disagreements says only that no flag was added outside the table.
  • A7 rule page: on a verb-first tool, when a flag is misplaced and its value is outside the set, name the value in the refusal.

0.1.13 (2026-09-07)

Features

  • check: the text report says what the run reached, and A7 says what clears it (6f5c6a0)
  • check: the verdict says what the run did not reach, and A7 says what would clear it (3270aa0)

Bug Fixes

  • declaration: the modelled remedy names the route that exists (c7b76e3)
  • docs: what the cold reads found on the pages the adopter round touched (8aaf93c)

0.1.12 — the sentences two adopters had to grep for, now in their guides

Choose a tag to compare

@github-actions github-actions released this 04 Sep 06:17
2bce6f1

Documentation only. No behaviour of acc changed; the prose did, plus one command's --help
notes. Every item below already held and is now written where you meet it. Each names the output
you would have seen and the page that explains it: the two guides open with
acc show <handle> --body; STANDARD.md, the README and the acc skill are files in the
repository.

  • A group node whose rejection lists its subcommands in choices and nothing else. The census
    line reads did not enumerate at <path> … a choices list of N was present and its members are not flag-shaped, and the path lands under NOT COMPARED. Put "validFlags": [] in the same
    rejection, beside choices, and the path compares like any other. Surfaces guide
    (acc show how-to-record-surfaces-below-the-root --body); STANDARD.md, group-node section.
  • A declaration row the root rejection's flag list does not contain as a token, -h and -V
    being the usual ones. The census reports declared-not-accepted -h at (root) for each. The
    accepted set is that list alone; a probe your tool answers does not count, and the -h the kit
    sends for another rule is read for nothing here. Either list every declared spelling in the
    rejection or do not declare it.
    One-registry guide, step 5 (acc show how-to-derive-your-surface-from-one-registry --body).
  • Your own census loop, with no kit in it. If a script of yours diffs your tool's rejections
    against your declaration, probe the root as well as every command path. "Omit the root" is the
    surfaces guide's instruction for a batch handed to acc check --recorded-surfaces, where acc
    probes the root itself. Surfaces guide, step 1; STANDARD.md, census section.
  • A verb-first tool whose unknown-flag rejection lists verbs in choices. The root reads
    did not enumerate at the root, and with no recorded batch the report says
    THE DIFF DID NOT RUN — 0 of N declared command paths compared. Answer the unknown-flag
    rejection with the root's own flags (--help, --version and their short forms, if the root
    answers them) and the unknown-verb rejection with its verbs. A root that accepts no flag at
    all answers the first with "validFlags": [], as in the first item. Surfaces guide, step 1.
  • A positional in your declaration with any key other than name, required (always
    present) and variadic (optional).
    acc check exits 2 with unknown key "<key>" … known keys are name, required, variadic and runs no comparison. The guide's minimal declaration now lists the three keys.
    Surfaces guide.
  • Testing an A6 fix (the -- terminator rule) by hand on a .ts target through bun. Pass
    -- twice: bun cli.ts -- --nope reaches your tool as --nope alone, because bun strips
    the first, and bun cli.ts -- -- --nope reaches it as -- --nope, which is what the kit
    sends. README and the acc skill.
  • acc report <file> with stdout piped or redirected. In a terminal it prints the text
    report; piped or redirected it emits JSON, as every command does and as it always has.
    --format text gets the text rendering onto a pipe. The command's --help notes now say so.

One repository fix rides along: both docs lints now walk directories in sorted order, so CI on
ext4 cannot read two pages in a different order from a laptop on APFS and fail a test that
passes locally. Nothing a consumer runs is affected.

These come from GitHub issues #37 (glamour) and #38–#40 (anthill); each is filed in the
repository as an adopter report under docs/reports/. A sketch under docs/plans/ lays out a
possible change to how the census reads a subcommand list; nothing in it is built.

0.1.12 (2026-09-04)

Bug Fixes

  • docs-lint: walk directories in sorted order, so CI's ext4 readdir order cannot flip a verdict (f5fb4e1)

Miscellaneous Chores

  • pin v0.1.12 so the adopter guide sentences reach a tag (eaa8438)

0.1.11 — a fourth surface status, and stored reports checked when read

Choose a tag to compare

@github-actions github-actions released this 03 Sep 02:03
07e85f1

Most of what follows applies only if your target answers an unknown-flag probe with an explicit
empty set. Item 4 is the exception — it can move an exit code for anyone holding stored report
files, whatever their target does.

No rule verdict, conformant, fullyVerified or acc check exit code moves.

1. A fourth surface.status: enumerated-none. A target that answers an unknown-flag probe
with a present-but-empty set now records what it said, instead of not-enumerated — which meant it
named no set, and dropped the path from the census. In the text report:

stated an empty set of flags at the root under `validFlags`; 7 rejections read, and the set the target named held nothing (the target's own answer, not silence read as one)

Recognised keys are every pairing of valid/accepted/allowed/known/supported/available
with flags/options/switches/arguments, plus choices. choices is the exception: at the
root an empty one still records not-enumerated
, because the root is also where the kit reads
choices as a list of subcommands. Below the root it mints the new status like any other key.

2. Your report can gain declaration findings. The path enters the census instead of being
dropped, and is compared against an empty accepted set — so every flag your declaration marks
valid there is reported declared-not-accepted. This happens even with no recorded-surface batch,
if your root states an empty set:

1 of 1 declared command path compared; 1 disagreement (modelled declaration)
declared-not-accepted  --help at (root) [probed-by-kit]

The trailing tag names where the accepted set came from, not which side declared the flag.

A run whose only evidence was empty enumerations no longer prints THE DIFF DID NOT RUN — the text
report's notice that no comparison happened — and a clean headline can gain a
· but see N declaration disagreement(s) clause.

If your CI asserts on a disagreement count or on the compared-paths fraction, re-baseline it.
And note a finding can be your serializer rather than your parser: an empty array reads the same
whether a tool has nothing to declare or something dropped the contents.

3. A new JSON field, emptySetKeys — an array of strings naming which recognised keys held the
empty set, so you can check the claim against your own bytes. Absent, not empty, on every other
status. It appears at .data.surface.emptySetKeys, on each entry of
.data.recordedSurfaces.readings[], and on each row of acc compare's .data.surfaces[].

4. acc report and acc compare now reject a malformed stored report. A surface carrying
flags on any status other than enumerated, or missing flags on enumerated, exits 2
(usage) with details.reason of flags-on-non-enumerated-surface or
enumerated-surface-without-flags. Previously compare accepted such a file at exit 0 and report
republished it. enumerated-none is not enumerated for this rule — it carries no flags.
Rewrite a rejected file with acc check <target> --json; there is no override, so a report whose
target you no longer have cannot be read by either command.

5. An acc older than this release mis-renders reports written by this one. It prints "nothing
readable was recorded" for the new status, the opposite of what it means. Reached through
acc report and acc compare, which accept any report file — so upgrade anywhere those run.

6. A stored not-enumerated written before this release is ambiguous. It was the only way to
say "named no set", so it cannot be told apart from a target that answered with an empty set, and it
cannot be recomputed. Re-run acc check if the distinction matters.

7. skills/acc/SKILL.md was wrong about coverage. It said adopting an enumerated rejection
converts part of your report from unverified to checked. It does not. What does still hold is
declaring your machine-readable default — "defaultOutput": "json" in acc.config.json.

0.1.11 (2026-09-03)

Features

  • acc: the declaration diff admits the set the target named and left empty (6a8e425)
  • acc: the third enumeration state, and a renderer that cannot fall through (05419c9)
  • kit: a recorded reading publishes emptySetKeys, and the guide says what it carries (ca0a84f)

Bug Fixes

  • acc: an empty choices settles nothing, and the exhaustiveness error names its member (c4b2782)
  • acc: carry the enumerated-none near-miss clause across check and compare (af69a27)
  • acc: the empty-choices exclusion is a position, not a property of the key (8ec16f6)
  • check: re-type VERDICT_WORD as Record<SurfaceStatus, string> (3738419)
  • compare: a malformed stored report is a usage error, not an internal one (0ab567c)
  • kit: a stored status this build cannot read renders a sentence, not a token (5722d23)
  • kit: the half of the flags contract the guard left open (8656c49)
  • lint: the version check read git-ignored scratch, and its skip list was hand-kept (2d623cf)
  • report: the second boundary that published a malformed surface (9a6319e)
  • skills: the anchor check read a number that diagnoses nothing (ae1c21d)

0.1.10 — two stated limits, and a leaner standard

Choose a tag to compare

@github-actions github-actions released this 02 Sep 17:58
009a1ff

If you consume acc as a binary, nothing you can observe changed. No rule was minted or
retired; no verdict, checker, report shape, exit code or CLI surface moved; the catalogue is still
23 rules. This release changes what the project tells you, not what acc does. The one edit under
src/ is a corrected relative link in a fixture's PROVENANCE.md — found by the widened link check
below.

If you contribute here, three of the changes below ask something of you, and those come first.

Three changes for contributors

1. If you cite STANDARD.md by anchor, check your links. Two headings were renamed:

was is
#where-a-declined-recommendation-is-recorded-which-is-not-in-either-of-those-files #where-a-declined-recommendation-is-recorded
#where-they-disagreed-and-it-is-not-settled-here #two-questions-this-page-leaves-open

The second of those is STANDARD.md's own pair — exit-code ownership, and whether the declaration
lives in one file or two. It is not the pair CHARTER.md holds open, which appears further down.

These two are the only anchors that changed. The compression below removed prose inside sections
rather than sections themselves, so no other citation into STANDARD.md has gone stale.

2. Inline links and anchors are now checked in every Markdown file git tracks — where inline
and tracked are both load-bearing, and CHANGELOG.md is exempt by name. Previously docs/lint.ts
read docs/reports/, docs/plans/ and docs/research/, and docs/wiki/lint.ts read the wiki; the
root documents, docs/roadmap.md, skills/acc/, the skill definitions and two fixture documents
under src/ were link targets the check protected and link sources it never opened. Twenty
tracked files newly join it, and the PROVENANCE.md above is one of them.

Three properties, because each will surprise someone:

  • The corpus is git ls-files, not a directory list. A document joins the check at git add,
    before the commit that would publish it. An untracked file is not checked, which cuts both ways: a
    scratch file cannot wedge your commit, and a new document's links go unchecked until you git add
    it.
  • Reference-style links are not read. [text][ref] with a [ref]: target definition is
    invisible in both directions. That is a property of the shared checkLinks helper, and it is why
    the claim above says inline.
  • CHANGELOG.md is exempt by name. release-please writes it, and a check satisfiable only by
    editing a file you do not own would wedge every release.

3. Expect bun run check to take longer: it now runs bun run docs:build. The site build was
never in bun run check; a build failure now fails the gate like any other step. Green on this
branch: typecheck, biome, prettier, both doc lints, the build, and 1,810 tests.

What a clean run does not establish is now written down

CHARTER.md gains a section, What this does not establish, holding two standing limits.
Neither is a gap being closed and neither has scheduled work behind it.

  • The kit compares artifacts against artifacts, never against the world. Help against parser,
    declared surface against recorded surface. Two artifacts wrong in the same way agree, and the
    census reports 0 disagreements and is right to — a flag that is documented, accepted and inert
    is invisible. Reading artifacts against each other is what makes the kit language-agnostic; this
    is the price of that reach.
  • Nothing here reads the prose you ship. On an agent-facing CLI the SKILL.md, the README and
    the paragraph pasted into a system prompt are the interface an agent meets before the binary, and
    no rule looks at them.

README.md carries a pointer to both, where a reader meets conformant and fullyVerified. Read
that section before taking a green run as broader than it is.

STANDARD.md states the guidance and stops arguing for it

1,296 lines to 1,026 and 14,665 words to 11,152 — a fifth of its lines, a quarter of its words — by
separating what a reader acts on from the record of how the project came to believe it. Every
recommendation kept its reason, its evidence link and its [C] / [C?] / [—] checkability mark.
Every removed passage is preserved verbatim with its original line numbers in
what came out of the standard.

One recommendation was added rather than removed. § Parsing now carries a group node that
refuses a flag should name its subcommands
, marked [C?] and framed as a design choice rather than
a defect. No rule id was minted, and none should be cited for it — the catalogue is unchanged.
The evidence and the decision are in
the group-command candidate.

That recommendation rests on a six-tool survey of how group nodes answer an unknown flag.
Re-measuring the three tools whose counts the argument turns on corrected two of them: gh repo
prints 18 subcommands in its rejection where the survey said 19, and kubectl config holds
15 where it said 16 (gh 2.98.0, kubectl v1.34.1; docker image's 12 was right). The
argument is unaffected, and the report carries a dated correction.

The built site no longer emits links that only work if you hold the repository

Links to files the site does not render — a checker in src/, a report — were filesystem paths
relative to the built page, which resolved only for someone reading docs/dist from inside a
checkout. They are now absolute GitHub URLs, read from package.json's repository.url: 128
href occurrences across 37 pages, resolving to 60 distinct files
in the build this branch
produces. The build now rejects any relative link that escapes the output root, rather than only
checking that the target exists.

The ref is main, not the commit being built. A site built from a branch therefore links to
main's copy, and for a file the branch adds there is no such copy: in this build exactly two
href occurrences, one to each of the two reports this release adds, resolve only once the
promotion merge lands. Pinning the commit instead would 404 for every unpushed one, which is why the
ref is main.

STANDARD.md and CHARTER.md now render as satellites alongside docs/roadmap.md, and the wiki
index's "Start here" table gains a row routing a reader deciding how a CLI should behave to the
standard. Nothing in this repository publishes docs/dist — it is built to be checked, and handed
over by hand — so this repairs the artifact before anyone receives it rather than fixing a live
site.

Recorded, not shipped

  • Adopter evidence on the two questions CHARTER.md holds open — the L0/L1/L2 ladder, and
    whether the 23 rules are the right rules. Three adopting agents, asked separately, all said delete
    the ladder and all said keep something; the sharpest form is names for boundaries, yes; numbered
    rungs, no
    . The ladder is unchanged here — acc rules --probe-level still takes L0|L1|L2
    and the verdict line still reads L0. No human owner of an adopted tool has answered, a caution
    that travels with the evidence.
  • Three roadmap entries, none scheduled: acc check's text report and its first screen, the
    ladder and what replaces L0, and three open findings from reading STANDARD.md against the
    wiki.
  • Two new skills under .claude/skills/, and three existing ones revised — this project's own
    authoring toolkit, not the shipped skills/acc/, which is unchanged. An adopter installing acc
    gets nothing from them.
  • Findings left unrepaired and named as such, in
    what came out of the standard: prose
    defects in CHARTER.md and README.md that were out of scope, and two figures in STANDARD.md
    that fit none of the four evidentiary phrases the page declares and were left unmarked rather than
    forced into the nearest slot.

0.1.10 (2026-09-02)

Features

  • lint: the gate reads the markdown no other lint opened (f766377)
  • skills: add repair-chain methodology (d1cd2a6)
  • skills: guidance-not-argument, the pass this session ran by hand (ad8bd3b)
  • skills: strengthen cascade and two-lens review (5e59b2f)

Bug Fixes

  • docs: four contradictions a cold reading of the tree found before release (d760879)
  • lint: gate what git tracks, and publish the two documents the wiki points at (6a088e4)
  • site: a link out of the wiki points at the repository, not at a path (e96419a)

0.1.9 — eight corrections to the report guide, and a gate against absent fields

Choose a tag to compare

@github-actions github-actions released this 30 Aug 17:54
5ca90b8

This release changes what the kit SAYS, not what it does. Every consumer-visible
change is in the check-report JSON guide — the page acc show how-to-read-the-check-report-json --body prints and the page a script author is
pointed at. Eight of its statements were wrong; at least one of them describes a
check a reader can write today that will never fire.

What did not change

Nothing about the tool's behaviour, and this is the more useful half of the note
if you are deciding whether to upgrade.

  • No report field was added, removed, renamed or retyped. The same target checked
    with the previous release's kit and this one produces documents that differ only
    where any two runs differ: capturedAt and the millisecond timings.
  • acc schema is byte-identical across the range.
  • No rule, checker, verdict, count, exit code, flag or command changed. The only
    source file in the range is src/acc/kit/report.ts, and its changes are two
    new exported constants used by the repo's own lint plus corrected comments; no
    interface gained, lost or altered a field.

The upgrade itself breaks nothing. The corrections below are a different
matter: several describe checks that were wrong when you wrote them and are
wrong now, so read them as a work list rather than as reference.

Corrections you may have to act on

Each was checked against the code or the tests before being written here.

  • spawnFailed is always false in any report this kit writes. The guide
    previously invited a CI check on it. That check can never fire: the first probe
    that cannot spawn aborts the run (record.ts) and surfaces as a not_found
    error envelope (commands/check.ts), so no report is produced at all. If you
    branch on spawnFailed to catch a target that will not execute, you have a
    check that passes silently forever — test for the not_found envelope instead.
    This is the one correction that turns a working-looking pipeline into a known
    blind spot.
  • exitCode and signal are not "exactly one of the two". Both are null on
    the path where the target's close never arrived, and exitCode is also
    nulled when the kit killed the target itself — timedOut OR truncated, and
    the guide never mentioned the second. Code shaped as exitCode === null →
    "signal names the death" breaks on both. Read crashed, timedOut and
    truncated before attributing a death.
  • counts is not a partition; do not derive one count by subtracting others.
    Every count is taken over the applicable, unwaived findings, and a passing
    diagnostic finding is counted by none of them.
  • evidenceGaps is not scoped to passes. Fails and unverifieds contribute
    rows too, opening with the verdict and the finding's detail. Reading a row as
    a qualified pass is the misreading this field most invites; check the
    matching finding's verdict first.
  • sweep reads in one direction only. The hash covers observation ids, exit
    codes, signals and the two stream digests, and nothing else. Two different
    marks prove the evidence differs; equal marks do NOT prove it identical — a
    hang and an outside kill that both died silently collide, and so do two runs
    differing only in timing. The old text claimed the equal direction.
  • launchAdjustment is a replay aid, not a delivery check. It reports the
    argv the kit put on the WIRE; what the target receives is args, which is the
    point of the compensation. Presence tells you the compensation fired; absence
    tells you nothing, because the ordinary absence is a target with no bun layer to
    compensate for. So you cannot read the field, either way, as confirming that the
    target got what was recorded — including in the one documented case where
    delivery really does differ, a bun layer hidden inside a wrapper script, where
    nothing fires and nothing is reported.
  • excused was never explained at all — it appeared only as a value in the
    specimen, so this is a new statement rather than a corrected one, listed here
    because it is equally actionable. It is true only while a knownFailures entry
    is suppressing a live fail or unverified; it goes false the moment the rule
    passes, and the config entry then surfaces in staleExpectations.
  • timeToFirstByteMs: null is not a hang. A silent success produces the same
    null; timedOut is what separates them.
  • A finding's detail carries no per-observation clause. The lookup table
    promised one clause per observation that decided the verdict, which the body of
    the same page denied. Use probes for what was sent, never the clause order.

The page also gained entries rather than corrections for fields it never
mentioned at all — capturedAt, declaration, recordedSurfaces,
stdoutDigest and stderrDigest — and for fields that previously appeared only
inside the JSON specimen or in passing, where a reader got a value and no
statement: probeLevel, waived, unresolved, notApplicable, and
the staleExpectations/inertExpectations pair. It also gained an
advertisedVerbs specimen printed by a command the page gives you, run against
the kit's own CLI, so its shape can be checked rather than trusted.

For contributors: the gate behind it

Two report fields shipped undocumented in two consecutive releases, each caught
only because a reviewer happened to look. Two chained gates now make that a build
failure rather than a habit. DOCUMENTED_REPORT_FIELDS in
src/acc/kit/report.ts is a spec bound by a mapped type to the four interfaces
a report is built from — Report, ReportedFinding, ReportedObservation and
EvidenceProbe — so adding a field to an interface fails tsc until the field
is listed; being listed then fails the artifact docs lint
(bun run docs:lint:artifacts) until the field is either defined in the guide or
declared in UNDOCUMENTED_REPORT_FIELDS. The lint prints two numbers on every
run — currently report fields: 64 listed, 0 declared undocumented — because one
number cannot separate a field that got documented from a field that left the
type.

State the limits, because a green check is a claim:

  • Top-level keys only. A key whose value is an object — surface, counts,
    configSource, targetIdentity, declaration, recordedSurfaces,
    advertisedVerbs, the elements of waivers — is a leaf to the gate, and the
    fields inside it are covered by neither half. Both misses that motivated the
    work were top-level keys; a recursive spec is separate, larger work.
  • Shape, not truth. The gate certifies that a definition-shaped sentence
    exists. It cannot read one. A bullet that restates the field's own name passes,
    and that is worse than absence, because absence at least signals undocumented.
  • Forward only. A field removed from a type but left in the guide is not
    caught.
  • Declared absence is a legal answer. A field may pass by being named in
    UNDOCUMENTED_REPORT_FIELDS instead of being explained. The property is "no
    field is absent without someone saying so", not "every field is documented".

The set is empty at this revision, so nothing is currently declared undocumented.

0.1.9 (2026-08-30)

Features

  • docs: declared absence, so no field is silently undocumented (a547130)
  • docs: the JSON guide cannot omit a report field (1031c1a)
  • docs: the JSON guide cannot silently omit a report field (0739e1d)

Bug Fixes

  • docs: correct four invented claims in the JSON guide rewrite (1299465)
  • docs: drop the overreaching counts clause and anchor the term fallback (0bb7a60)
  • docs: repair an inverted field description and six review findings (4f8ca86)
  • docs: three more false bullets, and both homes of the sweep claim (a69fb5a)

0.1.8 — A6 measures bun-launched targets, and the record names the wire argv

Choose a tag to compare

@github-actions github-actions released this 29 Aug 19:49
bb653ee

This release makes A6 — the -- terminator
rule — return a real verdict for targets acc launches through bun. It previously returned
unverified for every such target, permanently, which for an adopter whose every CLI is a bun
script meant the rule could never be measured on the population acc handles best.

Are you affected?

targetArgv0 in your report is an array — look at its first element. It tells you whether
acc launched your tool through bun directly. It cannot tell you whether something it names runs
bun underneath, so if the first element is not bun, you need to know that about your own script.

  • It is bun. This release changes what A6 tells you; read on. You land here when your
    file's shebang names bun at any path (#!/usr/bin/env bun and #!/opt/homebrew/bin/bun both
    count — only the final path segment is read), or when it is a .ts file carrying no shebang.
  • It is a wrapper script of yours that runs bun underneath. Nothing in this release changes
    your verdicts, but read the first bullet under "What is not fixed" anyway — A6 has been,
    and still is, judging your tool on an argv it never received. This is the case most worth your
    attention, and it is the one this release does not fix.
  • It is anything else. A6 behaved correctly for you before and still does. Nothing to do.

What changed

A6 no longer refuses bun-launched targets. Bun strips one bare -- immediately after the
script path — one per bun layer between the launcher and the script — before the script sees
process.argv, and A6's probe leads with that terminator, so the probe was never delivered. The
runner now prepends one extra -- at the spawn when it can see that bun is the launcher. Bun
eats the extra one; the target receives what a native target receives.

Measured: on this repository's own CLI, launched through bun, A6 now reports pass where it
reported unverified; on kit/fixtures/broken/ignores-double-dash.ts, also launched through
bun, it reports fail.

What a consumer has to act on

Verdicts move. If your targetArgv0 starts with bun, A6 changes from unverified to
pass or fail. Automation that treated A6's unverified as a fixed property of bun targets
should stop assuming it. A6 stays diagnostic, so a new fail does not move your exit code or
your conformant claim. For a target launched through bun it is a real measurement that your
tool did not honour --, so read the rule page rather than filtering it out. Behind a wrapper it
is not — see "What is not fixed".

New optional field in the report JSON: launchAdjustment, on an observation. It is present
only when the argv acc itself put on the wire differed from that observation's recorded args.
It reports what acc did at the spawn, and cannot see a launcher below that — a wrapper that
eats the terminator internally produces no launchAdjustment, because acc spawned exactly what
it recorded.
Today the only source is this bun compensation. Its value is a single line of prose — do not
parse it programmatically
, branch on the key's presence instead. Do read it when replaying a
probe by hand: the wire argv is spelled out inside it. Example (wrapped here for width; the real
value contains no newline):

one `--` was prepended for the bun launcher and consumed before delivery; the wire argv was
["bun","/abs/path/cli.ts","--","--","--acc-probe-xyzzy-value"]

The recorded args are not the wire argv, so a hand replay of args alone against a bun target
will not reproduce the kit's run. They stay uncompensated so that A6's evidence id and
acc compare's alignment do not fork between bun and non-bun targets. See
how to read the check report JSON.

What is not fixed

  • A wrapper script that hides a bun layer is still uncompensated. The deleted unverified
    short-circuit keyed on the target's argv0 naming bun, and a wrapper arrives as its own
    path — so it never matched that guard, and was already collecting a verdict measured against
    an argv it never received. What is new is that this is now a named A6 coverage gap instead of
    a silent one. Measured: a conforming .ts fixture reports A6 pass passed directly and
    fail behind a one-line exec bun wrapper. Pass a direct path wherever you have one.
  • The compensation is by exactly one --, and that is a property of acc, not of bun.
    Measured on bun 1.4.0: stripping is one per bun layer — bun <script> strips one, but
    bun run <script> interposes a second layer and strips two. One is the right amount only
    because acc never constructs a launcher deeper than a single bun layer, which a test asserts.
  • A bun build --compile binary needs no compensation and gets none: measured, it strips
    nothing.
  • A7 still carries its caveat. The
    eight owner-CLI run measured both A6 and A7 as
    unverified across all eight targets. A6's cause is addressed here; A7's prose extractor
    still finds no closed value set to falsify on that population, and that run has not been
    repeated against this change.

Provenance

All bun behaviour above was measured on bun 1.4.0, macOS (darwin 25.6.0), and is written up in
docs/research/2026-08-29-bun-terminator-stripping.md.
README.md, STANDARD.md, the A6 rule page, the probing concept, the JSON report guide, the
first-CLI tutorial and the checker-authoring guide were repaired to match the new behaviour. The
repository's full gate — typecheck, lint, markdown format, both doc linters and the test suite —
is green at 1771 tests.

0.1.8 (2026-08-29)

Bug Fixes

  • kit: A6 measures bun-launched targets instead of refusing them (dbfee51)
  • kit: A6 measures bun-launched targets instead of refusing them (a12ddea)
  • kit: A6 reaches the bun population (7949ae9)
  • kit: the A6 terminator survives a bun launcher (4d17a4f)
  • kit: the A6 terminator survives a bun launcher (8e0f233)
  • kit: the record says which argv it means (37eabae)
  • kit: the record says which argv it means (06f81f0)

0.1.7 — the verb set a tool advertises, against the paths it records

Choose a tag to compare

@github-actions github-actions released this 29 Aug 06:13
f26cda0

acc check now prints an ADVERTISED VERBS vs RECORDED PATHS block: the verb
set a target names at its own root, stated against the first token of every
path in a --recorded-surfaces batch, in both directions. recorded but never advertised is the defect direction. advertised but never recorded renders
as not covered by this batch and never as "missing", because for a
deliberately partial batch that is the expected state.

IT IS EVIDENCE, NOT A RULE, and the condition is load-bearing: the recorded
side is the caller's own attestation, so nothing the KIT fails on may rest on
it. It carries no rule id, contributes to no verdict and to no rule tally, and
changes no exit code — the block is not on any exit path, and the two cases
checked behaved accordingly: a fixture that fails core rules still exits 9 and
a conforming one still exits 0, with the block printed either way. (The block
does print counts of its own, and the JSON carries lists; what it does not do
is move any number this kit already reports.)

An adopter who wants a gate builds it themselves, on the JSON, and status is
the test rather than field presence: every run this release writes carries
.data.advertisedVerbs, a report written before it carries none, and the
three comparison arrays appear only when a comparison happened.
.data.advertisedVerbs carries status, one of not-asserted, no-batch or
compared; capturesRead; open, the openness of the union; union, the
set the directions are computed over; hedged, naming captures that were read
and NOT asserted; quoted, the capture whose words are shown, with its own
shape, from, open and originating observation; and, when status is
compared, recordedNotAdvertised, notCoveredByBatch and disagreement.
union is the set the directions are computed over; hedged names captures
that were read and NOT asserted, so a run can say which surfaces it declined.

A gate over that has three failure modes worth closing explicitly, because two
of them fail silently:

  1. Check the exit code first. 1-8 means the invocation failed, so the JSON is
    absent or partial and status is undefined. A gate written purely over fields
    passes a broken run.
  2. Branch on status, do not just test recordedNotAdvertised. compared
    with an empty array is a clean comparison; not-asserted and no-batch mean
    nothing was compared at all. A gate that treats those alike is green because
    nothing ran — on the fleet described further down, that is half the tools.
  3. Read open — the top-level one — before failing. It carries the
    openness of the UNION, which is the set the directions are computed over;
    quoted.open describes only the capture whose words were quoted, and the two
    differ when one capture's list is elided and the other's is not. On an open
    list the verb may live in the elided tail, so a defect there is a question
    rather than a finding.

It has to be TWO checks, not one conjunction, and this is the part most
likely to be got wrong: a defect-only gate answers fire or do-not-fire, and
not-asserted and a clean compared both answer do-not-fire. They are not
the same result and a build cannot tell them apart. Note also that
recordedNotAdvertised is present only when status is compared, so
testing it non-empty already implies the status — adding a status clause to
the defect conjunction changes no outcome and buys nothing.

ran   = exit code NOT in 1-8, and status === "compared"
clean = ran, and recordedNotAdvertised is empty (or top-level `open` is true)

Fail the build on ran && !clean. Treat !ran as its own state — it is not a
pass, and on the fleet described further down it is half the tools. Exit codes
1-8 are excluded because the invocation failed and the JSON is absent or
partial; the band above 8 stays open-ended, and 10 already exists, so a
literal allowlist of 0 and 9 would skip a run whose JSON is complete.
Gating on it means accepting caller-attested evidence as a failure condition —
which is the thing this kit will not do on your behalf.

THE BLOCK PRINTS ON EVERY RUN, including the runs where it did nothing, and
the case where it says it did NOT run is the one to read first:

THE COMPARISON DID NOT RUN — no advertised verb set could be asserted at the
root, so the recorded paths were compared against nothing.
2 root captures read, none asserted a verb set (NOT a tool that advertises
no verbs)
This is not agreement: nothing was compared, and nothing here is a finding
about the tool.

On the fleet this was designed against, half the tools fail on BOTH root
sources: their bare invocation prints a help screen with no usage:-anchored
bracket group, and their unknown-verb rejection enumerates nothing at all.
With neither source asserting, this is the common render there rather than a
safety net — the adopter's measurement, not ours, and it is why a reader who
mistook it for an empty advertised set would turn every recorded path into a
finding against the tool that was really an artifact of our parse. The
measurement behind "half" is the adopter's, reported in
docs/plans/2026-08-28-the-declaration-skeleton.md; it is not ours.

TWO SHAPES ARE READ, AND ONLY FROM TWO PLACES. A choices array in a JSON
error envelope, and a pipe-delimited bracket group on a usage: line — the
legacy shape, which is where the drift this was reported for actually lives.
Both are read only from the bare invocation and the unknown-verb rejection,
never from a --help body, and the existing exclusions (timeout, crash, spawn
failure, truncation) apply to whichever is read.

THE NARROWING STACK REFUSES CHEAPEST-FIRST: a usage:-anchored line, the
first bracket group after the program token (so usage: cli <open|state> <file> never contributes file), at least one pipe — which is what kills a
standalone <file> or <command>, and is not a formatting quirk, and every
member token-shaped or the whole blob is refused. A two-member blob is not
asserted on shape alone — <name|id> is a type union and <get|set> is a
two-verb tool, and no lexical rule separates them; at that size the last
discriminator is whether a majority of the blob's members appear among the
recorded first tokens.

⚠ That discriminator has a consequence worth stating, because it runs against
this block's own purpose: on a TWO-VERB tool the admission test consults the
recorded side, so drift can suppress the finding it would have produced. A
tool advertising <get|set> whose recorded paths are get and deploy
matches one of two, the set is refused, status is not-asserted, and
deploy is never reported — recorded, unadvertised, and silent. That is
not-asserted arising from the drift itself rather than from a parse failure,
and it is a known limit of the two-member rule rather than a defect in it: no
lexical rule separates a two-verb tool from a type union, and the alternative
is asserting <name|id> as a verb set.

… AND ... ARE OPEN-SET MARKERS, dropped from the set and carried as a
flag, and the flag qualifies the finding rather than suppressing it — a defect
line on an open list says the verb may live in the elided tail.

WHERE TWO CAPTURES BOTH ASSERT, precedence and comparison are split on
purpose. The rejection's words are quoted; the UNION of the asserted sets is
what the directions are computed over. Testing recorded paths against the
rejection alone would turn a verb only the bare capture names into an
accusation manufactured by our own choice of source. A disagreement line
names spellings one asserted capture carries and the other does not — it
catches a partial retrofit, where a tool grows a choices array while its
hand-maintained usage string goes stale. That line has NOT been exercised
against a real divergence; it is recorded as untested rather than assumed
passed.

THE JSON CARRIES THE WHOLE SET AND THE TEXT LINE DOES NOT. The text caps at 32
verbs with the true count beside it; the JSON is never sampled. That asymmetry
is the point: the action on a defect line is "go add THESE to your help", and
a sample plus a count would be the tool saying what it did while withholding
what it found — so the work list lives where a script picks it up. The JSON
set is not truncated to fit. A target emitting a pathological list overruns
MAX_STREAM_BYTES, and a capture cut at that ceiling is EXCLUDED rather than
read — the same exclusion that covers a timeout, a crash and a spawn failure.
So an asserted set is a whole set, an overrunning target yields no assertion
at all, and a script never receives a short list that looks complete. The set
lives in a new advertisedVerbs field rather than widening
surface.nonFlagCandidates, whose contract is diagnostic where this one is an
assertion, and it is derived once so nothing parses a stream twice.

A STORED REPORT WRITTEN BEFORE THIS SAYS SO. acc report on a JSON with no
advertisedVerbs prints that the artifact predates the comparison, naming the
kit that wrote it, rather than omitting the block.

KNOWN LIMIT: the comparison reads the FIRST TOKEN of each recorded path, so
state show contributes state and paths deeper than one token are invisible
to it. A usage: line's bracket group names one token and cannot express
depth, so this is a property of what can be advertised rather than an
oversight — recorded in the plan rather than left for a reader to discover.

VALIDATED BY THE ADOPTER WHO ASKED FOR IT, on a develop install against three
targets of their choosing. It found a true positive on its first real run: a
dispatched alias that works, carried in the tool's help prose — which this
census never reads — and absent from the rejection's choices array, which it
does. So the verb was recorded, was not advertised at the surface the census
reads, and the line said so correctly. The fix is tool-side.

That is also the shape the note's own caveat is about, and the tw...

Read more

0.1.6 — the upgrade remedy stops wiping your cache

Choose a tag to compare

@github-actions github-actions released this 28 Aug 23:00
8b233ab

Four things a consumer acts on: the upgrade remedy, the install transport, a
duplicate-entry failure that is easy to mistake for the other two, and a new
field in the error envelope. Everything else is method and report work that
changes no behaviour, listed at the end.

THE UPGRADE REMEDY NO LONGER WIPES YOUR WHOLE BUN CACHE. acc version --check
on exit 10 — "a newer release exists" — used to hand every caller three
commands: bun remove, then bun pm cache rm, then a pinned bun add over
git+ssh://. The middle one clears bun's ENTIRE global cache and takes no
package argument — which on a shared CI runner means cold-caching every
dependency of every job sharing it. It is gone from the sequence. What exit
10 prints now, and what it puts in the machine-readable next array, are
generated from one source so they cannot drift:

bun remove agent-cli-conformance
bun add -d git+https://github.com/ichabodcole/agent-cli-conformance.git#<tag>
acc version --check

WHY IT WENT. Measured on bun 1.4.0 against this repository while it is public
— both conditions load-bearing, and a bun release, a private fork or a
non-GitHub host could change either. Under them, bun normalises
git+https://github.com/… to github:owner/repo: the two spellings produce
one @GH@<owner>-<repo>-<sha> cache entry and no bare clone. git+ssh://
produces a bare clone (<hash>.git, carrying a HEAD) plus a @G@<sha>
entry. Both were read off the cache directory after installing each way. The
stale-bare-clone refusal — no commit matching "…" — that the wipe existed to
clear needs that clone. The wipe was correct when it was written, because
the sequence then printed git+ssh://, which does create one. What
invalidated it is the transport change in this same release: the sequence now
prints git+https://, and on that line there is no clone to clear. Two
consequence checks on the new line agree with the cache reading: a pinned
v0.1.4 → v0.1.5 upgrade in one warm cache exits 0 with no refusal, and
an unpinned install into a package.json holding no entry, against a cache
holding only 0.1.4, delivered 0.1.5.

The command is not deleted. Exit 10 now prints it in a paragraph addressed
to callers whose install line is git+ssh://, with its cost attached, and
docs/wiki/guides/how-to-fix-a-broken-install.md keeps it as that transport's
remedy. ⚠ That paragraph is prose only: the next array carries the three
commands above and nothing conditional, so a PROGRAM reading next on an ssh
install does not receive the cache step. Until that is closed, a program that
may run on either transport should read its own dependency URL from
package.json and, when it is git+ssh://, FAIL LOUDLY rather than proceed —
do not silently insert the cache step, which is the one command this release
exists to keep out of CI. next is advisory, and this is the one case where
it is incomplete rather than wrong.

The tests that asserted the wipe was present did not just go. They now assert
the invariant behind both versions of this mistake — every command in the
sequence either remedies a failure that reproduces on the transport the
sequence prints or verifies the result, ordered so it works — plus one test
that does nothing today and requires any reinstated whole-cache step to carry
its own cost in its own description.

IF YOU ALREADY HAVE acc INSTALLED, YOUR OWN KIT STILL PRINTS THE OLD
SEQUENCE. (Installing for the first time? Skip to the install documents below;
none of this paragraph applies to you.) The fix ships in the binary, so it
arrives only once you have upgraded — and the message that prompts an upgrade
is the one carrying the old advice. This note is the way around that. If
your install line is the git+https:// one above
, run the three commands
here instead of the ones your installed acc version --check prints, and get
<tag> from:

git ls-remote --tags --refs --sort=-v:refname \
  https://github.com/ichabodcole/agent-cli-conformance.git 'v*' \
  | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+$' | head -1

If you are on git+ssh:// deliberately — a contributor with a key, a
private fork, or a network whose proxy rewrites github.com — do NOT paste the
https line: it points at a repository that is not yours or a transport your
network will not serve. Keep your own URL and put bun remove first, for the
reason below. On that transport bun pm cache rm before the add is doing real
work — and it carries the same cost there as anywhere: it clears bun's WHOLE
cache, takes no package argument, and does not belong in CI or a build step.
Derive your <tag> by pointing the ls-remote line at YOUR OWN remote, not
the one above.

THE INSTALL IS ANONYMOUS. This repository is public. Every live surface that
named the install still said otherwise, and the claim was load-bearing: it is
why every documented command used git+ssh://. Measured with the global git
config disabled and terminal prompts off, on a machine whose other credential
paths were not exercised: an anonymous git ls-remote https://… answers with
the tags, and bun add -d 'git+https://…#<tag>' installs at exit 0 and
delivers that tag. So https leads now everywhere the old claim reached: the
README, the acc skill's step 1, the install guide, the L0 guide, the wiki's
routing row, the exit-10 output, and the remote list acc version --check
tries. Against THIS repository, on a network that reaches github.com directly,
that means a CI runner or a sandboxed agent needs no key, no token and no
credential helper — which is the case the old ssh-only line could not serve at
all.

git+ssh:// is not dropped: still the second remote tried, still what a
contributor with a key uses, still what survives a rewriting proxy, still what
a private fork needs. GIT_TERMINAL_PROMPT=0 stays — the guard is about what
happens when a read does NOT succeed, which is when nobody is watching.

THE INSTALL DOCUMENTS NOW SEPARATE A FIRST INSTALL FROM AN UPGRADE. The
skill's step 1 derives the current tag with the ls-remote line above and
asks you to substitute it into the pin. The placeholder is deliberately not a
shell variable: an interpolated empty ref leaves a bare #, which resolves
exactly as no ref does — measured both ways against one seeded cache, same
result — while looking pinned in the package.json you commit. Against a
cache already holding an older resolution that is what you get: exit 0, an
older kit, and a committed URL that reads as pinned. Pasted unsubstituted the
add refuses at exit 1 and changes nothing, also measured, on a fresh
project and on one already holding the dependency.

⚠ ADDING OVER AN EXISTING ENTRY IS A THIRD THING TO ACT ON, and it is not
the same as the stale-cache failures above.
bun add pointed at a new ref
does not replace the dependency: it appends a SECOND entry under the same key
and resolves the first — measured on bun 1.4.0, with no warn: Duplicate key
printed at all, and with the success line naming the sha you asked for while
the other one installs. That is why the two measurements in this note differ
rather than disagree: an unpinned install into a package.json with NO entry
resolved fresh and delivered the newest release, while an add over an existing
entry delivered the old one. Whether your package.json already names this
dependency is what decides which you are in. bun remove first is the fix, on
every transport, which is why it leads the sequence.

The guide documents three ways an install hands you the wrong bytes — the
duplicate key, the stale bare clone, and a stale extracted package — and each
is now scoped, in the guide and in the README and the wiki row that route to
it, to the transport that can produce it. On git+https:// only the duplicate
key is reachable, so a reader there is no longer sent hunting a stale bare
clone they have no way to create. (These three are failures, not the four
changes this note opens with; they are different lists.)

EXIT-2 REFUSALS NOW SAY WHICH REFUSAL THEY ARE. acc report and acc compare
refuse a bad input file with exit 2, and until now nothing machine-readable
separated the cases — which matter differently: a corrupt file needs fixing,
and a well-formed file that is not a report needs a different command run over
it. Each refusal now carries a stable details.reason in the error envelope:

not-json       the file is not JSON at all — it is corrupt or truncated.
               `acc report` and `acc compare`.
not-a-report   valid JSON, but not an `acc check` report — it lacks the
               target and observations. Most likely a recorded batch or a
               different tool's output; run `acc check` to produce a report.
               `acc report` and `acc compare`.
no-verdict     an `acc check` report carrying observations but no verdict,
               so there is nothing to render. `acc report` only — `acc
               compare` never needed a verdict and never conflated this.

not-json and not-a-report come from the loader both commands share, so
they appear identically in each; that is read from the shared code path, and I
ran all three against acc report rather than all five combinations. No exit
code was added or changed, and the field is additive, so nothing branching on
exit codes today behaves differently.

NOT CHANGED: no command, flag or exit code added or removed. The test suite is
green — 1740 tests across 55 files, bun 1.4.0.

INTERNAL, and a consumer acts on none of it, with one exception. The
cold-repair method page gains a pre-edit dependency-list stage, two named seam
checks and a second defect class kept deliberately out of its counts; a
per-class baseline for the two earlier runs was recovered from evidence
already in the tree, so the next run has something to compare against. The
exception, for anyone working on the probe-plan proposal: it recorded that the
war...

Read more