@yusukeshib/pi-babysit 0.6.21 → 0.6.22
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 +33 -12
- package/index.ts +96 -44
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,8 +32,8 @@ reachable from anywhere (`~/.pi-babysit/<pi-session-id>/`). Two kinds:
|
|
|
32
32
|
|
|
33
33
|
| kind | started by | completion | on completion |
|
|
34
34
|
| ---- | ---------- | ---------- | ------------- |
|
|
35
|
-
| **process** | `babysit_run { command }` | process **exit** | automatic notification message (`triggerTurn`), batched for all exits observed in the same poll — the agent may end its turn after starting and is resumed on exit, same contract as the old `process` tool |
|
|
36
|
-
| **subagent** | `babysit_run { profile: "subagent", task }` | `agent_settled` in the RPC event stream (worker remains reusable during its idle grace) | `foreground: true` returns the answer and nested usage in the same tool call; background workers must be collected with `babysit_wait` before the parent finishes, and remain reusable until self-reap |
|
|
35
|
+
| **process** | `babysit_run { description, command }` | process **exit** | automatic notification message (`triggerTurn`), batched for all exits observed in the same poll — the agent may end its turn after starting and is resumed on exit, same contract as the old `process` tool |
|
|
36
|
+
| **subagent** | `babysit_run { description, profile: "subagent", task }` | `agent_settled` in the RPC event stream (worker remains reusable during its idle grace) | `foreground: true` returns the answer and nested usage in the same tool call; background workers must be collected with `babysit_wait` before the parent finishes, and remain reusable until self-reap |
|
|
37
37
|
|
|
38
38
|
The **profile is a tool parameter, not a separate tool set**: domain knowledge
|
|
39
39
|
(RPC bookkeeping, per-task byte offsets, parked-turn detection, PTY-safe
|
|
@@ -44,8 +44,8 @@ Subagent recursion is **disabled by default**: a top-level pi may create workers
|
|
|
44
44
|
(depth 1), but those workers cannot create more workers. A top-level caller can
|
|
45
45
|
explicitly opt in for a specific tree with `maxDepth: 2` (or higher) when it
|
|
46
46
|
creates the first worker. Descendants inherit that ceiling and cannot raise it.
|
|
47
|
-
Normal `babysit_run { command }` process execution remains available
|
|
48
|
-
depth.
|
|
47
|
+
Normal `babysit_run { description, command }` process execution remains available
|
|
48
|
+
at every depth.
|
|
49
49
|
|
|
50
50
|
Because sessions are real PTYs, the agent can also **drive interactive
|
|
51
51
|
programs** (installers, wizards, REPLs): type with `babysit_send`
|
|
@@ -56,7 +56,7 @@ 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`/`lifecycle`). 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. Foreground and non-interactive process waits default to lifecycle `attached`, which terminates the process tree if that owning tool call is interrupted; background starts default to `detached`. 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 |
|
|
59
|
+
| `babysit_run` | Run any command (`description` + `command`, optional `name`/`pty`/`timeout`/`idleTimeout`/`retryOnWorkerDeath`/`notificationGroup`/`lifecycle`). 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. Foreground and non-interactive process waits default to lifecycle `attached`, which terminates the process tree if that owning tool call is interrupted; background starts default to `detached`. Or start a named background subagent (`description`, `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"` |
|
|
@@ -71,6 +71,27 @@ calls if another extension or preset re-enables `bash`, including shell
|
|
|
71
71
|
backgrounding (`… &`, `nohup`, `setsid`, `disown`). Set
|
|
72
72
|
`PI_BABYSIT_ALLOW_BASH=1` to retain direct `bash` explicitly.
|
|
73
73
|
|
|
74
|
+
## Describe every run
|
|
75
|
+
|
|
76
|
+
Every `babysit_run` requires a non-blank `description`, for both commands and
|
|
77
|
+
subagents. Explain the purpose briefly in language the user can understand;
|
|
78
|
+
`name` remains a stable session identifier, while `command` / `task` contains
|
|
79
|
+
the actual execution instructions. Descriptions are trimmed before saving and
|
|
80
|
+
are display metadata, not shell code or instructions sent to the subagent.
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
babysit_run { description: "Run regression tests", command: "bun test", foreground: true }
|
|
84
|
+
babysit_run { description: "Review the authentication changes", profile: "subagent", task: "Review the authentication diff for security regressions", maxTurns: 5 }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
In Pi's main transcript the description is the prominent yellow heading, with
|
|
88
|
+
the execution kind in muted gray. The full command or task is always visible
|
|
89
|
+
in gray beneath it, even when tool results are collapsed. Session lists, the
|
|
90
|
+
widget, and completion notifications also show the description. Older saved sessions and tool calls
|
|
91
|
+
without one remain readable, falling back to their command or task. Callers
|
|
92
|
+
must supply a description for new runs; blank or missing values are rejected
|
|
93
|
+
before launch.
|
|
94
|
+
|
|
74
95
|
## Remote processes over SSH
|
|
75
96
|
|
|
76
97
|
Install a `babysit` CLI with `--host` support on both machines (tested with
|
|
@@ -79,8 +100,8 @@ SSH aliases, keys, known hosts, and ProxyJump are handled by babysit/SSH;
|
|
|
79
100
|
pi-babysit does not install remote software or copy credentials.
|
|
80
101
|
|
|
81
102
|
```text
|
|
82
|
-
babysit_run { host: "yusuke-t4", cwd: "/home/yusuke/project", command: "bun test", foreground: true, returnLines: 30 }
|
|
83
|
-
babysit_run { host: "user@server", command: "python3 -i", name: "remote-repl", continueAfterStart: true }
|
|
103
|
+
babysit_run { description: "Run tests on Linux", host: "yusuke-t4", cwd: "/home/yusuke/project", command: "bun test", foreground: true, returnLines: 30 }
|
|
104
|
+
babysit_run { description: "Open a remote Python REPL", host: "user@server", command: "python3 -i", name: "remote-repl", continueAfterStart: true }
|
|
84
105
|
babysit_check { id: "remote-repl", screen: true }
|
|
85
106
|
babysit_send { id: "remote-repl", text: "print(42)" }
|
|
86
107
|
babysit_kill { id: "remote-repl" }
|
|
@@ -132,9 +153,9 @@ summary counts use `RUNNING` / `IDLE`, and every row is labeled
|
|
|
132
153
|
Click the header to toggle to `babysits(n all)`, which also shows finished and failed
|
|
133
154
|
sessions, and click again to return to running-only. Both modes show newest first,
|
|
134
155
|
ten per page; `< 1/4 >` shows the current/total page when needed. Click the
|
|
135
|
-
arrows to page. Rows occupy one line, previewing the
|
|
136
|
-
|
|
137
|
-
replace the list with a tall **log viewer in the same widget area**, not an
|
|
156
|
+
arrows to page. Rows occupy one line, previewing the description (or the command
|
|
157
|
+
for older processes and task for older agents), with `…` when clipped. Click an
|
|
158
|
+
AGENT or PROCESS row to replace the list with a tall **log viewer in the same widget area**, not an
|
|
138
159
|
overlay or separate screen. `Esc` or the right-hand **Close** button restores
|
|
139
160
|
the list, preserving its mode and page. The viewer uses Pi's `ScrollView` for
|
|
140
161
|
scroll position and follow behavior, with a widget-local scrollbar styled using
|
|
@@ -269,7 +290,7 @@ because blindly rerunning an arbitrary command can duplicate side effects.
|
|
|
269
290
|
must still call `babysit_wait` so the answer and nested usage are persisted.
|
|
270
291
|
Unlike `agent_end`, `agent_settled` cannot precede an automatic retry,
|
|
271
292
|
compaction retry, or queued continuation. A settled run containing a
|
|
272
|
-
**parked** toolResult — a `babysit_run { command }` result carrying the
|
|
293
|
+
**parked** toolResult — a `babysit_run { description, command }` result carrying the
|
|
273
294
|
`[notify-on-exit]` marker (or the legacy `process` tool) — only means "turn
|
|
274
295
|
parked awaiting a process-exit notification; pi resumes on its own", so the
|
|
275
296
|
wait continues. The marker-bearing result may be followed by a short
|
|
@@ -280,7 +301,7 @@ because blindly rerunning an arbitrary command can duplicate side effects.
|
|
|
280
301
|
Subagents load `self-reap.ts`, which exits an idle finished subagent after a
|
|
281
302
|
grace window using the same parked-turn rule, so a subagent waiting on a long
|
|
282
303
|
build is never false-killed. Callers can set `reapAfter: "5m"` on
|
|
283
|
-
`babysit_run { profile: "subagent", task, reapAfter: "5m" }`. Omission uses
|
|
304
|
+
`babysit_run { description, profile: "subagent", task, reapAfter: "5m" }`. Omission uses
|
|
284
305
|
`PI_BABYSIT_REAP_AFTER`, then `PI_SUBAGENT_REAP_AFTER`, then 120s.
|
|
285
306
|
`reapAfter: "none"` (also `"off"` or `"0"`) disables idle reaping; the absolute
|
|
286
307
|
`timeout` still applies. Follow-up tasks cancel the idle timer. This differs
|
package/index.ts
CHANGED
|
@@ -127,7 +127,7 @@ export function planSubagentSpawn(
|
|
|
127
127
|
error:
|
|
128
128
|
`Nested subagent creation is disabled at depth ${currentDepth}: spawning would reach depth ${childDepth}, ` +
|
|
129
129
|
`but the inherited maxDepth is ${maxDepth}. Have the top-level parent explicitly opt in with ` +
|
|
130
|
-
`babysit_run { profile: "subagent", task, maxDepth: ${childDepth} } when creating the first worker.`,
|
|
130
|
+
`babysit_run { description, profile: "subagent", task, maxDepth: ${childDepth} } when creating the first worker.`,
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
133
|
return { allowed: true, childDepth, maxDepth };
|
|
@@ -160,7 +160,7 @@ export function subagentGuidance(
|
|
|
160
160
|
const shellGuidance = directBashAvailable
|
|
161
161
|
? "Direct bash is available, and babysit_run can supervise longer commands."
|
|
162
162
|
: babysitRunAvailable
|
|
163
|
-
? "Direct bash is unavailable; run shell commands with babysit_run { command }."
|
|
163
|
+
? "Direct bash is unavailable; run shell commands with babysit_run { description, command }."
|
|
164
164
|
: "No shell execution tool is available in this task's tool allowlist; do not attempt shell commands.";
|
|
165
165
|
const nestingGuidance = depth >= maxDepth
|
|
166
166
|
? `You are at the inherited subagent depth limit (${depth}/${maxDepth}); do not attempt to spawn another subagent.`
|
|
@@ -521,6 +521,8 @@ interface Meta {
|
|
|
521
521
|
remoteId?: string;
|
|
522
522
|
remoteTerminalState?: string;
|
|
523
523
|
kind: "process" | "subagent";
|
|
524
|
+
/** Human-facing purpose. Optional only for metadata written by older versions. */
|
|
525
|
+
description?: string;
|
|
524
526
|
// process
|
|
525
527
|
name?: string;
|
|
526
528
|
command?: string;
|
|
@@ -568,6 +570,11 @@ interface Meta {
|
|
|
568
570
|
}
|
|
569
571
|
|
|
570
572
|
const metaDir = () => path.join(ROOT, "meta");
|
|
573
|
+
|
|
574
|
+
/** Prefer the human-facing purpose while preserving previews for old sessions. */
|
|
575
|
+
export function sessionPreview(meta: Pick<Meta, "description" | "kind" | "task" | "command"> | undefined): string {
|
|
576
|
+
return meta?.description?.trim() || (meta?.kind === "subagent" ? meta.task : meta?.command) || "";
|
|
577
|
+
}
|
|
571
578
|
const localLogPath = (id: string) => path.join(ROOT, "sessions", id, "output.log");
|
|
572
579
|
const logPath = (id: string) => {
|
|
573
580
|
const meta = readMeta(id);
|
|
@@ -1397,6 +1404,7 @@ export interface ProcessCompletionNotice {
|
|
|
1397
1404
|
status: CompletionStatus;
|
|
1398
1405
|
runtime: string;
|
|
1399
1406
|
summary: string;
|
|
1407
|
+
description?: string;
|
|
1400
1408
|
command: string | undefined;
|
|
1401
1409
|
logPath: string;
|
|
1402
1410
|
output: string;
|
|
@@ -1414,6 +1422,7 @@ interface ProcessCompletionMessage {
|
|
|
1414
1422
|
runtime?: string;
|
|
1415
1423
|
logPath?: string;
|
|
1416
1424
|
command?: string;
|
|
1425
|
+
description?: string;
|
|
1417
1426
|
count: number;
|
|
1418
1427
|
totalCount: number;
|
|
1419
1428
|
remainingCount: number;
|
|
@@ -1424,6 +1433,7 @@ interface ProcessCompletionMessage {
|
|
|
1424
1433
|
status: CompletionStatus;
|
|
1425
1434
|
runtime: string;
|
|
1426
1435
|
command: string | undefined;
|
|
1436
|
+
description?: string;
|
|
1427
1437
|
logPath: string;
|
|
1428
1438
|
}>;
|
|
1429
1439
|
};
|
|
@@ -1465,9 +1475,11 @@ export function buildProcessCompletionMessage(
|
|
|
1465
1475
|
count < totalCount
|
|
1466
1476
|
? `\n\n${totalCount - count} completion${totalCount - count === 1 ? "" : "s"} deferred to the next poll.`
|
|
1467
1477
|
: "";
|
|
1478
|
+
const noticeSummary = (notice: ProcessCompletionNotice) =>
|
|
1479
|
+
notice.summary + (notice.description ? `\nDescription: ${summarizeNotificationCommand(notice.description)}` : "");
|
|
1468
1480
|
const renderCompact = (batch: ProcessCompletionNotice[]) =>
|
|
1469
1481
|
header(batch.length) +
|
|
1470
|
-
batch.map((notice) => `${notice
|
|
1482
|
+
batch.map((notice) => `${noticeSummary(notice)}\nLog: ${notice.logPath}`).join("\n\n") +
|
|
1471
1483
|
deferred(batch.length) +
|
|
1472
1484
|
footer;
|
|
1473
1485
|
|
|
@@ -1492,7 +1504,7 @@ export function buildProcessCompletionMessage(
|
|
|
1492
1504
|
}
|
|
1493
1505
|
|
|
1494
1506
|
const blockBase = (notice: ProcessCompletionNotice) =>
|
|
1495
|
-
`${notice
|
|
1507
|
+
`${noticeSummary(notice)}\nCommand: ${summarizeNotificationCommand(notice.command)}\nLog: ${notice.logPath}`;
|
|
1496
1508
|
const outputs = batch.map((notice) =>
|
|
1497
1509
|
notice.output.startsWith("\n\nOutput:") ? AGGREGATE_OUTPUT_OMISSION : notice.output,
|
|
1498
1510
|
);
|
|
@@ -1533,6 +1545,7 @@ export function buildProcessCompletionMessage(
|
|
|
1533
1545
|
status: notice.status,
|
|
1534
1546
|
runtime: notice.runtime,
|
|
1535
1547
|
command: notice.command,
|
|
1548
|
+
description: notice.description,
|
|
1536
1549
|
logPath: notice.logPath,
|
|
1537
1550
|
}));
|
|
1538
1551
|
const single = totalCount === 1 ? batch[0] : undefined;
|
|
@@ -1548,6 +1561,7 @@ export function buildProcessCompletionMessage(
|
|
|
1548
1561
|
runtime: single?.runtime,
|
|
1549
1562
|
logPath: single?.logPath,
|
|
1550
1563
|
command: single?.command,
|
|
1564
|
+
description: single?.description,
|
|
1551
1565
|
count: batch.length,
|
|
1552
1566
|
totalCount,
|
|
1553
1567
|
remainingCount: totalCount - batch.length,
|
|
@@ -2109,6 +2123,7 @@ async function reserveUniqueSessionId(name: string): Promise<string> {
|
|
|
2109
2123
|
}
|
|
2110
2124
|
|
|
2111
2125
|
interface ProcOpts {
|
|
2126
|
+
description: string;
|
|
2112
2127
|
name?: string;
|
|
2113
2128
|
command: string;
|
|
2114
2129
|
cwd?: string;
|
|
@@ -2161,7 +2176,7 @@ async function spawnProcess(opts: ProcOpts): Promise<{ id: string } | { error: s
|
|
|
2161
2176
|
const reservedId = remote ? await reserveUniqueSessionId(opts.name ?? "remote-proc") : opts.name ? await reserveUniqueSessionId(opts.name) : undefined;
|
|
2162
2177
|
const remoteId = remote ? remoteProcessId(ROOT) : undefined;
|
|
2163
2178
|
if (reservedId) bsArgs.push("--id", remoteId ?? reservedId);
|
|
2164
|
-
if (remote && !writeMeta(reservedId!, { kind: "process", host: opts.host, remoteId, name: opts.name ?? reservedId, command: opts.command, notificationGroup: opts.notificationGroup, notified: false, startedAt: Date.now() })) {
|
|
2179
|
+
if (remote && !writeMeta(reservedId!, { kind: "process", host: opts.host, remoteId, name: opts.name ?? reservedId, description: opts.description, command: opts.command, notificationGroup: opts.notificationGroup, notified: false, startedAt: Date.now() })) {
|
|
2165
2180
|
reservedSessionIds.delete(reservedId!);
|
|
2166
2181
|
return { error: "Cannot persist remote identity; process was not started." };
|
|
2167
2182
|
}
|
|
@@ -2196,6 +2211,7 @@ async function spawnProcess(opts: ProcOpts): Promise<{ id: string } | { error: s
|
|
|
2196
2211
|
writeMeta(id, {
|
|
2197
2212
|
kind: "process",
|
|
2198
2213
|
name: opts.name ?? id,
|
|
2214
|
+
description: opts.description,
|
|
2199
2215
|
command: opts.command,
|
|
2200
2216
|
notificationGroup: opts.notificationGroup,
|
|
2201
2217
|
notified: false,
|
|
@@ -2251,6 +2267,7 @@ function discardDelivery(delivery: DeliverableMessage): void {
|
|
|
2251
2267
|
}
|
|
2252
2268
|
|
|
2253
2269
|
interface SubagentOpts {
|
|
2270
|
+
description: string;
|
|
2254
2271
|
name?: string;
|
|
2255
2272
|
agent?: AgentConfig;
|
|
2256
2273
|
task: string;
|
|
@@ -2381,6 +2398,7 @@ async function spawnSubagent(
|
|
|
2381
2398
|
writeMeta(id, {
|
|
2382
2399
|
kind: "subagent",
|
|
2383
2400
|
name: opts.name ?? id,
|
|
2401
|
+
description: opts.description,
|
|
2384
2402
|
task: opts.task,
|
|
2385
2403
|
launchCommand,
|
|
2386
2404
|
notified: true,
|
|
@@ -2470,6 +2488,7 @@ async function spawnSubagent(
|
|
|
2470
2488
|
writeMeta(id, {
|
|
2471
2489
|
kind: "subagent",
|
|
2472
2490
|
name: opts.name ?? id,
|
|
2491
|
+
description: opts.description,
|
|
2473
2492
|
task: opts.task,
|
|
2474
2493
|
launchCommand,
|
|
2475
2494
|
promptOffset: resp.offset,
|
|
@@ -2566,6 +2585,7 @@ export interface WidgetSessionListState {
|
|
|
2566
2585
|
|
|
2567
2586
|
export interface WidgetSessionDisplay {
|
|
2568
2587
|
id: string;
|
|
2588
|
+
description?: string;
|
|
2569
2589
|
header: string;
|
|
2570
2590
|
tail: string[];
|
|
2571
2591
|
viewable: boolean;
|
|
@@ -2603,7 +2623,7 @@ export function createWidgetComponent(
|
|
|
2603
2623
|
const pushSession = (session: WidgetSessionDisplay) => {
|
|
2604
2624
|
const target = session.viewable ? { type: "session" as const, id: session.id } : undefined;
|
|
2605
2625
|
const detail = session.details?.[0];
|
|
2606
|
-
const preview = detail?.startsWith("task: ") ? detail.slice(6) : detail ?? session.tail.at(-1);
|
|
2626
|
+
const preview = session.description ?? (detail?.startsWith("task: ") ? detail.slice(6) : detail ?? session.tail.at(-1));
|
|
2607
2627
|
push(`${session.header}${preview ? ` ${preview}` : ""}`, target, "…");
|
|
2608
2628
|
};
|
|
2609
2629
|
|
|
@@ -2689,8 +2709,9 @@ export function createSessionWidget(options: {
|
|
|
2689
2709
|
const log = options.readLog(selectedId);
|
|
2690
2710
|
const session = displays.find((session) => session.id === selectedId);
|
|
2691
2711
|
const header = session?.header ?? selectedId;
|
|
2692
|
-
const
|
|
2693
|
-
|
|
2712
|
+
const context = (session?.details ?? [])
|
|
2713
|
+
.map((detail) => detail.startsWith("task: ") ? detail.slice(6) : detail)
|
|
2714
|
+
.join("\n");
|
|
2694
2715
|
if (log.file) {
|
|
2695
2716
|
if (!source || source.file !== log.file || source.isAgent !== log.isAgent)
|
|
2696
2717
|
source = new FileLogSource(log.file, log.isAgent);
|
|
@@ -3540,7 +3561,7 @@ async function waitForExit(
|
|
|
3540
3561
|
kind: "exited",
|
|
3541
3562
|
ok,
|
|
3542
3563
|
text:
|
|
3543
|
-
`Process ${id}${meta
|
|
3564
|
+
`Process ${id}${sessionPreview(meta) ? ` (${summarizeNotificationCommand(sessionPreview(meta))})` : ""} ` +
|
|
3544
3565
|
(workerDead
|
|
3545
3566
|
? "worker-dead: the babysit supervisor disappeared without an exit status"
|
|
3546
3567
|
: ok ? "completed successfully" : `exited with code ${st.exit_code ?? "?"}`) +
|
|
@@ -3852,6 +3873,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3852
3873
|
runtime,
|
|
3853
3874
|
summary,
|
|
3854
3875
|
command: meta.command,
|
|
3876
|
+
description: meta.description,
|
|
3855
3877
|
logPath: logPath(session.id),
|
|
3856
3878
|
output,
|
|
3857
3879
|
});
|
|
@@ -3873,7 +3895,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3873
3895
|
if (!latest || latest.transportError || latest.state === "running") return [];
|
|
3874
3896
|
if (!isNotificationGroupReady(current, finalSessions, readMeta)) return [];
|
|
3875
3897
|
metadataById.set(notice.id, current);
|
|
3876
|
-
return [{ ...notice, command: current.command }];
|
|
3898
|
+
return [{ ...notice, command: current.command, description: current.description }];
|
|
3877
3899
|
});
|
|
3878
3900
|
// Output collection above yields to the event loop. Re-check idleness so a
|
|
3879
3901
|
// newly-started agent turn cannot receive a duplicate completion mid-turn.
|
|
@@ -3897,7 +3919,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3897
3919
|
): Promise<void> {
|
|
3898
3920
|
if (shouldDeferCompletionNotification(ctx.isIdle())) return;
|
|
3899
3921
|
const sessions = snapshot ?? (await listSessions()).sessions;
|
|
3900
|
-
const ready: Array<{ id: string; offset: number; state: string; summary: string }> = [];
|
|
3922
|
+
const ready: Array<{ id: string; offset: number; state: string; summary: string; description?: string }> = [];
|
|
3901
3923
|
for (const session of sessions) {
|
|
3902
3924
|
const meta = readMeta(session.id);
|
|
3903
3925
|
if (!shouldDeliverSubagentCompletion(meta)) continue;
|
|
@@ -3920,7 +3942,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3920
3942
|
const summary = progress.done
|
|
3921
3943
|
? `task settled; ${progress.turns} turns, ${progress.toolCallCount} tools, $${progress.cost.toFixed(4)}`
|
|
3922
3944
|
: `worker ${session.state} with exit code ${session.exit_code ?? "?"}; partial usage $${progress.cost.toFixed(4)}`;
|
|
3923
|
-
ready.push({ id: session.id, offset, state: session.state, summary });
|
|
3945
|
+
ready.push({ id: session.id, offset, state: session.state, summary, description: meta.description });
|
|
3924
3946
|
}
|
|
3925
3947
|
if (ready.length === 0 || shouldDeferCompletionNotification(ctx.isIdle())) return;
|
|
3926
3948
|
|
|
@@ -3934,10 +3956,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
3934
3956
|
customType: "pi-babysit-subagent-ready",
|
|
3935
3957
|
content:
|
|
3936
3958
|
`${deliverable.length === 1 ? "A background subagent is" : `${deliverable.length} background subagents are`} ready to collect:\n` +
|
|
3937
|
-
deliverable.map(({ id, summary }) => `- ${id}: ${summary}`).join("\n") +
|
|
3959
|
+
deliverable.map(({ id, summary, description }) => `- ${id}${description ? ` (${summarizeNotificationCommand(description)})` : ""}: ${summary}`).join("\n") +
|
|
3938
3960
|
"\nCall babysit_wait now to retrieve the answer and charge nested usage before finishing the parent task.",
|
|
3939
3961
|
display: true,
|
|
3940
|
-
details: { subagents: deliverable.map(({ id, state }) => ({ id, state })) },
|
|
3962
|
+
details: { subagents: deliverable.map(({ id, state, description }) => ({ id, state, description })) },
|
|
3941
3963
|
},
|
|
3942
3964
|
{ triggerTurn: true, deliverAs: "steer" },
|
|
3943
3965
|
);
|
|
@@ -3992,12 +4014,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
3992
4014
|
const id = session.id;
|
|
3993
4015
|
const meta = readMeta(id);
|
|
3994
4016
|
const isSubagent = meta?.kind === "subagent";
|
|
4017
|
+
const description = meta?.description?.trim()
|
|
4018
|
+
? theme.fg("warning", theme.bold(escapeCommandForDisplay(meta.description.trim())))
|
|
4019
|
+
: undefined;
|
|
3995
4020
|
const details = isSubagent
|
|
3996
4021
|
? [
|
|
3997
|
-
...(meta?.task ? [`task: ${escapeCommandForDisplay(meta.task)}`] : []),
|
|
3998
|
-
...(meta?.launchCommand ? [theme.
|
|
4022
|
+
...(meta?.task ? [theme.fg("muted", `task: ${escapeCommandForDisplay(meta.task)}`)] : []),
|
|
4023
|
+
...(meta?.launchCommand ? [theme.fg("muted", escapeCommandForDisplay(readableLaunchCommand(meta.launchCommand)))] : []),
|
|
3999
4024
|
]
|
|
4000
|
-
: meta?.command ? [theme.
|
|
4025
|
+
: meta?.command ? [theme.fg("muted", escapeCommandForDisplay(meta.command))] : [];
|
|
4026
|
+
if (description) details.unshift(description);
|
|
4001
4027
|
if (meta?.host) details.unshift(`host: ${meta.host}${session.transportError ? " (status unknown)" : ""} · ${attachCmd(id)}`);
|
|
4002
4028
|
if (session.state === "running") {
|
|
4003
4029
|
const state: WidgetSessionState = isSubagent && progressById.get(id)?.done ? "idle" : "running";
|
|
@@ -4005,6 +4031,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4005
4031
|
id,
|
|
4006
4032
|
header: renderWidgetSessionHeader(id, isSubagent ? "agent" : "process", state, elapsedOf(id), theme) + (session.transportError ? " · status unknown" : ""),
|
|
4007
4033
|
tail: activeTails.get(id) ?? [],
|
|
4034
|
+
description,
|
|
4008
4035
|
viewable: true,
|
|
4009
4036
|
active: true,
|
|
4010
4037
|
details,
|
|
@@ -4020,6 +4047,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4020
4047
|
theme,
|
|
4021
4048
|
),
|
|
4022
4049
|
details,
|
|
4050
|
+
description,
|
|
4023
4051
|
// Only read/parse visible sessions on demand.
|
|
4024
4052
|
get tail() {
|
|
4025
4053
|
try {
|
|
@@ -4115,25 +4143,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
4115
4143
|
success?: boolean;
|
|
4116
4144
|
exitCode?: number | null;
|
|
4117
4145
|
command?: string;
|
|
4146
|
+
description?: string;
|
|
4118
4147
|
count?: number;
|
|
4119
4148
|
};
|
|
4120
4149
|
const status =
|
|
4121
4150
|
d.status ??
|
|
4122
4151
|
(d.success ? "success" : d.exitCode == null ? "terminated" : "failed");
|
|
4123
|
-
const payload = d.
|
|
4124
|
-
?
|
|
4125
|
-
: d.
|
|
4126
|
-
?
|
|
4127
|
-
:
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
);
|
|
4152
|
+
const payload = d.description?.trim()
|
|
4153
|
+
? theme.fg("warning", theme.bold(summarizeNotificationCommand(d.description)))
|
|
4154
|
+
: d.command
|
|
4155
|
+
? theme.fg("muted", summarizeNotificationCommand(d.command))
|
|
4156
|
+
: d.count && d.count > 1
|
|
4157
|
+
? theme.fg("muted", `×${d.count}`)
|
|
4158
|
+
: "";
|
|
4159
|
+
const header = theme.fg("muted", theme.bold("babysit_run COMMAND")) + (payload ? ` ${payload}` : "");
|
|
4132
4160
|
const box = new Box(1, 1, (t) => theme.bg("toolSuccessBg", t));
|
|
4133
4161
|
box.addChild(new Text(header, 0, 0));
|
|
4134
4162
|
box.addChild(new Text(renderStatus(status, theme), 0, 0));
|
|
4135
4163
|
if (expanded && content) {
|
|
4136
|
-
box.addChild(new Text(
|
|
4164
|
+
box.addChild(new Text(content.split("\n").map((line) =>
|
|
4165
|
+
theme.fg(line.startsWith("Command: ") ? "muted" : "toolOutput", line),
|
|
4166
|
+
).join("\n"), 0, 0));
|
|
4137
4167
|
}
|
|
4138
4168
|
return box;
|
|
4139
4169
|
},
|
|
@@ -4281,7 +4311,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4281
4311
|
block: true,
|
|
4282
4312
|
reason:
|
|
4283
4313
|
`This bash command tries to run in the background. Use babysit_run instead, e.g. ` +
|
|
4284
|
-
`babysit_run({ name: "background-process", command: ${JSON.stringify(command.replace(/\s*&\s*$/, ""))} })`,
|
|
4314
|
+
`babysit_run({ description: "Describe this background process's purpose", name: "background-process", command: ${JSON.stringify(command.replace(/\s*&\s*$/, ""))} })`,
|
|
4285
4315
|
};
|
|
4286
4316
|
}
|
|
4287
4317
|
if (isAllowedDirectBash(command)) return;
|
|
@@ -4290,7 +4320,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4290
4320
|
reason:
|
|
4291
4321
|
"Use babysit_run for shell commands so output is supervised and captured outside model context. " +
|
|
4292
4322
|
"Inspect an existing session log with babysit_check { id, lines, pattern? }. " +
|
|
4293
|
-
`Retry as babysit_run({ command: ${JSON.stringify(command)} }).`,
|
|
4323
|
+
`Retry with a short purpose description as babysit_run({ description: "Explain the purpose of this command", command: ${JSON.stringify(command)} }).`,
|
|
4294
4324
|
};
|
|
4295
4325
|
});
|
|
4296
4326
|
|
|
@@ -4300,11 +4330,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
4300
4330
|
label: "Babysit: run",
|
|
4301
4331
|
description:
|
|
4302
4332
|
"Run a supervised shell command, or start a reusable pi subagent with `profile: \"subagent\"`. " +
|
|
4333
|
+
"Always provide a short human-readable `description` of what this run does and why. " +
|
|
4303
4334
|
"Use `foreground` for results needed now; otherwise long commands notify on exit. Full logs stay on disk. " +
|
|
4304
4335
|
"`returnPattern`/`returnLines` bound foreground output. Sessions support check, wait, send, and kill.",
|
|
4305
4336
|
promptSnippet: "Run supervised commands or bounded pi subagents with context-safe logs",
|
|
4306
4337
|
promptGuidelines: [
|
|
4307
4338
|
"Use babysit_run for shell commands and give meaningful sessions a stable name; bundle tiny related read-only observations into one command.",
|
|
4339
|
+
"Every babysit_run requires a short description explaining its purpose for the user, for both commands and subagents. Describe the actual work, not a generic 'run command' or 'start agent'.",
|
|
4308
4340
|
"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.",
|
|
4309
4341
|
"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.",
|
|
4310
4342
|
"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.",
|
|
@@ -4318,6 +4350,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
4318
4350
|
return prepareBabysitRunArguments(args) as never;
|
|
4319
4351
|
},
|
|
4320
4352
|
parameters: Type.Object({
|
|
4353
|
+
description: Type.String({
|
|
4354
|
+
minLength: 1,
|
|
4355
|
+
pattern: "\\S",
|
|
4356
|
+
description: "Required short human-readable purpose for this command or subagent. Explain what it does and why; shown prominently to the user.",
|
|
4357
|
+
}),
|
|
4321
4358
|
host: Type.Optional(Type.String({ description: "Process SSH destination/config alias (default local). Remote subagents are unsupported." })),
|
|
4322
4359
|
cwd: Type.Optional(Type.String({ description: "Process working directory. Remote default is login home; never inferred from local cwd." })),
|
|
4323
4360
|
command: Type.Optional(
|
|
@@ -4448,6 +4485,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
4448
4485
|
|
|
4449
4486
|
}),
|
|
4450
4487
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
4488
|
+
if (typeof params.description !== "string" || !params.description.trim()) {
|
|
4489
|
+
throw new Error("`description` is required and must be a non-blank string for both commands and subagents.");
|
|
4490
|
+
}
|
|
4491
|
+
const description = params.description.trim();
|
|
4451
4492
|
await requireBabysit();
|
|
4452
4493
|
const isSubagent = params.profile === "subagent";
|
|
4453
4494
|
validateProcessHost(params.host);
|
|
@@ -4555,6 +4596,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4555
4596
|
? { pattern: params.returnPattern, lines: params.returnLines, maxBytes: params.maxBytes }
|
|
4556
4597
|
: undefined;
|
|
4557
4598
|
const spawnOpts: ProcOpts = {
|
|
4599
|
+
description,
|
|
4558
4600
|
name: params.name,
|
|
4559
4601
|
command: params.command as string,
|
|
4560
4602
|
host: params.host,
|
|
@@ -4605,6 +4647,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4605
4647
|
kind: "process",
|
|
4606
4648
|
command: params.command,
|
|
4607
4649
|
logPath: logPath(res.id),
|
|
4650
|
+
description,
|
|
4608
4651
|
retried,
|
|
4609
4652
|
status: outcomeStatus(outcome),
|
|
4610
4653
|
},
|
|
@@ -4641,6 +4684,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4641
4684
|
kind: "process",
|
|
4642
4685
|
command: params.command,
|
|
4643
4686
|
logPath: logPath(res.id),
|
|
4687
|
+
description,
|
|
4644
4688
|
retried,
|
|
4645
4689
|
status: outcomeStatus(outcome),
|
|
4646
4690
|
},
|
|
@@ -4657,7 +4701,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4657
4701
|
type: "text",
|
|
4658
4702
|
text:
|
|
4659
4703
|
`${retried ? "Retried once after external worker death.\n" : ""}` +
|
|
4660
|
-
`Process started (id: ${res.id}). ${NOTIFY_MARKER}\nLog: ${logPath(res.id)}\n${nextStep}\n` +
|
|
4704
|
+
`Process started (id: ${res.id}). ${NOTIFY_MARKER}\nDescription: ${escapeCommandForDisplay(description)}\nLog: ${logPath(res.id)}\n${nextStep}\n` +
|
|
4661
4705
|
`Inspect: babysit_check { id: "${res.id}" } (screen: true for TUIs) · ` +
|
|
4662
4706
|
`Wait: babysit_wait { id: "${res.id}" } · Kill: babysit_kill { id: "${res.id}" }\n` +
|
|
4663
4707
|
`Human can watch/take over: /babysit`,
|
|
@@ -4667,6 +4711,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4667
4711
|
id: res.id,
|
|
4668
4712
|
kind: "process",
|
|
4669
4713
|
command: params.command,
|
|
4714
|
+
description,
|
|
4670
4715
|
logPath: logPath(res.id),
|
|
4671
4716
|
retried,
|
|
4672
4717
|
status: "started" satisfies DisplayStatus,
|
|
@@ -4702,6 +4747,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4702
4747
|
// The branch above guarantees a successful plan in subagent mode.
|
|
4703
4748
|
const subagentNesting = nesting as Extract<SubagentSpawnPlan, { allowed: true }>;
|
|
4704
4749
|
const res = await spawnSubagent({
|
|
4750
|
+
description,
|
|
4705
4751
|
name: params.name,
|
|
4706
4752
|
agent,
|
|
4707
4753
|
task: params.task as string,
|
|
@@ -4750,6 +4796,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4750
4796
|
agent: agent?.name,
|
|
4751
4797
|
model: res.model,
|
|
4752
4798
|
task: params.task,
|
|
4799
|
+
description,
|
|
4753
4800
|
depth: subagentNesting.childDepth,
|
|
4754
4801
|
maxDepth: subagentNesting.maxDepth,
|
|
4755
4802
|
status: outcomeStatus(outcome),
|
|
@@ -4762,7 +4809,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4762
4809
|
type: "text",
|
|
4763
4810
|
text:
|
|
4764
4811
|
`Subagent started (id: ${res.id})${agent ? ` [agent: ${agent.name}]` : ""}${res.model ? ` [model: ${res.model}]` : ""} [depth: ${subagentNesting.childDepth}/${subagentNesting.maxDepth}].\n` +
|
|
4765
|
-
`
|
|
4812
|
+
`Description: ${escapeCommandForDisplay(description)}\nTask accepted — running in the background. You MUST collect it with babysit_wait before finishing the parent task; use foreground: true next time when no independent work is available.\n` +
|
|
4766
4813
|
`Progress: babysit_check { id: "${res.id}" } (only when inspection is needed)\n` +
|
|
4767
4814
|
`Collect: babysit_wait { id: "${res.id}" }\n` +
|
|
4768
4815
|
`Human can watch/steer: /babysit (pick ${res.id})`,
|
|
@@ -4775,25 +4822,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
4775
4822
|
agent: agent?.name,
|
|
4776
4823
|
model: res.model,
|
|
4777
4824
|
task: params.task,
|
|
4825
|
+
description,
|
|
4778
4826
|
depth: subagentNesting.childDepth,
|
|
4779
4827
|
maxDepth: subagentNesting.maxDepth,
|
|
4780
4828
|
status: "started" satisfies DisplayStatus,
|
|
4781
4829
|
},
|
|
4782
4830
|
};
|
|
4783
4831
|
},
|
|
4784
|
-
//
|
|
4785
|
-
//
|
|
4832
|
+
// The human-readable purpose is primary; the executable payload is secondary.
|
|
4833
|
+
// Legacy calls without descriptions still render their original payload safely.
|
|
4786
4834
|
renderCall(args, theme) {
|
|
4787
4835
|
const isSubagent = args.profile === "subagent";
|
|
4788
4836
|
const kind = isSubagent ? "AGENT" : "COMMAND";
|
|
4789
4837
|
const payload = isSubagent ? args.task : args.command;
|
|
4790
|
-
const agent = isSubagent && args.agent ? ` [${args.agent}]` : "";
|
|
4791
|
-
const title = theme.bold(`babysit_run ${kind}`);
|
|
4792
|
-
const
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4838
|
+
const agent = isSubagent && args.agent ? ` [${escapeCommandForDisplay(args.agent)}]` : "";
|
|
4839
|
+
const title = theme.fg("muted", `${theme.bold(`babysit_run ${kind}`)}${agent}`);
|
|
4840
|
+
const description = typeof args.description === "string" ? args.description.trim() : "";
|
|
4841
|
+
const raw = typeof payload === "string" ? escapeCommandForDisplay(payload) : "";
|
|
4842
|
+
const heading = description
|
|
4843
|
+
? `${title} ${theme.fg("warning", theme.bold(escapeCommandForDisplay(description)))}`
|
|
4844
|
+
: `${title}${raw ? theme.fg("muted", ` ${raw}`) : ""}`;
|
|
4845
|
+
const detail = description && raw
|
|
4846
|
+
? `\n${theme.fg("muted", raw)}`
|
|
4847
|
+
: "";
|
|
4848
|
+
return new Text(heading + detail, 0, 0);
|
|
4797
4849
|
},
|
|
4798
4850
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
4799
4851
|
const details = (result.details ?? {}) as {
|
|
@@ -4932,7 +4984,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4932
4984
|
kind === "subagent" && meta?.depth != null
|
|
4933
4985
|
? ` depth=${meta.depth}/${meta.maxDepth ?? "?"}`
|
|
4934
4986
|
: "";
|
|
4935
|
-
const what = (
|
|
4987
|
+
const what = escapeCommandForDisplay(sessionPreview(meta));
|
|
4936
4988
|
const preview = what.length > 60 ? `${what.slice(0, 57)}…` : what;
|
|
4937
4989
|
return `${s.id} [${kind}${s.host ? ` host=${s.host}` : ""}] ${s.transportError ? "unknown" : s.state}${ec}${depth}${flag}${preview ? ` — ${preview}` : ""}`;
|
|
4938
4990
|
});
|
|
@@ -5000,6 +5052,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
5000
5052
|
if (el) header += ` elapsed=${el}`;
|
|
5001
5053
|
}
|
|
5002
5054
|
if (st.exit_code != null) header += ` exit_code=${st.exit_code}`;
|
|
5055
|
+
if (meta?.description) header += `\ndescription: ${escapeCommandForDisplay(meta.description)}`;
|
|
5003
5056
|
if (meta?.command) header += `\ncommand: ${summarizeNotificationCommand(meta.command)}`;
|
|
5004
5057
|
header += `\nlog: ${logPath(params.id)}`;
|
|
5005
5058
|
if (st.note) header += ` ⚑ ${st.note}`;
|
|
@@ -5045,6 +5098,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
5045
5098
|
if (prog.tokens != null) header += ` ctx=${prog.tokens}`;
|
|
5046
5099
|
if (prog.modelCalls > 0) header += ` usage=${prog.usageTokens} $${prog.cost.toFixed(4)}`;
|
|
5047
5100
|
if (st.note) header += ` ⚑ ${st.note}`;
|
|
5101
|
+
if (meta.description) header += `\ndescription: ${escapeCommandForDisplay(meta.description)}`;
|
|
5048
5102
|
parts.push(header);
|
|
5049
5103
|
|
|
5050
5104
|
if (prog.errorMsg) parts.push(`⚠ error: ${clip(prog.errorMsg, ANSWER_MAX_BYTES)}`);
|
|
@@ -5593,9 +5647,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
5593
5647
|
|
|
5594
5648
|
const whatOf = (s: BsSession): string => {
|
|
5595
5649
|
const meta = readMeta(s.id);
|
|
5596
|
-
return ((meta
|
|
5597
|
-
.replace(/\s+/g, " ")
|
|
5598
|
-
.trim();
|
|
5650
|
+
return escapeCommandForDisplay(sessionPreview(meta).replace(/\s+/g, " ").trim());
|
|
5599
5651
|
};
|
|
5600
5652
|
|
|
5601
5653
|
// Labels must be unique for index mapping; the id makes them unique.
|