pi-crew 0.1.5 → 0.1.7

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 (35) hide show
  1. package/package.json +2 -1
  2. package/schema.json +24 -0
  3. package/src/agents/agent-config.ts +2 -0
  4. package/src/agents/discover-agents.ts +29 -5
  5. package/src/config/config.ts +148 -9
  6. package/src/extension/register.ts +10 -2
  7. package/src/extension/team-tool.ts +113 -10
  8. package/src/prompt/prompt-runtime.ts +12 -2
  9. package/src/runtime/agent-control.ts +64 -0
  10. package/src/runtime/agent-observability.ts +88 -0
  11. package/src/runtime/async-runner.ts +30 -1
  12. package/src/runtime/background-runner.ts +4 -2
  13. package/src/runtime/child-pi.ts +137 -7
  14. package/src/runtime/crew-agent-records.ts +137 -0
  15. package/src/runtime/crew-agent-runtime.ts +54 -0
  16. package/src/runtime/foreground-control.ts +82 -0
  17. package/src/runtime/group-join.ts +88 -0
  18. package/src/runtime/live-session-runtime.ts +33 -0
  19. package/src/runtime/pi-args.ts +29 -0
  20. package/src/runtime/policy-engine.ts +23 -0
  21. package/src/runtime/recovery-recipes.ts +74 -0
  22. package/src/runtime/role-permission.ts +28 -0
  23. package/src/runtime/runtime-resolver.ts +75 -0
  24. package/src/runtime/session-usage.ts +79 -0
  25. package/src/runtime/task-graph-scheduler.ts +107 -0
  26. package/src/runtime/task-output-context.ts +106 -0
  27. package/src/runtime/task-runner.ts +220 -4
  28. package/src/runtime/team-runner.ts +86 -14
  29. package/src/runtime/worker-startup.ts +57 -0
  30. package/src/state/contracts.ts +7 -0
  31. package/src/state/event-log.ts +103 -2
  32. package/src/state/state-store.ts +23 -2
  33. package/src/state/types.ts +3 -0
  34. package/src/ui/run-dashboard.ts +82 -10
  35. package/src/worktree/branch-freshness.ts +45 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -68,6 +68,7 @@
68
68
  "@mariozechner/pi-tui": "*"
69
69
  },
70
70
  "dependencies": {
71
+ "jiti": "^2.6.1",
71
72
  "typebox": "^1.1.24"
72
73
  },
73
74
  "devDependencies": {
package/schema.json CHANGED
@@ -54,6 +54,30 @@
54
54
  "maxTasksPerRun": { "type": "integer", "minimum": 1 },
55
55
  "heartbeatStaleMs": { "type": "integer", "minimum": 1 }
56
56
  }
57
+ },
58
+ "runtime": {
59
+ "type": "object",
60
+ "additionalProperties": false,
61
+ "description": "Crew runtime selection and live-agent behavior knobs.",
62
+ "properties": {
63
+ "mode": { "type": "string", "enum": ["auto", "scaffold", "child-process", "live-session"] },
64
+ "preferLiveSession": { "type": "boolean" },
65
+ "allowChildProcessFallback": { "type": "boolean" },
66
+ "maxTurns": { "type": "integer", "minimum": 1 },
67
+ "graceTurns": { "type": "integer", "minimum": 1 },
68
+ "inheritContext": { "type": "boolean" },
69
+ "promptMode": { "type": "string", "enum": ["replace", "append"] },
70
+ "groupJoin": { "type": "string", "enum": ["off", "group", "smart"] }
71
+ }
72
+ },
73
+ "control": {
74
+ "type": "object",
75
+ "additionalProperties": false,
76
+ "description": "Agent control-plane settings for attention/stale activity detection.",
77
+ "properties": {
78
+ "enabled": { "type": "boolean" },
79
+ "needsAttentionAfterMs": { "type": "integer", "minimum": 1 }
80
+ }
57
81
  }
58
82
  }
59
83
  }
@@ -24,4 +24,6 @@ export interface AgentConfig {
24
24
  inheritProjectContext?: boolean;
25
25
  inheritSkills?: boolean;
26
26
  routing?: RoutingMetadata;
27
+ disabled?: boolean;
28
+ override?: { source: "config"; path: string };
27
29
  }
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { AgentConfig, ResourceSource } from "./agent-config.ts";
4
+ import { loadConfig } from "../config/config.ts";
4
5
  import { parseCsv, parseFrontmatter } from "../utils/frontmatter.ts";
5
6
  import { packageRoot, projectPiRoot, userPiRoot } from "../utils/paths.ts";
6
7
 
@@ -40,6 +41,7 @@ function parseAgentFile(filePath: string, source: ResourceSource): AgentConfig |
40
41
  systemPromptMode: frontmatter.systemPromptMode === "append" ? "append" : "replace",
41
42
  inheritProjectContext: frontmatter.inheritProjectContext === "true",
42
43
  inheritSkills: frontmatter.inheritSkills === "true",
44
+ disabled: frontmatter.disabled === "true" || frontmatter.enabled === "false",
43
45
  routing: triggers || useWhen || avoidWhen || cost || category ? { triggers, useWhen, avoidWhen, cost, category } : undefined,
44
46
  };
