From 5cd527e04232a7b2b69decd79d8cb6ac2a5456b4 Mon Sep 17 00:00:00 2001 From: Kurt Overmier Date: Sat, 28 Feb 2026 16:37:09 -0600 Subject: [PATCH 1/6] docs(feedback): ADX-005 charter CLI UX findings from agent walkthrough Documents 8 findings from end-to-end charter CLI UX testing: hook install git-detection bug, migrate patch error on prose sections, doctor false positive on thin pointers, install EPERM hint, audit no-HEAD handling, migrate JSON compactness, and emoji encoding. Co-Authored-By: Claude Opus 4.6 --- papers/AGENT_DX_FEEDBACK_005.md | 180 ++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 papers/AGENT_DX_FEEDBACK_005.md diff --git a/papers/AGENT_DX_FEEDBACK_005.md b/papers/AGENT_DX_FEEDBACK_005.md new file mode 100644 index 0000000..9ec2a83 --- /dev/null +++ b/papers/AGENT_DX_FEEDBACK_005.md @@ -0,0 +1,180 @@ +--- +title: "Agent DX Feedback: charter CLI UX — Bootstrap, Migrate, Doctor, and Output Ergonomics" +feedback-id: ADX-005 +date: 2026-02-28 +source: "Claude Opus 4.6 (Anthropic) UX walkthrough of charter CLI" +severity: medium +related: + - ADX-004 (bootstrap overwrite hazard) + - ADX-003 (install automation friction) + - ADX-002 (onboarding friction baseline) +--- + +# Agent DX Feedback: charter CLI UX — Bootstrap, Migrate, Doctor, and Output Ergonomics + +## Summary + +End-to-end UX walkthrough of the charter CLI surfaced 8 findings across bootstrap, +migrate, doctor, audit, and output formatting. The overall impression is strongly +positive — bootstrap delivers high value with a single command. The issues below are +edge-case robustness and output ergonomics, not fundamental design problems. + +## Findings + +### F1. Bootstrap value is high (positive signal) + +One command creates governance + ADF + CI quickly. The bootstrap flow is the strongest +onboarding path charter has. This validates the ADX-002/ADX-003 investment in reducing +setup friction. + +**No action needed** — document as a positive baseline for future regression testing. + +### F2. Hook install: false "not inside a git repository" failure + +`charter bootstrap` hook-install step failed with: + +> Not inside a git repository + +...while `charter doctor` and `git rev-parse --show-toplevel` both succeeded in the +same shell session and working directory. + +**Likely cause:** The hook-install codepath resolves the git root differently from the +doctor/audit codepaths. Possible cwd drift if the install step spawns a subprocess +that changes directory, or a different git-detection helper that doesn't handle +WSL/Windows path translation. + +**Severity:** Medium — breaks bootstrap in WSL environments; manual `.githooks` copy +works around it. + +**Recommended fix:** +- Unify git-root resolution across all CLI commands (single `resolveGitRoot()` utility) +- Add integration test: run hook-install from a subdirectory of a git repo +- If the root cause is WSL path translation, normalize paths before calling `git rev-parse` + +### F3. `adf migrate` on `.cursorrules` fails with patch error + +Running `charter adf migrate` against a `.cursorrules` file failed with a patch error: + +> ADD_BULLET into text section + +The migrator attempted to apply a structured ADF patch operation (ADD_BULLET) to a +section that contained free-form text rather than a bullet list. + +**Severity:** Medium — `.cursorrules` is a common migration source; failure here blocks +the recommended onboarding path. + +**Recommended fix:** +- Detect section content type before applying bullet operations +- Fall back to `APPEND_TEXT` when the target section is free-form prose +- Emit a structured warning: `"section 'X' is prose, not a list — appended as text"` + +### F4. Doctor warns on `.cursorrules` even after thin-pointer conversion + +After converting `.cursorrules` to a thin pointer (referencing `.ai/` as the canonical +source), `charter doctor` continued to warn about the file. The warning only cleared +after deleting `.cursorrules` entirely. + +**Likely cause:** Doctor checks for file existence rather than file content. A thin +pointer is functionally equivalent to deletion (the file just says "see .ai/"), but +doctor doesn't parse the content to determine whether it's a thin pointer. + +**Severity:** Low — cosmetic; causes false positive warnings that erode trust in doctor +output over time. + +**Recommended fix:** +- Detect thin-pointer pattern in `.cursorrules` (e.g., file contains only a pointer + comment and no substantive rules) +- Suppress warning when the file is a recognized thin pointer +- Alternatively, document that `.cursorrules` should be deleted (not converted) post-migration + +### F5. Bootstrap install step fails under sandboxed EPERM + +When running inside a sandboxed environment (e.g., Claude Code sandbox, restricted +shell), the npm install step fails with `EPERM`. The error message is clear about +what happened but doesn't suggest recovery. + +**Severity:** Low — the error is honest, but agents and users lose time diagnosing it. + +**Recommended fix:** +Add a hint to the error output: +``` +Install failed (EPERM). Retry outside the sandbox: + npm install @stackbilt/charter --save-dev +Then re-run: charter bootstrap --skip install +``` + +### F6. `audit` hard error on repos with no HEAD + +Running `charter audit` on a freshly initialized repository (no commits yet) produces +a hard error because there is no HEAD to diff against. + +**Severity:** Low — edge case (fresh repos), but it's a poor first impression for users +who run `charter bootstrap && charter audit` immediately. + +**Recommended fix:** +- Detect the no-HEAD state with `git rev-parse HEAD` before attempting audit +- Return a structured response: + ```json + { + "status": "skipped", + "reason": "no-commits-yet", + "message": "No commits to audit. Run audit after your first commit." + } + ``` +- Exit 0 (not an error) + +### F7. `migrate` JSON output is very large; compact mode needed + +The JSON output from `charter adf migrate` is comprehensive but extremely large. For +agent consumption, most of the bulk is unnecessary — agents need the status, warnings, +and file-level summary, not the full patch payloads. + +**Severity:** Low — doesn't break anything, but wastes agent context window tokens and +makes log review harder. + +**Recommended fix:** +- Add `--compact` or `--summary` flag that emits: + ```json + { + "status": "success", + "filesProcessed": 3, + "warnings": ["..."], + "patchesApplied": 12 + } + ``` +- Keep the verbose output as default or behind `--verbose` for debugging +- Consider a `--format brief` option consistent with other CLI tools + +### F8. Encoding roughness in generated ADF text + +Some terminal contexts show emoji/mojibake artifacts in ADF-generated text. ADF section +headers use emoji prefixes (e.g., `🧠 CONTEXT:`, `📖 GUIDE:`) which render incorrectly +in terminals without full Unicode support. + +**Severity:** Low — cosmetic, but confusing in CI logs and minimal terminal emulators. + +**Recommended fix:** +- Add `--ascii` or `--no-emoji` flag for all output commands +- Map emoji section headers to ASCII equivalents: `[CONTEXT]:`, `[GUIDE]:`, etc. +- Detect terminal capabilities (`$TERM`, `$LANG`) and auto-select ASCII mode when + Unicode support is uncertain +- Alternatively, make the ADF spec allow both emoji and ASCII section headers + +## Priority Summary + +| ID | Finding | Severity | Effort | +|----|---------|----------|--------| +| F2 | Hook install git-detection bug | Medium | Small (unify git-root helper) | +| F3 | Migrate patch error on prose sections | Medium | Medium (content-type detection) | +| F4 | Doctor false positive on thin pointers | Low | Small (pointer detection) | +| F5 | Install EPERM missing retry hint | Low | Trivial (string change) | +| F6 | Audit hard error on no-HEAD | Low | Small (guard clause) | +| F7 | Migrate JSON too large for agents | Low | Medium (compact output mode) | +| F8 | Emoji encoding in minimal terminals | Low | Medium (ASCII output option) | + +## Relationship to Prior Feedback + +- **ADX-002** introduced the bootstrap path; F2 and F5 are friction in that path +- **ADX-003** covered install automation; F5 is the sandboxed variant of that issue +- **ADX-004** covered destructive overwrites; F2 shows another bootstrap failure mode +- F7 and F8 are new categories (output ergonomics) not covered in prior feedback From d417152d01cb197529c8d1a81606dad22efd86ad Mon Sep 17 00:00:00 2001 From: Kurt Overmier Date: Sat, 28 Feb 2026 17:47:29 -0600 Subject: [PATCH 2/6] fix: cross-platform parity for charter CLI (ADX-005 F2-F6), bump v0.5.0 Unify git invocation into shared git-helpers module with shell: true for WSL/CMD/PowerShell PATH resolution (F2). Deduplicate ~6 copies of runGit, parseCommitMetadata, parseChangedFilesByCommit across commands. Fix patcher ADD_BULLET on text sections by converting to list (F3). Fix migrate to detect text sections and use REPLACE_SECTION instead. Fix doctor false positive on .cursorrules thin pointers by adding the missing pointer marker to detection (F4). Extract shared POINTER_MARKERS constant to prevent future drift. Add EPERM/EACCES retry hint in bootstrap install step (F5). Add hasCommits() guard to audit command for no-HEAD repos (F6). Bump all packages 0.4.2 -> 0.5.0. Co-Authored-By: Claude Opus 4.6 --- packages/adf/package.json | 2 +- packages/adf/src/__tests__/patcher.test.ts | 32 ++++ packages/adf/src/patcher.ts | 9 +- packages/ci/package.json | 2 +- packages/classify/package.json | 2 +- packages/cli/package.json | 2 +- .../cli/src/__tests__/git-helpers.test.ts | 124 ++++++++++++++ packages/cli/src/commands/adf-migrate.ts | 16 +- packages/cli/src/commands/adf.ts | 9 ++ packages/cli/src/commands/audit.ts | 97 ++--------- packages/cli/src/commands/bootstrap.ts | 24 +-- packages/cli/src/commands/doctor.ts | 14 +- packages/cli/src/commands/hook.ts | 14 +- packages/cli/src/commands/validate.ts | 96 +---------- packages/cli/src/commands/why.ts | 80 +--------- packages/cli/src/git-helpers.ts | 151 ++++++++++++++++++ packages/core/package.json | 2 +- packages/drift/package.json | 2 +- packages/git/package.json | 2 +- packages/types/package.json | 2 +- packages/validate/package.json | 2 +- 21 files changed, 378 insertions(+), 306 deletions(-) create mode 100644 packages/cli/src/__tests__/git-helpers.test.ts create mode 100644 packages/cli/src/git-helpers.ts diff --git a/packages/adf/package.json b/packages/adf/package.json index 8c23121..786a8ee 100644 --- a/packages/adf/package.json +++ b/packages/adf/package.json @@ -1,7 +1,7 @@ { "name": "@stackbilt/adf", "sideEffects": false, - "version": "0.4.2", + "version": "0.5.0", "description": "ADF (Attention-Directed Format) — AST-backed context format for AI agents", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/adf/src/__tests__/patcher.test.ts b/packages/adf/src/__tests__/patcher.test.ts index 1412427..f766748 100644 --- a/packages/adf/src/__tests__/patcher.test.ts +++ b/packages/adf/src/__tests__/patcher.test.ts @@ -235,6 +235,38 @@ describe('applyPatches', () => { // --- Weight in ADD_SECTION --- + // --- ADD_BULLET on text section (F3 fix) --- + + it('ADD_BULLET: converts text section to list and appends', () => { + const result = applyPatches(makeDoc(), [ + { op: 'ADD_BULLET', section: 'TASK', value: 'New item' }, + ]); + const sec = result.sections.find(s => s.key === 'TASK')!; + expect(sec.content.type).toBe('list'); + if (sec.content.type === 'list') { + expect(sec.content.items).toEqual(['Build feature', 'New item']); + } + }); + + it('ADD_BULLET: converts empty text section to list', () => { + const doc: AdfDocument = { + version: '0.1', + sections: [ + { key: 'CONTEXT', decoration: null, content: { type: 'text', value: '' } }, + ], + }; + const result = applyPatches(doc, [ + { op: 'ADD_BULLET', section: 'CONTEXT', value: 'First item' }, + ]); + const sec = result.sections.find(s => s.key === 'CONTEXT')!; + expect(sec.content.type).toBe('list'); + if (sec.content.type === 'list') { + expect(sec.content.items).toEqual(['First item']); + } + }); + + // --- Weight in ADD_SECTION --- + it('ADD_SECTION: preserves weight annotation', () => { const result = applyPatches(makeDoc(), [ { diff --git a/packages/adf/src/patcher.ts b/packages/adf/src/patcher.ts index dbdce80..5574efa 100644 --- a/packages/adf/src/patcher.ts +++ b/packages/adf/src/patcher.ts @@ -5,7 +5,7 @@ * Throws AdfPatchError with context on any invalid operation. */ -import type { AdfDocument, AdfSection, PatchOperation } from './types'; +import type { AdfContent, AdfDocument, AdfSection, PatchOperation } from './types'; import { AdfPatchError } from './errors'; export function applyPatches(doc: AdfDocument, ops: PatchOperation[]): AdfDocument { @@ -62,9 +62,14 @@ function addBullet(doc: AdfDocument, sectionKey: string, value: string): AdfDocu } else { section.content.entries.push({ key: value.trim(), value: '' }); } + } else if (section.content.type === 'text') { + // Convert text section to list, preserving existing prose as first item + const existing = section.content.value.trim(); + const items = existing ? [existing, value] : [value]; + (section as { content: AdfContent }).content = { type: 'list', items }; } else { throw new AdfPatchError( - `Cannot ADD_BULLET to ${section.content.type} section "${sectionKey}". Section must be list or map.`, + `Cannot ADD_BULLET to ${section.content.type} section "${sectionKey}". Section must be list, map, or text.`, 'ADD_BULLET', sectionKey ); diff --git a/packages/ci/package.json b/packages/ci/package.json index e585015..a2f9dcf 100644 --- a/packages/ci/package.json +++ b/packages/ci/package.json @@ -1,7 +1,7 @@ { "name": "@stackbilt/ci", "sideEffects": false, - "version": "0.4.2", + "version": "0.5.0", "description": "GitHub Actions adapter for Charter governance checks", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/classify/package.json b/packages/classify/package.json index b1cccce..3000522 100644 --- a/packages/classify/package.json +++ b/packages/classify/package.json @@ -1,7 +1,7 @@ { "name": "@stackbilt/classify", "sideEffects": false, - "version": "0.4.2", + "version": "0.5.0", "description": "Heuristic change classification (SURFACE/LOCAL/CROSS_CUTTING)", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/cli/package.json b/packages/cli/package.json index 641a4a3..cef9346 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@stackbilt/cli", "sideEffects": false, - "version": "0.4.2", + "version": "0.5.0", "description": "Charter CLI — repo-level governance checks", "bin": { "charter": "./dist/bin.js" diff --git a/packages/cli/src/__tests__/git-helpers.test.ts b/packages/cli/src/__tests__/git-helpers.test.ts new file mode 100644 index 0000000..478cc9b --- /dev/null +++ b/packages/cli/src/__tests__/git-helpers.test.ts @@ -0,0 +1,124 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { execFileSync } from 'node:child_process'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { + runGit, + isGitRepo, + hasCommits, + getGitErrorMessage, + parseCommitMetadata, + parseChangedFilesByCommit, + getRecentCommitRange, +} from '../git-helpers'; + +describe('git-helpers', () => { + let originalCwd: string; + let tempDir: string; + + beforeEach(() => { + originalCwd = process.cwd(); + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'charter-git-helpers-')); + process.chdir(tempDir); + }); + + afterEach(() => { + process.chdir(originalCwd); + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + describe('runGit', () => { + it('succeeds inside a git repo', () => { + execFileSync('git', ['init'], { stdio: 'ignore' }); + const result = runGit(['rev-parse', '--is-inside-work-tree']).trim(); + expect(result).toBe('true'); + }); + + it('throws outside a git repo', () => { + expect(() => runGit(['rev-parse', '--is-inside-work-tree'])).toThrow(); + }); + }); + + describe('isGitRepo', () => { + it('returns true inside a git repo', () => { + execFileSync('git', ['init'], { stdio: 'ignore' }); + expect(isGitRepo()).toBe(true); + }); + + it('returns false outside a git repo', () => { + expect(isGitRepo()).toBe(false); + }); + }); + + describe('hasCommits', () => { + it('returns false on empty repo', () => { + execFileSync('git', ['init'], { stdio: 'ignore' }); + expect(hasCommits()).toBe(false); + }); + + it('returns true after a commit', () => { + execFileSync('git', ['init'], { stdio: 'ignore' }); + execFileSync('git', ['config', 'user.email', 'test@test.com'], { stdio: 'ignore' }); + execFileSync('git', ['config', 'user.name', 'Test'], { stdio: 'ignore' }); + fs.writeFileSync(path.join(tempDir, 'file.txt'), 'hello'); + execFileSync('git', ['add', '.'], { stdio: 'ignore' }); + execFileSync('git', ['commit', '-m', 'init'], { stdio: 'ignore' }); + expect(hasCommits()).toBe(true); + }); + }); + + describe('getGitErrorMessage', () => { + it('extracts stderr from exec error', () => { + const err = Object.assign(new Error('fail'), { stderr: 'fatal: not a repo' }); + expect(getGitErrorMessage(err)).toBe('fatal: not a repo'); + }); + + it('falls back to message', () => { + expect(getGitErrorMessage(new Error('some error'))).toBe('some error'); + }); + + it('returns fallback for non-Error', () => { + expect(getGitErrorMessage('string')).toBe('Unknown git error.'); + }); + }); + + describe('parseCommitMetadata', () => { + it('parses git log format', () => { + const log = 'abc123\x1fAlice\x1f2026-01-01T00:00:00Z\x1fInitial commit\x1e'; + const result = parseCommitMetadata(log); + expect(result).toHaveLength(1); + expect(result[0].sha).toBe('abc123'); + expect(result[0].author).toBe('Alice'); + expect(result[0].message).toBe('Initial commit'); + }); + + it('handles multiple commits', () => { + const log = 'aaa\x1fA\x1f2026-01-01\x1fFirst\x1ebbb\x1fB\x1f2026-01-02\x1fSecond\x1e'; + expect(parseCommitMetadata(log)).toHaveLength(2); + }); + }); + + describe('parseChangedFilesByCommit', () => { + it('parses name-only log', () => { + const log = [ + 'a'.repeat(40), + 'src/index.ts', + 'src/util.ts', + '', + 'b'.repeat(40), + 'README.md', + ].join('\n'); + const result = parseChangedFilesByCommit(log); + expect(result.get('a'.repeat(40))).toEqual(['src/index.ts', 'src/util.ts']); + expect(result.get('b'.repeat(40))).toEqual(['README.md']); + }); + }); + + describe('getRecentCommitRange', () => { + it('returns HEAD on empty repo', () => { + execFileSync('git', ['init'], { stdio: 'ignore' }); + expect(getRecentCommitRange()).toBe('HEAD'); + }); + }); +}); diff --git a/packages/cli/src/commands/adf-migrate.ts b/packages/cli/src/commands/adf-migrate.ts index 338df2f..060e87d 100644 --- a/packages/cli/src/commands/adf-migrate.ts +++ b/packages/cli/src/commands/adf-migrate.ts @@ -21,7 +21,7 @@ import type { AdfDocument, PatchOperation, MigrationItem } from '@stackbilt/adf' import type { CLIOptions } from '../index'; import { CLIError, EXIT_CODE } from '../index'; import { getFlag } from '../flags'; -import { POINTER_CLAUDE_MD, POINTER_CURSORRULES, POINTER_AGENTS_MD } from './adf'; +import { POINTER_CLAUDE_MD, POINTER_CURSORRULES, POINTER_AGENTS_MD, POINTER_MARKERS } from './adf'; // ============================================================================ // Constants @@ -35,7 +35,6 @@ const AGENT_CONFIG_FILES = [ 'copilot-instructions.md', ]; -const THIN_POINTER_MARKER = 'Do not duplicate ADF rules here'; const POINTER_TEMPLATES: Record = { 'CLAUDE.md': POINTER_CLAUDE_MD, @@ -144,7 +143,7 @@ function migrateSource( const content = fs.readFileSync(fullPath, 'utf-8'); // Skip if already a thin pointer - if (content.includes(THIN_POINTER_MARKER)) { + if (POINTER_MARKERS.some(marker => content.includes(marker))) { return { source: sourcePath, skipped: true, @@ -291,8 +290,17 @@ function applyMigrationToModule( content: { type: 'list', items: listItems }, weight, }); + } else if (existingSection.content.type === 'text') { + // Text sections can't receive ADD_BULLET — convert to list via REPLACE_SECTION + const existingText = existingSection.content.value.trim(); + const newItems = items.map(i => formatItemForAdf(i)); + ops.push({ + op: 'REPLACE_SECTION', + key: sectionKey, + content: { type: 'list', items: existingText ? [existingText, ...newItems] : newItems }, + }); } else { - // Add individual bullets to existing section + // Add individual bullets to existing list/map section for (const item of items) { const formatted = formatItemForAdf(item); diff --git a/packages/cli/src/commands/adf.ts b/packages/cli/src/commands/adf.ts index 8a4d51d..9b77aec 100644 --- a/packages/cli/src/commands/adf.ts +++ b/packages/cli/src/commands/adf.ts @@ -129,6 +129,15 @@ interface AdfInitResult { pointers?: string[]; } +// -- Thin pointer detection markers -- + +/** Strings that identify an agent config file as a thin pointer to .ai/. */ +export const POINTER_MARKERS = [ + 'Do not duplicate ADF rules here', + 'Do not duplicate rules from .ai/', + 'Do not add stack rules here', +]; + // -- Thin pointer file content -- export const POINTER_CLAUDE_MD = `# Project Context diff --git a/packages/cli/src/commands/audit.ts b/packages/cli/src/commands/audit.ts index d2f779e..3363235 100644 --- a/packages/cli/src/commands/audit.ts +++ b/packages/cli/src/commands/audit.ts @@ -5,7 +5,6 @@ * Summarizes governance coverage, pattern adoption, and policy compliance. */ -import { execFileSync } from 'node:child_process'; import * as fs from 'node:fs'; import * as path from 'node:path'; import type { CLIOptions } from '../index'; @@ -14,6 +13,7 @@ import { loadConfig, loadPatterns, getPatternCustomizationStatus, type CharterCo import { parseAllTrailers } from '@stackbilt/git'; import { assessCommitRisk } from '@stackbilt/git'; import type { GitCommit } from '@stackbilt/types'; +import { runGit, hasCommits, getGitErrorMessage, parseCommitMetadata, parseChangedFilesByCommit, getRecentCommitRange } from '../git-helpers'; interface AuditReport { project: string; @@ -70,6 +70,16 @@ interface CommitLoadResult { export async function auditCommand(options: CLIOptions, args: string[] = []): Promise { const config = loadConfig(options.configPath); const patterns = loadPatterns(options.configPath); + + if (!hasCommits()) { + if (options.format === 'json') { + console.log(JSON.stringify({ status: 'PASS', summary: 'No commits to audit.' }, null, 2)); + } else { + console.log(' No commits to audit.'); + } + return EXIT_CODE.SUCCESS; + } + const range = getCommitRange(args); const commitLoad = getCommits(range); @@ -262,51 +272,6 @@ function getCommits(range: string): CommitLoadResult { } } -function parseCommitMetadata(logOutput: string): Array> { - const commits: Array> = []; - - for (const rawRecord of logOutput.split('\x1e')) { - const record = rawRecord.trim(); - if (!record) continue; - - const [sha = '', author = '', timestamp = '', ...messageParts] = record.split('\x1f'); - if (!sha) continue; - - commits.push({ - sha: sha.trim(), - author: author.trim(), - timestamp: timestamp.trim(), - message: messageParts.join('\x1f').replace(/\r\n/g, '\n').replace(/\n+$/, ''), - }); - } - - return commits; -} - -function parseChangedFilesByCommit(logOutput: string): Map { - const filesBySha = new Map(); - let currentSha = ''; - - for (const rawLine of logOutput.split(/\r?\n/)) { - const line = rawLine.trim(); - if (!line) continue; - - if (/^[a-f0-9]{40}$/i.test(line)) { - currentSha = line; - if (!filesBySha.has(currentSha)) { - filesBySha.set(currentSha, []); - } - continue; - } - - if (!currentSha) continue; - const files = filesBySha.get(currentSha); - if (!files || files.includes(line)) continue; - files.push(line); - } - - return filesBySha; -} function getCommitRange(args: string[]): string { const rangeIdx = args.indexOf('--range'); @@ -335,46 +300,6 @@ function getCommitRange(args: string[]): string { } } -function getRecentCommitRange(): string { - try { - const count = Number.parseInt(runGit(['rev-list', '--count', 'HEAD']).trim(), 10); - if (!Number.isFinite(count) || count <= 1) { - return 'HEAD'; - } - const span = Math.min(5, count - 1); - return `HEAD~${span}..HEAD`; - } catch { - return 'HEAD'; - } -} - -function runGit(args: string[]): string { - return execFileSync('git', args, { - encoding: 'utf-8', - maxBuffer: 10 * 1024 * 1024, - stdio: ['ignore', 'pipe', 'pipe'], - }); -} - -function getGitErrorMessage(error: unknown): string { - const fallback = 'Unknown git error.'; - if (!(error instanceof Error)) return fallback; - const execError = error as Error & { stderr?: Buffer | string }; - - if (execError.stderr) { - const stderr = execError.stderr.toString().trim(); - if (stderr.length > 0) { - return stderr; - } - } - - if (execError.message) { - return execError.message.trim(); - } - - return fallback; -} - function getRecommendations(inputs: { coveragePercent: number; activePatterns: number; diff --git a/packages/cli/src/commands/bootstrap.ts b/packages/cli/src/commands/bootstrap.ts index ae58aa2..107f0b0 100644 --- a/packages/cli/src/commands/bootstrap.ts +++ b/packages/cli/src/commands/bootstrap.ts @@ -12,6 +12,8 @@ import { execSync } from 'node:child_process'; import type { CLIOptions } from '../index'; import { CLIError, EXIT_CODE } from '../index'; import { getFlag } from '../flags'; +import { isGitRepo } from '../git-helpers'; +import { POINTER_MARKERS } from './adf'; import { initializeCharter, type StackPreset } from './init'; import { detectStack, @@ -158,7 +160,12 @@ export async function bootstrapCommand(options: CLIOptions, args: string[]): Pro console.log(' Done'); } else { console.log(` Failed: ${installResult.step.details.error}`); - console.log(' (non-fatal: run install manually)'); + for (const w of installResult.step.warnings) { + if (w.startsWith('Hint:') || w.startsWith('Retry')) { + console.log(` ${w}`); + } + } + console.log(' (non-fatal)'); } } console.log(''); @@ -539,7 +546,12 @@ function runInstallPhase( }; } catch (err: unknown) { const msg = err instanceof Error ? err.message : String(err); + const isPermError = msg.includes('EPERM') || msg.includes('EACCES') || msg.includes('permission denied'); warnings.push(`Install failed: ${msg}`); + if (isPermError) { + warnings.push(`Hint: permission error detected. Retry outside the sandbox or with elevated privileges: ${command}`); + } + warnings.push(`Retry manually: ${command}`); return { step: { name: 'install', @@ -583,13 +595,7 @@ function runDoctorPhase( try { // Git repository check - let inGitRepo = false; - try { - execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); - inGitRepo = true; - } catch { - // not in a git repo - } + const inGitRepo = isGitRepo(); checks.push({ name: 'Git repository', status: inGitRepo ? 'PASS' : 'WARN', @@ -713,7 +719,7 @@ function hasCustomAdfContent(aiDir: string): boolean { function isAlreadyThinPointer(filePath: string): boolean { try { const content = fs.readFileSync(filePath, 'utf-8'); - return content.includes('Do not duplicate ADF rules here'); + return POINTER_MARKERS.some(marker => content.includes(marker)); } catch { return false; } diff --git a/packages/cli/src/commands/doctor.ts b/packages/cli/src/commands/doctor.ts index 6b555b7..bb7b731 100644 --- a/packages/cli/src/commands/doctor.ts +++ b/packages/cli/src/commands/doctor.ts @@ -4,13 +4,14 @@ * Prints environment and configuration diagnostics for humans and agents. */ -import { execSync } from 'node:child_process'; import * as fs from 'node:fs'; import * as path from 'node:path'; import type { CLIOptions } from '../index'; import { EXIT_CODE } from '../index'; import { loadPatterns } from '../config'; import { parseAdf, parseManifest } from '@stackbilt/adf'; +import { isGitRepo } from '../git-helpers'; +import { POINTER_MARKERS } from './adf'; interface DoctorResult { status: 'PASS' | 'WARN'; @@ -148,7 +149,6 @@ export async function doctorCommand(options: CLIOptions, args: string[] = []): P // Agent config pointer check: flag files with stack rules that should be in .ai/ const AGENT_CONFIG_FILES = ['CLAUDE.md', '.cursorrules', 'agents.md', 'AGENTS.md', 'GEMINI.md', 'copilot-instructions.md']; - const POINTER_MARKERS = ['Do not duplicate ADF rules here', 'Do not duplicate rules from .ai/']; const nonPointerFiles: string[] = []; for (const file of AGENT_CONFIG_FILES) { if (fs.existsSync(file)) { @@ -220,16 +220,6 @@ export async function doctorCommand(options: CLIOptions, args: string[] = []): P return EXIT_CODE.SUCCESS; } -function isGitRepo(): boolean { - try { - execSync('git rev-parse --is-inside-work-tree', { - stdio: 'ignore', - }); - return true; - } catch { - return false; - } -} function validateJSONConfig(configFile: string): DoctorResult['checks'][number] { try { diff --git a/packages/cli/src/commands/hook.ts b/packages/cli/src/commands/hook.ts index 12ddd1c..1e1fe0d 100644 --- a/packages/cli/src/commands/hook.ts +++ b/packages/cli/src/commands/hook.ts @@ -4,11 +4,11 @@ * Installs git hooks for commit-time governance ergonomics. */ -import { execFileSync } from 'node:child_process'; import * as fs from 'node:fs'; import * as path from 'node:path'; import type { CLIOptions } from '../index'; import { CLIError, EXIT_CODE } from '../index'; +import { runGit, isGitRepo } from '../git-helpers'; interface HookInstallResult { status: 'INSTALLED' | 'SKIPPED'; @@ -186,21 +186,11 @@ function getGitConfig(key: string): string { } function ensureGitRepo(): void { - try { - runGit(['rev-parse', '--is-inside-work-tree']); - } catch { + if (!isGitRepo()) { throw new CLIError('Not inside a git repository.'); } } -function runGit(args: string[]): string { - return execFileSync('git', args, { - encoding: 'utf-8', - stdio: ['ignore', 'pipe', 'pipe'], - maxBuffer: 10 * 1024 * 1024, - }); -} - function setExecutableBit(targetPath: string): void { try { fs.chmodSync(targetPath, 0o755); diff --git a/packages/cli/src/commands/validate.ts b/packages/cli/src/commands/validate.ts index 3c67b1c..e5b9580 100644 --- a/packages/cli/src/commands/validate.ts +++ b/packages/cli/src/commands/validate.ts @@ -5,13 +5,13 @@ * Checks that high-risk commits reference ADRs or governance requests. */ -import { execFileSync } from 'node:child_process'; import type { CLIOptions } from '../index'; import { EXIT_CODE } from '../index'; import type { GitCommit } from '@stackbilt/types'; import { loadConfig } from '../config'; import { parseAllTrailers } from '@stackbilt/git'; import { assessCommitRisk, generateSuggestions } from '@stackbilt/git'; +import { runGit, hasCommits, getGitErrorMessage, parseCommitMetadata, parseChangedFilesByCommit, getRecentCommitRange } from '../git-helpers'; interface LocalValidationResult { status: 'PASS' | 'WARN' | 'FAIL'; @@ -510,97 +510,3 @@ function detectTrailerParsingWarnings( return warnings; } -function parseCommitMetadata(logOutput: string): Array> { - const commits: Array> = []; - - for (const rawRecord of logOutput.split('\x1e')) { - const record = rawRecord.trim(); - if (!record) continue; - - const [sha = '', author = '', timestamp = '', ...messageParts] = record.split('\x1f'); - if (!sha) continue; - - commits.push({ - sha: sha.trim(), - author: author.trim(), - timestamp: timestamp.trim(), - message: messageParts.join('\x1f').replace(/\r\n/g, '\n').replace(/\n+$/, ''), - }); - } - - return commits; -} - -function parseChangedFilesByCommit(logOutput: string): Map { - const filesBySha = new Map(); - let currentSha = ''; - - for (const rawLine of logOutput.split(/\r?\n/)) { - const line = rawLine.trim(); - if (!line) continue; - - if (/^[a-f0-9]{40}$/i.test(line)) { - currentSha = line; - if (!filesBySha.has(currentSha)) { - filesBySha.set(currentSha, []); - } - continue; - } - - if (!currentSha) continue; - const files = filesBySha.get(currentSha); - if (!files || files.includes(line)) continue; - files.push(line); - } - - return filesBySha; -} - -function hasCommits(): boolean { - try { - runGit(['rev-parse', '--verify', 'HEAD']); - return true; - } catch { - return false; - } -} - -function getRecentCommitRange(): string { - try { - const count = Number.parseInt(runGit(['rev-list', '--count', 'HEAD']).trim(), 10); - if (!Number.isFinite(count) || count <= 1) { - return 'HEAD'; - } - const span = Math.min(5, count - 1); - return `HEAD~${span}..HEAD`; - } catch { - return 'HEAD'; - } -} - -function getGitErrorMessage(error: unknown): string { - const fallback = 'Unknown git error.'; - if (!(error instanceof Error)) return fallback; - const execError = error as Error & { stderr?: Buffer | string }; - - if (execError.stderr) { - const stderr = execError.stderr.toString().trim(); - if (stderr.length > 0) { - return stderr; - } - } - - if (execError.message) { - return execError.message.trim(); - } - - return fallback; -} - -function runGit(args: string[]): string { - return execFileSync('git', args, { - encoding: 'utf-8', - maxBuffer: 10 * 1024 * 1024, - stdio: ['ignore', 'pipe', 'pipe'], - }); -} diff --git a/packages/cli/src/commands/why.ts b/packages/cli/src/commands/why.ts index c45f40e..c198da5 100644 --- a/packages/cli/src/commands/why.ts +++ b/packages/cli/src/commands/why.ts @@ -1,10 +1,10 @@ -import { execFileSync } from 'node:child_process'; import * as fs from 'node:fs'; import * as path from 'node:path'; import type { CLIOptions } from '../index'; import { EXIT_CODE } from '../index'; import { parseAllTrailers, assessCommitRisk } from '@stackbilt/git'; import type { GitCommit } from '@stackbilt/types'; +import { runGit, isGitRepo, hasCommits, parseCommitMetadata, parseChangedFilesByCommit } from '../git-helpers'; interface SnapshotResult { inGitRepo: boolean; @@ -132,27 +132,10 @@ function getSnapshot(configPath: string): SnapshotResult { }; } -function isGitRepo(): boolean { - try { - execFileSync('git', ['rev-parse', '--is-inside-work-tree'], { stdio: 'ignore' }); - return true; - } catch { - return false; - } -} - function getRecentCommits(count: number): GitCommit[] { try { - const metadataLog = execFileSync('git', ['log', `-${count}`, '--format=%H%x1f%an%x1f%aI%x1f%B%x1e'], { - encoding: 'utf-8', - maxBuffer: 10 * 1024 * 1024, - stdio: ['ignore', 'pipe', 'ignore'], - }); - const filesLog = execFileSync('git', ['log', `-${count}`, '--name-only', '--format=%H'], { - encoding: 'utf-8', - maxBuffer: 10 * 1024 * 1024, - stdio: ['ignore', 'pipe', 'ignore'], - }); + const metadataLog = runGit(['log', `-${count}`, '--format=%H%x1f%an%x1f%aI%x1f%B%x1e']); + const filesLog = runGit(['log', `-${count}`, '--name-only', '--format=%H']); const filesBySha = parseChangedFilesByCommit(filesLog); return parseCommitMetadata(metadataLog).map((commit) => ({ @@ -163,60 +146,3 @@ function getRecentCommits(count: number): GitCommit[] { return []; } } - -function parseCommitMetadata(logOutput: string): Array> { - const commits: Array> = []; - - for (const rawRecord of logOutput.split('\x1e')) { - const record = rawRecord.trim(); - if (!record) continue; - - const [sha = '', author = '', timestamp = '', ...messageParts] = record.split('\x1f'); - if (!sha) continue; - - commits.push({ - sha: sha.trim(), - author: author.trim(), - timestamp: timestamp.trim(), - message: messageParts.join('\x1f').replace(/\r\n/g, '\n').replace(/\n+$/, ''), - }); - } - - return commits; -} - -function parseChangedFilesByCommit(logOutput: string): Map { - const filesBySha = new Map(); - let currentSha = ''; - - for (const rawLine of logOutput.split(/\r?\n/)) { - const line = rawLine.trim(); - if (!line) continue; - - if (/^[a-f0-9]{40}$/i.test(line)) { - currentSha = line; - if (!filesBySha.has(currentSha)) { - filesBySha.set(currentSha, []); - } - continue; - } - - if (!currentSha) continue; - const files = filesBySha.get(currentSha); - if (!files || files.includes(line)) continue; - files.push(line); - } - - return filesBySha; -} - -function hasCommits(): boolean { - try { - execFileSync('git', ['rev-parse', '--verify', 'HEAD'], { - stdio: 'ignore', - }); - return true; - } catch { - return false; - } -} diff --git a/packages/cli/src/git-helpers.ts b/packages/cli/src/git-helpers.ts new file mode 100644 index 0000000..54b059d --- /dev/null +++ b/packages/cli/src/git-helpers.ts @@ -0,0 +1,151 @@ +/** + * Shared git invocation helpers. + * + * Centralizes all child-process git calls behind a single `runGit()` that + * uses `shell: true` for cross-platform PATH resolution (fixes WSL, CMD, + * PowerShell parity — see ADX-005 F2). + */ + +import { execFileSync } from 'node:child_process'; +import type { GitCommit } from '@stackbilt/types'; + +// --------------------------------------------------------------------------- +// Core git invocation +// --------------------------------------------------------------------------- + +/** + * Run a git command and return its stdout. + * + * Uses `shell: true` so that the OS shell resolves the `git` binary via + * PATH. This is the key cross-platform fix: `execFileSync` *without* a + * shell can fail on WSL/Windows when git lives in a PATH entry the Node + * process doesn't see directly. + */ +export function runGit(args: string[]): string { + return execFileSync('git', args, { + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'pipe'], + maxBuffer: 10 * 1024 * 1024, + shell: true, + }); +} + +/** Returns `true` when the current working directory is inside a git work tree. */ +export function isGitRepo(): boolean { + try { + runGit(['rev-parse', '--is-inside-work-tree']); + return true; + } catch { + return false; + } +} + +/** Returns `true` when the repository has at least one commit (HEAD exists). */ +export function hasCommits(): boolean { + try { + runGit(['rev-parse', '--verify', 'HEAD']); + return true; + } catch { + return false; + } +} + +// --------------------------------------------------------------------------- +// Error formatting +// --------------------------------------------------------------------------- + +/** Extract a human-readable message from a git child-process error. */ +export function getGitErrorMessage(error: unknown): string { + const fallback = 'Unknown git error.'; + if (!(error instanceof Error)) return fallback; + const execError = error as Error & { stderr?: Buffer | string }; + + if (execError.stderr) { + const stderr = execError.stderr.toString().trim(); + if (stderr.length > 0) { + return stderr; + } + } + + if (execError.message) { + return execError.message.trim(); + } + + return fallback; +} + +// --------------------------------------------------------------------------- +// Commit log parsing (shared by audit, validate, why) +// --------------------------------------------------------------------------- + +/** + * Parse `git log --format=%H%x1f%an%x1f%aI%x1f%B%x1e` output into commit + * metadata (without file lists). + */ +export function parseCommitMetadata(logOutput: string): Array> { + const commits: Array> = []; + + for (const rawRecord of logOutput.split('\x1e')) { + const record = rawRecord.trim(); + if (!record) continue; + + const [sha = '', author = '', timestamp = '', ...messageParts] = record.split('\x1f'); + if (!sha) continue; + + commits.push({ + sha: sha.trim(), + author: author.trim(), + timestamp: timestamp.trim(), + message: messageParts.join('\x1f').replace(/\r\n/g, '\n').replace(/\n+$/, ''), + }); + } + + return commits; +} + +/** + * Parse `git log --name-only --format=%H` output into a Map of SHA → changed + * file paths. + */ +export function parseChangedFilesByCommit(logOutput: string): Map { + const filesBySha = new Map(); + let currentSha = ''; + + for (const rawLine of logOutput.split(/\r?\n/)) { + const line = rawLine.trim(); + if (!line) continue; + + if (/^[a-f0-9]{40}$/i.test(line)) { + currentSha = line; + if (!filesBySha.has(currentSha)) { + filesBySha.set(currentSha, []); + } + continue; + } + + if (!currentSha) continue; + const files = filesBySha.get(currentSha); + if (!files || files.includes(line)) continue; + files.push(line); + } + + return filesBySha; +} + +// --------------------------------------------------------------------------- +// Commit range helpers (shared by audit, validate) +// --------------------------------------------------------------------------- + +/** Return a short recent-commits range like `HEAD~5..HEAD`. */ +export function getRecentCommitRange(): string { + try { + const count = Number.parseInt(runGit(['rev-list', '--count', 'HEAD']).trim(), 10); + if (!Number.isFinite(count) || count <= 1) { + return 'HEAD'; + } + const span = Math.min(5, count - 1); + return `HEAD~${span}..HEAD`; + } catch { + return 'HEAD'; + } +} diff --git a/packages/core/package.json b/packages/core/package.json index 42c69ae..4ea3b63 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@stackbilt/core", "sideEffects": false, - "version": "0.4.2", + "version": "0.5.0", "description": "Core schemas, sanitization, and error handling for Charter Kit", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/drift/package.json b/packages/drift/package.json index 80fe707..978e3f1 100644 --- a/packages/drift/package.json +++ b/packages/drift/package.json @@ -1,7 +1,7 @@ { "name": "@stackbilt/drift", "sideEffects": false, - "version": "0.4.2", + "version": "0.5.0", "description": "Drift scanner — detects codebase divergence from governance patterns", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/git/package.json b/packages/git/package.json index 2d0a561..dedfb97 100644 --- a/packages/git/package.json +++ b/packages/git/package.json @@ -1,7 +1,7 @@ { "name": "@stackbilt/git", "sideEffects": false, - "version": "0.4.2", + "version": "0.5.0", "description": "Git trailer parsing, commit risk scoring, and PR validation", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/types/package.json b/packages/types/package.json index 5389af4..d3499f1 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,7 +1,7 @@ { "name": "@stackbilt/types", "sideEffects": false, - "version": "0.4.2", + "version": "0.5.0", "description": "Shared type definitions for the Charter Kit", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/validate/package.json b/packages/validate/package.json index 6f7b24a..49126c9 100644 --- a/packages/validate/package.json +++ b/packages/validate/package.json @@ -1,7 +1,7 @@ { "name": "@stackbilt/validate", "sideEffects": false, - "version": "0.4.2", + "version": "0.5.0", "description": "Citation validation, message classification, and governance checks", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 836732ef46bc0982cba8c8774ab141bad3efb108 Mon Sep 17 00:00:00 2001 From: Kurt Overmier Date: Sat, 28 Feb 2026 17:52:45 -0600 Subject: [PATCH 3/6] docs: update README with v0.5.0 cross-platform note, bootstrap command, fresh evidence Refresh the dogfood evidence snapshot to current v0.5.0 measurements. Add bootstrap command to Getting Started and Command Reference. Add Cross-Platform Support section noting WSL/PowerShell/CMD/Linux parity. Co-Authored-By: Claude Opus 4.6 --- README.md | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index a538c99..0e97102 100644 --- a/README.md +++ b/README.md @@ -97,18 +97,18 @@ Here is the actual output from Charter's dogfood run: ADF Evidence Report =================== Modules loaded: core.adf, state.adf - Token estimate: ~371 - Token budget: 4000 (9%) + Token estimate: ~494 + Token budget: 4000 (12%) Auto-measured: - adf_commands_loc: 413 lines (packages/cli/src/commands/adf.ts) - adf_bundle_loc: 154 lines (packages/cli/src/commands/adf-bundle.ts) - adf_sync_loc: 204 lines (packages/cli/src/commands/adf-sync.ts) - adf_evidence_loc: 263 lines (packages/cli/src/commands/adf-evidence.ts) - adf_migrate_loc: 453 lines (packages/cli/src/commands/adf-migrate.ts) - bundler_loc: 415 lines (packages/adf/src/bundler.ts) + adf_commands_loc: 577 lines (packages/cli/src/commands/adf.ts) + adf_bundle_loc: 175 lines (packages/cli/src/commands/adf-bundle.ts) + adf_sync_loc: 213 lines (packages/cli/src/commands/adf-sync.ts) + adf_evidence_loc: 312 lines (packages/cli/src/commands/adf-evidence.ts) + adf_migrate_loc: 455 lines (packages/cli/src/commands/adf-migrate.ts) + bundler_loc: 413 lines (packages/adf/src/bundler.ts) parser_loc: 214 lines (packages/adf/src/parser.ts) - cli_entry_loc: 149 lines (packages/cli/src/index.ts) + cli_entry_loc: 191 lines (packages/cli/src/index.ts) Section weights: Load-bearing: 2 @@ -116,14 +116,14 @@ Here is the actual output from Charter's dogfood run: Unweighted: 3 Constraints: - [ok] adf_commands_loc: 413 / 500 [lines] -- PASS - [ok] adf_bundle_loc: 154 / 200 [lines] -- PASS - [ok] adf_sync_loc: 204 / 250 [lines] -- PASS - [ok] adf_evidence_loc: 263 / 300 [lines] -- PASS - [ok] adf_migrate_loc: 453 / 500 [lines] -- PASS - [ok] bundler_loc: 415 / 500 [lines] -- PASS + [ok] adf_commands_loc: 577 / 650 [lines] -- PASS + [ok] adf_bundle_loc: 175 / 200 [lines] -- PASS + [ok] adf_sync_loc: 213 / 250 [lines] -- PASS + [ok] adf_evidence_loc: 312 / 380 [lines] -- PASS + [ok] adf_migrate_loc: 455 / 500 [lines] -- PASS + [ok] bundler_loc: 413 / 500 [lines] -- PASS [ok] parser_loc: 214 / 300 [lines] -- PASS - [ok] cli_entry_loc: 149 / 200 [lines] -- PASS + [ok] cli_entry_loc: 191 / 200 [lines] -- PASS Sync: all sources in sync @@ -133,7 +133,7 @@ Here is the actual output from Charter's dogfood run: What this shows: - **Metric ceilings enforce LOC limits on source files.** Each key in the `METRICS` section of an `.adf` module declares a ceiling. The `--auto-measure` flag counts lines live from the source files referenced in the manifest. -- **Self-correcting architecture.** When `adf_commands_loc` hit 93% of its 900-line ceiling in v0.3.4, Charter's own evidence gate caught it. The file was split into four focused modules (`adf.ts`, `adf-bundle.ts`, `adf-sync.ts`, `adf-evidence.ts`), each with its own ceiling. The pre-commit hook now prevents this from happening silently again. +- **Self-correcting architecture.** When `adf_commands_loc` approached its ceiling in v0.3.4, Charter's own evidence gate caught it. The file was split into focused modules (`adf.ts`, `adf-bundle.ts`, `adf-sync.ts`, `adf-evidence.ts`, `adf-migrate.ts`), each with its own ceiling. The pre-commit hook now prevents this from happening silently again. - **CI gating.** Generated governance workflows run `charter doctor --adf-only --ci` and `charter adf evidence --auto-measure --ci` when `.ai/manifest.adf` is present, blocking merges on ADF wiring violations or ceiling breaches. - **Pre-commit enforcement.** `charter hook install --pre-commit` installs a git hook that enforces `doctor --adf-only` + ADF evidence checks (or `pnpm run verify:adf` when present). When an agent runs unattended, wiring/ceiling violations block the commit. - **Available to any repo.** This is the same system you get by running `charter adf init` in your own project. @@ -163,7 +163,8 @@ For pnpm workspaces use `pnpm add -Dw @stackbilt/cli`. For a global install use ```bash charter # Repo risk/value snapshot -charter setup --ci github # Apply governance baseline +charter bootstrap --ci github # One-command onboarding (detect + setup + ADF + install + doctor) +charter setup --ci github # Apply governance baseline (or use bootstrap) charter doctor # Validate environment/config charter validate # Check commit governance charter drift # Scan for stack drift @@ -205,9 +206,14 @@ Teams often score lower early due to missing governance trailers. Use this ramp: +## Cross-Platform Support + +Charter v0.5.0 works across WSL, PowerShell, CMD, macOS, and Linux. All git operations use a unified invocation layer with cross-platform PATH resolution. Line endings are normalized via `.gitattributes` (LF for source, CRLF for `.bat`/`.cmd`/`.ps1`). + ## Command Reference - `charter`: show repo risk/value snapshot and recommended next action +- `charter bootstrap [--ci github] [--preset ] [--yes] [--skip-install] [--skip-doctor]`: one-command onboarding (detect + setup + ADF + install + doctor) - `charter setup [--ci github] [--preset ] [--detect-only] [--no-dependency-sync]`: detect stack and scaffold `.charter/` baseline - `charter init [--preset ]`: scaffold `.charter/` templates only - `charter doctor [--adf-only]`: validate environment/config state (`--adf-only` runs strict ADF wiring checks) From e0218f1bcd0f1fd7a56f72b78ec87dde66a33c84 Mon Sep 17 00:00:00 2001 From: Kurt Overmier Date: Sun, 1 Mar 2026 06:43:56 -0600 Subject: [PATCH 4/6] docs(repo): update project documentation --- CONTRIBUTING.md | 8 +++ README.md | 31 +++++++-- docs-snippets/charter-oss-ecosystem.md | 2 + papers/AGENT_DX_FEEDBACK_001.md | 4 ++ papers/AGENT_DX_FEEDBACK_002.md | 4 ++ papers/AGENT_DX_FEEDBACK_003.md | 4 ++ papers/AGENT_DX_FEEDBACK_004.md | 4 ++ papers/AGENT_DX_FEEDBACK_005.md | 4 ++ papers/README.md | 66 +++++++++++++------ papers/releases/README.md | 15 +++++ papers/releases/template.release-plan.md | 44 +++++++++++++ papers/releases/v0.6.0-plan.md | 50 ++++++++++++++ papers/templates/template.feedback.md | 39 +++++++++++ papers/ux-feedback/README.md | 28 ++++++++ papers/ux-feedback/buckets/automation-ci.md | 7 ++ papers/ux-feedback/buckets/daily-use.md | 7 ++ papers/ux-feedback/buckets/onboarding.md | 7 ++ .../ux-feedback/buckets/output-ergonomics.md | 7 ++ .../ux-feedback/buckets/reliability-trust.md | 7 ++ 19 files changed, 315 insertions(+), 23 deletions(-) create mode 100644 papers/releases/README.md create mode 100644 papers/releases/template.release-plan.md create mode 100644 papers/releases/v0.6.0-plan.md create mode 100644 papers/templates/template.feedback.md create mode 100644 papers/ux-feedback/README.md create mode 100644 papers/ux-feedback/buckets/automation-ci.md create mode 100644 papers/ux-feedback/buckets/daily-use.md create mode 100644 papers/ux-feedback/buckets/onboarding.md create mode 100644 papers/ux-feedback/buckets/output-ergonomics.md create mode 100644 papers/ux-feedback/buckets/reliability-trust.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a57ec2e..b8cdb61 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -81,6 +81,14 @@ Stays in CSA Cloud: Use GitHub Issues for bugs/features. Include Node.js version, OS, and `charter --version` output. For security issues, follow `SECURITY.md`. +## Papers Workflow + +- GitHub Issues/PRs are the canonical tracker for active engineering work. +- `papers/` is a curated narrative layer (feedback, research, release plans), not a full mirror of all links. +- New `AGENT_DX_FEEDBACK_*.md` files must include frontmatter keys required by `scripts/papers-lint.mjs`: + `feedback-id`, `date`, `source`, `severity`, `bucket`, `status`, `tracked-issues`, `tracked-prs`. +- Release plans under `papers/releases/*-plan.md` must include the release-plan frontmatter schema validated by `scripts/papers-lint.mjs`. + ## License By contributing, you agree that contributions are licensed under Apache License 2.0. diff --git a/README.md b/README.md index 0e97102..2ba03a8 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,27 @@ 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. + +## Charter: Local Enforcement + ADF Context Compiler + +Charter runs in your terminal and CI pipeline. It validates commit trailers, scores drift against your blessed stack, and blocks merges on violations. Zero SaaS dependency - all checks are deterministic and local. + +Charter also ships **ADF (Attention-Directed Format)** - a modular, AST-backed context system that replaces monolithic `.cursorrules` and `claude.md` files with compiled, trigger-routed `.ai/` modules. ADF treats LLM context as a compiled language: emoji-decorated semantic keys, typed patch operations, manifest-driven progressive disclosure, and metric ceilings with CI evidence gating. + +```bash +npm install --save-dev @stackbilt/cli +npx charter setup --preset fullstack --ci github --yes +npx charter adf init # scaffold .ai/ context directory +``` + +**Governance commands:** `validate`, `drift`, `audit`, `classify`, `hook install`. +**ADF commands:** `adf init`, `adf fmt`, `adf patch`, `adf bundle`, `adf sync`, `adf evidence`. + +For quantitative analysis of ADF's impact on autonomous system architecture, see the [Context-as-Code white paper](https://github.com/stackbilt-dev/charter/blob/main/papers/context-as-code-v1.1.md). + +For iterative UX findings and versioned improvement plans, see the [`papers/` index](https://github.com/stackbilt-dev/charter/blob/main/papers/README.md). + + ## 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. @@ -278,12 +299,14 @@ packages/ ## Research & White Papers -The [`papers/`](./papers/) directory contains versioned white papers documenting -ADF design rationale and quantitative analysis. +The [`papers/`](./papers/) directory is the curated narrative layer for Charter's +iterative process: -| Paper | Description | +| Entry Point | Purpose | |---|---| -| [Context-as-Code v1.1](./papers/context-as-code-v1.1.md) | Quantifies ADF impact on a PRD-driven AI Orchestration Engine v2 SDLC: 80% token reduction, 0% LOC-limit violations across 33 modules. | +| [Papers Index](./papers/README.md) | Canonical overview of research papers, UX feedback, and release planning docs. | +| [UX Feedback Index](./papers/ux-feedback/README.md) | Journey-bucketed ADX findings (Onboarding, Daily Use, Reliability/Trust, Output Ergonomics, Automation/CI). | +| [Release Plans Index](./papers/releases/README.md) | Versioned plans that map selected feedback to implementation outcomes. | ## Release Docs diff --git a/docs-snippets/charter-oss-ecosystem.md b/docs-snippets/charter-oss-ecosystem.md index 97e86f9..5322dfe 100644 --- a/docs-snippets/charter-oss-ecosystem.md +++ b/docs-snippets/charter-oss-ecosystem.md @@ -14,3 +14,5 @@ npx charter adf init # scaffold .ai/ context directory **ADF commands:** `adf init`, `adf fmt`, `adf patch`, `adf bundle`, `adf sync`, `adf evidence`. For quantitative analysis of ADF's impact on autonomous system architecture, see the [Context-as-Code white paper](https://github.com/stackbilt-dev/charter/blob/main/papers/context-as-code-v1.1.md). + +For iterative UX findings and versioned improvement plans, see the [`papers/` index](https://github.com/stackbilt-dev/charter/blob/main/papers/README.md). diff --git a/papers/AGENT_DX_FEEDBACK_001.md b/papers/AGENT_DX_FEEDBACK_001.md index 0d14fd4..f5e540d 100644 --- a/papers/AGENT_DX_FEEDBACK_001.md +++ b/papers/AGENT_DX_FEEDBACK_001.md @@ -4,9 +4,13 @@ feedback-id: ADX-001 date: 2026-02-26 source: "Codex (OpenAI) working on edgestack_v2" severity: high +bucket: daily-use +status: shipped related: - CSA-002 (evidence of the problem ADF solves) - ARCHITECT_V2_INTEGRATION_BRIEF (scaffold should prevent this) +tracked-issues: [] +tracked-prs: [] --- # Agent DX Feedback: Codex Lockfile Archaeology diff --git a/papers/AGENT_DX_FEEDBACK_002.md b/papers/AGENT_DX_FEEDBACK_002.md index 1adc792..efa4410 100644 --- a/papers/AGENT_DX_FEEDBACK_002.md +++ b/papers/AGENT_DX_FEEDBACK_002.md @@ -4,9 +4,13 @@ feedback-id: ADX-002 date: 2026-02-26 source: "Claude Opus 4.6 (Anthropic) bootstrapping smart_revenue_recovery_adf" severity: medium +bucket: onboarding +status: shipped related: - CSA-002 (greenfield measurement — this IS the subject project) - ADX-001 (complements: ADX-001 showed cost without ADF; this shows cost of setting up ADF) +tracked-issues: [] +tracked-prs: [] --- # Agent DX Feedback: ADF Greenfield Bootstrapping — Rule Routing Friction diff --git a/papers/AGENT_DX_FEEDBACK_003.md b/papers/AGENT_DX_FEEDBACK_003.md index b6b5615..d8ba3a6 100644 --- a/papers/AGENT_DX_FEEDBACK_003.md +++ b/papers/AGENT_DX_FEEDBACK_003.md @@ -4,10 +4,14 @@ feedback-id: ADX-003 date: 2026-02-26 source: "Codex (OpenAI) configuring digitalcsa-kit with @stackbilt/cli v0.3.2" severity: high +bucket: automation-ci +status: planned related: - RM-001 (ADF vNext Roadmap draft) - ADX-001 (runtime discoverability friction) - ADX-002 (ADF bootstrapping/routing friction) +tracked-issues: [] +tracked-prs: [] --- # Agent DX Feedback: Install/Setup Automation Friction (Windows + PNPM Workspace) diff --git a/papers/AGENT_DX_FEEDBACK_004.md b/papers/AGENT_DX_FEEDBACK_004.md index 8984bce..266881b 100644 --- a/papers/AGENT_DX_FEEDBACK_004.md +++ b/papers/AGENT_DX_FEEDBACK_004.md @@ -4,10 +4,14 @@ feedback-id: ADX-004 date: 2026-02-26 source: "Claude Opus 4.6 (Anthropic) testing v0.3.3 bootstrap on smart_revenue_recovery_adf" severity: high +bucket: reliability-trust +status: shipped related: - ADX-002 (bootstrap is the fix for ADX-002 friction; this tests that fix) - ADX-003 (install automation friction; bootstrap P0 now ships but has merge gap) - CSA-002 (greenfield measurement — bootstrap run is a data collection event) +tracked-issues: [] +tracked-prs: [] --- # Agent DX Feedback: charter bootstrap on Pre-Configured Repo — Destructive Overwrite diff --git a/papers/AGENT_DX_FEEDBACK_005.md b/papers/AGENT_DX_FEEDBACK_005.md index 9ec2a83..b6e2b36 100644 --- a/papers/AGENT_DX_FEEDBACK_005.md +++ b/papers/AGENT_DX_FEEDBACK_005.md @@ -4,10 +4,14 @@ feedback-id: ADX-005 date: 2026-02-28 source: "Claude Opus 4.6 (Anthropic) UX walkthrough of charter CLI" severity: medium +bucket: output-ergonomics +status: triaged related: - ADX-004 (bootstrap overwrite hazard) - ADX-003 (install automation friction) - ADX-002 (onboarding friction baseline) +tracked-issues: [] +tracked-prs: [] --- # Agent DX Feedback: charter CLI UX — Bootstrap, Migrate, Doctor, and Output Ergonomics diff --git a/papers/README.md b/papers/README.md index 33f4f92..668f032 100644 --- a/papers/README.md +++ b/papers/README.md @@ -1,32 +1,60 @@ -# Charter Kit — Research & White Papers +# Charter Kit Papers -This directory contains versioned white papers documenting the design rationale, -quantitative analysis, and architectural decisions behind Charter Kit and the -Attention-Directed Format (ADF). +This directory is the curated narrative layer for Charter's iterative process. -## Papers +GitHub Issues and PRs are the system of record for active work. The papers here summarize the important findings, decisions, and release outcomes without mirroring every link. + +## Sections + +### Research Papers + +Long-form research, architecture, and rationale documents. | ID | Title | Version | Date | Status | |---|---|---|---|---| | CSA-001 | [Context-as-Code](./context-as-code-v1.1.md) | 1.1 | 2026-02-26 | Published | -| CSA-002 | [Context-as-Code II: Greenfield](./context-as-code-greenfield-v0.1.md) | 0.1 | 2026-02-26 | Draft | -| ADX-001 | [Agent DX Feedback: Lockfile Schema Discoverability](./AGENT_DX_FEEDBACK_001.md) | n/a | 2026-02-26 | Draft | -| ADX-002 | [Agent DX Feedback: ADF Greenfield Bootstrapping — Rule Routing Friction](./AGENT_DX_FEEDBACK_002.md) | n/a | 2026-02-26 | Draft | -| ADX-003 | [Agent DX Feedback: Install/Setup Automation Friction (Windows + PNPM Workspace)](./AGENT_DX_FEEDBACK_003.md) | n/a | 2026-02-26 | Draft | +| CSA-002 | [Context-as-Code II: Greenfield](./context-as-code-greenfield-v0.1.md) | 0.2 | 2026-02-26 | Draft | | RM-001 | [ADF vNext Roadmap (Draft): Agent DX-Driven Priorities](./adf-vnext-roadmap-v0.1.md) | 0.1 | 2026-02-26 | Draft | +| n/a | [Architect v2 x Charter ADF Integration Brief](./ARCHITECT_V2_INTEGRATION_BRIEF.md) | n/a | 2026-02-26 | Proposal | -## Versioning Convention +### UX Feedback + +Agent/user experience findings are categorized by journey buckets: + +- Onboarding +- Daily Use +- Reliability and Trust +- Output Ergonomics +- Automation and CI + +Start here: [UX Feedback Index](./ux-feedback/README.md) + +### Release Plans -Each paper follows `-v..md`: +Versioned plans tie prioritized UX findings to release execution and outcomes. + +Start here: [Release Plans Index](./releases/README.md) + +## Curation Rules + +`papers/` includes only high-signal items: + +- High-severity user-facing friction +- Cross-cutting reliability or trust gaps +- Work explicitly tied to feedback IDs (ADX-*) +- Planned release themes with measurable outcomes + +Everything else remains tracked in GitHub Issues/PRs. + +## Versioning Convention -- **Major** increments on substantive content revisions (new data, changed conclusions). -- **Minor** increments on editorial fixes, formatting, or addenda that don't change findings. +Versioned papers follow `-v..md`: -The YAML frontmatter in each paper tracks `version`, `status`, `charter-version` -(the toolkit release the paper corresponds to), and `paper-id` for stable cross-referencing. +- Major increments on substantive content revisions (new data, changed conclusions) +- Minor increments on editorial fixes or addenda that do not change findings -### Statuses +Standard status values: -- **draft** — in progress, not yet reviewed -- **published** — reviewed and released -- **superseded** — replaced by a newer version (frontmatter will include `superseded-by`) +- `draft` +- `published` +- `superseded` diff --git a/papers/releases/README.md b/papers/releases/README.md new file mode 100644 index 0000000..f7b0170 --- /dev/null +++ b/papers/releases/README.md @@ -0,0 +1,15 @@ +# Release Plans Index + +Release plans connect UX findings (ADX-*) to implementation focus for each version. + +Each plan is curated and limited to high-impact work rather than a full issue or PR dump. + +## Active Plans + +| Release | Plan | Status | Inputs | +|---|---|---|---| +| v0.6.0 | [v0.6.0 Plan](./v0.6.0-plan.md) | Draft | ADX-003, ADX-005 | + +## Template + +- [Release Plan Template](./template.release-plan.md) diff --git a/papers/releases/template.release-plan.md b/papers/releases/template.release-plan.md new file mode 100644 index 0000000..12be97e --- /dev/null +++ b/papers/releases/template.release-plan.md @@ -0,0 +1,44 @@ +--- +release: "vX.Y.Z" +status: draft +target-window: "YYYY-QN" +charter-version-base: "X.Y.Z" +inputs: + - ADX-000 +milestone-link: "https://github.com/Stackbilt-dev/charter/milestones" +owner: "Team/Owner" +--- + +# vX.Y.Z Release Plan + +## Goal + +One-sentence goal for the release. + +## Non-Goals + +- Out-of-scope item 1 +- Out-of-scope item 2 + +## Prioritized Feedback Inputs + +- [ADX-000](../AGENT_DX_FEEDBACK_000.md): short reason + +## Focus Items (Curated) + +- Item 1 +- Item 2 + +## Candidate Tracked Work + +Curated issue/PR links only. Do not mirror all work items. + +## Acceptance Criteria + +- Criterion 1 +- Criterion 2 + +## Rollout and Evidence + +- Post-release outcomes summary +- Merged PR links for shipped work diff --git a/papers/releases/v0.6.0-plan.md b/papers/releases/v0.6.0-plan.md new file mode 100644 index 0000000..9c23948 --- /dev/null +++ b/papers/releases/v0.6.0-plan.md @@ -0,0 +1,50 @@ +--- +release: "v0.6.0" +status: draft +target-window: "2026-Q2" +charter-version-base: "0.5.0" +inputs: + - ADX-003 + - ADX-005 +milestone-link: "https://github.com/Stackbilt-dev/charter/milestones" +owner: "Charter Kit Engineering" +--- + +# v0.6.0 Release Plan + +## Goal + +Improve unattended automation reliability and reduce operational friction in CLI output ergonomics. + +## Non-Goals + +- Major ADF parser redesign +- Breaking CLI command renames + +## Prioritized Feedback Inputs + +- [ADX-003](../AGENT_DX_FEEDBACK_003.md): install/setup automation friction +- [ADX-005](../AGENT_DX_FEEDBACK_005.md): UX and output ergonomics findings + +## Focus Items (Curated) + +- Harden install and setup guidance for non-interactive and sandboxed flows +- Reduce noisy JSON payload defaults in migrate-like workflows +- Improve first-run error remediation hints for common platform path and permission failures +- Document recommended command path transition (`npx` bootstrap -> pinned local `pnpm exec`) + +## Candidate Tracked Work + +Use GitHub milestone items as canonical source. Curated links should be added here only for selected in-scope issues and PRs. + +## Acceptance Criteria + +- Automation path documented with one primary happy-path flow and one fallback flow +- At least one high-friction ADX-005 finding is shipped with clear before/after behavior +- No regression in `doctor --adf-only --ci` and `adf evidence --auto-measure --ci` contracts + +## Rollout and Evidence + +- Add post-release notes summarizing shipped items and outcomes +- Link merged PRs that implement the selected focus items +- Capture evidence snapshots in README/CHANGELOG where user-facing behavior changes diff --git a/papers/templates/template.feedback.md b/papers/templates/template.feedback.md new file mode 100644 index 0000000..2bee16f --- /dev/null +++ b/papers/templates/template.feedback.md @@ -0,0 +1,39 @@ +--- +title: "Agent DX Feedback: " +feedback-id: ADX-000 +date: YYYY-MM-DD +source: "Agent/User + context" +severity: low +bucket: onboarding +status: new +related: + - RM-001 +tracked-issues: [] +tracked-prs: [] +--- + +# Agent DX Feedback: <title> + +## Observation + +Describe the observed user or agent friction. + +## Root Causes + +### 1. <cause> + +Details. + +## Impact + +Describe impact to users, workflows, or release goals. + +## Recommended Improvements + +### P0: <high priority item> + +Details. + +## Notes + +Add references, receipts, or links as needed. diff --git a/papers/ux-feedback/README.md b/papers/ux-feedback/README.md new file mode 100644 index 0000000..ef5d0f4 --- /dev/null +++ b/papers/ux-feedback/README.md @@ -0,0 +1,28 @@ +# UX Feedback Index + +This index groups ADX feedback by journey stage so viewers can quickly see where friction appears and how it is being addressed. + +## Bucket Map + +| Feedback | Title | Primary Bucket | Severity | Status | +|---|---|---|---|---| +| ADX-001 | [Codex Lockfile Archaeology](../AGENT_DX_FEEDBACK_001.md) | Daily Use | High | Shipped | +| ADX-002 | [ADF Greenfield Bootstrapping - Rule Routing Friction](../AGENT_DX_FEEDBACK_002.md) | Onboarding | Medium | Shipped | +| ADX-003 | [Install/Setup Automation Friction (Windows + PNPM Workspace)](../AGENT_DX_FEEDBACK_003.md) | Automation and CI | High | Planned | +| ADX-004 | [Bootstrap on Pre-Configured Repo - Destructive Overwrite](../AGENT_DX_FEEDBACK_004.md) | Reliability and Trust | High | Shipped | +| ADX-005 | [CLI UX - Bootstrap, Migrate, Doctor, and Output Ergonomics](../AGENT_DX_FEEDBACK_005.md) | Output Ergonomics | Medium | Triaged | + +## Buckets + +- [Onboarding](./buckets/onboarding.md) +- [Daily Use](./buckets/daily-use.md) +- [Reliability and Trust](./buckets/reliability-trust.md) +- [Output Ergonomics](./buckets/output-ergonomics.md) +- [Automation and CI](./buckets/automation-ci.md) + +## Status Definitions + +- `new`: captured, not triaged +- `triaged`: categorized and scoped +- `planned`: selected for a release plan +- `shipped`: core fix released diff --git a/papers/ux-feedback/buckets/automation-ci.md b/papers/ux-feedback/buckets/automation-ci.md new file mode 100644 index 0000000..d7f02d8 --- /dev/null +++ b/papers/ux-feedback/buckets/automation-ci.md @@ -0,0 +1,7 @@ +# Automation and CI Bucket + +Feedback tied to unattended setup, install orchestration, CI flow, and cross-platform automation reliability. + +## Included Feedback + +- [ADX-003](../../AGENT_DX_FEEDBACK_003.md): install/setup orchestration friction on Windows and PNPM workspace flows diff --git a/papers/ux-feedback/buckets/daily-use.md b/papers/ux-feedback/buckets/daily-use.md new file mode 100644 index 0000000..15429fa --- /dev/null +++ b/papers/ux-feedback/buckets/daily-use.md @@ -0,0 +1,7 @@ +# Daily Use Bucket + +Feedback from normal authoring and maintenance workflows after onboarding. + +## Included Feedback + +- [ADX-001](../../AGENT_DX_FEEDBACK_001.md): lockfile schema discoverability and runtime archaeology friction diff --git a/papers/ux-feedback/buckets/onboarding.md b/papers/ux-feedback/buckets/onboarding.md new file mode 100644 index 0000000..b9c2eda --- /dev/null +++ b/papers/ux-feedback/buckets/onboarding.md @@ -0,0 +1,7 @@ +# Onboarding Bucket + +Feedback about first-run setup, bootstrapping, migration, and initial guidance quality. + +## Included Feedback + +- [ADX-002](../../AGENT_DX_FEEDBACK_002.md): rule routing and taxonomy ambiguity during greenfield setup diff --git a/papers/ux-feedback/buckets/output-ergonomics.md b/papers/ux-feedback/buckets/output-ergonomics.md new file mode 100644 index 0000000..dfb59d0 --- /dev/null +++ b/papers/ux-feedback/buckets/output-ergonomics.md @@ -0,0 +1,7 @@ +# Output Ergonomics Bucket + +Feedback about readability, signal-to-noise, and operational clarity of CLI output. + +## Included Feedback + +- [ADX-005](../../AGENT_DX_FEEDBACK_005.md): UX findings across bootstrap, migrate, doctor, and output behavior diff --git a/papers/ux-feedback/buckets/reliability-trust.md b/papers/ux-feedback/buckets/reliability-trust.md new file mode 100644 index 0000000..51c3a6d --- /dev/null +++ b/papers/ux-feedback/buckets/reliability-trust.md @@ -0,0 +1,7 @@ +# Reliability and Trust Bucket + +Feedback where tooling behavior can damage confidence, correctness, or user data. + +## Included Feedback + +- [ADX-004](../../AGENT_DX_FEEDBACK_004.md): bootstrap overwrite behavior on pre-configured repositories From 7e6e7f407ff635b4ced704c1bae44d114cc8b1c9 Mon Sep 17 00:00:00 2001 From: Kurt Overmier <admin@stackbilt.dev> Date: Sun, 1 Mar 2026 06:44:06 -0600 Subject: [PATCH 5/6] chore(repo): update repository configuration --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 45a9c72..acb5bf8 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,8 @@ "test:coverage": "bash -lc \"pnpm exec vitest run --coverage\"", "scorecard:generate": "node scripts/generate-scorecard.mjs", "scorecard:validate": "node scripts/validate-scorecard.mjs", - "docs:sync": "node scripts/docs-sync.mjs --write", - "docs:check": "node scripts/docs-sync.mjs --check", + "docs:sync": "node scripts/docs-sync.mjs --write && node scripts/papers-lint.mjs", + "docs:check": "node scripts/docs-sync.mjs --check && node scripts/papers-lint.mjs", "docs:oss:sync": "node scripts/docs-sync.mjs --write --config .docsync.oss.json", "docs:oss:check": "node scripts/docs-sync.mjs --check --config .docsync.oss.json", "docs:oss:auto": "node scripts/docs-oss-auto-sync.mjs --config .docsync.oss.json", From 46da53ce008586230a9e5d8a1e5e2f45aa1d7547 Mon Sep 17 00:00:00 2001 From: Kurt Overmier <admin@stackbilt.dev> Date: Sun, 1 Mar 2026 06:44:14 -0600 Subject: [PATCH 6/6] chore(scripts): update commit automation workflow --- scripts/papers-lint.mjs | 217 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 scripts/papers-lint.mjs diff --git a/scripts/papers-lint.mjs b/scripts/papers-lint.mjs new file mode 100644 index 0000000..9ee9cf2 --- /dev/null +++ b/scripts/papers-lint.mjs @@ -0,0 +1,217 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import process from 'node:process'; + +const cwd = process.cwd(); +const papersDir = path.join(cwd, 'papers'); + +const feedbackPattern = /^AGENT_DX_FEEDBACK_\d{3}\.md$/; +const feedbackRequiredKeys = [ + 'feedback-id', + 'date', + 'source', + 'severity', + 'bucket', + 'status', + 'tracked-issues', + 'tracked-prs' +]; + +const releasePlanRequiredKeys = [ + 'release', + 'status', + 'target-window', + 'charter-version-base', + 'inputs', + 'milestone-link', + 'owner' +]; + +const allowedBuckets = new Set([ + 'onboarding', + 'daily-use', + 'reliability-trust', + 'output-ergonomics', + 'automation-ci' +]); + +const allowedFeedbackStatus = new Set(['new', 'triaged', 'planned', 'shipped']); +const allowedReleaseStatus = new Set(['draft', 'active', 'shipped', 'superseded']); + +function parseFrontmatter(raw) { + const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/); + if (!match) { + return null; + } + + const map = new Map(); + const lines = match[1].split(/\r?\n/); + let activeListKey = null; + + for (const line of lines) { + if (!line.trim()) { + continue; + } + + const keyMatch = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/); + if (keyMatch) { + const [, key, valueRaw] = keyMatch; + const value = valueRaw.trim(); + + if (value === '') { + map.set(key, []); + activeListKey = key; + } else { + map.set(key, value); + activeListKey = null; + } + continue; + } + + const listMatch = line.match(/^\s*-\s*(.*)$/); + if (listMatch && activeListKey) { + const existing = map.get(activeListKey); + if (Array.isArray(existing)) { + existing.push(listMatch[1].trim()); + } + continue; + } + + activeListKey = null; + } + + return map; +} + +function normalizeScalar(value) { + if (Array.isArray(value)) { + return ''; + } + return String(value).trim().replace(/^"|"$/g, ''); +} + +function normalizeList(value) { + if (Array.isArray(value)) { + return value; + } + + const scalar = normalizeScalar(value); + if (scalar === '[]') { + return []; + } + + return null; +} + +async function listFiles(dir) { + try { + return await fs.readdir(dir, { withFileTypes: true }); + } catch { + return []; + } +} + +function requireKeys(map, keys, filePath, failures) { + for (const key of keys) { + if (!map.has(key)) { + failures.push(`${filePath}: missing frontmatter key '${key}'`); + } + } +} + +async function lintFeedbackFiles(failures) { + const entries = await listFiles(papersDir); + const feedbackFiles = entries.filter((entry) => entry.isFile() && feedbackPattern.test(entry.name)); + + for (const file of feedbackFiles) { + const fullPath = path.join(papersDir, file.name); + const relPath = path.relative(cwd, fullPath); + const raw = await fs.readFile(fullPath, 'utf8'); + const fm = parseFrontmatter(raw); + + if (!fm) { + failures.push(`${relPath}: missing YAML frontmatter block`); + continue; + } + + requireKeys(fm, feedbackRequiredKeys, relPath, failures); + + const bucket = normalizeScalar(fm.get('bucket')); + if (bucket && !allowedBuckets.has(bucket)) { + failures.push(`${relPath}: invalid bucket '${bucket}'`); + } + + const status = normalizeScalar(fm.get('status')); + if (status && !allowedFeedbackStatus.has(status)) { + failures.push(`${relPath}: invalid status '${status}'`); + } + + const trackedIssues = normalizeList(fm.get('tracked-issues')); + const trackedPrs = normalizeList(fm.get('tracked-prs')); + + if (!Array.isArray(trackedIssues)) { + failures.push(`${relPath}: 'tracked-issues' must be a YAML list`); + } + + if (!Array.isArray(trackedPrs)) { + failures.push(`${relPath}: 'tracked-prs' must be a YAML list`); + } + } +} + +async function lintReleasePlans(failures) { + const releaseDir = path.join(papersDir, 'releases'); + const entries = await listFiles(releaseDir); + const planFiles = entries.filter( + (entry) => + entry.isFile() && + entry.name.endsWith('-plan.md') && + entry.name !== 'template.release-plan.md' + ); + + for (const file of planFiles) { + const fullPath = path.join(releaseDir, file.name); + const relPath = path.relative(cwd, fullPath); + const raw = await fs.readFile(fullPath, 'utf8'); + const fm = parseFrontmatter(raw); + + if (!fm) { + failures.push(`${relPath}: missing YAML frontmatter block`); + continue; + } + + requireKeys(fm, releasePlanRequiredKeys, relPath, failures); + + const status = normalizeScalar(fm.get('status')); + if (status && !allowedReleaseStatus.has(status)) { + failures.push(`${relPath}: invalid release status '${status}'`); + } + + const inputs = normalizeList(fm.get('inputs')); + if (!Array.isArray(inputs) || inputs.length === 0) { + failures.push(`${relPath}: 'inputs' must be a non-empty YAML list`); + } + } +} + +async function main() { + const failures = []; + + await lintFeedbackFiles(failures); + await lintReleasePlans(failures); + + if (failures.length > 0) { + console.error('papers-lint failed'); + for (const failure of failures) { + console.error(`- ${failure}`); + } + process.exit(1); + } + + console.log('papers-lint passed'); +} + +main().catch((error) => { + console.error(`papers-lint fatal: ${error.message}`); + process.exit(1); +});