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
@@ -3,6 +3,7 @@ import type { AgentConfig } from "../agents/agent-config.ts";
3
3
  import type { CrewLimitsConfig, CrewRuntimeConfig, CrewReliabilityConfig } from "../config/config.ts";
4
4
  import type { CrewRuntimeCapabilities } from "./runtime-resolver.ts";
5
5
  import { writeArtifact } from "../state/artifact-store.ts";
6
+ import { executeHook, appendHookEvent } from "../hooks/registry.ts";
6
7
  import { appendEvent } from "../state/event-log.ts";
7
8
  import type { TeamConfig } from "../teams/team-config.ts";
8
9
  import type { ArtifactDescriptor, PolicyDecision, TeamRunManifest, TaskAttemptState, TeamTaskState } from "../state/types.ts";
@@ -25,6 +26,8 @@ import { childCorrelation, withCorrelation } from "../observability/correlation.
25
26
  import { resolveBatchConcurrency } from "./concurrency.ts";
26
27
  import { mapConcurrent } from "./parallel-utils.ts";
27
28
  import { permissionForRole } from "./role-permission.ts";
29
+ import { CrewCancellationError, buildSyntheticTerminalEvidence, cancellationReasonFromSignal } from "./cancellation.ts";
30
+ import { effectivenessPolicyDecision, evaluateRunEffectiveness, formatRunEffectivenessLines } from "./effectiveness.ts";
28
31
 
29
32
  export interface ExecuteTeamRunInput {
30
33
  manifest: TeamRunManifest;
@@ -43,6 +46,8 @@ export interface ExecuteTeamRunInput {
43
46
  signal?: AbortSignal;
44
47
  reliability?: CrewReliabilityConfig;
45
48
  metricRegistry?: MetricRegistry;
49
+ /** Skill override from the team tool. false disables skill injection for this run. */
50
+ skillOverride?: string[] | false;
46
51
  /** Optional callback for JSON events from child Pi. Used for overflow recovery tracking. */
47
52
  onJsonEvent?: (taskId: string, runId: string, event: unknown) => void;
48
53
  }
@@ -380,7 +385,11 @@ function formatTaskProgress(task: TeamTaskState): string {
380
385
  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
386
  }
382
387
 
383
- function writeProgress(manifest: TeamRunManifest, tasks: TeamTaskState[], producer: string): TeamRunManifest {
388
+ function runEffectivenessLines(manifest: TeamRunManifest, tasks: TeamTaskState[], executeWorkers: boolean, runtimeConfig?: CrewRuntimeConfig): string[] {
389
+ return formatRunEffectivenessLines(evaluateRunEffectiveness({ manifest, tasks, executeWorkers, runtimeConfig }));
390
+ }
391
+
392
+ function writeProgress(manifest: TeamRunManifest, tasks: TeamTaskState[], producer: string, executeWorkers = true, runtimeConfig?: CrewRuntimeConfig): TeamRunManifest {
384
393
  const counts = new Map<string, number>();
385
394
  for (const task of tasks) counts.set(task.status, (counts.get(task.status) ?? 0) + 1);
386
395
  const queue = taskGraphSnapshot(tasks);
@@ -401,6 +410,9 @@ function writeProgress(manifest: TeamRunManifest, tasks: TeamTaskState[], produc
401
410
  "## Tasks",
402
411
  ...tasks.map(formatTaskProgress),
403
412
  "",
413
+ "## Effectiveness",
414
+ ...runEffectivenessLines(manifest, tasks, executeWorkers, runtimeConfig),
415
+ "",
404
416
  ].join("\n"),
405
417
  });
406
418
  return { ...manifest, updatedAt: new Date().toISOString(), artifacts: [...manifest.artifacts.filter((artifact) => !(artifact.kind === "progress" && artifact.path === progress.path)), progress] };
@@ -495,6 +507,39 @@ function hasPendingMutatingAdaptiveTask(tasks: TeamTaskState[]): boolean {
495
507
  export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
496
508
  let workflow = input.workflow;
497
509
  let manifest = updateRunStatus(input.manifest, "running", input.executeWorkers ? "Executing team workflow." : "Creating workflow prompts and placeholder results.");
510
+
511
+ try {
512
+ return await executeTeamRunCore(input, manifest, workflow);
513
+ } catch (error) {
514
+ // P1: Catch unhandled errors — ensure manifest is set to "failed" so it doesn't stay "running" forever.
515
+ const message = error instanceof Error ? error.message : String(error);
516
+ try {
517
+ manifest = updateRunStatus(manifest, "failed", `Unhandled error in team runner: ${message}`);
518
+ await saveRunManifestAsync(manifest);
519
+ } catch {
520
+ // Best-effort — state write may also fail
521
+ }
522
+ const tasks = refreshTaskGraphQueues(input.tasks).map((task) =>
523
+ task.status === "running" || task.status === "queued" || task.status === "waiting"
524
+ ? { ...task, status: "failed" as const, finishedAt: new Date().toISOString(), error: message }
525
+ : task,
526
+ );
527
+ return { manifest, tasks };
528
+ }
529
+ }
530
+
531
+ async function executeTeamRunCore(
532
+ input: ExecuteTeamRunInput,
533
+ manifest: TeamRunManifest,
534
+ workflow: WorkflowConfig,
535
+ ): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
536
+ // Execute before_run_start hook (non-blocking by default)
537
+ const beforeRunReport = await executeHook("before_run_start", { runId: manifest.runId, cwd: manifest.cwd });
538
+ appendHookEvent(manifest, beforeRunReport);
539
+ if (beforeRunReport.outcome === "block") {
540
+ manifest = updateRunStatus(manifest, "blocked", beforeRunReport.reason ?? "before_run_start hook blocked the run.");
541
+ return { manifest, tasks: input.tasks };
542
+ }
498
543
  let tasks = refreshTaskGraphQueues(input.tasks);
499
544
  let queueIndex = buildTaskGraphIndex(tasks);
500
545
  const canInjectAdaptivePlan = workflow.name === "implementation";
@@ -528,16 +573,28 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
528
573
  manifest = updateRunStatus(manifest, "cancelled", "Plan approval was cancelled.");
529
574
  return { manifest, tasks };
530
575
  }
