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,482 @@
1
+ import * as vm from "node:vm";
2
+ import * as acorn from "acorn";
3
+ import { WorkflowSandbox, type SandboxOptions } from "./sandbox.ts";
4
+
5
+ /**
6
+ * Forbidden globals that could compromise sandbox security or cause side effects.
7
+ * These are checked during AST validation before execution.
8
+ */
9
+ export const FORBIDDEN_GLOBALS = [
10
+ "Date",
11
+ "Math.random",
12
+ "require",
13
+ "import",
14
+ "module",
15
+ "exports",
16
+ "__dirname",
17
+ "__filename",
18
+ "process.exit",
19
+ "process.kill",
20
+ "process.hrtime",
21
+ "process.memoryUsage",
22
+ "process.cpuUsage",
23
+ "process.binding",
24
+ "process.dlopen",
25
+ "process._tickCallback",
26
+ "eval",
27
+ "Function",
28
+ "AsyncFunction",
29
+ "GeneratorFunction",
30
+ "Proxy",
31
+ "Reflect",
32
+ "WebAssembly",
33
+ "global",
34
+ "globalThis",
35
+ "window",
36
+ "document",
37
+ "XMLHttpRequest",
38
+ "fetch",
39
+ "WebSocket",
40
+ "Worker",
41
+ "SharedArrayBuffer",
42
+ "Atomics",
43
+ ] as const;
44
+
45
+ // Freeze the array at runtime to ensure it's truly immutable
46
+ Object.freeze(FORBIDDEN_GLOBALS);
47
+
48
+ export type ForbiddenGlobal = (typeof FORBIDDEN_GLOBALS)[number];
49
+
50
+ export interface ScriptValidationResult {
51
+ valid: boolean;
52
+ errors: ScriptValidationError[];
53
+ warnings: ScriptValidationWarning[];
54
+ }
55
+
56
+ export interface ScriptValidationError {
57
+ type: "forbidden_global" | "forbidden_syntax" | "parse_error";
58
+ message: string;
59
+ location?: { line: number; column: number };
60
+ }
61
+
62
+ export interface ScriptValidationWarning {
63
+ type: "deprecated_api" | "potentially_unsafe";
64
+ message: string;
65
+ location?: { line: number; column: number };
66
+ }
67
+
68
+ export interface DynamicScriptOptions {
69
+ timeout?: number;
70
+ maxTokens?: number;
71
+ allowAwait?: boolean;
72
+ allowAsync?: boolean;
73
+ strictMode?: boolean;
74
+ /** Enable strict AST whitelist mode (C2) - reject dynamic property access, call expressions */
75
+ strictAstWhitelist?: boolean;
76
+ }
77
+
78
+ export interface ScriptExecutionResult {
79
+ success: boolean;
80
+ value?: unknown;
81
+ error?: string;
82
+ executionTime: number;
83
+ validation: ScriptValidationResult;
84
+ }
85
+
86
+ /**
87
+ * DynamicScriptRunner executes JavaScript in a VM sandbox with AST validation
88
+ * and forbidden pattern detection.
89
+ *
90
+ * Note: AST parsing is simplified without acorn. For full AST validation,
91
+ * add acorn as a dependency.
92
+ */
93
+ export class DynamicScriptRunner {
94
+ private sandbox: WorkflowSandbox;
95
+ private defaultTimeout: number;
96
+ private options: DynamicScriptOptions;
97
+
98
+ constructor(options: DynamicScriptOptions = {}) {
99
+ this.defaultTimeout = options.timeout ?? 30000;
100
+ this.options = options;
101
+ this.sandbox = new WorkflowSandbox({
102
+ timeout: this.defaultTimeout,
103
+ });
104
+ }
105
+
106
+ /**
107
+ * Validate script before execution using full AST parsing (C1).
108
+ * Uses acorn for comprehensive syntax tree analysis.
109
+ */
110
+ validate(code: string): ScriptValidationResult {
111
+ const errors: ScriptValidationError[] = [];
112
+ const warnings: ScriptValidationWarning[] = [];
113
+
114
+ // C1: Full AST parsing with acorn for complete validation
115
+ let ast: acorn.Node | null = null;
116
+ try {
117
+ ast = acorn.parse(code, {
118
+ ecmaVersion: "latest",
119
+ sourceType: "script",
120
+ allowReturnOutsideFunction: true,
121
+ allowAwaitOutsideFunction: this.options.allowAwait ?? false,
122
+ });
123
+ } catch (parseError) {
124
+ errors.push({
125
+ type: "parse_error",
126
+ message: `Parse error: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
127
+ });
128
+ return { valid: false, errors, warnings };
129
+ }
130
+
131
+ // C2: Strict AST whitelist mode - reject dynamic property access and call expressions
132
+ if (this.options.strictAstWhitelist) {
133
+ this.validateAstWhitelist(ast, errors);
134
+ }
135
+
136
+ // C4: Bytecode compilation verification - verify script compiles correctly
137
+ this.verifyCompilation(code, errors);
138
+
139
+ // Check for forbidden globals using regex patterns (fallback)
140
+ this.checkForForbiddenGlobals(code, errors);
141
+
142
+ // Check for forbidden syntax patterns
143
+ this.checkForForbiddenSyntax(code, errors, warnings);
144
+
145
+ // Check for potentially unsafe patterns
146
+ this.checkForPotentiallyUnsafePatterns(code, warnings);
147
+
148
+ return {
149
+ valid: errors.length === 0,
150
+ errors,
151
+ warnings,
152
+ };
153
+ }
154
+
155
+ /**
156
+ * C2: Validate AST against strict whitelist - reject dynamic property access,
157
+ * call expressions, and other potentially dangerous patterns.
158
+ */
159
+ private validateAstWhitelist(
160
+ ast: acorn.Node,
161
+ errors: ScriptValidationError[],
162
+ ): void {
163
+ const walkNode = (node: acorn.Node): void => {
164
+ const n = node as acorn.Node & { type: string };
165
+
166
+ // Reject MemberExpression (e.g., obj.prop, obj[key])
167
+ if (n.type === "MemberExpression") {
168
+ errors.push({
169
+ type: "forbidden_syntax",
170
+ message: "Dynamic property access is not allowed in strict whitelist mode",
171
+ });
172
+ }
173
+
174
+ // Reject CallExpression (e.g., fn(), obj.method())
175
+ if (n.type === "CallExpression") {
176
+ errors.push({
177
+ type: "forbidden_syntax",
178
+ message: "Call expressions are not allowed in strict whitelist mode",
179
+ });
180
+ }
181
+
182
+ // Reject NewExpression (e.g., new Foo())
183
+ if (n.type === "NewExpression") {
184
+ errors.push({
185
+ type: "forbidden_syntax",
186
+ message: "Constructor calls are not allowed in strict whitelist mode",
187
+ });
188
+ }
189
+
190
+ // Reject UpdateExpression (++a, a--, etc.)
191
+ if (n.type === "UpdateExpression") {
192
+ errors.push({
193
+ type: "forbidden_syntax",
194
+ message: "Update expressions are not allowed in strict whitelist mode",
195
+ });
196
+ }
197
+
198
+ // Reject AssignmentExpression (a = b, a += b, etc.)
199
+ if (n.type === "AssignmentExpression") {
200
+ errors.push({
201
+ type: "forbidden_syntax",
202
+ message: "Assignment expressions are not allowed in strict whitelist mode",
203
+ });
204
+ }
205
+
206
+ // Reject ForInStatement and ForOfStatement
207
+ if (n.type === "ForInStatement" || n.type === "ForOfStatement") {
208
+ errors.push({
209
+ type: "forbidden_syntax",
210
+ message: "For-in/for-of loops are not allowed in strict whitelist mode",
211
+ });
212
+ }
213
+
214
+ // Recurse into children
215
+ for (const key of Object.keys(n as object)) {
216
+ const value = (n as unknown as Record<string, unknown>)[key];
217
+ if (value && typeof value === "object") {
218
+ if (Array.isArray(value)) {
219
+ for (const child of value) {
220
+ if (child && typeof child === "object" && "type" in child) {
221
+ walkNode(child as acorn.Node);
222
+ }
223
+ }
224
+ } else if ("type" in value) {
225
+ walkNode(value as acorn.Node);
226
+ }
227
+ }
228
+ }
229
+ };
230
+
231
+ // Cast to access body property (Program node)
232
+ const programNode = ast as unknown as { body: acorn.Node[] };
233
+ for (const node of programNode.body) {
234
+ walkNode(node);
235
+ }
236
+ }
237
+
238
+ /**
239
+ * C4: Verify script compiles to bytecode correctly. If compilation fails,
240
+ * the script cannot be executed safely.
241
+ */
242
+ private verifyCompilation(code: string, errors: ScriptValidationError[]): void {
243
+ try {
244
+ const wrappedCode = `(function(){ ${code} })()`;
245
+ new vm.Script(wrappedCode, {
246
+ filename: "compile-check.js",
247
+ });
248
+ } catch (compileError) {
249
+ errors.push({
250
+ type: "parse_error",
251
+ message: `Compilation verification failed: ${compileError instanceof Error ? compileError.message : String(compileError)}`,
252
+ });
253
+ }
254
+ }
255
+
256
+ private checkForForbiddenGlobals(code: string, errors: ScriptValidationError[]): void {
257
+ // Check each forbidden global pattern
258
+ for (const forbidden of FORBIDDEN_GLOBALS) {
259
+ if (forbidden.includes(".")) {
260
+ // Check for member expressions like Math.random, process.exit
261
+ const [obj, prop] = forbidden.split(".");
262
+ const pattern = new RegExp(`\\b${obj}\\s*\\.\\s*${prop}\\b`);
263
+ if (pattern.test(code)) {
264
+ errors.push({
265
+ type: "forbidden_global",
266
+ message: `Forbidden global access: '${forbidden}'`,
267
+ });
268
+ }
269
+ } else {
270
+ // Check for simple identifiers
271
+ // But avoid false positives like "myDate" matching "Date"
272
+ const pattern = new RegExp(`\\b${forbidden}\\b`);
273
+ if (pattern.test(code)) {
274
+ errors.push({
275
+ type: "forbidden_global",
276
+ message: `Forbidden global: '${forbidden}'`,
277
+ });
278
+ }
279
+ }
280
+ }
281
+ }
282
+
283
+ private checkForForbiddenSyntax(
284
+ code: string,
285
+ errors: ScriptValidationError[],
286
+ warnings: ScriptValidationWarning[],
287
+ ): void {
288
+ // Check for eval()
289
+ if (/\beval\s*\(/.test(code)) {
290
+ errors.push({
291
+ type: "forbidden_syntax",
292
+ message: "eval() is not allowed",
293
+ });
294
+ }
295
+
296
+ // Check for Function constructor
297
+ if (/\bnew\s+Function\s*\(/.test(code) || /\bFunction\s*\(\s*['"`]/.test(code)) {
298
+ errors.push({
299
+ type: "forbidden_syntax",
300
+ message: "Function constructor is not allowed",
301
+ });
302
+ }
303
+
304
+ // Check for AsyncFunction constructor
305
+ if (/\bnew\s+AsyncFunction\s*\(/.test(code) || /\bAsyncFunction\s*\(\s*['"`]/.test(code)) {
306
+ errors.push({
307
+ type: "forbidden_syntax",
308
+ message: "AsyncFunction constructor is not allowed",
309
+ });
310
+ }
311
+
312
+ // Check for GeneratorFunction constructor
313
+ if (/\bnew\s+GeneratorFunction\s*\(/.test(code)) {
314
+ errors.push({
315
+ type: "forbidden_syntax",
316
+ message: "GeneratorFunction constructor is not allowed",
317
+ });
318
+ }
319
+
320
+ // Check for Promise constructor - warn but don't block
321
+ if (/\bnew\s+Promise\s*\(/.test(code)) {
322
+ warnings.push({
323
+ type: "potentially_unsafe",
324
+ message: "Direct Promise constructor usage - consider using async/await instead",
325
+ });
326
+ }
327
+ }
328
+
329
+ private checkForPotentiallyUnsafePatterns(code: string, warnings: ScriptValidationWarning[]): void {
330
+ // Check for try-catch with broad catch - warn
331
+ if (/\bcatch\s*\(\s*\)\s*\{/.test(code)) {
332
+ warnings.push({
333
+ type: "potentially_unsafe",
334
+ message: "Broad catch clause - consider catching specific error types",
335
+ });
336
+ }
337
+
338
+ // Check for nested function declarations - warn about potential complexity
339
+ if (/function\s+\w+\s*\([^)]*\)\s*\{[^}]*function\s+/.test(code)) {
340
+ warnings.push({
341
+ type: "potentially_unsafe",
342
+ message: "Nested function declaration - consider extracting to module level",
343
+ });
344
+ }
345
+
346
+ // Check for with statement - deprecated and potentially unsafe
347
+ if (/\bwith\s*\(/.test(code)) {
348
+ warnings.push({
349
+ type: "potentially_unsafe",
350
+ message: "with statement is deprecated and potentially unsafe",
351
+ });
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Execute a script with validation.
357
+ * @param code - The JavaScript code to execute
358
+ * @param options - Execution options
359
+ * @returns The execution result
360
+ */
361
+ execute(code: string, options?: DynamicScriptOptions): ScriptExecutionResult {
362
+ const startTime = Date.now();
363
+ const timeout = options?.timeout ?? this.defaultTimeout;
364
+
365
+ // Validate first
366
+ const validation = this.validate(code);
367
+ if (!validation.valid) {
368
+ return {
369
+ success: false,
370
+ error: validation.errors.map((e) => e.message).join("; "),
371
+ executionTime: Date.now() - startTime,
372
+ validation,
373
+ };
374
+ }
375
+
376
+ try {
377
+ const value = this.sandbox.execute(code, timeout);
378
+ return {
379
+ success: true,
380
+ value,
381
+ executionTime: Date.now() - startTime,
382
+ validation,
383
+ };
384
+ } catch (error) {
385
+ return {
386
+ success: false,
387
+ error: error instanceof Error ? error.message : String(error),
388
+ executionTime: Date.now() - startTime,
389
+ validation,
390
+ };
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Execute an async script with validation.
396
+ * @param code - The JavaScript code to execute (must be async or return Promise)
397
+ * @param options - Execution options
398
+ * @returns Promise resolving to the execution result
399
+ */
400
+ async executeAsync(code: string, options?: DynamicScriptOptions): Promise<ScriptExecutionResult> {
401
+ const startTime = Date.now();
402
+ const timeout = options?.timeout ?? this.defaultTimeout;
403
+
404
+ // Wrap in async IIFE for async/await support
405
+ const asyncCode = `(async () => { ${code} })()`;
406
+
407
+ // Validate the wrapped code (not the original code)
408
+ const validation = this.validate(asyncCode);
409
+ if (!validation.valid) {
410
+ return {
411
+ success: false,
412
+ error: validation.errors.map((e) => e.message).join("; "),
413
+ executionTime: Date.now() - startTime,
414
+ validation,
415
+ };
416
+ }
417
+
418
+ try {
419
+ // Execute using vm directly for async support
420
+ const script = new vm.Script(asyncCode, {
421
+ filename: "workflow.js",
422
+ });
423
+
424
+ const result = await script.runInContext(this.sandbox.getContext(), {
425
+ timeout,
426
+ displayErrors: true,
427
+ });
428
+ return {
429
+ success: true,
430
+ value: result,
431
+ executionTime: Date.now() - startTime,
432
+ validation,
433
+ };
434
+ } catch (error) {
435
+ return {
436
+ success: false,
437
+ error: error instanceof Error ? error.message : String(error),
438
+ executionTime: Date.now() - startTime,
439
+ validation,
440
+ };
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Execute a script without validation (assumes pre-validated).
446
+ * Use with caution - prefer execute() for untrusted scripts.
447
+ */
448
+ executeUnchecked(code: string, timeout?: number): ScriptExecutionResult {
449
+ const startTime = Date.now();
450
+
451
+ try {
452
+ const value = this.sandbox.execute(code, timeout ?? this.defaultTimeout);
453
+ return {
454
+ success: true,
455
+ value,
456
+ executionTime: Date.now() - startTime,
457
+ validation: { valid: true, errors: [], warnings: [] },
458
+ };
459
+ } catch (error) {
460
+ return {
461
+ success: false,
462
+ error: error instanceof Error ? error.message : String(error),
463
+ executionTime: Date.now() - startTime,
464
+ validation: { valid: true, errors: [], warnings: [] },
465
+ };
466
+ }
467
+ }
468
+
469
+ /**
470
+ * Get the list of forbidden globals for documentation.
471
+ */
472
+ getForbiddenGlobals(): readonly string[] {
473
+ return FORBIDDEN_GLOBALS;
474
+ }
475
+ }
476
+
477
+ /**
478
+ * Create a pre-configured script runner for workflow execution.
479
+ */
480
+ export function createScriptRunner(options?: DynamicScriptOptions): DynamicScriptRunner {
481
+ return new DynamicScriptRunner(options);
482
+ }
@@ -4,6 +4,8 @@ import { appendEvent } from "../state/event-log.ts";
4
4
  import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
5
5
  import { checkProcessLiveness, isActiveRunStatus } from "./process-status.ts";
6
6
  import { readCrewAgents } from "./crew-agent-records.ts";
7
+ import { logInternalError } from "../utils/internal-error.ts";
8
+ import { sleepSync } from "../utils/sleep.ts";
7
9
 
8
10
  export type ForegroundControlRequestType = "interrupt" | "status";
9
11
 
@@ -59,24 +61,92 @@ export function readForegroundControlStatus(manifest: TeamRunManifest, tasks: Te
59
61
 
60
62
  export function writeForegroundInterruptRequest(manifest: TeamRunManifest, reason = "User requested foreground interrupt."): ForegroundControlRequest {
61
63
  const controlPath = foregroundControlPath(manifest);
64
+ const lockDir = `${controlPath}.lock`;
65
+ const pidFile = path.join(lockDir, "pid");
62
66
  let requests: ForegroundControlRequest[] = [];
63
- if (fs.existsSync(controlPath)) {
64
- try {
65
- const parsed = JSON.parse(fs.readFileSync(controlPath, "utf-8")) as { requests?: ForegroundControlRequest[] };
66
- requests = Array.isArray(parsed.requests) ? parsed.requests : [];
67
- } catch {
68
- requests = [];
67
+
68
+ // FIX: Use file locking to prevent race condition in read-modify-write
69
+ // Added stale lock detection similar to event-log.ts
70
+ const acquireLock = (): void => {
71
+ const timeout = 5000;
72
+ const staleMs = 10000;
73
+ const start = Date.now();
74
+ while (true) {
75
+ try {
76
+ fs.mkdirSync(lockDir, { recursive: true });
77
+ try { fs.writeFileSync(pidFile, String(process.pid), "utf-8"); } catch { /* best-effort */ }
78
+ break;
79
+ } catch {
80
+ if (Date.now() - start > timeout) {
81
+ // Check if lock is stale before giving up
82
+ try {
83
+ const raw = fs.readFileSync(pidFile, "utf-8").trim();
84
+ const ownerPid = Number.parseInt(raw, 10);
85
+ if (!Number.isNaN(ownerPid) && ownerPid !== process.pid) {
86
+ let alive = false;
87
+ try { process.kill(ownerPid, 0); alive = true; } catch { /* dead */ }
88
+ if (!alive) {
89
+ // Lock is stale — clear it and retry
90
+ fs.rmSync(lockDir, { recursive: true, force: true });
91
+ continue;
92
+ }
93
+ // Lock held by live process — throw
94
+ const err = new Error(`Foreground control lock timeout for ${controlPath}`);
95
+ logInternalError("foreground-control.lock-timeout", err, `controlPath=${controlPath}`);
96
+ throw err;
97
+ }
98
+ } catch { /* no pid file — continue to throw */ }
99
+ const err = new Error(`Foreground control lock timeout for ${controlPath}`);
100
+ logInternalError("foreground-control.lock-timeout", err, `controlPath=${controlPath}`);
101
+ throw err;
102
+ }
103
+ // Stale detection: if the owning process is dead, remove the stale lock
104
+ try {
105
+ const raw = fs.readFileSync(pidFile, "utf-8").trim();
106
+ const ownerPid = Number.parseInt(raw, 10);
107
+ if (!Number.isNaN(ownerPid) && ownerPid !== process.pid) {
108
+ let alive = false;
109
+ try { process.kill(ownerPid, 0); alive = true; } catch { /* dead */ }
110
+ if (!alive) {
111
+ const stat = fs.statSync(lockDir);
112
+ if (Date.now() - stat.mtimeMs > staleMs) {
113
+ fs.rmSync(lockDir, { recursive: true, force: true });
114
+ continue;
115
+ }
116
+ }
117
+ }
118
+ } catch { /* no pid file — fall through to sleep */ }
119
+ // Brief sleep to avoid CPU spinning
120
+ sleepSync(10);
121
+ }
69
122
  }
70
- }
71
- const request: ForegroundControlRequest = {
72
- id: `fg_${Date.now().toString(36)}_${Math.random().toString(16).slice(2, 10)}`,
73
- type: "interrupt",
74
- createdAt: new Date().toISOString(),
75
- reason,
76
- acknowledged: false,
77
123
  };
78
- fs.mkdirSync(path.dirname(controlPath), { recursive: true });
79
- fs.writeFileSync(controlPath, `${JSON.stringify({ requests: [...requests, request] }, null, 2)}\n`, "utf-8");
80
- appendEvent(manifest.eventsPath, { type: "foreground.interrupt_requested", runId: manifest.runId, message: reason, data: { requestId: request.id, controlPath } });
81
- return request;
124
+ const releaseLock = (): void => {
125
+ try { fs.rmSync(lockDir, { recursive: true, force: true }); } catch { /* best-effort */ }
126
+ };
127
+
128
+ acquireLock();
129
+ try {
130
+ if (fs.existsSync(controlPath)) {
131
+ try {
132
+ const parsed = JSON.parse(fs.readFileSync(controlPath, "utf-8")) as { requests?: ForegroundControlRequest[] };
133
+ requests = Array.isArray(parsed.requests) ? parsed.requests : [];
134
+ } catch {
135
+ requests = [];
136
+ }
137
+ }
138
+ const request: ForegroundControlRequest = {
139
+ id: `fg_${Date.now().toString(36)}_${Math.random().toString(16).slice(2, 10)}`,
140
+ type: "interrupt",
141
+ createdAt: new Date().toISOString(),
142
+ reason,
143
+ acknowledged: false,
144
+ };
145
+ fs.mkdirSync(path.dirname(controlPath), { recursive: true });
146
+ fs.writeFileSync(controlPath, `${JSON.stringify({ requests: [...requests, request] }, null, 2)}\n`, "utf-8");
147
+ appendEvent(manifest.eventsPath, { type: "foreground.interrupt_requested", runId: manifest.runId, message: reason, data: { requestId: request.id, controlPath } });
148
+ return request;
149
+ } finally {
150
+ releaseLock();
151
+ }
82
152
  }