@webpresso/agent-kit 3.1.30 → 3.3.0

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

Potentially problematic release.


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

Files changed (300) hide show
  1. package/README.md +53 -15
  2. package/bin/_run.js +183 -20
  3. package/catalog/AGENTS.md.tpl +3 -3
  4. package/catalog/agent/agents/implementer.md +45 -0
  5. package/catalog/agent/rules/parallel-orchestration-quota.md +100 -0
  6. package/catalog/agent/rules/pre-implementation.md +28 -1
  7. package/catalog/agent/rules/supported-agent-clis.md +9 -0
  8. package/catalog/agent/rules/typescript-version.md +50 -0
  9. package/catalog/agent/skills/codex/SKILL.md +25 -1
  10. package/catalog/agent/skills/verify/SKILL.md +4 -2
  11. package/catalog/docs/templates/blueprint.md +1 -1
  12. package/dist/esm/audit/agents.js +1 -0
  13. package/dist/esm/audit/blueprint-lifecycle-sql.d.ts +12 -0
  14. package/dist/esm/audit/blueprint-lifecycle-sql.js +23 -2
  15. package/dist/esm/audit/blueprint-pr-coverage.js +4 -0
  16. package/dist/esm/audit/blueprint-trust.js +14 -10
  17. package/dist/esm/audit/changeset-evidence.d.ts +23 -0
  18. package/dist/esm/audit/changeset-evidence.js +30 -0
  19. package/dist/esm/audit/changeset-required.d.ts +10 -0
  20. package/dist/esm/audit/changeset-required.js +73 -0
  21. package/dist/esm/audit/ci-guardrails-detection.d.ts +27 -0
  22. package/dist/esm/audit/ci-guardrails-detection.js +68 -0
  23. package/dist/esm/audit/ci-guardrails-wiring.d.ts +22 -0
  24. package/dist/esm/audit/ci-guardrails-wiring.js +80 -0
  25. package/dist/esm/audit/ci-test-perf.js +30 -0
  26. package/dist/esm/audit/command-surface.js +4 -2
  27. package/dist/esm/audit/host-substitution-risk.d.ts +2 -0
  28. package/dist/esm/audit/host-substitution-risk.js +256 -0
  29. package/dist/esm/audit/registry.d.ts +4 -4
  30. package/dist/esm/audit/registry.js +13 -1
  31. package/dist/esm/audit/toolchain-isolation.js +1 -2
  32. package/dist/esm/audit/typescript-version.d.ts +2 -0
  33. package/dist/esm/audit/typescript-version.js +123 -0
  34. package/dist/esm/blueprint/core/parser.js +101 -2
  35. package/dist/esm/blueprint/core/schema.d.ts +1 -0
  36. package/dist/esm/blueprint/core/schema.js +26 -0
  37. package/dist/esm/blueprint/core/validation/criteria.d.ts +14 -0
  38. package/dist/esm/blueprint/core/validation/criteria.js +41 -2
  39. package/dist/esm/blueprint/core/validation/state.js +25 -14
  40. package/dist/esm/blueprint/db/migrations/run.js +4 -2
  41. package/dist/esm/blueprint/lifecycle/audit.d.ts +20 -0
  42. package/dist/esm/blueprint/lifecycle/audit.js +87 -20
  43. package/dist/esm/blueprint/lifecycle/canonicalize.d.ts +6 -0
  44. package/dist/esm/blueprint/lifecycle/canonicalize.js +135 -0
  45. package/dist/esm/blueprint/lifecycle/local.d.ts +2 -1
  46. package/dist/esm/blueprint/lifecycle/local.js +19 -3
  47. package/dist/esm/blueprint/lifecycle/review-provenance.d.ts +6 -0
  48. package/dist/esm/blueprint/lifecycle/review-provenance.js +68 -12
  49. package/dist/esm/blueprint/lifecycle/rollout-anchor.d.ts +97 -0
  50. package/dist/esm/blueprint/lifecycle/rollout-anchor.js +161 -0
  51. package/dist/esm/blueprint/service/BlueprintCreationService.d.ts +1 -0
  52. package/dist/esm/blueprint/service/BlueprintCreationService.js +53 -4
  53. package/dist/esm/blueprint/service/blueprint-tech-debt-links.js +16 -2
  54. package/dist/esm/blueprint/sync/client.d.ts +2 -1
  55. package/dist/esm/blueprint/sync/client.js +1 -1
  56. package/dist/esm/blueprint/trust/command-runner.d.ts +26 -0
  57. package/dist/esm/blueprint/trust/command-runner.js +148 -0
  58. package/dist/esm/blueprint/trust/dossier.d.ts +1 -0
  59. package/dist/esm/blueprint/trust/dossier.js +40 -6
  60. package/dist/esm/blueprint/trust/gates.d.ts +7 -0
  61. package/dist/esm/blueprint/trust/gates.js +20 -0
  62. package/dist/esm/blueprint/trust/promotion.d.ts +8 -0
  63. package/dist/esm/blueprint/trust/promotion.js +74 -112
  64. package/dist/esm/blueprint/trust/scaffold.d.ts +19 -0
  65. package/dist/esm/blueprint/trust/scaffold.js +64 -0
  66. package/dist/esm/blueprint/trust/validator.d.ts +1 -0
  67. package/dist/esm/blueprint/trust/validator.js +7 -1
  68. package/dist/esm/blueprint/utils/archive.js +27 -5
  69. package/dist/esm/blueprint/utils/document-paths.d.ts +18 -0
  70. package/dist/esm/blueprint/utils/document-paths.js +45 -12
  71. package/dist/esm/build/cli-mcp-parity.js +15 -0
  72. package/dist/esm/cli/bundle/commands/blueprint.d.ts +1 -0
  73. package/dist/esm/cli/bundle/commands/blueprint.js +1 -0
  74. package/dist/esm/cli/cli.d.ts +1 -1
  75. package/dist/esm/cli/cli.js +43 -0
  76. package/dist/esm/cli/commands/agent-launch.d.ts +8 -1
  77. package/dist/esm/cli/commands/agent-launch.js +93 -4
  78. package/dist/esm/cli/commands/audit-core.d.ts +1 -1
  79. package/dist/esm/cli/commands/audit-core.js +8 -1
  80. package/dist/esm/cli/commands/audit.js +21 -5
  81. package/dist/esm/cli/commands/blueprint/execution.js +18 -6
  82. package/dist/esm/cli/commands/blueprint/mutations.js +59 -40
  83. package/dist/esm/cli/commands/blueprint/router-dispatch.d.ts +2 -1
  84. package/dist/esm/cli/commands/blueprint/router-dispatch.js +20 -1
  85. package/dist/esm/cli/commands/blueprint/router-output.d.ts +21 -0
  86. package/dist/esm/cli/commands/blueprint/router-output.js +48 -17
  87. package/dist/esm/cli/commands/blueprint/router.d.ts +7 -0
  88. package/dist/esm/cli/commands/blueprint/router.js +13 -1
  89. package/dist/esm/cli/commands/ci-preflight.d.ts +13 -2
  90. package/dist/esm/cli/commands/ci-preflight.js +30 -6
  91. package/dist/esm/cli/commands/compile.js +5 -31
  92. package/dist/esm/cli/commands/dash/delivery-status.d.ts +1 -0
  93. package/dist/esm/cli/commands/dash/delivery-status.js +11 -4
  94. package/dist/esm/cli/commands/doctor.js +5 -0
  95. package/dist/esm/cli/commands/hook.d.ts +5 -0
  96. package/dist/esm/cli/commands/hook.js +15 -9
  97. package/dist/esm/cli/commands/init/config.js +2 -2
  98. package/dist/esm/cli/commands/init/convergence-apply.d.ts +88 -0
  99. package/dist/esm/cli/commands/init/convergence-apply.js +325 -0
  100. package/dist/esm/cli/commands/init/convergence-state.d.ts +13 -0
  101. package/dist/esm/cli/commands/init/convergence-state.js +54 -0
  102. package/dist/esm/cli/commands/init/convergence.d.ts +23 -0
  103. package/dist/esm/cli/commands/init/convergence.js +57 -0
  104. package/dist/esm/cli/commands/init/git-convergence.d.ts +19 -0
  105. package/dist/esm/cli/commands/init/git-convergence.js +168 -0
  106. package/dist/esm/cli/commands/init/gitignore-patcher.js +2 -0
  107. package/dist/esm/cli/commands/init/grok-privacy-smoke.d.ts +52 -0
  108. package/dist/esm/cli/commands/init/grok-privacy-smoke.js +91 -0
  109. package/dist/esm/cli/commands/init/host-native-proof.d.ts +18 -0
  110. package/dist/esm/cli/commands/init/host-native-proof.js +88 -0
  111. package/dist/esm/cli/commands/init/host-visibility.d.ts +2 -2
  112. package/dist/esm/cli/commands/init/host-visibility.js +94 -5
  113. package/dist/esm/cli/commands/init/index.d.ts +3 -0
  114. package/dist/esm/cli/commands/init/index.js +326 -173
  115. package/dist/esm/cli/commands/init/mcp-spec.d.ts +57 -0
  116. package/dist/esm/cli/commands/init/mcp-spec.js +273 -0
  117. package/dist/esm/cli/commands/init/merge.d.ts +1 -0
  118. package/dist/esm/cli/commands/init/merge.js +30 -1
  119. package/dist/esm/cli/commands/init/package-root.d.ts +7 -0
  120. package/dist/esm/cli/commands/init/package-root.js +10 -2
  121. package/dist/esm/cli/commands/init/plugin-cache-prune.d.ts +0 -3
  122. package/dist/esm/cli/commands/init/plugin-cache-prune.js +0 -1
  123. package/dist/esm/cli/commands/init/scaffold-agents-md.d.ts +1 -1
  124. package/dist/esm/cli/commands/init/scaffold-agents-md.js +9 -3
  125. package/dist/esm/cli/commands/init/scaffold-base-kit.d.ts +20 -0
  126. package/dist/esm/cli/commands/init/scaffold-base-kit.js +61 -5
  127. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/grok.d.ts +14 -8
  128. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/grok.js +41 -24
  129. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.d.ts +2 -0
  130. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.js +54 -24
  131. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.d.ts +12 -31
  132. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +288 -202
  133. package/dist/esm/cli/commands/init/scaffolders/file-transaction.d.ts +22 -0
  134. package/dist/esm/cli/commands/init/scaffolders/file-transaction.js +230 -0
  135. package/dist/esm/cli/commands/init/scaffolders/grok-mcp/index.d.ts +38 -0
  136. package/dist/esm/cli/commands/init/scaffolders/grok-mcp/index.js +73 -0
  137. package/dist/esm/cli/commands/init/scaffolders/opencode-plugin/index.d.ts +21 -0
  138. package/dist/esm/cli/commands/init/scaffolders/opencode-plugin/index.js +559 -63
  139. package/dist/esm/cli/commands/init/untracked-collision.d.ts +14 -0
  140. package/dist/esm/cli/commands/init/untracked-collision.js +42 -0
  141. package/dist/esm/cli/commands/opencode-probe.d.ts +107 -0
  142. package/dist/esm/cli/commands/opencode-probe.js +323 -0
  143. package/dist/esm/cli/commands/opencode-rotate.d.ts +72 -0
  144. package/dist/esm/cli/commands/opencode-rotate.js +147 -0
  145. package/dist/esm/cli/commands/public-readiness.d.ts +11 -0
  146. package/dist/esm/cli/commands/public-readiness.js +42 -0
  147. package/dist/esm/cli/commands/quality-runner.js +3 -3
  148. package/dist/esm/cli/commands/review.d.ts +35 -3
  149. package/dist/esm/cli/commands/review.js +751 -209
  150. package/dist/esm/cli/commands/run-wait.d.ts +2 -0
  151. package/dist/esm/cli/commands/run-wait.js +122 -0
  152. package/dist/esm/cli/commands/rust-check.d.ts +24 -0
  153. package/dist/esm/cli/commands/rust-check.js +110 -0
  154. package/dist/esm/cli/commands/setup-help.js +1 -1
  155. package/dist/esm/cli/commands/setup-repair-help.js +1 -1
  156. package/dist/esm/cli/commands/typecheck.d.ts +1 -0
  157. package/dist/esm/cli/commands/typecheck.js +30 -1
  158. package/dist/esm/cli/commands/worktree/router-dispatch.d.ts +6 -0
  159. package/dist/esm/cli/commands/worktree/router-dispatch.js +126 -9
  160. package/dist/esm/cli/commands/worktree/router.js +10 -0
  161. package/dist/esm/cli/direct-provider-launch.js +9 -5
  162. package/dist/esm/cli/optional-tool-freshness.d.ts +27 -0
  163. package/dist/esm/cli/optional-tool-freshness.js +219 -9
  164. package/dist/esm/daemon/domains/git-actions.d.ts +29 -0
  165. package/dist/esm/daemon/domains/git-actions.js +136 -0
  166. package/dist/esm/daemon/domains/run-registry-interop.d.ts +16 -0
  167. package/dist/esm/daemon/domains/run-registry-interop.js +45 -0
  168. package/dist/esm/daemon/domains/sessions-search.d.ts +11 -0
  169. package/dist/esm/daemon/domains/sessions-search.js +29 -0
  170. package/dist/esm/daemon/handlers/blueprints-handlers.d.ts +5 -0
  171. package/dist/esm/daemon/handlers/blueprints-handlers.js +36 -0
  172. package/dist/esm/daemon/handlers/deploy-handlers.d.ts +6 -0
  173. package/dist/esm/daemon/handlers/deploy-handlers.js +5 -0
  174. package/dist/esm/daemon/handlers/fleet-handlers.d.ts +15 -0
  175. package/dist/esm/daemon/handlers/fleet-handlers.js +91 -0
  176. package/dist/esm/daemon/handlers/git-handlers.d.ts +6 -0
  177. package/dist/esm/daemon/handlers/git-handlers.js +27 -0
  178. package/dist/esm/daemon/handlers/product.d.ts +7 -0
  179. package/dist/esm/daemon/handlers/product.js +10 -119
  180. package/dist/esm/daemon/handlers/sessions-handlers.d.ts +12 -0
  181. package/dist/esm/daemon/handlers/sessions-handlers.js +31 -0
  182. package/dist/esm/daemon/handlers/shared.d.ts +5 -0
  183. package/dist/esm/daemon/handlers/shared.js +16 -0
  184. package/dist/esm/daemon/methods.d.ts +8 -1
  185. package/dist/esm/daemon/methods.js +7 -0
  186. package/dist/esm/daemon/protocol/contract.d.ts +43 -0
  187. package/dist/esm/daemon/protocol/contract.js +26 -0
  188. package/dist/esm/daemon/protocol/index.d.ts +1 -1
  189. package/dist/esm/daemon/protocol/index.js +1 -1
  190. package/dist/esm/hooks/__conformance__/matrix.d.ts +6 -0
  191. package/dist/esm/hooks/__conformance__/matrix.js +137 -0
  192. package/dist/esm/hooks/conformance/batch.js +5 -0
  193. package/dist/esm/hooks/doctor.d.ts +5 -3
  194. package/dist/esm/hooks/doctor.js +67 -16
  195. package/dist/esm/hooks/permission-request/index.js +9 -3
  196. package/dist/esm/hooks/permission-request/policy.d.ts +9 -0
  197. package/dist/esm/hooks/permission-request/policy.js +13 -0
  198. package/dist/esm/hooks/pretool-guard/dev-routing.d.ts +9 -0
  199. package/dist/esm/hooks/pretool-guard/dev-routing.js +41 -5
  200. package/dist/esm/hooks/pretool-guard/logger.d.ts +7 -0
  201. package/dist/esm/hooks/pretool-guard/logger.js +16 -0
  202. package/dist/esm/hooks/pretool-guard/runner.d.ts +6 -0
  203. package/dist/esm/hooks/pretool-guard/runner.js +68 -16
  204. package/dist/esm/hooks/pretool-guard/validators/blueprint.js +30 -10
  205. package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +54 -0
  206. package/dist/esm/hooks/pretool-guard/validators/index.d.ts +7 -0
  207. package/dist/esm/hooks/pretool-guard/validators/index.js +21 -0
  208. package/dist/esm/hooks/pretool-guard/validators/path-contract.d.ts +19 -0
  209. package/dist/esm/hooks/pretool-guard/validators/path-contract.js +25 -8
  210. package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.js +6 -1
  211. package/dist/esm/hooks/shared/guard-outcome.d.ts +149 -0
  212. package/dist/esm/hooks/shared/guard-outcome.js +215 -0
  213. package/dist/esm/hooks/shared/hook-bootstrap.d.ts +26 -0
  214. package/dist/esm/hooks/shared/hook-bootstrap.js +64 -1
  215. package/dist/esm/hooks/shared/types.d.ts +8 -5
  216. package/dist/esm/hooks/shared/types.js +50 -9
  217. package/dist/esm/hooks/stop/qa-changed-files.d.ts +11 -0
  218. package/dist/esm/hooks/stop/qa-changed-files.js +92 -0
  219. package/dist/esm/mcp/blueprint/_shared/lifecycle.js +14 -1
  220. package/dist/esm/mcp/blueprint/handlers/document-mutations.d.ts +5 -0
  221. package/dist/esm/mcp/blueprint/handlers/document-mutations.js +109 -11
  222. package/dist/esm/mcp/blueprint/handlers/review-log.js +8 -1
  223. package/dist/esm/mcp/blueprint/handlers/task-advance.js +113 -54
  224. package/dist/esm/mcp/blueprint/handlers/task-verify.js +39 -19
  225. package/dist/esm/mcp/blueprint/registration.js +3 -2
  226. package/dist/esm/mcp/server.d.ts +1 -0
  227. package/dist/esm/mcp/server.js +27 -0
  228. package/dist/esm/mcp/tools/_names.d.ts +1 -1
  229. package/dist/esm/mcp/tools/_names.js +5 -0
  230. package/dist/esm/mcp/tools/_registry.js +10 -0
  231. package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
  232. package/dist/esm/mcp/tools/_shared/result.js +44 -1
  233. package/dist/esm/mcp/tools/audit.d.ts +5 -1
  234. package/dist/esm/mcp/tools/audit.js +42 -0
  235. package/dist/esm/mcp/tools/audits.d.ts +4 -0
  236. package/dist/esm/mcp/tools/ci-preflight.d.ts +50 -0
  237. package/dist/esm/mcp/tools/ci-preflight.js +183 -0
  238. package/dist/esm/mcp/tools/pr-upsert.js +39 -1
  239. package/dist/esm/mcp/tools/review-run.d.ts +28 -0
  240. package/dist/esm/mcp/tools/review-run.js +216 -0
  241. package/dist/esm/mcp/tools/run-wait.d.ts +86 -0
  242. package/dist/esm/mcp/tools/run-wait.js +593 -0
  243. package/dist/esm/mcp/tools/session-fetch-and-index.d.ts +1 -1
  244. package/dist/esm/mcp/tools/session-id.d.ts +3 -0
  245. package/dist/esm/mcp/tools/session-id.js +45 -0
  246. package/dist/esm/mcp/tools/session-info.d.ts +3 -0
  247. package/dist/esm/mcp/tools/session-info.js +49 -0
  248. package/dist/esm/output-transforms/audit.d.ts +23 -0
  249. package/dist/esm/output-transforms/audit.js +47 -0
  250. package/dist/esm/output-transforms/index.js +2 -0
  251. package/dist/esm/package.json +2 -0
  252. package/dist/esm/platform/client.d.ts +7 -1
  253. package/dist/esm/platform/github-contract-source.d.ts +2 -1
  254. package/dist/esm/pr-description/contract.js +24 -4
  255. package/dist/esm/review/authority.js +27 -14
  256. package/dist/esm/review/availability.d.ts +33 -0
  257. package/dist/esm/review/availability.js +122 -20
  258. package/dist/esm/review/delivery-verifier.d.ts +27 -0
  259. package/dist/esm/review/delivery-verifier.js +115 -0
  260. package/dist/esm/review/events.d.ts +3 -2
  261. package/dist/esm/review/events.js +23 -3
  262. package/dist/esm/review/execution/adapters.d.ts +11 -1
  263. package/dist/esm/review/execution/adapters.js +36 -10
  264. package/dist/esm/review/execution/artifacts.d.ts +1 -1
  265. package/dist/esm/review/execution/output-policy.d.ts +2 -0
  266. package/dist/esm/review/execution/output-policy.js +12 -0
  267. package/dist/esm/review/execution/review-checkout.js +121 -21
  268. package/dist/esm/review/execution/supervisor.js +36 -4
  269. package/dist/esm/review/execution/types.d.ts +9 -0
  270. package/dist/esm/review/opencode-account.d.ts +24 -0
  271. package/dist/esm/review/opencode-account.js +68 -0
  272. package/dist/esm/review/opencode-auth.d.ts +33 -0
  273. package/dist/esm/review/opencode-auth.js +79 -0
  274. package/dist/esm/review/subject.d.ts +25 -0
  275. package/dist/esm/review/subject.js +116 -26
  276. package/dist/esm/runtime/opencode-account-materializer.d.ts +34 -0
  277. package/dist/esm/runtime/opencode-account-materializer.js +33 -0
  278. package/dist/esm/rust/affected-manifests.d.ts +38 -0
  279. package/dist/esm/rust/affected-manifests.js +69 -0
  280. package/dist/esm/session-memory/fetch-index.d.ts +1 -1
  281. package/dist/esm/status/snapshot.js +32 -25
  282. package/dist/esm/test/duration-sequencer.d.ts +20 -0
  283. package/dist/esm/test/duration-sequencer.js +54 -0
  284. package/dist/esm/test/shard-durations.json +922 -0
  285. package/dist/esm/test/worker-budget.d.ts +6 -0
  286. package/dist/esm/test/worker-budget.js +59 -1
  287. package/dist/esm/test-helpers/global-setup.d.ts +1 -2
  288. package/dist/esm/test-helpers/global-setup.js +79 -32
  289. package/dist/esm/typecheck/planner.d.ts +9 -1
  290. package/dist/esm/typecheck/planner.js +31 -0
  291. package/dist/esm/ultragoal/service.d.ts +7 -0
  292. package/dist/esm/ultragoal/service.js +42 -2
  293. package/dist/esm/utils/write-json-file.js +3 -1
  294. package/dist/esm/worktrees/codex-project-trust.d.ts +15 -0
  295. package/dist/esm/worktrees/codex-project-trust.js +56 -0
  296. package/dist/esm/worktrees/orphan-scan.d.ts +50 -0
  297. package/dist/esm/worktrees/orphan-scan.js +211 -0
  298. package/dist/esm/worktrees/registry.d.ts +8 -0
  299. package/dist/esm/worktrees/registry.js +18 -3
  300. package/package.json +18 -13