531
- manifest = writeProgress(manifest, tasks, "team-runner");
576
+ manifest = writeProgress(manifest, tasks, "team-runner", input.executeWorkers, input.runtimeConfig);
532
577
  await saveRunManifestAsync(manifest);
533
578
  const runtimeKind = input.runtime?.kind ?? (input.executeWorkers ? "child-process" : "scaffold");
534
579
  saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
535
580
 
536
581
  while (tasks.some((task) => task.status === "queued")) {
537
582
  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);
583
+ const cancelReason = cancellationReasonFromSignal(input.signal);
584
+ const message = `${cancelReason.message} (${cancelReason.code})`;
585
+ const cancelledTaskIds: string[] = [];
586
+ tasks = tasks.map((task) => {
587
+ if (task.status !== "queued" && task.status !== "running" && task.status !== "waiting") return task;
588
+ cancelledTaskIds.push(task.id);
589
+ const base = { ...task, status: "cancelled" as const, finishedAt: new Date().toISOString(), error: message };
590
+ if (task.status === "running") {
591
+ return { ...base, terminalEvidence: [...(task.terminalEvidence ?? []), buildSyntheticTerminalEvidence("worker", cancelReason, task.startedAt)] };
592
+ }
593
+ return base;
594
+ });
539
595
  await saveRunTasksAsync(manifest, tasks);
540
- manifest = updateRunStatus(manifest, "cancelled", "Run cancelled.");
596
+ for (const taskId of cancelledTaskIds) appendEvent(manifest.eventsPath, { type: "task.cancelled", runId: manifest.runId, taskId, message, data: { reason: cancelReason.code } });
597
+ manifest = updateRunStatus(manifest, "cancelled", message, { data: { reason: cancelReason.code, cancelledTaskIds } });
541
598
  return { manifest, tasks };
542
599
  }
543
600
 
@@ -575,31 +632,44 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
575
632
  }
576
633
 
