@event4u/agent-config 14.11.0 → 14.12.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 (350) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +156 -538
  5. package/README.md +1 -1
  6. package/dist/agent-src/contexts/execution/evidence-discipline.md +26 -0
  7. package/dist/agent-src/skills/code-refactoring/SKILL.md +45 -1
  8. package/dist/agent-src/skills/code-review/SKILL.md +65 -6
  9. package/dist/agent-src/skills/command-writing/SKILL.md +35 -0
  10. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +8 -1
  11. package/dist/agent-src/skills/project-analysis-core/SKILL.md +23 -0
  12. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +29 -12
  13. package/dist/agent-src/skills/skill-writing/references/procedure.md +24 -2
  14. package/dist/agent-src/skills/storybook-workshop/SKILL.md +9 -0
  15. package/dist/agent-src/skills/ui-component-architect/SKILL.md +8 -0
  16. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  17. package/dist/cli-delegate/{chunk-L43NCYPB.js → chunk-JCW7ZLHK.js} +5 -0
  18. package/dist/cli-delegate/{chunk-236PNQZK.js → chunk-JJA2QEWA.js} +1 -1
  19. package/dist/cli-delegate/cmd_export.js +2 -2
  20. package/dist/cli-delegate/cmd_prune.js +2 -2
  21. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  22. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  23. package/dist/cli-delegate/cmd_sync.js +2 -2
  24. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  25. package/dist/cli-delegate/cmd_validate.js +2 -2
  26. package/dist/discovery/deprecation-report.md +1 -1
  27. package/dist/discovery/discovery-manifest.json +10 -10
  28. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  29. package/dist/discovery/discovery-manifest.summary.md +1 -1
  30. package/dist/discovery/dormancy-report.md +1 -1
  31. package/dist/discovery/orphan-report.md +1 -1
  32. package/dist/discovery/packs.json +2 -2
  33. package/dist/discovery/trust-report.md +1 -1
  34. package/dist/discovery/workspaces.json +2 -2
  35. package/dist/hooks/dispatch.js +1081 -879
  36. package/dist/install/install.mjs +5 -0
  37. package/dist/mcp/registry-manifest.json +2 -2
  38. package/dist/mcp/server.json +2 -2
  39. package/dist/server/schemas/settings.js +3 -0
  40. package/docs/contracts/settings-classes.md +24 -4
  41. package/docs/guidelines/agent-infra/downstream-changes-mechanics.md +91 -0
  42. package/docs/guidelines/code-clarity.md +21 -0
  43. package/docs/guidelines/redundancy-taxonomy.md +180 -0
  44. package/package.json +6 -2
  45. package/src/config/agent-settings.template.yml +16 -0
  46. package/src/config/budgets.yml +19 -0
  47. package/src/config/ci-local-parity.yml +15 -0
  48. package/src/config/discovery/prompt-patterns.yml +56 -0
  49. package/src/config/estate-count-budget.json +8 -3
  50. package/src/config/gate-coverage.yml +191 -6
  51. package/src/config/gate-violation-baselines.json +3 -3
  52. package/src/config/pack-size-budget.json +39 -12
  53. package/src/config/preamble-payload-budget.json +97 -71
  54. package/src/config/routing-coverage-seed.json +29 -0
  55. package/src/config/suggestion-capture.json +28 -0
  56. package/src/config/surface-matrix.yml +21 -2
  57. package/src/config/web-launch-readiness.json +188 -0
  58. package/src/scripts/_lib/gate_baseline.ts +150 -1
  59. package/src/scripts/_lib/skill_estate.ts +115 -0
  60. package/src/scripts/ai_council/argument_exhaustion.ts +130 -0
  61. package/src/scripts/check_bundle_path_leakage.ts +162 -7
  62. package/src/scripts/check_command_examples.ts +557 -0
  63. package/src/scripts/check_estate_count.ts +122 -3
  64. package/src/scripts/check_preamble_payload_budget.ts +30 -3
  65. package/src/scripts/check_routing_coverage.ts +352 -0
  66. package/src/scripts/check_score_contract.ts +505 -0
  67. package/src/scripts/check_skill_admissions.ts +514 -0
  68. package/src/scripts/check_web_launch_readiness.ts +705 -0
  69. package/src/scripts/description_route_check.ts +494 -0
  70. package/src/scripts/hook_manifest.json +1 -1
  71. package/src/scripts/hook_manifest.yaml +29 -2
  72. package/src/scripts/hooks/concern_registry.ts +2 -0
  73. package/src/scripts/hooks/suggestion_capture_hook.ts +368 -0
  74. package/src/scripts/lint_budget_ownership.ts +54 -4
  75. package/src/scripts/lint_skill_trigger_corpus.ts +349 -0
  76. package/src/scripts/pack_install_smoke.ts +363 -0
  77. package/src/scripts/pr_conflict_census.ts +187 -0
  78. package/src/scripts/report_pack_delta.ts +204 -0
  79. package/src/scripts/select_analysis_mode.ts +112 -0
  80. package/src/scripts/sync_pr_branch.ts +40 -1
  81. package/src/server/schemas/settings.ts +5 -0
  82. package/dist/agent-src/skills/accessibility-auditor/evals/evals.json +0 -15
  83. package/dist/agent-src/skills/agent-security-review/evals/triggers.json +0 -52
  84. package/dist/agent-src/skills/ai-code-blindspots/evals/triggers.json +0 -52
  85. package/dist/agent-src/skills/alerting-doctrine/evals/triggers.json +0 -17
  86. package/dist/agent-src/skills/analysis-skill-router/evals/evals.json +0 -13
  87. package/dist/agent-src/skills/brand/evals/triggers.json +0 -17
  88. package/dist/agent-src/skills/brand-asset-generation/evals/triggers.json +0 -17
  89. package/dist/agent-src/skills/brand-audit/evals/triggers.json +0 -17
  90. package/dist/agent-src/skills/brand-identity/evals/triggers.json +0 -17
  91. package/dist/agent-src/skills/brand-strategy/evals/triggers.json +0 -17
  92. package/dist/agent-src/skills/brand-to-tokens/evals/triggers.json +0 -17
  93. package/dist/agent-src/skills/bug-analyzer/evals/evals.json +0 -13
  94. package/dist/agent-src/skills/canvas-design/evals/triggers.json +0 -17
  95. package/dist/agent-src/skills/character-consistency/evals/evals.json +0 -16
  96. package/dist/agent-src/skills/code-intelligence/evals/triggers.json +0 -52
  97. package/dist/agent-src/skills/code-review/evals/evals.json +0 -100
  98. package/dist/agent-src/skills/command-routing/evals/evals.json +0 -14
  99. package/dist/agent-src/skills/complexity-first-planning/evals/triggers.json +0 -17
  100. package/dist/agent-src/skills/contract-review/evals/triggers.json +0 -17
  101. package/dist/agent-src/skills/conventional-commits-writing/evals/evals.json +0 -15
  102. package/dist/agent-src/skills/corpus-grounding/evals/triggers.json +0 -17
  103. package/dist/agent-src/skills/dashboard-design/evals/evals.json +0 -16
  104. package/dist/agent-src/skills/dcf-modeling/evals/domain-truth.json +0 -29
  105. package/dist/agent-src/skills/dcf-modeling/evals/evals.json +0 -20
  106. package/dist/agent-src/skills/decision-record/evals/evals.json +0 -20
  107. package/dist/agent-src/skills/decision-record/evals/triggers.json +0 -87
  108. package/dist/agent-src/skills/design-intelligence/evals/evals.json +0 -14
  109. package/dist/agent-src/skills/design-intelligence/evals/triggers.json +0 -17
  110. package/dist/agent-src/skills/design-system-capture/evals/evals.json +0 -15
  111. package/dist/agent-src/skills/design-tokens/evals/triggers.json +0 -17
  112. package/dist/agent-src/skills/design-variations/evals/triggers.json +0 -17
  113. package/dist/agent-src/skills/developer-like-execution/evals/evals.json +0 -16
  114. package/dist/agent-src/skills/doc-coauthoring/evals/evals.json +0 -16
  115. package/dist/agent-src/skills/doc-coauthoring/evals/triggers.json +0 -17
  116. package/dist/agent-src/skills/docx-authoring/evals/triggers.json +0 -52
  117. package/dist/agent-src/skills/dpa-review/evals/triggers.json +0 -17
  118. package/dist/agent-src/skills/editorial-calendar/evals/domain-truth.json +0 -16
  119. package/dist/agent-src/skills/editorial-calendar/evals/evals.json +0 -16
  120. package/dist/agent-src/skills/eloquent/evals/triggers.json +0 -17
  121. package/dist/agent-src/skills/estimate-ticket/evals/evals.json +0 -16
  122. package/dist/agent-src/skills/estimate-ticket/evals/output-schema.yml +0 -20
  123. package/dist/agent-src/skills/estimate-ticket/evals/triggers.json +0 -19
  124. package/dist/agent-src/skills/evaluate-llm-feature/evals/triggers.json +0 -52
  125. package/dist/agent-src/skills/existing-ui-audit/evals/triggers.json +0 -47
  126. package/dist/agent-src/skills/experiment-loop/evals/output-schema.yml +0 -27
  127. package/dist/agent-src/skills/experiment-loop/evals/triggers.json +0 -17
  128. package/dist/agent-src/skills/fe-design/evals/triggers.json +0 -67
  129. package/dist/agent-src/skills/forecasting/evals/domain-truth.json +0 -29
  130. package/dist/agent-src/skills/forecasting/evals/evals.json +0 -20
  131. package/dist/agent-src/skills/forensics-report/evals/triggers.json +0 -17
  132. package/dist/agent-src/skills/frontend-render-security/evals/triggers.json +0 -52
  133. package/dist/agent-src/skills/fundraising-narrative/evals/domain-truth.json +0 -16
  134. package/dist/agent-src/skills/fundraising-narrative/evals/evals.json +0 -16
  135. package/dist/agent-src/skills/gated-reach/evals/triggers.json +0 -72
  136. package/dist/agent-src/skills/github-ci/evals/evals.json +0 -25
  137. package/dist/agent-src/skills/history-design/evals/triggers.json +0 -17
  138. package/dist/agent-src/skills/html-deck/evals/triggers.json +0 -17
  139. package/dist/agent-src/skills/humanizer/evals/strip_fixtures.json +0 -57
  140. package/dist/agent-src/skills/humanizer/evals/triggers.json +0 -17
  141. package/dist/agent-src/skills/iconography/evals/triggers.json +0 -17
  142. package/dist/agent-src/skills/image-analyser/evals/triggers.json +0 -17
  143. package/dist/agent-src/skills/image-creator/evals/triggers.json +0 -17
  144. package/dist/agent-src/skills/image-editing/evals/triggers.json +0 -17
  145. package/dist/agent-src/skills/image-generation/evals/triggers.json +0 -17
  146. package/dist/agent-src/skills/image-provider-routing/evals/triggers.json +0 -17
  147. package/dist/agent-src/skills/incident-commander/evals/domain-truth.json +0 -16
  148. package/dist/agent-src/skills/incident-commander/evals/evals.json +0 -20
  149. package/dist/agent-src/skills/js-library-packaging/evals/triggers.json +0 -17
  150. package/dist/agent-src/skills/judge-artifact-completeness/evals/triggers.json +0 -17
  151. package/dist/agent-src/skills/judge-injection-defense/evals/triggers.json +0 -17
  152. package/dist/agent-src/skills/judge-synthesis/evals/triggers.json +0 -17
  153. package/dist/agent-src/skills/launch-readiness/evals/evals.json +0 -21
  154. package/dist/agent-src/skills/learning-tutor/evals/triggers.json +0 -52
  155. package/dist/agent-src/skills/legal-intake-triage/evals/triggers.json +0 -17
  156. package/dist/agent-src/skills/legal-practice-profile/evals/triggers.json +0 -17
  157. package/dist/agent-src/skills/license-compliance-audit/evals/triggers.json +0 -52
  158. package/dist/agent-src/skills/license-compliance-borrow-check/evals/triggers.json +0 -52
  159. package/dist/agent-src/skills/license-compliance-credits/evals/triggers.json +0 -52
  160. package/dist/agent-src/skills/llm-provider-knowledge/evals/triggers.json +0 -20
  161. package/dist/agent-src/skills/logging-monitoring/evals/evals.json +0 -16
  162. package/dist/agent-src/skills/logo-generation/evals/triggers.json +0 -17
  163. package/dist/agent-src/skills/messaging-architecture/evals/domain-truth.json +0 -16
  164. package/dist/agent-src/skills/messaging-architecture/evals/evals.json +0 -16
  165. package/dist/agent-src/skills/monorepo-workspace/evals/triggers.json +0 -17
  166. package/dist/agent-src/skills/nda-triage/evals/triggers.json +0 -17
  167. package/dist/agent-src/skills/operational-readiness/evals/triggers.json +0 -17
  168. package/dist/agent-src/skills/pdf-tools/evals/triggers.json +0 -52
  169. package/dist/agent-src/skills/perf-feedback-craft/evals/evals.json +0 -16
  170. package/dist/agent-src/skills/persona-improvement/evals/triggers.json +0 -52
  171. package/dist/agent-src/skills/php-coder/evals/triggers.json +0 -17
  172. package/dist/agent-src/skills/prediction-pool-optimizer/evals/triggers.json +0 -21
  173. package/dist/agent-src/skills/prompt-engineering-image/evals/triggers.json +0 -17
  174. package/dist/agent-src/skills/prompt-engineering-patterns/evals/evals.json +0 -16
  175. package/dist/agent-src/skills/prompt-validator/evals/triggers.json +0 -17
  176. package/dist/agent-src/skills/quality-tools/evals/evals.json +0 -15
  177. package/dist/agent-src/skills/reasoning-orchestrator/evals/triggers.json +0 -17
  178. package/dist/agent-src/skills/refine-prompt/evals/evals.json +0 -16
  179. package/dist/agent-src/skills/refine-ticket/evals/evals.json +0 -16
  180. package/dist/agent-src/skills/refine-ticket/evals/output-schema.yml +0 -16
  181. package/dist/agent-src/skills/refine-ticket/evals/triggers.json +0 -17
  182. package/dist/agent-src/skills/release-comms/evals/evals.json +0 -16
  183. package/dist/agent-src/skills/rice-prioritization/evals/evals.json +0 -20
  184. package/dist/agent-src/skills/runway-cognition/evals/domain-truth.json +0 -29
  185. package/dist/agent-src/skills/runway-cognition/evals/evals.json +0 -20
  186. package/dist/agent-src/skills/scenario-modeling/evals/domain-truth.json +0 -29
  187. package/dist/agent-src/skills/scenario-modeling/evals/evals.json +0 -20
  188. package/dist/agent-src/skills/schema-review/evals/triggers.json +0 -17
  189. package/dist/agent-src/skills/screenshot-hygiene/evals/triggers.json +0 -52
  190. package/dist/agent-src/skills/security-maturity-assessment/evals/triggers.json +0 -52
  191. package/dist/agent-src/skills/server-hardening/evals/triggers.json +0 -17
  192. package/dist/agent-src/skills/skill-writing/evals/triggers.json +0 -17
  193. package/dist/agent-src/skills/spreadsheet-authoring/evals/triggers.json +0 -47
  194. package/dist/agent-src/skills/storybook-workshop/evals/triggers.json +0 -17
  195. package/dist/agent-src/skills/subagent-orchestration/evals/evals.json +0 -60
  196. package/dist/agent-src/skills/supply-chain-intake/evals/triggers.json +0 -52
  197. package/dist/agent-src/skills/systematic-debugging/evals/evals.json +0 -20
  198. package/dist/agent-src/skills/test-case-discovery/evals/triggers.json +0 -52
  199. package/dist/agent-src/skills/test-driven-development/evals/evals.json +0 -16
  200. package/dist/agent-src/skills/threat-modeling/evals/evals.json +0 -21
  201. package/dist/agent-src/skills/typography-system/evals/evals.json +0 -14
  202. package/dist/agent-src/skills/typography-system/evals/triggers.json +0 -17
  203. package/dist/agent-src/skills/unit-economics-modeling/evals/domain-truth.json +0 -40
  204. package/dist/agent-src/skills/unit-economics-modeling/evals/evals.json +0 -24
  205. package/dist/agent-src/skills/verify-completion-evidence/evals/evals.json +0 -16
  206. package/dist/agent-src/skills/verify-repair-loop/evals/output-schema.yml +0 -20
  207. package/dist/agent-src/skills/verify-repair-loop/evals/triggers.json +0 -17
  208. package/dist/agent-src/skills/vision-articulation/evals/evals.json +0 -16
  209. package/dist/agent-src/skills/voice-and-tone-design/evals/evals.json +0 -16
  210. package/dist/agent-src/skills/wireframe/evals/triggers.json +0 -17
  211. package/dist/agent-src/skills/workspace-link/evals/triggers.json +0 -17
  212. package/dist/agent-src/skills/worktree-lifecycle/evals/evals.json +0 -38
  213. package/dist/agent-src/skills/worktree-lifecycle/evals/triggers.json +0 -52
  214. package/dist/cli/agent-config.js.map +0 -1
  215. package/dist/cli/bash/runBash.js.map +0 -1
  216. package/dist/cli/commands/commands.js.map +0 -1
  217. package/dist/cli/commands/config.js.map +0 -1
  218. package/dist/cli/commands/doctorShell.js.map +0 -1
  219. package/dist/cli/commands/mcpServer.js.map +0 -1
  220. package/dist/cli/commands/packs.js.map +0 -1
  221. package/dist/cli/commands/profiles.js.map +0 -1
  222. package/dist/cli/commands/recordTriggerEval.js.map +0 -1
  223. package/dist/cli/commands/rtkDetect.js.map +0 -1
  224. package/dist/cli/commands/settings.js.map +0 -1
  225. package/dist/cli/commands/uiAudit.js.map +0 -1
  226. package/dist/cli/commands/uiRender.js.map +0 -1
  227. package/dist/cli/commands/uiServe.js.map +0 -1
  228. package/dist/cli/commands/versions.js.map +0 -1
  229. package/dist/cli/commands/workspaces.js.map +0 -1
  230. package/dist/cli/configRoot.js.map +0 -1
  231. package/dist/cli/discovery/loadManifest.js.map +0 -1
  232. package/dist/cli/firstRunNotice.js.map +0 -1
  233. package/dist/cli/help.js.map +0 -1
  234. package/dist/cli/initRouting.js.map +0 -1
  235. package/dist/cli/log/logger.js.map +0 -1
  236. package/dist/cli/main.js.map +0 -1
  237. package/dist/cli/mcp/content.js.map +0 -1
  238. package/dist/cli/mcp/dispatch.js.map +0 -1
  239. package/dist/cli/mcp/stdio.js.map +0 -1
  240. package/dist/cli/paths.js.map +0 -1
  241. package/dist/cli/python/knowledge_ingest.js.map +0 -1
  242. package/dist/cli/python/workspace_analytics.js.map +0 -1
  243. package/dist/cli/python/workspace_crypto.js.map +0 -1
  244. package/dist/cli/python/workspace_documents.js.map +0 -1
  245. package/dist/cli/python/workspace_drive.js.map +0 -1
  246. package/dist/cli/python/workspace_drive_health.js.map +0 -1
  247. package/dist/cli/python/workspace_explain.js.map +0 -1
  248. package/dist/cli/python/workspace_hosts.js.map +0 -1
  249. package/dist/cli/python/workspace_inbox.js.map +0 -1
  250. package/dist/cli/python/workspace_render.js.map +0 -1
  251. package/dist/cli/python/workspace_roles.js.map +0 -1
  252. package/dist/cli/python/workspace_secrets.js.map +0 -1
  253. package/dist/cli/python/workspace_sessions.js.map +0 -1
  254. package/dist/cli/python/workspace_skills.js.map +0 -1
  255. package/dist/cli/registry.js.map +0 -1
  256. package/dist/install/agentSwitchDetection.js.map +0 -1
  257. package/dist/install/agentSwitchProfile.js.map +0 -1
  258. package/dist/install/atomic.js.map +0 -1
  259. package/dist/install/claudePathsPlan.js.map +0 -1
  260. package/dist/install/conflict.js.map +0 -1
  261. package/dist/install/detect.js.map +0 -1
  262. package/dist/install/emit_host_rules_cli.js.map +0 -1
  263. package/dist/install/globalRuleLayers.js.map +0 -1
  264. package/dist/install/hostLayerFingerprint.js.map +0 -1
  265. package/dist/install/partitionEligibility.js.map +0 -1
  266. package/dist/install/paths.js.map +0 -1
  267. package/dist/install/plan.js.map +0 -1
  268. package/dist/install/preflight.js.map +0 -1
  269. package/dist/install/rtkDetection.js.map +0 -1
  270. package/dist/install/ruleInScope.js.map +0 -1
  271. package/dist/install/ruleLayerPartition.js.map +0 -1
  272. package/dist/install/rule_scope.js.map +0 -1
  273. package/dist/install/rule_scope_cli.js.map +0 -1
  274. package/dist/install/selectedTools.js.map +0 -1
  275. package/dist/install/toolDetection.js.map +0 -1
  276. package/dist/install/txlog.js.map +0 -1
  277. package/dist/install/types.js.map +0 -1
  278. package/dist/install/wizard-plan.js.map +0 -1
  279. package/dist/install/wizardDismissals.js.map +0 -1
  280. package/dist/scripts/_lib/agent_settings.js.map +0 -1
  281. package/dist/scripts/_lib/agent_src.js.map +0 -1
  282. package/dist/scripts/_lib/claude_builtin_names.js.map +0 -1
  283. package/dist/scripts/_lib/claude_plugin.js.map +0 -1
  284. package/dist/scripts/_lib/claude_settings_hooks.js.map +0 -1
  285. package/dist/scripts/_lib/compile_time_toggles.js.map +0 -1
  286. package/dist/scripts/_lib/environment_detector.js.map +0 -1
  287. package/dist/scripts/_lib/host_launch.js.map +0 -1
  288. package/dist/scripts/_lib/install_layout.js.map +0 -1
  289. package/dist/scripts/_lib/installed_lock.js.map +0 -1
  290. package/dist/scripts/_lib/lean_projection_mode.js.map +0 -1
  291. package/dist/scripts/_lib/model_tier.js.map +0 -1
  292. package/dist/scripts/_lib/prune_empty_dirs.js.map +0 -1
  293. package/dist/scripts/_lib/scoped_projection.js.map +0 -1
  294. package/dist/scripts/_lib/script_output.js.map +0 -1
  295. package/dist/scripts/_lib/spawn_env.js.map +0 -1
  296. package/dist/scripts/_lib/surface_tiers.js.map +0 -1
  297. package/dist/scripts/_lib/token_count.js.map +0 -1
  298. package/dist/scripts/_lib/trigger_eval_floors.js.map +0 -1
  299. package/dist/scripts/_lib/ui_surface.js.map +0 -1
  300. package/dist/scripts/_lib/user_global_paths.js.map +0 -1
  301. package/dist/scripts/ai_council/modes.js.map +0 -1
  302. package/dist/scripts/ai_council/transport_resolver.js.map +0 -1
  303. package/dist/scripts/condense.js.map +0 -1
  304. package/dist/scripts/project_thin_rules.js.map +0 -1
  305. package/dist/server/app.js.map +0 -1
  306. package/dist/server/io/atomicMultiWrite.js.map +0 -1
  307. package/dist/server/io/atomicWrite.js.map +0 -1
  308. package/dist/server/io/sharedWriteCheck.js.map +0 -1
  309. package/dist/server/io/substituteTemplate.js.map +0 -1
  310. package/dist/server/io/yamlIO.js.map +0 -1
  311. package/dist/server/nicknameResolver.js.map +0 -1
  312. package/dist/server/port.js.map +0 -1
  313. package/dist/server/routes/discovery.js.map +0 -1
  314. package/dist/server/routes/install.js.map +0 -1
  315. package/dist/server/routes/ping.js.map +0 -1
  316. package/dist/server/routes/schema.js.map +0 -1
  317. package/dist/server/routes/settings.js.map +0 -1
  318. package/dist/server/routes/settingsChanges.js.map +0 -1
  319. package/dist/server/routes/userMd.js.map +0 -1
  320. package/dist/server/routes/wizard.js.map +0 -1
  321. package/dist/server/routes/workspace.js.map +0 -1
  322. package/dist/server/schemas/settings.js.map +0 -1
  323. package/dist/server/serverInfo.js.map +0 -1
  324. package/dist/server/token.js.map +0 -1
  325. package/dist/server/writeRoot.js.map +0 -1
  326. package/dist/shared/capabilities.js.map +0 -1
  327. package/dist/shared/interactiveContext.js.map +0 -1
  328. package/dist/shared/nicknamePrefill.js.map +0 -1
  329. package/dist/shared/settingsAsks.js.map +0 -1
  330. package/dist/shared/settingsCarveOut.js.map +0 -1
  331. package/dist/shared/settingsClasses.js.map +0 -1
  332. package/dist/shared/settingsConsent.js.map +0 -1
  333. package/dist/shared/settingsSurface.js.map +0 -1
  334. package/dist/shared/skillRanking.js.map +0 -1
  335. package/dist/shared/userMd/formAdapter.js.map +0 -1
  336. package/dist/shared/userMd/schema.js.map +0 -1
  337. package/dist/shared/userMd/utils.js.map +0 -1
  338. package/dist/ui/assets/index-Cxgy63kz.js.map +0 -1
  339. package/src/scripts/_lib/adr_frontmatter.test.ts +0 -365
  340. package/src/scripts/_lib/adversarial_council_gate.test.ts +0 -112
  341. package/src/scripts/_lib/adversarial_reconcile.test.ts +0 -197
  342. package/src/scripts/_lib/auto_dispatch.corpus.test.ts +0 -150
  343. package/src/scripts/_lib/secret_detector.test.ts +0 -108
  344. package/src/scripts/_lib/secret_detector_adversarial.test.ts +0 -116
  345. package/src/scripts/_lib/ui_authority.test.ts +0 -251
  346. package/src/scripts/_lib/untrusted_content.test.ts +0 -175
  347. package/src/scripts/check_secret_leak.test.ts +0 -90
  348. package/src/scripts/design_slop_rules.test.ts +0 -246
  349. package/src/scripts/self_review_gate.test.ts +0 -296
  350. package/src/shared/capabilities.test.ts +0 -43
