pi-crew 0.1.45 → 0.1.46

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 (198) hide show
  1. package/README.md +5 -5
  2. package/agents/analyst.md +1 -1
  3. package/agents/critic.md +1 -1
  4. package/agents/executor.md +1 -1
  5. package/agents/explorer.md +1 -1
  6. package/agents/planner.md +1 -1
  7. package/agents/reviewer.md +1 -1
  8. package/agents/security-reviewer.md +1 -1
  9. package/agents/test-engineer.md +1 -1
  10. package/agents/verifier.md +1 -1
  11. package/agents/writer.md +1 -1
  12. package/docs/next-upgrade-roadmap.md +733 -0
  13. package/docs/refactor-tasks-phase3.md +394 -394
  14. package/docs/refactor-tasks-phase4.md +564 -564
  15. package/docs/refactor-tasks-phase5.md +402 -402
  16. package/docs/refactor-tasks-phase6.md +662 -662
  17. package/docs/research-awesome-agent-skills-distillation.md +100 -0
  18. package/docs/research-extension-examples.md +297 -297
  19. package/docs/research-extension-system.md +324 -324
  20. package/docs/research-oh-my-pi-distillation.md +322 -0
  21. package/docs/research-optimization-plan.md +548 -548
  22. package/docs/research-phase10-distillation.md +198 -198
  23. package/docs/research-phase11-distillation.md +201 -201
  24. package/docs/research-pi-coding-agent.md +357 -357
  25. package/docs/research-source-pi-crew-reference.md +174 -174
  26. package/docs/runtime-flow.md +148 -148
  27. package/docs/source-runtime-refactor-map.md +107 -83
  28. package/docs/usage.md +3 -3
  29. package/index.ts +6 -6
  30. package/install.mjs +52 -8
  31. package/package.json +1 -1
  32. package/schema.json +2 -1
  33. package/skills/async-worker-recovery/SKILL.md +42 -0
  34. package/skills/context-artifact-hygiene/SKILL.md +52 -0
  35. package/skills/delegation-patterns/SKILL.md +54 -0
  36. package/skills/mailbox-interactive/SKILL.md +40 -0
  37. package/skills/model-routing-context/SKILL.md +39 -0
  38. package/skills/multi-perspective-review/SKILL.md +58 -0
  39. package/skills/observability-reliability/SKILL.md +41 -0
  40. package/skills/ownership-session-security/SKILL.md +41 -0
  41. package/skills/pi-extension-lifecycle/SKILL.md +39 -0
  42. package/skills/requirements-to-task-packet/SKILL.md +63 -0
  43. package/skills/resource-discovery-config/SKILL.md +41 -0
  44. package/skills/runtime-state-reader/SKILL.md +44 -0
  45. package/skills/secure-agent-orchestration-review/SKILL.md +45 -0
  46. package/skills/state-mutation-locking/SKILL.md +42 -0
  47. package/skills/systematic-debugging/SKILL.md +67 -0
  48. package/skills/ui-render-performance/SKILL.md +39 -0
  49. package/skills/verification-before-done/SKILL.md +57 -0
  50. package/skills/worktree-isolation/SKILL.md +39 -0
  51. package/src/agents/agent-serializer.ts +34 -34
  52. package/src/agents/discover-agents.ts +12 -11
  53. package/src/config/config.ts +48 -24
  54. package/src/config/defaults.ts +14 -0
  55. package/src/extension/cross-extension-rpc.ts +82 -82
  56. package/src/extension/project-init.ts +62 -2
  57. package/src/extension/register.ts +11 -9
  58. package/src/extension/registration/commands.ts +32 -25
  59. package/src/extension/registration/compaction-guard.ts +125 -125
  60. package/src/extension/registration/subagent-helpers.ts +8 -0
  61. package/src/extension/registration/subagent-tools.ts +149 -148
  62. package/src/extension/registration/team-tool.ts +8 -6
  63. package/src/extension/run-bundle-schema.ts +89 -89
  64. package/src/extension/run-index.ts +13 -5
  65. package/src/extension/run-maintenance.ts +62 -43
  66. package/src/extension/team-tool/api.ts +25 -8
  67. package/src/extension/team-tool/cancel.ts +33 -4
  68. package/src/extension/team-tool/context.ts +5 -0
  69. package/src/extension/team-tool/handle-settings.ts +188 -188
  70. package/src/extension/team-tool/inspect.ts +41 -41
  71. package/src/extension/team-tool/lifecycle-actions.ts +91 -79
  72. package/src/extension/team-tool/plan.ts +19 -19
  73. package/src/extension/team-tool/respond.ts +37 -17
  74. package/src/extension/team-tool/run.ts +52 -10
  75. package/src/extension/team-tool/status.ts +12 -1
  76. package/src/extension/team-tool-types.ts +2 -0
  77. package/src/extension/team-tool.ts +32 -11
  78. package/src/i18n.ts +184 -184
  79. package/src/observability/event-to-metric.ts +8 -1
  80. package/src/observability/exporters/otlp-exporter.ts +77 -77
  81. package/src/prompt/prompt-runtime.ts +72 -72
  82. package/src/runtime/agent-control.ts +63 -63
  83. package/src/runtime/agent-memory.ts +72 -72
  84. package/src/runtime/agent-observability.ts +114 -114
  85. package/src/runtime/async-marker.ts +26 -26
  86. package/src/runtime/attention-events.ts +28 -28
  87. package/src/runtime/background-runner.ts +59 -53
  88. package/src/runtime/cancellation.ts +51 -0
  89. package/src/runtime/child-pi.ts +457 -444
  90. package/src/runtime/completion-guard.ts +190 -190
  91. package/src/runtime/crash-recovery.ts +1 -0
  92. package/src/runtime/crew-agent-records.ts +38 -6
  93. package/src/runtime/deadletter.ts +1 -0
  94. package/src/runtime/delivery-coordinator.ts +46 -25
  95. package/src/runtime/direct-run.ts +35 -35
  96. package/src/runtime/effectiveness.ts +76 -0
  97. package/src/runtime/foreground-control.ts +82 -82
  98. package/src/runtime/green-contract.ts +46 -46
  99. package/src/runtime/group-join.ts +106 -106
  100. package/src/runtime/heartbeat-gradient.ts +28 -28
  101. package/src/runtime/heartbeat-watcher.ts +124 -124
  102. package/src/runtime/live-agent-control.ts +88 -87
  103. package/src/runtime/live-agent-manager.ts +103 -85
  104. package/src/runtime/live-control-realtime.ts +36 -36
  105. package/src/runtime/live-session-runtime.ts +309 -305
  106. package/src/runtime/manifest-cache.ts +17 -2
  107. package/src/runtime/model-fallback.ts +6 -4
  108. package/src/runtime/parallel-research.ts +44 -44
  109. package/src/runtime/pi-args.ts +18 -3
  110. package/src/runtime/pi-json-output.ts +111 -111
  111. package/src/runtime/policy-engine.ts +79 -79
  112. package/src/runtime/process-status.ts +5 -1
  113. package/src/runtime/progress-event-coalescer.ts +43 -43
  114. package/src/runtime/recovery-recipes.ts +74 -74
  115. package/src/runtime/retry-executor.ts +81 -64
  116. package/src/runtime/role-permission.ts +39 -39
  117. package/src/runtime/runtime-resolver.ts +22 -6
  118. package/src/runtime/session-resources.ts +25 -25
  119. package/src/runtime/session-snapshot.ts +59 -59
  120. package/src/runtime/session-usage.ts +79 -79
  121. package/src/runtime/sidechain-output.ts +29 -29
  122. package/src/runtime/skill-instructions.ts +222 -0
  123. package/src/runtime/stale-reconciler.ts +4 -14
  124. package/src/runtime/subagent-manager.ts +3 -0
  125. package/src/runtime/supervisor-contact.ts +59 -59
  126. package/src/runtime/task-display.ts +38 -38
  127. package/src/runtime/task-output-context.ts +127 -127
  128. package/src/runtime/task-runner/capabilities.ts +78 -0
  129. package/src/runtime/task-runner/live-executor.ts +105 -101
  130. package/src/runtime/task-runner/progress.ts +119 -119
  131. package/src/runtime/task-runner/prompt-builder.ts +3 -1
  132. package/src/runtime/task-runner/prompt-pipeline.ts +64 -0
  133. package/src/runtime/task-runner/result-utils.ts +14 -14
  134. package/src/runtime/task-runner/state-helpers.ts +22 -22
  135. package/src/runtime/task-runner.ts +44 -5
  136. package/src/runtime/team-runner.ts +78 -15
  137. package/src/runtime/worker-heartbeat.ts +21 -21
  138. package/src/runtime/worker-startup.ts +57 -57
  139. package/src/schema/config-schema.ts +1 -0
  140. package/src/schema/team-tool-schema.ts +3 -3
  141. package/src/state/active-run-registry.ts +165 -0
  142. package/src/state/contracts.ts +1 -1
  143. package/src/state/mailbox.ts +44 -4
  144. package/src/state/state-store.ts +8 -1
  145. package/src/state/task-claims.ts +44 -44
  146. package/src/state/types.ts +44 -2
  147. package/src/state/usage.ts +29 -29
  148. package/src/subagents/async-entry.ts +1 -1
  149. package/src/subagents/index.ts +3 -3
  150. package/src/subagents/live/control.ts +1 -1
  151. package/src/subagents/live/manager.ts +1 -1
  152. package/src/subagents/live/realtime.ts +1 -1
  153. package/src/subagents/live/session-runtime.ts +1 -1
  154. package/src/subagents/manager.ts +1 -1
  155. package/src/subagents/spawn.ts +1 -1
  156. package/src/teams/team-config.ts +1 -0
  157. package/src/teams/team-serializer.ts +38 -38
  158. package/src/types/diff.d.ts +18 -18
  159. package/src/ui/crew-footer.ts +101 -101
  160. package/src/ui/crew-select-list.ts +111 -111
  161. package/src/ui/crew-widget.ts +4 -3
  162. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  163. package/src/ui/dashboard-panes/progress-pane.ts +2 -0
  164. package/src/ui/dynamic-border.ts +25 -25
  165. package/src/ui/layout-primitives.ts +106 -106
  166. package/src/ui/loaders.ts +158 -158
  167. package/src/ui/render-diff.ts +119 -119
  168. package/src/ui/render-scheduler.ts +143 -143
  169. package/src/ui/run-snapshot-cache.ts +10 -2
  170. package/src/ui/snapshot-types.ts +2 -0
  171. package/src/ui/spinner.ts +17 -17
  172. package/src/ui/status-colors.ts +58 -58
  173. package/src/ui/syntax-highlight.ts +116 -116
  174. package/src/utils/atomic-write.ts +33 -33
  175. package/src/utils/completion-dedupe.ts +63 -63
  176. package/src/utils/frontmatter.ts +68 -68
  177. package/src/utils/git.ts +262 -262
  178. package/src/utils/ids.ts +12 -12
  179. package/src/utils/names.ts +27 -27
  180. package/src/utils/paths.ts +4 -2
  181. package/src/utils/redaction.ts +44 -44
  182. package/src/utils/safe-paths.ts +47 -47
  183. package/src/utils/sleep.ts +32 -32
  184. package/src/workflows/validate-workflow.ts +40 -40
  185. package/src/workflows/workflow-config.ts +1 -0
  186. package/src/worktree/branch-freshness.ts +45 -45
  187. package/teams/default.team.md +12 -12
  188. package/teams/fast-fix.team.md +11 -11
  189. package/teams/implementation.team.md +18 -18
  190. package/teams/parallel-research.team.md +14 -14
  191. package/teams/research.team.md +11 -11
  192. package/teams/review.team.md +12 -12
  193. package/workflows/default.workflow.md +29 -29
  194. package/workflows/fast-fix.workflow.md +22 -22
  195. package/workflows/implementation.workflow.md +38 -38
  196. package/workflows/parallel-research.workflow.md +46 -46
  197. package/workflows/research.workflow.md +22 -22
  198. package/workflows/review.workflow.md +30 -30
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import type { AgentConfig } from "../agents/agent-config.ts";
3
3
  import type { CrewLimitsConfig, CrewRuntimeConfig } from "../config/config.ts";
