@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
@@ -107,7 +107,17 @@ function findCanonicalBlueprintDocument(normalized, roots) {
107
107
  }
108
108
  return null;
109
109
  }
110
- function getCanonicalBlueprintDocument(filePath, blueprintsRoot) {
110
+ /**
111
+ * Same detection as {@link isCanonicalBlueprintDocumentPath}, but returns the
112
+ * parsed `<state>/<slug>/<doc>` identity instead of a boolean. Callers that
113
+ * need to DISPLAY the match (e.g. a guard denial message) should build their
114
+ * text from this clean, root-relative identity rather than from the raw
115
+ * input path — a raw path can be an arbitrarily long, cwd-relative string
116
+ * (e.g. several parent-directory segments climbing out of an unrelated
117
+ * worktree) that is both confusing and liable to consume an entire
118
+ * wire-message budget before any actionable guidance is shown.
119
+ */
120
+ export function getCanonicalBlueprintDocument(filePath, blueprintsRoot) {
111
121
  const normalized = normalizePlanningPath(filePath);
112
122
  const roots = blueprintsRoot ? [blueprintsRoot] : CANONICAL_BLUEPRINTS_ROOTS;
113
123
  return findCanonicalBlueprintDocument(normalized, roots);
@@ -115,12 +125,14 @@ function getCanonicalBlueprintDocument(filePath, blueprintsRoot) {
115
125
  function parseReviewLedgerRelativePath(tail) {
116
126
  const parts = tail.split("/").filter((segment) => segment.length > 0);
117
127
  if (parts.length < 3)
118
- return false;
128
+ return null;
119
129
  const [state, slug, ...rest] = parts;
120
130
  if (!isBlueprintStatus(state) || !slug)
121
- return false;
131
+ return null;
122
132
  const doc = rest.join("/");
123
- return doc === "reviews.md" || doc === "review-events.jsonl";
133
+ if (doc !== "reviews.md" && doc !== "review-events.jsonl")
134
+ return null;
135
+ return { state, slug, doc };
124
136
  }
125
137
  /**
126
138
  * Prefix-agnostic detection of a blueprint's review-ledger AUTHORITY files:
@@ -140,17 +152,22 @@ function findCanonicalBlueprintReviewLedgerPath(normalized, roots) {
140
152
  if (!matchesHere)
141
153
  continue;
142
154
  const tail = segments.slice(start + rootSegments.length).join("/");
143
- if (parseReviewLedgerRelativePath(tail))
144
- return true;
155
+ const parsed = parseReviewLedgerRelativePath(tail);
156
+ if (parsed)
157
+ return parsed;
145
158
  }
146
159
  }
147
- return false;
160
+ return null;
148
161
  }
149
- export function isCanonicalBlueprintReviewLedgerPath(filePath, blueprintsRoot) {
162
+ /** Same detection as {@link isCanonicalBlueprintReviewLedgerPath}, returning the parsed identity. */
163
+ export function getCanonicalBlueprintReviewLedgerPath(filePath, blueprintsRoot) {
150
164
  const normalized = normalizePlanningPath(filePath);
151
165
  const roots = blueprintsRoot ? [blueprintsRoot] : CANONICAL_BLUEPRINTS_ROOTS;
152
166
  return findCanonicalBlueprintReviewLedgerPath(normalized, roots);
153
167
  }
168
+ export function isCanonicalBlueprintReviewLedgerPath(filePath, blueprintsRoot) {
169
+ return getCanonicalBlueprintReviewLedgerPath(filePath, blueprintsRoot) !== null;
170
+ }
154
171
  export function getBlueprintPathViolation(filePath, blueprintsRoot, cwd = process.cwd()) {
155
172
  const normalized = normalizePlanningPath(filePath);
156
173
  if (!isBlueprintPath(normalized, blueprintsRoot))
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Typed guard-outcome vocabulary for the pretool-guard and permission-request
3
+ * hook surfaces.
4
+ *
5
+ * Two orthogonal axes describe every guard decision:
6
+ *
7
+ * - `GUARD_EFFECT` — what actually reaches the host on the wire.
8
+ * - `GUARD_INTENT` — why the guard produced that effect.
9
+ *
10
+ * They are deliberately separate because the earlier flat Proceed/Deny/Guide
11
+ * draft was not mutually exclusive ("Guide" still emits a deny; a host
12
+ * pass-through is not "Proceed"). The two-axis model is exhaustive and honest
13
+ * about what the host receives versus what the policy meant.
14
+ *
15
+ * Every guard denial carries a stable `GUARD_<CODE>` that survives the
16
+ * host-displayed wire reason (see {@link formatGuardReason}). Consumers and
17
+ * audits key on the code, not on fragile English wording, and the code is
18
+ * identical across every Tier-1 host.
19
+ *
20
+ * Precedent for the as-const vocabulary shape: `HOOK_STATUS`
21
+ * (src/hooks/shared/vocabulary.ts).
22
+ */
23
+ export declare const GUARD_EFFECT: {
24
+ /** A deny envelope reaches the host (exit 2 / permissionDecision:"deny"). */
25
+ readonly block: "block";
26
+ /** The host is told to proceed (permission allow), or nothing is denied. */
27
+ readonly allow: "allow";
28
+ /** No wire change; the host's own UI/policy stays authoritative. */
29
+ readonly defer: "defer";
30
+ };
31
+ export type GuardEffect = (typeof GUARD_EFFECT)[keyof typeof GUARD_EFFECT];
32
+ export declare const GUARD_INTENT: {
33
+ /** Block-and-hold: the caller must comply; no automatic reroute. */
34
+ readonly enforce: "enforce";
35
+ /** Actively reroute the caller onto a sanctioned sandbox/MCP lane. */
36
+ readonly redirect: "redirect";
37
+ /** Non-blocking guidance surfaced in logs only. */
38
+ readonly advise: "advise";
39
+ /** Defer to the host default (pass-through); guard expresses no opinion. */
40
+ readonly hostDefault: "hostDefault";
41
+ };
42
+ export type GuardIntent = (typeof GUARD_INTENT)[keyof typeof GUARD_INTENT];
43
+ export type GuardOutcome = {
44
+ readonly effect: GuardEffect;
45
+ readonly intent: GuardIntent;
46
+ };
47
+ /**
48
+ * Closed set of guard reason codes, one per stable policy invariant (a single
49
+ * validator enforcing multiple invariants may map to multiple codes). Each code
50
+ * matches `/^GUARD_[A-Z0-9_]+$/u` and carries its {@link GuardOutcome}.
51
+ *
52
+ * Per-validator `GUARD_<NAME>_UNEXPECTED` fallback codes are NOT listed here;
53
+ * they are a deterministically-derived open family (see
54
+ * {@link validatorUnexpectedCode}) so that adding a validator never leaves a
55
+ * failure unclassified.
56
+ */
57
+ export declare const GUARD_REASON_CODES: {
58
+ readonly GUARD_RAW_GIT_MUTATION: GuardOutcome;
59
+ readonly GUARD_MANUAL_PR_WAIT: GuardOutcome;
60
+ readonly GUARD_PR_DISCLOSURE: GuardOutcome;
61
+ readonly GUARD_WRAPPED_WP: GuardOutcome;
62
+ readonly GUARD_SOURCE_ENTRYPOINT: GuardOutcome;
63
+ readonly GUARD_DEV_TOOL_REDIRECT: GuardOutcome;
64
+ readonly GUARD_DATA_HEAVY_SANDBOX: GuardOutcome;
65
+ readonly GUARD_SESSION_MEMORY_REDIRECT: GuardOutcome;
66
+ readonly GUARD_PROMPT_INTENT: GuardOutcome;
67
+ readonly GUARD_PRETOOL_FAILURE: GuardOutcome;
68
+ readonly GUARD_FORBIDDEN_COMMAND: GuardOutcome;
69
+ readonly GUARD_DANGEROUS_COMMAND: GuardOutcome;
70
+ readonly GUARD_BLUEPRINT_INVALID: GuardOutcome;
71
+ readonly GUARD_BLUEPRINT_FIRST: GuardOutcome;
72
+ readonly GUARD_DOCS_GOVERNANCE: GuardOutcome;
73
+ readonly GUARD_PLAN_FRONTMATTER: GuardOutcome;
74
+ readonly GUARD_COMPLEXITY: GuardOutcome;
75
+ readonly GUARD_PACKAGE_IMPORTS: GuardOutcome;
76
+ readonly GUARD_FILE_CONVENTIONS: GuardOutcome;
77
+ readonly GUARD_COMMAND_FILE: GuardOutcome;
78
+ readonly GUARD_TEST_QUALITY: GuardOutcome;
79
+ readonly GUARD_UX_QUALITY: GuardOutcome;
80
+ readonly GUARD_WORKTREE_DISCIPLINE: GuardOutcome;
81
+ readonly GUARD_PERMISSION_ALLOWED: GuardOutcome;
82
+ readonly GUARD_PERMISSION_DENIED: GuardOutcome;
83
+ readonly GUARD_HOST_DEFAULT: GuardOutcome;
84
+ };
85
+ export type GuardReasonCode = keyof typeof GUARD_REASON_CODES;
86
+ /** Deterministically-derived per-validator fallback code family. */
87
+ export type GuardUnexpectedCode = `GUARD_${string}_UNEXPECTED`;
88
+ /** Any accepted guard reason token: a registered code or an UNEXPECTED fallback. */
89
+ export type GuardReasonToken = GuardReasonCode | GuardUnexpectedCode;
90
+ /**
91
+ * Map a pretool-guard validator name to its canonical invariant code. Runner
92
+ * derivation falls back to {@link validatorUnexpectedCode} when a validator is
93
+ * not listed here, so a newly-added validator still produces a classified,
94
+ * pattern-valid code.
95
+ */
96
+ export declare const GUARD_VALIDATOR_CODES: Readonly<Record<string, GuardReasonCode>>;
97
+ /**
98
+ * Deterministic, length-bounded per-validator fallback code, e.g. `test-quality`
99
+ * -> `GUARD_TEST_QUALITY_UNEXPECTED`. Always {@link GUARD_INTENT.enforce}. The
100
+ * name segment is capped so even a pathologically long validator name yields a
101
+ * token well under the wire budget.
102
+ */
103
+ export declare function validatorUnexpectedCode(validatorName: string): GuardUnexpectedCode;
104
+ /**
105
+ * Narrow to a registered guard reason code. This is SOUND: it returns true
106
+ * only for actual keys of the closed {@link GUARD_REASON_CODES} set. A
107
+ * per-validator `GUARD_<NAME>_UNEXPECTED` fallback is NOT a registered code —
108
+ * use {@link isGuardReasonToken} for the broader accepted family.
109
+ */
110
+ export declare function isGuardReasonCode(value: string): value is GuardReasonCode;
111
+ /**
112
+ * Narrow to any accepted guard reason token: a registered code or a well-formed
113
+ * `GUARD_<NAME>_UNEXPECTED` fallback. A fallback token is only accepted when it
114
+ * fits the wire budget as a prefix (token + ": " <= GUARD_REASON_BUDGET), so an
115
+ * accepted token can never force formatGuardReason to truncate its own prefix —
116
+ * consistent with the length-bounded {@link validatorUnexpectedCode}.
117
+ */
118
+ export declare function isGuardReasonToken(value: string): value is GuardReasonToken;
119
+ /** Resolve the outcome for a token; UNEXPECTED fallbacks are always block+enforce. */
120
+ export declare function outcomeForCode(code: string): GuardOutcome;
121
+ /** Total wire budget (in Unicode code points) for a host-displayed deny reason. */
122
+ export declare const GUARD_REASON_BUDGET = 80;
123
+ /** Truncate to at most `max` Unicode code points (never splits a surrogate pair). */
124
+ export declare function truncateCodePoints(value: string, max: number): string;
125
+ /**
126
+ * Truncate a wire reason to the {@link GUARD_REASON_BUDGET} in Unicode code
127
+ * points (never splitting a surrogate pair). Truncates from the end, so a
128
+ * leading `GUARD_<CODE>: ` prefix always survives. Shared by buildDenyEnvelope
129
+ * so the defensive budget matches formatGuardReason exactly.
130
+ */
131
+ export declare function truncateReasonToBudget(reason: string): string;
132
+ /**
133
+ * Produce the host-displayed wire reason `"<CODE>: <message>"`.
134
+ *
135
+ * The code prefix is reserved FIRST, then the message is sanitized (whitespace
136
+ * collapsed) and truncated by Unicode code points so the total never exceeds
137
+ * {@link GUARD_REASON_BUDGET} and no emoji/surrogate pair is split. The code
138
+ * therefore always survives the host budget — that survival is the entire
139
+ * cross-host stdout integration.
140
+ *
141
+ * `code` is a typed {@link GuardReasonToken}, so an over-budget prefix is not a
142
+ * reachable input (all registered codes and validator fallbacks are far under
143
+ * 80). If one somehow occurs, the prefix is preserved and the message dropped
144
+ * rather than corrupting the code.
145
+ *
146
+ * A message that already begins with the same `CODE: ` prefix is not
147
+ * double-prefixed (idempotent for re-formatted reasons).
148
+ */
149
+ export declare function formatGuardReason(code: GuardReasonToken, message: string): string;
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Typed guard-outcome vocabulary for the pretool-guard and permission-request
3
+ * hook surfaces.
4
+ *
5
+ * Two orthogonal axes describe every guard decision:
6
+ *
7
+ * - `GUARD_EFFECT` — what actually reaches the host on the wire.
8
+ * - `GUARD_INTENT` — why the guard produced that effect.
9
+ *
10
+ * They are deliberately separate because the earlier flat Proceed/Deny/Guide
11
+ * draft was not mutually exclusive ("Guide" still emits a deny; a host
12
+ * pass-through is not "Proceed"). The two-axis model is exhaustive and honest
13
+ * about what the host receives versus what the policy meant.
14
+ *
15
+ * Every guard denial carries a stable `GUARD_<CODE>` that survives the
16
+ * host-displayed wire reason (see {@link formatGuardReason}). Consumers and
17
+ * audits key on the code, not on fragile English wording, and the code is
18
+ * identical across every Tier-1 host.
19
+ *
20
+ * Precedent for the as-const vocabulary shape: `HOOK_STATUS`
21
+ * (src/hooks/shared/vocabulary.ts).
22
+ */
23
+ export const GUARD_EFFECT = {
24
+ /** A deny envelope reaches the host (exit 2 / permissionDecision:"deny"). */
25
+ block: "block",
26
+ /** The host is told to proceed (permission allow), or nothing is denied. */
27
+ allow: "allow",
28
+ /** No wire change; the host's own UI/policy stays authoritative. */
29
+ defer: "defer",
30
+ };
31
+ export const GUARD_INTENT = {
32
+ /** Block-and-hold: the caller must comply; no automatic reroute. */
33
+ enforce: "enforce",
34
+ /** Actively reroute the caller onto a sanctioned sandbox/MCP lane. */
35
+ redirect: "redirect",
36
+ /** Non-blocking guidance surfaced in logs only. */
37
+ advise: "advise",
38
+ /** Defer to the host default (pass-through); guard expresses no opinion. */
39
+ hostDefault: "hostDefault",
40
+ };
41
+ const BLOCK_ENFORCE = { effect: GUARD_EFFECT.block, intent: GUARD_INTENT.enforce };
42
+ const BLOCK_REDIRECT = { effect: GUARD_EFFECT.block, intent: GUARD_INTENT.redirect };
43
+ const ALLOW_ENFORCE = { effect: GUARD_EFFECT.allow, intent: GUARD_INTENT.enforce };
44
+ const DEFER_HOST = { effect: GUARD_EFFECT.defer, intent: GUARD_INTENT.hostDefault };
45
+ /**
46
+ * Closed set of guard reason codes, one per stable policy invariant (a single
47
+ * validator enforcing multiple invariants may map to multiple codes). Each code
48
+ * matches `/^GUARD_[A-Z0-9_]+$/u` and carries its {@link GuardOutcome}.
49
+ *
50
+ * Per-validator `GUARD_<NAME>_UNEXPECTED` fallback codes are NOT listed here;
51
+ * they are a deterministically-derived open family (see
52
+ * {@link validatorUnexpectedCode}) so that adding a validator never leaves a
53
+ * failure unclassified.
54
+ */
55
+ export const GUARD_REASON_CODES = {
56
+ // --- pretool-guard dev-routing (block + redirect: each names a wp_* lane) --
57
+ GUARD_RAW_GIT_MUTATION: BLOCK_REDIRECT,
58
+ GUARD_MANUAL_PR_WAIT: BLOCK_REDIRECT,
59
+ GUARD_PR_DISCLOSURE: BLOCK_REDIRECT,
60
+ GUARD_WRAPPED_WP: BLOCK_REDIRECT,
61
+ GUARD_SOURCE_ENTRYPOINT: BLOCK_REDIRECT,
62
+ GUARD_DEV_TOOL_REDIRECT: BLOCK_REDIRECT,
63
+ GUARD_DATA_HEAVY_SANDBOX: BLOCK_REDIRECT,
64
+ GUARD_SESSION_MEMORY_REDIRECT: BLOCK_REDIRECT,
65
+ // --- pretool-guard prompt routing (block + enforce: no tool redirect) ------
66
+ GUARD_PROMPT_INTENT: BLOCK_ENFORCE,
67
+ // --- pretool-guard fail-closed error path (block + enforce) ----------------
68
+ GUARD_PRETOOL_FAILURE: BLOCK_ENFORCE,
69
+ // --- pretool-guard validators (block + enforce) ---------------------------
70
+ GUARD_FORBIDDEN_COMMAND: BLOCK_ENFORCE,
71
+ GUARD_DANGEROUS_COMMAND: BLOCK_ENFORCE,
72
+ GUARD_BLUEPRINT_INVALID: BLOCK_ENFORCE,
73
+ GUARD_BLUEPRINT_FIRST: BLOCK_ENFORCE,
74
+ GUARD_DOCS_GOVERNANCE: BLOCK_ENFORCE,
75
+ GUARD_PLAN_FRONTMATTER: BLOCK_ENFORCE,
76
+ GUARD_COMPLEXITY: BLOCK_ENFORCE,
77
+ GUARD_PACKAGE_IMPORTS: BLOCK_ENFORCE,
78
+ GUARD_FILE_CONVENTIONS: BLOCK_ENFORCE,
79
+ GUARD_COMMAND_FILE: BLOCK_ENFORCE,
80
+ GUARD_TEST_QUALITY: BLOCK_ENFORCE,
81
+ GUARD_UX_QUALITY: BLOCK_ENFORCE,
82
+ GUARD_WORKTREE_DISCIPLINE: BLOCK_ENFORCE,
83
+ // --- permission-request (host-boundary decisions) -------------------------
84
+ GUARD_PERMISSION_ALLOWED: ALLOW_ENFORCE,
85
+ GUARD_PERMISSION_DENIED: BLOCK_ENFORCE,
86
+ GUARD_HOST_DEFAULT: DEFER_HOST,
87
+ };
88
+ /**
89
+ * Map a pretool-guard validator name to its canonical invariant code. Runner
90
+ * derivation falls back to {@link validatorUnexpectedCode} when a validator is
91
+ * not listed here, so a newly-added validator still produces a classified,
92
+ * pattern-valid code.
93
+ */
94
+ export const GUARD_VALIDATOR_CODES = {
95
+ "forbidden-commands": "GUARD_FORBIDDEN_COMMAND",
96
+ "dangerous-commands": "GUARD_DANGEROUS_COMMAND",
97
+ blueprint: "GUARD_BLUEPRINT_INVALID",
98
+ "blueprint-first": "GUARD_BLUEPRINT_FIRST",
99
+ "docs-governance": "GUARD_DOCS_GOVERNANCE",
100
+ "plan-frontmatter": "GUARD_PLAN_FRONTMATTER",
101
+ complexity: "GUARD_COMPLEXITY",
102
+ "package-imports": "GUARD_PACKAGE_IMPORTS",
103
+ "file-conventions": "GUARD_FILE_CONVENTIONS",
104
+ "command-file": "GUARD_COMMAND_FILE",
105
+ "test-quality": "GUARD_TEST_QUALITY",
106
+ "ux-quality": "GUARD_UX_QUALITY",
107
+ "worktree-discipline": "GUARD_WORKTREE_DISCIPLINE",
108
+ };
109
+ const GUARD_CODE_RE = /^GUARD_[A-Z0-9_]+$/u;
110
+ const GUARD_UNEXPECTED_RE = /^GUARD_[A-Z0-9_]+_UNEXPECTED$/u;
111
+ /**
112
+ * Max code-point length of the derived validator-name segment in an UNEXPECTED
113
+ * fallback. Bounds the whole token to `GUARD_` (6) + 48 + `_UNEXPECTED` (11) =
114
+ * 65 code points, comfortably under {@link GUARD_REASON_BUDGET}, so the fallback
115
+ * path can never blow the 80-code-point wire budget.
116
+ */
117
+ const UNEXPECTED_NAME_MAX = 48;
118
+ /**
119
+ * Deterministic, length-bounded per-validator fallback code, e.g. `test-quality`
120
+ * -> `GUARD_TEST_QUALITY_UNEXPECTED`. Always {@link GUARD_INTENT.enforce}. The
121
+ * name segment is capped so even a pathologically long validator name yields a
122
+ * token well under the wire budget.
123
+ */
124
+ export function validatorUnexpectedCode(validatorName) {
125
+ const screaming = validatorName
126
+ .toUpperCase()
127
+ .replace(/[^A-Z0-9]+/gu, "_")
128
+ .replace(/^_+|_+$/gu, "")
129
+ .slice(0, UNEXPECTED_NAME_MAX)
130
+ .replace(/_+$/gu, "");
131
+ return `GUARD_${screaming || "UNKNOWN"}_UNEXPECTED`;
132
+ }
133
+ /**
134
+ * Narrow to a registered guard reason code. This is SOUND: it returns true
135
+ * only for actual keys of the closed {@link GUARD_REASON_CODES} set. A
136
+ * per-validator `GUARD_<NAME>_UNEXPECTED` fallback is NOT a registered code —
137
+ * use {@link isGuardReasonToken} for the broader accepted family.
138
+ */
139
+ export function isGuardReasonCode(value) {
140
+ return Object.prototype.hasOwnProperty.call(GUARD_REASON_CODES, value);
141
+ }
142
+ /**
143
+ * Narrow to any accepted guard reason token: a registered code or a well-formed
144
+ * `GUARD_<NAME>_UNEXPECTED` fallback. A fallback token is only accepted when it
145
+ * fits the wire budget as a prefix (token + ": " <= GUARD_REASON_BUDGET), so an
146
+ * accepted token can never force formatGuardReason to truncate its own prefix —
147
+ * consistent with the length-bounded {@link validatorUnexpectedCode}.
148
+ */
149
+ export function isGuardReasonToken(value) {
150
+ if (isGuardReasonCode(value))
151
+ return true;
152
+ if (!(GUARD_UNEXPECTED_RE.test(value) && GUARD_CODE_RE.test(value)))
153
+ return false;
154
+ // Count Unicode code points to match formatGuardReason's budget exactly
155
+ // (the regex already constrains tokens to ASCII, so this is belt-and-suspenders).
156
+ return [...value].length + ": ".length <= GUARD_REASON_BUDGET;
157
+ }
158
+ /** Resolve the outcome for a token; UNEXPECTED fallbacks are always block+enforce. */
159
+ export function outcomeForCode(code) {
160
+ if (isGuardReasonCode(code)) {
161
+ return GUARD_REASON_CODES[code];
162
+ }
163
+ return BLOCK_ENFORCE;
164
+ }
165
+ /** Total wire budget (in Unicode code points) for a host-displayed deny reason. */
166
+ export const GUARD_REASON_BUDGET = 80;
167
+ /** Truncate to at most `max` Unicode code points (never splits a surrogate pair). */
168
+ export function truncateCodePoints(value, max) {
169
+ if (max <= 0)
170
+ return "";
171
+ const points = Array.from(value);
172
+ return points.length <= max ? value : points.slice(0, max).join("");
173
+ }
174
+ /**
175
+ * Truncate a wire reason to the {@link GUARD_REASON_BUDGET} in Unicode code
176
+ * points (never splitting a surrogate pair). Truncates from the end, so a
177
+ * leading `GUARD_<CODE>: ` prefix always survives. Shared by buildDenyEnvelope
178
+ * so the defensive budget matches formatGuardReason exactly.
179
+ */
180
+ export function truncateReasonToBudget(reason) {
181
+ return truncateCodePoints(reason, GUARD_REASON_BUDGET);
182
+ }
183
+ /**
184
+ * Produce the host-displayed wire reason `"<CODE>: <message>"`.
185
+ *
186
+ * The code prefix is reserved FIRST, then the message is sanitized (whitespace
187
+ * collapsed) and truncated by Unicode code points so the total never exceeds
188
+ * {@link GUARD_REASON_BUDGET} and no emoji/surrogate pair is split. The code
189
+ * therefore always survives the host budget — that survival is the entire
190
+ * cross-host stdout integration.
191
+ *
192
+ * `code` is a typed {@link GuardReasonToken}, so an over-budget prefix is not a
193
+ * reachable input (all registered codes and validator fallbacks are far under
194
+ * 80). If one somehow occurs, the prefix is preserved and the message dropped
195
+ * rather than corrupting the code.
196
+ *
197
+ * A message that already begins with the same `CODE: ` prefix is not
198
+ * double-prefixed (idempotent for re-formatted reasons).
199
+ */
200
+ export function formatGuardReason(code, message) {
201
+ const prefix = `${code}: `;
202
+ const collapsed = message.replace(/\s+/gu, " ").trim();
203
+ const withoutDuplicatePrefix = collapsed.startsWith(prefix)
204
+ ? collapsed.slice(prefix.length)
205
+ : collapsed;
206
+ const prefixPoints = Array.from(prefix).length;
207
+ const room = GUARD_REASON_BUDGET - prefixPoints;
208
+ if (room <= 0) {
209
+ // Unreachable for real tokens (all registered codes and bounded UNEXPECTED
210
+ // fallbacks are far under budget). If a caller ever hands in an oversized
211
+ // token, the 80-code-point budget still wins: truncate code-point-safe.
212
+ return truncateCodePoints(prefix, GUARD_REASON_BUDGET);
213
+ }
214
+ return `${prefix}${truncateCodePoints(withoutDuplicatePrefix, room)}`;
215
+ }
@@ -8,6 +8,32 @@ export declare function writeHookOutput(output: string): void;
8
8
  * Cross-platform: no-ops on Windows (fd redirect not portable there).
9
9
  */
10
10
  export declare function suppressStderr(): void;
11
+ /**
12
+ * Capture intentional guard feedback at an in-process execution boundary.
13
+ *
14
+ * Production launchers leave this unset and write to the real fd 2. The
15
+ * conformance batch installs a row-scoped capture so direct fd delivery stays
16
+ * attributable to the row that emitted it.
17
+ */
18
+ export declare function installGuardStderrCapture(capture: (text: string) => void): () => void;
19
+ /**
20
+ * Suppress only DECORATIVE JS stderr (console.error / process.stderr.write
21
+ * diagnostics) while keeping the real fd 2 open. Unlike {@link suppressStderr}
22
+ * (which closes fd 2 to also block native stderr), this preserves the host's
23
+ * stderr channel so a PreToolUse deny can deliver its coded reason there —
24
+ * Claude reads STDERR, not stdout, as the blocking feedback on an exit-2 deny.
25
+ *
26
+ * Use this instead of {@link suppressStderr} on the pretool-guard path, which
27
+ * must both hide noise AND surface the coded deny reason on stderr.
28
+ */
29
+ export declare function suppressDecorativeStderr(): void;
30
+ /**
31
+ * Write intentional guard feedback (a coded deny reason) to the REAL stderr —
32
+ * the channel Claude reads on an exit-2 PreToolUse deny. When decorative stderr
33
+ * is suppressed (fd 2 still open), write directly to fd 2 to bypass the swallow;
34
+ * otherwise use the normal `process.stderr`. Never throws.
35
+ */
36
+ export declare function writeGuardStderr(text: string): void;
11
37
  /**
12
38
  * Read all stdin bytes and return as UTF-8 string.
13
39
  */
@@ -1,4 +1,4 @@
1
- import { closeSync, openSync } from "node:fs";
1
+ import { closeSync, openSync, writeSync } from "node:fs";
2
2
  import { rememberHookStdin } from "#hooks/logs/store.js";
3
3
  export function writeHookOutput(output) {
4
4
  process.stdout.write(output);
@@ -22,6 +22,69 @@ export function suppressStderr() {
22
22
  // If stderr is already closed or /dev/null unavailable, ignore silently
23
23
  }
24
24
  }
25
+ /**
26
+ * True once {@link suppressDecorativeStderr} has swallowed decorative JS stderr
27
+ * while keeping the real fd 2 open, so {@link writeGuardStderr} delivers the
28
+ * coded reason to the real stderr via a direct fd write.
29
+ */
30
+ let decorativeStderrSuppressed = false;
31
+ let guardStderrCapture = null;
32
+ /**
33
+ * Capture intentional guard feedback at an in-process execution boundary.
34
+ *
35
+ * Production launchers leave this unset and write to the real fd 2. The
36
+ * conformance batch installs a row-scoped capture so direct fd delivery stays
37
+ * attributable to the row that emitted it.
38
+ */
39
+ export function installGuardStderrCapture(capture) {
40
+ const previous = guardStderrCapture;
41
+ guardStderrCapture = capture;
42
+ return () => {
43
+ if (guardStderrCapture === capture)
44
+ guardStderrCapture = previous;
45
+ };
46
+ }
47
+ /**
48
+ * Suppress only DECORATIVE JS stderr (console.error / process.stderr.write
49
+ * diagnostics) while keeping the real fd 2 open. Unlike {@link suppressStderr}
50
+ * (which closes fd 2 to also block native stderr), this preserves the host's
51
+ * stderr channel so a PreToolUse deny can deliver its coded reason there —
52
+ * Claude reads STDERR, not stdout, as the blocking feedback on an exit-2 deny.
53
+ *
54
+ * Use this instead of {@link suppressStderr} on the pretool-guard path, which
55
+ * must both hide noise AND surface the coded deny reason on stderr.
56
+ */
57
+ export function suppressDecorativeStderr() {
58
+ if (process.platform === "win32")
59
+ return;
60
+ if (decorativeStderrSuppressed)
61
+ return;
62
+ const swallow = (() => true);
63
+ process.stderr.write = swallow;
64
+ decorativeStderrSuppressed = true;
65
+ }
66
+ /**
67
+ * Write intentional guard feedback (a coded deny reason) to the REAL stderr —
68
+ * the channel Claude reads on an exit-2 PreToolUse deny. When decorative stderr
69
+ * is suppressed (fd 2 still open), write directly to fd 2 to bypass the swallow;
70
+ * otherwise use the normal `process.stderr`. Never throws.
71
+ */
72
+ export function writeGuardStderr(text) {
73
+ try {
74
+ if (guardStderrCapture) {
75
+ guardStderrCapture(text);
76
+ return;
77
+ }
78
+ if (decorativeStderrSuppressed) {
79
+ writeSync(2, text);
80
+ return;
81
+ }
82
+ process.stderr.write(text);
83
+ }
84
+ catch {
85
+ // Never block the hook on feedback delivery.
86
+ }
87
+ }
25
88
  /**
26
89
  * Read all stdin bytes and return as UTF-8 string.
27
90
  */
@@ -63,8 +63,9 @@ export type ClaudeCodexDenyEnvelope = {
63
63
  readonly hookSpecificOutput: ClaudeDenyHookSpecificOutput;
64
64
  };
65
65
  /**
66
- * Dual shape for Grok freeload/native: Grok honors decision:"deny" (+ exit 2);
67
- * Claude still reads hookSpecificOutput.
66
+ * Dual shape for native/unlabeled Grok: Grok honors decision:"deny" (+ exit 2).
67
+ * Claude Code / Codex reject a top-level `decision` other than approve|block,
68
+ * so they never receive this shape — they read hookSpecificOutput instead.
68
69
  */
69
70
  export type GrokDualDenyEnvelope = {
70
71
  readonly decision: "deny";
@@ -75,9 +76,11 @@ export type DenyEnvelope = ClaudeCodexDenyEnvelope | GrokDualDenyEnvelope;
75
76
  /**
76
77
  * Build a policy deny envelope.
77
78
  *
78
- * - `codex`: Claude-compatible hookSpecificOutput only (golden schema).
79
- * - `grok` / default (incl. freeload with host labeled claude): dual Grok
80
- * decision:"deny" + Claude hookSpecificOutput. Callers must also exit 2.
79
+ * - `claude` / `codex`: hookSpecificOutput only, because Claude Code and Codex
80
+ * strict-validate the hook-output root and reject any top-level `decision`
81
+ * other than approve|block. Deny is still delivered via the caller's exit 2.
82
+ * - `grok` / default / unknown: dual Grok decision:"deny" + hookSpecificOutput
83
+ * (Grok reads the top-level decision). Callers must also exit 2.
81
84
  */
82
85
  export declare function buildDenyEnvelope(options: {
83
86
  readonly reason: string;
@@ -1,3 +1,4 @@
1
+ import { truncateReasonToBudget } from "#hooks/shared/guard-outcome";
1
2
  /**
2
3
  * Prefer snake_case when both styles are present (Claude/Codex contract).
3
4
  * Accept Grok camelCase envelopes so freeload and native Grok hooks parse.
@@ -148,25 +149,39 @@ function resolveDenyHost(env = process.env) {
148
149
  /**
149
150
  * Build a policy deny envelope.
150
151
  *
151
- * - `codex`: Claude-compatible hookSpecificOutput only (golden schema).
152
- * - `grok` / default (incl. freeload with host labeled claude): dual Grok
153
- * decision:"deny" + Claude hookSpecificOutput. Callers must also exit 2.
152
+ * - `claude` / `codex`: hookSpecificOutput only, because Claude Code and Codex
153
+ * strict-validate the hook-output root and reject any top-level `decision`
154
+ * other than approve|block. Deny is still delivered via the caller's exit 2.
155
+ * - `grok` / default / unknown: dual Grok decision:"deny" + hookSpecificOutput
156
+ * (Grok reads the top-level decision). Callers must also exit 2.
154
157
  */
155
158
  export function buildDenyEnvelope(options, env = process.env) {
159
+ // Defensive budget: the permissionDecisionReason contract is <=80 chars. A
160
+ // reason from formatGuardReason is already within budget (idempotent here);
161
+ // a raw over-budget reason from any future caller is truncated by Unicode
162
+ // code points (never splitting a surrogate pair, matching formatGuardReason),
163
+ // from the end, so a leading GUARD_ code prefix always survives.
164
+ const reason = truncateReasonToBudget(options.reason);
156
165
  const hookSpecificOutput = {
157
166
  hookEventName: "PreToolUse",
158
167
  permissionDecision: "deny",
159
- permissionDecisionReason: options.reason,
168
+ permissionDecisionReason: reason,
160
169
  };
170
+ // Claude Code and Codex strict-validate the PreToolUse hook-output root and
171
+ // REJECT any top-level `decision` other than approve|block (Claude Code
172
+ // surfaces "Hook JSON output validation failed"). A real Claude host is
173
+ // labeled WP_HOOK_HOST=claude, so it MUST get the schema-valid
174
+ // hookSpecificOutput shape — not the dual envelope. The runner's exit-2
175
+ // delivers the deny regardless of stdout shape.
161
176
  const host = resolveDenyHost(env);
162
- if (host === "codex") {
177
+ if (host === "codex" || host === "claude") {
163
178
  return { hookSpecificOutput };
164
179
  }
165
- // Grok native, freeload (often WP_HOOK_HOST=claude), and unknown hosts:
166
- // dual-emit so Grok JSON deny works; Claude ignores extra top-level keys.
180
+ // Native Grok (WP_HOOK_HOST=grok) and unknown/unlabeled hosts (treated as a
181
+ // Grok freeload fallback): dual-emit so Grok's JSON deny works. Callers exit 2.
167
182
  return {
168
183
  decision: "deny",
169
- reason: options.reason,
184
+ reason,
170
185
  hookSpecificOutput,
171
186
  };
172
187
  }
@@ -21,6 +21,17 @@ export declare function getChangedFiles(projectDir: string): string[];
21
21
  export declare function filterQaFiles(files: string[]): string[];
22
22
  export declare function getTypecheckFiles(files: string[]): string[];
23
23
  export declare function findTestFiles(sourceFile: string, projectDir: string): string[];
24
+ /**
25
+ * Every test path the cross-cutting map can yield.
26
+ *
27
+ * Exported so a guard test can assert these still exist on disk. Without that
28
+ * guard a rename would keep the unit tests green — they only compare strings —
29
+ * while the caller's existence filter silently dropped the path, reinstating
30
+ * the very under-selection this map exists to prevent.
31
+ */
32
+ export declare function crossCuttingTestDependentPaths(): string[];
33
+ /** Non-colocated tests a changed file is known to affect. */
34
+ export declare function findCrossCuttingTestFiles(sourceFile: string): string[];
24
35
  export declare function discoverTestFiles(changedFiles: string[], projectDir: string): string[];
25
36
  export declare function buildTypecheckCommand(files: string[]): string | null;
26
37
  export declare function buildTestCommand(files: string[]): string | null;