45
47
  } catch {
@@ -56,18 +58,40 @@ function readAgentDir(dir: string, source: ResourceSource): AgentConfig[] {
56
58
  .sort((a, b) => a.name.localeCompare(b.name));
57
59
  }
58
60
 
61
+ function applyAgentOverrides(agents: AgentConfig[], cwd: string): AgentConfig[] {
62
+ const loaded = loadConfig(cwd);
63
+ const config = loaded.config.agents;
64
+ const overrides = config?.overrides ?? {};
65
+ return agents
66
+ .filter((agent) => !(config?.disableBuiltins && agent.source === "builtin"))
67
+ .map((agent) => {
68
+ const overrideEntry = Object.entries(overrides).find(([name]) => name.toLowerCase() === agent.name.toLowerCase());
69
+ if (!overrideEntry) return agent;
70
+ const [, override] = overrideEntry;
71
+ return {
72
+ ...agent,
73
+ disabled: override.disabled ?? agent.disabled,
74
+ model: override.model === false ? undefined : override.model ?? agent.model,
75
+ fallbackModels: override.fallbackModels === false ? undefined : override.fallbackModels ?? agent.fallbackModels,
76
+ thinking: override.thinking === false ? undefined : override.thinking ?? agent.thinking,
77
+ tools: override.tools === false ? undefined : override.tools ?? agent.tools,
78
+ override: { source: "config", path: loaded.path },
79
+ };
80
+ });
81
+ }
82
+
59
83
  export function discoverAgents(cwd: string): AgentDiscoveryResult {
60
84
  return {
61
- builtin: readAgentDir(path.join(packageRoot(), "agents"), "builtin"),
62
- user: readAgentDir(path.join(userPiRoot(), "agents"), "user"),
63
- project: readAgentDir(path.join(projectPiRoot(cwd), "agents"), "project"),
85
+ builtin: applyAgentOverrides(readAgentDir(path.join(packageRoot(), "agents"), "builtin"), cwd),
86
+ user: applyAgentOverrides(readAgentDir(path.join(userPiRoot(), "agents"), "user"), cwd),
87
+ project: applyAgentOverrides(readAgentDir(path.join(projectPiRoot(cwd), "agents"), "project"), cwd),
64
88
  };
65
89
  }
66
90
 
67
91
  export function allAgents(discovery: AgentDiscoveryResult): AgentConfig[] {
68
92
  const byName = new Map<string, AgentConfig>();
69
93
  for (const agent of [...discovery.builtin, ...discovery.user, ...discovery.project]) {
70
- byName.set(agent.name, agent);
94
+ byName.set(agent.name.toLowerCase(), agent);
71
95
  }
72
- return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
96
+ return [...byName.values()].filter((agent) => !agent.disabled).sort((a, b) => a.name.localeCompare(b.name));
73
97
  }
@@ -23,6 +23,37 @@ export interface CrewLimitsConfig {
23
23
  heartbeatStaleMs?: number;
24
24
  }
25
25
 
26
+ export type CrewRuntimeMode = "auto" | "scaffold" | "child-process" | "live-session";
27
+
28
+ export interface CrewRuntimeConfig {
29
+ mode?: CrewRuntimeMode;
30
+ preferLiveSession?: boolean;
31
+ allowChildProcessFallback?: boolean;
32
+ maxTurns?: number;
33
+ graceTurns?: number;
34
+ inheritContext?: boolean;
35
+ promptMode?: "replace" | "append";
36
+ groupJoin?: "off" | "group" | "smart";
37
+ }
38
+
39
+ export interface CrewControlConfig {
40
+ enabled?: boolean;
41
+ needsAttentionAfterMs?: number;
42
+ }
43
+
44
+ export interface AgentOverrideConfig {
45
+ disabled?: boolean;
46
+ model?: string | false;
47
+ fallbackModels?: string[] | false;
48
+ thinking?: string | false;
49
+ tools?: string[] | false;
50
+ }
51
+
52
+ export interface CrewAgentsConfig {
53
+ disableBuiltins?: boolean;
54
+ overrides?: Record<string, AgentOverrideConfig>;
55
+ }
56
+
26
57
  export interface PiTeamsConfig {
27
58
  asyncByDefault?: boolean;
28
59
  executeWorkers?: boolean;
@@ -30,6 +61,9 @@ export interface PiTeamsConfig {
30
61
  requireCleanWorktreeLeader?: boolean;
31
62
  autonomous?: PiTeamsAutonomousConfig;
32
63
  limits?: CrewLimitsConfig;
64
+ runtime?: CrewRuntimeConfig;
65
+ control?: CrewControlConfig;
66
+ agents?: CrewAgentsConfig;
33
67
  }
34
68
 
35
69
  export interface LoadedPiTeamsConfig {
@@ -77,6 +111,29 @@ function mergeConfig(base: PiTeamsConfig, override: PiTeamsConfig): PiTeamsConfi
77
111
  ...withoutUndefined((override.limits ?? {}) as Record<string, unknown>),
78
112
  };
79
113
  }
114
+ if (base.runtime || override.runtime) {
115
+ merged.runtime = {
116
+ ...(base.runtime ?? {}),
117
+ ...withoutUndefined((override.runtime ?? {}) as Record<string, unknown>),
118
+ };
119
+ }
120
+ if (base.control || override.control) {
121
+ merged.control = {
122
+ ...(base.control ?? {}),
123
+ ...withoutUndefined((override.control ?? {}) as Record<string, unknown>),
124
+ };
125
+ }
126
+ if (base.agents || override.agents) {
127
+ merged.agents = {
128
+ ...(base.agents ?? {}),
129
+ ...withoutUndefined((override.agents ?? {}) as Record<string, unknown>),
130
+ overrides: {
131
+ ...(base.agents?.overrides ?? {}),
132
+ ...(override.agents?.overrides ?? {}),
133
+ },
134
+ };
135
+ }
136
+ if (merged.agents?.overrides && Object.keys(merged.agents.overrides).length === 0) delete merged.agents.overrides;
80
137
  return merged;
81
138
  }
