@webpresso/agent-kit 3.3.3 → 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 (136) hide show
  1. package/THIRD-PARTY-NOTICES.md +12 -10
  2. package/catalog/agent/rules/changeset-release.md +15 -4
  3. package/catalog/agent/rules/managed-tool-binaries.md +67 -0
  4. package/catalog/agent/rules/parallel-orchestration-quota.md +99 -1
  5. package/catalog/agent/rules/pre-implementation.md +36 -20
  6. package/catalog/agent/rules/rtk-routing.md +22 -16
  7. package/catalog/agent/skills/autopilot/SKILL.md +4 -3
  8. package/catalog/agent/skills/ultragoal/SKILL.md +11 -9
  9. package/dist/esm/audit/hook-surface.js +5 -1
  10. package/dist/esm/audit/repo-guardrails.d.ts +8 -0
  11. package/dist/esm/audit/repo-guardrails.js +71 -1
  12. package/dist/esm/blueprint/core/parser.js +38 -12
  13. package/dist/esm/blueprint/execution/artifacts.js +8 -4
  14. package/dist/esm/blueprint/execution/frontmatter-keys.d.ts +17 -0
  15. package/dist/esm/blueprint/execution/frontmatter-keys.js +24 -0
  16. package/dist/esm/blueprint/execution/metadata.js +8 -4
  17. package/dist/esm/blueprint/lifecycle/audit.d.ts +21 -0
  18. package/dist/esm/blueprint/lifecycle/audit.js +89 -3
  19. package/dist/esm/blueprint/lifecycle/review-provenance.js +63 -10
  20. package/dist/esm/blueprint/lifecycle/rollout-anchor.d.ts +22 -0
  21. package/dist/esm/blueprint/lifecycle/rollout-anchor.js +37 -1
  22. package/dist/esm/blueprint/markdown/blocked-line.d.ts +49 -0
  23. package/dist/esm/blueprint/markdown/blocked-line.js +63 -0
  24. package/dist/esm/blueprint/markdown/helpers.js +10 -4
  25. package/dist/esm/blueprint/tracked-document/parser.js +3 -3
  26. package/dist/esm/blueprint/trust/gate-row.d.ts +55 -0
  27. package/dist/esm/blueprint/trust/gate-row.js +113 -0
  28. package/dist/esm/blueprint/trust/promotion.d.ts +17 -0
  29. package/dist/esm/blueprint/trust/promotion.js +19 -19
  30. package/dist/esm/build/atomic-file-copy.d.ts +32 -0
  31. package/dist/esm/build/atomic-file-copy.js +44 -0
  32. package/dist/esm/build/vendor-oxlint-plugins.d.ts +8 -0
  33. package/dist/esm/build/vendor-oxlint-plugins.js +53 -0
  34. package/dist/esm/cli/commands/blueprint/mutations.d.ts +15 -0
  35. package/dist/esm/cli/commands/blueprint/mutations.js +8 -2
  36. package/dist/esm/cli/commands/compile.d.ts +13 -0
  37. package/dist/esm/cli/commands/compile.js +182 -62
  38. package/dist/esm/cli/commands/format.js +17 -1
  39. package/dist/esm/cli/commands/gain/index.js +10 -3
  40. package/dist/esm/cli/commands/init/gitignore-patcher.js +6 -0
  41. package/dist/esm/cli/commands/init/index.js +29 -48
  42. package/dist/esm/cli/commands/init/mcp-spec.d.ts +48 -13
  43. package/dist/esm/cli/commands/init/mcp-spec.js +86 -30
  44. package/dist/esm/cli/commands/init/merge.js +30 -10
  45. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.d.ts +56 -31
  46. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +180 -43
  47. package/dist/esm/cli/commands/init/scaffolders/rtk/index.d.ts +17 -5
  48. package/dist/esm/cli/commands/init/scaffolders/rtk/index.js +56 -75
  49. package/dist/esm/cli/commands/init/scaffolders/runtime-check/index.js +7 -6
  50. package/dist/esm/cli/commands/init/untracked-collision.d.ts +12 -1
  51. package/dist/esm/cli/commands/init/untracked-collision.js +23 -1
  52. package/dist/esm/cli/commands/pr-help.js +1 -1
  53. package/dist/esm/cli/commands/pr.js +18 -4
  54. package/dist/esm/cli/commands/review.js +60 -6
  55. package/dist/esm/cli/commands/worktree/router-dispatch.d.ts +24 -2
  56. package/dist/esm/cli/commands/worktree/router-dispatch.js +80 -48
  57. package/dist/esm/cli/commands/worktree/router.js +5 -2
  58. package/dist/esm/cli/direct-provider-launch.js +12 -4
  59. package/dist/esm/compiler/flatten.d.ts +10 -1
  60. package/dist/esm/compiler/flatten.js +14 -4
  61. package/dist/esm/compose/admin-blocks-registry-integrity.d.ts +22 -0
  62. package/dist/esm/compose/admin-blocks-registry-integrity.js +90 -0
  63. package/dist/esm/compose/compose.d.ts +8 -0
  64. package/dist/esm/compose/compose.js +115 -8
  65. package/dist/esm/compose/ops-report-schema.js +17 -3
  66. package/dist/esm/compose/registry-resolve.d.ts +17 -0
  67. package/dist/esm/compose/registry-resolve.js +78 -11
  68. package/dist/esm/config/oxlint/graphql-conventions.js +2 -5
  69. package/dist/esm/config/oxlint/import-hygiene.js +21 -8
  70. package/dist/esm/config/oxlint/oxlint-config.fixture.d.ts +1 -1
  71. package/dist/esm/config/oxlint/oxlint-config.fixture.js +2 -3
  72. package/dist/esm/config/oxlint/oxlintrc.d.ts +5 -7
  73. package/dist/esm/config/oxlint/oxlintrc.js +18 -21
  74. package/dist/esm/config/oxlint/path-roles.js +53 -0
  75. package/dist/esm/config/oxlint/query-patterns.js +2 -5
  76. package/dist/esm/daemon/domains/blueprint-exec-ledger.d.ts +86 -0
  77. package/dist/esm/daemon/domains/blueprint-exec-ledger.js +134 -0
  78. package/dist/esm/daemon/domains/blueprint-exec.d.ts +106 -0
  79. package/dist/esm/daemon/domains/blueprint-exec.js +161 -0
  80. package/dist/esm/daemon/domains/launch.d.ts +25 -0
  81. package/dist/esm/daemon/domains/launch.js +30 -1
  82. package/dist/esm/daemon/handlers/blueprints-handlers.d.ts +12 -1
  83. package/dist/esm/daemon/handlers/blueprints-handlers.js +148 -6
  84. package/dist/esm/daemon/handlers/product.d.ts +10 -0
  85. package/dist/esm/daemon/handlers/product.js +10 -1
  86. package/dist/esm/daemon/methods.d.ts +21 -1
  87. package/dist/esm/daemon/methods.js +20 -0
  88. package/dist/esm/daemon/protocol/contract.d.ts +58 -0
  89. package/dist/esm/daemon/protocol/contract.js +38 -0
  90. package/dist/esm/daemon/server.js +7 -0
  91. package/dist/esm/docs-linter/blueprint-plan.js +2 -2
  92. package/dist/esm/errors/wp-error.js +21 -2
  93. package/dist/esm/hooks/doctor.d.ts +11 -0
  94. package/dist/esm/hooks/doctor.js +114 -45
  95. package/dist/esm/hooks/pretool-guard/dev-routing.js +1 -1
  96. package/dist/esm/hooks/pretool-guard/rtk-rewrite.d.ts +24 -0
  97. package/dist/esm/hooks/pretool-guard/rtk-rewrite.js +50 -0
  98. package/dist/esm/hooks/pretool-guard/runner.js +6 -0
  99. package/dist/esm/hooks/status/index.js +1 -1
  100. package/dist/esm/mcp/blueprint/_shared/lifecycle.js +39 -11
  101. package/dist/esm/mcp/blueprint/handlers/review-log.d.ts +14 -7
  102. package/dist/esm/mcp/blueprint/handlers/review-log.js +46 -22
  103. package/dist/esm/mcp/blueprint/registration.js +1 -1
  104. package/dist/esm/mcp/tools/_shared/redact.d.ts +22 -0
  105. package/dist/esm/mcp/tools/_shared/redact.js +114 -8
  106. package/dist/esm/mcp/tools/gain.js +28 -2
  107. package/dist/esm/mcp/tools/worktree.js +65 -3
  108. package/dist/esm/mcp/tools/wp-ui-compose.js +38 -4
  109. package/dist/esm/mcp/tools/wp-ui-preview.js +14 -7
  110. package/dist/esm/review/lifecycle-writers.d.ts +76 -0
  111. package/dist/esm/review/lifecycle-writers.js +330 -0
  112. package/dist/esm/review/subject.js +66 -32
  113. package/dist/esm/status/snapshot.d.ts +5 -0
  114. package/dist/esm/status/snapshot.js +27 -2
  115. package/dist/esm/test/shard-durations.json +0 -1
  116. package/dist/esm/test-helpers/global-setup.js +38 -3
  117. package/dist/esm/tool-runtime/managed-rtk.d.ts +44 -0
  118. package/dist/esm/tool-runtime/managed-rtk.js +275 -0
  119. package/dist/esm/tool-runtime/resolve-runner.js +18 -194
  120. package/dist/esm/utils/package-root.d.ts +32 -0
  121. package/dist/esm/utils/package-root.js +82 -0
  122. package/dist/esm/worktrees/git-metadata.d.ts +41 -0
  123. package/dist/esm/worktrees/git-metadata.js +48 -0
  124. package/package.json +16 -21
  125. package/dist/esm/config/oxlint/code-safety.d.ts +0 -18
  126. package/dist/esm/config/oxlint/foundation-purity.d.ts +0 -21
  127. package/dist/esm/config/oxlint/graphql-conventions.d.ts +0 -23
  128. package/dist/esm/config/oxlint/import-hygiene.d.ts +0 -34
  129. package/dist/esm/config/oxlint/index.d.ts +0 -22
  130. package/dist/esm/config/oxlint/index.js +0 -28
  131. package/dist/esm/config/oxlint/monorepo-paths.d.ts +0 -22
  132. package/dist/esm/config/oxlint/query-patterns.d.ts +0 -24
  133. package/dist/esm/config/oxlint/testing-quality.d.ts +0 -35
  134. package/dist/esm/config/oxlint/tier-boundaries.d.ts +0 -35
  135. package/dist/esm/output-transforms/rulesync.d.ts +0 -2
  136. package/dist/esm/output-transforms/rulesync.js +0 -79
