agent-yes 1.284.1 → 1.285.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/{SUPPORTED_CLIS-DZwCkmvh.js → SUPPORTED_CLIS-CXvLHLG2.js} +3 -3
  2. package/dist/{SUPPORTED_CLIS-B2Jis2RD.js → SUPPORTED_CLIS-tYZC_KWR.js} +2 -2
  3. package/dist/{agentShare-BSh14vJq.js → agentShare-0hRw0bQn.js} +2 -2
  4. package/dist/{callback-nuwvhn5x.js → callback-BN6cqDjb.js} +3 -3
  5. package/dist/{callback-Cyr2rDek.js → callback-DCaGBxD1.js} +2 -2
  6. package/dist/cli.js +5 -5
  7. package/dist/{cmdPs-tzFiiKC5.js → cmdPs-CUkarOK7.js} +123 -18
  8. package/dist/index.js +2 -2
  9. package/dist/{notifyDaemon-BBQKDl7Q.js → notifyDaemon-DNTRkaSU.js} +2 -2
  10. package/dist/{parentPingLoop-CVy-ueZc.js → parentPingLoop-CovRlBFM.js} +2 -2
  11. package/dist/{rustBinary-CRR9Sr3R.js → rustBinary-CkKsMycJ.js} +2 -2
  12. package/dist/{rustBinary-C9B5hRAU.js → rustBinary-ydpdBd3-.js} +2 -2
  13. package/dist/{schedule-CH_evU-N.js → schedule-CgzQNLfP.js} +4 -4
  14. package/dist/{serve-CC074vGJ.js → serve-nk0r7fxV.js} +17 -17
  15. package/dist/{setup-DDZMpGsh.js → setup-P7AMLtbV.js} +2 -2
  16. package/dist/{subcommands-D2iZ_g0A.js → subcommands-B_Lg_zDe.js} +15 -15
  17. package/dist/{subcommands-DafbEv0e.js → subcommands-SeVXiezY.js} +1 -1
  18. package/dist/{terminal-C-UzqGmj.js → terminal-DM_kIymf.js} +2 -2
  19. package/dist/trayApp-BIOUrF8R.js +5 -0
  20. package/dist/{trayApp-Dylg_sbJ.js → trayApp-CUxqXOmu.js} +2 -2
  21. package/dist/{ts-BmiGlG1v.js → ts-LgWg13cL.js} +3 -3
  22. package/dist/{versionChecker-D59qJF9N.js → versionChecker-CbqhEGUc.js} +2 -2
  23. package/dist/{widget-BENlkcvI.js → widget-DUkLAsSR.js} +3 -3
  24. package/dist/{ws-PKok408W.js → ws-BAy7xMDI.js} +2 -2
  25. package/dist/{ws-DvMM1i2j.js → ws-ChkhtUQm.js} +2 -2
  26. package/package.json +1 -1
  27. package/ts/cmdPs.spec.ts +126 -1
  28. package/ts/cmdPs.ts +148 -12
  29. package/ts/procStats.spec.ts +32 -6
  30. package/ts/procStats.ts +77 -5
  31. package/dist/trayApp-D5SzLS6p.js +0 -5
