pi-crew 0.1.51 → 0.2.0

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 (239) hide show
  1. package/CHANGELOG.md +56 -1
  2. package/README.md +176 -781
  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 +70 -11
  12. package/agents/writer.md +11 -11
  13. package/docs/actions-reference.md +595 -0
  14. package/docs/commands-reference.md +347 -0
  15. package/docs/runtime-flow.md +148 -148
  16. package/index.ts +6 -6
  17. package/package.json +99 -99
  18. package/skills/async-worker-recovery/SKILL.md +42 -42
  19. package/skills/context-artifact-hygiene/SKILL.md +52 -52
  20. package/skills/delegation-patterns/SKILL.md +54 -54
  21. package/skills/mailbox-interactive/SKILL.md +40 -40
  22. package/skills/model-routing-context/SKILL.md +39 -39
  23. package/skills/multi-perspective-review/SKILL.md +58 -58
  24. package/skills/observability-reliability/SKILL.md +41 -41
  25. package/skills/orchestration/SKILL.md +157 -157
  26. package/skills/ownership-session-security/SKILL.md +41 -41
  27. package/skills/pi-extension-lifecycle/SKILL.md +39 -39
  28. package/skills/requirements-to-task-packet/SKILL.md +63 -63
  29. package/skills/resource-discovery-config/SKILL.md +41 -41
  30. package/skills/runtime-state-reader/SKILL.md +44 -44
  31. package/skills/secure-agent-orchestration-review/SKILL.md +45 -45
  32. package/skills/state-mutation-locking/SKILL.md +42 -42
  33. package/skills/systematic-debugging/SKILL.md +67 -67
  34. package/skills/ui-render-performance/SKILL.md +39 -39
  35. package/skills/verification-before-done/SKILL.md +57 -57
  36. package/skills/worktree-isolation/SKILL.md +39 -39
  37. package/src/adapters/claude-adapter.ts +25 -0
  38. package/src/adapters/codex-adapter.ts +21 -0
  39. package/src/adapters/cursor-adapter.ts +17 -0
  40. package/src/adapters/export-util.ts +137 -0
  41. package/src/adapters/index.ts +15 -0
  42. package/src/adapters/registry.ts +18 -0
  43. package/src/adapters/types.ts +23 -0
  44. package/src/agents/agent-config.ts +2 -0
  45. package/src/agents/agent-search.ts +98 -98
  46. package/src/agents/discover-agents.ts +2 -1
  47. package/src/config/config.ts +13 -1
  48. package/src/config/drift-detector.ts +211 -0
  49. package/src/config/markers.ts +327 -0
  50. package/src/config/resilient-parser.ts +108 -0
  51. package/src/config/suggestions.ts +74 -0
  52. package/src/extension/cross-extension-rpc.ts +103 -94
  53. package/src/extension/project-init.ts +21 -1
  54. package/src/extension/register.ts +45 -14
  55. package/src/extension/registration/commands.ts +77 -8
  56. package/src/extension/registration/subagent-tools.ts +10 -1
  57. package/src/extension/registration/team-tool.ts +10 -1
  58. package/src/extension/registration/viewers.ts +48 -34
  59. package/src/extension/run-bundle-schema.ts +89 -89
  60. package/src/extension/run-import.ts +25 -1
  61. package/src/extension/run-index.ts +5 -1
  62. package/src/extension/run-maintenance.ts +142 -68
  63. package/src/extension/team-manager-command.ts +10 -1
  64. package/src/extension/team-tool/doctor.ts +28 -3
  65. package/src/extension/team-tool/handle-settings.ts +195 -188
  66. package/src/extension/team-tool/inspect.ts +41 -41
  67. package/src/extension/team-tool/intent-policy.ts +42 -42
  68. package/src/extension/team-tool/lifecycle-actions.ts +27 -8
  69. package/src/extension/team-tool/plan.ts +19 -19
  70. package/src/extension/team-tool/run.ts +12 -1
  71. package/src/extension/team-tool.ts +11 -1
  72. package/src/i18n.ts +184 -184
  73. package/src/observability/exporters/otlp-exporter.ts +92 -77
  74. package/src/prompt/prompt-runtime.ts +72 -72
  75. package/src/runtime/agent-memory.ts +72 -72
  76. package/src/runtime/agent-observability.ts +114 -114
  77. package/src/runtime/async-marker.ts +26 -26
  78. package/src/runtime/attention-events.ts +28 -28
  79. package/src/runtime/auto-resume.ts +100 -0
  80. package/src/runtime/background-runner.ts +11 -1
  81. package/src/runtime/cancellation-token.ts +89 -89
  82. package/src/runtime/cancellation.ts +61 -61
  83. package/src/runtime/capability-inventory.ts +116 -116
  84. package/src/runtime/child-pi.ts +7 -2
  85. package/src/runtime/compaction-summary.ts +271 -0
  86. package/src/runtime/completion-guard.ts +190 -190
  87. package/src/runtime/crash-recovery.ts +33 -0
  88. package/src/runtime/delta-conflict.ts +360 -0
  89. package/src/runtime/direct-run.ts +35 -35
  90. package/src/runtime/foreground-control.ts +82 -82
  91. package/src/runtime/green-contract.ts +46 -46
  92. package/src/runtime/group-join.ts +106 -106
  93. package/src/runtime/heartbeat-gradient.ts +28 -28
  94. package/src/runtime/heartbeat-watcher.ts +124 -124
  95. package/src/runtime/iteration-hooks.ts +262 -0
  96. package/src/runtime/live-agent-control.ts +88 -88
  97. package/src/runtime/live-control-realtime.ts +36 -36
  98. package/src/runtime/live-extension-bridge.ts +150 -150
  99. package/src/runtime/live-irc.ts +92 -92
  100. package/src/runtime/live-session-health.ts +100 -100
  101. package/src/runtime/loop-gates.ts +129 -0
  102. package/src/runtime/metric-parser.ts +40 -0
  103. package/src/runtime/notebook-helpers.ts +90 -90
  104. package/src/runtime/orphan-sentinel.ts +7 -7
  105. package/src/runtime/parallel-research.ts +44 -44
  106. package/src/runtime/phase-progress.ts +217 -0
  107. package/src/runtime/pi-args.ts +38 -11
  108. package/src/runtime/pi-json-output.ts +111 -111
  109. package/src/runtime/pi-spawn.ts +57 -7
  110. package/src/runtime/policy-engine.ts +79 -79
  111. package/src/runtime/post-checks.ts +122 -0
  112. package/src/runtime/progress-event-coalescer.ts +43 -43
  113. package/src/runtime/prose-compressor.ts +164 -164
  114. package/src/runtime/recovery-recipes.ts +74 -74
  115. package/src/runtime/result-extractor.ts +121 -121
  116. package/src/runtime/role-permission.ts +39 -39
  117. package/src/runtime/sensitive-paths.ts +2 -2
  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/stream-preview.ts +177 -177
  123. package/src/runtime/supervisor-contact.ts +59 -59
  124. package/src/runtime/task-display.ts +38 -38
  125. package/src/runtime/task-graph.ts +207 -0
  126. package/src/runtime/task-quality.ts +207 -0
  127. package/src/runtime/task-runner/capabilities.ts +78 -78
  128. package/src/runtime/task-runner/live-executor.ts +7 -1
  129. package/src/runtime/task-runner/progress.ts +119 -119
  130. package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
  131. package/src/runtime/task-runner/result-utils.ts +14 -14
  132. package/src/runtime/task-runner/run-projection.ts +103 -103
  133. package/src/runtime/task-runner/state-helpers.ts +22 -22
  134. package/src/runtime/team-runner.ts +117 -7
  135. package/src/runtime/worker-heartbeat.ts +21 -21
  136. package/src/runtime/worker-startup.ts +57 -57
  137. package/src/runtime/workflow-state.ts +187 -0
  138. package/src/runtime/workspace-tree.ts +298 -298
  139. package/src/schema/config-schema.ts +11 -0
  140. package/src/schema/validation-types.ts +148 -0
  141. package/src/skills/skill-templates.ts +374 -0
  142. package/src/state/active-run-registry.ts +35 -11
  143. package/src/state/atomic-write.ts +33 -26
  144. package/src/state/contracts.ts +1 -0
  145. package/src/state/event-reconstructor.ts +217 -0
  146. package/src/state/locks.ts +2 -13
  147. package/src/state/mailbox.ts +4 -3
  148. package/src/state/state-store.ts +32 -14
  149. package/src/state/task-claims.ts +44 -44
  150. package/src/state/types.ts +9 -0
  151. package/src/state/usage.ts +29 -29
  152. package/src/subagents/async-entry.ts +1 -1
  153. package/src/subagents/index.ts +3 -3
  154. package/src/subagents/live/control.ts +1 -1
  155. package/src/subagents/live/manager.ts +1 -1
  156. package/src/subagents/live/realtime.ts +1 -1
  157. package/src/subagents/live/session-runtime.ts +1 -1
  158. package/src/subagents/manager.ts +1 -1
  159. package/src/subagents/spawn.ts +1 -1
  160. package/src/teams/team-serializer.ts +38 -38
  161. package/src/types/diff.d.ts +18 -18
  162. package/src/ui/crew-footer.ts +101 -101
  163. package/src/ui/crew-select-list.ts +111 -111
  164. package/src/ui/crew-widget.ts +5 -2
  165. package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
  166. package/src/ui/dashboard-panes/capability-pane.ts +59 -59
  167. package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
  168. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  169. package/src/ui/dashboard-panes/progress-pane.ts +11 -0
  170. package/src/ui/dynamic-border.ts +25 -25
  171. package/src/ui/layout-primitives.ts +106 -106
  172. package/src/ui/loaders.ts +158 -158
  173. package/src/ui/render-coalescer.ts +51 -51
  174. package/src/ui/render-diff.ts +119 -119
  175. package/src/ui/render-scheduler.ts +143 -143
  176. package/src/ui/run-action-dispatcher.ts +10 -1
  177. package/src/ui/spinner.ts +17 -17
  178. package/src/ui/status-colors.ts +58 -58
  179. package/src/ui/syntax-highlight.ts +116 -116
  180. package/src/ui/transcript-entries.ts +258 -258
  181. package/src/utils/completion-dedupe.ts +63 -63
  182. package/src/utils/frontmatter.ts +68 -68
  183. package/src/utils/git.ts +262 -262
  184. package/src/utils/ids.ts +17 -17
  185. package/src/utils/incremental-reader.ts +104 -104
  186. package/src/utils/names.ts +27 -27
  187. package/src/utils/redaction.ts +44 -44
  188. package/src/utils/safe-paths.ts +47 -47
  189. package/src/utils/scan-cache.ts +136 -136
  190. package/src/utils/sleep.ts +40 -26
  191. package/src/utils/task-name-generator.ts +337 -337
  192. package/src/workflows/validate-workflow.ts +40 -40
  193. package/src/worktree/branch-freshness.ts +45 -45
  194. package/teams/default.team.md +12 -12
  195. package/teams/fast-fix.team.md +11 -11
  196. package/teams/implementation.team.md +18 -18
  197. package/teams/parallel-research.team.md +14 -14
  198. package/teams/research.team.md +11 -11
  199. package/teams/review.team.md +12 -12
  200. package/workflows/default.workflow.md +30 -29
  201. package/workflows/fast-fix.workflow.md +23 -22
  202. package/workflows/implementation.workflow.md +43 -43
  203. package/workflows/parallel-research.workflow.md +46 -46
  204. package/workflows/research.workflow.md +22 -22
  205. package/workflows/review.workflow.md +30 -30
  206. package/docs/refactor-tasks-phase3.md +0 -394
  207. package/docs/refactor-tasks-phase4.md +0 -564
  208. package/docs/refactor-tasks-phase5.md +0 -402
  209. package/docs/refactor-tasks-phase6.md +0 -662
  210. package/docs/refactor-tasks.md +0 -1484
  211. package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +0 -261
  212. package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +0 -111
  213. package/docs/research/AUDIT_OH_MY_PI.md +0 -261
  214. package/docs/research/AUDIT_PI_CREW.md +0 -457
  215. package/docs/research/CAVEMAN-DEEP-RESEARCH.md +0 -281
  216. package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +0 -264
  217. package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +0 -343
  218. package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +0 -480
  219. package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +0 -354
  220. package/docs/research/IMPLEMENTATION_PLAN.md +0 -385
  221. package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +0 -502
  222. package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +0 -266
  223. package/docs/research/REMAINING-GAPS-PLAN.md +0 -363
  224. package/docs/research/SESSION-SUMMARY-2026-05-08.md +0 -146
  225. package/docs/research/UI-RESPONSIVENESS-AUDIT.md +0 -173
  226. package/docs/research-awesome-agent-skills-distillation.md +0 -100
  227. package/docs/research-extension-examples.md +0 -297
  228. package/docs/research-extension-system.md +0 -324
  229. package/docs/research-oh-my-pi-distillation.md +0 -369
  230. package/docs/research-optimization-plan.md +0 -548
  231. package/docs/research-phase10-distillation.md +0 -199
  232. package/docs/research-phase11-distillation.md +0 -201
  233. package/docs/research-phase8-operator-experience-plan.md +0 -819
  234. package/docs/research-phase9-observability-reliability-plan.md +0 -1190
  235. package/docs/research-pi-coding-agent.md +0 -357
  236. package/docs/research-source-pi-crew-reference.md +0 -174
  237. package/docs/research-ui-optimization-plan.md +0 -480
  238. package/docs/source-runtime-refactor-map.md +0 -107
  239. package/src/utils/atomic-write.ts +0 -33
