@selesai/code 0.5.29 → 0.6.1
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 +24 -0
- package/README.md +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 +1 -2
- 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 +28 -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/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/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
|
@@ -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 () => {
|
|
@@ -341,7 +341,11 @@ describe("result watcher", () => {
|
|
|
341
341
|
assert.notEqual(state.watcherRestartTimer, null);
|
|
342
342
|
|
|
343
343
|
fs.writeFileSync(childSessionPath, "", "utf-8");
|
|
344
|
-
|
|
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
|
-
|
|
381
|
-
|
|
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 ?? "", /
|
|
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
|
-
|
|
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
|
-
|
|
1318
|
-
|
|
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 ?? "", /
|
|
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 ?? "", /
|
|
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 ?? "", /
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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"
|
|
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
|
});
|