82
139
 
@@ -127,25 +184,104 @@ function parseAutonomousConfig(value: unknown): PiTeamsAutonomousConfig | undefi
127
184
  };
128
185
  }
129
186
 
130
- function parsePositiveInteger(value: unknown): number | undefined {
131
- return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
187
+ const LIMIT_CEILINGS = {
188
+ maxConcurrentWorkers: 1024,
189
+ maxTaskDepth: 100,
190
+ maxChildrenPerTask: 1000,
191
+ maxRunMinutes: 1440,
192
+ maxRetriesPerTask: 100,
193
+ maxTasksPerRun: 10_000,
194
+ heartbeatStaleMs: 24 * 60 * 60 * 1000,
195
+ runtimeMaxTurns: 10_000,
196
+ runtimeGraceTurns: 1_000,
197
+ } as const;
198
+
199
+ function parsePositiveInteger(value: unknown, max = Number.MAX_SAFE_INTEGER): number | undefined {
200
+ return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= max ? value : undefined;
132
201
  }
133
202
 
134
203
  function parseLimitsConfig(value: unknown): CrewLimitsConfig | undefined {
135
204
  if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
136
205
  const obj = value as Record<string, unknown>;
137
206
  const limits: CrewLimitsConfig = {
138
- maxConcurrentWorkers: parsePositiveInteger(obj.maxConcurrentWorkers),
139
- maxTaskDepth: parsePositiveInteger(obj.maxTaskDepth),
140
- maxChildrenPerTask: parsePositiveInteger(obj.maxChildrenPerTask),
141
- maxRunMinutes: parsePositiveInteger(obj.maxRunMinutes),
142
- maxRetriesPerTask: parsePositiveInteger(obj.maxRetriesPerTask),
143
- maxTasksPerRun: parsePositiveInteger(obj.maxTasksPerRun),
144
- heartbeatStaleMs: parsePositiveInteger(obj.heartbeatStaleMs),
207
+ maxConcurrentWorkers: parsePositiveInteger(obj.maxConcurrentWorkers, LIMIT_CEILINGS.maxConcurrentWorkers),
208
+ maxTaskDepth: parsePositiveInteger(obj.maxTaskDepth, LIMIT_CEILINGS.maxTaskDepth),
209
+ maxChildrenPerTask: parsePositiveInteger(obj.maxChildrenPerTask, LIMIT_CEILINGS.maxChildrenPerTask),
210
+ maxRunMinutes: parsePositiveInteger(obj.maxRunMinutes, LIMIT_CEILINGS.maxRunMinutes),
211
+ maxRetriesPerTask: parsePositiveInteger(obj.maxRetriesPerTask, LIMIT_CEILINGS.maxRetriesPerTask),
212
+ maxTasksPerRun: parsePositiveInteger(obj.maxTasksPerRun, LIMIT_CEILINGS.maxTasksPerRun),
213
+ heartbeatStaleMs: parsePositiveInteger(obj.heartbeatStaleMs, LIMIT_CEILINGS.heartbeatStaleMs),
145
214
  };
146
215
  return Object.values(limits).some((entry) => entry !== undefined) ? limits : undefined;
147
216
  }
148
217
 
218
+ function parseRuntimeMode(value: unknown): CrewRuntimeMode | undefined {
219
+ return value === "auto" || value === "scaffold" || value === "child-process" || value === "live-session" ? value : undefined;
220
+ }
221
+
222
+ function parseRuntimeConfig(value: unknown): CrewRuntimeConfig | undefined {
223
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
224
+ const obj = value as Record<string, unknown>;
225
+ const runtime: CrewRuntimeConfig = {
226
+ mode: parseRuntimeMode(obj.mode),
227
+ preferLiveSession: typeof obj.preferLiveSession === "boolean" ? obj.preferLiveSession : undefined,
228
+ allowChildProcessFallback: typeof obj.allowChildProcessFallback === "boolean" ? obj.allowChildProcessFallback : undefined,
229
+ maxTurns: parsePositiveInteger(obj.maxTurns, LIMIT_CEILINGS.runtimeMaxTurns),
230
+ graceTurns: parsePositiveInteger(obj.graceTurns, LIMIT_CEILINGS.runtimeGraceTurns),
231
+ inheritContext: typeof obj.inheritContext === "boolean" ? obj.inheritContext : undefined,
232
+ promptMode: obj.promptMode === "replace" || obj.promptMode === "append" ? obj.promptMode : undefined,
233
+ groupJoin: obj.groupJoin === "off" || obj.groupJoin === "group" || obj.groupJoin === "smart" ? obj.groupJoin : undefined,
234
+ };
235
+ return Object.values(runtime).some((entry) => entry !== undefined) ? runtime : undefined;
236
+ }
237
+
238
+ function parseControlConfig(value: unknown): CrewControlConfig | undefined {
239
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
240
+ const obj = value as Record<string, unknown>;
241
+ const control: CrewControlConfig = {
242
+ enabled: typeof obj.enabled === "boolean" ? obj.enabled : undefined,
243
+ needsAttentionAfterMs: parsePositiveInteger(obj.needsAttentionAfterMs),
244
+ };
245
+ return Object.values(control).some((entry) => entry !== undefined) ? control : undefined;
246
+ }
247
+
248
+ function parseStringArrayOrFalse(value: unknown): string[] | false | undefined {
249
+ if (value === false) return false;
250
+ if (typeof value === "string") return value.split(",").map((entry) => entry.trim()).filter(Boolean);
251
+ if (Array.isArray(value)) return value.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0).map((entry) => entry.trim());
252
+ return undefined;
253
+ }
254
+
255
+ function parseAgentOverride(value: unknown): AgentOverrideConfig | undefined {
256
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
257
+ const obj = value as Record<string, unknown>;
258
+ const override: AgentOverrideConfig = {
259
+ disabled: typeof obj.disabled === "boolean" ? obj.disabled : undefined,
260
+ model: typeof obj.model === "string" || obj.model === false ? obj.model : undefined,
261
+ fallbackModels: parseStringArrayOrFalse(obj.fallbackModels),
262
+ thinking: typeof obj.thinking === "string" || obj.thinking === false ? obj.thinking : undefined,
263
+ tools: parseStringArrayOrFalse(obj.tools),
264
+ };
265
+ return Object.values(override).some((entry) => entry !== undefined) ? override : undefined;
266
+ }
267
+
268
+ function parseAgentsConfig(value: unknown): CrewAgentsConfig | undefined {
269
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
270
+ const obj = value as Record<string, unknown>;
271
+ const overrides: Record<string, AgentOverrideConfig> = {};
272
+ if (obj.overrides && typeof obj.overrides === "object" && !Array.isArray(obj.overrides)) {
273
+ for (const [name, rawOverride] of Object.entries(obj.overrides)) {
274
+ const parsed = parseAgentOverride(rawOverride);
275
+ if (parsed) overrides[name] = parsed;
276
+ }
277
+ }
278
+ const agents: CrewAgentsConfig = {
279
+ disableBuiltins: typeof obj.disableBuiltins === "boolean" ? obj.disableBuiltins : undefined,
280
+ overrides: Object.keys(overrides).length > 0 ? overrides : undefined,
281
+ };
282
+ return Object.values(agents).some((entry) => entry !== undefined) ? agents : undefined;
283
+ }
284
+
149
285
  function parseConfig(raw: unknown): PiTeamsConfig {
150
286
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
151
287
  const obj = raw as Record<string, unknown>;
@@ -156,6 +292,9 @@ function parseConfig(raw: unknown): PiTeamsConfig {
156
292
  requireCleanWorktreeLeader: typeof obj.requireCleanWorktreeLeader === "boolean" ? obj.requireCleanWorktreeLeader : undefined,
157
293
  autonomous: parseAutonomousConfig(obj.autonomous),
158
294
  limits: parseLimitsConfig(obj.limits),
295
+ runtime: parseRuntimeConfig(obj.runtime),
296
+ control: parseControlConfig(obj.control),
297
+ agents: parseAgentsConfig(obj.agents),
159
298
  };
160
299
  }
