@event4u/agent-config 16.0.0 → 16.2.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 (382) 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 +289 -140
  5. package/CONTRIBUTING.md +9 -0
  6. package/README.md +2 -2
  7. package/dist/agent-src/commands/analyze/inbox.md +26 -0
  8. package/dist/agent-src/commands/analyze/roadmap-repos.md +27 -0
  9. package/dist/agent-src/commands/challenge-me/closure.md +178 -0
  10. package/dist/agent-src/commands/challenge-me.md +9 -2
  11. package/dist/agent-src/commands/council/default.md +25 -5
  12. package/dist/agent-src/commands/feature/plan.md +28 -1
  13. package/dist/agent-src/commands/feature/roadmap.md +28 -1
  14. package/dist/agent-src/commands/implement-ticket.md +27 -0
  15. package/dist/agent-src/commands/jira-ticket.md +27 -0
  16. package/dist/agent-src/commands/pr/create.md +29 -0
  17. package/dist/agent-src/commands/pr/merge.md +60 -9
  18. package/dist/agent-src/commands/roadmap/ai-council.md +7 -1
  19. package/dist/agent-src/commands/roadmap/create.md +74 -27
  20. package/dist/agent-src/commands/roadmap/materialize.md +27 -0
  21. package/dist/agent-src/commands/roadmap/process-full.md +123 -18
  22. package/dist/agent-src/commands/team.md +22 -0
  23. package/dist/agent-src/commands/work.md +4 -0
  24. package/dist/agent-src/contexts/authority/scope-growth-ownership.md +80 -0
  25. package/dist/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +47 -0
  26. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +44 -3
  27. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +19 -23
  28. package/dist/agent-src/contexts/execution/decision-closure-execution.md +93 -0
  29. package/dist/agent-src/contexts/execution/fix-loop-ladder.md +108 -0
  30. package/dist/agent-src/contexts/execution/plan-confidence-gate.md +4 -2
  31. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +75 -6
  32. package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
  33. package/dist/agent-src/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
  34. package/dist/agent-src/guidelines/design-fidelity-routing.md +29 -0
  35. package/dist/agent-src/rules/autonomous-execution.md +15 -8
  36. package/dist/agent-src/rules/delegation-policy.md +23 -45
  37. package/dist/agent-src/rules/design-fidelity.md +32 -37
  38. package/dist/agent-src/rules/evaluator-independence.md +43 -49
  39. package/dist/agent-src/rules/test-first.md +41 -0
  40. package/dist/agent-src/rules/ui-audit-gate.md +1 -2
  41. package/dist/agent-src/skills/accessibility-auditor/data/aria-patterns.csv +1 -1
  42. package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
  43. package/dist/agent-src/skills/ai-council/SKILL.md +11 -3
  44. package/dist/agent-src/skills/api-design/data/manifest.json +3 -3
  45. package/dist/agent-src/skills/database/data/manifest.json +4 -4
  46. package/dist/agent-src/skills/database/data/query-tuning.csv +3 -3
  47. package/dist/agent-src/skills/design-review/SKILL.md +80 -4
  48. package/dist/agent-src/skills/design-review/references/verification-automation.md +32 -0
  49. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +30 -5
  50. package/dist/agent-src/skills/existing-ui-audit/references/component-taxonomy-detection.md +56 -0
  51. package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
  52. package/dist/agent-src/skills/markitdown/SKILL.md +4 -0
  53. package/dist/agent-src/skills/playwright-testing/SKILL.md +30 -6
  54. package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
  55. package/dist/agent-src/skills/storybook-workshop/SKILL.md +42 -3
  56. package/dist/agent-src/skills/threat-modeling/SKILL.md +9 -4
  57. package/dist/agent-src/skills/threat-modeling/data/manifest.json +4 -4
  58. package/dist/agent-src/skills/ui-component-architect/SKILL.md +23 -0
  59. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +37 -3
  60. package/dist/agent-src/templates/agent-settings.md +1 -1
  61. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  62. package/dist/agent-src/templates/roadmaps.md +218 -0
  63. package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
  64. package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
  65. package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
  66. package/dist/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
  67. package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
  68. package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
  69. package/dist/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
  70. package/dist/agent-src/templates/scripts/work_engine/state.ts +57 -7
  71. package/dist/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
  72. package/dist/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
  73. package/dist/cli/python/knowledge_ingest.js +1 -1
  74. package/dist/cli/python/workspace_drive.js +1 -1
  75. package/dist/cli-delegate/check_update_banner.js +40 -2
  76. package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
  77. package/dist/cli-delegate/{chunk-S3VFXMTM.js → chunk-4XFP2Y3E.js} +1 -1
  78. package/dist/cli-delegate/chunk-5R7HOLAF.js +99 -0
  79. package/dist/cli-delegate/{chunk-IVBPRCQO.js → chunk-6KFEC3G7.js} +1 -1
  80. package/dist/cli-delegate/{chunk-2TAQ2K4V.js → chunk-6KM6ORRY.js} +55 -386
  81. package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-7XGBX6ZH.js} +973 -555
  82. package/dist/cli-delegate/{chunk-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
  83. package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-FP36SWJD.js} +587 -141
  84. package/dist/cli-delegate/{chunk-3FKR64R5.js → chunk-IGD6ERX6.js} +62 -7
  85. package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-JNMVLWLR.js} +99 -14
  86. package/dist/cli-delegate/{chunk-H32DGUJH.js → chunk-P6ZHP5W2.js} +1 -1
  87. package/dist/cli-delegate/{chunk-57PN4GYJ.js → chunk-RGNIWTEA.js} +1 -1
  88. package/dist/cli-delegate/chunk-ROYYTEZU.js +39 -0
  89. package/dist/cli-delegate/{chunk-2DK2VJWG.js → chunk-WGXTVKCQ.js} +20 -6
  90. package/dist/cli-delegate/chunk-YDPILCOL.js +152 -0
  91. package/dist/cli-delegate/{chunk-QJWAUMJW.js → chunk-YJ72UIAK.js} +730 -15
  92. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  93. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  94. package/dist/cli-delegate/cmd_conformance.js +985 -831
  95. package/dist/cli-delegate/cmd_doctor.js +7 -5
  96. package/dist/cli-delegate/cmd_explain.js +2 -2
  97. package/dist/cli-delegate/cmd_export.js +9 -7
  98. package/dist/cli-delegate/cmd_fleet.js +4 -2
  99. package/dist/cli-delegate/cmd_handoff.js +7 -7
  100. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  101. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  102. package/dist/cli-delegate/cmd_migrate.js +2 -2
  103. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  104. package/dist/cli-delegate/cmd_preflight.js +4 -2
  105. package/dist/cli-delegate/cmd_prune.js +10 -8
  106. package/dist/cli-delegate/cmd_route_audit.js +3 -3
  107. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  108. package/dist/cli-delegate/cmd_session_recycle.js +4 -4
  109. package/dist/cli-delegate/cmd_settings_get.js +28 -2
  110. package/dist/cli-delegate/cmd_settings_migrate.js +9 -7
  111. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  112. package/dist/cli-delegate/cmd_sync.js +10 -8
  113. package/dist/cli-delegate/cmd_uninstall.js +16 -13
  114. package/dist/cli-delegate/cmd_update.js +1 -1
  115. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  116. package/dist/cli-delegate/cmd_validate.js +12 -10
  117. package/dist/cli-delegate/cmd_versions.js +1 -1
  118. package/dist/discovery/deprecation-report.md +1 -1
  119. package/dist/discovery/discovery-manifest.json +90 -43
  120. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  121. package/dist/discovery/discovery-manifest.summary.md +4 -4
  122. package/dist/discovery/dormancy-report.md +1 -1
  123. package/dist/discovery/orphan-report.md +1 -1
  124. package/dist/discovery/packs.json +10 -8
  125. package/dist/discovery/trust-report.md +3 -3
  126. package/dist/discovery/workspaces.json +8 -6
  127. package/dist/hooks/dispatch.js +7682 -2772
  128. package/dist/hooks/dispatch.sha256 +1 -0
  129. package/dist/install/claudeRuleRewrite.js +14 -3
  130. package/dist/install/conflict.js +60 -10
  131. package/dist/install/conflictTracker.js +218 -0
  132. package/dist/install/fsPrimitives.js +54 -0
  133. package/dist/install/install.mjs +2200 -1559
  134. package/dist/install/manifestFiles.js +259 -0
  135. package/dist/install/preflight.js +14 -2
  136. package/dist/install/preserve.js +157 -0
  137. package/dist/install/recordedOwnership.js +132 -0
  138. package/dist/install/txlog.js +11 -2
  139. package/dist/mcp/registry-manifest.json +3 -3
  140. package/dist/mcp/server.json +2 -2
  141. package/dist/mcp/server.mjs +3066 -1470
  142. package/dist/router.json +1 -1
  143. package/dist/scripts/_lib/agent_settings.js +3 -2
  144. package/dist/scripts/_lib/fs_atomic.js +167 -0
  145. package/dist/scripts/_lib/installed_tools.js +460 -0
  146. package/dist/scripts/_lib/lean_projection_mode.js +3 -2
  147. package/dist/scripts/_lib/settings_renamed_keys.js +78 -0
  148. package/dist/server/routes/install.js +8 -0
  149. package/dist/server/routes/wizard.js +9 -3
  150. package/dist/server/schemas/settings.js +40 -5
  151. package/dist/shared/missionExecution.js +97 -0
  152. package/dist/ui/assets/index-Cwq4d0hX.js +23 -0
  153. package/dist/ui/assets/index-Cyjy8bWO.css +1 -0
  154. package/dist/ui/index.html +2 -2
  155. package/docs/MIGRATION.md +103 -0
  156. package/docs/contracts/settings-classes.md +28 -7
  157. package/docs/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
  158. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
  159. package/docs/guidelines/design-fidelity-routing.md +29 -0
  160. package/package.json +4 -4
  161. package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
  162. package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
  163. package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
  164. package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
  165. package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
  166. package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
  167. package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
  168. package/src/agent-src/templates/scripts/work_engine/state.ts +57 -7
  169. package/src/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
  170. package/src/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
  171. package/src/config/agent-settings.template.yml +168 -27
  172. package/src/config/auto-rule-budget.json +16 -0
  173. package/src/config/ci-local-parity.yml +36 -5
  174. package/src/config/continuity-surface.json +23 -2
  175. package/src/config/gate-coverage.yml +377 -13
  176. package/src/config/gate-violation-baselines.json +10 -6
  177. package/src/config/hook-latency-budget.json +21 -0
  178. package/src/config/hook-token-budget.json +4 -2
  179. package/src/config/loop-surfaces.yaml +215 -0
  180. package/src/config/metric-registry.yml +114 -0
  181. package/src/config/placeholder-drift-budget.json +10 -0
  182. package/src/config/platform-anchor.json +3 -2
  183. package/src/config/release-gate-locality.yml +11 -0
  184. package/src/config/routing-coverage-seed.json +33 -5
  185. package/src/config/rule-enforcement-baseline.json +0 -1
  186. package/src/install/claudeRuleRewrite.ts +16 -2
  187. package/src/install/conflict.ts +65 -9
  188. package/src/install/conflictTracker.ts +299 -0
  189. package/src/install/fsPrimitives.ts +54 -0
  190. package/src/install/manifestFiles.ts +312 -0
  191. package/src/install/preflight.ts +16 -2
  192. package/src/install/preserve.ts +205 -0
  193. package/src/install/recordedOwnership.ts +149 -0
  194. package/src/install/txlog.ts +11 -2
  195. package/src/install/types.ts +19 -4
  196. package/src/scripts/_cli/cmd_conformance.ts +88 -15
  197. package/src/scripts/_cli/cmd_doctor.ts +7 -20
  198. package/src/scripts/_cli/cmd_route_explain.ts +9 -2
  199. package/src/scripts/_cli/cmd_settings_get.ts +44 -0
  200. package/src/scripts/_cli/doctor_execution.ts +173 -0
  201. package/src/scripts/_cli/doctor_network_posture.ts +226 -0
  202. package/src/scripts/_lib/activation_payload.ts +247 -0
  203. package/src/scripts/_lib/agent_settings.ts +4 -2
  204. package/src/scripts/_lib/authority_path.ts +102 -0
  205. package/src/scripts/_lib/body_portable.ts +102 -0
  206. package/src/scripts/_lib/cascade_base.ts +94 -0
  207. package/src/scripts/_lib/cc_transcript.ts +132 -1
  208. package/src/scripts/_lib/changelog_eras.ts +7 -2
  209. package/src/scripts/_lib/concern_sla_window.ts +200 -0
  210. package/src/scripts/_lib/continuation_ladder.ts +149 -2
  211. package/src/scripts/_lib/council_transport.ts +129 -0
  212. package/src/scripts/_lib/delivery_ready.ts +112 -0
  213. package/src/scripts/_lib/forge_protection.ts +381 -0
  214. package/src/scripts/_lib/forge_reader.ts +558 -0
  215. package/src/scripts/_lib/hook_platform_keys.ts +27 -0
  216. package/src/scripts/_lib/host_env_write.ts +111 -0
  217. package/src/scripts/_lib/json_python_parity.ts +127 -0
  218. package/src/scripts/_lib/lean_projection_mode.ts +14 -3
  219. package/src/scripts/_lib/llm_proposer_transport.ts +14 -4
  220. package/src/scripts/_lib/loop_surfaces.ts +382 -0
  221. package/src/scripts/_lib/mission_record.ts +190 -0
  222. package/src/scripts/_lib/obligation_frequency.ts +217 -0
  223. package/src/scripts/_lib/obligations.ts +513 -0
  224. package/src/scripts/_lib/orchestration_record.ts +11 -20
  225. package/src/scripts/_lib/probe_inputs.ts +271 -0
  226. package/src/scripts/_lib/read_surface_scan.ts +322 -0
  227. package/src/scripts/_lib/release_highlights.ts +201 -11
  228. package/src/scripts/_lib/release_holds.ts +493 -0
  229. package/src/scripts/_lib/release_migration_gate.ts +184 -0
  230. package/src/scripts/_lib/retrieval_sanitize.ts +77 -16
  231. package/src/scripts/_lib/rides_along.ts +102 -0
  232. package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
  233. package/src/scripts/_lib/shell_write_shapes.ts +108 -0
  234. package/src/scripts/_lib/structural_hiding.ts +622 -0
  235. package/src/scripts/_lib/test_delta.ts +166 -0
  236. package/src/scripts/_lib/test_provenance.ts +171 -0
  237. package/src/scripts/_lib/turn_end_refusals.ts +525 -4
  238. package/src/scripts/_lib/turn_end_transcript.ts +216 -0
  239. package/src/scripts/_lib/typed_op_grant.ts +246 -0
  240. package/src/scripts/_lib/typed_op_watch.ts +185 -0
  241. package/src/scripts/_lib/verification_command.ts +324 -0
  242. package/src/scripts/_lib/verification_evidence.ts +647 -0
  243. package/src/scripts/_lib/verify_clause.ts +280 -0
  244. package/src/scripts/ai_council/config.ts +8 -11
  245. package/src/scripts/ai_council/fallback_config.ts +5 -3
  246. package/src/scripts/ai_council/ownership_classes.ts +118 -0
  247. package/src/scripts/ai_team/team_dispatch.ts +4 -0
  248. package/src/scripts/ask_block_census.ts +285 -15
  249. package/src/scripts/before_complete_hook.ts +340 -23
  250. package/src/scripts/bench_hook_latency.ts +412 -5
  251. package/src/scripts/cache_realization_report.ts +61 -1
  252. package/src/scripts/check_argument_hint.ts +287 -0
  253. package/src/scripts/check_claims.ts +125 -1
  254. package/src/scripts/check_enforcement_coverage.ts +38 -9
  255. package/src/scripts/check_enforcement_matrix.ts +866 -0
  256. package/src/scripts/check_finding_dispositions.ts +22 -3
  257. package/src/scripts/check_gate_reachability.ts +173 -10
  258. package/src/scripts/check_generator_sync.ts +1025 -0
  259. package/src/scripts/check_host_docs_digest.ts +786 -0
  260. package/src/scripts/check_host_format_column.ts +559 -0
  261. package/src/scripts/check_input_references.ts +228 -0
  262. package/src/scripts/check_inputs_parity.ts +235 -0
  263. package/src/scripts/check_kernel_edit_ratified.ts +47 -5
  264. package/src/scripts/check_kill_switch_table.ts +294 -0
  265. package/src/scripts/check_placeholder_drift.ts +221 -0
  266. package/src/scripts/check_preamble_payload_budget.ts +75 -2
  267. package/src/scripts/check_read_surface_coverage.ts +515 -0
  268. package/src/scripts/check_release_highlights.ts +33 -1
  269. package/src/scripts/check_release_holds.ts +568 -0
  270. package/src/scripts/check_routing_coverage.ts +378 -11
  271. package/src/scripts/check_rule_projection_integrity.ts +22 -12
  272. package/src/scripts/check_static_parity.ts +17 -0
  273. package/src/scripts/check_test_delta.ts +294 -0
  274. package/src/scripts/check_test_weakening.ts +299 -0
  275. package/src/scripts/ci_settle.ts +145 -19
  276. package/src/scripts/closure_scan.ts +447 -0
  277. package/src/scripts/compile_hook_manifest.ts +102 -18
  278. package/src/scripts/council_record_shape.ts +209 -0
  279. package/src/scripts/external_sources_denylist.json +29 -4
  280. package/src/scripts/hook_manifest.json +1 -1
  281. package/src/scripts/hook_manifest.yaml +127 -11
  282. package/src/scripts/hooks/block_config_weakening.ts +229 -4
  283. package/src/scripts/hooks/block_kernel_rule_writes.ts +9 -78
  284. package/src/scripts/hooks/block_plumbing_writes.ts +314 -0
  285. package/src/scripts/hooks/bundle_integrity.ts +291 -0
  286. package/src/scripts/hooks/chain_nudge_hook.ts +1 -2
  287. package/src/scripts/hooks/code_graph_context_hook.ts +1 -2
  288. package/src/scripts/hooks/comment_discipline_hook.ts +1 -3
  289. package/src/scripts/hooks/concern_registry.ts +6 -0
  290. package/src/scripts/hooks/concern_timings.ts +94 -0
  291. package/src/scripts/hooks/delegation_nudge_hook.ts +1 -2
  292. package/src/scripts/hooks/design_pass_hook.ts +221 -5
  293. package/src/scripts/hooks/design_slop_hook.ts +1 -2
  294. package/src/scripts/hooks/dispatch_hook.ts +25 -19
  295. package/src/scripts/hooks/edit_shape_hook.ts +1 -2
  296. package/src/scripts/hooks/end_review_nudge_hook.ts +1 -1
  297. package/src/scripts/hooks/evidence_independence.ts +97 -2
  298. package/src/scripts/hooks/exit_codes.ts +128 -0
  299. package/src/scripts/hooks/host_lowering.json +1 -0
  300. package/src/scripts/hooks/host_lowering.ts +128 -3
  301. package/src/scripts/hooks/host_lowering.yaml +262 -46
  302. package/src/scripts/hooks/host_semantics.ts +30 -2
  303. package/src/scripts/hooks/injection_budget.ts +9 -2
  304. package/src/scripts/hooks/interruption_ledger_hook.ts +1 -2
  305. package/src/scripts/hooks/journal_record_hook.ts +1 -2
  306. package/src/scripts/hooks/memory_recall_hook.ts +539 -0
  307. package/src/scripts/hooks/obligation_settle_hook.ts +328 -0
  308. package/src/scripts/hooks/one_question_per_ask_hook.ts +25 -4
  309. package/src/scripts/hooks/orchestration_record_hook.ts +1 -2
  310. package/src/scripts/hooks/permission_gate.ts +1 -2
  311. package/src/scripts/hooks/push_settle_hook.ts +1 -3
  312. package/src/scripts/hooks/replay_hook.ts +2 -1
  313. package/src/scripts/hooks/reread_guard_hook.ts +1 -2
  314. package/src/scripts/hooks/review_baseline_hook.ts +1 -2
  315. package/src/scripts/hooks/rtk_wrap_hook.ts +1 -2
  316. package/src/scripts/hooks/rule_inject_hook.ts +55 -4
  317. package/src/scripts/hooks/run_continuation_hook.ts +30 -35
  318. package/src/scripts/hooks/skill_route_hook.ts +1 -2
  319. package/src/scripts/hooks/source_first_gate_hook.ts +1 -2
  320. package/src/scripts/hooks/spawn_guard_shadow_hook.ts +1 -2
  321. package/src/scripts/hooks/subagent_ledger_hook.ts +1 -2
  322. package/src/scripts/hooks/suggestion_capture_hook.ts +1 -1
  323. package/src/scripts/hooks/table_fingerprint.ts +49 -0
  324. package/src/scripts/hooks/telemetry_flush_hook.ts +1 -2
  325. package/src/scripts/hooks/telemetry_usage_hook.ts +1 -2
  326. package/src/scripts/hooks/tool_result_bytes_hook.ts +93 -9
  327. package/src/scripts/hooks/turn_end_gate_hook.ts +337 -324
  328. package/src/scripts/hooks/ui_route_nudge_hook.ts +1 -2
  329. package/src/scripts/hooks_doctor.ts +85 -0
  330. package/src/scripts/hooks_status.ts +19 -0
  331. package/src/scripts/install.ts +109 -210
  332. package/src/scripts/janitor.ts +7 -0
  333. package/src/scripts/lint_decision_classes.ts +464 -0
  334. package/src/scripts/lint_deny_text.ts +419 -0
  335. package/src/scripts/lint_exit_codes.ts +262 -0
  336. package/src/scripts/lint_hook_manifest.ts +66 -0
  337. package/src/scripts/lint_major_migration_sections.ts +450 -0
  338. package/src/scripts/lint_plan_risk_register.ts +47 -3
  339. package/src/scripts/lint_roadmap_blockers.ts +139 -12
  340. package/src/scripts/lint_roadmap_producers.ts +202 -0
  341. package/src/scripts/lint_skill_descriptions.ts +1 -1
  342. package/src/scripts/lint_skill_scripts_readonly.ts +1 -1
  343. package/src/scripts/mcp_server/prompts.ts +142 -1
  344. package/src/scripts/measure_rule_budget.ts +101 -5
  345. package/src/scripts/measure_skill_ranker_baseline.ts +305 -51
  346. package/src/scripts/measure_turn_end_gate.ts +193 -0
  347. package/src/scripts/memory_lookup.ts +1 -1
  348. package/src/scripts/new_roadmap.ts +37 -0
  349. package/src/scripts/orchestration_record.ts +0 -5
  350. package/src/scripts/prepack-check.mjs +53 -0
  351. package/src/scripts/release.ts +14 -98
  352. package/src/scripts/release_drill.ts +267 -1
  353. package/src/scripts/release_publication.ts +64 -1
  354. package/src/scripts/report_enforcement_drift.ts +566 -0
  355. package/src/scripts/report_host_injection_effect.ts +3 -11
  356. package/src/scripts/report_invocation_surface.ts +383 -0
  357. package/src/scripts/report_obligation_carriers.ts +699 -0
  358. package/src/scripts/report_obligation_writer_split.ts +236 -0
  359. package/src/scripts/report_skill_menu_census.ts +208 -0
  360. package/src/scripts/report_standing_payload_by_host.ts +26 -152
  361. package/src/scripts/roadmap_verify_share.ts +213 -0
  362. package/src/scripts/schemas/agent-settings.schema.json +23 -0
  363. package/src/scripts/schemas/command.schema.json +48 -0
  364. package/src/scripts/schemas/review-findings.schema.json +4 -0
  365. package/src/scripts/schemas/skill.schema.json +44 -0
  366. package/src/scripts/scope_growth.ts +219 -0
  367. package/src/scripts/self_review_gate.ts +359 -10
  368. package/src/scripts/semantic_parity_census.ts +314 -0
  369. package/src/scripts/session_eol_report.ts +54 -1
  370. package/src/scripts/sweep_dead_scan_roots.ts +1 -1
  371. package/src/scripts/trigger_eval_grandfather.json +0 -3
  372. package/src/scripts/trigger_eval_rotation.ts +122 -27
  373. package/src/scripts/ui_conformance_probe.ts +774 -0
  374. package/src/scripts/update_prices.ts +85 -3
  375. package/src/scripts/write_bundle_digest.mjs +74 -0
  376. package/src/server/schemas/settings.ts +62 -7
  377. package/src/shared/missionExecution.ts +126 -0
  378. package/src/templates/consumer-settings/ONBOARDING.md +1 -0
  379. package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
  380. package/dist/ui/assets/index-D0jaCRPM.js +0 -29
  381. package/dist/ui/assets/index-jSfMW8_t.css +0 -1
  382. /package/dist/cli-delegate/{chunk-YUM5KURG.js → chunk-7RQP4UDH.js} +0 -0
