@webpresso/agent-kit 3.1.30 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @webpresso/agent-kit might be problematic. Click here for more details.

Files changed (257) hide show
  1. package/README.md +53 -15
  2. package/bin/_run.js +179 -19
  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/ci-guardrails-wiring.d.ts +22 -0
  17. package/dist/esm/audit/ci-guardrails-wiring.js +90 -0
  18. package/dist/esm/audit/ci-test-perf.js +30 -0
  19. package/dist/esm/audit/command-surface.js +4 -2
  20. package/dist/esm/audit/host-substitution-risk.d.ts +2 -0
  21. package/dist/esm/audit/host-substitution-risk.js +256 -0
  22. package/dist/esm/audit/registry.d.ts +4 -4
  23. package/dist/esm/audit/registry.js +8 -1
  24. package/dist/esm/audit/toolchain-isolation.js +1 -2
  25. package/dist/esm/audit/typescript-version.d.ts +2 -0
  26. package/dist/esm/audit/typescript-version.js +123 -0
  27. package/dist/esm/blueprint/core/parser.js +43 -1
  28. package/dist/esm/blueprint/core/validation/criteria.d.ts +14 -0
  29. package/dist/esm/blueprint/core/validation/criteria.js +41 -2
  30. package/dist/esm/blueprint/core/validation/state.js +25 -14
  31. package/dist/esm/blueprint/db/migrations/run.js +4 -2
  32. package/dist/esm/blueprint/lifecycle/audit.d.ts +20 -0
  33. package/dist/esm/blueprint/lifecycle/audit.js +65 -10
  34. package/dist/esm/blueprint/lifecycle/canonicalize.d.ts +6 -0
  35. package/dist/esm/blueprint/lifecycle/canonicalize.js +135 -0
  36. package/dist/esm/blueprint/lifecycle/local.d.ts +2 -1
  37. package/dist/esm/blueprint/lifecycle/local.js +19 -3
  38. package/dist/esm/blueprint/lifecycle/review-provenance.js +36 -11
  39. package/dist/esm/blueprint/service/BlueprintCreationService.d.ts +1 -0
  40. package/dist/esm/blueprint/service/BlueprintCreationService.js +53 -4
  41. package/dist/esm/blueprint/service/blueprint-tech-debt-links.js +16 -2
  42. package/dist/esm/blueprint/trust/command-runner.d.ts +26 -0
  43. package/dist/esm/blueprint/trust/command-runner.js +148 -0
  44. package/dist/esm/blueprint/trust/gates.d.ts +7 -0
  45. package/dist/esm/blueprint/trust/gates.js +20 -0
  46. package/dist/esm/blueprint/trust/promotion.d.ts +8 -0
  47. package/dist/esm/blueprint/trust/promotion.js +62 -112
  48. package/dist/esm/blueprint/trust/scaffold.d.ts +19 -0
  49. package/dist/esm/blueprint/trust/scaffold.js +64 -0
  50. package/dist/esm/blueprint/trust/validator.d.ts +1 -0
  51. package/dist/esm/blueprint/trust/validator.js +7 -1
  52. package/dist/esm/blueprint/utils/archive.js +27 -5
  53. package/dist/esm/blueprint/utils/document-paths.d.ts +18 -0
  54. package/dist/esm/blueprint/utils/document-paths.js +45 -12
  55. package/dist/esm/build/cli-mcp-parity.js +10 -0
  56. package/dist/esm/cli/bundle/commands/blueprint.d.ts +1 -0
  57. package/dist/esm/cli/bundle/commands/blueprint.js +1 -0
  58. package/dist/esm/cli/cli.d.ts +1 -1
  59. package/dist/esm/cli/cli.js +34 -0
  60. package/dist/esm/cli/commands/agent-launch.d.ts +6 -1
  61. package/dist/esm/cli/commands/agent-launch.js +82 -4
  62. package/dist/esm/cli/commands/audit-core.d.ts +1 -1
  63. package/dist/esm/cli/commands/audit-core.js +8 -1
  64. package/dist/esm/cli/commands/audit.js +17 -5
  65. package/dist/esm/cli/commands/blueprint/execution.js +18 -6
  66. package/dist/esm/cli/commands/blueprint/mutations.js +59 -40
  67. package/dist/esm/cli/commands/blueprint/router-dispatch.d.ts +2 -1
  68. package/dist/esm/cli/commands/blueprint/router-dispatch.js +20 -1
  69. package/dist/esm/cli/commands/blueprint/router-output.d.ts +21 -0
  70. package/dist/esm/cli/commands/blueprint/router-output.js +48 -17
  71. package/dist/esm/cli/commands/blueprint/router.d.ts +7 -0
  72. package/dist/esm/cli/commands/blueprint/router.js +13 -1
  73. package/dist/esm/cli/commands/ci-preflight.d.ts +13 -2
  74. package/dist/esm/cli/commands/ci-preflight.js +6 -3
  75. package/dist/esm/cli/commands/compile.js +5 -31
  76. package/dist/esm/cli/commands/dash/delivery-status.d.ts +1 -0
  77. package/dist/esm/cli/commands/dash/delivery-status.js +11 -4
  78. package/dist/esm/cli/commands/doctor.js +5 -0
  79. package/dist/esm/cli/commands/hook.d.ts +5 -0
  80. package/dist/esm/cli/commands/hook.js +15 -9
  81. package/dist/esm/cli/commands/init/config.js +2 -2
  82. package/dist/esm/cli/commands/init/convergence-apply.d.ts +24 -0
  83. package/dist/esm/cli/commands/init/convergence-apply.js +221 -0
  84. package/dist/esm/cli/commands/init/convergence-state.d.ts +13 -0
  85. package/dist/esm/cli/commands/init/convergence-state.js +54 -0
  86. package/dist/esm/cli/commands/init/convergence.d.ts +23 -0
  87. package/dist/esm/cli/commands/init/convergence.js +57 -0
  88. package/dist/esm/cli/commands/init/git-convergence.d.ts +19 -0
  89. package/dist/esm/cli/commands/init/git-convergence.js +168 -0
  90. package/dist/esm/cli/commands/init/gitignore-patcher.js +2 -0
  91. package/dist/esm/cli/commands/init/grok-privacy-smoke.d.ts +52 -0
  92. package/dist/esm/cli/commands/init/grok-privacy-smoke.js +91 -0
  93. package/dist/esm/cli/commands/init/host-native-proof.d.ts +18 -0
  94. package/dist/esm/cli/commands/init/host-native-proof.js +88 -0
  95. package/dist/esm/cli/commands/init/host-visibility.d.ts +2 -2
  96. package/dist/esm/cli/commands/init/host-visibility.js +94 -5
  97. package/dist/esm/cli/commands/init/index.d.ts +1 -0
  98. package/dist/esm/cli/commands/init/index.js +326 -173
  99. package/dist/esm/cli/commands/init/mcp-spec.d.ts +57 -0
  100. package/dist/esm/cli/commands/init/mcp-spec.js +273 -0
  101. package/dist/esm/cli/commands/init/merge.d.ts +1 -0
  102. package/dist/esm/cli/commands/init/merge.js +30 -1
  103. package/dist/esm/cli/commands/init/plugin-cache-prune.d.ts +0 -3
  104. package/dist/esm/cli/commands/init/plugin-cache-prune.js +0 -1
  105. package/dist/esm/cli/commands/init/scaffold-agents-md.d.ts +1 -1
  106. package/dist/esm/cli/commands/init/scaffold-agents-md.js +9 -3
  107. package/dist/esm/cli/commands/init/scaffold-base-kit.d.ts +11 -0
  108. package/dist/esm/cli/commands/init/scaffold-base-kit.js +56 -1
  109. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/grok.d.ts +14 -8
  110. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/grok.js +41 -24
  111. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.d.ts +2 -0
  112. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.js +51 -23
  113. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.d.ts +12 -31
  114. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +288 -202
  115. package/dist/esm/cli/commands/init/scaffolders/file-transaction.d.ts +7 -0
  116. package/dist/esm/cli/commands/init/scaffolders/file-transaction.js +58 -0
  117. package/dist/esm/cli/commands/init/scaffolders/grok-mcp/index.d.ts +38 -0
  118. package/dist/esm/cli/commands/init/scaffolders/grok-mcp/index.js +73 -0
  119. package/dist/esm/cli/commands/init/scaffolders/opencode-plugin/index.d.ts +21 -0
  120. package/dist/esm/cli/commands/init/scaffolders/opencode-plugin/index.js +559 -63
  121. package/dist/esm/cli/commands/init/untracked-collision.d.ts +14 -0
  122. package/dist/esm/cli/commands/init/untracked-collision.js +42 -0
  123. package/dist/esm/cli/commands/opencode-rotate.d.ts +49 -0
  124. package/dist/esm/cli/commands/opencode-rotate.js +135 -0
  125. package/dist/esm/cli/commands/public-readiness.d.ts +11 -0
  126. package/dist/esm/cli/commands/public-readiness.js +42 -0
  127. package/dist/esm/cli/commands/review.d.ts +35 -3
  128. package/dist/esm/cli/commands/review.js +729 -209
  129. package/dist/esm/cli/commands/run-wait.d.ts +2 -0
  130. package/dist/esm/cli/commands/run-wait.js +122 -0
  131. package/dist/esm/cli/commands/setup-help.js +1 -1
  132. package/dist/esm/cli/commands/setup-repair-help.js +1 -1
  133. package/dist/esm/cli/commands/worktree/router-dispatch.js +13 -8
  134. package/dist/esm/cli/direct-provider-launch.js +9 -5
  135. package/dist/esm/cli/optional-tool-freshness.d.ts +27 -0
  136. package/dist/esm/cli/optional-tool-freshness.js +219 -9
  137. package/dist/esm/daemon/domains/git-actions.d.ts +29 -0
  138. package/dist/esm/daemon/domains/git-actions.js +136 -0
  139. package/dist/esm/daemon/domains/run-registry-interop.d.ts +16 -0
  140. package/dist/esm/daemon/domains/run-registry-interop.js +45 -0
  141. package/dist/esm/daemon/domains/sessions-search.d.ts +11 -0
  142. package/dist/esm/daemon/domains/sessions-search.js +29 -0
  143. package/dist/esm/daemon/handlers/blueprints-handlers.d.ts +5 -0
  144. package/dist/esm/daemon/handlers/blueprints-handlers.js +36 -0
  145. package/dist/esm/daemon/handlers/deploy-handlers.d.ts +6 -0
  146. package/dist/esm/daemon/handlers/deploy-handlers.js +5 -0
  147. package/dist/esm/daemon/handlers/fleet-handlers.d.ts +15 -0
  148. package/dist/esm/daemon/handlers/fleet-handlers.js +91 -0
  149. package/dist/esm/daemon/handlers/git-handlers.d.ts +6 -0
  150. package/dist/esm/daemon/handlers/git-handlers.js +27 -0
  151. package/dist/esm/daemon/handlers/product.d.ts +7 -0
  152. package/dist/esm/daemon/handlers/product.js +10 -119
  153. package/dist/esm/daemon/handlers/sessions-handlers.d.ts +12 -0
  154. package/dist/esm/daemon/handlers/sessions-handlers.js +31 -0
  155. package/dist/esm/daemon/handlers/shared.d.ts +5 -0
  156. package/dist/esm/daemon/handlers/shared.js +16 -0
  157. package/dist/esm/daemon/methods.d.ts +8 -1
  158. package/dist/esm/daemon/methods.js +7 -0
  159. package/dist/esm/daemon/protocol/contract.d.ts +43 -0
  160. package/dist/esm/daemon/protocol/contract.js +26 -0
  161. package/dist/esm/daemon/protocol/index.d.ts +1 -1
  162. package/dist/esm/daemon/protocol/index.js +1 -1
  163. package/dist/esm/hooks/__conformance__/matrix.d.ts +6 -0
  164. package/dist/esm/hooks/__conformance__/matrix.js +137 -0
  165. package/dist/esm/hooks/conformance/batch.js +5 -0
  166. package/dist/esm/hooks/doctor.d.ts +3 -3
  167. package/dist/esm/hooks/doctor.js +30 -15
  168. package/dist/esm/hooks/permission-request/index.js +9 -3
  169. package/dist/esm/hooks/permission-request/policy.d.ts +9 -0
  170. package/dist/esm/hooks/permission-request/policy.js +13 -0
  171. package/dist/esm/hooks/pretool-guard/dev-routing.d.ts +9 -0
  172. package/dist/esm/hooks/pretool-guard/dev-routing.js +41 -5
  173. package/dist/esm/hooks/pretool-guard/logger.d.ts +7 -0
  174. package/dist/esm/hooks/pretool-guard/logger.js +16 -0
  175. package/dist/esm/hooks/pretool-guard/runner.d.ts +6 -0
  176. package/dist/esm/hooks/pretool-guard/runner.js +68 -16
  177. package/dist/esm/hooks/pretool-guard/validators/blueprint.js +30 -10
  178. package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +54 -0
  179. package/dist/esm/hooks/pretool-guard/validators/index.d.ts +7 -0
  180. package/dist/esm/hooks/pretool-guard/validators/index.js +21 -0
  181. package/dist/esm/hooks/pretool-guard/validators/path-contract.d.ts +19 -0
  182. package/dist/esm/hooks/pretool-guard/validators/path-contract.js +25 -8
  183. package/dist/esm/hooks/shared/guard-outcome.d.ts +149 -0
  184. package/dist/esm/hooks/shared/guard-outcome.js +215 -0
  185. package/dist/esm/hooks/shared/hook-bootstrap.d.ts +26 -0
  186. package/dist/esm/hooks/shared/hook-bootstrap.js +64 -1
  187. package/dist/esm/hooks/shared/types.d.ts +8 -5
  188. package/dist/esm/hooks/shared/types.js +23 -8
  189. package/dist/esm/hooks/stop/qa-changed-files.d.ts +11 -0
  190. package/dist/esm/hooks/stop/qa-changed-files.js +74 -0
  191. package/dist/esm/mcp/blueprint/_shared/lifecycle.js +14 -1
  192. package/dist/esm/mcp/blueprint/handlers/document-mutations.js +78 -4
  193. package/dist/esm/mcp/blueprint/handlers/review-log.js +8 -1
  194. package/dist/esm/mcp/blueprint/handlers/task-advance.js +113 -54
  195. package/dist/esm/mcp/blueprint/handlers/task-verify.js +39 -19
  196. package/dist/esm/mcp/blueprint/registration.js +3 -2
  197. package/dist/esm/mcp/server.d.ts +1 -0
  198. package/dist/esm/mcp/server.js +27 -0
  199. package/dist/esm/mcp/tools/_names.d.ts +1 -1
  200. package/dist/esm/mcp/tools/_names.js +5 -0
  201. package/dist/esm/mcp/tools/_registry.js +10 -0
  202. package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
  203. package/dist/esm/mcp/tools/_shared/result.js +44 -1
  204. package/dist/esm/mcp/tools/audit.d.ts +4 -1
  205. package/dist/esm/mcp/tools/audit.js +30 -0
  206. package/dist/esm/mcp/tools/audits.d.ts +3 -0
  207. package/dist/esm/mcp/tools/ci-preflight.d.ts +50 -0
  208. package/dist/esm/mcp/tools/ci-preflight.js +183 -0
  209. package/dist/esm/mcp/tools/pr-upsert.js +39 -1
  210. package/dist/esm/mcp/tools/review-run.d.ts +28 -0
  211. package/dist/esm/mcp/tools/review-run.js +216 -0
  212. package/dist/esm/mcp/tools/run-wait.d.ts +86 -0
  213. package/dist/esm/mcp/tools/run-wait.js +593 -0
  214. package/dist/esm/mcp/tools/session-id.d.ts +3 -0
  215. package/dist/esm/mcp/tools/session-id.js +45 -0
  216. package/dist/esm/mcp/tools/session-info.d.ts +3 -0
  217. package/dist/esm/mcp/tools/session-info.js +49 -0
  218. package/dist/esm/output-transforms/audit.d.ts +23 -0
  219. package/dist/esm/output-transforms/audit.js +47 -0
  220. package/dist/esm/output-transforms/index.js +2 -0
  221. package/dist/esm/review/authority.js +13 -5
  222. package/dist/esm/review/availability.d.ts +22 -0
  223. package/dist/esm/review/availability.js +107 -20
  224. package/dist/esm/review/delivery-verifier.d.ts +27 -0
  225. package/dist/esm/review/delivery-verifier.js +115 -0
  226. package/dist/esm/review/events.d.ts +3 -2
  227. package/dist/esm/review/events.js +23 -3
  228. package/dist/esm/review/execution/adapters.js +26 -9
  229. package/dist/esm/review/execution/output-policy.d.ts +2 -0
  230. package/dist/esm/review/execution/output-policy.js +12 -0
  231. package/dist/esm/review/execution/review-checkout.js +87 -6
  232. package/dist/esm/review/execution/supervisor.js +5 -0
  233. package/dist/esm/review/execution/types.d.ts +8 -0
  234. package/dist/esm/review/opencode-account.d.ts +24 -0
  235. package/dist/esm/review/opencode-account.js +68 -0
  236. package/dist/esm/review/opencode-auth.d.ts +33 -0
  237. package/dist/esm/review/opencode-auth.js +79 -0
  238. package/dist/esm/review/subject.d.ts +12 -0
  239. package/dist/esm/review/subject.js +74 -22
  240. package/dist/esm/runtime/opencode-account-materializer.d.ts +34 -0
  241. package/dist/esm/runtime/opencode-account-materializer.js +33 -0
  242. package/dist/esm/status/snapshot.js +30 -19
  243. package/dist/esm/test/duration-sequencer.d.ts +20 -0
  244. package/dist/esm/test/duration-sequencer.js +54 -0
  245. package/dist/esm/test/shard-durations.json +922 -0
  246. package/dist/esm/test/worker-budget.d.ts +6 -0
  247. package/dist/esm/test/worker-budget.js +59 -1
  248. package/dist/esm/test-helpers/global-setup.d.ts +1 -1
  249. package/dist/esm/test-helpers/global-setup.js +68 -24
  250. package/dist/esm/ultragoal/service.d.ts +7 -0
  251. package/dist/esm/ultragoal/service.js +42 -2
  252. package/dist/esm/utils/write-json-file.js +3 -1
  253. package/dist/esm/worktrees/codex-project-trust.d.ts +15 -0
  254. package/dist/esm/worktrees/codex-project-trust.js +56 -0
  255. package/dist/esm/worktrees/registry.d.ts +8 -0
  256. package/dist/esm/worktrees/registry.js +18 -3
  257. package/package.json +16 -13
