@event4u/agent-config 8.13.0 → 9.1.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 (316) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +183 -0
  3. package/MIGRATION.md +14 -0
  4. package/README.md +2 -2
  5. package/dist/agent-src/commands/agent-handoff.md +35 -0
  6. package/dist/agent-src/commands/agents/init.md +1 -0
  7. package/dist/agent-src/commands/agents/optimize.md +1 -0
  8. package/dist/agent-src/commands/agents/user/accept.md +3 -2
  9. package/dist/agent-src/commands/agents/user/init.md +5 -4
  10. package/dist/agent-src/commands/agents/user/update.md +3 -2
  11. package/dist/agent-src/commands/agents/user.md +1 -0
  12. package/dist/agent-src/commands/agents.md +1 -0
  13. package/dist/agent-src/commands/analytics/show.md +1 -0
  14. package/dist/agent-src/commands/analytics.md +1 -0
  15. package/dist/agent-src/commands/analyze/decision.md +1 -0
  16. package/dist/agent-src/commands/analyze/incident.md +1 -0
  17. package/dist/agent-src/commands/analyze/near-miss.md +1 -0
  18. package/dist/agent-src/commands/analyze/postmortem.md +1 -0
  19. package/dist/agent-src/commands/analyze/premortem.md +1 -0
  20. package/dist/agent-src/commands/analyze/reference-repo.md +1 -0
  21. package/dist/agent-src/commands/analyze.md +3 -2
  22. package/dist/agent-src/commands/brand/identity.md +6 -5
  23. package/dist/agent-src/commands/brand/review.md +6 -5
  24. package/dist/agent-src/commands/brand/strategy.md +6 -4
  25. package/dist/agent-src/commands/brand/tokens.md +7 -6
  26. package/dist/agent-src/commands/brand/voice.md +5 -4
  27. package/dist/agent-src/commands/brand.md +9 -7
  28. package/dist/agent-src/commands/bug/fix.md +1 -0
  29. package/dist/agent-src/commands/bug/investigate.md +1 -0
  30. package/dist/agent-src/commands/bug.md +2 -1
  31. package/dist/agent-src/commands/challenge-me/vision.md +1 -0
  32. package/dist/agent-src/commands/challenge-me/with-docs.md +1 -0
  33. package/dist/agent-src/commands/challenge-me.md +28 -6
  34. package/dist/agent-src/commands/chat-history.md +1 -0
  35. package/dist/agent-src/commands/check-current-md.md +1 -0
  36. package/dist/agent-src/commands/commit.md +1 -0
  37. package/dist/agent-src/commands/context/create.md +3 -2
  38. package/dist/agent-src/commands/context.md +1 -0
  39. package/dist/agent-src/commands/cost/profile.md +1 -0
  40. package/dist/agent-src/commands/cost.md +2 -1
  41. package/dist/agent-src/commands/council/analysis.md +1 -0
  42. package/dist/agent-src/commands/council/debate.md +1 -0
  43. package/dist/agent-src/commands/council/default.md +21 -20
  44. package/dist/agent-src/commands/council/design.md +1 -0
  45. package/dist/agent-src/commands/council/optimize.md +1 -0
  46. package/dist/agent-src/commands/council/pr.md +1 -0
  47. package/dist/agent-src/commands/council.md +1 -0
  48. package/dist/agent-src/commands/estimate-ticket.md +1 -0
  49. package/dist/agent-src/commands/explain-run.md +88 -0
  50. package/dist/agent-src/commands/feature/dev.md +1 -0
  51. package/dist/agent-src/commands/feature/explore.md +1 -0
  52. package/dist/agent-src/commands/feature/plan.md +1 -0
  53. package/dist/agent-src/commands/feature/refactor.md +1 -0
  54. package/dist/agent-src/commands/feature/roadmap.md +1 -0
  55. package/dist/agent-src/commands/feature.md +2 -1
  56. package/dist/agent-src/commands/fix/pr-comments.md +19 -0
  57. package/dist/agent-src/commands/fix.md +2 -1
  58. package/dist/agent-src/commands/ghostwriter/delete.md +1 -0
  59. package/dist/agent-src/commands/ghostwriter/fetch.md +1 -0
  60. package/dist/agent-src/commands/ghostwriter/show.md +1 -0
  61. package/dist/agent-src/commands/ghostwriter/write.md +1 -0
  62. package/dist/agent-src/commands/ghostwriter.md +1 -0
  63. package/dist/agent-src/commands/grill-me.md +17 -12
  64. package/dist/agent-src/commands/humanize.md +1 -0
  65. package/dist/agent-src/commands/image/analyse.md +2 -1
  66. package/dist/agent-src/commands/image/create.md +1 -0
  67. package/dist/agent-src/commands/image/verify.md +1 -0
  68. package/dist/agent-src/commands/image.md +16 -12
  69. package/dist/agent-src/commands/implement-ticket.md +1 -0
  70. package/dist/agent-src/commands/judge/on-diff.md +1 -0
  71. package/dist/agent-src/commands/judge/solo.md +1 -0
  72. package/dist/agent-src/commands/judge/steps.md +1 -0
  73. package/dist/agent-src/commands/judge.md +1 -0
  74. package/dist/agent-src/commands/knowledge/cross-repo.md +1 -0
  75. package/dist/agent-src/commands/knowledge/forget.md +1 -0
  76. package/dist/agent-src/commands/knowledge/ingest.md +1 -0
  77. package/dist/agent-src/commands/knowledge/list.md +10 -0
  78. package/dist/agent-src/commands/knowledge.md +1 -0
  79. package/dist/agent-src/commands/memory/learn-low-impact.md +1 -0
  80. package/dist/agent-src/commands/memory/mine-session.md +1 -0
  81. package/dist/agent-src/commands/memory/promote.md +3 -2
  82. package/dist/agent-src/commands/memory.md +1 -0
  83. package/dist/agent-src/commands/mission/upgrade.md +45 -24
  84. package/dist/agent-src/commands/mode.md +1 -0
  85. package/dist/agent-src/commands/module/create.md +1 -0
  86. package/dist/agent-src/commands/module/explore.md +1 -0
  87. package/dist/agent-src/commands/module.md +1 -0
  88. package/dist/agent-src/commands/optimize/agents-dir.md +1 -0
  89. package/dist/agent-src/commands/optimize/project.md +1 -0
  90. package/dist/agent-src/commands/optimize/prompt.md +1 -0
  91. package/dist/agent-src/commands/optimize.md +2 -1
  92. package/dist/agent-src/commands/orchestrate.md +1 -0
  93. package/dist/agent-src/commands/override.md +1 -0
  94. package/dist/agent-src/commands/package.md +2 -1
  95. package/dist/agent-src/commands/post-as/ghostwriter.md +1 -0
  96. package/dist/agent-src/commands/post-as/me.md +1 -0
  97. package/dist/agent-src/commands/post-as.md +1 -0
  98. package/dist/agent-src/commands/pr/create/description-only.md +139 -8
  99. package/dist/agent-src/commands/pr/create.md +35 -22
  100. package/dist/agent-src/commands/prediction-pool.md +7 -6
  101. package/dist/agent-src/commands/prepare-for-review.md +1 -0
  102. package/dist/agent-src/commands/profile/activate.md +1 -0
  103. package/dist/agent-src/commands/profile/deactivate.md +1 -0
  104. package/dist/agent-src/commands/profile.md +1 -0
  105. package/dist/agent-src/commands/project.md +2 -1
  106. package/dist/agent-src/commands/refine-ticket.md +7 -0
  107. package/dist/agent-src/commands/research/deep.md +1 -0
  108. package/dist/agent-src/commands/research.md +1 -0
  109. package/dist/agent-src/commands/review/routing.md +1 -0
  110. package/dist/agent-src/commands/review.md +2 -1
  111. package/dist/agent-src/commands/roadmap/ai-council.md +1 -0
  112. package/dist/agent-src/commands/roadmap/materialize.md +1 -0
  113. package/dist/agent-src/commands/roadmap/process-full.md +1 -0
  114. package/dist/agent-src/commands/roadmap/process-phase.md +1 -0
  115. package/dist/agent-src/commands/roadmap/process-step.md +1 -0
  116. package/dist/agent-src/commands/roadmap.md +1 -0
  117. package/dist/agent-src/commands/security-audit-config.md +2 -1
  118. package/dist/agent-src/commands/skill/preview.md +1 -0
  119. package/dist/agent-src/commands/skill.md +1 -0
  120. package/dist/agent-src/commands/skills/discover.md +1 -0
  121. package/dist/agent-src/commands/skills.md +1 -0
  122. package/dist/agent-src/commands/sync/agent-settings.md +1 -0
  123. package/dist/agent-src/commands/sync.md +2 -1
  124. package/dist/agent-src/commands/team/adversarial.md +3 -2
  125. package/dist/agent-src/commands/team/delegate.md +3 -2
  126. package/dist/agent-src/commands/team/review.md +3 -2
  127. package/dist/agent-src/commands/team/status.md +49 -18
  128. package/dist/agent-src/commands/team-knowledge.md +8 -6
  129. package/dist/agent-src/commands/team.md +21 -16
  130. package/dist/agent-src/commands/tests/e2e-heal.md +1 -0
  131. package/dist/agent-src/commands/tests/e2e-plan.md +1 -0
  132. package/dist/agent-src/commands/tests/execute.md +1 -0
  133. package/dist/agent-src/commands/tests.md +2 -1
  134. package/dist/agent-src/commands/update-form-request-messages.md +1 -0
  135. package/dist/agent-src/commands/upstream-contribute.md +1 -0
  136. package/dist/agent-src/commands/video/from-script.md +3 -2
  137. package/dist/agent-src/commands/video/from-song.md +22 -21
  138. package/dist/agent-src/commands/video/scene.md +1 -0
  139. package/dist/agent-src/commands/video/stitch.md +2 -1
  140. package/dist/agent-src/commands/video/storyboard.md +1 -0
  141. package/dist/agent-src/commands/video.md +1 -0
  142. package/dist/agent-src/commands/work.md +1 -0
  143. package/dist/agent-src/commands/worktree/cleanup.md +8 -7
  144. package/dist/agent-src/commands/worktree/create.md +1 -0
  145. package/dist/agent-src/commands/worktree/verify.md +1 -0
  146. package/dist/agent-src/commands/worktree.md +1 -0
  147. package/dist/agent-src/rules/active-remediation.md +12 -36
  148. package/dist/agent-src/rules/artifact-drafting-protocol.md +7 -64
  149. package/dist/agent-src/rules/context-hygiene.md +19 -113
  150. package/dist/agent-src/rules/design-fidelity.md +4 -60
  151. package/dist/agent-src/rules/domain-adoption-policy.md +10 -106
  152. package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +12 -60
  153. package/dist/agent-src/rules/improve-before-implement.md +4 -0
  154. package/dist/agent-src/rules/minimal-safe-diff.md +6 -86
  155. package/dist/agent-src/rules/size-enforcement.md +10 -0
  156. package/dist/agent-src/rules/tool-safety.md +17 -3
  157. package/dist/agent-src/skills/agent-security-review/SKILL.md +12 -1
  158. package/dist/agent-src/skills/ai-council/SKILL.md +11 -0
  159. package/dist/agent-src/skills/bug-analyzer/SKILL.md +24 -2
  160. package/dist/agent-src/skills/code-review/SKILL.md +105 -93
  161. package/dist/agent-src/skills/code-review/checklists/backend.md +50 -0
  162. package/dist/agent-src/skills/code-review/checklists/database.md +19 -0
  163. package/dist/agent-src/skills/code-review/checklists/dependency.md +18 -0
  164. package/dist/agent-src/skills/code-review/checklists/docs.md +14 -0
  165. package/dist/agent-src/skills/code-review/checklists/frontend.md +19 -0
  166. package/dist/agent-src/skills/code-review/checklists/infra.md +17 -0
  167. package/dist/agent-src/skills/code-review/checklists/producing-the-review.md +62 -0
  168. package/dist/agent-src/skills/code-review/checklists/test-only.md +13 -0
  169. package/dist/agent-src/skills/code-review/evals/evals.json +53 -15
  170. package/dist/agent-src/skills/condense-memory/SKILL.md +3 -2
  171. package/dist/agent-src/skills/conventional-commits-writing/evals/evals.json +15 -0
  172. package/dist/agent-src/skills/docker/SKILL.md +10 -0
  173. package/dist/agent-src/skills/docx-authoring/SKILL.md +102 -0
  174. package/dist/agent-src/skills/github-ci/evals/evals.json +25 -0
  175. package/dist/agent-src/skills/humanizer/SKILL.md +79 -3
  176. package/dist/agent-src/skills/humanizer/references/anti-aiisms.md +112 -0
  177. package/dist/agent-src/skills/humanizer/references/fixtures.md +70 -0
  178. package/dist/agent-src/skills/judge-bug-hunter/SKILL.md +7 -1
  179. package/dist/agent-src/skills/laravel/SKILL.md +10 -0
  180. package/dist/agent-src/skills/launch-readiness/SKILL.md +35 -0
  181. package/dist/agent-src/skills/llm-provider-knowledge/SKILL.md +146 -0
  182. package/dist/agent-src/skills/llm-provider-knowledge/evals/triggers.json +20 -0
  183. package/dist/agent-src/skills/merge-conflicts/SKILL.md +30 -0
  184. package/dist/agent-src/skills/pdf-tools/SKILL.md +101 -0
  185. package/dist/agent-src/skills/quality-tools/evals/evals.json +15 -0
  186. package/dist/agent-src/skills/security-audit/SKILL.md +29 -0
  187. package/dist/agent-src/skills/security-maturity-assessment/SKILL.md +90 -0
  188. package/dist/agent-src/skills/security-maturity-assessment/evals/triggers.json +52 -0
  189. package/dist/agent-src/skills/skill-writing/SKILL.md +79 -0
  190. package/dist/agent-src/skills/spreadsheet-authoring/SKILL.md +11 -3
  191. package/dist/agent-src/skills/subagent-orchestration/prompts/README.md +11 -0
  192. package/dist/agent-src/skills/subagent-orchestration/schemas/team-review-status.json +103 -0
  193. package/dist/agent-src/skills/terraform/SKILL.md +10 -0
  194. package/dist/agent-src/skills/test-driven-development/SKILL.md +31 -0
  195. package/dist/agent-src/skills/threat-modeling/SKILL.md +15 -0
  196. package/dist/agent-src/skills/token-optimizer/SKILL.md +10 -4
  197. package/dist/agent-src/templates/agent-settings.md +28 -0
  198. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  199. package/dist/agent-src/templates/scripts/telemetry/aggregator.ts +0 -0
  200. package/dist/agent-src/templates/scripts/telemetry/engagement.ts +30 -0
  201. package/dist/agent-src/templates/scripts/telemetry/report_renderer.ts +22 -9
  202. package/dist/agent-src/templates/scripts/telemetry_record.ts +5 -0
  203. package/dist/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +4 -1
  204. package/dist/discovery/deprecation-report.md +1 -1
  205. package/dist/discovery/discovery-manifest.json +314 -194
  206. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  207. package/dist/discovery/discovery-manifest.summary.md +5 -5
  208. package/dist/discovery/orphan-report.md +1 -1
  209. package/dist/discovery/packs.json +23 -18
  210. package/dist/discovery/trust-report.md +8 -8
  211. package/dist/discovery/workspaces.json +18 -13
  212. package/dist/install/install.mjs +48 -33
  213. package/dist/mcp/registry-manifest.json +3 -3
  214. package/dist/router.json +1 -1
  215. package/dist/scripts/_lib/agent_settings.js +4 -1
  216. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  217. package/dist/scripts/condense.js +5 -0
  218. package/dist/scripts/condense.js.map +1 -1
  219. package/dist/server/schemas/settings.js +10 -1
  220. package/dist/server/schemas/settings.js.map +1 -1
  221. package/docs/CLAIMS.md +9 -2
  222. package/docs/MIGRATION.md +20 -0
  223. package/docs/SKILL_CENSUS.md +8 -0
  224. package/docs/architecture.md +3 -3
  225. package/docs/benchmark.md +67 -2
  226. package/docs/catalog.md +2 -2
  227. package/docs/command-flows.md +1 -1
  228. package/docs/contracts/agents-layout.md +1 -1
  229. package/docs/contracts/ai-council-config.md +9 -2
  230. package/docs/contracts/ai-team-config.md +37 -4
  231. package/docs/contracts/consumer-bridge.md +55 -63
  232. package/docs/contracts/install-layout.md +1 -1
  233. package/docs/contracts/migrate-command.md +1 -1
  234. package/docs/contracts/prelaunch-diagnostics.md +151 -0
  235. package/docs/contracts/release-pr-gating.md +1 -0
  236. package/docs/customization.md +22 -1
  237. package/docs/decisions/ADR-020-global-only-consumer-scope.md +34 -0
  238. package/docs/decisions/ADR-049-configuration-trust-boundary.md +9 -1
  239. package/docs/featured-skills.md +2 -2
  240. package/docs/getting-started-by-role.md +1 -1
  241. package/docs/getting-started.md +17 -6
  242. package/docs/governance-advantage.md +1 -1
  243. package/docs/guidelines/agent-infra/active-remediation-mechanics.md +45 -0
  244. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +28 -0
  245. package/docs/guidelines/agent-infra/api-cost-levers.md +126 -0
  246. package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +76 -0
  247. package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +90 -0
  248. package/docs/guidelines/agent-infra/domain-adoption-gates.md +112 -0
  249. package/docs/guidelines/agent-infra/framework-neutrality-patterns.md +66 -0
  250. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +99 -0
  251. package/docs/guidelines/agent-infra/skill-quality-checklist.md +13 -0
  252. package/docs/guidelines/design-fidelity-mechanics.md +76 -0
  253. package/docs/installation.md +2 -1
  254. package/docs/maintainers/dev-mode.md +3 -2
  255. package/docs/parity/command-frontmatter.md +60 -0
  256. package/docs/proof.md +7 -6
  257. package/package.json +1 -1
  258. package/src/agent-src/templates/scripts/telemetry/aggregator.ts +0 -0
  259. package/src/agent-src/templates/scripts/telemetry/engagement.ts +30 -0
  260. package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +22 -9
  261. package/src/agent-src/templates/scripts/telemetry_record.ts +5 -0
  262. package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +4 -1
  263. package/src/config/agent-settings.template.yml +73 -10
  264. package/src/config/gitignore-block.txt +16 -6
  265. package/src/config/prelaunch-areas.yml +36 -0
  266. package/src/scripts/_cli/cmd_conformance.ts +2 -2
  267. package/src/scripts/_cli/cmd_doctor.ts +280 -39
  268. package/src/scripts/_cli/cmd_refresh.ts +35 -118
  269. package/src/scripts/_lib/agent_settings.ts +4 -1
  270. package/src/scripts/_lib/claude_plugin.ts +77 -2
  271. package/src/scripts/_lib/orchestration_record.ts +5 -0
  272. package/src/scripts/_lib/tool_adapter_registry.ts +120 -0
  273. package/src/scripts/ai_council/clients.ts +68 -6
  274. package/src/scripts/ai_council/pricing.ts +62 -0
  275. package/src/scripts/ai_team/config.ts +73 -1
  276. package/src/scripts/ai_team/review_gate.ts +636 -0
  277. package/src/scripts/ai_team/team_dispatch.ts +612 -0
  278. package/src/scripts/bench_ab_task_runner.ts +11 -1
  279. package/src/scripts/bench_parity_count.ts +279 -0
  280. package/src/scripts/build_linear_digest.ts +3 -1
  281. package/src/scripts/check_bridge_derivation.ts +172 -0
  282. package/src/scripts/check_consumer_scope_flip.ts +179 -0
  283. package/src/scripts/check_council_config_location.ts +115 -15
  284. package/src/scripts/check_host_loadability.ts +81 -0
  285. package/src/scripts/condense.ts +5 -0
  286. package/src/scripts/council_cli.ts +19 -3
  287. package/src/scripts/hook_manifest.yaml +12 -1
  288. package/src/scripts/install.ts +44 -45
  289. package/src/scripts/lint_agents_layout.ts +10 -4
  290. package/src/scripts/lint_breaking_changes_index.ts +303 -0
  291. package/src/scripts/lint_handoffs.ts +42 -0
  292. package/src/scripts/lint_skill_descriptions.ts +228 -0
  293. package/src/scripts/lint_skill_descriptions_allowlist.json +1 -0
  294. package/src/scripts/lint_skill_scripts_readonly.ts +145 -0
  295. package/src/scripts/lint_skill_scripts_readonly_allowlist.json +8 -0
  296. package/src/scripts/lint_supported_tools_matrix.ts +35 -0
  297. package/src/scripts/mcp_server/tools.ts +1 -3
  298. package/src/scripts/optimize_skill_description.ts +144 -0
  299. package/src/scripts/orchestration_record.ts +2 -1
  300. package/src/scripts/prelaunch_diagnostics.ts +248 -0
  301. package/src/scripts/run_skill_evals.ts +70 -4
  302. package/src/scripts/schemas/agent-settings.schema.json +17 -0
  303. package/src/scripts/schemas/command.schema.json +7 -0
  304. package/src/scripts/schemas/evals.schema.json +105 -14
  305. package/src/scripts/schemas/prelaunch-report.schema.json +107 -0
  306. package/src/scripts/schemas/skill.schema.json +18 -1
  307. package/src/scripts/schemas/subagent.schema.json +6 -0
  308. package/src/scripts/self_review_gate.test.ts +11 -0
  309. package/src/scripts/self_review_gate.ts +7 -1
  310. package/src/scripts/skill_eval_coverage.ts +4 -1
  311. package/src/scripts/skill_linter.ts +43 -3
  312. package/src/scripts/smoke_host_loadability.sh +73 -0
  313. package/src/scripts/team_review_gate_hook.ts +115 -0
  314. package/src/scripts/trigger_eval_grandfather.json +1 -2
  315. package/src/scripts/utilization_report.ts +225 -0
  316. package/src/server/schemas/settings.ts +24 -1
