@selesai/code 0.8.4 → 0.8.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 (147) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +7 -1
  3. package/dist/core/slash-commands.js +1 -0
  4. package/dist/defaults/models.json +46 -55
  5. package/dist/defaults/settings.json +7 -8
  6. package/dist/extensions/agent-browser.test.ts +178 -2
  7. package/dist/extensions/agent-browser.ts +2 -0
  8. package/dist/extensions/copy-turn.test.ts +229 -0
  9. package/dist/extensions/copy-turn.ts +11 -0
  10. package/dist/extensions/grep-app/index.test.ts +356 -1
  11. package/dist/extensions/grep-app/index.ts +6 -1
  12. package/dist/extensions/handoff-new.test.ts +351 -161
  13. package/dist/extensions/handoff-new.ts +3 -0
  14. package/dist/extensions/inline-skills.test.ts +93 -0
  15. package/dist/extensions/inline-skills.ts +3 -0
  16. package/dist/extensions/pi-subagents/CHANGELOG.md +26 -3
  17. package/dist/extensions/pi-subagents/README.md +2 -0
  18. package/dist/extensions/pi-subagents/agents/builder.md +5 -2
  19. package/dist/extensions/pi-subagents/agents/commentator.md +1 -1
  20. package/dist/extensions/pi-subagents/docs/configuration.md +54 -2
  21. package/dist/extensions/pi-subagents/docs/observability.md +1 -1
  22. package/dist/extensions/pi-subagents/docs/tool-reference.md +1 -1
  23. package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
  24. package/dist/extensions/pi-subagents/package-lock.json +2 -2
  25. package/dist/extensions/pi-subagents/package.json +1 -1
  26. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +1 -1
  27. package/dist/extensions/pi-subagents/src/agents/agents.ts +9 -3
  28. package/dist/extensions/pi-subagents/src/extension/config.ts +12 -0
  29. package/dist/extensions/pi-subagents/src/extension/doctor.ts +40 -0
  30. package/dist/extensions/pi-subagents/src/extension/index.ts +104 -14
  31. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +5 -0
  32. package/dist/extensions/pi-subagents/src/extension/rpc.ts +3 -9
  33. package/dist/extensions/pi-subagents/src/extension/schemas.ts +2 -2
  34. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -1
  35. package/dist/extensions/pi-subagents/src/missions/lifecycle.ts +4 -7
  36. package/dist/extensions/pi-subagents/src/missions/store.ts +4 -4
  37. package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +6 -2
  38. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +374 -0
  39. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +9 -5
  40. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +117 -29
  41. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +12 -6
  42. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +7 -1
  43. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +14 -5
  44. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +33 -15
  45. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +5 -2
  46. package/dist/extensions/pi-subagents/src/runs/background/owned-process-tree.ts +104 -0
  47. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +17 -3
  48. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +19 -1
  49. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +2 -2
  50. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +5 -2
  51. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +12 -6
  52. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +3 -3
  53. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +84 -36
  54. package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +2 -1
  55. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +37 -2
  56. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +135 -22
  57. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +12 -0
  58. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +7 -4
  59. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +171 -0
  60. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +653 -196
  61. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +13 -4
  62. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +286 -0
  63. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +23 -0
  64. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  65. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +44 -1
  66. package/dist/extensions/pi-subagents/src/runs/shared/run-fanout-budget.ts +280 -0
  67. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +4 -2
  68. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +19 -3
  69. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +17 -5
  70. package/dist/extensions/pi-subagents/src/shared/session-lineage.ts +71 -0
  71. package/dist/extensions/pi-subagents/src/shared/types.ts +108 -0
  72. package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -1
  73. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +7 -5
  74. package/dist/extensions/pi-subagents/src/tui/fleet.ts +226 -13
  75. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +22 -5
  76. package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +28 -0
  77. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +87 -0
  78. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +96 -6
  79. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +28 -5
  80. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +8 -5
  81. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +83 -2
  82. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +211 -15
  83. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +41 -3
  84. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +314 -10
  85. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +31 -1
  86. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +317 -0
  87. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +21 -2
  88. package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +16 -1
  89. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +6 -1
  90. package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +75 -4
  91. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +18 -0
  92. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +29 -1
  93. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +31 -5
  94. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +139 -1
  95. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +10 -0
  96. package/dist/extensions/pi-subagents/test/unit/handoff-adoption.test.ts +103 -0
  97. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +22 -0
  98. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +2 -1
  99. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +171 -0
  100. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +30 -4
  101. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +14 -0
  102. package/dist/extensions/pi-subagents/test/unit/owned-process-tree.test.ts +69 -0
  103. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +83 -0
  104. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +2 -2
  105. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +55 -2
  106. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +12 -1
  107. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +7 -7
  108. package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +121 -0
  109. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +9 -0
  110. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +4 -3
  111. package/dist/extensions/pi-subagents/test/unit/session-lineage.test.ts +73 -0
  112. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +28 -0
  113. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +10 -1
  114. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +39 -0
  115. package/dist/extensions/pi-subagents/test/unit/timeout-defaults.test.ts +65 -0
  116. package/dist/extensions/pi-subagents/test/unit/workflow-auto-relaunch.test.ts +45 -0
  117. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +61 -0
  118. package/dist/extensions/ponytail/index.js +11 -0
  119. package/dist/extensions/ponytail/ponytail-config.cjs +2 -0
  120. package/dist/extensions/ponytail/ponytail-instructions.cjs +6 -0
  121. package/dist/extensions/ponytail/test/extension.test.js +274 -140
  122. package/dist/extensions/ponytail/test/helpers.test.js +280 -92
  123. package/dist/extensions/question/index.ts +18 -0
  124. package/dist/extensions/question/question-list.ts +22 -0
  125. package/dist/extensions/question/tests/batch.test.ts +103 -65
  126. package/dist/extensions/question/tests/helpers.test.ts +68 -35
  127. package/dist/extensions/question/tests/question-list.test.ts +506 -204
  128. package/dist/extensions/question/tests/row-layout.test.ts +180 -114
  129. package/dist/extensions/question/tests/schemas.test.ts +42 -19
  130. package/dist/extensions/question/tests/shortcuts.test.ts +106 -84
  131. package/dist/extensions/question/tests/wizard.test.ts +771 -50
  132. package/dist/extensions/rtk.test.ts +180 -1
  133. package/dist/extensions/tokenin-onboarding.ts +3 -0
  134. package/dist/extensions/undo.test.ts +720 -0
  135. package/dist/extensions/undo.ts +6 -0
  136. package/dist/extensions/web-agent-onboarding.test.ts +415 -0
  137. package/dist/extensions/workflow/extension.ts +3 -3
  138. package/dist/extensions/workflow/modes.ts +41 -19
  139. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  140. package/dist/modes/interactive/interactive-mode.js +48 -1
  141. package/dist/skills/pi-subagents/references/execution-controls.md +1 -1
  142. package/docs/plans/workflow-autoloop-reference.md +1 -2
  143. package/docs/plans/workflow-handoff-carryover.md +108 -0
  144. package/docs/quickstart.md +27 -43
  145. package/docs/settings.md +4 -0
  146. package/docs/usage.md +1 -0
  147. package/package.json +5 -1
