@webpresso/agent-kit 3.3.3 → 3.3.5

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 (156) hide show
  1. package/THIRD-PARTY-NOTICES.md +12 -10
  2. package/catalog/agent/rules/changeset-release.md +15 -4
  3. package/catalog/agent/rules/managed-tool-binaries.md +67 -0
  4. package/catalog/agent/rules/parallel-orchestration-quota.md +99 -1
  5. package/catalog/agent/rules/pre-implementation.md +36 -20
  6. package/catalog/agent/rules/rtk-routing.md +22 -16
  7. package/catalog/agent/skills/autopilot/SKILL.md +4 -3
  8. package/catalog/agent/skills/ultragoal/SKILL.md +11 -9
  9. package/dist/esm/audit/agents.js +126 -26
  10. package/dist/esm/audit/hook-surface.js +5 -1
  11. package/dist/esm/audit/repo-guardrails.d.ts +8 -0
  12. package/dist/esm/audit/repo-guardrails.js +71 -1
  13. package/dist/esm/blueprint/core/parser.js +38 -12
  14. package/dist/esm/blueprint/execution/artifacts.js +8 -4
  15. package/dist/esm/blueprint/execution/frontmatter-keys.d.ts +17 -0
  16. package/dist/esm/blueprint/execution/frontmatter-keys.js +24 -0
  17. package/dist/esm/blueprint/execution/metadata.js +8 -4
  18. package/dist/esm/blueprint/lifecycle/audit.d.ts +21 -0
  19. package/dist/esm/blueprint/lifecycle/audit.js +89 -3
  20. package/dist/esm/blueprint/lifecycle/review-provenance.js +63 -10
  21. package/dist/esm/blueprint/lifecycle/rollout-anchor.d.ts +22 -0
  22. package/dist/esm/blueprint/lifecycle/rollout-anchor.js +37 -1
  23. package/dist/esm/blueprint/markdown/blocked-line.d.ts +49 -0
  24. package/dist/esm/blueprint/markdown/blocked-line.js +63 -0
  25. package/dist/esm/blueprint/markdown/helpers.js +10 -4
  26. package/dist/esm/blueprint/tracked-document/parser.js +3 -3
  27. package/dist/esm/blueprint/trust/dossier.js +41 -1
  28. package/dist/esm/blueprint/trust/gate-row.d.ts +55 -0
  29. package/dist/esm/blueprint/trust/gate-row.js +113 -0
  30. package/dist/esm/blueprint/trust/promotion.d.ts +17 -0
  31. package/dist/esm/blueprint/trust/promotion.js +19 -19
  32. package/dist/esm/build/atomic-file-copy.d.ts +32 -0
  33. package/dist/esm/build/atomic-file-copy.js +44 -0
  34. package/dist/esm/build/vendor-oxlint-plugins.d.ts +11 -0
  35. package/dist/esm/build/vendor-oxlint-plugins.js +48 -0
  36. package/dist/esm/cli/commands/blueprint/mutations.d.ts +15 -0
  37. package/dist/esm/cli/commands/blueprint/mutations.js +8 -2
  38. package/dist/esm/cli/commands/compile.d.ts +13 -0
  39. package/dist/esm/cli/commands/compile.js +182 -62
  40. package/dist/esm/cli/commands/format.js +17 -1
  41. package/dist/esm/cli/commands/gain/index.js +10 -3
  42. package/dist/esm/cli/commands/init/gitignore-patcher.js +6 -0
  43. package/dist/esm/cli/commands/init/index.js +32 -52
  44. package/dist/esm/cli/commands/init/mcp-spec.d.ts +48 -13
  45. package/dist/esm/cli/commands/init/mcp-spec.js +86 -30
  46. package/dist/esm/cli/commands/init/merge.js +30 -10
  47. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.d.ts +56 -31
  48. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +180 -43
  49. package/dist/esm/cli/commands/init/scaffolders/rtk/index.d.ts +17 -5
  50. package/dist/esm/cli/commands/init/scaffolders/rtk/index.js +56 -75
  51. package/dist/esm/cli/commands/init/scaffolders/runtime-check/index.js +7 -6
  52. package/dist/esm/cli/commands/init/untracked-collision.d.ts +12 -1
  53. package/dist/esm/cli/commands/init/untracked-collision.js +23 -1
  54. package/dist/esm/cli/commands/pr-help.js +1 -1
  55. package/dist/esm/cli/commands/pr.js +18 -4
  56. package/dist/esm/cli/commands/review.d.ts +1 -0
  57. package/dist/esm/cli/commands/review.js +137 -7
  58. package/dist/esm/cli/commands/sync.d.ts +8 -6
  59. package/dist/esm/cli/commands/sync.js +11 -9
  60. package/dist/esm/cli/commands/worktree/router-dispatch.d.ts +24 -2
  61. package/dist/esm/cli/commands/worktree/router-dispatch.js +80 -48
  62. package/dist/esm/cli/commands/worktree/router.js +5 -2
  63. package/dist/esm/cli/direct-provider-launch.js +12 -4
  64. package/dist/esm/compiler/flatten.d.ts +10 -1
  65. package/dist/esm/compiler/flatten.js +14 -4
  66. package/dist/esm/compose/admin-blocks-registry-integrity.d.ts +22 -0
  67. package/dist/esm/compose/admin-blocks-registry-integrity.js +90 -0
  68. package/dist/esm/compose/compose.d.ts +8 -0
  69. package/dist/esm/compose/compose.js +115 -8
  70. package/dist/esm/compose/ops-report-schema.js +17 -3
  71. package/dist/esm/compose/registry-resolve.d.ts +17 -0
  72. package/dist/esm/compose/registry-resolve.js +78 -11
  73. package/dist/esm/config/oxlint/graphql-conventions.js +2 -5
  74. package/dist/esm/config/oxlint/import-hygiene.js +21 -8
  75. package/dist/esm/config/oxlint/oxlint-config.fixture.d.ts +1 -1
  76. package/dist/esm/config/oxlint/oxlint-config.fixture.js +2 -3
  77. package/dist/esm/config/oxlint/oxlintrc.d.ts +5 -7
  78. package/dist/esm/config/oxlint/oxlintrc.js +19 -31
  79. package/dist/esm/config/oxlint/path-roles.js +53 -0
  80. package/dist/esm/config/oxlint/query-patterns.js +2 -5
  81. package/dist/esm/content/dispatch.js +4 -0
  82. package/dist/esm/content/loader.d.ts +1 -1
  83. package/dist/esm/content/loader.js +10 -6
  84. package/dist/esm/content/skill-policy.d.ts +11 -0
  85. package/dist/esm/content/skill-policy.js +33 -0
  86. package/dist/esm/daemon/domains/blueprint-exec-ledger.d.ts +86 -0
  87. package/dist/esm/daemon/domains/blueprint-exec-ledger.js +134 -0
  88. package/dist/esm/daemon/domains/blueprint-exec.d.ts +106 -0
  89. package/dist/esm/daemon/domains/blueprint-exec.js +161 -0
  90. package/dist/esm/daemon/domains/launch.d.ts +25 -0
  91. package/dist/esm/daemon/domains/launch.js +30 -1
  92. package/dist/esm/daemon/handlers/blueprints-handlers.d.ts +12 -1
  93. package/dist/esm/daemon/handlers/blueprints-handlers.js +148 -6
  94. package/dist/esm/daemon/handlers/product.d.ts +10 -0
  95. package/dist/esm/daemon/handlers/product.js +10 -1
  96. package/dist/esm/daemon/methods.d.ts +21 -1
  97. package/dist/esm/daemon/methods.js +20 -0
  98. package/dist/esm/daemon/protocol/contract.d.ts +58 -0
  99. package/dist/esm/daemon/protocol/contract.js +38 -0
  100. package/dist/esm/daemon/server.js +7 -0
  101. package/dist/esm/docs-linter/blueprint-plan.js +2 -2
  102. package/dist/esm/errors/wp-error.js +21 -2
  103. package/dist/esm/hooks/doctor.d.ts +11 -0
  104. package/dist/esm/hooks/doctor.js +114 -45
  105. package/dist/esm/hooks/pretool-guard/dev-routing.js +68 -5
  106. package/dist/esm/hooks/pretool-guard/rtk-rewrite.d.ts +24 -0
  107. package/dist/esm/hooks/pretool-guard/rtk-rewrite.js +50 -0
  108. package/dist/esm/hooks/pretool-guard/runner.js +6 -0
  109. package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.js +62 -3
  110. package/dist/esm/hooks/status/index.js +1 -1
  111. package/dist/esm/mcp/blueprint/_shared/lifecycle.js +39 -11
  112. package/dist/esm/mcp/blueprint/handlers/review-log.d.ts +14 -7
  113. package/dist/esm/mcp/blueprint/handlers/review-log.js +46 -22
  114. package/dist/esm/mcp/blueprint/registration.js +1 -1
  115. package/dist/esm/mcp/tools/_shared/redact.d.ts +22 -0
  116. package/dist/esm/mcp/tools/_shared/redact.js +114 -8
  117. package/dist/esm/mcp/tools/gain.js +28 -2
  118. package/dist/esm/mcp/tools/worktree.js +65 -3
  119. package/dist/esm/mcp/tools/wp-ui-compose.js +38 -4
  120. package/dist/esm/mcp/tools/wp-ui-preview.js +14 -7
  121. package/dist/esm/review/authority.js +56 -1
  122. package/dist/esm/review/delivery-verifier.d.ts +4 -0
  123. package/dist/esm/review/delivery-verifier.js +9 -1
  124. package/dist/esm/review/lifecycle-writers.d.ts +76 -0
  125. package/dist/esm/review/lifecycle-writers.js +330 -0
  126. package/dist/esm/review/subject.js +66 -32
  127. package/dist/esm/status/snapshot.d.ts +5 -0
  128. package/dist/esm/status/snapshot.js +27 -2
  129. package/dist/esm/symlinker/consumers.d.ts +3 -1
  130. package/dist/esm/symlinker/consumers.js +7 -1
  131. package/dist/esm/symlinker/unified-sync.d.ts +3 -3
  132. package/dist/esm/symlinker/unified-sync.js +88 -11
  133. package/dist/esm/test/shard-durations.json +0 -2
  134. package/dist/esm/test-helpers/global-setup.js +38 -3
  135. package/dist/esm/tool-runtime/managed-rtk.d.ts +44 -0
  136. package/dist/esm/tool-runtime/managed-rtk.js +275 -0
  137. package/dist/esm/tool-runtime/resolve-runner.js +18 -194
  138. package/dist/esm/utils/package-root.d.ts +32 -0
  139. package/dist/esm/utils/package-root.js +82 -0
  140. package/dist/esm/worktrees/git-metadata.d.ts +41 -0
  141. package/dist/esm/worktrees/git-metadata.js +48 -0
  142. package/package.json +16 -21
  143. package/dist/esm/cli/commands/init/scaffolders/subagents/index.d.ts +0 -6
  144. package/dist/esm/cli/commands/init/scaffolders/subagents/index.js +0 -77
  145. package/dist/esm/config/oxlint/code-safety.d.ts +0 -18
  146. package/dist/esm/config/oxlint/foundation-purity.d.ts +0 -21
  147. package/dist/esm/config/oxlint/graphql-conventions.d.ts +0 -23
  148. package/dist/esm/config/oxlint/import-hygiene.d.ts +0 -34
  149. package/dist/esm/config/oxlint/index.d.ts +0 -22
  150. package/dist/esm/config/oxlint/index.js +0 -28
  151. package/dist/esm/config/oxlint/monorepo-paths.d.ts +0 -22
  152. package/dist/esm/config/oxlint/query-patterns.d.ts +0 -24
  153. package/dist/esm/config/oxlint/testing-quality.d.ts +0 -35
  154. package/dist/esm/config/oxlint/tier-boundaries.d.ts +0 -35
  155. package/dist/esm/output-transforms/rulesync.d.ts +0 -2
  156. package/dist/esm/output-transforms/rulesync.js +0 -79
