pi-crew 0.6.1 → 0.6.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 (119) hide show
  1. package/CHANGELOG.md +194 -0
  2. package/README.md +81 -33
  3. package/docs/issue-29-analysis.md +189 -0
  4. package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +1268 -0
  5. package/docs/ui-optimization-plan.md +447 -0
  6. package/package.json +2 -2
  7. package/src/config/config.ts +106 -15
  8. package/src/errors.ts +107 -0
  9. package/src/extension/async-notifier.ts +6 -2
  10. package/src/extension/crew-cleanup.ts +8 -5
  11. package/src/extension/management.ts +464 -109
  12. package/src/extension/register.ts +213 -35
  13. package/src/extension/registration/brief-tool-overrides.ts +400 -0
  14. package/src/extension/registration/commands.ts +27 -2
  15. package/src/extension/registration/subagent-helpers.ts +2 -2
  16. package/src/extension/registration/subagent-tools.ts +9 -4
  17. package/src/extension/registration/team-tool.ts +17 -10
  18. package/src/extension/registration/viewers.ts +2 -2
  19. package/src/extension/team-tool/api.ts +3 -3
  20. package/src/extension/team-tool/cancel.ts +3 -3
  21. package/src/extension/team-tool/explain.ts +1 -1
  22. package/src/extension/team-tool/handle-schedule.ts +40 -0
  23. package/src/extension/team-tool/inspect.ts +3 -3
  24. package/src/extension/team-tool/lifecycle-actions.ts +4 -4
  25. package/src/extension/team-tool/respond.ts +2 -2
  26. package/src/extension/team-tool/run.ts +64 -14
  27. package/src/extension/team-tool/status.ts +1 -1
  28. package/src/extension/team-tool-types.ts +2 -0
  29. package/src/extension/team-tool.ts +173 -46
  30. package/src/hooks/registry.ts +77 -13
  31. package/src/hooks/types.ts +9 -0
  32. package/src/plugins/plugin-define.ts +6 -0
  33. package/src/plugins/plugin-registry.ts +32 -0
  34. package/src/plugins/plugins/index.ts +3 -0
  35. package/src/plugins/plugins/nextjs.ts +19 -0
  36. package/src/plugins/plugins/vite.ts +14 -0
  37. package/src/plugins/plugins/vitest.ts +14 -0
  38. package/src/runtime/async-runner.ts +35 -7
  39. package/src/runtime/background-runner.ts +529 -144
  40. package/src/runtime/chain-parser.ts +5 -1
  41. package/src/runtime/chain-runner.ts +9 -3
  42. package/src/runtime/checkpoint.ts +262 -180
  43. package/src/runtime/child-pi.ts +164 -37
  44. package/src/runtime/crash-recovery.ts +25 -14
  45. package/src/runtime/crew-agent-records.ts +2 -0
  46. package/src/runtime/diagnostic-export.ts +1 -1
  47. package/src/runtime/dynamic-script-runner.ts +10 -7
  48. package/src/runtime/foreground-watchdog.ts +1 -1
  49. package/src/runtime/heartbeat-watcher.ts +19 -2
  50. package/src/runtime/intercom-bridge.ts +7 -0
  51. package/src/runtime/iteration-hooks.ts +1 -1
  52. package/src/runtime/manifest-cache.ts +4 -0
  53. package/src/runtime/orphan-worker-registry.ts +444 -0
  54. package/src/runtime/parent-guard.ts +70 -16
  55. package/src/runtime/pi-args.ts +437 -14
  56. package/src/runtime/pi-spawn.ts +1 -0
  57. package/src/runtime/post-checks.ts +1 -1
  58. package/src/runtime/retry-runner.ts +9 -3
  59. package/src/runtime/run-tracker.ts +38 -10
  60. package/src/runtime/sandbox.ts +17 -2
  61. package/src/runtime/scheduler.ts +25 -2
  62. package/src/runtime/skill-effectiveness.ts +105 -62
  63. package/src/runtime/skill-instructions.ts +110 -32
  64. package/src/runtime/stale-reconciler.ts +310 -69
  65. package/src/runtime/subagent-manager.ts +251 -57
  66. package/src/runtime/task-health.ts +76 -0
  67. package/src/runtime/task-id.ts +20 -0
  68. package/src/runtime/task-runner/live-executor.ts +1 -1
  69. package/src/runtime/task-runner/progress.ts +1 -1
  70. package/src/runtime/task-runner/state-helpers.ts +110 -6
  71. package/src/runtime/task-runner.ts +92 -70
  72. package/src/runtime/team-runner.ts +186 -20
  73. package/src/schema/team-tool-schema.ts +27 -9
  74. package/src/skills/discover-skills.ts +9 -3
  75. package/src/state/active-run-registry.ts +170 -38
  76. package/src/state/artifact-store.ts +25 -13
  77. package/src/state/atomic-write-v2.ts +86 -0
  78. package/src/state/atomic-write.ts +346 -55
  79. package/src/state/blob-store.ts +178 -10
  80. package/src/state/crew-init.ts +161 -28
  81. package/src/state/decision-ledger.ts +172 -111
  82. package/src/state/event-log-rotation.ts +82 -52
  83. package/src/state/event-log.ts +254 -70
  84. package/src/state/health-store.ts +71 -0
  85. package/src/state/locks.ts +102 -20
  86. package/src/state/mailbox.ts +45 -7
  87. package/src/state/observation-store.ts +4 -1
  88. package/src/state/run-graph.ts +141 -130
  89. package/src/state/run-metrics.ts +24 -8
  90. package/src/state/session-state-map.ts +51 -0
  91. package/src/state/state-store.ts +330 -43
  92. package/src/ui/live-run-sidebar.ts +1 -1
  93. package/src/ui/loaders.ts +4 -0
  94. package/src/ui/overlays/agent-picker-overlay.ts +1 -1
  95. package/src/ui/overlays/mailbox-detail-overlay.ts +1 -1
  96. package/src/ui/powerbar-publisher.ts +1 -1
  97. package/src/ui/run-action-dispatcher.ts +2 -2
  98. package/src/ui/run-snapshot-cache.ts +1 -1
  99. package/src/ui/status-colors.ts +5 -1
  100. package/src/ui/theme-adapter.ts +80 -1
  101. package/src/ui/tool-progress-formatter.ts +9 -5
  102. package/src/ui/tool-render.ts +4 -0
  103. package/src/ui/tool-renderers/brief-mode.ts +207 -0
  104. package/src/ui/tool-renderers/index.ts +627 -0
  105. package/src/ui/widget/index.ts +224 -0
  106. package/src/ui/widget/widget-formatters.ts +148 -0
  107. package/src/ui/widget/widget-model.ts +90 -0
  108. package/src/ui/widget/widget-renderer.ts +130 -0
  109. package/src/ui/widget/widget-types.ts +37 -0
  110. package/src/utils/env-filter.ts +66 -0
  111. package/src/utils/file-coalescer.ts +9 -1
  112. package/src/utils/guards.ts +110 -0
  113. package/src/utils/paths.ts +80 -5
  114. package/src/utils/redaction.ts +6 -1
  115. package/src/utils/safe-paths.ts +281 -10
  116. package/src/worktree/cleanup.ts +112 -24
  117. package/src/worktree/worktree-manager.ts +128 -15
  118. package/test-bugs-all.mjs +10 -6
  119. package/test-integration-check.ts +4 -0
