@webpresso/agent-kit 3.2.0 → 3.3.0

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

Potentially problematic release.


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

Files changed (93) hide show
  1. package/bin/_run.js +4 -1
  2. package/dist/esm/audit/blueprint-trust.js +14 -10
  3. package/dist/esm/audit/changeset-evidence.d.ts +23 -0
  4. package/dist/esm/audit/changeset-evidence.js +30 -0
  5. package/dist/esm/audit/changeset-required.d.ts +10 -0
  6. package/dist/esm/audit/changeset-required.js +73 -0
  7. package/dist/esm/audit/ci-guardrails-detection.d.ts +27 -0
  8. package/dist/esm/audit/ci-guardrails-detection.js +68 -0
  9. package/dist/esm/audit/ci-guardrails-wiring.js +3 -13
  10. package/dist/esm/audit/registry.d.ts +4 -4
  11. package/dist/esm/audit/registry.js +6 -1
  12. package/dist/esm/blueprint/core/parser.js +61 -4
  13. package/dist/esm/blueprint/core/schema.d.ts +1 -0
  14. package/dist/esm/blueprint/core/schema.js +26 -0
  15. package/dist/esm/blueprint/lifecycle/audit.js +22 -10
  16. package/dist/esm/blueprint/lifecycle/review-provenance.d.ts +6 -0
  17. package/dist/esm/blueprint/lifecycle/review-provenance.js +39 -8
  18. package/dist/esm/blueprint/lifecycle/rollout-anchor.d.ts +97 -0
  19. package/dist/esm/blueprint/lifecycle/rollout-anchor.js +161 -0
  20. package/dist/esm/blueprint/sync/client.d.ts +2 -1
  21. package/dist/esm/blueprint/sync/client.js +1 -1
  22. package/dist/esm/blueprint/trust/dossier.d.ts +1 -0
  23. package/dist/esm/blueprint/trust/dossier.js +40 -6
  24. package/dist/esm/blueprint/trust/promotion.js +12 -0
  25. package/dist/esm/build/cli-mcp-parity.js +5 -0
  26. package/dist/esm/cli/cli.d.ts +1 -1
  27. package/dist/esm/cli/cli.js +9 -0
  28. package/dist/esm/cli/commands/agent-launch.d.ts +2 -0
  29. package/dist/esm/cli/commands/agent-launch.js +31 -20
  30. package/dist/esm/cli/commands/audit.js +4 -0
  31. package/dist/esm/cli/commands/ci-preflight.js +24 -3
  32. package/dist/esm/cli/commands/init/convergence-apply.d.ts +64 -0
  33. package/dist/esm/cli/commands/init/convergence-apply.js +127 -23
  34. package/dist/esm/cli/commands/init/index.d.ts +2 -0
  35. package/dist/esm/cli/commands/init/index.js +2 -2
  36. package/dist/esm/cli/commands/init/package-root.d.ts +7 -0
  37. package/dist/esm/cli/commands/init/package-root.js +10 -2
  38. package/dist/esm/cli/commands/init/scaffold-base-kit.d.ts +12 -3
  39. package/dist/esm/cli/commands/init/scaffold-base-kit.js +31 -30
  40. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.js +3 -1
  41. package/dist/esm/cli/commands/init/scaffolders/file-transaction.d.ts +17 -2
  42. package/dist/esm/cli/commands/init/scaffolders/file-transaction.js +189 -17
  43. package/dist/esm/cli/commands/opencode-probe.d.ts +107 -0
  44. package/dist/esm/cli/commands/opencode-probe.js +323 -0
  45. package/dist/esm/cli/commands/opencode-rotate.d.ts +23 -0
  46. package/dist/esm/cli/commands/opencode-rotate.js +23 -11
  47. package/dist/esm/cli/commands/quality-runner.js +3 -3
  48. package/dist/esm/cli/commands/review.js +27 -5
  49. package/dist/esm/cli/commands/rust-check.d.ts +24 -0
  50. package/dist/esm/cli/commands/rust-check.js +110 -0
  51. package/dist/esm/cli/commands/typecheck.d.ts +1 -0
  52. package/dist/esm/cli/commands/typecheck.js +30 -1
  53. package/dist/esm/cli/commands/worktree/router-dispatch.d.ts +6 -0
  54. package/dist/esm/cli/commands/worktree/router-dispatch.js +113 -1
  55. package/dist/esm/cli/commands/worktree/router.js +10 -0
  56. package/dist/esm/hooks/doctor.d.ts +2 -0
  57. package/dist/esm/hooks/doctor.js +37 -1
  58. package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.js +6 -1
  59. package/dist/esm/hooks/shared/types.js +27 -1
  60. package/dist/esm/hooks/stop/qa-changed-files.js +18 -0
  61. package/dist/esm/mcp/blueprint/handlers/document-mutations.d.ts +5 -0
  62. package/dist/esm/mcp/blueprint/handlers/document-mutations.js +31 -7
  63. package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
  64. package/dist/esm/mcp/tools/audit.d.ts +2 -1
  65. package/dist/esm/mcp/tools/audit.js +12 -0
  66. package/dist/esm/mcp/tools/audits.d.ts +1 -0
  67. package/dist/esm/mcp/tools/session-fetch-and-index.d.ts +1 -1
  68. package/dist/esm/package.json +2 -0
  69. package/dist/esm/platform/client.d.ts +7 -1
  70. package/dist/esm/platform/github-contract-source.d.ts +2 -1
  71. package/dist/esm/pr-description/contract.js +24 -4
  72. package/dist/esm/review/authority.js +14 -9
  73. package/dist/esm/review/availability.d.ts +11 -0
  74. package/dist/esm/review/availability.js +18 -3
  75. package/dist/esm/review/execution/adapters.d.ts +11 -1
  76. package/dist/esm/review/execution/adapters.js +10 -1
  77. package/dist/esm/review/execution/artifacts.d.ts +1 -1
  78. package/dist/esm/review/execution/review-checkout.js +35 -16
  79. package/dist/esm/review/execution/supervisor.js +31 -4
  80. package/dist/esm/review/execution/types.d.ts +1 -0
  81. package/dist/esm/review/subject.d.ts +13 -0
  82. package/dist/esm/review/subject.js +42 -4
  83. package/dist/esm/rust/affected-manifests.d.ts +38 -0
  84. package/dist/esm/rust/affected-manifests.js +69 -0
  85. package/dist/esm/session-memory/fetch-index.d.ts +1 -1
  86. package/dist/esm/status/snapshot.js +2 -6
  87. package/dist/esm/test-helpers/global-setup.d.ts +0 -1
  88. package/dist/esm/test-helpers/global-setup.js +18 -15
  89. package/dist/esm/typecheck/planner.d.ts +9 -1
  90. package/dist/esm/typecheck/planner.js +31 -0
  91. package/dist/esm/worktrees/orphan-scan.d.ts +50 -0
  92. package/dist/esm/worktrees/orphan-scan.js +211 -0
  93. package/package.json +14 -12
