@event4u/agent-config 14.21.0 → 14.23.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 (235) 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 +164 -187
  5. package/CREDITS.md +37 -0
  6. package/README.md +1 -1
  7. package/dist/agent-src/commands/analyze/inbox.md +380 -5
  8. package/dist/agent-src/commands/analyze/repo.md +4 -2
  9. package/dist/agent-src/commands/pr/merge.md +47 -38
  10. package/dist/agent-src/commands/roadmap/next.md +8 -0
  11. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
  12. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
  13. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +12 -0
  14. package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
  15. package/dist/agent-src/rules/notes-first-reasoning.md +36 -25
  16. package/dist/agent-src/rules/token-efficiency.md +26 -3
  17. package/dist/agent-src/scripts/_cli_entry.ts +51 -0
  18. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +120 -27
  19. package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
  20. package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
  21. package/dist/agent-src/scripts/stubs_due.ts +4 -18
  22. package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
  23. package/dist/agent-src/skills/code-intelligence/SKILL.md +82 -1
  24. package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +9 -0
  25. package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
  26. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +52 -2
  27. package/dist/agent-src/templates/agent-settings.md +0 -1
  28. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  29. package/dist/cli/registry.js +1 -1
  30. package/dist/cli-delegate/_cli_entry.js +8 -0
  31. package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
  32. package/dist/cli-delegate/archival_sweep.js +8 -0
  33. package/dist/cli-delegate/archive_completed_roadmaps.js +542 -0
  34. package/dist/cli-delegate/check_update_banner.js +923 -0
  35. package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-2KKXJW25.js} +23 -9
  36. package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
  37. package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
  38. package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
  39. package/dist/cli-delegate/{chunk-4FS4WRXP.js → chunk-IAI3XL43.js} +15 -3
  40. package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
  41. package/dist/cli-delegate/{chunk-643Z7FIC.js → chunk-KCNTUSPI.js} +7 -1
  42. package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
  43. package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
  44. package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
  45. package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
  46. package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
  47. package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
  48. package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
  49. package/dist/cli-delegate/cmd_conformance.js +2 -2
  50. package/dist/cli-delegate/cmd_export.js +2 -2
  51. package/dist/cli-delegate/cmd_handoff.js +45 -13
  52. package/dist/cli-delegate/cmd_prune.js +2 -2
  53. package/dist/cli-delegate/cmd_session_recycle.js +4 -14
  54. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  55. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  56. package/dist/cli-delegate/cmd_sync.js +2 -2
  57. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  58. package/dist/cli-delegate/cmd_validate.js +2 -2
  59. package/dist/cli-delegate/dashboard_mode.js +16 -0
  60. package/dist/cli-delegate/gate_budget.js +24 -0
  61. package/dist/cli-delegate/gate_execute.js +25 -0
  62. package/dist/cli-delegate/guarded_baseline.js +18 -0
  63. package/dist/cli-delegate/resume_probe.js +22 -0
  64. package/dist/cli-delegate/roadmap_gates.js +638 -0
  65. package/dist/cli-delegate/roadmap_set_step.js +173 -0
  66. package/dist/cli-delegate/stub_queue.js +47 -0
  67. package/dist/cli-delegate/stubs_due.js +38 -0
  68. package/dist/cli-delegate/ticket_bundles.js +59 -0
  69. package/dist/cli-delegate/update_roadmap_progress.js +60 -0
  70. package/dist/discovery/deprecation-report.md +1 -1
  71. package/dist/discovery/discovery-manifest.json +13 -13
  72. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  73. package/dist/discovery/discovery-manifest.summary.md +1 -1
  74. package/dist/discovery/dormancy-report.md +1 -1
  75. package/dist/discovery/orphan-report.md +1 -1
  76. package/dist/discovery/packs.json +2 -2
  77. package/dist/discovery/trust-report.md +1 -1
  78. package/dist/discovery/workspaces.json +2 -2
  79. package/dist/hooks/dispatch.js +1813 -946
  80. package/dist/install/claudeLayerCarriage.js +237 -0
  81. package/dist/install/claudePathsPlan.js +79 -0
  82. package/dist/install/hostLayerFingerprint.js +1 -1
  83. package/dist/install/install.mjs +21 -3
  84. package/dist/install/partitionEligibility.js +231 -117
  85. package/dist/install/ruleLayerPartition.js +10 -7
  86. package/dist/mcp/registry-manifest.json +2 -2
  87. package/dist/mcp/server.json +2 -2
  88. package/dist/mcp/server.mjs +10870 -7641
  89. package/dist/scripts/_lib/lean_projection_mode.js +108 -0
  90. package/dist/scripts/_lib/router_match.js +243 -0
  91. package/dist/scripts/_lib/rule_injection.js +216 -0
  92. package/dist/scripts/condense.js +52 -45
  93. package/dist/scripts/project_thin_rules.js +139 -19
  94. package/dist/server/schemas/settings.js +16 -3
  95. package/docs/MIGRATION.md +105 -1
  96. package/docs/contracts/settings-classes.md +40 -8
  97. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
  98. package/docs/guidelines/agent-infra/notes-horizon-mechanics.md +90 -0
  99. package/llms.txt +1 -1
  100. package/package.json +7 -4
  101. package/src/agent-src/scripts/_cli_entry.ts +51 -0
  102. package/src/agent-src/scripts/archive_completed_roadmaps.ts +120 -27
  103. package/src/agent-src/scripts/roadmap_gates.ts +4 -20
  104. package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
  105. package/src/agent-src/scripts/stubs_due.ts +4 -18
  106. package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
  107. package/src/config/agent-settings.template.yml +47 -1
  108. package/src/config/continuity-surface.json +516 -0
  109. package/src/config/estate-count-budget.json +1 -1
  110. package/src/config/evaluator-budgets.json +4 -4
  111. package/src/config/gate-coverage.yml +29 -8
  112. package/src/config/gate-violation-baselines.json +8 -8
  113. package/src/config/gitignore-block.txt +19 -0
  114. package/src/config/hook-token-budget.json +7 -6
  115. package/src/config/host-injection-effect.json +73 -0
  116. package/src/config/packed-binary-manifest.json +33 -0
  117. package/src/config/preamble-payload-budget.json +112 -105
  118. package/src/config/publish-surface.json +4 -2
  119. package/src/config/routing-coverage-seed.json +9 -2
  120. package/src/install/claudeLayerCarriage.ts +273 -0
  121. package/src/install/claudePathsPlan.ts +76 -0
  122. package/src/install/hostLayerFingerprint.ts +1 -1
  123. package/src/install/partitionEligibility.ts +261 -127
  124. package/src/install/ruleLayerPartition.ts +19 -23
  125. package/src/scripts/_cli/cmd_session_recycle.ts +2 -25
  126. package/src/scripts/_cli/handoff_sessions.ts +14 -44
  127. package/src/scripts/_dispatch.bash +77 -27
  128. package/src/scripts/_lib/auto_dispatch.ts +2 -2
  129. package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
  130. package/src/scripts/_lib/continuity_slot.ts +295 -0
  131. package/src/scripts/_lib/continuity_surface.ts +296 -0
  132. package/src/scripts/_lib/continuity_writer.ts +253 -0
  133. package/src/scripts/_lib/corpus_manifest.ts +26 -9
  134. package/src/scripts/_lib/hook_settings.ts +86 -1
  135. package/src/scripts/_lib/injection_effect.ts +151 -0
  136. package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
  137. package/src/scripts/_lib/lean_projection_mode.ts +127 -0
  138. package/src/scripts/_lib/packed_binary_predicate.ts +262 -0
  139. package/src/scripts/_lib/pin_resolver.ts +13 -0
  140. package/src/scripts/_lib/recycle_envelope_paths.ts +109 -21
  141. package/src/scripts/_lib/regression_neighbourhood.ts +159 -22
  142. package/src/scripts/_lib/retired_status.ts +74 -0
  143. package/src/scripts/_lib/session_eol.ts +44 -0
  144. package/src/scripts/_lib/session_index_trust.ts +346 -0
  145. package/src/scripts/_lib/skill_catalogue.ts +156 -21
  146. package/src/scripts/_lib/standing_bound_ratchet.ts +154 -0
  147. package/src/scripts/_lib/subagent_capsule.ts +117 -8
  148. package/src/scripts/_lib/vendored_grammar_upstream.ts +280 -0
  149. package/src/scripts/ai_council/api_transport.ts +5 -0
  150. package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
  151. package/src/scripts/ai_council/prompts.ts +9 -0
  152. package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
  153. package/src/scripts/audit_mcp_tools.ts +69 -25
  154. package/src/scripts/build_mcp_catalog.ts +24 -1
  155. package/src/scripts/capture_skill_catalogue.ts +24 -11
  156. package/src/scripts/check_candidate_lines.ts +499 -0
  157. package/src/scripts/check_continuity_surface.ts +475 -0
  158. package/src/scripts/check_dependency_floors.ts +13 -6
  159. package/src/scripts/check_estate_count.ts +40 -79
  160. package/src/scripts/check_host_tree_parity.ts +291 -0
  161. package/src/scripts/check_pack_size.ts +262 -23
  162. package/src/scripts/check_preamble_payload_budget.ts +136 -7
  163. package/src/scripts/check_roadmap_trackable.ts +19 -16
  164. package/src/scripts/check_rule_layer_partition.ts +54 -34
  165. package/src/scripts/check_rule_projection_integrity.ts +102 -2
  166. package/src/scripts/check_single_delivery.ts +97 -31
  167. package/src/scripts/check_update_banner.ts +13 -0
  168. package/src/scripts/ci_settle.ts +205 -5
  169. package/src/scripts/code_graph/build.ts +217 -15
  170. package/src/scripts/code_graph/cli.ts +334 -15
  171. package/src/scripts/code_graph/detect.ts +38 -0
  172. package/src/scripts/code_graph/extract.ts +11 -0
  173. package/src/scripts/code_graph/grammar_probe.ts +43 -0
  174. package/src/scripts/code_graph/loader.ts +88 -14
  175. package/src/scripts/code_graph/query.ts +144 -11
  176. package/src/scripts/code_graph/resolution_tiers.ts +238 -0
  177. package/src/scripts/code_graph/sqlite_store.ts +262 -6
  178. package/src/scripts/code_graph/types.ts +117 -3
  179. package/src/scripts/code_graph/validate.ts +26 -1
  180. package/src/scripts/code_graph/verbs.ts +590 -0
  181. package/src/scripts/condense.ts +54 -42
  182. package/src/scripts/corpus_manifest.ts +5 -5
  183. package/src/scripts/council_cli.ts +11 -12
  184. package/src/scripts/count_intent_disagreement.ts +426 -0
  185. package/src/scripts/handoff_context_hook.ts +118 -1
  186. package/src/scripts/hook_effect_doctor.ts +82 -1
  187. package/src/scripts/hook_manifest.json +1 -1
  188. package/src/scripts/hook_manifest.yaml +87 -12
  189. package/src/scripts/hooks/block_speaking_inbox_dir.ts +182 -16
  190. package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
  191. package/src/scripts/hooks/code_graph_context_hook.ts +201 -0
  192. package/src/scripts/hooks/concern_registry.ts +4 -2
  193. package/src/scripts/hooks/host_semantics.ts +1 -1
  194. package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
  195. package/src/scripts/hooks/push_settle_hook.ts +3 -2
  196. package/src/scripts/hooks/rule_inject_hook.ts +108 -21
  197. package/src/scripts/hooks/session_eol_hook.ts +120 -2
  198. package/src/scripts/hooks/skill_route_hook.ts +11 -4
  199. package/src/scripts/hot_context_hook.ts +62 -7
  200. package/src/scripts/inbox_source_census.ts +322 -0
  201. package/src/scripts/install-hooks.sh +82 -0
  202. package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
  203. package/src/scripts/lint_plan_risk_register.ts +13 -17
  204. package/src/scripts/lint_roadmap_blockers.ts +164 -1
  205. package/src/scripts/mcp_server/consumer_tool_catalog.json +152 -2
  206. package/src/scripts/mcp_server/graph_tools.ts +294 -0
  207. package/src/scripts/mcp_server/path_util.ts +31 -0
  208. package/src/scripts/mcp_server/suggest_skill.ts +72 -0
  209. package/src/scripts/mcp_server/tool_catalog_source.ts +7 -1
  210. package/src/scripts/mcp_server/tools.ts +5 -69
  211. package/src/scripts/media/lib/adapter-contract.md +7 -2
  212. package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
  213. package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
  214. package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
  215. package/src/scripts/model_rule_injection.ts +61 -4
  216. package/src/scripts/project_thin_rules.ts +149 -16
  217. package/src/scripts/rdp_candidates_delta.ts +276 -0
  218. package/src/scripts/rdp_quality_eval.ts +217 -19
  219. package/src/scripts/release_mix_taxonomy.json +3 -2
  220. package/src/scripts/report_host_injection_effect.ts +189 -0
  221. package/src/scripts/report_layer_overlap.ts +41 -0
  222. package/src/scripts/report_skill_menu_census.ts +341 -0
  223. package/src/scripts/report_standing_payload_by_host.ts +650 -0
  224. package/src/scripts/schemas/agent-settings.schema.json +7 -1
  225. package/src/scripts/session_memory_index.ts +81 -8
  226. package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
  227. package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
  228. package/src/server/schemas/settings.ts +23 -4
  229. package/src/vendor/grammars/README.md +86 -0
  230. package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
  231. package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
  232. package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
  233. package/src/scripts/_lib/carrier_status.ts +0 -118
  234. package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
  235. package/src/scripts/hooks/code_graph_nudge_hook.ts +0 -191
