forgedev 1.2.0 → 1.3.0

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.
Files changed (171) hide show
  1. package/README.md +57 -10
  2. package/bin/chainproof.js +126 -0
  3. package/package.json +25 -7
  4. package/src/chainproof-bridge.js +330 -0
  5. package/src/ci-mode.js +85 -0
  6. package/src/claude-configurator.js +86 -49
  7. package/src/cli.js +30 -7
  8. package/src/composer.js +159 -34
  9. package/src/doctor-checks-chainproof.js +106 -0
  10. package/src/doctor-checks.js +39 -20
  11. package/src/doctor-prompts.js +9 -9
  12. package/src/doctor.js +37 -4
  13. package/src/guided.js +3 -3
  14. package/src/index.js +31 -10
  15. package/src/init-mode.js +64 -11
  16. package/src/menu.js +178 -0
  17. package/src/prompts.js +5 -12
  18. package/src/recommender.js +134 -10
  19. package/src/scanner.js +57 -2
  20. package/src/uat-generator.js +204 -189
  21. package/src/update-check.js +9 -4
  22. package/src/update.js +1 -1
  23. package/src/utils.js +64 -5
  24. package/templates/ai/guardrails-py/backend/app/ai/__init__.py +29 -0
  25. package/templates/ai/guardrails-py/backend/app/ai/audit_log.py +133 -0
  26. package/templates/ai/guardrails-py/backend/app/ai/client.py.template +323 -0
  27. package/templates/ai/guardrails-py/backend/app/ai/health.py.template +157 -0
  28. package/templates/ai/guardrails-py/backend/app/ai/input_guard.py +98 -0
  29. package/templates/ai/guardrails-ts/src/lib/ai/audit-log.ts.template +164 -0
  30. package/templates/ai/guardrails-ts/src/lib/ai/client.ts.template +403 -0
  31. package/templates/ai/guardrails-ts/src/lib/ai/health.ts.template +165 -0
  32. package/templates/ai/guardrails-ts/src/lib/ai/index.ts.template +17 -0
  33. package/templates/ai/guardrails-ts/src/lib/ai/input-guard.ts.template +124 -0
  34. package/templates/auth/nextauth/src/lib/auth.ts.template +12 -7
  35. package/templates/backend/express/Dockerfile.template +18 -0
  36. package/templates/backend/express/package.json.template +33 -0
  37. package/templates/backend/express/src/index.ts.template +34 -0
  38. package/templates/backend/express/src/routes/health.ts.template +27 -0
  39. package/templates/backend/express/tsconfig.json +17 -0
  40. package/templates/backend/fastapi/backend/Dockerfile.template +5 -0
  41. package/templates/backend/fastapi/backend/app/api/health.py.template +1 -1
  42. package/templates/backend/fastapi/backend/app/core/config.py.template +1 -1
  43. package/templates/backend/fastapi/backend/app/core/errors.py +1 -1
  44. package/templates/backend/fastapi/backend/app/main.py.template +3 -1
  45. package/templates/backend/fastapi/backend/requirements.txt.template +2 -0
  46. package/templates/backend/hono/Dockerfile.template +18 -0
  47. package/templates/backend/hono/package.json.template +31 -0
  48. package/templates/backend/hono/src/index.ts.template +32 -0
  49. package/templates/backend/hono/src/routes/health.ts.template +27 -0
  50. package/templates/backend/hono/tsconfig.json +18 -0
  51. package/templates/base/docs/uat/UAT_TEMPLATE.md.template +1 -1
  52. package/templates/chainproof/base/.chainproof/config.json.template +11 -0
  53. package/templates/chainproof/base/.chainproof/mcp-server.mjs +310 -0
  54. package/templates/chainproof/base/.mcp.json +9 -0
  55. package/templates/chainproof/fastapi/.chainproof/middleware.json.template +14 -0
  56. package/templates/chainproof/nextjs/.chainproof/hooks.json.template +19 -0
  57. package/templates/chainproof/polyglot/.chainproof/config.json.template +21 -0
  58. package/templates/claude-code/agents/architect.md +25 -11
  59. package/templates/claude-code/agents/build-error-resolver.md +19 -5
  60. package/templates/claude-code/agents/chief-of-staff.md +42 -8
  61. package/templates/claude-code/agents/code-quality-reviewer.md +14 -0
  62. package/templates/claude-code/agents/database-reviewer.md +15 -1
  63. package/templates/claude-code/agents/deep-reviewer.md +191 -0
  64. package/templates/claude-code/agents/doc-updater.md +19 -5
  65. package/templates/claude-code/agents/docs-lookup.md +19 -5
  66. package/templates/claude-code/agents/e2e-runner.md +26 -12
  67. package/templates/claude-code/agents/enforcement-gate.md +102 -0
  68. package/templates/claude-code/agents/frontend-builder.md +188 -0
  69. package/templates/claude-code/agents/harness-optimizer.md +36 -1
  70. package/templates/claude-code/agents/loop-operator.md +27 -13
  71. package/templates/claude-code/agents/planner.md +21 -7
  72. package/templates/claude-code/agents/product-strategist.md +24 -10
  73. package/templates/claude-code/agents/production-readiness.md +14 -0
  74. package/templates/claude-code/agents/prompt-auditor.md +115 -0
  75. package/templates/claude-code/agents/refactor-cleaner.md +22 -8
  76. package/templates/claude-code/agents/security-reviewer.md +14 -0
  77. package/templates/claude-code/agents/spec-validator.md +15 -1
  78. package/templates/claude-code/agents/tdd-guide.md +21 -7
  79. package/templates/claude-code/agents/uat-validator.md +14 -0
  80. package/templates/claude-code/claude-md/base.md +14 -7
  81. package/templates/claude-code/claude-md/fastapi.md +8 -8
  82. package/templates/claude-code/claude-md/fullstack.md +6 -6
  83. package/templates/claude-code/claude-md/hono.md +18 -0
  84. package/templates/claude-code/claude-md/nextjs.md +5 -5
  85. package/templates/claude-code/claude-md/remix.md +18 -0
  86. package/templates/claude-code/commands/audit-security.md +14 -0
  87. package/templates/claude-code/commands/audit-spec.md +14 -0
  88. package/templates/claude-code/commands/audit-wiring.md +14 -0
  89. package/templates/claude-code/commands/build-fix.md +28 -0
  90. package/templates/claude-code/commands/build-ui.md +59 -0
  91. package/templates/claude-code/commands/code-review.md +53 -31
  92. package/templates/claude-code/commands/fix-loop.md +211 -0
  93. package/templates/claude-code/commands/full-audit.md +36 -8
  94. package/templates/claude-code/commands/generate-prd.md +1 -1
  95. package/templates/claude-code/commands/generate-sdd.md +74 -0
  96. package/templates/claude-code/commands/generate-uat.md +107 -35
  97. package/templates/claude-code/commands/help.md +68 -0
  98. package/templates/claude-code/commands/live-uat.md +268 -0
  99. package/templates/claude-code/commands/optimize-claude-md.md +15 -1
  100. package/templates/claude-code/commands/plan.md +3 -3
  101. package/templates/claude-code/commands/pre-pr.md +57 -19
  102. package/templates/claude-code/commands/product-strategist.md +21 -0
  103. package/templates/claude-code/commands/resume-session.md +10 -10
  104. package/templates/claude-code/commands/run-uat.md +59 -2
  105. package/templates/claude-code/commands/save-session.md +10 -10
  106. package/templates/claude-code/commands/simplify.md +36 -0
  107. package/templates/claude-code/commands/tdd.md +17 -18
  108. package/templates/claude-code/commands/verify-all.md +24 -0
  109. package/templates/claude-code/commands/verify-intent.md +55 -0
  110. package/templates/claude-code/commands/workflows.md +52 -40
  111. package/templates/claude-code/hooks/polyglot.json +10 -1
  112. package/templates/claude-code/hooks/python.json +10 -1
  113. package/templates/claude-code/hooks/scripts/autofix-polyglot.mjs +2 -2
  114. package/templates/claude-code/hooks/scripts/autofix-python.mjs +1 -1
  115. package/templates/claude-code/hooks/scripts/autofix-typescript.mjs +1 -1
  116. package/templates/claude-code/hooks/scripts/code-hygiene.mjs +293 -0
  117. package/templates/claude-code/hooks/scripts/pre-commit-gate.mjs +207 -0
  118. package/templates/claude-code/hooks/typescript.json +10 -1
  119. package/templates/claude-code/skills/ai-prompts/SKILL.md +119 -41
  120. package/templates/claude-code/skills/git-workflow/SKILL.md +5 -5
  121. package/templates/claude-code/skills/nextjs/SKILL.md +1 -1
  122. package/templates/claude-code/skills/playwright/SKILL.md +5 -5
  123. package/templates/claude-code/skills/security-api/SKILL.md +1 -1
  124. package/templates/claude-code/skills/security-web/SKILL.md +1 -1
  125. package/templates/claude-code/skills/testing-patterns/SKILL.md +9 -9
  126. package/templates/database/prisma-postgres/{.env.example → .env.example.template} +1 -0
  127. package/templates/database/sqlalchemy-postgres/{.env.example → .env.example.template} +1 -0
  128. package/templates/docs-portal/fastapi/backend/app/portal/__pycache__/docs_reader.cpython-314.pyc +0 -0
  129. package/templates/docs-portal/fastapi/backend/app/portal/docs_reader.py +201 -0
  130. package/templates/docs-portal/fastapi/backend/app/portal/html_renderer.py +229 -0
  131. package/templates/docs-portal/fastapi/backend/app/portal/router.py.template +35 -0
  132. package/templates/docs-portal/nextjs/src/app/portal/[category]/[slug]/page.tsx +81 -0
  133. package/templates/docs-portal/nextjs/src/app/portal/[category]/page.tsx +65 -0
  134. package/templates/docs-portal/nextjs/src/app/portal/layout.tsx.template +54 -0
  135. package/templates/docs-portal/nextjs/src/app/portal/page.tsx +85 -0
  136. package/templates/docs-portal/nextjs/src/components/portal/markdown-renderer.tsx +101 -0
  137. package/templates/docs-portal/nextjs/src/components/portal/mobile-portal-nav.tsx +81 -0
  138. package/templates/docs-portal/nextjs/src/components/portal/portal-nav.tsx +86 -0
  139. package/templates/docs-portal/nextjs/src/lib/docs.ts +139 -0
  140. package/templates/frontend/nextjs/package.json.template +3 -1
  141. package/templates/frontend/react/index.html.template +12 -0
  142. package/templates/frontend/react/package.json.template +34 -0
  143. package/templates/frontend/react/src/App.tsx.template +10 -0
  144. package/templates/frontend/react/src/index.css +1 -0
  145. package/templates/frontend/react/src/main.tsx +10 -0
  146. package/templates/frontend/react/tsconfig.json +17 -0
  147. package/templates/frontend/react/vite.config.ts.template +15 -0
  148. package/templates/frontend/react/vitest.config.ts +9 -0
  149. package/templates/frontend/remix/app/root.tsx.template +31 -0
  150. package/templates/frontend/remix/app/routes/_index.tsx.template +19 -0
  151. package/templates/frontend/remix/app/routes/api.health.ts.template +10 -0
  152. package/templates/frontend/remix/app/tailwind.css +1 -0
  153. package/templates/frontend/remix/package.json.template +39 -0
  154. package/templates/frontend/remix/tsconfig.json +18 -0
  155. package/templates/frontend/remix/vite.config.ts.template +7 -0
  156. package/templates/infra/github-actions/.github/workflows/ci.yml.template +3 -0
  157. package/docs/00-README.md +0 -310
  158. package/docs/01-universal-prompt-library.md +0 -1049
  159. package/docs/02-claude-code-mastery-playbook.md +0 -283
  160. package/docs/03-multi-agent-verification.md +0 -565
  161. package/docs/04-errata-and-verification-checklist.md +0 -284
  162. package/docs/05-universal-scaffolder-vision.md +0 -452
  163. package/docs/06-confidence-assessment-and-repo-prompt.md +0 -407
  164. package/docs/errata.md +0 -58
  165. package/docs/multi-agent-verification.md +0 -66
  166. package/docs/playbook.md +0 -95
  167. package/docs/prompt-library.md +0 -160
  168. package/docs/uat/UAT_CHECKLIST.csv +0 -9
  169. package/docs/uat/UAT_TEMPLATE.md +0 -163
  170. package/templates/claude-code/commands/done.md +0 -19
  171. /package/{docs/plans/.gitkeep → templates/docs-portal/fastapi/backend/app/portal/__init__.py} +0 -0
