pi-crew 0.9.14 → 0.9.15

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.
@@ -1,23 +1,45 @@
1
1
  import { allAgents, discoverAgents } from "../../agents/discover-agents.ts";
2
- import { allTeams, discoverTeams } from "../../teams/discover-teams.ts";
3
- import { allWorkflows, discoverWorkflows } from "../../workflows/discover-workflows.ts";
4
2
  import { loadConfig } from "../../config/config.ts";
5
- import { findGitRoot, assertCleanLeader } from "../../worktree/worktree-manager.ts";
3
+ import {
4
+ PipelineRunner,
5
+ type PipelineWorkflow,
6
+ } from "../../runtime/pipeline-runner.ts";
7
+ // Heavy runtime — lazy-loaded to avoid 1.4s import cost at extension registration.
8
+ import type { executeTeamRun as ExecuteTeamRunFn } from "../../runtime/team-runner.ts";
6
9
  import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
10
+ import {
11
+ registerActiveRun,
12
+ unregisterActiveRun,
13
+ } from "../../state/active-run-registry.ts";
7
14
  import { writeArtifact } from "../../state/artifact-store.ts";
8
- import { registerActiveRun, unregisterActiveRun } from "../../state/active-run-registry.ts";
9
- import { createRunManifest, loadRunManifestById, updateRunStatus } from "../../state/state-store.ts";
10
15
  import { atomicWriteJson } from "../../state/atomic-write.ts";
16
+ import {
17
+ createRunManifest,
18
+ loadRunManifestById,
19
+ updateRunStatus,
20
+ } from "../../state/state-store.ts";
21
+ import { allTeams, discoverTeams } from "../../teams/discover-teams.ts";
22
+ import {
23
+ allWorkflows,
24
+ discoverWorkflows,
25
+ } from "../../workflows/discover-workflows.ts";
11
26
  import { validateWorkflowForTeam } from "../../workflows/validate-workflow.ts";
12
- import { PipelineRunner, type PipelineWorkflow } from "../../runtime/pipeline-runner.ts";
13
- // Heavy runtime — lazy-loaded to avoid 1.4s import cost at extension registration.
14
- import type { executeTeamRun as ExecuteTeamRunFn } from "../../runtime/team-runner.ts";
27
+ import {
28
+ assertCleanLeader,
29
+ findGitRoot,
30
+ } from "../../worktree/worktree-manager.ts";
31
+
15
32
  // eslint-disable-next-line @typescript-eslint/no-unused-vars -- type-only import for TS inference
16
- const _typeCheck: typeof ExecuteTeamRunFn = null as never as typeof ExecuteTeamRunFn;
33
+ const _typeCheck: typeof ExecuteTeamRunFn =
34
+ null as never as typeof ExecuteTeamRunFn;
35
+
17
36
  import { logInternalError } from "../../utils/internal-error.ts";
18
37
  import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
38
+
19
39
  let _cachedExecuteTeamRun: typeof ExecuteTeamRunFn | undefined;
