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: 6 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '20'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm run build
- name: Measure brief token budget
run: node scripts/measure-brief-size.mjs
- name: Eval brief orientation coverage
run: node scripts/eval-brief-coverage.mjs
- run: pnpm run typecheck
- name: Docs sync check
run: pnpm run docs:check
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable changes to this project are documented in this file.

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

## [0.14.0] - 2026-05-19

Synchronized version bump for all `@stackbilt/*` packages to 0.14.0.

### Added
- **`charter context`** — new CLI command that generates a pre-digested repo brief (identity, surface, hotspots, sensitivity, governance) within a 2,000-token budget. Designed as the fastest way to orient an AI agent entering a Charter-governed repo; replaces 15-30 cold-boot discovery tool calls. Flags: `--stdout-only`, `--verbose` (no ceiling), `--write` (file only). Writes `.charter/context.md` by default.
- **`charter_brief` MCP tool** — `charter serve` now registers `charter_brief`, which calls `generateBrief()` and returns the same bounded brief over MCP. Tool description instructs agents to call it first in any session.
- **CI brief quality gates** — `scripts/measure-brief-size.mjs` (exits 1 if brief > 2000 tokens) and `scripts/eval-brief-coverage.mjs` (fixture-based check requiring all 5 sections and 5 orientation questions to be answerable from the brief alone) are added to `.github/workflows/ci.yml` after the build step.
- **`context.md` excluded from `.charter/.gitignore`** — generated brief is local-only by default; committed to `.gitignore` by `charter bootstrap` and `charter init`.

## [0.13.0] - 2026-05-19

Synchronized version bump for all `@stackbilt/*` packages to 0.13.0.
Expand Down Expand Up @@ -505,6 +515,7 @@ All 345 existing tests pass.
### Security
- Added repository security policy and reporting process.

[0.14.0]: https://github.com/stackbilt-dev/charter/compare/v0.13.0...v0.14.0
[0.13.0]: https://github.com/stackbilt-dev/charter/compare/v0.12.1...v0.13.0
[0.12.1]: https://github.com/stackbilt-dev/charter/compare/v0.12.0...v0.12.1
[0.5.0]: https://github.com/stackbilt-dev/charter/compare/v0.4.2...v0.5.0
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ METRICS [load-bearing]:

Claude Code can query `getProjectContext`, `getArchitecturalDecisions`, `getProjectState`, and `getRecentChanges` directly.

The `charter_brief` MCP tool composes routes, hotspots, and governance into a single pre-digested brief — call it first in any agent session to skip 15-30 cold-boot discovery calls.

## Commands

### Govern
Expand All @@ -107,6 +109,7 @@ Claude Code can query `getProjectContext`, `getArchitecturalDecisions`, `getProj
charter # Repo risk/value snapshot
charter bootstrap --ci github # One-command onboarding
charter bootstrap --security-sensitive # SECURITY.md + hard security drift denies
charter context # pre-digested repo brief for AI agents (routes, hotspots, governance)
charter doctor # Environment/config health check
charter validate # Commit governance (trailers)
charter drift # Pattern drift scanning
Expand Down
60 changes: 60 additions & 0 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,66 @@ npx charter blast src/foo.ts --root ./packages/server # scan a subdirector

**Semantics:** zero runtime dependencies, no LLM calls, no TypeScript compiler API. Regex-based import extraction — trades some precision for universality across JavaScript/TypeScript/ESM/CommonJS projects.

### charter context

Pre-digested repo brief for AI agents. Composes routes (surface), hotspots (blast), sensitivity tags, and governance posture into a single bounded markdown document.

**The fastest way to orient an AI agent in a Charter-governed repo.** Reading the brief replaces 15-30 cold-boot discovery tool calls.

#### Usage

```bash
npx charter context # print brief + write .charter/context.md
npx charter context --stdout-only # print only, no file write
npx charter context --verbose # no token ceiling (for human reading)
npx charter context --write # write .charter/context.md only (for hooks)
```

#### Brief sections

| Section | Source | Always present |
| ------- | ------ | -------------- |
| Identity | `.charter/config.json` + `package.json` | Yes |
| Surface | `charter surface` (routes + D1 tables) | Yes |
| Hotspots | `charter blast` (top hot files by importer count) | Yes |
| Sensitivity | `.charter/config.json` sensitivity tags | Yes |
| Governance | `.ai/manifest.adf` module routing | Yes |

#### Token budget

The brief is capped at **2000 tokens** (~8000 characters). When content exceeds the budget, sections are truncated in this order: hotspots tail → D1 tables → routes → governance ON_DEMAND entries. A `## Truncated` section is appended listing what was reduced.

Use `--verbose` to remove the ceiling for interactive sessions.

#### MCP tool

`charter serve` registers `charter_brief`. Agents should call this first:

> "CALL THIS FIRST when entering a Charter-governed repo. Returns routes, hotspots, sensitivity tags, and governance in a single pre-digested brief — replaces 15-30 discovery tool calls."

