@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
@@ -35,15 +35,23 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
35
35
  import * as YAML from 'yaml';
36
36
 
37
37
  import {
38
+ _domains_command_logical,
38
39
  artefact_roots as _agent_src_artefact_roots,
39
40
  iter_all_sources as _agent_src_iter_all_sources,
40
41
  resolve_logical as _agent_src_resolve_logical,
41
42
  strip_source_prefix,
42
43
  } from './_lib/agent_src.js';
44
+ // Import-safety note: `project_thin_rules` guards its CLI entry
45
+ // (`_isCliEntry()` before `process.exit(main())`), so importing it here is
46
+ // side-effect-free. condense.ts is bundled into the installer, where a bare
47
+ // top-level `process.exit` would fire at consumer runtime — the documented
48
+ // bundled-CLI-entry-guard landmine. Verified before wiring, not assumed.
49
+ import { build_thin } from './project_thin_rules.js';
43
50
  import { build_claude_hook_matrix } from './_lib/claude_settings_hooks.js';
44
51
  import { is_claude_builtin_name } from './_lib/claude_builtin_names.js';
45
52
  import { project_settings_path, load_agent_settings } from './_lib/agent_settings.js';
46
53
  import { rule_is_compile_enabled } from './_lib/compile_time_toggles.js';
54
+ import { resolve_rule_pack_scope } from './_lib/scoped_projection.js';
47
55
  import { info, success, flush_summary, resolve_level } from './_lib/script_output.js';