@@ -41,6 +41,7 @@ import { ACTIVE_RUN_INDEX_DIR } from "../../src/runs/background/active-run-index
41
41
  import { CHILD_WATCHDOG_STATUS_EVENT } from "../../src/watchdog/child-status.ts";
42
42
  import { WAIT_TOOL_ENABLED_ENV } from "../../src/runs/background/wait-config.ts";
43
43
  import { TOOL_BUDGET_ENV, TOOL_BUDGET_ZERO_AUTH_ENV } from "../../src/runs/shared/tool-budget.ts";
44
+ import { createRunFanoutBudget, encodeRunFanoutBudgetDescriptor, RUN_FANOUT_BUDGET_ENV } from "../../src/runs/shared/run-fanout-budget.ts";
44
45
  import { MainWatchdogRuntime } from "../../src/watchdog/runtime.ts";
45
46
  import { MAX_CHILD_PENDING_LINE_BYTES, MAX_CHILD_STDERR_BYTES } from "../../src/runs/shared/child-protocol.ts";
46
47
  import {
@@ -66,6 +67,7 @@ interface ProgressSummary {
66
67
  agent: string;
67
68
  index: number;
68
69
  status: string;
70
+ task?: string;
69
71
  activityState?: string;
70
72
  lastActivityAt?: number;
71
73
  currentTool?: string;
@@ -79,6 +81,7 @@ interface ProgressSummary {
79
81
  }
80
82
 
81
83
  interface ArtifactPaths {
84
+ inputPath?: string;
82
85
  outputPath: string;
83
86
  transcriptPath?: string;
84
87
  metadataPath?: string;
@@ -103,6 +106,7 @@ interface RuntimeAcknowledgedExtensions {
103
106
  interface RunSyncResult {
104
107
  exitCode: number;
105
108
  agent: string;
109
+ task?: string;
106
110
  messages: unknown[];
107
111
  error?: string;
108
112
  protocolError?: { code?: string; stream?: string; limitBytes?: number; observedBytes?: number };
@@ -322,6 +326,13 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
322
326
  return readCall().args;
323
327
  }
324
328
 
329
+ function readAllCallArgs(): string[][] {
330
+ return fs.readdirSync(mockPi.dir)
331
+ .filter((name) => name.startsWith("call-") && name.endsWith(".json"))
332
+ .sort()
333
+ .map((name) => (JSON.parse(fs.readFileSync(path.join(mockPi.dir, name), "utf-8")) as MockPiCallRecord).args);
334
+ }
335
+
325
336
  function makeExecutor(
326
337
  agents = [makeAgent("echo")],
327
338
  config: Record<string, unknown> = {},
@@ -387,6 +398,19 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
387
398
  assert.equal(mockPi.callCount(), 0);
388
399
  });
389
400
 
401
+ it("rejects internal fan-out fields from public workflows", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
402
+ const executor = makeExecutor([makeAgent("echo")]);
403
+ for (const params of [
404
+ { workflowScript: `return runs.run("main", { agent: "echo", task: "work" })`, runFanoutBudget: { version: 1 } },
405
+ { workflowScript: `return runs.run("main", { agent: "echo", task: "work" })`, runFanoutAdmitted: true },
406
+ ] as const) {
407
+ const result = await executor.executePublic("private-fanout", params, new AbortController().signal, undefined, makeMinimalCtx(tempDir));
408
+ assert.equal(result.isError, true);
409
+ assert.match(result.content[0]?.text ?? "", /does not accept internal run fan-out fields/);
410
+ }
411
+ assert.equal(mockPi.callCount(), 0);
412
+ });
413
+
390
414
  it("allows schedule.create to carry the required workflowScript target", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
391
415
  let forwarded;
392
416
  const executor = makeExecutor([makeAgent("echo")], {}, false, undefined, true, new Map(), undefined, async (params) => {
@@ -494,6 +518,105 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
494
518
  fs.rmSync(resultPath, { force: true });
495
519
  });
496
520
 
521
+ it("runs an external CLI workflow child with subagents.defaultModel configured", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
522
+ const markerPath = path.join(tempDir, "external-started");
523
+ const executor = makeExecutor([
524
+ makeAgent("external", {
525
+ runner: { type: "external-cli", command: process.execPath, args: ["-e", `require("node:fs").writeFileSync(${JSON.stringify(markerPath)}, "started"); process.stdout.write("external result")`] },
526
+ model: "mock/default-model",
527
+ modelSource: { type: "subagents.defaultModel", scope: "user", path: "/settings.json", model: "mock/default-model" },
528
+ }),
529
+ ]);
530
+ const ctx = { ...makeMinimalCtx(tempDir), model: { provider: "mock", id: "parent-model" } };
531
+ const started = await executor.execute(
532
+ `external-workflow-${Date.now()}`,
533
+ { workflowScript: `return await runs.run("external", { agent: "external", task: "Run external", async: true });` },
534
+ new AbortController().signal,
535
+ undefined,
536
+ ctx,
537
+ );
538
+
539
+ assert.equal(started.isError, undefined);
540
+ assert.ok(started.details.asyncId);
541
+ const workflowResultPath = path.join(DIRS.results, `${started.details.asyncId}.json`);
542
+ let workflowResult: { state?: string; results?: Array<{ output?: string; runId?: string }> } = {};
543
+ for (let attempt = 0; attempt < 100; attempt++) {
544
+ if (fs.existsSync(workflowResultPath)) workflowResult = JSON.parse(fs.readFileSync(workflowResultPath, "utf-8"));
545
+ if (workflowResult.state === "complete" || workflowResult.state === "failed") break;
546
+ await new Promise((resolve) => setTimeout(resolve, 20));
547
+ }
548
+ assert.equal(workflowResult.state, "complete");
549
+ assert.match(workflowResult.results?.[0]?.output ?? "", /Async: external/);
550
+ for (let attempt = 0; attempt < 100 && !fs.existsSync(markerPath); attempt++) {
551
+ await new Promise((resolve) => setTimeout(resolve, 20));
552
+ }
553
+ assert.equal(fs.readFileSync(markerPath, "utf-8"), "started");
554
+ assert.equal(mockPi.callCount(), 0);
555
+
556
+ fs.rmSync(started.details.asyncDir!, { recursive: true, force: true });
557
+ fs.rmSync(workflowResultPath, { force: true });
558
+ });
559
+
560
+ it("rejects explicit model overrides for external CLI agents", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
561
+ const executor = makeExecutor([
562
+ makeAgent("external", {
563
+ runner: { type: "external-cli", command: process.execPath, args: ["-e", "process.stdout.write('unreachable')"] },
564
+ }),
565
+ ]);
566
+ const result = await executor.execute(
567
+ "external-explicit-model",
568
+ { agent: "external", task: "Run external", async: true, model: "mock/override" },
569
+ new AbortController().signal,
570
+ undefined,
571
+ makeMinimalCtx(tempDir),
572
+ );
573
+
574
+ assert.equal(result.isError, true);
575
+ assert.match(result.content[0]?.text ?? "", /does not support: model override/);
576
+ assert.equal(mockPi.callCount(), 0);
577
+ });
578
+
579
+ it("rejects external CLI agent models that differ from inherited subagents.defaultModel", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
580
+ const executor = makeExecutor([
581
+ makeAgent("external", {
582
+ runner: { type: "external-cli", command: process.execPath, args: ["-e", "process.stdout.write('unreachable')"] },
583
+ model: "mock/override-model",
584
+ modelSource: { type: "subagents.defaultModel", scope: "user", path: "/settings.json", model: "mock/default-model" },
585
+ }),
586
+ ]);
587
+ const result = await executor.execute(
588
+ "external-agent-override-model",
589
+ { agent: "external", task: "Run external", async: true },
590
+ new AbortController().signal,
591
+ undefined,
592
+ makeMinimalCtx(tempDir),
593
+ );
594
+
595
+ assert.equal(result.isError, true);
596
+ assert.match(result.content[0]?.text ?? "", /does not support: model override/);
597
+ assert.equal(mockPi.callCount(), 0);
598
+ });
599
+
600
+ it("rejects external CLI agent models that equal inherited subagents.defaultModel without provenance", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
601
+ const executor = makeExecutor([
602
+ makeAgent("external", {
603
+ runner: { type: "external-cli", command: process.execPath, args: ["-e", "process.stdout.write('unreachable')"] },
604
+ model: "mock/default-model",
605
+ }),
606
+ ]);
607
+ const result = await executor.execute(
608
+ "external-agent-same-value-override-model",
609
+ { agent: "external", task: "Run external", async: true },
610
+ new AbortController().signal,
611
+ undefined,
612
+ makeMinimalCtx(tempDir),
613
+ );
614
+
615
+ assert.equal(result.isError, true);
616
+ assert.match(result.content[0]?.text ?? "", /does not support: model override/);
617
+ assert.equal(mockPi.callCount(), 0);
618
+ });
619
+
497
620
  it("projects live child activity into async workflow status", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
498
621
  mockPi.onCall({
499
622
  steps: [
@@ -728,9 +851,9 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
728
851
  assert.equal(childResult.parentWorkflowRunId, workflowRunId);
729
852
  assert.equal(childResult.workflowKey, "background");
730
853
  assert.equal(fs.existsSync(workflowStepSessionFile), true);
731
- fs.rmSync(started.details.asyncDir!, { recursive: true, force: true });
854
+ fs.rmSync(started.details.asyncDir!, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
732
855
  fs.rmSync(workflowResultPath, { force: true });
733
- fs.rmSync(childDir, { recursive: true, force: true });
856
+ fs.rmSync(childDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
734
857
  fs.rmSync(path.join(DIRS.results, `${childRunId}.json`), { force: true });
735
858
  });
736
859
 
@@ -821,7 +944,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
821
944
  const missionFiles = fs.readdirSync(missionDir).filter((entry) => entry.endsWith(".json"));
822
945
  assert.equal(missionFiles.length, 1);
823
946
  const mission = JSON.parse(fs.readFileSync(path.join(missionDir, missionFiles[0]!), "utf-8")) as { objective?: string };
824
- assert.equal(mission.objective, "Scan auth");
947
+ assert.equal(mission.objective, utils.PROMPT_REDACTED);
825
948
  assert.deepEqual(result.details.workflow?.trace.filter((entry) => entry.state === "completed").map((entry) => entry.key), ["scan", "review"]);
826
949
  });
827
950
 
@@ -1015,6 +1138,30 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
1015
1138
  assert.deepEqual(result.details.workflow?.trace.filter((entry) => entry.state !== "started").map(({ state }) => state).sort(), ["completed", "failed"]);
1016
1139
  });
