@webpresso/agent-kit 3.3.2 → 3.3.3
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/catalog/AGENTS.md.tpl +3 -5
- package/catalog/agent/skills/verify/SKILL.md +12 -12
- package/catalog/compose/admin-blocks-registry.json +659 -0
- package/dist/esm/blueprint/core/parser.js +2 -4
- package/dist/esm/blueprint/core/validation/criteria.js +2 -1
- package/dist/esm/blueprint/lifecycle/review-provenance.js +85 -30
- package/dist/esm/build/cli-mcp-parity.js +3 -2
- package/dist/esm/build/package-manifest.js +7 -2
- package/dist/esm/cli/commands/audit-core.js +9 -1
- package/dist/esm/cli/commands/audit.js +16 -5
- package/dist/esm/cli/commands/init/mcp-spec.d.ts +18 -3
- package/dist/esm/cli/commands/init/mcp-spec.js +54 -6
- package/dist/esm/cli/commands/init/scaffold-agents-md.js +5 -7
- package/dist/esm/cli/commands/package-manager.js +8 -2
- package/dist/esm/compose/compose.d.ts +38 -0
- package/dist/esm/compose/compose.js +143 -0
- package/dist/esm/compose/escape-html.d.ts +5 -0
- package/dist/esm/compose/escape-html.js +12 -0
- package/dist/esm/compose/ops-report-render.d.ts +5 -0
- package/dist/esm/compose/ops-report-render.js +71 -0
- package/dist/esm/compose/ops-report-schema.d.ts +42 -0
- package/dist/esm/compose/ops-report-schema.js +46 -0
- package/dist/esm/compose/preview.d.ts +14 -0
- package/dist/esm/compose/preview.js +32 -0
- package/dist/esm/compose/registry-resolve.d.ts +47 -0
- package/dist/esm/compose/registry-resolve.js +133 -0
- package/dist/esm/git/changed-files.d.ts +20 -0
- package/dist/esm/git/changed-files.js +63 -0
- package/dist/esm/hooks/stop/qa-changed-files.js +6 -1
- package/dist/esm/mcp/blueprint/handlers/validate.js +6 -0
- package/dist/esm/mcp/tools/_names.d.ts +1 -1
- package/dist/esm/mcp/tools/_names.js +4 -0
- package/dist/esm/mcp/tools/_registry.js +8 -0
- package/dist/esm/mcp/tools/release-progress.d.ts +9 -0
- package/dist/esm/mcp/tools/release-progress.js +110 -0
- package/dist/esm/mcp/tools/wp-ui-blocks-list.d.ts +3 -0
- package/dist/esm/mcp/tools/wp-ui-blocks-list.js +43 -0
- package/dist/esm/mcp/tools/wp-ui-compose.d.ts +3 -0
- package/dist/esm/mcp/tools/wp-ui-compose.js +62 -0
- package/dist/esm/mcp/tools/wp-ui-preview.d.ts +3 -0
- package/dist/esm/mcp/tools/wp-ui-preview.js +52 -0
- package/dist/esm/package.json +2 -0
- package/package.json +23 -12
|
@@ -139,6 +139,83 @@ function workingContentMatchesRef(repoRoot, file, purpose, ref) {
|
|
|
139
139
|
return false;
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Candidate delivery subjects for the working path.
|
|
144
|
+
*
|
|
145
|
+
* Two sources, in this order:
|
|
146
|
+
*
|
|
147
|
+
* 1. **The live tree at HEAD.** This mirrors what {@link readReviewAuthorityAtRef}
|
|
148
|
+
* already does for delivery on the ref path: it builds the subject from the
|
|
149
|
+
* ref under audit and matches events by digest, never resolving the commit an
|
|
150
|
+
* event recorded. Doing the same here removes the working path's outlier
|
|
151
|
+
* behaviour.
|
|
152
|
+
* 2. **Reconstruction at each event's `reviewedCommit`**, for events the live
|
|
153
|
+
* subject does not already cover.
|
|
154
|
+
*
|
|
155
|
+
* Why the live tree has to come first: `reviewedCommit` is a *locator*, not the
|
|
156
|
+
* binding. The binding is `subjectDigest`, which content-addresses the reviewed
|
|
157
|
+
* tree. A squash merge destroys the branch commit while preserving its tree, so
|
|
158
|
+
* an approval recorded at the branch tip kept a digest that still describes the
|
|
159
|
+
* post-merge tree exactly — yet resolving its now-orphaned SHA threw, the event
|
|
160
|
+
* was marked compromised, and a genuinely valid approval was discarded. The
|
|
161
|
+
* standing workaround was to hand-append a rebind event after every squash.
|
|
162
|
+
*
|
|
163
|
+
* Matching on the digest first is not a loosening: an event is accepted only
|
|
164
|
+
* when the tree it recorded is byte-for-byte the tree we have. Content that
|
|
165
|
+
* differs still fails, and an event whose commit is gone *and* whose digest
|
|
166
|
+
* matches nothing reachable is still marked unresolvable — fail-closed.
|
|
167
|
+
*/
|
|
168
|
+
function collectWorkingDeliverySubjects(input) {
|
|
169
|
+
const { repoRoot, slug, blueprintPath, events, unresolvableEventIds, issues } = input;
|
|
170
|
+
const candidates = new Map();
|
|
171
|
+
// Nothing to resolve against: skip before paying for a whole-tree read. This
|
|
172
|
+
// path also runs for plan-only ledgers, where the old code cost nothing.
|
|
173
|
+
if (!events.some((event) => event.purpose === "delivery"))
|
|
174
|
+
return [];
|
|
175
|
+
let liveDigest = null;
|
|
176
|
+
try {
|
|
177
|
+
const live = createDeliverySubjectAtRef(repoRoot, "HEAD", slug, blueprintPath);
|
|
178
|
+
liveDigest = live.digest;
|
|
179
|
+
candidates.set(live.digest, { scheme: live.scheme, digest: live.digest });
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
// No HEAD (fresh repo) or no blueprint document at HEAD (first commit of a
|
|
183
|
+
// new blueprint). Not fatal: fall through to per-event reconstruction.
|
|
184
|
+
issues?.push(`Working delivery subject unavailable at HEAD for ${slug}: ${error instanceof Error ? error.message : String(error)}`);
|
|
185
|
+
}
|
|
186
|
+
for (const event of events) {
|
|
187
|
+
if (event.purpose !== "delivery")
|
|
188
|
+
continue;
|
|
189
|
+
// Already covered by the live tree — the reviewed content is exactly what
|
|
190
|
+
// we have, so the recorded commit does not need to exist any more.
|
|
191
|
+
if (liveDigest !== null && event.subjectDigest === liveDigest)
|
|
192
|
+
continue;
|
|
193
|
+
// Scope subject-construction failure to THIS event. A rebase rewrites
|
|
194
|
+
// commit SHAs, so a previously-recorded `reviewedCommit` can become
|
|
195
|
+
// unresolvable; letting that throw would escape to the outer catch and
|
|
196
|
+
// discard every approval, including valid ones for commits that still
|
|
197
|
+
// resolve. Mirror the ref-path's markCompromisedArtifactEvents: record the
|
|
198
|
+
// issue, mark the event compromised, and drop only it.
|
|
199
|
+
let subject;
|
|
200
|
+
try {
|
|
201
|
+
subject = createDeliverySubjectAtRef(repoRoot, event.reviewedCommit, slug, blueprintPath);
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
issues?.push(`Working delivery review subject unavailable for ${event.id} at ${event.reviewedCommit}: ${error instanceof Error ? error.message : String(error)}`);
|
|
205
|
+
// Compromised (not merely skipped) so the event can never grant approval
|
|
206
|
+
// while still participating in reviewer supersession — see
|
|
207
|
+
// evaluateReviewEvents.
|
|
208
|
+
unresolvableEventIds.add(event.id);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (subject.digest !== event.subjectDigest) {
|
|
212
|
+
issues?.push(`Working delivery review subject mismatch for ${event.id}: expected ${subject.digest}, found ${event.subjectDigest}.`);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
candidates.set(subject.digest, { scheme: subject.scheme, digest: subject.digest });
|
|
216
|
+
}
|
|
217
|
+
return [...candidates.values()];
|
|
218
|
+
}
|
|
142
219
|
// Working-ledger acceptance: pre-commit validates a blueprint whose new review
|
|
143
220
|
// events are staged alongside it and therefore not yet at any git ref. An
|
|
144
221
|
// event still counts only when its subjectDigest matches the digest of the
|
|
@@ -167,36 +244,14 @@ function collectWorkingV2ReviewProvenance(file, purpose, issues) {
|
|
|
167
244
|
createLegacyPlanSubjectDigestForContent(slug, readFileSync(file)),
|
|
168
245
|
].map((digest) => [digest, { scheme: "wp-blueprint-v1", digest }])).values(),
|
|
169
246
|
]
|
|
170
|
-
:
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
// including valid ones for commits that still resolve. Mirror
|
|
179
|
-
// the ref-path's markCompromisedArtifactEvents: record the
|
|
180
|
-
// issue, mark the event compromised, and drop only it.
|
|
181
|
-
let subject;
|
|
182
|
-
try {
|
|
183
|
-
subject = createDeliverySubjectAtRef(repoRoot, event.reviewedCommit, slug, normalizePath(path.relative(repoRoot, file)));
|
|
184
|
-
}
|
|
185
|
-
catch (error) {
|
|
186
|
-
issues?.push(`Working delivery review subject unavailable for ${event.id} at ${event.reviewedCommit}: ${error instanceof Error ? error.message : String(error)}`);
|
|
187
|
-
// Compromised (not merely skipped) so the event can never
|
|
188
|
-
// grant approval while still participating in reviewer
|
|
189
|
-
// supersession — see evaluateReviewEvents.
|
|
190
|
-
unresolvableEventIds.add(event.id);
|
|
191
|
-
return [];
|
|
192
|
-
}
|
|
193
|
-
if (subject.digest !== event.subjectDigest) {
|
|
194
|
-
issues?.push(`Working delivery review subject mismatch for ${event.id}: expected ${subject.digest}, found ${event.subjectDigest}.`);
|
|
195
|
-
return [];
|
|
196
|
-
}
|
|
197
|
-
return [[subject.digest, subject]];
|
|
198
|
-
})).values(),
|
|
199
|
-
];
|
|
247
|
+
: collectWorkingDeliverySubjects({
|
|
248
|
+
repoRoot,
|
|
249
|
+
slug,
|
|
250
|
+
blueprintPath: normalizePath(path.relative(repoRoot, file)),
|
|
251
|
+
events: working.events,
|
|
252
|
+
unresolvableEventIds,
|
|
253
|
+
issues,
|
|
254
|
+
});
|
|
200
255
|
return subjects
|
|
201
256
|
.flatMap((subject) => evaluateReviewEvents(working.events, {
|
|
202
257
|
purpose,
|
|
@@ -52,8 +52,9 @@ export const CLI_MCP_PARITY_CLASSIFICATION = {
|
|
|
52
52
|
},
|
|
53
53
|
"release-progress": {
|
|
54
54
|
command: "release-progress",
|
|
55
|
-
classification: "
|
|
56
|
-
|
|
55
|
+
classification: "existing-mcp",
|
|
56
|
+
tools: ["wp_release_progress"],
|
|
57
|
+
reason: "Release workflow phase reporter (detect/native-matrix/publish) is available as wp_release_progress MCP and CLI",
|
|
57
58
|
},
|
|
58
59
|
cleanup: {
|
|
59
60
|
command: "cleanup",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { parse as parseYaml } from "yaml";
|
|
4
|
-
import { assertBuiltBlueprintMigrationSqlAssets } from "./blueprint-migration-assets.js";
|
|
5
|
-
import { assertBuiltSastRuleAssets } from "./sast-rule-assets.js";
|
|
4
|
+
import { assertBuiltBlueprintMigrationSqlAssets, syncBlueprintMigrationSqlAssets, } from "./blueprint-migration-assets.js";
|
|
5
|
+
import { assertBuiltSastRuleAssets, syncSastRuleAssets } from "./sast-rule-assets.js";
|
|
6
6
|
import { SESSION_MEMORY_NATIVE_TARGETS } from "#session-memory/native-targets.js";
|
|
7
7
|
import { RUNTIME_TARGETS } from "./runtime-targets.js";
|
|
8
8
|
const DEPENDENCY_SECTIONS = [
|
|
@@ -304,6 +304,11 @@ export function preparePackedManifest(rootDir, options = {}) {
|
|
|
304
304
|
// out-of-org consumers cannot resolve.
|
|
305
305
|
const packedManifest = createPackedManifest(manifest, readWorkspaceCatalogs(workspacePath), options);
|
|
306
306
|
if (options.assertBlueprintMigrationAssets ?? true) {
|
|
307
|
+
// Sync authoring sources into dist before assert so pack/prepack paths
|
|
308
|
+
// (version dry-run, npm pack) do not require a full `pnpm run build` first.
|
|
309
|
+
// Assert still fails closed on missing source or content drift.
|
|
310
|
+
syncBlueprintMigrationSqlAssets(rootDir);
|
|
311
|
+
syncSastRuleAssets(rootDir);
|
|
307
312
|
assertBuiltBlueprintMigrationSqlAssets(rootDir);
|
|
308
313
|
assertBuiltSastRuleAssets(rootDir);
|
|
309
314
|
}
|
|
@@ -79,7 +79,15 @@ export async function runAuditDispatch(auditKind, targets, options, deps) {
|
|
|
79
79
|
case "blueprint-pr-coverage": {
|
|
80
80
|
const { auditBlueprintPrCoverage } = await import("#audit/blueprint-pr-coverage");
|
|
81
81
|
const root = options.root ?? target ?? deps.root;
|
|
82
|
-
const
|
|
82
|
+
const explicitBaseRef = options.baseRef ?? options.base;
|
|
83
|
+
// Same pinned-base hazard as the branch-scoped audits: CI's
|
|
84
|
+
// `github.event.pull_request.base.sha` stops being the fork point once the
|
|
85
|
+
// branch is rebased, and this audit diffs `<baseRef>...HEAD`. Narrow it to
|
|
86
|
+
// the real fork point so coverage is judged on this PR's own files.
|
|
87
|
+
const { resolveBranchDiffBase } = await import("#git/changed-files");
|
|
88
|
+
const baseRef = explicitBaseRef
|
|
89
|
+
? resolveBranchDiffBase(root, explicitBaseRef)
|
|
90
|
+
: explicitBaseRef;
|
|
83
91
|
const result = auditBlueprintPrCoverage(root, { baseRef, headRef: options.headRef });
|
|
84
92
|
return { kind: "repo-result", name: "blueprint-pr-coverage", result };
|
|
85
93
|
}
|
|
@@ -9,7 +9,7 @@ import { STRYKER_COMMAND } from "#audit/run-stryker.js";
|
|
|
9
9
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { addAffectedOptions, resolveAffectedTargets } from "#git/affected";
|
|
12
|
-
import { defaultBranchBaseRef, getBranchRemovedFiles, getStagedRemovedFiles, } from "#git/changed-files";
|
|
12
|
+
import { defaultBranchBaseRef, getBranchRemovedFiles, getStagedRemovedFiles, resolveBranchDiffBase, } from "#git/changed-files";
|
|
13
13
|
import { runAuditDispatch } from "#cli/commands/audit-core.js";
|
|
14
14
|
import { createCliLogSink } from "#cli/commands/quality-log-store.js";
|
|
15
15
|
import { emitCliCommandOutput, runLoggedChildCommand } from "#cli/commands/quality-runner.js";
|
|
@@ -284,6 +284,17 @@ export function registerAuditCommand(cli) {
|
|
|
284
284
|
let dispatchOptions = options;
|
|
285
285
|
let auditScope = "full";
|
|
286
286
|
let changeRemovesFile = false;
|
|
287
|
+
// A caller-supplied `--base` is a PINNED sha (CI passes
|
|
288
|
+
// `github.event.pull_request.base.sha`, which does not advance with the
|
|
289
|
+
// base branch). After a rebase it stops being the fork point, so scoping
|
|
290
|
+
// off it silently absorbs everything merged into the base branch since
|
|
291
|
+
// the PR opened. Narrow it to the real fork point once, and use that one
|
|
292
|
+
// value everywhere the branch scope is derived so the affected file set,
|
|
293
|
+
// the removal probe, and the audit's own base-vs-head range agree.
|
|
294
|
+
const explicitBaseRef = options.baseRef ?? options.base;
|
|
295
|
+
const branchBaseRef = branch && explicitBaseRef
|
|
296
|
+
? resolveBranchDiffBase(auditRoot, explicitBaseRef)
|
|
297
|
+
: explicitBaseRef;
|
|
287
298
|
if (affected || branch) {
|
|
288
299
|
if (kind &&
|
|
289
300
|
AUDIT_KINDS.includes(kind) &&
|
|
@@ -309,7 +320,7 @@ export function registerAuditCommand(cli) {
|
|
|
309
320
|
cwd: auditRoot,
|
|
310
321
|
affected,
|
|
311
322
|
branch,
|
|
312
|
-
baseRef:
|
|
323
|
+
baseRef: branchBaseRef,
|
|
313
324
|
explicitTargets: [],
|
|
314
325
|
policy: "fail-closed",
|
|
315
326
|
mapChangedFiles: (files) => files,
|
|
@@ -360,7 +371,7 @@ export function registerAuditCommand(cli) {
|
|
|
360
371
|
changeRemovesFile = affected
|
|
361
372
|
? affectedChangeRemovesFile(auditRoot, {
|
|
362
373
|
branch,
|
|
363
|
-
baseRef:
|
|
374
|
+
baseRef: branchBaseRef,
|
|
364
375
|
})
|
|
365
376
|
: false;
|
|
366
377
|
if (resolution.kind === "empty" && !changeRemovesFile) {
|
|
@@ -392,7 +403,7 @@ export function registerAuditCommand(cli) {
|
|
|
392
403
|
changedOnly: true,
|
|
393
404
|
affectedFiles: [],
|
|
394
405
|
root: options.root ?? resolution.cwd,
|
|
395
|
-
baseRef:
|
|
406
|
+
baseRef: branchBaseRef,
|
|
396
407
|
};
|
|
397
408
|
}
|
|
398
409
|
if (resolution.kind === "scoped") {
|
|
@@ -404,7 +415,7 @@ export function registerAuditCommand(cli) {
|
|
|
404
415
|
changedOnly: true,
|
|
405
416
|
affectedFiles: [...resolution.targets],
|
|
406
417
|
root: options.root ?? resolution.cwd,
|
|
407
|
-
baseRef:
|
|
418
|
+
baseRef: branchBaseRef,
|
|
408
419
|
};
|
|
409
420
|
}
|
|
410
421
|
}
|
|
@@ -36,13 +36,28 @@ export interface WebpressoInstallProbe {
|
|
|
36
36
|
* machine. Probes the locations consumers use to install webpresso, in order
|
|
37
37
|
* of stability:
|
|
38
38
|
*
|
|
39
|
-
* 1. the stable global `wp` shim available on PATH
|
|
40
|
-
*
|
|
39
|
+
* 1. the stable global `wp` shim available on PATH (including the
|
|
40
|
+
* vite-plus multi-package shim at `~/.vite-plus/bin/wp`)
|
|
41
|
+
* 2. the currently executing `@webpresso/agent-kit` package **only when it
|
|
42
|
+
* is not a vite-plus `agent-kit#<installId>` payload path**
|
|
41
43
|
* 3. Claude plugin install — `~/.claude/plugins/cache/.../agent-kit/`
|
|
42
44
|
* 4. bun global — `~/.bun/install/global/node_modules/@webpresso/agent-kit/`
|
|
43
|
-
*
|
|
45
|
+
*
|
|
46
|
+
* Host MCP configs (Grok/Codex/etc.) must never be pinned to a versioned
|
|
47
|
+
* installId tree: the next `wp update` / `vp update -g` deletes that path and
|
|
48
|
+
* leaves every host with a dead launcher. Returns `null` when none of the
|
|
49
|
+
* candidates yield a durable `wp`.
|
|
44
50
|
*/
|
|
45
51
|
export declare function findWebpressoMcpEntry(probe?: WebpressoInstallProbe): string | null;
|
|
52
|
+
/**
|
|
53
|
+
* True when `entryPath` is the stable vite-plus multi-package `wp` shim that
|
|
54
|
+
* dispatches to `@webpresso/agent-kit` via `~/.vite-plus/bins/wp.json`.
|
|
55
|
+
*
|
|
56
|
+
* Unlike {@link isWebpressoWpEntry}, this does **not** require realpath to land
|
|
57
|
+
* inside a package `bin/wp` tree — vite-plus's `wp` is a symlink to the `vp`
|
|
58
|
+
* binary, so package-root ownership is proven via the bins metadata instead.
|
|
59
|
+
*/
|
|
60
|
+
export declare function isVitePlusManagedWebpressoWp(entryPath: string): boolean;
|
|
46
61
|
export declare function resolveWebpressoMcpEntry(probe?: WebpressoInstallProbe): {
|
|
47
62
|
entryPath: string | null;
|
|
48
63
|
checked: readonly string[];
|
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { accessSync, constants, existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
|
-
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
15
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
16
16
|
import { execFileSync } from "node:child_process";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { isVersionedAgentKitPayloadPath } from "#cli/auto-update/self-invocation.js";
|
|
18
19
|
import { pathCandidates } from "#runtime/command-exists.js";
|
|
19
20
|
export const WEBPRESSO_MCP_SERVER_NAME = "webpresso";
|
|
20
21
|
/**
|
|
@@ -67,11 +68,17 @@ const WP_LAUNCHER_REQUIRED_RELATIVES = [
|
|
|
67
68
|
* machine. Probes the locations consumers use to install webpresso, in order
|
|
68
69
|
* of stability:
|
|
69
70
|
*
|
|
70
|
-
* 1. the stable global `wp` shim available on PATH
|
|
71
|
-
*
|
|
71
|
+
* 1. the stable global `wp` shim available on PATH (including the
|
|
72
|
+
* vite-plus multi-package shim at `~/.vite-plus/bin/wp`)
|
|
73
|
+
* 2. the currently executing `@webpresso/agent-kit` package **only when it
|
|
74
|
+
* is not a vite-plus `agent-kit#<installId>` payload path**
|
|
72
75
|
* 3. Claude plugin install — `~/.claude/plugins/cache/.../agent-kit/`
|
|
73
76
|
* 4. bun global — `~/.bun/install/global/node_modules/@webpresso/agent-kit/`
|
|
74
|
-
*
|
|
77
|
+
*
|
|
78
|
+
* Host MCP configs (Grok/Codex/etc.) must never be pinned to a versioned
|
|
79
|
+
* installId tree: the next `wp update` / `vp update -g` deletes that path and
|
|
80
|
+
* leaves every host with a dead launcher. Returns `null` when none of the
|
|
81
|
+
* candidates yield a durable `wp`.
|
|
75
82
|
*/
|
|
76
83
|
export function findWebpressoMcpEntry(probe = {}) {
|
|
77
84
|
return resolveWebpressoMcpEntry(probe).entryPath;
|
|
@@ -79,6 +86,37 @@ export function findWebpressoMcpEntry(probe = {}) {
|
|
|
79
86
|
function isUsableWebpressoMcpRoot(root) {
|
|
80
87
|
return WP_LAUNCHER_REQUIRED_RELATIVES.every((relativePath) => existsSync(join(root, relativePath)));
|
|
81
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* True when `entryPath` is the stable vite-plus multi-package `wp` shim that
|
|
91
|
+
* dispatches to `@webpresso/agent-kit` via `~/.vite-plus/bins/wp.json`.
|
|
92
|
+
*
|
|
93
|
+
* Unlike {@link isWebpressoWpEntry}, this does **not** require realpath to land
|
|
94
|
+
* inside a package `bin/wp` tree — vite-plus's `wp` is a symlink to the `vp`
|
|
95
|
+
* binary, so package-root ownership is proven via the bins metadata instead.
|
|
96
|
+
*/
|
|
97
|
+
export function isVitePlusManagedWebpressoWp(entryPath) {
|
|
98
|
+
try {
|
|
99
|
+
if (isVersionedAgentKitPayloadPath(entryPath))
|
|
100
|
+
return false;
|
|
101
|
+
const binDir = dirname(entryPath);
|
|
102
|
+
if (basename(binDir) !== "bin")
|
|
103
|
+
return false;
|
|
104
|
+
if (basename(dirname(binDir)) !== ".vite-plus")
|
|
105
|
+
return false;
|
|
106
|
+
const base = basename(entryPath).toLowerCase();
|
|
107
|
+
if (base !== "wp" && base !== "wp.exe")
|
|
108
|
+
return false;
|
|
109
|
+
const metaPath = join(dirname(binDir), "bins", "wp.json");
|
|
110
|
+
const meta = JSON.parse(readFileSync(metaPath, "utf8"));
|
|
111
|
+
return meta.package === "@webpresso/agent-kit";
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function isDurableMcpEntryPath(entryPath) {
|
|
118
|
+
return !isVersionedAgentKitPayloadPath(entryPath);
|
|
119
|
+
}
|
|
82
120
|
export function resolveWebpressoMcpEntry(probe = {}) {
|
|
83
121
|
const checked = [];
|
|
84
122
|
if (probe.candidates === undefined) {
|
|
@@ -88,10 +126,17 @@ export function resolveWebpressoMcpEntry(probe = {}) {
|
|
|
88
126
|
if (stableEntry) {
|
|
89
127
|
checked.push(stableEntry);
|
|
90
128
|
const packageWp = packageWpFromAdjacentNpmCmdShim(stableEntry);
|
|
91
|
-
if (packageWp !== null)
|
|
129
|
+
if (packageWp !== null && isDurableMcpEntryPath(packageWp)) {
|
|
92
130
|
return { entryPath: packageWp, checked };
|
|
131
|
+
}
|
|
93
132
|
const verifierAllows = probe.stableEntryIsWebpresso?.(stableEntry) ?? true;
|
|
94
|
-
|
|
133
|
+
// Prefer the vite-plus multi-package shim before package-root ownership:
|
|
134
|
+
// it survives installId swaps and is what PATH `wp` resolves to in the
|
|
135
|
+
// default consumer install.
|
|
136
|
+
if (verifierAllows && isVitePlusManagedWebpressoWp(stableEntry)) {
|
|
137
|
+
return { entryPath: stableEntry, checked };
|
|
138
|
+
}
|
|
139
|
+
if (verifierAllows && isWebpressoWpEntry(stableEntry) && isDurableMcpEntryPath(stableEntry)) {
|
|
95
140
|
return { entryPath: stableEntry, checked };
|
|
96
141
|
}
|
|
97
142
|
}
|
|
@@ -103,6 +148,9 @@ export function resolveWebpressoMcpEntry(probe = {}) {
|
|
|
103
148
|
continue;
|
|
104
149
|
checked.push(root);
|
|
105
150
|
const wpBin = join(root, WP_BIN_RELATIVE);
|
|
151
|
+
// Never materialize host MCP configs against a vite-plus installId tree.
|
|
152
|
+
if (!isDurableMcpEntryPath(wpBin))
|
|
153
|
+
continue;
|
|
106
154
|
if (isTrustedWebpressoPackageRoot(root))
|
|
107
155
|
return { entryPath: wpBin, checked };
|
|
108
156
|
}
|
|
@@ -30,13 +30,11 @@ const TECH_STACK_RULES = [
|
|
|
30
30
|
{ dep: /^typescript$/, label: "TypeScript" },
|
|
31
31
|
];
|
|
32
32
|
// Kept under the 8 KB (8192-byte) hard ceiling. The rendered template embeds
|
|
33
|
-
// the full `native_tool_names` registry list
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
// breach it should trigger a conscious restructure (or exclude the
|
|
39
|
-
// auto-generated tool list from the prose budget), not another reflexive bump.
|
|
33
|
+
// the full `native_tool_names` registry list (~15-17 bytes per tool). Soft
|
|
34
|
+
// target 8_180 leaves headroom under 8_192. The ui-compose batch
|
|
35
|
+
// (wp_ui_blocks_list, wp_ui_compose, wp_ui_preview) required trimming catalog
|
|
36
|
+
// AGENTS.md.tpl prose rather than raising the ceiling. Next breach of 8_192
|
|
37
|
+
// should restructure (or stop embedding the full tool list), not bump again.
|
|
40
38
|
export const AGENTS_MD_MAX_BYTES = 8_180;
|
|
41
39
|
export function renderRepositoryMap(consumer) {
|
|
42
40
|
const packages = consumer.workspacePackages;
|
|
@@ -184,6 +184,9 @@ function runGlobalUpdateCommand(deps) {
|
|
|
184
184
|
else {
|
|
185
185
|
failed = true;
|
|
186
186
|
console.error(message);
|
|
187
|
+
// Required step failed — stop so optional host refreshes cannot
|
|
188
|
+
// look like a successful update after a broken agent-kit install.
|
|
189
|
+
break;
|
|
187
190
|
}
|
|
188
191
|
}
|
|
189
192
|
}
|
|
@@ -195,6 +198,7 @@ function runGlobalUpdateCommand(deps) {
|
|
|
195
198
|
else {
|
|
196
199
|
failed = true;
|
|
197
200
|
console.error(message);
|
|
201
|
+
break;
|
|
198
202
|
}
|
|
199
203
|
}
|
|
200
204
|
}
|
|
@@ -313,8 +317,10 @@ function runAgentKitSelfUpdateStep(deps) {
|
|
|
313
317
|
return spawnLike(status, new Error("another agent-kit installer is active; re-run `wp update` once it finishes"));
|
|
314
318
|
case GUARD_EXIT_OWNERSHIP_LOST:
|
|
315
319
|
return spawnLike(status, new Error("install lease ownership was lost mid-update; the takeover installer verifies the store — re-run `wp update` to confirm"));
|
|
316
|
-
default:
|
|
317
|
-
|
|
320
|
+
default: {
|
|
321
|
+
const code = status === 0 ? 1 : status;
|
|
322
|
+
return spawnLike(code, new Error(`agent-kit self-install failed (self-install-guard exit ${code}); re-run \`wp update\` or \`vp install -g @webpresso/agent-kit\``));
|
|
323
|
+
}
|
|
318
324
|
}
|
|
319
325
|
}
|
|
320
326
|
function refreshCodexPlugin(deps) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type ResolvedBlockRegistry } from "./registry-resolve.js";
|
|
3
|
+
export declare const composeInputSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
4
|
+
profile: z.ZodLiteral<"admin-crud">;
|
|
5
|
+
kind: z.ZodEnum<{
|
|
6
|
+
detail: "detail";
|
|
7
|
+
form: "form";
|
|
8
|
+
list: "list";
|
|
9
|
+
}>;
|
|
10
|
+
spec: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
11
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
12
|
+
profile: z.ZodLiteral<"ops-report">;
|
|
13
|
+
spec: z.ZodUnknown;
|
|
14
|
+
}, z.core.$strict>], "profile">;
|
|
15
|
+
export type ComposeInput = z.infer<typeof composeInputSchema>;
|
|
16
|
+
export interface ComposeDiagnostic {
|
|
17
|
+
readonly path: string;
|
|
18
|
+
readonly message: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ComposeResult {
|
|
21
|
+
readonly ok: boolean;
|
|
22
|
+
readonly profile: "admin-crud" | "ops-report" | "unknown";
|
|
23
|
+
/** Legal admin-crud returns the input spec unchanged (validate-only). */
|
|
24
|
+
readonly spec: unknown;
|
|
25
|
+
readonly diagnostics: readonly ComposeDiagnostic[];
|
|
26
|
+
readonly registrySource?: "project" | "snapshot";
|
|
27
|
+
readonly registryVersion?: string;
|
|
28
|
+
readonly warnings: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Profile-discriminated compose validation (tool-level).
|
|
32
|
+
* Registry resolve may walk the filesystem from `cwd` / `process.cwd()` when
|
|
33
|
+
* `resolvedRegistry` is not injected.
|
|
34
|
+
*/
|
|
35
|
+
export declare function composeValidate(raw: unknown, options?: {
|
|
36
|
+
readonly cwd?: string;
|
|
37
|
+
readonly resolvedRegistry?: ResolvedBlockRegistry;
|
|
38
|
+
}): ComposeResult;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { opsReportSpecSchema } from "./ops-report-schema.js";
|
|
3
|
+
import { blockNamesForView, resolveBlockRegistry, } from "./registry-resolve.js";
|
|
4
|
+
const adminKindSchema = z.enum(["list", "detail", "form"]);
|
|
5
|
+
const adminCrudInputSchema = z
|
|
6
|
+
.object({
|
|
7
|
+
profile: z.literal("admin-crud"),
|
|
8
|
+
kind: adminKindSchema,
|
|
9
|
+
/** Opaque page/spec payload; blocks[] validated against registry when present. */
|
|
10
|
+
spec: z.record(z.string(), z.unknown()),
|
|
11
|
+
})
|
|
12
|
+
.strict();
|
|
13
|
+
const opsReportInputSchema = z
|
|
14
|
+
.object({
|
|
15
|
+
profile: z.literal("ops-report"),
|
|
16
|
+
spec: z.unknown(),
|
|
17
|
+
})
|
|
18
|
+
.strict();
|
|
19
|
+
export const composeInputSchema = z.discriminatedUnion("profile", [
|
|
20
|
+
adminCrudInputSchema,
|
|
21
|
+
opsReportInputSchema,
|
|
22
|
+
]);
|
|
23
|
+
function zodToDiagnostics(error) {
|
|
24
|
+
return error.issues.map((issue) => ({
|
|
25
|
+
path: issue.path.length > 0 ? issue.path.join(".") : "(root)",
|
|
26
|
+
message: issue.message,
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
function validateAdminBlocks(kind, spec, resolved) {
|
|
30
|
+
const diagnostics = [];
|
|
31
|
+
const warnings = [];
|
|
32
|
+
const allowed = blockNamesForView(resolved.registry, kind);
|
|
33
|
+
const blocks = spec["blocks"];
|
|
34
|
+
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 };
|
|
37
|
+
}
|
|
38
|
+
if (!Array.isArray(blocks)) {
|
|
39
|
+
diagnostics.push({ path: "spec.blocks", message: "blocks must be an array" });
|
|
40
|
+
return { diagnostics, warnings };
|
|
41
|
+
}
|
|
42
|
+
for (const [index, block] of blocks.entries()) {
|
|
43
|
+
if (block === null || typeof block !== "object" || Array.isArray(block)) {
|
|
44
|
+
diagnostics.push({ path: `spec.blocks.${index}`, message: "block must be an object" });
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const type = block["type"];
|
|
48
|
+
if (typeof type !== "string" || type.length === 0) {
|
|
49
|
+
diagnostics.push({
|
|
50
|
+
path: `spec.blocks.${index}.type`,
|
|
51
|
+
message: "block requires string type",
|
|
52
|
+
});
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (!allowed.has(type)) {
|
|
56
|
+
diagnostics.push({
|
|
57
|
+
path: `spec.blocks.${index}.type`,
|
|
58
|
+
message: `illegal block type "${type}" for ${kind}; allowed: ${[...allowed].sort().join(", ")}`,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { diagnostics, warnings };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Profile-discriminated compose validation (tool-level).
|
|
66
|
+
* Registry resolve may walk the filesystem from `cwd` / `process.cwd()` when
|
|
67
|
+
* `resolvedRegistry` is not injected.
|
|
68
|
+
*/
|
|
69
|
+
export function composeValidate(raw, options = {}) {
|
|
70
|
+
const parsed = composeInputSchema.safeParse(raw);
|
|
71
|
+
if (!parsed.success) {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
profile: "unknown",
|
|
75
|
+
spec: null,
|
|
76
|
+
diagnostics: zodToDiagnostics(parsed.error),
|
|
77
|
+
warnings: [],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const input = parsed.data;
|
|
81
|
+
/** Prefer original caller object identity for admin-crud passthrough. */
|
|
82
|
+
const originalSpec = raw !== null && typeof raw === "object" && !Array.isArray(raw) && "spec" in raw
|
|
83
|
+
? raw.spec
|
|
84
|
+
: input.spec;
|
|
85
|
+
if (input.profile === "ops-report") {
|
|
86
|
+
const ops = opsReportSpecSchema.safeParse(input.spec);
|
|
87
|
+
if (!ops.success) {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
profile: "ops-report",
|
|
91
|
+
spec: originalSpec,
|
|
92
|
+
diagnostics: zodToDiagnostics(ops.error),
|
|
93
|
+
warnings: [],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
ok: true,
|
|
98
|
+
profile: "ops-report",
|
|
99
|
+
spec: ops.data,
|
|
100
|
+
diagnostics: [],
|
|
101
|
+
warnings: [],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// admin-crud: validate-only — return caller's spec object unchanged when legal
|
|
105
|
+
let resolved = options.resolvedRegistry;
|
|
106
|
+
if (!resolved) {
|
|
107
|
+
try {
|
|
108
|
+
resolved = resolveBlockRegistry({ cwd: options.cwd });
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
profile: "admin-crud",
|
|
115
|
+
spec: originalSpec,
|
|
116
|
+
diagnostics: [{ path: "registry", message }],
|
|
117
|
+
warnings: [],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const { diagnostics: blockDiagnostics, warnings: blockWarnings } = validateAdminBlocks(input.kind, input.spec, resolved);
|
|
122
|
+
const warnings = [...resolved.warnings, ...blockWarnings];
|
|
123
|
+
if (blockDiagnostics.length > 0) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
profile: "admin-crud",
|
|
127
|
+
spec: originalSpec,
|
|
128
|
+
diagnostics: blockDiagnostics,
|
|
129
|
+
registrySource: resolved.source,
|
|
130
|
+
registryVersion: resolved.registryVersion,
|
|
131
|
+
warnings,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
ok: true,
|
|
136
|
+
profile: "admin-crud",
|
|
137
|
+
spec: originalSpec,
|
|
138
|
+
diagnostics: [],
|
|
139
|
+
registrySource: resolved.source,
|
|
140
|
+
registryVersion: resolved.registryVersion,
|
|
141
|
+
warnings,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single HTML encode chokepoint for compose/ops-report output.
|
|
3
|
+
* All untrusted strings must pass through here before interpolation.
|
|
4
|
+
*/
|
|
5
|
+
export function escapeHtml(value) {
|
|
6
|
+
return value
|
|
7
|
+
.replaceAll("&", "&")
|
|
8
|
+
.replaceAll("<", "<")
|
|
9
|
+
.replaceAll(">", ">")
|
|
10
|
+
.replaceAll('"', """)
|
|
11
|
+
.replaceAll("'", "'");
|
|
12
|
+
}
|