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
8 changes: 8 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ charter adf migrate # Migrate existing configs
charter adf compile --target all --write # Re-render compiled-mode CLAUDE.md, AGENTS.md, .cursorrules, GEMINI.md
charter adf compile --target all --write --mode compiled # Switch pointer files to compiled output
charter adf vendor status # Show each vendor file's mode (pointer, compiled, unmanaged)
charter adf vendor restore CLAUDE.md # Rewrite one pointer from its template (refuses if content would be lost)
charter adf patch <file> --ops <json> # Apply structured patch operations to an ADF file
charter adf sync --check # Verify files match lock
charter adf fmt .ai/core.adf --write # Reformat to canonical form
Expand Down
37 changes: 33 additions & 4 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,11 @@ npx charter bootstrap --skip-install --skip-doctor # minimal
- `--security-sensitive` — generate `SECURITY.md`, seed hard-fail drift denies in `.charter/patterns/security-deny.json`, and warn in `doctor` when no `security*` or `l4*` test file exists
- `--skip-install` — skip dependency installation phase
- `--skip-doctor` — skip health check phase
- `--force` — re-scaffold `.ai/*.adf` (the old files are backed up to `.ai/.backup/`) and rewrite Charter thin pointers from the current template. It never overwrites a hand-authored vendor file (one with no Charter marker), compiled output (the `adf compile` banner, or a file recorded `compiled`), or a file recorded `unmanaged`: each is skipped with a warning naming the next step. Vendor files are not backed up, so a hand-authored file is left for phase 4 to migrate into `.ai/` (with `--yes`). Valid `VENDORS` entries are carried over to the re-scaffolded `manifest.adf`.
- `-y, --yes` — accept all prompts

