pi-crew 0.1.37 → 0.1.39

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 (162) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +27 -0
  3. package/README.md +5 -0
  4. package/agents/analyst.md +11 -11
  5. package/agents/critic.md +11 -11
  6. package/agents/executor.md +11 -11
  7. package/agents/explorer.md +11 -11
  8. package/agents/planner.md +11 -11
  9. package/agents/reviewer.md +11 -11
  10. package/agents/security-reviewer.md +11 -11
  11. package/agents/test-engineer.md +11 -11
  12. package/agents/verifier.md +11 -11
  13. package/agents/writer.md +11 -11
  14. package/docs/refactor-tasks-phase3.md +394 -394
  15. package/docs/refactor-tasks-phase4.md +564 -564
  16. package/docs/refactor-tasks-phase5.md +402 -402
  17. package/docs/refactor-tasks-phase6.md +662 -662
  18. package/docs/research-extension-examples.md +297 -297
  19. package/docs/research-extension-system.md +324 -324
  20. package/docs/research-optimization-plan.md +548 -548
  21. package/docs/research-pi-coding-agent.md +357 -357
  22. package/docs/research-source-pi-crew-reference.md +174 -174
  23. package/docs/resource-formats.md +10 -8
  24. package/docs/runtime-flow.md +148 -148
  25. package/docs/source-runtime-refactor-map.md +83 -83
  26. package/docs/usage.md +6 -0
  27. package/index.ts +6 -6
  28. package/package.json +3 -3
  29. package/schema.json +2 -2
  30. package/src/agents/agent-serializer.ts +34 -34
  31. package/src/config/config.ts +8 -4
  32. package/src/extension/cross-extension-rpc.ts +82 -82
  33. package/src/extension/import-index.ts +18 -2
  34. package/src/extension/register.ts +11 -1
  35. package/src/extension/registration/compaction-guard.ts +125 -125
  36. package/src/extension/registration/subagent-helpers.ts +30 -6
  37. package/src/extension/registration/subagent-tools.ts +8 -3
  38. package/src/extension/result-watcher.ts +98 -98
  39. package/src/extension/run-import.ts +12 -2
  40. package/src/extension/run-index.ts +12 -2
  41. package/src/extension/run-maintenance.ts +24 -24
  42. package/src/extension/team-tool/api.ts +54 -14
  43. package/src/extension/team-tool/cancel.ts +31 -31
  44. package/src/extension/team-tool/doctor.ts +179 -179
  45. package/src/extension/team-tool/inspect.ts +41 -41
  46. package/src/extension/team-tool/lifecycle-actions.ts +79 -79
  47. package/src/extension/team-tool/plan.ts +19 -19
  48. package/src/extension/team-tool/status.ts +73 -73
  49. package/src/observability/correlation.ts +35 -35
  50. package/src/observability/event-to-metric.ts +54 -54
  51. package/src/observability/exporters/adapter.ts +24 -24
  52. package/src/observability/exporters/otlp-exporter.ts +65 -65
  53. package/src/observability/exporters/prometheus-exporter.ts +47 -47
  54. package/src/observability/metric-registry.ts +72 -72
  55. package/src/observability/metric-retention.ts +46 -46
  56. package/src/observability/metric-sink.ts +51 -51
  57. package/src/observability/metrics-primitives.ts +166 -166
  58. package/src/prompt/prompt-runtime.ts +68 -68
  59. package/src/runtime/agent-control.ts +64 -64
  60. package/src/runtime/agent-memory.ts +72 -72
  61. package/src/runtime/agent-observability.ts +114 -113
  62. package/src/runtime/async-marker.ts +26 -26
  63. package/src/runtime/background-runner.ts +53 -53
  64. package/src/runtime/crash-recovery.ts +56 -56
  65. package/src/runtime/crew-agent-records.ts +54 -9
  66. package/src/runtime/crew-agent-runtime.ts +58 -58
  67. package/src/runtime/deadletter.ts +36 -36
  68. package/src/runtime/direct-run.ts +35 -35
  69. package/src/runtime/foreground-control.ts +82 -82
  70. package/src/runtime/green-contract.ts +46 -46
  71. package/src/runtime/group-join.ts +88 -88
  72. package/src/runtime/heartbeat-gradient.ts +28 -28
  73. package/src/runtime/heartbeat-watcher.ts +80 -80
  74. package/src/runtime/live-agent-control.ts +87 -78
  75. package/src/runtime/live-agent-manager.ts +85 -85
  76. package/src/runtime/live-control-realtime.ts +36 -36
  77. package/src/runtime/live-session-runtime.ts +299 -299
  78. package/src/runtime/manifest-cache.ts +248 -212
  79. package/src/runtime/model-fallback.ts +261 -261
  80. package/src/runtime/parallel-research.ts +44 -44
  81. package/src/runtime/parallel-utils.ts +99 -99
  82. package/src/runtime/pi-json-output.ts +111 -111
  83. package/src/runtime/policy-engine.ts +78 -78
  84. package/src/runtime/post-exit-stdio-guard.ts +86 -86
  85. package/src/runtime/process-status.ts +56 -56
  86. package/src/runtime/progress-event-coalescer.ts +43 -43
  87. package/src/runtime/recovery-recipes.ts +74 -74
  88. package/src/runtime/retry-executor.ts +59 -59
  89. package/src/runtime/role-permission.ts +39 -39
  90. package/src/runtime/session-usage.ts +79 -79
  91. package/src/runtime/sidechain-output.ts +28 -28
  92. package/src/runtime/subagent-manager.ts +80 -12
  93. package/src/runtime/task-display.ts +38 -38
  94. package/src/runtime/task-output-context.ts +127 -106
  95. package/src/runtime/task-runner/live-executor.ts +98 -98
  96. package/src/runtime/task-runner/progress.ts +111 -111
  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/team-runner.ts +1 -1
  100. package/src/runtime/worker-heartbeat.ts +21 -21
  101. package/src/runtime/worker-startup.ts +57 -57
  102. package/src/schema/config-schema.ts +21 -21
  103. package/src/schema/team-tool-schema.ts +100 -100
  104. package/src/state/artifact-store.ts +122 -108
  105. package/src/state/contracts.ts +105 -105
  106. package/src/state/jsonl-writer.ts +77 -77
  107. package/src/state/mailbox.ts +67 -22
  108. package/src/state/state-store.ts +36 -5
  109. package/src/state/task-claims.ts +42 -42
  110. package/src/state/usage.ts +29 -29
  111. package/src/subagents/async-entry.ts +1 -1
  112. package/src/subagents/index.ts +3 -3
  113. package/src/subagents/live/control.ts +1 -1
  114. package/src/subagents/live/manager.ts +1 -1
  115. package/src/subagents/live/realtime.ts +1 -1
  116. package/src/subagents/live/session-runtime.ts +1 -1
  117. package/src/subagents/manager.ts +1 -1
  118. package/src/subagents/spawn.ts +1 -1
  119. package/src/teams/discover-teams.ts +27 -5
  120. package/src/teams/team-serializer.ts +38 -36
  121. package/src/types/diff.d.ts +18 -18
  122. package/src/ui/crew-footer.ts +101 -101
  123. package/src/ui/crew-select-list.ts +111 -111
  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/mascot.ts +441 -441
  129. package/src/ui/render-diff.ts +119 -119
  130. package/src/ui/run-dashboard.ts +5 -2
  131. package/src/ui/run-snapshot-cache.ts +19 -8
  132. package/src/ui/spinner.ts +17 -17
  133. package/src/ui/status-colors.ts +54 -54
  134. package/src/ui/syntax-highlight.ts +116 -116
  135. package/src/ui/transcript-viewer.ts +15 -1
  136. package/src/utils/completion-dedupe.ts +63 -63
  137. package/src/utils/file-coalescer.ts +84 -84
  138. package/src/utils/frontmatter.ts +36 -36
  139. package/src/utils/fs-watch.ts +31 -31
  140. package/src/utils/git.ts +262 -262
  141. package/src/utils/ids.ts +12 -12
  142. package/src/utils/names.ts +26 -26
  143. package/src/utils/paths.ts +3 -2
  144. package/src/utils/safe-paths.ts +34 -0
  145. package/src/utils/sleep.ts +32 -32
  146. package/src/utils/timings.ts +31 -31
  147. package/src/utils/visual.ts +159 -159
  148. package/src/workflows/discover-workflows.ts +30 -3
  149. package/src/workflows/validate-workflow.ts +40 -40
  150. package/src/worktree/branch-freshness.ts +45 -45
  151. package/teams/default.team.md +12 -12
  152. package/teams/fast-fix.team.md +11 -11
  153. package/teams/implementation.team.md +18 -18
  154. package/teams/parallel-research.team.md +14 -14
  155. package/teams/research.team.md +11 -11
  156. package/teams/review.team.md +12 -12
  157. package/workflows/default.workflow.md +29 -29
  158. package/workflows/fast-fix.workflow.md +22 -22
  159. package/workflows/implementation.workflow.md +38 -38
  160. package/workflows/parallel-research.workflow.md +46 -46
  161. package/workflows/research.workflow.md +22 -22
  162. package/workflows/review.workflow.md +30 -30
