@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
@@ -0,0 +1,57 @@
1
+ export declare const WEBPRESSO_MCP_SERVER_NAME = "webpresso";
2
+ /**
3
+ * The single canonical launch spec for the webpresso MCP server. `name` is the
4
+ * server alias every host registers; `args` are appended after the resolved
5
+ * `wp` launcher; `env` is forwarded to the spawned process by every
6
+ * env-capable channel. `WP_SKIP_UPDATE_CHECK=1` keeps MCP startup from doing a
7
+ * network update probe on every host session.
8
+ */
9
+ export declare const WEBPRESSO_MCP_SPEC: {
10
+ readonly name: "webpresso";
11
+ readonly args: readonly ["mcp"];
12
+ readonly env: {
13
+ readonly WP_SKIP_UPDATE_CHECK: "1";
14
+ };
15
+ };
16
+ export type WebpressoMcpSpec = typeof WEBPRESSO_MCP_SPEC;
17
+ /**
18
+ * Resolve `binary` against PATH, returning the first executable candidate.
19
+ * Returns the candidate as an absolute path: relative PATH entries (`bin`,
20
+ * `.`) resolve against the current working directory — persisting them
21
+ * verbatim into host configs would resolve differently in later sessions.
22
+ * No realpath is taken, so generated configs stay anchored to a stable
23
+ * global-bin symlink rather than a version-pinned store path.
24
+ */
25
+ export declare function resolveExecutableOnPath(binary: string, env: NodeJS.ProcessEnv): string | null;
26
+ export interface WebpressoInstallProbe {
27
+ /** Test seam — override the candidate roots. Default: probe in fixed order. */
28
+ candidates?: readonly string[];
29
+ /** Test seam — resolve the stable global `wp` shim available on PATH. */
30
+ stableEntry?: () => string | null;
31
+ /** Test seam — verify that a PATH entry is the Webpresso command. */
32
+ stableEntryIsWebpresso?: (entryPath: string) => boolean;
33
+ }
34
+ /**
35
+ * Resolve the absolute path to webpresso's global `bin/wp` launcher on this
36
+ * machine. Probes the locations consumers use to install webpresso, in order
37
+ * of stability:
38
+ *
39
+ * 1. the stable global `wp` shim available on PATH
40
+ * 2. the currently executing `@webpresso/agent-kit` package
41
+ * 3. Claude plugin install — `~/.claude/plugins/cache/.../agent-kit/`
42
+ * 4. bun global — `~/.bun/install/global/node_modules/@webpresso/agent-kit/`
43
+ * Returns `null` when none of the candidates contain a usable `bin/wp`.
44
+ */
45
+ export declare function findWebpressoMcpEntry(probe?: WebpressoInstallProbe): string | null;
46
+ export declare function resolveWebpressoMcpEntry(probe?: WebpressoInstallProbe): {
47
+ entryPath: string | null;
48
+ checked: readonly string[];
49
+ };
50
+ export declare function isWebpressoWpEntry(entryPath: string): boolean;
51
+ /** Run a discovery probe with bounded output and SIGKILL escalation. */
52
+ export declare function runQuiet(cmd: string, args: readonly string[], timeoutMs?: number): string | null;
53
+ /** Launch tuple for the webpresso MCP server given a resolved `wp` launcher. */
54
+ export declare function agentKitMcpLaunchCommand(entryPath: string): {
55
+ command: string;
56
+ args: string[];
57
+ };
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Canonical Webpresso MCP server spec + shared `wp` binary resolver.
3
+ *
4
+ * One source of truth for how every supported host launches the webpresso MCP
5
+ * server. Each host channel (Claude plugin manifest, Codex plugin manifest,
6
+ * Codex `config.toml`, OpenCode `opencode.json`, Grok `.grok/config.toml`)
7
+ * projects this spec into its own config dialect. The two committed plugin
8
+ * manifests reference the launcher via `${CLAUDE_PLUGIN_ROOT}`/`${PLUGIN_ROOT}`;
9
+ * the setup-time writers resolve an absolute `bin/wp` path via
10
+ * {@link findWebpressoMcpEntry}. Compare channels *semantically* (command
11
+ * basename, args, env) — never byte-for-byte.
12
+ */
13
+ import { accessSync, constants, existsSync, readFileSync, realpathSync, statSync } from "node:fs";
14
+ import { homedir } from "node:os";
15
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
16
+ import { execFileSync } from "node:child_process";
17
+ import { fileURLToPath } from "node:url";
18
+ import { pathCandidates } from "#runtime/command-exists.js";
19
+ export const WEBPRESSO_MCP_SERVER_NAME = "webpresso";
20
+ /**
21
+ * The single canonical launch spec for the webpresso MCP server. `name` is the
22
+ * server alias every host registers; `args` are appended after the resolved
23
+ * `wp` launcher; `env` is forwarded to the spawned process by every
24
+ * env-capable channel. `WP_SKIP_UPDATE_CHECK=1` keeps MCP startup from doing a
25
+ * network update probe on every host session.
26
+ */
27
+ export const WEBPRESSO_MCP_SPEC = {
28
+ name: WEBPRESSO_MCP_SERVER_NAME,
29
+ args: ["mcp"],
30
+ env: { WP_SKIP_UPDATE_CHECK: "1" },
31
+ };
32
+ /**
33
+ * Resolve `binary` against PATH, returning the first executable candidate.
34
+ * Returns the candidate as an absolute path: relative PATH entries (`bin`,
35
+ * `.`) resolve against the current working directory — persisting them
36
+ * verbatim into host configs would resolve differently in later sessions.
37
+ * No realpath is taken, so generated configs stay anchored to a stable
38
+ * global-bin symlink rather than a version-pinned store path.
39
+ */
40
+ export function resolveExecutableOnPath(binary, env) {
41
+ for (const candidate of pathCandidates(binary, {
42
+ pathEnv: env.PATH,
43
+ pathExtEnv: env.PATHEXT,
44
+ })) {
45
+ try {
46
+ if (!statSync(candidate).isFile())
47
+ continue;
48
+ if (process.platform !== "win32")
49
+ accessSync(candidate, constants.X_OK);
50
+ return isAbsolute(candidate) ? candidate : resolve(candidate);
51
+ }
52
+ catch {
53
+ // Continue scanning PATH.
54
+ }
55
+ }
56
+ return null;
57
+ }
58
+ const WP_BIN_RELATIVE = join("bin", "wp");
59
+ const WP_LAUNCHER_REQUIRED_RELATIVES = [
60
+ WP_BIN_RELATIVE,
61
+ join("bin", "_run.js"),
62
+ join("bin", "runtime-lanes.js"),
63
+ join("bin", "runtime-manifest.json"),
64
+ ];
65
+ /**
66
+ * Resolve the absolute path to webpresso's global `bin/wp` launcher on this
67
+ * machine. Probes the locations consumers use to install webpresso, in order
68
+ * of stability:
69
+ *
70
+ * 1. the stable global `wp` shim available on PATH
71
+ * 2. the currently executing `@webpresso/agent-kit` package
72
+ * 3. Claude plugin install — `~/.claude/plugins/cache/.../agent-kit/`
73
+ * 4. bun global — `~/.bun/install/global/node_modules/@webpresso/agent-kit/`
74
+ * Returns `null` when none of the candidates contain a usable `bin/wp`.
75
+ */
76
+ export function findWebpressoMcpEntry(probe = {}) {
77
+ return resolveWebpressoMcpEntry(probe).entryPath;
78
+ }
79
+ function isUsableWebpressoMcpRoot(root) {
80
+ return WP_LAUNCHER_REQUIRED_RELATIVES.every((relativePath) => existsSync(join(root, relativePath)));
81
+ }
82
+ export function resolveWebpressoMcpEntry(probe = {}) {
83
+ const checked = [];
84
+ if (probe.candidates === undefined) {
85
+ const stableEntry = probe.stableEntry !== undefined
86
+ ? probe.stableEntry()
87
+ : resolveExecutableOnPath("wp", process.env);
88
+ if (stableEntry) {
89
+ checked.push(stableEntry);
90
+ const packageWp = packageWpFromAdjacentNpmCmdShim(stableEntry);
91
+ if (packageWp !== null)
92
+ return { entryPath: packageWp, checked };
93
+ const verifierAllows = probe.stableEntryIsWebpresso?.(stableEntry) ?? true;
94
+ if (verifierAllows && isWebpressoWpEntry(stableEntry)) {
95
+ return { entryPath: stableEntry, checked };
96
+ }
97
+ }
98
+ }
99
+ const candidateGroups = probe.candidates !== undefined ? [probe.candidates] : [defaultFixedCandidates()];
100
+ for (const candidates of candidateGroups) {
101
+ for (const root of candidates) {
102
+ if (!root)
103
+ continue;
104
+ checked.push(root);
105
+ const wpBin = join(root, WP_BIN_RELATIVE);
106
+ if (isTrustedWebpressoPackageRoot(root))
107
+ return { entryPath: wpBin, checked };
108
+ }
109
+ }
110
+ return { entryPath: null, checked };
111
+ }
112
+ function defaultFixedCandidates() {
113
+ const home = process.env.HOME || homedir();
114
+ const packageRoot = currentAgentKitPackageRoot();
115
+ const claudePlugin = join(home, ".claude", "plugins", "cache", "webpresso", "agent-kit");
116
+ const bunGlobal = join(home, ".bun", "install", "global", "node_modules", "@webpresso", "agent-kit");
117
+ return [packageRoot ?? "", claudePlugin, bunGlobal];
118
+ }
119
+ function currentAgentKitPackageRoot() {
120
+ let current = dirname(fileURLToPath(import.meta.url));
121
+ for (let depth = 0; depth < 12; depth += 1) {
122
+ const packagePath = join(current, "package.json");
123
+ if (existsSync(packagePath)) {
124
+ try {
125
+ const pkg = JSON.parse(readFileSync(packagePath, "utf8"));
126
+ if (pkg.name === "@webpresso/agent-kit")
127
+ return current;
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ }
133
+ const parent = dirname(current);
134
+ if (parent === current)
135
+ break;
136
+ current = parent;
137
+ }
138
+ return null;
139
+ }
140
+ function isUsableWebpressoPackageRoot(packageRoot) {
141
+ try {
142
+ const pkg = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
143
+ return pkg.name === "@webpresso/agent-kit" && isUsableWebpressoMcpRoot(packageRoot);
144
+ }
145
+ catch {
146
+ return false;
147
+ }
148
+ }
149
+ function isPathInsideOrEqual(parent, candidate) {
150
+ const parentRoot = realpathSync(parent);
151
+ const candidateRoot = realpathSync(candidate);
152
+ const rel = relative(resolve(parentRoot), resolve(candidateRoot));
153
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
154
+ }
155
+ function isTrustedWebpressoPackageRoot(packageRoot) {
156
+ try {
157
+ if (!isUsableWebpressoPackageRoot(packageRoot))
158
+ return false;
159
+ const realPackageRoot = realpathSync(packageRoot);
160
+ const sourceRoot = currentAgentKitPackageRoot();
161
+ if (sourceRoot !== null && realPackageRoot === realpathSync(sourceRoot))
162
+ return true;
163
+ return trustedHomeInstallPackageRoots().some((anchor) => existsSync(anchor) && isPathInsideOrEqual(anchor, realPackageRoot));
164
+ }
165
+ catch {
166
+ return false;
167
+ }
168
+ }
169
+ function trustedHomeInstallPackageRoots() {
170
+ const home = process.env.HOME || homedir();
171
+ return [
172
+ join(home, ".claude", "plugins", "cache", "webpresso", "agent-kit"),
173
+ join(home, ".bun", "install", "global", "node_modules", "@webpresso", "agent-kit"),
174
+ ];
175
+ }
176
+ function packageWpFromAdjacentNpmCmdShim(entryPath) {
177
+ if (!entryPath.toLowerCase().endsWith(".cmd"))
178
+ return null;
179
+ const packageRoot = join(dirname(entryPath), "node_modules", "@webpresso", "agent-kit");
180
+ return isTrustedWebpressoPackageRoot(packageRoot) ? join(packageRoot, "bin", "wp") : null;
181
+ }
182
+ export function isWebpressoWpEntry(entryPath) {
183
+ try {
184
+ if (entryPath.toLowerCase().endsWith(".cmd"))
185
+ return false;
186
+ // Validate package ownership from the symlink target. Executing a binary
187
+ // and trusting its self-reported version lets any same-named PATH entry
188
+ // impersonate Webpresso.
189
+ const resolvedEntry = realpathSync(entryPath);
190
+ const packageRoot = dirname(dirname(resolvedEntry));
191
+ if (resolvedEntry !== join(packageRoot, WP_BIN_RELATIVE))
192
+ return false;
193
+ return isTrustedWebpressoPackageRoot(packageRoot);
194
+ }
195
+ catch {
196
+ return false;
197
+ }
198
+ }
199
+ const PROBE_SUPERVISOR = String.raw `
200
+ const { spawn } = require("node:child_process");
201
+ const input = JSON.parse(process.argv[1]);
202
+ const child = spawn(input.cmd, input.args, {
203
+ detached: process.platform !== "win32",
204
+ env: { ...process.env, WP_SKIP_UPDATE_CHECK: "1" },
205
+ stdio: ["ignore", "pipe", "ignore"],
206
+ windowsHide: true,
207
+ });
208
+ let output = "";
209
+ let timedOut = false;
210
+ let finished = false;
211
+ const signal = (name) => {
212
+ try {
213
+ if (process.platform !== "win32" && child.pid) process.kill(-child.pid, name);
214
+ else child.kill(name);
215
+ } catch {}
216
+ };
217
+ const finish = (value) => {
218
+ if (finished) return;
219
+ finished = true;
220
+ clearTimeout(deadline);
221
+ clearTimeout(forceKill);
222
+ process.stdout.write(JSON.stringify(value));
223
+ };
224
+ let forceKill;
225
+ const deadline = setTimeout(() => {
226
+ timedOut = true;
227
+ signal("SIGTERM");
228
+ forceKill = setTimeout(() => signal("SIGKILL"), input.killGraceMs);
229
+ }, input.timeoutMs);
230
+ child.stdout.setEncoding("utf8");
231
+ child.stdout.on("data", (chunk) => {
232
+ output += chunk;
233
+ if (Buffer.byteLength(output) > input.maxBytes) {
234
+ timedOut = true;
235
+ output = "";
236
+ child.stdout.destroy();
237
+ signal("SIGKILL");
238
+ }
239
+ });
240
+ child.on("error", () => finish(null));
241
+ child.on("exit", (code) => finish(!timedOut && code === 0 ? output.trim() || null : null));
242
+ `;
243
+ /** Run a discovery probe with bounded output and SIGKILL escalation. */
244
+ export function runQuiet(cmd, args, timeoutMs = 1_500) {
245
+ try {
246
+ const output = execFileSync(process.execPath, [
247
+ "-e",
248
+ PROBE_SUPERVISOR,
249
+ JSON.stringify({
250
+ cmd,
251
+ args: [...args],
252
+ timeoutMs,
253
+ killGraceMs: 100,
254
+ maxBytes: 64 * 1024,
255
+ }),
256
+ ], {
257
+ encoding: "utf8",
258
+ env: { ...process.env, WP_SKIP_UPDATE_CHECK: "1" },
259
+ stdio: ["ignore", "pipe", "ignore"],
260
+ timeout: timeoutMs + 1_000,
261
+ // JSON escaping can expand the supervisor's already-bounded 64 KiB payload.
262
+ maxBuffer: 512 * 1024,
263
+ });
264
+ return JSON.parse(output);
265
+ }
266
+ catch {
267
+ return null;
268
+ }
269
+ }
270
+ /** Launch tuple for the webpresso MCP server given a resolved `wp` launcher. */
271
+ export function agentKitMcpLaunchCommand(entryPath) {
272
+ return { command: entryPath, args: [...WEBPRESSO_MCP_SPEC.args] };
273
+ }
@@ -4,6 +4,7 @@ export interface MergeOptions {
4
4
  overwrite?: boolean;
5
5
  dryRun?: boolean;
6
6
  ownership?: MergeOwnership;
7
+ repoRoot?: string;
7
8
  }
8
9
  export interface MergeResult {
9
10
  targetPath: string;
@@ -7,7 +7,33 @@
7
7
  * only.
8
8
  */
9
9
  import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync, readdirSync } from "node:fs";
10
- import { dirname, join } from "node:path";
10
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
11
+ import { convergeUnitFromTarget } from "./convergence.js";
12
+ import { resolveUntrackedCollision } from "./untracked-collision.js";
13
+ function guardUntrackedOverwrite(targetPath, incoming, opts) {
14
+ if (!opts.repoRoot)
15
+ return;
16
+ const repoRoot = resolve(opts.repoRoot);
17
+ const target = resolve(targetPath);
18
+ const repoRelative = relative(repoRoot, target);
19
+ if (repoRelative.length === 0 ||
20
+ repoRelative === ".." ||
21
+ repoRelative.startsWith(`..${sep}`) ||
22
+ isAbsolute(repoRelative)) {
23
+ return;
24
+ }
25
+ const result = resolveUntrackedCollision({
26
+ unit: convergeUnitFromTarget(repoRoot, target, {
27
+ action: "modify",
28
+ scope: "repo-committed",
29
+ reason: "setup merge target",
30
+ }),
31
+ desiredContent: incoming,
32
+ force: opts.overwrite,
33
+ });
34
+ if (!result.ok)
35
+ throw new Error(result.error);
36
+ }
11
37
  export function writeFileMerged(targetPath, incoming, opts = {}) {
12
38
  const exists = existsSync(targetPath);
13
39
  const existingContent = exists ? readFileSync(targetPath, "utf8") : null;
@@ -25,6 +51,7 @@ export function writeFileMerged(targetPath, incoming, opts = {}) {
25
51
  if (shouldOverwrite) {
26
52
  if (opts.dryRun)
27
53
  return { targetPath, action: "skipped-dry" };
54
+ guardUntrackedOverwrite(targetPath, incoming, opts);
28
55
  writeFileSync(targetPath, incoming);
29
56
  return { targetPath, action: "overwritten" };
30
57
  }
@@ -92,6 +119,8 @@ export function patchJsonFile(targetPath, patcher, opts = {}) {
92
119
  if (opts.dryRun) {
93
120
  return { targetPath, action: "skipped-dry" };
94
121
  }
122
+ if (exists)
123
+ guardUntrackedOverwrite(targetPath, incoming, opts);
95
124
  mkdirSync(dirname(targetPath), { recursive: true });
96
125
  writeFileSync(targetPath, incoming);
97
126
  return { targetPath, action: exists ? "overwritten" : "created" };
@@ -10,9 +10,6 @@ export declare const PLUGIN_CACHE_TARGETS: readonly [{
10
10
  }, {
11
11
  readonly host: "opencode";
12
12
  readonly rootParts: readonly [".config", "opencode", "plugins", "cache"];
13
- }, {
14
- readonly host: "windsurf";
15
- readonly rootParts: readonly [".windsurf", "plugins", "cache"];
16
13
  }, {
17
14
  readonly host: "agents";
18
15
  readonly rootParts: readonly [".agents", "plugins", "cache"];
@@ -6,7 +6,6 @@ export const PLUGIN_CACHE_TARGETS = [
6
6
  { host: "codex", rootParts: [".codex", "plugins", "cache"] },
7
7
  { host: "opencode", rootParts: [".opencode", "plugins", "cache"] },
8
8
  { host: "opencode", rootParts: [".config", "opencode", "plugins", "cache"] },
9
- { host: "windsurf", rootParts: [".windsurf", "plugins", "cache"] },
10
9
  { host: "agents", rootParts: [".agents", "plugins", "cache"] },
11
10
  { host: "factory", rootParts: [".factory", "plugins", "cache"] },
12
11
  ];
@@ -1,7 +1,7 @@
1
1
  import type { AgentkitConfig } from "./config.js";
2
2
  import type { ConsumerContext } from "./detect-consumer.js";
3
3
  import { type MergeOptions, type MergeResult } from "./merge.js";
4
- export declare const AGENTS_MD_MAX_BYTES = 8100;
4
+ export declare const AGENTS_MD_MAX_BYTES = 8180;
5
5
  export declare function renderRepositoryMap(consumer: ConsumerContext): string;
6
6
  export declare function renderTechStack(consumer: ConsumerContext): string;
7
7
  export interface ScaffoldAgentsMdInput {
@@ -29,9 +29,15 @@ const TECH_STACK_RULES = [
29
29
  { dep: /^zod$/, label: "Zod" },
30
30
  { dep: /^typescript$/, label: "TypeScript" },
31
31
  ];
32
- // Kept under 8 KB (8192 bytes); headroom above 8_000 absorbs the longer
33
- // `wp_ultragoal_*` native tool names in the Codex routing surface.
34
- export const AGENTS_MD_MAX_BYTES = 8_100;
32
+ // Kept under the 8 KB (8192-byte) hard ceiling. The rendered template embeds
33
+ // the full `native_tool_names` registry list, so this soft target tracks the
34
+ // tool count: adding an MCP tool grows the list by ~15-17 bytes. Bumped
35
+ // 8_100 -> 8_180 for the four-tool batch (wp_ci_preflight, wp_session_id,
36
+ // wp_session_info, wp_review_run), which renders ~8_160 — still 30+ bytes under
37
+ // 8_192. The 8_192 ceiling is the real guardrail; the next addition that would
38
+ // breach it should trigger a conscious restructure (or exclude the
39
+ // auto-generated tool list from the prose budget), not another reflexive bump.
40
+ export const AGENTS_MD_MAX_BYTES = 8_180;
35
41
  export function renderRepositoryMap(consumer) {
36
42
  const packages = consumer.workspacePackages;
37
43
  if (packages.length === 0) {
@@ -14,6 +14,17 @@ interface PackageJsonLike {
14
14
  [key: string]: unknown;
15
15
  }
16
16
  export declare function readSetupWpActionRef(metaUrl: string): string;
17
+ /**
18
+ * Idempotent, inject-by-content-absence repair for the managed `wp audit
19
+ * guardrails` CI step. On every `wp setup`, re-inserts the step into any
20
+ * wp-enabled quality workflow whose content lacks a guardrails invocation, so a
21
+ * consumer who deleted the step (with or without its comments) gets it back —
22
+ * and therefore keeps inheriting new guardrails (e.g. typescript-version) on
23
+ * their next agent-kit bump. Returns the content unchanged when the step is
24
+ * already present, the workflow is not a wp quality pipeline, or no test step
25
+ * anchor exists.
26
+ */
27
+ export declare function ensureGuardrailsCiStep(content: string): string;
17
28
  export declare function readAgentConfigVersion(metaUrl: string): string;
18
29
  export declare function collectRuntimeContractGuidance(packageJson: PackageJsonLike | null | undefined): RuntimeContractGuidance;
19
30
  export declare const BASE_KIT_QUALITY_TARGETS: string[];
@@ -162,6 +162,61 @@ function migrateWorkflowContent(content, actionRef, version) {
162
162
  })
163
163
  .join("\n");
164
164
  }
165
+ // Detects whether a workflow already invokes `wp audit guardrails`, keyed on the
166
+ // command itself (not a `wp` prefix nor a marker comment) — mirrors how
167
+ // `hasAuditBlock()` (audit-hooks scaffolder) self-heals the pre-commit hook by
168
+ // scanning for the invocation string, so re-injection works even if a consumer
169
+ // stripped every surrounding comment.
170
+ const GUARDRAILS_CI_INVOCATION = /\baudit guardrails\b/u;
171
+ // A workflow is a wp-enabled quality pipeline when it both installs the `wp`
172
+ // runtime and runs the default test suite. Gating on a wp-runtime anchor keeps
173
+ // injection off non-wp workflows (a reusable-workflow release.yml, or
174
+ // agent-kit's own source-built CI) that must never get a consumer step. Two
175
+ // install shapes count as the anchor: the `setup-wp` GitHub Action, and a
176
+ // global npm/pnpm/vp install of `@webpresso/agent-kit` — the documented
177
+ // third-party quick start for consumers that don't use the action (see
178
+ // `package-conventions.md`; e.g. ozby/edge-matte's ci.yml).
179
+ const SETUP_WP_MARKER = "setup-wp@";
180
+ const GLOBAL_AGENT_KIT_INSTALL = /^(?=.*\b(?:npm|pnpm|vp)\s+(?:install|i|add)\b)(?=.*(?:-g\b|--global\b))(?=.*@webpresso\/agent-kit\b).*$/imu;
181
+ const GUARDRAILS_CI_TEST_RUN = /(?:\bvp\s+run\s+test\b|\bpnpm\s+(?:run\s+)?test\b|\bnpm\s+(?:run\s+)?test\b|\bbun\s+(?:run\s+)?test\b|\bwp\s+test\b|\bvitest\s+run\b)/iu;
182
+ function hasWpRuntimeAnchor(content) {
183
+ return content.includes(SETUP_WP_MARKER) || GLOBAL_AGENT_KIT_INSTALL.test(content);
184
+ }
185
+ /**
186
+ * Idempotent, inject-by-content-absence repair for the managed `wp audit
187
+ * guardrails` CI step. On every `wp setup`, re-inserts the step into any
188
+ * wp-enabled quality workflow whose content lacks a guardrails invocation, so a
189
+ * consumer who deleted the step (with or without its comments) gets it back —
190
+ * and therefore keeps inheriting new guardrails (e.g. typescript-version) on
191
+ * their next agent-kit bump. Returns the content unchanged when the step is
192
+ * already present, the workflow is not a wp quality pipeline, or no test step
193
+ * anchor exists.
194
+ */
195
+ export function ensureGuardrailsCiStep(content) {
196
+ if (GUARDRAILS_CI_INVOCATION.test(content))
197
+ return content;
198
+ if (!hasWpRuntimeAnchor(content))
199
+ return content;
200
+ const lines = content.split("\n");
201
+ let anchorStepStart = -1;
202
+ let lastStepStart = -1;
203
+ for (let index = 0; index < lines.length; index += 1) {
204
+ const line = lines[index];
205
+ if (line.trim().startsWith("#"))
206
+ continue;
207
+ if (/^\s*-\s/u.test(line))
208
+ lastStepStart = index;
209
+ if (GUARDRAILS_CI_TEST_RUN.test(line)) {
210
+ anchorStepStart = lastStepStart;
211
+ break;
212
+ }
213
+ }
214
+ if (anchorStepStart === -1)
215
+ return content;
216
+ const indent = lines[anchorStepStart].match(/^(\s*)/u)?.[1] ?? "";
217
+ lines.splice(anchorStepStart, 0, `${indent}- name: Run wp audit guardrails`, `${indent} run: wp audit guardrails`);
218
+ return lines.join("\n");
219
+ }
165
220
  function migrateConsumerWorkflows(repoRoot, actionRef, version, options) {
166
221
  const workflowDir = join(repoRoot, ".github", "workflows");
167
222
  if (!existsSync(workflowDir))
@@ -171,7 +226,7 @@ function migrateConsumerWorkflows(repoRoot, actionRef, version, options) {
171
226
  .map((name) => {
172
227
  const targetPath = join(workflowDir, name);
173
228
  const existing = readFileSync(targetPath, "utf8");
174
- const migrated = migrateWorkflowContent(existing, actionRef, version);
229
+ const migrated = ensureGuardrailsCiStep(migrateWorkflowContent(existing, actionRef, version));
175
230
  if (migrated === existing)
176
231
  return { targetPath, action: "identical" };
177
232
  if (options.dryRun)
@@ -1,14 +1,20 @@
1
1
  /**
2
2
  * Grok Build native project hooks emitter.
3
3
  *
4
- * Writes a project-local `.grok/hooks.json` surface with managed commands
5
- * labeled `WP_HOOK_HOST=grok`. Claude freeload remains the fallback delivery
6
- * path; this is the first-class native project surface for doctor/setup.
4
+ * Grok reads project hook configs from `.grok/hooks/*.json` a near-clone of
5
+ * the Claude Code hook contract (stdin event JSON incl. hookEventName/cwd,
6
+ * regex matchers, exit code 2 = deny, JSON stdout for decisions). We write one
7
+ * managed file, `.grok/hooks/webpresso.json`, with commands labeled
8
+ * `WP_HOOK_HOST=grok`. Hooks are trust-gated: the user must run `/hooks-trust`
9
+ * once in Grok before they fire (fail-open on timeout).
10
+ *
11
+ * Managed events are the intersection of WP_HOOK_SPECS with Grok's documented
12
+ * event set — notably Grok has no PermissionRequest event (it has
13
+ * PermissionDenied instead), so that spec is skipped.
7
14
  */
8
- export declare const GROK_HOOKS_RELATIVE_PATH = ".grok/hooks.json";
15
+ export declare const GROK_HOOKS_DIR_RELATIVE_PATH = ".grok/hooks";
16
+ export declare const GROK_HOOKS_RELATIVE_PATH = ".grok/hooks/webpresso.json";
9
17
  export type GrokHooksDocument = {
10
- readonly version: 1;
11
- readonly host: "grok";
12
18
  readonly hooks: Readonly<Record<string, readonly {
13
19
  readonly matcher?: string;
14
20
  readonly hooks: readonly {
@@ -19,8 +25,8 @@ export type GrokHooksDocument = {
19
25
  }[]>>;
20
26
  };
21
27
  /**
22
- * Build managed Grok hook groups. PreToolUse uses a broad Bash|Write|Edit matcher;
23
- * PermissionRequest and SessionStart are included without matchers.
28
+ * Build the managed Grok hook document (Claude-contract wrapped shape:
29
+ * `{ "hooks": { Event: [ { matcher?, hooks: [...] } ] } }`).
24
30
  */
25
31
  export declare function buildGrokHookDocument(input: {
26
32
  readonly resolveBin: (name: string) => string;
@@ -1,33 +1,55 @@
1
1
  /**
2
2
  * Grok Build native project hooks emitter.
3
3
  *
4
- * Writes a project-local `.grok/hooks.json` surface with managed commands
5
- * labeled `WP_HOOK_HOST=grok`. Claude freeload remains the fallback delivery
6
- * path; this is the first-class native project surface for doctor/setup.
4
+ * Grok reads project hook configs from `.grok/hooks/*.json` a near-clone of
5
+ * the Claude Code hook contract (stdin event JSON incl. hookEventName/cwd,
6
+ * regex matchers, exit code 2 = deny, JSON stdout for decisions). We write one
7
+ * managed file, `.grok/hooks/webpresso.json`, with commands labeled
8
+ * `WP_HOOK_HOST=grok`. Hooks are trust-gated: the user must run `/hooks-trust`
9
+ * once in Grok before they fire (fail-open on timeout).
10
+ *
11
+ * Managed events are the intersection of WP_HOOK_SPECS with Grok's documented
12
+ * event set — notably Grok has no PermissionRequest event (it has
13
+ * PermissionDenied instead), so that spec is skipped.
7
14
  */
8
- import { mkdirSync, writeFileSync } from "node:fs";
9
15
  import { join } from "node:path";
10
16
  import { WP_HOOK_SPECS } from "#cli/commands/init/scaffolders/agent-hooks/ir.js";
11
- export const GROK_HOOKS_RELATIVE_PATH = ".grok/hooks.json";
17
+ import { updateScaffolderTextFile } from "#cli/commands/init/scaffolders/file-transaction.js";
18
+ export const GROK_HOOKS_DIR_RELATIVE_PATH = ".grok/hooks";
19
+ export const GROK_HOOKS_RELATIVE_PATH = `${GROK_HOOKS_DIR_RELATIVE_PATH}/webpresso.json`;
20
+ /** Grok's documented lifecycle events. */
21
+ const GROK_HOOK_EVENTS = new Set([
22
+ "SessionStart",
23
+ "SessionEnd",
24
+ "UserPromptSubmit",
25
+ "PreToolUse",
26
+ "PostToolUse",
27
+ "PostToolUseFailure",
28
+ "PermissionDenied",
29
+ "Stop",
30
+ "StopFailure",
31
+ "Notification",
32
+ "SubagentStart",
33
+ "SubagentStop",
34
+ "PreCompact",
35
+ "PostCompact",
36
+ ]);
12
37
  /**
13
- * Build managed Grok hook groups. PreToolUse uses a broad Bash|Write|Edit matcher;
14
- * PermissionRequest and SessionStart are included without matchers.
38
+ * Build the managed Grok hook document (Claude-contract wrapped shape:
39
+ * `{ "hooks": { Event: [ { matcher?, hooks: [...] } ] } }`).
15
40
  */
16
41
  export function buildGrokHookDocument(input) {
17
42
  const { resolveBin } = input;
18
43
  const hooks = {};
19
44
  for (const spec of WP_HOOK_SPECS) {
20
- // Grok managed subset: PreToolUse, PermissionRequest, SessionStart, PostToolUse.
21
- if (spec.event !== "PreToolUse" &&
22
- spec.event !== "PermissionRequest" &&
23
- spec.event !== "SessionStart" &&
24
- spec.event !== "PostToolUse") {
45
+ if (!GROK_HOOK_EVENTS.has(spec.event))
25
46
  continue;
26
- }
27
- // resolveBin (GROK_BIN) already labels WP_HOOK_HOST=grok on each branch of the
28
- // launcher chain. Only prefix when the resolver returns a bare command.
47
+ // resolveBin (GROK_BIN) already labels WP_HOOK_HOST=grok on each branch of
48
+ // the launcher chain with shell quoting (`WP_HOOK_HOST='grok'`), so match
49
+ // the variable name, not one quoting variant. Only prefix a bare command;
50
+ // prefixing a compound `if …` launcher would produce invalid `env … if`.
29
51
  const resolved = resolveBin(spec.bin);
30
- const command = resolved.includes("WP_HOOK_HOST=grok")
52
+ const command = /WP_HOOK_HOST=(['"]?)grok\1/u.test(resolved)
31
53
  ? resolved
32
54
  : `env WP_HOOK_HOST=grok ${resolved}`;
33
55
  const group = {
@@ -41,11 +63,7 @@ export function buildGrokHookDocument(input) {
41
63
  const existing = hooks[spec.event] ?? [];
42
64
  hooks[spec.event] = [...existing, group];
43
65
  }
44
- return {
45
- version: 1,
46
- host: "grok",
47
- hooks,
48
- };
66
+ return { hooks };
49
67
  }
50
68
  export function writeGrokProjectHooks(input) {
51
69
  const path = join(input.repoRoot, GROK_HOOKS_RELATIVE_PATH);
@@ -53,7 +71,6 @@ export function writeGrokProjectHooks(input) {
53
71
  if (input.dryRun) {
54
72
  return { path, written: false };
55
73
  }
56
- mkdirSync(join(input.repoRoot, ".grok"), { recursive: true });
57
- writeFileSync(path, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
58
- return { path, written: true };
74
+ const outcome = updateScaffolderTextFile(path, () => `${JSON.stringify(doc, null, 2)}\n`);
75
+ return { path, written: outcome === "written" };
59
76
  }