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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ The format is based on Keep a Changelog and follows Semantic Versioning.
- **The interactive y/N prompts (`bootstrap`'s "Register these modules now?" and the `--reset-adf` "Replace them?") left the CLI to exit 0 mid-run when stdin closed at the prompt or Ctrl-C was pressed** (`#375`). Ctrl-D on an empty line (or any close before an answer) now counts as N, with `No answer (input closed); treating it as N.` on stderr: bootstrap leaves the orphan modules unregistered and carries on, and `--reset-adf` is declined (exit 1, nothing written). Ctrl-C stops the command: it prints `charter: Aborted.` on stderr and exits 130 (the usage-log event is still recorded), so bootstrap does not go on to migrate agent configs or install packages.
- **`charter init --guided --preset <name>` silently ignored `--preset`** (`#375`). It now seeds the answer: the preset is used when the language answer names no known stack (blank, or something like `cobol`), and the first question says `blank keeps --preset <name>`. A recognized language or a Cloudflare `y` still decides, as before.
- Preset lists named four or five presets where six are accepted: the root `--help` (`charter init`, `charter setup` and the `--preset` option), `charter adf --help`, `packages/cli/README.md`, `docs-snippets/charter-cli-reference.md` and the Global Flags table of `docs/cli-reference.md` now list all six (`worker|frontend|backend|fullstack|docs|rust-wasm`).
- **Help text gaps** (`#363`). The root `--help` lists `charter doctor [--adf-only] [--ci] [--format text|json]` and the pre-commit-hook-only `--staged [--tidy-skipped "<files>"]`, and its `charter adf` entry names all fifteen subcommands (it named nine). `charter adf --help` describes `tidy --source` and `--print-paths`, gives each `adf vendor` subcommand (`status`, `set`, `init`, `restore`, `rename-legacy`) its own line and flags, and documents `adf context`, which it left out. `docs/cli-reference.md` lists doctor's `--format json`.

## [2.1.0] - 2026-10-17

Expand Down
1 change: 1 addition & 0 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ npx charter doctor --ci --format json # CI mode: exit 1 on warnings

- `--adf-only` — run only ADF readiness checks, skip Charter config validation
- `--ci` — non-interactive, exits with policy violation code on warnings
- `--format json` — print the result (`status`, `checks`) as JSON instead of text
- `--staged` — the pre-commit hook's gate ([#357](https://github.com/Stackbilt-dev/charter/issues/357)): the `adf vendor bloat` check counts only vendor files staged in the git index (staged deletions excluded). It selects files by name and still reads their working-tree content, not the staged blob. Bloat in other vendor files moves to an INFO row, `adf vendor bloat (not blocking)`, so it does not fail `--ci`. No other check is scoped. If the index cannot be read, every vendor file counts, as without the flag.
- `--tidy-skipped "<files>"` — with `--staged` only: space-separated vendor files the hook's tidy block skipped. Their bloat goes to the same INFO row. The value may be empty (`""`). Used without `--staged`, it is a usage error (exit 2).
- `CHARTER_HOOK_SCOPED_DOCTOR` (environment) — set only by the generated pre-commit hook while it runs `verify:adf`, after its own `--staged` doctor. Doctor honours it only when its value has the hook's token form (`<pid>-<seconds>`) and the file `charter-hook-scoped-doctor-<token>` in the git directory holds that token. It then reports the `adf vendor bloat` check as INFO (`adf vendor bloat (checked by pre-commit hook)`) and says so on stderr. No other check changes. The variable on its own (no matching file) has no effect, and `--staged` takes precedence over it.
Expand Down
52 changes: 52 additions & 0 deletions packages/cli/src/__tests__/help-routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,58 @@ describe('--help routing', () => {
expect(createdEntries).toEqual([]);
});

it('documents doctor\'s --ci, --format and hook-only --staged/--tidy-skipped flags in the root help (#363)', async () => {
const { output } = await captureRun(['--help']);

expect(output).toContain('charter doctor [--adf-only] [--ci] [--format text|json]');
expect(output).toMatch(/--staged \[--tidy-skipped "<files>"\]: pre-commit hook only/);
});

// The subcommands adfCommand dispatches (its "Supported:" list). Each must be
// named in the root help's adf entry and documented in `adf --help`.
const ADF_SUBCOMMANDS = [
'init', 'fmt', 'patch', 'create', 'populate', 'bundle', 'sync', 'evidence',
'migrate', 'tidy', 'metrics', 'context', 'compile', 'suggest', 'vendor',
];

it('keeps the subcommand list above in step with adfCommand\'s dispatch', async () => {
const message = await captureRun(['adf', 'no-such-subcommand']).then(
() => '',
(err: Error) => err.message,
);
const supported = message.match(/Supported: (.*)$/)?.[1].split(', ');

expect(supported).toEqual(ADF_SUBCOMMANDS);
});

it('lists every adf subcommand in the root help and in `adf --help` (#363)', async () => {
const root = (await captureRun(['--help'])).output;
const adfEntry = root.slice(root.indexOf('charter adf <subcommand>'), root.indexOf('charter serve'));
const adf = (await captureRun(['adf', '--help'])).output;

for (const sub of ADF_SUBCOMMANDS) {
expect(adfEntry).toMatch(new RegExp(`[(\\s]${sub}[,)]`));
expect(adf).toContain(`charter adf ${sub}`);
}
});

it('describes tidy --source/--print-paths and each adf vendor subcommand in `adf --help` (#363)', async () => {
const { output } = await captureRun(['adf', '--help']);

expect(output).toContain('--source <file>: tidy only this file; repeat it');
expect(output).toContain('--print-paths: print only `vendor <path>` then `module <path>` lines');
expect(output).toContain('--mode compiled: with --write');
for (const usage of [
'status [--all]:',
'set <file> <pointer|compiled|unmanaged> [--dry-run]:',
'init [--dry-run]:',
'restore <file> [--dry-run]:',
'rename-legacy [--dry-run]:',
]) {
expect(output).toContain(` ${usage}`);
}
});

it('still reports the version for `--version` after a command name', async () => {
const { exitCode, output } = await captureRun(['adf', '--version']);

Expand Down
48 changes: 24 additions & 24 deletions packages/cli/src/commands/adf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -903,11 +903,11 @@ function printHelp(): void {
console.log(' --preset <preset>: scaffold preset-aware modules (worker|frontend|backend|fullstack|docs|rust-wasm)');
console.log(' --emit-pointers: also generate thin pointer files (CLAUDE.md, .cursorrules, AGENTS.md)');
console.log('');
console.log(' charter adf fmt <file> [--check] [--write]');
console.log(' charter adf fmt <file> [--check] [--write] | charter adf fmt --explain');
console.log(' Parse and reformat an ADF file to canonical form.');
console.log(' --check: exit 1 if not canonical (no write)');
console.log(' --write: reformat file in place');
console.log(' Default: print formatted output to stdout.');
console.log(' Default: print formatted output to stdout. --explain: show the canonical section ordering.');
console.log('');
console.log(' charter adf patch <file> --ops <json> | --ops-file <path>');
console.log(' Apply ADF_PATCH operations to a file.');
Expand All @@ -929,33 +929,26 @@ function printHelp(): void {
console.log(' charter adf bundle --task "<prompt>" [--ai-dir <dir>]');
console.log(' Resolve manifest modules for a task and output merged context.');
console.log('');
console.log(' charter adf fmt --explain');
console.log(' Show canonical section ordering used by the formatter.');
console.log(' charter adf context <--files "<a,b>" and/or --keywords "<text>"> [--bundle] [--ai-dir <dir>]');
console.log(' Resolve which modules apply to file paths and/or keywords; --bundle: emit the merged ADF document.');
console.log('');
console.log(' charter adf sync --check [--ai-dir <dir>]');
console.log(' Verify source .adf files match their locked hashes.');
console.log(' Exit 1 if any source has changed since last sync.');
console.log('');
console.log(' charter adf sync --write [--ai-dir <dir>]');
console.log(' Update .adf.lock with current source hashes.');
console.log('');
console.log(' charter adf sync --explain');
console.log(' Show .adf.lock schema documentation.');
console.log(' charter adf sync <--check | --write> [--ai-dir <dir>] | charter adf sync --explain');
console.log(' --check: verify source .adf files match their locked hashes; exit 1 if any changed since last sync');
console.log(' --write: update .adf.lock with current source hashes');
console.log(' --explain: show .adf.lock schema documentation');
console.log('');
console.log(' charter adf evidence [--task "<prompt>"] [--ai-dir <dir>] [--auto-measure]');
console.log(' [--context \'{"key": value}\']');
console.log(' Validate metric constraints and produce a structured evidence report.');
console.log(' --task: resolve on-demand modules for task. Omit for defaultLoad only.');
console.log(' --auto-measure: count lines in files from manifest METRICS section.');
console.log(' --context: JSON object of external metric overrides (wins over auto).');
console.log(' --context-file: read --context JSON from a file instead.');
console.log(' --context: JSON object of external metric overrides (wins over auto); --context-file: read it from a file.');
console.log(' In --ci mode, exit 1 if any constraint fails.');
console.log('');
console.log(' charter adf migrate [--dry-run] [--source <file>] [--no-backup]');
console.log(' [--merge-strategy append|dedupe|replace] [--ai-dir <dir>]');
console.log(' Ingest existing agent config files (CLAUDE.md, .cursorrules, etc.) and');
console.log(' migrate their content into structured ADF modules. Replaces originals');
console.log(' with thin pointers that retain environment-specific rules.');
console.log(' Ingest existing agent config files (CLAUDE.md, .cursorrules, etc.) into structured ADF modules.');
console.log(' Replaces originals with thin pointers that retain environment-specific rules.');
console.log(' --dry-run: preview migration plan without writing files');
console.log(' --source: migrate a single file instead of scanning all agent configs');
console.log(' --no-backup: skip creating .pre-adf-migrate.bak backups');
Expand All @@ -965,7 +958,10 @@ function printHelp(): void {
console.log(' Scan vendor config files for content added beyond the thin pointer,');
console.log(' classify and route to ADF modules, restore thin pointer.');
console.log(' --dry-run: preview without modifying files');
console.log(' --ci: exit 1 if bloat found (with --dry-run, for pre-commit gating)');
console.log(' --source <file>: tidy only this file; repeat it to tidy several, in argument order');
console.log(' --print-paths: print only `vendor <path>` then `module <path>` lines for files tidy writes');
console.log(' (would write, with --dry-run); no lines means nothing written. Not with --format json.');
console.log(' --ci: with --dry-run, exit 1 if bloat found (pre-commit gating); no effect without --dry-run');
console.log('');
console.log(' charter adf metrics recalibrate [--headroom <percent>] [--reason "<text>"|--auto-rationale] [--dry-run]');
console.log(' Recalibrate metric baselines/ceilings from current LOC with required rationale.');
Expand All @@ -980,15 +976,19 @@ function printHelp(): void {
console.log(' --force: with --write, overwrite hand-authored content (never changes a file\'s mode)');
console.log(' Default (no --write/--check): print to stdout (single target only)');
console.log('');
console.log(' charter adf vendor <status [--all] | set <file> <pointer|compiled|unmanaged> | init | restore <file> | rename-legacy> [--ai-dir <dir>] [--dry-run]');
console.log(' charter adf vendor <subcommand> [--ai-dir <dir>]');
console.log(' Show or record whether each vendor file is a pointer, compiled output, or unmanaged');
console.log(' (VENDORS in manifest.adf). adf compile and adf tidy act on it. restore rewrites one pointer, refusing on any content loss.');
console.log(' (VENDORS in manifest.adf). adf compile and adf tidy act on it.');
console.log(' status [--all]: recorded or inferred mode of each vendor file (read-only; --all adds absent names)');
console.log(' set <file> <pointer|compiled|unmanaged> [--dry-run]: record one file\'s mode');
console.log(' init [--dry-run]: record the inferred mode of every unrecorded vendor file on disk');
console.log(' restore <file> [--dry-run]: rewrite one pointer from its template; refuses on any content loss');
console.log(' rename-legacy [--dry-run]: rename a pre-#297 agents.md to AGENTS.md, moving its VENDORS key');
console.log('');
console.log(' charter adf suggest [--min-occurrences <n>] [--window-minutes <n>] [--ai-dir <dir>]');
console.log(' [--emit-ops <file>]');
console.log(' Diagnostics over .charter/telemetry/events.ndjson: dead modules,');
console.log(' recurring unmatched keywords, and modules that load yet a downstream');
console.log(' command still fails — all report-only, no module attribution guessed.');
console.log(' Diagnostics over .charter/telemetry/events.ndjson: dead modules, recurring unmatched keywords,');
console.log(' and modules that load yet a downstream command still fails (report-only, no attribution guessed).');
console.log(' Also reports co-occurrence trigger candidates (a weaker, non-failure-');
console.log(' backed heuristic: keyword X often appears alongside a keyword that');
console.log(' already triggers module M). --emit-ops writes non-ambiguous candidates');
Expand Down
10 changes: 8 additions & 2 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ Usage:
charter hook install --pre-commit [--force]
Install git pre-commit hook for ADF evidence gate
charter hook print --claude Print Claude Code session hook config (paste into .claude/settings.json)
charter adf <subcommand> ADF context format tools (init, fmt, patch, create, bundle, sync, evidence, migrate, metrics)
charter adf <subcommand> ADF context format tools; details: charter adf --help
(init, fmt, patch, create, populate, bundle, sync, evidence, migrate, tidy,
metrics, context, compile, suggest, vendor)
charter serve [--name <name>] [--ai-dir <dir>]
Expose ADF project context as an MCP server (stdio, for Claude Code/Codex/Cursor)
charter score [--ai-dir <dir>] AI-readiness audit for the current repo
Expand All @@ -80,7 +82,11 @@ Usage:
--exclude <glob>: skip paths matching glob (repeatable; supports ** and *)
charter telemetry report Local telemetry summary (passive CLI observability)
charter why Explain why teams adopt Charter and expected ROI
charter doctor [--adf-only] Check CLI + config health (or ADF-only wiring checks)
charter doctor [--adf-only] [--ci] [--format text|json]
Check CLI + config health (or ADF-only wiring checks)
--ci: exit 1 on any WARN; --format json: print the checks as JSON
--staged [--tidy-skipped "<files>"]: pre-commit hook only; vendor bloat
blocks only in staged files (minus those tidy skipped), the rest is INFO
charter --help Show this help
charter --version Show version

Expand Down
Loading