@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.
Files changed (3) hide show
  1. package/README.md +33 -12
  2. package/index.ts +96 -44
  3. 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 at every
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 command for processes and
136
- the task for agents (with `…` when clipped). Click an AGENT or PROCESS row to
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.summary}\nLog: ${notice.logPath}`).join("\n\n") +
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.summary}\nCommand: ${summarizeNotificationCommand(notice.command)}\nLog: ${notice.logPath}`;
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 detail = session?.details?.[0] ?? "";
2693
- const context = detail.startsWith("task: ") ? detail.slice(6) : detail;
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?.command ? ` (${summarizeNotificationCommand(meta.command)})` : ""} ` +
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.bold(escapeCommandForDisplay(readableLaunchCommand(meta.launchCommand)))] : []),
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.bold(escapeCommandForDisplay(meta.command))] : [];
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.command
4124
- ? ` ${summarizeNotificationCommand(d.command)}`
4125
- : d.count && d.count > 1
4126
- ? ` ×${d.count}`
4127
- : "";
4128
- const header = theme.fg(
4129
- "warning",
4130
- theme.bold(`babysit_run COMMAND${payload}`),
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(theme.fg("toolOutput", content), 0, 0));
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
- `Task 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` +
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
- // Make the execution kind and payload identifiable before the result arrives.
4785
- // Use semantic theme colors rather than the fallback gray tool-call text.
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 detail =
4793
- typeof payload === "string" && payload.length > 0
4794
- ? ` ${escapeCommandForDisplay(payload)}`
4795
- : "";
4796
- return new Text(theme.fg("warning", `${title}${agent}${detail}`), 0, 0);
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 = (kind === "subagent" ? meta?.task : meta?.command) ?? "";
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?.kind === "subagent" ? meta.task : meta?.command) ?? "")
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yusukeshib/pi-babysit",
3
- "version": "0.6.21",
3
+ "version": "0.6.22",
4
4
  "description": "Run any shell command and pi subagents under babysit, with context-safe captured output.",
5
5
  "keywords": [
6
6
  "pi-package",