1017
1140
 
1141
+ it("rejects an over-limit runs.all batch before launching any workflow child", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
1142
+ const executor = makeExecutor([makeAgent("echo")], { maxSubagentSpawnsPerRun: 1 });
1143
+
1144
+ const result = await executor.execute(
1145
+ "scripted-workflow-fanout-limit",
1146
+ {
1147
+ async: false,
1148
+ workflowScript: `return await runs.all([
1149
+ { key: "first", agent: "echo", task: "First task" },
1150
+ { key: "second", agent: "echo", task: "Second task" }
1151
+ ]);`,
1152
+ },
1153
+ new AbortController().signal,
1154
+ undefined,
1155
+ makeMinimalCtx(tempDir),
1156
+ );
1157
+
1158
+ assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
1159
+ assert.equal(mockPi.callCount(), 0);
1160
+ const children = result.details.workflow?.value as Array<{ ok: boolean; error?: string }>;
1161
+ assert.deepEqual(children.map(({ ok }) => ok), [false, false]);
1162
+ for (const child of children) assert.match(child.error ?? "", /workflow\[second\].*0\/1 used; 2 requested, 1 remaining/);
1163
+ });
1164
+
1018
1165
  it("runs a direct child gate as host-verified acceptance", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
1019
1166
  const markerFile = "direct-gate.txt";
1020
1167
  const markerPath = path.join(tempDir, markerFile);
@@ -1779,6 +1926,46 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
1779
1926
  assert.equal(mockPi.callCount(), 1);
1780
1927
  });
