pi-crew 0.1.51 → 0.2.1

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 (240) 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/api.ts +441 -441
  65. package/src/extension/team-tool/doctor.ts +28 -3
  66. package/src/extension/team-tool/handle-settings.ts +195 -188
  67. package/src/extension/team-tool/inspect.ts +41 -41
  68. package/src/extension/team-tool/intent-policy.ts +42 -42
  69. package/src/extension/team-tool/lifecycle-actions.ts +27 -8
  70. package/src/extension/team-tool/plan.ts +19 -19
  71. package/src/extension/team-tool/run.ts +12 -1
  72. package/src/extension/team-tool.ts +332 -322
  73. package/src/i18n.ts +184 -184
  74. package/src/observability/exporters/otlp-exporter.ts +92 -77
  75. package/src/prompt/prompt-runtime.ts +72 -72
  76. package/src/runtime/agent-memory.ts +72 -72
  77. package/src/runtime/agent-observability.ts +114 -114
  78. package/src/runtime/async-marker.ts +26 -26
  79. package/src/runtime/attention-events.ts +28 -28
  80. package/src/runtime/auto-resume.ts +100 -0
  81. package/src/runtime/background-runner.ts +11 -1
  82. package/src/runtime/cancellation-token.ts +89 -89
  83. package/src/runtime/cancellation.ts +61 -61
  84. package/src/runtime/capability-inventory.ts +116 -116
  85. package/src/runtime/child-pi.ts +7 -2
  86. package/src/runtime/compaction-summary.ts +271 -0
  87. package/src/runtime/completion-guard.ts +190 -190
  88. package/src/runtime/crash-recovery.ts +33 -1
  89. package/src/runtime/delta-conflict.ts +360 -0
  90. package/src/runtime/direct-run.ts +35 -35
  91. package/src/runtime/foreground-control.ts +82 -82
  92. package/src/runtime/green-contract.ts +46 -46
  93. package/src/runtime/group-join.ts +106 -106
  94. package/src/runtime/heartbeat-gradient.ts +28 -28
  95. package/src/runtime/heartbeat-watcher.ts +124 -124
  96. package/src/runtime/iteration-hooks.ts +264 -0
  97. package/src/runtime/live-agent-control.ts +88 -88
  98. package/src/runtime/live-control-realtime.ts +36 -36
  99. package/src/runtime/live-extension-bridge.ts +150 -150
  100. package/src/runtime/live-irc.ts +92 -92
  101. package/src/runtime/live-session-health.ts +100 -100
  102. package/src/runtime/loop-gates.ts +129 -0
  103. package/src/runtime/metric-parser.ts +40 -0
  104. package/src/runtime/notebook-helpers.ts +90 -90
  105. package/src/runtime/orphan-sentinel.ts +7 -7
  106. package/src/runtime/parallel-research.ts +44 -44
  107. package/src/runtime/phase-progress.ts +217 -0
  108. package/src/runtime/pi-args.ts +38 -11
  109. package/src/runtime/pi-json-output.ts +111 -111
  110. package/src/runtime/pi-spawn.ts +57 -7
  111. package/src/runtime/policy-engine.ts +79 -79
  112. package/src/runtime/post-checks.ts +122 -0
  113. package/src/runtime/progress-event-coalescer.ts +43 -43
  114. package/src/runtime/prose-compressor.ts +164 -164
  115. package/src/runtime/recovery-recipes.ts +74 -74
  116. package/src/runtime/result-extractor.ts +121 -121
  117. package/src/runtime/role-permission.ts +39 -39
  118. package/src/runtime/sensitive-paths.ts +2 -2
  119. package/src/runtime/session-resources.ts +25 -25
  120. package/src/runtime/session-snapshot.ts +59 -59
  121. package/src/runtime/session-usage.ts +79 -79
  122. package/src/runtime/sidechain-output.ts +29 -29
  123. package/src/runtime/stream-preview.ts +177 -177
  124. package/src/runtime/supervisor-contact.ts +59 -59
  125. package/src/runtime/task-display.ts +38 -38
  126. package/src/runtime/task-graph.ts +207 -0
  127. package/src/runtime/task-quality.ts +207 -0
  128. package/src/runtime/task-runner/capabilities.ts +78 -78
  129. package/src/runtime/task-runner/live-executor.ts +7 -1
  130. package/src/runtime/task-runner/progress.ts +119 -119
  131. package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
  132. package/src/runtime/task-runner/result-utils.ts +14 -14
  133. package/src/runtime/task-runner/run-projection.ts +103 -103
  134. package/src/runtime/task-runner/state-helpers.ts +22 -22
  135. package/src/runtime/team-runner.ts +117 -7
  136. package/src/runtime/worker-heartbeat.ts +21 -21
  137. package/src/runtime/worker-startup.ts +57 -57
  138. package/src/runtime/workflow-state.ts +187 -0
  139. package/src/runtime/workspace-tree.ts +298 -298
  140. package/src/schema/config-schema.ts +11 -0
  141. package/src/schema/validation-types.ts +148 -0
  142. package/src/skills/skill-templates.ts +374 -0
  143. package/src/state/active-run-registry.ts +35 -11
  144. package/src/state/atomic-write.ts +33 -26
  145. package/src/state/contracts.ts +1 -0
  146. package/src/state/event-reconstructor.ts +217 -0
  147. package/src/state/locks.ts +2 -13
  148. package/src/state/mailbox.ts +4 -3
  149. package/src/state/state-store.ts +16 -6
  150. package/src/state/task-claims.ts +44 -44
  151. package/src/state/types.ts +9 -0
  152. package/src/state/usage.ts +29 -29
  153. package/src/subagents/async-entry.ts +1 -1
  154. package/src/subagents/index.ts +3 -3
  155. package/src/subagents/live/control.ts +1 -1
  156. package/src/subagents/live/manager.ts +1 -1
  157. package/src/subagents/live/realtime.ts +1 -1
  158. package/src/subagents/live/session-runtime.ts +1 -1
  159. package/src/subagents/manager.ts +1 -1
  160. package/src/subagents/spawn.ts +1 -1
  161. package/src/teams/team-serializer.ts +38 -38
  162. package/src/types/diff.d.ts +18 -18
  163. package/src/ui/crew-footer.ts +101 -101
  164. package/src/ui/crew-select-list.ts +111 -111
  165. package/src/ui/crew-widget.ts +5 -2
  166. package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
  167. package/src/ui/dashboard-panes/capability-pane.ts +59 -59
  168. package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
  169. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  170. package/src/ui/dashboard-panes/progress-pane.ts +11 -0
  171. package/src/ui/dynamic-border.ts +25 -25
  172. package/src/ui/layout-primitives.ts +106 -106
  173. package/src/ui/loaders.ts +158 -158
  174. package/src/ui/render-coalescer.ts +51 -51
  175. package/src/ui/render-diff.ts +119 -119
  176. package/src/ui/render-scheduler.ts +143 -143
  177. package/src/ui/run-action-dispatcher.ts +10 -1
  178. package/src/ui/spinner.ts +17 -17
  179. package/src/ui/status-colors.ts +58 -58
  180. package/src/ui/syntax-highlight.ts +116 -116
  181. package/src/ui/transcript-entries.ts +258 -258
  182. package/src/utils/completion-dedupe.ts +63 -63
  183. package/src/utils/frontmatter.ts +68 -68
  184. package/src/utils/git.ts +262 -262
  185. package/src/utils/ids.ts +17 -17
  186. package/src/utils/incremental-reader.ts +104 -104
  187. package/src/utils/names.ts +27 -27
  188. package/src/utils/redaction.ts +44 -44
  189. package/src/utils/safe-paths.ts +47 -47
  190. package/src/utils/scan-cache.ts +136 -136
  191. package/src/utils/sleep.ts +40 -26
  192. package/src/utils/task-name-generator.ts +337 -337
  193. package/src/workflows/validate-workflow.ts +40 -40
  194. package/src/worktree/branch-freshness.ts +45 -45
  195. package/teams/default.team.md +12 -12
  196. package/teams/fast-fix.team.md +11 -11
  197. package/teams/implementation.team.md +18 -18
  198. package/teams/parallel-research.team.md +14 -14
  199. package/teams/research.team.md +11 -11
  200. package/teams/review.team.md +12 -12
  201. package/workflows/default.workflow.md +30 -29
  202. package/workflows/fast-fix.workflow.md +23 -22
  203. package/workflows/implementation.workflow.md +43 -43
  204. package/workflows/parallel-research.workflow.md +46 -46
  205. package/workflows/research.workflow.md +22 -22
  206. package/workflows/review.workflow.md +30 -30
  207. package/docs/refactor-tasks-phase3.md +0 -394
  208. package/docs/refactor-tasks-phase4.md +0 -564
  209. package/docs/refactor-tasks-phase5.md +0 -402
  210. package/docs/refactor-tasks-phase6.md +0 -662
  211. package/docs/refactor-tasks.md +0 -1484
  212. package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +0 -261
  213. package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +0 -111
  214. package/docs/research/AUDIT_OH_MY_PI.md +0 -261
  215. package/docs/research/AUDIT_PI_CREW.md +0 -457
  216. package/docs/research/CAVEMAN-DEEP-RESEARCH.md +0 -281
  217. package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +0 -264
  218. package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +0 -343
  219. package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +0 -480
  220. package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +0 -354
  221. package/docs/research/IMPLEMENTATION_PLAN.md +0 -385
  222. package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +0 -502
  223. package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +0 -266
  224. package/docs/research/REMAINING-GAPS-PLAN.md +0 -363
  225. package/docs/research/SESSION-SUMMARY-2026-05-08.md +0 -146
  226. package/docs/research/UI-RESPONSIVENESS-AUDIT.md +0 -173
  227. package/docs/research-awesome-agent-skills-distillation.md +0 -100
  228. package/docs/research-extension-examples.md +0 -297
  229. package/docs/research-extension-system.md +0 -324
  230. package/docs/research-oh-my-pi-distillation.md +0 -369
  231. package/docs/research-optimization-plan.md +0 -548
  232. package/docs/research-phase10-distillation.md +0 -199
  233. package/docs/research-phase11-distillation.md +0 -201
  234. package/docs/research-phase8-operator-experience-plan.md +0 -819
  235. package/docs/research-phase9-observability-reliability-plan.md +0 -1190
  236. package/docs/research-pi-coding-agent.md +0 -357
  237. package/docs/research-source-pi-crew-reference.md +0 -174
  238. package/docs/research-ui-optimization-plan.md +0 -480
  239. package/docs/source-runtime-refactor-map.md +0 -107
  240. 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
+ }