@webpresso/agent-kit 3.3.4 → 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.

Files changed (42) hide show
  1. package/dist/esm/audit/agents.js +126 -26
  2. package/dist/esm/blueprint/trust/dossier.js +41 -1
  3. package/dist/esm/blueprint/verification.d.ts +12 -1
  4. package/dist/esm/blueprint/verification.js +26 -3
  5. package/dist/esm/build/vendor-oxlint-plugins.d.ts +4 -1
  6. package/dist/esm/build/vendor-oxlint-plugins.js +9 -14
  7. package/dist/esm/cli/commands/init/index.js +3 -4
  8. package/dist/esm/cli/commands/init/scaffold-base-kit.js +5 -1
  9. package/dist/esm/cli/commands/mcp.js +2 -0
  10. package/dist/esm/cli/commands/review.d.ts +1 -0
  11. package/dist/esm/cli/commands/review.js +77 -1
  12. package/dist/esm/cli/commands/sync.d.ts +8 -6
  13. package/dist/esm/cli/commands/sync.js +19 -9
  14. package/dist/esm/config/oxlint/oxlintrc.d.ts +1 -1
  15. package/dist/esm/config/oxlint/oxlintrc.js +3 -12
  16. package/dist/esm/content/dispatch.js +4 -0
  17. package/dist/esm/content/loader.d.ts +1 -1
  18. package/dist/esm/content/loader.js +10 -6
  19. package/dist/esm/content/skill-policy.d.ts +11 -0
  20. package/dist/esm/content/skill-policy.js +33 -0
  21. package/dist/esm/hooks/doctor.js +114 -10
  22. package/dist/esm/hooks/pretool-guard/dev-routing.js +67 -4
  23. package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.js +62 -3
  24. package/dist/esm/mcp/blueprint/_shared/lifecycle.d.ts +13 -0
  25. package/dist/esm/mcp/blueprint/_shared/lifecycle.js +40 -8
  26. package/dist/esm/mcp/blueprint/handlers/document-mutations.d.ts +97 -0
  27. package/dist/esm/mcp/blueprint/handlers/document-mutations.js +23 -3
  28. package/dist/esm/mcp/blueprint/handlers/finalize.js +18 -7
  29. package/dist/esm/mcp/blueprint/registration.js +31 -2
  30. package/dist/esm/review/authority.js +56 -1
  31. package/dist/esm/review/delivery-verifier.d.ts +4 -0
  32. package/dist/esm/review/delivery-verifier.js +9 -1
  33. package/dist/esm/symlinker/consumers.d.ts +20 -1
  34. package/dist/esm/symlinker/consumers.js +15 -1
  35. package/dist/esm/symlinker/ownership.d.ts +48 -0
  36. package/dist/esm/symlinker/ownership.js +83 -0
  37. package/dist/esm/symlinker/unified-sync.d.ts +35 -3
  38. package/dist/esm/symlinker/unified-sync.js +112 -14
  39. package/dist/esm/test/shard-durations.json +0 -1
  40. package/package.json +12 -12
  41. package/dist/esm/cli/commands/init/scaffolders/subagents/index.d.ts +0 -6
  42. package/dist/esm/cli/commands/init/scaffolders/subagents/index.js +0 -77
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Unified rule + skill sync.
2
+ * Unified rule + skill + agent sync.
3
3
  *
4
- * Source of truth: catalog (`<pkg>/dist/catalog/agent/{rules,skills}/`) UNION
5
- * consumer (`<repo>/agent-{rules,skills}/`). The loader returns a
4
+ * Source of truth: catalog (`<pkg>/dist/catalog/agent/{rules,skills,agents}/`)
5
+ * UNION consumer (`<repo>/agent-{rules,skills,agents}/`). The loader returns a
6
6
  * source-tagged record list; this module projects that list into per-IDE
7
7
  * surfaces according to `DEFAULT_UNIFIED_CONSUMERS`.
8
8
  *
