pi-crew 0.1.45 → 0.1.49

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 (178) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +5 -5
  3. package/agents/analyst.md +11 -11
  4. package/agents/critic.md +11 -11
  5. package/agents/executor.md +11 -11
  6. package/agents/explorer.md +11 -11
  7. package/agents/planner.md +11 -11
  8. package/agents/reviewer.md +11 -11
  9. package/agents/security-reviewer.md +11 -11
  10. package/agents/test-engineer.md +11 -11
  11. package/agents/verifier.md +11 -11
  12. package/agents/writer.md +11 -11
  13. package/docs/next-upgrade-roadmap.md +808 -0
  14. package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +261 -0
  15. package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +111 -0
  16. package/docs/research/AUDIT_OH_MY_PI.md +261 -0
  17. package/docs/research/AUDIT_PI_CREW.md +457 -0
  18. package/docs/research/CAVEMAN-DEEP-RESEARCH.md +281 -0
  19. package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +264 -0
  20. package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +343 -0
  21. package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +480 -0
  22. package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +354 -0
  23. package/docs/research/IMPLEMENTATION_PLAN.md +385 -0
  24. package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +502 -0
  25. package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +266 -0
  26. package/docs/research/REMAINING-GAPS-PLAN.md +363 -0
  27. package/docs/research/SESSION-SUMMARY-2026-05-08.md +146 -0
  28. package/docs/research/UI-RESPONSIVENESS-AUDIT.md +173 -0
  29. package/docs/research-awesome-agent-skills-distillation.md +100 -0
  30. package/docs/research-oh-my-pi-distillation.md +369 -0
  31. package/docs/source-runtime-refactor-map.md +24 -0
  32. package/docs/usage.md +3 -3
  33. package/install.mjs +52 -8
  34. package/package.json +99 -98
  35. package/schema.json +10 -1
  36. package/skills/async-worker-recovery/SKILL.md +42 -0
  37. package/skills/context-artifact-hygiene/SKILL.md +52 -0
  38. package/skills/delegation-patterns/SKILL.md +54 -0
  39. package/skills/mailbox-interactive/SKILL.md +40 -0
  40. package/skills/model-routing-context/SKILL.md +39 -0
  41. package/skills/multi-perspective-review/SKILL.md +58 -0
  42. package/skills/observability-reliability/SKILL.md +41 -0
  43. package/skills/orchestration/SKILL.md +157 -0
  44. package/skills/ownership-session-security/SKILL.md +41 -0
  45. package/skills/pi-extension-lifecycle/SKILL.md +39 -0
  46. package/skills/requirements-to-task-packet/SKILL.md +63 -0
  47. package/skills/resource-discovery-config/SKILL.md +41 -0
  48. package/skills/runtime-state-reader/SKILL.md +44 -0
  49. package/skills/secure-agent-orchestration-review/SKILL.md +45 -0
  50. package/skills/state-mutation-locking/SKILL.md +42 -0
  51. package/skills/systematic-debugging/SKILL.md +67 -0
  52. package/skills/ui-render-performance/SKILL.md +39 -0
  53. package/skills/verification-before-done/SKILL.md +57 -0
  54. package/skills/worktree-isolation/SKILL.md +39 -0
  55. package/src/agents/agent-config.ts +6 -0
  56. package/src/agents/agent-search.ts +98 -0
  57. package/src/agents/agent-serializer.ts +38 -34
  58. package/src/agents/discover-agents.ts +29 -15
  59. package/src/config/config.ts +72 -24
  60. package/src/config/defaults.ts +25 -0
  61. package/src/extension/autonomous-policy.ts +26 -33
  62. package/src/extension/help.ts +1 -0
  63. package/src/extension/management.ts +5 -0
  64. package/src/extension/project-init.ts +62 -2
  65. package/src/extension/register.ts +69 -22
  66. package/src/extension/registration/commands.ts +64 -25
  67. package/src/extension/registration/compaction-guard.ts +1 -1
  68. package/src/extension/registration/subagent-helpers.ts +8 -0
  69. package/src/extension/registration/subagent-tools.ts +149 -148
  70. package/src/extension/registration/team-tool.ts +14 -10
  71. package/src/extension/run-index.ts +35 -21
  72. package/src/extension/run-maintenance.ts +30 -5
  73. package/src/extension/team-tool/api.ts +47 -9
  74. package/src/extension/team-tool/cancel.ts +109 -5
  75. package/src/extension/team-tool/context.ts +8 -0
  76. package/src/extension/team-tool/intent-policy.ts +42 -0
  77. package/src/extension/team-tool/lifecycle-actions.ts +120 -79
  78. package/src/extension/team-tool/parallel-dispatch.ts +156 -0
  79. package/src/extension/team-tool/respond.ts +46 -18
  80. package/src/extension/team-tool/run.ts +55 -12
  81. package/src/extension/team-tool/status.ts +13 -2
  82. package/src/extension/team-tool-types.ts +3 -0
  83. package/src/extension/team-tool.ts +45 -14
  84. package/src/hooks/registry.ts +61 -0
  85. package/src/hooks/types.ts +41 -0
  86. package/src/observability/event-to-metric.ts +8 -1
  87. package/src/runtime/agent-control.ts +169 -63
  88. package/src/runtime/async-runner.ts +3 -1
  89. package/src/runtime/background-runner.ts +78 -53
  90. package/src/runtime/cancellation-token.ts +89 -0
  91. package/src/runtime/cancellation.ts +61 -0
  92. package/src/runtime/capability-inventory.ts +116 -0
  93. package/src/runtime/child-pi.ts +458 -444
  94. package/src/runtime/code-summary.ts +247 -0
  95. package/src/runtime/crash-recovery.ts +182 -0
  96. package/src/runtime/crew-agent-records.ts +70 -10
  97. package/src/runtime/crew-agent-runtime.ts +1 -0
  98. package/src/runtime/custom-tools/irc-tool.ts +201 -0
  99. package/src/runtime/custom-tools/submit-result-tool.ts +90 -0
  100. package/src/runtime/deadletter.ts +1 -0
  101. package/src/runtime/delivery-coordinator.ts +48 -25
  102. package/src/runtime/effectiveness.ts +81 -0
  103. package/src/runtime/event-stream-bridge.ts +90 -0
  104. package/src/runtime/live-agent-control.ts +2 -1
  105. package/src/runtime/live-agent-manager.ts +179 -85
  106. package/src/runtime/live-control-realtime.ts +1 -1
  107. package/src/runtime/live-extension-bridge.ts +150 -0
  108. package/src/runtime/live-irc.ts +92 -0
  109. package/src/runtime/live-session-health.ts +100 -0
  110. package/src/runtime/live-session-runtime.ts +599 -305
  111. package/src/runtime/manifest-cache.ts +17 -2
  112. package/src/runtime/mcp-proxy.ts +113 -0
  113. package/src/runtime/model-fallback.ts +6 -4
  114. package/src/runtime/notebook-helpers.ts +90 -0
  115. package/src/runtime/orphan-sentinel.ts +7 -0
  116. package/src/runtime/output-validator.ts +187 -0
  117. package/src/runtime/parallel-utils.ts +57 -0
  118. package/src/runtime/parent-guard.ts +80 -0
  119. package/src/runtime/pi-args.ts +18 -3
  120. package/src/runtime/process-status.ts +5 -1
  121. package/src/runtime/prose-compressor.ts +164 -0
  122. package/src/runtime/result-extractor.ts +121 -0
  123. package/src/runtime/retry-executor.ts +81 -64
  124. package/src/runtime/runtime-resolver.ts +23 -10
  125. package/src/runtime/semaphore.ts +131 -0
  126. package/src/runtime/sensitive-paths.ts +92 -0
  127. package/src/runtime/skill-instructions.ts +222 -0
  128. package/src/runtime/stale-reconciler.ts +4 -14
  129. package/src/runtime/stream-preview.ts +177 -0
  130. package/src/runtime/subagent-manager.ts +6 -2
  131. package/src/runtime/subprocess-tool-registry.ts +67 -0
  132. package/src/runtime/task-output-context.ts +177 -127
  133. package/src/runtime/task-runner/capabilities.ts +78 -0
  134. package/src/runtime/task-runner/live-executor.ts +107 -101
  135. package/src/runtime/task-runner/prompt-builder.ts +72 -8
  136. package/src/runtime/task-runner/prompt-pipeline.ts +64 -0
  137. package/src/runtime/task-runner/run-projection.ts +104 -0
  138. package/src/runtime/task-runner.ts +115 -5
  139. package/src/runtime/team-runner.ts +134 -19
  140. package/src/runtime/workspace-tree.ts +298 -0
  141. package/src/runtime/yield-handler.ts +189 -0
  142. package/src/schema/config-schema.ts +7 -0
  143. package/src/schema/team-tool-schema.ts +14 -4
  144. package/src/skills/discover-skills.ts +67 -0
  145. package/src/state/active-run-registry.ts +167 -0
  146. package/src/state/artifact-store.ts +4 -1
  147. package/src/state/atomic-write.ts +50 -1
  148. package/src/state/blob-store.ts +117 -0
  149. package/src/state/contracts.ts +2 -1
  150. package/src/state/event-log-rotation.ts +158 -0
  151. package/src/state/event-log.ts +52 -2
  152. package/src/state/mailbox.ts +129 -9
  153. package/src/state/state-store.ts +32 -5
  154. package/src/state/types.ts +64 -2
  155. package/src/teams/team-config.ts +1 -0
  156. package/src/ui/agent-management-overlay.ts +144 -0
  157. package/src/ui/crew-widget.ts +15 -5
  158. package/src/ui/dashboard-panes/cancellation-pane.ts +43 -0
  159. package/src/ui/dashboard-panes/capability-pane.ts +60 -0
  160. package/src/ui/dashboard-panes/mailbox-pane.ts +35 -11
  161. package/src/ui/dashboard-panes/progress-pane.ts +2 -0
  162. package/src/ui/live-run-sidebar.ts +4 -0
  163. package/src/ui/powerbar-publisher.ts +77 -15
  164. package/src/ui/render-coalescer.ts +51 -0
  165. package/src/ui/run-dashboard.ts +4 -0
  166. package/src/ui/run-event-bus.ts +209 -0
  167. package/src/ui/run-snapshot-cache.ts +78 -18
  168. package/src/ui/snapshot-types.ts +10 -0
  169. package/src/ui/transcript-entries.ts +258 -0
  170. package/src/utils/ids.ts +5 -0
  171. package/src/utils/incremental-reader.ts +104 -0
  172. package/src/utils/paths.ts +4 -2
  173. package/src/utils/scan-cache.ts +137 -0
  174. package/src/utils/sse-parser.ts +134 -0
  175. package/src/utils/task-name-generator.ts +337 -0
  176. package/src/utils/visual.ts +33 -2
  177. package/src/workflows/workflow-config.ts +1 -0
  178. package/src/worktree/cleanup.ts +2 -1