4
- import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
4
+ import type { ArtifactDescriptor, OperationTerminalEvidence, TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
5
5
  import { writeArtifact } from "../state/artifact-store.ts";
6
6
  import { appendEvent } from "../state/event-log.ts";
7
7
  import { saveRunManifest } from "../state/state-store.ts";
@@ -22,12 +22,16 @@ import { parseSessionUsage } from "./session-usage.ts";
22
22
  import type { CrewAgentProgress, CrewRuntimeKind } from "./crew-agent-runtime.ts";
23
23
  import { shouldAppendProgressEventUpdate, type ProgressEventSummary } from "./progress-event-coalescer.ts";
24
24
  import { coordinationBridgeInstructions, renderTaskPrompt } from "./task-runner/prompt-builder.ts";
25
+ import { buildWorkerPromptPipeline } from "./task-runner/prompt-pipeline.ts";
26
+ import { buildWorkerCapabilityInventory } from "./task-runner/capabilities.ts";
25
27
  import { applyAgentProgressEvent, applyUsageToProgress, progressEventSummary, shouldFlushProgressEvent } from "./task-runner/progress.ts";
26
28
  import { checkpointTask, persistSingleTaskUpdate, updateTask } from "./task-runner/state-helpers.ts";
27
29
  import { cleanResultText, isFinalChildEvent } from "./task-runner/result-utils.ts";
28
30
  import { evaluateCompletionMutationGuard } from "./completion-guard.ts";
31
+ import { cancellationReasonFromSignal } from "./cancellation.ts";
29
32
  import { appendTaskAttentionEvent } from "./attention-events.ts";
30
33
  import { parseSupervisorContactFromLine, recordSupervisorContact } from "./supervisor-contact.ts";
34
+ import { renderSkillInstructions } from "./skill-instructions.ts";
31
35
 
32
36
  export interface TaskRunnerInput {
33
37
  manifest: TeamRunManifest;
@@ -43,8 +47,14 @@ export interface TaskRunnerInput {
43
47
  parentModel?: unknown;
44
48
  modelRegistry?: unknown;
45
49
  modelOverride?: string;
50
+ teamRoleModel?: string;
51
+ teamRoleSkills?: string[] | false;
52
+ skillOverride?: string[] | false;
46
53
  limits?: CrewLimitsConfig;
47
54
  dependencyContextText?: string;
55
+ skillBlock?: string;
56
+ skillNames?: string[];
57
+ skillPaths?: string[];
48
58
  /** Optional callback for JSON events from child Pi. Used for overflow recovery tracking. */
49
59
  onJsonEvent?: (taskId: string, runId: string, event: unknown) => void;
50
60
  }
@@ -75,8 +85,12 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
75
85
  upsertCrewAgent(manifest, recordFromTask(manifest, task, runtimeKind));
76
86
  appendEvent(manifest.eventsPath, { type: "task.started", runId: manifest.runId, taskId: task.id, data: { role: task.role, agent: task.agent, runtime: runtimeKind, cwd: task.cwd, worktreePath: workspace.worktreePath, worktreeBranch: workspace.branch, worktreeReused: workspace.reused } });
77
87
  const permissionMode = permissionForRole(task.role);
88
+ const renderedSkills = input.skillBlock === undefined ? renderSkillInstructions({ cwd: task.cwd, role: task.role, agent: input.agent, teamRole: { skills: input.teamRoleSkills }, step: input.step, override: input.skillOverride }) : undefined;
89
+ const skillBlock = input.skillBlock ?? renderedSkills?.block;
90
+ const skillNames = input.skillNames ?? renderedSkills?.names;
91
+ const skillPaths = input.skillPaths ?? renderedSkills?.paths;
78
92
 
79
- const prompt = renderTaskPrompt(manifest, input.step, task, input.agent);
93
+ const prompt = renderTaskPrompt(manifest, input.step, task, input.agent, skillBlock);
80
94
  const promptArtifact = writeArtifact(manifest.artifactsRoot, {
81
95
  kind: "prompt",
82
96
  relativePath: `prompts/${task.id}.md`,
@@ -93,9 +107,16 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
93
107
  let parsedOutput: ParsedPiJsonOutput | undefined;
94
108
  let finalStdout = "";
95
109
  let transcriptPath: string | undefined;
110
+ let terminalEvidence: OperationTerminalEvidence[] = [];
96
111
 
97
112
  let startupEvidence = createStartupEvidence({ command: runtimeKind === "child-process" ? "pi" : runtimeKind === "live-session" ? "live-session" : "safe-scaffold", startedAt: new Date(task.startedAt ?? new Date().toISOString()), finishedAt: new Date(), promptSentAt: new Date(task.startedAt ?? new Date().toISOString()), promptAccepted: true, exitCode: 0 });
98
113
  const inputsArtifact = writeTaskInputsArtifact(manifest, task, dependencyContext);
114
+ const skillArtifact = skillBlock ? writeArtifact(manifest.artifactsRoot, {
115
+ kind: "metadata",
116
+ relativePath: `metadata/${task.id}.skills.md`,
117
+ content: [`Selected skills: ${skillNames?.join(", ") ?? "(none)"}`, `Skill paths passed to child Pi: ${(skillPaths ?? []).length}`, "", skillBlock, ""].join("\n"),
118
+ producer: task.id,
119
+ }) : undefined;
99
120
  const coordinationArtifact = writeArtifact(manifest.artifactsRoot, {
100
121
  kind: "metadata",
101
122
  relativePath: `metadata/${task.id}.coordination-bridge.md`,
@@ -103,7 +124,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
103
124
  producer: task.id,
104
125
  });
105
126
  if (runtimeKind === "child-process") {
106
- const modelRoutingPlan = buildConfiguredModelRouting({ overrideModel: input.modelOverride, stepModel: input.step.model, agentModel: input.agent.model, fallbackModels: input.agent.fallbackModels, parentModel: input.parentModel, modelRegistry: input.modelRegistry, cwd: manifest.cwd });
127
+ const modelRoutingPlan = buildConfiguredModelRouting({ overrideModel: input.modelOverride, stepModel: input.step.model, teamRoleModel: input.teamRoleModel, agentModel: input.agent.model, fallbackModels: input.agent.fallbackModels, parentModel: input.parentModel, modelRegistry: input.modelRegistry, cwd: task.cwd });
107
128
  const candidates = modelRoutingPlan.candidates;
108
129
  const attemptModels = candidates.length > 0 ? candidates : [undefined];
109
130
  const logs: string[] = [];
@@ -151,6 +172,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
151
172
  signal: input.signal,
152
173
  transcriptPath,
153
174
  maxDepth: input.limits?.maxTaskDepth,
175
+ skillPaths,
154
176
  onSpawn: (pid) => {
155
177
  ({ task, tasks } = checkpointTask(manifest, tasks, task, "child-spawned", pid));
156
178
  },
@@ -181,6 +203,9 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
181
203
  persistChildProgress(event);
182
204
  },
183
205
  });
206
+ const evidenceStatus = childResult.exitStatus?.cancelled ? "cancelled" : childResult.error || (childResult.exitCode && childResult.exitCode !== 0) ? "failed" : "completed";
207
+ terminalEvidence = [...terminalEvidence, { operation: "worker", status: evidenceStatus, startedAt: attemptStartedAt.toISOString(), finishedAt: new Date().toISOString(), ...(input.signal?.aborted ? { reason: cancellationReasonFromSignal(input.signal) } : {}), ...(childResult.exitStatus ? { exitStatus: childResult.exitStatus } : {}) }];
208
+ if (evidenceStatus === "cancelled") appendEvent(manifest.eventsPath, { type: "worker.cancelled", runId: manifest.runId, taskId: task.id, message: input.signal?.aborted ? cancellationReasonFromSignal(input.signal).message : "Worker cancelled.", data: { terminalEvidence: terminalEvidence.at(-1) } });
184
209
  startupEvidence = createStartupEvidence({ command: "pi", startedAt: attemptStartedAt, finishedAt: new Date(), promptSentAt: attemptStartedAt, promptAccepted: childResult.exitCode === 0 && !childResult.error, stderr: childResult.stderr, error: childResult.error, exitCode: childResult.exitCode });
185
210
  exitCode = childResult.exitCode;
186
211
  finalStdout = childResult.stdout;
@@ -238,7 +263,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
238
263
  ({ task, tasks } = checkpointTask(manifest, tasks, task, "artifact-written"));
239
264
  } else if (runtimeKind === "live-session") {
240
265
  const { runLiveTask } = await import("./task-runner/live-executor.ts");
241
- const live = await runLiveTask({ manifest, tasks, task, step: input.step, agent: input.agent, prompt, signal: input.signal, runtimeConfig: input.runtimeConfig, parentContext: input.parentContext, parentModel: input.parentModel, modelRegistry: input.modelRegistry });
266
+ const live = await runLiveTask({ manifest, tasks, task, step: input.step, agent: input.agent, prompt, signal: input.signal, runtimeConfig: input.runtimeConfig, parentContext: input.parentContext, parentModel: input.parentModel, modelRegistry: input.modelRegistry, modelOverride: input.modelOverride, teamRoleModel: input.teamRoleModel });
242
267
  task = live.task;
243
268
  tasks = live.tasks;
244
269
  startupEvidence = live.startupEvidence;
@@ -310,6 +335,8 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
310
335
  resultArtifact,
311
336
  claim: undefined,
312
337
  heartbeat: touchWorkerHeartbeat(task.heartbeat ?? createWorkerHeartbeat(task.id), { alive: false }),
338
+ workerExitStatus: terminalEvidence.at(-1)?.exitStatus,
339
+ terminalEvidence: terminalEvidence.length ? [...(task.terminalEvidence ?? []), ...terminalEvidence] : task.terminalEvidence,
313
340
  ...(logArtifact ? { logArtifact } : {}),
314
341
  ...(transcriptArtifact ? { transcriptArtifact } : {}),
315
342
  };
@@ -339,7 +366,19 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
339
366
  content: `${JSON.stringify({ role: task.role, permissionMode }, null, 2)}\n`,
340
367
  producer: task.id,
341
368
  });