@@ -1,39 +1,39 @@
1
- export type RolePermissionMode = "read_only" | "workspace_write" | "danger_full_access" | "explicit_confirm";
2
-
3
- const READ_ONLY_ROLES = new Set(["explorer", "reviewer", "security-reviewer", "verifier", "analyst", "critic", "planner", "writer"]);
4
- const WRITE_ROLES = new Set(["executor", "test-engineer"]);
5
- const READ_ONLY_COMMANDS = new Set(["cat", "head", "tail", "less", "more", "wc", "ls", "find", "grep", "rg", "awk", "sed", "echo", "printf", "which", "where", "whoami", "pwd", "env", "printenv", "date", "df", "du", "uname", "file", "stat", "diff", "sort", "uniq", "tr", "cut", "paste", "test", "true", "false", "type", "readlink", "realpath", "basename", "dirname", "sha256sum", "md5sum", "xxd", "hexdump", "od", "strings", "tree", "jq", "git", "gh"]);
6
-
7
- export interface PermissionCheckResult {
8
- allowed: boolean;
9
- mode: RolePermissionMode;
10
- reason?: string;
11
- }
12
-
13
- export function permissionForRole(role: string): RolePermissionMode {
14
- if (READ_ONLY_ROLES.has(role)) return "read_only";
15
- if (WRITE_ROLES.has(role)) return "workspace_write";
16
- return "workspace_write";
17
- }
18
-
19
- export function isReadOnlyCommand(command: string): boolean {
20
- const first = command.trim().split(/\s+/)[0]?.split(/[\\/]/).pop() ?? "";
21
- return READ_ONLY_COMMANDS.has(first) && !/\s(-i|--in-place)\b|\s>{1,2}\s|\brm\b|\bmv\b|\bcp\b|\bnpm\s+install\b|\bgit\s+(commit|push|merge|rebase|reset|checkout)\b/.test(command);
22
- }
23
-
24
- export function checkRolePermission(role: string, command: string): PermissionCheckResult {
25
- const mode = permissionForRole(role);
26
- if (mode === "read_only" && !isReadOnlyCommand(command)) return { allowed: false, mode, reason: `Role '${role}' is read-only and command may modify state.` };
27
- return { allowed: true, mode };
28
- }
29
-
30
- export function currentCrewRole(env: NodeJS.ProcessEnv = process.env): string | undefined {
31
- return env.PI_CREW_ROLE?.trim() || env.PI_TEAMS_ROLE?.trim() || undefined;
32
- }
33
-
34
- export function checkSubagentSpawnPermission(role: string | undefined): PermissionCheckResult {
35
- if (!role) return { allowed: true, mode: "workspace_write" };
36
- const mode = permissionForRole(role);
37
- if (mode === "read_only") return { allowed: false, mode, reason: `Role '${role}' is read-only and cannot spawn additional subagents.` };
38
- return { allowed: true, mode };
39
- }
1
+ export type RolePermissionMode = "read_only" | "workspace_write" | "danger_full_access" | "explicit_confirm";
2
+
3
+ const READ_ONLY_ROLES = new Set(["explorer", "reviewer", "security-reviewer", "verifier", "analyst", "critic", "planner", "writer"]);
4
+ const WRITE_ROLES = new Set(["executor", "test-engineer"]);
5
+ const READ_ONLY_COMMANDS = new Set(["cat", "head", "tail", "less", "more", "wc", "ls", "find", "grep", "rg", "awk", "sed", "echo", "printf", "which", "where", "whoami", "pwd", "env", "printenv", "date", "df", "du", "uname", "file", "stat", "diff", "sort", "uniq", "tr", "cut", "paste", "test", "true", "false", "type", "readlink", "realpath", "basename", "dirname", "sha256sum", "md5sum", "xxd", "hexdump", "od", "strings", "tree", "jq", "git", "gh"]);
6
+
7
+ export interface PermissionCheckResult {
8
+ allowed: boolean;
9
+ mode: RolePermissionMode;
10
+ reason?: string;
11
+ }
12
+
13
+ export function permissionForRole(role: string): RolePermissionMode {
14
+ if (READ_ONLY_ROLES.has(role)) return "read_only";
15
+ if (WRITE_ROLES.has(role)) return "workspace_write";
16
+ return "workspace_write";
17
+ }
18
+
19
+ export function isReadOnlyCommand(command: string): boolean {
20
+ const first = command.trim().split(/\s+/)[0]?.split(/[\\/]/).pop() ?? "";
21
+ return READ_ONLY_COMMANDS.has(first) && !/\s(-i|--in-place)\b|\s>{1,2}\s|\brm\b|\bmv\b|\bcp\b|\bnpm\s+install\b|\bgit\s+(commit|push|merge|rebase|reset|checkout)\b/.test(command);
22
+ }
23
+
24
+ export function checkRolePermission(role: string, command: string): PermissionCheckResult {
25
+ const mode = permissionForRole(role);
26
+ if (mode === "read_only" && !isReadOnlyCommand(command)) return { allowed: false, mode, reason: `Role '${role}' is read-only and command may modify state.` };
27
+ return { allowed: true, mode };
28
+ }
29
+
30
+ export function currentCrewRole(env: NodeJS.ProcessEnv = process.env): string | undefined {
31
+ return env.PI_CREW_ROLE?.trim() || env.PI_TEAMS_ROLE?.trim() || undefined;
32
+ }
33
+
34
+ export function checkSubagentSpawnPermission(role: string | undefined): PermissionCheckResult {
35
+ if (!role) return { allowed: true, mode: "workspace_write" };
36
+ const mode = permissionForRole(role);
37
+ if (mode === "read_only") return { allowed: false, mode, reason: `Role '${role}' is read-only and cannot spawn additional subagents.` };
38
+ return { allowed: true, mode };
39
+ }
@@ -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,28 +1,28 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
-
4
- export interface SidechainEntry {
5
- isSidechain: true;
6
- agentId: string;
7
- type: string;
8
- message: unknown;
9
- timestamp: string;
10
- cwd: string;
11
- }
12
-
13
- export function writeSidechainEntry(filePath: string, entry: Omit<SidechainEntry, "isSidechain" | "timestamp">): void {
14
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
15
- fs.appendFileSync(filePath, `${JSON.stringify({ isSidechain: true, timestamp: new Date().toISOString(), ...entry })}\n`, "utf-8");
16
- }
17
-
18
- export function sidechainOutputPath(stateRoot: string, taskId: string): string {
19
- return path.join(stateRoot, "agents", taskId, "sidechain.output.jsonl");
20
- }
21
-
22
- export function eventToSidechainType(event: unknown): string | undefined {
23
- if (!event || typeof event !== "object" || Array.isArray(event)) return undefined;
24
- const type = (event as { type?: unknown }).type;
25
- if (type === "message_start" || type === "message_update" || type === "message_end") return "message";
26
- if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") return "tool";
27
- return typeof type === "string" ? type : undefined;
28
- }
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ export interface SidechainEntry {
5
+ isSidechain: true;
6
+ agentId: string;
7
+ type: string;
8
+ message: unknown;
9
+ timestamp: string;
10
+ cwd: string;
11
+ }
12
+
13
+ export function writeSidechainEntry(filePath: string, entry: Omit<SidechainEntry, "isSidechain" | "timestamp">): void {
14
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
15
+ fs.appendFileSync(filePath, `${JSON.stringify({ isSidechain: true, timestamp: new Date().toISOString(), ...entry })}\n`, "utf-8");
16
+ }
17
+
18
+ export function sidechainOutputPath(stateRoot: string, taskId: string): string {
19
+ return path.join(stateRoot, "agents", taskId, "sidechain.output.jsonl");
20
+ }
21
+
22
+ export function eventToSidechainType(event: unknown): string | undefined {
23
+ if (!event || typeof event !== "object" || Array.isArray(event)) return undefined;
24
+ const type = (event as { type?: unknown }).type;
25
+ if (type === "message_start" || type === "message_update" || type === "message_end") return "message";
26
+ if (type === "tool_execution_start" || type === "tool_execution_update" || type === "tool_execution_end") return "tool";
27
+ return typeof type === "string" ? type : undefined;
28
+ }
@@ -108,6 +108,9 @@ function totalRunTurns(cwd: string, runId: string | undefined): number | undefin
108
108
 