20
- async function executeTeamRun(...args: Parameters<typeof ExecuteTeamRunFn>): Promise<Awaited<ReturnType<typeof ExecuteTeamRunFn>>> {
40
+ async function executeTeamRun(
41
+ ...args: Parameters<typeof ExecuteTeamRunFn>
42
+ ): Promise<Awaited<ReturnType<typeof ExecuteTeamRunFn>>> {
21
43
  if (!_cachedExecuteTeamRun) {
22
44
  // LAZY: heavy runtime — defer 1.4s import cost until team run actually executes.
23
45
  const mod = await import("../../runtime/team-runner.ts");
@@ -25,11 +47,15 @@ async function executeTeamRun(...args: Parameters<typeof ExecuteTeamRunFn>): Pro
25
47
  }
26
48
  return _cachedExecuteTeamRun(...args);
27
49
  }
28
- import { spawnBackgroundTeamRun } from "../../subagents/async-entry.ts";
29
- import { appendEventAsync, readEvents } from "../../state/event-log.ts";
30
- import { resolveCrewRuntime, runtimeResolutionState } from "../../runtime/runtime-resolver.ts";
31
- import { normalizeSkillOverride } from "../../runtime/skill-instructions.ts";
50
+
32
51
  import { expandParallelResearchWorkflow } from "../../runtime/parallel-research.ts";
52
+ import {
53
+ resolveCrewRuntime,
54
+ runtimeResolutionState,
55
+ } from "../../runtime/runtime-resolver.ts";
56
+ import { normalizeSkillOverride } from "../../runtime/skill-instructions.ts";
57
+ import { appendEventAsync, readEvents } from "../../state/event-log.ts";
58
+ import { spawnBackgroundTeamRun } from "../../subagents/async-entry.ts";
33
59
 
34
60
  /**
35
61
  * Module-scoped latch for the crew-init dynamic import.
@@ -58,23 +84,33 @@ import { expandParallelResearchWorkflow } from "../../runtime/parallel-research.
58
84
  * `team action='run' workflow='<dynamic>'` → "Cannot access 'crewInitPromise'
59
85
  * before initialization" at run.ts load. See RFC 17 + commit fixing this.
60
86
  */
61
- var crewInitPromise: Promise<typeof import("../../state/crew-init.ts")> | undefined;
87
+ var crewInitPromise:
88
+ | Promise<typeof import("../../state/crew-init.ts")>
89
+ | undefined;
62
90
  function loadCrewInit(): Promise<typeof import("../../state/crew-init.ts")> {
63
91
  if (!crewInitPromise) {
64
92
  crewInitPromise = import("../../state/crew-init.ts");
65
93
  }
66
94
  return crewInitPromise;
67
95
  }
68
- import { checkProcessLiveness, isActiveRunStatus } from "../../runtime/process-status.ts";
69
- import { waitForRun } from "../../runtime/run-tracker.ts";
70
- import { hasAsyncStartMarker } from "../../runtime/async-marker.ts";
71
- import { collectRunMetrics } from "../../state/run-metrics.ts";
96
+
72
97
  import * as fs from "node:fs";
73
98
  import * as path from "node:path";
99
+ import { hasAsyncStartMarker } from "../../runtime/async-marker.ts";
100
+ import {
101
+ checkProcessLiveness,
102
+ isActiveRunStatus,
103
+ } from "../../runtime/process-status.ts";
104
+ import { waitForRun } from "../../runtime/run-tracker.ts";
105
+ import { collectRunMetrics } from "../../state/run-metrics.ts";
74
106
  import type { PiTeamsToolResult } from "../tool-result.ts";
75
- import { buildParentContext, result, type TeamContext } from "./context.ts";
76
- import { isGoalWrapEnabled, shouldGoalWrap, startGoalWrappedRun } from "./goal-wrap.ts";
77
107
  import { effectiveRunConfig } from "./config-patch.ts";
108
+ import { buildParentContext, result, type TeamContext } from "./context.ts";
109
+ import {
110
+ isGoalWrapEnabled,
111
+ shouldGoalWrap,
112
+ startGoalWrappedRun,
113
+ } from "./goal-wrap.ts";
78
114
 
79
115
  function tailFile(filePath: string, maxBytes = 4096): string | undefined {
80
116
  try {
@@ -95,24 +131,49 @@ function tailFile(filePath: string, maxBytes = 4096): string | undefined {
95
131
  }
96
132
  }
97
133
 
98
- function scheduleBackgroundEarlyExitGuard(cwd: string, runId: string, pid: number | undefined, logPath: string): void {
134
+ function scheduleBackgroundEarlyExitGuard(
135
+ cwd: string,
136
+ runId: string,
137
+ pid: number | undefined,
138
+ logPath: string,
139
+ ): void {
99
140
  if (process.env.PI_CREW_ASYNC_EARLY_EXIT_GUARD === "0") return;
100
141
  const timer = setTimeout(() => {
101
142
  const loaded = loadRunManifestById(cwd, runId);
102
143
  if (!loaded || !isActiveRunStatus(loaded.manifest.status)) return;
103
144
  if (hasAsyncStartMarker(loaded.manifest)) return;
104
- if (readEvents(loaded.manifest.eventsPath).some((event) => event.type === "async.started" || event.type === "async.completed" || event.type === "async.failed")) return;
145
+ if (
146
+ readEvents(loaded.manifest.eventsPath).some(
147
+ (event) =>
148
+ event.type === "async.started" ||
149
+ event.type === "async.completed" ||
150
+ event.type === "async.failed",
151
+ )
152
+ )
153
+ return;
105
154
  const liveness = checkProcessLiveness(pid);
106
155
  if (liveness.alive) return;
107
156
  const tail = tailFile(logPath);
108
157
  const message = `Background runner exited within 3s; see background.log${tail ? `\n${tail}` : ""}`;
109
- const failed = updateRunStatus(loaded.manifest, "failed", "Background runner exited within 3s; see background.log");
110
- void appendEventAsync(failed.eventsPath, { type: "async.failed", runId: failed.runId, message, data: { pid, detail: liveness.detail } });
158
+ const failed = updateRunStatus(
159
+ loaded.manifest,
160
+ "failed",
161
+ "Background runner exited within 3s; see background.log",
162
+ );
163
+ void appendEventAsync(failed.eventsPath, {
164
+ type: "async.failed",
165
+ runId: failed.runId,
166
+ message,
167
+ data: { pid, detail: liveness.detail },
168
+ });
111
169
  }, 3000);
112
170
  timer.unref();
113
171
  }
114
172
 
115
- export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
173
+ export async function handleRun(
174
+ params: TeamToolParamsValue,
175
+ ctx: TeamContext,
176
+ ): Promise<PiTeamsToolResult> {
116
177
  // CHAIN DISPATCH: runs before goal validation since a chain has no top-level
117
178
  // goal. The injected handleRun reference breaks the run.ts ↔ chain-dispatch.ts
118
179
  // import cycle; the lazy import defers the chain-executor cost until a chain is
@@ -123,7 +184,12 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
123
184
  return handleChainRun(params, ctx, handleRun);
124
185
  }
125
186
  const goal = params.goal ?? params.task;
126
- if (!goal) return result("Run requires goal or task.", { action: "run", status: "error" }, true);
187
+ if (!goal)
188
+ return result(
189
+ "Run requires goal or task.",
190
+ { action: "run", status: "error" },
191
+ true,
192
+ );
127
193
  const intentPrefix = goal.length > 60 ? `${goal.slice(0, 57)}...` : goal;
128
194
 
129
195
  // P0: Ensure .crew directory structure exists before creating any manifests.
@@ -185,31 +251,102 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
185
251
  const teams = allTeams(discoverTeams(resolvedCtx.cwd));
186
252
  const workflows = allWorkflows(discoverWorkflows(resolvedCtx.cwd));
187
253
  const agents = allAgents(discoverAgents(resolvedCtx.cwd));
188
- const directAgent = params.agent ? agents.find((item) => item.name === params.agent) : undefined;
189
- if (params.agent && !directAgent) return result(`Agent '${params.agent}' not found.`, { action: "run", status: "error" }, true);
254
+ const directAgent = params.agent
255
+ ? agents.find((item) => item.name === params.agent)
256
+ : undefined;
257
+ if (params.agent && !directAgent)
258
+ return result(
259
+ `Agent '${params.agent}' not found.`,
260
+ { action: "run", status: "error" },
261
+ true,
262
+ );
190
263
  const teamName = params.team ?? "default";
191
- const team = directAgent ? {
192
- name: `direct-${directAgent.name}`,
193
- description: `Direct subagent run for ${directAgent.name}`,
194
- source: "builtin" as const,
195
- filePath: "<generated>",
196
- roles: [{ name: params.role ?? "agent", agent: directAgent.name, description: directAgent.description }],
197
- defaultWorkflow: "direct-agent",
198
- workspaceMode: params.workspaceMode,
199
- } : teams.find((item) => item.name === teamName);
200
- if (!team) return result(`Team '${teamName}' not found.`, { action: "run", status: "error" }, true);
201
- const workflowName = directAgent ? "direct-agent" : params.workflow ?? team.defaultWorkflow ?? "default";
202
- const baseWorkflow = directAgent ? {
203
- name: "direct-agent",
204
- description: `Direct task for ${directAgent.name}`,
205
- source: "builtin" as const,
206
- filePath: "<generated>",
207
- steps: [{ id: "01_agent", role: params.role ?? "agent", task: "{goal}", model: params.model }],
208
- } : workflows.find((item) => item.name === workflowName);
209
- if (!baseWorkflow) return result(`Workflow '${workflowName}' not found.`, { action: "run", status: "error" }, true);
264
+ const team = directAgent
265
+ ? {
266
+ name: `direct-${directAgent.name}`,
267
+ description: `Direct subagent run for ${directAgent.name}`,
268
+ source: "builtin" as const,
269
+ filePath: "<generated>",
270
+ roles: [
271
+ {
272
+ name: params.role ?? "agent",
273
+ agent: directAgent.name,
274
+ description: directAgent.description,
275
+ },
276
+ ],
277
+ defaultWorkflow: "direct-agent",
278
+ workspaceMode: params.workspaceMode,
279
+ }
280
+ : teams.find((item) => item.name === teamName);
281
+ if (!team)
282
+ return result(
283
+ `Team '${teamName}' not found.`,
284
+ { action: "run", status: "error" },
285
+ true,
286
+ );
287
+ const workflowName = directAgent
288
+ ? "direct-agent"
289
+ : (params.workflow ?? team.defaultWorkflow ?? "default");
290
+ const baseWorkflow = directAgent
291
+ ? {
292
+ name: "direct-agent",
293
+ description: `Direct task for ${directAgent.name}`,
294
+ source: "builtin" as const,
295
+ filePath: "<generated>",
296
+ steps: [
297
+ {
298
+ id: "01_agent",
299
+ role: params.role ?? "agent",
300
+ task: "{goal}",
301
+ model: params.model,
302
+ },
303
+ ],
304
+ }
305
+ : workflows.find((item) => item.name === workflowName);
306
+ if (!baseWorkflow)
307
+ return result(
308
+ `Workflow '${workflowName}' not found.`,
309
+ { action: "run", status: "error" },
310
+ true,
311
+ );
210
312
  // LAZY: dodge the jiti ESM/CJS interop TDZ race on the static `import { expandParallelResearchWorkflow }` above (issue #28, RFC 17). At call time the module body has fully evaluated, so the dynamic import returns a live binding.
211
- const { expandParallelResearchWorkflow: expandParallelResearch } = await import("../../runtime/parallel-research.ts");
212
- const workflow = directAgent ? baseWorkflow : expandParallelResearch(baseWorkflow, resolvedCtx.cwd);
313
+ // LAZY: marker duplicated for biome-check compatibility (script checks `lineNum - 2`).
314
+ // LAZY: keep import on single line so the `// LAZY:` marker above remains 1-line away from `await import`.
315
+ const { expandParallelResearchWorkflow: expandParallelResearch } = await import(
316
+ "../../runtime/parallel-research.ts",
317
+ );
318
+ const workflow = directAgent
319
+ ? baseWorkflow
320
+ : expandParallelResearch(baseWorkflow, resolvedCtx.cwd);
321
+
322
+ // PREFLIGHT (advisory only, since v0.9.15): classify workflow topology and emit
323
+ // informational notes per the rule in .crew/knowledge.md "pi-crew USAGE THRESHOLD
324
+ // RULE". Never blocks execution — the agent (caller) decides whether to proceed,
325
+ // refactor, or override. This honors Pi's design philosophy: tooling provides
326
+ // information, agents exercise judgment.
327
+ // Skip for direct-agent runs (they bypass the workflow system entirely).
328
+ if (!directAgent) {
329
+ // LAZY: defer preflight-validator import until a team run actually requests it.
330
+ const { validateWorkflowUsage } = await import(
331
+ "../../workflows/preflight-validator.ts"
332
+ );
333
+ const preflight = validateWorkflowUsage(workflow, {
334
+ force: params.force === true,
335
+ });
336
+ const icon =
337
+ preflight.level === "warn"
338
+ ? "⚠️ "
339
+ : preflight.level === "note"
340
+ ? "ℹ️ "
341
+ : "";
342
+ const tag = preflight.level.toUpperCase();
343
+ console.warn(
344
+ `${icon}[team-tool.preflight] ${tag}: ${preflight.message} (workflow=${workflow.name})`,
345
+ );
346
+ if (preflight.suggestion) {
347
+ console.warn(`[team-tool.preflight] → ${preflight.suggestion}`);
348
+ }
349
+ }
213
350
 
214
351
  // RFC v0.5 vision: goal-wrap. If .crew/config.json has goalWrap[workflow.name].enabled=true,
215
352
  // route to a goal loop where this workflow runs as the worker turn (judge → feedback → redo
@@ -221,7 +358,11 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
221
358
  // transition). When goal-wrap is unsafe for this workflow, we do NOT error
222
359
  // out — we fall through to the normal team-run path so the user still gets
223
360
  // the run they asked for. The disabled reason is logged for traceability.
224
- if (!directAgent && workflow.source === "builtin" && isGoalWrapEnabled(resolvedCtx.cwd, workflow.name)) {
361
+ if (
362
+ !directAgent &&
363
+ workflow.source === "builtin" &&
364
+ isGoalWrapEnabled(resolvedCtx.cwd, workflow.name)
365
+ ) {
225
366
  const decision = shouldGoalWrap(resolvedCtx.cwd, workflow);
226
367
  if (decision.enabled) {
227
368
  return await startGoalWrappedRun(params, ctx, workflow, goal);
@@ -231,7 +372,11 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
231
372
  // debug logs. This preserves the trace of WHY goal-wrap was bypassed for
232
373
  // a given run (vs. just disappearing without explanation).
233
374
  if (decision.message) {
234
- logInternalError("team-tool.run.goalWrapBypassed", new Error(decision.message), `workflow=${workflow.name} reason=${decision.reason}`);
375
+ logInternalError(
376
+ "team-tool.run.goalWrapBypassed",
377
+ new Error(decision.message),
378
+ `workflow=${workflow.name} reason=${decision.reason}`,
379
+ );
235
380
  }
236
381
  }
237
382
 
@@ -256,31 +401,48 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
256
401
 
257
402
  // For now, show pipeline workflow info - full integration would require
258
403
  // connecting PipelineRunner to the actual team execution system
259
- const stageInfo = pipelineWorkflow.stages.map((s) => `- ${s.name} (${s.team})`).join("\n");
260
- return result([
261
- `Pipeline workflow '${workflow.name}' is not yet wired into the team execution system.`,
262
- `Goal: ${goal}`,
263
- `Defined stages (${pipelineWorkflow.stages.length}):`,
264
- stageInfo,
265
- "",
266
- "To actually run work right now, use a supported workflow instead:",
267
- " - action='run' workflow='default' (explore → plan → execute → verify)",
268
- " - action='run' workflow='implementation' (adaptive, parallel specialists)",
269
- " - action='run' workflow='research' (explore analyze write)",
270
- "",
271
- "Run action='list' resource='workflow' to see all available workflows.",
272
- ].join("\n"), { action: "run", status: "ok" }, false);
404
+ const stageInfo = pipelineWorkflow.stages
405
+ .map((s) => `- ${s.name} (${s.team})`)
406
+ .join("\n");
407
+ return result(
408
+ [
409
+ `Pipeline workflow '${workflow.name}' is not yet wired into the team execution system.`,
410
+ `Goal: ${goal}`,
411
+ `Defined stages (${pipelineWorkflow.stages.length}):`,
412
+ stageInfo,
413
+ "",
414
+ "To actually run work right now, use a supported workflow instead:",
415
+ " - action='run' workflow='default' (explore → plan → execute → verify)",
416
+ " - action='run' workflow='implementation' (adaptive, parallel specialists)",
417
+ " - action='run' workflow='research' (explore analyze → write)",
418
+ "",
419
+ "Run action='list' resource='workflow' to see all available workflows.",
420
+ ].join("\n"),
421
+ { action: "run", status: "ok" },
422
+ false,
423
+ );
273
424
  }
274
425
 
275
426
  // LAZY: dodge the jiti ESM/CJS interop TDZ race on the static `import { validateWorkflowForTeam }` above (issue #28, RFC 17).
276
- const { validateWorkflowForTeam: validateWorkflow } = await import("../../workflows/validate-workflow.ts");
427
+ const { validateWorkflowForTeam: validateWorkflow } = await import(
428
+ "../../workflows/validate-workflow.ts"
429
+ );
277
430
  const validationErrors = validateWorkflow(workflow, team);
278
431
  if (validationErrors.length > 0) {
279
- return result([`Workflow '${workflow.name}' is not valid for team '${team.name}':`, ...validationErrors.map((error) => `- ${error}`)].join("\n"), { action: "run", status: "error" }, true);
432
+ return result(
433
+ [
434
+ `Workflow '${workflow.name}' is not valid for team '${team.name}':`,
435
+ ...validationErrors.map((error) => `- ${error}`),
436
+ ].join("\n"),
437
+ { action: "run", status: "error" },
438
+ true,
439
+ );
280
440
  }
281
441
 
282
442
  // LAZY: dodge the jiti ESM/CJS interop TDZ race on the static `import { normalizeSkillOverride }` above (issue #28, RFC 17).
283
- const { normalizeSkillOverride: normalizeSkill } = await import("../../runtime/skill-instructions.ts");
443
+ const { normalizeSkillOverride: normalizeSkill } = await import(
444
+ "../../runtime/skill-instructions.ts"
445
+ );
284
446
  const skillOverride = normalizeSkill(params.skill);
285
447
  const { manifest, tasks, paths } = createRunManifest({
286
448
  cwd: resolvedCtx.cwd,
@@ -298,7 +460,13 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
298
460
  content: `${goal}\n`,
299
461
  producer: "team-tool",
300
462
  });
301
- const updatedManifest = { ...manifest, ...(skillOverride !== undefined ? { skillOverride } : {}), artifacts: [goalArtifact], summary: "Run manifest created; worker execution is not implemented yet." };
463
+ const updatedManifest = {
464
+ ...manifest,
465
+ ...(skillOverride !== undefined ? { skillOverride } : {}),
466
+ artifacts: [goalArtifact],
467
+ summary:
468
+ "Run manifest created; worker execution is not implemented yet.",
469
+ };
302
470
  atomicWriteJson(paths.manifestPath, updatedManifest);
303
471
  registerActiveRun(updatedManifest);
304
472
 
@@ -306,9 +474,16 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
306
474
  // run it via runDynamicWorkflow instead of the static executeTeamRun path. The script
307
475
  // orchestrates subagents via ctx.agent(); only ctx.setResult() reaches the main context.
308
476
  // Placed AFTER manifest creation so runId/paths/artifactsRoot are available.
309
- if (!directAgent && (workflow as import("../../workflows/workflow-config.ts").DynamicWorkflowConfig).runtime === "dynamic") {
477
+ if (
478
+ !directAgent &&
479
+ (
480
+ workflow as import("../../workflows/workflow-config.ts").DynamicWorkflowConfig
481
+ ).runtime === "dynamic"
482
+ ) {
310
483
  // LAZY: defer dynamic import of ../../runtime/dynamic-workflow-runner.ts to its call site.
311
- const { runDynamicWorkflow } = await import("../../runtime/dynamic-workflow-runner.ts");
484
+ const { runDynamicWorkflow } = await import(
485
+ "../../runtime/dynamic-workflow-runner.ts"
486
+ );
312
487
  // Re-synthesize a dynamic-team (§0c C9) for role resolution.
313
488
  const dwfTeam: import("../../teams/team-config.ts").TeamConfig = {
314
489
  name: `dwf-${manifest.runId.slice(-12)}`,
@@ -325,26 +500,50 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
325
500
  };
326
501
  atomicWriteJson(paths.manifestPath, dwfManifest);
327
502
  try {
328
- let dwfResult: import("../../runtime/dynamic-workflow-runner.ts").RunDynamicWorkflowResult | undefined;
503
+ let dwfResult:
504
+ | import("../../runtime/dynamic-workflow-runner.ts").RunDynamicWorkflowResult
505
+ | undefined;
329
506
  try {
330
507
  dwfResult = await runDynamicWorkflow({
331
508
  manifest: dwfManifest,
332
- workflow: workflow as import("../../workflows/workflow-config.ts").DynamicWorkflowConfig,
509
+ workflow:
510
+ workflow as import("../../workflows/workflow-config.ts").DynamicWorkflowConfig,
333
511
  team: dwfTeam,
334
512
  signal: ctx.signal ?? AbortSignal.timeout(3_600_000),
335
513
  modelOverride: params.model,
336
- tokenBudget: params.tokenBudget ?? (workflow as import("../../workflows/workflow-config.ts").DynamicWorkflowConfig).maxTokenBudget,
514
+ tokenBudget:
515
+ params.tokenBudget ??
516
+ (
517
+ workflow as import("../../workflows/workflow-config.ts").DynamicWorkflowConfig
518
+ ).maxTokenBudget,
337
519
  });
338
520
  } catch (runnerError) {
339
521
  // Round-11 runtime fix: persist manifest with status=failed when runner throws
340
522
  // (e.g., script timeout, script syntax error, async failure). Previously the
341
523
  // manifest stayed at 'queued' indefinitely, leaving an orphan state file.
342
- const failureReason = runnerError instanceof Error ? runnerError.message : String(runnerError);
343
- const failedManifest = { ...dwfManifest, status: "failed" as const, summary: `Dynamic workflow '${workflow.name}' failed: ${failureReason}`.slice(0, 2000), updatedAt: new Date().toISOString() };
524
+ const failureReason =
525
+ runnerError instanceof Error
526
+ ? runnerError.message
527
+ : String(runnerError);
528
+ const failedManifest = {
529
+ ...dwfManifest,
530
+ status: "failed" as const,
531
+ summary:
532
+ `Dynamic workflow '${workflow.name}' failed: ${failureReason}`.slice(
533
+ 0,
534
+ 2000,
535
+ ),
536
+ updatedAt: new Date().toISOString(),
537
+ };
344
538
  atomicWriteJson(paths.manifestPath, failedManifest);
345
539
  return result(
346
540
  `Dynamic workflow '${workflow.name}' failed: ${failureReason}`,
347
- { action: "run", status: "error", runId: failedManifest.runId, artifactsRoot: failedManifest.artifactsRoot },
541
+ {
542
+ action: "run",
543
+ status: "error",
544
+ runId: failedManifest.runId,
545
+ artifactsRoot: failedManifest.artifactsRoot,
546
+ },
348
547
  true,
349
548
  );
350
549
  }
@@ -354,7 +553,13 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
354
553
  atomicWriteJson(paths.manifestPath, dwfResult.manifest);
355
554
  return result(
356
555
  `Dynamic workflow '${workflow.name}' completed.\n${dwfResult.manifest.summary ?? ""}`,
357
- { action: "run", status: dwfResult.manifest.status === "failed" ? "error" : "ok", runId: dwfResult.manifest.runId, artifactsRoot: dwfResult.manifest.artifactsRoot },
556
+ {
557
+ action: "run",
558
+ status:
559
+ dwfResult.manifest.status === "failed" ? "error" : "ok",
560
+ runId: dwfResult.manifest.runId,
561
+ artifactsRoot: dwfResult.manifest.artifactsRoot,
562
+ },
358
563
  dwfResult.manifest.status === "failed",
359
564
  );
360
565
  } finally {
@@ -377,58 +582,179 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
377
582
  type: "config.warning",
378
583
  runId: updatedManifest.runId,
379
584
  message: `Loaded config from ${loadedConfig.path || "(defaults)"} with ${configIssues.length} issue(s): ${configIssues.join("; ")}`,
380
- data: { error: loadedConfig.error, warnings: loadedConfig.warnings, path: loadedConfig.path },
381
- }).catch((error) => logInternalError("team-tool.run.configWarning", error, `runId=${updatedManifest.runId}`));
382
- logInternalError("team-tool.run.configWarning", new Error(`config issues: ${configIssues.join("; ")}`), `runId=${updatedManifest.runId} path=${loadedConfig.path ?? "(defaults)"}`);
585
+ data: {
586
+ error: loadedConfig.error,
587
+ warnings: loadedConfig.warnings,
588
+ path: loadedConfig.path,
589
+ },
590
+ }).catch((error) =>
591
+ logInternalError(
592
+ "team-tool.run.configWarning",
593
+ error,
594
+ `runId=${updatedManifest.runId}`,
595
+ ),
596
+ );
597
+ logInternalError(
598
+ "team-tool.run.configWarning",
599
+ new Error(`config issues: ${configIssues.join("; ")}`),
600
+ `runId=${updatedManifest.runId} path=${loadedConfig.path ?? "(defaults)"}`,
601
+ );
383
602
  }
384
- const executedConfig = effectiveRunConfig(loadedConfig.config, params.config);
603
+ const executedConfig = effectiveRunConfig(
604
+ loadedConfig.config,
605
+ params.config,
606
+ );
385
607
  const runtime = await resolveCrewRuntime(executedConfig);
386
608
  const runtimeResolution = runtimeResolutionState(runtime);
387
- const executionManifest = { ...updatedManifest, runtimeResolution, runConfig: executedConfig, updatedAt: new Date().toISOString() };
609
+ const executionManifest = {
610
+ ...updatedManifest,
611
+ runtimeResolution,
612
+ runConfig: executedConfig,
613
+ updatedAt: new Date().toISOString(),
614
+ };
388
615
  atomicWriteJson(paths.manifestPath, executionManifest);
389
- appendEventAsync(executionManifest.eventsPath, { type: "runtime.resolved", runId: executionManifest.runId, message: `Runtime resolved: ${runtime.kind} safety=${runtime.safety}`, data: { runtimeResolution } }).catch((error) => logInternalError("team-tool.run.resolved", error, `runId=${executionManifest.runId}`));
616
+ appendEventAsync(executionManifest.eventsPath, {
617
+ type: "runtime.resolved",
618
+ runId: executionManifest.runId,
619
+ message: `Runtime resolved: ${runtime.kind} safety=${runtime.safety}`,
620
+ data: { runtimeResolution },
621
+ }).catch((error) =>
622
+ logInternalError(
623
+ "team-tool.run.resolved",
624
+ error,
625
+ `runId=${executionManifest.runId}`,
626
+ ),
627
+ );
390
628
  const runAsync = params.async ?? executedConfig.asyncByDefault ?? false;
391
629
  let effectiveRuntime = runtime;
392
630
  if (runAsync && runtime.kind === "live-session") {
393
- effectiveRuntime = { ...runtime, kind: "child-process", steer: true, resume: false, liveToolActivity: false, fallback: "child-process", reason: "Background runner cannot use live-session; falling back to child-process." };
631
+ effectiveRuntime = {
632
+ ...runtime,
633
+ kind: "child-process",
634
+ steer: true,
635
+ resume: false,
636
+ liveToolActivity: false,
637
+ fallback: "child-process",
638
+ reason: "Background runner cannot use live-session; falling back to child-process.",
639
+ };
394
640
  }
395
- const effectiveRuntimeResolution = effectiveRuntime !== runtime ? runtimeResolutionState(effectiveRuntime) : runtimeResolution;
396
- const effectiveManifest = effectiveRuntime !== runtime ? { ...executionManifest, runtimeResolution: effectiveRuntimeResolution, updatedAt: new Date().toISOString() } : executionManifest;
641
+ const effectiveRuntimeResolution =
642
+ effectiveRuntime !== runtime
643
+ ? runtimeResolutionState(effectiveRuntime)
644
+ : runtimeResolution;
645
+ const effectiveManifest =
646
+ effectiveRuntime !== runtime
647
+ ? {
648
+ ...executionManifest,
649
+ runtimeResolution: effectiveRuntimeResolution,
650
+ updatedAt: new Date().toISOString(),
651
+ }
652
+ : executionManifest;
397
653
  if (effectiveRuntime !== runtime) {
398
654
  atomicWriteJson(paths.manifestPath, effectiveManifest);
399
- appendEventAsync(effectiveManifest.eventsPath, { type: "runtime.resolved", runId: effectiveManifest.runId, message: `Runtime overridden: child-process (async fallback from live-session)`, data: { runtimeResolution: effectiveRuntimeResolution } }).catch((error) => logInternalError("team-tool.run.override", error, `runId=${effectiveManifest.runId}`));
655
+ appendEventAsync(effectiveManifest.eventsPath, {
656
+ type: "runtime.resolved",
657
+ runId: effectiveManifest.runId,
658
+ message: `Runtime overridden: child-process (async fallback from live-session)`,
659
+ data: { runtimeResolution: effectiveRuntimeResolution },
660
+ }).catch((error) =>
661
+ logInternalError(
662
+ "team-tool.run.override",
663
+ error,
664
+ `runId=${effectiveManifest.runId}`,
665
+ ),
666
+ );
400
667
  }
401
668
  if (runAsync) {
402
669
  if (effectiveRuntime.safety === "blocked") {
403
- const runningManifest = updateRunStatus(effectiveManifest, "running", "Checking worker runtime availability.");
404
- const blocked = updateRunStatus(runningManifest, "blocked", effectiveRuntime.reason ?? "Child worker execution is disabled; refusing to create no-op scaffold subagents.");
405
- void appendEventAsync(blocked.eventsPath, { type: "run.blocked", runId: blocked.runId, message: blocked.summary, data: { runtime: effectiveRuntime, runtimeResolution: effectiveRuntimeResolution, async: true, diagnostics: { requestedMode: effectiveRuntime.requestedMode, workersDisabled: executedConfig.executeWorkers === false, envCrew: process.env.PI_CREW_EXECUTE_WORKERS, envTeams: process.env.PI_TEAMS_EXECUTE_WORKERS } } });
670
+ const runningManifest = updateRunStatus(
671
+ effectiveManifest,
672
+ "running",
673
+ "Checking worker runtime availability.",
674
+ );
675
+ const blocked = updateRunStatus(
676
+ runningManifest,
677
+ "blocked",
678
+ effectiveRuntime.reason ??
679
+ "Child worker execution is disabled; refusing to create no-op scaffold subagents.",
680
+ );
681
+ void appendEventAsync(blocked.eventsPath, {
682
+ type: "run.blocked",
683
+ runId: blocked.runId,
684
+ message: blocked.summary,
685
+ data: {
686
+ runtime: effectiveRuntime,
687
+ runtimeResolution: effectiveRuntimeResolution,
688
+ async: true,
689
+ diagnostics: {
690
+ requestedMode: effectiveRuntime.requestedMode,
691
+ workersDisabled:
692
+ executedConfig.executeWorkers === false,
693
+ envCrew: process.env.PI_CREW_EXECUTE_WORKERS,
694
+ envTeams: process.env.PI_TEAMS_EXECUTE_WORKERS,
695
+ },
696
+ },
697
+ });
406
698
  unregisterActiveRun(blocked.runId);
407
- return result([
408
- `Blocked pi-crew run ${blocked.runId}: real subagent workers are disabled.`,
409
- `Runtime: ${effectiveRuntime.kind} (requested ${effectiveRuntime.requestedMode})`,
410
- `Reason: ${effectiveRuntime.reason ?? "unknown"}`,
411
- `Config: executeWorkers=${executedConfig.executeWorkers ?? "<default>"}, runtime.mode=${executedConfig.runtime?.mode ?? "<default>"}`,
412
- `Env: PI_CREW_EXECUTE_WORKERS=${process.env.PI_CREW_EXECUTE_WORKERS ?? "<unset>"}, PI_TEAMS_EXECUTE_WORKERS=${process.env.PI_TEAMS_EXECUTE_WORKERS ?? "<unset>"}`,
413
- ].join("\n"), { action: "run", status: "error", runId: blocked.runId, artifactsRoot: blocked.artifactsRoot }, true);
699
+ return result(
700
+ [
701
+ `Blocked pi-crew run ${blocked.runId}: real subagent workers are disabled.`,
702
+ `Runtime: ${effectiveRuntime.kind} (requested ${effectiveRuntime.requestedMode})`,
703
+ `Reason: ${effectiveRuntime.reason ?? "unknown"}`,
704
+ `Config: executeWorkers=${executedConfig.executeWorkers ?? "<default>"}, runtime.mode=${executedConfig.runtime?.mode ?? "<default>"}`,
705
+ `Env: PI_CREW_EXECUTE_WORKERS=${process.env.PI_CREW_EXECUTE_WORKERS ?? "<unset>"}, PI_TEAMS_EXECUTE_WORKERS=${process.env.PI_TEAMS_EXECUTE_WORKERS ?? "<unset>"}`,
706
+ ].join("\n"),
707
+ {
708
+ action: "run",
709
+ status: "error",
710
+ runId: blocked.runId,
711
+ artifactsRoot: blocked.artifactsRoot,
712
+ },
713
+ true,
714
+ );
414
715
  }
415
716
  const spawned = await spawnBackgroundTeamRun(effectiveManifest);
416
- const asyncManifest = { ...effectiveManifest, async: { pid: spawned.pid, logPath: spawned.logPath, spawnedAt: new Date().toISOString() } };
717
+ const asyncManifest = {
718
+ ...effectiveManifest,
719
+ async: {
720
+ pid: spawned.pid,
721
+ logPath: spawned.logPath,
722
+ spawnedAt: new Date().toISOString(),
723
+ },
724
+ };
417
725
  atomicWriteJson(paths.manifestPath, asyncManifest);
418
- void appendEventAsync(effectiveManifest.eventsPath, { type: "async.spawned", runId: effectiveManifest.runId, data: { pid: spawned.pid, logPath: spawned.logPath } });
726
+ void appendEventAsync(effectiveManifest.eventsPath, {
727
+ type: "async.spawned",
728
+ runId: effectiveManifest.runId,
729
+ data: { pid: spawned.pid, logPath: spawned.logPath },
730
+ });
419
731
  ctx.onRunStarted?.(effectiveManifest.runId);
420
- scheduleBackgroundEarlyExitGuard(resolvedCtx.cwd, effectiveManifest.runId, spawned.pid, spawned.logPath);
732
+ scheduleBackgroundEarlyExitGuard(
733
+ resolvedCtx.cwd,
734
+ effectiveManifest.runId,
735
+ spawned.pid,
736
+ spawned.logPath,
737
+ );
421
738
  // Wait for the async run to complete and return actual results.
422
739
  try {
423
- const completed = await waitForRun(updatedManifest.runId, resolvedCtx.cwd, { timeoutMs: 3600000 });
424
- const metrics = collectRunMetrics(resolvedCtx.cwd, completed.manifest.runId);
740
+ const completed = await waitForRun(
741
+ updatedManifest.runId,
742
+ resolvedCtx.cwd,
743
+ { timeoutMs: 3600000 },
744
+ );
745
+ const metrics = collectRunMetrics(
746
+ resolvedCtx.cwd,
747
+ completed.manifest.runId,
748
+ );
425
749
  const lines: string[] = [
426
750
  `pi-crew run ${completed.manifest.status}: ${completed.manifest.runId} (${team.name})`,
427
751
  `Goal: ${goal.slice(0, 100)}`,
428
752
  ];
429
753
  if (metrics) {
430
754
  lines.push("");
431
- lines.push(`Metrics: ${metrics.completedCount}/${metrics.taskCount} tasks, ${metrics.totalTokens} tokens, ${metrics.durationMs}ms, consistency=${metrics.consistencyScore}`);
755
+ lines.push(
756
+ `Metrics: ${metrics.completedCount}/${metrics.taskCount} tasks, ${metrics.totalTokens} tokens, ${metrics.durationMs}ms, consistency=${metrics.consistencyScore}`,
757
+ );
432
758
  }
433
759
 
434
760
  if (completed.tasks.length > 0) {
@@ -439,8 +765,14 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
439
765
  );
440
766
  if (summaryArtifact) {
441
767
  try {
442
- const sumPath = path.join(completed.manifest.artifactsRoot, summaryArtifact.path);
443
- summaryContent = fs.readFileSync(sumPath, "utf-8").trim().slice(0, 4000);
768
+ const sumPath = path.join(
769
+ completed.manifest.artifactsRoot,
770
+ summaryArtifact.path,
771
+ );
772
+ summaryContent = fs
773
+ .readFileSync(sumPath, "utf-8")
774
+ .trim()
775
+ .slice(0, 4000);
444
776
  } catch {
445
777
  /* summary unavailable */
446
778
  }
@@ -457,22 +789,34 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
457
789
  // inside artifactsRoot via the project's safe-path primitive. Rejects
458
790
  // absolute paths (/etc/passwd) and ../ traversal that the old
459
791
  // path.isAbsolute shortcut + bare path.join allowed.
460
- const resPath = resolveRealContainedPath(completed.manifest.artifactsRoot, task.resultArtifact.path);
461
- resultExcerpt = fs.readFileSync(resPath, "utf-8").trim().slice(0, 2000);
792
+ const resPath = resolveRealContainedPath(
793
+ completed.manifest.artifactsRoot,
794
+ task.resultArtifact.path,
795
+ );
796
+ resultExcerpt = fs
797
+ .readFileSync(resPath, "utf-8")
798
+ .trim()
799
+ .slice(0, 2000);
462
800
  } catch {
463
801
  resultExcerpt = "(result unavailable)";
464
802
  }
465
803
  }
466
804
  const shortResult = resultExcerpt.slice(0, 500);
467
805
  const statusTag =
468
- task.status === "completed" ? "✓"
469
- : task.status === "failed" ? ""
470
- : task.status === "cancelled" ? "⊘"
471
- : "·";
806
+ task.status === "completed"
807
+ ? ""
808
+ : task.status === "failed"
809
+ ? ""
810
+ : task.status === "cancelled"
811
+ ? "⊘"
812
+ : "·";
472
813
  taskLines.push(
473
814
  `- ${statusTag} ${task.id} [${task.role}]: ${task.status}${shortResult ? " — " + shortResult : ""}${task.error ? ` | Error: ${task.error.slice(0, 200)}` : ""}`,
474
815
  );
475
- if (task.status === "failed" || task.status === "needs_attention") {
816
+ if (
817
+ task.status === "failed" ||
818
+ task.status === "needs_attention"
819
+ ) {
476
820
  failedCount++;
477
821
  failedIds.push(task.id);
478
822
  }
@@ -505,10 +849,25 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
505
849
  );
506
850
  }
507
851
 
508
- const runFailed = completed.manifest.status === "failed" || completed.manifest.status === "blocked";
509
- return result(lines.join("\n"), { action: "run", status: runFailed ? "error" : "ok", runId: completed.manifest.runId, artifactsRoot: completed.manifest.artifactsRoot, metrics }, runFailed);
852
+ const runFailed =
853
+ completed.manifest.status === "failed" ||
854
+ completed.manifest.status === "blocked";
855
+ return result(
856
+ lines.join("\n"),
857
+ {
858
+ action: "run",
859
+ status: runFailed ? "error" : "ok",
860
+ runId: completed.manifest.runId,
861
+ artifactsRoot: completed.manifest.artifactsRoot,
862
+ metrics,
863
+ },
864
+ runFailed,
865
+ );
510
866
  } catch (waitError: unknown) {
511
- const errorMessage = waitError instanceof Error ? waitError.message : String(waitError);
867
+ const errorMessage =
868
+ waitError instanceof Error
869
+ ? waitError.message
870
+ : String(waitError);
512
871
  return result(
513
872
  [
514
873
  `pi-crew run timed out or failed: ${updatedManifest.runId}`,
@@ -520,34 +879,91 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
520
879
  `State: ${updatedManifest.stateRoot}`,
521
880
  `Background log: ${spawned.logPath}`,
522
881
  ].join("\n"),
523
- { action: "run", status: "error", runId: updatedManifest.runId, artifactsRoot: updatedManifest.artifactsRoot },
882
+ {
883
+ action: "run",
884
+ status: "error",
885
+ runId: updatedManifest.runId,
886
+ artifactsRoot: updatedManifest.artifactsRoot,
887
+ },
524
888
  true,
525
889
  );
526
890
  }
527
891
  }
