> For the complete documentation index, see [llms.txt](https://docs.thecolliery.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.thecolliery.org/tools/coalledger/changelog.md).

# Changelog

All notable changes to CoalLedger are documented here. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning: [SemVer](https://semver.org/) (the version lives in `.claude-plugin/plugin.json`).

## \[0.14.0-beta.1] - 2026-10-02

Closes a git-spawn environment-poisoning hazard class with a census gate, bounds and contains every repo-derived file read and write, reports a config candidate that exists but cannot be read, and fixes a table-cell escaped-pipe bug in the structure engine.

### Added

* **The session-start conductor reports a config candidate that exists but cannot be read, named by reason.** A directory sitting where a file is expected, a FIFO or device file, a file this process is denied access to, JSON that fails to parse, or a value that parses but isn't a JSON object, at either the winning per-project candidate or the global config, now surfaces as one context line—`[CoalLedger] UNREADABLE: <path> exists but is not a readable config (<reason>); it was skipped—canonical = <that side's own canonical path>`—the same shape as the existing LEGACY and IGNORED lines. Before this, such a file was silently skipped with no report at all, although the directory-skip SELECTION itself was already correct. Wired at both read sites (the project candidate and the global config independently)—test: `scripts/lib/config-load.test.mjs`.

### Fixed

* **GFM's escaped pipe inside a code span in a table cell kept its backslash instead of losing it.** `splitRow()` now drops the backslash while splitting a table row, before inline parsing, matching the GFM Tables extension (cmark-gfm's `unescape_pipes`)—a cell holding `a\|b` reads `a|b`, not `a\|b`; a pipe after an even run of backslashes (`\\|`) stays a cell delimiter. `makeRow()` splits the cell's raw source segments at each dropped backslash so inline source positions stay accurate, and a link or other inline node whose end lands exactly at a dropped backslash now keeps its correct end position rather than absorbing the backslash into its span—test: `scripts/lib/md-checks.test.mjs`, fixture `scripts/fixtures/table-escaped-pipe.md`.

### Security

* **Every `git` child process this room spawns now takes its environment from one shared, stripped-and-ceiling-set helper, never a bare or `process.env`-derived environment.** An unguarded spawn inheriting an inherited `GIT_DIR`/`GIT_INDEX_FILE` from the caller's environment can silently redirect a `git init` (or any other git command) at an unrelated repository—the exact shape that turned a linked-worktree hook invocation into an accidental `core.bare = true` flip on a real repository elsewhere on disk. `scripts/lib/git-env.mjs` strips every `GIT_*` key and sets `GIT_CEILING_DIRECTORIES`; a new census (`scripts/lib/git-env-census.mjs`), wired into `scripts/verify.mjs`, fails the build on any `git` spawn under `scripts/` that carries no `env:` at all, an `env:` built from `process.env` in any form, or an `env:` expression other than a direct call to the shared helper—one narrow, counted exemption for the hazard-reproduction test itself, which must stay unguarded by design—test: `scripts/lib/git-env.test.mjs`, `scripts/lib/git-env-census.test.mjs`.
* **Every repo-derived file this room reads is now kind-gated and bounded before it is opened, and every write is contained and crash-safe.** `scripts/lib/repo-fs.mjs` refuses a FIFO, device file, or an escaping symlink via an `lstat` check before the file is ever opened, re-checks after opening, and bounds the read; a write refuses an existing non-file target and goes through a temp-file-then-rename so a crash mid-write never leaves a half-written file. A write that falls back to an in-place open—the one path that activates only when the rename itself fails (a target another process holds open)—opens the file descriptor directly with `O_NOFOLLOW`/`O_NONBLOCK` where the platform supports them and refuses unless it resolves to a single-link regular file, so a symlink or hard link swapped in during that narrow window can no longer redirect the write; Windows lacks both flags, so the single-link check is the sole guard there, named in the code as an accepted, privilege-gated residual. Applied to every repo-derived read (the project and global config, the three doc-scanning CLI engines' own file reads) and to `configure.mjs`'s config write-back—test: `scripts/lib/repo-fs.test.mjs`.
* **CI workflow hardening, three changes.** `dependabot-auto-merge.yml` now routes the PR URL through an `env:` variable instead of interpolating it directly into the `run:` script (GitHub's own script-injection hardening guidance)—the URL is GitHub-generated on this trigger, so this closes a shape rather than a live hole. Every job across every workflow file now carries a `timeout-minutes`, sized from this room's own measured run time rather than a generic placeholder. `persist-credentials: false` is now set on every checkout step that pushes nothing (the sole exception, `dependabot-auto-merge.yml`, has no checkout step at all)—`claude-ai-zips.yml` already authenticates its own `gh release` calls via the `GITHUB_TOKEN` environment variable, never the checkout's persisted credential, so the setting is correct there too. `.gitignore` gains a new entry for CoalBoard's own session working directory—its own top-level dir when a board session touches this repo—not previously named by any of this room's existing ignore lines.

## \[0.13.0-beta.1] - 2026-09-22

### Added

* **A GitBook landing contract at the repo root (UMB-169 PHASE 2): `.gitbook.yaml` and `SUMMARY.md`.** `SUMMARY.md` lists only the public deep set—README, CHANGELOG, and all seven canary `SKILL.md` files—so nothing else in the repo becomes a public page by accident. `README.md` gains one `**Docs:**` line, placed as its own isolated paragraph, pointing at the not-yet-live GitBook site with an explicit `*(publishing soon)*` marker.
* **CoalGob joins the siblings list (SWEEP-MARKS Event 4, mark 5).** `README.md`'s badge line and doctrine paragraph both name it: OS-trash delete guard, PUBLIC BETA v0.1.0-beta.1.

### Fixed

* **A CodeRabbit trial run (CWK-120) against this room found 11 findings; 10 are closed here (nine code rows plus `PRIVACY.md:3`'s wording fix below); the eleventh is DEFERRED. 10 + 1 = 11.**
* **`PRIVACY.md`'s opening summary undercounted its own body.** It said "the one online step is yours to consent to" while the very next lines name two distinct consented online actions (the self-update check, the grounding fetch)—now "every online step," matching what the doc already enumerates.
* **`scripts/build-claude-ai-zips.mjs` reported success while attaching ZERO assets.** `plugin/skills` present but holding no skill directories fell through the existing absent-check guard silently (`Done: 0/0 skill(s) staged`, exit 0). Now fails loud on an empty skill list, the same class as the pre-existing absent-directory guard.
* **`build-plugin.mjs`'s own sync gate was silently vacuous for a whole missing `DIST_ITEM`.** `filesUnder` returned `[]` for an absent source, so deleting a `DIST_ITEM` from BOTH source and `plugin/` still reported "in sync." New `missingDistSources(srcRoot)` names every `DIST_ITEM` whose source is absent; the GENERATED-loop's own source read is now existence-checked first instead of crashing ENOENT on the same absence.
* **`scripts/configure.mjs` accepted a non-object top-level config (an array, a string, a bare number) as valid JSON and wrote it back as the config**, silently producing a file no reader can use. The read side already refused this (`config-load.mjs`); `configure.mjs` now mirrors that same guard and routes the failure into the existing malformed-config path (backup + warn + non-zero exit).
* **`codeql.yml`'s `analyze` job carried no job-level `contents: read`, so an omitted job-level `permissions:` block replaced the workflow-level grant with `none`.** Latent hardening, not a live break (CodeQL has run green on every recent HEAD); `contents: read` added, `actions: read` deliberately withheld (minimum-power, `codeql-action`'s own recommendation is for private repos only).
* **`link-check.yml` word-split and glob-expanded its own file list.** A newline-joined string was interpolated unquoted; a filename containing a space or a glob character would have broken the check (or worse, silently matched the wrong files). Switched to a NUL-delimited array (`git ls-files -z` + `mapfile -d ''`), proven red→green against a scratch repo carrying a space-named file; the pre-existing empty-list guard is preserved and now explicit rather than leaning on a pipeline exit code.
* **`configure.mjs --help --global` printed a fixed `~/.claude/.coalledger.json`, ignoring `CLAUDE_CONFIG_DIR`.** The help line now derives the path from the same `globalConfigPath()` the writer itself uses, so the printed destination can no longer drift from where the write actually lands.
* **`desc-cap.mjs`'s block-scalar scan stopped at the first blank line, undercounting a description's length past it**—an over-cap description could pass the one gate whose job is that cap. Fixed to carry blank lines through and stop only at the next top-level YAML field, with a dedicated over-reach regression test. The same truncated-scan shape in `build-claude-ai-zips.mjs`'s `replaceDescriptionField` is fixed alongside it, closing a second site that would otherwise have left an orphaned description tail in a shipped `SKILL.md` frontmatter on the next rewrite.
* **`lang-mechanics.mjs` sliced a finding's reported snippet from the MASKED line, not the raw one**—any finding whose context window touched inline code, a link, a tag, or a URL reported an unreadable run of `x` characters. The slice now reads from `raw` at the same (length-preserving) indices `mask` already guarantees line up.
* **`verify.test.mjs` turned a host-git environment difference (a git build without the lone-CR quirk) into a fail-the-whole-test result**, and the naive fix (`t.skip` + early `return`) would have silently stopped every later assertion in the same test from running (this room's own ONE-SKIPPABLE-LEG-PER-TEST rail). Split into a capability-gated characterization test that skips visibly, naming the git version, plus an unconditional test carrying the control and end-to-end proofs—both sharing one fixture helper. The skip path is exercised, not assumed: an inverted-condition copy reports `pass 6 / skipped 1` with the reason named, the unconditional test still running and passing in the same run.
* **`.coderabbit.yaml` adopted at the repo root, byte-identical from `.github/templates/published-code/.coderabbit.yaml`**—no doc names it; it is not plugin-written state, so `PRIVACY.md`'s "Local files only" list and `SECURITY.md`'s "complete write list FOR THE INSTALLED PLUGIN" both stay true unchanged.

The eleventh finding (`ci.yml`'s `paths-ignore` vs. the branch ruleset's required-status-check list) is **DEFERRED, not fixed here**—the contradiction lives in the org canon itself (`.github/SKILL-REPO-PATTERN.md` mandates the filter while also recording GitHub's own warning against it), is present in all 7 rooms, and the required-check surface is owner-held (CWK-058); a room-level edit would be the wrong seat for a pattern-level fix.

## \[0.12.0-beta.1] - 2026-09-22

### Added

* **The session-start conductor now reports where the project config came from, and a config file it will NOT read (UMB-133).** Two one-line context lines, computed by a new `configNotices` export in `scripts/lib/config-load.mjs` and shared by the Claude Code conductor and the Antigravity adapter (the adapter computes them from the payload's workspace, not the hook process's cwd): `[CoalLedger] LEGACY: <path> is a deprecated config path (still read); move it to .claude/coal/coalledger.json` when the config that won is a legacy one (one line, only for the winner), and `[CoalLedger] IGNORED: <path> is not a config path; canonical = .claude/coal/coalledger.json` for a file at a path nothing reads. The IGNORED probe is a fixed short list derived from the walk's own agent dirs—the leading dot dropped, `coal/` dropped, the legacy dotfile under `.agents`/`.gemini`, the dot kept inside `coal/`—checked by a file-existence test inside the one project root the walk resolved (a directory carrying one of those names is not a config and earns no line): no directory crawl, and a config anywhere outside that list is not reported. A sibling room's config is never reported, and neither is your own global. **Cost, stated:** a project still on a legacy path, or carrying a stray file, gets one extra context line per session start until it is fixed. **Ceiling, stated:** a fully gated-off conductor (`coalledgerMode: "off"`, or `disabledCanaries` `conductor`/`all`) returns before any notice is considered—that silence is a consent gate and stays—so `/coalledger:stats` is the channel for that user—test: `scripts/lib/config-load.test.mjs`, `scripts/lib/conductor.test.mjs`
* **`/coalledger:stats` gains a "Config in force" line.** It names the file the project config was read from, marks a legacy hit, and names any IGNORED file. It is assembled by the agent checking those paths, not computed in code (the hook's lines are the code-computed ones), which is why it is also the answer for a user whose conductor is off.

### Changed

* **`scripts/configure.mjs` now migrates BOTH legacy shapes on write (UMB-133).** Before, only a root `.coalledger.json` moved to the project's own agent-dir home; the nested `.claude/.coalledger.json` is now moved the same way, and either old file is removed only after the new one was written. A deprecation whose migration fired for one legacy shape and not the other would be two rules under one name. A hook still never migrates (Phoenix #5), and `configure.mjs` ships from no `DIST_ITEM`—test: `scripts/configure.test.mjs`

### Deprecated

* **Both legacy per-project config paths—`<project>/.claude/.coalledger.json` and `<project>/.coalledger.json` (UMB-133).** The global `~/.claude/.coalledger.json` is a different file and is unchanged.
  * **Marker + replacement:** both are marked DEPRECATED in the README's Configure section, which names the canonical path `<project>/.claude/coal/coalledger.json` verbatim as the replacement. A canonical file always wins over both, and `.claude/.coalledger.json` is read before the root file.
  * **Window:** deprecated at this MINOR, removable no earlier than the next MAJOR—this series' own SemVer boundary (`scripts-quality.md` §3), not a calendar count. Until then both keep being read, exactly as before.
  * **Owner:** CoalLedger. `node scripts/configure.mjs` moves either legacy file to the canonical path on its next write (nothing is moved on a mere read).
  * **Channel:** this entry and the README note. **No hook prints a deprecation warning**—Phoenix #13 keeps hooks to their sanctioned channels, so nothing appears in the terminal. The one runtime signal is the conductor's single `LEGACY` context line described under Added: a path report, sent only when the config actually read is a legacy one, not the deprecation notice itself.

### Fixed

* **A project config at `<project>/.claude/.coalledger.json` was silently ignored (UMB-133).** It was never a candidate in the per-project read order—only the root dotfile was honoured as legacy—so a config written where a user reasonably expects it had no effect and nothing said so. The order is now canonical (own agent dir, then `.agents`, then `.gemini`), then `.claude/.coalledger.json`, then `.coalledger.json`; first found wins, and the merge, the safer-value clamp and the global layer are untouched. A project anchored only by the nested file now resolves its own root, in both the config walk and the docs-drift tracker's own root-finder. **A second silent-dead-config shape closed in the same pass:** every candidate and every root marker is now tested as a FILE, not merely for existence—before this, a DIRECTORY named `coalledger.json` (or any other candidate name) won the walk, parsed as an empty config, and killed every setting below it with nothing said. `.git` keeps its plain existence test, since it is legitimately a directory. **A guard the change needed:** that nested path is spelled exactly like the global config, so when the project root IS your home directory it is the global, not a project file. It is never offered as a candidate or a root marker there, and never reported as IGNORED (a `CLAUDE_CONFIG_DIR` pointing at another agent dir is covered too)—test: `scripts/lib/config-load.test.mjs`, `scripts/lib/hooks.test.mjs`

## \[0.11.0-beta.1] - 2026-09-17

### Added

* **doc-quality gains a second mechanical engine, `lang-mechanics.mjs`—script-mechanics rules keyed on Unicode script range, never on a doc's declared language (CWK-101).** This unit ships the design plus one language end-to-end: ZH, two rules (`zh-halfwidth-punct`, `zh-space-before-punct`; authority GB/T 15834-2011《标点符号用法》—every rule names its standard, or it does not ship). JA/KO/CLDR/TH/EN are the design's own plan for later units, not shipped here. Markdown-aware: a fenced code block skipped whole, inline code and a link destination masked same-length, a bare `http(s)://` URL masked, an HTML comment or tag masked up to its first `>`, a blockquote line skipped whole, a 4-space-indented code block skipped whole (the same trade `emdash.mjs` accepts: a nested list item or a list-continuation paragraph indented to that depth is over-excluded too—a miss, never a manufactured hit), and a YAML front matter block skipped only when line 1 is non-blank and a closing `---`/`...` line is found before any blank line and before any fence opener, via a look-ahead—an unterminated leading `---`, a thematic-break pair around ordinary prose, or a closer sitting inside a fence is left as ordinary prose instead of silently swallowing part or all of the document; front matter that itself contains a blank line before its own closer is likewise scanned as prose, a named trade—plus the same LICENSE/NOTICE/COPYING-class-file and third-party-marker exclusions `emdash.mjs` ships. Same `BUILD_ONLY` + skill-local generated-copy shape, same exit contract (exit 0 on findings, exit 1 only on an unreadable file). Wired into `skills/doc-quality/SKILL.md` as Method step 2c, alongside the existing em-dash step 2b—test: `scripts/lib/lang-mechanics.test.mjs`.

### Changed

* **The house's own docs converted to `emDash: unspaced`, the owner's decided convention (AH-1, 2026-09-03; sweep UMB-042).** 444 spaced em-dashes across the 14 shipped root/skill/command docs converted to unspaced (443 persist—one was later restored inside a code span, LOW-5), prose only—fenced/inline code, URLs, blockquotes, Thai lines, and the third-party marker excluded exactly as `emdash.mjs` excludes them at scan time. The factory default stays `off` in both `config-schema.mjs` and `platform-configs/.coalledger.json`—this is a house choice recorded on this room's own docs, never forced on another repo. `--mode=unspaced` now reads `TOTAL: 0` across all 14 files.

### Fixed

* **The shared CLI/entry-point guard failed OPEN through a junction or a symlinked path, across three more shipped engines (MED-3, r34 INSPECT).** `plugin/skills/doc-structure/lib/md-checks.mjs`, the top-level `plugin/scripts/lib/md-checks.mjs` copy, `plugin/skills/doc-quality/lib/emdash.mjs`, and the new `plugin/skills/doc-quality/lib/lang-mechanics.mjs` all compared a LEXICAL `process.argv[1]` against `import.meta.url`—reached through a symlinked `~/.claude` or a Windows junction, every one of them printed nothing and exited 0, a silent clean bill. Fixed by comparing both sides through `fs.realpathSync.native`, fail-closed on an unresolvable path—test: `scripts/lib/main-module-guard.test.mjs`.
* **README's CoalMine sibling links still pointed at `HetCreep/CoalMine`, the repo's pre-transfer address (CWK-108).** The owner moved CoalMine into the org 2026-09-17; the old address still redirects (verified live: `GET /repos/HetCreep/CoalMine` → 301 to repository id 1259836955), but a shipped doc names the real address, not a redirect it happens to survive on. Four links in `README.md` (the siblings line, twice in the front-door prose, and the closing sibling list) now point at `https://github.com/TheColliery/CoalMine`. `plugin.json`'s `author.name: "HetCreep"` is untouched—that names the account, not the repo.

Two riders landed in this unit's own commit sequence, correctly outside this entry: **LOW-3** (a CLI import-guard fix, `scripts/configure.mjs` and `scripts/build-claude-ai-zips.mjs`) and **LOW-4** (naming `.githooks/` in `CONTRIBUTING.md`, r33's own finding)—both touch files with no `DIST_ITEM` path, so neither of these two riders moves the shipped dist, unlike the guard fix above; neither owes a CHANGELOG entry of its own, per this room's own no-dist-no-version rule. They are recorded in their own commit messages and the room's `MEMORY.md`.

### Security

* **CodeQL now also scans this room's own GitHub Actions workflow files, not just `javascript-typescript` (CWK-112 part 1).** `codeql.yml`'s `init` step gained a second language on the same job (`languages: javascript-typescript, actions`)—one job, no matrix leg, per the chief's flock-wide ruling: a matrix would rename the `analyze` job and leave the branch ruleset's required status-check context permanently unreported (the CWK-053 class). Both `paths-ignore` comments claiming "CodeQL analyzes js/ts only" are now false and are corrected to name both languages; markdown still needs no scan, so `**.md`/`LICENSE` stay ignored.

## \[0.10.0-beta.1] - 2026-09-10

### Added

* **doc-quality gains a config-keyed em-dash typography rule (CWK-073), the house convention landing as an ONGOING gate rather than a one-time hand sweep (CWK-062).** New key `emDash: "unspaced" | "spaced" | "off"` (factory `off`—a house choice, never forced on another user's repo): `unspaced` = the house form `word—word`, so a SPACED em-dash is the finding; `spaced` = the inverse; `off` = the rule never runs. Report-only, choice-gated fix menu, mechanical (Quick) layer—no new consent surface.
* **The engine (`scripts/lib/emdash.mjs`) moved from `scripts/emdash.mjs`, gained a `mode` parameter, and now ships INSIDE the skill** at `skills/doc-quality/lib/` (generated, byte-identical to its one source, the same self-contained-skill shape `doc-structure` already ships for its own AST engine)—the skill now works even when it travels alone. **The polarity needed care, not a straight port:** the CWK-062 instrument's own hardcoded default (flag an UNSPACED em-dash, treat SPACED as correct) is the OPPOSITE of the house's adopted convention—confirmed against CWK-062's own verification method, which greps for the SPACED form and expects zero after its sweep. `scanText`'s default parameter preserves the original direction unmodified (mode `'spaced'`, so all 17 pre-existing selftest cases still pass byte-for-byte); `'unspaced'` is the new, correct polarity doc-quality's Method actually invokes.
* **Exclusion classes widened past the original two-name `LICENSE`/`NOTICE` set:** a case-insensitive basename glob now also covers `COPYING*` and any `docs/license.md`-class rendering (matched by basename, not a directory-scoped path, so it reaches a vendored legal file regardless of which directory holds it)—narrowed to a `.`-separator only after a false-EXEMPTION was caught in testing (a `-`-separator glob would have silently exempted an ordinary file like `notice-of-changes.md`; the residual miss in the OTHER direction, a dual-licence file like `LICENSE-MIT`, is named in the code's own header rather than silently left one-sided). A new literal marker on its OWN LINE excludes a WHOLE document that carries verbatim third-party wording without a legal-shaped filename—naming the marker in ordinary prose or inside a code span does not trigger it, only a standalone occurrence does. **Blockquote-prefixed lines are RULED excluded** (the one mechanically visible quotation marker Markdown has); an inline quotation with no blockquote marker stays a stated, undetectable limit—the caller adjudicates, the instrument does not guess.

### Fixed

* **`scripts/build-plugin.mjs`'s `BUILD_ONLY_LIB_NAMES` gains `emdash.mjs`**—nothing imports it from the wholesale `scripts/lib` dist copy (no hook needs it), so it is excluded there while remaining the SOURCE for its own generated skill-local copy—the first file in this room to be both at once—test: `scripts/lib/emdash.test.mjs`
* **The third-party marker check was a raw whole-document `String.includes`, so any doc merely NAMING the marker in prose silently exempted itself from the entire em-dash rule (INSPECT r31 MED-1).** This room's own `CHANGELOG.md` entry describing the feature above wrote the marker literally to illustrate it—which switched the rule off for this whole 61 KB file (157 findings under `mode=unspaced` collapsing to 0, both polarities, no signal). Fixed to a line-anchored regex: the marker excludes a document only when it occupies its own line (optionally with surrounding whitespace); naming it in running prose or inside a code span no longer disables the file—test: `scripts/lib/emdash.test.mjs`

## \[0.9.0-beta.1] - 2026-09-03

### Added

* **`verify.mjs` now fails the build when a scanned surface names `.coalledger.json` beside a config key without naming the global+project cascade—a BARE, unclamped read path (CWK-064).** It mechanises a flock convention the owner authorised as convention rather than by press: **ONE CONFIG-READ PATH PER ROOM—no key is read from a bare project file, by hook or by agent instruction.** Ships a fourth declaration list (`READ_PATH_EXCEPTIONS`) with the same EVENT-expiry discipline as the other three: an entry whose mention vanished FAILS, an entry protecting nothing FAILS, so it cannot rot into a bypass. **Name its blind spot honestly:** the predicate needs the filename and a key on the same line, so a surface that discusses a key WITHOUT naming the file is invisible to it—that is how `scanEverything`'s own two bare-read claims survived until INSPECT found them by hand.

### Fixed

* **7 lines across 6 `SKILL.md` told the agent to read the BARE project file; the gate reddened on 8 findings against HEAD** (`doc-leak:14` fires twice, for two keys)**—all 8 now name the cascade**, matching `commands/stats.md:11`'s existing wording. **Why it mattered, plainly:** the owner's `~/.claude/.coalledger.json` carries `scanEverything: true` with no project file, so an agent following the old lines read an absent file and missed the setting entirely. Also corrected: `scanEverything`'s schema `help:` and template comment, which said the key is read *"from the project file directly, never through loadMergedConfig()"*—true about what the CLAMP can ENFORCE, wrong as a READ PATH, and three entries from `severityFloor` saying the opposite about a key read in the same breath. **The bound survives in both:** the gate closes the convention's REGRESSION risk mechanically; the instruction the agent follows is still PROSE-STRENGTH—`hooks-safety.md` §9's clamp has no path to an agent-read key, and that residue is stated on every corrected surface, never claimed closed.

## \[0.8.0-beta.1] - 2026-09-03

### Added

* **A `verify.mjs` gate that fails the build when ship-text names a config key that does not exist in the schema (CWK-060, ported from CoalMine's CWK-059/061—four rooms hit this defect class in one night, ours among them).** It scans **8 doc surfaces (the 7 `SKILL.md` + `README.md`) and 4 hooks**, plus a structured pass over README's Configure key table where a first-cell backtick is a key claim regardless of shape; `CHANGELOG.md` is deliberately OUT—it names retired and planned keys BY DESIGN, so a red there would fire on accurate history. **It under-fires on purpose.** Detection needs an internal capital, so a single lowercase key is invisible to it—measured on our own surfaces: a naive rule flags 144 tokens / 133 non-keys (92% noise), the capital rule 12 / 2. A gate that cries wolf is a dead gate; a miss is a bug, a flood is worse. `language` is that blind spot and it is DECLARED, not hidden—an undeclared shape-invisible key is a hard FAIL, and the pass line reads "every DETECTABLE config key" with a visible SKIP naming what it could not see. Three allowlists expire on EVENTS rather than dates: a planned key that now exists FAILS, a never-a-key that becomes one FAILS, a declared blind key the rule can now see FAILS, and a declaration nothing references FAILS—so they cannot rot into bypasses. The proof is SYNTHETIC, and this says so: our history has never contained this defect class (checked per key—the schema-add commit and the first ship-text commit are the same commit, 11/11).

## \[0.7.0-beta.1] - 2026-09-03

### Added

* **`scanEverything`, a boolean config key that bypasses `severityFloor` for one run—report everything down to `low`—and nothing else (CWK-057, owner's antivirus-scope law; default `false`, positive polarity: `true` = MORE disclosure, never a double negative; one flock one colour with CoalMine's key of the same name/polarity/default, shipped the same day).** CoalLedger has NO automatic scan-scope cut to bypass at all—every canary is agent-invoked, never auto-batch-scanned the way CoalMine's touch/stop pipeline is—so the key's honest reach is narrower than its name suggests elsewhere in the flock: it does NOT re-enable a canary turned off via `disabledCanaries`/`docLeak`/`coalledgerMode` (those are ON/OFF consent switches, not scan scope), and does NOT force `quickVsFull` to `full` (that would escalate PAID spend through a scope-widening key). A consent-cascade clamp was added and is correct against `mergeSafety` by test, but it is honestly OUT OF REACH on every shipped route: like `severityFloor`, this key is read by the AGENT from the project file directly, per SKILL.md prose, never through the global+project consent cascade—so it is an instruction the agent follows, not a gate the code enforces. All 7 SKILL.md carry the bypass in their own "then honor `severityFloor`" step, and each carries the disclosure in the same clause: when the key is on, the run says `severityFloor` was bypassed—never that every scope cut was, since these canaries have none to bypass.

## \[0.6.0-beta.2] - 2026-09-02

### Fixed

* **The docs memory-drift tracker went silently blind on any project rooted under `os.tmpdir()`—a CI runner workspace, a container build dir, a `mktemp -d` checkout, or this org's own `%TEMP%/claude/<project>/` convention (CWK-054, INSPECT-reproduced with a control row proving the probe wasn't dead).** `hooks/coalledger-doctrack.js`'s temp-scratch exclusion tested only whether the EDITED FILE's path sat under tmp, never whether the file's own PROJECT lived there too—so in that whole workspace class every doc edit, and the `MEMORY.md` satisfier gated by the same check, was silently dropped: a session with a genuine drift obligation produced output byte-identical to one where nothing was edited. Fixed by widening the predicate to exclude a file only when it is under tmp AND its own project root is not (a `findProjectRootLocal` walk mirroring `config-load.mjs`'s marker set, `.git` included)—an ordinary throwaway scratch file outside any project is excluded exactly as before; a real project that merely happens to be checked out under tmp now tracks its docs and sets its satisfier normally, in this room's hook layer. CoalMine's own code-pole fix for the identical law—a suppressed scan must not read identical to a clean one—shipped the same day.

## \[0.6.0-beta.1] - 2026-09-02

### Added

* **`scripts/configure.mjs` — CLI config editing (CWK-023, owner-signed ใบ D: `configure.mjs` is now a flock standard).** Every `.coalledger.json` key is now settable from the command line, not just by hand-editing the file: `node scripts/configure.mjs --<key> <value>` writes the project config, `--global` writes the global layer, `--help` lists every flag with its current default (both derived from the single schema table in `scripts/lib/config-schema.mjs`, so a key added there is automatically settable and documented). Ported from CoalMine's own `configure.mjs`, same shape, adapted to this room's own config-load/jsonc libraries rather than duplicating them. Also user-visible: a project with no config anywhere yet now gets written to the first agent dir it **already has** on disk (`.claude` → `.agents` → `.gemini`) instead of always defaulting to `.claude` — the same fix CoalMine's own installer already carries, ported here because adding a writer is what makes the old always-`.claude` fallback actually plant a foreign directory into a project that has never used Claude Code.

### Fixed

* **`README.md`** **`## Permissions` and `SECURITY.md`'s write list stated a promise the new CLI broke (CWK-023 MED-1/LOW-1, found by INSPECT).** Documenting `scripts/configure.mjs` in the README left that same README promising "Writes only its own scratch" and "no file is ever deleted"—both false once a writer exists that writes the project config, writes the global config on `--global`, writes a `.bak` beside a malformed config, and deletes the LEGACY root `.coalledger.json` on migration (reproduced in a sandbox by the reviewer, not inferred). The text was corrected, never the code: both surfaces now scope their promise to THE INSTALLED SKILL in the lead itself—`configure.mjs` ships from no `DIST_ITEM`, so an installed-only user never receives it—and name the checkout-only writer separately with exactly what it writes and the one condition under which it deletes. Also corrected in the same pass: the `## Configure` fallback sentence claimed a bare `.claude` is "never" planted, but `ownDirDefault` returns `.claude` when NONE of the three agent dirs exists; it now states both halves.

### Changed

* **Sharpened the `quickVsFull`/`severityFloor` "out of reach" wording after a real reader inverted it (CWK-038).** The prior text was technically correct—the consent-cascade clamp cannot PROTECT either key—but a fresh reader (the CoalWorks chief) came away believing the clamp had made them "permanently unsettable" and cut a ticket to restore settability they never lost. Fixed at all three sites a reader meets these keys (`config-load.mjs`'s OUT OF REACH block, `config-schema.mjs`'s `help:` for both keys, the `.coalledger.json` template comments) to state the direction explicitly and first: both keys ARE fully settable per project, with or without a global present—what is absent is protection, never permission. No reasoning was cut; this is the room's own "a claim propagates through prose exactly like through code" lesson, and this ticket is the evidence for it.

## \[0.5.0-beta.3] - 2026-08-31

### Security

* **Parser DoS: the engine's own `walk()` overflowed the call stack on a doc well under the size cap, and the code's own comments/tests denied the vector (CoalBoard board U13/F1, HIGH—judge-reproduced).** `md-checks.mjs`'s pre-parse guard checked SIZE (512 KB) and NUL bytes only, before handing the tree to a RECURSIVE `walk()` (one call frame per tree level); an 8 KB doc (0.016× the size cap) with 5,000+ nested `>` markers threw `RangeError: Maximum call stack size exceeded`—reproduced live before the fix. Worse, the CLI's `--json` batch path ran `checkDocument` with no try/catch, so one crafted doc crashed the whole run to empty stdout—the exact false "0 findings" clean bill `doc-unreadable` exists to prevent. Two root fixes, not a patch over the symptom: (1) `walk()` rewritten iterative (explicit heap-allocated stack, not the call stack)—it cannot overflow on depth alone, whatever the input; (2) a new pre-parse container-depth guard, check id `doc-too-nested` (a third pre-parse sibling to `doc-too-large`/`doc-unreadable`, tuned to real-document headroom—a genuine 12-level email-quote thread scans clean, the cap sits \~17× above that), catches pathological nesting before the parser even runs. The CLI batch path now survives a crafted doc between two real files with no lost findings. The engine's own prior comments ("doc-too-large is the parser-DoS root fix", "transitive vector closed") and a vacuous test that only exercised 3-4 levels of nesting are corrected to state the depth axis is a SEPARATE guard from the size one.—test: `scripts/lib/md-ast.test.mjs` ("deep blockquote nesting... does not crash the tree walk", red-first against the pre-fix recursive form), `scripts/lib/md-checks.test.mjs` ("U13/F1: doc-too-nested fires...", "the CLI batch survives a crafted doc...", plus two findings-back regression tests for a list-marker bypass and a fenced-code false-positive)
* **`.docs` session tracker followed a symlink at the destination on every accumulating write; its sibling one-shot marker already refused this (board U13/F2, LOW/SUSPECTED).** `coalledger-doctrack.js`'s `.docmemmoved` marker uses an atomic `wx` create that refuses to write through a pre-planted symlink; the `.docs` accumulator (one doc path appended per line, all session) used a plain `appendFileSync`, which follows a symlink at the destination—`wx` isn't usable there without breaking accumulate-across-the-session semantics. Fixed with an `lstatSync` check (does not follow a symlink) immediately before the append, refusing the write if the destination is a symlink while preserving normal accumulation otherwise. **Residual, named rather than hidden:** a symlink planted in the narrow window between the `lstatSync` read and the `appendFileSync` write (TOCTOU) is not closed by this check—accepted at LOW given the sid-scoped unpredictable filename, doc-path-string-only content, Windows symlink creation being EPERM-blocked without admin, and `fs.constants.O_NOFOLLOW` being unavailable on Windows (verified live on this box) so no portable close exists.—test: `scripts/lib/hooks.test.mjs` ("board U13/F2: proves the lstat symlink guard did not break append-accumulate semantics")
* **`severityFloor` DOCUMENTED as outside the consent-cascade clamp's reach, not clamped (board U13, item 4—hooks-safety.md §9 THE CEILING).** Every reader of `severityFloor` is the agent (the "honor severityFloor" step in all 7 SKILL.md + `commands/stats.md`)—no hook ever sees it, so a merge-layer clamp here would be coverage that only looks like coverage. Clamping it toward the schema default would also break the ordinary user who legitimately sets a HIGH floor on their own noisy repo, since an absent global already resolves to that same default. Given its own reasoned entry (previously sharing one parenthetical with `language`/`publicMode`, which have no such blast) in `config-load.mjs`, `config-schema.mjs`'s `help:`, and the `.coalledger.json` template—matching the disposition `quickVsFull` already has. The residual is named, not implied covered: a cloned repo's project file can still narrow what a run reports; `/coalledger:stats` surfaces the effective floor so this stays inspectable.—test: none (documents an existing read path; no behavior change)

### Fixed

* **`doc-too-nested` (the container-depth guard above) shipped with no user-visible surface—the room's own second miss on the exact same table (board U13/M2).** `doc-too-large` was itself missing from `doc-structure/SKILL.md`'s `## Checks (engine ids)` table until backfilled at `225eb55` (2026-07-30); the new sibling repeated the gap. Added its row to the Checks table and named it in the CLASSIFY-BLOCK read row's enumeration (was "same posture as `doc-too-large`/`doc-unreadable`", now includes `doc-too-nested`).
* **`doc-standard/SKILL.md` claimed engine-determinism ("via the AST") it cannot have—doc-structure is this room's sole engine home; doc-standard ships SKILL.md only, no `lib/` (board U13/M-1, confirmed by INSPECT across all 4 sites).** Fixed at its description, Method step 2, the CLASSIFY-BLOCK read row (which also held a live grant/claim contradiction—`Read`·`Grep`·`Glob` cannot execute a parser), and the Multilingual section: all now state the mechanical layer is an agent read by structure/position/meaning, never an English keyword—the true language-neutral half is kept, only the AST attribution is removed. The same shape was found and fixed a third time in `README.md` (the suite-wide "mechanical layers are language-agnostic (an AST does not care...)" line attributed AST to every mechanical layer; now scoped to `doc-structure` alone) and `README.md`'s "Detection is mechanical; severity never is" line (a false universal—`doc-consistency` and `doc-leak` are semantic-only, no mechanical layer at all; the severity half was true and is kept).
* **`scripts/lib/md-ast.mjs`'s header named no CommonMark spec version and pointed at a file a dist consumer never receives.** "0.31.2" lived only in this CHANGELOG and the gitignored `MEMORY.md`; the header's own "blueprint §7" pointer targets `COALLEDGER_BLUEPRINT.md`, which is not a `DIST_ITEM`. Pinned `CommonMark 0.31.2 + GFM` in the header, stated the genuinely in-house origin (zero-import, zero-network, not derived from a third-party parser), and marked both blueprint pointers as dev-only design records rather than something an installed plugin ships.

## \[0.5.0-beta.2] - 2026-08-22

### Changed

* **`doc-structure` SKILL.md body leaned (campaign #6, belt 2—CB v2.3.1 · CT v1.5.1 · CW v1.3.1 · CF v0.7.3 shipped the same pattern first).** Measured whole-product first via `claude plugin details coalledger`, source byte-verified against the installed cache before reading anything (all 7 skills matched). Per-skill body, not whole-product, is this room's shape—confirmed already documented as passing the \~5k-token body budget with wide headroom (`skill-authoring.md` §3b's own reference table: \~1.1k–1.4k on-invoke). Real residue found in exactly one place: the `## Checks (engine ids)` table's `heading-duplicate`/`table-ragged`/`ref-undefined` rows each mixed a genuine rail (what fires the check, the changelog exception) with a pure mechanism-explanation aside that survives elsewhere—the engine emits the identical slug-mechanics explanation in every live finding (verified by running two real repros), and the explanation is separately anchored in `scripts/lib/md-checks.mjs`'s own standing warning and `CHANGELOG.md`'s prior entry. Cut the asides; kept every rail, including the explicit non-flag exception. `doc-structure` body: 6,920 → 6,593 chars (LF-normalized, frontmatter-excluded), **-4.7%**. The other 6 skills carry no comparable candidate—checked cell-by-cell, none touched. **Honest frame:** this room's residue was never over budget to begin with, unlike the four whole-product skills this campaign started with; the finding here is a small, real cut, not a forced one—declaring "already lean" would have been equally honest had the one candidate not existed.
* **Variance-walked in two rounds, scope confined to the touched table cells per `skill-authoring.md` §3b's SCOPE-FOLLOWS-THE-DIFF rule** (5 leaves each: 2× weak/haiku, 2× medium/sonnet, 1× strong/opus; report-only, zero prior context). Round 1 (`heading-duplicate`): 5/5 identical on both the firing condition and the changelog exception. Round 2 (`table-ragged`+`ref-undefined`, added after INSPECT found the pass had applied its own criterion to one row of three and not the other two): 5/5 identical on both rows across all three tiers—the removed facts (a browser silently drops surplus cells; an undefined reference renders as literal brackets) are common markdown-domain knowledge, volunteered unprompted by more than one leaf from general knowledge alone. Zero variance both rounds; no declared bound needed.
* INSPECT (this room's own reviewer resident, `93b912a4`, confirmed alive and dispatched without `--allowedTools` after last unit's `--resume` incompatibility was isolated) argued the cut from both sides before ruling non-behavioral: the removed text does contain a scope-sounding fragment ("the check itself is sibling-scoped"), but the surviving row already states that scope affirmatively, the agent never re-derives this check by design (`Method` step 1's own prohibition), and the full slug-mechanics explanation reaches the agent at the moment it matters—inside the engine's own finding message, verified live. SHIP verdict, plus two LOW findings (both closed same pass) and two named-not-fixed candidates in other skills' TIER lines, judged weak-value and left for scope discipline rather than expanding this unit's diff into two more lanes.

## \[0.5.0-beta.1] - 2026-08-19

### Added

* **`## Grants & denials (CLASSIFY-BLOCK)` on all 7 canaries (gold-standard F22).** Every skill now declares, per step class it actually uses, which tool grants it needs and what happens on a denial—a permission refusal reaches the worker as a visible message but propagates no further (never to the dispatcher, never as a catchable condition in the skill's own logic), so without a stated branch a denied step is silently indistinguishable from one that simply found nothing. Branch idioms follow `CoalMine/skills/gold-standard/SKILL.md`'s own shipped shape (`skill-authoring.md` §5b states the requirement, not a wording template): read → refuse that file, report it unscanned, never a false clean bill (doc-structure's own `doc-too-large`/`doc-unreadable` idiom, reused as the read row across all 7); write → report + courier the intended change, never claim applied. doc-structure's read step additionally needed a `Bash` grant, not `Read`/`Grep`/`Glob`—the AST engine runs via `node ./lib/md-checks.mjs`, and the skill's own Method step 1 forbids the only fallback ("never re-derive these checks by reading markdown yourself"); every write row's checkpoint (`git stash`/`commit`) needed the same `Bash` annotation—both caught by this unit's own INSPECT, not assumed correct from the shape alone. doc-grounding and doc-standard's pre-existing network-degrade prose (`⚠️ unverified: check [source]`) is pointed at, not restated. No skill in this room spawns—no `spawn` row anywhere. doc-leak's write row names its own stakes explicitly: a "redacted" that was never actually written is a shipped secret. Method/Fix-mode/Output/Escalation-boundary sections are untouched, but the denial path itself now carries real new rails (never report a denied step as done, skipped, or clean)—that is a behavior change on the unhappy path, not merely documentation.

## \[0.4.0-beta.1] - 2026-08-16

### Added

* **claude.ai ZIP packaging via CI (board #40, ported from CoalMine—the flock exemplar).** A new `.github/workflows/claude-ai-zips.yml` builds one ZIP per canary on every version tag and attaches them to the GitHub Release as assets, so a claude.ai user downloads a ready-to-upload ZIP instead of hand-zipping a skill folder. The build (`scripts/build-claude-ai-zips.mjs`, backed by new `scripts/lib/claude-ai-trim.mjs`) deterministically trims each canary's frontmatter `description` to claude.ai's 200-char skill-listing cap (our own cross-platform cap is 1024, `scripts/lib/desc-cap.mjs`)—a DERIVED artifact staged by that script into a gitignored directory; the source `skills/*/SKILL.md` files are never edited. Each Release also carries a `SHA256SUMS.txt` covering every ZIP for integrity verification (board #99's fix, inherited). README's claude.ai paragraph now points at the Releases page instead of instructing a hand-zip, and names the real problem it fixes: this room's own descriptions run up to 895 chars, well past claude.ai's cap, so a hand-zipped folder was never guaranteed to work there.
* **`scripts/lib/desc-cap.mjs` extracted from `verify.mjs`'s own former inline copy** (frontmatter parser + the 1024-char cap), matching CoalMine's actual current shape rather than duplicating the parser a third time for the new build script. Behavior-preserving—`verify.mjs`'s checks are byte-identical in output, only their source moved.
* **The workflow will not publish a GitHub Release for a pre-release tag—found by this board's own independent INSPECT before shipping.** CoalMine's exemplar has no such guard because CoalMine has never cut a beta tag; CoalLedger's entire tag history (13/13) is pre-release, so the unguarded port would have created this room's first-ever GitHub Release for a beta and, with no `--prerelease` flag, marked it "Latest". A new gate step checks `GITHUB_REF_NAME` for a `-` (SemVer's own prerelease marker) and skips every Release-touching step when present; the build/zip/checksum steps still run so the workflow itself stays exercised. **Live-confirmed at `v0.4.0-beta.1`, this release's own tag**, not just claimed: the gate fired, the six subsequent steps show `conclusion: skipped` in the run, and zero GitHub Releases exist on this repo afterward—checked via the Releases API, not assumed.
* **Verification gap, now closed for the gate/skip path, still open for the eventual publish path.** `build-claude-ai-zips.mjs` was run locally against all 7 skills before shipping (7/7 staged, every staged `SKILL.md`'s frontmatter independently re-parsed and confirmed valid), and the workflow's first live run (`v0.4.0-beta.1`) confirmed the trigger, the gate, and the correct skip—genuinely validated, not hand-waved. What remains unvalidated: the ZIP/checksum build and the actual Release-attach steps, since every tag cut so far (including this one) is a pre-release and skips them by design. That path validates itself the first time this room ships a stable tag.

## \[0.3.0-beta.6] - 2026-08-13

### Security

* **The consent-cascade clamp (hooks-safety.md §9) skipped clamping whenever the GLOBAL side of the merge was undefined—the common case, since most users never write `~/.claude/.coalledger.json` (board #111).** A project config arriving inside a cloned repo could set `updateMode` to `auto` (standing consent to network-check for updates) with zero clamp when no global config existed anywhere. Fixed: an absent global now substitutes its schema default as the clamp ceiling, per hooks-safety.md §9 ("a global sitting on the factory default... leaves the project free to set anything" is the hole, not a rule—the comment defending it is removed along with the defect). Real bite is `updateMode` only (schema default `'ask'` sits below the enum's loudest `'auto'`); `coalledgerMode`/`docLeak` already default to their own enum's ceiling (`'auto'`/`true`), so this closes the mechanism structurally for them without changing today's observable behavior. Matches the sibling-room fix already shipped in CoalBoard/CoalFace/CoalTipple's conductors and CoalHearth's own `config-load.mjs` (four of the five rooms `hooks-safety.md` §9 names; CoalMine was found still holed during this board's own INSPECT and is reported separately, out of this room's jurisdiction to fix).
* **Independent review of the fix above (same board) found it incomplete: an INVALID value on either side of the merge also bypassed the clamp entirely, and this hole was wider than the one just closed—it needed no absent config, just a typo, and defeated an EXPLICIT global.** A project value that fails the enum lookup (`'yes'`, `null`, a stray space, any typo) fell through the old `if (gi === -1 || pi === -1) continue;` unclamped, leaving the raw junk for `clampedRead` to independently resolve to the SCHEMA DEFAULT—never to the global the user actually set (`{coalledgerMode:'off'}` + a project `{coalledgerMode:'yes'}` produced `'auto'`, not `'off'`). Fixed the same way CoalWash already had it (`scripts/lib/config-load.mjs`'s K1 fix, cited by name in the code): an invalid project value now gets no say at all (treated as absent, the effective global stands); an invalid or schema-drifted global value falls back to its own schema default's index, then to the safest index as a last resort.

### Fixed

* **The docs memory-drift Stop advisory could silently discard a resident's real final answer under `-p --output-format json` (board #82).** The hook emitted the note via `hookSpecificOutput.additionalContext`, which—specifically on the Stop event, not on SessionStart/UserPromptSubmit—forces Claude Code to run one more agent turn to "digest" the injected context; under `-p --output-format json` that extra turn REPLACES the real final `result` with whatever the model says in reaction (observed: a genuine answer discarded, `result` came back empty). The hook now emits the identical text via `systemMessage` instead, which does not force that extra turn and still reaches the same surfaces (the session transcript, an interactive user). Firing conditions (coalledgerMode/disabledCanaries/docsDriftNudge gates, the MEMORY.md-update check) are unchanged.

## \[0.3.0-beta.5] - 2026-08-09

### Added

* **Per-project config now lives under an agent dir, never bare at the project root (namespace campaign #69+#39).** Read order, first found wins: `.claude/coal/coalledger.json` (the running agent's own dir—the config loader takes no agent-identity signal, so this always collapses onto `.claude` whether Claude Code or the ported Antigravity adapter is running) → other known agent dirs, `.agents/coal/coalledger.json` → `.gemini/coal/coalledger.json` → LEGACY: a root `.coalledger.json` (the pre-2026-08-08 shape), still read normally so an existing user's config keeps working—backward-compatible, nothing breaks. The machine-global self-update throttle stamp moved the same way, to `~/.claude/coal/coalledger/update-check`, reading an old `~/.claude/.coalledger-update-check` once as a migration fallback and then dropping it on the next write. The global config itself (`~/.claude/.coalledger.json`) is unchanged.

## \[0.3.0-beta.4] - 2026-08-08

### Fixed

* **All 7 canaries now wire Claude Code's `ReportFindings` panel when it's callable (board #68)**—severity prefixed in `summary` (`[HIGH] …`), SUSPECTED as `verdict: PLAUSIBLE` (doc-leak: every finding, no exception—it has no CONFIRMED tier), ranked most-severe first, chat carries only a wrap-up line + the fix menu, never a restatement. Not callable → the existing full text table, unchanged; no host detection, failure IS the fallback. An Apply-fixes click routes to each skill's own existing safe-fix tier where one exists (doc-structure, doc-grounding, doc-rot, doc-quality); the three skills with no mechanical safe tier (doc-standard, doc-consistency, doc-leak) degrade the click to their existing pick/draft/propose path instead of inventing an auto-apply that was never safe. After any fix round, the same findings re-report with `outcome: fixed`/`skipped`/`no_change_needed`—no new fix-mode behavior, only how it's surfaced.
* **The 6 semantic canaries' "whole repo docs" SCOPE default was a vague phrase; doc-structure's default silently missed `.mdx`/`.markdown` (board #50).** Grounded into the room's own already-shipped `DOC_EXTS` list (`hooks/coalledger-doctrack.js`): the 6 semantic canaries now default whole-repo scope to the full 8-extension set (`.md`/`.mdx`/`.markdown`/`.rst`/`.txt`/`.adoc`/`.asciidoc`/`.org`); doc-structure narrows to the Markdown-family subset (`.md`/`.mdx`/`.markdown`) since its engine is a CommonMark+GFM parser and cannot meaningfully read the other five dialects. Named/touched-files scope is untouched.

## \[0.3.0-beta.3] - 2026-08-06

### Fixed

* **The docs memory-drift Stop advisory unconditionally COMMANDED "update MEMORY.md"—including to a production-line station worker forbidden by org law to write it.** The line now ROUTES instead: the session owner is told to update MEMORY.md before ending; a station worker who cannot write it is told to report the drift in their return instead. The drift signal itself is never lost—it still fires on the same conditions, only the requested action changed.
* **`.claude-plugin/plugin.json`'s `description` never mentioned the docs memory-drift reminder**, and PRIVACY.md's "Local files only" bullet named only 2 of 5 real state files (the update-check stamp and the config), omitting the docs-drift session state (`.docs`/`.docmemmoved`) and the Antigravity conductor marker. The description now names the reminder explicitly as NOT a canary (no scan, no findings—6+1 stays 6+1); PRIVACY.md's list is now complete—the same four write targets SECURITY.md's own Structural Safety enumeration names, plus the config file, which PRIVACY.md additionally covers because a user can read it (SECURITY.md's list is write targets only, and CoalLedger never writes its config).

## \[0.3.0-beta.2] - 2026-07-30

### Added

* **`doc-structure` catches images with no alt text (`image-alt-missing`).** Design credit to community contributor **mehvetero**, who first raised this gap (#11, #12)—this project's own contribution is the classification: the finding is **SUSPECTED-only, always, with no config toggle**. An empty `alt` is the WCAG 1.1.1-*correct* choice for a purely decorative image, so the engine can confirm alt is empty but never whether that is right; only a human can tell decorative from content. Covers `image` and every `imageReference` form (full/collapsed/shortcut), AST-native so a code span or fenced block showing `![](x)` as documentation stays silent, and whitespace-only alt counts as empty. The finding shape gained a backward-compatible `suspected` field—every existing CONFIRMED check omits it, unchanged. Message: `image has empty alt — intentional for purely decorative images (WCAG 1.1.1); a content image needs a description (MD045)`.
* **`doc-structure` catches duplicate headings (`heading-duplicate`).** Two headings under the same parent section with the same text make their anchors ambiguous: slugs are handed out in document order, so `#slug` goes to whichever heading first claims that slug anywhere in the file—which may not even be one of the two duplicates, since an earlier heading with different text can claim it first—and later claimants become `#slug-1`, `#slug-2`, shifting the moment a heading is inserted above them. **Siblings-only by design**—a `### Added` repeated under different `## version` headings is the keep-a-changelog format, not a defect, so this project's own `CHANGELOG.md` still scans clean. The key is the heading's rendered text, not its slug, so two headings whose text differs while their slugs collide (`Setup!` and `Setup` both slug to `setup`) are deliberately not reported—a documented limit. The anchor quoted in the finding is the engine's own GitHub slug, so a Thai, CJK or accented heading names the anchor you would actually type. Engine in `scripts/lib/md-checks.mjs`; the planted-defect fixture and the clean decoy each gained a case, and the unit suite pins the exact check ids and line numbers.

### Changed

* **"Part of TheColliery" is readable again—it was one unbroken 1,007-character paragraph.** Six sibling links, the compose promise and the shared doctrine were welded into a single block sitting right before the License, i.e. the last thing a visitor reads. Same content, now in the flock-canonical shape: a one-line lead, the six siblings as a list (one per line, with each one's role), the compose promise plus CoalLedger's own health-layer line, then the doctrine paragraph. The closing line keeps its "offline **by default**" wording—a deliberate difference from the siblings' plain "offline"—and now names the reason on the spot: the consent-gated Full tier's source verification (`doc-grounding`, `doc-standard`) and the self-update check go online, while the shipped hooks and engine never do.
* **The README now documents what you can invoke and what CoalLedger is allowed to do.** Two required sections were missing: `## Commands` (every invocable surface with its exact form—the seven canaries, `/coalledger:stats`, `/coalledger:update`—and a note that slash commands are the Claude Code form) and `## Permissions` (what it reads, the scratch it writes, the one local engine it runs, and the things it deliberately never does: no doc edited without your pick, no deletions, no subagents, no network outside the consented Full tier and update check). Neither adds behaviour; both describe what already shipped.

### Fixed

* **The file-copy install instructions taught a layout that could not work.** They said to copy the repo's `skills/` and `scripts/lib/` "keeping the relative layout (each SKILL.md resolves the engine at `../../scripts/lib`)"—a resolution the self-contained-skill fix below replaced. Source `skills/doc-structure/` holds `SKILL.md` alone, so following the instruction verbatim and running the shipped command threw `Cannot find module`, on Antigravity, on any other agent, and in the claude.ai ZIP path. All three now point at the BUILT folders in `plugin/skills/`, where each skill is self-contained (`doc-structure` carries its engine in its own `lib/`)—verified by copying that folder outside the repo, with no `scripts/lib` present anywhere, and running the skill's command verbatim: two correct findings, exit 0. The conductor's own dependency is stated separately, since it is the one piece that genuinely needs `hooks/` and `scripts/lib/` side by side; the earlier instruction supplied that only by accident.
* **A cloned repo's `.coalledger.json` could switch CoalLedger back ON after you had turned it off globally.** The two-level config cascade let the project layer overwrite the global one for every key, so a project file shipped inside a repo you cloned could raise `coalledgerMode` or `updateMode` from `off` to `auto`, or empty out `disabledCanaries`, and the session-start conductor would then inject offers and schedule update checks you never consented to. Those keys—`coalledgerMode`, `updateMode`, `disabledCanaries` and `docLeak`—now merge **safer-value-wins**: a project may quieten them, never escalate. `docLeak` belongs with them because it gates whether a canary is *offered* at all, on the same conductor filter as `disabledCanaries`. `docsDriftNudge` is deliberately left alone despite being the same type: it suppresses one quiet line with no offer, scan or spend, so a project re-enabling it costs nothing. Every other key keeps plain project-wins.
* **Three dead stores cleared in the AST engine (CodeQL `js/useless-assignment-to-local` ×2, `js/unused-local-variable`).** `matchListItem`'s `mp` initializer (overwritten on every surviving path), a write-only `itemSawBlank`, and an unused `rest` binding in the lazy-continuation block—all pure deletions with no semantic effect. Engine output verified byte-identical to the previous release across the repo's markdown corpus and 4000 list/blockquote-heavy fuzz documents (the exact code paths involved); the planted-defect fixture gate is unchanged at 13/13 found, 0 on the clean decoys.
* **A fourth alert on the same file, `js/bad-tag-filter`, was adjudicated a FALSE POSITIVE and deliberately not "fixed"—with a regression test to keep it that way.** The query targets HTML sanitizers, which must match the browser (WHATWG) tokenizer, where `--!>` closes a comment. This is a CommonMark parser, and CommonMark 0.31.2 does not recognise `--!>` anywhere: HTML block type 2 ends at "a line containing the string `-->`", the inline comment production likewise ends only at `-->`, and the literal `--!>` appears zero times in the spec. Continuing the comment past `--!>` is therefore spec-mandated and matches GitHub; ending it there would make the scanner report markdown findings on content GitHub does not parse as markdown—the cry-wolf class this engine exists to avoid. A conformance test now pins the behaviour (control, the `--!>` case, and two verbatim spec examples) so the wrong fix cannot land silently.
* **`doc-structure` was unrunnable whenever the skill folder travelled alone.** Its SKILL.md told the agent to run `node "<plugin root>/scripts/lib/md-checks.mjs"`—correct inside a Claude Code plugin install, but a dangling instruction anywhere the skill folder ships by itself (a claude.ai ZIP upload, any standalone consumer), where the dist folder held `SKILL.md` and nothing else. A packaging gate now refuses such skills, and CoalLedger was the flock's only violator. **Fix:** `scripts/build-plugin.mjs` copies the engine into `plugin/skills/doc-structure/lib/` as generated dist content, and SKILL.md invokes it relatively—`cd "<skill base dir>" && node ./lib/md-checks.mjs --json <absolute-file.md> …`. The `cd` is part of the instruction on purpose: `./lib/` resolves against the skill folder, not the agent's project cwd, and the skill's base directory is already in the agent's context (without it the command throws `Cannot find module`). `verify.mjs` now asserts that exact relative form—the previous check accepted any text containing `md-checks.mjs`, so the old broken path would have passed it. **The engine source stays single** (`scripts/lib/md-ast.mjs` + `md-checks.mjs`): the copy is build output, never hand-edited, and `verify.mjs` byte-checks it against that one source in both directions (a tampered or deleted copy fails the gate—verified by negative test). Tracking a second copy as source was rejected: it would fork the engine the moment a standalone consumer appeared. `md-checks.mjs`'s `./md-ast.mjs` import resolves unchanged because both files land side by side. Proven end-to-end by copying the built skill folder outside the repo, with no plugin root present, and running the instruction verbatim.
* **The conductor no longer advertises a second, different engine path.** `hooks/coalledger-conductor.js`'s doc-structure offer hard-coded the old `<plugin root>/scripts/lib/…` command; it now defers to the skill contract ("run the AST engine the way its skill contract states"), so there is one invocation truth instead of two that can drift apart.
* **The engine's shipped header no longer claims the whole suite.** `md-ast.mjs`'s header said "every mechanical doc check runs on THIS tree"—a scope over-claim: the AST tree serves the `doc-structure` canary only (`md-checks.mjs` is its sole consumer; the other canaries' mechanical layers are agent-run, and `doc-consistency`/`doc-leak` declare no mechanical layer at all). The README's front-door line carried the same suite-wide reading and now says "structure detection". Wording only; no behaviour change.
* **CoalBoard escalation pointers are now conditional.** The four canaries that escalate a formal-verification claim (`doc-grounding` · `doc-consistency` · `doc-rot` · `doc-standard`) said "escalate to CoalBoard (`/coalboard`)" unconditionally. CoalBoard is deliberately never ported to other surfaces, so on any platform without it that was a dangling instruction; each now reads "if that skill is installed", with doc-grounding naming the fallback (flag it as needing formal verification).

## \[0.3.0-beta.1] - 2026-07-25

The **docs memory-drift** reminder—the DOCS mirror of CoalMine's CODE memory-drift, on the same quiet channel.

### Added

* **A new auto hook layer (PostToolUse + Stop).** Until now CoalLedger's only auto surface was the `SessionStart` conductor; the mirror needs two more hooks:
  * **`hooks/coalledger-doctrack.js` (PostToolUse: `Write|Edit|MultiEdit`)**—records which DOC files were edited (`.md .mdx .markdown .rst .txt .adoc .asciidoc .org`) into `os.tmpdir()/coalledger-<sid>.docs`, and records a `MEMORY.md` edit as the session-long SATISFIER marker `coalledger-<sid>.docmemmoved`. Config-free (a named divergence from CoalMine's touch hook: this tracker runs no scan, so it has nothing costly to gate—all config gating lives in the Stop hook). Excludes files resident under `os.tmpdir()` (lab/scratch never ships—the same guard CoalMine added in v3.12.2). Defensive path extraction (Claude Code + Antigravity payload shapes); traversal-shaped session ids fail closed.
  * **`hooks/coalledger-drift-stop.js` (Stop)**—when the agent finishes responding: if doc files were edited in that batch but `MEMORY.md` has not been updated this session, and the project uses the `MEMORY.md` convention (a root `MEMORY.md` exists), emit ONE quiet model-context line via `hookSpecificOutput.additionalContext` (CC v2.1.15x+ Stop channel—feedback without blocking). No report, no severity table, no skill-invoke, no fix menu, never `decision:block`. The same quiet JSON shape CoalMine's revamped drift note uses (one flock). Off-switchable; a global `off` / `disabledCanaries:["all"]` also silences it. CC-only—Antigravity's engine documents no Stop inject channel.
* **`docsDriftNudge` config key** (bool, default `true`)—the off-switch, the DOCS mirror of CoalMine's `memoryDriftNudge`. In `config-schema.mjs`, the `.coalledger.json` template, and the README Configure table.
* **No clash with CoalMine (the disjoint-trigger design).** CoalMine watches CODE extensions, CoalLedger watches DOC extensions—the factory sets never overlap, so a `.js` edit is CoalMine's alone and a `.md` edit is CoalLedger's alone. `MEMORY.md` is the SATISFIER for BOTH and a trigger for NEITHER (intercepted before the extension gate), so updating the record clears both drifts and double-counts neither. A session touching both code and docs may get two quiet notes, one per axis, distinct in wording (CODE vs DOCS). State files use the `coalledger-<sid>` prefix, disjoint from CoalMine's `rot-canary-<sid>`.
* **+20 hermetic spawn tests** (`scripts/lib/hooks.test.mjs`) → suite 105 → 125: the quiet channel (`additionalContext`, never `decision:block`), the disjoint trigger (a code edit records nothing, a doc edit records), `MEMORY.md`-as-satisfier clears the drift, the satisfier surviving across stops, every off-switch, the `os.tmpdir()` exclude + boundary-safety, `stop_hook_active` loop guard, idempotent batch cleanup, exit 0 + Phoenix-13 silence on every path. `verify.mjs` gains the two hook rows + the PostToolUse/Stop wiring assertions.

### Fixed

* **\[HIGH, pre-release review] the satisfier was destroyed on every Stop, producing a false nudge in a session that DID update `MEMORY.md`.** Claude Code's `Stop` fires when the agent finishes responding—several times a session, not once at the end—and the Stop hook's `cleanup()` deleted both the edit batch (`.docs`) and the satisfier (`.docmemmoved`). Reproduced: update `MEMORY.md` + edit a doc in turn 1 (correctly silent, state wiped) → edit another doc in turn 2 → the nudge fired claiming no `MEMORY.md` update was recorded. Fixed by clearing only the batch, so the satisfier lives for the whole session; the 0-byte marker left behind is OS-tmp reaped (CoalMine avoids the same trap with its `.scanned` ACK-mtime machinery—overkill for a one-line advisory). Caught by review, not the suite: all the pre-existing Stop tests ran their stops with no edit interleaved, so a regression test that edits BETWEEN two stops ships with the fix.

## \[0.2.0-beta.1] - 2026-07-24

Antigravity 2.0 gains the auto-conductor: the docs-health canary offers now ride AG's real hook engine, alongside the always-available manual cross-agent skill contract.

### Added

* **`hooks/ag-conductor.js`**—the canary offers ride the FIRST `PreInvocation` of a session (AG never fires `SessionStart`; PreInvocation fires per model call, so a per-session atomic marker in `os.tmpdir()/coalledger/` guards the injection to once per session, and **fails CLOSED** on any marker-write failure—repeating an advisory payload on every model call is exactly the spam the guard exists to prevent). Requires its sibling `hooks/coalledger-conductor.js`, which now exports shared `{buildOffers, languageLine, lib}` behind a `require.main` gate—one offer text for both platforms; the Claude Code `SessionStart` path is unchanged (all 11 of its existing tests stayed green, untouched by this diff). Emits the current AG contract, `{"injectSteps":[{"ephemeralMessage":...}]}` (protojson; re-derived 2026-07-23 from the installed build's own hooks doc—the pilot-era `additionalContext` key is a dead letter there, 0 engine hits). Honors the payload's workspace via `loadMergedConfig({ cwd })`, reading `workspacePaths[0]` as authoritative with a legacy `cwd` fallback—a named mechanism divergence from CoalFace's adapter, which `process.chdir()`s instead; same "the payload's workspace is authoritative" rule, no process-state mutation. Deliberately NOT ported: the KIND-1 self-update nudge (its payload, `claude plugin update coalledger@coalledger`, is Claude-Code plugin machinery; AG installs by file-copy).
* **`platform-configs/hooks.json`**—the AG wiring template. Carries an explicit caveat: the wire location itself regressed on an AG update (2026-07-12 → 07-16)—the two previously-documented locations (global `~/.gemini/config/hooks.json`, project `<workspace>/.agents/hooks.json`) stopped executing, and the AG state dir moved—so the template tells the reader to re-derive the current path from AG's own docs before wiring. Wiring at a dead path is inert-harmless: the adapter simply never runs, and manual canary invocation (the reliable floor on every non-Claude-Code surface) is unaffected.
* +12 hermetic AG spawn tests (`scripts/lib/conductor.test.mjs`) → 93 → 105 total (ground-truthed by a live run 2026-07-24: 105/105); `verify.mjs` gains the 2 new file rows.
* **Tier: wired**—built and hermetically tested against the current AG hook contract; live delivery into a real AG session is not yet validated. No "works on Antigravity" claim until a real run proves it.

## \[0.1.0-beta.6] - 2026-07-17

Fourth CoalBoard dogfood pass (full-mirror, nasa-L3)—a HIGH the beta.3 fix left half-open.

### Fixed

* **\[HIGH, CoalBoard nasa-L3 audit] the markdown parser was STILL quadratic after beta.3—`processEmphasis` was O(N²), and the beta.3 "near-linear … never a hang" claim was false.** beta.3 bounded only `parseInlineDest`'s non-angle branch; three quadratic paths remained in `scripts/lib/md-ast.mjs`, all reachable through the documented `checkDocument` entry on benign-looking input under the 512 KB cap:
  * **`processEmphasis`** reset the closer index to 0 after every match and array-spliced the node list each time → O(N²) on dense emphasis. A plain doc of `a*b_c*d_` repeated hung any scan—measured **200 KB ≈ 57 s**, and **\~500 KB (under the cap) did not finish in 5 minutes** (pure wasted CPU, 0 findings). Fixed by porting the **LINEAR CommonMark reference "process emphasis"**: a doubly-linked delimiter stack + an `openers_bottom` bucket table (keyed by char, `canOpen`, and original-run-length mod 3—the invariants the match rule uses) so no closer ever re-scans, plus a sibling linked list so wrapping a span is O(1) instead of an O(N) splice.
  * **the `<…>` angle-destination scan** used an unbounded `indexOf('>')` → O(N²) on `[](<` spam; now length-bounded by `MAX_INLINE_DEST`, matching the sibling non-angle branch.
  * **the code-span (backtick) scan** re-scanned the tail to EOS per run → O(N²) on backtick spam; a failed-run-length memo makes each length scan at most once.
  * **Result:** linear now—the 200 KB emphasis doc scans in **\~0.5 s**, the 512 KB worst case in **\~1.3 s**. **Parser output is unchanged**—verified structurally identical to the previous parser across 4025 fuzz docs and, where the two agreed, identical to the `commonmark` reference. +2 hermetic guards (91 → 93): a dense-emphasis wall-bound through `checkDocument`, and a functional bound on the angle destination (both fail on the pre-fix code). The 512 KB `MAX_DOC_BYTES` cap remains as the backstop for the residual native-scan paths (reference-label, table), which the cap already holds under \~2 s.

## \[0.1.0-beta.5] - 2026-07-09

A reconciliation pass (`plugin.json` vs `CHANGELOG.md`) plus the third CoalBoard dogfood pass (full-mirror, nasa—recorded in commit `cbb5fe7`): two LOW findings and a bookkeeping gap.

### Fixed

* **CHANGELOG backfill: v0.1.0-beta.4 shipped with no entry.** `plugin.json` had already moved to `0.1.0-beta.4` while this file's newest entry stayed at beta.3—an undocumented version bump, caught by a reconciliation pass, not the audit. Reconstructed from `git log`/`git show` v0.1.0-beta.3..v0.1.0-beta.4 (never from memory) and backfilled below. Lesson: a version bump without its CHANGELOG entry is an undocumented release—the release checklist's entry-before-tag order exists for exactly this.
* **\[LOW, CoalBoard nasa audit] degenerate/binary input parsed to a near-empty tree, reporting a false "0 findings" clean bill.** `parseMarkdown` never throws, so a corrupted or non-text `.md` produced no structural findings at all—the beta.3 size cap addressed volume, not content validity. Added a `doc-unreadable` pre-parse guard (`scripts/lib/md-checks.mjs`): a NUL byte in the first 8 KB (git/grep/diff's own binary-detection heuristic) is now flagged instead of silently parsed. Documented the remaining honest ceiling in the file's "Known limits" header—genuinely garbled-but-NUL-free UTF-8 text has no such signal and still parses near-empty (this is a structural scanner, not a content validator), and `anchorsOf`'s `catch → null` still treats every unreadable cross-file anchor target alike. +1 hermetic test (90 → 91).
* **\[LOW, CoalBoard nasa audit] README doctrine link pointed at the org root instead of the `.github` repo.** "Series doctrine: `TheColliery/.github`" linked `https://github.com/TheColliery` though the visible text names the `.github` repo specifically—corrected to `https://github.com/TheColliery/.github`.

## \[0.1.0-beta.4] - 2026-07-09

*(Backfilled 2026-07-09—this release shipped with no CHANGELOG entry; reconstructed from `git log`/`git show` v0.1.0-beta.3..v0.1.0-beta.4, never from memory.)* CodeQL flagged a second-order sanitization gap in the same file the beta.3 DoS fix touched.

### Fixed

* **\[HIGH, CodeQL `js/incomplete-multi-character-sanitization`] the HTML-comment strip in `collectAnchors` could leave a residual comment on overlapping/adjacent markers** (`scripts/lib/md-checks.mjs`). A single `.replace(HTML_COMMENT_RE, '')` pass doesn't re-scan its own output, so input like `<!--<!---->-->` left a partial comment behind—the canonical incomplete-multi-character-sanitization pattern. Fixed with `stripHtmlComments()`, which repeats the replace to a FIXED POINT (loops until the string stops changing) before `collectAnchors` scans for `id`/`name` attributes. The beta.2 anchor-precision property is unaffected; +0 test change (90/90 held). CodeQL's `bad-tag-filter` and dead-code alerts on the same region were reviewed and dismissed—the parser is a block-classifier, not a security sanitizer, and raw HTML is passthrough-flagged, never executed or re-emitted.

### Changed

* CI: `github/codeql-action` (`init`/`analyze`/`upload-sarif`) 4.36.3 → 4.37.0 and `DavidAnson/markdownlint-cli2-action` 23.2.0 → 24.0.0 (Dependabot, SHA-pinned). `dependabot.yml` now groups the three `codeql-action` bumps into one PR (avoids an init/analyze version-skew that reds CodeQL) and assigns bump PRs to the maintainer so they notify at any GitHub watch level.

## \[0.1.0-beta.3] - 2026-07-09

Second CoalBoard dogfood pass (full-mirror, nasa)—a HIGH the first pass missed.

> **Correction (0.1.0-beta.6):** the "Parse is now near-linear … never a hang" claim below was **incomplete** — it fixed only the `parseInlineDest` (`[a](`) vector. `processEmphasis`, the `<…>` angle-destination scan, and the backtick scan remained O(N²); a dense-emphasis doc still hung. Closed for real in 0.1.0-beta.6.

### Fixed

* **\[HIGH] the markdown parser was quadratic-time; a crafted doc could hang any scan.** `md-ast.mjs` `parseInlineDest` re-scanned the tail to end-of-string on every `]` with an unclosed `(`—`[a](` repeated N times parsed in O(N²) (measured: 8 KB ≈ 190 ms, 16 KB ≈ 680 ms, 32 KB ≈ 2.9 s, extrapolated \~1 MB ≈ 1 hr), while a benign 273 KB doc parsed in 5 ms. Fixed at the root: the inline destination/title scans are length-bounded (`MAX_INLINE_DEST`; over the cap = not a valid inline link → literal text), and `checkDocument` refuses a doc over `MAX_DOC_BYTES` (512 KB)—flagging `doc-too-large` instead of parsing—which also closes the transitive vector (a benign doc that links a poisoned `.md`). Parse is now near-linear (16 KB pathological ≈ 340 ms, bounded ≈ 5.7 s at the 512 KB cap, never a hang). +2 timing regression tests (88 → 90).

## \[0.1.0-beta.2] - 2026-07-09

Launch-day **CoalBoard dogfood** (nasa rigor, opus blind lenses + judge) caught a precision bug the fixture gate missed.

### Fixed

* **\[MED] `anchor-missing` could pass a genuinely-broken `#link`.** `collectAnchors`'s `HTML_ID_RE` matched `id`/`name` anywhere in raw HTML—`data-id="x"`, `item-name='y'`, or an `id` inside an HTML comment all registered as FALSE anchors, so a link to a non-existent anchor slipped through. The regex now requires an attribute boundary (a negative lookbehind for a word-char/hyphen) and HTML comments are stripped before scanning—only real `id`/`name` anchors count.

## \[0.1.0-beta.1] - 2026-07-09

Initial public beta—phase 1 (engine + pilot) and phase 2 (the full suite + public docs) together.

### Added

* **AST engine (zero-dependency ESM):** `md-ast.mjs` vendored CommonMark+GFM parser with a Unicode-safe slugger (Thai/CJK headings resolve) · `md-checks.mjs` mechanical structure checks (8 stable check ids: heading-skip, heading-multiple-h1, anchor-missing, file-missing, table-ragged, ref-undefined, def-orphan, bare-url)—detection by AST, never regex over raw markdown; fixture-gated against planted defects AND clean decoys (any decoy finding fails the gate—anti-cry-wolf).
* **The 6+1 canaries** (`skills/`): `doc-structure` (BROKEN, mechanical—the engine pilot) · `doc-grounding` (WRONG—claims vs source of truth: code/data/original text/reality; real-time multi-source, offline degrades to `⚠️ unverified`) · `doc-standard` (INCOMPLETE—vs the doc kind's standard) · `doc-rot` (STALE—age-markers + superseded content) · `doc-consistency` (CONTRADICTORY—incl. cross-language drift) · `doc-quality` (UNREADABLE/MALFORMED—bloat, clarity, language mechanics) · `doc-leak` (LEAKED—prose-level audience boundary, SUSPECTED-only, config-gated via `docLeak`). Every canary: CONFIRMED/SUSPECTED split, context-judged severity (never a fixed map), choice-gated fix menu (never auto-fix a live doc), CoalBoard escalation at the correctness boundary, reports in the user's language.
* **SessionStart conductor** (`hooks/coalledger-conductor.js`, Phoenix-13): offer-on-domain-entry for the full set, honoring `disabledCanaries` and the `docLeak` gate; kind-1 self-update scheduling (hook schedules via a throttled local stamp, the agent verifies online with consent).
* **Commands:** `/coalledger:stats` (measurement standard-system—session findings by canary/severity + suite config state, read-only) · `/coalledger:update` (consent-gated self-update procedure).
* **Config system:** `.coalledger.json` global + per-project cascade (project wins), schema SSoT with clamped reads (`coalledgerMode`, `language`, `disabledCanaries`, `severityFloor`, `quickVsFull`, `docLeak`, `publicMode`, `updateMode`, `updateCheckDays`), commented factory template.
* **Gates:** `build-plugin.mjs` (clean dist: manifest + commands + hooks + skills + engine; tests/fixtures never ship) · `verify.mjs` (files, manifests, skill frontmatter contract, version pins, factory-vs-schema, engine fixture smoke, dist-sync both directions) · `test.mjs` (explicit-file-list runner; hermetic conductor spawn tests included).
* **Docs:** README, SECURITY, PRIVACY, CONTRIBUTING, Apache-2.0 LICENSE + NOTICE.
* **CI:** the flock's four SHA-pinned workflows (ci · codeql · markdownlint · scorecard), dependabot, issue templates.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.thecolliery.org/tools/coalledger/changelog.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