161
300
 
@@ -167,7 +167,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
167
167
  const config: Record<string, unknown> = { operation };
168
168
  for (const token of tokens.filter((item) => item.includes("="))) {
169
169
  const [key, ...rest] = token.split("=");
170
- if (key) config[key] = rest.join("=");
170
+ if (key) config[key] = parseScalar(rest.join("="));
171
171
  }
172
172
  const result = await handleTeamTool({ action: "api", runId, config }, ctx);
173
173
  await notifyCommandResult(ctx, commandText(result));
@@ -255,7 +255,15 @@ export function registerPiTeams(pi: ExtensionAPI): void {
255
255
  if (selection.action === "reload") continue;
256
256
  const result = selection.action === "api"
257
257
  ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-manifest" } }, ctx)
258
- : await handleTeamTool({ action: selection.action, runId: selection.runId }, ctx);
258
+ : selection.action === "agents"
259
+ ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "agent-dashboard" } }, ctx)
260
+ : selection.action === "agent-events"
261
+ ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-events", limit: 50 } }, ctx)
262
+ : selection.action === "agent-output"
263
+ ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-output", maxBytes: 32_000 } }, ctx)
264
+ : selection.action === "agent-transcript"
265
+ ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-transcript" } }, ctx)
266
+ : await handleTeamTool({ action: selection.action, runId: selection.runId }, ctx);
259
267
  await notifyCommandResult(ctx, commandText(result));
260
268
  return;
261
269
  }
@@ -21,7 +21,7 @@ import { getPiSpawnCommand } from "../runtime/pi-spawn.ts";
21
21
  import { executeTeamRun } from "../runtime/team-runner.ts";
22
22
  import { spawnBackgroundTeamRun } from "../runtime/async-runner.ts";
