@llblab/pi-actors 0.38.1 → 0.40.0

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 (97) hide show
  1. package/AGENTS.md +11 -4
  2. package/BACKLOG.md +2 -11
  3. package/CHANGELOG.md +51 -0
  4. package/README.md +120 -131
  5. package/dist/index.js +6 -4
  6. package/dist/lib/async-runs.d.ts +4 -0
  7. package/dist/lib/async-runs.js +112 -17
  8. package/dist/lib/command-templates.d.ts +9 -0
  9. package/dist/lib/command-templates.js +92 -11
  10. package/dist/lib/config.js +0 -5
  11. package/dist/lib/execution.d.ts +31 -0
  12. package/dist/lib/execution.js +145 -12
  13. package/dist/lib/file-state.d.ts +1 -0
  14. package/dist/lib/file-state.js +91 -3
  15. package/dist/lib/observability.d.ts +1 -1
  16. package/dist/lib/observability.js +7 -4
  17. package/dist/lib/pi.d.ts +1 -1
  18. package/dist/lib/pi.js +2 -2
  19. package/dist/lib/prompts.d.ts +1 -2
  20. package/dist/lib/prompts.js +2 -3
  21. package/dist/lib/recipes-context.js +17 -9
  22. package/dist/lib/recipes-discovery.js +11 -5
  23. package/dist/lib/recipes-references.d.ts +1 -1
  24. package/dist/lib/recipes-references.js +4 -5
  25. package/dist/lib/recipes-usage.d.ts +2 -0
  26. package/dist/lib/recipes-usage.js +35 -21
  27. package/dist/lib/registry.d.ts +0 -2
  28. package/dist/lib/registry.js +33 -10
  29. package/dist/lib/runs-ownership.d.ts +7 -0
  30. package/dist/lib/runs-ownership.js +82 -0
  31. package/dist/lib/runs-process.d.ts +17 -2
  32. package/dist/lib/runs-process.js +99 -11
  33. package/dist/lib/runs-retention.d.ts +3 -0
  34. package/dist/lib/runs-retention.js +18 -3
  35. package/dist/lib/runs-start.d.ts +2 -2
  36. package/dist/lib/runs-start.js +51 -17
  37. package/dist/lib/runs-status.d.ts +1 -1
  38. package/dist/lib/runs-status.js +8 -6
  39. package/dist/lib/runtime.js +69 -13
  40. package/dist/lib/tools-inspect.d.ts +2 -0
  41. package/dist/lib/tools-inspect.js +39 -2
  42. package/dist/lib/tools-register.js +0 -1
  43. package/dist/lib/tools-spawn.js +3 -2
  44. package/dist/lib/tools.d.ts +1 -0
  45. package/dist/lib/tools.js +3 -0
  46. package/dist/pi-actors/index.js +1 -0
  47. package/dist/recipes/subagent-judge.json +2 -1
  48. package/dist/recipes/subagent-merge.json +2 -1
  49. package/dist/recipes/subagent-normalize.json +2 -1
  50. package/dist/recipes/subagent-review-coordinator.json +1 -1
  51. package/dist/recipes/subagent-review.json +2 -1
  52. package/dist/recipes/subagent-verify.json +2 -1
  53. package/dist/scripts/async-runner.mjs +274 -6
  54. package/dist/scripts/build-dist.mjs +14 -1
  55. package/dist/skills/actors/SKILL.md +11 -7
  56. package/dist/skills/swarm/SKILL.md +1 -1
  57. package/docs/actor-messages.md +1 -1
  58. package/docs/async-runs.md +14 -5
  59. package/docs/command-templates.md +4 -2
  60. package/docs/recipe-library.md +1 -0
  61. package/docs/template-recipes.md +5 -7
  62. package/docs/tool-registry.md +4 -2
  63. package/index.ts +18 -7
  64. package/lib/async-runs.ts +138 -19
  65. package/lib/command-templates.ts +132 -13
  66. package/lib/config.ts +0 -4
  67. package/lib/execution.ts +198 -13
  68. package/lib/file-state.ts +106 -3
  69. package/lib/observability.ts +11 -5
  70. package/lib/pi.ts +3 -3
  71. package/lib/prompts.ts +2 -4
  72. package/lib/recipes-context.ts +17 -9
  73. package/lib/recipes-discovery.ts +10 -5
  74. package/lib/recipes-references.ts +5 -6
  75. package/lib/recipes-usage.ts +36 -20
  76. package/lib/registry.ts +43 -13
  77. package/lib/runs-ownership.ts +117 -0
  78. package/lib/runs-process.ts +138 -16
  79. package/lib/runs-retention.ts +22 -2
  80. package/lib/runs-start.ts +89 -31
  81. package/lib/runs-status.ts +15 -6
  82. package/lib/runtime.ts +64 -12
  83. package/lib/tools-inspect.ts +46 -4
  84. package/lib/tools-register.ts +0 -3
  85. package/lib/tools-spawn.ts +5 -5
  86. package/lib/tools.ts +8 -0
  87. package/package.json +2 -2
  88. package/recipes/subagent-judge.json +2 -1
  89. package/recipes/subagent-merge.json +2 -1
  90. package/recipes/subagent-normalize.json +2 -1
  91. package/recipes/subagent-review-coordinator.json +1 -1
  92. package/recipes/subagent-review.json +2 -1
  93. package/recipes/subagent-verify.json +2 -1
  94. package/scripts/async-runner.mjs +274 -6
  95. package/scripts/build-dist.mjs +14 -1
  96. package/skills/actors/SKILL.md +11 -7
  97. package/skills/swarm/SKILL.md +1 -1