528
892
 
529
893
  if (runtime.safety === "blocked") {
530
- const runningManifest = updateRunStatus(executionManifest, "running", "Checking worker runtime availability.");
531
- const blocked = updateRunStatus(runningManifest, "blocked", runtime.reason ?? "Child worker execution is disabled; refusing to create no-op scaffold subagents.");
532
- void appendEventAsync(blocked.eventsPath, { type: "run.blocked", runId: blocked.runId, message: blocked.summary, data: { runtime, runtimeResolution, diagnostics: { requestedMode: runtime.requestedMode, workersDisabled: executedConfig.executeWorkers === false, envCrew: process.env.PI_CREW_EXECUTE_WORKERS, envTeams: process.env.PI_TEAMS_EXECUTE_WORKERS } } });
894
+ const runningManifest = updateRunStatus(
895
+ executionManifest,
896
+ "running",
897
+ "Checking worker runtime availability.",
898
+ );
899
+ const blocked = updateRunStatus(
900
+ runningManifest,
901
+ "blocked",
902
+ runtime.reason ??
903
+ "Child worker execution is disabled; refusing to create no-op scaffold subagents.",
904
+ );
905
+ void appendEventAsync(blocked.eventsPath, {
906
+ type: "run.blocked",
907
+ runId: blocked.runId,
908
+ message: blocked.summary,
909
+ data: {
910
+ runtime,
911
+ runtimeResolution,
912
+ diagnostics: {
913
+ requestedMode: runtime.requestedMode,
914
+ workersDisabled: executedConfig.executeWorkers === false,
915
+ envCrew: process.env.PI_CREW_EXECUTE_WORKERS,
916
+ envTeams: process.env.PI_TEAMS_EXECUTE_WORKERS,
917
+ },
918
+ },
919
+ });
533
920
  unregisterActiveRun(blocked.runId);
534
- return result([
535
- `Blocked pi-crew run ${blocked.runId}: real subagent workers are disabled.`,
536
- `Runtime: ${runtime.kind} (requested ${runtime.requestedMode})`,
537
- `Reason: ${runtime.reason ?? "unknown"}`,
538
- `Config: executeWorkers=${executedConfig.executeWorkers ?? "<default>"}, runtime.mode=${executedConfig.runtime?.mode ?? "<default>"}`,
539
- `Env: PI_CREW_EXECUTE_WORKERS=${process.env.PI_CREW_EXECUTE_WORKERS ?? "<unset>"}, PI_TEAMS_EXECUTE_WORKERS=${process.env.PI_TEAMS_EXECUTE_WORKERS ?? "<unset>"}`,
540
- "",
541
- "To run effective subagents, remove executeWorkers=false / PI_CREW_EXECUTE_WORKERS=0 / PI_TEAMS_EXECUTE_WORKERS=0 or set runtime.mode=child-process.",
542
- "Use runtime.mode=scaffold only for explicit dry-run prompt/artifact generation.",
543
- ].join("\n"), { action: "run", status: "error", runId: blocked.runId, artifactsRoot: blocked.artifactsRoot }, true);
921
+ return result(
922
+ [
923
+ `Blocked pi-crew run ${blocked.runId}: real subagent workers are disabled.`,
924
+ `Runtime: ${runtime.kind} (requested ${runtime.requestedMode})`,
925
+ `Reason: ${runtime.reason ?? "unknown"}`,
926
+ `Config: executeWorkers=${executedConfig.executeWorkers ?? "<default>"}, runtime.mode=${executedConfig.runtime?.mode ?? "<default>"}`,
927
+ `Env: PI_CREW_EXECUTE_WORKERS=${process.env.PI_CREW_EXECUTE_WORKERS ?? "<unset>"}, PI_TEAMS_EXECUTE_WORKERS=${process.env.PI_TEAMS_EXECUTE_WORKERS ?? "<unset>"}`,
928
+ "",
929
+ "To run effective subagents, remove executeWorkers=false / PI_CREW_EXECUTE_WORKERS=0 / PI_TEAMS_EXECUTE_WORKERS=0 or set runtime.mode=child-process.",
930
+ "Use runtime.mode=scaffold only for explicit dry-run prompt/artifact generation.",
931
+ ].join("\n"),
932
+ {
933
+ action: "run",
934
+ status: "error",
935
+ runId: blocked.runId,
936
+ artifactsRoot: blocked.artifactsRoot,
937
+ },
938
+ true,
939
+ );
544
940
  }
