@webpresso/agent-kit 3.1.30 → 3.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 (300) hide show
  1. package/README.md +53 -15
  2. package/bin/_run.js +183 -20
  3. package/catalog/AGENTS.md.tpl +3 -3
  4. package/catalog/agent/agents/implementer.md +45 -0
  5. package/catalog/agent/rules/parallel-orchestration-quota.md +100 -0
  6. package/catalog/agent/rules/pre-implementation.md +28 -1
  7. package/catalog/agent/rules/supported-agent-clis.md +9 -0
  8. package/catalog/agent/rules/typescript-version.md +50 -0
  9. package/catalog/agent/skills/codex/SKILL.md +25 -1
  10. package/catalog/agent/skills/verify/SKILL.md +4 -2
  11. package/catalog/docs/templates/blueprint.md +1 -1
  12. package/dist/esm/audit/agents.js +1 -0
  13. package/dist/esm/audit/blueprint-lifecycle-sql.d.ts +12 -0
  14. package/dist/esm/audit/blueprint-lifecycle-sql.js +23 -2
  15. package/dist/esm/audit/blueprint-pr-coverage.js +4 -0
  16. package/dist/esm/audit/blueprint-trust.js +14 -10
  17. package/dist/esm/audit/changeset-evidence.d.ts +23 -0
  18. package/dist/esm/audit/changeset-evidence.js +30 -0
  19. package/dist/esm/audit/changeset-required.d.ts +10 -0
  20. package/dist/esm/audit/changeset-required.js +73 -0
  21. package/dist/esm/audit/ci-guardrails-detection.d.ts +27 -0
  22. package/dist/esm/audit/ci-guardrails-detection.js +68 -0
  23. package/dist/esm/audit/ci-guardrails-wiring.d.ts +22 -0
  24. package/dist/esm/audit/ci-guardrails-wiring.js +80 -0
  25. package/dist/esm/audit/ci-test-perf.js +30 -0
  26. package/dist/esm/audit/command-surface.js +4 -2
  27. package/dist/esm/audit/host-substitution-risk.d.ts +2 -0
  28. package/dist/esm/audit/host-substitution-risk.js +256 -0
  29. package/dist/esm/audit/registry.d.ts +4 -4
  30. package/dist/esm/audit/registry.js +13 -1
  31. package/dist/esm/audit/toolchain-isolation.js +1 -2
  32. package/dist/esm/audit/typescript-version.d.ts +2 -0
  33. package/dist/esm/audit/typescript-version.js +123 -0
  34. package/dist/esm/blueprint/core/parser.js +101 -2
  35. package/dist/esm/blueprint/core/schema.d.ts +1 -0
  36. package/dist/esm/blueprint/core/schema.js +26 -0
  37. package/dist/esm/blueprint/core/validation/criteria.d.ts +14 -0
  38. package/dist/esm/blueprint/core/validation/criteria.js +41 -2
  39. package/dist/esm/blueprint/core/validation/state.js +25 -14
  40. package/dist/esm/blueprint/db/migrations/run.js +4 -2
  41. package/dist/esm/blueprint/lifecycle/audit.d.ts +20 -0
  42. package/dist/esm/blueprint/lifecycle/audit.js +87 -20
  43. package/dist/esm/blueprint/lifecycle/canonicalize.d.ts +6 -0
  44. package/dist/esm/blueprint/lifecycle/canonicalize.js +135 -0
  45. package/dist/esm/blueprint/lifecycle/local.d.ts +2 -1
  46. package/dist/esm/blueprint/lifecycle/local.js +19 -3
  47. package/dist/esm/blueprint/lifecycle/review-provenance.d.ts +6 -0
  48. package/dist/esm/blueprint/lifecycle/review-provenance.js +68 -12
  49. package/dist/esm/blueprint/lifecycle/rollout-anchor.d.ts +97 -0
  50. package/dist/esm/blueprint/lifecycle/rollout-anchor.js +161 -0
  51. package/dist/esm/blueprint/service/BlueprintCreationService.d.ts +1 -0
  52. package/dist/esm/blueprint/service/BlueprintCreationService.js +53 -4
  53. package/dist/esm/blueprint/service/blueprint-tech-debt-links.js +16 -2
  54. package/dist/esm/blueprint/sync/client.d.ts +2 -1
  55. package/dist/esm/blueprint/sync/client.js +1 -1
  56. package/dist/esm/blueprint/trust/command-runner.d.ts +26 -0
  57. package/dist/esm/blueprint/trust/command-runner.js +148 -0
  58. package/dist/esm/blueprint/trust/dossier.d.ts +1 -0
  59. package/dist/esm/blueprint/trust/dossier.js +40 -6
  60. package/dist/esm/blueprint/trust/gates.d.ts +7 -0
  61. package/dist/esm/blueprint/trust/gates.js +20 -0
  62. package/dist/esm/blueprint/trust/promotion.d.ts +8 -0
  63. package/dist/esm/blueprint/trust/promotion.js +74 -112
  64. package/dist/esm/blueprint/trust/scaffold.d.ts +19 -0
  65. package/dist/esm/blueprint/trust/scaffold.js +64 -0
  66. package/dist/esm/blueprint/trust/validator.d.ts +1 -0
  67. package/dist/esm/blueprint/trust/validator.js +7 -1
  68. package/dist/esm/blueprint/utils/archive.js +27 -5
  69. package/dist/esm/blueprint/utils/document-paths.d.ts +18 -0
  70. package/dist/esm/blueprint/utils/document-paths.js +45 -12
  71. package/dist/esm/build/cli-mcp-parity.js +15 -0
  72. package/dist/esm/cli/bundle/commands/blueprint.d.ts +1 -0
  73. package/dist/esm/cli/bundle/commands/blueprint.js +1 -0
  74. package/dist/esm/cli/cli.d.ts +1 -1
  75. package/dist/esm/cli/cli.js +43 -0
  76. package/dist/esm/cli/commands/agent-launch.d.ts +8 -1
  77. package/dist/esm/cli/commands/agent-launch.js +93 -4
  78. package/dist/esm/cli/commands/audit-core.d.ts +1 -1
  79. package/dist/esm/cli/commands/audit-core.js +8 -1
  80. package/dist/esm/cli/commands/audit.js +21 -5
  81. package/dist/esm/cli/commands/blueprint/execution.js +18 -6
  82. package/dist/esm/cli/commands/blueprint/mutations.js +59 -40
  83. package/dist/esm/cli/commands/blueprint/router-dispatch.d.ts +2 -1
  84. package/dist/esm/cli/commands/blueprint/router-dispatch.js +20 -1
  85. package/dist/esm/cli/commands/blueprint/router-output.d.ts +21 -0
  86. package/dist/esm/cli/commands/blueprint/router-output.js +48 -17
  87. package/dist/esm/cli/commands/blueprint/router.d.ts +7 -0
  88. package/dist/esm/cli/commands/blueprint/router.js +13 -1
  89. package/dist/esm/cli/commands/ci-preflight.d.ts +13 -2
  90. package/dist/esm/cli/commands/ci-preflight.js +30 -6
  91. package/dist/esm/cli/commands/compile.js +5 -31
  92. package/dist/esm/cli/commands/dash/delivery-status.d.ts +1 -0
  93. package/dist/esm/cli/commands/dash/delivery-status.js +11 -4
  94. package/dist/esm/cli/commands/doctor.js +5 -0
  95. package/dist/esm/cli/commands/hook.d.ts +5 -0
  96. package/dist/esm/cli/commands/hook.js +15 -9
  97. package/dist/esm/cli/commands/init/config.js +2 -2
  98. package/dist/esm/cli/commands/init/convergence-apply.d.ts +88 -0
  99. package/dist/esm/cli/commands/init/convergence-apply.js +325 -0
  100. package/dist/esm/cli/commands/init/convergence-state.d.ts +13 -0
  101. package/dist/esm/cli/commands/init/convergence-state.js +54 -0
  102. package/dist/esm/cli/commands/init/convergence.d.ts +23 -0
  103. package/dist/esm/cli/commands/init/convergence.js +57 -0
  104. package/dist/esm/cli/commands/init/git-convergence.d.ts +19 -0
  105. package/dist/esm/cli/commands/init/git-convergence.js +168 -0
  106. package/dist/esm/cli/commands/init/gitignore-patcher.js +2 -0
  107. package/dist/esm/cli/commands/init/grok-privacy-smoke.d.ts +52 -0
  108. package/dist/esm/cli/commands/init/grok-privacy-smoke.js +91 -0
  109. package/dist/esm/cli/commands/init/host-native-proof.d.ts +18 -0
  110. package/dist/esm/cli/commands/init/host-native-proof.js +88 -0
  111. package/dist/esm/cli/commands/init/host-visibility.d.ts +2 -2
  112. package/dist/esm/cli/commands/init/host-visibility.js +94 -5
  113. package/dist/esm/cli/commands/init/index.d.ts +3 -0
  114. package/dist/esm/cli/commands/init/index.js +326 -173
  115. package/dist/esm/cli/commands/init/mcp-spec.d.ts +57 -0
  116. package/dist/esm/cli/commands/init/mcp-spec.js +273 -0
  117. package/dist/esm/cli/commands/init/merge.d.ts +1 -0
  118. package/dist/esm/cli/commands/init/merge.js +30 -1
  119. package/dist/esm/cli/commands/init/package-root.d.ts +7 -0
  120. package/dist/esm/cli/commands/init/package-root.js +10 -2
  121. package/dist/esm/cli/commands/init/plugin-cache-prune.d.ts +0 -3
  122. package/dist/esm/cli/commands/init/plugin-cache-prune.js +0 -1
  123. package/dist/esm/cli/commands/init/scaffold-agents-md.d.ts +1 -1
  124. package/dist/esm/cli/commands/init/scaffold-agents-md.js +9 -3
  125. package/dist/esm/cli/commands/init/scaffold-base-kit.d.ts +20 -0
  126. package/dist/esm/cli/commands/init/scaffold-base-kit.js +61 -5
  127. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/grok.d.ts +14 -8
  128. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/grok.js +41 -24
  129. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.d.ts +2 -0
  130. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.js +54 -24
  131. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.d.ts +12 -31
  132. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +288 -202
  133. package/dist/esm/cli/commands/init/scaffolders/file-transaction.d.ts +22 -0
  134. package/dist/esm/cli/commands/init/scaffolders/file-transaction.js +230 -0
  135. package/dist/esm/cli/commands/init/scaffolders/grok-mcp/index.d.ts +38 -0
  136. package/dist/esm/cli/commands/init/scaffolders/grok-mcp/index.js +73 -0
  137. package/dist/esm/cli/commands/init/scaffolders/opencode-plugin/index.d.ts +21 -0
  138. package/dist/esm/cli/commands/init/scaffolders/opencode-plugin/index.js +559 -63
  139. package/dist/esm/cli/commands/init/untracked-collision.d.ts +14 -0
  140. package/dist/esm/cli/commands/init/untracked-collision.js +42 -0
  141. package/dist/esm/cli/commands/opencode-probe.d.ts +107 -0
  142. package/dist/esm/cli/commands/opencode-probe.js +323 -0
  143. package/dist/esm/cli/commands/opencode-rotate.d.ts +72 -0
  144. package/dist/esm/cli/commands/opencode-rotate.js +147 -0
  145. package/dist/esm/cli/commands/public-readiness.d.ts +11 -0
  146. package/dist/esm/cli/commands/public-readiness.js +42 -0
  147. package/dist/esm/cli/commands/quality-runner.js +3 -3
  148. package/dist/esm/cli/commands/review.d.ts +35 -3
  149. package/dist/esm/cli/commands/review.js +751 -209
  150. package/dist/esm/cli/commands/run-wait.d.ts +2 -0
  151. package/dist/esm/cli/commands/run-wait.js +122 -0
  152. package/dist/esm/cli/commands/rust-check.d.ts +24 -0
  153. package/dist/esm/cli/commands/rust-check.js +110 -0
  154. package/dist/esm/cli/commands/setup-help.js +1 -1
  155. package/dist/esm/cli/commands/setup-repair-help.js +1 -1
  156. package/dist/esm/cli/commands/typecheck.d.ts +1 -0
  157. package/dist/esm/cli/commands/typecheck.js +30 -1
  158. package/dist/esm/cli/commands/worktree/router-dispatch.d.ts +6 -0
  159. package/dist/esm/cli/commands/worktree/router-dispatch.js +126 -9
  160. package/dist/esm/cli/commands/worktree/router.js +10 -0
  161. package/dist/esm/cli/direct-provider-launch.js +9 -5
  162. package/dist/esm/cli/optional-tool-freshness.d.ts +27 -0
  163. package/dist/esm/cli/optional-tool-freshness.js +219 -9
  164. package/dist/esm/daemon/domains/git-actions.d.ts +29 -0
  165. package/dist/esm/daemon/domains/git-actions.js +136 -0
  166. package/dist/esm/daemon/domains/run-registry-interop.d.ts +16 -0
  167. package/dist/esm/daemon/domains/run-registry-interop.js +45 -0
  168. package/dist/esm/daemon/domains/sessions-search.d.ts +11 -0
  169. package/dist/esm/daemon/domains/sessions-search.js +29 -0
  170. package/dist/esm/daemon/handlers/blueprints-handlers.d.ts +5 -0
  171. package/dist/esm/daemon/handlers/blueprints-handlers.js +36 -0
  172. package/dist/esm/daemon/handlers/deploy-handlers.d.ts +6 -0
  173. package/dist/esm/daemon/handlers/deploy-handlers.js +5 -0
  174. package/dist/esm/daemon/handlers/fleet-handlers.d.ts +15 -0
  175. package/dist/esm/daemon/handlers/fleet-handlers.js +91 -0
  176. package/dist/esm/daemon/handlers/git-handlers.d.ts +6 -0
  177. package/dist/esm/daemon/handlers/git-handlers.js +27 -0
  178. package/dist/esm/daemon/handlers/product.d.ts +7 -0
  179. package/dist/esm/daemon/handlers/product.js +10 -119
  180. package/dist/esm/daemon/handlers/sessions-handlers.d.ts +12 -0
  181. package/dist/esm/daemon/handlers/sessions-handlers.js +31 -0
  182. package/dist/esm/daemon/handlers/shared.d.ts +5 -0
  183. package/dist/esm/daemon/handlers/shared.js +16 -0
  184. package/dist/esm/daemon/methods.d.ts +8 -1
  185. package/dist/esm/daemon/methods.js +7 -0
  186. package/dist/esm/daemon/protocol/contract.d.ts +43 -0
  187. package/dist/esm/daemon/protocol/contract.js +26 -0
  188. package/dist/esm/daemon/protocol/index.d.ts +1 -1
  189. package/dist/esm/daemon/protocol/index.js +1 -1
  190. package/dist/esm/hooks/__conformance__/matrix.d.ts +6 -0
  191. package/dist/esm/hooks/__conformance__/matrix.js +137 -0
  192. package/dist/esm/hooks/conformance/batch.js +5 -0
  193. package/dist/esm/hooks/doctor.d.ts +5 -3
  194. package/dist/esm/hooks/doctor.js +67 -16
  195. package/dist/esm/hooks/permission-request/index.js +9 -3
  196. package/dist/esm/hooks/permission-request/policy.d.ts +9 -0
  197. package/dist/esm/hooks/permission-request/policy.js +13 -0
  198. package/dist/esm/hooks/pretool-guard/dev-routing.d.ts +9 -0
  199. package/dist/esm/hooks/pretool-guard/dev-routing.js +41 -5
  200. package/dist/esm/hooks/pretool-guard/logger.d.ts +7 -0
  201. package/dist/esm/hooks/pretool-guard/logger.js +16 -0
  202. package/dist/esm/hooks/pretool-guard/runner.d.ts +6 -0
  203. package/dist/esm/hooks/pretool-guard/runner.js +68 -16
  204. package/dist/esm/hooks/pretool-guard/validators/blueprint.js +30 -10
  205. package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +54 -0
  206. package/dist/esm/hooks/pretool-guard/validators/index.d.ts +7 -0
  207. package/dist/esm/hooks/pretool-guard/validators/index.js +21 -0
  208. package/dist/esm/hooks/pretool-guard/validators/path-contract.d.ts +19 -0
  209. package/dist/esm/hooks/pretool-guard/validators/path-contract.js +25 -8
  210. package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.js +6 -1
  211. package/dist/esm/hooks/shared/guard-outcome.d.ts +149 -0
  212. package/dist/esm/hooks/shared/guard-outcome.js +215 -0
  213. package/dist/esm/hooks/shared/hook-bootstrap.d.ts +26 -0
  214. package/dist/esm/hooks/shared/hook-bootstrap.js +64 -1
  215. package/dist/esm/hooks/shared/types.d.ts +8 -5
  216. package/dist/esm/hooks/shared/types.js +50 -9
  217. package/dist/esm/hooks/stop/qa-changed-files.d.ts +11 -0
  218. package/dist/esm/hooks/stop/qa-changed-files.js +92 -0
  219. package/dist/esm/mcp/blueprint/_shared/lifecycle.js +14 -1
  220. package/dist/esm/mcp/blueprint/handlers/document-mutations.d.ts +5 -0
  221. package/dist/esm/mcp/blueprint/handlers/document-mutations.js +109 -11
  222. package/dist/esm/mcp/blueprint/handlers/review-log.js +8 -1
  223. package/dist/esm/mcp/blueprint/handlers/task-advance.js +113 -54
  224. package/dist/esm/mcp/blueprint/handlers/task-verify.js +39 -19
  225. package/dist/esm/mcp/blueprint/registration.js +3 -2
  226. package/dist/esm/mcp/server.d.ts +1 -0
  227. package/dist/esm/mcp/server.js +27 -0
  228. package/dist/esm/mcp/tools/_names.d.ts +1 -1
  229. package/dist/esm/mcp/tools/_names.js +5 -0
  230. package/dist/esm/mcp/tools/_registry.js +10 -0
  231. package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
  232. package/dist/esm/mcp/tools/_shared/result.js +44 -1
  233. package/dist/esm/mcp/tools/audit.d.ts +5 -1
  234. package/dist/esm/mcp/tools/audit.js +42 -0
  235. package/dist/esm/mcp/tools/audits.d.ts +4 -0
  236. package/dist/esm/mcp/tools/ci-preflight.d.ts +50 -0
  237. package/dist/esm/mcp/tools/ci-preflight.js +183 -0
  238. package/dist/esm/mcp/tools/pr-upsert.js +39 -1
  239. package/dist/esm/mcp/tools/review-run.d.ts +28 -0
  240. package/dist/esm/mcp/tools/review-run.js +216 -0
  241. package/dist/esm/mcp/tools/run-wait.d.ts +86 -0
  242. package/dist/esm/mcp/tools/run-wait.js +593 -0
  243. package/dist/esm/mcp/tools/session-fetch-and-index.d.ts +1 -1
  244. package/dist/esm/mcp/tools/session-id.d.ts +3 -0
  245. package/dist/esm/mcp/tools/session-id.js +45 -0
  246. package/dist/esm/mcp/tools/session-info.d.ts +3 -0
  247. package/dist/esm/mcp/tools/session-info.js +49 -0
  248. package/dist/esm/output-transforms/audit.d.ts +23 -0
  249. package/dist/esm/output-transforms/audit.js +47 -0
  250. package/dist/esm/output-transforms/index.js +2 -0
  251. package/dist/esm/package.json +2 -0
  252. package/dist/esm/platform/client.d.ts +7 -1
  253. package/dist/esm/platform/github-contract-source.d.ts +2 -1
  254. package/dist/esm/pr-description/contract.js +24 -4
  255. package/dist/esm/review/authority.js +27 -14
  256. package/dist/esm/review/availability.d.ts +33 -0
  257. package/dist/esm/review/availability.js +122 -20
  258. package/dist/esm/review/delivery-verifier.d.ts +27 -0
  259. package/dist/esm/review/delivery-verifier.js +115 -0
  260. package/dist/esm/review/events.d.ts +3 -2
  261. package/dist/esm/review/events.js +23 -3
  262. package/dist/esm/review/execution/adapters.d.ts +11 -1
  263. package/dist/esm/review/execution/adapters.js +36 -10
  264. package/dist/esm/review/execution/artifacts.d.ts +1 -1
  265. package/dist/esm/review/execution/output-policy.d.ts +2 -0
  266. package/dist/esm/review/execution/output-policy.js +12 -0
  267. package/dist/esm/review/execution/review-checkout.js +121 -21
  268. package/dist/esm/review/execution/supervisor.js +36 -4
  269. package/dist/esm/review/execution/types.d.ts +9 -0
  270. package/dist/esm/review/opencode-account.d.ts +24 -0
  271. package/dist/esm/review/opencode-account.js +68 -0
  272. package/dist/esm/review/opencode-auth.d.ts +33 -0
  273. package/dist/esm/review/opencode-auth.js +79 -0
  274. package/dist/esm/review/subject.d.ts +25 -0
  275. package/dist/esm/review/subject.js +116 -26
  276. package/dist/esm/runtime/opencode-account-materializer.d.ts +34 -0
  277. package/dist/esm/runtime/opencode-account-materializer.js +33 -0
  278. package/dist/esm/rust/affected-manifests.d.ts +38 -0
  279. package/dist/esm/rust/affected-manifests.js +69 -0
  280. package/dist/esm/session-memory/fetch-index.d.ts +1 -1
  281. package/dist/esm/status/snapshot.js +32 -25
  282. package/dist/esm/test/duration-sequencer.d.ts +20 -0
  283. package/dist/esm/test/duration-sequencer.js +54 -0
  284. package/dist/esm/test/shard-durations.json +922 -0
  285. package/dist/esm/test/worker-budget.d.ts +6 -0
  286. package/dist/esm/test/worker-budget.js +59 -1
  287. package/dist/esm/test-helpers/global-setup.d.ts +1 -2
  288. package/dist/esm/test-helpers/global-setup.js +79 -32
  289. package/dist/esm/typecheck/planner.d.ts +9 -1
  290. package/dist/esm/typecheck/planner.js +31 -0
  291. package/dist/esm/ultragoal/service.d.ts +7 -0
  292. package/dist/esm/ultragoal/service.js +42 -2
  293. package/dist/esm/utils/write-json-file.js +3 -1
  294. package/dist/esm/worktrees/codex-project-trust.d.ts +15 -0
  295. package/dist/esm/worktrees/codex-project-trust.js +56 -0
  296. package/dist/esm/worktrees/orphan-scan.d.ts +50 -0
  297. package/dist/esm/worktrees/orphan-scan.js +211 -0
  298. package/dist/esm/worktrees/registry.d.ts +8 -0
  299. package/dist/esm/worktrees/registry.js +18 -3
  300. package/package.json +18 -13
