@webpresso/agent-kit 3.2.0 → 3.3.1

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 (103) hide show
  1. package/bin/_run.js +4 -1
  2. package/catalog/agent/rules/pre-implementation.md +15 -0
  3. package/dist/esm/audit/blueprint-trust.js +14 -10
  4. package/dist/esm/audit/changeset-evidence.d.ts +23 -0
  5. package/dist/esm/audit/changeset-evidence.js +30 -0
  6. package/dist/esm/audit/changeset-required.d.ts +10 -0
  7. package/dist/esm/audit/changeset-required.js +73 -0
  8. package/dist/esm/audit/ci-guardrails-detection.d.ts +27 -0
  9. package/dist/esm/audit/ci-guardrails-detection.js +68 -0
  10. package/dist/esm/audit/ci-guardrails-wiring.js +3 -13
  11. package/dist/esm/audit/registry.d.ts +4 -4
  12. package/dist/esm/audit/registry.js +6 -1
  13. package/dist/esm/blueprint/core/parser.js +61 -4
  14. package/dist/esm/blueprint/core/schema.d.ts +1 -0
  15. package/dist/esm/blueprint/core/schema.js +26 -0
  16. package/dist/esm/blueprint/lifecycle/audit.js +22 -10
  17. package/dist/esm/blueprint/lifecycle/review-provenance.d.ts +6 -0
  18. package/dist/esm/blueprint/lifecycle/review-provenance.js +39 -8
  19. package/dist/esm/blueprint/lifecycle/rollout-anchor.d.ts +97 -0
  20. package/dist/esm/blueprint/lifecycle/rollout-anchor.js +161 -0
  21. package/dist/esm/blueprint/sync/client.d.ts +2 -1
  22. package/dist/esm/blueprint/sync/client.js +1 -1
  23. package/dist/esm/blueprint/trust/dossier.d.ts +1 -0
  24. package/dist/esm/blueprint/trust/dossier.js +40 -6
  25. package/dist/esm/blueprint/trust/promotion.js +12 -0
  26. package/dist/esm/build/cli-mcp-parity.js +5 -0
  27. package/dist/esm/cli/cli.d.ts +1 -1
  28. package/dist/esm/cli/cli.js +9 -0
  29. package/dist/esm/cli/commands/agent-launch.d.ts +2 -0
  30. package/dist/esm/cli/commands/agent-launch.js +31 -20
  31. package/dist/esm/cli/commands/audit.js +4 -0
  32. package/dist/esm/cli/commands/blueprint/abandon.d.ts +21 -0
  33. package/dist/esm/cli/commands/blueprint/abandon.js +178 -0
  34. package/dist/esm/cli/commands/blueprint/router-dispatch.js +21 -0
  35. package/dist/esm/cli/commands/blueprint/router-output.js +1 -0
  36. package/dist/esm/cli/commands/blueprint/router.d.ts +2 -0
  37. package/dist/esm/cli/commands/blueprint/router.js +5 -2
  38. package/dist/esm/cli/commands/ci-preflight.js +24 -3
  39. package/dist/esm/cli/commands/init/convergence-apply.d.ts +87 -0
  40. package/dist/esm/cli/commands/init/convergence-apply.js +189 -23
  41. package/dist/esm/cli/commands/init/index.d.ts +2 -0
  42. package/dist/esm/cli/commands/init/index.js +2 -2
  43. package/dist/esm/cli/commands/init/package-root.d.ts +7 -0
  44. package/dist/esm/cli/commands/init/package-root.js +10 -2
  45. package/dist/esm/cli/commands/init/scaffold-base-kit.d.ts +12 -3
  46. package/dist/esm/cli/commands/init/scaffold-base-kit.js +31 -30
  47. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.js +3 -1
  48. package/dist/esm/cli/commands/init/scaffolders/file-transaction.d.ts +17 -2
  49. package/dist/esm/cli/commands/init/scaffolders/file-transaction.js +189 -17
  50. package/dist/esm/cli/commands/opencode-probe.d.ts +118 -0
  51. package/dist/esm/cli/commands/opencode-probe.js +323 -0
  52. package/dist/esm/cli/commands/opencode-rotate.d.ts +23 -0
  53. package/dist/esm/cli/commands/opencode-rotate.js +23 -11
  54. package/dist/esm/cli/commands/quality-runner.js +8 -4
  55. package/dist/esm/cli/commands/review.js +27 -5
  56. package/dist/esm/cli/commands/rust-check.d.ts +24 -0
  57. package/dist/esm/cli/commands/rust-check.js +110 -0
  58. package/dist/esm/cli/commands/typecheck.d.ts +1 -0
  59. package/dist/esm/cli/commands/typecheck.js +30 -1
  60. package/dist/esm/cli/commands/worktree/router-dispatch.d.ts +14 -0
  61. package/dist/esm/cli/commands/worktree/router-dispatch.js +138 -2
  62. package/dist/esm/cli/commands/worktree/router.js +10 -0
  63. package/dist/esm/hooks/doctor.d.ts +2 -0
  64. package/dist/esm/hooks/doctor.js +37 -1
  65. package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.js +6 -1
  66. package/dist/esm/hooks/shared/types.js +27 -1
  67. package/dist/esm/hooks/stop/qa-changed-files.js +18 -0
  68. package/dist/esm/mcp/blueprint/handlers/document-mutations.d.ts +5 -0
  69. package/dist/esm/mcp/blueprint/handlers/document-mutations.js +31 -7
  70. package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
  71. package/dist/esm/mcp/tools/audit.d.ts +2 -1
  72. package/dist/esm/mcp/tools/audit.js +12 -0
  73. package/dist/esm/mcp/tools/audits.d.ts +1 -0
  74. package/dist/esm/mcp/tools/session-fetch-and-index.d.ts +1 -1
  75. package/dist/esm/package.json +2 -0
  76. package/dist/esm/platform/client.d.ts +7 -1
  77. package/dist/esm/platform/github-contract-source.d.ts +2 -1
  78. package/dist/esm/pr-description/contract.js +24 -4
  79. package/dist/esm/review/authority.js +14 -9
  80. package/dist/esm/review/availability.d.ts +11 -0
  81. package/dist/esm/review/availability.js +18 -3
  82. package/dist/esm/review/execution/adapters.d.ts +11 -1
  83. package/dist/esm/review/execution/adapters.js +10 -1
  84. package/dist/esm/review/execution/artifacts.d.ts +1 -1
  85. package/dist/esm/review/execution/review-checkout.d.ts +6 -0
  86. package/dist/esm/review/execution/review-checkout.js +61 -16
  87. package/dist/esm/review/execution/supervisor.js +31 -4
  88. package/dist/esm/review/execution/types.d.ts +1 -0
  89. package/dist/esm/review/subject.d.ts +13 -0
  90. package/dist/esm/review/subject.js +42 -4
  91. package/dist/esm/rust/affected-manifests.d.ts +38 -0
  92. package/dist/esm/rust/affected-manifests.js +69 -0
  93. package/dist/esm/session-memory/fetch-index.d.ts +1 -1
  94. package/dist/esm/status/snapshot.js +2 -6
  95. package/dist/esm/test-helpers/global-setup.d.ts +0 -1
  96. package/dist/esm/test-helpers/global-setup.js +18 -15
  97. package/dist/esm/typecheck/planner.d.ts +9 -1
  98. package/dist/esm/typecheck/planner.js +31 -0
  99. package/dist/esm/worktrees/orphan-scan.d.ts +50 -0
  100. package/dist/esm/worktrees/orphan-scan.js +211 -0
  101. package/dist/esm/worktrees/owner-dirt.d.ts +40 -0
  102. package/dist/esm/worktrees/owner-dirt.js +104 -0
  103. package/package.json +14 -12