#### Post-commit hook (keep brief fresh)

```bash
echo 'charter context --write' >> .git/hooks/post-commit
chmod +x .git/hooks/post-commit
```

Or use `charter bootstrap` — it adds this as a suggested next step.

#### Blast seed strategy

Seeds for hotspot analysis are chosen by preset (from `.charter/config.json`):

| Preset | Seeds |
| ------ | ----- |
| worker | `src/index.*`, route files, wrangler entry |
| frontend | `src/App.*`, `src/main.*`, `src/index.*` |
| backend | `src/index.*`, `src/server.*`, `src/app.*` |
| fullstack | worker + frontend seeds |
| cli | `bin/` entries, `src/commands/*.ts` |
| docs | `README.md`, `docs/*.md` |
| unknown | `src/index.*`, `src/main.*` |

### charter surface

Extract the API surface of a project: HTTP routes and database schema tables.
Expand Down
2 changes: 1 addition & 1 deletion packages/adf/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@stackbilt/adf",
"sideEffects": false,
"version": "0.13.0",
"version": "0.14.0",
"description": "ADF (Attention-Directed Format) — AST-backed context format for AI agents",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/blast/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@stackbilt/blast",
"sideEffects": false,
"version": "0.13.0",
"version": "0.14.0",
"description": "Blast radius analysis via reverse dependency graph + BFS traversal",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down
4 changes: 2 additions & 2 deletions packages/ci/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@stackbilt/ci",
"sideEffects": false,
"version": "0.13.0",
"version": "0.14.0",
"description": "GitHub Actions adapter for Charter governance checks",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down Expand Up @@ -29,7 +29,7 @@
},
"homepage": "https://github.com/Stackbilt-dev/charter#readme",
"dependencies": {
"@stackbilt/types": "^0.13.0"
"@stackbilt/types": "^0.14.0"
},
"scripts": {
"prepublishOnly": "node ../../scripts/ensure-pnpm-publish.mjs"
Expand Down
4 changes: 2 additions & 2 deletions packages/classify/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@stackbilt/classify",
"sideEffects": false,
"version": "0.13.0",
"version": "0.14.0",
"description": "Heuristic change classification (SURFACE/LOCAL/CROSS_CUTTING)",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down Expand Up @@ -29,7 +29,7 @@
},
"homepage": "https://github.com/Stackbilt-dev/charter#readme",
"dependencies": {
"@stackbilt/types": "^0.13.0"
"@stackbilt/types": "^0.14.0"
},
"scripts": {
"prepublishOnly": "node ../../scripts/ensure-pnpm-publish.mjs"
Expand Down
20 changes: 10 additions & 10 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@stackbilt/cli",
"sideEffects": false,
"version": "0.13.0",
"version": "0.14.0",
"description": "Charter CLI — repo-level governance checks + architecture scaffolding",
"bin": {
"charter": "./dist/bin.js",
Expand Down Expand Up @@ -41,15 +41,15 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.29.0",
"@stackbilt/adf": "^0.13.0",
"@stackbilt/blast": "^0.13.0",
"@stackbilt/classify": "^0.13.0",
"@stackbilt/core": "^0.13.0",
"@stackbilt/drift": "^0.13.0",
"@stackbilt/git": "^0.13.0",
"@stackbilt/surface": "^0.13.0",
"@stackbilt/types": "^0.13.0",
"@stackbilt/validate": "^0.13.0"
"@stackbilt/adf": "^0.14.0",
"@stackbilt/blast": "^0.14.0",
"@stackbilt/classify": "^0.14.0",
"@stackbilt/core": "^0.14.0",
"@stackbilt/drift": "^0.14.0",
"@stackbilt/git": "^0.14.0",
"@stackbilt/surface": "^0.14.0",
"@stackbilt/types": "^0.14.0",
"@stackbilt/validate": "^0.14.0"
},
"license": "Apache-2.0",
"author": "Stackbilt LLC"
Expand Down
156 changes: 156 additions & 0 deletions packages/cli/src/__tests__/context.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { CLIOptions } from '../index';
import { generateBrief, contextCommand } from '../commands/context';

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

// Track original cwd and temp dirs for cleanup
const originalCwd = process.cwd();
const tempDirs: string[] = [];

function makeTempDir(): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'charter-context-test-'));
tempDirs.push(dir);
return dir;
}

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

