@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
@@ -1 +1,23 @@
1
+ /**
2
+ * Redaction for the shared read-only MCP path (`wp_pr_status`, `wp_pr_wait`,
3
+ * `wp_run_wait` error tails, `_readonly-ops`, worker tails, hook logs — 11
4
+ * consumers). Output reaches agent context, session memory, and committed
5
+ * review artifacts in a public repository, so a credential surviving here is
6
+ * irreversible and externally exploitable.
7
+ *
8
+ * Three properties are load-bearing; changing any of them breaks a consumer:
9
+ *
10
+ * 1. {@link mask} is **partial disclosure by design** — first and last two
11
+ * characters survive above six, and the mask is deterministic.
12
+ * `run-wait.ts` redacts both sides of its SHA comparison and relies on
13
+ * identical input producing identical output.
14
+ * 2. The generic 40+-character rule masks full commit SHAs as a side effect,
15
+ * compensated downstream by that same mask-aware equality. Do not "fix" it
16
+ * here without updating those consumers.
17
+ * 3. **Every quantifier is bounded.** Callers pass unbounded log text —
18
+ * `run-wait.ts` redacts the whole tail *before* clipping it to 2000 bytes —
19
+ * so an unbounded `[A-Za-z0-9_]*` around a keyword alternation backtracks
20
+ * quadratically and a hostile CI log can stall the MCP path for tens of
21
+ * seconds. Keep the `{0,64}` bounds.
22
+ */
1
23
  export declare function redactText(value: string | undefined): string | undefined;
