pi-agent-replicate 0.0.0-stage → 1.4.1

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 (82) hide show
  1. package/AGENTS.md +27 -0
  2. package/CHANGELOG.md +152 -0
  3. package/CONTEXT.md +63 -0
  4. package/LICENSE +21 -0
  5. package/README.md +68 -2
  6. package/RELEASING.md +132 -0
  7. package/assets/banner.png +0 -0
  8. package/config.json.example +6 -0
  9. package/docs/README.md +23 -0
  10. package/docs/adr/0003-installable-role-packs.md +29 -0
  11. package/docs/adr/0008-adopt-pi-only-subagent-execution.md +22 -0
  12. package/docs/adr/0009-durable-recursive-subagent-ownership.md +23 -0
  13. package/docs/adr/0010-public-subagent-tree-registry.md +16 -0
  14. package/docs/configuration.md +14 -0
  15. package/docs/development.md +27 -0
  16. package/docs/public-subagent-tree-api.md +58 -0
  17. package/docs/retained-roles.md +71 -0
  18. package/docs/settled-lifecycle-contract.md +71 -0
  19. package/docs/worktree-subagents.md +58 -0
  20. package/examples/role-pack/extension.ts +18 -0
  21. package/examples/role-pack/package.json +16 -0
  22. package/examples/role-pack/roles/example-reviewer.md +12 -0
  23. package/index.d.ts +116 -0
  24. package/index.js +1064 -0
  25. package/index.ts +155 -0
  26. package/package.json +70 -4
  27. package/pi-extension/subagents/activity.js +575 -0
  28. package/pi-extension/subagents/activity.ts +980 -0
  29. package/pi-extension/subagents/completion.js +184 -0
  30. package/pi-extension/subagents/completion.ts +229 -0
  31. package/pi-extension/subagents/delivery-drain.js +15 -0
  32. package/pi-extension/subagents/delivery-drain.ts +20 -0
  33. package/pi-extension/subagents/delivery-files.js +86 -0
  34. package/pi-extension/subagents/delivery-files.ts +105 -0
  35. package/pi-extension/subagents/delivery-log.js +51 -0
  36. package/pi-extension/subagents/delivery-log.ts +51 -0
  37. package/pi-extension/subagents/delivery-payload.ts +55 -0
  38. package/pi-extension/subagents/delivery.ts +219 -0
  39. package/pi-extension/subagents/execution-owner.js +30 -0
  40. package/pi-extension/subagents/execution-owner.ts +38 -0
  41. package/pi-extension/subagents/herdr.js +645 -0
  42. package/pi-extension/subagents/herdr.ts +927 -0
  43. package/pi-extension/subagents/index.ts +3400 -0
  44. package/pi-extension/subagents/launch.js +774 -0
  45. package/pi-extension/subagents/launch.ts +1124 -0
  46. package/pi-extension/subagents/lifecycle.js +477 -0
  47. package/pi-extension/subagents/lifecycle.ts +582 -0
  48. package/pi-extension/subagents/lineage.js +806 -0
  49. package/pi-extension/subagents/lineage.ts +841 -0
  50. package/pi-extension/subagents/replica-background.js +32 -0
  51. package/pi-extension/subagents/replica-background.ts +38 -0
  52. package/pi-extension/subagents/replica-branch.ts +45 -0
  53. package/pi-extension/subagents/replica-directory-settings.ts +51 -0
  54. package/pi-extension/subagents/replica-history.js +22 -0
  55. package/pi-extension/subagents/replica-history.ts +21 -0
  56. package/pi-extension/subagents/replica-work.js +326 -0
  57. package/pi-extension/subagents/replica-work.ts +336 -0
  58. package/pi-extension/subagents/replica-worker.js +74 -0
  59. package/pi-extension/subagents/replica-worker.ts +71 -0
  60. package/pi-extension/subagents/replication.ts +99 -0
  61. package/pi-extension/subagents/result-delivery-queue.ts +33 -0
  62. package/pi-extension/subagents/role-catalog.js +123 -0
  63. package/pi-extension/subagents/role-catalog.ts +24 -0
  64. package/pi-extension/subagents/runtime-routing.js +154 -0
  65. package/pi-extension/subagents/runtime-routing.ts +239 -0
  66. package/pi-extension/subagents/session-binding.js +346 -0
  67. package/pi-extension/subagents/session-binding.ts +339 -0
  68. package/pi-extension/subagents/session-work.js +219 -0
  69. package/pi-extension/subagents/session-work.ts +167 -0
  70. package/pi-extension/subagents/session.js +318 -0
  71. package/pi-extension/subagents/session.ts +448 -0
  72. package/pi-extension/subagents/settled-contract.js +47 -0
  73. package/pi-extension/subagents/settled-contract.ts +175 -0
  74. package/pi-extension/subagents/source-state.js +180 -0
  75. package/pi-extension/subagents/source-state.ts +184 -0
  76. package/pi-extension/subagents/status.ts +493 -0
  77. package/pi-extension/subagents/subagent-done.js +805 -0
  78. package/pi-extension/subagents/subagent-done.ts +878 -0
  79. package/pi-extension/subagents/terminal.js +111 -0
  80. package/pi-extension/subagents/terminal.ts +208 -0
  81. package/pi-extension/subagents/watchdog.js +122 -0
  82. package/pi-extension/subagents/watchdog.ts +171 -0
