pi-crew 0.1.45 → 0.1.49

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 (178) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +5 -5
  3. package/agents/analyst.md +11 -11
  4. package/agents/critic.md +11 -11
  5. package/agents/executor.md +11 -11
  6. package/agents/explorer.md +11 -11
  7. package/agents/planner.md +11 -11
  8. package/agents/reviewer.md +11 -11
  9. package/agents/security-reviewer.md +11 -11
  10. package/agents/test-engineer.md +11 -11
  11. package/agents/verifier.md +11 -11
  12. package/agents/writer.md +11 -11
  13. package/docs/next-upgrade-roadmap.md +808 -0
  14. package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +261 -0
  15. package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +111 -0
  16. package/docs/research/AUDIT_OH_MY_PI.md +261 -0
  17. package/docs/research/AUDIT_PI_CREW.md +457 -0
  18. package/docs/research/CAVEMAN-DEEP-RESEARCH.md +281 -0
  19. package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +264 -0
  20. package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +343 -0
  21. package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +480 -0
  22. package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +354 -0
  23. package/docs/research/IMPLEMENTATION_PLAN.md +385 -0
  24. package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +502 -0
  25. package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +266 -0
  26. package/docs/research/REMAINING-GAPS-PLAN.md +363 -0
  27. package/docs/research/SESSION-SUMMARY-2026-05-08.md +146 -0
  28. package/docs/research/UI-RESPONSIVENESS-AUDIT.md +173 -0
  29. package/docs/research-awesome-agent-skills-distillation.md +100 -0
  30. package/docs/research-oh-my-pi-distillation.md +369 -0
  31. package/docs/source-runtime-refactor-map.md +24 -0
  32. package/docs/usage.md +3 -3
  33. package/install.mjs +52 -8
  34. package/package.json +99 -98
  35. package/schema.json +10 -1
  36. package/skills/async-worker-recovery/SKILL.md +42 -0
  37. package/skills/context-artifact-hygiene/SKILL.md +52 -0
  38. package/skills/delegation-patterns/SKILL.md +54 -0
  39. package/skills/mailbox-interactive/SKILL.md +40 -0
  40. package/skills/model-routing-context/SKILL.md +39 -0
  41. package/skills/multi-perspective-review/SKILL.md +58 -0
  42. package/skills/observability-reliability/SKILL.md +41 -0
  43. package/skills/orchestration/SKILL.md +157 -0
  44. package/skills/ownership-session-security/SKILL.md +41 -0
  45. package/skills/pi-extension-lifecycle/SKILL.md +39 -0
  46. package/skills/requirements-to-task-packet/SKILL.md +63 -0
  47. package/skills/resource-discovery-config/SKILL.md +41 -0
  48. package/skills/runtime-state-reader/SKILL.md +44 -0
  49. package/skills/secure-agent-orchestration-review/SKILL.md +45 -0
  50. package/skills/state-mutation-locking/SKILL.md +42 -0
  51. package/skills/systematic-debugging/SKILL.md +67 -0
  52. package/skills/ui-render-performance/SKILL.md +39 -0
  53. package/skills/verification-before-done/SKILL.md +57 -0
  54. package/skills/worktree-isolation/SKILL.md +39 -0
  55. package/src/agents/agent-config.ts +6 -0
  56. package/src/agents/agent-search.ts +98 -0
  57. package/src/agents/agent-serializer.ts +38 -34
  58. package/src/agents/discover-agents.ts +29 -15
  59. package/src/config/config.ts +72 -24
  60. package/src/config/defaults.ts +25 -0
  61. package/src/extension/autonomous-policy.ts +26 -33
  62. package/src/extension/help.ts +1 -0
  63. package/src/extension/management.ts +5 -0
  64. package/src/extension/project-init.ts +62 -2
  65. package/src/extension/register.ts +69 -22
  66. package/src/extension/registration/commands.ts +64 -25
  67. package/src/extension/registration/compaction-guard.ts +1 -1
  68. package/src/extension/registration/subagent-helpers.ts +8 -0
  69. package/src/extension/registration/subagent-tools.ts +149 -148
  70. package/src/extension/registration/team-tool.ts +14 -10
  71. package/src/extension/run-index.ts +35 -21
  72. package/src/extension/run-maintenance.ts +30 -5
  73. package/src/extension/team-tool/api.ts +47 -9
  74. package/src/extension/team-tool/cancel.ts +109 -5
  75. package/src/extension/team-tool/context.ts +8 -0
  76. package/src/extension/team-tool/intent-policy.ts +42 -0
  77. package/src/extension/team-tool/lifecycle-actions.ts +120 -79
  78. package/src/extension/team-tool/parallel-dispatch.ts +156 -0
  79. package/src/extension/team-tool/respond.ts +46 -18
  80. package/src/extension/team-tool/run.ts +55 -12
  81. package/src/extension/team-tool/status.ts +13 -2
  82. package/src/extension/team-tool-types.ts +3 -0
  83. package/src/extension/team-tool.ts +45 -14
  84. package/src/hooks/registry.ts +61 -0
  85. package/src/hooks/types.ts +41 -0
  86. package/src/observability/event-to-metric.ts +8 -1
  87. package/src/runtime/agent-control.ts +169 -63
  88. package/src/runtime/async-runner.ts +3 -1
  89. package/src/runtime/background-runner.ts +78 -53
  90. package/src/runtime/cancellation-token.ts +89 -0
  91. package/src/runtime/cancellation.ts +61 -0
  92. package/src/runtime/capability-inventory.ts +116 -0
  93. package/src/runtime/child-pi.ts +458 -444
  94. package/src/runtime/code-summary.ts +247 -0
  95. package/src/runtime/crash-recovery.ts +182 -0
  96. package/src/runtime/crew-agent-records.ts +70 -10
  97. package/src/runtime/crew-agent-runtime.ts +1 -0
  98. package/src/runtime/custom-tools/irc-tool.ts +201 -0
  99. package/src/runtime/custom-tools/submit-result-tool.ts +90 -0
  100. package/src/runtime/deadletter.ts +1 -0
  101. package/src/runtime/delivery-coordinator.ts +48 -25
  102. package/src/runtime/effectiveness.ts +81 -0
  103. package/src/runtime/event-stream-bridge.ts +90 -0
  104. package/src/runtime/live-agent-control.ts +2 -1
  105. package/src/runtime/live-agent-manager.ts +179 -85
  106. package/src/runtime/live-control-realtime.ts +1 -1
  107. package/src/runtime/live-extension-bridge.ts +150 -0
  108. package/src/runtime/live-irc.ts +92 -0
  109. package/src/runtime/live-session-health.ts +100 -0
  110. package/src/runtime/live-session-runtime.ts +599 -305
  111. package/src/runtime/manifest-cache.ts +17 -2
  112. package/src/runtime/mcp-proxy.ts +113 -0
  113. package/src/runtime/model-fallback.ts +6 -4
  114. package/src/runtime/notebook-helpers.ts +90 -0
  115. package/src/runtime/orphan-sentinel.ts +7 -0
  116. package/src/runtime/output-validator.ts +187 -0
  117. package/src/runtime/parallel-utils.ts +57 -0
  118. package/src/runtime/parent-guard.ts +80 -0
  119. package/src/runtime/pi-args.ts +18 -3
  120. package/src/runtime/process-status.ts +5 -1
  121. package/src/runtime/prose-compressor.ts +164 -0
  122. package/src/runtime/result-extractor.ts +121 -0
  123. package/src/runtime/retry-executor.ts +81 -64
  124. package/src/runtime/runtime-resolver.ts +23 -10
  125. package/src/runtime/semaphore.ts +131 -0
  126. package/src/runtime/sensitive-paths.ts +92 -0
  127. package/src/runtime/skill-instructions.ts +222 -0
  128. package/src/runtime/stale-reconciler.ts +4 -14
  129. package/src/runtime/stream-preview.ts +177 -0
  130. package/src/runtime/subagent-manager.ts +6 -2
  131. package/src/runtime/subprocess-tool-registry.ts +67 -0
  132. package/src/runtime/task-output-context.ts +177 -127
  133. package/src/runtime/task-runner/capabilities.ts +78 -0
  134. package/src/runtime/task-runner/live-executor.ts +107 -101
  135. package/src/runtime/task-runner/prompt-builder.ts +72 -8
  136. package/src/runtime/task-runner/prompt-pipeline.ts +64 -0
  137. package/src/runtime/task-runner/run-projection.ts +104 -0
  138. package/src/runtime/task-runner.ts +115 -5
  139. package/src/runtime/team-runner.ts +134 -19
  140. package/src/runtime/workspace-tree.ts +298 -0
  141. package/src/runtime/yield-handler.ts +189 -0
  142. package/src/schema/config-schema.ts +7 -0
  143. package/src/schema/team-tool-schema.ts +14 -4
  144. package/src/skills/discover-skills.ts +67 -0
  145. package/src/state/active-run-registry.ts +167 -0
  146. package/src/state/artifact-store.ts +4 -1
  147. package/src/state/atomic-write.ts +50 -1
  148. package/src/state/blob-store.ts +117 -0
  149. package/src/state/contracts.ts +2 -1
  150. package/src/state/event-log-rotation.ts +158 -0
  151. package/src/state/event-log.ts +52 -2
  152. package/src/state/mailbox.ts +129 -9
  153. package/src/state/state-store.ts +32 -5
  154. package/src/state/types.ts +64 -2
  155. package/src/teams/team-config.ts +1 -0
  156. package/src/ui/agent-management-overlay.ts +144 -0
  157. package/src/ui/crew-widget.ts +15 -5
  158. package/src/ui/dashboard-panes/cancellation-pane.ts +43 -0
  159. package/src/ui/dashboard-panes/capability-pane.ts +60 -0
  160. package/src/ui/dashboard-panes/mailbox-pane.ts +35 -11
  161. package/src/ui/dashboard-panes/progress-pane.ts +2 -0
  162. package/src/ui/live-run-sidebar.ts +4 -0
  163. package/src/ui/powerbar-publisher.ts +77 -15
  164. package/src/ui/render-coalescer.ts +51 -0
  165. package/src/ui/run-dashboard.ts +4 -0
  166. package/src/ui/run-event-bus.ts +209 -0
  167. package/src/ui/run-snapshot-cache.ts +78 -18
  168. package/src/ui/snapshot-types.ts +10 -0
  169. package/src/ui/transcript-entries.ts +258 -0
  170. package/src/utils/ids.ts +5 -0
  171. package/src/utils/incremental-reader.ts +104 -0
  172. package/src/utils/paths.ts +4 -2
  173. package/src/utils/scan-cache.ts +137 -0
  174. package/src/utils/sse-parser.ts +134 -0
  175. package/src/utils/task-name-generator.ts +337 -0
  176. package/src/utils/visual.ts +33 -2
  177. package/src/workflows/workflow-config.ts +1 -0
  178. package/src/worktree/cleanup.ts +2 -1