@@ -1,22 +1,128 @@
1
- const KEY_PATTERNS = [
2
- /\b(?:GH_PACKAGES_TOKEN|GITHUB_TOKEN|GITHUB_PAT|NEON_API_KEY(?:_PLATFORM)?|CHEF_CI_TOKEN)\b\s*=\s*([^\s]+)/giu,
1
+ /**
2
+ * Redaction for the shared read-only MCP path (`wp_pr_status`, `wp_pr_wait`,
3
+ * `wp_run_wait` error tails, `_readonly-ops`, worker tails, hook logs — 11
4
+ * consumers). Output reaches agent context, session memory, and committed
5
+ * review artifacts in a public repository, so a credential surviving here is
6
+ * irreversible and externally exploitable.
7
+ *
8
+ * Three properties are load-bearing; changing any of them breaks a consumer:
9
+ *
10
+ * 1. {@link mask} is **partial disclosure by design** — first and last two
11
+ * characters survive above six, and the mask is deterministic.
12
+ * `run-wait.ts` redacts both sides of its SHA comparison and relies on
13
+ * identical input producing identical output.
14
+ * 2. The generic 40+-character rule masks full commit SHAs as a side effect,
15
+ * compensated downstream by that same mask-aware equality. Do not "fix" it
16
+ * here without updating those consumers.
17
+ * 3. **Every quantifier is bounded.** Callers pass unbounded log text —
18
+ * `run-wait.ts` redacts the whole tail *before* clipping it to 2000 bytes —
19
+ * so an unbounded `[A-Za-z0-9_]*` around a keyword alternation backtracks
20
+ * quadratically and a hostile CI log can stall the MCP path for tens of
21
+ * seconds. Keep the `{0,64}` bounds.
22
+ */
23
+ /** Key-name fragments that advertise a secret. */
24
+ const SECRET_WORD = "PASSWORD|PASSWD|SECRET|TOKEN|API[_-]?KEY|ACCESS[_-]?KEY|PRIVATE[_-]?KEY|CREDENTIALS?";
25
+ /**
26
+ * Optional opening quote OUTSIDE the capture, then the value up to the first
27
+ * closing delimiter.
28
+ *
29
+ * Both halves matter. Excluding quotes from the class stops a greedy capture
30
+ * pulling the closing quote into the mask (`sk***6"` instead of `sk***56`).
31
+ * Consuming an optional *opening* quote is what keeps `KEY="secret"` matching
32
+ * at all — omitting it makes the `+` match zero characters, the pattern fail,
33
+ * and the most common real-world shape (quoted shell/JSON/YAML values) sail
34
+ * through completely unredacted.
35
+ */
36
+ // `\x60` is the backtick: a literal one cannot appear inside a String.raw
37
+ // template, and `\`` is an invalid escape under the `u` flag.
38
+ const QUOTED_VALUE = String.raw `["'\x60]?([^\s"'\x60,;)\]}\[]+)`;
39
+ /** As above, with a length floor — see the `:` rule below. */
40
+ const QUOTED_VALUE_LONG = String.raw `["'\x60]?([^\s"'\x60,;)\]}\[]{12,})`;
41
+ /**
42
+ * Optional closing quote on the KEY, before the separator. JSON and quoted-key
43
+ * YAML put it there — `{"password":"…"}` — and without this the separator match
44
+ * fails on the key's own closing quote and the whole line sails through.
45
+ */
46
+ const KEY_CLOSE = String.raw `["']?[ \t]*`;
47
+ /**
48
+ * Credential value for the keyword rules (`Bearer`, `Basic`, `Cookie`), which
49
+ * carry no assignment signal at all — and whose keywords are ordinary English
50
+ * words. Without a floor, `Basic checks passed` becomes
51
+ * `Basic [REDACTED] passed`, and that reaches users: `pr-upsert.ts` pushes PR
52
+ * **titles** through this function, so "Add Bearer token support" would render
53
+ * corrupted.
54
+ *
55
+ * So the value must be ≥12 characters AND contain at least one uppercase
56
+ * letter, digit or base64 punctuation — enough to separate `dXNlcjpwYXNzd29yZA==`
57
+ * from `authentication`.
58
+ *
59
+ * The keyword rules therefore drop the `i` flag and spell each casing out.
60
+ * Under `i`, `[A-Z]` also matches lowercase, so this lookahead would be
61
+ * silently satisfied by any long lowercase word and the check would do nothing.
62
+ */
63
+ const KEYWORD_CRED = String.raw `["'\x60]?((?:(?=[^\s"'\x60,;)\]}\[]*[A-Z0-9+/=_-])[^\s"'\x60,;)\]}\[]{12,}|[^\s"'\x60,;)\]}\[]{24,}))`;
64
+ /**
65
+ * Patterns whose **first capture group** is the secret. Only that group is
66
+ * masked, so the header name, config key or URL host stays readable — a
67
+ * redactor that destroys the line it protects gets routed around.
68
+ */
69
+ const CAPTURING_PATTERNS = [
70
+ // Originally-named credential env assignments.
71
+ new RegExp(String.raw `\b(?:GH_PACKAGES_TOKEN|GITHUB_TOKEN|GITHUB_PAT|NEON_API_KEY(?:_PLATFORM)?|CHEF_CI_TOKEN)\b` +
72
+ KEY_CLOSE +
73
+ String.raw `=[ \t]*` +
74
+ QUOTED_VALUE, "giu"),
75
+ // Generic secret-named key with `=`. Assignment is an unambiguous signal, so
76
+ // no length floor: `PASSWORD=x` should mask even when short.
77
+ new RegExp(String.raw `\b[A-Za-z0-9_]{0,64}(?:${SECRET_WORD})[A-Za-z0-9_]{0,64}` +
78
+ KEY_CLOSE +
79
+ String.raw `=[ \t]*` +
80
+ QUOTED_VALUE, "giu"),
81
+ // Generic secret-named key with `:`. Deliberately stricter than `=`, because
82
+ // `:` is overloaded across YAML, tables, markdown and plain prose — without a
83
+ // floor this masks `tokens: 512`, `secret: true`, `credentials: verified for
84
+ // 3 users` and `**Token:** an opaque handle`, corrupting ordinary CI output.
85
+ // A 12-character floor admits real secrets while excluding those.
86
+ new RegExp(String.raw `\b[A-Za-z0-9_]{0,64}(?:${SECRET_WORD})[A-Za-z0-9_]{0,64}` +
87
+ KEY_CLOSE +
88
+ String.raw `:[ \t]*` +
89
+ QUOTED_VALUE_LONG, "giu"),
90
+ // `Authorization: Bearer <token>` / bare `Bearer <token>`. Casings spelled
91
+ // out because `KEYWORD_CRED`'s lookahead is case-sensitive by necessity.
92
+ new RegExp(String.raw `\b(?:Bearer|bearer|BEARER)\s+` + KEYWORD_CRED, "gu"),
93
+ // `Authorization: Basic <base64>` — same header family, and the base64 is
94
+ // usually under the generic 40-character floor.
95
+ new RegExp(String.raw `\b(?:Basic|basic|BASIC)\s+` + KEYWORD_CRED, "gu"),
96
+ // Session cookies: the cookie NAME (`session`, `sid`) never matches a
97
+ // secret-word rule, so the value needs its own case or session tokens ride
98
+ // through CI and proxy logs intact. Anchoring on `name=` both avoids matching
99
+ // prose after a bare `Cookie:` and keeps the cookie name readable in output.
100
+ new RegExp(String.raw `\b(?:Set-|set-|SET-)?(?:Cookie|cookie|COOKIE)\s*:\s*[^\s=;]{1,64}=` + KEYWORD_CRED, "gu"),
101
+ // Credential-bearing URL userinfo: `https://user:pass@host/…`. Masking the
102
+ // whole userinfo keeps the host, which is what makes the log useful.
103
+ /:\/\/([^\s/@]+)@/gu,
3
104
  ];
4
- const TOKEN_PATTERNS = [
105
+ /** Patterns where the entire match is the secret. */
106
+ const WHOLE_MATCH_PATTERNS = [
5
107
  /\bghp_[A-Za-z0-9]{20,}\b/gu,
6
108
  /\b(?:cf|sk|tok)_[A-Za-z0-9_-]{16,}\b/gu,
109
+ // JWTs, whose segments routinely fall under the generic 40-character floor.
110
+ /\beyJ[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\b/gu,
7
111
  /\b[A-Za-z0-9+/_-]{40,}\b/gu,
8
112
  ];
9
113
  export function redactText(value) {
10
114
  if (!value)
11
115
  return value;
12
116
  let output = value;
13
- for (const pattern of KEY_PATTERNS) {
14
- output = output.replace(pattern, (whole, token) => {
15
- const trimmed = String(token ?? "").trim();
16
- return whole.includes("=") ? whole.replace(trimmed, mask(trimmed)) : mask(trimmed);
117
+ for (const pattern of CAPTURING_PATTERNS) {
118
+ output = output.replace(pattern, (whole, secret) => {
119
+ const trimmed = String(secret ?? "").trim();
120
+ if (trimmed.length === 0)
121
+ return whole;
122
+ return whole.replace(trimmed, mask(trimmed));
17
123
  });
18
124
  }
19
- for (const pattern of TOKEN_PATTERNS) {
125
+ for (const pattern of WHOLE_MATCH_PATTERNS) {
20
126
  output = output.replace(pattern, (token) => mask(token));
21
127
  }
22
128
  return output;
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { resolveManagedRtkExecutable } from "#tool-runtime/managed-rtk.js";
2
3
  import { createSummaryOutputSchema, createSummaryResult } from "./_shared/result.js";
3
4
  import { readonlyOpsBaseSchema, resolveReadonlyCwd, runReadonlyCommand } from "./_readonly-ops.js";
4
5
  const inputSchema = readonlyOpsBaseSchema
@@ -19,8 +20,12 @@ const outputSchema = createSummaryOutputSchema({
19
20
  }),
20
21
  });
21
22
  function commandFor(input) {
22
- if (input.source === "rtk")
23
- return { command: "rtk", args: ["gain", "--format", "json"] };
23
+ if (input.source === "rtk") {
24
+ const rtkBin = resolveManagedRtkExecutable();
25
+ if (!rtkBin)
26
+ return null;
27
+ return { command: rtkBin, args: ["gain", "--format", "json"] };
28
+ }
24
29
  return { command: "./bin/wp", args: input.sessions ? ["gain", "--sessions"] : ["gain"] };
25
30
  }
26
31
  const tool = {
@@ -39,6 +44,27 @@ const tool = {
39
44
  const input = inputSchema.parse(raw ?? {});
40
45
  const cwd = resolveReadonlyCwd(input);
41
46
  const command = commandFor(input);
47
+ if (command === null) {
48
+ return createSummaryResult({
49
+ passed: false,
50
+ summary: "managed RTK not available (wp setup --with rtk or WP_RTK_BIN)",
51
+ exitCode: 1,
52
+ counts: { commandCount: 0, passedCount: 0, failedCount: 1 },
53
+ details: {
54
+ cwd,
55
+ source: input.source,
56
+ format: input.format,
57
+ sessions: input.sessions,
58
+ command: {},
59
+ result: { error: "managed_rtk_missing" },
60
+ },
61
+ rawOutput: "",
62
+ truncated: false,
63
+ timedOut: false,
64
+ aborted: false,
65
+ warnings: ["managed RTK binary missing"],
66
+ });
67
+ }
42
68
  const result = await runReadonlyCommand("gain", command.command, command.args, {
43
69
  cwd,
44
70
  timeoutMs: input.timeoutMs,
@@ -0,0 +1,9 @@
1
+ import type { ToolDescriptor } from "#mcp/auto-discover";
2
+ import { type RunReleaseProgressOptions } from "#cli/commands/release-progress.js";
3
+ export type ReleaseProgressToolDeps = Pick<RunReleaseProgressOptions, "fetchJobs" | "resolveLatestRunId">;
4
+ /**
5
+ * Build the tool descriptor. Tests inject gh seams; production uses defaults.
6
+ */
7
+ export declare function createReleaseProgressTool(deps?: ReleaseProgressToolDeps): ToolDescriptor;
8
+ declare const tool: ToolDescriptor;
9
+ export default tool;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * `wp_release_progress` — read-only Release workflow phase reporter for agents.
3
+ *
4
+ * Shares {@link runReleaseProgress} with the CLI so MCP and `wp release-progress`
5
+ * never diverge. Does not mutate GitHub; only reads via `gh`.
6
+ */
7
+ import { z } from "zod";
8
+ import { runReleaseProgress, } from "#cli/commands/release-progress.js";
9
+ import { createSummaryOutputSchema, createSummaryResult } from "./_shared/result.js";
10
+ import { readonlyOpsBaseSchema } from "./_readonly-ops.js";
11
+ const inputSchema = readonlyOpsBaseSchema
12
+ .extend({
13
+ runId: z.union([z.string().min(1), z.number().int().positive()]).optional(),
14
+ repo: z.string().min(1).optional(),
15
+ })
16
+ .strict();
17
+ const releaseProgressDetailsSchema = z
18
+ .object({
19
+ runId: z.string(),
20
+ repo: z.string(),
21
+ phase: z.enum(["detect", "native-matrix", "publish", "done", "failed"]),
22
+ detail: z.string(),
23
+ native: z.object({
24
+ done: z.number().int().nonnegative(),
25
+ total: z.number().int().nonnegative(),
26
+ failed: z.number().int().nonnegative(),
27
+ inProgress: z.number().int().nonnegative(),
28
+ }),
29
+ publishRunning: z.boolean(),
30
+ publishDone: z.boolean(),
31
+ publishFailed: z.boolean(),
32
+ line: z.string(),
33
+ })
34
+ .strict();
35
+ const outputSchema = createSummaryOutputSchema({
36
+ details: releaseProgressDetailsSchema,
37
+ });
38
+ /**
39
+ * Build the tool descriptor. Tests inject gh seams; production uses defaults.
40
+ */
41
+ export function createReleaseProgressTool(deps = {}) {
42
+ return {
43
+ name: "wp_release_progress",
44
+ description: "Read-only GitHub Actions Release workflow phase reporter (detect | native-matrix | publish | done | failed) with session-memory native matrix counts. Use instead of silent npm view polls after Version Packages merge. Does not mutate GitHub. Prefer this over shelling `wp release-progress` when available.",
45
+ inputSchema,
46
+ outputSchema,
47
+ annotations: {
48
+ title: "Release progress",
49
+ readOnlyHint: true,
50
+ destructiveHint: false,
51
+ idempotentHint: true,
52
+ openWorldHint: true,
53
+ },
54
+ handler: async (raw) => {
55
+ const input = inputSchema.parse(raw ?? {});
56
+ const repo = input.repo ?? "webpresso/agent-kit";
57
+ try {
58
+ const result = runReleaseProgress({
59
+ runId: input.runId,
60
+ repo,
61
+ json: false,
62
+ fetchJobs: deps.fetchJobs,
63
+ resolveLatestRunId: deps.resolveLatestRunId,
64
+ // Suppress CLI text I/O; structuredContent is the MCP contract.
65
+ stdout: { write: () => undefined },
66
+ stderr: { write: () => undefined },
67
+ });
68
+ const { progress, runId, line } = result;
69
+ const failed = progress.phase === "failed";
70
+ return createSummaryResult({
71
+ passed: !failed,
72
+ summary: line,
73
+ details: {
74
+ runId,
75
+ repo,
76
+ phase: progress.phase,
77
+ detail: progress.detail,
78
+ native: progress.native,
79
+ publishRunning: progress.publishRunning,
80
+ publishDone: progress.publishDone,
81
+ publishFailed: progress.publishFailed,
82
+ line,
83
+ },
84
+ warnings: failed ? [progress.detail] : [],
85
+ }, { isError: failed });
86
+ }
87
+ catch (error) {
88
+ const message = error instanceof Error ? error.message : String(error);
89
+ return createSummaryResult({
90
+ passed: false,
91
+ summary: `release-progress failed: ${message}`,
92
+ details: {
93
+ runId: input.runId === undefined ? "" : String(input.runId),
94
+ repo,
95
+ phase: "failed",
96
+ detail: message,
97
+ native: { done: 0, total: 0, failed: 0, inProgress: 0 },
98
+ publishRunning: false,
99
+ publishDone: false,
100
+ publishFailed: false,
101
+ line: `release-progress phase=failed native=0/0 ${message}`,
102
+ },
103
+ warnings: [message],
104
+ }, { isError: true });
105
+ }
106
+ },
107
+ };
108
+ }
109
+ const tool = createReleaseProgressTool();
110
+ export default tool;
@@ -15,6 +15,7 @@ import { gitBranchExists, listEntries, resolveNewWorktreeTarget, resolveWorktree
15
15
  import { deriveRepoNamespace, resolveKnownManagedWorktreeRoots, resolveWorktreeRoot, } from "#worktrees/location.js";
16
16
  import { readRepoOriginUrl, repoManagedRoot } from "#worktrees/manager.js";
17
17
  import { buildWorktreeInventory, canonicalizeWorktreePath, isManagedWorktreePath, resolveRepoIdentity, } from "#worktrees/identity.js";
18
+ import { isStaleWorktreeRegistration, pruneStaleGitWorktreeMetadata, } from "#worktrees/git-metadata.js";
18
19
  import { findRegistryCandidatesByPath, pruneStaleWorktreeRegistryEntries, removeWorktreeRegistryEntries, repoScopedPathPredicate, upsertWorktreeRegistryEntry, } from "#worktrees/registry.js";
19
20
  const ACTIONS = ["list", "root", "new", "remove", "prune", "refresh"];
20
21
  const MUTATING_ACTIONS = new Set(["new", "remove", "prune", "refresh"]);
@@ -105,6 +106,53 @@ function isRegisteredManagedWorktree(resolvedPath, _context) {
105
106
  // a legacy-root worktree registered by an earlier binary authorizes removal.
106
107
  return findRegistryCandidatesByPath(canonicalizeWorktreePath(resolvedPath)).length > 0;
107
108
  }
109
+ /**
110
+ * A stale registration may already have lost its registry entry to an earlier
111
+ * prune, so registration alone cannot gate reconciling it. Fall back to the
112
+ * managed-root boundary, which still keeps unmanaged worktrees protected.
113
+ */
114
+ function isRemovableManagedTarget(resolved, context, stale) {
115
+ if (isRegisteredManagedWorktree(resolved, context))
116
+ return true;
117
+ if (!stale)
118
+ return false;
119
+ return isManagedWorktreePath(canonicalizeWorktreePath(resolved), resolveKnownManagedWorktreeRoots());
120
+ }
121
+ /** Reconcile git + registry for a worktree whose directory is already gone. */
122
+ function clearStaleRegistration(input, repoRoot, resolved, entries, entry) {
123
+ const gitPruned = pruneStaleGitWorktreeMetadata(repoRoot, entries);
124
+ if (gitPruned.error) {
125
+ return {
126
+ passed: false,
127
+ summary: `wp_worktree remove failed: git worktree prune failed: ${gitPruned.error}`,
128
+ action: input.action,
129
+ executed: false,
130
+ warnings: ["git_worktree_prune_failed"],
131
+ nextAction: "Inspect the repository's git worktree admin records and retry.",
132
+ };
133
+ }
134
+ const warnings = [];
135
+ try {
136
+ removeWorktreeRegistryEntries(repoScopedPathPredicate(resolved, { authoritativeRepoKey: resolveRepoIdentity(repoRoot) }));
137
+ }
138
+ catch {
139
+ warnings.push("registry_cleanup_failed");
140
+ }
141
+ return {
142
+ passed: true,
143
+ summary: `Cleared stale worktree registration ${resolved} (directory already gone)`,
144
+ action: input.action,
145
+ executed: true,
146
+ removed: {
147
+ path: resolved,
148
+ ...(entry?.branch ? { branch: entry.branch.replace("refs/heads/", "") } : {}),
149
+ },
150
+ warnings,
151
+ ...(warnings.includes("registry_cleanup_failed")
152
+ ? { nextAction: "Run wp_worktree prune with execute:true to repair stale registry metadata." }
153
+ : {}),
154
+ };
155
+ }
108
156
  function refreshManagedEntries(repoRoot) {
109
157
  const { originUrl, repoNamespace } = managedContext(repoRoot);
110
158
  const knownRoots = resolveKnownManagedWorktreeRoots();
@@ -234,6 +282,7 @@ function handleRemove(input, repoRoot) {
234
282
  }
235
283
  const entry = entries.find((candidate) => candidate.path === resolved);
236
284
  const context = managedContext(repoRoot);
285
+ const stale = isStaleWorktreeRegistration(entry, resolved, existsSync);
237
286
  if (resolved === repoRoot) {
238
287
  return {
239
288
  passed: false,
@@ -243,7 +292,7 @@ function handleRemove(input, repoRoot) {
243
292
  warnings: ["main_checkout_protected"],
244
293
  };
245
294
  }
246
- if (!isRegisteredManagedWorktree(resolved, context)) {
295
+ if (!isRemovableManagedTarget(resolved, context, stale)) {
247
296
  return {
248
297
  passed: false,
249
298
  summary: `wp_worktree remove refused: ${resolved} is not a registered managed worktree for this repository`,
@@ -263,6 +312,10 @@ function handleRemove(input, repoRoot) {
263
312
  nextAction: "Unlock the worktree explicitly with git after confirming it is safe, then retry.",
264
313
  };
265
314
  }
315
+ // Checked before the dirty gate: `git status` fails in a missing directory,
316
+ // which would otherwise be reported as "uncommitted changes".
317
+ if (stale)
318
+ return clearStaleRegistration(input, repoRoot, resolved, entries, entry);
266
319
  if (isDirty(resolved)) {
267
320
  return {
268
321
  passed: false,
@@ -368,6 +421,10 @@ const tool = {
368
421
  warnings: [],
369
422
  }, false);
370
423
  }
424
+ // Reconcile git's own admin records FIRST: a hand-deleted worktree
425
+ // directory leaves a record that still claims its branch, and it would
426
+ // otherwise read as live below and pin its registry entry in place too.
427
+ const gitPruned = pruneStaleGitWorktreeMetadata(repoRoot, listEntries(repoRoot));
371
428
  // Repository-scoped, three-state prune across every known root: only THIS
372
429
  // repo's entries proven stale against a COMPLETE git-live inventory drop;
373
430
  // unknown, other-repo, and live-legacy entries are retained. `existsSync`
@@ -377,12 +434,17 @@ const tool = {
377
434
  inventory,
378
435
  scope: { authoritativeRepoKey: resolveRepoIdentity(repoRoot) },
379
436
  });
437
+ const gitSummary = gitPruned.error
438
+ ? ""
439
+ : ` and ${gitPruned.prunedPaths.length} stale git worktree registration${gitPruned.prunedPaths.length === 1 ? "" : "s"}`;
380
440
  return result({
381
441
  passed: true,
382
- summary: `Pruned ${pruned.removed.length} stale managed registry entr${pruned.removed.length === 1 ? "y" : "ies"}`,
442
+ summary: `Pruned ${pruned.removed.length} stale managed registry entr${pruned.removed.length === 1 ? "y" : "ies"}${gitSummary}`,
383
443
  action: input.action,
384
444
  executed: true,
385
- warnings: pruned.warnings,
445
+ warnings: gitPruned.error
446
+ ? [...pruned.warnings, `git_worktree_prune_failed:${gitPruned.error}`]
447
+ : pruned.warnings,
386
448
  }, false);
387
449
  }
388
450
  catch (error) {
@@ -0,0 +1,3 @@
1
+ import type { ToolDescriptor } from "#mcp/auto-discover";
2
+ declare const tool: ToolDescriptor;
3
+ export default tool;
@@ -0,0 +1,43 @@
1
+ import { z } from "zod";
2
+ import { resolveBlockRegistry } from "#compose/registry-resolve.js";
3
+ const inputSchema = z
4
+ .object({
5
+ cwd: z.string().optional(),
6
+ profile: z.enum(["admin-crud", "ops-report"]).optional(),
7
+ })
8
+ .strict();
9
+ const tool = {
10
+ name: "wp_ui_blocks_list",
11
+ description: "List the agent UI block catalog for composition. Resolves project `.webpresso/generated/frontend/blocks/registry.json` when present, otherwise the offline catalog snapshot. Always returns source (project|snapshot), registryVersion, and warnings (e.g. snapshot used inside a webpresso tree without a generated registry).",
12
+ inputSchema,
13
+ annotations: {
14
+ title: "UI blocks list",
15
+ readOnlyHint: true,
16
+ destructiveHint: false,
17
+ idempotentHint: true,
18
+ openWorldHint: false,
19
+ },
20
+ handler: async (raw) => {
21
+ const input = inputSchema.parse(raw ?? {});
22
+ const resolved = resolveBlockRegistry({ cwd: input.cwd });
23
+ // admin-crud: full registry (list/detail/form). ops-report: no admin blocks.
24
+ const blocks = input.profile === "ops-report" ? [] : resolved.registry.blocks;
25
+ const summary = `UI blocks: ${blocks.length} (source=${resolved.source}, version=${resolved.registryVersion})`;
26
+ const payload = {
27
+ passed: true,
28
+ summary,
29
+ source: resolved.source,
30
+ registryVersion: resolved.registryVersion,
31
+ path: resolved.path,
32
+ warnings: [...resolved.warnings],
33
+ profiles: ["admin-crud", "ops-report"],
34
+ blocks,
35
+ webpressoPresentWithoutRegistry: resolved.webpressoPresentWithoutRegistry,
36
+ };
37
+ return {
38
+ content: [{ type: "text", text: summary }],
39
+ structuredContent: payload,
40
+ };
41
+ },
42
+ };
43
+ export default tool;
@@ -0,0 +1,3 @@
1
+ import type { ToolDescriptor } from "#mcp/auto-discover";
2
+ declare const tool: ToolDescriptor;
3
+ export default tool;
@@ -0,0 +1,96 @@
1
+ import { z } from "zod";
2
+ import { composeValidate } from "#compose/compose.js";
3
+ const inputSchema = z
4
+ .object({
5
+ cwd: z.string().optional(),
6
+ profile: z.enum(["admin-crud", "ops-report"]),
7
+ kind: z.enum(["list", "detail", "form"]).optional(),
8
+ spec: z.unknown(),
9
+ })
10
+ .strict()
11
+ .superRefine((value, ctx) => {
12
+ if (value.profile === "admin-crud" && value.kind === undefined) {
13
+ ctx.addIssue({
14
+ code: "custom",
15
+ message: "kind is required for admin-crud (list|detail|form)",
16
+ path: ["kind"],
17
+ });
18
+ }
19
+ });
20
+ const tool = {
21
+ name: "wp_ui_compose",
22
+ description: "Validate a profile-discriminated UI compose input. admin-crud: validate list/detail/form specs against the block registry (returns input spec unchanged when legal). ops-report: validate flat OpsReportSpec. Does not write files or open a browser.",
23
+ inputSchema,
24
+ annotations: {
25
+ title: "UI compose validate",
26
+ readOnlyHint: true,
27
+ destructiveHint: false,
28
+ idempotentHint: true,
29
+ openWorldHint: false,
30
+ },
31
+ handler: async (raw) => {
32
+ const parsed = inputSchema.safeParse(raw ?? {});
33
+ if (!parsed.success) {
34
+ const summary = `Invalid wp_ui_compose input: ${parsed.error.issues.map((i) => i.message).join("; ")}`;
35
+ return {
36
+ content: [{ type: "text", text: summary }],
37
+ structuredContent: {
38
+ passed: false,
39
+ summary,
40
+ ok: false,
41
+ diagnostics: parsed.error.issues.map((i) => ({
42
+ path: i.path.length > 0 ? i.path.join(".") : "(root)",
43
+ message: i.message,
44
+ })),
45
+ },
46
+ isError: true,
47
+ };
48
+ }
49
+ const input = parsed.data;
50
+ if (input.profile === "admin-crud") {
51
+ if (input.spec === null || typeof input.spec !== "object" || Array.isArray(input.spec)) {
52
+ const summary = "Invalid wp_ui_compose admin-crud spec: expected a non-null object (not string/array/empty coercion)";
53
+ return {
54
+ content: [{ type: "text", text: summary }],
55
+ structuredContent: {
56
+ passed: false,
57
+ summary,
58
+ ok: false,
59
+ diagnostics: [{ path: "spec", message: summary }],
60
+ },
61
+ isError: true,
62
+ };
63
+ }
64
+ }
65
+ const composeInput = input.profile === "admin-crud"
66
+ ? {
67
+ profile: "admin-crud",
68
+ kind: input.kind,
69
+ spec: input.spec,
70
+ }
71
+ : { profile: "ops-report", spec: input.spec };
72
+ const result = composeValidate(composeInput, { cwd: input.cwd });
73
+ const summary = result.ok
74
+ ? `compose ok (${result.profile})`
75
+ : `compose failed (${result.profile}): ${result.diagnostics.map((d) => d.message).join("; ")}`;
76
+ // MCP tool execution errors (incl. domain validation) use isError:true so
77
+ // models self-correct; diagnostics stay in content text + structuredContent.
78
+ return {
79
+ content: [{ type: "text", text: summary }],
80
+ structuredContent: {
81
+ passed: result.ok,
82
+ summary,
83
+ profile: result.profile,
84
+ ok: result.ok,
85
+ spec: result.spec,
86
+ diagnostics: result.diagnostics,
87
+ registrySource: result.registrySource,
88
+ registryVersion: result.registryVersion,
89
+ warnings: result.warnings,
90
+ blockTypesValidated: result.blockTypesValidated,
91
+ },
92
+ ...(result.ok ? {} : { isError: true }),
93
+ };
94
+ },
95
+ };
96
+ export default tool;
@@ -0,0 +1,3 @@
1
+ import type { ToolDescriptor } from "#mcp/auto-discover";
2
+ declare const tool: ToolDescriptor;
3
+ export default tool;
@@ -0,0 +1,59 @@
1
+ import { z } from "zod";
2
+ import { previewOpsReport } from "#compose/preview.js";
3
+ const inputSchema = z
4
+ .object({
5
+ cwd: z.string().optional(),
6
+ profile: z.literal("ops-report"),
7
+ spec: z.unknown(),
8
+ })
9
+ .strict();
10
+ const tool = {
11
+ name: "wp_ui_preview",
12
+ description: "Validate an ops-report compose spec, write self-contained HTML under the worktree state surface (or .webpresso/compose fallback), and return { path } only. Does not open a browser. admin-crud is not supported for preview.",
13
+ inputSchema,
14
+ annotations: {
15
+ title: "UI ops-report preview",
16
+ readOnlyHint: false,
17
+ destructiveHint: false,
18
+ idempotentHint: true,
19
+ openWorldHint: false,
20
+ },
21
+ handler: async (raw) => {
22
+ const parsed = inputSchema.safeParse(raw ?? {});
23
+ if (!parsed.success) {
24
+ const message = `Invalid wp_ui_preview input: ${parsed.error.issues.map((i) => i.message).join("; ")} (ops-report profile only; admin-crud is not supported)`;
25
+ return {
26
+ content: [{ type: "text", text: message }],
27
+ structuredContent: {
28
+ passed: false,
29
+ summary: message,
30
+ },
31
+ isError: true,
32
+ };
33
+ }
34
+ try {
35
+ const { path } = previewOpsReport({ cwd: parsed.data.cwd, spec: parsed.data.spec });
36
+ const summary = `ops-report preview written: ${path}`;
37
+ return {
38
+ content: [{ type: "text", text: summary }],
39
+ structuredContent: {
40
+ passed: true,
41
+ summary,
42
+ path,
43
+ },
44
+ };
45
+ }
46
+ catch (error) {
47
+ const message = error instanceof Error ? error.message : String(error);
48
+ return {
49
+ content: [{ type: "text", text: message }],
50
+ structuredContent: {
51
+ passed: false,
52
+ summary: message,
53
+ },
54
+ isError: true,
55
+ };
56
+ }
57
+ },
58
+ };
59
+ export default tool;