@@ -0,0 +1,400 @@
1
+ /**
2
+ * Brief tool overrides — re-registers built-in Pi tools with custom rendering.
3
+ *
4
+ * Inspired by oh-my-pi's pi-brief extension. Wraps each built-in tool
5
+ * (read, bash, edit, write, find, grep, ls) keeping the original execute
6
+ * but replacing renderCall/renderResult with themed, brief-aware versions.
7
+ *
8
+ * Brief mode shows CONTEXTUAL one-liners that preserve WHAT was done:
9
+ * read ~/file.ts:1-50 → 142 lines · 2.3s
10
+ * bash $ npm test → done · 12.5s
11
+ * edit ~/file.ts → +3 -1 · 0.8s
12
+ * write ~/file.ts (42 lines) → ✓ · 0.2s
13
+ * find *.ts in ~/src → 5 files · 0.5s
14
+ * grep /pattern/ in ~/src → 3 matches · 0.3s
15
+ * ls ~/src → 8 entries · 0.1s
16
+ *
17
+ * Timing: ctx.state.startedAt is set in renderCall, read in renderResult.
18
+ * NOTE: Pi's executionStarted is a boolean flag (NOT a timestamp).
19
+ */
20
+
21
+ import { homedir } from "node:os";
22
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
23
+ import {
24
+ createBashTool,
25
+ createEditTool,
26
+ createFindTool,
27
+ createGrepTool,
28
+ createLsTool,
29
+ createReadTool,
30
+ createWriteTool,
31
+ } from "@earendil-works/pi-coding-agent";
32
+ import { Text } from "@earendil-works/pi-tui";
33
+ import { isBrief } from "../../ui/tool-renderers/brief-mode.ts";
34
+
35
+ // ── Path shortening ────────────────────────────────────────────────────
36
+
37
+ const HOME = homedir();
38
+ function shortenPath(p: string): string {
39
+ if (!p) return "";
40
+ if (p.startsWith(HOME)) return `~${p.slice(HOME.length)}`;
41
+ return p;
42
+ }
43
+
44
+ // ── Types ──────────────────────────────────────────────────────────────
45
+
46
+ interface TextResult {
47
+ content: Array<{ type: string; text?: string }>;
48
+ }
49
+
50
+ interface Theme {
51
+ fg: (slot: string, text: string) => string;
52
+ bold: (text: string) => string;
53
+ }
54
+
55
+ interface RenderCtx {
56
+ args?: Record<string, unknown>;
57
+ state?: Record<string, unknown>;
58
+ }
59
+
60
+ function fullText(result: TextResult): string | undefined {
61
+ const c = result.content.find((x): x is { type: "text"; text: string } => x.type === "text");
62
+ return c?.text;
63
+ }
64
+
65
+ function fullRender(result: TextResult, theme: Theme): Text {
66
+ const text = fullText(result);
67
+ if (!text) return new Text("", 0, 0);
68
+ const lines = text
69
+ .trim()
70
+ .split("\n")
71
+ .map((line) => theme.fg("toolOutput", line))
72
+ .join("\n");
73
+ return new Text(`\n${lines}`, 0, 0);
74
+ }
75
+
76
+ /** Record start time in ctx.state during renderCall (called once per tool invocation). */
77
+ function noteStarted(ctx: RenderCtx | undefined): void {
78
+ if (ctx?.state && !ctx.state.briefStartedAt) {
79
+ ctx.state.briefStartedAt = Date.now();
80
+ }
81
+ }
82
+
83
+ /** Compute elapsed from ctx.state.briefStartedAt. Returns " · 1.2s" or "". */
84
+ function elapsed(ctx: RenderCtx | undefined): string {
85
+ const startedAt = ctx?.state?.briefStartedAt as number | undefined;
86
+ if (!startedAt || typeof startedAt !== "number") return "";
87
+ const ms = Date.now() - startedAt;
88
+ if (ms < 1000) return "";
89
+ if (ms < 60_000) return ` · ${(ms / 1000).toFixed(1)}s`;
90
+ const m = Math.floor(ms / 60_000), s = Math.floor((ms % 60_000) / 1000);
91
+ return ` · ${m}m${s}s`;
92
+ }
93
+
94
+ /** Truncate to maxLen with ellipsis (visual). */
95
+ function trunc(text: string, maxLen: number): string {
96
+ if (text.length <= maxLen) return text;
97
+ return text.slice(0, maxLen - 1) + "…";
98
+ }
99
+
100
+ /** Flatten multiline command to single line, truncated. */
101
+ function flatCmd(cmd: string, maxLen: number): string {
102
+ return trunc(cmd.replace(/\s+/g, " ").trim(), maxLen);
103
+ }
104
+
105
+ // ── Tool registration ──────────────────────────────────────────────────
106
+
107
+ export function registerBriefToolOverrides(pi: ExtensionAPI, cwd: string): void {
108
+ const tools = {
109
+ read: createReadTool(cwd),
110
+ bash: createBashTool(cwd),
111
+ edit: createEditTool(cwd),
112
+ write: createWriteTool(cwd),
113
+ find: createFindTool(cwd),
114
+ grep: createGrepTool(cwd),
115
+ ls: createLsTool(cwd),
116
+ };
117
+
118
+ // ─── Read ───
119
+ pi.registerTool({
120
+ name: "read",
121
+ label: "read",
122
+ description: tools.read.description,
123
+ parameters: tools.read.parameters,
124
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
125
+ async execute(toolCallId: string, params: any, signal: any, onUpdate: any, ctx: any) {
126
+ return tools.read.execute(toolCallId, params, signal, onUpdate);
127
+ },
128
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
129
+ renderCall(args: any, theme: any, ctx: any): any {
130
+ noteStarted(ctx);
131
+ const p = shortenPath(args.path || "");
132
+ const pathDisplay = p ? theme.fg("accent", p) : theme.fg("toolOutput", "...");
133
+ let text = `${theme.fg("toolTitle", theme.bold("read"))} ${pathDisplay}`;
134
+ if (args.offset !== undefined || args.limit !== undefined) {
135
+ const startLine = args.offset ?? 1;
136
+ const endLine = args.limit !== undefined ? startLine + args.limit - 1 : "";
137
+ text += theme.fg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
138
+ }
139
+ return new Text(text, 0, 0);
140
+ },
141
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
142
+ renderResult(result: any, options: any, theme: any, ctx: any): any {
143
+ if (!isBrief() || options.expanded) return fullRender(result, theme);
144
+ const args = ctx?.args ?? {};
145
+ const p = shortenPath(args.path || "?");
146
+ let range = "";
147
+ if (args.offset || args.limit) {
148
+ const s = args.offset ?? 1;
149
+ const e = args.limit ? s + args.limit - 1 : "";
150
+ range = `:${s}${e ? `-${e}` : ""}`;
151
+ }
152
+ const text = fullText(result);
153
+ const count = text ? text.trim().split("\n").filter(Boolean).length : 0;
154
+ const time = elapsed(ctx);
155
+ const label = count === 0 ? "empty" : `${count} lines`;
156
+ return new Text(
157
+ `${theme.fg("toolTitle", "read")} ${theme.fg("accent", p)}${theme.fg("warning", range)} ${theme.fg("dim", "→")} ${theme.fg("muted", label)}${theme.fg("dim", time)}`,
158
+ 0, 0,
159
+ );
160
+ },
161
+ });
162
+
163
+ // ─── Bash ───
164
+ pi.registerTool({
165
+ name: "bash",
166
+ label: "bash",
167
+ description: tools.bash.description,
168
+ parameters: tools.bash.parameters,
169
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
170
+ async execute(toolCallId: string, params: any, signal: any, onUpdate: any, ctx: any) {
171
+ return tools.bash.execute(toolCallId, params, signal, onUpdate);
172
+ },
173
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
174
+ renderCall(args: any, theme: any, ctx: any): any {
175
+ noteStarted(ctx);
176
+ const command = args.command || "...";
177
+ const timeout = args.timeout as number | undefined;
178
+ const timeoutSuffix = timeout ? theme.fg("muted", ` (${timeout}s)`) : "";
179
+ return new Text(theme.fg("toolTitle", theme.bold(`$ ${command}`)) + timeoutSuffix, 0, 0);
180
+ },
181
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
182
+ renderResult(result: any, options: any, theme: any, ctx: any): any {
183
+ if (!isBrief() || options.expanded) return fullRender(result, theme);
184
+ const args = ctx?.args ?? {};
185
+ const cmd = flatCmd(String(args.command || "?"), 50);
186
+ const text = fullText(result);
187
+ const time = elapsed(ctx);
188
+ let label: string;
189
+ let color: string;
190
+ if (!text || !text.trim()) {
191
+ label = "done";
192
+ color = "muted";
193
+ } else {
194
+ const lines = text.trim().split("\n");
195
+ if (lines.length === 1) {
196
+ label = trunc(lines[0]!, 40);
197
+ color = "muted";
198
+ } else {
199
+ label = `${lines.length} lines`;
200
+ color = "muted";
201
+ }
202
+ }
203
+ return new Text(
204
+ `${theme.fg("toolTitle", "$")} ${theme.fg("accent", cmd)} ${theme.fg("dim", "→")} ${theme.fg(color, label)}${theme.fg("dim", time)}`,
205
+ 0, 0,
206
+ );
207
+ },
208
+ });
209
+
210
+ // ─── Edit ───
211
+ pi.registerTool({
212
+ name: "edit",
213
+ label: "edit",
214
+ description: tools.edit.description,
215
+ parameters: tools.edit.parameters,
216
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
217
+ async execute(toolCallId: string, params: any, signal: any, onUpdate: any, ctx: any) {
218
+ return tools.edit.execute(toolCallId, params, signal, onUpdate);
219
+ },
220
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
221
+ renderCall(args: any, theme: any, ctx: any): any {
222
+ noteStarted(ctx);
223
+ const p = shortenPath(args.path || "");
224
+ const pathDisplay = p ? theme.fg("accent", p) : theme.fg("toolOutput", "...");
225
+ return new Text(`${theme.fg("toolTitle", theme.bold("edit"))} ${pathDisplay}`, 0, 0);
226
+ },
227
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
228
+ renderResult(result: any, options: any, theme: any, ctx: any): any {
229
+ if (!isBrief() || options.expanded) return fullRender(result, theme);
230
+ const args = ctx?.args ?? {};
231
+ const p = shortenPath(args.path || "?");
232
+ const text = fullText(result);
233
+ const time = elapsed(ctx);
234
+ if (text && (text.includes("Error") || text.includes("error"))) {
235
+ return new Text(
236
+ `${theme.fg("toolTitle", "edit")} ${theme.fg("accent", p)} ${theme.fg("dim", "→")} ${theme.fg("error", "failed")}${theme.fg("dim", time)}`,
237
+ 0, 0,
238
+ );
239
+ }
240
+ const added = text ? (text.match(/^\+ /gm) ?? []).length : 0;
241
+ const removed = text ? (text.match(/^- /gm) ?? []).length : 0;
242
+ if (added === 0 && removed === 0) {
243
+ return new Text(
244
+ `${theme.fg("toolTitle", "edit")} ${theme.fg("accent", p)} ${theme.fg("dim", "→")} ${theme.fg("success", "edited")}${theme.fg("dim", time)}`,
245
+ 0, 0,
246
+ );
247
+ }
248
+ return new Text(
249
+ `${theme.fg("toolTitle", "edit")} ${theme.fg("accent", p)} ${theme.fg("dim", "→")} ${theme.fg("toolDiffAdded", `+${added} `)}${theme.fg("toolDiffRemoved", `-${removed}`)}${theme.fg("dim", time)}`,
250
+ 0, 0,
251
+ );
252
+ },
253
+ });
254
+
255
+ // ─── Write ───
256
+ pi.registerTool({
257
+ name: "write",
258
+ label: "write",
259
+ description: tools.write.description,
260
+ parameters: tools.write.parameters,
261
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
262
+ async execute(toolCallId: string, params: any, signal: any, onUpdate: any, ctx: any) {
263
+ return tools.write.execute(toolCallId, params, signal, onUpdate);
264
+ },
265
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
266
+ renderCall(args: any, theme: any, ctx: any): any {
267
+ noteStarted(ctx);
268
+ const p = shortenPath(args.path || "");
269
+ const pathDisplay = p ? theme.fg("accent", p) : theme.fg("toolOutput", "...");
270
+ const lineCount = args.content ? String(args.content).split("\n").length : 0;
271
+ const lineInfo = lineCount > 0 ? theme.fg("muted", ` (${lineCount} lines)`) : "";
272
+ return new Text(`${theme.fg("toolTitle", theme.bold("write"))} ${pathDisplay}${lineInfo}`, 0, 0);
273
+ },
274
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
275
+ renderResult(result: any, options: any, theme: any, ctx: any): any {
276
+ if (!isBrief() || options.expanded) return fullRender(result, theme);
277
+ const args = ctx?.args ?? {};
278
+ const p = shortenPath(args.path || "?");
279
+ const text = fullText(result);
280
+ const time = elapsed(ctx);
281
+ if (text) {
282
+ return new Text(
283
+ `${theme.fg("toolTitle", "write")} ${theme.fg("accent", p)} ${theme.fg("dim", "→")} ${theme.fg("error", trunc(text.trim().split("\n")[0] ?? "", 30))}${theme.fg("dim", time)}`,
284
+ 0, 0,
285
+ );
286
+ }
287
+ return new Text(
288
+ `${theme.fg("toolTitle", "write")} ${theme.fg("accent", p)} ${theme.fg("dim", "→")} ${theme.fg("success", "✓")}${theme.fg("dim", time)}`,
289
+ 0, 0,
290
+ );
291
+ },
292
+ });
293
+
294
+ // ─── Find ───
295
+ pi.registerTool({
296
+ name: "find",
297
+ label: "find",
298
+ description: tools.find.description,
299
+ parameters: tools.find.parameters,
300
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
301
+ async execute(toolCallId: string, params: any, signal: any, onUpdate: any, ctx: any) {
302
+ return tools.find.execute(toolCallId, params, signal, onUpdate);
303
+ },
304
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
305
+ renderCall(args: any, theme: any, ctx: any): any {
306
+ noteStarted(ctx);
307
+ const pattern = args.pattern || "";
308
+ const p = shortenPath(args.path || ".");
309
+ let text = `${theme.fg("toolTitle", theme.bold("find"))} ${theme.fg("accent", pattern)}`;
310
+ text += theme.fg("toolOutput", ` in ${p}`);
311
+ return new Text(text, 0, 0);
312
+ },
313
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
314
+ renderResult(result: any, options: any, theme: any, ctx: any): any {
315
+ if (!isBrief() || options.expanded) return fullRender(result, theme);
316
+ const args = ctx?.args ?? {};
317
+ const pattern = trunc(String(args.pattern || "*"), 20);
318
+ const p = shortenPath(args.path || ".");
319
+ const text = fullText(result);
320
+ const count = text ? text.trim().split("\n").filter(Boolean).length : 0;
321
+ const time = elapsed(ctx);
322
+ const label = count === 0 ? "none" : `${count} files`;
323
+ const color = count === 0 ? "dim" : "muted";
324
+ return new Text(
325
+ `${theme.fg("toolTitle", "find")} ${theme.fg("accent", pattern)} ${theme.fg("dim", `in ${p} →`)} ${theme.fg(color, label)}${theme.fg("dim", time)}`,
326
+ 0, 0,
327
+ );
328
+ },
329
+ });
330
+
331
+ // ─── Grep ───
332
+ pi.registerTool({
333
+ name: "grep",
334
+ label: "grep",
335
+ description: tools.grep.description,
336
+ parameters: tools.grep.parameters,
337
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
338
+ async execute(toolCallId: string, params: any, signal: any, onUpdate: any, ctx: any) {
339
+ return tools.grep.execute(toolCallId, params, signal, onUpdate);
340
+ },
341
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
342
+ renderCall(args: any, theme: any, ctx: any): any {
343
+ noteStarted(ctx);
344
+ const pattern = args.pattern || "";
345
+ const p = shortenPath(args.path || ".");
346
+ let text = `${theme.fg("toolTitle", theme.bold("grep"))} ${theme.fg("accent", `/${pattern}/`)}`;
347
+ text += theme.fg("toolOutput", ` in ${p}`);
348
+ return new Text(text, 0, 0);
349
+ },
350
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
351
+ renderResult(result: any, options: any, theme: any, ctx: any): any {
352
+ if (!isBrief() || options.expanded) return fullRender(result, theme);
353
+ const args = ctx?.args ?? {};
354
+ const pattern = trunc(String(args.pattern || "?"), 20);
355
+ const p = shortenPath(args.path || ".");
356
+ const text = fullText(result);
357
+ const count = text ? text.trim().split("\n").filter(Boolean).length : 0;
358
+ const time = elapsed(ctx);
359
+ const label = count === 0 ? "none" : `${count} matches`;
360
+ const color = count === 0 ? "dim" : "muted";
361
+ return new Text(
362
+ `${theme.fg("toolTitle", "grep")} ${theme.fg("accent", `/${pattern}/`)} ${theme.fg("dim", `in ${p} →`)} ${theme.fg(color, label)}${theme.fg("dim", time)}`,
363
+ 0, 0,
364
+ );
365
+ },
366
+ });
367
+
368
+ // ─── Ls ───
369
+ pi.registerTool({
370
+ name: "ls",
371
+ label: "ls",
372
+ description: tools.ls.description,
373
+ parameters: tools.ls.parameters,
374
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
375
+ async execute(toolCallId: string, params: any, signal: any, onUpdate: any, ctx: any) {
376
+ return tools.ls.execute(toolCallId, params, signal, onUpdate);
377
+ },
378
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
379
+ renderCall(args: any, theme: any, ctx: any): any {
380
+ noteStarted(ctx);
381
+ const p = shortenPath(args.path || ".");
382
+ return new Text(`${theme.fg("toolTitle", theme.bold("ls"))} ${theme.fg("accent", p)}`, 0, 0);
383
+ },
384
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
385
+ renderResult(result: any, options: any, theme: any, ctx: any): any {
386
+ if (!isBrief() || options.expanded) return fullRender(result, theme);
387
+ const args = ctx?.args ?? {};
388
+ const p = shortenPath(args.path || ".");
389
+ const text = fullText(result);
390
+ const count = text ? text.trim().split("\n").filter(Boolean).length : 0;
391
+ const time = elapsed(ctx);
392
+ const label = count === 0 ? "empty" : `${count} entries`;
393
+ const color = count === 0 ? "dim" : "muted";
394
+ return new Text(
395
+ `${theme.fg("toolTitle", "ls")} ${theme.fg("accent", p)} ${theme.fg("dim", "→")} ${theme.fg(color, label)}${theme.fg("dim", time)}`,
396
+ 0, 0,
397
+ );
398
+ },
399
+ });
400
+ }
@@ -154,7 +154,7 @@ function teamCommandContext(ctx: ExtensionCommandContext): ExtensionCommandConte
154
154
  }