@@ -0,0 +1,293 @@
1
+ #!/usr/bin/env node
2
+ // Code Hygiene Gate — runs on Stop to enforce structural quality
3
+ // Checks: file length, duplicate code blocks, repeated functions, stale test files
4
+ // Exit 0 = pass (with warnings), Exit 2 = blocked (critical issues found)
5
+
6
+ import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
7
+ import { join, relative, extname, basename } from 'node:path';
8
+
9
+ // ── Config ──────────────────────────────────────────────────────────────────
10
+ const MAX_FILE_LINES = 300;
11
+ const MAX_FUNCTION_LINES = 50;
12
+ const MIN_DUPLICATE_LINES = 6; // minimum consecutive matching lines to flag
13
+ const MAX_FILES_PER_DIR = 20; // warn if a single directory has too many files
14
+ const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.rs']);
15
+ const IGNORE_DIRS = new Set(['node_modules', '.next', '__pycache__', '.git', 'dist', 'build', '.claude', 'coverage', '.venv', 'venv']);
16
+ const IGNORE_FILES = new Set(['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock']);
17
+
18
+ // ── Helpers ─────────────────────────────────────────────────────────────────
19
+ function walk(dir, files = []) {
20
+ let entries;
21
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return files; }
22
+ for (const entry of entries) {
23
+ if (IGNORE_DIRS.has(entry.name)) continue;
24
+ const full = join(dir, entry.name);
25
+ if (entry.isDirectory()) {
26
+ walk(full, files);
27
+ } else if (SOURCE_EXTENSIONS.has(extname(entry.name)) && !IGNORE_FILES.has(entry.name)) {
28
+ files.push(full);
29
+ }
30
+ }
31
+ return files;
32
+ }
33
+
34
+ function readLines(filePath) {
35
+ try { return readFileSync(filePath, 'utf-8').split('\n'); } catch { return []; }
36
+ }
37
+
38
+ // ── Checks ──────────────────────────────────────────────────────────────────
39
+
40
+ function checkFileLengths(files, cwd) {
41
+ const warnings = [];
42
+ for (const file of files) {
43
+ const lines = readLines(file);
44
+ if (lines.length > MAX_FILE_LINES) {
45
+ warnings.push({
46
+ level: lines.length > MAX_FILE_LINES * 2 ? 'critical' : 'warning',
47
+ file: relative(cwd, file),
48
+ message: `${lines.length} lines (limit: ${MAX_FILE_LINES}). Split into smaller, focused modules.`,
49
+ });
50
+ }
51
+ }
52
+ return warnings;
53
+ }
54
+
55
+ function checkFunctionLengths(files, cwd) {
56
+ const warnings = [];
57
+ // Match common function declarations across JS/TS/Python
58
+ const fnPatterns = [
59
+ /^(?:export\s+)?(?:async\s+)?function\s+(\w+)/, // function foo()
60
+ /^(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:async\s*)?\(/, // const foo = (
61
+ /^(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[^=])\s*=>/, // const foo = () =>
62
+ /^\s+(?:async\s+)?(\w+)\s*\([^)]*\)\s*\{/, // method() {
63
+ /^(?:async\s+)?def\s+(\w+)/, // def foo (Python)
64
+ ];
65
+
66
+ for (const file of files) {
67
+ const fileExt = extname(file);
68
+ if (fileExt === '.py') continue;
69
+
70
+ const lines = readLines(file);
71
+ let currentFn = null;
72
+ let fnStart = 0;
73
+ let braceDepth = 0;
74
+ let inFunction = false;
75
+
76
+ for (let i = 0; i < lines.length; i++) {
77
+ const line = lines[i];
78
+
79
+ // Check if this line starts a new function
80
+ let newFnStarted = false;
81
+ for (const pattern of fnPatterns) {
82
+ const match = line.match(pattern);
83
+ if (match) {
84
+ // Close previous function if open
85
+ if (inFunction && currentFn) {
86
+ const len = i - fnStart;
87
+ if (len > MAX_FUNCTION_LINES) {
88
+ warnings.push({
89
+ level: 'warning',
90
+ file: relative(cwd, file),
91
+ message: `Function "${currentFn}" is ${len} lines (limit: ${MAX_FUNCTION_LINES}) at line ${fnStart + 1}. Extract helper functions.`,
92
+ });
93
+ }
94
+ }
95
+ currentFn = match[1];
96
+ fnStart = i;
97
+ inFunction = true;
98
+ braceDepth = 0;
99
+ newFnStarted = true;
100
+ break;
101
+ }
102
+ }
103
+
104
+ // Skip brace tracking on the line that started a new function
105
+ if (newFnStarted) continue;
106
+
107
+ // Track brace depth for JS/TS
108
+ if (inFunction) {
109
+ for (const ch of line) {
110
+ if (ch === '{') braceDepth++;
111
+ if (ch === '}') braceDepth--;
112
+ }
113
+ if (braceDepth <= 0 && i > fnStart && line.trim()) {
114
+ const len = i - fnStart + 1;
115
+ if (len > MAX_FUNCTION_LINES) {
116
+ warnings.push({
117
+ level: 'warning',
118
+ file: relative(cwd, file),
119
+ message: `Function "${currentFn}" is ${len} lines (limit: ${MAX_FUNCTION_LINES}) at line ${fnStart + 1}. Extract helper functions.`,
120
+ });
121
+ }
122
+ inFunction = false;
123
+ currentFn = null;
124
+ }
125
+ }
126
+ }
127
+ }
128
+ return warnings;
129
+ }
130
+
131
+ function checkDuplicateBlocks(files, cwd) {
132
+ const warnings = [];
133
+ // Build a map of normalized line sequences -> locations
134
+ const blockMap = new Map();
135
+
136
+ for (const file of files) {
137
+ const lines = readLines(file).map(l => l.trim()).filter(l => l && !l.startsWith('//') && !l.startsWith('#') && !l.startsWith('*') && !l.startsWith('import') && !l.startsWith('from'));
138
+
139
+ // Sliding window of MIN_DUPLICATE_LINES
140
+ for (let i = 0; i <= lines.length - MIN_DUPLICATE_LINES; i++) {
141
+ const block = lines.slice(i, i + MIN_DUPLICATE_LINES).join('\n');
142
+ // Skip trivial blocks (mostly braces, returns, empty patterns)
143
+ if (block.replace(/[{}\s();\n]/g, '').length < 30) continue;
144
+
145
+ if (!blockMap.has(block)) {
146
+ blockMap.set(block, []);
147
+ }
148
+ blockMap.get(block).push({ file: relative(cwd, file), line: i + 1 });
149
+ }
150
+ }
151
+
152
+ // Report blocks found in multiple files
153
+ const reported = new Set();
154
+ for (const [block, locations] of blockMap) {
155
+ const uniqueFiles = [...new Set(locations.map(l => l.file))];
156
+ if (uniqueFiles.length < 2) continue;
157
+
158
+ const key = uniqueFiles.sort().join('|');
159
+ if (reported.has(key)) continue;
160
+ reported.add(key);
161
+
162
+ const preview = block.split('\n')[0].substring(0, 60);
163
+ warnings.push({
164
+ level: 'warning',
165
+ file: uniqueFiles[0],
166
+ message: `Duplicate code block found in ${uniqueFiles.length} files: "${preview}..." Also in: ${uniqueFiles.slice(1).join(', ')}. Extract to a shared utility.`,
167
+ });
168
+
169
+ // Limit duplicate reports to avoid noise
170
+ if (warnings.length > 5) break;
171
+ }
172
+
173
+ return warnings;
174
+ }
175
+
176
+ function checkDirectoryBloat(cwd) {
177
+ const warnings = [];
178
+
179
+ function checkDir(dir) {
180
+ let entries;
181
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
182
+
183
+ const sourceFiles = entries.filter(e =>
184
+ !e.isDirectory() && SOURCE_EXTENSIONS.has(extname(e.name)) && !IGNORE_FILES.has(e.name)
185
+ );
186
+
187
+ if (sourceFiles.length > MAX_FILES_PER_DIR) {
188
+ warnings.push({
189
+ level: 'warning',
190
+ file: relative(cwd, dir) || '.',
191
+ message: `Directory has ${sourceFiles.length} source files (limit: ${MAX_FILES_PER_DIR}). Consider grouping into subdirectories by feature or domain.`,
192
+ });
193
+ }
194
+
195
+ for (const entry of entries) {
196
+ if (entry.isDirectory() && !IGNORE_DIRS.has(entry.name)) {
197
+ checkDir(join(dir, entry.name));
198
+ }
199
+ }
200
+ }
201
+
202
+ checkDir(cwd);
203
+ return warnings;
204
+ }
205
+
206
+ function checkStaleTests(files, cwd) {
207
+ const warnings = [];
208
+ const sourceFiles = new Set(
209
+ files
210
+ .filter(f => !basename(f).includes('.test.') && !basename(f).includes('.spec.') && !f.includes('__tests__'))
211
+ .map(f => basename(f).replace(extname(f), ''))
212
+ );
213
+
214
+ const testFiles = files.filter(f =>
215
+ basename(f).includes('.test.') || basename(f).includes('.spec.') || f.includes('__tests__')
216
+ );
217
+
218
+ for (const testFile of testFiles) {
219
+ const testBase = basename(testFile)
220
+ .replace('.test', '')
221
+ .replace('.spec', '')
222
+ .replace(extname(testFile), '');
223
+
224
+ // If the source file this test corresponds to doesn't exist, flag it
225
+ if (testBase && !sourceFiles.has(testBase)) {
226
+ const lines = readLines(testFile);
227
+ // Check if the test file imports something that doesn't exist
228
+ const imports = lines.filter(l => l.includes('import') && l.includes('from'));
229
+ let hasDeadImport = false;
230
+ for (const imp of imports) {
231
+ const match = imp.match(/from\s+['"]([^'"]+)['"]/);
232
+ if (match && match[1].startsWith('.')) {
233
+ // Relative import - check if the file exists
234
+ const importPath = join(testFile, '..', match[1]);
235
+ const extensions = ['.ts', '.tsx', '.js', '.jsx', '.py', ''];
236
+ const exists = extensions.some(ext => existsSync(importPath + ext) || existsSync(importPath));
237
+ if (!exists) hasDeadImport = true;
238
+ }
239
+ }
240
+
241
+ if (hasDeadImport) {
242
+ warnings.push({
243
+ level: 'warning',
244
+ file: relative(cwd, testFile),
245
+ message: `Test file may be stale — imports reference files that no longer exist. Review and delete if no longer needed.`,
246
+ });
247
+ }
248
+ }
249
+ }
250
+
251
+ return warnings;
252
+ }
253
+
254
+ // ── Main ────────────────────────────────────────────────────────────────────
255
+
256
+ const cwd = process.cwd();
257
+ const files = walk(cwd);
258
+
259
+ const allWarnings = [
260
+ ...checkFileLengths(files, cwd),
261
+ ...checkFunctionLengths(files, cwd),
262
+ ...checkDuplicateBlocks(files, cwd),
263
+ ...checkDirectoryBloat(cwd),
264
+ ...checkStaleTests(files, cwd),
265
+ ];
266
+
267
+ const criticals = allWarnings.filter(w => w.level === 'critical');
268
+ const warnings = allWarnings.filter(w => w.level === 'warning');
269
+
270
+ if (allWarnings.length === 0) {
271
+ process.stderr.write('\n[code-hygiene] All clean. No structural issues found.\n');
272
+ process.exit(0);
273
+ }
274
+
275
+ process.stderr.write('\n[code-hygiene] Structural quality report:\n\n');
276
+
277
+ for (const w of criticals) {
278
+ process.stderr.write(` CRITICAL ${w.file}\n ${w.message}\n\n`);
279
+ }
280
+ for (const w of warnings) {
281
+ process.stderr.write(` WARNING ${w.file}\n ${w.message}\n\n`);
282
+ }
283
+
284
+ process.stderr.write(` Summary: ${criticals.length} critical, ${warnings.length} warnings\n\n`);
285
+
286
+ if (criticals.length > 0) {
287
+ process.stderr.write('[code-hygiene] BLOCKED: Fix critical issues before completing.\n');
288
+ process.stderr.write(' Tip: Use /simplify to auto-refactor long files and extract shared utilities.\n');
289
+ process.exit(2);
290
+ }
291
+
292
+ process.stderr.write('[code-hygiene] Passed with warnings. Consider running /simplify to clean up.\n');
293
+ process.exit(0);
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Pre-commit gate — the single quality checkpoint before any commit.
5
+ * Replaces /done. Triggered by PreToolUse hook on Bash when command contains "git commit".
6
+ *
7
+ * Phase 1 (automated, fast):
8
+ * - No .env files or secrets staged
9
+ * - No merge conflict markers
10
+ * - Lint passes
11
+ * - Tests pass
12
+ *
13
+ * Phase 2 (agent review):
14
+ * - Tells Claude to run code-quality-reviewer and security-reviewer on changed files
15
+ * - Only triggers if Phase 1 passes and changed files haven't been reviewed yet
16
+ *
17
+ * Exit 0 = allow commit
18
+ * Exit 2 = block commit with message
19
+ */
20
+
21
+ import { execSync } from 'node:child_process';
22
+ import fs from 'node:fs';
23
+ import path from 'node:path';
24
+
25
+ const input = process.env.CLAUDE_TOOL_INPUT || '{}';
26
+
27
+ let parsed;
28
+ try {
29
+ parsed = JSON.parse(input);
30
+ } catch {
31
+ process.exit(0);
32
+ }
33
+
34
+ const command = parsed.command || '';
35
+
36
+ // Only gate actual git commit commands
37
+ if (!command.match(/git\s+commit/)) {
38
+ process.exit(0);
39
+ }
40
+
41
+ // Skip amend-only or empty commits
42
+ if (command.includes('--allow-empty')) {
43
+ process.exit(0);
44
+ }
45
+
46
+ const errors = [];
47
+
48
+ // Get staged files once, reuse across checks
49
+ let stagedFiles = '';
50
+ try {
51
+ stagedFiles = execSync('git diff --cached --name-only', { encoding: 'utf-8' }).trim();
52
+ } catch {
53
+ // git not available
54
+ }
55
+
56
+ // === PHASE 1: Automated checks ===
57
+
58
+ // Check 1: No secrets staged
59
+ if (stagedFiles) {
60
+ const secretFilePatterns = ['.env', 'credentials', '.pem', '.key'];
61
+ const secretNamePatterns = ['secret'];
62
+ const flagged = stagedFiles.split('\n').filter(f => {
63
+ const base = path.basename(f).toLowerCase();
64
+ if (f.includes('.chainproof/keys/public.pem')) return false;
65
+ // Always flag secret file extensions regardless of language
66
+ if (secretFilePatterns.some(p => base.includes(p))) return true;
67
+ return secretNamePatterns.some(p => base.includes(p));
68
+ });
69
+ if (flagged.length > 0) {
70
+ errors.push(`Potential secrets staged: ${flagged.join(', ')}\nUnstage these files or confirm they are safe.`);
71
+ }
72
+ }
73
+
74
+ // Check 2: No merge conflict markers
75
+ try {
76
+ const staged = execSync('git diff --cached', { encoding: 'utf-8' });
77
+ if (staged.includes('<<<<<<<') || staged.includes('>>>>>>>')) {
78
+ errors.push('Merge conflict markers found in staged changes. Resolve conflicts first.');
79
+ }
80
+ } catch {
81
+ // skip
82
+ }
83
+
84
+ // Helper: run a shell command, push to errors on failure
85
+ function tryExec(cmd, label, timeout = 30000) {
86
+ try {
87
+ execSync(`${cmd} 2>&1`, { encoding: 'utf-8', timeout });
88
+ return true;
89
+ } catch (e) {
90
+ if (e.status || e.killed) {
91
+ const msg = e.killed ? 'Command timed out' : (e.stdout || e.message);
92
+ errors.push(`${label}:\n${String(msg).slice(0, 500)}`);
93
+ }
94
+ return false;
95
+ }
96
+ }
97
+
98
+ function readPkg(file = 'package.json') {
99
+ try {
100
+ return JSON.parse(fs.readFileSync(file, 'utf-8'));
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+
106
+ // Check 3: Lint
107
+ function runLint() {
108
+ if (fs.existsSync('package.json')) {
109
+ const pkg = readPkg();
110
+ if (!pkg) { errors.push('Lint skipped: package.json is malformed JSON'); return; }
111
+ if (pkg.scripts?.lint) { tryExec('npm run lint', 'Lint failed'); return; }
112
+ tryExec('npx eslint . --ext .js,.ts,.tsx', 'Lint failed');
113
+ return;
114
+ }
115
+ if (fs.existsSync('requirements.txt') || fs.existsSync('pyproject.toml')) {
116
+ tryExec('ruff check .', 'Lint failed');
117
+ return;
118
+ }
119
+ if (fs.existsSync('frontend/package.json')) {
120
+ tryExec('cd frontend && npx eslint .', 'Frontend lint failed');
121
+ }
122
+ if (fs.existsSync('backend/requirements.txt')) {
123
+ tryExec('cd backend && ruff check .', 'Backend lint failed');
124
+ }
125
+ }
126
+
127
+ // Check 4: Tests
128
+ function runTests() {
129
+ const testTimeout = 120000;
130
+ if (fs.existsSync('package.json')) {
131
+ const pkg = readPkg();
132
+ if (!pkg) { errors.push('Tests skipped: package.json is malformed JSON'); return; }
133
+ if (pkg.scripts?.test) { tryExec('npm test', 'Tests failed', testTimeout); return; }
134
+ tryExec('npx vitest run', 'Tests failed', testTimeout);
135
+ return;
136
+ }
137
+ if (fs.existsSync('requirements.txt') || fs.existsSync('pyproject.toml')) {
138
+ tryExec('pytest', 'Tests failed', testTimeout);
139
+ return;
140
+ }
141
+ if (fs.existsSync('frontend/package.json')) {
142
+ tryExec('cd frontend && npx vitest run', 'Frontend tests failed', testTimeout);
143
+ }
144
+ if (fs.existsSync('backend/requirements.txt')) {
145
+ tryExec('cd backend && pytest', 'Backend tests failed', testTimeout);
146
+ }
147
+ }
148
+
149
+ runLint();
150
+ runTests();
151
+
152
+ if (errors.length > 0) {
153
+ console.error('[pre-commit] BLOCKED - fix these issues first:\n');
154
+ errors.forEach(e => console.error(` ${e}\n`));
155
+ console.error('Run /build-fix to auto-resolve lint and build errors.');
156
+ process.exit(2);
157
+ }
158
+
159
+ // === PHASE 2: Agent review gate ===
160
+ // Check if changed files have been reviewed in this session.
161
+ // We use a marker file that gets created when agents complete review.
162
+
163
+ const reviewMarker = '.claude/.last-review';
164
+ let needsReview = false;
165
+
166
+ try {
167
+ if (!stagedFiles) {
168
+ // Nothing staged, allow
169
+ process.exit(0);
170
+ }
171
+
172
+ const stagedList = stagedFiles.split('\n').filter(f =>
173
+ f.endsWith('.js') || f.endsWith('.ts') || f.endsWith('.tsx') ||
174
+ f.endsWith('.py') || f.endsWith('.jsx') || f.endsWith('.mjs')
175
+ );
176
+
177
+ if (stagedList.length === 0) {
178
+ // No code files staged (just docs/config), skip review
179
+ process.exit(0);
180
+ }
181
+
182
+ if (fs.existsSync(reviewMarker)) {
183
+ try {
184
+ const review = JSON.parse(fs.readFileSync(reviewMarker, 'utf-8'));
185
+ const reviewedFiles = new Set(review.files || []);
186
+ const unreviewed = stagedList.filter(f => !reviewedFiles.has(f));
187
+ if (unreviewed.length > 0) {
188
+ needsReview = true;
189
+ }
190
+ } catch {
191
+ needsReview = true;
192
+ }
193
+ } else {
194
+ needsReview = true;
195
+ }
196
+ } catch {
197
+ // Can't determine, allow
198
+ process.exit(0);
199
+ }
200
+
201
+ if (needsReview) {
202
+ console.error('[pre-commit] Code review required before commit.\n');
203
+ console.error('Run code-quality-reviewer and security-reviewer agents on the changed files,');
204
+ console.error('then retry the commit. Or run /code-review to do this automatically.\n');
205
+ console.error('To mark files as reviewed, the agents will update .claude/.last-review.');
206
+ process.exit(2);
207
+ }
@@ -9,6 +9,15 @@
9
9
  "command": "node .claude/hooks/guard-protected-files.mjs"
10
10
  }
11
11
  ]
12
+ },
13
+ {
14
+ "matcher": "Bash",
15
+ "hooks": [
16
+ {
17
+ "type": "command",
18
+ "command": "node .claude/hooks/pre-commit-gate.mjs"
19
+ }
20
+ ]
12
21
  }
