@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
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The `suggest_skill_for_task` MCP handler, extracted from `tools.ts`.
3
+ *
4
+ * `tools.ts` sits ~580 lines past the 1,500-line ceiling `check_source_size_budget`
5
+ * enforces as a shrink-only ratchet, so every line there costs one unit of excess
6
+ * and the gate names re-pinning the baseline a defect rather than a fix. Adding a
7
+ * `skills_roots` field to this handler's answer cost four; extracting the handler
8
+ * pays that back many times over and puts it in a file where its prose is free.
9
+ *
10
+ * Cohesive on its own terms too: this is the one handler in the allowlist that
11
+ * owns a ranking policy rather than reading a file, and it is the only consumer
12
+ * of the skill-catalogue resolver on the server side.
13
+ */
14
+
15
+ /**
16
+ * `suggest_skill_for_task` — the recovery path for a truncated catalogue.
17
+ *
18
+ * Promoted from a discovery stub because the defect it answers is measured, not
19
+ * hypothetical: on the one host that publishes its own truncation, a default
20
+ * install had 402 entries dropped from the model-visible skills list — the
21
+ * host's own count against its own denominator, not a subtraction over ours.
22
+ * An agent
23
+ * that cannot see a skill also cannot ask for it by name, so the only reachable
24
+ * question is "what fits this task" — which is what the deterministic ranker
25
+ * answers, over the TREE rather than over the host's catalogue.
26
+ *
27
+ * Read-only and shell-free by construction: it reads SKILL.md frontmatter and
28
+ * returns names, scores and declared personas. No skill BODY is returned — the
29
+ * caller opens what it picked, so a wrong rank costs one read rather than a
30
+ * context payload.
31
+ */
32
+ export async function suggestSkillForTask(
33
+ args: Record<string, unknown>,
34
+ consumerRoot: string,
35
+ ): Promise<Record<string, unknown>> {
36
+ const task = typeof args['task'] === 'string' ? args['task'] : '';
37
+ if (task.trim() === '') {
38
+ return { status: 'error', error: 'task must be a non-empty string', suggestions: [] };
39
+ }
40
+ // `limit`, not `top` — the stub published `limit` and a consumer may have
41
+ // read that contract already. Renaming it on promotion would break a
42
+ // caller for cosmetic reasons.
43
+ const limitRaw = args['limit'];
44
+ const limit =
45
+ typeof limitRaw === 'number' && Number.isFinite(limitRaw) ? Math.floor(limitRaw) : 5;
46
+
47
+ const catalogue = await import('../_lib/skill_catalogue.js');
48
+ // EVERY readable root — see `resolveSkillCatalogueRoots` on why not the first.
49
+ const roots = catalogue.resolveSkillCatalogueRoots(consumerRoot);
50
+ if (roots.length === 0) {
51
+ // A missing catalogue is UNKNOWN, never "no skill fits this task". The
52
+ // second reads as a ranked answer and would be a confident empty list —
53
+ // the same zero-from-silence failure `capture_skill_catalogue` refuses.
54
+ return {
55
+ status: 'no_catalogue',
56
+ searched: [...catalogue.DEFAULT_CATALOGUE_ROOTS],
57
+ consumer_root: consumerRoot,
58
+ suggestions: [],
59
+ };
60
+ }
61
+
62
+ const { rank } = await import('../skill_tools/score_skill_relevance.js');
63
+ const rows = rank(task, roots).slice(0, Math.max(1, limit));
64
+ return {
65
+ status: 'ok',
66
+ // `skills_root` kept verbatim (a consumer may read it); `skills_roots` is
67
+ // the additive truth about what was actually ranked.
68
+ skills_root: roots[0] as string,
69
+ skills_roots: roots,
70
+ suggestions: rows.map(([name, score, personas]) => ({ skill: name, score, personas })),
71
+ };
72
+ }
@@ -33,7 +33,13 @@ export const CATALOG_DESCRIPTION =
33
33
  'backlog: per ADR-132 they are NOT registered on the stdio tools/list ' +
34
34
  "(the wire carries implemented tools only); the 'not_implemented' " +
35
35
  'envelope contract (docs/contracts/mcp-tool-stub-envelope.md) remains ' +
