@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.
- package/README.md +18 -36
- package/index.ts +92 -250
- 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 **
|
|
4
|
-
|
|
5
|
-
|
|
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` |
|
|
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
|
-
##
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
97
|
-
read
|
|
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
|
|
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,
|
|
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; //
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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 →
|
|
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).\
|
|
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
|
-
(
|
|
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
|
-
|
|
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,
|
|
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
|
-
"
|
|
1346
|
-
"
|
|
1347
|
-
"
|
|
1348
|
-
"
|
|
1349
|
-
"
|
|
1350
|
-
"
|
|
1351
|
-
"
|
|
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
|
|
1257
|
+
"Run any shell command with context-safe captured output; quick commands return metadata, longer ones continue in background",
|
|
1357
1258
|
promptGuidelines: [
|
|
1358
|
-
"
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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.
|
|
4
|
-
"description": "Run
|
|
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",
|