@@ -0,0 +1,493 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ export const SNAPSHOT_STALLED_AFTER_MS = 60_000;
6
+ export const DEFAULT_STATUS_LINE_LIMIT = 4;
7
+ export const MAX_STATUS_NAME_LENGTH = 72;
8
+ export const MAX_STATUS_LINE_LENGTH = 120;
9
+
10
+ const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "../..");
11
+ const DEFAULT_STATUS_CONFIG_PATH = join(PACKAGE_ROOT, "config.json");
12
+ const STATUS_CONFIG_EXAMPLE_PATH = join(PACKAGE_ROOT, "config.json.example");
13
+
14
+ export type SubagentStatusKind = "starting" | "active" | "waiting" | "stalled";
15
+ export type SubagentStatusTransition = "stalled" | "recovered" | null;
16
+ export type StatusSnapshotState = "unseen" | "present" | "missing" | "invalid" | "wrong-id";
17
+ export type StatusActivityPhase = "starting" | "active" | "waiting" | "done";
18
+
19
+ export interface StatusConfig {
20
+ enabled: boolean;
21
+ lineLimit: number;
22
+ quietThresholdMs?: number;
23
+ }
24
+
25
+ export type StatusObservation =
26
+ | {
27
+ snapshot: "present";
28
+ updatedAt: number;
29
+ sequence: number;
30
+ phase: StatusActivityPhase;
31
+ active?: boolean;
32
+ activeScope?: string;
33
+ activeSince?: number;
34
+ waitingSince?: number;
35
+ latestEvent?: string;
36
+ activityLabel?: string;
37
+ }
38
+ | {
39
+ snapshot: "missing" | "invalid" | "wrong-id";
40
+ snapshotError?: string;
41
+ };
42
+
43
+ export interface SubagentStatusState {
44
+ startTimeMs: number;
45
+ firstObservationAtMs: number | null;
46
+ lastActivityAtMs: number | null;
47
+ lastActivitySequence: number | null;
48
+ localOverrideAtMs: number | null;
49
+ localOverrideSequence: number | null;
50
+ activeNow: boolean;
51
+ activeSinceMs: number | null;
52
+ activeScope: string | null;
53
+ waitingSinceMs: number | null;
54
+ phase: StatusActivityPhase | null;
55
+ latestEvent: string | null;
56
+ activityLabel: string | null;
57
+ snapshotState: StatusSnapshotState;
58
+ snapshotProblemSinceMs: number | null;
59
+ snapshotError: string | null;
60
+ currentKind: SubagentStatusKind;
61
+ }
62
+
63
+ export interface StatusSnapshot {
64
+ kind: SubagentStatusKind;
65
+ elapsedMs: number;
66
+ elapsedText: string;
67
+ activeSinceMs: number | null;
68
+ activeDurationText: string | null;
69
+ activeScope: string | null;
70
+ waitingSinceMs: number | null;
71
+ waitingDurationText: string | null;
72
+ latestEvent: string | null;
73
+ activityLabel: string | null;
74
+ snapshotState: StatusSnapshotState;
75
+ snapshotError: string | null;
76
+ snapshotProblemText: string | null;
77
+ statusLabel: string | null;
78
+ }
79
+
80
+ export interface CappedStatusLines {
81
+ visibleLines: string[];
82
+ overflow: number;
83
+ }
84
+
85
+ function invalidStatusConfig(source: string, message: string): never {
86
+ throw new Error(`Invalid subagent status config in ${source}: ${message}`);
87
+ }
88
+
89
+ function requireObject(value: unknown, source: string, fieldName: string): Record<string, unknown> {
90
+ if (value == null || typeof value !== "object" || Array.isArray(value)) {
91
+ invalidStatusConfig(source, `${fieldName} must be an object`);
92
+ }
93
+ return value as Record<string, unknown>;
94
+ }
95
+
96
+ function requireBoolean(value: unknown, source: string, fieldName: string): boolean {
97
+ if (typeof value !== "boolean") {
98
+ invalidStatusConfig(source, `${fieldName} must be a boolean`);
99
+ }
100
+ return value;
101
+ }
102
+
103
+
104
+ function optionalPositiveInteger(value: unknown, source: string, fieldName: string): number | undefined {
105
+ if (value === undefined) return undefined;
106
+ if (!Number.isInteger(value) || (value as number) <= 0) {
107
+ invalidStatusConfig(source, `${fieldName} must be a positive integer`);
108
+ }
109
+ return value as number;
110
+ }
111
+
112
+ function rejectUnsupportedKeys(
113
+ value: Record<string, unknown>,
114
+ allowedKeys: string[],
115
+ source: string,
116
+ fieldName: string,
117
+ ): void {
118
+ const unsupportedKeys = Object.keys(value).filter((key) => !allowedKeys.includes(key));
119
+ if (unsupportedKeys.length > 0) {
120
+ invalidStatusConfig(source, `${fieldName} has unsupported key(s): ${unsupportedKeys.join(", ")}`);
121
+ }
122
+ }
123
+
124
+ function truncateText(text: string, maxLength: number): string {
125
+ if (text.length <= maxLength) return text;
126
+ if (maxLength <= 1) return text.slice(0, maxLength);
127
+ return `${text.slice(0, maxLength - 1)}…`;
128
+ }
129
+
130
+ export function normalizeStatusName(name: string): string {
131
+ const collapsed = name.replace(/\s+/g, " ").trim() || "subagent";
132
+ return truncateText(collapsed, MAX_STATUS_NAME_LENGTH);
133
+ }
134
+
135
+ function boundStatusLine(line: string): string {
136
+ return truncateText(line.replace(/\s+/g, " ").trim(), MAX_STATUS_LINE_LENGTH);
137
+ }
138
+
139
+ function snapshotProblemLabel(snapshotState: StatusSnapshotState): string | null {
140
+ if (snapshotState === "wrong-id") return "wrong activity id";
141
+ return null;
142
+ }
143
+
144
+ function activityLabel(snapshot: Pick<StatusSnapshot, "activityLabel" | "activeScope">): string | null {
145
+ return snapshot.activityLabel ?? snapshot.activeScope;
146
+ }
147
+
148
+ export function parseStatusConfig(rawConfig: unknown, source = "config.json"): StatusConfig {
149
+ const config = requireObject(rawConfig, source, "root");
150
+ const status = requireObject(config.status, source, "status");
151
+ rejectUnsupportedKeys(status, ["enabled", "quietThresholdMs"], source, "status");
152
+ const enabled = requireBoolean(status.enabled, source, "status.enabled");
153
+ const quietThresholdMs = optionalPositiveInteger(status.quietThresholdMs, source, "status.quietThresholdMs");
154
+
155
+ return {
156
+ enabled,
157
+ lineLimit: DEFAULT_STATUS_LINE_LIMIT,
158
+ ...(quietThresholdMs ? { quietThresholdMs } : {}),
159
+ };
160
+ }
161
+
162
+ function readStatusConfigFile(configPath: string, examplePath: string): { sourcePath: string; rawConfig: string } {
163
+ try {
164
+ return { sourcePath: configPath, rawConfig: readFileSync(configPath, "utf8") };
165
+ } catch (error) {
166
+ const errno = error as NodeJS.ErrnoException;
167
+ if (errno.code !== "ENOENT") throw error;
168
+ }
169
+
170
+ try {
171
+ return { sourcePath: examplePath, rawConfig: readFileSync(examplePath, "utf8") };
172
+ } catch (error) {
173
+ const errno = error as NodeJS.ErrnoException;
174
+ if (errno.code === "ENOENT") {
175
+ throw new Error(
176
+ `Missing subagent status config. Expected ${configPath} or ${examplePath}.`,
177
+ );
178
+ }
179
+ throw error;
180
+ }
181
+ }
182
+
183
+ export function loadStatusConfig(
184
+ configPath = DEFAULT_STATUS_CONFIG_PATH,
185
+ examplePath = STATUS_CONFIG_EXAMPLE_PATH,
186
+ ): StatusConfig {
187
+ const { sourcePath, rawConfig } = readStatusConfigFile(configPath, examplePath);
188
+
189
+ let parsed: unknown;
190
+ try {
191
+ parsed = JSON.parse(rawConfig) as unknown;
192
+ } catch (error) {
193
+ const detail = error instanceof Error ? error.message : String(error);
194
+ throw new Error(`Invalid JSON in subagent config ${sourcePath}: ${detail}`);
195
+ }
196
+
197
+ return parseStatusConfig(parsed, sourcePath);
198
+ }
199
+
200
+ export function formatElapsedDuration(ms: number): string {
201
+ const totalSeconds = Math.max(0, Math.floor(ms / 1000));
202
+ if (totalSeconds < 60) return `${totalSeconds}s`;
203
+
204
+ const hours = Math.floor(totalSeconds / 3600);
205
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
206
+ if (hours > 0) return `${hours}h ${minutes}m`;
207
+
208
+ return `${minutes}m`;
209
+ }
210
+
211
+ export function createStatusState(params: {
212
+ startTimeMs: number;
213
+ }): SubagentStatusState {
214
+ return {
215
+ startTimeMs: params.startTimeMs,
216
+ firstObservationAtMs: null,
217
+ lastActivityAtMs: null,
218
+ lastActivitySequence: null,
219
+ localOverrideAtMs: null,
220
+ localOverrideSequence: null,
221
+ activeNow: false,
222
+ activeSinceMs: null,
223
+ activeScope: null,
224
+ waitingSinceMs: null,
225
+ phase: null,
226
+ latestEvent: null,
227
+ activityLabel: null,
228
+ snapshotState: "unseen",
229
+ snapshotProblemSinceMs: null,
230
+ snapshotError: null,
231
+ currentKind: "starting",
232
+ };
233
+ }
234
+
235
+ export function observeStatus(
236
+ state: SubagentStatusState,
237
+ observation: StatusObservation,
238
+ now: number,
239
+ ): SubagentStatusState {
240
+ if (observation.snapshot !== "present") {
241
+ return {
242
+ ...state,
243
+ firstObservationAtMs: state.firstObservationAtMs ?? now,
244
+ snapshotState: observation.snapshot,
245
+ snapshotProblemSinceMs: state.snapshotProblemSinceMs ?? now,
246
+ snapshotError: observation.snapshotError ?? null,
247
+ };
248
+ }
249
+
250
+ const updatedAt = observation.updatedAt;
251
+ const sequence = observation.sequence;
252
+ const lastActivityAtMs = state.lastActivityAtMs;
253
+ const lastActivitySequence = state.lastActivitySequence;
254
+ const olderThanLastActivity = lastActivityAtMs != null && (
255
+ updatedAt < lastActivityAtMs ||
256
+ (updatedAt === lastActivityAtMs && lastActivitySequence != null && sequence < lastActivitySequence)
257
+ );
258
+ if (olderThanLastActivity) return state;
259
+
260
+ const blockedByLocalOverride = state.localOverrideAtMs != null && (
261
+ updatedAt < state.localOverrideAtMs ||
262
+ (updatedAt === state.localOverrideAtMs && state.localOverrideSequence != null && sequence <= state.localOverrideSequence)
263
+ );
264
+ if (blockedByLocalOverride) return state;
265
+
266
+ const phase = observation.phase;
267
+ const activeNow = phase === "active" || observation.active === true;
268
+ const activeSinceMs = activeNow
269
+ ? observation.activeSince ?? state.activeSinceMs ?? updatedAt
270
+ : null;
271
+ const waitingSinceMs = phase === "waiting"
272
+ ? observation.waitingSince ?? state.waitingSinceMs ?? updatedAt
273
+ : null;
274
+
275
+ return {
276
+ ...state,
277
+ firstObservationAtMs: state.firstObservationAtMs ?? now,
278
+ lastActivityAtMs: updatedAt,
279
+ lastActivitySequence: sequence,
280
+ activeNow,
281
+ activeSinceMs,
282
+ activeScope: activeNow ? observation.activeScope ?? null : null,
283
+ waitingSinceMs,
284
+ phase,
285
+ latestEvent: observation.latestEvent ?? null,
286
+ activityLabel: observation.activityLabel ?? null,
287
+ snapshotState: "present",
288
+ snapshotProblemSinceMs: null,
289
+ snapshotError: null,
290
+ localOverrideAtMs: null,
291
+ localOverrideSequence: null,
292
+ };
293
+ }
294
+
295
+ export function forceStatusAfterInterrupt(state: SubagentStatusState, now: number): SubagentStatusState {
296
+ return {
297
+ ...state,
298
+ firstObservationAtMs: state.firstObservationAtMs ?? now,
299
+ lastActivityAtMs: now,
300
+ localOverrideAtMs: now,
301
+ localOverrideSequence: state.lastActivitySequence,
302
+ activeNow: false,
303
+ activeSinceMs: null,
304
+ activeScope: null,
305
+ waitingSinceMs: now,
306
+ phase: "waiting",
307
+ latestEvent: "interrupt_requested",
308
+ activityLabel: "interrupted",
309
+ snapshotState: "present",
310
+ snapshotProblemSinceMs: null,
311
+ snapshotError: null,
312
+ currentKind: "waiting",
313
+ };
314
+ }
315
+
316
+ function classifyProblemState(state: SubagentStatusState, now: number): Pick<StatusSnapshot, "kind" | "statusLabel"> {
317
+ const problemLabel = snapshotProblemLabel(state.snapshotState);
318
+ const hasValidSnapshot = state.lastActivityAtMs != null;
319
+
320
+ if (!hasValidSnapshot) {
321
+ const referenceMs = state.firstObservationAtMs ?? state.startTimeMs;
322
+ const elapsedMs = Math.max(0, now - referenceMs);
323
+ return elapsedMs >= SNAPSHOT_STALLED_AFTER_MS
324
+ ? { kind: "stalled", statusLabel: problemLabel }
325
+ : { kind: "starting", statusLabel: null };
326
+ }
327
+
328
+ const problemSinceMs = state.snapshotProblemSinceMs ?? now;
329
+ const problemMs = Math.max(0, now - problemSinceMs);
330
+ if (problemMs >= SNAPSHOT_STALLED_AFTER_MS) return { kind: "stalled", statusLabel: problemLabel };
331
+
332
+ const lastHealthyKind = state.activeNow
333
+ ? "active"
334
+ : state.waitingSinceMs != null || state.phase === "done"
335
+ ? "waiting"
336
+ : state.currentKind === "stalled"
337
+ ? "starting"
338
+ : state.currentKind;
339
+ return { kind: lastHealthyKind, statusLabel: problemLabel };
340
+ }
341
+
342
+ export function classifyStatus(state: SubagentStatusState, now: number): StatusSnapshot {
343
+ const elapsedMs = Math.max(0, now - state.startTimeMs);
344
+ const elapsedText = formatElapsedDuration(elapsedMs);
345
+
346
+ let kind: SubagentStatusKind;
347
+ let statusLabel: string | null = null;
348
+
349
+ if (state.snapshotState === "present") {
350
+ if (state.phase === "active" || state.activeNow) {
351
+ kind = "active";
352
+ } else if (state.phase === "waiting") {
353
+ kind = "waiting";
354
+ } else if (state.phase === "done") {
355
+ kind = "waiting";
356
+ statusLabel = "done";
357
+ } else {
358
+ const referenceMs = state.firstObservationAtMs ?? state.startTimeMs;
359
+ const elapsedSinceObservationMs = Math.max(0, now - referenceMs);
360
+ kind = elapsedSinceObservationMs >= SNAPSHOT_STALLED_AFTER_MS ? "stalled" : "starting";
361
+ statusLabel = null;
362
+ }
363
+ } else {
364
+ const classified = classifyProblemState(state, now);
365
+ kind = classified.kind;
366
+ statusLabel = classified.statusLabel;
367
+ }
368
+
369
+ const activeDurationText = state.activeSinceMs == null
370
+ ? null
371
+ : formatElapsedDuration(now - state.activeSinceMs);
372
+ const waitingDurationText = state.waitingSinceMs == null
373
+ ? null
374
+ : formatElapsedDuration(now - state.waitingSinceMs);
375
+ const snapshotProblemText = state.snapshotProblemSinceMs == null
376
+ ? null
377
+ : formatElapsedDuration(now - state.snapshotProblemSinceMs);
378
+
379
+ return {
380
+ kind,
381
+ elapsedMs,
382
+ elapsedText,
383
+ activeSinceMs: state.activeSinceMs,
384
+ activeDurationText,
385
+ activeScope: state.activeScope,
386
+ waitingSinceMs: state.waitingSinceMs,
387
+ waitingDurationText,
388
+ latestEvent: state.latestEvent,
389
+ activityLabel: state.activityLabel,
390
+ snapshotState: state.snapshotState,
391
+ snapshotError: state.snapshotError,
392
+ snapshotProblemText,
393
+ statusLabel,
394
+ };
395
+ }
396
+
397
+ export function advanceStatusState(
398
+ state: SubagentStatusState,
399
+ now: number,
400
+ ): {
401
+ nextState: SubagentStatusState;
402
+ snapshot: StatusSnapshot;
403
+ transition: SubagentStatusTransition;
404
+ } {
405
+ const snapshot = classifyStatus(state, now);
406
+ const transition =
407
+ state.currentKind !== "stalled" && snapshot.kind === "stalled"
408
+ ? "stalled"
409
+ : state.currentKind === "stalled" && (snapshot.kind === "active" || snapshot.kind === "waiting")
410
+ ? "recovered"
411
+ : null;
412
+
413
+ return {
414
+ snapshot,
415
+ transition,
416
+ nextState: {
417
+ ...state,
418
+ currentKind: snapshot.kind,
419
+ },
420
+ };
421
+ }
422
+
423
+ function formatActiveDetail(snapshot: StatusSnapshot): string {
424
+ const label = activityLabel(snapshot);
425
+ if (!label) return "active";
426
+ const duration = snapshot.activeDurationText ? ` ${snapshot.activeDurationText}` : "";
427
+ return `active (${label}${duration})`;
428
+ }
429
+
430
+ function formatWaitingDetail(snapshot: StatusSnapshot): string {
431
+ const duration = snapshot.waitingDurationText ? ` ${snapshot.waitingDurationText}` : "";
432
+ return `waiting${duration}`;
433
+ }
434
+
435
+ function formatStalledDetail(snapshot: StatusSnapshot): string {
436
+ const detail = snapshot.statusLabel ? ` (${snapshot.statusLabel})` : "";
437
+ const duration = snapshot.snapshotProblemText ? ` ${snapshot.snapshotProblemText}` : "";
438
+ return `stalled${duration}${detail}`;
439
+ }
440
+
441
+ export function formatStatusLine(name: string, snapshot: StatusSnapshot): string {
442
+ const boundedName = normalizeStatusName(name);
443
+
444
+ if (snapshot.kind === "starting") {
445
+ const label = snapshot.statusLabel ? ` (${snapshot.statusLabel})` : "";
446
+ return boundStatusLine(`${boundedName} running ${snapshot.elapsedText}, starting${label}.`);
447
+ }
448
+
449
+ if (snapshot.kind === "active") {
450
+ return boundStatusLine(`${boundedName} running ${snapshot.elapsedText}, ${formatActiveDetail(snapshot)}.`);
451
+ }
452
+
453
+ if (snapshot.kind === "waiting") {
454
+ const problem = snapshot.statusLabel && snapshot.statusLabel !== "done"
455
+ ? ` (${snapshot.statusLabel})`
456
+ : snapshot.statusLabel === "done"
457
+ ? " (done)"
458
+ : "";
459
+ return boundStatusLine(`${boundedName} running ${snapshot.elapsedText}, ${formatWaitingDetail(snapshot)}${problem}.`);
460
+ }
461
+
462
+ return boundStatusLine(`${boundedName} running ${snapshot.elapsedText}, ${formatStalledDetail(snapshot)}.`);
463
+ }
464
+
465
+ export function formatTransitionLine(
466
+ name: string,
467
+ snapshot: StatusSnapshot,
468
+ transition: Exclude<SubagentStatusTransition, null>,
469
+ ): string {
470
+ const boundedName = normalizeStatusName(name);
471
+
472
+ if (transition === "recovered") {
473
+ const detail = snapshot.kind === "waiting" ? formatWaitingDetail(snapshot) : formatActiveDetail(snapshot);
474
+ return boundStatusLine(`${boundedName} running ${snapshot.elapsedText}, recovered; ${detail}.`);
475
+ }
476
+
477
+ return formatStatusLine(boundedName, snapshot);
478
+ }
479
+
480
+ export function capStatusLines(lines: string[], lineLimit: number): CappedStatusLines {
481
+ const visibleLines = lines.slice(0, lineLimit);
482
+ return {
483
+ visibleLines,
484
+ overflow: Math.max(0, lines.length - visibleLines.length),
485
+ };
486
+ }
487
+
488
+ export function formatStatusAggregate(lines: string[], lineLimit: number): string {
489
+ const { visibleLines, overflow } = capStatusLines(lines, lineLimit);
490
+ const bulletLines = visibleLines.map((line) => `• ${line}`);
491
+ if (overflow > 0) bulletLines.push(`• +${overflow} more running.`);
492
+ return `Subagent status:\n${bulletLines.join("\n")}`;
493
+ }