@webpresso/agent-kit 3.1.25 → 3.1.26

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.
@@ -180,7 +180,11 @@ for the current repository and package configuration contract.
180
180
  `npm view <package> version`. For consumer setup smoke, use
181
181
  `vp install -g @webpresso/agent-kit@latest`, `wp setup`, and
182
182
  `wp sync --check`.
183
- 6. After the OIDC publish path succeeds, set npm package publishing access to
183
+ 6. If the Version Packages PR has no WP check / CI is `action_required` with
184
+ zero jobs, follow
185
+ [`docs/runbooks/version-packages-bot-ci.md`](../../../docs/runbooks/version-packages-bot-ci.md)
186
+ (`gh run rerun` or Approve workflows). Do not admin-merge without WP check.
187
+ 7. After the OIDC publish path succeeds, set npm package publishing access to
184
188
  require two-factor authentication and disallow tokens, then revoke obsolete
185
189
  automation tokens. Do not remove a credential that another verified release
186
190
  path still needs.
@@ -10,6 +10,12 @@ export interface CliLogEntry {
10
10
  readonly summary?: string;
11
11
  }
12
12
  export declare function isCliLogCommandName(value: string): value is CliLogCommandName;
13
+ /**
14
+ * Create a durable CLI log sink. When the preferred repo state surface is not
15
+ * writable (EPERM/EACCES/EROFS — common in Codex/read-only sandboxes), fall
16
+ * back to TMPDIR so `wp audit`/`wp test` still execute. The fallback is loud
17
+ * on stderr; it is not a silent swallow.
18
+ */
13
19
  export declare function createCliLogSink(command: CliLogCommandName, cwd?: string): CliLogSink;