@@ -110,6 +110,96 @@ function denyDecision(obj) {
110
110
  return "deny";
111
111
  return null;
112
112
  }
113
+ /** Extract the host-displayed deny reason from either envelope shape. */
114
+ function denyReasonText(obj) {
115
+ const hookSpecific = obj?.hookSpecificOutput;
116
+ if (hookSpecific && typeof hookSpecific === "object") {
117
+ const reason = hookSpecific.permissionDecisionReason;
118
+ if (typeof reason === "string")
119
+ return reason;
120
+ }
121
+ // Grok dual envelope carries a top-level reason.
122
+ if (typeof obj?.reason === "string")
123
+ return obj.reason;
124
+ return null;
125
+ }
126
+ const GUARD_CODED_REASON_RE = /^GUARD_[A-Z0-9_]+: /u;
127
+ /**
128
+ * Every PreToolUse deny reason on a parseable stdout envelope carries a stable
129
+ * `GUARD_<CODE>: ` prefix within the 80-char permissionDecisionReason budget.
130
+ * The code is the cross-host contract; the prose is truncated to fit.
131
+ *
132
+ * This reads `result.stdout` directly (NOT the decision-path `obj`, which is
133
+ * null for a Claude exit-2 deny) so the coded reason is asserted on BOTH hosts —
134
+ * pretool-guard always prints the deny envelope to stdout (dual for claude/grok,
135
+ * hookSpecificOutput-only for codex) even when Claude reads the exit code for the
136
+ * decision itself. Non-JSON stdout (the synthetic "stdout is ignored" fixture)
137
+ * and the empty-stdout exit-code-deny convention carry no reason and are skipped.
138
+ */
139
+ /** Parse an optional deny reason out of a JSON stdout deny envelope. */
140
+ function stdoutDenyReason(stdout) {
141
+ const text = stdout.trim();
142
+ if (text === "")
143
+ return null;
144
+ let parsed;
145
+ try {
146
+ parsed = JSON.parse(text);
147
+ }
148
+ catch {
149
+ return null;
150
+ }
151
+ if (parsed === null || typeof parsed !== "object")
152
+ return null;
153
+ return denyReasonText(parsed);
154
+ }
155
+ /** First GUARD_-coded line in a captured stderr blob (Claude's exit-2 channel). */
156
+ function stderrCodedReason(stderr) {
157
+ if (!stderr)
158
+ return null;
159
+ for (const line of stderr.split("\n")) {
160
+ if (GUARD_CODED_REASON_RE.test(line.trim()))
161
+ return line.trim();
162
+ }
163
+ return null;
164
+ }
165
+ /** Assert a single reason is GUARD_-coded and within the 80-code-point budget. */
166
+ function assertCodedWithinBudget(row, label, reason) {
167
+ if (!GUARD_CODED_REASON_RE.test(reason)) {
168
+ throw new Error(`[${row.name}] ${label} must start with a GUARD_ code prefix, got ${JSON.stringify(reason)}`);
169
+ }
170
+ const points = [...reason].length;
171
+ if (points > 80) {
172
+ throw new Error(`[${row.name}] ${label} exceeds the 80-code-point budget (${points}): ${JSON.stringify(reason)}`);
173
+ }
174
+ }
175
+ function assertGuardCodedDenyReason(row, result) {
176
+ // The coded reason must reach the host on the channel it reads: the stdout
177
+ // deny envelope (Codex/Grok) or stderr (Claude reads stderr on an exit-2
178
+ // PreToolUse deny). Single-spawn tests capture the host boundary directly;
179
+ // the batch runner installs a row-scoped intentional-stderr capture so this
180
+ // same assertion also applies to source/compiled parity.
181
+ const reason = stdoutDenyReason(result.stdout) ?? stderrCodedReason(result.stderr);
182
+ if (reason === null) {
183
+ throw new Error(`[${row.name}] a PreToolUse deny must carry a GUARD_-coded reason on the stdout envelope or stderr; found none`);
184
+ }
185
+ assertCodedWithinBudget(row, "deny reason", reason);
186
+ }
187
+ function assertCodexExitTwoBlockingReason(row, result) {
188
+ if (row.host !== "codex" ||
189
+ row.event !== "PreToolUse" ||
190
+ result.exitCode !== 2 ||
191
+ result.stderr === undefined) {
192
+ return;
193
+ }
194
+ const stderrReason = stderrCodedReason(result.stderr);
195
+ if (stderrReason === null) {
196
+ throw new Error(`[${row.name}] Codex exit-2 PreToolUse deny must write a GUARD_-coded blocking reason to stderr`);
197
+ }
198
+ const stdoutReason = stdoutDenyReason(result.stdout);
199
+ if (stdoutReason !== null && stderrReason !== stdoutReason) {
200
+ throw new Error(`[${row.name}] Codex stdout and stderr deny reasons must match exactly`);
201
+ }
202
+ }
113
203
  /** PermissionRequest golden wire: hookSpecificOutput.decision.behavior allow|deny. */
