@selesai/code 0.5.29 → 0.6.2

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 (94) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +1 -1
  3. package/dist/config.d.ts +16 -3
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +106 -5
  6. package/dist/config.js.map +1 -1
  7. package/dist/core/system-prompt.d.ts.map +1 -1
  8. package/dist/core/system-prompt.js +18 -0
  9. package/dist/core/system-prompt.js.map +1 -1
  10. package/dist/core/system-prompt.test.d.ts +2 -0
  11. package/dist/core/system-prompt.test.d.ts.map +1 -0
  12. package/dist/core/system-prompt.test.js +89 -0
  13. package/dist/core/system-prompt.test.js.map +1 -0
  14. package/dist/defaults/models.json +13 -45
  15. package/dist/defaults/settings.json +5 -7
  16. package/dist/extensions/copy-turn.test.ts +131 -0
  17. package/dist/extensions/copy-turn.ts +6 -1
  18. package/dist/extensions/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  19. package/dist/extensions/package.json +0 -1
  20. package/dist/extensions/pi-subagents/CHANGELOG.md +3 -0
  21. package/dist/extensions/pi-subagents/README.md +27 -32
  22. package/dist/extensions/pi-subagents/agents/architect.md +4 -4
  23. package/dist/extensions/pi-subagents/agents/builder.md +5 -4
  24. package/dist/extensions/pi-subagents/agents/commentator.md +3 -2
  25. package/dist/extensions/pi-subagents/agents/explorer.md +3 -2
  26. package/dist/extensions/pi-subagents/agents/recapper.md +3 -2
  27. package/dist/extensions/pi-subagents/agents/researcher.md +4 -3
  28. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +2 -0
  29. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +10 -9
  30. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +12 -11
  31. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +10 -11
  32. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +56 -9
  33. package/dist/extensions/pi-subagents/src/agents/task-aware-routing.ts +125 -0
  34. package/dist/extensions/pi-subagents/src/api/preflight.ts +1 -1
  35. package/dist/extensions/pi-subagents/src/extension/index.ts +5 -1
  36. package/dist/extensions/pi-subagents/src/extension/schemas.ts +2 -2
  37. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +24 -7
  38. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +23 -5
  39. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +27 -1
  40. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +64 -6
  41. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +16 -1
  42. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +72 -18
  43. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +19 -5
  44. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +127 -31
  45. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +4 -6
  46. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +63 -9
  47. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +21 -0
  48. package/dist/extensions/pi-subagents/src/shared/types.ts +41 -2
  49. package/dist/extensions/pi-subagents/src/shared/utils.ts +29 -1
  50. package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +5 -1
  51. package/dist/extensions/pi-subagents/src/tui/render.ts +32 -6
  52. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +111 -6
  53. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +74 -43
  54. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +36 -21
  55. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +5 -3
  56. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +20 -8
  57. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +14 -7
  58. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +227 -0
  59. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +81 -5
  60. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +49 -10
  61. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +18 -2
  62. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +1 -1
  63. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +70 -6
  64. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +161 -1
  65. package/dist/extensions/pi-subagents/test/unit/builtin-agent-documentation.test.ts +63 -0
  66. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-agent-allowlist.test.ts +34 -0
  67. package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +24 -0
  68. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +6 -1
  69. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +29 -0
  70. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +2 -0
  71. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +12 -0
  72. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +91 -1
  73. package/dist/extensions/pi-subagents/test/unit/task-aware-routing.test.ts +213 -0
  74. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +23 -1
  75. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +60 -9
  76. package/dist/extensions/pi-web-agent/package.json +1 -1
  77. package/dist/skills/pi-subagents/SKILL.md +43 -0
  78. package/dist/skills/pi-subagents/references/constraints-and-recipes.md +257 -0
  79. package/dist/skills/pi-subagents/references/execution-controls.md +431 -0
  80. package/dist/skills/pi-subagents/references/management-authoring-rpc.md +144 -0
  81. package/dist/skills/pi-subagents/references/prompting-and-roles.md +281 -0
  82. package/dist/skills/ponytail/SKILL.md +1 -3
  83. package/docs/plans/subagent-delegation/phase-0-correctness.md +265 -0
  84. package/docs/plans/subagent-delegation/phase-1-behavioral-contract.md +486 -0
  85. package/docs/plans/subagent-delegation/phase-2-context-controls.md +282 -0
  86. package/docs/plans/subagent-delegation/phase-3-advisory-routing.md +362 -0
  87. package/docs/plans/subagent-delegation/phase-4-optional-enforcement.md +381 -0
  88. package/package.json +2 -2
  89. package/dist/extensions/caveman/caveman-instructions.cjs +0 -11
  90. package/dist/extensions/caveman/index.js +0 -118
  91. package/dist/extensions/caveman/package.json +0 -8
  92. package/dist/extensions/caveman/test/extension.test.js +0 -203
  93. package/dist/extensions/caveman/test/helpers.test.js +0 -58
  94. package/dist/skills/caveman/SKILL.md +0 -50