1781
1928
 
1929
+ it("qualifies inherited nested claims with the generated nested run id", async () => {
1930
+ mockPi.onCall({ output: "nested completed" });
1931
+ const descriptor = createRunFanoutBudget("root-run", 2);
1932
+ const previous = process.env[RUN_FANOUT_BUDGET_ENV];
1933
+ try {
1934
+ process.env[RUN_FANOUT_BUDGET_ENV] = encodeRunFanoutBudgetDescriptor({ ...descriptor, parentPath: "tasks[0]" });
1935
+ const executor = makeExecutor([makeAgent("echo")]);
1936
+ const result = await executor.execute("nested", { agent: "echo", task: "Nested work" }, new AbortController().signal, undefined, makeMinimalCtx(tempDir));
1937
+
1938
+ assert.equal(result.isError, undefined, result.content[0]?.text ?? "nested run failed");
1939
+ const claims = fs.readdirSync(path.join(descriptor.directory, "claims"));
1940
+ assert.equal(claims.length, 1);
1941
+ const claim = JSON.parse(fs.readFileSync(path.join(descriptor.directory, "claims", claims[0]!), "utf-8")) as { path: string };
1942
+ assert.match(claim.path, /^tasks\[0\]\/[a-f0-9]{8}\/single$/);
1943
+ } finally {
1944
+ if (previous === undefined) delete process.env[RUN_FANOUT_BUDGET_ENV];
1945
+ else process.env[RUN_FANOUT_BUDGET_ENV] = previous;
1946
+ fs.rmSync(descriptor.directory, { recursive: true, force: true });
1947
+ }
1948
+ });
1949
+
1950
+ it("rejects an over-limit static run fan-out before creating session artifacts", async () => {
1951
+ const sessionDir = path.join(tempDir, "run-fanout-preflight");
1952
+ const executor = makeExecutor([makeAgent("echo"), makeAgent("second")], { maxSubagentSpawnsPerRun: 1 });
1953
+ const result = await executor.execute(
1954
+ "run-fanout-preflight",
1955
+ { tasks: [{ agent: "echo", task: "First" }, { agent: "second", task: "Second" }], sessionDir },
1956
+ new AbortController().signal,
1957
+ undefined,
1958
+ makeMinimalCtx(tempDir),
1959
+ );
1960
+
1961
+ assert.equal(result.isError, true);
1962
+ assert.match(result.content[0]?.text ?? "", /Run fan-out limit reached at tasks\[1\] \(0\/1 used; 2 requested, 1 remaining\)/);
1963
+ assert.deepEqual(result.details.runFanoutBudget, { used: 0, limit: 1, remaining: 1 });
1964
+ assert.equal(result.details.runFanoutRejection?.path, "tasks[1]");
1965
+ assert.equal(fs.existsSync(sessionDir), false);
1966
+ assert.equal(mockPi.callCount(), 0);
1967
+ });
1968
+
1782
1969
  it("reports structured spawn-budget usage through status", async () => {
1783
1970
  const spawnState = { sessionId: "session-123", count: 3, configuredLimit: 4, granted: 1, grantHistory: [] };
1784
1971
  const executor = makeExecutor([makeAgent("echo")], { maxSubagentSpawnsPerSession: 4 }, false, spawnState);
@@ -2457,12 +2644,15 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
2457
2644
  assert.match(result.content[0]?.text ?? "", /Implemented/);
2458
2645
  });