342
- manifest = { ...manifest, updatedAt: new Date().toISOString(), artifacts: [...manifest.artifacts, promptArtifact, resultArtifact, inputsArtifact, coordinationArtifact, packetArtifact, verificationArtifact, startupArtifact, permissionArtifact, ...(sharedOutputArtifact ? [sharedOutputArtifact] : []), ...(logArtifact ? [logArtifact] : []), ...(transcriptArtifact ? [transcriptArtifact] : []), ...(diffArtifact ? [diffArtifact] : []), ...(diffStatArtifact ? [diffStatArtifact] : [])] };
369
+ const capabilityArtifact = writeArtifact(manifest.artifactsRoot, {
370
+ kind: "metadata",
371
+ relativePath: `metadata/${task.id}.capabilities.json`,
372
+ content: `${JSON.stringify(buildWorkerCapabilityInventory({ taskId: task.id, role: task.role, agent: input.agent, runtime: runtimeKind, permissionMode, skillNames, skillPaths, skillsDisabled: input.skillOverride === false || input.teamRoleSkills === false, modelOverride: input.modelOverride, teamRoleModel: input.teamRoleModel, stepModel: input.step.model }), null, 2)}\n`,
373
+ producer: task.id,
374
+ });
375
+ const promptPipelineArtifact = writeArtifact(manifest.artifactsRoot, {
376
+ kind: "metadata",
377
+ relativePath: `metadata/${task.id}.prompt-pipeline.json`,
378
+ content: `${JSON.stringify(buildWorkerPromptPipeline({ artifactsRoot: manifest.artifactsRoot, taskId: task.id, promptArtifact, inputsArtifact, skillArtifact, capabilityArtifact, coordinationArtifact, skillInstructionCount: skillNames?.length ?? 0, skillsDisabled: input.skillOverride === false || input.teamRoleSkills === false }), null, 2)}\n`,
379
+ producer: task.id,
380
+ });
381
+ manifest = { ...manifest, updatedAt: new Date().toISOString(), artifacts: [...manifest.artifacts, promptArtifact, resultArtifact, inputsArtifact, coordinationArtifact, ...(skillArtifact ? [skillArtifact] : []), packetArtifact, verificationArtifact, startupArtifact, permissionArtifact, capabilityArtifact, promptPipelineArtifact, ...(sharedOutputArtifact ? [sharedOutputArtifact] : []), ...(logArtifact ? [logArtifact] : []), ...(transcriptArtifact ? [transcriptArtifact] : []), ...(diffArtifact ? [diffArtifact] : []), ...(diffStatArtifact ? [diffStatArtifact] : [])] };
343
382
  saveRunManifest(manifest);
