brl-subagent 2.3.4

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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +257 -0
  3. package/package.json +49 -0
  4. package/presets/code-reviewer.md +37 -0
  5. package/presets/data-analyst.md +33 -0
  6. package/presets/debugger.md +36 -0
  7. package/presets/dev-agent.md +30 -0
  8. package/presets/rapid-prototyper.md +25 -0
  9. package/presets/refactorer.md +34 -0
  10. package/presets/security-auditor.md +39 -0
  11. package/presets/tech-writer.md +33 -0
  12. package/presets/test-engineer.md +34 -0
  13. package/src/concurrency.ts +186 -0
  14. package/src/diff.ts +102 -0
  15. package/src/event-bus.ts +86 -0
  16. package/src/git.ts +187 -0
  17. package/src/history.ts +177 -0
  18. package/src/index.ts +3824 -0
  19. package/src/logging.ts +135 -0
  20. package/src/messaging.ts +164 -0
  21. package/src/metrics.ts +221 -0
  22. package/src/model-availability.ts +73 -0
  23. package/src/notify-completion.ts +207 -0
  24. package/src/params.ts +225 -0
  25. package/src/preflight.ts +150 -0
  26. package/src/presets.ts +425 -0
  27. package/src/prompt.ts +169 -0
  28. package/src/reports.ts +325 -0
  29. package/src/router.ts +133 -0
  30. package/src/runner.ts +877 -0
  31. package/src/sanitize.ts +365 -0
  32. package/src/scheduler.ts +187 -0
  33. package/src/session-manager.ts +1065 -0
  34. package/src/state.ts +590 -0
  35. package/src/templates.ts +469 -0
  36. package/src/transcript-tail.ts +327 -0
  37. package/src/transcript.ts +119 -0
  38. package/src/tui-format.ts +51 -0
  39. package/src/tui.ts +3103 -0
  40. package/src/types.ts +803 -0
  41. package/src/unit-run.ts +269 -0
  42. package/src/update.ts +109 -0
  43. package/src/validate.ts +302 -0
  44. package/templates/analyze-data.md +9 -0
  45. package/templates/code-review.md +10 -0
  46. package/templates/debug-issue.md +9 -0
  47. package/templates/implement-feature.md +9 -0
  48. package/templates/prototype.md +9 -0
  49. package/templates/refactor.md +9 -0
  50. package/templates/security-audit.md +10 -0
  51. package/templates/write-docs.md +9 -0
  52. package/templates/write-tests.md +10 -0
