@event4u/agent-config 16.1.0 → 16.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (467) 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 +98 -257
  5. package/README.md +14 -12
  6. package/dist/agent-src/commands/analyze/inbox.md +1 -1
  7. package/dist/agent-src/commands/analyze/roadmap-repos.md +1 -1
  8. package/dist/agent-src/commands/bug/fix.md +17 -2
  9. package/dist/agent-src/commands/feature/plan.md +1 -1
  10. package/dist/agent-src/commands/feature/roadmap.md +1 -1
  11. package/dist/agent-src/commands/fix/ci.md +1 -1
  12. package/dist/agent-src/commands/implement-ticket.md +1 -1
  13. package/dist/agent-src/commands/jira-ticket.md +1 -1
  14. package/dist/agent-src/commands/roadmap/create.md +1 -1
  15. package/dist/agent-src/commands/roadmap/materialize.md +1 -1
  16. package/dist/agent-src/commands/roadmap/process-full.md +5 -5
  17. package/dist/agent-src/commands/roadmap/resolve-blockers.md +361 -0
  18. package/dist/agent-src/commands/roadmap/triage-parked.md +325 -0
  19. package/dist/agent-src/commands/roadmap.md +18 -4
  20. package/dist/agent-src/commands/tests/create.md +17 -3
  21. package/dist/agent-src/commands/tests/execute.md +22 -5
  22. package/dist/agent-src/commands/work.md +4 -0
  23. package/dist/agent-src/contexts/authority/decision-revisit-gate-enforcement.md +43 -0
  24. package/dist/agent-src/contexts/communication/rules-auto/design-fidelity-enforcement.md +24 -0
  25. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +6 -6
  26. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +4 -8
  27. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +32 -2
  28. package/dist/agent-src/contexts/execution/session-canary-enforcement-history.md +67 -0
  29. package/dist/agent-src/contexts/execution/toolchain-resolver.md +101 -7
  30. package/dist/agent-src/rules/active-remediation.md +1 -1
  31. package/dist/agent-src/rules/autonomous-execution.md +1 -0
  32. package/dist/agent-src/rules/broken-access-control.md +1 -0
  33. package/dist/agent-src/rules/command-suggestion-policy.md +1 -0
  34. package/dist/agent-src/rules/context-hygiene.md +1 -1
  35. package/dist/agent-src/rules/decision-revisit-gate.md +16 -33
  36. package/dist/agent-src/rules/design-fidelity.md +13 -16
  37. package/dist/agent-src/rules/doc-screenshot-hygiene.md +1 -0
  38. package/dist/agent-src/rules/domain-safety-disclaimer.md +1 -0
  39. package/dist/agent-src/rules/domain-safety-pii.md +1 -0
  40. package/dist/agent-src/rules/domain-safety-retention.md +1 -0
  41. package/dist/agent-src/rules/engineering-safety-floor.md +1 -0
  42. package/dist/agent-src/rules/evaluator-independence.md +3 -2
  43. package/dist/agent-src/rules/fast-path-marker-visibility.md +1 -0
  44. package/dist/agent-src/rules/finance-safety-floor.md +1 -0
  45. package/dist/agent-src/rules/git-history-discipline.md +1 -0
  46. package/dist/agent-src/rules/image-likeness-and-rights.md +1 -0
  47. package/dist/agent-src/rules/legal-safety-floor.md +3 -2
  48. package/dist/agent-src/rules/lethal-trifecta-guard.md +1 -0
  49. package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
  50. package/dist/agent-src/rules/media-governance-routing.md +14 -17
  51. package/dist/agent-src/rules/persona-governance.md +2 -2
  52. package/dist/agent-src/rules/provider-lifecycle-discipline.md +1 -1
  53. package/dist/agent-src/rules/question-not-instruction.md +1 -0
  54. package/dist/agent-src/rules/roadmap-progress-sync.md +1 -1
  55. package/dist/agent-src/rules/runtime-safety.md +1 -0
  56. package/dist/agent-src/rules/secret-vcs-guard.md +1 -0
  57. package/dist/agent-src/rules/security-sensitive-stop.md +3 -2
  58. package/dist/agent-src/rules/session-canary.md +8 -62
  59. package/dist/agent-src/rules/settings-ask-protocol.md +1 -0
  60. package/dist/agent-src/rules/source-confidentiality.md +1 -0
  61. package/dist/agent-src/rules/strategy-safety-floor.md +1 -0
  62. package/dist/agent-src/rules/tool-safety.md +1 -0
  63. package/dist/agent-src/rules/ui-audit-gate.md +1 -2
  64. package/dist/agent-src/rules/untrusted-input-defense.md +1 -0
  65. package/dist/agent-src/rules/user-interrupt-priority.md +1 -0
  66. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +115 -20
  67. package/dist/agent-src/scripts/deferral_carry.ts +339 -0
  68. package/dist/agent-src/scripts/update_roadmap_progress.ts +4 -1
  69. package/dist/agent-src/skills/accessibility-auditor/data/aria-patterns.csv +1 -1
  70. package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +2 -2
  71. package/dist/agent-src/skills/api-design/data/manifest.json +2 -2
  72. package/dist/agent-src/skills/api-testing/SKILL.md +17 -0
  73. package/dist/agent-src/skills/code-intelligence/SKILL.md +14 -2
  74. package/dist/agent-src/skills/design-review/SKILL.md +30 -4
  75. package/dist/agent-src/skills/design-review/references/verification-automation.md +32 -0
  76. package/dist/agent-src/skills/design-system-capture/references/import-procedure.md +1 -1
  77. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +18 -3
  78. package/dist/agent-src/skills/existing-ui-audit/references/component-taxonomy-detection.md +56 -0
  79. package/dist/agent-src/skills/fe-design/SKILL.md +1 -1
  80. package/dist/agent-src/skills/iconography/SKILL.md +1 -1
  81. package/dist/agent-src/skills/js-library-packaging/SKILL.md +2 -2
  82. package/dist/agent-src/skills/markitdown/SKILL.md +4 -0
  83. package/dist/agent-src/skills/monorepo-workspace/SKILL.md +1 -1
  84. package/dist/agent-src/skills/playwright-testing/SKILL.md +30 -6
  85. package/dist/agent-src/skills/quality-tools/SKILL.md +11 -1
  86. package/dist/agent-src/skills/quality-tools/references/go-tools.md +76 -0
  87. package/dist/agent-src/skills/quality-tools/references/python-tools.md +104 -0
  88. package/dist/agent-src/skills/review-routing/SKILL.md +2 -2
  89. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
  90. package/dist/agent-src/skills/storybook-workshop/SKILL.md +3 -3
  91. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
  92. package/dist/agent-src/skills/test-case-discovery/SKILL.md +7 -0
  93. package/dist/agent-src/skills/test-driven-development/SKILL.md +17 -0
  94. package/dist/agent-src/skills/test-performance/SKILL.md +19 -1
  95. package/dist/agent-src/skills/ui-component-architect/SKILL.md +25 -2
  96. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  97. package/dist/agent-src/templates/roadmaps.md +30 -0
  98. package/dist/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
  99. package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +302 -21
  100. package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
  101. package/dist/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
  102. package/dist/agent-src/templates/scripts/work_engine/stack/runner.ts +774 -109
  103. package/dist/agent-src/templates/scripts/work_engine/state.ts +57 -7
  104. package/dist/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
  105. package/dist/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
  106. package/dist/cli/python/knowledge_ingest.js +1 -1
  107. package/dist/cli/python/workspace_drive.js +1 -1
  108. package/dist/cli/registry.js +1 -1
  109. package/dist/cli-delegate/archive_completed_roadmaps.js +92 -12
  110. package/dist/cli-delegate/check_update_banner.js +12 -1
  111. package/dist/cli-delegate/chunk-523HCFYO.js +90 -0
  112. package/dist/cli-delegate/{chunk-M2DPGFZ4.js → chunk-6V6TA25H.js} +22 -152
  113. package/dist/cli-delegate/chunk-7IQE3D5S.js +191 -0
  114. package/dist/cli-delegate/{chunk-6KFEC3G7.js → chunk-CQWX336L.js} +1 -1
  115. package/dist/cli-delegate/chunk-DEIV7XP7.js +108 -0
  116. package/dist/cli-delegate/chunk-EEHDXSYD.js +2040 -0
  117. package/dist/cli-delegate/{chunk-P6ZHP5W2.js → chunk-GKE7ZVFA.js} +4 -4
  118. package/dist/cli-delegate/chunk-JQOM7TTM.js +1201 -0
  119. package/dist/cli-delegate/{chunk-UNCRJBT3.js → chunk-JQVYKIZA.js} +41 -0
  120. package/dist/cli-delegate/{chunk-FC75PGJE.js → chunk-JY2I2DXV.js} +12 -1
  121. package/dist/cli-delegate/chunk-KB5FZ655.js +208 -0
  122. package/dist/cli-delegate/{chunk-YUM5KURG.js → chunk-KR6EM23A.js} +235 -1455
  123. package/dist/cli-delegate/{chunk-LTYVIY76.js → chunk-LIYTLH6O.js} +1 -1
  124. package/dist/cli-delegate/chunk-NQ7WT43P.js +2729 -0
  125. package/dist/cli-delegate/chunk-NXEILF2K.js +2160 -0
  126. package/dist/cli-delegate/{chunk-5R7HOLAF.js → chunk-NYX3L2PZ.js} +1 -1
  127. package/dist/cli-delegate/{chunk-4E2W63DA.js → chunk-OKJUIAPS.js} +62 -1
  128. package/dist/cli-delegate/{chunk-QAJS5YWY.js → chunk-PHD7OOLE.js} +2 -1
  129. package/dist/cli-delegate/{chunk-RGNIWTEA.js → chunk-QLDHIPWR.js} +1 -1
  130. package/dist/cli-delegate/{chunk-HDVVGPQW.js → chunk-RQNNW7IF.js} +1405 -567
  131. package/dist/cli-delegate/{chunk-S3VFXMTM.js → chunk-S4UMZNES.js} +1 -1
  132. package/dist/cli-delegate/{chunk-6KM6ORRY.js → chunk-S4YWNRKO.js} +24 -8
  133. package/dist/cli-delegate/{chunk-GJCBB2UG.js → chunk-TRI6DQZ6.js} +3054 -325
  134. package/dist/cli-delegate/chunk-ULQRUR23.js +208 -0
  135. package/dist/cli-delegate/{chunk-NUHLI2RX.js → chunk-YCU3TIXH.js} +2 -0
  136. package/dist/cli-delegate/chunk-ZBZV2G5R.js +210 -0
  137. package/dist/cli-delegate/{chunk-WGXTVKCQ.js → chunk-ZUZKZYCN.js} +5 -3
  138. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  139. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  140. package/dist/cli-delegate/cmd_conformance.js +1862 -809
  141. package/dist/cli-delegate/cmd_doctor.js +13 -6
  142. package/dist/cli-delegate/cmd_doctor_neighbours.js +23 -0
  143. package/dist/cli-delegate/cmd_explain.js +5 -5
  144. package/dist/cli-delegate/cmd_export.js +11 -8
  145. package/dist/cli-delegate/cmd_fleet.js +3 -3
  146. package/dist/cli-delegate/cmd_handoff.js +10 -7
  147. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  148. package/dist/cli-delegate/cmd_memory_get.js +3 -1
  149. package/dist/cli-delegate/cmd_migrate.js +2 -2
  150. package/dist/cli-delegate/cmd_packs_active.js +3 -3
  151. package/dist/cli-delegate/cmd_preflight.js +6 -6
  152. package/dist/cli-delegate/cmd_prune.js +11 -8
  153. package/dist/cli-delegate/cmd_refresh.js +2 -1
  154. package/dist/cli-delegate/cmd_route_audit.js +8 -6
  155. package/dist/cli-delegate/cmd_route_explain.js +4 -4
  156. package/dist/cli-delegate/cmd_session_recycle.js +4 -3
  157. package/dist/cli-delegate/cmd_settings_get.js +1 -1
  158. package/dist/cli-delegate/cmd_settings_migrate.js +11 -8
  159. package/dist/cli-delegate/cmd_settings_set.js +4 -4
  160. package/dist/cli-delegate/cmd_sync.js +11 -8
  161. package/dist/cli-delegate/cmd_uninstall.js +11 -8
  162. package/dist/cli-delegate/cmd_update.js +1 -1
  163. package/dist/cli-delegate/cmd_upgrade.js +3 -2
  164. package/dist/cli-delegate/cmd_validate.js +11 -8
  165. package/dist/cli-delegate/cmd_versions.js +1 -1
  166. package/dist/cli-delegate/deferral_carry.js +16 -0
  167. package/dist/cli-delegate/gate_execute.js +2 -2
  168. package/dist/cli-delegate/roadmap_gates.js +5 -5
  169. package/dist/cli-delegate/update_roadmap_progress.js +1 -1
  170. package/dist/discovery/deprecation-report.md +1 -1
  171. package/dist/discovery/discovery-manifest.json +132 -80
  172. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  173. package/dist/discovery/discovery-manifest.summary.md +3 -3
  174. package/dist/discovery/dormancy-report.md +1 -1
  175. package/dist/discovery/orphan-report.md +1 -1
  176. package/dist/discovery/packs.json +7 -5
  177. package/dist/discovery/trust-report.md +2 -2
  178. package/dist/discovery/workspaces.json +6 -4
  179. package/dist/hooks/dispatch.js +7972 -5186
  180. package/dist/hooks/dispatch.sha256 +1 -0
  181. package/dist/install/claudePathsPlan.js +47 -6
  182. package/dist/install/claudeRuleRewrite.js +7 -0
  183. package/dist/install/emit_host_rules_cli.js +39 -36
  184. package/dist/install/globalRuleScope.js +87 -0
  185. package/dist/install/host_rule_ownership.js +160 -0
  186. package/dist/install/install.mjs +2226 -1055
  187. package/dist/install/installThinLayer.js +261 -0
  188. package/dist/install/installedRuleLinks.js +176 -0
  189. package/dist/install/wizard-plan.js +24 -0
  190. package/dist/mcp/registry-manifest.json +3 -3
  191. package/dist/mcp/server.json +2 -2
  192. package/dist/mcp/server.mjs +40913 -34865
  193. package/dist/scripts/_lib/agent_settings.js +11 -0
  194. package/dist/scripts/_lib/claude_settings_hooks.js +10 -8
  195. package/dist/scripts/_lib/installed_layer.js +378 -0
  196. package/dist/scripts/_lib/lean_projection_mode.js +218 -15
  197. package/dist/scripts/_lib/rule_consequence_class.js +45 -0
  198. package/dist/scripts/_lib/rule_injection.js +67 -10
  199. package/dist/scripts/_lib/rule_law_section.js +122 -0
  200. package/dist/scripts/_lib/thin_rules.js +458 -0
  201. package/dist/scripts/_lib/windsurf_render.js +60 -0
  202. package/dist/scripts/condense.js +14 -26
  203. package/dist/scripts/project_thin_rules.js +14 -294
  204. package/dist/server/schemas/settings.js +29 -0
  205. package/dist/shared/skillRanking.js +74 -13
  206. package/dist/ui/assets/index-Cwq4d0hX.js +23 -0
  207. package/dist/ui/assets/index-Cyjy8bWO.css +1 -0
  208. package/dist/ui/index.html +2 -2
  209. package/docs/contracts/settings-classes.md +10 -6
  210. package/package.json +4 -4
  211. package/src/agent-src/scripts/archive_completed_roadmaps.ts +115 -20
  212. package/src/agent-src/scripts/deferral_carry.ts +339 -0
  213. package/src/agent-src/scripts/update_roadmap_progress.ts +4 -1
  214. package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
  215. package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +302 -21
  216. package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
  217. package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
  218. package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +774 -109
  219. package/src/agent-src/templates/scripts/work_engine/state.ts +57 -7
  220. package/src/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
  221. package/src/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
  222. package/src/config/agent-settings.template.yml +91 -22
  223. package/src/config/ci-local-parity.yml +18 -0
  224. package/src/config/continuity-surface.json +9 -2
  225. package/src/config/evaluator-budgets.json +4 -4
  226. package/src/config/gate-coverage.yml +327 -3
  227. package/src/config/gate-violation-baselines.json +21 -9
  228. package/src/config/hook-bundle-budget.json +25 -0
  229. package/src/config/hook-latency-budget.json +21 -0
  230. package/src/config/hook-token-budget.json +8 -3
  231. package/src/config/host-instruction-limits.json +33 -0
  232. package/src/config/metric-registry.yml +114 -0
  233. package/src/config/placeholder-drift-budget.json +10 -0
  234. package/src/config/routing-coverage-seed.json +38 -3
  235. package/src/config/rule-activation-census.json +7 -3
  236. package/src/config/rule-consequence-class.json +161 -0
  237. package/src/config/rule-law-ceilings.json +99 -0
  238. package/src/install/claudePathsPlan.ts +46 -4
  239. package/src/install/claudeRuleRewrite.ts +10 -1
  240. package/src/install/emit_host_rules_cli.ts +43 -34
  241. package/src/install/globalRuleScope.ts +109 -0
  242. package/src/install/host_rule_ownership.ts +170 -0
  243. package/src/install/installThinLayer.ts +312 -0
  244. package/src/install/installedRuleLinks.ts +220 -0
  245. package/src/install/wizard-plan.ts +24 -0
  246. package/src/scripts/_cli/cmd_doctor.ts +28 -90
  247. package/src/scripts/_cli/cmd_doctor_neighbours.ts +293 -0
  248. package/src/scripts/_cli/doctor_execution.ts +100 -17
  249. package/src/scripts/_cli/doctor_network_posture.ts +226 -0
  250. package/src/scripts/_dispatch.bash +16 -2
  251. package/src/scripts/_lib/agent_settings.ts +11 -0
  252. package/src/scripts/_lib/body_portable.ts +102 -0
  253. package/src/scripts/_lib/cc_transcript.ts +132 -1
  254. package/src/scripts/_lib/claude_settings_hooks.ts +10 -8
  255. package/src/scripts/_lib/concern_sla_window.ts +200 -0
  256. package/src/scripts/_lib/estate_offsets.ts +241 -0
  257. package/src/scripts/_lib/exemption_shape.ts +143 -0
  258. package/src/scripts/_lib/fnv.ts +23 -0
  259. package/src/scripts/_lib/foreign_scan.ts +106 -0
  260. package/src/scripts/_lib/forge_protection.ts +114 -0
  261. package/src/scripts/_lib/forge_reader.ts +558 -0
  262. package/src/scripts/_lib/gitignore_block.ts +314 -0
  263. package/src/scripts/_lib/global_deploy_inventory.ts +32 -0
  264. package/src/scripts/_lib/graph_feeder_record.ts +214 -0
  265. package/src/scripts/_lib/hook_settings.ts +73 -0
  266. package/src/scripts/_lib/host_capability.ts +40 -19
  267. package/src/scripts/_lib/host_env_write.ts +111 -0
  268. package/src/scripts/_lib/host_hook_merge.ts +165 -0
  269. package/src/scripts/_lib/installed_layer.ts +538 -0
  270. package/src/scripts/_lib/json_merge.ts +82 -0
  271. package/src/scripts/_lib/json_pointers.ts +93 -11
  272. package/src/scripts/_lib/lean_projection_mode.ts +275 -15
  273. package/src/scripts/_lib/llm_proposer_transport.ts +14 -4
  274. package/src/scripts/_lib/machine_wake.ts +16 -3
  275. package/src/scripts/_lib/neighbour_census.ts +760 -0
  276. package/src/scripts/_lib/neighbour_scan.ts +122 -0
  277. package/src/scripts/_lib/neighbour_tool_use.ts +189 -0
  278. package/src/scripts/_lib/obligations.ts +119 -8
  279. package/src/scripts/_lib/probe_inputs.ts +271 -0
  280. package/src/scripts/_lib/prompt_shape.ts +46 -0
  281. package/src/scripts/_lib/read_surface_scan.ts +322 -0
  282. package/src/scripts/_lib/release_highlights.ts +2 -1
  283. package/src/scripts/_lib/release_migration_gate.ts +19 -3
  284. package/src/scripts/_lib/reserved_name_sweep.ts +73 -0
  285. package/src/scripts/_lib/retrieval_sanitize.ts +77 -16
  286. package/src/scripts/_lib/review_coverage.ts +134 -0
  287. package/src/scripts/_lib/rule_consequence_class.ts +71 -0
  288. package/src/scripts/_lib/rule_injection.ts +130 -10
  289. package/src/scripts/_lib/rule_law_section.ts +147 -0
  290. package/src/scripts/_lib/rule_layer_overlap.ts +58 -0
  291. package/src/scripts/_lib/shell_write_shapes.ts +108 -0
  292. package/src/scripts/_lib/skill_origin.ts +254 -0
  293. package/src/scripts/_lib/structural_hiding.ts +622 -0
  294. package/src/scripts/_lib/structured_ask.ts +64 -21
  295. package/src/scripts/_lib/thin_rules.ts +540 -0
  296. package/src/scripts/_lib/touched_file_quality.ts +434 -0
  297. package/src/scripts/_lib/turn_end_refusals.ts +600 -4
  298. package/src/scripts/_lib/turn_end_transcript.ts +216 -0
  299. package/src/scripts/_lib/verification_command.ts +324 -0
  300. package/src/scripts/_lib/verification_evidence.ts +647 -0
  301. package/src/scripts/_lib/verify_clause.ts +280 -0
  302. package/src/scripts/_lib/windsurf_render.ts +63 -0
  303. package/src/scripts/ai_council/cli_call_budget.ts +13 -5
  304. package/src/scripts/ai_council/clients.ts +26 -57
  305. package/src/scripts/ai_council/config.ts +1 -1
  306. package/src/scripts/ai_council/vendor_defaults.ts +45 -0
  307. package/src/scripts/ai_team/team_dispatch.ts +4 -0
  308. package/src/scripts/audit_skill_overlap.ts +261 -0
  309. package/src/scripts/before_complete_hook.ts +573 -25
  310. package/src/scripts/bench_hook_latency.ts +412 -5
  311. package/src/scripts/bench_touched_file_quality.ts +238 -0
  312. package/src/scripts/cache_realization_report.ts +61 -1
  313. package/src/scripts/check_argument_hint.ts +287 -0
  314. package/src/scripts/check_claims.ts +125 -1
  315. package/src/scripts/check_enforcement_coverage.ts +38 -9
  316. package/src/scripts/check_enforcement_matrix.ts +621 -22
  317. package/src/scripts/check_estate_count.ts +130 -153
  318. package/src/scripts/check_finding_dispositions.ts +138 -7
  319. package/src/scripts/check_generator_sync.ts +71 -0
  320. package/src/scripts/check_hook_bundle_composition.ts +370 -0
  321. package/src/scripts/check_host_docs_digest.ts +786 -0
  322. package/src/scripts/check_host_format_column.ts +559 -0
  323. package/src/scripts/check_input_references.ts +228 -0
  324. package/src/scripts/check_inputs_parity.ts +235 -0
  325. package/src/scripts/check_kernel_edit_ratified.ts +89 -9
  326. package/src/scripts/check_kill_switch_table.ts +294 -0
  327. package/src/scripts/check_memory.ts +7 -1
  328. package/src/scripts/check_placeholder_drift.ts +221 -0
  329. package/src/scripts/check_read_surface_coverage.ts +515 -0
  330. package/src/scripts/check_release_highlights.ts +10 -0
  331. package/src/scripts/check_release_holds.ts +2 -1
  332. package/src/scripts/check_release_pr_shape.ts +38 -15
  333. package/src/scripts/check_routing_coverage.ts +378 -11
  334. package/src/scripts/check_standing_rule_delivery.ts +33 -13
  335. package/src/scripts/check_static_parity.ts +17 -0
  336. package/src/scripts/check_verify_expectation_delta.ts +521 -0
  337. package/src/scripts/ci_settle.ts +145 -19
  338. package/src/scripts/closure_scan.ts +115 -1
  339. package/src/scripts/code_graph/detect.ts +127 -7
  340. package/src/scripts/code_graph/foreign.ts +194 -0
  341. package/src/scripts/code_graph/query.ts +217 -4
  342. package/src/scripts/code_graph/types.ts +26 -2
  343. package/src/scripts/code_graph/validate.ts +3 -1
  344. package/src/scripts/code_graph/verbs.ts +103 -0
  345. package/src/scripts/compile_hook_manifest.ts +90 -14
  346. package/src/scripts/condense.ts +14 -27
  347. package/src/scripts/estate_exemption_shape.ts +202 -0
  348. package/src/scripts/external_sources_denylist.json +29 -4
  349. package/src/scripts/generate_host_cost_table.ts +1 -1
  350. package/src/scripts/hook_manifest.json +1 -1
  351. package/src/scripts/hook_manifest.yaml +192 -14
  352. package/src/scripts/hooks/block_config_weakening.ts +294 -4
  353. package/src/scripts/hooks/block_kernel_rule_writes.ts +9 -78
  354. package/src/scripts/hooks/block_plumbing_writes.ts +342 -0
  355. package/src/scripts/hooks/bundle_integrity.ts +291 -0
  356. package/src/scripts/hooks/chain_nudge_hook.ts +1 -2
  357. package/src/scripts/hooks/code_graph_context_hook.ts +253 -25
  358. package/src/scripts/hooks/comment_discipline_hook.ts +1 -3
  359. package/src/scripts/hooks/concern_failure_policy.ts +280 -0
  360. package/src/scripts/hooks/concern_registry.ts +2 -0
  361. package/src/scripts/hooks/concern_timings.ts +94 -0
  362. package/src/scripts/hooks/delegation_nudge_hook.ts +1 -2
  363. package/src/scripts/hooks/design_pass_hook.ts +100 -9
  364. package/src/scripts/hooks/design_slop_hook.ts +1 -2
  365. package/src/scripts/hooks/dispatch_hook.ts +77 -71
  366. package/src/scripts/hooks/edit_shape_hook.ts +1 -2
  367. package/src/scripts/hooks/end_review_nudge_hook.ts +1 -1
  368. package/src/scripts/hooks/evidence_independence.ts +1 -2
  369. package/src/scripts/hooks/exit_codes.ts +129 -0
  370. package/src/scripts/hooks/host_lowering.json +1 -0
  371. package/src/scripts/hooks/host_lowering.ts +146 -3
  372. package/src/scripts/hooks/host_lowering.yaml +262 -46
  373. package/src/scripts/hooks/host_semantics.ts +30 -2
  374. package/src/scripts/hooks/injection_budget.ts +9 -2
  375. package/src/scripts/hooks/interruption_ledger_hook.ts +1 -2
  376. package/src/scripts/hooks/journal_record_hook.ts +1 -2
  377. package/src/scripts/hooks/memory_recall_hook.ts +1 -2
  378. package/src/scripts/hooks/obligation_settle_hook.ts +151 -5
  379. package/src/scripts/hooks/one_question_per_ask_hook.ts +43 -6
  380. package/src/scripts/hooks/orchestration_record_hook.ts +1 -2
  381. package/src/scripts/hooks/payload_stub.ts +10 -1
  382. package/src/scripts/hooks/permission_gate.ts +1 -2
  383. package/src/scripts/hooks/push_settle_hook.ts +1 -3
  384. package/src/scripts/hooks/replay_hook.ts +2 -1
  385. package/src/scripts/hooks/reread_guard_hook.ts +1 -2
  386. package/src/scripts/hooks/review_baseline_hook.ts +1 -2
  387. package/src/scripts/hooks/rtk_wrap_hook.ts +1 -2
  388. package/src/scripts/hooks/rule_inject_hook.ts +765 -68
  389. package/src/scripts/hooks/run_continuation_hook.ts +13 -15
  390. package/src/scripts/hooks/skill_route_hook.ts +1 -2
  391. package/src/scripts/hooks/source_first_gate_hook.ts +1 -2
  392. package/src/scripts/hooks/spawn_guard_shadow_hook.ts +1 -2
  393. package/src/scripts/hooks/subagent_ledger_hook.ts +1 -2
  394. package/src/scripts/hooks/suggestion_capture_hook.ts +1 -1
  395. package/src/scripts/hooks/table_fingerprint.ts +70 -0
  396. package/src/scripts/hooks/telemetry_flush_hook.ts +1 -2
  397. package/src/scripts/hooks/telemetry_usage_hook.ts +65 -10
  398. package/src/scripts/hooks/tool_result_bytes_hook.ts +93 -9
  399. package/src/scripts/hooks/turn_end_gate_hook.ts +421 -324
  400. package/src/scripts/hooks/ui_route_nudge_hook.ts +1 -2
  401. package/src/scripts/hooks_status.ts +112 -0
  402. package/src/scripts/injection_scan_hook.ts +7 -0
  403. package/src/scripts/install.ts +75 -189
  404. package/src/scripts/installed_layer_report.ts +103 -0
  405. package/src/scripts/language_mirror_hook.ts +15 -6
  406. package/src/scripts/learning_sidecar.ts +119 -8
  407. package/src/scripts/lint_deny_text.ts +419 -0
  408. package/src/scripts/lint_exit_codes.ts +262 -0
  409. package/src/scripts/lint_hook_manifest.ts +108 -0
  410. package/src/scripts/lint_major_migration_sections.ts +156 -26
  411. package/src/scripts/lint_pack_boundaries.ts +63 -5
  412. package/src/scripts/lint_roadmap_blockers.ts +78 -12
  413. package/src/scripts/lint_rule_law_section.ts +517 -0
  414. package/src/scripts/lint_skill_descriptions.ts +1 -1
  415. package/src/scripts/lint_skill_scripts_readonly.ts +1 -1
  416. package/src/scripts/lint_workflow_security.ts +161 -5
  417. package/src/scripts/mcp_server/consumer_tool_catalog.json +53 -0
  418. package/src/scripts/mcp_server/graph_tools.ts +72 -3
  419. package/src/scripts/mcp_server/prompts.ts +142 -1
  420. package/src/scripts/measure_routing_signal.ts +54 -16
  421. package/src/scripts/measure_skill_ranker_baseline.ts +515 -53
  422. package/src/scripts/measure_turn_end_gate.ts +193 -0
  423. package/src/scripts/memory_learn_hook.ts +91 -6
  424. package/src/scripts/memory_lookup.ts +1 -1
  425. package/src/scripts/model_rule_injection.ts +52 -5
  426. package/src/scripts/prepack-check.mjs +53 -0
  427. package/src/scripts/project_thin_rules.ts +34 -327
  428. package/src/scripts/release_publication.ts +13 -1
  429. package/src/scripts/report_evidence_temperature.ts +657 -0
  430. package/src/scripts/report_installed_rule_links.ts +157 -0
  431. package/src/scripts/report_invocation_surface.ts +383 -0
  432. package/src/scripts/report_label_agreement.ts +233 -0
  433. package/src/scripts/report_obligation_carriers.ts +699 -0
  434. package/src/scripts/report_obligation_mechanism.ts +230 -0
  435. package/src/scripts/report_obligation_writer_split.ts +236 -0
  436. package/src/scripts/report_single_token_triggers.ts +277 -0
  437. package/src/scripts/report_skill_menu_census.ts +208 -0
  438. package/src/scripts/report_skill_ranker_confusion.ts +442 -0
  439. package/src/scripts/report_standing_payload_by_host.ts +188 -15
  440. package/src/scripts/report_verification_record_coverage.ts +306 -0
  441. package/src/scripts/roadmap_verify_share.ts +213 -0
  442. package/src/scripts/rule_activation_census.ts +5 -2
  443. package/src/scripts/rule_trigger_eval.ts +9 -9
  444. package/src/scripts/schemas/command.schema.json +44 -0
  445. package/src/scripts/schemas/review-findings.schema.json +7 -2
  446. package/src/scripts/schemas/rule.schema.json +5 -0
  447. package/src/scripts/schemas/skill.schema.json +44 -0
  448. package/src/scripts/self_review_gate.ts +421 -17
  449. package/src/scripts/semantic_parity_census.ts +314 -0
  450. package/src/scripts/session_eol_report.ts +54 -1
  451. package/src/scripts/skill_tools/score_skill_relevance.ts +290 -25
  452. package/src/scripts/sweep_dead_scan_roots.ts +1 -1
  453. package/src/scripts/sweep_skill_ranker_signals.ts +129 -0
  454. package/src/scripts/sync_gitignore.ts +29 -235
  455. package/src/scripts/trigger_eval_grandfather.json +0 -7
  456. package/src/scripts/trigger_eval_rotation.ts +122 -27
  457. package/src/scripts/ui_conformance_probe.ts +156 -4
  458. package/src/scripts/update_prices.ts +85 -3
  459. package/src/scripts/write_bundle_digest.mjs +74 -0
  460. package/src/server/schemas/settings.ts +41 -0
  461. package/src/shared/skillRanking.ts +117 -11
  462. package/src/templates/consumer-settings/ONBOARDING.md +1 -0
  463. package/dist/cli-delegate/chunk-FXYUYU4N.js +0 -4416
  464. package/dist/cli-delegate/chunk-QJWAUMJW.js +0 -1072
  465. package/dist/cli-delegate/chunk-YDPILCOL.js +0 -152
  466. package/dist/ui/assets/index-DH1xc62v.js +0 -29
  467. package/dist/ui/assets/index-jSfMW8_t.css +0 -1
