@skydiveai/pi-extensions 0.1.0-beta.8 → 0.1.0-beta.802

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.
Files changed (2) hide show
  1. package/dist/index.mjs +346 -96
  2. package/package.json +2 -7
package/dist/index.mjs CHANGED
@@ -21,6 +21,10 @@ import { BatchSpanProcessor, NodeTracerProvider } from "@opentelemetry/sdk-trace
21
21
  import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
22
22
  import { hc } from "hono/client";
23
23
  import { parse } from "yaml";
24
+ import { execFile } from "node:child_process";
25
+ import { availableParallelism } from "node:os";
26
+ import { promisify } from "node:util";
27
+ import { quote } from "shell-quote";
24
28
  import { createWriteStream } from "node:fs";
25
29
  import { finished } from "node:stream/promises";
26
30
  import { createLocalBashOperations } from "@earendil-works/pi-coding-agent";
@@ -255,7 +259,7 @@ function createHealthHandler({ metadata }) {
255
259
  * read on the hot path before every LLM call), it falls back to the default
256
260
  * for that knob and logs once.
257
261
  */
258
- const log$13 = logger.child({ module: "context-management-config" });
262
+ const log$14 = logger.child({ module: "context-management-config" });
259
263
  const DEFAULT_CONTEXT_MANAGEMENT_CONFIG = {
260
264
  enabled: false,
261
265
  perResultMaxBytes: 16 * 1024,
@@ -303,7 +307,7 @@ function resolveContextManagementConfig(env = process.env) {
303
307
  maxModelCallsPerTurn: env.SKYDIVE_CTX_MAX_MODEL_CALLS
304
308
  });