23
23
  import { checkProcessLiveness, isActiveRunStatus } from "../runtime/process-status.ts";
24
- import { appendEvent, readEvents } from "../state/event-log.ts";
24
+ import { appendEvent, readEvents, readEventsCursor } from "../state/event-log.ts";
25
25
  import { cleanupRunWorktrees } from "../worktree/cleanup.ts";
26
26
  import { piTeamsHelp } from "./help.ts";
27
27
  import { initializeProject } from "./project-init.ts";
@@ -35,6 +35,12 @@ import { formatValidationReport, validateResources } from "./validate-resources.
35
35
  import { formatRecommendation, recommendTeam } from "./team-recommendation.ts";
36
36
  import { toolResult, type PiTeamsToolResult } from "./tool-result.ts";
37
37
  import { touchWorkerHeartbeat } from "../runtime/worker-heartbeat.ts";
38
+ import { agentEventsPath, agentOutputPath, readCrewAgentEvents, readCrewAgentEventsCursor, readCrewAgentStatus, readCrewAgents } from "../runtime/crew-agent-records.ts";
39
+ import { resolveCrewRuntime } from "../runtime/runtime-resolver.ts";
40
+ import { probeLiveSessionRuntime } from "../runtime/live-session-runtime.ts";
41
+ import { applyAttentionState, formatActivityAge, resolveCrewControlConfig } from "../runtime/agent-control.ts";
42
+ import { buildAgentDashboard, readAgentOutput } from "../runtime/agent-observability.ts";
43
+ import { readForegroundControlStatus, writeForegroundInterruptRequest } from "../runtime/foreground-control.ts";
38
44
 
39
45
  export interface TeamToolDetails {
40
46
  action: string;
@@ -255,8 +261,9 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
255
261
  return result(text, { action: "run", status: "ok", runId: updatedManifest.runId, artifactsRoot: updatedManifest.artifactsRoot });
256
262
  }
257
263
 
258
- const executeWorkers = loadedConfig.config.executeWorkers === true || process.env.PI_TEAMS_EXECUTE_WORKERS === "1";
259
- const executed = await executeTeamRun({ manifest: updatedManifest, tasks, team, workflow, agents, executeWorkers, limits: loadedConfig.config.limits });
264
+ const runtime = await resolveCrewRuntime(loadedConfig.config);
265
+ const executeWorkers = runtime.kind === "child-process";
266
+ const executed = await executeTeamRun({ manifest: updatedManifest, tasks, team, workflow, agents, executeWorkers, limits: loadedConfig.config.limits, runtime, runtimeConfig: loadedConfig.config.runtime });
260
267
  const text = [
261
268
  `Created pi-crew run ${executed.manifest.runId}.`,
262
269
  `Team: ${team.name}`,
@@ -266,9 +273,10 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
266
273
  `State: ${executed.manifest.stateRoot}`,
267
274
  `Artifacts: ${executed.manifest.artifactsRoot}`,
268
275
  "",
276
+ `Runtime: ${runtime.kind}${runtime.fallback ? ` (fallback from ${runtime.requestedMode})` : ""}${runtime.reason ? ` - ${runtime.reason}` : ""}`,
269
277
  executeWorkers
270
- ? "Child Pi worker execution was enabled with PI_TEAMS_EXECUTE_WORKERS=1."
271
- : "Safe scaffold mode: child Pi workers were not launched. Set PI_TEAMS_EXECUTE_WORKERS=1 to enable real worker execution.",
278
+ ? "Child Pi worker execution was enabled."
279
+ : "Safe scaffold mode: child Pi workers were not launched. Set PI_TEAMS_EXECUTE_WORKERS=1 or runtime.mode=child-process to enable real worker execution.",
272
280
  ].join("\n");
273
281
  return result(text, { action: "run", status: executed.manifest.status === "failed" ? "error" : "ok", runId: executed.manifest.runId, artifactsRoot: executed.manifest.artifactsRoot }, executed.manifest.status === "failed");
274
282
  }
@@ -291,6 +299,8 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
291
299
  const counts = new Map<string, number>();
292
300
  for (const task of tasks) counts.set(task.status, (counts.get(task.status) ?? 0) + 1);
293
301
  const events = readEvents(manifest.eventsPath).slice(-8);
302
+ const controlConfig = resolveCrewControlConfig(loadConfig(ctx.cwd).config);
303
+ const crewAgents = readCrewAgents(manifest).map((agent) => applyAttentionState(manifest, agent, controlConfig));
294
304
  const artifactLines = manifest.artifacts.slice(-10).map((artifact) => `- ${artifact.kind}: ${artifact.path}${artifact.sizeBytes !== undefined ? ` (${artifact.sizeBytes} bytes)` : ""}`);
295
305
  const totalUsage = aggregateUsage(tasks);
