tribunal-kit 5.8.2 → 5.8.4

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 (101) hide show
  1. package/.agent/ARCHITECTURE.md +12 -12
  2. package/.agent/agents/complexity-reviewer.md +53 -0
  3. package/.agent/agents/precedence-reviewer.md +12 -12
  4. package/.agent/agents/swarm-worker-registry.md +3 -3
  5. package/.agent/history/integrity_manifest.json +958 -0
  6. package/.agent/history/memory/.memory.idx +305 -1
  7. package/.agent/history/memory/MEMORY.md +21 -1
  8. package/.agent/rules/GEMINI.md +31 -28
  9. package/.agent/scripts/case_law_manager.js +3 -15
  10. package/.agent/scripts/context_broker.js +3 -1
  11. package/.agent/scripts/guardrail_engine.js +643 -0
  12. package/.agent/scripts/integrity_manifest.js +486 -0
  13. package/.agent/scripts/signal_detector.js +1 -1
  14. package/.agent/scripts/skill_evolution.js +3 -15
  15. package/.agent/scripts/skill_integrator.js +2 -12
  16. package/.agent/scripts/swarm_dispatcher.js +2 -12
  17. package/.agent/skills/12-principles-of-animation/SKILL.md +69 -0
  18. package/.agent/skills/60fps-animation/SKILL.md +78 -0
  19. package/.agent/skills/accessible-animation/SKILL.md +86 -0
  20. package/.agent/skills/adapt/SKILL.md +75 -0
  21. package/.agent/skills/animation-on-scroll/SKILL.md +110 -0
  22. package/.agent/skills/animation-systems/SKILL.md +69 -0
  23. package/.agent/skills/antfu-conventions/SKILL.md +74 -0
  24. package/.agent/skills/app-builder/SKILL.md +1 -1
  25. package/.agent/skills/apple-design/SKILL.md +108 -0
  26. package/.agent/skills/audit-and-fix/SKILL.md +83 -0
  27. package/.agent/skills/baseline-ui/SKILL.md +74 -0
  28. package/.agent/skills/better-colors/SKILL.md +124 -0
  29. package/.agent/skills/better-typography/SKILL.md +111 -0
  30. package/.agent/skills/better-ui/SKILL.md +102 -0
  31. package/.agent/skills/bolder/SKILL.md +68 -0
  32. package/.agent/skills/build-primitive/SKILL.md +70 -0
  33. package/.agent/skills/clarify/SKILL.md +75 -0
  34. package/.agent/skills/cobejs/SKILL.md +96 -0
  35. package/.agent/skills/codebase-design/SKILL.md +84 -0
  36. package/.agent/skills/colorize/SKILL.md +77 -0
  37. package/.agent/skills/compact-landing/SKILL.md +70 -0
  38. package/.agent/skills/company-logos/SKILL.md +73 -0
  39. package/.agent/skills/create-design-md/SKILL.md +104 -0
  40. package/.agent/skills/critique/SKILL.md +95 -0
  41. package/.agent/skills/delight/SKILL.md +65 -0
  42. package/.agent/skills/design-lab/SKILL.md +59 -0
  43. package/.agent/skills/diagnosing-bugs/SKILL.md +190 -0
  44. package/.agent/skills/distill/SKILL.md +65 -0
  45. package/.agent/skills/domain-modeling/SKILL.md +78 -0
  46. package/.agent/skills/fixing-accessibility/SKILL.md +98 -0
  47. package/.agent/skills/fixing-metadata/SKILL.md +97 -0
  48. package/.agent/skills/fixing-motion-performance/SKILL.md +96 -0
  49. package/.agent/skills/gpt-taste/SKILL.md +68 -0
  50. package/.agent/skills/harden/SKILL.md +74 -0
  51. package/.agent/skills/impeccable/SKILL.md +72 -0
  52. package/.agent/skills/improve-codebase-architecture/SKILL.md +67 -0
  53. package/.agent/skills/improve-ui/SKILL.md +83 -0
  54. package/.agent/skills/landing-page/SKILL.md +88 -0
  55. package/.agent/skills/lint-and-validate/SKILL.md +1 -2
  56. package/.agent/skills/local-first-architecture/SKILL.md +66 -0
  57. package/.agent/skills/lottie-animation/SKILL.md +82 -0
  58. package/.agent/skills/marquee-loop/SKILL.md +96 -0
  59. package/.agent/skills/masked-reveal/SKILL.md +88 -0
  60. package/.agent/skills/micro-interaction/SKILL.md +100 -0
  61. package/.agent/skills/morphing-icons/SKILL.md +85 -0
  62. package/.agent/skills/page-transition-animation/SKILL.md +108 -0
  63. package/.agent/skills/parallel-agents/SKILL.md +5 -5
  64. package/.agent/skills/polish/SKILL.md +63 -0
  65. package/.agent/skills/pricing-page/SKILL.md +68 -0
  66. package/.agent/skills/progressive-blur/SKILL.md +100 -0
  67. package/.agent/skills/project-idioms/SKILL.md +7 -7
  68. package/.agent/skills/quieter/SKILL.md +68 -0
  69. package/.agent/skills/react-doctor/SKILL.md +70 -0
  70. package/.agent/skills/redesign-skill/SKILL.md +65 -0
  71. package/.agent/skills/shape/SKILL.md +73 -0
  72. package/.agent/skills/soft-skill/SKILL.md +72 -0
  73. package/.agent/skills/sounds-on-the-web/SKILL.md +100 -0
  74. package/.agent/skills/svg-animation/SKILL.md +91 -0
  75. package/.agent/skills/swiss-design/SKILL.md +64 -0
  76. package/.agent/skills/taste-skill/SKILL.md +71 -0
  77. package/.agent/skills/tdd-workflow/SKILL.md +36 -165
  78. package/.agent/skills/test-result-analyzer/SKILL.md +1 -1
  79. package/.agent/skills/thermo-nuclear-code-quality-review/SKILL.md +82 -0
  80. package/.agent/skills/to-spring-or-not-to-spring/SKILL.md +66 -0
  81. package/.agent/skills/transitions-dev/SKILL.md +101 -0
  82. package/.agent/skills/typeset/SKILL.md +76 -0
  83. package/.agent/skills/ui-skills-root/SKILL.md +85 -0
  84. package/.agent/skills/web-quality-audit/SKILL.md +67 -0
  85. package/.agent/workflows/fix.md +2 -2
  86. package/.agent/workflows/preview.md +5 -5
  87. package/.agent/workflows/status.md +1 -1
  88. package/.agent/workflows/tribunal-full.md +3 -3
  89. package/.agent/workflows/tribunal-speed.md +2 -2
  90. package/README.md +348 -238
  91. package/bin/tribunal-kit.js +62 -1482
  92. package/dist/cli.js +13 -0
  93. package/dist/commands/align.js +1 -1
  94. package/dist/commands/compile.js +1 -1
  95. package/dist/commands/guardrail.js +275 -0
  96. package/dist/commands/init.js +10 -9
  97. package/dist/commands/optimize.js +318 -0
  98. package/dist/utils/version.js +1 -1
  99. package/package.json +10 -8
  100. package/scripts/fix-vbc.js +98 -0
  101. package/scripts/fix-vbc.ps1 +119 -0