@@ -0,0 +1,558 @@
1
+ /**
2
+ * The live forge read behind `doctor --json`'s `forge_protection` block.
3
+ *
4
+ * `road-to-adversarial-verification-and-long-runs` AC-5. Phase 3.2 shipped a
5
+ * correct pure mapper (`forge_protection.ts`) and no production caller, so
6
+ * `agent-config doctor --json` — the command the criterion names — reported five
7
+ * `unread` rows while the forge satisfied all five. The rows were established by
8
+ * a human running `gh api` by hand and reading the mapper's output in a test.
9
+ * That is the exact substitution this roadmap's Phase 3 exists to forbid: a
10
+ * mechanical check replaced by a person's reading of the same evidence.
11
+ *
12
+ * **This IS a reversal of Phase 3.2's recorded decision, and the roadmap carries
13
+ * the supersession.** An earlier draft of this header argued the opposite — that
14
+ * `decision-revisit-gate`'s mechanism-match test fails, so no lock applies — and
15
+ * a 2/2 convergent council pass refuted it: that note recorded TWO boundaries,
16
+ * `doctor` does not reach the network AND the reading is injected, and moving
17
+ * `gh api` in here reverses both. The refuted reasoning is corrected rather than
18
+ * left standing, for the reason the sibling docblock gives: a module header is
19
+ * what the next reader meets first, so it is where a dead argument gets reused.
20
+ *
21
+ * What survives is the narrower AVAILABILITY property the supersession rests on:
22
+ * every failure path returns {@link UNREAD_FORGE}, so offline the rows, their
23
+ * `source` templates and the action lines are what Phase 3.2 printed — no error,
24
+ * no hang. Not the whole document: the block gained a top-level `repository`
25
+ * key, `null` on every such path.
26
+ *
27
+ * **The degradation is the load-bearing claim, so it is tested rather than
28
+ * asserted.** Six distinct failures — no repository, a dead repo record, a dead
29
+ * ruleset detail, a dead environments call, a non-boolean flag, a throwing API —
30
+ * each land on `unread` for the rows they touch and leave the others alone. Two
31
+ * of those directions have a more obvious wrong implementation than the right
32
+ * one, and both are pinned: a PARTIAL ruleset list (understates protection in
33
+ * one direction, overstates it in the other), and ALL-OR-NOTHING across surfaces
34
+ * that do not depend on each other (a dead environments call says nothing about
35
+ * whether a ruleset protects the branch).
36
+ *
37
+ * The network lives here; the judgement lives in `forge_protection.ts`. Both
38
+ * polarities stay testable offline because the API is injected.
39
+ */
40
+
41
+ import { spawnSync } from 'node:child_process';
42
+
43
+ import {
44
+ UNREAD_FORGE,
45
+ deployRestrictedFrom,
46
+ type EnvironmentPolicy,
47
+ type ForgeReading,
48
+ } from './forge_protection.js';
49
+ import type { RulesetDetail } from './platform_anchor.js';
50
+
51
+ /**
52
+ * One forge API call. `null` on ANY failure — exit status, empty output,
53
+ * unparseable JSON, timeout, missing binary, missing credentials.
54
+ *
55
+ * Injected rather than imported so every failure above is reachable from a test
56
+ * without a network, which is what makes the degradation claim checkable.
57
+ */
58
+ export interface ForgeApi {
59
+ get(apiPath: string, paginate?: boolean): unknown | null;
60
+ }
61
+
62
+ /**
63
+ * Per-call ceiling.
64
+ *
65
+ * A diagnostic that hangs is worse than one that reports `unread`: the reader
66
+ * learns nothing either way, and only one of the two costs them the terminal.
67
+ * Ten seconds is a stated default rather than a measured optimum — it is
68
+ * generous for a `gh api` round trip and short enough that five of them stay
69
+ * inside the patience budget of a command people run to get unstuck.
70
+ */
71
+ export const FORGE_CALL_TIMEOUT_MS = 10_000;
72
+
73
+ /** Ceiling on the one `git` spawn, which sits inside the whole-read budget. */
74
+ export const GIT_REMOTE_TIMEOUT_MS = 2_000;
75
+
76
+ /**
77
+ * Ceiling on the WHOLE read, not on one call.
78
+ *
79
+ * The call count is `3 + one per ruleset + one per environment carrying custom
80
+ * branch policies` — it is NOT a fixed five, and sizing the budget against a
81
+ * fixed five was the defect an independent review named. On a repository with
82
+ * many rulesets the budget is what stops the read, and the rows it did not
83
+ * reach report `unread`, which is the honest answer rather than a wrong one.
84
+ *
85
+ * Fifteen seconds is a stated default, not a measured optimum. It is enforced
86
+ * as a real ceiling rather than only at admission: {@link budgetOf} returns the
87
+ * milliseconds left, {@link liveForgeApi} takes that as its `timeoutFor` and
88
+ * caps each spawn at `min(FORGE_CALL_TIMEOUT_MS, remaining)`, and
89
+ * {@link withDeadline} refuses to dispatch once it reaches zero. So a call
90
+ * admitted near the deadline cannot run past it.
91
+ */
92
+ export const FORGE_TOTAL_BUDGET_MS = 15_000;
93
+
94
+ /**
95
+ * Environment switches that skip the read entirely, before any process spawns.
96
+ *
97
+ * Both are read as the literal `1`, which is the contract `cmd_versions.ts` and
98
+ * `cmd_update.ts` already use for `AGENT_CONFIG_OFFLINE`. Accepting any
99
+ * non-empty value here would give one variable two opposite meanings inside one
100
+ * binary — `AGENT_CONFIG_OFFLINE=0` would mean online to `update` and offline
101
+ * to `doctor` — and the surprising direction is the one a user reads as "off".
102
+ */
103
+ export const NO_FORGE_ENV = ['AGENT_CONFIG_DOCTOR_NO_FORGE', 'AGENT_CONFIG_OFFLINE'] as const;
104
+
105
+ /** `true` when a switch in {@link NO_FORGE_ENV} is set to the literal `1`. */
106
+ export function forgeReadDisabled(env: Readonly<Record<string, string | undefined>>): boolean {
107
+ return NO_FORGE_ENV.some((k) => env[k] === '1');
108
+ }
109
+
110
+ function asObject(v: unknown): Record<string, unknown> | null {
111
+ return v !== null && typeof v === 'object' && !Array.isArray(v)
112
+ ? (v as Record<string, unknown>)
113
+ : null;
114
+ }
115
+
116
+ /** What a spawn returns, narrowed to the two fields this module reads. */
117
+ export interface SpawnResult {
118
+ readonly status: number | null;
119
+ readonly stdout?: string | undefined;
120
+ }
121
+
122
+ /** A subprocess runner. Injected so every failure branch is testable offline. */
123
+ export type Runner = (
124
+ cmd: string,
125
+ args: readonly string[],
126
+ timeoutMs: number,
127
+ cwd?: string,
128
+ ) => SpawnResult;
129
+
130
+ const defaultRunner: Runner = (cmd, args, timeoutMs, cwd) =>
131
+ spawnSync(cmd, [...args], {
132
+ encoding: 'utf8',
133
+ timeout: timeoutMs,
134
+ maxBuffer: 32 * 1024 * 1024,
135
+ ...(cwd === undefined ? {} : { cwd }),
136
+ });
137
+
138
+ /**
139
+ * The `timeout` for one call, given the milliseconds left of the whole budget.
140
+ *
141
+ * **Two Node behaviours meet here, and both bite in the same place.**
142
+ * `spawnSync`'s `timeout` must be an INTEGER — `budgetOf` reads
143
+ * `performance.now()`, which is fractional, so an unfloored value threw
144
+ * `ERR_OUT_OF_RANGE`, was swallowed, and the read degraded with no subprocess
145
+ * started. And `timeout: 0` installs NO kill timer at all, so flooring alone
146
+ * turned a sub-millisecond remainder into an UNBOUNDED spawn — the ceiling
147
+ * failing open in exactly the budget-exhaustion case it exists for, which is
148
+ * the worse of the two directions.
149
+ *
150
+ * So the floor is 1 ms, not 0: a call admitted with a sliver of budget left is
151
+ * started and killed almost immediately, which lands on `unread` the way every
152
+ * other failure does. Never `0`, never fractional.
153
+ */
154
+ export function callTimeout(remainingMs: number): number {
155
+ return Math.max(1, Math.floor(Math.min(FORGE_CALL_TIMEOUT_MS, remainingMs)));
156
+ }
157
+
158
+ /** The repository to read, and the host it lives on. */
159
+ export interface ForgeTarget {
160
+ readonly host: string;
161
+ readonly slug: string;
162
+ }
163
+
164
+ /**
165
+ * The live `gh api` caller.
166
+ *
167
+ * `timeoutFor` returns the ceiling for THIS call, so the whole-read budget can
168
+ * shorten it. A fixed per-call timeout is what let a call admitted just inside
169
+ * the budget run well past it — the budget checked admission and nothing
170
+ * checked duration.
171
+ *
172
+ * `--paginate --slurp` needs `gh` 2.43 or newer. On an older client the call
173
+ * fails and the rulesets read blanks, which lands on `unread` rather than on a
174
+ * wrong answer — the degradation this module guarantees, reached by a cause it
175
+ * cannot name. Stated here because nothing else in the diff documents it.
176
+ */
177
+ export function liveForgeApi(
178
+ timeoutFor: () => number = () => FORGE_CALL_TIMEOUT_MS,
179
+ run: Runner = defaultRunner,
180
+ where: { readonly host?: string; readonly cwd?: string } = {},
181
+ ): ForgeApi {
182
+ // **`gh` is addressed at the host and directory the SLUG came from.** The
183
+ // slug is resolved with `cwd: root`, and a `gh` spawned with neither
184
+ // inherits the process cwd and gh's default host — so with `--project`,
185
+ // `--root` or `AGENT_CONFIG_PROJECT_ROOT` pointing elsewhere, or with a
186
+ // GitHub Enterprise remote, the two halves of the read addressed two
187
+ // different repositories and a same-named repo elsewhere produced a
188
+ // confident verdict about somebody else's project.
189
+ const hostArgs =
190
+ where.host === undefined || where.host.toLowerCase() === 'github.com'
191
+ ? []
192
+ : ['--hostname', where.host];
193
+ return {
194
+ get(apiPath: string, paginate = false): unknown | null {
195
+ const budget = timeoutFor();
196
+ if (budget <= 0) return null;
197
+ const args = paginate
198
+ ? ['api', '--paginate', '--slurp', ...hostArgs, apiPath]
199
+ : ['api', ...hostArgs, apiPath];
200
+ let r: SpawnResult;
201
+ try {
202
+ r = run('gh', args, callTimeout(budget), where.cwd);
203
+ } catch {
204
+ return null;
205
+ }
206
+ if (r.status !== 0 || typeof r.stdout !== 'string' || r.stdout.trim() === '') {
207
+ return null;
208
+ }
209
+ try {
210
+ return JSON.parse(r.stdout) as unknown;
211
+ } catch {
212
+ return null;
213
+ }
214
+ },
215
+ };
216
+ }
217
+
218
+ /**
219
+ * `owner/repo` from a git remote URL, or `null`.
220
+ *
221
+ * **Host-gated on purpose.** `gh api` speaks GitHub, so attempting it against a
222
+ * GitLab or Bitbucket remote buys a doomed subprocess on every `doctor` run and
223
+ * returns the same `unread` the skip returns for free. The gate is the literal
224
+ * `github` in the host, which covers `github.com` and the usual GitHub
225
+ * Enterprise naming. A GHES install on a host that does not carry the vendor
226
+ * name is skipped and reports `unread` — the safe degradation, and stated here
227
+ * rather than left for a reader to discover from a blank row.
228
+ */
229
+ export function resolveForgeRepo(originUrl: string | null): ForgeTarget | null {
230
+ if (originUrl === null || originUrl.trim() === '') return null;
231
+ const url = originUrl.trim();
232
+ const m = /^(?:[A-Za-z][A-Za-z0-9+.-]*:\/\/)?(?:[^@/]+@)?([^/:]+)[:/]([^/:]+\/[^/]+?)(?:\.git)?$/.exec(
233
+ url,
234
+ );
235
+ const host = m?.[1];
236
+ const slug = m?.[2];
237
+ if (host === undefined || slug === undefined) return null;
238
+ if (!host.toLowerCase().includes('github')) return null;
239
+ // The slug is interpolated into an API path and used as a replacement
240
+ // string, so its character set is checked rather than assumed. The previous
241
+ // pattern admitted `$`, `?`, `#` and `..` — one of those is a path
242
+ // traversal and another is a `String.replace` control sequence.
243
+ if (!/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(slug)) return null;
244
+ if (slug.split('/').some((seg) => seg === '.' || seg === '..')) return null;
245
+ return { host, slug };
246
+ }
247
+
248
+ /**
249
+ * The repository's `origin` URL, or `null`.
250
+ *
251
+ * Timed out like every other spawn here. It was the one subprocess in the
252
+ * module with no ceiling and it ran before the budget was even constructed, so
253
+ * a wedged `git` sat entirely outside a read that advertised a total ceiling.
254
+ */
255
+ export function originUrl(
256
+ root: string,
257
+ run: Runner = defaultRunner,
258
+ timeoutMs = GIT_REMOTE_TIMEOUT_MS,
259
+ ): string | null {
260
+ let r: SpawnResult;
261
+ try {
262
+ // `ls-remote --get-url` APPLIES `url.<base>.insteadOf` rewrites and still
263
+ // makes no network call; `remote get-url` returns the configured URL
264
+ // verbatim. The difference matters in the unsafe direction: a
265
+ // github.com-shaped URL rewritten onto another host would otherwise be
266
+ // accepted and the read attributed to the wrong repository — which is a
267
+ // condition the Phase 3.2 amendment names as reversing it.
268
+ r = run('git', ['ls-remote', '--get-url', 'origin'], timeoutMs, root);
269
+ } catch {
270
+ return null;
271
+ }
272
+ if (r.status !== 0 || typeof r.stdout !== 'string') return null;
273
+ const out = r.stdout.trim();
274
+ // With no such remote, `ls-remote --get-url` echoes the name back.
275
+ return out === '' || out === 'origin' ? null : out;
276
+ }
277
+
278
+ /**
279
+ * Every ruleset, with its detail, or `null`.
280
+ *
281
+ * The listing endpoint returns summaries without `rules`, so each is fetched
282
+ * individually — and a single failed detail read blanks the WHOLE list rather
283
+ * than returning what was collected. Same contract as the sibling anchor
284
+ * source, for the same reason it states: a subset understates the effective
285
+ * protection in one direction and overstates it in the other, and neither is a
286
+ * verdict worth recording. `--paginate --slurp` because the listing pages at 30
287
+ * and a silently truncated reading is that same partial verdict wearing a
288
+ * different cause.
289
+ */
290
+ function readRulesets(repo: string, api: ForgeApi): RulesetDetail[] | null {
291
+ // One flattener for the one `--paginate --slurp` contract. This read grew
292
+ // its own inline version first, which left two implementations of the same
293
+ // page shape free to drift apart.
294
+ const list = slurped(api.get(`repos/${repo}/rulesets`, true), 'rulesets');
295
+ if (list === null) return null;
296
+ const out: RulesetDetail[] = [];
297
+ for (const entry of list) {
298
+ const id = asObject(entry)?.['id'];
299
+ if (typeof id !== 'number') return null;
300
+ const detail = api.get(`repos/${repo}/rulesets/${String(id)}`);
301
+ if (asObject(detail) === null) return null;
302
+ out.push(detail as RulesetDetail);
303
+ }
304
+ return out;
305
+ }
306
+
307
+ /**
308
+ * Flatten a `--paginate --slurp` payload down to one list.
309
+ *
310
+ * `--slurp` yields an array of PAGES. Each page is either a bare array (a list
311
+ * endpoint) or an object carrying the list under `key` (`environments`,
312
+ * `branch_policies`). `null` when the shape is not one of those — never a
313
+ * partial list, for the reason {@link readRulesets} states.
314
+ */
315
+ function slurped(payload: unknown, key: string): unknown[] | null {
316
+ if (Array.isArray(payload)) {
317
+ // An array of pages, each itself an array — a list endpoint under
318
+ // `--slurp`.
319
+ if (payload.length > 0 && payload.every((p) => Array.isArray(p))) {
320
+ return (payload as unknown[][]).flat();
321
+ }
322
+ // An array of pages, each an object carrying the list under `key`.
323
+ if (payload.length > 0 && payload.every((p) => Array.isArray(asObject(p)?.[key]))) {
324
+ return payload.flatMap((p) => asObject(p)?.[key] as unknown[]);
325
+ }
326
+ // A flat list of items — what a list endpoint returns UNPAGINATED, and
327
+ // what `gh` emits when a single page is not wrapped. Kept because
328
+ // collapsing it into the page shapes above was what broke the flat
329
+ // case when the two flatteners were merged.
330
+ return payload;
331
+ }
332
+ const listed = asObject(payload)?.[key];
333
+ return Array.isArray(listed) ? [...listed] : null;
334
+ }
335
+
336
+ /**
337
+ * Whether every environment restricts its deployment branches, or `null`.
338
+ *
339
+ * The custom-policy NAMES live behind a second call per environment, and a
340
+ * policy pattern of `*` restricts nothing whatever the flag says. A failed
341
+ * names read therefore makes the whole row `unread`, and that is a CORRECTION:
342
+ * the first draft fell back to trusting the flag, which `deployRestrictedFrom`
343
+ * calls the guarantee that WAS NOT CHECKED — the weaker reading, not the
344
+ * narrower one. An independent review followed the consequence through: an
345
+ * environment whose real policy is `*` would then report `satisfied`, which is
346
+ * the overstatement direction this module says it never takes, and it would be
347
+ * indistinguishable in the output from a genuine restriction. A read that
348
+ * failed is a read nobody made, so it lands where every other unmade read
349
+ * lands.
350
+ *
351
+ * The environment name is URL-ENCODED into the path. GitHub admits spaces and
352
+ * other path-unsafe characters in an environment name, and an unencoded name
353
+ * is exactly how the failed-read branch above gets reached by accident.
354
+ */
355
+ function readDeployRestricted(repo: string, api: ForgeApi): boolean | null {
356
+ // PAGINATED. Unpaginated, a repository with more environments than one page
357
+ // returns a subset, `deployRestrictedFrom`'s `every()` runs over what it can
358
+ // see, and an unlisted unrestricted environment reads as `satisfied` — the
359
+ // overstatement direction this module says it never takes. `readRulesets`
360
+ // guards the identical hazard one function up.
361
+ const envsRaw = slurped(api.get(`repos/${repo}/environments`, true), 'environments');
362
+ if (envsRaw === null) return null;
363
+ const envs: EnvironmentPolicy[] = [];
364
+ const patternsByEnv: Record<string, readonly string[]> = {};
365
+ for (const raw of envsRaw) {
366
+ const env = asObject(raw);
367
+ const name = env?.['name'];
368
+ if (typeof name !== 'string') return null;
369
+ const policy = asObject(env?.['deployment_branch_policy']);
370
+ // A flag PRESENT but not a boolean makes the whole row `unread`. The
371
+ // first version omitted it, and the comment justified that with
372
+ // "`deployRestrictedFrom` distinguishes absent from false" — which is
373
+ // simply wrong: that function tests `=== true` and `!== true`, so
374
+ // absent, `undefined` and `false` collapse identically. The effect was
375
+ // the row rendering "at least one environment accepts a deployment from
376
+ // any branch" — a positive claim about the forge built out of data
377
+ // nobody could parse. Every sibling unparseable input here already
378
+ // takes the null route: a non-string environment name, a non-numeric
379
+ // ruleset id, a non-boolean `allow_auto_merge`, a non-string
380
+ // branch-policy name. An ABSENT flag is a different input and stays
381
+ // absent.
382
+ for (const k of ['protected_branches', 'custom_branch_policies'] as const) {
383
+ const v = policy?.[k];
384
+ if (v !== undefined && typeof v !== 'boolean') return null;
385
+ }
386
+ envs.push({
387
+ name,
388
+ deployment_branch_policy:
389
+ policy === null
390
+ ? null
391
+ : {
392
+ ...(typeof policy['protected_branches'] === 'boolean'
393
+ ? { protected_branches: policy['protected_branches'] }
394
+ : {}),
395
+ ...(typeof policy['custom_branch_policies'] === 'boolean'
396
+ ? { custom_branch_policies: policy['custom_branch_policies'] }
397
+ : {}),
398
+ },
399
+ });
400
+ if (policy?.['custom_branch_policies'] !== true) continue;
401
+ // `protected_branches` already decides the row for this environment —
402
+ // `deployRestrictedFrom` short-circuits on it before consulting the
403
+ // pattern map — so the extra call would spend budget on an answer that
404
+ // cannot change the verdict, and degrade the row to `unread` if it
405
+ // failed.
406
+ if (policy['protected_branches'] === true) continue;
407
+ const names = api.get(
408
+ `repos/${repo}/environments/${encodeURIComponent(name)}/deployment-branch-policies`,
409
+ true,
410
+ );
411
+ const listed = slurped(names, 'branch_policies');
412
+ // A read that failed is a read nobody made — `unread`, never a trusted
413
+ // flag. Trusting it here is how a wildcard policy reports `satisfied`.
414
+ if (listed === null) return null;
415
+ const patterns = listed.map((p) => asObject(p)?.['name']);
416
+ // A non-string pattern name makes the row `unread`, not `false`.
417
+ // Filtering it away silently left an EMPTY pattern list, which
418
+ // `deployRestrictedFrom` maps to `false` — so the row would render
419
+ // "at least one environment accepts a deployment from any branch",
420
+ // a positive claim about the forge derived from data nobody could
421
+ // parse. Every sibling field here already takes the null route: a
422
+ // non-string environment name, a non-numeric ruleset id, a non-boolean
423
+ // `allow_auto_merge`. A confirmed-empty list is a different input and
424
+ // keeps its own `false`.
425
+ if (patterns.some((n) => typeof n !== 'string')) return null;
426
+ patternsByEnv[name] = patterns as string[];
427
+ }
428
+ return deployRestrictedFrom(envs, patternsByEnv);
429
+ }
430
+
431
+ /**
432
+ * Read the four fields the mapper consumes.
433
+ *
434
+ * The repository record is first and GATES the rest: it carries two of the four
435
+ * fields, and the other two are meaningless without the default branch it
436
+ * names. A reader that kept going after it failed would spend calls on answers
437
+ * it could not use, so this one returns immediately — asserted by a test over
438
+ * the call list rather than over the returned value, since both shapes return
439
+ * the same reading.
440
+ */
441
+ export function readForge(repo: string, api: ForgeApi): ForgeReading {
442
+ const record = asObject(api.get(`repos/${repo}`));
443
+ if (record === null) return UNREAD_FORGE;
444
+ const branch = record['default_branch'];
445
+ const autoMerge = record['allow_auto_merge'];
446
+ return {
447
+ defaultBranch: typeof branch === 'string' && branch !== '' ? branch : null,
448
+ // Never coerced: a flag the payload did not carry as a boolean is a flag
449
+ // nobody read, and `unread` is the state reserved for exactly that.
450
+ allowAutoMerge: typeof autoMerge === 'boolean' ? autoMerge : null,
451
+ rulesets: readRulesets(repo, api),
452
+ deployRestricted: readDeployRestricted(repo, api),
453
+ };
454
+ }
455
+
456
+ /**
457
+ * Wrap an API in a whole-command budget.
458
+ *
459
+ * **Per-call timeouts bound each call and nothing bounds their sum.** Five
460
+ * calls at a ten-second ceiling can take fifty seconds without any one of them
461
+ * misbehaving, so the advertised ceiling is not the one a user experiences.
462
+ * This adds the missing deadline, and it does so by returning `null` once the
463
+ * budget is spent — which routes straight into the `unread` degradation the
464
+ * rest of this module already guarantees. Running out of time therefore reads
465
+ * as *nobody looked*, never as *the forge said no*; that direction is the one
466
+ * that matters and it is pinned by a test.
467
+ */
468
+ export function withDeadline(api: ForgeApi, remaining: () => number): ForgeApi {
469
+ return {
470
+ get(apiPath: string, paginate?: boolean): unknown | null {
471
+ return remaining() <= 0 ? null : api.get(apiPath, paginate);
472
+ },
473
+ };
474
+ }
475
+
476
+ /**
477
+ * Milliseconds left of a budget of `ms`, never below zero.
478
+ *
479
+ * The clock is `performance.now`, which is monotonic. `Date.now` is wall-clock
480
+ * and steppable: an NTP correction backwards silently extends the budget past
481
+ * its stated ceiling and one forwards expires it early. The word was in the
482
+ * first draft's comment while the implementation used the steppable clock.
483
+ */
484
+ export function budgetOf(
485
+ ms: number,
486
+ now: () => number = () => performance.now(),
487
+ ): () => number {
488
+ const until = now() + ms;
489
+ return () => Math.max(0, until - now());
490
+ }
491
+
492
+ /** What {@link forgeReadingFor} needs. Every field injectable, so no test spawns. */
493
+ export interface ForgeReadRequest {
494
+ readonly env: Readonly<Record<string, string | undefined>>;
495
+ /**
496
+ * Resolve the repository — a THUNK rather than a value, so the opt-out can
497
+ * run before it. Resolving reads the git remote, which is a subprocess; an
498
+ * opt-out that only skipped the API calls would still spawn `git` to work
499
+ * out a repository it was never going to use, and "this switch makes no
500
+ * network calls" would be true of the HTTP traffic and false of the claim
501
+ * a reader takes from it.
502
+ */
503
+ readonly resolveRepo: () => ForgeTarget | null;
504
+ /**
505
+ * Build the API for the resolved target.
506
+ *
507
+ * A FACTORY rather than a ready API, so the host and working directory the
508
+ * slug came from necessarily reach `gh`. Handing in a pre-built client was
509
+ * how the two halves of the read came to address different repositories.
510
+ */
511
+ readonly apiFor: (target: ForgeTarget) => ForgeApi;
512
+ /** Milliseconds left. Defaults to a {@link FORGE_TOTAL_BUDGET_MS} budget. */
513
+ readonly remaining?: () => number;
514
+ }
515
+
516
+ /**
517
+ * A reading plus the repository it came from.
518
+ *
519
+ * **The slug travels with the rows, and leaving it out was the review's one
520
+ * high finding.** The moment the read went live, *which repository* stopped
521
+ * being obvious: `origin` may point at a fork, a mirror, or — in a consumer
522
+ * install — somebody else's project entirely, and five rows reading `satisfied`
523
+ * say nothing about whose branch is protected. For a block whose own header
524
+ * calls an unstated provenance the guess it exists to refuse, that was the one
525
+ * fact the provenance omitted. `null` when nothing was read.
526
+ */
527
+ export interface ForgeRead {
528
+ readonly repo: string | null;
529
+ readonly reading: ForgeReading;
530
+ }
531
+
532
+ /**
533
+ * The production entry: read the forge, or return the unread reading.
534
+ *
535
+ * Three skips happen BEFORE any process spawns — an opt-out switch, the general
536
+ * offline switch, and an unresolvable repository — and each is asserted on the
537
+ * CALL LIST rather than on the return value, because a reader that queried and
538
+ * then discarded would satisfy an output-only assertion while still paying the
539
+ * latency the opt-out exists to avoid.
540
+ *
541
+ * The `catch` is not defensive padding. `doctor` is the command people run when
542
+ * something is already wrong; a diagnostic that dies because its own optional
543
+ * read threw is strictly worse than one reporting `unread`.
544
+ */
545
+ export function forgeReadingFor(req: ForgeReadRequest): ForgeRead {
546
+ if (forgeReadDisabled(req.env)) return { repo: null, reading: UNREAD_FORGE };
547
+ // The budget is built BEFORE the repository is resolved, because resolving
548
+ // spawns `git` and a budget that starts after it does not bound it.
549
+ const remaining = req.remaining ?? budgetOf(FORGE_TOTAL_BUDGET_MS);
550
+ try {
551
+ const target = req.resolveRepo();
552
+ if (target === null) return { repo: null, reading: UNREAD_FORGE };
553
+ const reading = readForge(target.slug, withDeadline(req.apiFor(target), remaining));
554
+ return { repo: target.slug, reading };
555
+ } catch {
556
+ return { repo: null, reading: UNREAD_FORGE };
557
+ }
558
+ }