pi-crew 0.9.8 → 0.9.10

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 (47) hide show
  1. package/CHANGELOG.md +311 -0
  2. package/README.md +2 -2
  3. package/docs/fixes/v0.9.10/locks-fix-verify.md +3 -0
  4. package/docs/fixes/v0.9.10/smoke-test.md +12 -0
  5. package/package.json +1 -1
  6. package/src/extension/register.ts +94 -21
  7. package/src/extension/registration/subagent-helpers.ts +1 -0
  8. package/src/extension/registration/subagent-tools.ts +9 -0
  9. package/src/extension/team-tool/doctor.ts +41 -18
  10. package/src/runtime/batch-barrier.ts +145 -0
  11. package/src/runtime/child-pi.ts +135 -22
  12. package/src/runtime/compact-pipeline.ts +56 -0
  13. package/src/runtime/compact-stages/ansi-strip-stage.ts +25 -0
  14. package/src/runtime/compact-stages/blank-collapse-stage.ts +31 -0
  15. package/src/runtime/compact-stages/deduplicate-stage.ts +34 -0
  16. package/src/runtime/compact-stages/head-snap-stage.ts +57 -0
  17. package/src/runtime/compact-stages/index.ts +13 -0
  18. package/src/runtime/compact-stages/tail-capture-stage.ts +72 -0
  19. package/src/runtime/compact-stages/truncation-stage.ts +71 -0
  20. package/src/runtime/crash-classification.ts +208 -0
  21. package/src/runtime/custom-tools/irc-tool.ts +47 -7
  22. package/src/runtime/handoff-manager.ts +10 -0
  23. package/src/runtime/important-line-classifier.ts +130 -0
  24. package/src/runtime/iteration-hooks.ts +7 -19
  25. package/src/runtime/live-agent-manager.ts +185 -0
  26. package/src/runtime/live-session-runtime.ts +50 -1
  27. package/src/runtime/model-fallback.ts +29 -1
  28. package/src/runtime/process-lifecycle.ts +481 -0
  29. package/src/runtime/role-permission.ts +2 -2
  30. package/src/runtime/stream-preview.ts +9 -2
  31. package/src/runtime/subagent-manager.ts +6 -0
  32. package/src/runtime/task-output-context.ts +209 -24
  33. package/src/runtime/task-runner.ts +76 -15
  34. package/src/runtime/tool-output-pruner.ts +334 -0
  35. package/src/state/locks.ts +16 -0
  36. package/src/state/state-store.ts +8 -2
  37. package/src/state/types.ts +5 -0
  38. package/src/ui/live-run-sidebar.ts +6 -1
  39. package/src/ui/loaders.ts +24 -4
  40. package/src/ui/run-dashboard.ts +6 -1
  41. package/src/ui/run-event-bus.ts +1 -1
  42. package/src/ui/run-snapshot-cache.ts +50 -16
  43. package/src/ui/widget/index.ts +27 -5
  44. package/src/ui/widget/widget-renderer.ts +43 -13
  45. package/src/utils/redaction.ts +17 -1
  46. package/src/utils/visual.ts +6 -0
  47. package/src/ui/crew-widget.ts +0 -544
@@ -11,12 +11,21 @@ import { Box, Text } from "../layout-primitives.ts";
11
11
  import { listLiveAgents } from "../../runtime/live-agent-manager.ts";
12
12
  import { computePhaseProgress, formatPhaseProgressLine } from "../../runtime/phase-progress.ts";
13
13
  import { spinnerFrame } from "../spinner.ts";
14
- import { agentActivity, agentStats, notificationBadge } from "./widget-formatters.ts";
15
- import { shortRunLabel } from "./widget-model.ts";
14
+ import { computeLiveDurationMs } from "../live-duration.ts";
15
+ import { getTaskUsage } from "../../runtime/usage-tracker.ts";
16
+ import { agentActivity, agentStats, elapsed, formatTokensCompact, notificationBadge } from "./widget-formatters.ts";
17
+ import { activeWidgetRuns, shortRunLabel } from "./widget-model.ts";
16
18
  import type { WidgetRun } from "./widget-types.ts";
17
19
 
18
20
  const MAX_AGENTS_DISPLAY = 3;
19
21
  const FINISHED_LINGER_MAX_AGE = 1;
22
+ /** Default terminal width when caller doesn't pass one explicitly. Keep <= 116
23
+ * (the same default used elsewhere in pi-crew tool renderers) so we never paint
24
+ * a line wider than the smallest expected TUI. Callers SHOULD pass the real
25
+ * width when known (via ctx.width || process.stdout.columns). */
26
+ export const DEFAULT_WIDGET_WIDTH = 100;
27
+ /** Cap per-component text so a single field cannot blow past width on its own. */
28
+ export const TASK_DESC_MAX = 60;
20
29
  const ERROR_LINGER_MAX_AGE = 2;
