@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
@@ -15,9 +15,10 @@ import { runWorktreeSetup, runWorktreeTeardown } from "#worktrees/lifecycle-scri
15
15
  import { hydrateWorktreeDependencies, reconcileWorktreeDependencies, } from "#worktrees/dependencies.js";
16
16
  import { projectWorktreeAgentSurfaces } from "#worktrees/project-agent-surfaces.js";
17
17
  import { assertNotLocalMainBranch } from "#worktrees/main-ownership.js";
18
- import { formatTargetDirtyRefusal } from "#worktrees/owner-dirt.js";
18
+ import { formatBoundedPaths, formatTargetDirtyRefusal, parsePorcelainV1Z, } from "#worktrees/owner-dirt.js";
19
19
  import { buildWorktreeInventory, canonicalizeWorktreePath, classifyMembership, isManagedWorktreePath, resolveRepoIdentity, } from "#worktrees/identity.js";
20
20
  import { findRegistryCandidatesByPath, planStaleWorktreeRegistryPrune, readWorktreeRegistry, removeWorktreeRegistryEntries, repoScopedPathPredicate, upsertWorktreeRegistryEntry, } from "#worktrees/registry.js";
21
+ import { isStaleWorktreeRegistration, pruneStaleGitWorktreeMetadata, } from "#worktrees/git-metadata.js";
21
22
  import { buildRegistryRepoRootMap, classifyOrphanCandidate, discoverOrphanScanCandidates, resolveRepoRootFromGitPointer, } from "#worktrees/orphan-scan.js";
22
23
  function parseDepsOption(value) {
23
24
  if (value === undefined || value === null || value === "")
@@ -165,9 +166,24 @@ export function planMergeCleanup(targetPath, baseRef) {
165
166
  mergeArgs: ["merge", "--ff-only", baseRef],
166
167
  };
167
168
  }
