@webpresso/agent-kit 3.3.0 → 3.3.1

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.
@@ -320,6 +320,21 @@ checkout target. Local cleanup and primary resync cannot run from GitHub Actions
320
320
  do not claim landing complete until this cleanup succeeds or is recorded as a
321
321
  blocker with evidence.
322
322
 
323
+ **Owner dirt precondition:** the owner worktree must have empty
324
+ `git status --porcelain` before merge-cleanup. If you ran `wp blueprint new` and
325
+ left `blueprints/draft/<slug>/` uncommitted (or staged only), either:
326
+
327
+ 1. **Land the draft** — include `blueprints/draft/<slug>/` (and complete the
328
+ lifecycle) in the PR, or
329
+ 2. **Abandon the scaffold** — `wp blueprint abandon <slug> --confirm` (preview
330
+ without `--confirm`), then re-run merge-cleanup.
331
+
332
+ `Blueprint-exempt` is for work that **never created a blueprint**. Do not
333
+ scaffold a draft only to exempt it. For multi-file exempt follow-ups, use a
334
+ **non-`bp/`** worktree prefix (`exempt/<name>` or `chore/<name>`) via
335
+ `wp worktree new` — never `bp/<slug>`, which pre-claims the owner branch and
336
+ collides with `wp blueprint start`.
337
+
323
338
  When the primary checkout has user-owned edits or is still on a topic branch,
324
339
  prefer:
325
340
 
