diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0488089..f45d7e0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,21 @@ Versions follow [Semantic Versioning](https://semver.org/).
---
+## v3.2.0 — 2026-05-16
+
+**Governance Evidence Artifact.**
+
+- Added `tools/governance-report.js` — single-pass governance report generator that runs file length, secrets, behavioral tests, source integrity, and runtime hook checks, producing a structured JSON artifact (`.code-warden-report.json`) and optional Markdown output
+- Three output modes: default (writes JSON artifact + prints summary), `--format=json` (JSON to stdout), `--format=md` (Markdown table to stdout for `$GITHUB_STEP_SUMMARY`)
+- Report includes git metadata (branch, commit), check results with violation details, and runtime hook registration status (Claude Code, Codex)
+- Exit code reflects overall result: `0` = all checks pass, `1` = one or more failures
+- Updated `templates/ci/github-actions.yml` — replaces individual lint/secrets steps with governance report, adds `$GITHUB_STEP_SUMMARY` Markdown publishing, adds artifact upload with 90-day retention
+- Added npm scripts: `report`, `report:json`, `report:md`
+- Updated README with "Governance Evidence" section and new positioning
+- New positioning: "Verifiable governance for AI-assisted development — checks, hooks, and evidence that agents stayed within policy"
+
+---
+
## v3.1.1 — 2026-05-15
**Stabilization — behavioral tests, shared policy modules, line-count fix.**
diff --git a/README.md b/README.md
index 9a4618d..68c48bb 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
-
+
@@ -148,7 +148,9 @@ node install.js --uninstall-hooks=codex # remove Codex hooks
```bash
npm run lint # scan full project tree for oversized files
npm run check-secrets # scan full project tree for hardcoded credentials
-npm run ci # lint + secrets + doctor
+npm run report # governance report — writes .code-warden-report.json
+npm run report:md # governance report as Markdown (pipe to PR summary)
+npm run ci # lint + secrets + test + doctor
npm run install-auto # node install.js
npm run install-doctor # node install.js --doctor
```
@@ -198,20 +200,40 @@ Codex exposes `apply_patch` and `Bash` at `PreToolUse` — not `Write`/`Edit`. T
Doctor and `--verify-target=` validate hook script paths when hooks are registered.
+## Governance Evidence
+
+Code-Warden produces a machine-readable governance report — verifiable evidence that checks ran and passed:
+
+```bash
+node tools/governance-report.js . # writes .code-warden-report.json
+node tools/governance-report.js . --format=md # Markdown table for PR summaries
+```
+
+The report covers file length, hardcoded credentials, behavioral tests, source integrity, and runtime hook status in a single pass. In CI, it pipes directly into `$GITHUB_STEP_SUMMARY` so every PR shows what was checked.
+
## CI Integration
```yaml
- name: Install Code-Warden
run: |
curl -fsSL -o cw.zip \
- https://github.com/Kodaxadev/Code-Warden/releases/download/v3.1.0/code-warden-v3.1.0.zip
+ https://github.com/Kodaxadev/Code-Warden/releases/download/v3.2.0/code-warden-v3.2.0.zip
unzip -q cw.zip -d .code-warden-ci
-- name: Lint — file length limits
- run: node .code-warden-ci/tools/warden-lint.js .
+- name: Governance report
+ run: node .code-warden-ci/tools/governance-report.js .
+
+- name: Publish governance summary
+ if: always()
+ run: node .code-warden-ci/tools/governance-report.js . --format=md >> $GITHUB_STEP_SUMMARY
-- name: Secrets — zero-trust scan
- run: node .code-warden-ci/tools/verify-secrets.js .
+- name: Upload governance artifact
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: code-warden-report
+ path: .code-warden-report.json
+ retention-days: 90
```
Full template: [`code-warden/templates/ci/github-actions.yml`](code-warden/templates/ci/github-actions.yml)
@@ -234,7 +256,7 @@ Full template: [`code-warden/templates/ci/github-actions.yml`](code-warden/templ
## Version
-v3.1.0 — See [`code-warden/SKILL.md`](code-warden/SKILL.md) for full changelog.
+v3.2.0 — See [`CHANGELOG.md`](CHANGELOG.md) for full changelog.
## Author
diff --git a/code-warden/README.md b/code-warden/README.md
index dec42df..2adb83e 100644
--- a/code-warden/README.md
+++ b/code-warden/README.md
@@ -2,7 +2,10 @@
> Portable AI Coding Governance Layer
-Code-Warden is a portable governance layer for AI coding agents. It enforces scoped planning, patch discipline, file-size limits, the zero-trust secrets policy, verification evidence, install health, and optional Claude Code pre-tool-use blocking.
+Code-Warden provides verifiable governance for AI-assisted development.
+It does not just ask agents to follow rules — it adds Scope Gates, Plan Gates,
+local checks, CI enforcement, runtime hooks where supported, and governance
+artifacts that show what was checked before code was accepted.
## Four Layers
@@ -17,6 +20,43 @@ Code-Warden is a portable governance layer for AI coding agents. It enforces sco
| **Installer and health** | Cross-app auto-installer, manifest-backed installs, `--doctor`, `--verify-target`, Windsurf adapter |
| **Hard enforcement** | Claude Code `PreToolUse` hooks — block oversized writes and hardcoded secrets before the file system is touched |
+## Governance Evidence
+
+Generate a machine-readable governance report that can be stored in CI, attached to PRs, or used as audit evidence:
+
+```bash
+node tools/governance-report.js . # write .code-warden-report.json + summary
+node tools/governance-report.js . --format=json # JSON to stdout
+node tools/governance-report.js . --format=md # Markdown to stdout
+```
+
+The report runs all checks in a single pass (file length, secrets, behavioral tests, source integrity) and produces a structured artifact:
+
+```json
+{
+ "tool": "code-warden",
+ "version": "3.2.0",
+ "checks": {
+ "fileLength": { "status": "pass", "filesScanned": 34, "violations": 0 },
+ "secrets": { "status": "pass", "filesScanned": 34, "violations": 0 },
+ "behavioralTests": { "status": "pass", "tests": 8, "failures": 0 },
+ "installHealth": { "status": "pass" }
+ },
+ "result": "pass"
+}
+```
+
+In CI, the Markdown format pipes directly into `$GITHUB_STEP_SUMMARY` for PR-visible evidence:
+
+| Check | Result | Details |
+|-------|--------|---------|
+| File length | PASS | 34 files scanned, 0 violations |
+| Hardcoded credentials | PASS | 34 files scanned, 0 violations |
+| Behavioral tests | PASS | 8 tests, 0 failures |
+| Install health | PASS | All source files present |
+
+See [`templates/ci/github-actions.yml`](templates/ci/github-actions.yml) for the full CI template with artifact upload.
+
## Install
```bash
@@ -48,6 +88,9 @@ Each install writes a `.code-warden-install.json` manifest (version, target, for
```bash
npm run lint # warden-lint on full project tree
npm run check-secrets # verify-secrets on full project tree
+npm run report # governance report, writes .code-warden-report.json
+npm run report:json # governance report as JSON to stdout
+npm run report:md # governance report as Markdown to stdout
npm run install-auto # node install.js
npm run install-dry-run # node install.js --dry-run
npm run install-list # node install.js --list
diff --git a/code-warden/package.json b/code-warden/package.json
index 7798b1a..88cb449 100644
--- a/code-warden/package.json
+++ b/code-warden/package.json
@@ -1,12 +1,15 @@
{
"name": "code-warden",
- "version": "3.1.1",
- "description": "Production-grade AI development governance skill for Codex, Claude Code, and Cowork.",
+ "version": "3.2.0",
+ "description": "Verifiable governance for AI-assisted development — checks, hooks, and evidence.",
"main": "SKILL.md",
"scripts": {
"lint": "node tools/warden-lint.js .",
"check-secrets": "node tools/verify-secrets.js .",
"get-context": "node tools/get-context.js",
+ "report": "node tools/governance-report.js .",
+ "report:json": "node tools/governance-report.js . --format=json",
+ "report:md": "node tools/governance-report.js . --format=md",
"install-auto": "node install.js",
"install-dry-run": "node install.js --dry-run",
"install-list": "node install.js --list",
diff --git a/code-warden/templates/ci/github-actions.yml b/code-warden/templates/ci/github-actions.yml
index ab351b1..8a6ac89 100644
--- a/code-warden/templates/ci/github-actions.yml
+++ b/code-warden/templates/ci/github-actions.yml
@@ -4,8 +4,15 @@
# Copy this file to .github/workflows/code-warden.yml in your project.
#
# What it enforces:
-# - File length limits (warden-lint.js, default 400 lines per codewarden.json)
-# - Zero-trust secrets (verify-secrets.js, hardcoded-credential patterns)
+# - File length limits (default 400 lines per codewarden.json)
+# - Zero-trust secrets (hardcoded-credential patterns)
+# - Behavioral tests (scanner and hook pass/fail verification)
+# - Source integrity (required files present)
+#
+# What it produces:
+# - .code-warden-report.json — machine-readable governance artifact
+# - Markdown summary on the workflow run / PR (via GITHUB_STEP_SUMMARY)
+# - Uploaded artifact for audit trail (90-day retention)
#
# How code-warden is made available in CI (choose one):
#
@@ -33,7 +40,7 @@ on:
branches: [main, master]
env:
- CODE_WARDEN_VERSION: v3.1.0
+ CODE_WARDEN_VERSION: v3.2.0
CODE_WARDEN_PATH: .code-warden-ci
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
@@ -59,8 +66,18 @@ jobs:
mkdir -p ${{ env.CODE_WARDEN_PATH }}
unzip -q cw.zip -d ${{ env.CODE_WARDEN_PATH }}
- - name: Lint — enforce file length limits
- run: node ${{ env.CODE_WARDEN_PATH }}/tools/warden-lint.js .
+ - name: Governance report
+ run: node ${{ env.CODE_WARDEN_PATH }}/tools/governance-report.js .
+
+ - name: Publish governance summary
+ if: always()
+ run: node ${{ env.CODE_WARDEN_PATH }}/tools/governance-report.js . --format=md >> $GITHUB_STEP_SUMMARY
- - name: Secrets — zero-trust scan
- run: node ${{ env.CODE_WARDEN_PATH }}/tools/verify-secrets.js .
+ - name: Upload governance artifact
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: code-warden-report
+ path: .code-warden-report.json
+ if-no-files-found: ignore
+ retention-days: 90
diff --git a/code-warden/tools/governance-report.js b/code-warden/tools/governance-report.js
new file mode 100644
index 0000000..ae85f45
--- /dev/null
+++ b/code-warden/tools/governance-report.js
@@ -0,0 +1,302 @@
+#!/usr/bin/env node
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const os = require('os');
+const { spawnSync } = require('child_process');
+const { countLines } = require('./lib/line-count');
+const { collectFiles } = require('./lib/file-collection');
+const { scanForSecrets } = require('./lib/secret-patterns');
+const { loadConfig } = require('./lib/config');
+
+const ROOT = path.join(__dirname, '..');
+const PKG = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
+const VERSION = PKG.version;
+
+// ---------------------------------------------------------------------------
+// CLI
+// ---------------------------------------------------------------------------
+
+function parseArgs(argv) {
+ const args = argv.slice(2);
+ const formatArg = args.find(a => a.startsWith('--format='));
+ const format = formatArg ? formatArg.split('=')[1] : null;
+ const scanPath = args.find(a => !a.startsWith('--')) || '.';
+ return { format, scanPath };
+}
+
+// ---------------------------------------------------------------------------
+// Git metadata
+// ---------------------------------------------------------------------------
+
+function gitInfo() {
+ const run = (gitArgs) => {
+ const r = spawnSync('git', gitArgs, { encoding: 'utf8', timeout: 5000 });
+ return r.status === 0 ? r.stdout.trim() : null;
+ };
+ return {
+ branch: run(['rev-parse', '--abbrev-ref', 'HEAD']),
+ commit: run(['rev-parse', '--short', 'HEAD']),
+ };
+}
+
+// ---------------------------------------------------------------------------
+// File length + secrets (single pass over all files)
+// ---------------------------------------------------------------------------
+
+function runScans(scanPath) {
+ const { maxFileLength } = loadConfig();
+ const resolved = path.resolve(scanPath);
+
+ if (!fs.existsSync(resolved)) {
+ console.error(`[CodeWarden] Error: scan path not found: ${scanPath}`);
+ process.exit(1);
+ }
+
+ const files = [];
+ if (fs.statSync(resolved).isDirectory()) {
+ collectFiles(resolved, files);
+ } else {
+ files.push(resolved);
+ }
+
+ const lengthViolations = [];
+ const secretViolations = [];
+
+ for (const f of files) {
+ let content;
+ try { content = fs.readFileSync(f, 'utf8'); } catch { continue; }
+
+ const rel = path.relative(resolved, f);
+
+ const lineCount = countLines(content);
+ if (lineCount > maxFileLength) {
+ lengthViolations.push({ file: rel, lines: lineCount, limit: maxFileLength });
+ }
+
+ const hit = scanForSecrets(content);
+ if (hit) {
+ secretViolations.push({ file: rel, pattern: hit.label });
+ }
+ }
+
+ return {
+ fileLength: {
+ status: lengthViolations.length === 0 ? 'pass' : 'fail',
+ filesScanned: files.length,
+ violations: lengthViolations.length,
+ details: lengthViolations.length > 0 ? lengthViolations : undefined,
+ },
+ secrets: {
+ status: secretViolations.length === 0 ? 'pass' : 'fail',
+ filesScanned: files.length,
+ violations: secretViolations.length,
+ details: secretViolations.length > 0 ? secretViolations : undefined,
+ },
+ };
+}
+
+// ---------------------------------------------------------------------------
+// Behavioral tests
+// ---------------------------------------------------------------------------
+
+function checkTests() {
+ const testScript = path.join(__dirname, 'tests', 'run-tests.js');
+ if (!fs.existsSync(testScript)) {
+ return { status: 'skip', tests: 0, failures: 0 };
+ }
+
+ const r = spawnSync(process.execPath, [testScript], {
+ encoding: 'utf8',
+ timeout: 30000,
+ cwd: ROOT,
+ });
+
+ const out = (r.stdout || '') + (r.stderr || '');
+ const passMatch = out.match(/pass\s+(\d+)/);
+ const failMatch = out.match(/fail\s+(\d+)/);
+
+ let passed, failed;
+ if (passMatch || failMatch) {
+ passed = parseInt(passMatch?.[1] || '0', 10);
+ failed = parseInt(failMatch?.[1] || '0', 10);
+ } else {
+ passed = (out.match(/^(?:ok \d+|✔)/gm) || []).length;
+ failed = (out.match(/^(?:not ok \d+|✖)/gm) || []).length;
+ }
+
+ return {
+ status: r.status === 0 ? 'pass' : 'fail',
+ tests: passed + failed,
+ failures: failed,
+ };
+}
+
+// ---------------------------------------------------------------------------
+// Source integrity
+// ---------------------------------------------------------------------------
+
+function checkInstallHealth() {
+ const required = [
+ 'SKILL.md',
+ 'references',
+ 'tools/warden-lint.js',
+ 'tools/verify-secrets.js',
+ 'tools/get-context.js',
+ ];
+ const missing = required.filter(f => !fs.existsSync(path.join(ROOT, f)));
+ return {
+ status: missing.length === 0 ? 'pass' : 'fail',
+ missing: missing.length > 0 ? missing : undefined,
+ };
+}
+
+// ---------------------------------------------------------------------------
+// Runtime hook detection
+// ---------------------------------------------------------------------------
+
+function checkRuntimeHooks() {
+ const home = os.homedir();
+ const result = {};
+
+ const claudeSettings = path.join(home, '.claude', 'settings.json');
+ if (fs.existsSync(claudeSettings)) {
+ try {
+ const s = JSON.parse(fs.readFileSync(claudeSettings, 'utf8'));
+ const hooks = (s?.hooks?.PreToolUse || [])
+ .flatMap(m => m.hooks || [])
+ .filter(h => String(h.description || '').startsWith('code-warden:'));
+ if (hooks.length > 0) {
+ const valid = hooks.every(h => h.args?.[0] && fs.existsSync(h.args[0]));
+ result.claude = valid ? 'registered' : 'registered_broken';
+ } else {
+ result.claude = 'not_registered';
+ }
+ } catch { result.claude = 'error'; }
+ } else {
+ result.claude = 'not_configured';
+ }
+
+ const codexHooksPath = path.join(home, '.codex', 'hooks.json');
+ if (fs.existsSync(codexHooksPath)) {
+ try {
+ const h = JSON.parse(fs.readFileSync(codexHooksPath, 'utf8'));
+ const cw = (h?.PreToolUse || [])
+ .filter(e => String(e.description || '').startsWith('code-warden:'));
+ if (cw.length > 0) {
+ const valid = cw.every(e => e.args?.[0] && fs.existsSync(e.args[0]));
+ result.codex = valid ? 'registered' : 'registered_broken';
+ } else {
+ result.codex = 'not_registered';
+ }
+ } catch { result.codex = 'error'; }
+ } else {
+ result.codex = 'not_configured';
+ }
+
+ return result;
+}
+
+// ---------------------------------------------------------------------------
+// Report assembly
+// ---------------------------------------------------------------------------
+
+function generateReport(scanPath) {
+ const repo = gitInfo();
+ const { fileLength, secrets } = runScans(scanPath);
+ const behavioralTests = checkTests();
+ const installHealth = checkInstallHealth();
+ const runtimeHooks = checkRuntimeHooks();
+
+ const checks = { fileLength, secrets, behavioralTests, installHealth };
+ const result = Object.values(checks).every(c => c.status === 'pass' || c.status === 'skip')
+ ? 'pass' : 'fail';
+
+ return {
+ tool: 'code-warden',
+ version: VERSION,
+ timestamp: new Date().toISOString(),
+ repository: { branch: repo.branch, commit: repo.commit },
+ checks,
+ governance: {
+ scopeGate: 'session_only',
+ planGate: 'session_only',
+ runtimeHooks,
+ },
+ result,
+ };
+}
+
+// ---------------------------------------------------------------------------
+// Markdown formatter
+// ---------------------------------------------------------------------------
+
+function formatMarkdown(report) {
+ const badge = s => s === 'pass' ? 'PASS' : s === 'skip' ? 'SKIP' : 'FAIL';
+ const hookLabel = (id) => {
+ const s = report.governance.runtimeHooks[id];
+ if (s === 'registered') return 'verified';
+ if (s === 'registered_broken') return 'broken';
+ if (s === 'not_registered') return 'none';
+ return 'n/a';
+ };
+
+ const healthDetail = report.checks.installHealth.missing
+ ? 'Missing: ' + report.checks.installHealth.missing.join(', ')
+ : 'All source files present';
+
+ const lines = [
+ '## Code-Warden Governance Report',
+ '',
+ '| Check | Result | Details |',
+ '|-------|--------|---------|',
+ `| File length | ${badge(report.checks.fileLength.status)} | ${report.checks.fileLength.filesScanned} files scanned, ${report.checks.fileLength.violations} violations |`,
+ `| Hardcoded credentials | ${badge(report.checks.secrets.status)} | ${report.checks.secrets.filesScanned} files scanned, ${report.checks.secrets.violations} violations |`,
+ `| Behavioral tests | ${badge(report.checks.behavioralTests.status)} | ${report.checks.behavioralTests.tests} tests, ${report.checks.behavioralTests.failures} failures |`,
+ `| Install health | ${badge(report.checks.installHealth.status)} | ${healthDetail} |`,
+ `| Runtime hooks | — | Claude: ${hookLabel('claude')} / Codex: ${hookLabel('codex')} |`,
+ '',
+ `**Result:** ${report.result === 'pass' ? 'All governed checks passed.' : 'One or more checks failed.'}`,
+ '',
+ `> Generated by Code-Warden v${report.version} at ${report.timestamp}`,
+ ];
+
+ return lines.join('\n');
+}
+
+// ---------------------------------------------------------------------------
+// One-line summary (default mode stdout)
+// ---------------------------------------------------------------------------
+
+function formatSummary(report) {
+ const c = report.checks;
+ const parts = [
+ `lint:${c.fileLength.status}`,
+ `secrets:${c.secrets.status}`,
+ `tests:${c.behavioralTests.status}`,
+ `health:${c.installHealth.status}`,
+ ];
+ return `[CodeWarden] Governance report: ${report.result.toUpperCase()} (${parts.join(', ')})`;
+}
+
+// ---------------------------------------------------------------------------
+// Main
+// ---------------------------------------------------------------------------
+
+const { format, scanPath } = parseArgs(process.argv);
+const report = generateReport(scanPath);
+
+if (format === 'md') {
+ console.log(formatMarkdown(report));
+} else if (format === 'json') {
+ console.log(JSON.stringify(report, null, 2));
+} else {
+ const json = JSON.stringify(report, null, 2);
+ const outPath = path.resolve('.code-warden-report.json');
+ fs.writeFileSync(outPath, json, 'utf8');
+ console.log(formatSummary(report));
+ console.log(`[CodeWarden] Report written to ${outPath}`);
+}
+
+process.exit(report.result === 'pass' ? 0 : 1);