2459
2646
 
2460
- it("returns error for unknown agent", async () => {
2647
+ it("returns error for unknown agent without retaining the prompt", async () => {
2461
2648
  const agents = makeAgentConfigs(["echo"]);
2462
- const result = await runSync(tempDir, agents, "nonexistent", "Do something", {});
2649
+ const sentinel = "PROMPT_AUDIT_SENTINEL_UNKNOWN";
2650
+ const result = await runSync(tempDir, agents, "nonexistent", sentinel, {});
2463
2651
 
2464
2652
  assert.equal(result.exitCode, 1);
2465
2653
  assert.ok(result.error?.includes("Unknown agent"));
2654
+ assert.equal(result.task, "[prompt redacted]");
2655
+ assert.doesNotMatch(JSON.stringify(result), new RegExp(sentinel));
2466
2656
  });
2467
2657
 
2468
2658
 
@@ -2578,6 +2768,29 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
2578
2768
  assert.equal(mockPi.callCount(), 2);
2579
2769
  });
2580
2770
 
2771
+ it("escalates to file task delivery after a zero-activity SIGKILL startup exit", { skip: process.platform === "win32" ? "POSIX child signal reporting is unavailable on Windows" : false }, async () => {
2772
+ mockPi.onCall({ signal: "SIGKILL" });
2773
+ mockPi.onCall({ output: "Recovered via file delivery" });
2774
+ const agents = [makeAgent("worker", { model: "openai/gpt-5-mini" })];
2775
+
2776
+ const result = await runSync(tempDir, agents, "worker", "Do work", {
2777
+ runId: "startup-sigkill-file-delivery-sync",
2778
+ acceptance: false,
2779
+ });
2780
+
2781
+ assert.equal(result.exitCode, 0);
2782
+ assert.equal(result.finalOutput, "Recovered via file delivery");
2783
+ assert.equal(mockPi.callCount(), 2);
2784
+ const [firstArgs, retryArgs] = readAllCallArgs();
2785
+ assert.ok(firstArgs?.includes("Task: Do work"), "first attempt should deliver the task inline");
2786
+ const retryTaskArg = retryArgs?.at(-1) ?? "";
2787
+ assert.ok(
2788
+ retryTaskArg.startsWith("@") && retryTaskArg.endsWith("task.md"),
2789
+ `retry should reference a task file instead of inline argv, got: ${retryTaskArg}`,
2790
+ );
2791
+ assert.match(result.progress.recentOutput.join("\n"), /\[startup-retry\].*file task delivery/i);
2792
+ });
2793
+
2581
2794
  it("does not retry a non-zero exit after tool activity", async () => {
2582
2795
  mockPi.onCall({ jsonl: [events.toolStart("read", { path: "package.json" })], exitCode: 1 });
2583
2796
  mockPi.onCall({ output: "must not run" });
@@ -2981,7 +3194,9 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
2981
3194
 
2982
3195
  assert.equal(result.exitCode, 1);
2983
3196
  assert.match(result.error ?? "", /429 quota exceeded/);
2984
- assert.deepEqual(result.modelAttempts?.map((attempt) => attempt.success), [false]);
3197
+ // No fallback model configured: the last candidate is retried on the same
3198
+ // model up to MODEL_RETRY_DELAYS_MS.length times before giving up.
3199
+ assert.deepEqual(result.modelAttempts?.map((attempt) => attempt.success), [false, false, false, false]);
2985
3200
  });