@@ -0,0 +1,21 @@
1
+ export interface AbandonBlueprintOptions {
2
+ readonly projectRoot?: string;
3
+ readonly confirm?: boolean;
4
+ /** Injected for tests. */
5
+ readonly resolveOwnerPath?: (repoRoot: string, slug: string) => string;
6
+ readonly readPorcelainZ?: (cwd: string) => string;
7
+ readonly hasCommittedDraft?: (cwd: string, slug: string) => boolean;
8
+ readonly isPrimaryCheckout?: (ownerPath: string, repoRoot: string) => boolean;
9
+ }
10
+ export interface AbandonBlueprintResult {
11
+ readonly slug: string;
12
+ readonly ownerPath: string;
13
+ readonly preview: boolean;
14
+ readonly message: string;
15
+ readonly paths: readonly string[];
16
+ }
17
+ /**
18
+ * Abandon an uncommitted draft scaffold so merge-cleanup can proceed.
19
+ * Without --confirm: print preview and throw (exit non-zero at CLI).
20
+ */
21
+ export declare function abandonBlueprint(rawSlug: string, options?: AbandonBlueprintOptions): Promise<AbandonBlueprintResult>;
@@ -0,0 +1,178 @@
1
+ import { existsSync, lstatSync, rmSync } from "node:fs";
2
+ import { join, resolve, sep } from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { isValidBlueprintSlug, relativeBlueprintSlug } from "#lifecycle/local.js";
5
+ import { resolveProjectRoot } from "#mcp/tools/_shared/project-root.js";
6
+ import { classifyOwnerDirt, draftScaffoldPrefix, formatBoundedPaths, } from "#worktrees/owner-dirt.js";
7
+ import { resolveOwnerWorktreePath } from "#worktrees/location.js";
8
+ function gitStdout(cwd, args) {
9
+ const result = spawnSync("git", [...args], { cwd, encoding: "utf8" });
10
+ if (result.status !== 0) {
11
+ const err = String(result.stderr ?? result.stdout ?? "git failed").trim();
12
+ throw new Error(err || `git ${args.join(" ")} failed`);
13
+ }
14
+ return String(result.stdout ?? "");
15
+ }
16
+ function defaultReadPorcelainZ(cwd) {
17
+ return gitStdout(cwd, [
18
+ "status",
19
+ "--porcelain=v1",
20
+ "--untracked-files=all",
21
+ "--ignored=no",
22
+ "-z",
23
+ ]);
24
+ }
25
+ function defaultHasCommittedDraft(cwd, slug) {
26
+ const prefix = draftScaffoldPrefix(slug);
27
+ const mergeBase = spawnSync("git", ["merge-base", "HEAD", "origin/main"], {
28
+ cwd,
29
+ encoding: "utf8",
30
+ });
31
+ const range = mergeBase.status === 0 && String(mergeBase.stdout ?? "").trim()
32
+ ? `${String(mergeBase.stdout).trim()}..HEAD`
33
+ : "HEAD";
34
+ const log = spawnSync("git", ["log", "--oneline", range, "--", prefix], {
35
+ cwd,
36
+ encoding: "utf8",
37
+ });
38
+ if (log.status !== 0) {
39
+ // Fail closed: if history probe fails, do not delete.
40
+ return true;
41
+ }
42
+ return String(log.stdout ?? "").trim().length > 0;
43
+ }
44
+ function assertPathInsideDraftRoot(ownerPath, draftAbs) {
45
+ const draftRoot = resolve(ownerPath, "blueprints", "draft") + sep;
46
+ const target = resolve(draftAbs);
47
+ if (target !== resolve(ownerPath, "blueprints", "draft") && !target.startsWith(draftRoot)) {
48
+ throw new Error(`Refusing abandon: path escapes blueprints/draft/ (${target}). next: inspect worktree layout`);
49
+ }
50
+ }
51
+ function isOwnerPathForSlug(path, slug) {
52
+ const normalized = path.replace(/\\/gu, "/");
53
+ return (normalized.endsWith(`/blueprints/${slug}/owner`) ||
54
+ normalized.endsWith(`/blueprints/${slug}/owner/`));
55
+ }
56
+ function resolveOwnerForSlug(repoRoot, slug, resolveOwnerPath) {
57
+ const cwd = resolve(repoRoot);
58
+ // Already inside the owner worktree for this slug (typical agent cwd).
59
+ if (isOwnerPathForSlug(cwd, slug)) {
60
+ return cwd;
61
+ }
62
+ const ownerPath = resolve(resolveOwnerPath(repoRoot, slug));
63
+ if (ownerPath === cwd) {
64
+ throw new Error(`Refusing abandon: resolved owner is the primary checkout (${ownerPath}). next: abandon only from a bp/<slug> owner worktree, or remove primary draft manually after review`);
65
+ }
66
+ if (!isOwnerPathForSlug(ownerPath, slug)) {
67
+ throw new Error(`Refusing abandon: worktree ${ownerPath} is not the owner path for ${slug}. next: run from bp/${slug} owner or pass the correct project root`);
68
+ }
69
+ return ownerPath;
70
+ }
71
+ /**
72
+ * Abandon an uncommitted draft scaffold so merge-cleanup can proceed.
73
+ * Without --confirm: print preview and throw (exit non-zero at CLI).
74
+ */
75
+ export async function abandonBlueprint(rawSlug, options = {}) {
76
+ const slug = relativeBlueprintSlug(rawSlug.trim());
77
+ if (!isValidBlueprintSlug(slug)) {
78
+ throw new Error(`Refusing abandon: invalid blueprint slug ${JSON.stringify(rawSlug)}. next: use a kebab-case slug like my-feature`);
79
+ }
80
+ const projectRoot = options.projectRoot
81
+ ? resolveProjectRoot({ cwd: options.projectRoot, explicitCwd: options.projectRoot })
82
+ : resolveProjectRoot();
83
+ const resolveOwnerPath = options.resolveOwnerPath ?? resolveOwnerWorktreePath;
84
+ const readPorcelainZ = options.readPorcelainZ ?? defaultReadPorcelainZ;
85
+ const hasCommittedDraft = options.hasCommittedDraft ?? defaultHasCommittedDraft;
86
+ const ownerPath = resolveOwnerForSlug(projectRoot, slug, resolveOwnerPath);
87
+ const draftRel = draftScaffoldPrefix(slug);
88
+ const draftAbs = join(ownerPath, draftRel);
89
+ assertPathInsideDraftRoot(ownerPath, draftAbs);
90
+ if (existsSync(draftAbs)) {
91
+ const st = lstatSync(draftAbs);
92
+ if (st.isSymbolicLink()) {
93
+ // Only remove the link itself when confirmed — never follow.
94
+ if (options.confirm !== true) {
95
+ throw new Error([
96
+ `Would remove symlink draft scaffold ${draftRel} in ${ownerPath}`,
97
+ "Re-run with --confirm to remove the symlink only (not the target).",
98
+ ].join("\n"));
99
+ }
100
+ rmSync(draftAbs, { force: true });
101
+ const afterLink = classifyOwnerDirt(readPorcelainZ(ownerPath), slug);
102
+ if (afterLink.kind !== "clean") {
103
+ throw new Error(`Abandon incomplete; remaining dirt:\n${formatBoundedPaths(afterLink.paths)}\nnext: git status in ${ownerPath}`);
104
+ }
105
+ return {
106
+ slug,
107
+ ownerPath,
108
+ preview: false,
109
+ paths: [draftRel],
110
+ message: `Abandoned draft symlink scaffold for ${slug}; worktree clean — next: wp worktree merge-cleanup bp/${slug}`,
111
+ };
112
+ }
113
+ }
114
+ if (hasCommittedDraft(ownerPath, slug)) {
115
+ throw new Error([
116
+ `Refusing abandon: commits on this branch touch ${draftRel}.`,
117
+ "next: land the draft with the PR, or revert those commits (git revert / interactive rebase) — abandon only deletes uncommitted scaffolds",
118
+ ].join("\n"));
119
+ }
120
+ const porcelainZ = readPorcelainZ(ownerPath);
121
+ const classification = classifyOwnerDirt(porcelainZ, slug);
122
+ if (classification.kind === "clean" && !existsSync(draftAbs)) {
123
+ throw new Error(`Nothing to abandon for ${slug}: no draft scaffold dirt. next: wp worktree merge-cleanup bp/${slug}`);
124
+ }
125
+ if (classification.kind === "other") {
126
+ throw new Error([
127
+ `Refusing abandon: owner has non-scaffold dirt for ${slug}:`,
128
+ formatBoundedPaths(classification.paths),
129
+ `next: commit or stash non-scaffold paths in ${ownerPath}, then re-run abandon or merge-cleanup`,
130
+ ].join("\n"));
131
+ }
132
+ // scaffold-only or clean-but-empty-dir leftover with no porcelain (empty dirs invisible)
133
+ const paths = classification.paths.length > 0 ? classification.paths : existsSync(draftAbs) ? [draftRel] : [];
134
+ if (options.confirm !== true) {
135
+ throw new Error([
136
+ `Would abandon draft scaffold for ${slug} in ${ownerPath}:`,
137
+ formatBoundedPaths(paths.length > 0 ? paths : [draftRel]),
138
+ "Re-run with --confirm to delete. next: wp blueprint abandon <slug> --confirm",
139
+ ].join("\n"));
140
+ }
141
+ // Unstage any index entries under the scaffold, then delete the tree.
142
+ const cached = spawnSync("git", ["rm", "-r", "--cached", "--ignore-unmatch", "--", draftRel], {
143
+ cwd: ownerPath,
144
+ encoding: "utf8",
145
+ });
146
+ if (cached.status !== 0 && cached.status !== null) {
147
+ // ignore-unmatch should make this soft; still fail closed on hard errors
148
+ const err = String(cached.stderr ?? "").trim();
149
+ if (err && !/did not match/iu.test(err)) {
150
+ throw new Error(`git rm --cached failed: ${err}. next: git status in ${ownerPath}`);
151
+ }
152
+ }
153
+ if (existsSync(draftAbs)) {
154
+ assertPathInsideDraftRoot(ownerPath, draftAbs);
155
+ const st = lstatSync(draftAbs);
156
+ if (st.isSymbolicLink()) {
157
+ rmSync(draftAbs, { force: true });
158
+ }
159
+ else if (st.isDirectory() || st.isFile()) {
160
+ rmSync(draftAbs, { recursive: true, force: true });
161
+ }
162
+ }
163
+ const after = classifyOwnerDirt(readPorcelainZ(ownerPath), slug);
164
+ if (after.kind !== "clean") {
165
+ throw new Error([
166
+ `Abandon incomplete; remaining dirt:`,
167
+ formatBoundedPaths(after.paths),
168
+ `next: git status in ${ownerPath}`,
169
+ ].join("\n"));
170
+ }
171
+ return {
172
+ slug,
173
+ ownerPath,
174
+ preview: false,
175
+ paths,
176
+ message: `Abandoned draft scaffold for ${slug}; worktree clean — next: wp worktree merge-cleanup bp/${slug}`,
177
+ };
178
+ }
@@ -6,6 +6,7 @@ import { executeBlueprintDbSubcommand } from "./db-commands.js";
6
6
  import { getBlueprintSubcommandNames } from "./router-output.js";