package/src/types.ts ADDED
@@ -0,0 +1,803 @@
1
+ /**
2
+ * brl-subagent — Type Definitions
3
+ *
4
+ * All shared type definitions and constants for the brl-subagent extension.
5
+ */
6
+
7
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
8
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
9
+ import type { TranscriptMessage } from "./transcript-tail";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Core types
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export type ApprovalMode = "auto" | "writes" | "always";
16
+
17
+ export const COMPLETION_NOTIFY_MODES = ["all", "failed", "off"] as const;
18
+ export type CompletionNotifyMode = (typeof COMPLETION_NOTIFY_MODES)[number];
19
+
20
+ export const DEFAULT_COMPLETION_NOTIFY: CompletionNotifyMode = "all";
21
+
22
+ export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
23
+
24
+ export const THINKING_LEVELS: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh"];
25
+
26
+ export type Priority = "critical" | "high" | "normal" | "low";
27
+
28
+ export const PRIORITY_ORDER: Record<Priority, number> = {
29
+ critical: 0,
30
+ high: 1,
31
+ normal: 2,
32
+ low: 3,
33
+ };
34
+
35
+ export const DEFAULT_PRIORITY: Priority = "normal";
36
+
37
+ export type ErrorCategory =
38
+ | "timeout"
39
+ | "model_unavailable"
40
+ | "tool_error"
41
+ | "permission_denied"
42
+ | "parse_error"
43
+ | "aborted"
44
+ | "exit_error"
45
+ | "crash"
46
+ | "unknown";
47
+
48
+ // Issue #120 (Review round): single source of truth for the user-abort stamp.
49
+ // Used by the abort handler (runner.ts), the stopped/aborted settlement paths
50
+ // and the settle-handler catch-all (session-manager.ts), and matched (via its
51
+ // distinctive "aborted by user" substring) by classifyError below. Keep the
52
+ // literal in sync with classifyError — do NOT reword one without the other.
53
+ export const SUBAGENT_ABORTED_MESSAGE = "Subagent aborted by user";
54
+
55
+ /**
56
+ * Classify a subagent result into an error category based on its errorMessage,
57
+ * stopReason, exitCode, and stderr content. Inspects patterns in priority order.
58
+ */
59
+ export function classifyError(result: SubagentResult): ErrorCategory {
60
+ if (result.stopReason === "aborted") return "aborted";
61
+
62
+ const msg = (result.errorMessage ?? "").toLowerCase();
63
+ const err = (result.stderr ?? "").toLowerCase();
64
+
65
+ if (msg.includes("timed out")) return "timeout";
66
+
67
+ // Issue #120 (Track 1): an abort stamp written BEFORE a kill (the abort
68
+ // handler) carries the honest source in errorMessage (SUBAGENT_ABORTED_MESSAGE,
69
+ // "Subagent aborted by user"). classifyError can't infer an abort from a dead
70
+ // process — the exitCode is just non-zero — so the stamped message must
71
+ // survive reclassification. Matched on the EXACT stamped substring here so a
72
+ // provider-side "stream aborted" is not misclassified as our user-abort (and
73
+ // skips Track-2 enrichment); keep this substring in sync with
74
+ // SUBAGENT_ABORTED_MESSAGE. The stamped category is preserved so it is not
75
+ // clobbered by the exitError/unknown fallbacks below.
76
+ if (msg.includes("aborted by user")) return "aborted";
77
+
78
+ if (msg.includes("model not found") || msg.includes("model unavailable"))
79
+ return "model_unavailable";
80
+
81
+ if (msg.includes("permission denied") || msg.includes("eacces")) return "permission_denied";
82
+
83
+ if (err.includes("parse error") || err.includes("parse_error") || err.includes("unexpected token"))
84
+ return "parse_error";
85
+
86
+ if (err.includes("crash") || err.includes("panic") || err.includes("segmentation fault"))
87
+ return "crash";
88
+
89
+ if (msg.includes("spawn") || msg.includes("not found") || msg.includes("enoent"))
90
+ return "tool_error";
91
+
92
+ if (result.exitCode !== 0) return "exit_error";
93
+
94
+ if (result.stopReason === "error") return "exit_error";
95
+
96
+ return "unknown";
97
+ }
98
+
99
+ export interface SubagentPreset {
100
+ name: string;
101
+ description?: string;
102
+ systemPrompt?: string;
103
+ inheritSystemPrompt?: boolean;
104
+ thinkingLevel?: string;
105
+ model?: string; // "provider/model-id", e.g. "anthropic/claude-opus-4-6"
106
+ outputFile?: string;
107
+ timeout?: number;
108
+ tools?: string[];
109
+ excludeTools?: string[];
110
+ noBuiltinTools?: boolean;
111
+ promptGuideline?: string;
112
+ }
113
+
114
+ export interface TaskTemplate {
115
+ name: string;
116
+ description?: string;
117
+ task: string;
118
+ preset?: string;
119
+ thinkingLevel?: string;
120
+ outputFile?: string;
121
+ timeout?: number;
122
+ tools?: string[];
123
+ excludeTools?: string[];
124
+ noBuiltinTools?: boolean;
125
+ inheritSystemPrompt?: boolean;
126
+ }
127
+
128
+ export interface CircuitBreakerState {
129
+ consecutiveFailures: number;
130
+ lastFailureTime: number; // epoch ms
131
+ circuitOpen: boolean;
132
+ degradedThinkingLevel?: ThinkingLevel;
133
+ }
134
+
135
+ export type GitMode = "branch" | "none";
136
+
137
+ export interface SLAMetrics {
138
+ errorCategoryBreakdown: Record<string, number>;
139
+ totalRuns: number;
140
+ successRate: number;
141
+ failureRate: number;
142
+ averageDurationMs: number;
143
+ p50DurationMs: number;
144
+ p95DurationMs: number;
145
+ p99DurationMs: number;
146
+ totalCost: number;
147
+ averageCost: number;
148
+ }
149
+
150
+ export interface DegradationReport {
151
+ degraded: boolean;
152
+ successRateChange: number;
153
+ p95Change: number;
154
+ recommendations: string[];
155
+ }
156
+
157
+ export const DEFAULT_SLA_WINDOW_SIZE = 50;
158
+ export const MIN_SLA_WINDOW_SIZE = 10;
159
+ export const MAX_SLA_WINDOW_SIZE = 500;
160
+
161
+ // E5: Compliance report types
162
+ export interface FileAccessReport {
163
+ files: Record<string, string[]>;
164
+ }
165
+
166
+ export interface SecretsExposureEntry {
167
+ file: string;
168
+ runId: string;
169
+ runLabel?: string;
170
+ severity: "high" | "medium" | "low";
171
+ }
172
+
173
+ export interface SecretsExposureReport {
174
+ exposures: SecretsExposureEntry[];
175
+ }
176
+
177
+ export interface ComplianceSummary {
178
+ totalRuns: number;
179
+ dateRange: {
180
+ earliest: string;
181
+ latest: string;
182
+ };
183
+ metrics: SLAMetrics;
184
+ filesAccessed: number;
185
+ sensitiveFilesTouched: number;
186
+ highSeverityFindings: number;
187
+ }
188
+
189
+ export interface SubagentState {
190
+ model?: { provider: string; id: string };
191
+ maxThinkingLevel: ThinkingLevel;
192
+ maxParallel: number; // 0 = unlimited
193
+ maxSubagentDepth: number; // 0 = no recursion allowed, 1 = one level, etc.
194
+ gitMode: GitMode; // P3: branch-based git workflow
195
+ approvalMode: ApprovalMode; // P4: change approval workflow
196
+ // Issue #147: completion-push wake knob — when a background run reaches a
197
+ // terminal state the conductor is woken (delivery is always-on, minimum
198
+ // nextTurn; wake is the configurable part). "all" wakes on every terminal
199
+ // state, "failed" wakes only on failed/stopped, "off" never wakes (still
200
+ // delivers passively at the next user prompt).
201
+ completionNotify: CompletionNotifyMode;
202
+ maxHistoryEntries: number; // 0 = unlimited
203
+ sessionCostLimit: number; // 0 = unlimited
204
+ perTaskCostEstimate: number; // 0 = no estimate, use default
205
+ seenRunIds: string[];
206
+ presets: SubagentPreset[];
207
+ templates: TaskTemplate[];
208
+ circuitBreaker: CircuitBreakerState;
209
+ slaTrackingEnabled: boolean; // E4: SLA tracking toggle
210
+ slaWindowSize: number; // E4: number of recent runs to analyze (10-500)
211
+ lastSLAMetrics?: SLAMetrics; // E4: persisted baseline for degradation comparison
212
+ updateCheckEnabled: boolean; // Version update notifier toggle
213
+ lastUpdateCheck: number; // Epoch ms of last update check
214
+ }
215
+
216
+ /**
217
+ * Result shape returned by brl-subagent tool handlers.
218
+ *
219
+ * `AgentToolResult<T>` is the pi SDK contract (details is required). The
220
+ * handlers extend it with the legacy `isError` flag the tool contract and
221
+ * tests pin — the pi runtime ignores `isError` on returned results (v0.84
222
+ * signals errors by throwing), so it is metadata for the repo's own contract,
223
+ * not a pi behavior switch.
224
+ */
225
+ export type ToolResult<T = unknown> = AgentToolResult<T> & { isError?: boolean };
226
+
227
+ export interface SubagentRun {
228
+ id: string;
229
+ task: string;
230
+ label?: string;
231
+ // Issue #98: background run entries carry the agent description (the label
232
+ // the caller passed at spawn) — kept separate from `label` for symmetry
233
+ // with the background agent record.
234
+ description?: string;
235
+ status: "running" | "done" | "failed";
236
+ model: string;
237
+ thinkingLevel: string;
238
+ // Issue #114: per-unit priority (may be absent — the run's slot priority
239
+ // then fell back to the call-level default, which is not recorded here).
240
+ priority?: string;
241
+ startedAt: string;
242
+ finishedAt?: string;
243
+ durationMs?: number;
244
+ cost?: number;
245
+ tokensIn?: number;
246
+ tokensOut?: number;
247
+ errorMessage?: string;
248
+ outputSummary?: string;
249
+ fullOutput?: string;
250
+ originalParams?: {
251
+ systemPrompt?: string;
252
+ inheritSystemPrompt?: boolean;
253
+ model?: string;
254
+ thinkingLevel?: string;
255
+ // Issue #114: per-unit priority survives retries — the snapshot carries
256
+ // it through so a retried run keeps its priority.
257
+ priority?: string;
258
+ outputFile?: string;
259
+ timeout?: number;
260
+ cwd?: string;
261
+ tools?: string[];
262
+ excludeTools?: string[];
263
+ noBuiltinTools?: boolean;
264
+ preset?: string;
265
+ errorCategory?: ErrorCategory;
266
+ };
267
+ }
268
+
269
+ export interface LiveSubagent {
270
+ id: string;
271
+ label?: string;
272
+ task: string;
273
+ model: string;
274
+ thinkingLevel: string;
275
+ // Issue #114: per-unit priority for the drill-in header (`p:critical` etc.).
276
+ priority?: string;
277
+ startedAt: number;
278
+ liveOutput: string;
279
+ usage: { input: number; output: number };
280
+ ctx: ExtensionContext;
281
+ /**
282
+ * Issue #105: live streaming transcript for FOREGROUND runs — captured from
283
+ * the subprocess's `message_update` JSONL events (thinking/text/toolCall
284
+ * deltas) and threaded through the live monitor. Background runs render
285
+ * from `_sessionRef` instead; this field is populated only on the
286
+ * foreground path.
287
+ */
288
+ transcript?: TranscriptMessage[];
289
+ }
290
+
291
+ export interface UsageStats {
292
+ input: number;
293
+ output: number;
294
+ cacheRead: number;
295
+ cacheWrite: number;
296
+ cost: number;
297
+ contextTokens: number;
298
+ turns: number;
299
+ }
300
+
301
+ export interface SubagentResult {
302
+ messages: Array<Record<string, unknown>>;
303
+ model?: string;
304
+ usage: UsageStats;
305
+ stopReason?: string;
306
+ errorMessage?: string;
307
+ exitCode: number;
308
+ stderr: string;
309
+ label?: string;
310
+ errorCategory?: ErrorCategory;
311
+ /**
312
+ * Issue #105: streaming transcript captured from the subprocess's
313
+ * `message_update` JSONL events (foreground parity with the background
314
+ * drill-in). Populated incrementally while the subprocess runs; the array
315
+ * is a live tail capped at LIVE_TRANSCRIPT_MAX_MESSAGES / 64KB (runner.ts).
316
+ */
317
+ liveTranscript?: TranscriptMessage[];
318
+ // P3: Git integration fields
319
+ gitBranch?: string;
320
+ gitDiff?: string;
321
+ // P4: Change approval workflow
322
+ approved?: boolean;
323
+ }
324
+
325
+ export interface SubagentToolOptions {
326
+ tools?: string[];
327
+ excludeTools?: string[];
328
+ noBuiltinTools?: boolean;
329
+ }
330
+
331
+ // ---------------------------------------------------------------------------
332
+ // P1+P2: Task chaining & parallel mode types
333
+ // ---------------------------------------------------------------------------
334
+
335
+ /** One step in a chain or one task in a parallel group. */
336
+ export interface SubTaskParams {
337
+ task: string;
338
+ label?: string;
339
+ /** Model override for this step (provider/model-id, e.g. "anthropic/claude-opus-4-6"). */
340
+ model?: string;
341
+ thinkingLevel?: string;
342
+ /** Issue #114: per-unit concurrency priority for tasks[]/graph[] items. */
343
+ priority?: string;
344
+ systemPrompt?: string;
345
+ inheritSystemPrompt?: boolean;
346
+ cwd?: string;
347
+ timeout?: number;
348
+ outputFile?: string;
349
+ tools?: string[];
350
+ excludeTools?: string[];
351
+ noBuiltinTools?: boolean;
352
+ }
353
+
354
+ /** Result of a single subtask execution. */
355
+ export interface SubTaskResult {
356
+ label?: string;
357
+ task: string;
358
+ step?: number;
359
+ exitCode: number;
360
+ messages: Array<Record<string, unknown>>;
361
+ stderr: string;
362
+ usage: UsageStats;
363
+ model?: string;
364
+ stopReason?: string;
365
+ errorMessage?: string;
366
+ errorCategory?: ErrorCategory;
367
+ gitBranch?: string;
368
+ gitDiff?: string;
369
+ }
370
+
371
+ /** Aggregate result for chain and parallel modes. */
372
+ export interface MultiSubagentDetails {
373
+ mode: "chain" | "parallel";
374
+ results: SubTaskResult[];
375
+ totalInput: number;
376
+ totalOutput: number;
377
+ totalCost: number;
378
+ totalTurns: number;
379
+ }
380
+
381
+ export interface ChainDetails extends MultiSubagentDetails {
382
+ mode: "chain";
383
+ completedSteps: number;
384
+ totalSteps: number;
385
+ stoppedEarly: boolean;
386
+ }
387
+
388
+ export interface ParallelDetails extends MultiSubagentDetails {
389
+ mode: "parallel";
390
+ succeeded: number;
391
+ failed: number;
392
+ }
393
+
394
+ // ---------------------------------------------------------------------------
395
+ // P10: Dependency graph types
396
+ // ---------------------------------------------------------------------------
397
+
398
+ /** A single node in a dependency graph. */
399
+ export interface GraphTask {
400
+ id: string;
401
+ task: string;
402
+ label?: string;
403
+ /** Model override for this task (provider/model-id, e.g. "anthropic/claude-opus-4-6"). */
404
+ model?: string;
405
+ thinkingLevel?: string;
406
+ /** Issue #114: per-unit concurrency priority for this graph node. */
407
+ priority?: string;
408
+ cwd?: string;
409
+ timeout?: number;
410
+ outputFile?: string;
411
+ tools?: string[];
412
+ excludeTools?: string[];
413
+ noBuiltinTools?: boolean;
414
+ systemPrompt?: string;
415
+ inheritSystemPrompt?: boolean;
416
+ dependsOn: string[];
417
+ }
418
+
419
+ /** One wave of tasks executed together in a graph. */
420
+ export interface GraphWave {
421
+ waveIndex: number;
422
+ tasks: SubTaskResult[];
423
+ parallel: boolean;
424
+ }
425
+
426
+ /** Aggregate result for graph mode. */
427
+ export interface GraphDetails {
428
+ mode: "graph";
429
+ waves: GraphWave[];
430
+ totalInput: number;
431
+ totalOutput: number;
432
+ totalCost: number;
433
+ totalTurns: number;
434
+ }
435
+
436
+ /**
437
+ * Union of every details shape delegate_task can produce — the tool's TDetails
438
+ * generic (issue #117). Single mode yields SubagentResult; chain / parallel /
439
+ * graph modes yield their mode-specific aggregates (GraphDetails does NOT
440
+ * extend MultiSubagentDetails, so it must be a member explicitly); error paths
441
+ * yield undefined.
442
+ */
443
+ export type DelegateTaskDetails =
444
+ | SubagentResult
445
+ | ChainDetails
446
+ | ParallelDetails
447
+ | GraphDetails
448
+ | undefined;
449
+
450
+ // ---------------------------------------------------------------------------
451
+ // Resolved params (after preset merging + validation)
452
+ // ---------------------------------------------------------------------------
453
+
454
+ // ---------------------------------------------------------------------------
455
+ // Resolved params (after preset merging + validation)
456
+ // ---------------------------------------------------------------------------
457
+
458
+ export interface ResolvedParams {
459
+ task: string;
460
+ label: string | undefined;
461
+ inheritSP: boolean;
462
+ customSP: string | undefined;
463
+ outputFile: string | undefined;
464
+ timeout: number | undefined;
465
+ effectiveCwd: string;
466
+ thinkingLevel: ThinkingLevel;
467
+ toolOptions: SubagentToolOptions | undefined;
468
+ /** Set only when autoRoutePreset chose the preset (not explicitly requested). */
469
+ autoRoutedPreset: SubagentPreset | undefined;
470
+ }
471
+
472
+ // ---------------------------------------------------------------------------
473
+ // Log levels
474
+ // ---------------------------------------------------------------------------
475
+
476
+ export type LogLevel = "debug" | "info" | "warn" | "error";
477
+
478
+ // ---------------------------------------------------------------------------
479
+ // Event bus types
480
+ // ---------------------------------------------------------------------------
481
+
482
+ /** Types of lifecycle events that can be emitted. */
483
+ export type SubagentEventType =
484
+ | "subagent:created"
485
+ | "subagent:started"
486
+ | "subagent:completed"
487
+ | "subagent:failed"
488
+ | "subagent:stopped"
489
+ | "subagent:steered";
490
+
491
+ /** An event emitted during the subagent lifecycle. */
492
+ export interface SubagentEvent {
493
+ type: SubagentEventType;
494
+ agentId: string;
495
+ timestamp: number;
496
+ data: Record<string, unknown>;
497
+ }
498
+
499
+ /** Callback function for event listeners. */
500
+ export type SubagentEventListener = (event: SubagentEvent) => void;
501
+
502
+ // ---------------------------------------------------------------------------
503
+ // Transcript (agent conversation log) types
504
+ // ---------------------------------------------------------------------------
505
+
506
+ /** Entry kinds written to an agent's transcript file. */
507
+ export type TranscriptEntryType = "system" | "user";
508
+
509
+ /** A single line in an agent's transcript file. */
510
+ export interface TranscriptEntry {
511
+ type: TranscriptEntryType;
512
+ timestamp: number;
513
+ content: string;
514
+ metadata?: Record<string, unknown>;
515
+ }
516
+
517
+ // ---------------------------------------------------------------------------
518
+ // Constants
519
+ // ---------------------------------------------------------------------------
520
+
521
+ export const EMPTY_USAGE: UsageStats = {
522
+ input: 0,
523
+ output: 0,
524
+ cacheRead: 0,
525
+ cacheWrite: 0,
526
+ cost: 0,
527
+ contextTokens: 0,
528
+ turns: 0,
529
+ };
530
+
531
+ export const NAV_FOOTER = "\u2191\u2193 navigate \u2022 enter select \u2022 esc cancel";
532
+
533
+ export const SIGKILL_GRACE_MS = 5000;
534
+ export const STATUS_RESET_DELAY_MS = 3000;
535
+ export const TEMP_FILE_MODE = 0o600;
536
+ export const TASK_PREVIEW_MAX_LENGTH = 80;
537
+ export const COLLAPSED_OUTPUT_LINES = 5;
538
+ export const DEFAULT_OUTPUT_CAP_BYTES = 100 * 1024; // 100KB
539
+
540
+ // P5: Output diffing constants
541
+ export const MAX_HUNKS_PER_FILE = 10;
542
+ export const COLLAPSED_DIFF_FILES_PREVIEW = 5; // max file entries in collapsed view
543
+ export const EXPANDED_HUNKS_PER_FILE = 5; // max hunks shown per file in expanded view
544
+
545
+ export const DEFAULT_MAX_SUBAGENT_DEPTH = 3;
546
+
547
+ export const MAX_RUN_HISTORY_ENTRIES = 500;
548
+ export const MAX_TEMP_DIR_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
549
+
550
+ // Circuit breaker constants
551
+ export const MAX_CONSECUTIVE_FAILURES = 5;
552
+ export const CIRCUIT_BREAKER_RESET_MS = 60000; // 1 min auto-recovery
553
+ export const CIRCUIT_DEGRADED_THINKING: ThinkingLevel = "minimal";
554
+
555
+ export const TEMPLATE_PARAM_RE = /\$\{(\w+)\}/g;
556
+
557
+ export const DEFAULT_SESSION_COST_LIMIT = 0;
558
+
559
+ export const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
560
+
561
+ export const CUSTOM_ENTRY_TYPES = {
562
+ run: "brl-subagent-run",
563
+ state: "brl-subagent-state",
564
+ } as const;
565
+
566
+ // P5: File-level diff summary for structured diff output
567
+ export interface FileDiff {
568
+ path: string; // relative file path (e.g. "src/logging.ts")
569
+ additions: number; // count of + lines
570
+ deletions: number; // count of - lines
571
+ hunks: string[]; // first 10 hunk texts, capped
572
+ totalHunks: number; // actual total (may be more than hunks.length)
573
+ }
574
+
575
+ // ---------------------------------------------------------------------------
576
+ // P1+P2: Chain / parallel constants
577
+ // ---------------------------------------------------------------------------
578
+
579
+ export const MAX_CHAIN_STEPS = 10;
580
+ export const MAX_PARALLEL_TASKS = 8;
581
+ export const MAX_GRAPH_TASKS = 12;
582
+ export const PREVIOUS_OUTPUT_PLACEHOLDER = "{previous}";
583
+ export const GRAPH_OUTPUT_PLACEHOLDER_RE = /\{(\w+)\}/g;
584
+
585
+ /** Pattern matching names starting and ending with double underscores (TUI sentinels). */
586
+ export const RESERVED_NAME_PATTERN = /^__.*__$/;
587
+
588
+ /** Command names that collide with /brl-subagent completions. */
589
+ export const RESERVED_COMMAND_NAMES = new Set([
590
+ "model", "thinking", "concurrency", "depth", "history", "monitor",
591
+ "preset", "retry", "reset", "priority", "templates", "dashboard", "approval",
592
+ "costlimit", "historyentries", "sla", "update-check", "completionnotify",
593
+ "graph", "sla-stats",
594
+ ]);
595
+
596
+ // ---------------------------------------------------------------------------
597
+ // Phase 6: Background agent session types
598
+ // ---------------------------------------------------------------------------
599
+
600
+ export type AgentStatus = "pending" | "running" | "steered" | "completed" | "failed" | "stopped";
601
+
602
+ export interface BackgroundAgent {
603
+ id: string;
604
+ sessionId: string;
605
+ type: string;
606
+ description: string;
607
+ status: AgentStatus;
608
+ startedAt: number;
609
+ completedAt?: number;
610
+ task: string;
611
+ model: string;
612
+ thinkingLevel: ThinkingLevel;
613
+ // Issue #114: per-unit priority for the drill-in header (`p:critical` etc.).
614
+ priority?: string;
615
+ error?: string;
616
+ result?: SubagentResult;
617
+ finalOutput?: string;
618
+ /** @internal — session reference for live monitor polling */
619
+ _sessionRef?: import('@earendil-works/pi-coding-agent').AgentSession;
620
+ }
621
+
622
+ // ---------------------------------------------------------------------------
623
+ // Helper functions
624
+ // ---------------------------------------------------------------------------
625
+
626
+ export function resolveThinkingLevel(
627
+ requested: ThinkingLevel | undefined,
628
+ maxAllowed: ThinkingLevel,
629
+ ): ThinkingLevel {
630
+ if (!requested) return maxAllowed;
631
+ const requestedIdx = THINKING_LEVELS.indexOf(requested);
632
+ const maxIdx = THINKING_LEVELS.indexOf(maxAllowed);
633
+ return THINKING_LEVELS[Math.min(requestedIdx, maxIdx)];
634
+ }
635
+
636
+ export function formatTokens(count: number): string {
637
+ if (count < 1000) return count.toString();
638
+ if (count < 10_000) return `${(count / 1000).toFixed(1)}k`;
639
+ if (count < 1_000_000) return `${Math.round(count / 1000)}k`;
640
+ return `${(count / 1_000_000).toFixed(1)}M`;
641
+ }
642
+
643
+ export function formatUsageStats(usage: UsageStats, model?: string): string {
644
+ const parts: string[] = [];
645
+ if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
646
+ if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
647
+ if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
648
+ if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
649
+ if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
650
+ if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
651
+ if (usage.contextTokens > 0) parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
652
+ if (model) parts.push(model);
653
+ return parts.join(" ");
654
+ }
655
+
656
+ export function getFinalOutput(messages: Array<Record<string, unknown>>): string {
657
+ for (let i = messages.length - 1; i >= 0; i--) {
658
+ const msg = messages[i] as Record<string, unknown>;
659
+ if (msg.role === "assistant") {
660
+ const content = msg.content as Array<Record<string, unknown>> | undefined;
661
+ if (content) {
662
+ for (const part of content) {
663
+ if (part.type === "text") return part.text as string;
664
+ }
665
+ }
666
+ }
667
+ }
668
+ return "";
669
+ }
670
+
671
+ export function isSubagentError(result: SubagentResult): boolean {
672
+ return result.exitCode !== 0
673
+ || result.stopReason === "error"
674
+ || result.stopReason === "aborted";
675
+ }
676
+
677
+ export function formatModel(m: { provider: string; id: string } | undefined): string {
678
+ return m ? `${m.provider}/${m.id}` : "Not set (will use main agent\u2019s model)";
679
+ }
680
+
681
+ export function formatMaxParallel(n: number): string {
682
+ return n === 0 ? "unlimited" : String(n);
683
+ }
684
+
685
+ /**
686
+ * Type guard: checks if a value is a valid state object shape.
687
+ * Used to safely restore persisted state without `as any` casts.
688
+ */
689
+ export function isSubagentStateShape(value: unknown): value is SubagentState {
690
+ if (!value || typeof value !== "object") return false;
691
+ const v = value as Record<string, unknown>;
692
+
693
+ // model is optional but must be valid if present
694
+ if (v.model !== undefined && v.model !== null) {
695
+ if (typeof v.model !== "object") return false;
696
+ const m = v.model as Record<string, unknown>;
697
+ if (typeof m.provider !== "string" || typeof m.id !== "string") return false;
698
+ }
699
+
700
+ // maxThinkingLevel must be a valid level if present
701
+ if (v.maxThinkingLevel !== undefined && typeof v.maxThinkingLevel === "string") {
702
+ if (!THINKING_LEVELS.includes(v.maxThinkingLevel as ThinkingLevel)) return false;
703
+ }
704
+
705
+ // maxParallel must be a non-negative number
706
+ if (v.maxParallel !== undefined) {
707
+ if (typeof v.maxParallel !== "number" || v.maxParallel < 0) return false;
708
+ }
709
+
710
+ // maxSubagentDepth must be a non-negative number if present
711
+ if (v.maxSubagentDepth !== undefined) {
712
+ if (typeof v.maxSubagentDepth !== "number" || v.maxSubagentDepth < 0) return false;
713
+ }
714
+
715
+ // gitMode must be "branch" or "none" if present
716
+ if (v.gitMode !== undefined) {
717
+ if (v.gitMode !== "branch" && v.gitMode !== "none") return false;
718
+ }
719
+
720
+ // approvalMode must be "auto", "writes", or "always" if present
721
+ if (v.approvalMode !== undefined) {
722
+ if (v.approvalMode !== "auto" && v.approvalMode !== "writes" && v.approvalMode !== "always") return false;
723
+ }
724
+
725
+ // completionNotify must be one of COMPLETION_NOTIFY_MODES if present
726
+ if (v.completionNotify !== undefined) {
727
+ if (!COMPLETION_NOTIFY_MODES.includes(v.completionNotify as CompletionNotifyMode)) return false;
728
+ }
729
+
730
+ // maxHistoryEntries must be a non-negative number if present
731
+ if (v.maxHistoryEntries !== undefined) {
732
+ if (typeof v.maxHistoryEntries !== "number" || v.maxHistoryEntries < 0) return false;
733
+ }
734
+
735
+ // sessionCostLimit must be a non-negative number if present
736
+ if (v.sessionCostLimit !== undefined) {
737
+ if (typeof v.sessionCostLimit !== "number" || v.sessionCostLimit < 0) return false;
738
+ }
739
+
740
+ // perTaskCostEstimate must be a non-negative number if present
741
+ if (v.perTaskCostEstimate !== undefined) {
742
+ if (typeof v.perTaskCostEstimate !== "number" || v.perTaskCostEstimate < 0) return false;
743
+ }
744
+
745
+ // slaTrackingEnabled must be a boolean if present
746
+ if (v.slaTrackingEnabled !== undefined && typeof v.slaTrackingEnabled !== "boolean") return false;
747
+
748
+ // slaWindowSize must be a number 10-500 if present
749
+ if (v.slaWindowSize !== undefined) {
750
+ if (typeof v.slaWindowSize !== "number" || v.slaWindowSize < 10 || v.slaWindowSize > 500) return false;
751
+ }
752
+
753
+ // updateCheckEnabled must be a boolean if present
754
+ if (v.updateCheckEnabled !== undefined && typeof v.updateCheckEnabled !== "boolean") return false;
755
+
756
+ // lastUpdateCheck must be a non-negative number if present
757
+ if (v.lastUpdateCheck !== undefined) {
758
+ if (typeof v.lastUpdateCheck !== "number" || v.lastUpdateCheck < 0) return false;
759
+ }
760
+
761
+ return true;
762
+ }
763
+
764
+ /**
765
+ * Type guard: validates a SubagentRun entry shape.
766
+ */
767
+ export function isSubagentRunShape(value: unknown): value is SubagentRun {
768
+ if (!value || typeof value !== "object") return false;
769
+ const v = value as Record<string, unknown>;
770
+ if (typeof v.id !== "string") return false;
771
+ if (typeof v.task !== "string") return false;
772
+ if (!["running", "done", "failed"].includes(v.status as string)) return false;
773
+ return true;
774
+ }
775
+
776
+ /**
777
+ * Type guard: checks if a value is a valid MultiSubagentDetails shape.
778
+ */
779
+ export function isMultiSubagentDetails(value: unknown): value is MultiSubagentDetails {
780
+ if (!value || typeof value !== "object") return false;
781
+ const v = value as Record<string, unknown>;
782
+ if (v.mode !== "chain" && v.mode !== "parallel" && v.mode !== "graph") return false;
783
+ if (typeof v.totalInput !== "number") return false;
784
+ if (typeof v.totalOutput !== "number") return false;
785
+ if (typeof v.totalCost !== "number") return false;
786
+ if (typeof v.totalTurns !== "number") return false;
787
+ return true;
788
+ }
789
+
790
+ /** Type guard for graph mode details. */
791
+ export function isGraphDetails(value: unknown): value is GraphDetails {
792
+ if (!value || typeof value !== "object") return false;
793
+ const v = value as Record<string, unknown>;
794
+ if (v.mode !== "graph") return false;
795
+ if (!Array.isArray(v.waves)) return false;
796
+ if (typeof v.totalInput !== "number") return false;
797
+ if (typeof v.totalOutput !== "number") return false;
798
+ if (typeof v.totalCost !== "number") return false;
799
+ if (typeof v.totalTurns !== "number") return false;
800
+ return true;
801
+ }
802
+
803
+ // ---------------------------------------------------------------------------