@@ -0,0 +1,643 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * guardrail_engine.js — Tribunal Kit Neurosymbolic Guardrail Engine
4
+ * ==================================================================
5
+ * A deterministic, zero-LLM-call validator that checks agent output
6
+ * for phantom references, wrong counts, broken script extensions,
7
+ * unresolved VERIFY tags, and contradictory claims.
8
+ *
9
+ * Runs sub-millisecond. Pure Node.js. No external dependencies.
10
+ *
11
+ * This is a NEUROSYMBOLIC layer — it enforces hard rules that no
12
+ * amount of prompt engineering can guarantee. The LLM can hallucinate
13
+ * freely; this engine catches structural errors before they reach disk.
14
+ *
15
+ * Usage:
16
+ * node .agent/scripts/guardrail_engine.js --file output.md
17
+ * node .agent/scripts/guardrail_engine.js --scan → scan all .agent/ files
18
+ * node .agent/scripts/guardrail_engine.js --fix output.md → auto-fix simple violations
19
+ *
20
+ * API:
21
+ * const { validate } = require('./guardrail_engine');
22
+ * const result = validate(text, manifest, options);
23
+ */
24
+
25
+ "use strict";
26
+
27
+ const fs = require("fs");
28
+ const path = require("path");
29
+
30
+ // ── Rule Definitions ──────────────────────────────────────────────────────────
31
+
32
+ /**
33
+ * Rule 1: file-exists
34
+ * Checks that references to .agent/ files point to real files.
35
+ */
36
+ function ruleFileExists(content, manifest, _ctx) {
37
+ const violations = [];
38
+ const refs = manifest.cross_references || [];
39
+
40
+ const agentDir = path.join(_ctx.projectRoot, ".agent");
41
+ const currentRelSource = _ctx.filePath ? path.relative(agentDir, _ctx.filePath).replace(/\\/g, "/") : null;
42
+
43
+ // Check phantom references from the manifest
44
+ const phantoms = refs.filter((r) => !r.exists && (!currentRelSource || r.source === currentRelSource));
45
+ for (const p of phantoms) {
46
+ violations.push({
47
+ rule: "file-exists",
48
+ severity: "critical",
49
+ location: p.source,
50
+ message: `Phantom reference: "${p.ref}" does not exist`,
51
+ suggestion: null,
52
+ autoFixable: false,
53
+ });
54
+ }
55
+
56
+ return violations;
57
+ }
58
+
59
+ /**
60
+ * Rule 2: script-extension
61
+ * Catches .py references to scripts that are actually .js files.
62
+ */
63
+ function ruleScriptExtension(content, manifest, _ctx) {
64
+ const violations = [];
65
+ const scriptFiles = manifest.scripts ? manifest.scripts.files : {};
66
+
67
+ // Find all script references in the content
68
+ const scriptRefRegex = /scripts\/([\w_-]+)\.(js|py)\b/g;
69
+ let match;
70
+ while ((match = scriptRefRegex.exec(content)) !== null) {
71
+ const baseName = match[1];
72
+ const referencedExt = match[2];
73
+ const referencedFile = `${baseName}.${referencedExt}`;
74
+
75
+ // Check if the referenced file exists
76
+ if (scriptFiles[referencedFile]) continue; // exact match — fine
77
+
78
+ // Check if the OTHER extension exists
79
+ const altExt = referencedExt === "js" ? "py" : "js";
80
+ const altFile = `${baseName}.${altExt}`;
81
+
82
+ if (scriptFiles[altFile]) {
83
+ violations.push({
84
+ rule: "script-extension",
85
+ severity: "warning",
86
+ location: `content match: scripts/${referencedFile}`,
87
+ message: `Script "scripts/${referencedFile}" does not exist, but "scripts/${altFile}" does`,
88
+ suggestion: `Replace "scripts/${referencedFile}" with "scripts/${altFile}"`,
89
+ autoFixable: true,
90
+ fix: { find: `scripts/${referencedFile}`, replace: `scripts/${altFile}` },
91
+ });
92
+ }
93
+ }
94
+
95
+ return violations;
96
+ }
97
+
98
+ /**
99
+ * Rule 3: reviewer-count
100
+ * Validates that numeric claims about reviewer counts match the manifest.
101
+ */
102
+ function ruleReviewerCount(content, manifest, _ctx) {
103
+ const violations = [];
104
+ const actualCount = manifest.agents ? manifest.agents.reviewer_count : null;
105
+ if (actualCount === null) return violations;
106
+
107
+ const countRegex = /(\d+)[-\s]+(?:parallel\s+)?(?:reviewers?|agents?)/gi;
108
+ let match;
109
+ while ((match = countRegex.exec(content)) !== null) {
110
+ const claimed = parseInt(match[1], 10);
111
+ if (claimed < 3 || claimed > 500) continue; // Skip noise
112
+
113
+ if (claimed !== actualCount) {
114
+ violations.push({
115
+ rule: "reviewer-count",
116
+ severity: "warning",
117
+ location: `content match: "${match[0]}"`,
118
+ message: `Claims ${claimed} reviewers, but manifest reports ${actualCount}`,
119
+ suggestion: `Replace "${match[0]}" with "${actualCount} reviewers"`,
120
+ autoFixable: true,
121
+ fix: { find: match[0], replace: match[0].replace(String(claimed), String(actualCount)) },
122
+ });
123
+ }
124
+ }
125
+
126
+ return violations;
127
+ }
128
+
129
+ /**
130
+ * Rule 4: skill-exists
131
+ * Validates that skill names referenced in routing tables exist.
132
+ */
133
+ function ruleSkillExists(content, manifest, _ctx) {
134
+ const violations = [];
135
+ const skillNames = new Set(manifest.skills ? manifest.skills.names : []);
136
+ if (skillNames.size === 0) return violations;
137
+
138
+ // Look for backtick-quoted skill references that match the pattern
139
+ const skillRefRegex = /`([\w-]+(?:-[\w-]+)*)`/g;
140
+ let match;
141
+ while ((match = skillRefRegex.exec(content)) !== null) {
142
+ const name = match[1];
143
+
144
+ // Only check names that look like skill names (contain a hyphen, not pure keywords)
145
+ if (!name.includes("-")) continue;
146
+ // Skip common non-skill patterns
147
+ if (
148
+ name.startsWith("--") ||
149
+ name.endsWith(".md") ||
150
+ name.endsWith(".js") ||
151
+ name.endsWith(".ts") ||
152
+ name.endsWith(".py") ||
153
+ name.startsWith("npm-") ||
154
+ name.startsWith("git-")
155
+ ) {
156
+ continue;
157
+ }
158
+
159
+ // Check if it could be a skill or agent name
160
+ const agentNames = new Set(manifest.agents ? manifest.agents.all : []);
161
+ const isKnownSkill = skillNames.has(name);
162
+ const isKnownAgent = agentNames.has(name);
163
+
164
+ // If it matches the naming pattern of skills/agents but doesn't exist as either
165
+ if (!isKnownSkill && !isKnownAgent) {
166
+ // Only flag if it looks like it's being used as a skill/agent reference
167
+ // Check surrounding context for routing-table-like patterns
168
+ const lineStart = content.lastIndexOf("\n", match.index) + 1;
169
+ const lineEnd = content.indexOf("\n", match.index);
170
+ const line = content.substring(lineStart, lineEnd === -1 ? content.length : lineEnd);
171
+
172
+ // Flag only if the line looks like a routing/reference context
173
+ if (
174
+ /agent|skill|specialist|reviewer|route|domain/i.test(line) &&
175
+ !/(example|e\.g\.|sample|template|placeholder)/i.test(line)
176
+ ) {
177
+ violations.push({
178
+ rule: "skill-exists",
179
+ severity: "info",
180
+ location: `content match: \`${name}\``,
181
+ message: `"${name}" is not a recognized skill or agent name`,
182
+ suggestion: `Verify that "${name}" exists in .agent/skills/ or .agent/agents/`,
183
+ autoFixable: false,
184
+ });
185
+ }
186
+ }
187
+ }
188
+
189
+ return violations;
190
+ }
191
+
192
+ /**
193
+ * Rule 5: agent-exists
194
+ * Validates that agent names referenced exist as .md files.
195
+ */
196
+ function ruleAgentExists(content, manifest, _ctx) {
197
+ const violations = [];
198
+ const agentNames = new Set(manifest.agents ? manifest.agents.all : []);
199
+ if (agentNames.size === 0) return violations;
200
+
201
+ // Look for agents/name.md references
202
+ const agentRefRegex = /agents\/([\w-]+)\.md/g;
203
+ let match;
204
+ while ((match = agentRefRegex.exec(content)) !== null) {
205
+ const name = match[1];
206
+ if (!agentNames.has(name)) {
207
+ violations.push({
208
+ rule: "agent-exists",
209
+ severity: "critical",
210
+ location: `content match: agents/${name}.md`,
211
+ message: `Agent "agents/${name}.md" is referenced but does not exist`,
212
+ suggestion: null,
213
+ autoFixable: false,
214
+ });
215
+ }
216
+ }
217
+
218
+ return violations;
219
+ }
220
+
221
+ /**
222
+ * Rule 6: unresolved-verify
223
+ * Blocks output that contains // VERIFY: tags (must be resolved before shipping).
224
+ */
225
+ function ruleUnresolvedVerify(content, _manifest, ctx) {
226
+ const violations = [];
227
+
228
+ // Skip this check for the internal .agent documentation files themselves
229
+ // because they contain instructions ABOUT the VERIFY tag.
230
+ if (ctx && ctx.filePath && ctx.filePath.includes(".agent") && ctx.filePath.endsWith(".md")) {
231
+ return violations;
232
+ }
233
+
234
+ const lines = content.split(/\r?\n/);
235
+
236
+ for (let i = 0; i < lines.length; i++) {
237
+ const verifyMatch = lines[i].match(/\/\/\s*VERIFY:\s*(.+)/);
238
+ if (verifyMatch) {
239
+ violations.push({
240
+ rule: "unresolved-verify",
241
+ severity: "critical",
242
+ location: `line ${i + 1}`,
243
+ message: `Unresolved VERIFY tag: "${verifyMatch[1].trim()}"`,
244
+ suggestion: "Resolve this verification before shipping",
245
+ autoFixable: false,
246
+ });
247
+ }
248
+ }
249
+
250
+ return violations;
251
+ }
252
+
253
+ /**
254
+ * Rule 7: numeric-consistency
255
+ * Checks that numeric claims in the content are consistent with the manifest.
256
+ */
257
+ function ruleNumericConsistency(content, manifest, _ctx) {
258
+ const violations = [];
259
+ const agentDir = _ctx && _ctx.projectRoot ? path.join(_ctx.projectRoot, ".agent") : "";
260
+ const currentRelSource = _ctx && _ctx.filePath ? path.relative(agentDir, _ctx.filePath).replace(/\\/g, "/") : null;
261
+
262
+ const invalidClaims = (manifest.numeric_claims || []).filter(
263
+ (c) => !c.valid && (!currentRelSource || c.source.startsWith(currentRelSource + ":")),
264
+ );
265
+
266
+ for (const claim of invalidClaims) {
267
+ violations.push({
268
+ rule: "numeric-consistency",
269
+ severity: "warning",
270
+ location: claim.source,
271
+ message: `Claims "${claim.claim}" but actual count is ${claim.actual}`,
272
+ suggestion: `Update to "${claim.actual} ${claim.claim.replace(/\d+\s*/, "")}"`,
273
+ autoFixable: true,
274
+ fix: {
275
+ find: claim.claim,
276
+ replace: `${claim.actual} ${claim.claim.replace(/\d+\s*/, "")}`,
277
+ },
278
+ });
279
+ }
280
+
281
+ return violations;
282
+ }
283
+
284
+ /**
285
+ * Rule 8: import-phantom
286
+ * Checks for import/require statements referencing packages not in package.json.
287
+ */
288
+ function ruleImportPhantom(content, _manifest, ctx) {
289
+ const violations = [];
290
+ const projectRoot = ctx.projectRoot;
291
+ const filePath = ctx.filePath || "";
292
+
293
+ // Only check Javascript/Typescript files for phantom imports
294
+ if (!filePath.endsWith(".js") && !filePath.endsWith(".ts")) return violations;
295
+
296
+ // Load package.json dependencies
297
+ let deps = new Set();
298
+ try {
299
+ const pkgPath = path.join(projectRoot, "package.json");
300
+ if (fs.existsSync(pkgPath)) {
301
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
302
+ const allDeps = {
303
+ ...(pkg.dependencies || {}),
304
+ ...(pkg.devDependencies || {}),
305
+ ...(pkg.peerDependencies || {}),
306
+ ...(pkg.optionalDependencies || {}),
307
+ };
308
+ deps = new Set(Object.keys(allDeps));
309
+ }
310
+ } catch {
311
+ return violations; // Can't check without package.json
312
+ }
313
+
314
+ if (deps.size === 0) return violations;
315
+
316
+ // Node.js built-in modules
317
+ const builtins = new Set([
318
+ "assert", "buffer", "child_process", "cluster", "console", "constants",
319
+ "crypto", "dgram", "dns", "domain", "events", "fs", "http", "http2",
320
+ "https", "module", "net", "os", "path", "perf_hooks", "process",
321
+ "punycode", "querystring", "readline", "repl", "stream", "string_decoder",
322
+ "sys", "timers", "tls", "trace_events", "tty", "url", "util", "v8",
323
+ "vm", "wasi", "worker_threads", "zlib", "node:fs", "node:path",
324
+ "node:crypto", "node:http", "node:https", "node:url", "node:util",
325
+ "node:os", "node:child_process", "node:stream", "node:events",
326
+ "node:buffer", "node:net", "node:tls", "node:worker_threads",
327
+ "node:readline", "node:zlib", "node:querystring", "node:assert",
328
+ "node:test",
329
+ ]);
330
+
331
+ // Find require() and import statements for external packages
332
+ const requireRegex = /require\s*\(\s*['"]([^'"./][^'"]*)['"]\s*\)/g;
333
+ const importRegex = /import\s+(?:[\s\S]*?\s+from\s+)?['"]([^'"./][^'"]*)['"]/g;
334
+
335
+ const checkPackage = (regex) => {
336
+ let match;
337
+ while ((match = regex.exec(content)) !== null) {
338
+ const pkg = match[1];
339
+ // Extract the package name (handle scoped packages like @scope/name)
340
+ const pkgName = pkg.startsWith("@")
341
+ ? pkg.split("/").slice(0, 2).join("/")
342
+ : pkg.split("/")[0];
343
+
344
+ if (!builtins.has(pkgName) && !deps.has(pkgName)) {
345
+ violations.push({
346
+ rule: "import-phantom",
347
+ severity: "info",
348
+ location: `content match: "${match[0]}"`,
349
+ message: `Package "${pkgName}" is not in package.json`,
350
+ suggestion: `Verify "${pkgName}" is installed or add it to dependencies`,
351
+ autoFixable: false,
352
+ });
353
+ }
354
+ }
355
+ };
356
+
357
+ checkPackage(requireRegex);
358
+ checkPackage(importRegex);
359
+
360
+ return violations;
361
+ }
362
+
363
+ // ── Rule Registry ─────────────────────────────────────────────────────────────
364
+
365
+ const RULES = {
366
+ "file-exists": ruleFileExists,
367
+ "script-extension": ruleScriptExtension,
368
+ "reviewer-count": ruleReviewerCount,
369
+ "skill-exists": ruleSkillExists,
370
+ "agent-exists": ruleAgentExists,
371
+ "unresolved-verify": ruleUnresolvedVerify,
372
+ "numeric-consistency": ruleNumericConsistency,
373
+ "import-phantom": ruleImportPhantom,
374
+ };
375
+
376
+ // ── Main Validate Function ────────────────────────────────────────────────────
377
+
378
+ /**
379
+ * Validate content against the integrity manifest.
380
+ *
381
+ * @param {string} content - The text content to validate
382
+ * @param {object} manifest - The integrity manifest (from integrity_manifest.js)
383
+ * @param {object} options - Validation options
384
+ * @param {string[]} options.rules - Which rules to run ('all' or specific names)
385
+ * @param {boolean} options.autoFix - If true, returns auto-fixed content
386
+ * @param {object} options.context - Additional context (projectRoot, etc.)
387
+ * @returns {object} Validation result
388
+ */
389
+ function validate(content, manifest, options = {}) {
390
+ const {
391
+ rules: ruleFilter = ["all"],
392
+ autoFix = false,
393
+ context = { projectRoot: process.cwd() },
394
+ } = options;
395
+
396
+ const activeRules =
397
+ ruleFilter.includes("all")
398
+ ? Object.entries(RULES)
399
+ : Object.entries(RULES).filter(([name]) => ruleFilter.includes(name));
400
+
401
+ const allViolations = [];
402
+
403
+ for (const [_name, ruleFn] of activeRules) {
404
+ try {
405
+ const violations = ruleFn(content, manifest, context);
406
+ allViolations.push(...violations);
407
+ } catch (err) {
408
+ allViolations.push({
409
+ rule: _name,
410
+ severity: "info",
411
+ location: "engine",
412
+ message: `Rule "${_name}" threw an error: ${err.message}`,
413
+ suggestion: null,
414
+ autoFixable: false,
415
+ });
416
+ }
417
+ }
418
+
419
+ // Apply auto-fixes if requested
420
+ let autoFixedContent = null;
421
+ if (autoFix) {
422
+ autoFixedContent = content;
423
+ const fixableViolations = allViolations.filter(
424
+ (v) => v.autoFixable && v.fix,
425
+ );
426
+ for (const v of fixableViolations) {
427
+ autoFixedContent = autoFixedContent.split(v.fix.find).join(v.fix.replace);
428
+ }
429
+ }
430
+
431
+ // Compute pass/fail
432
+ const hasCritical = allViolations.some((v) => v.severity === "critical");
433
+ const hasWarning = allViolations.some((v) => v.severity === "warning");
434
+
435
+ return {
436
+ passed: !hasCritical,
437
+ violations: allViolations,
438
+ autoFixedContent,
439
+ summary: hasCritical
440
+ ? `REJECTED — ${allViolations.filter((v) => v.severity === "critical").length} critical violation(s)`
441
+ : hasWarning
442
+ ? `WARNING — ${allViolations.filter((v) => v.severity === "warning").length} warning(s), no critical issues`
443
+ : allViolations.length > 0
444
+ ? `PASSED — ${allViolations.length} informational note(s)`
445
+ : "PASSED — clean, no violations",
446
+ };
447
+ }
448
+
449
+ // ── CLI Entry Point ───────────────────────────────────────────────────────────
450
+
451
+ function main() {
452
+ const args = process.argv.slice(2);
453
+
454
+ const fileIdx = args.indexOf("--file");
455
+ const scanMode = args.includes("--scan");
456
+ const fixMode = args.includes("--fix");
457
+ const jsonMode = args.includes("--json");
458
+
459
+ // Load or generate manifest
460
+ const projectRoot = process.cwd();
461
+ let manifest;
462
+
463
+ const manifestPath = path.join(
464
+ projectRoot,
465
+ ".agent",
466
+ "history",
467
+ "integrity_manifest.json",
468
+ );
469
+
470
+ if (false && fs.existsSync(manifestPath)) {
471
+ manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
472
+ } else {
473
+ // Generate on the fly
474
+ try {
475
+ const { generateManifest } = require("./integrity_manifest");
476
+ manifest = generateManifest(projectRoot);
477
+ } catch {
478
+ console.error("❌ Cannot load or generate integrity manifest.");
479
+ console.error(
480
+ " Run: node .agent/scripts/integrity_manifest.js first.",
481
+ );
482
+ process.exit(1);
483
+ }
484
+ }
485
+
486
+ if (manifest.error) {
487
+ console.error(`❌ ${manifest.error}`);
488
+ process.exit(1);
489
+ }
490
+
491
+ // Determine what to validate
492
+ let filesToCheck = [];
493
+
494
+ if (fileIdx !== -1 && args[fileIdx + 1]) {
495
+ filesToCheck.push(path.resolve(args[fileIdx + 1]));
496
+ } else if (fixMode && args[0] && !args[0].startsWith("--")) {
497
+ filesToCheck.push(path.resolve(args[0]));
498
+ } else if (scanMode) {
499
+ // Scan all .agent/ markdown files
500
+ const agentDir = path.join(projectRoot, ".agent");
501
+ filesToCheck = walkAgentFiles(agentDir);
502
+ } else {
503
+ console.log("Usage:");
504
+ console.log(
505
+ " node .agent/scripts/guardrail_engine.js --file <path> Validate a file",
506
+ );
507
+ console.log(
508
+ " node .agent/scripts/guardrail_engine.js --scan Scan all .agent/ files",
509
+ );
510
+ console.log(
511
+ " node .agent/scripts/guardrail_engine.js --fix <path> Auto-fix a file",
512
+ );
513
+ process.exit(0);
514
+ }
515
+
516
+ // Run validation
517
+ let totalViolations = 0;
518
+ let totalCritical = 0;
519
+ const allResults = [];
520
+
521
+ for (const file of filesToCheck) {
522
+ if (!fs.existsSync(file)) {
523
+ console.error(`⚠️ File not found: ${file}`);
524
+ continue;
525
+ }
526
+
527
+ const content = fs.readFileSync(file, "utf8");
528
+ const result = validate(content, manifest, {
529
+ autoFix: fixMode,
530
+ context: { projectRoot },
531
+ });
532
+
533
+ totalViolations += result.violations.length;
534
+ totalCritical += result.violations.filter(
535
+ (v) => v.severity === "critical",
536
+ ).length;
537
+
538
+ if (result.violations.length > 0) {
539
+ allResults.push({
540
+ file: path.relative(projectRoot, file),
541
+ ...result,
542
+ });
543
+ }
544
+
545
+ // Apply auto-fix
546
+ if (fixMode && result.autoFixedContent && result.autoFixedContent !== content) {
547
+ fs.writeFileSync(file, result.autoFixedContent, "utf8");
548
+ console.log(
549
+ ` ✅ Auto-fixed: ${path.relative(projectRoot, file)}`,
550
+ );
551
+ }
552
+ }
553
+
554
+ if (jsonMode) {
555
+ console.log(JSON.stringify(allResults, null, 2));
556
+ return;
557
+ }
558
+
559
+ // Human-readable output
560
+ console.log(`\n🛡️ Guardrail Engine — Validation Report`);
561
+ console.log(` ─────────────────────────────────`);
562
+ console.log(` Files scanned: ${filesToCheck.length}`);
563
+ console.log(` Violations: ${totalViolations}`);
564
+ console.log(` Critical: ${totalCritical}`);
565
+ console.log(` ─────────────────────────────────`);
566
+
567
+ for (const result of allResults) {
568
+ console.log(`\n 📄 ${result.file} — ${result.summary}`);
569
+ for (const v of result.violations) {
570
+ const icon =
571
+ v.severity === "critical"
572
+ ? "🔴"
573
+ : v.severity === "warning"
574
+ ? "🟡"
575
+ : "🔵";
576
+ console.log(` ${icon} [${v.rule}] ${v.message}`);
577
+ if (v.suggestion) {
578
+ console.log(` 💡 ${v.suggestion}`);
579
+ }
580
+ }
581
+ }
582
+
583
+ if (totalViolations === 0) {
584
+ console.log(`\n ✅ All files clean. No violations detected.`);
585
+ }
586
+
587
+ console.log();
588
+ process.exit(totalCritical > 0 ? 1 : 0);
589
+ }
590
+
591
+ // ── Helpers ───────────────────────────────────────────────────────────────────
592
+
593
+ function walkAgentFiles(dir, fileList = []) {
594
+ if (!fs.existsSync(dir)) return fileList;
595
+
596
+ try {
597
+ const entries = fs.readdirSync(dir);
598
+ for (const entry of entries) {
599
+ const fullPath = path.join(dir, entry);
600
+ if (
601
+ ["node_modules", ".git", "history", ".backups", ".shared"].includes(
602
+ entry,
603
+ )
604
+ ) {
605
+ continue;
606
+ }
607
+
608
+ try {
609
+ const stat = fs.statSync(fullPath);
610
+ if (stat.isDirectory()) {
611
+ walkAgentFiles(fullPath, fileList);
612
+ } else if (entry.endsWith(".md") || entry.endsWith(".json") || entry.endsWith(".js") || entry.endsWith(".py")) {
613
+ fileList.push(fullPath);
614
+ }
615
+ } catch {
616
+ // Skip files we can't stat
617
+ }
618
+ }
619
+ } catch {
620
+ // Skip dirs we can't read
621
+ }
622
+
623
+ return fileList;
624
+ }
625
+
626
+ // ── Exports ───────────────────────────────────────────────────────────────────
627
+
628
+ module.exports = {
629
+ validate,
630
+ RULES,
631
+ ruleFileExists,
632
+ ruleScriptExtension,
633
+ ruleReviewerCount,
634
+ ruleSkillExists,
635
+ ruleAgentExists,
636
+ ruleUnresolvedVerify,
637
+ ruleNumericConsistency,
638
+ ruleImportPhantom,
639
+ };
640
+
641
+ if (require.main === module) {
642
+ main();
643
+ }