@@ -51,6 +51,10 @@ export function hasStaleAsyncProcess(run: TeamRunManifest): boolean {
51
51
 
52
52
  export function isDisplayActiveRun(run: TeamRunManifest, agents: CrewAgentRecord[] = [], now = Date.now()): boolean {
53
53
  if (!isActiveRunStatus(run.status) || hasStaleAsyncProcess(run) || isLikelyOrphanedActiveRun(run, agents, now)) return false;
54
- if (agents.length === 0) return true;
54
+ // Keep the always-visible widget quiet until a worker actually exists.
55
+ // Empty active manifests can be created briefly at startup, by old fixture/scaffold
56
+ // runs, or from cross-cwd registry history; showing them causes noisy 0/0 rows and
57
+ // needless spinner redraws. The full dashboard can still list historical runs.
58
+ if (agents.length === 0) return false;
55
59
  return agents.some(hasDurableActiveAgentEvidence);
56
60
  }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Pure TypeScript prose compression module.
3
+ *
4
+ * Inspired by caveman's compress.js — reduces tool descriptions and other
5
+ * prose by removing filler words, pleasantries, hedging, and articles while
6
+ * preserving code, URLs, paths, identifiers, and other protected segments
7
+ * byte-for-byte.
8
+ *
9
+ * No external dependencies. No `any` types. Regex-only pattern matching.
10
+ */
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Protected segment patterns (order matters — earlier patterns take priority)
14
+ // ---------------------------------------------------------------------------
15
+
16
+ const PROTECTED_PATTERNS: readonly RegExp[] = [
17
+ /```[\s\S]*?```/g, // fenced code blocks
18
+ /`[^`\n]+`/g, // inline code
19
+ /\bhttps?:\/\/\S+/gi, // URLs
20
+ /\b[\w.-]*[\/\\][\w.\/\\\-]+/g, // paths with / or \
21
+ /\b[A-Z][A-Z0-9]*(?:_[A-Z][A-Z0-9]*)+\b/g, // CONSTANT_CASE
22
+ /\b\w+(?:\.\w+)+\(\)/g, // dotted.method() calls
23
+ /[A-Za-z_][A-Za-z0-9_]*\s*\([^)]*\)/g, // function calls: name(args)
24
+ /\b\d+\.\d+\.\d+\b/g, // version numbers x.y.z
25
+ ];
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Compression patterns (applied to unprotected prose only)
29
+ // ---------------------------------------------------------------------------
30
+
31
+ const FILLERS = /\b(?:just|really|basically|actually|simply|quite|very|essentially|literally)\b/gi;
32
+
33
+ const PLEASANTRIES = /\b(?:please|kindly|thank you|thanks|sure|certainly|of course|happy to)\b[,.]?\s*/gi;
34
+
35
+ const HEDGES = /\b(?:perhaps|maybe|might|could potentially|would like to|i think|in my opinion)\b\s*/gi;
36
+
37
+ const LEADERS = /^(?:i'?\s*ll|i will|i can|you can|we will|we can|let me|let'?\s*s)\s+/gim;
38
+
39
+ const ARTICLES = /\b(?:a|an|the)\s+(?=[a-z])/gi;
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Sentinel helpers
43
+ // ---------------------------------------------------------------------------
44
+
45
+ const SENTINEL_OPEN = "\x00";
46
+ const SENTINEL_CLOSE = "\x00";
47
+ const SENTINEL_RE = /\x00(\d+)\x00/g;
48
+
49
+ interface SegmentStore {
50
+ segments: string[];
51
+ }
52
+
53
+ function extractProtected(text: string, store: SegmentStore): string {
54
+ let working = text;
55
+ for (const re of PROTECTED_PATTERNS) {
56
+ // Reset lastIndex for reused RegExp objects
57
+ re.lastIndex = 0;
58
+ working = working.replace(re, (match: string): string => {
59
+ const index = store.segments.length;
60
+ store.segments.push(match);
61
+ return `${SENTINEL_OPEN}${index}${SENTINEL_CLOSE}`;
62
+ });
63
+ }
64
+ return working;
65
+ }
66
+
67
+ function restoreProtected(text: string, store: SegmentStore): string {
68
+ return text.replace(SENTINEL_RE, (_match: string, indexStr: string): string => {
69
+ const index = Number(indexStr);
70
+ return store.segments[index] ?? "";
71
+ });
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Core prose compression
76
+ // ---------------------------------------------------------------------------
77
+
78
+ function compressProseText(text: string): string {
79
+ let s = text;
80
+
81
+ // Remove leading "I'll / I will / you can / ..." sentence openers
82
+ s = s.replace(LEADERS, "");
83
+
84
+ // Remove pleasantries
85
+ s = s.replace(PLEASANTRIES, "");
86
+
87
+ // Remove hedging phrases
88
+ s = s.replace(HEDGES, "");
89
+
90
+ // Remove filler adverbs
91
+ s = s.replace(FILLERS, "");
92
+
93
+ // Remove articles before lowercase words
94
+ s = s.replace(ARTICLES, "");
95
+
96
+ // Collapse whitespace introduced by removals
97
+ s = s.replace(/[ \t]{2,}/g, " ");
98
+
99
+ // Fix punctuation spacing (e.g. "word ," → "word,")
100
+ s = s.replace(/\s+([,.;:!?])/g, "$1");
101
+
102
+ // Collapse excessive newlines
103
+ s = s.replace(/\n{3,}/g, "\n\n");
104
+
105
+ // Re-capitalize first letter of each sentence after removals
106
+ s = s.replace(/(^|[.!?]\s+)([a-z])/g, (_match: string, pre: string, ch: string): string => {
107
+ return pre + ch.toUpperCase();
108
+ });
109
+
110
+ return s.trim();
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Public API
115
+ // ---------------------------------------------------------------------------
116
+
117
+ export interface CompressResult {
118
+ compressed: string;
119
+ originalLength: number;
120
+ compressedLength: number;
121
+ savingsPercent: number;
122
+ }
123
+
124
+ /**
125
+ * Compress prose text by removing filler, pleasantries, hedging, and articles
126
+ * while preserving code blocks, URLs, paths, identifiers, and other protected
127
+ * segments byte-for-byte.
128
+ */
129
+ export function compressProse(text: string): CompressResult {
130
+ if (typeof text !== "string" || text.length === 0) {
131
+ return {
132
+ compressed: "",
133
+ originalLength: 0,
134
+ compressedLength: 0,
135
+ savingsPercent: 0,
136
+ };
137
+ }
138
+
139
+ const store: SegmentStore = { segments: [] };
140
+ const extracted = extractProtected(text, store);
141
+ const compressed = compressProseText(extracted);
142
+ const restored = restoreProtected(compressed, store);
143
+
144
+ const originalLength = text.length;
145
+ const compressedLength = restored.length;
146
+ const savingsPercent = originalLength > 0
147
+ ? Math.round(((originalLength - compressedLength) / originalLength) * 100 * 100) / 100
148
+ : 0;
149
+
150
+ return {
151
+ compressed: restored,
152
+ originalLength,
153
+ compressedLength,
154
+ savingsPercent,
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Convenience wrapper — returns just the compressed string for tool
160
+ * descriptions and other single-field use cases.
161
+ */
162
+ export function compressToolDescription(description: string): string {
163
+ return compressProse(description).compressed;
164
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Structured Result Extractor — attempts to extract structured data from worker output.
3
+ * Tries multiple extraction strategies before falling back to raw text.
4
+ */
5
+ export interface ExtractedResult {
6
+ /** Whether structured data was successfully extracted */
7
+ structured: boolean;
8
+ /** Parsed structured data (if structured=true) */
9
+ data: unknown;
10
+ /** Raw text output (always available) */
11
+ rawText: string;
12
+ /** Error message if extraction was attempted but failed */
13
+ error?: string;
14
+ }
15
+
16
+ /**
17
+ * Extract structured result from raw worker output text.
18
+ * Tries strategies in order: direct JSON, fenced JSON, key-value markers.
19
+ */
20
+ export function extractStructuredResult(raw: string, _schema?: Record<string, unknown>): ExtractedResult {
21
+ const trimmed = raw.trim();
22
+ if (!trimmed) {
23
+ return { structured: false, data: null, rawText: raw };
24
+ }
25
+
26
+ // Strategy 1: Direct JSON parse (entire output is JSON)
27
+ const directResult = tryDirectJson(trimmed);
28
+ if (directResult !== undefined) {
29
+ return { structured: true, data: directResult, rawText: raw };
30
+ }
31
+
32
+ // Strategy 2: Extract from ```json ... ``` fence
33
+ const fencedResult = tryFencedJson(trimmed);
34
+ if (fencedResult !== undefined) {
35
+ return { structured: true, data: fencedResult, rawText: raw };
36
+ }
37
+
38
+ // Strategy 3: Extract from markers like "RESULT:" or "OUTPUT:"
39
+ const markerResult = tryMarkerExtraction(trimmed);
40
+ if (markerResult !== undefined) {
41
+ return { structured: true, data: markerResult, rawText: raw };
42
+ }
43
+
44
+ return { structured: false, data: null, rawText: raw };
45
+ }
46
+
47
+ function tryDirectJson(text: string): unknown | undefined {
48
+ if (!text.startsWith("{") && !text.startsWith("[")) return undefined;
49
+ try {
50
+ return JSON.parse(text);
51
+ } catch {
52
+ return undefined;
53
+ }
54
+ }
55
+
56
+ function tryFencedJson(text: string): unknown | undefined {
57
+ const match = text.match(/```json\s*\n([\s\S]*?)\n\s*```/);
58
+ if (!match?.[1]) return undefined;
59
+ try {
60
+ return JSON.parse(match[1].trim());
61
+ } catch {
62
+ return undefined;
63
+ }
64
+ }
65
+
66
+ function tryMarkerExtraction(text: string): unknown | undefined {
67
+ // Try to find JSON after common markers
68
+ const markers = ["RESULT:", "OUTPUT:", "ANSWER:", "### Result\n", "## Output\n"];
69
+ for (const marker of markers) {
70
+ const idx = text.indexOf(marker);
71
+ if (idx === -1) continue;
72
+ const after = text.slice(idx + marker.length).trim();
73
+ // Try JSON parse on text after marker
74
+ if (after.startsWith("{") || after.startsWith("[")) {
75
+ try {
76
+ return JSON.parse(after);
77
+ } catch {
78
+ // Try to find just the JSON object/array
79
+ const jsonEnd = findMatchingBracket(after);
80
+ if (jsonEnd > 0) {
81
+ try {
82
+ return JSON.parse(after.slice(0, jsonEnd));
83
+ } catch {
84
+ continue;
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
90
+ return undefined;
91
+ }
92
+
93
+ function findMatchingBracket(text: string): number {
94
+ const openChar = text[0];
95
+ const closeChar = openChar === "{" ? "}" : "]";
96
+ let depth = 0;
97
+ let inString = false;
98
+ let escape = false;
99
+ for (let i = 0; i < text.length; i++) {
100
+ const ch = text[i];
101
+ if (escape) {
102
+ escape = false;
103
+ continue;
104
+ }
105
+ if (ch === "\\") {
106
+ escape = true;
107
+ continue;
108
+ }
109
+ if (ch === '"') {
110
+ inString = !inString;
111
+ continue;
112
+ }
113
+ if (inString) continue;
114
+ if (ch === openChar) depth++;
115
+ if (ch === closeChar) {
116
+ depth--;
117
+ if (depth === 0) return i + 1;
118
+ }
119
+ }
120
+ return -1;
121
+ }
@@ -1,64 +1,81 @@
1
- import { sleep } from "../utils/sleep.ts";
2
-
3
- export interface RetryPolicy {
4
- maxAttempts: number;
5
- backoffMs: number;
6
- jitterRatio: number;
7
- exponentialFactor: number;
8
- retryableErrors?: string[];
9
- }
10
-
11
- export interface RetryHooks {
12
- onAttemptFailed?: (attempt: number, error: Error, nextDelayMs: number) => void;
13
- onRetryGivenUp?: (attempts: number, error: Error) => void;
14
- signal?: AbortSignal;
15
- }
16
-
17
- export const DEFAULT_RETRY_POLICY: RetryPolicy = { maxAttempts: 3, backoffMs: 1000, jitterRatio: 0.3, exponentialFactor: 2 };
18
-
19
- function asError(error: unknown): Error {
20
- return error instanceof Error ? error : new Error(String(error));
21
- }
22
-
23
- function globToRegex(pattern: string): RegExp {
24
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
25
- return new RegExp(`^${escaped}$`, "i");
26
- }
27
-
28
- function isRetryable(error: Error, policy: RetryPolicy): boolean {
29
- const patterns = policy.retryableErrors ?? [];
30
- if (!patterns.length) return true;
31
- return patterns.some((pattern) => globToRegex(pattern).test(error.message));
32
- }
33
-
34
- export function calculateRetryDelay(attempt: number, policy: RetryPolicy = DEFAULT_RETRY_POLICY, random = Math.random): number {
35
- const base = policy.backoffMs * Math.pow(policy.exponentialFactor, Math.max(0, attempt - 1));
36
- const jitter = (random() * 2 - 1) * policy.jitterRatio * base;
37
- return Math.max(0, base + jitter);
38
- }
39
-
40
- export async function executeWithRetry<T>(fn: (attempt: number) => Promise<T>, policy: RetryPolicy = DEFAULT_RETRY_POLICY, hooks: RetryHooks = {}): Promise<T> {
41
- const normalized: RetryPolicy = { ...DEFAULT_RETRY_POLICY, ...policy, maxAttempts: Math.max(1, policy.maxAttempts ?? DEFAULT_RETRY_POLICY.maxAttempts) };
42
- let lastError: Error | undefined;
43
- for (let attempt = 1; attempt <= normalized.maxAttempts; attempt += 1) {
44
- if (hooks.signal?.aborted) throw new Error("Retry aborted.");
45
- try {
46
- return await fn(attempt);
47
- } catch (error) {
48
- lastError = asError(error);
49
- // Never retry if aborted — sleep() would immediately reject on every attempt.
50
- if (hooks.signal?.aborted) {
51
- hooks.onRetryGivenUp?.(attempt, lastError);
52
- throw lastError;
53
- }
54
- if (attempt >= normalized.maxAttempts || !isRetryable(lastError, normalized)) {
55
- hooks.onRetryGivenUp?.(attempt, lastError);
56
- throw lastError;
57
- }
58
- const delay = calculateRetryDelay(attempt, normalized);
59
- hooks.onAttemptFailed?.(attempt, lastError, delay);
60
- await sleep(delay, hooks.signal);
61
- }
62
- }
63
- throw lastError ?? new Error("Retry failed without error.");
64
- }
1
+ import { sleep } from "../utils/sleep.ts";
2
+ import { throwIfCancelled } from "./cancellation.ts";
3
+
4
+ export interface RetryPolicy {
5
+ maxAttempts: number;
6
+ backoffMs: number;
7
+ jitterRatio: number;
8
+ exponentialFactor: number;
9
+ retryableErrors?: string[];
10
+ }
11
+
12
+ export interface RetryAttemptInfo {
13
+ attempt: number;
14
+ attemptId: string;
15
+ }
16
+
17
+ export interface RetryHooks {
18
+ onAttemptFailed?: (attempt: number, error: Error, nextDelayMs: number, info: RetryAttemptInfo) => void;
19
+ onRetryGivenUp?: (attempts: number, error: Error, info: RetryAttemptInfo) => void;
20
+ attemptId?: (attempt: number) => string;
21
+ signal?: AbortSignal;
22
+ }
23
+
24
+ export const DEFAULT_RETRY_POLICY: RetryPolicy = { maxAttempts: 3, backoffMs: 1000, jitterRatio: 0.3, exponentialFactor: 2 };
25
+
26
+ function asError(error: unknown): Error {
27
+ return error instanceof Error ? error : new Error(String(error));
28
+ }
29
+
30
+ function globToRegex(pattern: string): RegExp {
31
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
32
+ return new RegExp(`^${escaped}$`, "i");
33
+ }
34
+
35
+ function isRetryable(error: Error, policy: RetryPolicy): boolean {
36
+ const patterns = policy.retryableErrors ?? [];
37
+ if (!patterns.length) return true;
38
+ return patterns.some((pattern) => globToRegex(pattern).test(error.message));
39
+ }
40
+
41
+ export function calculateRetryDelay(attempt: number, policy: RetryPolicy = DEFAULT_RETRY_POLICY, random = Math.random): number {
42
+ const base = policy.backoffMs * Math.pow(policy.exponentialFactor, Math.max(0, attempt - 1));
43
+ const jitter = (random() * 2 - 1) * policy.jitterRatio * base;
44
+ return Math.max(0, base + jitter);
45
+ }
46
+
47
+ function retryAttemptInfo(attempt: number, hooks: RetryHooks): RetryAttemptInfo {
48
+ return { attempt, attemptId: hooks.attemptId?.(attempt) ?? `retry_attempt_${attempt}` };
49
+ }
50
+
51
+ export async function executeWithRetry<T>(fn: (attempt: number, info: RetryAttemptInfo) => Promise<T>, policy: RetryPolicy = DEFAULT_RETRY_POLICY, hooks: RetryHooks = {}): Promise<T> {
52
+ const normalized: RetryPolicy = { ...DEFAULT_RETRY_POLICY, ...policy, maxAttempts: Math.max(1, policy.maxAttempts ?? DEFAULT_RETRY_POLICY.maxAttempts) };
53
+ let lastError: Error | undefined;
54
+ for (let attempt = 1; attempt <= normalized.maxAttempts; attempt += 1) {
55
+ throwIfCancelled(hooks.signal);
56
+ const info = retryAttemptInfo(attempt, hooks);
57
+ try {
58
+ return await fn(attempt, info);
59
+ } catch (error) {
60
+ lastError = asError(error);
61
+ // Never retry if aborted — sleep() would immediately reject on every attempt.
62
+ if (hooks.signal?.aborted) {
63
+ hooks.onRetryGivenUp?.(attempt, lastError, info);
64
+ throw lastError;
65
+ }
66
+ if (attempt >= normalized.maxAttempts || !isRetryable(lastError, normalized)) {
67
+ hooks.onRetryGivenUp?.(attempt, lastError, info);
68
+ throw lastError;
69
+ }
70
+ const delay = calculateRetryDelay(attempt, normalized);
71
+ hooks.onAttemptFailed?.(attempt, lastError, delay, info);
72
+ try {
73
+ await sleep(delay, hooks.signal);
74
+ } catch (sleepError) {
75
+ if (hooks.signal?.aborted) throwIfCancelled(hooks.signal);
76
+ throw sleepError;
77
+ }
78
+ }
79
+ }
80
+ throw lastError ?? new Error("Retry failed without error.");
81
+ }
@@ -1,8 +1,11 @@
1
1
  import type { PiTeamsConfig } from "../config/config.ts";
2
+ import type { RuntimeResolutionState } from "../state/types.ts";
2
3
  import type { CrewRuntimeKind } from "./crew-agent-runtime.ts";
3
4
 
4
5
  export type CrewRuntimeMode = "auto" | "scaffold" | "child-process" | "live-session";
5
6
 
7
+ export type CrewRuntimeSafety = "trusted" | "explicit_dry_run" | "blocked";
8
+
6
9
  export interface CrewRuntimeCapabilities {
7
10
  kind: CrewRuntimeKind;
8
11
  requestedMode: CrewRuntimeMode;
@@ -13,12 +16,22 @@ export interface CrewRuntimeCapabilities {
13
16
  liveToolActivity: boolean;
14
17
  transcript: boolean;
15
18
  reason?: string;
19
+ safety: CrewRuntimeSafety;
20
+ }
21
+
22
+ export function runtimeResolutionState(runtime: CrewRuntimeCapabilities, resolvedAt = new Date().toISOString()): RuntimeResolutionState {
23
+ return {
24
+ kind: runtime.kind,
25
+ requestedMode: runtime.requestedMode,
26
+ safety: runtime.safety,
27
+ available: runtime.available,
28
+ ...(runtime.fallback ? { fallback: runtime.fallback } : {}),
29
+ ...(runtime.reason ? { reason: runtime.reason } : {}),
30
+ resolvedAt,
31
+ };
16
32
  }
17
33
 
18
34
  export async function isLiveSessionRuntimeAvailable(timeoutMs = 1500, env: NodeJS.ProcessEnv = process.env): Promise<{ available: boolean; reason?: string }> {
19
- if (env.PI_CREW_ENABLE_EXPERIMENTAL_LIVE_SESSION !== "1") {
20
- return { available: false, reason: "Live-session runtime adapter is experimental and disabled. Set PI_CREW_ENABLE_EXPERIMENTAL_LIVE_SESSION=1 to probe SDK support." };
21
- }
22
35
  if (env.PI_CREW_MOCK_LIVE_SESSION === "success") {
23
36
  return { available: true, reason: "Mock live-session runtime is enabled." };
24
37
  }
@@ -52,26 +65,26 @@ export async function isLiveSessionRuntimeAvailable(timeoutMs = 1500, env: NodeJ
52
65
  export async function resolveCrewRuntime(config: PiTeamsConfig, env: NodeJS.ProcessEnv = process.env): Promise<CrewRuntimeCapabilities> {
53
66
  const requestedMode = config.runtime?.mode ?? "auto";
54
67
  const workersDisabled = config.executeWorkers === false || env.PI_CREW_EXECUTE_WORKERS === "0" || env.PI_TEAMS_EXECUTE_WORKERS === "0";
55
- if (requestedMode === "scaffold") return scaffoldCaps(requestedMode);
56
- if (workersDisabled) return scaffoldCaps(requestedMode, "Child worker execution disabled by config/env. Set runtime.mode=scaffold or executeWorkers=false only for dry runs.");
68
+ if (requestedMode === "scaffold") return scaffoldCaps(requestedMode, undefined, "explicit_dry_run");
69
+ if (workersDisabled) return scaffoldCaps(requestedMode, "Child worker execution disabled by config/env. Set runtime.mode=scaffold or executeWorkers=false only for dry runs.", "blocked");
57
70
  if (requestedMode === "child-process") return childCaps(requestedMode);
58
71
  if (requestedMode === "live-session" || (requestedMode === "auto" && config.runtime?.preferLiveSession === true)) {
59
72
  const live = await isLiveSessionRuntimeAvailable(1500, env);
60
73
  if (live.available) return liveCaps(requestedMode);
61
- if (requestedMode === "live-session" && config.runtime?.allowChildProcessFallback === false) return { ...scaffoldCaps(requestedMode), available: false, reason: live.reason };
74
+ if (requestedMode === "live-session" && config.runtime?.allowChildProcessFallback === false) return scaffoldCaps(requestedMode, live.reason, "blocked");
62
75
  return { ...childCaps(requestedMode), fallback: "child-process", reason: live.reason };
63
76
  }
64
77
  return childCaps(requestedMode);
65
78
  }
66
79
 
67
- function scaffoldCaps(requestedMode: CrewRuntimeMode, reason?: string): CrewRuntimeCapabilities {
68
- return { kind: "scaffold", requestedMode, available: true, steer: false, resume: false, liveToolActivity: false, transcript: false, ...(reason ? { reason } : {}) };
80
+ function scaffoldCaps(requestedMode: CrewRuntimeMode, reason?: string, safety: CrewRuntimeSafety = "explicit_dry_run"): CrewRuntimeCapabilities {
81
+ return { kind: "scaffold", requestedMode, available: safety !== "blocked", steer: false, resume: false, liveToolActivity: false, transcript: false, safety, ...(reason ? { reason } : {}) };
69
82
  }
70
83
 
71
84
  function childCaps(requestedMode: CrewRuntimeMode, reason?: string): CrewRuntimeCapabilities {
72
- return { kind: "child-process", requestedMode, available: true, steer: false, resume: false, liveToolActivity: false, transcript: true, ...(reason ? { reason } : {}) };
85
+ return { kind: "child-process", requestedMode, available: true, steer: false, resume: false, liveToolActivity: false, transcript: true, safety: "trusted", ...(reason ? { reason } : {}) };
73
86
  }
74
87
 
75
88
  function liveCaps(requestedMode: CrewRuntimeMode): CrewRuntimeCapabilities {
76
- return { kind: "live-session", requestedMode, available: true, steer: true, resume: true, liveToolActivity: true, transcript: true };
89
+ return { kind: "live-session", requestedMode, available: true, steer: true, resume: true, liveToolActivity: true, transcript: true, safety: "trusted" };
77
90
  }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Phase 6: Semaphore and fail-fast parallel execution.
3
+ *
4
+ * Adapted from oh-my-pi's `parallel.ts` Semaphore class and
5
+ * `mapWithConcurrencyLimit` implementation. Provides:
6
+ * - Explicit acquire/release Semaphore for concurrency control
7
+ * - Fail-fast on first error (via Promise.race)
8
+ * - AbortSignal support for graceful cancellation
9
+ * - Partial results on abort
10
+ */
11
+
12
+ /**
13
+ * Simple counting semaphore for limiting concurrency across independently-scheduled async work.
14
+ */
15
+ export class Semaphore {
16
+ #max: number;
17
+ #current = 0;
18
+ #queue: Array<() => void> = [];
19
+
20
+ constructor(max: number) {
21
+ this.#max = Math.max(1, max);
22
+ }
23
+
24
+ async acquire(): Promise<void> {
25
+ if (this.#current < this.#max) {
26
+ this.#current++;
27
+ return;
28
+ }
29
+ const { promise, resolve } = (() => {
30
+ let res: () => void;
31
+ const p = new Promise<void>((r) => { res = r; });
32
+ return { promise: p, resolve: res! };
33
+ })();
34
+ this.#queue.push(resolve);
35
+ return promise;
36
+ }
37
+
38
+ release(): void {
39
+ const next = this.#queue.shift();
40
+ if (next) {
41
+ next();
42
+ } else if (this.#current > 0) {
43
+ this.#current--;
44
+ }
45
+ // Guard: over-release is a no-op to prevent #current going negative
46
+ }
47
+
48
+ /** Current number of acquired slots. */
49
+ get current(): number {
50
+ return this.#current;
51
+ }
52
+
53
+ /** Number of waiters in the queue. */
54
+ get waiting(): number {
55
+ return this.#queue.length;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Result of parallel execution with fail-fast support.
61
+ */
62
+ export interface ParallelResult<R> {
63
+ /** Results array — undefined entries indicate tasks that were skipped due to abort. */
64
+ results: (R | undefined)[];
65
+ /** Whether execution was aborted before all tasks completed. */
66
+ aborted: boolean;
67
+ /** The first error that triggered fail-fast, if any. */
68
+ firstError?: unknown;
69
+ }
70
+
71
+ /**
72
+ * Execute items with a concurrency limit, fail-fast, and abort signal support.
73
+ *
74
+ * - On first error: aborts remaining workers and rethrows.
75
+ * - On external abort: returns partial results with `aborted: true`.
76
+ * - Results are returned in the same order as input items.
77
+ *
78
+ * Adapted from oh-my-pi's `mapWithConcurrencyLimit`.
79
+ */
80
+ export async function mapWithFailFast<T, R>(
81
+ items: T[],
82
+ concurrency: number,
83
+ fn: (item: T, index: number, signal: AbortSignal) => Promise<R>,
84
+ signal?: AbortSignal,
85
+ ): Promise<ParallelResult<R>> {
86
+ const limit = Math.max(1, Math.min(concurrency, items.length));
87
+ const results: (R | undefined)[] = new Array(items.length);
88
+ let nextIndex = 0;
89
+
90
+ // Internal abort controller for fail-fast
91
+ const abortController = new AbortController();
92
+ const workerSignal = signal
93
+ ? AbortSignal.any([signal, abortController.signal])
94
+ : abortController.signal;
95
+
96
+ // Promise that rejects on first error — used for fail-fast
97
+ let rejectFirst: (error: unknown) => void;
98
+ const firstErrorPromise = new Promise<never>((_, reject) => {
99
+ rejectFirst = reject;
100
+ });
101
+
102
+ const worker = async (): Promise<void> => {
103
+ while (true) {
104
+ if (workerSignal.aborted) return;
105
+ const index = nextIndex++;
106
+ if (index >= items.length) return;
107
+ try {
108
+ results[index] = await fn(items[index], index, workerSignal);
109
+ } catch (error) {
110
+ if (!workerSignal.aborted) {
111
+ abortController.abort();
112
+ rejectFirst(error);
113
+ throw error;
114
+ }
115
+ }
116
+ }
117
+ };
118
+
119
+ const workers = Array.from({ length: limit }, () => worker());
120
+
121
+ try {
122
+ await Promise.race([Promise.all(workers), firstErrorPromise]);
123
+ } catch (error) {
124
+ if (signal?.aborted) {
125
+ return { results, aborted: true, firstError: error };
126
+ }
127
+ throw error;
128
+ }
129
+
130
+ return { results, aborted: signal?.aborted ?? false };
131
+ }