@webpresso/agent-kit 3.3.5 → 3.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @webpresso/agent-kit might be problematic. Click here for more details.
- package/dist/esm/blueprint/verification.d.ts +12 -1
- package/dist/esm/blueprint/verification.js +26 -3
- package/dist/esm/cli/commands/init/scaffold-base-kit.js +5 -1
- package/dist/esm/cli/commands/mcp.js +2 -0
- package/dist/esm/cli/commands/sync.js +8 -0
- package/dist/esm/hooks/doctor.js +114 -10
- package/dist/esm/mcp/blueprint/_shared/lifecycle.d.ts +13 -0
- package/dist/esm/mcp/blueprint/_shared/lifecycle.js +40 -8
- package/dist/esm/mcp/blueprint/handlers/document-mutations.d.ts +97 -0
- package/dist/esm/mcp/blueprint/handlers/document-mutations.js +23 -3
- package/dist/esm/mcp/blueprint/handlers/finalize.js +18 -7
- package/dist/esm/mcp/blueprint/registration.js +31 -2
- package/dist/esm/symlinker/consumers.d.ts +17 -0
- package/dist/esm/symlinker/consumers.js +9 -1
- package/dist/esm/symlinker/ownership.d.ts +48 -0
- package/dist/esm/symlinker/ownership.js +83 -0
- package/dist/esm/symlinker/unified-sync.d.ts +32 -0
- package/dist/esm/symlinker/unified-sync.js +90 -69
- package/package.json +12 -12
|
@@ -110,6 +110,17 @@ export declare function readTaskVerification(markdown: string, taskId: string):
|
|
|
110
110
|
*/
|
|
111
111
|
export declare function assertTaskHasCanonicalPassingEvidence(markdown: string, taskId: string): readonly Evidence[];
|
|
112
112
|
/**
|
|
113
|
-
*
|
|
113
|
+
* Collect a failure message for every supplied task id that is missing
|
|
114
|
+
* task-local canonical passing evidence, instead of stopping at the first
|
|
115
|
+
* one. Callers that need to report every blocking condition in a single
|
|
116
|
+
* response (e.g. `wp_blueprint_finalize`) use this directly; callers that
|
|
117
|
+
* only need a single throw use {@link assertAllTasksHaveCanonicalPassingEvidence}.
|
|
118
|
+
*/
|
|
119
|
+
export declare function collectMissingCanonicalPassingEvidence(markdown: string, taskIds: readonly string[]): string[];
|
|
120
|
+
/**
|
|
121
|
+
* Assert that each supplied task id has task-local canonical passing
|
|
122
|
+
* evidence. Throws once with every failing task's message joined, rather
|
|
123
|
+
* than stopping at the first failure — a caller fixing one issue at a time
|
|
124
|
+
* would otherwise need one failed round-trip per missing-evidence task.
|
|
114
125
|
*/
|
|
115
126
|
export declare function assertAllTasksHaveCanonicalPassingEvidence(markdown: string, taskIds: readonly string[]): void;
|
|
@@ -173,11 +173,34 @@ export function assertTaskHasCanonicalPassingEvidence(markdown, taskId) {
|
|
|
173
173
|
return evidence;
|
|
174
174
|
}
|
|
175
175
|
/**
|
|
176
|
-
*
|
|
176
|
+
* Collect a failure message for every supplied task id that is missing
|
|
177
|
+
* task-local canonical passing evidence, instead of stopping at the first
|
|
178
|
+
* one. Callers that need to report every blocking condition in a single
|
|
179
|
+
* response (e.g. `wp_blueprint_finalize`) use this directly; callers that
|
|
180
|
+
* only need a single throw use {@link assertAllTasksHaveCanonicalPassingEvidence}.
|
|
177
181
|
*/
|
|
178
|
-
export function
|
|
182
|
+
export function collectMissingCanonicalPassingEvidence(markdown, taskIds) {
|
|
183
|
+
const failures = [];
|
|
179
184
|
for (const taskId of taskIds) {
|
|
180
|
-
|
|
185
|
+
try {
|
|
186
|
+
assertTaskHasCanonicalPassingEvidence(markdown, taskId);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
failures.push(error instanceof Error ? error.message : String(error));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return failures;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Assert that each supplied task id has task-local canonical passing
|
|
196
|
+
* evidence. Throws once with every failing task's message joined, rather
|
|
197
|
+
* than stopping at the first failure — a caller fixing one issue at a time
|
|
198
|
+
* would otherwise need one failed round-trip per missing-evidence task.
|
|
199
|
+
*/
|
|
200
|
+
export function assertAllTasksHaveCanonicalPassingEvidence(markdown, taskIds) {
|
|
201
|
+
const failures = collectMissingCanonicalPassingEvidence(markdown, taskIds);
|
|
202
|
+
if (failures.length > 0) {
|
|
203
|
+
throw new Error(failures.join("; "));
|
|
181
204
|
}
|
|
182
205
|
}
|
|
183
206
|
// ---------------------------------------------------------------------------
|
|
@@ -209,7 +209,11 @@ export function ensureGuardrailsCiStep(content, repoAlreadyWired = false) {
|
|
|
209
209
|
// Safe: anchorStepStart is an index returned by findLiveTestRunAnchor over
|
|
210
210
|
// this same content.split("\n"), so it is always within lines' bounds here.
|
|
211
211
|
const indent = lines[anchorStepStart].match(/^(\s*)/u)?.[1] ?? "";
|
|
212
|
-
|
|
212
|
+
// continue-on-error: true matches framework quality.yml and keeps
|
|
213
|
+
// ci-guardrails-wiring green without hard-failing thin consumers on
|
|
214
|
+
// agent-kit dogfood audits (command-surface, etc.) or known suite debt.
|
|
215
|
+
// Explicit hard-fail audits remain separate workflow steps.
|
|
216
|
+
lines.splice(anchorStepStart, 0, `${indent}- name: Run wp audit guardrails`, `${indent} continue-on-error: true`, `${indent} run: wp audit guardrails`);
|
|
213
217
|
return lines.join("\n");
|
|
214
218
|
}
|
|
215
219
|
function migrateConsumerWorkflows(repoRoot, actionRef, version, options) {
|
|
@@ -6,6 +6,8 @@ export const MCP_COMMAND_HELP = [
|
|
|
6
6
|
"by a Claude Code plugin manifest entry (`mcpServers.webpresso`) or any MCP",
|
|
7
7
|
"client. Tools are auto-discovered from `dist/esm/mcp/tools/*.js`.",
|
|
8
8
|
"",
|
|
9
|
+
"No subcommands are supported; use `wp review gate` for review workflows.",
|
|
10
|
+
"",
|
|
9
11
|
"Examples:",
|
|
10
12
|
" wp mcp",
|
|
11
13
|
].join("\n");
|
|
@@ -142,6 +142,14 @@ export function registerSyncCommand(cli) {
|
|
|
142
142
|
}
|
|
143
143
|
throw error;
|
|
144
144
|
}
|
|
145
|
+
// Operator-owned paths agent-kit refused to replace. A warning, never
|
|
146
|
+
// drift: treating it as a mismatch would red `wp sync --check` on every
|
|
147
|
+
// run for someone who legitimately customized a subagent.
|
|
148
|
+
for (const entry of result.preserved) {
|
|
149
|
+
console.warn(`wp sync: preserved ${entry.targetPath} — it is not an agent-kit projection, ` +
|
|
150
|
+
`so the catalog '${entry.slug}' agent was not written there. ` +
|
|
151
|
+
`Rename yours, or move it to agent-agents/ to have it managed.`);
|
|
152
|
+
}
|
|
145
153
|
const retiredMcp = cleanupRetiredMcpFanouts(repoRoot, { check });
|
|
146
154
|
for (const targetPath of retiredMcp.preserved) {
|
|
147
155
|
console.warn(`wp sync: preserved divergent retired MCP target ${targetPath}; remove it manually if it is no longer needed.`);
|
package/dist/esm/hooks/doctor.js
CHANGED
|
@@ -28,7 +28,7 @@ import { findAgentKitPackageRoot, resolveAgentKitPackageRoot, } from "#cli/comma
|
|
|
28
28
|
import { readInstalledHooksMap } from "#hooks/shared/installed-hooks.js";
|
|
29
29
|
import { auditCodexConfigPaths, checkCodexConfigPathIntegrity } from "#hooks/codex-config-paths.js";
|
|
30
30
|
import { findInstalledCodexPluginMcpManifests, pruneOwnedMcpServerBlock, } from "#cli/commands/init/scaffolders/codex-mcp/index.js";
|
|
31
|
-
import { isAgentKitOwnedWpCommand, WEBPRESSO_MCP_SERVER_NAME, } from "#cli/commands/init/mcp-spec.js";
|
|
31
|
+
import { resolveExecutableOnPath, WEBPRESSO_MCP_SPEC, isAgentKitOwnedWpCommand, WEBPRESSO_MCP_SERVER_NAME, } from "#cli/commands/init/mcp-spec.js";
|
|
32
32
|
import { parseProjectMcpServers, scanCodexConfigDrift } from "#hooks/host-config-contract.js";
|
|
33
33
|
import { defaultCodexConfigFilePath } from "#cli/commands/init/scaffolders/agent-hooks/codex-trust-sync.js";
|
|
34
34
|
import { expectedRootWpBinRelativePath, formatRootLauncherContractFailure, rootContractMode, validateRootLauncherContract, } from "#launcher/root-contract.js";
|
|
@@ -1201,31 +1201,135 @@ function runCommand(command, args, cwd = process.cwd(), timeoutMs = DEFAULT_HOOK
|
|
|
1201
1201
|
});
|
|
1202
1202
|
});
|
|
1203
1203
|
}
|
|
1204
|
-
|
|
1204
|
+
function asRecord(value) {
|
|
1205
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
1206
|
+
? value
|
|
1207
|
+
: null;
|
|
1208
|
+
}
|
|
1209
|
+
function unsupportedCodexJsonFlag(result) {
|
|
1210
|
+
const text = `${result.stderr}\n${result.stdout}`;
|
|
1211
|
+
return (/--json/u.test(text) && /(unexpected|unknown|unrecognized|invalid|found argument)/iu.test(text));
|
|
1212
|
+
}
|
|
1213
|
+
function classifyCodexMcpListJson(stdout) {
|
|
1214
|
+
let parsed;
|
|
1215
|
+
try {
|
|
1216
|
+
parsed = JSON.parse(stdout);
|
|
1217
|
+
}
|
|
1218
|
+
catch (error) {
|
|
1219
|
+
return {
|
|
1220
|
+
kind: "malformed",
|
|
1221
|
+
reason: error instanceof Error ? error.message : "invalid JSON",
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
if (!Array.isArray(parsed)) {
|
|
1225
|
+
return { kind: "malformed", reason: "expected a top-level array" };
|
|
1226
|
+
}
|
|
1227
|
+
const webpressoEntry = parsed
|
|
1228
|
+
.map((entry) => asRecord(entry))
|
|
1229
|
+
.find((entry) => entry?.name === WEBPRESSO_MCP_SERVER_NAME);
|
|
1230
|
+
if (!webpressoEntry)
|
|
1231
|
+
return { kind: "missing" };
|
|
1232
|
+
if (webpressoEntry.enabled !== true) {
|
|
1233
|
+
const reason = typeof webpressoEntry.disabled_reason === "string" &&
|
|
1234
|
+
webpressoEntry.disabled_reason.length > 0
|
|
1235
|
+
? webpressoEntry.disabled_reason
|
|
1236
|
+
: "enabled=false";
|
|
1237
|
+
return { kind: "disabled", reason };
|
|
1238
|
+
}
|
|
1239
|
+
const transport = asRecord(webpressoEntry.transport);
|
|
1240
|
+
if (!transport || transport.type !== "stdio" || typeof transport.command !== "string") {
|
|
1241
|
+
return { kind: "nonstdio" };
|
|
1242
|
+
}
|
|
1243
|
+
if (transport.command === "wp" && stringArrayEquals(transport.args, WEBPRESSO_MCP_SPEC.args)) {
|
|
1244
|
+
return { kind: "canonical", command: "wp", args: ["mcp"] };
|
|
1245
|
+
}
|
|
1246
|
+
const args = Array.isArray(transport.args)
|
|
1247
|
+
? transport.args.map((arg) => String(arg)).join(" ")
|
|
1248
|
+
: "";
|
|
1249
|
+
return { kind: "noncanonical", command: `${transport.command}${args ? ` ${args}` : ""}` };
|
|
1250
|
+
}
|
|
1251
|
+
function classifyCodexMcpLaunchability(stdout, env) {
|
|
1252
|
+
const classification = classifyCodexMcpListJson(stdout);
|
|
1253
|
+
switch (classification.kind) {
|
|
1254
|
+
case "canonical": {
|
|
1255
|
+
const resolved = resolveExecutableOnPath(classification.command, env);
|
|
1256
|
+
if (resolved) {
|
|
1257
|
+
return null;
|
|
1258
|
+
}
|
|
1259
|
+
const pathValue = env.PATH && env.PATH.length > 0 ? env.PATH : "(empty)";
|
|
1260
|
+
return {
|
|
1261
|
+
kind: "failure",
|
|
1262
|
+
detail: `@webpresso/codex-plugin/.mcp.json configures webpresso MCP as \`wp mcp\`, but \`wp\` is not resolvable on this doctor's PATH (${pathValue}); fix is not automatic: restore global \`wp\` in the Codex launch environment or run \`wp install codex\` after PATH is fixed`,
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
case "disabled":
|
|
1266
|
+
return {
|
|
1267
|
+
kind: "failure",
|
|
1268
|
+
detail: `webpresso MCP entry is configured but disabled by \`codex mcp list --json\` (${classification.reason}); fix is not automatic: inspect Codex MCP configuration and run \`wp install codex\` if the bundled plugin is stale`,
|
|
1269
|
+
};
|
|
1270
|
+
case "malformed":
|
|
1271
|
+
return {
|
|
1272
|
+
kind: "failure",
|
|
1273
|
+
detail: `codex mcp list --json returned malformed output (${classification.reason}); cannot verify webpresso MCP launchability; fix is not automatic: run \`wp install codex\` if the Codex CLI is stale`,
|
|
1274
|
+
};
|
|
1275
|
+
case "missing":
|
|
1276
|
+
return {
|
|
1277
|
+
kind: "failure",
|
|
1278
|
+
detail: "missing MCP entry (webpresso=false); run `wp install codex` to install the bundled plugin",
|
|
1279
|
+
};
|
|
1280
|
+
case "nonstdio":
|
|
1281
|
+
return {
|
|
1282
|
+
kind: "advisory",
|
|
1283
|
+
detail: "webpresso MCP configured with a non-stdio transport; launchability was not verified for user-owned transports",
|
|
1284
|
+
};
|
|
1285
|
+
case "noncanonical":
|
|
1286
|
+
return {
|
|
1287
|
+
kind: "advisory",
|
|
1288
|
+
detail: `webpresso MCP configured with non-canonical command \`${classification.command}\`; launchability was not verified for user-owned commands`,
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
async function checkCodexHost(env = process.env) {
|
|
1205
1293
|
const available = await runCommand("codex", ["--version"]);
|
|
1206
1294
|
if (!available.ok) {
|
|
1207
1295
|
return { name: "Codex host integration", ok: true, detail: "skipped (codex not on PATH)" };
|
|
1208
1296
|
}
|
|
1209
|
-
const result = await runCommand("codex", ["mcp", "list"]);
|
|
1297
|
+
const result = await runCommand("codex", ["mcp", "list", "--json"]);
|
|
1210
1298
|
if (!result.ok) {
|
|
1299
|
+
if (unsupportedCodexJsonFlag(result)) {
|
|
1300
|
+
return {
|
|
1301
|
+
name: "Codex host integration",
|
|
1302
|
+
ok: false,
|
|
1303
|
+
detail: "codex mcp list --json is unsupported by the installed Codex CLI; fix is not automatic: run `wp install codex` to refresh the Codex integration",
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1211
1306
|
return {
|
|
1212
1307
|
name: "Codex host integration",
|
|
1213
1308
|
ok: false,
|
|
1214
1309
|
detail: result.stderr.trim() || `exit ${result.code}`,
|
|
1215
1310
|
};
|
|
1216
1311
|
}
|
|
1217
|
-
const
|
|
1218
|
-
|
|
1312
|
+
const launchability = classifyCodexMcpLaunchability(result.stdout, env);
|
|
1313
|
+
const findings = launchability?.kind === "failure" ? [launchability.detail] : [];
|
|
1314
|
+
const advisories = launchability?.kind === "advisory" ? [launchability.detail] : [];
|
|
1315
|
+
const legacy = classifyLegacyCodexWebpressoChannel();
|
|
1316
|
+
if (legacy)
|
|
1317
|
+
findings.push(legacy);
|
|
1318
|
+
if (findings.length > 0) {
|
|
1219
1319
|
return {
|
|
1220
1320
|
name: "Codex host integration",
|
|
1221
1321
|
ok: false,
|
|
1222
|
-
detail:
|
|
1322
|
+
detail: [...findings, ...advisories].join("; "),
|
|
1223
1323
|
};
|
|
1224
1324
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
return {
|
|
1325
|
+
if (advisories.length > 0) {
|
|
1326
|
+
return { name: "Codex host integration", ok: true, detail: advisories.join("; ") };
|
|
1327
|
+
}
|
|
1328
|
+
return {
|
|
1329
|
+
name: "Codex host integration",
|
|
1330
|
+
ok: true,
|
|
1331
|
+
detail: "webpresso MCP visible (wp mcp launch command resolves on PATH)",
|
|
1332
|
+
};
|
|
1229
1333
|
}
|
|
1230
1334
|
/**
|
|
1231
1335
|
* Codex must receive `webpresso` over exactly one channel — the bundled Codex
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
export declare const ALL_BLUEPRINT_STATES: readonly ["draft", "planned", "in-progress", "parked", "archived", "completed"];
|
|
2
2
|
export declare const NON_COMPLETED_BLUEPRINT_STATES: readonly ["draft", "planned", "in-progress", "parked", "archived"];
|
|
3
|
+
/**
|
|
4
|
+
* Collect every reason "${slug}" cannot move to `completed`, without
|
|
5
|
+
* stopping at the first one: zero-task / unfinished-task blockers, missing
|
|
6
|
+
* task-local evidence per done task, `runValidate` document gaps, and the
|
|
7
|
+
* completed approval/digest gate. Callers that need to report every
|
|
8
|
+
* blocking condition in a single response (e.g. `wp_blueprint_finalize`)
|
|
9
|
+
* use this directly, before attempting any external side effect (platform
|
|
10
|
+
* sync) or local move — so a refusal never fires mid-move and a caller
|
|
11
|
+
* fixing issues one at a time never needs one failed round-trip per
|
|
12
|
+
* condition. {@link assertBlueprintCanComplete} throws the same combined
|
|
13
|
+
* message for callers that only need a single throw.
|
|
14
|
+
*/
|
|
15
|
+
export declare function collectBlueprintCompletionBlockers(overviewPath: string, slug: string): string[];
|
|
3
16
|
export declare function assertBlueprintCanComplete(overviewPath: string, slug: string): void;
|
|
4
17
|
export declare function applyLocalBlueprintTransition(input: {
|
|
5
18
|
projectCwd: string;
|
|
@@ -6,7 +6,7 @@ import { setBlueprintFrontmatterFields } from "#lifecycle/engine";
|
|
|
6
6
|
import { assertCompletedApprovalGate, evaluateApprovalGate, formatApprovalGateRequirement, } from "#lifecycle/audit";
|
|
7
7
|
import { resolveBlueprintRoot } from "#utils/blueprint-root.js";
|
|
8
8
|
import { getBlueprintDocumentPaths } from "#utils/document-paths.js";
|
|
9
|
-
import {
|
|
9
|
+
import { collectMissingCanonicalPassingEvidence } from "#verification.js";
|
|
10
10
|
import { writeFileAtomic } from "#shared-utils/write-json-file.js";
|
|
11
11
|
import { applyPromotionTrustGate } from "#trust/promotion.js";
|
|
12
12
|
import { reIngest } from "#mcp/blueprint/_shared/db";
|
|
@@ -34,18 +34,50 @@ function formatBlueprintProgress(totalTasks, doneTasks, blockedTasks) {
|
|
|
34
34
|
const percent = totalTasks === 0 ? 0 : Math.round((doneTasks / totalTasks) * 100);
|
|
35
35
|
return `${percent}% (${doneTasks}/${totalTasks} tasks done, ${blockedTasks} blocked, updated ${todayIsoDate()})`;
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Collect every reason "${slug}" cannot move to `completed`, without
|
|
39
|
+
* stopping at the first one: zero-task / unfinished-task blockers, missing
|
|
40
|
+
* task-local evidence per done task, `runValidate` document gaps, and the
|
|
41
|
+
* completed approval/digest gate. Callers that need to report every
|
|
42
|
+
* blocking condition in a single response (e.g. `wp_blueprint_finalize`)
|
|
43
|
+
* use this directly, before attempting any external side effect (platform
|
|
44
|
+
* sync) or local move — so a refusal never fires mid-move and a caller
|
|
45
|
+
* fixing issues one at a time never needs one failed round-trip per
|
|
46
|
+
* condition. {@link assertBlueprintCanComplete} throws the same combined
|
|
47
|
+
* message for callers that only need a single throw.
|
|
48
|
+
*/
|
|
49
|
+
export function collectBlueprintCompletionBlockers(overviewPath, slug) {
|
|
50
|
+
const blockers = [];
|
|
38
51
|
const markdown = readFileSync(overviewPath, "utf8");
|
|
39
52
|
const blueprint = parseBlueprint(markdown, slug);
|
|
40
53
|
if (blueprint.tasks.length === 0) {
|
|
41
|
-
|
|
54
|
+
blockers.push(`Cannot complete "${slug}": zero-task blueprints cannot move to completed through the public lifecycle surface`);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const unfinished = blueprint.tasks.filter((task) => task.status !== "done" && task.status !== "dropped");
|
|
58
|
+
if (unfinished.length > 0) {
|
|
59
|
+
const list = unfinished.map((task) => `${task.id} (${task.status})`).join(", ");
|
|
60
|
+
blockers.push(`Cannot complete "${slug}": the following tasks are not done: ${list}`);
|
|
61
|
+
}
|
|
62
|
+
blockers.push(...collectMissingCanonicalPassingEvidence(markdown, blueprint.tasks.filter((task) => task.status !== "dropped").map((task) => task.id)));
|
|
63
|
+
}
|
|
64
|
+
const validated = runValidate(overviewPath);
|
|
65
|
+
if (!validated.valid) {
|
|
66
|
+
blockers.push(...validated.gaps.map((gap) => `Cannot complete "${slug}": ${gap}`));
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
assertCompletedApprovalGate(overviewPath, markdown);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
blockers.push(`Cannot complete "${slug}": ${error instanceof Error ? error.message : String(error)}`);
|
|
42
73
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
74
|
+
return blockers;
|
|
75
|
+
}
|
|
76
|
+
export function assertBlueprintCanComplete(overviewPath, slug) {
|
|
77
|
+
const blockers = collectBlueprintCompletionBlockers(overviewPath, slug);
|
|
78
|
+
if (blockers.length > 0) {
|
|
79
|
+
throw new Error(blockers.join("; "));
|
|
47
80
|
}
|
|
48
|
-
assertAllTasksHaveCanonicalPassingEvidence(markdown, blueprint.tasks.filter((task) => task.status !== "dropped").map((task) => task.id));
|
|
49
81
|
}
|
|
50
82
|
export async function applyLocalBlueprintTransition(input) {
|
|
51
83
|
const { projectCwd, slug, to_state, found, trustedMarkdown: preflightTrustedMarkdown } = input;
|
|
@@ -1,5 +1,80 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
import type { ProjectResolver } from "#project-resolver.js";
|
|
2
3
|
import type { ToolHandlerResult } from "#mcp/auto-discover.js";
|
|
4
|
+
declare const putDocumentSchema: z.ZodObject<{
|
|
5
|
+
type: z.ZodDefault<z.ZodLiteral<"blueprint">>;
|
|
6
|
+
title: z.ZodString;
|
|
7
|
+
status: z.ZodEnum<{
|
|
8
|
+
archived: "archived";
|
|
9
|
+
completed: "completed";
|
|
10
|
+
draft: "draft";
|
|
11
|
+
"in-progress": "in-progress";
|
|
12
|
+
parked: "parked";
|
|
13
|
+
planned: "planned";
|
|
14
|
+
}>;
|
|
15
|
+
complexity: z.ZodEnum<{
|
|
16
|
+
L: "L";
|
|
17
|
+
M: "M";
|
|
18
|
+
S: "S";
|
|
19
|
+
XL: "XL";
|
|
20
|
+
XS: "XS";
|
|
21
|
+
}>;
|
|
22
|
+
owner: z.ZodString;
|
|
23
|
+
created: z.ZodString;
|
|
24
|
+
last_updated: z.ZodString;
|
|
25
|
+
progress: z.ZodOptional<z.ZodString>;
|
|
26
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
27
|
+
product_wedge_anchor: z.ZodObject<{
|
|
28
|
+
stage_outcome: z.ZodString;
|
|
29
|
+
consuming_surface: z.ZodString;
|
|
30
|
+
new_user_visible_capability: z.ZodString;
|
|
31
|
+
}, z.core.$strip>;
|
|
32
|
+
summary: z.ZodString;
|
|
33
|
+
tasks: z.ZodArray<z.ZodObject<{
|
|
34
|
+
id: z.ZodString;
|
|
35
|
+
title: z.ZodString;
|
|
36
|
+
status: z.ZodDefault<z.ZodEnum<{
|
|
37
|
+
blocked: "blocked";
|
|
38
|
+
done: "done";
|
|
39
|
+
dropped: "dropped";
|
|
40
|
+
"in-progress": "in-progress";
|
|
41
|
+
todo: "todo";
|
|
42
|
+
}>>;
|
|
43
|
+
wave: z.ZodOptional<z.ZodString>;
|
|
44
|
+
lane: z.ZodOptional<z.ZodString>;
|
|
45
|
+
description: z.ZodOptional<z.ZodString>;
|
|
46
|
+
acceptance: z.ZodArray<z.ZodString>;
|
|
47
|
+
}, z.core.$strip>>;
|
|
48
|
+
trust_dossier: z.ZodOptional<z.ZodObject<{
|
|
49
|
+
readiness: z.ZodObject<{
|
|
50
|
+
promotion_ready: z.ZodBoolean;
|
|
51
|
+
unresolved_count: z.ZodNumber;
|
|
52
|
+
verified_at: z.ZodString;
|
|
53
|
+
verified_head: z.ZodString;
|
|
54
|
+
trust_gate_version: z.ZodString;
|
|
55
|
+
}, z.core.$strip>;
|
|
56
|
+
material_claims: z.ZodArray<z.ZodObject<{
|
|
57
|
+
id: z.ZodString;
|
|
58
|
+
claim: z.ZodString;
|
|
59
|
+
evidence: z.ZodString;
|
|
60
|
+
}, z.core.$strip>>;
|
|
61
|
+
material_decisions: z.ZodArray<z.ZodObject<{
|
|
62
|
+
id: z.ZodString;
|
|
63
|
+
decision: z.ZodString;
|
|
64
|
+
chosen_option: z.ZodString;
|
|
65
|
+
rejected_alternatives: z.ZodString;
|
|
66
|
+
rationale: z.ZodString;
|
|
67
|
+
}, z.core.$strip>>;
|
|
68
|
+
promotion_gates: z.ZodArray<z.ZodObject<{
|
|
69
|
+
gate: z.ZodString;
|
|
70
|
+
command: z.ZodString;
|
|
71
|
+
expected_outcome: z.ZodString;
|
|
72
|
+
last_result: z.ZodString;
|
|
73
|
+
defer: z.ZodOptional<z.ZodLiteral<"pre-implementation">>;
|
|
74
|
+
}, z.core.$strip>>;
|
|
75
|
+
residual_unknowns: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
76
|
+
}, z.core.$strip>>;
|
|
77
|
+
}, z.core.$strip>;
|
|
3
78
|
export declare const putInputJsonSchema: {
|
|
4
79
|
type: string;
|
|
5
80
|
properties: {
|
|
@@ -233,6 +308,28 @@ export declare const putInputJsonSchema: {
|
|
|
233
308
|
};
|
|
234
309
|
required: string[];
|
|
235
310
|
};
|
|
311
|
+
/**
|
|
312
|
+
* Promotion-grade dossier completeness (promotion-ready: true,
|
|
313
|
+
* unresolved-count: 0, Residual Unknowns exactly `None.`, non-empty material
|
|
314
|
+
* claims/decisions/gates, passing gate results) is a PROMOTION-time
|
|
315
|
+
* requirement, not a draft-authoring one. `wp_blueprint_put` only ever writes
|
|
316
|
+
* `status: draft` documents (see the refusal in `handleBlueprintPut` above);
|
|
317
|
+
* gating every intermediate draft save on full promotion readiness forced
|
|
318
|
+
* authors to either fabricate readiness fields or keep genuinely-finished
|
|
319
|
+
* tasks marked `todo` to dodge the "checked box implies unreviewed
|
|
320
|
+
* completion" ambiguity scan -- inverting the draft -> planned lifecycle.
|
|
321
|
+
*
|
|
322
|
+
* `promotionCandidate` is therefore derived from the document's OWN target
|
|
323
|
+
* `status`, not hardcoded: a `draft` target skips promotion-grade dossier
|
|
324
|
+
* completeness entirely (an incomplete/in-progress dossier round-trips
|
|
325
|
+
* as-is), while any non-draft target keeps today's full strictness
|
|
326
|
+
* unchanged. Real promotion strictness is enforced separately and
|
|
327
|
+
* unconditionally at `wp_blueprint_transition`'s `crossesDraftApprovalBoundary`
|
|
328
|
+
* gate (`applyPromotionTrustGate` in `#trust/promotion.ts`), which still
|
|
329
|
+
* hardcodes `promotionCandidate: true` and is untouched by this function.
|
|
330
|
+
*/
|
|
331
|
+
export declare function validatePutTrustDossier(repoRoot: string, file: string, markdown: string, targetStatus: z.infer<typeof putDocumentSchema>["status"]): string | null;
|
|
236
332
|
export declare function handleBlueprintPut(projectResolver: ProjectResolver, cwd: string, raw: unknown): Promise<ToolHandlerResult>;
|
|
237
333
|
export declare function handleBlueprintTransition(projectResolver: ProjectResolver, cwd: string, raw: unknown): Promise<ToolHandlerResult>;
|
|
238
334
|
export declare function handleBlueprintCreate(projectResolver: ProjectResolver, cwd: string, raw: unknown): Promise<ToolHandlerResult>;
|
|
335
|
+
export {};
|
|
@@ -427,13 +427,33 @@ function describeEvidenceLossRefusal(taskId, submittedStatus) {
|
|
|
427
427
|
`that evidence. Keep task ${taskId} present with status "done" in the payload; put will carry ` +
|
|
428
428
|
`the recorded evidence forward automatically.`);
|
|
429
429
|
}
|
|
430
|
-
|
|
430
|
+
/**
|
|
431
|
+
* Promotion-grade dossier completeness (promotion-ready: true,
|
|
432
|
+
* unresolved-count: 0, Residual Unknowns exactly `None.`, non-empty material
|
|
433
|
+
* claims/decisions/gates, passing gate results) is a PROMOTION-time
|
|
434
|
+
* requirement, not a draft-authoring one. `wp_blueprint_put` only ever writes
|
|
435
|
+
* `status: draft` documents (see the refusal in `handleBlueprintPut` above);
|
|
436
|
+
* gating every intermediate draft save on full promotion readiness forced
|
|
437
|
+
* authors to either fabricate readiness fields or keep genuinely-finished
|
|
438
|
+
* tasks marked `todo` to dodge the "checked box implies unreviewed
|
|
439
|
+
* completion" ambiguity scan -- inverting the draft -> planned lifecycle.
|
|
440
|
+
*
|
|
441
|
+
* `promotionCandidate` is therefore derived from the document's OWN target
|
|
442
|
+
* `status`, not hardcoded: a `draft` target skips promotion-grade dossier
|
|
443
|
+
* completeness entirely (an incomplete/in-progress dossier round-trips
|
|
444
|
+
* as-is), while any non-draft target keeps today's full strictness
|
|
445
|
+
* unchanged. Real promotion strictness is enforced separately and
|
|
446
|
+
* unconditionally at `wp_blueprint_transition`'s `crossesDraftApprovalBoundary`
|
|
447
|
+
* gate (`applyPromotionTrustGate` in `#trust/promotion.ts`), which still
|
|
448
|
+
* hardcodes `promotionCandidate: true` and is untouched by this function.
|
|
449
|
+
*/
|
|
450
|
+
export function validatePutTrustDossier(repoRoot, file, markdown, targetStatus) {
|
|
431
451
|
const validated = validateBlueprintTrust({
|
|
432
452
|
repoRoot,
|
|
433
453
|
file,
|
|
434
454
|
status: "draft",
|
|
435
455
|
markdown,
|
|
436
|
-
promotionCandidate:
|
|
456
|
+
promotionCandidate: targetStatus !== "draft",
|
|
437
457
|
requirePassingGates: false,
|
|
438
458
|
scanTaskAmbiguity: true,
|
|
439
459
|
});
|
|
@@ -509,7 +529,7 @@ export async function handleBlueprintPut(projectResolver, cwd, raw) {
|
|
|
509
529
|
markdown = preserved.markdown;
|
|
510
530
|
}
|
|
511
531
|
if (document.trust_dossier) {
|
|
512
|
-
const trustDossierFailure = validatePutTrustDossier(writeCwd, overviewPath, markdown);
|
|
532
|
+
const trustDossierFailure = validatePutTrustDossier(writeCwd, overviewPath, markdown, document.status);
|
|
513
533
|
if (trustDossierFailure) {
|
|
514
534
|
return err("wp_blueprint_put validation error", trustDossierFailure);
|
|
515
535
|
}
|
|
@@ -7,7 +7,7 @@ import { ensureProjectionReady } from "#projection-ready.js";
|
|
|
7
7
|
import { resolveBlueprintRoot } from "#utils/blueprint-root.js";
|
|
8
8
|
import { dbPath } from "#mcp/blueprint/_shared/db";
|
|
9
9
|
import { err, finishPayload, jsonContent } from "#mcp/blueprint/_shared/errors";
|
|
10
|
-
import { applyLocalBlueprintTransition,
|
|
10
|
+
import { applyLocalBlueprintTransition, collectBlueprintCompletionBlockers, NON_COMPLETED_BLUEPRINT_STATES, } from "#mcp/blueprint/_shared/lifecycle";
|
|
11
11
|
import { toStr } from "#mcp/blueprint/_shared/payload";
|
|
12
12
|
import { findBlueprintDir, resolveToolProject } from "#mcp/blueprint/_shared/project";
|
|
13
13
|
import { resolveSyncAdapter, runPlatformMutationSync } from "#mcp/blueprint/_shared/sync";
|
|
@@ -53,8 +53,6 @@ export async function handleFinalize(projectResolver, cwd, raw) {
|
|
|
53
53
|
finally {
|
|
54
54
|
conn.close();
|
|
55
55
|
}
|
|
56
|
-
if (openTasks.length > 0)
|
|
57
|
-
return err("wp_blueprint_finalize refused", `Blueprint "${slug}" has open tasks: ${openTasks.map((t) => `${t.task_id} (${t.status})`).join(", ")}`);
|
|
58
56
|
const root = resolveBlueprintRoot(projectCwd);
|
|
59
57
|
const found = findBlueprintDir(root, slug, NON_COMPLETED_BLUEPRINT_STATES);
|
|
60
58
|
if (!found) {
|
|
@@ -69,11 +67,24 @@ export async function handleFinalize(projectResolver, cwd, raw) {
|
|
|
69
67
|
});
|
|
70
68
|
return err("wp_blueprint_finalize failed", `Blueprint "${slug}" not found`);
|
|
71
69
|
}
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
// Gather EVERY blocking condition up front, from every check this handler
|
|
71
|
+
// (and applyLocalBlueprintTransition, invoked later) is capable of raising
|
|
72
|
+
// — open tasks per the DB projection, zero-task/unfinished-task/missing-
|
|
73
|
+
// evidence per the raw markdown, doc-validate gaps, and the completed
|
|
74
|
+
// approval/digest gate. Refusing once per condition costs one failed
|
|
75
|
+
// round-trip per fix; a single combined refusal costs one. This also
|
|
76
|
+
// fixes atomicity at the handler level: neither the platform-sync side
|
|
77
|
+
// effect below nor the local move in applyLocalBlueprintTransition is
|
|
78
|
+
// ever attempted while any blocker remains, so a failed finalize call
|
|
79
|
+
// can never partially move — or platform-sync without locally moving —
|
|
80
|
+
// the blueprint.
|
|
81
|
+
const blockers = [];
|
|
82
|
+
if (openTasks.length > 0) {
|
|
83
|
+
blockers.push(`Blueprint "${slug}" has open tasks: ${openTasks.map((t) => `${t.task_id} (${t.status})`).join(", ")}`);
|
|
74
84
|
}
|
|
75
|
-
|
|
76
|
-
|
|
85
|
+
blockers.push(...collectBlueprintCompletionBlockers(found.path, slug));
|
|
86
|
+
if (blockers.length > 0) {
|
|
87
|
+
return err("wp_blueprint_finalize refused", blockers.join("; "));
|
|
77
88
|
}
|
|
78
89
|
// Platform-first path: push event + pull fresh replica before local move.
|
|
79
90
|
// Iron rule: resolveSyncAdapter() returns null when WP_BLUEPRINT_PLATFORM_DISABLED=1.
|
|
@@ -255,7 +255,7 @@ export async function registerBlueprintTools(registrar, cwd, projectResolver = c
|
|
|
255
255
|
"next_action",
|
|
256
256
|
],
|
|
257
257
|
}, (r) => handleBlueprintCreate(projectResolver, cwd, r), { title: "Blueprint Create", destructiveHint: false, openWorldHint: false });
|
|
258
|
-
registrar.registerTool("wp_blueprint_task_verify", "Mark a task done with an Evidence Contract. Requires at least one pass evidence item. Accepts optional request_id for idempotent retries, optional head_at_ingest from wp_blueprint_get/wp_blueprint_context to reject stale writes, and re-ingests DB on success. Returns { status, idempotent, next_action? }.", {
|
|
258
|
+
registrar.registerTool("wp_blueprint_task_verify", "Mark a task done with an Evidence Contract. Requires at least one pass evidence item. Accepts optional request_id for idempotent retries, optional head_at_ingest from wp_blueprint_get/wp_blueprint_context to reject stale writes, and re-ingests DB on success. Returns { status, idempotent, next_action? }. Each evidence item's required fields depend on its `kind`: 'test' requires `command` + `exit_code` (must be 0 when result='pass'); 'integration' additionally requires a non-empty `target_files`; 'audit' requires `audit_kind` + `passed:true` when result='pass'; 'manual' requires `actor`, `description`, the literal `allow_manual:true` (an explicit anti-shortcut opt-in), and a non-empty `log_excerpt` (<=4096 bytes) — it does NOT accept a `note` field.", {
|
|
259
259
|
type: "object",
|
|
260
260
|
properties: {
|
|
261
261
|
project_id: { type: "string" },
|
|
@@ -271,7 +271,36 @@ export async function registerBlueprintTools(registrar, cwd, projectResolver = c
|
|
|
271
271
|
properties: {
|
|
272
272
|
kind: { type: "string", enum: ["test", "integration", "audit", "manual"] },
|
|
273
273
|
result: { type: "string", enum: ["pass", "fail"] },
|
|
274
|
-
ts: { type: "string" },
|
|
274
|
+
ts: { type: "string", description: "ISO 8601 date-time string" },
|
|
275
|
+
agent: { type: "string" },
|
|
276
|
+
command: {
|
|
277
|
+
type: "string",
|
|
278
|
+
description: "Required for kind='test'/'integration'",
|
|
279
|
+
},
|
|
280
|
+
exit_code: {
|
|
281
|
+
type: "number",
|
|
282
|
+
description: "Required for kind='test'/'integration'; must be 0 when result='pass'",
|
|
283
|
+
},
|
|
284
|
+
target_files: {
|
|
285
|
+
type: "array",
|
|
286
|
+
items: { type: "string" },
|
|
287
|
+
description: "Required non-empty array for kind='integration'",
|
|
288
|
+
},
|
|
289
|
+
audit_kind: { type: "string", description: "Required for kind='audit'" },
|
|
290
|
+
passed: {
|
|
291
|
+
type: "boolean",
|
|
292
|
+
description: "Required for kind='audit'; must be true when result='pass'",
|
|
293
|
+
},
|
|
294
|
+
actor: { type: "string", description: "Required for kind='manual'" },
|
|
295
|
+
description: { type: "string", description: "Required for kind='manual'" },
|
|
296
|
+
allow_manual: {
|
|
297
|
+
type: "boolean",
|
|
298
|
+
description: "Required for kind='manual'; must be the literal boolean true (anti-shortcut opt-in)",
|
|
299
|
+
},
|
|
300
|
+
log_excerpt: {
|
|
301
|
+
type: "string",
|
|
302
|
+
description: "Required for kind='manual'; <=4096 bytes",
|
|
303
|
+
},
|
|
275
304
|
},
|
|
276
305
|
required: ["kind", "result", "ts"],
|
|
277
306
|
},
|
|
@@ -78,6 +78,23 @@ export interface UnifiedConsumerConfig {
|
|
|
78
78
|
* always project.
|
|
79
79
|
*/
|
|
80
80
|
readonly host?: AgentHostName;
|
|
81
|
+
/**
|
|
82
|
+
* Who owns the target directory, and therefore what agent-kit may destroy in
|
|
83
|
+
* it. Defaults to `"exclusive"`, preserving the historical sweep behavior.
|
|
84
|
+
*
|
|
85
|
+
* - `"exclusive"` — agent-kit owns the directory. It is generated and
|
|
86
|
+
* gitignored, has no hand-authoring workflow, and unexpected entries
|
|
87
|
+
* matching the managed shape are stale projections, safe to remove.
|
|
88
|
+
* - `"shared"` — a host-native authoring surface co-owned with the user
|
|
89
|
+
* (`.claude/agents` is Claude Code's own subagent authoring location).
|
|
90
|
+
* agent-kit may destroy only paths it can prove it created; anything else
|
|
91
|
+
* is preserved, because the surface is gitignored and loss is permanent.
|
|
92
|
+
*
|
|
93
|
+
* Declared rather than inferred on purpose: the stance used to be derived
|
|
94
|
+
* from `acceptsKind === "agent"` at each use site, which meant one code path
|
|
95
|
+
* had the guard and another silently did not.
|
|
96
|
+
*/
|
|
97
|
+
readonly ownership?: "exclusive" | "shared";
|
|
81
98
|
/**
|
|
82
99
|
* Legacy flag retained for shape compatibility. Plugin hosts no longer
|
|
83
100
|
* reactivate project-local skill projections under skip envs.
|
|
@@ -74,7 +74,15 @@ export const DEFAULT_UNIFIED_CONSUMERS = [
|
|
|
74
74
|
// Claude: canonical subagents are scaffolded to .claude/agents. Host-agnostic
|
|
75
75
|
// (unconditional) because subagents were previously scaffolded unconditionally
|
|
76
76
|
// by the now-retired standalone scaffoldSubagents scaffolder.
|
|
77
|
-
{
|
|
77
|
+
{
|
|
78
|
+
id: "claude-agents",
|
|
79
|
+
dir: ".claude/agents",
|
|
80
|
+
acceptsKind: "agent",
|
|
81
|
+
strategy: "symlink",
|
|
82
|
+
// Claude Code's own subagent authoring location: the user may hand-author
|
|
83
|
+
// here, and agent-kit gitignores it, so a wrong deletion is unrecoverable.
|
|
84
|
+
ownership: "shared",
|
|
85
|
+
},
|
|
78
86
|
];
|
|
79
87
|
/**
|
|
80
88
|
* Projection surfaces retired by a newer agent-kit release.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who may destroy a path under a consumer directory.
|
|
3
|
+
*
|
|
4
|
+
* This is the single owner of that question. It exists because the answer was
|
|
5
|
+
* previously implemented twice — conservatively in `pruneStale`, not at all in
|
|
6
|
+
* `applyTarget` — and the two drifted, which is how a consumer-authored file
|
|
7
|
+
* whose name collided with a catalog slug came to be deleted silently.
|
|
8
|
+
*
|
|
9
|
+
* The predicate is deliberately narrow: agent-kit may destroy a path only when
|
|
10
|
+
* it can PROVE it created that path, i.e. the path is a symlink resolving into
|
|
11
|
+
* a directory agent-kit projects from. A real file, a real directory, or a
|
|
12
|
+
* symlink pointing anywhere else is somebody else's and is never destroyed on a
|
|
13
|
+
* `shared` surface.
|
|
14
|
+
*/
|
|
15
|
+
export interface ManagedSourceDirsInput {
|
|
16
|
+
/** Catalog root passed to `runUnifiedSync` (may itself be a symlink alias). */
|
|
17
|
+
readonly catalogDir: string;
|
|
18
|
+
/** Consumer repo root, already realpath-resolved by the caller. */
|
|
19
|
+
readonly consumerRoot: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Directories agent-kit projects agent records from.
|
|
23
|
+
*
|
|
24
|
+
* Derived from CONFIGURATION, never from the planned record set, so the result
|
|
25
|
+
* is non-empty even when the catalog agents directory is empty or entirely
|
|
26
|
+
* absent. That matters: with a plans-derived set, a broken or partial install
|
|
27
|
+
* produces zero plans, an empty managed set, and a sweep that can no longer
|
|
28
|
+
* tell agent-kit's own relics from the operator's files.
|
|
29
|
+
*
|
|
30
|
+
* The catalog child is derived from `realpathSync(catalogDir)` rather than by
|
|
31
|
+
* realpathing the child itself. `loadContent` throws when `catalogDir` is
|
|
32
|
+
* missing, so the parent always resolves — whereas the child may not exist yet
|
|
33
|
+
* (or any more), and a non-existent child silently contributes only its
|
|
34
|
+
* unresolved alias form, which then fails to match a symlink recorded against
|
|
35
|
+
* the real path.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveManagedSourceDirs(input: ManagedSourceDirsInput): ReadonlySet<string>;
|
|
38
|
+
/**
|
|
39
|
+
* True when `targetPath` is a symlink agent-kit projected — the only shape it
|
|
40
|
+
* is allowed to destroy on a `shared` surface.
|
|
41
|
+
*
|
|
42
|
+
* The resolved target need not still exist: a dangling link left behind by a
|
|
43
|
+
* removed catalog record is agent-kit's own relic and must stay prunable.
|
|
44
|
+
* Containment is delegated to `isPathContained`, which canonicalizes both sides
|
|
45
|
+
* and rejects `..` escapes, so a sibling directory such as `<managed>-evil`
|
|
46
|
+
* cannot masquerade as managed.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isAgentKitOwned(targetPath: string, managedSourceDirs: ReadonlySet<string>): boolean;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who may destroy a path under a consumer directory.
|
|
3
|
+
*
|
|
4
|
+
* This is the single owner of that question. It exists because the answer was
|
|
5
|
+
* previously implemented twice — conservatively in `pruneStale`, not at all in
|
|
6
|
+
* `applyTarget` — and the two drifted, which is how a consumer-authored file
|
|
7
|
+
* whose name collided with a catalog slug came to be deleted silently.
|
|
8
|
+
*
|
|
9
|
+
* The predicate is deliberately narrow: agent-kit may destroy a path only when
|
|
10
|
+
* it can PROVE it created that path, i.e. the path is a symlink resolving into
|
|
11
|
+
* a directory agent-kit projects from. A real file, a real directory, or a
|
|
12
|
+
* symlink pointing anywhere else is somebody else's and is never destroyed on a
|
|
13
|
+
* `shared` surface.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, lstatSync, readlinkSync, realpathSync } from "node:fs";
|
|
16
|
+
import { dirname, join, resolve } from "node:path";
|
|
17
|
+
import { isPathContained } from "#worktrees/identity.js";
|
|
18
|
+
/**
|
|
19
|
+
* Directories agent-kit projects agent records from.
|
|
20
|
+
*
|
|
21
|
+
* Derived from CONFIGURATION, never from the planned record set, so the result
|
|
22
|
+
* is non-empty even when the catalog agents directory is empty or entirely
|
|
23
|
+
* absent. That matters: with a plans-derived set, a broken or partial install
|
|
24
|
+
* produces zero plans, an empty managed set, and a sweep that can no longer
|
|
25
|
+
* tell agent-kit's own relics from the operator's files.
|
|
26
|
+
*
|
|
27
|
+
* The catalog child is derived from `realpathSync(catalogDir)` rather than by
|
|
28
|
+
* realpathing the child itself. `loadContent` throws when `catalogDir` is
|
|
29
|
+
* missing, so the parent always resolves — whereas the child may not exist yet
|
|
30
|
+
* (or any more), and a non-existent child silently contributes only its
|
|
31
|
+
* unresolved alias form, which then fails to match a symlink recorded against
|
|
32
|
+
* the real path.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveManagedSourceDirs(input) {
|
|
35
|
+
const dirs = new Set();
|
|
36
|
+
const catalogRoots = new Set([resolve(input.catalogDir)]);
|
|
37
|
+
if (existsSync(input.catalogDir))
|
|
38
|
+
catalogRoots.add(realpathSync(input.catalogDir));
|
|
39
|
+
for (const catalogRoot of catalogRoots)
|
|
40
|
+
dirs.add(join(catalogRoot, "agents"));
|
|
41
|
+
// Consumer-side override dir. It is frequently absent (no scaffolder creates
|
|
42
|
+
// it), so it gets the same parent-anchored treatment.
|
|
43
|
+
const consumerRoots = new Set([resolve(input.consumerRoot)]);
|
|
44
|
+
if (existsSync(input.consumerRoot))
|
|
45
|
+
consumerRoots.add(realpathSync(input.consumerRoot));
|
|
46
|
+
for (const consumerRootForm of consumerRoots)
|
|
47
|
+
dirs.add(join(consumerRootForm, "agent-agents"));
|
|
48
|
+
return dirs;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* True when `targetPath` is a symlink agent-kit projected — the only shape it
|
|
52
|
+
* is allowed to destroy on a `shared` surface.
|
|
53
|
+
*
|
|
54
|
+
* The resolved target need not still exist: a dangling link left behind by a
|
|
55
|
+
* removed catalog record is agent-kit's own relic and must stay prunable.
|
|
56
|
+
* Containment is delegated to `isPathContained`, which canonicalizes both sides
|
|
57
|
+
* and rejects `..` escapes, so a sibling directory such as `<managed>-evil`
|
|
58
|
+
* cannot masquerade as managed.
|
|
59
|
+
*/
|
|
60
|
+
export function isAgentKitOwned(targetPath, managedSourceDirs) {
|
|
61
|
+
let stat;
|
|
62
|
+
try {
|
|
63
|
+
stat = lstatSync(targetPath);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
if (!stat.isSymbolicLink())
|
|
69
|
+
return false;
|
|
70
|
+
let resolved;
|
|
71
|
+
try {
|
|
72
|
+
resolved = resolve(dirname(targetPath), readlinkSync(targetPath));
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
const linkParent = dirname(resolved);
|
|
78
|
+
for (const managedDir of managedSourceDirs) {
|
|
79
|
+
if (isPathContained(managedDir, linkParent))
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
@@ -16,6 +16,22 @@
|
|
|
16
16
|
* filename pattern but is not in the expected set is removed. This propagates
|
|
17
17
|
* deletions in `agent-rules/` and `agent-skills/` to per-IDE cleanup.
|
|
18
18
|
*
|
|
19
|
+
* Ownership: a consumer declares `ownership` (see `UnifiedConsumerConfig`).
|
|
20
|
+
* On a `shared` surface agent-kit destroys only what it can prove it created,
|
|
21
|
+
* via the single predicate in `ownership.ts`. Both write paths consult that one
|
|
22
|
+
* predicate but take DIFFERENT actions, and the asymmetry is deliberate:
|
|
23
|
+
*
|
|
24
|
+
* | path | situation | action |
|
|
25
|
+
* |-------------|------------------------------------|-------------------------|
|
|
26
|
+
* | pruneStale | nothing in the catalog claims this | leave untouched |
|
|
27
|
+
* | | name | |
|
|
28
|
+
* | applyTarget | a catalog record DOES claim it | preserve + skip + warn |
|
|
29
|
+
*
|
|
30
|
+
* Quarantining or deleting an unclaimed file would be gratuitous — it is in
|
|
31
|
+
* nobody's way. Silently replacing a claimed one is the data-loss bug this
|
|
32
|
+
* guard exists to prevent, because `.claude/agents` is gitignored and the loss
|
|
33
|
+
* is unrecoverable.
|
|
34
|
+
*
|
|
19
35
|
* `--check` mode (dry-run): produce a list of (target, status) pairs and
|
|
20
36
|
* return the count of mismatches; perform no writes.
|
|
21
37
|
*/
|
|
@@ -68,6 +84,22 @@ export interface UnifiedSyncResult {
|
|
|
68
84
|
readonly fixCount: number;
|
|
69
85
|
/** Mismatches surfaced in check mode. Empty in non-check mode. */
|
|
70
86
|
readonly mismatches: readonly UnifiedSyncMismatch[];
|
|
87
|
+
/**
|
|
88
|
+
* Paths on a `shared` surface that agent-kit refused to replace because it
|
|
89
|
+
* could not prove it created them.
|
|
90
|
+
*
|
|
91
|
+
* Deliberately NOT mismatches: a mismatch means drift the tool should fix,
|
|
92
|
+
* and `wp sync --check` exits non-zero on drift. A user who legitimately
|
|
93
|
+
* customized a subagent would then red every CI run forever, which is the
|
|
94
|
+
* pressure that makes people delete their own file. These are warnings.
|
|
95
|
+
*/
|
|
96
|
+
readonly preserved: readonly UnifiedSyncPreserved[];
|
|
97
|
+
}
|
|
98
|
+
export interface UnifiedSyncPreserved {
|
|
99
|
+
readonly consumerId: string;
|
|
100
|
+
readonly targetPath: string;
|
|
101
|
+
/** Slug the catalog wanted to project there. */
|
|
102
|
+
readonly slug: string;
|
|
71
103
|
}
|
|
72
104
|
export declare function isSymlinkPointingTo(linkPath: string, expectedAbs: string): boolean;
|
|
73
105
|
/**
|
|
@@ -16,6 +16,22 @@
|
|
|
16
16
|
* filename pattern but is not in the expected set is removed. This propagates
|
|
17
17
|
* deletions in `agent-rules/` and `agent-skills/` to per-IDE cleanup.
|
|
18
18
|
*
|
|
19
|
+
* Ownership: a consumer declares `ownership` (see `UnifiedConsumerConfig`).
|
|
20
|
+
* On a `shared` surface agent-kit destroys only what it can prove it created,
|
|
21
|
+
* via the single predicate in `ownership.ts`. Both write paths consult that one
|
|
22
|
+
* predicate but take DIFFERENT actions, and the asymmetry is deliberate:
|
|
23
|
+
*
|
|
24
|
+
* | path | situation | action |
|
|
25
|
+
* |-------------|------------------------------------|-------------------------|
|
|
26
|
+
* | pruneStale | nothing in the catalog claims this | leave untouched |
|
|
27
|
+
* | | name | |
|
|
28
|
+
* | applyTarget | a catalog record DOES claim it | preserve + skip + warn |
|
|
29
|
+
*
|
|
30
|
+
* Quarantining or deleting an unclaimed file would be gratuitous — it is in
|
|
31
|
+
* nobody's way. Silently replacing a claimed one is the data-loss bug this
|
|
32
|
+
* guard exists to prevent, because `.claude/agents` is gitignored and the loss
|
|
33
|
+
* is unrecoverable.
|
|
34
|
+
*
|
|
19
35
|
* `--check` mode (dry-run): produce a list of (target, status) pairs and
|
|
20
36
|
* return the count of mismatches; perform no writes.
|
|
21
37
|
*/
|
|
@@ -24,6 +40,7 @@ import { dirname, join, relative, resolve } from "node:path";
|
|
|
24
40
|
import { loadContent } from "#content/loader";
|
|
25
41
|
import { DEFAULT_PACKAGED_SKILL_SLUG_SET } from "#content/skill-policy";
|
|
26
42
|
import { RETIRED_UNIFIED_CONSUMERS, selectUnifiedConsumers, unifiedRuleFilename, } from "./consumers.js";
|
|
43
|
+
import { isAgentKitOwned, resolveManagedSourceDirs } from "./ownership.js";
|
|
27
44
|
import { pathHasSymlinkComponent } from "./retired-surfaces.js";
|
|
28
45
|
/**
|
|
29
46
|
* Create a symlink with explicit Windows type hint. POSIX ignores the type;
|
|
@@ -170,12 +187,49 @@ function dirsEqual(srcDir, destDir) {
|
|
|
170
187
|
}
|
|
171
188
|
return true;
|
|
172
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* True when applying `plan` would destroy something agent-kit cannot prove it
|
|
192
|
+
* created, on a surface the user co-owns.
|
|
193
|
+
*
|
|
194
|
+
* This is the apply-side half of the ownership question. The prune side asks
|
|
195
|
+
* the same question via the same predicate but takes a different action, and
|
|
196
|
+
* the asymmetry is deliberate — see the module docstring.
|
|
197
|
+
*
|
|
198
|
+
* An `exclusive` consumer is unaffected: its directory is generated and
|
|
199
|
+
* gitignored, so replacing an unexpected entry is the intended behavior.
|
|
200
|
+
*/
|
|
201
|
+
function wouldDestroyForeignPath(plan, managedSourceDirs) {
|
|
202
|
+
if ((plan.consumer.ownership ?? "exclusive") !== "shared")
|
|
203
|
+
return false;
|
|
204
|
+
if (!existsSync(plan.targetPath) && !isSymlinkLike(plan.targetPath))
|
|
205
|
+
return false;
|
|
206
|
+
return !isAgentKitOwned(plan.targetPath, managedSourceDirs);
|
|
207
|
+
}
|
|
208
|
+
/** `existsSync` follows symlinks, so a dangling link needs its own probe. */
|
|
209
|
+
function isSymlinkLike(path) {
|
|
210
|
+
try {
|
|
211
|
+
return lstatSync(path).isSymbolicLink();
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
173
217
|
/**
|
|
174
218
|
* Apply a single planned target. Returns 1 on write, 0 if already correct.
|
|
219
|
+
*
|
|
220
|
+
* `preserved` is a third outcome, distinct from both: the projection was
|
|
221
|
+
* skipped because the target belongs to the operator. It is reported as a
|
|
222
|
+
* warning, never as drift — see `UnifiedSyncResult`.
|
|
175
223
|
*/
|
|
176
|
-
function applyTarget(plan, check) {
|
|
224
|
+
function applyTarget(plan, check, managedSourceDirs) {
|
|
177
225
|
const { consumer, record, targetPath } = plan;
|
|
178
226
|
const sourcePath = record.kind === "skill" ? dirname(record.filePath) : record.filePath;
|
|
227
|
+
// Gate EVERY strategy, not just the symlink branch: each one below uses its
|
|
228
|
+
// "is it already what we'd write?" check as the destruction gate, and none of
|
|
229
|
+
// those checks can distinguish "stale projection" from "operator's file".
|
|
230
|
+
if (wouldDestroyForeignPath(plan, managedSourceDirs)) {
|
|
231
|
+
return { wrote: 0, preserved: targetPath };
|
|
232
|
+
}
|
|
179
233
|
switch (consumer.strategy) {
|
|
180
234
|
case "symlink": {
|
|
181
235
|
// Use the resolved (real) source path so links survive pnpm version churn.
|
|
@@ -250,33 +304,6 @@ function applyTarget(plan, check) {
|
|
|
250
304
|
}
|
|
251
305
|
}
|
|
252
306
|
}
|
|
253
|
-
/**
|
|
254
|
-
* True when `fullPath` is a symlink whose resolved target's directory is one
|
|
255
|
-
* of `managedSourceDirs`. Used to distinguish an agent-kit-managed symlink
|
|
256
|
-
* (safe to prune when stale/renamed) from a real file or a foreign symlink
|
|
257
|
-
* (never agent-kit-managed, must survive `wp sync`). The resolved target
|
|
258
|
-
* itself need not currently exist — a dangling symlink to a since-removed
|
|
259
|
-
* catalog agent still resolves into the managed source dir and is prunable.
|
|
260
|
-
*/
|
|
261
|
-
function isManagedAgentSymlink(fullPath, managedSourceDirs) {
|
|
262
|
-
let stat;
|
|
263
|
-
try {
|
|
264
|
-
stat = lstatSync(fullPath);
|
|
265
|
-
}
|
|
266
|
-
catch {
|
|
267
|
-
return false;
|
|
268
|
-
}
|
|
269
|
-
if (!stat.isSymbolicLink())
|
|
270
|
-
return false;
|
|
271
|
-
try {
|
|
272
|
-
const linkTarget = readlinkSync(fullPath);
|
|
273
|
-
const resolved = resolve(dirname(fullPath), linkTarget);
|
|
274
|
-
return managedSourceDirs.has(dirname(resolved));
|
|
275
|
-
}
|
|
276
|
-
catch {
|
|
277
|
-
return false;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
307
|
/**
|
|
281
308
|
* Remove entries under each consumer dir that no longer correspond to any
|
|
282
309
|
* planned record. Only removes entries whose shape matches the consumer's
|
|
@@ -294,19 +321,10 @@ function pruneStale(plans, consumers, consumerRoot, check, preserveSkillSlugs, c
|
|
|
294
321
|
}
|
|
295
322
|
set.add(plan.entryName);
|
|
296
323
|
}
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
// subagent authoring location. A consumer-authored `.claude/agents/foo.md`
|
|
302
|
-
// that was never agent-kit-managed must survive `wp sync`; only symlinks
|
|
303
|
-
// that resolve into a known managed source dir are eligible for pruning.
|
|
304
|
-
//
|
|
305
|
-
// The set unions plan-derived dirs with the CONFIGURED source dirs so it is
|
|
306
|
-
// never empty: with an empty or missing catalog agents dir there are no
|
|
307
|
-
// plans, and a plans-only set would leave nothing provably managed — the
|
|
308
|
-
// sweep must then delete nothing, not everything. Dangling managed symlinks
|
|
309
|
-
// (pointing into a configured dir whose file is gone) stay prunable.
|
|
324
|
+
// Plan-derived source dirs are folded in alongside the configuration-derived
|
|
325
|
+
// set. The configured set already guarantees non-emptiness (see
|
|
326
|
+
// `resolveManagedSourceDirs`); the plan-derived entries additionally cover a
|
|
327
|
+
// consumer override dir the configuration cannot know about.
|
|
310
328
|
const agentSourceDirs = new Set([
|
|
311
329
|
...configuredAgentSourceDirs,
|
|
312
330
|
...plans
|
|
@@ -350,16 +368,18 @@ function pruneStale(plans, consumers, consumerRoot, check, preserveSkillSlugs, c
|
|
|
350
368
|
if (!matchesRulePattern && !matchesSkillPattern && !isLink)
|
|
351
369
|
continue;
|
|
352
370
|
const fullPath = join(dirAbs, name);
|
|
353
|
-
// Conservative prune
|
|
354
|
-
// symlinks (anything
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
|
|
371
|
+
// Conservative prune on a shared surface: skip real files, real dirs, and
|
|
372
|
+
// foreign symlinks (anything agent-kit cannot prove it created). The
|
|
373
|
+
// stance is read from the consumer's DECLARED ownership rather than
|
|
374
|
+
// inferred from its kind — inference by `.every(c => acceptsKind ===
|
|
375
|
+
// "agent")` would silently switch the guard off for the whole directory
|
|
376
|
+
// the moment any non-agent consumer was registered on it.
|
|
377
|
+
//
|
|
378
|
+
// Exclusive dirs (rules, skills) are generated and gitignored with no
|
|
379
|
+
// hand-authoring workflow, so their sweep-everything behavior is
|
|
380
|
+
// deliberately unchanged.
|
|
381
|
+
const isSharedDir = consumersForDir.some((c) => (c.ownership ?? "exclusive") === "shared");
|
|
382
|
+
if (isSharedDir && !isAgentKitOwned(fullPath, agentSourceDirs)) {
|
|
363
383
|
continue;
|
|
364
384
|
}
|
|
365
385
|
if (check) {
|
|
@@ -440,10 +460,27 @@ export function runUnifiedSync(options) {
|
|
|
440
460
|
throw new Error(`wp sync: slug collisions between catalog and consumer (rename consumer copy):\n${lines.join("\n")}`);
|
|
441
461
|
}
|
|
442
462
|
const plans = planTargets(filteredRecords, consumers, consumerRoot);
|
|
463
|
+
// The directories agent-kit projects agent records from. Configuration-
|
|
464
|
+
// derived and therefore never empty, so a broken install cannot collapse
|
|
465
|
+
// ownership into "everything is ours". Owned by ownership.ts, shared by the
|
|
466
|
+
// apply and prune paths so they cannot answer the question differently.
|
|
467
|
+
const configuredAgentSourceDirs = resolveManagedSourceDirs({
|
|
468
|
+
catalogDir: options.catalogDir,
|
|
469
|
+
consumerRoot,
|
|
470
|
+
});
|
|
443
471
|
let fixCount = 0;
|
|
444
472
|
const mismatches = [];
|
|
473
|
+
const preserved = [];
|
|
445
474
|
for (const plan of plans) {
|
|
446
|
-
const result = applyTarget(plan, options.check === true);
|
|
475
|
+
const result = applyTarget(plan, options.check === true, configuredAgentSourceDirs);
|
|
476
|
+
if (result.preserved !== undefined) {
|
|
477
|
+
preserved.push({
|
|
478
|
+
consumerId: plan.consumer.id,
|
|
479
|
+
targetPath: result.preserved,
|
|
480
|
+
slug: plan.record.slug,
|
|
481
|
+
});
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
447
484
|
if (result.wrote > 0) {
|
|
448
485
|
fixCount += result.wrote;
|
|
449
486
|
if (options.check === true && result.mismatch !== undefined) {
|
|
@@ -455,22 +492,6 @@ export function runUnifiedSync(options) {
|
|
|
455
492
|
}
|
|
456
493
|
}
|
|
457
494
|
}
|
|
458
|
-
// Configuration-derived agent source dirs (catalog `agents/` plus the
|
|
459
|
-
// consumer `agent-agents/` override). Added in both raw-resolved and
|
|
460
|
-
// realpath forms so set membership survives the /var → /private/var realm
|
|
461
|
-
// split on macOS. Present even when the dirs are empty or absent — that is
|
|
462
|
-
// the point: prune eligibility must not collapse to "everything" when a
|
|
463
|
-
// broken install yields zero planned agent records.
|
|
464
|
-
const configuredAgentSourceDirs = new Set();
|
|
465
|
-
for (const candidate of [
|
|
466
|
-
join(options.catalogDir, "agents"),
|
|
467
|
-
join(consumerRoot, "agent-agents"),
|
|
468
|
-
]) {
|
|
469
|
-
configuredAgentSourceDirs.add(resolve(candidate));
|
|
470
|
-
if (existsSync(candidate)) {
|
|
471
|
-
configuredAgentSourceDirs.add(realpathSync(candidate));
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
495
|
const prune = pruneStale(plans, consumers, consumerRoot, options.check === true, options.preserveSkillSlugs, configuredAgentSourceDirs);
|
|
475
496
|
fixCount += prune.removed;
|
|
476
497
|
mismatches.push(...prune.mismatches);
|
|
@@ -480,5 +501,5 @@ export function runUnifiedSync(options) {
|
|
|
480
501
|
const retiredDirectories = pruneEmptyRetiredConsumerDirs(retiredConsumers, consumerRoot, options.check === true);
|
|
481
502
|
fixCount += retiredDirectories.removed;
|
|
482
503
|
mismatches.push(...retiredDirectories.mismatches);
|
|
483
|
-
return { fixCount, mismatches };
|
|
504
|
+
return { fixCount, mismatches, preserved };
|
|
484
505
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpresso/agent-kit",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "TypeScript-first agent harness for guarded develop/deploy workflows: wp CLI gates, MCP tools, hooks, memory, worktrees, secrets, audits, and evidence checks.",
|
|
6
6
|
"keywords": [
|
|
@@ -514,16 +514,16 @@
|
|
|
514
514
|
"setupWpActionRef": "c2c71a7a4be446fc6858e6b57bf55a11ccfa2d88"
|
|
515
515
|
},
|
|
516
516
|
"optionalDependencies": {
|
|
517
|
-
"@webpresso/agent-kit-runtime-darwin-arm64": "3.3.
|
|
518
|
-
"@webpresso/agent-kit-runtime-darwin-x64": "3.3.
|
|
519
|
-
"@webpresso/agent-kit-runtime-linux-x64": "3.3.
|
|
520
|
-
"@webpresso/agent-kit-runtime-linux-arm64": "3.3.
|
|
521
|
-
"@webpresso/agent-kit-runtime-windows-x64": "3.3.
|
|
522
|
-
"@webpresso/agent-kit-session-memory-darwin-x64": "3.3.
|
|
523
|
-
"@webpresso/agent-kit-session-memory-darwin-arm64": "3.3.
|
|
524
|
-
"@webpresso/agent-kit-session-memory-linux-x64": "3.3.
|
|
525
|
-
"@webpresso/agent-kit-session-memory-linux-arm64": "3.3.
|
|
526
|
-
"@webpresso/agent-kit-session-memory-win32-x64": "3.3.
|
|
527
|
-
"@webpresso/agent-kit-session-memory-win32-arm64": "3.3.
|
|
517
|
+
"@webpresso/agent-kit-runtime-darwin-arm64": "3.3.6",
|
|
518
|
+
"@webpresso/agent-kit-runtime-darwin-x64": "3.3.6",
|
|
519
|
+
"@webpresso/agent-kit-runtime-linux-x64": "3.3.6",
|
|
520
|
+
"@webpresso/agent-kit-runtime-linux-arm64": "3.3.6",
|
|
521
|
+
"@webpresso/agent-kit-runtime-windows-x64": "3.3.6",
|
|
522
|
+
"@webpresso/agent-kit-session-memory-darwin-x64": "3.3.6",
|
|
523
|
+
"@webpresso/agent-kit-session-memory-darwin-arm64": "3.3.6",
|
|
524
|
+
"@webpresso/agent-kit-session-memory-linux-x64": "3.3.6",
|
|
525
|
+
"@webpresso/agent-kit-session-memory-linux-arm64": "3.3.6",
|
|
526
|
+
"@webpresso/agent-kit-session-memory-win32-x64": "3.3.6",
|
|
527
|
+
"@webpresso/agent-kit-session-memory-win32-arm64": "3.3.6"
|
|
528
528
|
}
|
|
529
529
|
}
|