@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
@@ -7,14 +7,24 @@
7
7
  * none, and a gate that blocks unobserved is the shape that discipline exists to
8
8
  * prevent.
9
9
  *
10
- * This module is the reader. It owns three things and deliberately not a fourth:
10
+ * This module is the reader. It owns five things and deliberately not a sixth:
11
11
  *
12
12
  * 1. the on-disk record shape, extended so a session's refusals are COUNTED
13
13
  * rather than overwritten (§ Phase 1 step 1.1);
14
14
  * 2. a TTL prune, which the gate's own header admits is missing (step 1.2);
15
15
  * 3. the split by recorded package version, so claim 10's prediction — that
16
16
  * refusals correlate with the local 12.1 install — is TESTED rather than
17
- * assumed (step 1.3).
17
+ * assumed (step 1.3);
18
+ * 4. the SHADOW record — what a retry would have been refused for, written on
19
+ * an allow path (`road-to-a-stop-that-holds` step 2.1);
20
+ * 5. the rollup over those records (step 2.2). Read `ShadowLayerStats` before
21
+ * quoting anything it produces: the share it computes is NOT the Q1 the
22
+ * demotion contract registers bars against, and the difference is named
23
+ * there rather than left to a reader of the number.
24
+ *
25
+ * (This list read "three things and deliberately not a fourth" until 2026-10-01.
26
+ * It was accurate when written and silently false for the two steps after it —
27
+ * an independent review caught it. Items 4 and 5 are the two that landed.)
18
28
  *
19
29
  * The fourth thing it does not own is a **rate over all sessions**, and the
20
30
  * reason is structural rather than an omission. A record is written only when a
@@ -335,6 +345,243 @@ export function readInstallBoundary(): InstallBoundary {
335
345
  }
336
346
  }
337
347
 