@@ -367,8 +367,9 @@ describe("chain execution — sequential", { skip: !available ? "pi packages not
367
367
  assert.doesNotMatch(firstTaskArg, /\[Write to:|Write your findings to exactly this path/);
368
368
  assert.ok(result.details.results[0]?.savedOutputPath);
369
369
  assert.equal(fs.readFileSync(result.details.results[0].savedOutputPath, "utf-8"), "full chain output\nwith details");
370
- assert.match(result.details.results[0]?.finalOutput ?? "", /Output saved to:/);
371
- assert.doesNotMatch(result.details.results[0]?.finalOutput ?? "", /full chain output/);
370
+ assert.equal(result.details.results[0]?.finalOutput, undefined);
371
+ assert.match(result.details.results[0]?.outputReference?.message ?? "", /Output saved to:/);
372
+ assert.doesNotMatch(result.details.results[0]?.outputReference?.message ?? "", /full chain output/);
372
373
  const secondTaskArg = readCallArgs(1).at(-1) ?? "";
373
374
  assert.match(secondTaskArg, /Output saved to:/);
374
375
  assert.match(secondTaskArg, /2 lines/);
@@ -404,7 +405,8 @@ describe("chain execution — sequential", { skip: !available ? "pi packages not
404
405
  );
405
406
 
406
407
  assert.ok(!result.isError, `chain should succeed: ${JSON.stringify(result.content)}`);
407
- assert.match(result.details.results[0]?.finalOutput ?? "", /Output saved to:/);
408
+ assert.equal(result.details.results[0]?.finalOutput, undefined);
409
+ assert.match(result.details.results[0]?.outputReference?.message ?? "", /Output saved to:/);
408
410
  assert.equal(result.details.results[0]?.acceptance?.status, "checked");
409
411
  assert.ok(result.details.results[0]?.acceptance?.childReport);
410
412
 
@@ -630,10 +632,13 @@ describe("chain execution — sequential", { skip: !available ? "pi packages not
630
632
 
631
633
  assert.ok(!result.isError);
632
634
  const step2Task = result.details.results[1].task;
635
+ // Reference-first handoff: {previous} carries the saved-output reference and
636
+ // the step reads the named path instead of re-inlined prose.
633
637
  assert.ok(
634
- step2Task.includes("MARKER_ABC_123"),
635
- `step 2 task should contain step 1 output via {previous}: ${step2Task.slice(0, 200)}`,
638
+ step2Task.includes("Output saved to:"),
639
+ `step 2 task should contain the saved-output reference via {previous}: ${step2Task.slice(0, 200)}`,
636
640
  );
641
+ assert.doesNotMatch(step2Task, /MARKER_ABC_123/);
637
642
  });
638
643
 
639
644
  it("passes named sequential outputs through {outputs.name}", async () => {
@@ -652,7 +657,8 @@ describe("chain execution — sequential", { skip: !available ? "pi packages not
652
657
  );
653
658
 
654
659
  assert.ok(!result.isError);
655
- assert.match(readCallArgs(1).at(-1) ?? "", /CTX_123/);
660
+ assert.match(readCallArgs(1).at(-1) ?? "", /Output saved to: /);
661
+ assert.doesNotMatch(readCallArgs(1).at(-1) ?? "", /CTX_123/);
656
662
  assert.equal(result.details.workflowGraph?.nodes[0]?.outputName, "contextOutput");
657
663
  });
658
664
 
@@ -692,9 +698,10 @@ describe("chain execution — sequential", { skip: !available ? "pi packages not
692
698
  assert.match(readCallArgs(1).at(-1) ?? "", /Review src\/a\.ts/);
693
699
  assert.match(readCallArgs(2).at(-1) ?? "", /Review src\/b\.ts/);
694
700
  assert.match(readCallArgs(3).at(-1) ?? "", /"key":"src\/a\.ts"/);
695
- const collected = result.details.outputs?.reviews?.structured as Array<{ key: string; structured: unknown }>;
696
- assert.deepEqual(collected.map((item) => item.key), ["src/a.ts", "src/b.ts"]);
697
- assert.deepEqual(collected.map((item) => item.structured), [{ ok: "a" }, { ok: "b" }]);
701
+ // Terminal details strip chain outputs text/structured; bindings remain
702
+ // reference-first via {outputs.name} and the workflow graph keeps item keys.
703
+ assert.equal(result.details.outputs?.reviews?.text, "");
704
+ assert.equal(result.details.outputs?.reviews?.structured, undefined);
698
705
  const dynamicNode = result.details.workflowGraph?.nodes[1];
699
706
  assert.equal(dynamicNode?.kind, "dynamic-parallel-group");
700
707
  assert.deepEqual(dynamicNode?.children?.map((child) => child.itemKey), ["src/a.ts", "src/b.ts"]);
@@ -868,7 +875,7 @@ describe("chain execution — sequential", { skip: !available ? "pi packages not
868
875
  { agent: "scout", task: "Return targets", as: "targets", outputSchema: { type: "object" } },
869
876
  {
870
877
  expand: { from: { output: "targets", path: "/items" }, key: "/path", maxItems: 4 },
871
- parallel: { agent: "reviewer", task: "Review {item.path}", outputMode: "file-only" },
878
+ parallel: { agent: "reviewer", task: "Review {item.path}", output: false, outputMode: "file-only" },
872
879
  collect: { as: "reviews" },
873
880
  },
874
881
  ],
@@ -906,7 +913,8 @@ describe("chain execution — sequential", { skip: !available ? "pi packages not
906
913
 
907
914
  assert.ok(!result.isError, `chain should succeed: ${JSON.stringify(result.content)}`);
908
915
  assert.equal(mockPi.callCount(), 2);
909
- assert.deepEqual(result.details.outputs?.reviews?.structured, []);
916
+ assert.equal(result.details.outputs?.reviews?.text, "");
917
+ assert.equal(result.details.outputs?.reviews?.structured, undefined);
910
918
  assert.equal(result.details.workflowGraph?.nodes[1]?.status, "completed");
911
919
  assert.deepEqual(result.details.workflowGraph?.nodes[1]?.children, []);
912
920
  });
@@ -1123,7 +1131,9 @@ describe("chain execution — sequential", { skip: !available ? "pi packages not
1123
1131
  assert.equal(result.details.results.length, 2);
1124
1132
  assert.equal(result.details.results[0]?.acceptance?.status, "rejected");
1125
1133
  assert.equal(result.details.results[0]?.exitCode, 0);
1126
- assert.match(result.details.results[1]?.finalOutput ?? "", /Step 2 ran/);
1134
+ assert.equal(result.details.results[1]?.finalOutput, undefined);
1135
+ assert.ok(result.details.results[1]?.savedOutputPath);
1136
+ assert.equal(fs.readFileSync(result.details.results[1]!.savedOutputPath!, "utf-8"), "Step 2 ran");
1127
1137
  });
1128
1138
 
1129
1139
  it("agent contract v1 chain can gate progression on acceptance", async () => {
@@ -1195,7 +1205,8 @@ describe("chain execution — sequential", { skip: !available ? "pi packages not
1195
1205
  );
1196
1206
 
1197
1207
  const finalTaskArg = readCallArgs(chainLength - 1).at(-1) ?? "";
1198
- assert.match(finalTaskArg, /step-38-output/);
1208
+ assert.match(finalTaskArg, /Output saved to: /);
1209
+ assert.doesNotMatch(finalTaskArg, /step-38-output/);
1199
1210
  assert.doesNotMatch(finalTaskArg, /step-37-output/);
1200
1211
  assert.match(result.content[0]?.text ?? "", /40 steps/);
1201
1212
  });
@@ -1284,7 +1295,9 @@ describe("chain execution — sequential", { skip: !available ? "pi packages not
1284
1295
  );
1285
1296
 
1286
1297
  assert.ok(!result.isError);
1287
- assert.deepEqual(JSON.parse(result.details.results[0].finalOutput ?? "{}"), {
1298
+ const depthChild = result.details.results[0];
1299
+ assert.ok(depthChild.savedOutputPath);
1300
+ assert.deepEqual(JSON.parse(fs.readFileSync(depthChild.savedOutputPath, "utf-8")), {
1288
1301
  SELESAI_SUBAGENT_DEPTH: "1",
1289
1302
  SELESAI_SUBAGENT_MAX_DEPTH: "1",
1290
1303
  });
@@ -1474,8 +1487,9 @@ describe("chain execution — parallel steps", { skip: !available ? "pi packages
1474
1487
 
1475
1488
  assert.ok(!result.isError);
1476
1489
  const finalTask = readCallArgs(2).at(-1) ?? "";
1477
- assert.match(finalTask, /Alpha named output/);
1478
- assert.match(finalTask, /Beta named output/);
1490
+ assert.match(finalTask, /Output saved to: /);
1491
+ assert.doesNotMatch(finalTask, /Alpha named output/);
1492
+ assert.doesNotMatch(finalTask, /Beta named output/);
1479
1493
  });
1480
1494
 
1481
1495
  it("funnels an initial parallel step through one agent, then fans the funnel output back out", async () => {
@@ -1512,13 +1526,14 @@ describe("chain execution — parallel steps", { skip: !available ? "pi packages
1512
1526
  assert.equal(result.details.totalSteps, 3);
1513
1527
  const funnelTask = readCallArgsMatching("Synthesize:").at(-1) ?? "";
1514
1528
  assert.match(funnelTask, /=== Parallel Task 1 \(scout-a\) ===/);
1515
- assert.match(funnelTask, /Scout A findings/);
1516
1529
  assert.match(funnelTask, /=== Parallel Task 2 \(scout-b\) ===/);
1517
- assert.match(funnelTask, /Scout B findings/);
1530
+ assert.match(funnelTask, /Output saved to: /);
1531
+ assert.doesNotMatch(funnelTask, /Scout A findings/);
1532
+ assert.doesNotMatch(funnelTask, /Scout B findings/);
1518
1533
  const fanoutTaskA = readCallArgsMatching("Review funnel A:").at(-1) ?? "";
1519
1534
  const fanoutTaskB = readCallArgsMatching("Review funnel B:").at(-1) ?? "";
1520
- assert.match(fanoutTaskA, /Review funnel A:\nFunnel synthesis/);
1521
- assert.match(fanoutTaskB, /Review funnel B:\nFunnel synthesis/);
1535
+ assert.match(fanoutTaskA, /Review funnel A:\nOutput saved to: /);
1536
+ assert.match(fanoutTaskB, /Review funnel B:\nOutput saved to: /);
1522
1537
  assert.equal(result.details.workflowGraph?.nodes[0]?.kind, "parallel-group");
1523
1538
  assert.equal(result.details.workflowGraph?.nodes[1]?.kind, "step");
1524
1539
  assert.equal(result.details.workflowGraph?.nodes[2]?.kind, "parallel-group");
@@ -1560,7 +1575,7 @@ describe("chain execution — parallel steps", { skip: !available ? "pi packages
1560
1575
  makeChainParams(
1561
1576
  [{
1562
1577
  parallel: [
1563
- { agent: "reviewer-a", task: "Review A", outputMode: "file-only" },
1578
+ { agent: "reviewer-a", task: "Review A", output: false, outputMode: "file-only" },
1564
1579
  { agent: "reviewer-b", task: "Review B", output: "b.md" },
1565
1580
  ],
1566
1581
  }],
@@ -310,7 +310,8 @@ describe("fork context execution wiring", { skip: !available ? "subagent executo
310
310
 
311
311
  assert.equal(result.isError, undefined);
312
312
  const args = readCallArgs();
313
- assert.ok((args.at(-1) ?? "").startsWith("Task: \n\n## Acceptance Contract"));
313
+ assert.ok((args.at(-1) ?? "").startsWith("Task: \n\n---\n**Output:**"));
314
+ assert.match(args.at(-1) ?? "", /## Acceptance Contract/);
314
315
  });
315
316
 
316
317
  it("does not treat top-level agent as single mode when tasks are present", async () => {
@@ -327,7 +328,8 @@ describe("fork context execution wiring", { skip: !available ? "subagent executo
327
328
 
328
329
  assert.equal(result.isError, undefined);
329
330
  const args = readCallArgs();
330
- assert.ok((args.at(-1) ?? "").startsWith("Task: parallel task\n\n## Acceptance Contract"));
331
+ assert.ok((args.at(-1) ?? "").startsWith("Task: parallel task\n\n---\n**Output:**"));
332
+ assert.match(args.at(-1) ?? "", /## Acceptance Contract/);
331
333
  });
332
334
 
333
335
  it("uses agent defaultContext fork when launch context is omitted", async () => {
@@ -1827,7 +1829,7 @@ describe("fork context execution wiring", { skip: !available ? "subagent executo
1827
1829
  );
1828
1830
 
1829
1831
  assert.equal(result.isError, undefined);
1830
- const args = readAllCallArgs().find((callArgs) => (callArgs.at(-1) ?? "").startsWith(`Task: ${task}\n\n## Acceptance Contract`));
1832
+ const args = readAllCallArgs().find((callArgs) => (callArgs.at(-1) ?? "").startsWith(`Task: ${task}\n\n---\n**Output:**`));
1831
1833
  assert.ok(args, "expected a recorded mock pi call for this test task");
1832
1834
  const modelIndex = args.indexOf("--model");
1833
1835
  assert.notEqual(modelIndex, -1);
@@ -227,10 +227,13 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
227
227
  assert.match(result.content[0]?.text ?? "", /Delivered single subagent result via intercom\./);
228
228
  assert.doesNotMatch(result.content[0]?.text ?? "", /Full child output from worker/);
229
229
  assert.equal(result.details?.results?.[0]?.finalOutput, undefined);
230
- assert.match(String(payload.message ?? ""), /Full child output from worker/);
230
+ // Reference-first intercom delivery: the child summary is the saved-output
231
+ // reference (error/status first for failed children), never raw child prose.
232
+ assert.match(String(payload.message ?? ""), /Output saved to: /);
233
+ assert.doesNotMatch(String(payload.message ?? ""), /Full child output from worker/);
231
234
  });
232
235
 
233
- it("falls back to legacy foreground output when the bridge is inactive", async () => {
236
+ it("returns the native saved-output receipt when the bridge is inactive", async () => {
234
237
  mockPi.onCall({ output: "Legacy foreground output" });
235
238
  const { executor, events } = makeExecutor({ bridgeMode: "off" });
236
239
 
@@ -243,10 +246,14 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
243
246
  );
244
247
 
245
248
  assert.equal(events.emitted.some((entry) => entry.channel === "subagent:result-intercom"), false);
246
- assert.match(result.content[0]?.text ?? "", /Legacy foreground output/);
249
+ assert.match(result.content[0]?.text ?? "", /Output saved to: /);
250
+ assert.doesNotMatch(result.content[0]?.text ?? "", /Legacy foreground output/);
251
+ const runId = result.details?.runId;
252
+ assert.ok(runId);
253
+ assert.equal(fs.readFileSync(path.join(tempDir, ".pi-subagents", "artifacts", "outputs", runId, "result.md"), "utf-8"), "Legacy foreground output");
247
254
  });
248
255
 
249
- it("keeps native foreground output without attempting external grouped delivery when disabled", async () => {
256
+ it("keeps the native saved-output receipt without attempting external grouped delivery when disabled", async () => {
250
257
  mockPi.onCall({ output: "Native foreground output" });
251
258
  const { executor, events } = makeExecutor({ resultDelivery: false });
252
259
 
@@ -259,10 +266,11 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
259
266
  );
260
267
 
261
268
  assert.equal(events.emitted.some((entry) => entry.channel === "subagent:result-intercom"), false);
262
- assert.match(result.content[0]?.text ?? "", /Native foreground output/);
269
+ assert.match(result.content[0]?.text ?? "", /Output saved to: /);
270
+ assert.doesNotMatch(result.content[0]?.text ?? "", /Native foreground output/);
263
271
  });
264
272
 
265
- it("falls back to legacy foreground output when grouped delivery is not acknowledged", async () => {
273
+ it("falls back to the compact native saved-output receipt when grouped delivery is not acknowledged", async () => {
266
274
  mockPi.onCall({ output: "Unacknowledged foreground output" });
267
275
  const { executor, events } = makeExecutor({ acknowledgeResults: false });
268
276
 
@@ -275,7 +283,8 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
275
283
  );
276
284
 
277
285
  assert.equal(events.emitted.some((entry) => entry.channel === "subagent:result-intercom"), true);
278
- assert.match(result.content[0]?.text ?? "", /Unacknowledged foreground output/);
286
+ assert.match(result.content[0]?.text ?? "", /Output saved to: /);
287
+ assert.doesNotMatch(result.content[0]?.text ?? "", /Unacknowledged foreground output/);
279
288
  });
280
289
 
281
290
  it("top-level parallel runs emit one grouped event containing all children", async () => {
@@ -1250,7 +1259,10 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
1250
1259
  const payload = completion.payload as { success?: boolean; summary?: string };
1251
1260
  assert.equal(payload.success, false);
1252
1261
  assert.match(payload.summary ?? "", /Acceptance rejected/);
1253
- assert.match(payload.summary ?? "", /final answer with rejected acceptance evidence/);
1262
+ // Reference-first foreground completion: the recovery summary carries the
1263
+ // saved-output reference (with error/status), never raw child prose.
1264
+ assert.match(payload.summary ?? "", /Output saved to: /);
1265
+ assert.doesNotMatch(payload.summary ?? "", /final answer with rejected acceptance evidence/);
1254
1266
 
1255
1267
  const status = await executor.execute(
1256
1268
  "foreground-detached-failed-status",
@@ -287,7 +287,9 @@ describe("parallel agent execution", { skip: !piAvailable ? "pi packages not ava
287
287
  groups: Array<{ children: Array<{ agent: string; summary: string; patch: { path: string } }>; cleanup: { state: string; tasks: Array<{ path: string; worktreeRemoved: boolean; branchRemoved: boolean }> } }>;
288
288
  };
289
289
  assert.equal(handoff.groups[0]!.children[0]!.agent, "echo");
290
- assert.equal(handoff.groups[0]!.children[0]!.summary, "Worktree task complete");
290
+ // Reference-first handoff: the summary carries the saved-output reference.
291
+ assert.match(handoff.groups[0]!.children[0]!.summary, /Output saved to: /);
292
+ assert.doesNotMatch(handoff.groups[0]!.children[0]!.summary, /Worktree task complete/);
291
293
  assert.equal(fs.existsSync(handoff.groups[0]!.children[0]!.patch.path), true);
292
294
  assert.ok(result.details?.runId);
293
295
  assert.ok(handoff.groups[0]!.children[0]!.patch.path.includes(`${path.sep}worktree-diffs${path.sep}${result.details.runId}${path.sep}`));
@@ -351,7 +353,8 @@ describe("parallel agent execution", { skip: !piAvailable ? "pi packages not ava
351
353
 
352
354
  assert.equal(result.isError, undefined);
353
355
  assert.equal(result.details?.mode, "parallel");
354
- assert.match(result.content[0]?.text ?? "", new RegExp(`${action} alias finished`));
356
+ assert.match(result.content[0]?.text ?? "", /Output saved to: /);
357
+ assert.doesNotMatch(result.content[0]?.text ?? "", new RegExp(`${action} alias finished`));
355
358
  }
356
359
  });
357
360
 
@@ -465,7 +468,9 @@ describe("parallel agent execution", { skip: !piAvailable ? "pi packages not ava
465
468
  assert.equal(result.details?.results?.[0]?.timedOut, true);
466
469
  assert.equal(result.details?.results?.[0]?.error, "Subagent timed out after 300ms.");
467
470
  assert.equal(result.details?.results?.[1]?.exitCode, 0);
468
- assert.equal(result.details?.results?.[1]?.finalOutput, "fast done");
471
+ assert.equal(result.details?.results?.[1]?.finalOutput, undefined);
472
+ assert.ok(result.details?.results?.[1]?.savedOutputPath);
473
+ assert.equal(fs.readFileSync(result.details?.results?.[1]?.savedOutputPath as string, "utf-8"), "fast done");
469
474
  assert.match(result.content[0]?.text ?? "", /1\/2 succeeded/);
470
475
  assert.match(result.content[0]?.text ?? "", /TIMED OUT: Subagent timed out after 300ms\./);
471
476
  });
@@ -490,8 +495,9 @@ describe("parallel agent execution", { skip: !piAvailable ? "pi packages not ava
490
495
  assert.match(text, /Output saved to:/);
491
496
  assert.match(text, /2 lines/);
492
497
  assert.doesNotMatch(text, /Parallel full report/);
493
- assert.match(result.details?.results?.[0]?.finalOutput ?? "", /Output saved to:/);
494
- assert.doesNotMatch(result.details?.results?.[0]?.finalOutput ?? "", /Parallel full report/);
498
+ assert.equal(result.details?.results?.[0]?.finalOutput, undefined);
499
+ assert.match(result.details?.results?.[0]?.outputReference?.message ?? "", /Output saved to:/);
500
+ assert.doesNotMatch(result.details?.results?.[0]?.outputReference?.message ?? "", /Parallel full report/);
495
501
  assert.equal(fs.readFileSync(outputPath, "utf-8"), "Parallel full report\nwith details");
496
502
  });
497
503
 
@@ -500,7 +506,7 @@ describe("parallel agent execution", { skip: !piAvailable ? "pi packages not ava
500
506
 
501
507
  const result = await executor.execute(
502
508
  "parallel-file-only-missing-output",
503
- { tasks: [{ agent: "echo", task: "Write report", outputMode: "file-only" }] },
509
+ { tasks: [{ agent: "echo", task: "Write report", output: false, outputMode: "file-only" }] },
504
510
  new AbortController().signal,
505
511
  undefined,
506
512
  makeMinimalCtx(tempDir),
@@ -604,7 +610,8 @@ describe("parallel agent execution", { skip: !piAvailable ? "pi packages not ava
604
610
 
605
611
  Inspect
606
612
 
607
- ## Acceptance Contract`));
613
+ ---\n**Output:**`));
614
+ assert.match(taskArg, /## Acceptance Contract/);
608
615
  });
609
616
 
610
617
  it("top-level parallel defaultProgress uses isolated run storage", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
@@ -1,4 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
2
5
  import { describe, it } from "node:test";
3
6
  import { keyText } from "@selesai/code";
4
7
 
@@ -852,4 +855,228 @@ describe("renderSubagentResult fork indicator", () => {
852
855
  assert.match(text, /Step 1: scout/);
853
856
  assert.doesNotMatch(text, /parallel group:/);
854
857
  });
858
+
859
+ it("keeps settled expanded file-only results reference-first without re-inlining saved child prose", () => {
860
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-render-"));
861
+ try {
862
+ const outputPath = path.join(tempDir, "child-output.md");
863
+ const childProse = Array.from({ length: 40 }, (_, index) => `SENSITIVE FULL CHILD OUTPUT line ${index + 1}`).join("\n");
864
+ fs.writeFileSync(outputPath, childProse, "utf-8");
865
+ const bytes = Buffer.byteLength(childProse, "utf-8");
866
+ const referenceMessage = `Output saved to: ${outputPath} (${bytes} B, 40 lines). Read this file if needed.`;
867
+ const widget = renderSubagentResult!({
868
+ content: [{ type: "text", text: "done" }],
869
+ details: {
870
+ mode: "single",
871
+ results: [{
872
+ agent: "writer",
873
+ task: "Write the report",
874
+ exitCode: 0,
875
+ messages: [],
876
+ usage: emptyUsage,
877
+ outputMode: "file-only",
878
+ savedOutputPath: outputPath,
879
+ outputReference: { path: outputPath, bytes, lines: 40, message: referenceMessage },
880
+ sessionFile: path.join(tempDir, "session.jsonl"),
881
+ artifactPaths: { outputPath, dir: tempDir },
882
+ }],
883
+ },
884
+ }, { expanded: true }, theme);
885
+
886
+ const text = widget.render(200).join("\n");
887
+ assert.doesNotMatch(text, /SENSITIVE FULL CHILD OUTPUT/);
888
+ assert.match(text, /Output saved to:/);
889
+ assert.match(text, new RegExp(outputPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
890
+ assert.match(text, /Session:/);
891
+ assert.match(text, /Artifacts:/);
892
+ } finally {
893
+ fs.rmSync(tempDir, { recursive: true, force: true });
894
+ }
895
+ });
896
+
897
+ it("keeps explicit inline output and live progress rendering in expanded results", () => {
898
+ const inline = renderSubagentResult!({
899
+ content: [{ type: "text", text: "done" }],
900
+ details: {
901
+ mode: "single",
902
+ results: [{
903
+ agent: "writer",
904
+ task: "Inline task",
905
+ exitCode: 0,
906
+ messages: [],
907
+ usage: emptyUsage,
908
+ outputMode: "inline",
909
+ finalOutput: "FULL INLINE RESULT TEXT",
910
+ }],
911
+ },
912
+ }, { expanded: true }, theme).render(200).join("\n");
913
+ assert.match(inline, /FULL INLINE RESULT TEXT/);
914
+
915
+ const running = renderSubagentResult!({
916
+ content: [{ type: "text", text: "running" }],
917
+ details: {
918
+ mode: "single",
919
+ results: [{
920
+ agent: "reviewer",
921
+ task: "review",
922
+ exitCode: 0,
923
+ messages: [],
924
+ usage: emptyUsage,
925
+ progress: { index: 0, agent: "reviewer", status: "running", task: "review", recentTools: [], recentOutput: [], toolCount: 1, tokens: 42, durationMs: 3_000 },
926
+ }],
927
+ },
928
+ }, { expanded: true }, theme).render(200).join("\n");
929
+ assert.match(running, /running/);
930
+ assert.match(running, /for live detail/);
931
+ });
932
+
933
+ it("synthesizes a path-only output reference for legacy savedOutputPath results without reading the file", () => {
934
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-render-"));
935
+ try {
936
+ const outputPath = path.join(tempDir, "legacy-child-output.md");
937
+ // The file does not exist on disk: the renderer must never read it.
938
+ const widget = renderSubagentResult!({
939
+ content: [{ type: "text", text: "done" }],
940
+ details: {
941
+ mode: "single",
942
+ results: [{
943
+ agent: "writer",
944
+ task: "Write the report",
945
+ exitCode: 0,
946
+ messages: [],
947
+ usage: emptyUsage,
948
+ savedOutputPath: outputPath,
949
+ }],
950
+ },
951
+ }, { expanded: true }, theme);
952
+
953
+ const text = widget.render(200).join("\n");
954
+ assert.match(text, new RegExp(`Output saved to: ${outputPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`));
955
+ assert.match(text, /Read this file if needed/);
956
+ assert.doesNotMatch(text, /\(\d+ B, \d+ lines\)/);
957
+ } finally {
958
+ fs.rmSync(tempDir, { recursive: true, force: true });
959
+ }
960
+ });
961
+
962
+ it("keeps foreign savedOutputPath results reference-first even when they carry inline text", () => {
963
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-render-"));
964
+ try {
965
+ const outputPath = path.join(tempDir, "foreign-child-output.md");
966
+ const widget = renderSubagentResult!({
967
+ content: [{ type: "text", text: "done" }],
968
+ details: {
969
+ mode: "single",
970
+ results: [{
971
+ agent: "writer",
972
+ task: "Write the report",
973
+ exitCode: 0,
974
+ messages: [],
975
+ usage: emptyUsage,
976
+ savedOutputPath: outputPath,
977
+ finalOutput: "SENSITIVE FOREIGN CHILD PROSE that must not be inlined",
978
+ }],
979
+ },
980
+ }, { expanded: true }, theme);
981
+
982
+ const text = widget.render(200).join("\n");
983
+ assert.doesNotMatch(text, /SENSITIVE FOREIGN CHILD PROSE/);
984
+ assert.match(text, new RegExp(`Output saved to: ${outputPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`));
985
+ } finally {
986
+ fs.rmSync(tempDir, { recursive: true, force: true });
987
+ }
988
+ });
989
+
990
+ it("keeps compact file-only results reference-first with the saved-output reference", () => {
991
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-render-"));
992
+ try {
993
+ const outputPath = path.join(tempDir, "child-output.md");
994
+ const childProse = Array.from({ length: 40 }, (_, index) => `SENSITIVE FULL CHILD OUTPUT line ${index + 1}`).join("\n");
995
+ fs.writeFileSync(outputPath, childProse, "utf-8");
996
+ const bytes = Buffer.byteLength(childProse, "utf-8");
997
+ const referenceMessage = `Output saved to: ${outputPath} (${bytes} B, 40 lines). Read this file if needed.`;
998
+ const widget = renderSubagentResult!({
999
+ content: [{ type: "text", text: "done" }],
1000
+ details: {
1001
+ mode: "single",
1002
+ results: [{
1003
+ agent: "writer",
1004
+ task: "Write the report",
1005
+ exitCode: 0,
1006
+ messages: [],
1007
+ usage: emptyUsage,
1008
+ outputMode: "file-only",
1009
+ savedOutputPath: outputPath,
1010
+ outputReference: { path: outputPath, bytes, lines: 40, message: referenceMessage },
1011
+ }],
1012
+ },
1013
+ }, { expanded: false }, theme);
1014
+
1015
+ const text = widget.render(200).join("\n");
1016
+ assert.doesNotMatch(text, /SENSITIVE FULL CHILD OUTPUT/);
1017
+ assert.match(text, /Output saved to:/);
1018
+ assert.doesNotMatch(text, /no text output/);
1019
+ } finally {
1020
+ fs.rmSync(tempDir, { recursive: true, force: true });
1021
+ }
1022
+ });
1023
+
1024
+ it("keeps compact multi file-only results reference-first and success-marked", () => {
1025
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-render-"));
1026
+ try {
1027
+ const outputPathA = path.join(tempDir, "a.md");
1028
+ const outputPathB = path.join(tempDir, "b.md");
1029
+ fs.writeFileSync(outputPathA, "SENSITIVE CHILD OUTPUT A", "utf-8");
1030
+ fs.writeFileSync(outputPathB, "SENSITIVE CHILD OUTPUT B", "utf-8");
1031
+ const refA = { path: outputPathA, bytes: 24, lines: 1, message: `Output saved to: ${outputPathA} (24 B, 1 line). Read this file if needed.` };
1032
+ const refB = { path: outputPathB, bytes: 24, lines: 1, message: `Output saved to: ${outputPathB} (24 B, 1 line). Read this file if needed.` };
1033
+ const widget = renderSubagentResult!({
1034
+ content: [{ type: "text", text: "done" }],
1035
+ details: {
1036
+ mode: "parallel",
1037
+ totalSteps: 2,
1038
+ results: [
1039
+ { agent: "scout", task: "scan", exitCode: 0, messages: [], usage: emptyUsage, outputMode: "file-only", savedOutputPath: outputPathA, outputReference: refA },
1040
+ { agent: "reviewer", task: "review", exitCode: 0, messages: [], usage: emptyUsage, outputMode: "file-only", savedOutputPath: outputPathB, outputReference: refB },
1041
+ ],
1042
+ },
1043
+ }, { expanded: false }, theme);
1044
+
1045
+ const text = widget.render(200).join("\n");
1046
+ assert.doesNotMatch(text, /SENSITIVE CHILD OUTPUT/);
1047
+ assert.doesNotMatch(text, /no text output/);
1048
+ assert.match(text, /^✓ parallel/);
1049
+ assert.match(text, /Agent 1\/2: scout/);
1050
+ assert.match(text, /Agent 2\/2: reviewer/);
1051
+ } finally {
1052
+ fs.rmSync(tempDir, { recursive: true, force: true });
1053
+ }
1054
+ });
1055
+
1056
+ it("marks legacy compact multi savedOutputPath results as done without inlining child prose", () => {
1057
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-render-"));
1058
+ try {
1059
+ const outputPath = path.join(tempDir, "legacy.md");
1060
+ const widget = renderSubagentResult!({
1061
+ content: [{ type: "text", text: "done" }],
1062
+ details: {
1063
+ mode: "parallel",
1064
+ totalSteps: 2,
1065
+ results: [
1066
+ { agent: "scout", task: "scan", exitCode: 0, messages: [], usage: emptyUsage, savedOutputPath: outputPath, finalOutput: "LEGACY SENSITIVE PROSE" },
1067
+ { agent: "writer", task: "Write the report", exitCode: 0, messages: [], usage: emptyUsage, savedOutputPath: outputPath, finalOutput: "MORE LEGACY SENSITIVE PROSE" },
1068
+ ],
1069
+ },
1070
+ }, { expanded: false }, theme);
1071
+
1072
+ const text = widget.render(200).join("\n");
1073
+ assert.doesNotMatch(text, /LEGACY SENSITIVE PROSE|MORE LEGACY SENSITIVE PROSE/);
1074
+ assert.doesNotMatch(text, /no text output/);
1075
+ assert.match(text, /^✓ parallel/);
1076
+ assert.match(text, /Agent 1\/2: scout/);
1077
+ assert.match(text, /Agent 2\/2: writer/);
1078
+ } finally {
1079
+ fs.rmSync(tempDir, { recursive: true, force: true });
1080
+ }
1081
+ });
855
1082
  });