2986
3201
 
2987
3202
  it("treats recovered child tool errors as successful foreground runs", async () => {
@@ -3089,7 +3304,9 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
3089
3304
  });
3090
3305
 
3091
3306
  assert.equal(result.exitCode, 1);
3092
- assert.deepEqual(result.modelAttempts?.map((attempt) => attempt.success), [false, false]);
3307
+ // Primary fails, fallback fails, then the last (fallback) candidate is
3308
+ // retried on the same model up to MODEL_RETRY_DELAYS_MS.length times.
3309
+ assert.deepEqual(result.modelAttempts?.map((attempt) => attempt.success), [false, false, false, false, false]);
3093
3310
  assert.match(result.error ?? "", /429 quota exceeded/);
3094
3311
  });
3095
3312
 
@@ -3341,7 +3558,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
3341
3558
  assert.equal(mockPi.callCount(), 0);
3342
3559
  });
3343
3560
 
3344
- it("writes artifacts when configured", async () => {
3561
+ it("writes artifacts without retaining the effective prompt", async () => {
3345
3562
  mockPi.onCall({
3346
3563
  output: "Result text",
3347
3564
  runtimeAcknowledgedExtensions: { version: 1, source: "child-runtime", ids: ["ext.ok"], omitted: 0 },
@@ -3349,8 +3566,9 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
3349
3566
  const privateExtension = path.join(tempDir, "extensions", "private-extension.ts");
3350
3567
  const agents = [makeAgent("echo", { extensions: [privateExtension] })];
3351
3568
  const artifactsDir = path.join(tempDir, "artifacts");
3569
+ const sentinel = "PROMPT_AUDIT_SENTINEL_1021";
3352
3570
 
3353
- const result = await runSync(tempDir, agents, "echo", "Task", {
3571
+ const result = await runSync(tempDir, agents, "echo", sentinel, {
3354
3572
  runId: "test-run",
3355
3573
  artifactsDir,
3356
3574
  artifactConfig: { enabled: true, includeInput: true, includeOutput: true, includeMetadata: true },
@@ -3358,16 +3576,27 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
3358
3576
 
3359
3577
  assert.equal(result.exitCode, 0);
3360
3578
  assert.ok(result.artifactPaths, "should have artifact paths");
3579
+ assert.ok(result.artifactPaths.inputPath, "should have a redacted input artifact");
3580
+ assert.doesNotMatch(fs.readFileSync(result.artifactPaths.inputPath, "utf-8"), new RegExp(sentinel));
3581
+ assert.match(fs.readFileSync(result.artifactPaths.inputPath, "utf-8"), /live Prompt Audit only/);
3361
3582
  assert.ok(result.transcriptPath, "should expose transcript path on the result");
3362
3583
  assert.equal(result.transcriptPath, result.artifactPaths.transcriptPath);
3363
3584
  assert.ok(fs.existsSync(result.transcriptPath), "transcript should be written");
3364
3585
  const transcript = fs.readFileSync(result.transcriptPath, "utf-8").trim().split("\n").map((line) => JSON.parse(line) as { recordType?: string; source?: string; text?: string });
3365
3586
  assert.equal(transcript[0]?.recordType, "message");
3366
3587
  assert.equal(transcript[0]?.source, "foreground");
3588
+ assert.match(transcript[0]?.text ?? "", /live Prompt Audit only/);
3589
+ assert.doesNotMatch(fs.readFileSync(result.transcriptPath, "utf-8"), new RegExp(sentinel));
3367
3590
  assert.match(transcript.at(-1)?.text ?? "", /^Result text/);
3368
3591
  assert.equal(result.transcriptError, undefined);
3369
3592
  assert.ok(fs.existsSync(artifactsDir), "artifacts dir should exist");
3370
- const metadata = JSON.parse(fs.readFileSync(result.artifactPaths.metadataPath, "utf-8")) as { launchContractDigest?: string; launchResolvedExtensions?: LaunchResolvedExtensions; runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions };
3593
+ const metadataText = fs.readFileSync(result.artifactPaths.metadataPath, "utf-8");
3594
+ const metadata = JSON.parse(metadataText) as { task?: string; launchContractDigest?: string; launchResolvedExtensions?: LaunchResolvedExtensions; runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions };
3595
+ assert.doesNotMatch(metadataText, new RegExp(sentinel));
3596
+ assert.equal(metadata.task, "[prompt redacted]");
3597
+ assert.equal(result.task, "[prompt redacted]");
3598
+ assert.equal(result.progress.task, "[prompt redacted]");
3599
+ assert.match(readCallArgs().join("\n"), new RegExp(sentinel));
3371
3600
  assert.equal(metadata.launchContractDigest, result.launchContractDigest);
3372
3601
  assert.equal(result.launchResolvedExtensions?.source, "launch-resolved");
3373
3602
  assert.equal(result.launchResolvedExtensions?.disableAmbientExtensions, true);
@@ -3756,6 +3985,81 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
3756
3985
  assert.equal(agentResult.details?.timeoutMs, 4_000);
3757
3986
  });
3758
3987
 
3988
+ it("threads the global config timeout default from deps.config, without overriding explicit or agent values", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
3989
+ const NINETY_MIN = 90 * 60 * 1000;
3990
+ mockPi.onCall({ output: "config default" });
3991
+ mockPi.onCall({ output: "explicit over config" });
3992
+ mockPi.onCall({ output: "agent over config" });
3993
+ mockPi.onCall({ output: "invalid config ignored" });
3994
+
3995
+ // A global config.timeoutMs replaces the built-in 30-minute foreground backstop.
3996
+ const configExecutor = makeExecutor([makeAgent("echo")], { timeoutMs: NINETY_MIN });
3997
+ const configResult = await configExecutor.execute(
3998
+ "config-timeout-default",
3999
+ { agent: "echo", task: "Task", async: false },
4000
+ new AbortController().signal,
4001
+ undefined,
4002
+ makeMinimalCtx(tempDir),
4003
+ );
4004
+ assert.equal(configResult.details?.timeoutMs, NINETY_MIN);
4005
+
4006
+ // An explicit call value still wins over the global config default.
4007
+ const explicitResult = await configExecutor.execute(
4008
+ "config-timeout-explicit",
4009
+ { agent: "echo", task: "Task", async: false, timeoutMs: 2_000 },
4010
+ new AbortController().signal,
4011
+ undefined,
4012
+ makeMinimalCtx(tempDir),
4013
+ );
4014
+ assert.equal(explicitResult.details?.timeoutMs, 2_000);
4015
+
4016
+ // An agent frontmatter default still wins over the global config default (single launches).
4017
+ const agentResult = await makeExecutor([makeAgent("echo", { defaultTimeoutMs: 4_000 })], { timeoutMs: NINETY_MIN }).execute(
4018
+ "config-timeout-agent-default",
4019
+ { agent: "echo", task: "Task", async: false },
4020
+ new AbortController().signal,
4021
+ undefined,
4022
+ makeMinimalCtx(tempDir),
4023
+ );
4024
+ assert.equal(agentResult.details?.timeoutMs, 4_000);
4025
+
4026
+ // An invalid config value is ignored -> falls back to the built-in 30-minute default.
4027
+ const invalidResult = await makeExecutor([makeAgent("echo")], { timeoutMs: -1 }).execute(
4028
+ "config-timeout-invalid",
4029
+ { agent: "echo", task: "Task", async: false },
4030
+ new AbortController().signal,
4031
+ undefined,
4032
+ makeMinimalCtx(tempDir),
4033
+ );
4034
+ assert.equal(invalidResult.details?.timeoutMs, executorMod?.DEFAULT_FOREGROUND_TIMEOUT_MS);
4035
+ });
4036
+
4037
+ it("applies the global config timeout default to foreground workflow scripts", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
4038
+ mockPi.onCall({ delay: 5_000, output: "too late" });
4039
+ mockPi.onCall({ delay: 5_000, output: "too late" });
4040
+ const executor = makeExecutor([makeAgent("echo")], { timeoutMs: 250 });
4041
+
4042
+ const configResult = await executor.execute(
4043
+ "workflow-config-timeout-default",
4044
+ { async: false, workflowScript: `return await runs.run("slow", { agent: "echo", task: "Wait" });` },
4045
+ new AbortController().signal,
4046
+ undefined,
4047
+ makeMinimalCtx(tempDir),
4048
+ );
4049
+ assert.equal(configResult.isError, true);
4050
+ assert.match(configResult.content[0]?.text ?? "", /Workflow script timed out after 250ms/);
4051
+
4052
+ const explicitResult = await executor.execute(
4053
+ "workflow-config-timeout-explicit",
4054
+ { async: false, timeoutMs: 150, workflowScript: `return await runs.run("slow", { agent: "echo", task: "Wait" });` },
4055
+ new AbortController().signal,
4056
+ undefined,
4057
+ makeMinimalCtx(tempDir),
4058
+ );
4059
+ assert.equal(explicitResult.isError, true);
4060
+ assert.match(explicitResult.content[0]?.text ?? "", /Workflow script timed out after 150ms/);
4061
+ });
4062
+
3759
4063
  it("runs omitted async launches in the background when the global default is enabled", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
3760
4064
  const executor = makeExecutor([makeAgent("echo")], {}, true);
3761
4065
 
@@ -245,6 +245,7 @@ describe("acceptance gates", () => {
245
245
  assert.match(prompt, /Patch the bug/);
246
246
  assert.match(prompt, /```acceptance-report/);
247
247
  assert.match(prompt, /array fields contain strings/);
248
+ assert.match(prompt, /empty-string entr(?:y|ies).*\[\s*""\s*\]/i);
248
249
  assert.match(prompt, /criteriaSatisfied\[\]\.status.*satisfied, not-satisfied, not-applicable/);
249
250
  assert.match(prompt, /commandsRun\[\]\.result.*passed, failed, not-run/);
250
251
  assert.match(prompt, /manualNotes.*optional strings.*empty string.*does not satisfy.*manual-notes/);
@@ -482,7 +483,7 @@ describe("acceptance gates", () => {
482
483
  [{ criteriaSatisfied: [{ id: "criterion-1", status: "maybe", evidence: "proof" }] }, /criteriaSatisfied\[0\]\.status.*got "maybe"/],
483
484
  [{ criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "proof", confidence: 1 }] }, /criteriaSatisfied\[0\]\.confidence: unsupported acceptance criterion field/],
484
485
  [{ criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "proof" }, { id: "Criterion_1", status: "satisfied", evidence: "proof" }] }, /duplicate normalized criterion id 'criterion-1'/],
485
- [{ reviewFindings: [""] }, /reviewFindings\[0\]: expected non-empty string; got ""/],
486
+ [{ reviewFindings: [{}] }, /reviewFindings\[0\]: expected non-empty string; got object/],
486
487
  [{ noStagedFiles: "yes" }, /noStagedFiles: expected boolean; got "yes"/],
487
488
  ] as const) {
488
489
  const parsed = parseAcceptanceReport(report(overrides));
@@ -491,6 +492,35 @@ describe("acceptance gates", () => {
491
492
  }
492
493
  });
493
494
 
495
+ it("tolerates empty-string entries in string-array fields by dropping them at parse time", () => {
496
+ // A model writing ["\"\""] for "no items" must not fail the whole report.
497
+ for (const field of ["changedFiles", "testsAddedOrUpdated", "validationOutput", "residualRisks", "reviewFindings"] as const) {
498
+ const parsed = parseAcceptanceReport(report({ [field]: [""] }));
499
+ assert.equal(parsed.error, undefined, `${field}: ["\"\""] should parse`);
500
+ assert.deepEqual(parsed.report?.[field], [], `${field}: empty-string entries dropped`);
501
+ }
502
+
503
+ // Mixed arrays keep only meaningful entries.
504
+ const mixed = parseAcceptanceReport(report({ reviewFindings: ["blocker: file.ts:12", ""] }));
505
+ assert.equal(mixed.error, undefined);
506
+ assert.deepEqual(mixed.report?.reviewFindings, ["blocker: file.ts:12"]);
507
+
508
+ // Whitespace-only entries are treated as empty.
509
+ const whitespace = parseAcceptanceReport(report({ validationOutput: [" "] }));
510
+ assert.equal(whitespace.error, undefined);
511
+ assert.deepEqual(whitespace.report?.validationOutput, []);
512
+
513
+ // A single string is still coerced to a one-item array.
514
+ const coerced = parseAcceptanceReport(report({ changedFiles: "src/file.ts" }));
515
+ assert.equal(coerced.error, undefined);
516
+ assert.deepEqual(coerced.report?.changedFiles, ["src/file.ts"]);
517
+
518
+ // Structural garbage (non-string entries) is still rejected.
519
+ const invalid = parseAcceptanceReport(report({ reviewFindings: [{}] }));
520
+ assert.equal(invalid.report, undefined);
521
+ assert.match(invalid.error ?? "", /reviewFindings\[0\]: expected non-empty string; got object/);
522
+ });
523
+
494
524
  it("accepts empty optional note strings without treating them as evidence", async () => {
495
525
  for (const field of ["manualNotes", "notes"] as const) {
496
526
  const parsed = parseAcceptanceReport(report({ notes: undefined, [field]: "" }));