**Vendor modes.** Phase 3 decides each pointer file from its `VENDORS` entry (see [`charter adf vendor`](#charter-adf-vendor)) and its markers, first match wins: recorded `compiled` or a compile banner on disk → skipped (absent and recorded `compiled` → skipped, run `adf compile --write`); recorded `unmanaged` → left alone; absent → pointer written; no Charter marker → skipped as custom content, with or without `--force`; a Charter pointer → kept, or rewritten under `--force`. Phase 4 migrates only hand-authored files with no `VENDORS` entry; a file recorded `unmanaged` is not migrated, and the warning names `charter adf migrate --source <file>` for doing it anyway.

### charter setup

Bootstraps `.charter/` config and optionally writes CI workflow scaffolding. For full onboarding, prefer `charter bootstrap` which orchestrates setup + ADF init + install + doctor.
Expand Down Expand Up @@ -156,6 +159,26 @@ are present; doctor names it and asks for a re-install. The local hook checks
only the commit being written, so CI should still run `charter validate --ci`;
the workflow generated by `charter setup --ci github` includes that step.

#### Vendor file modes

The `adf vendor mode` check compares each vendor file's `VENDORS` entry (see [`charter adf vendor`](#charter-adf-vendor)) with the markers in the file. It runs under `--adf-only`, so the pre-commit hook runs it too. It is one check whose status is its worst row, and `--format json` adds a `rows` array to that check only: `kind`, `status`, `file`, `recordedMode`, `inferredMode`, `source`, `raw`, `remedy`.

| Row | When | Status |
|---|---|---|
| `recorded-ok` | entry matches the file (a recorded `unmanaged` file always matches) | PASS |
| `recorded-unmanaged-absent` | recorded `unmanaged`, no file | PASS |
| `inferred` | file with no entry; one line for all of them, naming `charter adf vendor init` | INFO |
| `recorded-drift` | recorded `pointer` or `compiled`, but the file looks like another mode. The content-preserving fix (`adf vendor set <file> <mode>`) is named first; for a recorded `compiled` file the regenerate command says what it would discard | WARN |
| `recorded-absent` | recorded `pointer` or `compiled`, no file. Recreate with `adf vendor restore <file>` or `adf compile --target <t> --write` | WARN |
| `entry-problem` | a malformed, unknown-file or not-allowed entry (WARN); a duplicate entry, which names the ignored line to delete because a later entry wins (INFO) | WARN / INFO |
| `alias-recorded` | both `AGENTS.md` and `agents.md` recorded for one file | INFO |
| `legacy-spelling` | the file `AGENTS.md` recorded under the legacy name `agents.md` | INFO |
| `section-not-list` | `VENDORS` does not read as a list | WARN |

A repo without a `VENDORS` section gets only the `inferred` INFO line, so upgrading never fails `doctor --ci`. A WARN appears only where an explicit record is contradicted or ignored. Doctor never writes the record or a vendor file.

The `adf agent config`, `adf vendor bloat` and `adf cold start` checks read the record too. Each physical file is checked once (`AGENTS.md` and `agents.md` on a case-insensitive filesystem are one file). A hand-authored file recorded `unmanaged` no longer warns that it "contains stack rules", and the PASS line counts thin pointers and compiled files separately. Bloat and cold start look only at pointers: a file in pointer mode that looks like a pointer on disk.

#### Tracked telemetry

Charter appends to `<configPath>/telemetry/events.ndjson` on every run. In a
Expand Down Expand Up @@ -236,6 +259,8 @@ npx charter score --badge --write # also write .charter/badge.json
- `--badge` — print shields.io endpoint JSON instead of the full report
- `--badge --write` — additionally write the badge JSON to `.charter/badge.json`

**Agent config and vendor modes.** `CLAUDE.md`, `.cursorrules` and `AGENTS.md` (or `agents.md`) score higher when they are substantive. A hand-authored or compiled file is judged on its own text. A thin pointer is as substantive as the compiled file it routes to: score compiles that target from `.ai/` in memory (nothing is written) and counts the pointer as substantive when either its text or that output is. Scores can therefore only rise compared with earlier versions. `--format json` adds `mode`, `source` and `substantiveVia` (`file`, `ai-dir` or `none`) to each of these files under `signals.agentConfig`. Recommendations never tell you to hand-edit a pointer or a compiled `CLAUDE.md`; when one has little to route to, the recommendation is to add content to `<ai-dir>/core.adf` or run `charter adf populate`. The `VENDORS` section is ignored when checking manifest path references.

**Grounding and path references.** The grounding dimension counts how many file paths referenced
in your docs actually resolve. A reference that does not exist at its literal path is retried by
filename: if exactly one file in the repository has that basename *and* the referenced path's
Expand Down Expand Up @@ -366,6 +391,8 @@ npx charter adf migrate --no-backup # skip .pre-adf-migrate.bak fil
- `--no-backup` — skip creating backup files
- `--ai-dir <dir>` — path to ADF directory (default: `.ai`)

Without `--source`, a file recorded `unmanaged` in `VENDORS` is skipped with a note. Naming it with `--source` migrates it and changes its entry to `pointer` before the pointer is written; if the migration then fails, the entry goes back to `unmanaged`.

Every module that receives content is registered in `manifest.adf`, after the module merge and before the pointer is written, so the `CLAUDE.md` module index lists it. `core.adf` and `state.adf` go to `DEFAULT_LOAD`. Other modules go to `ON_DEMAND`, with up to 6 triggers inferred from the headings routed there. The heading word the router matched (e.g. `deployment`) counts double, and words that already trigger another module are left out. When nothing usable is left, the module's lowercase file stem is the trigger. Registration is append-only: an existing entry is never rewritten or moved, and its triggers are never merged. New bullets are inserted as text after the section's last item. Every other byte of the manifest is left as it was, including comments, section order and line endings. Already-registered modules are reported with their existing section and triggers. When `core.adf` is listed under `ON_DEMAND`, migrate warns instead of moving it. When `manifest.adf` is missing, registration is skipped with a warning to run `charter adf init`. The plan shows one `REGISTER` line per module, for example `REGISTER .ai/manifest.adf (infra.adf → ON_DEMAND: deployment, docker [planned])`, and `--format json` adds `sources[].registrations[]` with `module`, `loadPolicy`, `triggers`, `triggerSources`, `reason`, `status` (`planned`, `added`, `already-registered` or `skipped`) and, when skipped, `skipReason`. A `--dry-run` across several sources reports each module once. It does not simulate an earlier source's new triggers when routing a later source, though a real run does.

### charter adf fmt
Expand Down Expand Up @@ -509,7 +536,7 @@ When every target is skipped, compile says that nothing was written and how to s
| Status | When | Exit |
|---|---|---|
| `fresh` / `stale` | compiled: bytes equal / differ (remedy: `compile --target <t> --write`) | 0 / 1 |
| `missing` | no file on disk (a recorded pointer suggests `git checkout -- <file>` or `adf vendor set <file> unmanaged`) | 1 |
| `missing` | no file on disk (a recorded pointer suggests `adf vendor restore <file>` or `adf vendor set <file> unmanaged`) | 1 |
| `pointer-ok` | pointer with nothing `adf tidy` would extract | 0 |
| `pointer-bloated` | pointer carrying content that belongs in `.ai/` (remedy: `adf tidy --source <file>`) | 1 |
| `mode-drift` | recorded `compiled` or `pointer`, but the file looks like another mode. Both fixes are printed, the content-preserving one first; regenerating says how many items it would discard | 1 |
Expand Down Expand Up @@ -606,6 +633,7 @@ npx charter adf vendor status --all --format json # include absent names
npx charter adf vendor set CLAUDE.md compiled # record one file's mode
npx charter adf vendor set CLAUDE.md pointer --dry-run
npx charter adf vendor init # record inferred modes for unrecorded files
npx charter adf vendor restore CLAUDE.md # rewrite one pointer from its template
```

```
Expand All @@ -617,15 +645,16 @@ npx charter adf vendor init # record inferred modes for
- `status`: read-only. A file with a valid entry shows its recorded mode, plus `(recorded; file looks compiled)` when the markers on disk disagree. A file without one shows the mode inferred from its markers, the way `adf tidy` classifies it: the compile banner means `compiled`, a pointer marker means `pointer`, and anything else is `unmanaged`. Malformed, duplicate (the last entry wins), unknown-file and not-allowed entries are reported and ignored. A missing manifest exits 0 with a warning.
- `set <file> <mode>`: records or replaces one entry, editing only that line. Only `CLAUDE.md`, `AGENTS.md`, `.cursorrules` and `GEMINI.md` may be `compiled`. `agents.md` and `copilot-instructions.md` have no compile target, so `compiled` is refused with exit 1. An unknown file or mode, or a missing manifest, exits 2. The file does not have to exist yet.
- `init`: records the inferred mode of every vendor file on disk that has no entry. It never changes an existing entry, and a second run writes nothing.
- `restore <file>`: rewrites one file as its thin pointer template (`CLAUDE.md` gets the hybrid template with the module index filled in, as bootstrap writes it). It runs only when the file's mode is `pointer` (recorded or inferred), or the file is absent with no entry, and it refuses (exit 1, file untouched) whenever content would be lost: `mode-not-pointer` (record the intent first with `vendor set <file> pointer`), `hand-authored` (run `adf migrate --source <file>`), `pointer-bloated` (run `adf tidy --source <file>`), `retained-divergent` (a compiled file carrying your own `## Environment` or protocol section), and `compiled-differs` (a compiled file that differs from what `.ai/` compiles to now, checked in memory; keep it with `vendor set <file> compiled`). For a pointer, every non-blank line must survive: a line that is not from a Charter pointer template (current or earlier) or the generated module index, and that the new pointer would not carry, is refused as `hand-authored` with the first few lines named. Retained sections of a pointer are carried over verbatim and listed. It is the fix for a recorded pointer that now holds compiled output. A file with no entry gets `pointer` recorded before the file is written; if that cannot be recorded, nothing is written (`record-write`), and if the file write fails, the manifest is put back byte for byte (`write-failed`, with `writeError`). A result identical to the file on disk is `unchanged` and writes nothing. `--dry-run` reports `planned`. An unknown file name exits 2. JSON output: `file`, `status` (`restored`, `planned`, `unchanged`, `refused`), `refusal`, `keptSections`, `remedy`, `writeError`; text lines go to stderr under `--format json`.

One physical file has one entry. When two vendor names are the same file (`AGENTS.md` and `agents.md` on a case-insensitive filesystem, or a hard link), every writer records it under the spelling already recorded, or `AGENTS.md` when neither is. `status` shows it once. `set` names the spelling it used.
- `--dry-run`: with `set` or `init`, report the change without writing
- `--dry-run`: with `set`, `init` or `restore`, report the change without writing
- `--all`: with `status`, also list vendor names that are absent and unrecorded
- `--ai-dir <dir>`: custom `.ai/` directory (default: `.ai`)

`bootstrap` and `adf init --emit-pointers` record `pointer` for each pointer file they write. `adf migrate` records `pointer` for each source it replaces with a pointer, and `adf compile --write` records `compiled` for each target it writes (with `--mode compiled`, replacing the entry). The other writers only add missing entries. If a file is already recorded with another mode, the entry is kept and a warning names `adf vendor set` as the way to change it. Nothing is recorded under `--dry-run`, and a failed record write is only a warning, except under `adf compile --write --mode compiled`, where the target is refused (`refused-record`) and its file is not written. Every manifest edit is text-level and keeps line endings and every other byte, and the block it writes is already in `adf fmt` canonical form. It is re-parsed before writing, and if the result would change anything else, nothing is written.

`adf compile` (`--check` and `--write`) and `adf tidy` act on the record; see their sections. `doctor` and the pre-commit hook still decide from the markers in each file, and `score` skips the section when it checks manifest path references. Later changes under [#318](https://github.com/Stackbilt-dev/charter/issues/318) make them read it.
`adf compile` (`--check` and `--write`), `adf tidy`, `doctor`, `score`, `bootstrap` and `adf migrate` act on the record; see their sections. The pre-commit hook still decides from the markers in each file; a later change under [#318](https://github.com/Stackbilt-dev/charter/issues/318) makes it read the record. `score` skips the section when it checks manifest path references.

### charter blast

Expand Down Expand Up @@ -903,7 +932,7 @@ npx charter surface --schema db/schema.sql # explicit schema path
| `--preset <name>` | Stack preset (`worker`, `frontend`, `backend`, `fullstack`, `docs`) |
| `--detect-only` | Setup mode: detect stack/preset and exit |
| `--no-dependency-sync` | Setup mode: do not rewrite `@stackbilt/cli` devDependency |
| `--force` | Overwrite existing files (hooks, ADF modules, config) |
| `--force` | Overwrite existing files (hooks, ADF modules, config). `bootstrap --force` never overwrites a hand-authored or compiled vendor file |

## Exit Codes

Expand Down
Loading
Loading