pi-crew 0.1.32 → 0.1.34
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/docs/architecture.md +3 -3
- package/docs/research-phase8-operator-experience-plan.md +819 -0
- package/docs/research-phase9-observability-reliability-plan.md +1190 -0
- package/docs/research-ui-optimization-plan.md +480 -0
- package/package.json +1 -1
- package/schema.json +14 -0
- package/src/config/config.ts +69 -0
- package/src/config/defaults.ts +7 -0
- package/src/extension/autonomous-policy.ts +56 -2
- package/src/extension/notification-router.ts +116 -0
- package/src/extension/notification-sink.ts +51 -0
- package/src/extension/register.ts +133 -35
- package/src/extension/registration/commands.ts +110 -3
- package/src/extension/registration/team-tool.ts +5 -2
- package/src/extension/registration/viewers.ts +3 -1
- package/src/extension/team-recommendation.ts +16 -8
- package/src/runtime/child-pi.ts +1 -0
- package/src/runtime/diagnostic-export.ts +107 -0
- package/src/runtime/pi-spawn.ts +4 -1
- package/src/runtime/task-packet.ts +11 -2
- package/src/runtime/task-runner/prompt-builder.ts +3 -0
- package/src/schema/config-schema.ts +11 -0
- package/src/ui/crew-widget.ts +350 -285
- package/src/ui/dashboard-panes/agents-pane.ts +25 -0
- package/src/ui/dashboard-panes/health-pane.ts +30 -0
- package/src/ui/dashboard-panes/mailbox-pane.ts +10 -0
- package/src/ui/dashboard-panes/progress-pane.ts +14 -0
- package/src/ui/dashboard-panes/transcript-pane.ts +10 -0
- package/src/ui/heartbeat-aggregator.ts +53 -0
- package/src/ui/keybinding-map.ts +92 -0
- package/src/ui/live-run-sidebar.ts +20 -8
- package/src/ui/overlays/agent-picker-overlay.ts +57 -0
- package/src/ui/overlays/confirm-overlay.ts +58 -0
- package/src/ui/overlays/mailbox-compose-overlay.ts +144 -0
- package/src/ui/overlays/mailbox-compose-preview.ts +63 -0
- package/src/ui/overlays/mailbox-detail-overlay.ts +122 -0
- package/src/ui/pi-ui-compat.ts +57 -0
- package/src/ui/powerbar-publisher.ts +128 -94
- package/src/ui/render-scheduler.ts +103 -0
- package/src/ui/run-action-dispatcher.ts +107 -0
- package/src/ui/run-dashboard.ts +418 -372
- package/src/ui/run-snapshot-cache.ts +359 -0
- package/src/ui/snapshot-types.ts +47 -0
- package/src/ui/transcript-cache.ts +94 -0
- package/src/ui/transcript-viewer.ts +316 -302
|
@@ -7,16 +7,108 @@ import { loadRunManifestById } from "../../state/state-store.ts";
|
|
|
7
7
|
import type { TeamRunManifest } from "../../state/types.ts";
|
|
8
8
|
import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
|
|
9
9
|
import { AnimatedMascot } from "../../ui/mascot.ts";
|
|
10
|
+
import * as path from "node:path";
|
|
10
11
|
import { RunDashboard, type RunDashboardSelection } from "../../ui/run-dashboard.ts";
|
|
11
12
|
import { DurableTextViewer } from "../../ui/transcript-viewer.ts";
|
|
13
|
+
import { ConfirmOverlay, type ConfirmOptions } from "../../ui/overlays/confirm-overlay.ts";
|
|
14
|
+
import { MailboxDetailOverlay, type MailboxAction } from "../../ui/overlays/mailbox-detail-overlay.ts";
|
|
15
|
+
import { MailboxComposeOverlay, type MailboxComposeResult } from "../../ui/overlays/mailbox-compose-overlay.ts";
|
|
16
|
+
import { AgentPickerOverlay } from "../../ui/overlays/agent-picker-overlay.ts";
|
|
17
|
+
import { dispatchDiagnosticExport, dispatchHealthRecovery, dispatchKillStaleWorkers, dispatchMailboxAck, dispatchMailboxAckAll, dispatchMailboxCompose, dispatchMailboxNudge } from "../../ui/run-action-dispatcher.ts";
|
|
18
|
+
import { listRecentDiagnostic } from "../../runtime/diagnostic-export.ts";
|
|
12
19
|
import { commandText, notifyCommandResult, parseRunArgs, parseScalar, pushUnset, setNestedConfig } from "./command-utils.ts";
|
|
13
20
|
import { openTranscriptViewer, selectAgentTask } from "./viewers.ts";
|
|
14
21
|
import { printTimings, time } from "../../utils/timings.ts";
|
|
22
|
+
import { requestRenderTarget } from "../../ui/pi-ui-compat.ts";
|
|
23
|
+
import type { createRunSnapshotCache } from "../../ui/run-snapshot-cache.ts";
|
|
15
24
|
|
|
16
25
|
export interface RegisterTeamCommandsDeps {
|
|
17
26
|
startForegroundRun: (ctx: ExtensionContext, runner: (signal?: AbortSignal) => Promise<void>, runId?: string) => void;
|
|
18
27
|
openLiveSidebar: (ctx: ExtensionContext, runId: string) => void;
|
|
19
28
|
getManifestCache: (cwd: string) => { list(max?: number): TeamRunManifest[] };
|
|
29
|
+
getRunSnapshotCache?: (cwd: string) => ReturnType<typeof createRunSnapshotCache>;
|
|
30
|
+
dismissNotifications?: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function openConfirm(ctx: ExtensionCommandContext, options: ConfirmOptions): Promise<boolean> {
|
|
34
|
+
if (!ctx.hasUI) return false;
|
|
35
|
+
return await ctx.ui.custom<boolean>((_tui, theme, _keybindings, done) => new ConfirmOverlay(options, done, theme), { overlay: true, overlayOptions: { width: 64, maxHeight: "70%", anchor: "center" } });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function handleMailboxDashboardAction(ctx: ExtensionCommandContext, runId: string): Promise<void> {
|
|
39
|
+
if (!ctx.hasUI) return;
|
|
40
|
+
const action = await ctx.ui.custom<MailboxAction | undefined>((_tui, theme, _keybindings, done) => new MailboxDetailOverlay({ runId, cwd: ctx.cwd, done, theme }), { overlay: true, overlayOptions: { width: "90%", maxHeight: "85%", anchor: "center" } });
|
|
41
|
+
if (!action || action.type === "close") return;
|
|
42
|
+
let resultMessage: string | undefined;
|
|
43
|
+
let ok = true;
|
|
44
|
+
if (action.type === "ack") {
|
|
45
|
+
const result = await dispatchMailboxAck(ctx as ExtensionContext, runId, action.messageId);
|
|
46
|
+
ok = result.ok;
|
|
47
|
+
resultMessage = result.message;
|
|
48
|
+
} else if (action.type === "ackAll") {
|
|
49
|
+
const confirmed = await openConfirm(ctx, { title: "Acknowledge all unread messages?", body: "This cannot be undone. Y=ack all, N=cancel.", dangerLevel: "medium", defaultAction: "cancel" });
|
|
50
|
+
if (!confirmed) return;
|
|
51
|
+
const result = await dispatchMailboxAckAll(ctx as ExtensionContext, runId);
|
|
52
|
+
ok = result.ok;
|
|
53
|
+
resultMessage = result.message;
|
|
54
|
+
} else if (action.type === "compose") {
|
|
55
|
+
const compose = await ctx.ui.custom<MailboxComposeResult>((_tui, theme, _keybindings, done) => new MailboxComposeOverlay({ done, theme }), { overlay: true, overlayOptions: { width: "90%", maxHeight: "85%", anchor: "center" } });
|
|
56
|
+
if (compose.type === "cancel") return;
|
|
57
|
+
const result = await dispatchMailboxCompose(ctx as ExtensionContext, runId, compose.payload);
|
|
58
|
+
ok = result.ok;
|
|
59
|
+
resultMessage = result.message;
|
|
60
|
+
} else if (action.type === "nudge") {
|
|
61
|
+
let agentId = action.agentId;
|
|
62
|
+
if (!agentId) {
|
|
63
|
+
const picked = await ctx.ui.custom<{ agentId: string } | undefined>((_tui, theme, _keybindings, done) => new AgentPickerOverlay({ cwd: ctx.cwd, runId, done, theme }), { overlay: true, overlayOptions: { width: 72, maxHeight: "75%", anchor: "center" } });
|
|
64
|
+
agentId = picked?.agentId;
|
|
65
|
+
}
|
|
66
|
+
if (!agentId) return;
|
|
67
|
+
const result = await dispatchMailboxNudge(ctx as ExtensionContext, runId, agentId, "Please report your current status, blocker, or smallest next step.");
|
|
68
|
+
ok = result.ok;
|
|
69
|
+
resultMessage = result.message;
|
|
70
|
+
}
|
|
71
|
+
depsNotify(ctx, resultMessage ?? "Mailbox action complete.", ok ? "info" : "error");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function depsNotify(ctx: ExtensionCommandContext, message: string, level: "info" | "warning" | "error"): void {
|
|
75
|
+
ctx.ui.notify(message, level);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function handleHealthDashboardAction(ctx: ExtensionCommandContext, selection: RunDashboardSelection): Promise<void> {
|
|
79
|
+
const loaded = loadRunManifestById(ctx.cwd, selection.runId);
|
|
80
|
+
if (!loaded) {
|
|
81
|
+
depsNotify(ctx, `Run '${selection.runId}' not found.`, "error");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (selection.action === "health-recovery") {
|
|
85
|
+
if (loaded.manifest.async) {
|
|
86
|
+
depsNotify(ctx, "Recovery is only available for foreground runs.", "warning");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const confirmed = await openConfirm(ctx, { title: "Interrupt foreground run?", body: "Tasks may be marked failed. Y=interrupt, N=cancel.", dangerLevel: "high", defaultAction: "cancel" });
|
|
90
|
+
if (!confirmed) return;
|
|
91
|
+
const result = await dispatchHealthRecovery(ctx as ExtensionContext, selection.runId);
|
|
92
|
+
depsNotify(ctx, result.message, result.ok ? "info" : "error");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (selection.action === "health-kill-stale") {
|
|
96
|
+
const confirmed = await openConfirm(ctx, { title: "Mark stale workers dead?", body: "This updates worker heartbeat state. Y=mark dead, N=cancel.", dangerLevel: "medium", defaultAction: "cancel" });
|
|
97
|
+
if (!confirmed) return;
|
|
98
|
+
const result = await dispatchKillStaleWorkers(ctx as ExtensionContext, selection.runId);
|
|
99
|
+
depsNotify(ctx, result.message, result.ok ? "info" : "error");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (selection.action === "health-diagnostic-export") {
|
|
103
|
+
const diagDir = path.join(loaded.manifest.artifactsRoot, "diagnostic");
|
|
104
|
+
const recent = listRecentDiagnostic(diagDir, 60_000);
|
|
105
|
+
if (recent) {
|
|
106
|
+
const confirmed = await openConfirm(ctx, { title: "Recent diagnostic exists", body: `File ${recent} was created <1min ago. Export another diagnostic?`, defaultAction: "cancel" });
|
|
107
|
+
if (!confirmed) return;
|
|
108
|
+
}
|
|
109
|
+
const result = await dispatchDiagnosticExport(ctx as ExtensionContext, selection.runId);
|
|
110
|
+
depsNotify(ctx, result.message, result.ok ? "info" : "error");
|
|
111
|
+
}
|
|
20
112
|
}
|
|
21
113
|
|
|
22
114
|
export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommandsDeps): void {
|
|
@@ -133,11 +225,26 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
|
|
|
133
225
|
const uiConfig = loadConfig(ctx.cwd).config.ui;
|
|
134
226
|
const rightPanel = uiConfig?.dashboardPlacement !== "center";
|
|
135
227
|
const width = rightPanel ? Math.min(90, Math.max(40, uiConfig?.dashboardWidth ?? 56)) : "90%";
|
|
136
|
-
const selection = await ctx.ui.custom<RunDashboardSelection | undefined>((_tui, theme, _keybindings, done) => new RunDashboard(runs, done, theme, { placement: rightPanel ? "right" : "center", showModel: uiConfig?.showModel, showTokens: uiConfig?.showTokens, showTools: uiConfig?.showTools }), { overlay: true, overlayOptions: rightPanel ? { width, minWidth: 40, maxHeight: "100%", anchor: "top-right", offsetX: 0, offsetY: 0, margin: { top: 0, right: 0, bottom: 0, left: 0 } } : { width, maxHeight: "90%", anchor: "center", margin: 2 } });
|
|
228
|
+
const selection = await ctx.ui.custom<RunDashboardSelection | undefined>((_tui, theme, _keybindings, done) => new RunDashboard(runs, done, theme, { placement: rightPanel ? "right" : "center", showModel: uiConfig?.showModel, showTokens: uiConfig?.showTokens, showTools: uiConfig?.showTools, snapshotCache: deps.getRunSnapshotCache?.(ctx.cwd), runProvider: () => deps.getManifestCache(ctx.cwd).list(50) }), { overlay: true, overlayOptions: rightPanel ? { width, minWidth: 40, maxHeight: "100%", anchor: "top-right", offsetX: 0, offsetY: 0, margin: { top: 0, right: 0, bottom: 0, left: 0 } } : { width, maxHeight: "90%", anchor: "center", margin: 2 } });
|
|
137
229
|
if (!selection) return;
|
|
138
230
|
if (selection.action === "reload") continue;
|
|
231
|
+
if (selection.action === "notifications-dismiss") {
|
|
232
|
+
deps.dismissNotifications?.();
|
|
233
|
+
ctx.ui.notify("pi-crew notifications dismissed.", "info");
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (selection.action === "mailbox-detail") {
|
|
237
|
+
await handleMailboxDashboardAction(ctx, selection.runId);
|
|
238
|
+
deps.getRunSnapshotCache?.(ctx.cwd).invalidate(selection.runId);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (selection.action === "health-recovery" || selection.action === "health-kill-stale" || selection.action === "health-diagnostic-export") {
|
|
242
|
+
await handleHealthDashboardAction(ctx, selection);
|
|
243
|
+
deps.getRunSnapshotCache?.(ctx.cwd).invalidate(selection.runId);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
139
246
|
if (selection.action === "agent-transcript" && await openTranscriptViewer(ctx, selection.runId)) continue;
|
|
140
|
-
const result = selection.action === "api" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-manifest" } }, ctx) : selection.action === "agents" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "agent-dashboard" } }, ctx) : selection.action === "agent-events" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-events", limit: 50 } }, ctx) : selection.action === "agent-output" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-output", maxBytes: 32_000 } }, ctx) : selection.action === "agent-transcript" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-transcript" } }, ctx) : await handleTeamTool({ action: selection.action, runId: selection.runId }, ctx);
|
|
247
|
+
const result = selection.action === "api" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-manifest" } }, ctx) : selection.action === "agents" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "agent-dashboard" } }, ctx) : selection.action === "mailbox" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-mailbox" } }, ctx) : selection.action === "agent-events" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-events", limit: 50 } }, ctx) : selection.action === "agent-output" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-output", maxBytes: 32_000 } }, ctx) : selection.action === "agent-transcript" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-transcript" } }, ctx) : await handleTeamTool({ action: selection.action, runId: selection.runId }, ctx);
|
|
141
248
|
await notifyCommandResult(ctx, commandText(result));
|
|
142
249
|
return;
|
|
143
250
|
}
|
|
@@ -151,7 +258,7 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
|
|
|
151
258
|
const effectArg = tokens.find((t) => ["random", "none", "typewriter", "scanline", "rain", "fade", "crt", "glitch", "dissolve"].includes(t));
|
|
152
259
|
const style = (styleArg as "cat" | "armin" | undefined) ?? uiConfig?.mascotStyle ?? "cat";
|
|
153
260
|
const effect = (effectArg as "random" | "none" | "typewriter" | "scanline" | "rain" | "fade" | "crt" | "glitch" | "dissolve" | undefined) ?? uiConfig?.mascotEffect ?? "random";
|
|
154
|
-
await ctx.ui.custom<undefined>((tui, theme, _keybindings, done) => new AnimatedMascot(theme, () => done(undefined), { frameIntervalMs: style === "armin" ? 33 : 180, autoCloseMs: 7000, requestRender: () => (tui
|
|
261
|
+
await ctx.ui.custom<undefined>((tui, theme, _keybindings, done) => new AnimatedMascot(theme, () => done(undefined), { frameIntervalMs: style === "armin" ? 33 : 180, autoCloseMs: 7000, requestRender: () => requestRenderTarget(tui), style, effect }), { overlay: true, overlayOptions: { width: style === "armin" ? 48 : 62, maxHeight: "85%", anchor: "center" } });
|
|
155
262
|
} });
|
|
156
263
|
|
|
157
264
|
pi.registerCommand("team-init", { description: "Initialize project-local pi-crew directories and gitignore entries", handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
@@ -5,6 +5,7 @@ import type { CrewWidgetState } from "../../ui/crew-widget.ts";
|
|
|
5
5
|
import { updateCrewWidget } from "../../ui/crew-widget.ts";
|
|
6
6
|
import { updatePiCrewPowerbar } from "../../ui/powerbar-publisher.ts";
|
|
7
7
|
import type { createManifestCache } from "../../runtime/manifest-cache.ts";
|
|
8
|
+
import type { createRunSnapshotCache } from "../../ui/run-snapshot-cache.ts";
|
|
8
9
|
import { handleTeamTool } from "../team-tool.ts";
|
|
9
10
|
|
|
10
11
|
export interface RegisterTeamToolDeps {
|
|
@@ -12,6 +13,7 @@ export interface RegisterTeamToolDeps {
|
|
|
12
13
|
startForegroundRun: (ctx: ExtensionContext, runner: (signal?: AbortSignal) => Promise<void>, runId?: string) => void;
|
|
13
14
|
openLiveSidebar: (ctx: ExtensionContext, runId: string) => void;
|
|
14
15
|
getManifestCache: (cwd: string) => ReturnType<typeof createManifestCache>;
|
|
16
|
+
getRunSnapshotCache?: (cwd: string) => ReturnType<typeof createRunSnapshotCache>;
|
|
15
17
|
widgetState: CrewWidgetState;
|
|
16
18
|
}
|
|
17
19
|
|
|
@@ -48,8 +50,9 @@ export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps):
|
|
|
48
50
|
}
|
|
49
51
|
const config = loadConfig(ctx.cwd).config.ui;
|
|
50
52
|
const cache = deps.getManifestCache(ctx.cwd);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
const snapshotCache = deps.getRunSnapshotCache?.(ctx.cwd);
|
|
54
|
+
updateCrewWidget(ctx, deps.widgetState, config, cache, snapshotCache);
|
|
55
|
+
updatePiCrewPowerbar(pi.events, ctx.cwd, config, cache, snapshotCache, ctx);
|
|
53
56
|
return output;
|
|
54
57
|
} finally {
|
|
55
58
|
signal?.removeEventListener("abort", abort);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { loadRunManifestById } from "../../state/state-store.ts";
|
|
3
3
|
import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
|
|
4
|
+
import { loadConfig } from "../../config/config.ts";
|
|
4
5
|
import { DurableTranscriptViewer } from "../../ui/transcript-viewer.ts";
|
|
5
6
|
|
|
6
7
|
export async function selectAgentTask(ctx: ExtensionCommandContext, runId: string | undefined, taskId?: string): Promise<{ runId: string; taskId?: string } | undefined> {
|
|
@@ -24,7 +25,8 @@ export async function openTranscriptViewer(ctx: ExtensionCommandContext, initial
|
|
|
24
25
|
if (!runId || !ctx.hasUI) return false;
|
|
25
26
|
const loaded = loadRunManifestById(ctx.cwd, runId);
|
|
26
27
|
if (!loaded) return false;
|
|
27
|
-
|
|
28
|
+
const uiConfig = loadConfig(ctx.cwd).config.ui;
|
|
29
|
+
await ctx.ui.custom<undefined>((_tui, theme, _keybindings, done) => new DurableTranscriptViewer(loaded.manifest, theme, done, taskId, { maxTailBytes: uiConfig?.transcriptTailBytes }), {
|
|
28
30
|
overlay: true,
|
|
29
31
|
overlayOptions: { width: "90%", maxHeight: "85%", anchor: "center" },
|
|
30
32
|
});
|
|
@@ -26,8 +26,8 @@ const REVIEW_TERMS = ["review", "audit", "security", "vulnerability", "diff", "p
|
|
|
26
26
|
const RESEARCH_TERMS = ["research", "investigate", "compare", "analyze", "document", "docs", "explain", "architecture", "đọc sâu", "source", "projects"];
|
|
27
27
|
const PARALLEL_RESEARCH_RE = /(?:đọc sâu|deep read|deep research|source audit|multiple projects|các project|pi-\*|source\/|@source)/i;
|
|
28
28
|
const FAST_FIX_TERMS = ["quick fix", "fast-fix", "small bug", "typo", "one-line", "minor", "lint"];
|
|
29
|
-
const IMPLEMENTATION_TERMS = ["implement", "refactor", "migrate", "feature", "tests", "test", "integration", "upgrade", "build", "create", "add"];
|
|
30
|
-
const RISKY_TERMS = ["migration", "refactor", "large", "multiple", "parallel", "concurrent", "risky", "critical"];
|
|
29
|
+
const IMPLEMENTATION_TERMS = ["implement", "refactor", "migrate", "feature", "tests", "test", "integration", "upgrade", "build", "create", "add", "fix", "update", "sửa", "thêm", "cập nhật", "kiểm thử"];
|
|
30
|
+
const RISKY_TERMS = ["migration", "refactor", "large", "multiple", "parallel", "concurrent", "risky", "critical", "nhiều file", "nhiều task"];
|
|
31
31
|
const NUMBERED_LINE_RE = /^\s*\d+[.)]\s+(.+)$/;
|
|
32
32
|
const BULLETED_LINE_RE = /^\s*[-*•]\s+(.+)$/;
|
|
33
33
|
const CONJUNCTION_SPLIT_RE = /\s+(?:and|,\s*and|,)\s+/i;
|
|
@@ -67,12 +67,14 @@ export function decomposeGoal(goal: string): { strategy: DecompositionStrategy;
|
|
|
67
67
|
const subtask = makeSubtask(goal);
|
|
68
68
|
return { strategy: "atomic", subtasks: [subtask], fanout: 1 };
|
|
69
69
|
}
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
const numberedLines = lines.map((line) => line.match(NUMBERED_LINE_RE)?.[1]).filter((line): line is string => line !== undefined);
|
|
71
|
+
if (numberedLines.length >= 2 && numberedLines.length >= lines.length - 1) {
|
|
72
|
+
const subtasks = numberedLines.map((line) => makeSubtask(line));
|
|
72
73
|
return { strategy: "numbered", subtasks, fanout: subtasks.length };
|
|
73
74
|
}
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
const bulletedLines = lines.map((line) => line.match(BULLETED_LINE_RE)?.[1]).filter((line): line is string => line !== undefined);
|
|
76
|
+
if (bulletedLines.length >= 2 && bulletedLines.length >= lines.length - 1) {
|
|
77
|
+
const subtasks = bulletedLines.map((line) => makeSubtask(line));
|
|
76
78
|
return { strategy: "bulleted", subtasks, fanout: subtasks.length };
|
|
77
79
|
}
|
|
78
80
|
if (lines.length === 1) {
|
|
@@ -94,6 +96,7 @@ function metadataMatches(goal: string, values: string[] | undefined): string[] {
|
|
|
94
96
|
export function recommendTeam(goal: string, config: PiTeamsAutonomousConfig = {}, resources?: { teams?: TeamConfig[]; agents?: AgentConfig[] }): TeamRecommendation {
|
|
95
97
|
const normalized = goal.toLowerCase();
|
|
96
98
|
const intents = detectTeamIntent(goal, config);
|
|
99
|
+
const decomposition = decomposeGoal(goal);
|
|
97
100
|
const reasons: string[] = [];
|
|
98
101
|
let team: TeamRecommendation["team"] = "default";
|
|
99
102
|
let workflow: TeamRecommendation["workflow"] = "default";
|
|
@@ -138,10 +141,15 @@ export function recommendTeam(goal: string, config: PiTeamsAutonomousConfig = {}
|
|
|
138
141
|
workflow = "fast-fix";
|
|
139
142
|
confidence = "high";
|
|
140
143
|
reasons.push(`Small fix terms detected: ${fastFixMatches.join(", ") || "fast-fix intent"}.`);
|
|
144
|
+
} else if (intents.includes("taskList")) {
|
|
145
|
+
team = "implementation";
|
|
146
|
+
workflow = "implementation";
|
|
147
|
+
confidence = "high";
|
|
148
|
+
reasons.push(`Actionable multi-item task list detected (${decomposition.fanout} bullet${decomposition.fanout === 1 ? "" : "s"}); use coordinated implementation planning.`);
|
|
141
149
|
} else if (intents.includes("implementation") || implementationMatches.length > 0) {
|
|
142
150
|
team = "implementation";
|
|
143
151
|
workflow = "implementation";
|
|
144
|
-
confidence = implementationMatches.length >= 2 || riskyMatches.length > 0 ? "high" : "medium";
|
|
152
|
+
confidence = implementationMatches.length >= 2 || riskyMatches.length > 0 || decomposition.fanout >= 2 ? "high" : "medium";
|
|
145
153
|
reasons.push(`Implementation terms detected: ${implementationMatches.join(", ") || "implementation intent"}.`);
|
|
146
154
|
} else {
|
|
147
155
|
action = "plan";
|
|
@@ -149,7 +157,7 @@ export function recommendTeam(goal: string, config: PiTeamsAutonomousConfig = {}
|
|
|
149
157
|
reasons.push("No strong team-specific intent detected; start with planning/default discovery.");
|
|
150
158
|
}
|
|
151
159
|
|
|
152
|
-
|
|
160
|
+
|
|
153
161
|
if (decomposition.strategy !== "atomic") reasons.push(`Goal decomposes into ${decomposition.subtasks.length} subtasks using ${decomposition.strategy} parsing.`);
|
|
154
162
|
const async = config.preferAsyncForLongTasks === true && (wordCount(goal) > 24 || riskyMatches.length > 0 || implementationMatches.length >= 2 || decomposition.fanout >= 3);
|
|
155
163
|
const workspaceMode = config.allowWorktreeSuggestion === false ? "single" : (riskyMatches.length > 0 && team === "implementation" ? "worktree" : "single");
|
package/src/runtime/child-pi.ts
CHANGED
|
@@ -399,6 +399,7 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
399
399
|
|
|
400
400
|
input.signal?.addEventListener("abort", abort, { once: true });
|
|
401
401
|
child.stdout?.on("data", (chunk: Buffer) => {
|
|
402
|
+
restartNoResponseTimer();
|
|
402
403
|
lineObserver.observe(chunk.toString("utf-8"));
|
|
403
404
|
});
|
|
404
405
|
child.stderr?.on("data", (chunk: Buffer) => {
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { readCrewAgents } from "./crew-agent-records.ts";
|
|
5
|
+
import { readEvents, type TeamEvent } from "../state/event-log.ts";
|
|
6
|
+
import { loadRunManifestById } from "../state/state-store.ts";
|
|
7
|
+
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
8
|
+
import { summarizeHeartbeats, type HeartbeatSummary } from "../ui/heartbeat-aggregator.ts";
|
|
9
|
+
import type { RunUiSnapshot } from "../ui/snapshot-types.ts";
|
|
10
|
+
|
|
11
|
+
export interface DiagnosticReport {
|
|
12
|
+
runId: string;
|
|
13
|
+
exportedAt: string;
|
|
14
|
+
manifest: TeamRunManifest;
|
|
15
|
+
tasks: TeamTaskState[];
|
|
16
|
+
recentEvents: TeamEvent[];
|
|
17
|
+
heartbeat: HeartbeatSummary;
|
|
18
|
+
agents: unknown[];
|
|
19
|
+
envRedacted: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const SECRET_KEY_PATTERN = /(token|key|password|secret|credential|auth)/i;
|
|
23
|
+
|
|
24
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
25
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function redactSecrets(value: unknown, keyName = ""): unknown {
|
|
29
|
+
if (SECRET_KEY_PATTERN.test(keyName)) return "***";
|
|
30
|
+
if (typeof value === "string") return value.replace(/((?:token|key|password|secret|credential|auth)[\w.-]*\s*[=:]\s*)[^\s,;]+/gi, "$1***");
|
|
31
|
+
if (Array.isArray(value)) return value.map((item) => redactSecrets(item));
|
|
32
|
+
if (isRecord(value)) {
|
|
33
|
+
const output: Record<string, unknown> = {};
|
|
34
|
+
for (const [key, entry] of Object.entries(value)) output[key] = redactSecrets(entry, key);
|
|
35
|
+
return output;
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function envRedacted(): Record<string, string> {
|
|
41
|
+
const output: Record<string, string> = {};
|
|
42
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
43
|
+
if (SECRET_KEY_PATTERN.test(key)) output[key] = "***";
|
|
44
|
+
else if (typeof value === "string") output[key] = value;
|
|
45
|
+
}
|
|
46
|
+
return output;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildSnapshot(manifest: TeamRunManifest, tasks: TeamTaskState[]): RunUiSnapshot {
|
|
50
|
+
const agents = readCrewAgents(manifest);
|
|
51
|
+
return {
|
|
52
|
+
runId: manifest.runId,
|
|
53
|
+
cwd: manifest.cwd,
|
|
54
|
+
fetchedAt: Date.now(),
|
|
55
|
+
signature: `${manifest.runId}:${manifest.updatedAt}`,
|
|
56
|
+
manifest,
|
|
57
|
+
tasks,
|
|
58
|
+
agents,
|
|
59
|
+
progress: {
|
|
60
|
+
total: tasks.length,
|
|
61
|
+
completed: tasks.filter((task) => task.status === "completed").length,
|
|
62
|
+
running: tasks.filter((task) => task.status === "running").length,
|
|
63
|
+
failed: tasks.filter((task) => task.status === "failed").length,
|
|
64
|
+
queued: tasks.filter((task) => task.status === "queued").length,
|
|
65
|
+
},
|
|
66
|
+
usage: { tokensIn: 0, tokensOut: 0, toolUses: 0 },
|
|
67
|
+
mailbox: { inboxUnread: 0, outboxPending: 0, needsAttention: 0 },
|
|
68
|
+
recentEvents: [],
|
|
69
|
+
recentOutputLines: [],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function exportDiagnostic(ctx: Pick<ExtensionContext, "cwd">, runId: string): Promise<{ path: string; report: DiagnosticReport }> {
|
|
74
|
+
const loaded = loadRunManifestById(ctx.cwd, runId);
|
|
75
|
+
if (!loaded) throw new Error(`Run '${runId}' not found.`);
|
|
76
|
+
const exportedAt = new Date().toISOString();
|
|
77
|
+
const safeTimestamp = exportedAt.replace(/[:.]/g, "-");
|
|
78
|
+
const recentEvents = readEvents(loaded.manifest.eventsPath).slice(-200);
|
|
79
|
+
const report: DiagnosticReport = {
|
|
80
|
+
runId,
|
|
81
|
+
exportedAt,
|
|
82
|
+
manifest: redactSecrets(loaded.manifest) as TeamRunManifest,
|
|
83
|
+
tasks: redactSecrets(loaded.tasks) as TeamTaskState[],
|
|
84
|
+
recentEvents: redactSecrets(recentEvents) as TeamEvent[],
|
|
85
|
+
heartbeat: summarizeHeartbeats(buildSnapshot(loaded.manifest, loaded.tasks)),
|
|
86
|
+
agents: redactSecrets(readCrewAgents(loaded.manifest)) as unknown[],
|
|
87
|
+
envRedacted: envRedacted(),
|
|
88
|
+
};
|
|
89
|
+
const dir = path.join(loaded.manifest.artifactsRoot, "diagnostic");
|
|
90
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
91
|
+
const filePath = path.join(dir, `diagnostic-${safeTimestamp}.json`);
|
|
92
|
+
fs.writeFileSync(filePath, `${JSON.stringify(report, null, 2)}\n`, "utf-8");
|
|
93
|
+
return { path: filePath, report };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function listRecentDiagnostic(dir: string, windowMs: number, now = Date.now()): string | undefined {
|
|
97
|
+
try {
|
|
98
|
+
if (!fs.existsSync(dir)) return undefined;
|
|
99
|
+
return fs.readdirSync(dir)
|
|
100
|
+
.filter((file) => file.startsWith("diagnostic-") && file.endsWith(".json"))
|
|
101
|
+
.map((file) => ({ file, mtimeMs: fs.statSync(path.join(dir, file)).mtimeMs }))
|
|
102
|
+
.filter((entry) => now - entry.mtimeMs < windowMs)
|
|
103
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs)[0]?.file;
|
|
104
|
+
} catch {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
}
|
package/src/runtime/pi-spawn.ts
CHANGED
|
@@ -87,7 +87,10 @@ function resolvePiCliScript(): string | undefined {
|
|
|
87
87
|
|
|
88
88
|
export function getPiSpawnCommand(args: string[]): PiSpawnCommand {
|
|
89
89
|
const explicit = process.env.PI_TEAMS_PI_BIN?.trim();
|
|
90
|
-
if (explicit && fs.existsSync(explicit)
|
|
90
|
+
if (explicit && fs.existsSync(explicit)) {
|
|
91
|
+
if (isRunnableNodeScript(explicit)) return { command: process.execPath, args: [explicit, ...args] };
|
|
92
|
+
return { command: explicit, args };
|
|
93
|
+
}
|
|
91
94
|
if (process.platform === "win32") {
|
|
92
95
|
const script = resolvePiCliScript();
|
|
93
96
|
if (script) return { command: process.execPath, args: [script, ...args] };
|
|
@@ -43,12 +43,13 @@ export function buildTaskPacket(input: BuildTaskPacketInput): TaskPacket {
|
|
|
43
43
|
branchPolicy: input.manifest.workspaceMode === "worktree" ? "Use the assigned task worktree and avoid modifying the leader checkout." : "Use the current checkout; do not create branches unless explicitly requested.",
|
|
44
44
|
acceptanceTests: [],
|
|
45
45
|
commitPolicy: "Do not commit unless explicitly requested by the user or workflow.",
|
|
46
|
-
reportingContract: "Report changed files, verification evidence, blockers, and next recommended action.",
|
|
47
|
-
escalationPolicy: "Stop and report if scope is ambiguous, destructive action is needed, permissions are missing,
|
|
46
|
+
reportingContract: "Report intended/changed files, verification evidence, blockers, conflict risks, and next recommended action.",
|
|
47
|
+
escalationPolicy: "Stop and report if scope is ambiguous, destructive action is needed, permissions are missing, verification cannot be completed, or edits may overlap with another worker/task.",
|
|
48
48
|
constraints: [
|
|
49
49
|
"Stay within the assigned task scope.",
|
|
50
50
|
"Do not claim completion without verification evidence.",
|
|
51
51
|
"Use mailbox/API state for coordination when available.",
|
|
52
|
+
"Do not make overlapping edits to the same file/symbol without explicit leader sequencing or ownership guidance.",
|
|
52
53
|
],
|
|
53
54
|
expectedArtifacts: ["prompt", "result", "verification"],
|
|
54
55
|
verification: defaultVerificationContract(input.step),
|
|
@@ -66,6 +67,14 @@ export function validateTaskPacket(packet: TaskPacket): TaskPacketValidationResu
|
|
|
66
67
|
if ((packet.scope === "module" || packet.scope === "single_file" || packet.scope === "custom") && !packet.scopePath?.trim()) {
|
|
67
68
|
errors.push(`scopePath is required for scope '${packet.scope}'`);
|
|
68
69
|
}
|
|
70
|
+
if (packet.constraints.length === 0) errors.push("constraints must contain at least one entry");
|
|
71
|
+
for (const [index, constraint] of packet.constraints.entries()) {
|
|
72
|
+
if (!constraint.trim()) errors.push(`constraints contains an empty value at index ${index}`);
|
|
73
|
+
}
|
|
74
|
+
if (packet.expectedArtifacts.length === 0) errors.push("expectedArtifacts must contain at least one entry");
|
|
75
|
+
for (const [index, artifact] of packet.expectedArtifacts.entries()) {
|
|
76
|
+
if (!artifact.trim()) errors.push(`expectedArtifacts contains an empty value at index ${index}`);
|
|
77
|
+
}
|
|
69
78
|
for (const [index, test] of packet.acceptanceTests.entries()) {
|
|
70
79
|
if (!test.trim()) errors.push(`acceptanceTests contains an empty value at index ${index}`);
|
|
71
80
|
}
|
|
@@ -23,6 +23,9 @@ export function coordinationBridgeInstructions(task: TeamTaskState): string {
|
|
|
23
23
|
`Mailbox target for this task: ${task.id}`,
|
|
24
24
|
"Use the run mailbox contract for coordination with the leader/orchestrator:",
|
|
25
25
|
"- If blocked or uncertain, report the blocker in your final result and, when mailbox tools/API are available, send an inbox/outbox message addressed to the leader.",
|
|
26
|
+
"- Ask the leader before editing when scope is ambiguous, requirements conflict, destructive action is needed, or you discover likely overlap with another task.",
|
|
27
|
+
"- Before making non-trivial edits, state intended changed files in your notes/result; if another worker may touch the same file/symbol, pause and request sequencing/ownership guidance.",
|
|
28
|
+
"- Do not resolve cross-worker conflicts silently. Escalate via mailbox/result with: file/symbol, conflicting task if known, proposed owner, and safest next step.",
|
|
26
29
|
"- If nudged, answer with current status, blocker, or smallest next step.",
|
|
27
30
|
"- Treat inherited/dependency context as reference-only; do not continue the parent conversation directly.",
|
|
28
31
|
"- Completion handoff should include: DONE/FAILED, summary, changed/read files, verification evidence, and remaining risks.",
|
|
@@ -72,6 +72,15 @@ export const PiTeamsTelemetryConfigSchema = Type.Object({
|
|
|
72
72
|
enabled: Type.Optional(Type.Boolean()),
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
+
export const PiTeamsNotificationsConfigSchema = Type.Object({
|
|
76
|
+
enabled: Type.Optional(Type.Boolean()),
|
|
77
|
+
severityFilter: Type.Optional(Type.Array(Type.Union([Type.Literal("info"), Type.Literal("warning"), Type.Literal("error"), Type.Literal("critical")]))),
|
|
78
|
+
dedupWindowMs: Type.Optional(Type.Integer({ minimum: 1000 })),
|
|
79
|
+
batchWindowMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
80
|
+
quietHours: Type.Optional(Type.String({ pattern: "^\\d{2}:\\d{2}-\\d{2}:\\d{2}$" })),
|
|
81
|
+
sinkRetentionDays: Type.Optional(Type.Integer({ minimum: 1, maximum: 90 })),
|
|
82
|
+
});
|
|
83
|
+
|
|
75
84
|
export const PiTeamsUiConfigSchema = Type.Object({
|
|
76
85
|
widgetPlacement: Type.Optional(Type.Union([Type.Literal("aboveEditor"), Type.Literal("belowEditor")])),
|
|
77
86
|
widgetMaxLines: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
@@ -84,6 +93,7 @@ export const PiTeamsUiConfigSchema = Type.Object({
|
|
|
84
93
|
showModel: Type.Optional(Type.Boolean()),
|
|
85
94
|
showTokens: Type.Optional(Type.Boolean()),
|
|
86
95
|
showTools: Type.Optional(Type.Boolean()),
|
|
96
|
+
transcriptTailBytes: Type.Optional(Type.Number({ minimum: 1024 })),
|
|
87
97
|
mascotStyle: Type.Optional(Type.Union([Type.Literal("cat"), Type.Literal("armin")])),
|
|
88
98
|
mascotEffect: Type.Optional(Type.Union([Type.Literal("random"), Type.Literal("none"), Type.Literal("typewriter"), Type.Literal("scanline"), Type.Literal("rain"), Type.Literal("fade"), Type.Literal("crt"), Type.Literal("glitch"), Type.Literal("dissolve")])),
|
|
89
99
|
});
|
|
@@ -101,5 +111,6 @@ export const PiTeamsConfigSchema = Type.Object({
|
|
|
101
111
|
agents: Type.Optional(PiTeamsAgentsConfigSchema),
|
|
102
112
|
tools: Type.Optional(PiTeamsToolsConfigSchema),
|
|
103
113
|
telemetry: Type.Optional(PiTeamsTelemetryConfigSchema),
|
|
114
|
+
notifications: Type.Optional(PiTeamsNotificationsConfigSchema),
|
|
104
115
|
ui: Type.Optional(PiTeamsUiConfigSchema),
|
|
105
116
|
});
|