@event4u/agent-config 16.2.0 → 16.3.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 (332) 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 +89 -237
  5. package/README.md +14 -12
  6. package/dist/agent-src/commands/analyze/inbox.md +1 -1
  7. package/dist/agent-src/commands/analyze/roadmap-repos.md +1 -1
  8. package/dist/agent-src/commands/bug/fix.md +17 -2
  9. package/dist/agent-src/commands/feature/plan.md +1 -1
  10. package/dist/agent-src/commands/feature/roadmap.md +1 -1
  11. package/dist/agent-src/commands/fix/ci.md +1 -1
  12. package/dist/agent-src/commands/implement-ticket.md +1 -1
  13. package/dist/agent-src/commands/jira-ticket.md +1 -1
  14. package/dist/agent-src/commands/roadmap/create.md +1 -1
  15. package/dist/agent-src/commands/roadmap/materialize.md +1 -1
  16. package/dist/agent-src/commands/roadmap/process-full.md +4 -4
  17. package/dist/agent-src/commands/roadmap/resolve-blockers.md +361 -0
  18. package/dist/agent-src/commands/roadmap/triage-parked.md +325 -0
  19. package/dist/agent-src/commands/roadmap.md +18 -4
  20. package/dist/agent-src/commands/tests/create.md +17 -3
  21. package/dist/agent-src/commands/tests/execute.md +22 -5
  22. package/dist/agent-src/contexts/authority/decision-revisit-gate-enforcement.md +43 -0
  23. package/dist/agent-src/contexts/communication/rules-auto/design-fidelity-enforcement.md +24 -0
  24. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +6 -6
  25. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +32 -2
  26. package/dist/agent-src/contexts/execution/session-canary-enforcement-history.md +67 -0
  27. package/dist/agent-src/contexts/execution/toolchain-resolver.md +101 -7
  28. package/dist/agent-src/rules/active-remediation.md +1 -1
  29. package/dist/agent-src/rules/autonomous-execution.md +1 -0
  30. package/dist/agent-src/rules/broken-access-control.md +1 -0
  31. package/dist/agent-src/rules/command-suggestion-policy.md +1 -0
  32. package/dist/agent-src/rules/context-hygiene.md +1 -1
  33. package/dist/agent-src/rules/decision-revisit-gate.md +16 -33
  34. package/dist/agent-src/rules/design-fidelity.md +13 -16
  35. package/dist/agent-src/rules/doc-screenshot-hygiene.md +1 -0
  36. package/dist/agent-src/rules/domain-safety-disclaimer.md +1 -0
  37. package/dist/agent-src/rules/domain-safety-pii.md +1 -0
  38. package/dist/agent-src/rules/domain-safety-retention.md +1 -0
  39. package/dist/agent-src/rules/engineering-safety-floor.md +1 -0
  40. package/dist/agent-src/rules/evaluator-independence.md +3 -2
  41. package/dist/agent-src/rules/fast-path-marker-visibility.md +1 -0
  42. package/dist/agent-src/rules/finance-safety-floor.md +1 -0
  43. package/dist/agent-src/rules/git-history-discipline.md +1 -0
  44. package/dist/agent-src/rules/image-likeness-and-rights.md +1 -0
  45. package/dist/agent-src/rules/legal-safety-floor.md +3 -2
  46. package/dist/agent-src/rules/lethal-trifecta-guard.md +1 -0
  47. package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
  48. package/dist/agent-src/rules/media-governance-routing.md +14 -17
  49. package/dist/agent-src/rules/persona-governance.md +2 -2
  50. package/dist/agent-src/rules/provider-lifecycle-discipline.md +1 -1
  51. package/dist/agent-src/rules/question-not-instruction.md +1 -0
  52. package/dist/agent-src/rules/roadmap-progress-sync.md +1 -1
  53. package/dist/agent-src/rules/runtime-safety.md +1 -0
  54. package/dist/agent-src/rules/secret-vcs-guard.md +1 -0
  55. package/dist/agent-src/rules/security-sensitive-stop.md +3 -2
  56. package/dist/agent-src/rules/session-canary.md +8 -62
  57. package/dist/agent-src/rules/settings-ask-protocol.md +1 -0
  58. package/dist/agent-src/rules/source-confidentiality.md +1 -0
  59. package/dist/agent-src/rules/strategy-safety-floor.md +1 -0
  60. package/dist/agent-src/rules/tool-safety.md +1 -0
  61. package/dist/agent-src/rules/untrusted-input-defense.md +1 -0
  62. package/dist/agent-src/rules/user-interrupt-priority.md +1 -0
  63. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +115 -20
  64. package/dist/agent-src/scripts/deferral_carry.ts +339 -0
  65. package/dist/agent-src/scripts/update_roadmap_progress.ts +4 -1
  66. package/dist/agent-src/skills/api-design/data/manifest.json +2 -2
  67. package/dist/agent-src/skills/api-testing/SKILL.md +17 -0
  68. package/dist/agent-src/skills/code-intelligence/SKILL.md +14 -2
  69. package/dist/agent-src/skills/design-system-capture/references/import-procedure.md +1 -1
  70. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +2 -2
  71. package/dist/agent-src/skills/fe-design/SKILL.md +1 -1
  72. package/dist/agent-src/skills/iconography/SKILL.md +1 -1
  73. package/dist/agent-src/skills/js-library-packaging/SKILL.md +2 -2
  74. package/dist/agent-src/skills/monorepo-workspace/SKILL.md +1 -1
  75. package/dist/agent-src/skills/quality-tools/SKILL.md +11 -1
  76. package/dist/agent-src/skills/quality-tools/references/go-tools.md +76 -0
  77. package/dist/agent-src/skills/quality-tools/references/python-tools.md +104 -0
  78. package/dist/agent-src/skills/review-routing/SKILL.md +2 -2
  79. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
  80. package/dist/agent-src/skills/storybook-workshop/SKILL.md +3 -3
  81. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
  82. package/dist/agent-src/skills/test-case-discovery/SKILL.md +7 -0
  83. package/dist/agent-src/skills/test-driven-development/SKILL.md +17 -0
  84. package/dist/agent-src/skills/test-performance/SKILL.md +19 -1
  85. package/dist/agent-src/skills/ui-component-architect/SKILL.md +2 -2
  86. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  87. package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +48 -6
  88. package/dist/agent-src/templates/scripts/work_engine/stack/runner.ts +774 -109
  89. package/dist/cli/registry.js +1 -1
  90. package/dist/cli-delegate/archive_completed_roadmaps.js +92 -12
  91. package/dist/cli-delegate/check_update_banner.js +12 -1
  92. package/dist/cli-delegate/chunk-523HCFYO.js +90 -0
  93. package/dist/cli-delegate/{chunk-M2DPGFZ4.js → chunk-6V6TA25H.js} +22 -152
  94. package/dist/cli-delegate/chunk-7IQE3D5S.js +191 -0
  95. package/dist/cli-delegate/{chunk-6KFEC3G7.js → chunk-CQWX336L.js} +1 -1
  96. package/dist/cli-delegate/chunk-DEIV7XP7.js +108 -0
  97. package/dist/cli-delegate/chunk-EEHDXSYD.js +2040 -0
  98. package/dist/cli-delegate/{chunk-P6ZHP5W2.js → chunk-GKE7ZVFA.js} +4 -4
  99. package/dist/cli-delegate/chunk-JQOM7TTM.js +1201 -0
  100. package/dist/cli-delegate/{chunk-IGD6ERX6.js → chunk-JQVYKIZA.js} +10 -0
  101. package/dist/cli-delegate/{chunk-FC75PGJE.js → chunk-JY2I2DXV.js} +12 -1
  102. package/dist/cli-delegate/chunk-KB5FZ655.js +208 -0
  103. package/dist/cli-delegate/{chunk-7RQP4UDH.js → chunk-KR6EM23A.js} +235 -1455
  104. package/dist/cli-delegate/{chunk-LTYVIY76.js → chunk-LIYTLH6O.js} +1 -1
  105. package/dist/cli-delegate/{chunk-YJ72UIAK.js → chunk-NQ7WT43P.js} +1186 -244
  106. package/dist/cli-delegate/chunk-NXEILF2K.js +2160 -0
  107. package/dist/cli-delegate/{chunk-5R7HOLAF.js → chunk-NYX3L2PZ.js} +1 -1
  108. package/dist/cli-delegate/{chunk-4E2W63DA.js → chunk-OKJUIAPS.js} +62 -1
  109. package/dist/cli-delegate/{chunk-QAJS5YWY.js → chunk-PHD7OOLE.js} +2 -1
  110. package/dist/cli-delegate/{chunk-RGNIWTEA.js → chunk-QLDHIPWR.js} +1 -1
  111. package/dist/cli-delegate/{chunk-7XGBX6ZH.js → chunk-RQNNW7IF.js} +1314 -560
  112. package/dist/cli-delegate/{chunk-4XFP2Y3E.js → chunk-S4UMZNES.js} +1 -1
  113. package/dist/cli-delegate/{chunk-6KM6ORRY.js → chunk-S4YWNRKO.js} +24 -8
  114. package/dist/cli-delegate/{chunk-FP36SWJD.js → chunk-TRI6DQZ6.js} +2781 -340
  115. package/dist/cli-delegate/chunk-ULQRUR23.js +208 -0
  116. package/dist/cli-delegate/{chunk-NUHLI2RX.js → chunk-YCU3TIXH.js} +2 -0
  117. package/dist/cli-delegate/chunk-ZBZV2G5R.js +210 -0
  118. package/dist/cli-delegate/{chunk-WGXTVKCQ.js → chunk-ZUZKZYCN.js} +5 -3
  119. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  120. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  121. package/dist/cli-delegate/cmd_conformance.js +1167 -234
  122. package/dist/cli-delegate/cmd_doctor.js +13 -6
  123. package/dist/cli-delegate/cmd_doctor_neighbours.js +23 -0
  124. package/dist/cli-delegate/cmd_explain.js +5 -5
  125. package/dist/cli-delegate/cmd_export.js +11 -8
  126. package/dist/cli-delegate/cmd_fleet.js +3 -3
  127. package/dist/cli-delegate/cmd_handoff.js +10 -7
  128. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  129. package/dist/cli-delegate/cmd_memory_get.js +3 -1
  130. package/dist/cli-delegate/cmd_migrate.js +2 -2
  131. package/dist/cli-delegate/cmd_packs_active.js +3 -3
  132. package/dist/cli-delegate/cmd_preflight.js +6 -6
  133. package/dist/cli-delegate/cmd_prune.js +11 -8
  134. package/dist/cli-delegate/cmd_refresh.js +2 -1
  135. package/dist/cli-delegate/cmd_route_audit.js +8 -6
  136. package/dist/cli-delegate/cmd_route_explain.js +4 -4
  137. package/dist/cli-delegate/cmd_session_recycle.js +4 -3
  138. package/dist/cli-delegate/cmd_settings_get.js +1 -1
  139. package/dist/cli-delegate/cmd_settings_migrate.js +11 -8
  140. package/dist/cli-delegate/cmd_settings_set.js +4 -4
  141. package/dist/cli-delegate/cmd_sync.js +11 -8
  142. package/dist/cli-delegate/cmd_uninstall.js +11 -8
  143. package/dist/cli-delegate/cmd_update.js +1 -1
  144. package/dist/cli-delegate/cmd_upgrade.js +3 -2
  145. package/dist/cli-delegate/cmd_validate.js +11 -8
  146. package/dist/cli-delegate/cmd_versions.js +1 -1
  147. package/dist/cli-delegate/deferral_carry.js +16 -0
  148. package/dist/cli-delegate/gate_execute.js +2 -2
  149. package/dist/cli-delegate/roadmap_gates.js +5 -5
  150. package/dist/cli-delegate/update_roadmap_progress.js +1 -1
  151. package/dist/discovery/deprecation-report.md +1 -1
  152. package/dist/discovery/discovery-manifest.json +127 -75
  153. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  154. package/dist/discovery/discovery-manifest.summary.md +3 -3
  155. package/dist/discovery/dormancy-report.md +1 -1
  156. package/dist/discovery/orphan-report.md +1 -1
  157. package/dist/discovery/packs.json +7 -5
  158. package/dist/discovery/trust-report.md +2 -2
  159. package/dist/discovery/workspaces.json +6 -4
  160. package/dist/hooks/dispatch.js +5357 -6656
  161. package/dist/hooks/dispatch.sha256 +1 -1
  162. package/dist/install/claudePathsPlan.js +47 -6
  163. package/dist/install/claudeRuleRewrite.js +7 -0
  164. package/dist/install/emit_host_rules_cli.js +39 -36
  165. package/dist/install/globalRuleScope.js +87 -0
  166. package/dist/install/host_rule_ownership.js +160 -0
  167. package/dist/install/install.mjs +2080 -1024
  168. package/dist/install/installThinLayer.js +261 -0
  169. package/dist/install/installedRuleLinks.js +176 -0
  170. package/dist/install/wizard-plan.js +24 -0
  171. package/dist/mcp/registry-manifest.json +3 -3
  172. package/dist/mcp/server.json +2 -2
  173. package/dist/mcp/server.mjs +31965 -27154
  174. package/dist/scripts/_lib/agent_settings.js +11 -0
  175. package/dist/scripts/_lib/claude_settings_hooks.js +10 -8
  176. package/dist/scripts/_lib/installed_layer.js +378 -0
  177. package/dist/scripts/_lib/lean_projection_mode.js +218 -15
  178. package/dist/scripts/_lib/rule_consequence_class.js +45 -0
  179. package/dist/scripts/_lib/rule_injection.js +67 -10
  180. package/dist/scripts/_lib/rule_law_section.js +122 -0
  181. package/dist/scripts/_lib/thin_rules.js +458 -0
  182. package/dist/scripts/_lib/windsurf_render.js +60 -0
  183. package/dist/scripts/condense.js +14 -26
  184. package/dist/scripts/project_thin_rules.js +14 -294
  185. package/dist/server/schemas/settings.js +6 -0
  186. package/dist/shared/skillRanking.js +74 -13
  187. package/docs/contracts/settings-classes.md +7 -5
  188. package/package.json +1 -1
  189. package/src/agent-src/scripts/archive_completed_roadmaps.ts +115 -20
  190. package/src/agent-src/scripts/deferral_carry.ts +339 -0
  191. package/src/agent-src/scripts/update_roadmap_progress.ts +4 -1
  192. package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +48 -6
  193. package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +774 -109
  194. package/src/config/agent-settings.template.yml +62 -22
  195. package/src/config/continuity-surface.json +7 -0
  196. package/src/config/evaluator-budgets.json +4 -4
  197. package/src/config/gate-coverage.yml +96 -3
  198. package/src/config/gate-violation-baselines.json +17 -9
  199. package/src/config/hook-bundle-budget.json +25 -0
  200. package/src/config/hook-token-budget.json +6 -1
  201. package/src/config/host-instruction-limits.json +33 -0
  202. package/src/config/routing-coverage-seed.json +24 -3
  203. package/src/config/rule-activation-census.json +7 -3
  204. package/src/config/rule-consequence-class.json +161 -0
  205. package/src/config/rule-law-ceilings.json +99 -0
  206. package/src/install/claudePathsPlan.ts +46 -4
  207. package/src/install/claudeRuleRewrite.ts +10 -1
  208. package/src/install/emit_host_rules_cli.ts +43 -34
  209. package/src/install/globalRuleScope.ts +109 -0
  210. package/src/install/host_rule_ownership.ts +170 -0
  211. package/src/install/installThinLayer.ts +312 -0
  212. package/src/install/installedRuleLinks.ts +220 -0
  213. package/src/install/wizard-plan.ts +24 -0
  214. package/src/scripts/_cli/cmd_doctor.ts +23 -71
  215. package/src/scripts/_cli/cmd_doctor_neighbours.ts +293 -0
  216. package/src/scripts/_dispatch.bash +16 -2
  217. package/src/scripts/_lib/agent_settings.ts +11 -0
  218. package/src/scripts/_lib/claude_settings_hooks.ts +10 -8
  219. package/src/scripts/_lib/estate_offsets.ts +241 -0
  220. package/src/scripts/_lib/exemption_shape.ts +143 -0
  221. package/src/scripts/_lib/fnv.ts +23 -0
  222. package/src/scripts/_lib/foreign_scan.ts +106 -0
  223. package/src/scripts/_lib/gitignore_block.ts +314 -0
  224. package/src/scripts/_lib/global_deploy_inventory.ts +32 -0
  225. package/src/scripts/_lib/graph_feeder_record.ts +214 -0
  226. package/src/scripts/_lib/hook_settings.ts +73 -0
  227. package/src/scripts/_lib/host_capability.ts +40 -19
  228. package/src/scripts/_lib/host_hook_merge.ts +165 -0
  229. package/src/scripts/_lib/installed_layer.ts +538 -0
  230. package/src/scripts/_lib/json_merge.ts +82 -0
  231. package/src/scripts/_lib/json_pointers.ts +93 -11
  232. package/src/scripts/_lib/lean_projection_mode.ts +275 -15
  233. package/src/scripts/_lib/machine_wake.ts +16 -3
  234. package/src/scripts/_lib/neighbour_census.ts +760 -0
  235. package/src/scripts/_lib/neighbour_scan.ts +122 -0
  236. package/src/scripts/_lib/neighbour_tool_use.ts +189 -0
  237. package/src/scripts/_lib/prompt_shape.ts +46 -0
  238. package/src/scripts/_lib/release_migration_gate.ts +19 -3
  239. package/src/scripts/_lib/reserved_name_sweep.ts +73 -0
  240. package/src/scripts/_lib/review_coverage.ts +134 -0
  241. package/src/scripts/_lib/rule_consequence_class.ts +71 -0
  242. package/src/scripts/_lib/rule_injection.ts +130 -10
  243. package/src/scripts/_lib/rule_law_section.ts +147 -0
  244. package/src/scripts/_lib/rule_layer_overlap.ts +58 -0
  245. package/src/scripts/_lib/skill_origin.ts +254 -0
  246. package/src/scripts/_lib/structured_ask.ts +64 -21
  247. package/src/scripts/_lib/thin_rules.ts +540 -0
  248. package/src/scripts/_lib/touched_file_quality.ts +434 -0
  249. package/src/scripts/_lib/turn_end_refusals.ts +77 -2
  250. package/src/scripts/_lib/windsurf_render.ts +63 -0
  251. package/src/scripts/ai_council/cli_call_budget.ts +13 -5
  252. package/src/scripts/ai_council/clients.ts +26 -57
  253. package/src/scripts/ai_council/config.ts +1 -1
  254. package/src/scripts/ai_council/vendor_defaults.ts +45 -0
  255. package/src/scripts/audit_skill_overlap.ts +261 -0
  256. package/src/scripts/before_complete_hook.ts +243 -12
  257. package/src/scripts/bench_touched_file_quality.ts +238 -0
  258. package/src/scripts/check_enforcement_matrix.ts +377 -20
  259. package/src/scripts/check_estate_count.ts +130 -153
  260. package/src/scripts/check_finding_dispositions.ts +116 -4
  261. package/src/scripts/check_hook_bundle_composition.ts +370 -0
  262. package/src/scripts/check_kernel_edit_ratified.ts +49 -11
  263. package/src/scripts/check_memory.ts +7 -1
  264. package/src/scripts/check_release_highlights.ts +10 -0
  265. package/src/scripts/check_release_pr_shape.ts +38 -15
  266. package/src/scripts/check_standing_rule_delivery.ts +33 -13
  267. package/src/scripts/check_verify_expectation_delta.ts +521 -0
  268. package/src/scripts/code_graph/detect.ts +127 -7
  269. package/src/scripts/code_graph/foreign.ts +194 -0
  270. package/src/scripts/code_graph/query.ts +217 -4
  271. package/src/scripts/code_graph/types.ts +26 -2
  272. package/src/scripts/code_graph/validate.ts +3 -1
  273. package/src/scripts/code_graph/verbs.ts +103 -0
  274. package/src/scripts/compile_hook_manifest.ts +33 -1
  275. package/src/scripts/condense.ts +14 -27
  276. package/src/scripts/estate_exemption_shape.ts +202 -0
  277. package/src/scripts/generate_host_cost_table.ts +1 -1
  278. package/src/scripts/hook_manifest.json +1 -1
  279. package/src/scripts/hook_manifest.yaml +150 -9
  280. package/src/scripts/hooks/block_config_weakening.ts +67 -2
  281. package/src/scripts/hooks/block_plumbing_writes.ts +29 -1
  282. package/src/scripts/hooks/code_graph_context_hook.ts +252 -23
  283. package/src/scripts/hooks/concern_failure_policy.ts +280 -0
  284. package/src/scripts/hooks/dispatch_hook.ts +52 -52
  285. package/src/scripts/hooks/exit_codes.ts +2 -1
  286. package/src/scripts/hooks/host_lowering.json +1 -1
  287. package/src/scripts/hooks/host_lowering.ts +19 -1
  288. package/src/scripts/hooks/one_question_per_ask_hook.ts +41 -25
  289. package/src/scripts/hooks/payload_stub.ts +10 -1
  290. package/src/scripts/hooks/rule_inject_hook.ts +757 -63
  291. package/src/scripts/hooks/table_fingerprint.ts +21 -0
  292. package/src/scripts/hooks/telemetry_usage_hook.ts +64 -8
  293. package/src/scripts/hooks/turn_end_gate_hook.ts +84 -0
  294. package/src/scripts/hooks_status.ts +112 -0
  295. package/src/scripts/injection_scan_hook.ts +7 -0
  296. package/src/scripts/install.ts +72 -185
  297. package/src/scripts/installed_layer_report.ts +103 -0
  298. package/src/scripts/language_mirror_hook.ts +15 -6
  299. package/src/scripts/learning_sidecar.ts +119 -8
  300. package/src/scripts/lint_hook_manifest.ts +53 -0
  301. package/src/scripts/lint_major_migration_sections.ts +156 -26
  302. package/src/scripts/lint_pack_boundaries.ts +63 -5
  303. package/src/scripts/lint_rule_law_section.ts +517 -0
  304. package/src/scripts/lint_workflow_security.ts +161 -5
  305. package/src/scripts/mcp_server/consumer_tool_catalog.json +53 -0
  306. package/src/scripts/mcp_server/graph_tools.ts +72 -3
  307. package/src/scripts/measure_routing_signal.ts +54 -16
  308. package/src/scripts/measure_skill_ranker_baseline.ts +216 -8
  309. package/src/scripts/memory_learn_hook.ts +91 -6
  310. package/src/scripts/model_rule_injection.ts +52 -5
  311. package/src/scripts/project_thin_rules.ts +34 -327
  312. package/src/scripts/report_evidence_temperature.ts +657 -0
  313. package/src/scripts/report_installed_rule_links.ts +157 -0
  314. package/src/scripts/report_label_agreement.ts +233 -0
  315. package/src/scripts/report_obligation_mechanism.ts +230 -0
  316. package/src/scripts/report_single_token_triggers.ts +277 -0
  317. package/src/scripts/report_skill_ranker_confusion.ts +442 -0
  318. package/src/scripts/report_standing_payload_by_host.ts +188 -15
  319. package/src/scripts/report_verification_record_coverage.ts +306 -0
  320. package/src/scripts/rule_activation_census.ts +5 -2
  321. package/src/scripts/rule_trigger_eval.ts +9 -9
  322. package/src/scripts/schemas/review-findings.schema.json +3 -2
  323. package/src/scripts/schemas/rule.schema.json +5 -0
  324. package/src/scripts/self_review_gate.ts +62 -7
  325. package/src/scripts/skill_tools/score_skill_relevance.ts +290 -25
  326. package/src/scripts/sweep_skill_ranker_signals.ts +129 -0
  327. package/src/scripts/sync_gitignore.ts +29 -235
  328. package/src/scripts/trigger_eval_grandfather.json +0 -5
  329. package/src/server/schemas/settings.ts +10 -0
  330. package/src/shared/skillRanking.ts +117 -11
  331. package/dist/cli-delegate/chunk-JNMVLWLR.js +0 -4457
  332. package/dist/cli-delegate/chunk-YDPILCOL.js +0 -152