@@ -1,35 +1,125 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
3
  import { existsSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
4
- import { tmpdir } from "node:os";
5
4
  import { dirname, join, resolve } from "node:path";
6
5
  import { createRequire } from "node:module";
7
6
  import { z } from "zod";
8
7
  import { flattenAgentDir, writeFlattenedAssets } from "#compiler/flatten";
9
- const PINNED_RULESYNC_VERSION = "9.4.0";
8
+ import { findPackageAsset } from "#utils/package-assets.js";
9
+ import { readPackageManifest, resolvePackageRoot, resolvePackageVersion, } from "#shared-utils/package-root.js";
10
10
  const SUPPORTED_TARGETS = ["claude", "codex", "opencode"];
11
11
  const DEFAULT_TARGETS = SUPPORTED_TARGETS.join(",");
12
12
  const COMPILE_MANIFEST_VERSION = 1;
13
+ /**
14
+ * wp's target names are the stable CLI contract; rulesync's own ids differ for
15
+ * two of the three. rulesync does not reject an unknown target — it logs a skip
16
+ * and exits 0 — so passing wp's names straight through generated nothing for
17
+ * `claude` and `codex` while still reporting success.
18
+ */
19
+ const RULESYNC_TARGET_IDS = {
20
+ claude: "claudecode",
21
+ codex: "codexcli",
22
+ opencode: "opencode",
23
+ };
24
+ /**
25
+ * rulesync defaults `--features` to `rules`, which agent-kit never emits — so
26
+ * omitting the flag guaranteed an empty generate. This is the set agent-kit
27
+ * actually flattens.
28
+ *
29
+ * Deliberately not `*`: that also enables `rules`, whose output writes
30
+ * `AGENTS.md`/`CLAUDE.md` at the repo root and collides with the mergeAgentsMd
31
+ * step below, plus `ignore`/`mcp`/`hooks`/`permissions`, whose loaders print
32
+ * ENOENT lines straight to the user's terminal through the inherited stdio.
33
+ */
34
+ const RULESYNC_FEATURES = "skills,commands,subagents";
35
+ const ASSET_KINDS = ["skills", "commands", "agents"];
36
+ /**
37
+ * Where rulesync writes each asset kind, per wp target.
38
+ *
39
+ * A kind omitted from a target's entry is one rulesync does not emit in project
40
+ * scope — notably codexcli commands, which it supports only under `--global`
41
+ * and otherwise skips with a warning. Omitting it keeps the check below honest
42
+ * rather than demanding a file rulesync was never going to write.
43
+ */
44
+ const RULESYNC_OUTPUT_PATHS = {
45
+ claude: {
46
+ skills: (name) => join(".claude", "skills", name, "SKILL.md"),
47
+ commands: (name) => join(".claude", "commands", `${name}.md`),
48
+ agents: (name) => join(".claude", "agents", `${name}.md`),
49
+ },
50
+ codex: {
51
+ skills: (name) => join(".agents", "skills", name, "SKILL.md"),
52
+ // Codex subagents are TOML, not markdown — every other target emits .md.
53
+ agents: (name) => join(".codex", "agents", `${name}.toml`),
54
+ },
55
+ opencode: {
56
+ skills: (name) => join(".opencode", "skills", name, "SKILL.md"),
57
+ commands: (name) => join(".opencode", "commands", `${name}.md`),
58
+ agents: (name) => join(".opencode", "agents", `${name}.md`),
59
+ },
60
+ };
61
+ /** Every output path a target is expected to produce for the given assets. */
62
+ function expectedOutputsForTarget(target, assets) {
63
+ const paths = RULESYNC_OUTPUT_PATHS[target];
64
+ const expected = [];
65
+ for (const kind of ASSET_KINDS) {
66
+ const toPath = paths[kind];
67
+ if (!toPath)
68
+ continue;
69
+ for (const name of Object.keys(assets[kind]))
70
+ expected.push(toPath(name));
71
+ }
72
+ return expected;
73
+ }
74
+ function expectedOutputs(targets, assets) {
75
+ return targets.flatMap((target) => expectedOutputsForTarget(target, assets));
76
+ }
13
77
  // Resolve rulesync bin — checks consumer's node_modules first,
14
78
  // then falls back to webpresso's own bundled rulesync.
79
+ // rulesync's `exports` map declares only ".", so its package.json is not
80
+ // resolvable by subpath — see #shared-utils/package-root for the full trap.
15
81
  const _require = createRequire(import.meta.url);
16
- function resolveRulesyncBinFromAgentKit() {
17
- try {
18
- const pkgPath = _require.resolve("rulesync/package.json");
19
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
20
- const binField = pkg.bin;
21
- const rel = typeof binField === "string"
22
- ? binField
23
- : typeof binField === "object" && binField !== null && "rulesync" in binField
24
- ? String(binField["rulesync"])
25
- : null;
26
- if (!rel)
27
- return null;
28
- return join(dirname(pkgPath), rel);
29
- }
30
- catch {
82
+ /**
83
+ * Resolve rulesync relative to agent-kit's installed location rather than to
84
+ * this module's URL.
85
+ *
86
+ * In a bundled-Bun single-file binary `import.meta.url` is a `/$bunfs/root/...`
87
+ * virtual path, so a require anchored on it resolves nothing — and that is
88
+ * exactly the globally-installed case where the consumer has no local rulesync
89
+ * and this fallback is the only copy available.
90
+ */
91
+ function agentKitAnchoredRequire() {
92
+ const manifestPath = findPackageAsset("package.json");
93
+ if (manifestPath === null)
94
+ return _require;
95
+ return createRequire(join(dirname(manifestPath), "noop.cjs"));
96
+ }
97
+ export function resolveRulesyncBinFromAgentKit(resolver = agentKitAnchoredRequire()) {
98
+ const root = resolvePackageRoot("rulesync", resolver);
99
+ if (!root)
31
100
  return null;
32
- }
101
+ const binField = readPackageManifest(root)?.bin;
102
+ const rel = typeof binField === "string"
103
+ ? binField
104
+ : typeof binField === "object" && binField !== null && "rulesync" in binField
105
+ ? String(binField["rulesync"])
106
+ : null;
107
+ if (!rel)
108
+ return null;
109
+ return join(root, rel);
110
+ }
111
+ /**
112
+ * The rulesync version agent-kit itself resolves — the only honest baseline to
113
+ * compare a consumer's install against.
114
+ *
115
+ * This used to be a hand-maintained `PINNED_RULESYNC_VERSION` literal, which
116
+ * drifted five majors behind the declared dependency (9.4.0 vs 14.0.1). The
117
+ * mismatch warning then fired on every single run, including inside agent-kit
118
+ * itself, which trains readers to ignore the one warning meant to catch a real
119
+ * mismatch. Deriving it from the resolved package makes that drift impossible.
120
+ */
121
+ export function resolveBundledRulesyncVersion(resolver = agentKitAnchoredRequire()) {
122
+ return resolvePackageVersion("rulesync", resolver);
33
123
  }
34
124
  const CompileManifestSchema = z.object({
35
125
  version: z.number(),
@@ -56,25 +146,6 @@ function readRulesyncVersion(cwd) {
56
146
  return null;
57
147
  }
58
148
  }
59
- function contentHash(assets) {
60
- const entries = [
61
- ...Object.entries(assets.skills).map(([k, v]) => `s:${k}:${v}`),
62
- ...Object.entries(assets.commands).map(([k, v]) => `c:${k}:${v}`),
63
- ...Object.entries(assets.agents).map(([k, v]) => `a:${k}:${v}`),
64
- ];
65
- entries.sort();
66
- return entries.join("\0");
67
- }
68
- function readHashFile(p) {
69
- if (!existsSync(p))
70
- return null;
71
- try {
72
- return readFileSync(p, "utf-8").trim();
73
- }
74
- catch {
75
- return null;
76
- }
77
- }
78
149
  /** SHA-256 hash of all .md files under agentDir, recursively (content only). */
79
150
  export function hashAgentDir(agentDir) {
80
151
  const h = createHash("sha256");
@@ -144,20 +215,26 @@ export async function runCompile(options) {
144
215
  }
145
216
  const agentDir = join(cwd, ".agent");
146
217
  const lockPath = join(agentDir, ".compile.lock");
147
- const hashPath = join(agentDir, ".compile.hash");
148
218
  const manifestPath = join(agentDir, ".compile-manifest.json");
149
219
  const rulesyncBin = resolveRulesyncBin(cwd);
220
+ const expectedVersion = resolveBundledRulesyncVersion();
150
221
  if (!rulesyncBin || !existsSync(rulesyncBin)) {
222
+ const installHint = expectedVersion ? `rulesync@${expectedVersion}` : "rulesync";
151
223
  return {
152
224
  ok: false,
153
225
  targets: targetList,
154
226
  noOp: false,
155
- message: "rulesync is not installed — run `vp add -w rulesync@9.4.0`",
227
+ message: `rulesync is not installed — run \`vp add -w ${installHint}\``,
156
228
  };
157
229
  }
158
230
  const installedVersion = readRulesyncVersion(cwd);
159
- if (installedVersion !== null && installedVersion !== PINNED_RULESYNC_VERSION) {
160
- process.stderr.write(`wp compile: warning installed rulesync@${installedVersion} does not match pinned @${PINNED_RULESYNC_VERSION}\n`);
231
+ // Only compare against a version we actually resolved: with no baseline there
232
+ // is nothing to assert, and a fabricated one is what produced the old
233
+ // permanently-firing warning.
234
+ if (expectedVersion !== null &&
235
+ installedVersion !== null &&
236
+ installedVersion !== expectedVersion) {
237
+ process.stderr.write(`wp compile: warning — installed rulesync@${installedVersion} does not match the rulesync@${expectedVersion} agent-kit resolves\n`);
161
238
  }
162
239
  // Atomic lock via O_EXCL — fails if another compile is running
163
240
  try {
@@ -171,31 +248,48 @@ export async function runCompile(options) {
171
248
  message: `wp compile: lock file exists at ${lockPath} — another compile is running`,
172
249
  };
173
250
  }
251
+ // Staging dir lives in cwd (see the rename below), so an interrupted compile
252
+ // would strand a `.rulesync.tmp-*` directory inside the user's repo that no
253
+ // later run reaps. Tracked here so the signal path clears it too.
254
+ let stagingDir = null;
174
255
  const cleanup = () => {
175
256
  try {
176
257
  if (existsSync(lockPath))
177
258
  unlinkSync(lockPath);
259
+ if (stagingDir !== null)
260
+ rmSync(stagingDir, { recursive: true, force: true });
178
261
  }
179
262
  catch {
180
263
  /* best-effort */
181
264
  }
182
265
  };
183
- process.on("exit", cleanup);
184
- process.on("SIGINT", () => {
266
+ // Held as named refs so `finally` can detach them: runCompile is called
267
+ // repeatedly within one process (the test suites do exactly this), and
268
+ // anonymous handlers would stack up a MaxListenersExceededWarning plus N
269
+ // SIGINT handlers each racing to call process.exit.
270
+ const onSigint = () => {
185
271
  cleanup();
186
272
  process.exit(130);
187
- });
188
- process.on("SIGTERM", () => {
273
+ };
274
+ const onSigterm = () => {
189
275
  cleanup();
190
276
  process.exit(143);
191
- });
277
+ };
278
+ process.on("exit", cleanup);
279
+ process.on("SIGINT", onSigint);
280
+ process.on("SIGTERM", onSigterm);
192
281
  try {
193
282
  const assets = flattenAgentDir(agentDir);
194
- const hash = contentHash(assets);
195
283
  const sourceHash = hashAgentDir(agentDir);
196
- // Check compile manifest for idempotent no-op
284
+ // An unchanged source hash only proves the *input* is the same; it says
285
+ // nothing about whether the generated surfaces still exist. Deleting
286
+ // .claude/skills/ would otherwise leave compile reporting "no changes"
287
+ // forever while the files it exists to produce stay missing — the very
288
+ // silent-zero-output failure this command was repaired to eliminate.
289
+ const expected = expectedOutputs(targetList, assets);
290
+ const outputsPresent = expected.every((rel) => existsSync(join(cwd, rel)));
197
291
  const existingManifest = readCompileManifest(manifestPath);
198
- if (existingManifest !== null && existingManifest.sourceHash === sourceHash) {
292
+ if (outputsPresent && existingManifest !== null && existingManifest.sourceHash === sourceHash) {
199
293
  return {
200
294
  ok: true,
201
295
  targets: targetList,
@@ -203,30 +297,35 @@ export async function runCompile(options) {
203
297
  message: "No changes (manifest matches)",
204
298
  };
205
299
  }
206
- // Legacy hash file check (pre-manifest path)
207
- if (existingManifest === null && readHashFile(hashPath) === hash) {
208
- return {
209
- ok: true,
210
- targets: targetList,
211
- noOp: true,
212
- message: "wp compile: no-op (content unchanged)",
213
- };
214
- }
215
- // Step 1+2: Write flattened assets to tmpdir then atomically rename to .rulesync/
216
- const tmpOut = mkdtempSync(join(tmpdir(), "wp-compile-"));
300
+ // Step 1+2: Stage the flattened assets, then atomically rename to .rulesync/.
301
+ //
302
+ // The staging dir must live inside `cwd`, not in os.tmpdir(): renameSync
303
+ // cannot cross filesystems, and on any host where /tmp is its own mount
304
+ // (tmpfs on most systemd Linux distros, Docker --tmpfs, many CI runners)
305
+ // the swap below would throw EXDEV and take the whole command down. The
306
+ // test suites cannot catch that on their own, because their cwd is itself
307
+ // under tmpdir() and so always lands on the same filesystem.
308
+ const tmpOut = mkdtempSync(join(cwd, ".rulesync.tmp-"));
309
+ stagingDir = tmpOut;
217
310
  try {
218
311
  await writeFlattenedAssets(assets, tmpOut);
219
312
  const rulesyncInputDir = join(cwd, ".rulesync");
220
313
  if (existsSync(rulesyncInputDir))
221
314
  rmSync(rulesyncInputDir, { recursive: true, force: true });
222
315
  renameSync(tmpOut, rulesyncInputDir);
316
+ // Renamed away: nothing left at the staging path for cleanup to remove.
317
+ stagingDir = null;
223
318
  }
224
319
  catch (err) {
225
320
  rmSync(tmpOut, { recursive: true, force: true });
321
+ stagingDir = null;
226
322
  throw err;
227
323
  }
228
324
  // Step 3: Run rulesync generate
229
- const result = spawnSync(rulesyncBin, ["generate", "--targets", targets], {
325
+ const rulesyncTargets = targetList
326
+ .map((target) => RULESYNC_TARGET_IDS[target])
327
+ .join(",");
328
+ const result = spawnSync(rulesyncBin, ["generate", "--targets", rulesyncTargets, "--features", RULESYNC_FEATURES], {
230
329
  cwd,
231
330
  stdio: "inherit",
232
331
  });
@@ -247,6 +346,21 @@ export async function runCompile(options) {
247
346
  message: `wp compile: rulesync exited with code ${exitCode}`,
248
347
  };
249
348
  }
349
+ // rulesync exits 0 when it generates nothing at all ("All files are up to
350
+ // date"), so the exit code above cannot distinguish a real generate from a
351
+ // silent no-op caused by a target it skipped or input it could not see.
352
+ // The files consumers need are the contract, so assert they exist.
353
+ const missing = expected.filter((rel) => !existsSync(join(cwd, rel)));
354
+ if (missing.length > 0) {
355
+ const shown = missing.slice(0, 5).join(", ");
356
+ const rest = missing.length > 5 ? ` (and ${missing.length - 5} more)` : "";
357
+ return {
358
+ ok: false,
359
+ targets: targetList,
360
+ noOp: false,
361
+ message: `wp compile: rulesync exited 0 but ${missing.length} expected output(s) were not generated: ${shown}${rest}`,
362
+ };
363
+ }
250
364
  // Step 4: Run mergeAgentsMd to produce AGENTS.md at repo root
251
365
  // (Plugin manifests are hand-maintained SSOT under packages/*-plugin/;
252
366
  // wp compile no longer emits them — golden tests + version stamping own
@@ -270,6 +384,10 @@ export async function runCompile(options) {
270
384
  const outputHashes = {
271
385
  "AGENTS.md": hashFile(join(cwd, "AGENTS.md")),
272
386
  };
387
+ // Every generated surface, not just AGENTS.md — this is what gives
388
+ // `wp audit compile-drift` something real to compare against.
389
+ for (const rel of expected)
390
+ outputHashes[rel] = hashFile(join(cwd, rel));
273
391
  const compileManifest = {
274
392
  version: COMPILE_MANIFEST_VERSION,
275
393
  timestamp: new Date().toISOString(),
@@ -277,7 +395,6 @@ export async function runCompile(options) {
277
395
  outputHashes,
278
396
  };
279
397
  writeCompileManifest(manifestPath, compileManifest);
280
- writeFileSync(hashPath, hash);
281
398
  return {
282
399
  ok: true,
283
400
  targets: targetList,
@@ -287,6 +404,9 @@ export async function runCompile(options) {
287
404
  }
288
405
  finally {
289
406
  cleanup();
407
+ process.removeListener("exit", cleanup);
408
+ process.removeListener("SIGINT", onSigint);
409
+ process.removeListener("SIGTERM", onSigterm);
290
410
  }
291
411
  }
292
412
  function collectAgentsLayers(agentDir) {
@@ -178,11 +178,27 @@ function formatterTargetFromCwd(file, cwd) {
178
178
  return file;
179
179
  return relativeTarget;
180
180
  }
181
+ // The blueprint root is not always the top-level `blueprints/` directory: a
182
+ // consumer repo can resolve to `webpresso/blueprints/` (the webpresso-legacy
183
+ // fallback layout — see #utils/blueprint-root.js) or a `.webpressorc.json`
184
+ // override. A prefix anchored to one specific root silently fails to exclude
185
+ // review-artifact markdown under any other layout, so `wp format` reformats
186
+ // it. That mutates the artifact's bytes between when `wp review gate`
187
+ // records its sha256 into review-events.jsonl and when the file is
188
+ // committed, producing a false "Review artifact digest mismatch" at
189
+ // `wp_blueprint_finalize` time (src/review/authority.ts correctly detects
190
+ // the drift and fails closed). Match on the directory name alone — not
191
+ // anchored to any specific root prefix — so this stays correct across every
192
+ // current and future blueprint root layout without tracking
193
+ // resolveBlueprintRoot's precedence rules in a second place. This also keeps
194
+ // parity with the whole-repo formatter's own ignore layer
195
+ // (vite.config.ts#fmt.ignorePatterns), which uses the same unanchored
196
+ // `**/review-artifacts/**` glob for the same reason.
181
197
  function filterFormatableFiles(files, cwd) {
182
198
  return files.filter((file) => {
183
199
  const extension = path.extname(file).toLowerCase();
184
200
  const normalized = file.replaceAll("\\", "/");
185
- const isReviewArtifact = normalized.startsWith("blueprints/") && normalized.includes("/review-artifacts/");
201
+ const isReviewArtifact = normalized.includes("/review-artifacts/");
186
202
  return OXFMT_EXTENSIONS.has(extension) && !isReviewArtifact && existsSync(path.join(cwd, file));
187
203
  });
188
204
  }
@@ -1,17 +1,24 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { SessionMemoryStore } from "#session-memory/store.js";
3
3
  import { defaultIndexDbPath } from "#mcp/tools/session-restore.js";
4
+ import { resolveManagedRtkExecutable } from "#tool-runtime/managed-rtk.js";
4
5
  export function runGain(options = {}) {
5
6
  const dbPath = options.indexDbPath ?? defaultIndexDbPath(options.cwd);
6
7
  const webpressoStats = readWebpressoGainStats(dbPath);
7
8
  printWebpressoGain(webpressoStats, dbPath, { sessions: options.sessions === true });
8
9
  console.log("\n── RTK Gain Totals ──────────────────────────────────────────");
9
- const rtk = spawnSync("rtk", ["gain", "--format", "json"], { encoding: "utf8" });
10
+ const rtkBin = resolveManagedRtkExecutable();
11
+ if (!rtkBin) {
12
+ console.log(" Managed RTK not available.");
13
+ console.log(" Enable: wp setup --with rtk (or set WP_RTK_BIN to a capable rtk binary)");
14
+ return 0;
15
+ }
16
+ const rtk = spawnSync(rtkBin, ["gain", "--format", "json"], { encoding: "utf8" });
10
17
  if (rtk.error) {
11
18
  const err = rtk.error;
12
19
  if (err instanceof Error && "code" in err && err.code === "ENOENT") {
13
- console.log(" RTK not installed.");
14
- console.log(" Enable: wp setup --with rtk | Manual: brew install rtk");
20
+ console.log(" Managed RTK binary missing at resolved path.");
21
+ console.log(" Enable: wp setup --with rtk (or set WP_RTK_BIN)");
15
22
  return 0;
16
23
  }
17
24
  throw err;
@@ -76,6 +76,12 @@ export const GENERATED_PATHS_BLOCK = {
76
76
  ".agent/.blueprints.lock",
77
77
  ".agent/.blueprints.snapshot.sql",
78
78
  ".agent/.tail-hint-history.jsonl",
79
+ "# `wp compile` derives .rulesync/ from the canonical agent sources and replaces",
80
+ "# it wholesale on every run, so it is a build artifact rather than the",
81
+ "# hand-authored source tree rulesync's own docs describe. The .tmp- dirs are",
82
+ "# its staging area, normally renamed away but left behind by an interrupt.",
83
+ ".rulesync/",
84
+ ".rulesync.tmp-*",
79
85
  "# Stryker writes these mutation-test artifacts beside consumer source trees.",
80
86
  ".stryker-tmp/",
81
87
  "reports/mutation/",
@@ -54,7 +54,7 @@ import { ensureClaudeCodeUserPlugin } from "./scaffolders/claude-plugin/index.js
54
54
  import { ensureCodexUserPlugin } from "./scaffolders/codex-plugin/index.js";
55
55
  import { scaffoldClaudeRules } from "./scaffolders/claude-rules/index.js";
56
56
  import { ensureCodexCli } from "./scaffolders/codex-cli/index.js";
57
- import { ensureClaudePlaywrightMcp, ensureCodexProjectMcpServers, ensureCodexWebpressoMcp, } from "./scaffolders/codex-mcp/index.js";
57
+ import { ensureClaudePlaywrightMcp, ensureCodexProjectMcpServers, pruneLegacyCodexWebpressoMcp, } from "./scaffolders/codex-mcp/index.js";
58
58
  import { scaffoldExampleSkill } from "./scaffolders/example-skill/index.js";
59
59
  import { scaffoldLoreCommits } from "./scaffolders/lore-commits/index.js";
60
60
  import { ensureOpenCodeWebpressoPlugin } from "./scaffolders/opencode-plugin/index.js";
@@ -381,24 +381,7 @@ async function runUserOnlySetup(input) {
381
381
  console.log(" playwright mcp: - skipped (the preset requires a project-owned .mcp.json; run setup inside the project repo)");
382
382
  }
383
383
  if (selectedHosts.includes("codex")) {
384
- const webpressoMcpResult = ensureCodexWebpressoMcp({ options: userScopeOptions });
385
- switch (webpressoMcpResult.kind) {
386
- case "codex-webpresso-mcp-written":
387
- console.log(` codex webpresso mcp: ✓ ${webpressoMcpResult.path} → ${webpressoMcpResult.entryPath}`);
388
- break;
389
- case "codex-webpresso-mcp-unchanged":
390
- console.log(` codex webpresso mcp: already configured at ${webpressoMcpResult.path}`);
391
- break;
392
- case "codex-webpresso-mcp-ambiguous":
393
- console.warn(` codex webpresso mcp: ⚠ ambiguous MCP config at ${webpressoMcpResult.path}; left unchanged`);
394
- break;
395
- case "codex-webpresso-mcp-skipped-dry-run":
396
- console.log(" codex webpresso mcp: skipped (--dry-run)");
397
- break;
398
- case "codex-webpresso-mcp-not-installed":
399
- console.log(` codex webpresso mcp: ⚠ no install root found (checked ${webpressoMcpResult.checked.length} paths). Install @webpresso/agent-kit globally (\`bun add -g @webpresso/agent-kit\`) or via the Claude plugin to wire up codex MCP.`);
400
- break;
401
- }
384
+ reportLegacyCodexWebpressoMcpPrune(pruneLegacyCodexWebpressoMcp({ options: userScopeOptions }));
402
385
  console.log(" codex project mcp: - skipped (no project .mcp.json outside a repo)");
403
386
  }
404
387
  if (isCiEnvironment) {
@@ -696,6 +679,25 @@ function printCodexPluginResult(result) {
696
679
  break;
697
680
  }
698
681
  }
682
+ function reportLegacyCodexWebpressoMcpPrune(result) {
683
+ switch (result.kind) {
684
+ case "codex-legacy-webpresso-mcp-removed":
685
+ console.log(` codex webpresso mcp: ✓ retired legacy [mcp_servers.webpresso] in ${result.path} (the bundled Codex plugin is the single delivery channel)`);
686
+ break;
687
+ case "codex-legacy-webpresso-mcp-absent":
688
+ console.log(" codex webpresso mcp: ✓ delivered by the bundled Codex plugin");
689
+ break;
690
+ case "codex-legacy-webpresso-mcp-preserved":
691
+ console.warn(` codex webpresso mcp: ⚠ ${result.path} declares a user-authored webpresso server (${result.command ?? "no command"}); left unchanged — remove it manually to use the bundled Codex plugin`);
692
+ break;
693
+ case "codex-legacy-webpresso-mcp-ambiguous":
694
+ console.warn(` codex webpresso mcp: ⚠ ambiguous MCP config at ${result.path}; left unchanged`);
695
+ break;
696
+ case "codex-legacy-webpresso-mcp-skipped-dry-run":
697
+ console.log(" codex webpresso mcp: skipped (--dry-run)");
698
+ break;
699
+ }
700
+ }
699
701
  function reconcileCodexProjectMcpServers(repoRoot, options) {
700
702
  const result = ensureCodexProjectMcpServers({ repoRoot, options });
701
703
  switch (result.kind) {
@@ -1625,29 +1627,14 @@ export async function runInit(flags, deps = {}) {
1625
1627
  ? { kind: "apply" }
1626
1628
  : { kind: "skip", reason: skipGlobal ? "--skip-global" : "user-scope writes declined" });
1627
1629
  }
1628
- // Always upsert webpresso's MCP entry into the user's codex config when
1629
- // an install root is discoverable. Codex's config.toml is user-global, so
1630
- // we resolve to whatever absolute install path exists today (Claude
1631
- // plugin / bun global / pnpm global / npm global) and write that.
1630
+ // Codex receives webpresso over ONE channel: the bundled Codex plugin's
1631
+ // `.mcp.json`. Setup retires the user-global `[mcp_servers.webpresso]`
1632
+ // block earlier versions wrote here rather than refreshing it.
1632
1633
  if (selectedHosts.includes("codex")) {
1633
- const webpressoMcpResult = ensureCodexWebpressoMcp({ options: userScopeOptions });
1634
- switch (webpressoMcpResult.kind) {
1635
- case "codex-webpresso-mcp-written":
1636
- console.log(` codex webpresso mcp: ✓ ${webpressoMcpResult.path} → ${webpressoMcpResult.entryPath}`);
1637
- recordUserScope("codex-config");
1638
- break;
1639
- case "codex-webpresso-mcp-unchanged":
1640
- console.log(` codex webpresso mcp: already configured at ${webpressoMcpResult.path}`);
1641
- break;
1642
- case "codex-webpresso-mcp-ambiguous":
1643
- console.warn(` codex webpresso mcp: ⚠ ambiguous MCP config at ${webpressoMcpResult.path}; left unchanged`);
1644
- break;
1645
- case "codex-webpresso-mcp-skipped-dry-run":
1646
- console.log(" codex webpresso mcp: skipped (--dry-run)");
1647
- break;
1648
- case "codex-webpresso-mcp-not-installed":
1649
- console.log(` codex webpresso mcp: ⚠ no install root found (checked ${webpressoMcpResult.checked.length} paths). Install @webpresso/agent-kit globally (\`bun add -g @webpresso/agent-kit\`) or via the Claude plugin to wire up codex MCP.`);
1650
- break;
1634
+ const pruneResult = pruneLegacyCodexWebpressoMcp({ options: userScopeOptions });
1635
+ reportLegacyCodexWebpressoMcpPrune(pruneResult);
1636
+ if (pruneResult.kind === "codex-legacy-webpresso-mcp-removed") {
1637
+ recordUserScope("codex-config");
1651
1638
  }
1652
1639
  reconcileCodexProjectMcpServers(consumer.repoRoot, userScopeOptions);
1653
1640
  }
@@ -1829,7 +1816,7 @@ export async function runInit(flags, deps = {}) {
1829
1816
  const rtkResult = ensureRtk({ repoRoot: consumer.repoRoot, options: userScopeOptions });
1830
1817
  switch (rtkResult.kind) {
1831
1818
  case "rtk-ok":
1832
- console.log(rtkResult.installed ? " rtk: ✓ installed + configured" : " rtk: ✓");
1819
+ console.log(rtkResult.installed ? " rtk: ✓ managed binary installed" : " rtk: ✓");
1833
1820
  recordUserScope("rtk-install");
1834
1821
  break;
1835
1822
  case "rtk-skipped-dry-run":
@@ -1839,10 +1826,6 @@ export async function runInit(flags, deps = {}) {
1839
1826
  console.error(` rtk: ✗ ${rtkResult.hint}`);
1840
1827
  rtkFailure = "not-found";
1841
1828
  break;
1842
- case "rtk-init-failed":
1843
- console.error(` rtk: ✗ rtk init exited with ${rtkResult.exitCode}`);
1844
- rtkFailure = "init-failed";
1845
- break;
1846
1829
  }
1847
1830
  }
1848
1831
  const all = [
@@ -2039,8 +2022,6 @@ export async function runInit(flags, deps = {}) {
2039
2022
  console.log("\nwp init: setup phases finished.");
2040
2023
  if (rtkFailure === "not-found")
2041
2024
  return EXIT_SETUP_FAIL;
2042
- if (rtkFailure === "init-failed")
2043
- return EXIT_WRITE_FAIL;
2044
2025
  if (isTelemetryEnabled(process.env)) {
2045
2026
  const payload = {
2046
2027
  event: "setup-complete",
@@ -30,7 +30,24 @@ export interface WebpressoInstallProbe {
30
30
  stableEntry?: () => string | null;
31
31
  /** Test seam — verify that a PATH entry is the Webpresso command. */
32
32
  stableEntryIsWebpresso?: (entryPath: string) => boolean;
33
+ /**
34
+ * Test seam — resolve the currently executing agent-kit package root.
35
+ *
36
+ * Without this seam the self-trust branch below is only reachable when the
37
+ * test file itself happens to live in a managed worktree, so the regression
38
+ * guard would silently skip in CI — where the checkout is a plain clone.
39
+ */
40
+ sourceRoot?: () => string | null;
33
41
  }
42
+ /**
43
+ * True when `entryPath` is the stable vite-plus multi-package `wp` shim that
44
+ * dispatches to `@webpresso/agent-kit` via `~/.vite-plus/bins/wp.json`.
45
+ *
46
+ * Unlike {@link isWebpressoWpEntry}, this does **not** require realpath to land
47
+ * inside a package `bin/wp` tree — vite-plus's `wp` is a symlink to the `vp`
48
+ * binary, so package-root ownership is proven via the bins metadata instead.
49
+ */
50
+ export declare function isVitePlusManagedWebpressoWp(entryPath: string): boolean;
34
51
  /**
35
52
  * Resolve the absolute path to webpresso's global `bin/wp` launcher on this
36
53
  * machine. Probes the locations consumers use to install webpresso, in order
@@ -43,25 +60,43 @@ export interface WebpressoInstallProbe {
43
60
  * 3. Claude plugin install — `~/.claude/plugins/cache/.../agent-kit/`
44
61
  * 4. bun global — `~/.bun/install/global/node_modules/@webpresso/agent-kit/`
45
62
  *
46
- * Host MCP configs (Grok/Codex/etc.) must never be pinned to a versioned
63
+ * Host MCP configs (Grok/OpenCode) must never be pinned to a versioned
47
64
  * installId tree: the next `wp update` / `vp update -g` deletes that path and
48
- * leaves every host with a dead launcher. Returns `null` when none of the
49
- * candidates yield a durable `wp`.
65
+ * leaves every host with a dead launcher. `entryPath` is `null` when none of
66
+ * the candidates yield a durable `wp`.
50
67
  */
51
- export declare function findWebpressoMcpEntry(probe?: WebpressoInstallProbe): string | null;
52
- /**
53
- * True when `entryPath` is the stable vite-plus multi-package `wp` shim that
54
- * dispatches to `@webpresso/agent-kit` via `~/.vite-plus/bins/wp.json`.
55
- *
56
- * Unlike {@link isWebpressoWpEntry}, this does **not** require realpath to land
57
- * inside a package `bin/wp` tree — vite-plus's `wp` is a symlink to the `vp`
58
- * binary, so package-root ownership is proven via the bins metadata instead.
59
- */
60
- export declare function isVitePlusManagedWebpressoWp(entryPath: string): boolean;
61
68
  export declare function resolveWebpressoMcpEntry(probe?: WebpressoInstallProbe): {
62
69
  entryPath: string | null;
63
70
  checked: readonly string[];
64
71
  };
72
+ /**
73
+ * Seams for classifying a persisted host-config `command` path as agent-kit's
74
+ * own. Kept separate from {@link WebpressoInstallProbe}: that probe answers
75
+ * "where should we WRITE a launcher", this one answers "did WE write this".
76
+ */
77
+ export interface AgentKitOwnedCommandProbe {
78
+ /** Test seam — override `$HOME`. */
79
+ readonly home?: string;
80
+ /** Test seam — resolve the currently executing agent-kit package root. */
81
+ readonly sourceRoot?: () => string | null;
82
+ /** Test seam — filesystem existence check for the persisted command path. */
83
+ readonly exists?: (path: string) => boolean;
84
+ }
85
+ /**
86
+ * Every root under which agent-kit itself installs (or checks out) a `bin/wp`.
87
+ * A `wp` launcher recorded inside one of these was written by agent-kit; a
88
+ * launcher anywhere else is an operator's deliberate choice.
89
+ */
90
+ export declare function agentKitOwnedCommandRoots(probe?: AgentKitOwnedCommandProbe): readonly string[];
91
+ /**
92
+ * Ownership — not shape — decides whether setup may retire a persisted MCP
93
+ * entry. An entry is agent-kit's own when it launches `wp mcp` from an
94
+ * ABSOLUTE path AND either that path is gone (one we would have deleted) or
95
+ * it lives inside one
96
+ * of {@link agentKitOwnedCommandRoots}. A *live* `wp mcp` launcher outside
97
+ * those roots is an operator pointing Codex at a custom build: preserve it.
98
+ */
99
+ export declare function isAgentKitOwnedWpCommand(command: string, args: readonly string[], probe?: AgentKitOwnedCommandProbe): boolean;
65
100
  export declare function isWebpressoWpEntry(entryPath: string): boolean;
66
101
  /** Run a discovery probe with bounded output and SIGKILL escalation. */
67
102
  export declare function runQuiet(cmd: string, args: readonly string[], timeoutMs?: number): string | null;