@@ -18,15 +18,17 @@
18
18
  * provisioned, exact-version binaries with environment-variable names
19
19
  * forwarded through Codex's stdio `env_vars` allow-list.
20
20
  */
21
- import { execFileSync, spawnSync } from "node:child_process";
22
- import { accessSync, constants, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
21
+ import { spawnSync } from "node:child_process";
22
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
23
23
  import { homedir } from "node:os";
24
24
  import { dirname, join } from "node:path";
25
- import { fileURLToPath } from "node:url";
26
- import lockfile from "proper-lockfile";
25
+ import { agentKitMcpLaunchCommand, findWebpressoMcpEntry, resolveExecutableOnPath, resolveWebpressoMcpEntry, WEBPRESSO_MCP_SERVER_NAME, WEBPRESSO_MCP_SPEC, } from "#cli/commands/init/mcp-spec.js";
26
+ // Re-exported so existing importers (tests) keep a stable path; the canonical
27
+ // implementation now lives in `mcp-spec.ts`.
28
+ export { findWebpressoMcpEntry, WEBPRESSO_MCP_SERVER_NAME };
29
+ import { updateScaffolderTextFile } from "#cli/commands/init/scaffolders/file-transaction.js";
27
30
  import { isJsonRecord, isUnsafeRuntimeLauncher, parseJson, parseProjectMcpServers, parseTomlString, } from "#hooks/host-config-contract.js";
28
31
  import { appendGlobalCapableVpArgs, resolveGlobalCapableVpCommand, } from "#cli/global-vp.js";
29
- import { pathCandidates } from "#runtime/command-exists.js";
30
32
  import { writeFileAtomic } from "#shared-utils/write-json-file.js";
31
33
  export const PLAYWRIGHT_MCP_SERVER_NAME = "playwright";
32
34
  export const PLAYWRIGHT_MCP_VERSION = "0.0.78";
@@ -42,56 +44,8 @@ const PLAYWRIGHT_STDIO_MCP_DEFINITION = {
42
44
  command: "vp",
43
45
  args: ["dlx", PLAYWRIGHT_MCP_PACKAGE, ...PLAYWRIGHT_MCP_ARGS],
44
46
  };
45
- const CODEX_CONFIG_LOCK_TIMEOUT_MS = 5_000;
46
- const CODEX_CONFIG_LOCK_POLL_MS = 25;
47
- const CODEX_CONFIG_LOCK_STALE_MS = 30_000;
48
- function sleepSync(ms) {
49
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
50
- }
51
- function acquireCodexConfigLock(configPath) {
52
- mkdirSync(dirname(configPath), { recursive: true });
53
- const startedAt = Date.now();
54
- while (true) {
55
- try {
56
- return lockfile.lockSync(configPath, {
57
- realpath: false,
58
- stale: CODEX_CONFIG_LOCK_STALE_MS,
59
- update: CODEX_CONFIG_LOCK_STALE_MS / 2,
60
- });
61
- }
62
- catch (error) {
63
- if (error.code !== "ELOCKED")
64
- throw error;
65
- if (Date.now() - startedAt >= CODEX_CONFIG_LOCK_TIMEOUT_MS) {
66
- throw new Error(`Timed out waiting for Codex config lock: ${configPath}`, {
67
- cause: error,
68
- });
69
- }
70
- sleepSync(CODEX_CONFIG_LOCK_POLL_MS);
71
- }
72
- }
73
- }
74
- function withCodexConfigLock(configPath, action) {
75
- const release = acquireCodexConfigLock(configPath);
76
- try {
77
- return action();
78
- }
79
- finally {
80
- release();
81
- }
82
- }
83
- function readCodexConfig(configPath) {
84
- return existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
85
- }
86
47
  function updateCodexConfig(configPath, transform) {
87
- return withCodexConfigLock(configPath, () => {
88
- const existing = readCodexConfig(configPath);
89
- const next = transform(existing);
90
- if (next === existing)
91
- return "unchanged";
92
- writeFileAtomic(configPath, next, "utf8");
93
- return "written";
94
- });
48
+ return updateScaffolderTextFile(configPath, transform);
95
49
  }
96
50
  function tomlString(value) {
97
51
  return JSON.stringify(value);
@@ -118,15 +72,254 @@ export function commandMcpCodexBlock(definition) {
118
72
  if (definition.codexEnvVars && definition.codexEnvVars.length > 0) {
119
73
  lines.push(`env_vars = ${tomlStringArray(definition.codexEnvVars)}`);
120
74
  }
75
+ if (definition.codexEnv && Object.keys(definition.codexEnv).length > 0) {
76
+ const pairs = Object.entries(definition.codexEnv).map(([key, value]) => `${key} = ${tomlString(value)}`);
77
+ lines.push(`env = { ${pairs.join(", ")} }`);
78
+ }
121
79
  lines.push("enabled = true");
122
80
  for (const [key, value] of Object.entries(definition.codexFields ?? {})) {
123
81
  lines.push(`${key} = ${tomlValue(value)}`);
124
82
  }
125
83
  return `${lines.join("\n")}\n`;
126
84
  }
85
+ function parseTomlTableHeader(line) {
86
+ const match = line.match(/^\s*\[\s*([\s\S]*?)\s*\]\s*(?:#.*)?$/u);
87
+ if (!match || line.trimStart().startsWith("[["))
88
+ return null;
89
+ return parseTomlDottedKey(match[1] ?? "");
90
+ }
91
+ function parseTomlArrayTableHeader(line) {
92
+ const match = line.match(/^\s*\[\[\s*([\s\S]*?)\s*\]\]\s*(?:#.*)?$/u);
93
+ if (!match)
94
+ return null;
95
+ return parseTomlDottedKey(match[1] ?? "");
96
+ }
97
+ function parseTomlDottedKey(raw) {
98
+ const segments = [];
99
+ let i = 0;
100
+ const skipSpace = () => {
101
+ while (/\s/u.test(raw[i] ?? ""))
102
+ i += 1;
103
+ };
104
+ while (i < raw.length) {
105
+ skipSpace();
106
+ const segment = parseTomlKeySegment(raw, i);
107
+ if (!segment)
108
+ return null;
109
+ segments.push(segment.value);
110
+ i = segment.next;
111
+ skipSpace();
112
+ if (i >= raw.length)
113
+ break;
114
+ if (raw[i] !== ".")
115
+ return null;
116
+ i += 1;
117
+ }
118
+ return segments.length > 0 ? segments : null;
119
+ }
120
+ function parseTomlKeySegment(raw, start) {
121
+ const first = raw[start];
122
+ if (first === "'") {
123
+ const end = raw.indexOf("'", start + 1);
124
+ if (end === -1)
125
+ return null;
126
+ return { value: raw.slice(start + 1, end), next: end + 1 };
127
+ }
128
+ if (first === '"') {
129
+ return parseBasicTomlKeySegment(raw, start);
130
+ }
131
+ let end = start;
132
+ while (/[A-Za-z0-9_-]/u.test(raw[end] ?? ""))
133
+ end += 1;
134
+ if (end === start)
135
+ return null;
136
+ return { value: raw.slice(start, end), next: end };
137
+ }
138
+ function parseBasicTomlKeySegment(raw, start) {
139
+ let value = "";
140
+ for (let i = start + 1; i < raw.length; i += 1) {
141
+ const ch = raw[i];
142
+ if (ch === '"')
143
+ return { value, next: i + 1 };
144
+ if (ch !== "\\") {
145
+ value += ch;
146
+ continue;
147
+ }
148
+ const escaped = raw[i + 1];
149
+ if (escaped === undefined)
150
+ return null;
151
+ const simple = {
152
+ b: "\b",
153
+ t: "\t",
154
+ n: "\n",
155
+ f: "\f",
156
+ r: "\r",
157
+ '"': '"',
158
+ "\\": "\\",
159
+ };
160
+ if (escaped in simple) {
161
+ value += simple[escaped];
162
+ i += 1;
163
+ continue;
164
+ }
165
+ if (escaped === "u" || escaped === "U") {
166
+ const width = escaped === "u" ? 4 : 8;
167
+ const hex = raw.slice(i + 2, i + 2 + width);
168
+ if (!new RegExp(`^[0-9A-Fa-f]{${width}}$`, "u").test(hex))
169
+ return null;
170
+ value += String.fromCodePoint(Number.parseInt(hex, 16));
171
+ i += 1 + width;
172
+ continue;
173
+ }
174
+ return null;
175
+ }
176
+ return null;
177
+ }
178
+ function isServerTableHeader(line, serverId) {
179
+ const segments = parseTomlTableHeader(line);
180
+ return segments?.length === 2 && segments[0] === "mcp_servers" && segments[1] === serverId;
181
+ }
127
182
  function isOwnedSubTableHeader(line, serverId) {
128
- const trimmed = line.trim();
129
- return trimmed.startsWith(`[mcp_servers.${serverId}.`) && trimmed.endsWith("]");
183
+ const segments = parseTomlTableHeader(line);
184
+ return (segments !== null &&
185
+ segments.length > 2 &&
186
+ segments[0] === "mcp_servers" &&
187
+ segments[1] === serverId);
188
+ }
189
+ function isOwnedServerOrDescendantTableHeader(line, serverId) {
190
+ const segments = parseTomlArrayTableHeader(line) ?? parseTomlTableHeader(line);
191
+ return (segments !== null &&
192
+ segments.length >= 2 &&
193
+ segments[0] === "mcp_servers" &&
194
+ segments[1] === serverId);
195
+ }
196
+ function tomlLinesStartingInsideMultilineString(lines) {
197
+ const insideAtStart = [];
198
+ let delimiter = null;
199
+ for (const line of lines) {
200
+ insideAtStart.push(delimiter !== null);
201
+ for (let i = 0; i < line.length; i += 1) {
202
+ if (delimiter !== null) {
203
+ const end = findTomlMultilineStringEnd(line, i, delimiter);
204
+ if (end === -1)
205
+ break;
206
+ i = end + delimiter.length - 1;
207
+ delimiter = null;
208
+ continue;
209
+ }
210
+ const ch = line[i];
211
+ if (ch === "#")
212
+ break;
213
+ if (line.startsWith('"""', i) || line.startsWith("'''", i)) {
214
+ delimiter = line.startsWith('"""', i) ? '"""' : "'''";
215
+ i += delimiter.length - 1;
216
+ continue;
217
+ }
218
+ if (ch === '"') {
219
+ i += 1;
220
+ while (i < line.length) {
221
+ const inner = line[i];
222
+ if (inner === "\\") {
223
+ i += 2;
224
+ continue;
225
+ }
226
+ if (inner === '"')
227
+ break;
228
+ i += 1;
229
+ }
230
+ continue;
231
+ }
232
+ if (ch === "'") {
233
+ const end = line.indexOf("'", i + 1);
234
+ if (end === -1)
235
+ break;
236
+ i = end;
237
+ }
238
+ }
239
+ }
240
+ return insideAtStart;
241
+ }
242
+ function findTomlMultilineStringEnd(line, start, delimiter) {
243
+ if (delimiter === "'''")
244
+ return line.indexOf(delimiter, start);
245
+ for (let i = start; i < line.length; i += 1) {
246
+ if (!line.startsWith(delimiter, i))
247
+ continue;
248
+ let backslashes = 0;
249
+ for (let j = i - 1; j >= 0 && line[j] === "\\"; j -= 1)
250
+ backslashes += 1;
251
+ if (backslashes % 2 === 0)
252
+ return i;
253
+ }
254
+ return -1;
255
+ }
256
+ function tomlAssignmentKey(line) {
257
+ let quote = null;
258
+ let escaped = false;
259
+ for (let i = 0; i < line.length; i += 1) {
260
+ const ch = line[i];
261
+ if (quote !== null) {
262
+ if (quote === '"' && escaped) {
263
+ escaped = false;
264
+ }
265
+ else if (quote === '"' && ch === "\\") {
266
+ escaped = true;
267
+ }
268
+ else if (ch === quote) {
269
+ quote = null;
270
+ }
271
+ continue;
272
+ }
273
+ if (ch === '"' || ch === "'") {
274
+ quote = ch;
275
+ continue;
276
+ }
277
+ if (ch === "#")
278
+ return null;
279
+ if (ch === "=")
280
+ return parseTomlDottedKey(line.slice(0, i).trim());
281
+ }
282
+ return null;
283
+ }
284
+ function hasDottedServerDefinition(lines, startsInsideMultiline, serverId) {
285
+ let currentTable = null;
286
+ return lines.some((line, index) => {
287
+ if (startsInsideMultiline[index])
288
+ return false;
289
+ const arrayTable = parseTomlArrayTableHeader(line);
290
+ if (arrayTable !== null &&
291
+ arrayTable.length >= 2 &&
292
+ arrayTable[0] === "mcp_servers" &&
293
+ arrayTable[1] === serverId) {
294
+ return true;
295
+ }
296
+ const table = parseTomlTableHeader(line);
297
+ if (table !== null) {
298
+ currentTable = table;
299
+ return false;
300
+ }
301
+ const segments = tomlAssignmentKey(line);
302
+ return (segments !== null &&
303
+ ((segments.length >= 2 && segments[0] === "mcp_servers" && segments[1] === serverId) ||
304
+ (currentTable?.length === 1 &&
305
+ currentTable[0] === "mcp_servers" &&
306
+ segments[0] === serverId)));
307
+ });
308
+ }
309
+ export function hasAmbiguousMcpServerDottedDefinition(raw, serverId) {
310
+ const lines = raw.trimEnd().split(/\r?\n/);
311
+ const startsInsideMultiline = tomlLinesStartingInsideMultilineString(lines);
312
+ const start = lines.findIndex((line, index) => !startsInsideMultiline[index] && isServerTableHeader(line, serverId));
313
+ return start === -1 && hasDottedServerDefinition(lines, startsInsideMultiline, serverId);
314
+ }
315
+ export function hasManagedMcpServerTableConflict(raw, serverId) {
316
+ const lines = raw.trimEnd().split(/\r?\n/);
317
+ const startsInsideMultiline = tomlLinesStartingInsideMultilineString(lines);
318
+ const start = lines.findIndex((line, index) => !startsInsideMultiline[index] && isServerTableHeader(line, serverId));
319
+ if (start === -1)
320
+ return hasDottedServerDefinition(lines, startsInsideMultiline, serverId);
321
+ const end = managedSectionEnd(lines, startsInsideMultiline, start, serverId);
322
+ return hasOwnedServerTableOutsideManagedSection(lines, startsInsideMultiline, start, end, serverId);
130
323
  }
131
324
  /**
132
325
  * Find the exclusive end index of the managed section region: the parent block
@@ -136,9 +329,11 @@ function isOwnedSubTableHeader(line, serverId) {
136
329
  * sub-tables under foreign or absent parents are never touched (they remain
137
330
  * doctor-report-only territory).
138
331
  */
139
- function managedSectionEnd(lines, start, serverId) {
332
+ function managedSectionEnd(lines, startsInsideMultiline, start, serverId) {
140
333
  let end = lines.length;
141
334
  for (let i = start + 1; i < lines.length; i += 1) {
335
+ if (startsInsideMultiline[i])
336
+ continue;
142
337
  if (!lines[i].trim().startsWith("["))
143
338
  continue;
144
339
  if (isOwnedSubTableHeader(lines[i], serverId))
@@ -146,20 +341,43 @@ function managedSectionEnd(lines, start, serverId) {
146
341
  end = i;
147
342
  break;
148
343
  }
344
+ // Trailing comment/blank lines between the managed block and the next table
345
+ // belong to the surrounding document (often annotating the *next* section) —
346
+ // leave them in place rather than swallowing them into the regenerated block.
347
+ while (end > start + 1) {
348
+ const previous = lines[end - 1].trim();
349
+ if (previous.length === 0 || previous.startsWith("#")) {
350
+ end -= 1;
351
+ continue;
352
+ }
353
+ break;
354
+ }
149
355
  return end;
150
356
  }
151
- function upsertMcpServerBlock(raw, serverId, block) {
152
- const header = commandMcpHeader(serverId);
357
+ function hasOwnedServerTableOutsideManagedSection(lines, startsInsideMultiline, start, end, serverId) {
358
+ return lines.some((line, index) => !startsInsideMultiline[index] &&
359
+ (index < start || index >= end) &&
360
+ isOwnedServerOrDescendantTableHeader(line, serverId));
361
+ }
362
+ export function upsertMcpServerBlock(raw, serverId, block) {
153
363
  const lines = raw.trimEnd().split(/\r?\n/);
154
364
  const hasContent = raw.trim().length > 0;
155
- const start = lines.findIndex((line) => line.trim() === header);
365
+ const startsInsideMultiline = tomlLinesStartingInsideMultilineString(lines);
366
+ const start = lines.findIndex((line, index) => !startsInsideMultiline[index] && isServerTableHeader(line, serverId));
156
367
  if (start === -1) {
368
+ if (hasDottedServerDefinition(lines, startsInsideMultiline, serverId))
369
+ return raw;
157
370
  const prefix = hasContent ? `${raw.trimEnd()}\n\n` : "";
158
371
  return `${prefix}${block}`;
159
372
  }
160
- const end = managedSectionEnd(lines, start, serverId);
373
+ const end = managedSectionEnd(lines, startsInsideMultiline, start, serverId);
374
+ if (hasOwnedServerTableOutsideManagedSection(lines, startsInsideMultiline, start, end, serverId)) {
375
+ return raw;
376
+ }
161
377
  const replacement = block.trimEnd().split("\n");
162
- if (end < lines.length)
378
+ // Separate the block from a preserved tail — unless the tail already starts
379
+ // with its own blank line (kept by the trailing-comment walk-back).
380
+ if (end < lines.length && lines[end].trim().length !== 0)
163
381
  replacement.push("");
164
382
  return [...lines.slice(0, start), ...replacement, ...lines.slice(end)].join("\n") + "\n";
165
383
  }
@@ -176,31 +394,11 @@ export function commandMcpClaudeServer(definition) {
176
394
  }
177
395
  return server;
178
396
  }
179
- export const WEBPRESSO_MCP_SERVER_NAME = "webpresso";
180
397
  export const WEBPRESSO_MCP_HEADER = `[mcp_servers.${WEBPRESSO_MCP_SERVER_NAME}]`;
181
398
  function defaultConfigPath() {
182
399
  const codexHome = process.env.CODEX_HOME || join(process.env.HOME || homedir(), ".codex");
183
400
  return join(codexHome, "config.toml");
184
401
  }
185
- function resolvePinnedMcpExecutable(binary, env) {
186
- for (const candidate of pathCandidates(binary, {
187
- pathEnv: env.PATH,
188
- pathExtEnv: env.PATHEXT,
189
- })) {
190
- try {
191
- if (!statSync(candidate).isFile())
192
- continue;
193
- if (process.platform !== "win32")
194
- accessSync(candidate, constants.X_OK);
195
- return candidate;
196
- }
197
- catch {
198
- // Continue scanning PATH. Returning the candidate, not its realpath, keeps
199
- // the generated config anchored to Vite+'s stable global-bin symlink.
200
- }
201
- }
202
- return null;
203
- }
204
402
  function installedGlobalPackageMetadata(vpCommand, packageName, spawn) {
205
403
  const listCommand = appendGlobalCapableVpArgs(vpCommand, [
206
404
  "list",
@@ -250,7 +448,7 @@ function selectPackageBinary(definition, metadata) {
250
448
  function provisionPinnedCodexMcp(input, definition) {
251
449
  const env = input.env ?? process.env;
252
450
  const spawn = input.spawn ?? spawnSync;
253
- const resolveExecutable = input.resolveExecutable ?? ((binary) => resolvePinnedMcpExecutable(binary, env));
451
+ const resolveExecutable = input.resolveExecutable ?? ((binary) => resolveExecutableOnPath(binary, env));
254
452
  const vpCommand = input.resolveVpCommand !== undefined
255
453
  ? input.resolveVpCommand()
256
454
  : resolveGlobalCapableVpCommand(env.PATH ?? "", process.platform, env);
@@ -462,130 +660,13 @@ export function ensureClaudePlaywrightMcp(input) {
462
660
  // ────────────────────────────────────────────────────────────────────────────
463
661
  // Agent-kit MCP server registration
464
662
  // ────────────────────────────────────────────────────────────────────────────
465
- const WP_BIN_RELATIVE = join("bin", process.platform === "win32" ? "wp.cmd" : "wp");
466
- const WP_LAUNCHER_REQUIRED_RELATIVES = [
467
- WP_BIN_RELATIVE,
468
- join("bin", "_run.js"),
469
- join("bin", "runtime-lanes.js"),
470
- join("bin", "runtime-manifest.json"),
471
- ];
472
- /**
473
- * Resolve the absolute path to webpresso's global `bin/wp` launcher on this machine. Probes
474
- * the locations consumers use to install webpresso, in order of stability:
475
- *
476
- * 1. the stable global `wp` shim available on PATH
477
- * 2. the currently executing `@webpresso/agent-kit` package
478
- * 3. Claude plugin install — `~/.claude/plugins/cache/.../agent-kit/`
479
- * (path-stable; updated by Claude Code's plugin manager)
480
- * 4. bun global — `~/.bun/install/global/node_modules/@webpresso/agent-kit/`
481
- * 5. pnpm global — `$(pnpm root -g)/@webpresso/agent-kit/`
482
- * 6. npm global — `$(npm root -g)/@webpresso/agent-kit/`
483
- *
484
- * Returns `null` when none of the candidates contain `bin/wp`. The caller
485
- * surfaces a clear error rather than writing a broken codex config.
486
- */
487
- export function findWebpressoMcpEntry(probe = {}) {
488
- return resolveWebpressoMcpEntry(probe).entryPath;
489
- }
490
- function isUsableWebpressoMcpRoot(root) {
491
- return WP_LAUNCHER_REQUIRED_RELATIVES.every((relativePath) => existsSync(join(root, relativePath)));
492
- }
493
- function resolveWebpressoMcpEntry(probe = {}) {
494
- const checked = [];
495
- if (probe.candidates === undefined) {
496
- const stableEntry = probe.stableEntry !== undefined
497
- ? probe.stableEntry()
498
- : resolvePinnedMcpExecutable("wp", process.env);
499
- if (stableEntry)
500
- return { entryPath: stableEntry, checked: [stableEntry] };
501
- }
502
- const candidateGroups = probe.candidates !== undefined ? [probe.candidates] : [defaultFixedCandidates()];
503
- for (const candidates of candidateGroups) {
504
- for (const root of candidates) {
505
- if (!root)
506
- continue;
507
- checked.push(root);
508
- const wpBin = join(root, WP_BIN_RELATIVE);
509
- if (isUsableWebpressoMcpRoot(root))
510
- return { entryPath: wpBin, checked };
511
- }
512
- }
513
- if (probe.candidates === undefined) {
514
- for (const root of defaultPackageManagerCandidates(probe)) {
515
- if (!root)
516
- continue;
517
- checked.push(root);
518
- const wpBin = join(root, WP_BIN_RELATIVE);
519
- if (isUsableWebpressoMcpRoot(root))
520
- return { entryPath: wpBin, checked };
521
- }
522
- }
523
- return { entryPath: null, checked };
524
- }
525
- function defaultFixedCandidates() {
526
- const home = process.env.HOME || homedir();
527
- const packageRoot = currentAgentKitPackageRoot();
528
- const claudePlugin = join(home, ".claude", "plugins", "cache", "webpresso", "agent-kit");
529
- const bunGlobal = join(home, ".bun", "install", "global", "node_modules", "@webpresso", "agent-kit");
530
- return [packageRoot ?? "", claudePlugin, bunGlobal];
531
- }
532
- function defaultPackageManagerCandidates(probe) {
533
- const pnpmRoot = (probe.pnpmGlobalRoot ?? probePnpmGlobalRoot)();
534
- const npmRoot = (probe.npmGlobalRoot ?? probeNpmGlobalRoot)();
535
- return [
536
- pnpmRoot ? join(pnpmRoot, "@webpresso", "agent-kit") : "",
537
- npmRoot ? join(npmRoot, "@webpresso", "agent-kit") : "",
538
- ];
539
- }
540
- function currentAgentKitPackageRoot() {
541
- let current = dirname(fileURLToPath(import.meta.url));
542
- for (let depth = 0; depth < 12; depth += 1) {
543
- const packagePath = join(current, "package.json");
544
- if (existsSync(packagePath)) {
545
- try {
546
- const pkg = JSON.parse(readFileSync(packagePath, "utf8"));
547
- if (pkg.name === "@webpresso/agent-kit")
548
- return current;
549
- }
550
- catch {
551
- return null;
552
- }
553
- }
554
- const parent = dirname(current);
555
- if (parent === current)
556
- break;
557
- current = parent;
558
- }
559
- return null;
560
- }
561
- function probePnpmGlobalRoot() {
562
- return runQuiet("pnpm", ["root", "-g"]);
563
- }
564
- function probeNpmGlobalRoot() {
565
- return runQuiet("npm", ["root", "-g"]);
566
- }
567
- function runQuiet(cmd, args) {
568
- try {
569
- const output = execFileSync(cmd, [...args], {
570
- encoding: "utf8",
571
- stdio: ["ignore", "pipe", "ignore"],
572
- });
573
- const trimmed = output.trim();
574
- return trimmed.length > 0 ? trimmed : null;
575
- }
576
- catch {
577
- return null;
578
- }
579
- }
580
- export function agentKitMcpLaunchCommand(entryPath) {
581
- return { command: entryPath, args: ["mcp"] };
582
- }
583
663
  export function agentKitMcpBlock(entryPath) {
584
664
  const launch = agentKitMcpLaunchCommand(entryPath);
585
665
  return commandMcpCodexBlock({
586
666
  id: WEBPRESSO_MCP_SERVER_NAME,
587
667
  command: launch.command,
588
668
  args: launch.args,
669
+ codexEnv: WEBPRESSO_MCP_SPEC.env,
589
670
  });
590
671
  }
591
672
  export function upsertWebpressoMcpServer(raw, entryPath) {
@@ -602,7 +683,12 @@ export function ensureCodexWebpressoMcp(input) {
602
683
  const checked = resolution?.checked ?? [];
603
684
  return { kind: "codex-webpresso-mcp-not-installed", path: configPath, checked };
604
685
  }
605
- const result = updateCodexConfig(configPath, (existing) => upsertWebpressoMcpServer(existing, entryPath));
686
+ const existing = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
687
+ if (hasAmbiguousMcpServerDottedDefinition(existing, WEBPRESSO_MCP_SERVER_NAME) ||
688
+ hasManagedMcpServerTableConflict(existing, WEBPRESSO_MCP_SERVER_NAME)) {
689
+ return { kind: "codex-webpresso-mcp-ambiguous", path: configPath, entryPath };
690
+ }
691
+ const result = updateCodexConfig(configPath, (raw) => upsertWebpressoMcpServer(raw, entryPath));
606
692
  if (result === "unchanged") {
607
693
  return { kind: "codex-webpresso-mcp-unchanged", path: configPath, entryPath };
608
694
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Test-only seams following the `claimInstallLock` options precedent. `isAlive`
3
+ * and `now` make liveness/age deterministic; `onBeforeReclaimDelete` fires while
4
+ * the reaper is held, just before a stale lock is removed, so a test can prove
5
+ * reclaim is serialized. Never supplied in production.
6
+ */
7
+ export interface ScaffolderLockSeams {
8
+ readonly isAlive?: (pid: number) => boolean;
9
+ readonly now?: () => number;
10
+ readonly onBeforeReclaimDelete?: () => void;
11
+ }
12
+ /**
13
+ * Serialize a complete read/modify/write transaction for a host config file.
14
+ * These are local single-machine home-config locks (`~/.codex`, `~/.grok`), so
15
+ * owner-pid liveness is meaningful; this is not a shared-filesystem lock.
16
+ */
17
+ export declare function withScaffolderFileLock<T>(path: string, action: () => T, seams?: ScaffolderLockSeams): T;
18
+ /**
19
+ * Serialize a text-file read/modify/write and install the replacement strictly
20
+ * atomically without weakening an existing file's permissions.
21
+ */
22
+ export declare function updateScaffolderTextFile(path: string, transform: (existing: string) => string): "written" | "unchanged";