pi-crew 0.1.43 → 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 (158) 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 +199 -0
  20. package/docs/research-phase11-distillation.md +201 -0
  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/agents/discover-agents.ts +1 -0
  29. package/src/config/config.ts +19 -0
  30. package/src/extension/cross-extension-rpc.ts +82 -82
  31. package/src/extension/register.ts +134 -8
  32. package/src/extension/registration/commands.ts +18 -2
  33. package/src/extension/registration/compaction-guard.ts +125 -125
  34. package/src/extension/registration/subagent-tools.ts +148 -148
  35. package/src/extension/registration/team-tool.ts +27 -8
  36. package/src/extension/run-bundle-schema.ts +89 -89
  37. package/src/extension/run-index.ts +19 -0
  38. package/src/extension/run-maintenance.ts +43 -43
  39. package/src/extension/team-tool/api.ts +1 -1
  40. package/src/extension/team-tool/cancel.ts +79 -4
  41. package/src/extension/team-tool/context.ts +2 -0
  42. package/src/extension/team-tool/handle-settings.ts +188 -188
  43. package/src/extension/team-tool/inspect.ts +41 -41
  44. package/src/extension/team-tool/lifecycle-actions.ts +79 -79
  45. package/src/extension/team-tool/plan.ts +19 -19
  46. package/src/extension/team-tool/respond.ts +84 -0
  47. package/src/extension/team-tool/run.ts +3 -2
  48. package/src/extension/team-tool/status.ts +7 -1
  49. package/src/extension/team-tool-types.ts +4 -0
  50. package/src/extension/team-tool.ts +2 -0
  51. package/src/i18n.ts +184 -184
  52. package/src/observability/event-to-metric.ts +6 -0
  53. package/src/observability/exporters/otlp-exporter.ts +77 -77
  54. package/src/prompt/prompt-runtime.ts +72 -72
  55. package/src/runtime/agent-control.ts +63 -63
  56. package/src/runtime/agent-memory.ts +72 -72
  57. package/src/runtime/agent-observability.ts +114 -114
  58. package/src/runtime/async-marker.ts +26 -26
  59. package/src/runtime/attention-events.ts +28 -28
  60. package/src/runtime/background-runner.ts +53 -53
  61. package/src/runtime/child-pi.ts +444 -444
  62. package/src/runtime/completion-guard.ts +87 -0
  63. package/src/runtime/crash-recovery.ts +30 -0
  64. package/src/runtime/crew-agent-records.ts +8 -0
  65. package/src/runtime/crew-agent-runtime.ts +2 -1
  66. package/src/runtime/delivery-coordinator.ts +154 -0
  67. package/src/runtime/direct-run.ts +35 -35
  68. package/src/runtime/foreground-control.ts +82 -82
  69. package/src/runtime/green-contract.ts +46 -46
  70. package/src/runtime/group-join.ts +106 -106
  71. package/src/runtime/heartbeat-gradient.ts +28 -28
  72. package/src/runtime/heartbeat-watcher.ts +124 -124
  73. package/src/runtime/live-agent-control.ts +87 -87
  74. package/src/runtime/live-agent-manager.ts +85 -85
  75. package/src/runtime/live-control-realtime.ts +36 -36
  76. package/src/runtime/live-session-runtime.ts +305 -305
  77. package/src/runtime/model-fallback.ts +5 -2
  78. package/src/runtime/overflow-recovery.ts +176 -0
  79. package/src/runtime/parallel-research.ts +44 -44
  80. package/src/runtime/pi-json-output.ts +111 -111
  81. package/src/runtime/policy-engine.ts +79 -79
  82. package/src/runtime/process-status.ts +1 -1
  83. package/src/runtime/progress-event-coalescer.ts +43 -43
  84. package/src/runtime/recovery-recipes.ts +74 -74
  85. package/src/runtime/retry-executor.ts +64 -64
  86. package/src/runtime/role-permission.ts +39 -39
  87. package/src/runtime/session-resources.ts +25 -0
  88. package/src/runtime/session-snapshot.ts +59 -0
  89. package/src/runtime/session-usage.ts +79 -79
  90. package/src/runtime/sidechain-output.ts +29 -29
  91. package/src/runtime/stale-reconciler.ts +199 -0
  92. package/src/runtime/supervisor-contact.ts +59 -0
  93. package/src/runtime/task-display.ts +38 -38
  94. package/src/runtime/task-output-context.ts +127 -127
  95. package/src/runtime/task-runner/live-executor.ts +101 -101
  96. package/src/runtime/task-runner/progress.ts +119 -119
  97. package/src/runtime/task-runner/result-utils.ts +14 -14
  98. package/src/runtime/task-runner/state-helpers.ts +22 -22
  99. package/src/runtime/task-runner.ts +14 -0
  100. package/src/runtime/team-runner.ts +19 -8
  101. package/src/runtime/worker-heartbeat.ts +21 -21
  102. package/src/runtime/worker-startup.ts +57 -57
  103. package/src/schema/config-schema.ts +1 -0
  104. package/src/schema/team-tool-schema.ts +6 -1
  105. package/src/state/contracts.ts +6 -2
  106. package/src/state/state-store.ts +43 -0
  107. package/src/state/task-claims.ts +44 -44
  108. package/src/state/types.ts +2 -0
  109. package/src/state/usage.ts +29 -29
  110. package/src/subagents/async-entry.ts +1 -1
  111. package/src/subagents/index.ts +3 -3
  112. package/src/subagents/live/control.ts +1 -1
  113. package/src/subagents/live/manager.ts +1 -1
  114. package/src/subagents/live/realtime.ts +1 -1
  115. package/src/subagents/live/session-runtime.ts +1 -1
  116. package/src/subagents/manager.ts +1 -1
  117. package/src/subagents/spawn.ts +1 -1
  118. package/src/teams/team-serializer.ts +38 -38
  119. package/src/types/diff.d.ts +18 -18
  120. package/src/ui/crew-footer.ts +101 -101
  121. package/src/ui/crew-select-list.ts +111 -111
  122. package/src/ui/crew-widget.ts +10 -5
  123. package/src/ui/dashboard-panes/mailbox-pane.ts +2 -1
  124. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  125. package/src/ui/dynamic-border.ts +25 -25
  126. package/src/ui/layout-primitives.ts +106 -106
  127. package/src/ui/loaders.ts +158 -158
  128. package/src/ui/powerbar-publisher.ts +4 -4
  129. package/src/ui/render-diff.ts +119 -119
  130. package/src/ui/render-scheduler.ts +143 -143
  131. package/src/ui/run-snapshot-cache.ts +310 -17
  132. package/src/ui/snapshot-types.ts +5 -0
  133. package/src/ui/spinner.ts +17 -17
  134. package/src/ui/status-colors.ts +58 -54
  135. package/src/ui/syntax-highlight.ts +116 -116
  136. package/src/utils/atomic-write.ts +33 -0
  137. package/src/utils/completion-dedupe.ts +63 -63
  138. package/src/utils/frontmatter.ts +68 -68
  139. package/src/utils/git.ts +262 -262
  140. package/src/utils/ids.ts +12 -12
  141. package/src/utils/names.ts +27 -27
  142. package/src/utils/redaction.ts +44 -44
  143. package/src/utils/safe-paths.ts +47 -47
  144. package/src/utils/sleep.ts +32 -32
  145. package/src/workflows/validate-workflow.ts +40 -40
  146. package/src/worktree/branch-freshness.ts +45 -45
  147. package/teams/default.team.md +12 -12
  148. package/teams/fast-fix.team.md +11 -11
  149. package/teams/implementation.team.md +18 -18
  150. package/teams/parallel-research.team.md +14 -14
  151. package/teams/research.team.md +11 -11
  152. package/teams/review.team.md +12 -12
  153. package/workflows/default.workflow.md +29 -29
  154. package/workflows/fast-fix.workflow.md +22 -22
  155. package/workflows/implementation.workflow.md +38 -38
  156. package/workflows/parallel-research.workflow.md +46 -46
  157. package/workflows/research.workflow.md +22 -22
  158. package/workflows/review.workflow.md +30 -30