296
306
  const lines = [
@@ -308,6 +318,8 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
308
318
  "Tasks:",
309
319
  ...(tasks.length ? tasks.map((task) => `- ${task.id} [${task.status}] ${task.role} -> ${task.agent}${task.taskPacket ? ` scope=${task.taskPacket.scope}` : ""}${task.verification ? ` green=${task.verification.observedGreenLevel}/${task.verification.requiredGreenLevel}` : ""}${task.modelAttempts?.length ? ` attempts=${task.modelAttempts.length}` : ""}${task.jsonEvents !== undefined ? ` jsonEvents=${task.jsonEvents}` : ""}${task.usage ? ` usage=${JSON.stringify(task.usage)}` : ""}${task.worktree ? ` worktree=${task.worktree.path}` : ""}${task.error ? ` error=${task.error}` : ""}`) : ["- (none)"]),
310
320
  `Task counts: ${[...counts.entries()].map(([status, count]) => `${status}=${count}`).join(", ") || "none"}`,
321
+ "Agents:",
322
+ ...(crewAgents.length ? crewAgents.map((agent) => `- ${agent.id} [${agent.status}] ${agent.role} -> ${agent.agent} runtime=${agent.runtime}${agent.progress?.activityState === "needs_attention" ? " needs_attention" : ""}${formatActivityAge(agent) ? ` activity=${formatActivityAge(agent)}` : ""}${agent.progress?.currentTool ? ` tool=${agent.progress.currentTool}` : ""}${agent.toolUses ? ` tools=${agent.toolUses}` : ""}${agent.progress?.tokens ? ` tokens=${agent.progress.tokens}` : ""}${agent.progress?.turns ? ` turns=${agent.progress.turns}` : ""}${agent.jsonEvents !== undefined ? ` jsonEvents=${agent.jsonEvents}` : ""}${agent.statusPath ? ` status=${agent.statusPath}` : ""}${agent.error ? ` error=${agent.error}` : ""}`) : ["- (none)"]),
311
323
  "Policy decisions:",
312
324
  ...(manifest.policyDecisions?.length ? manifest.policyDecisions.map((item) => `- ${item.action} (${item.reason})${item.taskId ? ` ${item.taskId}` : ""}: ${item.message}`) : ["- (none)"]),
313
325
  `Total usage: ${formatUsage(totalUsage)}`,
@@ -370,8 +382,9 @@ export async function handleResume(params: TeamToolParamsValue, ctx: TeamContext
370
382
  saveRunTasks(loaded.manifest, resetTasks);
371
383
  appendEvent(loaded.manifest.eventsPath, { type: "run.resume_requested", runId: loaded.manifest.runId });
372
384
  const loadedConfig = loadConfig(ctx.cwd);
373
- const executeWorkers = loadedConfig.config.executeWorkers === true || process.env.PI_TEAMS_EXECUTE_WORKERS === "1";
374
- const executed = await executeTeamRun({ manifest: loaded.manifest, tasks: resetTasks, team, workflow, agents: allAgents(discoverAgents(ctx.cwd)), executeWorkers, limits: loadedConfig.config.limits });
385
+ const runtime = await resolveCrewRuntime(loadedConfig.config);
386
+ const executeWorkers = runtime.kind === "child-process";
387
+ const executed = await executeTeamRun({ manifest: loaded.manifest, tasks: resetTasks, team, workflow, agents: allAgents(discoverAgents(ctx.cwd)), executeWorkers, limits: loadedConfig.config.limits, runtime, runtimeConfig: loadedConfig.config.runtime });
375
388
  return result([`Resumed run ${executed.manifest.runId}.`, `Status: ${executed.manifest.status}`, `Tasks: ${executed.tasks.length}`, `Artifacts: ${executed.manifest.artifactsRoot}`].join("\n"), { action: "resume", status: executed.manifest.status === "failed" ? "error" : "ok", runId: executed.manifest.runId, artifactsRoot: executed.manifest.artifactsRoot }, executed.manifest.status === "failed");
376
389
  });
377
390
  }
