@event4u/agent-config 7.3.0 → 7.5.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 (100) hide show
  1. package/.claude-plugin/marketplace.json +4 -1
  2. package/CHANGELOG.md +81 -0
  3. package/README.md +2 -2
  4. package/dist/agent-src/commands/fix/pr-comments.md +42 -13
  5. package/dist/agent-src/commands/review-changes.md +14 -7
  6. package/dist/agent-src/rules/artifact-engagement-recording.md +9 -0
  7. package/dist/agent-src/rules/cli-output-handling.md +18 -0
  8. package/dist/agent-src/rules/domain-safety-pii.md +4 -3
  9. package/dist/agent-src/rules/icon-consistency.md +1 -0
  10. package/dist/agent-src/rules/legal-safety-floor.md +2 -2
  11. package/dist/agent-src/rules/untrusted-input-defense.md +8 -5
  12. package/dist/agent-src/skills/adr-create/SKILL.md +5 -1
  13. package/dist/agent-src/skills/adversarial-review/SKILL.md +1 -1
  14. package/dist/agent-src/skills/blade-ui/SKILL.md +4 -0
  15. package/dist/agent-src/skills/brand-to-tokens/SKILL.md +2 -1
  16. package/dist/agent-src/skills/code-review/SKILL.md +4 -0
  17. package/dist/agent-src/skills/context-authoring/SKILL.md +1 -1
  18. package/dist/agent-src/skills/design-intelligence/SKILL.md +40 -0
  19. package/dist/agent-src/skills/design-review/SKILL.md +23 -11
  20. package/dist/agent-src/skills/design-system-capture/SKILL.md +41 -0
  21. package/dist/agent-src/skills/design-system-capture/reference/design-system-json.md +62 -0
  22. package/dist/agent-src/skills/design-tokens/SKILL.md +1 -0
  23. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +6 -0
  24. package/dist/agent-src/skills/flux/SKILL.md +4 -0
  25. package/dist/agent-src/skills/iconography/SKILL.md +1 -0
  26. package/dist/agent-src/skills/image-editing/evals/triggers.json +13 -13
  27. package/dist/agent-src/skills/judge-artifact-completeness/SKILL.md +136 -0
  28. package/dist/agent-src/skills/judge-artifact-completeness/calibration/fixtures.json +149 -0
  29. package/dist/agent-src/skills/judge-artifact-completeness/evals/triggers.json +17 -0
  30. package/dist/agent-src/skills/judge-artifact-completeness/rubrics/architecture-score.json +73 -0
  31. package/dist/agent-src/skills/judge-artifact-completeness/rubrics/pr-review-score.json +65 -0
  32. package/dist/agent-src/skills/judge-artifact-completeness/rubrics/roadmap-score.json +73 -0
  33. package/dist/agent-src/skills/judge-artifact-completeness/rubrics/ticket-quality-score.json +73 -0
  34. package/dist/agent-src/skills/judge-injection-defense/SKILL.md +99 -0
  35. package/dist/agent-src/skills/judge-injection-defense/calibration/fixtures.json +24 -0
  36. package/dist/agent-src/skills/judge-injection-defense/evals/triggers.json +17 -0
  37. package/dist/agent-src/skills/judge-injection-defense/fixtures/behavioral-fixtures.json +63 -0
  38. package/dist/agent-src/skills/judge-injection-defense/fixtures/coverage-report.md +53 -0
  39. package/dist/agent-src/skills/judge-injection-defense/fixtures/perturbation-taxonomy.json +110 -0
  40. package/dist/agent-src/skills/judge-synthesis/SKILL.md +134 -0
  41. package/dist/agent-src/skills/judge-synthesis/evals/triggers.json +17 -0
  42. package/dist/agent-src/skills/logo-generation/SKILL.md +1 -1
  43. package/dist/agent-src/skills/markitdown/SKILL.md +1 -1
  44. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +18 -0
  45. package/dist/agent-src/skills/react-shadcn-ui/reference/registry.md +59 -0
  46. package/dist/agent-src/skills/refine-ticket/SKILL.md +5 -1
  47. package/dist/agent-src/skills/release-comms/SKILL.md +1 -1
  48. package/dist/agent-src/skills/roadmap-management/SKILL.md +4 -0
  49. package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
  50. package/dist/agent-src/skills/script-writing/SKILL.md +1 -1
  51. package/dist/agent-src/skills/sql-writing/SKILL.md +1 -1
  52. package/dist/agent-src/skills/systematic-debugging/SKILL.md +1 -1
  53. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +4 -0
  54. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +1 -1
  55. package/dist/agent-src/skills/token-optimizer/SKILL.md +1 -1
  56. package/dist/agent-src/skills/typography-system/SKILL.md +2 -1
  57. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  58. package/dist/cli/registry.js +1 -0
  59. package/dist/cli/registry.js.map +1 -1
  60. package/dist/discovery/deprecation-report.md +1 -1
  61. package/dist/discovery/discovery-manifest.json +109 -43
  62. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  63. package/dist/discovery/discovery-manifest.summary.md +3 -3
  64. package/dist/discovery/orphan-report.md +1 -1
  65. package/dist/discovery/packs.json +8 -5
  66. package/dist/discovery/trust-report.md +2 -2
  67. package/dist/discovery/workspaces.json +7 -4
  68. package/dist/mcp/registry-manifest.json +3 -3
  69. package/dist/router.json +1 -1
  70. package/dist/server/schemas/settings.js +6 -0
  71. package/dist/server/schemas/settings.js.map +1 -1
  72. package/docs/architecture.md +2 -2
  73. package/docs/catalog.md +28 -20
  74. package/docs/command-flows.md +1 -1
  75. package/docs/enforcement-by-host.md +9 -0
  76. package/docs/governance-advantage.md +72 -0
  77. package/docs/guidelines/design-antipatterns.md +26 -0
  78. package/docs/guidelines/design-canon.md +62 -0
  79. package/package.json +2 -1
  80. package/src/config/agent-settings.template.yml +21 -0
  81. package/src/scripts/_cli/cmd_analyze_session.ts +347 -0
  82. package/src/scripts/_dispatch.bash +8 -0
  83. package/src/scripts/_lib/token_count.ts +110 -35
  84. package/src/scripts/audit_initial_context.ts +37 -12
  85. package/src/scripts/bench_quality_run.ts +253 -0
  86. package/src/scripts/bench_rule_load_latency.ts +178 -0
  87. package/src/scripts/check_kernel_prefix_stability.ts +0 -0
  88. package/src/scripts/check_quality_regression.ts +244 -0
  89. package/src/scripts/check_token_quality_golden.ts +253 -0
  90. package/src/scripts/check_token_regression.ts +266 -0
  91. package/src/scripts/design_slop_rules.test.ts +193 -0
  92. package/src/scripts/design_slop_rules.ts +502 -0
  93. package/src/scripts/external_sources_denylist.json +7 -1
  94. package/src/scripts/hook_manifest.yaml +26 -3
  95. package/src/scripts/hooks/design_slop_hook.ts +181 -0
  96. package/src/scripts/hooks/rtk_wrap_hook.ts +228 -0
  97. package/src/scripts/lint_agent_security.ts +1 -0
  98. package/src/scripts/lint_confusables.ts +197 -0
  99. package/src/scripts/lint_design_slop.ts +245 -0
  100. package/src/scripts/probe_host_compliance.ts +145 -0
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lint_design_slop — deterministic anti-slop DETECTOR for consumer projects.
4
+ *
5
+ * The aesthetic-PROVENANCE layer that `lint_design_quality` deliberately omits.
6
+ * It scans HTML/CSS/JSX/Markdown for the "AI tells" catalogued in
7
+ * `docs/guidelines/design-antipatterns.md` and emits structured findings
8
+ * (rule-id + catalog-id + severity P0–P3 + file:line). The rule registry lives
9
+ * in `design_slop_rules.ts`.
10
+ *
11
+ * FLAGS, NEVER A HARD BLOCK. Default exit is 0 regardless of findings — slop
12
+ * tells are rebuttable presumptions, not failures. CI opts into failing with
13
+ * `--fail-on <P0|P1|P2|P3>` (fails when any finding at or above that severity
14
+ * remains). This honours the council 2026-06-28 "flags not blocks" verdict.
15
+ *
16
+ * Zero runtime token cost: this runs in Node (CI / pre-commit / optional hook),
17
+ * never in the model's context. `design-review` cites the structured `--json`
18
+ * output instead of reloading the 12 KB prose catalog.
19
+ *
20
+ * Dependency-free pattern analysis (no postcss/@babel) — see the design note in
21
+ * design_slop_rules.ts. Same posture as lint_design_quality.
22
+ *
23
+ * Usage:
24
+ * npx tsx lint_design_slop.ts --dir src/ [--json] [--fail-on P1] [--quiet]
25
+ *
26
+ * Context gate: a consumer DESIGN.md (project root or scan dir) that declares a
27
+ * pattern as intentional suppresses the matching rule (e.g. "warm-neutral" →
28
+ * C5 cream-palette is not flagged). Inline-ignore (eslint-style):
29
+ * design-slop-disable <rule-id> (file scope, top of file)
30
+ * design-slop-disable-next-line <rule-id>
31
+ * design-slop-disable-line <rule-id>
32
+ * Per-project config (.design-quality.json, shared with lint_design_quality):
33
+ * { "ignoreRules": ["slop-c5-cream-palette"], "ignoreFiles": ["src/legacy/**"] }
34
+ *
35
+ * Exit codes: 0 = clean OR findings-below-threshold, 2 = findings at/above
36
+ * --fail-on threshold, 1 = internal error.
37
+ */
38
+ import fs from "node:fs";
39
+ import path from "node:path";
40
+ import {
41
+ SLOP_RULES,
42
+ type DesignContext,
43
+ type Engine,
44
+ type Severity,
45
+ } from "./design_slop_rules.js";
46
+
47
+ interface Finding {
48
+ rule: string;
49
+ catalogId: string;
50
+ severity: Severity;
51
+ file: string;
52
+ line: number;
53
+ snippet: string;
54
+ message: string;
55
+ }
56
+
57
+ interface ProjectConfig {
58
+ ignoreRules?: string[];
59
+ ignoreFiles?: string[];
60
+ }
61
+
62
+ const SEVERITY_ORDER: Severity[] = ["P3", "P2", "P1", "P0"];
63
+ const sevRank = (s: Severity): number => SEVERITY_ORDER.indexOf(s);
64
+
65
+ // ext → which engine-classes a file of that type can contain
66
+ function enginesForExt(ext: string): Set<Engine> {
67
+ if (/\.(css|scss|sass|less)$/i.test(ext)) return new Set<Engine>(["css"]);
68
+ if (/\.(html|htm|vue|svelte|astro)$/i.test(ext)) return new Set<Engine>(["css", "html", "copy"]);
69
+ if (/\.(jsx|tsx)$/i.test(ext)) return new Set<Engine>(["css", "jsx", "copy"]);
70
+ if (/\.(md|mdx)$/i.test(ext)) return new Set<Engine>(["copy"]);
71
+ return new Set<Engine>();
72
+ }
73
+
74
+ function* walkDir(dir: string, ext: RegExp): Generator<string> {
75
+ if (!fs.existsSync(dir)) return;
76
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
77
+ const full = path.join(dir, entry.name);
78
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
79
+ if (entry.isDirectory()) yield* walkDir(full, ext);
80
+ else if (ext.test(entry.name)) yield full;
81
+ }
82
+ }
83
+
84
+ function loadConfig(dir: string): ProjectConfig {
85
+ const p = path.join(dir, ".design-quality.json");
86
+ if (!fs.existsSync(p)) return {};
87
+ try {
88
+ return JSON.parse(fs.readFileSync(p, "utf-8")) as ProjectConfig;
89
+ } catch {
90
+ return {};
91
+ }
92
+ }
93
+
94
+ /** Read DESIGN.md (scan dir, then project root) as the context-gate source. */
95
+ export function loadDesignContext(scanDir: string): DesignContext {
96
+ const candidates = [
97
+ path.join(scanDir, "DESIGN.md"),
98
+ path.join(process.cwd(), "DESIGN.md"),
99
+ path.join(scanDir, "..", "DESIGN.md"),
100
+ ];
101
+ let raw = "";
102
+ for (const c of candidates) {
103
+ if (fs.existsSync(c)) {
104
+ raw = fs.readFileSync(c, "utf-8").toLowerCase();
105
+ break;
106
+ }
107
+ }
108
+ return {
109
+ raw,
110
+ has: (...keywords: string[]) => keywords.some((k) => raw.includes(k.toLowerCase())),
111
+ };
112
+ }
113
+
114
+ function parseIgnores(lines: string[]): Map<string, Set<number>> {
115
+ const ignores = new Map<string, Set<number>>();
116
+ const add = (rule: string, line: number) => {
117
+ const set = ignores.get(rule) ?? new Set<number>();
118
+ set.add(line);
119
+ ignores.set(rule, set);
120
+ };
121
+ for (let i = 0; i < lines.length; i++) {
122
+ const line = lines[i] ?? "";
123
+ const file = /design-slop-disable\s+([\w-]+)/i.exec(line);
124
+ if (file && file[1] && !/-line|-next-line/.test(line)) {
125
+ ignores.set(file[1].toLowerCase(), new Set([...Array(99999).keys()].map((n) => n + 1)));
126
+ }
127
+ const next = /design-slop-disable-next-line\s+([\w-]+)/i.exec(line);
128
+ if (next && next[1]) add(next[1].toLowerCase(), i + 2);
129
+ const cur = /design-slop-disable-line\s+([\w-]+)/i.exec(line);
130
+ if (cur && cur[1]) add(cur[1].toLowerCase(), i + 1);
131
+ }
132
+ return ignores;
133
+ }
134
+
135
+ export function scanFile(
136
+ content: string,
137
+ relPath: string,
138
+ ctx: DesignContext,
139
+ ): Finding[] {
140
+ const lines = content.split("\n");
141
+ const ext = path.extname(relPath).toLowerCase();
142
+ const fileEngines = enginesForExt(ext);
143
+ if (fileEngines.size === 0) return [];
144
+ const ignores = parseIgnores(lines);
145
+
146
+ const out: Finding[] = [];
147
+ for (const rule of SLOP_RULES) {
148
+ if (!rule.engines.some((e) => fileEngines.has(e))) continue;
149
+ if (rule.gated && rule.gated(ctx)) continue; // DESIGN.md declares intent → suppress
150
+ let hits;
151
+ try {
152
+ hits = rule.detect({ content, lines, ext, ctx });
153
+ } catch {
154
+ continue; // a single rule throwing never kills the scan
155
+ }
156
+ for (const h of hits) {
157
+ const ruleIgnore = ignores.get(rule.id.toLowerCase());
158
+ if (ruleIgnore && ruleIgnore.has(h.line)) continue;
159
+ out.push({
160
+ rule: rule.id,
161
+ catalogId: rule.catalogId,
162
+ severity: rule.severity,
163
+ file: relPath,
164
+ line: h.line,
165
+ snippet: h.snippet,
166
+ message: rule.message,
167
+ });
168
+ }
169
+ }
170
+ return out;
171
+ }
172
+
173
+ function main(): void {
174
+ const args = process.argv.slice(2);
175
+ const jsonMode = args.includes("--json");
176
+ const quiet = args.includes("--quiet");
177
+ const dirIdx = args.indexOf("--dir");
178
+ const scanDir = dirIdx >= 0 && args[dirIdx + 1] ? path.resolve(args[dirIdx + 1]!) : path.resolve(process.cwd(), "src");
179
+ const failIdx = args.indexOf("--fail-on");
180
+ const failOn = (failIdx >= 0 ? args[failIdx + 1] : undefined) as Severity | undefined;
181
+
182
+ if (!fs.existsSync(scanDir)) {
183
+ if (!quiet) {
184
+ process.stderr.write(`[lint_design_slop] scan directory not found: ${scanDir}\n`);
185
+ process.stderr.write("Opt-in for consumer projects. Run with --dir <path> at your UI source.\n");
186
+ }
187
+ process.exit(0);
188
+ }
189
+
190
+ const ctx = loadDesignContext(scanDir);
191
+ const config = loadConfig(process.cwd());
192
+ const ignoreRules = new Set((config.ignoreRules ?? []).map((r) => r.toLowerCase()));
193
+ const ignoreGlobs = config.ignoreFiles ?? [];
194
+
195
+ const files: string[] = [];
196
+ for (const f of walkDir(scanDir, /\.(html|htm|css|scss|sass|less|vue|svelte|astro|jsx|tsx|md|mdx)$/i)) {
197
+ if (!ignoreGlobs.some((g) => f.includes(g.replace("**", "")))) files.push(f);
198
+ }
199
+
200
+ const findings: Finding[] = [];
201
+ for (const fp of files) {
202
+ const rel = path.relative(process.cwd(), fp);
203
+ try {
204
+ const fileFindings = scanFile(fs.readFileSync(fp, "utf-8"), rel, ctx).filter(
205
+ (f) => !ignoreRules.has(f.rule.toLowerCase()),
206
+ );
207
+ findings.push(...fileFindings);
208
+ } catch (err) {
209
+ if (!quiet) process.stderr.write(`[lint_design_slop] error: ${fp}: ${err}\n`);
210
+ }
211
+ }
212
+
213
+ findings.sort((a, b) => sevRank(b.severity) - sevRank(a.severity));
214
+
215
+ const overThreshold = failOn
216
+ ? findings.filter((f) => sevRank(f.severity) >= sevRank(failOn))
217
+ : [];
218
+
219
+ if (jsonMode) {
220
+ process.stdout.write(JSON.stringify({ findings, failOn: failOn ?? null, overThreshold: overThreshold.length }, null, 2) + "\n");
221
+ process.exit(overThreshold.length > 0 ? 2 : 0);
222
+ }
223
+
224
+ if (findings.length === 0) {
225
+ if (!quiet) process.stdout.write(`✅ lint_design_slop: no anti-slop tells in ${files.length} file(s).\n`);
226
+ process.exit(0);
227
+ }
228
+
229
+ for (const f of findings) {
230
+ const icon = f.severity === "P0" || f.severity === "P1" ? "⚠️ " : "•";
231
+ process.stdout.write(`${icon} [${f.severity} ${f.catalogId}] ${f.file}:${f.line} (${f.rule}) — ${f.message}\n ${f.snippet}\n`);
232
+ }
233
+ process.stdout.write(
234
+ `\nlint_design_slop: ${findings.length} tell(s) flagged (rebuttable — declare intent in DESIGN.md, ` +
235
+ `design-slop-disable-next-line <rule>, or .design-quality.json).` +
236
+ (failOn ? ` ${overThreshold.length} at/above ${failOn}.` : " Flags only; no CI failure (pass --fail-on to gate).") +
237
+ "\n",
238
+ );
239
+ process.exit(overThreshold.length > 0 ? 2 : 0);
240
+ }
241
+
242
+ // Run only when invoked directly (not when imported by the test guard).
243
+ const invokedDirectly =
244
+ process.argv[1] !== undefined && /lint_design_slop\.ts$/.test(process.argv[1]);
245
+ if (invokedDirectly) main();
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Host-compliance probe for thin projection (token-saving Phase 0).
4
+ *
5
+ * Thin projection is a CONTRACT WITH THE HOST: a non-kernel rule body is
6
+ * demoted to a router pointer, and the host is trusted to load the body only
7
+ * on trigger-match. On a host that reconstructs rule bodies internally (e.g. a
8
+ * plugin loader that re-inlines everything), thin projection is a silent no-op
9
+ * — so it must be falsified per host before the thin flip ships.
10
+ *
11
+ * This probe has two halves:
12
+ *
13
+ * MECHANICAL (here, automated): run the real thin projector (`thin_entry`)
14
+ * on a canary rule fixture and assert the body is replaced by a pointer that
15
+ * still carries the trigger hint (so the router can select it). This proves
16
+ * the projector demotes correctly — the prerequisite for any host test.
17
+ *
18
+ * LIVE (operator gate): install the thin-projected canary into each host,
19
+ * invoke its keyword, and confirm the host shows the POINTER, not the body.
20
+ * This is a human-run step (live trigger-eval is a human gate) — the probe
21
+ * prints the checklist + a results table to fill in.
22
+ *
23
+ * CLI:
24
+ * ./scripts-run src/scripts/probe_host_compliance # mechanical check + checklist
25
+ * ./scripts-run src/scripts/probe_host_compliance --json
26
+ *
27
+ * Exit codes: 0 demotion verified · 1 file error · 2 projector failed to demote.
28
+ */
29
+ import * as fs from 'node:fs';
30
+ import * as path from 'node:path';
31
+ import process from 'node:process';
32
+ import { fileURLToPath, pathToFileURL } from 'node:url';
33
+
34
+ import { thin_entry } from './project_thin_rules.js';
35
+
36
+ const _HERE = fileURLToPath(import.meta.url);
37
+ const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
38
+ const CANARY = path.join(
39
+ REPO_ROOT,
40
+ 'tests/fixtures/host-compliance/host-compliance-canary.md',
41
+ );
42
+ const CANARY_ID = 'host-compliance-canary';
43
+ const SENTINEL = 'CANARY_BODY_SENTINEL_DO_NOT_INLINE';
44
+ const KEYWORD = 'xyzzy-canary-probe';
45
+ const POINTER_MARKER = 'Routed rule — load the body on trigger-match';
46
+
47
+ // The supported hosts whose live thin-compliance must be falsified.
48
+ const HOSTS = ['claude-code', 'cursor', 'augment'];
49
+
50
+ export interface DemotionResult {
51
+ ok: boolean;
52
+ pointer_present: boolean;
53
+ body_removed: boolean;
54
+ trigger_hint_preserved: boolean;
55
+ link_present: boolean;
56
+ }
57
+
58
+ /** Assert a thinned canary is a valid pointer (body gone, still selectable). */
59
+ export function evaluate_demotion(
60
+ thinned: string,
61
+ opts: { sentinel: string; keyword: string } = { sentinel: SENTINEL, keyword: KEYWORD },
62
+ ): DemotionResult {
63
+ const pointer_present = thinned.includes(POINTER_MARKER);
64
+ const body_removed = !thinned.includes(opts.sentinel);
65
+ const trigger_hint_preserved = thinned.includes(opts.keyword);
66
+ const link_present = /Body: \[`[^`]+`\]\(/.test(thinned);
67
+ return {
68
+ ok: pointer_present && body_removed && trigger_hint_preserved && link_present,
69
+ pointer_present,
70
+ body_removed,
71
+ trigger_hint_preserved,
72
+ link_present,
73
+ };
74
+ }
75
+
76
+ function operator_checklist(): string {
77
+ const rows = HOSTS.map(
78
+ (h) => ` - [ ] ${h}: fires on \`${KEYWORD}\` AND shows the pointer, NOT the body sentinel`,
79
+ ).join('\n');
80
+ return (
81
+ 'LIVE host-compliance step (operator gate — live trigger-eval is human-run):\n' +
82
+ ' 1. Set `lean_projection.mode: thin` and run `task generate-tools && task sync`.\n' +
83
+ ' 2. Install the projected canary into each host.\n' +
84
+ ` 3. Invoke the keyword \`${KEYWORD}\` in a session on each host.\n` +
85
+ ' 4. PASS = the rule fires AND the host surfaces the router pointer;\n' +
86
+ ` FAIL = the host surfaces the body sentinel (\`${SENTINEL}\`) → thin\n` +
87
+ ' projection is a no-op there; escalate to the host vendor.\n' +
88
+ 'Results:\n' +
89
+ rows +
90
+ '\n'
91
+ );
92
+ }
93
+
94
+ function main(argv: string[]): number {
95
+ const asJson = argv.includes('--json');
96
+ for (const a of argv) {
97
+ if (!['--json', '-h', '--help'].includes(a)) {
98
+ process.stderr.write(`error: unknown argument: ${a}\n`);
99
+ return 1;
100
+ }
101
+ if (a === '-h' || a === '--help') {
102
+ process.stdout.write('usage: probe_host_compliance [--json]\n');
103
+ return 0;
104
+ }
105
+ }
106
+
107
+ let text: string;
108
+ try {
109
+ text = fs.readFileSync(CANARY, 'utf-8');
110
+ } catch {
111
+ process.stderr.write(`error: cannot read canary fixture ${path.relative(REPO_ROOT, CANARY)}\n`);
112
+ return 1;
113
+ }
114
+ const thinned = thin_entry(CANARY_ID, text);
115
+ const result = evaluate_demotion(thinned);
116
+
117
+ if (asJson) {
118
+ process.stdout.write(
119
+ JSON.stringify({ ...result, hosts: HOSTS, keyword: KEYWORD, sentinel: SENTINEL }, null, 2) + '\n',
120
+ );
121
+ return result.ok ? 0 : 2;
122
+ }
123
+
124
+ process.stdout.write(
125
+ `${result.ok ? '✅' : '❌'} mechanical demotion: ` +
126
+ `pointer=${result.pointer_present} body-removed=${result.body_removed} ` +
127
+ `hint=${result.trigger_hint_preserved} link=${result.link_present}\n`,
128
+ );
129
+ if (!result.ok) {
130
+ process.stdout.write('❌ the thin projector did NOT demote the canary correctly.\n');
131
+ return 2;
132
+ }
133
+ process.stdout.write('\n' + operator_checklist());
134
+ return 0;
135
+ }
136
+
137
+ const _IS_MAIN =
138
+ process.argv[1] !== undefined &&
139
+ import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href;
140
+
141
+ if (_IS_MAIN) {
142
+ process.exit(main(process.argv.slice(2)));
143
+ }
144
+
145
+ export { main };