@@ -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;
@@ -86,7 +103,9 @@ function planTargets(records, consumers, consumerRoot) {
86
103
  const consumerDirAbs = join(consumerRoot, consumer.dir);
87
104
  let entryName;
88
105
  let targetPath;
89
- if (record.kind === "rule") {
106
+ if (record.kind === "rule" || record.kind === "agent") {
107
+ // agent records are file-shaped like rules (a single <slug>.md),
108
+ // not dir-shaped like skills.
90
109
  entryName = unifiedRuleFilename(consumer, record.slug);
91
110
  targetPath = join(consumerDirAbs, entryName);
92
111
  }
@@ -168,12 +187,49 @@ function dirsEqual(srcDir, destDir) {
168
187
  }
169
188
  return true;
170
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
+ }
171
217
  /**
172
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`.
173
223
  */
174
- function applyTarget(plan, check) {
224
+ function applyTarget(plan, check, managedSourceDirs) {
175
225
  const { consumer, record, targetPath } = plan;
176
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
+ }
177
233
  switch (consumer.strategy) {
178
234
  case "symlink": {
179
235
  // Use the resolved (real) source path so links survive pnpm version churn.
@@ -254,7 +310,7 @@ function applyTarget(plan, check) {
254
310
  * managed pattern (rule files: `<slug><ext>`; skill dirs: subdirectories).
255
311
  * Real files outside the managed pattern (README.md, .gitkeep) are left alone.
256
312
  */
257
- function pruneStale(plans, consumers, consumerRoot, check, preserveSkillSlugs) {
313
+ function pruneStale(plans, consumers, consumerRoot, check, preserveSkillSlugs, configuredAgentSourceDirs) {
258
314
  const expectedByConsumerDir = new Map();
259
315
  for (const plan of plans) {
260
316
  const key = plan.consumer.dir;
@@ -265,6 +321,16 @@ function pruneStale(plans, consumers, consumerRoot, check, preserveSkillSlugs) {
265
321
  }
266
322
  set.add(plan.entryName);
267
323
  }
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.
328
+ const agentSourceDirs = new Set([
329
+ ...configuredAgentSourceDirs,
330
+ ...plans
331
+ .filter((plan) => plan.record.kind === "agent")
332
+ .map((plan) => dirname(plan.record.filePath)),
333
+ ]);
268
334
  let removed = 0;
269
335
  const mismatches = [];
270
336
  // Each unique dir gets a single sweep — multiple consumers can share a dir
@@ -292,15 +358,30 @@ function pruneStale(plans, consumers, consumerRoot, check, preserveSkillSlugs) {
292
358
  preserveSkillSlugs.has(name) &&
293
359
  entry.isDirectory())
294
360
  continue;
295
- // Only prune entries that match a managed shape: rule extension OR a
296
- // directory (skill).
297
- const matchesRulePattern = consumersForDir.some((c) => c.acceptsKind === "rule" && name.endsWith(c.ruleExtension ?? ".md"));
361
+ // Only prune entries that match a managed shape: rule/agent extension
362
+ // OR a directory (skill). Agent records are file-shaped like rules.
363
+ const matchesRulePattern = consumersForDir.some((c) => (c.acceptsKind === "rule" || c.acceptsKind === "agent") &&
364
+ name.endsWith(c.ruleExtension ?? ".md"));
298
365
  const matchesSkillPattern = consumersForDir.some((c) => c.acceptsKind === "skill") && entry.isDirectory();
299
366
  // Also handle stale symlinks of either shape.
300
367
  const isLink = entry.isSymbolicLink();
301
368
  if (!matchesRulePattern && !matchesSkillPattern && !isLink)
302
369
  continue;
303
370
  const fullPath = join(dirAbs, name);
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)) {
383
+ continue;
384
+ }
304
385
  if (check) {
305
386
  mismatches.push({
306
387
  consumerId: consumersForDir[0]?.id ?? dir,
@@ -348,7 +429,7 @@ function pruneEmptyRetiredConsumerDirs(consumers, consumerRoot, check) {
348
429
  * Main entrypoint. See module docstring.
349
430
  */
350
431
  export function runUnifiedSync(options) {
351
- const kinds = options.kinds ?? ["rule", "skill"];
432
+ const kinds = options.kinds ?? ["rule", "skill", "agent"];
352
433
  const kindSet = new Set(kinds);
353
434
  const consumers = (options.consumers ?? selectUnifiedConsumers(options.hosts)).filter((consumer) => kindSet.has(consumer.acceptsKind));
354
435
  const activeDirs = new Set(consumers.map((consumer) => consumer.dir));
@@ -379,10 +460,27 @@ export function runUnifiedSync(options) {
379
460
  throw new Error(`wp sync: slug collisions between catalog and consumer (rename consumer copy):\n${lines.join("\n")}`);
380
461
  }
381
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
+ });
382
471
  let fixCount = 0;
383
472
  const mismatches = [];
473
+ const preserved = [];
384
474
  for (const plan of plans) {
385
- 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
+ }
386
484
  if (result.wrote > 0) {
387
485
  fixCount += result.wrote;
388
486
  if (options.check === true && result.mismatch !== undefined) {
@@ -394,14 +492,14 @@ export function runUnifiedSync(options) {
394
492
  }
395
493
  }
396
494
  }
397
- const prune = pruneStale(plans, consumers, consumerRoot, options.check === true, options.preserveSkillSlugs);
495
+ const prune = pruneStale(plans, consumers, consumerRoot, options.check === true, options.preserveSkillSlugs, configuredAgentSourceDirs);
398
496
  fixCount += prune.removed;
399
497
  mismatches.push(...prune.mismatches);
400
- const retiredPrune = pruneStale([], retiredConsumers, consumerRoot, options.check === true, undefined);
498
+ const retiredPrune = pruneStale([], retiredConsumers, consumerRoot, options.check === true, undefined, configuredAgentSourceDirs);
401
499
  fixCount += retiredPrune.removed;
402
500
  mismatches.push(...retiredPrune.mismatches);
403
501
  const retiredDirectories = pruneEmptyRetiredConsumerDirs(retiredConsumers, consumerRoot, options.check === true);
404
502
  fixCount += retiredDirectories.removed;
405
503
  mismatches.push(...retiredDirectories.mismatches);
406
- return { fixCount, mismatches };
504
+ return { fixCount, mismatches, preserved };
407
505
  }
@@ -296,7 +296,6 @@
296
296
  "src/hooks/dispatch/index.test.ts": 11.771874999999994,
297
297
  "src/hooks/shared/hook-bootstrap.test.ts": 58.217375000000004,
298
298
  "src/cli/commands/init/scaffolders/agent-hooks/emitters/grok.test.ts": 67.57654199999999,
299
- "src/cli/commands/init/scaffolders/subagents/index.test.ts": 49.99270800000005,
300
299
  "src/blueprint/core/validation/criteria.test.ts": 9.635125000000016,
301
300
  "src/cli/commands/session.test.ts": 662.8378339999999,
302
301
  "src/cli/commands/dash/run-registry.test.ts": 793.002083,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpresso/agent-kit",
3
- "version": "3.3.4",
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.4",
518
- "@webpresso/agent-kit-runtime-darwin-x64": "3.3.4",
519
- "@webpresso/agent-kit-runtime-linux-x64": "3.3.4",
520
- "@webpresso/agent-kit-runtime-linux-arm64": "3.3.4",
521
- "@webpresso/agent-kit-runtime-windows-x64": "3.3.4",
522
- "@webpresso/agent-kit-session-memory-darwin-x64": "3.3.4",
523
- "@webpresso/agent-kit-session-memory-darwin-arm64": "3.3.4",
524
- "@webpresso/agent-kit-session-memory-linux-x64": "3.3.4",
525
- "@webpresso/agent-kit-session-memory-linux-arm64": "3.3.4",
526
- "@webpresso/agent-kit-session-memory-win32-x64": "3.3.4",
527
- "@webpresso/agent-kit-session-memory-win32-arm64": "3.3.4"
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
  }
@@ -1,6 +0,0 @@
1
- import type { MergeOptions, MergeResult } from "#cli/commands/init/merge";
2
- export interface ScaffoldSubagentsInput {
3
- repoRoot: string;
4
- options: MergeOptions;
5
- }
6
- export declare function scaffoldSubagents(input: ScaffoldSubagentsInput): MergeResult[];
@@ -1,77 +0,0 @@
1
- import { existsSync, lstatSync, mkdirSync, readdirSync, readlinkSync, rmSync, symlinkSync, } from "node:fs";
2
- import { dirname, join, relative } from "node:path";
3
- import { readPackageJson } from "#cli/commands/init/detect-consumer";
4
- import { resolveAgentKitPackageRootOrThrow } from "#cli/commands/init/package-root";
5
- function detectMode(repoRoot) {
6
- const pkg = readPackageJson(repoRoot).info;
7
- if (pkg?.name === "@webpresso/agent-kit") {
8
- return {
9
- mode: "self",
10
- sourceRoot: join(repoRoot, "catalog", "agent", "agents"),
11
- };
12
- }
13
- const installedPackageJsonPath = join(repoRoot, "node_modules", "@webpresso", "agent-kit", "package.json");
14
- const installedAgentsRoot = join(repoRoot, "node_modules", "@webpresso", "agent-kit", "catalog", "agent", "agents");
15
- if (existsSync(installedPackageJsonPath) && existsSync(installedAgentsRoot)) {
16
- return {
17
- mode: "consumer",
18
- sourceRoot: installedAgentsRoot,
19
- };
20
- }
21
- return {
22
- mode: "package-fallback",
23
- sourceRoot: join(resolveCurrentPackageRoot(), "catalog", "agent", "agents"),
24
- };
25
- }
26
- function resolveCurrentPackageRoot() {
27
- return resolveAgentKitPackageRootOrThrow("wp init: could not locate the webpresso package root for subagents fallback.", { requireCatalog: true });
28
- }
29
- export function scaffoldSubagents(input) {
30
- const { repoRoot, options } = input;
31
- const mode = detectMode(repoRoot);
32
- const sourceRoot = mode.sourceRoot;
33
- const targetRoot = join(repoRoot, ".claude", "agents");
34
- const results = [];
35
- if (!existsSync(sourceRoot))
36
- return results;
37
- const entries = readdirSync(sourceRoot).filter((f) => f.endsWith(".md") && f !== "README.md");
38
- if (entries.length === 0)
39
- return results;
40
- if (!options.dryRun) {
41
- mkdirSync(targetRoot, { recursive: true });
42
- }
43
- for (const name of entries) {
44
- const sourcePath = join(sourceRoot, name);
45
- const targetPath = join(targetRoot, name);
46
- const symTarget = relative(dirname(targetPath), sourcePath);
47
- if (options.dryRun) {
48
- results.push({ targetPath, action: "created" });
49
- continue;
50
- }
51
- try {
52
- const stat = lstatSync(targetPath);
53
- if (stat.isSymbolicLink()) {
54
- const currentTarget = readlinkSync(targetPath);
55
- if (currentTarget === symTarget) {
56
- results.push({ targetPath, action: "identical" });
57
- }
58
- else if (options.overwrite) {
59
- rmSync(targetPath);
60
- symlinkSync(symTarget, targetPath);
61
- results.push({ targetPath, action: "overwritten" });
62
- }
63
- else {
64
- results.push({ targetPath, action: "drifted" });
65
- }
66
- }
67
- else {
68
- results.push({ targetPath, action: "identical" });
69
- }
70
- }
71
- catch {
72
- symlinkSync(symTarget, targetPath);
73
- results.push({ targetPath, action: "created" });
74
- }
75
- }
76
- return results;
77
- }