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
2 changes: 1 addition & 1 deletion .ai/.adf.lock
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"core.adf": "74a53c306c131c1c"
"core.adf": "9ea44032a909ed99"
}
2 changes: 1 addition & 1 deletion .ai/core.adf
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ ADF: 0.1
adf_commands_loc: 0 / 650 [lines]
adf_bundle_loc: 0 / 200 [lines]
adf_sync_loc: 0 / 250 [lines]
adf_evidence_loc: 0 / 300 [lines]
adf_evidence_loc: 0 / 380 [lines]
adf_migrate_loc: 0 / 500 [lines]
bundler_loc: 0 / 500 [lines]
parser_loc: 0 / 300 [lines]
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/charter-governance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ jobs:
fetch-depth: 0

- uses: pnpm/action-setup@v4
with:
version: 9

- uses: actions/setup-node@v4
with:
Expand All @@ -35,6 +33,7 @@ jobs:

- name: Drift Scan
run: npx charter drift --ci --format text
if: hashFiles('.charter/patterns/*.json') != ''

- name: ADF Wiring & Pointer Integrity
run: npx charter doctor --adf-only --ci --format text
Expand Down
14 changes: 7 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm run build
- run: pnpm run typecheck
- run: pnpm run build
- run: pnpm run docs:check
- run: pnpm run verify:adf
- run: pnpm run test
- run: node packages/cli/dist/bin.js --help
- name: Docs sync check
run: pnpm run docs:check
if: hashFiles('.docsync.json') != ''
- run: pnpm run verify:adf
- run: pnpm run test
- run: node packages/cli/dist/bin.js --help

8 changes: 7 additions & 1 deletion .github/workflows/governance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,19 @@ jobs:
with:
fetch-depth: 0 # Full history needed for commit analysis

- uses: pnpm/action-setup@v4
if: hashFiles('pnpm-lock.yaml') != ''

- uses: actions/setup-node@v4
with:
node-version: '20'
cache: ${{ hashFiles('pnpm-lock.yaml') != '' && 'pnpm' || hashFiles('package-lock.json') != '' && 'npm' || '' }}

- name: Install dependencies
run: |
if [ -f package-lock.json ]; then
if [ -f pnpm-lock.yaml ]; then
pnpm install --frozen-lockfile
elif [ -f package-lock.json ]; then
npm ci
else
npm install
Expand Down
91 changes: 91 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: Release

on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: 'Existing tag to publish (for backfill), e.g. v0.4.2'
required: true
type: string

permissions:
contents: write

jobs:
publish-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Resolve tag
id: tag
shell: bash
run: |
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
TAG="${{ inputs.tag }}"
else
TAG="${GITHUB_REF_NAME}"
fi

if [[ -z "${TAG}" ]]; then
echo "Tag could not be resolved." >&2
exit 1
fi

echo "value=${TAG}" >> "$GITHUB_OUTPUT"

- name: Verify tag
shell: bash
run: |
TAG="${{ steps.tag.outputs.value }}"
if [[ ! "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Invalid tag format: ${TAG}. Expected v<major>.<minor>.<patch>" >&2
exit 1
fi

if [[ "${GITHUB_EVENT_NAME}" == "push" ]]; then
PKG_VERSION="$(node -p "require('./packages/cli/package.json').version")"
EXPECTED_TAG="v${PKG_VERSION}"

if [[ "${TAG}" != "${EXPECTED_TAG}" ]]; then
echo "Tag/version mismatch on push: got ${TAG}, expected ${EXPECTED_TAG}" >&2
exit 1
fi
else
if ! git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
echo "Tag not found in repository: ${TAG}" >&2
exit 1
fi
fi

- name: Build release notes from CHANGELOG
shell: bash
run: |
TAG="${{ steps.tag.outputs.value }}"
VERSION="${TAG#v}"

awk -v version="${VERSION}" '
BEGIN { in_section=0 }
$0 ~ "^## \\[" version "\\]" { in_section=1; print; next }
in_section && $0 ~ "^## \\[" { exit }
in_section { print }
' CHANGELOG.md > release_notes.md

if [[ ! -s release_notes.md ]]; then
echo "## ${TAG}" > release_notes.md
echo >> release_notes.md
echo "See CHANGELOG.md for release details." >> release_notes.md
fi

- name: Create or update GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.tag.outputs.value }}
name: ${{ steps.tag.outputs.value }}
body_path: release_notes.md
generate_release_notes: true
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ AGENTS.md
CLAUDE.md
plans/
governance/

# local telemetry artifacts
.charter/telemetry/
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ All notable changes to this project are documented in this file.

The format is based on Keep a Changelog and follows Semantic Versioning.

## [Unreleased]

