@yusukeshib/pi-babysit 0.6.0 → 0.6.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.
Files changed (3) hide show
  1. package/README.md +26 -11
  2. package/index.ts +134 -32
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -56,24 +56,27 @@ programs** (installers, wizards, REPLs): type with `babysit_send`
56
56
 
57
57
  | Tool | What it does |
58
58
  | ---- | ------------ |
59
- | `babysit_run` | Run any command (`command`, optional `name`/`pty`/`timeout`/`idleTimeout`/`retryOnWorkerDeath`/`notificationGroup`). Set `foreground: true` for one process or subagent whose result is needed in the same tool call; use `returnPattern`/`returnLines`/`maxBytes` to keep noisy process output bounded. Or start a named background subagent (`profile: "subagent"`, `task`, optional `name`/`agent`/`model`/`tools`/`maxDepth` and budget fields), then always collect it with `babysit_wait`. `maxDepth` defaults to 1. Quick commands return inline; longer process runs notify in the background |
59
+ | `babysit_run` | Run any command (`command`, optional `name`/`pty`/`timeout`/`idleTimeout`/`retryOnWorkerDeath`/`notificationGroup`). Set `foreground: true` for one process or subagent whose result is needed in the same tool call; use `returnPattern`/`returnLines`/`maxBytes` to keep noisy process output bounded. Or start a named background subagent (`profile: "subagent"`, `task`, optional `name`/`agent`/`model`/`tools`/`maxDepth` and budget fields), then always collect it with `babysit_wait`. `maxDepth` defaults to 1. Subagent `continueAfterStart: true` is accepted as a compatibility alias for this default background behavior. Quick commands return inline; longer process runs notify in the background |
60
60
  | `babysit_check` | Without an id, list sessions with state/kind filters. With an id, inspect bounded output, search with `pattern`, or capture a TUI with `screen: true`; `maxBytes` overrides the 4 KB default up to 24 KB |
61
61
  | `babysit_send` | Process: type `text` / press `keys` into the PTY. Subagent: steer mid-run, or send a follow-up task when confirmed settled (`mode: auto/steer/task`); explicit task mode rejects busy, parked, or unknown state |
62
62
  | `babysit_wait` | Block until done: process exit (or `expect: "regex"` readiness marker), subagent task completion. Multi-wait: up to 32 unique `ids` + `mode: "any"\|"all"` |
63
63
  | `babysit_kill` | Terminate a session, verify terminal state, then suppress the exit notification |
64
64
 
65
65
  The built-in `bash` tool is removed from the active tool set so the model does
66
- not waste a failed tool turn before choosing `babysit_run`. A fallback
67
- `tool_call` hook still blocks direct shell calls if another extension or preset
68
- re-enables `bash`, including shell backgrounding (`… &`, `nohup`, `setsid`,
69
- `disown`). Set `PI_BABYSIT_ALLOW_BASH=1` to retain direct `bash` explicitly.
66
+ not waste a failed tool turn before choosing `babysit_run`. Process commands
67
+ retain the built-in Bash tool's dynamic Pi metadata environment:
68
+ `PI_SESSION_ID`, `PI_SESSION_FILE`, `PI_PROVIDER`, `PI_MODEL`, and
69
+ `PI_REASONING_LEVEL`. A fallback `tool_call` hook still blocks direct shell
70
+ calls if another extension or preset re-enables `bash`, including shell
71
+ backgrounding (`… &`, `nohup`, `setsid`, `disown`). Set
72
+ `PI_BABYSIT_ALLOW_BASH=1` to retain direct `bash` explicitly.
70
73
 
71
74
  ## Commands (human)
72
75
 
73
76
  | Command | What it does |
74
77
  | ------- | ------------ |
