@webpresso/agent-kit 3.3.2 → 3.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/THIRD-PARTY-NOTICES.md +12 -10
  2. package/catalog/AGENTS.md.tpl +3 -5
  3. package/catalog/agent/rules/changeset-release.md +15 -4
  4. package/catalog/agent/rules/managed-tool-binaries.md +67 -0
  5. package/catalog/agent/rules/parallel-orchestration-quota.md +99 -1
  6. package/catalog/agent/rules/pre-implementation.md +36 -20
  7. package/catalog/agent/rules/rtk-routing.md +22 -16
  8. package/catalog/agent/skills/autopilot/SKILL.md +4 -3
  9. package/catalog/agent/skills/ultragoal/SKILL.md +11 -9
  10. package/catalog/agent/skills/verify/SKILL.md +12 -12
  11. package/catalog/compose/admin-blocks-registry.json +659 -0
  12. package/dist/esm/audit/hook-surface.js +5 -1
  13. package/dist/esm/audit/repo-guardrails.d.ts +8 -0
  14. package/dist/esm/audit/repo-guardrails.js +71 -1
  15. package/dist/esm/blueprint/core/parser.js +40 -16
  16. package/dist/esm/blueprint/core/validation/criteria.js +2 -1
  17. package/dist/esm/blueprint/execution/artifacts.js +8 -4
  18. package/dist/esm/blueprint/execution/frontmatter-keys.d.ts +17 -0
  19. package/dist/esm/blueprint/execution/frontmatter-keys.js +24 -0
  20. package/dist/esm/blueprint/execution/metadata.js +8 -4
  21. package/dist/esm/blueprint/lifecycle/audit.d.ts +21 -0
  22. package/dist/esm/blueprint/lifecycle/audit.js +89 -3
  23. package/dist/esm/blueprint/lifecycle/review-provenance.js +148 -40
  24. package/dist/esm/blueprint/lifecycle/rollout-anchor.d.ts +22 -0
  25. package/dist/esm/blueprint/lifecycle/rollout-anchor.js +37 -1
  26. package/dist/esm/blueprint/markdown/blocked-line.d.ts +49 -0
  27. package/dist/esm/blueprint/markdown/blocked-line.js +63 -0
  28. package/dist/esm/blueprint/markdown/helpers.js +10 -4
  29. package/dist/esm/blueprint/tracked-document/parser.js +3 -3
  30. package/dist/esm/blueprint/trust/gate-row.d.ts +55 -0
  31. package/dist/esm/blueprint/trust/gate-row.js +113 -0
  32. package/dist/esm/blueprint/trust/promotion.d.ts +17 -0
  33. package/dist/esm/blueprint/trust/promotion.js +19 -19
  34. package/dist/esm/build/atomic-file-copy.d.ts +32 -0
  35. package/dist/esm/build/atomic-file-copy.js +44 -0
  36. package/dist/esm/build/cli-mcp-parity.js +3 -2
  37. package/dist/esm/build/package-manifest.js +7 -2
  38. package/dist/esm/build/vendor-oxlint-plugins.d.ts +8 -0
  39. package/dist/esm/build/vendor-oxlint-plugins.js +53 -0
  40. package/dist/esm/cli/commands/audit-core.js +9 -1
  41. package/dist/esm/cli/commands/audit.js +16 -5
  42. package/dist/esm/cli/commands/blueprint/mutations.d.ts +15 -0
  43. package/dist/esm/cli/commands/blueprint/mutations.js +8 -2
  44. package/dist/esm/cli/commands/compile.d.ts +13 -0
  45. package/dist/esm/cli/commands/compile.js +182 -62
  46. package/dist/esm/cli/commands/format.js +17 -1
  47. package/dist/esm/cli/commands/gain/index.js +10 -3
  48. package/dist/esm/cli/commands/init/gitignore-patcher.js +6 -0
  49. package/dist/esm/cli/commands/init/index.js +29 -48
  50. package/dist/esm/cli/commands/init/mcp-spec.d.ts +54 -4
  51. package/dist/esm/cli/commands/init/mcp-spec.js +126 -22
  52. package/dist/esm/cli/commands/init/merge.js +30 -10
  53. package/dist/esm/cli/commands/init/scaffold-agents-md.js +5 -7
  54. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.d.ts +56 -31
  55. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +180 -43
  56. package/dist/esm/cli/commands/init/scaffolders/rtk/index.d.ts +17 -5
  57. package/dist/esm/cli/commands/init/scaffolders/rtk/index.js +56 -75
  58. package/dist/esm/cli/commands/init/scaffolders/runtime-check/index.js +7 -6
  59. package/dist/esm/cli/commands/init/untracked-collision.d.ts +12 -1
  60. package/dist/esm/cli/commands/init/untracked-collision.js +23 -1
  61. package/dist/esm/cli/commands/package-manager.js +8 -2
  62. package/dist/esm/cli/commands/pr-help.js +1 -1
  63. package/dist/esm/cli/commands/pr.js +18 -4
  64. package/dist/esm/cli/commands/review.js +60 -6
  65. package/dist/esm/cli/commands/worktree/router-dispatch.d.ts +24 -2
  66. package/dist/esm/cli/commands/worktree/router-dispatch.js +80 -48
  67. package/dist/esm/cli/commands/worktree/router.js +5 -2
  68. package/dist/esm/cli/direct-provider-launch.js +12 -4
  69. package/dist/esm/compiler/flatten.d.ts +10 -1
  70. package/dist/esm/compiler/flatten.js +14 -4
  71. package/dist/esm/compose/admin-blocks-registry-integrity.d.ts +22 -0
  72. package/dist/esm/compose/admin-blocks-registry-integrity.js +90 -0
  73. package/dist/esm/compose/compose.d.ts +46 -0
  74. package/dist/esm/compose/compose.js +250 -0
  75. package/dist/esm/compose/escape-html.d.ts +5 -0
  76. package/dist/esm/compose/escape-html.js +12 -0
  77. package/dist/esm/compose/ops-report-render.d.ts +5 -0
  78. package/dist/esm/compose/ops-report-render.js +71 -0
  79. package/dist/esm/compose/ops-report-schema.d.ts +42 -0
  80. package/dist/esm/compose/ops-report-schema.js +60 -0
  81. package/dist/esm/compose/preview.d.ts +14 -0
  82. package/dist/esm/compose/preview.js +32 -0
  83. package/dist/esm/compose/registry-resolve.d.ts +64 -0
  84. package/dist/esm/compose/registry-resolve.js +200 -0
  85. package/dist/esm/config/oxlint/graphql-conventions.js +2 -5
  86. package/dist/esm/config/oxlint/import-hygiene.js +21 -8
  87. package/dist/esm/config/oxlint/oxlint-config.fixture.d.ts +1 -1
  88. package/dist/esm/config/oxlint/oxlint-config.fixture.js +2 -3
  89. package/dist/esm/config/oxlint/oxlintrc.d.ts +5 -7
  90. package/dist/esm/config/oxlint/oxlintrc.js +18 -21
  91. package/dist/esm/config/oxlint/path-roles.js +53 -0
  92. package/dist/esm/config/oxlint/query-patterns.js +2 -5
  93. package/dist/esm/daemon/domains/blueprint-exec-ledger.d.ts +86 -0
  94. package/dist/esm/daemon/domains/blueprint-exec-ledger.js +134 -0
  95. package/dist/esm/daemon/domains/blueprint-exec.d.ts +106 -0
  96. package/dist/esm/daemon/domains/blueprint-exec.js +161 -0
  97. package/dist/esm/daemon/domains/launch.d.ts +25 -0
  98. package/dist/esm/daemon/domains/launch.js +30 -1
  99. package/dist/esm/daemon/handlers/blueprints-handlers.d.ts +12 -1
  100. package/dist/esm/daemon/handlers/blueprints-handlers.js +148 -6
  101. package/dist/esm/daemon/handlers/product.d.ts +10 -0
  102. package/dist/esm/daemon/handlers/product.js +10 -1
  103. package/dist/esm/daemon/methods.d.ts +21 -1
  104. package/dist/esm/daemon/methods.js +20 -0
  105. package/dist/esm/daemon/protocol/contract.d.ts +58 -0
  106. package/dist/esm/daemon/protocol/contract.js +38 -0
  107. package/dist/esm/daemon/server.js +7 -0
  108. package/dist/esm/docs-linter/blueprint-plan.js +2 -2
  109. package/dist/esm/errors/wp-error.js +21 -2
  110. package/dist/esm/git/changed-files.d.ts +20 -0
  111. package/dist/esm/git/changed-files.js +63 -0
  112. package/dist/esm/hooks/doctor.d.ts +11 -0
  113. package/dist/esm/hooks/doctor.js +114 -45
  114. package/dist/esm/hooks/pretool-guard/dev-routing.js +1 -1
  115. package/dist/esm/hooks/pretool-guard/rtk-rewrite.d.ts +24 -0
  116. package/dist/esm/hooks/pretool-guard/rtk-rewrite.js +50 -0
  117. package/dist/esm/hooks/pretool-guard/runner.js +6 -0
  118. package/dist/esm/hooks/status/index.js +1 -1
  119. package/dist/esm/hooks/stop/qa-changed-files.js +6 -1
  120. package/dist/esm/mcp/blueprint/_shared/lifecycle.js +39 -11
  121. package/dist/esm/mcp/blueprint/handlers/review-log.d.ts +14 -7
  122. package/dist/esm/mcp/blueprint/handlers/review-log.js +46 -22
  123. package/dist/esm/mcp/blueprint/handlers/validate.js +6 -0
  124. package/dist/esm/mcp/blueprint/registration.js +1 -1
  125. package/dist/esm/mcp/tools/_names.d.ts +1 -1
  126. package/dist/esm/mcp/tools/_names.js +4 -0
  127. package/dist/esm/mcp/tools/_registry.js +8 -0
  128. package/dist/esm/mcp/tools/_shared/redact.d.ts +22 -0
  129. package/dist/esm/mcp/tools/_shared/redact.js +114 -8
  130. package/dist/esm/mcp/tools/gain.js +28 -2
  131. package/dist/esm/mcp/tools/release-progress.d.ts +9 -0
  132. package/dist/esm/mcp/tools/release-progress.js +110 -0
  133. package/dist/esm/mcp/tools/worktree.js +65 -3
  134. package/dist/esm/mcp/tools/wp-ui-blocks-list.d.ts +3 -0
  135. package/dist/esm/mcp/tools/wp-ui-blocks-list.js +43 -0
  136. package/dist/esm/mcp/tools/wp-ui-compose.d.ts +3 -0
  137. package/dist/esm/mcp/tools/wp-ui-compose.js +96 -0
  138. package/dist/esm/mcp/tools/wp-ui-preview.d.ts +3 -0
  139. package/dist/esm/mcp/tools/wp-ui-preview.js +59 -0
  140. package/dist/esm/package.json +2 -0
  141. package/dist/esm/review/lifecycle-writers.d.ts +76 -0
  142. package/dist/esm/review/lifecycle-writers.js +330 -0
  143. package/dist/esm/review/subject.js +66 -32
  144. package/dist/esm/status/snapshot.d.ts +5 -0
  145. package/dist/esm/status/snapshot.js +27 -2
  146. package/dist/esm/test/shard-durations.json +0 -1
  147. package/dist/esm/test-helpers/global-setup.js +38 -3
  148. package/dist/esm/tool-runtime/managed-rtk.d.ts +44 -0
  149. package/dist/esm/tool-runtime/managed-rtk.js +275 -0
  150. package/dist/esm/tool-runtime/resolve-runner.js +18 -194
  151. package/dist/esm/utils/package-root.d.ts +32 -0
  152. package/dist/esm/utils/package-root.js +82 -0
  153. package/dist/esm/worktrees/git-metadata.d.ts +41 -0
  154. package/dist/esm/worktrees/git-metadata.js +48 -0
  155. package/package.json +27 -21
  156. package/dist/esm/config/oxlint/code-safety.d.ts +0 -18
  157. package/dist/esm/config/oxlint/foundation-purity.d.ts +0 -21
  158. package/dist/esm/config/oxlint/graphql-conventions.d.ts +0 -23
  159. package/dist/esm/config/oxlint/import-hygiene.d.ts +0 -34
  160. package/dist/esm/config/oxlint/index.d.ts +0 -22
  161. package/dist/esm/config/oxlint/index.js +0 -28
  162. package/dist/esm/config/oxlint/monorepo-paths.d.ts +0 -22
  163. package/dist/esm/config/oxlint/query-patterns.d.ts +0 -24
  164. package/dist/esm/config/oxlint/testing-quality.d.ts +0 -35
  165. package/dist/esm/config/oxlint/tier-boundaries.d.ts +0 -35
  166. package/dist/esm/output-transforms/rulesync.d.ts +0 -2
  167. package/dist/esm/output-transforms/rulesync.js +0 -79