577
634
  appendEvent(manifest.eventsPath, { type: "task.progress", runId: manifest.runId, message: `Starting ready batch with ${readyBatch.length} task(s).`, data: { taskIds: readyBatch.map((task) => task.id), readyCount: snapshot.ready.length, blockedCount: snapshot.blocked.length, runningCount: snapshot.running.length, doneCount: snapshot.done.length, selectedCount: readyBatch.length, maxConcurrent: concurrency.maxConcurrent, defaultConcurrency: concurrency.defaultConcurrency, concurrencyReason: approvalPending ? `${concurrency.reason};plan-approval-read-only` : concurrency.reason } });
635
+ // Execute before_task_start hooks for the batch
636
+ for (const task of readyBatch) {
637
+ const taskReport = await executeHook("before_task_start", { runId: manifest.runId, taskId: task.id, cwd: manifest.cwd });
638
+ appendHookEvent(manifest, taskReport);
639
+ if (taskReport.outcome === "block") {
640
+ tasks = tasks.map((t) => t.id === task.id ? { ...t, status: "skipped" as const, error: taskReport.reason ?? "before_task_start hook blocked execution." } : t);
641
+ manifest = updateRunStatus(manifest, manifest.status, `Task '${task.id}' blocked by hook.`);
642
+ }
643
+ }
644
+ const batchTasks = readyBatch.filter((task) => tasks.find((t) => t.id === task.id && t.status !== "skipped"));
578
645
  const results = await mapConcurrent(
579
- readyBatch,
646
+ batchTasks,
580
647
  concurrency.selectedCount,
581
648
  async (task) => {
582
649
  const step = findStep(workflow, task);
583
650
  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 };
651
+ const teamRole = input.team.roles.find((role) => role.name === task.role);
652
+ 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
653
  if (input.reliability?.autoRetry !== true) return withCorrelation(childCorrelation(manifest.runId, task.id), () => runTeamTask(baseInput));
586
654
  let lastFailed: { manifest: TeamRunManifest; tasks: TeamTaskState[] } | undefined;
655
+ let lastAttemptId: string | undefined;
587
656
  const attemptsSoFar: TaskAttemptState[] = [...(task.attempts ?? [])];
588
657
  const policy = retryPolicyFromConfig(input.reliability);
589
658
  try {
590
- return await executeWithRetry(async (attempt) => {
659
+ return await executeWithRetry(async (attempt, info) => {
591
660
  const startedAt = new Date().toISOString();
592
- const inFlightAttempts: TaskAttemptState[] = [...attemptsSoFar, { startedAt }];
661
+ const inFlightAttempts: TaskAttemptState[] = [...attemptsSoFar, { attemptId: info.attemptId, startedAt }];
593
662
  input.metricRegistry?.counter("crew.task.retry_attempt_total", "Retry attempts by run and task").inc({ runId: manifest.runId, taskId: task.id });
594
663
  const fresh = loadRunManifestById(manifest.cwd, manifest.runId);
595
664
  const freshManifest = fresh?.manifest ?? manifest;
596
665
  const freshTasks = fresh?.tasks ?? tasks;
597
666
  const freshTask = freshTasks.find((item) => item.id === task.id) ?? task;
667
+ if (freshTask.status !== "queued" && freshTask.status !== "running") return { manifest: freshManifest, tasks: freshTasks };
598
668
  const taskWithAttempt: TeamTaskState = { ...freshTask, attempts: inFlightAttempts };
599
669
  const result = await withCorrelation(childCorrelation(freshManifest.runId, task.id), () => runTeamTask({ ...baseInput, manifest: freshManifest, tasks: freshTasks, task: taskWithAttempt }));
600
670
  const failed = failedTaskFrom(result, task.id);
601
671
  const endedAt = new Date().toISOString();
602
- const finishedAttempt: TaskAttemptState = { startedAt, endedAt, ...(failed?.error ? { error: failed.error } : {}) };
672
+ const finishedAttempt: TaskAttemptState = { attemptId: info.attemptId, startedAt, endedAt, ...(failed?.error ? { error: failed.error } : {}) };
603
673
  attemptsSoFar.push(finishedAttempt);
604
674
  const withAttempt = result.tasks.map((item) => item.id === task.id ? { ...item, attempts: [...attemptsSoFar] } : item);
605
675
  const enriched = { manifest: result.manifest, tasks: withAttempt };
@@ -611,28 +681,54 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
611
681
  return enriched;
612
682
  }, policy, {
613
683
  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 } });
684
+ attemptId: (attempt) => `${manifest.runId}:${task.id}:attempt-${attempt}`,
685
+ onAttemptFailed: (attempt, error, delayMs, info) => {
686
+ lastAttemptId = info.attemptId;
687
+ appendEvent(manifest.eventsPath, { type: "crew.task.retry_attempt", runId: manifest.runId, taskId: task.id, message: error.message, data: { attempt, attemptId: info.attemptId, delayMs }, metadata: { attemptId: info.attemptId } });
616
688
  input.metricRegistry?.histogram("crew.task.retry_delay_ms", "Retry backoff delay, milliseconds").observe({ runId: manifest.runId, taskId: task.id }, delayMs);
617
689
  },
618
- onRetryGivenUp: (attempts, error) => {
619
- appendDeadletter(manifest, { runId: manifest.runId, taskId: task.id, reason: "max-retries", attempts, lastError: error.message, timestamp: new Date().toISOString() });
690
+ onRetryGivenUp: (attempts, error, info) => {
691
+ lastAttemptId = info.attemptId;
692
+ appendDeadletter(manifest, { runId: manifest.runId, taskId: task.id, reason: "max-retries", attempts, attemptId: info.attemptId, lastError: error.message, timestamp: new Date().toISOString() });
620
693
  input.metricRegistry?.counter("crew.task.deadletter_total", "Deadletter triggers by reason").inc({ reason: "max-retries" });
621
694
  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
695
  },
623
696
  });
624
- } catch {
697
+ } catch (retryError) {
698
+ if (retryError instanceof CrewCancellationError || input.signal?.aborted) {
699
+ const reason = retryError instanceof CrewCancellationError ? retryError.reason : cancellationReasonFromSignal(input.signal);
700
+ const fresh = loadRunManifestById(manifest.cwd, manifest.runId);
701
+ const freshManifest = fresh?.manifest ?? manifest;
702
+ const freshTasks = fresh?.tasks ?? tasks;
703
+ 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);
704
+ appendEvent(freshManifest.eventsPath, { type: "task.cancelled", runId: freshManifest.runId, taskId: task.id, message: reason.message, data: { reason, phase: "retry" }, metadata: lastAttemptId ? { attemptId: lastAttemptId } : undefined });
705
+ return { manifest: updateRunStatus(freshManifest, "cancelled", reason.message), tasks: cancelledTasks };
706
+ }
625
707
  if (lastFailed) return lastFailed;
626
708
  const fresh = loadRunManifestById(manifest.cwd, manifest.runId);
627
709
  const freshManifest = fresh?.manifest ?? manifest;
628
710
  const freshTasks = fresh?.tasks ?? tasks;
629
711
  const freshTask = freshTasks.find((item) => item.id === task.id) ?? task;
712
+ if (freshTask.status !== "queued" && freshTask.status !== "running") return { manifest: freshManifest, tasks: freshTasks };
630
713
  return withCorrelation(childCorrelation(freshManifest.runId, task.id), () => runTeamTask({ ...baseInput, manifest: freshManifest, tasks: freshTasks, task: freshTask }));
631
714
  }
