@event4u/agent-config 9.13.0 → 9.15.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 (280) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +113 -67
  3. package/README.md +9 -1
  4. package/dist/agent-src/commands/agents/audit.md +7 -7
  5. package/dist/agent-src/commands/agents/optimize.md +1 -1
  6. package/dist/agent-src/commands/analyze/reference-repo.md +1 -1
  7. package/dist/agent-src/commands/chat-history.md +1 -0
  8. package/dist/agent-src/commands/context.md +1 -0
  9. package/dist/agent-src/commands/council.md +1 -1
  10. package/dist/agent-src/commands/ghostwriter.md +1 -1
  11. package/dist/agent-src/commands/memory.md +2 -1
  12. package/dist/agent-src/commands/module.md +1 -0
  13. package/dist/agent-src/commands/optimize/agents-dir.md +1 -1
  14. package/dist/agent-src/commands/optimize/deep.md +150 -0
  15. package/dist/agent-src/commands/optimize/skills.md +5 -5
  16. package/dist/agent-src/commands/optimize.md +12 -6
  17. package/dist/agent-src/commands/override.md +1 -0
  18. package/dist/agent-src/commands/prediction-pool.md +4 -4
  19. package/dist/agent-src/commands/research/deep.md +1 -1
  20. package/dist/agent-src/commands/roadmap.md +2 -1
  21. package/dist/agent-src/commands/rule-compliance-audit.md +1 -1
  22. package/dist/agent-src/commands/tdd.md +1 -0
  23. package/dist/agent-src/commands/tests.md +1 -0
  24. package/dist/agent-src/commands/upstream-contribute.md +5 -5
  25. package/dist/agent-src/commands/video/from-script.md +1 -1
  26. package/dist/agent-src/commands/video/from-song.md +1 -1
  27. package/dist/agent-src/commands/video/scene.md +1 -1
  28. package/dist/agent-src/commands/video/stitch.md +1 -1
  29. package/dist/agent-src/commands/video/storyboard.md +1 -1
  30. package/dist/agent-src/commands/worktree/create.md +2 -1
  31. package/dist/agent-src/contexts/augment-infrastructure.md +1 -1
  32. package/dist/agent-src/contexts/contracts/artifact-engagement-flow.md +5 -5
  33. package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +3 -3
  34. package/dist/agent-src/contexts/contracts/emergency-triage-block.md +2 -2
  35. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +4 -3
  36. package/dist/agent-src/contexts/execution/subagent-routing.md +19 -0
  37. package/dist/agent-src/contexts/override-system.md +1 -1
  38. package/dist/agent-src/ghostwriter/fictional-fixture-v1.md +1 -1
  39. package/dist/agent-src/rules/augment-edit-discipline.md +1 -1
  40. package/dist/agent-src/rules/autonomous-execution.md +2 -1
  41. package/dist/agent-src/rules/brand-consistency.md +1 -0
  42. package/dist/agent-src/rules/brand-source-of-truth.md +1 -0
  43. package/dist/agent-src/rules/code-comment-discipline.md +2 -0
  44. package/dist/agent-src/rules/communication-through-line.md +1 -0
  45. package/dist/agent-src/rules/content-quoting-floor.md +1 -0
  46. package/dist/agent-src/rules/cross-source-consistency.md +0 -1
  47. package/dist/agent-src/rules/downstream-changes.md +1 -0
  48. package/dist/agent-src/rules/external-code-graph-interop.md +1 -0
  49. package/dist/agent-src/rules/external-reference-deep-dive.md +1 -0
  50. package/dist/agent-src/rules/fast-path-marker-visibility.md +1 -0
  51. package/dist/agent-src/rules/finance-safety-floor.md +0 -1
  52. package/dist/agent-src/rules/icon-consistency.md +1 -0
  53. package/dist/agent-src/rules/image-likeness-and-rights.md +1 -0
  54. package/dist/agent-src/rules/improve-before-implement.md +1 -1
  55. package/dist/agent-src/rules/invite-challenge.md +1 -0
  56. package/dist/agent-src/rules/lethal-trifecta-guard.md +1 -0
  57. package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
  58. package/dist/agent-src/rules/markdown-safe-codeblocks.md +1 -0
  59. package/dist/agent-src/rules/media-governance-routing.md +4 -3
  60. package/dist/agent-src/rules/media-sync-ground-truth.md +1 -0
  61. package/dist/agent-src/rules/no-attribution-footers.md +1 -0
  62. package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +1 -0
  63. package/dist/agent-src/rules/no-pr-progress-comments.md +1 -0
  64. package/dist/agent-src/rules/onboarding-gate.md +1 -0
  65. package/dist/agent-src/rules/output-discipline.md +1 -0
  66. package/dist/agent-src/rules/prefer-enums-over-literals.md +1 -0
  67. package/dist/agent-src/rules/preservation-guard.md +1 -0
  68. package/dist/agent-src/rules/question-not-instruction.md +1 -0
  69. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +1 -0
  70. package/dist/agent-src/rules/role-mode-adherence.md +1 -0
  71. package/dist/agent-src/rules/runtime-safety.md +1 -0
  72. package/dist/agent-src/rules/security-sensitive-stop.md +1 -0
  73. package/dist/agent-src/rules/session-canary.md +9 -2
  74. package/dist/agent-src/rules/source-confidentiality.md +1 -0
  75. package/dist/agent-src/rules/source-of-truth.md +1 -0
  76. package/dist/agent-src/rules/spreadsheet-source-quality.md +1 -0
  77. package/dist/agent-src/rules/think-before-action.md +1 -0
  78. package/dist/agent-src/rules/token-budget-discipline.md +1 -0
  79. package/dist/agent-src/rules/token-efficiency.md +1 -0
  80. package/dist/agent-src/rules/token-optimizer-maintenance.md +5 -5
  81. package/dist/agent-src/rules/tool-safety.md +1 -0
  82. package/dist/agent-src/rules/untrusted-input-defense.md +2 -0
  83. package/dist/agent-src/rules/user-interaction.md +1 -0
  84. package/dist/agent-src/rules/user-interrupt-priority.md +1 -0
  85. package/dist/agent-src/skills/adr-create/SKILL.md +1 -1
  86. package/dist/agent-src/skills/check-refs/SKILL.md +1 -1
  87. package/dist/agent-src/skills/competitive-positioning/SKILL.md +1 -1
  88. package/dist/agent-src/skills/condense-memory/SKILL.md +1 -1
  89. package/dist/agent-src/skills/context-authoring/SKILL.md +1 -1
  90. package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +1 -1
  91. package/dist/agent-src/skills/description-assist/SKILL.md +1 -1
  92. package/dist/agent-src/skills/github-ci/SKILL.md +1 -0
  93. package/dist/agent-src/skills/guideline-writing/SKILL.md +1 -1
  94. package/dist/agent-src/skills/lint-skills/SKILL.md +1 -1
  95. package/dist/agent-src/skills/readme-reviewer/SKILL.md +1 -1
  96. package/dist/agent-src/skills/roadmap-management/SKILL.md +8 -1
  97. package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
  98. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
  99. package/dist/agent-src/skills/skill-writing/SKILL.md +4 -4
  100. package/dist/agent-src/skills/token-optimizer/SKILL.md +4 -4
  101. package/dist/agent-src/skills/upstream-contribute/SKILL.md +6 -6
  102. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +25 -0
  103. package/dist/agent-src/templates/AGENTS.md +1 -1
  104. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  105. package/dist/agent-src/templates/command.md +1 -1
  106. package/dist/agent-src/templates/copilot-instructions.md +3 -3
  107. package/dist/agent-src/templates/rule.md +2 -2
  108. package/dist/agent-src/templates/scripts/work_engine/orchestration.ts +1 -1
  109. package/dist/agent-src/templates/skill-archive-note.md +5 -5
  110. package/dist/agent-src/templates/skill.md +1 -1
  111. package/dist/cli/agent-config.js +66 -19
  112. package/dist/cli/agent-config.js.map +1 -1
  113. package/dist/cli/main.js +9 -4
  114. package/dist/cli/main.js.map +1 -1
  115. package/dist/cli/python/workspace_drive.js +2 -0
  116. package/dist/cli/python/workspace_drive.js.map +1 -1
  117. package/dist/cli/python/workspace_drive_health.js +2 -0
  118. package/dist/cli/python/workspace_drive_health.js.map +1 -1
  119. package/dist/cli/python/workspace_hosts.js +2 -0
  120. package/dist/cli/python/workspace_hosts.js.map +1 -1
  121. package/dist/cli/python/workspace_sessions.js +2 -0
  122. package/dist/cli/python/workspace_sessions.js.map +1 -1
  123. package/dist/cli/registry.js +1 -0
  124. package/dist/cli/registry.js.map +1 -1
  125. package/dist/cli-delegate/{chunk-A3HJ6NTI.js → chunk-63WSP6YZ.js} +2 -1
  126. package/dist/cli-delegate/{chunk-EJYIGZ3T.js → chunk-BKO2AZ7Z.js} +1 -1
  127. package/dist/cli-delegate/{chunk-QFKQLQ7X.js → chunk-H574KOUH.js} +340 -462
  128. package/dist/cli-delegate/chunk-NXFWFAYV.js +339 -0
  129. package/dist/cli-delegate/{chunk-TEGJU2JX.js → chunk-ZXXJLL5K.js} +2 -2
  130. package/dist/cli-delegate/cmd_conformance.js +29 -23
  131. package/dist/cli-delegate/cmd_doctor.js +3 -3
  132. package/dist/cli-delegate/cmd_export.js +4 -4
  133. package/dist/cli-delegate/cmd_preflight.js +2 -2
  134. package/dist/cli-delegate/cmd_prune.js +4 -4
  135. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  136. package/dist/cli-delegate/cmd_sync.js +4 -4
  137. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  138. package/dist/cli-delegate/cmd_validate.js +4 -4
  139. package/dist/discovery/deprecation-report.md +1 -1
  140. package/dist/discovery/discovery-manifest.json +163 -103
  141. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  142. package/dist/discovery/discovery-manifest.summary.md +3 -3
  143. package/dist/discovery/orphan-report.md +1 -1
  144. package/dist/discovery/packs.json +6 -5
  145. package/dist/discovery/trust-report.md +3 -3
  146. package/dist/discovery/workspaces.json +5 -4
  147. package/dist/hooks/dispatch.js +729 -430
  148. package/dist/install/install.mjs +700 -625
  149. package/dist/install/rule_scope.js +18 -2
  150. package/dist/install/rule_scope.js.map +1 -1
  151. package/dist/mcp/registry-manifest.json +3 -3
  152. package/dist/mcp/server.json +2 -2
  153. package/dist/mcp/server.mjs +564 -424
  154. package/dist/router.json +1 -1
  155. package/dist/scripts/_lib/claude_settings_hooks.js +30 -5
  156. package/dist/scripts/_lib/claude_settings_hooks.js.map +1 -1
  157. package/dist/scripts/_lib/scoped_projection.js +223 -0
  158. package/dist/scripts/_lib/scoped_projection.js.map +1 -0
  159. package/dist/scripts/_lib/surface_tiers.js +125 -0
  160. package/dist/scripts/_lib/surface_tiers.js.map +1 -0
  161. package/dist/scripts/_lib/token_count.js +166 -0
  162. package/dist/scripts/_lib/token_count.js.map +1 -0
  163. package/dist/scripts/_lib/trigger_eval_floors.js +8 -0
  164. package/dist/scripts/_lib/trigger_eval_floors.js.map +1 -1
  165. package/dist/scripts/condense.js +139 -9
  166. package/dist/scripts/condense.js.map +1 -1
  167. package/dist/scripts/project_thin_rules.js +450 -0
  168. package/dist/scripts/project_thin_rules.js.map +1 -0
  169. package/dist/server/routes/workspace.js +2 -0
  170. package/dist/server/routes/workspace.js.map +1 -1
  171. package/dist/server/schemas/settings.js +9 -3
  172. package/dist/server/schemas/settings.js.map +1 -1
  173. package/dist/ui/assets/index-DJY_vQVn.js +40 -0
  174. package/dist/ui/assets/index-DJY_vQVn.js.map +1 -0
  175. package/dist/ui/index.html +1 -1
  176. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  177. package/package.json +11 -11
  178. package/src/agent-src/templates/AGENTS.md +1 -1
  179. package/src/agent-src/templates/scripts/work_engine/orchestration.ts +1 -1
  180. package/src/config/agent-settings.template.yml +59 -11
  181. package/src/config/ci-local-parity.yml +8 -0
  182. package/src/config/discovery/unassigned-artefacts.yml +23 -25
  183. package/src/config/evaluator-budgets.json +35 -5
  184. package/src/config/gate-coverage.yml +147 -0
  185. package/src/config/gate-violation-baselines.json +25 -0
  186. package/src/install/rule_scope.ts +27 -2
  187. package/src/scripts/_cli/cmd_conformance.ts +13 -24
  188. package/src/scripts/_dispatch.bash +10 -0
  189. package/src/scripts/_lib/claude_settings_hooks.ts +30 -4
  190. package/src/scripts/_lib/exec_evidence.ts +5 -0
  191. package/src/scripts/_lib/gate_baseline.ts +213 -0
  192. package/src/scripts/_lib/scoped_projection.ts +262 -0
  193. package/src/scripts/_lib/tier_budget_routing.ts +234 -0
  194. package/src/scripts/_lib/tool_adapter_registry.ts +8 -1
  195. package/src/scripts/_lib/trigger_eval_floors.ts +9 -0
  196. package/src/scripts/_lib/trigger_routers.ts +17 -5
  197. package/src/scripts/adr/regenerate_index.ts +4 -3
  198. package/src/scripts/ai-video/lib/parse-blueprint.sh +1 -1
  199. package/src/scripts/ai_council/compile_corpus.ts +1 -1
  200. package/src/scripts/audit_cloud_compatibility.ts +37 -2
  201. package/src/scripts/audit_initial_context.ts +53 -19
  202. package/src/scripts/audit_likelihood.ts +31 -2
  203. package/src/scripts/audit_user_type_axis.ts +58 -11
  204. package/src/scripts/bench_hook_latency.ts +172 -41
  205. package/src/scripts/build_mcp_catalog.ts +12 -1
  206. package/src/scripts/check_always_budget.ts +122 -11
  207. package/src/scripts/check_artefact_count_messaging.ts +128 -15
  208. package/src/scripts/check_augment_description_cap.ts +24 -3
  209. package/src/scripts/check_ci_strict_superset.ts +150 -0
  210. package/src/scripts/check_cluster_patterns.ts +100 -0
  211. package/src/scripts/check_consumer_scope_flip.ts +26 -4
  212. package/src/scripts/check_evaluator_budgets.ts +198 -19
  213. package/src/scripts/check_gate_coverage.ts +337 -1
  214. package/src/scripts/check_generator_output_coverage.ts +2 -1
  215. package/src/scripts/check_iron_law_prominence.ts +1 -1
  216. package/src/scripts/check_kernel_rule_bundle.ts +1 -1
  217. package/src/scripts/check_no_new_legacy_path.ts +167 -3
  218. package/src/scripts/check_pack_size.ts +18 -4
  219. package/src/scripts/check_portability.ts +37 -7
  220. package/src/scripts/check_references.ts +19 -3
  221. package/src/scripts/check_release_pr_shape.ts +13 -0
  222. package/src/scripts/check_token_regression.ts +9 -2
  223. package/src/scripts/condense.ts +155 -12
  224. package/src/scripts/cost/budget.mjs +63 -2
  225. package/src/scripts/count_scoped_projection.ts +104 -0
  226. package/src/scripts/evaluator_umbrella.sh +34 -1
  227. package/src/scripts/generate_capability_matrix.ts +8 -0
  228. package/src/scripts/generate_router_coverage_from_matrix.ts +155 -0
  229. package/src/scripts/hook_manifest.yaml +17 -3
  230. package/src/scripts/hooks/block_config_weakening.ts +337 -0
  231. package/src/scripts/hooks/concern_registry.ts +2 -0
  232. package/src/scripts/hooks/probe_throttle.ts +75 -0
  233. package/src/scripts/install-hooks.sh +24 -2
  234. package/src/scripts/install.ts +15 -92
  235. package/src/scripts/lint_agents_md.ts +9 -3
  236. package/src/scripts/lint_archived_skills.ts +1 -1
  237. package/src/scripts/lint_artefact_frontmatter.ts +185 -37
  238. package/src/scripts/lint_command_routing.ts +21 -52
  239. package/src/scripts/lint_command_verbs.ts +47 -28
  240. package/src/scripts/lint_context_spine_usage.ts +38 -12
  241. package/src/scripts/lint_framework_leakage.ts +25 -6
  242. package/src/scripts/lint_handoffs.ts +49 -2
  243. package/src/scripts/lint_media_policy_linkage.ts +62 -30
  244. package/src/scripts/lint_namespace.ts +92 -7
  245. package/src/scripts/lint_new_skill_gate.ts +39 -31
  246. package/src/scripts/lint_originality_shingles.ts +7 -1
  247. package/src/scripts/lint_override_kernel_guard.ts +21 -3
  248. package/src/scripts/lint_pack_boundaries.ts +461 -91
  249. package/src/scripts/lint_pack_dependencies.ts +24 -57
  250. package/src/scripts/lint_persona_governance.ts +1 -1
  251. package/src/scripts/lint_role_experiences.ts +34 -5
  252. package/src/scripts/lint_trigger_precision.ts +142 -0
  253. package/src/scripts/mcp_server/consumer_tool_catalog.json +12 -201
  254. package/src/scripts/mcp_server/tools.ts +1 -1
  255. package/src/scripts/prepack-check.mjs +40 -1
  256. package/src/scripts/prepack_router_targets.d.mts +7 -0
  257. package/src/scripts/prepack_router_targets.mjs +93 -0
  258. package/src/scripts/print_required_checks.ts +72 -18
  259. package/src/scripts/profile_staleness_hook.ts +12 -0
  260. package/src/scripts/record_evaluator_measurements.mjs +105 -0
  261. package/src/scripts/recruit_preflight.sh +1 -1
  262. package/src/scripts/release.ts +105 -1
  263. package/src/scripts/router_target_paths.d.mts +4 -0
  264. package/src/scripts/router_target_paths.mjs +69 -0
  265. package/src/scripts/router_telemetry.ts +52 -1
  266. package/src/scripts/routing_doctor.ts +485 -0
  267. package/src/scripts/rule_trigger_eval.ts +403 -0
  268. package/src/scripts/schemas/rule.schema.json +5 -1
  269. package/src/scripts/session_canary_hook.ts +96 -5
  270. package/src/scripts/skill_linter.ts +90 -42
  271. package/src/scripts/smoke/skills.sh +1 -1
  272. package/src/scripts/sweep_dead_scan_roots.ts +356 -10
  273. package/src/scripts/trigger_coverage.ts +13 -2
  274. package/src/scripts/update_counts.ts +91 -12
  275. package/src/scripts/validate_telegraph_carveouts.ts +1 -1
  276. package/src/scripts/wrapper_freshness_hook.ts +14 -0
  277. package/src/server/schemas/settings.ts +19 -5
  278. package/dist/cli-delegate/chunk-TJIAM5FW.js +0 -133
  279. package/dist/ui/assets/index-BpSQt5vm.js +0 -40
  280. package/dist/ui/assets/index-BpSQt5vm.js.map +0 -1
