pi-crew 0.1.44 → 0.1.45

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 (142) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/agents/analyst.md +11 -11
  3. package/agents/critic.md +11 -11
  4. package/agents/executor.md +11 -11
  5. package/agents/explorer.md +11 -11
  6. package/agents/planner.md +11 -11
  7. package/agents/reviewer.md +11 -11
  8. package/agents/security-reviewer.md +11 -11
  9. package/agents/test-engineer.md +11 -11
  10. package/agents/verifier.md +11 -11
  11. package/agents/writer.md +11 -11
  12. package/docs/refactor-tasks-phase3.md +394 -394
  13. package/docs/refactor-tasks-phase4.md +564 -564
  14. package/docs/refactor-tasks-phase5.md +402 -402
  15. package/docs/refactor-tasks-phase6.md +662 -662
  16. package/docs/research-extension-examples.md +297 -297
  17. package/docs/research-extension-system.md +324 -324
  18. package/docs/research-optimization-plan.md +548 -548
  19. package/docs/research-phase10-distillation.md +198 -198
  20. package/docs/research-phase11-distillation.md +201 -201
  21. package/docs/research-pi-coding-agent.md +357 -357
  22. package/docs/research-source-pi-crew-reference.md +174 -174
  23. package/docs/runtime-flow.md +148 -148
  24. package/docs/source-runtime-refactor-map.md +83 -83
  25. package/index.ts +6 -6
  26. package/package.json +1 -1
  27. package/src/agents/agent-serializer.ts +34 -34
  28. package/src/extension/cross-extension-rpc.ts +82 -82
  29. package/src/extension/register.ts +8 -1
  30. package/src/extension/registration/commands.ts +18 -2
  31. package/src/extension/registration/compaction-guard.ts +125 -125
  32. package/src/extension/registration/subagent-tools.ts +148 -148
  33. package/src/extension/registration/team-tool.ts +26 -8
  34. package/src/extension/run-bundle-schema.ts +89 -89
  35. package/src/extension/run-maintenance.ts +43 -43
  36. package/src/extension/team-tool/cancel.ts +105 -102
  37. package/src/extension/team-tool/context.ts +1 -0
  38. package/src/extension/team-tool/handle-settings.ts +188 -188
  39. package/src/extension/team-tool/inspect.ts +41 -41
  40. package/src/extension/team-tool/lifecycle-actions.ts +79 -79
  41. package/src/extension/team-tool/plan.ts +19 -19
  42. package/src/extension/team-tool/respond.ts +83 -66
  43. package/src/extension/team-tool/run.ts +1 -0
  44. package/src/i18n.ts +184 -184
  45. package/src/observability/exporters/otlp-exporter.ts +77 -77
  46. package/src/prompt/prompt-runtime.ts +72 -72
  47. package/src/runtime/agent-control.ts +63 -63
  48. package/src/runtime/agent-memory.ts +72 -72
  49. package/src/runtime/agent-observability.ts +114 -114
  50. package/src/runtime/async-marker.ts +26 -26
  51. package/src/runtime/attention-events.ts +28 -28
  52. package/src/runtime/background-runner.ts +53 -53
  53. package/src/runtime/child-pi.ts +444 -444
  54. package/src/runtime/completion-guard.ts +190 -190
  55. package/src/runtime/crew-agent-records.ts +8 -0
  56. package/src/runtime/delivery-coordinator.ts +153 -142
  57. package/src/runtime/direct-run.ts +35 -35
  58. package/src/runtime/foreground-control.ts +82 -82
  59. package/src/runtime/green-contract.ts +46 -46
  60. package/src/runtime/group-join.ts +106 -106
  61. package/src/runtime/heartbeat-gradient.ts +28 -28
  62. package/src/runtime/heartbeat-watcher.ts +124 -124
  63. package/src/runtime/live-agent-control.ts +87 -87
  64. package/src/runtime/live-agent-manager.ts +85 -85
  65. package/src/runtime/live-control-realtime.ts +36 -36
  66. package/src/runtime/live-session-runtime.ts +305 -305
  67. package/src/runtime/overflow-recovery.ts +175 -156
  68. package/src/runtime/parallel-research.ts +44 -44
  69. package/src/runtime/pi-json-output.ts +111 -111
  70. package/src/runtime/policy-engine.ts +79 -79
  71. package/src/runtime/progress-event-coalescer.ts +43 -43
  72. package/src/runtime/recovery-recipes.ts +74 -74
  73. package/src/runtime/retry-executor.ts +64 -64
  74. package/src/runtime/role-permission.ts +39 -39
  75. package/src/runtime/session-resources.ts +25 -25
  76. package/src/runtime/session-snapshot.ts +59 -59
  77. package/src/runtime/session-usage.ts +79 -79
  78. package/src/runtime/sidechain-output.ts +29 -29
  79. package/src/runtime/stale-reconciler.ts +199 -179
  80. package/src/runtime/supervisor-contact.ts +59 -59
  81. package/src/runtime/task-display.ts +38 -38
  82. package/src/runtime/task-output-context.ts +127 -127
  83. package/src/runtime/task-runner/live-executor.ts +101 -101
  84. package/src/runtime/task-runner/progress.ts +119 -119
  85. package/src/runtime/task-runner/result-utils.ts +14 -14
  86. package/src/runtime/task-runner/state-helpers.ts +22 -22
  87. package/src/runtime/team-runner.ts +13 -4
  88. package/src/runtime/worker-heartbeat.ts +21 -21
  89. package/src/runtime/worker-startup.ts +57 -57
  90. package/src/state/state-store.ts +43 -0
  91. package/src/state/task-claims.ts +44 -44
  92. package/src/state/types.ts +2 -0
  93. package/src/state/usage.ts +29 -29
  94. package/src/subagents/async-entry.ts +1 -1
  95. package/src/subagents/index.ts +3 -3
  96. package/src/subagents/live/control.ts +1 -1
  97. package/src/subagents/live/manager.ts +1 -1
  98. package/src/subagents/live/realtime.ts +1 -1
  99. package/src/subagents/live/session-runtime.ts +1 -1
  100. package/src/subagents/manager.ts +1 -1
  101. package/src/subagents/spawn.ts +1 -1
  102. package/src/teams/team-serializer.ts +38 -38
  103. package/src/types/diff.d.ts +18 -18
  104. package/src/ui/crew-footer.ts +101 -101
  105. package/src/ui/crew-select-list.ts +111 -111
  106. package/src/ui/crew-widget.ts +5 -1
  107. package/src/ui/dashboard-panes/mailbox-pane.ts +2 -1
  108. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  109. package/src/ui/dynamic-border.ts +25 -25
  110. package/src/ui/layout-primitives.ts +106 -106
  111. package/src/ui/loaders.ts +158 -158
  112. package/src/ui/powerbar-publisher.ts +1 -1
  113. package/src/ui/render-diff.ts +119 -119
  114. package/src/ui/render-scheduler.ts +143 -143
  115. package/src/ui/run-snapshot-cache.ts +56 -37
  116. package/src/ui/snapshot-types.ts +5 -0
  117. package/src/ui/spinner.ts +17 -17
  118. package/src/ui/status-colors.ts +58 -58
  119. package/src/ui/syntax-highlight.ts +116 -116
  120. package/src/utils/atomic-write.ts +33 -33
  121. package/src/utils/completion-dedupe.ts +63 -63
  122. package/src/utils/frontmatter.ts +68 -68
  123. package/src/utils/git.ts +262 -262
  124. package/src/utils/ids.ts +12 -12
  125. package/src/utils/names.ts +27 -27
  126. package/src/utils/redaction.ts +44 -44
  127. package/src/utils/safe-paths.ts +47 -47
  128. package/src/utils/sleep.ts +32 -32
  129. package/src/workflows/validate-workflow.ts +40 -40
  130. package/src/worktree/branch-freshness.ts +45 -45
  131. package/teams/default.team.md +12 -12
  132. package/teams/fast-fix.team.md +11 -11
  133. package/teams/implementation.team.md +18 -18
  134. package/teams/parallel-research.team.md +14 -14
  135. package/teams/research.team.md +11 -11
  136. package/teams/review.team.md +12 -12
  137. package/workflows/default.workflow.md +29 -29
  138. package/workflows/fast-fix.workflow.md +22 -22
  139. package/workflows/implementation.workflow.md +38 -38
  140. package/workflows/parallel-research.workflow.md +46 -46
  141. package/workflows/research.workflow.md +22 -22
  142. package/workflows/review.workflow.md +30 -30