@@ -152,6 +152,7 @@ import { reportScanned, DeadScopeError } from './_lib/scan_scope.js';
152
152
  import { GateLedger } from './_lib/gate_ledger.js';
153
153
  import { resolveBaseRef } from './_lib/ratchet_base_ref.js';
154
154
  import { materialiseSubtree } from './_lib/base_tree.js';
155
+ import { SKILLS_POSIX, measureSkillEstate } from './_lib/skill_estate.js';
155
156
  import { runGateCli, runSelfTest, type SelfTestCase } from './_lib/gate_self_test.js';
156
157
 
157
158
  const GATE = 'check_estate_count';
@@ -171,11 +172,35 @@ export interface EstateCounts {
171
172
  active_roadmaps: number;
172
173
  later_roadmaps: number;
173
174
  open_blockers: number;
175
+ /**
176
+ * Maintained `SKILL.md` files — `lifecycle: deprecated` excluded.
177
+ *
178
+ * A FOURTH corpus on the same machinery, added 2026-08-24 on AI council 2/2
179
+ * over the alternative of a second budget file and a second gate: the
180
+ * floor-from-the-base-ref machinery is the expensive part and should not be
181
+ * written twice, and a second ratchet is a second place to forget.
182
+ */
183
+ skill_count: number;
184
+ /**
185
+ * Exact-BPE tokens across those skills' `description:` fields.
186
+ *
187
+ * The second DIMENSION, not a companion figure. A count ratchet alone is
188
+ * gameable by merging four large skills into one file: the count falls, the
189
+ * payload a host must carry does not. `0` here means UNRESOLVED, never
190
+ * "no tokens" — see `tokensExact`.
191
+ */
192
+ skill_description_tokens: number;
174
193
  }