@@ -32,11 +32,11 @@
32
32
  *
33
33
  * TWO of the five are CONDITIONAL, and saying "unconditional" here would be the
34
34
  * same stale-header defect this block corrects below. A and D are the
35
- * completion-adjacent pair an open subagent dispatch excuses, so `main()` runs
36
- * them only when `dispatchOpen` is false; B, C and E run on every turn-end. That
37
- * narrowing is deliberate (Phase 3 Step 2, narrowed again by R2 round 2) and is
38
- * the third allow path, alongside the two re-entrancy layers — see the
39
- * per-detector list in `main()`.
35
+ * completion-adjacent pair an open subagent dispatch excuses, so `runDetectors()`
36
+ * runs them only when `dispatchOpen` is false; B, C and E run on every turn-end.
37
+ * That narrowing is deliberate (Phase 3 Step 2, narrowed again by R2 round 2) and
38
+ * is the third allow path, alongside the two re-entrancy layers — see the
39
+ * per-detector list in `runDetectors()`.
40
40
  *
41
41
  * This block said "Three" and listed A/B/C until 2026-08-18, while `DetectorId`
42
42
  * below has carried four since round 7. `DETECTOR_IDS` in
@@ -154,7 +154,6 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
154
154
  import {
155
155
  classify,
156
156
  hasStableSessionId,
157
- isSyntheticPrompt,
158
157
  statePathFor as languageStatePathFor,
159
158
  type Verdict,
160
159
  } from '../language_mirror_hook.js';
