@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
@@ -341,7 +341,11 @@ describe("result watcher", () => {
341
341
  assert.notEqual(state.watcherRestartTimer, null);
342
342
 
343
343
  fs.writeFileSync(childSessionPath, "", "utf-8");
344
- fs.writeFileSync(path.join(resultsDir, "async-fallback.json"), JSON.stringify({
344
+ const childOutputA = path.join(resultsDir, "child-a.md");
345
+ const childOutputB = path.join(resultsDir, "child-b.md");
346
+ fs.writeFileSync(childOutputA, "Result from a", "utf-8");
347
+ fs.writeFileSync(childOutputB, "Result from b", "utf-8");
348
+ fs.writeFileSync(path.join(resultsDir, "async-fallback.json"), JSON.stringify({
345
349
  id: "async-fallback",
346
350
  runId: "run-fallback",
347
351
  agent: "parallel:a+b",
@@ -350,8 +354,8 @@ describe("result watcher", () => {
350
354
  state: "complete",
351
355
  summary: "Combined summary",
352
356
  results: [
353
- { agent: "a", output: "Result from a", success: true, sessionFile: childSessionPath, intercomTarget: "subagent-a-run-fallback-1" },
354
- { agent: "b", output: "Result from b", success: false, error: "B failed", intercomTarget: "subagent-b-run-fallback-2" },
357
+ { agent: "a", output: "Result from a", outputPath: childOutputA, success: true, sessionFile: childSessionPath, intercomTarget: "subagent-a-run-fallback-1" },
358
+ { agent: "b", output: "Result from b", outputPath: childOutputB, success: false, error: "B failed", intercomTarget: "subagent-b-run-fallback-2" },
355
359
  ],
356
360
  sessionId: "session-1",
357
361
  intercomTarget: "subagent-chat-main",
@@ -377,8 +381,12 @@ describe("result watcher", () => {
377
381
  assert.equal(completion?.results?.[0]?.sessionPath, childSessionPath);
378
382
  assert.equal(payload.children?.[1]?.status, "failed");
379
383
  assert.equal(completion?.results?.[1]?.status, "failed");
380
- assert.equal(payload.children?.[1]?.summary, "B failed\n\nOutput:\nResult from b");
381
- assert.equal(completion?.results?.[1]?.summary, "B failed\n\nOutput:\nResult from b");
384
+ // Reference-only async delivery: child summaries are built from the
385
+ // authoritative output path (error/status first for failed children).
386
+ assert.match(payload.children?.[1]?.summary ?? "", /^B failed\n\nOutput saved to: .*child-b\.md/);
387
+ assert.match(completion?.results?.[1]?.summary ?? "", /^B failed\n\nOutput saved to: /);
388
+ assert.match(payload.children?.[0]?.summary ?? "", /^Output saved to: .*child-a\.md/);
389
+ assert.doesNotMatch(payload.children?.[1]?.summary ?? "", /Result from b/);
382
390
  } finally {
383
391
  fs.rmSync(resultsDir, { recursive: true, force: true });
384
392
  }
@@ -1063,6 +1071,74 @@ describe("result watcher", () => {
1063
1071
  }
1064
1072
  });
1065
1073
 
1074
+ it("uses a bounded fallback when a child output path is absent or unreadable", async () => {
1075
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-fallback-"));
1076
+ try {
1077
+ const emitted: Array<{ event: string; data: unknown }> = [];
1078
+ const listeners = new Map<string, Set<(payload: unknown) => void>>();
1079
+ const pi = {
1080
+ events: {
1081
+ on(event: string, handler: (payload: unknown) => void) {
1082
+ const set = listeners.get(event) ?? new Set();
1083
+ set.add(handler);
1084
+ listeners.set(event, set);
1085
+ return () => set.delete(handler);
1086
+ },
1087
+ emit(event: string, data: unknown) {
1088
+ emitted.push({ event, data });
1089
+ for (const handler of listeners.get(event) ?? []) handler(data);
1090
+ },
1091
+ },
1092
+ };
1093
+ const state = createState();
1094
+ state.currentSessionId = "session-1";
1095
+ const watcher = createResultWatcher(pi, state, resultsDir, 60_000);
1096
+ const missingOutputPath = path.join(resultsDir, "missing-child.md");
1097
+ const unreadablePath = path.join(resultsDir, "unreadable-child.md");
1098
+ fs.writeFileSync(unreadablePath, "unreadable child output", "utf-8");
1099
+ fs.chmodSync(unreadablePath, 0o000);
1100
+ try {
1101
+ fs.writeFileSync(path.join(resultsDir, "fallback.json"), JSON.stringify({
1102
+ id: "fallback",
1103
+ runId: "run-fallback",
1104
+ agent: "parallel:a+b",
1105
+ mode: "parallel",
1106
+ success: false,
1107
+ state: "failed",
1108
+ summary: "run summary that must never leak",
1109
+ results: [
1110
+ { agent: "a", outputPath: missingOutputPath, output: "internal a", success: false, error: "A exploded" },
1111
+ { agent: "b", outputPath: unreadablePath, output: "internal b", success: false, error: "B exploded" },
1112
+ ],
1113
+ sessionId: "session-1",
1114
+ }), "utf-8");
1115
+ watcher.primeExistingResults();
1116
+ await new Promise((resolve) => setTimeout(resolve, 100));
1117
+ } finally {
1118
+ fs.chmodSync(unreadablePath, 0o644);
1119
+ watcher.stopResultWatcher();
1120
+ }
1121
+ const completion = emitted.find((entry) => entry.event === "subagent:async-complete")?.data as { results?: Array<{ summary?: string }> } | undefined;
1122
+ assert.ok(completion, "expected an async completion");
1123
+ const summaryA = completion.results?.[0]?.summary ?? "";
1124
+ const summaryB = completion.results?.[1]?.summary ?? "";
1125
+ assert.match(summaryA, /\[Full output unavailable\]/);
1126
+ assert.match(summaryA, /Failed to read saved output: ENOENT/);
1127
+ assert.match(summaryA, /missing-child\.md/);
1128
+ assert.match(summaryB, /\[Full output unavailable\]/);
1129
+ assert.match(summaryB, /Failed to read saved output: EACCES: permission denied/);
1130
+ assert.match(summaryB, /unreadable-child\.md/);
1131
+ // Only a bounded excerpt of the internal text is exposed, never the raw
1132
+ // full output shape or the run summary.
1133
+ assert.match(summaryA, /internal a/);
1134
+ assert.match(summaryB, /internal b/);
1135
+ assert.doesNotMatch(summaryA, /Output:\ninternal a/);
1136
+ assert.doesNotMatch(summaryA + summaryB, /run summary that must never leak/);
1137
+ } finally {
1138
+ fs.rmSync(resultsDir, { recursive: true, force: true });
1139
+ }
1140
+ });
1141
+
1066
1142
  it("delivers a result when a previous duplicate key has expired", async () => {
1067
1143
  const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-expired-"));
1068
1144
  try {
@@ -374,7 +374,8 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
374
374
  );
375
375
 
376
376
  assert.equal(result.isError, undefined);
377
- assert.match(result.content[0]?.text ?? "", /single alias finished/);
377
+ assert.match(result.content[0]?.text ?? "", /Output saved to: /);
378
+ assert.doesNotMatch(result.content[0]?.text ?? "", /single alias finished/);
378
379
  });
379
380
 
380
381
  it("admits a zero run-level tool budget only for marked v2 delegated execution", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
@@ -412,7 +413,13 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
412
413
  ctx,
413
414
  );
414
415
  assert.equal(v2Delegated.isError, undefined);
415
- const env = JSON.parse(v2Delegated.content[0]?.text ?? "{}") as Record<string, string>;
416
+ // Reference-first delegated completion: the full echoed env snapshot is
417
+ // persisted to the generated result file; the content is the reference.
418
+ const delegatedOutputsDir = path.join(tempDir, ".pi-subagents", "artifacts", "outputs");
419
+ const delegatedRunDirs = fs.readdirSync(delegatedOutputsDir);
420
+ assert.equal(delegatedRunDirs.length, 1);
421
+ const persistedEnv = fs.readFileSync(path.join(delegatedOutputsDir, delegatedRunDirs[0]!, "result.md"), "utf-8");
422
+ const env = JSON.parse(persistedEnv) as Record<string, string>;
416
423
  assert.deepEqual(JSON.parse(env[TOOL_BUDGET_ENV] ?? "null"), zeroBudget);
417
424
  assert.equal(env[TOOL_BUDGET_ZERO_AUTH_ENV], "1");
418
425
  assert.equal(mockPi.callCount(), 1);
@@ -1248,7 +1255,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
1248
1255
 
1249
1256
  const result = await executor.execute(
1250
1257
  "single-schema-file-only-missing-path",
1251
- { agent: "echo", task: "Return structured data", outputSchema: { type: "object", required: ["ok"], properties: { ok: { type: "boolean" } } }, outputMode: "file-only", acceptance: false, artifacts: false },
1258
+ { agent: "echo", task: "Return structured data", outputSchema: { type: "object", required: ["ok"], properties: { ok: { type: "boolean" } } }, outputMode: "file-only", output: false, acceptance: false, artifacts: false },
1252
1259
  new AbortController().signal,
1253
1260
  undefined,
1254
1261
  makeMinimalCtx(tempDir),
@@ -1305,7 +1312,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
1305
1312
 
1306
1313
  const result = await executor.execute(
1307
1314
  "failed-single-output",
1308
- { agent: "oracle", task: "Implement the approved file changes" },
1315
+ { agent: "oracle", task: "Implement the approved file changes", artifacts: true },
1309
1316
  new AbortController().signal,
1310
1317
  undefined,
1311
1318
  makeMinimalCtx(tempDir),
@@ -1314,8 +1321,14 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
1314
1321
  const text = result.content[0]?.text ?? "";
1315
1322
  assert.equal(result.isError, true);
1316
1323
  assert.match(text, /completed without making edits/);
1317
- assert.match(text, /Output:\nOracle review:\n- finding one\n- finding two/);
1318
- assert.match(text, /Output artifact: /);
1324
+ // Reference-first failure: error/status plus the saved-output reference;
1325
+ // the full output stays readable in the persisted result file.
1326
+ assert.match(text, /Output saved to: /);
1327
+ assert.doesNotMatch(text, /Output:\nOracle review:/);
1328
+ const outputsDir = path.join(tempDir, ".pi-subagents", "artifacts", "outputs");
1329
+ const runDirs = fs.readdirSync(outputsDir);
1330
+ assert.equal(runDirs.length, 1);
1331
+ assert.equal(fs.readFileSync(path.join(outputsDir, runDirs[0]!, "result.md"), "utf-8"), "Oracle review:\n- finding one\n- finding two");
1319
1332
  });
1320
1333
 
1321
1334
  it("fails future-tense implementation summaries when no mutation attempt occurred", async () => {
@@ -1399,7 +1412,8 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
1399
1412
 
1400
1413
  assert.equal(result.isError, undefined);
1401
1414
  assert.equal(result.details?.results[0]?.agent, "worker");
1402
- assert.match(result.content[0]?.text ?? "", /Implemented/);
1415
+ assert.match(result.content[0]?.text ?? "", /Output saved to: /);
1416
+ assert.doesNotMatch(result.content[0]?.text ?? "", /^Implemented$/);
1403
1417
  });
1404
1418
 
1405
1419
  it("returns error for unknown agent", async () => {
@@ -2283,6 +2297,29 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
2283
2297
  assert.ok(!JSON.stringify(result.launchResolvedExtensions).includes(tempDir), "projection should not expose raw extension paths");
2284
2298
  });
2285
2299
 
2300
+ it("does not write debug artifacts by default", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
2301
+ mockPi.onCall({ output: "plain result" });
2302
+ const executor = makeExecutor([makeAgent("echo")]);
2303
+ const result = await executor.execute(
2304
+ "default-no-artifacts",
2305
+ { agent: "echo", task: "Run without debug artifacts", runId: "default-no-artifacts" },
2306
+ new AbortController().signal,
2307
+ undefined,
2308
+ makeMinimalCtx(tempDir),
2309
+ );
2310
+
2311
+ assert.equal(result.isError, undefined);
2312
+ assert.equal(result.details?.artifacts, undefined);
2313
+ // Generated durable output persists even with artifacts:false; per-child
2314
+ // debug artifacts (_input/_output/_meta/_transcript) remain opt-in.
2315
+ const defaultOutputsDir = path.join(tempDir, ".pi-subagents", "artifacts", "outputs");
2316
+ const defaultRunDirs = fs.readdirSync(defaultOutputsDir);
2317
+ assert.equal(defaultRunDirs.length, 1);
2318
+ const defaultRunDir = path.join(defaultOutputsDir, defaultRunDirs[0]!);
2319
+ assert.equal(fs.readFileSync(path.join(defaultRunDir, "result.md"), "utf-8"), "plain result");
2320
+ assert.deepEqual(fs.readdirSync(defaultRunDir), ["result.md"]);
2321
+ });
2322
+
2286
2323
  it("routes foreground artifacts to the configured session directory", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
2287
2324
  mockPi.onCall({ output: "session artifact result" });
2288
2325
  const sessionFile = path.join(tempDir, "sessions", "parent-session", "session.jsonl");
@@ -2292,7 +2329,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
2292
2329
 
2293
2330
  const result = await executor.execute(
2294
2331
  "session-artifact-dir",
2295
- { agent: "echo", task: "Write session-scoped artifacts", runId: "session-artifacts" },
2332
+ { agent: "echo", task: "Write session-scoped artifacts", runId: "session-artifacts", artifacts: true },
2296
2333
  new AbortController().signal,
2297
2334
  undefined,
2298
2335
  ctx,
@@ -2686,7 +2723,8 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
2686
2723
  );
2687
2724
 
2688
2725
  assert.equal(result.isError, undefined);
2689
- assert.match(result.content[0]?.text ?? "", /agent foreground default finished/);
2726
+ assert.match(result.content[0]?.text ?? "", /Output saved to: /);
2727
+ assert.doesNotMatch(result.content[0]?.text ?? "", /agent foreground default finished/);
2690
2728
  assert.equal(result.details?.asyncId, undefined);
2691
2729
  });
2692
2730
 
@@ -2715,7 +2753,8 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
2715
2753
  );
2716
2754
 
2717
2755
  assert.equal(result.isError, undefined);
2718
- assert.match(result.content[0]?.text ?? "", /explicit foreground finished/);
2756
+ assert.match(result.content[0]?.text ?? "", /Output saved to: /);
2757
+ assert.doesNotMatch(result.content[0]?.text ?? "", /explicit foreground finished/);
2719
2758
  assert.equal(result.details?.asyncId, undefined);
2720
2759
  });
2721
2760
 
@@ -1,4 +1,4 @@
1
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
1
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -47,6 +47,21 @@ export function subagentCall(args: Record<string, unknown>, id = "call-subagent-
47
47
  return fauxToolCall("subagent", args, { id });
48
48
  }
49
49
 
50
+ function resolvedSubagentResultText(resultText: string): string {
51
+ // Reference-first completion: the tool result carries "Output saved to:
52
+ // <path> (…)". Resolve the durable file so the scripted parent can relay the
53
+ // exact child output.
54
+ if (!resultText.includes("Output saved to: ")) return resultText;
55
+ const rest = resultText.split("Output saved to: ")[1];
56
+ if (!rest) return resultText;
57
+ const outputPath = rest.split(" (")[0];
58
+ try {
59
+ return readFileSync(outputPath, "utf-8");
60
+ } catch {
61
+ return resultText;
62
+ }
63
+ }
64
+
50
65
  export function routeParentThroughSubagent(input: {
51
66
  childMarker: string;
52
67
  subagentArgs: Record<string, unknown>;
@@ -56,7 +71,8 @@ export function routeParentThroughSubagent(input: {
56
71
  if (!isParent) return "Unexpected non-parent model call.";
57
72
  const resultText = latestSubagentToolResultText(context.messages as Array<{ role?: string; toolName?: string; content?: unknown }>);
58
73
  if (resultText !== undefined) {
59
- return `Parent relays: ${resultText.includes(input.childMarker) ? input.childMarker : "CHILD_MISSING"}`;
74
+ const resolved = resolvedSubagentResultText(resultText);
75
+ return `Parent relays: ${resolved.includes(input.childMarker) ? input.childMarker : "CHILD_MISSING"}`;
60
76
  }
61
77
  return subagentCall(input.subagentArgs);
62
78
  };
@@ -177,7 +177,7 @@ describe("builtin agent disabling", () => {
177
177
  { cwd: tempProject, modelRegistry: { getAvailable: () => [] } },
178
178
  ));
179
179
 
180
- assert.match(text, /Executable agents:\n- helper \(project\): Helper/);
180
+ assert.match(text, /Executable agents:\n- helper \(project, context: fresh\): Helper/);
181
181
  assert.doesNotMatch(text, /Disabled builtins:/);
182
182
  for (const name of disabledBuiltinNames) {
183
183
  assert.doesNotMatch(text, new RegExp(`^- ${name} \\(builtin`, "m"));
@@ -6,7 +6,7 @@ import { afterEach, describe, it } from "node:test";
6
6
  import { handleManagementAction } from "../../src/agents/agent-management.ts";
7
7
  import { serializeAgent } from "../../src/agents/agent-serializer.ts";
8
8
  import { parseChain, serializeChain } from "../../src/agents/chain-serializer.ts";
9
- import { discoverAgents, discoverAgentsAll, type AgentConfig } from "../../src/agents/agents.ts";
9
+ import { BUILTIN_AGENT_NAMES, discoverAgents, discoverAgentsAll, type AgentConfig } from "../../src/agents/agents.ts";
10
10
  import { parseFrontmatter } from "../../src/agents/frontmatter.ts";
11
11
  import { buildPiArgs } from "../../src/runs/shared/pi-args.ts";
12
12
  import { THINKING_LEVELS } from "../../src/shared/model-info.ts";
@@ -357,14 +357,54 @@ Do work
357
357
  assert.equal(worker?.defaultContext, "fork");
358
358
  });
359
359
 
360
- it("loads packaged architect and recapper with fork defaultContext", () => {
360
+ it("loads builtin agents with the authoritative six-agent defaultContext map", () => {
361
361
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-builtin-default-context-"));
362
362
  tempDirs.push(dir);
363
363
  const agents = discoverAgentsAll(dir).builtin;
364
364
 
365
- for (const name of ["architect", "recapper"]) {
365
+ const expected: Record<string, "fresh" | "fork"> = {
366
+ architect: "fork",
367
+ recapper: "fork",
368
+ builder: "fresh",
369
+ commentator: "fresh",
370
+ explorer: "fresh",
371
+ researcher: "fresh",
372
+ };
373
+ assert.deepEqual(
374
+ agents.map((agent) => agent.name).sort(),
375
+ [...BUILTIN_AGENT_NAMES].sort(),
376
+ );
377
+ for (const [name, context] of Object.entries(expected)) {
378
+ const agent = agents.find((candidate) => candidate.name === name);
379
+ assert.equal(agent?.defaultContext, context, `${name} should default to ${context} context`);
380
+ }
381
+ });
382
+ });
383
+
384
+ describe("bundled agent descriptions and acceptance roles", () => {
385
+ it("survives discovery with declared descriptions and acceptance roles", () => {
386
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-bundled-catalog-roles-"));
387
+ tempDirs.push(dir);
388
+ const agents = discoverAgentsAll(dir).builtin;
389
+
390
+ const expected: Record<string, { description: string; acceptanceRole: "read-only" | "writer" }> = {
391
+ architect: { description: "Read-only architecture and implementation planning", acceptanceRole: "read-only" },
392
+ builder: { description: "Mutation-capable scoped implementation", acceptanceRole: "writer" },
393
+ commentator: { description: "Read-only evidence-based review", acceptanceRole: "read-only" },
394
+ explorer: { description: "Read-only local codebase reconnaissance", acceptanceRole: "read-only" },
395
+ recapper: { description: "Read-only handoff and context synthesis", acceptanceRole: "read-only" },
396
+ researcher: { description: "Read-only external and code-first research", acceptanceRole: "read-only" },
397
+ };
398
+
399
+ assert.deepEqual(
400
+ agents.map((agent) => agent.name).sort(),
401
+ [...BUILTIN_AGENT_NAMES].sort(),
402
+ );
403
+ for (const [name, expectedEntry] of Object.entries(expected)) {
366
404
  const agent = agents.find((candidate) => candidate.name === name);
367
- assert.equal(agent?.defaultContext, "fork", `${name} should default to fork context`);
405
+ assert.ok(agent, `bundled agent ${name} not discovered`);
406
+ assert.equal(agent.description, expectedEntry.description, `${name} description`);
407
+ assert.equal(agent.acceptanceRole, expectedEntry.acceptanceRole, `${name} acceptanceRole`);
368
408
  }
369
409
  });
370
410
  });
@@ -1331,7 +1371,7 @@ Do work
1331
1371
  }
1332
1372
  });
1333
1373
 
1334
- it("builder includes the child-facing supervisor tool", () => {
1374
+ it("builder builtin tools exclude the bridge-injected supervisor tool", () => {
1335
1375
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-builtin-supervisor-tool-"));
1336
1376
  const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-builtin-supervisor-tool-home-"));
1337
1377
  tempDirs.push(dir);
@@ -1344,7 +1384,31 @@ Do work
1344
1384
  process.env.USERPROFILE = homeDir;
1345
1385
  const agent = discoverAgentsAll(dir).builtin.find((candidate) => candidate.name === "builder");
1346
1386
  assert.ok(agent, "builder builtin should be discovered");
1347
- assert.deepEqual(agent?.tools, ["read", "grep", "find", "ls", "bash", "edit", "write", "contact_supervisor"]);
1387
+ assert.deepEqual(agent?.tools, ["read", "grep", "find", "ls", "bash", "edit", "write"]);
1388
+ assert.equal(agent?.tools?.includes("contact_supervisor"), false);
1389
+ } finally {
1390
+ if (previousHome === undefined) delete process.env.HOME;
1391
+ else process.env.HOME = previousHome;
1392
+ if (previousUserProfile === undefined) delete process.env.USERPROFILE;
1393
+ else process.env.USERPROFILE = previousUserProfile;
1394
+ }
1395
+ });
1396
+
1397
+ it("researcher builtin routes grep.app tools into mcpDirectTools", () => {
1398
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-builtin-researcher-tools-"));
1399
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-builtin-researcher-tools-home-"));
1400
+ tempDirs.push(dir);
1401
+ tempDirs.push(homeDir);
1402
+ const previousHome = process.env.HOME;
1403
+ const previousUserProfile = process.env.USERPROFILE;
1404
+
1405
+ try {
1406
+ process.env.HOME = homeDir;
1407
+ process.env.USERPROFILE = homeDir;
1408
+ const agent = discoverAgentsAll(dir).builtin.find((candidate) => candidate.name === "researcher");
1409
+ assert.ok(agent, "researcher builtin should be discovered");
1410
+ assert.deepEqual(agent?.tools, ["read", "web_explore"]);
1411
+ assert.deepEqual(agent?.mcpDirectTools, ["grep_app_search", "grep_app_fetch"]);
1348
1412
  } finally {
1349
1413
  if (previousHome === undefined) delete process.env.HOME;
1350
1414
  else process.env.HOME = previousHome;
@@ -53,7 +53,7 @@ describe("agent management config parsing", () => {
53
53
  );
54
54
 
55
55
  assert.equal(result.isError, false);
56
- assert.match(readText(result), /- scout \(project\): Project scout override/);
56
+ assert.match(readText(result), /- scout \(project, context: fresh\): Project scout override/);
57
57
  assert.doesNotMatch(readText(result), /- scout \(builtin/);
58
58
  });
59
59
 
@@ -722,6 +722,126 @@ Drive the failing test first.
722
722
  assert.match(content, /inheritSkills: false/);
723
723
  });
724
724
 
725
+ it("exposes a rich human and machine catalog for a custom project agent and chain", () => {
726
+ const ctx = { cwd: tempDir, modelRegistry: { getAvailable: () => [] } };
727
+ const created = handleCreate(
728
+ {
729
+ config: {
730
+ name: "Catalog Writer",
731
+ description: "Scoped catalog writer",
732
+ scope: "project",
733
+ aliases: "catw",
734
+ acceptanceRole: "writer",
735
+ defaultContext: "fork",
736
+ tools: "read, grep, find, ls, bash, edit, write, mcp:fixture_search",
737
+ systemPrompt: "Implement scoped changes.",
738
+ },
739
+ },
740
+ ctx,
741
+ );
742
+ assert.equal(created.isError, false);
743
+
744
+ const chainCreated = handleCreate(
745
+ { config: { name: "Review Flow", description: "Catalog review flow", scope: "project", steps: [{ agent: "catalog-writer", task: "Inspect" }] } },
746
+ ctx,
747
+ );
748
+ assert.equal(chainCreated.isError, false);
749
+
750
+ const listed = handleManagementAction("list", {}, ctx);
751
+ assert.equal(listed.isError, false);
752
+ const text = readText(listed);
753
+ assert.match(text, /Executable agents:/);
754
+ assert.match(
755
+ text,
756
+ /- catalog-writer \(project, context: fork, role: writer, aliases: catw, tools: read, grep, find, ls, bash, edit, write, mcp:fixture_search\): Scoped catalog writer/,
757
+ );
758
+ assert.match(text, /Chains:\n- review-flow \(project\): Catalog review flow/);
759
+
760
+ const details = (listed as { details: { catalog: unknown } }).details.catalog as {
761
+ version: number;
762
+ agents: Array<{
763
+ name: string;
764
+ source: string;
765
+ description: string;
766
+ executable: boolean;
767
+ aliases?: string[];
768
+ defaultContext: string;
769
+ acceptanceRole?: string;
770
+ tools?: string[];
771
+ }>;
772
+ chains: Array<{ name: string; source: string; description: string }>;
773
+ };
774
+ assert.equal(details.version, 1);
775
+ const entry = details.agents.find((agent) => agent.name === "catalog-writer");
776
+ assert.ok(entry, "catalog must include the custom project agent");
777
+ assert.equal(entry.source, "project");
778
+ assert.equal(entry.description, "Scoped catalog writer");
779
+ assert.equal(entry.executable, true);
780
+ assert.deepEqual(entry.aliases, ["catw"]);
781
+ assert.equal(entry.defaultContext, "fork");
782
+ assert.equal(entry.acceptanceRole, "writer");
783
+ assert.deepEqual(entry.tools, ["read", "grep", "find", "ls", "bash", "edit", "write", "mcp:fixture_search"]);
784
+ assert.deepEqual(details.chains, [{ name: "review-flow", source: "project", description: "Catalog review flow" }]);
785
+ });
786
+
787
+ it("normalizes unset defaultContext to fresh in the machine catalog", () => {
788
+ const ctx = { cwd: tempDir, modelRegistry: { getAvailable: () => [] } };
789
+ const created = handleCreate(
790
+ { config: { name: "No Context", description: "No explicit context", scope: "project", systemPrompt: "Work" } },
791
+ ctx,
792
+ );
793
+ assert.equal(created.isError, false);
794
+
795
+ const listed = handleManagementAction("list", {}, ctx);
796
+ const details = (listed as { details: { catalog: unknown } }).details.catalog as {
797
+ agents: Array<{ name: string; defaultContext: string }>;
798
+ };
799
+ const entry = details.agents.find((agent) => agent.name === "no-context");
800
+ assert.ok(entry);
801
+ assert.equal(entry.defaultContext, "fresh");
802
+ assert.match(readText(listed), /- no-context \(project, context: fresh\)/);
803
+ });
804
+
805
+ it("omits disabled agents from both human text and machine catalog metadata", () => {
806
+ const ctx = { cwd: tempDir, modelRegistry: { getAvailable: () => [] } };
807
+ const created = handleCreate(
808
+ { config: { name: "Hidden Writer", description: "Should stay hidden", scope: "project", systemPrompt: "Work" } },
809
+ ctx,
810
+ );
811
+ assert.equal(created.isError, false);
812
+
813
+ const disabled = handleManagementAction("disable", { agent: "hidden-writer" }, ctx);
814
+ assert.equal(disabled.isError, false);
815
+
816
+ const listed = handleManagementAction("list", {}, ctx);
817
+ assert.equal(listed.isError, false);
818
+ assert.doesNotMatch(readText(listed), /hidden-writer/);
819
+ const details = (listed as { details: { catalog: unknown } }).details.catalog as {
820
+ agents: Array<{ name: string }>;
821
+ };
822
+ assert.equal(details.agents.some((agent) => agent.name === "hidden-writer"), false);
823
+ });
824
+
825
+ it("returns (none) text and an empty versioned catalog when no agents are discovered", () => {
826
+ fs.mkdirSync(path.join(tempDir, "agent-home"), { recursive: true });
827
+ fs.writeFileSync(
828
+ path.join(tempDir, "agent-home", "settings.json"),
829
+ JSON.stringify({ subagents: { disableBuiltins: true } }, null, 2),
830
+ "utf-8",
831
+ );
832
+ const ctx = { cwd: tempDir, modelRegistry: { getAvailable: () => [] } };
833
+ const listed = handleManagementAction("list", {}, ctx);
834
+ assert.equal(listed.isError, false);
835
+ const text = readText(listed);
836
+ assert.match(text, /Executable agents:\n- \(none\)/);
837
+ assert.match(text, /Chains:\n- \(none\)/);
838
+ assert.deepEqual((listed as { details: { catalog: unknown } }).details.catalog, {
839
+ version: 1,
840
+ agents: [],
841
+ chains: [],
842
+ });
843
+ });
844
+
725
845
  it("lists proactive skill subagent suggestions from repeated configured skill use", () => {
726
846
  const ctx = { cwd: tempDir, modelRegistry: { getAvailable: () => [] } };
727
847
  fs.mkdirSync(path.join(tempDir, ".selesai", "agents"), { recursive: true });
@@ -774,4 +894,44 @@ Inspect cleanup.
774
894
  assert.doesNotMatch(readText(listed), /Proactive skill subagent suggestions:/);
775
895
  });
776
896
 
897
+ it("appends explicit-only task-aware advisory routing to list output when task is provided", () => {
898
+ const ctx = { cwd: tempDir, modelRegistry: { getAvailable: () => [] } };
899
+ handleCreate(
900
+ { config: { name: "Code Reviewer", description: "Reviews without editing", scope: "project", acceptanceRole: "read-only", tools: "read, grep, find, ls", systemPrompt: "Review." } },
901
+ ctx,
902
+ );
903
+ handleCreate(
904
+ { config: { name: "Fixer", description: "Implements changes", scope: "project", acceptanceRole: "writer", tools: "read, grep, bash, edit, write", systemPrompt: "Fix." } },
905
+ ctx,
906
+ );
907
+
908
+ const implementation = handleManagementAction("list", { task: "Implement the fix" }, ctx);
909
+ assert.equal(implementation.isError, false);
910
+ assert.deepEqual((implementation as { details: { results: unknown[] } }).details.results, []);
911
+ const implementationText = readText(implementation);
912
+ assert.match(implementationText, /Task-aware advisory routing:/);
913
+ assert.match(implementationText, /- Intent: implementation/);
914
+ assert.match(implementationText, /- Recommended: fixer \(project\)/);
915
+ assert.match(implementationText, /- Advisory only: no subagent was launched\. To proceed, explicitly call subagent with this canonical agent name and the task\./);
916
+
917
+ const review = handleManagementAction("list", { task: "Review only; do not edit files" }, ctx);
918
+ assert.equal(review.isError, false);
919
+ const reviewText = readText(review);
920
+ assert.match(reviewText, /- Intent: read-only/);
921
+ assert.match(reviewText, /- Recommended: code-reviewer \(project\)/);
922
+
923
+ const vague = handleManagementAction("list", { task: "Look into this" }, ctx);
924
+ assert.equal(vague.isError, false);
925
+ const vagueText = readText(vague);
926
+ assert.match(vagueText, /- Intent: unknown/);
927
+ assert.match(vagueText, /- Recommendation: none/);
928
+ assert.match(vagueText, /Clarify whether the task is read-only analysis\/review or implementation allowed to edit files\./);
929
+
930
+ for (const noTaskParams of [{}, { task: " " }]) {
931
+ const noTask = handleManagementAction("list", noTaskParams, ctx);
932
+ assert.equal(noTask.isError, false);
933
+ assert.doesNotMatch(readText(noTask), /Task-aware advisory routing:/);
934
+ }
935
+ });
936
+
777
937
  });
@@ -0,0 +1,63 @@
1
+ import assert from "node:assert/strict";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { describe, it } from "node:test";
5
+ import { fileURLToPath } from "node:url";
6
+ import { BUILTIN_AGENT_NAMES } from "../../src/agents/agents.ts";
7
+
8
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
9
+
10
+ const README_PATH = path.join(packageRoot, "README.md");
11
+ const PROMPTING_AND_ROLES_PATH = path.join(packageRoot, "skills", "pi-subagents", "references", "prompting-and-roles.md");
12
+
13
+ const README_TABLE_HEADING = "## Builtin agents in plain English";
14
+ const PROMPTING_AND_ROLES_TABLE_HEADING = "## Builtin Agents";
15
+
16
+ /**
17
+ * Extracts the first-column agent names from every table row under the given
18
+ * markdown section heading. Skips the header row and the separator row.
19
+ */
20
+ function tableFirstColumnNames(markdown: string, sectionHeading: string): string[] {
21
+ const lines = markdown.split(/\r?\n/);
22
+ const headingIndex = lines.findIndex((line) => line.trim() === sectionHeading);
23
+ assert.ok(headingIndex >= 0, `missing section heading: ${sectionHeading}`);
24
+
25
+ const names: string[] = [];
26
+ for (let i = headingIndex + 1; i < lines.length; i++) {
27
+ const line = lines[i]!.trim();
28
+ if (line.startsWith("#")) break;
29
+ if (!line.startsWith("|")) continue;
30
+
31
+ const cells = line.split("|").map((cell) => cell.trim());
32
+ const firstCell = (cells[1] ?? "").replaceAll("`", "").trim();
33
+ if (firstCell === "Agent") continue;
34
+ if (/^:?-+:?$/.test(firstCell)) continue;
35
+ if (firstCell.length === 0) continue;
36
+ names.push(firstCell);
37
+ }
38
+ return names;
39
+ }
40
+
41
+ function assertExactlySixBuiltins(tablePath: string, heading: string, markdown: string): void {
42
+ const names = tableFirstColumnNames(markdown, heading);
43
+
44
+ assert.equal(names.length, 6, `${path.basename(tablePath)}: role table must contain exactly six builtin rows`);
45
+ assert.equal(new Set(names).size, names.length, `${path.basename(tablePath)}: role table contains duplicate rows`);
46
+ assert.deepEqual(
47
+ [...names].sort(),
48
+ [...BUILTIN_AGENT_NAMES].sort(),
49
+ `${path.basename(tablePath)}: role table must list exactly ${BUILTIN_AGENT_NAMES.join(", ")} once each, with no aliases`,
50
+ );
51
+ }
52
+
53
+ describe("builtin agent documentation role tables", () => {
54
+ it("README lists exactly the six canonical builtins once, with no duplicates or aliases", () => {
55
+ const markdown = fs.readFileSync(README_PATH, "utf-8");
56
+ assertExactlySixBuiltins(README_PATH, README_TABLE_HEADING, markdown);
57
+ });
58
+
59
+ it("prompting-and-roles reference lists exactly the six canonical builtins once, with no duplicates or aliases", () => {
60
+ const markdown = fs.readFileSync(PROMPTING_AND_ROLES_PATH, "utf-8");
61
+ assertExactlySixBuiltins(PROMPTING_AND_ROLES_PATH, PROMPTING_AND_ROLES_TABLE_HEADING, markdown);
62
+ });
63
+ });