348
+ // ---------------------------------------------------------------------------
349
+ // The shadow read — road-to-a-stop-that-holds step 2.1
350
+ // ---------------------------------------------------------------------------
351
+
352
+ /**
353
+ * One observation of "this retry would still have been refused".
354
+ *
355
+ * `turn-end-detector-demotion.md` § Q1 asks what share of the turns the gate
356
+ * allows on a re-entrancy layer would have been refused again. Both layers
357
+ * return `EXIT_ALLOW` unconditionally, so that share is not derivable from the
358
+ * refusal record: a retry leaves no trace at all, and a detector that fired
359
+ * twice is indistinguishable from one that was satisfied on the second pass.
360
+ * This row is that trace.
361
+ *
362
+ * `layer` is which allow path produced it, because the two are not the same
363
+ * measurement. `stop_hook_active` is the HOST's answer and is set on any stop
364
+ * hook's block, including another concern's; `refused_turn` is this gate's own
365
+ * marker and therefore always follows a refusal by this gate. Pooling them
366
+ * would let another concern's retries inflate Q1's numerator against a
367
+ * denominator that only counts this gate's refusals.
368
+ */
369
+ export interface WouldRefuseAgainRow {
370
+ detector: RefusalDetectorId;
371
+ /** The turn ordinal the shadow ran on — joins to `refused_turn`. */
372
+ turn: number;
373
+ at: string;
374
+ layer: ShadowLayer;
375
+ }
376
+
377
+ /**
378
+ * The two allow paths a shadow read can run on, in the order `main()` reaches
379
+ * them.
380
+ *
381
+ * A LIST rather than a bare union, and the counter below is derived from it,
382
+ * for the reason `emptyCounts` gives one screen up: a hand-written counter
383
+ * object satisfied `Record<Union, number>` on the day it was typed and read
384
+ * back `undefined` the moment a member was added, so every `+=` over it
385
+ * produced `NaN` and the whole rollup went quiet. Adding a third layer here
386
+ * cannot repeat that.
387
+ */
388
+ export const SHADOW_LAYERS = ['stop_hook_active', 'refused_turn'] as const;
389
+
390
+ export type ShadowLayer = (typeof SHADOW_LAYERS)[number];
391
+
392
+ /** A zeroed retry counter per layer, DERIVED from `SHADOW_LAYERS`. */
393
+ export function emptyRetryCounts(): Record<ShadowLayer, number> {
394
+ const out = {} as Record<ShadowLayer, number>;
395
+ for (const l of SHADOW_LAYERS) out[l] = 0;
396
+ return out;
397
+ }
398
+
399
+ /**
400
+ * Per-session shadow state, in its own file beside the refusal record.
401
+ *
402
+ * SEPARATE FILE, deliberately, and this is the one design decision in the step
403
+ * worth defending. The plan says "into the session state" and the refusal
404
+ * record is the obvious place — but `refused_turn` in that record is the
405
+ * re-entrancy wedge guard, `parseRecord` rejects any record lacking it, and a
406
+ * Layer-1 retry can occur with no refusal by this gate at all (another stop
407
+ * concern blocked). Writing a shadow row there would mean either synthesising
408
+ * a `refused_turn` this gate never wrote — corrupting the one field a wedge
409
+ * depends on — or loosening the parser that protects it. A sibling file costs
410
+ * one more path and touches neither.
411
+ *
412
+ * It lives in this module rather than in the gate for the reason the header
413
+ * already gives about the pruner: a directory with two writers each holding
414
+ * their own idea of where the files are is how a reader and a writer end up
415
+ * disagreeing.
416
+ *
417
+ * Two limits bound what a reading of this record may claim.
418
+ *
419
+ * **It counts retries with a READABLE TRANSCRIPT, not all retries.** A retry
420
+ * whose transcript is absent, over `TRANSCRIPT_READ_MAX_BYTES`, or carries no
421
+ * assistant text records nothing at all — the gate's input assembly returns
422
+ * null and there is no detector verdict to shadow. The numerator is unaffected
423
+ * and the denominator shrinks, so the bias is UPWARD: a Q1 read off this
424
+ * record is an upper bound, never a point estimate.
425
+ *
426
+ * **On a host that sends no `session_id` it pools across sessions.** The key
427
+ * is `deriveSessionKey(rawSessionId || 'unknown-session')`, so every such
428
+ * session shares one file and one counter. The gate documents the same
429
+ * collision for the re-entrancy marker and argues it degrades safely, toward
430
+ * under-refusing. That argument does NOT transfer here: a merged measurement
431
+ * does not under-report, it reports one number for several sessions and a
432
+ * reader takes it for one. Named rather than inherited, because the two
433
+ * failures are opposite in kind.
434
+ */
435
+ export interface ShadowRecord {
436
+ /** Newest last. Bounded by `SHADOW_MAX_ROWS`. */
437
+ would_refuse_again: WouldRefuseAgainRow[];
438
+ /**
439
+ * Retries observed on an allow path in this session, refusing or NOT,
440
+ * SPLIT BY LAYER.
441
+ *
442
+ * Kept because a shadow row array alone cannot distinguish "no retry
443
+ * happened" from "every retry came back clean", and those are opposite
444
+ * readings of the same empty list.
445
+ *
446
+ * Split because pooling it made a per-layer Q1 uncomputable, which the
447
+ * first version shipped and an independent review caught before any row
448
+ * accumulated. `layer` is on every ROW for a stated reason — a
449
+ * `stop_hook_active` retry follows ANY stop concern's block, not only this
450
+ * gate's — so a pooled numerator would read another concern's retries
451
+ * against this gate's refusals. A pooled DENOMINATOR has the same defect
452
+ * one level down, and worse: a CLEAN retry adds no row at all, so it left
453
+ * no layer trace anywhere. The numerator could be split and the
454
+ * denominator could not, which is not a sanity check on anything.
455
+ *
456
+ * Keyed rather than two fields so a third layer, if one is ever added,
457
+ * cannot silently land in neither bucket.
458
+ */
459
+ retries_observed: Record<ShadowLayer, number>;
460
+ /** Rows dropped to the cap. A non-zero value makes the array a sample. */
461
+ dropped: number;
462
+ first_at: string;
463
+ last_at: string;
464
+ }
465
+
466
+ /**
467
+ * Row cap per session.
468
+ *
469
+ * A stated bound, not a measured optimum. A row is ~90 bytes and a session
470
+ * with 200 retries is already pathological, so the cap exists to keep an
471
+ * unbounded write off the Stop path rather than to fit a distribution. Oldest
472
+ * rows are dropped first: Q1 is a rate, and the most recent observations are
473
+ * the ones a reader is deciding on. A non-zero `dropped` on a real session is
474
+ * the signal that the number was chosen too low.
475
+ */
476
+ export const SHADOW_MAX_ROWS = 200;
477
+
478
+ /**
479
+ * The shadow file's suffix, named rather than inlined because TWO readers need
480
+ * to agree on it: the writer below and `pruneAgedRefusalState`, which scans the
481
+ * same directory for `*.json` and would otherwise keep every shadow record
482
+ * forever.
483
+ */
484
+ export const SHADOW_SUFFIX = '.shadow.json';
485
+
486
+ export function sessionShadowFile(workspaceRoot: string, sessionKey: string): string {
487
+ return path.join(refusalStateDir(workspaceRoot), `${sessionKey}${SHADOW_SUFFIX}`);
488
+ }
489
+
490
+ /**
491
+ * Parse one shadow record. `null` for anything unreadable — same fail-open
492
+ * contract as `parseRecord`, and for the same reason: this is the Stop path.
493
+ */
494
+ export function parseShadowRecord(raw: string): ShadowRecord | null {
495
+ let decoded: unknown;
496
+ try {
497
+ decoded = JSON.parse(raw);
498
+ } catch {
499
+ return null;
500
+ }
501
+ if (typeof decoded !== 'object' || decoded === null || Array.isArray(decoded)) return null;
502
+ const o = decoded as Record<string, unknown>;
503
+ const rowsRaw = o['would_refuse_again'];
504
+ if (!Array.isArray(rowsRaw)) return null;
505
+ const rows: WouldRefuseAgainRow[] = [];
506
+ for (const r of rowsRaw) {
507
+ if (typeof r !== 'object' || r === null || Array.isArray(r)) continue;
508
+ const ro = r as Record<string, unknown>;
509
+ const det = ro['detector'];
510
+ const turn = ro['turn'];
511
+ const at = ro['at'];
512
+ const layer = ro['layer'];
513
+ if (!isDetector(det)) continue;
514
+ if (typeof turn !== 'number' || !Number.isFinite(turn)) continue;
515
+ if (typeof at !== 'string') continue;
516
+ if (layer !== 'stop_hook_active' && layer !== 'refused_turn') continue;
517
+ rows.push({ detector: det, turn, at, layer });
518
+ }
519
+ const num = (v: unknown): number =>
520
+ typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : 0;
521
+ const observed = o['retries_observed'];
522
+ const byLayer = emptyRetryCounts();
523
+ if (typeof observed === 'object' && observed !== null && !Array.isArray(observed)) {
524
+ for (const l of SHADOW_LAYERS) {
525
+ byLayer[l] = num((observed as Record<string, unknown>)[l]);
526
+ }
527
+ }
528
+ return {
529
+ would_refuse_again: rows,
530
+ retries_observed: byLayer,
531
+ dropped: num(o['dropped']),
532
+ first_at: typeof o['first_at'] === 'string' ? o['first_at'] : '',
533
+ last_at: typeof o['last_at'] === 'string' ? o['last_at'] : '',
534
+ };
535
+ }
536
+
537
+ /**
538
+ * Fold one retry observation into a shadow record. Pure — the caller writes.
539
+ *
540
+ * `detectors` EMPTY is a real and load-bearing input: a retry whose detectors
541
+ * all came back silent raises `retries_observed` for its layer and adds no
542
+ * row, which is the only way Q1 can ever read below 1.
543
+ */
544
+ export function foldShadow(
545
+ prev: ShadowRecord | null,
546
+ input: {
547
+ detectors: readonly RefusalDetectorId[];
548
+ turnOrdinal: number;
549
+ at: string;
550
+ layer: ShadowLayer;
551
+ },
552
+ ): ShadowRecord {
553
+ const rows = [...(prev?.would_refuse_again ?? [])];
554
+ for (const d of input.detectors) {
555
+ rows.push({ detector: d, turn: input.turnOrdinal, at: input.at, layer: input.layer });
556
+ }
557
+ let dropped = prev?.dropped ?? 0;
558
+ if (rows.length > SHADOW_MAX_ROWS) {
559
+ dropped += rows.length - SHADOW_MAX_ROWS;
560
+ rows.splice(0, rows.length - SHADOW_MAX_ROWS);
561
+ }
562
+ const retries = emptyRetryCounts();
563
+ for (const l of SHADOW_LAYERS) retries[l] = prev?.retries_observed[l] ?? 0;
564
+ retries[input.layer] += 1;
565
+ return {
566
+ would_refuse_again: rows,
567
+ retries_observed: retries,
568
+ dropped,
569
+ first_at: prev?.first_at !== undefined && prev.first_at !== '' ? prev.first_at : input.at,
570
+ last_at: input.at,
571
+ };
572
+ }
573
+
574
+ /** This session's shadow record, or `null` when it has never retried. */
575
+ export function readShadowRecord(workspaceRoot: string, sessionKey: string): ShadowRecord | null {
576
+ try {
577
+ return parseShadowRecord(
578
+ fs.readFileSync(sessionShadowFile(workspaceRoot, sessionKey), 'utf-8'),
579
+ );
580
+ } catch {
581
+ return null;
582
+ }
583
+ }
584
+
338
585
  // ---------------------------------------------------------------------------