@@ -0,0 +1,636 @@
1
+ /**
2
+ * Review-Gate governance — road-to-team-mode Phase 4.
3
+ *
4
+ * The codex plugin's Stop-hook Review Gate (upstream: `codex@openai-codex`,
5
+ * `scripts/stop-review-gate-hook.mjs`) can BLOCK every Stop and re-trigger a
6
+ * Claude↔Codex loop without any bound — upstream's own README warns: "The
7
+ * review gate can create a long-running Claude/Codex loop and may drain
8
+ * usage limits quickly." The gate itself stays upstream's; this module owns
9
+ * the GOVERNANCE:
10
+ *
11
+ * 1. Parse the gate transcript's first-line contract — upstream's prompt
12
+ * (`prompts/stop-review-gate.md`) demands the first line be exactly
13
+ * `ALLOW: <reason>` or `BLOCK: <reason>`. Anything else is honestly
14
+ * `UNKNOWN` and is NEVER counted as a BLOCK.
15
+ * 2. Count CONSECUTIVE BLOCK verdicts per session (state: one small JSON
16
+ * under `agents/runtime/state/`, same convention as the other hook
17
+ * concerns, written via the shared `state_io` lock). ALLOW resets.
18
+ * 3. At `ai_team.review_gate.max_consecutive_blocks`, surface a visible
19
+ * circuit-breaker notice EXACTLY ONCE and report the circuit as open
20
+ * (the managed layer stops re-blocking — the user decides).
21
+ * 4. Ledger: one events-log line per gate verdict (`team.gate: BLOCK 2/3`
22
+ * / `ALLOW reset`) under `agents/runtime/team/events.log`, following
23
+ * the council events-log convention (`src/scripts/ai_council/
24
+ * events_log.ts`: schema_version + ts_utc + compact JSON line, honours
25
+ * the same `AGENT_CONFIG_NO_EVENTS_LOG` kill-switch, no free-form
26
+ * content — verdict enum + counter only, PII-excluded by construction).
27
+ *
28
+ * Composition point (documented, not yet manifest-wired): the minimal
29
+ * call-site is `src/scripts/team_review_gate_hook.ts`, a `stop` concern in
30
+ * the dispatcher shape. Claude Code gives sibling Stop hooks no view of the
31
+ * plugin gate's own stdout, so the hook reads the gate verdict from the
32
+ * plugin's persisted job record (`jobs/<id>.json` → `result.rawOutput`,
33
+ * title `Codex Stop Gate Review`) — see `find_latest_gate_transcript`.
34
+ * Registering the concern in `src/scripts/hook_manifest.yaml` (claude/
35
+ * cowork `stop`, after `chat-history`) is the one remaining wiring step;
36
+ * it drags the manifest lint + installer + tests/hooks snapshot surface and
37
+ * is deliberately left to the orchestrator. With
38
+ * `ai_team.review_gate.managed: false` (default) every entry point here is
39
+ * a no-op — byte-identical pre-Phase-4 behavior.
40
+ */
41
+ import * as crypto from 'node:crypto';
42
+ import * as fs from 'node:fs';
43
+ import * as os from 'node:os';
44
+ import * as path from 'node:path';
45
+
46
+ import { claude_config_dir, CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_ID } from '../_lib/claude_plugin.js';
47
+ import { atomic_write_json } from '../hooks/state_io.js';
48
+ import { AI_TEAM_DEFAULTS, load_ai_team_config, type AiTeamConfig } from './config.js';
49
+
50
+ export type GateVerdict = 'ALLOW' | 'BLOCK' | 'UNKNOWN';
51
+
52
+ export const GATE_STATE_BASENAME = 'team-review-gate.json';
53
+ export const GATE_LEDGER_RELPATH = path.join('agents', 'runtime', 'team', 'events.log');
54
+ export const GATE_LEDGER_KIND = 'team.gate';
55
+
56
+ /** Same kill-switch the council events log honours. */
57
+ const _KILL_SWITCH_ENV = 'AGENT_CONFIG_NO_EVENTS_LOG';
58
+
59
+ /** Keep the per-session state file tiny — newest N sessions survive. */
60
+ const _MAX_TRACKED_SESSIONS = 20;
61
+
62
+ // ── verdict parsing ─────────────────────────────────────────────────
63
+
64
+ /**
65
+ * Parse the first-line ALLOW/BLOCK contract of a gate transcript.
66
+ *
67
+ * Upstream's compact-output contract: the first line is exactly
68
+ * `ALLOW: <short reason>` or `BLOCK: <short reason>` (the gate hook itself
69
+ * matches `startsWith('ALLOW:')` / `startsWith('BLOCK:')`). We additionally
70
+ * accept a bare `ALLOW` / `BLOCK` first line. Everything else — empty
71
+ * output, prose, lowercase, a verdict buried on line 2 — is honestly
72
+ * `UNKNOWN` and MUST NOT be counted as a BLOCK.
73
+ */
74
+ export function parse_gate_verdict(transcript_text: string | null | undefined): GateVerdict {
75
+ const text = String(transcript_text ?? '').trim();
76
+ if (text === '') {
77
+ return 'UNKNOWN';
78
+ }
79
+ const first_line = (text.split(/\r?\n/, 1)[0] ?? '').trim();
80
+ if (first_line === 'ALLOW' || first_line.startsWith('ALLOW:')) {
81
+ return 'ALLOW';
82
+ }
83
+ if (first_line === 'BLOCK' || first_line.startsWith('BLOCK:')) {
84
+ return 'BLOCK';
85
+ }
86
+ return 'UNKNOWN';
87
+ }
88
+
89
+ // ── per-session counter state ───────────────────────────────────────
90
+
91
+ interface GateSessionState {
92
+ consecutive_blocks: number;
93
+ notice_emitted: boolean;
94
+ last_dedupe_key: string;
95
+ updated_utc: string;
96
+ }
97
+
98
+ interface GateStateFile {
99
+ schema_version: number;
100
+ sessions: Record<string, GateSessionState>;
101
+ }
102
+
103
+ /** Canonical state path — `agents/runtime/state/team-review-gate.json`. */
104
+ export function gate_state_path(project_root: string): string {
105
+ return path.join(project_root, 'agents', 'runtime', 'state', GATE_STATE_BASENAME);
106
+ }
107
+
108
+ function _safe_session(session_id: string | null | undefined): string {
109
+ const raw = (session_id ?? '').trim() || 'unknown-session';
110
+ return raw.replace(/\//g, '_').replace(/\\/g, '_').replace(/\.\./g, '_');
111
+ }
112
+
113
+ function _read_state(state_path: string): GateStateFile {
114
+ const empty: GateStateFile = { schema_version: 1, sessions: {} };
115
+ let parsed: unknown;
116
+ try {
117
+ parsed = JSON.parse(fs.readFileSync(state_path, 'utf-8'));
118
+ } catch {
119
+ return empty;
120
+ }
121
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
122
+ return empty;
123
+ }
124
+ const sessions_raw = (parsed as Record<string, unknown>)['sessions'];
125
+ if (sessions_raw === null || typeof sessions_raw !== 'object' || Array.isArray(sessions_raw)) {
126
+ return empty;
127
+ }
128
+ const sessions: Record<string, GateSessionState> = {};
129
+ for (const [key, value] of Object.entries(sessions_raw as Record<string, unknown>)) {
130
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
131
+ continue;
132
+ }
133
+ const v = value as Record<string, unknown>;
134
+ const blocks = v['consecutive_blocks'];
135
+ sessions[key] = {
136
+ consecutive_blocks:
137
+ typeof blocks === 'number' && Number.isInteger(blocks) && blocks >= 0 ? blocks : 0,
138
+ notice_emitted: v['notice_emitted'] === true,
139
+ last_dedupe_key: typeof v['last_dedupe_key'] === 'string' ? v['last_dedupe_key'] : '',
140
+ updated_utc: typeof v['updated_utc'] === 'string' ? v['updated_utc'] : '',
141
+ };
142
+ }
143
+ return { schema_version: 1, sessions };
144
+ }
145
+
146
+ function _prune_sessions(state: GateStateFile): void {
147
+ const keys = Object.keys(state.sessions);
148
+ if (keys.length <= _MAX_TRACKED_SESSIONS) {
149
+ return;
150
+ }
151
+ const sorted = keys.sort((a, b) =>
152
+ (state.sessions[b]?.updated_utc ?? '').localeCompare(state.sessions[a]?.updated_utc ?? ''),
153
+ );
154
+ for (const stale of sorted.slice(_MAX_TRACKED_SESSIONS)) {
155
+ delete state.sessions[stale];
156
+ }
157
+ }
158
+
159
+ // ── circuit-breaker notice ──────────────────────────────────────────
160
+
161
+ /** The visible circuit-breaker notice — rendered by the Stop-hook layer. */
162
+ export function circuit_breaker_notice(bound: number): string {
163
+ return (
164
+ `⚠️ team review-gate circuit breaker: ${bound} consecutive BLOCK verdict(s) ` +
165
+ 'this session (bound: `ai_team.review_gate.max_consecutive_blocks`). ' +
166
+ 'The managed gate stops re-blocking now — you decide: review the last ' +
167
+ 'BLOCK reason via `/team:status`, fix what is real, ignore what is not. ' +
168
+ 'An ALLOW verdict resets the counter.'
169
+ );
170
+ }
171
+
172
+ // ── ledger (events-log convention) ──────────────────────────────────
173
+
174
+ function _kill_switch_active(): boolean {
175
+ const value = process.env[_KILL_SWITCH_ENV] ?? '';
176
+ return !(value === '' || value === '0' || value === 'false' || value === 'False');
177
+ }
178
+
179
+ function _iso_seconds_z(d: Date): string {
180
+ const pad = (n: number, w = 2): string => String(n).padStart(w, '0');
181
+ return (
182
+ `${pad(d.getUTCFullYear(), 4)}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}` +
183
+ `T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}Z`
184
+ );
185
+ }
186
+
187
+ export interface GateLedgerEntry {
188
+ schema_version: number;
189
+ ts_utc: string;
190
+ kind: string; // GATE_LEDGER_KIND
191
+ session_id: string;
192
+ verdict: GateVerdict;
193
+ counter: string; // "2/3" | "reset" | "n/a"
194
+ }
195
+
196
+ /** Canonical ledger path — `agents/runtime/team/events.log`. */
197
+ export function gate_ledger_path(project_root: string): string {
198
+ return path.join(project_root, GATE_LEDGER_RELPATH);
199
+ }
200
+
201
+ function _append_ledger_line(target: string, entry: GateLedgerEntry): boolean {
202
+ if (_kill_switch_active()) {
203
+ return false;
204
+ }
205
+ fs.mkdirSync(path.dirname(target), { recursive: true });
206
+ fs.appendFileSync(target, JSON.stringify(entry) + '\n', { encoding: 'utf-8' });
207
+ return true;
208
+ }
209
+
210
+ /** Human line for `/team:status` / session replay: `team.gate: BLOCK 2/3`. */
211
+ export function format_gate_ledger_line(entry: Pick<GateLedgerEntry, 'verdict' | 'counter'>): string {
212
+ return `${GATE_LEDGER_KIND}: ${entry.verdict} ${entry.counter}`;
213
+ }
214
+
215
+ /**
216
+ * Read helper for `/team:status` — newest-last gate ledger entries,
217
+ * optionally filtered by session. Fail-open: unreadable file / broken
218
+ * lines yield an empty / partial list, never a throw.
219
+ */
220
+ export function read_gate_ledger(opts: {
221
+ project_root?: string;
222
+ ledger_path?: string;
223
+ session_id?: string | null;
224
+ limit?: number;
225
+ } = {}): GateLedgerEntry[] {
226
+ const target =
227
+ opts.ledger_path ?? gate_ledger_path(opts.project_root ?? process.cwd());
228
+ let raw: string;
229
+ try {
230
+ raw = fs.readFileSync(target, 'utf-8');
231
+ } catch {
232
+ return [];
233
+ }
234
+ const want_session = opts.session_id ? _safe_session(opts.session_id) : null;
235
+ const entries: GateLedgerEntry[] = [];
236
+ for (const line of raw.split('\n')) {
237
+ const trimmed = line.trim();
238
+ if (trimmed === '') {
239
+ continue;
240
+ }
241
+ let parsed: unknown;
242
+ try {
243
+ parsed = JSON.parse(trimmed);
244
+ } catch {
245
+ continue;
246
+ }
247
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
248
+ continue;
249
+ }
250
+ const rec = parsed as Record<string, unknown>;
251
+ if (rec['kind'] !== GATE_LEDGER_KIND) {
252
+ continue;
253
+ }
254
+ const verdict = rec['verdict'];
255
+ if (verdict !== 'ALLOW' && verdict !== 'BLOCK' && verdict !== 'UNKNOWN') {
256
+ continue;
257
+ }
258
+ const entry: GateLedgerEntry = {
259
+ schema_version: typeof rec['schema_version'] === 'number' ? rec['schema_version'] : 1,
260
+ ts_utc: typeof rec['ts_utc'] === 'string' ? rec['ts_utc'] : '',
261
+ kind: GATE_LEDGER_KIND,
262
+ session_id: typeof rec['session_id'] === 'string' ? rec['session_id'] : '',
263
+ verdict,
264
+ counter: typeof rec['counter'] === 'string' ? rec['counter'] : '',
265
+ };
266
+ if (want_session !== null && entry.session_id !== want_session) {
267
+ continue;
268
+ }
269
+ entries.push(entry);
270
+ }
271
+ const limit = opts.limit ?? entries.length;
272
+ return limit >= entries.length ? entries : entries.slice(entries.length - limit);
273
+ }
274
+
275
+ // ── the managed-mode counter (state machine) ────────────────────────
276
+
277
+ export interface GateRecordOptions {
278
+ session_id: string | null | undefined;
279
+ /** Raw gate transcript (first-line contract). Ignored when `verdict` set. */
280
+ transcript_text?: string | null;
281
+ /** Pre-parsed verdict (skips parsing). */
282
+ verdict?: GateVerdict;
283
+ /**
284
+ * Idempotency key for the underlying gate run (e.g. the plugin job id).
285
+ * The same key twice in a row for one session is a re-observation of the
286
+ * same gate run — it is not recounted.
287
+ */
288
+ dedupe_key?: string | null;
289
+ /** Injected config (tests). Default: `load_ai_team_config()` from cwd. */
290
+ config?: AiTeamConfig;
291
+ /** Root for state + ledger paths. Default: `process.cwd()`. */
292
+ project_root?: string;
293
+ state_path?: string;
294
+ ledger_path?: string;
295
+ now?: Date;
296
+ }
297
+
298
+ export interface GateOutcome {
299
+ verdict: GateVerdict;
300
+ /** Consecutive BLOCKs after this verdict was applied. */
301
+ consecutive_blocks: number;
302
+ bound: number;
303
+ /** false when unmanaged, deduped, or verdict UNKNOWN (never counted). */
304
+ counted: boolean;
305
+ /** true at/beyond the bound — the managed layer must not re-block. */
306
+ circuit_open: boolean;
307
+ /** Non-null exactly once per session per trip — the visible notice. */
308
+ notice: string | null;
309
+ /** The `team.gate: …` line appended to the ledger (null when none). */
310
+ ledger_line: string | null;
311
+ }
312
+
313
+ /**
314
+ * Apply one gate verdict to the per-session consecutive-BLOCK counter.
315
+ *
316
+ * State machine (managed mode only — `managed: false` is a strict no-op
317
+ * that touches neither state nor ledger):
318
+ *
319
+ * ALLOW → counter := 0, notice flag cleared. Ledger `ALLOW reset`.
320
+ * BLOCK → counter += 1. At counter == bound: notice returned (once),
321
+ * circuit opens. Beyond: circuit stays open, no second notice.
322
+ * UNKNOWN → counter unchanged, never counted as BLOCK.
323
+ * Ledger `UNKNOWN n/bound` (honest visibility).
324
+ */
325
+ export function record_gate_verdict(opts: GateRecordOptions): GateOutcome {
326
+ const config = opts.config ?? _load_config_fail_open();
327
+ const bound = config.review_gate.max_consecutive_blocks;
328
+ const verdict = opts.verdict ?? parse_gate_verdict(opts.transcript_text);
329
+
330
+ if (!config.review_gate.managed) {
331
+ return {
332
+ verdict,
333
+ consecutive_blocks: 0,
334
+ bound,
335
+ counted: false,
336
+ circuit_open: false,
337
+ notice: null,
338
+ ledger_line: null,
339
+ };
340
+ }
341
+
342
+ const project_root = opts.project_root ?? process.cwd();
343
+ const state_path = opts.state_path ?? gate_state_path(project_root);
344
+ const ledger_path = opts.ledger_path ?? gate_ledger_path(project_root);
345
+ const now = opts.now ?? new Date();
346
+ const session_key = _safe_session(opts.session_id);
347
+ const dedupe_key = (opts.dedupe_key ?? '').trim();
348
+
349
+ const state = _read_state(state_path);
350
+ const session: GateSessionState = state.sessions[session_key] ?? {
351
+ consecutive_blocks: 0,
352
+ notice_emitted: false,
353
+ last_dedupe_key: '',
354
+ updated_utc: '',
355
+ };
356
+
357
+ if (dedupe_key !== '' && session.last_dedupe_key === dedupe_key) {
358
+ // Same gate run observed again (e.g. a Stop event without a fresh
359
+ // gate execution) — never recount.
360
+ return {
361
+ verdict,
362
+ consecutive_blocks: session.consecutive_blocks,
363
+ bound,
364
+ counted: false,
365
+ circuit_open: session.consecutive_blocks >= bound,
366
+ notice: null,
367
+ ledger_line: null,
368
+ };
369
+ }
370
+
371
+ let counted = false;
372
+ let notice: string | null = null;
373
+ if (verdict === 'ALLOW') {
374
+ session.consecutive_blocks = 0;
375
+ session.notice_emitted = false;
376
+ counted = true;
377
+ } else if (verdict === 'BLOCK') {
378
+ session.consecutive_blocks += 1;
379
+ counted = true;
380
+ if (session.consecutive_blocks >= bound && !session.notice_emitted) {
381
+ notice = circuit_breaker_notice(bound);
382
+ session.notice_emitted = true;
383
+ }
384
+ }
385
+ // UNKNOWN: counter untouched, counted stays false.
386
+
387
+ session.last_dedupe_key = dedupe_key;
388
+ session.updated_utc = _iso_seconds_z(now);
389
+ state.sessions[session_key] = session;
390
+ _prune_sessions(state);
391
+ atomic_write_json(state_path, state);
392
+
393
+ const counter =
394
+ verdict === 'ALLOW' ? 'reset' : `${session.consecutive_blocks}/${bound}`;
395
+ const entry: GateLedgerEntry = {
396
+ schema_version: 1,
397
+ ts_utc: session.updated_utc,
398
+ kind: GATE_LEDGER_KIND,
399
+ session_id: session_key,
400
+ verdict,
401
+ counter,
402
+ };
403
+ const written = _append_ledger_line(ledger_path, entry);
404
+
405
+ return {
406
+ verdict,
407
+ consecutive_blocks: session.consecutive_blocks,
408
+ bound,
409
+ counted,
410
+ circuit_open: session.consecutive_blocks >= bound,
411
+ notice,
412
+ ledger_line: written ? format_gate_ledger_line(entry) : null,
413
+ };
414
+ }
415
+
416
+ function _load_config_fail_open(): AiTeamConfig {
417
+ try {
418
+ return load_ai_team_config();
419
+ } catch {
420
+ // A broken ai_team block must never break the Stop path — doctor
421
+ // owns reporting it. Defaults = managed off = strict no-op.
422
+ return AI_TEAM_DEFAULTS;
423
+ }
424
+ }
425
+
426
+ // ── upstream plugin state discovery (read-only) ─────────────────────
427
+
428
+ /** Upstream's plugin-data directory name under `~/.claude/plugins/data/`. */
429
+ const _CODEX_PLUGIN_DATA_DIRNAME = `${CODEX_PLUGIN_ID}-${CODEX_MARKETPLACE_NAME}`;
430
+
431
+ /** First line of upstream's stop-gate task prompt — tags the gate job. */
432
+ export const UPSTREAM_GATE_JOB_TITLE = 'Codex Stop Gate Review';
433
+
434
+ /**
435
+ * Mirror upstream `state.mjs::resolveStateDir` workspace segment:
436
+ * `<sanitized basename>-<sha256(realpath(workspace_root))[:16]>`.
437
+ */
438
+ function _workspace_state_segment(workspace_root: string): string {
439
+ let canonical = workspace_root;
440
+ try {
441
+ canonical = fs.realpathSync.native(workspace_root);
442
+ } catch {
443
+ canonical = workspace_root;
444
+ }
445
+ const slug_source = path.basename(workspace_root) || 'workspace';
446
+ const slug =
447
+ slug_source.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'workspace';
448
+ const hash = crypto.createHash('sha256').update(canonical).digest('hex').slice(0, 16);
449
+ return `${slug}-${hash}`;
450
+ }
451
+
452
+ /**
453
+ * Candidate upstream state dirs for this workspace, highest priority first:
454
+ * `$CLAUDE_PLUGIN_DATA` (set by Claude Code inside plugin hooks), the
455
+ * observed on-disk plugin-data layout under the Claude config dir, and
456
+ * upstream's tmpdir fallback. Read-only — never created.
457
+ */
458
+ export function upstream_state_dir_candidates(workspace_root: string): string[] {
459
+ const segment = _workspace_state_segment(workspace_root);
460
+ const candidates: string[] = [];
461
+ const env = (process.env['CLAUDE_PLUGIN_DATA'] ?? '').trim();
462
+ if (env !== '') {
463
+ candidates.push(path.join(env, 'state', segment));
464
+ }
465
+ candidates.push(
466
+ path.join(claude_config_dir(), 'plugins', 'data', _CODEX_PLUGIN_DATA_DIRNAME, 'state', segment),
467
+ );
468
+ candidates.push(path.join(os.tmpdir(), 'codex-companion', segment));
469
+ return candidates;
470
+ }
471
+
472
+ /**
473
+ * True when the upstream plugin's Review Gate is switched on for this
474
+ * workspace (`config.stopReviewGate: true` in the plugin's `state.json`,
475
+ * toggled by `/codex:setup --enable-review-gate`). Unreadable / absent
476
+ * state → false. Read-only.
477
+ */
478
+ export function upstream_gate_enabled(workspace_root: string): boolean {
479
+ for (const dir of upstream_state_dir_candidates(workspace_root)) {
480
+ let parsed: unknown;
481
+ try {
482
+ parsed = JSON.parse(fs.readFileSync(path.join(dir, 'state.json'), 'utf-8'));
483
+ } catch {
484
+ continue;
485
+ }
486
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
487
+ continue;
488
+ }
489
+ const cfg = (parsed as Record<string, unknown>)['config'];
490
+ if (cfg !== null && typeof cfg === 'object' && !Array.isArray(cfg)) {
491
+ if ((cfg as Record<string, unknown>)['stopReviewGate'] === true) {
492
+ return true;
493
+ }
494
+ }
495
+ }
496
+ return false;
497
+ }
498
+
499
+ export interface GateTranscript {
500
+ /** Raw gate output — first line carries the ALLOW/BLOCK contract. */
501
+ transcript: string;
502
+ /** Upstream job id — used as the dedupe key for the counter. */
503
+ job_id: string;
504
+ }
505
+
506
+ /**
507
+ * Locate the newest completed stop-gate review job for this workspace
508
+ * (and session, when both sides carry a session id) in the upstream
509
+ * plugin's `jobs/` dir, and return its raw output. The plugin persists
510
+ * `result.rawOutput` (the codex final answer whose first line is
511
+ * `ALLOW:`/`BLOCK:`) in `jobs/<id>.json` with title
512
+ * `Codex Stop Gate Review`. Null when no such job exists. Read-only.
513
+ */
514
+ export function find_latest_gate_transcript(
515
+ workspace_root: string,
516
+ session_id: string | null | undefined,
517
+ ): GateTranscript | null {
518
+ let best: { transcript: string; job_id: string; sort_key: string } | null = null;
519
+ for (const dir of upstream_state_dir_candidates(workspace_root)) {
520
+ const jobs_dir = path.join(dir, 'jobs');
521
+ let names: string[];
522
+ try {
523
+ names = fs.readdirSync(jobs_dir);
524
+ } catch {
525
+ continue;
526
+ }
527
+ for (const name of names) {
528
+ if (!name.endsWith('.json')) {
529
+ continue;
530
+ }
531
+ let job: unknown;
532
+ try {
533
+ job = JSON.parse(fs.readFileSync(path.join(jobs_dir, name), 'utf-8'));
534
+ } catch {
535
+ continue;
536
+ }
537
+ if (job === null || typeof job !== 'object' || Array.isArray(job)) {
538
+ continue;
539
+ }
540
+ const rec = job as Record<string, unknown>;
541
+ if (rec['title'] !== UPSTREAM_GATE_JOB_TITLE) {
542
+ continue;
543
+ }
544
+ const job_session = typeof rec['sessionId'] === 'string' ? rec['sessionId'] : '';
545
+ const want = (session_id ?? '').trim();
546
+ if (want !== '' && job_session !== '' && job_session !== want) {
547
+ continue;
548
+ }
549
+ const result = rec['result'];
550
+ if (result === null || typeof result !== 'object' || Array.isArray(result)) {
551
+ continue;
552
+ }
553
+ const raw_output = (result as Record<string, unknown>)['rawOutput'];
554
+ if (typeof raw_output !== 'string' || raw_output.trim() === '') {
555
+ continue;
556
+ }
557
+ const sort_key = String(rec['completedAt'] ?? rec['createdAt'] ?? '');
558
+ const job_id = typeof rec['id'] === 'string' && rec['id'] !== '' ? rec['id'] : name;
559
+ if (best === null || sort_key.localeCompare(best.sort_key) > 0) {
560
+ best = { transcript: raw_output, job_id, sort_key };
561
+ }
562
+ }
563
+ }
564
+ return best === null ? null : { transcript: best.transcript, job_id: best.job_id };
565
+ }
566
+
567
+ // ── doctor sub-signal (c) — Review-Gate governance ──────────────────
568
+
569
+ /** Upstream's own cost warning, quoted verbatim in the doctor remedy. */
570
+ export const UPSTREAM_GATE_COST_WARNING =
571
+ 'The review gate can create a long-running Claude/Codex loop and may drain usage limits quickly.';
572
+
573
+ export interface GateDoctorSignal {
574
+ /** Detail fragment for the folded team-check message. */
575
+ gate_str: string;
576
+ /** Remediation sentences to fold into the team-check remedy. */
577
+ remedies: string[];
578
+ }
579
+
580
+ /**
581
+ * Doctor sub-signal (c) — Review-Gate governance. Consumes the RAW
582
+ * `ai_team.review_gate` mapping (the doctor reads settings leniently and
583
+ * must not crash on an invalid block) plus the read-only upstream gate
584
+ * probe. WARN states:
585
+ *
586
+ * - plugin gate ON while `managed: false` → the unbounded-loop shape;
587
+ * remedy carries the enable hint + upstream's cost warning quoted.
588
+ * - `managed: true` with an invalid `max_consecutive_blocks` in the raw
589
+ * settings (the strict loader would reject the whole block) → WARN.
590
+ */
591
+ export function review_gate_doctor_signal(
592
+ project_root: string,
593
+ gate_raw: Record<string, unknown>,
594
+ ): GateDoctorSignal {
595
+ const managed = gate_raw['managed'] === true;
596
+ const bound_raw = gate_raw['max_consecutive_blocks'];
597
+ const bound_invalid =
598
+ 'max_consecutive_blocks' in gate_raw &&
599
+ (typeof bound_raw !== 'number' || !Number.isInteger(bound_raw) || bound_raw < 1);
600
+ const plugin_gate_on = upstream_gate_enabled(project_root);
601
+
602
+ if (managed && bound_invalid) {
603
+ return {
604
+ gate_str: 'review-gate ⚠️ managed, loop bound invalid',
605
+ remedies: [
606
+ 'set `ai_team.review_gate.max_consecutive_blocks` to a positive ' +
607
+ 'integer (the strict loader rejects the current value)',
608
+ ],
609
+ };
610
+ }
611
+ if (plugin_gate_on && !managed) {
612
+ return {
613
+ gate_str: 'review-gate ⚠️ plugin gate on, unmanaged',
614
+ remedies: [
615
+ 'the codex plugin Review Gate is enabled but ' +
616
+ '`ai_team.review_gate.managed` is false — no loop bound applies ' +
617
+ `(upstream: "${UPSTREAM_GATE_COST_WARNING}"); set ` +
618
+ '`ai_team.review_gate.managed: true` in .agent-settings.yml, or ' +
619
+ 'disable the gate via `/codex:setup --disable-review-gate`',
620
+ ],
621
+ };
622
+ }
623
+ if (managed) {
624
+ const bound =
625
+ typeof bound_raw === 'number' && Number.isInteger(bound_raw) && bound_raw >= 1
626
+ ? bound_raw
627
+ : AI_TEAM_DEFAULTS.review_gate.max_consecutive_blocks;
628
+ return {
629
+ gate_str:
630
+ `review-gate on (managed, bound ${bound}, ` +
631
+ `plugin gate ${plugin_gate_on ? 'on' : 'off'})`,
632
+ remedies: [],
633
+ };
634
+ }
635
+ return { gate_str: 'review-gate off', remedies: [] };
636
+ }