155
155
 
156
156
  async function handleHealthDashboardAction(ctx: ExtensionCommandContext, selection: RunDashboardSelection): Promise<void> {
157
- const loaded = loadRunManifestById(ctx.cwd, selection.runId);
157
+ const loaded = loadRunManifestById(ctx.cwd, selection.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
158
158
  if (!loaded) {
159
159
  depsNotify(ctx, `Run '${selection.runId}' not found.`, "error");
160
160
  return;
@@ -379,7 +379,7 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
379
379
  pi.registerCommand("team-result", { description: "Open a pi-crew agent result viewer: <runId> [taskId]", handler: async (args: string, ctx: ExtensionCommandContext) => {
380
380
  const [runId, rawTaskId] = args.trim().split(/\s+/).filter(Boolean);
381
381
  const selected = await selectAgentTask(ctx, runId, rawTaskId);
382
- const loaded = selected ? loadRunManifestById(ctx.cwd, selected.runId) : undefined;
382
+ const loaded = selected ? loadRunManifestById(ctx.cwd, selected.runId) : undefined; // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
383
383
  if (ctx.hasUI && loaded) {
384
384
  const agent = readCrewAgents(loaded.manifest).find((item) => item.taskId === selected?.taskId || item.id === selected?.taskId) ?? readCrewAgents(loaded.manifest)[0];
385
385
  const resultText = agent?.resultArtifactPath ? commandText(await handleTeamTool({ action: "api", runId: selected?.runId ?? "", config: { operation: "read-agent-output", agentId: agent.taskId, maxBytes: 64_000 } }, teamCommandContext(ctx))) : "(no result)";
@@ -571,6 +571,31 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
571
571
  pi.registerCommand("team-help", { description: "Show pi-crew command help", handler: async (_args: string, ctx: ExtensionCommandContext) => {
572
572
  await notifyCommandResult(ctx, piTeamsHelp());
573
573
  } });
574
+
575
+ // Brief mode command — toggle compact tool output display
576
+ pi.registerCommand("crew-brief", {
577
+ description: "Toggle brief tool output mode: on | off | status",
578
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
579
+ const { isBrief, setBrief, BRIEF_ENTRY_TYPE, makeBriefEntry } = await import("../../ui/tool-renderers/brief-mode.ts");
580
+ const trimmed = args.trim();
581
+
582
+ if (trimmed === "on") {
583
+ setBrief(true);
584
+ pi.appendEntry(BRIEF_ENTRY_TYPE, { enabled: true });
585
+ ctx.ui.notify("Brief mode: on — tool output will show compact summaries", "info");
586
+ return;
587
+ }
588
+ if (trimmed === "off") {
589
+ setBrief(false);
590
+ pi.appendEntry(BRIEF_ENTRY_TYPE, { enabled: false });
591
+ ctx.ui.notify("Brief mode: off — full tool output restored", "info");
592
+ return;
593
+ }
594
+ // status (default)
595
+ ctx.ui.notify(`Brief mode: ${isBrief() ? "on" : "off"}`, "info");
596
+ },
597
+ });
598
+
574
599
  time("register.commands");
575
600
  printTimings();
576
601
  }
@@ -34,7 +34,7 @@ export function sendAgentWakeUp(pi: ExtensionAPI, content: string): boolean {
34
34
 
35
35
  export function refreshPersistedSubagentRecord(ctx: ExtensionContext | ExtensionCommandContext, record: SubagentRecord): SubagentRecord {
36
36
  if (!record.runId) return record;
37
- const loaded = loadRunManifestById(ctx.cwd, record.runId);
37
+ const loaded = loadRunManifestById(ctx.cwd, record.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
38
38
  if (!loaded) return record;
39
39
  if (loaded.manifest.status === "completed" || loaded.manifest.status === "failed" || loaded.manifest.status === "cancelled" || loaded.manifest.status === "blocked") {
40
40
  const refreshed = {
@@ -65,7 +65,7 @@ export function formatSubagentRecord(record: SubagentRecord): string {
65
65
 
66
66
  export function readSubagentRunResult(ctx: ExtensionContext | ExtensionCommandContext, record: SubagentRecord): string | undefined {
67
67
  if (!record.runId) return record.result;
68
- const loaded = loadRunManifestById(ctx.cwd, record.runId);
68
+ const loaded = loadRunManifestById(ctx.cwd, record.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
69
69
  const task = loaded?.tasks.find((item) => item.resultArtifact) ?? loaded?.tasks[0];
70
70
  const artifactPath = task?.resultArtifact?.path;
71
71
  if (!artifactPath || !loaded) return undefined;
@@ -22,7 +22,8 @@ import { t } from "../../i18n.ts";
22
22
  import { loadRunManifestById } from "../../state/state-store.ts";
23
23
  import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
24
24
  import { formatCompactToolProgress } from "../../ui/tool-progress-formatter.ts";
25
- import { renderAgentToolCall, renderAgentToolResult } from "../../ui/tool-render.ts";
25
+ import { Text } from "@earendil-works/pi-tui";
26
+ import { agentToolRenderer } from "../../ui/tool-renderers/index.ts";
26
27
 
27
28
  const TOOL_PROGRESS_TICK_MS = 1000;
28
29
 
@@ -98,11 +99,15 @@ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: Subagen
98
99
  },
99
100
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
100
101
  renderCall(args: any, theme: any, context: any): any {
101
- return renderAgentToolCall(args, theme, context);
102
+ return agentToolRenderer.renderCall(args, theme, context);
102
103
  },
103
104
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
104
105
  renderResult(result: any, options: any, theme: any, context: any): any {
105
- return renderAgentToolResult(result, options, theme, context);
106
+ try {
107
+ return agentToolRenderer.renderResult(result, options, theme, context);
108
+ } catch (e: any) {
109
+ return new Text('agent-err: ' + (e?.message ?? 'unknown'), 0, 0);
110
+ }
106
111
  },
107
112
  };
108
113
 
@@ -204,7 +209,7 @@ function startAgentToolProgress(cwd: string, agentRecordId: string, onUpdate: On
204
209
  let tasks;
205
210
  let agents;
206
211
  if (record.runId) {
207
- const loaded = loadRunManifestById(cwd, record.runId);
212
+ const loaded = loadRunManifestById(cwd, record.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
208
213
  if (loaded) {
209
214
  manifest = loaded.manifest;
210
215
  tasks = loaded.tasks;
@@ -1,15 +1,16 @@
1
- import * as fs from "node:fs";
1
+ import { statSync } from "node:fs";
2
2
  import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
3
3
  import { loadConfig } from "../../config/config.ts";
4
4
  import { TeamToolParams, type TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
5
- import type { CrewWidgetState } from "../../ui/crew-widget.ts";
6
- import { updateCrewWidget } from "../../ui/crew-widget.ts";
5
+ import type { CrewWidgetState } from "../../ui/widget/index.ts";
6
+ import { updateCrewWidget } from "../../ui/widget/index.ts";
7
7
  import { updatePiCrewPowerbar } from "../../ui/powerbar-publisher.ts";
8
8
  import type { createManifestCache } from "../../runtime/manifest-cache.ts";
9
9
  import type { createRunSnapshotCache } from "../../ui/run-snapshot-cache.ts";
10
10
  import type { MetricRegistry } from "../../observability/metric-registry.ts";
11
11
  import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
12
- import { renderTeamToolCall, renderTeamToolResult } from "../../ui/tool-render.ts";
12
+ import { Text } from "@earendil-works/pi-tui";
13
+ import { teamToolRenderer, statusIcon } from "../../ui/tool-renderers/index.ts";
13
14
  // Team tool handler — lazy-loaded because team-tool.ts imports many modules
14
15
  import type { handleTeamTool as HandleTeamToolFn } from "../team-tool.ts";
15
16
  let _cachedHandleTeamTool: typeof HandleTeamToolFn | undefined;
@@ -48,7 +49,7 @@ export function resolveCwdOverride(baseCwd: string, override: string | undefined
48
49
  if (!override) return { ok: true, cwd: baseCwd };
49
50
  try {
50
51
  const resolved = resolveRealContainedPath(baseCwd, override);
51
- const stat = fs.statSync(resolved);
52
+ const stat = statSync(resolved);
52
53
  if (!stat.isDirectory()) return { ok: false, error: `cwd override is not a directory: ${resolved}` };
53
54
  return { ok: true, cwd: resolved };
54
55
  } catch (error) {
@@ -107,11 +108,15 @@ export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps):
107
108
  },
108
109
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
109
110
  renderCall(args: any, theme: any, context: any): any {
110
- return renderTeamToolCall(args, theme, context);
111
+ return teamToolRenderer.renderCall(args, theme, context);
111
112
  },
112
113
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
113
114
  renderResult(result: any, options: any, theme: any, context: any): any {
114
- return renderTeamToolResult(result, options, theme, context);
115
+ try {
116
+ return teamToolRenderer.renderResult(result, options, theme, context);
117
+ } catch {
118
+ return new Text(statusIcon("completed", theme) + " done", 0, 0);
119
+ }
115
120
  },
116
121
  };
117
122
  pi.registerTool(tool);
@@ -133,13 +138,15 @@ function startTeamToolProgressBinder(onUpdate: OnUpdate | undefined): TeamToolPr
133
138
  try {
134
139
  if (!cwd || !runId) {
135
140
  const elapsed = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
136
- onUpdate({ content: [{ type: "text", text: `team status=starting elapsed=${elapsed}s` }] });
141
+ const msg = `team status=starting elapsed=${elapsed}s`;
142
+ onUpdate({ content: [{ type: "text", text: msg }] });
137
143
  return;
138
144
  }
139
145
  const loaded = loadRunManifestById(cwd, runId);
140
146
  if (!loaded) {
141
147
  const elapsed = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
142
- onUpdate({ content: [{ type: "text", text: `team run=${runId} elapsed=${elapsed}s (manifest pending)` }] });
148
+ const msg = `team run=${runId} elapsed=${elapsed}s (manifest pending)`;
149
+ onUpdate({ content: [{ type: "text", text: msg }] });
143
150
  return;
144
151
  }
145
152
  let agents;
@@ -162,7 +169,7 @@ function startTeamToolProgressBinder(onUpdate: OnUpdate | undefined): TeamToolPr
162
169
  const timer = setInterval(tick, TEAM_TOOL_PROGRESS_TICK_MS);
163
170
  if (typeof timer.unref === "function") timer.unref();
164
171
  return {
165
- attach: (boundCwd, boundRunId) => { cwd = boundCwd; runId = boundRunId; tick(); },
172
+ attach: (boundCwd: string, boundRunId: string) => { cwd = boundCwd; runId = boundRunId; tick(); },
166
173
  stop: () => clearInterval(timer),
167
174
  };
168
175
  }
@@ -23,7 +23,7 @@ async function getViewer(): Promise<typeof DurableTranscriptViewerType> {
23
23
  export async function selectAgentTask(ctx: ExtensionCommandContext, runId: string | undefined, taskId?: string): Promise<{ runId: string; taskId?: string } | undefined> {
24
24
  if (!runId) return undefined;
25
25
  if (taskId) return { runId, taskId };
26
- const loaded = loadRunManifestById(ctx.cwd, runId);
26
+ const loaded = loadRunManifestById(ctx.cwd, runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
27
27
  if (!loaded) return { runId };
28
28
  const agents = readCrewAgents(loaded.manifest);
29
29
  if (ctx.hasUI && agents.length > 1) {
@@ -39,7 +39,7 @@ export async function openTranscriptViewer(ctx: ExtensionCommandContext, initial
39
39
  const runId = selected.runId;
40
40
  const taskId = selected.taskId;
41
41
  if (!runId || !ctx.hasUI) return false;
42
- const loaded = loadRunManifestById(ctx.cwd, runId);
42
+ const loaded = loadRunManifestById(ctx.cwd, runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
43
43
  if (!loaded) return false;
44
44
  const uiConfig = loadConfig(ctx.cwd).config.ui;
45
45
  const DurableTranscriptViewer = await getViewer();
@@ -92,7 +92,7 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
92
92
  if (!params.runId) return result("API requires runId.", { action: "api", status: "error" }, true);
93
93
  const runCwd = locateRunCwd(params.runId, ctx.cwd);
94
94
  if (!runCwd) return result(`Run '${params.runId}' not found.`, { action: "api", status: "error" }, true);
95
- const loaded = loadRunManifestById(runCwd, params.runId);
95
+ const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
96
96
  if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "api", status: "error" }, true);
97
97
  if (operation === "read-manifest") {
98
98
  return result(JSON.stringify(loaded.manifest, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
@@ -102,7 +102,7 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
102
102
  if (!permission.allowed) return result(permission.reason ?? "Plan approval is not allowed in this context.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
103
103
  try {
104
104
  return withRunLockSync(loaded.manifest, () => {
105
- const current = loadRunManifestById(ctx.cwd, loaded.manifest.runId) ?? loaded;
105
+ const current = loadRunManifestById(ctx.cwd, loaded.manifest.runId) ?? loaded; // NOTE: inside withRunLockSync - consistent read
106
106
  const approval = current.manifest.planApproval;
107
107
  if (!approval?.required || approval.status !== "pending") return result("Run has no pending plan approval request.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
108
108
  const now = new Date().toISOString();
@@ -121,7 +121,7 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
121
121
  if (!permission.allowed) return result(permission.reason ?? "Plan approval cancellation is not allowed in this context.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
122
122
  try {
123
123
  return withRunLockSync(loaded.manifest, () => {
124
- const current = loadRunManifestById(ctx.cwd, loaded.manifest.runId) ?? loaded;
124
+ const current = loadRunManifestById(ctx.cwd, loaded.manifest.runId) ?? loaded; // NOTE: inside withRunLockSync - consistent read
125
125
  const approval = current.manifest.planApproval;
126
126
  if (!approval?.required || approval.status !== "pending") return result("Run has no pending plan approval request.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
127
127
  const now = new Date().toISOString();
@@ -41,7 +41,7 @@ export function abortOwned(
41
41
  ): AbortOwnedResult {
42
42
  const runCwd = locateRunCwd(runId, ctx.cwd);
43
43
  if (!runCwd) return { abortedIds: [], missingIds: taskIds ?? [], foreignIds: [] };
44
- const loaded = loadRunManifestById(runCwd, runId);
44
+ const loaded = loadRunManifestById(runCwd, runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
45
45
  if (!loaded) return { abortedIds: [], missingIds: taskIds ?? [], foreignIds: [] };
46
46
 
47
47
  const result: AbortOwnedResult = { abortedIds: [], missingIds: [], foreignIds: [] };
@@ -81,7 +81,7 @@ export async function handleRetry(params: TeamToolParamsValue, ctx: TeamContext,
81
81
  if (!params.runId) return result("Retry requires runId.", { action: "retry", status: "error" }, true);
82
82
  const runCwd = locateRunCwd(params.runId, ctx.cwd);
83
83
  if (!runCwd) return result(`Run '${params.runId}' not found.`, { action: "retry", status: "error" }, true);
84
- const loaded = loadRunManifestById(runCwd, params.runId);
84
+ const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
85
85
  if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "retry", status: "error" }, true);
86
86
 
87
87
  // Pre-lock ownership check: reject foreign-owned runs unless force is set
@@ -146,7 +146,7 @@ export async function handleCancel(params: TeamToolParamsValue, ctx: TeamContext
146
146
  if (!params.runId) return result("Cancel requires runId.", { action: "cancel", status: "error" }, true);
147
147
  const runCwd = locateRunCwd(params.runId, ctx.cwd);
148
148
  if (!runCwd) return result(`Run '${params.runId}' not found.`, { action: "cancel", status: "error" }, true);
149
- const loaded = loadRunManifestById(runCwd, params.runId);
149
+ const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
150
150
  if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "cancel", status: "error" }, true);
151
151
 
152
152
  // Pre-lock ownership check: reject foreign-owned runs unless force is set
@@ -209,7 +209,7 @@ export function handleExplain(params: {
209
209
  return result("explain requires runId", { action: "explain", status: "error" }, true);
210
210
  }
211
211
 
212
- const loaded = loadRunManifestById(cwd, params.runId);
212
+ const loaded = loadRunManifestById(cwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
213
213
  if (!loaded) {
214
214
  return result(`Run '${params.runId}' not found.`, { action: "explain", status: "error" }, true);
215
215
  }