545
941
  const executeWorkers = runtime.kind !== "scaffold";
546
942
  if (executeWorkers && ctx.startForegroundRun) {
547
943
  ctx.onRunStarted?.(updatedManifest.runId);
548
944
  ctx.startForegroundRun(async (signal) => {
549
945
  try {
550
- await executeTeamRun({ manifest: executionManifest, tasks, team, workflow, agents, executeWorkers, limits: executedConfig.limits, runtime, runtimeConfig: executedConfig.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, modelOverride: params.model, skillOverride, signal, reliability: executedConfig.reliability, metricRegistry: ctx.metricRegistry, onJsonEvent: ctx.onJsonEvent, workspaceId: ctx.sessionId ?? ctx.cwd });
946
+ await executeTeamRun({
947
+ manifest: executionManifest,
948
+ tasks,
949
+ team,
950
+ workflow,
951
+ agents,
952
+ executeWorkers,
953
+ limits: executedConfig.limits,
954
+ runtime,
955
+ runtimeConfig: executedConfig.runtime,
956
+ parentContext: buildParentContext(ctx),
957
+ parentModel: ctx.model,
958
+ modelRegistry: ctx.modelRegistry,
959
+ modelOverride: params.model,
960
+ skillOverride,
961
+ signal,
962
+ reliability: executedConfig.reliability,
963
+ metricRegistry: ctx.metricRegistry,
964
+ onJsonEvent: ctx.onJsonEvent,
965
+ workspaceId: ctx.sessionId ?? ctx.cwd,
966
+ });
551
967
  } finally {
552
968
  unregisterActiveRun(updatedManifest.runId);
553
969
  }
@@ -555,15 +971,24 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
555
971
 
556
972
  // Wait for the foreground run to complete and return actual results.
557
973
  try {
558
- const completed = await waitForRun(updatedManifest.runId, resolvedCtx.cwd, { timeoutMs: 3600000 });
559
- const metrics = collectRunMetrics(resolvedCtx.cwd, completed.manifest.runId);
974
+ const completed = await waitForRun(
975
+ updatedManifest.runId,
976
+ resolvedCtx.cwd,
977
+ { timeoutMs: 3600000 },
978
+ );
979
+ const metrics = collectRunMetrics(
980
+ resolvedCtx.cwd,
981
+ completed.manifest.runId,
982
+ );
560
983
  const lines: string[] = [
561
984
  `pi-crew run ${completed.manifest.status}: ${completed.manifest.runId} (${team.name})`,
562
985
  `Goal: ${goal.slice(0, 100)}`,
563
986
  ];
564
987
  if (metrics) {
565
988
  lines.push("");
566
- lines.push(`Metrics: ${metrics.completedCount}/${metrics.taskCount} tasks, ${metrics.totalTokens} tokens, ${metrics.durationMs}ms, consistency=${metrics.consistencyScore}`);
989
+ lines.push(
990
+ `Metrics: ${metrics.completedCount}/${metrics.taskCount} tasks, ${metrics.totalTokens} tokens, ${metrics.durationMs}ms, consistency=${metrics.consistencyScore}`,
991
+ );
567
992
  }
568
993
 
569
994
  if (completed.tasks.length > 0) {
@@ -574,8 +999,14 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
574
999
  );
575
1000
  if (summaryArtifact) {
576
1001
  try {
577
- const sumPath = path.join(completed.manifest.artifactsRoot, summaryArtifact.path);
578
- summaryContent = fs.readFileSync(sumPath, "utf-8").trim().slice(0, 4000);
1002
+ const sumPath = path.join(
1003
+ completed.manifest.artifactsRoot,
1004
+ summaryArtifact.path,
1005
+ );
1006
+ summaryContent = fs
1007
+ .readFileSync(sumPath, "utf-8")
1008
+ .trim()
1009
+ .slice(0, 4000);
579
1010
  } catch {
580
1011
  /* summary unavailable */
581
1012
  }
@@ -592,22 +1023,34 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
592
1023
  // inside artifactsRoot via the project's safe-path primitive. Rejects
593
1024
  // absolute paths (/etc/passwd) and ../ traversal that the old
594
1025
  // path.isAbsolute shortcut + bare path.join allowed.
595
- const resPath = resolveRealContainedPath(completed.manifest.artifactsRoot, task.resultArtifact.path);
596
- resultExcerpt = fs.readFileSync(resPath, "utf-8").trim().slice(0, 2000);
1026
+ const resPath = resolveRealContainedPath(
1027
+ completed.manifest.artifactsRoot,
1028
+ task.resultArtifact.path,
1029
+ );
1030
+ resultExcerpt = fs
1031
+ .readFileSync(resPath, "utf-8")
1032
+ .trim()
1033
+ .slice(0, 2000);
597
1034
  } catch {
598
1035
  resultExcerpt = "(result unavailable)";
599
1036
  }
600
1037
  }
601
1038
  const shortResult = resultExcerpt.slice(0, 500);
602
1039
  const statusTag =
603
- task.status === "completed" ? "✓"
604
- : task.status === "failed" ? ""
605
- : task.status === "cancelled" ? "⊘"
606
- : "·";
1040
+ task.status === "completed"
1041
+ ? ""
1042
+ : task.status === "failed"
1043
+ ? ""
1044
+ : task.status === "cancelled"
1045
+ ? "⊘"
1046
+ : "·";
607
1047
  taskLines.push(
608
1048
  `- ${statusTag} ${task.id} [${task.role}]: ${task.status}${shortResult ? " — " + shortResult : ""}${task.error ? ` | Error: ${task.error.slice(0, 200)}` : ""}`,
609
1049
  );
610
- if (task.status === "failed" || task.status === "needs_attention") {
1050
+ if (
1051
+ task.status === "failed" ||
1052
+ task.status === "needs_attention"
1053
+ ) {
611
1054
  failedCount++;
612
1055
  failedIds.push(task.id);
613
1056
  }
@@ -640,10 +1083,25 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
640
1083
  );
641
1084
  }
642
1085
 
643
- const runFailed = completed.manifest.status === "failed" || completed.manifest.status === "blocked";
644
- return result(lines.join("\n"), { action: "run", status: runFailed ? "error" : "ok", runId: completed.manifest.runId, artifactsRoot: completed.manifest.artifactsRoot, metrics }, runFailed);
1086
+ const runFailed =
1087
+ completed.manifest.status === "failed" ||
1088
+ completed.manifest.status === "blocked";
1089
+ return result(
1090
+ lines.join("\n"),
1091
+ {
1092
+ action: "run",
1093
+ status: runFailed ? "error" : "ok",
1094
+ runId: completed.manifest.runId,
1095
+ artifactsRoot: completed.manifest.artifactsRoot,
1096
+ metrics,
1097
+ },
1098
+ runFailed,
1099
+ );
645
1100
  } catch (waitError: unknown) {
646
- const errorMessage = waitError instanceof Error ? waitError.message : String(waitError);
1101
+ const errorMessage =
1102
+ waitError instanceof Error
1103
+ ? waitError.message
1104
+ : String(waitError);
647
1105
  return result(
648
1106
  [
649
1107
  `pi-crew run timed out or failed: ${updatedManifest.runId}`,
@@ -654,14 +1112,39 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
654
1112
  `Check status with: team status runId=${updatedManifest.runId}`,
655
1113
  `State: ${updatedManifest.stateRoot}`,
656
1114
  ].join("\n"),
657
- { action: "run", status: "error", runId: updatedManifest.runId, artifactsRoot: updatedManifest.artifactsRoot },
1115
+ {
1116
+ action: "run",
1117
+ status: "error",
1118
+ runId: updatedManifest.runId,
1119
+ artifactsRoot: updatedManifest.artifactsRoot,
1120
+ },
658
1121
  true,
659
1122
  );
660
1123
  }
661
1124
  }