@@ -37,4 +37,112 @@ export function writesThinFiles(mode) {
37
37
  export function deliversBodies(mode) {
38
38
  return mode === 'delivery';
39
39
  }
40
+ /**
41
+ * The hosts a thinning mode may actually thin.
42
+ *
43
+ * Deliberately NOT "every host id the package knows". `condense`'s `TOOL_DIRS`
44
+ * writes a per-rule tree for exactly three hosts, so those are the only ids for
45
+ * which `lean_projection.hosts` can mean anything: naming `windsurf` there would
46
+ * be a request to thin a single concatenated file that has no per-rule stub
47
+ * shape, and naming `codex` a request to thin a tree the installer writes and
48
+ * `condense` never touches. Accepting such an id silently would leave the
49
+ * operator believing a host was scoped when nothing reads the entry.
50
+ */
51
+ export const THINNABLE_HOSTS = ['claude-code', 'cursor', 'cline'];
52
+ /**
53
+ * The shipped default host set for a thinning mode.
54
+ *
55
+ * One host, and it is the one host that both binds `pre_tool_use` and honours a
56
+ * deny (`docs/enforcement-by-host.md:18-28`) — i.e. the only host where the
57
+ * delivery concern's verdict is acted on. Widening this is a decision, never a
58
+ * default: see `resolveLeanProjectionHosts`, which never widens implicitly.
59
+ */
60
+ export const DEFAULT_LEAN_PROJECTION_HOSTS = ['claude-code'];
61
+ /**
62
+ * Resolve a configured `lean_projection.hosts` list.
63
+ *
64
+ * Three properties, each of which is a failure this repository has seen in a
65
+ * neighbouring config surface:
66
+ *
67
+ * 1. **Absent means the default, never "all".** An empty or missing list
68
+ * resolves to `DEFAULT_LEAN_PROJECTION_HOSTS`. A key nobody set must not
69
+ * thin every host — that is D1, the defect this axis exists to repair.
70
+ * 2. **The set never widens implicitly.** Every id is checked against
71
+ * `THINNABLE_HOSTS`; anything else is dropped. A typo cannot enrol a host,
72
+ * and neither can a real host id that has no per-rule tree.
73
+ * 3. **A drop is reported, not swallowed.** The caller gets the ids and the
74
+ * reason so it can print them. Silently ignoring an entry an operator wrote
75
+ * is how a setting comes to mean nothing while looking configured.
76
+ *
77
+ * A configured list that resolves to NOTHING keeps `usedDefault: false` and an
78
+ * empty `hosts` — that is "thin no host", which is a legitimate and safe answer
79
+ * and must not fall back to the default. Falling back there would turn a
80
+ * fully-typo'd list into a Claude Code flip the operator never asked for.
81
+ *
82
+ * WHICH FILES REACH THIS AT ALL, stated because R2 finding 6 read the drop path
83
+ * as unreachable: `agent-settings.schema.json` and the wizard's Zod schema both
84
+ * constrain the items to an `enum`, so an out-of-vocabulary id written through
85
+ * either surface is a hard validation FAILURE and never arrives here. What
86
+ * arrives here is a HAND-EDITED `.agent-settings.yml` — the file
87
+ * `condense._lean_projection_settings` and `hooks/rule_inject_hook.gateOpen`
88
+ * both read directly, with no validation step in between. The two layers are
89
+ * defence in depth, not one contract stated twice: the schema is the authoring
90
+ * gate, this function is the read-time gate, and the drop wording below exists
91
+ * for the file the schema never saw.
92
+ */
93
+ export function resolveLeanProjectionHosts(raw) {
94
+ const known = new Set(THINNABLE_HOSTS);
95
+ if (!Array.isArray(raw) || raw.length === 0) {
96
+ return { hosts: [...DEFAULT_LEAN_PROJECTION_HOSTS], dropped: [], usedDefault: true };
97
+ }
98
+ const hosts = [];
99
+ const dropped = [];
100
+ for (const entry of raw) {
101
+ const id = typeof entry === 'string' ? entry.trim().toLowerCase() : '';
102
+ if (id === '') {
103
+ dropped.push({ id: String(entry), reason: 'unknown' });
104
+ continue;
105
+ }
106
+ if (!known.has(id)) {
107
+ // `not-thinnable` is reserved for an id this package genuinely knows
108
+ // as a host but which owns no per-rule tree. Everything else is a
109
+ // typo or an invention, and the two deserve different wording
110
+ // because the operator's next action differs: fix the spelling, or
111
+ // learn that the host cannot be thinned at all.
112
+ dropped.push({ id, reason: KNOWN_NON_THINNABLE_HOSTS.has(id) ? 'not-thinnable' : 'unknown' });
113
+ continue;
114
+ }
115
+ if (!hosts.includes(id))
116
+ hosts.push(id);
117
+ }
118
+ hosts.sort();
119
+ return { hosts, dropped, usedDefault: false };
120
+ }
121
+ /**
122
+ * Host ids the package ships surfaces for that own no per-rule rule tree.
123
+ *
124
+ * Only used to word a drop precisely; it grants nothing. Sourced from
125
+ * `condense._ALL_TOOLS` plus the two hosts outside it that the host table
126
+ * carries (`codex`, `cowork`).
127
+ */
128
+ const KNOWN_NON_THINNABLE_HOSTS = new Set([
129
+ 'claude-desktop', 'augment', 'copilot', 'windsurf', 'gemini', 'codex', 'cowork',
130
+ ]);
131
+ /** One line per dropped id, for a caller that prints warnings. Empty when nothing was dropped. */
132
+ export function describeDroppedHosts(res) {
133
+ return res.dropped.map(({ id, reason }) => reason === 'not-thinnable'
134
+ ? `lean_projection.hosts: "${id}" is a known host but has no per-rule rule tree — dropped`
135
+ : `lean_projection.hosts: "${id}" is not a thinnable host id — dropped`);
136
+ }
137
+ /**
138
+ * Does the projector write stubs for this host, under this mode and host set?
139
+ *
140
+ * The single predicate `condense` calls. Mode is checked FIRST and on purpose:
141
+ * with `mode` unset the answer is `false` for every host regardless of what
142
+ * `hosts` says, so a `hosts:` list left behind after a rollback to `eager-all`
143
+ * thins nothing.
144
+ */
145
+ export function thinsHost(mode, hosts, hostId) {
146
+ return writesThinFiles(mode) && hosts.includes(hostId);
147
+ }
40
148
  //# sourceMappingURL=lean_projection_mode.js.map
@@ -0,0 +1,243 @@
1
+ /**
2
+ * THE single trigger-matcher implementation for `dist/router.json`.
3
+ *
4
+ * Parity by construction: every surface that answers "which rules fire on
5
+ * this prompt?" — router replay (`router_telemetry.ts`), trigger-coverage
6
+ * CI (`trigger_coverage.ts`), `agent-config explain route`, and
7
+ * `agent-config route:explain` — MUST import the matchers from this module.
8
+ * A second implementation of these semantics anywhere in the tree is a
9
+ * violation (enforced by `tests/scripts/router_match_parity.test.ts`); the
10
+ * historical divergence this kills was `cmd_explain.ts` shipping unanchored
11
+ * keyword matching while the replay path was anchored.
12
+ *
13
+ * Canonical semantics: `docs/contracts/rule-router.md` § `triggers:` shape
14
+ * (the trigger-semantics table — anchored `keyword`, unanchored `phrase`,
15
+ * `command` prefix, `path_prefix` / `file_pattern` over open files).
16
+ *
17
+ * Extracted verbatim from `router_telemetry.ts` (which re-exports the same
18
+ * names for existing importers). Behaviour is pinned by the routing-matrix
19
+ * and trigger-coverage suites — do not "fix" quirks here.
20
+ */
21
+ // ── fnmatch (Python fnmatch.fnmatch) ─────────────────────────────────────
22
+ /**
23
+ * Translate a Unix shell pattern into a RegExp, matching CPython's
24
+ * `fnmatch.translate`. `*` → `.*`, `?` → `.`, `[...]` char classes
25
+ * preserved (with `!` → `^` negation), everything else escaped.
26
+ * Matching is case-sensitive on POSIX (the corpus paths are POSIX).
27
+ */
28
+ function _fnmatchToRegExp(pat) {
29
+ let res = "";
30
+ let i = 0;
31
+ const n = pat.length;
32
+ while (i < n) {
33
+ const c = pat[i];
34
+ i += 1;
35
+ if (c === "*") {
36
+ res += ".*";
37
+ }
38
+ else if (c === "?") {
39
+ res += ".";
40
+ }
41
+ else if (c === "[") {
42
+ let j = i;
43
+ if (j < n && pat[j] === "!") {
44
+ j += 1;
45
+ }
46
+ if (j < n && pat[j] === "]") {
47
+ j += 1;
48
+ }
49
+ while (j < n && pat[j] !== "]") {
50
+ j += 1;
51
+ }
52
+ if (j >= n) {
53
+ res += "\\[";
54
+ }
55
+ else {
56
+ let stuff = pat.slice(i, j);
57
+ if (!stuff.includes("-")) {
58
+ stuff = stuff.replace(/\\/g, "\\\\");
59
+ }
60
+ else {
61
+ // Faithful port of CPython's dash-handling in fnmatch.translate:
62
+ // split the class body on `-` (bounded to [i, j)), escaping each
63
+ // chunk, then re-join with `-` so ranges survive.
64
+ const chunks = [];
65
+ let k = pat[i] === "!" ? i + 2 : i + 1;
66
+ let start = i;
67
+ while (true) {
68
+ const rel = pat.slice(k, j).indexOf("-");
69
+ if (rel < 0) {
70
+ break;
71
+ }
72
+ const idx = k + rel;
73
+ chunks.push(pat.slice(start, idx));
74
+ start = idx + 1;
75
+ k = idx + 3;
76
+ }
77
+ chunks.push(pat.slice(start, j));
78
+ stuff = chunks
79
+ .map((s) => s.replace(/\\/g, "\\\\").replace(/-/g, "\\-"))
80
+ .join("-");
81
+ }
82
+ // Escape regex set-meta chars except backslash/dash already handled.
83
+ stuff = stuff.replace(/([&~|])/g, "\\$1");
84
+ i = j + 1;
85
+ if (stuff.startsWith("!")) {
86
+ stuff = "^" + stuff.slice(1);
87
+ }
88
+ else if (stuff.startsWith("^") || stuff.startsWith("[")) {
89
+ stuff = "\\" + stuff;
90
+ }
91
+ res += `[${stuff}]`;
92
+ }
93
+ }
94
+ else {
95
+ res += c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
96
+ }
97
+ }
98
+ return new RegExp(`^(?:${res})$`, "s");
99
+ }
100
+ export function _fnmatch(name, pat) {
101
+ return _fnmatchToRegExp(pat).test(name);
102
+ }
103
+ // ── Trigger matching ─────────────────────────────────────────────────────
104
+ /** Apply one trigger to a prompt + context; return True on match. */
105
+ /** Unicode word character — letters, digits, underscore (incl. umlauts). */
106
+ const _WORD_CHAR = /[\p{L}\p{N}_]/u;
107
+ function _isWordChar(c) {
108
+ return c !== undefined && _WORD_CHAR.test(c);
109
+ }
110
+ /**
111
+ * Word-boundary-anchored keyword match (road-to-tested-routing Phase 3).
112
+ *
113
+ * A `keyword` occurrence counts only when its word-character edges sit on
114
+ * word boundaries: `ac` no longer fires inside "black", `plan` no longer
115
+ * fires inside "planet". Two deliberate reliefs:
116
+ *
117
+ * - An edge that is itself a NON-word character (e.g. `__()`, `/image:`,
118
+ * `trans(`, an emoji) carries no boundary requirement on that side —
119
+ * punctuation-shaped keywords keep their exact substring semantics.
120
+ * - The right boundary accepts one optional plural `s` ("icons", "options",
121
+ * "secrets" still fire their singular keyword). Richer inflection (German
122
+ * verb endings: "implementiere", "committen") is a DOCUMENTED accepted
123
+ * recall cost of anchoring — the matrices carry standalone-token phrasings
124
+ * for those languages instead.
125
+ *
126
+ * `phrase` deliberately stays unanchored substring; since this change the
127
+ * two trigger kinds genuinely differ.
128
+ */
129
+ export function keyword_matches_anchored(prompt_lower, kw_lower) {
130
+ if (kw_lower === "") {
131
+ return false;
132
+ }
133
+ const needLeft = _isWordChar(kw_lower[0]);
134
+ const needRight = _isWordChar(kw_lower[kw_lower.length - 1]);
135
+ let idx = prompt_lower.indexOf(kw_lower);
136
+ while (idx !== -1) {
137
+ const leftOk = !needLeft || !_isWordChar(prompt_lower[idx - 1]);
138
+ let rightOk = true;
139
+ if (needRight) {
140
+ let end = idx + kw_lower.length;
141
+ if (prompt_lower[end] === "s") {
142
+ end += 1; // optional plural `s`
143
+ }
144
+ rightOk = !_isWordChar(prompt_lower[end]);
145
+ }
146
+ if (leftOk && rightOk) {
147
+ return true;
148
+ }
149
+ idx = prompt_lower.indexOf(kw_lower, idx + 1);
150
+ }
151
+ return false;
152
+ }
153
+ export function trigger_matches(trigger, prompt, open_files, command) {
154
+ const prompt_lower = prompt.toLowerCase();
155
+ if ("keyword" in trigger) {
156
+ return keyword_matches_anchored(prompt_lower, String(trigger["keyword"]).toLowerCase());
157
+ }
158
+ if ("phrase" in trigger) {
159
+ return prompt_lower.includes(String(trigger["phrase"]).toLowerCase());
160
+ }
161
+ if ("command" in trigger) {
162
+ if (!command) {
163
+ return false;
164
+ }
165
+ return command.startsWith(String(trigger["command"]));
166
+ }
167
+ if ("path_prefix" in trigger) {
168
+ if (!open_files) {
169
+ return false;
170
+ }
171
+ const pref = String(trigger["path_prefix"]);
172
+ for (const p of open_files) {
173
+ if (String(p).startsWith(pref)) {
174
+ return true;
175
+ }
176
+ }
177
+ return false;
178
+ }
179
+ if ("file_pattern" in trigger) {
180
+ if (!open_files) {
181
+ return false;
182
+ }
183
+ const pat = String(trigger["file_pattern"]);
184
+ for (const p of open_files) {
185
+ if (_fnmatch(String(p), pat)) {
186
+ return true;
187
+ }
188
+ }
189
+ return false;
190
+ }
191
+ return false;
192
+ }
193
+ export function _asRuleList(value) {
194
+ if (!Array.isArray(value)) {
195
+ return [];
196
+ }
197
+ return value;
198
+ }
199
+ export function _asTriggerList(value) {
200
+ if (!Array.isArray(value)) {
201
+ return [];
202
+ }
203
+ return value;
204
+ }
205
+ /**
206
+ * Return the matched-triggers + activated-rules for one prompt.
207
+ *
208
+ * Kernel rules are always active. tier_1 always considered. tier_2
209
+ * only considered when `profile == 'full'`.
210
+ */
211
+ export function match_prompt(router, prompt, profile = "full", open_files, command) {
212
+ const tiers = [
213
+ ["tier_1", _asRuleList(router["tier_1"])],
214
+ ];
215
+ if (profile === "full") {
216
+ tiers.push(["tier_2", _asRuleList(router["tier_2"])]);
217
+ }
218
+ const matched_triggers = [];
219
+ const activated_rules = [];
220
+ for (const [tier_name, rules] of tiers) {
221
+ for (const rule of rules) {
222
+ const rule_id = rule["id"];
223
+ const rule_triggers = _asTriggerList(rule["triggers"]);
224
+ let rule_hit = false;
225
+ for (const trig of rule_triggers) {
226
+ if (trigger_matches(trig, prompt, open_files, command)) {
227
+ matched_triggers.push({ tier: tier_name, rule: rule_id, trigger: trig });
228
+ rule_hit = true;
229
+ }
230
+ }
231
+ if (rule_hit) {
232
+ activated_rules.push({ tier: tier_name, rule: rule_id });
233
+ }
234
+ }
235
+ }
236
+ // Kernel rules are always active.
237
+ const kernel = Array.isArray(router["kernel"]) ? router["kernel"] : [];
238
+ for (const kid of kernel) {
239
+ activated_rules.push({ tier: "kernel", rule: kid });
240
+ }
241
+ return { matched_triggers, activated_rules };
242
+ }
243
+ //# sourceMappingURL=router_match.js.map
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Rule-injection core — THE single module both the offline model and the
3
+ * runtime concern read.
4
+ *
5
+ * WHY ONE MODULE. `road-to-trigger-delivered-rule-bodies` step 0.5 states the
6
+ * failure this prevents in as many words: *an experiment whose offline pricing
7
+ * and runtime delivery use different matchers measures nothing*. So the price /
8
+ * recall model (`src/scripts/model_rule_injection.ts`) and the delivery concern
9
+ * (`src/scripts/hooks/rule_inject_hook.ts`) both import THIS file, and neither
10
+ * re-implements selection, ordering, capping, or body loading.
11
+ *
12
+ * IT OWNS NO MATCHER. Trigger semantics belong to
13
+ * [`_lib/router_match.ts`](router_match.ts), which is already the single
14
+ * implementation for every surface that answers "which rules fire on this
15
+ * prompt?" and is pinned by `tests/scripts/router_match_parity.test.ts`. A
16
+ * second matcher here would be the violation that test exists to catch, so
17
+ * `matchTierRules` is a thin, deterministic wrapper over `match_prompt` and
18
+ * nothing more. Step 0.3's pre-registered comparison found no reason to write
19
+ * one — see `model_rule_injection.ts`'s header for the measured verdict.
20
+ *
21
+ * KERNEL IS NEVER INJECTED. The nine kernel rules are in standing context by
22
+ * definition; injecting one would be paying twice for a body that is already
23
+ * there. `match_prompt` returns kernel ids in `activated_rules` because kernel
24
+ * rules are always active — this module filters them out by construction rather
25
+ * than relying on a caller to remember.
26
+ *
27
+ * IT IMPORTS NO TOKENIZER, AND THAT IS A HOT-PATH FACT RATHER THAN A STYLE
28
+ * CHOICE. `_lib/token_count.ts` resolves `js-tiktoken` AT MODULE LOAD, and the
29
+ * concern that imports this file is statically reachable from
30
+ * `concern_registry.ts` — so importing it here made every hook dispatch on
31
+ * every slot pay a tokenizer load for a concern that is default-OFF and emits
32
+ * nothing. Measured on this tree: unbinding the concern moved the
33
+ * `pre_tool_use` p95 from 202 ms to 196 ms, and the CI latency gate went red on
34
+ * the branch that introduced it while passing on main.
35
+ *
36
+ * So the runtime cap is in BYTES. That is not a weaker bound — it is the same
37
+ * bound in the unit the budget actually enforces: `hook-token-budget.json`
38
+ * measures "bytes of concern stdout payload fields", so the cap and its
39
+ * registered row are now the same number instead of two units that need a
40
+ * conversion factor to compare. The offline model keeps exact-BPE tokens for
41
+ * its price table, where the tokenizer is the point and nothing is on a hot
42
+ * path.
43
+ *
44
+ * DETERMINISM IS THE CONTRACT, and the collision cases are the reason it needs
45
+ * stating. Two triggers on the SAME rule collapse to one entry (a rule is
46
+ * delivered once, however many of its triggers fired) while the trigger count
47
+ * survives as `score`. Two DIFFERENT rules both firing are returned in router
48
+ * declaration order, tier_1 before tier_2 — never in match order, which would
49
+ * make the output depend on trigger authoring order inside a rule. Capping
50
+ * sorts by descending score with router order as the tie-break, so the same
51
+ * prompt against the same router always yields the same bytes.
52
+ */
53
+ import * as fs from 'node:fs';
54
+ import * as path from 'node:path';
55
+ import { match_prompt } from './router_match.js';
56
+ /** Kinds of trigger a prompt alone can never fire. */
57
+ export const PATH_TRIGGER_KINDS = ['path_prefix', 'file_pattern'];
58
+ export function loadRouter(repoRoot) {
59
+ const p = path.join(repoRoot, 'dist', 'router.json');
60
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
61
+ }
62
+ /** Kernel rule ids, as declared by the router. */
63
+ export function kernelIds(router) {
64
+ const k = Array.isArray(router['kernel']) ? router['kernel'] : [];
65
+ return new Set(k.map((x) => String(x)));
66
+ }
67
+ function tierEntries(router, tier) {
68
+ const raw = router[tier];
69
+ if (!Array.isArray(raw))
70
+ return [];
71
+ return raw.map((r) => ({
72
+ id: String(r['id'] ?? ''),
73
+ triggers: Array.isArray(r['triggers']) ? r['triggers'] : [],
74
+ }));
75
+ }
76
+ /** Every tier rule declared by the router, with its trigger list. */
77
+ export function allTierRules(router) {
78
+ const out = [];
79
+ for (const tier of ['tier_1', 'tier_2']) {
80
+ for (const e of tierEntries(router, tier))
81
+ out.push({ ...e, tier });
82
+ }
83
+ return out;
84
+ }
85
+ /** Rules whose triggers are ALL path-shaped — unreachable from a prompt alone. */
86
+ export function pathOnlyRuleIds(router) {
87
+ const ids = new Set();
88
+ for (const r of allTierRules(router)) {
89
+ if (r.triggers.length === 0)
90
+ continue;
91
+ const allPath = r.triggers.every((t) => PATH_TRIGGER_KINDS.some((k) => k in t));
92
+ if (allPath)
93
+ ids.add(r.id);
94
+ }
95
+ return ids;
96
+ }
97
+ /** Rules declaring at least one path-shaped trigger. */
98
+ export function pathCapableRuleIds(router) {
99
+ const ids = new Set();
100
+ for (const r of allTierRules(router)) {
101
+ if (r.triggers.some((t) => PATH_TRIGGER_KINDS.some((k) => k in t)))
102
+ ids.add(r.id);
103
+ }
104
+ return ids;
105
+ }
106
+ /** Rules declaring no trigger at all — cannot be delivered, must stay eager. */
107
+ export function triggerlessRuleIds(router) {
108
+ return allTierRules(router)
109
+ .filter((r) => r.triggers.length === 0)
110
+ .map((r) => r.id);
111
+ }
112
+ /**
113
+ * Which tier rules fire on this prompt (+ optional open files / command).
114
+ *
115
+ * Kernel is excluded. Order is router declaration order, tier_1 first.
116
+ */
117
+ export function matchTierRules(router, prompt, openFiles, command) {
118
+ const kernel = kernelIds(router);
119
+ const res = match_prompt(router, prompt, 'full', openFiles, command);
120
+ const scores = new Map();
121
+ for (const mt of res.matched_triggers) {
122
+ const id = String(mt.rule ?? '');
123
+ if (id === '' || kernel.has(id))
124
+ continue;
125
+ scores.set(id, (scores.get(id) ?? 0) + 1);
126
+ }
127
+ const out = [];
128
+ let order = 0;
129
+ for (const r of allTierRules(router)) {
130
+ const s = scores.get(r.id);
131
+ if (s === undefined) {
132
+ order += 1;
133
+ continue;
134
+ }
135
+ out.push({ id: r.id, tier: r.tier, score: s, order });
136
+ order += 1;
137
+ }
138
+ return out;
139
+ }
140
+ /** Where the projected body of a rule lives. */
141
+ export function ruleBodyPath(repoRoot, id) {
142
+ return path.join(repoRoot, 'dist', 'agent-src', 'rules', `${id}.md`);
143
+ }
144
+ /** The projected body, or `null` when the rule has no projected file. */
145
+ export function loadRuleBody(repoRoot, id) {
146
+ const p = ruleBodyPath(repoRoot, id);
147
+ if (!fs.existsSync(p))
148
+ return null;
149
+ return fs.readFileSync(p, 'utf8');
150
+ }
151
+ /**
152
+ * UTF-8 byte length — the runtime measure, dependency-free by design.
153
+ *
154
+ * Deliberately NOT a token count: see the header. Bytes are exact, need no
155
+ * tokenizer, and are the unit `hook-token-budget.json` enforces.
156
+ */
157
+ export function bytesOf(text) {
158
+ return Buffer.byteLength(text, 'utf8');
159
+ }
160
+ /**
161
+ * Apply the per-prompt BYTE cap.
162
+ *
163
+ * Highest score first, router order as tie-break; a rule whose body would push
164
+ * the running total past `capBytes` is dropped and the walk continues, so one
165
+ * oversized body cannot starve the rest. The returned `selected` list is
166
+ * re-sorted back into router order — cap order is a budgeting concern, delivery
167
+ * order is not.
168
+ *
169
+ * ONE selection for both callers. The offline model and the runtime concern
170
+ * call THIS function with THE SAME cap, so the set the price table prices is
171
+ * byte-for-byte the set the concern delivers. That is step 0.5's requirement,
172
+ * and expressing the cap in bytes makes it hold without a token/byte
173
+ * conversion sitting between the two arms.
174
+ */
175
+ export function selectForInjection(repoRoot, matches, capBytes, shortlist) {
176
+ const bodyBytes = new Map();
177
+ for (const m of matches) {
178
+ const body = loadRuleBody(repoRoot, m.id);
179
+ bodyBytes.set(m.id, body === null ? 0 : bytesOf(body));
180
+ }
181
+ // STEP 6.3 — the optional lexical shortlist, as a TIE-BREAK and nothing more.
182
+ //
183
+ // Absent (the default), the cap order is exactly what it was: score, then
184
+ // router order. Present, it sits BETWEEN them, never above `score` — the
185
+ // matcher's verdict is read first and the shortlist only orders what the
186
+ // matcher already tied. Membership is untouched in both directions: a
187
+ // shortlisted id the matcher did not return is not in `matches` and so is
188
+ // never consulted, and no matched id is dropped for being unshortlisted
189
+ // (it sorts at Infinity, behind the shortlisted ones, and still competes
190
+ // for the cap). That is `_lib/lexical_shortlist.ts`'s "never decides
191
+ // alone" expressed at the one place the cap actually binds.
192
+ const rank = new Map();
193
+ for (const [i, id] of (shortlist ?? []).entries()) {
194
+ if (!rank.has(id))
195
+ rank.set(id, i);
196
+ }
197
+ const ranked = [...matches].sort((a, b) => b.score - a.score ||
198
+ (rank.get(a.id) ?? Infinity) - (rank.get(b.id) ?? Infinity) ||
199
+ a.order - b.order);
200
+ const selected = [];
201
+ const dropped = [];
202
+ let total = 0;
203
+ for (const m of ranked) {
204
+ const t = bodyBytes.get(m.id) ?? 0;
205
+ if (total + t > capBytes && selected.length > 0) {
206
+ dropped.push(m);
207
+ continue;
208
+ }
209
+ selected.push(m);
210
+ total += t;
211
+ }
212
+ selected.sort((a, b) => a.order - b.order);
213
+ dropped.sort((a, b) => a.order - b.order);
214
+ return { selected, dropped, bytes: total, bodyBytes };
215
+ }
216
+ //# sourceMappingURL=rule_injection.js.map