diff --git a/CHANGELOG.md b/CHANGELOG.md index df33b8d..fb991f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,57 +6,107 @@ The format is based on Keep a Changelog and follows Semantic Versioning. ## [Unreleased] -**Breaking:** after `charter bootstrap`, `charter adf compile --target all --write` no longer replaces the thin pointer files with compiled output; it skips them and writes nothing. To convert them, run `charter adf compile --target all --write --mode compiled`, or record a file's mode once with `charter adf vendor set compiled`. - -### Changed - -- **Compiled files that carry your own retained sections change bytes on the next `adf compile --write`** (`#303`, `#318` part 5) — a compiled `CLAUDE.md`, `AGENTS.md`, `.cursorrules` or `GEMINI.md` with a hand-added `## Environment` (or another section `adf tidy` retains) no longer loses it. The next `--write` moves it into a `` … `` block at the end of the file, and every later `--write` copies that block as it stands. A compiled file with nothing to carry gets no block and compiles byte-identically to before. The block uses HTML markers in every target, `.cursorrules` included. All-caps headings the compiler could have produced (a module since removed from `.ai/`) and `## Module Index` are never carried. `adf compile` to stdout and `charter score` still use the plain render. -- **`adf compile --check` reads such a file as stale until one `--write`** (`#303`) — the expected bytes are now the render plus the file's own retained block, so an edit inside the block is never stale. A compiled file whose retained sections sit outside the block reads `[stale]` with `staleReason: "retained-unfenced"`, and the remedy (`compile --target --write`) keeps them. Each `--check` JSON row adds `staleReason` (`content`, `retained-unfenced`, or null). -- **`refused-retained` is retired from `adf compile --write`, and `retained-divergent` from `adf vendor restore`** (`#303`) — JSON consumers no longer see either value. A pointer converted with `--mode compiled` now carries the retained sections that differ from its template into the block, instead of refusing. It still refuses, exit 1 with nothing written and `VENDORS` unchanged, over content the output would not carry: `refused-bloated` (items `adf tidy` would extract; the first remedy is `adf tidy --source `, then `--force`, which discards them) and `refused-would-drop` (other lines of yours). `--mode compiled --force` over a hand-authored file carries its retained sections and discards the rest. `--force` never drops retained sections. Write JSON rows add `retainedSections`, `retainedSource`, `retainedSkipped` and `retainedAmbiguous`. `retainedSkipped` lists retained-looking sections whose heading is still in a `.ai/` module, so they are rendered `.ai/` text left behind by an edit; they are not carried, and each one is named on stderr. A section left inside a generated section that `.ai/` no longer renders, with no `.ai/` match, may be stale output or your own. `--write` refuses that target as `refused-ambiguous` (new value), with "wrap it in the retained block" as the first remedy and `--force` (which discards it) as the second. `--check` reports it with `staleReason: "retained-ambiguous"`. Parsing is code-fence aware, and marker lines match as loosely as `stripCharterSentinels`. `vendor restore` of a compiled file carries its retained sections into the pointer and lists them in `keptSections`, and the block is not counted as a `compiled-differs` difference. A block holding a non-retained heading, or text before its first heading, is refused as `would-drop` (new value) with `vendor set compiled` as the first remedy, because a pointer cannot keep it. -- **The pre-commit hook tidies only the staged vendor files and re-stages only the files tidy wrote** (`#320`, `#318` part 4) — re-run `charter hook install --pre-commit` to pick up staged-only tidy. An installed hook keeps its old behaviour until you do, and `charter doctor` reports it with an INFO `pre-commit hook` row (INFO, so `--ci` does not fail). Reinstalling overwrites a Charter-managed hook without `--force`, as before. The old hook ran a whole-tree `adf tidy` whenever any vendor file was staged, then ran `git add` on every staged vendor file and on all of `.ai/*.adf`. That swept unrelated `.ai/` edits into the commit and rewrote vendor files that were not part of it. The new hook passes each staged vendor file to `adf tidy` with `--source` and stages only the paths that both the dry run and the real run report. A staged vendor file that also has unstaged changes is skipped with a message, because tidy works on the working tree and staging its result would commit those hunks. If tidy would write into a module that has unstaged changes or is untracked, nothing is tidied for that commit and a message says why. A gitignored module (for example an `.ai/` kept out of git) is not treated as work in progress: tidy writes it, and the hook says it was left unstaged. Any file the hook cannot stage, and any file the real run wrote that the dry run did not plan, is named and left unstaged rather than aborting the hook. A staged lowercase `agents.md` is now tidied (`#302`), and a commit staging only compiled output still makes no CLI call. The tidy block itself never fails a commit, but the ADF evidence gate after it can. The gate's handling of vendor bloat outside the commit is fixed by `#357` (see Fixed). A new hook with an older local CLI that does not know `--print-paths` reports the files as clean and tidies nothing, so upgrade the CLI too. -- **BREAKING: `charter adf compile --write` no longer converts pointer files without `--mode compiled`** (`#318`, part 2) — compile used to overwrite any Charter thin pointer with compiled output, silently changing its mode. Each target's mode now comes from the `VENDORS` record, or from its markers when there is no entry, and `--write` only writes targets that are compiled or absent. Under `--target all`, pointer and unmanaged targets are skipped (exit 0), and each `[skip]` line names the command that converts it. A single such `--target` is refused (exit 1). `--force` alone never changes a mode. `--mode compiled` records `compiled` before writing. If that cannot be recorded, nothing is written (`refused-record`). If the write then fails, the record is restored and the run continues. A converted file is reported as `Written CLAUDE.md (mode: pointer -> compiled)`. JSON output adds `skipped` and per-target `targets`. A compiled-only repo with no `VENDORS` section compiles as before. - -- **`charter bootstrap --force` no longer overwrites hand-authored or compiled vendor files** (`#318`, part 3) — this changes what a documented flag does. `--force` still re-scaffolds `.ai/*.adf` (with backups in `.ai/.backup/`) and still rewrites Charter thin pointers from the current template. It now skips a `CLAUDE.md`, `AGENTS.md`, `.cursorrules`, `GEMINI.md` or `copilot-instructions.md` that has no Charter marker, carries the compile banner, or is recorded `compiled` or `unmanaged` in `VENDORS`, with a warning naming the next step (`adf migrate --source `, or `adf vendor set pointer && adf vendor restore ` for compiled output). With `--yes`, phase 4 then migrates a hand-authored file that has no `VENDORS` entry into `.ai/`, as it would without `--force`, and the phase 3 warning says so; a hand-authored file that already has an entry is left as it is. Phase 4 no longer migrates a file recorded `unmanaged`, and valid `VENDORS` entries are carried over to the re-scaffolded `manifest.adf`. To replace a hand-authored file without migrating it, delete it and re-run bootstrap. -- **`charter doctor` reads each vendor file's mode, and can now fail `--ci` and the pre-commit hook in repos that record modes** (`#318`, part 3) — a new `adf vendor mode` check compares the `VENDORS` record with the files on disk. It WARNs, and so fails `doctor --ci` and the pre-commit hook (`doctor --adf-only --ci`), when a recorded `pointer` or `compiled` file looks like another mode or is missing, when an entry is malformed, names an unknown file or a mode that file cannot have, or when `VENDORS` is not a list. Each line names the fix, the content-preserving one first. A repo without a `VENDORS` section gets one `INFO` line suggesting `charter adf vendor init` and no new WARN. JSON adds `rows` to that check only. `adf agent config` no longer warns about a hand-authored file recorded `unmanaged`, its PASS line counts thin pointers and compiled files separately (compiled output used to be called a thin pointer), and it, `adf vendor bloat` and `adf cold start` check each physical file once, so `AGENTS.md` and `agents.md` on a case-insensitive filesystem are no longer counted twice. Bloat and cold start look at pointers only. -- **`charter score` counts a thin pointer as substantive when `.ai/` is** (`#318`, part 3) — score numbers in JSON change, and only upward. A pointer `CLAUDE.md`, `.cursorrules` or `AGENTS.md` used to score as "thin" (−5, and "Expand `CLAUDE.md` beyond a thin pointer") however much its `.ai/` held. It is now substantive when its text is, or when the file `adf compile` would render for it is (compiled in memory; nothing is written). Hand-authored and compiled files are scored on their text as before, so no score goes down. `signals.agentConfig` adds `mode`, `source` and `substantiveVia` per file. Recommendations no longer tell you to edit a pointer or compiled `CLAUDE.md`; a pointer with little to route to gets "Add repo commands and guardrails to `.ai/core.adf` (or run: charter adf populate)". -- **`charter adf migrate` without `--source` skips a file recorded `unmanaged`** (`#318`, part 3) — naming the file with `--source` still migrates it, and now changes its entry to `pointer` before the pointer is written (back to `unmanaged` if the migration fails), so doctor reports no drift afterwards. -- **`adf compile --check` names `charter adf vendor restore ` instead of `git checkout -- `** (`#318`, part 3) — for a missing recorded pointer, and for a recorded pointer that now holds another mode's content. -- **A legacy-keyed `AGENTS.md` gets a specific refusal from `adf compile` and `adf vendor set`** (`#302`, `#318` part 6) — when `AGENTS.md` is recorded under the legacy name `agents.md`, `adf compile --target agents|all --write --mode compiled` used to report the generic `refused-record`. It now reports `refused-legacy-spelling` (new `targets[].status` value, exit 1), renders nothing and leaves the record alone. Without `--mode`, the skip or refusal remedy becomes `charter adf vendor rename-legacy && charter adf compile --target agents --write --mode compiled`. `adf vendor set AGENTS.md compiled` exits 1 with a message naming the rename instead of "no compile target writes it". Doctor's `legacy-spelling` row now names `charter adf vendor rename-legacy`, and sets `remedy` to it. - -### Added - -- **`charter adf vendor rename-legacy`, and doctor rows for a tracked lowercase `agents.md`** (`#302`, `#318` part 6) — repos bootstrapped before `#297` track `agents.md`, which nothing renamed. `rename-legacy` moves the `VENDORS` key first (`agents.md: ` becomes `AGENTS.md: `, or a same-mode duplicate `agents.md` line is removed through a new `removeManifestBullet` and the same re-parse guard), then renames in two steps through `agents.md.charter-rename`, which works on case-insensitive filesystems. **Index effect:** when git tracks `agents.md`, both steps are `git mv`, so the command leaves a staged rename (`R agents.md -> AGENTS.md`) and stages nothing else. `manifest.adf` is left unstaged, and the output names `git add .ai/manifest.adf && git commit` (with `AGENTS.md` in the `git add` when its title line was rewritten), plus a note that `git commit` also records anything already staged. An untracked file, or a directory outside git, is renamed on disk only. **Content effect:** the only edit is a first line that is exactly `# agents.md`, rewritten to `# AGENTS.md` and left unstaged. Mentions of `agents.md` elsewhere are listed, never edited. It refuses (exit 1, nothing changed) when both spellings are tracked or are two different files (it never merges or deletes), when `agents.md` has uncommitted changes, when the temp name exists, when the two keys are recorded with different modes or `agents.md` is listed twice, when the git index cannot be read inside a work tree, or when it is run below the repository root. The index is read with plain exact paths, so exported `GIT_*_PATHSPECS` variables cannot hide a tracked `agents.md`. If a rename step fails, the completed step is undone and the record put back. `--dry-run` writes nothing, and `--format json` prints one result. Nothing calls it automatically: bootstrap, tidy, compile and the hook never rename. `charter doctor`'s `adf vendor mode` check adds three INFO rows read from the git index only: `legacy-tracked` (names `rename-legacy`), `legacy-both-distinct` (merge into `AGENTS.md`, then `git rm agents.md`) and `legacy-both-one-file` (`git rm --cached agents.md`). They never fail `--ci`. - -- **`charter adf tidy --source` is repeatable, and `--print-paths` lists the files tidy writes** (`#320`) — each `--source` is processed in argument order, and two names for one file (`AGENTS.md` and `agents.md` on a case-insensitive filesystem, or a hard link) are tidied once, under the first name. A `--source` with no value is now a usage error (exit 2); it used to tidy every vendor file silently. `--print-paths` prints one `vendor ` or `module ` line per file tidy writes (or would write, under `--dry-run`) and nothing else on stdout; warnings go to stderr. It cannot be combined with `--format json` (exit 2). JSON output adds `paths` (`vendor` and `modules`); `modulesModified` is unchanged. A single `--source` gives the same output as before. -- **`charter adf vendor restore `** (`#318`, part 3) — rewrites one vendor file as its thin pointer template (`CLAUDE.md` gets the hybrid template with its module index, as bootstrap writes it), carrying `## Environment` and other retained sections over verbatim. It runs only for a file in `pointer` mode, or an absent file with no entry, and refuses (exit 1, nothing written) whenever content would be lost: hand-authored content, content `adf tidy` would extract, any line of a pointer that is not from a Charter template and that the new pointer would not carry (an edited title, an extra note, edited `## Session Start` text), your own sections in a compiled file, or a compiled file that differs from what `.ai/` compiles to now (checked in memory). It is the fix doctor names for a recorded pointer that now holds compiled output. A file with no entry gets `pointer` recorded first; a failed file write puts the manifest back and is reported as `write-failed` with `writeError`. `--dry-run` and `--format json` are supported. -- **`charter adf migrate` registers the modules it routes content into** (`#239`, completes the migrate half of `#241`) — migrate could create `infra.adf`, `frontend.adf` or `core.adf` without adding them to `.ai/manifest.adf` or the `CLAUDE.md` module index, so no agent knew to load them and `charter doctor` failed until someone wired them by hand. Each target module that is not already in the manifest is now registered after the module merge and before the pointer is written, so the hybrid `CLAUDE.md` index includes it. A source other than `CLAUDE.md` that adds a module refreshes that index afterwards. `core.adf` and `state.adf` go to `DEFAULT_LOAD`, through the same classification `doctor` checks. Other modules go to `ON_DEMAND` with up to six triggers inferred from the headings of the items routed there. The heading word the router matched counts double, a named module's curated triggers come first, words that already trigger another module are dropped, and the lowercase module stem is the fallback. To supply that heading word, `@stackbilt/adf`'s `RoutingTrace` gains an optional `headingKeyword`. `classifyElement` now always sets `routingTrace`, even without a trigger map, in which case `candidateScores` is `{}`. That is an additive change, but code that read a defined `routingTrace` as "content routing ran" should check `candidateScores` instead. Registration is append-only. New bullets are inserted as text after the section's last item, in that item's indentation and bullet style, and a missing section is appended at the end of the file. Comments, HTML comments, tables, section order, blank lines and CRLF or LF line endings are left byte-for-byte. Before writing, the edited manifest is re-parsed and must equal the old one plus exactly the new entries; otherwise nothing is written and a warning is printed. An existing entry is never rewritten, moved or given new triggers, and a re-run writes nothing. An existing entry is reported as it stands, in its real section with its triggers as spelled. A section header with no items counts as an empty list and is filled. `core.adf` under `ON_DEMAND` is left in place with a warning naming the move. A missing `manifest.adf` warns to run `charter adf init`, an unparseable one warns to fix it or run `charter doctor`, and in both cases registration is skipped. The plan prints a `REGISTER` line per module, and `--format json` adds `sources[].registrations[]` (`module`, `loadPolicy`, `triggers`, `triggerSources`, `reason`, `status`, `skipReason`). `--dry-run` writes nothing and reports each planned module once across sources. As a known gap, it does not simulate an earlier source's new triggers when routing a later one. Bootstrap's two orphan writers now use the same shared writer, which also removes a line-splice ON_DEMAND writer that could register a module twice. That brings three behavior changes. Orphan triggers are now the lowercase stem (`Observability.adf` gets `observability`). A `DEFAULT_LOAD` that is not a list is now skipped with a warning, where it used to have a raw `DEFAULT_LOAD:` block appended. And with `--format json`, writer warnings now appear in the phase 3 step's `warnings`. A bootstrap `--yes` run registers migrate's modules in phase 4, so a re-run finds no orphans. `charter adf create` keeps its own upsert writer, and the stale `charter adf register` hint in bootstrap is tracked separately in `#347`. -- **A record of each vendor file's mode, and `charter adf vendor status|set|init`** (`#318`, part 1) — nothing recorded whether `CLAUDE.md`, `AGENTS.md`, `.cursorrules` or `GEMINI.md` was meant to be a thin pointer to `.ai/` or `adf compile` output, so every command guessed from the markers in the file. `.ai/manifest.adf` can now carry a `VENDORS` list (` - CLAUDE.md: pointer`) that gives each file's mode as `pointer`, `compiled` or `unmanaged`. `charter adf vendor status` is read-only. It shows each file's recorded mode, or the mode inferred from its markers when none is recorded, and flags a recorded mode that the file on disk contradicts. Inference follows `adf tidy`: the banner means `compiled`, a pointer marker means `pointer`, and anything else is `unmanaged`. `charter adf vendor set ` records one file, and `charter adf vendor init` records the inferred mode of every unrecorded file on disk. `agents.md` and `copilot-instructions.md` may never be `compiled`, since no compile target writes them. `bootstrap`, `adf init --emit-pointers`, `adf migrate` and `adf compile --write` now record the mode of each vendor file they write. They only add a missing entry, so an existing one is kept with a warning. Under `--dry-run` nothing is recorded. A record that cannot be written produces a warning and changes no exit code or JSON. Under `--format json` that warning goes into bootstrap's phase-3 `warnings`, and to stderr for the other writers. The writer reuses the text-level helpers from `#239`, so other bytes and line endings are kept, and the edit is re-parsed before writing. `@stackbilt/adf`'s `parseManifest` gains a permissive `vendors` field. `VENDORS` gets the bare 🏷 decoration (U+1F3F7 without U+FE0F, which the parser drops, so a written block stays `adf fmt` canonical), sorts after `SYNC`, and is never rendered into compiled output. When `AGENTS.md` and `agents.md` are one physical file (case-insensitive filesystem or hard link), every writer records it once, under the spelling already recorded. `charter score` leaves the section out of its manifest path-reference check. In this part no command acted on the record yet. Part 2 (above) makes `compile` and `tidy` read it, and `bootstrap`, `migrate`, `doctor`, `score` and both pre-commit hooks still decide as before until later parts of `#318`. +**Breaking:** `charter adf compile --write` changes a vendor file's mode only with `--mode compiled`. In 1.9.3 it refused a thin pointer unless you passed `--force`, which then replaced it with compiled output. Now `--force` never converts a pointer, `--target all --write` skips pointer and unmanaged files (exit 0), and a single `--target` refuses them (exit 1). Convert with `charter adf compile --target all --write --mode compiled`, or record a file's mode once with `charter adf vendor set compiled`. `charter bootstrap --force` also no longer overwrites hand-authored or compiled vendor files. Installed git hooks keep their 1.x behaviour until you re-run `charter hook install`. See [Upgrading to 2.0](./docs/upgrading-to-2.0.md). + +### Added + +- **A record of each vendor file's mode, and `charter adf vendor status|set|init`** (`#318`) — commands used to guess from markers whether `CLAUDE.md`, `AGENTS.md`, `.cursorrules` or `GEMINI.md` was a thin pointer or `adf compile` output. `.ai/manifest.adf` can now carry a `VENDORS` list (` - CLAUDE.md: pointer`) with the mode `pointer`, `compiled` or `unmanaged`. + - `status` is read-only. It shows each file's recorded mode, or the mode inferred from its markers (banner → `compiled`, pointer marker → `pointer`, otherwise `unmanaged`), and flags a recorded mode the file contradicts. + - `set ` records one file. `init` records the inferred mode of every unrecorded file on disk. `agents.md` and `copilot-instructions.md` can never be `compiled`. + - `bootstrap`, `adf init --emit-pointers`, `adf migrate` and `adf compile --write` record the mode of each vendor file they write. They only add a missing entry, and keep an existing one with a warning. Nothing is recorded under `--dry-run`. + - A failed record write is a warning only and changes no exit code or JSON: it goes to bootstrap's phase-3 `warnings` under `--format json`, and to stderr for the other writers. + - Manifest edits are text-level: other bytes and line endings are kept, and the result is re-parsed before writing. `VENDORS` uses the bare 🏷 decoration, so a written block stays `adf fmt` canonical. It sorts after `SYNC` and is never rendered into compiled output. + - When `AGENTS.md` and `agents.md` are one physical file (case-insensitive filesystem or hard link), it is recorded once, under the spelling already recorded. `charter score` leaves `VENDORS` out of its manifest path-reference check. +- **`charter adf vendor restore `** (`#318`) — rewrites one vendor file as its thin pointer template (`CLAUDE.md` gets the hybrid template with its module index, as bootstrap writes it). It carries `## Environment` and other retained sections over verbatim, listed in `keptSections`. It is the fix doctor names for a recorded pointer that now holds compiled output. + - It runs only for a file in `pointer` mode, or an absent file with no entry. A file with no entry gets `pointer` recorded first. + - It refuses (exit 1, nothing written) whenever content would be lost: hand-authored content, content `adf tidy` would extract, a pointer line that is not from a Charter template and that the new pointer would not carry (an edited title, an extra note, edited `## Session Start` text), or a compiled file that differs from what `.ai/` compiles to now (checked in memory). + - A compiled file's retained block, and retained sections outside it, are carried into the pointer and are not a `compiled-differs` difference. A block holding a non-retained heading, or text before its first heading, is refused as `would-drop`, with `vendor set compiled` as the first remedy. + - A failed file write puts the manifest back and is reported as `write-failed` with `writeError`. `--dry-run` and `--format json` are supported. +- **`charter adf vendor rename-legacy`** (`#302`, `#318`) — renames the lowercase `agents.md` that repos bootstrapped before `#297` still track to `AGENTS.md`. Nothing calls it automatically: bootstrap, tidy, compile and the hook never rename. + - The `VENDORS` key moves first: `agents.md: ` becomes `AGENTS.md: `, or a same-mode duplicate `agents.md` line is removed. The rename then goes through `agents.md.charter-rename` in two steps, so it works on case-insensitive filesystems. + - When git tracks `agents.md`, both steps are `git mv`. The command leaves a staged rename (`R agents.md -> AGENTS.md`) and stages nothing else. `manifest.adf` is left unstaged, and the output names `git add .ai/manifest.adf && git commit` (with `AGENTS.md` when its title was rewritten). It also notes that `git commit` records anything already staged. An untracked file, or a directory outside git, is renamed on disk only. + - The only content edit is a first line that is exactly `# agents.md`, rewritten to `# AGENTS.md` and left unstaged. Other mentions of `agents.md` are listed, never edited. + - It refuses (exit 1, nothing changed) when both spellings are tracked or are two different files (it never merges or deletes), when `agents.md` has uncommitted changes, when the temp name exists, when the two keys have different modes or `agents.md` is listed twice, when the git index cannot be read inside a work tree, or when run below the repository root. Exported `GIT_*_PATHSPECS` variables cannot hide a tracked `agents.md`. If a rename step fails, the completed step is undone and the record put back. `--dry-run` writes nothing, and `--format json` prints one result. + - While `AGENTS.md` is still recorded under the legacy key, `adf compile --target agents|all --write --mode compiled` refuses it as `refused-legacy-spelling` (exit 1; nothing rendered, record untouched), and the skip or refusal remedy without `--mode` becomes `charter adf vendor rename-legacy && charter adf compile --target agents --write --mode compiled`. `adf vendor set AGENTS.md compiled` exits 1 naming the rename. + - Doctor's `adf vendor mode` check reports, from the git index only, `legacy-tracked` (names `rename-legacy`), `legacy-both-distinct` (merge into `AGENTS.md`, then `git rm agents.md`) and `legacy-both-one-file` (`git rm --cached agents.md`) as INFO rows that never fail `--ci`. Its `legacy-spelling` row names `rename-legacy` and sets it as `remedy`. +- **`charter doctor` checks each vendor file's mode** (`#318`) — a new `adf vendor mode` check compares the `VENDORS` record with the files on disk. + - It WARNs, and so fails `doctor --ci` and the pre-commit gate, when a recorded `pointer` or `compiled` file looks like another mode or is missing, when an entry is malformed, names an unknown file or a mode that file cannot have, or when `VENDORS` is not a list. Each line names the fix, the content-preserving one first. + - A repo without `VENDORS` gets one INFO line suggesting `charter adf vendor init` and no new WARN, so upgrading does not fail CI. JSON adds `rows` to this check only. + - `adf agent config` no longer warns about a hand-authored file recorded `unmanaged`, and its PASS line counts thin pointers and compiled files separately (compiled output used to be called a thin pointer). It, `adf vendor bloat` and `adf cold start` check each physical file once, so `AGENTS.md` and `agents.md` on a case-insensitive filesystem are not counted twice. Bloat and cold start look at pointers only. +- **`charter doctor --staged` and `--tidy-skipped ""`** (`#357`) — `--staged` limits the `adf vendor bloat` check to vendor files staged in the index. `--tidy-skipped` takes the staged files the hook's tidy block skipped off that list. Bloat in the other files is reported in an INFO row, `adf vendor bloat (not blocking)`, naming each file, why it does not block, and a per-file `adf tidy --source` command. `--tidy-skipped` without `--staged` is a usage error (exit 2). Without `--staged`, doctor is unchanged, so `charter doctor --ci` in CI still fails on all vendor bloat. The hook-only `CHARTER_HOOK_SCOPED_DOCTOR` variable is described under the pre-commit hook in Changed. +- **`charter adf tidy --source` is repeatable, and `--print-paths` lists the files tidy writes** (`#320`) — each `--source` is processed in argument order. Two names for one file (`AGENTS.md` and `agents.md` on a case-insensitive filesystem, or a hard link) are tidied once, under the first name. A `--source` with no value is now a usage error (exit 2); it used to tidy every vendor file. `--print-paths` prints one `vendor ` or `module ` line per file tidy writes (or would write, under `--dry-run`) and nothing else on stdout; warnings go to stderr. It cannot be combined with `--format json` (exit 2). JSON output adds `paths` (`vendor`, `modules`); `modulesModified` is unchanged. A single `--source` gives the same output as before. +- **`charter adf migrate` registers the modules it routes content into** (`#239`, `#241`) — migrate could create `infra.adf`, `frontend.adf` or `core.adf` without adding them to `.ai/manifest.adf` or the `CLAUDE.md` module index, so no agent loaded them and `charter doctor` failed until they were wired by hand. + - Each new target module is registered after the module merge and before the pointer is written, so the hybrid `CLAUDE.md` index includes it. A source other than `CLAUDE.md` that adds a module refreshes that index afterwards. + - `core.adf` and `state.adf` go to `DEFAULT_LOAD`, by the same rule `doctor` checks. Other modules go to `ON_DEMAND` with up to six triggers inferred from the headings routed there. The heading word the router matched counts double, a named module's curated triggers come first, words that already trigger another module are dropped, and the lowercase module stem is the fallback. + - Registration is append-only. New bullets are inserted after the section's last item in its indentation and bullet style, and a missing section is appended at the end. Comments, tables, section order, blank lines and line endings are kept byte for byte. The edited manifest is re-parsed and must equal the old one plus exactly the new entries, or nothing is written and a warning is printed. + - An existing entry is never rewritten, moved or given new triggers, and is reported as it stands. A re-run writes nothing. An empty section header counts as an empty list and is filled. `core.adf` under `ON_DEMAND` is left in place with a warning naming the move. + - A missing `manifest.adf` warns to run `charter adf init`, and an unparseable one warns to fix it or run `charter doctor`. In both cases registration is skipped. + - The plan prints a `REGISTER` line per module, and `--format json` adds `sources[].registrations[]` (`module`, `loadPolicy`, `triggers`, `triggerSources`, `reason`, `status`, `skipReason`). `--dry-run` writes nothing and reports each planned module once across sources. Known gap: it does not simulate an earlier source's new triggers when routing a later one. + - Bootstrap's orphan registration uses the same writer, which removes a writer that could register a module twice. Orphan triggers are now the lowercase stem (`Observability.adf` gets `observability`). A `DEFAULT_LOAD` that is not a list is skipped with a warning instead of getting a raw block appended. Under `--format json`, writer warnings appear in the phase 3 step's `warnings`. A `bootstrap --yes` run registers migrate's modules in phase 4, so a re-run finds no orphans. `charter adf create` keeps its own writer. The stale `charter adf register` hint in bootstrap is tracked in `#347`. + +### Changed + +- **`charter adf compile --write` acts on each target's mode** (`#318`, `#295`) — the mode comes from `VENDORS`, or from the file's markers when there is no entry. Compile used to change a pointer's mode silently under `--force`. + - `--write` writes targets that are compiled, or absent. Under `--target all`, pointer and unmanaged targets are skipped (exit 0), and each `[skip]` line names the command that converts it. A single such `--target` is refused (exit 1). `--force` alone never changes a mode; it only overrides the hand-authored guard (`refused-hand-authored` for a file with no Charter marker). + - `--mode compiled` converts a pointer or unmanaged target. A clean bootstrap pointer converts without `--force`. `compiled` is recorded before writing; if it cannot be recorded, nothing is written (`refused-record`). If the write then fails, the record is restored and the remaining targets are processed. A converted file is reported as `Written CLAUDE.md (mode: pointer -> compiled)`. + - Converting a pointer still refuses (exit 1, nothing written, `VENDORS` unchanged) over content the output would not carry. `refused-bloated` covers items `adf tidy` would extract: run `adf tidy --source ` first, or pass `--force` to discard them. `refused-would-drop` covers other lines of yours. `--mode compiled --force` over a hand-authored file carries its retained sections and discards the rest. + - JSON output adds `skipped` and per-target `targets` (`state`, `source`, `status`, `recordedMode`, `previousMode`, `remedy`, `writeError`, `retainedSections`, `retainedSource`, `retainedSkipped`, `retainedAmbiguous`). A compiled-only repo with no `VENDORS` section compiles as before. +- **`charter adf compile --check` checks each target by its mode** (`#318`, `#303`) — it used to compare every target byte for byte against compiled output, so a thin pointer was reported `[stale]` forever. + - A compiled file is `fresh` or `stale`, where the expected bytes are the render plus the file's own retained block, so an edit inside the block never makes it stale. Retained sections outside the block read `[stale]` with `staleReason: "retained-unfenced"` until one `compile --target --write` moves them in. A possibly-stale legacy section reads `staleReason: "retained-ambiguous"` (see Fixed). + - A pointer is `pointer-ok`, or `pointer-bloated` (exit 1, remedy `charter adf tidy --source `) when tidy would extract something. An unmanaged file is `skipped-unmanaged` (exit 0). A recorded `compiled` or `pointer` file that looks like another mode is `mode-drift` (exit 1), with both fixes printed: `charter adf vendor set ` to keep the file first, then the command that regenerates it, with how many items that would discard. A missing file is `missing`. For a missing recorded pointer, or one now holding another mode's content, the remedy is `charter adf vendor restore ` (or `adf vendor set unmanaged`). + - No remedy suggests `compile --write` for a pointer or unmanaged file. JSON keeps `stale` (which now also lists bloated and drifted files), `current`, `missing`, `upToDate` and `nextActions`, and adds `skipped` and `targets`, each row with `staleReason` (`content`, `retained-unfenced`, `retained-ambiguous`, or null). +- **`charter adf tidy` reads each vendor file's mode** (`#318`, `#302`) — it processes a file only when it is a pointer: no compile banner, at least one pointer marker, and not recorded otherwise. The banner wins whatever the record says (`#296`). + - A file recorded `compiled` without a banner is skipped with status `compiled`. Its warning names the content-preserving fix first (`adf vendor set pointer`, then `adf tidy --source `) and says how many items regenerating would discard. + - An unmanaged file (recorded, or hand-authored with no marker) has status `unmanaged` instead of `not-found`, with a warning when it is recorded or named with `--source`. A recorded pointer with no marker is skipped rather than guessed at. + - File rows gain `vendorFile`, `mode`, `source` and `skipReason`. A `--source` path that is not a repo-root vendor name keeps the marker logic. `AGENTS.md` and `agents.md` that are one physical file are walked once. + - With no `SYNC` entries, `charter adf sync` now points to `charter adf vendor status`, in text and in JSON `nextActions`. +- **The pre-commit hook tidies only staged vendor files, and its gate blocks only on bloat in files the commit includes** (`#320`, `#357`) — re-run `charter hook install --pre-commit` to pick this up. An installed hook keeps its old behaviour, and `charter doctor` reports it with an INFO `pre-commit hook` row that does not fail `--ci`: a pre-`#320` hook, a `#320`-era hook, or, only in a repo with a `verify:adf` script, a hook from the first `#357` fix. Reinstalling overwrites a Charter-managed hook without `--force`. + - The old hook ran a whole-tree `adf tidy` whenever any vendor file was staged, then ran `git add` on every staged vendor file and on all of `.ai/*.adf`. That swept unrelated `.ai/` edits into the commit and rewrote vendor files that were not part of it. + - The new hook passes each staged vendor file (`AGENTS.md` and legacy `agents.md` included) to `adf tidy --source`, and stages only the paths both the dry run and the real run report. A commit staging only compiled output makes no CLI call. + - A staged vendor file that also has unstaged changes is skipped with a message, because tidy works on the working tree and staging its result would commit those hunks. If tidy would write into a module that has unstaged changes or is untracked, nothing is tidied for that commit and a message says why. A gitignored module is written and left unstaged, and the hook says so. A file the hook cannot stage, or one the real run wrote that the dry run did not plan, is named and left unstaged rather than aborting the hook. + - The tidy block never fails a commit; the evidence gate after it can. That gate now runs `charter doctor --adf-only --ci --staged --tidy-skipped ""`, so a commit is no longer refused for bloat it does not contain: an unstaged `GEMINI.md`, a partially staged file the hook skipped, or a file skipped because a module has work in progress. A staged vendor file that tidy ran on and could not clean, or that was not tidied because tidy failed or `CHARTER_SKIP_TIDY=1` is set, still blocks. Every other doctor check still blocks, vendor-mode drift and a malformed `VENDORS` entry included. + - The skip messages no longer claim the gate will block. They say that a plain `charter doctor --ci` still reports the file's bloat. A failing gate no longer prints a "Blocked because … auto-tidy was skipped" line, or runs an extra `adf tidy --dry-run` per skipped file. + - The scoped doctor runs in every repo, including bootstrapped ones. After it, the gate runs `verify:adf` if `package.json` has that script (`charter setup` and `bootstrap` add one), otherwise `charter adf evidence --auto-measure --ci`. So a repo whose custom `verify:adf` leaves doctor out now gets `npx charter doctor --adf-only --ci --staged …` on every commit. The 1.x hook called `pnpm run verify:adf` by name, which kept that script's doctor unscoped and failed on machines without pnpm. `verify:adf` now runs with `pnpm` when the repo has a `pnpm-lock.yaml` and pnpm is installed, with `yarn` for a `yarn.lock` and yarn installed, and with `npm` otherwise. + - So that the doctor inside `verify:adf` does not re-check bloat across the whole tree, the hook sets `CHARTER_HOOK_SCOPED_DOCTOR` to a one-time token and writes the same token to its own file in the git directory, removed on exit (one file per run, so overlapping commits in one worktree do not interfere). When the two match, that doctor reports `adf vendor bloat (checked by pre-commit hook)` as INFO and says so on stderr. Every other doctor check, and the script's other commands, still block. If the hook cannot write the file, it prints one `[pre-commit]` line and that run's `verify:adf` checks every vendor file. The variable without the matching file has no effect, so CI, or ` run verify:adf` run by hand, still checks every vendor file. + - The new hook needs a 2.0 CLI. With an older local CLI that does not know `--print-paths`, it reports the files as clean and tidies nothing, and the gate ignores `--staged` and `--tidy-skipped` and behaves as before. +- **The commit-msg hook rejects a missing trailer when `git.requireTrailers` is on** (`#299`) — `charter hook install --commit-msg` used to install a normalizer that exited 0. With bootstrap's default `requireTrailers: true`, every commit passed locally and the first push failed `charter validate --ci`. + - The hook keeps the `git interpret-trailers` normalization, then applies CI's rule. Either `Governed-By` or `Resolves-Request` counts, with the key case-insensitive, only inside the terminal trailer block. It checks the message git will record: comment lines stripped (honouring `core.commentChar`) and a `commit -v` diff below the scissors line dropped. + - The config is read with `node` and resolved as `validate` resolves it. An absent `git` section or `requireTrailers` key means the policy is on, any truthy value turns it on, `citationStrictness` defaults to `FAIL`, and an unparseable file falls back to the defaults, which the hook names on stderr. + - It blocks exactly when CI would fail the commit for the policy (`FAIL` or `STRICT`, or `ci.failOnWarn: true`), and prints the reason with an example, `Governed-By: ADR-001`. Advisory strictness prints a warning and lets the commit through. + - It skips the gate with a stderr warning when there is no `.charter/config.json`, or no `node` on `PATH`. Like CI, it exempts nothing: merge, revert and `fixup!` messages need a trailer too. Known gap: it always strips `#` lines, but under `-m` or `--cleanup=verbatim` git keeps them, so a trailer followed by a `#...` line can pass locally and still fail CI. Baselining the CI validation range at adoption is not addressed. + - `charter doctor` counts the new hook as a local gate. For a commit-msg hook installed before this change, it names the hook and asks for a re-install, which overwrites it without `--force`. +- **`charter bootstrap --force` no longer overwrites hand-authored or compiled vendor files** (`#318`) — this changes what a documented flag does, and fixes data loss. `--force` used to overwrite every existing pointer-file name in phase 3, before phase 4 could migrate it. Vendor files are not backed up, so a hand-authored `CLAUDE.md`'s rules were gone with no `.pre-adf-migrate.bak`. + - `--force` still re-scaffolds `.ai/*.adf` (with backups in `.ai/.backup/`) and still rewrites Charter thin pointers from the current template. + - It now skips a `CLAUDE.md`, `AGENTS.md`, `.cursorrules`, `GEMINI.md` or `copilot-instructions.md` that has no Charter marker, carries the compile banner, or is recorded `compiled` or `unmanaged`. The warning names the next step: `adf migrate --source `, or `adf vendor set pointer && adf vendor restore ` for compiled output. + - With `--yes`, phase 4 then migrates a hand-authored file that has no `VENDORS` entry into `.ai/` and writes the backup, and the phase 3 warning says so. A hand-authored file that already has an entry is left as it is. Phase 4 no longer migrates a file recorded `unmanaged`. + - Valid `VENDORS` entries are carried over to the re-scaffolded `manifest.adf`. To replace a hand-authored file without migrating it, delete it and re-run bootstrap. +- **`charter score` counts a thin pointer as substantive when `.ai/` is** (`#318`) — score numbers can only go up. A pointer `CLAUDE.md`, `.cursorrules` or `AGENTS.md` used to score as "thin" (−5, and "Expand `CLAUDE.md` beyond a thin pointer") however much `.ai/` held. It is now substantive when its own text is, or when the file `adf compile` would render for it is (compiled in memory; nothing is written). Hand-authored and compiled files are scored on their text as before. `signals.agentConfig` adds `mode`, `source` and `substantiveVia` per file. Recommendations no longer tell you to edit a pointer or compiled `CLAUDE.md`. A pointer with little to route to gets "Add repo commands and guardrails to `.ai/core.adf` (or run: charter adf populate)". +- **`charter adf migrate` without `--source` skips a file recorded `unmanaged`** (`#318`) — naming the file with `--source` still migrates it. Its entry changes to `pointer` before the pointer is written, and back to `unmanaged` if the migration fails, so doctor reports no drift afterwards. +- **`@stackbilt/adf` API additions** (`#239`, `#318`) — `parseManifest` gains a permissive `vendors` field. `RoutingTrace` gains an optional `headingKeyword`. `classifyElement` now always sets `routingTrace`, with `candidateScores: {}` when there is no trigger map. That is additive, but code that read a defined `routingTrace` as "content routing ran" should check `candidateScores` instead. +- **CLI reference completeness** (`#308`, `#321`) — added entries for `adf compile`, `adf tidy`, `adf populate`, `adf context` and `adf suggest`, and for the top-level `score`, `telemetry` and `stamp-policies` commands, which were implemented but undocumented. Corrected the `adf tidy` entry, which said compiler output was skipped only by the pre-commit hook. Added [Upgrading to 2.0](./docs/upgrading-to-2.0.md). ### Fixed -- **The pre-commit hook's evidence gate blocks on vendor bloat only in files the commit includes** (`#357`) — re-run `charter hook install --pre-commit` to pick this up. After staged-only tidy (`#320`), the generated hook (no `verify:adf` script) still ran `charter doctor --adf-only --ci` over the whole working tree, so a commit could be refused for bloat it did not contain: an unstaged `GEMINI.md`, a partially staged vendor file the hook skips, or a vendor file skipped because a module tidy would write has unstaged or untracked work. The hook skips such files on purpose, and blocking on them was a regression from `#320`; the old hook only passed these commits by rewriting or staging files you had not staged. The gate now runs `charter doctor --adf-only --ci --staged --tidy-skipped ""`. `--staged` limits the `adf vendor bloat` check to vendor files staged in the index; `--tidy-skipped` takes the staged files the tidy block skipped off that list. Bloat in the rest is reported in a new INFO row, `adf vendor bloat (not blocking)`, which names each file and why it does not block, and gives a per-file `adf tidy --source` command. A staged vendor file that tidy ran on and could not clean, or that was not tidied because tidy failed or `CHARTER_SKIP_TIDY=1` is set, still blocks. Every other doctor check still blocks as before, vendor-mode drift and a malformed `VENDORS` entry included. Without `--staged`, `charter doctor` is unchanged, so CI's `charter doctor --ci` still fails on all vendor bloat. The skip messages no longer claim the gate will block, and say that a plain `charter doctor --ci` still reports the file's bloat. The gate's failure output no longer has the "Blocked because … auto-tidy was skipped" line, so a failing gate no longer runs an extra `adf tidy --dry-run` for each skipped file. `--tidy-skipped` without `--staged` is a usage error (exit 2). An installed hook keeps the old gate until you reinstall, and `charter doctor` reports a `#320`-era hook with an INFO `pre-commit hook` row. With an older local CLI, the new hook's `--staged` and `--tidy-skipped` flags are ignored and the gate behaves as before. -- **The pre-commit hook's commit-scoped gate now also applies in repos with a `verify:adf` script, and runs that script with the repo's package manager** (`#357`) — re-run `charter hook install --pre-commit` to pick this up. The scoped gate above applied only when `package.json` had no `verify:adf` script. `charter setup` and `charter bootstrap` add one, so in most Node repos the hook ran `pnpm run verify:adf` instead, and that script's `charter doctor --adf-only --ci` still blocked a commit on bloat in vendor files it did not include. The hook also called `pnpm` by name, so the gate failed in npm and yarn repos on machines without pnpm. The hook now always runs the scoped doctor first, then `verify:adf` if the script exists, or `adf evidence` if it does not. This means a repo with a `verify:adf` script now gets `npx charter doctor --adf-only --ci --staged …` on every commit, even when its custom `verify:adf` leaves doctor out. It runs `verify:adf` with `pnpm` when there is a `pnpm-lock.yaml` and pnpm is installed, with `yarn` when there is a `yarn.lock` and yarn is installed, and with `npm` otherwise. While `verify:adf` runs, the hook sets `CHARTER_HOOK_SCOPED_DOCTOR` to a one-time token and writes the same token to its own file in the git directory, named after the token, which it deletes when it exits. Two commits running at once in one worktree therefore never delete each other's file. Doctor skips the `adf vendor bloat` check only when the variable and that file match. If the hook cannot write the file, it prints one `[pre-commit]` line saying so, and the doctor inside `verify:adf` checks every vendor file that time, as CI does. It then reports the check as an INFO row, `adf vendor bloat (checked by pre-commit hook)`, and prints a line saying so on stderr. Every other doctor check in `verify:adf` still blocks, and so do the script's other commands. Setting the variable without the file changes nothing, so a CI job that sets it by mistake still gets a full `charter doctor --ci`. `charter doctor` now also reports a hook installed with the earlier `#357` fix with an INFO `pre-commit hook` row, but only in a repo with a `verify:adf` script. Without one, that hook already runs the scoped gate. -- **`adf compile --write` no longer deletes your `## Environment` section from a compiled file** (`#303`) — the `#295` guard only covered pointer files. Re-compiling a file that already carried the compile banner silently dropped a user-added `## Environment` (or protocol) section with exit 0, and git was the only way back. Those sections are now kept in the retained block. -- **`charter bootstrap --force` no longer destroys a hand-authored `CLAUDE.md` before it can be migrated** (`#318`, part 3) — phase 3 overwrote every existing pointer-file name with the template under `--force`, before phase 4 (migrate) ran. Vendor files are not backed up (only `.ai/*.adf` go to `.ai/.backup/`), so phase 4 then found a pointer, migrated nothing, and the file's rules were gone with no `.pre-adf-migrate.bak`. The file is now skipped in phase 3, and with `--yes` phase 4 migrates it into `.ai/` and writes the backup (when it has no `VENDORS` entry). -- **`charter adf compile --check` and `charter adf tidy` read each vendor file's mode** (`#318`, part 2; `#302`) — `--check` compared every target byte-for-byte against compiled output, so a thin pointer was reported `[stale]` forever. Each target is now checked by its mode. A compiled file is `fresh` or `stale` as before. A pointer is `pointer-ok` when `adf tidy` would extract nothing from it, and `pointer-bloated` (exit 1, remedy `charter adf tidy --source `) when it would. An unmanaged file is `skipped-unmanaged` (exit 0). A recorded `compiled` or `pointer` file that looks like another mode on disk is `mode-drift` (exit 1), with both fixes printed: `charter adf vendor set ` to keep the file, or the command that restores it. A missing file is `missing`, and a recorded pointer that is missing names `git checkout -- ` or `adf vendor set unmanaged`. No remedy suggests `compile --write` for a pointer or unmanaged file. JSON keeps `stale` (which now also lists bloated and drifted files), `current`, `missing`, `upToDate` and `nextActions`, and adds `skipped` and `targets`. `adf tidy` processes a file only when it is a pointer: no compile banner, at least one pointer marker, and not recorded otherwise. The banner still wins whatever the record says (`#296`). A file recorded `compiled` is skipped with status `compiled`. Its warning lists the content-preserving fix first (`adf vendor set pointer`, then `adf tidy --source `) and says how many items regenerating would discard; `--check` mode-drift does the same. An `unmanaged` file (recorded, or hand-authored with no marker) now has status `unmanaged` instead of `not-found`, and its warning appears when it is recorded or named with `--source`. A recorded pointer with no marker is skipped rather than guessed at. File rows gain `vendorFile`, `mode`, `source` and `skipReason`. A `--source` path that is not a repo-root vendor name keeps the old marker logic. Tidy now finds files through the shared discovery helper, so `AGENTS.md` and `agents.md` that are one file on a case-insensitive filesystem are walked once, not twice. With no `SYNC` entries, `charter adf sync` now points to `charter adf vendor status` in text and in JSON `nextActions`, rather than ending at "Wrote empty lock file". Record entry problems are still reported only by `adf vendor status`. `doctor`, `score`, `bootstrap` and both pre-commit hooks are unchanged and are later parts of `#318`. -- **`charter doctor` now names the fix when `core.adf` or `state.adf` is missing from `DEFAULT_LOAD`** (`#241`) — the `adf required wiring` warning said only `Missing from DEFAULT_LOAD: core.adf`. It now says to move the module from `ON_DEMAND` to `DEFAULT_LOAD` in `.ai/manifest.adf` when it is registered there, or to add it (or run `charter adf create --load default`) when it is not registered at all. The rule itself is unchanged and is filename-only — `core.adf` and `state.adf` are required in `DEFAULT_LOAD`, and module content is not consulted — but it now lives in one shared helper that `doctor` and `bootstrap`'s orphan registration both call, so `adf migrate` can adopt the same classification when it starts registering the modules it creates (`#239`). -- **`charter validate --ci` no longer counts a whitespace-only trailer value** (`#340`) — the trailer parser in `@stackbilt/git` matched values with `\s*.+`, which backtracks to accept `Governed-By: `, so a commit the commit-msg gate from `#299` rejects still counted as governed in CI. Values must now contain a non-whitespace character, matching the hook, which sees the message after `git stripspace` has dropped trailing blanks. As in the hook, such a line also ends the terminal trailer block, so trailers above it are not counted either. Whitespace before or after a real value is still accepted and trimmed, as before. -- **`charter adf migrate` now discovers `AGENTS.md` without `--source`** (`#343`) — its default discovery list spelled the file `agents.md`, so on a case-sensitive filesystem a bare `charter adf migrate` never found the `AGENTS.md` that bootstrap, `adf init --emit-pointers` and the `agents` compile target write; on a case-insensitive one it was found but reported as `agents.md`. Bootstrap's phase-4 migrate scan had its own copy of the same list and the same miss. Both now share one discovery helper that lists `AGENTS.md` alongside the legacy lowercase spelling, as `adf tidy` and `doctor` already did, and reports a file reached by both names once, under `AGENTS.md`, so it is never migrated twice. Files are identified by their full-precision (bigint) inode, because NTFS file indexes exceed 2^53 and, read as a JS number, two different files such as `CLAUDE.md` and `.cursorrules` could collide and one be dropped (`#318`). -- **The commit-msg hook now rejects a missing trailer when `git.requireTrailers` is on** (`#299`) — `charter hook install --commit-msg` installed a normalizer, not a gate: it rewrote a `Governed-By` or `Resolves-Request` trailer the author had already typed and exited 0 otherwise. With bootstrap's default `requireTrailers: true`, every commit passed locally and the first push failed `charter validate --ci`, so the author learned about the policy from a red build rather than at the moment they could act. The generated hook keeps the `git interpret-trailers` normalization and then applies the same rule CI does: either trailer, key case-insensitive, counts only inside the message's terminal trailer block, and the check runs on the message git will record — comment lines stripped (honouring `core.commentChar`) and a `commit -v` diff below the scissors line dropped. The config is read with `node`, which the generated pre-commit hook already requires through `npx`, and resolved the way `loadConfig` resolves it for `validate`: each section merged over the defaults, so an absent `git` section or `requireTrailers` key means the policy is on, any truthy `requireTrailers` turns it on, `citationStrictness` defaults to `FAIL`, and an unparseable file falls back to the defaults — which the hook names on stderr so the rejection is explicable. It blocks exactly when CI would fail the commit for the policy — `FAIL` or `STRICT`, or `ci.failOnWarn: true` — and prints the reason with a one-line example, `Governed-By: ADR-001`. Advisory strictness prints a warning and lets the commit through, matching CI's `WARN`. Two deliberate exceptions to parity skip the gate with a stderr warning: no `.charter/config.json` at all, since the hook cannot tell that the repo opted in, and no `node` on `PATH`, since the config cannot be evaluated. One known gap: the hook always strips `#` lines, as `git commit` does with its default editor cleanup, but under `-m` or `--cleanup=verbatim` git keeps them, so a trailer followed by a `#...` line can pass locally and still fail CI. Like CI, the hook exempts nothing — merge, revert and `fixup!` messages need a trailer too. `charter doctor` now counts the new hook as a local gate, and for a commit-msg hook installed before this change names it and asks for a re-install, since the old marker line is kept so re-installing overwrites it without `--force`. Baselining the CI validation range at adoption, the issue's second request, is not addressed here. -- **`charter bootstrap` no longer advises `--force` for an existing pointer file** (`#305`) — the two phase-3 warnings for a `CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, `.cursorrules` or `copilot-instructions.md` that bootstrap declined to replace both ended in "use --force to overwrite". Bootstrap's `--force` is not scoped to pointer files: it also re-scaffolds `.ai/*.adf` and overwrites customized modules, so the advice traded one file for the hand-authored ADF content Charter exists to protect. A file with custom content now names `charter adf migrate --source ` (spelled out so the migration is scoped to that one file rather than every agent config file `migrate` discovers), or hand-editing or deleting the file and re-running bootstrap. A file that is already Charter-managed says so and names deletion as the way to regenerate it. Both warnings state that `--force` also re-scaffolds `.ai/*.adf`, matching the `.mcp.json` warning from `#304`. The flag itself is unchanged; splitting it into scoped flags remains open. -- **`adf tidy` no longer folds compiled vendor output back into `.ai/`** (`#296`) — one marker list was answering two different questions. `adf compile --write` renders the whole ruleset into `CLAUDE.md` behind a `GENERATED by charter adf compile` banner, and that banner was a member of `POINTER_MARKERS`, so `adf tidy` classified the artifact as a thin pointer and treated everything past its preamble — the compiled copy of `.ai/` itself — as content the user had added beyond the pointer. Every run copied the rendered ruleset back into the source it was rendered from, and the pre-commit hook committed the result silently: the reporter's hand-authored 26-line `core.adf` reached 66 lines, with the module index, `METRICS` and `state.adf`'s `STATE` arriving inside `core.adf` `CONSTRAINTS`, and a rule they had deliberately deleted restored to `[load-bearing]` from the stale artifact. The markers are now two lists with a note at the definition explaining why they must not be merged again. `POINTER_MARKERS` answers "is this file a thin pointer?" and excludes the banner; `CHARTER_OWNED_MARKERS` answers "did Charter author this file?" and is the banner plus every pointer marker. The `adf compile --write` overwrite guard (`#295`), `adf migrate`, `bootstrap`, `doctor` and `score` all ask the ownership question and are unchanged — a compile artifact stays overwritable without `--force`, and stays something `migrate` will not ingest. `adf tidy` asks the pointer question, and rather than skipping in silence it now names the file as compiler output and points at `adf compile --write` as the way to regenerate it. The wider vendor-mode questions this exposes — `compile --check` reporting thin pointers as permanently stale, and `adf sync --write` writing an empty lock — are filed separately as `#318` and are not addressed here. -- **`charter bootstrap` now prints every phase's warnings, not only some of them** (`#307`) — every phase added `step.warnings.length` to the `Bootstrap complete. N warnings.` tally, but phases 6 (`populate`) and 7 (`doctor`) never printed the text, so a default run reported a count with nothing on screen to read and the wording was reachable only under `--format json`. That is the same silence `#304` fixed in phase 2, where it had been hiding advice to run a destructive `bootstrap --force` (`#305`) — so rather than copy that print loop into two more places, recording a phase and showing it are now one operation. Every phase passes through a single `completePhase` helper that appends the step to the result, draws the phase's section, and prints as `Warning: ` whatever that section did not already word itself; it is the only code path into `result.steps`, so a phase cannot reach the JSON payload without its warnings reaching the screen, and a future phase 8 cannot reintroduce the gap by omission. Phases that word their own warnings are untouched and still print them once: phase 1's wrapped not-a-git-repo notice, phase 4's dry-run list, and phase 5's `Failed:`/`Hint:`/`Retry manually:` lines. Two further warnings that were recorded but never shown now appear — `Detection failed:` from phase 1's failure path and `Migration failed:` from phase 4's — and the summary tally itself is unchanged. -- **`charter bootstrap` now recognises a Windows absolute `--ai-dir` on a POSIX host** (`#307`) — the check that warns when a git-tracked `.mcp.json` pins a machine-specific `--ai-dir` (`#298`) gated on `path.isAbsolute`, which is the POSIX implementation on Linux and macOS and so reports `false` for `C:\Users\...` and `\\server\share\...`. A `.mcp.json` authored on native Windows is read on a POSIX host every time it reaches CI, a container or a colleague's checkout — precisely the cross-machine sharing the warning exists to catch — and it passed in silence. The value is now tested under `path.win32` as well as the host's own flavour, so drive-letter roots (backslash or forward slash) and UNC roots are detected wherever bootstrap runs; a rooted, driveless `\project\.ai` is absolute under `path.win32` too and is flagged for the same machine-specific reason. Drive-relative `C:project\.ai`, which is absolute under neither flavour, and the repo-relative `.ai` that bootstrap itself writes stay unflagged, and the tracked-ness gate from `#298` is unchanged: an untracked `.mcp.json` remains the documented machine-local escape hatch. -- **`charter doctor` now reports a telemetry log that is already tracked by git** (`#306`) — `#298` stopped newly bootstrapped repos from staging `.charter/telemetry/events.ndjson` by writing `telemetry/` into the generated `/.gitignore`, but an ignore rule never untracks a file already in the index. A repo bootstrapped before that change had nothing stopping the log from being staged, and once it is tracked every `git add -A` stages it again — while the documented one-time cleanup only helps people who go looking for it. `doctor` now reads the git index for `/telemetry`, names the tracked files, and prints the remedy in the order that actually works: ensure `telemetry/` is ignored *first*, then `git rm -r --cached "/telemetry"` — otherwise the next reflexive `git add -A` re-stages the file, since the untrack leaves it on disk so `charter telemetry report` keeps working. The diagnostic is advisory (`INFO`) and **never fails CI**: the condition is state Charter's own bootstrap created rather than anything the user opted into, so reporting it cannot break an existing repo's build on upgrade — the same ruling as the source LOC budget nudge (`#186`). The check honours `--config`, consults only the index so the result is identical on case-sensitive and case-insensitive checkouts, and is withheld under `--adf-only` and outside a git repository. -- **`charter --help` now reaches the command's own docs** (`#240`) — the top-level dispatcher matched `--help`/`-h` anywhere in the raw arguments, so `charter adf migrate --help` printed the global help and the ADF subcommand reference was only reachable by running `charter adf` bare. `adf`, `hook`, `score` and `telemetry` now render their own help. The remaining commands have no `--help` handler and deliberately keep the root-help fallback, so a stray `--help` can never execute one for real. -- **`adf populate` no longer invents an ESM extension rule for bundler projects** (`#294`) — the constraint "Use .js extensions for all ESM imports" was emitted whenever any `package.json` carried `"type": "module"`, which is true of every Vite, esbuild and modern Workers project. Under `moduleResolution: "bundler"` extensionless relative imports are the correct convention, so bootstrap was writing a wrong rule into `CONSTRAINTS [load-bearing]` and inviting agents to "fix" a correct codebase. The rule is now gated on TypeScript's `moduleResolution` — emitted for `node16`/`nodenext`, withheld for `bundler`, `node` and anything unrecognised — and a single bundler package vetoes it for the repository. Ambiguity (an unparseable JSONC `tsconfig.json`, a solution-style config, an `extends` chain this resolver does not follow) withholds the rule rather than guessing. -- **`adf compile --write` no longer refuses the files `bootstrap` just generated** (`#295`) — the overwrite guard keyed on the compile banner alone, so a clean `bootstrap` followed by the documented `adf compile --target all --write` produced a wall of refusals and trained users to reach for `--force`. The guard now recognises Charter thin pointers as well as compiler output. Hand-authored files carrying no Charter marker are still refused, and a file converted from pointer to compiled output is reported as `(replaced charter pointer stub)` so the transition is visible. -- **`adf compile --write` no longer discards retained sections** (`#295`) — it refuses, rather than silently overwriting, a pointer file carrying your own content under `## Environment` or another retained heading. Compiled output has no such section and `.ai/` does not hold that content, so the previous behavior lost it with git as the only recovery. Pass `--force` to discard deliberately. Since `#303`, the file is written with that content carried into a retained block instead (see Changed). -- **`charter score` no longer counts directory-tree filenames as broken references** (`#293`) — a fenced tree block expresses the path prefix structurally, through indentation, so `random.ts` and `simulation/random.ts` were reported broken although `src/simulation/random.ts` exists. Grounding now falls back to matching a reference against the repository's real files before classifying it as broken. Two conditions must both hold: the basename is unique, and every path segment the author actually wrote agrees with the real file's trailing segments. The second is checked against the authored text rather than the resolved path, so a reference written inside `.ai/` still resolves, while a stale `src/utils/helper.ts` pointing at a file since moved to `packages/core/lib/helper.ts` stays broken — documentation rot is precisely what this check exists to surface. An ambiguous basename such as `index.ts` under several directories, and a filename that exists nowhere, also stay broken. On this repository the broken count falls from 79 to 48 and Grounding rises from 13/20 to 15/20, while genuinely absent files (`Cargo.toml`, `wrangler.toml`, `schema.sql`, `src/worker.ts`) are all still reported. - -### Changed - -- **CLI reference completeness** — added entries for `adf compile`, `adf tidy`, `adf populate`, `adf context` and `adf suggest`, which were implemented but undocumented, and for the top-level `score`, `telemetry` and `stamp-policies` commands, which were likewise missing. The `adf tidy` entry previously stated that compiler output was skipped by the generated pre-commit hook rather than by the command, and that tidy run directly against a compiled file would fold it back into `.ai/`; both were true when written and are corrected by `#296`. +- **`adf compile --write` keeps your `## Environment` notes** (`#303`, `#295`) — re-compiling a compiled file, or converting a pointer, silently dropped a user-added `## Environment` (or another section `adf tidy` retains) with exit 0, and git was the only way back. Compiled `CLAUDE.md`, `AGENTS.md`, `.cursorrules` and `GEMINI.md` now keep those sections in a `` … `` block at the end of the file. + - The next `--write` moves such sections into the block, so the file's bytes change once. Every later `--write` copies the block as it stands. A file with nothing to carry gets no block and compiles byte-identically to before. The block uses HTML markers in every target, `.cursorrules` included. + - A pointer converted with `--mode compiled` carries the retained sections that differ from its template. `--force` never drops retained sections. + - All-caps headings the compiler could have produced (a module since removed from `.ai/`) and `## Module Index` are never carried. A retained-looking section whose heading is still in a `.ai/` module is rendered `.ai/` text left behind by an edit: it is not carried, it is listed in `retainedSkipped`, and it is named on stderr. + - A section left inside a generated section that `.ai/` no longer renders, with no `.ai/` match, may be stale output or your own. `--write` refuses that target as `refused-ambiguous` (exit 1), with "wrap it in the retained block" as the first remedy and `--force` (which discards it) as the second. `--check` reports it with `staleReason: "retained-ambiguous"`. + - Parsing is code-fence aware, and marker lines match as loosely as `stripCharterSentinels`. `adf compile` to stdout and `charter score` still use the plain render. The interim `refused-retained` (`compile --write`) and `retained-divergent` (`vendor restore`) values from unreleased builds are retired. +- **`adf tidy` no longer folds compiled vendor output back into `.ai/`** (`#296`) — the compile banner counted as a pointer marker, so tidy treated everything in a compiled `CLAUDE.md` past its preamble as user content. Every run copied the rendered ruleset back into the source it came from, and the pre-commit hook committed the result: one reporter's 26-line `core.adf` grew to 66 lines, and a rule they had deleted came back as `[load-bearing]`. Tidy now names the file as compiler output and points at `adf compile --write`. A compile artifact stays overwritable without `--force`, and `migrate` still will not ingest it. +- **Bootstrap writes `AGENTS.md`, not lowercase `agents.md`** (`#297`) — bootstrap, `adf init --emit-pointers` and the generated pre-commit hook wrote `agents.md`, while `adf compile` writes `AGENTS.md`. On a case-sensitive checkout those are two files, and harnesses read only `AGENTS.md`. Existing repos can rename with `charter adf vendor rename-legacy`. +- **`charter adf migrate` discovers `AGENTS.md` without `--source`** (`#343`) — its default list, and bootstrap's phase-4 scan, spelled the file `agents.md`. On a case-sensitive filesystem a bare `charter adf migrate` never found `AGENTS.md`, and on a case-insensitive one it reported it as `agents.md`. Both now list `AGENTS.md` alongside the legacy spelling and report a file reached by both names once, under `AGENTS.md`, so it is never migrated twice. Files are identified by their full-precision (bigint) inode, because NTFS file indexes exceed 2^53 and two different files such as `CLAUDE.md` and `.cursorrules` could collide and one be dropped. +- **Two generated files were unsafe to commit** (`#298`) — `.charter/.gitignore` now covers `telemetry/`, so the per-machine `events.ndjson` is no longer staged by `git add -A`. `.mcp.json` now writes a relative `--ai-dir` instead of an absolute path, which broke the MCP server for every other clone and for CI. In an existing repo where git tracks a `.mcp.json` that pins an absolute `--ai-dir`, bootstrap leaves the file alone and warns, naming the fix: replace the path with the repo-relative one, or gitignore the file and `git rm --cached .mcp.json` if it is meant to stay machine-local. Doctor does not check this, so repos bootstrapped by 1.x should fix it by hand (see the upgrade guide). Bootstrap's setup-phase warnings are now printed in text mode. +- **`charter doctor` reports a telemetry log that git already tracks** (`#306`) — the `#298` ignore rule does not untrack a file already in the index. Doctor reads the index for `/telemetry`, names the tracked files, and gives the remedy in the order that works: make sure `telemetry/` is ignored first, then `git rm -r --cached "/telemetry"`. The file stays on disk, so `charter telemetry report` keeps working. The row is INFO and never fails CI. It honours `--config`, reads only the index (so case-sensitivity does not matter), and is withheld under `--adf-only` and outside a git repository. +- **`charter bootstrap` flags a Windows absolute `--ai-dir` on a POSIX host** (`#307`) — the `#298` check for a git-tracked `.mcp.json` with a machine-specific `--ai-dir` used the host's `path.isAbsolute`, so on Linux and macOS it missed `C:\Users\...` and `\\server\share\...`. Drive-letter roots (either slash), UNC roots and a rooted, driveless `\project\.ai` are now flagged wherever bootstrap runs. Drive-relative `C:project\.ai` and the repo-relative `.ai` bootstrap writes are not. An untracked `.mcp.json` remains the documented machine-local escape hatch. +- **`charter bootstrap` prints every phase's warnings** (`#307`) — phases 6 (`populate`) and 7 (`doctor`) counted warnings in the `Bootstrap complete. N warnings.` tally but never printed them. Every phase now prints its warnings, including `Detection failed:` (phase 1) and `Migration failed:` (phase 4), which were recorded but never shown. The tally is unchanged. +- **`charter bootstrap` no longer advises `--force` for an existing pointer file** (`#305`) — bootstrap's `--force` also re-scaffolds `.ai/*.adf` and overwrites customized modules, so "use --force to overwrite" traded one file for the hand-authored ADF content. A file with custom content now names `charter adf migrate --source `, or hand-editing or deleting the file and re-running bootstrap. A file that is already Charter-managed says so and names deletion as the way to regenerate it. Both warnings say that `--force` also re-scaffolds `.ai/*.adf`. Splitting `--force` into scoped flags remains open. +- **`charter doctor` names the fix when `core.adf` or `state.adf` is missing from `DEFAULT_LOAD`** (`#241`) — the `adf required wiring` warning said only `Missing from DEFAULT_LOAD: core.adf`. It now says to move the module from `ON_DEMAND` to `DEFAULT_LOAD` when it is registered there, or to add it (or run `charter adf create --load default`) when it is not registered. The rule itself is unchanged and filename-only, and `adf migrate` and bootstrap's orphan registration now use the same rule. +- **`charter validate --ci` no longer counts a whitespace-only trailer value** (`#340`) — the trailer parser in `@stackbilt/git` matched `Governed-By: `, so a commit the commit-msg gate rejects still counted as governed in CI. Values must now contain a non-whitespace character, matching the hook. As in the hook, such a line also ends the terminal trailer block, so trailers above it are not counted either. Whitespace around a real value is still trimmed. +- **`charter --help` reaches the command's own docs** (`#240`) — `charter adf migrate --help` printed the global help, because `--help` was matched anywhere in the arguments. `adf`, `hook`, `score` and `telemetry` now render their own help. The other commands keep the root-help fallback, so a stray `--help` never runs one for real. +- **`adf populate` no longer invents an ESM extension rule for bundler projects** (`#294`) — "Use .js extensions for all ESM imports" was emitted for any `"type": "module"` package, which includes every Vite, esbuild and modern Workers project, where extensionless imports are correct. The rule is now emitted only for TypeScript `moduleResolution` `node16`/`nodenext`, and is withheld for `bundler`, `node` and anything unrecognised. A single bundler package vetoes it for the repository. Ambiguity (an unparseable JSONC `tsconfig.json`, a solution-style config, an `extends` chain the resolver does not follow) withholds it rather than guessing. +- **`charter score` no longer counts directory-tree filenames as broken references** (`#293`) — a fenced tree block gives the path prefix through indentation, so `random.ts` was reported broken although `src/simulation/random.ts` exists. A reference now falls back to matching the repository's real files when the basename is unique and every path segment the author wrote agrees with the real file's trailing segments, so a reference written inside `.ai/` still resolves. A stale `src/utils/helper.ts` for a file moved to `packages/core/lib/helper.ts`, an ambiguous basename such as `index.ts`, and a filename that exists nowhere all stay broken. On this repository the broken count falls from 79 to 48 and Grounding rises from 13/20 to 15/20. +- **Supply-chain advisories** (`#337`) — raised stale `pnpm.overrides` pins (`fast-uri`, `ip-address`) to patched versions, added pins for `proxy-addr` and `source-map-js`, and bumped `@modelcontextprotocol/sdk` to `^1.32.1` (GHSA-6qxp-vccf-f47h). ## [1.9.3] - 2026-09-08 diff --git a/README.md b/README.md index aa0e6fc..f1cc30f 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,8 @@ npm install --save-dev @stackbilt/cli For pnpm workspaces: `pnpm add -Dw @stackbilt/cli`. For global install: `npm install -g @stackbilt/cli`. +Upgrading a repo set up with Charter 1.x? `adf compile --write` now converts pointer files only with `--mode compiled`, and installed git hooks need a re-install. Follow [Upgrading to 2.0](docs/upgrading-to-2.0.md). + ### Non-Node repos (C++, Go, Rust, etc.) Charter works as a local governance tool in any repo — Node is not required as your primary build system. Add it as a dev dependency and invoke it without a global install: @@ -255,15 +257,17 @@ For live session continuity snapshots, use `charter context-refresh` to produce | I want to compile task-specific agent context | `charter adf bundle --task "Fix the login flow"` | | I want to enforce metric/file-size ceilings | `charter adf evidence --auto-measure --ci` | | I want to migrate existing agent docs | `charter adf migrate --dry-run` | +| I want to see whether each vendor file is a pointer or compiled output | `charter adf vendor status` (record the modes with `charter adf vendor init`) | | I want to regenerate CLAUDE.md / AGENTS.md / .cursorrules / GEMINI.md from `.ai/` | `charter adf compile --target all --write` (add `--mode compiled` to convert pointer files; your `## Environment` notes are kept in a retained block at the end) | | I want a letter-grade AI-readiness audit | `charter score` | | I want to validate governance in CI | `charter validate --ci --format json` | | I want to check pattern drift | `charter drift --ci --format json` | | I want to audit governance coverage | `charter audit --ci --format json` | -| I want to install commit trailer normalization | `charter hook install --commit-msg` | +| I want to require commit trailers before CI does | `charter hook install --commit-msg` | | I want to install ADF pre-commit checks | `charter hook install --pre-commit` | | I want session context for agent workflows | `charter context-refresh --once` or `charter hook print --claude` | | I want to start the MCP server for Claude Code / Codex / Cursor | `charter serve` | +| I am upgrading a repo set up with Charter 1.x | Follow [Upgrading to 2.0](docs/upgrading-to-2.0.md) | ## Commands diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 72c0b1a..a5bb4cb 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -404,7 +404,7 @@ npx charter adf create api-patterns --force # overwrite existi ### charter adf migrate -Scans existing agent config files (`CLAUDE.md`, `.cursorrules`, `agents.md`, `GEMINI.md`, `copilot-instructions.md`), classifies their content, and migrates structured blocks into ADF modules. Replaces originals with thin pointers. +Scans existing agent config files (`CLAUDE.md`, `.cursorrules`, `AGENTS.md` or legacy `agents.md`, `GEMINI.md`, `copilot-instructions.md`), classifies their content, and migrates structured blocks into ADF modules. Replaces originals with thin pointers. ```bash npx charter adf migrate --dry-run # preview migration plan diff --git a/docs/upgrading-to-2.0.md b/docs/upgrading-to-2.0.md new file mode 100644 index 0000000..62e5a81 --- /dev/null +++ b/docs/upgrading-to-2.0.md @@ -0,0 +1,219 @@ +# Upgrading to Charter 2.0 + +Charter 2.0 records whether each vendor file (`CLAUDE.md`, `AGENTS.md`, `.cursorrules`, `GEMINI.md`, `agents.md`, `copilot-instructions.md`) is a thin **pointer** to `.ai/`, **compiled** output of `charter adf compile`, or **unmanaged** (hand-authored, left alone). Commands act on that record instead of guessing from markers. Most repos upgrade with the seven steps below. The rest of this page explains what changes and what each new doctor line means. + +The full list of changes is in the [CHANGELOG](../CHANGELOG.md). Flag details are in the [CLI reference](./cli-reference.md). + +## Upgrade in seven steps + +Run these from the repository root, starting from a clean working tree (`git status` shows nothing to commit). + +```bash +# 1. Upgrade the CLI. The git hooks call `npx charter`, so the local devDependency must be 2.0 too. +npm install --save-dev @stackbilt/cli@latest # or: pnpm add -D @stackbilt/cli@latest +npx charter --version + +# 2. Clean up two files 1.x bootstrap made unsafe to commit (both commands are no-ops if already fine) +# a. Stop tracking the telemetry log: every charter run rewrites it, so it would land in the commit +grep -qx 'telemetry/' .charter/.gitignore 2>/dev/null || echo 'telemetry/' >> .charter/.gitignore +git rm -r -q --cached --ignore-unmatch .charter/telemetry +# b. Replace an absolute --ai-dir in .mcp.json with the repo-relative .ai (it breaks other clones and CI) +node -e 'const fs=require("fs"),f=".mcp.json";if(!fs.existsSync(f))process.exit();const j=JSON.parse(fs.readFileSync(f,"utf8")),a=(j.mcpServers&&j.mcpServers.charter&&j.mcpServers.charter.args)||[],i=a.indexOf("--ai-dir");if(i>=0&&/^([A-Za-z]:)?[\\/]/.test(a[i+1])){a[i+1]=".ai";fs.writeFileSync(f,JSON.stringify(j,null,2)+"\n");console.log("fixed .mcp.json")}' + +# 3. See what Charter infers about each vendor file, then record it +npx charter adf vendor status +npx charter adf vendor init + +# 4. Reinstall the hooks you use. Required: installed hooks keep their 1.x behaviour until you do. +npx charter hook install --pre-commit +npx charter hook install --commit-msg # only if you use the commit-msg hook + +# 5. Rename a lowercase agents.md, if you have one ("Nothing to rename" means you don't) +npx charter adf vendor rename-legacy --dry-run +npx charter adf vendor rename-legacy # stages the rename as a git mv + +# 6. Check, and re-compile if you use compiled files +npx charter adf compile --target all --check +npx charter adf compile --target all --write # only needed if --check reports [stale] + +# 7. Read doctor and fix what it names (see the table below), then commit +npx charter doctor +git add -u # manifest, re-compiled files, .gitignore, .mcp.json, package.json, lockfile +git status +git commit -m "chore: upgrade charter to 2.0" -m "Governed-By: ADR-001" # the trailer is required once the commit-msg hook is on +``` + +`charter adf vendor init` writes only to `.ai/manifest.adf`. It records each unrecorded file's inferred mode and never changes an existing entry. If `status` shows a mode you did not mean, record the right one with `charter adf vendor set `. + +`git add -u` stages every tracked file you changed, which is why the steps start from a clean tree. It also picks up `AGENTS.md` when `rename-legacy` rewrote its `# agents.md` title line. The telemetry log stays on disk after step 2, so `charter telemetry report` keeps working. Use your own policy reference in the trailer (`Governed-By:` or `Resolves-Request:`). If your `.ai/` directory is not `.ai`, use its repo-relative path in step 2b. If `.mcp.json` is meant to stay machine-local, skip 2b and instead add `.mcp.json` to `.gitignore` and run `git rm --cached .mcp.json`. + +## Breaking: only `--mode compiled` turns a pointer into compiled output + +In 1.9.3, `charter adf compile --write` refused the thin pointers that `bootstrap` writes, and its message said to "run --force once to convert". `--force` then replaced them with compiled output, which changed the file's mode without recording it. In 2.0, `--force` never changes a file's mode, and `--write` writes only targets that are compiled already, or absent: + +| Target is | `--target all --write` | `--target --write` | +|---|---|---| +| compiled, or absent with no record | written | written | +| pointer or unmanaged | skipped, exit 0, `[skip]` line names the fix | refused, exit 1 | + +`--target all --write` on a bootstrapped repo therefore exits 0 and writes nothing, where 1.9.3 exited 1. + +To switch files to compiled output, choose one: + +```bash +# Convert pointers in one run (records compiled first, then writes) +npx charter adf compile --target all --write --mode compiled + +# Or record the mode for one file, then compile as usual +npx charter adf vendor set CLAUDE.md compiled +npx charter adf compile --target claude --write +``` + +`--force` never changes a file's mode. It still means "overwrite hand-authored content". Converting a hand-authored file therefore needs `--mode compiled --force`. + +A converted pointer is still refused (exit 1, nothing written) if it holds content the compiled file would not keep: + +- `refused-bloated`: run `charter adf tidy --source ` first. +- `refused-would-drop`: move the lines under `## Environment` or into a `.ai/` module. + +`--force` discards that content. It never discards retained sections. + +If a script runs `adf compile --target all --write --force` to convert pointers, replace `--force` with `--mode compiled`. Once the files are recorded `compiled`, plain `--write` re-renders them. + +## Re-run the hook installs + +Installed hooks are not upgraded for you. Replace every pre-2.0 hook by re-running the installs from step 4 (`charter hook install --pre-commit`, and `--commit-msg` if you use it) to get: + +- **pre-commit**: tidies only the vendor files the commit stages, and re-stages only the files tidy wrote. A staged vendor file that also has unstaged changes is skipped, with a message. The old hook re-staged all of `.ai/*.adf` and could sweep unrelated edits into a commit. The evidence gate runs `charter doctor --adf-only --ci --staged …` and blocks on vendor bloat only in files the commit includes. A staged file that tidy could not clean, or did not tidy because tidy failed or `CHARTER_SKIP_TIDY=1` is set, still blocks. Then it runs your `verify:adf` script, if `package.json` has one, or `charter adf evidence` (see the note below). +- **commit-msg**: now rejects a commit with no `Governed-By` or `Resolves-Request` trailer when `git.requireTrailers` is on and strictness is `FAIL`/`STRICT`, or `ci.failOnWarn` is `true`. Under advisory strictness it only warns. The old hook only normalized trailers you had already typed, so a missing trailer first showed up as a red CI run. + +The new pre-commit hook calls `charter adf tidy --print-paths` and `charter doctor --staged`. With a 1.x CLI in `node_modules`, `--print-paths` is unknown, so the hook reports files as clean and tidies nothing. Upgrade the CLI as well as the hook. + +> **`verify:adf` script.** `charter setup` and `bootstrap` add a `verify:adf` script. The hook runs it after the scoped doctor, with your package manager: `pnpm` (with `pnpm-lock.yaml` and pnpm installed), `yarn` (with `yarn.lock` and yarn installed), otherwise `npm`. pnpm is no longer required. Inside the hook, the doctor in `verify:adf` reports vendor bloat as INFO, `adf vendor bloat (checked by pre-commit hook)`, because the scoped check already ran. Its other checks still block. Running `npm run verify:adf` (or your package manager's equivalent) by hand or in CI still checks every vendor file. The hook runs `npx charter doctor …` even if your custom `verify:adf` leaves doctor out. +> +> **Known limitation:** Yarn Plug'n'Play repos are not supported by the hook yet ([#366](https://github.com/Stackbilt-dev/charter/issues/366)). + +## What doctor's new lines mean + +None of the INFO lines fails `charter doctor --ci`. A repo that has no `VENDORS` record yet gets only INFO lines from the new checks, so the upgrade itself does not turn CI red. + +| Line | Status | What it means | Fix | +|---|---|---|---| +| `pre-commit hook` | INFO | The installed pre-commit hook is outdated: it tidies every vendor file and re-stages all of `.ai/*.adf`, or its gate blocks on bloat outside the commit. | `charter hook install --pre-commit` | +| `gate enforcement` naming the commit-msg hook | WARN | The commit-msg hook only normalizes trailers, so nothing blocks a missing trailer locally. | `charter hook install --commit-msg`, or run `charter validate --ci` in CI | +| `adf vendor mode`: "N vendor file(s) have no recorded mode" | INFO | No `VENDORS` entry. Modes are inferred from markers. | `charter adf vendor init` | +| `adf vendor mode`: "git tracks agents.md under its pre-#297 lowercase name" (`legacy-tracked`) | INFO | Bootstrap before #297 wrote `agents.md`. Harnesses on case-sensitive systems read `AGENTS.md`. | `charter adf vendor rename-legacy` | +| `adf vendor mode`: `legacy-both-distinct` | INFO | Git tracks both spellings, and they are two different files. | Move what you need into `AGENTS.md`, then `git rm agents.md` | +| `adf vendor mode`: `legacy-both-one-file` | INFO | Git has both index entries for one file (a case-insensitive checkout). | `git rm --cached agents.md` | +| `adf vendor mode`: `legacy-spelling` | INFO | `AGENTS.md` is recorded under the legacy key `agents.md`. | `charter adf vendor rename-legacy` | +| `adf vendor mode`: `recorded-drift` | **WARN** | A file recorded `pointer` or `compiled` looks like another mode on disk. | The line names the content-preserving fix first: `charter adf vendor set ` to keep the file as it is, or the command that restores it (`charter adf vendor restore ` for a pointer) | +| `adf vendor mode`: `recorded-absent` | **WARN** | A file recorded `pointer` or `compiled` is missing. | `charter adf vendor restore `, `charter adf compile --target --write`, or `charter adf vendor set unmanaged` | +| `adf vendor mode`: malformed entry, unknown file, mode the file cannot have, or `VENDORS` is not a list | **WARN** | Hand edit to `.ai/manifest.adf`. | Fix the named line. `agents.md` and `copilot-instructions.md` can never be `compiled` | +| `adf vendor bloat (not blocking)` | INFO | Shown only under `doctor --staged` (the pre-commit gate). Vendor bloat in files the commit does not include. | `charter adf tidy --source ` (named per file) | +| `telemetry tracking` | INFO | `.charter/telemetry/events.ndjson` is tracked by git. Repos bootstrapped by 1.x committed it. | Make sure `telemetry/` is ignored in `.charter/.gitignore`, then `git rm -r --cached ".charter/telemetry"` | + +The WARN rows in `adf vendor mode` also fail the pre-commit gate, which runs `doctor --adf-only --ci`. They appear only where a record you wrote is contradicted or broken. + +Under `doctor --staged` (what the hook passes), the `adf vendor bloat` check counts only staged vendor files. Plain `charter doctor --ci`, as in CI, still fails on vendor bloat in every file. + +## Lowercase `agents.md` + +Repos bootstrapped before #297 track `agents.md`, which nothing renames automatically. `rename-legacy` renames it to `AGENTS.md` safely on case-insensitive filesystems, and moves its `VENDORS` key first: + +```bash +npx charter adf vendor rename-legacy --dry-run # see the plan +npx charter adf vendor rename-legacy # stages R agents.md -> AGENTS.md +git add .ai/manifest.adf AGENTS.md # or run exactly the git add line the output prints +git commit -m "chore: rename agents.md to AGENTS.md" +``` + +When git tracks the file, the rename is staged and nothing else is. If the `VENDORS` key moved, `.ai/manifest.adf` is left for you to `git add`, and if the title line was rewritten, so is `AGENTS.md`. The output names the files to add. (After `adf vendor init` on a case-insensitive filesystem, the key is often already `AGENTS.md`, so only `AGENTS.md` is named.) Its only content edit is a first line that is exactly `# agents.md`. Other mentions of `agents.md` are listed, not edited. It refuses, exit 1, and changes nothing when: + +- both spellings are tracked, or are two different files; +- `agents.md` has uncommitted changes; +- the two keys are recorded with different modes; +- the git index cannot be read; +- it is run below the repository root. + +Until you rename, `adf compile --target agents --write --mode compiled` refuses with `refused-legacy-spelling` and names the rename. + +## Your `## Environment` notes in compiled files + +In 1.x, re-compiling a compiled file deleted a hand-added `## Environment` section, or another section that `adf tidy` keeps (`## Session …` other than `## Session Start`, or headings containing Protocol, Receipt, Provenance or Registration). In 2.0, `compile --write` keeps them in a block at the end of the file: + +```markdown + + + +## Environment +- Use WSL for builds + +``` + +Edit freely inside the block. Every later `--write` copies it as it stands, and an edit inside it never makes `--check` stale. A compiled file with nothing to carry gets no block and compiles byte-for-byte as before. + +### One-time `--check` reports after upgrading + +| `--check` says | Why | Fix | +|---|---|---| +| `[stale]` with `staleReason: "retained-unfenced"` | A compiled file has retained sections outside the block (written by 1.x). | `charter adf compile --target --write` once. It moves them into the block. | +| `[stale]` with `staleReason: "retained-ambiguous"` | A section sits inside generated text that `.ai/` no longer renders, and no `.ai/` module holds its heading. It may be stale output or your own text. `--write` refuses it as `refused-ambiguous`. | Keep it: wrap it between `` and ``, then `--write`. Or discard it: `charter adf compile --target --write --force`. | + +A pointer is no longer reported `[stale]` forever. `--check` now checks each file by its mode: + +- compiled files: `fresh`/`stale`; +- pointers: `pointer-ok`, or `pointer-bloated` (exit 1; fix with `charter adf tidy --source `); +- unmanaged files: `skipped-unmanaged`; +- a file that contradicts its record: `mode-drift`; +- a recorded file that is gone: `missing`. + +`--check` compares bytes. On Windows with `core.autocrlf=true`, a compiled file checked out with CRLF line endings reads stale. Add `*.md text eol=lf` and `.cursorrules text eol=lf` to `.gitattributes` if this affects you. + +## `bootstrap --force` keeps your vendor files + +`charter bootstrap --force` still re-scaffolds `.ai/*.adf` (backups go to `.ai/.backup/`) and still rewrites Charter thin pointers from the current template. It no longer overwrites vendor files that are: + +- hand-authored (no Charter marker); +- compiled output; +- recorded `compiled` or `unmanaged`. + +Each skipped file gets a warning naming the next step. That is `charter adf migrate --source `, or for compiled output `charter adf vendor set pointer && charter adf vendor restore `. In 1.x, `--force` overwrote a hand-authored `CLAUDE.md` before phase 4 could migrate it, and its rules were lost. With `--yes`, phase 4 now migrates such a file into `.ai/` and writes `.pre-adf-migrate.bak`, if it has no `VENDORS` entry. + +To replace a hand-authored file without migrating it, delete it and re-run bootstrap. + +## `charter score` may go up + +A thin pointer used to score as "thin" (−5 and "Expand `CLAUDE.md` beyond a thin pointer") however much `.ai/` held. It now counts as substantive when the file `adf compile` would render for it is substantive. That file is compiled in memory and nothing is written. Hand-authored and compiled files are scored on their text as before. Scores can only rise. A committed `.charter/badge.json` may need `charter score --badge --write`. + +## For JSON consumers and scripts + +**Changed values:** + +- `adf tidy` reports an unmanaged file (recorded `unmanaged`, or hand-authored with no Charter marker) as `unmanaged`. In 1.x it was `not-found`, which now means only that the file does not exist. +- If you built against unreleased `main` before 2.0: `refused-retained` (`adf compile --write`) and `retained-divergent` (`adf vendor restore`) are retired and never shipped in a release. Retained sections are now carried instead. + +**New values and fields:** + +| Command | Added | +|---|---| +| `adf compile --write` | `skipped`, `targets[]` with `status`: `written`, `skipped-mode`, `refused-mode-change`, `refused-hand-authored`, `refused-bloated`, `refused-would-drop`, `refused-ambiguous`, `refused-record`, `refused-legacy-spelling`; plus `state`, `source`, `recordedMode`, `previousMode`, `remedy`, `writeError`, `retainedSections`, `retainedSource`, `retainedSkipped`, `retainedAmbiguous` | +| `adf compile --check` | `skipped`, `targets[]` with `pointer-ok`, `pointer-bloated`, `mode-drift`, `skipped-unmanaged`, `missing`, and `staleReason` (`content`, `retained-unfenced`, `retained-ambiguous`, or null). `stale` now also lists bloated and drifted files | +| `adf vendor restore` | `status` (`restored`, `planned`, `unchanged`, `refused`), `refusal` (including `would-drop`, `write-failed`), `keptSections` | +| `adf vendor rename-legacy` | one result: `status`, `method`, `from`, `to`, `staged`, `record`, `titleUpdated`, `references`, `refusal`, `remedy`, `error` | +| `adf tidy` | `paths` (`vendor`, `modules`); file rows add `vendorFile`, `mode`, `source`, `skipReason`; status `compiled` | +| `adf migrate` | `sources[].registrations[]` | +| `doctor` | `rows` on the `adf vendor mode` check only; new check names `adf vendor mode`, `pre-commit hook`, `telemetry tracking` and `adf vendor bloat (not blocking)` | +| `score` | `signals.agentConfig` adds `mode`, `source`, `substantiveVia` per file | + +Exit codes that changed: + +- `adf compile --target all --check` on thin pointers exits 0 (`pointer-ok`). 1.9.3 reported every pointer `[stale]` and exited 1, so a CI step that expected that failure now passes. A pointer with content `adf tidy` would extract still exits 1 (`pointer-bloated`). +- `adf compile --target all --write` with pointer or unmanaged targets exits 0 and skips them (1.9.3 refused them, exit 1). +- `adf compile --write --force` no longer converts a pointer (1.9.3 did). A single `--target` is refused with exit 1, and `--target all` skips it with exit 0. +- `adf tidy --source` with no value exits 2 (it used to tidy every file). + +New flags that are usage errors (exit 2) when misused: `adf tidy --print-paths` with `--format json`, and `doctor --tidy-skipped` without `--staged`. + +**`@stackbilt/adf` library:** `classifyElement` now always sets `routingTrace`, with `candidateScores: {}` when there is no trigger map. Check `candidateScores` instead of "is `routingTrace` defined" to tell whether content routing ran. `RoutingTrace` gains an optional `headingKeyword`, and `parseManifest` gains a `vendors` field. + +**`@stackbilt/git` library:** the trailer parser no longer accepts a whitespace-only value (`Governed-By: `), and such a line ends the terminal trailer block, so trailers above it are not counted (`#340`). diff --git a/packages/cli/README.md b/packages/cli/README.md index 5d9bd9d..0463e2c 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -64,6 +64,8 @@ npm install --save-dev @stackbilt/cli@latest npx --no-install charter --version ``` +Coming from 1.x? `adf compile --write` now converts pointer files only with `--mode compiled`, and installed git hooks need a re-install. Follow [Upgrading to 2.0](https://github.com/Stackbilt-dev/charter/blob/main/docs/upgrading-to-2.0.md). + For CI pipelines, install as a dev dependency: ```bash @@ -300,7 +302,7 @@ charter hook install --pre-commit ``` - `--commit-msg`: Install a commit-msg hook that normalizes `Governed-By` and `Resolves-Request` trailers using `git interpret-trailers`, and rejects a message carrying neither whenever `charter validate --ci` would fail it. The hook reads `.charter/config.json` with the same defaults as `charter validate`: `git.requireTrailers` is on unless set to a falsy value, `validation.citationStrictness` defaults to `FAIL`, and an unparseable file falls back to those defaults (the hook says so on stderr). It blocks under `FAIL` or `STRICT`, or with `ci.failOnWarn: true`, and only warns under advisory strictness. Two deliberate exceptions skip the gate with a stderr warning: no `.charter/config.json` at all, since the hook cannot tell the repo adopted Charter, and no `node` on `PATH`, since the config cannot be read. The hook strips `#` comment lines before checking, as `git commit` does with its default editor cleanup; with `-m` or `--cleanup=verbatim` git keeps them, so a trailer followed by a `#...` line can pass locally and still fail CI. -- `--pre-commit`: Install a pre-commit hook that tidies the staged vendor files, enforces ADF routing with `charter doctor --adf-only --ci --staged`, and validates ceilings with `charter adf evidence --auto-measure --ci` (or `pnpm run verify:adf` when that script exists). Only gates when `.ai/manifest.adf` exists — no-op otherwise. Tidy runs on each staged vendor file (`adf tidy --source`) and re-stages only the files it wrote. It skips, with a message, a file that also has unstaged changes, and skips tidy for that commit when it would write into a `.ai/` module with unstaged or untracked work. Skip it with `CHARTER_SKIP_TIDY=1`. Vendor bloat blocks the commit only in staged vendor files that tidy did not skip. Bloat in unstaged or skipped files is reported as INFO, and CI's plain `charter doctor --ci` still enforces it. Hooks installed before #320 re-stage all of `.ai/*.adf`, and hooks installed before #357 block on vendor bloat outside the commit. Re-run `charter hook install --pre-commit` to upgrade (`charter doctor` flags an old hook as INFO). +- `--pre-commit`: Install a pre-commit hook that tidies the staged vendor files, enforces ADF routing with `charter doctor --adf-only --ci --staged`, and then runs the `verify:adf` script when `package.json` has one (with `pnpm`, `yarn` or `npm`, chosen from the lockfile and what is installed), or `charter adf evidence --auto-measure --ci` otherwise. Inside the hook, that script's doctor reports vendor bloat as INFO because the scoped check already ran; run by hand or in CI it checks every vendor file. Only gates when `.ai/manifest.adf` exists — no-op otherwise. Tidy runs on each staged vendor file (`adf tidy --source`) and re-stages only the files it wrote. It skips, with a message, a file that also has unstaged changes, and skips tidy for that commit when it would write into a `.ai/` module with unstaged or untracked work. Skip it with `CHARTER_SKIP_TIDY=1`. Vendor bloat blocks the commit only in staged vendor files that tidy did not skip. Bloat in unstaged or skipped files is reported as INFO, and CI's plain `charter doctor --ci` still enforces it. Hooks installed before #320 re-stage all of `.ai/*.adf`, and hooks installed before #357 (and, in a repo with a `verify:adf` script, a hook from the first #357 fix) block on vendor bloat outside the commit. Re-run `charter hook install --pre-commit` to upgrade (`charter doctor` flags an old hook as INFO). - `--force` (or global `--yes`) allows overwrite when a non-Charter hook already exists. - Both flags can be passed together to install both hooks in one command. @@ -338,7 +340,7 @@ charter adf vendor rename-legacy [--dry-run] [--ai-dir ] - `sync --write`: Update `.adf.lock` with current source hashes. - `evidence`: Validate all metric ceilings in the merged document and produce a structured pass/fail evidence report. `--auto-measure` counts lines in files referenced by the manifest METRICS section. `--context` or `--context-file` inject external metric overrides that take precedence over auto-measured and document values. In `--ci` mode, exits 1 on constraint failures (warnings don't fail). Also reports stale-baseline warnings (baseline vs current delta + recommended ceiling) when baseline values drift significantly. The governance workflow template runs this automatically on PRs when `.ai/manifest.adf` is present. - `metrics recalibrate`: Re-measure current LOC from manifest metric sources, propose and apply new ceilings using configurable headroom, and append rationale records to `BUDGET_RATIONALES`. Requires explicit rationale (`--reason`) unless `--auto-rationale` is used. -- `migrate`: Scan existing agent config files (CLAUDE.md, .cursorrules, agents.md, GEMINI.md, copilot-instructions.md), classify content using the ADX-002 decision tree, and migrate into ADF modules. `--dry-run` previews the migration plan without writing files. `--source ` targets a single file. `--no-backup` skips `.pre-adf-migrate.bak` creation. `--merge-strategy` controls deduplication: `dedupe` (default, skip items already in ADF), `append` (always add), or `replace`. Environment-specific rules (WSL, PATH, credential helpers) are retained in the thin pointer. Each module that receives content is registered in `manifest.adf`: `core.adf` and `state.adf` under `DEFAULT_LOAD`, the rest under `ON_DEMAND` with triggers inferred from the migrated headings. Registration is append-only, so existing entries are never rewritten. The `CLAUDE.md` module index is refreshed to match, and the plan shows a `REGISTER` line per module. +- `migrate`: Scan existing agent config files (CLAUDE.md, .cursorrules, AGENTS.md or legacy agents.md, GEMINI.md, copilot-instructions.md), classify content using the ADX-002 decision tree, and migrate into ADF modules. `--dry-run` previews the migration plan without writing files. `--source ` targets a single file. `--no-backup` skips `.pre-adf-migrate.bak` creation. `--merge-strategy` controls deduplication: `dedupe` (default, skip items already in ADF), `append` (always add), or `replace`. Environment-specific rules (WSL, PATH, credential helpers) are retained in the thin pointer. Each module that receives content is registered in `manifest.adf`: `core.adf` and `state.adf` under `DEFAULT_LOAD`, the rest under `ON_DEMAND` with triggers inferred from the migrated headings. Registration is append-only, so existing entries are never rewritten. The `CLAUDE.md` module index is refreshed to match, and the plan shows a `REGISTER` line per module. - `vendor`: Show or record whether each vendor file (`CLAUDE.md`, `AGENTS.md`, `.cursorrules`, `GEMINI.md`, `agents.md`, `copilot-instructions.md`) is a thin `pointer`, `compiled` output, or `unmanaged`. The record is a `VENDORS` list in `manifest.adf` (` - CLAUDE.md: pointer`). `status` is read-only and shows each file's recorded mode, or the mode inferred from its markers when none is recorded. `set` records one file's mode; only the four compile targets may be `compiled`. `init` records the inferred mode of every vendor file on disk that has no entry yet, and never changes an existing one. `bootstrap`, `adf init --emit-pointers`, `adf migrate` and `adf compile --write` also record the mode of each file they write, without changing an existing entry (an explicit `adf migrate --source ` changes `unmanaged` to `pointer`). `restore ` rewrites one pointer from its template, keeping retained sections such as `## Environment`, and refuses whenever content would be lost (hand-authored content, bloat, or a compiled file that differs from what `.ai/` compiles to); a compiled file's retained sections are carried into the pointer. `rename-legacy` renames a pre-#297 lowercase `agents.md` to `AGENTS.md`: it moves the `VENDORS` key first, then renames in two steps; when git tracks the file both steps are `git mv`, so the rename is staged and `manifest.adf` (and `AGENTS.md`, when its title line was rewritten) is left for you to `git add`. Its only content edit is an exact `# agents.md` title line. It refuses (exit 1) when both spellings are tracked or are two files, when `agents.md` has uncommitted changes, when the two keys disagree, when the git index cannot be read, or when run below the repository root, and undoes itself if a step fails. `doctor` reports a tracked lowercase `agents.md` as INFO (`#302`). `adf compile --write` keeps your `## Environment` and other retained sections in a `charter:retained` block at the end of each compiled file, converts a pointer with notes without refusing, and refuses only over content it would not carry (`refused-bloated`, `refused-would-drop`) (`#303`). `adf compile`, `adf tidy`, `doctor`, `score`, `bootstrap` and `adf migrate` act on the record: for example, `bootstrap --force` never replaces a compiled or hand-authored vendor file, and bare `adf migrate` skips a file recorded `unmanaged` (`#318`). ### `charter blast`