21
30
  const ERROR_STATUSES = new Set(["failed", "cancelled", "stopped", "needs_attention"]);
22
31
 
@@ -37,8 +46,12 @@ export function widgetHeader(runs: WidgetRun[], runningGlyph: string, maxLines =
37
46
 
38
47
  // ── Line builder ──────────────────────────────────────────────────────
39
48
 
40
- export function buildWidgetLines(cwd: string, frame = 0, maxLines = 8, providedRuns?: WidgetRun[], notificationCount = 0): string[] {
41
- const runs = providedRuns ?? [];
49
+ export function buildWidgetLines(cwd: string, frame = 0, maxLines = 8, providedRuns?: WidgetRun[], notificationCount = 0, width = DEFAULT_WIDGET_WIDTH): string[] {
50
+ // Match the legacy `buildCrewWidgetLines` API: when no runs are supplied,
51
+ // auto-fetch via activeWidgetRuns(cwd). Otherwise widgets calling with
52
+ // only `(cwd, frame)` would render an empty line set (regression vs. the
53
+ // pre-refactor implementation that called activeWidgetRuns here).
54
+ const runs = providedRuns ?? activeWidgetRuns(cwd);
42
55
  if (!runs.length) return [];
43
56
 
44
57
  const runningGlyph = spinnerFrame("widget-header");
@@ -56,9 +69,23 @@ export function buildWidgetLines(cwd: string, frame = 0, maxLines = 8, providedR
56
69
  });
57
70
  const completed = agents.filter((a) => a.status === "completed").length;
58
71
  const runGlyph = iconForStatus(run.status, { runningGlyph });
59
- const phaseLine = snapshot ? formatPhaseProgressLine(computePhaseProgress(snapshot.tasks)) : "";
60
- const progressPart = phaseLine || `${completed}/${agents.length} done`;
61
- lines.push(`├─ ${runGlyph} ${shortRunLabel(run)} · ${progressPart} · ${run.runId.slice(-8)}`);
72
+ // Run progress line. v1–v3 flickered on snapshot.tasks state, v4 was
73
+ // too minimal (`0/1 agents` only), v5 duplicated the worker activity
74
+ // line (tools/tokens/duration already shown one row below). v6 (this)
75
+ // shows only data that is RUN-level (not already in the per-agent
76
+ // activity line) and is GUARANTEED stable across ticks:
77
+ // - agents count — from `agents` array, always populated, never empty.
78
+ // - run elapsed — from `run.createdAt`, always set on manifest.
79
+ // Both come from sources with no race window — `agents` is read from
80
+ // snapshot.agents OR agentsFor(run) (both always return same length
81
+ // for a healthy run), and `run.createdAt` is immutable. The format
82
+ // shape `"X/Y agents · Ns"` is therefore truly invariant: same number
83
+ // of `·`-separated fields, same field meanings, every render tick.
84
+ const agentCountText = `${completed}/${agents.length} agents`;
85
+ const runElapsedMs = Math.max(0, Date.now() - new Date(run.createdAt).getTime());
86
+ const runElapsedText = `${Math.floor(runElapsedMs / 1000)}s`;
87
+ const progressPart = `${agentCountText} · ${runElapsedText}`;
88
+ lines.push(truncate(`├─ ${runGlyph} ${shortRunLabel(run)} · ${progressPart} · ${run.runId.slice(-8)}`, width));
62
89
 
63
90
  const liveForRun = listLiveAgents().filter((a) => a.runId === run.runId);
64
91
 
@@ -67,8 +94,9 @@ export function buildWidgetLines(cwd: string, frame = 0, maxLines = 8, providedR
67
94
  const name = liveHandle?.agent ?? agent.agent;
68
95
  const icon = agent.status === "completed" ? "✓" : agent.status === "failed" ? "✗" : agent.status === "needs_attention" ? "⚠" : "▪";
69
96
  const stats = agentStats(agent, liveHandle);
70
- const desc = liveHandle?.description ?? agent.role;
71
- lines.push(`│ ├─ ${icon} ${name} · ${desc}${stats ? ` · ${stats}` : ""}`);
97
+ const desc = truncate(liveHandle?.description ?? agent.role ?? "", TASK_DESC_MAX);
98
+ const _finished = truncate(`│ ├─ ${icon} ${name} · ${desc}${stats ? ` · ${stats}` : ""}`, width);
99
+ lines.push(_finished);
72
100
  }
73
101
 
74
102
  const visibleAgents = activeAgents.slice(0, MAX_AGENTS_DISPLAY);
@@ -79,13 +107,15 @@ export function buildWidgetLines(cwd: string, frame = 0, maxLines = 8, providedR
79
107
  const liveHandle = liveForRun.find((h) => h.taskId === agent.taskId);
80
108
  const stats = agentStats(agent, liveHandle);
81
109
  const name = liveHandle?.agent ?? agent.agent;
82
- const desc = liveHandle?.description ?? agent.role;
83
- lines.push(`│ ${branch} ${agentGlyph} ${name}${desc ? ` · ${desc}` : ` · ${agent.role}`}`);
84
- lines.push(`│ ⊶ ${agentActivity(agent, liveHandle)}${stats ? ` · ${stats}` : ""}`);
110
+ const desc = truncate(liveHandle?.description ?? agent.role ?? "", TASK_DESC_MAX);
111
+ const _activeMain = truncate(`│ ${branch} ${agentGlyph} ${name}${desc ? ` · ${desc}` : ` · ${agent.role}`}`, width);
112
+ lines.push(_activeMain);
113
+ const _activity = truncate(`│ ⊶ ${agentActivity(agent, liveHandle)}${stats ? ` · ${stats}` : ""}`, width);
114
+ lines.push(_activity);
85
115
  }
86
116
 
87
117
  if (activeAgents.length > MAX_AGENTS_DISPLAY) {
88
- lines.push(`│ └─ … +${activeAgents.length - MAX_AGENTS_DISPLAY} more agents`);
118
+ lines.push(truncate(`│ └─ … +${activeAgents.length - MAX_AGENTS_DISPLAY} more agents`, width));
89
119
  }
90
120
 
91
121
  if (lines.length >= maxLines) break;
@@ -208,6 +208,22 @@ export function redactSecretString(value: string): string {
208
208
  // i++ (advance 1 char) and re-scanned from i+1, so a long run was rescanned O(n)
209
209
  // times = O(n^2). The P1f ReDoS test (300KB no-dot input) surfaced this pre-existing
210
210
  // bug. Now advances past the whole run when it isn't a redactable secret -> O(n).
211
+ // FIX (BG2 follow-up): the inner-loop regex test /[a-zA-Z0-9_-]/.test(value[j])
212
+ // is O(1) per call but with measurable regex-engine overhead — for a 100KB
213
+ // underscore run that's 100K regex calls, well over the 200ms budget the
214
+ // P1f regression test allows. Replaced with a charCodeAt check (~5x faster
215
+ // in practice, O(1) per call with no regex allocation/eval cost).
216
+ function isKeyChar(c: string): boolean {
217
+ const code = c.charCodeAt(0);
218
+ return (
219
+ (code >= 48 && code <= 57) || // 0-9
220
+ (code >= 65 && code <= 90) || // A-Z
221
+ (code >= 97 && code <= 122) || // a-z
222
+ code === 95 || // _
223
+ code === 45 // -
224
+ );
225
+ }
226
+
211
227
  function redactInlineSecrets(value: string): string {
212
228
  const result: string[] = [];
213
229
  let i = 0;
@@ -215,7 +231,7 @@ function redactInlineSecrets(value: string): string {
215
231
  while (i < value.length) {
216
232
  // Collect a run of key characters (alphanumeric, underscore, hyphen).
217
233
  let j = i;
218
- while (j < value.length && /[a-zA-Z0-9_-]/.test(value[j])) {
234
+ while (j < value.length && isKeyChar(value[j])) {
219
235
  j++;
220
236
  }
221
237
  const keyLen = j - i;
@@ -29,6 +29,12 @@ const WIDE_RANGES: Array<[number, number]> = [
29
29
  [0x274C, 0x274C], [0x274E, 0x274E], [0x2753, 0x2755], [0x2757, 0x2757],
30
30
  [0x2763, 0x2764], [0x2795, 0x2797], [0x27A1, 0x27A1], [0x27B0, 0x27B0],
31
31
  [0x27BF, 0x27BF],
32
+ // Geometric Shapes / Misc Symbols-Arrows emoji that pi-tui upstream counts as
33
+ // width=2 (RGI emoji). Mismatch here caused the "Rendered line N exceeds
34
+ // terminal width (160 > 159)" TUI crash: pi-crew truncated to width 159 by
35
+ // its own (mismatched) measure, then Box padded to 159 chars, but pi-tui
36
+ // re-measured the padded line at 160 because ⬜ counts as 2 upstream.
37
+ [0x2B1B, 0x2B1C], // ⬛ BLACK LARGE SQUARE, ⬜ WHITE LARGE SQUARE
32
38
  [0x1F300, 0x1F9FF], // Misc Symbols, Emoticons, Transport, Map, Supplement
33
39
  [0x1FA00, 0x1FAFF], // Symbols Extended-A
34
40
  [0x1F000, 0x1F02F], // Mahjong, Dominos