@@ -1,79 +1,79 @@
1
- import * as fs from "node:fs";
2
- import type { UsageState } from "../state/types.ts";
3
-
4
- function asRecord(value: unknown): Record<string, unknown> | undefined {
5
- return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
6
- }
7
-
8
- function numberField(obj: Record<string, unknown>, keys: string[]): number | undefined {
9
- for (const key of keys) {
10
- const value = obj[key];
11
- if (typeof value === "number" && Number.isFinite(value)) return value;
12
- }
13
- return undefined;
14
- }
15
-
16
- function usageFromValue(value: unknown): UsageState | undefined {
17
- const obj = asRecord(value);
18
- if (!obj) return undefined;
19
- const direct: UsageState = {
20
- input: numberField(obj, ["input", "inputTokens", "input_tokens"]),
21
- output: numberField(obj, ["output", "outputTokens", "output_tokens"]),
22
- cacheRead: numberField(obj, ["cacheRead", "cache_read", "cacheReadTokens", "cache_read_tokens"]),
23
- cacheWrite: numberField(obj, ["cacheWrite", "cache_write", "cacheWriteTokens", "cache_write_tokens"]),
24
- cost: numberField(obj, ["cost", "costUsd", "cost_usd"]),
25
- turns: numberField(obj, ["turns", "turnCount", "turn_count"]),
26
- };
27
- if (Object.values(direct).some((entry) => entry !== undefined)) return direct;
28
- for (const key of ["usage", "tokenUsage", "tokens", "stats"]) {
29
- const nested = usageFromValue(obj[key]);
30
- if (nested) return nested;
31
- }
32
- const message = asRecord(obj.message);
33
- return message ? usageFromValue(message.usage) : undefined;
34
- }
35
-
36
- function addUsage(total: UsageState, usage: UsageState): UsageState {
37
- return {
38
- input: (total.input ?? 0) + (usage.input ?? 0),
39
- output: (total.output ?? 0) + (usage.output ?? 0),
40
- cacheRead: (total.cacheRead ?? 0) + (usage.cacheRead ?? 0),
41
- cacheWrite: (total.cacheWrite ?? 0) + (usage.cacheWrite ?? 0),
42
- cost: (total.cost ?? 0) + (usage.cost ?? 0),
43
- turns: (total.turns ?? 0) + (usage.turns ?? 0),
44
- };
45
- }
46
-
47
- function compactUsage(total: UsageState, foundKeys: Set<keyof UsageState>): UsageState | undefined {
48
- if (foundKeys.size === 0) return undefined;
49
- const compact: UsageState = {};
50
- for (const key of foundKeys) compact[key] = total[key];
51
- return compact;
52
- }
53
-
54
- export function parseSessionUsageFromJsonlText(text: string): UsageState | undefined {
55
- let total: UsageState = {};
56
- const foundKeys = new Set<keyof UsageState>();
57
- for (const line of text.split(/\r?\n/)) {
58
- const trimmed = line.trim();
59
- if (!trimmed) continue;
60
- try {
61
- const usage = usageFromValue(JSON.parse(trimmed) as unknown);
62
- if (!usage) continue;
63
- for (const key of Object.keys(usage) as Array<keyof UsageState>) foundKeys.add(key);
64
- total = addUsage(total, usage);
65
- } catch {
66
- // Session JSONL can contain partial/corrupt lines after interrupted workers.
67
- }
68
- }
69
- return compactUsage(total, foundKeys);
70
- }
71
-
72
- export function parseSessionUsage(filePath: string): UsageState | undefined {
73
- try {
74
- if (!fs.existsSync(filePath)) return undefined;
75
- return parseSessionUsageFromJsonlText(fs.readFileSync(filePath, "utf-8"));
76
- } catch {
77
- return undefined;
78
- }
79
- }
1
+ import * as fs from "node:fs";
2
+ import type { UsageState } from "../state/types.ts";
3
+
4
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
5
+ return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
6
+ }
7
+
8
+ function numberField(obj: Record<string, unknown>, keys: string[]): number | undefined {
9
+ for (const key of keys) {
10
+ const value = obj[key];
11
+ if (typeof value === "number" && Number.isFinite(value)) return value;
12
+ }
13
+ return undefined;
14
+ }
15
+
16
+ function usageFromValue(value: unknown): UsageState | undefined {
17
+ const obj = asRecord(value);
18
+ if (!obj) return undefined;
19
+ const direct: UsageState = {
20
+ input: numberField(obj, ["input", "inputTokens", "input_tokens"]),
21
+ output: numberField(obj, ["output", "outputTokens", "output_tokens"]),
22
+ cacheRead: numberField(obj, ["cacheRead", "cache_read", "cacheReadTokens", "cache_read_tokens"]),
23
+ cacheWrite: numberField(obj, ["cacheWrite", "cache_write", "cacheWriteTokens", "cache_write_tokens"]),
24
+ cost: numberField(obj, ["cost", "costUsd", "cost_usd"]),
25
+ turns: numberField(obj, ["turns", "turnCount", "turn_count"]),
26
+ };
27
+ if (Object.values(direct).some((entry) => entry !== undefined)) return direct;
28
+ for (const key of ["usage", "tokenUsage", "tokens", "stats"]) {
29
+ const nested = usageFromValue(obj[key]);
30
+ if (nested) return nested;
31
+ }
32
+ const message = asRecord(obj.message);
33
+ return message ? usageFromValue(message.usage) : undefined;
34
+ }
35
+
36
+ function addUsage(total: UsageState, usage: UsageState): UsageState {
37
+ return {
38
+ input: (total.input ?? 0) + (usage.input ?? 0),
39
+ output: (total.output ?? 0) + (usage.output ?? 0),
40
+ cacheRead: (total.cacheRead ?? 0) + (usage.cacheRead ?? 0),
41
+ cacheWrite: (total.cacheWrite ?? 0) + (usage.cacheWrite ?? 0),
42
+ cost: (total.cost ?? 0) + (usage.cost ?? 0),
43
+ turns: (total.turns ?? 0) + (usage.turns ?? 0),
44
+ };
45
+ }
46
+
47
+ function compactUsage(total: UsageState, foundKeys: Set<keyof UsageState>): UsageState | undefined {
48
+ if (foundKeys.size === 0) return undefined;
49
+ const compact: UsageState = {};
50
+ for (const key of foundKeys) compact[key] = total[key];
51
+ return compact;
52
+ }
53
+
54
+ export function parseSessionUsageFromJsonlText(text: string): UsageState | undefined {
55
+ let total: UsageState = {};
56
+ const foundKeys = new Set<keyof UsageState>();
57
+ for (const line of text.split(/\r?\n/)) {
58
+ const trimmed = line.trim();
59
+ if (!trimmed) continue;
60
+ try {
61
+ const usage = usageFromValue(JSON.parse(trimmed) as unknown);
62
+ if (!usage) continue;
63
+ for (const key of Object.keys(usage) as Array<keyof UsageState>) foundKeys.add(key);
64
+ total = addUsage(total, usage);
65
+ } catch {
66
+ // Session JSONL can contain partial/corrupt lines after interrupted workers.
67
+ }
68
+ }
69
+ return compactUsage(total, foundKeys);
70
+ }
71
+
72
+ export function parseSessionUsage(filePath: string): UsageState | undefined {
73
+ try {
74
+ if (!fs.existsSync(filePath)) return undefined;
75
+ return parseSessionUsageFromJsonlText(fs.readFileSync(filePath, "utf-8"));
76
+ } catch {
77
+ return undefined;
78
+ }
79
+ }
@@ -1,29 +1,29 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import { redactSecrets } from "../utils/redaction.ts";
4
-
5
- export interface SidechainEntry {
6
- isSidechain: true;
7
- agentId: string;
8
- type: string;
9
- message: unknown;
10
- timestamp: string;
11
- cwd: string;
12
- }
13
-
14
- export function writeSidechainEntry(filePath: string, entry: Omit<SidechainEntry, "isSidechain" | "timestamp">): void {
15
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
16
- fs.appendFileSync(filePath, `${JSON.stringify(redactSecrets({ isSidechain: true, timestamp: new Date().toISOString(), ...entry }))}\n`, "utf-8");
17
- }
18
-
19
- export function sidechainOutputPath(stateRoot: string, taskId: string): string {
20
- return path.join(stateRoot, "agents", taskId, "sidechain.output.jsonl");
21
- }
22
-
23
- export function eventToSidechainType(event: unknown): string | undefined {
24
- if (!event || typeof event !== "object" || Array.isArray(event)) return undefined;
25
- const type = (event as { type?: unknown }).type;
26
- if (type === "message_start" || type === "message_update" || type === "message_end") return "message";
27
- if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") return "tool";
28
- return typeof type === "string" ? type : undefined;
29
- }
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { redactSecrets } from "../utils/redaction.ts";
4
+
5
+ export interface SidechainEntry {
6
+ isSidechain: true;
7
+ agentId: string;
8
+ type: string;
9
+ message: unknown;
10
+ timestamp: string;
11
+ cwd: string;
12
+ }
13
+
14
+ export function writeSidechainEntry(filePath: string, entry: Omit<SidechainEntry, "isSidechain" | "timestamp">): void {
15
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
16
+ fs.appendFileSync(filePath, `${JSON.stringify(redactSecrets({ isSidechain: true, timestamp: new Date().toISOString(), ...entry }))}\n`, "utf-8");
17
+ }
18
+
19
+ export function sidechainOutputPath(stateRoot: string, taskId: string): string {
20
+ return path.join(stateRoot, "agents", taskId, "sidechain.output.jsonl");
21
+ }
22
+
23
+ export function eventToSidechainType(event: unknown): string | undefined {
24
+ if (!event || typeof event !== "object" || Array.isArray(event)) return undefined;
25
+ const type = (event as { type?: unknown }).type;
26
+ if (type === "message_start" || type === "message_update" || type === "message_end") return "message";
27
+ if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") return "tool";
28
+ return typeof type === "string" ? type : undefined;
29
+ }
@@ -0,0 +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
+ 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
+ }
@@ -0,0 +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,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
+ }