@event4u/agent-config 9.16.0 → 9.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +80 -130
  4. package/README.md +1 -1
  5. package/dist/agent-src/commands/agent-handoff.md +100 -48
  6. package/dist/agent-src/commands/challenge-me/vision.md +5 -0
  7. package/dist/agent-src/commands/commit.md +1 -1
  8. package/dist/agent-src/commands/condense.md +19 -64
  9. package/dist/agent-src/commands/evals/agent-handoff.json +13 -1
  10. package/dist/agent-src/commands/evals/rule-compliance-audit.json +46 -0
  11. package/dist/agent-src/commands/feature/plan.md +20 -0
  12. package/dist/agent-src/commands/feature/roadmap.md +19 -0
  13. package/dist/agent-src/commands/memory/promote.md +14 -1
  14. package/dist/agent-src/commands/mode.md +9 -5
  15. package/dist/agent-src/commands/optimize/deep.md +72 -11
  16. package/dist/agent-src/commands/pr/create.md +29 -0
  17. package/dist/agent-src/commands/roadmap/create.md +29 -0
  18. package/dist/agent-src/commands/rule-compliance-audit.md +28 -5
  19. package/dist/agent-src/contexts/authority/commit-mechanics.md +5 -5
  20. package/dist/agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +1 -0
  21. package/dist/agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +1 -1
  22. package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +6 -1
  23. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +4 -1
  24. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +27 -1
  25. package/dist/agent-src/contexts/execution/plan-confidence-gate.md +133 -0
  26. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +5 -2
  27. package/dist/agent-src/profiles/agency.yml +3 -3
  28. package/dist/agent-src/profiles/content_creator.yml +2 -2
  29. package/dist/agent-src/profiles/developer.yml +2 -2
  30. package/dist/agent-src/profiles/finance.yml +1 -1
  31. package/dist/agent-src/profiles/founder.yml +2 -2
  32. package/dist/agent-src/profiles/ops.yml +2 -2
  33. package/dist/agent-src/rules/active-remediation.md +2 -0
  34. package/dist/agent-src/rules/architecture.md +4 -2
  35. package/dist/agent-src/rules/augment-edit-discipline.md +2 -0
  36. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  37. package/dist/agent-src/rules/brand-consistency.md +10 -62
  38. package/dist/agent-src/rules/brand-source-of-truth.md +62 -11
  39. package/dist/agent-src/rules/broken-access-control.md +4 -0
  40. package/dist/agent-src/rules/cli-output-handling.md +5 -0
  41. package/dist/agent-src/rules/code-comment-discipline.md +3 -0
  42. package/dist/agent-src/rules/commit-conventions.md +2 -0
  43. package/dist/agent-src/rules/context-hygiene.md +3 -2
  44. package/dist/agent-src/rules/cross-source-consistency.md +3 -0
  45. package/dist/agent-src/rules/design-fidelity.md +2 -0
  46. package/dist/agent-src/rules/doc-screenshot-hygiene.md +2 -0
  47. package/dist/agent-src/rules/docker-commands.md +4 -0
  48. package/dist/agent-src/rules/domain-adoption-policy.md +2 -0
  49. package/dist/agent-src/rules/domain-safety-disclaimer.md +5 -2
  50. package/dist/agent-src/rules/domain-safety-pii.md +1 -1
  51. package/dist/agent-src/rules/engineering-safety-floor.md +2 -0
  52. package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +9 -0
  53. package/dist/agent-src/rules/image-likeness-and-rights.md +4 -0
  54. package/dist/agent-src/rules/improve-before-implement.md +4 -0
  55. package/dist/agent-src/rules/laravel-routing.md +4 -0
  56. package/dist/agent-src/rules/legal-safety-floor.md +3 -0
  57. package/dist/agent-src/rules/lethal-trifecta-guard.md +3 -0
  58. package/dist/agent-src/rules/media-governance-routing.md +5 -0
  59. package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
  60. package/dist/agent-src/rules/no-roadmap-references.md +2 -0
  61. package/dist/agent-src/rules/notes-first-reasoning.md +2 -0
  62. package/dist/agent-src/rules/output-discipline.md +9 -2
  63. package/dist/agent-src/rules/php-coding.md +2 -0
  64. package/dist/agent-src/rules/prefer-enums-over-literals.md +3 -0
  65. package/dist/agent-src/rules/provider-lifecycle-discipline.md +5 -0
  66. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +2 -0
  67. package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
  68. package/dist/agent-src/rules/rule-type-governance.md +2 -0
  69. package/dist/agent-src/rules/scale-discipline.md +3 -0
  70. package/dist/agent-src/rules/secret-vcs-guard.md +2 -0
  71. package/dist/agent-src/rules/security-sensitive-stop.md +9 -2
  72. package/dist/agent-src/rules/senior-engineering-discipline.md +6 -0
  73. package/dist/agent-src/rules/skill-quality.md +2 -0
  74. package/dist/agent-src/rules/source-confidentiality.md +4 -0
  75. package/dist/agent-src/rules/source-discovery-gate.md +3 -0
  76. package/dist/agent-src/rules/source-of-truth.md +2 -0
  77. package/dist/agent-src/rules/strategy-safety-floor.md +2 -0
  78. package/dist/agent-src/rules/think-before-action.md +5 -0
  79. package/dist/agent-src/rules/token-budget-discipline.md +2 -2
  80. package/dist/agent-src/rules/ui-audit-gate.md +2 -0
  81. package/dist/agent-src/rules/user-interaction.md +3 -4
  82. package/dist/agent-src/skills/build-buy-partner/SKILL.md +1 -1
  83. package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +4 -4
  84. package/dist/agent-src/skills/copilot-config/SKILL.md +1 -1
  85. package/dist/agent-src/skills/deep-reading-analyst/SKILL.md +7 -4
  86. package/dist/agent-src/skills/description-assist/SKILL.md +1 -1
  87. package/dist/agent-src/skills/design-intelligence/SKILL.md +6 -180
  88. package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +100 -0
  89. package/dist/agent-src/skills/design-intelligence/references/integration-mapping.md +91 -0
  90. package/dist/agent-src/skills/design-review/SKILL.md +4 -141
  91. package/dist/agent-src/skills/design-review/references/review-communication.md +92 -0
  92. package/dist/agent-src/skills/design-review/references/verification-automation.md +61 -0
  93. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +8 -27
  94. package/dist/agent-src/skills/existing-ui-audit/references/anti-slop-cross-reference.md +31 -0
  95. package/dist/agent-src/skills/fe-design/SKILL.md +4 -289
  96. package/dist/agent-src/skills/fe-design/references/design-patterns.md +227 -0
  97. package/dist/agent-src/skills/fe-design/references/design-read-and-memory.md +75 -0
  98. package/dist/agent-src/skills/market-entry-analysis/SKILL.md +1 -1
  99. package/dist/agent-src/skills/persona-writing/SKILL.md +2 -2
  100. package/dist/agent-src/skills/readme-reviewer/SKILL.md +2 -1
  101. package/dist/agent-src/skills/roadmap-management/SKILL.md +10 -0
  102. package/dist/agent-src/skills/roadmap-writing/SKILL.md +12 -1
  103. package/dist/agent-src/skills/rule-writing/SKILL.md +4 -2
  104. package/dist/agent-src/skills/skill-reviewer/SKILL.md +1 -1
  105. package/dist/agent-src/skills/skill-writing/SKILL.md +2 -2
  106. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +3 -1
  107. package/dist/agent-src/skills/token-optimizer/SKILL.md +2 -2
  108. package/dist/agent-src/skills/ui-component-architect/SKILL.md +6 -3
  109. package/dist/agent-src/templates/agent-settings.md +13 -15
  110. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +6 -2
  111. package/dist/agent-src/templates/persona.md +2 -2
  112. package/dist/agent-src/templates/roadmaps.md +13 -1
  113. package/dist/agent-src/templates/rule.md +1 -1
  114. package/dist/agent-src/templates/scripts/work_engine/directives/ui/_fix_lane.ts +52 -0
  115. package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +8 -0
  116. package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +6 -0
  117. package/dist/agent-src/templates/scripts/work_engine/intent/classify.ts +56 -4
  118. package/dist/agent-src/templates/skill.md +1 -1
  119. package/dist/cli/registry.js +3 -0
  120. package/dist/cli/registry.js.map +1 -1
  121. package/dist/cli-delegate/chunk-AIIUU73N.js +2040 -0
  122. package/dist/cli-delegate/{chunk-ZXXJLL5K.js → chunk-BSLUHV2F.js} +127 -1487
  123. package/dist/cli-delegate/{chunk-H574KOUH.js → chunk-ELMFOQPW.js} +35 -1
  124. package/dist/cli-delegate/chunk-LF2KKJ4Q.js +183 -0
  125. package/dist/cli-delegate/chunk-VSYT7OHO.js +1382 -0
  126. package/dist/cli-delegate/chunk-XGA2NTU4.js +3727 -0
  127. package/dist/cli-delegate/cmd_conformance.js +4 -3
  128. package/dist/cli-delegate/cmd_converge.js +3 -3
  129. package/dist/cli-delegate/cmd_doctor.js +3 -2
  130. package/dist/cli-delegate/cmd_explain.js +36 -36
  131. package/dist/cli-delegate/cmd_export.js +4 -4
  132. package/dist/cli-delegate/cmd_handoff.js +1169 -0
  133. package/dist/cli-delegate/cmd_memory_get.js +35 -3754
  134. package/dist/cli-delegate/cmd_migrate.js +2 -2
  135. package/dist/cli-delegate/cmd_prune.js +4 -4
  136. package/dist/cli-delegate/cmd_route_audit.js +249 -0
  137. package/dist/cli-delegate/cmd_route_explain.js +285 -0
  138. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  139. package/dist/cli-delegate/cmd_sync.js +4 -4
  140. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  141. package/dist/cli-delegate/cmd_update.js +3 -3
  142. package/dist/cli-delegate/cmd_upgrade.js +3 -3
  143. package/dist/cli-delegate/cmd_validate.js +4 -4
  144. package/dist/cli-delegate/cmd_versions.js +3 -3
  145. package/dist/discovery/deprecation-report.md +1 -1
  146. package/dist/discovery/discovery-manifest.json +84 -84
  147. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  148. package/dist/discovery/discovery-manifest.summary.md +1 -1
  149. package/dist/discovery/orphan-report.md +1 -1
  150. package/dist/discovery/packs.json +2 -2
  151. package/dist/discovery/trust-report.md +1 -1
  152. package/dist/discovery/workspaces.json +2 -2
  153. package/dist/hooks/dispatch.js +602 -507
  154. package/dist/install/install.mjs +34 -0
  155. package/dist/mcp/awesome-mcp-servers.row.md +1 -1
  156. package/dist/mcp/mcp-cloudflare-catalogue.json +2 -2
  157. package/dist/mcp/registry-manifest.json +4 -4
  158. package/dist/mcp/server.json +2 -2
  159. package/dist/mcp/server.mjs +25 -3
  160. package/dist/router.json +1 -1
  161. package/dist/scripts/_lib/agent_src.js +39 -6
  162. package/dist/scripts/_lib/agent_src.js.map +1 -1
  163. package/dist/server/schemas/settings.js +25 -0
  164. package/dist/server/schemas/settings.js.map +1 -1
  165. package/docs/guidelines/abstraction-thresholds.md +53 -0
  166. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +1 -1
  167. package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +7 -0
  168. package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +2 -2
  169. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +5 -3
  170. package/docs/guidelines/agent-infra/size-and-scope.md +8 -2
  171. package/docs/guidelines/component-oriented-and-oop-development.md +11 -6
  172. package/docs/guidelines/docs/readme-size-and-splitting.md +5 -2
  173. package/llms.txt +1 -0
  174. package/package.json +1 -1
  175. package/src/agent-src/templates/scripts/work_engine/directives/ui/_fix_lane.ts +52 -0
  176. package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +8 -0
  177. package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +6 -0
  178. package/src/agent-src/templates/scripts/work_engine/intent/classify.ts +56 -4
  179. package/src/config/agent-settings.template.yml +68 -0
  180. package/src/config/evaluator-budgets.json +8 -11
  181. package/src/config/gate-coverage.yml +70 -0
  182. package/src/scripts/_cli/cmd_explain.ts +41 -36
  183. package/src/scripts/_cli/cmd_handoff.ts +405 -0
  184. package/src/scripts/_cli/cmd_route_audit.ts +290 -0
  185. package/src/scripts/_cli/cmd_route_explain.ts +269 -0
  186. package/src/scripts/_cli/handoff_generate.ts +453 -0
  187. package/src/scripts/_cli/handoff_sessions.ts +435 -0
  188. package/src/scripts/_dispatch.bash +32 -0
  189. package/src/scripts/_lib/agent_src.ts +36 -6
  190. package/src/scripts/_lib/git_env.ts +40 -0
  191. package/src/scripts/_lib/md_table.ts +27 -0
  192. package/src/scripts/_lib/planning_settings.ts +43 -0
  193. package/src/scripts/_lib/release_material.ts +156 -0
  194. package/src/scripts/_lib/router_match.ts +301 -0
  195. package/src/scripts/annotate_r1_outcomes.ts +258 -0
  196. package/src/scripts/check_completion_review.ts +1140 -0
  197. package/src/scripts/check_condensation.ts +2 -1
  198. package/src/scripts/check_finding_dispositions.ts +278 -0
  199. package/src/scripts/check_pack_size.ts +4 -3
  200. package/src/scripts/check_references.ts +1 -0
  201. package/src/scripts/check_release_highlights.ts +300 -0
  202. package/src/scripts/check_release_pr_shape.ts +4 -0
  203. package/src/scripts/check_release_surface_equality.ts +132 -0
  204. package/src/scripts/check_reply_consistency.ts +187 -138
  205. package/src/scripts/compile_router.ts +20 -3
  206. package/src/scripts/dispatch_r2_reviewer.ts +968 -0
  207. package/src/scripts/eval_ui_triviality.ts +156 -0
  208. package/src/scripts/handoff_context_hook.ts +148 -0
  209. package/src/scripts/hook_manifest.yaml +17 -7
  210. package/src/scripts/hooks/concern_registry.ts +2 -0
  211. package/src/scripts/lint_abstraction_thresholds.ts +327 -0
  212. package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
  213. package/src/scripts/lint_plan_risk_register.ts +931 -0
  214. package/src/scripts/lint_profile_personas.ts +153 -0
  215. package/src/scripts/lint_token_budget_discipline.ts +148 -0
  216. package/src/scripts/lint_trigger_collisions.ts +257 -0
  217. package/src/scripts/release.ts +156 -32
  218. package/src/scripts/release_drill.ts +491 -0
  219. package/src/scripts/router_telemetry.ts +16 -290
  220. package/src/scripts/schemas/command.schema.json +33 -0
  221. package/src/scripts/schemas/rule.schema.json +10 -0
  222. package/src/scripts/self_review_gate.ts +41 -3
  223. package/src/scripts/skill_linter.ts +3 -1
  224. package/src/scripts/smoke/router.sh +1 -1
  225. package/src/scripts/update_counts.ts +60 -12
  226. package/src/server/schemas/settings.ts +41 -0
  227. package/dist/cli-delegate/{chunk-XX6TOPD7.js → chunk-WR6GTRHJ.js} +3 -3
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Single-source release material (release-truth Phase 1).
3
+ *
4
+ * The recorded 9.14.0 failure: PR body, CHANGELOG entry, GitHub release
5
+ * body, and tag metadata were generated at different times from different
6
+ * scopes — the PR body said 10,054 tests while the merged changelog carried
7
+ * 10,056 and more items. The fix is structural: the CHANGELOG section at the
8
+ * relevant head is the ONE final source, and every other surface is a pure
9
+ * derivation of it (release.ts re-derives on branch update / resume / merge,
10
+ * so late commits cannot desynchronize the surfaces).
11
+ *
12
+ * Everything here is a pure function of strings so the equality gate
13
+ * (`check_release_surface_equality.ts`) and the fixture tests exercise the
14
+ * exact production code paths.
15
+ */
16
+
17
+ export interface ChangelogSection {
18
+ /** The `## …` heading line, verbatim. */
19
+ heading: string;
20
+ /** Section content below the heading, outer blank lines stripped. */
21
+ body: string;
22
+ }
23
+
24
+ const _NEXT_HEADING_RE = /^##\s+\[?\d+\.\d+\.\d+/m;
25
+
26
+ function _reEscape(s: string): string {
27
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
28
+ }
29
+
30
+ /**
31
+ * Extract the CHANGELOG section for `version` — heading plus body up to the
32
+ * next version heading (or end of file). Returns null when no heading for
33
+ * `version` exists. Matches both heading shapes the generator emits
34
+ * (`## X.Y.Z (date)` and `## [X.Y.Z](compare-url) (date)`).
35
+ */
36
+ export function extract_changelog_section(text: string, version: string): ChangelogSection | null {
37
+ const headingRe = new RegExp(`^##\\s+\\[?${_reEscape(version)}\\b.*$`, 'm');
38
+ const m = headingRe.exec(text);
39
+ if (!m) {
40
+ return null;
41
+ }
42
+ const bodyStart = m.index + m[0].length;
43
+ const rest = text.slice(bodyStart);
44
+ const next = _NEXT_HEADING_RE.exec(rest);
45
+ const sectionEnd = bodyStart + (next ? next.index : rest.length);
46
+ const body = text
47
+ .slice(bodyStart, sectionEnd)
48
+ .replace(/^\n+/u, '')
49
+ .replace(/\s+$/u, '');
50
+ return { heading: m[0], body };
51
+ }
52
+
53
+ /** Footer appended to the release-PR body; the strip helper accepts legacy variants. */
54
+ export const PR_BODY_FOOTER = 'Created by `src/scripts/release.ts`.';
55
+ const _LEGACY_PR_FOOTERS = ['Created by `scripts/release.py`.', PR_BODY_FOOTER];
56
+
57
+ /** Marker `_cap_body` embeds when a surface was truncated to a GitHub limit. */
58
+ export const TRUNCATION_MARKER = '_Changelog truncated to fit GitHub';
59
+
60
+ /** Release-PR body derived from the (possibly capped) changelog-section body. */
61
+ export function pr_body_from_section(cappedBody: string, version: string): string {
62
+ return `Release ${version}.\n\n${cappedBody}\n\n${PR_BODY_FOOTER}`;
63
+ }
64
+
65
+ /** GitHub-release notes derived from the (possibly capped) changelog-section body. */
66
+ export function release_notes_from_section(cappedBody: string, version: string): string {
67
+ return cappedBody || `Release ${version}`;
68
+ }
69
+
70
+ /** Annotated-tag message derived from the merged changelog-section body. */
71
+ export function tag_message_from_section(body: string, version: string): string {
72
+ const trailer = body ? `\n\n${body}` : '';
73
+ return `release: ${version}${trailer}\n`;
74
+ }
75
+
76
+ /**
77
+ * Inverse of `pr_body_from_section`: return the changelog-derived middle of a
78
+ * release-PR body, or null when the body does not carry the canonical
79
+ * `Release X.Y.Z.` prefix (then it was not generated by this pipeline).
80
+ */
81
+ export function strip_pr_wrapper(prBody: string, version: string): string | null {
82
+ const prefix = `Release ${version}.`;
83
+ let s = prBody.replace(/\r\n/gu, '\n').trim();
84
+ if (!s.startsWith(prefix)) {
85
+ return null;
86
+ }
87
+ s = s.slice(prefix.length);
88
+ for (const footer of _LEGACY_PR_FOOTERS) {
89
+ if (s.trimEnd().endsWith(footer)) {
90
+ s = s.trimEnd();
91
+ s = s.slice(0, s.length - footer.length);
92
+ break;
93
+ }
94
+ }
95
+ return s.replace(/^\n+/u, '').replace(/\s+$/u, '');
96
+ }
97
+
98
+ /**
99
+ * Whitespace/anchor normalization — deliberately nothing stronger, per the
100
+ * roadmap lock ("not 'similar', equal"): CRLF → LF, trailing whitespace per
101
+ * line dropped, runs of blank lines collapsed to one, outer blank lines
102
+ * trimmed. Content differences always survive normalization.
103
+ */
104
+ export function normalize_release_text(s: string): string {
105
+ return s
106
+ .replace(/\r\n/gu, '\n')
107
+ .split('\n')
108
+ .map((line) => line.replace(/[ \t]+$/u, ''))
109
+ .join('\n')
110
+ .replace(/\n{3,}/gu, '\n\n')
111
+ .replace(/^\n+/u, '')
112
+ .replace(/\n+$/u, '');
113
+ }
114
+
115
+ /**
116
+ * Compare a derived release surface against the changelog-section body it
117
+ * must equal. Returns null when equal (after normalization), else a
118
+ * human-readable first-divergence report. A truncated surface (carrying the
119
+ * `_cap_body` notice) is compared as a prefix of the changelog side — the
120
+ * truncation itself is not a divergence.
121
+ */
122
+ export function surface_divergence(surfaceText: string, changelogBody: string): string | null {
123
+ const a = normalize_release_text(surfaceText);
124
+ const b = normalize_release_text(changelogBody);
125
+ if (a.includes(TRUNCATION_MARKER)) {
126
+ const cut = a.indexOf(TRUNCATION_MARKER);
127
+ const prefix = a
128
+ .slice(0, cut)
129
+ .replace(/>\s*$/u, '')
130
+ .replace(/\n+$/u, '');
131
+ if (b.startsWith(prefix)) {
132
+ return null;
133
+ }
134
+ return _first_diff(prefix, b.slice(0, prefix.length));
135
+ }
136
+ if (a === b) {
137
+ return null;
138
+ }
139
+ return _first_diff(a, b);
140
+ }
141
+
142
+ function _first_diff(a: string, b: string): string {
143
+ const al = a.split('\n');
144
+ const bl = b.split('\n');
145
+ const n = Math.max(al.length, bl.length);
146
+ for (let i = 0; i < n; i++) {
147
+ if (al[i] !== bl[i]) {
148
+ return (
149
+ `first divergence at line ${i + 1}: ` +
150
+ `surface=${JSON.stringify(al[i] ?? '<missing>')} ` +
151
+ `changelog=${JSON.stringify(bl[i] ?? '<missing>')}`
152
+ );
153
+ }
154
+ }
155
+ return 'surfaces differ only in normalization-invisible content';
156
+ }
@@ -0,0 +1,301 @@
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
+
22
+ export type JsonValue =
23
+ | string
24
+ | number
25
+ | boolean
26
+ | null
27
+ | JsonValue[]
28
+ | { [key: string]: JsonValue };
29
+
30
+ export type Trigger = Record<string, JsonValue>;
31
+
32
+ export interface Rule {
33
+ id?: JsonValue;
34
+ triggers?: JsonValue;
35
+ [key: string]: JsonValue;
36
+ }
37
+
38
+ export interface Router {
39
+ kernel?: JsonValue;
40
+ tier_1?: JsonValue;
41
+ tier_2?: JsonValue;
42
+ [key: string]: JsonValue;
43
+ }
44
+
45
+ // ── fnmatch (Python fnmatch.fnmatch) ─────────────────────────────────────
46
+
47
+ /**
48
+ * Translate a Unix shell pattern into a RegExp, matching CPython's
49
+ * `fnmatch.translate`. `*` → `.*`, `?` → `.`, `[...]` char classes
50
+ * preserved (with `!` → `^` negation), everything else escaped.
51
+ * Matching is case-sensitive on POSIX (the corpus paths are POSIX).
52
+ */
53
+ function _fnmatchToRegExp(pat: string): RegExp {
54
+ let res = "";
55
+ let i = 0;
56
+ const n = pat.length;
57
+ while (i < n) {
58
+ const c = pat[i] as string;
59
+ i += 1;
60
+ if (c === "*") {
61
+ res += ".*";
62
+ } else if (c === "?") {
63
+ res += ".";
64
+ } else if (c === "[") {
65
+ let j = i;
66
+ if (j < n && pat[j] === "!") {
67
+ j += 1;
68
+ }
69
+ if (j < n && pat[j] === "]") {
70
+ j += 1;
71
+ }
72
+ while (j < n && pat[j] !== "]") {
73
+ j += 1;
74
+ }
75
+ if (j >= n) {
76
+ res += "\\[";
77
+ } else {
78
+ let stuff = pat.slice(i, j);
79
+ if (!stuff.includes("-")) {
80
+ stuff = stuff.replace(/\\/g, "\\\\");
81
+ } else {
82
+ // Faithful port of CPython's dash-handling in fnmatch.translate:
83
+ // split the class body on `-` (bounded to [i, j)), escaping each
84
+ // chunk, then re-join with `-` so ranges survive.
85
+ const chunks: string[] = [];
86
+ let k = pat[i] === "!" ? i + 2 : i + 1;
87
+ let start = i;
88
+ while (true) {
89
+ const rel = pat.slice(k, j).indexOf("-");
90
+ if (rel < 0) {
91
+ break;
92
+ }
93
+ const idx = k + rel;
94
+ chunks.push(pat.slice(start, idx));
95
+ start = idx + 1;
96
+ k = idx + 3;
97
+ }
98
+ chunks.push(pat.slice(start, j));
99
+ stuff = chunks
100
+ .map((s) => s.replace(/\\/g, "\\\\").replace(/-/g, "\\-"))
101
+ .join("-");
102
+ }
103
+ // Escape regex set-meta chars except backslash/dash already handled.
104
+ stuff = stuff.replace(/([&~|])/g, "\\$1");
105
+ i = j + 1;
106
+ if (stuff.startsWith("!")) {
107
+ stuff = "^" + stuff.slice(1);
108
+ } else if (stuff.startsWith("^") || stuff.startsWith("[")) {
109
+ stuff = "\\" + stuff;
110
+ }
111
+ res += `[${stuff}]`;
112
+ }
113
+ } else {
114
+ res += c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
115
+ }
116
+ }
117
+ return new RegExp(`^(?:${res})$`, "s");
118
+ }
119
+
120
+ export function _fnmatch(name: string, pat: string): boolean {
121
+ return _fnmatchToRegExp(pat).test(name);
122
+ }
123
+
124
+ // ── Trigger matching ─────────────────────────────────────────────────────
125
+
126
+ /** Apply one trigger to a prompt + context; return True on match. */
127
+ /** Unicode word character — letters, digits, underscore (incl. umlauts). */
128
+ const _WORD_CHAR = /[\p{L}\p{N}_]/u;
129
+
130
+ function _isWordChar(c: string | undefined): boolean {
131
+ return c !== undefined && _WORD_CHAR.test(c);
132
+ }
133
+
134
+ /**
135
+ * Word-boundary-anchored keyword match (road-to-tested-routing Phase 3).
136
+ *
137
+ * A `keyword` occurrence counts only when its word-character edges sit on
138
+ * word boundaries: `ac` no longer fires inside "black", `plan` no longer
139
+ * fires inside "planet". Two deliberate reliefs:
140
+ *
141
+ * - An edge that is itself a NON-word character (e.g. `__()`, `/image:`,
142
+ * `trans(`, an emoji) carries no boundary requirement on that side —
143
+ * punctuation-shaped keywords keep their exact substring semantics.
144
+ * - The right boundary accepts one optional plural `s` ("icons", "options",
145
+ * "secrets" still fire their singular keyword). Richer inflection (German
146
+ * verb endings: "implementiere", "committen") is a DOCUMENTED accepted
147
+ * recall cost of anchoring — the matrices carry standalone-token phrasings
148
+ * for those languages instead.
149
+ *
150
+ * `phrase` deliberately stays unanchored substring; since this change the
151
+ * two trigger kinds genuinely differ.
152
+ */
153
+ export function keyword_matches_anchored(prompt_lower: string, kw_lower: string): boolean {
154
+ if (kw_lower === "") {
155
+ return false;
156
+ }
157
+ const needLeft = _isWordChar(kw_lower[0]);
158
+ const needRight = _isWordChar(kw_lower[kw_lower.length - 1]);
159
+ let idx = prompt_lower.indexOf(kw_lower);
160
+ while (idx !== -1) {
161
+ const leftOk = !needLeft || !_isWordChar(prompt_lower[idx - 1]);
162
+ let rightOk = true;
163
+ if (needRight) {
164
+ let end = idx + kw_lower.length;
165
+ if (prompt_lower[end] === "s") {
166
+ end += 1; // optional plural `s`
167
+ }
168
+ rightOk = !_isWordChar(prompt_lower[end]);
169
+ }
170
+ if (leftOk && rightOk) {
171
+ return true;
172
+ }
173
+ idx = prompt_lower.indexOf(kw_lower, idx + 1);
174
+ }
175
+ return false;
176
+ }
177
+
178
+ export function trigger_matches(
179
+ trigger: Trigger,
180
+ prompt: string,
181
+ open_files?: Iterable<string> | null,
182
+ command?: string | null,
183
+ ): boolean {
184
+ const prompt_lower = prompt.toLowerCase();
185
+ if ("keyword" in trigger) {
186
+ return keyword_matches_anchored(prompt_lower, String(trigger["keyword"]).toLowerCase());
187
+ }
188
+ if ("phrase" in trigger) {
189
+ return prompt_lower.includes(String(trigger["phrase"]).toLowerCase());
190
+ }
191
+ if ("command" in trigger) {
192
+ if (!command) {
193
+ return false;
194
+ }
195
+ return command.startsWith(String(trigger["command"]));
196
+ }
197
+ if ("path_prefix" in trigger) {
198
+ if (!open_files) {
199
+ return false;
200
+ }
201
+ const pref = String(trigger["path_prefix"]);
202
+ for (const p of open_files) {
203
+ if (String(p).startsWith(pref)) {
204
+ return true;
205
+ }
206
+ }
207
+ return false;
208
+ }
209
+ if ("file_pattern" in trigger) {
210
+ if (!open_files) {
211
+ return false;
212
+ }
213
+ const pat = String(trigger["file_pattern"]);
214
+ for (const p of open_files) {
215
+ if (_fnmatch(String(p), pat)) {
216
+ return true;
217
+ }
218
+ }
219
+ return false;
220
+ }
221
+ return false;
222
+ }
223
+
224
+ export interface MatchedTrigger {
225
+ tier: string;
226
+ rule: JsonValue | undefined;
227
+ trigger: Trigger;
228
+ }
229
+
230
+ export interface ActivatedRule {
231
+ tier: string;
232
+ rule: JsonValue | undefined;
233
+ }
234
+
235
+ export interface MatchResult {
236
+ matched_triggers: MatchedTrigger[];
237
+ activated_rules: ActivatedRule[];
238
+ }
239
+
240
+ export function _asRuleList(value: JsonValue | undefined): Rule[] {
241
+ if (!Array.isArray(value)) {
242
+ return [];
243
+ }
244
+ return value as unknown as Rule[];
245
+ }
246
+
247
+ export function _asTriggerList(value: JsonValue | undefined): Trigger[] {
248
+ if (!Array.isArray(value)) {
249
+ return [];
250
+ }
251
+ return value as unknown as Trigger[];
252
+ }
253
+
254
+ /**
255
+ * Return the matched-triggers + activated-rules for one prompt.
256
+ *
257
+ * Kernel rules are always active. tier_1 always considered. tier_2
258
+ * only considered when `profile == 'full'`.
259
+ */
260
+ export function match_prompt(
261
+ router: Router,
262
+ prompt: string,
263
+ profile = "full",
264
+ open_files?: Iterable<string> | null,
265
+ command?: string | null,
266
+ ): MatchResult {
267
+ const tiers: Array<[string, Rule[]]> = [
268
+ ["tier_1", _asRuleList(router["tier_1"])],
269
+ ];
270
+ if (profile === "full") {
271
+ tiers.push(["tier_2", _asRuleList(router["tier_2"])]);
272
+ }
273
+
274
+ const matched_triggers: MatchedTrigger[] = [];
275
+ const activated_rules: ActivatedRule[] = [];
276
+
277
+ for (const [tier_name, rules] of tiers) {
278
+ for (const rule of rules) {
279
+ const rule_id = rule["id"];
280
+ const rule_triggers = _asTriggerList(rule["triggers"]);
281
+ let rule_hit = false;
282
+ for (const trig of rule_triggers) {
283
+ if (trigger_matches(trig, prompt, open_files, command)) {
284
+ matched_triggers.push({ tier: tier_name, rule: rule_id, trigger: trig });
285
+ rule_hit = true;
286
+ }
287
+ }
288
+ if (rule_hit) {
289
+ activated_rules.push({ tier: tier_name, rule: rule_id });
290
+ }
291
+ }
292
+ }
293
+
294
+ // Kernel rules are always active.
295
+ const kernel = Array.isArray(router["kernel"]) ? router["kernel"] : [];
296
+ for (const kid of kernel) {
297
+ activated_rules.push({ tier: "kernel", rule: kid });
298
+ }
299
+
300
+ return { matched_triggers, activated_rules };
301
+ }