@@ -446,12 +459,17 @@ function autonomousPatchFromConfig(config: unknown): PiTeamsAutonomousConfig {
446
459
 
447
460
  function configPatchFromConfig(config: unknown): PiTeamsConfig {
448
461
  const cfg = configRecord(config);
462
+ const control = configRecord(cfg.control);
449
463
  return {
450
464
  asyncByDefault: typeof cfg.asyncByDefault === "boolean" ? cfg.asyncByDefault : undefined,
451
465
  executeWorkers: typeof cfg.executeWorkers === "boolean" ? cfg.executeWorkers : undefined,
452
466
  notifierIntervalMs: typeof cfg.notifierIntervalMs === "number" && Number.isFinite(cfg.notifierIntervalMs) ? cfg.notifierIntervalMs : undefined,
453
467
  requireCleanWorktreeLeader: typeof cfg.requireCleanWorktreeLeader === "boolean" ? cfg.requireCleanWorktreeLeader : undefined,
454
468
  autonomous: typeof cfg.autonomous === "object" && cfg.autonomous !== null && !Array.isArray(cfg.autonomous) ? autonomousPatchFromConfig(cfg.autonomous) : undefined,
469
+ control: Object.keys(control).length > 0 ? {
470
+ enabled: typeof control.enabled === "boolean" ? control.enabled : undefined,
471
+ needsAttentionAfterMs: typeof control.needsAttentionAfterMs === "number" && Number.isInteger(control.needsAttentionAfterMs) && control.needsAttentionAfterMs > 0 ? control.needsAttentionAfterMs : undefined,
472
+ } : undefined,
455
473
  };
456
474
  }
457
475
 
@@ -540,7 +558,7 @@ export function handleCleanup(params: TeamToolParamsValue, ctx: TeamContext): Pi
540
558
  return result(lines.join("\n"), { action: "cleanup", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
541
559
  }
542
560
 
543
- export function handleApi(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
561
+ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
544
562
  if (!params.runId) return result("API requires runId.", { action: "api", status: "error" }, true);
545
563
  const loaded = loadRunManifestById(ctx.cwd, params.runId);
546
564
  if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "api", status: "error" }, true);
@@ -559,7 +577,92 @@ export function handleApi(params: TeamToolParamsValue, ctx: TeamContext): PiTeam
559
577
  return result(JSON.stringify(task, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
560
578
  }
561
579
  if (operation === "read-events") {
562
- return result(JSON.stringify(readEvents(loaded.manifest.eventsPath), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
580
+ const sinceSeq = typeof cfg.sinceSeq === "number" ? cfg.sinceSeq : undefined;
581
+ const limit = typeof cfg.limit === "number" ? cfg.limit : undefined;
582
+ const payload = sinceSeq !== undefined || limit !== undefined
583
+ ? readEventsCursor(loaded.manifest.eventsPath, { sinceSeq, limit })
584
+ : { events: readEvents(loaded.manifest.eventsPath), nextSeq: undefined, total: undefined };
585
+ return result(JSON.stringify(payload, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
586
+ }
587
+ if (operation === "runtime-capabilities") {
588
+ const loadedConfig = loadConfig(ctx.cwd);
589
+ return result(JSON.stringify(await resolveCrewRuntime(loadedConfig.config), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
590
+ }
591
+ if (operation === "probe-live-session") {
592
+ return result(JSON.stringify(await probeLiveSessionRuntime(), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
593
+ }
594
+ if (operation === "list-agents") {
595
+ return result(JSON.stringify(readCrewAgents(loaded.manifest), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
596
+ }
597
+ if (operation === "get-agent-result") {
598
+ const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
599
+ const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
600
+ if (!agent) return result("API get-agent-result requires config.agentId matching an agent id or task id.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
601
+ const task = loaded.tasks.find((item) => item.id === agent.taskId);
602
+ const text = task?.resultArtifact && fs.existsSync(task.resultArtifact.path) ? fs.readFileSync(task.resultArtifact.path, "utf-8") : JSON.stringify(agent, null, 2);
603
+ return result(text, { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
604
+ }
605
+ if (operation === "read-agent-status") {
606
+ const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
607
+ const agent = agentId ? readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId) : undefined;
608
+ const status = agent ? readCrewAgentStatus(loaded.manifest, agent.taskId) ?? agent : undefined;
609
+ if (!status) return result("API read-agent-status requires config.agentId matching an agent id or task id.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
610
+ return result(JSON.stringify(status, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
611
+ }
612
+ if (operation === "read-agent-events") {
613
+ const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
614
+ const agents = readCrewAgents(loaded.manifest);
615
+ const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
616
+ if (!agent) return result("API read-agent-events requires config.agentId matching an agent id or task id, or at least one agent in the run.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
617
+ const sinceSeq = typeof cfg.sinceSeq === "number" ? cfg.sinceSeq : undefined;
618
+ const limit = typeof cfg.limit === "number" ? cfg.limit : undefined;
619
+ const payload = sinceSeq !== undefined || limit !== undefined
620
+ ? readCrewAgentEventsCursor(loaded.manifest, agent.taskId, { sinceSeq, limit })
621
+ : { path: agentEventsPath(loaded.manifest, agent.taskId), events: readCrewAgentEvents(loaded.manifest, agent.taskId) };
622
+ return result(JSON.stringify(payload, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
623
+ }
624
+ if (operation === "read-agent-transcript") {
625
+ const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
626
+ const agents = readCrewAgents(loaded.manifest);
627
+ const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
628
+ if (!agent) return result("API read-agent-transcript requires config.agentId matching an agent id or task id, or at least one agent in the run.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
629
+ const transcriptPath = agent.transcriptPath && fs.existsSync(agent.transcriptPath) ? agent.transcriptPath : agentOutputPath(loaded.manifest, agent.taskId);
630
+ const text = fs.existsSync(transcriptPath) ? fs.readFileSync(transcriptPath, "utf-8") : "";
631
+ return result(text || `(no transcript at ${transcriptPath})`, { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
632
+ }
633
+ if (operation === "read-agent-output") {
634
+ const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
635
+ const agents = readCrewAgents(loaded.manifest);
636
+ const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
637
+ if (!agent) return result("API read-agent-output requires config.agentId matching an agent id or task id, or at least one agent in the run.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
638
+ const maxBytes = typeof cfg.maxBytes === "number" ? cfg.maxBytes : undefined;
639
+ return result(JSON.stringify(readAgentOutput(loaded.manifest, agent.taskId, maxBytes), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
640
+ }
641
+ if (operation === "agent-dashboard") {
642
+ return result(buildAgentDashboard(loaded.manifest).text, { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
643
+ }
644
+ if (operation === "foreground-status") {
645
+ return result(JSON.stringify(readForegroundControlStatus(loaded.manifest, loaded.tasks), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
646
+ }
647
+ if (operation === "foreground-interrupt") {
648
+ const reason = typeof cfg.reason === "string" && cfg.reason.trim() ? cfg.reason.trim() : undefined;
649
+ return result(JSON.stringify(writeForegroundInterruptRequest(loaded.manifest, reason), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
650
+ }
651
+ if (operation === "nudge-agent") {
652
+ const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
653
+ const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
654
+ if (!agent) return result("API nudge-agent requires config.agentId matching an agent id or task id.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
655
+ const messageText = typeof cfg.message === "string" && cfg.message.trim() ? cfg.message.trim() : "Please report your current status, blocker, or smallest next step.";
656
+ const message = appendMailboxMessage(loaded.manifest, { direction: "inbox", from: "leader", to: agent.taskId, taskId: agent.taskId, body: messageText });
657
+ appendEvent(loaded.manifest.eventsPath, { type: "agent.nudged", runId: loaded.manifest.runId, taskId: agent.taskId, message: messageText, data: { agentId: agent.id, mailboxMessageId: message.id } });
658
+ return result(JSON.stringify({ agentId: agent.id, mailboxMessage: message }, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
659
+ }
660
+ if (operation === "steer-agent" || operation === "stop-agent" || operation === "resume-agent" || operation === "interrupt-agent") {
661
+ const runtime = await resolveCrewRuntime(loadConfig(ctx.cwd).config);
662
+ if (!runtime.steer && operation === "steer-agent") return result(`Runtime '${runtime.kind}' does not support live steering. Use nudge-agent for mailbox-based child-process coordination.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
663
+ if (!runtime.resume && operation === "resume-agent") return result(`Runtime '${runtime.kind}' does not support live resume.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
664
+ if (operation === "interrupt-agent" && runtime.kind !== "live-session") return result(`Runtime '${runtime.kind}' does not expose per-agent interrupt yet. Use nudge-agent or cancel the run.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
665
+ return result(`Operation '${operation}' is reserved for live-session runtime and is not active for this run yet.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
563
666
  }
564
667
  if (operation === "read-mailbox") {
565
668
  const direction = cfg.direction === "inbox" || cfg.direction === "outbox" ? cfg.direction as MailboxDirection : undefined;
@@ -762,7 +865,7 @@ export async function handleTeamTool(params: TeamToolParamsValue, ctx: TeamConte
762
865
  }
763
866
  case "doctor": return handleDoctor(ctx, params);
764
867
  case "cleanup": return handleCleanup(params, ctx);
765
- case "api": return handleApi(params, ctx);
868
+ case "api": return await handleApi(params, ctx);
766
869
  case "events": return handleEvents(params, ctx);
767
870
  case "artifacts": return handleArtifacts(params, ctx);
768
871
  case "worktrees": return handleWorktrees(params, ctx);
@@ -2,6 +2,8 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
2
 
3
3
  export const PI_TEAMS_INHERIT_PROJECT_CONTEXT_ENV = "PI_TEAMS_INHERIT_PROJECT_CONTEXT";
4
4
  export const PI_TEAMS_INHERIT_SKILLS_ENV = "PI_TEAMS_INHERIT_SKILLS";
5
+ export const PI_CREW_INHERIT_PROJECT_CONTEXT_ENV = "PI_CREW_INHERIT_PROJECT_CONTEXT";
6
+ export const PI_CREW_INHERIT_SKILLS_ENV = "PI_CREW_INHERIT_SKILLS";
5
7
 
6
8
  const PROJECT_CONTEXT_HEADER = "\n\n# Project Context\n\nProject-specific instructions and guidelines:\n\n";
7
9
  const SKILLS_HEADER = "\n\nThe following skills provide specialized instructions for specific tasks.";
@@ -13,6 +15,14 @@ function readBooleanEnv(name: string): boolean | undefined {
13
15
  return value !== "0";
14
16
  }
15
17
 
18
+ function readBooleanEnvAny(...names: string[]): boolean | undefined {
19
+ for (const name of names) {
20
+ const value = readBooleanEnv(name);
21
+ if (value !== undefined) return value;
22
+ }
23
+ return undefined;
24
+ }
25
+
16
26
  function findSectionEnd(prompt: string, startIndex: number, nextHeaders: string[]): number {
17
27
  let endIndex = prompt.length;
18
28
  for (const header of nextHeaders) {
@@ -45,8 +55,8 @@ export function rewriteTeamWorkerPrompt(prompt: string, options: { inheritProjec
45
55
 
46
56
  export default function registerPiTeamsPromptRuntime(pi: ExtensionAPI): void {
47
57
  pi.on("before_agent_start", (event) => {
48
- const inheritProjectContext = readBooleanEnv(PI_TEAMS_INHERIT_PROJECT_CONTEXT_ENV);
49
- const inheritSkills = readBooleanEnv(PI_TEAMS_INHERIT_SKILLS_ENV);
58
+ const inheritProjectContext = readBooleanEnvAny(PI_CREW_INHERIT_PROJECT_CONTEXT_ENV, PI_TEAMS_INHERIT_PROJECT_CONTEXT_ENV);
59
+ const inheritSkills = readBooleanEnvAny(PI_CREW_INHERIT_SKILLS_ENV, PI_TEAMS_INHERIT_SKILLS_ENV);
50
60
  if (inheritProjectContext === undefined && inheritSkills === undefined) return;
51
61
  const rewritten = rewriteTeamWorkerPrompt(event.systemPrompt, {
52
62
  inheritProjectContext: inheritProjectContext ?? true,