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.
- package/CHANGELOG.md +27 -0
- package/agents/analyst.md +11 -11
- package/agents/critic.md +11 -11
- package/agents/executor.md +11 -11
- package/agents/explorer.md +11 -11
- package/agents/planner.md +11 -11
- package/agents/reviewer.md +11 -11
- package/agents/security-reviewer.md +11 -11
- package/agents/test-engineer.md +11 -11
- package/agents/verifier.md +11 -11
- package/agents/writer.md +11 -11
- package/docs/refactor-tasks-phase3.md +394 -394
- package/docs/refactor-tasks-phase4.md +564 -564
- package/docs/refactor-tasks-phase5.md +402 -402
- package/docs/refactor-tasks-phase6.md +662 -662
- package/docs/research-extension-examples.md +297 -297
- package/docs/research-extension-system.md +324 -324
- package/docs/research-optimization-plan.md +548 -548
- package/docs/research-phase10-distillation.md +198 -198
- package/docs/research-phase11-distillation.md +201 -201
- package/docs/research-pi-coding-agent.md +357 -357
- package/docs/research-source-pi-crew-reference.md +174 -174
- package/docs/runtime-flow.md +148 -148
- package/docs/source-runtime-refactor-map.md +83 -83
- package/index.ts +6 -6
- package/package.json +1 -1
- package/src/agents/agent-serializer.ts +34 -34
- package/src/extension/cross-extension-rpc.ts +82 -82
- package/src/extension/register.ts +8 -1
- package/src/extension/registration/commands.ts +18 -2
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-tools.ts +148 -148
- package/src/extension/registration/team-tool.ts +26 -8
- package/src/extension/run-bundle-schema.ts +89 -89
- package/src/extension/run-maintenance.ts +43 -43
- package/src/extension/team-tool/cancel.ts +105 -102
- package/src/extension/team-tool/context.ts +1 -0
- package/src/extension/team-tool/handle-settings.ts +188 -188
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +79 -79
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/respond.ts +83 -66
- package/src/extension/team-tool/run.ts +1 -0
- package/src/i18n.ts +184 -184
- package/src/observability/exporters/otlp-exporter.ts +77 -77
- package/src/prompt/prompt-runtime.ts +72 -72
- package/src/runtime/agent-control.ts +63 -63
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -114
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/attention-events.ts +28 -28
- package/src/runtime/background-runner.ts +53 -53
- package/src/runtime/child-pi.ts +444 -444
- package/src/runtime/completion-guard.ts +190 -190
- package/src/runtime/crew-agent-records.ts +8 -0
- package/src/runtime/delivery-coordinator.ts +153 -142
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +106 -106
- package/src/runtime/heartbeat-gradient.ts +28 -28
- package/src/runtime/heartbeat-watcher.ts +124 -124
- package/src/runtime/live-agent-control.ts +87 -87
- package/src/runtime/live-agent-manager.ts +85 -85
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +305 -305
- package/src/runtime/overflow-recovery.ts +175 -156
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/policy-engine.ts +79 -79
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +64 -64
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/session-resources.ts +25 -25
- package/src/runtime/session-snapshot.ts +59 -59
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/stale-reconciler.ts +199 -179
- package/src/runtime/supervisor-contact.ts +59 -59
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-output-context.ts +127 -127
- package/src/runtime/task-runner/live-executor.ts +101 -101
- package/src/runtime/task-runner/progress.ts +119 -119
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/team-runner.ts +13 -4
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/state/state-store.ts +43 -0
- package/src/state/task-claims.ts +44 -44
- package/src/state/types.ts +2 -0
- package/src/state/usage.ts +29 -29
- package/src/subagents/async-entry.ts +1 -1
- package/src/subagents/index.ts +3 -3
- package/src/subagents/live/control.ts +1 -1
- package/src/subagents/live/manager.ts +1 -1
- package/src/subagents/live/realtime.ts +1 -1
- package/src/subagents/live/session-runtime.ts +1 -1
- package/src/subagents/manager.ts +1 -1
- package/src/subagents/spawn.ts +1 -1
- package/src/teams/team-serializer.ts +38 -38
- package/src/types/diff.d.ts +18 -18
- package/src/ui/crew-footer.ts +101 -101
- package/src/ui/crew-select-list.ts +111 -111
- package/src/ui/crew-widget.ts +5 -1
- package/src/ui/dashboard-panes/mailbox-pane.ts +2 -1
- package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
- package/src/ui/dynamic-border.ts +25 -25
- package/src/ui/layout-primitives.ts +106 -106
- package/src/ui/loaders.ts +158 -158
- package/src/ui/powerbar-publisher.ts +1 -1
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/render-scheduler.ts +143 -143
- package/src/ui/run-snapshot-cache.ts +56 -37
- package/src/ui/snapshot-types.ts +5 -0
- package/src/ui/spinner.ts +17 -17
- package/src/ui/status-colors.ts +58 -58
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/utils/atomic-write.ts +33 -33
- package/src/utils/completion-dedupe.ts +63 -63
- package/src/utils/frontmatter.ts +68 -68
- package/src/utils/git.ts +262 -262
- package/src/utils/ids.ts +12 -12
- package/src/utils/names.ts +27 -27
- package/src/utils/redaction.ts +44 -44
- package/src/utils/safe-paths.ts +47 -47
- package/src/utils/sleep.ts +32 -32
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/worktree/branch-freshness.ts +45 -45
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/workflows/default.workflow.md +29 -29
- package/workflows/fast-fix.workflow.md +22 -22
- package/workflows/implementation.workflow.md +38 -38
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- 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
|
-
|
|
25
|
-
*
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
* For
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// Phase
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
+
}
|