pi-crew 0.3.7 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/package.json +1 -1
  3. package/src/agents/discover-agents.ts +354 -15
  4. package/src/config/config.ts +732 -208
  5. package/src/config/types.ts +34 -5
  6. package/src/extension/help.ts +1 -0
  7. package/src/extension/register.ts +1173 -257
  8. package/src/extension/registration/commands.ts +15 -2
  9. package/src/extension/registration/team-tool.ts +1 -1
  10. package/src/extension/session-summary.ts +11 -1
  11. package/src/extension/team-tool/api.ts +4 -1
  12. package/src/extension/team-tool/cache-control.ts +23 -0
  13. package/src/extension/team-tool/cancel.ts +15 -5
  14. package/src/extension/team-tool/context.ts +2 -0
  15. package/src/extension/team-tool/handle-settings.ts +2 -0
  16. package/src/extension/team-tool/health-monitor.ts +563 -0
  17. package/src/extension/team-tool/inspect.ts +10 -3
  18. package/src/extension/team-tool/respond.ts +5 -2
  19. package/src/extension/team-tool/status.ts +4 -1
  20. package/src/extension/team-tool-types.ts +2 -0
  21. package/src/extension/team-tool.ts +901 -177
  22. package/src/runtime/adaptive-plan.ts +1 -1
  23. package/src/runtime/foreground-watchdog.ts +129 -0
  24. package/src/runtime/manifest-cache.ts +4 -2
  25. package/src/runtime/run-tracker.ts +11 -0
  26. package/src/runtime/runtime-policy.ts +15 -2
  27. package/src/runtime/skill-instructions.ts +8 -2
  28. package/src/runtime/stale-reconciler.ts +322 -18
  29. package/src/runtime/task-packet.ts +48 -1
  30. package/src/runtime/task-runner.ts +6 -1
  31. package/src/schema/config-schema.ts +1 -0
  32. package/src/schema/team-tool-schema.ts +204 -76
  33. package/src/state/state-store.ts +9 -1
  34. package/src/teams/discover-teams.ts +2 -1
  35. package/src/ui/run-event-bus.ts +2 -1
  36. package/src/ui/settings-overlay.ts +2 -0
  37. 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).filter((run) => run.status === "queued" || run.status === "planning" || run.status === "running").slice(0, 5);
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 loaded = loadRunManifestById(ctx.cwd, params.runId);
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[];
@@ -37,7 +39,9 @@ export function abortOwned(
37
39
  ctx: TeamContext,
38
40
  force?: boolean,
39
41
  ): AbortOwnedResult {
40
- const loaded = loadRunManifestById(ctx.cwd, runId);
42
+ const runCwd = locateRunCwd(runId, ctx.cwd);
43
+ if (!runCwd) return { abortedIds: [], missingIds: taskIds ?? [], foreignIds: [] };
44
+ const loaded = loadRunManifestById(runCwd, runId);
41
45
  if (!loaded) return { abortedIds: [], missingIds: taskIds ?? [], foreignIds: [] };
42
46
 
43
47
  const result: AbortOwnedResult = { abortedIds: [], missingIds: [], foreignIds: [] };
@@ -73,9 +77,11 @@ function cancelReasonFromParams(params: TeamToolParamsValue): CancellationReason
73
77
  return { code: reason.code, message: reason.message };
74
78
  }
75
79
 
76
- export async function handleRetry(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
80
+ export async function handleRetry(params: TeamToolParamsValue, ctx: TeamContext, deps?: CacheControlDeps): Promise<PiTeamsToolResult> {
77
81
  if (!params.runId) return result("Retry requires runId.", { action: "retry", status: "error" }, true);
78
- const loaded = loadRunManifestById(ctx.cwd, params.runId);
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);
79
85
  if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "retry", status: "error" }, true);
80
86
 
81
87
  // Pre-lock ownership check: reject foreign-owned runs unless force is set
@@ -123,6 +129,7 @@ export async function handleRetry(params: TeamToolParamsValue, ctx: TeamContext)
123
129
  appendEvent(loaded.manifest.eventsPath, { type: "task.retried", runId: loaded.manifest.runId, taskId, message: `Task ${taskId} queued for retry.` });
124
130
  }
125
131
 
132
+ if (deps) invalidateSnapshot(loaded.manifest.runId, runCwd, deps);
126
133
  return result(`Retried ${retriedTaskIds.length} task(s) in run ${loaded.manifest.runId}.`, {
127
134
  action: "retry",
128
135
  status: "ok",
@@ -133,11 +140,13 @@ export async function handleRetry(params: TeamToolParamsValue, ctx: TeamContext)
133
140
  });
134
141
  }
135
142
 
136
- export async function handleCancel(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
143
+ export async function handleCancel(params: TeamToolParamsValue, ctx: TeamContext, deps?: CacheControlDeps): Promise<PiTeamsToolResult> {
137
144
  const intentError = enforceDestructiveIntent("cancel", params, ctx.config);
138
145
  if (intentError) return intentError;
139
146
  if (!params.runId) return result("Cancel requires runId.", { action: "cancel", status: "error" }, true);
140
- const loaded = loadRunManifestById(ctx.cwd, params.runId);
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);
141
150
  if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "cancel", status: "error" }, true);
142
151
 
143
152
  // Pre-lock ownership check: reject foreign-owned runs unless force is set
@@ -212,6 +221,7 @@ export async function handleCancel(params: TeamToolParamsValue, ctx: TeamContext
212
221
  if (abortResult.foreignIds.length > 0) parts.push(` ${abortResult.foreignIds.length} task(s) belong to another session and were not cancelled: ${abortResult.foreignIds.join(", ")}.`);
213
222
  if (abortResult.missingIds.length > 0) parts.push(` ${abortResult.missingIds.length} task ID(s) not found: ${abortResult.missingIds.join(", ")}.`);
214
223
 
224
+ if (deps) invalidateSnapshot(updated.runId, runCwd, deps);
215
225
  return result(parts.join(""), {
216
226
  action: "cancel",
217
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",