@@ -1,9 +1,24 @@
1
1
  import type { AgentConfig } from "../../agents/agent-config.ts";
2
- import type { TeamRunManifest, TeamTaskState } from "../../state/types.ts";
2
+ import type { TeamRunManifest, TeamTaskState, TaskOutputSchema } from "../../state/types.ts";
3
3
  import type { WorkflowStep } from "../../workflows/workflow-config.ts";
4
4
  import { buildMemoryBlock } from "../agent-memory.ts";
5
5
  import { permissionForRole } from "../role-permission.ts";
6
6
  import { renderTaskPacket } from "../task-packet.ts";
7
+ import { buildWorkspaceTree } from "../workspace-tree.ts";
8
+
9
+ /**
10
+ * When loadMode is "lean", emit a tool guidance block that tells the worker
11
+ * which tools to prefer. This is a prompt-level hint only — actual tool
12
+ * filtering at the Pi level is a future optimisation (Phase 3.2+).
13
+ */
14
+ export function toolGuidanceBlock(agent?: AgentConfig): string {
15
+ if (!agent || agent.loadMode !== "lean" || !agent.defaultTools?.length) return "";
16
+ return [
17
+ "# Tool Guidance",
18
+ `This role uses a focused tool set. Preferred tools: ${agent.defaultTools.join(", ")}.`,
19
+ "Other tools are available but should only be used when explicitly needed for the task.",
20
+ ].join("\n");
21
+ }
7
22
 