### Added
- **`charter adf metrics recalibrate`**: New subcommand to re-measure LOC from manifest metric sources, propose new ceilings with configurable headroom, and update metric baselines/ceilings with required rationale (`--reason` or `--auto-rationale`).
- **Budget rationale trail**: Recalibration writes `BUDGET_RATIONALES` map entries so metric-cap changes carry explicit context for later review.

### Changed
- **Stale baseline detection in evidence**: `charter adf evidence` now detects stale metric baselines (current vs baseline drift), emits structured `staleBaselines` warnings (baseline/current/delta/recommendedCeiling/rationaleRequired), and suggests recalibration actions.

## [0.4.2] - 2026-02-27

### Added
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# Charter Kit

[![npm version](https://img.shields.io/npm/v/@stackbilt/cli?label=charter&color=5F7FFF&style=for-the-badge)](https://www.npmjs.com/package/@stackbilt/cli)

![Charter Kit hero](./stackbilt-charter-2.png)

> **ADF is currently running its inaugural full-SDLC cycle to gather real-world data for iterative improvement. Updates coming soon and regularly.**
> **ADF has completed its inaugural full-SDLC cycle and is now in iterative improvement. Expect frequent updates as real-world feedback shapes the format.**

Charter is a local-first governance toolkit with a built-in AI context compiler. It ships **ADF (Attention-Directed Format)** -- a modular, AST-backed context system that replaces monolithic `.cursorrules` and `claude.md` files -- alongside offline governance checks for commit trailers, risk scoring, drift detection, and change classification.

![ADF Architecture](./ADF_1.png)

## ADF: Attention-Directed Format

ADF treats LLM context as a compiled language. Instead of dumping flat markdown into a context window, ADF uses emoji-decorated semantic keys, a strict AST, and a module system with progressive disclosure -- so agents load only the context they need for the current task.
Expand Down Expand Up @@ -46,6 +46,7 @@ charter adf sync --check

# Validate metric constraints and produce a structured evidence report
charter adf evidence --auto-measure --format json
charter adf metrics recalibrate --headroom 15 --reason "Added new built modules after scope expansion" --dry-run
charter telemetry report --period 24h --format json
```

Expand Down Expand Up @@ -225,6 +226,7 @@ Teams often score lower early due to missing governance trailers. Use this ramp:
- `charter adf sync --check [--ai-dir <dir>]`: verify source .adf files match locked hashes (exit 1 on drift)
- `charter adf sync --write [--ai-dir <dir>]`: update `.adf.lock` with current source hashes
- `charter adf evidence [--task "<prompt>"] [--ai-dir <dir>] [--auto-measure] [--context '{"k":v}'] [--context-file <path>]`: validate metric constraints and produce structured evidence report
- `charter adf metrics recalibrate [--headroom <percent>] [--reason "<text>"|--auto-rationale] [--dry-run]`: recalibrate metric baselines/ceilings from current LOC and record budget rationale
- `charter adf migrate [--dry-run] [--source <file>] [--no-backup] [--merge-strategy append|dedupe|replace]`: ingest existing agent config files and migrate content into ADF modules
- `charter telemetry report [--period <30m|24h|7d>]`: summarize passive local CLI telemetry from `.charter/telemetry/events.ndjson`
- `charter why`: explain adoption rationale and expected payoff
Expand Down
13 changes: 13 additions & 0 deletions docs-snippets/charter-cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,19 @@ npx charter adf evidence --context-file metrics.json
**CI mode:** exits 1 on any constraint failure. Warnings (at boundary) surface in the report but do not fail the build.

Output includes constraint results, weight summary (load-bearing / advisory / unweighted), sync status, advisory-only warnings, and a `nextActions` array.
When stale baselines are detected, JSON output includes `staleBaselines` entries with `baseline`, `current`, `delta`, `recommendedCeiling`, and `rationaleRequired`.

### charter adf metrics recalibrate

Re-measures LOC from manifest metric sources and recalibrates metric values/ceilings using a configurable headroom policy.

```bash
npx charter adf metrics recalibrate --headroom 15 --reason "Added new built modules after scope increase" --dry-run
npx charter adf metrics recalibrate --headroom 20 --auto-rationale
```

- Writes metric rationale entries into a `BUDGET_RATIONALES` section.
- `--reason` (or `--auto-rationale`) is required for recalibration updates.

### ADF Automation Gate

Expand Down
4 changes: 3 additions & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ charter adf sync --check [--ai-dir <dir>]
charter adf sync --write [--ai-dir <dir>]
charter adf evidence [--task "<prompt>"] [--ai-dir <dir>] [--auto-measure]
[--context '{"key": value}'] [--context-file <path>]
charter adf metrics recalibrate [--headroom <percent>] [--reason "<text>"|--auto-rationale] [--dry-run]
charter adf migrate [--dry-run] [--source <file>] [--no-backup]
[--merge-strategy append|dedupe|replace] [--ai-dir <dir>]
```
Expand All @@ -294,7 +295,8 @@ charter adf migrate [--dry-run] [--source <file>] [--no-backup]
- `bundle`: Read `manifest.adf`, resolve ON_DEMAND modules via keyword matching against the task, and output merged context with token estimate, trigger observability (matched keywords, load reasons), unmatched modules, and advisory-only warnings. Missing ON_DEMAND files are warnings in output (`missingModules` in JSON), while missing DEFAULT_LOAD files still fail.
- `sync --check`: Verify source `.adf` files match their locked hashes. Exits 1 if any source has drifted since last sync.
- `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). The governance workflow template runs this automatically on PRs when `.ai/manifest.adf` is present.
- `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 <file>` 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.

### `charter telemetry`
Expand Down
95 changes: 95 additions & 0 deletions packages/cli/src/__tests__/adf-metrics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { CLIOptions } from '../index';
import { adfMetricsCommand } from '../commands/adf-metrics';
import { adfEvidence } from '../commands/adf-evidence';

const baseOptions: CLIOptions = {
configPath: '.charter',
format: 'json',
ciMode: false,
yes: false,
};

const originalCwd = process.cwd();
const tempDirs: string[] = [];

afterEach(() => {
process.chdir(originalCwd);
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir) {
fs.rmSync(dir, { recursive: true, force: true });
}
}
vi.restoreAllMocks();
});

function writeFixtureRepo(tmp: string, baseline = 100): void {
fs.mkdirSync(path.join(tmp, '.ai'), { recursive: true });
fs.mkdirSync(path.join(tmp, 'src'), { recursive: true });

fs.writeFileSync(path.join(tmp, '.ai', 'manifest.adf'), `ADF: 0.1
DEFAULT_LOAD:
- core.adf
- state.adf

METRICS:
COMPONENTS_TOTAL_LOC: src/components.ts
`);
fs.writeFileSync(path.join(tmp, '.ai', 'core.adf'), `ADF: 0.1
METRICS:
components_total_loc: ${baseline} / 120 [lines]
`);
fs.writeFileSync(path.join(tmp, '.ai', 'state.adf'), 'ADF: 0.1\nSTATE:\n CURRENT: testing\n');
fs.writeFileSync(path.join(tmp, 'src', 'components.ts'), Array.from({ length: 200 }, (_, i) => `line_${i}`).join('\n') + '\n');
}

describe('adf metrics recalibrate', () => {
it('requires rationale by default', () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'charter-metrics-test-'));
tempDirs.push(tmp);
process.chdir(tmp);
writeFixtureRepo(tmp);

expect(() => adfMetricsCommand(baseOptions, ['recalibrate'])).toThrow('requires --reason');
});

it('recalibrates metric ceilings and writes rationale entries', () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'charter-metrics-test-'));
tempDirs.push(tmp);
process.chdir(tmp);
writeFixtureRepo(tmp);

const exitCode = adfMetricsCommand(baseOptions, ['recalibrate', '--headroom', '20', '--reason', 'Scope expanded with new built views']);
expect(exitCode).toBe(0);

const measured = fs.readFileSync(path.join(tmp, 'src', 'components.ts'), 'utf-8').split('\n').length;
const ceiling = Math.ceil(measured * 1.2);
const core = fs.readFileSync(path.join(tmp, '.ai', 'core.adf'), 'utf-8');
expect(core).toContain(`components_total_loc: ${measured} / ${ceiling} [lines]`);
expect(core).toContain('BUDGET_RATIONALES');
expect(core).toContain('Scope expanded with new built views');
});
});

describe('adf evidence stale baseline warnings', () => {
it('emits staleBaselines when measured value greatly exceeds baseline value', () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'charter-evidence-test-'));
tempDirs.push(tmp);
process.chdir(tmp);
writeFixtureRepo(tmp, 80);

const logs: string[] = [];
vi.spyOn(console, 'log').mockImplementation((msg: string) => logs.push(msg));
const exitCode = adfEvidence(baseOptions, ['--ai-dir', '.ai', '--auto-measure']);
expect(exitCode).toBe(0);

const out = JSON.parse(logs[0]) as { staleBaselines?: Array<{ metric: string; rationaleRequired: boolean }> };
expect(out.staleBaselines?.length).toBeGreaterThan(0);
expect(out.staleBaselines?.[0].metric).toBe('components_total_loc');
expect(out.staleBaselines?.[0].rationaleRequired).toBe(true);
});
});
Loading