114
204
  function permissionRequestBehavior(obj) {
115
205
  const hookSpecific = obj?.hookSpecificOutput;
@@ -121,6 +211,45 @@ function permissionRequestBehavior(obj) {
121
211
  const behavior = decision.behavior;
122
212
  return typeof behavior === "string" ? behavior : null;
123
213
  }
214
+ /** PermissionRequest golden wire message: hookSpecificOutput.decision.message. */
215
+ function permissionRequestMessage(obj) {
216
+ const hookSpecific = obj?.hookSpecificOutput;
217
+ if (!hookSpecific || typeof hookSpecific !== "object")
218
+ return null;
219
+ const decision = hookSpecific.decision;
220
+ if (!decision || typeof decision !== "object")
221
+ return null;
222
+ const message = decision.message;
223
+ return typeof message === "string" ? message : null;
224
+ }
225
+ /**
226
+ * A PermissionRequest allow/deny message on a parseable envelope carries the
227
+ * same `GUARD_<CODE>: ` prefix within the 80-char budget as PreToolUse denies —
228
+ * the vocabulary is central, not PreToolUse-only. Skipped when there is no
229
+ * envelope message (e.g. a pure exit-2 deny with no stdout).
230
+ */
231
+ function assertGuardCodedPermissionMessage(row, result, expect) {
232
+ let stdoutMessage = null;
233
+ const text = result.stdout.trim();
234
+ if (text !== "") {
235
+ try {
236
+ const parsed = JSON.parse(text);
237
+ if (parsed !== null && typeof parsed === "object") {
238
+ stdoutMessage = permissionRequestMessage(parsed);
239
+ }
240
+ }
241
+ catch {
242
+ stdoutMessage = null;
243
+ }
244
+ }
245
+ // A deny exits 2 and also carries the coded message on stderr; an allow exits 0
246
+ // with the coded message on the stdout envelope.
247
+ const message = stdoutMessage ?? stderrCodedReason(result.stderr);
248
+ if (message === null) {
249
+ throw new Error(`[${row.name}] PermissionRequest ${expect} must carry a GUARD_-coded message on the stdout envelope or stderr; found none`);
250
+ }
251
+ assertCodedWithinBudget(row, "PermissionRequest message", message);
252
+ }
124
253
  function assertNoCodexUnsupportedFields(row, obj) {
125
254
  if (row.host !== "codex" || obj === null)
126
255
  return;
@@ -170,6 +299,11 @@ export function assertConformance(row, result) {
170
299
  // exit-code deny convention). A deny envelope printed just before the hook
171
300
  // crashes (exit 1) is not a valid decision.
172
301
  assertExitCode(row, result, true);
302
+ // Every deny reason is GUARD_-coded and within the 80-char budget on
303
+ // both Tier-1 hosts (read from stdout, which carries the envelope even
304
+ // when Claude uses the exit code for the decision).
305
+ assertGuardCodedDenyReason(row, result);
306
+ assertCodexExitTwoBlockingReason(row, result);
173
307
  }
174
308
  else {
175
309
  if (decision === "deny") {
@@ -196,6 +330,8 @@ export function assertConformance(row, result) {
196
330
  if (result.exitCode !== 2) {
197
331
  throw new Error(`[${row.name}] expected PermissionRequest deny via exit 2, got exit ${result.exitCode ?? "null"}`);
198
332
  }
333
+ // The deny message carries the central GUARD_ code contract too.
334
+ assertGuardCodedPermissionMessage(row, result, "deny");
199
335
  return;
200
336
  }
201
337
  if (row.expect === "allow") {
@@ -203,6 +339,7 @@ export function assertConformance(row, result) {
203
339
  throw new Error(`[${row.name}] expected PermissionRequest allow envelope, got behavior=${behavior ?? "none"} (stdout: ${result.stdout.slice(0, 160)})`);
204
340
  }
205
341
  assertExitCode(row, result, false);
342
+ assertGuardCodedPermissionMessage(row, result, "allow");
206
343
  return;
207
344
  }
208
345
  // pass-through: host default UI — empty object (or empty stdout), no decision, exit 0.
@@ -85,6 +85,7 @@ export async function runHookConformanceBatch() {
85
85
  }
86
86
  const raw = await readBatchStdin();
87
87
  const { isHookName, runHookCommand } = await import("#cli/commands/hook.js");
88
+ const { installGuardStderrCapture } = await import("#hooks/shared/hook-bootstrap");
88
89
  const payload = parseBatchPayload(raw, isHookName);
89
90
  const protocolWrite = process.stdout.write.bind(process.stdout);
90
91
  for (const row of payload.rows) {
@@ -92,6 +93,9 @@ export async function runHookConformanceBatch() {
92
93
  let stdout = "";
93
94
  let stderr = "";
94
95
  const restoreEnv = setRowEnvironment(row);
96
+ const restoreGuardStderrCapture = installGuardStderrCapture((text) => {
97
+ stderr += text;
98
+ });
95
99
  const originalStdoutWrite = process.stdout.write;
96
100
  const originalStderrWrite = process.stderr.write;
97
101
  process.stdout.write = (chunk) => {
@@ -112,6 +116,7 @@ export async function runHookConformanceBatch() {
112
116
  finally {
113
117
  process.stdout.write = originalStdoutWrite;
114
118
  process.stderr.write = originalStderrWrite;
119
+ restoreGuardStderrCapture();
115
120
  restoreEnv();
116
121
  }
117
122
  protocolWrite(`${JSON.stringify({ type: "result", id: row.id, stdout, stderr, exitCode })}\n`);
@@ -118,15 +118,17 @@ export declare function checkCodexConfigKeys(configFilePath?: string, deps?: Hos
118
118
  export declare function checkProjectMcpPins(cwd?: string, deps?: HostConfigDoctorDeps): DoctorCheck;
119
119
  export interface ClaudeMcpDuplicateRegistrationOptions {
120
120
  readonly home?: string;
121
+ /** Project root used to discover a project-local `.mcp.json` registration. */
122
+ readonly projectRoot?: string;
121
123
  readonly exists?: (path: string) => boolean;
122
124
  readonly readFile?: (path: string) => string;
123
125
  }
124
126
  /**
125
127
  * `claude-mcp-duplicate-registration` — advisory. A machine can register the
126
128
  * webpresso MCP server twice for the same Claude Code session: a manual
127
- * `claude mcp add webpresso` entry in `~/.claude.json` (user scope or a
128
- * project scope) plus the agent-kit marketplace plugin's bundled server
129
- * (plugin cache under `~/.claude/plugins/`). Both spawn `wp mcp`, so affected
129
+ * `claude mcp add webpresso` entry in Claude Code's home JSON config (user
130
+ * scope or a project scope) plus the agent-kit marketplace plugin's bundled
131
+ * server from the Claude plugin cache. Both spawn `wp mcp`, so affected
130
132
  * sessions run two server processes and carry a duplicated wp_* tool list.
131
133
  * Degrades to ok on missing/unreadable config — loud only on real duplication.
132
134
  */
@@ -658,11 +658,15 @@ function probeDecisionRow(wpCli, row) {
658
658
  detached: process.platform !== "win32",
659
659
  });
660
660
  let stdout = "";
661
+ let stderr = "";
661
662
  const timeoutMs = hookProbeTimeoutMs();
662
663
  const settle = createOwnedProbeSettler(child, timeoutMs, () => ({ ok: false, detail: `decision probe timed out after ${timeoutMs}ms` }), resolve);
663
664
  child.stdout.on("data", (chunk) => {
664
665
  stdout += String(chunk);
665
666
  });
667
+ child.stderr.on("data", (chunk) => {
668
+ stderr += String(chunk);
669
+ });
666
670
  child.stdin.on?.("error", (err) => {
667
671
  settle({ ok: false, detail: `stdin write failed: ${err.message}` });
668
672
  });
@@ -674,7 +678,7 @@ function probeDecisionRow(wpCli, row) {
674
678
  });
675
679
  child.on("close", (code) => {
676
680
  try {
677
- assertConformance(row, { stdout, exitCode: code });
681
+ assertConformance(row, { stdout, stderr, exitCode: code });
678
682
  settle({ ok: true });
679
683
  }
680
684
  catch (error) {
@@ -1256,13 +1260,15 @@ async function checkClaudeHost() {
1256
1260
  }
1257
1261
  async function checkGrokHost(cwd = process.cwd()) {
1258
1262
  const available = await runCommand("grok", ["--version"]);
1259
- const hooksPath = join(cwd, ".grok", "hooks.json");
1263
+ const hooksPath = join(cwd, ".grok", "hooks", "webpresso.json");
1260
1264
  let hooksPresent = false;
1261
1265
  let hostLabeled = false;
1262
1266
  try {
1263
1267
  const body = readFileSync(hooksPath, "utf8");
1264
1268
  hooksPresent = true;
1265
- hostLabeled = body.includes("WP_HOOK_HOST=grok");
1269
+ // The generated launcher chain shell-quotes the value (WP_HOOK_HOST='grok');
1270
+ // accept any quoting variant so the doctor recognizes its own output.
1271
+ hostLabeled = /WP_HOOK_HOST=(['"]?)grok\1/u.test(body);
1266
1272
  }
1267
1273
  catch {
1268
1274
  // Missing project hooks is advisory when binary absent; fail when binary present.
@@ -1273,8 +1279,8 @@ async function checkGrokHost(cwd = process.cwd()) {
1273
1279
  ok: true,
1274
1280
  detail: hooksPresent
1275
1281
  ? hostLabeled
1276
- ? "skipped (grok not on PATH); native .grok/hooks.json present with WP_HOOK_HOST=grok"
1277
- : "skipped (grok not on PATH); .grok/hooks.json present but missing WP_HOOK_HOST=grok"
1282
+ ? "skipped (grok not on PATH); native .grok/hooks/webpresso.json present with WP_HOOK_HOST=grok"
1283
+ : "skipped (grok not on PATH); .grok/hooks/webpresso.json present but missing WP_HOOK_HOST=grok"
1278
1284
  : "skipped (grok not on PATH)",
1279
1285
  };
1280
1286
  }
@@ -1282,20 +1288,20 @@ async function checkGrokHost(cwd = process.cwd()) {
1282
1288
  return {
1283
1289
  name: "Grok host integration",
1284
1290
  ok: false,
1285
- detail: "grok on PATH but missing project .grok/hooks.json — run wp setup to scaffold native hooks",
1291
+ detail: "grok on PATH but missing project .grok/hooks/webpresso.json — run wp setup to scaffold native hooks, then run /hooks-trust once in Grok",
1286
1292
  };
1287
1293
  }
1288
1294
  if (!hostLabeled) {
1289
1295
  return {
1290
1296
  name: "Grok host integration",
1291
1297
  ok: false,
1292
- detail: ".grok/hooks.json present but managed commands lack WP_HOOK_HOST=grok",
1298
+ detail: ".grok/hooks/webpresso.json present but managed commands lack WP_HOOK_HOST=grok",
1293
1299
  };
1294
1300
  }
1295
1301
  return {
1296
1302
  name: "Grok host integration",
1297
1303
  ok: true,
1298
- detail: "native .grok/hooks.json with WP_HOOK_HOST=grok (PreToolUse fail-open residual applies)",
1304
+ detail: "native .grok/hooks/webpresso.json with WP_HOOK_HOST=grok (trust-gated: run /hooks-trust once; PreToolUse fail-open residual applies)",
1299
1305
  };
1300
1306
  }
1301
1307
  // Markers for the direct hook commands `wp setup` writes into
@@ -1793,9 +1799,9 @@ export function checkProjectMcpPins(cwd = process.cwd(), deps = {}) {
1793
1799
  /**
1794
1800
  * `claude-mcp-duplicate-registration` — advisory. A machine can register the
1795
1801
  * webpresso MCP server twice for the same Claude Code session: a manual
1796
- * `claude mcp add webpresso` entry in `~/.claude.json` (user scope or a
1797
- * project scope) plus the agent-kit marketplace plugin's bundled server
1798
- * (plugin cache under `~/.claude/plugins/`). Both spawn `wp mcp`, so affected
1802
+ * `claude mcp add webpresso` entry in Claude Code's home JSON config (user
1803
+ * scope or a project scope) plus the agent-kit marketplace plugin's bundled
1804
+ * server from the Claude plugin cache. Both spawn `wp mcp`, so affected
1799
1805
  * sessions run two server processes and carry a duplicated wp_* tool list.
1800
1806
  * Degrades to ok on missing/unreadable config — loud only on real duplication.
1801
1807
  */
@@ -1810,17 +1816,17 @@ export function checkClaudeMcpDuplicateRegistration(options = {}) {
1810
1816
  }
1811
1817
  const claudeJsonPath = join(home, ".claude.json");
1812
1818
  if (!exists(claudeJsonPath)) {
1813
- return { name, ok: true, detail: "skipped (no ~/.claude.json)" };
1819
+ return { name, ok: true, detail: "skipped (no Claude Code home JSON config)" };
1814
1820
  }
1815
1821
  let parsed;
1816
1822
  try {
1817
1823
  parsed = JSON.parse(readFile(claudeJsonPath));
1818
1824
  }
1819
1825
  catch {
1820
- return { name, ok: true, detail: "skipped (~/.claude.json unreadable)" };
1826
+ return { name, ok: true, detail: "skipped (Claude Code home JSON config unreadable)" };
1821
1827
  }
1822
1828
  if (parsed === null || typeof parsed !== "object") {
1823
- return { name, ok: true, detail: "skipped (~/.claude.json is not an object)" };
1829
+ return { name, ok: true, detail: "skipped (Claude Code home JSON config is not an object)" };
1824
1830
  }
1825
1831
  const config = parsed;
1826
1832
  const manualSources = [];
@@ -1832,6 +1838,29 @@ export function checkClaudeMcpDuplicateRegistration(options = {}) {
1832
1838
  manualSources.push(`project scope ${projectDir}`);
1833
1839
  }
1834
1840
  }
1841
+ // Also scan project-local `.mcp.json` (and ancestors). Claude Code honors
1842
+ // this file as a third registration surface; the home JSON projects map
1843
+ // does not always mirror it, so a plugin + project-file pair still doubles
1844
+ // the tool list without the home-JSON check ever firing.
1845
+ const projectRoot = options.projectRoot ?? process.cwd();
1846
+ for (const candidate of collectProjectMcpJsonCandidates(projectRoot)) {
1847
+ if (!exists(candidate))
1848
+ continue;
1849
+ try {
1850
+ const projectParsed = JSON.parse(readFile(candidate));
1851
+ if (projectParsed !== null &&
1852
+ typeof projectParsed === "object" &&
1853
+ "mcpServers" in projectParsed &&
1854
+ projectParsed.mcpServers &&
1855
+ "webpresso" in
1856
+ (projectParsed.mcpServers ?? {})) {
1857
+ manualSources.push(`project file ${candidate}`);
1858
+ }
1859
+ }
1860
+ catch {
1861
+ // Unreadable/malformed .mcp.json is not a duplicate signal — degrade.
1862
+ }
1863
+ }
1835
1864
  if (manualSources.length === 0) {
1836
1865
  return { name, ok: true, detail: "plugin-scoped registration only" };
1837
1866
  }
@@ -1839,12 +1868,25 @@ export function checkClaudeMcpDuplicateRegistration(options = {}) {
1839
1868
  name,
1840
1869
  ok: false,
1841
1870
  advisory: true,
1842
- detail: `the agent-kit Claude plugin bundles the webpresso MCP server, but ~/.claude.json also ` +
1871
+ detail: `the agent-kit Claude plugin bundles the webpresso MCP server, but Claude Code's home JSON config also ` +
1843
1872
  `registers "webpresso" manually (${manualSources.join(", ")}). Affected sessions run two ` +
1844
1873
  `wp mcp processes with a duplicated wp_* tool list. Remove the manual entry: ` +
1845
1874
  `claude mcp remove webpresso (run inside the project directory for project-scope entries).`,
1846
1875
  };
1847
1876
  }
1877
+ /** Walk projectRoot → filesystem root collecting `.mcp.json` paths that may register MCP servers. */
1878
+ function collectProjectMcpJsonCandidates(projectRoot) {
1879
+ const candidates = [];
1880
+ let current = projectRoot;
1881
+ for (;;) {
1882
+ candidates.push(join(current, ".mcp.json"));
1883
+ const parent = dirname(current);
1884
+ if (parent === current)
1885
+ break;
1886
+ current = parent;
1887
+ }
1888
+ return candidates;
1889
+ }
1848
1890
  const QUOTED_TOML_STRING = /"((?:[^"\\]|\\.)*)"/gu;
1849
1891
  const DANGLING_SYMLINK_SCAN_DEPTH = 3;
1850
1892
  function collectDanglingWorktreePaths(raw, worktreesRoot, exists, alreadyReported) {
@@ -2049,7 +2091,7 @@ export async function runHooksDoctor(opts = {}) {
2049
2091
  checks.push(checkCodexConfigKeys());
2050
2092
  }
2051
2093
  checks.push(checkProjectMcpPins(opts.cwd));
2052
- checks.push(checkClaudeMcpDuplicateRegistration());
2094
+ checks.push(checkClaudeMcpDuplicateRegistration({ projectRoot: opts.cwd ?? process.cwd() }));
2053
2095
  checks.push({ advisory: true, ...checkDanglingHostPaths() });
2054
2096
  checks.push({ advisory: true, ...checkRootLauncherContract() });
2055
2097
  checks.push({ advisory: true, ...checkNativePluginRuntime() });
@@ -2142,6 +2184,15 @@ export async function runHooksDoctor(opts = {}) {
2142
2184
  detail: "claude required but not on PATH",
2143
2185
  });
2144
2186
  }
2187
+ if (host === "grok") {
2188
+ const available = await runCommand("grok", ["--version"]);
2189
+ if (!available.ok)
2190
+ checks.push({
2191
+ name: "Grok host integration",
2192
+ ok: false,
2193
+ detail: "grok required but not on PATH",
2194
+ });
2195
+ }
2145
2196
  }
2146
2197
  }
2147
2198
  checks.push(checkHooksManifest(opts.cwd));
@@ -9,7 +9,8 @@
9
9
  * Stdin: uses shared `readStdinJson` so conformance-batch can inject payloads via
10
10
  * `WP_CONFORMANCE_STDIN` (same contract as other managed hooks).
11
11
  */
12
- import { readStdinJson, writeHookOutput } from "#hooks/shared/hook-bootstrap";
12
+ import { formatGuardReason } from "#hooks/shared/guard-outcome";
13
+ import { readStdinJson, writeGuardStderr, writeHookOutput } from "#hooks/shared/hook-bootstrap";
13
14
  import { buildPermissionRequestEnvelope } from "./envelope.js";
14
15
  import { evaluatePermissionRequest } from "./policy.js";
15
16
  function isRecord(value) {
@@ -53,9 +54,14 @@ export async function main() {
53
54
  return;
54
55
  }
55
56
  // Claude + Codex share the golden PermissionRequest wire shape
56
- // (hookSpecificOutput.hookEventName + decision.behavior).
57
- writeHookOutput(JSON.stringify(buildPermissionRequestEnvelope(result.decision, result.reason)));
57
+ // (hookSpecificOutput.hookEventName + decision.behavior). The wire message
58
+ // carries the GUARD_* code so allow/deny decisions are keyable across hosts.
59
+ const codedMessage = formatGuardReason(result.code, result.reason);
60
+ writeHookOutput(JSON.stringify(buildPermissionRequestEnvelope(result.decision, codedMessage)));
58
61
  if (result.decision === "deny") {
62
+ // A deny exits 2; deliver the coded message to the real stderr too so it is
63
+ // observable even when a host prefers the exit-code channel.
64
+ writeGuardStderr(`${codedMessage}\n`);
59
65
  // Must use process.exit so runHookCommand's synthetic-exit trap records exit 2.
60
66
  process.exit(2);
61
67
  }
@@ -5,6 +5,7 @@
5
5
  * authoritative when policy is uncertain. Explicit deny only for high-confidence
6
6
  * unsafe escalations; explicit allow only for clearly safe read-only escalations.
7
7
  */
8
+ import type { GuardReasonCode } from "#hooks/shared/guard-outcome";
8
9
  export type PermissionRequestDecision = "allow" | "deny";
9
10
  export type PermissionRequestPolicyInput = {
10
11
  readonly toolName?: string;
@@ -15,13 +16,21 @@ export type PermissionRequestPolicyInput = {
15
16
  export type PermissionRequestPolicyResult = {
16
17
  readonly decision: "allow";
17
18
  readonly reason: string;
19
+ readonly code: GuardReasonCode;
18
20
  } | {
19
21
  readonly decision: "deny";
20
22
  readonly reason: string;
23
+ readonly code: GuardReasonCode;
21
24
  } | {
22
25
  readonly decision: undefined;
23
26
  readonly reason: string;
27
+ readonly code: GuardReasonCode;
24
28
  };
29
+ /**
30
+ * Every GUARD_* code the permission-request policy can emit. Consumed by the
31
+ * guard-outcome registry-completeness test.
32
+ */
33
+ export declare const PERMISSION_GUARD_CODES: readonly ["GUARD_PERMISSION_ALLOWED", "GUARD_PERMISSION_DENIED", "GUARD_HOST_DEFAULT"];
25
34
  /**
26
35
  * Decide allow / deny / pass-through for a PermissionRequest payload.
27
36
  */
@@ -5,6 +5,15 @@
5
5
  * authoritative when policy is uncertain. Explicit deny only for high-confidence
6
6
  * unsafe escalations; explicit allow only for clearly safe read-only escalations.
7
7
  */
8
+ /**
9
+ * Every GUARD_* code the permission-request policy can emit. Consumed by the
10
+ * guard-outcome registry-completeness test.
11
+ */
12
+ export const PERMISSION_GUARD_CODES = [
13
+ "GUARD_PERMISSION_ALLOWED",
14
+ "GUARD_PERMISSION_DENIED",
15
+ "GUARD_HOST_DEFAULT",
16
+ ];
8
17
  const SECRET_PATH_RE = /(?:^|\/)(?:\.env(?:\..+)?|.*\.(?:pem|key|p12|pfx)|id_rsa|id_ed25519|credentials(?:\.json)?|secrets?(?:\.ya?ml)?|\.npmrc|\.netrc)(?:$|\/)/iu;
9
18
  const DANGEROUS_BASH_RE = /\b(?:rm\s+-[a-zA-Z]*f|sudo\s+|curl\s+[^\n]*\|\s*(?:ba)?sh|wget\s+[^\n]*\|\s*(?:ba)?sh|mkfs\.|dd\s+if=)/iu;
10
19
  function asCommand(input) {
@@ -39,12 +48,14 @@ export function evaluatePermissionRequest(input) {
39
48
  return {
40
49
  decision: "deny",
41
50
  reason: "PermissionRequest denied: secret or credential path escalation",
51
+ code: "GUARD_PERMISSION_DENIED",
42
52
  };
43
53
  }
44
54
  if (DANGEROUS_BASH_RE.test(command) || DANGEROUS_BASH_RE.test(blob)) {
45
55
  return {
46
56
  decision: "deny",
47
57
  reason: "PermissionRequest denied: dangerous shell escalation",
58
+ code: "GUARD_PERMISSION_DENIED",
48
59
  };
49
60
  }
50
61
  // Explicit safe read-only escalations (narrow allow).
@@ -54,10 +65,12 @@ export function evaluatePermissionRequest(input) {
54
65
  return {
55
66
  decision: "allow",
56
67
  reason: "PermissionRequest allowed: read-only tool escalation",
68
+ code: "GUARD_PERMISSION_ALLOWED",
57
69
  };
58
70
  }
59
71
  return {
60
72
  decision: undefined,
61
73
  reason: "PermissionRequest pass-through: no high-confidence policy match",
74
+ code: "GUARD_HOST_DEFAULT",
62
75
  };
63
76
  }
@@ -1,14 +1,23 @@
1
+ import type { GuardReasonCode } from "#hooks/shared/guard-outcome";
1
2
  export type GuidanceType = "test" | "lint" | "typecheck" | "qa" | "format" | "e2e" | "worktree";
2
3
  export type RouteAction = {
3
4
  action: "deny";
4
5
  tool: string;
5
6
  guidance: string;
7
+ code: GuardReasonCode;
6
8
  } | {
7
9
  action: "sandbox";
8
10
  guidance: string;
11
+ code: GuardReasonCode;
9
12
  } | {
10
13
  action: "passthrough";
11
14
  };
15
+ /**
16
+ * Every GUARD_* code a dev-routing deny/sandbox can emit. The guard-outcome
17
+ * registry-completeness test asserts this stays a subset of GUARD_REASON_CODES
18
+ * and that each listed code is reachable from a construction site below.
19
+ */
20
+ export declare const DEV_ROUTING_GUARD_CODES: readonly ["GUARD_RAW_GIT_MUTATION", "GUARD_MANUAL_PR_WAIT", "GUARD_PR_DISCLOSURE", "GUARD_WRAPPED_WP", "GUARD_SOURCE_ENTRYPOINT", "GUARD_DEV_TOOL_REDIRECT", "GUARD_DATA_HEAVY_SANDBOX", "GUARD_SESSION_MEMORY_REDIRECT"];
12
21
  export interface RouteDecision {
13
22
  action: RouteAction;
14
23
  }
@@ -1,5 +1,20 @@
1
1
  import { detectWrappedWpCommand, stripLeadingSecretWrappers, wrappedWpGuidanceForArgs, } from "#cli/wrapped-wp";
2
2
  import { isSessionSandboxToolName } from "#hooks/shared/types";
3
+ /**
4
+ * Every GUARD_* code a dev-routing deny/sandbox can emit. The guard-outcome
5
+ * registry-completeness test asserts this stays a subset of GUARD_REASON_CODES
6
+ * and that each listed code is reachable from a construction site below.
7
+ */
8
+ export const DEV_ROUTING_GUARD_CODES = [
9
+ "GUARD_RAW_GIT_MUTATION",
10
+ "GUARD_MANUAL_PR_WAIT",
11
+ "GUARD_PR_DISCLOSURE",
12
+ "GUARD_WRAPPED_WP",
13
+ "GUARD_SOURCE_ENTRYPOINT",
14
+ "GUARD_DEV_TOOL_REDIRECT",
15
+ "GUARD_DATA_HEAVY_SANDBOX",
16
+ "GUARD_SESSION_MEMORY_REDIRECT",
17
+ ];
3
18
  const RAW_MUTATING_GIT_WORKTREE_RE = /^git\s+worktree\s+(?:add|remove|move|prune)(?:\s|$)/u;
4
19
  const MANUAL_PR_WAIT_RE = /^sleep\s+\S+[\s\S]*(?:gh\s+pr\s+(?:view|checks|status)|wp(?:_pr_status|\s+pr\s+status))/u;
5
20
  function routeRawMutatingGitWorktree(command) {
@@ -9,6 +24,7 @@ function routeRawMutatingGitWorktree(command) {
9
24
  action: "deny",
10
25
  tool: "wp_worktree",
11
26
  guidance: "Use the `wp_worktree` MCP tool (or `wp worktree` CLI when MCP is unavailable) instead — managed repositories may mutate linked worktrees only through agent-kit so owner bindings and the global registry stay consistent.",
27
+ code: "GUARD_RAW_GIT_MUTATION",
12
28
  };
13
29
  }
14
30
  function routeManualPrWait(command) {
@@ -18,6 +34,7 @@ function routeManualPrWait(command) {
18
34
  action: "deny",
19
35
  tool: "wp_pr_wait",
20
36
  guidance: "Use wp_pr_wait MCP tool instead — it blocks on PR checks or merge state with bounded final output and avoids manual sleep plus status polling loops",
37
+ code: "GUARD_MANUAL_PR_WAIT",
21
38
  };
22
39
  }
23
40
  const PR_DESCRIPTION_WRITE_GUIDANCE = "Use wp_pr_upsert MCP tool instead — it validates the repository PR disclosure contract before creating or updating a PR title/body.";
@@ -27,6 +44,7 @@ function prDescriptionWriteDeny() {
27
44
  action: "deny",
28
45
  tool: "wp_pr_upsert",
29
46
  guidance: PR_DESCRIPTION_WRITE_GUIDANCE,
47
+ code: "GUARD_PR_DISCLOSURE",
30
48
  };
31
49
  }
32
50
  function isGhPrDescriptionFlag(token) {
@@ -214,7 +232,7 @@ const ROUTING_RULES = [
214
232
  tool: "wp_gain",
215
233
  },
216
234
  {
217
- prefixes: ["vp run public:readiness", "vp run changeset:status"],
235
+ prefixes: ["wp run changeset:status"],
218
236
  guidanceType: "qa",
219
237
  guidance: "Use wp_release_readiness MCP tool instead — release gates are aggregated without publishing, tagging, versioning, or merging",
220
238
  tool: "wp_release_readiness",
@@ -851,7 +869,13 @@ function extractStringField(record, keys) {
851
869
  return null;
852
870
  }
853
871
  function sessionSandboxDecision(tool, guidance) {
854
- return { action: { action: "sandbox", guidance: `Use ${tool} instead — ${guidance}` } };
872
+ return {
873
+ action: {
874
+ action: "sandbox",
875
+ guidance: `Use ${tool} instead — ${guidance}`,
876
+ code: "GUARD_SESSION_MEMORY_REDIRECT",
877
+ },
878
+ };
855
879
  }
856
880
  const MAX_SAFE_GREP_HEAD_LIMIT = 100;
857
881
  function numericField(record, keys) {
@@ -952,6 +976,7 @@ export function routeCommand(command, _sessionId) {
952
976
  action: "deny",
953
977
  tool,
954
978
  guidance,
979
+ code: "GUARD_WRAPPED_WP",
955
980
  },
956
981
  };
957
982
  }
@@ -973,6 +998,7 @@ export function routeCommand(command, _sessionId) {
973
998
  action: "deny",
974
999
  tool: sourceEntrypointRule.tool,
975
1000
  guidance: sourceEntrypointRule.guidance,
1001
+ code: "GUARD_SOURCE_ENTRYPOINT",
976
1002
  },
977
1003
  };
978
1004
  }
@@ -981,20 +1007,30 @@ export function routeCommand(command, _sessionId) {
981
1007
  for (const prefix of rule.prefixes) {
982
1008
  if (matchesPrefix(trimmed, prefix) || matchesDirectToolCommand(trimmed, rule)) {
983
1009
  return {
984
- action: { action: "deny", tool: rule.tool, guidance: rule.guidance },
1010
+ action: {
1011
+ action: "deny",
1012
+ tool: rule.tool,
1013
+ guidance: rule.guidance,
1014
+ code: "GUARD_DEV_TOOL_REDIRECT",
1015
+ },
985
1016
  };
986
1017
  }
987
1018
  }
988
1019
  if (matchesPackageManagerDirectToolCommand(trimmed, rule)) {
989
1020
  return {
990
- action: { action: "deny", tool: rule.tool, guidance: rule.guidance },
1021
+ action: {
1022
+ action: "deny",
1023
+ tool: rule.tool,
1024
+ guidance: rule.guidance,
1025
+ code: "GUARD_DEV_TOOL_REDIRECT",
1026
+ },
991
1027
  };
992
1028
  }
993
1029
  }
994
1030
  // Sandbox rules (data-heavy commands)
995
1031
  for (const { prefix, guidance } of SANDBOX_PREFIXES) {
996
1032
  if (matchesPrefix(trimmed, prefix)) {
997
- return { action: { action: "sandbox", guidance } };
1033
+ return { action: { action: "sandbox", guidance, code: "GUARD_DATA_HEAVY_SANDBOX" } };
998
1034
  }
999
1035
  }
1000
1036
  // Unknown — null (let callers decide)