Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ pnpm exec tsc --noEmit
# Charter pre-commit vendor tidy: scoped to staged files
# Skip with CHARTER_SKIP_TIDY=1
# This block never fails the commit; the ADF evidence gate after it may.
# TIDY_SKIPPED lists the vendor files it skipped, so the gate can say why it blocks.
# TIDY_SKIPPED lists the vendor files it skipped; the generated hook passes it to
# doctor --staged --tidy-skipped, so their bloat does not block the commit (#357).
TIDY_SKIPPED=""
if [ "${CHARTER_SKIP_TIDY:-0}" != "1" ] && [ -f ".ai/manifest.adf" ]; then
VENDOR_FILES="CLAUDE.md .cursorrules AGENTS.md agents.md GEMINI.md copilot-instructions.md"
Expand All @@ -24,7 +25,7 @@ if [ "${CHARTER_SKIP_TIDY:-0}" != "1" ] && [ -f ".ai/manifest.adf" ]; then
fi
if ! git diff --quiet -- "$vf"; then
echo "[pre-commit] $vf has unstaged changes, so auto-tidy is skipped for it in this commit."
echo "[pre-commit] If it carries bloat, the ADF evidence gate may still block this commit. Ways out: stage or stash the unstaged changes; or run: charter adf tidy --source $vf, then git add the result; or bypass the hooks with git commit --no-verify."
echo "[pre-commit] Its bloat, if any, is left as-is; a plain charter doctor --ci (as CI runs it) still reports it. To tidy it: stage or stash the unstaged changes and commit again; or run: charter adf tidy --source $vf, then git add the result."
TIDY_SKIPPED="$TIDY_SKIPPED $vf"
continue
fi
Expand All @@ -51,7 +52,7 @@ if [ "${CHARTER_SKIP_TIDY:-0}" != "1" ] && [ -f ".ai/manifest.adf" ]; then
set +f
if [ -n "$BLOCKED" ]; then
echo "[pre-commit] Auto-tidy skipped for this commit: tidy would write into files with unstaged or untracked work:$BLOCKED"
echo "[pre-commit] The ADF evidence gate may still block this commit while$NAMES carries bloat. Ways out: stage or stash that work; or run: charter adf tidy$SOURCES, then git add the result; or bypass the hooks with git commit --no-verify."
echo "[pre-commit] Bloat in$NAMES, if any, is left as-is; a plain charter doctor --ci (as CI runs it) still reports it. To tidy: stage or stash that work and commit again; or run: charter adf tidy$SOURCES, then git add the result."
TIDY_SKIPPED="$TIDY_SKIPPED$NAMES"
else
echo "[pre-commit] Moving content beyond the thin pointer into .ai/ ..."
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ The format is based on Keep a Changelog and follows Semantic Versioning.
- **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 `<!-- charter:retained:start -->` … `<!-- charter:retained:end -->` 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 <t> --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 <file>`, 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 <file> 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, and it is unchanged. In the generated hook (no `verify:adf` script), vendor bloat that a skipped tidy leaves in the working tree still fails `charter doctor --adf-only --ci`. The skip message now says so and names the ways out: stage or stash the unstaged work; run `charter adf tidy --source <file>` and `git add` the result; or `git commit --no-verify`. When the gate then fails, one line says which skipped file still carries how many items. 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.
- **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 <file>`, or `adf vendor set <file> pointer && adf vendor restore <file>` 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.
Expand All @@ -34,6 +34,7 @@ The format is based on Keep a Changelog and follows Semantic Versioning.

### 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 "<files>"`. `--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.
- **`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 <file>`) 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 <file> <mode>` 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 -- <file>` or `adf vendor set <file> 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 <file> pointer`, then `adf tidy --source <file>`) 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`.
Expand Down
Loading
Loading