109
109
  export class SubagentManager {
110
110
  private readonly records = new Map<string, SubagentRecord>();
111
+ private readonly cwdByRecord = new Map<string, string>();
112
+ private readonly controllers = new Map<string, AbortController>();
113
+ private readonly controllerCleanup = new Map<string, () => void>();
111
114
  private queue: QueuedSpawn[] = [];
112
115
  private runningBackground = 0;
113
116
  private counter = 0;
@@ -135,6 +138,7 @@ export class SubagentManager {
135
138
  background: options.background,
136
139
  };
137
140
  this.records.set(record.id, record);
141
+ this.cwdByRecord.set(record.id, options.cwd);
138
142
  savePersistedSubagentRecord(options.cwd, record);
139
143
  if (record.status === "queued") {
140
144
  this.queue.push({ record, options, runner, signal });
@@ -157,28 +161,26 @@ export class SubagentManager {
157
161
  if (!record) return false;
158
162
  if (record.status === "queued") {
159
163
  this.queue = this.queue.filter((entry) => entry.record.id !== id);
160
- record.status = "stopped";
161
- record.completedAt = Date.now();
164
+ this.markStopped(record);
162
165
  return true;
163
166
  }
164
167
  if (record.status !== "running" && record.status !== "blocked") return false;
165
- record.status = "stopped";
166
- record.completedAt = Date.now();
168
+ this.controllers.get(id)?.abort();
169
+ this.markStopped(record);
167
170
  return true;
168
171
  }
169
172
 
170
173
  abortAll(): number {
171
174
  let count = 0;
172
175
  for (const entry of this.queue) {
173
- entry.record.status = "stopped";
174
- entry.record.completedAt = Date.now();
176
+ this.markStopped(entry.record);
175
177
  count++;
176
178
  }
177
179
  this.queue = [];
178
180
  for (const record of this.records.values()) {
179
181
  if (record.status === "running" || record.status === "blocked") {
180
- record.status = "stopped";
181
- record.completedAt = Date.now();
182
+ this.controllers.get(record.id)?.abort();
183
+ this.markStopped(record);
182
184
  count++;
183
185
  }
184
186
  }
@@ -188,7 +190,7 @@ export class SubagentManager {
188
190
  async waitForAll(): Promise<void> {
189
191
  while (true) {
190
192
  this.drainQueue();
191
- const pending = this.listAgents().filter((record) => record.status === "running" || record.status === "blocked" || record.status === "queued").map((record) => record.promise).filter((promise): promise is Promise<void> => Boolean(promise));
193
+ const pending = this.listAgents().filter((record) => record.status === "running" || record.status === "queued").map((record) => record.promise).filter((promise): promise is Promise<void> => Boolean(promise));
192
194
  if (!pending.length) break;
193
195
  await Promise.allSettled(pending);
194
196
  }
@@ -198,7 +200,7 @@ export class SubagentManager {
198
200
  while (true) {
199
201
  const record = this.records.get(id);
200
202
  if (!record) return undefined;
201
- if (record.status !== "running" && record.status !== "blocked" && record.status !== "queued") return record;
203
+ if (record.status !== "running" && record.status !== "queued") return record;
202
204
  if (record.promise) await record.promise;
203
205
  else await new Promise((resolve) => setTimeout(resolve, 100));
204
206
  }
@@ -213,10 +215,13 @@ export class SubagentManager {
213
215
  if (options.background) this.runningBackground++;
214
216
  record.status = "running";
215
217
  record.startedAt = Date.now();
218
+ record.completedAt = undefined;
219
+ const runSignal = this.createRunSignal(record.id, signal);
216
220
  savePersistedSubagentRecord(options.cwd, record);
217
221
  record.promise = (async () => {
218
222
  try {
219
- const result = await runner(options, signal);
223
+ const result = await runner(options, runSignal);
224
+ if (record.status === "stopped") return;
220
225
  record.runId = detailsRunId(result);
221
226
  record.result = resultText(result);
222
227
  savePersistedSubagentRecord(options.cwd, record);
@@ -228,11 +233,16 @@ export class SubagentManager {
228
233
  if (record.runId) await this.pollRunToTerminal(options.cwd, record);
229
234
  else record.status = "completed";
230
235
  } catch (error) {
236
+ if (record.status === "stopped" || runSignal.aborted) {
237
+ record.status = "stopped";
238
+ return;
239
+ }
231
240
  record.status = "error";
232
241
  record.error = error instanceof Error ? error.message : String(error);
233
242
  } finally {
243
+ this.cleanupRunSignal(record.id);
234
244
  if (options.background) this.runningBackground = Math.max(0, this.runningBackground - 1);
235
- record.completedAt = record.completedAt ?? Date.now();
245
+ if (record.status !== "blocked") record.completedAt = record.completedAt ?? Date.now();
236
246
  savePersistedSubagentRecord(options.cwd, record);
237
247
  if (record.status === "completed" || record.status === "failed" || record.status === "cancelled" || record.status === "error" || record.status === "stopped") {
238
248
  // Phase 1.6: Populate telemetry fields
@@ -246,6 +256,34 @@ export class SubagentManager {
246
256
  })();
247
257
  }
248
258
 
259
+ private markStopped(record: SubagentRecord): void {
260
+ record.status = "stopped";
261
+ record.completedAt = Date.now();
262
+ const cwd = this.cwdByRecord.get(record.id);
263
+ if (cwd) savePersistedSubagentRecord(cwd, record);
264
+ }
265
+
266
+ private createRunSignal(id: string, signal?: AbortSignal): AbortSignal {
267
+ const controller = new AbortController();
268
+ this.controllers.set(id, controller);
269
+ if (signal?.aborted) {
270
+ controller.abort();
271
+ return controller.signal;
272
+ }
273
+ if (signal) {
274
+ const abort = (): void => controller.abort();
275
+ signal.addEventListener("abort", abort, { once: true });
276
+ this.controllerCleanup.set(id, () => signal.removeEventListener("abort", abort));
277
+ }
278
+ return controller.signal;
279
+ }
280
+
281
+ private cleanupRunSignal(id: string): void {
282
+ this.controllerCleanup.get(id)?.();
283
+ this.controllerCleanup.delete(id);
284
+ this.controllers.delete(id);
285
+ }
286
+
249
287
  private drainQueue(): void {
250
288
  while (this.queue.length > 0 && this.runningBackground < this.maxConcurrent) {
251
289
  const next = this.queue.shift();
@@ -286,6 +324,7 @@ export class SubagentManager {
286
324
  record.completedAt = undefined;
287
325
  this.onComplete?.(record);
288
326
  this.scheduleStuckBlockedNotify(cwd, record);
327
+ this.scheduleBlockedTerminalPoll(cwd, record);
289
328
  }
290
329
  savePersistedSubagentRecord(cwd, record);
291
330
  return;
@@ -294,6 +333,35 @@ export class SubagentManager {
294
333
  }
295
334
  }
296
335
 
336
+ private scheduleBlockedTerminalPoll(cwd: string, record: SubagentRecord): void {
337
+ const poll = (): void => {
338
+ const current = this.records.get(record.id);
339
+ if (!current || current.status !== "blocked" || !current.runId) return;
340
+ const loaded = loadRunManifestById(cwd, current.runId);
341
+ if (!loaded || loaded.manifest.status === "blocked" || loaded.manifest.status === "running" || loaded.manifest.status === "planning" || loaded.manifest.status === "queued") {
342
+ const timer = setTimeout(poll, this.pollIntervalMs);
343
+ timer.unref?.();
344
+ return;
345
+ }
346
+ const persisted = readPersistedSubagentRecord(cwd, current.id);
347
+ current.resultConsumed = current.resultConsumed || persisted?.resultConsumed;
348
+ if (loaded.manifest.status === "completed") {
349
+ current.status = "completed";
350
+ current.error = undefined;
351
+ } else if (loaded.manifest.status === "failed" || loaded.manifest.status === "cancelled") {
352
+ current.status = loaded.manifest.status;
353
+ current.error = loaded.manifest.summary;
354
+ } else return;
355
+ current.completedAt = Date.now();
356
+ current.turnCount = current.turnCount ?? totalRunTurns(cwd, current.runId);
357
+ current.durationMs = Math.max(0, current.completedAt - current.startedAt);
358
+ savePersistedSubagentRecord(cwd, current);
359
+ this.onComplete?.(current);
360
+ };
361
+ const timer = setTimeout(poll, this.pollIntervalMs);
362
+ timer.unref?.();
363
+ }
364
+
297
365
  private scheduleStuckBlockedNotify(cwd: string, record: SubagentRecord): void {
298
366
  const threshold = DEFAULT_SUBAGENT.stuckBlockedNotifyMs;
299
367
  const fire = (): void => {
@@ -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
+ }