code-warden 3.3.0 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONFIGURE.md +39 -39
- package/DECISIONS.md +107 -107
- package/README.md +6 -0
- package/SKILL.md +169 -169
- package/bin/code-warden.js +82 -82
- package/codewarden.json +14 -14
- package/examples/governed-session.md +132 -132
- package/install.js +399 -399
- package/install.ps1 +32 -32
- package/install.sh +33 -33
- package/package.json +62 -62
- package/references/anti-drift.md +55 -55
- package/references/architecture.md +26 -26
- package/references/cleanup.md +30 -30
- package/references/cognition.md +36 -36
- package/references/operations.md +45 -45
- package/references/planning-gates.md +83 -83
- package/references/research-and-fit.md +51 -51
- package/references/safety.md +31 -31
- package/tools/auto-detect.js +91 -91
- package/tools/auto-targets.js +104 -104
- package/tools/auto-windsurf-adapter.js +75 -75
- package/tools/get-context.js +50 -50
- package/tools/governance-report.js +302 -302
- package/tools/hooks/claude/install-hooks.js +112 -112
- package/tools/hooks/claude/uninstall-hooks.js +75 -75
- package/tools/hooks/claude/warden-lint-hook.js +106 -106
- package/tools/hooks/claude/warden-secrets-hook.js +73 -73
- package/tools/hooks/codex/install-hooks.js +100 -100
- package/tools/hooks/codex/uninstall-hooks.js +53 -53
- package/tools/hooks/codex/warden-apply-patch-hook.js +113 -113
- package/tools/hooks/codex/warden-bash-hook.js +51 -51
- package/tools/lib/config.js +49 -49
- package/tools/lib/file-collection.js +5 -2
- package/tools/lib/line-count.js +28 -28
- package/tools/lib/secret-patterns.js +57 -57
- package/tools/tests/fixtures/clean.js +9 -9
- package/tools/tests/run-tests.js +210 -210
- package/tools/verify-secrets.js +26 -26
- package/tools/warden-lint.js +27 -27
|
@@ -1,302 +1,302 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const os = require('os');
|
|
7
|
-
const { spawnSync } = require('child_process');
|
|
8
|
-
const { countLines } = require('./lib/line-count');
|
|
9
|
-
const { collectFiles } = require('./lib/file-collection');
|
|
10
|
-
const { scanForSecrets } = require('./lib/secret-patterns');
|
|
11
|
-
const { loadConfig } = require('./lib/config');
|
|
12
|
-
|
|
13
|
-
const ROOT = path.join(__dirname, '..');
|
|
14
|
-
const PKG = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
|
|
15
|
-
const VERSION = PKG.version;
|
|
16
|
-
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
// CLI
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
function parseArgs(argv) {
|
|
22
|
-
const args = argv.slice(2);
|
|
23
|
-
const formatArg = args.find(a => a.startsWith('--format='));
|
|
24
|
-
const format = formatArg ? formatArg.split('=')[1] : null;
|
|
25
|
-
const scanPath = args.find(a => !a.startsWith('--')) || '.';
|
|
26
|
-
return { format, scanPath };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
// Git metadata
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
|
|
33
|
-
function gitInfo() {
|
|
34
|
-
const run = (gitArgs) => {
|
|
35
|
-
const r = spawnSync('git', gitArgs, { encoding: 'utf8', timeout: 5000 });
|
|
36
|
-
return r.status === 0 ? r.stdout.trim() : null;
|
|
37
|
-
};
|
|
38
|
-
return {
|
|
39
|
-
branch: run(['rev-parse', '--abbrev-ref', 'HEAD']),
|
|
40
|
-
commit: run(['rev-parse', '--short', 'HEAD']),
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// ---------------------------------------------------------------------------
|
|
45
|
-
// File length + secrets (single pass over all files)
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
|
|
48
|
-
function runScans(scanPath) {
|
|
49
|
-
const { maxFileLength } = loadConfig();
|
|
50
|
-
const resolved = path.resolve(scanPath);
|
|
51
|
-
|
|
52
|
-
if (!fs.existsSync(resolved)) {
|
|
53
|
-
console.error(`[CodeWarden] Error: scan path not found: ${scanPath}`);
|
|
54
|
-
process.exit(1);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const files = [];
|
|
58
|
-
if (fs.statSync(resolved).isDirectory()) {
|
|
59
|
-
collectFiles(resolved, files);
|
|
60
|
-
} else {
|
|
61
|
-
files.push(resolved);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const lengthViolations = [];
|
|
65
|
-
const secretViolations = [];
|
|
66
|
-
|
|
67
|
-
for (const f of files) {
|
|
68
|
-
let content;
|
|
69
|
-
try { content = fs.readFileSync(f, 'utf8'); } catch { continue; }
|
|
70
|
-
|
|
71
|
-
const rel = path.relative(resolved, f);
|
|
72
|
-
|
|
73
|
-
const lineCount = countLines(content);
|
|
74
|
-
if (lineCount > maxFileLength) {
|
|
75
|
-
lengthViolations.push({ file: rel, lines: lineCount, limit: maxFileLength });
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const hit = scanForSecrets(content);
|
|
79
|
-
if (hit) {
|
|
80
|
-
secretViolations.push({ file: rel, pattern: hit.label });
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
fileLength: {
|
|
86
|
-
status: lengthViolations.length === 0 ? 'pass' : 'fail',
|
|
87
|
-
filesScanned: files.length,
|
|
88
|
-
violations: lengthViolations.length,
|
|
89
|
-
details: lengthViolations.length > 0 ? lengthViolations : undefined,
|
|
90
|
-
},
|
|
91
|
-
secrets: {
|
|
92
|
-
status: secretViolations.length === 0 ? 'pass' : 'fail',
|
|
93
|
-
filesScanned: files.length,
|
|
94
|
-
violations: secretViolations.length,
|
|
95
|
-
details: secretViolations.length > 0 ? secretViolations : undefined,
|
|
96
|
-
},
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// ---------------------------------------------------------------------------
|
|
101
|
-
// Behavioral tests
|
|
102
|
-
// ---------------------------------------------------------------------------
|
|
103
|
-
|
|
104
|
-
function checkTests() {
|
|
105
|
-
const testScript = path.join(__dirname, 'tests', 'run-tests.js');
|
|
106
|
-
if (!fs.existsSync(testScript)) {
|
|
107
|
-
return { status: 'skip', tests: 0, failures: 0 };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const r = spawnSync(process.execPath, [testScript], {
|
|
111
|
-
encoding: 'utf8',
|
|
112
|
-
timeout: 30000,
|
|
113
|
-
cwd: ROOT,
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
const out = (r.stdout || '') + (r.stderr || '');
|
|
117
|
-
const passMatch = out.match(/pass\s+(\d+)/);
|
|
118
|
-
const failMatch = out.match(/fail\s+(\d+)/);
|
|
119
|
-
|
|
120
|
-
let passed, failed;
|
|
121
|
-
if (passMatch || failMatch) {
|
|
122
|
-
passed = parseInt(passMatch?.[1] || '0', 10);
|
|
123
|
-
failed = parseInt(failMatch?.[1] || '0', 10);
|
|
124
|
-
} else {
|
|
125
|
-
passed = (out.match(/^(?:ok \d+|✔)/gm) || []).length;
|
|
126
|
-
failed = (out.match(/^(?:not ok \d+|✖)/gm) || []).length;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return {
|
|
130
|
-
status: r.status === 0 ? 'pass' : 'fail',
|
|
131
|
-
tests: passed + failed,
|
|
132
|
-
failures: failed,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// ---------------------------------------------------------------------------
|
|
137
|
-
// Source integrity
|
|
138
|
-
// ---------------------------------------------------------------------------
|
|
139
|
-
|
|
140
|
-
function checkInstallHealth() {
|
|
141
|
-
const required = [
|
|
142
|
-
'SKILL.md',
|
|
143
|
-
'references',
|
|
144
|
-
'tools/warden-lint.js',
|
|
145
|
-
'tools/verify-secrets.js',
|
|
146
|
-
'tools/get-context.js',
|
|
147
|
-
];
|
|
148
|
-
const missing = required.filter(f => !fs.existsSync(path.join(ROOT, f)));
|
|
149
|
-
return {
|
|
150
|
-
status: missing.length === 0 ? 'pass' : 'fail',
|
|
151
|
-
missing: missing.length > 0 ? missing : undefined,
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// ---------------------------------------------------------------------------
|
|
156
|
-
// Runtime hook detection
|
|
157
|
-
// ---------------------------------------------------------------------------
|
|
158
|
-
|
|
159
|
-
function checkRuntimeHooks() {
|
|
160
|
-
const home = os.homedir();
|
|
161
|
-
const result = {};
|
|
162
|
-
|
|
163
|
-
const claudeSettings = path.join(home, '.claude', 'settings.json');
|
|
164
|
-
if (fs.existsSync(claudeSettings)) {
|
|
165
|
-
try {
|
|
166
|
-
const s = JSON.parse(fs.readFileSync(claudeSettings, 'utf8'));
|
|
167
|
-
const hooks = (s?.hooks?.PreToolUse || [])
|
|
168
|
-
.flatMap(m => m.hooks || [])
|
|
169
|
-
.filter(h => String(h.description || '').startsWith('code-warden:'));
|
|
170
|
-
if (hooks.length > 0) {
|
|
171
|
-
const valid = hooks.every(h => h.args?.[0] && fs.existsSync(h.args[0]));
|
|
172
|
-
result.claude = valid ? 'registered' : 'registered_broken';
|
|
173
|
-
} else {
|
|
174
|
-
result.claude = 'not_registered';
|
|
175
|
-
}
|
|
176
|
-
} catch { result.claude = 'error'; }
|
|
177
|
-
} else {
|
|
178
|
-
result.claude = 'not_configured';
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const codexHooksPath = path.join(home, '.codex', 'hooks.json');
|
|
182
|
-
if (fs.existsSync(codexHooksPath)) {
|
|
183
|
-
try {
|
|
184
|
-
const h = JSON.parse(fs.readFileSync(codexHooksPath, 'utf8'));
|
|
185
|
-
const cw = (h?.PreToolUse || [])
|
|
186
|
-
.filter(e => String(e.description || '').startsWith('code-warden:'));
|
|
187
|
-
if (cw.length > 0) {
|
|
188
|
-
const valid = cw.every(e => e.args?.[0] && fs.existsSync(e.args[0]));
|
|
189
|
-
result.codex = valid ? 'registered' : 'registered_broken';
|
|
190
|
-
} else {
|
|
191
|
-
result.codex = 'not_registered';
|
|
192
|
-
}
|
|
193
|
-
} catch { result.codex = 'error'; }
|
|
194
|
-
} else {
|
|
195
|
-
result.codex = 'not_configured';
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return result;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// ---------------------------------------------------------------------------
|
|
202
|
-
// Report assembly
|
|
203
|
-
// ---------------------------------------------------------------------------
|
|
204
|
-
|
|
205
|
-
function generateReport(scanPath) {
|
|
206
|
-
const repo = gitInfo();
|
|
207
|
-
const { fileLength, secrets } = runScans(scanPath);
|
|
208
|
-
const behavioralTests = checkTests();
|
|
209
|
-
const installHealth = checkInstallHealth();
|
|
210
|
-
const runtimeHooks = checkRuntimeHooks();
|
|
211
|
-
|
|
212
|
-
const checks = { fileLength, secrets, behavioralTests, installHealth };
|
|
213
|
-
const result = Object.values(checks).every(c => c.status === 'pass' || c.status === 'skip')
|
|
214
|
-
? 'pass' : 'fail';
|
|
215
|
-
|
|
216
|
-
return {
|
|
217
|
-
tool: 'code-warden',
|
|
218
|
-
version: VERSION,
|
|
219
|
-
timestamp: new Date().toISOString(),
|
|
220
|
-
repository: { branch: repo.branch, commit: repo.commit },
|
|
221
|
-
checks,
|
|
222
|
-
governance: {
|
|
223
|
-
scopeGate: 'session_only',
|
|
224
|
-
planGate: 'session_only',
|
|
225
|
-
runtimeHooks,
|
|
226
|
-
},
|
|
227
|
-
result,
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// ---------------------------------------------------------------------------
|
|
232
|
-
// Markdown formatter
|
|
233
|
-
// ---------------------------------------------------------------------------
|
|
234
|
-
|
|
235
|
-
function formatMarkdown(report) {
|
|
236
|
-
const badge = s => s === 'pass' ? 'PASS' : s === 'skip' ? 'SKIP' : 'FAIL';
|
|
237
|
-
const hookLabel = (id) => {
|
|
238
|
-
const s = report.governance.runtimeHooks[id];
|
|
239
|
-
if (s === 'registered') return 'verified';
|
|
240
|
-
if (s === 'registered_broken') return 'broken';
|
|
241
|
-
if (s === 'not_registered') return 'none';
|
|
242
|
-
return 'n/a';
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
const healthDetail = report.checks.installHealth.missing
|
|
246
|
-
? 'Missing: ' + report.checks.installHealth.missing.join(', ')
|
|
247
|
-
: 'All source files present';
|
|
248
|
-
|
|
249
|
-
const lines = [
|
|
250
|
-
'## Code-Warden Governance Report',
|
|
251
|
-
'',
|
|
252
|
-
'| Check | Result | Details |',
|
|
253
|
-
'|-------|--------|---------|',
|
|
254
|
-
`| File length | ${badge(report.checks.fileLength.status)} | ${report.checks.fileLength.filesScanned} files scanned, ${report.checks.fileLength.violations} violations |`,
|
|
255
|
-
`| Hardcoded credentials | ${badge(report.checks.secrets.status)} | ${report.checks.secrets.filesScanned} files scanned, ${report.checks.secrets.violations} violations |`,
|
|
256
|
-
`| Behavioral tests | ${badge(report.checks.behavioralTests.status)} | ${report.checks.behavioralTests.tests} tests, ${report.checks.behavioralTests.failures} failures |`,
|
|
257
|
-
`| Install health | ${badge(report.checks.installHealth.status)} | ${healthDetail} |`,
|
|
258
|
-
`| Runtime hooks | — | Claude: ${hookLabel('claude')} / Codex: ${hookLabel('codex')} |`,
|
|
259
|
-
'',
|
|
260
|
-
`**Result:** ${report.result === 'pass' ? 'All governed checks passed.' : 'One or more checks failed.'}`,
|
|
261
|
-
'',
|
|
262
|
-
`> Generated by Code-Warden v${report.version} at ${report.timestamp}`,
|
|
263
|
-
];
|
|
264
|
-
|
|
265
|
-
return lines.join('\n');
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// ---------------------------------------------------------------------------
|
|
269
|
-
// One-line summary (default mode stdout)
|
|
270
|
-
// ---------------------------------------------------------------------------
|
|
271
|
-
|
|
272
|
-
function formatSummary(report) {
|
|
273
|
-
const c = report.checks;
|
|
274
|
-
const parts = [
|
|
275
|
-
`lint:${c.fileLength.status}`,
|
|
276
|
-
`secrets:${c.secrets.status}`,
|
|
277
|
-
`tests:${c.behavioralTests.status}`,
|
|
278
|
-
`health:${c.installHealth.status}`,
|
|
279
|
-
];
|
|
280
|
-
return `[CodeWarden] Governance report: ${report.result.toUpperCase()} (${parts.join(', ')})`;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// ---------------------------------------------------------------------------
|
|
284
|
-
// Main
|
|
285
|
-
// ---------------------------------------------------------------------------
|
|
286
|
-
|
|
287
|
-
const { format, scanPath } = parseArgs(process.argv);
|
|
288
|
-
const report = generateReport(scanPath);
|
|
289
|
-
|
|
290
|
-
if (format === 'md') {
|
|
291
|
-
console.log(formatMarkdown(report));
|
|
292
|
-
} else if (format === 'json') {
|
|
293
|
-
console.log(JSON.stringify(report, null, 2));
|
|
294
|
-
} else {
|
|
295
|
-
const json = JSON.stringify(report, null, 2);
|
|
296
|
-
const outPath = path.resolve('.code-warden-report.json');
|
|
297
|
-
fs.writeFileSync(outPath, json, 'utf8');
|
|
298
|
-
console.log(formatSummary(report));
|
|
299
|
-
console.log(`[CodeWarden] Report written to ${outPath}`);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
process.exit(report.result === 'pass' ? 0 : 1);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const { spawnSync } = require('child_process');
|
|
8
|
+
const { countLines } = require('./lib/line-count');
|
|
9
|
+
const { collectFiles } = require('./lib/file-collection');
|
|
10
|
+
const { scanForSecrets } = require('./lib/secret-patterns');
|
|
11
|
+
const { loadConfig } = require('./lib/config');
|
|
12
|
+
|
|
13
|
+
const ROOT = path.join(__dirname, '..');
|
|
14
|
+
const PKG = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
|
|
15
|
+
const VERSION = PKG.version;
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// CLI
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
function parseArgs(argv) {
|
|
22
|
+
const args = argv.slice(2);
|
|
23
|
+
const formatArg = args.find(a => a.startsWith('--format='));
|
|
24
|
+
const format = formatArg ? formatArg.split('=')[1] : null;
|
|
25
|
+
const scanPath = args.find(a => !a.startsWith('--')) || '.';
|
|
26
|
+
return { format, scanPath };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Git metadata
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
function gitInfo() {
|
|
34
|
+
const run = (gitArgs) => {
|
|
35
|
+
const r = spawnSync('git', gitArgs, { encoding: 'utf8', timeout: 5000 });
|
|
36
|
+
return r.status === 0 ? r.stdout.trim() : null;
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
branch: run(['rev-parse', '--abbrev-ref', 'HEAD']),
|
|
40
|
+
commit: run(['rev-parse', '--short', 'HEAD']),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// File length + secrets (single pass over all files)
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
function runScans(scanPath) {
|
|
49
|
+
const { maxFileLength } = loadConfig();
|
|
50
|
+
const resolved = path.resolve(scanPath);
|
|
51
|
+
|
|
52
|
+
if (!fs.existsSync(resolved)) {
|
|
53
|
+
console.error(`[CodeWarden] Error: scan path not found: ${scanPath}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const files = [];
|
|
58
|
+
if (fs.statSync(resolved).isDirectory()) {
|
|
59
|
+
collectFiles(resolved, files);
|
|
60
|
+
} else {
|
|
61
|
+
files.push(resolved);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const lengthViolations = [];
|
|
65
|
+
const secretViolations = [];
|
|
66
|
+
|
|
67
|
+
for (const f of files) {
|
|
68
|
+
let content;
|
|
69
|
+
try { content = fs.readFileSync(f, 'utf8'); } catch { continue; }
|
|
70
|
+
|
|
71
|
+
const rel = path.relative(resolved, f);
|
|
72
|
+
|
|
73
|
+
const lineCount = countLines(content);
|
|
74
|
+
if (lineCount > maxFileLength) {
|
|
75
|
+
lengthViolations.push({ file: rel, lines: lineCount, limit: maxFileLength });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const hit = scanForSecrets(content);
|
|
79
|
+
if (hit) {
|
|
80
|
+
secretViolations.push({ file: rel, pattern: hit.label });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
fileLength: {
|
|
86
|
+
status: lengthViolations.length === 0 ? 'pass' : 'fail',
|
|
87
|
+
filesScanned: files.length,
|
|
88
|
+
violations: lengthViolations.length,
|
|
89
|
+
details: lengthViolations.length > 0 ? lengthViolations : undefined,
|
|
90
|
+
},
|
|
91
|
+
secrets: {
|
|
92
|
+
status: secretViolations.length === 0 ? 'pass' : 'fail',
|
|
93
|
+
filesScanned: files.length,
|
|
94
|
+
violations: secretViolations.length,
|
|
95
|
+
details: secretViolations.length > 0 ? secretViolations : undefined,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Behavioral tests
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
function checkTests() {
|
|
105
|
+
const testScript = path.join(__dirname, 'tests', 'run-tests.js');
|
|
106
|
+
if (!fs.existsSync(testScript)) {
|
|
107
|
+
return { status: 'skip', tests: 0, failures: 0 };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const r = spawnSync(process.execPath, [testScript], {
|
|
111
|
+
encoding: 'utf8',
|
|
112
|
+
timeout: 30000,
|
|
113
|
+
cwd: ROOT,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const out = (r.stdout || '') + (r.stderr || '');
|
|
117
|
+
const passMatch = out.match(/pass\s+(\d+)/);
|
|
118
|
+
const failMatch = out.match(/fail\s+(\d+)/);
|
|
119
|
+
|
|
120
|
+
let passed, failed;
|
|
121
|
+
if (passMatch || failMatch) {
|
|
122
|
+
passed = parseInt(passMatch?.[1] || '0', 10);
|
|
123
|
+
failed = parseInt(failMatch?.[1] || '0', 10);
|
|
124
|
+
} else {
|
|
125
|
+
passed = (out.match(/^(?:ok \d+|✔)/gm) || []).length;
|
|
126
|
+
failed = (out.match(/^(?:not ok \d+|✖)/gm) || []).length;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
status: r.status === 0 ? 'pass' : 'fail',
|
|
131
|
+
tests: passed + failed,
|
|
132
|
+
failures: failed,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Source integrity
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
function checkInstallHealth() {
|
|
141
|
+
const required = [
|
|
142
|
+
'SKILL.md',
|
|
143
|
+
'references',
|
|
144
|
+
'tools/warden-lint.js',
|
|
145
|
+
'tools/verify-secrets.js',
|
|
146
|
+
'tools/get-context.js',
|
|
147
|
+
];
|
|
148
|
+
const missing = required.filter(f => !fs.existsSync(path.join(ROOT, f)));
|
|
149
|
+
return {
|
|
150
|
+
status: missing.length === 0 ? 'pass' : 'fail',
|
|
151
|
+
missing: missing.length > 0 ? missing : undefined,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Runtime hook detection
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
function checkRuntimeHooks() {
|
|
160
|
+
const home = os.homedir();
|
|
161
|
+
const result = {};
|
|
162
|
+
|
|
163
|
+
const claudeSettings = path.join(home, '.claude', 'settings.json');
|
|
164
|
+
if (fs.existsSync(claudeSettings)) {
|
|
165
|
+
try {
|
|
166
|
+
const s = JSON.parse(fs.readFileSync(claudeSettings, 'utf8'));
|
|
167
|
+
const hooks = (s?.hooks?.PreToolUse || [])
|
|
168
|
+
.flatMap(m => m.hooks || [])
|
|
169
|
+
.filter(h => String(h.description || '').startsWith('code-warden:'));
|
|
170
|
+
if (hooks.length > 0) {
|
|
171
|
+
const valid = hooks.every(h => h.args?.[0] && fs.existsSync(h.args[0]));
|
|
172
|
+
result.claude = valid ? 'registered' : 'registered_broken';
|
|
173
|
+
} else {
|
|
174
|
+
result.claude = 'not_registered';
|
|
175
|
+
}
|
|
176
|
+
} catch { result.claude = 'error'; }
|
|
177
|
+
} else {
|
|
178
|
+
result.claude = 'not_configured';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const codexHooksPath = path.join(home, '.codex', 'hooks.json');
|
|
182
|
+
if (fs.existsSync(codexHooksPath)) {
|
|
183
|
+
try {
|
|
184
|
+
const h = JSON.parse(fs.readFileSync(codexHooksPath, 'utf8'));
|
|
185
|
+
const cw = (h?.PreToolUse || [])
|
|
186
|
+
.filter(e => String(e.description || '').startsWith('code-warden:'));
|
|
187
|
+
if (cw.length > 0) {
|
|
188
|
+
const valid = cw.every(e => e.args?.[0] && fs.existsSync(e.args[0]));
|
|
189
|
+
result.codex = valid ? 'registered' : 'registered_broken';
|
|
190
|
+
} else {
|
|
191
|
+
result.codex = 'not_registered';
|
|
192
|
+
}
|
|
193
|
+
} catch { result.codex = 'error'; }
|
|
194
|
+
} else {
|
|
195
|
+
result.codex = 'not_configured';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// Report assembly
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
function generateReport(scanPath) {
|
|
206
|
+
const repo = gitInfo();
|
|
207
|
+
const { fileLength, secrets } = runScans(scanPath);
|
|
208
|
+
const behavioralTests = checkTests();
|
|
209
|
+
const installHealth = checkInstallHealth();
|
|
210
|
+
const runtimeHooks = checkRuntimeHooks();
|
|
211
|
+
|
|
212
|
+
const checks = { fileLength, secrets, behavioralTests, installHealth };
|
|
213
|
+
const result = Object.values(checks).every(c => c.status === 'pass' || c.status === 'skip')
|
|
214
|
+
? 'pass' : 'fail';
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
tool: 'code-warden',
|
|
218
|
+
version: VERSION,
|
|
219
|
+
timestamp: new Date().toISOString(),
|
|
220
|
+
repository: { branch: repo.branch, commit: repo.commit },
|
|
221
|
+
checks,
|
|
222
|
+
governance: {
|
|
223
|
+
scopeGate: 'session_only',
|
|
224
|
+
planGate: 'session_only',
|
|
225
|
+
runtimeHooks,
|
|
226
|
+
},
|
|
227
|
+
result,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// Markdown formatter
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
function formatMarkdown(report) {
|
|
236
|
+
const badge = s => s === 'pass' ? 'PASS' : s === 'skip' ? 'SKIP' : 'FAIL';
|
|
237
|
+
const hookLabel = (id) => {
|
|
238
|
+
const s = report.governance.runtimeHooks[id];
|
|
239
|
+
if (s === 'registered') return 'verified';
|
|
240
|
+
if (s === 'registered_broken') return 'broken';
|
|
241
|
+
if (s === 'not_registered') return 'none';
|
|
242
|
+
return 'n/a';
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const healthDetail = report.checks.installHealth.missing
|
|
246
|
+
? 'Missing: ' + report.checks.installHealth.missing.join(', ')
|
|
247
|
+
: 'All source files present';
|
|
248
|
+
|
|
249
|
+
const lines = [
|
|
250
|
+
'## Code-Warden Governance Report',
|
|
251
|
+
'',
|
|
252
|
+
'| Check | Result | Details |',
|
|
253
|
+
'|-------|--------|---------|',
|
|
254
|
+
`| File length | ${badge(report.checks.fileLength.status)} | ${report.checks.fileLength.filesScanned} files scanned, ${report.checks.fileLength.violations} violations |`,
|
|
255
|
+
`| Hardcoded credentials | ${badge(report.checks.secrets.status)} | ${report.checks.secrets.filesScanned} files scanned, ${report.checks.secrets.violations} violations |`,
|
|
256
|
+
`| Behavioral tests | ${badge(report.checks.behavioralTests.status)} | ${report.checks.behavioralTests.tests} tests, ${report.checks.behavioralTests.failures} failures |`,
|
|
257
|
+
`| Install health | ${badge(report.checks.installHealth.status)} | ${healthDetail} |`,
|
|
258
|
+
`| Runtime hooks | — | Claude: ${hookLabel('claude')} / Codex: ${hookLabel('codex')} |`,
|
|
259
|
+
'',
|
|
260
|
+
`**Result:** ${report.result === 'pass' ? 'All governed checks passed.' : 'One or more checks failed.'}`,
|
|
261
|
+
'',
|
|
262
|
+
`> Generated by Code-Warden v${report.version} at ${report.timestamp}`,
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
return lines.join('\n');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
// One-line summary (default mode stdout)
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
function formatSummary(report) {
|
|
273
|
+
const c = report.checks;
|
|
274
|
+
const parts = [
|
|
275
|
+
`lint:${c.fileLength.status}`,
|
|
276
|
+
`secrets:${c.secrets.status}`,
|
|
277
|
+
`tests:${c.behavioralTests.status}`,
|
|
278
|
+
`health:${c.installHealth.status}`,
|
|
279
|
+
];
|
|
280
|
+
return `[CodeWarden] Governance report: ${report.result.toUpperCase()} (${parts.join(', ')})`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
// Main
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
const { format, scanPath } = parseArgs(process.argv);
|
|
288
|
+
const report = generateReport(scanPath);
|
|
289
|
+
|
|
290
|
+
if (format === 'md') {
|
|
291
|
+
console.log(formatMarkdown(report));
|
|
292
|
+
} else if (format === 'json') {
|
|
293
|
+
console.log(JSON.stringify(report, null, 2));
|
|
294
|
+
} else {
|
|
295
|
+
const json = JSON.stringify(report, null, 2);
|
|
296
|
+
const outPath = path.resolve('.code-warden-report.json');
|
|
297
|
+
fs.writeFileSync(outPath, json, 'utf8');
|
|
298
|
+
console.log(formatSummary(report));
|
|
299
|
+
console.log(`[CodeWarden] Report written to ${outPath}`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
process.exit(report.result === 'pass' ? 0 : 1);
|