75
78
  | `/babysit` | Arrow-key picker over all sessions. Renders an **inline snapshot** (no tmux): running **process** → current rendered screen + recent output + a copy-paste `babysit attach` take-over hint (detach `Ctrl-\ Ctrl-\`); running **subagent** → read-only progress (RPC stdin stays untouchable); finished → summary. Re-run `/babysit` to refresh |
76
- | `/babysit gc [days]` | Preview and confirm deletion of old Pi-session roots (default 14 days). Active leases, live supervisor/child PIDs, unknown states, the current root, and recent roots are retained; deletion uses a GC lock and atomic rename |
79
+ | `/babysit gc [days]` | Preview and confirm deletion of old Pi-session roots (default 14 days). Active leases, live supervisor/child PIDs, unknown states, the current root, and recent roots are retained; old empty roots are eligible once their lease is gone. Deletion uses a GC lock and atomic rename |
77
80
 
78
81
  A widget below the editor separates session **kind** from task **state** at a glance:
79
82
  summary counts use `RUNNING` / `IDLE`, and every row is labeled
@@ -127,6 +130,14 @@ suite at the end. Background subagents must likewise be collected before the
127
130
  parent task finishes so their answer and nested usage are not lost.
128
131
  Set `PI_BABYSIT_ALLOW_BASH=1` only as an explicit emergency escape hatch.
129
132
 
133
+ ## Pi shutdown and reload
134
+
135
+ A real Pi quit terminates every running process and subagent in the current Pi
136
+ session namespace. The workers are killed concurrently, so shutdown time does
137
+ not grow linearly when many sessions are active. Lifecycle transitions that keep
138
+ Pi running—`/reload`, `/new`, `/resume`, and `/fork`—leave detached workers
139
+ alone. Reloading or resuming the same Pi session reconnects to its namespace.
140
+
130
141
  ## Unexpected worker loss
131
142
 
132
143
  If the babysit supervisor disappears without recording an exit, pi-babysit
@@ -149,7 +160,9 @@ because blindly rerunning an arbitrary command can duplicate side effects.
149
160
  currently running member to stop and then share one notification even when
150
161
  their exits span multiple polls.
151
162
  `babysit_kill` and an exit already reported by `babysit_wait` suppress the
152
- notification.
163
+ notification. Kill results are reconciled with persisted terminal state, so
164
+ a backend escalation warning that arrives after the child has already exited
165
+ does not produce a false failure followed by a redundant notification.
153
166
  - **Subagent**: `foreground: true` or `babysit_wait` blocks on
154
167
  `babysit expect '"type":"agent_settled"'`. A background task that settles or
155
168
  exits before it is collected emits one ready-to-collect reminder; the parent
@@ -169,10 +182,12 @@ grace window (`PI_BABYSIT_REAP_AFTER`, default 120s) using the same parked-turn
169
182
  rule, so a subagent waiting on a long build is never false-killed. Give bounded
170
183
  recon/review tasks at least one cost, turn, tool-call, or token budget; omit
171
184
  budgets only for intentionally open-ended work. Optional task budgets are
172
- observed by the parent poller. At 80% of a limit the worker is steered to wrap
173
- up; reaching the configured limit starts the hard grace immediately, even if a
174
- wedged worker cannot accept steering. An in-flight model call or parallel tool
175
- batch can still overshoot before the next poll. If the worker remains active
185
+ observed by the parent poller. `maxUsageTokens` counts cumulative input, output,
186
+ and cache-token usage, so size it above the worker's initial context and
187
+ expected calls. At 80% of a limit the worker is steered to wrap up; reaching the
188
+ configured limit starts the hard grace immediately, even if a wedged worker
189
+ cannot accept steering. An in-flight model call or parallel tool batch can
190
+ still overshoot before the next poll. If the worker remains active
176
191
  after `PI_BABYSIT_BUDGET_GRACE`, termination is verified before it is marked
177
192
  budget-killed. Usage shown by check/wait is cumulative, and the first terminal
178
193
  wait for each task charges that nested usage exactly once to the parent Pi
package/index.ts CHANGED
@@ -220,9 +220,15 @@ function bs(
220
220
  resolve({ stdout: "", stderr: "aborted", code: 130 });
221
221
  return;
222
222
  }
223
+ const env: NodeJS.ProcessEnv = { ...process.env };
224
+ for (const [name, value] of Object.entries(opts.env ?? {})) {
225
+ if (value === undefined) delete env[name];
226
+ else env[name] = value;
227
+ }
228
+ env.BABYSIT_DIR = ROOT;
223
229
  const child = spawn(BABYSIT_BIN, args, {
224
230
  cwd: opts.cwd,
225
- env: { ...process.env, ...opts.env, BABYSIT_DIR: ROOT },
231
+ env,
226
232
  });
227
233
  let stdout = "";
228
234
  let stderr = "";
@@ -236,7 +242,12 @@ function bs(
236
242
  });
237
243
  child.on("error", (e) => {
238
244
  opts.signal?.removeEventListener("abort", onAbort);
239
- resolve({ stdout, stderr: stderr + String(e), code: 1 });
245
+ const installHint = babysitSpawnInstallHint(e);
246
+ if (installHint) {
247
+ babysitPreflightError = installHint;
248
+ babysitPreflightCheckedAt = Date.now();
249
+ }
250
+ resolve({ stdout, stderr: installHint ?? stderr + String(e), code: 1 });
240
251
  });
241
252
  child.on("close", (code) => {
242
253
  opts.signal?.removeEventListener("abort", onAbort);
@@ -261,6 +272,10 @@ const INSTALL_HINT =
261
272
  `The \`babysit\` binary was not found (tried "${BABYSIT_BIN}").\n` + INSTALL_STEPS;
262
273
  const MIN_BABYSIT_VERSION = [0, 13, 0] as const;
263
274
 
275
+ export function babysitSpawnInstallHint(error: unknown): string | null {
276
+ return (error as NodeJS.ErrnoException | undefined)?.code === "ENOENT" ? INSTALL_HINT : null;
277
+ }
278
+
264
279
  export function isSupportedBabysitVersion(output: string): boolean {
265
280
  const match = /\b(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?\b/.exec(output);
266
281
  if (!match) return false;
@@ -271,7 +286,8 @@ export function isSupportedBabysitVersion(output: string): boolean {
271
286
  return match[4] === undefined;
272
287
  }
273
288
 
274
- // Cached preflight — probe `babysit --version` exactly once per process.
289
+ // Cached preflight. A supported binary stays cached until a later spawn reports
290
+ // ENOENT; failures are retried after a short delay so installation can recover.
275
291
  // undefined = not probed, null = supported, string = actionable error.
276
292
  let babysitPreflightError: string | null | undefined;
277
293
  let babysitPreflightCheckedAt = 0;
@@ -355,6 +371,22 @@ export function validateKillResponse(stdout: string): string | null {
355
371
  }
356
372
  }
357
373
 
374
+ export function resolveKillConfirmation(
375
+ id: string,
376
+ backendError: string | null,
377
+ state: string | undefined,
378
+ ): { confirmed: true; warning?: string } | { confirmed: false; error: string } {
379
+ if (state && isConfirmedTerminalState(state)) {
380
+ return backendError ? { confirmed: true, warning: backendError } : { confirmed: true };
381
+ }
382
+ if (backendError) return { confirmed: false, error: backendError };
383
+ if (!state) return { confirmed: false, error: `Kill could not be verified: session ${id} disappeared.` };
384
+ return {
385
+ confirmed: false,
386
+ error: `Kill was acknowledged but ${id} is still ${state}; completion notifications were restored.`,
387
+ };
388
+ }
389
+
358
390
  async function awaitConfirmedTermination(id: string): Promise<BsSession | null> {
359
391
  const initial = await statusOf(id);
360
392
  if (!initial || isConfirmedTerminalState(initial.state) || initial.state === "dead") {
@@ -367,6 +399,35 @@ async function awaitConfirmedTermination(id: string): Promise<BsSession | null>
367
399
  return statusOf(id);
368
400
  }
369
401
 
402
+ export function shouldTerminateSessionsOnShutdown(reason: string | undefined): boolean {
403
+ return reason === "quit";
404
+ }
405
+
406
+ async function terminateRunningSessions(): Promise<void> {
407
+ let listed: Awaited<ReturnType<typeof listSessions>>;
408
+ try {
409
+ listed = await listSessions();
410
+ } catch {
411
+ return;
412
+ }
413
+ if (listed.error) return;
414
+
415
+ // A namespace may contain many independent commands and agents. Terminate
416
+ // them concurrently so quitting Pi does not wait for serial kill timeouts.
417
+ // allSettled ensures one unreachable worker cannot skip cleanup for others.
418
+ await Promise.allSettled(
419
+ listed.sessions
420
+ .filter((session) => session.state === "running")
421
+ .map(async (session) => {
422
+ const result = await bs(["kill", "-s", session.id, "--json"]);
423
+ if (result.code !== 0 || validateKillResponse(result.stdout)) return;
424
+ // A later resume of this Pi session must not turn intentional shutdown
425
+ // cleanup into a process-completion notification.
426
+ suppressNotify(session.id, "kill");
427
+ }),
428
+ );
429
+ }
430
+
370
431
  // ---------------------------------------------------------------------------
371
432
  // per-session metadata
372
433
  // ---------------------------------------------------------------------------
@@ -562,9 +623,12 @@ function gcRootIsSafe(root: string): boolean {
562
623
  let sessionDirs: fs.Dirent[];
563
624
  try {
564
625
  sessionDirs = fs.readdirSync(sessionsDir, { withFileTypes: true });
565
- } catch {
566
- return false;
626
+ } catch (error) {
627
+ // session_start acquires a lease before the first worker exists. Once that
628
+ // lease is gone, an old root with no sessions is safe to collect.
629
+ return (error as NodeJS.ErrnoException).code === "ENOENT";
567
630
  }
631
+ if (!sessionDirs.some((entry) => entry.isDirectory())) return true;
568
632
  let sawStatus = false;
569
633
  for (const sessionEntry of sessionDirs) {
570
634
  if (!sessionEntry.isDirectory()) continue;
@@ -1864,12 +1928,26 @@ interface ProcOpts {
1864
1928
  name?: string;
1865
1929
  command: string;
1866
1930
  cwd: string;
1931
+ env?: Record<string, string | undefined>;
1867
1932
  timeout?: string; // default: none — dev servers may run indefinitely
1868
1933
  idleTimeout?: string;
1869
1934
  pty: boolean;
1870
1935
  notificationGroup?: string;
1871
1936
  }
1872
1937
 
1938
+ export function processSessionEnvironment(
1939
+ ctx: ExtensionContext,
1940
+ reasoningLevel = (ctx as ExtensionContext & { thinkingLevel?: string }).thinkingLevel,
1941
+ ): Record<string, string | undefined> {
1942
+ return {
1943
+ PI_SESSION_ID: ctx.sessionManager.getSessionId(),
1944
+ PI_SESSION_FILE: ctx.sessionManager.getSessionFile(),
1945
+ PI_PROVIDER: ctx.model?.provider,
1946
+ PI_MODEL: ctx.model?.id,
1947
+ PI_REASONING_LEVEL: reasoningLevel,
1948
+ };
1949
+ }
1950
+
1873
1951
  async function spawnProcess(opts: ProcOpts): Promise<{ id: string } | { error: string }> {
1874
1952
  const bsArgs = ["run", "-d", "--json", "--size", "120x40"];
1875
1953
  if (!opts.pty) bsArgs.push("--no-tty");
@@ -1882,7 +1960,7 @@ async function spawnProcess(opts: ProcOpts): Promise<{ id: string } | { error: s
1882
1960
 
1883
1961
  let r: Awaited<ReturnType<typeof bs>>;
1884
1962
  try {
1885
- r = await bs(bsArgs, { cwd: opts.cwd });
1963
+ r = await bs(bsArgs, { cwd: opts.cwd, env: opts.env });
1886
1964
  } finally {
1887
1965
  if (reservedId) reservedSessionIds.delete(reservedId);
1888
1966
  }
@@ -2814,6 +2892,21 @@ export function automaticNotificationGroup(entry: unknown): string | undefined {
2814
2892
  return runs.length >= 2 ? group : undefined;
2815
2893
  }
2816
2894
 
2895
+ export function prepareBabysitRunArguments(args: unknown): unknown {
2896
+ if (!args || typeof args !== "object") return args;
2897
+ const input = args as Record<string, unknown>;
2898
+ if (
2899
+ input.profile === "subagent" &&
2900
+ input.continueAfterStart === true &&
2901
+ input.foreground !== true
2902
+ ) {
2903
+ const prepared = { ...input };
2904
+ delete prepared.continueAfterStart;
2905
+ return prepared;
2906
+ }
2907
+ return args;
2908
+ }
2909
+
2817
2910
  export function resolveSubagentSendMode(
2818
2911
  requested: "auto" | "steer" | "task",
2819
2912
  streaming?: boolean,
@@ -3338,9 +3431,15 @@ export default function (pi: ExtensionAPI) {
3338
3431
  }, POLL_MS);
3339
3432
  });
3340
3433
 
3341
- pi.on("session_shutdown", async () => {
3434
+ pi.on("session_shutdown", async (event) => {
3342
3435
  if (pollTimer) clearInterval(pollTimer);
3343
3436
  pollTimer = undefined;
3437
+ // Pi also emits session_shutdown while hot-reloading and switching sessions.
3438
+ // Preserve detached work for those lifecycle transitions; only a real Pi
3439
+ // quit owns and terminates every worker in the current session namespace.
3440
+ if (shouldTerminateSessionsOnShutdown(event?.reason)) {
3441
+ await terminateRunningSessions();
3442
+ }
3344
3443
  releaseRootLease(rootLeasePath);
3345
3444
  rootLeasePath = undefined;
3346
3445
  });
@@ -3395,14 +3494,18 @@ export default function (pi: ExtensionAPI) {
3395
3494
  promptSnippet: "Run supervised commands or bounded pi subagents with context-safe logs",
3396
3495
  promptGuidelines: [
3397
3496
  "Use babysit_run for shell commands and give meaningful sessions a stable name; bundle tiny related read-only observations into one command.",
3398
- "Use babysit_run foreground mode for one process or subagent whose result is needed now; never issue sibling foreground runs in parallel. For parallel checks, start background runs with continueAfterStart and collect them with one multi-session babysit_wait.",
3497
+ "Use babysit_run foreground mode for one process or subagent whose result is needed now; never issue sibling foreground runs in parallel. For parallel process checks, start background runs with continueAfterStart and collect them with one multi-session babysit_wait.",
3399
3498
  "Use returnPattern/returnLines for noisy commands. During edit/fix loops run targeted checks first and one full validation suite at the end instead of repeating every full gate.",
3400
- "After a background process starts, stop the turn for its automatic notification; never poll or sleep. Use continueAfterStart only for specific non-polling work.",
3499
+ "After a background process starts, stop the turn for its automatic notification; never poll or sleep. Use continueAfterStart only for specific non-polling process work.",
3401
3500
  "Inspect large logs with a narrow babysit_check pattern and maxBytes rather than broad tails.",
3402
3501
  "Use retryOnWorkerDeath only once and only for idempotent commands; retries may duplicate side effects.",
3403
- "Delegate independent work with bounded babysit_run subagents. Prefer foreground for one result needed now; every background subagent must be collected with babysit_wait before the parent task finishes. Size budgets above the worker's initial context and expected tool count.",
3502
+ "Delegate independent work with bounded babysit_run subagents. Prefer foreground for one result needed now; every background subagent must be collected with babysit_wait before the parent task finishes. Size budgets above the worker's initial context and expected tool count; maxUsageTokens counts cumulative input/cache tokens and can overshoot by one in-flight model call.",
3503
+ "Omit babysit_run.agent unless you know a named agent definition exists in the selected agentScope.",
3404
3504
  "Subagent recursion defaults to depth 1; only a top-level caller may explicitly raise maxDepth.",
3405
3505
  ],
3506
+ prepareArguments(args) {
3507
+ return prepareBabysitRunArguments(args) as never;
3508
+ },
3406
3509
  parameters: Type.Object({
3407
3510
  command: Type.Optional(
3408
3511
  Type.String({
@@ -3509,7 +3612,7 @@ export default function (pi: ExtensionAPI) {
3509
3612
  continueAfterStart: Type.Optional(
3510
3613
  Type.Boolean({
3511
3614
  description:
3512
- "Process mode only. Default false: starting a process ENDS the current turn (you are resumed by the exit notification). Set true only when you have immediate, specific, non-polling work to do after starting.",
3615
+ "Process mode. Default false: starting a process ENDS the current turn (you are resumed by the exit notification). Set true only for immediate, specific, non-polling process work. In subagent mode true is accepted as a compatibility alias for the default background behavior.",
3513
3616
  }),
3514
3617
  ),
3515
3618
  retryOnWorkerDeath: Type.Optional(
@@ -3570,14 +3673,7 @@ export default function (pi: ExtensionAPI) {
3570
3673
  details: {},
3571
3674
  };
3572
3675
  }
3573
- if (isSubagent && params.continueAfterStart != null) {
3574
- return {
3575
- content: [{ type: "text", text: "`continueAfterStart` is available only in process mode." }],
3576
- isError: true,
3577
- details: {},
3578
- };
3579
- }
3580
- if (!isSubagent && params.foreground && params.continueAfterStart) {
3676
+ if (params.foreground && params.continueAfterStart) {
3581
3677
  return {
3582
3678
  content: [{ type: "text", text: "`foreground` and `continueAfterStart` are mutually exclusive." }],
3583
3679
  isError: true,
@@ -3622,6 +3718,7 @@ export default function (pi: ExtensionAPI) {
3622
3718
  timeout: params.timeout,
3623
3719
  idleTimeout: params.idleTimeout,
3624
3720
  pty: params.pty ?? true,
3721
+ env: processSessionEnvironment(ctx, pi.getThinkingLevel()),
3625
3722
  notificationGroup: params.notificationGroup?.trim() || undefined,
3626
3723
  };
3627
3724
  let res = await spawnProcess(spawnOpts);
@@ -3750,7 +3847,7 @@ export default function (pi: ExtensionAPI) {
3750
3847
  const avail = agents.map((a) => a.name).join(", ") || "none";
3751
3848
  return {
3752
3849
  content: [
3753
- { type: "text", text: `Unknown agent "${params.agent}". Available: ${avail}.` },
3850
+ { type: "text", text: `Unknown agent "${params.agent}". Available: ${avail}. Omit \`agent\` to use the default subagent configuration.` },
3754
3851
  ],
3755
3852
  isError: true,
3756
3853
  details: {},
@@ -4546,29 +4643,34 @@ export default function (pi: ExtensionAPI) {
4546
4643
  };
4547
4644
 
4548
4645
  const r = await bs(["kill", "-s", params.id, "--json"]);
4549
- if (r.code !== 0) return fail((r.stderr || r.stdout || "kill failed").trim());
4550
-
4551
- const responseError = validateKillResponse(r.stdout);
4552
- if (responseError) return fail(responseError);
4553
-
4646
+ const backendError = r.code !== 0
4647
+ ? (r.stderr || r.stdout || "kill failed").trim()
4648
+ : validateKillResponse(r.stdout);
4649
+ // A backend can report an escalation error after the child has already
4650
+ // reached a persisted terminal state. Reconcile against authoritative
4651
+ // state before deciding whether to restore completion notifications.
4554
4652
  const status = await awaitConfirmedTermination(params.id);
4653
+ const confirmation = resolveKillConfirmation(params.id, backendError, status?.state);
4654
+ if (!confirmation.confirmed) return fail(confirmation.error, status);
4555
4655
  if (!status) return fail(`Kill could not be verified: session ${params.id} disappeared.`);
4556
- if (!isConfirmedTerminalState(status.state)) {
4557
- return fail(
4558
- `Kill was acknowledged but ${params.id} is still ${status.state}; completion notifications were restored.`,
4559
- status,
4560
- );
4561
- }
4562
4656
 
4563
4657
  suppressNotify(params.id, "kill");
4564
4658
  await refreshWidget(ctx);
4565
4659
  return {
4566
- content: [{ type: "text", text: `Killed ${params.id} (confirmed ${status.state}).` }],
4660
+ content: [
4661
+ {
4662
+ type: "text",
4663
+ text:
4664
+ `Killed ${params.id} (confirmed ${status.state}).` +
4665
+ (confirmation.warning ? ` Backend warning after termination: ${confirmation.warning}` : ""),
4666
+ },
4667
+ ],
4567
4668
  details: {
4568
4669
  id: params.id,
4569
4670
  status: status.state,
4570
4671
  exitCode: status.exit_code,
4571
4672
  logPath: logPath(params.id),
4673
+ backendWarning: confirmation.warning,
4572
4674
  },
4573
4675
  };
4574
4676
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yusukeshib/pi-babysit",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Run any shell command and pi subagents under babysit, with context-safe captured output.",
5
5
  "keywords": [
6
6
  "pi-package",