632
715
  },
633
716
  );
717
+ if (results.length === 0) break;
634
718
  manifest = { ...results.at(-1)!.manifest, artifacts: mergeArtifacts([manifest.artifacts, ...results.map((item) => item.manifest.artifacts)].flat()) };
635
719
  tasks = __test__mergeTaskUpdates(tasks, results);
720
+ const cancelledResult = results.find((item) => item.manifest.status === "cancelled");
721
+ if (cancelledResult || input.signal?.aborted) {
722
+ const reason = input.signal?.aborted ? cancellationReasonFromSignal(input.signal) : undefined;
723
+ const message = reason?.message ?? cancelledResult?.manifest.summary ?? "Run cancelled during task execution.";
724
+ manifest = { ...manifest, status: "running" };
725
+ manifest = updateRunStatus(manifest, "cancelled", message);
726
+ await saveRunTasksAsync(manifest, tasks);
727
+ saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
728
+ await saveRunManifestAsync(manifest);
729
+ appendEvent(manifest.eventsPath, { type: "run.cancelled", runId: manifest.runId, message, data: { reason, phase: "task-batch", cancelledResultRunId: cancelledResult?.manifest.runId } });
730
+ return { manifest, tasks };
731
+ }
636
732
  queueIndex = buildTaskGraphIndex(tasks);