168
- function primaryStashMessage(input) {
169
- const current = input.currentBranch || "(detached)";
170
- return `wp worktree merge-cleanup preserve primary state from ${current} before syncing ${input.expectedPrimaryBranch} and removing ${basename(input.targetPath)}`;
169
+ /**
170
+ * Refusal for a DIRTY primary checkout. Reuses the same low-level porcelain
171
+ * parser and bounded-list formatter `formatTargetDirtyRefusal` uses for the
172
+ * target worktree (`#worktrees/owner-dirt.js`) — this is NOT a classifier:
173
+ * it never decides whose dirt it is or whether it's "safe" to move. A dirty
174
+ * primary always refuses; this only lists the paths so the human knows what
175
+ * blocked the command.
176
+ */
177
+ function formatPrimaryDirtyRefusal(repoRoot, porcelainZ) {
178
+ const paths = [
179
+ ...new Set(parsePorcelainV1Z(porcelainZ).flatMap((record) => record.paths.filter((path) => path.length > 0))),
180
+ ].toSorted();
181
+ return [
182
+ `wp worktree merge-cleanup refused: primary checkout ${repoRoot} has uncommitted changes`,
183
+ "Paths:",
184
+ formatBoundedPaths(paths),
185
+ "fix: commit or `git stash` the listed paths yourself (or `wp blueprint abandon <slug> --confirm` if this is leftover blueprint draft scaffold), then re-run merge-cleanup",
186
+ ].join("\n");
171
187
  }
172
188
  export function decideMergeCleanup(input) {
173
189
  const entry = input.entries.find((candidate) => candidate.path === input.targetPath);
@@ -193,34 +209,18 @@ export function decideMergeCleanup(input) {
193
209
  throw new Error(`wp worktree merge-cleanup refused: ${input.targetPath} has uncommitted changes`);
194
210
  }
195
211
  if (input.repoDirty) {
196
- if (input.stashPrimary === true) {
197
- return {
198
- ...plan,
199
- stashArgs: [
200
- "stash",
201
- "push",
202
- "--include-untracked",
203
- "--message",
204
- primaryStashMessage({
205
- currentBranch: input.currentBranch,
206
- expectedPrimaryBranch: plan.expectedPrimaryBranch,
207
- targetPath: input.targetPath,
208
- }),
209
- ],
210
- ...(input.currentBranch !== plan.expectedPrimaryBranch
211
- ? {
212
- switchArgs: ["switch", plan.expectedPrimaryBranch],
213
- preservedPrimaryBranch: input.currentBranch || "(detached)",
214
- }
215
- : {
216
- preservedPrimaryBranch: input.currentBranch || "(detached)",
217
- }),
218
- };
219
- }
220
- throw new Error(`wp worktree merge-cleanup refused: primary checkout ${input.repoRoot} has uncommitted changes`);
212
+ // A dirty primary ALWAYS refuses -- unconditionally, regardless of
213
+ // --switch-primary/--stash-primary. merge-cleanup never stashes primary
214
+ // state; it never decides whose dirt it is or whether moving it is safe.
215
+ throw new Error(input.repoPorcelainZ !== undefined
216
+ ? formatPrimaryDirtyRefusal(input.repoRoot, input.repoPorcelainZ)
217
+ : `wp worktree merge-cleanup refused: primary checkout ${input.repoRoot} has uncommitted changes`);
221
218
  }
222
219
  if (input.currentBranch !== plan.expectedPrimaryBranch) {
223
- if (input.stashPrimary === true) {
220
+ // --switch-primary bypasses ONLY this refusal, and only for a CLEAN
221
+ // primary: switching a clean checkout to the expected branch is
222
+ // harmless (nothing to lose), unlike stashing dirty state.
223
+ if (input.switchPrimary === true) {
224
224
  return {
225
225
  ...plan,
226
226
  switchArgs: ["switch", plan.expectedPrimaryBranch],
@@ -456,10 +456,14 @@ function handleRefresh(opts) {
456
456
  async function handlePrune(opts) {
457
457
  if (!opts.all)
458
458
  throw new Error("Usage: wp worktree prune --all");
459
+ const repoRoot = resolveWorktreeRepoRoot(opts.repo ?? opts.cwd ?? process.cwd());
460
+ // Reconcile git's own admin records FIRST: a hand-deleted worktree directory
461
+ // leaves a record that still claims its branch, and it would otherwise report
462
+ // as live below and pin the matching registry entry in place too.
463
+ const gitPruned = pruneStaleGitWorktreeMetadata(repoRoot, listEntries(repoRoot));
459
464
  // Repository-scoped, three-state prune: only THIS repo's entries that are
460
465
  // proven stale against a COMPLETE git-live inventory are dropped; unknown,
461
466
  // other-repo, and live-legacy entries are retained.
462
- const repoRoot = resolveWorktreeRepoRoot(opts.repo ?? opts.cwd ?? process.cwd());
463
467
  const inventory = buildWorktreeInventory(repoRoot);
464
468
  const result = planStaleWorktreeRegistryPrune({
465
469
  inventory,
@@ -468,6 +472,13 @@ async function handlePrune(opts) {
468
472
  await removeCodexProjectTrusts(result.removed.map((entry) => entry.path));
469
473
  result.commit();
470
474
  console.log(`Pruned ${result.removed.length} stale managed registry entr${result.removed.length === 1 ? "y" : "ies"}.`);
475
+ if (gitPruned.error)
476
+ console.log(`warning: git worktree prune failed: ${gitPruned.error}`);
477
+ else {
478
+ console.log(`Cleared ${gitPruned.prunedPaths.length} stale git worktree registration${gitPruned.prunedPaths.length === 1 ? "" : "s"}.`);
479
+ for (const path of gitPruned.prunedPaths)
480
+ console.log(` ${path}`);
481
+ }
471
482
  for (const warning of result.warnings)
472
483
  console.log(`warning: ${warning}`);
473
484
  }
@@ -546,6 +557,23 @@ async function handleRemove(nameOrPath, opts) {
546
557
  const repoRoot = resolvePrimaryRoot(callerRoot, entries);
547
558
  const resolved = resolveWorktreePath(nameOrPath, entries);
548
559
  const entry = entries.find((candidate) => candidate.path === resolved);
560
+ // The directory is already gone (hand-deleted, or a failed earlier removal):
561
+ // `git worktree remove` cannot act on it and teardown scripts have no cwd, so
562
+ // reconcile git's leftover admin record instead of failing.
563
+ if (isStaleWorktreeRegistration(entry, resolved, existsSync)) {
564
+ // Staleness is not a bypass for the managed-root boundary, which is the
565
+ // same protection the MCP surface applies to registry-gone stale targets.
566
+ if (!isManagedWorktreePath(canonicalizeWorktreePath(resolved), resolveKnownManagedWorktreeRoots())) {
567
+ throw new Error(`Refusing to reconcile ${resolved}: its directory is gone but it is not under a known managed worktree root.`);
568
+ }
569
+ const gitPruned = pruneStaleGitWorktreeMetadata(repoRoot, entries);
570
+ if (gitPruned.error)
571
+ throw new Error(`git worktree prune failed: ${gitPruned.error}`);
572
+ await removeCodexProjectTrust(resolved);
573
+ removeWorktreeRegistryEntries(repoScopedPathPredicate(resolved, { authoritativeRepoKey: resolveRepoIdentity(repoRoot) }));
574
+ console.log(`Cleared stale worktree registration for ${resolved} (directory already gone).`);
575
+ return;
576
+ }
549
577
  if (!opts.force) {
550
578
  runWorktreeTeardown(readConfig(resolved) ?? readConfig(repoRoot), {
551
579
  worktreePath: resolved,
@@ -713,6 +741,22 @@ async function handleMergeCleanup(nameOrPath, opts) {
713
741
  })()
714
742
  : undefined;
715
743
  const targetBlueprintSlug = inferBlueprintSlugFromOwnerPath(resolved);
744
+ // `--switch-primary`/legacy `--stash-primary` only ever bypasses the
745
+ // clean-primary wrong-branch refusal; a dirty primary always refuses (see
746
+ // `decideMergeCleanup`), so merge-cleanup never stashes primary state.
747
+ const switchPrimary = opts.switchPrimary === true || opts.stashPrimary === true;
748
+ const repoDirty = isDirty(repoRoot);
749
+ const repoPorcelainZ = repoDirty
750
+ ? (() => {
751
+ const status = spawnSync("git", ["status", "--porcelain=v1", "--untracked-files=all", "--ignored=no", "-z"], { cwd: repoRoot, encoding: "utf8" });
752
+ // Unlike the target-worktree probe, do NOT substitute a sentinel path
753
+ // on failure: a fake "(git-status-failed)" path in the refusal message
754
+ // would misrepresent what's actually dirty. Leaving this undefined
755
+ // falls back to the generic (path-free) refusal in decideMergeCleanup
756
+ // -- the refusal itself is unaffected either way.
757
+ return status.status === 0 ? String(status.stdout ?? "") : undefined;
758
+ })()
759
+ : undefined;
716
760
  const plan = decideMergeCleanup({
717
761
  repoRoot,
718
762
  targetPath: resolved,
@@ -720,16 +764,15 @@ async function handleMergeCleanup(nameOrPath, opts) {
720
764
  entries,
721
765
  registryEntries: scopedRegistryEntries,
722
766
  currentBranch: currentBranch(repoRoot),
723
- repoDirty: isDirty(repoRoot),
767
+ repoDirty,
724
768
  targetDirty,
725
- stashPrimary: opts.stashPrimary === true,
769
+ switchPrimary,
726
770
  ...(targetPorcelainZ !== undefined ? { targetPorcelainZ } : {}),
727
771
  ...(targetBlueprintSlug !== undefined ? { targetBlueprintSlug } : {}),
772
+ ...(repoPorcelainZ !== undefined ? { repoPorcelainZ } : {}),
728
773
  });
729
774
  if (opts.dryRun) {
730
775
  console.log("[dry-run] Would run merge cleanup:");
731
- if (plan.stashArgs)
732
- console.log(` stash: git ${plan.stashArgs.join(" ")}`);
733
776
  if (plan.switchArgs)
734
777
  console.log(` switch: git ${plan.switchArgs.join(" ")}`);
735
778
  console.log(` remove: git ${plan.removeArgs.join(" ")}`);
@@ -737,12 +780,6 @@ async function handleMergeCleanup(nameOrPath, opts) {
737
780
  console.log(` sync: git ${plan.mergeArgs.join(" ")}`);
738
781
  return;
739
782
  }
740
- if (plan.stashArgs) {
741
- const stashResult = spawnSync("git", [...plan.stashArgs], { cwd: repoRoot, stdio: "inherit" });
742
- if (stashResult.status !== 0) {
743
- throw new Error("git stash push failed");
744
- }
745
- }
746
783
  if (plan.switchArgs) {
747
784
  const switchResult = spawnSync("git", [...plan.switchArgs], {
748
785
  cwd: repoRoot,
@@ -775,13 +812,8 @@ async function handleMergeCleanup(nameOrPath, opts) {
775
812
  throw new Error(`git merge --ff-only ${baseRef} failed`);
776
813
  }
777
814
  console.log(`Removed worktree ${resolved} and fast-forwarded ${repoRoot} to ${baseRef}.`);
778
- if (plan.stashArgs) {
779
- const stashRef = spawnSync("git", ["stash", "list", "--format=%gd %gs", "-n", "1"], {
780
- cwd: repoRoot,
781
- encoding: "utf8",
782
- });
783
- const summary = String(stashRef.stdout ?? "").trim();
784
- console.log(`Preserved prior primary checkout state${plan.preservedPrimaryBranch ? ` from ${plan.preservedPrimaryBranch}` : ""} in ${summary || "git stash list"}.`);
815
+ if (plan.switchArgs && plan.preservedPrimaryBranch) {
816
+ console.log(`Switched primary checkout from ${plan.preservedPrimaryBranch} to ${plan.expectedPrimaryBranch}.`);
785
817
  }
786
818
  }
787
819
  export async function executeWorktreeSubcommand(subcommand, args, opts) {
@@ -27,7 +27,9 @@ const HELP_TEXT = [
27
27
  " --dry-run Print the resolved worktree target without writing",
28
28
  " --no-setup Skip configured worktree.setup scripts (new only)",
29
29
  " --force Skip configured teardown scripts and force removal",
30
- " --stash-primary Stash dirty primary state and switch to the expected primary branch before merge-cleanup",
30
+ " --switch-primary Switch a CLEAN primary checkout to the expected branch before merge-cleanup",
31
+ " (a DIRTY primary always refuses, with or without this flag; it is never stashed)",
32
+ " --stash-primary Deprecated alias for --switch-primary. No longer stashes anything.",
31
33
  " --orphans Filesystem-level orphan sweep for prune (see prune --orphans above)",
32
34
  " --repo-id <id> Scope prune --orphans to one repo namespace under repos/<id>",
33
35
  " --yes Alias for --force (prune --orphans only)",
@@ -44,7 +46,8 @@ export function registerWorktreeRouter(cli) {
44
46
  .option("--no-setup", "Skip configured worktree.setup scripts (new only)")
45
47
  .option("--deps <mode>", "Dependency hydration for new: install-locked (default, pnpm GVS path) or reconcile")
46
48
  .option("--force", "Skip configured teardown scripts and force removal (remove/merge-cleanup only)")
47
- .option("--stash-primary", "Stash dirty primary state and switch to the expected primary branch before merge-cleanup")
49
+ .option("--switch-primary", "Switch a CLEAN primary checkout to the expected branch before merge-cleanup (a dirty primary always refuses; never stashed)")
50
+ .option("--stash-primary", "Deprecated alias for --switch-primary. No longer stashes anything.")
48
51
  .option("--all", "Use global managed inventory where supported")
49
52
  .option("--repo <dir>", "Repo root for refresh/rebind (default: cwd)")
50
53
  .option("--orphans", "Filesystem-level orphan sweep for prune (dry-run unless --force/--yes)")
@@ -11,7 +11,7 @@ import { canonicalizeWorktreePath } from "#worktrees/identity.js";
11
11
  import { dashProviderOwnershipName, parseDashPermissionMode, } from "#cli/commands/dash/provider-capabilities.js";
12
12
  import { optionalToolCanonicalCommand } from "#cli/optional-tools.js";
13
13
  import { commandExists } from "#runtime/command-exists.js";
14
- import { ensureCodexProjectMcpServers, ensureCodexWebpressoMcp, } from "#cli/commands/init/scaffolders/codex-mcp/index.js";
14
+ import { ensureCodexProjectMcpServers, findInstalledCodexPluginMcpManifests, pruneLegacyCodexWebpressoMcp, } from "#cli/commands/init/scaffolders/codex-mcp/index.js";
15
15
  export class DirectProviderLaunchPreparationError extends Error {
16
16
  action;
17
17
  constructor(message, action) {
@@ -91,11 +91,19 @@ function validateDirectProviderLaunch(provider, cwd) {
91
91
  vpCommand,
92
92
  };
93
93
  }
94
+ /**
95
+ * `wp codex` is the only path that reaches machines which never re-run setup,
96
+ * so it carries the legacy-channel retirement too. The surviving channel is
97
+ * the bundled Codex plugin: probe its cache directly and WARN when it is
98
+ * absent. A `codex plugin marketplace` subprocess here would add latency to
99
+ * every launch and could hard-fail a machine where the plugin already works —
100
+ * a degraded session beats a blocked launch.
101
+ */
94
102
  function ensureCodexSessionTooling(repoRoot) {
95
103
  const options = { dryRun: false, overwrite: false };
96
- const webpresso = ensureCodexWebpressoMcp({ options });
97
- if (webpresso.kind === "codex-webpresso-mcp-not-installed") {
98
- throw new DirectProviderLaunchPreparationError(`Codex webpresso MCP is not configured because no usable agent-kit install root was found. Checked: ${webpresso.checked.join(", ") || "(none)"}`, "Run `wp setup --yes-global` from this repo, then retry `wp codex`.");
104
+ pruneLegacyCodexWebpressoMcp({ options });
105
+ if (findInstalledCodexPluginMcpManifests().length === 0) {
106
+ console.warn("⚠ The webpresso Codex plugin is not installed, so the webpresso MCP server will be unavailable in this session. Run `wp setup` to install it.");
99
107
  }
100
108
  const project = ensureCodexProjectMcpServers({ repoRoot, options });
101
109
  if (project.kind === "codex-project-mcp-invalid") {
@@ -5,5 +5,14 @@ export interface FlattenedAssets {
5
5
  }
6
6
  /** Reads `.agent/{skills,commands,agents}/` and returns an in-memory flattened structure. */
7
7
  export declare function flattenAgentDir(agentDir: string): FlattenedAssets;
8
- /** Writes flattened assets to a directory structure mirroring rulesync's expected layout. */
8
+ /**
9
+ * Writes flattened assets to the layout rulesync reads.
10
+ *
11
+ * The three shapes are not interchangeable, and getting one wrong is silent:
12
+ * rulesync discovers skills by globbing `skills/` for *directories* and reading
13
+ * `SKILL.md` inside each, reads subagents from `subagents/` (not `agents/`),
14
+ * and reads commands as flat `commands/*.md`. An asset written in the wrong
15
+ * shape is simply never seen — rulesync still exits 0, having generated
16
+ * nothing for it.
17
+ */
9
18
  export declare function writeFlattenedAssets(assets: FlattenedAssets, outDir: string): Promise<void>;
@@ -60,19 +60,29 @@ export function flattenAgentDir(agentDir) {
60
60
  }
61
61
  return { skills, commands, agents };
62
62
  }
63
- /** Writes flattened assets to a directory structure mirroring rulesync's expected layout. */
63
+ /**
64
+ * Writes flattened assets to the layout rulesync reads.
65
+ *
66
+ * The three shapes are not interchangeable, and getting one wrong is silent:
67
+ * rulesync discovers skills by globbing `skills/` for *directories* and reading
68
+ * `SKILL.md` inside each, reads subagents from `subagents/` (not `agents/`),
69
+ * and reads commands as flat `commands/*.md`. An asset written in the wrong
70
+ * shape is simply never seen — rulesync still exits 0, having generated
71
+ * nothing for it.
72
+ */
64
73
  export async function writeFlattenedAssets(assets, outDir) {
65
- const subdirs = ["skills", "commands", "agents"];
74
+ const subdirs = ["skills", "commands", "subagents"];
66
75
  for (const sub of subdirs) {
67
76
  mkdirSync(join(outDir, sub), { recursive: true });
68
77
  }
69
78
  for (const [name, content] of Object.entries(assets.skills)) {
70
- writeFileSync(join(outDir, "skills", `${name}.md`), content);
79
+ mkdirSync(join(outDir, "skills", name), { recursive: true });
80
+ writeFileSync(join(outDir, "skills", name, "SKILL.md"), content);
71
81
  }
72
82
  for (const [name, content] of Object.entries(assets.commands)) {
73
83
  writeFileSync(join(outDir, "commands", `${name}.md`), content);
74
84
  }
75
85
  for (const [name, content] of Object.entries(assets.agents)) {
76
- writeFileSync(join(outDir, "agents", `${name}.md`), content);
86
+ writeFileSync(join(outDir, "subagents", `${name}.md`), content);
77
87
  }
78
88
  }
@@ -0,0 +1,22 @@
1
+ import { type BlockRegistry } from "./registry-resolve.js";
2
+ export interface SnapshotIntegrityResult {
3
+ readonly ok: boolean;
4
+ readonly path: string;
5
+ readonly registry?: BlockRegistry;
6
+ readonly diagnostics: readonly string[];
7
+ }
8
+ /**
9
+ * Fail-on-drift **equivalent** for agent-kit CI without a framework dependency:
10
+ * load the committed catalog snapshot and enforce generator-contract invariants
11
+ * (schema parse, version shape, unique name+view, well-formed slots).
12
+ *
13
+ * Full regenerate-and-diff against `@webpresso/codegen` remains a cross-repo
14
+ * operator step; this gate prevents a rotten or hand-broken snapshot from landing.
15
+ */
16
+ /**
17
+ * Check (does not throw). Callers must inspect `.ok` — this is a gate helper,
18
+ * not a Node assertion that aborts the process.
19
+ */
20
+ export declare function checkAdminBlocksSnapshotIntegrity(options?: {
21
+ readonly snapshotPath?: string;
22
+ }): SnapshotIntegrityResult;
@@ -0,0 +1,90 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { blockRegistrySchema, defaultSnapshotPath, } from "./registry-resolve.js";
3
+ const SEMVER_LIKE = /^\d+\.\d+\.\d+/;
4
+ const slotSchemaLoose = {
5
+ isSlot(value) {
6
+ if (value === null || typeof value !== "object" || Array.isArray(value))
7
+ return false;
8
+ const rec = value;
9
+ return typeof rec["name"] === "string" && rec["name"].length > 0;
10
+ },
11
+ };
12
+ /**
13
+ * Fail-on-drift **equivalent** for agent-kit CI without a framework dependency:
14
+ * load the committed catalog snapshot and enforce generator-contract invariants
15
+ * (schema parse, version shape, unique name+view, well-formed slots).
16
+ *
17
+ * Full regenerate-and-diff against `@webpresso/codegen` remains a cross-repo
18
+ * operator step; this gate prevents a rotten or hand-broken snapshot from landing.
19
+ */
20
+ /**
21
+ * Check (does not throw). Callers must inspect `.ok` — this is a gate helper,
22
+ * not a Node assertion that aborts the process.
23
+ */
24
+ export function checkAdminBlocksSnapshotIntegrity(options = {}) {
25
+ const path = options.snapshotPath ?? defaultSnapshotPath();
26
+ const diagnostics = [];
27
+ let raw;
28
+ try {
29
+ raw = readFileSync(path, "utf8");
30
+ }
31
+ catch (error) {
32
+ const detail = error instanceof Error ? error.message : String(error);
33
+ return { ok: false, path, diagnostics: [`cannot read snapshot: ${detail}`] };
34
+ }
35
+ let parsed;
36
+ try {
37
+ parsed = JSON.parse(raw);
38
+ }
39
+ catch (error) {
40
+ const detail = error instanceof Error ? error.message : String(error);
41
+ return { ok: false, path, diagnostics: [`snapshot is not valid JSON: ${detail}`] };
42
+ }
43
+ const result = blockRegistrySchema.safeParse(parsed);
44
+ if (!result.success) {
45
+ return {
46
+ ok: false,
47
+ path,
48
+ diagnostics: result.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`),
49
+ };
50
+ }
51
+ const registry = result.data;
52
+ if (!SEMVER_LIKE.test(registry.version)) {
53
+ diagnostics.push(`version "${registry.version}" is not semver-like`);
54
+ }
55
+ if (registry.minVersion !== undefined && !SEMVER_LIKE.test(registry.minVersion)) {
56
+ diagnostics.push(`minVersion "${registry.minVersion}" is not semver-like`);
57
+ }
58
+ const seen = new Set();
59
+ for (const [index, block] of registry.blocks.entries()) {
60
+ const key = `${block.view}::${block.name}`;
61
+ if (seen.has(key)) {
62
+ diagnostics.push(`duplicate block ${key} at index ${index}`);
63
+ }
64
+ seen.add(key);
65
+ if (block.slots !== undefined) {
66
+ if (!Array.isArray(block.slots)) {
67
+ diagnostics.push(`blocks[${index}].slots must be an array`);
68
+ continue;
69
+ }
70
+ for (const [slotIndex, slot] of block.slots.entries()) {
71
+ if (!slotSchemaLoose.isSlot(slot)) {
72
+ diagnostics.push(`blocks[${index}].slots[${slotIndex}] requires string name`);
73
+ continue;
74
+ }
75
+ const accepts = slot.accepts;
76
+ if (accepts !== undefined) {
77
+ if (!Array.isArray(accepts) || accepts.some((a) => typeof a !== "string")) {
78
+ diagnostics.push(`blocks[${index}].slots[${slotIndex}].accepts must be string[] when present`);
79
+ }
80
+ }
81
+ }
82
+ }
83
+ }
84
+ return {
85
+ ok: diagnostics.length === 0,
86
+ path,
87
+ registry,
88
+ diagnostics,
89
+ };
90
+ }
@@ -26,7 +26,15 @@ export interface ComposeResult {
26
26
  readonly registrySource?: "project" | "snapshot";
27
27
  readonly registryVersion?: string;
28
28
  readonly warnings: readonly string[];
29
+ /**
30
+ * Admin-crud only: true when top-level `blocks[]` types were checked against
31
+ * the registry. false when paint-path shape succeeded without blocks[] checks.
32
+ * Omitted for ops-report / unknown profile.
33
+ */
34
+ readonly blockTypesValidated?: boolean;
29
35
  }
36
+ /** Structural paint-path shape used by platform JsonRender list consumers. */
37
+ export declare function isPaintPathListSpec(spec: Record<string, unknown>): boolean;
30
38
  /**
31
39
  * Profile-discriminated compose validation (tool-level).
32
40
  * Registry resolve may walk the filesystem from `cwd` / `process.cwd()` when
@@ -6,7 +6,10 @@ const adminCrudInputSchema = z
6
6
  .object({
7
7
  profile: z.literal("admin-crud"),
8
8
  kind: adminKindSchema,
9
- /** Opaque page/spec payload; blocks[] validated against registry when present. */
9
+ /**
10
+ * Page-definition `blocks[]` and/or JsonRender* list/detail/form shapes.
11
+ * Empty objects and non-records are illegal (honesty contract).
12
+ */
10
13
  spec: z.record(z.string(), z.unknown()),
11
14
  })
12
15
  .strict();
@@ -26,25 +29,118 @@ function zodToDiagnostics(error) {
26
29
  message: issue.message,
27
30
  }));
28
31
  }
32
+ /** Structural paint-path shape used by platform JsonRender list consumers. */
33
+ export function isPaintPathListSpec(spec) {
34
+ return typeof spec["entity"] === "string" && Array.isArray(spec["columns"]);
35
+ }
36
+ function isPaintPathDetailOrFormSpec(spec) {
37
+ // Lightweight honesty: non-empty object with at least one recognized key.
38
+ return (typeof spec["entity"] === "string" ||
39
+ typeof spec["title"] === "string" ||
40
+ Array.isArray(spec["fields"]) ||
41
+ Array.isArray(spec["sections"]));
42
+ }
43
+ function hasRecognizedAdminShape(kind, spec) {
44
+ if (Array.isArray(spec["blocks"]))
45
+ return true;
46
+ if (kind === "list")
47
+ return isPaintPathListSpec(spec);
48
+ return isPaintPathDetailOrFormSpec(spec);
49
+ }
50
+ function slotDefsForBlock(registry, kind, type) {
51
+ const def = registry.blocks.find((b) => b.name === type && b.view === kind);
52
+ if (!def?.slots || !Array.isArray(def.slots))
53
+ return [];
54
+ const out = [];
55
+ for (const raw of def.slots) {
56
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
57
+ continue;
58
+ const rec = raw;
59
+ const name = rec["name"];
60
+ if (typeof name !== "string" || name.length === 0)
61
+ continue;
62
+ const acceptsRaw = rec["accepts"];
63
+ const accepts = Array.isArray(acceptsRaw)
64
+ ? acceptsRaw.filter((a) => typeof a === "string")
65
+ : [];
66
+ out.push({ name, accepts });
67
+ }
68
+ return out;
69
+ }
70
+ /**
71
+ * Slot children are only hard-checked when they look like typed registry blocks
72
+ * (`{ type: string, ... }`). String enums (JsonRender list actions) and ActionSpec
73
+ * objects (`id`/`label`) are legal paint shapes — warn that they were not
74
+ * registry-type-checked instead of dishonestly rejecting them.
75
+ */
76
+ function validateSlotChildren(blockPath, block, slots, diagnostics, warnings) {
77
+ for (const slot of slots) {
78
+ const value = block[slot.name];
79
+ if (value === undefined)
80
+ continue;
81
+ const items = Array.isArray(value) ? value : [value];
82
+ const acceptsList = [...slot.accepts].sort().join(", ");
83
+ for (const [childIndex, child] of items.entries()) {
84
+ const path = `${blockPath}.${slot.name}.${childIndex}`;
85
+ // JsonRender-style string action enums (e.g. "create" | "export")
86
+ if (typeof child === "string") {
87
+ warnings.push(`${path}: slot "${slot.name}" string child not registry-type-checked (paint-path action enum shape)`);
88
+ continue;
89
+ }
90
+ if (child === null || typeof child !== "object" || Array.isArray(child)) {
91
+ warnings.push(`${path}: slot "${slot.name}" child is not a typed block object; not registry-type-checked`);
92
+ continue;
93
+ }
94
+ const childType = child["type"];
95
+ if (typeof childType !== "string" || childType.length === 0) {
96
+ // ActionSpec-like { id, label, ... } without registry type
97
+ warnings.push(`${path}: slot "${slot.name}" object has no string type; not registry-type-checked`);
98
+ continue;
99
+ }
100
+ if (slot.accepts.length > 0 && !slot.accepts.includes(childType)) {
101
+ diagnostics.push({
102
+ path: `${path}.type`,
103
+ message: `illegal slot type "${childType}" for "${slot.name}"; accepts: ${acceptsList}`,
104
+ });
105
+ }
106
+ }
107
+ }
108
+ }
29
109
  function validateAdminBlocks(kind, spec, resolved) {
30
110
  const diagnostics = [];
31
111
  const warnings = [];
32
112
  const allowed = blockNamesForView(resolved.registry, kind);
113
+ const allowedList = [...allowed].sort().join(", ");
33
114
  const blocks = spec["blocks"];
115
+ if (Object.keys(spec).length === 0) {
116
+ diagnostics.push({
117
+ path: "spec",
118
+ message: "admin-crud spec is empty; provide blocks[] (page composition) or a paint-path shape (e.g. list entity+columns)",
119
+ });
120
+ return { diagnostics, warnings, blockTypesValidated: false };
121
+ }
34
122
  if (blocks === undefined) {
35
- warnings.push("admin-crud spec has no top-level blocks[]; registry types were not checked (validate-only passthrough).");
36
- return { diagnostics, warnings };
123
+ if (!hasRecognizedAdminShape(kind, spec)) {
124
+ diagnostics.push({
125
+ path: "spec",
126
+ message: "admin-crud spec has no blocks[] and no recognized paint-path fields; cannot claim registry validation",
127
+ });
128
+ return { diagnostics, warnings, blockTypesValidated: false };
129
+ }
130
+ warnings.push("admin-crud spec has no top-level blocks[]; registry block types were not checked (paint-path / JsonRender shape).");
131
+ return { diagnostics, warnings, blockTypesValidated: false };
37
132
  }
38
133
  if (!Array.isArray(blocks)) {
39
134
  diagnostics.push({ path: "spec.blocks", message: "blocks must be an array" });
40
- return { diagnostics, warnings };
135
+ return { diagnostics, warnings, blockTypesValidated: false };
41
136
  }
42
137
  for (const [index, block] of blocks.entries()) {
43
138
  if (block === null || typeof block !== "object" || Array.isArray(block)) {
44
139
  diagnostics.push({ path: `spec.blocks.${index}`, message: "block must be an object" });
45
140
  continue;
46
141
  }
47
- const type = block["type"];
142
+ const record = block;
143
+ const type = record["type"];
48
144
  if (typeof type !== "string" || type.length === 0) {
49
145
  diagnostics.push({
50
146
  path: `spec.blocks.${index}.type`,
@@ -55,11 +151,20 @@ function validateAdminBlocks(kind, spec, resolved) {
55
151
  if (!allowed.has(type)) {
56
152
  diagnostics.push({
57
153
  path: `spec.blocks.${index}.type`,
58
- message: `illegal block type "${type}" for ${kind}; allowed: ${[...allowed].sort().join(", ")}`,
154
+ message: `illegal block type "${type}" for ${kind}; allowed: ${allowedList}`,
59
155
  });
156
+ continue;
157
+ }
158
+ const def = resolved.registry.blocks.find((b) => b.name === type && b.view === kind);
159
+ if (def?.deprecated === true) {
160
+ warnings.push(`spec.blocks.${index}.type "${type}" is deprecated in the registry`);
161
+ }
162
+ const slots = slotDefsForBlock(resolved.registry, kind, type);
163
+ if (slots.length > 0) {
164
+ validateSlotChildren(`spec.blocks.${index}`, record, slots, diagnostics, warnings);
60
165
  }
61
166
  }
62
- return { diagnostics, warnings };
167
+ return { diagnostics, warnings, blockTypesValidated: true };
63
168
  }
64
169
  /**
65
170
  * Profile-discriminated compose validation (tool-level).
@@ -118,7 +223,7 @@ export function composeValidate(raw, options = {}) {
118
223
  };
119
224
  }
120
225
  }
121
- const { diagnostics: blockDiagnostics, warnings: blockWarnings } = validateAdminBlocks(input.kind, input.spec, resolved);
226
+ const { diagnostics: blockDiagnostics, warnings: blockWarnings, blockTypesValidated, } = validateAdminBlocks(input.kind, input.spec, resolved);
122
227
  const warnings = [...resolved.warnings, ...blockWarnings];
123
228
  if (blockDiagnostics.length > 0) {
124
229
  return {
@@ -129,6 +234,7 @@ export function composeValidate(raw, options = {}) {
129
234
  registrySource: resolved.source,
130
235
  registryVersion: resolved.registryVersion,
131
236
  warnings,
237
+ blockTypesValidated,
132
238
  };
133
239
  }
134
240
  return {
@@ -139,5 +245,6 @@ export function composeValidate(raw, options = {}) {
139
245
  registrySource: resolved.source,
140
246
  registryVersion: resolved.registryVersion,
141
247
  warnings,
248
+ blockTypesValidated,
142
249
  };
143
250
  }