@webpresso/agent-kit 3.3.3 → 3.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD-PARTY-NOTICES.md +12 -10
- package/catalog/agent/rules/changeset-release.md +15 -4
- package/catalog/agent/rules/managed-tool-binaries.md +67 -0
- package/catalog/agent/rules/parallel-orchestration-quota.md +99 -1
- package/catalog/agent/rules/pre-implementation.md +36 -20
- package/catalog/agent/rules/rtk-routing.md +22 -16
- package/catalog/agent/skills/autopilot/SKILL.md +4 -3
- package/catalog/agent/skills/ultragoal/SKILL.md +11 -9
- package/dist/esm/audit/hook-surface.js +5 -1
- package/dist/esm/audit/repo-guardrails.d.ts +8 -0
- package/dist/esm/audit/repo-guardrails.js +71 -1
- package/dist/esm/blueprint/core/parser.js +38 -12
- package/dist/esm/blueprint/execution/artifacts.js +8 -4
- package/dist/esm/blueprint/execution/frontmatter-keys.d.ts +17 -0
- package/dist/esm/blueprint/execution/frontmatter-keys.js +24 -0
- package/dist/esm/blueprint/execution/metadata.js +8 -4
- package/dist/esm/blueprint/lifecycle/audit.d.ts +21 -0
- package/dist/esm/blueprint/lifecycle/audit.js +89 -3
- package/dist/esm/blueprint/lifecycle/review-provenance.js +63 -10
- package/dist/esm/blueprint/lifecycle/rollout-anchor.d.ts +22 -0
- package/dist/esm/blueprint/lifecycle/rollout-anchor.js +37 -1
- package/dist/esm/blueprint/markdown/blocked-line.d.ts +49 -0
- package/dist/esm/blueprint/markdown/blocked-line.js +63 -0
- package/dist/esm/blueprint/markdown/helpers.js +10 -4
- package/dist/esm/blueprint/tracked-document/parser.js +3 -3
- package/dist/esm/blueprint/trust/gate-row.d.ts +55 -0
- package/dist/esm/blueprint/trust/gate-row.js +113 -0
- package/dist/esm/blueprint/trust/promotion.d.ts +17 -0
- package/dist/esm/blueprint/trust/promotion.js +19 -19
- package/dist/esm/build/atomic-file-copy.d.ts +32 -0
- package/dist/esm/build/atomic-file-copy.js +44 -0
- package/dist/esm/build/vendor-oxlint-plugins.d.ts +8 -0
- package/dist/esm/build/vendor-oxlint-plugins.js +53 -0
- package/dist/esm/cli/commands/blueprint/mutations.d.ts +15 -0
- package/dist/esm/cli/commands/blueprint/mutations.js +8 -2
- package/dist/esm/cli/commands/compile.d.ts +13 -0
- package/dist/esm/cli/commands/compile.js +182 -62
- package/dist/esm/cli/commands/format.js +17 -1
- package/dist/esm/cli/commands/gain/index.js +10 -3
- package/dist/esm/cli/commands/init/gitignore-patcher.js +6 -0
- package/dist/esm/cli/commands/init/index.js +29 -48
- package/dist/esm/cli/commands/init/mcp-spec.d.ts +48 -13
- package/dist/esm/cli/commands/init/mcp-spec.js +86 -30
- package/dist/esm/cli/commands/init/merge.js +30 -10
- package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.d.ts +56 -31
- package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +180 -43
- package/dist/esm/cli/commands/init/scaffolders/rtk/index.d.ts +17 -5
- package/dist/esm/cli/commands/init/scaffolders/rtk/index.js +56 -75
- package/dist/esm/cli/commands/init/scaffolders/runtime-check/index.js +7 -6
- package/dist/esm/cli/commands/init/untracked-collision.d.ts +12 -1
- package/dist/esm/cli/commands/init/untracked-collision.js +23 -1
- package/dist/esm/cli/commands/pr-help.js +1 -1
- package/dist/esm/cli/commands/pr.js +18 -4
- package/dist/esm/cli/commands/review.js +60 -6
- package/dist/esm/cli/commands/worktree/router-dispatch.d.ts +24 -2
- package/dist/esm/cli/commands/worktree/router-dispatch.js +80 -48
- package/dist/esm/cli/commands/worktree/router.js +5 -2
- package/dist/esm/cli/direct-provider-launch.js +12 -4
- package/dist/esm/compiler/flatten.d.ts +10 -1
- package/dist/esm/compiler/flatten.js +14 -4
- package/dist/esm/compose/admin-blocks-registry-integrity.d.ts +22 -0
- package/dist/esm/compose/admin-blocks-registry-integrity.js +90 -0
- package/dist/esm/compose/compose.d.ts +8 -0
- package/dist/esm/compose/compose.js +115 -8
- package/dist/esm/compose/ops-report-schema.js +17 -3
- package/dist/esm/compose/registry-resolve.d.ts +17 -0
- package/dist/esm/compose/registry-resolve.js +78 -11
- package/dist/esm/config/oxlint/graphql-conventions.js +2 -5
- package/dist/esm/config/oxlint/import-hygiene.js +21 -8
- package/dist/esm/config/oxlint/oxlint-config.fixture.d.ts +1 -1
- package/dist/esm/config/oxlint/oxlint-config.fixture.js +2 -3
- package/dist/esm/config/oxlint/oxlintrc.d.ts +5 -7
- package/dist/esm/config/oxlint/oxlintrc.js +18 -21
- package/dist/esm/config/oxlint/path-roles.js +53 -0
- package/dist/esm/config/oxlint/query-patterns.js +2 -5
- package/dist/esm/daemon/domains/blueprint-exec-ledger.d.ts +86 -0
- package/dist/esm/daemon/domains/blueprint-exec-ledger.js +134 -0
- package/dist/esm/daemon/domains/blueprint-exec.d.ts +106 -0
- package/dist/esm/daemon/domains/blueprint-exec.js +161 -0
- package/dist/esm/daemon/domains/launch.d.ts +25 -0
- package/dist/esm/daemon/domains/launch.js +30 -1
- package/dist/esm/daemon/handlers/blueprints-handlers.d.ts +12 -1
- package/dist/esm/daemon/handlers/blueprints-handlers.js +148 -6
- package/dist/esm/daemon/handlers/product.d.ts +10 -0
- package/dist/esm/daemon/handlers/product.js +10 -1
- package/dist/esm/daemon/methods.d.ts +21 -1
- package/dist/esm/daemon/methods.js +20 -0
- package/dist/esm/daemon/protocol/contract.d.ts +58 -0
- package/dist/esm/daemon/protocol/contract.js +38 -0
- package/dist/esm/daemon/server.js +7 -0
- package/dist/esm/docs-linter/blueprint-plan.js +2 -2
- package/dist/esm/errors/wp-error.js +21 -2
- package/dist/esm/hooks/doctor.d.ts +11 -0
- package/dist/esm/hooks/doctor.js +114 -45
- package/dist/esm/hooks/pretool-guard/dev-routing.js +1 -1
- package/dist/esm/hooks/pretool-guard/rtk-rewrite.d.ts +24 -0
- package/dist/esm/hooks/pretool-guard/rtk-rewrite.js +50 -0
- package/dist/esm/hooks/pretool-guard/runner.js +6 -0
- package/dist/esm/hooks/status/index.js +1 -1
- package/dist/esm/mcp/blueprint/_shared/lifecycle.js +39 -11
- package/dist/esm/mcp/blueprint/handlers/review-log.d.ts +14 -7
- package/dist/esm/mcp/blueprint/handlers/review-log.js +46 -22
- package/dist/esm/mcp/blueprint/registration.js +1 -1
- package/dist/esm/mcp/tools/_shared/redact.d.ts +22 -0
- package/dist/esm/mcp/tools/_shared/redact.js +114 -8
- package/dist/esm/mcp/tools/gain.js +28 -2
- package/dist/esm/mcp/tools/worktree.js +65 -3
- package/dist/esm/mcp/tools/wp-ui-compose.js +38 -4
- package/dist/esm/mcp/tools/wp-ui-preview.js +14 -7
- package/dist/esm/review/lifecycle-writers.d.ts +76 -0
- package/dist/esm/review/lifecycle-writers.js +330 -0
- package/dist/esm/review/subject.js +66 -32
- package/dist/esm/status/snapshot.d.ts +5 -0
- package/dist/esm/status/snapshot.js +27 -2
- package/dist/esm/test/shard-durations.json +0 -1
- package/dist/esm/test-helpers/global-setup.js +38 -3
- package/dist/esm/tool-runtime/managed-rtk.d.ts +44 -0
- package/dist/esm/tool-runtime/managed-rtk.js +275 -0
- package/dist/esm/tool-runtime/resolve-runner.js +18 -194
- package/dist/esm/utils/package-root.d.ts +32 -0
- package/dist/esm/utils/package-root.js +82 -0
- package/dist/esm/worktrees/git-metadata.d.ts +41 -0
- package/dist/esm/worktrees/git-metadata.js +48 -0
- package/package.json +16 -21
- package/dist/esm/config/oxlint/code-safety.d.ts +0 -18
- package/dist/esm/config/oxlint/foundation-purity.d.ts +0 -21
- package/dist/esm/config/oxlint/graphql-conventions.d.ts +0 -23
- package/dist/esm/config/oxlint/import-hygiene.d.ts +0 -34
- package/dist/esm/config/oxlint/index.d.ts +0 -22
- package/dist/esm/config/oxlint/index.js +0 -28
- package/dist/esm/config/oxlint/monorepo-paths.d.ts +0 -22
- package/dist/esm/config/oxlint/query-patterns.d.ts +0 -24
- package/dist/esm/config/oxlint/testing-quality.d.ts +0 -35
- package/dist/esm/config/oxlint/tier-boundaries.d.ts +0 -35
- package/dist/esm/output-transforms/rulesync.d.ts +0 -2
- 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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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
|
|
767
|
+
repoDirty,
|
|
724
768
|
targetDirty,
|
|
725
|
-
|
|
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.
|
|
779
|
-
|
|
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
|
-
" --
|
|
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("--
|
|
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,
|
|
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
|
-
|
|
97
|
-
if (
|
|
98
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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", "
|
|
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
|
-
|
|
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, "
|
|
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
|
-
/**
|
|
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
|
-
|
|
36
|
-
|
|
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
|
|
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: ${
|
|
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
|
}
|