@@ -1,5 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { existsSync, lstatSync, readFileSync, rmSync } from "node:fs";
2
+ import { existsSync, lstatSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  const GIT_TIMEOUT_MS = 15_000;
4
4
  const OWNERSHIP_MARKER = /^\uFEFF?[ \t]*(?:#|\/\/|\/\*|<!--|")?[ \t]*(?:>>> managed by webpresso\b|Generated by webpresso\b)/iu;
5
5
  function git(repoRoot, args) {
@@ -12,6 +12,25 @@ function git(repoRoot, args) {
12
12
  });
13
13
  return { ok: result.status === 0 && !result.error };
14
14
  }
15
+ /** True when `gitPath` is git-ignored in `repoRoot` (machine-local, never committed). */
16
+ export function isGitIgnored(repoRoot, gitPath) {
17
+ return git(repoRoot, ["check-ignore", "--no-index", "-q", "--", gitPath]).ok;
18
+ }
19
+ function resolveIgnoredDivergence(options) {
20
+ const { unit } = options;
21
+ if (options.patchDerived) {
22
+ return { ok: true, resolution: "ignored-patch-derived" };
23
+ }
24
+ const current = readFileSync(unit.targetPath);
25
+ const backupPath = `${unit.targetPath}.bak`;
26
+ writeFileSync(backupPath, current);
27
+ return {
28
+ ok: true,
29
+ resolution: "ignored-backed-up",
30
+ warning: `Untracked, git-ignored ${unit.gitPath} had unmanaged content; ` +
31
+ `the original was backed up to ${unit.gitPath}.bak before it was overwritten.`,
32
+ };
33
+ }
15
34
  export function resolveUntrackedCollision(options) {
16
35
  const { unit } = options;
17
36
  if (!existsSync(unit.targetPath))
@@ -34,6 +53,9 @@ export function resolveUntrackedCollision(options) {
34
53
  rmSync(unit.targetPath);
35
54
  return { ok: true, resolution: "removed-owned" };
36
55
  }
56
+ if (unit.scope === "repo-ignored") {
57
+ return resolveIgnoredDivergence(options);
58
+ }
37
59
  return {
38
60
  ok: false,
39
61
  error: `Unknown untracked content blocks ${unit.gitPath}; it was preserved${options.force ? " despite --force" : ""}. ` +
@@ -53,7 +53,7 @@ export function getPrWaitCommandHelpText() {
53
53
  " --branch <name> Branch selector (defaults to current branch)",
54
54
  " --pr <number-or-url> Explicit PR selector",
55
55
  " --until <target> ready | checks-terminal | checks-green | merged (default: checks-terminal)",
56
- " --timeout-ms <ms> Overall deadline; for ready, per-probe timeout only",
56
+ " --timeout-ms <ms> Overall deadline, max 300000 (5min); for ready, per-probe timeout only",
57
57
  " --poll-interval-ms <ms> Poll interval (default: 15000)",
58
58
  " --include-checks Include final check snapshot (default: true)",
59
59
  " --no-include-checks Omit final check snapshot from output",
@@ -2,15 +2,29 @@ import { PR_WAIT_UNTIL_VALUES, runPrWait } from "#mcp/tools/pr-wait.js";
2
2
  import { buildStatusSnapshot, formatStatusSnapshot } from "#status/snapshot.js";
3
3
  // PR help text lives in ./pr-help.ts (dependency-free) so the `wp pr --help`
4
4
  // fast path can render it without importing this heavy command module.
5
- function parsePositiveInteger(value, name) {
5
+ // Mirrors the hard cap `readonlyOpsBaseSchema` enforces downstream for `pr
6
+ // wait` (src/mcp/tools/_readonly-ops.ts). `pr status` shares the same
7
+ // `--timeout-ms` flag but reaches `buildStatusSnapshot` directly, which has
8
+ // no schema of its own, so an over-cap value was silently accepted with no
9
+ // error and no clamp. Validating here makes both subcommands fail the same
10
+ // way, with a clear message, before any network probe starts, instead of
11
+ // relying on a downstream schema that not every code path shares.
12
+ const MAX_TIMEOUT_MS = 300_000;
13
+ function parsePositiveInteger(value, name, max) {
6
14
  if (value === undefined)
7
15
  return undefined;
8
16
  const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
9
17
  if (!Number.isFinite(parsed) || parsed <= 0) {
10
18
  throw new Error(`${name} must be a positive integer`);
11
19
  }
20
+ if (max !== undefined && parsed > max) {
21
+ throw new Error(`${name} must not exceed ${max}`);
22
+ }
12
23
  return parsed;
13
24
  }
25
+ function parseTimeoutMs(value) {
26
+ return parsePositiveInteger(value, "--timeout-ms", MAX_TIMEOUT_MS);
27
+ }
14
28
  function normalizeUntil(value) {
15
29
  if (value === undefined)
16
30
  return undefined;
@@ -26,7 +40,7 @@ export function registerPrCommand(cli) {
26
40
  .option("--branch <name>", "Branch selector")
27
41
  .option("--pr <number-or-url>", "Explicit PR selector")
28
42
  .option("--until <target>", "ready | checks-terminal | checks-green | merged")
29
- .option("--timeout-ms <ms>", "Overall deadline; for ready, per-probe timeout only")
43
+ .option("--timeout-ms <ms>", "Overall deadline (max 300000ms / 5min); for ready, per-probe timeout only")
30
44
  .option("--poll-interval-ms <ms>", "Poll interval")
31
45
  .option("--include-checks", "Include final check snapshot", { default: true })
32
46
  .option("--max-output-bytes <n>", "Per-command output budget")
@@ -41,7 +55,7 @@ export function registerPrCommand(cli) {
41
55
  includePr: true,
42
56
  maxOutputBytes: parsePositiveInteger(options.maxOutputBytes, "--max-output-bytes"),
43
57
  pr: options.pr === undefined ? undefined : String(options.pr),
44
- timeoutMs: parsePositiveInteger(options.timeoutMs, "--timeout-ms"),
58
+ timeoutMs: parseTimeoutMs(options.timeoutMs),
45
59
  });
46
60
  console.log(options.json ? JSON.stringify(snapshot, null, 2) : formatStatusSnapshot(snapshot));
47
61
  return options.requireReady && snapshot.status !== "ready" ? 1 : 0;
@@ -57,7 +71,7 @@ export function registerPrCommand(cli) {
57
71
  maxOutputBytes: parsePositiveInteger(options.maxOutputBytes, "--max-output-bytes"),
58
72
  pollIntervalMs: parsePositiveInteger(options.pollIntervalMs, "--poll-interval-ms"),
59
73
  pr: options.pr === undefined ? undefined : String(options.pr),
60
- timeoutMs: parsePositiveInteger(options.timeoutMs, "--timeout-ms"),
74
+ timeoutMs: parseTimeoutMs(options.timeoutMs),
61
75
  until,
62
76
  });
63
77
  console.log(JSON.stringify(payload, null, 2));
@@ -220,6 +220,7 @@ export interface ReviewGateDependencies {
220
220
  */
221
221
  readonly prepareOpencodeCredential?: (account: OpencodeAccount) => string;
222
222
  }
223
+ export declare function describeVerificationFailure(verification: ReviewGateVerification | undefined, gateId: string): string;
223
224
  /**
224
225
  * Sanctioned fallback when `wp review gate` does not reach "approved" — points
225
226
  * at the tracked-provenance path (`wp review log` / `wp_blueprint_review_log`)
@@ -28,6 +28,7 @@ import { isPolicyApprovalVerdict } from "#review/verdict.js";
28
28
  import { verificationArtifactSection, verificationPromptSummary, verifyDeliveryPromotionGates, } from "#review/delivery-verifier.js";
29
29
  import { createDeliverySubjectAtRef, createPlanSubjectAtRef, findUnreviewedWorkingTreeChanges, } from "#review/subject.js";
30
30
  import { runCommand } from "#mcp/tools/_shared/run-command.js";
31
+ import { ALLOWED_WP_COMMAND_TIMEOUT_MS } from "#trust/command-runner.js";
31
32
  import { NotInGitRepoError, withLock } from "#paths/state-root.js";
32
33
  import { writeFileAtomic, writeJsonFileAtomic } from "#shared-utils/write-json-file.js";
33
34
  export const REVIEWER_IDS = [
@@ -1463,6 +1464,35 @@ function gateFailureFromSummary(summary) {
1463
1464
  ...(summary.retryAfterMs === undefined ? {} : { retryAfterMs: summary.retryAfterMs }),
1464
1465
  };
1465
1466
  }
1467
+ // `providerErrorDetail`'s doc comment promises "sanitized, length-bounded"
1468
+ // text; a checkout-creation error's message/cause chain is target/environment
1469
+ // text (pnpm/git stderr), not attacker-shaped, but is still capped here so
1470
+ // the invariant holds regardless.
1471
+ const MAX_PROVIDER_ERROR_DETAIL_LENGTH = 2000;
1472
+ // Walks an `Error.cause` chain into a single human-readable line. Used to
1473
+ // surface the real reason a review checkout could not be created instead of
1474
+ // discarding it behind a fixed string (see the bare `catch` this replaces in
1475
+ // `invokeProvider`). Bounded depth + a `seen` set guard against a pathological
1476
+ // or cyclic cause chain.
1477
+ function formatErrorWithCauseChain(error, maxDepth = 5) {
1478
+ const parts = [];
1479
+ const seen = new Set();
1480
+ let current = error;
1481
+ for (let depth = 0; depth < maxDepth; depth += 1) {
1482
+ if (current === undefined || current === null || seen.has(current))
1483
+ break;
1484
+ seen.add(current);
1485
+ if (current instanceof Error) {
1486
+ parts.push(current.message);
1487
+ current = current.cause;
1488
+ }
1489
+ else {
1490
+ parts.push(String(current));
1491
+ break;
1492
+ }
1493
+ }
1494
+ return parts.join(" -> caused by: ");
1495
+ }
1466
1496
  export async function invokeProvider(input, executeReview = runReviewExecution, runCatalogCommand = runCommand, createTargetCheckout = createReviewTargetCheckout,
1467
1497
  // A thunk (not a checkout instance) so callers that never reach a real
1468
1498
  // provider attempt — injected fakes included — never pay the checkout's
@@ -1475,19 +1505,34 @@ getSharedCheckout) {
1475
1505
  checkout = existingCheckout ?? createTargetCheckout(input.projectRoot, input.targetSha);
1476
1506
  checkout.beginAttempt();
1477
1507
  }
1478
- catch {
1508
+ catch (error) {
1509
+ // Bind + chain via Error.cause (ES2022) rather than discarding the real
1510
+ // failure — this exact swallow previously hid a genuine supply-chain
1511
+ // signal (`ERR_PNPM_TRUST_DOWNGRADE`) behind a fixed string for a full
1512
+ // day. See catalog/agent/rules/ts-coding-conventions.md.
1513
+ const isolationFailure = new Error("Unable to create the disposable review checkout; review was not started", { cause: error });
1514
+ let cleanupError;
1479
1515
  if (ownsCheckout) {
1480
1516
  try {
1481
1517
  checkout?.cleanup();
1482
1518
  }
1483
- catch {
1484
- // The attempt already fails closed as an isolation failure.
1519
+ catch (cleanupFailure) {
1520
+ // Cleanup failing must not mask the primary isolation failure above;
1521
+ // capture it as secondary diagnostic context instead of discarding it.
1522
+ cleanupError = cleanupFailure;
1485
1523
  }
1486
1524
  }
1487
1525
  const requestedModel = input.model ?? defaultReviewModel(input.provider) ?? "unresolved";
1488
1526
  const model = resolveReviewModel(input.provider, requestedModel);
1527
+ // Chain from the original `error`, not `isolationFailure` — the latter's
1528
+ // own message is already the fixed prefix below, so chaining from it
1529
+ // would print that fixed sentence twice.
1530
+ const providerErrorDetail = (formatErrorWithCauseChain(error) +
1531
+ (cleanupError === undefined
1532
+ ? ""
1533
+ : ` | cleanup after isolation failure also failed: ${formatErrorWithCauseChain(cleanupError)}`)).slice(0, MAX_PROVIDER_ERROR_DETAIL_LENGTH);
1489
1534
  return {
1490
- output: "Unable to create the disposable review checkout; review was not started.",
1535
+ output: `${isolationFailure.message}: ${providerErrorDetail}`,
1491
1536
  model,
1492
1537
  reviewer: input.provider === "opencode" && model !== "unresolved"
1493
1538
  ? reviewerFamily(model)
@@ -1497,6 +1542,7 @@ getSharedCheckout) {
1497
1542
  verdict: "no-verdict",
1498
1543
  terminalClassification: "review-isolation-failure",
1499
1544
  failureCode: "review-isolation-failure",
1545
+ providerErrorDetail,
1500
1546
  };
1501
1547
  }
1502
1548
  try {
@@ -1799,6 +1845,64 @@ function selectGateProviders(input) {
1799
1845
  const available = ordered.filter((provider) => !isProviderSuppressed(input.availability, provider));
1800
1846
  return available.length > 0 ? available : [];
1801
1847
  }
1848
+ // Builds the `message` for a "verification-failed" gate result: host-side
1849
+ // verification (delivery Promotion Gates + Trust Dossier parsing) died before
1850
+ // any review provider ran, so `attempts` is empty and the default
1851
+ // approval-gate-template message ("Approval gate not satisfied (...)") reads
1852
+ // like a reviewer rejected the work when no reviewer was ever invoked. Names
1853
+ // the actual cause and the corrective action so a failed gate does not cost
1854
+ // a repeat run just to discover what happened. Exported for tests.
1855
+ export function describeVerificationFailure(verification, gateId) {
1856
+ const resume = `Retry with --resume ${gateId} once fixed.`;
1857
+ const preamble = "Host verification failed before any review provider ran (0 review attempts) -- this is not a provider/quota failure.";
1858
+ if (!verification || verification.status !== "failed") {
1859
+ return `${preamble} ${resume}`;
1860
+ }
1861
+ if (verification.failureCode === "malformed-trust-dossier") {
1862
+ const fields = (verification.violations ?? [])
1863
+ .map((violation) => `${violation.section}: ${violation.message}`)
1864
+ .join("; ");
1865
+ return `${preamble} The Trust Dossier is malformed${fields ? ` (${fields})` : ""}. Fill in the named field(s) via wp_blueprint_put (or wp blueprint promote to see the same detail) before gating again. ${resume}`;
1866
+ }
1867
+ const failedCommand = verification.commands.find((command) => command.outcome !== "passed");
1868
+ if (verification.failureCode === "command-timed-out" && failedCommand) {
1869
+ const budgetSeconds = ALLOWED_WP_COMMAND_TIMEOUT_MS / 1000;
1870
+ return (`${preamble} The Promotion Gate command \`${failedCommand.command}\` exceeded the ` +
1871
+ `${budgetSeconds}s sandboxed gate budget and was killed -- this is a fixed per-command ` +
1872
+ "budget, not a live outage, and raising it is not the fix. Either replace this gate with " +
1873
+ "a faster command (e.g. `wp lint`, or a narrow `wp audit <kind>`), or mark this Promotion " +
1874
+ 'Gates row `defer: "pre-implementation"` so it is recorded without being executed at gate ' +
1875
+ `time. ${resume}`);
1876
+ }
1877
+ if (failedCommand) {
1878
+ const exitDetail = failedCommand.exitCode === null ? "no exit code" : `exit ${failedCommand.exitCode}`;
1879
+ const logDetail = verification.logPath ? ` Log: ${verification.logPath}.` : "";
1880
+ return (`${preamble} Gate command \`${failedCommand.command}\` failed (${exitDetail}, ` +
1881
+ `outcome: ${failedCommand.outcome}).${logDetail} This is usually a missing dependency or ` +
1882
+ "stale install in the sandboxed checkout -- run the repo's install command " +
1883
+ `(e.g. \`wp install\`) and retry. ${resume}`);
1884
+ }
1885
+ const codeDetail = verification.failureCode ? ` (failureCode: ${verification.failureCode})` : "";
1886
+ return `${preamble} Verification failed${codeDetail} before any gate command ran. ${resume}`;
1887
+ }
1888
+ // Human-readable summary of why every candidate provider/reviewer is
1889
+ // currently suppressed in the project's availability.json cache, so a
1890
+ // zero-candidate gate result names the file and the reason instead of
1891
+ // looking like a silent no-op.
1892
+ function describeSuppressedCandidates(availability, candidates) {
1893
+ return candidates
1894
+ .map((provider) => {
1895
+ const record = availability.providers[provider];
1896
+ if (!record?.suppressed)
1897
+ return undefined;
1898
+ const reason = record.suppressionReason ?? record.terminalClassification ?? "unknown reason";
1899
+ const since = record.suppressedAt ? ` since ${record.suppressedAt}` : "";
1900
+ const retry = record.retryAfterMs === undefined ? "" : ` (retry window ${record.retryAfterMs}ms)`;
1901
+ return `${provider}: suppressed${since} (${reason}${retry})`;
1902
+ })
1903
+ .filter((value) => value !== undefined)
1904
+ .join("; ");
1905
+ }
1802
1906
  function safeArtifactPath(blueprintPath, relativeArtifact) {
1803
1907
  if (path.isAbsolute(relativeArtifact) || relativeArtifact.split(/[\\/]/u).includes("..")) {
1804
1908
  throw new Error(`Unsafe review artifact path: ${relativeArtifact}`);
@@ -2127,21 +2231,30 @@ export async function runReviewGate(projectRoot, slug, input, dependencies = { i
2127
2231
  // never need one (plan-purpose reuse of an existing approval, terminal
2128
2232
  // rejections, exhausted scope). A setup failure is remembered so provider
2129
2233
  // attempts still surface the same isolation-failure evidence.
2234
+ // Captures the real failure the first time checkoutFactory is
2235
+ // attempted this run, so the re-throw below (reached whenever a later
2236
+ // caller reuses the memoized failure) can chain to it via Error.cause
2237
+ // instead of discarding it — see ts-coding-conventions.md "never
2238
+ // swallow an error".
2239
+ let checkoutSetupError;
2130
2240
  const ensureSharedCheckout = () => {
2131
2241
  if (sharedCheckout !== undefined || checkoutSetupFailed)
2132
2242
  return sharedCheckout;
2133
2243
  try {
2134
2244
  sharedCheckout = checkoutFactory(projectRoot, targetSha);
2135
2245
  }
2136
- catch {
2246
+ catch (error) {
2137
2247
  checkoutSetupFailed = true;
2248
+ checkoutSetupError = error;
2138
2249
  }
2139
2250
  return sharedCheckout;
2140
2251
  };
2141
2252
  const createCheckoutForAttempt = () => {
2142
2253
  // Only reachable when ensureSharedCheckout() already failed — a live
2143
2254
  // shared checkout is always passed to the attempt as existingCheckout.
2144
- throw new Error("review checkout setup failed");
2255
+ throw new Error("review checkout setup failed", {
2256
+ cause: checkoutSetupError,
2257
+ });
2145
2258
  };
2146
2259
  let sharedCheckoutCleaned = false;
2147
2260
  const cleanupSharedCheckout = () => {
@@ -2171,7 +2284,7 @@ export async function runReviewGate(projectRoot, slug, input, dependencies = { i
2171
2284
  prompt = renderPrompt(summary);
2172
2285
  }
2173
2286
  if (verification?.status === "failed") {
2174
- const result = makeResult(currentApprovalGate(), "verification-failed");
2287
+ const result = makeResult(currentApprovalGate(), "verification-failed", describeVerificationFailure(verification, gateId));
2175
2288
  assertReviewRefUnchanged(projectRoot, input.target ?? "HEAD", targetSha);
2176
2289
  persistGateState(projectRoot, result);
2177
2290
  cleanupSharedCheckout();
@@ -2243,6 +2356,23 @@ export async function runReviewGate(projectRoot, slug, input, dependencies = { i
2243
2356
  availability,
2244
2357
  ...(explicitProvider ? { explicitProvider } : {}),
2245
2358
  });
2359
+ if (providerQueue.length === 0) {
2360
+ const candidates = hostNeutralProviderOrder(explicitProvider);
2361
+ const detail = describeSuppressedCandidates(availability, candidates);
2362
+ const availabilityFile = path
2363
+ .relative(projectRoot, reviewAvailabilityPath(projectRoot))
2364
+ .replace(/\\/gu, "/");
2365
+ const result = makeResult(approvalGate, "needs-review", `No review provider was invoked: every candidate provider is suppressed by the ` +
2366
+ `cached availability record at ${availabilityFile}${detail ? ` (${detail})` : ""}. ` +
2367
+ "This is a stale-cache condition, not a live provider outage -- if the " +
2368
+ "account/provider has since recovered or been re-authenticated, delete the stale " +
2369
+ `entry (or the whole file) at ${availabilityFile}, or wait for its suppression ` +
2370
+ `window to elapse, then retry with --resume ${gateId}.`);
2371
+ assertReviewRefUnchanged(projectRoot, input.target ?? "HEAD", targetSha);
2372
+ persistGateState(projectRoot, result);
2373
+ cleanupSharedCheckout();
2374
+ return result;
2375
+ }
2246
2376
  const usedReviewers = new Set([
2247
2377
  ...attempts
2248
2378
  .filter((attempt) => !isRetryableGateAttempt(attempt))
@@ -1,16 +1,18 @@
1
1
  /**
2
- * `wp sync` — projects the canonical webpresso rule/skill catalog into the
3
- * supported host surfaces.
2
+ * `wp sync` — projects the canonical webpresso rule/skill/agent catalog into
3
+ * the supported host surfaces.
4
4
  *
5
- * Projects unified rule + skill content (catalog ∪ consumer) into per-IDE
6
- * surfaces according to `DEFAULT_UNIFIED_CONSUMERS`.
5
+ * Projects unified rule + skill + agent content (catalog ∪ consumer) into
6
+ * per-IDE surfaces according to `DEFAULT_UNIFIED_CONSUMERS`.
7
7
  *
8
8
  * Flags:
9
- * --kind rules|skills Filter to a single kind (default: both).
10
- * --check Dry-run; exit 1 on first drift, no writes.
9
+ * --kind rules|skills|agents Filter to a single kind (default: all).
10
+ * --check Dry-run; exit 1 on first drift, no writes.
11
11
  */
12
12
  import type { CAC } from "cac";
13
+ import type { ContentKind } from "#content/loader";
13
14
  import { type ConsumerContext } from "./init/detect-consumer.js";
15
+ export declare function parseKind(input: string | undefined): readonly ContentKind[] | undefined;
14
16
  export declare function computeSyncDriftCounts(input: {
15
17
  readonly surfaceFixCount: number;
16
18
  readonly agentsChanged: boolean;
@@ -1,13 +1,13 @@
1
1
  /**
2
- * `wp sync` — projects the canonical webpresso rule/skill catalog into the
3
- * supported host surfaces.
2
+ * `wp sync` — projects the canonical webpresso rule/skill/agent catalog into
3
+ * the supported host surfaces.
4
4
  *
5
- * Projects unified rule + skill content (catalog ∪ consumer) into per-IDE
6
- * surfaces according to `DEFAULT_UNIFIED_CONSUMERS`.
5
+ * Projects unified rule + skill + agent content (catalog ∪ consumer) into
6
+ * per-IDE surfaces according to `DEFAULT_UNIFIED_CONSUMERS`.
7
7
  *
8
8
  * Flags:
9
- * --kind rules|skills Filter to a single kind (default: both).
10
- * --check Dry-run; exit 1 on first drift, no writes.
9
+ * --kind rules|skills|agents Filter to a single kind (default: all).
10
+ * --check Dry-run; exit 1 on first drift, no writes.
11
11
  */
12
12
  import { existsSync } from "node:fs";
13
13
  import { join } from "node:path";
@@ -25,14 +25,16 @@ function commandError(message, exitCode = 1) {
25
25
  err.exitCode = exitCode;
26
26
  return err;
27
27
  }
28
- function parseKind(input) {
28
+ export function parseKind(input) {
29
29
  if (input === undefined)
30
30
  return undefined;
31
31
  if (input === "rules" || input === "rule")
32
32
  return ["rule"];
33
33
  if (input === "skills" || input === "skill")
34
34
  return ["skill"];
35
- throw commandError(`Invalid --kind: ${input}. Must be 'rules' or 'skills'.`);
35
+ if (input === "agents" || input === "agent")
36
+ return ["agent"];
37
+ throw commandError(`Invalid --kind: ${input}. Must be 'rules', 'skills', or 'agents'.`);
36
38
  }
37
39
  function formatMismatches(mismatches) {
38
40
  return mismatches.map((m) => ` - [${m.consumerId}] ${m.targetPath}: ${m.reason}`).join("\n");
@@ -90,7 +92,7 @@ export function isUnmaterializedAgentKitSourceWorktree(consumer) {
90
92
  export function registerSyncCommand(cli) {
91
93
  cli
92
94
  .command("sync", "Sync agent rules + skills across all supported host surfaces")
93
- .option("--kind <kind>", "Filter: rules | skills (default: both)")
95
+ .option("--kind <kind>", "Filter: rules | skills | agents (default: all)")
94
96
  .option("--check", "Exit 1 on drift; no writes")
95
97
  .action(async (options = {}) => {
96
98
  const kinds = parseKind(options.kind);
@@ -8,6 +8,18 @@ export interface WorktreeCommandOptions {
8
8
  prefix?: string;
9
9
  dryRun?: boolean;
10
10
  force?: boolean;
11
+ /**
12
+ * Bypass the "primary must be on the expected branch" merge-cleanup
13
+ * refusal by switching a CLEAN primary checkout to that branch. Never
14
+ * bypasses the "primary is dirty" refusal — a dirty primary always
15
+ * refuses, regardless of this flag (see `decideMergeCleanup`).
16
+ */
17
+ switchPrimary?: boolean;
18
+ /**
19
+ * @deprecated Renamed to `switchPrimary`. Kept as an accepted alias only
20
+ * (mapped from the CLI's legacy `--stash-primary` flag) — it no longer
21
+ * stashes anything; a dirty primary always refuses regardless of this flag.
22
+ */
11
23
  stashPrimary?: boolean;
12
24
  noSetup?: boolean;
13
25
  /** Dependency hydration for `new` only: install-locked (default) or reconcile. */
@@ -41,7 +53,6 @@ export interface MergeCleanupPlan {
41
53
  readonly removeArgs: readonly string[];
42
54
  readonly fetchArgs: readonly string[];
43
55
  readonly mergeArgs: readonly string[];
44
- readonly stashArgs?: readonly string[];
45
56
  readonly switchArgs?: readonly string[];
46
57
  readonly preservedPrimaryBranch?: string;
47
58
  }
@@ -54,7 +65,18 @@ export interface MergeCleanupDecisionInput {
54
65
  readonly currentBranch: string;
55
66
  readonly repoDirty: boolean;
56
67
  readonly targetDirty: boolean;
57
- readonly stashPrimary?: boolean;
68
+ /**
69
+ * Bypasses the wrong-branch refusal for a CLEAN primary only. A dirty
70
+ * primary always refuses — see `decideMergeCleanup` — this never bypasses
71
+ * that refusal, and merge-cleanup never stashes primary state.
72
+ */
73
+ readonly switchPrimary?: boolean;
74
+ /**
75
+ * When the primary is dirty, optional porcelain -z lists the offending
76
+ * paths in the refusal message. Omitted messages stay generic (e.g. for
77
+ * unit tests that don't care about message content).
78
+ */
79
+ readonly repoPorcelainZ?: string;
58
80
  /**
59
81
  * When target is dirty, optional porcelain -z + slug enrich the refusal message
60
82
  * (scaffold-only vs other dirt). Omitted messages stay generic for unit tests.