@@ -7,6 +7,7 @@ import { type CurrentPolicyProvenance } from "./model-context.ts";
7
7
  import * as RecipesReferences from "./recipes-references.ts";
8
8
  import { type RunArtifactDeclaration } from "./runs-artifacts.ts";
9
9
  import { type RunOutboxEvent } from "./runs-outbox.ts";
10
+ import { type RunProcessIdentity } from "./runs-process.ts";
10
11
  import * as RunsIndex from "./runs-index.ts";
11
12
  import { type ProcessRunInboxResult, type RunInboxMessage, type RunInboxStatus } from "./runs-mailbox.ts";
12
13
  import { type SendRunMessageOptions } from "./runs-messages.ts";
@@ -35,6 +36,7 @@ export interface AsyncRunStartParams {
35
36
  when?: boolean | string;
36
37
  timeout?: number | string;
37
38
  delay?: number | string;
39
+ accept_output?: "review_evidence";
38
40
  output?: string;
39
41
  artifacts?: Record<string, RunArtifactDeclaration>;
40
42
  mailbox?: RecipesReferences.TemplateRecipeMailbox;
@@ -68,6 +70,7 @@ export interface AsyncRunMeta {
68
70
  control?: AsyncRunControlEndpoint;
69
71
  mailbox?: RecipesReferences.TemplateRecipeMailbox;
70
72
  model_policy?: CurrentPolicyProvenance;
73
+ process_identity?: RunProcessIdentity;
71
74
  recipe_context_records?: RecipesReferences.TemplateRecipeContextRecord[];
72
75
  retire_when?: "children_terminal";
73
76
  }
@@ -112,6 +115,7 @@ export type { SendRunMessageOptions } from "./runs-messages.ts";
112
115
  export declare function sendRunMessage(runOrDir: string, message: string, options?: SendRunMessageOptions): Promise<Record<string, unknown>>;
113
116
  export { getRunProcessSignalPlan } from "./runs-control.ts";
114
117
  export type { RunProcessSignalPlan } from "./runs-control.ts";
118
+ export declare function markRunTerminalNotificationHandled(stateDir: string, status: string): void;
115
119
  export declare function cancelRun(runOrDir: string): Record<string, unknown>;
116
120
  export declare function archiveRun(runOrDir: string): Record<string, unknown>;
117
121
  export declare function pruneRun(runOrDir: string, options?: {
@@ -3,8 +3,8 @@
3
3
  * Owns launch, state observation, listing, message/control facade methods, and retention while runs-* subdomains own narrower run internals.
4
4
  */
5
5
  import { spawn } from "node:child_process";
6
- import { closeSync, existsSync, mkdirSync, openSync, writeFileSync, } from "node:fs";
7
- import { basename, dirname, extname, join, resolve } from "node:path";
6
+ import { closeSync, existsSync, mkdirSync, openSync, readdirSync, statSync, writeFileSync, } from "node:fs";
7
+ import { basename, dirname, extname, join, relative, resolve } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { writeJsonAtomic } from "./file-state.js";
10
10
  import { CURRENT_MODEL_VALUE_KEY, CURRENT_THINKING_VALUE_KEY, describeCurrentPolicyProvenance, } from "./model-context.js";
@@ -15,8 +15,9 @@ import { resolveArtifactPaths, } from "./runs-artifacts.js";
15
15
  import { safeRunId } from "./runs-identity.js";
16
16
  import { buildTerminalProgress, markTerminalHandled, signalOwnedRunProcess, } from "./runs-control.js";
17
17
  import { buildRunOutboxEventPayload, parseRunOutboxEventLine, } from "./runs-outbox.js";
18
+ import { claimRunStateDirectory } from "./runs-ownership.js";
18
19
  import { archiveTerminalRun, pruneTerminalRun } from "./runs-retention.js";
19
- import { isAlive, isWithinRunnerIdentityGrace, pidMatchesRun, } from "./runs-process.js";
20
+ import { captureRunProcessIdentity, verifyRunProcessIdentity, } from "./runs-process.js";
20
21
  import * as RunsStart from "./runs-start.js";
21
22
  import * as RunsIndex from "./runs-index.js";
22
23
  import { claimRunInboxMessageInStateDir, parseRunInboxLine, processRunInboxMessagesInStateDir, runInboxFile, updateRunInboxMessageStatusInStateDir, } from "./runs-mailbox.js";
@@ -57,6 +58,7 @@ function resolveRunTemplate(params) {
57
58
  "when",
58
59
  "timeout",
59
60
  "delay",
61
+ "accept_output",
60
62
  "output",
61
63
  "retry",
62
64
  "failure",
@@ -228,6 +230,7 @@ export function startRun(params, cwd) {
228
230
  mkdirSync(stateDir, { recursive: true });
229
231
  const releaseStartLock = acquireStateStartLock(stateDir);
230
232
  try {
233
+ claimRunStateDirectory(stateDir, run);
231
234
  assertNoActiveRunState(stateDir);
232
235
  prepareStateDirForStart(stateDir);
233
236
  const stdout = join(stateDir, "stdout.log");
@@ -289,6 +292,9 @@ export function startRun(params, cwd) {
289
292
  closeSync(outFd);
290
293
  closeSync(errFd);
291
294
  meta.pid = child.pid ?? 0;
295
+ const processIdentity = captureRunProcessIdentity(meta.pid, cwd, stateDir, RUNNER_PATH);
296
+ if (processIdentity)
297
+ meta.process_identity = processIdentity;
292
298
  writeJsonAtomic(join(stateDir, "run.json"), meta);
293
299
  writeJsonAtomic(join(stateDir, "progress.json"), {
294
300
  completed: 0,
@@ -384,14 +390,18 @@ export async function sendRunMessage(runOrDir, message, options = {}) {
384
390
  const status = getRunStatus(runOrDir);
385
391
  const stateDir = String(status.state_dir);
386
392
  const run = String(status.run ?? runOrDir);
387
- if (status.status !== "running")
388
- throw new Error(`Run is not running: ${run}`);
389
393
  const pid = Number(status.pid || 0);
390
- if (!pid || !isAlive(pid))
391
- throw new Error(`Run pid is not alive: ${run}`);
392
- if (!pidMatchesRun(pid, String(status.cwd), stateDir, RUNNER_PATH) &&
393
- !isWithinRunnerIdentityGrace(status, RUNNER_IDENTITY_GRACE_MS))
394
- throw new Error(`Run pid owner mismatch: ${run}`);
394
+ const identity = verifyRunProcessIdentity(pid, status.process_identity);
395
+ if (status.status !== "running") {
396
+ if (identity.status === "owner_mismatch" ||
397
+ identity.status === "unsupported_proof") {
398
+ throw new Error(`Run process identity ${identity.status}: ${run}`);
399
+ }
400
+ throw new Error(`Run is not running: ${run}`);
401
+ }
402
+ if (!identity.valid) {
403
+ throw new Error(`Run process identity ${identity.status}: ${run}`);
404
+ }
395
405
  return deliverRunMessage(status, run, stateDir, message, options);
396
406
  }
397
407
  export { getRunProcessSignalPlan } from "./runs-control.js";
@@ -402,26 +412,111 @@ function markTerminalProgress(stateDir, phase) {
402
412
  : undefined;
403
413
  writeJsonAtomic(join(stateDir, "progress.json"), buildTerminalProgress(progress, phase));
404
414
  }
415
+ function finalizeInterruptedReviewEvidence(stateDir, phase, signal) {
416
+ const evidencePath = join(stateDir, "review-evidence.json");
417
+ const manifest = readJson(evidencePath);
418
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest))
419
+ return;
420
+ const record = manifest;
421
+ if (!Array.isArray(record.commands))
422
+ return;
423
+ const completedAt = new Date().toISOString();
424
+ const effectiveExitCode = signal === "SIGKILL" ? 137 : 143;
425
+ const commands = record.commands.map((command) => {
426
+ if (!command || typeof command !== "object" || Array.isArray(command)) {
427
+ return command;
428
+ }
429
+ const entry = command;
430
+ if (entry.status !== "running" || typeof entry.id !== "string")
431
+ return entry;
432
+ const captureDir = join(stateDir, "captures", entry.id);
433
+ const attempts = existsSync(captureDir)
434
+ ? readdirSync(captureDir)
435
+ .filter((name) => /^attempt-\d+$/.test(name))
436
+ .sort()
437
+ .map((name, index) => {
438
+ const attemptDir = join(captureDir, name);
439
+ const stdoutFile = join(attemptDir, "stdout.log");
440
+ const stderrFile = join(attemptDir, "stderr.log");
441
+ return {
442
+ attempt: index + 1,
443
+ stdout: {
444
+ path: relative(stateDir, stdoutFile),
445
+ bytes: existsSync(stdoutFile) ? statSync(stdoutFile).size : 0,
446
+ },
447
+ stderr: {
448
+ path: relative(stateDir, stderrFile),
449
+ bytes: existsSync(stderrFile) ? statSync(stderrFile).size : 0,
450
+ },
451
+ };
452
+ })
453
+ : [];
454
+ return {
455
+ ...entry,
456
+ status: phase,
457
+ completed_at: completedAt,
458
+ attempts,
459
+ effective_exit_code: effectiveExitCode,
460
+ killed: true,
461
+ ...(entry.semantic_acceptance === "pending"
462
+ ? { semantic_acceptance: "interrupted" }
463
+ : {}),
464
+ };
465
+ });
466
+ writeJsonAtomic(evidencePath, {
467
+ ...record,
468
+ status: phase,
469
+ commands,
470
+ updated_at: completedAt,
471
+ });
472
+ }
405
473
  function stopRun(runOrDir, signal, event) {
406
474
  const status = getRunStatus(runOrDir);
407
475
  const pid = Number(status.pid || 0);
408
476
  const stateDir = String(status.state_dir);
409
- if (status.status !== "running")
477
+ if (status.status !== "running" && status.status !== "exited") {
410
478
  return { stopped: false, reason: "not running", status };
411
- if (!pid || !isAlive(pid))
412
- return { stopped: false, reason: "pid not alive", status };
413
- if (!pidMatchesRun(pid, String(status.cwd), stateDir, RUNNER_PATH)) {
414
- return { stopped: false, reason: "pid owner mismatch", status };
479
+ }
480
+ const identity = verifyRunProcessIdentity(pid, status.process_identity);
481
+ if (status.status === "exited") {
482
+ if (identity.status === "owner_mismatch" ||
483
+ identity.status === "unsupported_proof") {
484
+ return {
485
+ stopped: false,
486
+ reason: identity.status.replaceAll("_", " "),
487
+ process_identity_status: identity.status,
488
+ status,
489
+ };
490
+ }
491
+ return { stopped: false, reason: "not running", status };
492
+ }
493
+ if (!identity.valid) {
494
+ return {
495
+ stopped: false,
496
+ reason: identity.status.replaceAll("_", " "),
497
+ process_identity_status: identity.status,
498
+ status,
499
+ };
415
500
  }
416
501
  const signalResult = signalOwnedRunProcess(pid, signal);
417
502
  writeFileSync(join(stateDir, "events.jsonl"), `${JSON.stringify({ event, pid, signal, ...signalResult, ts: new Date().toISOString() })}\n`, { flag: "a" });
418
503
  markTerminalHandled(stateDir, { event, signal });
419
- if (event === "run.kill")
504
+ if (event === "run.kill") {
505
+ finalizeInterruptedReviewEvidence(stateDir, "killed", signal);
420
506
  markTerminalProgress(stateDir, "killed");
421
- if (event === "run.cancel")
507
+ }
508
+ if (event === "run.cancel") {
509
+ finalizeInterruptedReviewEvidence(stateDir, "cancelled", signal);
422
510
  markTerminalProgress(stateDir, "cancelled");
511
+ }
423
512
  return { stopped: true, pid, signal, ...signalResult, state_dir: stateDir };
424
513
  }
514
+ export function markRunTerminalNotificationHandled(stateDir, status) {
515
+ markTerminalHandled(stateDir, {
516
+ event: "run.notification",
517
+ status,
518
+ });
519
+ }
425
520
  export function cancelRun(runOrDir) {
426
521
  const result = stopRun(runOrDir, "SIGTERM", "run.cancel");
427
522
  return Object.hasOwn(result, "stopped")
@@ -23,6 +23,7 @@ export interface CommandTemplateObjectConfig {
23
23
  defaults?: Record<string, unknown>;
24
24
  timeout?: number | string;
25
25
  delay?: number | string;
26
+ accept_output?: "review_evidence";
26
27
  output?: string;
27
28
  retry?: number | string;
28
29
  failure?: CommandTemplateFailureScope;
@@ -45,12 +46,20 @@ export interface CommandTemplateExecOptions {
45
46
  stdin?: string;
46
47
  killGrace?: number;
47
48
  retry?: number;
49
+ captureDir?: string;
50
+ captureLimitBytes?: number;
48
51
  }
49
52
  export interface CommandTemplateExecResult {
50
53
  stdout: string;
51
54
  stderr: string;
52
55
  code: number;
53
56
  killed: boolean;
57
+ stdoutBytes?: number;
58
+ stderrBytes?: number;
59
+ stdoutFile?: string;
60
+ stderrFile?: string;
61
+ stdoutTruncated?: boolean;
62
+ stderrTruncated?: boolean;
54
63
  }
55
64
  export type CommandTemplateRiskLabel = "risk.shell" | "risk.eval" | "risk.broad_fs_write" | "risk.destructive_fs" | "risk.network" | "risk.external_side_effect" | "risk.long_running" | "risk.platform_specific" | "risk.secret_touching";
56
65
  export type CommandTemplateExecCommand = (command: string, args: string[], options?: CommandTemplateExecOptions) => Promise<CommandTemplateExecResult>;
@@ -4,8 +4,10 @@
4
4
  * Owns portable command-template parsing, expansion, risk checks, retries, timeouts, and direct execution.
5
5
  */
6
6
  import { spawn } from "node:child_process";
7
- import { homedir } from "node:os";
8
- import { isAbsolute, resolve } from "node:path";
7
+ import { appendFileSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
8
+ import { homedir, tmpdir } from "node:os";
9
+ import { isAbsolute, join, resolve as resolvePath } from "node:path";
10
+ const DEFAULT_COMMAND_CAPTURE_LIMIT_BYTES = 1024 * 1024;
9
11
  const COMMAND_TEMPLATE_RISK_LABEL_ORDER = [
10
12
  "risk.shell",
11
13
  "risk.eval",
@@ -386,9 +388,9 @@ export function expandCommandTemplateExecutable(command, cwd) {
386
388
  if (command === "~")
387
389
  return homedir();
388
390
  if (command.startsWith("~/"))
389
- return resolve(homedir(), command.slice(2));
391
+ return resolvePath(homedir(), command.slice(2));
390
392
  if (command.includes("/") && !isAbsolute(command))
391
- return resolve(cwd, command);
393
+ return resolvePath(cwd, command);
392
394
  return command;
393
395
  }
394
396
  function evaluateCommandTemplateExpression(expression, values) {
@@ -592,13 +594,67 @@ export async function execCommandTemplate(command, args, options = {}) {
592
594
  killed: false,
593
595
  };
594
596
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
595
- const result = await execCommandTemplateOnce(command, args, options);
597
+ const attemptOptions = options.captureDir
598
+ ? {
599
+ ...options,
600
+ captureDir: join(options.captureDir, `attempt-${String(attempt).padStart(3, "0")}`),
601
+ }
602
+ : options;
603
+ const result = await execCommandTemplateOnce(command, args, attemptOptions);
596
604
  if (result.code === 0)
597
605
  return result;
598
606
  lastResult = result;
599
607
  }
600
608
  return lastResult;
601
609
  }
610
+ function trimCaptureTail(value, limit) {
611
+ if (value.length <= limit)
612
+ return value;
613
+ let start = value.length - limit;
614
+ while (start < value.length && (value[start] & 0xc0) === 0x80)
615
+ start += 1;
616
+ return value.subarray(start);
617
+ }
618
+ function createBoundedCommandCapture(stream, limit, getCaptureDir, persistCompleteStream) {
619
+ let bytes = 0;
620
+ let content = Buffer.alloc(0);
621
+ let file = persistCompleteStream
622
+ ? join(getCaptureDir(), `${stream}.log`)
623
+ : undefined;
624
+ if (file)
625
+ writeFileSync(file, Buffer.alloc(0));
626
+ let truncated = false;
627
+ return {
628
+ append(value) {
629
+ const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
630
+ bytes += chunk.length;
631
+ if (bytes > limit)
632
+ truncated = true;
633
+ if (!file && bytes <= limit) {
634
+ content = Buffer.concat([content, chunk]);
635
+ return;
636
+ }
637
+ if (!file) {
638
+ file = join(getCaptureDir(), `${stream}.log`);
639
+ writeFileSync(file, content);
640
+ }
641
+ appendFileSync(file, chunk);
642
+ content = trimCaptureTail(Buffer.concat([content, chunk]), limit);
643
+ },
644
+ result() {
645
+ if (!file && persistCompleteStream) {
646
+ file = join(getCaptureDir(), `${stream}.log`);
647
+ writeFileSync(file, content);
648
+ }
649
+ return {
650
+ bytes,
651
+ content: content.toString("utf8"),
652
+ ...(file ? { file } : {}),
653
+ truncated,
654
+ };
655
+ },
656
+ };
657
+ }
602
658
  function execCommandTemplateOnce(command, args, options = {}) {
603
659
  return new Promise((resolve) => {
604
660
  const proc = spawn(command, args, {
@@ -606,8 +662,20 @@ function execCommandTemplateOnce(command, args, options = {}) {
606
662
  shell: false,
607
663
  stdio: [options.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
608
664
  });
609
- let stdout = "";
610
- let stderr = "";
665
+ const captureLimit = Math.max(1, options.captureLimitBytes ?? DEFAULT_COMMAND_CAPTURE_LIMIT_BYTES);
666
+ let captureDir;
667
+ const getCaptureDir = () => {
668
+ if (captureDir)
669
+ return captureDir;
670
+ captureDir = options.captureDir
671
+ ? resolvePath(options.captureDir)
672
+ : mkdtempSync(join(tmpdir(), "pi-actors-command-"));
673
+ mkdirSync(captureDir, { recursive: true });
674
+ return captureDir;
675
+ };
676
+ const persistCompleteStreams = options.captureDir !== undefined;
677
+ const stdoutCapture = createBoundedCommandCapture("stdout", captureLimit, getCaptureDir, persistCompleteStreams);
678
+ const stderrCapture = createBoundedCommandCapture("stderr", captureLimit, getCaptureDir, persistCompleteStreams);
611
679
  let killed = false;
612
680
  let settled = false;
613
681
  let timeoutId;
@@ -632,7 +700,20 @@ function execCommandTemplateOnce(command, args, options = {}) {
632
700
  clearTimeout(killTimeoutId);
633
701
  if (options.signal)
634
702
  options.signal.removeEventListener("abort", killProcess);
635
- resolve({ stdout, stderr, code, killed });
703
+ const stdout = stdoutCapture.result();
704
+ const stderr = stderrCapture.result();
705
+ resolve({
706
+ stdout: stdout.content,
707
+ stderr: stderr.content,
708
+ code,
709
+ killed,
710
+ stdoutBytes: stdout.bytes,
711
+ stderrBytes: stderr.bytes,
712
+ ...(stdout.file ? { stdoutFile: stdout.file } : {}),
713
+ ...(stderr.file ? { stderrFile: stderr.file } : {}),
714
+ ...(stdout.truncated ? { stdoutTruncated: true } : {}),
715
+ ...(stderr.truncated ? { stderrTruncated: true } : {}),
716
+ });
636
717
  };
637
718
  if (options.signal) {
638
719
  if (options.signal.aborted)
@@ -643,16 +724,16 @@ function execCommandTemplateOnce(command, args, options = {}) {
643
724
  if (options.timeout !== undefined && options.timeout > 0)
644
725
  timeoutId = setTimeout(killProcess, options.timeout);
645
726
  proc.stdout?.on("data", (data) => {
646
- stdout += data.toString();
727
+ stdoutCapture.append(data);
647
728
  });
648
729
  proc.stderr?.on("data", (data) => {
649
- stderr += data.toString();
730
+ stderrCapture.append(data);
650
731
  });
651
732
  proc.stdin?.on("error", () => { });
652
733
  if (options.stdin !== undefined)
653
734
  proc.stdin?.end(options.stdin);
654
735
  proc.on("error", (error) => {
655
- stderr += error instanceof Error ? error.message : String(error);
736
+ stderrCapture.append(error instanceof Error ? error.message : String(error));
656
737
  settle(1);
657
738
  });
658
739
  proc.on("close", (code) => {
@@ -24,8 +24,6 @@ export function serializeTools(source) {
24
24
  entry.name = cfg.recipe.name;
25
25
  if (cfg.recipe?.async !== undefined)
26
26
  entry.async = cfg.recipe.async;
27
- if (cfg.recipe?.state_dir)
28
- entry.state_dir = cfg.recipe.state_dir;
29
27
  if (cfg.recipe?.values)
30
28
  entry.values = cfg.recipe.values;
31
29
  if (cfg.template)
@@ -113,9 +111,6 @@ export function normalizeStoredTool(key, value, reservedToolNames) {
113
111
  ? {
114
112
  name: recipeName,
115
113
  ...(typeof record.async === "boolean" ? { async: record.async } : {}),
116
- ...(typeof record.state_dir === "string" && record.state_dir.trim()
117
- ? { state_dir: record.state_dir.trim() }
118
- : {}),
119
114
  template,
120
115
  ...(record.values &&
121
116
  typeof record.values === "object" &&
@@ -7,17 +7,33 @@ import * as CommandTemplates from "./command-templates.ts";
7
7
  import type { RegisteredTool } from "./config.ts";
8
8
  export interface ToolExecOptions {
9
9
  actorRecipeContext?: CommandTemplates.CommandTemplateActorRecipeContext;
10
+ evidenceContext?: {
11
+ acceptOutput?: "review_evidence";
12
+ label?: string;
13
+ repeatIndex?: string;
14
+ };
10
15
  cwd?: string;
11
16
  signal?: AbortSignal;
12
17
  stdin?: string;
13
18
  timeout?: number;
14
19
  retry?: number;
20
+ captureDir?: string;
21
+ captureLimitBytes?: number;
15
22
  }
16
23
  export interface ToolExecResult {
17
24
  stdout: string;
18
25
  stderr: string;
19
26
  code: number;
20
27
  killed: boolean;
28
+ stdoutBytes?: number;
29
+ stderrBytes?: number;
30
+ stdoutFile?: string;
31
+ stderrFile?: string;
32
+ evidenceRef?: string;
33
+ stdoutTruncated?: boolean;
34
+ stderrTruncated?: boolean;
35
+ /** Complete internal stdout for downstream pipeline stdin; never returned to model-facing output. */
36
+ pipelineStdout?: string;
21
37
  }
22
38
  export interface BranchReport {
23
39
  code: number;
@@ -28,8 +44,14 @@ export interface BranchReport {
28
44
  status: "done" | "failed" | "timeout";
29
45
  stderr?: string;
30
46
  stderrBytes: number;
47
+ stderrCapturedBytes: number;
48
+ stderrFile?: string;
49
+ stderrTruncated?: boolean;
31
50
  stdout?: string;
32
51
  stdoutBytes: number;
52
+ stdoutCapturedBytes: number;
53
+ stdoutFile?: string;
54
+ stdoutTruncated?: boolean;
33
55
  }
34
56
  export interface SoftQuorumReport {
35
57
  coverage: number;
@@ -50,6 +72,14 @@ export interface RegisteredToolExecutionResult {
50
72
  command: string;
51
73
  fullOutputPath?: string;
52
74
  killed: boolean;
75
+ stderrBytes?: number;
76
+ stderrCapturedBytes?: number;
77
+ stderrFile?: string;
78
+ stderrTruncated?: boolean;
79
+ stdoutBytes?: number;
80
+ stdoutCapturedBytes?: number;
81
+ stdoutFile?: string;
82
+ stdoutTruncated?: boolean;
53
83
  nonCriticalFailures?: Array<{
54
84
  code: number;
55
85
  command: string;
@@ -64,4 +94,5 @@ export interface RegisteredToolExecutionResult {
64
94
  };
65
95
  }
66
96
  export type RegisteredToolExec = (command: string, args: string[], options?: ToolExecOptions) => Promise<ToolExecResult>;
97
+ export declare function applyOutputAcceptancePolicy(result: ToolExecResult, output: string | undefined): Promise<ToolExecResult>;
67
98
  export declare function executeRegisteredTool(cfg: RegisteredTool, params: Record<string, unknown>, exec: RegisteredToolExec, cwd: string, signal?: AbortSignal): Promise<RegisteredToolExecutionResult>;