@@ -163,7 +162,15 @@ import {
163
162
  // write" is only true while the two agree, and importing a constant made that
164
163
  // agreement invisible when the producer's layout moved. A builder makes the move
165
164
  // a type error.
166
- import { statePathFor as ciStatePathFor } from '../before_complete_hook.js';
165
+ import {
166
+ statePathFor as ciStatePathFor,
167
+ readTurnRunState,
168
+ type TurnRunState,
169
+ } from '../before_complete_hook.js';
170
+ // Re-exported: two test files address `readTurnRunState` at this path, and a
171
+ // reader arriving at detector C should find the reader where the detector uses
172
+ // it. The DEFINITION lives with the producer of the shape it reads.
173
+ export { readTurnRunState };
167
174
  // The spec-backed options-block reading, imported rather than re-derived:
168
175
  // `user-interaction` Iron Law 1's definition of an ask — a block PLUS its
169
176
  // recommendation line — lives in exactly one place and detector E reads it from
@@ -179,9 +186,18 @@ import { statePathFor as ciStatePathFor } from '../before_complete_hook.js';
179
186
  // Detector E, extracted to `_lib` where its prose costs no ratchet debt.
180
187
  // Re-exported so every existing importer of this module is unchanged.
181
188
  import { detectDroppedDecision } from '../_lib/dropped_decision.js';
189
+ import { isVerificationCommand } from '../_lib/verification_command.js';
190
+ // Round 8 — the record path detector C prefers over its own regex. The regex
191
+ // reads a command's TEXT and `echo test` matches it; these read what the run
192
+ // exited with and what it printed.
193
+ import {
194
+ NO_RED_EVIDENCE_REASON,
195
+ describeRecordFinding as recordReason,
196
+ hasRedThenGreen,
197
+ readRunEvidence,
198
+ } from '../_lib/verification_evidence.js';
182
199
 
183
200
  export { detectDroppedDecision };
184
- import { isSafeTranscriptPath } from './end_review_nudge_hook.js';
185
201
  import { unwrap, type JsonObject, type JsonValue } from './envelope.js';
186
202
  import { readHookStdin } from './hook_stdin.js';
187
203
  import {
@@ -193,18 +209,23 @@ import { openRecordStats } from './subagent_ledger_hook.js';
193
209
  import {
194
210
  deriveSessionKey,
195
211
  foldRefusal,
212
+ foldShadow,
196
213
  parseRecord,
214
+ parseShadowRecord,
197
215
  readInstallBoundary,
198
216
  sessionRefusalFile,
217
+ sessionShadowFile,
199
218
  type RefusalRecord,
219
+ type ShadowLayer,
220
+ type ShadowRecord,
200
221
  } from '../_lib/turn_end_refusals.js';
222
+ import { EXIT_ALLOW, EXIT_BLOCK } from './exit_codes.js';
201
223
 
202
224
  /** Dispatcher-internal block code. Pinned to 1 by `concern_block_exit_parity`. */
203
- const EXIT_BLOCK = 1;
204
- const EXIT_ALLOW = 0;
205
225
 
206
226
  /**
207
- * Transcript-read ceiling for this hook, passed at the call site in `main()`.
227
+ * Transcript-read ceiling for this hook, passed at its one production call
228
+ * site, `assembleDetectorInputs()`.
208
229
  * Deliberately well under `isSafeTranscriptPath`'s own 50 MB refusal: the
209
230
  * ordinal is a count over every entry, so the whole file is walked once per
210
231
  * turn-end, and a session file grows for the life of the session. Past this the
@@ -227,6 +248,18 @@ export interface Finding {
227
248
  /** The span that triggered it — quoted back so the refusal is actionable. */
228
249
  evidence: string;
229
250
  reason: string;
251
+ /**
252
+ * Which evidence path produced this finding, where a detector has two.
253
+ *
254
+ * Detectors C and F set it — the sentence here read "Only detector C" while
255
+ * F set it two functions below, which an independent review caught.
256
+ * `record` means a persisted run record was read;
257
+ * `transcript` means the recorder left nothing for this turn and the
258
+ * command-text scan answered instead — the mode a replay through
259
+ * `measure_turn_end_gate` / `check_detector_corpus` always runs in, so a
260
+ * corpus reading stays reproducible after the record path landed.
261
+ */
262
+ mode?: 'record' | 'transcript';
230
263
  }
231
264
 
232
265
  // ---------------------------------------------------------------------------
@@ -487,6 +520,7 @@ export function readCiSettled(
487
520
  return { seen: false, settled: false };
488
521
  }
489
522
 
523
+
490
524
  /**
491
525
  * A completion claim in the delivered reply. Deliberately narrow: the German and
492
526
  * English closings the corpus actually produced, anchored to a line so a mid-reply
@@ -656,62 +690,14 @@ export function detectLanguage(reply: string, pinned: Verdict): Finding | null {
656
690
  const _EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
657
691
 
658
692
  /**
659
- * Shell commands that count as verification.
660
- *
661
- * A positive list, and deliberately narrow. The alternative — treating any
662
- * `Bash` call as verification — was rejected because `ls` would then clear an
663
- * unverified edit, which is the failure mode `verify-before-complete` names
664
- * ("relying on partial verification"). The cost of narrowness is a MISSED
665
- * detection when a project verifies by some command not listed here, and that is
666
- * the safe direction for a gate that can refuse a turn: a false negative costs
667
- * one unguarded turn, a false positive teaches the user to switch the gate off.
668
- *
669
- * ## Audited 2026-08-17 — `road-to-stop-gate-honesty` step 2.2
670
- *
671
- * 52 commands this project's own surface actually produces (its `package.json`
672
- * scripts, its `Taskfile` targets, `./scripts-run`, `./agent-config`, and the
673
- * PHP / Python / Go / Rust toolchains) were run through
674
- * `isVerificationCommand`. The table of every one, with its verdict, is
675
- * `turn_end_verify_allowlist.test.ts`; that file is the fixture step 2.2
676
- * requires, and every addition below has its own row in it.
677
- *
678
- * The draft that opened the roadmap expected to add `phpunit` and `pest`. Claim
679
- * 4 had already overtaken that — both matched before this audit, as did
680
- * `composer test` and `php artisan test` — so what the audit actually found was
681
- * two different gaps:
682
- *
683
- * · `phpstan` — a static analyser of exactly the class already listed
684
- * (`mypy`, `pyright`, `clippy`). Added.
685
- * · `lint` followed by a WORD character — `lint_persistence`,
686
- * `lint_provenance`, and every other `lint_*` script in `src/scripts/`
687
- * missed, because `\blint\b` needs a boundary and `_` is a word character.
688
- * `lint[-_:a-z]*` is the exact mirror of the `check[-_:a-z]*` this list
689
- * already carried, which is why it is the narrow fix rather than a new idea.
693
+ * Re-exported, not re-derived: the selector moved to `_lib/verification_command.ts`
694
+ * so the evidence classifier can read it without a hook<->lib import cycle.
690
695
  *
691
- * Four further misses were found and deliberately NOT added, because Risk 2 of
692
- * that roadmap is that every addition is a way to satisfy the gate without
693
- * verifying anything:
694
- *
695
- * · `npm run prepack` — a lifecycle hook whose content is per-project. Here it
696
- * validates; elsewhere it copies files. Recognising it would clear an
697
- * unverified edit in any repo whose prepack is a build step.
698
- * · `task sync` / `task generate-tools` / `agent-config roadmap:progress` —
699
- * GENERATORS. They rewrite the tree; they check nothing. A generator that
700
- * cleared the gate would be the rubber stamp in its purest form.
701
- * · `agent-config gates --all` — enumerates gates, runs none.
702
- * · `vendor/bin/rector process --dry-run` — a refactoring tool. Its dry run
703
- * prints a diff; it does not assert anything holds.
704
- *
705
- * `psalm` is the obvious sibling of `phpstan` and is also NOT added: nothing in
706
- * the audited surface runs it, and "the team actually runs" is the step's own
707
- * standard. It is named here so the next audit does not re-derive the question.
696
+ * The name stays exported HERE because two test files pin it at this path and
697
+ * because a reader arriving at detector C should find the selector where the
698
+ * detector uses it, not have to know it was relocated.
708
699
  */
709
- const _VERIFY_RE =
710
- /\b(test|tests|vitest|jest|pytest|phpunit|pest|tsc|eslint|ruff|mypy|pyright|clippy|phpstan|typecheck|lint[-_:a-z]*|build|ci|preflight|smoke[-_:a-z]*|check[-_:a-z]*|validate[-_:a-z]*)\b|\b(task|npm|pnpm|yarn|composer|cargo|go|make|php artisan|bun)\s+(run\s+)?\S*(test|check|lint|build|ci|typecheck)/i;
711
-
712
- export function isVerificationCommand(command: string): boolean {
713
- return _VERIFY_RE.test(command);
714
- }
700
+ export { isVerificationCommand };
715
701
 
716
702
  /**
717
703
  * Fire when the turn changed a file and then ran nothing that could have
@@ -728,28 +714,55 @@ export function isVerificationCommand(command: string): boolean {
728
714
  * no claim to verify, and refusing one would make the gate fire on the majority
729
715
  * of turns, which is how a guard gets disabled.
730
716
  */
731
- export function detectUnverifiedEdit(toolCalls: readonly ToolCall[]): Finding | null {
717
+ export function detectUnverifiedEdit(
718
+ toolCalls: readonly ToolCall[],
719
+ runState: TurnRunState | null = null,
720
+ ): Finding | null {
732
721
  let lastEdit = -1;
733
722
  for (let i = 0; i < toolCalls.length; i += 1) {
734
723
  if (_EDIT_TOOLS.has(toolCalls[i]!.name)) lastEdit = i;
735
724
  }
736
725
  if (lastEdit === -1) return null;
726
+ const edited = toolCalls[lastEdit]!.path ?? toolCalls[lastEdit]!.name;
727
+
728
+ // THE RECORD PATH. Preferred whenever the recorder demonstrably witnessed
729
+ // this turn — see `readTurnRunState`, which returns null otherwise. The
730
+ // regex path below stays reachable and is not deprecated: it is the only
731
+ // path a transcript replay has, and it is what a host that binds no
732
+ // `post_tool_use` slot keeps.
733
+ if (runState !== null) {
734
+ const reading = readRunEvidence(runState);
735
+ if (reading.passed) return null;
736
+ // An instrument gap is never refused on (Risk 1 of the plan): the host
737
+ // surfaced no exit code, or wrote a record nothing can place, and
738
+ // neither is a fact about the operator's work. The transcript answer
739
+ // stands in, which is exactly the behavior before this path existed.
740
+ if (!reading.instrumentGap) {
741
+ return {
742
+ detector: 'verification',
743
+ evidence: edited,
744
+ reason: recordReason(reading.failed, reading.reasons),
745
+ mode: 'record',
746
+ };
747
+ }
748
+ }
749
+
737
750
  for (let i = lastEdit + 1; i < toolCalls.length; i += 1) {
738
751
  const c = toolCalls[i]!;
739
752
  if (c.name === 'Bash' && c.command !== undefined && isVerificationCommand(c.command)) {
740
753
  return null;
741
754
  }
742
755
  }
743
- const edited = toolCalls[lastEdit]!.path;
744
756
  return {
745
757
  detector: 'verification',
746
758
  // The path, never the diff: the evidence span is quoted into a refusal,
747
759
  // and `ToolCall` is shaped so a file body cannot reach it.
748
- evidence: edited ?? toolCalls[lastEdit]!.name,
760
+ evidence: edited,
749
761
  reason:
750
762
  'this turn changed a file and then ran no verification command — ' +
751
763
  'no test, type-check, lint or build call follows the last edit ' +
752
764
  '(verify-before-complete: a claim without a fresh run is unverified)',
765
+ mode: 'transcript',
753
766
  };
754
767
  }
755
768
 
@@ -827,13 +840,55 @@ function _isProductionSource(p: string): boolean {
827
840
  * turn; drop the second and it fires on a turn that did its job; drop the third
828
841
  * and it fires mid-cycle on the red step of red-green-refactor.
829
842
  */
830
- export function detectUntestedChange(reply: string, toolCalls: readonly ToolCall[]): Finding | null {
843
+ export function detectUntestedChange(
844
+ reply: string,
845
+ toolCalls: readonly ToolCall[],
846
+ runState: TurnRunState | null = null,
847
+ ): Finding | null {
831
848
  const edited = toolCalls
832
849
  .filter((c) => _EDIT_TOOLS.has(c.name) && c.path !== undefined)
833
850
  .map((c) => c.path as string);
834
851
  const source = edited.filter(_isProductionSource);
835
852
  if (source.length === 0) return null;
836
- if (edited.some(_isTestPath)) return null;
853
+
854
+ // THE TEST-FILE ESCAPE, and why step 5.1 narrowed it rather than removed it.
855
+ //
856
+ // "A test file was touched somewhere in the turn" is satisfied by a file
857
+ // containing `it('works', () => expect(true).toBe(true))`. A test never seen
858
+ // red has unknown sensitivity — it may assert nothing the change could
859
+ // break — so the escape as written accepted the shape of evidence instead of
860
+ // evidence.
861
+ //
862
+ // With records available, the escape asks for the cheapest observable proof
863
+ // that the assertion discriminates: the same target red, then green after
864
+ // the last edit. Without records it stays exactly as it was, because the
865
+ // transcript cannot see an exit code and a detector that refused on a
866
+ // transcript-only host would refuse every honest turn there.
867
+ // A NEW test file, not any test edit. Step 5.1 says "accepts a NEW test file
868
+ // only with that pair present", and `edited.some(_isTestPath)` is any
869
+ // test-path edit — so adjusting an assertion in an existing test alongside a
870
+ // production change and running it green once was refused. New-vs-existing
871
+ // is not decidable from a transcript; `Write` on a test path is the usable
872
+ // proxy, because `Edit` and `MultiEdit` presuppose a file that already
873
+ // existed. Narrowing here can only ALLOW turns the previous line refused.
874
+ const wroteNewTest = toolCalls.some(
875
+ (c) => c.name === 'Write' && c.path !== undefined && _isTestPath(c.path),
876
+ );
877
+ let noRedEvidence = false;
878
+ if (edited.some(_isTestPath)) {
879
+ // An EXISTING test adjusted alongside a production change keeps the old
880
+ // escape untouched — `Edit` and `MultiEdit` presuppose a file that was
881
+ // already there, so nothing new was claimed and there is no new
882
+ // assertion whose sensitivity is unknown.
883
+ if (!wroteNewTest) return null;
884
+ if (runState === null) return null;
885
+ if (hasRedThenGreen(runState)) return null;
886
+ // An instrument gap is not a missing red. Same rule as detector C: the
887
+ // host surfaced no exit code, so nothing about the operator's work is
888
+ // known, and the pre-record behavior stands in.
889
+ if (readRunEvidence(runState).instrumentGap) return null;
890
+ noRedEvidence = true;
891
+ }
837
892
 
838
893
  // The claim gate, reusing detector D's pair rather than a second dialect of
839
894
  // "done" — two lists of completion phrasings would drift, and the negation
@@ -844,8 +899,21 @@ export function detectUntestedChange(reply: string, toolCalls: readonly ToolCall
844
899
  if (_NEGATED_CLAIM_RE.test(_lineAround(prose, m.index))) return null;
845
900
 
846
901
  const shown = source.slice(0, 3).join(', ');
902
+ if (noRedEvidence) {
903
+ return {
904
+ detector: 'untested',
905
+ evidence: shown + (source.length > 3 ? ` (+${String(source.length - 3)} more)` : ''),
906
+ mode: 'record',
907
+ reason: NO_RED_EVIDENCE_REASON,
908
+ };
909
+ }
910
+ // `transcript` unconditionally: no record contributed to THIS verdict — it
911
+ // is reached because the turn touched no test file at all, which is read off
912
+ // the tool calls. Labelling it `record` because a run state happened to be
913
+ // available would misattribute the evidence.
847
914
  return {
848
915
  detector: 'untested',
916
+ mode: 'transcript',
849
917
  evidence: shown + (source.length > 3 ? ` (+${String(source.length - 3)} more)` : ''),
850
918
  reason:
851
919
  'a completion claim over production code this turn changed, with NO test file ' +
@@ -984,199 +1052,20 @@ function markRefusedTurn(
984
1052
  // Transcript
985
1053
  // ---------------------------------------------------------------------------
986
1054
 
987
- /**
988
- * One tool call the assistant made, reduced to what a detector can reason
989
- * about: the tool's name, the shell command when it is a shell call, and the
990
- * target path when it is a file write.
991
- *
992
- * Nothing else is kept. A tool input can hold a whole file body, and this
993
- * struct is what a refusal quotes back — so it is shaped to be INCAPABLE of
994
- * carrying one, the same PII-exclusion-by-construction discipline
995
- * `domain-safety-pii` § Surface 2 asks for in a log line.
996
- */
997
- export interface ToolCall {
998
- name: string;
999
- /** `Bash` only — the command line, so "did anything verify" is answerable. */
1000
- command?: string;
1001
- /** Edit/Write only — the file the turn changed, used as the evidence span. */
1002
- path?: string;
1003
- }
1004
-
1005
- export interface TranscriptTail {
1006
- lastAssistant: string;
1007
- /**
1008
- * Every assistant text of the CURRENT turn, in order — reset at each genuine
1009
- * user prompt exactly like `toolCalls`, and for the same reason: a question
1010
- * asked three turns ago was answered, a question asked earlier in THIS turn
1011
- * was not. Detector E reads it; the other four read `lastAssistant`, which is
1012
- * always the last element when this is non-empty.
1013
- *
1014
- * A tool-only assistant entry contributes NOTHING here. `_messageText`
1015
- * returns null for it and the collector skips it, so the ordinary shape of a
1016
- * working turn — prose, tool call, prose — produces two elements and not
1017
- * three. That is the whole false-positive surface of detector E, and it is
1018
- * closed here rather than in the detector.
1019
- */
1020
- assistantTurnTexts: string[];
1021
- /**
1022
- * How many GENUINE user prompts the transcript carries — harness-injected
1023
- * user-role entries excluded via `isSyntheticPrompt`. This is the turn's
1024
- * identity; see `alreadyRefusedTurn` for why the prompt's text is not.
1025
- */
1026
- turnOrdinal: number;
1027
- /**
1028
- * The tool calls of the CURRENT turn, in order — reset at every genuine user
1029
- * prompt, so a verification from three turns ago cannot vouch for an edit
1030
- * made now. `_messageText` keeps only `type === 'text'` blocks, which is why
1031
- * this needed its own extraction rather than a reading of `lastAssistant`:
1032
- * tool activity is not in the prose at all.
1033
- */
1034
- toolCalls: ToolCall[];
1035
- }
1036
-
1037
- /**
1038
- * The last assistant text, plus the turn ordinal, from a Claude JSONL
1039
- * transcript. Mirrors `chat_history._extract_claude_transcript_response` for
1040
- * the assistant half.
1041
- *
1042
- * `isSyntheticPrompt` is applied to every user-role entry — the same filter
1043
- * `language_mirror_hook` uses, and for the same reason it was added there
1044
- * (round-5 § 6.5): a background-task notification and a `<system-reminder>`
1045
- * both occupy the user role without being chat messages. Counting them would
1046
- * move the turn ordinal mid-turn, which is R2 finding 3.
1047
- */
1048
- export function readTranscriptTail(
1049
- transcriptPath: string,
1050
- opts: { homeDir?: string; maxBytes?: number } = {},
1051
- ): TranscriptTail {
1052
- const empty: TranscriptTail = {
1053
- lastAssistant: '',
1054
- turnOrdinal: 0,
1055
- toolCalls: [],
1056
- assistantTurnTexts: [],
1057
- };
1058
- if (!transcriptPath || !isSafeTranscriptPath(transcriptPath, opts)) return empty;
1059
- let lines: string[];
1060
- try {
1061
- // R2 finding 12: `maxBytes` was accepted in the options type and never
1062
- // used, so the declared cap was decoration. It is enforced here as well
1063
- // as inside `isSafeTranscriptPath`, because this is the read it bounds.
1064
- // The whole file still has to be walked — the turn ordinal is a count
1065
- // over all entries, not something a tail can answer — so the cap is the
1066
- // guard, not an optimisation.
1067
- if (opts.maxBytes !== undefined && fs.statSync(transcriptPath).size > opts.maxBytes) {
1068
- return empty;
1069
- }
1070
- lines = fs.readFileSync(transcriptPath, 'utf-8').split('\n');
1071
- } catch {
1072
- return empty;
1073
- }
1074
- let lastAssistant = '';
1075
- let turnOrdinal = 0;
1076
- let toolCalls: ToolCall[] = [];
1077
- let assistantTurnTexts: string[] = [];
1078
- for (const rawLine of lines) {
1079
- const line = rawLine.trim();
1080
- if (!line) continue;
1081
- let obj: unknown;
1082
- try {
1083
- obj = JSON.parse(line);
1084
- } catch {
1085
- continue;
1086
- }
1087
- if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) continue;
1088
- const entry = obj as Record<string, unknown>;
1089
- // Sidechain entries are a SUBAGENT's conversation recorded in the same
1090
- // JSONL. A subagent prompt is a genuine-looking user-role text entry
1091
- // appended mid-turn, so counting it moves the ordinal within the turn —
1092
- // finding 3's failure class in a new shape (R2 round 2, finding 3).
1093
- if (entry['isSidechain'] === true) continue;
1094
- const role = entry['type'];
1095
- if (role !== 'assistant' && role !== 'user') continue;
1096
- const msg = entry['message'];
1097
- if (typeof msg !== 'object' || msg === null || Array.isArray(msg)) continue;
1098
- const content = (msg as Record<string, unknown>)['content'];
1099
- // Tool calls are read BEFORE the text guard: an assistant entry that is
1100
- // only a tool_use block has no text at all, so `continue`-ing on a null
1101
- // text would drop exactly the entries this detector exists to see.
1102
- if (role === 'assistant') {
1103
- toolCalls.push(...extractToolCalls(content));
1104
- }
1105
- const text = _messageText(content);
1106
- if (text === null) continue;
1107
- if (role === 'assistant') {
1108
- lastAssistant = text;
1109
- assistantTurnTexts.push(text);
1110
- } else if (!isSyntheticPrompt(text)) {
1111
- turnOrdinal += 1;
1112
- // A genuine user prompt starts a new turn, so the previous turn's
1113
- // tool activity stops counting. Without this reset, a verification
1114
- // run three turns ago would vouch for an edit made now — the
1115
- // "fresh" in edit-without-FRESH-verification is this line.
1116
- toolCalls = [];
1117
- // Same boundary, same reason, for detector E: an options block the
1118
- // user has since replied to is answered, not dropped. Resetting on
1119
- // the identical line is what makes "in the SAME user turn" true of
1120
- // the array rather than merely intended.
1121
- assistantTurnTexts = [];
1122
- }
1123
- }
1124
- return {
1125
- lastAssistant: lastAssistant.trim(),
1126
- turnOrdinal,
1127
- toolCalls,
1128
- assistantTurnTexts,
1129
- };
1130
- }
1131
-
1132
- /**
1133
- * Extract this entry's tool calls, keeping only name, shell command and target
1134
- * path. A tool input can hold a whole file body; nothing but those three fields
1135
- * is carried forward.
1136
- *
1137
- * Exported because `measure_turn_end_gate` scores detectors C and E over a real
1138
- * transcript corpus and therefore has to rebuild the same `ToolCall[]` the gate
1139
- * sees. A second extractor there would be a second dialect of "what the turn
1140
- * did", and the measurement would then be of that dialect rather than of the
1141
- * shipped gate — the exact population-parity defect that script's own header
1142
- * documents twice.
1143
- */
1144
- export function extractToolCalls(content: unknown): ToolCall[] {
1145
- if (!Array.isArray(content)) return [];
1146
- const out: ToolCall[] = [];
1147
- for (const blk of content) {
1148
- if (typeof blk !== 'object' || blk === null || Array.isArray(blk)) continue;
1149
- const b = blk as Record<string, unknown>;
1150
- if (b['type'] !== 'tool_use') continue;
1151
- const name = b['name'];
1152
- if (typeof name !== 'string') continue;
1153
- const input = b['input'];
1154
- const call: ToolCall = { name };
1155
- if (typeof input === 'object' && input !== null && !Array.isArray(input)) {
1156
- const inp = input as Record<string, unknown>;
1157
- const cmd = inp['command'];
1158
- if (typeof cmd === 'string') call.command = cmd;
1159
- const p = inp['file_path'] ?? inp['path'] ?? inp['notebook_path'];
1160
- if (typeof p === 'string') call.path = p;
1161
- }
1162
- out.push(call);
1163
- }
1164
- return out;
1165
- }
1166
-
1167
- function _messageText(content: unknown): string | null {
1168
- if (typeof content === 'string') return content;
1169
- if (!Array.isArray(content)) return null;
1170
- const parts: string[] = [];
1171
- for (const blk of content) {
1172
- if (typeof blk !== 'object' || blk === null || Array.isArray(blk)) continue;
1173
- const b = blk as Record<string, unknown>;
1174
- if (b['type'] !== 'text') continue;
1175
- const t = b['text'];
1176
- if (typeof t === 'string') parts.push(t);
1177
- }
1178
- return parts.length > 0 ? parts.join('\n') : null;
1179
- }
1055
+ // Extracted to `_lib/turn_end_transcript.ts` — same move, same reason, as
1056
+ // detector E above: `_lib` is where the prose costs no ratchet debt, and
1057
+ // this file hit its 1,500-line source budget when the shadow read landed.
1058
+ // Re-exported so every existing importer of this module is unchanged;
1059
+ // `tests/scripts/turn_end_gate_hook.test.ts` addresses all three names at
1060
+ // this path.
1061
+ import { readTranscriptTail, type ToolCall } from '../_lib/turn_end_transcript.js';
1062
+
1063
+ export {
1064
+ extractToolCalls,
1065
+ readTranscriptTail,
1066
+ type ToolCall,
1067
+ type TranscriptTail,
1068
+ } from '../_lib/turn_end_transcript.js';
1180
1069
 
1181
1070
  // ---------------------------------------------------------------------------
1182
1071
  // main
@@ -1186,18 +1075,41 @@ function str(v: JsonValue | undefined): string {
1186
1075
  return typeof v === 'string' ? v : '';
1187
1076
  }
1188
1077
 
1189
- export function main(): number {
1190
- let envelope: JsonObject;
1191
- let payload: JsonObject;
1192
- try {
1193
- [envelope, payload] = unwrap(readHookStdin(), 'claude');
1194
- } catch {
1195
- return EXIT_ALLOW;
1196
- }
1197
-
1198
- // Layer 1 — the host's own answer to "did I already refuse this turn?".
1199
- if (payload['stop_hook_active'] === true) return EXIT_ALLOW;
1078
+ /**
1079
+ * Everything the detectors read, assembled once per stop event.
1080
+ *
1081
+ * Extracted for step 2.1 of `road-to-a-stop-that-holds`, and the extraction —
1082
+ * not the shadow write — is the load-bearing half. The contract's Q1 asks how
1083
+ * often a turn the gate ALLOWS on a re-entrancy layer would have been refused
1084
+ * again, which needs the detectors to run on the allow path. There were two
1085
+ * ways to get them there: call them from a second assembly written beside the
1086
+ * first, or assemble once and call from both. `obligation_settle_hook.ts`'s
1087
+ * header already rules out the first — "a detector whose shadow measurement
1088
+ * and live behaviour come from different code measures nothing" — so this
1089
+ * function exists so that there is exactly one assembly and exactly one
1090
+ * detector list, read by the live verdict and by the shadow alike.
1091
+ *
1092
+ * `null` means the turn has no assistant text, which is `main()`'s existing
1093
+ * allow and is not a detector input the shadow could have measured either.
1094
+ */
1095
+ interface DetectorInputs {
1096
+ workspaceRoot: string;
1097
+ rawSessionId: string;
1098
+ sessionKey: string;
1099
+ turnOrdinal: number;
1100
+ promptId: string;
1101
+ lastAssistant: string;
1102
+ toolCalls: ToolCall[];
1103
+ turnTexts: string[];
1104
+ closingFromPayload: boolean;
1105
+ dispatchOpen: boolean;
1106
+ runState: TurnRunState | null;
1107
+ }
1200
1108
 
1109
+ function assembleDetectorInputs(
1110
+ envelope: JsonObject,
1111
+ payload: JsonObject,
1112
+ ): DetectorInputs | null {
1201
1113
  const workspaceRoot = str(envelope['workspace_root'] as JsonValue | undefined) || process.cwd();
1202
1114
 
1203
1115
  // Layer 1b — an open subagent dispatch is an EXPLICIT allow
@@ -1219,7 +1131,8 @@ export function main(): number {
1219
1131
  // suppressed ALL FOUR detectors. A pending dispatch explains a promissory
1220
1132
  // closing (A) and an unsettled completion claim (D); it explains nothing
1221
1133
  // about a language mismatch (B) or an unverified edit (C), and silencing
1222
- // those was scope Step 2 never asked for. Applied per detector below.
1134
+ // those was scope Step 2 never asked for. Applied per detector in
1135
+ // `runDetectors`.
1223
1136
  //
1224
1137
  // R2 round 2, finding 1: the count is TTL-filtered inside
1225
1138
  // `openRecordStats`. A leaked record from a dispatch that never returned
@@ -1252,7 +1165,7 @@ export function main(): number {
1252
1165
  transcriptPath,
1253
1166
  { maxBytes: TRANSCRIPT_READ_MAX_BYTES },
1254
1167
  );
1255
- if (!lastAssistant) return EXIT_ALLOW;
1168
+ if (!lastAssistant) return null;
1256
1169
 
1257
1170
  // The closing reply comes from the field the host hands us when it supplies
1258
1171
  // one, not only from a file the host writes asynchronously. The transcript
@@ -1272,42 +1185,57 @@ export function main(): number {
1272
1185
  ? [...assistantTurnTexts, payloadClosing]
1273
1186
  : assistantTurnTexts;
1274
1187
 
1275
- // Layer 2 — keyed on the turn's ORDINAL, never on the prompt's text.
1276
- // A host that sends no `session_id` shares one bucket AND one small-integer
1277
- // ordinal namespace, so an unrelated session whose ordinal matches a stored
1278
- // `refused_turn` reads as already-refused and goes unguarded. R2 round 2,
1279
- // finding 16: the sibling hook documents its own bucket as degrading rather
1280
- // than colliding, and this one collides. It is named here rather than
1281
- // papered over — the degradation is toward UNDER-refusing, which is the safe
1282
- // direction, and layer 1 still covers the host that sends the flag.
1188
+ // The host's own id for the prompt being processed. Recorded beside the
1189
+ // ordinal in the refusal marker — never used as the guard key. The
1190
+ // ordinal's drift is what both re-entrancy layers exist for, so a later
1191
+ // reading needs the pair to tell a drifted ordinal from a second prompt.
1192
+ const promptId = str((payload['prompt_id'] ?? payload['promptId']) as JsonValue | undefined);
1283
1193
  // RAW id, kept beside the derived key rather than replaced by it:
1284
1194
  // `readLanguagePin` addresses the producer's own per-session file, and the
1285
- // producer keys that on the raw `session_id`. Passing `sessionKey` here would
1286
- // read a path nothing writes — the same shape as the STATE_FILE break this
1287
- // parameter exists to close.
1288
- // The host's own id for the prompt being processed. Read here and recorded
1289
- // beside the ordinal in the refusal marker — never used as the guard key.
1290
- // The ordinal's drift is what both re-entrancy layers exist for, so a later
1291
- // reading needs the pair to tell a drifted ordinal from a second prompt.
1292
- const promptId = str(
1293
- (payload['prompt_id'] ?? payload['promptId']) as JsonValue | undefined,
1294
- );
1195
+ // producer keys that on the raw `session_id`. Passing `sessionKey` there
1196
+ // would read a path nothing writes — the same shape as the STATE_FILE break
1197
+ // this pairing exists to close.
1295
1198
  const rawSessionId = str(envelope['session_id'] as JsonValue | undefined) || '';
1296
1199
  const sessionKey = deriveSessionKey(rawSessionId || 'unknown-session');
1297
- if (alreadyRefusedTurn(workspaceRoot, sessionKey, turnOrdinal)) return EXIT_ALLOW;
1298
-
1299
- // B and C run on every turn-end; A and D run only when no dispatch is open
1300
- // (the ternaries below). For the detectors that DO run, the gating is INSIDE
1301
- // each one — no promise, no pin mismatch, no unverified edit, no unsettled
1302
- // completion claim ⇒ no finding ⇒ the turn ends. That is the whole of "fires
1303
- // when it is warranted"; there is no second, configurable notion of warranted
1304
- // layered on top of it.
1200
+
1201
+ // ONE read of the recorder's state, shared by detectors C and F. Two reads
1202
+ // would be two file opens on the stop slot for one answer, and — worse —
1203
+ // could disagree if a post-tool event landed between them.
1305
1204
  //
1306
- // This said "Every detector runs on every turn-end" and enumerated three of
1307
- // four, three lines above the two `dispatchOpen` ternaries that refute it.
1308
- // Corrected 2026-08-19 with the header block at the top of this file: a reader
1309
- // arriving here met the false claim first, and the D comment below deferred to
1310
- // "the note at the top of this loop" — which was this sentence.
1205
+ // It used to sit AFTER the Layer-2 allow, with a comment saying "a retry
1206
+ // pays no file read it immediately discards". Step 2.1 makes that false
1207
+ // rather than leaving the comment to rot: a retry now RUNS the detectors
1208
+ // to record whether it would have been refused again, so the read is
1209
+ // consumed on that path too. The cost is one `readFileSync` per retry,
1210
+ // which is the budget line 2.1 pre-registers.
1211
+ const runState = readTurnRunState(workspaceRoot, rawSessionId);
1212
+
1213
+ return {
1214
+ workspaceRoot,
1215
+ rawSessionId,
1216
+ sessionKey,
1217
+ turnOrdinal,
1218
+ promptId,
1219
+ lastAssistant,
1220
+ toolCalls,
1221
+ turnTexts,
1222
+ closingFromPayload,
1223
+ dispatchOpen,
1224
+ runState,
1225
+ };
1226
+ }
1227
+
1228
+ /**
1229
+ * Run every detector over one assembled input set.
1230
+ *
1231
+ * The one list, called by the live verdict and by the shadow read. Its
1232
+ * ordering and its `dispatchOpen` ternaries are the gate's behaviour; nothing
1233
+ * about the shadow may change them, which is why the shadow calls this rather
1234
+ * than carrying a copy.
1235
+ */
1236
+ function runDetectors(inputs: DetectorInputs): Finding[] {
1237
+ const { dispatchOpen, lastAssistant, workspaceRoot, rawSessionId, toolCalls, runState } =
1238
+ inputs;
1311
1239
  const findings: Finding[] = [];
1312
1240
  for (const f of [
1313
1241
  // A and D are the completion-adjacent pair a pending dispatch excuses
@@ -1315,17 +1243,14 @@ export function main(): number {
1315
1243
  // excused by anything about a dispatch and run unchanged.
1316
1244
  dispatchOpen ? null : detectPromissory(lastAssistant),
1317
1245
  detectLanguage(lastAssistant, readLanguagePin(workspaceRoot, rawSessionId)),
1318
- detectUnverifiedEdit(toolCalls),
1319
- // Round 7 § Phase 1 — detector D. It is NOT unconditional, and this
1320
- // comment said it was while sitting one line above the `dispatchOpen`
1321
- // ternary that conditions it: A and D are both excused by an open
1322
- // dispatch, per the note at the top of this loop. Corrected 2026-08-18.
1323
- // It shipped with its own settings flag one commit earlier
1324
- // and that flag is gone: `hooks.turn_end_gate.*` was deleted on
1325
- // 2026-08-12 because a default-off safety gate is an absent one. Its
1326
- // gating is where the comment above says gating belongs — inside the
1327
- // detector: no CI observed, or a settled read, or no completion claim
1328
- // ⇒ no finding.
1246
+ detectUnverifiedEdit(toolCalls, runState),
1247
+ // Round 7 § Phase 1 — detector D. It is NOT unconditional: A and D are
1248
+ // both excused by an open dispatch, per the note above. It shipped with
1249
+ // its own settings flag one commit earlier and that flag is gone:
1250
+ // `hooks.turn_end_gate.*` was deleted on 2026-08-12 because a
1251
+ // default-off safety gate is an absent one. Its gating is where the
1252
+ // note above says gating belongs — inside the detector: no CI observed,
1253
+ // or a settled read, or no completion claim ⇒ no finding.
1329
1254
  dispatchOpen
1330
1255
  ? null
1331
1256
  : detectCompletionClaim(lastAssistant, readCiSettled(workspaceRoot, rawSessionId)),
@@ -1337,25 +1262,113 @@ export function main(): number {
1337
1262
  // that produced the measured failure — IS the continuation that drops
1338
1263
  // the decision, so narrowing E the same way would silence it in exactly
1339
1264
  // the case it exists for.
1340
- detectDroppedDecision(turnTexts, closingFromPayload),
1265
+ detectDroppedDecision(inputs.turnTexts, inputs.closingFromPayload),
1341
1266
  // Detector F takes the OTHER side of that same question, and the two
1342
1267
  // sitting adjacent is the clearest place to say why. E fires on a
1343
1268
  // question already asked, which an open dispatch cannot excuse. F fires
1344
1269
  // on a completion CLAIM, and a turn waiting on a subagent has not
1345
1270
  // finished — so its closing is not the claim F is about. Same slot,
1346
1271
  // opposite trigger, opposite treatment of `dispatchOpen`.
1347
- dispatchOpen ? null : detectUntestedChange(lastAssistant, toolCalls),
1272
+ dispatchOpen ? null : detectUntestedChange(lastAssistant, toolCalls, runState),
1348
1273
  ]) {
1349
1274
  if (f) findings.push(f);
1350
1275
  }
1276
+ return findings;
1277
+ }
1278
+
1279
+ /**
1280
+ * Record what this retry WOULD have been refused for, then let it end.
1281
+ *
1282
+ * Never changes a verdict: every caller returns `EXIT_ALLOW` whatever happens
1283
+ * here, and the whole body is wrapped because a retry that crashed on its own
1284
+ * instrument would be a new failure mode on a path that previously did no work
1285
+ * at all. A retry with no findings still lands, as `retries_observed` without a
1286
+ * row — that is the only way Q1 can read below 1.
1287
+ */
1288
+ function recordShadow(inputs: DetectorInputs, layer: ShadowLayer): void {
1289
+ if (is_replay_mode()) return;
1290
+ try {
1291
+ const findings = runDetectors(inputs);
1292
+ const file = sessionShadowFile(inputs.workspaceRoot, inputs.sessionKey);
1293
+ let prev: ShadowRecord | null = null;
1294
+ try {
1295
+ prev = parseShadowRecord(fs.readFileSync(file, 'utf-8'));
1296
+ } catch {
1297
+ // Absent or unreadable prior state starts a fresh record.
1298
+ }
1299
+ atomic_write_json(
1300
+ file,
1301
+ foldShadow(prev, {
1302
+ detectors: findings.map((f) => f.detector),
1303
+ turnOrdinal: inputs.turnOrdinal,
1304
+ at: new Date().toISOString(),
1305
+ layer,
1306
+ }) as unknown as Record<string, unknown>,
1307
+ );
1308
+ } catch {
1309
+ // An instrument is never a reason to change what the gate does.
1310
+ }
1311
+ }
1312
+
1313
+ export function main(): number {
1314
+ let envelope: JsonObject;
1315
+ let payload: JsonObject;
1316
+ try {
1317
+ [envelope, payload] = unwrap(readHookStdin(), 'claude');
1318
+ } catch {
1319
+ return EXIT_ALLOW;
1320
+ }
1321
+
1322
+ // Layer 1 — the host's own answer to "did I already refuse this turn?".
1323
+ //
1324
+ // Still an unconditional allow. What changed in step 2.1 is that the turn
1325
+ // is MEASURED on the way out: the detectors run once against the same
1326
+ // inputs the live path would have built, and a finding becomes a
1327
+ // `would_refuse_again` row. The verdict is byte-identical to the version
1328
+ // that returned here immediately.
1329
+ if (payload['stop_hook_active'] === true) {
1330
+ try {
1331
+ const retryInputs = assembleDetectorInputs(envelope, payload);
1332
+ if (retryInputs !== null) recordShadow(retryInputs, 'stop_hook_active');
1333
+ } catch {
1334
+ // Assembly is throw-safe by construction today; the guard is here
1335
+ // because this path did NO work before 2.1, so a regression in any
1336
+ // reader it now calls must not turn a clean retry into a crash.
1337
+ }
1338
+ return EXIT_ALLOW;
1339
+ }
1340
+
1341
+ const inputs = assembleDetectorInputs(envelope, payload);
1342
+ if (inputs === null) return EXIT_ALLOW;
1343
+
1344
+ // Layer 2 — keyed on the turn's ORDINAL, never on the prompt's text.
1345
+ // A host that sends no `session_id` shares one bucket AND one small-integer
1346
+ // ordinal namespace, so an unrelated session whose ordinal matches a stored
1347
+ // `refused_turn` reads as already-refused and goes unguarded. R2 round 2,
1348
+ // finding 16: the sibling hook documents its own bucket as degrading rather
1349
+ // than colliding, and this one collides. It is named here rather than
1350
+ // papered over — the degradation is toward UNDER-refusing, which is the safe
1351
+ // direction, and layer 1 still covers the host that sends the flag.
1352
+ if (alreadyRefusedTurn(inputs.workspaceRoot, inputs.sessionKey, inputs.turnOrdinal)) {
1353
+ recordShadow(inputs, 'refused_turn');
1354
+ return EXIT_ALLOW;
1355
+ }
1356
+
1357
+ // B and C run on every turn-end; A and D run only when no dispatch is open.
1358
+ // For the detectors that DO run, the gating is INSIDE each one — no promise,
1359
+ // no pin mismatch, no unverified edit, no unsettled completion claim ⇒ no
1360
+ // finding ⇒ the turn ends. That is the whole of "fires when it is
1361
+ // warranted"; there is no second, configurable notion of warranted layered
1362
+ // on top of it.
1363
+ const findings = runDetectors(inputs);
1351
1364
  if (findings.length === 0) return EXIT_ALLOW;
1352
1365
 
1353
1366
  markRefusedTurn(
1354
- workspaceRoot,
1355
- sessionKey,
1356
- turnOrdinal,
1367
+ inputs.workspaceRoot,
1368
+ inputs.sessionKey,
1369
+ inputs.turnOrdinal,
1357
1370
  findings.map((f) => f.detector),
1358
- promptId,
1371
+ inputs.promptId,
1359
1372
  );
1360
1373
 
1361
1374
  const lines = findings.map(