662
1125
  let executed: Awaited<ReturnType<typeof executeTeamRun>>;
663
1126
  try {
664
- executed = await executeTeamRun({ manifest: executionManifest, tasks, team, workflow, agents, executeWorkers, limits: executedConfig.limits, runtime, runtimeConfig: executedConfig.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, modelOverride: params.model, skillOverride, signal: ctx.signal, reliability: executedConfig.reliability, metricRegistry: ctx.metricRegistry, onJsonEvent: ctx.onJsonEvent, workspaceId: ctx.cwd });
1127
+ executed = await executeTeamRun({
1128
+ manifest: executionManifest,
1129
+ tasks,
1130
+ team,
1131
+ workflow,
1132
+ agents,
1133
+ executeWorkers,
1134
+ limits: executedConfig.limits,
1135
+ runtime,
1136
+ runtimeConfig: executedConfig.runtime,
1137
+ parentContext: buildParentContext(ctx),
1138
+ parentModel: ctx.model,
1139
+ modelRegistry: ctx.modelRegistry,
1140
+ modelOverride: params.model,
1141
+ skillOverride,
1142
+ signal: ctx.signal,
1143
+ reliability: executedConfig.reliability,
1144
+ metricRegistry: ctx.metricRegistry,
1145
+ onJsonEvent: ctx.onJsonEvent,
1146
+ workspaceId: ctx.cwd,
1147
+ });
665
1148
  } finally {
666
1149
  unregisterActiveRun(updatedManifest.runId);
667
1150
  }
@@ -681,5 +1164,18 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
681
1164
  ? "Experimental live-session worker execution was enabled."
682
1165
  : "Safe scaffold mode: child Pi workers were not launched because runtime.mode=scaffold or executeWorkers=false was configured.",
683
1166
  ].join("\n");
684
- return result(text, { action: "run", status: executed.manifest.status === "failed" ? "error" : "ok", runId: executed.manifest.runId, artifactsRoot: executed.manifest.artifactsRoot, metrics: collectRunMetrics(resolvedCtx.cwd, executed.manifest.runId) }, executed.manifest.status === "failed");
1167
+ return result(
1168
+ text,
1169
+ {
1170
+ action: "run",
1171
+ status: executed.manifest.status === "failed" ? "error" : "ok",
1172
+ runId: executed.manifest.runId,
1173
+ artifactsRoot: executed.manifest.artifactsRoot,
1174
+ metrics: collectRunMetrics(
1175
+ resolvedCtx.cwd,
1176
+ executed.manifest.runId,
1177
+ ),
1178
+ },
1179
+ executed.manifest.status === "failed",
1180
+ );
685
1181
  }