344
383
  tasks = persistSingleTaskUpdate(manifest, tasks, task);
345
384
  upsertCrewAgent(manifest, recordFromTask(manifest, task, runtimeKind));
@@ -25,6 +25,8 @@ import { childCorrelation, withCorrelation } from "../observability/correlation.
25
25
  import { resolveBatchConcurrency } from "./concurrency.ts";
26
26
  import { mapConcurrent } from "./parallel-utils.ts";
27
27
  import { permissionForRole } from "./role-permission.ts";
28
+ import { CrewCancellationError, cancellationReasonFromSignal } from "./cancellation.ts";
29
+ import { effectivenessPolicyDecision, evaluateRunEffectiveness, formatRunEffectivenessLines } from "./effectiveness.ts";
28
30
 
29
31
  export interface ExecuteTeamRunInput {
30
32
  manifest: TeamRunManifest;
@@ -43,6 +45,8 @@ export interface ExecuteTeamRunInput {
43
45
  signal?: AbortSignal;
44
46
  reliability?: CrewReliabilityConfig;
45
47
  metricRegistry?: MetricRegistry;
48
+ /** Skill override from the team tool. false disables skill injection for this run. */
49
+ skillOverride?: string[] | false;
46
50
  /** Optional callback for JSON events from child Pi. Used for overflow recovery tracking. */
47
51
  onJsonEvent?: (taskId: string, runId: string, event: unknown) => void;
48
52
  }
@@ -380,7 +384,11 @@ function formatTaskProgress(task: TeamTaskState): string {
380
384
  return `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.taskPacket ? ` scope=${task.taskPacket.scope}` : ""}${task.verification ? ` green=${task.verification.observedGreenLevel}/${task.verification.requiredGreenLevel}` : ""}${task.error ? ` - ${task.error}` : ""}`;
381
385
  }
382
386
 
383
- function writeProgress(manifest: TeamRunManifest, tasks: TeamTaskState[], producer: string): TeamRunManifest {
387
+ function runEffectivenessLines(manifest: TeamRunManifest, tasks: TeamTaskState[], executeWorkers: boolean, runtimeConfig?: CrewRuntimeConfig): string[] {
388
+ return formatRunEffectivenessLines(evaluateRunEffectiveness({ manifest, tasks, executeWorkers, runtimeConfig }));
389
+ }
390
+
391
+ function writeProgress(manifest: TeamRunManifest, tasks: TeamTaskState[], producer: string, executeWorkers = true, runtimeConfig?: CrewRuntimeConfig): TeamRunManifest {
384
392
  const counts = new Map<string, number>();
385
393
  for (const task of tasks) counts.set(task.status, (counts.get(task.status) ?? 0) + 1);
386
394
  const queue = taskGraphSnapshot(tasks);
@@ -401,6 +409,9 @@ function writeProgress(manifest: TeamRunManifest, tasks: TeamTaskState[], produc
401
409
  "## Tasks",
402
410
  ...tasks.map(formatTaskProgress),
403
411
  "",
412
+ "## Effectiveness",
413
+ ...runEffectivenessLines(manifest, tasks, executeWorkers, runtimeConfig),
414
+ "",
404
415
  ].join("\n"),
405
416
  });
406
417
  return { ...manifest, updatedAt: new Date().toISOString(), artifacts: [...manifest.artifacts.filter((artifact) => !(artifact.kind === "progress" && artifact.path === progress.path)), progress] };
@@ -528,16 +539,24 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
528
539
  manifest = updateRunStatus(manifest, "cancelled", "Plan approval was cancelled.");
529
540
  return { manifest, tasks };
530
541
  }