8
23
  function readOnlyRoleInstructions(role: string): string {
9
24
  if (permissionForRole(role) !== "read_only") return "";
@@ -36,9 +51,45 @@ function inputDependencyContext(task: TeamTaskState): string {
36
51
  return (task as TeamTaskState & { dependencyContextText?: string }).dependencyContextText ?? "";
37
52
  }
38
53
 
39
- export function renderTaskPrompt(manifest: TeamRunManifest, step: WorkflowStep, task: TeamTaskState, agent?: AgentConfig): string {
54
+ export function renderOutputSchemaBlock(outputSchema: TaskOutputSchema): string {
55
+ const lines: string[] = ["## Expected Output Format"];
56
+ lines.push(`Your final output must be ${outputSchema.format}.`);
57
+ if (outputSchema.description) {
58
+ lines.push(outputSchema.description);
59
+ }
60
+ if (outputSchema.format === "json" && outputSchema.schema) {
61
+ lines.push("The output must match this schema:");
62
+ lines.push("```json");
63
+ lines.push(JSON.stringify(outputSchema.schema, null, 2));
64
+ lines.push("```");
65
+ }
66
+ if (outputSchema.example) {
67
+ lines.push("Example output:");
68
+ lines.push("```");
69
+ lines.push(outputSchema.example);
70
+ lines.push("```");
71
+ }
72
+ return lines.join("\n");
73
+ }
74
+
75
+ export interface RenderedTaskPrompt {
76
+ /** Stable sections that rarely change between tasks of the same role/cwd. */
77
+ stablePrefix: string;
78
+ /** Dynamic sections that change per-task (goal, task packet, skills, dependency context). */
79
+ dynamicSuffix: string;
80
+ /** Full rendered prompt (stablePrefix + dynamicSuffix). */
81
+ full: string;
82
+ }
83
+
84
+ export async function renderTaskPrompt(manifest: TeamRunManifest, step: WorkflowStep, task: TeamTaskState, agent?: AgentConfig, skillBlock = ""): Promise<RenderedTaskPrompt> {
40
85
  const memoryBlock = agent?.memory ? buildMemoryBlock(agent.name, agent.memory, task.cwd, Boolean(agent.tools?.some((tool) => tool === "write" || tool === "edit"))) : "";
41
- return [
86
+
87
+ // Build workspace tree for stable context
88
+ const tree = await buildWorkspaceTree(task.cwd);
89
+ const treeBlock = tree.rendered ? `# Workspace Structure\n${tree.rendered}` : "";
90
+
91
+ // Stable prefix: role instructions, coordination, workspace tree — rarely changes
92
+ const stablePrefix = [
42
93
  "# pi-crew Worker Runtime Context",
43
94
  `Run ID: ${manifest.runId}`,
44
95
  `Team: ${manifest.team}`,
@@ -50,11 +101,6 @@ export function renderTaskPrompt(manifest: TeamRunManifest, step: WorkflowStep,
50
101
  `Task cwd: ${task.cwd}`,
51
102
  `Workspace mode: ${manifest.workspaceMode}`,
52
103
  "",
53
- `Goal:\n${manifest.goal}`,
54
- "",
55
- `Step: ${step.id}`,
56
- `Role: ${step.role}`,
57
- "",
58
104
  "Protocol:",
59
105
  "- Stay within the task scope unless the prompt explicitly says otherwise.",
60
106
  "- Report blockers and verification evidence in the final result.",
@@ -65,11 +111,29 @@ export function renderTaskPrompt(manifest: TeamRunManifest, step: WorkflowStep,
65
111
  "",
66
112
  coordinationBridgeInstructions(task),
67
113
  "",
114
+ treeBlock,
115
+ "",
116
+ toolGuidanceBlock(agent),
117
+ ].filter(Boolean).join("\n");
118
+
119
+ // Dynamic suffix: goal, step, skills, task packet, dependency context, memory — changes per task
120
+ const dynamicSuffix = [
121
+ `Goal:\n${manifest.goal}`,
122
+ "",
123
+ `Step: ${step.id}`,
124
+ `Role: ${step.role}`,
125
+ "",
126
+ skillBlock,
127
+ "",
68
128
  task.taskPacket ? renderTaskPacket(task.taskPacket) : "",
69
129
  "",
70
130
  (inputDependencyContext(task) || ""),
71
131
  memoryBlock,
132
+ task.taskPacket?.outputSchema ? renderOutputSchemaBlock(task.taskPacket.outputSchema) : "",
72
133
  "Task:",
73
134
  step.task.replaceAll("{goal}", manifest.goal),
74
135
  ].join("\n");
136
+
137
+ const full = [stablePrefix, "", dynamicSuffix].join("\n");
138
+ return { stablePrefix, dynamicSuffix, full };
75
139
  }
@@ -0,0 +1,64 @@
1
+ import * as path from "node:path";
2
+ import type { ArtifactDescriptor } from "../../state/types.ts";
3
+
4
+ export type WorkerPromptPipelineStageName =
5
+ | "task-packet-built"
6
+ | "dependency-context-collected"
7
+ | "skills-rendered-or-disabled"
8
+ | "capability-inventory-recorded"
9
+ | "coordination-bridge-attached"
10
+ | "prompt-rendered"
11
+ | "prompt-artifact-written";
12
+
13
+ export interface WorkerPromptPipelineStage {
14
+ name: WorkerPromptPipelineStageName;
15
+ references: string[];
16
+ details?: Record<string, string | number | boolean>;
17
+ }
18
+
19
+ export interface WorkerPromptPipelineArtifact {
20
+ schemaVersion: 1;
21
+ taskId: string;
22
+ stages: WorkerPromptPipelineStage[];
23
+ }
24
+
25
+ function artifactReference(artifactsRoot: string, artifact?: ArtifactDescriptor): string | undefined {
26
+ if (!artifact) return undefined;
27
+ const root = path.resolve(artifactsRoot);
28
+ const target = path.resolve(artifact.path);
29
+ const relative = path.relative(root, target);
30
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) return undefined;
31
+ return relative.replaceAll("\\", "/");
32
+ }
33
+
34
+ export interface BuildWorkerPromptPipelineInput {
35
+ artifactsRoot: string;
36
+ taskId: string;
37
+ promptArtifact: ArtifactDescriptor;
38
+ inputsArtifact: ArtifactDescriptor;
39
+ skillArtifact?: ArtifactDescriptor;
40
+ capabilityArtifact: ArtifactDescriptor;
41
+ coordinationArtifact: ArtifactDescriptor;
42
+ skillInstructionCount: number;
43
+ skillsDisabled: boolean;
44
+ }
45
+
46
+ export function buildWorkerPromptPipeline(input: BuildWorkerPromptPipelineInput): WorkerPromptPipelineArtifact {
47
+ return {
48
+ schemaVersion: 1,
49
+ taskId: input.taskId,
50
+ stages: [
51
+ { name: "task-packet-built", references: [`metadata/${input.taskId}.task-packet.json`] },
52
+ { name: "dependency-context-collected", references: [artifactReference(input.artifactsRoot, input.inputsArtifact) ?? `metadata/${input.taskId}.inputs.json`] },
53
+ {
54
+ name: "skills-rendered-or-disabled",
55
+ references: input.skillArtifact ? [artifactReference(input.artifactsRoot, input.skillArtifact) ?? `metadata/${input.taskId}.skills.md`] : [],
56
+ details: { disabled: input.skillsDisabled, skillInstructionCount: input.skillInstructionCount },
57
+ },
58
+ { name: "capability-inventory-recorded", references: [artifactReference(input.artifactsRoot, input.capabilityArtifact) ?? `metadata/${input.taskId}.capabilities.json`] },
59
+ { name: "coordination-bridge-attached", references: [artifactReference(input.artifactsRoot, input.coordinationArtifact) ?? `metadata/${input.taskId}.coordination-bridge.md`] },
60
+ { name: "prompt-rendered", references: [] },
61
+ { name: "prompt-artifact-written", references: [artifactReference(input.artifactsRoot, input.promptArtifact) ?? `prompts/${input.taskId}.md`] },
62
+ ],
63
+ };
64
+ }
@@ -0,0 +1,104 @@
1
+ import type { TeamRunManifest, TeamTaskState } from "../../state/types.ts";
2
+ import type { MailboxMessage } from "../../state/mailbox.ts";
3
+ import type { ArtifactDescriptor } from "../../state/types.ts";
4
+
5
+ export interface RunProjectionSource {
6
+ kind: "events" | "mailbox" | "artifacts" | "ui_metadata" | "runtime_metadata";
7
+ bounded: boolean;
8
+ reference?: string;
9
+ }
10
+
11
+ export interface RunProjectionResult {
12
+ sources: RunProjectionSource[];
13
+ summary: string;
14
+ injectedAsContext: boolean;
15
+ }
16
+
17
+ /**
18
+ * Transform run context before a worker starts.
19
+ * Builds a bounded projection of durable history that will be available
20
+ * to the worker as reference context, not as instructions.
21
+ *
22
+ * Rules:
23
+ * - Durable history retains events, mailbox, artifacts, UI/runtime metadata.
24
+ * - Worker prompt gets a bounded projection (truncated/summarized).
25
+ * - UI/runtime events are not prompt text unless explicitly selected.
26
+ */
27
+ export function transformRunContextBeforeWorkerStart(input: {
28
+ manifest: TeamRunManifest;
29
+ tasks: TeamTaskState[];
30
+ pendingMailbox: MailboxMessage[];
31
+ artifacts: ArtifactDescriptor[];
32
+ maxEvents?: number;
33
+ maxMailboxMessages?: number;
34
+ maxArtifactRefs?: number;
35
+ }): RunProjectionResult {
36
+ const maxEvents = input.maxEvents ?? 20;
37
+ const maxMailbox = input.maxMailboxMessages ?? 10;
38
+ const maxArtifacts = input.maxArtifactRefs ?? 15;
39
+
40
+ const sources: RunProjectionSource[] = [];
41
+ const lines: string[] = [];
42
+
43
+ // Project a bounded slice of task history
44
+ const completedTasks = input.tasks.filter((t) => t.status === "completed" || t.status === "failed");
45
+ if (completedTasks.length > 0) {
46
+ const tasks = completedTasks.slice(0, maxEvents);
47
+ sources.push({ kind: "events", bounded: true, reference: `tasks:${tasks.length}/${completedTasks.length}` });
48
+ lines.push(`Previous tasks (${tasks.length}/${completedTasks.length}):`);
49
+ for (const task of tasks) {
50
+ lines.push(`- ${task.id}: ${task.status}${task.error ? ` (${task.error})` : ""}`);
51
+ }
52
+ }
53
+
54
+ // Project pending mailbox that is relevant to this worker
55
+ if (input.pendingMailbox.length > 0) {
56
+ const messages = input.pendingMailbox.slice(0, maxMailbox);
57
+ sources.push({ kind: "mailbox", bounded: true, reference: `mailbox:${messages.length}/${input.pendingMailbox.length}` });
58
+ lines.push(`Pending messages (${messages.length}/${input.pendingMailbox.length}):`);
59
+ for (const msg of messages) {
60
+ lines.push(`- ${msg.kind ?? "message"}: ${msg.body.slice(0, 100)}`);
61
+ }
62
+ }
63
+
64
+ // Project artifact references (not content)
65
+ if (input.artifacts.length > 0) {
66
+ const artifacts = input.artifacts.slice(0, maxArtifacts);
67
+ sources.push({ kind: "artifacts", bounded: true, reference: `artifacts:${artifacts.length}/${input.artifacts.length}` });
68
+ lines.push(`Available artifacts (${artifacts.length}/${input.artifacts.length}):`);
69
+ for (const art of artifacts) {
70
+ lines.push(`- ${art.kind} (${art.producer})`);
71
+ }
72
+ }
73
+
74
+ // Metadata markers — not injected as prompt instructions
75
+ sources.push({ kind: "ui_metadata", bounded: false, reference: "excluded_from_prompt" });
76
+ sources.push({ kind: "runtime_metadata", bounded: false, reference: "excluded_from_prompt" });
77
+
78
+ return {
79
+ sources,
80
+ summary: lines.join("\n"),
81
+ injectedAsContext: true,
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Convert run history to a bounded worker prompt section.
87
+ * Same logic as transformRunContextBeforeWorkerStart but returns
88
+ * the prompt text directly for embedding in the worker prompt.
89
+ */
90
+ export function convertRunHistoryToWorkerPrompt(input: {
91
+ manifest: TeamRunManifest;
92
+ tasks: TeamTaskState[];
93
+ pendingMailbox: MailboxMessage[];
94
+ artifacts: ArtifactDescriptor[];
95
+ }): string {
96
+ const projection = transformRunContextBeforeWorkerStart(input);
97
+ if (!projection.summary) return "";
98
+ return [
99
+ "## Run Context (bounded projection)",
100
+ projection.summary,
101
+ "",
102
+ `Projection sources: ${projection.sources.map((s) => s.kind).join(", ")}`,
103
+ ].join("\n");
104
+ }
@@ -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";
@@ -13,21 +13,33 @@ import { buildConfiguredModelRouting, formatModelAttemptNote, isRetryableModelFa
13
13
  import { parsePiJsonOutput, type ParsedPiJsonOutput } from "./pi-json-output.ts";
14
14
  import { runChildPi } from "./child-pi.ts";
15
15
  import { buildTaskPacket } from "./task-packet.ts";
16
+ import { executeHook, appendHookEvent } from "../hooks/registry.ts";
16
17
  import { createVerificationEvidence } from "./green-contract.ts";
17
18
  import { createStartupEvidence } from "./worker-startup.ts";
18
19
  import { permissionForRole } from "./role-permission.ts";
19
20
  import { collectDependencyOutputContext, renderDependencyOutputContext, writeTaskInputsArtifact, writeTaskSharedOutput } from "./task-output-context.ts";
20
21
  import { appendCrewAgentEvent, appendCrewAgentOutput, emptyCrewAgentProgress, recordFromTask, upsertCrewAgent } from "./crew-agent-records.ts";
22
+ import { reserveControlChannel } from "./agent-control.ts";
21
23
  import { parseSessionUsage } from "./session-usage.ts";
22
24
  import type { CrewAgentProgress, CrewRuntimeKind } from "./crew-agent-runtime.ts";
23
25
  import { shouldAppendProgressEventUpdate, type ProgressEventSummary } from "./progress-event-coalescer.ts";
24
26
  import { coordinationBridgeInstructions, renderTaskPrompt } from "./task-runner/prompt-builder.ts";
27
+ import { buildWorkerPromptPipeline } from "./task-runner/prompt-pipeline.ts";
28
+ import { buildWorkerCapabilityInventory } from "./task-runner/capabilities.ts";
25
29
  import { applyAgentProgressEvent, applyUsageToProgress, progressEventSummary, shouldFlushProgressEvent } from "./task-runner/progress.ts";
26
30
  import { checkpointTask, persistSingleTaskUpdate, updateTask } from "./task-runner/state-helpers.ts";
27
31
  import { cleanResultText, isFinalChildEvent } from "./task-runner/result-utils.ts";
28
32
  import { evaluateCompletionMutationGuard } from "./completion-guard.ts";
33
+ import { cancellationReasonFromSignal, buildSyntheticTerminalEvidence } from "./cancellation.ts";
29
34
  import { appendTaskAttentionEvent } from "./attention-events.ts";
30
35
  import { parseSupervisorContactFromLine, recordSupervisorContact } from "./supervisor-contact.ts";
36
+ import { registerStreamBridge, bridgeEventFromJsonEvent } from "./event-stream-bridge.ts";
37
+ import { renderSkillInstructions } from "./skill-instructions.ts";
38
+ import { DEFAULT_YIELD_CONFIG, extractYieldResult, hasYieldInOutput, isYieldEvent, registerYieldTool, type YieldResult } from "./yield-handler.ts";
39
+ import { validateWorkerOutput, type OutputValidationResult } from "./output-validator.ts";
40
+
41
+ // Register the submit_result tool handler so subprocess events can extract yield data.
42
+ registerYieldTool();
31
43
 
32
44
  export interface TaskRunnerInput {
33
45
  manifest: TeamRunManifest;
@@ -43,14 +55,24 @@ export interface TaskRunnerInput {
43
55
  parentModel?: unknown;
44
56
  modelRegistry?: unknown;
45
57
  modelOverride?: string;
58
+ teamRoleModel?: string;
59
+ teamRoleSkills?: string[] | false;
60
+ skillOverride?: string[] | false;
46
61
  limits?: CrewLimitsConfig;
47
62
  dependencyContextText?: string;
63
+ skillBlock?: string;
64
+ skillNames?: string[];
65
+ skillPaths?: string[];
48
66
  /** Optional callback for JSON events from child Pi. Used for overflow recovery tracking. */
49
67
  onJsonEvent?: (taskId: string, runId: string, event: unknown) => void;
50
68
  }
51
69
 
52
70
  export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
53
71
  let manifest = input.manifest;
72
+ // H4: registerStreamBridge inside try so dispose() in finally is safe
73
+ let streamBridge: ReturnType<typeof registerStreamBridge> | undefined;
74
+ try {
75
+ streamBridge = registerStreamBridge(manifest.runId);
54
76
  const workspace = prepareTaskWorkspace(manifest, input.task);
55
77
  const worktree = workspace.worktreePath && workspace.branch ? { path: workspace.worktreePath, branch: workspace.branch, reused: workspace.reused ?? false } : input.task.worktree;
56
78
  const taskPacket = buildTaskPacket({ manifest, step: input.step, taskId: input.task.id, cwd: workspace.cwd, worktreePath: worktree?.path });
@@ -67,6 +89,8 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
67
89
  heartbeat: createWorkerHeartbeat(input.task.id),
68
90
  agentProgress: input.task.agentProgress ?? emptyCrewAgentProgress(),
69
91
  ...(dependencyContextText ? { dependencyContextText } : {}),
92
+ // Reserve control channel before spawn so cancel/steer can target this task immediately
93
+ controlReservation: reserveControlChannel(input.task.id, manifest.runId),
70
94
  } as TeamTaskState;
71
95
  let tasks = updateTask(input.tasks, task);
72
96
  const runtimeKind = input.runtimeKind ?? (input.executeWorkers ? "child-process" : "scaffold");
@@ -74,9 +98,17 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
74
98
  if (runtimeKind === "child-process") ({ task, tasks } = checkpointTask(manifest, tasks, task, "started"));
75
99
  upsertCrewAgent(manifest, recordFromTask(manifest, task, runtimeKind));
76
100
  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 } });
101
+ // Emit immediate UI notification so widget shows agent as "running" within ~100ms
102
+ // instead of waiting for child process first JSON event (2-5s delay).
103
+ streamBridge?.handler({ runId: manifest.runId, taskId: task.id, eventType: "task.started", timestamp: Date.now() });
77
104
  const permissionMode = permissionForRole(task.role);
105
+ 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;
106
+ const skillBlock = input.skillBlock ?? renderedSkills?.block;
107
+ const skillNames = input.skillNames ?? renderedSkills?.names;
108
+ const skillPaths = input.skillPaths ?? renderedSkills?.paths;
78
109
 
79
- const prompt = renderTaskPrompt(manifest, input.step, task, input.agent);
110
+ const promptResult = await renderTaskPrompt(manifest, input.step, task, input.agent, skillBlock);
111
+ const prompt = promptResult.full;
80
112
  const promptArtifact = writeArtifact(manifest.artifactsRoot, {
81
113
  kind: "prompt",
82
114
  relativePath: `prompts/${task.id}.md`,
@@ -93,9 +125,17 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
93
125
  let parsedOutput: ParsedPiJsonOutput | undefined;
94
126
  let finalStdout = "";
95
127
  let transcriptPath: string | undefined;
128
+ let terminalEvidence: OperationTerminalEvidence[] = [];
129
+ const collectedJsonEvents: Record<string, unknown>[] = [];
96
130
 
97
131
  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
132
  const inputsArtifact = writeTaskInputsArtifact(manifest, task, dependencyContext);
133
+ const skillArtifact = skillBlock ? writeArtifact(manifest.artifactsRoot, {
134
+ kind: "metadata",
135
+ relativePath: `metadata/${task.id}.skills.md`,
136
+ content: [`Selected skills: ${skillNames?.join(", ") ?? "(none)"}`, `Skill paths passed to child Pi: ${(skillPaths ?? []).length}`, "", skillBlock, ""].join("\n"),
137
+ producer: task.id,
138
+ }) : undefined;
99
139
  const coordinationArtifact = writeArtifact(manifest.artifactsRoot, {
100
140
  kind: "metadata",
101
141
  relativePath: `metadata/${task.id}.coordination-bridge.md`,
@@ -103,7 +143,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
103
143
  producer: task.id,
104
144
  });
105
145
  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 });
146
+ 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
147
  const candidates = modelRoutingPlan.candidates;
108
148
  const attemptModels = candidates.length > 0 ? candidates : [undefined];
109
149
  const logs: string[] = [];
@@ -151,6 +191,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
151
191
  signal: input.signal,
152
192
  transcriptPath,
153
193
  maxDepth: input.limits?.maxTaskDepth,
194
+ skillPaths,
154
195
  onSpawn: (pid) => {
155
196
  ({ task, tasks } = checkpointTask(manifest, tasks, task, "child-spawned", pid));
156
197
  },
@@ -165,9 +206,15 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
165
206
  },
166
207
  onJsonEvent: (event) => {
167
208
  appendCrewAgentEvent(manifest, task.id, event);
209
+ if (event && typeof event === "object" && !Array.isArray(event)) collectedJsonEvents.push(event as Record<string, unknown>);
168
210
  persistHeartbeat();
169
211
  task = { ...task, agentProgress: applyAgentProgressEvent(task.agentProgress ?? emptyCrewAgentProgress(), event, task.startedAt) };
170
212
  tasks = updateTask(tasks, task);
213
+ // Bridge event to UI event bus for near-instant updates
214
+ try {
215
+ const bridgeEvent = bridgeEventFromJsonEvent(manifest.runId, task.id, event);
216
+ if (bridgeEvent) streamBridge?.handler(bridgeEvent);
217
+ } catch { /* bridge errors should not affect task */ }
171
218
  // Feed overflow recovery tracker
172
219
  if (input.onJsonEvent) {
173
220
  try {
@@ -181,6 +228,13 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
181
228
  persistChildProgress(event);
182
229
  },
183
230
  });
231
+ const evidenceStatus = childResult.exitStatus?.cancelled ? "cancelled" : childResult.error || (childResult.exitCode && childResult.exitCode !== 0) ? "failed" : "completed";
232
+ 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 } : {}) }];
233
+ if (evidenceStatus === "cancelled") {
234
+ const cancelReason = input.signal?.aborted ? cancellationReasonFromSignal(input.signal) : { code: "caller_cancelled" as const, message: "Worker cancelled." };
235
+ terminalEvidence.push(buildSyntheticTerminalEvidence("tool", cancelReason, attemptStartedAt.toISOString()));
236
+ appendEvent(manifest.eventsPath, { type: "worker.cancelled", runId: manifest.runId, taskId: task.id, message: cancelReason.message, data: { terminalEvidence: terminalEvidence.at(-1) } });
237
+ }
184
238
  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
