@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
@@ -65,8 +65,16 @@ import {
65
65
  owns_session_state,
66
66
  } from "./hooks/state_io.js";
67
67
  import { readHookStdin } from "./hooks/hook_stdin.js";
68
+ import { hookSectionValue } from "./_lib/hook_settings.js";
68
69
  import { isVerificationCommand, mightBeVerification } from "./_lib/verification_command.js";
70
+ import {
71
+ advisoryLine,
72
+ runTouchedFileQuality,
73
+ touchedFilesAtStop,
74
+ type QualityRun,
75
+ } from "./_lib/touched_file_quality.js";
69
76
  import { runnerOf } from "./_lib/verification_evidence.js";
77
+ import { resolve_toolchain } from "../agent-src/templates/scripts/work_engine/stack/runner.js";
70
78
 
71
79
  // NOTE: the Python docstring says `agents/runtime/state/`, but the code
72
80
  // constant is `agents/state/`. Replicated verbatim — latent docstring/code
@@ -179,10 +187,87 @@ function _empty_state(): StateDict {
179
187
  // a reader must never have to distinguish "no runs yet" from "key absent".
180
188
  verification_runs: [],
181
189
  edits_this_turn: 0,
190
+ // NOTE — `quality_runs` and `quality_files_source` are deliberately ABSENT
191
+ // here, against the convention `verification_runs` above follows.
192
+ //
193
+ // That convention ("present in the empty state so a reader never has to tell
194
+ // 'no runs yet' from 'key absent'") is the right default and it loses to a
195
+ // stronger one: the roadmap's first acceptance criterion is that with
196
+ // `touched_file_quality: off` the record is BYTE-IDENTICAL to the base ref.
197
+ // Seeding two keys would break that for every install that never opted in,
198
+ // and "the default-off feature changed my state file" is exactly the kind of
199
+ // silent shape drift a consumer cannot audit. The convention's cost here is
200
+ // zero, because the only reader of these keys is 2.1's analysis, which counts
201
+ // stops — and a stop with no key is a stop where the pass did not run, which
202
+ // is a distinction it needs rather than one it must not make.
182
203
  checked_at: _now(),
183
204
  };
184
205
  }
185
206
 
207
+ /** The three values `hooks.verify_before_complete.touched_file_quality` may take. */
208
+ export type TouchedFileQualityMode = "off" | "shadow" | "warn";
209
+
210
+ /**
211
+ * The configured mode, defaulting to `off`.
212
+ *
213
+ * FAIL-CLOSED ON THE FLAG, FAIL-OPEN ON THE READ, exactly as
214
+ * `hookSectionEnabled` does: an unreadable settings file, a missing key, or a
215
+ * value outside the enum all resolve to `off`. A typo must never turn a pass
216
+ * ON — `warn` emits a line the operator did not ask for, and `shadow` spawns
217
+ * processes, so an unrecognised string is the one case where guessing is
218
+ * strictly worse than doing nothing.
219
+ */
220
+ export function readTouchedFileQualityMode(consumer_root: string): TouchedFileQualityMode {
221
+ const raw = hookSectionValue(consumer_root, "verify_before_complete", "touched_file_quality");
222
+ return raw === "shadow" || raw === "warn" ? raw : "off";
223
+ }
224
+
225
+ /**
226
+ * Run the shadow quality pass for this stop, or return nothing.
227
+ *
228
+ * Kept OUTSIDE the state lock on purpose. `update_json_under_lock` holds a lock
229
+ * around its mutator, and a mutator that spawns `tsc` would hold it for the
230
+ * whole typecheck — every concurrent concern writing this session's state would
231
+ * block behind a linter. The pass runs first, the result is handed to `_update`
232
+ * as data, and the lock stays as short as it was.
233
+ */
234
+ export function collectTouchedFileQuality(
235
+ consumer_root: string,
236
+ session_id: string,
237
+ ): { runs: QualityRun[]; source: string } | null {
238
+ let touched: { files: readonly string[]; source: string };
239
+ try {
240
+ touched = touchedFilesAtStop(consumer_root, session_id);
241
+ } catch {
242
+ return null;
243
+ }
244
+ if (touched.files.length === 0) {
245
+ // Nothing to measure. Recorded as an empty run list with its source, not as
246
+ // silence: 2.1 counts "stops with edits" against "stops with at least one
247
+ // entry", and a stop that produced no entry has to be distinguishable from
248
+ // a stop where the pass never ran.
249
+ return { runs: [], source: touched.source };
250
+ }
251
+ let commands: readonly string[] = [];
252
+ try {
253
+ commands = resolve_toolchain(consumer_root).quality;
254
+ } catch {
255
+ commands = [];
256
+ }
257
+ try {
258
+ return {
259
+ runs: runTouchedFileQuality({
260
+ root: consumer_root,
261
+ commands,
262
+ files: touched.files,
263
+ }),
264
+ source: touched.source,
265
+ };
266
+ } catch {
267
+ return null;
268
+ }
269
+ }
270
+
186
271
 