@@ -76,6 +76,9 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
76
76
  import type * as YamlModule from 'yaml';
77
77
 
78
78
  import { build_merge_entries } from './_lib/json_pointers.js';
79
+ import { deepMerge as deep_merge, isPlainObject as _isPlainObject, jsonEqual } from './_lib/json_merge.js';
80
+ import { mergeHostConfig, HOOK_SIGNATURES } from './_lib/host_hook_merge.js';
81
+ import { sweepReservedNames } from './_lib/reserved_name_sweep.js';
79
82
  import { withHostEnv } from './_lib/host_env_write.js';
80
83
  import { jsonDumpsCompact, jsonDumpsIndent } from './_lib/json_python_parity.js';
81
84
  import { is_claude_builtin_name } from './_lib/claude_builtin_names.js';
@@ -93,6 +96,7 @@ import * as rule_layer_overlap from './_lib/rule_layer_overlap.js';
93
96
  import * as installed_tools from './_lib/installed_tools.js';
94
97
  import { collect_drift, format_drift_report } from './_lib/install_drift.js';
95
98
  import * as user_global_paths from './_lib/user_global_paths.js';
99
+ import * as gitignore_block from './_lib/gitignore_block.js';
96
100
  // The LIBRARY half, deliberately not `capture_skill_catalogue.js`: that module