13
22
  ],
14
23
  "PostToolUse": [
@@ -27,7 +36,7 @@
27
36
  "hooks": [
28
37
  {
29
38
  "type": "command",
30
- "command": "npx tsc --noEmit 2>&1 && npx eslint . 2>&1"
39
+ "command": "node .claude/hooks/code-hygiene.mjs"
31
40
  }
32
41
  ]
33
42
  }
@@ -1,44 +1,122 @@
1
1
  ---
2
- name: AI Prompts
3
- description: AI/LLM integration patterns and best practices
2
+ name: ai-prompts
3
+ description: AI/LLM integration patterns, guardrails infrastructure, and compliance (EU AI Act, NIST AI RMF)
4
4
  ---
5
5
 
6
- # AI/LLM Integration
7
-
8
- ## Structured Output
9
- - Always validate AI responses with Pydantic (Python) or Zod (TypeScript)
10
- - Never use raw string responses in application logic
11
- - Define response schemas before making API calls
12
- - Handle malformed responses gracefully
13
-
14
- ## Prompt Engineering
15
- - Use system prompts for consistent behavior
16
- - Include examples (few-shot) for complex tasks
17
- - Be specific about output format
18
- - Test prompts with edge cases
19
-
20
- ## Failover Patterns
21
- - Implement rule-based fallback when AI is unavailable
22
- - Set aggressive timeouts (30-60s for most calls)
23
- - Retry with exponential backoff (max 3 attempts)
24
- - Cache responses when appropriate
25
- - Monitor token usage and costs
26
-
27
- ## Rate Limiting
28
- - Implement client-side rate limiting before API calls
29
- - Queue requests during high load
30
- - Use batch APIs when processing multiple items
31
- - Handle 429 responses gracefully
32
-
33
- ## Security
34
- - Never include user secrets in prompts
35
- - Sanitize user input before including in prompts
36
- - Protect against prompt injection attacks (delimiter tokens, input validation, output filtering)
37
- - Validate and sanitize AI output before using
38
- - Don't trust AI output for security decisions
39
-
40
- ## Testing
41
- - Use golden datasets for regression testing
42
- - Mock AI responses in unit tests
43
- - Test timeout and error handling paths
44
- - Monitor response quality over time
6
+ # AI/LLM Integration & Guardrails
7
+
8
+ This project includes AI guardrails infrastructure in `src/lib/ai/` (TypeScript) or `app/ai/` (Python).
9
+
10
+ ## Using the AI Client
11
+
12
+ All AI calls MUST go through the guardrails client. Never call the Anthropic SDK directly.
13
+
14
+ **TypeScript:**
15
+ ```typescript
16
+ import { getAIClient } from '@/lib/ai';
17
+ import { z } from 'zod';
18
+
19
+ const ai = getAIClient();
20
+ const result = await ai.generate({
21
+ prompt: 'Analyze this text',
22
+ schema: z.object({ sentiment: z.enum(['positive', 'negative', 'neutral']), confidence: z.number() }),
23
+ purpose: 'sentiment-analysis', // Required for audit trail
24
+ });
25
+
26
+ if (result.needsHumanReview) {
27
+ // Route to approval queue — confidence below threshold
28
+ }
29
+ ```
30
+
31
+ **Python:**
32
+ ```python
33
+ from app.ai import get_ai_client
34
+ from pydantic import BaseModel
35
+
36
+ class Sentiment(BaseModel):
37
+ sentiment: str
38
+ confidence: float
39
+
40
+ ai = get_ai_client()
41
+ result = await ai.generate(prompt="Analyze this text", schema=Sentiment, purpose="sentiment-analysis")
42
+
43
+ if result.needs_human_review:
44
+ # Route to approval queue
45
+ ```
46
+
47
+ ## Guardrails Architecture
48
+
49
+ | Layer | What It Does | Compliance |
50
+ |-------|-------------|------------|
51
+ | **Input Guard** | Prompt injection detection, input sanitization, length limits | EU AI Act Art. 15, NIST Manage 2.2 |
52
+ | **Output Validation** | Zod/Pydantic schema validation with retry on parse failure | NIST Manage 2.4 |
53
+ | **Confidence Scoring** | Scores each response (0-1), routes low confidence to human review | NIST Measure 2.5, EU AI Act Art. 14 |
54
+ | **Audit Logger** | Structured logging of every AI interaction (input preview, output, confidence, model, latency) | EU AI Act Art. 12, NIST Manage 1.3 |
55
+ | **Health Metrics** | AI-specific health endpoint: availability, confidence distribution, error rates, per-model stats | NIST Manage 3.2, EU AI Act Art. 9 |
56
+ | **AI Disclosure** | All AI responses carry `aiGenerated: true` flag | EU AI Act Art. 50 |
57
+
58
+ ## Rules
59
+
60
+ - **Never call the Anthropic/OpenAI SDK directly.** Always use `getAIClient()` / `get_ai_client()`
61
+ - **Never use raw string responses in application logic.** Always validate with Zod/Pydantic schemas
62
+ - **Never skip the `purpose` parameter.** Every AI call must be tagged for audit traceability
63
+ - **Never trust AI output for security decisions.** Always validate independently
64
+ - **Never log full prompts.** The audit logger captures only a 200-char preview to avoid PII leakage
65
+ - **Always handle `needsHumanReview`.** If confidence is below threshold, the response must be reviewed before acting on it
66
+
67
+ ## Confidence Thresholds
68
+
69
+ | Confidence | Action | Use Case |
70
+ |-----------|--------|----------|
71
+ | > 0.9 | Auto-accept | Low-risk: summaries, formatting, classification |
72
+ | 0.7 - 0.9 | Accept with logging | Medium-risk: recommendations, content generation |
73
+ | 0.5 - 0.7 | Flag for review | High-risk: decisions, user-facing content |
74
+ | < 0.5 | Require human approval | Critical: financial, medical, legal, safety |
75
+
76
+ Configure the threshold per-call via `confidenceThreshold` parameter.
77
+
78
+ ## Prompt Injection Protection
79
+
80
+ The input guard detects:
81
+ - **Instruction override**: "ignore previous instructions", "disregard your rules"
82
+ - **Role manipulation**: "you are now a...", "pretend to be..."
83
+ - **System prompt extraction**: "show me your system prompt", "repeat your instructions"
84
+ - **Delimiter injection**: `<system>`, `[INST]`, `<|im_start|>`
85
+ - **Data exfiltration**: "send this data to..."
86
+
87
+ Detected injections are blocked and logged. Suspicious patterns (encoded payloads, code execution) are logged but not blocked.
88
+
89
+ ## AI Health Endpoint
90
+
91
+ Mount at `/api/ai/health`. Returns:
92
+ ```json
93
+ {
94
+ "status": "ok | degraded | unhealthy",
95
+ "aiAvailable": true,
96
+ "metrics": {
97
+ "totalCalls": 142,
98
+ "successRate": 0.97,
99
+ "avgConfidence": 0.84,
100
+ "avgLatencyMs": 1230,
101
+ "lowConfidenceRate": 0.08,
102
+ "errorRate": 0.03
103
+ },
104
+ "models": {
105
+ "claude-sonnet-4-20250514": { "calls": 142, "successRate": 0.97, "avgLatencyMs": 1230 }
106
+ }
107
+ }
108
+ ```
109
+
110
+ ## Recommended Libraries
111
+
112
+ **TypeScript:** Zod (validation), @anthropic-ai/sdk (model calls), @instructor-ai/instructor (structured extraction)
113
+ **Python:** Pydantic (validation), anthropic (model calls), instructor (structured extraction), guardrails-ai (advanced validation), presidio (PII detection)
114
+ **Observability:** Langfuse (open-source LLM tracing), OpenTelemetry (spans), Helicone (proxy logging)
115
+
116
+ ## Testing AI Integrations
117
+
118
+ - Mock AI responses in unit tests — never make real API calls in CI
119
+ - Use golden datasets for regression testing prompt quality
120
+ - Test timeout, retry, and error handling paths explicitly
121
+ - Test with adversarial inputs (prompt injection patterns)
122
+ - Monitor confidence score distribution for drift over time