239
  exitCode = childResult.exitCode;
186
240
  finalStdout = childResult.stdout;
@@ -238,7 +292,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
238
292
  ({ task, tasks } = checkpointTask(manifest, tasks, task, "artifact-written"));
239
293
  } else if (runtimeKind === "live-session") {
240
294
  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 });
295
+ 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
296
  task = live.task;
243
297
  tasks = live.tasks;
244
298
  startupEvidence = live.startupEvidence;
@@ -262,6 +316,20 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
262
316
  });
263
317
  }
264
318
 
319
+ // --- Yield-based completion contract ---
320
+ let yieldResult: YieldResult | undefined;
321
+ const yieldEnabled = input.runtimeConfig?.yield?.enabled ?? DEFAULT_YIELD_CONFIG.enabled;
322
+ if (yieldEnabled && collectedJsonEvents.length > 0) {
323
+ if (hasYieldInOutput(collectedJsonEvents)) {
324
+ const yieldEvent = collectedJsonEvents.find((e) => isYieldEvent(e));
325
+ if (yieldEvent) {
326
+ yieldResult = extractYieldResult(yieldEvent);
327
+ }
328
+ } else if (!error) {
329
+ appendEvent(manifest.eventsPath, { type: "task.attention", runId: manifest.runId, taskId: task.id, message: "Worker completed without calling submit_result tool.", data: { activityState: "needs_attention", reason: "no_yield" } });
330
+ }
331
+ }
332
+
265
333
  const diffArtifact = workspace.worktreePath ? writeArtifact(manifest.artifactsRoot, {
266
334
  kind: "diff",
267
335
  relativePath: `diffs/${task.id}.diff`,
@@ -295,6 +363,22 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
295
363
  tasks = updateTask(tasks, task);
296
364
  }
297
365
 
366
+ // --- Output format validation (caveman Phase 4) ---
367
+ // Validate worker output against the role's output contract.
368
+ // On failure: emit attention event but don't fail the task.
369
+ let outputValidation: OutputValidationResult | undefined;
370
+ if (!error) {
371
+ const outputText = parsedOutput?.finalText ?? finalStdout;
372
+ if (outputText) {
373
+ outputValidation = validateWorkerOutput(task.role, outputText);
374
+ if (!outputValidation.valid) {
375
+ appendEvent(manifest.eventsPath, { type: "task.output_validation", runId: manifest.runId, taskId: task.id, data: { valid: false, formatMatch: outputValidation.formatMatch, structurePreserved: outputValidation.structurePreserved, issues: outputValidation.issues } });
376
+ task = { ...task, agentProgress: { ...(task.agentProgress ?? emptyCrewAgentProgress()), activityState: "needs_attention" } };
377
+ tasks = updateTask(tasks, task);
378
+ }
379
+ }
380
+ }
381
+
298
382
  task = {
299
383
  ...task,
300
384
  status: error ? "failed" : "completed",
@@ -310,6 +394,8 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
310
394
  resultArtifact,
311
395
  claim: undefined,
312
396
  heartbeat: touchWorkerHeartbeat(task.heartbeat ?? createWorkerHeartbeat(task.id), { alive: false }),
397
+ workerExitStatus: terminalEvidence.at(-1)?.exitStatus,
398
+ terminalEvidence: terminalEvidence.length ? [...(task.terminalEvidence ?? []), ...terminalEvidence] : task.terminalEvidence,
313
399
  ...(logArtifact ? { logArtifact } : {}),
314
400
  ...(transcriptArtifact ? { transcriptArtifact } : {}),
315
401
  };
@@ -339,10 +425,34 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
339
425
  content: `${JSON.stringify({ role: task.role, permissionMode }, null, 2)}\n`,
340
426
  producer: task.id,
341
427
  });
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] : [])] };
428
+ const capabilityArtifact = writeArtifact(manifest.artifactsRoot, {
429
+ kind: "metadata",
430
+ relativePath: `metadata/${task.id}.capabilities.json`,
431
+ 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`,
432
+ producer: task.id,
433
+ });
434
+ const promptPipelineArtifact = writeArtifact(manifest.artifactsRoot, {
435
+ kind: "metadata",
436
+ relativePath: `metadata/${task.id}.prompt-pipeline.json`,
437
+ 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`,
438
+ producer: task.id,
439
+ });
440
+ const outputValidationArtifact = outputValidation ? writeArtifact(manifest.artifactsRoot, {
441
+ kind: "metadata",
442
+ relativePath: `metadata/${task.id}.output-validation.json`,
443
+ content: `${JSON.stringify(outputValidation, null, 2)}\n`,
444
+ producer: task.id,
445
+ }) : undefined;
446
+ manifest = { ...manifest, updatedAt: new Date().toISOString(), artifacts: [...manifest.artifacts, promptArtifact, resultArtifact, inputsArtifact, coordinationArtifact, ...(skillArtifact ? [skillArtifact] : []), packetArtifact, verificationArtifact, startupArtifact, permissionArtifact, capabilityArtifact, promptPipelineArtifact, ...(outputValidationArtifact ? [outputValidationArtifact] : []), ...(sharedOutputArtifact ? [sharedOutputArtifact] : []), ...(logArtifact ? [logArtifact] : []), ...(transcriptArtifact ? [transcriptArtifact] : []), ...(diffArtifact ? [diffArtifact] : []), ...(diffStatArtifact ? [diffStatArtifact] : [])] };
343
447
  saveRunManifest(manifest);
344
448
  tasks = persistSingleTaskUpdate(manifest, tasks, task);
345
449
  upsertCrewAgent(manifest, recordFromTask(manifest, task, runtimeKind));
450
+ // Execute task_result hook before emitting terminal event
451
+ const hookReport = await executeHook("task_result", { runId: manifest.runId, taskId: task.id, cwd: manifest.cwd });
452
+ appendHookEvent(manifest, hookReport);
346
453
  appendEvent(manifest.eventsPath, { type: error ? "task.failed" : "task.completed", runId: manifest.runId, taskId: task.id, message: error });
347
454
  return { manifest, tasks };
455
+ } finally {
456
+ streamBridge?.dispose();
457
+ }
348
458
  }