pi-crew 0.3.6 → 0.3.8
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 +17 -0
- package/package.json +1 -1
- package/src/agents/discover-agents.ts +2 -1
- package/src/config/config.ts +760 -229
- package/src/config/types.ts +34 -5
- package/src/extension/help.ts +1 -0
- package/src/extension/management.ts +2 -1
- package/src/extension/register.ts +1176 -255
- package/src/extension/registration/commands.ts +15 -2
- package/src/extension/registration/team-tool.ts +1 -1
- package/src/extension/session-summary.ts +11 -1
- package/src/extension/team-tool/api.ts +4 -1
- package/src/extension/team-tool/cache-control.ts +23 -0
- package/src/extension/team-tool/cancel.ts +27 -16
- package/src/extension/team-tool/context.ts +2 -0
- package/src/extension/team-tool/handle-settings.ts +2 -0
- package/src/extension/team-tool/health-monitor.ts +563 -0
- package/src/extension/team-tool/inspect.ts +10 -3
- package/src/extension/team-tool/lifecycle-actions.ts +12 -5
- package/src/extension/team-tool/respond.ts +6 -3
- package/src/extension/team-tool/status.ts +4 -1
- package/src/extension/team-tool-types.ts +2 -0
- package/src/extension/team-tool.ts +901 -177
- package/src/runtime/adaptive-plan.ts +1 -1
- package/src/runtime/child-pi.ts +15 -2
- package/src/runtime/crash-recovery.ts +30 -0
- package/src/runtime/foreground-watchdog.ts +129 -0
- package/src/runtime/manifest-cache.ts +4 -2
- package/src/runtime/pi-args.ts +3 -2
- package/src/runtime/run-tracker.ts +11 -0
- package/src/runtime/runtime-policy.ts +15 -2
- package/src/runtime/skill-instructions.ts +11 -0
- package/src/runtime/stale-reconciler.ts +322 -18
- package/src/runtime/task-runner.ts +8 -1
- package/src/schema/config-schema.ts +1 -0
- package/src/schema/team-tool-schema.ts +204 -76
- package/src/state/atomic-write.ts +2 -2
- package/src/state/locks.ts +19 -0
- package/src/state/mailbox.ts +22 -5
- package/src/state/state-store.ts +13 -3
- package/src/teams/discover-teams.ts +2 -1
- package/src/ui/run-event-bus.ts +2 -1
- package/src/ui/settings-overlay.ts +2 -0
- package/src/workflows/discover-workflows.ts +5 -1
|
@@ -221,11 +221,24 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
|
|
|
221
221
|
] as const) {
|
|
222
222
|
pi.registerCommand(name, { description, handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
223
223
|
const runId = args.trim() || undefined;
|
|
224
|
-
const result = await handleTeamTool({ action, runId }, teamCommandContext(ctx));
|
|
224
|
+
const result = await handleTeamTool({ action, runId }, { ...teamCommandContext(ctx), getRunSnapshotCache: deps.getRunSnapshotCache });
|
|
225
225
|
await notifyCommandResult(ctx, commandText(result));
|
|
226
226
|
} });
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
pi.registerCommand("team-invalidate", {
|
|
230
|
+
description: "Invalidate the snapshot cache for a run so the UI refreshes immediately: <runId>",
|
|
231
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
232
|
+
const runId = args.trim() || undefined;
|
|
233
|
+
if (!runId) {
|
|
234
|
+
await notifyCommandResult(ctx, "Usage: /team-invalidate <runId>");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const result = await handleTeamTool({ action: "invalidate", runId }, { ...teamCommandContext(ctx), getRunSnapshotCache: deps.getRunSnapshotCache });
|
|
238
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
|
|
229
242
|
pi.registerCommand("team-retry", {
|
|
230
243
|
description: "Retry failed/cancelled pi-crew tasks: <runId> [taskId]",
|
|
231
244
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
@@ -236,7 +249,7 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
|
|
|
236
249
|
await notifyCommandResult(ctx, "Usage: /team-retry <runId> [taskId]");
|
|
237
250
|
return;
|
|
238
251
|
}
|
|
239
|
-
const retryResult = await handleTeamTool({ action: "retry", runId, taskId }, teamCommandContext(ctx));
|
|
252
|
+
const retryResult = await handleTeamTool({ action: "retry", runId, taskId }, { ...teamCommandContext(ctx), getRunSnapshotCache: deps.getRunSnapshotCache });
|
|
240
253
|
await notifyCommandResult(ctx, commandText(retryResult));
|
|
241
254
|
},
|
|
242
255
|
});
|
|
@@ -81,7 +81,7 @@ export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps):
|
|
|
81
81
|
const runLabel = resolved.team ?? resolved.agent ?? "direct";
|
|
82
82
|
pi.setSessionName(`pi-crew: ${runLabel}/${resolved.workflow ?? "default"} — ${resolved.goal.slice(0, 60)}`);
|
|
83
83
|
}
|
|
84
|
-
const output = await handleTeamTool(resolved, { ...toolCtx, signal: controller.signal, metricRegistry: deps.getMetricRegistry?.(), startForegroundRun: (runner, runId) => deps.startForegroundRun(toolCtx, runner, runId), abortForegroundRun: deps.abortForegroundRun, onRunStarted: (runId) => { stopProgress.attach(toolCtx.cwd, runId); deps.openLiveSidebar(toolCtx, runId); }, onJsonEvent: deps.onJsonEvent });
|
|
84
|
+
const output = await handleTeamTool(resolved, { ...toolCtx, signal: controller.signal, metricRegistry: deps.getMetricRegistry?.(), startForegroundRun: (runner, runId) => deps.startForegroundRun(toolCtx, runner, runId), abortForegroundRun: deps.abortForegroundRun, onRunStarted: (runId) => { stopProgress.attach(toolCtx.cwd, runId); deps.openLiveSidebar(toolCtx, runId); }, onJsonEvent: deps.onJsonEvent, getRunSnapshotCache: deps.getRunSnapshotCache });
|
|
85
85
|
if (resolved.action === "run" && !output.isError && typeof output.details?.runId === "string") {
|
|
86
86
|
pi.appendEntry("crew:run-started", {
|
|
87
87
|
runId: output.details.runId,
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { isDisplayActiveRun } from "../runtime/process-status.ts";
|
|
2
3
|
import { listRuns } from "./run-index.ts";
|
|
4
|
+
import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
3
5
|
|
|
4
6
|
export function notifyActiveRuns(ctx: ExtensionContext): void {
|
|
5
|
-
const active = listRuns(ctx.cwd)
|
|
7
|
+
const active = listRuns(ctx.cwd)
|
|
8
|
+
.filter((run) => {
|
|
9
|
+
if (run.status !== "queued" && run.status !== "planning" && run.status !== "running") return false;
|
|
10
|
+
// Use the same display filter as the widget/powerbar — runs without
|
|
11
|
+
// real agent evidence (e.g. integration test fixtures) must not appear.
|
|
12
|
+
const agents = readCrewAgents(run);
|
|
13
|
+
return isDisplayActiveRun(run, agents);
|
|
14
|
+
})
|
|
15
|
+
.slice(0, 5);
|
|
6
16
|
if (active.length === 0) return;
|
|
7
17
|
ctx.ui.notify(`pi-crew active runs: ${active.map((run) => `${run.runId} [${run.status}]`).join(", ")}`, "info");
|
|
8
18
|
}
|
|
@@ -21,6 +21,7 @@ import { liveControlRealtimeMessage, publishLiveControlRealtime } from "../../su
|
|
|
21
21
|
import { buildCapabilityInventory } from "../../runtime/capability-inventory.ts";
|
|
22
22
|
import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
|
|
23
23
|
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
24
|
+
import { locateRunCwd } from "../team-tool.ts";
|
|
24
25
|
import { configRecord, result, type TeamContext } from "./context.ts";
|
|
25
26
|
|
|
26
27
|
function globMatch(value: string, pattern: string): boolean {
|
|
@@ -83,7 +84,9 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
83
84
|
return result(JSON.stringify(inventory, null, 2), { action: "api", status: "ok" });
|
|
84
85
|
}
|
|
85
86
|
if (!params.runId) return result("API requires runId.", { action: "api", status: "error" }, true);
|
|
86
|
-
const
|
|
87
|
+
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
88
|
+
if (!runCwd) return result(`Run '${params.runId}' not found.`, { action: "api", status: "error" }, true);
|
|
89
|
+
const loaded = loadRunManifestById(runCwd, params.runId);
|
|
87
90
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "api", status: "error" }, true);
|
|
88
91
|
if (operation === "read-manifest") {
|
|
89
92
|
return result(JSON.stringify(loaded.manifest, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { runEventBus } from "../../ui/run-event-bus.ts";
|
|
2
|
+
import type { RunSnapshotCache } from "../../ui/run-snapshot-cache.ts";
|
|
3
|
+
|
|
4
|
+
export interface CacheControlDeps {
|
|
5
|
+
getRunSnapshotCache: (cwd: string) => RunSnapshotCache;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Invalidate the run snapshot cache for a specific runId and emit
|
|
10
|
+
* a runEventBus event so the render scheduler coalescer fires immediately.
|
|
11
|
+
* Call this after any state mutation that changes task/agent status.
|
|
12
|
+
*/
|
|
13
|
+
export function invalidateSnapshot(
|
|
14
|
+
runId: string,
|
|
15
|
+
runCwd: string,
|
|
16
|
+
deps: CacheControlDeps,
|
|
17
|
+
): void {
|
|
18
|
+
// 1. Invalidate snapshot cache entry
|
|
19
|
+
deps.getRunSnapshotCache(runCwd).invalidate(runId);
|
|
20
|
+
|
|
21
|
+
// 2. Emit runEventBus event so renderScheduler coalescer fires
|
|
22
|
+
runEventBus.emit({ runId, type: "run.cache_invalidated" });
|
|
23
|
+
}
|
|
@@ -10,8 +10,10 @@ import { killProcessPid } from "../../runtime/child-pi.ts";
|
|
|
10
10
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
11
11
|
import { executeHook, appendHookEvent } from "../../hooks/registry.ts";
|
|
12
12
|
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
13
|
+
import { locateRunCwd } from "../team-tool.ts";
|
|
13
14
|
import { result, type TeamContext } from "./context.ts";
|
|
14
15
|
import { enforceDestructiveIntent, intentFromConfig } from "./intent-policy.ts";
|
|
16
|
+
import { invalidateSnapshot, type CacheControlDeps } from "./cache-control.ts";
|
|
15
17
|
|
|
16
18
|
export interface AbortOwnedResult {
|
|
17
19
|
abortedIds: string[];
|
|
@@ -35,8 +37,11 @@ export function abortOwned(
|
|
|
35
37
|
runId: string,
|
|
36
38
|
taskIds: string[] | undefined,
|
|
37
39
|
ctx: TeamContext,
|
|
40
|
+
force?: boolean,
|
|
38
41
|
): AbortOwnedResult {
|
|
39
|
-
const
|
|
42
|
+
const runCwd = locateRunCwd(runId, ctx.cwd);
|
|
43
|
+
if (!runCwd) return { abortedIds: [], missingIds: taskIds ?? [], foreignIds: [] };
|
|
44
|
+
const loaded = loadRunManifestById(runCwd, runId);
|
|
40
45
|
if (!loaded) return { abortedIds: [], missingIds: taskIds ?? [], foreignIds: [] };
|
|
41
46
|
|
|
42
47
|
const result: AbortOwnedResult = { abortedIds: [], missingIds: [], foreignIds: [] };
|
|
@@ -51,7 +56,7 @@ export function abortOwned(
|
|
|
51
56
|
continue;
|
|
52
57
|
}
|
|
53
58
|
if (task.status !== "queued" && task.status !== "running" && task.status !== "waiting") continue;
|
|
54
|
-
if (foreignRun) {
|
|
59
|
+
if (foreignRun && !force) {
|
|
55
60
|
result.foreignIds.push(id);
|
|
56
61
|
continue;
|
|
57
62
|
}
|
|
@@ -72,15 +77,17 @@ function cancelReasonFromParams(params: TeamToolParamsValue): CancellationReason
|
|
|
72
77
|
return { code: reason.code, message: reason.message };
|
|
73
78
|
}
|
|
74
79
|
|
|
75
|
-
export async function handleRetry(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
|
|
80
|
+
export async function handleRetry(params: TeamToolParamsValue, ctx: TeamContext, deps?: CacheControlDeps): Promise<PiTeamsToolResult> {
|
|
76
81
|
if (!params.runId) return result("Retry requires runId.", { action: "retry", status: "error" }, true);
|
|
77
|
-
const
|
|
82
|
+
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
83
|
+
if (!runCwd) return result(`Run '${params.runId}' not found.`, { action: "retry", status: "error" }, true);
|
|
84
|
+
const loaded = loadRunManifestById(runCwd, params.runId);
|
|
78
85
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "retry", status: "error" }, true);
|
|
79
86
|
|
|
80
|
-
// Pre-lock ownership check: reject foreign-owned runs
|
|
87
|
+
// Pre-lock ownership check: reject foreign-owned runs unless force is set
|
|
81
88
|
const foreignRun = typeof loaded.manifest.ownerSessionId === "string" && loaded.manifest.ownerSessionId !== ctx.sessionId;
|
|
82
|
-
if (foreignRun) {
|
|
83
|
-
return result(`Run ${loaded.manifest.runId} belongs to another session
|
|
89
|
+
if (foreignRun && !params.force) {
|
|
90
|
+
return result(`Run ${loaded.manifest.runId} belongs to another session. Use force: true to override.`, { action: "retry", status: "error", runId: loaded.manifest.runId }, true);
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
// Execute before_retry hook after ownership confirmed, before mutation lock
|
|
@@ -122,6 +129,7 @@ export async function handleRetry(params: TeamToolParamsValue, ctx: TeamContext)
|
|
|
122
129
|
appendEvent(loaded.manifest.eventsPath, { type: "task.retried", runId: loaded.manifest.runId, taskId, message: `Task ${taskId} queued for retry.` });
|
|
123
130
|
}
|
|
124
131
|
|
|
132
|
+
if (deps) invalidateSnapshot(loaded.manifest.runId, runCwd, deps);
|
|
125
133
|
return result(`Retried ${retriedTaskIds.length} task(s) in run ${loaded.manifest.runId}.`, {
|
|
126
134
|
action: "retry",
|
|
127
135
|
status: "ok",
|
|
@@ -132,17 +140,19 @@ export async function handleRetry(params: TeamToolParamsValue, ctx: TeamContext)
|
|
|
132
140
|
});
|
|
133
141
|
}
|
|
134
142
|
|
|
135
|
-
export async function handleCancel(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
|
|
143
|
+
export async function handleCancel(params: TeamToolParamsValue, ctx: TeamContext, deps?: CacheControlDeps): Promise<PiTeamsToolResult> {
|
|
136
144
|
const intentError = enforceDestructiveIntent("cancel", params, ctx.config);
|
|
137
145
|
if (intentError) return intentError;
|
|
138
146
|
if (!params.runId) return result("Cancel requires runId.", { action: "cancel", status: "error" }, true);
|
|
139
|
-
const
|
|
147
|
+
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
148
|
+
if (!runCwd) return result(`Run '${params.runId}' not found.`, { action: "cancel", status: "error" }, true);
|
|
149
|
+
const loaded = loadRunManifestById(runCwd, params.runId);
|
|
140
150
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "cancel", status: "error" }, true);
|
|
141
151
|
|
|
142
|
-
// Pre-lock ownership check: reject foreign-owned runs
|
|
143
|
-
const preCheck = abortOwned(loaded.manifest.runId, undefined, ctx);
|
|
144
|
-
if (preCheck.abortedIds.length === 0 && preCheck.foreignIds.length > 0) {
|
|
145
|
-
return result(`Run ${loaded.manifest.runId} belongs to another session
|
|
152
|
+
// Pre-lock ownership check: reject foreign-owned runs unless force is set
|
|
153
|
+
const preCheck = abortOwned(loaded.manifest.runId, undefined, ctx, params.force);
|
|
154
|
+
if (preCheck.abortedIds.length === 0 && preCheck.foreignIds.length > 0 && !params.force) {
|
|
155
|
+
return result(`Run ${loaded.manifest.runId} belongs to another session. Use force: true to override.`, { action: "cancel", status: "error", runId: loaded.manifest.runId, foreignIds: preCheck.foreignIds }, true);
|
|
146
156
|
}
|
|
147
157
|
|
|
148
158
|
// Execute before_cancel hook after ownership confirmed, before mutation lock
|
|
@@ -169,9 +179,9 @@ export async function handleCancel(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
169
179
|
if ((loaded.manifest.status === "completed" || loaded.manifest.status === "cancelled") && !params.force) return result(`Run ${loaded.manifest.runId} is already ${loaded.manifest.status}; nothing to cancel. Use force: true to mark it cancelled anyway.`, { action: "cancel", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
170
180
|
|
|
171
181
|
// Classify tasks for foreign-aware cancellation
|
|
172
|
-
const abortResult = abortOwned(loaded.manifest.runId, undefined, ctx);
|
|
173
|
-
if (abortResult.abortedIds.length === 0 && abortResult.foreignIds.length > 0) {
|
|
174
|
-
return result(`Run ${loaded.manifest.runId} belongs to another session
|
|
182
|
+
const abortResult = abortOwned(loaded.manifest.runId, undefined, ctx, params.force);
|
|
183
|
+
if (abortResult.abortedIds.length === 0 && abortResult.foreignIds.length > 0 && !params.force) {
|
|
184
|
+
return result(`Run ${loaded.manifest.runId} belongs to another session. Use force: true to override.`, { action: "cancel", status: "error", runId: loaded.manifest.runId, foreignIds: abortResult.foreignIds }, true);
|
|
175
185
|
}
|
|
176
186
|
const cancellableIds = new Set(abortResult.abortedIds);
|
|
177
187
|
const cancelReason = cancelReasonFromParams(params);
|
|
@@ -211,6 +221,7 @@ export async function handleCancel(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
211
221
|
if (abortResult.foreignIds.length > 0) parts.push(` ${abortResult.foreignIds.length} task(s) belong to another session and were not cancelled: ${abortResult.foreignIds.join(", ")}.`);
|
|
212
222
|
if (abortResult.missingIds.length > 0) parts.push(` ${abortResult.missingIds.length} task ID(s) not found: ${abortResult.missingIds.join(", ")}.`);
|
|
213
223
|
|
|
224
|
+
if (deps) invalidateSnapshot(updated.runId, runCwd, deps);
|
|
214
225
|
return result(parts.join(""), {
|
|
215
226
|
action: "cancel",
|
|
216
227
|
status: "ok",
|
|
@@ -2,6 +2,7 @@ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
|
2
2
|
import type { PiTeamsConfig } from "../../config/config.ts";
|
|
3
3
|
import type { MetricRegistry } from "../../observability/metric-registry.ts";
|
|
4
4
|
import type { TeamToolDetails } from "../team-tool-types.ts";
|
|
5
|
+
import type { RunSnapshotCache } from "../../ui/run-snapshot-cache.ts";
|
|
5
6
|
import { toolResult, type PiTeamsToolResult } from "../tool-result.ts";
|
|
6
7
|
|
|
7
8
|
export type TeamContext = Pick<ExtensionContext, "cwd"> & Partial<Pick<ExtensionContext, "model">> & {
|
|
@@ -16,6 +17,7 @@ export type TeamContext = Pick<ExtensionContext, "cwd"> & Partial<Pick<Extension
|
|
|
16
17
|
onRunStarted?: (runId: string) => void;
|
|
17
18
|
onJsonEvent?: (taskId: string, runId: string, event: unknown) => void;
|
|
18
19
|
config?: PiTeamsConfig;
|
|
20
|
+
getRunSnapshotCache?: (cwd: string) => RunSnapshotCache;
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
export function withSessionId<T extends Pick<ExtensionContext, "sessionManager">>(ctx: T): T & { sessionId?: string } {
|
|
@@ -40,6 +40,7 @@ const EFFECTIVE_DEFAULTS: Record<string, unknown> = {
|
|
|
40
40
|
"notifierIntervalMs": 5000,
|
|
41
41
|
"reliability.autoRetry": false,
|
|
42
42
|
"reliability.autoRecover": false,
|
|
43
|
+
"reliability.cleanupOrphanedTempDirs": true,
|
|
43
44
|
"telemetry.enabled": false,
|
|
44
45
|
"notifications.enabled": false,
|
|
45
46
|
};
|
|
@@ -192,6 +193,7 @@ const KNOWN_KEYS = new Set([
|
|
|
192
193
|
// reliability
|
|
193
194
|
"reliability.autoRetry",
|
|
194
195
|
"reliability.autoRecover",
|
|
196
|
+
"reliability.cleanupOrphanedTempDirs",
|
|
195
197
|
"reliability.deadletterThreshold",
|
|
196
198
|
"reliability.retryPolicy.maxAttempts",
|
|
197
199
|
"reliability.retryPolicy.backoffMs",
|