pi-crew 0.5.2 → 0.5.6

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 (137) hide show
  1. package/CHANGELOG.md +183 -0
  2. package/README.md +17 -1
  3. package/docs/architecture.md +2 -0
  4. package/docs/bugs/cross-session-notification-leakage.md +82 -0
  5. package/docs/coding-agent-optimization.md +268 -0
  6. package/docs/deep-review-report.md +384 -0
  7. package/docs/distillation/cybersecurity-patterns.md +294 -0
  8. package/docs/migration-v0.4-v0.5.md +208 -0
  9. package/docs/optimization-plan.md +642 -0
  10. package/docs/pi-crew-v0.5.5-audit-fix-plan.md +133 -0
  11. package/docs/pi-mono-opportunities.md +969 -0
  12. package/docs/pi-mono-review.md +291 -0
  13. package/docs/skills/REFERENCE.md +144 -0
  14. package/package.json +12 -9
  15. package/skills/artifact-analysis-loop/SKILL.md +302 -0
  16. package/skills/async-worker-recovery/SKILL.md +19 -1
  17. package/skills/child-pi-spawning/SKILL.md +19 -6
  18. package/skills/context-artifact-hygiene/SKILL.md +19 -2
  19. package/skills/delegation-patterns/SKILL.md +68 -3
  20. package/skills/detection-pipeline-design/SKILL.md +285 -0
  21. package/skills/event-log-tracing/SKILL.md +20 -6
  22. package/skills/git-master/SKILL.md +20 -6
  23. package/skills/hunting-investigation-loop/SKILL.md +401 -0
  24. package/skills/incident-playbook-construction/SKILL.md +383 -0
  25. package/skills/live-agent-lifecycle/SKILL.md +20 -6
  26. package/skills/mailbox-interactive/SKILL.md +19 -6
  27. package/skills/model-routing-context/SKILL.md +19 -1
  28. package/skills/multi-perspective-review/SKILL.md +19 -4
  29. package/skills/observability-reliability/SKILL.md +19 -2
  30. package/skills/orchestration/SKILL.md +20 -2
  31. package/skills/ownership-session-security/SKILL.md +20 -2
  32. package/skills/pi-extension-lifecycle/SKILL.md +20 -2
  33. package/skills/post-mortem/SKILL.md +7 -2
  34. package/skills/read-only-explorer/SKILL.md +20 -6
  35. package/skills/requirements-to-task-packet/SKILL.md +23 -3
  36. package/skills/resource-discovery-config/SKILL.md +20 -2
  37. package/skills/runtime-state-reader/SKILL.md +20 -2
  38. package/skills/safe-bash/SKILL.md +21 -6
  39. package/skills/scrutinize/SKILL.md +20 -2
  40. package/skills/secure-agent-orchestration-review/SKILL.md +29 -2
  41. package/skills/security-review/SKILL.md +560 -0
  42. package/skills/state-mutation-locking/SKILL.md +22 -2
  43. package/skills/systematic-debugging/SKILL.md +8 -6
  44. package/skills/threat-hypothesis-framework/SKILL.md +175 -0
  45. package/skills/ui-render-performance/SKILL.md +20 -2
  46. package/skills/verification-before-done/SKILL.md +17 -2
  47. package/skills/widget-rendering/SKILL.md +21 -6
  48. package/skills/workspace-isolation/SKILL.md +20 -6
  49. package/skills/worktree-isolation/SKILL.md +20 -6
  50. package/src/agents/agent-config.ts +40 -1
  51. package/src/benchmark/benchmark-runner.ts +45 -0
  52. package/src/benchmark/feedback-loop.ts +5 -0
  53. package/src/config/config.ts +32 -5
  54. package/src/config/role-tools.ts +82 -0
  55. package/src/config/suggestions.ts +8 -0
  56. package/src/config/types.ts +4 -0
  57. package/src/extension/async-notifier.ts +10 -1
  58. package/src/extension/crew-cleanup.ts +114 -0
  59. package/src/extension/cross-extension-rpc.ts +1 -1
  60. package/src/extension/notification-router.ts +18 -0
  61. package/src/extension/register.ts +27 -19
  62. package/src/extension/registration/subagent-tools.ts +1 -1
  63. package/src/extension/team-tool/anchor.ts +201 -0
  64. package/src/extension/team-tool/api.ts +2 -1
  65. package/src/extension/team-tool/auto-summarize.ts +154 -0
  66. package/src/extension/team-tool/run.ts +42 -7
  67. package/src/extension/team-tool.ts +44 -2
  68. package/src/hooks/registry.ts +1 -3
  69. package/src/observability/event-bus.ts +69 -0
  70. package/src/observability/event-to-metric.ts +0 -2
  71. package/src/runtime/anchor-manager.ts +473 -0
  72. package/src/runtime/async-runner.ts +8 -4
  73. package/src/runtime/auto-summarize.ts +350 -0
  74. package/src/runtime/background-runner.ts +10 -3
  75. package/src/runtime/budget-tracker.ts +354 -0
  76. package/src/runtime/chain-runner.ts +507 -0
  77. package/src/runtime/child-pi.ts +123 -35
  78. package/src/runtime/crash-recovery.ts +5 -4
  79. package/src/runtime/crew-agent-runtime.ts +1 -0
  80. package/src/runtime/custom-tools/irc-tool.ts +13 -0
  81. package/src/runtime/custom-tools/submit-result-tool.ts +3 -2
  82. package/src/runtime/delivery-coordinator.ts +10 -3
  83. package/src/runtime/dynamic-script-runner.ts +482 -0
  84. package/src/runtime/foreground-control.ts +87 -17
  85. package/src/runtime/handoff-manager.ts +589 -0
  86. package/src/runtime/hidden-handoff.ts +424 -0
  87. package/src/runtime/live-agent-manager.ts +20 -4
  88. package/src/runtime/live-session-runtime.ts +39 -4
  89. package/src/runtime/manifest-cache.ts +2 -1
  90. package/src/runtime/model-resolver.ts +16 -4
  91. package/src/runtime/phase-tracker.ts +373 -0
  92. package/src/runtime/pi-args.ts +11 -1
  93. package/src/runtime/pi-json-output.ts +31 -0
  94. package/src/runtime/pipeline-runner.ts +514 -0
  95. package/src/runtime/progress-tracker.ts +124 -0
  96. package/src/runtime/retry-runner.ts +354 -0
  97. package/src/runtime/sandbox.ts +252 -0
  98. package/src/runtime/scheduler.ts +7 -2
  99. package/src/runtime/skill-effectiveness.ts +473 -0
  100. package/src/runtime/skill-instructions.ts +37 -3
  101. package/src/runtime/subagent-manager.ts +1 -1
  102. package/src/runtime/task-graph.ts +11 -1
  103. package/src/runtime/task-runner.ts +92 -18
  104. package/src/runtime/team-runner.ts +13 -12
  105. package/src/runtime/tool-progress.ts +10 -3
  106. package/src/runtime/verification-gates.ts +367 -0
  107. package/src/schema/team-tool-schema.ts +37 -0
  108. package/src/skills/discover-skills.ts +5 -0
  109. package/src/state/active-run-registry.ts +9 -2
  110. package/src/state/contracts.ts +9 -0
  111. package/src/state/crew-init.ts +3 -3
  112. package/src/state/decision-ledger.ts +98 -55
  113. package/src/state/event-log-rotation.ts +2 -2
  114. package/src/state/event-log.ts +144 -10
  115. package/src/state/hook-instinct-bridge.ts +5 -5
  116. package/src/state/mailbox.ts +10 -0
  117. package/src/state/run-cache.ts +18 -8
  118. package/src/state/state-store.ts +3 -1
  119. package/src/state/types.ts +4 -0
  120. package/src/tools/safe-bash-extension.ts +1 -0
  121. package/src/tools/safe-bash.ts +152 -20
  122. package/src/types/new-api-types.ts +34 -0
  123. package/src/ui/agent-management-overlay.ts +5 -1
  124. package/src/ui/crew-widget.ts +29 -15
  125. package/src/ui/overlays/mailbox-detail-overlay.ts +13 -2
  126. package/src/ui/powerbar-publisher.ts +101 -7
  127. package/src/ui/tool-render.ts +15 -15
  128. package/src/ui/transcript-cache.ts +13 -0
  129. package/src/utils/bm25-search.ts +16 -8
  130. package/src/utils/env-filter.ts +8 -5
  131. package/src/utils/redaction.ts +169 -15
  132. package/src/utils/session-utils.ts +52 -0
  133. package/src/utils/sse-parser.ts +10 -1
  134. package/src/worktree/cleanup.ts +6 -1
  135. package/src/worktree/worktree-manager.ts +32 -13
  136. package/workflows/chain.workflow.md +252 -0
  137. package/workflows/pipeline.workflow.md +27 -0