97
101
  // is a CLI entry with a top-level `process.exit()`, and esbuild would bundle
98
102
  // that exit into the installer a consumer loads (`check_installer_import_purity`
@@ -103,15 +107,17 @@ import * as claude_desktop_bundler from './_lib/claude_desktop_bundler.js';
103
107
  import * as claude_settings_hooks from './_lib/claude_settings_hooks.js';
104
108
  import { hostBindings } from './hooks/host_lowering.js';
105
109
  import { find_project_root_with_anchor, load_agent_settings } from './_lib/agent_settings.js';
106
- import {
107
- LEGACY_ALL,
108
- ruleFileArrives,
109
- ruleScopeFromSettings,
110
- type RuleScope,
111
- } from '../install/rule_scope.js';
110
+ import { ruleFileArrives, type RuleScope } from '../install/rule_scope.js';
112
111
  import { isExclusivelyPackageOnly, stampHostLayerFingerprint } from '../install/partitionEligibility.js'; // ADR-236
113
112
  import * as claude_rule_rewrite from '../install/claudeRuleRewrite.js';
114
- import { RULE_SOURCE_REL } from '../install/wizard-plan.js';
113
+ import {
114
+ describeThinInstalledLayer,
115
+ installReceiptBudgetLines,
116
+ thinInstalledRuleLayer,
117
+ } from '../install/installThinLayer.js';
118
+ import { resolveGlobalRuleScope } from '../install/globalRuleScope.js';
119
+ import { installerThinsHost } from './_lib/lean_projection_mode.js';
120
+ import { GLOBAL_DEPLOY_SOURCES, RULE_SOURCE_REL } from '../install/wizard-plan.js';
115
121
  import { flattenSurface, computeSurfaceDelta, type SettingsSurface } from '../shared/settingsSurface.js';