describe('generateBrief on minimal repo returns all 5 sections', () => {
it('generates a brief with all required sections', { timeout: 30000 }, async () => {
const tmp = makeTempDir();
process.chdir(tmp);

// Set up package.json
fs.writeFileSync(
path.join(tmp, 'package.json'),
JSON.stringify({
name: 'test-worker',
version: '1.0.0',
description: 'A test worker',
}),
'utf8'
);

// Set up .charter/config.json
fs.mkdirSync(path.join(tmp, '.charter'), { recursive: true });
fs.writeFileSync(
path.join(tmp, '.charter', 'config.json'),
JSON.stringify({ stack: 'worker', preset: 'worker' }),
'utf8'
);

// Set up src/index.ts with a Hono route
fs.mkdirSync(path.join(tmp, 'src'), { recursive: true });
fs.writeFileSync(
path.join(tmp, 'src', 'index.ts'),
`import { Hono } from 'hono';\nconst app = new Hono();\napp.get('/api/hello', (c) => c.json({ ok: true }));\nexport default app;\n`,
'utf8'
);

// Set up .ai/manifest.adf with core.adf in DEFAULT_LOAD
fs.mkdirSync(path.join(tmp, '.ai'), { recursive: true });
fs.writeFileSync(
path.join(tmp, '.ai', 'manifest.adf'),
`ADF: 0.1\n\nDEFAULT_LOAD:\n - core.adf\n\nON_DEMAND:\n - backend.adf (Triggers on: backend, api)\n`,
'utf8'
);

const result = await generateBrief({ configPath: '.charter', aiDir: '.ai' });

expect(result.markdown).toContain('## Identity');
expect(result.markdown).toContain('## Surface');
expect(result.markdown).toContain('## Hotspots');
expect(result.markdown).toContain('## Sensitivity');
expect(result.markdown).toContain('## Governance');
});
});

describe('generateBrief respects token ceiling', () => {
it('truncates when surface has many routes and keeps tokenCount <= 2000', { timeout: 30000 }, async () => {
const tmp = makeTempDir();
process.chdir(tmp);

fs.writeFileSync(
path.join(tmp, 'package.json'),
JSON.stringify({ name: 'large-surface', version: '1.0.0' }),
'utf8'
);
fs.mkdirSync(path.join(tmp, '.charter'), { recursive: true });
fs.writeFileSync(
path.join(tmp, '.charter', 'config.json'),
JSON.stringify({ stack: 'backend', preset: 'backend' }),
'utf8'
);

// Create many routes with very long paths to guarantee > 8000 chars in surface section
fs.mkdirSync(path.join(tmp, 'src'), { recursive: true });
const routeLines: string[] = [`import { Hono } from 'hono';`, `const app = new Hono();`];
// Generate 50 routes with very long paths (each route line ~100 chars in markdown table)
for (let i = 0; i < 50; i++) {
const pad = 'a'.repeat(60); // long path padding
routeLines.push(
`app.get('/api/${pad}-endpoint-number-${i}/very/deep/nesting/path', (c) => c.json({ id: ${i} }));`
);
}
routeLines.push('export default app;');
fs.writeFileSync(path.join(tmp, 'src', 'index.ts'), routeLines.join('\n'), 'utf8');

// Create a large manifest to push Governance section over budget
fs.mkdirSync(path.join(tmp, '.ai'), { recursive: true });
const manifestLines = ['ADF: 0.1', '', 'DEFAULT_LOAD:', ' - core.adf', '', 'ON_DEMAND:'];
for (let i = 0; i < 30; i++) {
// Long trigger lists to add bulk to the ON_DEMAND section
const triggers = Array.from({ length: 10 }, (_, j) => `feature-${i}-trigger-${j}-keyword`).join(', ');
manifestLines.push(` - module-${i}.adf (Triggers on: ${triggers})`);
}
fs.writeFileSync(path.join(tmp, '.ai', 'manifest.adf'), manifestLines.join('\n'), 'utf8');

const result = await generateBrief({ configPath: '.charter', aiDir: '.ai' });

expect(result.tokenCount).toBeLessThanOrEqual(2000);
expect(result.truncated).toBe(true);
});
});

describe('contextCommand writes .charter/context.md', () => {
it('writes context.md when --write flag is passed', { timeout: 30000 }, async () => {
const tmp = makeTempDir();
process.chdir(tmp);

fs.writeFileSync(
path.join(tmp, 'package.json'),
JSON.stringify({ name: 'write-test', version: '0.1.0' }),
'utf8'
);

// Silence stdout for the write-only test
vi.spyOn(console, 'log').mockImplementation(() => {});

const exitCode = await contextCommand(
{ ...options, configPath: path.join(tmp, '.charter') },
['--write']
);

expect(exitCode).toBe(0);
const outPath = path.join(tmp, '.charter', 'context.md');
expect(fs.existsSync(outPath)).toBe(true);
const content = fs.readFileSync(outPath, 'utf8');
expect(content).toContain('## Identity');
});
});
5 changes: 5 additions & 0 deletions packages/cli/src/commands/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,11 @@ export async function bootstrapCommand(options: CLIOptions, args: string[]): Pro
required: false,
reason: 'Install commit-msg hook for trailer enforcement',
});
result.nextSteps.push({
cmd: 'echo \'charter context --write\' >> .git/hooks/post-commit && chmod +x .git/hooks/post-commit',
required: false,
reason: 'Keep .charter/context.md fresh after each commit (charter brief auto-refresh)',
});
}

// ========================================================================
Expand Down
Loading
Loading