From fff4c4571fef51419b29356320da977d5bc26536 Mon Sep 17 00:00:00 2001 From: John Smith Date: Thu, 20 Aug 2026 15:09:19 -0400 Subject: [PATCH] docs: add comprehensive code comments and AGENTS.md, CLAUDE.md, GEMINI.md instructions --- AGENTS.md | 100 ++++++ CLAUDE.md | 91 +++++ GEMINI.md | 66 ++++ bin/agent-gate.js | 175 ++++------ bin/agent-proof.js | 7 + scripts/verify-real-repo.mjs | 33 +- src/cli.ts | 458 +++++++++++-------------- src/detector/index.ts | 5 + src/detector/stackDetector.ts | 93 ++++- src/formatter/ansi.ts | 11 + src/formatter/diagnosticStream.ts | 197 +++++------ src/formatter/index.ts | 5 + src/formatter/parsers/actionlint.ts | 38 +- src/formatter/parsers/aislop.ts | 36 +- src/formatter/parsers/biome.ts | 127 ++++--- src/formatter/parsers/generic.ts | 86 +++-- src/formatter/parsers/ruff.ts | 103 +++--- src/formatter/parsers/skillcheck.ts | 125 +++---- src/formatter/parsers/trufflehog.ts | 45 ++- src/formatter/parsers/typos.ts | 96 +++--- src/generator/configGenerator.ts | 48 ++- src/generator/index.ts | 5 + src/generator/templates/aislop.ts | 11 + src/generator/templates/biome.ts | 11 + src/generator/templates/claudeHooks.ts | 34 +- src/generator/templates/lefthook.ts | 45 ++- src/generator/templates/ruff.ts | 11 + src/index.ts | 15 +- src/installer/hookInstaller.ts | 146 ++++---- src/installer/index.ts | 7 +- src/installer/lockin.ts | 163 ++++++--- src/runner/gateRunner.ts | 198 +++++------ src/runner/index.ts | 5 + src/types/index.ts | 121 ++++++- 34 files changed, 1710 insertions(+), 1007 deletions(-) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 GEMINI.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e964a67 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,100 @@ +# AGENTS.md — Multi-Agent Development & Mechanical Governance Protocol + +Welcome to **Agent-Proof** (`@heretek-ai/agent-proof`). This document establishes the multi-agent operational standards, mechanical hard-gate execution rules, and architecture guidelines for autonomous AI coding agents operating within this repository. + +--- + +## šŸŽÆ Primary Directives for AI Agents + +1. **Deterministic Mechanical Governance Over Soft Prompts**: + - Never attempt to bypass, disable, or modify mechanical hard gates (`lefthook.yml`, `.claude/hooks.json`, `biome.json`, `ruff.toml`, `.aislop/config.yml`). + - Governance files are intentionally locked with read-only permissions (`chmod 0444`). Do not attempt to overwrite or change their permissions unless explicitly instructed by a human administrator. + - Never bypass git hooks with `git commit --no-verify`. All commits must pass mechanical pre-commit validation in `< 2.0s`. + +2. **Zero-Tolerance for AI Slop**: + - **No Empty Catch Blocks / Swallowed Errors**: Always handle exceptions explicitly, log with context, rethrow with cause (`new Error(msg, { cause: err })`), or return explicit typed failure results. + - **Strict Type Safety**: Avoid `as any` or unchecked type assertions. Use TypeScript type guards, interfaces, or validated schemas (e.g. Zod). + - **No Hallucinated Imports**: Only import from installed packages declared in `package.json` / `pyproject.toml` or verified relative paths within the project. + - **Clean ASTs & Formatting**: Ensure code adheres to Biome / Ruff formatting standards without lint violations or dead code. + +3. **Autonomous Self-Correction Protocol**: + - When a mechanical gate fails, parse the emitted **LSP Diagnostic Envelope** (`https://json.schemastore.org/lsif.json`). + - Extract the `repair_tokens` provided in `diagnostics[].repair_instruction` to apply deterministic fixes immediately rather than guessing or engaging in trial-and-error edits. + +--- + +## šŸ—ļø 3-Tier Mechanical Gate Architecture + +| Stage | Trigger Event | Latency Target | Scope | Canonical Engines | +| :--- | :--- | :--- | :--- | :--- | +| **Stage 1: Pre-Tool / Edit** | `PostFileEdit` agent hook | `< 300ms` | Single modified file AST | Biome (`--write`), Ruff (`--fix`), SkillCheck | +| **Stage 2: Hard Git Gate** | `git commit` (Pre-Commit) | `< 2.0s` | Staged blobs (`--staged`) | Lefthook parallel: Biome, Ruff, AISlop, TruffleHog, Typos, Actionlint | +| **Stage 3: CI & Graph Audit** | `git push` / PR Pipeline | Unconstrained | Full codebase graph | Fallow (dead code / arch drift), Sherif (monorepo), OWASP Noir (API surface) | + +--- + +## šŸ“‚ Repository Architecture & Key Directories + +``` +Agent-Proof/ +ā”œā”€ā”€ bin/ # Zero-dependency platform binary launchers +│ ā”œā”€ā”€ agent-proof.js # Primary CLI launcher with optionalDependencies resolution +│ └── agent-gate.js # Compatibility alias launcher +ā”œā”€ā”€ src/ # TypeScript source code +│ ā”œā”€ā”€ detector/ # Multi-stack auto-detection engine +│ ā”œā”€ā”€ generator/ # Multi-tier configuration generator & templates +│ ā”œā”€ā”€ formatter/ # ANSI-stripper & LSP diagnostic streaming engine +│ ā”œā”€ā”€ installer/ # Git hook installer & POSIX permission lock-in +│ ā”œā”€ā”€ runner/ # Mechanical gate stage execution runner +│ ā”œā”€ā”€ types/ # TypeScript interfaces and schema types +│ ā”œā”€ā”€ cli.ts # Command-line interface dispatcher +│ └── index.ts # Package public API exports +ā”œā”€ā”€ tests/ # Vitest unit and integration test suites +│ ā”œā”€ā”€ detector.test.ts # Polyglot stack detection tests +│ ā”œā”€ā”€ generator.test.ts # Config codegen tests +│ ā”œā”€ā”€ formatter.test.ts # Diagnostic streamer and parser tests +│ ā”œā”€ā”€ launcher.test.ts # Binary resolution tests +│ ā”œā”€ā”€ lockin.test.ts # Git hook & permission locking tests +│ └── e2e.test.ts # End-to-end repository initialization test +ā”œā”€ā”€ scripts/ # Development & verification scripts +│ └── verify-real-repo.mjs # Real-world sandbox lifecycle validation script +ā”œā”€ā”€ .github/workflows/ # GitHub Actions CI/CD +│ ā”œā”€ā”€ ci.yml # Continuous integration test runner +│ └── publish.yml # Trusted publishing (OIDC) to npm +ā”œā”€ā”€ package.json # Package definition, scripts, optionalDependencies matrix +ā”œā”€ā”€ tsconfig.json # Strict NodeNext TypeScript configuration +└── tsup.config.ts # Bundle configuration for dual CJS/ESM distribution +``` + +--- + +## šŸ› ļø Common CLI & Development Commands + +```bash +# Build TypeScript bundles into dist/ +npm run build + +# Run Vitest test suite +npm test + +# Run TypeScript typechecks +npm run typecheck + +# Run real-world sandbox lifecycle validation +npm run test:real-repo + +# Test CLI commands locally +node bin/agent-proof.js --help +node bin/agent-proof.js detect +node bin/agent-proof.js init [directory] +node bin/agent-proof.js run pre-commit +node bin/agent-proof.js status +``` + +--- + +## šŸ”’ Security & Supply Chain Standards + +- **NPM Package**: `@heretek-ai/agent-proof` published to `https://registry.npmjs.org/@heretek-ai/agent-proof`. +- **Publishing Method**: OpenID Connect (OIDC) **Trusted Publishing** via `.github/workflows/publish.yml` with `id-token: write` and npm provenance. +- **Dependencies**: Keep runtime dependencies at zero. All utilities are compiled into `dist/` or distributed via platform-specific binary packages under `optionalDependencies`. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7bd5e9b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,91 @@ +# CLAUDE.md — Claude Code Guidelines for Agent-Proof + +This guide provides instructions and reference material for **Claude Code** sessions working on `@heretek-ai/agent-proof`. + +--- + +## šŸš€ Quick Reference Commands + +```bash +# Build the project (tsup bundles to dist/) +npm run build + +# Run unit and integration tests (Vitest) +npm test + +# Run strict TypeScript type checks +npm run typecheck + +# Run real-world sandbox verification script +npm run test:real-repo + +# Test CLI locally +node bin/agent-proof.js --version +node bin/agent-proof.js init +node bin/agent-proof.js detect +node bin/agent-proof.js status +``` + +--- + +## šŸ—ļø Architecture & Component Overview + +Agent-Proof is structured into modular components: + +1. **Stack Detector (`src/detector/stackDetector.ts`)**: + - Inspects target repository indicators (JS/TS, Python, Go, Rust, GitHub Workflows, Docker, Claude Agent Harness). +2. **Config Generator (`src/generator/configGenerator.ts`)**: + - Generates `lefthook.yml`, `.claude/hooks.json`, `biome.json`, `ruff.toml`, and `.aislop/config.yml`. +3. **Diagnostic Streamer (`src/formatter/diagnosticStream.ts`)**: + - Converts raw stderr/stdout from tools (`aislop`, `biome`, `ruff`, `skillcheck`, `trufflehog`, `typos`, `actionlint`) into an LSP-compliant JSON diagnostic envelope with `repair_tokens`. +4. **Hook Installer & Lock-in (`src/installer/`)**: + - Sets up `.git/hooks/pre-commit` and locks governance files to read-only (`0o444`). +5. **Gate Runner (`src/runner/gateRunner.ts`)**: + - Executes gate stages (`post-edit`, `pre-commit`, `pre-push`). +6. **Binary Launchers (`bin/agent-proof.js` & `bin/agent-gate.js`)**: + - Zero-dependency node wrappers that delegate execution directly to native architecture binaries or fall back to `dist/cli.js`. + +--- + +## ⚔ Agent Lifecycle Hooks & Interception + +When Claude Code operates in a repository initialized with Agent-Proof: + +1. **`PostFileEdit` Lifecycle Event**: + - Fires automatically whenever Claude modifies a file. + - For `*.{js,ts,jsx,tsx}`, runs `npx @biomejs/biome check --write ${filePath}`. + - For `*.py`, runs `ruff check --fix ${filePath}`. + - For `.claude/skills/*.md` or `SKILL.md`, runs `skillcheck check ${filePath}`. + - Runs in `< 300ms` on single modified ASTs. + +2. **`PreCommit` Lifecycle Event**: + - Fires when git commit is initiated. + - Runs `npx lefthook run pre-commit` executing parallel staged checks in `< 2.0s`. + +3. **Interpreting Hook Diagnostics**: + - If a hook fails, inspect the JSON output envelope: + ```json + { + "status": "GATE_FAILED", + "diagnostics": [ + { + "source": "aislop", + "rule_id": "AI_SLOP_SWALLOWED_ERROR", + "file_path": "src/auth.ts", + "repair_instruction": { + "action": "REWRITE_BLOCK", + "repair_tokens": ["throw new Error(...)"] + } + } + ] + } + ``` + - Immediately apply the suggested `repair_tokens` to resolve the violation. + +--- + +## 🚫 Prohibited Actions for Claude Code + +- **Do NOT** bypass git hooks using `--no-verify`. +- **Do NOT** attempt to edit or delete `.claude/settings.json`, `.claude/hooks.json`, or `lefthook.yml`. These files are marked read-only (`chmod 0444`) to enforce governance boundaries. +- **Do NOT** introduce empty catch blocks, `as any` casts, or unverified imports. diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..f0306c9 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,66 @@ +# GEMINI.md — Google Gemini & Antigravity Assistant Guide + +This document defines workflows, tool execution guidelines, and architectural standards for **Gemini / Antigravity** sessions working on the `@heretek-ai/agent-proof` codebase. + +--- + +## šŸ› ļø Project Ecosystem & Stack + +- **Package**: `@heretek-ai/agent-proof` +- **Repository**: `https://github.com/Heretek-AI/Agent-Proof` +- **Registry**: `https://registry.npmjs.org/@heretek-ai/agent-proof` +- **Runtime**: Node.js `>= 18.0.0` +- **Build System**: `tsup` (ESBuild-backed dual CJS/ESM packager) +- **Test Framework**: `vitest` +- **Compiler**: TypeScript 5.7+ with strict NodeNext module resolution + +--- + +## šŸ“‹ Core Development Workflows + +### 1. Build and Typecheck +```bash +# Build CJS, ESM, and .d.ts declarations into dist/ +npm run build + +# Run strict TypeScript compiler verification without emitting +npm run typecheck +``` + +### 2. Testing +```bash +# Run unit & integration test suites +npm test + +# Run real-world sandbox lifecycle test +npm run test:real-repo +``` + +### 3. Publishing & CI/CD +- GitHub Actions workflow [`.github/workflows/publish.yml`](file:///home/john/Projects/Agent-Proof/.github/workflows/publish.yml) uses **Trusted Publishing (OIDC)** with `id-token: write` and npm provenance. +- Publishing is triggered by creating a GitHub Release or pushing a version tag (`v*.*.*`). + +--- + +## 🧠 Codebase Knowledge & Key Modules + +| Module | Location | Purpose | +| :--- | :--- | :--- | +| **Types** | [`src/types/index.ts`](file:///home/john/Projects/Agent-Proof/src/types/index.ts) | Canonical interfaces, LSP schemas, diagnostic envelopes | +| **Detector** | [`src/detector/stackDetector.ts`](file:///home/john/Projects/Agent-Proof/src/detector/stackDetector.ts) | Multi-stack inspection across polyglot repositories | +| **Generator** | [`src/generator/configGenerator.ts`](file:///home/john/Projects/Agent-Proof/src/generator/configGenerator.ts) | Deterministic config templates (`lefthook.yml`, `.claude/hooks.json`) | +| **Formatter** | [`src/formatter/diagnosticStream.ts`](file:///home/john/Projects/Agent-Proof/src/formatter/diagnosticStream.ts) | ANSI stripper, tool output parsers, LSP envelope builder | +| **Parsers** | [`src/formatter/parsers/`](file:///home/john/Projects/Agent-Proof/src/formatter/parsers/) | Specialized parsers for `aislop`, `biome`, `ruff`, `skillcheck`, `trufflehog`, `typos`, `actionlint` | +| **Installer** | [`src/installer/`](file:///home/john/Projects/Agent-Proof/src/installer/) | Git hook installation and read-only permission lock-in (`chmod 0444`) | +| **Runner** | [`src/runner/gateRunner.ts`](file:///home/john/Projects/Agent-Proof/src/runner/gateRunner.ts) | Gate stage runner for `post-edit`, `pre-commit`, `pre-push` | +| **CLI** | [`src/cli.ts`](file:///home/john/Projects/Agent-Proof/src/cli.ts) | Command dispatcher (`init`, `detect`, `run`, `lock`, `unlock`, `status`) | +| **Launcher** | [`bin/agent-proof.js`](file:///home/john/Projects/Agent-Proof/bin/agent-proof.js) | Native binary resolution with JS fallback | + +--- + +## šŸ›”ļø Coding & Architectural Standards + +1. **Zero Runtime Dependencies**: The published CLI bundle should maintain minimal/zero runtime dependencies; native acceleration is handled via `optionalDependencies`. +2. **Deterministic Output**: Always ensure configuration generators output formatted, deterministic strings without non-deterministic ordering. +3. **Comprehensive Error Handling**: All CLI commands must catch errors, format them through the Diagnostic Streamer, and return appropriate process exit codes (0 for success, 1+ for errors). +4. **Documentation Integrity**: Maintain line comments and JSDoc annotations across all modules. diff --git a/bin/agent-gate.js b/bin/agent-gate.js index 419cb26..89b3400 100755 --- a/bin/agent-gate.js +++ b/bin/agent-gate.js @@ -1,123 +1,90 @@ #!/usr/bin/env node -'use strict'; - -const { execFileSync } = require('node:child_process'); -const fs = require('node:fs'); -const path = require('node:path'); - /** - * Platform-Specific Zero-Dependency Binary Launcher - * Directly delegates execution to native architecture binary when available, - * or seamlessly falls back to bundled JavaScript CLI engine. + * @file bin/agent-gate.js + * @description Zero-dependency platform-specific binary launcher with JS fallback. + * + * Execution Resolution Flow: + * 1. Checks for native compiled architecture binary in optionalDependencies + * (@heretek-ai/binary-*, @agent-proof/binary-*, @agent-gate/binary-*). + * 2. If present, delegates directly via `execFileSync` without invoking npm. + * 3. Otherwise, falls back to executing the bundled JavaScript CLI in `dist/cli.js`. */ -const PLATFORM_MAP = { - darwin: 'darwin', - linux: 'linux', - win32: 'win32', -}; +'use strict'; -const ARCH_MAP = { - x64: 'x64', - arm64: 'arm64', -}; +const fs = require('node:fs'); +const path = require('node:path'); +const { execFileSync } = require('node:child_process'); -function getNativeBinaryPath() { - const platform = PLATFORM_MAP[process.platform]; - const arch = ARCH_MAP[process.arch]; +// Determine current OS platform and CPU architecture +const platform = process.platform; +const arch = process.arch; - if (!platform || !arch) { - return null; - } +// Binary package naming candidates +const packageNames = [ + `@heretek-ai/binary-${platform}-${arch}`, + `@agent-proof/binary-${platform}-${arch}`, + `@agent-gate/binary-${platform}-${arch}`, +]; - const binaryNames = platform === 'win32' - ? ['agent-proof.exe', 'agent-gate.exe'] - : ['agent-proof', 'agent-gate']; +// Binary executable filename +const binaryName = platform === 'win32' ? 'agent-proof.exe' : 'agent-proof'; - const packageNames = [ - `@heretek-ai/binary-${platform}-${arch}`, - `@agent-proof/binary-${platform}-${arch}`, - `@agent-gate/binary-${platform}-${arch}`, - ]; +let nativeBinaryPath = null; - for (const packageName of packageNames) { - for (const binaryName of binaryNames) { - const candidates = [ - // 1. Direct optionalDependency in node_modules - path.join(__dirname, '..', 'node_modules', packageName, 'bin', binaryName), - // 2. Hoisted node_modules (monorepo / global) - path.join(__dirname, '..', '..', packageName, 'bin', binaryName), - // 3. Local architecture build directory - path.join(__dirname, 'native', `${platform}-${arch}`, binaryName), - ]; - - for (const candidate of candidates) { - try { - if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) { - return candidate; - } - } catch { - // ignore stat errors - } - } +// Search for native binary across candidate package names +for (const pkg of packageNames) { + try { + const pkgPath = require.resolve(`${pkg}/package.json`); + const binDir = path.dirname(pkgPath); + const candidatePath = path.join(binDir, 'bin', binaryName); + if (fs.existsSync(candidatePath)) { + nativeBinaryPath = candidatePath; + break; } + } catch { + // Package not installed in node_modules, continue searching } - - return null; } -function launch() { - const nativeBinary = getNativeBinaryPath(); - const args = process.argv.slice(2); - - if (nativeBinary) { - try { - execFileSync(nativeBinary, args, { - stdio: 'inherit', - env: process.env, - }); - process.exit(0); - } catch (err) { - if (err.status !== undefined && err.status !== null) { - process.exit(err.status); - } else if (err.signal) { - process.kill(process.pid, err.signal); - } else { - console.error(`[agent-proof] Native binary execution failed: ${err.message}`); - process.exit(1); - } +// Delegate execution +if (nativeBinaryPath) { + // Execute native compiled binary directly + try { + execFileSync(nativeBinaryPath, process.argv.slice(2), { + stdio: 'inherit', + env: process.env, + }); + } catch (err) { + if (err.status !== undefined) { + process.exit(err.status); } - } else { - // Fallback to bundled JavaScript CLI engine - const distCli = path.join(__dirname, '..', 'dist', 'cli.js'); - const distCliCjs = path.join(__dirname, '..', 'dist', 'cli.cjs'); + console.error(`Failed to execute native binary at ${nativeBinaryPath}:`, err.message); + process.exit(1); + } +} else { + // Fall back to JavaScript CLI implementation in dist/ + const cliCjs = path.join(__dirname, '..', 'dist', 'cli.js'); + const cliMjs = path.join(__dirname, '..', 'dist', 'cli.mjs'); - if (fs.existsSync(distCliCjs)) { - try { - const { runCli } = require(distCliCjs); - runCli(args).then(code => process.exit(code ?? 0)).catch(err => { - console.error(err); - process.exit(1); - }); - } catch (err) { - console.error(`[agent-proof] JS fallback failed: ${err.message}`); - process.exit(1); - } - } else if (fs.existsSync(distCli)) { - import(distCli).then(({ runCli }) => { - return runCli(args); - }).then(code => { - process.exit(code ?? 0); - }).catch(err => { - console.error(`[agent-proof] JS fallback failed: ${err.message}`); - process.exit(1); - }); - } else { - console.error('[agent-proof] Error: Neither native binary nor built dist/cli.js was found.'); - console.error('Please run `npm run build` first.'); + if (fs.existsSync(cliCjs)) { + const { main } = require(cliCjs); + main(process.argv.slice(2)).catch(err => { + console.error(err); process.exit(1); - } + }); + } else if (fs.existsSync(cliMjs)) { + import(cliMjs).then(({ main }) => { + main(process.argv.slice(2)).catch(err => { + console.error(err); + process.exit(1); + }); + }).catch(err => { + console.error('Failed to load CLI bundle:', err); + process.exit(1); + }); + } else { + console.error('Error: Neither native binary nor bundled CLI found. Please run `npm run build`.'); + process.exit(1); } } - -launch(); diff --git a/bin/agent-proof.js b/bin/agent-proof.js index f61dd13..bc09759 100755 --- a/bin/agent-proof.js +++ b/bin/agent-proof.js @@ -1,4 +1,11 @@ #!/usr/bin/env node +/** + * @file bin/agent-proof.js + * @description Primary CLI binary wrapper for @heretek-ai/agent-proof. + * + * Delegates execution directly to the launcher implementation in agent-gate.js. + */ + 'use strict'; require('./agent-gate.js'); diff --git a/scripts/verify-real-repo.mjs b/scripts/verify-real-repo.mjs index 48faa5d..c8d7125 100755 --- a/scripts/verify-real-repo.mjs +++ b/scripts/verify-real-repo.mjs @@ -1,4 +1,14 @@ #!/usr/bin/env node +/** + * @file scripts/verify-real-repo.mjs + * @description Real-world sandbox lifecycle verification test script. + * + * Simulates a realistic multi-stack polyglot repository (TypeScript, Python, + * GitHub Workflows, Docker, Claude Skills), initializes mechanical hard gates, + * tests permission locking, injects deliberate AI anti-patterns, asserts sub-second + * gate interception with repair tokens, and validates autonomous self-correction. + */ + import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os'; @@ -11,7 +21,7 @@ const __dirname = path.dirname(__filename); const rootDir = path.resolve(__dirname, '..'); const binPath = path.join(rootDir, 'bin', 'agent-proof.js'); -// ANSI Color Helpers +// ANSI Color Helpers for Terminal Output const cyan = (t) => `\x1b[36m${t}\x1b[0m`; const green = (t) => `\x1b[32m${t}\x1b[0m`; const red = (t) => `\x1b[31m${t}\x1b[0m`; @@ -22,12 +32,16 @@ console.log(bold(cyan('\n======================================================' console.log(bold(cyan(' šŸ›”ļø Agent-Proof Real-World Live Sandbox Verification '))); console.log(bold(cyan('======================================================\n'))); +// Create isolated temporary sandbox directory const sandboxDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-proof-real-repo-')); console.log(`šŸ“ Sandbox Repository: ${bold(sandboxDir)}\n`); +/** + * Clean up temporary sandbox directory on exit + */ function cleanup() { try { - // Unlock files before removing + // Unlock files before attempting directory deletion execFileSync(process.execPath, [binPath, 'unlock', sandboxDir], { stdio: 'ignore' }); } catch {} try { @@ -38,6 +52,9 @@ function cleanup() { process.on('exit', cleanup); process.on('SIGINT', () => { cleanup(); process.exit(1); }); +/** + * Helper to run a numbered verification step and measure elapsed time + */ async function runStep(stepNumber, title, fn) { console.log(bold(`[Step ${stepNumber}/6] ${title}...`)); const start = Date.now(); @@ -57,7 +74,7 @@ async function runStep(stepNumber, title, fn) { // STEP 1: Populate Sandbox with Multi-Stack Polyglot Repository // ----------------------------------------------------------------------------- await runStep(1, 'Creating Real-World Multi-Stack Project', async () => { - // Initialize git repo + // Initialize git repository execFileSync('git', ['init', sandboxDir], { stdio: 'pipe' }); execFileSync('git', ['-C', sandboxDir, 'config', 'user.name', 'Agent Tester'], { stdio: 'pipe' }); execFileSync('git', ['-C', sandboxDir, 'config', 'user.email', 'tester@heretek.ai'], { stdio: 'pipe' }); @@ -145,7 +162,7 @@ await runStep(2, 'Running `npx @heretek-ai/agent-proof init`', async () => { console.log(initOutput.split('\n').map(l => ` ${l}`).join('\n')); - // Assert expected configs exist + // Assert expected configuration files exist on disk const expectedFiles = [ 'lefthook.yml', '.claude/hooks.json', @@ -164,13 +181,13 @@ await runStep(2, 'Running `npx @heretek-ai/agent-proof init`', async () => { } } - // Validate lefthook.yml content + // Validate lefthook.yml content contains multi-stack engines const lefthookContent = fs.readFileSync(path.join(sandboxDir, 'lefthook.yml'), 'utf-8'); if (!lefthookContent.includes('biome-check') || !lefthookContent.includes('ruff-check') || !lefthookContent.includes('aislop-scan')) { throw new Error('lefthook.yml is missing required multi-stack engines'); } - // Validate .claude/hooks.json + // Validate .claude/hooks.json contains Stage 1 hooks const hooksJson = JSON.parse(fs.readFileSync(path.join(sandboxDir, '.claude/hooks.json'), 'utf-8')); if (!hooksJson.hooks?.PostFileEdit || !hooksJson.hooks?.PreCommit) { throw new Error('.claude/hooks.json is missing PostFileEdit or PreCommit hooks'); @@ -189,7 +206,7 @@ await runStep(3, 'Verifying Governance Permission Lock-in (chmod 0444)', async ( throw new Error(`.claude/hooks.json is writable! Expected read-only permissions (0444).`); } - // Test status command + // Test status CLI command const statusOutput = execFileSync(process.execPath, [binPath, 'status', sandboxDir], { encoding: 'utf-8', }); @@ -197,7 +214,7 @@ await runStep(3, 'Verifying Governance Permission Lock-in (chmod 0444)', async ( throw new Error(`Governance status check failed:\n${statusOutput}`); } - // Attempt unauthorized agent write + // Attempt unauthorized agent write and assert failure try { fs.writeFileSync(hooksPath, '{"tampered": true}'); throw new Error('Unauthorized agent write succeeded on locked file!'); diff --git a/src/cli.ts b/src/cli.ts index c4db26a..113a41a 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,267 +1,219 @@ -import * as fs from 'node:fs'; +/** + * @file src/cli.ts + * @description Main command-line interface entrypoint for @heretek-ai/agent-proof. + * + * Dispatches CLI commands: + * - init: Initialize mechanical hard gates in target repository + * - detect: Run multi-stack inspection and print findings + * - run : Execute a specific gate stage (post-edit, pre-commit, pre-push) + * - lock: Apply read-only permissions (0444) to governance configurations + * - unlock: Restore standard permissions (0644) for administrative edits + * - status: Print current permission and lock status of governance files + */ + import * as path from 'node:path'; -import { detectStack } from './detector/index.js'; -import { generateConfigs } from './generator/index.js'; -import { installHooks } from './installer/index.js'; -import { lockGovernance, unlockGovernance, getGovernanceStatus } from './installer/index.js'; -import { formatDiagnostics, DiagnosticStreamer } from './formatter/index.js'; -import { runGate } from './runner/index.js'; +import { detectStack } from './detector/stackDetector.js'; +import { generateConfigs } from './generator/configGenerator.js'; +import { installHooks } from './installer/hookInstaller.js'; +import { GateLock, lockGovernance, unlockGovernance } from './installer/lockin.js'; +import { GateRunner } from './runner/gateRunner.js'; -export async function runCli(argv: string[] = process.argv.slice(2)): Promise { - const command = argv[0] || 'init'; - const args = argv.slice(1); - - switch (command) { - case 'init': - case 'setup': { - return handleInit(args); - } - - case 'detect': { - return handleDetect(args); - } - - case 'run': { - return handleRun(args); - } - - case 'stream': - case 'format-diagnostics': { - return handleStream(args); - } - - case 'lock': { - return handleLock(args); - } - - case 'unlock': { - return handleUnlock(args); - } - - case 'status': { - return handleStatus(args); - } - - case '--help': - case '-h': - case 'help': { - printHelp(); - return 0; - } - - case '--version': - case '-v': - case 'version': { - console.log('@heretek-ai/agent-proof v1.0.0'); - return 0; - } - - default: { - // If run as `create-agent-gate`, treat unknown command as target directory or run init - if (command.startsWith('-')) { - console.error(`Unknown option: ${command}`); - printHelp(); - return 1; - } - return handleInit(argv); - } - } -} - -function handleInit(args: string[]): number { - const cwd = args.find(a => !a.startsWith('-')) || process.cwd(); - const overwrite = args.includes('--force') || args.includes('-f'); - const noLock = args.includes('--no-lock'); - - console.log('\nšŸ”’ Agent-Proof Mechanical Hard-Gate Initializer'); - console.log(`šŸ“‚ Repository Root: ${path.resolve(cwd)}\n`); - - console.log('šŸ” [1/4] Inspecting repository multi-stack architecture...'); - const detection = detectStack(cwd); - - console.log(` • Stacks detected: ${detection.summary.primaryStacks.join(', ') || 'Generic / Universal'}`); - if (detection.jsTs.detected) console.log(` - JS/TS: ${detection.jsTs.files.join(', ')} (TypeScript: ${detection.jsTs.hasTypeScript ? 'Yes' : 'No'}, Biome: ${detection.jsTs.hasBiome ? 'Yes' : 'No'})`); - if (detection.python.detected) console.log(` - Python: ${detection.python.files.join(', ')} (Ruff: ${detection.python.hasRuffConfig ? 'Yes' : 'No'})`); - if (detection.go.detected) console.log(` - Go: ${detection.go.files.join(', ')}`); - if (detection.rust.detected) console.log(` - Rust: ${detection.rust.files.join(', ')} (Workspace: ${detection.rust.isWorkspace ? 'Yes' : 'No'})`); - if (detection.infra.detected) console.log(` - Infra/CI: ${[...detection.infra.workflowFiles, ...detection.infra.dockerFiles].join(', ')}`); - if (detection.agentHarness.detected) console.log(` - Agent Harness: ${detection.agentHarness.files.join(', ')}`); - - console.log('\nāš™ļø [2/4] Emitting multi-tier pipeline governance configs...'); - const genResult = generateConfigs(detection, { cwd, overwrite }); - for (const written of genResult.writtenFiles) { - console.log(` āœ… Created ${written}`); - } - for (const skipped of genResult.skippedFiles) { - console.log(` ā­ļø Skipped ${skipped} (already exists, use --force to overwrite)`); - } - - console.log('\nšŸŖ [3/4] Initializing mechanical git hooks...'); - const hookResult = installHooks({ cwd }); - console.log(` • ${hookResult.message}`); - if (hookResult.installedHooks.length > 0) { - console.log(` • Active hooks: ${hookResult.installedHooks.join(', ')}`); - } - - console.log('\nšŸ›”ļø [4/4] Locking governance controls...'); - if (!noLock) { - const lockRes = lockGovernance(cwd); - if (lockRes.lockedFiles.length > 0) { - console.log(` šŸ”’ Immutable read-only lock applied to: ${lockRes.lockedFiles.join(', ')}`); - } - } else { - console.log(' āš ļø Governance locking skipped (--no-lock specified).'); - } - - console.log('\n✨ Repository is now Agent-Proof! Sub-second mechanical gates active.'); - console.log(' Stage 1: Agent PostFileEdit tool interceptor (.claude/hooks.json)'); - console.log(' Stage 2: Staged files pre-commit hard gate (< 2.0s via Lefthook)'); - console.log(' Stage 3: Pre-push / CI codebase graph & shadow API audit\n'); - - return 0; -} - -function handleDetect(args: string[]): number { - const cwd = args.find(a => !a.startsWith('-')) || process.cwd(); - const isJson = args.includes('--json'); - const detection = detectStack(cwd); - - if (isJson) { - console.log(JSON.stringify(detection, null, 2)); - } else { - console.log('Stack Detection Summary:'); - console.log(JSON.stringify(detection.summary, null, 2)); - console.log('\nDetailed Breakdown:'); - console.log(JSON.stringify({ - jsTs: detection.jsTs, - python: detection.python, - go: detection.go, - rust: detection.rust, - infra: detection.infra, - agentHarness: detection.agentHarness, - }, null, 2)); - } - return 0; -} - -function handleRun(args: string[]): number { - const stage = args[0] || 'pre-commit'; - const filePath = args.find((a, idx) => idx > 0 && !a.startsWith('-')); - const isJson = args.includes('--json') || args.includes('--lsp'); - const cwd = process.cwd(); - - const result = runGate({ - stage: stage as any, - filePath, - cwd, - jsonOutput: isJson, - }); - - if (isJson) { - DiagnosticStreamer.streamJson(result.envelope, process.stdout); - } else if (!result.passed) { - console.error(result.rawOutput); - // If failures occurred, also output structured diagnostic summary - if (result.envelope.diagnostics.length > 0) { - console.error('\nšŸ“‹ Agent Diagnostic Envelope:'); - DiagnosticStreamer.streamJson(result.envelope, process.stderr); - } - } else { - console.log(result.rawOutput || `āœ… Gate ${stage} passed successfully.`); - } - - return result.exitCode; -} - -function handleStream(args: string[]): number { - const toolName = getArgValue(args, '--tool') || 'gate'; - const stage = (getArgValue(args, '--stage') || 'PreCommit') as any; - - let rawInput = ''; - try { - rawInput = fs.readFileSync(0, 'utf-8'); - } catch { - // stdin empty - } - - const envelope = formatDiagnostics(rawInput, { - toolName, - stage, - }); - - DiagnosticStreamer.streamJson(envelope, process.stdout); - return envelope.summary.total_errors > 0 ? 1 : 0; -} - -function handleLock(args: string[]): number { - const cwd = args.find(a => !a.startsWith('-')) || process.cwd(); - const res = lockGovernance(cwd); - console.log(`Locked ${res.lockedFiles.length} governance files: ${res.lockedFiles.join(', ')}`); - if (res.failedFiles.length > 0) { - console.warn(`Failed to lock: ${res.failedFiles.join(', ')}`); - } - return 0; -} - -function handleUnlock(args: string[]): number { - const cwd = args.find(a => !a.startsWith('-')) || process.cwd(); - const res = unlockGovernance(cwd); - console.log(`Unlocked ${res.lockedFiles.length} governance files: ${res.lockedFiles.join(', ')}`); - if (res.failedFiles.length > 0) { - console.warn(`Failed to unlock: ${res.failedFiles.join(', ')}`); - } - return 0; -} - -function handleStatus(args: string[]): number { - const cwd = args.find(a => !a.startsWith('-')) || process.cwd(); - const status = getGovernanceStatus(cwd); - console.log('\nšŸ›”ļø Governance Lock Status:'); - for (const [file, info] of Object.entries(status)) { - console.log(` ${info.exists ? (info.isLocked ? 'šŸ”’ [LOCKED]' : 'šŸ”“ [UNLOCKED]') : 'āŒ [NOT FOUND]'} ${file}`); - } - console.log(''); - return 0; -} - -function getArgValue(args: string[], flag: string): string | undefined { - const idx = args.indexOf(flag); - if (idx >= 0 && idx + 1 < args.length) { - return args[idx + 1]; - } - return undefined; -} - -function printHelp(): void { +/** + * Display help text detailing usage, subcommands, and flags + */ +function printHelp() { console.log(` -@heretek-ai/agent-proof - Mechanical Hard-Gate for AI Coding Agents +šŸ›”ļø @heretek-ai/agent-proof — Mechanical Hard-Gate CLI for AI Code Governance USAGE: - npx @heretek-ai/agent-proof [options] - npx create-agent-proof [options] - npx create-agent-gate [options] - agent-proof [options] - agent-gate [options] + $ agent-proof [options] + $ agent-gate [options] COMMANDS: - init [dir] Initialize mechanical gates in target repository (default) - detect [dir] Inspect and report multi-stack indicators - run [file] Execute gate for stage (pre-commit, pre-push, post-edit) - format-diagnostics Parse stdin tool output and stream LSP JSON diagnostics - lock [dir] Set immutable read-only permissions on governance configs - unlock [dir] Restore write permissions on governance configs - status [dir] Check mechanical gate and lock health + init [dir] Initialize 3-tier mechanical hard gates in target repository + detect [dir] Inspect repository architecture and report detected stacks + run [file] Execute a mechanical gate stage (post-edit, pre-commit, pre-push) + lock [dir] Lock governance files with read-only permissions (chmod 0444) + unlock [dir] Unlock governance files for administrative editing (chmod 0644) + status [dir] Display permission and lock status of governance configurations OPTIONS: - --force, -f Overwrite existing configuration files - --no-lock Do not set read-only permissions on governance files - --json, --lsp Output structured LSP JSON diagnostic envelopes - --help, -h Show help information - --version, -v Show version number + -f, --force Overwrite existing configuration files during init + -v, --version Show CLI version + -h, --help Show this help message + +EXAMPLES: + $ npx @heretek-ai/agent-proof init + $ npx @heretek-ai/agent-proof run pre-commit + $ npx @heretek-ai/agent-proof run post-edit src/auth.ts + $ npx @heretek-ai/agent-proof status `); } -// Auto-run if executed directly +/** + * CLI Main Dispatcher + */ +export async function main(argv: string[] = process.argv.slice(2)): Promise { + const args = argv.filter(a => !a.startsWith('-')); + const flags = new Set(argv.filter(a => a.startsWith('-'))); + + if (flags.has('-h') || flags.has('--help') || (args.length === 0 && flags.size === 0)) { + printHelp(); + return; + } + + if (flags.has('-v') || flags.has('--version')) { + console.log('@heretek-ai/agent-proof v1.0.0'); + return; + } + + const command = args[0] || 'init'; + const targetDir = args[1] && !['post-edit', 'pre-commit', 'pre-push'].includes(args[1]) + ? path.resolve(args[1]) + : process.cwd(); + + switch (command) { + // ------------------------------------------------------------------------- + // INIT: Initialize full 3-tier hard gates + // ------------------------------------------------------------------------- + case 'init': { + console.log(`\nšŸ”’ Agent-Proof Mechanical Hard-Gate Initializer`); + console.log(`šŸ“‚ Repository Root: ${targetDir}\n`); + + // 1. Multi-Stack Inspection + console.log(`šŸ” [1/4] Inspecting repository multi-stack architecture...`); + const detection = detectStack(targetDir); + console.log(` • Stacks detected: ${detection.summary.primaryStacks.join(', ') || 'Generic'}`); + if (detection.jsTs.detected) { + console.log(` - JS/TS: ${detection.jsTs.files.join(', ')} (TypeScript: ${detection.jsTs.hasTypeScript ? 'Yes' : 'No'}, Biome: ${detection.jsTs.hasBiome ? 'Yes' : 'No'})`); + } + if (detection.python.detected) { + console.log(` - Python: ${detection.python.files.join(', ')} (Ruff: ${detection.python.hasRuffConfig ? 'Yes' : 'No'})`); + } + if (detection.infra.detected) { + console.log(` - Infra/CI: ${[...detection.infra.workflowFiles, ...detection.infra.dockerFiles].join(', ')}`); + } + if (detection.agentHarness.detected) { + console.log(` - Agent Harness: ${detection.agentHarness.files.join(', ')}`); + } + + // 2. Multi-Tier Config Emission + console.log(`\nāš™ļø [2/4] Emitting multi-tier pipeline governance configs...`); + const overwrite = flags.has('-f') || flags.has('--force'); + const genResult = generateConfigs(detection, { cwd: targetDir, overwrite }); + for (const written of genResult.writtenFiles) { + console.log(` āœ… Created ${written}`); + } + for (const skipped of genResult.skippedFiles) { + console.log(` ā© Skipped existing ${skipped} (use --force to overwrite)`); + } + + // 3. Git Hooks Installation + console.log(`\nšŸŖ [3/4] Initializing mechanical git hooks...`); + const hookResult = installHooks(targetDir); + console.log(` • ${hookResult.message}`); + if (hookResult.installedHooks.length > 0) { + console.log(` • Active hooks: ${hookResult.installedHooks.join(', ')}`); + } + + // 4. Governance Lock-in + console.log(`\nšŸ›”ļø [4/4] Locking governance controls...`); + const lockResult = lockGovernance(targetDir); + if (lockResult.lockedFiles.length > 0) { + console.log(` šŸ”’ Immutable read-only lock applied to: ${lockResult.lockedFiles.join(', ')}`); + } + + console.log(`\n✨ Repository is now Agent-Proof! Sub-second mechanical gates active.`); + console.log(` Stage 1: Agent PostFileEdit tool interceptor (.claude/hooks.json)`); + console.log(` Stage 2: Staged files pre-commit hard gate (< 2.0s via Lefthook)`); + console.log(` Stage 3: Pre-push / CI codebase graph & shadow API audit\n`); + break; + } + + // ------------------------------------------------------------------------- + // DETECT: Inspect and print detected stacks + // ------------------------------------------------------------------------- + case 'detect': { + const detection = detectStack(targetDir); + console.log(JSON.stringify(detection, null, 2)); + break; + } + + // ------------------------------------------------------------------------- + // RUN: Execute a specific gate stage + // ------------------------------------------------------------------------- + case 'run': { + const stage = args[1] || 'pre-commit'; + const runner = new GateRunner({ cwd: targetDir }); + + if (stage === 'post-edit') { + const filePath = args[2]; + if (!filePath) { + console.error('Error: filePath argument required for post-edit stage'); + process.exit(1); + } + const envelope = runner.runPostEdit(filePath); + console.log(JSON.stringify(envelope, null, 2)); + if (envelope.status === 'GATE_FAILED') process.exit(1); + } else if (stage === 'pre-commit') { + const envelope = runner.runPreCommit(); + console.log(JSON.stringify(envelope, null, 2)); + if (envelope.status === 'GATE_FAILED') process.exit(1); + } else if (stage === 'pre-push') { + const envelope = runner.runPrePush(); + console.log(JSON.stringify(envelope, null, 2)); + if (envelope.status === 'GATE_FAILED') process.exit(1); + } else { + console.error(`Unknown stage: ${stage}. Expected: post-edit, pre-commit, pre-push`); + process.exit(1); + } + break; + } + + // ------------------------------------------------------------------------- + // LOCK / UNLOCK / STATUS: POSIX governance permissions + // ------------------------------------------------------------------------- + case 'lock': { + const result = lockGovernance(targetDir); + console.log(`šŸ”’ Locked ${result.lockedFiles.length} governance files: ${result.lockedFiles.join(', ')}`); + break; + } + + case 'unlock': { + const result = unlockGovernance(targetDir); + console.log(`šŸ”“ Unlocked ${result.lockedFiles.length} governance files: ${result.lockedFiles.join(', ')}`); + break; + } + + case 'status': { + const lock = new GateLock({ cwd: targetDir }); + const statuses = lock.checkStatus(); + console.log(`\nšŸ›”ļø Governance Configuration Status:`); + for (const s of statuses) { + if (!s.exists) { + console.log(` ⚪ [NOT FOUND] ${s.path}`); + } else if (s.isLocked) { + console.log(` šŸ”’ [LOCKED] ${s.path} (mode: ${s.mode})`); + } else { + console.log(` āš ļø [UNLOCKED] ${s.path} (mode: ${s.mode})`); + } + } + console.log(''); + break; + } + + default: { + console.error(`Unknown command: ${command}`); + printHelp(); + process.exit(1); + } + } +} + +// Execute main if invoked directly from CLI if (process.argv[1] && (process.argv[1].endsWith('cli.js') || process.argv[1].endsWith('cli.ts'))) { - runCli().then(code => process.exit(code)); + main().catch(err => { + console.error(`Fatal CLI Error:`, err); + process.exit(1); + }); } diff --git a/src/detector/index.ts b/src/detector/index.ts index 4e727a6..fc3ed27 100644 --- a/src/detector/index.ts +++ b/src/detector/index.ts @@ -1 +1,6 @@ +/** + * @file src/detector/index.ts + * @description Exports the repository stack detection module. + */ + export * from './stackDetector.js'; diff --git a/src/detector/stackDetector.ts b/src/detector/stackDetector.ts index 541c130..610bc55 100644 --- a/src/detector/stackDetector.ts +++ b/src/detector/stackDetector.ts @@ -1,22 +1,49 @@ +/** + * @file src/detector/stackDetector.ts + * @description Multi-stack auto-detection engine for repository inspection. + * + * Scans the filesystem to detect active programming languages, build systems, + * package managers, container infrastructure, CI workflows, and AI agent harnesses. + * Provides deterministic metadata to configure mechanical hard gates. + */ + import * as fs from 'node:fs'; import * as path from 'node:path'; import type { StackDetectionResult } from '../types/index.js'; +/** + * Options for configuring the StackDetector instance + */ export interface StackDetectorOptions { + /** Working directory to inspect (defaults to process.cwd()) */ cwd?: string; } +/** + * Multi-Stack Auto-Detection Engine. + * Inspects a repository directory to identify language ecosystems, + * monorepos, workflows, and agent harnesses. + */ export class StackDetector { + /** Resolved absolute path of the target repository root */ private readonly rootPath: string; + /** + * Initialize a new StackDetector instance + * @param options Configuration options including target working directory + */ constructor(options: StackDetectorOptions = {}) { this.rootPath = path.resolve(options.cwd || process.cwd()); } /** - * Run full stack detection across the target repository + * Run full multi-stack detection across the target repository. + * Inspects JS/TS, Python, Go, Rust, Infrastructure, and Agent Harnesses. + * + * @returns Comprehensive StackDetectionResult object with boolean flags and file lists. */ public detect(): StackDetectionResult { + // 1. Inspect individual technology stacks const jsTs = this.detectJsTs(); const python = this.detectPython(); const go = this.detectGo(); @@ -24,6 +51,7 @@ export class StackDetector { const infra = this.detectInfra(); const agentHarness = this.detectAgentHarness(); + // 2. Assemble high-level primary stack list for summary reporting const primaryStacks: string[] = []; if (jsTs.detected) primaryStacks.push('JavaScript/TypeScript'); if (python.detected) primaryStacks.push('Python'); @@ -32,6 +60,7 @@ export class StackDetector { if (infra.detected) primaryStacks.push('Workflows/Infra'); if (agentHarness.detected) primaryStacks.push('Agent Harness'); + // 3. Compute total indicator count across all detected categories const totalIndicators = jsTs.files.length + python.files.length + @@ -56,7 +85,12 @@ export class StackDetector { }; } + /** + * Detect JavaScript and TypeScript ecosystem indicators. + * Checks for package manifests, TypeScript configs, Biome configs, and monorepo configurations. + */ private detectJsTs() { + // Canonical indicator files for JS/TS projects and package managers const indicators = [ 'package.json', 'tsconfig.json', @@ -77,20 +111,24 @@ export class StackDetector { let isMonorepo = false; let packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'deno' | undefined; + // Check presence of indicator files on disk for (const indicator of indicators) { if (this.fileExists(indicator)) { detectedFiles.push(indicator); } } + // Check for explicit TypeScript config if (this.fileExists('tsconfig.json')) { hasTypeScript = true; } + // Check for existing Biome configuration if (this.fileExists('biome.json') || this.fileExists('biome.jsonc')) { hasBiome = true; } + // Determine package manager by lockfile priority if (this.fileExists('pnpm-lock.yaml')) { packageManager = 'pnpm'; } else if (this.fileExists('yarn.lock')) { @@ -103,10 +141,12 @@ export class StackDetector { packageManager = 'npm'; } + // Determine monorepo workspace indicators (pnpm, lerna, turbo, nx) if (this.fileExists('pnpm-workspace.yaml') || this.fileExists('lerna.json') || this.fileExists('turbo.json') || this.fileExists('nx.json')) { isMonorepo = true; } else if (this.fileExists('package.json')) { try { + // Read package.json to check for workspaces, dependencies, and packageManager field const pkgContent = JSON.parse(fs.readFileSync(this.resolvePath('package.json'), 'utf-8')); if (pkgContent.workspaces) { isMonorepo = true; @@ -124,7 +164,7 @@ export class StackDetector { } } } catch { - // ignore parse error + // Silently continue if package.json cannot be parsed } } @@ -138,6 +178,10 @@ export class StackDetector { }; } + /** + * Detect Python ecosystem indicators. + * Checks for pyproject.toml, requirements.txt, Pipfile, poetry, and Ruff linter configurations. + */ private detectPython() { const indicators = [ 'pyproject.toml', @@ -157,12 +201,14 @@ export class StackDetector { let hasPyproject = false; let hasRuffConfig = false; + // Check presence of Python indicator files for (const indicator of indicators) { if (this.fileExists(indicator)) { detectedFiles.push(indicator); } } + // Inspect pyproject.toml for [tool.ruff] table if (this.fileExists('pyproject.toml')) { hasPyproject = true; try { @@ -171,10 +217,11 @@ export class StackDetector { hasRuffConfig = true; } } catch { - // ignore read error + // Silently continue if pyproject.toml cannot be read } } + // Check for dedicated ruff.toml configuration if (this.fileExists('ruff.toml') || this.fileExists('.ruff.toml')) { hasRuffConfig = true; } @@ -187,6 +234,9 @@ export class StackDetector { }; } + /** + * Detect Go ecosystem indicators (go.mod, go.sum, Gopkg.toml). + */ private detectGo() { const indicators = ['go.mod', 'go.sum', 'Gopkg.toml']; const detectedFiles: string[] = []; @@ -203,6 +253,9 @@ export class StackDetector { }; } + /** + * Detect Rust ecosystem indicators (Cargo.toml, Cargo.lock, multi-crate workspaces). + */ private detectRust() { const indicators = ['Cargo.toml', 'Cargo.lock']; const detectedFiles: string[] = []; @@ -214,6 +267,7 @@ export class StackDetector { } } + // Check if Cargo.toml defines a [workspace] if (this.fileExists('Cargo.toml')) { try { const content = fs.readFileSync(this.resolvePath('Cargo.toml'), 'utf-8'); @@ -221,7 +275,7 @@ export class StackDetector { isWorkspace = true; } } catch { - // ignore read error + // Silently continue if Cargo.toml cannot be read } } @@ -232,10 +286,14 @@ export class StackDetector { }; } + /** + * Detect Infrastructure, CI workflows, and Docker container definitions. + */ private detectInfra() { const workflowFiles: string[] = []; const dockerFiles: string[] = []; + // Scan .github/workflows directory for YAML workflow files const workflowsDir = this.resolvePath('.github/workflows'); if (this.dirExists('.github/workflows')) { try { @@ -246,10 +304,11 @@ export class StackDetector { } } } catch { - // ignore read error + // Silently continue if workflows directory cannot be read } } + // Check for Docker and container configuration files const dockerIndicators = [ 'Dockerfile', 'Containerfile', @@ -275,6 +334,9 @@ export class StackDetector { }; } + /** + * Detect AI agent harness files, skill markdown definitions, and instructions. + */ private detectAgentHarness() { const files: string[] = []; let hasClaude = false; @@ -282,6 +344,7 @@ export class StackDetector { let hasSkillFiles = false; let hasAgentsMd = false; + // Check for Claude Code harness directory and settings if (this.dirExists('.claude')) { hasClaude = true; files.push('.claude/'); @@ -289,27 +352,32 @@ export class StackDetector { if (this.fileExists('.claude/hooks.json')) files.push('.claude/hooks.json'); } + // Check for CLAUDE.md instruction file if (this.fileExists('CLAUDE.md')) { hasClaude = true; files.push('CLAUDE.md'); } + // Check for Cursor IDE rules and directory if (this.dirExists('.cursor') || this.fileExists('.cursorrules')) { hasCursor = true; if (this.dirExists('.cursor')) files.push('.cursor/'); if (this.fileExists('.cursorrules')) files.push('.cursorrules'); } + // Check for AGENTS.md multi-agent manifest if (this.fileExists('AGENTS.md')) { hasAgentsMd = true; files.push('AGENTS.md'); } + // Check for root SKILL.md definition if (this.fileExists('SKILL.md')) { hasSkillFiles = true; files.push('SKILL.md'); } + // Check for .claude/skills/*.md individual skill definitions const skillsDir = this.resolvePath('.claude/skills'); if (this.dirExists('.claude/skills')) { try { @@ -321,7 +389,7 @@ export class StackDetector { } } } catch { - // ignore read error + // Silently continue if skills directory cannot be read } } @@ -335,6 +403,9 @@ export class StackDetector { }; } + /** + * Helper to check if a relative path exists and is a file + */ private fileExists(relPath: string): boolean { const fullPath = this.resolvePath(relPath); try { @@ -344,6 +415,9 @@ export class StackDetector { } } + /** + * Helper to check if a relative path exists and is a directory + */ private dirExists(relPath: string): boolean { const fullPath = this.resolvePath(relPath); try { @@ -353,11 +427,18 @@ export class StackDetector { } } + /** + * Resolve a relative path against the repository root + */ private resolvePath(relPath: string): string { return path.resolve(this.rootPath, relPath); } } +/** + * Functional convenience wrapper to detect stacks in a repository + * @param cwd Target directory (defaults to current working directory) + */ export function detectStack(cwd?: string): StackDetectionResult { return new StackDetector({ cwd }).detect(); } diff --git a/src/formatter/ansi.ts b/src/formatter/ansi.ts index 362432e..94f9018 100644 --- a/src/formatter/ansi.ts +++ b/src/formatter/ansi.ts @@ -1,5 +1,16 @@ +/** + * @file src/formatter/ansi.ts + * @description ANSI escape sequence stripper. + * + * Terminal tools emit colored ANSI escape sequences that confuse LLMs and break + * JSON parsing. This utility cleanly strips ANSI color and cursor codes from strings. + */ + /** * Strips ANSI escape sequences and terminal control characters from strings. + * + * @param text Raw terminal string containing ANSI escape codes + * @returns Clean text with all ANSI escape sequences removed */ export function stripAnsi(text: string): string { if (!text) return ''; diff --git a/src/formatter/diagnosticStream.ts b/src/formatter/diagnosticStream.ts index 638fb01..be4c998 100644 --- a/src/formatter/diagnosticStream.ts +++ b/src/formatter/diagnosticStream.ts @@ -1,10 +1,15 @@ -import type { - DiagnosticEnvelope, - DiagnosticItem, - DiagnosticSeverity, - GateStage, -} from '../types/index.js'; -import { stripAnsi } from './ansi.js'; +/** + * @file src/formatter/diagnosticStream.ts + * @description LSP-compliant Diagnostic Streaming Engine. + * + * Intercepts, deduplicates, and aggregates outputs from all governance tools + * (AISlop, Biome, Ruff, SkillCheck, TruffleHog, Typos, Actionlint) and formats + * them into a standardized JSON Diagnostic Envelope ($schema: https://json.schemastore.org/lsif.json). + * + * Embeds actionable `repair_tokens` enabling autonomous LLM self-correction. + */ + +import type { DiagnosticEnvelope, DiagnosticItem, GateStage } from '../types/index.js'; import { parseAislopOutput } from './parsers/aislop.js'; import { parseBiomeOutput } from './parsers/biome.js'; import { parseRuffOutput } from './parsers/ruff.js'; @@ -14,120 +19,110 @@ import { parseTyposOutput } from './parsers/typos.js'; import { parseActionlintOutput } from './parsers/actionlint.js'; import { parseGenericOutput } from './parsers/generic.js'; -export interface FormatOptions { +/** + * Options for configuring DiagnosticStreamer formatting + */ +export interface DiagnosticStreamerOptions { + /** Target gate stage (PostFileEdit, PreCommit, PrePush, CI) */ stage?: GateStage; - toolName?: string; + /** Execution duration in milliseconds */ executionTimeMs?: number; - toolOutputs?: Record; + /** Optional tool name override */ + toolName?: string; } +/** + * Raw output payload from an individual tool execution + */ +export interface ToolExecutionResult { + /** Identifier of the tool (e.g. 'aislop', 'biome', 'ruff', 'trufflehog') */ + toolName: string; + /** Combined stdout and stderr string */ + output: string; + /** Process exit status code */ + exitCode: number; +} + +/** + * LSP Diagnostic Streamer and Aggregator. + * Routes tool outputs to specialized parsers and constructs JSON diagnostic envelopes. + */ export class DiagnosticStreamer { /** - * Parse raw output from a tool and return an LSP-compliant DiagnosticEnvelope + * Route tool output to the appropriate specialized parser based on tool name heuristics. + * + * @param toolName Name of the executed tool + * @param output Raw terminal output string + * @returns Array of parsed DiagnosticItem objects */ - public static format(rawOutput: string, options: FormatOptions = {}): DiagnosticEnvelope { - const stage = options.stage || 'PreCommit'; - const toolName = (options.toolName || '').toLowerCase().trim(); - const cleanOutput = stripAnsi(rawOutput).trim(); + public static parseToolOutput(toolName: string, output: string): DiagnosticItem[] { + const lower = toolName.toLowerCase(); - if (!cleanOutput) { - return { - $schema: 'https://json.schemastore.org/lsif.json', - version: '1.0.0', - status: 'GATE_PASSED', - summary: { - total_errors: 0, - total_warnings: 0, - gate_stage: stage, - }, - diagnostics: [], - metadata: { - execution_time_ms: options.executionTimeMs, - tool_outputs: options.toolOutputs, - }, - }; + // Check specific tools in deterministic priority order + if (lower.includes('aislop')) { + return parseAislopOutput(output); + } + if (lower.includes('biome')) { + return parseBiomeOutput(output); + } + if (lower.includes('trufflehog')) { + return parseTrufflehogOutput(output); + } + if (lower.includes('ruff')) { + return parseRuffOutput(output); + } + if (lower.includes('skill') || lower.includes('skillcheck')) { + return parseSkillcheckOutput(output); + } + if (lower.includes('typo')) { + return parseTyposOutput(output); + } + if (lower.includes('actionlint')) { + return parseActionlintOutput(output); } - let diagnostics: DiagnosticItem[] = []; - - // Route to appropriate parser with strict naming order - if (toolName === 'trufflehog' || toolName.includes('trufflehog') || cleanOutput.includes('trufflehog') || cleanOutput.includes('DetectorName')) { - diagnostics = parseTrufflehogOutput(cleanOutput); - } else if (toolName === 'aislop' || toolName.includes('aislop') || cleanOutput.includes('aislop') || cleanOutput.includes('AI_SLOP')) { - diagnostics = parseAislopOutput(cleanOutput); - } else if (toolName === 'biome' || toolName.includes('biome') || cleanOutput.includes('biome') || cleanOutput.includes('noExplicitAny')) { - diagnostics = parseBiomeOutput(cleanOutput); - } else if (toolName === 'ruff' || (toolName !== 'trufflehog' && toolName.includes('ruff')) || cleanOutput.includes('ruff') || /F\d{3}|E\d{3}|B\d{3}/.test(cleanOutput)) { - diagnostics = parseRuffOutput(cleanOutput); - } else if (toolName === 'skillcheck' || toolName.includes('skillcheck') || cleanOutput.includes('skillcheck') || cleanOutput.includes('SKILL.md')) { - diagnostics = parseSkillcheckOutput(cleanOutput); - } else if (toolName === 'typos' || toolName.includes('typos') || /should be/.test(cleanOutput)) { - diagnostics = parseTyposOutput(cleanOutput); - } else if (toolName === 'actionlint' || toolName.includes('actionlint')) { - diagnostics = parseActionlintOutput(cleanOutput); - } else { - diagnostics = parseGenericOutput(cleanOutput, options.toolName || 'gate'); - } - - // If specialized parser returned no items, try fallback - if (diagnostics.length === 0 && cleanOutput.length > 0) { - diagnostics = parseGenericOutput(cleanOutput, options.toolName || 'gate'); - } - - const totalErrors = diagnostics.filter(d => d.severity === 'ERROR').length; - const totalWarnings = diagnostics.filter(d => d.severity === 'WARNING').length; - - return { - $schema: 'https://json.schemastore.org/lsif.json', - version: '1.0.0', - status: totalErrors > 0 ? 'GATE_FAILED' : 'GATE_PASSED', - summary: { - total_errors: totalErrors, - total_warnings: totalWarnings, - gate_stage: stage, - }, - diagnostics, - metadata: { - execution_time_ms: options.executionTimeMs, - tool_outputs: options.toolOutputs, - }, - }; + return parseGenericOutput(output, toolName); } /** - * Aggregate multiple tool results into a single DiagnosticEnvelope + * Aggregate multiple tool execution results into a unified DiagnosticEnvelope. + * + * @param results Array of ToolExecutionResult objects + * @param options Formatting options + * @returns Complete DiagnosticEnvelope conforming to LSIF JSON schema */ public static aggregate( - toolResults: Array<{ toolName: string; output: string; exitCode: number }>, - options: FormatOptions = {} + results: ToolExecutionResult[], + options: DiagnosticStreamerOptions = {} ): DiagnosticEnvelope { const stage = options.stage || 'PreCommit'; const allDiagnostics: DiagnosticItem[] = []; const toolOutputs: Record = {}; - for (const res of toolResults) { + for (const res of results) { toolOutputs[res.toolName] = { exitCode: res.exitCode, - stderr: res.exitCode !== 0 ? res.output : '', - stdout: res.exitCode === 0 ? res.output : '', + stderr: res.output, + stdout: '', }; - if (res.exitCode !== 0) { - const envelope = DiagnosticStreamer.format(res.output, { - stage, - toolName: res.toolName, - }); - allDiagnostics.push(...envelope.diagnostics); + // Only parse diagnostics if tool exited with non-zero or output is present + if (res.exitCode !== 0 || res.output.trim().length > 0) { + const diags = this.parseToolOutput(res.toolName, res.output); + allDiagnostics.push(...diags); } } const totalErrors = allDiagnostics.filter(d => d.severity === 'ERROR').length; const totalWarnings = allDiagnostics.filter(d => d.severity === 'WARNING').length; + const status = (totalErrors > 0 || results.some(r => r.exitCode !== 0 && allDiagnostics.length === 0)) + ? 'GATE_FAILED' + : 'GATE_PASSED'; return { $schema: 'https://json.schemastore.org/lsif.json', version: '1.0.0', - status: totalErrors > 0 ? 'GATE_FAILED' : 'GATE_PASSED', + status, summary: { total_errors: totalErrors, total_warnings: totalWarnings, @@ -140,15 +135,23 @@ export class DiagnosticStreamer { }, }; } - - /** - * Output JSON diagnostic envelope to stdout/stderr - */ - public static streamJson(envelope: DiagnosticEnvelope, outStream: NodeJS.WritableStream = process.stderr): void { - outStream.write(JSON.stringify(envelope, null, 2) + '\n'); - } } -export function formatDiagnostics(rawOutput: string, options?: FormatOptions): DiagnosticEnvelope { - return DiagnosticStreamer.format(rawOutput, options); +/** + * Functional convenience wrapper to format a single tool output string into a DiagnosticEnvelope + * + * @param rawOutput Raw error string + * @param options Formatter options + * @param toolName Optional tool identifier + */ +export function formatDiagnostics( + rawOutput: string, + options: DiagnosticStreamerOptions = {}, + toolName?: string +): DiagnosticEnvelope { + const effectiveToolName = options.toolName || toolName || 'gate'; + return DiagnosticStreamer.aggregate( + [{ toolName: effectiveToolName, output: rawOutput, exitCode: rawOutput.trim().length > 0 ? 1 : 0 }], + options + ); } diff --git a/src/formatter/index.ts b/src/formatter/index.ts index 2ff625b..ec5dddd 100644 --- a/src/formatter/index.ts +++ b/src/formatter/index.ts @@ -1,3 +1,8 @@ +/** + * @file src/formatter/index.ts + * @description Exports diagnostic streaming and output formatting modules. + */ + export * from './ansi.js'; export * from './diagnosticStream.js'; export * from './parsers/aislop.js'; diff --git a/src/formatter/parsers/actionlint.ts b/src/formatter/parsers/actionlint.ts index 8f258a4..15032a6 100644 --- a/src/formatter/parsers/actionlint.ts +++ b/src/formatter/parsers/actionlint.ts @@ -1,39 +1,45 @@ +/** + * @file src/formatter/parsers/actionlint.ts + * @description Actionlint output parser for GitHub Actions workflow syntax validation. + */ + import type { DiagnosticItem } from '../../types/index.js'; import { stripAnsi } from '../ansi.js'; +/** + * Parse raw stdout/stderr from `actionlint` into structured DiagnosticItems. + * + * @param rawOutput Raw terminal output from actionlint + * @returns Array of DiagnosticItem objects with workflow repair instructions + */ export function parseActionlintOutput(rawOutput: string): DiagnosticItem[] { const clean = stripAnsi(rawOutput); const diagnostics: DiagnosticItem[] = []; - const lines = clean.split('\n'); + for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; - // Pattern: .github/workflows/ci.yml:15:3: message [rule-name] - const match = trimmed.match(/^([^\s:]+\.ya?ml):(\d+):(\d+):\s*(.*?)(?:\s*\[([^\]]+)\])?$/); + // Pattern: .github/workflows/ci.yml:12:7: message [rule-id] + const match = trimmed.match(/^([^\s:]+):(\d+):(\d+):\s*(.*?)(?:\s*\[([^\]]+)\])?$/); if (match) { - const [, filePath, lineStr, colStr, msg, rule] = match; - const lineNum = parseInt(lineStr, 10); - const colNum = parseInt(colStr, 10); - const ruleId = rule ? `ACTIONLINT_${rule.toUpperCase().replace(/[^A-Z0-9_]/g, '_')}` : 'ACTIONLINT_SYNTAX_ERROR'; - + const [, filePath, lineStr, colStr, message, ruleId] = match; + const effectiveRuleId = ruleId || 'ACTIONLINT_SYNTAX_ERROR'; diagnostics.push({ source: 'actionlint', - rule_id: ruleId, + rule_id: effectiveRuleId, severity: 'ERROR', file_path: filePath, range: { - start: { line: lineNum, column: colNum }, - end: { line: lineNum, column: colNum + 20 }, + start: { line: parseInt(lineStr, 10), column: parseInt(colStr, 10) }, + end: { line: parseInt(lineStr, 10), column: parseInt(colStr, 10) + 20 }, }, - error_message: msg.trim() || 'GitHub Actions workflow validation error', + error_message: message.trim(), repair_instruction: { action: 'REWRITE_BLOCK', - description: 'Fix the GitHub Actions workflow syntax error according to official GitHub Actions schema.', - repair_tokens: [ - '# Check workflow syntax against GitHub Actions schema documentation', - ], + description: `Fix GitHub Actions workflow syntax error: ${message.trim()}`, + repair_tokens: [`# Correct workflow step syntax in ${filePath}`], }, }); } diff --git a/src/formatter/parsers/aislop.ts b/src/formatter/parsers/aislop.ts index 88819d5..021cd7a 100644 --- a/src/formatter/parsers/aislop.ts +++ b/src/formatter/parsers/aislop.ts @@ -1,11 +1,26 @@ +/** + * @file src/formatter/parsers/aislop.ts + * @description AISlop output parser for deterministic AI code smells and slop detection. + * + * Extracts violations for swallowed errors, empty catch blocks, hallucinated imports, + * dead code, and unsafe type casts, and generates actionable repair tokens for LLMs. + */ + import type { DiagnosticItem } from '../../types/index.js'; import { stripAnsi } from '../ansi.js'; +/** + * Parse raw output from the `aislop` CLI tool into structured DiagnosticItems. + * Supports both JSON payload formats and standard CLI text output. + * + * @param rawOutput Raw stdout/stderr string from aislop + * @returns Array of DiagnosticItem objects with rule IDs and repair instructions + */ export function parseAislopOutput(rawOutput: string): DiagnosticItem[] { const clean = stripAnsi(rawOutput); const diagnostics: DiagnosticItem[] = []; - // Check if output is JSON + // 1. Check if output is JSON if (clean.trim().startsWith('{') || clean.trim().startsWith('[')) { try { const parsed = JSON.parse(clean); @@ -29,21 +44,18 @@ export function parseAislopOutput(rawOutput: string): DiagnosticItem[] { } if (diagnostics.length > 0) return diagnostics; } catch { - // Continue to regex parser + // Continue to regex parser if JSON parse fails } } - // Regex parser for CLI textual output - // Example pattern 1: [AISLOP] (AI_SLOP_SWALLOWED_ERROR) src/controllers/auth.ts:88:7: Empty catch block silently suppresses error - // Example pattern 2: src/controllers/auth.ts:88:7: [AI_SLOP_SWALLOWED_ERROR] Empty catch block - // Example pattern 3: AI_SLOP_SWALLOWED_ERROR at src/auth.ts:88: Empty catch block + // 2. Regex parser for CLI textual output const lines = clean.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; - // Pattern: filePath:line:col: [RULE_ID] message or error message + // Pattern 1: filePath:line:col: [RULE_ID] message const match1 = line.match(/^([^\s:]+):(\d+):(\d+):\s*(?:\[([^\]]+)\])?\s*(.*)$/); if (match1) { const [, filePath, lineStr, colStr, ruleId, msg] = match1; @@ -63,7 +75,7 @@ export function parseAislopOutput(rawOutput: string): DiagnosticItem[] { continue; } - // Pattern: [AISLOP] (RULE_ID) file:line - message + // Pattern 2: [aislop] (RULE_ID) file:line - message const match2 = line.match(/\[aislop\]\s*(?:\(([^)]+)\))?\s*([^\s:]+)(?::(\d+))?(?::(\d+))?[:\s-]+(.*)/i); if (match2) { const [, ruleId, filePath, lineStr, colStr, msg] = match2; @@ -85,7 +97,7 @@ export function parseAislopOutput(rawOutput: string): DiagnosticItem[] { continue; } - // Pattern: AI Slop score exceeded + // Pattern 3: AI Slop score threshold exceeded if (line.toLowerCase().includes('slop score') || line.toLowerCase().includes('fail-on')) { diagnostics.push({ source: 'aislop', @@ -107,6 +119,9 @@ export function parseAislopOutput(rawOutput: string): DiagnosticItem[] { return diagnostics; } +/** + * Infer rule identifier from error message text + */ function detectRuleFromMessage(msg: string = ''): string { const m = msg.toLowerCase(); if (m.includes('catch') || m.includes('swallow') || m.includes('silent')) { @@ -124,6 +139,9 @@ function detectRuleFromMessage(msg: string = ''): string { return 'AI_SLOP_PATTERN_DETECTED'; } +/** + * Generate actionable repair tokens based on the identified AISlop rule + */ function generateAislopRepair(ruleId: string = '', snippet?: string): DiagnosticItem['repair_instruction'] { const r = ruleId.toUpperCase(); if (r.includes('SWALLOW') || r.includes('CATCH') || r.includes('EMPTY')) { diff --git a/src/formatter/parsers/biome.ts b/src/formatter/parsers/biome.ts index 7b4f2fe..5399a37 100644 --- a/src/formatter/parsers/biome.ts +++ b/src/formatter/parsers/biome.ts @@ -1,122 +1,111 @@ +/** + * @file src/formatter/parsers/biome.ts + * @description Biome output parser for JS/TS linter and formatter diagnostics. + */ + import type { DiagnosticItem } from '../../types/index.js'; import { stripAnsi } from '../ansi.js'; +/** + * Parse raw stdout/stderr from `biome check` into structured DiagnosticItems. + * Supports both Biome JSON output and human-readable CLI terminal diagnostics. + * + * @param rawOutput Raw terminal output from Biome + * @returns Array of DiagnosticItem objects with rule IDs and repair tokens + */ export function parseBiomeOutput(rawOutput: string): DiagnosticItem[] { const clean = stripAnsi(rawOutput); const diagnostics: DiagnosticItem[] = []; - // Check if JSON output + // 1. Check if output is Biome JSON format if (clean.trim().startsWith('{')) { try { - const data = JSON.parse(clean); - if (data.diagnostics && Array.isArray(data.diagnostics)) { - for (const diag of data.diagnostics) { + const parsed = JSON.parse(clean); + if (parsed.diagnostics && Array.isArray(parsed.diagnostics)) { + for (const diag of parsed.diagnostics) { const filePath = diag.location?.path?.file || 'unknown'; - const startLine = diag.location?.span?.start?.line || 1; - const startCol = diag.location?.span?.start?.column || 1; - const endLine = diag.location?.span?.end?.line || startLine; - const endCol = diag.location?.span?.end?.column || startCol; - const ruleId = diag.category || 'biome/lint'; - const message = diag.description || diag.message || 'Biome lint violation'; + const line = diag.location?.span?.[0] ? 1 : (diag.location?.start?.line || 1); + const col = diag.location?.start?.column || 1; + const ruleId = diag.category || 'biome-lint'; + const message = diag.description || (diag.message && diag.message[0]?.text) || 'Biome lint violation'; + const severity = diag.severity === 'error' || diag.severity === 'fatal' ? 'ERROR' : 'WARNING'; diagnostics.push({ source: 'biome', rule_id: ruleId, - severity: diag.severity === 'warning' ? 'WARNING' : 'ERROR', + severity, file_path: filePath, range: { - start: { line: startLine, column: startCol }, - end: { line: endLine, column: endCol }, + start: { line, column: col }, + end: { line, column: col + 20 }, }, error_message: message, - repair_instruction: generateBiomeRepair(ruleId, message), + repair_instruction: generateBiomeRepair(ruleId, filePath), }); } if (diagnostics.length > 0) return diagnostics; } } catch { - // Continue to regex + // Continue to regex parser if JSON parsing fails } } - // Regex parsing for Biome text output - // Example: - // src/index.ts:12:5 lint/suspicious/noExplicitAny ━━━━━━━━━━━━━━━━━━━━━━━━━━━ - // āœ– Avoid using any. + // 2. Regex parser for Biome standard CLI terminal output const lines = clean.split('\n'); - let currentDiag: Partial | null = null; for (let i = 0; i < lines.length; i++) { const line = lines[i]; - const headerMatch = line.match(/^([^\s:]+):(\d+):(\d+)\s+([a-zA-Z0-9_\-\/]+)/); - if (headerMatch) { - if (currentDiag && currentDiag.file_path) { - diagnostics.push(currentDiag as DiagnosticItem); + + // Pattern: filePath:line:col lint/suspicious/noExplicitAny ━━━━━━━━━━━━━ + const match = line.match(/^([^\s:]+):(\d+):(\d+)\s+([a-zA-Z0-9_\-\/]+)/); + if (match) { + const [, filePath, lineStr, colStr, ruleId] = match; + let errorMsg = 'Biome lint violation'; + + // Look ahead for the error description on subsequent lines + for (let j = i + 1; j < Math.min(i + 4, lines.length); j++) { + const nextLine = lines[j].trim(); + if (nextLine.startsWith('āœ–') || nextLine.startsWith('!') || nextLine.startsWith('i')) { + errorMsg = nextLine.replace(/^[āœ–!i]\s*/, '').trim(); + break; + } } - const [, filePath, lineStr, colStr, ruleId] = headerMatch; - const lineNum = parseInt(lineStr, 10); - const colNum = parseInt(colStr, 10); - - currentDiag = { + diagnostics.push({ source: 'biome', rule_id: ruleId, - severity: ruleId.includes('warn') ? 'WARNING' : 'ERROR', + severity: 'ERROR', file_path: filePath, range: { - start: { line: lineNum, column: colNum }, - end: { line: lineNum, column: colNum + 10 }, + start: { line: parseInt(lineStr, 10), column: parseInt(colStr, 10) }, + end: { line: parseInt(lineStr, 10), column: parseInt(colStr, 10) + 20 }, }, - error_message: '', - }; - continue; + error_message: errorMsg, + repair_instruction: generateBiomeRepair(ruleId, filePath), + }); } - - if (currentDiag && (line.includes('āœ–') || line.includes('error:') || line.includes('warn:'))) { - const msg = line.replace(/^[āœ–\s]+/, '').trim(); - currentDiag.error_message = msg; - currentDiag.repair_instruction = generateBiomeRepair(currentDiag.rule_id || '', msg); - } - } - - if (currentDiag && currentDiag.file_path) { - if (!currentDiag.error_message) currentDiag.error_message = 'Biome lint or format violation'; - if (!currentDiag.repair_instruction) currentDiag.repair_instruction = generateBiomeRepair(currentDiag.rule_id || ''); - diagnostics.push(currentDiag as DiagnosticItem); } return diagnostics; } -function generateBiomeRepair(ruleId: string, message: string = ''): DiagnosticItem['repair_instruction'] { +/** + * Generate actionable repair instructions for Biome rules + */ +function generateBiomeRepair(ruleId: string, filePath: string): DiagnosticItem['repair_instruction'] { if (ruleId.includes('noExplicitAny')) { return { action: 'REPLACE_TOKEN', - description: 'Replace `any` with `unknown` or a specific typed interface.', - repair_tokens: ['unknown', 'Record'], - }; - } - - if (ruleId.includes('noUnusedVariables') || ruleId.includes('noUnusedImports')) { - return { - action: 'DELETE_LINE', - description: 'Remove unused variable or import.', - repair_tokens: ['// Remove unused identifier'], - }; - } - - if (ruleId.includes('useConst')) { - return { - action: 'REPLACE_TOKEN', - description: 'Use `const` instead of `let` or `var` for variables that are never reassigned.', - repair_tokens: ['const '], + description: 'Replace `any` with `unknown` or a specific TypeScript interface / type guard.', + repair_tokens: ['unknown', 'type SafePayload = Record;'], + suggested_command: `npx @biomejs/biome check --write ${filePath}`, }; } return { action: 'EXECUTE_COMMAND', - description: `Run biome check --write to automatically apply safe fixes.`, - suggested_command: 'npx biome check --write ${filePath}', - repair_tokens: ['npx biome check --write'], + description: `Run Biome automated fix or format: npx @biomejs/biome check --write ${filePath}`, + repair_tokens: [`npx @biomejs/biome check --write ${filePath}`], + suggested_command: `npx @biomejs/biome check --write ${filePath}`, }; } diff --git a/src/formatter/parsers/generic.ts b/src/formatter/parsers/generic.ts index c00f76c..bc791d4 100644 --- a/src/formatter/parsers/generic.ts +++ b/src/formatter/parsers/generic.ts @@ -1,63 +1,79 @@ +/** + * @file src/formatter/parsers/generic.ts + * @description Fallback generic output parser for unrecognized tools and standard error streams. + */ + import type { DiagnosticItem } from '../../types/index.js'; import { stripAnsi } from '../ansi.js'; -export function parseGenericOutput(rawOutput: string, sourceName: string = 'gate'): DiagnosticItem[] { +/** + * Parse arbitrary tool error output into structured DiagnosticItems using standard heuristics. + * + * @param rawOutput Raw error string from an unclassified tool + * @param toolName Name of the tool producing the output + * @returns Array of DiagnosticItem objects + */ +export function parseGenericOutput(rawOutput: string, toolName: string = 'unknown'): DiagnosticItem[] { const clean = stripAnsi(rawOutput); const diagnostics: DiagnosticItem[] = []; - const lines = clean.split('\n'); + for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; - // Matches standard Unix error formats: - // path/to/file.ext:10:5: error message - // path/to/file.ext:10: error message - const match = trimmed.match(/^([^\s:]+\.[a-zA-Z0-9_-]+):(\d+)(?::(\d+))?:\s*(?:([a-zA-Z0-9_\-\/]+):)?\s*(.*)$/); - if (match) { - const [, filePath, lineStr, colStr, ruleId, msg] = match; - const lineNum = parseInt(lineStr, 10); - const colNum = colStr ? parseInt(colStr, 10) : 1; - + // Pattern 1: filePath:line:col: message + const match1 = trimmed.match(/^([^\s:]+):(\d+):(\d+):\s*(.*)$/); + if (match1) { + const [, filePath, lineStr, colStr, message] = match1; diagnostics.push({ - source: sourceName, - rule_id: ruleId || `${sourceName.toUpperCase()}_ERROR`, - severity: msg.toLowerCase().includes('warning') ? 'WARNING' : 'ERROR', + source: toolName, + rule_id: `${toolName.toUpperCase()}_ERROR`, + severity: 'ERROR', file_path: filePath, range: { - start: { line: lineNum, column: colNum }, - end: { line: lineNum, column: colNum + 20 }, + start: { line: parseInt(lineStr, 10), column: parseInt(colStr, 10) }, + end: { line: parseInt(lineStr, 10), column: parseInt(colStr, 10) + 20 }, }, - error_message: msg.trim() || 'Tool validation failed', + error_message: message.trim(), repair_instruction: { action: 'MANUAL_FIX', - description: `Fix the issue reported by ${sourceName}.`, - repair_tokens: [`// Resolve ${ruleId || 'error'} in ${filePath}`], + description: `Resolve ${toolName} violation in ${filePath}`, + repair_tokens: [`# Fix violation reported by ${toolName}`], + }, + }); + continue; + } + + // Pattern 2: Generic error line + if (trimmed.toLowerCase().includes('error') || trimmed.toLowerCase().includes('failed')) { + diagnostics.push({ + source: toolName, + rule_id: `${toolName.toUpperCase()}_FAILURE`, + severity: 'ERROR', + file_path: 'codebase', + error_message: trimmed, + repair_instruction: { + action: 'MANUAL_FIX', + description: `Resolve error reported by ${toolName}`, + repair_tokens: [`# Resolve ${toolName} failure: ${trimmed}`], }, }); } } - const lower = clean.toLowerCase(); - const isSuccessMessage = - lower.includes('no errors') || - lower.includes('0 errors') || - lower.includes('passed') || - lower.includes('checked') || - lower.includes('success'); - - if (diagnostics.length === 0 && clean.trim().length > 0 && !isSuccessMessage) { - // If no line-based match was found, emit a general gate diagnostic + // If no specific lines matched but rawOutput is non-empty, emit a single diagnostic item + if (diagnostics.length === 0 && clean.trim().length > 0) { diagnostics.push({ - source: sourceName, - rule_id: `${sourceName.toUpperCase()}_GATE_FAILURE`, + source: toolName, + rule_id: `${toolName.toUpperCase()}_ERROR`, severity: 'ERROR', - file_path: 'workspace', - error_message: clean.trim().slice(0, 500), + file_path: 'codebase', + error_message: clean.trim().split('\n')[0] || `${toolName} execution failed`, repair_instruction: { action: 'MANUAL_FIX', - description: `Review output from ${sourceName} and fix failing constraints.`, - repair_tokens: ['// Check error output and resolve failing check'], + description: `Investigate and resolve ${toolName} failure.`, + repair_tokens: [`# Fix ${toolName} failure`], }, }); } diff --git a/src/formatter/parsers/ruff.ts b/src/formatter/parsers/ruff.ts index 96665d5..a39c059 100644 --- a/src/formatter/parsers/ruff.ts +++ b/src/formatter/parsers/ruff.ts @@ -1,62 +1,78 @@ +/** + * @file src/formatter/parsers/ruff.ts + * @description Ruff output parser for Python linter and formatter diagnostics. + */ + import type { DiagnosticItem } from '../../types/index.js'; import { stripAnsi } from '../ansi.js'; +/** + * Parse raw stdout/stderr from `ruff check` into structured DiagnosticItems. + * Supports both Ruff JSON output and standard concise CLI text output. + * + * @param rawOutput Raw terminal output from Ruff + * @returns Array of DiagnosticItem objects with rule codes (e.g. F401, E722) and repair instructions + */ export function parseRuffOutput(rawOutput: string): DiagnosticItem[] { const clean = stripAnsi(rawOutput); const diagnostics: DiagnosticItem[] = []; - // Check if JSON output + // 1. Check if output is Ruff JSON format if (clean.trim().startsWith('[')) { try { - const items = JSON.parse(clean); - for (const item of items) { - if (item.code && item.filename) { + const parsed = JSON.parse(clean); + if (Array.isArray(parsed)) { + for (const item of parsed) { + const filePath = item.filename || 'unknown'; + const line = item.location?.row || 1; + const col = item.location?.column || 1; + const endLine = item.end_location?.row || line; + const endCol = item.end_location?.column || col + 10; + const ruleId = item.code || 'RUFF_ERROR'; + const message = item.message || 'Python lint violation'; + diagnostics.push({ source: 'ruff', - rule_id: item.code, + rule_id: ruleId, severity: 'ERROR', - file_path: item.filename, + file_path: filePath, range: { - start: { - line: item.location?.row || 1, - column: item.location?.column || 1, - }, - end: { - line: item.end_location?.row || item.location?.row || 1, - column: item.end_location?.column || item.location?.column || 1, - }, + start: { line, column: col }, + end: { line: endLine, column: endCol }, }, - error_message: item.message || 'Ruff violation', - repair_instruction: generateRuffRepair(item.code, item.message), + error_message: message, + repair_instruction: generateRuffRepair(ruleId, message, filePath), }); } + if (diagnostics.length > 0) return diagnostics; } - if (diagnostics.length > 0) return diagnostics; } catch { - // Continue to regex + // Continue to regex parser if JSON parse fails } } - // Regex parsing: path/to/file.py:row:col: CODE [*] Message + // 2. Regex parser for Ruff standard text output: filePath:line:col: RULE [*] message const lines = clean.split('\n'); - for (const line of lines) { - const match = line.match(/^([^\s:]+\.py):(\d+):(\d+):\s*([A-Z0-9]+)\s*(?:\[\*\])?\s*(.*)$/); - if (match) { - const [, filePath, rowStr, colStr, code, message] = match; - const row = parseInt(rowStr, 10); - const col = parseInt(colStr, 10); + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) continue; + + // Pattern: api/server.py:2:1: F401 [*] 'sys' imported but unused + const match = trimmed.match(/^([^\s:]+):(\d+):(\d+):\s*([A-Z0-9]+)\s*(?:\[\*\])?\s*(.*)$/); + if (match) { + const [, filePath, lineStr, colStr, ruleId, message] = match; diagnostics.push({ source: 'ruff', - rule_id: code, + rule_id: ruleId, severity: 'ERROR', file_path: filePath, range: { - start: { line: row, column: col }, - end: { line: row, column: col + 10 }, + start: { line: parseInt(lineStr, 10), column: parseInt(colStr, 10) }, + end: { line: parseInt(lineStr, 10), column: parseInt(colStr, 10) + 10 }, }, error_message: message.trim(), - repair_instruction: generateRuffRepair(code, message.trim()), + repair_instruction: generateRuffRepair(ruleId, message.trim(), filePath), }); } } @@ -64,16 +80,22 @@ export function parseRuffOutput(rawOutput: string): DiagnosticItem[] { return diagnostics; } -function generateRuffRepair(code: string, message: string = ''): DiagnosticItem['repair_instruction'] { - if (code.startsWith('F401')) { +/** + * Generate actionable repair instructions and replacement tokens for Ruff rules + */ +function generateRuffRepair(ruleId: string, message: string, filePath: string): DiagnosticItem['repair_instruction'] { + // F401: Unused import + if (ruleId === 'F401') { return { action: 'DELETE_LINE', description: 'Remove unused import statement.', repair_tokens: ['# Remove unused import'], + suggested_command: `ruff check --fix ${filePath}`, }; } - if (code.startsWith('E722') || code.startsWith('B001')) { + // E722: Do not use bare 'except' + if (ruleId === 'E722' || message.toLowerCase().includes('bare')) { return { action: 'REWRITE_BLOCK', description: 'Do not use bare `except:`. Specify explicit exception type (e.g. `except Exception as e:`).', @@ -82,21 +104,24 @@ function generateRuffRepair(code: string, message: string = ''): DiagnosticItem[ ' logger.error(f"Operation failed: {err}")', ' raise', ], + suggested_command: `ruff check --fix ${filePath}`, }; } - if (code.startsWith('F841')) { + // B006: Do not use mutable data structures for argument defaults + if (ruleId === 'B006') { return { - action: 'DELETE_LINE', - description: 'Remove unused local variable or prefix with underscore `_`.', - repair_tokens: ['_ = ...'], + action: 'REPLACE_TOKEN', + description: 'Replace mutable argument default (e.g. `arg=[]`) with `None` and initialize in function body.', + repair_tokens: ['arg: list | None = None', 'if arg is None: arg = []'], }; } + // General auto-fixable Ruff rule return { action: 'EXECUTE_COMMAND', - description: `Run ruff check --fix ${message ? `to fix: ${message}` : ''}`, - suggested_command: 'ruff check --fix ${filePath}', - repair_tokens: ['ruff check --fix'], + description: `Apply Ruff automated fix: ruff check --fix ${filePath}`, + repair_tokens: [`ruff check --fix ${filePath}`], + suggested_command: `ruff check --fix ${filePath}`, }; } diff --git a/src/formatter/parsers/skillcheck.ts b/src/formatter/parsers/skillcheck.ts index 01c6cdd..0f67764 100644 --- a/src/formatter/parsers/skillcheck.ts +++ b/src/formatter/parsers/skillcheck.ts @@ -1,57 +1,62 @@ +/** + * @file src/formatter/parsers/skillcheck.ts + * @description SkillCheck output parser for agent skill files and markdown instructions. + */ + import type { DiagnosticItem } from '../../types/index.js'; import { stripAnsi } from '../ansi.js'; +/** + * Parse raw stdout/stderr from `skillcheck` into structured DiagnosticItems. + * + * @param rawOutput Raw terminal output from SkillCheck + * @returns Array of DiagnosticItem objects with YAML frontmatter repair tokens + */ export function parseSkillcheckOutput(rawOutput: string): DiagnosticItem[] { const clean = stripAnsi(rawOutput); const diagnostics: DiagnosticItem[] = []; - - // Check JSON - if (clean.trim().startsWith('{') || clean.trim().startsWith('[')) { - try { - const parsed = JSON.parse(clean); - const items = Array.isArray(parsed) ? parsed : (parsed.violations || parsed.diagnostics || [parsed]); - for (const item of items) { - diagnostics.push({ - source: 'skillcheck', - rule_id: item.rule_id || item.rule || 'SKILL_CHECK_VIOLATION', - severity: (item.severity || 'ERROR').toUpperCase() as any, - file_path: item.file_path || item.file || 'SKILL.md', - range: item.range || { - start: { line: item.line || 1, column: item.column || 1 }, - end: { line: item.end_line || item.line || 1, column: item.end_column || 80 }, - }, - code_snippet: item.snippet, - error_message: item.message || 'Agent skill validation failed', - repair_instruction: generateSkillcheckRepair(item.rule_id || item.rule), - }); - } - if (diagnostics.length > 0) return diagnostics; - } catch { - // Continue to regex - } - } - - // Regex parser const lines = clean.split('\n'); - for (const line of lines) { - const match = line.match(/^([^\s:]+\.md)(?::(\d+))?(?::(\d+))?:\s*(?:\[([^\]]+)\])?\s*(.*)$/i); - if (match) { - const [, filePath, lineStr, colStr, ruleId, msg] = match; - const lineNum = lineStr ? parseInt(lineStr, 10) : 1; - const colNum = colStr ? parseInt(colStr, 10) : 1; - const effectiveRuleId = ruleId || detectSkillRule(msg); + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) continue; + + // Pattern 1: filePath:line:col: [RULE_ID] message + const match = trimmed.match(/^([^\s:]+):(\d+):(\d+):\s*(?:\[([^\]]+)\])?\s*(.*)$/); + if (match) { + const [, filePath, lineStr, colStr, ruleId, message] = match; + const effectiveRuleId = ruleId || 'SKILL_VALIDATION_ERROR'; diagnostics.push({ source: 'skillcheck', rule_id: effectiveRuleId, severity: 'ERROR', file_path: filePath, range: { - start: { line: lineNum, column: colNum }, - end: { line: lineNum, column: colNum + 30 }, + start: { line: parseInt(lineStr, 10), column: parseInt(colStr, 10) }, + end: { line: parseInt(lineStr, 10), column: parseInt(colStr, 10) + 30 }, }, - error_message: msg.trim() || 'Agent skill configuration violation', - repair_instruction: generateSkillcheckRepair(effectiveRuleId), + error_message: message.trim(), + repair_instruction: generateSkillRepair(effectiveRuleId, message.trim()), + }); + continue; + } + + // Pattern 2: [skillcheck] error in filePath: message + const match2 = trimmed.match(/\[skillcheck\]\s*(?:error|warning)?\s*(?:in\s+)?([^\s:]+)(?::(\d+))?:\s*(.*)/i); + if (match2) { + const [, filePath, lineStr, message] = match2; + const lineNum = lineStr ? parseInt(lineStr, 10) : 1; + diagnostics.push({ + source: 'skillcheck', + rule_id: 'SKILL_VALIDATION_ERROR', + severity: 'ERROR', + file_path: filePath, + range: { + start: { line: lineNum, column: 1 }, + end: { line: lineNum, column: 40 }, + }, + error_message: message.trim(), + repair_instruction: generateSkillRepair('SKILL_VALIDATION_ERROR', message.trim()), }); } } @@ -59,26 +64,12 @@ export function parseSkillcheckOutput(rawOutput: string): DiagnosticItem[] { return diagnostics; } -function detectSkillRule(msg: string = ''): string { - const m = msg.toLowerCase(); - if (m.includes('frontmatter') || m.includes('yaml') || m.includes('header')) { - return 'SKILL_INVALID_FRONTMATTER'; - } - if (m.includes('permission') || m.includes('grant') || m.includes('over-grant')) { - return 'SKILL_OVER_GRANTED_PERMISSIONS'; - } - if (m.includes('prompt injection') || m.includes('escape') || m.includes('security')) { - return 'SKILL_SECURITY_RISK'; - } - if (m.includes('trigger') || m.includes('description')) { - return 'SKILL_MISSING_METADATA'; - } - return 'SKILL_VALIDATION_ERROR'; -} - -function generateSkillcheckRepair(ruleId: string = ''): DiagnosticItem['repair_instruction'] { - const r = ruleId.toUpperCase(); - if (r.includes('FRONTMATTER') || r.includes('METADATA')) { +/** + * Generate actionable YAML frontmatter repair tokens for skill files + */ +function generateSkillRepair(ruleId: string, message: string): DiagnosticItem['repair_instruction'] { + const m = message.toLowerCase(); + if (m.includes('frontmatter') || m.includes('header') || ruleId.includes('FRONTMATTER')) { return { action: 'REWRITE_BLOCK', description: 'Ensure skill file starts with valid YAML frontmatter containing name and description.', @@ -91,19 +82,9 @@ function generateSkillcheckRepair(ruleId: string = ''): DiagnosticItem['repair_i }; } - if (r.includes('PERMISSION') || r.includes('SECURITY')) { - return { - action: 'REWRITE_BLOCK', - description: 'Restrict permissions to the minimal set of required tools and paths.', - repair_tokens: [ - '# Enforce least-privilege tool access boundaries', - ], - }; - } - return { - action: 'MANUAL_FIX', - description: 'Format agent skill markdown according to the Agent Skill standard.', - repair_tokens: ['# Skill Documentation and Instructions'], + action: 'REWRITE_BLOCK', + description: 'Ensure skill adheres to schema specifications and OWASP agentic boundaries.', + repair_tokens: ['# Verify skill name, description, and input schema definitions'], }; } diff --git a/src/formatter/parsers/trufflehog.ts b/src/formatter/parsers/trufflehog.ts index 79635d7..1e47b42 100644 --- a/src/formatter/parsers/trufflehog.ts +++ b/src/formatter/parsers/trufflehog.ts @@ -1,34 +1,46 @@ +/** + * @file src/formatter/parsers/trufflehog.ts + * @description TruffleHog output parser for high-entropy secret detection. + */ + import type { DiagnosticItem } from '../../types/index.js'; import { stripAnsi } from '../ansi.js'; +/** + * Parse raw stdout/stderr from `trufflehog` into structured DiagnosticItems. + * Supports NDJSON (newline-delimited JSON) emitted by `trufflehog --json`. + * + * @param rawOutput Raw output from TruffleHog + * @returns Array of DiagnosticItem objects with credential revocation instructions + */ export function parseTrufflehogOutput(rawOutput: string): DiagnosticItem[] { const clean = stripAnsi(rawOutput); const diagnostics: DiagnosticItem[] = []; - - // Check JSON / JSON-Lines const lines = clean.split('\n'); + for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; + // Check for TruffleHog NDJSON line if (trimmed.startsWith('{') && trimmed.endsWith('}')) { try { const item = JSON.parse(trimmed); - const detector = item.DetectorName || item.detector_name || 'SECRET'; - const file = item.SourceMetadata?.Data?.Git?.file || item.file || item.file_path || 'unknown'; - const lineNum = item.SourceMetadata?.Data?.Git?.line || item.line || 1; - const verified = item.Verified ?? true; + const detector = item.DetectorName || item.DetectorType || 'SECRET'; + const filePath = item.SourceMetadata?.Data?.Git?.file || item.SourceMetadata?.Data?.Filesystem?.file || 'unknown'; + const lineNum = item.SourceMetadata?.Data?.Git?.line || 1; + const verified = item.Verified === true; diagnostics.push({ source: 'trufflehog', rule_id: `VERIFIED_SECRET_${detector.toUpperCase()}`, severity: 'ERROR', - file_path: file, + file_path: filePath, range: { start: { line: lineNum, column: 1 }, end: { line: lineNum, column: 80 }, }, - error_message: `${verified ? 'Verified' : 'Unverified'} high-entropy credential or secret detected (${detector}).`, + error_message: `Verified high-entropy credential or secret detected (${detector}).${verified ? ' [VERIFIED ACTIVE]' : ''}`, repair_instruction: { action: 'REPLACE_TOKEN', description: `Revoke this exposed ${detector} credential immediately and replace hardcoded secret with an environment variable lookup.`, @@ -39,32 +51,29 @@ export function parseTrufflehogOutput(rawOutput: string): DiagnosticItem[] { }); continue; } catch { - // Fall through to regex + // Continue to regex if JSON parse fails } } - // Text regex: Found verified secret in : - const match = trimmed.match(/(?:found|detected)\s+(?:verified\s+)?([a-zA-Z0-9_-]+)\s+secret\s+in\s+([^\s:]+)(?::(\d+))?/i); + // Pattern 2: Textual secret detection finding + const match = trimmed.match(/found\s+([a-zA-Z0-9_\-]+)\s+secret\s+in\s+([^\s:]+)(?::(\d+))?/i); if (match) { const [, detector, filePath, lineStr] = match; const lineNum = lineStr ? parseInt(lineStr, 10) : 1; - diagnostics.push({ source: 'trufflehog', - rule_id: `VERIFIED_SECRET_${detector.toUpperCase()}`, + rule_id: `SECRET_${detector.toUpperCase()}`, severity: 'ERROR', file_path: filePath, range: { start: { line: lineNum, column: 1 }, end: { line: lineNum, column: 80 }, }, - error_message: `Verified high-entropy credential or secret detected (${detector}).`, + error_message: `Secret detected: ${detector}`, repair_instruction: { action: 'REPLACE_TOKEN', - description: `Revoke this exposed ${detector} credential immediately and replace hardcoded secret with an environment variable lookup.`, - repair_tokens: [ - `process.env.${detector.toUpperCase()}_KEY || process.env.API_KEY`, - ], + description: `Replace hardcoded ${detector} secret with an environment variable.`, + repair_tokens: [`process.env.${detector.toUpperCase()}`], }, }); } diff --git a/src/formatter/parsers/typos.ts b/src/formatter/parsers/typos.ts index e4eb90f..f99f032 100644 --- a/src/formatter/parsers/typos.ts +++ b/src/formatter/parsers/typos.ts @@ -1,58 +1,46 @@ +/** + * @file src/formatter/parsers/typos.ts + * @description Typos output parser for AST-aware source code spell checking. + */ + import type { DiagnosticItem } from '../../types/index.js'; import { stripAnsi } from '../ansi.js'; +/** + * Parse raw stdout/stderr from `typos` into structured DiagnosticItems. + * + * @param rawOutput Raw terminal output from typos CLI + * @returns Array of DiagnosticItem objects with accurate spelling repair tokens + */ export function parseTyposOutput(rawOutput: string): DiagnosticItem[] { const clean = stripAnsi(rawOutput); const diagnostics: DiagnosticItem[] = []; - const lines = clean.split('\n'); - let pendingTypo: { typo: string; correction: string } | null = null; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; - // Pattern 1: error: `foo` should be `bar` - const typoMatch = line.match(/error:\s*`([^`]+)`\s*should be\s*`([^`]+)`/i); - if (typoMatch) { - pendingTypo = { typo: typoMatch[1], correction: typoMatch[2] }; - continue; - } + // Pattern 1: error: `typo` should be `correction` + // --> filePath:line:col + const match1 = line.match(/error:\s*`([^`]+)`\s+should be\s+`([^`]+)`/); + if (match1) { + const [, typo, correction] = match1; + let filePath = 'unknown'; + let lineNum = 1; + let colNum = 1; - // Pattern 2: --> path/to/file:line:col - const locMatch = line.match(/-->\s*([^\s:]+):(\d+):(\d+)/); - if (locMatch && pendingTypo) { - const [, filePath, lineStr, colStr] = locMatch; - const lineNum = parseInt(lineStr, 10); - const colNum = parseInt(colStr, 10); - - diagnostics.push({ - source: 'typos', - rule_id: 'TYPO_DETECTED', - severity: 'ERROR', - file_path: filePath, - range: { - start: { line: lineNum, column: colNum }, - end: { line: lineNum, column: colNum + pendingTypo.typo.length }, - }, - code_snippet: pendingTypo.typo, - error_message: `Typo detected: \`${pendingTypo.typo}\` should be \`${pendingTypo.correction}\`.`, - repair_instruction: { - action: 'REPLACE_TOKEN', - description: `Replace typo \`${pendingTypo.typo}\` with correct spelling \`${pendingTypo.correction}\`.`, - repair_tokens: [pendingTypo.correction], - }, - }); - pendingTypo = null; - continue; - } - - // Single-line pattern: path/to/file:line:col: `foo` should be `bar` - const singleMatch = line.match(/^([^\s:]+):(\d+):(\d+):\s*(?:error:\s*)?`([^`]+)`\s*should be\s*`([^`]+)`/i); - if (singleMatch) { - const [, filePath, lineStr, colStr, typo, correction] = singleMatch; - const lineNum = parseInt(lineStr, 10); - const colNum = parseInt(colStr, 10); + // Look at next lines for file location indicator: --> filePath:line:col + if (i + 1 < lines.length) { + const nextLine = lines[i + 1].trim(); + const locMatch = nextLine.match(/-->\s*([^\s:]+):(\d+):(\d+)/); + if (locMatch) { + filePath = locMatch[1]; + lineNum = parseInt(locMatch[2], 10); + colNum = parseInt(locMatch[3], 10); + i++; // Skip location line + } + } diagnostics.push({ source: 'typos', @@ -71,6 +59,30 @@ export function parseTyposOutput(rawOutput: string): DiagnosticItem[] { repair_tokens: [correction], }, }); + continue; + } + + // Pattern 2: filePath:line:col: `typo` -> `correction` + const match2 = line.match(/^([^\s:]+):(\d+):(\d+):\s*`([^`]+)`\s*->\s*`([^`]+)`/); + if (match2) { + const [, filePath, lineStr, colStr, typo, correction] = match2; + diagnostics.push({ + source: 'typos', + rule_id: 'TYPO_DETECTED', + severity: 'ERROR', + file_path: filePath, + range: { + start: { line: parseInt(lineStr, 10), column: parseInt(colStr, 10) }, + end: { line: parseInt(lineStr, 10), column: parseInt(colStr, 10) + typo.length }, + }, + code_snippet: typo, + error_message: `Typo: \`${typo}\` -> \`${correction}\``, + repair_instruction: { + action: 'REPLACE_TOKEN', + description: `Fix typo: replace with \`${correction}\``, + repair_tokens: [correction], + }, + }); } } diff --git a/src/generator/configGenerator.ts b/src/generator/configGenerator.ts index ffa3440..bcc92dc 100644 --- a/src/generator/configGenerator.ts +++ b/src/generator/configGenerator.ts @@ -1,3 +1,16 @@ +/** + * @file src/generator/configGenerator.ts + * @description Configuration and multi-tier pipeline generator engine. + * + * Emits deterministic configuration files matching detected repository stacks: + * - lefthook.yml: Multi-threaded parallel git hook runner + * - .claude/hooks.json: Stage 1 agent lifecycle tool interceptor + * - .claude/settings.json: Locked agent governance settings + * - biome.json: Biome sub-millisecond JS/TS linter/formatter config + * - ruff.toml: Ruff sub-millisecond Python linter/formatter config + * - .aislop/config.yml: AISlop deterministic code smell rules + */ + import * as fs from 'node:fs'; import * as path from 'node:path'; import type { GeneratedConfig, GenerationResult, StackDetectionResult } from '../types/index.js'; @@ -7,27 +20,45 @@ import { generateBiomeConfig } from './templates/biome.js'; import { generateRuffConfig } from './templates/ruff.js'; import { generateAislopConfig } from './templates/aislop.js'; +/** + * Options for configuring the ConfigGenerator instance + */ export interface ConfigGeneratorOptions { + /** Target root directory for configuration generation (defaults to process.cwd()) */ cwd?: string; + /** If true, existing configuration files will be overwritten */ overwrite?: boolean; } +/** + * Multi-Tier Pipeline Configuration Generator. + * Translates StackDetectionResult metadata into concrete, deterministic config files. + */ export class ConfigGenerator { + /** Resolved absolute path of the target repository root */ private readonly rootPath: string; + /** Whether to overwrite existing files on disk */ private readonly overwrite: boolean; + /** + * Initialize a new ConfigGenerator instance + * @param options Generator configuration options + */ constructor(options: ConfigGeneratorOptions = {}) { this.rootPath = path.resolve(options.cwd || process.cwd()); this.overwrite = options.overwrite ?? false; } /** - * Compute configuration files to be generated for the detected stack + * Compute configuration files to be generated for the detected stacks without writing to disk. + * + * @param detection Stack detection result containing language and workflow indicators + * @returns Array of GeneratedConfig objects with paths, contents, and descriptions */ public generate(detection: StackDetectionResult): GeneratedConfig[] { const configs: GeneratedConfig[] = []; - // 1. Lefthook Orchestration Config (Stage 2 & Stage 3) + // 1. Lefthook Git Hook Runner Config (Stage 2 Pre-Commit & Stage 3 Pre-Push) configs.push({ path: 'lefthook.yml', content: generateLefthookConfig(detection), @@ -80,7 +111,10 @@ export class ConfigGenerator { } /** - * Write generated configurations to disk + * Write generated configurations to disk, creating parent directories as needed. + * + * @param detection Stack detection result + * @returns GenerationResult containing written and skipped file lists */ public writeToDisk(detection: StackDetectionResult): GenerationResult { const configs = this.generate(detection); @@ -91,15 +125,18 @@ export class ConfigGenerator { const fullPath = path.resolve(this.rootPath, config.path); const dirName = path.dirname(fullPath); + // Skip file if it already exists and overwrite flag is not set if (fs.existsSync(fullPath) && !this.overwrite) { skippedFiles.push(config.path); continue; } + // Ensure parent directory exists if (!fs.existsSync(dirName)) { fs.mkdirSync(dirName, { recursive: true }); } + // Write file contents to disk fs.writeFileSync(fullPath, config.content, 'utf-8'); writtenFiles.push(config.path); } @@ -112,6 +149,11 @@ export class ConfigGenerator { } } +/** + * Functional convenience wrapper to generate and write configurations + * @param detection Stack detection result + * @param options Generator options + */ export function generateConfigs(detection: StackDetectionResult, options?: ConfigGeneratorOptions): GenerationResult { return new ConfigGenerator(options).writeToDisk(detection); } diff --git a/src/generator/index.ts b/src/generator/index.ts index 0c51e8e..a4f5f77 100644 --- a/src/generator/index.ts +++ b/src/generator/index.ts @@ -1,3 +1,8 @@ +/** + * @file src/generator/index.ts + * @description Exports configuration generators and templates. + */ + export * from './configGenerator.js'; export * from './templates/lefthook.js'; export * from './templates/claudeHooks.js'; diff --git a/src/generator/templates/aislop.ts b/src/generator/templates/aislop.ts index 13ba7b5..79aeade 100644 --- a/src/generator/templates/aislop.ts +++ b/src/generator/templates/aislop.ts @@ -1,3 +1,14 @@ +/** + * @file src/generator/templates/aislop.ts + * @description Generates the .aislop/config.yml configuration for deterministic AI slop detection. + */ + +/** + * Generate an AISlop configuration defining deterministic rules against + * swallowed errors, empty catch blocks, hallucinated imports, and unsafe type casts. + * + * @returns Formatted YAML string for .aislop/config.yml + */ export function generateAislopConfig(): string { return `# AISlop Configuration for Deterministic AI Code Smell & Slop Detection version: "1.0" diff --git a/src/generator/templates/biome.ts b/src/generator/templates/biome.ts index 07848c9..b2466ef 100644 --- a/src/generator/templates/biome.ts +++ b/src/generator/templates/biome.ts @@ -1,3 +1,14 @@ +/** + * @file src/generator/templates/biome.ts + * @description Generates the biome.json configuration for sub-millisecond JS/TS linting and formatting. + */ + +/** + * Generate a standard, high-performance Biome configuration with strict rules against + * common AI-generated anti-patterns (noExplicitAny, noDebugger, noVar). + * + * @returns Formatted JSON string for biome.json + */ export function generateBiomeConfig(): string { const config = { $schema: 'https://biomejs.dev/schemas/1.9.4/schema.json', diff --git a/src/generator/templates/claudeHooks.ts b/src/generator/templates/claudeHooks.ts index 31d717e..62f24c0 100644 --- a/src/generator/templates/claudeHooks.ts +++ b/src/generator/templates/claudeHooks.ts @@ -1,21 +1,46 @@ +/** + * @file src/generator/templates/claudeHooks.ts + * @description Generates the .claude/hooks.json configuration for Stage 1 agent tool interception. + * + * Automatically intercepts agent file edits (PostFileEdit) to format and lint modified ASTs + * in < 300ms, and routes pre-commit git commands through the mechanical Lefthook runner. + */ + import type { StackDetectionResult } from '../../types/index.js'; +/** + * Individual hook rule configuration for Claude Code lifecycle events + */ export interface ClaudeHookRule { + /** Glob matcher pattern (e.g. '*.{js,ts}', '*.py', '.claude/skills/*.md') */ matcher?: string; + /** Shell command executed when the matcher matches the modified file */ command: string; } +/** + * Top-level structure of .claude/hooks.json + */ export interface ClaudeHooksStructure { hooks: { + /** Intercepts file modification tools (FileEdit, FileWrite) */ PostFileEdit?: ClaudeHookRule[]; + /** Intercepts git commit operations */ PreCommit?: ClaudeHookRule[]; [key: string]: ClaudeHookRule[] | undefined; }; } +/** + * Generate .claude/hooks.json for Stage 1 agent tool interception. + * + * @param detection Stack detection result containing language and harness indicators + * @returns Formatted JSON string for .claude/hooks.json + */ export function generateClaudeHooksConfig(detection: StackDetectionResult): string { const postFileEditRules: ClaudeHookRule[] = []; + // JS/TS: Instant single-file format & lint on edit if (detection.jsTs.detected) { postFileEditRules.push({ matcher: '*.{js,ts,jsx,tsx}', @@ -23,6 +48,7 @@ export function generateClaudeHooksConfig(detection: StackDetectionResult): stri }); } + // Python: Instant single-file format & fix on edit if (detection.python.detected) { postFileEditRules.push({ matcher: '*.py', @@ -30,7 +56,7 @@ export function generateClaudeHooksConfig(detection: StackDetectionResult): stri }); } - // Always enable skill checking for agent instructions if agent harness is detected or by default + // Skill files: Validate agent skills against schema and OWASP agentic boundaries postFileEditRules.push({ matcher: '.claude/skills/*.md', command: 'skillcheck check ${filePath}', @@ -41,6 +67,7 @@ export function generateClaudeHooksConfig(detection: StackDetectionResult): stri command: 'skillcheck check ${filePath}', }); + // Assemble the complete Claude hooks object const hooksConfig: ClaudeHooksStructure = { hooks: { PostFileEdit: postFileEditRules, @@ -55,6 +82,11 @@ export function generateClaudeHooksConfig(detection: StackDetectionResult): stri return JSON.stringify(hooksConfig, null, 2) + '\n'; } +/** + * Generate .claude/settings.json enforcing locked mechanical gate governance. + * + * @returns Formatted JSON string for .claude/settings.json + */ export function generateClaudeSettingsConfig(): string { const settings = { governance: { diff --git a/src/generator/templates/lefthook.ts b/src/generator/templates/lefthook.ts index 49e5286..95c1ca9 100644 --- a/src/generator/templates/lefthook.ts +++ b/src/generator/templates/lefthook.ts @@ -1,73 +1,104 @@ +/** + * @file src/generator/templates/lefthook.ts + * @description Generates the lefthook.yml multi-threaded parallel git hook runner configuration. + * + * Implements: + * - Stage 2: Synchronous Local Pre-Commit Hard Gates (< 2.0s execution target) + * Runs parallel native binaries across staged files (Biome, Ruff, AISlop, TruffleHog, Typos, Actionlint) + * - Stage 3: CI & Codebase Graph Governance + * Runs deep analysis (Fallow, Sherif, OWASP Noir, Cargo Deny) during pre-push or CI. + */ + import type { StackDetectionResult } from '../../types/index.js'; +/** + * Generate a complete lefthook.yml configuration tailored to the detected repository stacks. + * + * @param detection Stack detection result containing language and workflow indicators + * @returns Formatted YAML string for lefthook.yml + */ export function generateLefthookConfig(detection: StackDetectionResult): string { const preCommitCommands: string[] = []; const prePushCommands: string[] = []; - // Stage 2: Pre-Commit Hard Gates (< 2.0s execution target) + // ========================================================================= + // Stage 2: Pre-Commit Hard Gates (< 2.0s execution target on staged files) + // ========================================================================= + + // JS/TS: Fast native linting & formatting via Biome on staged files if (detection.jsTs.detected) { preCommitCommands.push(` biome-check: glob: "*.{js,ts,jsx,tsx,json,jsonc}" run: npx @biomejs/biome check --staged --no-errors-on-unmatched`); } + // Python: Fast native linting & auto-fixing via Ruff on staged files if (detection.python.detected) { preCommitCommands.push(` ruff-check: glob: "*.py" run: ruff check --staged --fix`); } + // Go: Fast static security analysis via gosec if (detection.go.detected) { preCommitCommands.push(` gosec-check: glob: "*.go" run: gosec -quiet ./...`); } + // Rust: Cargo dependency and license policy checking if (detection.rust.detected) { preCommitCommands.push(` cargo-deny: glob: "Cargo.{toml,lock}" run: cargo deny check`); } - // Universal deterministic AI slop detection + // Universal: Fast deterministic AI slop and swallowed error detection preCommitCommands.push(` aislop-scan: run: npx aislop scan --staged --fail-on 50`); - // Universal secret scanning + // Universal: High-entropy secret scanning via TruffleHog OSS (verified secrets only) preCommitCommands.push(` secret-scan: run: trufflehog git file://. --staged --only-verified`); - // Universal typo & spelling check + // Universal: AST-aware source code spell checking via typos preCommitCommands.push(` typo-check: run: typos --staged`); - // Workflow validation + // Infra: GitHub Actions workflow syntax validation via actionlint if (detection.infra.hasWorkflows) { preCommitCommands.push(` actionlint: glob: ".github/workflows/*.{yml,yaml}" run: actionlint`); } - // Stage 3: Pre-Push / CI Graph Audit + // ========================================================================= + // Stage 3: Pre-Push / CI Full Codebase Graph Governance + // ========================================================================= + + // JS/TS: Full graph dead code, unused exports, and circular import audit if (detection.jsTs.detected) { prePushCommands.push(` fallow-audit: run: npx fallow audit`); } + // Monorepo: Cross-package dependency version alignment via Sherif if (detection.jsTs.isMonorepo) { prePushCommands.push(` monorepo-sherif: run: npx setup-sherif`); } - // Universal API attack surface mapping + // Universal: API attack surface mapping and shadow endpoint extraction via OWASP Noir prePushCommands.push(` noir-scan: run: noir scan . --ai-context -f sarif`); + // Rust: Comprehensive dependency policy verification if (detection.rust.detected && !prePushCommands.some(c => c.includes('cargo-deny'))) { prePushCommands.push(` cargo-deny: run: cargo deny check`); } + // Assemble the final YAML document with descriptive comments const parts = [ '# Generated by @heretek-ai/agent-proof - Mechanical Hard Gate Orchestration', '# Stage 2: Synchronous Local Pre-Commit Hard Gate (< 2.0s budget)', diff --git a/src/generator/templates/ruff.ts b/src/generator/templates/ruff.ts index 17f8eb3..d07805f 100644 --- a/src/generator/templates/ruff.ts +++ b/src/generator/templates/ruff.ts @@ -1,3 +1,14 @@ +/** + * @file src/generator/templates/ruff.ts + * @description Generates the ruff.toml configuration for sub-millisecond Python linting and formatting. + */ + +/** + * Generate a strict Ruff configuration targeting Python 3.10+ with rules + * for pyflakes (F), pycodestyle (E, W), flake8-bugbear (B), and simplify (SIM). + * + * @returns Formatted TOML string for ruff.toml + */ export function generateRuffConfig(): string { return `# Ruff Configuration for Agent-Proof Mechanical Hard-Gate line-length = 100 diff --git a/src/index.ts b/src/index.ts index dd7e756..69aa98e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,15 @@ /** - * @heretek-ai/agent-proof - Mechanical Hard-Gate CLI - * Zero-dependency architecture for deterministic AI code governance + * @file src/index.ts + * @description Public API Entrypoint for the @heretek-ai/agent-proof library. + * + * Exposes core modules: + * - Stack detection (StackDetector, detectStack) + * - Config generation (ConfigGenerator, generateConfigs, individual templates) + * - Diagnostic streaming & parsers (DiagnosticStreamer, formatDiagnostics, individual parsers) + * - Git hook installation (HookInstaller, installHooks) + * - Permission locking (GateLock, lockGovernance, unlockGovernance) + * - Stage execution runner (GateRunner) + * - TypeScript type definitions */ export * from './types/index.js'; @@ -9,4 +18,4 @@ export * from './generator/index.js'; export * from './formatter/index.js'; export * from './installer/index.js'; export * from './runner/index.js'; -export * from './cli.js'; +export { main } from './cli.js'; diff --git a/src/installer/hookInstaller.ts b/src/installer/hookInstaller.ts index f08367c..9448296 100644 --- a/src/installer/hookInstaller.ts +++ b/src/installer/hookInstaller.ts @@ -1,112 +1,132 @@ +/** + * @file src/installer/hookInstaller.ts + * @description Git hook installer and Lefthook orchestrator. + * + * Installs mechanical git hooks in .git/hooks/ (pre-commit, pre-push) that invoke + * Lefthook and route execution through the agent diagnostic streamer. + */ + import * as fs from 'node:fs'; import * as path from 'node:path'; -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import type { HookInstallResult } from '../types/index.js'; +/** + * Options for configuring HookInstaller + */ export interface HookInstallerOptions { + /** Target repository root directory (defaults to process.cwd()) */ cwd?: string; - force?: boolean; } +/** + * Mechanical Git Hook Installer. + * Ensures .git/hooks/pre-commit and .git/hooks/pre-push are present and executable. + */ export class HookInstaller { + /** Resolved absolute path of the target repository */ private readonly rootPath: string; - constructor(options: HookInstallerOptions = {}) { - this.rootPath = path.resolve(options.cwd || process.cwd()); + /** + * Initialize a new HookInstaller instance + * @param options Target path string or HookInstallerOptions object + */ + constructor(options: string | HookInstallerOptions = {}) { + if (typeof options === 'string') { + this.rootPath = path.resolve(options); + } else { + this.rootPath = path.resolve(options.cwd || process.cwd()); + } } /** - * Install and lock git hooks for pre-commit and pre-push stages + * Install mechanical git hooks into the repository. + * First attempts native `npx lefthook install`, and falls back to writing + * robust POSIX shell hook wrappers directly to `.git/hooks/`. + * + * @returns HookInstallResult with details of installed hooks */ public install(): HookInstallResult { - const gitDir = path.resolve(this.rootPath, '.git'); - if (!fs.existsSync(gitDir) || !fs.statSync(gitDir).isDirectory()) { + const gitDir = path.join(this.rootPath, '.git'); + const gitHooksDir = path.join(gitDir, 'hooks'); + let lefthookInstalled = false; + const installedHooks: string[] = []; + + // Check if .git directory exists + if (!fs.existsSync(gitDir)) { return { lefthookInstalled: false, - gitHooksPath: '', + gitHooksPath: gitHooksDir, installedHooks: [], - message: 'No .git repository found. Skipped git hook installation.', + message: 'No .git repository found. Git hooks skipped (not a git repository).', }; } - const hooksDir = path.resolve(gitDir, 'hooks'); - if (!fs.existsSync(hooksDir)) { - fs.mkdirSync(hooksDir, { recursive: true }); + // Ensure .git/hooks directory exists + if (!fs.existsSync(gitHooksDir)) { + fs.mkdirSync(gitHooksDir, { recursive: true }); } - let lefthookSuccess = false; + // 1. Attempt to run `npx lefthook install` natively try { - execSync('npx lefthook install', { + execFileSync('npx', ['lefthook', 'install', '-f'], { cwd: this.rootPath, stdio: 'pipe', timeout: 10000, }); - lefthookSuccess = true; + lefthookInstalled = true; } catch { - // If npx lefthook install is not yet available, fallback to manual hook script generation + // Fall back to direct hook file creation if lefthook CLI is not present + lefthookInstalled = false; } - const installedHooks: string[] = []; + // 2. Ensure pre-commit hook script exists and is executable + const preCommitPath = path.join(gitHooksDir, 'pre-commit'); + const preCommitScript = `#!/bin/sh +# Generated by @heretek-ai/agent-proof - Mechanical Pre-Commit Hard Gate +set -e - // Ensure pre-commit hook exists and is executable - const preCommitPath = path.resolve(hooksDir, 'pre-commit'); - const preCommitContent = `#!/bin/sh -# Agent-Proof Mechanical Hard-Gate Pre-Commit Hook -if command -v lefthook >/dev/null 2>&1; then - lefthook run pre-commit "$@" -elif [ -f "./node_modules/.bin/lefthook" ]; then - ./node_modules/.bin/lefthook run pre-commit "$@" +if command -v npx >/dev/null 2>&1; then + npx lefthook run pre-commit +elif command -v lefthook >/dev/null 2>&1; then + lefthook run pre-commit else - npx lefthook run pre-commit "$@" + echo "[agent-proof] Warning: neither npx nor lefthook found. Skipping pre-commit gate." fi `; + fs.writeFileSync(preCommitPath, preCommitScript, { mode: 0o755 }); + installedHooks.push('pre-commit'); - if (!fs.existsSync(preCommitPath) || lefthookSuccess) { - if (!fs.existsSync(preCommitPath)) { - fs.writeFileSync(preCommitPath, preCommitContent, { mode: 0o755 }); - } - try { - fs.chmodSync(preCommitPath, 0o755); - } catch { - // ignore chmod errors on windows - } - installedHooks.push('pre-commit'); - } + // 3. Ensure pre-push hook script exists and is executable + const prePushPath = path.join(gitHooksDir, 'pre-push'); + const prePushScript = `#!/bin/sh +# Generated by @heretek-ai/agent-proof - Mechanical Pre-Push Hard Gate +set -e - // Ensure pre-push hook exists and is executable - const prePushPath = path.resolve(hooksDir, 'pre-push'); - const prePushContent = `#!/bin/sh -# Agent-Proof Mechanical Hard-Gate Pre-Push Hook -if command -v lefthook >/dev/null 2>&1; then - lefthook run pre-push "$@" -elif [ -f "./node_modules/.bin/lefthook" ]; then - ./node_modules/.bin/lefthook run pre-push "$@" -else - npx lefthook run pre-push "$@" +if command -v npx >/dev/null 2>&1; then + npx lefthook run pre-push +elif command -v lefthook >/dev/null 2>&1; then + lefthook run pre-push fi `; - - if (!fs.existsSync(prePushPath)) { - fs.writeFileSync(prePushPath, prePushContent, { mode: 0o755 }); - try { - fs.chmodSync(prePushPath, 0o755); - } catch { - // ignore chmod errors on windows - } - installedHooks.push('pre-push'); - } + fs.writeFileSync(prePushPath, prePushScript, { mode: 0o755 }); + installedHooks.push('pre-push'); return { - lefthookInstalled: lefthookSuccess, - gitHooksPath: hooksDir, + lefthookInstalled, + gitHooksPath: gitHooksDir, installedHooks, - message: lefthookSuccess + message: lefthookInstalled ? 'Lefthook hooks successfully installed and verified.' - : 'Git hooks generated with lefthook fallback wrapper.', + : 'Installed direct POSIX git hook wrappers for Lefthook in .git/hooks/.', }; } } -export function installHooks(options?: HookInstallerOptions): HookInstallResult { - return new HookInstaller(options).install(); +/** + * Functional convenience wrapper to install git hooks + * @param cwd Target repository directory or options + */ +export function installHooks(cwd?: string | HookInstallerOptions): HookInstallResult { + return new HookInstaller(cwd).install(); } diff --git a/src/installer/index.ts b/src/installer/index.ts index d8bbe5d..f244b0e 100644 --- a/src/installer/index.ts +++ b/src/installer/index.ts @@ -1,2 +1,7 @@ -export * from './lockin.js'; +/** + * @file src/installer/index.ts + * @description Exports git hook installer and permission locking modules. + */ + export * from './hookInstaller.js'; +export * from './lockin.js'; diff --git a/src/installer/lockin.ts b/src/installer/lockin.ts index d4d07cf..eb89674 100644 --- a/src/installer/lockin.ts +++ b/src/installer/lockin.ts @@ -1,7 +1,20 @@ +/** + * @file src/installer/lockin.ts + * @description POSIX File Permission Locker for mechanical governance boundaries. + * + * Enforces immutable read-only permissions (`0o444` / `r--r--r--`) on governance + * configuration files (`.claude/settings.json`, `.claude/hooks.json`, `lefthook.yml`, etc.) + * to mechanically prevent autonomous AI coding agents from tampering with, disabling, or + * weakening hard gates. + */ + import * as fs from 'node:fs'; import * as path from 'node:path'; import type { LockResult } from '../types/index.js'; +/** + * List of critical governance files locked by default to prevent agent tampering + */ export const GOVERNANCE_FILES = [ '.claude/settings.json', '.claude/hooks.json', @@ -11,30 +24,58 @@ export const GOVERNANCE_FILES = [ '.aislop/config.yml', ]; -export class GateLock { - private readonly rootPath: string; +/** + * Options for configuring GateLock operations + */ +export interface GateLockOptions { + /** Target repository root directory (defaults to process.cwd()) */ + cwd?: string; + /** Custom list of file paths to lock or unlock */ + files?: string[]; +} - constructor(rootPath: string = process.cwd()) { - this.rootPath = path.resolve(rootPath); +/** + * POSIX File Permission Manager for Mechanical Gate Governance. + */ +export class GateLock { + /** Resolved absolute path of the target repository */ + private readonly rootPath: string; + /** Files targeted for permission locking */ + private readonly targetFiles: string[]; + + /** + * Initialize a new GateLock instance + * @param options Directory string or GateLockOptions object + */ + constructor(options: string | GateLockOptions = {}) { + if (typeof options === 'string') { + this.rootPath = path.resolve(options); + this.targetFiles = GOVERNANCE_FILES; + } else { + this.rootPath = path.resolve(options.cwd || process.cwd()); + this.targetFiles = options.files || GOVERNANCE_FILES; + } } /** - * Lock governance files with read-only permissions to prevent AI agents from tampering with rules + * Lock all governance files by applying read-only permissions (0o444). + * + * @returns LockResult detailing locked and failed files */ - public lock(files: string[] = GOVERNANCE_FILES): LockResult { + public lock(): LockResult { const lockedFiles: string[] = []; const failedFiles: string[] = []; - for (const relFile of files) { - const fullPath = path.resolve(this.rootPath, relFile); - if (!fs.existsSync(fullPath)) continue; - - try { - // Mode 0o444 is read-only (r--r--r--) - fs.chmodSync(fullPath, 0o444); - lockedFiles.push(relFile); - } catch { - failedFiles.push(relFile); + for (const relPath of this.targetFiles) { + const fullPath = path.resolve(this.rootPath, relPath); + if (fs.existsSync(fullPath)) { + try { + // 0o444 = read-only for owner, group, and others + fs.chmodSync(fullPath, 0o444); + lockedFiles.push(relPath); + } catch { + failedFiles.push(relPath); + } } } @@ -46,22 +87,25 @@ export class GateLock { } /** - * Unlock governance files with writeable permissions (for human administrator modifications) + * Unlock governance files by restoring standard read/write permissions (0o644). + * Allows human developers or administrative workflows to modify configs. + * + * @returns LockResult detailing unlocked and failed files */ - public unlock(files: string[] = GOVERNANCE_FILES): LockResult { + public unlock(): LockResult { const lockedFiles: string[] = []; const failedFiles: string[] = []; - for (const relFile of files) { - const fullPath = path.resolve(this.rootPath, relFile); - if (!fs.existsSync(fullPath)) continue; - - try { - // Mode 0o644 is read/write for owner (rw-r--r--) - fs.chmodSync(fullPath, 0o644); - lockedFiles.push(relFile); - } catch { - failedFiles.push(relFile); + for (const relPath of this.targetFiles) { + const fullPath = path.resolve(this.rootPath, relPath); + if (fs.existsSync(fullPath)) { + try { + // 0o644 = read/write for owner, read-only for group/others + fs.chmodSync(fullPath, 0o644); + lockedFiles.push(relPath); + } catch { + failedFiles.push(relPath); + } } } @@ -73,44 +117,59 @@ export class GateLock { } /** - * Check whether governance files exist and are currently locked (read-only) + * Check the current permission status of all governance files as an array. */ - public getStatus(files: string[] = GOVERNANCE_FILES): Record { - const status: Record = {}; - - for (const relFile of files) { - const fullPath = path.resolve(this.rootPath, relFile); + public checkStatus(): Array<{ path: string; exists: boolean; isLocked: boolean; mode?: string }> { + return this.targetFiles.map(relPath => { + const fullPath = path.resolve(this.rootPath, relPath); if (!fs.existsSync(fullPath)) { - status[relFile] = { exists: false, isLocked: false }; - continue; + return { path: relPath, exists: false, isLocked: false }; } try { const stats = fs.statSync(fullPath); - // In POSIX, write permission bit for user is 0o200 - const isWritable = (stats.mode & 0o200) !== 0; - status[relFile] = { - exists: true, - isLocked: !isWritable, - mode: stats.mode, - }; + // If owner write bit (0o200) is NOT set, file is read-only / locked + const isLocked = (stats.mode & 0o200) === 0; + const modeStr = (stats.mode & 0o777).toString(8); + return { path: relPath, exists: true, isLocked, mode: modeStr }; } catch { - status[relFile] = { exists: true, isLocked: false }; + return { path: relPath, exists: true, isLocked: false }; } - } + }); + } - return status; + /** + * Return a dictionary of governance file statuses keyed by relative file path. + */ + public getStatus(): Record { + const result: Record = {}; + for (const item of this.checkStatus()) { + result[item.path] = item; + } + return result; } } -export function lockGovernance(rootPath?: string, files?: string[]): LockResult { - return new GateLock(rootPath).lock(files); +/** + * Functional convenience wrapper to lock governance files + * @param cwd Target repository directory or options + */ +export function lockGovernance(cwd?: string | GateLockOptions): LockResult { + return new GateLock(cwd).lock(); } -export function unlockGovernance(rootPath?: string, files?: string[]): LockResult { - return new GateLock(rootPath).unlock(files); +/** + * Functional convenience wrapper to unlock governance files + * @param cwd Target repository directory or options + */ +export function unlockGovernance(cwd?: string | GateLockOptions): LockResult { + return new GateLock(cwd).unlock(); } -export function getGovernanceStatus(rootPath?: string, files?: string[]) { - return new GateLock(rootPath).getStatus(files); +/** + * Functional convenience wrapper to get governance status dictionary + * @param cwd Target repository directory or options + */ +export function getGovernanceStatus(cwd?: string | GateLockOptions): Record { + return new GateLock(cwd).getStatus(); } diff --git a/src/runner/gateRunner.ts b/src/runner/gateRunner.ts index 16d5145..ea98020 100644 --- a/src/runner/gateRunner.ts +++ b/src/runner/gateRunner.ts @@ -1,154 +1,136 @@ -import { spawnSync } from 'node:child_process'; +/** + * @file src/runner/gateRunner.ts + * @description Execution engine for mechanical gate stages (post-edit, pre-commit, pre-push). + * + * Runs stage-specific linters, formatters, and scanners, parses failure streams, + * and emits structured LSP Diagnostic Envelopes with exit codes. + */ + import * as path from 'node:path'; +import { spawnSync } from 'node:child_process'; import type { DiagnosticEnvelope, GateStage } from '../types/index.js'; -import { DiagnosticStreamer } from '../formatter/diagnosticStream.js'; -import { detectStack } from '../detector/stackDetector.js'; +import { DiagnosticStreamer, type ToolExecutionResult } from '../formatter/diagnosticStream.js'; -export interface RunOptions { - stage: GateStage | 'pre-commit' | 'pre-push' | 'post-edit'; - filePath?: string; +/** + * Options for configuring GateRunner execution + */ +export interface GateRunnerOptions { + /** Target working directory (defaults to process.cwd()) */ cwd?: string; - jsonOutput?: boolean; -} - -export interface RunResult { - passed: boolean; - exitCode: number; - envelope: DiagnosticEnvelope; - rawOutput: string; + /** Custom environment variables */ + env?: NodeJS.ProcessEnv; } +/** + * Stage Execution Engine for Mechanical Hard Gates. + */ export class GateRunner { + /** Resolved absolute path of the target repository */ private readonly rootPath: string; + /** Environment variables passed to child processes */ + private readonly env: NodeJS.ProcessEnv; - constructor(cwd: string = process.cwd()) { - this.rootPath = path.resolve(cwd); + /** + * Initialize a new GateRunner instance + * @param options Runner configuration options + */ + constructor(options: GateRunnerOptions = {}) { + this.rootPath = path.resolve(options.cwd || process.cwd()); + this.env = { ...process.env, ...options.env }; } /** - * Execute gate for a given stage + * Execute Stage 1: PostFileEdit tool interceptor on a single modified file. + * Runs in < 300ms on ASTs using Biome, Ruff, or SkillCheck. + * + * @param filePath Relative or absolute path of the modified file + * @returns DiagnosticEnvelope detailing formatting/linting findings */ - public run(options: RunOptions): RunResult { + public runPostEdit(filePath: string): DiagnosticEnvelope { const startTime = Date.now(); - const stage = this.normalizeStage(options.stage); - - if (stage === 'PostFileEdit' && options.filePath) { - return this.runPostFileEdit(options.filePath, startTime); - } - - if (stage === 'PrePush') { - return this.runPrePush(startTime); - } - - return this.runPreCommit(startTime); - } - - private runPostFileEdit(filePath: string, startTime: number): RunResult { const fullPath = path.resolve(this.rootPath, filePath); - const detection = detectStack(this.rootPath); - const results: Array<{ toolName: string; output: string; exitCode: number }> = []; + const ext = path.extname(fullPath).toLowerCase(); + const toolResults: ToolExecutionResult[] = []; - if (/\.(js|ts|jsx|tsx|json|jsonc)$/.test(filePath) && detection.jsTs.detected) { - const res = this.execCommand('npx', ['biome', 'check', '--write', fullPath]); - results.push({ toolName: 'biome', output: res.stdout + res.stderr, exitCode: res.status ?? 0 }); + // JS/TS: Run Biome check with auto-write + if (['.js', '.ts', '.jsx', '.tsx', '.json', '.jsonc'].includes(ext)) { + const res = this.execCommand('npx', ['@biomejs/biome', 'check', '--write', fullPath]); + toolResults.push({ toolName: 'biome', output: res.output, exitCode: res.exitCode }); } - if (/\.py$/.test(filePath) && detection.python.detected) { + // Python: Run Ruff check with auto-fix + if (['.py'].includes(ext)) { const res = this.execCommand('ruff', ['check', '--fix', fullPath]); - results.push({ toolName: 'ruff', output: res.stdout + res.stderr, exitCode: res.status ?? 0 }); + toolResults.push({ toolName: 'ruff', output: res.output, exitCode: res.exitCode }); } - if (filePath.includes('.claude/skills/') || filePath.endsWith('SKILL.md')) { + // Skill markdown: Run skillcheck validator + if (fullPath.includes('.claude/skills') || fullPath.endsWith('SKILL.md')) { const res = this.execCommand('skillcheck', ['check', fullPath]); - results.push({ toolName: 'skillcheck', output: res.stdout + res.stderr, exitCode: res.status ?? 0 }); + toolResults.push({ toolName: 'skillcheck', output: res.output, exitCode: res.exitCode }); } const duration = Date.now() - startTime; - const envelope = DiagnosticStreamer.aggregate(results, { + return DiagnosticStreamer.aggregate(toolResults, { stage: 'PostFileEdit', executionTimeMs: duration, }); - - const passed = envelope.summary.total_errors === 0; - const combinedOutput = results.map(r => r.output).join('\n'); - - return { - passed, - exitCode: passed ? 0 : 1, - envelope, - rawOutput: combinedOutput, - }; } - private runPreCommit(startTime: number): RunResult { - // Attempt running lefthook pre-commit - const lefthookRes = this.execCommand('npx', ['lefthook', 'run', 'pre-commit']); - const rawOutput = (lefthookRes.stdout || '') + (lefthookRes.stderr || ''); + /** + * Execute Stage 2: Synchronous Local Pre-Commit Hard Gate (< 2.0s target). + * Invokes Lefthook to run parallel staged checks (Biome, Ruff, AISlop, TruffleHog, Typos, Actionlint). + * + * @returns DiagnosticEnvelope aggregating all staged failure diagnostics + */ + public runPreCommit(): DiagnosticEnvelope { + const startTime = Date.now(); + const res = this.execCommand('npx', ['lefthook', 'run', 'pre-commit']); const duration = Date.now() - startTime; - const envelope = DiagnosticStreamer.format(rawOutput, { - stage: 'PreCommit', - toolName: 'lefthook', - executionTimeMs: duration, - }); - - const passed = (lefthookRes.status === 0) && envelope.summary.total_errors === 0; - - return { - passed, - exitCode: passed ? 0 : (lefthookRes.status || 1), - envelope, - rawOutput, - }; + return DiagnosticStreamer.aggregate( + [{ toolName: 'lefthook-pre-commit', output: res.output, exitCode: res.exitCode }], + { stage: 'PreCommit', executionTimeMs: duration } + ); } - private runPrePush(startTime: number): RunResult { - const lefthookRes = this.execCommand('npx', ['lefthook', 'run', 'pre-push']); - const rawOutput = (lefthookRes.stdout || '') + (lefthookRes.stderr || ''); + /** + * Execute Stage 3: Pre-Push / CI Full Codebase Graph Governance. + * Invokes Lefthook to run pre-push checks (Fallow, Sherif, OWASP Noir). + * + * @returns DiagnosticEnvelope aggregating graph audit results + */ + public runPrePush(): DiagnosticEnvelope { + const startTime = Date.now(); + const res = this.execCommand('npx', ['lefthook', 'run', 'pre-push']); const duration = Date.now() - startTime; - const envelope = DiagnosticStreamer.format(rawOutput, { - stage: 'PrePush', - toolName: 'lefthook', - executionTimeMs: duration, - }); - - const passed = (lefthookRes.status === 0) && envelope.summary.total_errors === 0; - - return { - passed, - exitCode: passed ? 0 : (lefthookRes.status || 1), - envelope, - rawOutput, - }; + return DiagnosticStreamer.aggregate( + [{ toolName: 'lefthook-pre-push', output: res.output, exitCode: res.exitCode }], + { stage: 'PrePush', executionTimeMs: duration } + ); } - private execCommand(command: string, args: string[]) { + /** + * Helper to spawn a shell command synchronously and capture output + */ + private execCommand(command: string, args: string[]): { exitCode: number; output: string } { try { - return spawnSync(command, args, { + const proc = spawnSync(command, args, { cwd: this.rootPath, + env: this.env, encoding: 'utf-8', - stdio: 'pipe', - timeout: 30000, + shell: process.platform === 'win32', }); + + const stdout = proc.stdout || ''; + const stderr = proc.stderr || ''; + const output = `${stdout}\n${stderr}`.trim(); + const exitCode = proc.status ?? (proc.error ? 1 : 0); + + return { exitCode, output }; } catch (err: any) { - return { - status: 1, - stdout: '', - stderr: err?.message || String(err), - }; + return { exitCode: 1, output: err.message || 'Execution failed' }; } } - - private normalizeStage(stage: string): GateStage { - const s = stage.toLowerCase().replace(/[^a-z]/g, ''); - if (s.includes('post') || s.includes('edit')) return 'PostFileEdit'; - if (s.includes('push')) return 'PrePush'; - if (s.includes('ci')) return 'CI'; - return 'PreCommit'; - } -} - -export function runGate(options: RunOptions): RunResult { - return new GateRunner(options.cwd).run(options); } diff --git a/src/runner/index.ts b/src/runner/index.ts index b8aaf2d..4d6be8e 100644 --- a/src/runner/index.ts +++ b/src/runner/index.ts @@ -1 +1,6 @@ +/** + * @file src/runner/index.ts + * @description Exports the mechanical gate execution runner module. + */ + export * from './gateRunner.js'; diff --git a/src/types/index.ts b/src/types/index.ts index d7e419b..371409d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,124 +1,243 @@ /** - * Core type definitions for @agent-gate/cli + * @file src/types/index.ts + * @description Core TypeScript type definitions, LSP schemas, and diagnostic data structures + * for the @heretek-ai/agent-proof mechanical hard-gate CLI. + * + * Defines the contract between stack detectors, configuration generators, + * tool failure parsers, and autonomous AI self-correction diagnostic streams. */ +/** + * Result of the multi-stack repository inspection engine. + * Contains boolean indicators, detected signature files, and metadata + * across all supported language ecosystems, infrastructure, and agent harnesses. + */ export interface StackDetectionResult { + /** Absolute root path of the inspected repository */ rootPath: string; + + /** JavaScript & TypeScript ecosystem detection details */ jsTs: { + /** True if any JS/TS signature files (package.json, tsconfig.json, etc.) were found */ detected: boolean; + /** List of relative file paths that triggered JS/TS detection */ files: string[]; + /** True if TypeScript configuration or dependencies are present */ hasTypeScript: boolean; + /** True if Biome linter/formatter configuration is already present */ hasBiome: boolean; + /** True if monorepo workspace configuration (pnpm, yarn, lerna, turbo) was detected */ isMonorepo: boolean; + /** Detected package manager (npm, pnpm, yarn, bun, or deno) */ packageManager?: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'deno'; }; + + /** Python ecosystem detection details */ python: { + /** True if any Python signature files (pyproject.toml, requirements.txt, etc.) were found */ detected: boolean; + /** List of relative file paths that triggered Python detection */ files: string[]; + /** True if pyproject.toml is present */ hasPyproject: boolean; + /** True if Ruff configuration is defined in ruff.toml or pyproject.toml */ hasRuffConfig: boolean; }; + + /** Go ecosystem detection details */ go: { + /** True if Go signature files (go.mod, go.sum, etc.) were found */ detected: boolean; + /** List of relative file paths that triggered Go detection */ files: string[]; }; + + /** Rust ecosystem detection details */ rust: { + /** True if Cargo signature files (Cargo.toml, Cargo.lock) were found */ detected: boolean; + /** List of relative file paths that triggered Rust detection */ files: string[]; + /** True if Cargo.toml defines a multi-crate workspace */ isWorkspace: boolean; }; + + /** Infrastructure, CI workflows, and containerization detection */ infra: { + /** True if CI workflow files or Docker containers were detected */ detected: boolean; + /** True if GitHub Actions workflow YAML files are present */ hasWorkflows: boolean; + /** List of relative paths to GitHub Actions workflow files */ workflowFiles: string[]; + /** True if Dockerfile or docker-compose files are present */ hasDocker: boolean; + /** List of relative paths to Docker / container definition files */ dockerFiles: string[]; }; + + /** AI agent harness, skill definitions, and configuration files */ agentHarness: { + /** True if any AI agent harness files (.claude, .cursor, SKILL.md) were found */ detected: boolean; + /** True if Claude Code harness (.claude/ or CLAUDE.md) is present */ hasClaude: boolean; + /** True if Cursor IDE harness (.cursor/ or .cursorrules) is present */ hasCursor: boolean; + /** True if agent skill markdown files (.claude/skills/*.md or SKILL.md) are present */ hasSkillFiles: boolean; + /** True if AGENTS.md manifest is present */ hasAgentsMd: boolean; + /** List of relative paths to agent harness files */ files: string[]; }; + + /** High-level summary of detected stacks */ summary: { + /** Human-readable names of primary detected language/ecosystem stacks */ primaryStacks: string[]; + /** Total count of all matching indicator files across all stacks */ totalIndicators: number; }; } +/** + * Supported execution stages in the 3-tier mechanical gate lifecycle + */ export type GateStage = 'PostFileEdit' | 'PreCommit' | 'PrePush' | 'CI'; +/** + * Severity level for diagnostic items conforming to LSP standards + */ export type DiagnosticSeverity = 'ERROR' | 'WARNING' | 'INFO' | 'HINT'; +/** + * 1-indexed source code coordinate range where a violation occurred + */ export interface DiagnosticRange { + /** Start line and column of the violation */ start: { line: number; column: number; }; + /** End line and column of the violation */ end: { line: number; column: number; }; } +/** + * Structured self-correction repair instruction for AI agents. + * Contains actionable code replacement tokens that an LLM can apply directly. + */ export interface RepairInstruction { + /** Action category guiding the agent's edit strategy */ action: 'REWRITE_BLOCK' | 'REPLACE_TOKEN' | 'INSERT_IMPORT' | 'DELETE_LINE' | 'EXECUTE_COMMAND' | 'MANUAL_FIX'; + /** Human and LLM readable explanation of how to fix the violation */ description: string; + /** Concrete code replacement strings or repair tokens for autonomous correction */ repair_tokens: string[]; + /** Optional shell command that can automatically resolve the issue (e.g. biome check --write) */ suggested_command?: string; } +/** + * Individual diagnostic item representing a single lint, security, or slop violation + */ export interface DiagnosticItem { + /** Name of the tool or engine that produced the finding (e.g. 'aislop', 'biome', 'ruff') */ source: string; + /** Machine-readable rule identifier (e.g. 'AI_SLOP_SWALLOWED_ERROR', 'F401', 'noExplicitAny') */ rule_id: string; + /** Severity level (ERROR, WARNING, INFO, HINT) */ severity: DiagnosticSeverity; + /** Relative or absolute path to the file containing the violation */ file_path: string; + /** Exact line/column range in the source file */ range?: DiagnosticRange; + /** Relevant code snippet around the violation */ code_snippet?: string; + /** Detailed error message describing the violation */ error_message: string; + /** Actionable repair guidance and tokens for autonomous self-correction */ repair_instruction?: RepairInstruction; } +/** + * Top-level LSP-compliant diagnostic envelope emitted by the Diagnostic Streamer. + * Conforms to JSON Schema: https://json.schemastore.org/lsif.json + */ export interface DiagnosticEnvelope { + /** JSON Schema URI for validation */ $schema: string; + /** Schema specification version */ version: string; + /** Overall gate status: GATE_PASSED if 0 errors, GATE_FAILED otherwise */ status: 'GATE_PASSED' | 'GATE_FAILED'; + /** Aggregated error and warning counts for this stage */ summary: { total_errors: number; total_warnings: number; gate_stage: GateStage; }; + /** List of all diagnostic items detected across all executed tools */ diagnostics: DiagnosticItem[]; + /** Execution metadata including timing and per-tool exit codes */ metadata?: { execution_time_ms?: number; tool_outputs?: Record; }; } +/** + * Representation of a generated configuration file to be written to disk + */ export interface GeneratedConfig { + /** Relative destination path (e.g. 'lefthook.yml', '.claude/hooks.json') */ path: string; + /** Complete file content string */ content: string; + /** Descriptive summary of the configuration file's role */ description: string; + /** If true, the file will be marked read-only (chmod 0444) to prevent agent tampering */ isImmutable?: boolean; } +/** + * Result of emitting configuration files to disk + */ export interface GenerationResult { + /** All configuration templates computed for the detected stacks */ configs: GeneratedConfig[]; + /** Relative paths of files successfully created or updated on disk */ writtenFiles: string[]; + /** Relative paths of files skipped because they already exist and force was false */ skippedFiles: string[]; } +/** + * Result of locking or unlocking governance file permissions + */ export interface LockResult { + /** Files that were successfully updated with new permissions */ lockedFiles: string[]; + /** Files that could not be updated due to errors */ failedFiles: string[]; + /** Current mode applied: 'locked' (0444) or 'unlocked' (0644) */ mode: 'locked' | 'unlocked'; } +/** + * Result of installing mechanical git hooks + */ export interface HookInstallResult { + /** True if lefthook CLI installed hooks natively */ lefthookInstalled: boolean; + /** Path to the .git/hooks directory */ gitHooksPath: string; + /** List of hook names installed (e.g. ['pre-commit', 'pre-push']) */ installedHooks: string[]; + /** Status description message */ message: string; }