637
733
  const injectedAfterBatch = attemptAdaptivePlan();
638
734
  if (injectedAfterBatch.missing) {
@@ -657,30 +753,46 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
657
753
  }
658
754
  await saveRunTasksAsync(manifest, tasks);
659
755
  saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
660
- const completedBatch = readyBatch.map((task) => tasks.find((item) => item.id === task.id) ?? task);
756
+ const completedBatch = batchTasks.map((task) => tasks.find((item) => item.id === task.id) ?? task);
661
757
  const batchArtifact = writeArtifact(manifest.artifactsRoot, {
662
758
  kind: "summary",
663
- relativePath: `batches/${readyBatch.map((task) => task.id).join("+")}.md`,
759
+ relativePath: `batches/${batchTasks.map((task) => task.id).join("+")}.md`,
664
760
  producer: "team-runner",
665
761
  content: aggregateTaskOutputs(completedBatch, manifest),
666
762
  });
667
- const groupDelivery = deliverGroupJoin({ manifest, mode: resolveGroupJoinMode(input.runtimeConfig), batch: readyBatch, allTasks: tasks });
763
+ const groupDelivery = deliverGroupJoin({ manifest, mode: resolveGroupJoinMode(input.runtimeConfig), batch: batchTasks, allTasks: tasks });
668
764
  manifest = { ...manifest, artifacts: mergeArtifacts([...manifest.artifacts, batchArtifact, ...(groupDelivery?.artifact ? [groupDelivery.artifact] : [])]) };
669
- manifest = writeProgress(manifest, tasks, "team-runner");
765
+ manifest = writeProgress(manifest, tasks, "team-runner", input.executeWorkers, input.runtimeConfig);
670
766
  await saveRunManifestAsync(manifest);
671
767
  }
672
768
 
673
769
  const failed = tasks.find((task) => task.status === "failed");
770
+ const waiting = tasks.find((task) => task.status === "waiting");
771
+ const running = tasks.find((task) => task.status === "running");
674
772
  manifest = applyPolicy(manifest, tasks, input.limits);
773
+ const effectiveness = evaluateRunEffectiveness({ manifest, tasks, executeWorkers: input.executeWorkers, runtimeConfig: input.runtimeConfig });
774
+ const effectivenessDecision = effectivenessPolicyDecision(effectiveness);
775
+ if (effectivenessDecision) {
776
+ manifest = { ...manifest, policyDecisions: [...(manifest.policyDecisions ?? []), effectivenessDecision], updatedAt: new Date().toISOString() };
777
+ appendEvent(manifest.eventsPath, { type: "run.effectiveness", runId: manifest.runId, message: effectivenessDecision.message, data: { effectiveness, policyDecision: effectivenessDecision } });
778
+ }
675
779
  const blockingDecision = manifest.policyDecisions?.find((item) => item.action === "block" || item.action === "escalate");
676
780
  if (failed) {
677
781
  manifest = updateRunStatus(manifest, "failed", `Failed at task '${failed.id}'.`);
782
+ } else if (waiting) {
783
+ manifest = updateRunStatus(manifest, "blocked", `Waiting for response to task '${waiting.id}'.`);
784
+ } else if (running) {
785
+ manifest = updateRunStatus(manifest, "blocked", `Task '${running.id}' is still running.`);
786
+ } else if (effectiveness.severity === "failed") {
787
+ manifest = updateRunStatus(manifest, "failed", effectivenessDecision?.message ?? "Run effectiveness guard failed.");
788
+ } else if (effectiveness.severity === "blocked") {
789
+ manifest = updateRunStatus(manifest, "blocked", effectivenessDecision?.message ?? "Run effectiveness guard blocked completion.");
678
790
  } else if (blockingDecision) {
679
791
  manifest = updateRunStatus(manifest, "blocked", blockingDecision.message);
680
792
  } else {
681
793
  manifest = updateRunStatus(manifest, "completed", input.executeWorkers ? "Team workflow completed." : "Team workflow scaffold completed without launching child workers.");
682
794
  }