@@ -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;
@@ -23,24 +23,34 @@ function frame(hash, label, value) {
23
23
  hash.update(`${label}\0${bytes.byteLength}\0`, "utf8");
24
24
  hash.update(bytes);
25
25
  }
26
- function worktreeFingerprint(cwd) {
26
+ // Fingerprint over the filtered line set `status()` produces below, minus
27
+ // pure gitignored (`!!`) entries, so the two contamination checks
28
+ // (`changes()`'s status() diff and the caller worktree fingerprint) cannot
29
+ // drift apart on what counts as "source" content while still tolerating
30
+ // expected runtime bookkeeping.
31
+ //
32
+ // Before the status()-alignment fix, the fingerprint hashed the raw
33
+ // unfiltered `git status` output (without `--ignored`), so anything status()
34
+ // already tolerates (the self-hosting bootstrap's idempotent `package.json`
35
+ // trailing-whitespace normalization, and non-source ignored paths under
36
+ // dist/.tshy/node_modules) could still flip the fingerprint and false-flag a
37
+ // clean caller worktree as contaminated.
38
+ //
39
+ // Reusing status() alone re-introduces a different false positive: the
40
+ // supervisor writes process records under gitignored `.webpresso/` (and similar
41
+ // runtime roots) during the run. Those appear as `!!` lines only when
42
+ // `--ignored` is on. Dropping `!!` here restores the prior fingerprint
43
+ // contract for ignored paths while keeping the package.json / non-`!!` filters
44
+ // shared with status(). Checkout-side status() still reports non-derived
45
+ // gitignored writes (e.g. a stray `.env`) on the disposable review tree.
46
+ function worktreeFingerprint(cwd, ignoredPaths = new Set()) {
27
47
  const hash = createHash("sha256");
28
- const porcelain = git(cwd, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
29
- frame(hash, "porcelain", porcelain);
48
+ const lines = status(cwd, ignoredPaths).filter((line) => !line.startsWith("!!"));
49
+ frame(hash, "status-lines", lines.join("\n"));
30
50
  frame(hash, "staged", gitBytes(cwd, ["diff", "--cached", "--binary", "--no-ext-diff", "HEAD", "--"]));
31
- const records = porcelain.split("\0").filter(Boolean);
32
51
  const dirtyPaths = new Set();
33
- for (let index = 0; index < records.length; index += 1) {
34
- const record = records[index] ?? "";
35
- dirtyPaths.add(record.slice(3));
36
- if (record.slice(0, 2).match(/[RC]/u)) {
37
- const pairedPath = records[index + 1];
38
- if (pairedPath) {
39
- dirtyPaths.add(pairedPath);
40
- index += 1;
41
- }
42
- }
43
- }
52
+ for (const line of lines)
53
+ dirtyPaths.add(statusEntryPath(line));
44
54
  for (const relativePath of [...dirtyPaths].filter(Boolean).toSorted()) {
45
55
  const absolutePath = join(cwd, relativePath);
46
56
  frame(hash, "dirty-path", relativePath);
@@ -108,13 +118,22 @@ function isNormalizedPackageJsonModification(cwd, line) {
108
118
  return false;
109
119
  }
110
120
  }
121
+ // Single source of truth for turning a `git status --porcelain=v1` line into
122
+ // its relative path, shared by status()'s filter predicate and
123
+ // worktreeFingerprint()'s dirty-path hashing so both stay aligned by
124
+ // construction. Does not special-case the rename arrow (`old -> new`); a
125
+ // rename line's raw tail is used as-is, matching this function's prior
126
+ // (pre-refactor) inline behavior.
127
+ function statusEntryPath(line) {
128
+ return line.slice(3).replace(/\/$/u, "");
129
+ }
111
130
  function status(cwd, ignoredPaths = new Set()) {
112
131
  const output = git(cwd, ["status", "--porcelain=v1", "--untracked-files=all", "--ignored"]);
113
132
  return output
114
133
  .split(/\r?\n/u)
115
134
  .filter((line) => line.length > 0)
116
135
  .filter((line) => {
117
- const relativePath = line.slice(3).replace(/\/$/u, "");
136
+ const relativePath = statusEntryPath(line);
118
137
  if (ignoredPaths.has(relativePath))
119
138
  return false;
120
139
  if (line.startsWith("!!") && isNonSourceIgnoredPath(relativePath))
@@ -123,6 +123,10 @@ export async function superviseReviewExecution(input, adapter, deps = {}) {
123
123
  let fallbackSelectionTimer;
124
124
  let pendingFallback;
125
125
  const seenFrontiers = new Set();
126
+ const droppedEventWarnings = [];
127
+ const recordDroppedProviderEvent = (reason) => {
128
+ droppedEventWarnings.push(reason);
129
+ };
126
130
  const decoder = new StringDecoder("utf8");
127
131
  const diagnosticDecoder = new StringDecoder("utf8");
128
132
  let buffered = "";
@@ -230,19 +234,41 @@ export async function superviseReviewExecution(input, adapter, deps = {}) {
230
234
  const processLine = (line) => {
231
235
  if (line.trim().length === 0 || status !== null)
232
236
  return;
233
- if (Buffer.byteLength(line, "utf8") > MAX_PROVIDER_EVENT_BYTES) {
234
- fail("protocol-unsupported", "provider-event-too-large");
235
- return;
236
- }
237
+ const oversized = Buffer.byteLength(line, "utf8") > MAX_PROVIDER_EVENT_BYTES;
237
238
  let value;
238
239
  try {
239
240
  value = JSON.parse(line);
240
241
  }
241
242
  catch {
243
+ // An oversized frame that also fails to parse carries no recoverable
244
+ // signal (a genuinely truncated/malformed line looks the same either
245
+ // way) -- drop it with a warning instead of failing a run that may
246
+ // still receive a complete, well-formed terminal verdict later. A
247
+ // normal-sized malformed line is still a protocol violation.
248
+ if (oversized) {
249
+ recordDroppedProviderEvent("provider-event-too-large-unparseable");
250
+ return;
251
+ }
242
252
  fail("protocol-unsupported", "invalid-json-event");
243
253
  return;
244
254
  }
245
255
  const event = adapter.parseEvent(value);
256
+ if (oversized && (event.kind === "terminal" || event.kind === "failure")) {
257
+ // Terminal/verdict and failure frames are load-bearing: an oversized
258
+ // one is treated as a protocol violation, same as before this fix.
259
+ fail("protocol-unsupported", "provider-event-too-large");
260
+ return;
261
+ }
262
+ if (oversized) {
263
+ // Non-terminal (progress/neutral/unsupported) oversized frames are
264
+ // dropped rather than failing the whole run -- a provider that echoes
265
+ // e.g. a large lockfile read in a progress frame must not discard an
266
+ // otherwise-complete review verdict. See catalog/agent/rules
267
+ // /no-timeout-as-fix.md: the byte bound stays in place, we just stop
268
+ // treating "one big frame" as fatal to the whole run.
269
+ recordDroppedProviderEvent(`provider-event-too-large-${event.kind}`);
270
+ return;
271
+ }
246
272
  if (event.kind === "unsupported") {
247
273
  fail("protocol-unsupported", "unknown-provider-event");
248
274
  return;
@@ -433,6 +459,7 @@ export async function superviseReviewExecution(input, adapter, deps = {}) {
433
459
  ...(limitWindow === undefined ? {} : { limitWindow }),
434
460
  ...(retryAfterMs === undefined ? {} : { retryAfterMs }),
435
461
  summaryPath: artifacts.summaryPath,
462
+ ...(droppedEventWarnings.length > 0 ? { warnings: [...droppedEventWarnings] } : {}),
436
463
  };
437
464
  }
438
465
  function recordReviewProcessStart(reviewChild, processGroupId) {
@@ -81,4 +81,5 @@ export interface ReviewRunSummary {
81
81
  readonly limitWindow?: ReviewLimitWindow;
82
82
  readonly retryAfterMs?: number;
83
83
  readonly summaryPath: string;
84
+ readonly warnings?: readonly string[];
84
85
  }
@@ -43,9 +43,22 @@ export declare function createPlanSubjectDigestForContent(blueprintSlug: string,
43
43
  * `normalizePlanSubjectContent`.
44
44
  */
45
45
  export declare function legacyNormalizePlanSubjectContent(content: Buffer | string): Buffer;
46
+ /**
47
+ * Digest-compatibility variant for review events recorded after the lifecycle
48
+ * stripped-key extension but before the advisory `tags` strip. Verification
49
+ * accepts a match against this tier so existing plan approvals on tagged
50
+ * blueprints are not stranded by the tags-strip rollout.
51
+ */
52
+ export declare function preTagsStripNormalizePlanSubjectContent(content: Buffer | string): Buffer;
46
53
  export declare function createPlanSubjectAtRef(cwd: string, ref: string, blueprintSlug: string, blueprintPath?: string): ReviewSubject;
47
54
  /** Plan subject digested with the pre-extension normalization (see above). */
48
55
  export declare function createLegacyPlanSubjectAtRef(cwd: string, ref: string, blueprintSlug: string, blueprintPath?: string): ReviewSubject;
56
+ /** Plan subject digested without the advisory `tags` strip (pre-tags-strip tier). */
57
+ export declare function createPreTagsStripPlanSubjectAtRef(cwd: string, ref: string, blueprintSlug: string, blueprintPath?: string): ReviewSubject;
58
+ /** Working-ledger digest for the pre-tags-strip compatibility tier. */
59
+ export declare function createPreTagsStripPlanSubjectDigestForContent(blueprintSlug: string, content: Buffer | string): string;
60
+ /** Working-ledger digest for the pre-extension legacy compatibility tier. */
61
+ export declare function createLegacyPlanSubjectDigestForContent(blueprintSlug: string, content: Buffer | string): string;
49
62
  export declare const reviewSubjectInternals: {
50
63
  isExcludedReviewEnvelope: typeof isExcludedReviewEnvelope;
51
64
  isReviewEvidencePath: typeof isReviewEvidencePath;