116
122
  import { settingsSchema } from '../server/schemas/settings.js';
117
123
  import { zodToJsonSchema } from 'zod-to-json-schema';
@@ -481,63 +487,6 @@ function write_json_file(p: string, data: unknown): void {
481
487
  write_file(p, content);
482
488
  }
483
489
 
484
- function _isPlainObject(v: unknown): v is Record<string, unknown> {
485
- return typeof v === 'object' && v !== null && !Array.isArray(v);
486
- }
487
-
488
- /** `copy.deepcopy` for JSON-shaped values. */
489
- function deepcopy<T>(v: T): T {
490
- if (v === null || typeof v !== 'object') return v;
491
- if (Array.isArray(v)) return v.map((x) => deepcopy(x)) as unknown as T;
492
- const out: Record<string, unknown> = {};
493
- for (const k of Object.keys(v as Record<string, unknown>)) {
494
- out[k] = deepcopy((v as Record<string, unknown>)[k]);
495
- }
496
- return out as unknown as T;
497
- }
498
-
499
- function deep_merge(
500
- base: Record<string, unknown>,
501
- overlay: Record<string, unknown>,
502
- ): Record<string, unknown> {
503
- const result = deepcopy(base);
504
- for (const key of Object.keys(overlay)) {
505
- const value = overlay[key];
506
- if (
507
- Object.prototype.hasOwnProperty.call(result, key) &&
508
- _isPlainObject(result[key]) &&
509
- _isPlainObject(value)
510
- ) {
511
- result[key] = deep_merge(
512
- result[key] as Record<string, unknown>,
513
- value as Record<string, unknown>,
514
- );
515
- } else {
516
- result[key] = deepcopy(value);
517
- }
518
- }
519
- return result;
520
- }
521
-
522
- /** Deep structural equality for JSON-shaped values (Python dict `==`). */
523
- function jsonEqual(a: unknown, b: unknown): boolean {
524
- if (a === b) return true;
525
- if (typeof a !== typeof b) return false;
526
- if (Array.isArray(a) || Array.isArray(b)) {
527
- if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
528
- return a.every((v, i) => jsonEqual(v, b[i]));
529
- }
530
- if (_isPlainObject(a) && _isPlainObject(b)) {
531
- const ka = Object.keys(a);
532
- const kb = Object.keys(b);
533
- if (ka.length !== kb.length) return false;
534
- return ka.every(
535
- (k) => Object.prototype.hasOwnProperty.call(b, k) && jsonEqual(a[k], (b as Record<string, unknown>)[k]),
536
- );
537
- }
538
- return false;
539
- }
540
-
541
490
  function merge_json_file(
542
491
  p: string,
543
492
  new_data: Record<string, unknown>,
@@ -557,7 +506,9 @@ function merge_json_file(
557
506
  }
558
507
 
559
508
  const existing = read_json_file(p);
560
- const merged = deep_merge(existing, new_data);
509
+ // Hook arrays are shared with whatever else the consumer installed, so the
510
+ // `hooks` key merges per event by signature instead of being replaced.
511
+ const merged = mergeHostConfig(existing, new_data, HOOK_SIGNATURES[label] ?? '', deep_merge);
561
512
 
562
513
  if (jsonEqual(merged, existing)) {
563
514
  skip(`${label} already configured`);
@@ -1803,48 +1754,20 @@ export const PROJECT_BRIDGE_MARKERS: Record<string, string> = {
1803
1754
  kiro: '.kiro/steering/agent-config.md',
1804
1755
  };
1805
1756
 
1806
- const _CLAUDE_SKILL_BUNDLE: ReadonlyArray<readonly [string, string]> = [
1807
- ['dist/agent-src/rules', 'rules'],
1808
- ['dist/agent-src/skills', 'skills'],
1809
- ['dist/agent-src/commands', 'commands'],
1810
- ['dist/agent-src/personas', 'personas'],
1811
- ];
1812
-
1813
- export const GLOBAL_DEPLOY_SOURCES: Record<string, ReadonlyArray<readonly [string, string]>> = {
1814
- 'claude-code': _CLAUDE_SKILL_BUNDLE,
1815
- augment: [
1816
- ['dist/agent-src/rules', 'rules'],
1817
- ['dist/agent-src/skills', 'skills'],
1818
- ['dist/agent-src/commands', 'commands'],
1819
- ['dist/agent-src/contexts', 'contexts'],
1820
- ['dist/agent-src/personas', 'personas'],
1821
- ['dist/agent-src/templates', 'templates'],
1822
- ],
1823
- cursor: [
1824
- ['dist/agent-src/rules', 'rules'],
1825
- ['dist/agent-src/commands', 'commands'],
1826
- ['dist/agent-src/personas', 'personas'],
1827
- ],
1828
- windsurf: [['dist/agent-src/rules', 'rules']],
1829
- cline: [['dist/agent-src/rules', '']],
1830
- 'gemini-cli': _CLAUDE_SKILL_BUNDLE,
1831
- codex: _CLAUDE_SKILL_BUNDLE,
1832
- continue: _CLAUDE_SKILL_BUNDLE,
1833
- roocode: _CLAUDE_SKILL_BUNDLE,
1834
- kilocode: _CLAUDE_SKILL_BUNDLE,
1835
- qoder: _CLAUDE_SKILL_BUNDLE,
1836
- opencode: _CLAUDE_SKILL_BUNDLE,
1837
- trae: _CLAUDE_SKILL_BUNDLE,
1838
- antigravity: _CLAUDE_SKILL_BUNDLE,
1839
- codebuddy: _CLAUDE_SKILL_BUNDLE,
1840
- droid: _CLAUDE_SKILL_BUNDLE,
1841
- warp: _CLAUDE_SKILL_BUNDLE,
1842
- kiro: [
1843
- ['dist/agent-src/rules', 'rules'],
1844
- ['dist/agent-src/skills', 'steering'],
1845
- ['dist/agent-src/personas', 'personas'],
1846
- ],
1847
- };
1757
+ /**
1758
+ * Re-exported from `wizard-plan.ts`, which is now the ONLY definition.
1759
+ *
1760
+ * It was defined here too, and the two copies were byte-identical in every
1761
+ * one of their 18 host rows — a duplicate of the same truth, and the kind
1762
+ * that stays correct right up until somebody edits one of them. Step 1.2 of
1763
+ * `road-to-rule-triggers-and-links-that-hold` did exactly that: it added two
1764
+ * directories here and the wizard kept installing without them.
1765
+ *
1766
+ * Re-exported rather than moved-and-left-broken so every caller of
1767
+ * `install.ts::GLOBAL_DEPLOY_SOURCES` keeps working unchanged. Imported as
1768
+ * well as re-exported because this file's own call sites read the binding.
1769
+ */
1770
+ export { GLOBAL_DEPLOY_SOURCES };
1848
1771
 
1849
1772
  const _CLAUDE_DESKTOP_MARKER_TEMPLATE_HEAD = `# agent-config — Claude Desktop marker
1850
1773
 
@@ -2108,8 +2031,9 @@ export function _gate_rule_layer_overlap(
2108
2031
  layer: string | null,
2109
2032
  dry_run: boolean,
2110
2033
  ): boolean {
2111
- const global_dir = path.join(os.homedir(), '.claude', 'rules');
2112
- const project_dir = path.join(project_root, '.claude', 'rules');
2034
+ // ONE source of truth for which directories the host loads, shared with the
2035
+ // delivery carrier's scope filter (`rule_layer_overlap.hostRuleLayerDirs`).
2036
+ const [global_dir, project_dir] = rule_layer_overlap.hostRuleLayerDirs(project_root);
2113
2037
  const g = rule_layer_overlap.readRuleLayer(global_dir);
2114
2038
  const p = rule_layer_overlap.readRuleLayer(project_dir);
2115
2039
  if (g === null || p === null) return true; // only one layer exists — nothing to double
@@ -2957,26 +2881,12 @@ export function _apply_claude_flat_command_wrappers(
2957
2881
  anchor: string,
2958
2882
  package_root: string,
2959
2883
  current_files: Set<string>,
2960
- ): { wrapped: string[]; collisions: string[]; reserved: string[] } {
2884
+ recorded_files: ReadonlySet<string> = new Set<string>(),
2885
+ ): { wrapped: string[]; collisions: string[]; reserved: string[]; foreign: string[] } {
2961
2886
  const wrapped: string[] = [];
2962
2887
  const collisions: string[] = [];
2963
- const reserved: string[] = [];
2964
- // Reserved-name sweep over ALL flat command files (any tier): a name
2965
- // shadowing a Claude Code built-in never ships on the claude-code anchor.
2966
2888
  const commands_dir = path.join(anchor, 'commands');
2967
- let flat_entries: string[] = [];
2968
- try {
2969
- flat_entries = fs.readdirSync(commands_dir).filter((f) => f.endsWith('.md'));
2970
- } catch {
2971
- // No commands dir → nothing to sweep.
2972
- }
2973
- for (const fname of flat_entries.sort()) {
2974
- const slug = fname.slice(0, -'.md'.length);
2975
- if (!is_claude_builtin_name(slug)) continue;
2976
- fs.rmSync(path.join(commands_dir, fname), { force: true });
2977
- current_files.delete(`commands/${fname}`);
2978
- reserved.push(slug);
2979
- }
2889
+ const { reserved, foreign } = sweepReservedNames(commands_dir, current_files, is_claude_builtin_name, recorded_files);
2980
2890
  // Visible command slugs from the locked discovery manifest. `visibility`
2981
2891
  // is the source of truth (ADR-090/092); the integer `tier` stays only as
2982
2892
  // the fallback for a manifest published before `visibility` was emitted,
@@ -3023,7 +2933,7 @@ export function _apply_claude_flat_command_wrappers(
3023
2933
  current_files.add(`skills/${slug}/SKILL.md`);
3024
2934
  wrapped.push(slug);
3025
2935
  }
3026
- return { wrapped, collisions, reserved };
2936
+ return { wrapped, collisions, reserved, foreign };
3027
2937
  }
3028
2938
 
3029
2939
  function _deploy_global_content(
@@ -3096,15 +3006,33 @@ function _deploy_global_content(
3096
3006
  if (tool_id === 'claude-code') {
3097
3007
  // Rules land verbatim from the copy above; see the module. It also
3098
3008
  // renders the flat-command wrapper report, unchanged.
3099
- const res = _apply_claude_flat_command_wrappers(anchor, package_root, current_files);
3009
+ const res = _apply_claude_flat_command_wrappers(anchor, package_root, current_files, global_deploy_inventory.recorded_rel_files(tool_id, anchor));
3010
+ const preserved_now = new Set(
3011
+ tracker.conflictState.preserved.map((p) => path.resolve(p)),
3012
+ );
3100
3013
  claude_rule_rewrite.rewriteAndReport(
3101
3014
  path.join(anchor, 'rules'),
3102
3015
  state.QUIET,
3103
3016
  info,
3104
3017
  warn,
3105
3018
  res,
3106
- new Set(tracker.conflictState.preserved.map((p) => path.resolve(p))),
3019
+ preserved_now,
3107
3020
  );
3021
+ // Steps 1.1 and 1.4 of road-to-an-installed-layer-that-is-thinned;
3022
+ // both modules' headers carry the reasoning.
3023
+ if (installerThinsHost(tool_id, { packageRoot: package_root })) {
3024
+ try {
3025
+ const thin = thinInstalledRuleLayer({
3026
+ rulesDir: path.join(anchor, 'rules'),
3027
+ packageRoot: package_root,
3028
+ preserved: preserved_now,
3029
+ });
3030
+ if (!state.QUIET) for (const l of describeThinInstalledLayer(thin)) info(l);
3031
+ } catch (e) {
3032
+ warn(`claude-code: thinned rule layer not written — ${String(e)}`);
3033
+ }
3034
+ }
3035
+ if (!state.QUIET) for (const l of installReceiptBudgetLines(package_root)) info(l);
3108
3036
  }
3109
3037
 
3110
3038
  const missing_targets = _verify_deploy_targets(anchor, plan);
@@ -3339,65 +3267,21 @@ function _resolve_scoped_projection(
3339
3267
  }
3340
3268
 
3341
3269
  /**
3342
- * Resolve the rule scope governing THIS global deploy — the same way the wizard
3343
- * does (`src/server/routes/install.ts::_resolveRuleScope`), so the two global
3344
- * paths cannot ship different rule sets for the same settings.
3270
+ * Resolve the rule scope governing THIS global deploy.
3345
3271
  *
3346
- * Settings resolution mirrors `_resolve_scoped_projection` above: an existing
3347
- * global settings doc is authoritative, and only a genuinely fresh machine falls
3348
- * through to the packaged template. Any read or parse failure resolves to
3349
- * `LEGACY_ALL` — over-shipping is the safe direction, and the compat exclusion
3350
- * (`source-of-truth.md`) still applies even then.
3272
+ * Thin wrapper: the body moved to `src/install/globalRuleScope.ts` (see that
3273
+ * module's header for why). Name, signature and behaviour are unchanged, so the
3274
+ * re-export below and every caller and test keep what they had.
3351
3275
  */
3352
3276
  function _resolve_global_rule_scope(package_root: string): RuleScope {
3353
- const settings_path = _resolve_global_settings_path();
3354
-
3355
- // No global settings artefact at all — a genuinely fresh machine. Fall
3356
- // through to the packaged template silently: there is no user decision to
3357
- // contradict, and this is the documented upgrade-compat path.
3358
- if (settings_path === null) {
3359
- try {
3360
- return ruleScopeFromSettings(_load_default_settings(package_root), package_root);
3361
- } catch {
3362
- return LEGACY_ALL;
3363
- }
3364
- }
3365
-
3366
- // A doc EXISTS, so the user has expressed a configuration. Failing to read
3367
- // it is not the same as not having one: falling back to legacy-all here
3368
- // ships the maintainer-only rules the user may have deliberately scoped out,
3369
- // and `_load_yaml_doc` would report that as an indistinguishable `{}`. So
3370
- // parse it explicitly and be LOUD when it does not parse — over-shipping
3371
- // stays the safe direction, but it must not be a silent one.
3372
- let text: string;
3373
- try {
3374
- text = readText(settings_path);
3375
- } catch (e) {
3376
- warn(
3377
- `could not read ${settings_path} (${String(e)}) — rule scoping falls back ` +
3378
- 'to legacy-all, so ALL rules including maintainer-only ones will be ' +
3379
- 'installed. Fix the file to restore scoping.',
3380
- );
3381
- return LEGACY_ALL;
3382
- }
3383
- const parsed = yamlSafeLoad(text);
3384
- if (!_isPlainObject(parsed)) {
3385
- warn(
3386
- `${settings_path} is not a YAML mapping — rule scoping falls back to ` +
3387
- 'legacy-all, so ALL rules including maintainer-only ones will be ' +
3388
- 'installed. Fix the file to restore scoping.',
3389
- );
3390
- return LEGACY_ALL;
3391
- }
3392
- try {
3393
- return ruleScopeFromSettings(parsed as Record<string, unknown>, package_root);
3394
- } catch (e) {
3395
- warn(
3396
- `could not derive rule scope from ${settings_path} (${String(e)}) — ` +
3397
- 'falling back to legacy-all; ALL rules will be installed.',
3398
- );
3399
- return LEGACY_ALL;
3400
- }
3277
+ return resolveGlobalRuleScope({
3278
+ settingsPath: _resolve_global_settings_path(),
3279
+ packageRoot: package_root,
3280
+ readText,
3281
+ parseYaml: yamlSafeLoad,
3282
+ loadDefaults: () => _load_default_settings(package_root),
3283
+ warn,
3284
+ });
3401
3285
  }
3402
3286
 
3403
3287
  /**
@@ -5136,6 +5020,9 @@ function _main_project_install(
5136
5020
  if (will_launch) {
5137
5021
  return _wizard_spawn(project_root);
5138
5022
  }
5023
+ // No wizard, so nothing else installs the managed ignore block.
5024
+ const ign = gitignore_block.write_managed_block(project_root, opts.dry_run);
5025
+ if (ign > 0) success(`.gitignore: ${opts.dry_run ? 'would add ' : '+'}${ign} managed entries`);
5139
5026
  return 0;
5140
5027
  }
5141
5028
 
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Report what a host actually loads, per host rule directory (step 0.1).
4
+ *
5
+ * REPORT-ONLY, AND THE NAME SAYS SO. Phase 0 of
6
+ * `road-to-a-rule-carrier-that-works-outside-the-repo` makes no behavior
7
+ * change, so this never fails on a number: exit 0 whenever it could read, 2 on
8
+ * usage. It is also deliberately NOT named `check_*` or `lint_*` —
9
+ * `_lib/gate_population.ts` enters any script with those prefixes into the gate
10
+ * population and the ledger then owes it a per-target accounting, which is the
11
+ * wrong contract for an instrument. A later ceiling over this measurement is a
12
+ * different script with a `check_` name.
13
+ *
14
+ * `--home` and `--project` exist so this can be pointed at a temporary `HOME`,
15
+ * which is the step's own framing and the thing that refutes
16
+ * `check_standing_rule_delivery`'s "no CI workflow performs a user-scope
17
+ * install": the installer reads `HOME` from the environment, so a runner can
18
+ * stage one and measure it. The library behind this takes the same two as
19
+ * parameters, which is what keeps CI, `doctor` and the upgrade receipt from
20
+ * each growing their own reader.
21
+ */
22
+
23
+ import * as os from 'node:os';
24
+ import * as path from 'node:path';
25
+ import process from 'node:process';
26
+
27
+ import { hostVersion } from './cache_realization_report.js';
28
+ import { manifest_path } from './_lib/installed_tools.js';
29
+ import {
30
+ buildInstalledLayerReport,
31
+ renderInstalledLayerReport,
32
+ type InstalledLayerReport,
33
+ } from './_lib/installed_layer.js';
34
+
35
+ const PROG = 'installed_layer_report';
36
+
37
+ interface Args {
38
+ home: string;
39
+ project: string;
40
+ json: boolean;
41
+ }
42
+
43
+ export function parseArgs(argv: readonly string[]): Args | null {
44
+ let home = process.env['HOME'] ?? os.homedir();
45
+ let project = process.cwd();
46
+ let json = false;
47
+ for (let i = 0; i < argv.length; i += 1) {
48
+ const a = argv[i];
49
+ if (a === '--json') {
50
+ json = true;
51
+ } else if (a === '--home') {
52
+ i += 1;
53
+ const v = argv[i];
54
+ if (v === undefined) return null;
55
+ home = v;
56
+ } else if (a === '--project') {
57
+ i += 1;
58
+ const v = argv[i];
59
+ if (v === undefined) return null;
60
+ project = v;
61
+ } else if (a === '--quiet') {
62
+ // Accepted and ignored: this prints one block either way, and a flag
63
+ // that silences a report leaves nothing to have run it for.
64
+ } else {
65
+ return null;
66
+ }
67
+ }
68
+ return { home, project, json };
69
+ }
70
+
71
+ /** Build the report for one machine, resolving the manifest from the project root. */
72
+ export function reportFor(args: Args): InstalledLayerReport {
73
+ return buildInstalledLayerReport({
74
+ home: args.home,
75
+ projectRoot: args.project,
76
+ manifestPath: manifest_path(args.project),
77
+ hostVersion: hostVersion(),
78
+ });
79
+ }
80
+
81
+ export function main(argv: readonly string[] = process.argv.slice(2)): number {
82
+ const args = parseArgs(argv);
83
+ if (args === null) {
84
+ process.stderr.write(`usage: ${PROG} [--home <dir>] [--project <dir>] [--json]\n`);
85
+ return 2;
86
+ }
87
+ const report = reportFor(args);
88
+ if (args.json) {
89
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
90
+ } else {
91
+ process.stdout.write(`${renderInstalledLayerReport(report).join('\n')}\n`);
92
+ }
93
+ // One machine-readable line, in the shape every reporting script here uses.
94
+ // It counts FILES across every layer read, which is the denominator the
95
+ // character total is over — a reader comparing two runs needs both.
96
+ process.stdout.write(`scanned: ${String(report.totals.files)}\n`);
97
+ return 0;
98
+ }
99
+
100
+ const _entry = process.argv[1];
101
+ if (_entry !== undefined && path.resolve(_entry) === path.resolve(new URL(import.meta.url).pathname)) {
102
+ process.exit(main());
103
+ }
@@ -408,7 +408,19 @@ export function noticeText(language: Exclude<Verdict, "und">, source: PinSource)
408
408
  return `Reply language for this turn: ${name}.`;
409
409
  }
410
410
 
411
- /** The context block handed back to the model for this turn. */
411
+ /**
412
+ * The context block handed back to the model for this turn.
413
+ *
414
+ * It carries the target language, where it applies, and what is NOT the
415
+ * trigger — and deliberately no provenance. The paragraph that used to explain
416
+ * why the pin exists (the user-role skill bodies, the 626 wrong-language turns)
417
+ * is this file's own header, above: a maintainer reads it once, while the
418
+ * payload is injected on every prompt, so the same sentences were being paid
419
+ * for per turn to tell the model something it cannot act on. Removing it took
420
+ * the prompt-sourced block from 702 characters to 429 (German; 693 to 420 in
421
+ * English) and changed no instruction. Do not put it back; extend the header
422
+ * instead.
423
+ */
412
424
  export function pinText(language: Exclude<Verdict, "und">, source: PinSource = "prompt"): string {
413
425
  const name = LANGUAGE_NAME[language];
414
426
  if (source === "system-locale") {
@@ -430,11 +442,8 @@ export function pinText(language: Exclude<Verdict, "und">, source: PinSource = "
430
442
  `for EVERY user-visible token of your replies this turn — including the short ` +
431
443
  `inter-tool lines between tool calls, headings, table cells, bullets, status ` +
432
444
  `lines, and the recommendation label under any numbered-options block.\n\n` +
433
- `This pin exists because the trigger is not observable from the transcript ` +
434
- `alone: slash-command and skill bodies arrive in the user role and can be ` +
435
- `long and English, which is what drove the 626 wrong-language turns measured ` +
436
- `in the 30-session audit this hook was built from. Tool output, file ` +
437
- `contents, and an injected skill body are NOT the trigger — this pin is.\n` +
445
+ `Tool output, file contents, and an injected skill body are NOT the ` +
446
+ `trigger — this pin is.\n` +
438
447
  `</language-pin>`
439
448
  );
440
449
  }
@@ -22,8 +22,15 @@
22
22
  * auto-promoted) — it NEVER mutates the curated YAML truths. Deterministic:
23
23
  * byte-stable for a fixed `now` + a fixed intake set.
24
24
  *
25
+ * `--format status` is the third, human-facing view (road-to-learning-you-can-
26
+ * see Phase 2): the verdict counts, the top `preferred` lessons with their
27
+ * corroboration and age, and the sentence saying what a promotion requires. It
28
+ * is a FORMAT and not a verb (decision D2 cites ADR-041) — `memory:learn`
29
+ * passes argv straight through, so a format needs no registry row — and it is
30
+ * read-only to the point of refusing `--write` rather than ignoring it.
31
+ *
25
32
  * Usage: learning_sidecar.ts [--intake-dir DIR] [--out-dir DIR] [--now ISO]
26
- * [--write] [--format text|json]
33
+ * [--write] [--format text|json|status]
27
34
  * Exit codes: 0 ok, 2 usage error, 3 internal error.
28
35
  */
29
36
  import { createHash } from 'node:crypto';
@@ -48,7 +55,11 @@ export const SCHEMA_VERSION = 1;
48
55
  export type Polarity = 'preferred' | 'dead_end';
49
56
  export type Verdict = 'preferred' | 'contested' | 'dead_end';
50
57
 
51
- interface Signal {
58
+ /** The output formats. `status` is the one-screen human view (read-only). */
59
+ export const FORMATS = ['text', 'json', 'status'] as const;
60
+ export type Format = (typeof FORMATS)[number];
61
+
62
+ export interface Signal {
52
63
  id: string;
53
64
  ts: string;
54
65
  tsMs: number;
@@ -226,9 +237,21 @@ export function aggregate(signals: readonly Signal[], nowMs: number): Lesson[] {
226
237
  }
227
238
 
228
239
  export function buildSidecar(intakeDir: string, nowIso: string): Sidecar {
240
+ return buildSidecarFromSignals(readSignals(intakeDir), nowIso);
241
+ }
242
+
243
+ /**
244
+ * The half of `buildSidecar` that does not read the disk.
245
+ *
246
+ * Split out for a caller that needs BOTH the input count and the output
247
+ * lessons from one pass — the session-end dogfood ledger records `signals_in`
248
+ * beside `lessons_out`, and reading the intake twice to get them would make the
249
+ * two numbers describe two different reads under the 2 s budget they exist to
250
+ * measure.
251
+ */
252
+ export function buildSidecarFromSignals(signals: readonly Signal[], nowIso: string): Sidecar {
229
253
  const nowMs = _parseTsMs(nowIso) ?? 0;
230
- const lessons = aggregate(readSignals(intakeDir), nowMs);
231
- return { schema_version: SCHEMA_VERSION, generated_at: nowIso, lessons };
254
+ return { schema_version: SCHEMA_VERSION, generated_at: nowIso, lessons: aggregate(signals, nowMs) };
232
255
  }
233
256
 
234
257
  /** Render the human dead-ends + contested ledger. */
@@ -261,22 +284,98 @@ export function renderLessonsMd(sidecar: Sidecar): string {
261
284
  return L.join('\n');
262
285
  }
263
286
 
287
+ /** How many `preferred` lessons the status screen lists before it stops. */
288
+ export const STATUS_TOP_N = 5;
289
+
290
+ /**
291
+ * The human step. Printed as the LAST line of the status screen, because this
292
+ * tree writes no rule and no skill on its own: a lesson becomes a durable
293
+ * artefact only when a person carries it through `/memory:propose` or the
294
+ * `learning-to-rule-or-skill` skill. A list of preferred lessons is one flag
295
+ * away from `--apply`, and naming the hand-off on every screen is the cheapest
296
+ * standing answer to "why is there no button".
297
+ */
298
+ export const PROMOTION_HANDOFF =
299
+ 'Promote by hand: /memory:propose · src/skills/learning-to-rule-or-skill/SKILL.md';
300
+
301
+ /**
302
+ * One screen answering "what has this learned?" — verdict counts, the top
303
+ * `preferred` lessons with their corroboration and age, and what a promotion
304
+ * requires. READ ONLY by construction: it takes a built sidecar and returns a
305
+ * string, so there is no path from this function to the disk.
306
+ */
307
+ export function renderStatus(sidecar: Sidecar): string {
308
+ const byVerdict: Record<Verdict, number> = { preferred: 0, contested: 0, dead_end: 0 };
309
+ for (const l of sidecar.lessons) byVerdict[l.verdict] += 1;
310
+
311
+ const nowMs = _parseTsMs(sidecar.generated_at) ?? 0;
312
+ const L: string[] = [];
313
+ L.push(`${PROG}: ${String(sidecar.lessons.length)} lesson(s) as of ${sidecar.generated_at}`);
314
+ L.push(
315
+ `preferred: ${String(byVerdict.preferred)} · ` +
316
+ `contested: ${String(byVerdict.contested)} · ` +
317
+ `dead-end: ${String(byVerdict.dead_end)}`,
318
+ );
319
+ L.push('');
320
+
321
+ const preferred = sidecar.lessons
322
+ .filter((l) => l.verdict === 'preferred')
323
+ .sort((a, b) => b.score - a.score || (a.path < b.path ? -1 : 1))
324
+ .slice(0, STATUS_TOP_N);
325
+ L.push(`Top ${String(STATUS_TOP_N)} preferred:`);
326
+ if (preferred.length === 0) {
327
+ L.push(' (none — a lesson needs corroboration before it appears here)');
328
+ } else {
329
+ preferred.forEach((l, i) => {
330
+ // Age is floored, so a lesson seen hours ago reads `0d old` rather
331
+ // than rounding up into a day it has not lived.
332
+ const ageDays = Math.max(0, Math.floor((nowMs - (_parseTsMs(l.last_ts) ?? nowMs)) / 86400000));
333
+ L.push(
334
+ ` ${String(i + 1)}. ${l.entry_type} @ ${l.path} — ` +
335
+ `${String(l.corroborations)} origins, ${String(ageDays)}d old`,
336
+ );
337
+ L.push(` ${l.body}`);
338
+ });
339
+ }
340
+ L.push('');
341
+ L.push(
342
+ `A lesson is promoted at ${String(MIN_CORROBORATIONS)} distinct origins, ` +
343
+ `weighted by a ${String(HALF_LIFE_DAYS)}-day half-life; nothing here promotes itself.`,
344
+ );
345
+ L.push(PROMOTION_HANDOFF);
346
+ return `${L.join('\n')}\n`;
347
+ }
348
+
264
349
  export function main(argv: string[]): number {
265
350
  let intakeDir = path.join('agents', 'memory', 'intake');
266
351
  let outDir = path.join('agents', 'memory');
267
352
  let nowIso = '';
268
353
  let write = false;
269
- let format: 'text' | 'json' = 'text';
354
+ let format: Format = 'text';
270
355
  for (let i = 0; i < argv.length; i++) {
271
356
  const a = argv[i] as string;
272
357
  if (a === '--intake-dir') intakeDir = argv[++i] ?? intakeDir;
273
358
  else if (a === '--out-dir') outDir = argv[++i] ?? outDir;
274
359
  else if (a === '--now') nowIso = argv[++i] ?? '';
275
360
  else if (a === '--write') write = true;
276
- else if (a === '--format') format = (argv[++i] as 'text' | 'json') ?? 'text';
277
- else if (a === '-h' || a === '--help') {
361
+ else if (a === '--format') {
362
+ const raw = argv[++i] ?? '';
363
+ // Validated rather than cast. With two formats an unknown value
364
+ // fell through to `text` and looked like it had worked; with three,
365
+ // a typo in `status` would silently print the one-line text form
366
+ // and a reader would take that for the screen they asked for.
367
+ if (!(FORMATS as readonly string[]).includes(raw)) {
368
+ process.stderr.write(
369
+ `${PROG}: error: argument --format: invalid choice: '${raw}' ` +
370
+ `(choose from ${FORMATS.map((f) => `'${f}'`).join(', ')})\n`,
371
+ );
372
+ return 2;
373
+ }
374
+ format = raw as Format;
375
+ } else if (a === '-h' || a === '--help') {
278
376
  process.stdout.write(
279
- `usage: ${PROG} [--intake-dir DIR] [--out-dir DIR] [--now ISO] [--write] [--format text|json]\n`,
377
+ `usage: ${PROG} [--intake-dir DIR] [--out-dir DIR] [--now ISO] [--write] ` +
378
+ `[--format ${FORMATS.join('|')}]\n`,
280
379
  );
281
380
  return 0;
282
381
  } else {
@@ -284,6 +383,14 @@ export function main(argv: string[]): number {
284
383
  return 2;
285
384
  }
286
385
  }
386
+ if (format === 'status' && write) {
387
+ // Refused, not ignored. The acceptance criterion is that the screen
388
+ // leaves `git status` identical; ignoring the flag would honour that
389
+ // too, but it would do so silently, and a caller who asked for a write
390
+ // deserves to be told it did not happen.
391
+ process.stderr.write(`${PROG}: error: --format status is read-only; drop --write\n`);
392
+ return 2;
393
+ }
287
394
  if (!nowIso) {
288
395
  // Deterministic by contract: refuse to guess `now` from the clock when
289
396
  // writing, so output is reproducible. Callers pass --now explicitly.
@@ -310,6 +417,10 @@ export function main(argv: string[]): number {
310
417
  process.stdout.write(JSON.stringify(sidecar, null, 2) + '\n');
311
418
  return 0;
312
419
  }
420
+ if (format === 'status') {
421
+ process.stdout.write(renderStatus(sidecar));
422
+ return 0;
423
+ }
313
424
  const byVerdict = { preferred: 0, contested: 0, dead_end: 0 };
314
425
  for (const l of sidecar.lessons) byVerdict[l.verdict] += 1;
315
426
  process.stdout.write(