@@ -906,7 +906,7 @@ async function runSubcommand(argv) {
906
906
  switch (sub) {
907
907
  case "ls":
908
908
  case "list": return await cmdLs(rest);
909
- case "ps": return await (await import("./cmdPs-tzFiiKC5.js")).cmdPs(rest);
909
+ case "ps": return await (await import("./cmdPs-CUkarOK7.js")).cmdPs(rest);
910
910
  case "status": return await cmdStatus(rest);
911
911
  case "whoami": return await cmdWhoami(rest);
912
912
  case "result": return await cmdResult(rest);
@@ -953,35 +953,35 @@ async function runSubcommand(argv) {
953
953
  return cmdCh(rest);
954
954
  }
955
955
  case "term": {
956
- const { cmdTerm } = await import("./terminal-C-UzqGmj.js");
956
+ const { cmdTerm } = await import("./terminal-DM_kIymf.js");
957
957
  return cmdTerm(rest);
958
958
  }
959
959
  case "widget": {
960
- const { cmdWidget } = await import("./widget-BENlkcvI.js");
960
+ const { cmdWidget } = await import("./widget-DUkLAsSR.js");
961
961
  return cmdWidget(rest);
962
962
  }
963
963
  case "mint": {
964
- const { cmdMint } = await import("./widget-BENlkcvI.js");
964
+ const { cmdMint } = await import("./widget-DUkLAsSR.js");
965
965
  return cmdMint(rest);
966
966
  }
967
967
  case "serve": {
968
- const { cmdServe } = await import("./serve-CC074vGJ.js");
968
+ const { cmdServe } = await import("./serve-nk0r7fxV.js");
969
969
  return cmdServe(rest);
970
970
  }
971
971
  case "tray": {
972
- const { cmdTray } = await import("./trayApp-D5SzLS6p.js");
972
+ const { cmdTray } = await import("./trayApp-BIOUrF8R.js");
973
973
  return cmdTray(rest);
974
974
  }
975
975
  case "setup": {
976
- const { cmdSetup } = await import("./setup-DDZMpGsh.js");
976
+ const { cmdSetup } = await import("./setup-P7AMLtbV.js");
977
977
  return cmdSetup(rest);
978
978
  }
979
979
  case "ws": {
980
- const { cmdWs } = await import("./ws-DvMM1i2j.js");
980
+ const { cmdWs } = await import("./ws-ChkhtUQm.js");
981
981
  return cmdWs(rest);
982
982
  }
983
983
  case "schedule": {
984
- const { cmdSchedule } = await import("./schedule-CH_evU-N.js");
984
+ const { cmdSchedule } = await import("./schedule-CgzQNLfP.js");
985
985
  return cmdSchedule(rest);
986
986
  }
987
987
  case "remote": {
@@ -993,7 +993,7 @@ async function runSubcommand(argv) {
993
993
  return cmdExpose(rest);
994
994
  }
995
995
  case "callback": {
996
- const { cmdCallback } = await import("./callback-Cyr2rDek.js");
996
+ const { cmdCallback } = await import("./callback-DCaGBxD1.js");
997
997
  return cmdCallback(rest);
998
998
  }
999
999
  case "reap":
@@ -1004,7 +1004,7 @@ async function runSubcommand(argv) {
1004
1004
  return await cmdDsh(rest);
1005
1005
  }
1006
1006
  case "gc": {
1007
- const { gcOldBinaryDirs } = await import("./rustBinary-CRR9Sr3R.js");
1007
+ const { gcOldBinaryDirs } = await import("./rustBinary-CkKsMycJ.js");
1008
1008
  const res = gcOldBinaryDirs();
1009
1009
  if (res.removed.length === 0) process.stdout.write("no old agent-yes binary cache dirs to remove\n");
1010
1010
  else {
@@ -3481,7 +3481,7 @@ async function cmdAttach(rest) {
3481
3481
  const winsizeDir = path.join(ayHome, "winsize");
3482
3482
  await mkdir(winsizeDir, { recursive: true });
3483
3483
  const winsizePath = path.join(winsizeDir, String(record.pid));
3484
- const { resolveDaemonHttpBase, loadTokenReadOnly } = await import("./serve-CC074vGJ.js");
3484
+ const { resolveDaemonHttpBase, loadTokenReadOnly } = await import("./serve-nk0r7fxV.js");
3485
3485
  const daemonBase = await resolveDaemonHttpBase().catch(() => null);
3486
3486
  const daemonToken = daemonBase ? await loadTokenReadOnly().catch(() => null) ?? "" : "";
3487
3487
  const capViewer = `attach:${process.pid}`;
@@ -4199,7 +4199,7 @@ async function cmdNotify(rest) {
4199
4199
  }
4200
4200
  const ensure = async () => {
4201
4201
  if (!argv["ensure-daemon"]) return;
4202
- const { ensureDaemon } = await import("./notifyDaemon-BBQKDl7Q.js");
4202
+ const { ensureDaemon } = await import("./notifyDaemon-DNTRkaSU.js");
4203
4203
  await ensureDaemon().catch(() => null);
4204
4204
  };
4205
4205
  await heartbeatWatcher(parent, selfStartedAt);
@@ -4273,7 +4273,7 @@ async function cmdNotifyCursor(args) {
4273
4273
  }
4274
4274
  async function cmdNotifyd(rest) {
4275
4275
  const sub = rest[0] ?? "status";
4276
- const daemon = await import("./notifyDaemon-BBQKDl7Q.js");
4276
+ const daemon = await import("./notifyDaemon-DNTRkaSU.js");
4277
4277
  switch (sub) {
4278
4278
  case "run": return daemon.runDaemon();
4279
4279
  case "once": return daemon.runDaemon({ once: true });
@@ -4300,4 +4300,4 @@ async function cmdNotifyd(rest) {
4300
4300
 
4301
4301
  //#endregion
4302
4302
  export { buildAgentForest as $, readAgentPtysize as A, resolveReadWindow as B, isUnknownManagerToken as C, listRecords as D, lastStdinAt as E, recentReadEdges as F, stdinActivityPath as G, restartHintLines as H, renderLogTailLines as I, waitForLogQuiet as J, stopTipForCli as K, renderRawLog as L, readNotes as M, readPtysize as N, matchKeyword as O, recentMessageEdges as P, TYPING_BADGE as Q, renderRawLogLines as R, isSubcommand as S, lastReadAt as T, runSubcommand as U, resolveResumeArgs as V, snapshotStatus as W, writeToIpc as X, writeKeysPaced as Y, withIpcLock as Z, isAgentStuck as _, TYPING_WINDOW_MS as a, isPidAlive as b, controlCodeFromName as c, deriveLiveStatus as d, flattenForest as et, extractBadges as f, finalizedLines as g, extractTaskCounts as h, READ_WINDOW_MS as i, readLogForRender as j, menuSelectKeys as k, cursorAbs as l, extractNeedsInput as m, MAX_RENDER_BYTES as n, backoffWhileTyping as o, extractMenu as p, submitAndConfirm as q, READ_PAGE_DEFAULT as r, cmdHelp as s, GRACEFUL_EXIT_COMMANDS as t, deriveLiveState as u, isBareManagerInvocation as v, isUserTyping as w, isSlashCommand as x, isExitRequest as y, resolveOne as z };
4303
- //# sourceMappingURL=subcommands-D2iZ_g0A.js.map
4303
+ //# sourceMappingURL=subcommands-B_Lg_zDe.js.map
@@ -3,7 +3,7 @@ import "./globalPidIndex-DoaKEUkO.js";
3
3
  import "./messageLog-DdjBNqgt.js";
4
4
  import "./e2e-z1xpwSAg.js";
5
5
  import "./configShared-DKTgiz7K.js";
6
- import { A as readAgentPtysize, B as resolveReadWindow, C as isUnknownManagerToken, D as listRecords, E as lastStdinAt, F as recentReadEdges, G as stdinActivityPath, H as restartHintLines, I as renderLogTailLines, J as waitForLogQuiet, K as stopTipForCli, L as renderRawLog, M as readNotes, N as readPtysize, O as matchKeyword, P as recentMessageEdges, R as renderRawLogLines, S as isSubcommand, T as lastReadAt, U as runSubcommand, V as resolveResumeArgs, W as snapshotStatus, X as writeToIpc, Y as writeKeysPaced, _ as isAgentStuck, a as TYPING_WINDOW_MS, b as isPidAlive, c as controlCodeFromName, d as deriveLiveStatus, f as extractBadges, g as finalizedLines, h as extractTaskCounts, i as READ_WINDOW_MS, j as readLogForRender, k as menuSelectKeys, l as cursorAbs, m as extractNeedsInput, n as MAX_RENDER_BYTES, o as backoffWhileTyping, p as extractMenu, q as submitAndConfirm, r as READ_PAGE_DEFAULT, s as cmdHelp, t as GRACEFUL_EXIT_COMMANDS, u as deriveLiveState, v as isBareManagerInvocation, w as isUserTyping, x as isSlashCommand, y as isExitRequest, z as resolveOne } from "./subcommands-D2iZ_g0A.js";
6
+ import { A as readAgentPtysize, B as resolveReadWindow, C as isUnknownManagerToken, D as listRecords, E as lastStdinAt, F as recentReadEdges, G as stdinActivityPath, H as restartHintLines, I as renderLogTailLines, J as waitForLogQuiet, K as stopTipForCli, L as renderRawLog, M as readNotes, N as readPtysize, O as matchKeyword, P as recentMessageEdges, R as renderRawLogLines, S as isSubcommand, T as lastReadAt, U as runSubcommand, V as resolveResumeArgs, W as snapshotStatus, X as writeToIpc, Y as writeKeysPaced, _ as isAgentStuck, a as TYPING_WINDOW_MS, b as isPidAlive, c as controlCodeFromName, d as deriveLiveStatus, f as extractBadges, g as finalizedLines, h as extractTaskCounts, i as READ_WINDOW_MS, j as readLogForRender, k as menuSelectKeys, l as cursorAbs, m as extractNeedsInput, n as MAX_RENDER_BYTES, o as backoffWhileTyping, p as extractMenu, q as submitAndConfirm, r as READ_PAGE_DEFAULT, s as cmdHelp, t as GRACEFUL_EXIT_COMMANDS, u as deriveLiveState, v as isBareManagerInvocation, w as isUserTyping, x as isSlashCommand, y as isExitRequest, z as resolveOne } from "./subcommands-B_Lg_zDe.js";
7
7
  import "./notifyStore-2SWKGwZI.js";
8
8
  import "./webrtcLink-DLaJfTcB.js";
9
9
  import "./remotes-b6reVXzo.js";
@@ -98,7 +98,7 @@ async function cmdTermMint(args) {
98
98
  if (flags.ro && flags.interactive) throw new Error("--ro and --interactive are mutually exclusive");
99
99
  const ttlSec = parseTtlSec(typeof flags.ttl === "string" ? flags.ttl : "15m");
100
100
  const canSend = flags.interactive === true;
101
- const { mintScopedTermToken } = await import("./serve-CC074vGJ.js");
101
+ const { mintScopedTermToken } = await import("./serve-nk0r7fxV.js");
102
102
  const r = await mintScopedTermToken(pid, {
103
103
  ttlSec,
104
104
  canSend
@@ -135,4 +135,4 @@ async function cmdTerm(args) {
135
135
 
136
136
  //#endregion
137
137
  export { cmdTerm };
138
- //# sourceMappingURL=terminal-C-UzqGmj.js.map
138
+ //# sourceMappingURL=terminal-DM_kIymf.js.map
@@ -0,0 +1,5 @@
1
+ import "./versionChecker-CbqhEGUc.js";
2
+ import "./rustBinary-ydpdBd3-.js";
3
+ import { a as trayHiddenMarker, i as launchTray, n as hasDesktop, r as isTrayHidden, t as cmdTray } from "./trayApp-CUxqXOmu.js";
4
+
5
+ export { cmdTray };
@@ -1,4 +1,4 @@
1
- import { i as findTrayLauncher } from "./rustBinary-C9B5hRAU.js";
1
+ import { i as findTrayLauncher } from "./rustBinary-ydpdBd3-.js";
2
2
  import { t as agentYesHome } from "./agentYesHome-BNtn19J4.js";
3
3
  import { existsSync } from "node:fs";
4
4
  import path from "node:path";
@@ -97,4 +97,4 @@ async function cmdTray(args) {
97
97
 
98
98
  //#endregion
99
99
  export { trayHiddenMarker as a, launchTray as i, hasDesktop as n, isTrayHidden as r, cmdTray as t };
100
- //# sourceMappingURL=trayApp-Dylg_sbJ.js.map
100
+ //# sourceMappingURL=trayApp-CUxqXOmu.js.map
@@ -1,6 +1,6 @@
1
1
  import { n as __esmMin, r as __exportAll } from "./chunk-cZBX9HZv.js";
2
2
  import { n as logger, t as addTransport } from "./logger-CDIsZ-Pp.js";
3
- import { i as getInstalledPackage } from "./versionChecker-D59qJF9N.js";
3
+ import { i as getInstalledPackage } from "./versionChecker-CbqhEGUc.js";
4
4
  import { t as agentYesHome } from "./agentYesHome-BNtn19J4.js";
5
5
  import { u as removeControlCharacters } from "./todoAutomation-CbVVS4re.js";
6
6
  import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-BOIPD_Vs.js";
@@ -2071,7 +2071,7 @@ async function agentYes({ cli, cliArgs = [], skipPermissions = false, prompt, ro
2071
2071
  }, 100);
2072
2072
  const cleanupHeartbeat = () => clearInterval(heartbeatInterval);
2073
2073
  shell.onExit(cleanupHeartbeat);
2074
- const { startParentPingLoop } = await import("./parentPingLoop-CVy-ueZc.js");
2074
+ const { startParentPingLoop } = await import("./parentPingLoop-CovRlBFM.js");
2075
2075
  const parentPingLoop = startParentPingLoop({
2076
2076
  parentPid,
2077
2077
  selfWrapperPid: process.pid,
@@ -2334,4 +2334,4 @@ function sleep(ms) {
2334
2334
 
2335
2335
  //#endregion
2336
2336
  export { hasTodoStore as a, permissionBadge as c, answerAsk as i, AgentContext as l, agentYes as n, listAsks as o, config as r, listAsksForProject as s, CLIS_CONFIG as t, CLAUDE_SESSION_PIN_ENV as u };
2337
- //# sourceMappingURL=ts-BmiGlG1v.js.map
2337
+ //# sourceMappingURL=ts-LgWg13cL.js.map
@@ -7,7 +7,7 @@ import { fileURLToPath } from "url";
7
7
 
8
8
  //#region package.json
9
9
  var name = "agent-yes";
10
- var version = "1.284.1";
10
+ var version = "1.285.0";
11
11
 
12
12
  //#endregion
13
13
  //#region ts/versionChecker.ts
@@ -215,4 +215,4 @@ async function displayVersion() {
215
215
 
216
216
  //#endregion
217
217
  export { versionString as a, getInstalledPackage as i, compareVersions as n, displayVersion as r, checkAndAutoUpdate as t };
218
- //# sourceMappingURL=versionChecker-D59qJF9N.js.map
218
+ //# sourceMappingURL=versionChecker-CbqhEGUc.js.map
@@ -28,7 +28,7 @@ function parseFlags(args, known) {
28
28
  }
29
29
  /** Resolve the local daemon base URL + token (flags override discovery). */
30
30
  async function daemonTarget(flags) {
31
- const { resolveDaemonHttpBase, loadTokenReadOnly } = await import("./serve-CC074vGJ.js");
31
+ const { resolveDaemonHttpBase, loadTokenReadOnly } = await import("./serve-nk0r7fxV.js");
32
32
  const base = typeof flags.base === "string" ? flags.base : await resolveDaemonHttpBase();
33
33
  if (!base) throw new Error("no running ay serve daemon found — start `ay serve` or pass --base <url> (e.g. http://127.0.0.1:PORT)");
34
34
  const token = typeof flags.token === "string" ? flags.token : await loadTokenReadOnly() ?? "";
@@ -186,7 +186,7 @@ async function cmdMint(args) {
186
186
  const bad = caps.filter((c) => !KNOWN_CAPS.has(c));
187
187
  if (bad.length) throw new Error(`unknown cap(s): ${bad.join(", ")} (valid: ${[...KNOWN_CAPS].join(", ")})`);
188
188
  const ttlSec = parseTtlSec(typeof flags.ttl === "string" ? flags.ttl : "15m");
189
- const { mintScopedTermToken } = await import("./serve-CC074vGJ.js");
189
+ const { mintScopedTermToken } = await import("./serve-nk0r7fxV.js");
190
190
  const r = await mintScopedTermToken(target, {
191
191
  ttlSec,
192
192
  caps
@@ -202,4 +202,4 @@ async function cmdMint(args) {
202
202
 
203
203
  //#endregion
204
204
  export { cmdMint, cmdWidget };
205
- //# sourceMappingURL=widget-BENlkcvI.js.map
205
+ //# sourceMappingURL=widget-DUkLAsSR.js.map
@@ -1,4 +1,4 @@
1
- import { D as listRecords } from "./subcommands-D2iZ_g0A.js";
1
+ import { D as listRecords } from "./subcommands-B_Lg_zDe.js";
2
2
  import { a as getProvisionRoot } from "./workspaceConfig-D6QndOCz.js";
3
3
  import { existsSync } from "fs";
4
4
  import { lstat, opendir } from "fs/promises";
@@ -424,4 +424,4 @@ async function cmdWs(args) {
424
424
 
425
425
  //#endregion
426
426
  export { isPathInside as a, walkWorkspaces as c, importProvisionModule as i, workspaceStatus as l, cmdWs as n, loadProvision as o, collectWorkspaces as r, resolveOperand as s, WS_JSON_SCHEMA as t };
427
- //# sourceMappingURL=ws-PKok408W.js.map
427
+ //# sourceMappingURL=ws-BAy7xMDI.js.map
@@ -3,10 +3,10 @@ import "./globalPidIndex-DoaKEUkO.js";
3
3
  import "./messageLog-DdjBNqgt.js";
4
4
  import "./e2e-z1xpwSAg.js";
5
5
  import "./configShared-DKTgiz7K.js";
6
- import "./subcommands-D2iZ_g0A.js";
6
+ import "./subcommands-B_Lg_zDe.js";
7
7
  import "./notifyStore-2SWKGwZI.js";
8
8
  import "./webrtcLink-DLaJfTcB.js";
9
9
  import "./remotes-b6reVXzo.js";
10
- import { a as isPathInside, c as walkWorkspaces, i as importProvisionModule, l as workspaceStatus, n as cmdWs, o as loadProvision, r as collectWorkspaces, s as resolveOperand, t as WS_JSON_SCHEMA } from "./ws-PKok408W.js";
10
+ import { a as isPathInside, c as walkWorkspaces, i as importProvisionModule, l as workspaceStatus, n as cmdWs, o as loadProvision, r as collectWorkspaces, s as resolveOperand, t as WS_JSON_SCHEMA } from "./ws-BAy7xMDI.js";
11
11
 
12
12
  export { WS_JSON_SCHEMA, cmdWs, collectWorkspaces, isPathInside, loadProvision, walkWorkspaces, workspaceStatus };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-yes",
3
- "version": "1.284.1",
3
+ "version": "1.285.0",
4
4
  "description": "A wrapper tool that automates interactions with various AI CLI tools by automatically handling common prompts and responses.",
5
5
  "keywords": [
6
6
  "ai",
package/ts/cmdPs.spec.ts CHANGED
@@ -8,9 +8,10 @@ import {
8
8
  readPpidSync,
9
9
  renderTable,
10
10
  repoLabel,
11
+ subtreeTotals,
11
12
  type PsRow,
12
13
  } from "./cmdPs.ts";
13
- import type { SystemStats, TreeStats } from "./procStats.ts";
14
+ import type { ProcRow, SystemStats, TreeStats } from "./procStats.ts";
14
15
 
15
16
  const stats = (over: Partial<TreeStats> = {}): TreeStats => ({
16
17
  pid: 1,
@@ -151,11 +152,13 @@ const fixture: {
151
152
  trees: Map<number, TreeStats>;
152
153
  unattributed: TreeStats;
153
154
  system: SystemStats;
155
+ members: Map<number, ProcRow[]>;
154
156
  } = {
155
157
  records: [],
156
158
  trees: new Map(),
157
159
  unattributed: { pid: 0, rss: 0, cpuPercent: 0, procs: 0 },
158
160
  system: sys(),
161
+ members: new Map(),
159
162
  };
160
163
 
161
164
  vi.mock("./subcommands.ts", () => ({
@@ -172,6 +175,7 @@ vi.mock("./procStats.ts", async () => {
172
175
  sampleTrees: vi.fn(async () => ({
173
176
  trees: fixture.trees,
174
177
  unattributed: fixture.unattributed,
178
+ members: fixture.members ?? new Map(),
175
179
  })),
176
180
  };
177
181
  });
@@ -387,3 +391,124 @@ describe("parsePpidFromStat", () => {
387
391
  expect(parsePpidFromStat("1234 (bash) S notanumber 1234")).toBeNull();
388
392
  });
389
393
  });
394
+
395
+ describe("subtreeTotals", () => {
396
+ const n = (depth: number, rss: number, cpu: number, procs: number) => ({
397
+ depth,
398
+ stats: { pid: depth * 1000 + rss, rss, cpuPercent: cpu, procs },
399
+ });
400
+
401
+ it("returns null for a row with no descendants, so leaves render blank", () => {
402
+ expect(subtreeTotals([n(0, 10, 1, 1)])).toEqual([null]);
403
+ });
404
+
405
+ it("rolls a parent up with its children", () => {
406
+ const [parent, a, b] = subtreeTotals([n(0, 10, 1, 2), n(1, 5, 2, 3), n(1, 1, 4, 1)]);
407
+ expect(parent).toMatchObject({ rss: 16, cpuPercent: 7, procs: 6 });
408
+ expect(a).toBeNull();
409
+ expect(b).toBeNull();
410
+ });
411
+
412
+ it("includes grandchildren in the grandparent's total", () => {
413
+ // sampleTrees attributes exclusively, so without the deep walk a fan-out
414
+ // parent's real cost stays invisible.
415
+ const out = subtreeTotals([n(0, 1, 1, 1), n(1, 2, 2, 2), n(2, 4, 4, 4)]);
416
+ expect(out[0]).toMatchObject({ rss: 7, cpuPercent: 7, procs: 7 });
417
+ expect(out[1]).toMatchObject({ rss: 6, cpuPercent: 6, procs: 6 });
418
+ expect(out[2]).toBeNull();
419
+ });
420
+
421
+ it("stops at the next sibling — one subtree never absorbs another", () => {
422
+ const out = subtreeTotals([n(0, 1, 0, 1), n(1, 2, 0, 1), n(0, 100, 0, 1), n(1, 200, 0, 1)]);
423
+ expect(out[0]).toMatchObject({ rss: 3 });
424
+ expect(out[2]).toMatchObject({ rss: 300 });
425
+ });
426
+
427
+ it("skips a row whose stats are missing rather than throwing", () => {
428
+ // Defensive: the sampler can miss an agent that exited mid-window, and a
429
+ // rollup must not take the whole table down with it.
430
+ const missing = { depth: 1, stats: undefined as unknown as ReturnType<typeof stats> };
431
+ const out = subtreeTotals([n(0, 1, 0, 1), missing, n(1, 4, 0, 1)]);
432
+ expect(out[0]).toMatchObject({ rss: 5 }); // descendants: skipped + counted
433
+ // And a row whose OWN stats are missing yields null instead of throwing.
434
+ expect(subtreeTotals([missing])).toEqual([null]);
435
+ });
436
+
437
+ it("treats a missing depth as a root", () => {
438
+ const out = subtreeTotals([{ stats: { pid: 1, rss: 1, cpuPercent: 0, procs: 1 } }, n(1, 2, 0, 1)]);
439
+ expect(out[0]).toMatchObject({ rss: 3 });
440
+ });
441
+ });
442
+
443
+ describe("renderTable --tree", () => {
444
+ const treeRows = [
445
+ row({ pid: 1, repo: "alpha", stats: stats({ rss: 100, cpuPercent: 1, procs: 2 }) }),
446
+ row({ pid: 2, repo: "alpha", stats: stats({ rss: 50, cpuPercent: 2, procs: 1 }) }),
447
+ ];
448
+
449
+ it("omits the Σ columns entirely in flat mode", () => {
450
+ const out = renderTable(treeRows, null);
451
+ expect(out).not.toContain("ΣCPU%");
452
+ expect(out).not.toContain("ΣRSS");
453
+ });
454
+
455
+ it("lists each agent's OS processes under it, but only in tree mode", () => {
456
+ const withProcs = [
457
+ {
458
+ ...treeRows[0]!,
459
+ depth: 0,
460
+ prefix: "",
461
+ procRows: [
462
+ { pid: 11, ppid: 1, comm: "claude", rss: 1024, cpuPercent: 2, state: "S" },
463
+ { pid: 12, ppid: 11, comm: "bash", rss: 512, cpuPercent: 0, state: "S" },
464
+ { pid: 13, ppid: 11, comm: "gone", rss: 0, cpuPercent: 0, state: "Z" },
465
+ ],
466
+ },
467
+ ];
468
+ const flat = renderTable(withProcs, null);
469
+ expect(flat).not.toContain("bash"); // flat mode stays a one-row-per-agent table
470
+ const out = renderTable(withProcs, null, { tree: true });
471
+ expect(out).toContain("claude");
472
+ expect(out).toContain("bash");
473
+ // A zombie is called out — it is the one process state worth acting on.
474
+ expect(out).toContain("zombie");
475
+ });
476
+
477
+ it("renders a tree row that has no prefix at all (a lone root)", () => {
478
+ // prefix/depth are optional on PsRow; a row built without them must still
479
+ // render, and must not gain a stray indent.
480
+ const bare = [{ ...treeRows[0]!, subtree: null }];
481
+ const out = renderTable(bare, null, { tree: true });
482
+ const line = out.split("\n").find((l) => l.startsWith("1 ")) as string;
483
+ expect(line).toBeTruthy();
484
+ });
485
+
486
+ it("indents process rows relative to their agent's rails", () => {
487
+ const kid = { pid: 9, ppid: 1, comm: "zsh", rss: 1, cpuPercent: 0, state: "S" };
488
+ const nested = renderTable(
489
+ [{ ...treeRows[0]!, depth: 1, prefix: "└─ ", subtree: null, procRows: [kid] }],
490
+ null,
491
+ { tree: true },
492
+ );
493
+ const root = renderTable([{ ...treeRows[0]!, subtree: null, procRows: [kid] }], null, {
494
+ tree: true,
495
+ });
496
+ const indentOf = (out: string) =>
497
+ (out.split("\n").find((l) => l.includes("zsh")) as string).match(/^ */)![0].length;
498
+ // A nested agent's processes sit deeper than a root agent's.
499
+ expect(indentOf(nested)).toBeGreaterThan(indentOf(root));
500
+ });
501
+
502
+ it("adds the Σ columns and renders forest rails on the PID cell", () => {
503
+ const withTree = [
504
+ { ...treeRows[0]!, depth: 0, prefix: "", subtree: stats({ rss: 150, cpuPercent: 3, procs: 3 }) },
505
+ { ...treeRows[1]!, depth: 1, prefix: "└─ ", subtree: null },
506
+ ];
507
+ const out = renderTable(withTree, null, { tree: true });
508
+ expect(out).toContain("ΣCPU%");
509
+ const child = out.split("\n").find((l) => l.includes("└─ 2")) as string;
510
+ expect(child).toBeTruthy();
511
+ // A leaf's Σ cells stay blank — repeating its own numbers would be noise.
512
+ expect(child).not.toMatch(/\s3\s/);
513
+ });
514
+ });