@@ -6,26 +6,23 @@
6
6
  * the patch tiny and deterministic: per-server upserts, no TOML parser
7
7
  * dependency, no edits to unrelated user config.
8
8
  *
9
- * Managed blocks come from two sources:
10
- * 1. `[mcp_servers.webpresso]`points at webpresso's own MCP server.
11
- * Path-stability requires discovery: webpresso lives in different
12
- * locations depending on how the user installed it (Claude plugin
13
- * install, bun global, pnpm/npm global). Discovery happens at scaffold
14
- * time; the resolved absolute path is written into the codex config.
15
- * When the unified-cli sibling cutover lands (`webpresso mcp serve`
16
- * from a path-stable bin), this block collapses to a fixed `command`.
17
- * 2. Consumer-owned `.mcp.json` package declarations — compiled to setup-
18
- * provisioned, exact-version binaries with environment-variable names
19
- * forwarded through Codex's stdio `env_vars` allow-list.
9
+ * Webpresso's own MCP server is NOT one of them. Codex receives `webpresso`
10
+ * over exactly one delivery channel the bundled Codex plugin's `.mcp.json`.
11
+ * Setup therefore *retires* the `[mcp_servers.webpresso]` block earlier
12
+ * versions wrote here (see {@link pruneLegacyCodexWebpressoMcp}) instead of
13
+ * refreshing it. What remains managed here is consumer-owned `.mcp.json`
14
+ * package declarations compiled to setup-provisioned, exact-version binaries
15
+ * with environment-variable names forwarded through Codex's stdio `env_vars`
16
+ * allow-list.
20
17
  */
