@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.
- package/CHANGELOG.md +34 -0
- package/README.md +1 -1
- package/dist/config.d.ts +16 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +106 -5
- package/dist/config.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +18 -0
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/system-prompt.test.d.ts +2 -0
- package/dist/core/system-prompt.test.d.ts.map +1 -0
- package/dist/core/system-prompt.test.js +89 -0
- package/dist/core/system-prompt.test.js.map +1 -0
- package/dist/defaults/models.json +13 -45
- package/dist/defaults/settings.json +5 -7
- package/dist/extensions/copy-turn.test.ts +131 -0
- package/dist/extensions/copy-turn.ts +6 -1
- package/dist/extensions/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/dist/extensions/package.json +0 -1
- package/dist/extensions/pi-subagents/CHANGELOG.md +3 -0
- package/dist/extensions/pi-subagents/README.md +27 -32
- package/dist/extensions/pi-subagents/agents/architect.md +4 -4
- package/dist/extensions/pi-subagents/agents/builder.md +5 -4
- package/dist/extensions/pi-subagents/agents/commentator.md +3 -2
- package/dist/extensions/pi-subagents/agents/explorer.md +3 -2
- package/dist/extensions/pi-subagents/agents/recapper.md +3 -2
- package/dist/extensions/pi-subagents/agents/researcher.md +4 -3
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +2 -0
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +10 -9
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +12 -11
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +10 -11
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +56 -9
- package/dist/extensions/pi-subagents/src/agents/task-aware-routing.ts +125 -0
- package/dist/extensions/pi-subagents/src/api/preflight.ts +1 -1
- package/dist/extensions/pi-subagents/src/extension/index.ts +5 -1
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +2 -2
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +24 -7
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +23 -5
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +27 -1
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +64 -6
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +16 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +72 -18
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +19 -5
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +127 -31
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +4 -6
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +63 -9
- package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +21 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +41 -2
- package/dist/extensions/pi-subagents/src/shared/utils.ts +29 -1
- package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +5 -1
- package/dist/extensions/pi-subagents/src/tui/render.ts +32 -6
- package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +111 -6
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +74 -43
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +36 -21
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +5 -3
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +20 -8
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +14 -7
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +227 -0
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +81 -5
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +49 -10
- package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +18 -2
- package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +70 -6
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +161 -1
- package/dist/extensions/pi-subagents/test/unit/builtin-agent-documentation.test.ts +63 -0
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling-agent-allowlist.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +24 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +6 -1
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +29 -0
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +2 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +12 -0
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +91 -1
- package/dist/extensions/pi-subagents/test/unit/task-aware-routing.test.ts +213 -0
- package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +23 -1
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +60 -9
- package/dist/extensions/pi-web-agent/package.json +1 -1
- package/dist/skills/pi-subagents/SKILL.md +43 -0
- package/dist/skills/pi-subagents/references/constraints-and-recipes.md +257 -0
- package/dist/skills/pi-subagents/references/execution-controls.md +431 -0
- package/dist/skills/pi-subagents/references/management-authoring-rpc.md +144 -0
- package/dist/skills/pi-subagents/references/prompting-and-roles.md +281 -0
- package/dist/skills/ponytail/SKILL.md +1 -3
- package/docs/plans/subagent-delegation/phase-0-correctness.md +265 -0
- package/docs/plans/subagent-delegation/phase-1-behavioral-contract.md +486 -0
- package/docs/plans/subagent-delegation/phase-2-context-controls.md +282 -0
- package/docs/plans/subagent-delegation/phase-3-advisory-routing.md +362 -0
- package/docs/plans/subagent-delegation/phase-4-optional-enforcement.md +381 -0
- package/package.json +2 -2
- package/dist/extensions/caveman/caveman-instructions.cjs +0 -11
- package/dist/extensions/caveman/index.js +0 -118
- package/dist/extensions/caveman/package.json +0 -8
- package/dist/extensions/caveman/test/extension.test.js +0 -203
- package/dist/extensions/caveman/test/helpers.test.js +0 -58
- 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.
|
|
371
|
-
assert.
|
|
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.
|
|
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("
|
|
635
|
-
`step 2 task should contain
|
|
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) ?? "", /
|
|
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
|
-
|
|
696
|
-
|
|
697
|
-
assert.
|
|
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.
|
|
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.
|
|
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, /
|
|
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
|
-
|
|
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, /
|
|
1478
|
-
assert.
|
|
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, /
|
|
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:\
|
|
1521
|
-
assert.match(fanoutTaskB, /Review funnel B:\
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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("
|
|
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 ?? "", /
|
|
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
|
|
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 ?? "", /
|
|
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
|
|
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 ?? "", /
|
|
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
|
-
|
|
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
|
-
|
|
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 ?? "",
|
|
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,
|
|
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.
|
|
494
|
-
assert.
|
|
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
|
-
|
|
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
|
});
|