@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
@@ -0,0 +1,434 @@
1
+ /**
2
+ * touched_file_quality — run the project's OWN quality commands, in shadow,
3
+ * over the files a turn actually edited.
4
+ *
5
+ * loss_class: ephemeral-lossy
6
+ *
7
+ * The one place this module shortens content is `advisoryLine`, whose 200-byte
8
+ * cap (roadmap 2.2) trims a summary it synthesised itself from `QualityRun[]`.
9
+ * The trimmed tail is not stored anywhere and is not meant to be: the runs are
10
+ * in-memory for one invocation, the line goes to stderr, and the verdict it
11
+ * summarises is reproduced by re-running the same resolver command. A recovery
12
+ * locator would promise a retrieval nothing performs.
13
+ *
14
+ * `road-to-touched-files-that-pass-their-own-tools` Phase 1. The project already
15
+ * computes a per-stack quality list — `resolve_toolchain().quality` in
16
+ * `src/agent-src/templates/scripts/work_engine/stack/runner.ts` — and nothing at
17
+ * stop calls it. This module is the caller. It owns no list of its own: D2 of the
18
+ * roadmap pins the resolver as the single authority, because a second list is a
19
+ * second authority that drifts from the first in silence.
20
+ *
21
+ * WHAT THIS IS NOT. It is not verification. A command recorded here never reaches
22
+ * `_lib/verification_command.ts`, never raises `verifications_this_turn`, and
23
+ * never sets `verified_this_turn` — `eslint` exits 0 over a clean file set, and a
24
+ * zero exit that reached the completion gate would mean a turn could claim "I
25
+ * verified" by having edited nothing interesting. The producer writes
26
+ * `quality_runs[]`; the classifier reads `verification_runs[]`. Two arrays, two
27
+ * readers, no overlap — pinned by a negative fixture rather than by this comment.
28
+ *
29
+ * FOUR RULES, AND EACH ONE EXISTS BECAUSE THE NAIVE VERSION IS HARMFUL.
30
+ *
31
+ * 1. A command runs only if the resolver listed it. Nothing is synthesised.
32
+ * 2. A command that WRITES runs only in its check form, or not at all. The
33
+ * resolver emits `vendor/bin/pint`, which rewrites the consumer's source; a
34
+ * shadow pass that reformats the tree it is observing is not a shadow pass.
35
+ * Where a check flag is known (`--test`) it is used; where it is not, the row
36
+ * is `skipped: mutating` and the command is never spawned.
37
+ * 3. A command with no file-scoped form is `skipped: unscoped`. `tsc --noEmit`
38
+ * cannot take a file list without discarding `tsconfig.json`, and running it
39
+ * project-wide at every stop makes stop the slowest hook in the chain. The
40
+ * resolver carries no cost field, so nothing here guesses that an unknown
41
+ * command is cheap: an unrecognised command is `unscoped` and is not run.
42
+ * 4. ENOENT or exit 127 is `skipped: absent`, counted apart from verdicts.
43
+ * Detection upstream is by manifest (`typescript` in `devDependencies`), not
44
+ * by presence on PATH, so "the tool is not installed" must never read as a red.
45
+ *
46
+ * Output per command: the command, its exit code, and the first 20 lines of its
47
+ * combined output. Bounded on purpose — this lands in a per-session state file
48
+ * that another hook reads on every turn.
49
+ */
50
+ import { spawnSync } from 'node:child_process';
51
+ import * as fs from 'node:fs';
52
+ import * as path from 'node:path';
53
+
54
+ /** Lines of combined output retained per command. */
55
+ export const OUTPUT_HEAD_LINES = 20;
56
+
57
+ /** Default per-command wall-clock bound, milliseconds. */
58
+ export const DEFAULT_TIMEOUT_MS = 30_000;
59
+
60
+ /**
61
+ * Why a command was not executed.
62
+ *
63
+ * `no_files` is the fourth reason and is not in the roadmap's prose list, which
64
+ * named three. It is needed and it is honest: running `eslint` over a turn that
65
+ * touched only `.php` files produces a verdict about nothing. 2.1 counts
66
+ * `skipped:*` separately from verdicts, and the glob is what makes a fourth
67
+ * reason additive rather than a contract change.
68
+ */
69
+ export type SkipReason = 'mutating' | 'unscoped' | 'absent' | 'no_files';
70
+
71
+ /** One resolver command, classified into a runnable form or a skip reason. */
72
+ export interface QualityPlan {
73
+ /** The resolver's command string, verbatim — the provenance of this row. */
74
+ readonly source_command: string;
75
+ /** argv actually spawned, or `null` when the row is skipped. */
76
+ readonly argv: readonly string[] | null;
77
+ /** The touched files passed to it. */
78
+ readonly files: readonly string[];
79
+ readonly skipped: SkipReason | null;
80
+ }
81
+
82
+ /** One executed (or deliberately skipped) quality command. */
83
+ export interface QualityRun {
84
+ /** The command as run, or the resolver's command when skipped. */
85
+ readonly command: string;
86
+ readonly source_command: string;
87
+ /** `null` when the row was skipped — never `0`, which is a real verdict. */
88
+ readonly exit_code: number | null;
89
+ readonly output_head: readonly string[];
90
+ readonly skipped: SkipReason | null;
91
+ readonly files: readonly string[];
92
+ readonly at: string;
93
+ }
94
+
95
+ /**
96
+ * How one resolver command is turned into a file-scoped, non-writing form.
97
+ *
98
+ * Keyed on the exact string the resolver emits. Keying on the whole command
99
+ * rather than on a parsed head is deliberate: the key IS the resolver's output,
100
+ * so a resolver change that this table has not seen falls through to `unscoped`
101
+ * and is not run, instead of being half-recognised and run in a form nobody
102
+ * chose.
103
+ */
104
+ interface ToolSpec {
105
+ /** argv prefix; the touched files are appended. */
106
+ readonly argv: readonly string[];
107
+ /** Extensions this tool reads. Empty means "any file". */
108
+ readonly extensions: readonly string[];
109
+ /**
110
+ * The resolver's own command writes files, and `argv` above is its check
111
+ * form. Recorded so a reader can see that the executed command differs from
112
+ * the resolver's by exactly the flag that makes it read-only.
113
+ */
114
+ readonly check_form: boolean;
115
+ }
116
+
117
+ const JS_EXTENSIONS = ['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts'] as const;
118
+
119
+ /**
120
+ * The scoped forms, one row per resolver command that HAS one.
121
+ *
122
+ * Everything the resolver can emit that is absent here — `npx tsc --noEmit`,
123
+ * `go vet ./...`, `cargo clippy` — is absent because it has no per-file form,
124
+ * and absence is what produces `skipped: unscoped`.
125
+ */
126
+ const SCOPED_FORMS: ReadonlyMap<string, ToolSpec> = new Map<string, ToolSpec>([
127
+ ['npx eslint .', { argv: ['npx', 'eslint'], extensions: JS_EXTENSIONS, check_form: false }],
128
+ [
129
+ 'vendor/bin/phpstan analyse',
130
+ { argv: ['vendor/bin/phpstan', 'analyse'], extensions: ['.php'], check_form: false },
131
+ ],
132
+ // `vendor/bin/pint` REWRITES source. `--test` is its check form: same tool,
133
+ // same rules, reports instead of writing. Risk 3 of the roadmap is that this
134
+ // module reformats the consumer's tree; this row plus the fixture that proves
135
+ // no file changed is the mitigation.
136
+ [
137
+ 'vendor/bin/pint',
138
+ { argv: ['vendor/bin/pint', '--test'], extensions: ['.php'], check_form: true },
139
+ ],
140
+ ['ruff check', { argv: ['ruff', 'check'], extensions: ['.py'], check_form: false }],
141
+ ['mypy .', { argv: ['mypy'], extensions: ['.py'], check_form: false }],
142
+ ]);
143
+
144
+ /**
145
+ * Resolver commands known to write files for which NO check form is known.
146
+ *
147
+ * Empty today — every mutating command the resolver emits (`vendor/bin/pint`)
148
+ * has one, and it is in `SCOPED_FORMS` above. The map exists so that the day a
149
+ * writing command without a `--check` lands, the answer is a row here
150
+ * (`skipped: mutating`, never spawned) rather than a judgement call at the call
151
+ * site. A mutating command that fell through to `unscoped` would be recorded
152
+ * under the wrong reason and counted in the wrong bucket by 2.1.
153
+ */
154
+ const MUTATING_WITHOUT_CHECK_FORM: ReadonlySet<string> = new Set<string>([]);
155
+
156
+ function extensionOf(file: string): string {
157
+ const base = path.basename(file);
158
+ const dot = base.lastIndexOf('.');
159
+ return dot <= 0 ? '' : base.slice(dot).toLowerCase();
160
+ }
161
+
162
+ /**
163
+ * Classify the resolver's list against the turn's touched files. PURE.
164
+ *
165
+ * Separated from execution so the four rules above are testable without
166
+ * spawning anything, and so a reader can see the whole policy in one return
167
+ * value before any process starts.
168
+ */
169
+ export function planQualityRuns(
170
+ commands: readonly string[],
171
+ files: readonly string[],
172
+ ): QualityPlan[] {
173
+ return commands.map((source_command): QualityPlan => {
174
+ if (MUTATING_WITHOUT_CHECK_FORM.has(source_command)) {
175
+ return { source_command, argv: null, files: [], skipped: 'mutating' };
176
+ }
177
+ const spec = SCOPED_FORMS.get(source_command);
178
+ if (spec === undefined) {
179
+ return { source_command, argv: null, files: [], skipped: 'unscoped' };
180
+ }
181
+ const matched =
182
+ spec.extensions.length === 0
183
+ ? [...files]
184
+ : files.filter((f) => spec.extensions.includes(extensionOf(f)));
185
+ if (matched.length === 0) {
186
+ return { source_command, argv: null, files: [], skipped: 'no_files' };
187
+ }
188
+ return { source_command, argv: [...spec.argv, ...matched], files: matched, skipped: null };
189
+ });
190
+ }
191
+
192
+ /** `argv` rendered for the record. Not re-parsed — display and audit only. */
193
+ export function renderCommand(argv: readonly string[]): string {
194
+ return argv.join(' ');
195
+ }
196
+
197
+ function headLines(text: string, limit: number): string[] {
198
+ if (text === '') return [];
199
+ return text
200
+ .split(/\r\n|\r|\n/)
201
+ .filter((line, i, all) => !(line === '' && i === all.length - 1))
202
+ .slice(0, limit);
203
+ }
204
+
205
+ /** ISO-8601 to seconds, matching the record's other timestamps. */
206
+ function now(): string {
207
+ return new Date().toISOString().replace(/\.\d{3}Z$/, '+00:00');
208
+ }
209
+
210
+ export interface RunOptions {
211
+ readonly root: string;
212
+ readonly commands: readonly string[];
213
+ readonly files: readonly string[];
214
+ readonly timeout_ms?: number;
215
+ /** Injected in tests so the table's behaviour is observable without a toolchain. */
216
+ readonly spawn?: (
217
+ argv: readonly string[],
218
+ cwd: string,
219
+ timeout_ms: number,
220
+ ) => { status: number | null; output: string; enoent: boolean };
221
+ }
222
+
223
+ function defaultSpawn(
224
+ argv: readonly string[],
225
+ cwd: string,
226
+ timeout_ms: number,
227
+ ): { status: number | null; output: string; enoent: boolean } {
228
+ const [head, ...rest] = argv;
229
+ if (head === undefined) return { status: null, output: '', enoent: true };
230
+ const res = spawnSync(head, rest, {
231
+ cwd,
232
+ timeout: timeout_ms,
233
+ encoding: 'utf-8',
234
+ // No shell. The argv comes from a committed table, and a shell would
235
+ // make a file path with a space into an injection surface.
236
+ shell: false,
237
+ maxBuffer: 4 * 1024 * 1024,
238
+ });
239
+ const enoent =
240
+ res.error !== undefined &&
241
+ (res.error as NodeJS.ErrnoException).code === 'ENOENT';
242
+ const output = `${res.stdout ?? ''}${res.stderr ?? ''}`;
243
+ return { status: res.status, output, enoent };
244
+ }
245
+
246
+ /**
247
+ * Execute the plan and return one record per resolver command.
248
+ *
249
+ * Never throws. A spawn that fails for any reason becomes a row, because a
250
+ * shadow pass that can take the stop hook down with it is worse than no shadow
251
+ * pass at all.
252
+ */
253
+ export function runTouchedFileQuality(opts: RunOptions): QualityRun[] {
254
+ const timeout_ms = opts.timeout_ms ?? DEFAULT_TIMEOUT_MS;
255
+ const spawn = opts.spawn ?? defaultSpawn;
256
+ const out: QualityRun[] = [];
257
+ for (const plan of planQualityRuns(opts.commands, opts.files)) {
258
+ if (plan.argv === null) {
259
+ out.push({
260
+ command: plan.source_command,
261
+ source_command: plan.source_command,
262
+ exit_code: null,
263
+ output_head: [],
264
+ skipped: plan.skipped,
265
+ files: [],
266
+ at: now(),
267
+ });
268
+ continue;
269
+ }
270
+ let status: number | null = null;
271
+ let output = '';
272
+ let enoent = false;
273
+ try {
274
+ const res = spawn(plan.argv, opts.root, timeout_ms);
275
+ status = res.status;
276
+ output = res.output;
277
+ enoent = res.enoent;
278
+ } catch {
279
+ enoent = true;
280
+ }
281
+ // Exit 127 is the shell's "command not found" and reaches us when the
282
+ // tool is a wrapper script whose own dependency is missing. Same meaning
283
+ // as ENOENT for this module's purposes, and the same bucket in 2.1.
284
+ const absent = enoent || status === 127;
285
+ out.push({
286
+ command: renderCommand(plan.argv),
287
+ source_command: plan.source_command,
288
+ exit_code: absent ? null : status,
289
+ output_head: absent ? [] : headLines(output, OUTPUT_HEAD_LINES),
290
+ skipped: absent ? 'absent' : null,
291
+ files: plan.files,
292
+ at: now(),
293
+ });
294
+ }
295
+ return out;
296
+ }
297
+
298
+ /**
299
+ * The single advisory line `warn` mode emits — command, first file, verdict.
300
+ *
301
+ * `null` when nothing failed. Capped at 200 bytes by the roadmap's 2.2, and the
302
+ * cap is enforced on the UTF-8 byte length rather than on `String.length`, which
303
+ * counts code units and would let a path with non-ASCII characters through at
304
+ * nearly twice the budget.
305
+ */
306
+ export const WARN_LINE_MAX_BYTES = 200;
307
+
308
+ export function advisoryLine(runs: readonly QualityRun[]): string | null {
309
+ const failed = runs.find((r) => r.skipped === null && r.exit_code !== null && r.exit_code !== 0);
310
+ if (failed === undefined) return null;
311
+ const file = failed.files[0] ?? '(no file)';
312
+ const more = failed.files.length > 1 ? ` (+${String(failed.files.length - 1)} more)` : '';
313
+ const line = `touched-file quality: ${failed.source_command} exited ${String(failed.exit_code)} on ${file}${more}`;
314
+ return truncateToBytes(line, WARN_LINE_MAX_BYTES);
315
+ }
316
+
317
+ /** Cut `text` so its UTF-8 encoding is at most `max` bytes, never mid-character. */
318
+ export function truncateToBytes(text: string, max: number): string {
319
+ const buf = Buffer.from(text, 'utf-8');
320
+ if (buf.length <= max) return text;
321
+ let end = max;
322
+ // Back off into the start of a character: continuation bytes are 10xxxxxx.
323
+ while (end > 0 && (buf[end] ?? 0) >= 0x80 && (buf[end] ?? 0) < 0xc0) end -= 1;
324
+ return buf.subarray(0, end).toString('utf-8');
325
+ }
326
+
327
+ // ---------------------------------------------------------------------------
328
+ // Which files the turn touched
329
+ // ---------------------------------------------------------------------------
330
+
331
+ /** Where the touched-path set came from, recorded beside the runs. */
332
+ export type FileSource = 'minimal-safe-diff∩worktree' | 'none';
333
+
334
+ export interface TouchedFiles {
335
+ readonly files: readonly string[];
336
+ readonly source: FileSource;
337
+ }
338
+
339
+ /**
340
+ * The turn's touched files, intersected with what the working tree says changed.
341
+ *
342
+ * RISK 4 OF THE ROADMAP, AND THE REASON THIS IS NOT JUST A READ.
343
+ * `agents/state/minimal-safe-diff.json` is ONE FILE PER PROJECT ROOT, not per
344
+ * session — and under this repository's worktree workflow `CLAUDE_PROJECT_DIR`
345
+ * resolves to the parent checkout, so concurrent runs share it. Reading it
346
+ * straight would let a neighbouring session's edits become this session's
347
+ * quality target.
348
+ *
349
+ * Two independent narrowings, and both are needed:
350
+ *
351
+ * - OWNERSHIP. A state file whose `session_id` is not this session's is
352
+ * refused outright, which is the same stance `owns_session_state` takes on
353
+ * the verification record.
354
+ * - THE WORKING TREE. A path is kept only if git still reports it as changed.
355
+ * A path the recorder saw but the tree has since reverted is not a quality
356
+ * target, and an edit from a different checkout is not in THIS tree's status.
357
+ *
358
+ * An absent, foreign or unreadable recorder state yields `none` and NO files,
359
+ * which makes the shadow pass a no-op. The alternative — falling back to the
360
+ * whole working tree — is exactly the cross-session leak this is guarding, so
361
+ * the degradation is toward measuring nothing rather than toward measuring
362
+ * somebody else's work.
363
+ */
364
+ export function touchedFilesAtStop(
365
+ root: string,
366
+ session_id: string,
367
+ gitStatus: (root: string) => readonly string[] = defaultGitStatus,
368
+ ): TouchedFiles {
369
+ let recorded: string[];
370
+ try {
371
+ const raw = fs.readFileSync(
372
+ path.join(root, 'agents', 'state', 'minimal-safe-diff.json'),
373
+ 'utf-8',
374
+ );
375
+ const decoded: unknown = JSON.parse(raw);
376
+ if (typeof decoded !== 'object' || decoded === null || Array.isArray(decoded)) {
377
+ return { files: [], source: 'none' };
378
+ }
379
+ const state = decoded as Record<string, unknown>;
380
+ if (state['session_id'] !== session_id || session_id === '') {
381
+ return { files: [], source: 'none' };
382
+ }
383
+ const touched = state['files_touched_this_turn'];
384
+ if (!Array.isArray(touched)) return { files: [], source: 'none' };
385
+ recorded = touched.filter((x): x is string => typeof x === 'string');
386
+ } catch {
387
+ return { files: [], source: 'none' };
388
+ }
389
+
390
+ const changed = new Set(gitStatus(root));
391
+ const files = recorded.filter((f) => changed.has(normalizePath(f)));
392
+ return { files, source: 'minimal-safe-diff∩worktree' };
393
+ }
394
+
395
+ /** Repo-relative, forward slashes, no leading `./` — the shape git prints. */
396
+ export function normalizePath(p: string): string {
397
+ return p.replace(/\\/g, '/').replace(/^\.\//, '');
398
+ }
399
+
400
+ function defaultGitStatus(root: string): string[] {
401
+ const res = spawnSync('git', ['status', '--porcelain', '-z'], {
402
+ cwd: root,
403
+ encoding: 'utf-8',
404
+ timeout: 10_000,
405
+ shell: false,
406
+ maxBuffer: 4 * 1024 * 1024,
407
+ });
408
+ if (res.status !== 0 || typeof res.stdout !== 'string') return [];
409
+ return parsePorcelainZ(res.stdout);
410
+ }
411
+
412
+ /**
413
+ * Paths out of `git status --porcelain -z`.
414
+ *
415
+ * `-z` rather than the line-oriented form: without it git QUOTES a path holding
416
+ * a space or a non-ASCII byte, and the quoted form never matches a recorded
417
+ * path, so the intersection would silently drop exactly the files most likely
418
+ * to be mis-handled downstream. A rename entry carries two NUL-separated paths
419
+ * (`R <new>\0<old>`); the new path is the one that exists to be checked.
420
+ */
421
+ export function parsePorcelainZ(stdout: string): string[] {
422
+ const out: string[] = [];
423
+ const parts = stdout.split('\0');
424
+ for (let i = 0; i < parts.length; i += 1) {
425
+ const entry = parts[i];
426
+ if (entry === undefined || entry.length < 4) continue;
427
+ const status = entry.slice(0, 2);
428
+ out.push(normalizePath(entry.slice(3)));
429
+ // Rename / copy: the NEXT NUL-separated field is the ORIGIN path, which
430
+ // is consumed here so it is not read as a status entry of its own.
431
+ if (status.startsWith('R') || status.startsWith('C')) i += 1;
432
+ }
433
+ return out;
434
+ }
@@ -366,12 +366,44 @@ export function readInstallBoundary(): InstallBoundary {
366
366
  * would let another concern's retries inflate Q1's numerator against a
367
367
  * denominator that only counts this gate's refusals.
368
368
  */
369
+ /**
370
+ * Who set the `stop_hook_active` flag this row was recorded under.
371
+ *
372
+ * `layer` already says WHICH allow path produced the row, and the doc above
373
+ * says the host sets `stop_hook_active` on ANY stop hook's block — including a
374
+ * neighbour's. What it could not say is which: a `stop_hook_active` row and a
375
+ * `refused_turn` row were the only two things the record distinguished, so a
376
+ * retry caused by a foreign concern and a retry caused by this gate were
377
+ * indistinguishable once the layer was `stop_hook_active`.
378
+ *
379
+ * - `ours` — this session's refusal record exists and parses, so this gate
380
+ * refused at some point before this stop.
381
+ * - `foreign` — the record is ABSENT. This gate never refused in this session,
382
+ * and the host set the flag anyway, so something else did.
383
+ * - `unknown` — the record exists and could not be read. The distinction from
384
+ * `foreign` is the whole point: "we did not refuse" and "we
385
+ * cannot tell whether we refused" are different findings, and
386
+ * collapsing them would let an unreadable state directory read
387
+ * as evidence of a neighbour.
388
+ *
389
+ * On the `refused_turn` layer the answer is `ours` by construction — that
390
+ * marker is this gate's own and no other writer sets it.
391
+ */
392
+ export type ShadowSetBy = 'ours' | 'foreign' | 'unknown';
393
+
369
394
  export interface WouldRefuseAgainRow {
370
395
  detector: RefusalDetectorId;
371
396
  /** The turn ordinal the shadow ran on — joins to `refused_turn`. */
372
397
  turn: number;
373
398
  at: string;
374
399
  layer: ShadowLayer;
400
+ /**
401
+ * Absent on rows written before this field existed, which parse as
402
+ * `unknown` rather than being dropped — an old row is still a real
403
+ * observation of a retry, and discarding it would silently lower Q1's
404
+ * numerator.
405
+ */
406
+ set_by: ShadowSetBy;
375
407
  }
376
408
 
377
409
  /**
@@ -514,7 +546,12 @@ export function parseShadowRecord(raw: string): ShadowRecord | null {
514
546
  if (typeof turn !== 'number' || !Number.isFinite(turn)) continue;
515
547
  if (typeof at !== 'string') continue;
516
548
  if (layer !== 'stop_hook_active' && layer !== 'refused_turn') continue;
517
- rows.push({ detector: det, turn, at, layer });
549
+ const setByRaw = ro['set_by'];
550
+ const set_by: ShadowSetBy =
551
+ setByRaw === 'ours' || setByRaw === 'foreign' || setByRaw === 'unknown'
552
+ ? setByRaw
553
+ : 'unknown';
554
+ rows.push({ detector: det, turn, at, layer, set_by });
518
555
  }
519
556
  const num = (v: unknown): number =>
520
557
  typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : 0;
@@ -548,11 +585,24 @@ export function foldShadow(
548
585
  turnOrdinal: number;
549
586
  at: string;
550
587
  layer: ShadowLayer;
588
+ /**
589
+ * Omitted by a caller that cannot establish it, which is `unknown` —
590
+ * never `foreign`, because a missing answer is not evidence of a
591
+ * neighbour.
592
+ */
593
+ setBy?: ShadowSetBy;
551
594
  },
552
595
  ): ShadowRecord {
553
596
  const rows = [...(prev?.would_refuse_again ?? [])];
597
+ const set_by: ShadowSetBy = input.setBy ?? 'unknown';
554
598
  for (const d of input.detectors) {
555
- rows.push({ detector: d, turn: input.turnOrdinal, at: input.at, layer: input.layer });
599
+ rows.push({
600
+ detector: d,
601
+ turn: input.turnOrdinal,
602
+ at: input.at,
603
+ layer: input.layer,
604
+ set_by,
605
+ });
556
606
  }
557
607
  let dropped = prev?.dropped ?? 0;
558
608
  if (rows.length > SHADOW_MAX_ROWS) {
@@ -571,6 +621,31 @@ export function foldShadow(
571
621
  };
572
622
  }
573
623
 
624
+ /**
625
+ * Who set `stop_hook_active`, decided from this gate's OWN refusal record.
626
+ *
627
+ * Three states, and the third is not a hedge. `ENOENT` means this gate never
628
+ * refused in this session, so a flag the host set anyway came from somewhere
629
+ * else — `foreign`. Any OTHER read failure means the answer is unavailable, and
630
+ * reporting that as `foreign` would turn a broken state directory into evidence
631
+ * that a neighbour acted. The lane's own framing is the reason: what this suite
632
+ * can say about a neighbour is only what it observes, and an unreadable file is
633
+ * not an observation.
634
+ *
635
+ * `parseRecord` returning `null` on a present file is also `unknown`: the file
636
+ * exists, so something wrote it, and we cannot say what.
637
+ */
638
+ export function shadowSetBy(workspaceRoot: string, sessionKey: string): ShadowSetBy {
639
+ let raw: string;
640
+ try {
641
+ raw = fs.readFileSync(sessionRefusalFile(workspaceRoot, sessionKey), 'utf-8');
642
+ } catch (err) {
643
+ const code = (err as NodeJS.ErrnoException | undefined)?.code;
644
+ return code === 'ENOENT' ? 'foreign' : 'unknown';
645
+ }
646
+ return parseRecord(raw) === null ? 'unknown' : 'ours';
647
+ }
648
+
574
649
  /** This session's shadow record, or `null` when it has never retried. */
575
650
  export function readShadowRecord(workspaceRoot: string, sessionKey: string): ShadowRecord | null {
576
651
  try {
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The windsurf single-file surface, rendered — one renderer, two callers.
3
+ *
4
+ * `road-to-a-hook-bundle-with-one-yaml-reader` 3.1. `condense.generate_windsurfrules`
5
+ * WRITES `.windsurfrules`; the standing-payload census needs the same bytes to
6
+ * report windsurf's figure without reading that file, because it is an untracked
7
+ * generated projection and reading it published a number that tracked when the
8
+ * checkout last ran `task generate-tools` rather than anything about the tree.
9
+ *
10
+ * Both callers share THIS function. A second renderer living in the census would
11
+ * have measured a file nobody receives, which is Risk 3 of that roadmap.
12
+ *
13
+ * WHY A SEPARATE MODULE rather than an export from `condense.ts`: that file sits
14
+ * 1,228 lines over the 1,500-line cap `check_source_size_budget` ratchets, so
15
+ * every line added there is a new violation. Splitting the renderer out moves
16
+ * `condense.ts` DOWN. The file is small on purpose and carries only what the
17
+ * render needs.
18
+ *
19
+ * `strip_frontmatter` moved here with it — it is the render's one non-trivial
20
+ * dependency, it has exactly one caller left inside `condense.ts`, and leaving it
21
+ * behind would have meant either an import cycle or a second copy.
22
+ * `condense.ts` re-exports it, so `src/install/emit_host_rules_cli.ts` and every
23
+ * other importer keeps its path.
24
+ */
25
+ import * as fs from 'node:fs';
26
+ import * as path from 'node:path';
27
+
28
+ /** Python `str.lstrip("\n")` — strip only leading newlines. Mirrors `condense._lstripNewlines`. */
29
+ function lstripNewlines(s: string): string {
30
+ return s.replace(/^\n+/, '');
31
+ }
32
+
33
+ /** Python `str.strip()` with no args — strip leading/trailing Unicode whitespace. Mirrors `condense._strip`. */
34
+ function strip(s: string): string {
35
+ return s.replace(/^\s+/u, '').replace(/\s+$/u, '');
36
+ }
37
+
38
+ /** Drop a leading `---` frontmatter block, if the content opens with one. */
39
+ export function strip_frontmatter(content: string): string {
40
+ if (content.startsWith('---')) {
41
+ const end = content.indexOf('---', 3);
42
+ if (end !== -1) {
43
+ content = lstripNewlines(content.slice(end + 3));
44
+ }
45
+ }
46
+ return content;
47
+ }
48
+
49
+ /**
50
+ * The exact bytes `generate_windsurfrules` writes, for a given rule set.
51
+ *
52
+ * `rulesSource` is a parameter rather than `condense.MODULE_STATE.RULES_SOURCE`
53
+ * so a caller can render a tree it names — the census renders the projection
54
+ * source directly, with no module state to set up.
55
+ */
56
+ export function render_windsurfrules(rulesSource: string, rules: readonly string[]): string {
57
+ const parts = ['# Auto-generated from dist/agent-src/rules/ — do not edit directly\n'];
58
+ for (const rule of rules) {
59
+ const content = strip_frontmatter(fs.readFileSync(path.join(rulesSource, rule), 'utf-8'));
60
+ parts.push(`---\n\n${strip(content)}\n`);
61
+ }
62
+ return parts.join('\n') + '\n';
63
+ }
@@ -29,15 +29,23 @@ import * as path from 'node:path';
29
29
  export const CLI_CALLS_ATTRIBUTION_SUFFIX = '.attribution.json';
30
30
 
31
31
  /**
32
- * Who booked a call. Enumerated because the consumer set is closed: exactly two
33
- * files in the tree construct a `CliClient`.
32
+ * Who booked a call. Enumerated because the consumer set is closed: exactly
33
+ * three files in the tree construct a `CliClient`.
34
34
  *
35
- * `unknown` is a FINDING when it appears — it means a third booking path exists
36
- * that this enumeration does not know about. That is how the test suite was
37
- * caught spending the operator's live budget.
35
+ * `unknown` is a FINDING when it appears — it means a booking path exists that
36
+ * this enumeration does not know about. That is how the test suite was caught
37
+ * spending the operator's live budget.
38
38
  */
39
39
  export const CLI_CONSUMER_COUNCIL = 'council';
40
40
  export const CLI_CONSUMER_TEAM = 'team';
41
+ /**
42
+ * The dogfooded self-review gate. Third because `self_review_gate.ts` is
43
+ * CLI-first since this constant was added: the subscription transport is tried
44
+ * before the API one, so a local or self-hosted run books here rather than
45
+ * spending API credit. A hosted CI runner has no vendor CLI on PATH and falls
46
+ * through to the API transport, which books nothing against this counter.
47
+ */
48
+ export const CLI_CONSUMER_SELF_REVIEW = 'self-review';
41
49
  export const CLI_CONSUMER_UNKNOWN = 'unknown';
42
50
 
43
51
  /**