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.
- package/CHANGELOG.md +194 -0
- package/README.md +81 -33
- package/docs/issue-29-analysis.md +189 -0
- package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +1268 -0
- package/docs/ui-optimization-plan.md +447 -0
- package/package.json +2 -2
- package/src/config/config.ts +106 -15
- package/src/errors.ts +107 -0
- package/src/extension/async-notifier.ts +6 -2
- package/src/extension/crew-cleanup.ts +8 -5
- package/src/extension/management.ts +464 -109
- package/src/extension/register.ts +213 -35
- package/src/extension/registration/brief-tool-overrides.ts +400 -0
- package/src/extension/registration/commands.ts +27 -2
- package/src/extension/registration/subagent-helpers.ts +2 -2
- package/src/extension/registration/subagent-tools.ts +9 -4
- package/src/extension/registration/team-tool.ts +17 -10
- package/src/extension/registration/viewers.ts +2 -2
- package/src/extension/team-tool/api.ts +3 -3
- package/src/extension/team-tool/cancel.ts +3 -3
- package/src/extension/team-tool/explain.ts +1 -1
- package/src/extension/team-tool/handle-schedule.ts +40 -0
- package/src/extension/team-tool/inspect.ts +3 -3
- package/src/extension/team-tool/lifecycle-actions.ts +4 -4
- package/src/extension/team-tool/respond.ts +2 -2
- package/src/extension/team-tool/run.ts +64 -14
- package/src/extension/team-tool/status.ts +1 -1
- package/src/extension/team-tool-types.ts +2 -0
- package/src/extension/team-tool.ts +173 -46
- package/src/hooks/registry.ts +77 -13
- package/src/hooks/types.ts +9 -0
- package/src/plugins/plugin-define.ts +6 -0
- package/src/plugins/plugin-registry.ts +32 -0
- package/src/plugins/plugins/index.ts +3 -0
- package/src/plugins/plugins/nextjs.ts +19 -0
- package/src/plugins/plugins/vite.ts +14 -0
- package/src/plugins/plugins/vitest.ts +14 -0
- package/src/runtime/async-runner.ts +35 -7
- package/src/runtime/background-runner.ts +529 -144
- package/src/runtime/chain-parser.ts +5 -1
- package/src/runtime/chain-runner.ts +9 -3
- package/src/runtime/checkpoint.ts +262 -180
- package/src/runtime/child-pi.ts +164 -37
- package/src/runtime/crash-recovery.ts +25 -14
- package/src/runtime/crew-agent-records.ts +2 -0
- package/src/runtime/diagnostic-export.ts +1 -1
- package/src/runtime/dynamic-script-runner.ts +10 -7
- package/src/runtime/foreground-watchdog.ts +1 -1
- package/src/runtime/heartbeat-watcher.ts +19 -2
- package/src/runtime/intercom-bridge.ts +7 -0
- package/src/runtime/iteration-hooks.ts +1 -1
- package/src/runtime/manifest-cache.ts +4 -0
- package/src/runtime/orphan-worker-registry.ts +444 -0
- package/src/runtime/parent-guard.ts +70 -16
- package/src/runtime/pi-args.ts +437 -14
- package/src/runtime/pi-spawn.ts +1 -0
- package/src/runtime/post-checks.ts +1 -1
- package/src/runtime/retry-runner.ts +9 -3
- package/src/runtime/run-tracker.ts +38 -10
- package/src/runtime/sandbox.ts +17 -2
- package/src/runtime/scheduler.ts +25 -2
- package/src/runtime/skill-effectiveness.ts +105 -62
- package/src/runtime/skill-instructions.ts +110 -32
- package/src/runtime/stale-reconciler.ts +310 -69
- package/src/runtime/subagent-manager.ts +251 -57
- package/src/runtime/task-health.ts +76 -0
- package/src/runtime/task-id.ts +20 -0
- package/src/runtime/task-runner/live-executor.ts +1 -1
- package/src/runtime/task-runner/progress.ts +1 -1
- package/src/runtime/task-runner/state-helpers.ts +110 -6
- package/src/runtime/task-runner.ts +92 -70
- package/src/runtime/team-runner.ts +186 -20
- package/src/schema/team-tool-schema.ts +27 -9
- package/src/skills/discover-skills.ts +9 -3
- package/src/state/active-run-registry.ts +170 -38
- package/src/state/artifact-store.ts +25 -13
- package/src/state/atomic-write-v2.ts +86 -0
- package/src/state/atomic-write.ts +346 -55
- package/src/state/blob-store.ts +178 -10
- package/src/state/crew-init.ts +161 -28
- package/src/state/decision-ledger.ts +172 -111
- package/src/state/event-log-rotation.ts +82 -52
- package/src/state/event-log.ts +254 -70
- package/src/state/health-store.ts +71 -0
- package/src/state/locks.ts +102 -20
- package/src/state/mailbox.ts +45 -7
- package/src/state/observation-store.ts +4 -1
- package/src/state/run-graph.ts +141 -130
- package/src/state/run-metrics.ts +24 -8
- package/src/state/session-state-map.ts +51 -0
- package/src/state/state-store.ts +330 -43
- package/src/ui/live-run-sidebar.ts +1 -1
- package/src/ui/loaders.ts +4 -0
- package/src/ui/overlays/agent-picker-overlay.ts +1 -1
- package/src/ui/overlays/mailbox-detail-overlay.ts +1 -1
- package/src/ui/powerbar-publisher.ts +1 -1
- package/src/ui/run-action-dispatcher.ts +2 -2
- package/src/ui/run-snapshot-cache.ts +1 -1
- package/src/ui/status-colors.ts +5 -1
- package/src/ui/theme-adapter.ts +80 -1
- package/src/ui/tool-progress-formatter.ts +9 -5
- package/src/ui/tool-render.ts +4 -0
- package/src/ui/tool-renderers/brief-mode.ts +207 -0
- package/src/ui/tool-renderers/index.ts +627 -0
- package/src/ui/widget/index.ts +224 -0
- package/src/ui/widget/widget-formatters.ts +148 -0
- package/src/ui/widget/widget-model.ts +90 -0
- package/src/ui/widget/widget-renderer.ts +130 -0
- package/src/ui/widget/widget-types.ts +37 -0
- package/src/utils/env-filter.ts +66 -0
- package/src/utils/file-coalescer.ts +9 -1
- package/src/utils/guards.ts +110 -0
- package/src/utils/paths.ts +80 -5
- package/src/utils/redaction.ts +6 -1
- package/src/utils/safe-paths.ts +281 -10
- package/src/worktree/cleanup.ts +112 -24
- package/src/worktree/worktree-manager.ts +128 -15
- package/test-bugs-all.mjs +10 -6
- 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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
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/
|
|
6
|
-
import { updateCrewWidget } from "../../ui/
|
|
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 {
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|