@@ -0,0 +1,403 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * rule_trigger_eval — RULES mode for the live LLM trigger-eval harness
4
+ * (road-to-tested-routing Phase 4, advisory only, never gating).
5
+ *
6
+ * Sibling of `skill_trigger_eval.ts` rather than a `--mode` flag on it: the
7
+ * skill CLI is a byte-for-byte port of the retired Python implementation
8
+ * whose contract ("mirror EXACTLY. No behaviour changes") is pinned by its
9
+ * own test suite, and its /dev/tty confirmation gate must stay untouched.
10
+ * This script REUSES its abstractions (SkillMeta shape, MockRouter,
11
+ * compute_metrics, _extract_field, the fetch routers) and adds only what is
12
+ * rules-specific:
13
+ *
14
+ * - catalog: rule id + frontmatter description from
15
+ * `dist/agent-src/rules/<id>.md`, ids from `dist/router.json`
16
+ * tier_1 + tier_2 (kernel excluded — kernel rules always load, there is
17
+ * nothing to route);
18
+ * - suite: derived from `tests/eval/routing-matrix/*.yaml` — each positive
19
+ * becomes a case expecting its rule to be consulted, each near-miss a
20
+ * case expecting it NOT to be; capped deterministically (FNV-1a hash
21
+ * order, no Math.random) so a live run stays affordable;
22
+ * - question: "which rules would you consult for this prompt", answered as
23
+ * the same `{"would_load": [...]}` JSON the routers already parse.
24
+ *
25
+ * Boundaries (mirroring `trigger_eval_rotation.ts`):
26
+ * - The local interactive `skill_trigger_eval` CLI and its /dev/tty
27
+ * confirmation gate are untouched — this script is the CI-only live path;
28
+ * live authorization derives exclusively from the key file the canary
29
+ * workflow materializes from repo secrets. No env-var key fallback.
30
+ * - `--dry-run` (MockRouter, no key, no spend) exercises the plumbing only;
31
+ * the advisory floor is REPORTED but never fails the run.
32
+ * - A live floor breach fails the SCHEDULED canary job only — PRs are never
33
+ * blocked by live results.
34
+ *
35
+ * Exit codes: 0 pass (always, in --dry-run) · 1 live floor breach ·
36
+ * 2 usage / IO error.
37
+ */
38
+ import * as fs from 'node:fs';
39
+ import * as path from 'node:path';
40
+ import { fileURLToPath, pathToFileURL } from 'node:url';
41
+
42
+ import { parse as parseYaml } from 'yaml';
43
+
44
+ import {
45
+ MockRouter,
46
+ _extract_field,
47
+ compute_metrics,
48
+ type Metrics,
49
+ type SkillMeta,
50
+ type TriggerRouter,
51
+ } from './skill_trigger_eval.js';
52
+ import { AnthropicFetchRouter, loadKeyFromFile } from './_lib/trigger_routers.js';
53
+ import { RULES_MODE_FLOOR } from './_lib/trigger_eval_floors.js';
54
+
55
+ const _HERE = path.dirname(fileURLToPath(import.meta.url));
56
+ const REPO_ROOT = path.resolve(_HERE, '..', '..');
57
+ const ROUTER_JSON = path.join(REPO_ROOT, 'dist', 'router.json');
58
+ const RULES_DIR = path.join(REPO_ROOT, 'dist', 'agent-src', 'rules');
59
+ const MATRIX_DIR = path.join(REPO_ROOT, 'tests', 'eval', 'routing-matrix');
60
+ const DEFAULT_OUT = path.join(
61
+ REPO_ROOT,
62
+ 'internal',
63
+ 'evals',
64
+ 'results',
65
+ 'rotation',
66
+ 'rules-mode.json',
67
+ );
68
+ export const DEFAULT_MAX_CASES = 40;
69
+ export const DEFAULT_RULES_MODEL = 'claude-haiku-4-5-20251001';
70
+
71
+ /** A rule catalogue entry is structurally a SkillMeta (name = rule id). */
72
+ export type RuleMeta = SkillMeta;
73
+
74
+ /** Router that may expose an async route (fetch-based live routers). */
75
+ type AsyncCapableRouter = TriggerRouter & {
76
+ routeAsync?: (query: string, skills: SkillMeta[]) => Promise<[string[], number, number]>;
77
+ };
78
+
79
+ export const RULES_PROMPT_HEADER = `You are a rule-routing oracle. Given the rule catalogue below
80
+ and a single user prompt, return ONLY the JSON object {"would_load": [...]}
81
+ listing the rule ids you would consult before acting on the prompt.
82
+
83
+ Rules:
84
+ - Use the rule description verbatim as the only routing signal.
85
+ - Return at most 6 rule ids.
86
+ - If no rule applies, return {"would_load": []}.
87
+ - Output ONLY the JSON. No prose, no code fences.
88
+
89
+ Rule catalogue (id :: description):
90
+ `;
91
+
92
+ // ── Catalog ──────────────────────────────────────────────────────────────
93
+
94
+ /**
95
+ * Build the rule catalogue: tier_1 + tier_2 ids from `dist/router.json`
96
+ * (kernel excluded — kernel always loads), description from the rule file's
97
+ * frontmatter. A rule whose file or description is missing is a build error:
98
+ * the projection guarantees both, so a gap means the inputs are stale.
99
+ */
100
+ export function build_rule_catalog(
101
+ routerJsonPath: string = ROUTER_JSON,
102
+ rulesDir: string = RULES_DIR,
103
+ ): RuleMeta[] {
104
+ const router = JSON.parse(fs.readFileSync(routerJsonPath, 'utf-8')) as {
105
+ tier_1?: Array<{ id?: unknown }>;
106
+ tier_2?: Array<{ id?: unknown }>;
107
+ };
108
+ const ids: string[] = [];
109
+ for (const tier of [router.tier_1, router.tier_2]) {
110
+ if (!Array.isArray(tier)) continue;
111
+ for (const entry of tier) {
112
+ if (typeof entry?.id === 'string' && entry.id) ids.push(entry.id);
113
+ }
114
+ }
115
+ ids.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
116
+ const catalog: RuleMeta[] = [];
117
+ for (const id of ids) {
118
+ const rulePath = path.join(rulesDir, `${id}.md`);
119
+ const text = fs.readFileSync(rulePath, 'utf-8');
120
+ if (!text.startsWith('---')) {
121
+ throw new Error(`${rulePath}: no frontmatter`);
122
+ }
123
+ const end = text.indexOf('\n---', 3);
124
+ if (end < 0) {
125
+ throw new Error(`${rulePath}: unterminated frontmatter`);
126
+ }
127
+ const description = _extract_field(text.slice(3, end), 'description');
128
+ if (description === null || description === '') {
129
+ throw new Error(`${rulePath}: frontmatter has no description`);
130
+ }
131
+ catalog.push({ name: id, description });
132
+ }
133
+ return catalog;
134
+ }
135
+
136
+ // ── Derived suite ────────────────────────────────────────────────────────
137
+
138
+ export interface RuleCase {
139
+ /** Rule id under test for this case. */
140
+ rule: string;
141
+ /** The user prompt sent to the router. */
142
+ q: string;
143
+ /** true = positive (rule must be consulted) · false = near-miss (must not). */
144
+ expected: boolean;
145
+ }
146
+
147
+ /** FNV-1a 32-bit — deterministic sampling key (no Math.random). */
148
+ export function fnv1a(s: string): number {
149
+ let h = 0x811c9dc5;
150
+ for (let i = 0; i < s.length; i += 1) {
151
+ h ^= s.charCodeAt(i);
152
+ h = Math.imul(h, 0x01000193) >>> 0;
153
+ }
154
+ return h >>> 0;
155
+ }
156
+
157
+ /**
158
+ * Derive the eval suite from the routing-matrix fixtures: positive →
159
+ * expected [rule], near_miss → expected []. Only rules present in the
160
+ * catalogue are scored. Capped at `maxCases` by sorting on the FNV-1a hash
161
+ * of `rule|kind|prompt` (prompt as tiebreak) and taking the head — a
162
+ * deterministic pseudo-shuffle that spreads the sample across rules and
163
+ * both case kinds instead of favouring the alphabet.
164
+ */
165
+ export function derive_rule_cases(
166
+ catalog: readonly RuleMeta[],
167
+ matrixDir: string = MATRIX_DIR,
168
+ maxCases: number = DEFAULT_MAX_CASES,
169
+ ): RuleCase[] {
170
+ const known = new Set(catalog.map((r) => r.name));
171
+ let files: string[];
172
+ try {
173
+ files = fs.readdirSync(matrixDir).filter((n) => n.endsWith('.yaml'));
174
+ } catch {
175
+ return [];
176
+ }
177
+ files.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
178
+ const cases: RuleCase[] = [];
179
+ for (const file of files) {
180
+ const doc = parseYaml(fs.readFileSync(path.join(matrixDir, file), 'utf-8')) as {
181
+ rule?: unknown;
182
+ positives?: Array<{ prompt?: unknown }>;
183
+ near_misses?: Array<{ prompt?: unknown }>;
184
+ } | null;
185
+ const rule = typeof doc?.rule === 'string' ? doc.rule : '';
186
+ if (!rule || !known.has(rule)) continue;
187
+ for (const [items, expected] of [
188
+ [doc?.positives, true],
189
+ [doc?.near_misses, false],
190
+ ] as const) {
191
+ if (!Array.isArray(items)) continue;
192
+ for (const item of items) {
193
+ if (typeof item?.prompt === 'string' && item.prompt.trim()) {
194
+ cases.push({ rule, q: item.prompt, expected });
195
+ }
196
+ }
197
+ }
198
+ }
199
+ cases.sort((a, b) => {
200
+ const ha = fnv1a(`${a.rule}|${a.expected ? 'pos' : 'near'}|${a.q}`);
201
+ const hb = fnv1a(`${b.rule}|${b.expected ? 'pos' : 'near'}|${b.q}`);
202
+ if (ha !== hb) return ha - hb;
203
+ return a.q < b.q ? -1 : a.q > b.q ? 1 : 0;
204
+ });
205
+ return cases.slice(0, Math.max(0, maxCases));
206
+ }
207
+
208
+ // ── Eval ─────────────────────────────────────────────────────────────────
209
+
210
+ export interface RuleCaseResult {
211
+ rule: string;
212
+ q: string;
213
+ expected: boolean;
214
+ observed: boolean;
215
+ consulted: string[];
216
+ passed: boolean;
217
+ }
218
+
219
+ export interface RuleEvalResult {
220
+ mode: 'rules';
221
+ model: string;
222
+ timestamp: string;
223
+ router: string;
224
+ catalogue_size: number;
225
+ cases: RuleCaseResult[];
226
+ metrics: Metrics;
227
+ input_tokens: number;
228
+ output_tokens: number;
229
+ }
230
+
231
+ /** Run the derived suite through a router (awaiting routeAsync when present). */
232
+ export async function run_rule_eval(
233
+ cases: readonly RuleCase[],
234
+ router: AsyncCapableRouter,
235
+ catalog: readonly RuleMeta[],
236
+ model: string,
237
+ ): Promise<RuleEvalResult> {
238
+ const result: RuleEvalResult = {
239
+ mode: 'rules',
240
+ model,
241
+ timestamp: new Date().toISOString().replace(/\.\d{3}Z$/, '+00:00'),
242
+ router: router.name,
243
+ catalogue_size: catalog.length,
244
+ cases: [],
245
+ metrics: compute_metrics([]),
246
+ input_tokens: 0,
247
+ output_tokens: 0,
248
+ };
249
+ for (const c of cases) {
250
+ const [consulted, inTok, outTok] =
251
+ typeof router.routeAsync === 'function'
252
+ ? await router.routeAsync(c.q, [...catalog])
253
+ : router.route(c.q, [...catalog]);
254
+ const observed = consulted.includes(c.rule);
255
+ result.cases.push({
256
+ rule: c.rule,
257
+ q: c.q,
258
+ expected: c.expected,
259
+ observed,
260
+ consulted: [...consulted].sort(),
261
+ passed: observed === c.expected,
262
+ });
263
+ result.input_tokens += inTok;
264
+ result.output_tokens += outTok;
265
+ }
266
+ result.metrics = compute_metrics(
267
+ result.cases.map((c) => ({
268
+ q: c.q,
269
+ expected: c.expected,
270
+ observed: c.observed,
271
+ loaded_skills: c.consulted,
272
+ passed: c.passed,
273
+ })),
274
+ );
275
+ return result;
276
+ }
277
+
278
+ // ── CLI ──────────────────────────────────────────────────────────────────
279
+
280
+ export interface RuleEvalOptions {
281
+ dryRun?: boolean;
282
+ model?: string;
283
+ out?: string;
284
+ maxCases?: number;
285
+ /** Injectable router (tests). Default: MockRouter (dry) / AnthropicFetchRouter (live). */
286
+ router?: AsyncCapableRouter;
287
+ routerJsonPath?: string;
288
+ rulesDir?: string;
289
+ matrixDir?: string;
290
+ }
291
+
292
+ export interface RuleEvalSummary {
293
+ result: RuleEvalResult;
294
+ dry_run: boolean;
295
+ floor_passed: boolean;
296
+ out: string;
297
+ }
298
+
299
+ export async function run_rules_mode(opts: RuleEvalOptions = {}): Promise<RuleEvalSummary> {
300
+ const dryRun = opts.dryRun ?? false;
301
+ const model = opts.model ?? DEFAULT_RULES_MODEL;
302
+ const out = opts.out ?? DEFAULT_OUT;
303
+ const catalog = build_rule_catalog(opts.routerJsonPath, opts.rulesDir);
304
+ const cases = derive_rule_cases(catalog, opts.matrixDir, opts.maxCases ?? DEFAULT_MAX_CASES);
305
+ if (cases.length === 0) {
306
+ throw new Error('rules mode: derived suite is empty (no routing-matrix cases)');
307
+ }
308
+ const router: AsyncCapableRouter =
309
+ opts.router ??
310
+ (dryRun
311
+ ? new MockRouter((q, rules) =>
312
+ rules.filter((r) => q.toLowerCase().includes(r.name)).map((r) => r.name),
313
+ )
314
+ : (new AnthropicFetchRouter({
315
+ apiKey: loadKeyFromFile('anthropic.key'),
316
+ model,
317
+ promptHeader: RULES_PROMPT_HEADER,
318
+ }) as AsyncCapableRouter));
319
+
320
+ const result = await run_rule_eval(cases, router, catalog, model);
321
+ const floorPassed =
322
+ result.metrics.precision >= RULES_MODE_FLOOR.minPrecision &&
323
+ result.metrics.recall >= RULES_MODE_FLOOR.minRecall;
324
+
325
+ fs.mkdirSync(path.dirname(out), { recursive: true });
326
+ fs.writeFileSync(out, JSON.stringify(result, null, 2) + '\n', 'utf-8');
327
+
328
+ return { result, dry_run: dryRun, floor_passed: floorPassed, out };
329
+ }
330
+
331
+ function parse_args(argv: string[]): RuleEvalOptions & { help?: boolean } {
332
+ const out: RuleEvalOptions & { help?: boolean } = {};
333
+ for (let i = 0; i < argv.length; i += 1) {
334
+ const a = argv[i] as string;
335
+ const take = (): string => {
336
+ const v = argv[++i];
337
+ if (v === undefined) {
338
+ process.stderr.write(`rule_trigger_eval: error: argument ${a}: expected one argument\n`);
339
+ process.exit(2);
340
+ }
341
+ return v as string;
342
+ };
343
+ if (a === '--dry-run') out.dryRun = true;
344
+ else if (a === '--model') out.model = take();
345
+ else if (a === '--out') out.out = path.resolve(take());
346
+ else if (a === '--max-cases') out.maxCases = Number(take());
347
+ else if (a === '-h' || a === '--help') out.help = true;
348
+ else {
349
+ process.stderr.write(`rule_trigger_eval: error: unrecognized arguments: ${a}\n`);
350
+ process.exit(2);
351
+ }
352
+ }
353
+ if (out.maxCases !== undefined && (!Number.isInteger(out.maxCases) || out.maxCases <= 0)) {
354
+ process.stderr.write('rule_trigger_eval: error: --max-cases must be a positive integer\n');
355
+ process.exit(2);
356
+ }
357
+ return out;
358
+ }
359
+
360
+ export async function main(argv?: string[]): Promise<number> {
361
+ const opts = parse_args(argv ?? process.argv.slice(2));
362
+ if (opts.help) {
363
+ process.stdout.write(
364
+ 'usage: rule_trigger_eval [--dry-run] [--model MODEL] [--out FILE]\n' +
365
+ ' [--max-cases N]\n',
366
+ );
367
+ return 0;
368
+ }
369
+ let summary: RuleEvalSummary;
370
+ try {
371
+ summary = await run_rules_mode(opts);
372
+ } catch (err) {
373
+ process.stderr.write(`❌ rule_trigger_eval: ${err instanceof Error ? err.message : String(err)}\n`);
374
+ return 2;
375
+ }
376
+ const m = summary.result.metrics;
377
+ process.stdout.write(
378
+ `rules-mode trigger eval · ${summary.result.cases.length} cases over ` +
379
+ `${summary.result.catalogue_size} rules (kernel excluded)` +
380
+ `${summary.dry_run ? ' · DRY-RUN (floor advisory)' : ''}\n` +
381
+ ` precision ${m.precision} (floor ${RULES_MODE_FLOOR.minPrecision}) · ` +
382
+ `recall ${m.recall} (floor ${RULES_MODE_FLOOR.minRecall}) · ${summary.out}\n`,
383
+ );
384
+ for (const c of summary.result.cases) {
385
+ if (c.passed) continue;
386
+ process.stdout.write(
387
+ ` ❌ [${c.expected ? 'FN' : 'FP'}] ${c.rule} :: ${c.q}\n`,
388
+ );
389
+ }
390
+ if (!summary.floor_passed && !summary.dry_run) {
391
+ process.stderr.write(
392
+ '❌ rules-mode metrics below advisory floor — fails the scheduled canary job only, never a PR.\n',
393
+ );
394
+ return 1;
395
+ }
396
+ process.stdout.write('✅ rules-mode eval complete\n');
397
+ return 0;
398
+ }
399
+
400
+ const argvUrl = process.argv[1] === undefined ? '' : pathToFileURL(path.resolve(process.argv[1])).href;
401
+ if (import.meta.url === argvUrl) {
402
+ main().then((code) => process.exit(code));
403
+ }
@@ -74,7 +74,11 @@
74
74
  "routes_to": {
75
75
  "type": "array",
76
76
  "items": {"type": "string", "pattern": "^(skill|guideline|command|contract):"},
77
- "description": "Router targets (skill / guideline / command / contract). Forbidden on kernel rules. Schema: docs/contracts/rule-router.md."
77
+ "description": "Router targets (skill / guideline / command / contract). Forbidden on kernel rules. Non-kernel rules declare either routes_to or self_contained: true. Schema: docs/contracts/rule-router.md."
78
+ },
79
+ "self_contained": {
80
+ "type": "boolean",
81
+ "description": "Certifies a non-kernel rule whose body IS the constraint — a prohibition, gate, or output-format law with no procedure to delegate — so it legitimately declares no routes_to. May offload detail via load_context. Per-rule certification rationale lives in ADR-210's appendix; the linter errors when a non-kernel rule declares neither routes_to nor this marker. Schema: docs/contracts/rule-router.md."
78
82
  },