187
272
  /** Return [tool_name, command_text] from a tool-event payload. */
188
273
  function _extract_command(payload: StateDict): [string | null, string | null] {
@@ -402,6 +487,21 @@ function _cap_runs(runs: unknown[]): unknown[] {
402
487
  */
403
488
  const _ERROR_EXIT_PREFIX = /^Error:\s*Exit code\s*(\d+)/i;
404
489
 
490
+ /**
491
+ * The same statement without the `Error:` word, which is how the host words it
492
+ * on its FAILURE event.
493
+ *
494
+ * Measured 2026-10-01 on Claude Code 2.1.286: a failing `Bash` call fires
495
+ * `PostToolUseFailure`, whose envelope carries NO `tool_response` at all. The
496
+ * exit status is a top-level `error` string whose first line reads
497
+ * `Exit code N`, with the command's own diagnostics on the lines after it. The
498
+ * prefix above never matches that, so for as long as only the success event was
499
+ * bound this recorder could not have written a non-zero exit even once — which
500
+ * is exactly what 24 consecutive all-zero records on this host turned out to
501
+ * mean.
502
+ */
503
+ const _BARE_EXIT_PREFIX = /^\s*Exit code\s*(\d+)/i;
504
+
405
505
  /** The status a post-tool payload actually reports, and HOW it reported it. */
406
506
  interface ExitReading {
407
507
  readonly code: number | null;
@@ -450,27 +550,91 @@ function _numeric_exit_field(obj: StateDict): number | null {
450
550
  * record path was inert — found by an independent review of the branch that
451
551
  * introduced it, against 1,077 real tool results.
452
552
  */
453
- function _extract_exit_reading(payload: StateDict): ExitReading {
553
+ /**
554
+ * Did the HOST say this call failed, independently of anything in its text?
555
+ *
556
+ * Read from the native event name, which the host writes in two places and the
557
+ * agent in neither: `hook_event_name` inside the payload, and `native_event` on
558
+ * the dispatcher envelope. Either is authoritative; both are checked because a
559
+ * raw-payload invocation carries only the first and a synthesised envelope only
560
+ * the second.
561
+ *
562
+ * An explicit set of OBSERVED names, with a suffix match behind it as a declared
563
+ * fallback rather than as the primary rule. A reviewer called the suffix-only
564
+ * form a maintenance trap and was right about the direction: the set is what a
565
+ * reader checks against the manifest, and `PostToolUseFailure` is the one
566
+ * spelling this package has actually seen a host send.
567
+ *
568
+ * The fallback is kept, and keeping it is the deliberate half. A host whose
569
+ * failure event this tree has never observed is exactly the case where guessing
570
+ * wrong is cheapest in one direction and expensive in the other: a false
571
+ * positive costs a refusal to record a zero, a false negative costs a
572
+ * manufactured pass on a red run. So the set is the rule and the suffix is the
573
+ * safe default for a name nobody here has met yet.
574
+ */
575
+ export const FAILURE_EVENT_NAMES: ReadonlySet<string> = new Set(["PostToolUseFailure"]);
576
+
577
+ function _is_failure_event(payload: StateDict, envelope: StateDict): boolean {
578
+ for (const v of [payload["hook_event_name"], envelope["native_event"]]) {
579
+ if (typeof v !== "string" || v === "") continue;
580
+ if (FAILURE_EVENT_NAMES.has(v)) return true;
581
+ if (/failure$/i.test(v)) return true;
582
+ }
583
+ return false;
584
+ }
585
+
586
+ function _extract_exit_reading(payload: StateDict, failure_event = false): ExitReading {
587
+ // On an event the host itself named a FAILURE, a zero is never recorded —
588
+ // from any of the three readings. The host has already stated the call did not
589
+ // succeed, so a `0` arrived at by parsing its text would contradict the only
590
+ // authoritative thing in the envelope, and it would do so in the single most
591
+ // dangerous direction: manufacturing the strongest possible pass evidence out
592
+ // of a red run. `null` instead, which the classifier already treats as an
593
+ // instrument gap and never as a pass.
594
+ //
595
+ // Raised by an independent reviewer who asked what happens when a command's
596
+ // own error text begins `Exit code 0`. Nothing observed produces that, which
597
+ // is exactly why it is worth closing before something does.
598
+ const seal = (r: ExitReading): ExitReading =>
599
+ failure_event && r.code === 0 ? { code: null, source: null, interrupted: r.interrupted } : r;
600
+
454
601
  const direct = _numeric_exit_field(payload);
455
- if (direct !== null) return { code: direct, source: "field", interrupted: false };
602
+ if (direct !== null) return seal({ code: direct, source: "field", interrupted: false });
603
+
604
+ // The FAILURE envelope, read before the success keys because it carries none
605
+ // of them: no `tool_response`, the status in a top-level `error` string, and
606
+ // the interrupt flag spelled `is_interrupt` rather than the nested
607
+ // `interrupted` below. Interruption is checked FIRST — a killed call is a
608
+ // statement about the kill and not a verdict on the work, so a host that
609
+ // sends both must not have its kill read as a failing exit.
610
+ const interrupted_top = payload["is_interrupt"] === true;
611
+ const err = payload["error"];
612
+ if (typeof err === "string") {
613
+ if (interrupted_top) return { code: null, source: null, interrupted: true };
614
+ const bare = _BARE_EXIT_PREFIX.exec(err) ?? _ERROR_EXIT_PREFIX.exec(err);
615
+ if (bare?.[1] !== undefined) {
616
+ return seal({ code: Number(bare[1]), source: "error_prefix", interrupted: false });
617
+ }
618
+ }
619
+ if (interrupted_top) return { code: null, source: null, interrupted: true };
456
620
 
457
621
  for (const key of ["tool_response", "toolResponse", "result", "output"]) {
458
622
  const v = payload[key];
459
623
  if (typeof v === "string") {
460
624
  const m = _ERROR_EXIT_PREFIX.exec(v);
461
625
  if (m?.[1] !== undefined) {
462
- return { code: Number(m[1]), source: "error_prefix", interrupted: false };
626
+ return seal({ code: Number(m[1]), source: "error_prefix", interrupted: false });
463
627
  }
464
628
  continue;
465
629
  }
466
630
  if (v === null || typeof v !== "object" || Array.isArray(v)) continue;
467
631
  const obj = v as StateDict;
468
632
  const nested = _numeric_exit_field(obj);
469
- if (nested !== null) return { code: nested, source: "field", interrupted: false };
633
+ if (nested !== null) return seal({ code: nested, source: "field", interrupted: false });
470
634
  if (obj["interrupted"] === true) return { code: null, source: null, interrupted: true };
471
635
  const hasStream = typeof obj["stdout"] === "string" || typeof obj["stderr"] === "string";
472
636
  if (obj["interrupted"] === false && hasStream) {
473
- return { code: 0, source: "response_shape", interrupted: false };
637
+ return seal({ code: 0, source: "response_shape", interrupted: false });
474
638
  }
475
639
  }
476
640
  return _NO_EXIT;
@@ -504,10 +668,21 @@ function _extract_run_streams(payload: StateDict): { stdout: string; stderr: str
504
668
  }
505
669
  const topOut = payload["stdout"] ?? payload["output"];
506
670
  const topErr = payload["stderr"];
507
- return {
508
- stdout: typeof topOut === "string" ? topOut : "",
509
- stderr: typeof topErr === "string" ? topErr : "",
510
- };
671
+ if (typeof topOut === "string" || typeof topErr === "string") {
672
+ return {
673
+ stdout: typeof topOut === "string" ? topOut : "",
674
+ stderr: typeof topErr === "string" ? topErr : "",
675
+ };
676
+ }
677
+ // The failure envelope's one text field. Recorded as `stdout` because the
678
+ // host collapses both streams into it and says nothing about which was
679
+ // which — a recorder that split them would be inventing a fact — and because
680
+ // every parser in `verification_evidence` is line-anchored over stdout. The
681
+ // alternative, dropping it, would leave a red run recorded with its exit code
682
+ // and no reason, which is the half the reader actually needs.
683
+ const errText = payload["error"];
684
+ if (typeof errText === "string") return { stdout: errText, stderr: "" };
685
+ return { stdout: "", stderr: "" };
511
686
  }
512
687
 
513
688
  function _reset_turn(state: StateDict, session_id: string): StateDict {
@@ -521,6 +696,12 @@ function _reset_turn(state: StateDict, session_id: string): StateDict {
521
696
  // turn's edits, which is the freshness argument `verify-before-complete` makes.
522
697
  state["verification_runs"] = [];
523
698
  state["edits_this_turn"] = 0;
699
+ // TURN-scoped for the same reason as its neighbour: a quality verdict from
700
+ // the previous turn describes files this turn may already have changed, and a
701
+ // stale PASS is the direction that misleads. DELETED rather than emptied, so
702
+ // that a turn boundary restores the base-ref shape — see `_empty_state`.
703
+ delete state["quality_runs"];
704
+ delete state["quality_files_source"];
524
705
  // FC-3b turn-scoped counters: a CI settle must be witnessed within the same
525
706
  // turn that claims it, so the in-flight observation does not survive a turn.
526
707
  state["ci_saw_pending"] = false;
@@ -539,7 +720,19 @@ function _asInt(v: unknown): number {
539
720
  return Number.isFinite(n) ? Math.trunc(n) : 0;
540
721
  }
541
722
 
542
- function _update(state: StateDict, event: string, envelope: StateDict): StateDict {
723
+ /**
724
+ * Apply one event to the state.
725
+ *
726
+ * `quality` is the shadow pass's already-computed result, handed in rather than
727
+ * fetched: this function runs INSIDE the state lock, and a spawn in here would
728
+ * hold that lock for the duration of a typecheck.
729
+ */
730
+ function _update(
731
+ state: StateDict,
732
+ event: string,
733
+ envelope: StateDict,
734
+ quality: { runs: QualityRun[]; source: string } | null = null,
735
+ ): StateDict {
543
736
  const session_id = (envelope["session_id"] || state["session_id"] || "") as string;
544
737
  if (session_id && session_id !== state["session_id"]) {
545
738
  // Session boundary — reset session-scoped counters.
@@ -558,6 +751,16 @@ function _update(state: StateDict, event: string, envelope: StateDict): StateDic
558
751
  state = _reset_turn(state, session_id);
559
752
  }
560
753
 
754
+ // The host that wrote this file. Recorded because the instrument-gap reading
755
+ // is a PER-HOST question and the witness carried no answer to it: every
756
+ // coverage number had to be attributed by hand, and a file from an unknown
757
+ // host is indistinguishable from a host with nothing to report. Written on
758
+ // every update rather than once, so a file predating this field acquires it
759
+ // the first time the host touches it; until then a reader sees it absent,
760
+ // which is the honest reading and not a default.
761
+ const platform = envelope["platform"];
762
+ if (typeof platform === "string" && platform) state["platform"] = platform;
763
+
561
764
  let payload = envelope["payload"];
562
765
  if (!(typeof payload === "object" && payload !== null && !Array.isArray(payload))) {
563
766
  payload = {};
@@ -668,7 +871,7 @@ function _update(state: StateDict, event: string, envelope: StateDict): StateDic
668
871
  // own count is short.
669
872
  if (cmd && mightBeVerification(cmd)) {
670
873
  const streams = _extract_run_streams(pl);
671
- const exit = _extract_exit_reading(pl);
874
+ const exit = _extract_exit_reading(pl, _is_failure_event(pl, envelope));
672
875
  const runs = Array.isArray(state["verification_runs"])
673
876
  ? [...(state["verification_runs"] as unknown[])]
674
877
  : [];
@@ -699,6 +902,10 @@ function _update(state: StateDict, event: string, envelope: StateDict): StateDic
699
902
  }
700
903
  } else if (event === "stop") {
701
904
  state["last_stop_at"] = _now();
905
+ if (quality !== null) {
906
+ state["quality_runs"] = quality.runs;
907
+ state["quality_files_source"] = quality.source;
908
+ }
702
909
  }
703
910
 
704
911
  state["checked_at"] = _now();
@@ -760,6 +967,15 @@ export function run(
760
967
 
761
968
  const target = path.join(consumer_root, statePathFor(session_id));
762
969
 
970
+ // The shadow quality pass, computed BEFORE the lock (see `_update`'s header)
971
+ // and only at `stop`. `off` is the template default and the resolved value of
972
+ // every unreadable or unrecognised setting, so an install that never opted in
973
+ // spawns nothing, reads no manifest and writes no new field — the acceptance
974
+ // criterion is that its record is byte-identical to the base ref.
975
+ const quality_mode = event === "stop" ? readTouchedFileQualityMode(consumer_root) : "off";
976
+ const quality =
977
+ quality_mode === "off" ? null : collectTouchedFileQuality(consumer_root, session_id);
978
+
763
979
  // LOAD → UPDATE → PUBLISH under ONE lock, not three separate steps.
764
980
  //
765
981
  // This was `_load_state` / `_update` / `atomic_write_json`, which makes the
@@ -790,7 +1006,7 @@ export function run(
790
1006
  // treats a missing counter as 0 and the `=== true` guards treat a missing
791
1007
  // flag as false, so every assertion would still pass while `schema_version`
792
1008
  // and an explicit `ci_last: null` vanished from a freshly created file.
793
- state = _update({ ..._empty_state(), ...loaded } as StateDict, event, envelope);
1009
+ state = _update({ ..._empty_state(), ...loaded } as StateDict, event, envelope, quality);
794
1010
  return state;
795
1011
  });
796
1012
  if (outcome === "failed") {
@@ -811,6 +1027,21 @@ export function run(
811
1027
  );
812
1028
  }
813
1029
 
1030
+ // `warn` — ONE advisory line, and the exit code does not move.
1031
+ //
1032
+ // In this tree a warn is conventionally an exit of 2, and on the one host that
1033
+ // honours a deny that reads as a BLOCK. D3 of the roadmap settles it the other
1034
+ // way for this field: shadow → warn only, never a block, on the strength of
1035
+ // this hook's own contract ("the hook itself never blocks — it is
1036
+ // observability infra, not control flow"). So the line goes to stderr and the
1037
+ // function returns 0 on this path exactly as on every other.
1038
+ if (quality_mode === "warn" && quality !== null) {
1039
+ const line = advisoryLine(quality.runs);
1040
+ if (line !== null) {
1041
+ process.stderr.write(`${line}\n`);
1042
+ }
1043
+ }
1044
+
814
1045
  if (verbose) {
815
1046
  process.stderr.write(
816
1047
  `verify-before-complete-hook: event=${event} ` +
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * bench_touched_file_quality — what the shadow quality pass costs at stop.
4
+ *
5
+ * `road-to-touched-files-that-pass-their-own-tools` 1.4: record the `shadow`
6
+ * variant's p95 over a five-file TypeScript fixture BEFORE any default moves.
7
+ * Risk 1 of that roadmap is that a quality pass makes stop the slowest hook in
8
+ * the chain, and 2.3 defers the `shadow → warn` flip to the owner. Neither
9
+ * decision should be taken on an estimate.
10
+ *
11
+ * CORRECTED FROM REPRODUCTION — the step's premise was wrong, and the
12
+ * correction is why this is a file rather than a patch.
13
+ *
14
+ * The step reads "the per-concern bench already measures `verify-before-complete`
15
+ * at default". It does not. `bench_hook_latency.perConcernRows` is fed by
16
+ * `blockingConcerns()`, which filters `hook_manifest.yaml` to
17
+ * `severity: blocking`; `verify-before-complete` is declared `severity: advisory`
18
+ * (manifest line ~122), so it has never had a per-concern row and there was no
19
+ * `default` reading to add a `shadow` variant beside. Adding the concern to that
20
+ * filter would have changed what "blocking" means in a report whose consumer
21
+ * (step 3.3 of the kernel-plumbing roadmap) derives per-concern TIMEOUTS from it
22
+ * — a timeout for an advisory concern is a different decision, and smuggling it
23
+ * in through a measurement would have been the wrong place to take it.
24
+ *
25
+ * So this measures the pass directly, which is also the number the two open
26
+ * decisions actually need: the slot p95 in `docs/hook-latency.json` is dominated
27
+ * by spawn + bundle load, and what 1.4 asks about is the DELTA the pass adds.
28
+ *
29
+ * METHOD, pinned so the number reproduces:
30
+ * - A throwaway consumer-shaped project: `package.json` declaring `typescript`
31
+ * and `eslint`, five TypeScript files, a flat ESLint config, `git init`, and
32
+ * a `minimal-safe-diff.json` naming all five as this turn's touched files.
33
+ * - `node_modules` is SYMLINKED to this repository's, so `npx eslint` resolves
34
+ * a real, installed linter and runs offline. The measurement is therefore of
35
+ * a real tool over real files, not of a stub.
36
+ * - N iterations (default 20, `--runs N`) of `collectTouchedFileQuality` — the
37
+ * exact function the stop path calls — measured with `performance.now()`.
38
+ * That covers the recorder read, `git status`, `resolve_toolchain`, and every
39
+ * spawn the plan produced.
40
+ * - The `off` baseline is measured too, by the same loop with the pass skipped,
41
+ * so the row reports a DELTA rather than an absolute whose floor is unknown.
42
+ *
43
+ * WHAT THE NUMBER IS NOT. It is one machine's reading, and the dominant term is
44
+ * whichever tools the fixture's resolver happens to emit — a consumer whose
45
+ * toolchain resolves `phpstan` over a large tree will measure something else
46
+ * entirely. It bounds the decision for THIS shape; it is not a budget.
47
+ *
48
+ * Exit codes: 0 measured · 2 the fixture could not be built.
49
+ */
50
+ import { spawnSync } from 'node:child_process';
51
+ import * as fs from 'node:fs';
52
+ import * as os from 'node:os';
53
+ import * as path from 'node:path';
54
+ import { fileURLToPath } from 'node:url';
55
+
56
+ import { collectTouchedFileQuality } from './before_complete_hook.js';
57
+
58
+ const REPO_ROOT = path.resolve(path.dirname(path.resolve(fileURLToPath(import.meta.url))), '..', '..');
59
+
60
+ /** Files in the fixture. Five, per the step. */
61
+ export const FIXTURE_FILE_COUNT = 5;
62
+
63
+ const SESSION = 'bench-touched-file-quality';
64
+
65
+ export function percentile(samples: readonly number[], p: number): number {
66
+ if (samples.length === 0) return 0;
67
+ const sorted = [...samples].sort((a, b) => a - b);
68
+ const idx = Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1);
69
+ return sorted[Math.max(0, idx)] as number;
70
+ }
71
+
72
+ /**
73
+ * Build the throwaway consumer project and return its root.
74
+ *
75
+ * Deliberately NOT inside the repository: a fixture under `src/` or `agents/`
76
+ * would be picked up by the tree's own gates, and one that `git init`s inside an
77
+ * existing worktree would report the repository's status rather than its own.
78
+ */
79
+ export function buildFixture(): string {
80
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'tfq-bench-'));
81
+
82
+ fs.writeFileSync(
83
+ path.join(root, 'package.json'),
84
+ JSON.stringify(
85
+ { name: 'tfq-bench-fixture', private: true, devDependencies: { typescript: '*', eslint: '*' } },
86
+ null,
87
+ 2,
88
+ ),
89
+ 'utf-8',
90
+ );
91
+ // A flat config with no rules: eslint still parses every file, which is the
92
+ // cost being measured, without a rule set that would make the number a
93
+ // statement about this repository's lint configuration.
94
+ fs.writeFileSync(path.join(root, 'eslint.config.mjs'), 'export default [];\n', 'utf-8');
95
+
96
+ for (let i = 0; i < FIXTURE_FILE_COUNT; i += 1) {
97
+ fs.writeFileSync(
98
+ path.join(root, `mod${String(i)}.ts`),
99
+ `export interface Row${String(i)} { id: number; name: string }\n` +
100
+ `export function build${String(i)}(n: number): Row${String(i)}[] {\n` +
101
+ ` return Array.from({ length: n }, (_, j) => ({ id: j, name: 'r' + String(j) }));\n` +
102
+ `}\n`,
103
+ 'utf-8',
104
+ );
105
+ }
106
+
107
+ try {
108
+ fs.symlinkSync(path.join(REPO_ROOT, 'node_modules'), path.join(root, 'node_modules'), 'dir');
109
+ } catch {
110
+ // Left absent on a platform that refuses the link: `npx eslint` then
111
+ // resolves to nothing and the pass records `skipped: absent`, which the
112
+ // report states rather than hides.
113
+ }
114
+
115
+ spawnSync('git', ['init', '-q'], { cwd: root });
116
+
117
+ fs.mkdirSync(path.join(root, 'agents', 'state'), { recursive: true });
118
+ fs.writeFileSync(
119
+ path.join(root, 'agents', 'state', 'minimal-safe-diff.json'),
120
+ JSON.stringify({
121
+ session_id: SESSION,
122
+ files_touched_this_turn: Array.from(
123
+ { length: FIXTURE_FILE_COUNT },
124
+ (_, i) => `mod${String(i)}.ts`,
125
+ ),
126
+ }),
127
+ 'utf-8',
128
+ );
129
+
130
+ return root;
131
+ }
132
+
133
+ export interface BenchRow {
134
+ readonly concern: string;
135
+ readonly mode: 'off' | 'shadow';
136
+ readonly runs: number;
137
+ readonly p50_ms: number;
138
+ readonly p95_ms: number;
139
+ readonly max_ms: number;
140
+ /** What the pass actually did, so a row of zeros is never mistaken for speed. */
141
+ readonly commands: readonly string[];
142
+ readonly skipped: readonly string[];
143
+ }
144
+
145
+ export function measure(root: string, iterations: number): { off: BenchRow; shadow: BenchRow } {
146
+ const offSamples: number[] = [];
147
+ const shadowSamples: number[] = [];
148
+ let commands: string[] = [];
149
+ let skipped: string[] = [];
150
+
151
+ // One warm-up outside the samples: the first `npx` resolution pays a cost no
152
+ // subsequent stop pays, and reporting it inside the p95 would describe a
153
+ // session's first stop as if it were every stop.
154
+ collectTouchedFileQuality(root, SESSION);
155
+
156
+ for (let i = 0; i < iterations; i += 1) {
157
+ const t0 = performance.now();
158
+ const result = collectTouchedFileQuality(root, SESSION);
159
+ shadowSamples.push(performance.now() - t0);
160
+ if (i === 0 && result !== null) {
161
+ commands = result.runs.filter((r) => r.skipped === null).map((r) => r.command);
162
+ skipped = result.runs
163
+ .filter((r) => r.skipped !== null)
164
+ .map((r) => `${r.source_command} → ${String(r.skipped)}`);
165
+ }
166
+
167
+ // `off` is not "the same loop with a flag" — on the off path the stop
168
+ // handler never reaches this function at all, so the baseline is the
169
+ // empty interval, measured to keep the harness's own overhead visible.
170
+ const t1 = performance.now();
171
+ offSamples.push(performance.now() - t1);
172
+ }
173
+
174
+ const row = (mode: 'off' | 'shadow', xs: number[]): BenchRow => ({
175
+ concern: 'verify-before-complete',
176
+ mode,
177
+ runs: xs.length,
178
+ p50_ms: Math.round(percentile(xs, 50) * 1000) / 1000,
179
+ p95_ms: Math.round(percentile(xs, 95) * 1000) / 1000,
180
+ max_ms: Math.round(Math.max(...xs, 0) * 1000) / 1000,
181
+ commands: mode === 'shadow' ? commands : [],
182
+ skipped: mode === 'shadow' ? skipped : [],
183
+ });
184
+
185
+ return { off: row('off', offSamples), shadow: row('shadow', shadowSamples) };
186
+ }
187
+
188
+ export function render(rows: { off: BenchRow; shadow: BenchRow }): string {
189
+ const line = (r: BenchRow): string =>
190
+ `| ${r.concern} | ${r.mode} | ${String(r.runs)} | ${r.p50_ms.toFixed(3)} | ` +
191
+ `${r.p95_ms.toFixed(3)} | ${r.max_ms.toFixed(3)} |`;
192
+ const out: string[] = [
193
+ '',
194
+ `fixture: ${String(FIXTURE_FILE_COUNT)} TypeScript files, consumer-shaped project`,
195
+ '',
196
+ '| concern | mode | runs | p50_ms | p95_ms | max_ms |',
197
+ '|---|---|---|---|---|---|',
198
+ line(rows.off),
199
+ line(rows.shadow),
200
+ '',
201
+ `delta p95 (shadow − off): ${(rows.shadow.p95_ms - rows.off.p95_ms).toFixed(3)} ms`,
202
+ '',
203
+ `commands executed: ${rows.shadow.commands.length === 0 ? '(none)' : rows.shadow.commands.join(' · ')}`,
204
+ `skipped: ${rows.shadow.skipped.length === 0 ? '(none)' : rows.shadow.skipped.join(' · ')}`,
205
+ '',
206
+ ];
207
+ return out.join('\n');
208
+ }
209
+
210
+ export function main(argv: readonly string[]): number {
211
+ const runsIdx = argv.indexOf('--runs');
212
+ const parsed = runsIdx === -1 ? NaN : Number(argv[runsIdx + 1]);
213
+ const iterations = Number.isFinite(parsed) && parsed > 0 ? Math.trunc(parsed) : 20;
214
+ const asJson = argv.includes('--json');
215
+
216
+ let root: string;
217
+ try {
218
+ root = buildFixture();
219
+ } catch (err) {
220
+ process.stderr.write(`bench_touched_file_quality: could not build the fixture — ${String(err)}\n`);
221
+ return 2;
222
+ }
223
+
224
+ const rows = measure(root, iterations);
225
+ process.stdout.write(asJson ? `${JSON.stringify(rows, null, 2)}\n` : render(rows));
226
+
227
+ try {
228
+ fs.rmSync(root, { recursive: true, force: true });
229
+ } catch {
230
+ // A leftover temp directory is not a failed measurement.
231
+ }
232
+ return 0;
233
+ }
234
+
235
+ const _entry = process.argv[1];
236
+ if (_entry !== undefined && path.resolve(_entry) === path.resolve(fileURLToPath(import.meta.url))) {
237
+ process.exit(main(process.argv.slice(2)));
238
+ }