pi-better-harness 0.34.1 → 0.34.2

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-background-tasks",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "Pi extension for durable background shell tasks, watchers, logs, and status inspection.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -348,20 +348,60 @@ type MainListWidget = Component & {
348
348
  handleMouse(event: WidgetMouseEvent): WidgetMouseResult | undefined;
349
349
  };
350
350
 
351
+ const BACK_TO_MAIN = "← main (Esc)";
352
+ const BACK_TARGET = "back-to-main";
353
+
354
+ function highlightPointerTarget(theme: unknown, line: string, width: number): string {
355
+ const colors = theme as { bg?(color: string, value: string): string } | undefined;
356
+ return colors?.bg?.("selectedBg", line + " ".repeat(Math.max(0, width - visibleWidth(line))))
357
+ ?? themeFgFromTheme(theme)("accent", line);
358
+ }
359
+
360
+ function createPointerNavigation(targetAt: (event: WidgetMouseEvent) => string | undefined, activate: (target: string) => boolean) {
361
+ let hovered: string | undefined;
362
+ let pressed: { target: string; y: number } | undefined;
363
+ return {
364
+ get hovered() { return hovered; },
365
+ reset() { hovered = undefined; pressed = undefined; },
366
+ handleMouse(event: WidgetMouseEvent): WidgetMouseResult | undefined {
367
+ if (event.type === "move" || event.type === "leave") {
368
+ const target = event.type === "leave" ? undefined : targetAt(event);
369
+ const changed = hovered !== target;
370
+ hovered = target;
371
+ return changed || target ? { handled: true, render: changed } : undefined;
372
+ }
373
+ if (event.button !== "left" || event.shift || event.alt || event.ctrl) {
374
+ pressed = undefined;
375
+ return undefined;
376
+ }
377
+ if (event.type === "press") {
378
+ const target = targetAt(event);
379
+ pressed = target ? { target, y: event.y } : undefined;
380
+ return target ? { handled: true, render: false } : undefined;
381
+ }
382
+ if (event.type !== "click") return undefined;
383
+ const claim = pressed;
384
+ pressed = undefined;
385
+ if (!claim || claim.y !== event.y || targetAt(event) !== claim.target || !activate(claim.target)) return undefined;
386
+ return { handled: true, render: true };
387
+ },
388
+ };
389
+ }
390
+
351
391
  function createMainListWidget(tui: { requestRender?(): void }, theme: unknown, deps: HostDeps): MainListWidget {
352
392
  const requestRender = () => tui.requestRender?.();
353
393
  state().mainListRequestRender = requestRender;
354
394
  // navigatorId per rendered line, so a pointer row resolves by structure, never by row text.
355
395
  let lineOwners: Array<string | undefined> = [];
356
396
  let lineWidth = MAIN_LIST_FALLBACK_WIDTH;
357
- // Pi synthesizes a click only for the component that handled the initial press.
358
- let pressedRow: { navigatorId: string; y: number } | undefined;
359
397
  const rowAt = (event: WidgetMouseEvent): string | undefined => {
360
398
  const width = Number.isFinite(event.width) && event.width > 0 ? Math.floor(event.width) : lineWidth;
361
399
  if (!Number.isFinite(event.x) || event.x < 0 || event.x >= width) return undefined;
362
400
  if (!Number.isInteger(event.y) || event.y < 0 || event.y >= lineOwners.length) return undefined;
363
- return lineOwners[event.y];
401
+ const id = lineOwners[event.y];
402
+ return id && listRows().some((row) => row.navigatorId === id) ? id : undefined;
364
403
  };
404
+ const pointer = createPointerNavigation(rowAt, openNavigatorRow);
365
405
  return {
366
406
  render(width: number): string[] {
367
407
  const rows = listRows();
@@ -369,7 +409,7 @@ function createMainListWidget(tui: { requestRender?(): void }, theme: unknown, d
369
409
  if (!rows.length) {
370
410
  state().lastMainListLines = undefined;
371
411
  lineOwners = [];
372
- pressedRow = undefined;
412
+ pointer.reset();
373
413
  return [];
374
414
  }
375
415
  const layout = buildMainListLayout(rows, renderWidth(width), deps.truncate, themeFgFromTheme(theme), {
@@ -379,41 +419,24 @@ function createMainListWidget(tui: { requestRender?(): void }, theme: unknown, d
379
419
  lineWidth = renderWidth(width);
380
420
  // The leading gap line belongs to the widget too, so its slot stays ownerless.
381
421
  lineOwners = [undefined, ...layout.owners];
382
- const lines = ["", ...layout.lines];
422
+ const lines = ["", ...layout.lines].map((line, index) =>
423
+ pointer.hovered && lineOwners[index] === pointer.hovered ? highlightPointerTarget(theme, line, lineWidth) : line);
383
424
  state().lastMainListLines = lines;
384
425
  return lines;
385
426
  },
386
427
  handleMouse(event: WidgetMouseEvent): WidgetMouseResult | undefined {
387
- // Only an unmodified left press is ours; every other gesture reaches the host untouched.
388
- if (event.button !== "left" || event.shift === true || event.alt === true || event.ctrl === true) {
389
- pressedRow = undefined;
390
- return undefined;
391
- }
392
- if (event.type === "press") {
393
- pressedRow = undefined;
394
- const navigatorId = rowAt(event);
395
- if (!navigatorId || !listRows().some((row) => row.navigatorId === navigatorId)) return undefined;
396
- pressedRow = { navigatorId, y: event.y };
397
- return { handled: true, render: false };
398
- }
399
- if (event.type !== "click") return undefined;
400
- const pressed = pressedRow;
401
- pressedRow = undefined;
402
- // A click after another component's press, or after the layout moved, is not the row we claimed.
403
- if (!pressed || pressed.y !== event.y || rowAt(event) !== pressed.navigatorId) return undefined;
404
- if (!openNavigatorRow(pressed.navigatorId)) return undefined;
405
- return { handled: true, render: true };
428
+ return pointer.handleMouse(event);
406
429
  },
407
430
  invalidate() {
408
431
  state().lastMainListLines = undefined;
409
432
  lineOwners = [];
410
- pressedRow = undefined;
433
+ pointer.reset();
411
434
  },
412
435
  dispose() {
413
436
  const s = state();
414
437
  if (s.mainListRequestRender === requestRender) s.mainListRequestRender = undefined;
415
438
  lineOwners = [];
416
- pressedRow = undefined;
439
+ pointer.reset();
417
440
  },
418
441
  };
419
442
  }
@@ -958,6 +981,22 @@ function createOverlayComponent(
958
981
  let closed = false;
959
982
  let transcriptDetail: BackgroundWorkDetail | null = null;
960
983
  let transcriptComponent: Component | null = null;
984
+ let pointerTargets: Array<string | undefined> = [];
985
+ let pointerWidth = 0;
986
+ const pointer = createPointerNavigation((event) => {
987
+ if (closed || !Number.isInteger(event.y) || !Number.isFinite(event.x) || event.x < 0 || event.x >= pointerWidth) return undefined;
988
+ const target = pointerTargets[event.y];
989
+ if (target === BACK_TARGET) return event.x >= 3 && event.x < 3 + visibleWidth(BACK_TO_MAIN) ? target : undefined;
990
+ return target && listRows().some((row) => row.navigatorId === target) ? target : undefined;
991
+ }, (target) => {
992
+ if (target === BACK_TARGET) { backToMain(); return true; }
993
+ refreshRows();
994
+ const index = overlayState.rows.findIndex((row) => row.navigatorId === target);
995
+ if (index < 0) return false;
996
+ selectOverlayRow(index);
997
+ openDetail();
998
+ return true;
999
+ });
961
1000
 
962
1001
  const fg = (color: string, value: string) => theme?.fg ? theme.fg(color, value) : value;
963
1002
 
@@ -1036,6 +1075,11 @@ function createOverlayComponent(
1036
1075
  try { done(null); } catch { /* ignore */ }
1037
1076
  }
1038
1077
 
1078
+ function backToMain(): void {
1079
+ unfocusMainList();
1080
+ close();
1081
+ }
1082
+
1039
1083
  function handleCloseKey(): void {
1040
1084
  const row = selectedRow();
1041
1085
  if (!row || row.parentRow) return;
@@ -1101,14 +1145,17 @@ function createOverlayComponent(
1101
1145
  render(width: number) {
1102
1146
  closeIfEditorReplaced();
1103
1147
  refreshRows();
1104
- const railLines = buildMainListLines(overlayState.rows, width, deps.truncate, fg, {
1148
+ const rail = buildMainListLayout(overlayState.rows, width, deps.truncate, fg, {
1105
1149
  selectedId: selectedRow()?.navigatorId,
1106
1150
  focused: true,
1107
1151
  });
1152
+ const railLines = rail.lines;
1108
1153
  const editorLines = renderEditorLines(width);
1109
1154
  const bottomLines = [...railLines, ...editorLines];
1110
1155
  const overlayRows = state().detailOverlayRows;
1111
1156
  const detailRows = overlayRows === undefined ? undefined : Math.max(1, overlayRows - bottomLines.length);
1157
+ const backControl = pointer.hovered === BACK_TARGET
1158
+ ? highlightPointerTarget(theme, BACK_TO_MAIN, visibleWidth(BACK_TO_MAIN)) : undefined;
1112
1159
  let contentLines: string[];
1113
1160
  if (detail?.transcript && deps.createTranscriptComponent) {
1114
1161
  if (transcriptDetail !== detail || !transcriptComponent) {
@@ -1121,18 +1168,30 @@ function createOverlayComponent(
1121
1168
  width,
1122
1169
  deps.truncate,
1123
1170
  fg,
1124
- { minRows: detailRows, maxRows: detailRows, bottomFooter: false, logTailRows },
1171
+ { minRows: detailRows, maxRows: detailRows, bottomFooter: false, logTailRows, backControl },
1125
1172
  );
1126
1173
  } else {
1127
1174
  transcriptDetail = null;
1128
1175
  transcriptComponent = null;
1129
- contentLines = buildDetailLines(detail, width, deps.truncate, fg, { expandedSections, logTailRows, minRows: detailRows, maxRows: detailRows, bottomFooter: false });
1176
+ contentLines = buildDetailLines(detail, width, deps.truncate, fg, { expandedSections, logTailRows, minRows: detailRows, maxRows: detailRows, bottomFooter: false, backControl });
1130
1177
  }
1131
- if (overlayRows === undefined || detailRows === undefined) return [...contentLines, ...bottomLines];
1132
- const fittedContent = contentLines.slice(0, detailRows);
1133
- while (fittedContent.length < detailRows) fittedContent.push("");
1134
- // A terminal shorter than rail + editor keeps the bottom rows (the input frame) and drops the top.
1135
- return [...fittedContent, ...bottomLines].slice(-Math.max(1, overlayRows));
1178
+ const fittedContent = detailRows === undefined ? contentLines : contentLines.slice(0, detailRows);
1179
+ if (detailRows !== undefined) while (fittedContent.length < detailRows) fittedContent.push("");
1180
+ const contentTargets = fittedContent.map((_, index) => index === 1 ? BACK_TARGET : undefined);
1181
+ const allLines = [...fittedContent, ...bottomLines];
1182
+ const allTargets = [...contentTargets, ...rail.owners, ...editorLines.map(() => undefined)];
1183
+ // Match hit targets to the same viewport clipping as the visible lines.
1184
+ const start = overlayRows === undefined ? 0 : Math.max(0, allLines.length - Math.max(1, overlayRows));
1185
+ pointerTargets = allTargets.slice(start);
1186
+ pointerWidth = renderWidth(width);
1187
+ return allLines.slice(start).map((line, index) => {
1188
+ if (!pointer.hovered || pointerTargets[index] !== pointer.hovered) return line;
1189
+ if (pointer.hovered === BACK_TARGET) return line;
1190
+ return highlightPointerTarget(theme, line, width);
1191
+ });
1192
+ },
1193
+ handleMouse(event: WidgetMouseEvent): WidgetMouseResult | undefined {
1194
+ return pointer.handleMouse(event);
1136
1195
  },
1137
1196
  handleInput(data: string) {
1138
1197
  if (closed) return;
@@ -1151,7 +1210,7 @@ function createOverlayComponent(
1151
1210
  else if (deps.matchKey(data, "left")) {
1152
1211
  const mainIdx = overlayState.rows.findIndex((row) => row.parentRow && row.id === "main");
1153
1212
  if (mainIdx >= 0) selectOverlayRow(mainIdx);
1154
- close();
1213
+ backToMain();
1155
1214
  }
1156
1215
  else if (deps.matchKey(data, "enter")) {
1157
1216
  const row = selectedRow();
@@ -1179,8 +1238,7 @@ function createOverlayComponent(
1179
1238
  requestRender();
1180
1239
  }
1181
1240
  else if (deps.matchKey(data, "escape")) {
1182
- unfocusMainList();
1183
- close();
1241
+ backToMain();
1184
1242
  }
1185
1243
  },
1186
1244
  /** Show another row's detail in this overlay (the main list moved while it was unfocused). */
@@ -1195,8 +1253,10 @@ function createOverlayComponent(
1195
1253
  },
1196
1254
  /** Close the overlay (done), e.g. when the navigator is disposed. */
1197
1255
  dismiss() { close(); },
1198
- invalidate() { transcriptComponent?.invalidate(); },
1256
+ invalidate() { pointer.reset(); pointerTargets = []; transcriptComponent?.invalidate(); },
1199
1257
  dispose() {
1258
+ pointer.reset();
1259
+ pointerTargets = [];
1200
1260
  clearCloseArm();
1201
1261
  detailScheduler.dispose();
1202
1262
  },
@@ -1233,14 +1293,14 @@ function buildTranscriptDetailLines(
1233
1293
  width: number,
1234
1294
  truncate: (s: string, width: number) => string,
1235
1295
  fg: (color: string, value: string) => string,
1236
- options: { minRows?: number; maxRows?: number; bottomFooter?: boolean; logTailRows?: number } = {},
1296
+ options: { minRows?: number; maxRows?: number; bottomFooter?: boolean; logTailRows?: number; backControl?: string } = {},
1237
1297
  ): string[] {
1238
- const actions = [...(detail.footerActions ?? ["x close"]), "Esc close"].join(" · ");
1298
+ const actions = [...(detail.footerActions ?? ["x close"])].join(" · ");
1239
1299
  const tailRows = options.logTailRows ?? DEFAULT_LOG_TAIL_ROWS;
1240
1300
  let shownTranscriptLines = transcriptLines.length ? transcriptLines.slice(-tailRows) : [" (no transcript yet)"];
1241
1301
  const lines: string[] = [
1242
1302
  fg("accent", rule(detail.title, width)),
1243
- dim(` ← main · ${actions}`, fg),
1303
+ ` ${options.backControl ?? fg("accent", BACK_TO_MAIN)}${dim(` · ${actions}`, fg)}`,
1244
1304
  "",
1245
1305
  ` status ${fg(toneColor(detail.statusTone, detail.status), detail.status)}`,
1246
1306
  ];
@@ -1264,7 +1324,7 @@ function buildTranscriptDetailLines(
1264
1324
  lines.push(...shownTranscriptLines);
1265
1325
  lines.push("");
1266
1326
  if (options.bottomFooter === false) return lines.map((line) => safeTruncate(line, width, truncate));
1267
- const footerLines = [dim(` ← main · ${actions}`, fg), dim(rule("", width), fg)];
1327
+ const footerLines = [dim(` ${BACK_TO_MAIN} · ${actions}`, fg), dim(rule("", width), fg)];
1268
1328
  padBeforeFooter(lines, footerLines.length, options.minRows);
1269
1329
  lines.push(...footerLines);
1270
1330
  return lines.map((line) => safeTruncate(line, width, truncate));
@@ -1320,10 +1380,10 @@ function buildDetailLines(
1320
1380
  width: number,
1321
1381
  truncate: (s: string, width: number) => string,
1322
1382
  fg: (color: string, value: string) => string,
1323
- options: { expandedSections?: Set<string>; logTailRows?: number; minRows?: number; maxRows?: number; bottomFooter?: boolean } = {},
1383
+ options: { expandedSections?: Set<string>; logTailRows?: number; minRows?: number; maxRows?: number; bottomFooter?: boolean; backControl?: string } = {},
1324
1384
  ): string[] {
1325
1385
  if (!detail) {
1326
- const lines = [rule("Work unavailable", width), dim(" ← back · Esc close", fg), ""];
1386
+ const lines = [rule("Work unavailable", width), ` ${options.backControl ?? fg("accent", BACK_TO_MAIN)}`, ""];
1327
1387
  if (options.bottomFooter === false) return lines.map((line) => safeTruncate(line, width, truncate));
1328
1388
  const footerLines = [dim(rule("", width), fg)];
1329
1389
  padBeforeFooter(lines, footerLines.length, options.minRows);
@@ -1336,8 +1396,8 @@ function buildDetailLines(
1336
1396
  const foldedAction = toggleableSectionId
1337
1397
  ? (options.expandedSections?.has(toggleableSectionId) ? "Enter collapse" : "Enter expand")
1338
1398
  : null;
1339
- const actions = [foldedAction, ...(detail.footerActions?.length ? detail.footerActions : ["x close"]), "l 10/25", "Esc close"].filter(Boolean).join(" · ");
1340
- lines.push(dim(` ← back · ${actions}`, fg));
1399
+ const actions = [foldedAction, ...(detail.footerActions?.length ? detail.footerActions : ["x close"]), "l 10/25"].filter(Boolean).join(" · ");
1400
+ lines.push(` ${options.backControl ?? fg("accent", BACK_TO_MAIN)}${dim(` · ${actions}`, fg)}`);
1341
1401
  lines.push("");
1342
1402
  lines.push(` status ${fg(toneColor(detail.statusTone, detail.status), detail.status)}`);
1343
1403
  if (detail.subtitle) lines.push(` summary ${detail.subtitle}`);
@@ -1393,7 +1453,7 @@ function buildDetailLines(
1393
1453
  }
1394
1454
  lines.push("");
1395
1455
  if (options.bottomFooter === false) return lines.map((line) => safeTruncate(line, width, truncate));
1396
- const footerLines = [dim(` ← back · ${actions}`, fg), dim(rule("", width), fg)];
1456
+ const footerLines = [dim(` ${BACK_TO_MAIN} · ${actions}`, fg), dim(rule("", width), fg)];
1397
1457
  padBeforeFooter(lines, footerLines.length, options.minRows);
1398
1458
  lines.push(...footerLines);
1399
1459
  return lines.map((line) => safeTruncate(line, width, truncate));
@@ -25,6 +25,23 @@ launch starts it again. No separate background task or coordinator heartbeat is
25
25
  needed. Completion delivery retries use the callback batcher's own timer. Lost-run
26
26
  alerts retain supervision until their handoff is handled, so lost work is not silent.
27
27
 
28
+ ## Worker Ownership
29
+
30
+ Every child launch, including clean, batch and unsandboxed launches, loads a Pi
31
+ hook that keeps external CI/PR monitoring with its coordinator. Workers finish
32
+ their assigned implementation or diagnosis, run focused checks, push only when
33
+ authorized, write the result, and return. Pending CI is a valid handoff, not a pass.
34
+
35
+ The hook refuses recognized `gh run watch`, `gh pr checks --watch`, and bundled
36
+ delivery-watcher commands through `bash` and `run_application`. Short status reads,
37
+ failed-log retrieval, and legitimate long local tests and builds remain allowed.
38
+ If background tools are explicitly available to a child, recognized watcher
39
+ commands are refused there too. Coordinator sessions are unaffected.
40
+
41
+ This is an operational ownership guard, not a security boundary or a classifier
42
+ for arbitrary programs. Indirect scripts and custom polling implementations still
43
+ rely on the worker handoff contract. Command permissions and timeouts are unchanged.
44
+
28
45
  ## Install
29
46
 
30
47
  ```sh
@@ -0,0 +1,130 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { parse } from "shell-quote";
3
+ import { STEER_FILE_ENV } from "./child-steer.ts";
4
+
5
+ export const WORKER_HANDOFF = "Worker ownership: complete your assigned implementation or diagnosis, run focused checks, push only when authorized, write the requested result, then return. Pending CI is a handoff, not a failed implementation or a CI pass. The coordinator owns CI/PR monitoring, review scheduling, readiness, enqueueing and merge. Do not start external delivery watchers or background monitoring. Short status reads and failed-log retrieval for diagnosis are allowed. Legitimate local tests and builds may run to completion within their command limits.";
6
+ const REFUSAL = "Worker ownership: CI/PR monitoring belongs to the coordinator. Return the PR/run ID, pushed HEAD and current evidence with CI pending; do not retry the watcher through another tool or script. Local tests/builds and short diagnostic status/log reads remain allowed.";
7
+ const WATCH_SCRIPTS = new Set(["watch-pr-delivery.mjs", "watch-delivery.mjs", "watch-candidate-delivery.mjs"]);
8
+ const nameOf = (value: string) => value.replaceAll("\\", "/").split("/").at(-1)!.replace(/\.exe$/i, "");
9
+
10
+ // Preserve command boundaries without treating multiline strings or heredoc data as commands.
11
+ function commandLines(command: string): string[] {
12
+ const lines: string[] = [];
13
+ const documents: { delimiter: string; tabs: boolean }[] = [];
14
+ let quote = "", escaped = false, out = "";
15
+ for (const line of command.split("\n")) {
16
+ if (documents.length) {
17
+ const document = documents[0]!;
18
+ if ((document.tabs ? line.replace(/^\t+/, "") : line) === document.delimiter) documents.shift();
19
+ continue;
20
+ }
21
+ let comment = false;
22
+ for (const char of `${line}\n`) {
23
+ if (!quote && !escaped && char === "#" && (!out || /[\s;&|()<>]$/.test(out))) comment = true;
24
+ if (comment && char !== "\n") continue;
25
+ if (escaped) { out += char === "\n" ? "" : `\\${char}`; escaped = false; continue; }
26
+ if (char === "\\" && quote !== "'") { escaped = true; continue; }
27
+ if (char === quote) quote = "";
28
+ else if (!quote && (char === "'" || char === '"')) quote = char;
29
+ if (char !== "\n" || quote) { out += char; continue; }
30
+ lines.push(out);
31
+ try {
32
+ const tokens = parse(out);
33
+ for (let i = 0; i < tokens.length; i++) {
34
+ const token = tokens[i], delimiter = tokens[i + 1];
35
+ if (typeof token === "object" && "op" in token && ["<<", "<<-"].includes(token.op) && typeof delimiter === "string") {
36
+ documents.push({ delimiter, tabs: token.op === "<<-" });
37
+ }
38
+ }
39
+ } catch { /* The command tool, not this operational guard, validates shell syntax. */ }
40
+ out = "";
41
+ }
42
+ }
43
+ if (out) lines.push(out);
44
+ return lines;
45
+ }
46
+
47
+ function shellWatcher(command: string, depth: number): boolean {
48
+ if (depth > 3) return false;
49
+ let tokens;
50
+ try { tokens = commandLines(command).flatMap((line) => [...parse(line), { op: ";" as const }]); } catch { return false; }
51
+ let words: string[] = [];
52
+ let redirect = false;
53
+ for (const token of tokens) {
54
+ if (typeof token === "string" || ("op" in token && token.op === "glob")) {
55
+ if (redirect) { redirect = false; continue; }
56
+ words.push(typeof token === "string" ? token : token.pattern);
57
+ } else if ("op" in token && ["<", ">", ">>", ">&", "<&", ">|", "<>", "&>", "&>>", "<<", "<<-", "<<<"].includes(token.op)) {
58
+ if (words.length === 1 && /^\d+$/.test(words[0]!)) words = [];
59
+ redirect = true;
60
+ } else {
61
+ if (argvWatcher(words, depth + 1)) return true;
62
+ words = [];
63
+ redirect = false;
64
+ }
65
+ }
66
+ return argvWatcher(words, depth + 1);
67
+ }
68
+
69
+ function argvWatcher(input: readonly string[], depth = 0): boolean {
70
+ if (depth > 4) return false;
71
+ let words = [...input];
72
+ while (/^[A-Za-z_][A-Za-z_0-9]*=/.test(words[0] ?? "")) words.shift();
73
+ if (nameOf(words[0] ?? "") === "env") {
74
+ words.shift();
75
+ while (words.length) {
76
+ if (["-u", "--unset", "-C", "--chdir"].includes(words[0]!)) words.splice(0, 2);
77
+ else if (words[0] === "--" || words[0]!.startsWith("-") || /^[A-Za-z_][A-Za-z_0-9]*=/.test(words[0]!)) words.shift();
78
+ else break;
79
+ }
80
+ }
81
+ const program = nameOf(words.shift() ?? "");
82
+ if (["bash", "sh", "zsh", "dash"].includes(program)) {
83
+ const at = words.findIndex((word) => /^-[^-]*c/.test(word));
84
+ return at >= 0 && typeof words[at + 1] === "string" && shellWatcher(words[at + 1]!, depth + 1);
85
+ }
86
+ if (words.includes("--help") || words.includes("-h")) return false;
87
+ if (program === "gh") {
88
+ while (words.length) {
89
+ if (["--repo", "-R", "--hostname"].includes(words[0]!)) words.splice(0, 2);
90
+ else if (/^(?:--repo=.+|--hostname=.+|-R.+)$/.test(words[0]!)) words.shift();
91
+ else break;
92
+ }
93
+ return (words[0] === "run" && words[1] === "watch") ||
94
+ (words[0] === "pr" && words[1] === "checks" && words.slice(2).some((word) => ["--watch", "--watch=true"].includes(word)));
95
+ }
96
+ if (WATCH_SCRIPTS.has(program)) return true;
97
+ if (program === "node" || program === "nodejs") {
98
+ while (words[0]?.startsWith("-")) {
99
+ if (["-e", "--eval", "-p", "--print"].includes(words[0])) return false;
100
+ const takesValue = ["--import", "--loader", "--require", "-r"].includes(words[0]);
101
+ words.splice(0, takesValue ? 2 : 1);
102
+ }
103
+ return WATCH_SCRIPTS.has(nameOf(words[0] ?? ""));
104
+ }
105
+ return false;
106
+ }
107
+
108
+ /** Recognizes supported watcher entrypoints, not arbitrary program semantics or a security boundary. */
109
+ export function workerMonitoringRefusal(toolName: string, input: unknown): string | undefined {
110
+ if (!input || typeof input !== "object") return;
111
+ const value = input as Record<string, unknown>;
112
+ const argv = toolName === "run_application" && typeof value.executable === "string" &&
113
+ (value.args === undefined || (Array.isArray(value.args) && value.args.every((arg) => typeof arg === "string")))
114
+ ? [value.executable, ...((value.args ?? []) as string[])] : undefined;
115
+ if (argv && argvWatcher(argv)) return REFUSAL;
116
+ if (["bash", "remote_bash", "bg_task", "bg_task_spawn", "bg_task_watch"].includes(toolName)) {
117
+ if (typeof value.command === "string" && shellWatcher(value.command, 0)) return REFUSAL;
118
+ if (Array.isArray(value.argv) && value.argv.every((arg) => typeof arg === "string") && argvWatcher(value.argv)) return REFUSAL;
119
+ }
120
+ }
121
+
122
+ export default function childOwnership(pi: Pick<ExtensionAPI, "on">, env: NodeJS.ProcessEnv = process.env): void {
123
+ // This marker is supplied by the parent on every child launch, including clean/unsandboxed runs.
124
+ if (!env[STEER_FILE_ENV]) return;
125
+ pi.on("before_agent_start", (event) => ({ systemPrompt: `${event.systemPrompt}\n\n${WORKER_HANDOFF}` }));
126
+ pi.on("tool_call", (event) => {
127
+ const reason = workerMonitoringRefusal(event.toolName, event.input);
128
+ if (reason) return { block: true, reason };
129
+ });
130
+ }
@@ -1786,9 +1786,10 @@ export default function (pi: ExtensionAPI) {
1786
1786
  const { args: resolvedExtArgs, missing } = extensionArgs(resolution, resolveExtensionPath);
1787
1787
  const trustedExtArgs = [...new Set(taskTools?.trusted.map((tool) => tool.loadPath) ?? [])]
1788
1788
  .filter((path) => !resolvedExtArgs.includes(path)).flatMap((path) => ["--extension", path]);
1789
- // The harness's own child control extension (no tools, no command I/O) rides along in every
1790
- // mode, including clean: it is how a soft-deadline steer reaches the child session.
1791
- const extArgs = [...resolvedExtArgs, ...trustedExtArgs, "--extension", childSteerExtensionPath()]
1789
+ // Harness child controls ride along in every mode, including clean and unsandboxed runs.
1790
+ // They deliver deadline steers and keep external monitoring with the coordinator.
1791
+ const extArgs = [...resolvedExtArgs, ...trustedExtArgs,
1792
+ "--extension", childSteerExtensionPath(), "--extension", join(selfDir(), "child-ownership.ts")]
1792
1793
  .map((value, index, all) => sandboxEnabled && all[index - 1] === "--extension" ? canonicalizePath(value) : value);
1793
1794
  if (sandboxEnabled && resolution.mode === "inherit") {
1794
1795
  throw new Error("Task confinement requires explicit extensions; inheritExtensions is unsupported while sandboxing is enabled.");
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-subagents",
3
- "version": "0.22.1",
3
+ "version": "0.22.2",
4
4
  "description": "Pi extension for detached, sandboxed subagent runs that keep the foreground session free.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -62,6 +62,7 @@
62
62
  },
63
63
  "dependencies": {
64
64
  "proper-lockfile": "^4.1.2",
65
+ "shell-quote": "1.12.0",
65
66
  "smol-toml": "1.9.0",
66
67
  "yaml": "^2.9.1"
67
68
  }
@@ -348,20 +348,60 @@ type MainListWidget = Component & {
348
348
  handleMouse(event: WidgetMouseEvent): WidgetMouseResult | undefined;
349
349
  };
350
350
 
351
+ const BACK_TO_MAIN = "← main (Esc)";
352
+ const BACK_TARGET = "back-to-main";
353
+
354
+ function highlightPointerTarget(theme: unknown, line: string, width: number): string {
355
+ const colors = theme as { bg?(color: string, value: string): string } | undefined;
356
+ return colors?.bg?.("selectedBg", line + " ".repeat(Math.max(0, width - visibleWidth(line))))
357
+ ?? themeFgFromTheme(theme)("accent", line);
358
+ }
359
+
360
+ function createPointerNavigation(targetAt: (event: WidgetMouseEvent) => string | undefined, activate: (target: string) => boolean) {
361
+ let hovered: string | undefined;
362
+ let pressed: { target: string; y: number } | undefined;
363
+ return {
364
+ get hovered() { return hovered; },
365
+ reset() { hovered = undefined; pressed = undefined; },
366
+ handleMouse(event: WidgetMouseEvent): WidgetMouseResult | undefined {
367
+ if (event.type === "move" || event.type === "leave") {
368
+ const target = event.type === "leave" ? undefined : targetAt(event);
369
+ const changed = hovered !== target;
370
+ hovered = target;
371
+ return changed || target ? { handled: true, render: changed } : undefined;
372
+ }
373
+ if (event.button !== "left" || event.shift || event.alt || event.ctrl) {
374
+ pressed = undefined;
375
+ return undefined;
376
+ }
377
+ if (event.type === "press") {
378
+ const target = targetAt(event);
379
+ pressed = target ? { target, y: event.y } : undefined;
380
+ return target ? { handled: true, render: false } : undefined;
381
+ }
382
+ if (event.type !== "click") return undefined;
383
+ const claim = pressed;
384
+ pressed = undefined;
385
+ if (!claim || claim.y !== event.y || targetAt(event) !== claim.target || !activate(claim.target)) return undefined;
386
+ return { handled: true, render: true };
387
+ },
388
+ };
389
+ }
390
+
351
391
  function createMainListWidget(tui: { requestRender?(): void }, theme: unknown, deps: HostDeps): MainListWidget {
352
392
  const requestRender = () => tui.requestRender?.();
353
393
  state().mainListRequestRender = requestRender;
354
394
  // navigatorId per rendered line, so a pointer row resolves by structure, never by row text.
355
395
  let lineOwners: Array<string | undefined> = [];
356
396
  let lineWidth = MAIN_LIST_FALLBACK_WIDTH;
357
- // Pi synthesizes a click only for the component that handled the initial press.
358
- let pressedRow: { navigatorId: string; y: number } | undefined;
359
397
  const rowAt = (event: WidgetMouseEvent): string | undefined => {
360
398
  const width = Number.isFinite(event.width) && event.width > 0 ? Math.floor(event.width) : lineWidth;
361
399
  if (!Number.isFinite(event.x) || event.x < 0 || event.x >= width) return undefined;
362
400
  if (!Number.isInteger(event.y) || event.y < 0 || event.y >= lineOwners.length) return undefined;
363
- return lineOwners[event.y];
401
+ const id = lineOwners[event.y];
402
+ return id && listRows().some((row) => row.navigatorId === id) ? id : undefined;
364
403
  };
404
+ const pointer = createPointerNavigation(rowAt, openNavigatorRow);
365
405
  return {
366
406
  render(width: number): string[] {
367
407
  const rows = listRows();
@@ -369,7 +409,7 @@ function createMainListWidget(tui: { requestRender?(): void }, theme: unknown, d
369
409
  if (!rows.length) {
370
410
  state().lastMainListLines = undefined;
371
411
  lineOwners = [];
372
- pressedRow = undefined;
412
+ pointer.reset();
373
413
  return [];
374
414
  }
375
415
  const layout = buildMainListLayout(rows, renderWidth(width), deps.truncate, themeFgFromTheme(theme), {
@@ -379,41 +419,24 @@ function createMainListWidget(tui: { requestRender?(): void }, theme: unknown, d
379
419
  lineWidth = renderWidth(width);
380
420
  // The leading gap line belongs to the widget too, so its slot stays ownerless.
381
421
  lineOwners = [undefined, ...layout.owners];
382
- const lines = ["", ...layout.lines];
422
+ const lines = ["", ...layout.lines].map((line, index) =>
423
+ pointer.hovered && lineOwners[index] === pointer.hovered ? highlightPointerTarget(theme, line, lineWidth) : line);
383
424
  state().lastMainListLines = lines;
384
425
  return lines;
385
426
  },
386
427
  handleMouse(event: WidgetMouseEvent): WidgetMouseResult | undefined {
387
- // Only an unmodified left press is ours; every other gesture reaches the host untouched.
388
- if (event.button !== "left" || event.shift === true || event.alt === true || event.ctrl === true) {
389
- pressedRow = undefined;
390
- return undefined;
391
- }
392
- if (event.type === "press") {
393
- pressedRow = undefined;
394
- const navigatorId = rowAt(event);
395
- if (!navigatorId || !listRows().some((row) => row.navigatorId === navigatorId)) return undefined;
396
- pressedRow = { navigatorId, y: event.y };
397
- return { handled: true, render: false };
398
- }
399
- if (event.type !== "click") return undefined;
400
- const pressed = pressedRow;
401
- pressedRow = undefined;
402
- // A click after another component's press, or after the layout moved, is not the row we claimed.
403
- if (!pressed || pressed.y !== event.y || rowAt(event) !== pressed.navigatorId) return undefined;
404
- if (!openNavigatorRow(pressed.navigatorId)) return undefined;
405
- return { handled: true, render: true };
428
+ return pointer.handleMouse(event);
406
429
  },
407
430
  invalidate() {
408
431
  state().lastMainListLines = undefined;
409
432
  lineOwners = [];
410
- pressedRow = undefined;
433
+ pointer.reset();
411
434
  },
412
435
  dispose() {
413
436
  const s = state();
414
437
  if (s.mainListRequestRender === requestRender) s.mainListRequestRender = undefined;
415
438
  lineOwners = [];
416
- pressedRow = undefined;
439
+ pointer.reset();
417
440
  },
418
441
  };
419
442
  }
@@ -958,6 +981,22 @@ function createOverlayComponent(
958
981
  let closed = false;
959
982
  let transcriptDetail: BackgroundWorkDetail | null = null;
960
983
  let transcriptComponent: Component | null = null;
984
+ let pointerTargets: Array<string | undefined> = [];
985
+ let pointerWidth = 0;
986
+ const pointer = createPointerNavigation((event) => {
987
+ if (closed || !Number.isInteger(event.y) || !Number.isFinite(event.x) || event.x < 0 || event.x >= pointerWidth) return undefined;
988
+ const target = pointerTargets[event.y];
989
+ if (target === BACK_TARGET) return event.x >= 3 && event.x < 3 + visibleWidth(BACK_TO_MAIN) ? target : undefined;
990
+ return target && listRows().some((row) => row.navigatorId === target) ? target : undefined;
991
+ }, (target) => {
992
+ if (target === BACK_TARGET) { backToMain(); return true; }
993
+ refreshRows();
994
+ const index = overlayState.rows.findIndex((row) => row.navigatorId === target);
995
+ if (index < 0) return false;
996
+ selectOverlayRow(index);
997
+ openDetail();
998
+ return true;
999
+ });
961
1000
 
962
1001
  const fg = (color: string, value: string) => theme?.fg ? theme.fg(color, value) : value;
963
1002
 
@@ -1036,6 +1075,11 @@ function createOverlayComponent(
1036
1075
  try { done(null); } catch { /* ignore */ }
1037
1076
  }
1038
1077
 
1078
+ function backToMain(): void {
1079
+ unfocusMainList();
1080
+ close();
1081
+ }
1082
+
1039
1083
  function handleCloseKey(): void {
1040
1084
  const row = selectedRow();
1041
1085
  if (!row || row.parentRow) return;
@@ -1101,14 +1145,17 @@ function createOverlayComponent(
1101
1145
  render(width: number) {
1102
1146
  closeIfEditorReplaced();
1103
1147
  refreshRows();
1104
- const railLines = buildMainListLines(overlayState.rows, width, deps.truncate, fg, {
1148
+ const rail = buildMainListLayout(overlayState.rows, width, deps.truncate, fg, {
1105
1149
  selectedId: selectedRow()?.navigatorId,
1106
1150
  focused: true,
1107
1151
  });
1152
+ const railLines = rail.lines;
1108
1153
  const editorLines = renderEditorLines(width);
1109
1154
  const bottomLines = [...railLines, ...editorLines];
1110
1155
  const overlayRows = state().detailOverlayRows;
1111
1156
  const detailRows = overlayRows === undefined ? undefined : Math.max(1, overlayRows - bottomLines.length);
1157
+ const backControl = pointer.hovered === BACK_TARGET
1158
+ ? highlightPointerTarget(theme, BACK_TO_MAIN, visibleWidth(BACK_TO_MAIN)) : undefined;
1112
1159
  let contentLines: string[];
1113
1160
  if (detail?.transcript && deps.createTranscriptComponent) {
1114
1161
  if (transcriptDetail !== detail || !transcriptComponent) {
@@ -1121,18 +1168,30 @@ function createOverlayComponent(
1121
1168
  width,
1122
1169
  deps.truncate,
1123
1170
  fg,
1124
- { minRows: detailRows, maxRows: detailRows, bottomFooter: false, logTailRows },
1171
+ { minRows: detailRows, maxRows: detailRows, bottomFooter: false, logTailRows, backControl },
1125
1172
  );
1126
1173
  } else {
1127
1174
  transcriptDetail = null;
1128
1175
  transcriptComponent = null;
1129
- contentLines = buildDetailLines(detail, width, deps.truncate, fg, { expandedSections, logTailRows, minRows: detailRows, maxRows: detailRows, bottomFooter: false });
1176
+ contentLines = buildDetailLines(detail, width, deps.truncate, fg, { expandedSections, logTailRows, minRows: detailRows, maxRows: detailRows, bottomFooter: false, backControl });
1130
1177
  }
1131
- if (overlayRows === undefined || detailRows === undefined) return [...contentLines, ...bottomLines];
1132
- const fittedContent = contentLines.slice(0, detailRows);
1133
- while (fittedContent.length < detailRows) fittedContent.push("");
1134
- // A terminal shorter than rail + editor keeps the bottom rows (the input frame) and drops the top.
1135
- return [...fittedContent, ...bottomLines].slice(-Math.max(1, overlayRows));
1178
+ const fittedContent = detailRows === undefined ? contentLines : contentLines.slice(0, detailRows);
1179
+ if (detailRows !== undefined) while (fittedContent.length < detailRows) fittedContent.push("");
1180
+ const contentTargets = fittedContent.map((_, index) => index === 1 ? BACK_TARGET : undefined);
1181
+ const allLines = [...fittedContent, ...bottomLines];
1182
+ const allTargets = [...contentTargets, ...rail.owners, ...editorLines.map(() => undefined)];
1183
+ // Match hit targets to the same viewport clipping as the visible lines.
1184
+ const start = overlayRows === undefined ? 0 : Math.max(0, allLines.length - Math.max(1, overlayRows));
1185
+ pointerTargets = allTargets.slice(start);
1186
+ pointerWidth = renderWidth(width);
1187
+ return allLines.slice(start).map((line, index) => {
1188
+ if (!pointer.hovered || pointerTargets[index] !== pointer.hovered) return line;
1189
+ if (pointer.hovered === BACK_TARGET) return line;
1190
+ return highlightPointerTarget(theme, line, width);
1191
+ });
1192
+ },
1193
+ handleMouse(event: WidgetMouseEvent): WidgetMouseResult | undefined {
1194
+ return pointer.handleMouse(event);
1136
1195
  },
1137
1196
  handleInput(data: string) {
1138
1197
  if (closed) return;
@@ -1151,7 +1210,7 @@ function createOverlayComponent(
1151
1210
  else if (deps.matchKey(data, "left")) {
1152
1211
  const mainIdx = overlayState.rows.findIndex((row) => row.parentRow && row.id === "main");
1153
1212
  if (mainIdx >= 0) selectOverlayRow(mainIdx);
1154
- close();
1213
+ backToMain();
1155
1214
  }
1156
1215
  else if (deps.matchKey(data, "enter")) {
1157
1216
  const row = selectedRow();
@@ -1179,8 +1238,7 @@ function createOverlayComponent(
1179
1238
  requestRender();
1180
1239
  }
1181
1240
  else if (deps.matchKey(data, "escape")) {
1182
- unfocusMainList();
1183
- close();
1241
+ backToMain();
1184
1242
  }
1185
1243
  },
1186
1244
  /** Show another row's detail in this overlay (the main list moved while it was unfocused). */
@@ -1195,8 +1253,10 @@ function createOverlayComponent(
1195
1253
  },
1196
1254
  /** Close the overlay (done), e.g. when the navigator is disposed. */
1197
1255
  dismiss() { close(); },
1198
- invalidate() { transcriptComponent?.invalidate(); },
1256
+ invalidate() { pointer.reset(); pointerTargets = []; transcriptComponent?.invalidate(); },
1199
1257
  dispose() {
1258
+ pointer.reset();
1259
+ pointerTargets = [];
1200
1260
  clearCloseArm();
1201
1261
  detailScheduler.dispose();
1202
1262
  },
@@ -1233,14 +1293,14 @@ function buildTranscriptDetailLines(
1233
1293
  width: number,
1234
1294
  truncate: (s: string, width: number) => string,
1235
1295
  fg: (color: string, value: string) => string,
1236
- options: { minRows?: number; maxRows?: number; bottomFooter?: boolean; logTailRows?: number } = {},
1296
+ options: { minRows?: number; maxRows?: number; bottomFooter?: boolean; logTailRows?: number; backControl?: string } = {},
1237
1297
  ): string[] {
1238
- const actions = [...(detail.footerActions ?? ["x close"]), "Esc close"].join(" · ");
1298
+ const actions = [...(detail.footerActions ?? ["x close"])].join(" · ");
1239
1299
  const tailRows = options.logTailRows ?? DEFAULT_LOG_TAIL_ROWS;
1240
1300
  let shownTranscriptLines = transcriptLines.length ? transcriptLines.slice(-tailRows) : [" (no transcript yet)"];
1241
1301
  const lines: string[] = [
1242
1302
  fg("accent", rule(detail.title, width)),
1243
- dim(` ← main · ${actions}`, fg),
1303
+ ` ${options.backControl ?? fg("accent", BACK_TO_MAIN)}${dim(` · ${actions}`, fg)}`,
1244
1304
  "",
1245
1305
  ` status ${fg(toneColor(detail.statusTone, detail.status), detail.status)}`,
1246
1306
  ];
@@ -1264,7 +1324,7 @@ function buildTranscriptDetailLines(
1264
1324
  lines.push(...shownTranscriptLines);
1265
1325
  lines.push("");
1266
1326
  if (options.bottomFooter === false) return lines.map((line) => safeTruncate(line, width, truncate));
1267
- const footerLines = [dim(` ← main · ${actions}`, fg), dim(rule("", width), fg)];
1327
+ const footerLines = [dim(` ${BACK_TO_MAIN} · ${actions}`, fg), dim(rule("", width), fg)];
1268
1328
  padBeforeFooter(lines, footerLines.length, options.minRows);
1269
1329
  lines.push(...footerLines);
1270
1330
  return lines.map((line) => safeTruncate(line, width, truncate));
@@ -1320,10 +1380,10 @@ function buildDetailLines(
1320
1380
  width: number,
1321
1381
  truncate: (s: string, width: number) => string,
1322
1382
  fg: (color: string, value: string) => string,
1323
- options: { expandedSections?: Set<string>; logTailRows?: number; minRows?: number; maxRows?: number; bottomFooter?: boolean } = {},
1383
+ options: { expandedSections?: Set<string>; logTailRows?: number; minRows?: number; maxRows?: number; bottomFooter?: boolean; backControl?: string } = {},
1324
1384
  ): string[] {
1325
1385
  if (!detail) {
1326
- const lines = [rule("Work unavailable", width), dim(" ← back · Esc close", fg), ""];
1386
+ const lines = [rule("Work unavailable", width), ` ${options.backControl ?? fg("accent", BACK_TO_MAIN)}`, ""];
1327
1387
  if (options.bottomFooter === false) return lines.map((line) => safeTruncate(line, width, truncate));
1328
1388
  const footerLines = [dim(rule("", width), fg)];
1329
1389
  padBeforeFooter(lines, footerLines.length, options.minRows);
@@ -1336,8 +1396,8 @@ function buildDetailLines(
1336
1396
  const foldedAction = toggleableSectionId
1337
1397
  ? (options.expandedSections?.has(toggleableSectionId) ? "Enter collapse" : "Enter expand")
1338
1398
  : null;
1339
- const actions = [foldedAction, ...(detail.footerActions?.length ? detail.footerActions : ["x close"]), "l 10/25", "Esc close"].filter(Boolean).join(" · ");
1340
- lines.push(dim(` ← back · ${actions}`, fg));
1399
+ const actions = [foldedAction, ...(detail.footerActions?.length ? detail.footerActions : ["x close"]), "l 10/25"].filter(Boolean).join(" · ");
1400
+ lines.push(` ${options.backControl ?? fg("accent", BACK_TO_MAIN)}${dim(` · ${actions}`, fg)}`);
1341
1401
  lines.push("");
1342
1402
  lines.push(` status ${fg(toneColor(detail.statusTone, detail.status), detail.status)}`);
1343
1403
  if (detail.subtitle) lines.push(` summary ${detail.subtitle}`);
@@ -1393,7 +1453,7 @@ function buildDetailLines(
1393
1453
  }
1394
1454
  lines.push("");
1395
1455
  if (options.bottomFooter === false) return lines.map((line) => safeTruncate(line, width, truncate));
1396
- const footerLines = [dim(` ← back · ${actions}`, fg), dim(rule("", width), fg)];
1456
+ const footerLines = [dim(` ${BACK_TO_MAIN} · ${actions}`, fg), dim(rule("", width), fg)];
1397
1457
  padBeforeFooter(lines, footerLines.length, options.minRows);
1398
1458
  lines.push(...footerLines);
1399
1459
  return lines.map((line) => safeTruncate(line, width, truncate));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-harness",
3
- "version": "0.34.1",
3
+ "version": "0.34.2",
4
4
  "description": "Pi extension bundle for a write sandbox, subagents, background tasks, SSH, goals, and Outcome & Todo.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -53,12 +53,13 @@
53
53
  "test": "node --import tsx --import ../../scripts/isolate-registry.mjs --test test/*.test.mjs test/task-context/*.test.ts"
54
54
  },
55
55
  "dependencies": {
56
- "pi-better-background-tasks": "0.12.0",
56
+ "pi-better-background-tasks": "0.12.1",
57
57
  "pi-better-goal": "0.10.3",
58
58
  "pi-better-sandbox": "0.14.0",
59
59
  "pi-better-ssh": "0.1.3",
60
- "pi-better-subagents": "0.22.1",
60
+ "pi-better-subagents": "0.22.2",
61
61
  "proper-lockfile": "^4.1.2",
62
+ "shell-quote": "1.12.0",
62
63
  "smol-toml": "1.9.0",
63
64
  "yaml": "^2.9.1"
64
65
  },