14
20
  export interface CliLogSink {
15
21
  readonly command: CliLogCommandName;
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { closeSync, createWriteStream, mkdirSync, openSync, readdirSync, readFileSync, statSync, renameSync, utimesSync, rmSync, writeFileSync, } from "node:fs";
3
+ import { tmpdir } from "node:os";
3
4
  import { dirname, join } from "node:path";
4
5
  import { getSurfacePath } from "#paths/state-root.js";
5
6
  import { readTrustedJsonFile } from "#shared-utils/read-json-file.js";
@@ -18,9 +19,17 @@ const ORPHAN_LOG_GRACE_MS = 60_000;
18
19
  export function isCliLogCommandName(value) {
19
20
  return CLI_LOG_COMMANDS.includes(value);
20
21
  }
21
- export function createCliLogSink(command, cwd = process.cwd()) {
22
+ function isUnwritableFsError(error) {
23
+ if (typeof error !== "object" || error === null || !("code" in error))
24
+ return false;
25
+ const code = error.code;
26
+ return code === "EPERM" || code === "EACCES" || code === "EROFS";
27
+ }
28
+ /**
29
+ * Open a CLI log sink in an explicit directory (preferred state surface or fallback).
30
+ */
31
+ function openCliLogSink(commandDir, command, cwd) {
22
32
  const id = createLogId();
23
- const commandDir = getCommandLogDir(command, cwd);
24
33
  mkdirSync(commandDir, { recursive: true });
25
34
  const absoluteLogPath = join(commandDir, `${id}.log`);
26
35
  const activeMarkerPath = getActiveMarkerPath(absoluteLogPath);
@@ -80,7 +89,8 @@ export function createCliLogSink(command, cwd = process.cwd()) {
80
89
  };
81
90
  try {
82
91
  markLogRecentlyFinalized(absoluteLogPath);
83
- writeLogEntry(entry, cwd);
92
+ // Index/lock live next to the log file so fallback dirs stay consistent.
93
+ writeLogEntryInDir(entry, commandDir);
84
94
  }
85
95
  finally {
86
96
  rmSync(activeMarkerPath, { force: true });
@@ -89,6 +99,28 @@ export function createCliLogSink(command, cwd = process.cwd()) {
89
99
  },
90
100
  };
91
101
  }
102
+ /**
103
+ * Create a durable CLI log sink. When the preferred repo state surface is not
104
+ * writable (EPERM/EACCES/EROFS — common in Codex/read-only sandboxes), fall
105
+ * back to TMPDIR so `wp audit`/`wp test` still execute. The fallback is loud
106
+ * on stderr; it is not a silent swallow.
107
+ */
108
+ export function createCliLogSink(command, cwd = process.cwd()) {
109
+ const preferredDir = getCommandLogDir(command, cwd);
110
+ try {
111
+ return openCliLogSink(preferredDir, command, cwd);
112
+ }
113
+ catch (error) {
114
+ if (!isUnwritableFsError(error))
115
+ throw error;
116
+ const fallbackDir = join(tmpdir(), "webpresso", "cli-logs", command);
117
+ const code = typeof error === "object" && error !== null && "code" in error
118
+ ? String(error.code)
119
+ : "unknown";
120
+ process.stderr.write(`wp: CLI log directory not writable (${preferredDir}): ${code}; using ${fallbackDir}\n`);
121
+ return openCliLogSink(fallbackDir, command, cwd);
122
+ }
123
+ }
92
124
  export function readCliLogEntries(command, cwd = process.cwd()) {
93
125
  const indexPath = getCommandIndexPath(command, cwd);
94
126
  try {
@@ -104,11 +136,21 @@ export function readCliLogEntry(command, ordinal = 1, cwd = process.cwd()) {
104
136
  return;
105
137
  return readCliLogEntries(command, cwd)[ordinal - 1];
106
138
  }
107
- function writeLogEntry(entry, cwd) {
108
- withCommandIndexLock(entry.command, cwd, () => {
109
- const indexPath = getCommandIndexPath(entry.command, cwd);
139
+ function readCliLogEntriesFromDir(commandDir) {
140
+ const indexPath = join(commandDir, "index.json");
141
+ try {
142
+ const parsed = readTrustedJsonFile(indexPath);
143
+ return Array.isArray(parsed.entries) ? parsed.entries : [];
144
+ }
145
+ catch {
146
+ return [];
147
+ }
148
+ }
149
+ function writeLogEntryInDir(entry, commandDir) {
150
+ withCommandIndexLockInDir(commandDir, () => {
151
+ const indexPath = join(commandDir, "index.json");
110
152
  mkdirSync(dirname(indexPath), { recursive: true });
111
- const currentEntries = readCliLogEntries(entry.command, cwd).filter((current) => current.id !== entry.id);
153
+ const currentEntries = readCliLogEntriesFromDir(commandDir).filter((current) => current.id !== entry.id);
112
154
  const nextEntries = [entry, ...currentEntries].slice(0, 10);
113
155
  const retainedLogPaths = new Set(nextEntries.map((item) => item.logPath));
114
156
  for (const removed of currentEntries.slice(9)) {
@@ -116,7 +158,7 @@ function writeLogEntry(entry, cwd) {
116
158
  rmSync(removed.logPath, { force: true });
117
159
  }
118
160
  }
119
- pruneInactiveOrphanedLogFiles(entry.command, retainedLogPaths, cwd);
161
+ pruneInactiveOrphanedLogFilesInDir(commandDir, retainedLogPaths);
120
162
  const index = {
121
163
  version: 1,
122
164
  command: entry.command,
@@ -125,8 +167,7 @@ function writeLogEntry(entry, cwd) {
125
167
  writeIndexAtomically(indexPath, index);
126
168
  });
127
169
  }
128
- function withCommandIndexLock(command, cwd, fn) {
129
- const commandDir = getCommandLogDir(command, cwd);
170
+ function withCommandIndexLockInDir(commandDir, fn) {
130
171
  mkdirSync(commandDir, { recursive: true });
131
172
  const lockPath = join(commandDir, "index.lock");
132
173
  const started = Date.now();
@@ -157,13 +198,12 @@ function writeIndexAtomically(indexPath, index) {
157
198
  writeJsonFile(tmpPath, index);
158
199
  renameSync(tmpPath, indexPath);
159
200
  }
160
- function pruneInactiveOrphanedLogFiles(command, retainedLogPaths, cwd) {
161
- const directory = getCommandLogDir(command, cwd);
162
- mkdirSync(directory, { recursive: true });
163
- for (const file of readdirSync(directory)) {
201
+ function pruneInactiveOrphanedLogFilesInDir(commandDir, retainedLogPaths) {
202
+ mkdirSync(commandDir, { recursive: true });
203
+ for (const file of readdirSync(commandDir)) {
164
204
  if (!file.endsWith(".log"))
165
205
  continue;
166
- const absolutePath = join(directory, file);
206
+ const absolutePath = join(commandDir, file);
167
207
  if (!retainedLogPaths.has(absolutePath) && canPruneLogPath(absolutePath)) {
168
208
  rmSync(absolutePath, { force: true });
169
209
  }
@@ -86,11 +86,27 @@ export interface SelfInstallGuardDeps {
86
86
  */
87
87
  export declare function runSelfInstallGuard(rawArgs: readonly string[], deps?: SelfInstallGuardDeps): Promise<number>;
88
88
  /**
89
- * How wp re-invokes itself: the bun-compiled binary IS process.execPath (its
90
- * argv[1] is a virtual /$bunfs path), while the source/dev lane runs
91
- * `<runtime> <script>`.
89
+ * How wp re-invokes itself:
90
+ * - Prefer a **stable** global launcher (`~/.vite-plus/bin/wp` or PATH `wp`
91
+ * outside an installId package tree). Re-execing `node …/agent-kit#id/…/cli.js`
92
+ * dies after `vp update -g` deletes the pre-swap tree mid-guard.
93
+ * - Bun-compiled binary: process.execPath IS the binary (argv[1] is /$bunfs).
94
+ * - Source/dev lane without a stable launcher: `<runtime> <script>`.
92
95
  */
93
96
  export declare function buildSelfInvocationCommand(args: readonly string[], options?: {
94
97
  readonly execPath?: string;
95
98
  readonly argv1?: string;
99
+ /** Injectable stable launcher for tests; `null` forces legacy argv1 path. */
100
+ readonly stableWp?: string | null;
96
101
  }): string[];
102
+ /**
103
+ * A launcher that survives installId package-tree deletion during global update.
104
+ * Rejects paths under `agent-kit#<uuid>/` (versioned store payload).
105
+ */
106
+ export declare function resolveStableWpLauncher(options?: {
107
+ readonly home?: string;
108
+ readonly pathEnv?: string;
109
+ readonly existsSync?: (path: string) => boolean;
110
+ }): string | null;
111
+ /** True when path lives inside a vite-plus installId package tree. */
112
+ export declare function isVersionedAgentKitPayloadPath(filePath: string): boolean;
@@ -45,6 +45,8 @@
45
45
  * resource-side fencing.
46
46
  */
47
47
  import { spawn } from "node:child_process";
48
+ import { existsSync } from "node:fs";
49
+ import path from "node:path";
48
50
  import { buildVpGlobalInstallCommand, resolveBundledVpCommand, } from "#cli/auto-update/detect-pm.js";
49
51
  import { INSTALL_LOCK_HEARTBEAT_MS, claimInstallLock, refreshInstallLock, releaseInstallLock, resolveInstallLockPath, } from "#cli/auto-update/install-lock.js";
50
52
  import { probeGlobalWpResolvable } from "#cli/commands/init/package-root.js";
@@ -146,17 +148,55 @@ function extractChildCommand(rawArgs) {
146
148
  return rawArgs.slice(separator + 1);
147
149
  }
148
150
  /**
149
- * How wp re-invokes itself: the bun-compiled binary IS process.execPath (its
150
- * argv[1] is a virtual /$bunfs path), while the source/dev lane runs
151
- * `<runtime> <script>`.
151
+ * How wp re-invokes itself:
152
+ * - Prefer a **stable** global launcher (`~/.vite-plus/bin/wp` or PATH `wp`
153
+ * outside an installId package tree). Re-execing `node …/agent-kit#id/…/cli.js`
154
+ * dies after `vp update -g` deletes the pre-swap tree mid-guard.
155
+ * - Bun-compiled binary: process.execPath IS the binary (argv[1] is /$bunfs).
156
+ * - Source/dev lane without a stable launcher: `<runtime> <script>`.
152
157
  */
153
158
  export function buildSelfInvocationCommand(args, options = {}) {
159
+ const stableWp = options.stableWp !== undefined ? options.stableWp : resolveStableWpLauncher();
160
+ if (typeof stableWp === "string" && stableWp.length > 0) {
161
+ return [stableWp, ...args];
162
+ }
154
163
  const execPath = options.execPath ?? process.execPath;
155
164
  const argv1 = options.argv1 ?? process.argv[1] ?? "";
156
165
  if (argv1.startsWith("/$bunfs"))
157
166
  return [execPath, ...args];
158
167
  return [execPath, argv1, ...args];
159
168
  }
169
+ /**
170
+ * A launcher that survives installId package-tree deletion during global update.
171
+ * Rejects paths under `agent-kit#<uuid>/` (versioned store payload).
172
+ */
173
+ export function resolveStableWpLauncher(options = {}) {
174
+ const exists = options.existsSync ?? existsSync;
175
+ const home = options.home ?? process.env.HOME ?? process.env.USERPROFILE ?? "";
176
+ const candidates = [];
177
+ if (home) {
178
+ candidates.push(path.join(home, ".vite-plus", "bin", "wp"));
179
+ }
180
+ const pathEnv = options.pathEnv ?? process.env.PATH ?? "";
181
+ for (const dir of pathEnv.split(path.delimiter)) {
182
+ if (!dir)
183
+ continue;
184
+ candidates.push(path.join(dir, "wp"));
185
+ }
186
+ for (const candidate of candidates) {
187
+ if (!exists(candidate))
188
+ continue;
189
+ if (isVersionedAgentKitPayloadPath(candidate))
190
+ continue;
191
+ return candidate;
192
+ }
193
+ return null;
194
+ }
195
+ /** True when path lives inside a vite-plus installId package tree. */
196
+ export function isVersionedAgentKitPayloadPath(filePath) {
197
+ const normalized = filePath.replaceAll("\\", "/");
198
+ return /\/@webpresso\/agent-kit#[^/]+\//u.test(normalized);
199
+ }
160
200
  function defaultSpawnChild(command) {
161
201
  const [executable, ...args] = command;
162
202
  // detached: own process group, so lost-ownership kills reap descendants.
@@ -1,7 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { execFileSync } from "node:child_process";
3
- import { existsSync, mkdirSync, realpathSync } from "node:fs";
3
+ import { existsSync, mkdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { readFile, realpath, stat } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
5
6
  import { dirname, join } from "node:path";
6
7
  import envPaths from "env-paths";
7
8
  import lockfile from "proper-lockfile";
@@ -67,18 +68,44 @@ function normalizeCwdForCache(cwd) {
67
68
  function sha256Hex(value) {
68
69
  return createHash("sha256").update(value).digest("hex");
69
70
  }
71
+ /**
72
+ * Probe whether a directory can hold state files.
73
+ * Used so quality CLIs (wp audit/test/…) keep working in sandboxes that
74
+ * deny writes under Application Support (e.g. Codex outside-voice).
75
+ */
76
+ function canWriteStateRoot(root) {
77
+ try {
78
+ mkdirSync(root, { recursive: true });
79
+ const probe = join(root, `.wp-write-probe-${process.pid}`);
80
+ writeFileSync(probe, "ok");
81
+ rmSync(probe, { force: true });
82
+ return true;
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ }
70
88
  export function getStateRoot() {
71
89
  if (cachedStateRoot !== null)
72
90
  return cachedStateRoot;
73
91
  const override = process.env.WP_STATE_ROOT;
74
92
  if (override && override.trim().length > 0) {
93
+ // Explicit override is authoritative — fail later if unwritable rather than
94
+ // silently relocating the user's chosen root.
75
95
  cachedStateRoot = override;
76
96
  return override;
77
97
  }
78
98
  const paths = envPaths("webpresso", { suffix: "" });
79
99
  const root = paths.data;
80
- cachedStateRoot = root;
81
- return root;
100
+ if (canWriteStateRoot(root)) {
101
+ cachedStateRoot = root;
102
+ return root;
103
+ }
104
+ const fallback = join(tmpdir(), "webpresso-state");
105
+ mkdirSync(fallback, { recursive: true });
106
+ process.stderr.write(`wp: state root not writable (${root}); using ${fallback}\n`);
107
+ cachedStateRoot = fallback;
108
+ return fallback;
82
109
  }
83
110
  export function getRepoKey() {
84
111
  if (cachedRepoKey !== null)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpresso/agent-kit",
3
- "version": "3.1.25",
3
+ "version": "3.1.26",
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": [
@@ -499,16 +499,16 @@
499
499
  "setupWpActionRef": "c2c71a7a4be446fc6858e6b57bf55a11ccfa2d88"
500
500
  },
501
501
  "optionalDependencies": {
502
- "@webpresso/agent-kit-runtime-darwin-arm64": "3.1.25",
503
- "@webpresso/agent-kit-runtime-darwin-x64": "3.1.25",
504
- "@webpresso/agent-kit-runtime-linux-x64": "3.1.25",
505
- "@webpresso/agent-kit-runtime-linux-arm64": "3.1.25",
506
- "@webpresso/agent-kit-runtime-windows-x64": "3.1.25",
507
- "@webpresso/agent-kit-session-memory-darwin-x64": "3.1.25",
508
- "@webpresso/agent-kit-session-memory-darwin-arm64": "3.1.25",
509
- "@webpresso/agent-kit-session-memory-linux-x64": "3.1.25",
510
- "@webpresso/agent-kit-session-memory-linux-arm64": "3.1.25",
511
- "@webpresso/agent-kit-session-memory-win32-x64": "3.1.25",
512
- "@webpresso/agent-kit-session-memory-win32-arm64": "3.1.25"
502
+ "@webpresso/agent-kit-runtime-darwin-arm64": "3.1.26",
503
+ "@webpresso/agent-kit-runtime-darwin-x64": "3.1.26",
504
+ "@webpresso/agent-kit-runtime-linux-x64": "3.1.26",
505
+ "@webpresso/agent-kit-runtime-linux-arm64": "3.1.26",
506
+ "@webpresso/agent-kit-runtime-windows-x64": "3.1.26",
507
+ "@webpresso/agent-kit-session-memory-darwin-x64": "3.1.26",
508
+ "@webpresso/agent-kit-session-memory-darwin-arm64": "3.1.26",
509
+ "@webpresso/agent-kit-session-memory-linux-x64": "3.1.26",
510
+ "@webpresso/agent-kit-session-memory-linux-arm64": "3.1.26",
511
+ "@webpresso/agent-kit-session-memory-win32-x64": "3.1.26",
512
+ "@webpresso/agent-kit-session-memory-win32-arm64": "3.1.26"
513
513
  }
514
514
  }