48
56
  import {
49
57
  TIER_TO_CLAUDE_MODEL as _TIER_TO_CLAUDE_MODEL,
@@ -987,13 +995,16 @@ export { _HRR_BANNER_MARKER };
987
995
  // (rule-router contract § Schema v2). Untagged rules fail safe: they ship.
988
996
  // ADR-040: projection-time filtering only, no runtime resolver.
989
997
 
990
- function _read_projection_list(key: string): string[] | null {
998
+ function _read_projection_raw(key: string): unknown {
991
999
  const data = load_agent_settings({ project_path: MODULE_STATE.SETTINGS_FILE });
992
1000
  const proj = data['projection'];
993
- const value =
994
- typeof proj === 'object' && proj !== null && !Array.isArray(proj)
995
- ? (proj as Record<string, unknown>)[key]
996
- : null;
1001
+ return typeof proj === 'object' && proj !== null && !Array.isArray(proj)
1002
+ ? (proj as Record<string, unknown>)[key]
1003
+ : null;
1004
+ }
1005
+
1006
+ function _read_projection_list(key: string): string[] | null {
1007
+ const value = _read_projection_raw(key);
997
1008
  if (Array.isArray(value) && value.length > 0) {
998
1009
  return value.map((v) => String(v));
999
1010
  }
@@ -1004,8 +1015,28 @@ function _read_rule_workspaces(): string[] | null {
1004
1015
  return _read_projection_list('rule_workspaces');
1005
1016
  }
1006
1017
 
1018
+ /** `runtime.active_packs` overlay, empty when unset (fresh-install default). */
1019
+ function _read_runtime_active_packs(): string[] {
1020
+ const data = load_agent_settings({ project_path: MODULE_STATE.SETTINGS_FILE });
1021
+ const rt = data['runtime'];
1022
+ const value =
1023
+ typeof rt === 'object' && rt !== null && !Array.isArray(rt)
1024
+ ? (rt as Record<string, unknown>)['active_packs']
1025
+ : null;
1026
+ return Array.isArray(value) ? value.map((v) => String(v)) : [];
1027
+ }
1028
+
1029
+ /**
1030
+ * Pack scope for the rule layer. `rule_packs: auto` derives the active-pack
1031
+ * set (the same one the skill/command prune uses) instead of a hand-typed id
1032
+ * list; anything else keeps the historical list-or-inactive semantics.
1033
+ */
1007
1034
  function _read_rule_packs(): string[] | null {
1008
- return _read_projection_list('rule_packs');
1035
+ return resolve_rule_pack_scope(
1036
+ _read_projection_raw('rule_packs'),
1037
+ MODULE_STATE.PROJECT_ROOT,
1038
+ _read_runtime_active_packs(),
1039
+ );
1009
1040
  }
1010
1041
 
1011
1042
  /**
@@ -1044,11 +1075,19 @@ export function generate_rule_symlinks(): number {
1044
1075
 
1045
1076
  let thin_files: Record<string, string> | null = null;
1046
1077
  if (_lean_projection_mode() === 'thin') {
1047
- // The `project_thin_rules` twin is not ported (out of scope — thin mode
1048
- // is opt-in and not exercised by golden parity). Throw a clear error if
1049
- // a consumer actually enables it, mirroring an unmet import.
1050
- throw new Error(
1051
- 'lean_projection.mode=thin requires project_thin_rules (not ported in condense.ts)',
1078
+ // DEAD-SWITCH REPAIR (road-to-renewal-foundation Phase 2). This branch
1079
+ // used to THROW: the port was skipped as "out of scope, not exercised by
1080
+ // golden parity", which left a documented, settings-selectable mode that
1081
+ // could only ever crash. `build_thin` was ported and has been present the
1082
+ // whole time — the wiring was the only thing missing.
1083
+ //
1084
+ // Scope note: this repairs the SWITCH only. The default stays
1085
+ // `eager-all`. Flipping it is parked behind the thin-projection honest
1086
+ // null (thin win-rate 36.2% < the 48% pre-registered threshold), and
1087
+ // nothing here disturbs that verdict — but a mode that throws cannot
1088
+ // even be re-measured, which is why the repair earns its place alone.
1089
+ thin_files = Object.fromEntries(
1090
+ build_thin(MODULE_STATE.RULES_SOURCE, _read_rule_workspaces()),
1052
1091
  );
1053
1092
  }
1054
1093
 
@@ -1146,6 +1185,9 @@ function _WINDSURF_WORKFLOWS_DIR(): string {
1146
1185
  function _CURSOR_COMMANDS_DIR(): string {
1147
1186
  return path.join(MODULE_STATE.PROJECT_ROOT, '.cursor', 'commands');
1148
1187
  }
1188
+ function _CLAUDE_COMMANDS_DIR(): string {
1189
+ return path.join(MODULE_STATE.PROJECT_ROOT, '.claude', 'commands');
1190
+ }
1149
1191
 
1150
1192
  function _parse_frontmatter(content: string): [Record<string, unknown>, string] {
1151
1193
  if (!content.startsWith('---')) {
@@ -1566,6 +1608,92 @@ export function extract_description_from_md(content: string): string {
1566
1608
  return '';
1567
1609
  }
1568
1610
 
1611
+ /**
1612
+ * The `commands/<sub>` path of a command source file, or `null` when it is a
1613
+ * FLAT command (`commands/<name>.md`).
1614
+ *
1615
+ * A nested path is what Claude Code renders as `/cluster:sub`. Flat command
1616
+ * FILES are not registered by Claude Code (probed ≤ 2.1.204 — see the
1617
+ * flat-command mitigation in `install.ts`), which is why flat commands keep a
1618
+ * skill wrapper and nested ones do not need one.
1619
+ */
1620
+ function _nested_command_subpath(source_file: string): string | null {
1621
+ const rel = _relativeToPosix(source_file, MODULE_STATE.PROJECT_ROOT);
1622
+ const logical = _domains_command_logical(rel);
1623
+ if (logical === null || !logical.startsWith('commands/')) {
1624
+ return null;
1625
+ }
1626
+ const sub = logical.slice('commands/'.length).replace(/\.md$/, '');
1627
+ return sub.includes('/') ? sub : null;
1628
+ }
1629
+
1630
+ /**
1631
+ * Project-scope `.claude/commands/<cluster>/<sub>.md` — the colon form.
1632
+ *
1633
+ * Before this, the package repo's own `.claude/` carried NO commands
1634
+ * directory: every command reached Claude Code as a hyphen-named skill
1635
+ * wrapper in `.claude/skills/`, while the colon form existed only in the
1636
+ * user-global tree written by `install.ts`. Every clustered command was
1637
+ * therefore listed TWICE for anyone with a global install
1638
+ * (`roadmap-process-full` AND `roadmap:process-full`), costing 4,214 GPT tok
1639
+ * of always-loaded catalog for zero added reach — measured 2026-08-02.
1640
+ *
1641
+ * Emitting the nested form here lets `generate_claude_commands` stop wrapping
1642
+ * the clustered commands: one listing, same reachability, and reachability no
1643
+ * longer depends on a global deploy having run. Claude Code dedupes project
1644
+ * and user scope by name, so the two copies of `/cluster:sub` collapse.
1645
+ *
1646
+ * ADR-003 (colon canonical for clusters) and ADR-044 (flat commands stay
1647
+ * hyphenated) both hold: flat commands are untouched and keep their wrapper.
1648
+ */
1649
+ export function generate_claude_project_commands(
1650
+ active_command_slugs: ReadonlySet<string> | null = null,
1651
+ ): number {
1652
+ if (!_isDir(path.join(MODULE_STATE.PROJECT_ROOT, 'src', 'domains'))) {
1653
+ return 0;
1654
+ }
1655
+ const nested: Array<[string, string]> = [];
1656
+ for (const [source_file, slug] of iterCommands()) {
1657
+ if (active_command_slugs !== null && !active_command_slugs.has(slug)) {
1658
+ continue;
1659
+ }
1660
+ const sub = _nested_command_subpath(source_file);
1661
+ if (sub !== null) {
1662
+ nested.push([source_file, sub]);
1663
+ }
1664
+ }
1665
+
1666
+ const target_root = _CLAUDE_COMMANDS_DIR();
1667
+ const valid = new Set(nested.map(([, sub]) => `${sub}.md`));
1668
+ // Sweep stale links first: a renamed or deleted command must not linger as
1669
+ // a dangling `/cluster:sub`.
1670
+ if (_exists(target_root)) {
1671
+ for (const item of _rglobSorted(target_root, '*.md')) {
1672
+ const rel = _relativeToPosix(item, target_root);
1673
+ if (!valid.has(rel)) {
1674
+ fs.unlinkSync(item);
1675
+ }
1676
+ }
1677
+ }
1678
+
1679
+ let count = 0;
1680
+ for (const [source_file, sub] of nested) {
1681
+ const link = path.join(target_root, `${sub}.md`);
1682
+ _mkdirp(path.dirname(link));
1683
+ if (_existsOrSymlink(link)) {
1684
+ fs.unlinkSync(link);
1685
+ }
1686
+ // path.relative, not a hand-counted `../` run: the nesting depth
1687
+ // varies (2 and 3 segments both occur) and an off-by-one here writes
1688
+ // a dangling link that still LOOKS right in `ls -l`.
1689
+ const rel_target = path.relative(path.dirname(link), source_file);
1690
+ fs.symlinkSync(rel_target, link);
1691
+ count += 1;
1692
+ }
1693
+ info(` ✅ Linked ${count} \`.claude/commands/**/*.md\` files (colon form)`);
1694
+ return count;
1695
+ }
1696
+
1569
1697
  export function generate_claude_commands(active_command_slugs: ReadonlySet<string> | null = null): number {
1570
1698
  if (!_isDir(path.join(MODULE_STATE.PROJECT_ROOT, 'src', 'domains'))) {
1571
1699
  process.stderr.write(' ⚠️ src/domains/ not found — skipping commands\n');
@@ -1597,6 +1725,15 @@ export function generate_claude_commands(active_command_slugs: ReadonlySet<strin
1597
1725
  skipped += 1;
1598
1726
  continue;
1599
1727
  }
1728
+ // A clustered command already reaches Claude Code as `/cluster:sub`
1729
+ // from `.claude/commands/` (generate_claude_project_commands). A
1730
+ // hyphen wrapper on top would be a second listing of the same command
1731
+ // — the double-listing this de-duplicates. Flat commands still need
1732
+ // the wrapper: Claude Code does not register flat command FILES.
1733
+ if (_nested_command_subpath(source_file) !== null) {
1734
+ skipped += 1;
1735
+ continue;
1736
+ }
1600
1737
  // Never claim a `/name` that is a Claude Code built-in command or
1601
1738
  // bundled skill — a same-name entry shadows the built-in (e.g. a
1602
1739
  // `review` entry hides Claude Code's own /review). Nested
@@ -2144,6 +2281,11 @@ function _generate_tools_inner(
2144
2281
  generate_gemini_md();
2145
2282
  }
2146
2283
  const skills = _tool_active('claude-code') ? generate_claude_skills(skill_names) : 0;
2284
+ // Colon form first: `generate_claude_commands` skips every command this
2285
+ // one emitted, so the ordering is what keeps the two from double-listing.
2286
+ const claude_cmds = _tool_active('claude-code')
2287
+ ? generate_claude_project_commands(cmd_slugs)
2288
+ : 0;
2147
2289
  const commands = _tool_active('claude-code') ? generate_claude_commands(cmd_slugs) : 0;
2148
2290
  const subagents = _tool_active('claude-code') ? generate_claude_subagents() : 0;
2149
2291
  const subagent_contexts = generate_subagent_host_contexts();
@@ -2157,7 +2299,8 @@ function _generate_tools_inner(
2157
2299
  const windsurf_wf = _tool_active('windsurf') ? generate_windsurf_workflows(cmd_slugs) : 0;
2158
2300
  const summary =
2159
2301
  `✅ generate-tools — rules=${rules} skills=${skills} ` +
2160
- `commands=${commands} subagents=${subagents} subagent_contexts=${subagent_contexts} plugin_cmd_skills=${plugin_cmd_skills} ` +
2302
+ `claude_commands=${claude_cmds} command_skills=${commands} ` +
2303
+ `subagents=${subagents} subagent_contexts=${subagent_contexts} plugin_cmd_skills=${plugin_cmd_skills} ` +
2161
2304
  `plugin_hooks=${plugin_hooks} ` +
2162
2305
  `personas=${personas} user_types=${user_types} ` +
2163
2306
  `cursor_mdc=${cursor_mdc} windsurf_rules=${windsurf_modern} ` +
@@ -10,7 +10,7 @@
10
10
  // Env: BUDGET_STORE, BUDGET_CONFIG, BUDGET_PERIOD={today|week|month|all}, BUDGET_QUIET=1
11
11
 
12
12
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
13
- import { dirname } from 'node:path';
13
+ import { dirname, join } from 'node:path';
14
14
 
15
15
  const STORE = process.env.BUDGET_STORE || 'agents/cost-tracking/sessions.jsonl';
16
16
  const CONFIG = process.env.BUDGET_CONFIG || 'agents/cost-tracking/budget.json';
@@ -198,14 +198,75 @@ function cmdCheck() {
198
198
  if (hardStop) process.exit(1);
199
199
  }
200
200
 
201
+ // --- Per-tier budget state (docs/contracts/budget-routing.md) -------------
202
+ // v1 window for per-tier ceilings: rolling 24h. Sums ledger entries tagged
203
+ // `tier:` plus pending reserves from tier-reserves.jsonl (the atomic-permit
204
+ // file) so a concurrent dispatch cannot read a stale "available".
205
+
206
+ function loadPerTierCeilings() {
207
+ const out = { cheap: null, medium: null, strong: null };
208
+ if (!existsSync(SETTINGS)) return out;
209
+ let inPerTier = false;
210
+ for (const line of readFileSync(SETTINGS, 'utf-8').split('\n')) {
211
+ if (/^ {4}per_tier:/.test(line)) { inPerTier = true; continue; }
212
+ if (inPerTier) {
213
+ const m = line.match(/^ {6}(cheap|medium|strong):\s*([0-9.]+|null)\s*(#.*)?$/);
214
+ if (m) {
215
+ out[m[1]] = m[2] === 'null' ? null : Number(m[2]);
216
+ continue;
217
+ }
218
+ if (/^ {0,5}\S/.test(line)) inPerTier = false;
219
+ }
220
+ }
221
+ return out;
222
+ }
223
+
224
+ function cmdTier(rest) {
225
+ const tier = rest[0];
226
+ if (!['cheap', 'medium', 'strong'].includes(tier)) {
227
+ console.error('usage: budget.mjs tier {cheap|medium|strong}');
228
+ process.exit(2);
229
+ }
230
+ const dayMs = 24 * 3600 * 1000;
231
+ const now = Date.now();
232
+ const inWindow = (ts) => ts && (now - new Date(ts).getTime()) < dayMs;
233
+ const spent = loadSessions()
234
+ .filter((s) => s.tier === tier && inWindow(s.capturedAt || s.endedAt || s.ts))
235
+ .reduce((acc, s) => acc + (Number(s.total_cost_usd ?? s.cost_usd ?? 0) || 0), 0);
236
+ let reserved = 0;
237
+ const reservePath = join(dirname(STORE), 'tier-reserves.jsonl');
238
+ if (existsSync(reservePath)) {
239
+ for (const line of readFileSync(reservePath, 'utf-8').split('\n')) {
240
+ if (!line.trim()) continue;
241
+ try {
242
+ const e = JSON.parse(line);
243
+ if (e.tier === tier && e.status === 'pending' && (now - e.ts_ms) < dayMs) {
244
+ reserved += Number(e.est_usd || 0) || 0;
245
+ }
246
+ } catch { /* skip malformed line */ }
247
+ }
248
+ }
249
+ const ceiling = loadPerTierCeilings()[tier];
250
+ const available = ceiling === null ? true : spent + reserved < ceiling;
251
+ console.log(JSON.stringify({
252
+ tier,
253
+ window: 'rolling-24h',
254
+ ceiling_usd: ceiling,
255
+ spent_usd: spent,
256
+ reserved_usd: reserved,
257
+ available,
258
+ }));
259
+ }
260
+
201
261
  function main() {
202
262
  const [cmd, ...rest] = process.argv.slice(2);
203
263
  switch (cmd) {
204
264
  case 'set': return cmdSet(rest);
205
265
  case 'get': return cmdGet();
206
266
  case 'check': return cmdCheck();
267
+ case 'tier': return cmdTier(rest);
207
268
  default:
208
- console.error('usage: budget.mjs {set <usd>|get|check}');
269
+ console.error('usage: budget.mjs {set <usd>|get|check|tier <cheap|medium|strong>}');
209
270
  process.exit(2);
210
271
  }
211
272
  }
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Emit the scoped-projection skill count — the number `docs/CLAIMS.md`
4
+ * publishes for the default install
5
+ * (road-to-reproducible-artefact-counts).
6
+ *
7
+ * The claim used to read "217 of 288 skills … with the counting method
8
+ * pinned in the benchmark doc", while that benchmark doc said 215 of 286.
9
+ * Both numbers were hand-typed, only one of them tracked the catalog, and
10
+ * the gap grew by one on every skill added because no gate could compare
11
+ * them. This script is the single derivation both now come from.
12
+ *
13
+ * It adds no counting mechanism of its own — that would be a second path
14
+ * free to disagree with the first, which is the defect, not the fix:
15
+ *
16
+ * - the file set is `update_counts.iter_skills()`, the exact walk behind
17
+ * the canonical `count('skills')`, so `projected + pruned === total`
18
+ * holds by construction;
19
+ * - the in/out predicate is `_lib/scoped_projection.is_pruned_under_scoped`,
20
+ * the exact function `install.ts` applies when it prunes a real user's
21
+ * tree, so the published number cannot describe a projection the
22
+ * installer does not perform.
23
+ *
24
+ * `runtime.active_packs` is deliberately empty here: the claim is about the
25
+ * DEFAULT for a new install, and a fresh machine has no overlay. An operator
26
+ * who activates extra packs gets more skills than the claim states — the
27
+ * claim is a floor, and `--json` exposes `active_packs` so a reader can see
28
+ * the assumption rather than infer it.
29
+ *
30
+ * Usage:
31
+ * ./scripts-run src/scripts/count_scoped_projection # "217 of 288"
32
+ * ./scripts-run src/scripts/count_scoped_projection --json
33
+ * ./scripts-run src/scripts/count_scoped_projection --projected
34
+ *
35
+ * Exit codes: 0 ok · 1 the pack registry could not be read.
36
+ */
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 {
43
+ scoped_projection_stats,
44
+ type ScopedProjectionStats,
45
+ } from './_lib/scoped_projection.js';
46
+ import { iter_skills, REPO_ROOT } from './update_counts.js';
47
+
48
+ const _HERE = fileURLToPath(import.meta.url);
49
+
50
+ export type { ScopedProjectionStats };
51
+
52
+ /** Partition the canonical skill catalog with the installer's own predicate. */
53
+ export function stats(package_root: string = REPO_ROOT): ScopedProjectionStats {
54
+ return scoped_projection_stats(package_root, iter_skills());
55
+ }
56
+
57
+ export function main(argv: readonly string[] = process.argv.slice(2)): number {
58
+ let s: ScopedProjectionStats;
59
+ try {
60
+ s = stats();
61
+ } catch (err) {
62
+ process.stderr.write(
63
+ `❌ scoped-projection count unavailable: ${(err as Error).message}\n` +
64
+ ' (src/config/discovery/packs.yml must exist and parse to a list)\n',
65
+ );
66
+ return 1;
67
+ }
68
+
69
+ if (argv.includes('--json')) {
70
+ process.stdout.write(`${JSON.stringify(s, null, 2)}\n`);
71
+ return 0;
72
+ }
73
+ if (argv.includes('--projected')) {
74
+ process.stdout.write(`${s.projected}\n`);
75
+ return 0;
76
+ }
77
+ if (argv.includes('--total')) {
78
+ process.stdout.write(`${s.total}\n`);
79
+ return 0;
80
+ }
81
+ process.stdout.write(`${s.projected} of ${s.total}\n`);
82
+ return 0;
83
+ }
84
+
85
+ function _isCliEntry(): boolean {
86
+ if (process.argv[1] === undefined) {
87
+ return false;
88
+ }
89
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
90
+ if (import.meta.url === argvUrl) {
91
+ return true;
92
+ }
93
+ try {
94
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
95
+ const argv = fs.realpathSync(path.resolve(process.argv[1]));
96
+ return here === argv;
97
+ } catch {
98
+ return false;
99
+ }
100
+ }
101
+
102
+ if (_isCliEntry() || process.argv[1] === _HERE) {
103
+ process.exit(main(process.argv.slice(2)));
104
+ }
@@ -23,8 +23,12 @@
23
23
  # 9. npx smoke first-touch paths (--version, --help, mcp initialize)
24
24
  # 6-8 feed src/scripts/check_evaluator_budgets.ts against
25
25
  # src/config/evaluator-budgets.json (owner + review date lint-enforced).
26
+ # 8b. record publish with UMBRELLA_RECORD=1, writes the measurement set to
27
+ # agents/evidence/metrics/evaluator-measurements.json so
28
+ # drift is visible in a diff the day it lands
26
29
  #
27
30
  # Usage: bash src/scripts/evaluator_umbrella.sh [--skip-pack <tarball>]
31
+ # UMBRELLA_RECORD=1 bash src/scripts/evaluator_umbrella.sh # nightly
28
32
  set -euo pipefail
29
33
 
30
34
  ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
@@ -166,9 +170,38 @@ console.log(JSON.stringify(m, null, 1));' \
166
170
  "$MEASURE_TMP" "$UNPACKED_BYTES" "$NM_MB" "$DEP_COUNT" "$CLI_MS" "$MCP_MS" "$CLI_COUNT" "$MCP_TOOLS" \
167
171
  "$COLD_START_MS" "$COLD_START_CONDITIONS"
168
172
 
169
- step "budget gate (absolute + >regression_pct creep)"
173
+ # The budget gate runs BEFORE the record is refreshed, so the comparison is
174
+ # fresh-vs-committed. Refreshing first would make every drift check trivially
175
+ # green (road-to-gates-that-can-fail Phase 5).
176
+ step "budget gate (absolute + >regression_pct creep + drift vs the committed record)"
170
177
  ./scripts-run src/scripts/check_evaluator_budgets --measurements "$MEASURE_TMP"
171
178
 
179
+ # ------------------------------------------------- 8b. publish the record
180
+ # With UMBRELLA_RECORD=1 (the nightly sets it) the measurement set is written
181
+ # to a COMMITTED artifact, so budget drift is visible the day it lands instead
182
+ # of at the next release — `cli_help_command_count` drifted 74 → 80 against a
183
+ # value frozen at 79 with nobody seeing it. It lives under agents/evidence/,
184
+ # the repo's committed-evidence tree, and NOT under src/config/, which ships:
185
+ # this is maintainer evidence with nightly churn, not consumer runtime config,
186
+ # and keeping it out of the tarball also keeps evaluator-budgets.json (the
187
+ # policy) reviewable on its own. Sorted keys + fixed indent so a moved number
188
+ # is a one-line diff.
189
+ if [[ "${UMBRELLA_RECORD:-0}" == "1" ]]; then
190
+ step "publish the measurement record (agents/evidence/metrics/)"
191
+ RECORD_PATH="$ROOT/agents/evidence/metrics/evaluator-measurements.json"
192
+ mkdir -p "$(dirname "$RECORD_PATH")"
193
+ RUN_URL=""
194
+ if [[ -n "${GITHUB_REPOSITORY:-}" && -n "${GITHUB_RUN_ID:-}" ]]; then
195
+ RUN_URL="${GITHUB_SERVER_URL:-https://github.com}/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
196
+ fi
197
+ node src/scripts/record_evaluator_measurements.mjs \
198
+ --measurements "$MEASURE_TMP" \
199
+ --out "$RECORD_PATH" \
200
+ --sha "$(git rev-parse HEAD 2>/dev/null || echo '')" \
201
+ --run-url "$RUN_URL" \
202
+ --conditions "$COLD_START_CONDITIONS"
203
+ fi
204
+
172
205
  # ------------------------------------------------------------- 9. npx smoke
173
206
  step "first-touch smoke (installed bin)"
174
207
  "$CLI_BIN" --version >/dev/null
@@ -109,6 +109,14 @@ const _FN_SPEC: Record<string, FnSpec> = {
109
109
  artifact: 'commands',
110
110
  cells: { 'claude-code': 'native', augment: 'native' },
111
111
  },
112
+ // The colon form (`/cluster:sub`) in the project tree. Same artifact and
113
+ // the same native cell as the hyphen wrapper above — the two split the
114
+ // command population between them (nested here, flat there); this is not
115
+ // an added surface.
116
+ generate_claude_project_commands: {
117
+ artifact: 'commands',
118
+ cells: { 'claude-code': 'native' },
119
+ },
112
120
  generate_claude_subagents: {
113
121
  artifact: 'subagents',
114
122
  cells: { 'claude-code': 'native' },
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Derive the routing-matrix router-coverage corpus (road-to-tested-routing
4
+ * Phase 2, PREREG P2 corpus expansion).
5
+ *
6
+ * Reads every per-rule routing matrix under `tests/eval/routing-matrix/`
7
+ * and emits ONE generated corpus file,
8
+ * `internal/bench/corpora/router-coverage/routing-matrix-derived.yaml`,
9
+ * with one `router-coverage` prompt per matrix POSITIVE, labelled
10
+ * `intended_triggers: [<rule>]`. Near-misses are deliberately NOT emitted —
11
+ * the corpus measures intended activation; the matrices' negative half is
12
+ * enforced by `tests/scripts/routing_matrix.test.ts`.
13
+ *
14
+ * Ownership (two-gate pattern): this generator is the only writer;
15
+ * `--check` re-derives in memory and exits 1 on drift, so the derived file
16
+ * can never be hand-edited into disagreement with its source matrices.
17
+ */
18
+ import * as fs from "node:fs";
19
+ import * as path from "node:path";
20
+ import { fileURLToPath, pathToFileURL } from "node:url";
21
+ import { parse as parseYaml } from "yaml";
22
+
23
+ const REPO_ROOT = path.resolve(
24
+ path.dirname(fileURLToPath(import.meta.url)),
25
+ "..",
26
+ "..",
27
+ );
28
+ const MATRIX_DIR = path.join(REPO_ROOT, "tests", "eval", "routing-matrix");
29
+ export const OUT_FILE = path.join(
30
+ REPO_ROOT,
31
+ "internal",
32
+ "bench",
33
+ "corpora",
34
+ "router-coverage",
35
+ "routing-matrix-derived.yaml",
36
+ );
37
+
38
+ interface MatrixCase {
39
+ prompt: string;
40
+ open_files?: string[];
41
+ command?: string;
42
+ }
43
+ interface MatrixFile {
44
+ rule: string;
45
+ positives: MatrixCase[];
46
+ near_misses: MatrixCase[];
47
+ }
48
+
49
+ /** Cheap language tag for the corpus `language:` field. */
50
+ export function detect_language(prompt: string): "de" | "en" {
51
+ if (/[äöüßÄÖÜ]/.test(prompt)) {
52
+ return "de";
53
+ }
54
+ return /(^|\s)(der|die|das|und|nicht|bitte|keine?|mach|prüfe|erstelle|für)(\s|$)/i.test(prompt)
55
+ ? "de"
56
+ : "en";
57
+ }
58
+
59
+ function yamlQuote(s: string): string {
60
+ return JSON.stringify(s);
61
+ }
62
+
63
+ export function derive(): string {
64
+ const files = fs
65
+ .readdirSync(MATRIX_DIR)
66
+ .filter((f) => f.endsWith(".yaml"))
67
+ .sort();
68
+ const lines: string[] = [];
69
+ lines.push("# Router-coverage corpus — DERIVED FROM the per-rule routing matrices.");
70
+ lines.push("#");
71
+ lines.push("# GENERATED FILE — do not hand-edit. Source of truth:");
72
+ lines.push("# tests/eval/routing-matrix/*.yaml");
73
+ lines.push("# Regenerate: npx tsx src/scripts/generate_router_coverage_from_matrix.ts");
74
+ lines.push("# Drift gate: npx tsx src/scripts/generate_router_coverage_from_matrix.ts --check");
75
+ lines.push("");
76
+ lines.push("version: 1");
77
+ lines.push("corpus_id: router-coverage-routing-matrix-derived");
78
+ lines.push("selection_accuracy_target: 0.0");
79
+ lines.push("");
80
+ lines.push("prompts:");
81
+ for (const f of files) {
82
+ const parsed = parseYaml(fs.readFileSync(path.join(MATRIX_DIR, f), "utf-8")) as MatrixFile;
83
+ parsed.positives.forEach((c, i) => {
84
+ const id = `rm-${parsed.rule}-${String(i + 1).padStart(2, "0")}`;
85
+ lines.push(` - id: ${id}`);
86
+ lines.push(" category: router-coverage");
87
+ lines.push(` language: ${detect_language(c.prompt)}`);
88
+ lines.push(` prompt: ${yamlQuote(c.prompt)}`);
89
+ lines.push(" expected_skills: []");
90
+ lines.push(` intended_triggers: [${parsed.rule}]`);
91
+ if (c.open_files && c.open_files.length > 0) {
92
+ lines.push(` open_files: [${c.open_files.map(yamlQuote).join(", ")}]`);
93
+ }
94
+ lines.push("");
95
+ });
96
+ }
97
+ return `${lines.join("\n").trimEnd()}\n`;
98
+ }
99
+
100
+ export function main(argv?: string[]): number {
101
+ const args = argv ?? process.argv.slice(2);
102
+ const check = args.includes("--check");
103
+ const derived = derive();
104
+ if (check) {
105
+ let current = "";
106
+ try {
107
+ current = fs.readFileSync(OUT_FILE, "utf-8");
108
+ } catch {
109
+ process.stderr.write(`missing derived corpus: ${OUT_FILE} — run the generator\n`);
110
+ return 1;
111
+ }
112
+ if (current !== derived) {
113
+ process.stderr.write(
114
+ "routing-matrix-derived.yaml is stale — regenerate via generate_router_coverage_from_matrix\n",
115
+ );
116
+ return 1;
117
+ }
118
+ process.stdout.write("routing-matrix-derived corpus: fresh\n");
119
+ return 0;
120
+ }
121
+ fs.writeFileSync(OUT_FILE, derived, "utf-8");
122
+ const promptCount = (derived.match(/^ {2}- id: /gm) ?? []).length;
123
+ const ruleCount = new Set(
124
+ [...derived.matchAll(/intended_triggers: \[([^\]]+)\]/g)].map((m) => m[1]),
125
+ ).size;
126
+ process.stdout.write(
127
+ `wrote ${path.relative(REPO_ROOT, OUT_FILE)} — ${promptCount} prompts, ${ruleCount} distinct rule ids\n`,
128
+ );
129
+ return 0;
130
+ }
131
+
132
+ declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
133
+ function _isCliEntry(): boolean {
134
+ if (typeof __AGENT_CONFIG_BUNDLE__ !== "undefined" && __AGENT_CONFIG_BUNDLE__) {
135
+ return false;
136
+ }
137
+ if (process.argv[1] === undefined) {
138
+ return false;
139
+ }
140
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
141
+ if (import.meta.url === argvUrl) {
142
+ return true;
143
+ }
144
+ try {
145
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
146
+ const argv1 = fs.realpathSync(path.resolve(process.argv[1]));
147
+ return here === argv1;
148
+ } catch {
149
+ return false;
150
+ }
151
+ }
152
+
153
+ if (_isCliEntry()) {
154
+ process.exit(main());
155
+ }