@@ -0,0 +1,473 @@
1
+ /**
2
+ * Maximum number of anchors to prevent memory leaks.
3
+ */
4
+ const MAX_ANCHORS = 50;
5
+
6
+ /**
7
+ * Maximum number of handoffs per anchor to prevent memory leaks.
8
+ */
9
+ const MAX_HANDOFFS_PER_ANCHOR = 100;
10
+
11
+ /**
12
+ * AnchorManager - Creates shared summary points where multiple handoffs accumulate.
13
+ *
14
+ * Based on pi-boomerang's anchorMode pattern:
15
+ * - setAnchor() creates a shared summary point for a session
16
+ * - accumulateHandoff() adds handoffs to the anchor
17
+ * - clearAnchor() finalizes and returns accumulated summaries
18
+ * - getAnchorHandoff() retrieves accumulated summary without clearing
19
+ *
20
+ * @see docs/pi-boomerang-integration-plan.md
21
+ */
22
+
23
+ import type { HandoffSummary } from "./handoff-manager.ts";
24
+
25
+ /**
26
+ * Represents a shared summary point where multiple handoffs accumulate.
27
+ */
28
+ export interface Anchor {
29
+ /** Unique anchor identifier */
30
+ id: string;
31
+ /** Session ID this anchor belongs to */
32
+ sessionId: string;
33
+ /** Timestamp when anchor was created */
34
+ createdAt: number;
35
+ /** Accumulated handoffs */
36
+ handoffs: HandoffSummary[];
37
+ /** Initial context when anchor was set */
38
+ context: Record<string, unknown>;
39
+ }
40
+
41
+ /**
42
+ * Options for AnchorManager.
43
+ */
44
+ export interface AnchorManagerOptions {
45
+ /** Custom event emitter for anchor events */
46
+ eventEmitter?: AnchorEventEmitter;
47
+ }
48
+
49
+ /**
50
+ * Event emitter interface for anchor lifecycle events.
51
+ */
52
+ export interface AnchorEventEmitter {
53
+ emit(event: string, data: unknown): void;
54
+ }
55
+
56
+ /**
57
+ * Event data for anchor lifecycle events.
58
+ */
59
+ export interface AnchorEventData {
60
+ anchor: Anchor;
61
+ }
62
+
63
+ export interface AnchorClearedEventData {
64
+ anchorId: string;
65
+ accumulated: HandoffSummary;
66
+ }
67
+
68
+ export interface AnchorHandoffAccumulatedEventData {
69
+ anchorId: string;
70
+ handoff: HandoffSummary;
71
+ }
72
+
73
+ /**
74
+ * AnchorManager creates shared summary points where multiple handoffs accumulate.
75
+ * This enables scenarios where multiple agents contribute to a shared summary
76
+ * that is then passed to a parent or used for tree navigation.
77
+ */
78
+ export class AnchorManager {
79
+ private anchors: Map<string, Anchor> = new Map();
80
+ private sessionAnchors: Map<string, string> = new Map();
81
+ private options: AnchorManagerOptions;
82
+ private readonly MAX_ANCHORS = 1000;
83
+ private readonly TTL_MS = 300000; // 5 minutes
84
+
85
+ constructor(options: AnchorManagerOptions = {}) {
86
+ this.options = options;
87
+ }
88
+
89
+ /**
90
+ * Set an anchor point for a session.
91
+ * All subsequent handoffs will accumulate to this anchor.
92
+ *
93
+ * @param sessionId - The session ID to create anchor for
94
+ * @param context - Initial context for the anchor
95
+ * @returns The anchor ID
96
+ */
97
+ setAnchor(sessionId: string, context: Record<string, unknown> = {}): string {
98
+ const anchorId = this.generateAnchorId();
99
+
100
+ // Evict expired or overflow anchors before adding new one
101
+ this.evictExpiredAnchors();
102
+ if (this.anchors.size >= this.MAX_ANCHORS) {
103
+ this.evictOldestAnchor();
104
+ }
105
+
106
+ const anchor: Anchor = {
107
+ id: anchorId,
108
+ sessionId,
109
+ createdAt: Date.now(),
110
+ handoffs: [],
111
+ context,
112
+ };
113
+
114
+ this.anchors.set(anchorId, anchor);
115
+ this.sessionAnchors.set(sessionId, anchorId);
116
+
117
+ this.options.eventEmitter?.emit("anchor:created", { anchor });
118
+
119
+ return anchorId;
120
+ }
121
+
122
+ /**
123
+ * Get the current anchor for a session.
124
+ *
125
+ * @param sessionId - The session ID
126
+ * @returns The anchor if exists, null otherwise
127
+ */
128
+ getAnchor(sessionId: string): Anchor | null {
129
+ this.evictExpiredAnchors();
130
+ const anchorId = this.sessionAnchors.get(sessionId);
131
+ if (!anchorId) return null;
132
+ return this.anchors.get(anchorId) ?? null;
133
+ }
134
+
135
+ /**
136
+ * Get the anchor ID for a session.
137
+ *
138
+ * @param sessionId - The session ID
139
+ * @returns The anchor ID if exists, undefined otherwise
140
+ */
141
+ getAnchorId(sessionId: string): string | undefined {
142
+ return this.sessionAnchors.get(sessionId);
143
+ }
144
+
145
+ /**
146
+ * Clear an anchor and return the accumulated handoff summary.
147
+ * This removes the anchor and returns merged handoffs.
148
+ *
149
+ * @param anchorId - The anchor ID to clear
150
+ * @returns The accumulated handoff summary
151
+ * @throws Error if anchor not found or no handoffs accumulated
152
+ */
153
+ clearAnchor(anchorId: string): HandoffSummary {
154
+ const anchor = this.anchors.get(anchorId);
155
+
156
+ if (!anchor) {
157
+ throw new AnchorNotFoundError(anchorId);
158
+ }
159
+
160
+ const accumulated = this.accumulateHandoffs(anchor.handoffs);
161
+
162
+ // Clean up maps
163
+ this.sessionAnchors.delete(anchor.sessionId);
164
+ this.anchors.delete(anchorId);
165
+
166
+ this.options.eventEmitter?.emit("anchor:cleared", { anchorId, accumulated });
167
+
168
+ return accumulated;
169
+ }
170
+
171
+ /**
172
+ * Clear anchor by session ID.
173
+ *
174
+ * @param sessionId - The session ID
175
+ * @returns The accumulated handoff summary
176
+ */
177
+ clearAnchorBySession(sessionId: string): HandoffSummary | null {
178
+ const anchorId = this.sessionAnchors.get(sessionId);
179
+ if (!anchorId) return null;
180
+ return this.clearAnchor(anchorId);
181
+ }
182
+
183
+ /**
184
+ * Accumulate a handoff to an anchor.
185
+ * If anchor doesn't exist, creates an implicit anchor.
186
+ *
187
+ * @param anchorId - The anchor ID
188
+ * @param handoff - The handoff summary to accumulate
189
+ */
190
+ accumulateHandoff(anchorId: string, handoff: HandoffSummary): void {
191
+ let anchor = this.anchors.get(anchorId);
192
+
193
+ // Create implicit anchor if doesn't exist - create directly with the given anchorId
194
+ if (!anchor) {
195
+ // Evict oldest anchor if at capacity
196
+ if (this.anchors.size >= MAX_ANCHORS) {
197
+ const oldest = this.anchors.keys().next().value;
198
+ if (oldest) {
199
+ this.anchors.delete(oldest);
200
+ }
201
+ }
202
+ const implicitAnchor: Anchor = {
203
+ id: anchorId,
204
+ sessionId: handoff.runId,
205
+ createdAt: Date.now(),
206
+ handoffs: [],
207
+ context: {},
208
+ };
209
+ this.anchors.set(anchorId, implicitAnchor);
210
+ anchor = implicitAnchor;
211
+ }
212
+
213
+ // Enforce handoff limit per anchor to prevent unbounded growth
214
+ if (anchor!.handoffs.length >= MAX_HANDOFFS_PER_ANCHOR) {
215
+ anchor!.handoffs.shift();
216
+ }
217
+ anchor!.handoffs.push(handoff);
218
+
219
+ this.options.eventEmitter?.emit("anchor:handoffAccumulated", {
220
+ anchorId: anchor!.id,
221
+ handoff,
222
+ });
223
+ }
224
+
225
+ /**
226
+ * Accumulate handoff by session ID.
227
+ *
228
+ * @param sessionId - The session ID
229
+ * @param handoff - The handoff summary to accumulate
230
+ */
231
+ accumulateHandoffBySession(sessionId: string, handoff: HandoffSummary): void {
232
+ const anchorId = this.sessionAnchors.get(sessionId);
233
+ if (!anchorId) {
234
+ // Create new anchor for this session
235
+ const newAnchorId = this.setAnchor(sessionId);
236
+ this.accumulateHandoff(newAnchorId, handoff);
237
+ } else {
238
+ this.accumulateHandoff(anchorId, handoff);
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Get the accumulated handoff for an anchor without clearing it.
244
+ *
245
+ * @param anchorId - The anchor ID
246
+ * @returns The accumulated handoff summary, or null if anchor not found or no handoffs
247
+ */
248
+ getAnchorHandoff(anchorId: string): HandoffSummary | null {
249
+ const anchor = this.anchors.get(anchorId);
250
+ if (!anchor) return null;
251
+ if (anchor.handoffs.length === 0) return null;
252
+ return this.accumulateHandoffs(anchor.handoffs);
253
+ }
254
+
255
+ /**
256
+ * Get accumulated handoff by session ID.
257
+ *
258
+ * @param sessionId - The session ID
259
+ * @returns The accumulated handoff summary, or null if no anchor or handoffs
260
+ */
261
+ getAnchorHandoffBySession(sessionId: string): HandoffSummary | null {
262
+ const anchorId = this.sessionAnchors.get(sessionId);
263
+ if (!anchorId) return null;
264
+ return this.getAnchorHandoff(anchorId);
265
+ }
266
+
267
+ /**
268
+ * Get status information for an anchor.
269
+ *
270
+ * @param anchorId - The anchor ID
271
+ * @returns Status object or null if anchor not found
272
+ */
273
+ getAnchorStatus(anchorId: string): AnchorStatus | null {
274
+ const anchor = this.anchors.get(anchorId);
275
+ if (!anchor) return null;
276
+
277
+ return {
278
+ anchorId: anchor.id,
279
+ sessionId: anchor.sessionId,
280
+ createdAt: anchor.createdAt,
281
+ handoffCount: anchor.handoffs.length,
282
+ totalTokens: anchor.handoffs.reduce(
283
+ (sum, h) => sum + h.metrics.tokensUsed,
284
+ 0,
285
+ ),
286
+ totalDuration: anchor.handoffs.reduce(
287
+ (sum, h) => sum + h.metrics.duration,
288
+ 0,
289
+ ),
290
+ context: anchor.context,
291
+ };
292
+ }
293
+
294
+ /**
295
+ * Get status by session ID.
296
+ *
297
+ * @param sessionId - The session ID
298
+ * @returns Status object or null if no anchor
299
+ */
300
+ getAnchorStatusBySession(sessionId: string): AnchorStatus | null {
301
+ const anchorId = this.sessionAnchors.get(sessionId);
302
+ if (!anchorId) return null;
303
+ return this.getAnchorStatus(anchorId);
304
+ }
305
+
306
+ /**
307
+ * Check if an anchor has handoffs accumulated.
308
+ *
309
+ * @param anchorId - The anchor ID
310
+ * @returns True if anchor has handoffs
311
+ */
312
+ hasHandoffs(anchorId: string): boolean {
313
+ const anchor = this.anchors.get(anchorId);
314
+ return anchor ? anchor.handoffs.length > 0 : false;
315
+ }
316
+
317
+ /**
318
+ * Get all anchors.
319
+ *
320
+ * @returns Array of all anchors
321
+ */
322
+ getAllAnchors(): Anchor[] {
323
+ return Array.from(this.anchors.values());
324
+ }
325
+
326
+ /**
327
+ * Clear all anchors.
328
+ */
329
+ clearAll(): void {
330
+ this.anchors.clear();
331
+ this.sessionAnchors.clear();
332
+ this.options.eventEmitter?.emit("anchor:cleared_all", {});
333
+ }
334
+
335
+ /**
336
+ * Merge multiple handoffs into a single accumulated summary.
337
+ */
338
+ private accumulateHandoffs(handoffs: HandoffSummary[]): HandoffSummary {
339
+ if (handoffs.length === 0) {
340
+ throw new NoHandoffsError();
341
+ }
342
+
343
+ const allMetrics = handoffs.reduce(
344
+ (acc, h) => ({
345
+ tokensUsed: acc.tokensUsed + h.metrics.tokensUsed,
346
+ duration: acc.duration + h.metrics.duration,
347
+ iterations: acc.iterations + h.metrics.iterations,
348
+ toolsUsed: [...acc.toolsUsed, ...h.metrics.toolsUsed],
349
+ }),
350
+ { tokensUsed: 0, duration: 0, iterations: 0, toolsUsed: [] as string[] },
351
+ );
352
+
353
+ // Deduplicate tools
354
+ const uniqueTools = [...new Set(allMetrics.toolsUsed)];
355
+
356
+ return {
357
+ taskId: `anchor-${handoffs[0].taskId}`,
358
+ runId: handoffs[0].runId,
359
+ timestamp: Date.now(),
360
+
361
+ task: `Accumulated: ${handoffs.map((h) => h.task).join(" → ")}`,
362
+ outcome: handoffs.every((h) => h.outcome === "success")
363
+ ? "success"
364
+ : handoffs.some((h) => h.outcome === "failure")
365
+ ? "failure"
366
+ : "partial",
367
+
368
+ filesCreated: [...new Set(handoffs.flatMap((h) => h.filesCreated))],
369
+ filesModified: [...new Set(handoffs.flatMap((h) => h.filesModified))],
370
+ filesDeleted: [...new Set(handoffs.flatMap((h) => h.filesDeleted))],
371
+
372
+ decisions: handoffs.flatMap((h) => h.decisions),
373
+ blockers: [...new Set(handoffs.flatMap((h) => h.blockers))],
374
+ nextSteps: handoffs.flatMap((h) => h.nextSteps),
375
+
376
+ metrics: {
377
+ tokensUsed: allMetrics.tokensUsed,
378
+ duration: allMetrics.duration,
379
+ iterations: allMetrics.iterations,
380
+ toolsUsed: uniqueTools,
381
+ },
382
+
383
+ contextSnapshot: handoffs.map((h) => h.contextSnapshot).join("\n---\n"),
384
+ };
385
+ }
386
+
387
+ /**
388
+ * Generate a unique anchor ID.
389
+ */
390
+ private generateAnchorId(): string {
391
+ return `anchor-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
392
+ }
393
+
394
+ /**
395
+ * Evict expired anchors based on TTL.
396
+ */
397
+ private evictExpiredAnchors(): void {
398
+ const now = Date.now();
399
+ for (const [anchorId, anchor] of this.anchors) {
400
+ if (now - anchor.createdAt > this.TTL_MS) {
401
+ this.sessionAnchors.delete(anchor.sessionId);
402
+ this.anchors.delete(anchorId);
403
+ }
404
+ }
405
+ }
406
+
407
+ /**
408
+ * Evict the oldest anchor (LRU eviction when at max capacity).
409
+ */
410
+ private evictOldestAnchor(): void {
411
+ let oldestAnchorId: string | null = null;
412
+ let oldestTime = Infinity;
413
+ for (const [anchorId, anchor] of this.anchors) {
414
+ if (anchor.createdAt < oldestTime) {
415
+ oldestTime = anchor.createdAt;
416
+ oldestAnchorId = anchorId;
417
+ }
418
+ }
419
+ if (oldestAnchorId) {
420
+ const anchor = this.anchors.get(oldestAnchorId);
421
+ if (anchor) {
422
+ this.sessionAnchors.delete(anchor.sessionId);
423
+ }
424
+ this.anchors.delete(oldestAnchorId);
425
+ }
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Status information for an anchor.
431
+ */
432
+ export interface AnchorStatus {
433
+ anchorId: string;
434
+ sessionId: string;
435
+ createdAt: number;
436
+ handoffCount: number;
437
+ totalTokens: number;
438
+ totalDuration: number;
439
+ context: Record<string, unknown>;
440
+ }
441
+
442
+ /**
443
+ * Error thrown when an anchor is not found.
444
+ */
445
+ export class AnchorNotFoundError extends Error {
446
+ public readonly anchorId: string;
447
+
448
+ constructor(anchorId: string) {
449
+ super(`Anchor not found: ${anchorId}`);
450
+ this.name = "AnchorNotFoundError";
451
+ this.anchorId = anchorId;
452
+ }
453
+ }
454
+
455
+ /**
456
+ * Error thrown when there are no handoffs to accumulate.
457
+ */
458
+ export class NoHandoffsError extends Error {
459
+ constructor() {
460
+ super("No handoffs to accumulate");
461
+ this.name = "NoHandoffsError";
462
+ }
463
+ }
464
+
465
+ /**
466
+ * Create an AnchorManager with default options.
467
+ */
468
+ export function createAnchorManager(options?: AnchorManagerOptions): AnchorManager {
469
+ return new AnchorManager(options);
470
+ }
471
+
472
+ // Re-export HandoffSummary for consumers
473
+ export type { HandoffSummary } from "./handoff-manager.ts";
@@ -1,4 +1,4 @@
1
- import { spawn } from "node:child_process";
1
+ import { spawn, type SpawnOptions } from "node:child_process";
2
2
  import { createRequire } from "node:module";
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
@@ -150,14 +150,18 @@ export async function spawnBackgroundTeamRun(manifest: TeamRunManifest): Promise
150
150
  //
151
151
  // IMPORTANT: session_shutdown handlers must NOT kill async runners.
152
152
  // See register.ts cleanupRuntime — the kill loop was commented out.
153
- const child = spawn(process.execPath, command.args, {
153
+ // Type assertion for setsid is necessary because Node.js types don't include it
154
+ // in SpawnOptions on all platforms, but it's supported on Unix systems.
155
+ // Use explicit cast through unknown to satisfy TypeScript's strict type checking.
156
+ const spawnOpts = {
154
157
  cwd: manifest.cwd,
155
158
  detached: true,
156
- setsid: true as any,
159
+ setsid: true,
157
160
  stdio: ["ignore", "pipe", "pipe"],
158
161
  env: envWithoutParentPid,
159
162
  windowsHide: true,
160
- } as any) as any;
163
+ } as unknown as Parameters<typeof spawn>[2];
164
+ const child = spawn(process.execPath, command.args, spawnOpts);
161
165
  child.on("error", (error: Error) => {
162
166
  console.error(`[pi-crew] async spawn failed: ${error.message}`);
163
167
  });