@yusukeshib/pi-babysit 0.2.4 → 0.3.0

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 +18 -36
  2. package/index.ts +92 -250
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  # pi-babysit
2
2
 
3
- A [pi](https://github.com/earendil-works/pi) extension that runs **anything
4
- long-lived** under [babysit](https://github.com/yusukeshib/babysit)-supervised
5
- PTY sessions — one substrate for background processes **and** pi subagents.
3
+ A [pi](https://github.com/earendil-works/pi) extension that runs **any shell
4
+ command** under [babysit](https://github.com/yusukeshib/babysit)-supervised
5
+ sessions — one context-safe substrate for quick commands, background processes,
6
+ and pi subagents.
6
7
  It retires both `@mjakl/pi-processes` (the `process` tool) and the old
7
8
  `pi-subagent` extension.
8
9
 
@@ -48,9 +49,8 @@ programs** (installers, wizards, REPLs): type with `babysit_send`
48
49
 
49
50
  | Tool | What it does |
50
51
  | ---- | ------------ |
51
- | `babysit_run` | Start a process (`command`, optional `name`/`pty`/`timeout`/`idleTimeout`) or a subagent (`profile: "subagent"`, `task`, optional `agent`/`model`/`tools`). Non-blocking; returns a session id |
52
+ | `babysit_run` | Run any command (`command`, optional `name`/`pty`/`timeout`/`idleTimeout`) or start a subagent (`profile: "subagent"`, `task`, optional `agent`/`model`/`tools`). Quick commands return inline; longer ones continue in the background |
52
53
  | `babysit_check` | List all sessions, or inspect one: process → state + log tail (or `screen: true` for TUIs); subagent → live progress (turns, recent tool calls, partial answer) |
53
- | `babysit_analyze` | Run a local JavaScript, Python, or shell analyzer over a process's complete captured log; only its bounded report returns to the model |
54
54
  | `babysit_send` | Process: type `text` / press `keys` into the PTY. Subagent: steer mid-run, or send a follow-up task when idle (`mode: auto/steer/task`) |
55
55
  | `babysit_wait` | Block until done: process exit (or `expect: "regex"` readiness marker), subagent task completion. Multi-wait: `ids` + `mode: "any"\|"all"` |
56
56
  | `babysit_kill` | Terminate a session (suppresses the exit notification) |
@@ -68,39 +68,21 @@ A `tool_call` hook also blocks bash commands that background themselves
68
68
  A minimal widget above the editor shows live counts
69
69
  (`N processes · M subagents working · K idle`).
70
70
 
71
- ## Log reports without context flooding
72
-
73
- A process run can include a `report` program. On completion, babysit writes the
74
- complete recorded log to a temporary file, runs the program in a separate local
75
- child process, and sends **only the program's bounded stdout** in the automatic
76
- completion notification. This avoids putting a full test/build log in the model
77
- context while preserving the original log for `babysit_check`, `/babysit`, or
78
- `babysit attach`.
79
-
80
- ```ts
81
- babysit_run({
82
- name: "test",
83
- command: "npm test",
84
- report: {
85
- language: "javascript",
86
- code: `
87
- const failures = FILE_CONTENT.split("\\n")
88
- .filter(line => /FAIL|Error:|✗/.test(line));
89
- console.log(`failures: ${failures.length}`);
90
- console.log(failures.slice(-20).join("\\n"));
91
- `,
92
- },
93
- });
71
+ ## Logs without context flooding
72
+
73
+ `babysit_run`, `babysit_wait`, and automatic completion notifications always
74
+ return lifecycle metadata and the absolute path to the complete `output.log`.
75
+ When the complete output is at most 8 KB it is returned inline; larger output
76
+ stays out of model context. Inspect large logs on demand with bounded shell
77
+ commands such as:
78
+
79
+ ```sh
80
+ tail -n 50 /path/to/output.log
81
+ rg -n 'FAIL|ERROR' /path/to/output.log
94
82
  ```
95
83
 
96
- JavaScript and Python reports receive `FILE_CONTENT` and `INPUT`; shell reports
97
- read the log path from `$BABYSIT_REPORT_INPUT`. `report.timeout` defaults to
98
- 30 seconds. Input is capped at 64 MiB and returned report output at 12 KB.
99
- Use `babysit_analyze` with the same `language`, `code`, and optional `timeout`
100
- fields to analyze a running process's output so far or re-analyze a completed
101
- process. Report code executes with your local user permissions, just like any
102
- other extension tool command; it is isolated from the pi extension host but is
103
- not an OS security sandbox.
84
+ `babysit_check { id, lines }` remains available as a convenient bounded tail.
85
+ Do not read a potentially large log file in full.
104
86
 
105
87
  ## How completion detection works
106
88
 
package/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
- * pi-babysit: run ANYTHING long-lived under babysit — one supervision substrate
3
- * for background processes AND pi subagents. Retires both `pi-processes`
2
+ * pi-babysit: run ANY shell command under babysit — one context-safe supervision
3
+ * substrate for quick commands, background processes, AND pi subagents. Retires both `pi-processes`
4
4
  * (the `process` tool) and `pi-subagent`.
5
5
  *
6
6
  * Every session is a babysit-supervised PTY (state in $PI_BABYSIT_DIR,
@@ -26,7 +26,7 @@
26
26
  * surface (babysit_run/check/send/wait/kill) covers both, and domain knowledge
27
27
  * (RPC bookkeeping, byte offsets, parked-turn detection) stays in code.
28
28
  *
29
- * Tools (LLM): babysit_run, babysit_check, babysit_analyze, babysit_send, babysit_wait, babysit_kill
29
+ * Tools (LLM): babysit_run, babysit_check, babysit_send, babysit_wait, babysit_kill
30
30
  * Commands: /babysit (arrow-key picker: attach/tail/inspect)
31
31
  * Widget: live counts (processes running · subagents working · idle)
32
32
  */
@@ -80,6 +80,7 @@ const SUBAGENT_GUIDANCE = [
80
80
  "your controller reads it from the event stream.",
81
81
  ].join(" ");
82
82
  const POLL_MS = 2500;
83
+ const QUICK_COMMAND_GRACE = process.env.PI_BABYSIT_QUICK_GRACE ?? "1s";
83
84
 
84
85
  interface BsSession {
85
86
  id: string;
@@ -225,21 +226,13 @@ async function statusOf(id: string): Promise<BsSession | null> {
225
226
  // kind=process: name/command + `notified` (exit notification dedup).
226
227
  // kind=subagent: task + the raw-log byte offset of the last prompt, which lets
227
228
  // check/wait analyze only the CURRENT task's events (important for follow-ups).
228
- type ReportLanguage = "javascript" | "python" | "shell";
229
-
230
- interface ReportSpec {
231
- language: ReportLanguage;
232
- code: string;
233
- timeout?: string;
234
- }
235
-
236
229
  interface Meta {
237
230
  kind: "process" | "subagent";
238
231
  // process
239
232
  name?: string;
240
233
  command?: string;
241
- report?: ReportSpec;
242
234
  notified?: boolean;
235
+ completionObservedAt?: number;
243
236
  startedAt?: number;
244
237
  // subagent
245
238
  task?: string;
@@ -248,6 +241,7 @@ interface Meta {
248
241
  }
249
242
 
250
243
  const metaDir = () => path.join(ROOT, "meta");
244
+ const logPath = (id: string) => path.join(ROOT, "sessions", id, "output.log");
251
245
 
252
246
  function writeMeta(id: string, m: Meta): void {
253
247
  try {
@@ -373,11 +367,9 @@ function parseDurMs(s?: string): number | null {
373
367
  // megabytes, so we also cap bytes, eliding the middle so both the head and
374
368
  // the tail of the output stay visible.
375
369
 
376
- const TAIL_MAX_BYTES = 8_000; // log tails / screens
370
+ const TAIL_MAX_BYTES = 8_000; // explicit tails / screens
371
+ const INLINE_OUTPUT_MAX_BYTES = 8_000; // complete output returned only below this threshold
377
372
  const ANSWER_MAX_BYTES = 24_000; // subagent answers / error messages
378
- const REPORT_MAX_INPUT_BYTES = 64 * 1024 * 1024; // analyzer input log
379
- const REPORT_MAX_OUTPUT_BYTES = 12_000; // analyzer stdout/stderr returned to the agent
380
- const REPORT_DEFAULT_TIMEOUT_MS = 30_000;
381
373
 
382
374
  function clip(s: string, maxBytes = TAIL_MAX_BYTES): string {
383
375
  const buf = Buffer.from(s, "utf8");
@@ -389,132 +381,27 @@ function clip(s: string, maxBytes = TAIL_MAX_BYTES): string {
389
381
  return `${head}\n… [${buf.length - maxBytes} bytes elided] …\n${tail}`;
390
382
  }
391
383
 
392
- // ---------------------------------------------------------------------------
393
- // Report analyzers — a report receives the complete captured process log, but
394
- // only its bounded stdout is returned to the model. It intentionally runs in
395
- // a child process: report code is arbitrary user/model-authored code and must
396
- // never be evaluated inside pi's extension host.
397
-
398
- interface ReportOutcome {
399
- ok: boolean;
400
- text: string;
401
- }
402
-
403
- async function saveSessionLog(id: string, destination: string, signal?: AbortSignal): Promise<string | null> {
404
- return new Promise((resolve) => {
405
- const child = spawn(BABYSIT_BIN, ["log", "-s", id], {
406
- env: { ...process.env, BABYSIT_DIR: ROOT },
407
- });
408
- const output = fs.createWriteStream(destination);
409
- let bytes = 0;
410
- let finished = false;
411
- const finish = (error: string | null) => {
412
- if (finished) return;
413
- finished = true;
414
- signal?.removeEventListener("abort", abort);
415
- output.end(() => resolve(error));
416
- };
417
- const abort = () => {
418
- child.kill("SIGTERM");
419
- finish("report analysis was cancelled");
420
- };
421
- if (signal?.aborted) return abort();
422
- signal?.addEventListener("abort", abort, { once: true });
423
- child.stdout?.on("data", (chunk: Buffer) => {
424
- if (finished) return;
425
- bytes += chunk.length;
426
- if (bytes > REPORT_MAX_INPUT_BYTES) {
427
- child.kill("SIGTERM");
428
- finish(`process log exceeds the ${REPORT_MAX_INPUT_BYTES / 1024 / 1024} MiB report limit`);
429
- return;
430
- }
431
- output.write(chunk);
432
- });
433
- child.on("error", (error) => finish(`could not read process log: ${error.message}`));
434
- child.on("close", (code) => {
435
- if (finished) return;
436
- finish(code === 0 ? null : `could not read process log (babysit log exited ${code ?? 1})`);
437
- });
438
- });
439
- }
440
-
441
- async function executeReport(spec: ReportSpec, inputPath: string, signal?: AbortSignal): Promise<ReportOutcome> {
442
- const dir = path.dirname(inputPath);
443
- const scriptPath = path.join(dir, spec.language === "python" ? "report.py" : spec.language === "shell" ? "report.sh" : "report.cjs");
444
- const script =
445
- spec.language === "javascript"
446
- ? `const fs = require("node:fs");\nconst FILE_CONTENT = fs.readFileSync(process.env.BABYSIT_REPORT_INPUT, "utf8");\nconst INPUT = FILE_CONTENT;\n(async () => {\n${spec.code}\n})().catch((error) => { console.error(error?.stack ?? String(error)); process.exitCode = 1; });\n`
447
- : spec.language === "python"
448
- ? `import os\nfrom pathlib import Path\nFILE_CONTENT = Path(os.environ["BABYSIT_REPORT_INPUT"]).read_text()\nINPUT = FILE_CONTENT\n${spec.code}\n`
449
- : `# Read the complete log from $BABYSIT_REPORT_INPUT.\n${spec.code}\n`;
450
- fs.writeFileSync(scriptPath, script, { mode: 0o700 });
451
- const command = spec.language === "javascript" ? process.execPath : spec.language === "python" ? "python3" : SHELL;
452
- const args = spec.language === "shell" ? [scriptPath] : [scriptPath];
453
- const timeoutMs = parseDurMs(spec.timeout) ?? REPORT_DEFAULT_TIMEOUT_MS;
454
-
455
- return new Promise((resolve) => {
456
- const child = spawn(command, args, {
457
- env: { ...process.env, BABYSIT_REPORT_INPUT: inputPath },
458
- });
459
- let stdout = "";
460
- let stderr = "";
461
- let outputTruncated = false;
462
- let timedOut = false;
463
- let settled = false;
464
- const append = (current: string, chunk: Buffer) => {
465
- const remaining = REPORT_MAX_OUTPUT_BYTES - Buffer.byteLength(current);
466
- if (remaining <= 0) {
467
- outputTruncated = true;
468
- return current;
469
- }
470
- const text = chunk.subarray(0, remaining).toString("utf8");
471
- if (text.length < chunk.length) outputTruncated = true;
472
- return current + text;
473
- };
474
- const finish = (code: number | null) => {
475
- if (settled) return;
476
- settled = true;
477
- clearTimeout(timer);
478
- signal?.removeEventListener("abort", abort);
479
- const body = [stdout.trim(), stderr.trim()].filter(Boolean).join("\n");
480
- const suffix = outputTruncated ? "\n[report output truncated]" : "";
481
- if (timedOut) {
482
- resolve({ ok: false, text: `Report timed out after ${Math.ceil(timeoutMs / 1000)}s.${body ? `\n${clip(body, REPORT_MAX_OUTPUT_BYTES)}` : ""}${suffix}` });
483
- } else if (code === 0) {
484
- resolve({ ok: true, text: `${body || "(report produced no output)"}${suffix}` });
485
- } else {
486
- resolve({ ok: false, text: `Report failed (exit ${code ?? 1}).${body ? `\n${clip(body, REPORT_MAX_OUTPUT_BYTES)}` : ""}${suffix}` });
487
- }
488
- };
489
- const abort = () => child.kill("SIGTERM");
490
- const timer = setTimeout(() => {
491
- timedOut = true;
492
- child.kill("SIGTERM");
493
- }, timeoutMs);
494
- if (signal?.aborted) abort();
495
- signal?.addEventListener("abort", abort, { once: true });
496
- child.stdout?.on("data", (chunk: Buffer) => { stdout = append(stdout, chunk); });
497
- child.stderr?.on("data", (chunk: Buffer) => { stderr = append(stderr, chunk); });
498
- child.on("error", (error) => {
499
- stderr = append(stderr, Buffer.from(error.message));
500
- finish(1);
501
- });
502
- child.on("close", finish);
503
- });
504
- }
505
-
506
- async function runReport(id: string, spec: ReportSpec, signal?: AbortSignal): Promise<ReportOutcome> {
507
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-babysit-report-"));
508
- try {
509
- const inputPath = path.join(dir, "process.log");
510
- const error = await saveSessionLog(id, inputPath, signal);
511
- if (error) return { ok: false, text: `Could not prepare report input: ${error}` };
512
- return await executeReport(spec, inputPath, signal);
513
- } finally {
514
- fs.rmSync(dir, { recursive: true, force: true });
384
+ async function inlineOutput(id: string, status: BsSession): Promise<string> {
385
+ let bytes = status.output_bytes;
386
+ if (bytes == null) {
387
+ try {
388
+ bytes = fs.statSync(logPath(id)).size;
389
+ } catch {
390
+ bytes = Number.POSITIVE_INFINITY;
391
+ }
392
+ }
393
+ if (bytes > INLINE_OUTPUT_MAX_BYTES) {
394
+ const size = Number.isFinite(bytes) ? `${bytes} bytes` : "size unavailable";
395
+ return `\nOutput omitted (${size}; inline limit ${INLINE_OUTPUT_MAX_BYTES}).`;
515
396
  }
397
+ const output = (await bs(["log", "-s", id])).stdout.trimEnd();
398
+ if (Buffer.byteLength(output) > INLINE_OUTPUT_MAX_BYTES) {
399
+ return `\nOutput omitted (exceeds inline limit ${INLINE_OUTPUT_MAX_BYTES} bytes).`;
400
+ }
401
+ return output ? `\n\nOutput:\n${output}` : "";
516
402
  }
517
403
 
404
+
518
405
  // ---------------------------------------------------------------------------
519
406
  // parked-turn detection (shared rule with self-reap.ts)
520
407
  // ---------------------------------------------------------------------------
@@ -691,7 +578,6 @@ interface ProcOpts {
691
578
  timeout?: string; // default: none — dev servers may run indefinitely
692
579
  idleTimeout?: string;
693
580
  pty: boolean;
694
- report?: ReportSpec;
695
581
  }
696
582
 
697
583
  async function spawnProcess(opts: ProcOpts): Promise<{ id: string } | { error: string }> {
@@ -721,7 +607,6 @@ async function spawnProcess(opts: ProcOpts): Promise<{ id: string } | { error: s
721
607
  kind: "process",
722
608
  name: opts.name ?? id,
723
609
  command: opts.command,
724
- report: opts.report,
725
610
  notified: false,
726
611
  startedAt: Date.now(),
727
612
  });
@@ -970,7 +855,7 @@ interface WaitOutcome {
970
855
  // Wait for ONE subagent's current task. Completion = an agent_end whose last
971
856
  // message is NOT a parked babysit_run/process toolResult (that one only means
972
857
  // "turn parked, waiting for a background process — pi resumes on its own").
973
- // Loop: analyze the current-task log slice; done → report; still going →
858
+ // Loop: analyze the current-task log slice; done → return; still going →
974
859
  // block on the next agent_end via `babysit expect` (race-free byte offsets).
975
860
  async function waitForTask(
976
861
  id: string,
@@ -1089,6 +974,14 @@ function suppressNotify(id: string): void {
1089
974
  }
1090
975
  }
1091
976
 
977
+ function enableNotify(id: string): void {
978
+ const meta = readMeta(id);
979
+ if (meta && meta.kind === "process" && meta.notified) {
980
+ meta.notified = false;
981
+ writeMeta(id, meta);
982
+ }
983
+ }
984
+
1092
985
  // Wait for a PROCESS session: either until a regex appears in its output
1093
986
  // (`expect` — e.g. "server listening") or until the process exits.
1094
987
  async function waitForExit(
@@ -1105,12 +998,11 @@ async function waitForExit(
1105
998
  return { id, kind: "interrupted", ok: false, text: `wait for ${id} was interrupted.` };
1106
999
  }
1107
1000
  if (e.code === 0) {
1108
- const tail = clip((await bs(["log", "-s", id, "--tail", "10"])).stdout.trim());
1109
1001
  return {
1110
1002
  id,
1111
1003
  kind: "done",
1112
1004
  ok: true,
1113
- text: `Pattern /${expectPattern}/ matched in ${id} output (process still running).\n\nRecent output:\n${tail}`,
1005
+ text: `Pattern /${expectPattern}/ matched in ${id} output (process still running).\nLog: ${logPath(id)}`,
1114
1006
  };
1115
1007
  }
1116
1008
  const st0 = await statusOf(id);
@@ -1125,14 +1017,19 @@ async function waitForExit(
1125
1017
  }
1126
1018
  // fall through: session exited before the pattern appeared
1127
1019
  } else {
1020
+ // An explicit wait owns completion delivery. Mark it before blocking so
1021
+ // the exit poller cannot race us and inject a duplicate notification.
1022
+ suppressNotify(id);
1128
1023
  const w = await bs(["wait", "-s", id, "--timeout", t], { signal });
1129
1024
  if (signal?.aborted || w.code === 130) {
1025
+ enableNotify(id);
1130
1026
  return { id, kind: "interrupted", ok: false, text: `wait for ${id} was interrupted.` };
1131
1027
  }
1132
1028
  if (w.code === 124) {
1133
1029
  // 124 is ambiguous (timeout vs child exiting 124) — disambiguate.
1134
1030
  const st0 = await statusOf(id);
1135
1031
  if (st0?.state === "running") {
1032
+ enableNotify(id);
1136
1033
  return {
1137
1034
  id,
1138
1035
  kind: "timeout",
@@ -1150,9 +1047,8 @@ async function waitForExit(
1150
1047
  }
1151
1048
  suppressNotify(id); // the agent sees the exit here; don't notify again
1152
1049
  const meta = readMeta(id);
1153
- const report = meta?.report ? await runReport(id, meta.report, signal) : null;
1154
- const tail = report ? "" : clip((await bs(["log", "-s", id, "--tail", "20"])).stdout.trim());
1155
1050
  const ok = st.exit_code === 0;
1051
+ const output = await inlineOutput(id, st);
1156
1052
  return {
1157
1053
  id,
1158
1054
  kind: "exited",
@@ -1161,9 +1057,7 @@ async function waitForExit(
1161
1057
  `Process ${id}${meta?.command ? ` (${meta.command})` : ""} ` +
1162
1058
  `${ok ? "completed successfully" : `exited with code ${st.exit_code ?? "?"}`}` +
1163
1059
  `${expectPattern ? ` before /${expectPattern}/ appeared` : ""}.` +
1164
- (report
1165
- ? `\n\nReport${report.ok ? "" : " (failed)"}:\n${report.text}`
1166
- : tail ? `\n\nLast output:\n${tail}` : ""),
1060
+ `\nLog: ${logPath(id)}` + output,
1167
1061
  status: st,
1168
1062
  };
1169
1063
  }
@@ -1210,11 +1104,19 @@ export default function (pi: ExtensionAPI) {
1210
1104
  if (s.state === "running") continue;
1211
1105
  const meta = readMeta(s.id);
1212
1106
  if (!meta || meta.kind !== "process" || meta.notified) continue;
1107
+ // Delay delivery by one poll interval. This gives an agent that chose
1108
+ // babysit_wait immediately after babysit_run enough time to claim the
1109
+ // completion and suppress the otherwise duplicate automatic message.
1110
+ if (!meta.completionObservedAt) {
1111
+ meta.completionObservedAt = Date.now();
1112
+ writeMeta(s.id, meta);
1113
+ continue;
1114
+ }
1115
+ if (Date.now() - meta.completionObservedAt < POLL_MS) continue;
1213
1116
  meta.notified = true;
1214
1117
  writeMeta(s.id, meta);
1215
- const report = meta.report ? await runReport(s.id, meta.report) : null;
1216
- const tail = report ? "" : clip((await bs(["log", "-s", s.id, "--tail", "20"])).stdout.trim());
1217
1118
  const ok = s.exit_code === 0;
1119
+ const output = await inlineOutput(s.id, s);
1218
1120
  const runtime = meta.startedAt
1219
1121
  ? `${Math.round((Date.now() - meta.startedAt) / 1000)}s`
1220
1122
  : "?";
@@ -1227,13 +1129,10 @@ export default function (pi: ExtensionAPI) {
1227
1129
  {
1228
1130
  customType: "pi-babysit-process-end",
1229
1131
  content:
1230
- `${summary}\nCommand: ${meta.command ?? "?"}` +
1231
- (report
1232
- ? `\n\nReport${report.ok ? "" : " (failed)"}:\n${report.text}`
1233
- : tail ? `\n\nRecent output:\n${tail}` : "") +
1234
- `\n\nThis is the automatic process-end notification. Do not call babysit_check just to re-verify; use it once only if you need more logs for debugging.`,
1132
+ `${summary}\nCommand: ${meta.command ?? "?"}\nLog: ${logPath(s.id)}${output}` +
1133
+ `\n\nThis is the automatic process-end notification. Do not call babysit_check just to re-verify. Inspect the log only when needed, using bounded commands such as tail or rg; never read it in full.`,
1235
1134
  display: true,
1236
- details: { id: s.id, exitCode: s.exit_code, success: ok, runtime, report },
1135
+ details: { id: s.id, exitCode: s.exit_code, success: ok, runtime, logPath: logPath(s.id) },
1237
1136
  },
1238
1137
  { triggerTurn: true, deliverAs: "steer" },
1239
1138
  );
@@ -1342,22 +1241,25 @@ export default function (pi: ExtensionAPI) {
1342
1241
  name: "babysit_run",
1343
1242
  label: "Babysit: run",
1344
1243
  description:
1345
- "Start a supervised background session under babysit (NON-BLOCKING; returns a session id). " +
1346
- "In non-interactive mode (`pi -p`, no UI) process mode instead BLOCKS until the command " +
1347
- "exits and returns its output inline — there is no notification loop to resume a parked turn. " +
1348
- "Two modes: (1) `command` — run any long-lived or slow command (build, tests, dev server, " +
1349
- "watcher, interactive TUI) in a PTY; you get an AUTOMATIC notification when it exits, and " +
1350
- "you can type into it with babysit_send and read its screen with babysit_check. Optional `report` " +
1351
- "runs local code over the full captured log and returns only its concise output on completion. " +
1244
+ "Run any shell command in a supervised babysit session. Commands that finish within a short " +
1245
+ "grace period return completion metadata immediately; longer commands continue in the background " +
1246
+ "and trigger an automatic notification on exit. Complete output is returned inline only when it is " +
1247
+ "small; larger output stays in the log path for bounded inspection with tail or rg. " +
1248
+ "In non-interactive mode (`pi -p`, no UI), process mode blocks until exit because there is no " +
1249
+ "notification loop. Two modes: (1) `command` — run any shell command, including builds, tests, " +
1250
+ "dev servers, watchers, and interactive TUIs; you can type into it with babysit_send and read " +
1251
+ "its screen with babysit_check. " +
1252
+ "you can type into it with babysit_send and read its screen with babysit_check. " +
1352
1253
  "(2) `profile: \"subagent\"` + `task` — spawn a pi subagent that works on the task in the " +
1353
1254
  "background; poll with babysit_check, steer with babysit_send, block with babysit_wait, " +
1354
1255
  "stop with babysit_kill.",
1355
1256
  promptSnippet:
1356
- "Run a command or a pi subagent as a supervised background session (non-blocking); returns a session id",
1257
+ "Run any shell command with context-safe captured output; quick commands return metadata, longer ones continue in background",
1357
1258
  promptGuidelines: [
1358
- "Run every long-lived or slow command through babysit_run instead of bash: builds, test suites, dev servers, watchers, `tail -f`, anything expected to take more than a few seconds. Give it a clear stable `name` (e.g. cargo-build).",
1259
+ "Use babysit_run as the default for shell commands, not only long-running work. Small output is returned directly; large stdout/stderr stays out of model context in the returned log path. Give meaningful commands a clear stable `name`.",
1260
+ "Inspect a babysit log only with explicitly bounded commands such as `tail -n N` or focused `rg`; never read or cat a potentially large log in full. Those small inspection commands may use bash directly to avoid recursively creating babysit sessions.",
1359
1261
  "After babysit_run { command } starts a process, end your response immediately so the automatic process-end notification can resume you; NEVER poll with babysit_check or sleep. Set continueAfterStart: true only when you have immediate, specific, non-polling work to do next. Call babysit_wait when you must consume the result inside the current turn (optionally with `expect` to wait for a readiness line like 'listening on').",
1360
- "For a build/test/API command with potentially large output, give babysit_run a `report` program that extracts the needed facts from FILE_CONTENT; the automatic completion notification will contain only that report rather than a raw log tail.",
1262
+ "babysit_run returns only minimal lifecycle information and a path to the complete log. Inspect that file with bounded shell commands such as `tail` or `rg`; never read a potentially large log in full.",
1361
1263
  "babysit_run gives full PTY control: drive interactive programs (installers, wizards, REPLs) with babysit_send (text or named keys) and read the rendered screen with babysit_check { screen: true }.",
1362
1264
  "Delegate self-contained tasks (codebase recon, a parallelizable subtask, work that would pollute your context) with babysit_run { profile: \"subagent\", task }. Launch several for independent subtasks; they run concurrently.",
1363
1265
  "After spawning subagents, do not idle-wait and do not end your turn to wait for them: keep making progress, then call babysit_wait (ids + mode any/all) when you need their results. Steer or send follow-up tasks with babysit_send; kill runaways with babysit_kill.",
@@ -1417,20 +1319,7 @@ export default function (pi: ExtensionAPI) {
1417
1319
  "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.",
1418
1320
  }),
1419
1321
  ),
1420
- report: Type.Optional(
1421
- Type.Object({
1422
- language: StringEnum(["javascript", "python", "shell"] as const, {
1423
- description: "Language for the local report program.",
1424
- }),
1425
- code: Type.String({
1426
- description:
1427
- "Program that analyzes the complete captured log. JavaScript/Python receive FILE_CONTENT and INPUT strings; shell receives $BABYSIT_REPORT_INPUT. Print only the concise report.",
1428
- }),
1429
- timeout: Type.Optional(
1430
- Type.String({ description: "Report-program timeout (default 30s, e.g. '10s')." }),
1431
- ),
1432
- }),
1433
- ),
1322
+
1434
1323
  }),
1435
1324
  async execute(_id, params, _signal, _onUpdate, ctx) {
1436
1325
  await requireBabysit();
@@ -1456,13 +1345,6 @@ export default function (pi: ExtensionAPI) {
1456
1345
  details: {},
1457
1346
  };
1458
1347
  }
1459
- if (isSubagent && params.report) {
1460
- return {
1461
- content: [{ type: "text", text: "`report` is available only for process sessions." }],
1462
- isError: true,
1463
- details: {},
1464
- };
1465
- }
1466
1348
 
1467
1349
  // --- process mode ---
1468
1350
  if (!isSubagent) {
@@ -1473,8 +1355,7 @@ export default function (pi: ExtensionAPI) {
1473
1355
  timeout: params.timeout,
1474
1356
  idleTimeout: params.idleTimeout,
1475
1357
  pty: params.pty ?? true,
1476
- report: params.report,
1477
- });
1358
+ });
1478
1359
  if ("error" in res) {
1479
1360
  return {
1480
1361
  content: [{ type: "text", text: `Failed to start process: ${res.error}` }],
@@ -1496,7 +1377,23 @@ export default function (pi: ExtensionAPI) {
1496
1377
  return {
1497
1378
  content: [{ type: "text", text: outcome.text }],
1498
1379
  isError: !outcome.ok,
1499
- details: { id: res.id, kind: "process", command: params.command, report: params.report },
1380
+ details: { id: res.id, kind: "process", command: params.command, logPath: logPath(res.id) },
1381
+ };
1382
+ }
1383
+
1384
+ // Keep ordinary quick commands ergonomic. Give the process a short grace
1385
+ // period; if it exits, return only lifecycle metadata + log path now.
1386
+ // A timeout means it is genuinely background work and follows the normal
1387
+ // parked-turn / automatic-notification contract below.
1388
+ await bs(["wait", "-s", res.id, "--timeout", QUICK_COMMAND_GRACE], { signal: _signal });
1389
+ const quickStatus = await statusOf(res.id);
1390
+ if (quickStatus && quickStatus.state !== "running") {
1391
+ const outcome = await waitForExit(res.id, null, _signal);
1392
+ await refreshWidget(ctx);
1393
+ return {
1394
+ content: [{ type: "text", text: outcome.text }],
1395
+ isError: !outcome.ok,
1396
+ details: { id: res.id, kind: "process", command: params.command, logPath: logPath(res.id) },
1500
1397
  };
1501
1398
  }
1502
1399
 
@@ -1509,13 +1406,13 @@ export default function (pi: ExtensionAPI) {
1509
1406
  {
1510
1407
  type: "text",
1511
1408
  text:
1512
- `Process started (id: ${res.id}). ${NOTIFY_MARKER}\n${nextStep}\n` +
1409
+ `Process started (id: ${res.id}). ${NOTIFY_MARKER}\nLog: ${logPath(res.id)}\n${nextStep}\n` +
1513
1410
  `Inspect: babysit_check { id: "${res.id}" } (screen: true for TUIs) · ` +
1514
1411
  `Wait: babysit_wait { id: "${res.id}" } · Kill: babysit_kill { id: "${res.id}" }\n` +
1515
1412
  `Human can watch/take over: /babysit`,
1516
1413
  },
1517
1414
  ],
1518
- details: { id: res.id, kind: "process", command: params.command, report: params.report },
1415
+ details: { id: res.id, kind: "process", command: params.command, logPath: logPath(res.id) },
1519
1416
  // Do not return `terminate: true` here. In RPC/subagent hosts that hint
1520
1417
  // can shut down the hosting pi worker, whose process-tree cleanup then
1521
1418
  // kills the otherwise detached babysit supervisor and closes its PTY
@@ -1652,6 +1549,7 @@ export default function (pi: ExtensionAPI) {
1652
1549
  }
1653
1550
  if (st.exit_code != null) header += ` exit_code=${st.exit_code}`;
1654
1551
  if (meta?.command) header += `\ncommand: ${meta.command}`;
1552
+ header += `\nlog: ${logPath(params.id)}`;
1655
1553
  if (st.note) header += ` ⚑ ${st.note}`;
1656
1554
  parts.push(header);
1657
1555
  if (params.screen) {
@@ -1666,7 +1564,7 @@ export default function (pi: ExtensionAPI) {
1666
1564
  }
1667
1565
  return {
1668
1566
  content: [{ type: "text", text: parts.join("\n") }],
1669
- details: { status: st, kind: "process" },
1567
+ details: { status: st, kind: "process", logPath: logPath(params.id) },
1670
1568
  };
1671
1569
  }
1672
1570
 
@@ -1876,62 +1774,6 @@ export default function (pi: ExtensionAPI) {
1876
1774
  },
1877
1775
  });
1878
1776
 
1879
- // ----- babysit_analyze ----------------------------------------------------
1880
- pi.registerTool({
1881
- name: "babysit_analyze",
1882
- label: "Babysit: analyze",
1883
- description:
1884
- "Run a local analyzer over a babysit process's complete captured log. The raw log remains outside " +
1885
- "the model context; only the analyzer's bounded stdout is returned. JavaScript/Python receive " +
1886
- "FILE_CONTENT and INPUT strings; shell receives $BABYSIT_REPORT_INPUT. It can inspect a running " +
1887
- "session's output so far or a completed session.",
1888
- promptSnippet: "Analyze a babysit process log locally and return only a concise report",
1889
- promptGuidelines: [
1890
- "Use babysit_analyze to extract failures, counts, or other concise facts from a large babysit process log instead of calling babysit_check repeatedly or returning raw logs.",
1891
- ],
1892
- parameters: Type.Object({
1893
- id: Type.String({ description: "Process session id whose captured log should be analyzed." }),
1894
- language: StringEnum(["javascript", "python", "shell"] as const, {
1895
- description: "Language for the local analyzer program.",
1896
- }),
1897
- code: Type.String({
1898
- description:
1899
- "Analyzer program. JavaScript/Python receive FILE_CONTENT and INPUT strings; shell reads $BABYSIT_REPORT_INPUT. Print only the concise result.",
1900
- }),
1901
- timeout: Type.Optional(
1902
- Type.String({ description: "Analyzer timeout (default 30s, e.g. '10s')." }),
1903
- ),
1904
- }),
1905
- async execute(_id, params, signal) {
1906
- await requireBabysit();
1907
- const status = await statusOf(params.id);
1908
- if (!status) {
1909
- return {
1910
- content: [{ type: "text", text: `No such session: ${params.id}` }],
1911
- isError: true,
1912
- details: {},
1913
- };
1914
- }
1915
- if (kindOf(params.id) === "subagent") {
1916
- return {
1917
- content: [{ type: "text", text: "babysit_analyze supports process sessions only; use babysit_check for subagent progress." }],
1918
- isError: true,
1919
- details: {},
1920
- };
1921
- }
1922
- const report = await runReport(params.id, {
1923
- language: params.language,
1924
- code: params.code,
1925
- timeout: params.timeout,
1926
- }, signal);
1927
- return {
1928
- content: [{ type: "text", text: report.text }],
1929
- isError: !report.ok,
1930
- details: {},
1931
- };
1932
- },
1933
- });
1934
-
1935
1777
  // ----- babysit_wait -------------------------------------------------------
1936
1778
  pi.registerTool({
1937
1779
  name: "babysit_wait",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@yusukeshib/pi-babysit",
3
- "version": "0.2.4",
4
- "description": "Run long-lived processes and pi subagents under babysit, with bounded local reports over captured logs.",
3
+ "version": "0.3.0",
4
+ "description": "Run any shell command and pi subagents under babysit, with context-safe captured output.",
5
5
  "keywords": [
6
6
  "pi-package",
7
7
  "pi-extension",