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,99 +1,99 @@
1
- export interface RunnerSubagentStep {
2
- agent: string;
3
- task: string;
4
- cwd?: string;
5
- model?: string;
6
- modelCandidates?: string[];
7
- tools?: string[];
8
- extensions?: string[];
9
- mcpDirectTools?: string[];
10
- systemPrompt?: string | null;
11
- systemPromptMode?: "append" | "replace";
12
- inheritProjectContext: boolean;
13
- inheritSkills: boolean;
14
- skills?: string[];
15
- outputPath?: string;
16
- sessionFile?: string;
17
- maxSubagentDepth?: number;
18
- }
19
-
20
- export interface ParallelStepGroup {
21
- parallel: RunnerSubagentStep[];
22
- concurrency?: number;
23
- failFast?: boolean;
24
- worktree?: boolean;
25
- }
26
-
27
- export type RunnerStep = RunnerSubagentStep | ParallelStepGroup;
28
-
29
- export function isParallelGroup(step: RunnerStep): step is ParallelStepGroup {
30
- return "parallel" in step && Array.isArray(step.parallel);
31
- }
32
-
33
- export function flattenSteps(steps: RunnerStep[]): RunnerSubagentStep[] {
34
- const flat: RunnerSubagentStep[] = [];
35
- for (const step of steps) {
36
- if (isParallelGroup(step)) {
37
- for (const task of step.parallel) flat.push(task);
38
- } else {
39
- flat.push(step);
40
- }
41
- }
42
- return flat;
43
- }
44
-
45
- export async function mapConcurrent<T, R>(items: T[], limit: number, fn: (item: T, i: number) => Promise<R>): Promise<R[]> {
46
- const safeLimit = Math.max(1, Math.floor(limit) || 1);
47
- const results: R[] = new Array(items.length);
48
- let next = 0;
49
-
50
- const worker = async (_workerIndex: number): Promise<void> => {
51
- while (next < items.length) {
52
- const i = next++;
53
- results[i] = await fn(items[i], i);
54
- }
55
- };
56
-
57
- await Promise.all(Array.from({ length: Math.min(safeLimit, items.length) }, (_, workerIndex) => worker(workerIndex)));
58
- return results;
59
- }
60
-
61
- export interface ParallelTaskResult {
62
- agent: string;
63
- taskIndex?: number;
64
- output: string;
65
- exitCode: number | null;
66
- error?: string;
67
- model?: string;
68
- attemptedModels?: string[];
69
- outputTargetPath?: string;
70
- outputTargetExists?: boolean;
71
- }
72
-
73
- export function aggregateParallelOutputs(
74
- results: ParallelTaskResult[],
75
- headerFormat: (index: number, agent: string) => string = (i, agent) => `=== Parallel Task ${i + 1} (${agent}) ===`,
76
- ): string {
77
- return results
78
- .map((r, i) => {
79
- const header = headerFormat(r.taskIndex ?? i, r.agent);
80
- const hasOutput = Boolean(r.output?.trim());
81
- const status =
82
- r.exitCode === -1
83
- ? "SKIPPED"
84
- : r.exitCode !== 0 && r.exitCode !== null
85
- ? `FAILED (exit code ${r.exitCode})${r.error ? `: ${r.error}` : ""}`
86
- : r.error
87
- ? `WARNING: ${r.error}`
88
- : !hasOutput && r.outputTargetPath && r.outputTargetExists === false
89
- ? `EMPTY OUTPUT (expected output file missing: ${r.outputTargetPath})`
90
- : !hasOutput && !r.outputTargetPath
91
- ? "EMPTY OUTPUT (no textual response returned)"
92
- : "";
93
- const body = status ? (hasOutput ? `${status}\n${r.output}` : status) : r.output;
94
- return `${header}\n${body}`;
95
- })
96
- .join("\n\n");
97
- }
98
-
99
- export const MAX_PARALLEL_CONCURRENCY = 4;
1
+ export interface RunnerSubagentStep {
2
+ agent: string;
3
+ task: string;
4
+ cwd?: string;
5
+ model?: string;
6
+ modelCandidates?: string[];
7
+ tools?: string[];
8
+ extensions?: string[];
9
+ mcpDirectTools?: string[];
10
+ systemPrompt?: string | null;
11
+ systemPromptMode?: "append" | "replace";
12
+ inheritProjectContext: boolean;
13
+ inheritSkills: boolean;
14
+ skills?: string[];
15
+ outputPath?: string;
16
+ sessionFile?: string;
17
+ maxSubagentDepth?: number;
18
+ }
19
+
20
+ export interface ParallelStepGroup {
21
+ parallel: RunnerSubagentStep[];
22
+ concurrency?: number;
23
+ failFast?: boolean;
24
+ worktree?: boolean;
25
+ }
26
+
27
+ export type RunnerStep = RunnerSubagentStep | ParallelStepGroup;
28
+
29
+ export function isParallelGroup(step: RunnerStep): step is ParallelStepGroup {
30
+ return "parallel" in step && Array.isArray(step.parallel);
31
+ }
32
+
33
+ export function flattenSteps(steps: RunnerStep[]): RunnerSubagentStep[] {
34
+ const flat: RunnerSubagentStep[] = [];
35
+ for (const step of steps) {
36
+ if (isParallelGroup(step)) {
37
+ for (const task of step.parallel) flat.push(task);
38
+ } else {
39
+ flat.push(step);
40
+ }
41
+ }
42
+ return flat;
43
+ }
44
+
45
+ export async function mapConcurrent<T, R>(items: T[], limit: number, fn: (item: T, i: number) => Promise<R>): Promise<R[]> {
46
+ const safeLimit = Math.max(1, Math.floor(limit) || 1);
47
+ const results: R[] = new Array(items.length);
48
+ let next = 0;
49
+
50
+ const worker = async (_workerIndex: number): Promise<void> => {
51
+ while (next < items.length) {
52
+ const i = next++;
53
+ results[i] = await fn(items[i], i);
54
+ }
55
+ };
56
+
57
+ await Promise.all(Array.from({ length: Math.min(safeLimit, items.length) }, (_, workerIndex) => worker(workerIndex)));
58
+ return results;
59
+ }
60
+
61
+ export interface ParallelTaskResult {
62
+ agent: string;
63
+ taskIndex?: number;
64
+ output: string;
65
+ exitCode: number | null;
66
+ error?: string;
67
+ model?: string;
68
+ attemptedModels?: string[];
69
+ outputTargetPath?: string;
70
+ outputTargetExists?: boolean;
71
+ }
72
+
73
+ export function aggregateParallelOutputs(
74
+ results: ParallelTaskResult[],
75
+ headerFormat: (index: number, agent: string) => string = (i, agent) => `=== Parallel Task ${i + 1} (${agent}) ===`,
76
+ ): string {
77
+ return results
78
+ .map((r, i) => {
79
+ const header = headerFormat(r.taskIndex ?? i, r.agent);
80
+ const hasOutput = Boolean(r.output?.trim());
81
+ const status =
82
+ r.exitCode === -1
83
+ ? "SKIPPED"
84
+ : r.exitCode !== 0 && r.exitCode !== null
85
+ ? `FAILED (exit code ${r.exitCode})${r.error ? `: ${r.error}` : ""}`
86
+ : r.error
87
+ ? `WARNING: ${r.error}`
88
+ : !hasOutput && r.outputTargetPath && r.outputTargetExists === false
89
+ ? `EMPTY OUTPUT (expected output file missing: ${r.outputTargetPath})`
90
+ : !hasOutput && !r.outputTargetPath
91
+ ? "EMPTY OUTPUT (no textual response returned)"
92
+ : "";
93
+ const body = status ? (hasOutput ? `${status}\n${r.output}` : status) : r.output;
94
+ return `${header}\n${body}`;
95
+ })
96
+ .join("\n\n");
97
+ }
98
+
99
+ export const MAX_PARALLEL_CONCURRENCY = 4;
@@ -1,111 +1,111 @@
1
- export interface ParsedPiUsage {
2
- input?: number;
3
- output?: number;
4
- cacheRead?: number;
5
- cacheWrite?: number;
6
- cost?: number;
7
- turns?: number;
8
- }
9
-
10
- export interface ParsedPiJsonOutput {
11
- jsonEvents: number;
12
- textEvents: string[];
13
- finalText?: string;
14
- usage?: ParsedPiUsage;
15
- }
16
-
17
- function asRecord(value: unknown): Record<string, unknown> | undefined {
18
- return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
19
- }
20
-
21
- function numberField(obj: Record<string, unknown>, keys: string[]): number | undefined {
22
- for (const key of keys) {
23
- const value = obj[key];
24
- if (typeof value === "number" && Number.isFinite(value)) return value;
25
- }
26
- return undefined;
27
- }
28
-
29
- function mergeUsage(target: ParsedPiUsage, source: ParsedPiUsage): ParsedPiUsage {
30
- return {
31
- input: source.input ?? target.input,
32
- output: source.output ?? target.output,
33
- cacheRead: source.cacheRead ?? target.cacheRead,
34
- cacheWrite: source.cacheWrite ?? target.cacheWrite,
35
- cost: source.cost ?? target.cost,
36
- turns: source.turns ?? target.turns,
37
- };
38
- }
39
-
40
- function extractUsage(value: unknown): ParsedPiUsage | undefined {
41
- const obj = asRecord(value);
42
- if (!obj) return undefined;
43
- const direct: ParsedPiUsage = {
44
- input: numberField(obj, ["input", "inputTokens", "input_tokens"]),
45
- output: numberField(obj, ["output", "outputTokens", "output_tokens"]),
46
- cacheRead: numberField(obj, ["cacheRead", "cache_read", "cacheReadTokens", "cache_read_tokens"]),
47
- cacheWrite: numberField(obj, ["cacheWrite", "cache_write", "cacheWriteTokens", "cache_write_tokens"]),
48
- cost: numberField(obj, ["cost", "costUsd", "cost_usd"]),
49
- turns: numberField(obj, ["turns", "turnCount", "turn_count"]),
50
- };
51
- if (Object.values(direct).some((entry) => entry !== undefined)) return direct;
52
- for (const key of ["usage", "tokenUsage", "tokens", "stats"]) {
53
- const nested = extractUsage(obj[key]);
54
- if (nested) return nested;
55
- }
56
- return undefined;
57
- }
58
-
59
- function textFromContent(content: unknown): string[] {
60
- if (typeof content === "string") return [content];
61
- if (!Array.isArray(content)) return [];
62
- const text: string[] = [];
63
- for (const part of content) {
64
- const obj = asRecord(part);
65
- if (!obj) continue;
66
- if (obj.type === "text" && typeof obj.text === "string") text.push(obj.text);
67
- else if (typeof obj.content === "string") text.push(obj.content);
68
- }
69
- return text;
70
- }
71
-
72
- function extractText(value: unknown): string[] {
73
- const obj = asRecord(value);
74
- if (!obj) return [];
75
- const message = asRecord(obj.message);
76
- if (message?.role !== undefined && message.role !== "assistant") return [];
77
- const text: string[] = [];
78
- if (typeof obj.text === "string") text.push(obj.text);
79
- if (typeof obj.output === "string") text.push(obj.output);
80
- if (typeof obj.finalOutput === "string") text.push(obj.finalOutput);
81
- if (typeof obj.final_output === "string") text.push(obj.final_output);
82
- if (!message) text.push(...textFromContent(obj.content));
83
- if (message) text.push(...textFromContent(message.content));
84
- return text.filter((entry) => entry.trim().length > 0);
85
- }
86
-
87
- export function parsePiJsonOutput(stdout: string): ParsedPiJsonOutput {
88
- let jsonEvents = 0;
89
- const textEvents: string[] = [];
90
- let usage: ParsedPiUsage | undefined;
91
- for (const line of stdout.split("\n")) {
92
- const trimmed = line.trim();
93
- if (!trimmed) continue;
94
- let event: unknown;
95
- try {
96
- event = JSON.parse(trimmed) as unknown;
97
- } catch {
98
- continue;
99
- }
100
- jsonEvents++;
101
- textEvents.push(...extractText(event));
102
- const eventUsage = extractUsage(event);
103
- if (eventUsage) usage = mergeUsage(usage ?? {}, eventUsage);
104
- }
105
- return {
106
- jsonEvents,
107
- textEvents,
108
- finalText: textEvents.length > 0 ? textEvents[textEvents.length - 1] : undefined,
109
- usage,
110
- };
111
- }
1
+ export interface ParsedPiUsage {
2
+ input?: number;
3
+ output?: number;
4
+ cacheRead?: number;
5
+ cacheWrite?: number;
6
+ cost?: number;
7
+ turns?: number;
8
+ }
9
+
10
+ export interface ParsedPiJsonOutput {
11
+ jsonEvents: number;
12
+ textEvents: string[];
13
+ finalText?: string;
14
+ usage?: ParsedPiUsage;
15
+ }
16
+
17
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
18
+ return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
19
+ }
20
+
21
+ function numberField(obj: Record<string, unknown>, keys: string[]): number | undefined {
22
+ for (const key of keys) {
23
+ const value = obj[key];
24
+ if (typeof value === "number" && Number.isFinite(value)) return value;
25
+ }
26
+ return undefined;
27
+ }
28
+
29
+ function mergeUsage(target: ParsedPiUsage, source: ParsedPiUsage): ParsedPiUsage {
30
+ return {
31
+ input: source.input ?? target.input,
32
+ output: source.output ?? target.output,
33
+ cacheRead: source.cacheRead ?? target.cacheRead,
34
+ cacheWrite: source.cacheWrite ?? target.cacheWrite,
35
+ cost: source.cost ?? target.cost,
36
+ turns: source.turns ?? target.turns,
37
+ };
38
+ }
39
+
40
+ function extractUsage(value: unknown): ParsedPiUsage | undefined {
41
+ const obj = asRecord(value);
42
+ if (!obj) return undefined;
43
+ const direct: ParsedPiUsage = {
44
+ input: numberField(obj, ["input", "inputTokens", "input_tokens"]),
45
+ output: numberField(obj, ["output", "outputTokens", "output_tokens"]),
46
+ cacheRead: numberField(obj, ["cacheRead", "cache_read", "cacheReadTokens", "cache_read_tokens"]),
47
+ cacheWrite: numberField(obj, ["cacheWrite", "cache_write", "cacheWriteTokens", "cache_write_tokens"]),
48
+ cost: numberField(obj, ["cost", "costUsd", "cost_usd"]),
49
+ turns: numberField(obj, ["turns", "turnCount", "turn_count"]),
50
+ };
51
+ if (Object.values(direct).some((entry) => entry !== undefined)) return direct;
52
+ for (const key of ["usage", "tokenUsage", "tokens", "stats"]) {
53
+ const nested = extractUsage(obj[key]);
54
+ if (nested) return nested;
55
+ }
56
+ return undefined;
57
+ }
58
+
59
+ function textFromContent(content: unknown): string[] {
60
+ if (typeof content === "string") return [content];
61
+ if (!Array.isArray(content)) return [];
62
+ const text: string[] = [];
63
+ for (const part of content) {
64
+ const obj = asRecord(part);
65
+ if (!obj) continue;
66
+ if (obj.type === "text" && typeof obj.text === "string") text.push(obj.text);
67
+ else if (typeof obj.content === "string") text.push(obj.content);
68
+ }
69
+ return text;
70
+ }
71
+
72
+ function extractText(value: unknown): string[] {
73
+ const obj = asRecord(value);
74
+ if (!obj) return [];
75
+ const message = asRecord(obj.message);
76
+ if (message?.role !== undefined && message.role !== "assistant") return [];
77
+ const text: string[] = [];
78
+ if (typeof obj.text === "string") text.push(obj.text);
79
+ if (typeof obj.output === "string") text.push(obj.output);
80
+ if (typeof obj.finalOutput === "string") text.push(obj.finalOutput);
81
+ if (typeof obj.final_output === "string") text.push(obj.final_output);
82
+ if (!message) text.push(...textFromContent(obj.content));
83
+ if (message) text.push(...textFromContent(message.content));
84
+ return text.filter((entry) => entry.trim().length > 0);
85
+ }
86
+
87
+ export function parsePiJsonOutput(stdout: string): ParsedPiJsonOutput {
88
+ let jsonEvents = 0;
89
+ const textEvents: string[] = [];
90
+ let usage: ParsedPiUsage | undefined;
91
+ for (const line of stdout.split("\n")) {
92
+ const trimmed = line.trim();
93
+ if (!trimmed) continue;
94
+ let event: unknown;
95
+ try {
96
+ event = JSON.parse(trimmed) as unknown;
97
+ } catch {
98
+ continue;
99
+ }
100
+ jsonEvents++;
101
+ textEvents.push(...extractText(event));
102
+ const eventUsage = extractUsage(event);
103
+ if (eventUsage) usage = mergeUsage(usage ?? {}, eventUsage);
104
+ }
105
+ return {
106
+ jsonEvents,
107
+ textEvents,
108
+ finalText: textEvents.length > 0 ? textEvents[textEvents.length - 1] : undefined,
109
+ usage,
110
+ };
111
+ }
@@ -1,78 +1,78 @@
1
- import type { CrewLimitsConfig } from "../config/config.ts";
2
- import type { PolicyDecision, PolicyDecisionAction, PolicyDecisionReason, TeamRunManifest, TeamTaskState } from "../state/types.ts";
3
- import { evaluateGreenContract } from "./green-contract.ts";
4
- import { isWorkerHeartbeatStale } from "./worker-heartbeat.ts";
5
-
6
- export interface PolicyEngineInput {
7
- manifest: TeamRunManifest;
8
- tasks: TeamTaskState[];
9
- limits?: CrewLimitsConfig;
10
- now?: Date;
11
- }
12
-
13
- function decision(action: PolicyDecisionAction, reason: PolicyDecisionReason, message: string, taskId?: string): PolicyDecision {
14
- return {
15
- action,
16
- reason,
17
- message,
18
- taskId,
19
- createdAt: new Date().toISOString(),
20
- };
21
- }
22
-
23
- function taskDepth(task: TeamTaskState, tasksById: Map<string, TeamTaskState>): number {
24
- let depth = 0;
25
- let current = task.graph?.parentId;
26
- const seen = new Set<string>();
27
- while (current && !seen.has(current)) {
28
- seen.add(current);
29
- depth += 1;
30
- current = tasksById.get(current)?.graph?.parentId;
31
- }
32
- return depth;
33
- }
34
-
35
- export function evaluateCrewPolicy(input: PolicyEngineInput): PolicyDecision[] {
36
- const decisions: PolicyDecision[] = [];
37
- const maxTasksPerRun = input.limits?.maxTasksPerRun;
38
- if (maxTasksPerRun !== undefined && input.tasks.length > maxTasksPerRun) {
39
- decisions.push(decision("block", "limit_exceeded", `Run has ${input.tasks.length} tasks, exceeding maxTasksPerRun=${maxTasksPerRun}.`));
40
- }
41
- const runningCount = input.tasks.filter((task) => task.status === "running").length;
42
- if (input.limits?.maxConcurrentWorkers !== undefined && runningCount > input.limits.maxConcurrentWorkers) {
43
- decisions.push(decision("block", "limit_exceeded", `Run has ${runningCount} running workers, exceeding maxConcurrentWorkers=${input.limits.maxConcurrentWorkers}.`));
44
- }
45
- const tasksById = new Map(input.tasks.map((task) => [task.id, task]));
46
-
47
- for (const task of input.tasks) {
48
- if (input.limits?.maxChildrenPerTask !== undefined && (task.graph?.children.length ?? 0) > input.limits.maxChildrenPerTask) {
49
- decisions.push(decision("block", "limit_exceeded", `Task has ${task.graph?.children.length ?? 0} children, exceeding maxChildrenPerTask=${input.limits.maxChildrenPerTask}.`, task.id));
50
- }
51
- if (input.limits?.maxTaskDepth !== undefined && taskDepth(task, tasksById) > input.limits.maxTaskDepth) {
52
- decisions.push(decision("block", "limit_exceeded", `Task graph depth exceeds maxTaskDepth=${input.limits.maxTaskDepth}.`, task.id));
53
- }
54
- if (task.status === "failed") {
55
- const retryCount = task.policy?.retryCount ?? 0;
56
- const maxRetries = input.limits?.maxRetriesPerTask ?? 0;
57
- decisions.push(decision(retryCount < maxRetries ? "retry" : "escalate", "task_failed", task.error ? `Task failed: ${task.error}` : "Task failed.", task.id));
58
- }
59
- if ((task.status === "running" || task.status === "queued") && task.heartbeat && task.heartbeat.alive !== false && isWorkerHeartbeatStale(task.heartbeat, input.limits?.heartbeatStaleMs ?? 60_000, input.now)) {
60
- decisions.push(decision("escalate", "worker_stale", "Worker heartbeat is stale.", task.id));
61
- }
62
- if (task.taskPacket?.verification) {
63
- const outcome = evaluateGreenContract(task.taskPacket.verification, task.verification);
64
- if (!outcome.satisfied && task.status === "completed") {
65
- decisions.push(decision("block", "green_unsatisfied", `Green contract unsatisfied: required=${outcome.requiredGreenLevel}, observed=${outcome.observedGreenLevel}.`, task.id));
66
- }
67
- }
68
- }
69
-
70
- if (decisions.length === 0 && input.tasks.length > 0 && input.tasks.every((task) => task.status === "completed")) {
71
- decisions.push(decision("closeout", "run_complete", "All tasks completed and no policy blockers were found."));
72
- }
73
- return decisions;
74
- }
75
-
76
- export function summarizePolicyDecisions(decisions: PolicyDecision[]): string[] {
77
- return decisions.map((item) => `- ${item.action} (${item.reason})${item.taskId ? ` ${item.taskId}` : ""}: ${item.message}`);
78
- }
1
+ import type { CrewLimitsConfig } from "../config/config.ts";
2
+ import type { PolicyDecision, PolicyDecisionAction, PolicyDecisionReason, TeamRunManifest, TeamTaskState } from "../state/types.ts";
3
+ import { evaluateGreenContract } from "./green-contract.ts";
4
+ import { isWorkerHeartbeatStale } from "./worker-heartbeat.ts";
5
+
6
+ export interface PolicyEngineInput {
7
+ manifest: TeamRunManifest;
8
+ tasks: TeamTaskState[];
9
+ limits?: CrewLimitsConfig;
10
+ now?: Date;
11
+ }
12
+
13
+ function decision(action: PolicyDecisionAction, reason: PolicyDecisionReason, message: string, taskId?: string): PolicyDecision {
14
+ return {
15
+ action,
16
+ reason,
17
+ message,
18
+ taskId,
19
+ createdAt: new Date().toISOString(),
20
+ };
21
+ }
22
+
23
+ function taskDepth(task: TeamTaskState, tasksById: Map<string, TeamTaskState>): number {
24
+ let depth = 0;
25
+ let current = task.graph?.parentId;
26
+ const seen = new Set<string>();
27
+ while (current && !seen.has(current)) {
28
+ seen.add(current);
29
+ depth += 1;
30
+ current = tasksById.get(current)?.graph?.parentId;
31
+ }
32
+ return depth;
33
+ }
34
+
35
+ export function evaluateCrewPolicy(input: PolicyEngineInput): PolicyDecision[] {
36
+ const decisions: PolicyDecision[] = [];
37
+ const maxTasksPerRun = input.limits?.maxTasksPerRun;
38
+ if (maxTasksPerRun !== undefined && input.tasks.length > maxTasksPerRun) {
39
+ decisions.push(decision("block", "limit_exceeded", `Run has ${input.tasks.length} tasks, exceeding maxTasksPerRun=${maxTasksPerRun}.`));
40
+ }
41
+ const runningCount = input.tasks.filter((task) => task.status === "running").length;
42
+ if (input.limits?.maxConcurrentWorkers !== undefined && runningCount > input.limits.maxConcurrentWorkers) {
43
+ decisions.push(decision("block", "limit_exceeded", `Run has ${runningCount} running workers, exceeding maxConcurrentWorkers=${input.limits.maxConcurrentWorkers}.`));
44
+ }
45
+ const tasksById = new Map(input.tasks.map((task) => [task.id, task]));
46
+
47
+ for (const task of input.tasks) {
48
+ if (input.limits?.maxChildrenPerTask !== undefined && (task.graph?.children.length ?? 0) > input.limits.maxChildrenPerTask) {
49
+ decisions.push(decision("block", "limit_exceeded", `Task has ${task.graph?.children.length ?? 0} children, exceeding maxChildrenPerTask=${input.limits.maxChildrenPerTask}.`, task.id));
50
+ }
51
+ if (input.limits?.maxTaskDepth !== undefined && taskDepth(task, tasksById) > input.limits.maxTaskDepth) {
52
+ decisions.push(decision("block", "limit_exceeded", `Task graph depth exceeds maxTaskDepth=${input.limits.maxTaskDepth}.`, task.id));
53
+ }
54
+ if (task.status === "failed") {
55
+ const retryCount = task.policy?.retryCount ?? 0;
56
+ const maxRetries = input.limits?.maxRetriesPerTask ?? 0;
57
+ decisions.push(decision(retryCount < maxRetries ? "retry" : "escalate", "task_failed", task.error ? `Task failed: ${task.error}` : "Task failed.", task.id));
58
+ }
59
+ if ((task.status === "running" || task.status === "queued") && task.heartbeat && task.heartbeat.alive !== false && isWorkerHeartbeatStale(task.heartbeat, input.limits?.heartbeatStaleMs ?? 60_000, input.now)) {
60
+ decisions.push(decision("escalate", "worker_stale", "Worker heartbeat is stale.", task.id));
61
+ }
62
+ if (task.taskPacket?.verification) {
63
+ const outcome = evaluateGreenContract(task.taskPacket.verification, task.verification);
64
+ if (!outcome.satisfied && task.status === "completed") {
65
+ decisions.push(decision("block", "green_unsatisfied", `Green contract unsatisfied: required=${outcome.requiredGreenLevel}, observed=${outcome.observedGreenLevel}.`, task.id));
66
+ }
67
+ }
68
+ }
69
+
70
+ if (decisions.length === 0 && input.tasks.length > 0 && input.tasks.every((task) => task.status === "completed")) {
71
+ decisions.push(decision("closeout", "run_complete", "All tasks completed and no policy blockers were found."));
72
+ }
73
+ return decisions;
74
+ }
75
+
76
+ export function summarizePolicyDecisions(decisions: PolicyDecision[]): string[] {
77
+ return decisions.map((item) => `- ${item.action} (${item.reason})${item.taskId ? ` ${item.taskId}` : ""}: ${item.message}`);
78
+ }