36
- 'for Worker parity. Generated by `build_mcp_catalog` — ' +
36
+ 'for Worker parity. ' +
37
+ "The 'install_hint_stdio' field is the INSTALLED-BINARY form and " +
38
+ 'therefore assumes `agent-config` is on PATH (a global install, or an ' +
39
+ 'npx-resolved one the client already has); it deliberately carries no ' +
40
+ '`npx` invocation, because the un-prompted form of that command is the ' +
41
+ "one string this file's own gate forbids and the prompted form hangs a " +
42
+ 'non-interactive client start. Generated by `build_mcp_catalog` — ' +
37
43
  'never hand-edit this file.';
38
44
 
39
45
  export interface StubToolSource {
@@ -47,12 +47,15 @@ import {
47
47
  slice_around as _chSliceAround,
48
48
  read_header as _chReadHeaderImpl,
49
49
  } from '../chat_history.js';
50
+ import { suggestSkillForTask } from './suggest_skill.js';
50
51
  import {
51
52
  type CatalogEntry,
52
53
  install_hint as _catalog_install_hint,
53
54
  load_catalog,
54
55
  not_implemented_envelope,
55
56
  } from './catalog.js';
57
+ import { GRAPH_TOOLS } from './graph_tools.js';
58
+ import { resolvePath as _resolvePath, strip as _strip } from './path_util.js';
56
59
  import { type Outcome, record_call } from './telemetry.js';
57
60
  import type { PromptCache } from './prompts.js';
58
61
  import type { ResourceCache } from './resources.js';
@@ -110,19 +113,6 @@ export interface BuiltinTool {
110
113
  readonly handler: ToolHandler;
111
114
  }
112
115
 
113
- /** Mirror Python `str.strip()` (also strips the same Unicode whitespace set we care about here). */
114
- function _strip(s: string): string {
115
- return s.replace(/^\s+/, '').replace(/\s+$/, '');
116
- }
117
-
118
- /** Mirror Python `Path(...).resolve()` — absolutize + realpath, tolerating a missing tail. */
119
- function _resolvePath(p: string): string {
120
- try {
121
- return fs.realpathSync(p);
122
- } catch {
123
- return path.resolve(p);
124
- }
125
- }
126
116
 
127
117
  /**
128
118
  * Pick the consumer-project root.
@@ -1210,62 +1200,8 @@ async function _readResourceBodyHandler(
1210
1200
  // unlisted tool. Boot-time stderr log enumerates the registered set.
1211
1201
  // ---------------------------------------------------------------------
1212
1202
 
1213
- /**
1214
- * `suggest_skill_for_task` — the recovery path for a truncated catalogue.
1215
- *
1216
- * Promoted from a discovery stub because the defect it answers is measured, not
1217
- * hypothetical: on the one host that publishes its own truncation, a default
1218
- * install had 402 entries dropped from the model-visible skills list — the
1219
- * host's own count against its own denominator, not a subtraction over ours
1220
- * (`agents/evidence/analysis/scoped-projection-host-delivery.md`). An agent
1221
- * that cannot see a skill also cannot ask for it by name, so the only reachable
1222
- * question is "what fits this task" — which is what the deterministic ranker
1223
- * answers, over the TREE rather than over the host's catalogue.
1224
- *
1225
- * Read-only and shell-free by construction: it reads SKILL.md frontmatter and
1226
- * returns names, scores and declared personas. No skill BODY is returned — the
1227
- * caller opens what it picked, so a wrong rank costs one read rather than a
1228
- * context payload.
1229
- */
1230
- async function _suggestSkillForTaskHandler(
1231
- args: Record<string, unknown>,
1232
- consumerRoot: string,
1233
- ): Promise<Record<string, unknown>> {
1234
- const task = typeof args['task'] === 'string' ? args['task'] : '';
1235
- if (task.trim() === '') {
1236
- return { status: 'error', error: 'task must be a non-empty string', suggestions: [] };
1237
- }
1238
- // `limit`, not `top` — the stub published `limit` and a consumer may have
1239
- // read that contract already. Renaming it on promotion would break a
1240
- // caller for cosmetic reasons.
1241
- const limitRaw = args['limit'];
1242
- const limit =
1243
- typeof limitRaw === 'number' && Number.isFinite(limitRaw) ? Math.floor(limitRaw) : 5;
1244
-
1245
- const catalogue = await import('../_lib/skill_catalogue.js');
1246
- const skillsDir = catalogue.resolveSkillsRoot(consumerRoot);
1247
- if (skillsDir === null) {
1248
- // A missing catalogue is UNKNOWN, never "no skill fits this task". The
1249
- // second reads as a ranked answer and would be a confident empty list —
1250
- // the same zero-from-silence failure `capture_skill_catalogue` refuses.
1251
- return {
1252
- status: 'no_catalogue',
1253
- searched: [...catalogue.DEFAULT_CATALOGUE_ROOTS],
1254
- consumer_root: consumerRoot,
1255
- suggestions: [],
1256
- };
1257
- }
1258
-
1259
- const { rank } = await import('../skill_tools/score_skill_relevance.js');
1260
- const rows = rank(task, skillsDir).slice(0, Math.max(1, limit));
1261
- return {
1262
- status: 'ok',
1263
- skills_root: skillsDir,
1264
- suggestions: rows.map(([name, score, personas]) => ({ skill: name, score, personas })),
1265
- };
1266
- }
1267
-
1268
1203
  export const ALLOWLIST: Record<string, BuiltinTool> = {
1204
+ ...GRAPH_TOOLS,
1269
1205
  suggest_skill_for_task: {
1270
1206
  name: 'suggest_skill_for_task',
1271
1207
  side_effect: 'ro',
@@ -1296,7 +1232,7 @@ export const ALLOWLIST: Record<string, BuiltinTool> = {
1296
1232
  },
1297
1233
  additionalProperties: false,
1298
1234
  },
1299
- handler: _suggestSkillForTaskHandler,
1235
+ handler: suggestSkillForTask,
1300
1236
  },
1301
1237
  lint_skills: {
1302
1238
  name: 'lint_skills',
@@ -355,8 +355,13 @@ the explicit `dry-run` subcommand:
355
355
  `scripts/media/lib/fixtures/<adapter-id>/`.
356
356
  - Exit code 0.
357
357
 
358
- Fixtures are committed and cover one happy path per adapter.
359
- Phase 6 golden runs assert byte-identical stdout under
358
+ Fixtures are committed and cover one happy path per adapter;
359
+ committed media assets are **non-decodable ASCII placeholders**
360
+ whose documented paths and presence, rather than encoded-media
361
+ validity, form the fixture contract. The placeholder literal is
362
+ `FIXTURE-<adapter-id>-<ext>`. A test that needs decodable media
363
+ generates it at runtime — it must not read a fixture expecting
364
+ one. Phase 6 golden runs assert byte-identical stdout under
360
365
  `AIV_DRYRUN=true`.
361
366
 
362
367
  ## Logging & redaction
@@ -60,6 +60,7 @@ import {
60
60
  loadRuleBody,
61
61
  matchTierRules,
62
62
  pathCapableRuleIds,
63
+ pathOnlyRuleIds,
63
64
  selectForInjection,
64
65
  triggerlessRuleIds,
65
66
  type Router,
@@ -101,6 +102,18 @@ export interface CorpusCase {
101
102
  rule: string;
102
103
  prompt: string;
103
104
  openFiles: string[] | null;
105
+ /**
106
+ * The slash command the case declares, or `null` (R2 finding 1).
107
+ *
108
+ * The loader DROPPED this field, so every `command:` trigger scored as an
109
+ * unmatched plain prompt. Two entries in the frozen corpus carry one —
110
+ * `roadmap-progress-sync` and `user-interrupt-priority` — and the shipped
111
+ * concern DOES reach command triggers: `hooks/rule_inject_hook.ts` extracts
112
+ * a leading `/command` from the prompt text and passes it to
113
+ * `matchTierRules`. Dropping the field made the shipped-reach reading
114
+ * falsely low in exactly the two places it mattered.
115
+ */
116
+ command: string | null;
104
117
  label: 'positive' | 'near_miss';
105
118
  file: string;
106
119
  }
@@ -119,10 +132,12 @@ function _asCases(
119
132
  const prompt = typeof o['prompt'] === 'string' ? o['prompt'] : null;
120
133
  if (prompt === null) continue;
121
134
  const of = o['open_files'];
135
+ const cmd = o['command'];
122
136
  out.push({
123
137
  rule,
124
138
  prompt,
125
139
  openFiles: Array.isArray(of) ? of.map((x) => String(x)) : null,
140
+ command: typeof cmd === 'string' && cmd !== '' ? cmd : null,
126
141
  label,
127
142
  file,
128
143
  });
@@ -618,17 +633,54 @@ export function runEndpoints(corpusDir: string): EndpointResult[] {
618
633
  }
619
634
 
620
635
  // (b) No labelled rule may end with ZERO matched positives.
621
- const byRule = new Map<string, { hit: number; total: number }>();
636
+ //
637
+ // TWO READINGS SINCE 2026-09-08 (R2 finding 1), and the second one is the
638
+ // honest half. The pre-registered bar is scored with `open_files` HONOURED,
639
+ // which is the reach of the MECHANISM. It is not the reach of the SHIPPED
640
+ // BINDING: `rule-inject` is bound on `user_prompt_submit` + `pre_compact`
641
+ // only (the `pre_tool_use` binding was removed under owner ruling E2), and
642
+ // `user_prompt_submit` never populates `openFiles` — the payload carries no
643
+ // open-file list. So a rule whose only matching positives carry
644
+ // `open_files` is credited as reachable while no shipped slot can deliver
645
+ // it on that route.
646
+ //
647
+ // The bar is UNCHANGED (it is pre-registered; see
648
+ // `internal/bench/thin-inject-PREREG.md`). What is added is the column: the
649
+ // same recall computed with `open_files` ignored, and the names of the rules
650
+ // that differ between the two. A rule in that difference is either kept
651
+ // full-bodied by the always-eager residue — in which case it is reachable
652
+ // by projection rather than by delivery — or it is thinned, in which case it
653
+ // reaches a Claude Code session on the prompt route only.
654
+ const reach = (c: CorpusCase, honourOpenFiles: boolean): boolean =>
655
+ matchTierRules(
656
+ router,
657
+ c.prompt,
658
+ honourOpenFiles ? c.openFiles : null,
659
+ c.command,
660
+ ).some((m) => m.id === c.rule);
661
+
662
+ const byRule = new Map<string, { hit: number; total: number; shipped: number }>();
622
663
  for (const c of positives) {
623
- const e = byRule.get(c.rule) ?? { hit: 0, total: 0 };
664
+ const e = byRule.get(c.rule) ?? { hit: 0, total: 0, shipped: 0 };
624
665
  e.total += 1;
625
- if (matchTierRules(router, c.prompt, c.openFiles).some((m) => m.id === c.rule)) e.hit += 1;
666
+ if (reach(c, true)) e.hit += 1;
667
+ if (reach(c, false)) e.shipped += 1;
626
668
  byRule.set(c.rule, e);
627
669
  }
628
670
  const unreachable = [...byRule.entries()].filter(([, v]) => v.hit === 0).map(([k]) => k);
629
671
  const partial = [...byRule.entries()]
630
672
  .filter(([, v]) => v.hit > 0 && v.hit < v.total)
631
673
  .map(([k, v]) => `${k} ${v.hit}/${v.total}`);
674
+ const pathOnlyReach = [...byRule.entries()]
675
+ .filter(([, v]) => v.hit > 0 && v.shipped === 0)
676
+ .map(([k]) => k)
677
+ .sort();
678
+ const alwaysEager = new Set([
679
+ ...triggerlessRuleIds(router),
680
+ ...pathOnlyRuleIds(router),
681
+ ...kernelIds(router),
682
+ ]);
683
+ const thinnedAndPathOnly = pathOnlyReach.filter((id) => !alwaysEager.has(id));
632
684
 
633
685
  // (c) A near-miss must never deliver its labelled rule.
634
686
  const exact = scoreExact(router, cases, true);
@@ -659,7 +711,12 @@ export function runEndpoints(corpusDir: string): EndpointResult[] {
659
711
  reading:
660
712
  `${byRule.size - unreachable.length}/${byRule.size} rules reachable; ` +
661
713
  `unreachable: ${unreachable.length === 0 ? 'none' : unreachable.join(', ')}; ` +
662
- `partial: ${partial.length === 0 ? 'none' : partial.join(', ')}`,
714
+ `partial: ${partial.length === 0 ? 'none' : partial.join(', ')}` +
715
+ ` | shipped user_prompt_submit reach (open_files IGNORED): ` +
716
+ `${byRule.size - pathOnlyReach.length}/${byRule.size}; reachable only via a ` +
717
+ `path trigger: ${pathOnlyReach.length === 0 ? 'none' : pathOnlyReach.join(', ')}` +
718
+ ` (of which thinned, i.e. at no scope on that route: ` +
719
+ `${thinnedAndPathOnly.length === 0 ? 'none' : thinnedAndPathOnly.join(', ')})`,
663
720
  bar: 'unreachable == 0 (a rule with zero matched positives is a rule the mode removed)',
664
721
  passed: unreachable.length === 0,
665
722
  },
@@ -19,7 +19,7 @@
19
19
  * `.augment/` projections.
20
20
  *
21
21
  * NOTE (faithful literal): the thin-entry pointer keeps the verbatim
22
- * `Body: [`{rule_id}`](../../.agent-src.uncondensed/rules/{rule_id}.md)`
22
+ * `Body: [`{rule_id}`](../../dist/agent-src/rules/{rule_id}.md)`
23
23
  * Markdown link from the retired Python implementation. The same-basename `.py` twin
24
24
  * carries that literal, so the ADR-051 legacy-path guard exempts this `.ts`
25
25
  * file by twin-parity.
@@ -35,6 +35,11 @@ import * as path from 'node:path';
35
35
  import process from 'node:process';
36
36
  import { fileURLToPath, pathToFileURL } from 'node:url';
37
37
 
38
+ import {
39
+ pathOnlyRuleIds,
40
+ triggerlessRuleIds,
41
+ type Router,
42
+ } from './_lib/rule_injection.js';
38
43
  import * as token_count from './_lib/token_count.js';
39
44
 
40
45
  const _HERE = fileURLToPath(import.meta.url);
@@ -77,19 +82,87 @@ export function kernel_ids(): Set<string> {
77
82
  * visible instead of being absorbed into the pointer count
78
83
  * (road-to-trigger-delivered-rule-bodies 1.3).
79
84
  */
85
+ /**
86
+ * Rules whose ONLY triggers are path-shaped — reachable from `pre_tool_use`
87
+ * and from no other slot (road-to-delivery-for-every-host 3.2).
88
+ *
89
+ * They must never be thinned while the delivery concern is NOT bound on
90
+ * `pre_tool_use`, because a stub plus a slot nobody listens on is a rule that
91
+ * reaches the model at no scope at all. This is the same failure the
92
+ * `no_trigger_ids` exemption prevents, one step out: there the rule has nothing
93
+ * to match on, here it has nothing to match ON.
94
+ *
95
+ * Stated as a PROPERTY rather than a list of ids. If the concern is ever bound
96
+ * on `pre_tool_use` again, this exemption is what should be reconsidered — not
97
+ * three names someone has to remember.
98
+ *
99
+ * WHAT THIS EXEMPTION DOES **NOT** COVER — recorded 2026-09-08, R2 finding 1,
100
+ * because the silence read as coverage. The predicate is `every` trigger
101
+ * path-shaped. A rule with BOTH path and non-path triggers is therefore THINNED,
102
+ * and its path-shaped half then has no carrier at all: the stub carries no
103
+ * frontmatter (`condense.ts` writes it with `_writeText`, bypassing
104
+ * `_emit_claude_rule`'s host-native `paths:` key), `pre_tool_use` lost the
105
+ * binding under owner ruling E2, and `user_prompt_submit` never populates
106
+ * `openFiles`. Measured over the current `dist/router.json`: **18 such rules**,
107
+ * all non-kernel and all thinned — augment-edit-discipline, design-fidelity,
108
+ * doc-screenshot-hygiene, domain-adoption-policy,
109
+ * framework-neutrality-in-generic-skills, image-likeness-and-rights,
110
+ * laravel-translations, lethal-trifecta-guard, linked-projects-onboarding-gate,
111
+ * low-impact-corpus-privacy-floor, markdown-safe-codeblocks, onboarding-gate,
112
+ * persona-governance, php-coding, provider-lifecycle-discipline,
113
+ * roadmap-ci-steps-policy, roadmap-progress-sync, settings-ask-protocol.
114
+ *
115
+ * They do NOT reach the model at no scope: every non-path route
116
+ * (keyword / phrase / command) still delivers the body through the concern, and
117
+ * on `claude-code` they never had a separate host-native path route to lose —
118
+ * `_claude_paths_plan` emits no `paths:` for a mixed-trigger rule on purpose, so
119
+ * under `eager-all` they loaded UNCONDITIONALLY. What is lost is the difference
120
+ * between unconditional and prompt-triggered: a session that touches a matching
121
+ * file and says nothing that matches gets nothing.
122
+ *
123
+ * The labelled corpus cannot see this and the endpoint says so rather than
124
+ * implying otherwise — `model_rule_injection --endpoints` (b) now publishes the
125
+ * shipped `user_prompt_submit` reach beside the pre-registered reading, and
126
+ * every one of these 18 has at least one prompt-matching positive, so none
127
+ * appears in its "reachable only via a path trigger" column.
128
+ *
129
+ * WHY THIS IS NOT WIDENED TO `some` HERE. Measured: exempting all 18 moves the
130
+ * thin rule layer 23,592 -> 39,921 GPT tok (+16,329, +69 %), which is a budget
131
+ * move of a magnitude no agent may take, and it would erase most of the saving
132
+ * the flip is licensed on. Restoring a path route needs either that exemption
133
+ * under a re-anchored baseline or a `pre_tool_use` binding under a raised slot
134
+ * cap — both owner-reserved, so the closure is tracked rather than taken here.
135
+ * ADR-267's Consequences section names the receiver.
136
+ */
137
+ /**
138
+ * ONE spelling of the router-derived sets, shared with the injector (R2
139
+ * finding 11).
140
+ *
141
+ * `path_only_ids` and `no_trigger_ids` were each a second, hand-rolled
142
+ * implementation of `_lib/rule_injection.ts::pathOnlyRuleIds` /
143
+ * `triggerlessRuleIds` — verified identical over the current router at the time
144
+ * (symmetric difference empty), so drift risk rather than a live defect. It is
145
+ * exactly the hazard `THIN_ENTRY_MARKER`'s own comment argues against one
146
+ * screen below ("a gate that re-spelled this string would drift from the writer
147
+ * silently"), and the two consumers here are the PROJECTOR's exemption set and
148
+ * the INJECTOR's reachability set: they must not be able to disagree about which
149
+ * rules the delivery path can reach.
150
+ *
151
+ * The finding named `path_only_ids`. The defect-pattern sweep found the SECOND
152
+ * instance one function down — `no_trigger_ids` against `triggerlessRuleIds` —
153
+ * and it is delegated in the same change. Count: 2 of 2 duplicated
154
+ * router-derived sets in this file, 0 remaining.
155
+ */
156
+ function _router(): Router {
157
+ return JSON.parse(fs.readFileSync(ROUTER, 'utf-8')) as Router;
158
+ }
159
+
160
+ export function path_only_ids(): Set<string> {
161
+ return pathOnlyRuleIds(_router());
162
+ }
163
+
80
164
  export function no_trigger_ids(): Set<string> {
81
- const data = JSON.parse(fs.readFileSync(ROUTER, 'utf-8')) as Record<string, unknown>;
82
- const out = new Set<string>();
83
- for (const tier of ['tier_1', 'tier_2']) {
84
- const entries = data[tier];
85
- if (!Array.isArray(entries)) continue;
86
- for (const e of entries) {
87
- const obj = e as Record<string, unknown>;
88
- const t = obj.triggers;
89
- if (!Array.isArray(t) || t.length === 0) out.add(String(obj.id));
90
- }
91
- }
92
- return out;
165
+ return new Set(triggerlessRuleIds(_router()));
93
166
  }
94
167
 
95
168
  /**
@@ -196,6 +269,45 @@ function _title(s: string): string {
196
269
  return s.replace(/[A-Za-z]+/g, (w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase());
197
270
  }
198
271
 
272
+ /**
273
+ * The marker that makes a projected entry recognisable as a stub.
274
+ *
275
+ * Exported so the WRITER and every DETECTOR share one definition. A gate that
276
+ * re-spelled this string would drift from the writer silently, and the failure
277
+ * would be invisible in exactly the direction that matters: a stub the gate
278
+ * fails to recognise reads as a complete rule body.
279
+ */
280
+ export const THIN_ENTRY_MARKER = '> Routed rule — load the body on trigger-match.';
281
+
282
+ /** Is this projected entry a pointer stub rather than a rule body? */
283
+ export function is_thin_entry(text: string): boolean {
284
+ return text.includes(THIN_ENTRY_MARKER);
285
+ }
286
+
287
+ /**
288
+ * Where a stub tells a reader the body actually is.
289
+ *
290
+ * FIXED 2026-09-07: this pointed into the uncondensed source tree ADR-051
291
+ * retired — the literal path is deliberately NOT reproduced here, because
292
+ * `check_no_uncondensed_refs` matches the string and cannot tell a repair
293
+ * record from a live reference; `git log -S` on this file finds the old value.
294
+ * It exists in no checkout, so EVERY stub's fallback pointer
295
+ * resolved to nothing — invisible under `delivery`, where the hook loads the
296
+ * body from `dist/agent-src/rules` and never follows this link, and total under
297
+ * `thin`, where the pointer is the only path to the body there is.
298
+ *
299
+ * KNOWN LIMIT, stated rather than discovered later: one relative prefix cannot
300
+ * be right for every tree, because they sit at different depths — `.claude/rules`
301
+ * and `.cursor/rules` are two levels below the repo root and `.clinerules` is
302
+ * one. `../../` is correct for the first two, which are the only trees the
303
+ * shipped `lean_projection.hosts` default can thin. A stub in `.clinerules`
304
+ * needs an operator to have added `cline` to that list, and its pointer is off
305
+ * by one level. That is strictly better than the previous state, where the
306
+ * pointer was dead in all three, and it is recorded here so the remaining case
307
+ * is a known limit rather than a surprise.
308
+ */
309
+ const BODY_LINK_PREFIX = '../../dist/agent-src/rules/';
310
+
199
311
  /** Build the minimal progressive-disclosure pointer for a non-kernel rule. */
200
312
  export function thin_entry(rule_id: string, text: string): string {
201
313
  const [fm] = split_frontmatter(text);
@@ -205,8 +317,8 @@ export function thin_entry(rule_id: string, text: string): string {
205
317
  const fires = hint ? ` Fires on: ${hint}.` : '';
206
318
  return (
207
319
  `## ${title}\n` +
208
- `> Routed rule — load the body on trigger-match.${fires} ${desc} ` +
209
- `Body: [\`${rule_id}\`](../../.agent-src.uncondensed/rules/${rule_id}.md)\n`
320
+ `${THIN_ENTRY_MARKER}${fires} ${desc} ` +
321
+ `Body: [\`${rule_id}\`](${BODY_LINK_PREFIX}${rule_id}.md)\n`
210
322
  );
211
323
  }
212
324
 
@@ -214,9 +326,21 @@ export function thin_entry(rule_id: string, text: string): string {
214
326
  export function build_thin(
215
327
  rules_dir: string = RULES_SOURCE,
216
328
  scope: readonly string[] | null = null,
329
+ /**
330
+ * Optional sink for the D3 diagnostic
331
+ * (road-to-delivery-for-every-host 2.2).
332
+ *
333
+ * An `auto` rule the router gives NO trigger is already kept full-bodied by
334
+ * `noTrigger` below, which is the substantive protection and predates this
335
+ * roadmap. What did not exist is any way to SEE it happen: the rule is
336
+ * silently exempted, so an author who removes a rule's last trigger gets a
337
+ * silently eager rule and no signal. The line makes the exemption audible.
338
+ */
339
+ announce: ((message: string) => void) | null = null,
217
340
  ): Map<string, string> {
218
341
  const kernel = kernel_ids();
219
342
  const noTrigger = no_trigger_ids();
343
+ const pathOnly = path_only_ids();
220
344
  const wsMap = scope !== null ? rule_workspaces_map() : new Map<string, string[]>();
221
345
  const out = new Map<string, string>();
222
346
  for (const p of _globSortedMd(rules_dir)) {
@@ -225,7 +349,16 @@ export function build_thin(
225
349
  if (!id_in_scope(stem, scope, kernel, wsMap, fm_workspaces(text))) {
226
350
  continue; // out of workspace scope — no body, no pointer line
227
351
  }
228
- const full = kernel.has(stem) || noTrigger.has(stem);
352
+ const full = kernel.has(stem) || noTrigger.has(stem) || pathOnly.has(stem);
353
+ if (announce !== null && noTrigger.has(stem) && !kernel.has(stem)) {
354
+ announce(`D3: trigger-less auto rule ${path.basename(p)} — kept full-bodied, never thinned`);
355
+ }
356
+ if (announce !== null && pathOnly.has(stem) && !kernel.has(stem) && !noTrigger.has(stem)) {
357
+ announce(
358
+ `E2: path-only auto rule ${path.basename(p)} — kept full-bodied; its only triggers ` +
359
+ `are path-shaped and the delivery concern is not bound on pre_tool_use`,
360
+ );
361
+ }
229
362
  out.set(path.basename(p), full ? text : thin_entry(stem, text));
230
363
  }
231
364
  return out;