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.
- package/.agent/ARCHITECTURE.md +12 -12
- package/.agent/agents/complexity-reviewer.md +53 -0
- package/.agent/agents/precedence-reviewer.md +12 -12
- package/.agent/agents/swarm-worker-registry.md +3 -3
- package/.agent/history/integrity_manifest.json +958 -0
- package/.agent/history/memory/.memory.idx +305 -1
- package/.agent/history/memory/MEMORY.md +21 -1
- package/.agent/rules/GEMINI.md +31 -28
- package/.agent/scripts/case_law_manager.js +3 -15
- package/.agent/scripts/context_broker.js +3 -1
- package/.agent/scripts/guardrail_engine.js +643 -0
- package/.agent/scripts/integrity_manifest.js +486 -0
- package/.agent/scripts/signal_detector.js +1 -1
- package/.agent/scripts/skill_evolution.js +3 -15
- package/.agent/scripts/skill_integrator.js +2 -12
- package/.agent/scripts/swarm_dispatcher.js +2 -12
- package/.agent/skills/12-principles-of-animation/SKILL.md +69 -0
- package/.agent/skills/60fps-animation/SKILL.md +78 -0
- package/.agent/skills/accessible-animation/SKILL.md +86 -0
- package/.agent/skills/adapt/SKILL.md +75 -0
- package/.agent/skills/animation-on-scroll/SKILL.md +110 -0
- package/.agent/skills/animation-systems/SKILL.md +69 -0
- package/.agent/skills/antfu-conventions/SKILL.md +74 -0
- package/.agent/skills/app-builder/SKILL.md +1 -1
- package/.agent/skills/apple-design/SKILL.md +108 -0
- package/.agent/skills/audit-and-fix/SKILL.md +83 -0
- package/.agent/skills/baseline-ui/SKILL.md +74 -0
- package/.agent/skills/better-colors/SKILL.md +124 -0
- package/.agent/skills/better-typography/SKILL.md +111 -0
- package/.agent/skills/better-ui/SKILL.md +102 -0
- package/.agent/skills/bolder/SKILL.md +68 -0
- package/.agent/skills/build-primitive/SKILL.md +70 -0
- package/.agent/skills/clarify/SKILL.md +75 -0
- package/.agent/skills/cobejs/SKILL.md +96 -0
- package/.agent/skills/codebase-design/SKILL.md +84 -0
- package/.agent/skills/colorize/SKILL.md +77 -0
- package/.agent/skills/compact-landing/SKILL.md +70 -0
- package/.agent/skills/company-logos/SKILL.md +73 -0
- package/.agent/skills/create-design-md/SKILL.md +104 -0
- package/.agent/skills/critique/SKILL.md +95 -0
- package/.agent/skills/delight/SKILL.md +65 -0
- package/.agent/skills/design-lab/SKILL.md +59 -0
- package/.agent/skills/diagnosing-bugs/SKILL.md +190 -0
- package/.agent/skills/distill/SKILL.md +65 -0
- package/.agent/skills/domain-modeling/SKILL.md +78 -0
- package/.agent/skills/fixing-accessibility/SKILL.md +98 -0
- package/.agent/skills/fixing-metadata/SKILL.md +97 -0
- package/.agent/skills/fixing-motion-performance/SKILL.md +96 -0
- package/.agent/skills/gpt-taste/SKILL.md +68 -0
- package/.agent/skills/harden/SKILL.md +74 -0
- package/.agent/skills/impeccable/SKILL.md +72 -0
- package/.agent/skills/improve-codebase-architecture/SKILL.md +67 -0
- package/.agent/skills/improve-ui/SKILL.md +83 -0
- package/.agent/skills/landing-page/SKILL.md +88 -0
- package/.agent/skills/lint-and-validate/SKILL.md +1 -2
- package/.agent/skills/local-first-architecture/SKILL.md +66 -0
- package/.agent/skills/lottie-animation/SKILL.md +82 -0
- package/.agent/skills/marquee-loop/SKILL.md +96 -0
- package/.agent/skills/masked-reveal/SKILL.md +88 -0
- package/.agent/skills/micro-interaction/SKILL.md +100 -0
- package/.agent/skills/morphing-icons/SKILL.md +85 -0
- package/.agent/skills/page-transition-animation/SKILL.md +108 -0
- package/.agent/skills/parallel-agents/SKILL.md +5 -5
- package/.agent/skills/polish/SKILL.md +63 -0
- package/.agent/skills/pricing-page/SKILL.md +68 -0
- package/.agent/skills/progressive-blur/SKILL.md +100 -0
- package/.agent/skills/project-idioms/SKILL.md +7 -7
- package/.agent/skills/quieter/SKILL.md +68 -0
- package/.agent/skills/react-doctor/SKILL.md +70 -0
- package/.agent/skills/redesign-skill/SKILL.md +65 -0
- package/.agent/skills/shape/SKILL.md +73 -0
- package/.agent/skills/soft-skill/SKILL.md +72 -0
- package/.agent/skills/sounds-on-the-web/SKILL.md +100 -0
- package/.agent/skills/svg-animation/SKILL.md +91 -0
- package/.agent/skills/swiss-design/SKILL.md +64 -0
- package/.agent/skills/taste-skill/SKILL.md +71 -0
- package/.agent/skills/tdd-workflow/SKILL.md +36 -165
- package/.agent/skills/test-result-analyzer/SKILL.md +1 -1
- package/.agent/skills/thermo-nuclear-code-quality-review/SKILL.md +82 -0
- package/.agent/skills/to-spring-or-not-to-spring/SKILL.md +66 -0
- package/.agent/skills/transitions-dev/SKILL.md +101 -0
- package/.agent/skills/typeset/SKILL.md +76 -0
- package/.agent/skills/ui-skills-root/SKILL.md +85 -0
- package/.agent/skills/web-quality-audit/SKILL.md +67 -0
- package/.agent/workflows/fix.md +2 -2
- package/.agent/workflows/preview.md +5 -5
- package/.agent/workflows/status.md +1 -1
- package/.agent/workflows/tribunal-full.md +3 -3
- package/.agent/workflows/tribunal-speed.md +2 -2
- package/README.md +348 -238
- package/bin/tribunal-kit.js +62 -1482
- package/dist/cli.js +13 -0
- package/dist/commands/align.js +1 -1
- package/dist/commands/compile.js +1 -1
- package/dist/commands/guardrail.js +275 -0
- package/dist/commands/init.js +10 -9
- package/dist/commands/optimize.js +318 -0
- package/dist/utils/version.js +1 -1
- package/package.json +10 -8
- package/scripts/fix-vbc.js +98 -0
- 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
|
+
}
|