@@ -1,59 +1,59 @@
1
- import type { TeamRunManifest } from "../state/types.ts";
2
- import { appendEvent } from "../state/event-log.ts";
3
- import { logInternalError } from "../utils/internal-error.ts";
4
-
5
- export interface SupervisorContactPayload {
6
- runId: string;
7
- taskId: string;
8
- reason: "decision_needed" | "clarification" | "approval" | "error_escalation" | "custom";
9
- message: string;
10
- data?: Record<string, unknown>;
11
- timestamp: string;
12
- }
13
-
14
- /**
15
- * Record a supervisor contact event from a child task.
16
- * This represents a child→parent communication where the child needs
17
- * a decision, clarification, or approval to continue.
18
- */
19
- export function recordSupervisorContact(manifest: TeamRunManifest, payload: Omit<SupervisorContactPayload, "timestamp">): void {
20
- const fullPayload: SupervisorContactPayload = {
21
- ...payload,
22
- timestamp: new Date().toISOString(),
23
- };
24
- try {
25
- appendEvent(manifest.eventsPath, {
26
- type: "supervisor.contact",
27
- runId: manifest.runId,
28
- taskId: payload.taskId,
29
- data: fullPayload as unknown as Record<string, unknown>,
30
- });
31
- } catch (error) {
32
- logInternalError("supervisor-contact.record", error, `runId=${manifest.runId} taskId=${payload.taskId}`);
33
- }
34
- }
35
-
36
- /**
37
- * Parse a supervisor contact request from child Pi stdout.
38
- * Detects structured JSON lines with type "supervisor_contact".
39
- */
40
- export function parseSupervisorContactFromLine(line: string): Omit<SupervisorContactPayload, "timestamp" | "runId"> | undefined {
41
- if (!line.trim()) return undefined;
42
- let parsed: unknown;
43
- try {
44
- parsed = JSON.parse(line);
45
- } catch {
46
- return undefined;
47
- }
48
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
49
- const record = parsed as Record<string, unknown>;
50
- if (record.type !== "supervisor_contact" && record.type !== "crew_supervisor_contact") return undefined;
51
- return {
52
- taskId: typeof record.taskId === "string" ? record.taskId : "",
53
- reason: typeof record.reason === "string" && ["decision_needed", "clarification", "approval", "error_escalation", "custom"].includes(record.reason)
54
- ? record.reason as SupervisorContactPayload["reason"]
55
- : "custom",
56
- message: typeof record.message === "string" ? record.message : String(record.message ?? ""),
57
- data: record.data && typeof record.data === "object" && !Array.isArray(record.data) ? record.data as Record<string, unknown> : undefined,
58
- };
59
- }
1
+ import type { TeamRunManifest } from "../state/types.ts";
2
+ import { appendEvent } from "../state/event-log.ts";
3
+ import { logInternalError } from "../utils/internal-error.ts";
4
+
5
+ export interface SupervisorContactPayload {
6
+ runId: string;
7
+ taskId: string;
8
+ reason: "decision_needed" | "clarification" | "approval" | "error_escalation" | "custom";
9
+ message: string;
10
+ data?: Record<string, unknown>;
11
+ timestamp: string;
12
+ }
13
+
14
+ /**
15
+ * Record a supervisor contact event from a child task.
16
+ * This represents a child→parent communication where the child needs
17
+ * a decision, clarification, or approval to continue.
18
+ */
19
+ export function recordSupervisorContact(manifest: TeamRunManifest, payload: Omit<SupervisorContactPayload, "timestamp">): void {
20
+ const fullPayload: SupervisorContactPayload = {
21
+ ...payload,
22
+ timestamp: new Date().toISOString(),
23
+ };
24
+ try {
25
+ appendEvent(manifest.eventsPath, {
26
+ type: "supervisor.contact",
27
+ runId: manifest.runId,
28
+ taskId: payload.taskId,
29
+ data: fullPayload as unknown as Record<string, unknown>,
30
+ });
31
+ } catch (error) {
32
+ logInternalError("supervisor-contact.record", error, `runId=${manifest.runId} taskId=${payload.taskId}`);
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Parse a supervisor contact request from child Pi stdout.
38
+ * Detects structured JSON lines with type "supervisor_contact".
39
+ */
40
+ export function parseSupervisorContactFromLine(line: string): Omit<SupervisorContactPayload, "timestamp" | "runId"> | undefined {
41
+ if (!line.trim()) return undefined;
42
+ let parsed: unknown;
43
+ try {
44
+ parsed = JSON.parse(line);
45
+ } catch {
46
+ return undefined;
47
+ }
48
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
49
+ const record = parsed as Record<string, unknown>;
50
+ if (record.type !== "supervisor_contact" && record.type !== "crew_supervisor_contact") return undefined;
51
+ return {
52
+ taskId: typeof record.taskId === "string" ? record.taskId : "",
53
+ reason: typeof record.reason === "string" && ["decision_needed", "clarification", "approval", "error_escalation", "custom"].includes(record.reason)
54
+ ? record.reason as SupervisorContactPayload["reason"]
55
+ : "custom",
56
+ message: typeof record.message === "string" ? record.message : String(record.message ?? ""),
57
+ data: record.data && typeof record.data === "object" && !Array.isArray(record.data) ? record.data as Record<string, unknown> : undefined,
58
+ };
59
+ }
@@ -1,38 +1,38 @@
1
- import type { TeamTaskState } from "../state/types.ts";
2
- import type { CrewAgentRecord, CrewRuntimeKind } from "./crew-agent-runtime.ts";
3
- import { recordFromTask } from "./crew-agent-records.ts";
4
- import type { TeamRunManifest } from "../state/types.ts";
5
-
6
- export function shouldMaterializeAgent(task: TeamTaskState): boolean {
7
- return task.status !== "queued" && task.status !== "skipped";
8
- }
9
-
10
- export function recordsForMaterializedTasks(manifest: TeamRunManifest, tasks: TeamTaskState[], runtime: CrewRuntimeKind): CrewAgentRecord[] {
11
- return tasks.filter(shouldMaterializeAgent).map((task) => recordFromTask(manifest, task, runtime));
12
- }
13
-
14
- export function taskById(tasks: TeamTaskState[]): Map<string, TeamTaskState> {
15
- const map = new Map<string, TeamTaskState>();
16
- for (const task of tasks) {
17
- map.set(task.id, task);
18
- if (task.stepId) map.set(task.stepId, task);
19
- }
20
- return map;
21
- }
22
-
23
- export function waitingReason(task: TeamTaskState, tasks: TeamTaskState[]): string | undefined {
24
- if (task.status !== "queued") return undefined;
25
- const byId = taskById(tasks);
26
- const waiting = task.dependsOn.map((id) => byId.get(id)?.id ?? id).filter((id) => byId.get(id)?.status !== "completed");
27
- if (waiting.length === 0) return "ready";
28
- return `waiting for ${waiting.join(", ")}`;
29
- }
30
-
31
- export function formatTaskGraphLines(tasks: TeamTaskState[]): string[] {
32
- if (tasks.length === 0) return ["- (none)"];
33
- return tasks.map((task) => {
34
- const icon = task.status === "completed" ? "✓" : task.status === "running" ? "⠋" : task.status === "failed" ? "✗" : task.status === "cancelled" || task.status === "skipped" ? "■" : "◦";
35
- const wait = waitingReason(task, tasks);
36
- return `- ${icon} ${task.id} [${task.status}] ${task.role}->${task.agent}${wait && wait !== "ready" ? ` (${wait})` : ""}`;
37
- });
38
- }
1
+ import type { TeamTaskState } from "../state/types.ts";
2
+ import type { CrewAgentRecord, CrewRuntimeKind } from "./crew-agent-runtime.ts";
3
+ import { recordFromTask } from "./crew-agent-records.ts";
4
+ import type { TeamRunManifest } from "../state/types.ts";
5
+
6
+ export function shouldMaterializeAgent(task: TeamTaskState): boolean {
7
+ return task.status !== "queued" && task.status !== "skipped";
8
+ }
9
+
10
+ export function recordsForMaterializedTasks(manifest: TeamRunManifest, tasks: TeamTaskState[], runtime: CrewRuntimeKind): CrewAgentRecord[] {
11
+ return tasks.filter(shouldMaterializeAgent).map((task) => recordFromTask(manifest, task, runtime));
12
+ }
13
+
14
+ export function taskById(tasks: TeamTaskState[]): Map<string, TeamTaskState> {
15
+ const map = new Map<string, TeamTaskState>();
16
+ for (const task of tasks) {
17
+ map.set(task.id, task);
18
+ if (task.stepId) map.set(task.stepId, task);
19
+ }
20
+ return map;
21
+ }
22
+
23
+ export function waitingReason(task: TeamTaskState, tasks: TeamTaskState[]): string | undefined {
24
+ if (task.status !== "queued") return undefined;
25
+ const byId = taskById(tasks);
26
+ const waiting = task.dependsOn.map((id) => byId.get(id)?.id ?? id).filter((id) => byId.get(id)?.status !== "completed");
27
+ if (waiting.length === 0) return "ready";
28
+ return `waiting for ${waiting.join(", ")}`;
29
+ }
30
+
31
+ export function formatTaskGraphLines(tasks: TeamTaskState[]): string[] {
32
+ if (tasks.length === 0) return ["- (none)"];
33
+ return tasks.map((task) => {
34
+ const icon = task.status === "completed" ? "✓" : task.status === "running" ? "⠋" : task.status === "failed" ? "✗" : task.status === "cancelled" || task.status === "skipped" ? "■" : "◦";
35
+ const wait = waitingReason(task, tasks);
36
+ return `- ${icon} ${task.id} [${task.status}] ${task.role}->${task.agent}${wait && wait !== "ready" ? ` (${wait})` : ""}`;
37
+ });
38
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * DAG-based task execution order calculator.
3
+ *
4
+ * Uses Kahn's algorithm for topological sort and DFS for cycle detection.
5
+ * Groups tasks into parallel "waves" where all tasks in wave N can run
6
+ * concurrently and wave N+1 depends on at least one task in wave N.
7
+ */
8
+
9
+ /** A lightweight node representation for the execution DAG. */
10
+ export interface TaskNode {
11
+ id: string;
12
+ dependsOn: string[];
13
+ phase?: string;
14
+ }
15
+
16
+ /** A group of tasks that can all run in parallel. */
17
+ export interface ExecutionWave {
18
+ index: number;
19
+ taskIds: string[];
20
+ label?: string;
21
+ }
22
+
23
+ /** The full execution plan produced by topological sort. */
24
+ export interface ExecutionPlan {
25
+ waves: ExecutionWave[];
26
+ hasCycle: boolean;
27
+ cycleNodes?: string[];
28
+ }
29
+
30
+ /**
31
+ * Build an execution plan from a flat list of task nodes using Kahn's algorithm.
32
+ *
33
+ * - Tasks with empty `dependsOn` go into wave 0.
34
+ * - Each subsequent wave contains tasks whose dependencies are all in earlier waves.
35
+ * - If all tasks have empty `dependsOn`, they all go into wave 0 (backward compatible).
36
+ * - If a cycle is detected, `hasCycle` is true and `cycleNodes` lists the involved IDs.
37
+ */
38
+ export function buildExecutionPlan(tasks: TaskNode[]): ExecutionPlan {
39
+ if (tasks.length === 0) {
40
+ return { waves: [], hasCycle: false };
41
+ }
42
+
43
+ const idSet = new Set<string>(tasks.map((t) => t.id));
44
+ const adjacency = new Map<string, Set<string>>(); // id -> ids that depend on it
45
+ const inDegree = new Map<string, number>();
46
+
47
+ for (const task of tasks) {
48
+ adjacency.set(task.id, new Set<string>());
49
+ inDegree.set(task.id, 0);
50
+ }
51
+
52
+ for (const task of tasks) {
53
+ let degree = 0;
54
+ for (const dep of task.dependsOn) {
55
+ if (!idSet.has(dep)) continue; // ignore unknown deps
56
+ adjacency.get(dep)!.add(task.id);
57
+ degree++;
58
+ }
59
+ inDegree.set(task.id, degree);
60
+ }
61
+
62
+ // Kahn's algorithm with wave grouping
63
+ const waves: ExecutionWave[] = [];
64
+ const assigned = new Set<string>();
65
+ let currentWaveIds = tasks
66
+ .filter((t) => inDegree.get(t.id) === 0)
67
+ .map((t) => t.id);
68
+
69
+ let waveIndex = 0;
70
+ while (currentWaveIds.length > 0) {
71
+ for (const id of currentWaveIds) assigned.add(id);
72
+
73
+ const wave = buildWave(tasks, currentWaveIds, waveIndex);
74
+ waves.push(wave);
75
+
76
+ // Decrement in-degrees for dependents
77
+ const nextWaveCandidates = new Set<string>();
78
+ for (const id of currentWaveIds) {
79
+ for (const dependent of adjacency.get(id) ?? []) {
80
+ const current = inDegree.get(dependent)!;
81
+ inDegree.set(dependent, current - 1);
82
+ if (current - 1 === 0) nextWaveCandidates.add(dependent);
83
+ }
84
+ }
85
+
86
+ currentWaveIds = [...nextWaveCandidates];
87
+ waveIndex++;
88
+ }
89
+
90
+ // Detect cycle: if not all tasks were assigned, remaining ones form cycles
91
+ if (assigned.size < tasks.length) {
92
+ const cycleNodes = tasks
93
+ .filter((t) => !assigned.has(t.id))
94
+ .map((t) => t.id);
95
+ return {
96
+ waves,
97
+ hasCycle: true,
98
+ cycleNodes,
99
+ };
100
+ }
101
+
102
+ return { waves, hasCycle: false };
103
+ }
104
+
105
+ /**
106
+ * Derive the phase label for a wave. If all tasks in the wave share the same
107
+ * `phase` value, use it as the wave label; otherwise leave it undefined.
108
+ */
109
+ function buildWave(tasks: TaskNode[], ids: string[], index: number): ExecutionWave {
110
+ const taskMap = new Map(tasks.map((t) => [t.id, t]));
111
+ const waveTasks = ids.map((id) => taskMap.get(id)!).filter(Boolean);
112
+
113
+ let label: string | undefined;
114
+ if (waveTasks.length > 0 && waveTasks.every((t) => t.phase !== undefined)) {
115
+ const phases = new Set(waveTasks.map((t) => t.phase));
116
+ if (phases.size === 1) label = [...phases][0];
117
+ }
118
+
119
+ return { index, taskIds: ids, label };
120
+ }
121
+
122
+ /**
123
+ * Return the IDs of tasks that are ready to run given a set of completed tasks.
124
+ *
125
+ * A task is "ready" when all its dependencies are in `completedTaskIds` AND
126
+ * it has not already been completed itself. Returns tasks from the earliest
127
+ * wave that still has uncompleted tasks.
128
+ */
129
+ export function getReadyTasks(plan: ExecutionPlan, completedTaskIds: Set<string>): string[] {
130
+ if (plan.hasCycle || plan.waves.length === 0) return [];
131
+
132
+ const completed = completedTaskIds;
133
+
134
+ for (const wave of plan.waves) {
135
+ // All tasks in prior waves must be completed for this wave to be ready
136
+ const priorWavesComplete = plan.waves
137
+ .slice(0, wave.index)
138
+ .every((w) => w.taskIds.every((id) => completed.has(id)));
139
+
140
+ if (!priorWavesComplete) continue;
141
+
142
+ // Filter to tasks not already completed
143
+ const ready = wave.taskIds.filter((id) => !completed.has(id));
144
+ if (ready.length > 0) return ready;
145
+ }
146
+
147
+ return [];
148
+ }
149
+
150
+ /**
151
+ * Detect all cycles in the task graph using DFS.
152
+ *
153
+ * Returns an array of cycles, where each cycle is represented as an array of
154
+ * task IDs forming a path from a node back to itself.
155
+ */
156
+ export function detectCycles(tasks: TaskNode[]): string[][] {
157
+ if (tasks.length === 0) return [];
158
+
159
+ const idSet = new Set<string>(tasks.map((t) => t.id));
160
+ const adjacency = new Map<string, string[]>();
161
+ for (const task of tasks) {
162
+ adjacency.set(
163
+ task.id,
164
+ task.dependsOn.filter((dep) => idSet.has(dep)),
165
+ );
166
+ }
167
+
168
+ const WHITE = 0;
169
+ const GRAY = 1;
170
+ const BLACK = 2;
171
+
172
+ const color = new Map<string, number>();
173
+ for (const task of tasks) color.set(task.id, WHITE);
174
+
175
+ const cycles: string[][] = [];
176
+ const path: string[] = [];
177
+
178
+ function dfs(nodeId: string): void {
179
+ color.set(nodeId, GRAY);
180
+ path.push(nodeId);
181
+
182
+ const deps = adjacency.get(nodeId) ?? [];
183
+ for (const dep of deps) {
184
+ const depColor = color.get(dep);
185
+ if (depColor === GRAY) {
186
+ // Found a cycle: extract the path from dep to current node
187
+ const cycleStart = path.indexOf(dep);
188
+ if (cycleStart >= 0) {
189
+ cycles.push(path.slice(cycleStart));
190
+ }
191
+ } else if (depColor === WHITE) {
192
+ dfs(dep);
193
+ }
194
+ }
195
+
196
+ path.pop();
197
+ color.set(nodeId, BLACK);
198
+ }
199
+
200
+ for (const task of tasks) {
201
+ if (color.get(task.id) === WHITE) {
202
+ dfs(task.id);
203
+ }
204
+ }
205
+
206
+ return cycles;
207
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Task quality scoring — simple additive heuristic for evaluating task
3
+ * completion quality based on diagnostics, metrics, artifacts, and duration.
4
+ *
5
+ * Distilled from pi-autoresearch's quality scoring pattern.
6
+ */
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
+ import type { TeamTaskState } from "../state/types.ts";
10
+
11
+ /** Letter grade for task quality. */
12
+ export type QualityGrade = "A" | "B" | "C" | "D";
13
+
14
+ /** Breakdown of individual quality criteria. */
15
+ export interface QualityBreakdown {
16
+ /** Task has a non-empty diagnostics object. */
17
+ hasDiagnostics: boolean;
18
+ /** Task has a non-empty metrics object. */
19
+ hasMetrics: boolean;
20
+ /** Task produced files in the artifacts directory. */
21
+ producedArtifacts: boolean;
22
+ /** Task has a non-empty result/description. */
23
+ hasDescription: boolean;
24
+ /** Task duration is reasonable (> 0 and < 1 hour). */
25
+ durationReasonable: boolean;
26
+ }
27
+
28
+ /** Scored quality result for a task. */
29
+ export interface TaskQualityScore {
30
+ /** Numeric score (0–5). */
31
+ score: number;
32
+ /** Individual criterion breakdown. */
33
+ breakdown: QualityBreakdown;
34
+ /** Letter grade based on score thresholds. */
35
+ grade: QualityGrade;
36
+ }
37
+
38
+ /** One hour in milliseconds. */
39
+ const ONE_HOUR_MS = 3_600_000;
40
+
41
+ /**
42
+ * Determine the letter grade for a given numeric score.
43
+ *
44
+ * A: 4–5, B: 3, C: 2, D: 0–1
45
+ */
46
+ function scoreToGrade(score: number): QualityGrade {
47
+ if (score >= 4) return "A";
48
+ if (score === 3) return "B";
49
+ if (score === 2) return "C";
50
+ return "D";
51
+ }
52
+
53
+ /**
54
+ * Check whether the artifacts directory contains files for the given task.
55
+ *
56
+ * Looks for a subdirectory named after the task ID, or files containing
57
+ * the task ID prefix in the artifacts directory.
58
+ */
59
+ function hasTaskArtifacts(taskId: string, artifactsDir: string): boolean {
60
+ try {
61
+ if (!fs.existsSync(artifactsDir)) return false;
62
+
63
+ // Check for a task-specific subdirectory
64
+ const taskDir = path.join(artifactsDir, taskId);
65
+ if (fs.existsSync(taskDir)) {
66
+ const stat = fs.statSync(taskDir);
67
+ if (stat.isDirectory()) {
68
+ const entries = fs.readdirSync(taskDir);
69
+ return entries.length > 0;
70
+ }
71
+ }
72
+
73
+ // Check for files containing the task ID prefix
74
+ const entries = fs.readdirSync(artifactsDir);
75
+ const safePrefix = taskId.replace(/[^a-zA-Z0-9_-]/g, "");
76
+ return entries.some((entry) => entry.includes(safePrefix));
77
+ } catch {
78
+ return false;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Check if a task result string is a non-empty description.
84
+ *
85
+ * A result is considered descriptive if any of these sources have non-empty content:
86
+ * - task.resultArtifact exists with a path
87
+ * - task.error is a non-empty string (workers often set this with result info)
88
+ * - task.verification.satisfied is true
89
+ * - task.diagnostics contains a 'result' string
90
+ */
91
+ function isResultDescriptive(task: TeamTaskState): boolean {
92
+ // Check resultArtifact — presence of a result artifact indicates output was produced
93
+ if (task.resultArtifact?.path) return true;
94
+
95
+ // Check error field — workers often put result info here
96
+ if (typeof task.error === "string" && task.error.trim().length > 0) return true;
97
+
98
+ // Check verification — satisfied verification indicates meaningful output
99
+ if (task.verification?.satisfied) return true;
100
+
101
+ // Check diagnostics for an explicit result string
102
+ if (
103
+ task.diagnostics &&
104
+ typeof task.diagnostics === "object" &&
105
+ typeof task.diagnostics.result === "string" &&
106
+ (task.diagnostics.result as string).trim().length > 0
107
+ ) return true;
108
+
109
+ return false;
110
+ }
111
+
112
+ /**
113
+ * Check if the task duration is reasonable (started, finished, > 0, < 1 hour).
114
+ */
115
+ function isDurationReasonable(task: TeamTaskState): boolean {
116
+ if (!task.startedAt || !task.finishedAt) return false;
117
+
118
+ const started = new Date(task.startedAt).getTime();
119
+ const finished = new Date(task.finishedAt).getTime();
120
+
121
+ if (Number.isNaN(started) || Number.isNaN(finished)) return false;
122
+
123
+ const duration = finished - started;
124
+ return duration > 0 && duration < ONE_HOUR_MS;
125
+ }
126
+
127
+ /**
128
+ * Compute the quality score for a completed task.
129
+ *
130
+ * Uses simple additive scoring across 5 criteria:
131
+ * - hasDiagnostics: +1 if task.diagnostics exists and has keys
132
+ * - hasMetrics: +1 if task.metrics exists and has keys
133
+ * - producedArtifacts: +1 if artifactsDir has files for this task
134
+ * - hasDescription: +1 if task has a non-empty result/description
135
+ * - durationReasonable: +1 if task has both startedAt and finishedAt, duration > 0 and < 1 hour
136
+ *
137
+ * @param task - The task state to evaluate
138
+ * @param artifactsDir - Optional path to the run artifacts directory
139
+ * @returns TaskQualityScore with numeric score, breakdown, and letter grade
140
+ */
141
+ export function computeTaskQuality(
142
+ task: TeamTaskState,
143
+ artifactsDir?: string,
144
+ ): TaskQualityScore {
145
+ const hasDiagnostics =
146
+ task.diagnostics !== undefined &&
147
+ typeof task.diagnostics === "object" &&
148
+ Object.keys(task.diagnostics).length > 0;
149
+
150
+ const hasMetrics =
151
+ task.metrics !== undefined &&
152
+ typeof task.metrics === "object" &&
153
+ Object.keys(task.metrics).length > 0;
154
+
155
+ const producedArtifacts =
156
+ artifactsDir !== undefined && hasTaskArtifacts(task.id, artifactsDir);
157
+
158
+ const hasDescription = isResultDescriptive(task);
159
+
160
+ const durationReasonable = isDurationReasonable(task);
161
+
162
+ const breakdown: QualityBreakdown = {
163
+ hasDiagnostics,
164
+ hasMetrics,
165
+ producedArtifacts,
166
+ hasDescription,
167
+ durationReasonable,
168
+ };
169
+
170
+ const score =
171
+ (hasDiagnostics ? 1 : 0) +
172
+ (hasMetrics ? 1 : 0) +
173
+ (producedArtifacts ? 1 : 0) +
174
+ (hasDescription ? 1 : 0) +
175
+ (durationReasonable ? 1 : 0);
176
+
177
+ return {
178
+ score,
179
+ breakdown,
180
+ grade: scoreToGrade(score),
181
+ };
182
+ }
183
+
184
+ /** Human-readable labels for each quality criterion. */
185
+ const CRITERION_LABELS: Record<keyof QualityBreakdown, string> = {
186
+ hasDiagnostics: "diagnostics",
187
+ hasMetrics: "metrics",
188
+ producedArtifacts: "artifacts",
189
+ hasDescription: "description",
190
+ durationReasonable: "duration",
191
+ };
192
+
193
+ /**
194
+ * Format a quality score as a human-readable one-line string.
195
+ *
196
+ * Format: "Quality: B (3/5: diagnostics, metrics, description)"
197
+ *
198
+ * @param score - The quality score to format
199
+ * @returns Formatted string
200
+ */
201
+ export function formatQualityScore(score: TaskQualityScore): string {
202
+ const metCriteria = Object.entries(score.breakdown)
203
+ .filter(([, met]) => met)
204
+ .map(([key]) => CRITERION_LABELS[key as keyof QualityBreakdown]);
205
+
206
+ return `Quality: ${score.grade} (${score.score}/5${metCriteria.length > 0 ? `: ${metCriteria.join(", ")}` : ""})`;
207
+ }