175
194
 
176
195
  export type MetricName = keyof EstateCounts;
177
196
 
178
- export const METRICS: readonly MetricName[] = ['active_roadmaps', 'later_roadmaps', 'open_blockers'];
197
+ export const METRICS: readonly MetricName[] = [
198
+ 'active_roadmaps',
199
+ 'later_roadmaps',
200
+ 'open_blockers',
201
+ 'skill_count',
202
+ 'skill_description_tokens',
203
+ ];
179
204
 
180
205
  export interface EstateBudget {
181
206
  /**
@@ -247,6 +272,16 @@ export interface EstateVerdict {
247
272
  authorisedGrowth: GrowthFinding[];
248
273
  /** The claims found in this change's diff, with their reasons. */
249
274
  claims: GrowthClaim[];
275
+ /**
276
+ * Metrics dropped from the compare, each with its reason.
277
+ *
278
+ * Never empty-and-silent: a skill metric drops when the base ref carries no
279
+ * `src/skills` (an old tag, a shallow clone) or when the tokeniser resolved
280
+ * on one side and not the other. Comparing an exact reading against a proxy
281
+ * one would move the number by more than the growth this gate exists to
282
+ * catch, so the metric is stated unavailable rather than guessed.
283
+ */
284
+ skippedMetrics: { metric: MetricName; reason: string }[];
250
285
  /** `null` when the diff half could not run; the reason is printed either way. */
251
286
  offsets: OffsetLedger | null;
252
287
  offsetSkipReason: string | null;
@@ -342,13 +377,26 @@ export function countEstate(repoRoot: string): EstateCounts {
342
377
  const stats = collect(roadmapRoot);
343
378
  const openOf = (rows: readonly { open_blockers: readonly unknown[] }[]): number =>
344
379
  rows.reduce((n, r) => n + r.open_blockers.length, 0);
380
+ const skills = measureSkillEstate(repoRoot);
345
381
  return {
346
382
  active_roadmaps: stats.length,
347
383
  later_roadmaps: countIn(roadmapRoot, 'later'),
348
384
  open_blockers: openOf(stats) + openOf(laterRoadmaps(roadmapRoot)),
385
+ skill_count: skills.skill_count,
386
+ // 0 stands for unresolved. `skillTokensExact` below is what the caller
387
+ // reads to tell that apart from a genuinely empty corpus, and the
388
+ // verdict skips the metric rather than comparing an exact reading
389
+ // against a proxy one — they differ by more than the growth a ratchet
390
+ // is trying to catch.
391
+ skill_description_tokens: skills.skill_description_tokens ?? 0,
349
392
  };
350
393
  }
351
394
 
395
+ /** Did the tokeniser resolve under `root`? A ratchet must not mix the two modes. */
396
+ export function skillTokensExact(root: string): boolean {
397
+ return measureSkillEstate(root).skill_description_tokens !== null;
398
+ }
399
+
352
400
  /** `agents/roadmaps/<name>.md` — the active top level, never a subdirectory. */
353
401
  function isActiveTopLevel(rel: string): boolean {
354
402
  const norm = rel.split(path.sep).join('/');
@@ -506,19 +554,57 @@ export function evaluate(
506
554
  // property, and unlike a number nobody has to remember to update it.
507
555
  let floor: EstateCounts | null = null;
508
556
  let floorSkipReason: string | null = null;
557
+ /** Set when the base ref carries no `src/skills` — the skill metrics drop. */
558
+ let skillFloorSkipReason: string | null = null;
559
+ /** Set when either side's tokeniser did not resolve — that ONE metric drops. */
560
+ let skillTokenSkipReason: string | null = null;
561
+ if (!skillTokensExact(repoRoot)) {
562
+ skillTokenSkipReason =
563
+ 'the tokeniser did not resolve at HEAD; an exact reading may not be compared ' +
564
+ 'against a proxy one';
565
+ }
509
566
  if (baseRef === null || baseRef === undefined) {
510
567
  floorSkipReason =
511
568
  'no base ref resolved (no origin/main, no merge-commit parent) — the floor cannot be measured';
512
569
  } else {
513
570
  const base = materialiseSubtree(repoRoot, baseRef, ROADMAPS_POSIX);
571
+ // TWO subtrees, because the estate now spans two corpora and the floor
572
+ // must be measured on the base ref's own tree for BOTH. Materialised
573
+ // separately rather than widening the first call: `materialiseSubtree`
574
+ // takes one prefix, and the roadmap tree must stay readable even when
575
+ // the skills read fails (a `src/skills`-less base ref is a real state —
576
+ // an old tag, a shallow clone), in which case the skill metrics skip and
577
+ // the roadmap metrics still ratchet.
578
+ const baseSkills = materialiseSubtree(repoRoot, baseRef, SKILLS_POSIX);
514
579
  try {
515
580
  if (base.error !== null || base.files === 0) {
516
581
  floorSkipReason = `could not read ${ROADMAPS_POSIX} at ${baseRef} — ${base.error ?? 'no files'}`;
517
582
  } else {
518
- floor = countEstate(base.root);
583
+ const roadmapFloor = countEstate(base.root);
584
+ if (baseSkills.error !== null || baseSkills.files === 0) {
585
+ // Roadmap floor stands; the skill floor is stated as
586
+ // unavailable and its metrics are dropped from the compare.
587
+ floor = roadmapFloor;
588
+ skillFloorSkipReason =
589
+ `could not read ${SKILLS_POSIX} at ${baseRef} — ` +
590
+ `${baseSkills.error ?? 'no files'}`;
591
+ } else {
592
+ const s = measureSkillEstate(baseSkills.root);
593
+ floor = {
594
+ ...roadmapFloor,
595
+ skill_count: s.skill_count,
596
+ skill_description_tokens: s.skill_description_tokens ?? 0,
597
+ };
598
+ if (s.skill_description_tokens === null) {
599
+ skillTokenSkipReason =
600
+ `the tokeniser did not resolve at ${baseRef}; an exact reading may ` +
601
+ 'not be compared against a proxy one';
602
+ }
603
+ }
519
604
  }
520
605
  } finally {
521
606
  fs.rmSync(base.root, { recursive: true, force: true });
607
+ fs.rmSync(baseSkills.root, { recursive: true, force: true });
522
608
  }
523
609
  }
524
610
 
@@ -571,11 +657,32 @@ export function evaluate(
571
657
  active_roadmaps: offsets?.exempt.length ?? 0,
572
658
  later_roadmaps: offsets?.parked.length ?? 0,
573
659
  open_blockers: 0,
660
+ // The skill corpus gets NO allowance, and that is the whole point of the
661
+ // metric: the defect it addresses is a corpus that grew +8 in one release
662
+ // with nothing objecting. An addition takes the `estate_growth_exempt`
663
+ // claim path — a recorded reason in the diff — or it fails.
664
+ skill_count: 0,
665
+ skill_description_tokens: 0,
666
+ };
667
+
668
+ /** Metrics dropped from the compare, with the reason, never silently. */
669
+ const skippedMetrics: { metric: MetricName; reason: string }[] = [];
670
+ const skipMetric = (metric: MetricName): boolean => {
671
+ if (skillFloorSkipReason !== null && (metric === 'skill_count' || metric === 'skill_description_tokens')) {
672
+ skippedMetrics.push({ metric, reason: skillFloorSkipReason });
673
+ return true;
674
+ }
675
+ if (skillTokenSkipReason !== null && metric === 'skill_description_tokens') {
676
+ skippedMetrics.push({ metric, reason: skillTokenSkipReason });
677
+ return true;
678
+ }
679
+ return false;
574
680
  };
575
681
 
576
682
  const over: GrowthFinding[] = [];
577
683
  if (floor !== null) {
578
684
  for (const metric of METRICS) {
685
+ if (skipMetric(metric)) continue;
579
686
  const live = counts[metric];
580
687
  const allowed = floor[metric] + allowance[metric];
581
688
  if (live > allowed) {
@@ -596,6 +703,7 @@ export function evaluate(
596
703
  growth,
597
704
  authorisedGrowth,
598
705
  claims,
706
+ skippedMetrics,
599
707
  offsets,
600
708
  offsetSkipReason,
601
709
  unpaid,
@@ -1010,8 +1118,12 @@ export function main(argv: string[] = process.argv.slice(2)): number {
1010
1118
 
1011
1119
  for (const g of verdict.growth) {
1012
1120
  const allow = g.allowance > 0 ? ` (+${String(g.allowance)} allowed here)` : '';
1121
+ // Two corpora share this gate, so the line names which one grew. It used
1122
+ // to say "the roadmap estate" unconditionally, which was already the
1123
+ // wrong noun for `skill_count` the moment that metric landed.
1124
+ const corpus = g.metric.startsWith('skill_') ? 'the skill estate' : 'the roadmap estate';
1013
1125
  process.stderr.write(
1014
- `❌ the roadmap estate grew: ${g.metric} ${String(g.floor)} → ${String(g.live)}${allow}.\n` +
1126
+ `❌ ${corpus} grew: ${g.metric} ${String(g.floor)} → ${String(g.live)}${allow}.\n` +
1015
1127
  ` The floor is the measurement at ${verdict.floorRef ?? 'the base ref'}, not a number in a\n` +
1016
1128
  ' config — so there is no number to edit and nothing to walk. The estate does not\n' +
1017
1129
  ' walk down by itself, which is the defect this ratchet exists for. Either close,\n' +
@@ -1022,6 +1134,13 @@ export function main(argv: string[] = process.argv.slice(2)): number {
1022
1134
  );
1023
1135
  }
1024
1136
 
1137
+ // A dropped metric is REPORTED, never silent: a reader must be able to tell
1138
+ // "within its ratchet" from "within the ratchet of the metrics that could be
1139
+ // measured". This is the same distinction the floor-skip reason above keeps.
1140
+ for (const s of verdict.skippedMetrics) {
1141
+ process.stdout.write(` ⏭️ ${s.metric} not compared — ${s.reason}\n`);
1142
+ }
1143
+
1025
1144
  if (verdict.unpaid > 0 && verdict.offsets !== null) {
1026
1145
  process.stderr.write(
1027
1146
  `❌ ${String(verdict.unpaid)} new active roadmap(s) with no offset in the same change.\n` +
@@ -91,19 +91,46 @@ export function measureDeterministicPayload(
91
91
  ];
92
92
  }
93
93
 
94
- export function evaluate(repoRoot?: string, budgetFile?: string): BudgetVerdict {
94
+ /**
95
+ * A caller-supplied ceiling, and the one direction it may go.
96
+ *
97
+ * road-to-standing-payload-truth 1.1 arms this gate in CI behind a GRACE ceiling
98
+ * equal to the 2026-08-24 measurement, because HEAD is 28.4 % over the design
99
+ * ceiling and blocking at the design number would fail every pull request from
100
+ * the moment the workflow lands — nobody can shed 30,566 tokens inside the PR
101
+ * that arms the gate.
102
+ *
103
+ * **An override may only be LOOSER than the design ceiling, and it is refused if
104
+ * it is tighter.** That sounds backwards for one line and is the whole point: a
105
+ * tighter override would let a caller silently *lower* the bar this file owns,
106
+ * which is the config-weakening shape in reverse — the design ceiling stays the
107
+ * authority, and the override is a dated, expiring concession recorded in
108
+ * `ci_delivery`. Tightening happens by lowering `baseline_tokens`, in the file,
109
+ * with a reason, where a reviewer sees it.
110
+ */
111
+ export function evaluate(repoRoot?: string, budgetFile?: string, overrideCeiling?: number): BudgetVerdict {
95
112
  const budget = readBudget(budgetFile);
96
113
  const buckets = measureDeterministicPayload(repoRoot);
97
114
  const measured = buckets.reduce((sum, b) => sum + b.tokens, 0);
98
- const ceiling = Math.round(budget.baseline_tokens * (1 + budget.headroom_pct / 100));
115
+ const design = Math.round(budget.baseline_tokens * (1 + budget.headroom_pct / 100));
116
+ const ceiling =
117
+ overrideCeiling !== undefined && Number.isFinite(overrideCeiling) && overrideCeiling > design
118
+ ? overrideCeiling
119
+ : design;
99
120
  return { measured, baseline: budget.baseline_tokens, ceiling, withinBudget: measured <= ceiling, buckets };
100
121
  }
101
122
 
102
123
  export function main(argv: string[] = process.argv.slice(2)): number {
103
124
  const json = argv.includes('--format=json') || argv.includes('--json');
125
+ // `--ceiling <n>`: the grace ceiling the CI step reads out of
126
+ // `ci_delivery.grace_ceiling`. Read from the budget file there, never written
127
+ // in the workflow, so the number has exactly one home. A non-numeric or
128
+ // tighter-than-design value is IGNORED rather than honoured — see `evaluate`.
129
+ const ci = argv.indexOf('--ceiling');
130
+ const override = ci !== -1 && argv[ci + 1] !== undefined ? Number(argv[ci + 1]) : undefined;
104
131
  let verdict: BudgetVerdict;
105
132
  try {
106
- verdict = evaluate();
133
+ verdict = evaluate(undefined, undefined, override);
107
134
  } catch (err) {
108
135
  process.stderr.write(`❌ preamble-payload budget: ${(err as Error).message}\n`);
109
136
  return 2;
@@ -0,0 +1,352 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * `check_routing_coverage` — two coverage ratchets, one per routed scope.
4
+ *
5
+ * `road-to-routing-assurance` Phase 0.3. Coverage ratio = corpus cases / routed
6
+ * units, per scope, seeded at the measured current value. **CI fails only on a
7
+ * DECREASE** — the same COUNT-ratchet disposition `lint_trigger_precision.ts`
8
+ * already uses, and for the same reason: the number is a property of the estate,
9
+ * so a rise is somebody's work and a fall is somebody's omission.
10
+ *
11
+ * TWO scopes, never one aggregate, because the aggregate hides the finding:
12
+ *
13
+ * rules 94 / 105 = 0.895 deterministic corpus, gating
14
+ * skills 76 / 299 = 0.254 advisory live harness only
15
+ *
16
+ * The rules surface is ~90 % covered by a corpus that can fail a PR. The skills
17
+ * surface — the one production actually routes on — is 25 %, covered only by a
18
+ * harness that is "advisory only, never gating" (`rule_trigger_eval.ts:4`). One
19
+ * blended figure would read as ~46 % and describe neither.
20
+ *
21
+ * WHY A RATIO AND NOT A COUNT: the denominator moves. Adding 8 skills without
22
+ * adding corpus cases lowers coverage while every count rises, and a count
23
+ * ratchet would call that progress.
24
+ *
25
+ * Exit codes: 0 = at or above seed · 1 = a scope fell · 2 = dead scope.
26
+ */
27
+ import * as fs from 'node:fs';
28
+ import * as os from 'node:os';
29
+ import * as path from 'node:path';
30
+ import process from 'node:process';
31
+ import { fileURLToPath } from 'node:url';
32
+
33
+ import { GateLedger } from './_lib/gate_ledger.js';
34
+ import { type SelfTestCase, runGateCli, runSelfTest } from './_lib/gate_self_test.js';
35
+ import { DeadScopeError, reportScanned } from './_lib/scan_scope.js';
36
+
37
+ const HERE = fileURLToPath(import.meta.url);
38
+ const REAL_REPO_ROOT = path.resolve(path.dirname(HERE), '..', '..');
39
+ /**
40
+ * Scan root. The env override exists for `--self-test`, which drives the REAL
41
+ * CLI against a synthetic tree — an in-process call would skip the argv parsing
42
+ * and the entry guard, and a gate resolving its root from `import.meta.url`
43
+ * reads the live checkout whatever the cwd, so every fixture would be green for
44
+ * the wrong reason.
45
+ */
46
+ const REPO_ROOT = process.env['CHECK_ROUTING_COVERAGE_ROOT'] ?? REAL_REPO_ROOT;
47
+ export const SEED_REL = 'src/config/routing-coverage-seed.json';
48
+
49
+ export type Scope = 'rules' | 'skills';
50
+
51
+ export interface ScopeReading {
52
+ scope: Scope;
53
+ cases: number;
54
+ units: number;
55
+ ratio: number;
56
+ }
57
+
58
+ export interface Seed {
59
+ rules: number;
60
+ skills: number;
61
+ }
62
+
63
+ /** Routed rules = every tier-1 and tier-2 entry in the compiled router. */
64
+ export function measureRules(root: string): ScopeReading {
65
+ const routerPath = path.join(root, 'dist', 'router.json');
66
+ let doc: { tier_1?: { id: string }[]; tier_2?: { id: string }[] };
67
+ try {
68
+ doc = JSON.parse(fs.readFileSync(routerPath, 'utf-8')) as typeof doc;
69
+ } catch {
70
+ throw new DeadScopeError(
71
+ 'check_routing_coverage',
72
+ 'dist/router.json is unreadable — the routed-rule denominator cannot be measured, ' +
73
+ 'and a missing router is not a coverage of zero.',
74
+ );
75
+ }
76
+ const ids = new Set([...(doc.tier_1 ?? []), ...(doc.tier_2 ?? [])].map((r) => r.id));
77
+ const dir = path.join(root, 'tests', 'eval', 'routing-matrix');
78
+ let files: string[];
79
+ try {
80
+ files = fs.readdirSync(dir);
81
+ } catch {
82
+ files = [];
83
+ }
84
+ const stems = new Set(
85
+ files.filter((f) => /\.ya?ml$/.test(f)).map((f) => f.replace(/\.ya?ml$/, '')),
86
+ );
87
+ // Intersection, not the file count: a fixture naming no routed rule covers
88
+ // nothing, and counting it would let the ratio rise on dead files.
89
+ let covered = 0;
90
+ for (const s of stems) if (ids.has(s)) covered += 1;
91
+ return { scope: 'rules', cases: covered, units: ids.size, ratio: ids.size === 0 ? 0 : covered / ids.size };
92
+ }
93
+
94
+ /** Routed skills = every SKILL.md; a case = that skill's `evals/triggers.json`. */
95
+ export function measureSkills(root: string): ScopeReading {
96
+ const base = path.join(root, 'src', 'skills');
97
+ let entries: fs.Dirent[];
98
+ try {
99
+ entries = fs.readdirSync(base, { withFileTypes: true });
100
+ } catch {
101
+ throw new DeadScopeError(
102
+ 'check_routing_coverage',
103
+ 'src/skills is unreadable — the routed-skill denominator cannot be measured.',
104
+ );
105
+ }
106
+ let units = 0;
107
+ let cases = 0;
108
+ for (const e of entries) {
109
+ if (!e.isDirectory()) continue;
110
+ if (!fs.existsSync(path.join(base, e.name, 'SKILL.md'))) continue;
111
+ units += 1;
112
+ if (fs.existsSync(path.join(base, e.name, 'evals', 'triggers.json'))) cases += 1;
113
+ }
114
+ return { scope: 'skills', cases, units, ratio: units === 0 ? 0 : cases / units };
115
+ }
116
+
117
+ export function readSeed(root: string): Seed {
118
+ const p = path.join(root, SEED_REL);
119
+ let raw: string;
120
+ try {
121
+ raw = fs.readFileSync(p, 'utf-8');
122
+ } catch {
123
+ throw new DeadScopeError(
124
+ 'check_routing_coverage',
125
+ `${SEED_REL} is missing — a ratchet with no seed passes every tree.`,
126
+ );
127
+ }
128
+ const doc = JSON.parse(raw) as { seed?: Partial<Seed> };
129
+ const s = doc.seed ?? {};
130
+ if (typeof s.rules !== 'number' || typeof s.skills !== 'number') {
131
+ throw new DeadScopeError(
132
+ 'check_routing_coverage',
133
+ `${SEED_REL} carries no numeric seed for both scopes.`,
134
+ );
135
+ }
136
+ return { rules: s.rules, skills: s.skills };
137
+ }
138
+
139
+ export interface Verdict {
140
+ readings: ScopeReading[];
141
+ seed: Seed;
142
+ fallen: Scope[];
143
+ ledger: GateLedger;
144
+ }
145
+
146
+ /**
147
+ * Compare against the seed.
148
+ *
149
+ * Rounded to four decimals before comparing: an un-rounded float makes a ratio
150
+ * that did not change fail on a representation difference, which is a red the
151
+ * contributor cannot act on.
152
+ */
153
+ /**
154
+ * The comparison precision, shared by the verdict AND the display.
155
+ *
156
+ * Exported because the first version rounded only inside `evaluate` and printed
157
+ * the raw float: rules read 0.895238… against a seed of 0.8952 and rendered `↑`,
158
+ * skills read 0.254180… against 0.2542 and rendered `❌`, while the summary line
159
+ * correctly said green. A gate whose rows contradict its verdict is worse than
160
+ * one that is simply wrong, because the reader cannot tell which half to trust.
161
+ */
162
+ export function r4(n: number): number {
163
+ return Math.round(n * 10_000) / 10_000;
164
+ }
165
+
166
+ export function evaluate(root = REPO_ROOT): Verdict {
167
+ const seed = readSeed(root);
168
+ const readings = [measureRules(root), measureSkills(root)];
169
+ // The ledger's target is the SCOPE, not the individual unit: this gate's
170
+ // verdict is per scope, so a ledger over 404 units would report 404 rows
171
+ // no one can act on while hiding which of the two actually fell.
172
+ const ledger = new GateLedger('check_routing_coverage');
173
+ ledger.plan(readings.map((r) => r.scope));
174
+ const fallen: Scope[] = [];
175
+ for (const r of readings) {
176
+ if (r4(r.ratio) < r4(seed[r.scope])) {
177
+ fallen.push(r.scope);
178
+ ledger.fail(
179
+ r.scope,
180
+ `${r.ratio.toFixed(4)} < seed ${seed[r.scope].toFixed(4)} ` +
181
+ `(${String(r.cases)}/${String(r.units)})`,
182
+ );
183
+ } else {
184
+ ledger.complete(r.scope);
185
+ }
186
+ }
187
+ return { readings, seed, fallen, ledger };
188
+ }
189
+
190
+ /**
191
+ * Build a synthetic tree with `r` routed rules of which `rc` carry a fixture,
192
+ * and `s` routed skills of which `sc` carry a triggers file, plus a seed.
193
+ *
194
+ * A REALISTIC corpus rather than a minimal one: both scopes are always
195
+ * populated, because the gate reports two scopes and a fixture that leaves one
196
+ * empty would red for the empty scope no matter what the case under test does.
197
+ */
198
+ function selfTestRoot(
199
+ tmp: string,
200
+ spec: { r: number; rc: number; s: number; sc: number; seed?: Partial<Seed> | null },
201
+ ): string {
202
+ const root = fs.mkdtempSync(path.join(tmp, 'repo-'));
203
+ const ids: { id: string }[] = [];
204
+ const mdir = path.join(root, 'tests', 'eval', 'routing-matrix');
205
+ fs.mkdirSync(mdir, { recursive: true });
206
+ fs.mkdirSync(path.join(root, 'dist'), { recursive: true });
207
+ for (let i = 0; i < spec.r; i += 1) {
208
+ ids.push({ id: `st-rule-${String(i)}` });
209
+ if (i < spec.rc) fs.writeFileSync(path.join(mdir, `st-rule-${String(i)}.yaml`), 'cases: []\n');
210
+ }
211
+ fs.writeFileSync(
212
+ path.join(root, 'dist', 'router.json'),
213
+ JSON.stringify({ tier_1: ids.slice(0, 1), tier_2: ids.slice(1) }),
214
+ );
215
+ for (let i = 0; i < spec.s; i += 1) {
216
+ const d = path.join(root, 'src', 'skills', `st-skill-${String(i)}`);
217
+ fs.mkdirSync(d, { recursive: true });
218
+ fs.writeFileSync(path.join(d, 'SKILL.md'), '---\nname: st\n---\n');
219
+ if (i < spec.sc) {
220
+ fs.mkdirSync(path.join(d, 'evals'), { recursive: true });
221
+ fs.writeFileSync(path.join(d, 'evals', 'triggers.json'), '[]\n');
222
+ }
223
+ }
224
+ if (spec.seed !== null) {
225
+ fs.mkdirSync(path.join(root, path.dirname(SEED_REL)), { recursive: true });
226
+ fs.writeFileSync(
227
+ path.join(root, SEED_REL),
228
+ JSON.stringify({ seed: spec.seed ?? { rules: spec.rc / spec.r, skills: spec.sc / spec.s } }),
229
+ );
230
+ }
231
+ return root;
232
+ }
233
+
234
+ export function selfTest(): number {
235
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'crc-selftest-'));
236
+ const run = (root: string): number => {
237
+ process.env['CHECK_ROUTING_COVERAGE_ROOT'] = root;
238
+ try {
239
+ return runGateCli(REAL_REPO_ROOT, 'src/scripts/check_routing_coverage.ts', [], root);
240
+ } finally {
241
+ delete process.env['CHECK_ROUTING_COVERAGE_ROOT'];
242
+ }
243
+ };
244
+ const cases: SelfTestCase[] = [
245
+ {
246
+ name: 'a tree exactly at seed on both scopes is accepted',
247
+ expect: 'accept',
248
+ run: () => run(selfTestRoot(tmp, { r: 10, rc: 8, s: 10, sc: 5 })),
249
+ },
250
+ {
251
+ name: 'a tree ABOVE seed on both scopes is accepted',
252
+ expect: 'accept',
253
+ run: () =>
254
+ run(selfTestRoot(tmp, { r: 10, rc: 9, s: 10, sc: 6, seed: { rules: 0.8, skills: 0.5 } })),
255
+ },
256
+ {
257
+ name: 'a removed rules corpus case is rejected',
258
+ expect: 'reject',
259
+ run: () =>
260
+ run(selfTestRoot(tmp, { r: 10, rc: 7, s: 10, sc: 5, seed: { rules: 0.8, skills: 0.5 } })),
261
+ },
262
+ {
263
+ name: 'a removed skills corpus case is rejected',
264
+ expect: 'reject',
265
+ run: () =>
266
+ run(selfTestRoot(tmp, { r: 10, rc: 8, s: 10, sc: 4, seed: { rules: 0.8, skills: 0.5 } })),
267
+ },
268
+ {
269
+ name: 'ADDING routed units with no corpus cases is rejected — the count-ratchet blind spot',
270
+ expect: 'reject',
271
+ run: () =>
272
+ run(selfTestRoot(tmp, { r: 10, rc: 8, s: 12, sc: 5, seed: { rules: 0.8, skills: 0.5 } })),
273
+ },
274
+ {
275
+ name: 'a missing seed exits non-zero rather than passing every tree',
276
+ expect: 'reject',
277
+ run: () => run(selfTestRoot(tmp, { r: 10, rc: 8, s: 10, sc: 5, seed: null })),
278
+ },
279
+ {
280
+ name: 'an unreadable router exits non-zero rather than reporting coverage of zero',
281
+ expect: 'reject',
282
+ run: () => {
283
+ const root = selfTestRoot(tmp, { r: 10, rc: 8, s: 10, sc: 5 });
284
+ fs.writeFileSync(path.join(root, 'dist', 'router.json'), 'not json');
285
+ return run(root);
286
+ },
287
+ },
288
+ ];
289
+ try {
290
+ return runSelfTest({
291
+ gate: 'check_routing_coverage',
292
+ cases,
293
+ minCases: 7,
294
+ minRejectCases: 5,
295
+ });
296
+ } finally {
297
+ fs.rmSync(tmp, { recursive: true, force: true });
298
+ }
299
+ }
300
+
301
+ export function main(argv: string[] = process.argv.slice(2), root = REPO_ROOT): number {
302
+ if (argv.includes('--self-test')) return selfTest();
303
+ let v: Verdict;
304
+ try {
305
+ v = evaluate(root);
306
+ reportScanned({
307
+ gate: 'check_routing_coverage',
308
+ scanned: v.readings.reduce((n, r) => n + r.units, 0),
309
+ units: 'routed unit(s)',
310
+ roots: ['dist/router.json', 'src/skills'],
311
+ });
312
+ } catch (e) {
313
+ if (e instanceof DeadScopeError) {
314
+ process.stderr.write(`❌ ${e.message}\n`);
315
+ return 2;
316
+ }
317
+ throw e;
318
+ }
319
+ v.ledger.report(argv.includes('--quiet') ? () => undefined : process.stdout.write.bind(process.stdout));
320
+ if (argv.includes('--json')) {
321
+ process.stdout.write(`${JSON.stringify(v, null, 2)}\n`);
322
+ return v.fallen.length > 0 ? 1 : 0;
323
+ }
324
+ for (const r of v.readings) {
325
+ const seed = v.seed[r.scope];
326
+ // Same rounding as the verdict, deliberately: see r4's own comment.
327
+ const mark = r4(r.ratio) < r4(seed) ? '❌' : r4(r.ratio) > r4(seed) ? '↑' : '=';
328
+ process.stdout.write(
329
+ ` ${mark} ${r.scope.padEnd(6)} ${String(r.cases).padStart(4)} / ${String(r.units).padEnd(4)} = ` +
330
+ `${r.ratio.toFixed(4)} (seed ${seed.toFixed(4)})\n`,
331
+ );
332
+ }
333
+ if (v.fallen.length === 0) {
334
+ process.stdout.write('✅ routing coverage at or above seed in both scopes.\n');
335
+ return 0;
336
+ }
337
+ for (const s of v.fallen) {
338
+ const r = v.readings.find((x) => x.scope === s)!;
339
+ process.stderr.write(
340
+ `❌ ${s} coverage fell: ${r.ratio.toFixed(4)} < seed ${v.seed[s].toFixed(4)} ` +
341
+ `(${String(r.cases)} case(s) over ${String(r.units)} routed unit(s)).\n` +
342
+ ' A ratio falls two ways and the fix differs: a corpus case was removed ' +
343
+ '(restore it), or units were added without cases (add them). Lowering the seed ' +
344
+ `in ${SEED_REL} is a defect, not a fix.\n`,
345
+ );
346
+ }
347
+ return 1;
348
+ }
349
+
350
+ if (path.resolve(process.argv[1] ?? '') === path.resolve(HERE)) {
351
+ process.exit(main());
352
+ }