@@ -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)
@@ -1,3 +1,4 @@
1
+ import { type GuardOutcome } from "#hooks/shared/guard-outcome";
1
2
  export type LogStatus = "PASS" | "BLOCK" | "WARN" | "ERROR";
2
3
  export type ToolType = string;
3
4
  export interface LogEntry {
@@ -6,6 +7,10 @@ export interface LogEntry {
6
7
  tool: ToolType;
7
8
  failures?: string[];
8
9
  error?: string;
10
+ /** GUARD_* reason code for the decision (additive; present on BLOCK/WARN). */
11
+ code?: string;
12
+ /** Two-axis guard outcome for the decision (additive). */
13
+ outcome?: GuardOutcome;
9
14
  }
10
15
  export interface LogConfig {
11
16
  logDir: string;
@@ -20,6 +25,8 @@ export interface ParsedLogLine {
20
25
  target: string;
21
26
  failures?: string[];
22
27
  error?: string;
28
+ code?: string;
29
+ outcome?: GuardOutcome;
23
30
  }
24
31
  export declare function createLogConfig(): LogConfig;
25
32
  export declare function parseLogLine(line: string): ParsedLogLine | null;
@@ -1,5 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
+ import { GUARD_EFFECT, GUARD_INTENT } from "#hooks/shared/guard-outcome";
3
4
  import { redactText } from "#mcp/tools/_shared/redact.js";
4
5
  import { getSurfacePath } from "#paths/state-root.js";
5
6
  const DEFAULT_MAX_LINES = 250;
@@ -42,6 +43,8 @@ export function parseLogLine(line) {
42
43
  ? parsed.failures.filter((value) => typeof value === "string")
43
44
  : undefined,
44
45
  error: typeof parsed.error === "string" ? parsed.error : undefined,
46
+ code: typeof parsed.code === "string" ? parsed.code : undefined,
47
+ outcome: isGuardOutcome(parsed.outcome) ? parsed.outcome : undefined,
45
48
  };
46
49
  }
47
50
  export function rotateLines(lines, maxLines) {
@@ -100,3 +103,16 @@ function isLogStatus(value) {
100
103
  function isToolType(value) {
101
104
  return typeof value === "string" && value.length > 0;
102
105
  }
106
+ const GUARD_EFFECT_VALUES = new Set(Object.values(GUARD_EFFECT));
107
+ const GUARD_INTENT_VALUES = new Set(Object.values(GUARD_INTENT));
108
+ function isGuardOutcome(value) {
109
+ if (typeof value !== "object" || value === null)
110
+ return false;
111
+ const record = value;
112
+ const effect = record["effect"];
113
+ const intent = record["intent"];
114
+ return (typeof effect === "string" &&
115
+ GUARD_EFFECT_VALUES.has(effect) &&
116
+ typeof intent === "string" &&
117
+ GUARD_INTENT_VALUES.has(intent));
118
+ }
@@ -10,6 +10,12 @@ export interface ProcessValidationOptions {
10
10
  readonly optimizationRules?: readonly OptimizationRule[];
11
11
  readonly recordOptimization?: (record: OptimizationTelemetryRecord) => void;
12
12
  }
13
+ /**
14
+ * Registered GUARD_* codes the runner emits directly (beyond the dev-routing
15
+ * inventory and the per-validator invariant codes in GUARD_VALIDATOR_CODES).
16
+ * Consumed by the guard-outcome registry-completeness test.
17
+ */
18
+ export declare const RUNNER_GUARD_CODES: readonly ["GUARD_PROMPT_INTENT"];
13
19
  export declare function runAllValidators(input: ToolInput): AggregateResult;
14
20
  export declare function formatOutput(aggregate: AggregateResult, input: ToolInput): void;
15
21
  export declare function getToolType(input: ToolInput): string;
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { getActivePromptRouting, isGuardEnabled } from "#hooks/guard-switch/state";
3
+ import { formatGuardReason, GUARD_EFFECT, GUARD_INTENT, GUARD_VALIDATOR_CODES, outcomeForCode, validatorUnexpectedCode, } from "#hooks/shared/guard-outcome";
3
4
  import { routePromptIntent } from "#hooks/prompt-routing";
4
- import { readStdinJson, suppressStderr, writeHookOutput } from "#hooks/shared/hook-bootstrap";
5
+ import { readStdinJson, suppressDecorativeStderr, writeGuardStderr, writeHookOutput, } from "#hooks/shared/hook-bootstrap";
5
6
  import { isSyntheticProcessExit } from "#hooks/shared/synthetic-process-exit.js";
6
7
  import { buildDenyEnvelope, buildUpdatedInputEnvelope, cloneToolInputWithCommand, getCommand, getFilePath, isBashInput, parseToolInput, } from "#hooks/shared/types";
7
8
  import { logRun } from "./logger.js";
@@ -15,6 +16,22 @@ const YELLOW = "\x1b[33m";
15
16
  const DIM = "\x1b[2m";
16
17
  const BOLD = "\x1b[1m";
17
18
  const NC = "\x1b[0m";
19
+ /** Code the pretool-guard emits when a routed prompt-intent forces a redirect. */
20
+ const PROMPT_INTENT_CODE = "GUARD_PROMPT_INTENT";
21
+ /**
22
+ * Registered GUARD_* codes the runner emits directly (beyond the dev-routing
23
+ * inventory and the per-validator invariant codes in GUARD_VALIDATOR_CODES).
24
+ * Consumed by the guard-outcome registry-completeness test.
25
+ */
26
+ export const RUNNER_GUARD_CODES = [
27
+ PROMPT_INTENT_CODE,
28
+ ];
29
+ /** Resolve the GUARD_* code for a failing validator, with a per-validator fallback. */
30
+ function codeForValidator(validatorName) {
31
+ if (!validatorName)
32
+ return "GUARD_VALIDATION_UNEXPECTED";
33
+ return GUARD_VALIDATOR_CODES[validatorName] ?? validatorUnexpectedCode(validatorName);
34
+ }
18
35
  export function runAllValidators(input) {
19
36
  const results = VALIDATORS.map((v) => v(input));
20
37
  const failed = results.filter((r) => !r.passed);
@@ -62,20 +79,35 @@ export function getTarget(input) {
62
79
  export function logValidationResult(result, target, tool) {
63
80
  if (!result.passed) {
64
81
  const failed = result.results.filter((r) => !r.passed);
82
+ const code = codeForValidator(failed[0]?.validator);
65
83
  logRun({
66
84
  status: "BLOCK",
67
85
  target: target.slice(0, 100),
68
86
  tool,
69
87
  failures: failed.map((f) => f.validator),
88
+ code,
89
+ outcome: outcomeForCode(code),
70
90
  });
71
91
  return;
72
92
  }
73
93
  const warnings = result.results.filter((r) => r.passed && r.message);
94
+ if (warnings.length > 0) {
95
+ // A warn does not reach the wire; it is advisory guidance in logs only.
96
+ const code = codeForValidator(warnings[0]?.validator);
97
+ logRun({
98
+ status: "WARN",
99
+ target: target.slice(0, 100),
100
+ tool,
101
+ failures: warnings.map((w) => w.validator),
102
+ code,
103
+ outcome: { effect: GUARD_EFFECT.allow, intent: GUARD_INTENT.advise },
104
+ });
105
+ return;
106
+ }
74
107
  logRun({
75
- status: warnings.length > 0 ? "WARN" : "PASS",
108
+ status: "PASS",
76
109
  target: target.slice(0, 100),
77
110
  tool,
78
- failures: warnings.length > 0 ? warnings.map((w) => w.validator) : undefined,
79
111
  });
80
112
  }
81
113
  export function handleParseError(error, inputJson) {
@@ -87,18 +119,20 @@ export function handleParseError(error, inputJson) {
87
119
  });
88
120
  console.error(`${RED}❌ Pretool Guard: Error parsing input${NC}`);
89
121
  console.error(`${RED} ${error instanceof Error ? error.message : "Unknown error"}${NC}`);
122
+ // Fail-closed as a CODED deny envelope so malformed/unhandled input still
123
+ // carries the stable GUARD_PRETOOL_FAILURE code on the wire, not a bare exit 2.
124
+ const detail = error instanceof Error ? error.message : "unknown error";
125
+ writeDenyDecision(formatGuardReason("GUARD_PRETOOL_FAILURE", `pretool-guard could not evaluate tool input: ${detail}`));
90
126
  process.exit(2);
91
127
  }
92
128
  function writeDenyDecision(permissionDecisionReason) {
129
+ // Deliver the coded reason on BOTH host channels: the stdout deny envelope
130
+ // (Codex reads hookSpecificOutput; Grok reads the dual top-level decision) AND
131
+ // the REAL stderr via writeGuardStderr (Claude reads stderr as the blocking
132
+ // feedback on an exit-2 PreToolUse deny — it ignores stdout there). The
133
+ // decorative diagnostics that go through console.error stay suppressed.
93
134
  writeHookOutput(JSON.stringify(buildDenyEnvelope({ reason: permissionDecisionReason })));
94
- // Claude/Codex treat exit 2 as deny and read the reason from stderr when present.
95
- // suppressStderr may null out fd 2; dual JSON still carries the reason for Grok.
96
- try {
97
- process.stderr.write(`${permissionDecisionReason}\n`);
98
- }
99
- catch {
100
- // ignore
101
- }
135
+ writeGuardStderr(`${permissionDecisionReason}\n`);
102
136
  }
103
137
  function writeUpdatedInput(input, command) {
104
138
  writeHookOutput(JSON.stringify(buildUpdatedInputEnvelope(input, command)));
@@ -125,12 +159,16 @@ function evaluateRoutingPolicy(input) {
125
159
  return {
126
160
  snapshot: { promptIntentReason, toolInputAction, commandActions: [] },
127
161
  denyReason: promptIntentReason,
162
+ code: PROMPT_INTENT_CODE,
163
+ outcome: outcomeForCode(PROMPT_INTENT_CODE),
128
164
  };
129
165
  }
130
166
  if (toolInputAction?.action === "deny" || toolInputAction?.action === "sandbox") {
131
167
  return {
132
168
  snapshot: { promptIntentReason, toolInputAction, commandActions },
133
169
  denyReason: toolInputAction.guidance,
170
+ code: toolInputAction.code,
171
+ outcome: outcomeForCode(toolInputAction.code),
134
172
  };
135
173
  }
136
174
  for (const commandAction of commandActions) {
@@ -138,18 +176,24 @@ function evaluateRoutingPolicy(input) {
138
176
  return {
139
177
  snapshot: { promptIntentReason, toolInputAction, commandActions },
140
178
  denyReason: commandAction.action.guidance,
179
+ code: commandAction.action.code,
180
+ outcome: outcomeForCode(commandAction.action.code),
141
181
  };
142
182
  }
143
183
  if (commandAction.action?.action === "sandbox" && !commandAction.alreadySandboxed) {
144
184
  return {
145
185
  snapshot: { promptIntentReason, toolInputAction, commandActions },
146
186
  denyReason: commandAction.action.guidance,
187
+ code: commandAction.action.code,
188
+ outcome: outcomeForCode(commandAction.action.code),
147
189
  };
148
190
  }
149
191
  }
150
192
  return {
151
193
  snapshot: { promptIntentReason, toolInputAction, commandActions },
152
194
  denyReason: null,
195
+ code: null,
196
+ outcome: null,
153
197
  };
154
198
  }
155
199
  function policySnapshotsMatch(originalRouting, replacementRouting, originalValidation, replacementValidation) {
@@ -182,8 +226,11 @@ export function processValidation(inputJson, options = {}) {
182
226
  const input = parseToolInput(inputJson);
183
227
  const command = isBashInput(input) ? getCommand(input) : null;
184
228
  const originalRouting = evaluateRoutingPolicy(input);
185
- if (originalRouting.denyReason !== null) {
186
- writeDenyDecision(originalRouting.denyReason);
229
+ if (originalRouting.denyReason !== null && originalRouting.code !== null) {
230
+ // formatGuardReason owns the 80-char budget on BOTH deny paths (this routing
231
+ // path was previously untruncated) and rides the GUARD_* code inside the
232
+ // host-displayed reason so consumers key on the code, not on wording.
233
+ writeDenyDecision(formatGuardReason(originalRouting.code, originalRouting.denyReason));
187
234
  // Exit 2 so Grok (and other exit-code runners) treat this as an explicit
188
235
  // deny; dual JSON still carries Claude/Codex hookSpecificOutput.
189
236
  process.exit(2);
@@ -201,8 +248,11 @@ export function processValidation(inputJson, options = {}) {
201
248
  .map((r) => r.message)
202
249
  .find((message) => typeof message === "string" && message.length > 0);
203
250
  const validatorReason = failed.map((r) => r.validator).join(", ");
204
- const reason = (messageReason ?? (validatorReason || "pretool-guard validation failed")).slice(0, 80);
205
- writeDenyDecision(reason);
251
+ const rawReason = messageReason ?? (validatorReason || "pretool-guard validation failed");
252
+ // Code derives from the first failing validator's invariant, with a
253
+ // per-validator GUARD_<NAME>_UNEXPECTED fallback for unclassified failures.
254
+ const code = codeForValidator(failed[0]?.validator);
255
+ writeDenyDecision(formatGuardReason(code, rawReason));
206
256
  process.exit(2);
207
257
  }
208
258
  if (command) {
@@ -235,7 +285,9 @@ export function processValidation(inputJson, options = {}) {
235
285
  process.exit(result.exitCode);
236
286
  }
237
287
  export async function main() {
238
- suppressStderr();
288
+ // Suppress decorative stderr but keep fd 2 open so a deny can deliver its
289
+ // coded reason to the real stderr (Claude's exit-2 blocking-feedback channel).
290
+ suppressDecorativeStderr();
239
291
  const inputJson = await readStdinJson();
240
292
  if (!inputJson.trim()) {
241
293
  writeHookOutput("{}\n");
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { getFilePath, isFileEditInput, isFileWriteInput } from "#hooks/shared/types";
3
3
  import { validateBlueprint as validateBlueprintShared } from "#hooks/shared/validators/blueprint";
4
- import { isCanonicalBlueprintDocumentPath, isCanonicalBlueprintReviewLedgerPath, } from "./path-contract.js";
4
+ import { getCanonicalBlueprintDocument, getCanonicalBlueprintReviewLedgerPath, } from "./path-contract.js";
5
5
  import { buildRedirectMessage } from "./mcp-redirect.js";
6
6
  function normalizeFilePath(filePath, cwd) {
7
7
  if (!path.isAbsolute(filePath))
@@ -10,6 +10,23 @@ function normalizeFilePath(filePath, cwd) {
10
10
  return filePath.replace(/^\/+/, "");
11
11
  return path.relative(cwd, filePath).replace(/\\/g, "/");
12
12
  }
13
+ /**
14
+ * Build a short, deterministic `<state>/<slug>` display identity from the
15
+ * PARSED match, never from the raw input path. The raw path is a
16
+ * cwd-relative string that can legitimately climb several parent-directory
17
+ * segments out of the effective repo root (e.g. into
18
+ * `.webpresso/worktrees/repos/<hash>/.../_overview.md` when cwd is the
19
+ * primary checkout but the edited file lives inside a managed blueprint
20
+ * worktree) — confusing to read and, worse, long enough to consume
21
+ * the guard's entire wire-message budget (`GUARD_REASON_BUDGET`, 80 Unicode
22
+ * code points total, see `#hooks/shared/guard-outcome.ts`) before any
23
+ * actionable "use this MCP tool" guidance is shown. Dropping the root prefix
24
+ * and document filename (both implied by the "blueprint" category, not
25
+ * distinguishing information) buys back budget for that guidance.
26
+ */
27
+ function canonicalDisplayId(state, slug) {
28
+ return `${state}/${slug}`;
29
+ }
13
30
  export function validateBlueprint(input) {
14
31
  const filePath = getFilePath(input);
15
32
  if (filePath && (isFileWriteInput(input) || isFileEditInput(input))) {
@@ -17,28 +34,31 @@ export function validateBlueprint(input) {
17
34
  // Detect a canonical blueprint document from EITHER the cwd-normalized form
18
35
  // OR the raw path. Detection is prefix-agnostic (path-contract), so an
19
36
  // absolute worktree path is caught even when input.cwd is undefined or does
20
- // not reduce the path to a recognized blueprints root.
21
- if (isCanonicalBlueprintDocumentPath(normalized) ||
22
- isCanonicalBlueprintDocumentPath(filePath)) {
37
+ // not reduce the path to a recognized blueprints root — including when cwd
38
+ // points at an unrelated checkout (e.g. the primary checkout) and the file
39
+ // lives inside a managed worktree.
40
+ const canonicalDoc = getCanonicalBlueprintDocument(normalized) ?? getCanonicalBlueprintDocument(filePath);
41
+ if (canonicalDoc) {
23
42
  return {
24
43
  validator: "blueprint",
25
44
  passed: false,
26
45
  message: buildRedirectMessage({
27
46
  category: "blueprint",
28
- command: normalized,
29
- fallbackHint: "wp_blueprint_create / wp_blueprint_put / wp_blueprint_transition / wp_blueprint_task_verify",
47
+ command: canonicalDisplayId(canonicalDoc.state, canonicalDoc.slug),
48
+ fallbackHint: "wp_blueprint_put",
30
49
  }),
31
50
  };
32
51
  }
33
- if (isCanonicalBlueprintReviewLedgerPath(normalized) ||
34
- isCanonicalBlueprintReviewLedgerPath(filePath)) {
52
+ const ledgerDoc = getCanonicalBlueprintReviewLedgerPath(normalized) ??
53
+ getCanonicalBlueprintReviewLedgerPath(filePath);
54
+ if (ledgerDoc) {
35
55
  return {
36
56
  validator: "blueprint",
37
57
  passed: false,
38
58
  message: buildRedirectMessage({
39
59
  category: "blueprint",
40
- command: normalized,
41
- fallbackHint: "wp_blueprint_review_log (or wp review log --reviewer <id>)",
60
+ command: canonicalDisplayId(ledgerDoc.state, ledgerDoc.slug),
61
+ fallbackHint: "wp_blueprint_review_log",
42
62
  }),
43
63
  };
44
64
  }
@@ -348,6 +348,7 @@ export function createAuditResult(command, rule, options = {}) {
348
348
  }
349
349
  const AUDIT_KIND_SET = new Set(MCP_AUDIT_KINDS);
350
350
  const WP_AUDIT_RE = /^wp\s+audit\s+([a-z0-9-]+)\b/u;
351
+ const WP_CI_PREFLIGHT_RE = /^wp\s+ci-preflight\b/u;
351
352
  const SCRIPT_INVOCATION_RE = /^(?:pnpm run|vp run|npm run|pnpm|npm)\s+([A-Za-z0-9:_-]+)/u;
352
353
  const RAW_PM_RE = /^(?:pnpm|npm)\b/u;
353
354
  function loadGuardConfig(projectDir) {
@@ -389,6 +390,53 @@ function findWpAuditRedirect(command) {
389
390
  }
390
391
  return undefined;
391
392
  }
393
+ /** `wp ci-preflight` (CLI) → `wp_ci_preflight` MCP tool. 1:1 mapping, not gated on config. */
394
+ function findWpCiPreflightRedirect(command) {
395
+ for (const variant of getCommandVariants(command)) {
396
+ if (WP_CI_PREFLIGHT_RE.test(variant)) {
397
+ return `"${variant}" denied — use the webpresso MCP tool wp_ci_preflight(). Runs the same fail-fast, branch-scoped pre-push gate (format → guardrails → typecheck → lint → test) without shelling out. Returns structured, summary-first results.`;
398
+ }
399
+ }
400
+ return undefined;
401
+ }
402
+ const OPENCODE_RUN_RE = /^opencode\s+run\b/u;
403
+ const CODEX_EXEC_OR_REVIEW_RE = /^codex\s+(?:exec|review)\b/u;
404
+ /**
405
+ * Raw provider-CLI outside-voice review invocations (`opencode run ...`,
406
+ * `codex exec ...`, `codex review`) → `wp_review_run`. Generic; not gated on
407
+ * config, mirroring `findWpAuditRedirect`.
408
+ *
409
+ * Deliberately narrow: only the review-launching subcommands are blocked.
410
+ * `codex login status`, `codex --version`, `opencode models opencode-go`
411
+ * (listing available models — documented in the `codex` skill), and
412
+ * `opencode --version` are legitimate, non-review invocations and must stay
413
+ * unblocked.
414
+ *
415
+ * A single tool exists here (not a category with several natural
416
+ * `wp_<suffix>` matchers), so this deliberately does NOT go through the
417
+ * generic `CommandRule`/`buildRedirectMessage` category pipeline — that
418
+ * machinery always derives its "use the webpresso MCP tool" claim from a
419
+ * category → tool-suffix map, which is correct for lint/test/typecheck
420
+ * (real per-category MCP tools) but would misname the single review tool.
421
+ * Mirrors `findWpAuditRedirect`'s unconditional standalone-message shape:
422
+ * always names `wp_review_run` directly rather than gating on
423
+ * `resolveMcpReady` (the fallback tier that message implies — the `wp
424
+ * review run` CLI documented in the `codex` skill — is unconditionally
425
+ * still available as a manual fallback when MCP genuinely is not ready).
426
+ */
427
+ function findReviewCliRedirect(command) {
428
+ for (const variant of getCommandVariants(command)) {
429
+ if (!OPENCODE_RUN_RE.test(variant) && !CODEX_EXEC_OR_REVIEW_RE.test(variant))
430
+ continue;
431
+ return [
432
+ `"${variant}" denied — use the webpresso MCP tool: wp_review_run(...).`,
433
+ "It runs the probe + review stages of an outside-voice provider review in one call and returns structured, summary-first results with artifact capture under .webpresso/reviews.",
434
+ "Provider fallback (e.g. Codex out of API credits) is a `provider` PARAMETER change on that SAME tool call (claude|codex|opencode|grok) — never shell into a different provider's raw CLI.",
435
+ "Fallback if MCP is genuinely unavailable: the `wp review run` CLI (see the `codex` skill) — never a different provider's raw CLI.",
436
+ ].join("\n");
437
+ }
438
+ return undefined;
439
+ }
392
440
  /** Repo-declared `guard.scriptRoutes`: a package script mapped to an audit kind. */
393
441
  function findScriptRouteRedirect(command, routes) {
394
442
  for (const variant of getCommandVariants(command)) {
@@ -446,6 +494,12 @@ export function validateForbiddenCommands(input) {
446
494
  const wpAuditRedirect = findWpAuditRedirect(command);
447
495
  if (wpAuditRedirect)
448
496
  return guardRedirect(wpAuditRedirect);
497
+ const wpCiPreflightRedirect = findWpCiPreflightRedirect(command);
498
+ if (wpCiPreflightRedirect)
499
+ return guardRedirect(wpCiPreflightRedirect);
500
+ const reviewCliRedirect = findReviewCliRedirect(command);
501
+ if (reviewCliRedirect)
502
+ return guardRedirect(reviewCliRedirect);
449
503
  const guard = loadGuardConfig(projectDir);
450
504
  if (guard?.scriptRoutes) {
451
505
  const redirect = findScriptRouteRedirect(command, guard.scriptRoutes);
@@ -12,4 +12,11 @@ import { validateTestQuality } from "./test-quality.js";
12
12
  import { validateUxQuality } from "./ux-quality.js";
13
13
  import { validateWorktreeDiscipline } from "./worktree-discipline.js";
14
14
  export declare const VALIDATORS: (typeof validateBlueprintFirst | typeof validateDocsGovernance | typeof validatePackageImports)[];
15
+ /**
16
+ * Canonical `validator` name each entry in {@link VALIDATORS} reports on its
17
+ * `ValidationResult`. Kept in lockstep with the array above; the guard-outcome
18
+ * registry-completeness test asserts every name has a GUARD_* invariant code
19
+ * (and vice versa).
20
+ */
21
+ export declare const VALIDATOR_NAMES: readonly ["forbidden-commands", "dangerous-commands", "blueprint", "blueprint-first", "docs-governance", "plan-frontmatter", "complexity", "package-imports", "file-conventions", "command-file", "test-quality", "ux-quality", "worktree-discipline"];
15
22
  export { validateBlueprint, validateBlueprintFirst, validateCommandFile, validateComplexity, validateDangerousCommands, validateDocsGovernance, validateFileConventions, validateForbiddenCommands, validatePackageImports, validatePlanFrontmatter, validateTestQuality, validateUxQuality, validateWorktreeDiscipline, };
@@ -26,4 +26,25 @@ export const VALIDATORS = [
26
26
  validateUxQuality,
27
27
  validateWorktreeDiscipline,
28
28
  ];
29
+ /**
30
+ * Canonical `validator` name each entry in {@link VALIDATORS} reports on its
31
+ * `ValidationResult`. Kept in lockstep with the array above; the guard-outcome
32
+ * registry-completeness test asserts every name has a GUARD_* invariant code
33
+ * (and vice versa).
34
+ */
35
+ export const VALIDATOR_NAMES = [
36
+ "forbidden-commands",
37
+ "dangerous-commands",
38
+ "blueprint",
39
+ "blueprint-first",
40
+ "docs-governance",
41
+ "plan-frontmatter",
42
+ "complexity",
43
+ "package-imports",
44
+ "file-conventions",
45
+ "command-file",
46
+ "test-quality",
47
+ "ux-quality",
48
+ "worktree-discipline",
49
+ ];
29
50
  export { validateBlueprint, validateBlueprintFirst, validateCommandFile, validateComplexity, validateDangerousCommands, validateDocsGovernance, validateFileConventions, validateForbiddenCommands, validatePackageImports, validatePlanFrontmatter, validateTestQuality, validateUxQuality, validateWorktreeDiscipline, };
@@ -1,3 +1,4 @@
1
+ import type { BlueprintDocumentPath, BlueprintStatus } from "#utils/document-paths.js";
1
2
  export declare const BLUEPRINTS_ROOT = "webpresso/blueprints";
2
3
  export declare const TECH_DEBT_ROOT = "webpresso/tech-debt";
3
4
  /**
@@ -12,5 +13,23 @@ export declare function getNonCanonicalPlanningPathViolation(filePath: string, b
12
13
  */
13
14
  export declare function isCanonicalBlueprintOverviewPath(filePath: string, blueprintsRoot?: string): boolean;
14
15
  export declare function isCanonicalBlueprintDocumentPath(filePath: string, blueprintsRoot?: string): boolean;
16
+ /**
17
+ * Same detection as {@link isCanonicalBlueprintDocumentPath}, but returns the
18
+ * parsed `<state>/<slug>/<doc>` identity instead of a boolean. Callers that
19
+ * need to DISPLAY the match (e.g. a guard denial message) should build their
20
+ * text from this clean, root-relative identity rather than from the raw
21
+ * input path — a raw path can be an arbitrarily long, cwd-relative string
22
+ * (e.g. several parent-directory segments climbing out of an unrelated
23
+ * worktree) that is both confusing and liable to consume an entire
24
+ * wire-message budget before any actionable guidance is shown.
25
+ */
26
+ export declare function getCanonicalBlueprintDocument(filePath: string, blueprintsRoot?: string): BlueprintDocumentPath | null;
27
+ export interface ReviewLedgerDocumentPath {
28
+ state: BlueprintStatus;
29
+ slug: string;
30
+ doc: "reviews.md" | "review-events.jsonl";
31
+ }
32
+ /** Same detection as {@link isCanonicalBlueprintReviewLedgerPath}, returning the parsed identity. */
33
+ export declare function getCanonicalBlueprintReviewLedgerPath(filePath: string, blueprintsRoot?: string): ReviewLedgerDocumentPath | null;
15
34
  export declare function isCanonicalBlueprintReviewLedgerPath(filePath: string, blueprintsRoot?: string): boolean;
16
35
  export declare function getBlueprintPathViolation(filePath: string, blueprintsRoot?: string, cwd?: string): string | null;