@hwip/cli 0.0.1 → 0.1.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 (245) hide show
  1. package/LICENSE +190 -0
  2. package/NOTICE +11 -0
  3. package/README.md +1979 -0
  4. package/dist/agent-list.d.ts +76 -0
  5. package/dist/agent-list.js +102 -0
  6. package/dist/agent-read.d.ts +105 -0
  7. package/dist/agent-read.js +309 -0
  8. package/dist/agent-rules-fetch.d.ts +51 -0
  9. package/dist/agent-rules-fetch.js +275 -0
  10. package/dist/agent-rules.default.d.ts +30 -0
  11. package/dist/agent-rules.default.js +292 -0
  12. package/dist/agent-session.d.ts +23 -0
  13. package/dist/agent-session.js +44 -0
  14. package/dist/agent-state.d.ts +84 -0
  15. package/dist/agent-state.js +188 -0
  16. package/dist/agent-target.d.ts +60 -0
  17. package/dist/agent-target.js +81 -0
  18. package/dist/agents.d.ts +31 -0
  19. package/dist/agents.js +75 -0
  20. package/dist/api.d.ts +28 -0
  21. package/dist/api.js +76 -0
  22. package/dist/args.d.ts +74 -0
  23. package/dist/args.js +254 -0
  24. package/dist/ask-context-attach.d.ts +73 -0
  25. package/dist/ask-context-attach.js +151 -0
  26. package/dist/ask-context-screen.d.ts +21 -0
  27. package/dist/ask-context-screen.js +49 -0
  28. package/dist/ask-context-switch.d.ts +31 -0
  29. package/dist/ask-context-switch.js +74 -0
  30. package/dist/ask-context.d.ts +84 -0
  31. package/dist/ask-context.js +172 -0
  32. package/dist/ask-pending.d.ts +47 -0
  33. package/dist/ask-pending.js +221 -0
  34. package/dist/ask-seal.d.ts +120 -0
  35. package/dist/ask-seal.js +179 -0
  36. package/dist/ask.d.ts +107 -0
  37. package/dist/ask.js +551 -0
  38. package/dist/blocked-prompt-seal.d.ts +35 -0
  39. package/dist/blocked-prompt-seal.js +89 -0
  40. package/dist/blocked-prompt.d.ts +57 -0
  41. package/dist/blocked-prompt.js +181 -0
  42. package/dist/channel.d.ts +67 -0
  43. package/dist/channel.js +136 -0
  44. package/dist/check-update.d.ts +28 -0
  45. package/dist/check-update.js +153 -0
  46. package/dist/cli.d.ts +3 -0
  47. package/dist/cli.js +813 -0
  48. package/dist/codex-config.d.ts +24 -0
  49. package/dist/codex-config.js +169 -0
  50. package/dist/codex-transcript.d.ts +59 -0
  51. package/dist/codex-transcript.js +373 -0
  52. package/dist/command-refusal.d.ts +56 -0
  53. package/dist/command-refusal.js +127 -0
  54. package/dist/command-scan.d.ts +46 -0
  55. package/dist/command-scan.js +154 -0
  56. package/dist/command-seal.d.ts +57 -0
  57. package/dist/command-seal.js +88 -0
  58. package/dist/command-seq.d.ts +30 -0
  59. package/dist/command-seq.js +150 -0
  60. package/dist/config.d.ts +156 -0
  61. package/dist/config.js +392 -0
  62. package/dist/core-sections.d.ts +18 -0
  63. package/dist/core-sections.js +28 -0
  64. package/dist/crypto.d.ts +278 -0
  65. package/dist/crypto.js +698 -0
  66. package/dist/desktop-notify.d.ts +33 -0
  67. package/dist/desktop-notify.js +49 -0
  68. package/dist/device-directory.d.ts +36 -0
  69. package/dist/device-directory.js +97 -0
  70. package/dist/device-presence.d.ts +17 -0
  71. package/dist/device-presence.js +62 -0
  72. package/dist/downloads.d.ts +29 -0
  73. package/dist/downloads.js +106 -0
  74. package/dist/errors.d.ts +12 -0
  75. package/dist/errors.js +28 -0
  76. package/dist/file-crypto.d.ts +94 -0
  77. package/dist/file-crypto.js +272 -0
  78. package/dist/file-view-budget.d.ts +15 -0
  79. package/dist/file-view-budget.js +34 -0
  80. package/dist/file-view-switch.d.ts +28 -0
  81. package/dist/file-view-switch.js +122 -0
  82. package/dist/file-view.d.ts +108 -0
  83. package/dist/file-view.js +327 -0
  84. package/dist/gate.d.ts +141 -0
  85. package/dist/gate.js +336 -0
  86. package/dist/gemini-transcript.d.ts +32 -0
  87. package/dist/gemini-transcript.js +189 -0
  88. package/dist/http-failure.d.ts +98 -0
  89. package/dist/http-failure.js +349 -0
  90. package/dist/hwip-core.generated.d.ts +7 -0
  91. package/dist/hwip-core.generated.js +14 -0
  92. package/dist/hwip-hook.d.ts +77 -0
  93. package/dist/hwip-hook.js +340 -0
  94. package/dist/index.d.ts +7 -0
  95. package/dist/index.js +14 -0
  96. package/dist/input-sequencer.d.ts +65 -0
  97. package/dist/input-sequencer.js +142 -0
  98. package/dist/insecure-url.d.ts +16 -0
  99. package/dist/insecure-url.js +76 -0
  100. package/dist/installer.d.ts +96 -0
  101. package/dist/installer.js +850 -0
  102. package/dist/inventory-offload.d.ts +49 -0
  103. package/dist/inventory-offload.js +209 -0
  104. package/dist/inventory-worker.d.ts +2 -0
  105. package/dist/inventory-worker.js +27 -0
  106. package/dist/jsonc-doc.d.ts +106 -0
  107. package/dist/jsonc-doc.js +354 -0
  108. package/dist/keep-awake.d.ts +32 -0
  109. package/dist/keep-awake.js +46 -0
  110. package/dist/key-changes.d.ts +48 -0
  111. package/dist/key-changes.js +140 -0
  112. package/dist/lan-auth.d.ts +140 -0
  113. package/dist/lan-auth.js +217 -0
  114. package/dist/lan-endpoint.d.ts +41 -0
  115. package/dist/lan-endpoint.js +115 -0
  116. package/dist/lan-receiver.d.ts +122 -0
  117. package/dist/lan-receiver.js +510 -0
  118. package/dist/lan-sender.d.ts +76 -0
  119. package/dist/lan-sender.js +184 -0
  120. package/dist/lan-transfer.d.ts +36 -0
  121. package/dist/lan-transfer.js +54 -0
  122. package/dist/listener-device-id.d.ts +20 -0
  123. package/dist/listener-device-id.js +133 -0
  124. package/dist/listener-log.d.ts +4 -0
  125. package/dist/listener-log.js +23 -0
  126. package/dist/listener-process.d.ts +200 -0
  127. package/dist/listener-process.js +470 -0
  128. package/dist/listener-service.d.ts +225 -0
  129. package/dist/listener-service.js +682 -0
  130. package/dist/listener-worktree-cleanup.d.ts +27 -0
  131. package/dist/listener-worktree-cleanup.js +62 -0
  132. package/dist/listener.d.ts +1325 -0
  133. package/dist/listener.js +5680 -0
  134. package/dist/login-address.d.ts +25 -0
  135. package/dist/login-address.js +97 -0
  136. package/dist/login-page.d.ts +29 -0
  137. package/dist/login-page.js +93 -0
  138. package/dist/login.d.ts +98 -0
  139. package/dist/login.js +388 -0
  140. package/dist/mcp-command.d.ts +45 -0
  141. package/dist/mcp-command.js +50 -0
  142. package/dist/mcp.d.ts +19 -0
  143. package/dist/mcp.js +29 -0
  144. package/dist/node-support.d.ts +19 -0
  145. package/dist/node-support.js +38 -0
  146. package/dist/notify-failure.d.ts +57 -0
  147. package/dist/notify-failure.js +150 -0
  148. package/dist/opencode-transcript.d.ts +139 -0
  149. package/dist/opencode-transcript.js +390 -0
  150. package/dist/optional-features.d.ts +6 -0
  151. package/dist/optional-features.js +19 -0
  152. package/dist/pane-fit.d.ts +40 -0
  153. package/dist/pane-fit.js +119 -0
  154. package/dist/peer-key.d.ts +33 -0
  155. package/dist/peer-key.js +34 -0
  156. package/dist/pi-transcript.d.ts +26 -0
  157. package/dist/pi-transcript.js +255 -0
  158. package/dist/presence.d.ts +14 -0
  159. package/dist/presence.js +124 -0
  160. package/dist/private-files.d.ts +51 -0
  161. package/dist/private-files.js +218 -0
  162. package/dist/project-switch.d.ts +19 -0
  163. package/dist/project-switch.js +60 -0
  164. package/dist/redact.d.ts +10 -0
  165. package/dist/redact.js +21 -0
  166. package/dist/remote-agents.d.ts +410 -0
  167. package/dist/remote-agents.js +1218 -0
  168. package/dist/remote-hook.d.ts +12 -0
  169. package/dist/remote-hook.js +196 -0
  170. package/dist/replace-file.d.ts +50 -0
  171. package/dist/replace-file.js +328 -0
  172. package/dist/schedule-command.d.ts +35 -0
  173. package/dist/schedule-command.js +260 -0
  174. package/dist/schedule-launch.d.ts +15 -0
  175. package/dist/schedule-launch.js +24 -0
  176. package/dist/schedule-ready.d.ts +44 -0
  177. package/dist/schedule-ready.js +71 -0
  178. package/dist/schedule-remote.d.ts +77 -0
  179. package/dist/schedule-remote.js +167 -0
  180. package/dist/schedule-runner.d.ts +89 -0
  181. package/dist/schedule-runner.js +452 -0
  182. package/dist/schedule.d.ts +149 -0
  183. package/dist/schedule.js +428 -0
  184. package/dist/secret-mask.d.ts +29 -0
  185. package/dist/secret-mask.js +245 -0
  186. package/dist/self-register.d.ts +45 -0
  187. package/dist/self-register.js +67 -0
  188. package/dist/send.d.ts +56 -0
  189. package/dist/send.js +104 -0
  190. package/dist/session-diff.d.ts +161 -0
  191. package/dist/session-diff.js +445 -0
  192. package/dist/session-registry.d.ts +143 -0
  193. package/dist/session-registry.js +216 -0
  194. package/dist/sessions-report.d.ts +59 -0
  195. package/dist/sessions-report.js +99 -0
  196. package/dist/shell-audit.d.ts +34 -0
  197. package/dist/shell-audit.js +47 -0
  198. package/dist/subagent-transcripts.d.ts +167 -0
  199. package/dist/subagent-transcripts.js +437 -0
  200. package/dist/subscriber-trust.d.ts +128 -0
  201. package/dist/subscriber-trust.js +397 -0
  202. package/dist/templates.d.ts +105 -0
  203. package/dist/templates.js +871 -0
  204. package/dist/tmux-install.d.ts +44 -0
  205. package/dist/tmux-install.js +65 -0
  206. package/dist/tmux-liveness.d.ts +45 -0
  207. package/dist/tmux-liveness.js +71 -0
  208. package/dist/tmux-pane.d.ts +55 -0
  209. package/dist/tmux-pane.js +126 -0
  210. package/dist/tmux-socket.d.ts +20 -0
  211. package/dist/tmux-socket.js +292 -0
  212. package/dist/todo-fold.d.ts +31 -0
  213. package/dist/todo-fold.js +102 -0
  214. package/dist/transcript-tail.d.ts +312 -0
  215. package/dist/transcript-tail.js +718 -0
  216. package/dist/trust-command.d.ts +49 -0
  217. package/dist/trust-command.js +200 -0
  218. package/dist/trust-lock.d.ts +26 -0
  219. package/dist/trust-lock.js +94 -0
  220. package/dist/trust-pins.d.ts +37 -0
  221. package/dist/trust-pins.js +82 -0
  222. package/dist/turn-recorder-wire.d.ts +70 -0
  223. package/dist/turn-recorder-wire.js +165 -0
  224. package/dist/turn-recorder.d.ts +190 -0
  225. package/dist/turn-recorder.js +695 -0
  226. package/dist/turn-ring.d.ts +93 -0
  227. package/dist/turn-ring.js +418 -0
  228. package/dist/turn-watch.d.ts +177 -0
  229. package/dist/turn-watch.js +630 -0
  230. package/dist/types.d.ts +63 -0
  231. package/dist/types.js +2 -0
  232. package/dist/uninstall.d.ts +55 -0
  233. package/dist/uninstall.js +507 -0
  234. package/dist/usage.d.ts +17 -0
  235. package/dist/usage.js +421 -0
  236. package/dist/verify.d.ts +47 -0
  237. package/dist/verify.js +396 -0
  238. package/dist/worktree.d.ts +144 -0
  239. package/dist/worktree.js +310 -0
  240. package/dist/wrapper.d.ts +254 -0
  241. package/dist/wrapper.js +704 -0
  242. package/dist/ws-url.d.ts +109 -0
  243. package/dist/ws-url.js +202 -0
  244. package/package.json +78 -10
  245. package/bin/hwip.js +0 -3
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Claude Code subagents — the ones that never get a tmux pane.
3
+ *
4
+ * `listener.ts` finds a subagent by looking for an extra pane of a session
5
+ * (pi splits one off per subagent). Claude Code's `Agent` tool runs its
6
+ * subagents in-process instead: no pane, no process of their own, nothing the
7
+ * pane sweep can see. What they do leave is a transcript, beside the parent's:
8
+ *
9
+ * ~/.claude/projects/<project>/<sessionId>.jsonl ← parent
10
+ * ~/.claude/projects/<project>/<sessionId>/subagents/
11
+ * agent-<agentId>.jsonl ← one per subagent
12
+ *
13
+ * So this module is the pane sweep's counterpart for a source that is a
14
+ * directory of files rather than a list of panes. It returns the same thing the
15
+ * sweep returns — rows the phone can show — and nothing about what a row means
16
+ * changes: `<parent>.<n>` is still a view-only wire name, still never a tmux
17
+ * target (see `subagentParentOf` in the app's shared package).
18
+ *
19
+ * Everything here is Claude Code's private layout, measured on 2.1.268, not a
20
+ * published contract. The one state that proves it moved — the parent says it
21
+ * launched a subagent, the directory is not there — is logged once, because
22
+ * the alternative failure is this feature vanishing in silence.
23
+ */
24
+ import { type TailState } from './transcript-tail.js';
25
+ /** How recently a subagent transcript must have grown to count as working. */
26
+ export declare const SUBAGENT_WORKING_MS = 15000;
27
+ /**
28
+ * How long a quiet subagent stays on the roster.
29
+ *
30
+ * A finished subagent leaves its file behind forever — a session directory
31
+ * holds a dozen of them within a day (measured) — so "the file exists" cannot
32
+ * be what puts a chip on the phone. Recent activity is.
33
+ */
34
+ export declare const SUBAGENT_LIVE_MS: number;
35
+ /**
36
+ * How long a subagent stays ADDRESSABLE after it goes quiet — longer than it
37
+ * stays on the roster, because the two answer different questions.
38
+ *
39
+ * A chip is "is this worth showing"; an address is "can a viewer still read
40
+ * this". Dropping the address on the roster's clock closes the screen of
41
+ * whoever is reading it: the watcher asks whether the session still exists,
42
+ * the answer comes from this map, and a subagent thinking for five minutes
43
+ * (a long Bash, a long tool chain) would take its own reader down with it.
44
+ */
45
+ export declare const SUBAGENT_ADDRESSABLE_MS: number;
46
+ /**
47
+ * How far a subagent's last write may trail the parent's record of its end and
48
+ * still belong to the run that ended.
49
+ *
50
+ * The final flush lands after the notice: measured 2026-09-14 over 572 ended
51
+ * subagents, 20 wrote after their end record, all within 42 ms. The other three
52
+ * that did came 153 s or more later — resumed work, which must not read as
53
+ * ended. A second sits far from both.
54
+ */
55
+ export declare const SUBAGENT_END_SLACK_MS = 1000;
56
+ /**
57
+ * How much subagent transcript one sweep reads, in total, before it stops and
58
+ * lets the next sweep carry on. A first sight reads from the top (the count and
59
+ * the start have to be the subagent's own, not a window's), and transcripts
60
+ * reach megabytes — so a very large one, or many sessions' worth after a
61
+ * restart, is caught up over a few sweeps instead of one long stall, and a count
62
+ * only rises toward the true one meanwhile. A caller that sweeps several
63
+ * sessions passes one shared `catchUp` so the total holds across all of them.
64
+ */
65
+ export declare const SUBAGENT_CATCH_UP_BYTES: number;
66
+ /** What is left of a sweep's read budget; shared by every scan that gets the same object. */
67
+ export interface CatchUpBudget {
68
+ bytes: number;
69
+ }
70
+ /**
71
+ * How often a file that has been quiet past `SUBAGENT_ADDRESSABLE_MS` is
72
+ * looked at again. It cannot be on the roster, so the sweep has no use for its
73
+ * mtime — except to notice that it was written to, which a resumed subagent
74
+ * does (measured, see `SUBAGENT_END_SLACK_MS`). A minute late is fine for that;
75
+ * a stat of every old file every few seconds is not, once a long session's
76
+ * directory holds hundreds of them.
77
+ */
78
+ export declare const SUBAGENT_STALE_RECHECK_MS = 60000;
79
+ /** Rows one session may contribute. The switcher is a strip of chips, not a list. */
80
+ export declare const MAX_SUBAGENT_ROWS = 5;
81
+ export interface SubagentRow {
82
+ /** Wire name: `<parent>.<n>`, the shape the server reads as view-only. */
83
+ readonly name: string;
84
+ /** Recent enough to belong on the roster. A row that is not live is still
85
+ * addressable — see `SUBAGENT_ADDRESSABLE_MS`. */
86
+ readonly live: boolean;
87
+ readonly agentId: string;
88
+ readonly label: string;
89
+ readonly transcriptPath: string;
90
+ readonly lastActivityAt: string;
91
+ readonly working: boolean;
92
+ /**
93
+ * Calls the subagent has made — the phone's only measure of how much is
94
+ * happening inside a session that has gone quiet.
95
+ *
96
+ * Counted from the top of its transcript the first time it is seen, then
97
+ * incrementally, so a listener that restarts mid-subagent shows the same
98
+ * number it did before. A transcript too large for one sweep is read over
99
+ * several (`SUBAGENT_CATCH_UP_BYTES`) and reports a lower bound meanwhile.
100
+ */
101
+ readonly toolCount: number;
102
+ /** Its first stamped event, or null while it has written nothing worth timing. Null rather than the file's mtime: a fresh
103
+ * file would otherwise claim a duration it never ran for. */
104
+ readonly startedAt: string | null;
105
+ }
106
+ /** What has been read out of one subagent's transcript so far. */
107
+ interface Progress {
108
+ tail: TailState | null;
109
+ toolCount: number;
110
+ startedAt: string | null;
111
+ }
112
+ /**
113
+ * What must survive between sweeps, per parent session.
114
+ *
115
+ * `order` is the reason this is not a pure function: a subagent's number is
116
+ * whatever it got the first time it was seen. Numbering by anything read off
117
+ * the filesystem — mtime, readdir order — renumbers live chips underneath the
118
+ * viewer as other subagents come and go.
119
+ */
120
+ export interface SubagentScanState {
121
+ readonly order: Map<string, number>;
122
+ readonly labels: Map<string, string>;
123
+ /** Per subagent, by agentId: where its transcript was last read to, and
124
+ * what had been counted by then. Incremental for the same reason the
125
+ * parent's scan is — these files reach megabytes and this runs every sweep. */
126
+ readonly progress: Map<string, Progress>;
127
+ /** Per subagent, by agentId: when the parent was last told it ended. */
128
+ readonly endedAt: Map<string, number>;
129
+ /** Per subagent file, by agentId: the mtime last read and when. A file quiet
130
+ * past `SUBAGENT_ADDRESSABLE_MS` is trusted for `SUBAGENT_STALE_RECHECK_MS`
131
+ * rather than stat'd on every sweep. */
132
+ readonly seen: Map<string, {
133
+ mtimeMs: number;
134
+ checkedAt: number;
135
+ }>;
136
+ tail: TailState | null;
137
+ nextNumber: number;
138
+ launchSeen: boolean;
139
+ driftLogged: boolean;
140
+ }
141
+ export declare const initialSubagentScanState: () => SubagentScanState;
142
+ /** Where Claude Code keeps this session's subagent transcripts. */
143
+ export declare const subagentDirFor: (parentTranscriptPath: string) => string;
144
+ /**
145
+ * Whether a transcript is a subagent's own — which decides whether its sidechain
146
+ * entries are internals to hide or the whole content to show.
147
+ *
148
+ * Asked of the PATH rather than the session name: a name says what the phone
149
+ * calls a row, the directory says what Claude Code wrote, and the projection
150
+ * cares about the second.
151
+ */
152
+ export declare const isSubagentTranscriptPath: (transcriptPath: string) => boolean;
153
+ /**
154
+ * The live subagents of one Claude Code session, in chip order (by number).
155
+ *
156
+ * `state` is mutated — it belongs to the caller and lives as long as the
157
+ * session does, which is what keeps a chip's number and label stable across
158
+ * sweeps.
159
+ */
160
+ export declare const scanSubagents: (parentName: string, parentTranscriptPath: string, state: SubagentScanState, opts?: {
161
+ now?: number;
162
+ log?: (message: string) => void;
163
+ reserved?: ReadonlySet<number>;
164
+ catchUp?: CatchUpBudget;
165
+ }) => SubagentRow[];
166
+ export {};
167
+ //# sourceMappingURL=subagent-transcripts.d.ts.map
@@ -0,0 +1,437 @@
1
+ "use strict";
2
+ /**
3
+ * Claude Code subagents — the ones that never get a tmux pane.
4
+ *
5
+ * `listener.ts` finds a subagent by looking for an extra pane of a session
6
+ * (pi splits one off per subagent). Claude Code's `Agent` tool runs its
7
+ * subagents in-process instead: no pane, no process of their own, nothing the
8
+ * pane sweep can see. What they do leave is a transcript, beside the parent's:
9
+ *
10
+ * ~/.claude/projects/<project>/<sessionId>.jsonl ← parent
11
+ * ~/.claude/projects/<project>/<sessionId>/subagents/
12
+ * agent-<agentId>.jsonl ← one per subagent
13
+ *
14
+ * So this module is the pane sweep's counterpart for a source that is a
15
+ * directory of files rather than a list of panes. It returns the same thing the
16
+ * sweep returns — rows the phone can show — and nothing about what a row means
17
+ * changes: `<parent>.<n>` is still a view-only wire name, still never a tmux
18
+ * target (see `subagentParentOf` in the app's shared package).
19
+ *
20
+ * Everything here is Claude Code's private layout, measured on 2.1.268, not a
21
+ * published contract. The one state that proves it moved — the parent says it
22
+ * launched a subagent, the directory is not there — is logged once, because
23
+ * the alternative failure is this feature vanishing in silence.
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.scanSubagents = exports.isSubagentTranscriptPath = exports.subagentDirFor = exports.initialSubagentScanState = exports.MAX_SUBAGENT_ROWS = exports.SUBAGENT_STALE_RECHECK_MS = exports.SUBAGENT_CATCH_UP_BYTES = exports.SUBAGENT_END_SLACK_MS = exports.SUBAGENT_ADDRESSABLE_MS = exports.SUBAGENT_LIVE_MS = exports.SUBAGENT_WORKING_MS = void 0;
27
+ const node_fs_1 = require("node:fs");
28
+ const node_path_1 = require("node:path");
29
+ const transcript_tail_js_1 = require("./transcript-tail.js");
30
+ /** How recently a subagent transcript must have grown to count as working. */
31
+ exports.SUBAGENT_WORKING_MS = 15_000;
32
+ /**
33
+ * How long a quiet subagent stays on the roster.
34
+ *
35
+ * A finished subagent leaves its file behind forever — a session directory
36
+ * holds a dozen of them within a day (measured) — so "the file exists" cannot
37
+ * be what puts a chip on the phone. Recent activity is.
38
+ */
39
+ exports.SUBAGENT_LIVE_MS = 5 * 60_000;
40
+ /**
41
+ * How long a subagent stays ADDRESSABLE after it goes quiet — longer than it
42
+ * stays on the roster, because the two answer different questions.
43
+ *
44
+ * A chip is "is this worth showing"; an address is "can a viewer still read
45
+ * this". Dropping the address on the roster's clock closes the screen of
46
+ * whoever is reading it: the watcher asks whether the session still exists,
47
+ * the answer comes from this map, and a subagent thinking for five minutes
48
+ * (a long Bash, a long tool chain) would take its own reader down with it.
49
+ */
50
+ exports.SUBAGENT_ADDRESSABLE_MS = 60 * 60_000;
51
+ /**
52
+ * How far a subagent's last write may trail the parent's record of its end and
53
+ * still belong to the run that ended.
54
+ *
55
+ * The final flush lands after the notice: measured 2026-09-14 over 572 ended
56
+ * subagents, 20 wrote after their end record, all within 42 ms. The other three
57
+ * that did came 153 s or more later — resumed work, which must not read as
58
+ * ended. A second sits far from both.
59
+ */
60
+ exports.SUBAGENT_END_SLACK_MS = 1_000;
61
+ /**
62
+ * How much subagent transcript one sweep reads, in total, before it stops and
63
+ * lets the next sweep carry on. A first sight reads from the top (the count and
64
+ * the start have to be the subagent's own, not a window's), and transcripts
65
+ * reach megabytes — so a very large one, or many sessions' worth after a
66
+ * restart, is caught up over a few sweeps instead of one long stall, and a count
67
+ * only rises toward the true one meanwhile. A caller that sweeps several
68
+ * sessions passes one shared `catchUp` so the total holds across all of them.
69
+ */
70
+ exports.SUBAGENT_CATCH_UP_BYTES = 4 * 1024 * 1024;
71
+ /**
72
+ * How often a file that has been quiet past `SUBAGENT_ADDRESSABLE_MS` is
73
+ * looked at again. It cannot be on the roster, so the sweep has no use for its
74
+ * mtime — except to notice that it was written to, which a resumed subagent
75
+ * does (measured, see `SUBAGENT_END_SLACK_MS`). A minute late is fine for that;
76
+ * a stat of every old file every few seconds is not, once a long session's
77
+ * directory holds hundreds of them.
78
+ */
79
+ exports.SUBAGENT_STALE_RECHECK_MS = 60_000;
80
+ /** Rows one session may contribute. The switcher is a strip of chips, not a list. */
81
+ exports.MAX_SUBAGENT_ROWS = 5;
82
+ /** Quiet-but-addressable subagents kept beyond the live ones, so a viewer
83
+ * reading one is not cut off the moment newer subagents crowd the roster. */
84
+ const MAX_ADDRESSABLE_EXTRA = 5;
85
+ const AGENT_FILE = /^agent-(.+)\.jsonl$/;
86
+ const initialSubagentScanState = () => ({
87
+ order: new Map(),
88
+ labels: new Map(),
89
+ progress: new Map(),
90
+ endedAt: new Map(),
91
+ seen: new Map(),
92
+ tail: (0, transcript_tail_js_1.initialTailState)(),
93
+ nextNumber: 1,
94
+ launchSeen: false,
95
+ driftLogged: false,
96
+ });
97
+ exports.initialSubagentScanState = initialSubagentScanState;
98
+ const SUBAGENT_DIR = 'subagents';
99
+ /** Where Claude Code keeps this session's subagent transcripts. */
100
+ const subagentDirFor = (parentTranscriptPath) => (0, node_path_1.join)((0, node_path_1.dirname)(parentTranscriptPath), (0, node_path_1.basename)(parentTranscriptPath, '.jsonl'), SUBAGENT_DIR);
101
+ exports.subagentDirFor = subagentDirFor;
102
+ /**
103
+ * Whether a transcript is a subagent's own — which decides whether its sidechain
104
+ * entries are internals to hide or the whole content to show.
105
+ *
106
+ * Asked of the PATH rather than the session name: a name says what the phone
107
+ * calls a row, the directory says what Claude Code wrote, and the projection
108
+ * cares about the second.
109
+ */
110
+ const isSubagentTranscriptPath = (transcriptPath) => (0, node_path_1.basename)((0, node_path_1.dirname)(transcriptPath)) === SUBAGENT_DIR;
111
+ exports.isSubagentTranscriptPath = isSubagentTranscriptPath;
112
+ /**
113
+ * Watch the parent for the one thing the transcript directory cannot say: that
114
+ * a subagent was launched at all. That is what separates "this session never
115
+ * spawned" from "Claude Code stopped writing where we look" (see the drift log
116
+ * below).
117
+ *
118
+ * An `Agent` tool call is the signal — both spawn kinds make one, while the
119
+ * launch record (`toolUseResult.agentId`) is written only for the async ones.
120
+ * It is not every spawn: measured 2026-09-13 over every session on this
121
+ * machine, 33 of the 137 sessions that have subagent transcripts never called
122
+ * `Agent` in the parent (a skill or a teammate spawned them). Those sessions
123
+ * keep working; they just cannot report layout drift, which is a diagnostic.
124
+ *
125
+ * Incremental on purpose — a parent transcript reaches megabytes, and this runs
126
+ * every sweep. `readTranscriptDelta` returns null when the file has not moved,
127
+ * which is the normal case and costs nothing.
128
+ */
129
+ const isAgentToolUse = (line) => {
130
+ // Cheap reject first: parsing every line of a megabyte transcript to find
131
+ // one tool call would undo what the incremental read is for. The substring
132
+ // alone is not the answer — a transcript that merely QUOTES this code (a
133
+ // review of this file, say) contains it as prose.
134
+ if (!line.includes('"name":"Agent"'))
135
+ return false;
136
+ let entry;
137
+ try {
138
+ entry = JSON.parse(line);
139
+ }
140
+ catch {
141
+ return false;
142
+ }
143
+ const content = entry.message?.content;
144
+ return Array.isArray(content)
145
+ && content.some((block) => block?.type === 'tool_use' && block?.name === 'Agent');
146
+ };
147
+ /** The four ways Claude Code reports a background subagent stopping — measured
148
+ * 2026-09-14 over every parent transcript on this machine (787 completed, 12
149
+ * failed, 12 killed, 2 stopped, nothing else). */
150
+ const TASK_END = /<status>(?:completed|failed|killed|stopped)<\/status>/;
151
+ const TASK_ID = /<task-id>([^<]{1,200})<\/task-id>/;
152
+ /**
153
+ * The subagent a parent line says has ended, and when — or null.
154
+ *
155
+ * Two shapes, one per spawn kind. A background subagent's end arrives as a user
156
+ * turn the harness writes (`origin.kind: 'task-notification'`) naming it by
157
+ * `<task-id>`; a foreground one's is its `Agent` call returning, with the
158
+ * agent id on `toolUseResult`. The async LAUNCH record carries an agent id too,
159
+ * which is why the status is part of the match and not only the id.
160
+ *
161
+ * Both marks are the harness's own fields, never text: the parent's reply
162
+ * quoting a notice — which is what a session reporting its subagent's result
163
+ * does — carries the words but neither `origin` nor `toolUseResult`.
164
+ */
165
+ const subagentEndOf = (line) => {
166
+ // Cheap reject, as in `isAgentToolUse`: this reads every line the parent writes.
167
+ if (!line.includes('<task-notification>') && !line.includes('"status":"completed"'))
168
+ return null;
169
+ let entry;
170
+ try {
171
+ entry = JSON.parse(line);
172
+ }
173
+ catch {
174
+ return null;
175
+ }
176
+ if (typeof entry.timestamp !== 'string')
177
+ return null;
178
+ const at = Date.parse(entry.timestamp);
179
+ if (Number.isNaN(at))
180
+ return null;
181
+ const result = entry.toolUseResult;
182
+ if (result?.status === 'completed' && typeof result.agentId === 'string')
183
+ return { agentId: result.agentId, at };
184
+ const content = entry.message?.content;
185
+ if (entry.origin?.kind !== 'task-notification' || typeof content !== 'string' || !TASK_END.test(content))
186
+ return null;
187
+ const agentId = TASK_ID.exec(content)?.[1];
188
+ return agentId ? { agentId, at } : null;
189
+ };
190
+ /**
191
+ * Fold what the parent appended since the last sweep: whether it launched a
192
+ * subagent (until it has), and which subagents it was told have ended.
193
+ *
194
+ * Read every sweep, not only until the launch is seen, because the end notice
195
+ * comes later. Still costs a stat on a sweep where the parent has not moved.
196
+ * The first read opens a window off the end (`TRANSCRIPT_BACKFILL_BYTES`), so an
197
+ * end older than that window is not seen — that subagent leaves the roster on
198
+ * `SUBAGENT_LIVE_MS` instead, as every subagent did before this signal.
199
+ */
200
+ const absorbParentDelta = (parentTranscriptPath, state) => {
201
+ const read = (0, transcript_tail_js_1.readTranscriptDelta)(parentTranscriptPath, state.tail);
202
+ if (!read)
203
+ return;
204
+ state.tail = read.state;
205
+ if (!state.launchSeen)
206
+ state.launchSeen = read.lines.some(isAgentToolUse);
207
+ for (const line of read.lines) {
208
+ const end = subagentEndOf(line);
209
+ // The latest notice wins: a resumed subagent ends again.
210
+ if (end && end.at > (state.endedAt.get(end.agentId) ?? -Infinity))
211
+ state.endedAt.set(end.agentId, end.at);
212
+ }
213
+ };
214
+ /**
215
+ * The subagent's own label, from the sidecar Claude Code writes beside its
216
+ * transcript (`agent-<id>.meta.json`).
217
+ *
218
+ * Measured 2026-09-13 over every session on this machine — 823 subagent
219
+ * transcripts, 823 sidecars, `agentType` on all of them and `description` on
220
+ * 817 — while the parent's launch record covers only the async spawns (53%).
221
+ * So the sidecar is the source, and the parent is read for nothing but the
222
+ * fact that a spawn happened.
223
+ */
224
+ const readSidecarLabel = (transcriptPath) => {
225
+ let meta;
226
+ try {
227
+ meta = JSON.parse((0, node_fs_1.readFileSync)(`${transcriptPath.slice(0, -'.jsonl'.length)}.meta.json`, 'utf-8'));
228
+ }
229
+ catch {
230
+ return null;
231
+ }
232
+ for (const value of [meta.description, meta.agentType]) {
233
+ if (typeof value === 'string' && value.trim())
234
+ return value.trim();
235
+ }
236
+ return null;
237
+ };
238
+ /**
239
+ * The label for one subagent, cached once found. A positional fallback is NOT
240
+ * cached: the sidecar is written beside the transcript, so a scan that caught a
241
+ * subagent in its first moments can still learn its name on the next sweep.
242
+ */
243
+ const rememberLabel = (state, agentId, transcriptPath, number) => {
244
+ const label = readSidecarLabel(transcriptPath);
245
+ if (!label)
246
+ return `Agent ${number}`;
247
+ state.labels.set(agentId, label);
248
+ return label;
249
+ };
250
+ /**
251
+ * Read what a subagent has appended since the last sweep, and fold it into the
252
+ * running count.
253
+ *
254
+ * Its own transcript is entirely sidechain entries — that is what makes it a
255
+ * subagent's file — so the projection is told to keep them, exactly as
256
+ * `turn-watch` does when it tails one of these for a viewer.
257
+ */
258
+ const advanceProgress = (state, agentId, path, budget) => {
259
+ let progress = state.progress.get(agentId) ?? { tail: (0, transcript_tail_js_1.initialTailState)(), toolCount: 0, startedAt: null };
260
+ // Until caught up or out of budget: the usual sweep reads a few lines and
261
+ // stops after one pass, a first sight of a big file takes several.
262
+ while (budget.bytes > 0) {
263
+ const read = (0, transcript_tail_js_1.readTranscriptDelta)(path, progress.tail, { fromStart: true });
264
+ if (!read)
265
+ break;
266
+ // A new inode at the same path is a different file, and a file now shorter
267
+ // than where this read had got to was cut in place; everything counted off
268
+ // the old one describes work this one never did.
269
+ const restarted = progress.tail !== null
270
+ && (read.state.ino !== progress.tail.ino || read.state.size < progress.tail.offset);
271
+ const events = (0, transcript_tail_js_1.projectTranscriptEntries)(read.lines, { includeSidechain: true });
272
+ const firstStamp = events.find((event) => event.at)?.at ?? null;
273
+ progress = {
274
+ tail: read.state,
275
+ toolCount: (restarted ? 0 : progress.toolCount) + events.filter((event) => event.kind === 'tool').length,
276
+ startedAt: restarted ? firstStamp : (progress.startedAt ?? firstStamp),
277
+ };
278
+ state.progress.set(agentId, progress);
279
+ // Nothing consumed means the read stopped short of a character boundary
280
+ // (a write caught mid-sequence): leave it for the next sweep.
281
+ if (read.bytesRead === 0)
282
+ break;
283
+ budget.bytes -= read.bytesRead;
284
+ if (read.state.offset >= read.state.size)
285
+ break;
286
+ }
287
+ return progress;
288
+ };
289
+ /** `null` ONLY when the directory is not there; `'unreadable'` when it could not
290
+ * be listed for any other reason — reported as its own failure rather than as
291
+ * the layout having moved, and never taken as "the subagents are gone".
292
+ *
293
+ * A file is stat'd when it is new to this scan, when it was recently active
294
+ * (its mtime is what orders the roster), or when a quiet one is due another look
295
+ * (`SUBAGENT_STALE_RECHECK_MS`). Every other file — the long tail of subagents
296
+ * that finished hours ago — is taken from the last stat. */
297
+ const readAgentFiles = (dir, state, now, log) => {
298
+ let names;
299
+ try {
300
+ names = (0, node_fs_1.readdirSync)(dir);
301
+ }
302
+ catch (err) {
303
+ if (err.code === 'ENOENT')
304
+ return null;
305
+ log?.(`! ${dir}: unreadable (${err.message}) — subagents of this session stay hidden`);
306
+ return 'unreadable';
307
+ }
308
+ const files = [];
309
+ const present = new Set();
310
+ for (const name of names) {
311
+ const agentId = AGENT_FILE.exec(name)?.[1];
312
+ if (!agentId)
313
+ continue;
314
+ present.add(agentId);
315
+ const path = (0, node_path_1.join)(dir, name);
316
+ const known = state.seen.get(agentId);
317
+ if (known && now - known.mtimeMs > exports.SUBAGENT_ADDRESSABLE_MS && known.checkedAt <= now && now - known.checkedAt < exports.SUBAGENT_STALE_RECHECK_MS) {
318
+ files.push({ agentId, path, mtimeMs: known.mtimeMs });
319
+ continue;
320
+ }
321
+ try {
322
+ const mtimeMs = (0, node_fs_1.statSync)(path).mtimeMs;
323
+ state.seen.set(agentId, { mtimeMs, checkedAt: now });
324
+ files.push({ agentId, path, mtimeMs });
325
+ }
326
+ catch {
327
+ // Vanished between readdir and stat: a subagent file is never
328
+ // rewritten, so this is a deletion, not a race worth reporting.
329
+ present.delete(agentId);
330
+ }
331
+ }
332
+ for (const agentId of state.seen.keys())
333
+ if (!present.has(agentId))
334
+ state.seen.delete(agentId);
335
+ return files;
336
+ };
337
+ /**
338
+ * Drop what was kept about subagents that are off the roster for good: the file
339
+ * is gone, or has been quiet past `SUBAGENT_ADDRESSABLE_MS`. The maps are keyed
340
+ * by agentId and a session can run for days, so without this each holds a
341
+ * finished subagent's entry for as long as the session lives. A subagent that
342
+ * resumes after that is simply seen anew — read from the top, so its count is
343
+ * still its own. `seen` keeps the quiet ones: it is what spares them a stat.
344
+ */
345
+ const forgetDormant = (state, files, now) => {
346
+ const active = new Set(files.filter((file) => now - file.mtimeMs <= exports.SUBAGENT_ADDRESSABLE_MS).map((file) => file.agentId));
347
+ for (const map of [state.order, state.labels, state.progress, state.endedAt]) {
348
+ for (const agentId of map.keys())
349
+ if (!active.has(agentId))
350
+ map.delete(agentId);
351
+ }
352
+ };
353
+ /**
354
+ * The live subagents of one Claude Code session, in chip order (by number).
355
+ *
356
+ * `state` is mutated — it belongs to the caller and lives as long as the
357
+ * session does, which is what keeps a chip's number and label stable across
358
+ * sweeps.
359
+ */
360
+ const scanSubagents = (parentName, parentTranscriptPath, state, opts = {}) => {
361
+ const now = opts.now ?? Date.now();
362
+ const catchUp = opts.catchUp ?? { bytes: exports.SUBAGENT_CATCH_UP_BYTES };
363
+ absorbParentDelta(parentTranscriptPath, state);
364
+ const dir = (0, exports.subagentDirFor)(parentTranscriptPath);
365
+ const files = readAgentFiles(dir, state, now, opts.log);
366
+ // A listing that failed says nothing about the subagents: keep every number
367
+ // and count, and show nothing this sweep, as before.
368
+ if (files === 'unreadable')
369
+ return [];
370
+ if (!files) {
371
+ forgetDormant(state, [], now);
372
+ state.seen.clear();
373
+ // No directory. Only surprising once the parent has said it launched
374
+ // something — before that, this is every session that never spawned.
375
+ if (state.launchSeen && !state.driftLogged) {
376
+ state.driftLogged = true;
377
+ opts.log?.(`! ${parentName}: launched a subagent but ${dir} does not exist — Claude Code layout changed?`);
378
+ }
379
+ return [];
380
+ }
381
+ forgetDormant(state, files, now);
382
+ const kept = files
383
+ .sort((a, b) => b.mtimeMs - a.mtimeMs)
384
+ .filter((file) => now - file.mtimeMs <= exports.SUBAGENT_ADDRESSABLE_MS)
385
+ .slice(0, exports.MAX_SUBAGENT_ROWS + MAX_ADDRESSABLE_EXTRA)
386
+ // The roster is the most recent few; everything else kept here is an
387
+ // address for a viewer who is already inside one of them.
388
+ .map((file, index) => {
389
+ const endedAt = state.endedAt.get(file.agentId);
390
+ // Ended unless it wrote again after the notice — a resumed subagent
391
+ // is working again, and the notice describes the run before.
392
+ const ended = endedAt !== undefined && file.mtimeMs <= endedAt + exports.SUBAGENT_END_SLACK_MS;
393
+ return {
394
+ ...file,
395
+ ended,
396
+ live: !ended && index < exports.MAX_SUBAGENT_ROWS && now - file.mtimeMs <= exports.SUBAGENT_LIVE_MS,
397
+ };
398
+ });
399
+ // Numbered in activity order, then handed back in number order: the cut
400
+ // above keeps the subagents worth showing, and this keeps the chips from
401
+ // swapping places under the viewer every time one of them writes a line.
402
+ const rows = kept.map((file) => {
403
+ // A pane subagent of the same session owns its pane id as a number
404
+ // (`hwip-x.48`), and two rows with one name is a switcher showing the
405
+ // wrong transcript. Checked on every sweep, not only when the number is
406
+ // first handed out: pane ids keep being created, so a number that was
407
+ // free can stop being free — and a chip that renumbers beats two chips
408
+ // that are the same session name.
409
+ let number = state.order.get(file.agentId);
410
+ if (number === undefined || opts.reserved?.has(number)) {
411
+ while (opts.reserved?.has(state.nextNumber))
412
+ state.nextNumber++;
413
+ number = state.nextNumber++;
414
+ state.order.set(file.agentId, number);
415
+ }
416
+ // Only a row the phone shows is read: an addressable one that has gone
417
+ // quiet is read the moment it writes again and becomes live. Its last
418
+ // count, if it had one, stands in the meantime.
419
+ const progress = file.live
420
+ ? advanceProgress(state, file.agentId, file.path, catchUp)
421
+ : state.progress.get(file.agentId) ?? { tail: (0, transcript_tail_js_1.initialTailState)(), toolCount: 0, startedAt: null };
422
+ return {
423
+ number,
424
+ name: `${parentName}.${number}`,
425
+ agentId: file.agentId,
426
+ toolCount: progress.toolCount,
427
+ startedAt: progress.startedAt,
428
+ label: state.labels.get(file.agentId) ?? rememberLabel(state, file.agentId, file.path, number),
429
+ transcriptPath: file.path,
430
+ lastActivityAt: new Date(file.mtimeMs).toISOString(),
431
+ live: file.live,
432
+ working: !file.ended && now - file.mtimeMs <= exports.SUBAGENT_WORKING_MS,
433
+ };
434
+ });
435
+ return rows.sort((a, b) => a.number - b.number).map(({ number: _number, ...row }) => row);
436
+ };
437
+ exports.scanSubagents = scanSubagents;