305
309
  if (!parsed.success) {
306
- log$13.warn({
310
+ log$14.warn({
307
311
  event: "context_management_config_invalid",
308
312
  err: parsed.error
309
313
  }, "falling back to default context-management config");
@@ -437,7 +441,7 @@ const CAPABILITY_SOUL_NUDGE = "New capability gained — once the current task i
437
441
  * or `ToolDefinition[]`. Files starting with `_` or `.` are skipped, so
438
442
  * `tools/_example.ts` documents the shape without registering.
439
443
  */
440
- const log$12 = logger.child({ module: "local-tools-extension" });
444
+ const log$13 = logger.child({ module: "local-tools-extension" });
441
445
  const TOOLS_DIRNAME = "tools";
442
446
  const fileState = /* @__PURE__ */ new Map();
443
447
  let pendingLocalToolsUpdate = null;
@@ -581,7 +585,7 @@ async function reconcileAndQueue({ pi, dir, reason }) {
581
585
  dir
582
586
  });
583
587
  if (reason !== "session_start" && summaryHasChanges$1(summary)) pendingLocalToolsUpdate = summary;
584
- log$12.info({
588
+ log$13.info({
585
589
  event: "local_tools_reconcile",
586
590
  reason,
587
591
  total_tools: summary.totalTools,
@@ -603,7 +607,7 @@ const localToolsExtension = (pi) => {
603
607
  reason: "session_start"
604
608
  });
605
609
  } catch (err) {
606
- log$12.error({
610
+ log$13.error({
607
611
  err,
608
612
  event: "local_tools_reconcile_failed"
609
613
  }, "local tools reconcile failed");
@@ -615,7 +619,7 @@ const localToolsExtension = (pi) => {
615
619
  try {
616
620
  current = await listToolFiles(dir);
617
621
  } catch (err) {
618
- log$12.warn({
622
+ log$13.warn({
619
623
  err,
620
624
  event: "local_tools_listing_failed"
621
625
  }, "tools/ listing failed");
@@ -637,7 +641,7 @@ const localToolsExtension = (pi) => {
637
641
  reason: "auto_reload"
638
642
  });
639
643
  } catch (err) {
640
- log$12.error({
644
+ log$13.error({
641
645
  err,
642
646
  event: "local_tools_auto_reload_failed"
643
647
  }, "auto-reload after tools/ change failed");
@@ -862,7 +866,7 @@ async function loadMcpConfig(path) {
862
866
  * Clients are keyed by JSON-stringified config and reused across
863
867
  * reloads — only changed configs reconnect.
864
868
  */
865
- const log$11 = logger.child({ module: "mcp-extension" });
869
+ const log$12 = logger.child({ module: "mcp-extension" });
866
870
  async function closeConnected(connected) {
867
871
  try {
868
872
  await connected.client.close();
@@ -1226,7 +1230,7 @@ var McpExtension = class {
1226
1230
  });
1227
1231
  this.lastConfigMtimeMs = await readConfigMtimeMs(configPath);
1228
1232
  if (reason !== "session_start" && summaryHasChanges(summary)) this.pendingMcpUpdate = summary;
1229
- log$11.info({
1233
+ log$12.info({
1230
1234
  event: "mcp_reconcile",
1231
1235
  reason,
1232
1236
  total_tools: summary.totalTools,
@@ -1249,7 +1253,7 @@ var McpExtension = class {
1249
1253
  reason: "session_start"
1250
1254
  });
1251
1255
  } catch (err) {
1252
- log$11.error({
1256
+ log$12.error({
1253
1257
  err,
1254
1258
  event: "mcp_reconcile_failed"
1255
1259
  }, "MCP reconcile failed");
@@ -1261,7 +1265,7 @@ var McpExtension = class {
1261
1265
  try {
1262
1266
  mtime = await readConfigMtimeMs(configPath);
1263
1267
  } catch (err) {
1264
- log$11.warn({
1268
+ log$12.warn({
1265
1269
  err,
1266
1270
  event: "mcp_mtime_check_failed"
1267
1271
  }, "mtime check on mcp.config.json failed");
@@ -1275,7 +1279,7 @@ var McpExtension = class {
1275
1279
  reason: "auto_reload"
1276
1280
  });
1277
1281
  } catch (err) {
1278
- log$11.error({
1282
+ log$12.error({
1279
1283
  err,
1280
1284
  event: "mcp_auto_reload_failed"
1281
1285
  }, "auto-reload after mcp.config.json change failed");
@@ -1683,15 +1687,15 @@ const HEARTBEAT_THROTTLE_MS = 6e4;
1683
1687
  const TOOL_HEARTBEAT_INTERVAL_MS = 5e3;
1684
1688
  const MAX_TOOL_HEARTBEATS = 1440 * 60 * 1e3 / TOOL_HEARTBEAT_INTERVAL_MS;
1685
1689
  const DAEMON_URL = "http://localhost:38994";
1686
- const log$10 = logger.child({ module: "platform-ext" });
1690
+ const log$11 = logger.child({ module: "platform-ext" });
1687
1691
  function sandboxClient() {
1688
1692
  const apiUrl = apiBaseUrl();
1689
1693
  if (!apiUrl) return null;
1690
1694
  return hc(`${apiUrl}/api/v1/sandbox`);
1691
1695
  }
1692
1696
  /**
1693
- * Fetch every harness feature flag in one GET (`{ contextManagement, subagent,
1694
- * ... }` — see apps/anyone/api/src/routes/sandbox-feature-flags.ts). Returns
1697
+ * Fetch every harness feature flag in one GET (`{ contextManagement, ... }`
1698
+ * — see apps/anyone/api/src/routes/sandbox-feature-flags.ts). Returns
1695
1699
  * null when indeterminate (no api url, or the request failed) so the shared
1696
1700
  * poller keeps the last-known values rather than flipping on a transient error.
1697
1701
  * This is the single fetch behind `feature-flags-poll.ts`; extensions read the
@@ -1703,19 +1707,15 @@ async function fetchHarnessFlags() {
1703
1707
  try {
1704
1708
  const res = await client["feature-flags"].$get();
1705
1709
  if (!res.ok) {
1706
- log$10.debug({
1710
+ log$11.debug({
1707
1711
  status: res.status,
1708
1712
  event: "feature_flags_fetch_failed"
1709
1713
  }, "feature-flags fetch failed");
1710
1714
  return null;
1711
1715
  }
1712
- const body = await res.json();
1713
- return {
1714
- contextManagement: body.contextManagement ?? null,
1715
- subagent: body.subagent ?? null
1716
- };
1716
+ return { contextManagement: (await res.json()).contextManagement ?? null };
1717
1717
  } catch (err) {
1718
- log$10.debug({
1718
+ log$11.debug({
1719
1719
  err,
1720
1720
  event: "feature_flags_fetch_error"
1721
1721
  }, "feature-flags request errored");
@@ -1726,7 +1726,7 @@ function postHeartbeat({ messageId }) {
1726
1726
  const client = sandboxClient();
1727
1727
  if (!client) return;
1728
1728
  client.heartbeat.$post({ json: { messageId } }).catch((err) => {
1729
- log$10.debug({
1729
+ log$11.debug({
1730
1730
  err,
1731
1731
  event: "heartbeat_failed"
1732
1732
  }, "heartbeat failed");
@@ -1738,7 +1738,7 @@ async function resolveConversationFromApi(messageId) {
1738
1738
  try {
1739
1739
  const res = await client["message-conversation"].$get({ query: { messageId } });
1740
1740
  if (!res.ok) {
1741
- log$10.warn({
1741
+ log$11.warn({
1742
1742
  status: res.status,
1743
1743
  messageId,
1744
1744
  event: "resolve_conversation_failed"
@@ -1747,7 +1747,7 @@ async function resolveConversationFromApi(messageId) {
1747
1747
  }
1748
1748
  return (await res.json()).conversationId ?? null;
1749
1749
  } catch (err) {
1750
- log$10.warn({
1750
+ log$11.warn({
1751
1751
  err,
1752
1752
  messageId,
1753
1753
  event: "resolve_conversation_error"
@@ -1772,7 +1772,11 @@ async function postSubagentSpawn({ messageId, tasks }) {
1772
1772
  tasks
1773
1773
  } });
1774
1774
  if (!res.ok) throw new Error(`subagent-spawn POST failed: ${res.status}`);
1775
- return { taskIds: (await res.json()).taskIds };
1775
+ const body = await res.json();
1776
+ return {
1777
+ taskIds: body.taskIds,
1778
+ tasks: body.tasks ?? []
1779
+ };
1776
1780
  }
1777
1781
  function createHeartbeatThrottle({ messageId }) {
1778
1782
  let lastAt = 0;
@@ -1821,7 +1825,7 @@ function createToolHeartbeat({ messageId }) {
1821
1825
  }
1822
1826
  heartbeatCount++;
1823
1827
  if (heartbeatCount > MAX_TOOL_HEARTBEATS) {
1824
- log$10.warn({
1828
+ log$11.warn({
1825
1829
  heartbeatCount,
1826
1830
  activeToolCalls: [...activeToolCalls]
1827
1831
  }, "tool heartbeat max reached, stopping");
@@ -1852,7 +1856,7 @@ function postToDaemon(path, body) {
1852
1856
  headers: { "content-type": "application/json" },
1853
1857
  body: JSON.stringify(body)
1854
1858
  }).catch((err) => {
1855
- log$10.debug({
1859
+ log$11.debug({
1856
1860
  err,
1857
1861
  path,
1858
1862
  event: "daemon_post_failed"
@@ -1861,7 +1865,7 @@ function postToDaemon(path, body) {
1861
1865
  }
1862
1866
  function createPlatformExtensions({ sessionId, channelContext }) {
1863
1867
  return (pi) => {
1864
- log$10.info({
1868
+ log$11.info({
1865
1869
  sessionId,
1866
1870
  hasChannelContext: Boolean(channelContext)
1867
1871
  }, "platform extension initialized");
@@ -1910,7 +1914,7 @@ function createPlatformExtensions({ sessionId, channelContext }) {
1910
1914
  });
1911
1915
  });
1912
1916
  pi.on("agent_end", () => {
1913
- log$10.info({ sessionId }, "session ending");
1917
+ log$11.info({ sessionId }, "session ending");
1914
1918
  postToDaemon("/session/end", { sessionId });
1915
1919
  });
1916
1920
  };
@@ -1921,37 +1925,39 @@ function createPlatformExtensions({ sessionId, channelContext }) {
1921
1925
  * Shared harness feature-flag poll.
1922
1926
  *
1923
1927
  * The api exposes one `/feature-flags` GET that returns every harness flag in a
1924
- * single response (`{ contextManagement, subagent, commandFlags }` — see
1928
+ * single response (`{ contextManagement, commandFlags }` — see
1925
1929
  * apps/anyone/api/src/routes/sandbox-feature-flags.ts). Rather than each
1926
1930
  * extension issuing its own GET — and, worse, a *blocking* GET on the
1927
1931
  * pre-first-token `session_start` path — a single background poller fetches
1928
1932
  * that response once per interval and fans the values out to every subscriber.
1929
1933
  *
1930
- * Why one poller: the subagent extension gates its tool registration on the
1931
- * `subagent` flag. If it awaited a fresh GET inside `session_start` the tool
1932
- * schema (part of the prefill) couldn't be finalized until a serial
1933
- * sandbox→api round-trip settled, adding a net-new pre-token network hop on
1934
- * every session, flag on or off. Reading the last-polled value instead keeps
1935
- * the hot path allocation-only. A cold cache reads as `null` (fail-open to
1936
- * unregistered); a newly-flipped flag takes effect on the next poll, matching
1937
- * how context-management already treats its flag.
1934
+ * Why one poller: context-management consumes the `contextManagement` flag
1935
+ * without a blocking GET on the pre-first-token `session_start` path. Reading
1936
+ * the last-polled value keeps the hot path allocation-only; a cold cache reads
1937
+ * as `null` and a newly-flipped flag takes effect on the next poll.
1938
1938
  *
1939
1939
  * The poll is fire-and-forget and self-unref'd — it never keeps the process
1940
1940
  * alive and an indeterminate result (no api url / transient failure) leaves the
1941
1941
  * last-known values untouched so a blip can't silently flip behavior.
1942
1942
  */
1943
- const log$9 = logger.child({ module: "feature-flags-poll" });
1943
+ const log$10 = logger.child({ module: "feature-flags-poll" });
1944
1944
  const FLAG_POLL_INTERVAL_MS = 6e4;
1945
1945
  let contextManagement = null;
1946
- let subagent = null;
1947
- const subscribers = {
1948
- contextManagement: /* @__PURE__ */ new Set(),
1949
- subagent: /* @__PURE__ */ new Set()
1950
- };
1946
+ const subscribers = { contextManagement: /* @__PURE__ */ new Set() };
1951
1947
  let pollerStarted = false;
1948
+ let firstPollSettled = false;
1949
+ let resolveFirstPoll = null;
1950
+ new Promise((resolve) => {
1951
+ resolveFirstPoll = resolve;
1952
+ });
1953
+ function markFirstPollSettled() {
1954
+ if (firstPollSettled) return;
1955
+ firstPollSettled = true;
1956
+ resolveFirstPoll?.();
1957
+ }
1952
1958
  /** Last-polled value of a flag, or `null` if not yet resolved. */
1953
- function getPolledFlag(name) {
1954
- return name === "contextManagement" ? contextManagement : subagent;
1959
+ function getPolledFlag(_name) {
1960
+ return contextManagement;
1955
1961
  }
1956
1962
  /**
1957
1963
  * Subscribe to changes of a flag. The callback fires only on a *transition*
@@ -1964,23 +1970,25 @@ function onFlagChange(name, cb) {
1964
1970
  }
1965
1971
  function apply(name, next) {
1966
1972
  if (next === null) return;
1967
- const prev = name === "contextManagement" ? contextManagement : subagent;
1968
- if (name === "contextManagement") contextManagement = next;
1969
- else subagent = next;
1973
+ const prev = contextManagement;
1974
+ contextManagement = next;
1970
1975
  if (next !== prev) for (const cb of subscribers[name]) try {
1971
1976
  cb(next);
1972
1977
  } catch (err) {
1973
- log$9.warn({
1978
+ log$10.warn({
1974
1979
  err,
1975
1980
  flag: name
1976
1981
  }, "flag subscriber threw");
1977
1982
  }
1978
1983
  }
1979
1984
  async function pollOnce() {
1980
- const flags = await fetchHarnessFlags();
1981
- if (!flags) return;
1982
- apply("contextManagement", flags.contextManagement ?? null);
1983
- apply("subagent", flags.subagent ?? null);
1985
+ try {
1986
+ const flags = await fetchHarnessFlags();
1987
+ if (!flags) return;
1988
+ apply("contextManagement", flags.contextManagement ?? null);
1989
+ } catch (err) {
1990
+ log$10.debug({ err }, "feature-flag poll threw");
1991
+ }
1984
1992
  }
1985
1993
  /**
1986
1994
  * Start the shared background poll (idempotent). No-op when there's no
@@ -1991,7 +1999,7 @@ async function pollOnce() {
1991
1999
  function startFeatureFlagPoller() {
1992
2000
  if (pollerStarted || !hasFlagSource()) return;
1993
2001
  pollerStarted = true;
1994
- pollOnce();
2002
+ pollOnce().finally(markFirstPollSettled);
1995
2003
  setInterval(() => void pollOnce(), FLAG_POLL_INTERVAL_MS).unref?.();
1996
2004
  }
1997
2005
  //#endregion
@@ -2086,7 +2094,7 @@ function transformContextMessages(messages, config, now) {
2086
2094
  }
2087
2095
  //#endregion
2088
2096
  //#region src/extensions/context-management.ts
2089
- const log$8 = logger.child({ module: "context-management-extension" });
2097
+ const log$9 = logger.child({ module: "context-management-extension" });
2090
2098
  function isAnthropicMessagesPayload(payload) {
2091
2099
  if (typeof payload !== "object" || payload === null) return false;
2092
2100
  const candidate = payload;
@@ -2151,13 +2159,13 @@ function createContextManagementExtension() {
2151
2159
  setContextManagementFlagOverride(getPolledFlag("contextManagement"));
2152
2160
  onFlagChange("contextManagement", (enabled) => {
2153
2161
  setContextManagementFlagOverride(enabled);
2154
- log$8.info({
2162
+ log$9.info({
2155
2163
  event: "context_management_flag_update",
2156
2164
  enabled
2157
2165
  }, "context-management flag updated from platform");
2158
2166
  });
2159
2167
  startFeatureFlagPoller();
2160
- log$8.info({
2168
+ log$9.info({
2161
2169
  event: "context_management_registered",
2162
2170
  enabled: initial.enabled,
2163
2171
  flagSource: hasFlagSource(),
@@ -2169,13 +2177,13 @@ function createContextManagementExtension() {
2169
2177
  const { messages } = event;
2170
2178
  try {
2171
2179
  const result = transformContextIfEnabled(messages, getContextManagementConfig(), Date.now());
2172
- if (result.stats && (result.stats.clearedResults > 0 || result.stats.trimmedResults > 0)) log$8.info({
2180
+ if (result.stats && (result.stats.clearedResults > 0 || result.stats.trimmedResults > 0)) log$9.info({
2173
2181
  event: "context_management_applied",
2174
2182
  ...result.stats
2175
2183
  }, "trimmed/cleared tool output before LLM call");
2176
2184
  return { messages: result.messages };
2177
2185
  } catch (err) {
2178
- log$8.error({
2186
+ log$9.error({
2179
2187
  err,
2180
2188
  event: "context_management_transform_failed"
2181
2189
  }, "context transform failed; passing messages through unchanged");
@@ -2187,7 +2195,7 @@ function createContextManagementExtension() {
2187
2195
  }
2188
2196
  //#endregion
2189
2197
  //#region src/extensions/current-time.ts
2190
- const log$7 = logger.child({ module: "current-time-extension" });
2198
+ const log$8 = logger.child({ module: "current-time-extension" });
2191
2199
  const PI_DATE_LINE = /^Current date:.*$/m;
2192
2200
  function formatCurrentTimeLine(now) {
2193
2201
  return `Current date: ${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}-${String(now.getUTCDate()).padStart(2, "0")} (${new Intl.DateTimeFormat("en-US", {
@@ -2200,7 +2208,7 @@ const currentTimeExtension = (pi) => {
2200
2208
  const line = formatCurrentTimeLine(/* @__PURE__ */ new Date());
2201
2209
  const base = event.systemPrompt;
2202
2210
  if (PI_DATE_LINE.test(base)) {
2203
- log$7.info({ event: "pi_date_line_present" }, "pi base prompt carries its own 'Current date:' line again; replacing it in place (pi prompt format may have changed)");
2211
+ log$8.info({ event: "pi_date_line_present" }, "pi base prompt carries its own 'Current date:' line again; replacing it in place (pi prompt format may have changed)");
2204
2212
  return { systemPrompt: base.replace(PI_DATE_LINE, line) };
2205
2213
  }
2206
2214
  return { systemPrompt: `${base}\n${line}` };
@@ -2437,7 +2445,7 @@ function renderIndex(entries) {
2437
2445
  }
2438
2446
  //#endregion
2439
2447
  //#region src/extensions/memory.ts
2440
- const log$6 = logger.child({ module: "memory-extension" });
2448
+ const log$7 = logger.child({ module: "memory-extension" });
2441
2449
  /**
2442
2450
  * The standing instructions for the memory system. Always injected (even with
2443
2451
  * an empty `.memory/`) so the agent knows it can persist notes. `users/` is
@@ -2471,7 +2479,7 @@ const memoryExtension = (pi) => {
2471
2479
  index
2472
2480
  });
2473
2481
  } catch (err) {
2474
- log$6.warn({
2482
+ log$7.warn({
2475
2483
  err,
2476
2484
  event: "memory_index_failed"
2477
2485
  }, "memory index build failed; injecting instructions only");
@@ -2485,7 +2493,7 @@ const memoryExtension = (pi) => {
2485
2493
  };
2486
2494
  //#endregion
2487
2495
  //#region src/extensions/platform-memory.ts
2488
- const log$5 = logger.child({ module: "platform-memory-extension" });
2496
+ const log$6 = logger.child({ module: "platform-memory-extension" });
2489
2497
  /**
2490
2498
  * Resolve the human on this turn via the API, keyed by the message id.
2491
2499
  * `/sandbox/channel-context` only returns a sender for a platform-known
@@ -2498,13 +2506,13 @@ const log$5 = logger.child({ module: "platform-memory-extension" });
2498
2506
  async function resolveTurnUser(messageId) {
2499
2507
  const client = sandboxClient();
2500
2508
  if (!client) {
2501
- log$5.debug({ event: "resolve_turn_user_no_api_url" }, "no API url in env; withholding user memory");
2509
+ log$6.debug({ event: "resolve_turn_user_no_api_url" }, "no API url in env; withholding user memory");
2502
2510
  return null;
2503
2511
  }
2504
2512
  try {
2505
2513
  const res = await client["channel-context"].$get({ query: { messageId } });
2506
2514
  if (!res.ok) {
2507
- log$5.warn({
2515
+ log$6.warn({
2508
2516
  event: "resolve_turn_user_failed",
2509
2517
  status: res.status
2510
2518
  }, "channel-context returned non-ok; withholding user memory");
@@ -2517,7 +2525,7 @@ async function resolveTurnUser(messageId) {
2517
2525
  displayName: sender.displayName
2518
2526
  };
2519
2527
  } catch (err) {
2520
- log$5.warn({
2528
+ log$6.warn({
2521
2529
  err,
2522
2530
  event: "resolve_turn_user_failed"
2523
2531
  }, "failed to resolve current user; withholding user memory");
@@ -2564,7 +2572,7 @@ function createPlatformMemoryExtension({ channelContext }) {
2564
2572
  user
2565
2573
  });
2566
2574
  } catch (err) {
2567
- log$5.warn({
2575
+ log$6.warn({
2568
2576
  err,
2569
2577
  event: "user_memory_index_failed"
2570
2578
  }, "user memory index build failed; skipping injection");
@@ -2579,7 +2587,7 @@ function createPlatformMemoryExtension({ channelContext }) {
2579
2587
  }
2580
2588
  //#endregion
2581
2589
  //#region src/extensions/self-trace.ts
2582
- const log$4 = logger.child({ module: "self-trace-extension" });
2590
+ const log$5 = logger.child({ module: "self-trace-extension" });
2583
2591
  /**
2584
2592
  * Reports the agent's own execution as OpenTelemetry spans:
2585
2593
  * agent.session → agent.run → agent.turn.N → tool.NAME, with token/cost
@@ -2604,7 +2612,7 @@ const selfTraceExtension = (pi) => {
2604
2612
  sessionSpan = tracer.startSpan("agent.session", { attributes: { "agent.model": modelId } }, remoteCtx);
2605
2613
  sessionCtx = trace.setSpan(remoteCtx, sessionSpan);
2606
2614
  const sc = sessionSpan.spanContext();
2607
- log$4.info({
2615
+ log$5.info({
2608
2616
  event: "self_trace_session_start",
2609
2617
  trace_id: sc.traceId,
2610
2618
  span_id: sc.spanId,
@@ -2716,13 +2724,13 @@ const selfTraceExtension = (pi) => {
2716
2724
  * Lives in the harness package — soul.md is content from the agent's
2717
2725
  * own git repo, not from the platform — so its handling stays here.
2718
2726
  */
2719
- const log$3 = logger.child({ module: "soul-extension" });
2727
+ const log$4 = logger.child({ module: "soul-extension" });
2720
2728
  async function readSoul(cwd) {
2721
2729
  try {
2722
2730
  return (await readFile(join(cwd, "soul.md"), "utf8")).trim() || null;
2723
2731
  } catch (err) {
2724
2732
  if (err?.code === "ENOENT") return null;
2725
- log$3.warn({
2733
+ log$4.warn({
2726
2734
  err,
2727
2735
  event: "soul_read_failed"
2728
2736
  }, "soul.md read failed");
@@ -2736,7 +2744,7 @@ function soulSection(cwd, soul) {
2736
2744
 
2737
2745
  **\`soul.md\` is where behavior lives.** Any standing instruction about how you should act — a rule a user wants you to follow going forward, a tone or format preference, a workflow convention, a "from now on, always/never …" — belongs here, not in \`.memory/\`. Memory records *what happened* (facts, events, findings); soul defines *how you behave*. When a user gives you a durable behavioral rule, write it to \`soul.md\`. If you find behavioral rules that ended up in \`.memory/\`, treat that as misfiled and move them here.
2738
2746
 
2739
- Keep it current. When you gain a durable new capability — a tool you build, a skill or integration you set up, a service you connect — or a user hands you a lasting behavioral rule, record it in \`soul.md\` so a future conversation knows it's part of you rather than rediscovering it from scratch. Edit it (then \`git add soul.md && git commit && git push\`) to redefine yourself; picked up on the next message.
2747
+ Keep it current. When you gain a durable new capability — a tool you build, a skill or integration you set up, a service you connect, a secret or auth credential you wire in — or a user hands you a lasting behavioral rule, record it in \`soul.md\` so a future conversation knows it's part of you rather than rediscovering it from scratch. Do this the moment you gain the capability, and for a credential that means the moment it verifies with a real call, not after a human points out that you forgot. Connecting a capability is itself a durable change worth recording, not merely a step toward the task in front of you. Edit \`soul.md\` (then \`git add soul.md && git commit && git push\`) to redefine yourself; picked up on the next message.
2740
2748
 
2741
2749
  ${soul ? soul : "_(empty — write to `soul.md` to define your persona)_"}`;
2742
2750
  }
@@ -2754,13 +2762,23 @@ const soulExtension = (pi) => {
2754
2762
  };
2755
2763
  //#endregion
2756
2764
  //#region src/extensions/subagent/index.ts
2757
- const log$2 = logger.child({ module: "subagent-ext" });
2765
+ const log$3 = logger.child({ module: "subagent-ext" });
2758
2766
  const MAX_TASKS = 8;
2759
2767
  const TaskItem = Type.Object({
2760
2768
  task: Type.String({ description: "The task to delegate to a subagent run." }),
2769
+ title: Type.String({
2770
+ description: "A SHORT name for this task — 3-6 words, sentence case, no trailing period. This is what the person in the chat sees as the row for this subagent, so name the work, do not restate the prompt. Good: \"Audit the billing gate\", \"Compare competitor pricing\", \"Draft the migration\". Bad: \"You are looking at apps/anyone/web and should check every component…\".",
2771
+ maxLength: 120
2772
+ }),
2761
2773
  persona: Type.Optional(Type.String({ description: "Optional extra system prompt / role for this task, applied ON TOP of the child run's own default persona (your full identity and soul are still there underneath). Omit to run with just your default persona." })),
2762
- model: Type.Optional(Type.String({ description: "Optional model id to run this subagent on (e.g. \"anthropic/claude-opus-4-8\"). Must be a real catalogued model. Omit to run on your own model. If you are locked to a Google-compliant model, only compliant models are accepted." }))
2774
+ model: Type.Optional(Type.String({ description: "Optional model id to run this subagent on. PREFER A LOWER-COST, FASTER MODEL when the task is well-scoped and does not need your full reasoning depth — most delegated subtasks (searching, summarizing, mechanical edits, gathering or reformatting data, running a check) run just as well on a lighter model and cost far less. Reserve a top-tier model for subtasks that genuinely need deep reasoning or careful judgment. Must be a real catalogued model id. Omit to inherit your own model. If you are locked to a Google-compliant model, only compliant models are accepted." })),
2775
+ timeoutMinutes: Type.Optional(Type.Integer({
2776
+ description: "Optional wall-clock timeout for this subagent, in minutes. If the run is still going after this long it is ended and you are rewoken with a timeout result, so a hung subagent can never strand you. Omit for the default (30 minutes). Raise it for genuinely long work (a big migration, a large audit); lower it for a quick lookup. Range 1-360.",
2777
+ minimum: 1,
2778
+ maximum: 360
2779
+ }))
2763
2780
  });
2781
+ const DEFAULT_SUBAGENT_TIMEOUT_MS = 30 * 6e4;
2764
2782
  const SubagentParams = Type.Object({ tasks: Type.Array(TaskItem, {
2765
2783
  description: "One or more tasks to delegate. Each spawns an isolated subagent run linked to this conversation; they run in parallel and each rewakes you with its result when it finishes.",
2766
2784
  minItems: 1,
@@ -2775,7 +2793,8 @@ function buildTool(messageId) {
2775
2793
  "Delegate one or more tasks to subagent runs — fresh isolated copies of yourself, each with its own context window, linked to this conversation.",
2776
2794
  "Use it to parallelize independent work, to keep a large or noisy subtask out of your own context, or to run a task under a specialized persona.",
2777
2795
  "Fire-and-forget: this returns immediately after queueing. It does NOT wait for results. Each subagent runs on its own and, when it finishes, sends you its result on this thread — so queue the work, then keep going or end your turn. To chain, re-delegate after a result lands.",
2778
- "Pass tasks: [{ task, persona?, model? }]. persona is an optional extra system prompt layered ON TOP of your default persona for that task (it adds to, it does not replace, your identity); omit it to run with just your default persona. model is an optional model id for that task; omit it to run on your own model."
2796
+ "Pass tasks: [{ task, title, persona?, model? }]. title is a short 3-6 word name for the task — it is shown to the person in the chat as that subagent's row, so name the work rather than restating the prompt. persona is an optional extra system prompt layered ON TOP of your default persona for that task (it adds to, it does not replace, your identity); omit it to run with just your default persona. model is an optional model id for that task — prefer a lower-cost, faster model for well-scoped subtasks that don't need deep reasoning, and reserve a top-tier model for the ones that do; omit it to inherit your own model.",
2797
+ "Peering: each queued task comes back with its own conversation id. A subagent is a real linked conversation, so to see what one is doing RIGHT NOW while it runs — its reasoning, the tools it has called and their results, its progress — read that conversation with `platform conversations show <conversationId>` (you are already authorized; it is your own delegated run). Check in that way instead of waiting blind for the final result. The read reflects the child's persisted state, which lags a few seconds behind live (tool results land as they complete; in-progress reasoning can be up to ~5s stale), so peek between checkpoints rather than polling in a tight loop."
2779
2798
  ].join(" "),
2780
2799
  promptSnippet: "subagent — delegate tasks to isolated subagent runs; each rewakes you with its result when done",
2781
2800
  parameters: SubagentParams,
@@ -2791,29 +2810,41 @@ function buildTool(messageId) {
2791
2810
  };
2792
2811
  const spawnTasks = tasks.map((t) => ({
2793
2812
  task: t.task,
2813
+ title: t.title ?? null,
2794
2814
  persona: t.persona ?? null,
2795
- model: t.model ?? null
2815
+ model: t.model ?? null,
2816
+ timeoutMs: t.timeoutMinutes != null ? t.timeoutMinutes * 6e4 : DEFAULT_SUBAGENT_TIMEOUT_MS
2796
2817
  }));
2797
2818
  try {
2798
- const { taskIds } = await postSubagentSpawn({
2819
+ const spawned = await postSubagentSpawn({
2799
2820
  messageId,
2800
2821
  tasks: spawnTasks
2801
2822
  });
2802
- log$2.info({
2823
+ const { taskIds } = spawned;
2824
+ log$3.info({
2803
2825
  event: "subagent_spawned",
2804
2826
  count: taskIds.length
2805
2827
  }, "subagent tasks queued");
2806
- const lines = taskIds.map((id, i) => `- ${id}: ${spawnTasks[i]?.task ?? ""}`).join("\n");
2828
+ const convByTask = new Map(spawned.tasks.map((t) => [t.taskId, t.conversationId]));
2829
+ const lines = taskIds.map((id, i) => {
2830
+ const label = spawnTasks[i]?.title ?? spawnTasks[i]?.task ?? "";
2831
+ const conv = convByTask.get(id);
2832
+ return `- ${id}: ${label}${conv ? ` — peek: conversations show ${conv}` : ""}`;
2833
+ }).join("\n");
2834
+ const peerHint = spawned.tasks.length ? "\nTo see what a subagent is doing while it runs, read its conversation with `platform conversations show <conversationId>` (shown per task above)." : "";
2807
2835
  return {
2808
2836
  content: [{
2809
2837
  type: "text",
2810
- text: `Queued ${taskIds.length} subagent ${taskIds.length === 1 ? "run" : "runs"}. Each runs on its own and will send you its result on this thread when it finishes — keep working or end your turn meanwhile.\n${lines}`
2838
+ text: `Queued ${taskIds.length} subagent ${taskIds.length === 1 ? "run" : "runs"}. Each runs on its own and will send you its result on this thread when it finishes — keep working or end your turn meanwhile.\n${lines}${peerHint}`
2811
2839
  }],
2812
- details: { taskIds }
2840
+ details: {
2841
+ taskIds,
2842
+ tasks: spawned.tasks
2843
+ }
2813
2844
  };
2814
2845
  } catch (err) {
2815
2846
  const message = err instanceof Error ? err.message : String(err);
2816
- log$2.warn({
2847
+ log$3.warn({
2817
2848
  err,
2818
2849
  event: "subagent_spawn_failed"
2819
2850
  }, "subagent spawn failed");
@@ -2830,31 +2861,249 @@ function buildTool(messageId) {
2830
2861
  };
2831
2862
  }
2832
2863
  /**
2833
- * Gated on `harness-subagent-enabled`, read from the shared feature-flag poll.
2834
2864
  * The factory takes the session's channel context to resolve the originating
2835
2865
  * messageId — the api links each spawned run to the conversation that message
2836
2866
  * belongs to and rewakes it on completion (nothing about the parent is piped
2837
- * from the sandbox beyond that id).
2867
+ * from the sandbox beyond that id). The tool is registered unconditionally at
2868
+ * session_start.
2838
2869
  */
2839
2870
  function createSubagentExtension({ channelContext }) {
2840
2871
  return (pi) => {
2841
2872
  const messageId = extractMessageId(channelContext);
2842
- startFeatureFlagPoller();
2843
- pi.on("session_start", async () => {
2844
- if (getPolledFlag("subagent") === true) {
2845
- pi.registerTool(buildTool(messageId));
2846
- log$2.info({ event: "subagent_enabled" }, "subagent tool registered");
2847
- }
2873
+ let registered = false;
2874
+ const registerOnce = () => {
2875
+ if (registered) return;
2876
+ registered = true;
2877
+ pi.registerTool(buildTool(messageId));
2878
+ log$3.info({ event: "subagent_enabled" }, "subagent tool registered");
2879
+ };
2880
+ pi.on("session_start", () => {
2881
+ registerOnce();
2848
2882
  });
2849
2883
  };
2850
2884
  }
2851
2885
  //#endregion
2886
+ //#region src/extensions/resource-pressure-warning.ts
2887
+ /**
2888
+ * Mid-run resource-pressure warning to the agent.
2889
+ *
2890
+ * The sandbox already detects pressure — the boot scripts cap the
2891
+ * user-workload cgroup (memory.high/memory.max) and watchers log warn/crit
2892
+ * edges for memory and disk — but nothing told the *agent*, so a turn burned
2893
+ * straight to the OOM kill (or a full disk) and only learned about it from
2894
+ * the post-mortem notice. This extension closes that gap in-process: while a
2895
+ * turn is active it polls the agent cgroup and the root filesystem and, the
2896
+ * first time usage crosses a warn threshold, folds a system notification into
2897
+ * the open turn so the agent can checkpoint, shed work (constrain
2898
+ * parallelism, kill a background hog, clean scratch space), or request a
2899
+ * bigger tier BEFORE the kill.
2900
+ *
2901
+ * The notification is triggered by the two conditions that actually kill
2902
+ * work — memory near the cgroup hard cap, disk near full — and reports a
2903
+ * snapshot of all the relevant stats (memory, CPU utilization, disk) so the
2904
+ * agent can tell which resource is the problem and how much headroom the
2905
+ * others have.
2906
+ *
2907
+ * Edge-triggered, once per trigger per turn: the fired flags reset on
2908
+ * agent_start, so a turn that rides a threshold gets one warning per
2909
+ * resource, not a stream. Polling only runs while the agent is active — an
2910
+ * idle sandbox's resource usage is not the agent's problem and there is no
2911
+ * open turn to deliver into anyway.
2912
+ *
2913
+ * Best-effort throughout: any read failure (cgroup absent, controller not
2914
+ * delegated, non-cgroup-v2 host, df missing) reads as "no signal" for that
2915
+ * stat and the extension warns on what it can see — it must never break a
2916
+ * turn over an observability feature.
2917
+ */
2918
+ const execFileAsync = promisify(execFile);
2919
+ const log$2 = logger.child({ module: "resource-pressure-warning" });
2920
+ const POLL_INTERVAL_MS = 1e4;
2921
+ function envOverride(name) {
2922
+ for (const prefix of ["SKYDIVE_", "ANYONE_"]) {
2923
+ const value = process.env[`${prefix}${name}`];
2924
+ if (value != null && value !== "") return value;
2925
+ }
2926
+ return null;
2927
+ }
2928
+ function cgroupDir() {
2929
+ return envOverride("AGENT_CGROUP") ?? "/sys/fs/cgroup/agent";
2930
+ }
2931
+ function diskRoot() {
2932
+ return envOverride("DISK_ROOT") ?? "/";
2933
+ }
2934
+ /**
2935
+ * Read a cgroup v2 scalar file. Returns a number, or null for "max"
2936
+ * (uncapped), an empty/absent file, or any read/parse error — an uncapped or
2937
+ * unreadable limit means there is nothing meaningful to warn against.
2938
+ */
2939
+ async function readScalar(file) {
2940
+ try {
2941
+ const raw = (await readFile(`${cgroupDir()}/${file}`, "utf8")).trim();
2942
+ if (raw === "" || raw === "max") return null;
2943
+ const n = Number(raw);
2944
+ return Number.isFinite(n) ? n : null;
2945
+ } catch (_error) {
2946
+ return null;
2947
+ }
2948
+ }
2949
+ /**
2950
+ * Read a cgroup v2 "flat keyed" file (one `key value` pair per line, e.g.
2951
+ * cpu.stat) and return the counter for `key`, or null when absent.
2952
+ */
2953
+ async function readKeyedCounter(file, key) {
2954
+ try {
2955
+ const raw = await readFile(`${cgroupDir()}/${file}`, "utf8");
2956
+ for (const line of raw.split("\n")) {
2957
+ const [k, v] = line.trim().split(/\s+/);
2958
+ if (k === key) {
2959
+ const n = Number(v);
2960
+ return Number.isFinite(n) ? n : null;
2961
+ }
2962
+ }
2963
+ return null;
2964
+ } catch (_error) {
2965
+ return null;
2966
+ }
2967
+ }
2968
+ /**
2969
+ * Live memory usage as an integer percent of the hard cap, or null when
2970
+ * either side is unreadable/uncapped. Exported for tests.
2971
+ */
2972
+ async function readMemUsePct() {
2973
+ const [current, max] = await Promise.all([readScalar("memory.current"), readScalar("memory.max")]);
2974
+ if (current === null || max === null || max <= 0) return null;
2975
+ return {
2976
+ pct: Math.floor(current / max * 100),
2977
+ currentBytes: current,
2978
+ maxBytes: max
2979
+ };
2980
+ }
2981
+ /**
2982
+ * Root filesystem used% (df -P Capacity column), or null on any failure.
2983
+ * Exported for tests.
2984
+ */
2985
+ async function readDiskUsePct() {
2986
+ try {
2987
+ const { stdout } = await execFileAsync("df", ["-P", diskRoot()]);
2988
+ const dataRow = stdout.trim().split("\n")[1];
2989
+ if (dataRow == null) return null;
2990
+ const capacity = dataRow.trim().split(/\s+/)[4];
2991
+ if (capacity == null) return null;
2992
+ const pct = Number(capacity.replace("%", ""));
2993
+ return Number.isFinite(pct) ? pct : null;
2994
+ } catch (_error) {
2995
+ return null;
2996
+ }
2997
+ }
2998
+ /**
2999
+ * CPU utilization sampler. cgroup v2 exposes cumulative CPU time
3000
+ * (cpu.stat usage_usec); utilization is the delta between two samples over
3001
+ * the wall time between them, normalized by core count. The first call after
3002
+ * construction has no previous sample and returns null.
3003
+ */
3004
+ function createCpuSampler() {
3005
+ let prevUsageUsec = null;
3006
+ let prevAtMs = null;
3007
+ return async () => {
3008
+ const usage = await readKeyedCounter("cpu.stat", "usage_usec");
3009
+ const now = Date.now();
3010
+ const prev = prevUsageUsec;
3011
+ const prevAt = prevAtMs;
3012
+ prevUsageUsec = usage;
3013
+ prevAtMs = now;
3014
+ if (usage === null || prev === null || prevAt === null) return null;
3015
+ const wallUsec = (now - prevAt) * 1e3;
3016
+ if (wallUsec <= 0) return null;
3017
+ const cores = availableParallelism();
3018
+ const pct = Math.round((usage - prev) / (wallUsec * cores) * 100);
3019
+ return Math.max(0, Math.min(100, pct));
3020
+ };
3021
+ }
3022
+ function fmtMb(bytes) {
3023
+ return Math.round(bytes / 1024 / 1024);
3024
+ }
3025
+ /** The model-facing warning text. Exported for tests. */
3026
+ function resourcePressureWarningText(trigger, { mem, cpuPct, diskPct }) {
3027
+ const stats = [];
3028
+ if (mem) stats.push(`memory ${mem.pct}% of cap (${fmtMb(mem.currentBytes)}/${fmtMb(mem.maxBytes)} MB)`);
3029
+ if (cpuPct !== null) stats.push(`CPU ${cpuPct}%`);
3030
+ if (diskPct !== null) stats.push(`disk ${diskPct}% full`);
3031
+ const lead = trigger === "memory" ? `Your sandbox is at ${mem?.pct}% of its memory cap. If usage keeps climbing, the kernel will kill the offending process and this turn may die with it.` : `Your sandbox's disk is ${diskPct}% full. If it fills completely, writes will start failing and this turn may die with them.`;
3032
+ const remedy = trigger === "memory" ? "checkpoint in-flight work (commit and push), then reduce the footprint — constrain parallelism, run heavy steps sequentially, or kill background processes you no longer need." : "checkpoint in-flight work (commit and push), then free space — clean build artifacts, caches, and scratch files you no longer need.";
3033
+ return `<system_notification>${lead} Current usage: ${stats.join(", ")}. Act now: ${remedy} If the workload genuinely needs more resources, request a bigger sandbox with \`platform compute request\`. This is an automated resource warning, not a message from the user; continue the task, adjusted.</system_notification>`;
3034
+ }
3035
+ const resourcePressureWarningExtension = (pi) => {
3036
+ let agentActive = false;
3037
+ let warnedMemThisTurn = false;
3038
+ let warnedDiskThisTurn = false;
3039
+ let timer = null;
3040
+ const sampleCpu = createCpuSampler();
3041
+ async function checkOnce() {
3042
+ if (!agentActive || warnedMemThisTurn && warnedDiskThisTurn) return;
3043
+ const [mem, cpuPct, diskPct] = await Promise.all([
3044
+ readMemUsePct(),
3045
+ sampleCpu(),
3046
+ readDiskUsePct()
3047
+ ]);
3048
+ let trigger = null;
3049
+ if (!warnedMemThisTurn && mem !== null && mem.pct >= 80) {
3050
+ trigger = "memory";
3051
+ warnedMemThisTurn = true;
3052
+ } else if (!warnedDiskThisTurn && diskPct !== null && diskPct >= 80) {
3053
+ trigger = "disk";
3054
+ warnedDiskThisTurn = true;
3055
+ }
3056
+ if (trigger === null) return;
3057
+ log$2.warn({
3058
+ trigger,
3059
+ mem,
3060
+ cpuPct,
3061
+ diskPct
3062
+ }, "resource pressure warning delivered to agent");
3063
+ await pi.sendMessage({
3064
+ customType: "anyone-resource-pressure-warning",
3065
+ content: resourcePressureWarningText(trigger, {
3066
+ mem,
3067
+ cpuPct,
3068
+ diskPct
3069
+ }),
3070
+ display: false
3071
+ }, {
3072
+ triggerTurn: true,
3073
+ deliverAs: "followUp"
3074
+ });
3075
+ }
3076
+ pi.on("agent_start", async () => {
3077
+ agentActive = true;
3078
+ warnedMemThisTurn = false;
3079
+ warnedDiskThisTurn = false;
3080
+ if (!timer) {
3081
+ timer = setInterval(() => {
3082
+ checkOnce().catch((err) => {
3083
+ log$2.error({ err }, "resource pressure check failed");
3084
+ });
3085
+ }, POLL_INTERVAL_MS);
3086
+ timer.unref?.();
3087
+ }
3088
+ });
3089
+ pi.on("agent_end", async () => {
3090
+ agentActive = false;
3091
+ if (timer) {
3092
+ clearInterval(timer);
3093
+ timer = null;
3094
+ }
3095
+ });
3096
+ };
3097
+ //#endregion
2852
3098
  //#region src/extensions/tool-call-env.ts
2853
3099
  const TOOL_CALL_ID_VAR = "TOOL_CALL_ID";
3100
+ function shellQuoteValue(value) {
3101
+ return quote([value]);
3102
+ }
2854
3103
  function withToolCallId({ command, toolCallId }) {
2855
- return `export ${TOOL_CALL_ID_VAR}=${toolCallId}; ${command}`;
3104
+ return `export ${TOOL_CALL_ID_VAR}=${shellQuoteValue(toolCallId)}; ${command}`;
2856
3105
  }
2857
- const PLATFORM_EXPORT = new RegExp(`^\\s*export\\s+(?:${TOOL_CALL_ID_VAR}|ANYONE_\\w+|SKYDIVE_\\w+)=(?:"(?:\\\\.|[^"])*"|'[^']*'|[^;\\s]*)\\s*;\\s*`);
3106
+ const PLATFORM_EXPORT = new RegExp(`^\\s*export\\s+(?:${TOOL_CALL_ID_VAR}|ANYONE_\\w+|SKYDIVE_\\w+)=(?:"(?:\\\\.|[^"])*"|'(?:'\\\\''|[^'])*'|[^;\\s]*)\\s*;\\s*`);
2858
3107
  function stripPlatformExportsForDisplay(command) {
2859
3108
  let c = command;
2860
3109
  let m;
@@ -3619,7 +3868,8 @@ function platformExtensions({ sessionId, channelContext }) {
3619
3868
  selfTraceExtension,
3620
3869
  createBackgroundTasksExtension({ channelContext }),
3621
3870
  createSubagentExtension({ channelContext }),
3622
- createContextManagementExtension()
3871
+ createContextManagementExtension(),
3872
+ resourcePressureWarningExtension
3623
3873
  ];
3624
3874
  }
3625
3875
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skydiveai/pi-extensions",
3
- "version": "0.1.0-beta.8",
3
+ "version": "0.1.0-beta.802",
4
4
  "homepage": "https://skydive.com",
5
5
  "license": "MIT",
6
6
  "author": "Create, Inc.",
@@ -17,12 +17,6 @@
17
17
  },
18
18
  "publishConfig": {
19
19
  "access": "public",
20
- "exports": {
21
- ".": {
22
- "types": "./dist/index.d.mts",
23
- "default": "./dist/index.mjs"
24
- }
25
- },
26
20
  "registry": "https://registry.npmjs.org"
27
21
  },
28
22
  "scripts": {
@@ -45,6 +39,7 @@
45
39
  "@skydiveai/pi-server": "^0.1.0",
46
40
  "hono": "^4.6.14",
47
41
  "pino": "^9.6.0",
42
+ "shell-quote": "^1.8.4",
48
43
  "typebox": "^1.1.34",
49
44
  "yaml": "^2.8.3",
50
45
  "zod": "^3.25.0"