21
18
  import { spawnSync } from "node:child_process";
22
- import { existsSync, mkdirSync, readFileSync } from "node:fs";
19
+ import { existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
23
20
  import { homedir } from "node:os";
24
21
  import { dirname, join } from "node:path";
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 };
22
+ import { isAgentKitOwnedWpCommand, resolveExecutableOnPath, WEBPRESSO_MCP_SERVER_NAME, } from "#cli/commands/init/mcp-spec.js";
23
+ // Re-exported so existing importers keep a stable path; the canonical
24
+ // implementation lives in `mcp-spec.ts`.
25
+ export { WEBPRESSO_MCP_SERVER_NAME };
29
26
  import { updateScaffolderTextFile } from "#cli/commands/init/scaffolders/file-transaction.js";
30
27
  import { isJsonRecord, isUnsafeRuntimeLauncher, parseJson, parseProjectMcpServers, parseTomlString, } from "#hooks/host-config-contract.js";
31
28
  import { appendGlobalCapableVpArgs, resolveGlobalCapableVpCommand, } from "#cli/global-vp.js";
@@ -309,8 +306,10 @@ function hasDottedServerDefinition(lines, startsInsideMultiline, serverId) {
309
306
  export function hasAmbiguousMcpServerDottedDefinition(raw, serverId) {
310
307
  const lines = raw.trimEnd().split(/\r?\n/);
311
308
  const startsInsideMultiline = tomlLinesStartingInsideMultilineString(lines);
312
- const start = lines.findIndex((line, index) => !startsInsideMultiline[index] && isServerTableHeader(line, serverId));
313
- return start === -1 && hasDottedServerDefinition(lines, startsInsideMultiline, serverId);
309
+ // Scanned unconditionally, not only when no table header exists: a dotted key
310
+ // alongside the table would outlive the table's removal and re-declare the
311
+ // server with no launcher, which breaks the whole config load.
312
+ return hasDottedServerDefinition(lines, startsInsideMultiline, serverId);
314
313
  }
315
314
  export function hasManagedMcpServerTableConflict(raw, serverId) {
316
315
  const lines = raw.trimEnd().split(/\r?\n/);
@@ -658,39 +657,177 @@ export function ensureClaudePlaywrightMcp(input) {
658
657
  return { kind: "claude-playwright-mcp-written", path: configPath };
659
658
  }
660
659
  // ────────────────────────────────────────────────────────────────────────────
661
- // Agent-kit MCP server registration
660
+ // Legacy `[mcp_servers.webpresso]` retirement
661
+ //
662
+ // Earlier versions delivered webpresso to Codex over a SECOND channel: a
663
+ // `[mcp_servers.webpresso]` block in the user-global `config.toml` pinned to
664
+ // whatever absolute install path existed at setup time. The bundled Codex
665
+ // plugin is now the single channel, so setup must remove what it used to write
666
+ // — otherwise every machine keeps a duplicate registration pinned to a path
667
+ // agent-kit itself deletes on the next update or worktree cleanup.
668
+ //
669
+ // The removal predicate is OWNERSHIP-based, never shape-based: a live `wp mcp`
670
+ // launcher outside every agent-kit-owned root is an operator pointing Codex at
671
+ // a custom build and is preserved (and reported by `wp doctor`).
662
672
  // ────────────────────────────────────────────────────────────────────────────
663
- export function agentKitMcpBlock(entryPath) {
664
- const launch = agentKitMcpLaunchCommand(entryPath);
665
- return commandMcpCodexBlock({
666
- id: WEBPRESSO_MCP_SERVER_NAME,
673
+ const TOML_COMMAND_ASSIGNMENT = /^\s*command\s*=\s*(.+?)\s*$/u;
674
+ const TOML_ARGS_ASSIGNMENT = /^\s*args\s*=\s*(.+?)\s*$/u;
675
+ function firstTomlAssignmentValue(body, pattern) {
676
+ for (const line of body) {
677
+ const match = pattern.exec(line);
678
+ if (match?.[1] !== undefined)
679
+ return match[1];
680
+ }
681
+ return null;
682
+ }
683
+ function parseTomlStringArray(value) {
684
+ const trimmed = value.trim();
685
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]"))
686
+ return null;
687
+ const inner = trimmed.slice(1, -1).trim();
688
+ if (inner.length === 0)
689
+ return [];
690
+ const parsed = inner
691
+ .split(",")
692
+ .map((element) => element.trim())
693
+ .filter((element) => element.length > 0)
694
+ .map(parseTomlString);
695
+ return parsed.every((element) => element !== null) ? parsed : null;
696
+ }
697
+ /**
698
+ * Read the parent block's `command`/`args`, stopping at the first sub-table
699
+ * header so a `[mcp_servers.<id>.env]` child never supplies the launch tuple.
700
+ */
701
+ function readManagedBlockLaunch(lines, startsInsideMultiline, start, end) {
702
+ const body = [];
703
+ for (let index = start + 1; index < end; index += 1) {
704
+ // Multiline-string content is document text, not configuration: reading a
705
+ // decoy tuple from it — or letting a bracket line there end the scan —
706
+ // would delete the operator's real registration.
707
+ if (startsInsideMultiline[index])
708
+ continue;
709
+ if (lines[index].trim().startsWith("["))
710
+ break;
711
+ body.push(lines[index]);
712
+ }
713
+ const commandValue = firstTomlAssignmentValue(body, TOML_COMMAND_ASSIGNMENT);
714
+ const command = commandValue === null ? null : parseTomlString(commandValue);
715
+ if (command === null)
716
+ return null;
717
+ const argsValue = firstTomlAssignmentValue(body, TOML_ARGS_ASSIGNMENT);
718
+ const args = argsValue === null ? [] : parseTomlStringArray(argsValue);
719
+ return args === null ? null : { command, args };
720
+ }
721
+ /**
722
+ * The removal path is the only one that rewrites lines it did not classify, so
723
+ * it must reproduce the document's existing newline style rather than impose LF.
724
+ */
725
+ function dominantEol(raw) {
726
+ const crlf = raw.split("\r\n").length - 1;
727
+ const lf = raw.split("\n").length - 1;
728
+ return crlf * 2 > lf ? "\r\n" : "\n";
729
+ }
730
+ function rebuildWithoutSection(lines, start, end, eol) {
731
+ const kept = lines.slice(0, start);
732
+ while (kept.at(-1)?.trim() === "")
733
+ kept.pop();
734
+ const next = kept
735
+ .concat(lines.slice(end))
736
+ .join(eol)
737
+ .replace(/^(?:[ \t]*(?:\r\n|\n))+/u, "")
738
+ .trimEnd();
739
+ return next.length > 0 ? `${next}${eol}` : "";
740
+ }
741
+ /**
742
+ * Remove `[mcp_servers.<serverId>]` (plus its contiguous sub-tables) when
743
+ * `isOwned` claims the block's launch tuple. Fails closed — leaving `raw`
744
+ * byte-identical — for every TOML spelling this line scanner cannot rewrite
745
+ * safely (dotted keys, inline parent table, array tables, non-contiguous
746
+ * sub-tables).
747
+ */
748
+ export function pruneOwnedMcpServerBlock(raw, serverId, isOwned) {
749
+ if (hasAmbiguousMcpServerDottedDefinition(raw, serverId) ||
750
+ hasManagedMcpServerTableConflict(raw, serverId)) {
751
+ return { kind: "ambiguous", raw };
752
+ }
753
+ const lines = raw.trimEnd().split(/\r?\n/);
754
+ const startsInsideMultiline = tomlLinesStartingInsideMultilineString(lines);
755
+ const start = lines.findIndex((line, index) => !startsInsideMultiline[index] && isServerTableHeader(line, serverId));
756
+ if (start === -1)
757
+ return { kind: "absent", raw };
758
+ const end = managedSectionEnd(lines, startsInsideMultiline, start, serverId);
759
+ const launch = readManagedBlockLaunch(lines, startsInsideMultiline, start, end);
760
+ if (launch === null)
761
+ return { kind: "preserved", raw, command: null };
762
+ if (!isOwned(launch.command, launch.args)) {
763
+ return { kind: "preserved", raw, command: launch.command };
764
+ }
765
+ return {
766
+ kind: "removed",
767
+ raw: rebuildWithoutSection(lines, start, end, dominantEol(raw)),
667
768
  command: launch.command,
668
- args: launch.args,
669
- codexEnv: WEBPRESSO_MCP_SPEC.env,
670
- });
769
+ };
671
770
  }
672
- export function upsertWebpressoMcpServer(raw, entryPath) {
673
- return upsertMcpServerBlock(raw, WEBPRESSO_MCP_SERVER_NAME, agentKitMcpBlock(entryPath));
771
+ function legacyPruneResult(outcome, path) {
772
+ switch (outcome.kind) {
773
+ case "removed":
774
+ return { kind: "codex-legacy-webpresso-mcp-removed", path, command: outcome.command };
775
+ case "preserved":
776
+ return { kind: "codex-legacy-webpresso-mcp-preserved", path, command: outcome.command };
777
+ case "ambiguous":
778
+ return { kind: "codex-legacy-webpresso-mcp-ambiguous", path };
779
+ case "absent":
780
+ return { kind: "codex-legacy-webpresso-mcp-absent", path };
781
+ }
674
782
  }
675
- export function ensureCodexWebpressoMcp(input) {
783
+ /**
784
+ * Retire the legacy user-global `[mcp_servers.webpresso]` block. Runs from
785
+ * setup AND from the `wp codex` launch preflight — the launch path is the only
786
+ * one that reaches machines that never re-run setup.
787
+ */
788
+ export function pruneLegacyCodexWebpressoMcp(input) {
676
789
  const configPath = input.configPath ?? defaultConfigPath();
677
790
  if (input.options.dryRun) {
678
- return { kind: "codex-webpresso-mcp-skipped-dry-run", path: configPath };
791
+ return { kind: "codex-legacy-webpresso-mcp-skipped-dry-run", path: configPath };
679
792
  }
680
- const resolution = input.entryPath === undefined ? resolveWebpressoMcpEntry(input.probe) : undefined;
681
- const entryPath = input.entryPath ?? resolution?.entryPath;
682
- if (!entryPath) {
683
- const checked = resolution?.checked ?? [];
684
- return { kind: "codex-webpresso-mcp-not-installed", path: configPath, checked };
793
+ if (!existsSync(configPath)) {
794
+ return { kind: "codex-legacy-webpresso-mcp-absent", path: configPath };
685
795
  }
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 };
796
+ // Classify INSIDE the locked transaction so a concurrent writer's block is
797
+ // never judged from a pre-lock read (PR#767 lost-update guarantee).
798
+ let outcome = { kind: "absent", raw: "" };
799
+ updateCodexConfig(configPath, (raw) => {
800
+ outcome = pruneOwnedMcpServerBlock(raw, WEBPRESSO_MCP_SERVER_NAME, (command, args) => isAgentKitOwnedWpCommand(command, args, input.ownership));
801
+ return outcome.raw;
802
+ });
803
+ return legacyPruneResult(outcome, configPath);
804
+ }
805
+ /**
806
+ * Installed Codex plugin MCP manifests — the surviving delivery channel.
807
+ * Cheap directory probe only: no `codex plugin` subprocess belongs on the
808
+ * launch hot path.
809
+ */
810
+ export function findInstalledCodexPluginMcpManifests(homeDir = process.env.HOME || homedir()) {
811
+ const base = join(homeDir, ".codex", "plugins", "cache");
812
+ const manifests = [];
813
+ for (const marketplace of safeReadDir(base)) {
814
+ const pluginDir = join(base, marketplace, "agent-kit");
815
+ for (const version of safeReadDir(pluginDir)) {
816
+ const manifest = join(pluginDir, version, ".mcp.json");
817
+ if (existsSync(manifest))
818
+ manifests.push(manifest);
819
+ }
690
820
  }
691
- const result = updateCodexConfig(configPath, (raw) => upsertWebpressoMcpServer(raw, entryPath));
692
- if (result === "unchanged") {
693
- return { kind: "codex-webpresso-mcp-unchanged", path: configPath, entryPath };
821
+ return manifests;
822
+ }
823
+ function safeReadDir(dir) {
824
+ try {
825
+ const entries = readdirSync(dir);
826
+ if (!Array.isArray(entries))
827
+ return [];
828
+ return entries.filter((entry) => typeof entry === "string");
829
+ }
830
+ catch {
831
+ return [];
694
832
  }
695
- return { kind: "codex-webpresso-mcp-written", path: configPath, entryPath };
696
833
  }
@@ -1,24 +1,36 @@
1
- import { spawnSync } from "node:child_process";
2
1
  import type { MergeOptions } from "#cli/commands/init/merge";
3
2
  import { type SpinnerFactory } from "#cli/commands/init/scaffolders/spinner";
3
+ import { type ManagedRtkDownloader } from "#tool-runtime/managed-rtk.js";
4
4
  export interface EnsureRtkInput {
5
5
  repoRoot: string;
6
6
  options: MergeOptions;
7
- spawn?: typeof spawnSync;
7
+ /** @deprecated ignored — managed rtk no longer shells out to brew/PATH install */
8
+ spawn?: unknown;
8
9
  pinFilePath?: string;
9
10
  strict?: boolean;
10
11
  spinnerFactory?: SpinnerFactory;
12
+ /** Test seam: inject archive downloader for ensureManagedRtk */
13
+ downloader?: ManagedRtkDownloader;
11
14
  }
12
15
  export type EnsureRtkResult = {
13
16
  kind: "rtk-ok";
14
17
  installed: boolean;
18
+ path: string;
15
19
  } | {
16
20
  kind: "rtk-skipped-dry-run";
17
21
  } | {
18
22
  kind: "rtk-not-found";
19
23
  hint: string;
20
- } | {
21
- kind: "rtk-init-failed";
22
- exitCode: number;
23
24
  };
25
+ /**
26
+ * Strip freestanding RTK PreToolUse surface from project Claude settings and
27
+ * delete the retired script. Also removes the historical stale legacy-runtime
28
+ * PreToolUse group when a direct wp-pretool-guard hook is already present.
29
+ * Idempotent; no-op when settings are absent or already clean.
30
+ */
31
+ export declare function stripFreestandingRtkHooks(repoRoot: string): void;
32
+ /**
33
+ * Ensure managed (embedded-cache) rtk is available and freestanding hooks are gone.
34
+ * No brew, no rtk init --auto-patch, no PATH install requirement.
35
+ */
24
36
  export declare function ensureRtk(input: EnsureRtkInput): EnsureRtkResult;
@@ -1,32 +1,28 @@
1
- import { spawnSync } from "node:child_process";
2
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
2
  import { join } from "node:path";
4
3
  import { makeNoopSpinnerFactory, } from "#cli/commands/init/scaffolders/spinner";
5
- import { checkVersionPin } from "#cli/commands/init/scaffolders/version-pin";
6
- const NOT_FOUND_HINT = "rtk is not on PATH. Install it manually (macOS: `brew install rtk`) and re-run.";
7
- // `rtk --version` answers in ~10ms; 3s is generous headroom. Bounding the
8
- // presence probe keeps a hung/wrong binary from stalling `wp setup`. The
9
- // `brew install` / `rtk init` calls below are intentionally left unbounded —
10
- // they are user-driven installs with inherited stdio where a fixed deadline
11
- // would wrongly kill a legitimately slow run.
12
- const RTK_PROBE_TIMEOUT_MS = 3000;
4
+ import { ensureManagedRtk } from "#tool-runtime/managed-rtk.js";
5
+ const NOT_FOUND_HINT = "managed rtk binary could not be installed (network/platform). Set WP_SKIP_RTK=1 to skip, or WP_RTK_BIN=/path/to/rtk.";
13
6
  const RTK_HOOK_RELATIVE_PATH = join(".claude", "hooks", "rtk-rewrite.sh");
14
7
  const CLAUDE_SETTINGS_RELATIVE_PATH = join(".claude", "settings.json");
15
8
  const STALE_runtime_CLAUDE_COMMAND = "external-runtime/dist/scripts/codex-native-hook.js";
16
- function hasInstalledRtkHook(repoRoot) {
9
+ const FREESTANDING_RTK_HOOK_MARKERS = ["rtk-rewrite.sh", "rtk hook claude"];
10
+ /**
11
+ * Strip freestanding RTK PreToolUse surface from project Claude settings and
12
+ * delete the retired script. Also removes the historical stale legacy-runtime
13
+ * PreToolUse group when a direct wp-pretool-guard hook is already present.
14
+ * Idempotent; no-op when settings are absent or already clean.
15
+ */
16
+ export function stripFreestandingRtkHooks(repoRoot) {
17
17
  const hookPath = join(repoRoot, RTK_HOOK_RELATIVE_PATH);
18
- const settingsPath = join(repoRoot, CLAUDE_SETTINGS_RELATIVE_PATH);
19
- if (!existsSync(hookPath) || !existsSync(settingsPath))
20
- return false;
21
- try {
22
- const settings = readFileSync(settingsPath, "utf8");
23
- return settings.includes("rtk-rewrite.sh");
24
- }
25
- catch {
26
- return false;
18
+ if (existsSync(hookPath)) {
19
+ try {
20
+ rmSync(hookPath, { force: true });
21
+ }
22
+ catch {
23
+ // best-effort; doctor will still flag if the file remains
24
+ }
27
25
  }
28
- }
29
- function normalizeClaudeRtkSettings(repoRoot) {
30
26
  const settingsPath = join(repoRoot, CLAUDE_SETTINGS_RELATIVE_PATH);
31
27
  if (!existsSync(settingsPath))
32
28
  return;
@@ -41,73 +37,58 @@ function normalizeClaudeRtkSettings(repoRoot) {
41
37
  if (!Array.isArray(groups) || groups.length === 0)
42
38
  return;
43
39
  const hasDirectWpGuard = groups.some((group) => (group.hooks ?? []).some((hook) => hook.command?.includes("wp-pretool-guard")));
44
- if (!hasDirectWpGuard)
45
- return;
46
40
  let changed = false;
47
- const nextGroups = groups.filter((group) => {
41
+ const nextGroups = groups
42
+ .map((group) => {
43
+ const hooks = group.hooks ?? [];
44
+ const nextHooks = hooks.filter((hook) => {
45
+ const command = hook.command ?? "";
46
+ if (FREESTANDING_RTK_HOOK_MARKERS.some((marker) => command.includes(marker))) {
47
+ changed = true;
48
+ return false;
49
+ }
50
+ return true;
51
+ });
52
+ if (nextHooks.length !== hooks.length) {
53
+ return { ...group, hooks: nextHooks };
54
+ }
55
+ return group;
56
+ })
57
+ .filter((group) => {
48
58
  const commands = (group.hooks ?? []).map((hook) => hook.command ?? "");
49
- const isStaleOmcGroup = commands.some((command) => command.includes(STALE_runtime_CLAUDE_COMMAND));
50
- if (isStaleOmcGroup)
59
+ if (commands.length === 0) {
60
+ changed = true;
61
+ return false;
62
+ }
63
+ const isStaleOmcGroup = hasDirectWpGuard &&
64
+ commands.some((command) => command.includes(STALE_runtime_CLAUDE_COMMAND));
65
+ if (isStaleOmcGroup) {
51
66
  changed = true;
52
- return !isStaleOmcGroup;
67
+ return false;
68
+ }
69
+ return true;
53
70
  });
54
71
  if (!changed)
55
72
  return;
56
73
  parsed.hooks = { ...parsed.hooks, PreToolUse: nextGroups };
57
74
  writeFileSync(settingsPath, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
58
75
  }
76
+ /**
77
+ * Ensure managed (embedded-cache) rtk is available and freestanding hooks are gone.
78
+ * No brew, no rtk init --auto-patch, no PATH install requirement.
79
+ */
59
80
  export function ensureRtk(input) {
60
81
  if (input.options.dryRun)
61
82
  return { kind: "rtk-skipped-dry-run" };
62
- const spawn = input.spawn ?? spawnSync;
63
83
  const spinner = (input.spinnerFactory ?? makeNoopSpinnerFactory())("rtk");
64
- let installed = false;
65
84
  spinner.start();
66
- let probe = spawn("rtk", ["--version"], { encoding: "utf8", timeout: RTK_PROBE_TIMEOUT_MS });
67
- if (probe.error || (probe.status !== null && probe.status !== 0)) {
68
- if (process.platform !== "darwin") {
69
- spinner.fail("rtk not found");
70
- return { kind: "rtk-not-found", hint: NOT_FOUND_HINT };
71
- }
72
- const install = spawn("brew", ["install", "rtk"], { stdio: "inherit" });
73
- if (install.status !== 0) {
74
- spinner.fail("rtk install failed");
75
- return { kind: "rtk-not-found", hint: NOT_FOUND_HINT };
76
- }
77
- installed = true;
78
- probe = spawn("rtk", ["--version"], { encoding: "utf8", timeout: RTK_PROBE_TIMEOUT_MS });
79
- if (probe.error || (probe.status !== null && probe.status !== 0)) {
80
- spinner.fail("rtk not found after install");
81
- return { kind: "rtk-not-found", hint: NOT_FOUND_HINT };
82
- }
83
- }
84
- const installedVersion = String(probe.stdout ?? "").trim();
85
- const pinCheck = checkVersionPin("rtk", installedVersion, input.pinFilePath ?? join(input.repoRoot, "compatible-versions.json"));
86
- if (!pinCheck.ok) {
87
- if (input.strict) {
88
- spinner.fail("rtk version mismatch");
89
- return { kind: "rtk-init-failed", exitCode: -1 };
90
- }
91
- console.warn(pinCheck.warning);
92
- }
93
- if (hasInstalledRtkHook(input.repoRoot)) {
94
- normalizeClaudeRtkSettings(input.repoRoot);
95
- spinner.succeed("rtk ready");
96
- return { kind: "rtk-ok", installed };
97
- }
98
- const result = spawn("rtk", ["init", "-g", "--auto-patch"], {
99
- cwd: input.repoRoot,
100
- stdio: "inherit",
101
- env: {
102
- ...process.env,
103
- RTK_TELEMETRY_DISABLED: "1",
104
- },
105
- });
106
- if (result.status !== 0) {
107
- spinner.fail("rtk init failed");
108
- return { kind: "rtk-init-failed", exitCode: result.status ?? -1 };
85
+ // Always strip freestanding surface first so setup never leaves CLAUDE_PROJECT_DIR hooks.
86
+ stripFreestandingRtkHooks(input.repoRoot);
87
+ const ensured = ensureManagedRtk({ downloader: input.downloader });
88
+ if (!ensured.ok) {
89
+ spinner.fail("rtk not available");
90
+ return { kind: "rtk-not-found", hint: `${NOT_FOUND_HINT} (${ensured.error})` };
109
91
  }
110
- normalizeClaudeRtkSettings(input.repoRoot);
111
92
  spinner.succeed("rtk ready");
112
- return { kind: "rtk-ok", installed };
93
+ return { kind: "rtk-ok", installed: ensured.installed, path: ensured.path };
113
94
  }
@@ -1,11 +1,12 @@
1
1
  /**
2
- * Runtime detection for tools that webpresso consumers commonly need
3
- * but that `wp setup` cannot install for them (curl-pipe install scripts
4
- * or system package managers shouldn't be triggered silently).
2
+ * Runtime detection for host-install tools that webpresso does **not**
3
+ * managed-download (see catalog rule `managed-tool-binaries`).
5
4
  *
6
- * Auto-runs at the end of `wp setup`; prints a one-line status per
7
- * runtime so the consumer knows what's missing without surprising them
8
- * with a download.
5
+ * Contrasts with managed rtk (`ensureManagedRtk`): bun/vp/actionlint stay
6
+ * host package-manager installs. This probe only prints status + hints —
7
+ * it never curl-pipes or brew-installs silently.
8
+ *
9
+ * Auto-runs at the end of `wp setup`; one line per runtime.
9
10
  */
10
11
  import { spawnSync } from "node:child_process";
11
12
  function probeVersion(cmd, args = ["--version"]) {
@@ -1,14 +1,25 @@
1
1
  import type { ConvergeUnit } from "./convergence.js";
2
- export type CollisionResolution = "none" | "tracked" | "removed-identical" | "removed-owned";
2
+ export type CollisionResolution = "none" | "tracked" | "removed-identical" | "removed-owned" | "ignored-patch-derived" | "ignored-backed-up";
3
3
  export interface CollisionResult {
4
4
  readonly ok: boolean;
5
5
  readonly resolution?: CollisionResolution;
6
6
  readonly error?: string;
7
+ /** Set when the collision proceeded but the caller should surface a notice. */
8
+ readonly warning?: string;
7
9
  }
8
10
  export interface ResolveCollisionOptions {
9
11
  readonly unit: ConvergeUnit;
10
12
  readonly desiredContent: string | Uint8Array;
11
13
  /** Compatibility input only. Unknown content remains protected when true. */
12
14
  readonly force?: boolean;
15
+ /**
16
+ * True when `desiredContent` was computed FROM the existing file's own
17
+ * content (e.g. a JSON key-preserving patch), so a divergent unmarked
18
+ * existing file is safe to write through: the caller already folded the
19
+ * user's data into `desiredContent`.
20
+ */
21
+ readonly patchDerived?: boolean;
13
22
  }
23
+ /** True when `gitPath` is git-ignored in `repoRoot` (machine-local, never committed). */
24
+ export declare function isGitIgnored(repoRoot: string, gitPath: string): boolean;
14
25
  export declare function resolveUntrackedCollision(options: ResolveCollisionOptions): CollisionResult;
@@ -1,5 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { existsSync, lstatSync, readFileSync, rmSync } from "node:fs";
2
+ import { existsSync, lstatSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  const GIT_TIMEOUT_MS = 15_000;
4
4
  const OWNERSHIP_MARKER = /^\uFEFF?[ \t]*(?:#|\/\/|\/\*|<!--|")?[ \t]*(?:>>> managed by webpresso\b|Generated by webpresso\b)/iu;
5
5
  function git(repoRoot, args) {
@@ -12,6 +12,25 @@ function git(repoRoot, args) {
12
12
  });
13
13
  return { ok: result.status === 0 && !result.error };
14
14
  }
15
+ /** True when `gitPath` is git-ignored in `repoRoot` (machine-local, never committed). */
16
+ export function isGitIgnored(repoRoot, gitPath) {
17
+ return git(repoRoot, ["check-ignore", "--no-index", "-q", "--", gitPath]).ok;
18
+ }
19
+ function resolveIgnoredDivergence(options) {
20
+ const { unit } = options;
21
+ if (options.patchDerived) {
22
+ return { ok: true, resolution: "ignored-patch-derived" };
23
+ }
24
+ const current = readFileSync(unit.targetPath);
25
+ const backupPath = `${unit.targetPath}.bak`;
26
+ writeFileSync(backupPath, current);
27
+ return {
28
+ ok: true,
29
+ resolution: "ignored-backed-up",
30
+ warning: `Untracked, git-ignored ${unit.gitPath} had unmanaged content; ` +
31
+ `the original was backed up to ${unit.gitPath}.bak before it was overwritten.`,
32
+ };
33
+ }
15
34
  export function resolveUntrackedCollision(options) {
16
35
  const { unit } = options;
17
36
  if (!existsSync(unit.targetPath))
@@ -34,6 +53,9 @@ export function resolveUntrackedCollision(options) {
34
53
  rmSync(unit.targetPath);
35
54
  return { ok: true, resolution: "removed-owned" };
36
55
  }
56
+ if (unit.scope === "repo-ignored") {
57
+ return resolveIgnoredDivergence(options);
58
+ }
37
59
  return {
38
60
  ok: false,
39
61
  error: `Unknown untracked content blocks ${unit.gitPath}; it was preserved${options.force ? " despite --force" : ""}. ` +
@@ -184,6 +184,9 @@ function runGlobalUpdateCommand(deps) {
184
184
  else {
185
185
  failed = true;
186
186
  console.error(message);
187
+ // Required step failed — stop so optional host refreshes cannot
188
+ // look like a successful update after a broken agent-kit install.
189
+ break;
187
190
  }
188
191
  }
189
192
  }
@@ -195,6 +198,7 @@ function runGlobalUpdateCommand(deps) {
195
198
  else {
196
199
  failed = true;
197
200
  console.error(message);
201
+ break;
198
202
  }
199
203
  }
200
204
  }
@@ -313,8 +317,10 @@ function runAgentKitSelfUpdateStep(deps) {
313
317
  return spawnLike(status, new Error("another agent-kit installer is active; re-run `wp update` once it finishes"));
314
318
  case GUARD_EXIT_OWNERSHIP_LOST:
315
319
  return spawnLike(status, new Error("install lease ownership was lost mid-update; the takeover installer verifies the store — re-run `wp update` to confirm"));
316
- default:
317
- return spawnLike(status === 0 ? 1 : status, new Error(`self-install-guard exited ${status}`));
320
+ default: {
321
+ const code = status === 0 ? 1 : status;
322
+ return spawnLike(code, new Error(`agent-kit self-install failed (self-install-guard exit ${code}); re-run \`wp update\` or \`vp install -g @webpresso/agent-kit\``));
323
+ }
318
324
  }
319
325
  }
320
326
  function refreshCodexPlugin(deps) {
@@ -53,7 +53,7 @@ export function getPrWaitCommandHelpText() {
53
53
  " --branch <name> Branch selector (defaults to current branch)",
54
54
  " --pr <number-or-url> Explicit PR selector",
55
55
  " --until <target> ready | checks-terminal | checks-green | merged (default: checks-terminal)",
56
- " --timeout-ms <ms> Overall deadline; for ready, per-probe timeout only",
56
+ " --timeout-ms <ms> Overall deadline, max 300000 (5min); for ready, per-probe timeout only",
57
57
  " --poll-interval-ms <ms> Poll interval (default: 15000)",
58
58
  " --include-checks Include final check snapshot (default: true)",
59
59
  " --no-include-checks Omit final check snapshot from output",