531
- manifest = writeProgress(manifest, tasks, "team-runner");
542
+ manifest = writeProgress(manifest, tasks, "team-runner", input.executeWorkers, input.runtimeConfig);
532
543
  await saveRunManifestAsync(manifest);
533
544
  const runtimeKind = input.runtime?.kind ?? (input.executeWorkers ? "child-process" : "scaffold");
534
545
  saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
535
546
 
536
547
  while (tasks.some((task) => task.status === "queued")) {
537
548
  if (input.signal?.aborted) {
538
- tasks = tasks.map((task) => task.status === "queued" || task.status === "running" || task.status === "waiting" ? { ...task, status: "cancelled", finishedAt: new Date().toISOString(), error: "Run cancelled." } : task);
549
+ const cancelReason = cancellationReasonFromSignal(input.signal);
550
+ const message = `${cancelReason.message} (${cancelReason.code})`;
551
+ const cancelledTaskIds: string[] = [];
552
+ tasks = tasks.map((task) => {
553
+ if (task.status !== "queued" && task.status !== "running" && task.status !== "waiting") return task;
554
+ cancelledTaskIds.push(task.id);
555
+ return { ...task, status: "cancelled", finishedAt: new Date().toISOString(), error: message };
556
+ });
539
557
  await saveRunTasksAsync(manifest, tasks);
540
- manifest = updateRunStatus(manifest, "cancelled", "Run cancelled.");
558
+ for (const taskId of cancelledTaskIds) appendEvent(manifest.eventsPath, { type: "task.cancelled", runId: manifest.runId, taskId, message, data: { reason: cancelReason.code } });
559
+ manifest = updateRunStatus(manifest, "cancelled", message, { data: { reason: cancelReason.code, cancelledTaskIds } });
541
560
  return { manifest, tasks };
542
561
  }
543
562
 
@@ -581,25 +600,27 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
581
600
  async (task) => {
582
601
  const step = findStep(workflow, task);
583
602
  const agent = findAgent(input.agents, task);
584
- const baseInput = { manifest, tasks, task, step, agent, signal: input.signal, executeWorkers: input.executeWorkers, runtimeKind: input.runtime?.kind, runtimeConfig: input.runtimeConfig, parentContext: input.parentContext, parentModel: input.parentModel, modelRegistry: input.modelRegistry, modelOverride: input.modelOverride, limits: input.limits, onJsonEvent: input.onJsonEvent };
603
+ const teamRole = input.team.roles.find((role) => role.name === task.role);
604
+ const baseInput = { manifest, tasks, task, step, agent, signal: input.signal, executeWorkers: input.executeWorkers, runtimeKind: input.runtime?.kind, runtimeConfig: input.runtimeConfig, parentContext: input.parentContext, parentModel: input.parentModel, modelRegistry: input.modelRegistry, modelOverride: input.modelOverride, teamRoleModel: teamRole?.model, teamRoleSkills: teamRole?.skills, skillOverride: input.skillOverride, limits: input.limits, onJsonEvent: input.onJsonEvent };
585
605
  if (input.reliability?.autoRetry !== true) return withCorrelation(childCorrelation(manifest.runId, task.id), () => runTeamTask(baseInput));
586
606
  let lastFailed: { manifest: TeamRunManifest; tasks: TeamTaskState[] } | undefined;
587
607
  const attemptsSoFar: TaskAttemptState[] = [...(task.attempts ?? [])];
588
608
  const policy = retryPolicyFromConfig(input.reliability);
589
609
  try {
590
- return await executeWithRetry(async (attempt) => {
610
+ return await executeWithRetry(async (attempt, info) => {
591
611
  const startedAt = new Date().toISOString();
592
- const inFlightAttempts: TaskAttemptState[] = [...attemptsSoFar, { startedAt }];
612
+ const inFlightAttempts: TaskAttemptState[] = [...attemptsSoFar, { attemptId: info.attemptId, startedAt }];
593
613
  input.metricRegistry?.counter("crew.task.retry_attempt_total", "Retry attempts by run and task").inc({ runId: manifest.runId, taskId: task.id });
594
614
  const fresh = loadRunManifestById(manifest.cwd, manifest.runId);
595
615
  const freshManifest = fresh?.manifest ?? manifest;
596
616
  const freshTasks = fresh?.tasks ?? tasks;
597
617
  const freshTask = freshTasks.find((item) => item.id === task.id) ?? task;
618
+ if (freshTask.status !== "queued" && freshTask.status !== "running") return { manifest: freshManifest, tasks: freshTasks };
598
619
  const taskWithAttempt: TeamTaskState = { ...freshTask, attempts: inFlightAttempts };
599
620
  const result = await withCorrelation(childCorrelation(freshManifest.runId, task.id), () => runTeamTask({ ...baseInput, manifest: freshManifest, tasks: freshTasks, task: taskWithAttempt }));
600
621
  const failed = failedTaskFrom(result, task.id);
601
622
  const endedAt = new Date().toISOString();
602
- const finishedAttempt: TaskAttemptState = { startedAt, endedAt, ...(failed?.error ? { error: failed.error } : {}) };
623
+ const finishedAttempt: TaskAttemptState = { attemptId: info.attemptId, startedAt, endedAt, ...(failed?.error ? { error: failed.error } : {}) };
603
624
  attemptsSoFar.push(finishedAttempt);
604
625
  const withAttempt = result.tasks.map((item) => item.id === task.id ? { ...item, attempts: [...attemptsSoFar] } : item);
605
626
  const enriched = { manifest: result.manifest, tasks: withAttempt };
@@ -611,28 +632,51 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
611
632
  return enriched;
612
633
  }, policy, {
613
634
  signal: input.signal,
614
- onAttemptFailed: (attempt, error, delayMs) => {
615
- appendEvent(manifest.eventsPath, { type: "crew.task.retry_attempt", runId: manifest.runId, taskId: task.id, message: error.message, data: { attempt, delayMs } });
635
+ attemptId: (attempt) => `${manifest.runId}:${task.id}:attempt-${attempt}`,
636
+ onAttemptFailed: (attempt, error, delayMs, info) => {
637
+ appendEvent(manifest.eventsPath, { type: "crew.task.retry_attempt", runId: manifest.runId, taskId: task.id, message: error.message, data: { attempt, attemptId: info.attemptId, delayMs } });
616
638
  input.metricRegistry?.histogram("crew.task.retry_delay_ms", "Retry backoff delay, milliseconds").observe({ runId: manifest.runId, taskId: task.id }, delayMs);
617
639
  },
618
- onRetryGivenUp: (attempts, error) => {
619
- appendDeadletter(manifest, { runId: manifest.runId, taskId: task.id, reason: "max-retries", attempts, lastError: error.message, timestamp: new Date().toISOString() });
640
+ onRetryGivenUp: (attempts, error, info) => {
641
+ appendDeadletter(manifest, { runId: manifest.runId, taskId: task.id, reason: "max-retries", attempts, attemptId: info.attemptId, lastError: error.message, timestamp: new Date().toISOString() });
620
642
  input.metricRegistry?.counter("crew.task.deadletter_total", "Deadletter triggers by reason").inc({ reason: "max-retries" });
621
643
  input.metricRegistry?.histogram("crew.task.retry_count", "Retries per task", [0, 1, 2, 3, 5, 10]).observe({ runId: manifest.runId, team: input.team.name }, Math.max(0, attempts - 1));
622
644
  },
623
645
  });
624
- } catch {
646
+ } catch (retryError) {
647
+ if (retryError instanceof CrewCancellationError || input.signal?.aborted) {
648
+ const reason = retryError instanceof CrewCancellationError ? retryError.reason : cancellationReasonFromSignal(input.signal);
649
+ const fresh = loadRunManifestById(manifest.cwd, manifest.runId);
650
+ const freshManifest = fresh?.manifest ?? manifest;
651
+ const freshTasks = fresh?.tasks ?? tasks;
652
+ const cancelledTasks = freshTasks.map((item) => item.id === task.id && (item.status === "queued" || item.status === "running") ? { ...item, status: "cancelled" as const, finishedAt: new Date().toISOString(), error: `${reason.message} (${reason.code})` } : item);
653
+ appendEvent(freshManifest.eventsPath, { type: "task.cancelled", runId: freshManifest.runId, taskId: task.id, message: reason.message, data: { reason, phase: "retry" } });
654
+ return { manifest: updateRunStatus(freshManifest, "cancelled", reason.message), tasks: cancelledTasks };
655
+ }
625
656
  if (lastFailed) return lastFailed;
626
657
  const fresh = loadRunManifestById(manifest.cwd, manifest.runId);
627
658
  const freshManifest = fresh?.manifest ?? manifest;
628
659
  const freshTasks = fresh?.tasks ?? tasks;
629
660
  const freshTask = freshTasks.find((item) => item.id === task.id) ?? task;
661
+ if (freshTask.status !== "queued" && freshTask.status !== "running") return { manifest: freshManifest, tasks: freshTasks };
630
662
  return withCorrelation(childCorrelation(freshManifest.runId, task.id), () => runTeamTask({ ...baseInput, manifest: freshManifest, tasks: freshTasks, task: freshTask }));
631
663
  }
632
664
  },
633
665
  );
634
666
  manifest = { ...results.at(-1)!.manifest, artifacts: mergeArtifacts([manifest.artifacts, ...results.map((item) => item.manifest.artifacts)].flat()) };
635
667
  tasks = __test__mergeTaskUpdates(tasks, results);
668
+ const cancelledResult = results.find((item) => item.manifest.status === "cancelled");
669
+ if (cancelledResult || input.signal?.aborted) {
670
+ const reason = input.signal?.aborted ? cancellationReasonFromSignal(input.signal) : undefined;
671
+ const message = reason?.message ?? cancelledResult?.manifest.summary ?? "Run cancelled during task execution.";
672
+ manifest = { ...manifest, status: "running" };
673
+ manifest = updateRunStatus(manifest, "cancelled", message);
674
+ await saveRunTasksAsync(manifest, tasks);
675
+ saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
676
+ await saveRunManifestAsync(manifest);
677
+ appendEvent(manifest.eventsPath, { type: "run.cancelled", runId: manifest.runId, message, data: { reason, phase: "task-batch", cancelledResultRunId: cancelledResult?.manifest.runId } });
678
+ return { manifest, tasks };
679
+ }
636
680
  queueIndex = buildTaskGraphIndex(tasks);
637
681
  const injectedAfterBatch = attemptAdaptivePlan();
638
682
  if (injectedAfterBatch.missing) {
@@ -666,21 +710,37 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
666
710
  });
667
711
  const groupDelivery = deliverGroupJoin({ manifest, mode: resolveGroupJoinMode(input.runtimeConfig), batch: readyBatch, allTasks: tasks });
668
712
  manifest = { ...manifest, artifacts: mergeArtifacts([...manifest.artifacts, batchArtifact, ...(groupDelivery?.artifact ? [groupDelivery.artifact] : [])]) };
669
- manifest = writeProgress(manifest, tasks, "team-runner");
713
+ manifest = writeProgress(manifest, tasks, "team-runner", input.executeWorkers, input.runtimeConfig);
670
714
  await saveRunManifestAsync(manifest);
671
715
  }
672
716
 
673
717
  const failed = tasks.find((task) => task.status === "failed");
718
+ const waiting = tasks.find((task) => task.status === "waiting");
719
+ const running = tasks.find((task) => task.status === "running");
674
720
  manifest = applyPolicy(manifest, tasks, input.limits);
721
+ const effectiveness = evaluateRunEffectiveness({ manifest, tasks, executeWorkers: input.executeWorkers, runtimeConfig: input.runtimeConfig });
722
+ const effectivenessDecision = effectivenessPolicyDecision(effectiveness);
723
+ if (effectivenessDecision) {
724
+ manifest = { ...manifest, policyDecisions: [...(manifest.policyDecisions ?? []), effectivenessDecision], updatedAt: new Date().toISOString() };
725
+ appendEvent(manifest.eventsPath, { type: "run.effectiveness", runId: manifest.runId, message: effectivenessDecision.message, data: { effectiveness, policyDecision: effectivenessDecision } });
726
+ }
675
727
  const blockingDecision = manifest.policyDecisions?.find((item) => item.action === "block" || item.action === "escalate");
676
728
  if (failed) {
677
729
  manifest = updateRunStatus(manifest, "failed", `Failed at task '${failed.id}'.`);
730
+ } else if (waiting) {
731
+ manifest = updateRunStatus(manifest, "blocked", `Waiting for response to task '${waiting.id}'.`);
732
+ } else if (running) {
733
+ manifest = updateRunStatus(manifest, "blocked", `Task '${running.id}' is still running.`);
734
+ } else if (effectiveness.severity === "failed") {
735
+ manifest = updateRunStatus(manifest, "failed", effectivenessDecision?.message ?? "Run effectiveness guard failed.");
736
+ } else if (effectiveness.severity === "blocked") {
737
+ manifest = updateRunStatus(manifest, "blocked", effectivenessDecision?.message ?? "Run effectiveness guard blocked completion.");
678
738
  } else if (blockingDecision) {
679
739
  manifest = updateRunStatus(manifest, "blocked", blockingDecision.message);
680
740
  } else {
681
741
  manifest = updateRunStatus(manifest, "completed", input.executeWorkers ? "Team workflow completed." : "Team workflow scaffold completed without launching child workers.");
682
742
  }
683
- manifest = writeProgress(manifest, tasks, "team-runner");
743
+ manifest = writeProgress(manifest, tasks, "team-runner", input.executeWorkers, input.runtimeConfig);
684
744
  await saveRunManifestAsync(manifest);
685
745
  const usage = aggregateUsage(tasks);
686
746
  const summaryArtifact = writeArtifact(manifest.artifactsRoot, {
@@ -699,6 +759,9 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
699
759
  "## Tasks",
700
760
  ...tasks.map(formatTaskProgress),
701
761
  "",
762
+ "## Effectiveness",
763
+ ...runEffectivenessLines(manifest, tasks, input.executeWorkers, input.runtimeConfig),
764
+ "",
702
765
  "## Policy decisions",
703
766
  ...(manifest.policyDecisions?.length ? summarizePolicyDecisions(manifest.policyDecisions) : ["- (none)"]),
704
767
  "",
@@ -1,21 +1,21 @@
1
- export interface WorkerHeartbeatState {
2
- workerId: string;
3
- pid?: number;
4
- lastSeenAt: string;
5
- lastStdoutAt?: string;
6
- lastEventAt?: string;
7
- turnCount?: number;
8
- alive?: boolean;
9
- }
10
-
11
- export function createWorkerHeartbeat(workerId: string, pid?: number, now = new Date()): WorkerHeartbeatState {
12
- return { workerId, pid, lastSeenAt: now.toISOString(), alive: true };
13
- }
14
-
15
- export function touchWorkerHeartbeat(heartbeat: WorkerHeartbeatState, updates: Partial<Omit<WorkerHeartbeatState, "workerId">> = {}, now = new Date()): WorkerHeartbeatState {
16
- return { ...heartbeat, ...updates, lastSeenAt: now.toISOString() };
17
- }
18
-
19
- export function isWorkerHeartbeatStale(heartbeat: WorkerHeartbeatState, staleMs: number, now = new Date()): boolean {
20
- return now.getTime() - Date.parse(heartbeat.lastSeenAt) > staleMs;
21
- }
1
+ export interface WorkerHeartbeatState {
2
+ workerId: string;
3
+ pid?: number;
4
+ lastSeenAt: string;
5
+ lastStdoutAt?: string;
6
+ lastEventAt?: string;
7
+ turnCount?: number;
8
+ alive?: boolean;
9
+ }
10
+
11
+ export function createWorkerHeartbeat(workerId: string, pid?: number, now = new Date()): WorkerHeartbeatState {
12
+ return { workerId, pid, lastSeenAt: now.toISOString(), alive: true };
13
+ }
14
+
15
+ export function touchWorkerHeartbeat(heartbeat: WorkerHeartbeatState, updates: Partial<Omit<WorkerHeartbeatState, "workerId">> = {}, now = new Date()): WorkerHeartbeatState {
16
+ return { ...heartbeat, ...updates, lastSeenAt: now.toISOString() };
17
+ }
18
+
19
+ export function isWorkerHeartbeatStale(heartbeat: WorkerHeartbeatState, staleMs: number, now = new Date()): boolean {
20
+ return now.getTime() - Date.parse(heartbeat.lastSeenAt) > staleMs;
21
+ }
@@ -1,57 +1,57 @@
1
- export type WorkerLifecycleState = "spawning" | "trust_required" | "ready_for_prompt" | "running" | "finished" | "failed";
2
- export type StartupFailureClassification = "trust_required" | "prompt_misdelivery" | "prompt_acceptance_timeout" | "transport_dead" | "worker_crashed" | "unknown";
3
-
4
- export interface WorkerStartupEvidence {
5
- lastLifecycleState: WorkerLifecycleState;
6
- command: string;
7
- promptSentAt?: string;
8
- promptAccepted: boolean;
9
- trustPromptDetected: boolean;
10
- transportHealthy: boolean;
11
- childProcessAlive: boolean;
12
- elapsedMs: number;
13
- classification: StartupFailureClassification;
14
- stderrPreview?: string;
15
- }
16
-
17
- export function detectTrustPrompt(text: string): boolean {
18
- const lowered = text.toLowerCase();
19
- return lowered.includes("do you trust") || lowered.includes("trust this") || lowered.includes("untrusted") || lowered.includes("workspace trust") || lowered.includes("allow this folder");
20
- }
21
-
22
- export function classifyStartupFailure(evidence: Omit<WorkerStartupEvidence, "classification">): StartupFailureClassification {
23
- if (!evidence.transportHealthy) return "transport_dead";
24
- if (evidence.trustPromptDetected || evidence.lastLifecycleState === "trust_required") return "trust_required";
25
- if (evidence.promptSentAt && !evidence.promptAccepted && evidence.childProcessAlive) return "prompt_acceptance_timeout";
26
- if (evidence.promptSentAt && !evidence.promptAccepted && !evidence.childProcessAlive) return "worker_crashed";
27
- if (evidence.stderrPreview?.toLowerCase().includes("command not found") || evidence.stderrPreview?.toLowerCase().includes("not recognized")) return "prompt_misdelivery";
28
- if (!evidence.childProcessAlive && evidence.lastLifecycleState !== "finished") return "worker_crashed";
29
- return "unknown";
30
- }
31
-
32
- export function createStartupEvidence(input: {
33
- command: string;
34
- startedAt: Date;
35
- finishedAt?: Date;
36
- promptSentAt?: Date;
37
- promptAccepted?: boolean;
38
- stderr?: string;
39
- error?: string;
40
- exitCode?: number | null;
41
- }): WorkerStartupEvidence {
42
- const stderrPreview = (input.error || input.stderr || "").slice(0, 500) || undefined;
43
- const trustPromptDetected = detectTrustPrompt(stderrPreview ?? "");
44
- const childProcessAlive = input.exitCode === undefined || input.exitCode === null ? !input.finishedAt : false;
45
- const base: Omit<WorkerStartupEvidence, "classification"> = {
46
- lastLifecycleState: input.error || (input.exitCode !== undefined && input.exitCode !== null && input.exitCode !== 0) ? "failed" : input.finishedAt ? "finished" : "running",
47
- command: input.command,
48
- promptSentAt: input.promptSentAt?.toISOString(),
49
- promptAccepted: input.promptAccepted ?? !input.error,
50
- trustPromptDetected,
51
- transportHealthy: !input.error || !/enoent|spawn|transport/i.test(input.error),
52
- childProcessAlive,
53
- elapsedMs: Math.max(0, (input.finishedAt ?? new Date()).getTime() - input.startedAt.getTime()),
54
- stderrPreview,
55
- };
56
- return { ...base, classification: classifyStartupFailure(base) };
57
- }
1
+ export type WorkerLifecycleState = "spawning" | "trust_required" | "ready_for_prompt" | "running" | "finished" | "failed";
2
+ export type StartupFailureClassification = "trust_required" | "prompt_misdelivery" | "prompt_acceptance_timeout" | "transport_dead" | "worker_crashed" | "unknown";
3
+
4
+ export interface WorkerStartupEvidence {
5
+ lastLifecycleState: WorkerLifecycleState;
6
+ command: string;
7
+ promptSentAt?: string;
8
+ promptAccepted: boolean;
9
+ trustPromptDetected: boolean;
10
+ transportHealthy: boolean;
11
+ childProcessAlive: boolean;
12
+ elapsedMs: number;
13
+ classification: StartupFailureClassification;
14
+ stderrPreview?: string;
15
+ }
16
+
17
+ export function detectTrustPrompt(text: string): boolean {
18
+ const lowered = text.toLowerCase();
19
+ return lowered.includes("do you trust") || lowered.includes("trust this") || lowered.includes("untrusted") || lowered.includes("workspace trust") || lowered.includes("allow this folder");
20
+ }
21
+
22
+ export function classifyStartupFailure(evidence: Omit<WorkerStartupEvidence, "classification">): StartupFailureClassification {
23
+ if (!evidence.transportHealthy) return "transport_dead";
24
+ if (evidence.trustPromptDetected || evidence.lastLifecycleState === "trust_required") return "trust_required";
25
+ if (evidence.promptSentAt && !evidence.promptAccepted && evidence.childProcessAlive) return "prompt_acceptance_timeout";
26
+ if (evidence.promptSentAt && !evidence.promptAccepted && !evidence.childProcessAlive) return "worker_crashed";
27
+ if (evidence.stderrPreview?.toLowerCase().includes("command not found") || evidence.stderrPreview?.toLowerCase().includes("not recognized")) return "prompt_misdelivery";
28
+ if (!evidence.childProcessAlive && evidence.lastLifecycleState !== "finished") return "worker_crashed";
29
+ return "unknown";
30
+ }
31
+
32
+ export function createStartupEvidence(input: {
33
+ command: string;
34
+ startedAt: Date;
35
+ finishedAt?: Date;
36
+ promptSentAt?: Date;
37
+ promptAccepted?: boolean;
38
+ stderr?: string;
39
+ error?: string;
40
+ exitCode?: number | null;
41
+ }): WorkerStartupEvidence {
42
+ const stderrPreview = (input.error || input.stderr || "").slice(0, 500) || undefined;
43
+ const trustPromptDetected = detectTrustPrompt(stderrPreview ?? "");
44
+ const childProcessAlive = input.exitCode === undefined || input.exitCode === null ? !input.finishedAt : false;
45
+ const base: Omit<WorkerStartupEvidence, "classification"> = {
46
+ lastLifecycleState: input.error || (input.exitCode !== undefined && input.exitCode !== null && input.exitCode !== 0) ? "failed" : input.finishedAt ? "finished" : "running",
47
+ command: input.command,
48
+ promptSentAt: input.promptSentAt?.toISOString(),
49
+ promptAccepted: input.promptAccepted ?? !input.error,
50
+ trustPromptDetected,
51
+ transportHealthy: !input.error || !/enoent|spawn|transport/i.test(input.error),
52
+ childProcessAlive,
53
+ elapsedMs: Math.max(0, (input.finishedAt ?? new Date()).getTime() - input.startedAt.getTime()),
54
+ stderrPreview,
55
+ };
56
+ return { ...base, classification: classifyStartupFailure(base) };
57
+ }
@@ -39,6 +39,7 @@ export const PiTeamsRuntimeConfigSchema = Type.Object({
39
39
  groupJoinAckTimeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
40
40
  requirePlanApproval: Type.Optional(Type.Boolean()),
41
41
  completionMutationGuard: Type.Optional(Type.Union([Type.Literal("off"), Type.Literal("warn"), Type.Literal("fail")])),
42
+ effectivenessGuard: Type.Optional(Type.Union([Type.Literal("off"), Type.Literal("warn"), Type.Literal("block"), Type.Literal("fail")])),
42
43
  }, { additionalProperties: false });
43
44
 
44
45
  export const PiTeamsControlConfigSchema = Type.Object({
@@ -1,10 +1,10 @@
1
1
  import { Type } from "typebox";
2
2
 
3
3
  const SkillOverride = Type.Unsafe({
4
- description: "Skill name(s) to inject, array of skill names, or false to disable role defaults.",
4
+ description: "Skill name(s) to add to role/default skills, an array of skill names, or false to disable all injected skills for this run.",
5
5
  anyOf: [
6
- { type: "string" },
7
- { type: "array", items: { type: "string" } },
6
+ { type: "string", maxLength: 2048 },
7
+ { type: "array", maxItems: 32, items: { type: "string", maxLength: 80 } },
8
8
  { type: "boolean" },
9
9
  ],
10
10
  });