79
83
  "applies_to_user_types": {
80
84
  "type": "array",
@@ -19,14 +19,25 @@
19
19
  * new conversation on hook-capable hosts. Copilot (no hook surface) falls back
20
20
  * to the rule alone.
21
21
  *
22
- * Gate: `personal.canary_name` non-empty. Missing settings file, missing key,
23
- * or empty value → clean no-op (exit 0, no stdout). Never blocks
22
+ * Gate + name resolution (first non-empty wins — the canary is a PERSONAL,
23
+ * user-global concern; the project file is only an override):
24
+ *
25
+ * 1. `<workspace_root>/.agent-settings.yml` → `personal.canary_name`
26
+ * 2. `<event4u_root>/settings/.agent-settings.yml` → `personal.canary_name`
27
+ * (the wizard-managed user-global settings; legacy XDG root read as
28
+ * fallback via `user_global_paths.resolve_with_fallback`)
29
+ * 3. `<event4u_root>/settings/.agent-user.yml` → `identity.name`
30
+ * (the global user identity the setup wizard already collects — no
31
+ * per-project duplication of the name)
32
+ *
33
+ * No name anywhere → clean no-op (exit 0, no stdout). Never blocks
24
34
  * (fail_closed: false).
25
35
  */
26
36
  import fs from "node:fs";
27
37
  import path from "node:path";
28
38
  import { fileURLToPath, pathToFileURL } from "node:url";
29
39
  import { readHookStdin } from "./hooks/hook_stdin.js";
40
+ import { resolve_with_fallback } from "./_lib/user_global_paths.js";
30
41
 
31
42
  const EXIT_ALLOW = 0;
32
43
 
@@ -73,6 +84,86 @@ export function read_canary_name(settings_path: string): string | null {
73
84
  return null;
74
85
  }
75
86
 
87
+ /**
88
+ * Line-walker for `identity.name` in the wizard's user-global
89
+ * `settings/.agent-user.yml` — same no-YAML-dependency approach as
90
+ * `read_canary_name`. Returns the raw scalar or null when absent.
91
+ */
92
+ export function read_identity_name(user_yml_path: string): string | null {
93
+ let text: string;
94
+ try {
95
+ if (!fs.statSync(user_yml_path).isFile()) {
96
+ return null;
97
+ }
98
+ text = fs.readFileSync(user_yml_path, "utf-8");
99
+ } catch {
100
+ return null;
101
+ }
102
+ let in_identity = false;
103
+ for (const line of text.split(/\r?\n/)) {
104
+ if (/^identity\s*:\s*(?:#.*)?$/.test(line)) {
105
+ in_identity = true;
106
+ continue;
107
+ }
108
+ if (in_identity && /^\S/.test(line)) {
109
+ in_identity = false; // left the identity: block
110
+ }
111
+ if (in_identity) {
112
+ const m = /^\s+name\s*:\s*(.*?)\s*(?:#.*)?$/.exec(line);
113
+ if (m && m[1] !== undefined) {
114
+ return m[1].trim();
115
+ }
116
+ }
117
+ }
118
+ return null;
119
+ }
120
+
121
+ /**
122
+ * Resolve the canary name across the three layers (project override →
123
+ * user-global settings → user-global identity). First non-empty wins;
124
+ * an empty scalar at a layer means "not set here", not "feature off",
125
+ * so the walk continues to the next layer.
126
+ */
127
+ /** Which settings layer decided the canary name — surfaced verbatim in the
128
+ * hook's `reason` so `routing:doctor` can answer "why did/didn't this gate
129
+ * fire" with the deciding layer named (road-to-tested-routing Phase 1). */
130
+ export type CanaryLayer =
131
+ | "project override"
132
+ | "user-global settings"
133
+ | "user-global identity"
134
+ | "none";
135
+
136
+ export interface CanaryResolution {
137
+ name: string;
138
+ layer: CanaryLayer;
139
+ }
140
+
141
+ export function resolve_canary_name(workspace_root: string): CanaryResolution {
142
+ const project = sanitize_name(
143
+ read_canary_name(path.join(workspace_root, SETTINGS_FILE)),
144
+ );
145
+ if (project) {
146
+ return { name: project, layer: "project override" };
147
+ }
148
+ const global_settings = resolve_with_fallback(
149
+ path.join("settings", ".agent-settings.yml"),
150
+ );
151
+ if (global_settings) {
152
+ const name = sanitize_name(read_canary_name(global_settings));
153
+ if (name) {
154
+ return { name, layer: "user-global settings" };
155
+ }
156
+ }
157
+ const global_user = resolve_with_fallback(path.join("settings", ".agent-user.yml"));
158
+ if (global_user) {
159
+ const name = sanitize_name(read_identity_name(global_user));
160
+ if (name) {
161
+ return { name, layer: "user-global identity" };
162
+ }
163
+ }
164
+ return { name: "", layer: "none" };
165
+ }
166
+
76
167
  /**
77
168
  * The name is user-owned config, but it lands verbatim in model context —
78
169
  * strip quotes, collapse whitespace, drop markup-significant characters, and
@@ -135,15 +226,15 @@ export function main(): number {
135
226
  }
136
227
 
137
228
  const root = _workspaceRoot(env);
138
- const name = sanitize_name(read_canary_name(path.join(root, SETTINGS_FILE)));
229
+ const { name, layer } = resolve_canary_name(root);
139
230
  if (!name) {
140
- return EXIT_ALLOW; // canary not configured — clean no-op
231
+ return EXIT_ALLOW; // canary not configured on any layer — clean no-op
141
232
  }
142
233
 
143
234
  process.stdout.write(
144
235
  `${JSON.stringify({
145
236
  decision: "allow",
146
- reason: `session-canary: active for "${name}"`,
237
+ reason: `session-canary: active for "${name}" (${layer})`,
147
238
  context: build_canary_block(name),
148
239
  })}\n`,
149
240
  );