@@ -1,179 +1,199 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
4
- import { checkProcessLiveness } from "./process-status.ts";
5
- import { logInternalError } from "../utils/internal-error.ts";
6
- import { writeAtomicJson } from "../utils/atomic-write.ts";
7
-
8
- /**
9
- * Result of reconciling a single stale run.
10
- */
11
- export interface ReconcileResult {
12
- runId: string;
13
- /** What was found and what action was taken */
14
- verdict: "healthy" | "result_exists" | "pid_dead" | "pid_alive_stale" | "no_status";
15
- /** Whether repair was applied */
16
- repaired: boolean;
17
- /** Human-readable detail */
18
- detail: string;
19
- }
20
-
21
- const STALE_ALIVE_PID_MS = 24 * 60 * 60 * 1000; // 24 hours
22
-
23
- /**
24
- * Phase 1: Check if a result file already exists for the run.
25
- * If so, the run completed but status wasn't updated repair it.
26
- */
27
- function checkResultFile(
28
- manifest: TeamRunManifest,
29
- tasks: TeamTaskState[],
30
- ): { found: boolean; repaired: boolean } {
31
- // Check if all tasks already have terminal status (result was written but manifest wasn't updated)
32
- const allTerminal = tasks.length > 0 && tasks.every(
33
- (t) => t.status === "completed" || t.status === "failed" || t.status === "cancelled" || t.status === "skipped",
34
- );
35
- if (allTerminal) {
36
- return { found: true, repaired: false };
37
- }
38
- return { found: false, repaired: false };
39
- }
40
-
41
- /**
42
- * Phase 2: Check PID liveness.
43
- */
44
- function checkPidLiveness(pid: number | undefined): {
45
- alive: boolean;
46
- detail: string;
47
- } {
48
- if (pid === undefined || !Number.isInteger(pid) || pid <= 0) {
49
- return { alive: false, detail: "no pid recorded" };
50
- }
51
- const liveness = checkProcessLiveness(pid);
52
- return { alive: liveness.alive, detail: liveness.detail };
53
- }
54
-
55
- /**
56
- * Phase 3: For dead PIDs, repair immediately.
57
- * For alive PIDs, only mark stale if status hasn't updated in STALE_ALIVE_PID_MS.
58
- */
59
- function evaluateStaleness(
60
- manifest: TeamRunManifest,
61
- pidAlive: boolean,
62
- now: number,
63
- ): { stale: boolean; reason: string } {
64
- if (!pidAlive) {
65
- return { stale: true, reason: "pid_dead" };
66
- }
67
- const updatedAt = new Date(manifest.updatedAt).getTime();
68
- if (!Number.isFinite(updatedAt)) {
69
- return { stale: false, reason: "updated_at_invalid" };
70
- }
71
- if (now - updatedAt > STALE_ALIVE_PID_MS) {
72
- return { stale: true, reason: `alive_but_stale_${Math.round((now - updatedAt) / 3600_000)}h` };
73
- }
74
- return { stale: false, reason: "alive_and_recent" };
75
- }
76
-
77
- /**
78
- * Repair a stale run by marking it as failed and cancelling running tasks.
79
- */
80
- function repairStaleRun(
81
- manifest: TeamRunManifest,
82
- tasks: TeamTaskState[],
83
- reason: string,
84
- ): TeamTaskState[] {
85
- const now = new Date().toISOString();
86
- const repairedTasks = tasks.map((task) => {
87
- if (task.status === "running" || task.status === "queued" || task.status === "waiting") {
88
- return {
89
- ...task,
90
- status: "cancelled" as const,
91
- finishedAt: now,
92
- error: `Stale run reconciled: ${reason}`,
93
- };
94
- }
95
- return task;
96
- });
97
-
98
- // Write repaired tasks atomically
99
- const tasksPath = manifest.tasksPath;
100
- if (tasksPath) {
101
- try {
102
- writeAtomicJson(tasksPath, repairedTasks);
103
- } catch (error) {
104
- logInternalError("stale-reconciler.repair-tasks", error, `runId=${manifest.runId}`);
105
- }
106
- }
107
-
108
- return repairedTasks;
109
- }
110
-
111
- /**
112
- * Three-phase stale run reconciliation.
113
- *
114
- * 1. Check if result already exists → use it
115
- * 2. Check PID liveness
116
- * 3. Dead PID → repair immediately; alive PID → only fail if stale > 24h
117
- */
118
- export function reconcileStaleRun(
119
- manifest: TeamRunManifest,
120
- tasks: TeamTaskState[],
121
- now = Date.now(),
122
- ): ReconcileResult {
123
- const runId = manifest.runId;
124
-
125
- // Phase 1: Check if results already exist
126
- const phase1 = checkResultFile(manifest, tasks);
127
- if (phase1.found) {
128
- return {
129
- runId,
130
- verdict: "result_exists",
131
- repaired: false,
132
- detail: "All tasks already terminal — no repair needed",
133
- };
134
- }
135
-
136
- // Phase 2: Check PID liveness
137
- const pid = manifest.async?.pid;
138
- const pidStatus = checkPidLiveness(pid);
139
-
140
- if (pidStatus.detail === "no pid recorded") {
141
- // No async PID — not an async run, check updatedAt staleness
142
- const updatedAt = new Date(manifest.updatedAt).getTime();
143
- if (Number.isFinite(updatedAt) && now - updatedAt > STALE_ALIVE_PID_MS) {
144
- const repaired = repairStaleRun(manifest, tasks, "no_pid_stale");
145
- return {
146
- runId,
147
- verdict: "no_status",
148
- repaired: true,
149
- detail: `No PID; stale ${Math.round((now - updatedAt) / 3600_000)}h; repaired ${repaired.filter((t) => t.status === "cancelled").length} tasks`,
150
- };
151
- }
152
- return {
153
- runId,
154
- verdict: "no_status",
155
- repaired: false,
156
- detail: "No PID recorded; not stale enough to repair",
157
- };
158
- }
159
-
160
- // Phase 3: Evaluate staleness
161
- const staleness = evaluateStaleness(manifest, pidStatus.alive, now);
162
- if (!staleness.stale) {
163
- return {
164
- runId,
165
- verdict: "healthy",
166
- repaired: false,
167
- detail: `PID ${pid}: ${pidStatus.detail}, ${staleness.reason}`,
168
- };
169
- }
170
-
171
- // Repair
172
- const repaired = repairStaleRun(manifest, tasks, staleness.reason);
173
- return {
174
- runId,
175
- verdict: pidStatus.alive ? "pid_alive_stale" : "pid_dead",
176
- repaired: true,
177
- detail: `PID ${pid}: ${pidStatus.detail}; ${staleness.reason}; repaired ${repaired.filter((t) => t.status === "cancelled").length} tasks`,
178
- };
179
- }
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
4
+ import { checkProcessLiveness } from "./process-status.ts";
5
+ import { logInternalError } from "../utils/internal-error.ts";
6
+ import { writeAtomicJson } from "../utils/atomic-write.ts";
7
+
8
+ /**
9
+ * Result of reconciling a single stale run.
10
+ */
11
+ export interface ReconcileResult {
12
+ runId: string;
13
+ /** What was found and what action was taken */
14
+ verdict: "healthy" | "result_exists" | "pid_dead" | "pid_alive_stale" | "no_status";
15
+ /** Whether repair was applied */
16
+ repaired: boolean;
17
+ /** Human-readable detail */
18
+ detail: string;
19
+ }
20
+
21
+ const STALE_ALIVE_PID_MS = 24 * 60 * 60 * 1000; // 24 hours
22
+ const ACTIVE_EVIDENCE_TTL_MS = 5 * 60 * 1000;
23
+
24
+ /**
25
+ * Phase 1: Check if a result file already exists for the run.
26
+ * If so, the run completed but status wasn't updated — repair it.
27
+ */
28
+ function checkResultFile(
29
+ manifest: TeamRunManifest,
30
+ tasks: TeamTaskState[],
31
+ ): { found: boolean; repaired: boolean } {
32
+ // Check if all tasks already have terminal status (result was written but manifest wasn't updated)
33
+ const allTerminal = tasks.length > 0 && tasks.every(
34
+ (t) => t.status === "completed" || t.status === "failed" || t.status === "cancelled" || t.status === "skipped",
35
+ );
36
+ if (allTerminal) {
37
+ return { found: true, repaired: false };
38
+ }
39
+ return { found: false, repaired: false };
40
+ }
41
+
42
+ /**
43
+ * Phase 2: Check PID liveness.
44
+ */
45
+ function checkPidLiveness(pid: number | undefined): {
46
+ alive: boolean;
47
+ detail: string;
48
+ } {
49
+ if (pid === undefined || !Number.isInteger(pid) || pid <= 0) {
50
+ return { alive: false, detail: "no pid recorded" };
51
+ }
52
+ const liveness = checkProcessLiveness(pid);
53
+ return { alive: liveness.alive, detail: liveness.detail };
54
+ }
55
+
56
+ /**
57
+ * Phase 3: For dead PIDs, repair immediately.
58
+ * For alive PIDs, only mark stale if status hasn't updated in STALE_ALIVE_PID_MS.
59
+ */
60
+ function evaluateStaleness(
61
+ manifest: TeamRunManifest,
62
+ pidAlive: boolean,
63
+ now: number,
64
+ ): { stale: boolean; reason: string } {
65
+ if (!pidAlive) {
66
+ return { stale: true, reason: "pid_dead" };
67
+ }
68
+ const updatedAt = new Date(manifest.updatedAt).getTime();
69
+ if (!Number.isFinite(updatedAt)) {
70
+ return { stale: false, reason: "updated_at_invalid" };
71
+ }
72
+ if (now - updatedAt > STALE_ALIVE_PID_MS) {
73
+ return { stale: true, reason: `alive_but_stale_${Math.round((now - updatedAt) / 3600_000)}h` };
74
+ }
75
+ return { stale: false, reason: "alive_and_recent" };
76
+ }
77
+
78
+ function hasRecentActiveEvidence(tasks: TeamTaskState[], now: number): boolean {
79
+ return tasks.some((task) => {
80
+ if (task.status !== "running" && task.status !== "waiting") return false;
81
+ const heartbeatAt = task.heartbeat?.lastSeenAt ? new Date(task.heartbeat.lastSeenAt).getTime() : Number.NaN;
82
+ if (task.heartbeat?.alive !== false && Number.isFinite(heartbeatAt) && now - heartbeatAt <= ACTIVE_EVIDENCE_TTL_MS) return true;
83
+ const activityAt = task.agentProgress?.lastActivityAt ? new Date(task.agentProgress.lastActivityAt).getTime() : Number.NaN;
84
+ return Number.isFinite(activityAt) && now - activityAt <= ACTIVE_EVIDENCE_TTL_MS;
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Repair a stale run by marking it as failed and cancelling running tasks.
90
+ */
91
+ function repairStaleRun(
92
+ manifest: TeamRunManifest,
93
+ tasks: TeamTaskState[],
94
+ reason: string,
95
+ ): TeamTaskState[] {
96
+ const now = new Date().toISOString();
97
+ const repairedTasks = tasks.map((task) => {
98
+ if (task.status === "running" || task.status === "queued" || task.status === "waiting") {
99
+ return {
100
+ ...task,
101
+ status: "cancelled" as const,
102
+ finishedAt: now,
103
+ error: `Stale run reconciled: ${reason}`,
104
+ };
105
+ }
106
+ return task;
107
+ });
108
+
109
+ // Write repaired tasks atomically
110
+ const tasksPath = manifest.tasksPath;
111
+ if (tasksPath) {
112
+ try {
113
+ writeAtomicJson(tasksPath, repairedTasks);
114
+ } catch (error) {
115
+ logInternalError("stale-reconciler.repair-tasks", error, `runId=${manifest.runId}`);
116
+ }
117
+ }
118
+
119
+ return repairedTasks;
120
+ }
121
+
122
+ /**
123
+ * Three-phase stale run reconciliation.
124
+ *
125
+ * 1. Check if result already exists → use it
126
+ * 2. Check PID liveness
127
+ * 3. Dead PID → repair immediately; alive PID → only fail if stale > 24h
128
+ */
129
+ export function reconcileStaleRun(
130
+ manifest: TeamRunManifest,
131
+ tasks: TeamTaskState[],
132
+ now = Date.now(),
133
+ ): ReconcileResult {
134
+ const runId = manifest.runId;
135
+
136
+ // Phase 1: Check if results already exist
137
+ const phase1 = checkResultFile(manifest, tasks);
138
+ if (phase1.found) {
139
+ return {
140
+ runId,
141
+ verdict: "result_exists",
142
+ repaired: false,
143
+ detail: "All tasks already terminal no repair needed",
144
+ };
145
+ }
146
+
147
+ // Phase 2: Check PID liveness
148
+ const pid = manifest.async?.pid;
149
+ const pidStatus = checkPidLiveness(pid);
150
+
151
+ if (pidStatus.detail === "no pid recorded") {
152
+ // No async PID may be a foreground/live run. Preserve it if task heartbeat
153
+ // or agent progress proves active work even when manifest.updatedAt is old.
154
+ if (hasRecentActiveEvidence(tasks, now)) {
155
+ return {
156
+ runId,
157
+ verdict: "no_status",
158
+ repaired: false,
159
+ detail: "No PID recorded, but recent task heartbeat/progress exists; not repairing",
160
+ };
161
+ }
162
+ const updatedAt = new Date(manifest.updatedAt).getTime();
163
+ if (Number.isFinite(updatedAt) && now - updatedAt > STALE_ALIVE_PID_MS) {
164
+ const repaired = repairStaleRun(manifest, tasks, "no_pid_stale");
165
+ return {
166
+ runId,
167
+ verdict: "no_status",
168
+ repaired: true,
169
+ detail: `No PID; stale ${Math.round((now - updatedAt) / 3600_000)}h; repaired ${repaired.filter((t) => t.status === "cancelled").length} tasks`,
170
+ };
171
+ }
172
+ return {
173
+ runId,
174
+ verdict: "no_status",
175
+ repaired: false,
176
+ detail: "No PID recorded; not stale enough to repair",
177
+ };
178
+ }
179
+
180
+ // Phase 3: Evaluate staleness
181
+ const staleness = evaluateStaleness(manifest, pidStatus.alive, now);
182
+ if (!staleness.stale) {
183
+ return {
184
+ runId,
185
+ verdict: "healthy",
186
+ repaired: false,
187
+ detail: `PID ${pid}: ${pidStatus.detail}, ${staleness.reason}`,
188
+ };
189
+ }
190
+
191
+ // Repair
192
+ const repaired = repairStaleRun(manifest, tasks, staleness.reason);
193
+ return {
194
+ runId,
195
+ verdict: pidStatus.alive ? "pid_alive_stale" : "pid_dead",
196
+ repaired: true,
197
+ detail: `PID ${pid}: ${pidStatus.detail}; ${staleness.reason}; repaired ${repaired.filter((t) => t.status === "cancelled").length} tasks`,
198
+ };
199
+ }
@@ -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
+ }