339
586
  // TTL — step 1.2
340
587
  // ---------------------------------------------------------------------------
@@ -350,8 +597,9 @@ export function readInstallBoundary(): InstallBoundary {
350
597
  * LIVENESS and this bounds EVIDENCE — pruning evidence on a liveness clock would
351
598
  * delete the corpus this roadmap exists to read.
352
599
  *
353
- * *Revisit-if:* a rollup window longer than 90 days is needed to read a rate, or
354
- * the directory's file count becomes a measured cost rather than a projected one.
600
+ * Two things would show 90 is the wrong number: a rollup window longer than it
601
+ * being needed to read a rate, or the directory's file count becoming a
602
+ * measured cost rather than a projected one.
355
603
  */
356
604
  export const REFUSAL_STATE_MAX_AGE_DAYS = 90;
357
605
 
@@ -389,6 +637,53 @@ export function pruneAgedRefusalState(
389
637
  if (!name.endsWith('.json')) continue;
390
638
  result.scanned += 1;
391
639
  const file = path.join(dir, name);
640
+ // A shadow record is a sibling in the same directory whose name also
641
+ // ends `.json`, and `parseRecord` rejects it for lacking `refused_at`.
642
+ // Left to the branch below it would land in `kept` FOREVER — the
643
+ // unbounded growth this pruner exists to stop, reintroduced by the
644
+ // instrument step 2.1 added. Its own clock is `last_at`.
645
+ if (name.endsWith(SHADOW_SUFFIX)) {
646
+ let shadow: ShadowRecord | null = null;
647
+ try {
648
+ shadow = parseShadowRecord(fs.readFileSync(file, 'utf-8'));
649
+ } catch {
650
+ shadow = null;
651
+ }
652
+ const shadowAt = shadow === null ? NaN : Date.parse(shadow.last_at);
653
+ // KEEP-ON-UNPARSEABLE IS NOT INHERITED, and the asymmetry with the
654
+ // branch below is the point. A refusal record that will not parse
655
+ // may still carry the wedge marker, so deleting it is the one
656
+ // irreversible move available and it is refused. A shadow record
657
+ // is pure measurement with nothing to protect — keeping one whose
658
+ // `last_at` cannot be read means it never ages out at all, which
659
+ // is the unbounded growth this pruner exists to stop, surviving
660
+ // for exactly the subset nobody can read. An unreadable one is
661
+ // pruned on the filesystem mtime, which is the weaker clock this
662
+ // function refuses for refusal records precisely because they are
663
+ // evidence; for a corrupt measurement it is the only clock left.
664
+ if (!Number.isFinite(shadowAt)) {
665
+ let mtime = NaN;
666
+ try {
667
+ mtime = fs.statSync(file).mtimeMs;
668
+ } catch {
669
+ mtime = NaN;
670
+ }
671
+ if (!Number.isFinite(mtime) || mtime >= cutoffMs) {
672
+ result.kept += 1;
673
+ continue;
674
+ }
675
+ } else if (shadowAt >= cutoffMs) {
676
+ result.kept += 1;
677
+ continue;
678
+ }
679
+ try {
680
+ fs.unlinkSync(file);
681
+ result.pruned += 1;
682
+ } catch {
683
+ result.kept += 1;
684
+ }
685
+ continue;
686
+ }
392
687
  let rec: RefusalRecord | null = null;
393
688
  try {
394
689
  rec = parseRecord(fs.readFileSync(file, 'utf-8'));
@@ -492,6 +787,12 @@ export function collectRefusalStats(workspaceRoot: string): RefusalStats {
492
787
  const versions = new Map<string, VersionBucket>();
493
788
  for (const name of entries) {
494
789
  if (!name.endsWith('.json')) continue;
790
+ // Shadow records share the directory and the extension. `parseRecord`
791
+ // would reject each one anyway, so this line changes no number — it is
792
+ // here because relying on another function's strictness for a
793
+ // correctness property is how the property disappears when that
794
+ // function is loosened.
795
+ if (name.endsWith(SHADOW_SUFFIX)) continue;
495
796
  let rec: RefusalRecord | null = null;
496
797
  try {
497
798
  rec = parseRecord(fs.readFileSync(path.join(dir, name), 'utf-8'));
@@ -535,3 +836,223 @@ export function collectRefusalStats(workspaceRoot: string): RefusalStats {
535
836
  stats.byVersion = [...versions.values()].sort((a, b) => (a.version < b.version ? 1 : -1));
536
837
  return stats;
537
838
  }
839
+
840
+ // ---------------------------------------------------------------------------
841
+ // Q1 — the shadow rollup (step 2.2)
842
+ // ---------------------------------------------------------------------------
843
+
844
+ /**
845
+ * One layer's Q1 inputs, kept as counts so the ratio is computed where it is
846
+ * read rather than stored.
847
+ *
848
+ * SPLIT BY LAYER BEFORE DETECTOR, and that order is the shape's whole point
849
+ * rather than a presentation choice. A `stop_hook_active` retry follows ANY
850
+ * stop concern's block — not only this gate's — so pooling the two layers
851
+ * divides another concern's retries into this gate's refusals and calls the
852
+ * result a re-refusal share. `ShadowRecord.retries_observed` is keyed the same
853
+ * way so the denominator splits with the numerator; an independent review
854
+ * found the first version pooling the denominator, which left the per-layer
855
+ * ratio uncomputable even though every row already carried its `layer`.
856
+ */
857
+ export interface ShadowLayerStats {
858
+ layer: ShadowLayer;
859
+ /**
860
+ * Retries observed on this layer across every readable record — the
861
+ * denominator, counting retries that came back CLEAN as well as refusing
862
+ * ones. A clean retry writes no row, so without this a zero numerator and
863
+ * "no retry happened" are the same empty list.
864
+ */
865
+ retries: number;
866
+ /** Shadow rows on this layer, by detector — the numerators. */
867
+ byDetector: DetectorCounts;
868
+ /**
869
+ * Rows on this layer, summed.
870
+ *
871
+ * MAY EXCEED `retries`, legitimately: one retry that would have been
872
+ * refused by two detectors writes two rows, and the demotion contract's
873
+ * attribution rule counts it once for each. A per-detector ratio stays
874
+ * within [0, 1]; this total is not a share and is printed as a count.
875
+ */
876
+ rows: number;
877
+ }
878
+
879
+ /**
880
+ * Rollup over every session's shadow record under a workspace.
881
+ *
882
+ * Two bounds travel with every number this carries. Both are properties of the
883
+ * instrument rather than of a particular reading, both are stated on
884
+ * `ShadowRecord`, and both are repeated here because a consumer reads this
885
+ * type and not that comment:
886
+ *
887
+ * - A retry whose transcript is unreadable or oversized records NOTHING —
888
+ * no row and no retry. The denominator shrinks with the numerator, so a
889
+ * Q1 read off this is an UPPER bound rather than a point estimate.
890
+ * - On a host that sends no `session_id`, every session shares one record
891
+ * and one counter. `files` below then counts FILES and not sessions, and
892
+ * a reader taking one for the other over-states the sample.
893
+ */
894
+ export interface ShadowStats {
895
+ /**
896
+ * Shadow FILES parsed. Deliberately not named `sessions`: one file is one
897
+ * session only on a host that sends a `session_id`.
898
+ */
899
+ files: number;
900
+ /** Files present but unparseable. A non-zero value makes every rate a floor. */
901
+ unreadable: number;
902
+ /**
903
+ * Rows dropped to `SHADOW_MAX_ROWS`, summed. Non-zero means the row arrays
904
+ * are samples of their own sessions and every numerator is a floor.
905
+ */
906
+ dropped: number;
907
+ byLayer: ShadowLayerStats[];
908
+ earliest: string | null;
909
+ latest: string | null;
910
+ }
911
+
912
+ /** A zeroed rollup, DERIVED from `SHADOW_LAYERS` for the reason `emptyCounts` gives. */
913
+ export function emptyShadowStats(): ShadowStats {
914
+ return {
915
+ files: 0,
916
+ unreadable: 0,
917
+ dropped: 0,
918
+ byLayer: SHADOW_LAYERS.map((layer) => ({
919
+ layer,
920
+ retries: 0,
921
+ byDetector: emptyCounts(),
922
+ rows: 0,
923
+ })),
924
+ earliest: null,
925
+ latest: null,
926
+ };
927
+ }
928
+
929
+ /**
930
+ * Aggregate every shadow record under a workspace.
931
+ *
932
+ * Reads through `parseShadowRecord`, never by hand: the gate writes these
933
+ * files, and a reader carrying its own idea of their shape is how a producer
934
+ * and a consumer drift apart — the exact seam the 2026-09-30 review round
935
+ * found uncrossed between the verification recorder and its classifier.
936
+ */
937
+ export function collectShadowStats(workspaceRoot: string): ShadowStats {
938
+ const dir = refusalStateDir(workspaceRoot);
939
+ const stats = emptyShadowStats();
940
+ const byLayer = new Map(stats.byLayer.map((b) => [b.layer, b]));
941
+ let entries: string[];
942
+ try {
943
+ entries = fs.readdirSync(dir);
944
+ } catch {
945
+ return stats;
946
+ }
947
+ for (const name of entries) {
948
+ if (!name.endsWith(SHADOW_SUFFIX)) continue;
949
+ let rec: ShadowRecord | null = null;
950
+ try {
951
+ rec = parseShadowRecord(fs.readFileSync(path.join(dir, name), 'utf-8'));
952
+ } catch {
953
+ rec = null;
954
+ }
955
+ if (rec === null) {
956
+ stats.unreadable += 1;
957
+ continue;
958
+ }
959
+ stats.files += 1;
960
+ stats.dropped += rec.dropped;
961
+ for (const layer of SHADOW_LAYERS) {
962
+ const bucket = byLayer.get(layer);
963
+ if (bucket !== undefined) bucket.retries += rec.retries_observed[layer] ?? 0;
964
+ }
965
+ for (const row of rec.would_refuse_again) {
966
+ const bucket = byLayer.get(row.layer);
967
+ if (bucket === undefined) continue;
968
+ bucket.byDetector[row.detector] += 1;
969
+ bucket.rows += 1;
970
+ }
971
+ // EMPTY STRINGS ARE SKIPPED, NOT COMPARED. `parseShadowRecord` coerces a
972
+ // missing `first_at` to `''`, and `'' < <any ISO stamp>` is true, so a
973
+ // record lacking the field would win the earliest comparison and the
974
+ // report would print an empty window start beside a real end. An absent
975
+ // timestamp is not an earlier one.
976
+ if (rec.first_at !== '' && (stats.earliest === null || rec.first_at < stats.earliest)) {
977
+ stats.earliest = rec.first_at;
978
+ }
979
+ if (rec.last_at !== '' && (stats.latest === null || rec.last_at > stats.latest)) {
980
+ stats.latest = rec.last_at;
981
+ }
982
+ }
983
+ return stats;
984
+ }
985
+
986
+ /**
987
+ * Detectors whose share may NOT be reported off a shadow rollup, because the
988
+ * record cannot tell a dispatch-suppressed retry from a clean one.
989
+ *
990
+ * `runDetectors` in `turn_end_gate_hook.ts` skips A, D and F when a subagent
991
+ * dispatch is open — they did not run, so the turn is not an observation of
992
+ * them staying silent. `recordShadow` then increments `retries_observed`
993
+ * unconditionally and writes no dispatch flag, so every dispatch-suppressed
994
+ * retry lands in those three denominators as a "would not have refused" that
995
+ * nobody observed. In a suite whose delegation policy dispatches by default
996
+ * that is not a rare branch.
997
+ *
998
+ * The demotion contract already forbids publishing it: *"A rollup that cannot
999
+ * tell a dispatch-suppressed turn from a clean one may not report Q2 for A or
1000
+ * D at all."* This set is that clause applied to the shadow rollup, extended to
1001
+ * F — the contract's attribution paragraph names only A and D because it
1002
+ * predates F, while its own § The six detectors table and the gate both gate
1003
+ * all three. The wider set is the safe reading of a censoring rule.
1004
+ *
1005
+ * Closing this needs a `dispatch_open` flag on the shadow row, which is a
1006
+ * PRODUCER change. Until then these three are censored rather than estimated.
1007
+ */
1008
+ export const DISPATCH_CENSORED_DETECTORS: ReadonlySet<RefusalDetectorId> =
1009
+ new Set<RefusalDetectorId>(['promissory', 'completion', 'untested']);
1010
+
1011
+ /**
1012
+ * The **retry-conditioned re-refusal share** for one detector on one layer, or
1013
+ * `null` when the layer observed no retry.
1014
+ *
1015
+ * ```
1016
+ * THIS IS NOT THE Q1 `turn-end-detector-demotion.md` REGISTERS BARS AGAINST.
1017
+ * NEVER COMPARE IT TO A BAR IN THAT TABLE.
1018
+ * ```
1019
+ *
1020
+ * The contract defines Q1 as *"of **a detector's** eligible initial refusals,
1021
+ * the share whose immediate retry is refused again by the same detector"*. Both
1022
+ * halves of that are conditioned on the detector. This function conditions
1023
+ * NEITHER: the denominator is every retry observed on the layer, whatever
1024
+ * refusal produced it, and the numerator is every row where this detector would
1025
+ * have fired, whatever refusal produced the retry.
1026
+ *
1027
+ * **They disagree in direction, so this is not a bound either.** Take ten
1028
+ * retries on a layer, nine following `verification` refusals and one following
1029
+ * a `language` refusal, with one `language` row. The contract's Q1 for B is
1030
+ * 1/1 = 100 % — bar crossed, a demotion study authorised. This function returns
1031
+ * 1/10 = 10 % — bar not crossed. Opposite verdicts on identical evidence. The
1032
+ * numerator can also run the other way, since a `language` row on a retry that
1033
+ * followed a `verification` refusal counts here and does not count there.
1034
+ *
1035
+ * **Why it is shaped this way, which is a producer limit rather than a choice.**
1036
+ * `ShadowRecord` carries no originating detector: `retries_observed` is keyed by
1037
+ * layer alone and a row records only what WOULD fire, never what did. The
1038
+ * session's `RefusalRecord` holds per-detector COUNTS for the whole session and
1039
+ * `refused_turn` for the last refused turn, so the per-turn origin needed to
1040
+ * condition this denominator is not on disk anywhere. Computing the registered
1041
+ * Q1 needs the producer to record it; that is named as the open residue on
1042
+ * `road-to-a-stop-that-holds` step 2.2 rather than papered over here.
1043
+ *
1044
+ * `null` RATHER THAN ZERO on an empty denominator, and that distinction is why
1045
+ * `retries_observed` exists at all: zero means "retries happened and this
1046
+ * detector would not have refused any of them", a finding about the detector,
1047
+ * while null means "nothing was observed", a finding about the sample.
1048
+ * Collapsing them publishes the second as the first.
1049
+ */
1050
+ export function retryConditionedShare(
1051
+ stats: ShadowStats,
1052
+ layer: ShadowLayer,
1053
+ detector: RefusalDetectorId,
1054
+ ): number | null {
1055
+ const bucket = stats.byLayer.find((b) => b.layer === layer);
1056
+ if (bucket === undefined || bucket.retries === 0) return null;
1057
+ return bucket.byDetector[detector] / bucket.retries;
1058
+ }