683
- manifest = writeProgress(manifest, tasks, "team-runner");
795
+ manifest = writeProgress(manifest, tasks, "team-runner", input.executeWorkers, input.runtimeConfig);
684
796
  await saveRunManifestAsync(manifest);
685
797
  const usage = aggregateUsage(tasks);
686
798
  const summaryArtifact = writeArtifact(manifest.artifactsRoot, {
@@ -699,6 +811,9 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
699
811
  "## Tasks",
700
812
  ...tasks.map(formatTaskProgress),
701
813
  "",
814
+ "## Effectiveness",
815
+ ...runEffectivenessLines(manifest, tasks, input.executeWorkers, input.runtimeConfig),
816
+ "",
702
817
  "## Policy decisions",
703
818
  ...(manifest.policyDecisions?.length ? summarizePolicyDecisions(manifest.policyDecisions) : ["- (none)"]),
704
819
  "",
@@ -0,0 +1,298 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+
4
+ // ── Public types ───────────────────────────────────────────────────────
5
+
6
+ export interface WorkspaceTree {
7
+ rootPath: string;
8
+ rendered: string;
9
+ truncated: boolean;
10
+ totalLines: number;
11
+ }
12
+
13
+ export interface WorkspaceTreeOptions {
14
+ /** Directory depth below root. Root is depth 0. Default: 3 */
15
+ maxDepth?: number;
16
+ /** Max entries per directory. Default: 12 */
17
+ dirLimit?: number;
18
+ /** Hard line limit for the rendered output. Default: 120 */
19
+ lineCap?: number;
20
+ /** Directory names to skip entirely. Default: node_modules, .git, .next, dist, build, target, .venv, .cache, .turbo */
21
+ excludedDirs?: Set<string>;
22
+ }
23
+
24
+ // ── Defaults ───────────────────────────────────────────────────────────
25
+
26
+ const DEFAULT_MAX_DEPTH = 3;
27
+ const DEFAULT_DIR_LIMIT = 12;
28
+ const DEFAULT_LINE_CAP = 120;
29
+ const DEFAULT_EXCLUDED_DIRS: ReadonlySet<string> = new Set([
30
+ "node_modules",
31
+ ".git",
32
+ ".next",
33
+ "dist",
34
+ "build",
35
+ "target",
36
+ ".venv",
37
+ ".cache",
38
+ ".turbo",
39
+ ]);
40
+
41
+ // ── Internal types ─────────────────────────────────────────────────────
42
+
43
+ interface TreeNode {
44
+ name: string;
45
+ relativePath: string;
46
+ depth: number;
47
+ isDirectory: boolean;
48
+ mtimeMs: number;
49
+ size: number;
50
+ children: TreeNode[];
51
+ droppedChildCount: number;
52
+ }
53
+
54
+ interface RenderLine {
55
+ text: string;
56
+ depth: number;
57
+ isRoot: boolean;
58
+ }
59
+
60
+ // ── Helpers ────────────────────────────────────────────────────────────
61
+
62
+ export function formatBytes(bytes: number): string {
63
+ if (bytes < 1024) return `${bytes}B`;
64
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
65
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
66
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}GB`;
67
+ }
68
+
69
+ export function formatAge(seconds: number): string {
70
+ if (seconds < 60) return "just now";
71
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
72
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
73
+ if (seconds < 86400 * 14) return `${Math.floor(seconds / 86400)}d`;
74
+ return `${Math.floor(seconds / (86400 * 7))}w`;
75
+ }
76
+
77
+ // ── Tree building ──────────────────────────────────────────────────────
78
+
79
+ function compareByRecency(a: TreeNode, b: TreeNode): number {
80
+ const diff = b.mtimeMs - a.mtimeMs;
81
+ if (diff !== 0) return diff;
82
+ return a.name.localeCompare(b.name);
83
+ }
84
+
85
+ function applyDirLimit(
86
+ children: TreeNode[],
87
+ limit: number,
88
+ ): { visible: TreeNode[]; dropped: number } {
89
+ if (children.length <= limit) {
90
+ return { visible: children, dropped: 0 };
91
+ }
92
+ if (limit <= 1) {
93
+ return { visible: children.slice(0, limit), dropped: children.length - limit };
94
+ }
95
+ // Keep first (limit-1) by recency + always keep the oldest (last after sort)
96
+ const recent = children.slice(0, limit - 1);
97
+ const oldest = children[children.length - 1];
98
+ const visible = oldest ? [...recent, oldest] : recent;
99
+ return { visible, dropped: children.length - limit };
100
+ }
101
+
102
+ async function readChildren(
103
+ rootPath: string,
104
+ parent: TreeNode,
105
+ excludedDirs: ReadonlySet<string>,
106
+ ): Promise<TreeNode[]> {
107
+ const dirPath = parent.relativePath
108
+ ? path.join(rootPath, parent.relativePath)
109
+ : rootPath;
110
+
111
+ let names: string[];
112
+ try {
113
+ names = await fs.readdir(dirPath);
114
+ } catch {
115
+ return [];
116
+ }
117
+
118
+ const nodes = await Promise.all(
119
+ names.map(async (name): Promise<TreeNode | null> => {
120
+ // Skip hidden entries
121
+ if (name.startsWith(".")) return null;
122
+ const relativePath = parent.relativePath
123
+ ? `${parent.relativePath}/${name}`
124
+ : name;
125
+ const absolutePath = path.join(rootPath, relativePath);
126
+ try {
127
+ const stat = await fs.stat(absolutePath);
128
+ if (stat.isDirectory() && excludedDirs.has(name)) return null;
129
+ return {
130
+ name,
131
+ relativePath,
132
+ depth: parent.depth + 1,
133
+ isDirectory: stat.isDirectory(),
134
+ mtimeMs: stat.mtimeMs,
135
+ size: stat.size,
136
+ children: [],
137
+ droppedChildCount: 0,
138
+ };
139
+ } catch {
140
+ return null;
141
+ }
142
+ }),
143
+ );
144
+
145
+ return nodes.filter((n): n is TreeNode => n !== null).sort(compareByRecency);
146
+ }
147
+
148
+ async function collectTree(
149
+ rootPath: string,
150
+ maxDepth: number,
151
+ dirLimit: number,
152
+ excludedDirs: ReadonlySet<string>,
153
+ ): Promise<{ root: TreeNode; truncated: boolean }> {
154
+ const rootStat = await fs.stat(rootPath);
155
+ const root: TreeNode = {
156
+ name: ".",
157
+ relativePath: "",
158
+ depth: 0,
159
+ isDirectory: true,
160
+ mtimeMs: rootStat.mtimeMs,
161
+ size: rootStat.size,
162
+ children: [],
163
+ droppedChildCount: 0,
164
+ };
165
+
166
+ let truncated = false;
167
+ const queue: TreeNode[] = [root];
168
+ let cursor = 0;
169
+
170
+ while (cursor < queue.length) {
171
+ const parent = queue[cursor];
172
+ cursor += 1;
173
+ if (!parent || parent.depth >= maxDepth) continue;
174
+
175
+ const children = await readChildren(rootPath, parent, excludedDirs);
176
+ const { visible, dropped } = applyDirLimit(children, dirLimit);
177
+ parent.children = visible;
178
+ parent.droppedChildCount = dropped;
179
+ if (dropped > 0) truncated = true;
180
+
181
+ for (const child of visible) {
182
+ if (child.isDirectory) queue.push(child);
183
+ }
184
+ }
185
+
186
+ return { root, truncated };
187
+ }
188
+
189
+ // ── Rendering ──────────────────────────────────────────────────────────
190
+
191
+ function collectLines(node: TreeNode, nowMs: number, lines: RenderLine[]): void {
192
+ if (node.depth === 0) {
193
+ lines.push({ text: ".", depth: 0, isRoot: true });
194
+ } else {
195
+ const indent = " ".repeat(node.depth);
196
+ const suffix = node.isDirectory ? "/" : "";
197
+ const label = `${indent}- ${node.name}${suffix}`;
198
+ if (node.isDirectory) {
199
+ lines.push({ text: label, depth: node.depth, isRoot: false });
200
+ } else {
201
+ const ageSeconds = Math.max(0, Math.floor((nowMs - node.mtimeMs) / 1000));
202
+ const size = formatBytes(node.size);
203
+ const age = formatAge(ageSeconds);
204
+ lines.push({ text: `${label} ${size} ${age}`, depth: node.depth, isRoot: false });
205
+ }
206
+ }
207
+
208
+ if (node.droppedChildCount > 0) {
209
+ // When we kept recent + oldest, render recent children, then truncation line, then oldest
210
+ const recentChildren = node.children.slice(0, -1);
211
+ const oldestChild = node.children[node.children.length - 1];
212
+ for (const child of recentChildren) collectLines(child, nowMs, lines);
213
+
214
+ const childDepth = node.depth + 1;
215
+ const indent = " ".repeat(childDepth);
216
+ lines.push({
217
+ text: `${indent}- … ${node.droppedChildCount} more`,
218
+ depth: childDepth,
219
+ isRoot: false,
220
+ });
221
+
222
+ if (oldestChild) collectLines(oldestChild, nowMs, lines);
223
+ } else {
224
+ for (const child of node.children) collectLines(child, nowMs, lines);
225
+ }
226
+ }
227
+
228
+ function applyLineCap(
229
+ lines: RenderLine[],
230
+ cap: number,
231
+ ): { lines: RenderLine[]; elided: number } {
232
+ if (lines.length <= cap) return { lines, elided: 0 };
233
+
234
+ const target = Math.max(1, cap - 1);
235
+ const removeCount = lines.length - target;
236
+ // Remove deepest non-root entries first
237
+ const removable = lines
238
+ .map((line, index) => ({ line, index }))
239
+ .filter((item) => !item.line.isRoot)
240
+ .sort((a, b) => b.line.depth - a.line.depth || b.index - a.index)
241
+ .slice(0, removeCount);
242
+
243
+ if (removable.length === 0) return { lines, elided: 0 };
244
+
245
+ const removedIndexes = new Set(removable.map((item) => item.index));
246
+ const kept = lines.filter((_, index) => !removedIndexes.has(index));
247
+ kept.push({
248
+ text: `… (${removable.length} lines elided)`,
249
+ depth: 0,
250
+ isRoot: false,
251
+ });
252
+ return { lines: kept, elided: removable.length };
253
+ }
254
+
255
+ // ── Public API ─────────────────────────────────────────────────────────
256
+
257
+ const emptyResult = (rootPath: string): WorkspaceTree => ({
258
+ rootPath,
259
+ rendered: "",
260
+ truncated: false,
261
+ totalLines: 0,
262
+ });
263
+
264
+ export async function buildWorkspaceTree(
265
+ cwd: string,
266
+ options?: WorkspaceTreeOptions,
267
+ ): Promise<WorkspaceTree> {
268
+ const rootPath = path.resolve(cwd);
269
+ try {
270
+ const maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
271
+ const dirLimit = options?.dirLimit ?? DEFAULT_DIR_LIMIT;
272
+ const lineCap = options?.lineCap ?? DEFAULT_LINE_CAP;
273
+ const excludedDirs = options?.excludedDirs ?? DEFAULT_EXCLUDED_DIRS;
274
+
275
+ const { root, truncated: dirTruncated } = await collectTree(
276
+ rootPath,
277
+ maxDepth,
278
+ dirLimit,
279
+ excludedDirs,
280
+ );
281
+
282
+ const nowMs = Date.now();
283
+ const lines: RenderLine[] = [];
284
+ collectLines(root, nowMs, lines);
285
+
286
+ const { lines: capped, elided } = applyLineCap(lines, lineCap);
287
+ const rendered = capped.map((l) => l.text).join("\n");
288
+
289
+ return {
290
+ rootPath,
291
+ rendered,
292
+ truncated: dirTruncated || elided > 0,
293
+ totalLines: capped.length,
294
+ };
295
+ } catch {
296
+ return emptyResult(rootPath);
297
+ }
298
+ }