7
7
  import { listTemplates, resolveTemplate } from "./template-resolver.js";
8
8
  import { TRUST_DOSSIER_SCAFFOLD } from "#trust/scaffold.js";
9
+ import { abandonBlueprint } from "./abandon.js";
9
10
  // ---------------------------------------------------------------------------
10
11
  // Platform template fetcher (injectable for tests)
11
12
  // ---------------------------------------------------------------------------
@@ -261,6 +262,26 @@ export async function executeBlueprintSubcommand(subcommand, args, options, deps
261
262
  deps.printBlueprintOutput(options.json ? result : result.message, options.json);
262
263
  return;
263
264
  }
265
+ case "abandon": {
266
+ const slug = args[0];
267
+ if (!slug) {
268
+ throw new Error("Usage: wp blueprint abandon <slug> --confirm");
269
+ }
270
+ const result = await abandonBlueprint(slug, {
271
+ projectRoot: options.projectRoot,
272
+ confirm: options.confirm === true,
273
+ });
274
+ deps.printBlueprintOutput(options.json
275
+ ? {
276
+ slug: result.slug,
277
+ ownerPath: result.ownerPath,
278
+ preview: result.preview,
279
+ paths: result.paths,
280
+ message: result.message,
281
+ }
282
+ : result.message, options.json);
283
+ return;
284
+ }
264
285
  case "finalize": {
265
286
  const slug = args[0];
266
287
  if (!slug) {
@@ -18,6 +18,7 @@ export const BLUEPRINT_SUBCOMMANDS = [
18
18
  },
19
19
  { name: "start", usage: ["start <slug>"] },
20
20
  { name: "park", usage: ["park <slug>"] },
21
+ { name: "abandon", usage: ["abandon <slug> --confirm"] },
21
22
  {
22
23
  name: "task",
23
24
  usage: [
@@ -42,6 +42,8 @@ export interface BlueprintCommandOptions extends BlueprintAuditOptions, Blueprin
42
42
  template?: string;
43
43
  templatesDir?: string;
44
44
  to?: string;
45
+ /** Required for destructive `blueprint abandon`. */
46
+ confirm?: boolean;
45
47
  "--": string[];
46
48
  }
47
49
  export type { AdvanceTaskResult, PromoteBlueprintResult };
@@ -175,9 +175,11 @@ export async function createBlueprint(goal, options = {}) {
175
175
  : probeService;
176
176
  // Single full compile lives inside create() on the write root.
177
177
  const created = await writeService.create({ complexity, goal, type });
178
+ const draftHint = "Draft is uncommitted — include blueprints/draft/" +
179
+ `${created.slug}/ in your first commit, or run: wp blueprint abandon ${created.slug} --confirm before merge-cleanup.`;
178
180
  const message = authoring.routed
179
- ? `Created ${created.type} draft/${created.slug} (routed from primary-like checkout).`
180
- : `Created ${created.type} draft/${created.slug}.`;
181
+ ? `Created ${created.type} draft/${created.slug} (routed from primary-like checkout).\n${draftHint}`
182
+ : `Created ${created.type} draft/${created.slug}.\n${draftHint}`;
181
183
  return {
182
184
  ...created,
183
185
  message,
@@ -541,6 +543,7 @@ export function registerBlueprintRouter(cli) {
541
543
  .option("--template <name>", "Template name to scaffold new blueprint from (see --list-templates)")
542
544
  .option("--list-templates", "List available template names and exit")
543
545
  .option("--templates-dir <path>", "Override the templates directory (default: docs/templates/)")
546
+ .option("--confirm", "Confirm destructive blueprint abandon")
544
547
  .action(async (subcommand, args, options) => {
545
548
  try {
546
549
  await executeBlueprintSubcommand(subcommand, args, options, {
@@ -12,6 +12,16 @@ export declare const GIT_PROBE_TIMEOUT_MS = 15000;
12
12
  * raise/lower via `WP_SETUP_COMMIT_TIMEOUT_MS` or `AuthorCommitOptions.commitTimeoutMs`.
13
13
  */
14
14
  export declare const DEFAULT_GIT_COMMIT_TIMEOUT_MS = 600000;
15
+ /** Max stdout+stderr retained from probe git subprocesses (Node spawnSync maxBuffer). */
16
+ export declare const GIT_MAX_BUFFER: number;
17
+ /**
18
+ * Max stdout+stderr for the single hookful `git commit`. Quiet hooks cost nothing
19
+ * (Node does not preallocate); chatty consumer format+audit chains can exceed 1 MiB
20
+ * and would otherwise die with ENOBUFS+SIGTERM mid-commit.
21
+ */
22
+ export declare const GIT_COMMIT_MAX_BUFFER: number;
23
+ /** True when spawnSync killed the child for maxBuffer / stdio overflow. */
24
+ export declare function isBufferOverflowErrorCode(code: string | null | undefined): boolean;
15
25
  /**
16
26
  * Resolve the hookful-commit timeout. Preference order:
17
27
  * 1. explicit `commitTimeoutMs` option (tests / programmatic callers)
@@ -38,21 +48,34 @@ export declare function formatCommitInterruptError(options: {
38
48
  readonly signal: NodeJS.Signals;
39
49
  readonly hookOutput?: string;
40
50
  }): string;
51
+ /**
52
+ * First-class diagnosis when Node spawnSync kills the child because hook/git
53
+ * output exceeded maxBuffer (ENOBUFS / ERR_CHILD_PROCESS_STDIO_MAXBUFFER).
54
+ * Distinct from budget timeout and from external signal kills.
55
+ */
56
+ export declare function formatCommitBufferError(options: {
57
+ readonly maxBufferBytes: number;
58
+ readonly errorCode?: string | null;
59
+ readonly hookOutput?: string;
60
+ }): string;
41
61
  /**
42
62
  * Classify a failed hookful `git commit` result.
43
63
  *
44
- * - `timedOut` (spawnSync ETIMEDOUT) → budget timeout diagnosis
45
- * - non-null `signal` without timedOutinterrupt diagnosis (names the signal)
46
- * - otherwise hook/git stderr when present, else a generic preserved-index message
64
+ * Order (first match wins):
65
+ * 1. `timedOut` (spawnSync ETIMEDOUT)budget timeout diagnosis
66
+ * 2. buffer overflow (ENOBUFS / ERR_CHILD_PROCESS_STDIO_MAXBUFFER) buffer-limit diagnosis
67
+ * 3. non-null `signal` → interrupt diagnosis (names the signal)
68
+ * 4. otherwise → hook/git stderr when present, else spawn errorCode / generic preserved-index
47
69
  *
48
- * Never lead with bare hook "passed" lines for timeout/interrupt paths.
70
+ * Never lead with bare hook "passed" lines for first-class failure paths.
49
71
  */
50
72
  export declare function classifyCommitFailure(result: {
51
73
  readonly timedOut: boolean;
52
74
  readonly signal: NodeJS.Signals | null;
53
75
  readonly stderr: string;
54
76
  readonly stdout: string;
55
- }, timeoutMs: number): string;
77
+ readonly errorCode?: string | null;
78
+ }, timeoutMs: number, maxBufferBytes?: number): string;
56
79
  /**
57
80
  * Prefix every line of a setup failure so multi-line timeout diagnoses stay
58
81
  * labeled when printed via console.error (avoids a single `wp setup: ` on the
@@ -14,25 +14,44 @@ export const GIT_PROBE_TIMEOUT_MS = 15_000;
14
14
  * raise/lower via `WP_SETUP_COMMIT_TIMEOUT_MS` or `AuthorCommitOptions.commitTimeoutMs`.
15
15
  */
16
16
  export const DEFAULT_GIT_COMMIT_TIMEOUT_MS = 600_000;
17
- const GIT_MAX_BUFFER = 1024 * 1024;
18
- function git(repoRoot, args, timeoutMs) {
17
+ /** Max stdout+stderr retained from probe git subprocesses (Node spawnSync maxBuffer). */
18
+ export const GIT_MAX_BUFFER = 1024 * 1024;
19
+ /**
20
+ * Max stdout+stderr for the single hookful `git commit`. Quiet hooks cost nothing
21
+ * (Node does not preallocate); chatty consumer format+audit chains can exceed 1 MiB
22
+ * and would otherwise die with ENOBUFS+SIGTERM mid-commit.
23
+ */
24
+ export const GIT_COMMIT_MAX_BUFFER = 16 * 1024 * 1024;
25
+ /** Hook output snippet size kept in first-class failure diagnoses (tail, not head). */
26
+ const HOOK_OUTPUT_SNIPPET_CHARS = 800;
27
+ /** spawnSync error codes that mean maxBuffer / stdio overflow killed the child. */
28
+ const BUFFER_OVERFLOW_ERROR_CODES = new Set(["ENOBUFS", "ERR_CHILD_PROCESS_STDIO_MAXBUFFER"]);
29
+ function git(repoRoot, args, timeoutMs, maxBuffer = GIT_MAX_BUFFER) {
19
30
  const result = spawnSync("git", [...args], {
20
31
  cwd: repoRoot,
21
32
  encoding: "utf8",
22
33
  // Node: timeout > 0 arms SIGTERM; 0 / omitted means no artificial wall.
23
34
  ...(timeoutMs > 0 ? { timeout: timeoutMs } : {}),
24
- maxBuffer: GIT_MAX_BUFFER,
35
+ maxBuffer,
25
36
  stdio: ["ignore", "pipe", "pipe"],
26
37
  });
27
- const timedOut = result.error !== undefined && "code" in result.error && result.error.code === "ETIMEDOUT";
38
+ const errorCode = result.error !== undefined && "code" in result.error && result.error.code != null
39
+ ? String(result.error.code)
40
+ : null;
41
+ const timedOut = errorCode === "ETIMEDOUT";
28
42
  return {
29
43
  ok: result.status === 0 && !result.error,
30
44
  stdout: (result.stdout ?? "").trim(),
31
45
  stderr: (result.stderr ?? "").trim(),
32
46
  timedOut,
33
47
  signal: result.signal,
48
+ errorCode,
34
49
  };
35
50
  }
51
+ /** True when spawnSync killed the child for maxBuffer / stdio overflow. */
52
+ export function isBufferOverflowErrorCode(code) {
53
+ return typeof code === "string" && BUFFER_OVERFLOW_ERROR_CODES.has(code);
54
+ }
36
55
  /**
37
56
  * Resolve the hookful-commit timeout. Preference order:
38
57
  * 1. explicit `commitTimeoutMs` option (tests / programmatic callers)
@@ -60,7 +79,10 @@ function appendHookOutput(diagnosis, hookOutput) {
60
79
  const trimmed = hookOutput?.trim();
61
80
  if (!trimmed)
62
81
  return diagnosis;
63
- const snippet = trimmed.length > 400 ? `${trimmed.slice(0, 400)}…` : trimmed;
82
+ // Keep the tail: failing audits / last lines at kill time matter more than husky banner.
83
+ const snippet = trimmed.length > HOOK_OUTPUT_SNIPPET_CHARS
84
+ ? `…${trimmed.slice(-HOOK_OUTPUT_SNIPPET_CHARS)}`
85
+ : trimmed;
64
86
  return `${diagnosis}\n--- hook output (truncated) ---\n${snippet}`;
65
87
  }
66
88
  /** First-class timeout diagnosis; partial hook stdout/stderr is diagnostic only. */
@@ -87,24 +109,57 @@ export function formatCommitInterruptError(options) {
87
109
  `Partial hook lines such as "format applied" / "audit … passed" are not success.`;
88
110
  return appendHookOutput(diagnosis, options.hookOutput);
89
111
  }
112
+ /**
113
+ * First-class diagnosis when Node spawnSync kills the child because hook/git
114
+ * output exceeded maxBuffer (ENOBUFS / ERR_CHILD_PROCESS_STDIO_MAXBUFFER).
115
+ * Distinct from budget timeout and from external signal kills.
116
+ */
117
+ export function formatCommitBufferError(options) {
118
+ const code = options.errorCode?.trim() || "ENOBUFS";
119
+ const diagnosis = `Convergence commit failed because git/hook output exceeded the spawnSync maxBuffer limit ` +
120
+ `(${options.maxBufferBytes} bytes; Node error ${code}), so no setup commit was authored. ` +
121
+ `This is a buffer/output-limit failure, not a time budget timeout and not an external kill. ` +
122
+ `Staged and worktree changes were preserved for remediation. ` +
123
+ `Reduce hook verbosity, raise the maxBuffer only with measured need, or fix chatty hooks. ` +
124
+ `Partial hook lines such as "format applied" / "audit … passed" are not success.`;
125
+ return appendHookOutput(diagnosis, options.hookOutput);
126
+ }
90
127
  /**
91
128
  * Classify a failed hookful `git commit` result.
92
129
  *
93
- * - `timedOut` (spawnSync ETIMEDOUT) → budget timeout diagnosis
94
- * - non-null `signal` without timedOutinterrupt diagnosis (names the signal)
95
- * - otherwise hook/git stderr when present, else a generic preserved-index message
130
+ * Order (first match wins):
131
+ * 1. `timedOut` (spawnSync ETIMEDOUT)budget timeout diagnosis
132
+ * 2. buffer overflow (ENOBUFS / ERR_CHILD_PROCESS_STDIO_MAXBUFFER) buffer-limit diagnosis
133
+ * 3. non-null `signal` → interrupt diagnosis (names the signal)
134
+ * 4. otherwise → hook/git stderr when present, else spawn errorCode / generic preserved-index
96
135
  *
97
- * Never lead with bare hook "passed" lines for timeout/interrupt paths.
136
+ * Never lead with bare hook "passed" lines for first-class failure paths.
98
137
  */
99
- export function classifyCommitFailure(result, timeoutMs) {
138
+ export function classifyCommitFailure(result, timeoutMs, maxBufferBytes = GIT_COMMIT_MAX_BUFFER) {
100
139
  const hookOutput = [result.stderr, result.stdout].filter(Boolean).join("\n");
101
140
  if (result.timedOut) {
102
141
  return formatCommitTimeoutError({ timeoutMs, hookOutput });
103
142
  }
143
+ if (isBufferOverflowErrorCode(result.errorCode)) {
144
+ return formatCommitBufferError({
145
+ maxBufferBytes,
146
+ errorCode: result.errorCode,
147
+ hookOutput,
148
+ });
149
+ }
104
150
  if (result.signal) {
105
151
  return formatCommitInterruptError({ signal: result.signal, hookOutput });
106
152
  }
107
- return hookOutput || "git commit failed; hook changes were preserved.";
153
+ if (hookOutput) {
154
+ if (result.errorCode) {
155
+ return `${hookOutput}\n(spawn error ${result.errorCode})`;
156
+ }
157
+ return hookOutput;
158
+ }
159
+ if (result.errorCode) {
160
+ return `git commit failed (spawn error ${result.errorCode}); hook changes were preserved.`;
161
+ }
162
+ return "git commit failed; hook changes were preserved.";
108
163
  }
109
164
  /**
110
165
  * Prefix every line of a setup failure so multi-line timeout diagnoses stay
@@ -230,13 +285,20 @@ export function authorConvergenceCommit(options) {
230
285
  const commitTimeoutMs = resolveCommitTimeoutMs({
231
286
  commitTimeoutMs: options.commitTimeoutMs,
232
287
  });
233
- const commit = git(options.repoRoot, ["commit", "-m", options.message], commitTimeoutMs);
288
+ const budgetLabel = commitTimeoutMs > 0
289
+ ? commitTimeoutMs >= 60_000
290
+ ? `up to ${Math.round(commitTimeoutMs / 60_000)}m`
291
+ : `up to ${commitTimeoutMs}ms`
292
+ : "no artificial time wall";
293
+ // stderr: setup success path stays machine-friendly on stdout; hooks can take minutes.
294
+ console.error(`wp setup: running repository pre-commit hooks (${budgetLabel})…`);
295
+ const commit = git(options.repoRoot, ["commit", "-m", options.message], commitTimeoutMs, GIT_COMMIT_MAX_BUFFER);
234
296
  if (!commit.ok) {
235
297
  return {
236
298
  status: "failed",
237
299
  ok: false,
238
300
  allowlist,
239
- error: classifyCommitFailure(commit, commitTimeoutMs),
301
+ error: classifyCommitFailure(commit, commitTimeoutMs, GIT_COMMIT_MAX_BUFFER),
240
302
  };
241
303
  }
242
304
  const committedHead = probe(["rev-parse", "--verify", "HEAD"]);
@@ -71,6 +71,17 @@ export interface OpencodeProbeRunnerInput {
71
71
  }
72
72
  export type OpencodeProbeRunner = (input: OpencodeProbeRunnerInput) => Promise<OpencodeProbeResult>;
73
73
  export declare function isOpencodeProbeEnabled(env?: NodeJS.ProcessEnv): boolean;
74
+ /**
75
+ * Spawn a bounded `opencode run` against one account's isolated credential
76
+ * store and classify the first conclusive thing it says.
77
+ *
78
+ * There is no OpenCode Go HTTP endpoint anywhere in this codebase — the review
79
+ * adapter also reaches the provider only by spawning this binary — so a
80
+ * subprocess is the only available probe vehicle. Per-account isolation is the
81
+ * same `XDG_DATA_HOME` trick the review adapter uses, so a probe can never race
82
+ * a live interactive TUI's global `auth.json`.
83
+ */
84
+ export declare function spawnOpencodeProbe(input: OpencodeProbeRunnerInput): Promise<OpencodeProbeResult>;
74
85
  export interface ProbeOpencodeAccountDeps {
75
86
  readonly run?: OpencodeProbeRunner;
76
87
  readonly materialize?: (key: string, dataDir: string) => void;
@@ -157,7 +157,7 @@ function classifyChunk(captured) {
157
157
  * same `XDG_DATA_HOME` trick the review adapter uses, so a probe can never race
158
158
  * a live interactive TUI's global `auth.json`.
159
159
  */
160
- function spawnOpencodeProbe(input) {
160
+ export function spawnOpencodeProbe(input) {
161
161
  return new Promise((resolve) => {
162
162
  let probeCwd;
163
163
  try {
@@ -145,9 +145,13 @@ function renderSummaryLines(summary, transformed, entry, passed) {
145
145
  else if (!passed && transformed.rawOutput?.trim()) {
146
146
  lines.push(transformed.rawOutput.trimEnd());
147
147
  }
148
- if (!passed || transformed.truncated) {
148
+ // Any elided bytes must remain retrievable: fail, clip, OR compaction that
149
+ // recorded an elision without setting truncated. Hide Full log / retrieve
150
+ // hints only when the summary still contains the full content.
151
+ const hasElisions = (transformed.elisions?.length ?? 0) > 0;
152
+ if (!passed || transformed.truncated || hasElisions) {
149
153
  lines.push(`Full log: wp logs ${entry.command}`);
150
- if (transformed.elisions && transformed.elisions.length > 0) {
154
+ if (hasElisions) {
151
155
  lines.push(...transformed.elisions.map((elision) => `Retrieve elision: ${elision.retrieveTool} id=${elision.id}`));
152
156
  }
153
157
  }
@@ -24,7 +24,7 @@ export const TYPECHECK_COMMAND_HELP = [
24
24
  " wp typecheck --affected --branch # changed vs origin/${GITHUB_BASE_REF:-main}",
25
25
  " wp typecheck --pretty",
26
26
  " wp typecheck --tests # test-surface lane (tsconfig.test.json); required in",
27
- " # not part of the default gate or ci-preflight yet",
27
+ " # ci-preflight and CI (not the default untargeted gate)",
28
28
  ].join("\n");
29
29
  export function registerTypecheckCommand(cli, deps = {}) {
30
30
  addAffectedOptions(cli
@@ -55,6 +55,12 @@ export interface MergeCleanupDecisionInput {
55
55
  readonly repoDirty: boolean;
56
56
  readonly targetDirty: boolean;
57
57
  readonly stashPrimary?: boolean;
58
+ /**
59
+ * When target is dirty, optional porcelain -z + slug enrich the refusal message
60
+ * (scaffold-only vs other dirt). Omitted messages stay generic for unit tests.
61
+ */
62
+ readonly targetPorcelainZ?: string;
63
+ readonly targetBlueprintSlug?: string;
58
64
  }
59
65
  export interface NewWorktreeTargetInput {
60
66
  branch?: string;
@@ -97,4 +103,6 @@ export interface CreateManagedWorktreeResult {
97
103
  export declare function formatWorktreeList(entries: WorktreeEntry[], currentWorktreePath: string): string[];
98
104
  export declare function formatManagedWorktreeList(entries: readonly ManagedWorktreeEntry[]): string[];
99
105
  export declare function createManagedWorktree(opts?: CreateManagedWorktreeOptions): Promise<CreateManagedWorktreeResult>;
106
+ /** `.../blueprints/<slug>/owner` → slug */
107
+ export declare function inferBlueprintSlugFromOwnerPath(ownerPath: string): string | undefined;
100
108
  export declare function executeWorktreeSubcommand(subcommand: string, args: string[], opts: WorktreeCommandOptions): Promise<void>;
@@ -15,6 +15,7 @@ import { runWorktreeSetup, runWorktreeTeardown } from "#worktrees/lifecycle-scri
15
15
  import { hydrateWorktreeDependencies, reconcileWorktreeDependencies, } from "#worktrees/dependencies.js";
16
16
  import { projectWorktreeAgentSurfaces } from "#worktrees/project-agent-surfaces.js";
17
17
  import { assertNotLocalMainBranch } from "#worktrees/main-ownership.js";
18
+ import { formatTargetDirtyRefusal } from "#worktrees/owner-dirt.js";
18
19
  import { buildWorktreeInventory, canonicalizeWorktreePath, classifyMembership, isManagedWorktreePath, resolveRepoIdentity, } from "#worktrees/identity.js";
19
20
  import { findRegistryCandidatesByPath, planStaleWorktreeRegistryPrune, readWorktreeRegistry, removeWorktreeRegistryEntries, repoScopedPathPredicate, upsertWorktreeRegistryEntry, } from "#worktrees/registry.js";
20
21
  import { buildRegistryRepoRootMap, classifyOrphanCandidate, discoverOrphanScanCandidates, resolveRepoRootFromGitPointer, } from "#worktrees/orphan-scan.js";
@@ -182,6 +183,13 @@ export function decideMergeCleanup(input) {
182
183
  throw new Error(`wp worktree merge-cleanup refused: ${input.targetPath} is locked`);
183
184
  }
184
185
  if (input.targetDirty) {
186
+ if (input.targetPorcelainZ !== undefined) {
187
+ throw new Error(formatTargetDirtyRefusal({
188
+ targetPath: input.targetPath,
189
+ porcelainZ: input.targetPorcelainZ,
190
+ slug: input.targetBlueprintSlug,
191
+ }));
192
+ }
185
193
  throw new Error(`wp worktree merge-cleanup refused: ${input.targetPath} has uncommitted changes`);
186
194
  }
187
195
  if (input.repoDirty) {
@@ -561,6 +569,12 @@ function isDirty(path) {
561
569
  return true;
562
570
  return String(status.stdout ?? "").trim().length > 0;
563
571
  }
572
+ /** `.../blueprints/<slug>/owner` → slug */
573
+ export function inferBlueprintSlugFromOwnerPath(ownerPath) {
574
+ const normalized = ownerPath.replace(/\\/gu, "/");
575
+ const match = /\/blueprints\/([^/]+)\/owner\/?$/u.exec(normalized);
576
+ return match?.[1];
577
+ }
564
578
  function currentBranch(repoRoot) {
565
579
  const result = spawnSync("git", ["branch", "--show-current"], {
566
580
  cwd: repoRoot,
@@ -691,6 +705,14 @@ async function handleMergeCleanup(nameOrPath, opts) {
691
705
  const scopedRegistryEntries = findRegistryCandidatesByPath(resolved)
692
706
  .filter((candidate) => classifyMembership(candidate.entry.repoRoot, mergeAuthKey) === "same")
693
707
  .map((candidate) => candidate.entry);
708
+ const targetDirty = isDirty(resolved);
709
+ const targetPorcelainZ = targetDirty
710
+ ? (() => {
711
+ const status = spawnSync("git", ["status", "--porcelain=v1", "--untracked-files=all", "--ignored=no", "-z"], { cwd: resolved, encoding: "utf8" });
712
+ return status.status === 0 ? String(status.stdout ?? "") : "?? (git-status-failed)\0";
713
+ })()
714
+ : undefined;
715
+ const targetBlueprintSlug = inferBlueprintSlugFromOwnerPath(resolved);
694
716
  const plan = decideMergeCleanup({
695
717
  repoRoot,
696
718
  targetPath: resolved,
@@ -699,8 +721,10 @@ async function handleMergeCleanup(nameOrPath, opts) {
699
721
  registryEntries: scopedRegistryEntries,
700
722
  currentBranch: currentBranch(repoRoot),
701
723
  repoDirty: isDirty(repoRoot),
702
- targetDirty: isDirty(resolved),
724
+ targetDirty,
703
725
  stashPrimary: opts.stashPrimary === true,
726
+ ...(targetPorcelainZ !== undefined ? { targetPorcelainZ } : {}),
727
+ ...(targetBlueprintSlug !== undefined ? { targetBlueprintSlug } : {}),
704
728
  });
705
729
  if (opts.dryRun) {
706
730
  console.log("[dry-run] Would run merge cleanup:");
@@ -4,4 +4,10 @@ export interface ReviewTargetCheckout {
4
4
  readonly changes: () => readonly string[];
5
5
  readonly cleanup: () => void;
6
6
  }
7
+ declare function statusEntryPath(line: string): string;
7
8
  export declare function createReviewTargetCheckout(repositoryRoot: string, targetSha: string): ReviewTargetCheckout;
9
+ /** Test-only surface for porcelain path parsing. */
10
+ export declare const reviewCheckoutInternals: {
11
+ statusEntryPath: typeof statusEntryPath;
12
+ };
13
+ export {};
@@ -121,11 +121,33 @@ function isNormalizedPackageJsonModification(cwd, line) {
121
121
  // Single source of truth for turning a `git status --porcelain=v1` line into
122
122
  // its relative path, shared by status()'s filter predicate and
123
123
  // worktreeFingerprint()'s dirty-path hashing so both stay aligned by
124
- // construction. Does not special-case the rename arrow (`old -> new`); a
125
- // rename line's raw tail is used as-is, matching this function's prior
126
- // (pre-refactor) inline behavior.
124
+ // construction.
125
+ //
126
+ // Rename/copy lines use non-null porcelain form `XY ORIG -> DEST` (or quoted
127
+ // paths). The destination is the live path whose bytes must be fingerprinted;
128
+ // using the raw `ORIG -> DEST` tail makes lstat miss both sides and silently
129
+ // drops content from the hash (regression vs paired-record `-z` parsing).
127
130
  function statusEntryPath(line) {
128
- return line.slice(3).replace(/\/$/u, "");
131
+ const tail = line.slice(3).replace(/\/$/u, "");
132
+ const renameSep = " -> ";
133
+ const sep = tail.lastIndexOf(renameSep);
134
+ const pathPart = sep === -1 ? tail : tail.slice(sep + renameSep.length);
135
+ return unquotePorcelainPath(pathPart);
136
+ }
137
+ /** Strip git porcelain double-quotes and reverse C-style escapes for a path. */
138
+ function unquotePorcelainPath(pathPart) {
139
+ if (!pathPart.startsWith('"') || !pathPart.endsWith('"') || pathPart.length < 2) {
140
+ return pathPart;
141
+ }
142
+ return pathPart.slice(1, -1).replace(/\\([\\"ntr])/gu, (_m, ch) => {
143
+ if (ch === "n")
144
+ return "\n";
145
+ if (ch === "t")
146
+ return "\t";
147
+ if (ch === "r")
148
+ return "\r";
149
+ return ch;
150
+ });
129
151
  }
130
152
  function status(cwd, ignoredPaths = new Set()) {
131
153
  const output = git(cwd, ["status", "--porcelain=v1", "--untracked-files=all", "--ignored"]);
@@ -245,3 +267,7 @@ function projectCallerDependencies(repositoryRoot, checkoutRoot) {
245
267
  projected.add(dependencyPath);
246
268
  return projected;
247
269
  }
270
+ /** Test-only surface for porcelain path parsing. */
271
+ export const reviewCheckoutInternals = {
272
+ statusEntryPath,
273
+ };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Owner-worktree dirt classification for scaffold abandon + merge-cleanup hints.
3
+ *
4
+ * Machine-parse `git status --porcelain=v1 -z` only (NUL records; renames are
5
+ * destination then origin with no `->`). Do not use line-oriented porcelain for
6
+ * path classification — special characters and renames mis-parse.
7
+ *
8
+ * Parity with `isDirty`: ignored files are not considered (no `--ignored`).
9
+ */
10
+ export type OwnerDirtKind = "clean" | "scaffold-only" | "other";
11
+ export interface OwnerDirtClassification {
12
+ readonly kind: OwnerDirtKind;
13
+ /** Repo-relative paths contributing to dirt (bounded callers may slice). */
14
+ readonly paths: readonly string[];
15
+ /** Paths under the draft scaffold prefix, when slug is provided. */
16
+ readonly scaffoldPaths: readonly string[];
17
+ }
18
+ export interface PorcelainPathRecord {
19
+ readonly statusCode: string;
20
+ readonly paths: readonly string[];
21
+ }
22
+ /** Blueprint draft tree for a slug (no leading/trailing slash on slug). */
23
+ export declare function draftScaffoldPrefix(slug: string): string;
24
+ export declare function isUnderDraftScaffold(relativePath: string, slug: string): boolean;
25
+ /**
26
+ * Parse `git status --porcelain=v1 -z` stdout into path records.
27
+ * Rename/copy: first path is destination, second (next record) is origin.
28
+ */
29
+ export declare function parsePorcelainV1Z(output: string): readonly PorcelainPathRecord[];
30
+ /**
31
+ * Classify dirt relative to an optional draft slug.
32
+ * Without slug, any non-empty porcelain is `other` (or `clean` if empty).
33
+ */
34
+ export declare function classifyOwnerDirt(porcelainZ: string, slug?: string): OwnerDirtClassification;
35
+ export declare function formatBoundedPaths(paths: readonly string[], limit?: number): string;
36
+ export declare function formatTargetDirtyRefusal(input: {
37
+ readonly targetPath: string;
38
+ readonly porcelainZ: string;
39
+ readonly slug?: string;
40
+ }): string;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Owner-worktree dirt classification for scaffold abandon + merge-cleanup hints.
3
+ *
4
+ * Machine-parse `git status --porcelain=v1 -z` only (NUL records; renames are
5
+ * destination then origin with no `->`). Do not use line-oriented porcelain for
6
+ * path classification — special characters and renames mis-parse.
7
+ *
8
+ * Parity with `isDirty`: ignored files are not considered (no `--ignored`).
9
+ */
10
+ /** Blueprint draft tree for a slug (no leading/trailing slash on slug). */
11
+ export function draftScaffoldPrefix(slug) {
12
+ return `blueprints/draft/${slug}`;
13
+ }
14
+ export function isUnderDraftScaffold(relativePath, slug) {
15
+ const prefix = draftScaffoldPrefix(slug);
16
+ return relativePath === prefix || relativePath.startsWith(`${prefix}/`);
17
+ }
18
+ /**
19
+ * Parse `git status --porcelain=v1 -z` stdout into path records.
20
+ * Rename/copy: first path is destination, second (next record) is origin.
21
+ */
22
+ export function parsePorcelainV1Z(output) {
23
+ const records = output.split("\0").filter((record) => record.length > 0);
24
+ const parsed = [];
25
+ for (let index = 0; index < records.length; index += 1) {
26
+ const record = records[index];
27
+ if (record.length < 3)
28
+ continue;
29
+ const statusCode = record.slice(0, 2);
30
+ const firstPath = record.slice(3);
31
+ const paths = [firstPath];
32
+ if (/[RC]/u.test(statusCode)) {
33
+ const originalPath = records[index + 1];
34
+ if (originalPath !== undefined) {
35
+ paths.push(originalPath);
36
+ index += 1;
37
+ }
38
+ }
39
+ parsed.push({ statusCode, paths });
40
+ }
41
+ return parsed;
42
+ }
43
+ /**
44
+ * Classify dirt relative to an optional draft slug.
45
+ * Without slug, any non-empty porcelain is `other` (or `clean` if empty).
46
+ */
47
+ export function classifyOwnerDirt(porcelainZ, slug) {
48
+ const records = parsePorcelainV1Z(porcelainZ);
49
+ if (records.length === 0) {
50
+ return { kind: "clean", paths: [], scaffoldPaths: [] };
51
+ }
52
+ const paths = new Set();
53
+ const scaffoldPaths = new Set();
54
+ let allScaffold = slug !== undefined && slug.length > 0;
55
+ for (const record of records) {
56
+ for (const relativePath of record.paths) {
57
+ if (relativePath.length === 0)
58
+ continue;
59
+ paths.add(relativePath);
60
+ if (slug && isUnderDraftScaffold(relativePath, slug)) {
61
+ scaffoldPaths.add(relativePath);
62
+ }
63
+ else {
64
+ allScaffold = false;
65
+ }
66
+ }
67
+ }
68
+ const pathList = [...paths].toSorted();
69
+ const scaffoldList = [...scaffoldPaths].toSorted();
70
+ if (pathList.length === 0) {
71
+ return { kind: "clean", paths: [], scaffoldPaths: [] };
72
+ }
73
+ if (allScaffold && scaffoldList.length > 0) {
74
+ return { kind: "scaffold-only", paths: pathList, scaffoldPaths: scaffoldList };
75
+ }
76
+ return { kind: "other", paths: pathList, scaffoldPaths: scaffoldList };
77
+ }
78
+ export function formatBoundedPaths(paths, limit = 10) {
79
+ if (paths.length === 0)
80
+ return "(none)";
81
+ const head = paths.slice(0, limit);
82
+ const more = paths.length - head.length;
83
+ return more > 0 ? `${head.join("\n")}\n(and ${more} more)` : head.join("\n");
84
+ }
85
+ export function formatTargetDirtyRefusal(input) {
86
+ const classification = classifyOwnerDirt(input.porcelainZ, input.slug);
87
+ const pathBlock = formatBoundedPaths(classification.paths);
88
+ const header = `wp worktree merge-cleanup refused: ${input.targetPath} has uncommitted changes`;
89
+ if (classification.kind === "scaffold-only" && input.slug) {
90
+ return [
91
+ header,
92
+ "Owner has only uncommitted draft scaffold dirt:",
93
+ pathBlock,
94
+ `fix: wp blueprint abandon ${input.slug} --confirm`,
95
+ " or: commit the draft and land it with the PR",
96
+ ].join("\n");
97
+ }
98
+ return [
99
+ header,
100
+ "Paths:",
101
+ pathBlock,
102
+ `fix: commit or stash work in ${input.targetPath}, then re-run merge-cleanup`,
103
+ ].join("\n");
104
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpresso/agent-kit",
3
- "version": "3.3.0",
3
+ "version": "3.3.1",
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": [
@@ -507,16 +507,16 @@
507
507
  "setupWpActionRef": "c2c71a7a4be446fc6858e6b57bf55a11ccfa2d88"
508
508
  },
509
509
  "optionalDependencies": {
510
- "@webpresso/agent-kit-runtime-darwin-arm64": "3.3.0",
511
- "@webpresso/agent-kit-runtime-darwin-x64": "3.3.0",
512
- "@webpresso/agent-kit-runtime-linux-x64": "3.3.0",
513
- "@webpresso/agent-kit-runtime-linux-arm64": "3.3.0",
514
- "@webpresso/agent-kit-runtime-windows-x64": "3.3.0",
515
- "@webpresso/agent-kit-session-memory-darwin-x64": "3.3.0",
516
- "@webpresso/agent-kit-session-memory-darwin-arm64": "3.3.0",
517
- "@webpresso/agent-kit-session-memory-linux-x64": "3.3.0",
518
- "@webpresso/agent-kit-session-memory-linux-arm64": "3.3.0",
519
- "@webpresso/agent-kit-session-memory-win32-x64": "3.3.0",
520
- "@webpresso/agent-kit-session-memory-win32-arm64": "3.3.0"
510
+ "@webpresso/agent-kit-runtime-darwin-arm64": "3.3.1",
511
+ "@webpresso/agent-kit-runtime-darwin-x64": "3.3.1",
512
+ "@webpresso/agent-kit-runtime-linux-x64": "3.3.1",
513
+ "@webpresso/agent-kit-runtime-linux-arm64": "3.3.1",
514
+ "@webpresso/agent-kit-runtime-windows-x64": "3.3.1",
515
+ "@webpresso/agent-kit-session-memory-darwin-x64": "3.3.1",
516
+ "@webpresso/agent-kit-session-memory-darwin-arm64": "3.3.1",
517
+ "@webpresso/agent-kit-session-memory-linux-x64": "3.3.1",
518
+ "@webpresso/agent-kit-session-memory-linux-arm64": "3.3.1",
519
+ "@webpresso/agent-kit-session-memory-win32-x64": "3.3.1",
520
+ "@webpresso/agent-kit-session-memory-win32-arm64": "3.3.1"
521
521
  }
522
522
  }