docs: add comprehensive code comments and AGENTS.md, CLAUDE.md, GEMINI.md instructions

This commit is contained in:
John Smith committed 2026-08-20 15:09:19 -04:00
1 parent 3e4896a315
commit fff4c4571f
34 files changed
+1710 -1007

No files matched your search

+100
View File
@@ -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`.
+91
View File
@@ -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.
+66
View File
@@ -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.
+71 -104
View File
@@ -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();
+7
View File
@@ -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');
+25 -8
View File
@@ -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', '[email protected]'], { 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!');
+205 -253
View File
@@ -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 <stage>: 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<number> {
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 <command> [options]
agent-gate <command> [options]
$ agent-proof <command> [options]
$ agent-gate <command> [options]
COMMANDS:
init [dir] Initialize mechanical gates in target repository (default)
detect [dir] Inspect and report multi-stack indicators
run <stage> [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 <stage> [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<void> {
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);
});
}
+5
View File
@@ -1 +1,6 @@
/**
* @file src/detector/index.ts
* @description Exports the repository stack detection module.
*/
export * from './stackDetector.js';
+87 -6
View File
@@ -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();
}
+11
View File
@@ -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 '';
+100 -97
View File
@@ -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<string, { exitCode: number; stderr: string; stdout: string }>;
/** 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<string, { exitCode: number; stderr: string; stdout: string }> = {};
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
);
}
+5
View File
@@ -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';
+22 -16
View File
@@ -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}`],
},
});
}
+27 -9
View File
@@ -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')) {
+58 -69
View File
@@ -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<DiagnosticItem> | 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<string, unknown>'],
};
}
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<string, unknown>;'],
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}`,
};
}
+51 -35
View File
@@ -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`],
},
});
}
+64 -39
View File
@@ -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}`,
};
}
+53 -72
View File
@@ -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'],
};
}
+27 -18
View File
@@ -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 <detector> secret in <file>:<line>
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()}`],
},
});
}
+54 -42
View File
@@ -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],
},
});
}
}
+45 -3
View File
@@ -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);
}
+5
View File
@@ -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';
+11
View File
@@ -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"
+11
View File
@@ -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',
+33 -1
View File
@@ -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: {
+38 -7
View File
@@ -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)',
+11
View File
@@ -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
+12 -3
View File
@@ -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';
+83 -63
View File
@@ -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();
}
+6 -1
View File
@@ -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';
+111 -52
View File
@@ -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<string, { exists: boolean; isLocked: boolean; mode?: number }> {
const status: Record<string, { exists: boolean; isLocked: boolean; mode?: number }> = {};
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<string, { exists: boolean; isLocked: boolean; mode?: string }> {
const result: Record<string, { exists: boolean; isLocked: boolean; mode?: string }> = {};
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<string, { exists: boolean; isLocked: boolean; mode?: string }> {
return new GateLock(cwd).getStatus();
}
+90 -108
View File
@@ -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);
}
+5
View File
@@ -1 +1,6 @@
/**
* @file src/runner/index.ts
* @description Exports the mechanical gate execution runner module.
*/
export * from './gateRunner.js';
+120 -1
View File
@@ -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<string, { exitCode: number; stderr: string; stdout: string }>;
};
}
/**
* 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;
}