@@ -8,6 +8,10 @@ const HELP_TEXT = [
8
8
  " list [--all] List repo or global managed worktrees",
9
9
  " refresh [--all|--repo <dir>] Refresh managed registry from git",
10
10
  " prune --all Prune stale managed registry entries",
11
+ " prune --orphans [--repo-id <id>] [--force|--yes]",
12
+ " Scan managed roots for on-disk dirs absent",
13
+ " from git worktree list; dry-run by default,",
14
+ " removes only proven-clean orphans with --force",
11
15
  " migrate Move legacy sibling _worktrees",
12
16
  " adopt <blueprint-slug> <path> Claim existing checkout as owner",
13
17
  " rebind <blueprint-slug> [--path <path>] Repair owner metadata",
@@ -24,6 +28,9 @@ const HELP_TEXT = [
24
28
  " --no-setup Skip configured worktree.setup scripts (new only)",
25
29
  " --force Skip configured teardown scripts and force removal",
26
30
  " --stash-primary Stash dirty primary state and switch to the expected primary branch before merge-cleanup",
31
+ " --orphans Filesystem-level orphan sweep for prune (see prune --orphans above)",
32
+ " --repo-id <id> Scope prune --orphans to one repo namespace under repos/<id>",
33
+ " --yes Alias for --force (prune --orphans only)",
27
34
  " --cwd <dir> Repo root (default: process.cwd())",
28
35
  ].join("\n");
29
36
  export function registerWorktreeRouter(cli) {
@@ -40,6 +47,9 @@ export function registerWorktreeRouter(cli) {
40
47
  .option("--stash-primary", "Stash dirty primary state and switch to the expected primary branch before merge-cleanup")
41
48
  .option("--all", "Use global managed inventory where supported")
42
49
  .option("--repo <dir>", "Repo root for refresh/rebind (default: cwd)")
50
+ .option("--orphans", "Filesystem-level orphan sweep for prune (dry-run unless --force/--yes)")
51
+ .option("--repo-id <id>", "Scope prune --orphans to one repo namespace under repos/<id>")
52
+ .option("--yes", "Alias for --force (prune --orphans only)")
43
53
  .option("--cwd <dir>", "Repo root to operate from (default: process.cwd())")
44
54
  .action(async (subcommand, args, options) => {
45
55
  if (!subcommand) {
@@ -118,6 +118,8 @@ export declare function checkCodexConfigKeys(configFilePath?: string, deps?: Hos
118
118
  export declare function checkProjectMcpPins(cwd?: string, deps?: HostConfigDoctorDeps): DoctorCheck;
119
119
  export interface ClaudeMcpDuplicateRegistrationOptions {
120
120
  readonly home?: string;
121
+ /** Project root used to discover a project-local `.mcp.json` registration. */
122
+ readonly projectRoot?: string;
121
123
  readonly exists?: (path: string) => boolean;
122
124
  readonly readFile?: (path: string) => string;
123
125
  }
@@ -1838,6 +1838,29 @@ export function checkClaudeMcpDuplicateRegistration(options = {}) {
1838
1838
  manualSources.push(`project scope ${projectDir}`);
1839
1839
  }
1840
1840
  }
1841
+ // Also scan project-local `.mcp.json` (and ancestors). Claude Code honors
1842
+ // this file as a third registration surface; the home JSON projects map
1843
+ // does not always mirror it, so a plugin + project-file pair still doubles
1844
+ // the tool list without the home-JSON check ever firing.
1845
+ const projectRoot = options.projectRoot ?? process.cwd();
1846
+ for (const candidate of collectProjectMcpJsonCandidates(projectRoot)) {
1847
+ if (!exists(candidate))
1848
+ continue;
1849
+ try {
1850
+ const projectParsed = JSON.parse(readFile(candidate));
1851
+ if (projectParsed !== null &&
1852
+ typeof projectParsed === "object" &&
1853
+ "mcpServers" in projectParsed &&
1854
+ projectParsed.mcpServers &&
1855
+ "webpresso" in
1856
+ (projectParsed.mcpServers ?? {})) {
1857
+ manualSources.push(`project file ${candidate}`);
1858
+ }
1859
+ }
1860
+ catch {
1861
+ // Unreadable/malformed .mcp.json is not a duplicate signal — degrade.
1862
+ }
1863
+ }
1841
1864
  if (manualSources.length === 0) {
1842
1865
  return { name, ok: true, detail: "plugin-scoped registration only" };
1843
1866
  }
@@ -1851,6 +1874,19 @@ export function checkClaudeMcpDuplicateRegistration(options = {}) {
1851
1874
  `claude mcp remove webpresso (run inside the project directory for project-scope entries).`,
1852
1875
  };
1853
1876
  }
1877
+ /** Walk projectRoot → filesystem root collecting `.mcp.json` paths that may register MCP servers. */
1878
+ function collectProjectMcpJsonCandidates(projectRoot) {
1879
+ const candidates = [];
1880
+ let current = projectRoot;
1881
+ for (;;) {
1882
+ candidates.push(join(current, ".mcp.json"));
1883
+ const parent = dirname(current);
1884
+ if (parent === current)
1885
+ break;
1886
+ current = parent;
1887
+ }
1888
+ return candidates;
1889
+ }
1854
1890
  const QUOTED_TOML_STRING = /"((?:[^"\\]|\\.)*)"/gu;
1855
1891
  const DANGLING_SYMLINK_SCAN_DEPTH = 3;
1856
1892
  function collectDanglingWorktreePaths(raw, worktreesRoot, exists, alreadyReported) {
@@ -2055,7 +2091,7 @@ export async function runHooksDoctor(opts = {}) {
2055
2091
  checks.push(checkCodexConfigKeys());
2056
2092
  }
2057
2093
  checks.push(checkProjectMcpPins(opts.cwd));
2058
- checks.push(checkClaudeMcpDuplicateRegistration());
2094
+ checks.push(checkClaudeMcpDuplicateRegistration({ projectRoot: opts.cwd ?? process.cwd() }));
2059
2095
  checks.push({ advisory: true, ...checkDanglingHostPaths() });
2060
2096
  checks.push({ advisory: true, ...checkRootLauncherContract() });
2061
2097
  checks.push({ advisory: true, ...checkNativePluginRuntime() });
@@ -95,7 +95,12 @@ export function validateWorktreeDiscipline(input) {
95
95
  return { validator: VALIDATOR_NAME, passed: true };
96
96
  }
97
97
  function normalizedToolName(input) {
98
- return (input.tool_name ?? "").toLowerCase().replaceAll(/[^a-z]/gu, "");
98
+ // tool_name is typed string but malformed host envelopes can deliver non-strings;
99
+ // never call string methods without a typeof guard (same class as isBashInput).
100
+ const raw = input.tool_name;
101
+ if (typeof raw !== "string")
102
+ return "";
103
+ return raw.toLowerCase().replaceAll(/[^a-z]/gu, "");
99
104
  }
100
105
  function applyPatchTargets(input) {
101
106
  if (!normalizedToolName(input).endsWith("applypatch"))
@@ -78,7 +78,33 @@ export function resolveHookSessionId(input, env = process.env) {
78
78
  return undefined;
79
79
  }
80
80
  export function isBashInput(input) {
81
- return "command" in (input.tool_input || {});
81
+ // A bare `command` field is NOT sufficient: Codex delivers `apply_patch` patch
82
+ // text inside `tool_input.command`, the same field a shell command uses.
83
+ // Discriminate on the tool name instead. Absent / non-string tool name ⇒
84
+ // unknown/legacy host ⇒ classify as shell so command guards still fire (a
85
+ // guard must fail toward inspecting, never toward silently skipping). Malformed
86
+ // non-string tool_name must not throw (optional chaining only guards nullish).
87
+ if (!("command" in (input.tool_input || {})))
88
+ return false;
89
+ const rawToolName = input.tool_name;
90
+ const toolName = typeof rawToolName === "string" ? rawToolName.trim().toLowerCase() : undefined;
91
+ if (!toolName)
92
+ return true;
93
+ return (toolName === "bash" ||
94
+ toolName === "shell" ||
95
+ toolName === "exec" ||
96
+ toolName === "exec_command" ||
97
+ // Codex names its sandboxed shell tool `local_shell` (see
98
+ // session-memory/provider-readers `local_shell_call`); without it a real
99
+ // shell command bypasses the danger/forbidden/worktree guards.
100
+ toolName === "local_shell" ||
101
+ // Grok's shell tool: `run_terminal_command` (envelope name) and the internal
102
+ // `run_terminal_cmd` its Bash-mapping surfaces (docs.x.ai/build/features/hooks).
103
+ toolName === "run_terminal_command" ||
104
+ toolName === "run_terminal_cmd" ||
105
+ toolName.endsWith(".exec_command") ||
106
+ toolName.endsWith("_exec_command") ||
107
+ toolName.endsWith("__exec_command"));
82
108
  }
83
109
  export function isFileEditInput(input) {
84
110
  const toolInput = input.tool_input || {};
@@ -98,6 +98,24 @@ const CROSS_CUTTING_TEST_DEPENDENTS = [
98
98
  when: /^(README\.md|docs\/guides\/session-memory\.md)$/u,
99
99
  tests: ["src/mcp/tools/session-docs.test.ts", "src/build/package-manifest.test.ts"],
100
100
  },
101
+ {
102
+ // blueprint-server.ts owns the MCP architecture contract (tool naming,
103
+ // handler wiring, transport shape). Its architecture test is not colocated
104
+ // under the same basename, so --affected would otherwise skip it.
105
+ when: /^src\/mcp\/blueprint-server\./u,
106
+ tests: ["src/mcp/blueprint-server.test-architecture.test.ts"],
107
+ },
108
+ {
109
+ // Package surface / publishability contracts live in the audit package,
110
+ // not next to package.json or the manifest builder.
111
+ when: /^(package\.json|src\/build\/package-manifest\.ts)$/u,
112
+ tests: ["src/audit/package-surface.test.ts"],
113
+ },
114
+ {
115
+ // Marketplace and plugin.json validation is a build-side contract test.
116
+ when: /^\.claude-plugin\/(marketplace|plugin)\.json$/u,
117
+ tests: ["src/build/validate-marketplace.test.ts"],
118
+ },
101
119
  ];
102
120
  /**
103
121
  * Every test path the cross-cutting map can yield.
@@ -194,6 +194,11 @@ export declare const putInputJsonSchema: {
194
194
  last_result: {
195
195
  type: string;
196
196
  };
197
+ defer: {
198
+ type: string;
199
+ const: string;
200
+ description: string;
201
+ };
197
202
  };
198
203
  required: string[];
199
204
  additionalProperties: boolean;
@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
3
3
  import { z } from "zod";
4
4
  import matter from "gray-matter";
5
5
  import { parseBlueprint } from "#core/parser";
6
+ import { taskIdSchema } from "#core/schema";
6
7
  import { evaluateApprovalGate, formatApprovalGateRequirement } from "#lifecycle/audit";
7
8
  import { canonicalizeBlueprint } from "#lifecycle/canonicalize.js";
8
9
  import { BlueprintCreationService } from "#service/BlueprintCreationService";
@@ -40,7 +41,7 @@ const createSchema = MutationTarget.extend({
40
41
  head_at_ingest: z.string().nullable().optional(),
41
42
  });
42
43
  const putTaskSchema = z.object({
43
- id: z.string().min(1),
44
+ id: taskIdSchema,
44
45
  title: z.string().min(1),
45
46
  status: z.enum(["todo", "in-progress", "blocked", "done", "dropped"]).default("todo"),
46
47
  wave: z.string().optional(),
@@ -72,6 +73,12 @@ const putPromotionGateSchema = z.object({
72
73
  command: z.string().min(1),
73
74
  expected_outcome: z.string().min(1),
74
75
  last_result: z.string().min(1),
76
+ // Explicit per-gate pre-implementation deferral. When set, `wp blueprint
77
+ // promote` skips executing/existence-checking this gate's command entirely
78
+ // and records a `deferred: ...` Last result instead. See
79
+ // src/blueprint/trust/promotion.ts and catalog/agent/rules
80
+ // /pre-implementation.md's Promotion gate section.
81
+ defer: z.literal("pre-implementation").optional(),
75
82
  });
76
83
  const putTrustDossierSchema = z.object({
77
84
  readiness: putTrustReadinessSchema,
@@ -180,6 +187,11 @@ const putTrustDossierJsonSchema = {
180
187
  command: { type: "string", description: "wp or ./bin/wp facade command" },
181
188
  expected_outcome: { type: "string" },
182
189
  last_result: { type: "string" },
190
+ defer: {
191
+ type: "string",
192
+ const: "pre-implementation",
193
+ description: "Explicit opt-in: skip executing/existence-checking this gate at promote time",
194
+ },
183
195
  },
184
196
  required: ["gate", "command", "expected_outcome", "last_result"],
185
197
  additionalProperties: false,
@@ -323,12 +335,7 @@ function renderTrustDossierMarkdown(dossier) {
323
335
  "",
324
336
  "### Promotion Gates",
325
337
  "",
326
- ...renderMarkdownTable(["Gate", "Command", "Expected outcome", "Last result"], dossier.promotion_gates.map((gate) => [
327
- gate.gate,
328
- gate.command,
329
- gate.expected_outcome,
330
- gate.last_result,
331
- ])),
338
+ ...renderPromotionGatesTable(dossier.promotion_gates),
332
339
  "",
333
340
  "### Residual Unknowns",
334
341
  "",
@@ -336,6 +343,23 @@ function renderTrustDossierMarkdown(dossier) {
336
343
  "",
337
344
  ];
338
345
  }
346
+ // Only emits the (opt-in) Defer column when at least one gate uses it, so
347
+ // the common no-defer case keeps rendering the legacy 4-column table exactly
348
+ // as before -- matching src/blueprint/trust/dossier.ts's parser, which
349
+ // accepts either width.
350
+ function renderPromotionGatesTable(gates) {
351
+ const hasDefer = gates.some((gate) => gate.defer !== undefined);
352
+ if (!hasDefer) {
353
+ return renderMarkdownTable(["Gate", "Command", "Expected outcome", "Last result"], gates.map((gate) => [gate.gate, gate.command, gate.expected_outcome, gate.last_result]));
354
+ }
355
+ return renderMarkdownTable(["Gate", "Command", "Expected outcome", "Last result", "Defer"], gates.map((gate) => [
356
+ gate.gate,
357
+ gate.command,
358
+ gate.expected_outcome,
359
+ gate.last_result,
360
+ gate.defer ?? "",
361
+ ]));
362
+ }
339
363
  function normalizeResidualUnknowns(value) {
340
364
  if (value === undefined)
341
365
  return "None.";
@@ -1,3 +1,3 @@
1
- export declare const MCP_AUDIT_KINDS: readonly ["test-smells", "tph-e2e", "agents", "catalog-drift", "package-surface", "reference-parity-matrix", "docs-frontmatter", "blueprint-readme-drift", "blueprint-pr-coverage", "blueprint-lifecycle", "blueprint-trust", "architecture-drift", "cloudflare-deploy-contract", "absolute-path-policy", "no-first-party-mjs", "no-math-random", "roadmap-links", "bundle-budget", "commit-message", "tech-debt", "supported-agent-clis", "hook-surface", "harness-surfaces", "weakness-mining", "harness-overlay-evidence", "host-substitution-risk", "ai-contracts", "atomic-state-writes", "no-relative-package-scripts", "toolchain-isolation", "open-source-licenses", "secrets-policy", "no-dev-vars", "github-actions-secrets", "secret-provider-quarantine", "security-quality-regressions", "secrets-config", "consumer-agent-kit-dependency", "ci-test-perf", "test-scan-perf", "session-memory-hardcut", "worktree-main-ownership", "typescript-version", "ci-guardrails-wiring", "sast"];
1
+ export declare const MCP_AUDIT_KINDS: readonly ["test-smells", "tph-e2e", "agents", "catalog-drift", "package-surface", "reference-parity-matrix", "docs-frontmatter", "blueprint-readme-drift", "blueprint-pr-coverage", "blueprint-lifecycle", "blueprint-trust", "architecture-drift", "cloudflare-deploy-contract", "absolute-path-policy", "no-first-party-mjs", "no-math-random", "roadmap-links", "bundle-budget", "commit-message", "tech-debt", "supported-agent-clis", "hook-surface", "harness-surfaces", "weakness-mining", "harness-overlay-evidence", "host-substitution-risk", "ai-contracts", "atomic-state-writes", "no-relative-package-scripts", "toolchain-isolation", "open-source-licenses", "secrets-policy", "no-dev-vars", "github-actions-secrets", "secret-provider-quarantine", "security-quality-regressions", "secrets-config", "consumer-agent-kit-dependency", "ci-test-perf", "test-scan-perf", "session-memory-hardcut", "worktree-main-ownership", "typescript-version", "ci-guardrails-wiring", "sast", "changeset-required"];
2
2
  export type MCPAuditKind = (typeof MCP_AUDIT_KINDS)[number];
3
3
  export declare function isMCPAuditKind(value: string): value is MCPAuditKind;
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { z } from "zod";
17
17
  import type { ToolDescriptor } from "#mcp/auto-discover";
18
- export declare const KINDS: readonly ["test-smells", "tph-e2e", "agents", "catalog-drift", "package-surface", "reference-parity-matrix", "docs-frontmatter", "blueprint-readme-drift", "blueprint-pr-coverage", "blueprint-lifecycle", "blueprint-trust", "architecture-drift", "cloudflare-deploy-contract", "absolute-path-policy", "no-first-party-mjs", "no-math-random", "roadmap-links", "bundle-budget", "commit-message", "tech-debt", "supported-agent-clis", "hook-surface", "harness-surfaces", "weakness-mining", "harness-overlay-evidence", "host-substitution-risk", "ai-contracts", "atomic-state-writes", "no-relative-package-scripts", "toolchain-isolation", "open-source-licenses", "secrets-policy", "no-dev-vars", "github-actions-secrets", "secret-provider-quarantine", "security-quality-regressions", "secrets-config", "consumer-agent-kit-dependency", "ci-test-perf", "test-scan-perf", "session-memory-hardcut", "worktree-main-ownership", "typescript-version", "ci-guardrails-wiring", "sast"];
18
+ export declare const KINDS: readonly ["test-smells", "tph-e2e", "agents", "catalog-drift", "package-surface", "reference-parity-matrix", "docs-frontmatter", "blueprint-readme-drift", "blueprint-pr-coverage", "blueprint-lifecycle", "blueprint-trust", "architecture-drift", "cloudflare-deploy-contract", "absolute-path-policy", "no-first-party-mjs", "no-math-random", "roadmap-links", "bundle-budget", "commit-message", "tech-debt", "supported-agent-clis", "hook-surface", "harness-surfaces", "weakness-mining", "harness-overlay-evidence", "host-substitution-risk", "ai-contracts", "atomic-state-writes", "no-relative-package-scripts", "toolchain-isolation", "open-source-licenses", "secrets-policy", "no-dev-vars", "github-actions-secrets", "secret-provider-quarantine", "security-quality-regressions", "secrets-config", "consumer-agent-kit-dependency", "ci-test-perf", "test-scan-perf", "session-memory-hardcut", "worktree-main-ownership", "typescript-version", "ci-guardrails-wiring", "sast", "changeset-required"];
19
19
  export declare const inputSchema: z.ZodObject<{
20
20
  kind: z.ZodEnum<{
21
21
  "absolute-path-policy": "absolute-path-policy";
@@ -29,6 +29,7 @@ export declare const inputSchema: z.ZodObject<{
29
29
  "blueprint-trust": "blueprint-trust";
30
30
  "bundle-budget": "bundle-budget";
31
31
  "catalog-drift": "catalog-drift";
32
+ "changeset-required": "changeset-required";
32
33
  "ci-guardrails-wiring": "ci-guardrails-wiring";
33
34
  "ci-test-perf": "ci-test-perf";
34
35
  "cloudflare-deploy-contract": "cloudflare-deploy-contract";
@@ -569,6 +569,18 @@ export async function dispatchAudit(input) {
569
569
  details: auditResult,
570
570
  };
571
571
  }
572
+ case "changeset-required": {
573
+ const { auditChangesetRequired } = await import("#audit/changeset-required");
574
+ const auditResult = auditChangesetRequired(input.cwd ?? input.directory ?? process.cwd(), {
575
+ baseRef: input.baseRef,
576
+ });
577
+ return {
578
+ passed: auditResult.ok,
579
+ summary: summarizeRepoAudit(kind, auditResult),
580
+ kind,
581
+ details: auditResult,
582
+ };
583
+ }
572
584
  default: {
573
585
  // Exhaustiveness check — z.enum should make this unreachable.
574
586
  const _exhaustive = kind;
@@ -25,6 +25,7 @@ declare const inputSchema: z.ZodObject<{
25
25
  "blueprint-trust": "blueprint-trust";
26
26
  "bundle-budget": "bundle-budget";
27
27
  "catalog-drift": "catalog-drift";
28
+ "changeset-required": "changeset-required";
28
29
  "ci-guardrails-wiring": "ci-guardrails-wiring";
29
30
  "ci-test-perf": "ci-test-perf";
30
31
  "cloudflare-deploy-contract": "cloudflare-deploy-contract";
@@ -1,6 +1,6 @@
1
1
  import type { ToolDescriptor, ToolHandlerExtra, ToolHandlerResult } from "#mcp/auto-discover";
2
2
  export interface SessionFetchAndIndexDeps {
3
- readonly fetchImpl?: typeof fetch;
3
+ readonly fetchImpl?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
4
4
  }
5
5
  export declare function handleSessionFetchAndIndex(raw: unknown, extra?: ToolHandlerExtra, deps?: SessionFetchAndIndexDeps): Promise<ToolHandlerResult>;
6
6
  declare const tool: ToolDescriptor;
@@ -89,6 +89,8 @@
89
89
  "#compiler/*": "./compiler/*.js",
90
90
  "#review/*.js": "./review/*.js",
91
91
  "#review/*": "./review/*.js",
92
+ "#rust/*.js": "./rust/*.js",
93
+ "#rust/*": "./rust/*.js",
92
94
  "#telemetry/*.js": "./telemetry/*.js",
93
95
  "#telemetry/*": "./telemetry/*.js",
94
96
  "#codex/app-server/client": "./codex/app-server/client.js",
@@ -44,8 +44,14 @@ export interface PlatformResponse<T = unknown> {
44
44
  readonly status: number;
45
45
  readonly data: T;
46
46
  }
47
+ /**
48
+ * Injectable fetch call-signature used by platform transport and tests.
49
+ * Narrower than `typeof fetch` (which requires Bun/DOM statics like
50
+ * `preconnect`) so vitest mocks remain assignable without casts.
51
+ */
52
+ export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
47
53
  export interface PlatformClientOptions {
48
- readonly fetchFn?: typeof globalThis.fetch;
54
+ readonly fetchFn?: FetchLike;
49
55
  readonly maxAttempts?: number;
50
56
  readonly baseRetryDelayMs?: number;
51
57
  readonly maxRetryDelayMs?: number;
@@ -1,9 +1,10 @@
1
1
  import { type AuthenticatedPlatformContractSnapshot, type PlatformContractAuthority, type PlatformContractAuthorityProbeResult, type PlatformContractSourceAdapter } from "./contract-provenance.js";
2
+ import { type FetchLike } from "./client.js";
2
3
  export interface GitHubContractSourceAdapterOptions {
3
4
  readonly token: string;
4
5
  readonly authority?: string;
5
6
  readonly apiUrl?: string;
6
- readonly fetchFn?: typeof globalThis.fetch;
7
+ readonly fetchFn?: FetchLike;
7
8
  readonly sleep?: (delayMs: number) => Promise<void>;
8
9
  }
9
10
  export declare class GitHubContractSourceAdapter implements PlatformContractSourceAdapter {
@@ -31,13 +31,33 @@ const SESSION_SKIP_PREFIX = /^Session-skip:/im;
31
31
  function trim(value) {
32
32
  return value?.trim() ?? "";
33
33
  }
34
+ /**
35
+ * Finds the value of a "<label>: <value>" list item at the head of a line,
36
+ * tolerating a leading "- " bullet and markdown emphasis wrapping the label
37
+ * (`**bold**`, `__bold__`, `*italic*`, `_italic_`) on either side of the
38
+ * colon — e.g. both "- Execution model(s): x" and
39
+ * "- **Execution model(s):** x" match. Still anchored to the start of the
40
+ * line (after stripping the bullet/emphasis decoration): a mention of the
41
+ * label mid-sentence never matches. Mirrors the CI `require_field` awk
42
+ * matcher in `.github/workflows/ci.agent-kit.yml` — keep the two in lockstep.
43
+ */
34
44
  function fieldValue(body, label) {
35
45
  const expected = label.toLowerCase();
36
46
  for (const line of body.split(/\r?\n/u)) {
37
- const normalized = line.toLowerCase();
38
- if (normalized.startsWith(`- ${expected}:`) || normalized.startsWith(`${expected}:`)) {
39
- return line.slice(line.indexOf(":") + 1).trim();
40
- }
47
+ const colonIndex = line.indexOf(":");
48
+ if (colonIndex === -1)
49
+ continue;
50
+ const before = line
51
+ .slice(0, colonIndex)
52
+ .replace(/^\s+/u, "")
53
+ .replace(/^-\s+/u, "")
54
+ .replace(/[*_]+/gu, "");
55
+ if (before.toLowerCase() !== expected)
56
+ continue;
57
+ return line
58
+ .slice(colonIndex + 1)
59
+ .replace(/^[*_]+/u, "")
60
+ .trim();
41
61
  }
42
62
  return undefined;
43
63
  }
@@ -9,7 +9,7 @@ import { createHash } from "node:crypto";
9
9
  import { existsSync, lstatSync, readFileSync } from "node:fs";
10
10
  import path from "node:path";
11
11
  import { evaluateReviewEvents, parseReviewEventLog } from "./events.js";
12
- import { createDeliverySubjectAtRef, createLegacyPlanSubjectAtRef, createPlanSubjectAtRef, resolveReviewCommit, } from "./subject.js";
12
+ import { createDeliverySubjectAtRef, createLegacyPlanSubjectAtRef, createPlanSubjectAtRef, createPreTagsStripPlanSubjectAtRef, resolveReviewCommit, } from "./subject.js";
13
13
  const GIT_TIMEOUT_MS = 5_000;
14
14
  const GIT_MAX_BUFFER_BYTES = 128 * 1024 * 1024;
15
15
  const REGULAR_MODE_PATTERN = /^100(?:644|755)$/u;
@@ -138,18 +138,23 @@ export function readReviewAuthorityAtRef(cwd, ref, stableSlug, purpose) {
138
138
  subjectDigest: subject.digest,
139
139
  }, compromisedIds);
140
140
  if (purpose === "plan" && evaluation.approvals.length === 0) {
141
- // Digest-compatibility fallback: events recorded before the lifecycle
142
- // stripped-key extension bound their subjectDigest to the legacy
143
- // normalization. Accept those for the same tracked content; new events
144
- // always record the current digest (see legacyNormalizePlanSubjectContent).
145
- const legacySubject = createLegacyPlanSubjectAtRef(cwd, commit, stableSlug, overview.path);
146
- if (legacySubject.digest !== subject.digest) {
141
+ // Digest-compatibility fallbacks (newest-first after current):
142
+ // 1) pre-tags-strip: lifecycle strip without advisory `tags` (approvals recorded
143
+ // after the stripped-key extension but before the tags strip)
144
+ // 2) legacy: status + completed_at only (pre-extension)
145
+ // New events always record the current digest (with tags strip).
146
+ for (const make of [createPreTagsStripPlanSubjectAtRef, createLegacyPlanSubjectAtRef]) {
147
+ const alt = make(cwd, commit, stableSlug, overview.path);
148
+ if (alt.digest === subject.digest)
149
+ continue;
147
150
  evaluation = evaluateReviewEvents(events, {
148
151
  purpose,
149
152
  blueprintSlug: stableSlug,
150
- subjectScheme: legacySubject.scheme,
151
- subjectDigest: legacySubject.digest,
153
+ subjectScheme: alt.scheme,
154
+ subjectDigest: alt.digest,
152
155
  }, compromisedIds);
156
+ if (evaluation.approvals.length > 0)
157
+ break;
153
158
  }
154
159
  }
155
160
  return {
@@ -45,6 +45,17 @@ export declare function recordReviewAvailability(projectRoot: string, event: Rev
45
45
  export declare function recordReviewAvailabilityWithLock(projectRoot: string, event: ReviewAvailabilityEvent): Promise<ReviewAvailabilityState>;
46
46
  export declare function isProviderSuppressed(state: ReviewAvailabilityState, provider: ReviewProvider): boolean;
47
47
  export declare function suppressedReviewers(state: ReviewAvailabilityState): Set<string>;
48
+ /**
49
+ * Backstop age for suppressions recorded without a parseable retry window.
50
+ *
51
+ * Without this, a provider/account failure that never set `retryAfterMs`
52
+ * (unparsable limit text, incomplete event payload) stays suppressed forever
53
+ * and permanently removes that surface from rotation. 24h is long enough that
54
+ * a real multi-hour/day window still wins when present, and short enough that
55
+ * a sticky unknown-limit entry self-heals on the next calendar day instead of
56
+ * requiring a manual availability reset.
57
+ */
58
+ export declare const MAX_SUPPRESSION_AGE_MS: number;
48
59
  export declare function pruneExpiredAvailabilitySuppressions(state: ReviewAvailabilityState, nowMs?: number): {
49
60
  state: ReviewAvailabilityState;
50
61
  pruned: string[];
@@ -247,21 +247,36 @@ export function suppressedReviewers(state) {
247
247
  .filter((record) => record.suppressed)
248
248
  .map((record) => record.canonicalId));
249
249
  }
250
+ /**
251
+ * Backstop age for suppressions recorded without a parseable retry window.
252
+ *
253
+ * Without this, a provider/account failure that never set `retryAfterMs`
254
+ * (unparsable limit text, incomplete event payload) stays suppressed forever
255
+ * and permanently removes that surface from rotation. 24h is long enough that
256
+ * a real multi-hour/day window still wins when present, and short enough that
257
+ * a sticky unknown-limit entry self-heals on the next calendar day instead of
258
+ * requiring a manual availability reset.
259
+ */
260
+ export const MAX_SUPPRESSION_AGE_MS = 24 * 60 * 60 * 1000;
250
261
  /**
251
262
  * Shared expiry predicate for a suppressed availability record: applies to
252
263
  * both the reviewer/provider store and the opencode-go account store below.
253
264
  * A record clears when its terminal classification is no longer a failure
254
- * classification, or when its retry window has elapsed.
265
+ * classification, or when its retry window (or the max-age backstop) has
266
+ * elapsed.
255
267
  */
256
268
  function suppressionExpired(record, nowMs) {
257
269
  if (!record.suppressed)
258
270
  return false;
259
271
  if (!suppressedRecordStillApplies(record))
260
272
  return true;
261
- if (record.retryAfterMs === undefined || record.lastFailureAt === undefined)
273
+ if (record.lastFailureAt === undefined)
262
274
  return false;
263
275
  const failureAt = Date.parse(record.lastFailureAt);
264
- return Number.isFinite(failureAt) && failureAt + record.retryAfterMs <= nowMs;
276
+ if (!Number.isFinite(failureAt))
277
+ return false;
278
+ const windowMs = record.retryAfterMs === undefined ? MAX_SUPPRESSION_AGE_MS : record.retryAfterMs;
279
+ return failureAt + windowMs <= nowMs;
265
280
  }
266
281
  function pruneRecordMap(records, nowMs, pruned) {
267
282
  return Object.fromEntries(Object.entries(records).map(([key, record]) => {
@@ -1,4 +1,14 @@
1
- import type { ReviewRunInput, ReviewStreamAdapter } from "./types.js";
1
+ import type { ReviewAdapterEvent, ReviewRunInput, ReviewStreamAdapter } from "./types.js";
2
+ /**
3
+ * Classify an OpenCode Go usage-limit message and derive its real reset window.
4
+ *
5
+ * Exported because the `wp opencode` launch-time health probe
6
+ * (`#cli/commands/opencode-probe.js`) runs the same provider text through the
7
+ * same classifier. A second copy of this wording/duration math would drift the
8
+ * moment the provider changes a phrase, and the two consumers would then
9
+ * disagree about whether an account is capped.
10
+ */
11
+ export declare function opencodeUsageLimit(message: string): ReviewAdapterEvent | null;
2
12
  export declare const claudeReviewAdapter: ReviewStreamAdapter;
3
13
  export declare const codexReviewAdapter: ReviewStreamAdapter;
4
14
  export declare const opencodeReviewAdapter: ReviewStreamAdapter;
@@ -92,7 +92,16 @@ function parseResetDurationMs(message) {
92
92
  }
93
93
  return duration > 0 ? duration : undefined;
94
94
  }
95
- function opencodeUsageLimit(message) {
95
+ /**
96
+ * Classify an OpenCode Go usage-limit message and derive its real reset window.
97
+ *
98
+ * Exported because the `wp opencode` launch-time health probe
99
+ * (`#cli/commands/opencode-probe.js`) runs the same provider text through the
100
+ * same classifier. A second copy of this wording/duration math would drift the
101
+ * moment the provider changes a phrase, and the two consumers would then
102
+ * disagree about whether an account is capped.
103
+ */
104
+ export function opencodeUsageLimit(message) {
96
105
  if (!/\busage limit reached\b/iu.test(message))
97
106
  return null;
98
107
  const limitWindow = /\bweekly usage limit reached\b/iu.test(message)
@@ -14,4 +14,4 @@ export interface ReviewArtifactWriter {
14
14
  export declare function reviewRunStatePath(root: string, runId: string): string;
15
15
  export type ReviewRunStateCleanupResult = "missing" | "not-running" | "removed" | "refused";
16
16
  export declare function removeOwnedRunningReviewState(root: string, runId: string, reservationToken: string): ReviewRunStateCleanupResult;
17
- export declare function createReviewArtifactWriter(root: string, runId: string, startedAt: string, reservationToken?: `${string}-${string}-${string}-${string}-${string}`): ReviewArtifactWriter;
17
+ export declare function createReviewArtifactWriter(root: string, runId: string, startedAt: string, reservationToken?: string): ReviewArtifactWriter;
@@ -4,4 +4,10 @@ export interface ReviewTargetCheckout {
4
4
  readonly changes: () => readonly string[];
5
5
  readonly cleanup: () => void;
6
6
  }
7
+ declare function statusEntryPath(line: string): string;
7
8
  export declare function createReviewTargetCheckout(repositoryRoot: string, targetSha: string): ReviewTargetCheckout;
9
+ /** Test-only surface for porcelain path parsing. */
10
+ export declare const reviewCheckoutInternals: {
11
+ statusEntryPath: typeof statusEntryPath;
12
+ };
13
+ export {};