@event4u/agent-config 9.16.0 → 9.18.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +80 -130
- package/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +100 -48
- package/dist/agent-src/commands/challenge-me/vision.md +5 -0
- package/dist/agent-src/commands/commit.md +1 -1
- package/dist/agent-src/commands/condense.md +19 -64
- package/dist/agent-src/commands/evals/agent-handoff.json +13 -1
- package/dist/agent-src/commands/evals/rule-compliance-audit.json +46 -0
- package/dist/agent-src/commands/feature/plan.md +20 -0
- package/dist/agent-src/commands/feature/roadmap.md +19 -0
- package/dist/agent-src/commands/memory/promote.md +14 -1
- package/dist/agent-src/commands/mode.md +9 -5
- package/dist/agent-src/commands/optimize/deep.md +72 -11
- package/dist/agent-src/commands/pr/create.md +29 -0
- package/dist/agent-src/commands/roadmap/create.md +29 -0
- package/dist/agent-src/commands/rule-compliance-audit.md +28 -5
- package/dist/agent-src/contexts/authority/commit-mechanics.md +5 -5
- package/dist/agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +1 -0
- package/dist/agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +1 -1
- package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +6 -1
- package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +4 -1
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +27 -1
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +133 -0
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +5 -2
- package/dist/agent-src/profiles/agency.yml +3 -3
- package/dist/agent-src/profiles/content_creator.yml +2 -2
- package/dist/agent-src/profiles/developer.yml +2 -2
- package/dist/agent-src/profiles/finance.yml +1 -1
- package/dist/agent-src/profiles/founder.yml +2 -2
- package/dist/agent-src/profiles/ops.yml +2 -2
- package/dist/agent-src/rules/active-remediation.md +2 -0
- package/dist/agent-src/rules/architecture.md +4 -2
- package/dist/agent-src/rules/augment-edit-discipline.md +2 -0
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/brand-consistency.md +10 -62
- package/dist/agent-src/rules/brand-source-of-truth.md +62 -11
- package/dist/agent-src/rules/broken-access-control.md +4 -0
- package/dist/agent-src/rules/cli-output-handling.md +5 -0
- package/dist/agent-src/rules/code-comment-discipline.md +3 -0
- package/dist/agent-src/rules/commit-conventions.md +2 -0
- package/dist/agent-src/rules/context-hygiene.md +3 -2
- package/dist/agent-src/rules/cross-source-consistency.md +3 -0
- package/dist/agent-src/rules/design-fidelity.md +2 -0
- package/dist/agent-src/rules/doc-screenshot-hygiene.md +2 -0
- package/dist/agent-src/rules/docker-commands.md +4 -0
- package/dist/agent-src/rules/domain-adoption-policy.md +2 -0
- package/dist/agent-src/rules/domain-safety-disclaimer.md +5 -2
- package/dist/agent-src/rules/domain-safety-pii.md +1 -1
- package/dist/agent-src/rules/engineering-safety-floor.md +2 -0
- package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +9 -0
- package/dist/agent-src/rules/image-likeness-and-rights.md +4 -0
- package/dist/agent-src/rules/improve-before-implement.md +4 -0
- package/dist/agent-src/rules/laravel-routing.md +4 -0
- package/dist/agent-src/rules/legal-safety-floor.md +3 -0
- package/dist/agent-src/rules/lethal-trifecta-guard.md +3 -0
- package/dist/agent-src/rules/media-governance-routing.md +5 -0
- package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
- package/dist/agent-src/rules/no-roadmap-references.md +2 -0
- package/dist/agent-src/rules/notes-first-reasoning.md +2 -0
- package/dist/agent-src/rules/output-discipline.md +9 -2
- package/dist/agent-src/rules/php-coding.md +2 -0
- package/dist/agent-src/rules/prefer-enums-over-literals.md +3 -0
- package/dist/agent-src/rules/provider-lifecycle-discipline.md +5 -0
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +2 -0
- package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
- package/dist/agent-src/rules/rule-type-governance.md +2 -0
- package/dist/agent-src/rules/scale-discipline.md +3 -0
- package/dist/agent-src/rules/secret-vcs-guard.md +2 -0
- package/dist/agent-src/rules/security-sensitive-stop.md +9 -2
- package/dist/agent-src/rules/senior-engineering-discipline.md +6 -0
- package/dist/agent-src/rules/skill-quality.md +2 -0
- package/dist/agent-src/rules/source-confidentiality.md +4 -0
- package/dist/agent-src/rules/source-discovery-gate.md +3 -0
- package/dist/agent-src/rules/source-of-truth.md +2 -0
- package/dist/agent-src/rules/strategy-safety-floor.md +2 -0
- package/dist/agent-src/rules/think-before-action.md +5 -0
- package/dist/agent-src/rules/token-budget-discipline.md +2 -2
- package/dist/agent-src/rules/ui-audit-gate.md +2 -0
- package/dist/agent-src/rules/user-interaction.md +3 -4
- package/dist/agent-src/skills/build-buy-partner/SKILL.md +1 -1
- package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +4 -4
- package/dist/agent-src/skills/copilot-config/SKILL.md +1 -1
- package/dist/agent-src/skills/deep-reading-analyst/SKILL.md +7 -4
- package/dist/agent-src/skills/description-assist/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/SKILL.md +6 -180
- package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +100 -0
- package/dist/agent-src/skills/design-intelligence/references/integration-mapping.md +91 -0
- package/dist/agent-src/skills/design-review/SKILL.md +4 -141
- package/dist/agent-src/skills/design-review/references/review-communication.md +92 -0
- package/dist/agent-src/skills/design-review/references/verification-automation.md +61 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +8 -27
- package/dist/agent-src/skills/existing-ui-audit/references/anti-slop-cross-reference.md +31 -0
- package/dist/agent-src/skills/fe-design/SKILL.md +4 -289
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +227 -0
- package/dist/agent-src/skills/fe-design/references/design-read-and-memory.md +75 -0
- package/dist/agent-src/skills/market-entry-analysis/SKILL.md +1 -1
- package/dist/agent-src/skills/persona-writing/SKILL.md +2 -2
- package/dist/agent-src/skills/readme-reviewer/SKILL.md +2 -1
- package/dist/agent-src/skills/roadmap-management/SKILL.md +10 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +12 -1
- package/dist/agent-src/skills/rule-writing/SKILL.md +4 -2
- package/dist/agent-src/skills/skill-reviewer/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-writing/SKILL.md +2 -2
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +3 -1
- package/dist/agent-src/skills/token-optimizer/SKILL.md +2 -2
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +6 -3
- package/dist/agent-src/templates/agent-settings.md +13 -15
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +6 -2
- package/dist/agent-src/templates/persona.md +2 -2
- package/dist/agent-src/templates/roadmaps.md +13 -1
- package/dist/agent-src/templates/rule.md +1 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/_fix_lane.ts +52 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +8 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +6 -0
- package/dist/agent-src/templates/scripts/work_engine/intent/classify.ts +56 -4
- package/dist/agent-src/templates/skill.md +1 -1
- package/dist/cli/registry.js +3 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-AIIUU73N.js +2040 -0
- package/dist/cli-delegate/{chunk-ZXXJLL5K.js → chunk-BSLUHV2F.js} +127 -1487
- package/dist/cli-delegate/{chunk-H574KOUH.js → chunk-ELMFOQPW.js} +35 -1
- package/dist/cli-delegate/chunk-LF2KKJ4Q.js +183 -0
- package/dist/cli-delegate/chunk-VSYT7OHO.js +1382 -0
- package/dist/cli-delegate/chunk-XGA2NTU4.js +3727 -0
- package/dist/cli-delegate/cmd_conformance.js +4 -3
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +3 -2
- package/dist/cli-delegate/cmd_explain.js +36 -36
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +1169 -0
- package/dist/cli-delegate/cmd_memory_get.js +35 -3754
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_route_audit.js +249 -0
- package/dist/cli-delegate/cmd_route_explain.js +285 -0
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_update.js +3 -3
- package/dist/cli-delegate/cmd_upgrade.js +3 -3
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/cli-delegate/cmd_versions.js +3 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +84 -84
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +602 -507
- package/dist/install/install.mjs +34 -0
- package/dist/mcp/awesome-mcp-servers.row.md +1 -1
- package/dist/mcp/mcp-cloudflare-catalogue.json +2 -2
- package/dist/mcp/registry-manifest.json +4 -4
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +25 -3
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_src.js +39 -6
- package/dist/scripts/_lib/agent_src.js.map +1 -1
- package/dist/server/schemas/settings.js +25 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/guidelines/abstraction-thresholds.md +53 -0
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +1 -1
- package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +7 -0
- package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +2 -2
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +5 -3
- package/docs/guidelines/agent-infra/size-and-scope.md +8 -2
- package/docs/guidelines/component-oriented-and-oop-development.md +11 -6
- package/docs/guidelines/docs/readme-size-and-splitting.md +5 -2
- package/llms.txt +1 -0
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/work_engine/directives/ui/_fix_lane.ts +52 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +8 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +6 -0
- package/src/agent-src/templates/scripts/work_engine/intent/classify.ts +56 -4
- package/src/config/agent-settings.template.yml +68 -0
- package/src/config/evaluator-budgets.json +8 -11
- package/src/config/gate-coverage.yml +70 -0
- package/src/scripts/_cli/cmd_explain.ts +41 -36
- package/src/scripts/_cli/cmd_handoff.ts +405 -0
- package/src/scripts/_cli/cmd_route_audit.ts +290 -0
- package/src/scripts/_cli/cmd_route_explain.ts +269 -0
- package/src/scripts/_cli/handoff_generate.ts +453 -0
- package/src/scripts/_cli/handoff_sessions.ts +435 -0
- package/src/scripts/_dispatch.bash +32 -0
- package/src/scripts/_lib/agent_src.ts +36 -6
- package/src/scripts/_lib/git_env.ts +40 -0
- package/src/scripts/_lib/md_table.ts +27 -0
- package/src/scripts/_lib/planning_settings.ts +43 -0
- package/src/scripts/_lib/release_material.ts +156 -0
- package/src/scripts/_lib/router_match.ts +301 -0
- package/src/scripts/annotate_r1_outcomes.ts +258 -0
- package/src/scripts/check_completion_review.ts +1140 -0
- package/src/scripts/check_condensation.ts +2 -1
- package/src/scripts/check_finding_dispositions.ts +278 -0
- package/src/scripts/check_pack_size.ts +4 -3
- package/src/scripts/check_references.ts +1 -0
- package/src/scripts/check_release_highlights.ts +300 -0
- package/src/scripts/check_release_pr_shape.ts +4 -0
- package/src/scripts/check_release_surface_equality.ts +132 -0
- package/src/scripts/check_reply_consistency.ts +187 -138
- package/src/scripts/compile_router.ts +20 -3
- package/src/scripts/dispatch_r2_reviewer.ts +968 -0
- package/src/scripts/eval_ui_triviality.ts +156 -0
- package/src/scripts/handoff_context_hook.ts +148 -0
- package/src/scripts/hook_manifest.yaml +17 -7
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/lint_abstraction_thresholds.ts +327 -0
- package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
- package/src/scripts/lint_plan_risk_register.ts +931 -0
- package/src/scripts/lint_profile_personas.ts +153 -0
- package/src/scripts/lint_token_budget_discipline.ts +148 -0
- package/src/scripts/lint_trigger_collisions.ts +257 -0
- package/src/scripts/release.ts +156 -32
- package/src/scripts/release_drill.ts +491 -0
- package/src/scripts/router_telemetry.ts +16 -290
- package/src/scripts/schemas/command.schema.json +33 -0
- package/src/scripts/schemas/rule.schema.json +10 -0
- package/src/scripts/self_review_gate.ts +41 -3
- package/src/scripts/skill_linter.ts +3 -1
- package/src/scripts/smoke/router.sh +1 -1
- package/src/scripts/update_counts.ts +60 -12
- package/src/server/schemas/settings.ts +41 -0
- package/dist/cli-delegate/{chunk-XX6TOPD7.js → chunk-WR6GTRHJ.js} +3 -3
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Profile → persona id resolution gate.
|
|
4
|
+
*
|
|
5
|
+
* ## The defect this catches
|
|
6
|
+
*
|
|
7
|
+
* Every seed profile under `src/agent-src/profiles/*.yml` pre-selects persona
|
|
8
|
+
* ids in `defaults.personas`. Persona ids are the filenames under
|
|
9
|
+
* `src/agent-src/personas/` (plus `personas/advisors/`), and nothing else
|
|
10
|
+
* historically checked the join — all six seed profiles shipped ids
|
|
11
|
+
* (`reviewer`, `security`, `editor`, …) that resolved to no persona file at
|
|
12
|
+
* all, so the pre-selection was silently dead. This gate makes a broken id a
|
|
13
|
+
* CI failure with a nearest-match hint.
|
|
14
|
+
*
|
|
15
|
+
* Contract (src/config/gate-coverage.yml): emits exactly one `scanned: <N>`
|
|
16
|
+
* line, N = profiles scanned.
|
|
17
|
+
*
|
|
18
|
+
* Exit: 0 clean · 1 usage/IO error · 2 findings.
|
|
19
|
+
*/
|
|
20
|
+
import * as fs from 'node:fs';
|
|
21
|
+
import * as path from 'node:path';
|
|
22
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
23
|
+
import { parse as parseYaml } from 'yaml';
|
|
24
|
+
|
|
25
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
26
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
27
|
+
|
|
28
|
+
export interface Finding {
|
|
29
|
+
file: string;
|
|
30
|
+
id: string;
|
|
31
|
+
hint: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Persona ids = `.md` basenames under personas/ and personas/advisors/. */
|
|
35
|
+
export function known_persona_ids(repoRoot: string): Set<string> {
|
|
36
|
+
const ids = new Set<string>();
|
|
37
|
+
const roots = [
|
|
38
|
+
path.join(repoRoot, 'src', 'agent-src', 'personas'),
|
|
39
|
+
path.join(repoRoot, 'src', 'agent-src', 'personas', 'advisors'),
|
|
40
|
+
];
|
|
41
|
+
for (const dir of roots) {
|
|
42
|
+
if (!fs.existsSync(dir)) continue;
|
|
43
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
44
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
45
|
+
if (entry.name === 'README.md') continue;
|
|
46
|
+
ids.add(entry.name.slice(0, -'.md'.length));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return ids;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function levenshtein(a: string, b: string): number {
|
|
53
|
+
const prev: number[] = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
54
|
+
for (let i = 1; i <= a.length; i++) {
|
|
55
|
+
let diag = prev[0]!;
|
|
56
|
+
prev[0] = i;
|
|
57
|
+
for (let j = 1; j <= b.length; j++) {
|
|
58
|
+
const cur = prev[j]!;
|
|
59
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
60
|
+
prev[j] = Math.min(cur + 1, prev[j - 1]! + 1, diag + cost);
|
|
61
|
+
diag = cur;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return prev[b.length]!;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Closest known id by substring containment first, edit distance second. */
|
|
68
|
+
export function nearest_match(id: string, known: ReadonlySet<string>): string {
|
|
69
|
+
let best = '';
|
|
70
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
71
|
+
for (const candidate of known) {
|
|
72
|
+
const contains = candidate.includes(id) || id.includes(candidate);
|
|
73
|
+
const score = levenshtein(id, candidate) - (contains ? 100 : 0);
|
|
74
|
+
if (score < bestScore) {
|
|
75
|
+
bestScore = score;
|
|
76
|
+
best = candidate;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return best;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Scan every `profiles/*.yml` under `repoRoot`'s profile dir and resolve
|
|
84
|
+
* `defaults.personas` against the known persona ids.
|
|
85
|
+
*/
|
|
86
|
+
export function scan_profiles(repoRoot: string): { findings: Finding[]; scanned: number } {
|
|
87
|
+
const profileDir = path.join(repoRoot, 'src', 'agent-src', 'profiles');
|
|
88
|
+
if (!fs.existsSync(profileDir)) {
|
|
89
|
+
throw new Error(`profile directory not found: ${profileDir}`);
|
|
90
|
+
}
|
|
91
|
+
const known = known_persona_ids(repoRoot);
|
|
92
|
+
const findings: Finding[] = [];
|
|
93
|
+
let scanned = 0;
|
|
94
|
+
const files = fs
|
|
95
|
+
.readdirSync(profileDir)
|
|
96
|
+
.filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'))
|
|
97
|
+
.sort();
|
|
98
|
+
for (const file of files) {
|
|
99
|
+
const full = path.join(profileDir, file);
|
|
100
|
+
const rel = path.relative(repoRoot, full);
|
|
101
|
+
const parsed = parseYaml(fs.readFileSync(full, 'utf-8')) as Record<string, unknown> | null;
|
|
102
|
+
scanned += 1;
|
|
103
|
+
const profile = (parsed?.['profile'] ?? {}) as Record<string, unknown>;
|
|
104
|
+
const defaults = (profile['defaults'] ?? {}) as Record<string, unknown>;
|
|
105
|
+
const personas = defaults['personas'];
|
|
106
|
+
if (!Array.isArray(personas)) continue;
|
|
107
|
+
for (const raw of personas) {
|
|
108
|
+
const id = String(raw);
|
|
109
|
+
if (known.has(id)) continue;
|
|
110
|
+
const hint = known.size > 0 ? `did you mean \`${nearest_match(id, known)}\`?` : 'no personas found';
|
|
111
|
+
findings.push({ file: rel, id, hint });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { findings, scanned };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function main(argv?: readonly string[]): number {
|
|
118
|
+
let quiet = false;
|
|
119
|
+
for (const arg of argv ?? process.argv.slice(2)) {
|
|
120
|
+
if (arg === '--quiet') {
|
|
121
|
+
quiet = true;
|
|
122
|
+
} else {
|
|
123
|
+
process.stderr.write(`usage: lint_profile_personas [--quiet]\n`);
|
|
124
|
+
return 1;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let result: { findings: Finding[]; scanned: number };
|
|
129
|
+
try {
|
|
130
|
+
result = scan_profiles(REPO_ROOT);
|
|
131
|
+
} catch (e) {
|
|
132
|
+
process.stderr.write(`error: ${String(e)}\n`);
|
|
133
|
+
return 1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const f of result.findings) {
|
|
137
|
+
process.stdout.write(
|
|
138
|
+
`❌ ${f.file} defaults.personas id \`${f.id}\` resolves to no file under src/agent-src/personas/ — ${f.hint}\n`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
if (result.findings.length === 0 && !quiet) {
|
|
142
|
+
process.stdout.write(
|
|
143
|
+
`✅ profile personas: every defaults.personas id resolves to a persona file\n`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
// gate-coverage contract (src/config/gate-coverage.yml): profiles inspected.
|
|
147
|
+
process.stdout.write(`scanned: ${String(result.scanned)}\n`);
|
|
148
|
+
return result.findings.length > 0 ? 2 : 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
152
|
+
process.exit(main());
|
|
153
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Token-budget-discipline gate (src/rules/token-budget-discipline.md).
|
|
4
|
+
*
|
|
5
|
+
* The rule names this linter as its CI enforcer for two obligations on
|
|
6
|
+
* `token_budget_class: rich` skills:
|
|
7
|
+
*
|
|
8
|
+
* 1. CAP — at most 15 % of the suite's skills may claim `rich`. Exceeding
|
|
9
|
+
* the ratio is a hard fail (both numbers are printed).
|
|
10
|
+
* 2. JUSTIFICATION — every `rich`-tagged skill carries a
|
|
11
|
+
* `## Why this skill is rich` section explaining the irreducible
|
|
12
|
+
* complexity.
|
|
13
|
+
*
|
|
14
|
+
* Additionally, any `token_budget_class` value outside
|
|
15
|
+
* `lean | standard | rich` is a finding (schema drift).
|
|
16
|
+
*
|
|
17
|
+
* Exit: 0 clean · 1 usage/IO error · 2 findings.
|
|
18
|
+
*/
|
|
19
|
+
import * as fs from 'node:fs';
|
|
20
|
+
import * as path from 'node:path';
|
|
21
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
22
|
+
|
|
23
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
24
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
25
|
+
|
|
26
|
+
export const SKILLS_ROOT = 'src/skills';
|
|
27
|
+
|
|
28
|
+
/** The rule's cap: rich-tagged skills / total skills must stay <= 15 %. */
|
|
29
|
+
export const RICH_RATIO_CAP = 0.15;
|
|
30
|
+
|
|
31
|
+
export const VALID_CLASSES = ['lean', 'standard', 'rich'] as const;
|
|
32
|
+
|
|
33
|
+
export const RICH_SECTION_HEADING = '## Why this skill is rich';
|
|
34
|
+
|
|
35
|
+
export interface Finding {
|
|
36
|
+
/** repo-relative path */
|
|
37
|
+
file: string;
|
|
38
|
+
message: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ScanResult {
|
|
42
|
+
findings: Finding[];
|
|
43
|
+
/** SKILL.md files scanned. */
|
|
44
|
+
scanned: number;
|
|
45
|
+
/** Skills declaring token_budget_class: rich. */
|
|
46
|
+
richCount: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** `token_budget_class` value from a SKILL.md's YAML frontmatter, or null. */
|
|
50
|
+
export function frontmatter_class(content: string): string | null {
|
|
51
|
+
if (!content.startsWith('---')) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const end = content.indexOf('\n---', 3);
|
|
55
|
+
if (end === -1) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const frontmatter = content.slice(0, end);
|
|
59
|
+
const m = /^token_budget_class:\s*['"]?([^'"\s#]+)/m.exec(frontmatter);
|
|
60
|
+
return m ? m[1]! : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Scan one skills root (directory of `<skill>/SKILL.md`). */
|
|
64
|
+
export function scan_skills(skillsRoot: string): ScanResult {
|
|
65
|
+
const findings: Finding[] = [];
|
|
66
|
+
let scanned = 0;
|
|
67
|
+
let richCount = 0;
|
|
68
|
+
const entries = fs
|
|
69
|
+
.readdirSync(skillsRoot, { withFileTypes: true })
|
|
70
|
+
.filter((e) => e.isDirectory())
|
|
71
|
+
.map((e) => e.name)
|
|
72
|
+
.sort();
|
|
73
|
+
for (const name of entries) {
|
|
74
|
+
const skillFile = path.join(skillsRoot, name, 'SKILL.md');
|
|
75
|
+
if (!fs.existsSync(skillFile)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
scanned += 1;
|
|
79
|
+
const rel = path.relative(REPO_ROOT, skillFile).split(path.sep).join('/');
|
|
80
|
+
const content = fs.readFileSync(skillFile, 'utf-8');
|
|
81
|
+
const cls = frontmatter_class(content);
|
|
82
|
+
if (cls === null) {
|
|
83
|
+
continue; // no key = lean by default (rule § the three classes)
|
|
84
|
+
}
|
|
85
|
+
if (!(VALID_CLASSES as readonly string[]).includes(cls)) {
|
|
86
|
+
findings.push({
|
|
87
|
+
file: rel,
|
|
88
|
+
message: `invalid token_budget_class '${cls}' — must be one of ${VALID_CLASSES.join(' | ')}`,
|
|
89
|
+
});
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (cls === 'rich') {
|
|
93
|
+
richCount += 1;
|
|
94
|
+
if (!content.includes(RICH_SECTION_HEADING)) {
|
|
95
|
+
findings.push({
|
|
96
|
+
file: rel,
|
|
97
|
+
message: `rich-tagged skill missing the '${RICH_SECTION_HEADING}' justification section`,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (scanned > 0 && richCount > scanned * RICH_RATIO_CAP) {
|
|
103
|
+
findings.push({
|
|
104
|
+
file: SKILLS_ROOT,
|
|
105
|
+
message:
|
|
106
|
+
`rich-skill cap exceeded: ${String(richCount)} rich of ${String(scanned)} skills ` +
|
|
107
|
+
`(${((richCount / scanned) * 100).toFixed(1)} % > ${String(RICH_RATIO_CAP * 100)} % cap, ` +
|
|
108
|
+
`max ${String(Math.floor(scanned * RICH_RATIO_CAP))})`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return { findings, scanned, richCount };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function main(argv?: readonly string[]): number {
|
|
115
|
+
let quiet = false;
|
|
116
|
+
for (const arg of argv ?? process.argv.slice(2)) {
|
|
117
|
+
if (arg === '--quiet') {
|
|
118
|
+
quiet = true;
|
|
119
|
+
} else {
|
|
120
|
+
process.stderr.write('usage: lint_token_budget_discipline [--quiet]\n');
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let result: ScanResult;
|
|
126
|
+
try {
|
|
127
|
+
result = scan_skills(path.join(REPO_ROOT, SKILLS_ROOT));
|
|
128
|
+
} catch (e) {
|
|
129
|
+
process.stderr.write(`error: ${String(e)}\n`);
|
|
130
|
+
return 1;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const f of result.findings) {
|
|
134
|
+
process.stdout.write(`❌ ${f.file} ${f.message}\n`);
|
|
135
|
+
}
|
|
136
|
+
if (result.findings.length === 0 && !quiet) {
|
|
137
|
+
process.stdout.write(
|
|
138
|
+
`✅ token-budget discipline: ${String(result.richCount)} rich of ${String(result.scanned)} skills within the ${String(RICH_RATIO_CAP * 100)} % cap; every rich skill justifies itself\n`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
// gate-coverage contract (src/config/gate-coverage.yml): files inspected.
|
|
142
|
+
process.stdout.write(`scanned: ${String(result.scanned)}\n`);
|
|
143
|
+
return result.findings.length > 0 ? 2 : 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
147
|
+
process.exit(main());
|
|
148
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* lint_trigger_collisions — every shared trigger carries a written-down
|
|
4
|
+
* disposition, or the build is red.
|
|
5
|
+
*
|
|
6
|
+
* Defect measured: a trigger string shared by ≥2 routed rules makes both fire
|
|
7
|
+
* on the same prompt. Many collisions are correct (`refactor` firing both the
|
|
8
|
+
* analyze-first and the diff-shape floor is complementary), but before this
|
|
9
|
+
* gate nothing forced that argument to be WRITTEN DOWN — so accidental
|
|
10
|
+
* co-firing (the brand pair, secret/password, valuation/DCF) accumulated
|
|
11
|
+
* silently. This gate makes every collision either dispositioned or an error.
|
|
12
|
+
*
|
|
13
|
+
* Why this gate shape: the existing `tests/scripts/rule_trigger_collisions.test.ts`
|
|
14
|
+
* is a pair-level Jaccard SIMILARITY ratchet (near-duplicate trigger sets);
|
|
15
|
+
* it is kept untouched. This lint is per-trigger-VALUE disposition coverage —
|
|
16
|
+
* a different axis: one shared value between otherwise-different rules never
|
|
17
|
+
* moves Jaccard past 0.4, but still needs an argument.
|
|
18
|
+
*
|
|
19
|
+
* Disposition, in the sharing rule's frontmatter (both optional keys are in
|
|
20
|
+
* `schemas/rule.schema.json`):
|
|
21
|
+
*
|
|
22
|
+
* collision_ok:
|
|
23
|
+
* "refactor": "one-line reason why this rule also firing here is correct"
|
|
24
|
+
* precedence:
|
|
25
|
+
* "refactor": 1 # all sharers declare pairwise-distinct integers
|
|
26
|
+
*
|
|
27
|
+
* A collision is clean when EVERY sharer has `collision_ok[<value>]`, OR all
|
|
28
|
+
* sharers carry pairwise-distinct `precedence[<value>]` integers.
|
|
29
|
+
*
|
|
30
|
+
* Scope: `src/rules/*.md`, `type: manual` excluded (reference-only, no router
|
|
31
|
+
* emission — ADR-004), rules without `triggers:` excluded (kernel).
|
|
32
|
+
* Same-value-different-kind never collides (a `keyword` and a `path_prefix`
|
|
33
|
+
* match different surfaces).
|
|
34
|
+
*
|
|
35
|
+
* Modes: default lint (exit 0 clean, 2 findings/dead-scope, 1 internal error;
|
|
36
|
+
* emits one machine-readable `scanned: <N>` line for gate-coverage), plus
|
|
37
|
+
* `--report <path>` writing the collision-census markdown, `--json`, `--quiet`.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import * as fs from 'node:fs';
|
|
41
|
+
import * as path from 'node:path';
|
|
42
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
43
|
+
import { parse as parseYaml } from 'yaml';
|
|
44
|
+
|
|
45
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
46
|
+
|
|
47
|
+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
48
|
+
export const RULES_ROOT = 'src/rules';
|
|
49
|
+
const TRIGGER_KINDS = ['keyword', 'phrase', 'command', 'path_prefix', 'file_pattern'] as const;
|
|
50
|
+
|
|
51
|
+
export interface RuleTriggers {
|
|
52
|
+
id: string;
|
|
53
|
+
type: string;
|
|
54
|
+
triggers: { kind: string; value: string }[];
|
|
55
|
+
collision_ok: Record<string, string>;
|
|
56
|
+
precedence: Record<string, number>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface Collision {
|
|
60
|
+
kind: string;
|
|
61
|
+
value: string; // case-folded
|
|
62
|
+
sharers: string[]; // rule ids, sorted
|
|
63
|
+
undispositioned: string[]; // rule ids missing a valid disposition
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function _parse_frontmatter(text: string): Record<string, unknown> {
|
|
67
|
+
if (!text.startsWith('---\n')) return {};
|
|
68
|
+
const end = text.indexOf('\n---', 4);
|
|
69
|
+
if (end < 0) return {};
|
|
70
|
+
const data = parseYaml(text.slice(4, end), { version: '1.1' }) ?? {};
|
|
71
|
+
return data !== null && typeof data === 'object' && !Array.isArray(data)
|
|
72
|
+
? (data as Record<string, unknown>)
|
|
73
|
+
: {};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function _as_string_map(v: unknown): Record<string, string> {
|
|
77
|
+
if (v === null || typeof v !== 'object' || Array.isArray(v)) return {};
|
|
78
|
+
const out: Record<string, string> = {};
|
|
79
|
+
for (const [k, val] of Object.entries(v as Record<string, unknown>)) {
|
|
80
|
+
out[k.toLowerCase()] = String(val);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function _as_int_map(v: unknown): Record<string, number> {
|
|
86
|
+
if (v === null || typeof v !== 'object' || Array.isArray(v)) return {};
|
|
87
|
+
const out: Record<string, number> = {};
|
|
88
|
+
for (const [k, val] of Object.entries(v as Record<string, unknown>)) {
|
|
89
|
+
const n = Number(val);
|
|
90
|
+
if (Number.isInteger(n)) out[k.toLowerCase()] = n;
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function load_rules(rules_dir: string): RuleTriggers[] {
|
|
96
|
+
const out: RuleTriggers[] = [];
|
|
97
|
+
let files: string[] = [];
|
|
98
|
+
try {
|
|
99
|
+
files = fs.readdirSync(rules_dir).filter((f) => f.endsWith('.md')).sort();
|
|
100
|
+
} catch {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
for (const f of files) {
|
|
104
|
+
const fm = _parse_frontmatter(fs.readFileSync(path.join(rules_dir, f), 'utf-8'));
|
|
105
|
+
if (Object.keys(fm).length === 0) continue;
|
|
106
|
+
const type = String(fm['type'] ?? 'auto');
|
|
107
|
+
const rawTriggers = Array.isArray(fm['triggers']) ? (fm['triggers'] as unknown[]) : [];
|
|
108
|
+
const triggers: { kind: string; value: string }[] = [];
|
|
109
|
+
for (const t of rawTriggers) {
|
|
110
|
+
if (t === null || typeof t !== 'object' || Array.isArray(t)) continue;
|
|
111
|
+
for (const kind of TRIGGER_KINDS) {
|
|
112
|
+
if (kind in (t as Record<string, unknown>)) {
|
|
113
|
+
triggers.push({ kind, value: String((t as Record<string, unknown>)[kind]).toLowerCase() });
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
out.push({
|
|
119
|
+
id: f.replace(/\.md$/, ''),
|
|
120
|
+
type,
|
|
121
|
+
triggers,
|
|
122
|
+
collision_ok: _as_string_map(fm['collision_ok']),
|
|
123
|
+
precedence: _as_int_map(fm['precedence']),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function find_collisions(rules: RuleTriggers[]): Collision[] {
|
|
130
|
+
const routed = rules.filter((r) => r.type !== 'manual' && r.triggers.length > 0);
|
|
131
|
+
const byKey = new Map<string, Set<string>>();
|
|
132
|
+
const ruleById = new Map(routed.map((r) => [r.id, r]));
|
|
133
|
+
for (const r of routed) {
|
|
134
|
+
for (const t of r.triggers) {
|
|
135
|
+
const key = `${t.kind}\u0000${t.value}`;
|
|
136
|
+
const set = byKey.get(key) ?? new Set<string>();
|
|
137
|
+
set.add(r.id);
|
|
138
|
+
byKey.set(key, set);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const collisions: Collision[] = [];
|
|
142
|
+
for (const [key, sharerSet] of byKey) {
|
|
143
|
+
if (sharerSet.size < 2) continue;
|
|
144
|
+
const [kind, value] = key.split('\u0000') as [string, string];
|
|
145
|
+
const sharers = [...sharerSet].sort();
|
|
146
|
+
const withOk = sharers.filter((id) => value in (ruleById.get(id)?.collision_ok ?? {}));
|
|
147
|
+
const precedences = sharers
|
|
148
|
+
.map((id) => (ruleById.get(id)?.precedence ?? {})[value])
|
|
149
|
+
.filter((n): n is number => n !== undefined);
|
|
150
|
+
const precedence_ok =
|
|
151
|
+
precedences.length === sharers.length && new Set(precedences).size === sharers.length;
|
|
152
|
+
const undispositioned = precedence_ok ? [] : sharers.filter((id) => !withOk.includes(id));
|
|
153
|
+
collisions.push({ kind, value, sharers, undispositioned });
|
|
154
|
+
}
|
|
155
|
+
collisions.sort(
|
|
156
|
+
(a, b) => b.sharers.length - a.sharers.length || (a.value < b.value ? -1 : a.value > b.value ? 1 : 0),
|
|
157
|
+
);
|
|
158
|
+
return collisions;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function render_report(rules: RuleTriggers[], collisions: Collision[], sha: string): string {
|
|
162
|
+
const routed = rules.filter((r) => r.type !== 'manual' && r.triggers.length > 0);
|
|
163
|
+
const totalTriggers = routed.reduce((s, r) => s + r.triggers.length, 0);
|
|
164
|
+
const distinct = new Set(routed.flatMap((r) => r.triggers.map((t) => `${t.kind}\u0000${t.value}`))).size;
|
|
165
|
+
const undisp = collisions.filter((c) => c.undispositioned.length > 0);
|
|
166
|
+
const lines: string[] = [
|
|
167
|
+
'# Trigger-collision census',
|
|
168
|
+
'',
|
|
169
|
+
`> Generated by \`./scripts-run src/scripts/lint_trigger_collisions --report agents/evidence/reports/trigger-collision-census.md\` on 2026-08-04 at ${sha}.`,
|
|
170
|
+
'> Do not hand-edit — re-run the command.',
|
|
171
|
+
'',
|
|
172
|
+
`- Routed rules with triggers: ${routed.length} (of ${rules.length} rule files; \`type: manual\` and trigger-less kernel rules excluded)`,
|
|
173
|
+
`- Trigger entries: ${totalTriggers} · distinct (kind, value) pairs: ${distinct}`,
|
|
174
|
+
`- Colliding values (shared by ≥2 rules): ${collisions.length}`,
|
|
175
|
+
`- Undispositioned collisions: ${undisp.length} (the lint is red unless this is 0)`,
|
|
176
|
+
'',
|
|
177
|
+
'| # | Kind | Trigger | Rules | Disposition |',
|
|
178
|
+
'|---|------|---------|-------|-------------|',
|
|
179
|
+
];
|
|
180
|
+
collisions.forEach((c, i) => {
|
|
181
|
+
const status = c.undispositioned.length === 0 ? 'dispositioned' : `MISSING: ${c.undispositioned.join(', ')}`;
|
|
182
|
+
lines.push(`| ${i + 1} | ${c.kind} | \`${c.value}\` | ${c.sharers.join(', ')} | ${status} |`);
|
|
183
|
+
});
|
|
184
|
+
lines.push('');
|
|
185
|
+
return lines.join('\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function main(argv: string[]): number {
|
|
189
|
+
const quiet = argv.includes('--quiet');
|
|
190
|
+
const as_json = argv.includes('--json');
|
|
191
|
+
let reportPath: string | null = null;
|
|
192
|
+
const ri = argv.indexOf('--report');
|
|
193
|
+
if (ri >= 0) reportPath = argv[ri + 1] ?? null;
|
|
194
|
+
|
|
195
|
+
const rulesDir = path.join(REPO_ROOT, RULES_ROOT);
|
|
196
|
+
const rules = load_rules(rulesDir);
|
|
197
|
+
try {
|
|
198
|
+
assertScanned({
|
|
199
|
+
gate: 'lint_trigger_collisions',
|
|
200
|
+
scanned: rules.length,
|
|
201
|
+
units: 'rule file(s)',
|
|
202
|
+
roots: [RULES_ROOT],
|
|
203
|
+
});
|
|
204
|
+
} catch (exc) {
|
|
205
|
+
if (!(exc instanceof DeadScopeError)) throw exc;
|
|
206
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
207
|
+
return 2;
|
|
208
|
+
}
|
|
209
|
+
process.stdout.write(`scanned: ${rules.length}\n`);
|
|
210
|
+
|
|
211
|
+
const collisions = find_collisions(rules);
|
|
212
|
+
const findings = collisions.filter((c) => c.undispositioned.length > 0);
|
|
213
|
+
|
|
214
|
+
if (reportPath) {
|
|
215
|
+
let sha = 'HEAD';
|
|
216
|
+
try {
|
|
217
|
+
sha = fs.readFileSync(path.join(REPO_ROOT, '.git', 'HEAD'), 'utf-8').trim().slice(0, 9);
|
|
218
|
+
} catch {
|
|
219
|
+
// detached info unavailable — keep the placeholder
|
|
220
|
+
}
|
|
221
|
+
const target = path.isAbsolute(reportPath) ? reportPath : path.join(REPO_ROOT, reportPath);
|
|
222
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
223
|
+
fs.writeFileSync(target, render_report(rules, collisions, sha));
|
|
224
|
+
if (!quiet) process.stdout.write(`report → ${reportPath}\n`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (as_json) {
|
|
228
|
+
process.stdout.write(JSON.stringify({ collisions, findings: findings.length }, null, 2) + '\n');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (findings.length > 0) {
|
|
232
|
+
process.stderr.write(
|
|
233
|
+
`❌ ${findings.length} trigger collision(s) without a written disposition:\n`,
|
|
234
|
+
);
|
|
235
|
+
for (const c of findings) {
|
|
236
|
+
process.stderr.write(
|
|
237
|
+
` ${c.kind} "${c.value}" shared by [${c.sharers.join(', ')}] — missing collision_ok/precedence on: ${c.undispositioned.join(', ')}\n`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
process.stderr.write(
|
|
241
|
+
' Remedy: add `collision_ok: {"<value>": "<one-line reason>"}` (or pairwise-distinct\n' +
|
|
242
|
+
' `precedence: {"<value>": <int>}`) to EVERY sharer’s frontmatter, or disjoin the trigger sets.\n',
|
|
243
|
+
);
|
|
244
|
+
return 2;
|
|
245
|
+
}
|
|
246
|
+
if (!quiet) {
|
|
247
|
+
process.stdout.write(
|
|
248
|
+
`✅ ${collisions.length} trigger collision(s), all dispositioned (${rules.length} rules scanned)\n`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
return 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
255
|
+
if (process.argv[1] && (import.meta.url === pathToFileURL(process.argv[1]).href || path.resolve(process.argv[1]) === _HERE)) {
|
|
256
|
+
process.exitCode = main(process.argv.slice(2));
|
|
257
|
+
}
|