@selesai/code 0.13.2 → 0.13.4
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 +20 -0
- package/dist/defaults/models.json +85 -13
- package/dist/extensions/cost-reconcile.test.ts +200 -4
- package/dist/extensions/cost-reconcile.ts +88 -102
- package/dist/extensions/pi-intercom/CHANGELOG.md +13 -0
- package/dist/extensions/pi-intercom/README.md +4 -5
- package/dist/extensions/pi-intercom/config.test.ts +3 -31
- package/dist/extensions/pi-intercom/config.ts +0 -15
- package/dist/extensions/pi-intercom/index.ts +9 -46
- package/dist/extensions/pi-intercom/intercom.integration.test.ts +49 -57
- package/dist/extensions/pi-intercom/package.json +1 -1
- package/dist/extensions/pi-intercom/reply-tracker.test.ts +20 -0
- package/dist/extensions/pi-intercom/reply-tracker.ts +8 -0
- package/dist/extensions/pi-subagents/CHANGELOG.md +27 -0
- package/dist/extensions/pi-subagents/docs/tool-reference.md +4 -1
- package/dist/extensions/pi-subagents/docs/workflows.md +2 -2
- package/dist/extensions/pi-subagents/package-lock.json +2 -2
- package/dist/extensions/pi-subagents/package.json +1 -1
- package/dist/extensions/pi-subagents/skills/council-mode/SKILL.md +48 -243
- package/dist/extensions/pi-subagents/skills/council-mode/references/pass-contracts.md +150 -0
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +87 -37
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +29 -233
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +49 -8
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +34 -27
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/review-and-validation.md +73 -0
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +157 -28
- package/dist/extensions/pi-subagents/src/api/shared-types.ts +2 -0
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +1 -0
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +1 -0
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +4 -1
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +3 -0
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +45 -2
- package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +3 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +13 -2
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +5 -1
- package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +10 -2
- package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +3 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +11 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +98 -1
- package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +138 -4
- package/dist/extensions/pi-subagents/src/runs/shared/background-process-options.ts +9 -0
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-grant.ts +2 -5
- package/dist/extensions/pi-subagents/src/runs/shared/mutation-evidence.ts +52 -3
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +47 -1
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +45 -18
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +21 -2
- package/dist/extensions/pi-subagents/src/runs/shared/workflow-graph.ts +15 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +34 -1
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +11 -3
- package/dist/extensions/pi-subagents/src/tui/render-helpers.ts +31 -0
- package/dist/extensions/pi-subagents/src/tui/render.ts +597 -112
- package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +40 -1
- package/dist/extensions/pi-subagents/src/workflows/host-command.ts +6 -1
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +53 -2
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +55 -6
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +111 -1
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +206 -38
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +522 -31
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +123 -0
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +48 -0
- package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +57 -1
- package/dist/extensions/pi-subagents/test/unit/background-process-options.test.ts +17 -0
- package/dist/extensions/pi-subagents/test/unit/external-cli-runner.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +44 -4
- package/dist/extensions/pi-subagents/test/unit/fork-cache-key.test.ts +91 -0
- package/dist/extensions/pi-subagents/test/unit/host-command.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +0 -1
- package/dist/extensions/pi-subagents/test/unit/mcp-direct-tool-grant.test.ts +20 -3
- package/dist/extensions/pi-subagents/test/unit/mutation-evidence.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +93 -17
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +103 -26
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +58 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +22 -2
- package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +13 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +54 -0
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +2 -0
- package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +32 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +48 -1
- package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +21 -6
- package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +11 -0
- package/dist/extensions/pi-web-agent/CHANGELOG.md +410 -0
- package/dist/extensions/pi-web-agent/README.md +131 -0
- package/dist/extensions/pi-web-agent/package.json +4 -2
- package/dist/extensions/pi-web-agent/src/backends/config.ts +62 -5
- package/dist/extensions/pi-web-agent/src/backends/doctor.ts +136 -0
- package/dist/extensions/pi-web-agent/src/backends/factory.ts +96 -6
- package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +183 -47
- package/dist/extensions/pi-web-agent/src/extension.ts +62 -25
- package/dist/extensions/pi-web-agent/src/extract/readability.ts +19 -11
- package/dist/extensions/pi-web-agent/src/orchestration/candidate-selector.ts +5 -4
- package/dist/extensions/pi-web-agent/src/orchestration/direct-url.ts +2 -25
- package/dist/extensions/pi-web-agent/src/orchestration/evidence-quality.ts +5 -2
- package/dist/extensions/pi-web-agent/src/orchestration/evidence-ranker.ts +2 -0
- package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +72 -7
- package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +8 -1
- package/dist/extensions/pi-web-agent/src/orchestration/research-worker.ts +28 -3
- package/dist/extensions/pi-web-agent/src/orchestration/source-profile.ts +4 -0
- package/dist/extensions/pi-web-agent/src/orchestration/url.ts +35 -0
- package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +18 -6
- package/dist/extensions/pi-web-agent/src/presentation/search-presentation.ts +14 -2
- package/dist/extensions/pi-web-agent/src/readers/github-reader.ts +150 -0
- package/dist/extensions/pi-web-agent/src/readers/limits.ts +3 -0
- package/dist/extensions/pi-web-agent/src/readers/pdf-reader.ts +87 -0
- package/dist/extensions/pi-web-agent/src/readers/resolver.ts +25 -0
- package/dist/extensions/pi-web-agent/src/readers/types.ts +11 -0
- package/dist/extensions/pi-web-agent/src/readers/youtube-reader.ts +79 -0
- package/dist/extensions/pi-web-agent/src/search/duckduckgo.ts +32 -5
- package/dist/extensions/pi-web-agent/src/search/exa.ts +109 -0
- package/dist/extensions/pi-web-agent/src/search/fanout.ts +154 -0
- package/dist/extensions/pi-web-agent/src/search/tavily.ts +113 -0
- package/dist/extensions/pi-web-agent/src/search/youcom.ts +109 -0
- package/dist/extensions/pi-web-agent/src/tools/web-search.ts +22 -9
- package/dist/extensions/pi-web-agent/src/types.ts +19 -4
- package/dist/extensions/tokenin-onboarding.ts +302 -0
- package/package.json +1 -1
|
@@ -64,6 +64,7 @@ import { missionStatePath } from "../../src/missions/workflow-state.ts";
|
|
|
64
64
|
import { discardPreservedWorktrees } from "../../src/runs/shared/parallel-handoff.ts";
|
|
65
65
|
import { createWorktrees } from "../../src/runs/shared/worktree.ts";
|
|
66
66
|
import { resolveAsyncResumeTarget } from "../../src/runs/background/async-resume.ts";
|
|
67
|
+
import { createResultWatcher } from "../../src/runs/background/result-watcher.ts";
|
|
67
68
|
import { clearExclusions } from "../../src/runs/shared/model-exclusions.ts";
|
|
68
69
|
import { createWorkflowChildPermit, workflowChildPermitConsumed } from "../../src/shared/workflow-child-permit.ts";
|
|
69
70
|
import { toSubagentDelegationExecutionParams } from "../../src/slash/delegation-adapters.ts";
|
|
@@ -137,6 +138,7 @@ interface RunSyncResult {
|
|
|
137
138
|
processSignal?: string | null;
|
|
138
139
|
interrupted?: boolean;
|
|
139
140
|
timedOut?: boolean;
|
|
141
|
+
timeoutRecovery?: { changedFiles?: string[]; message?: string; recoveryNeeded?: boolean; reason?: string; reportStatus?: string };
|
|
140
142
|
turnBudget?: { maxTurns: number; graceTurns: number; outcome: string; turnCount: number; wrapUpRequestedAtTurn?: number; exceededAtTurn?: number };
|
|
141
143
|
turnBudgetExceeded?: boolean;
|
|
142
144
|
wrapUpRequested?: boolean;
|
|
@@ -915,6 +917,24 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
915
917
|
assert.deepEqual(failed.details.workflow?.receipt?.hostSteps?.map(({ state, reasonCode, exitCode }) => ({ state, reasonCode, exitCode })), [{ state: "error", reasonCode: "command_failed", exitCode: 4 }]);
|
|
916
918
|
});
|
|
917
919
|
|
|
920
|
+
it("explains the cwd workaround instead of launching a host step", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
921
|
+
const executor = makeExecutor([makeAgent("echo")]);
|
|
922
|
+
const result = await executor.executePublic(
|
|
923
|
+
"host-command-cwd",
|
|
924
|
+
{
|
|
925
|
+
async: false,
|
|
926
|
+
workflowScript: `return await runs.host("tests", { kind: "command", command: "npm test", timeoutMs: 5000, cwd: "/tmp" });`,
|
|
927
|
+
},
|
|
928
|
+
new AbortController().signal,
|
|
929
|
+
undefined,
|
|
930
|
+
makeMinimalCtx(tempDir),
|
|
931
|
+
);
|
|
932
|
+
|
|
933
|
+
assert.equal(result.isError, true);
|
|
934
|
+
assert.match(result.content[0]?.text ?? "", /does not accept per-step cwd.*workflow cwd.*outer subagent request.*cd \/path\/to\/worktree/);
|
|
935
|
+
assert.equal(mockPi.callCount(), 0);
|
|
936
|
+
});
|
|
937
|
+
|
|
918
938
|
it("rejects a child output claimed by an earlier host command", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
919
939
|
const scriptPath = path.join(tempDir, "host-output-owner.cjs");
|
|
920
940
|
fs.writeFileSync(scriptPath, `process.stdout.write("host owns output\\n");`);
|
|
@@ -1260,6 +1280,79 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1260
1280
|
fs.rmSync(resultPath, { force: true });
|
|
1261
1281
|
});
|
|
1262
1282
|
|
|
1283
|
+
it("delivers a terminal Darwin workflow failure after demand disappears", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1284
|
+
const state: SubagentState = {
|
|
1285
|
+
baseCwd: tempDir,
|
|
1286
|
+
currentSessionId: "session-1700",
|
|
1287
|
+
completionOwnerId: "owner-1700",
|
|
1288
|
+
asyncJobs: new Map(),
|
|
1289
|
+
foregroundControls: new Map(),
|
|
1290
|
+
lastForegroundControlId: null,
|
|
1291
|
+
completionSeen: new Map(),
|
|
1292
|
+
resultFileCoalescer: { schedule: () => false, clear: () => {} },
|
|
1293
|
+
};
|
|
1294
|
+
let pollCreated = false;
|
|
1295
|
+
const delivered: Array<{ id?: string; state?: string }> = [];
|
|
1296
|
+
const piEvents = createEventBus();
|
|
1297
|
+
const watcher = createResultWatcher({ events: piEvents }, state, DIRS.results, 60_000, {
|
|
1298
|
+
platform: "darwin",
|
|
1299
|
+
deliverIntercomResults: false,
|
|
1300
|
+
coalesceDelayMs: 0,
|
|
1301
|
+
hasDeliveryDemand: () => [...state.asyncJobs.values()].some((job) => job.status === "queued" || job.status === "running"),
|
|
1302
|
+
notifier: { deliver: async (result) => { delivered.push({ id: result.id, state: result.state }); return true; } },
|
|
1303
|
+
timers: {
|
|
1304
|
+
setTimeout,
|
|
1305
|
+
clearTimeout,
|
|
1306
|
+
setInterval: ((handler: () => void, delay?: number) => {
|
|
1307
|
+
assert.equal(delay, 3000);
|
|
1308
|
+
pollCreated = true;
|
|
1309
|
+
return { unref() {} } as NodeJS.Timeout;
|
|
1310
|
+
}) as typeof setInterval,
|
|
1311
|
+
clearInterval: (() => {}) as typeof clearInterval,
|
|
1312
|
+
},
|
|
1313
|
+
});
|
|
1314
|
+
let asyncDir: string | undefined;
|
|
1315
|
+
let resultPath: string | undefined;
|
|
1316
|
+
try {
|
|
1317
|
+
const executor = createSubagentExecutor!({
|
|
1318
|
+
pi: { events: piEvents, getSessionName: () => undefined },
|
|
1319
|
+
state,
|
|
1320
|
+
config: {},
|
|
1321
|
+
asyncByDefault: false,
|
|
1322
|
+
tempArtifactsDir: tempDir,
|
|
1323
|
+
getSubagentSessionRoot: () => path.join(tempDir, ".pi-subagents", "sessions"),
|
|
1324
|
+
expandTilde: (value: string) => value,
|
|
1325
|
+
discoverAgents: () => ({ agents: [makeAgent("echo")] }),
|
|
1326
|
+
refreshResultDelivery: watcher.refreshResultDelivery,
|
|
1327
|
+
});
|
|
1328
|
+
const launchPromise = executor.execute(
|
|
1329
|
+
"darwin-immediate-workflow-failure",
|
|
1330
|
+
{ async: true, workflowScript: "{" },
|
|
1331
|
+
new AbortController().signal,
|
|
1332
|
+
undefined,
|
|
1333
|
+
{ ...makeMinimalCtx(tempDir), sessionManager: { getSessionId: () => "session-1700", getSessionFile: () => null } },
|
|
1334
|
+
);
|
|
1335
|
+
watcher.startResultWatcher();
|
|
1336
|
+
assert.equal(pollCreated, true, "expected Darwin demand polling to be armed");
|
|
1337
|
+
const launch = await launchPromise;
|
|
1338
|
+
assert.equal(launch.isError, undefined, launch.content[0]?.text ?? "workflow launch failed");
|
|
1339
|
+
const workflowRunId = launch.details.asyncId;
|
|
1340
|
+
assert.ok(workflowRunId);
|
|
1341
|
+
asyncDir = launch.details.asyncDir;
|
|
1342
|
+
resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
|
|
1343
|
+
for (let attempt = 0; attempt < 100 && (state.asyncJobs.get(workflowRunId)?.status !== "failed" || delivered.length === 0); attempt += 1) {
|
|
1344
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1345
|
+
}
|
|
1346
|
+
assert.equal(state.asyncJobs.get(workflowRunId)?.status, "failed");
|
|
1347
|
+
assert.equal([...state.asyncJobs.values()].some((job) => job.status === "queued" || job.status === "running"), false);
|
|
1348
|
+
assert.deepEqual(delivered, [{ id: workflowRunId, state: "failed" }], "terminal completion must not require a manual result refresh");
|
|
1349
|
+
} finally {
|
|
1350
|
+
watcher.stopResultWatcher();
|
|
1351
|
+
if (asyncDir) fs.rmSync(asyncDir, { recursive: true, force: true });
|
|
1352
|
+
if (resultPath) fs.rmSync(resultPath, { force: true });
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1263
1356
|
it("keeps script workflow phase during async auto-resume", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1264
1357
|
mockPi.onCall({
|
|
1265
1358
|
jsonl: [
|
|
@@ -7641,6 +7734,36 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
7641
7734
|
assert.equal(result.progress.status, "failed");
|
|
7642
7735
|
});
|
|
7643
7736
|
|
|
7737
|
+
it("treats an unchanged pre-existing file-only output as missing on dirty foreground timeout", async () => {
|
|
7738
|
+
execFileSync("git", ["init"], { cwd: tempDir, stdio: "ignore" });
|
|
7739
|
+
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: tempDir });
|
|
7740
|
+
execFileSync("git", ["config", "user.name", "Test User"], { cwd: tempDir });
|
|
7741
|
+
const reportPath = path.join(tempDir, "report.md");
|
|
7742
|
+
fs.writeFileSync(path.join(tempDir, "input.md"), "base\n", "utf-8");
|
|
7743
|
+
fs.writeFileSync(reportPath, "stale report\n", "utf-8");
|
|
7744
|
+
execFileSync("git", ["add", "input.md", "report.md"], { cwd: tempDir });
|
|
7745
|
+
execFileSync("git", ["commit", "-m", "base"], { cwd: tempDir, stdio: "ignore" });
|
|
7746
|
+
|
|
7747
|
+
mockPi.onCall({
|
|
7748
|
+
writeFiles: [{ path: "input.md", content: "changed\n" }],
|
|
7749
|
+
steps: [{ delay: 10_000 }],
|
|
7750
|
+
});
|
|
7751
|
+
|
|
7752
|
+
const result = await runSync(tempDir, makeAgentConfigs(["slow"]), "slow", "Slow task", {
|
|
7753
|
+
timeoutMs: 150,
|
|
7754
|
+
outputPath: reportPath,
|
|
7755
|
+
outputMode: "file-only",
|
|
7756
|
+
acceptance: false,
|
|
7757
|
+
});
|
|
7758
|
+
|
|
7759
|
+
assert.equal(result.timedOut, true);
|
|
7760
|
+
assert.deepEqual(result.timeoutRecovery?.changedFiles, ["input.md"]);
|
|
7761
|
+
assert.equal(result.timeoutRecovery?.reportStatus, "missing");
|
|
7762
|
+
assert.equal(result.timeoutRecovery?.recoveryNeeded, true);
|
|
7763
|
+
assert.match(result.finalOutput ?? "", /requested report: missing/i);
|
|
7764
|
+
assert.equal(fs.readFileSync(reportPath, "utf-8"), "stale report\n");
|
|
7765
|
+
});
|
|
7766
|
+
|
|
7644
7767
|
it("ignores legacy turn-budget options without prompt injection or termination", async () => {
|
|
7645
7768
|
mockPi.onCall({
|
|
7646
7769
|
jsonl: [
|
|
@@ -60,6 +60,54 @@ describe("agent management config parsing", () => {
|
|
|
60
60
|
assert.doesNotMatch(readText(result), /- scout \(builtin/);
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
it("lists compact declared capabilities without exposing system prompts", () => {
|
|
64
|
+
const agentsDir = path.join(tempDir, ".selesai", "agents");
|
|
65
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
66
|
+
fs.writeFileSync(path.join(agentsDir, "capability-worker.md"), [
|
|
67
|
+
"---",
|
|
68
|
+
"name: capability-worker",
|
|
69
|
+
"description: Capability worker",
|
|
70
|
+
"aliases: capability",
|
|
71
|
+
"tools: read, grep, mcp:github/search",
|
|
72
|
+
"model: openai/gpt-5-mini",
|
|
73
|
+
"fallbackModels: openai/gpt-5-mini-fallback",
|
|
74
|
+
"async: true",
|
|
75
|
+
"timeoutMs: 123",
|
|
76
|
+
"thinking: high",
|
|
77
|
+
"skills: typescript-code",
|
|
78
|
+
"extensions: github",
|
|
79
|
+
"subagentOnlyExtensions: surf",
|
|
80
|
+
"mutationTools: edit, write",
|
|
81
|
+
"output: report.md",
|
|
82
|
+
"outputMode: file-only",
|
|
83
|
+
"---",
|
|
84
|
+
"SYSTEM_PROMPT_SENTINEL",
|
|
85
|
+
"---",
|
|
86
|
+
].join("\n"));
|
|
87
|
+
|
|
88
|
+
const capabilityRequest = { agentScope: "project", capabilities: true };
|
|
89
|
+
const listed = handleManagementAction("list", capabilityRequest, {
|
|
90
|
+
cwd: tempDir,
|
|
91
|
+
modelRegistry: { getAvailable: () => [] },
|
|
92
|
+
});
|
|
93
|
+
assert.equal(listed.isError, false);
|
|
94
|
+
const text = readText(listed);
|
|
95
|
+
assert.match(text, /^Executable agents \(capabilities\):/);
|
|
96
|
+
assert.match(text, /- capability-worker \(project, aliases: capability\): Description: Capability worker; Tools: read, grep, mcp:github\/search; Model: openai\/gpt-5-mini; Thinking: high/);
|
|
97
|
+
assert.doesNotMatch(text, /System Prompt:|SYSTEM_PROMPT_SENTINEL/);
|
|
98
|
+
const capabilities = listed.details?.agentCapabilities;
|
|
99
|
+
assert.ok(capabilities);
|
|
100
|
+
const row = capabilities.agents.find((agent) => agent.name === "capability-worker");
|
|
101
|
+
assert.ok(row);
|
|
102
|
+
assert.equal(row.executable, true);
|
|
103
|
+
assert.deepEqual(row.tools, { ambient: false, names: ["read", "grep"], mcpDirectTools: ["github/search"], mutationTools: ["edit", "write"] });
|
|
104
|
+
assert.deepEqual(row.model, { value: "openai/gpt-5-mini", fallbackModels: ["openai/gpt-5-mini-fallback"], thinking: "high" });
|
|
105
|
+
assert.deepEqual(row.execution, { defaultAsync: true, timeoutMs: 123 });
|
|
106
|
+
assert.deepEqual(row.output, { path: "report.md", mode: "file-only" });
|
|
107
|
+
assert.deepEqual(row.extensions, { names: ["github"], subagentOnly: ["surf"], skills: ["typescript-code"] });
|
|
108
|
+
assert.equal(JSON.stringify(capabilities).includes("SYSTEM_PROMPT_SENTINEL"), false);
|
|
109
|
+
});
|
|
110
|
+
|
|
63
111
|
it("rejects management attempts to widen the reserved read-only Claude profile", () => {
|
|
64
112
|
const ctx = { cwd: tempDir, modelRegistry: { getAvailable: () => [] } };
|
|
65
113
|
const writerRunner = { type: "external-cli", adapter: "claude-code-writer", command: "claude" };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { describe, it } from "node:test";
|
|
3
|
-
import type { AsyncJobState, HostStepNodeV1 } from "../../src/shared/types.ts";
|
|
3
|
+
import type { AsyncJobState, HostStepNodeV1, WorkflowGraphSnapshot, WorkflowNodeStatus } from "../../src/shared/types.ts";
|
|
4
4
|
import {
|
|
5
5
|
ASYNC_STATUS_SNAPSHOT_KIND,
|
|
6
6
|
ASYNC_STATUS_SNAPSHOT_VERSION,
|
|
@@ -30,6 +30,27 @@ function hostStep(overrides: Partial<HostStepNodeV1> = {}): HostStepNodeV1 {
|
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
const stagedLaneKeys = ["scope-scout", "red-tests", "label-helpers", "summary-title", "detail-row", "tiers-noise", "validation", "minimality-challenge", "fresh-review"];
|
|
34
|
+
|
|
35
|
+
function stagedLaneGraph(statuses: WorkflowNodeStatus[] = stagedLaneKeys.map((_, index) => index === 0 ? "running" : "pending")): WorkflowGraphSnapshot {
|
|
36
|
+
const nodeIds = stagedLaneKeys.map((key) => `issue-1695.${key}`);
|
|
37
|
+
return {
|
|
38
|
+
runId: "workflow-1695",
|
|
39
|
+
mode: "workflow",
|
|
40
|
+
phases: [{ title: "issue-1695", nodeIds }],
|
|
41
|
+
nodes: stagedLaneKeys.map((key, index) => ({
|
|
42
|
+
id: nodeIds[index]!,
|
|
43
|
+
kind: "step",
|
|
44
|
+
agent: index === 0 ? "scout" : index === 7 ? "simplifier" : "worker",
|
|
45
|
+
label: key,
|
|
46
|
+
status: statuses[index] ?? "pending",
|
|
47
|
+
flatIndex: index,
|
|
48
|
+
stepIndex: index,
|
|
49
|
+
})),
|
|
50
|
+
currentNodeId: nodeIds[statuses.findIndex((status) => status === "running")],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
33
54
|
describe("async status projection", () => {
|
|
34
55
|
it("projects already-loaded jobs in deterministic newest-first order", () => {
|
|
35
56
|
const snapshot = projectAsyncStatusSnapshot([
|
|
@@ -77,6 +98,18 @@ describe("async status projection", () => {
|
|
|
77
98
|
assert.doesNotMatch(serialized, /private\/report|fileMutation|Required file-only output/);
|
|
78
99
|
});
|
|
79
100
|
|
|
101
|
+
it("maps malformed persisted states to partial instead of widening the public state union", () => {
|
|
102
|
+
const snapshot = projectAsyncStatusSnapshot([{
|
|
103
|
+
asyncId: "bad-state",
|
|
104
|
+
asyncDir: "/tmp/bad-state",
|
|
105
|
+
status: "mystery",
|
|
106
|
+
steps: [{ agent: "worker", status: "also-mystery" }],
|
|
107
|
+
} as unknown as AsyncJobState]);
|
|
108
|
+
|
|
109
|
+
assert.equal(snapshot.runs[0]?.state, "partial");
|
|
110
|
+
assert.equal(snapshot.runs[0]?.children?.[0]?.state, "partial");
|
|
111
|
+
});
|
|
112
|
+
|
|
80
113
|
it("projects compact Fleet workflow rows without applying UI bounds", () => {
|
|
81
114
|
const rows = projectAsyncWorkflowRows([{
|
|
82
115
|
agent: "reviewer",
|
|
@@ -84,6 +117,7 @@ describe("async status projection", () => {
|
|
|
84
117
|
label: "Fresh review",
|
|
85
118
|
phase: "quality",
|
|
86
119
|
status: "partial",
|
|
120
|
+
context: "fresh",
|
|
87
121
|
activityState: "needs_attention",
|
|
88
122
|
startedAt: 10,
|
|
89
123
|
tokens: { input: 20, output: 5, total: 25, window: 18 },
|
|
@@ -92,6 +126,7 @@ describe("async status projection", () => {
|
|
|
92
126
|
assert.deepEqual(rows, [{
|
|
93
127
|
name: "quality: review · Fresh review (reviewer)",
|
|
94
128
|
state: "partial",
|
|
129
|
+
context: "fresh",
|
|
95
130
|
activity: "needs attention",
|
|
96
131
|
startedAt: 10,
|
|
97
132
|
tokens: 25,
|
|
@@ -172,6 +207,27 @@ describe("async status projection", () => {
|
|
|
172
207
|
]);
|
|
173
208
|
});
|
|
174
209
|
|
|
210
|
+
it("projects known runs.lanes stages from the workflow graph, including pending stages", () => {
|
|
211
|
+
const rows = projectAsyncWorkflowRows([{
|
|
212
|
+
agent: "scout",
|
|
213
|
+
workflowKey: "issue-1695.scope-scout",
|
|
214
|
+
label: "scope-scout",
|
|
215
|
+
status: "running",
|
|
216
|
+
}], stagedLaneGraph(), {
|
|
217
|
+
version: 1,
|
|
218
|
+
coverage: "complete",
|
|
219
|
+
lanes: [{ key: "issue-1695", mode: "scout" }],
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
for (const [index, key] of stagedLaneKeys.entries()) {
|
|
223
|
+
const row = rows.find((candidate) => candidate.name.includes(key));
|
|
224
|
+
assert.ok(row, `stage ${key} should remain discoverable`);
|
|
225
|
+
assert.equal(row?.state, index === 0 ? "running" : "planned", `stage ${key} state`);
|
|
226
|
+
}
|
|
227
|
+
assert.equal(rows.filter((row) => row.state === "planned").length, stagedLaneKeys.length - 1);
|
|
228
|
+
assert.equal(rows.every((row) => row.preflight?.mode === "scout"), true);
|
|
229
|
+
});
|
|
230
|
+
|
|
175
231
|
it("preserves duplicate loaded rows when a declared lane key is reused", () => {
|
|
176
232
|
const rows = projectAsyncWorkflowRows([
|
|
177
233
|
{ agent: "worker", workflowKey: "writer", label: "First", status: "complete" },
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { backgroundProcessOptions } from "../../src/runs/shared/background-process-options.ts";
|
|
4
|
+
|
|
5
|
+
test("Windows background runners stay hidden without requesting a new console", () => {
|
|
6
|
+
assert.deepEqual(backgroundProcessOptions("win32"), {
|
|
7
|
+
detached: false,
|
|
8
|
+
windowsHide: true,
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("background runners remain detached on Unix", () => {
|
|
13
|
+
assert.deepEqual(backgroundProcessOptions("linux"), {
|
|
14
|
+
detached: true,
|
|
15
|
+
windowsHide: true,
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -150,7 +150,7 @@ describe("external CLI runner", () => {
|
|
|
150
150
|
limits: { parserLineBytes: 64 },
|
|
151
151
|
parser: { parseLine() { return undefined; }, finish() { return { state: "completed" }; } },
|
|
152
152
|
}),
|
|
153
|
-
new Promise<never>((_, reject) => { settleTimer = setTimeout(() => reject(new Error("unterminated oversized line did not fail promptly")),
|
|
153
|
+
new Promise<never>((_, reject) => { settleTimer = setTimeout(() => reject(new Error("unterminated oversized line did not fail promptly")), 10_000); }),
|
|
154
154
|
]);
|
|
155
155
|
assert.equal(unterminated.exitCode, 1);
|
|
156
156
|
assert.match(unterminated.error ?? "", /line exceeded/);
|
|
@@ -258,6 +258,46 @@ describe("below-editor subagent FleetView", () => {
|
|
|
258
258
|
}
|
|
259
259
|
});
|
|
260
260
|
|
|
261
|
+
it("repaints unchanged running entries but keeps queued-only ticks quiet", () => {
|
|
262
|
+
const refreshCount = (status: "running" | "queued"): number => {
|
|
263
|
+
const state = stateForTest();
|
|
264
|
+
state.asyncJobs.set(`run-${status}`, {
|
|
265
|
+
asyncId: `run-${status}`,
|
|
266
|
+
asyncDir: `/tmp/run-${status}`,
|
|
267
|
+
status,
|
|
268
|
+
mode: "single",
|
|
269
|
+
startedAt: 10,
|
|
270
|
+
updatedAt: 20,
|
|
271
|
+
});
|
|
272
|
+
let widgetFactory: ((tui: unknown, theme: typeof theme) => { render(width: number): string[] }) | undefined;
|
|
273
|
+
let renderRequests = 0;
|
|
274
|
+
const ctx = {
|
|
275
|
+
hasUI: true,
|
|
276
|
+
ui: {
|
|
277
|
+
setWidget(_key: string, content: typeof widgetFactory | undefined) { if (content) widgetFactory = content; },
|
|
278
|
+
onTerminalInput() { return () => {}; },
|
|
279
|
+
getEditorText() { return ""; },
|
|
280
|
+
requestRender() {},
|
|
281
|
+
notify() {},
|
|
282
|
+
theme,
|
|
283
|
+
},
|
|
284
|
+
} as unknown as ExtensionContext;
|
|
285
|
+
const fleet = new SubagentFleetStatus(state, () => {}, { refreshMs: 60_000 });
|
|
286
|
+
try {
|
|
287
|
+
fleet.setContext(ctx);
|
|
288
|
+
assert.ok(widgetFactory);
|
|
289
|
+
widgetFactory!({ requestRender() { renderRequests++; } }, theme);
|
|
290
|
+
fleet.refresh();
|
|
291
|
+
return renderRequests;
|
|
292
|
+
} finally {
|
|
293
|
+
fleet.dispose();
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
assert.equal(refreshCount("running"), 1);
|
|
298
|
+
assert.equal(refreshCount("queued"), 0);
|
|
299
|
+
});
|
|
300
|
+
|
|
261
301
|
it("counts project panes in compact status", () => {
|
|
262
302
|
const state = stateForTest();
|
|
263
303
|
const projectRoot = path.join("fixtures", "peer-project");
|
|
@@ -977,8 +1017,8 @@ describe("below-editor subagent FleetView", () => {
|
|
|
977
1017
|
startedAt: 10,
|
|
978
1018
|
updatedAt: 20,
|
|
979
1019
|
steps: [
|
|
980
|
-
{ agent: "scout", workflowKey: "scan", phase: "Plan", label: "Find seams", status: "complete" as const, index: 0, tokens: { input: 10, output: 5, total: 15 } },
|
|
981
|
-
{ agent: "reviewer", workflowKey: "review", phase: "Review", status: "running" as const, index: 1, currentTool: "grep", tokens: { input: 20, output: 5, total: 25 } },
|
|
1020
|
+
{ agent: "scout", workflowKey: "scan", phase: "Plan", label: "Find seams", status: "complete" as const, index: 0, context: "fresh" as const, model: "openai-codex/gpt-5.6-luna", thinking: "max", tokens: { input: 10, output: 5, total: 15 } },
|
|
1021
|
+
{ agent: "reviewer", workflowKey: "review", phase: "Review", status: "running" as const, index: 1, context: "fork" as const, currentTool: "grep", tokens: { input: 20, output: 5, total: 25 } },
|
|
982
1022
|
{ agent: "tester", workflowKey: "test", phase: "Verify", status: "pending" as const, index: 2 },
|
|
983
1023
|
],
|
|
984
1024
|
};
|
|
@@ -1007,8 +1047,8 @@ describe("below-editor subagent FleetView", () => {
|
|
|
1007
1047
|
assert.deepEqual(fleet.handleKey("\x1b[B"), { consume: true });
|
|
1008
1048
|
const lines = component.render(140).join("\n");
|
|
1009
1049
|
assert.match(lines, /workflow · running/);
|
|
1010
|
-
assert.match(lines, /Plan: scan · Find seams \(scout\) · complete/);
|
|
1011
|
-
assert.match(lines, /Review: review \(reviewer\) · running · tool grep/);
|
|
1050
|
+
assert.match(lines, /Plan: scan · Find seams \(scout\) \[fresh\] \(gpt-5\.6-luna · thinking max\) · complete/);
|
|
1051
|
+
assert.match(lines, /Review: review \(reviewer\) \[fork\] · running · tool grep/);
|
|
1012
1052
|
assert.match(lines, /Verify: test \(tester\) · pending/);
|
|
1013
1053
|
} finally {
|
|
1014
1054
|
fleet.dispose();
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { afterEach, describe, it } from "node:test";
|
|
3
|
+
import type { ExtensionContext } from "@selesai/code";
|
|
4
|
+
import { buildPiArgs, deriveForkPromptCacheKey, SUBAGENT_FORK_CACHE_KEY_ENV } from "../../src/runs/shared/pi-args.ts";
|
|
5
|
+
import { rewriteForkCacheProviderRequest } from "../../src/runs/shared/subagent-prompt-runtime.ts";
|
|
6
|
+
|
|
7
|
+
const originalForkCacheKey = process.env[SUBAGENT_FORK_CACHE_KEY_ENV];
|
|
8
|
+
|
|
9
|
+
const baseLaunch = {
|
|
10
|
+
baseArgs: [] as string[],
|
|
11
|
+
task: "test",
|
|
12
|
+
sessionEnabled: false,
|
|
13
|
+
inheritProjectContext: false,
|
|
14
|
+
inheritGlobalContext: false,
|
|
15
|
+
inheritSkills: false,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function providerContext(api: string): Pick<ExtensionContext, "model"> {
|
|
19
|
+
return { model: { api } };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
if (originalForkCacheKey === undefined) delete process.env[SUBAGENT_FORK_CACHE_KEY_ENV];
|
|
24
|
+
else process.env[SUBAGENT_FORK_CACHE_KEY_ENV] = originalForkCacheKey;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("fork prompt cache keys", () => {
|
|
28
|
+
it("derives a bounded sibling cache key from the parent session id", () => {
|
|
29
|
+
assert.equal(deriveForkPromptCacheKey(undefined), undefined);
|
|
30
|
+
assert.equal(deriveForkPromptCacheKey(" "), undefined);
|
|
31
|
+
const key = deriveForkPromptCacheKey("parent-session");
|
|
32
|
+
assert.ok(key);
|
|
33
|
+
assert.match(key, /^pi-fork:[0-9a-f]{56}$/);
|
|
34
|
+
assert.equal(Array.from(key).length, 64);
|
|
35
|
+
assert.equal(key.includes("parent-session"), false);
|
|
36
|
+
|
|
37
|
+
const longKey = deriveForkPromptCacheKey(`${"🙂".repeat(80)}-parent`);
|
|
38
|
+
assert.ok(longKey);
|
|
39
|
+
assert.equal(Array.from(longKey).length, 64);
|
|
40
|
+
assert.match(longKey, /^pi-fork:[0-9a-f]{56}$/);
|
|
41
|
+
|
|
42
|
+
const prefix = "/Users/example/.pi/agent/sessions/".repeat(3);
|
|
43
|
+
assert.notEqual(deriveForkPromptCacheKey(`${prefix}/one.jsonl`), deriveForkPromptCacheKey(`${prefix}/two.jsonl`));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("passes the explicit fork cache key without inheriting ambient values", () => {
|
|
47
|
+
process.env[SUBAGENT_FORK_CACHE_KEY_ENV] = "leaked-parent-value";
|
|
48
|
+
|
|
49
|
+
assert.equal(buildPiArgs(baseLaunch).env[SUBAGENT_FORK_CACHE_KEY_ENV], undefined);
|
|
50
|
+
|
|
51
|
+
const forkCacheKey = deriveForkPromptCacheKey("parent-session");
|
|
52
|
+
assert.equal(buildPiArgs({ ...baseLaunch, forkCacheKey }).env[SUBAGENT_FORK_CACHE_KEY_ENV], forkCacheKey);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("rewrites only existing OpenAI-style string prompt cache keys", () => {
|
|
56
|
+
process.env[SUBAGENT_FORK_CACHE_KEY_ENV] = "pi-fork:parent-session";
|
|
57
|
+
const payload = {
|
|
58
|
+
model: "test-model",
|
|
59
|
+
input: [{ role: "user", content: "hello" }],
|
|
60
|
+
prompt_cache_key: "child-session",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
assert.deepEqual(
|
|
64
|
+
rewriteForkCacheProviderRequest({ type: "before_provider_request", payload }, providerContext("openai-responses")),
|
|
65
|
+
{ ...payload, prompt_cache_key: "pi-fork:parent-session" },
|
|
66
|
+
);
|
|
67
|
+
assert.deepEqual(
|
|
68
|
+
rewriteForkCacheProviderRequest({ type: "before_provider_request", payload: { ...payload, prompt_cache_key: "pi-fork:parent-session" } }, providerContext("openai-responses")),
|
|
69
|
+
{ ...payload, prompt_cache_key: "pi-fork:parent-session" },
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("does not add prompt cache keys or touch non-OpenAI payloads", () => {
|
|
74
|
+
process.env[SUBAGENT_FORK_CACHE_KEY_ENV] = "pi-fork:parent-session";
|
|
75
|
+
|
|
76
|
+
assert.equal(rewriteForkCacheProviderRequest({ type: "before_provider_request", payload: "raw" }, providerContext("openai-responses")), undefined);
|
|
77
|
+
assert.equal(rewriteForkCacheProviderRequest({ type: "before_provider_request", payload: [] }, providerContext("openai-responses")), undefined);
|
|
78
|
+
assert.equal(rewriteForkCacheProviderRequest({ type: "before_provider_request", payload: { model: "test" } }, providerContext("openai-responses")), undefined);
|
|
79
|
+
assert.equal(rewriteForkCacheProviderRequest({ type: "before_provider_request", payload: { model: "test", prompt_cache_key: undefined } }, providerContext("openai-responses")), undefined);
|
|
80
|
+
assert.equal(rewriteForkCacheProviderRequest({ type: "before_provider_request", payload: { model: "test", prompt_cache_key: "child" } }, providerContext("anthropic-messages")), undefined);
|
|
81
|
+
assert.equal(rewriteForkCacheProviderRequest({ type: "before_provider_request", payload: { model: "test", prompt_cache_key: "child" } }, providerContext("unknown-api")), undefined);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("is a no-op when fork cache affinity is not configured", () => {
|
|
85
|
+
delete process.env[SUBAGENT_FORK_CACHE_KEY_ENV];
|
|
86
|
+
assert.equal(
|
|
87
|
+
rewriteForkCacheProviderRequest({ type: "before_provider_request", payload: { model: "test", prompt_cache_key: "child" } }, providerContext("openai-responses")),
|
|
88
|
+
undefined,
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -33,6 +33,7 @@ describe("workflow host commands", () => {
|
|
|
33
33
|
assert.throws(() => normalizeWorkflowHostCommandParams({ kind: "command", command: "true", timeoutMs: 86_400_001 }), /integer from 1/);
|
|
34
34
|
assert.throws(() => normalizeWorkflowHostCommandParams({ kind: "command", command: "true", timeoutMs: 1000, output: path.resolve("outside.log") }), /relative path/);
|
|
35
35
|
assert.throws(() => normalizeWorkflowHostCommandParams({ kind: "command", command: "true", timeoutMs: 1000, provider: "bad\nprovider" }), /single-line/);
|
|
36
|
+
assert.throws(() => normalizeWorkflowHostCommandParams({ kind: "command", command: "true", timeoutMs: 1000, cwd: "/tmp" }), /does not accept per-step cwd.*workflow cwd.*outer subagent request.*cd \/path\/to\/worktree/);
|
|
36
37
|
});
|
|
37
38
|
|
|
38
39
|
it("runs without stdin and saves bounded command evidence", async () => {
|
|
@@ -476,7 +476,6 @@ describe("subagent extension child mode", () => {
|
|
|
476
476
|
const lines = result.render(120);
|
|
477
477
|
if (lines.length !== 6) throw new Error("expected outer spacer, box rows, and three capped result rows: " + JSON.stringify(lines));
|
|
478
478
|
if (!lines[4].includes("rows hidden")) throw new Error("compact cap was not applied: " + JSON.stringify(lines));
|
|
479
|
-
if (!lines[3].includes(" ✓ Agent 1/3: scout")) throw new Error("zero spacing was not applied: " + JSON.stringify(lines));
|
|
480
479
|
`;
|
|
481
480
|
const env = parentToolEnv();
|
|
482
481
|
env.SELESAI_CODING_AGENT_DIR = agentDir;
|
|
@@ -19,8 +19,8 @@ test("plans server grants from explicit metadata facts and preserves resource fi
|
|
|
19
19
|
|
|
20
20
|
assert.deepEqual(grant, {
|
|
21
21
|
selections: [
|
|
22
|
-
{ name: "
|
|
23
|
-
{ name: "
|
|
22
|
+
{ name: "browser-mcp_navigate", selector: "browser-mcp/navigate" },
|
|
23
|
+
{ name: "browser-mcp_get_console_logs", selector: "browser-mcp/get_console_logs" },
|
|
24
24
|
],
|
|
25
25
|
unresolvedSelectors: [],
|
|
26
26
|
});
|
|
@@ -95,7 +95,7 @@ test("enforces server includeTools before exclusions for tools and generated res
|
|
|
95
95
|
|
|
96
96
|
test("matches include and exclude patterns against raw and alternate prefix names", () => {
|
|
97
97
|
for (const [toolPrefix, expectedName] of [
|
|
98
|
-
["server", "
|
|
98
|
+
["server", "demo-mcp_search-records"],
|
|
99
99
|
["short", "demo_search-records"],
|
|
100
100
|
["none", "search-records"],
|
|
101
101
|
] as const) {
|
|
@@ -117,6 +117,23 @@ test("matches include and exclude patterns against raw and alternate prefix name
|
|
|
117
117
|
}
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
+
test("matches adapter names for test-mcp/example in every prefix mode", () => {
|
|
121
|
+
for (const [toolPrefix, expectedName] of [
|
|
122
|
+
["server", "test-mcp_example"],
|
|
123
|
+
["short", "test_example"],
|
|
124
|
+
["none", "example"],
|
|
125
|
+
] as const) {
|
|
126
|
+
const grant = planMcpDirectToolGrant({
|
|
127
|
+
selectors: ["test-mcp"],
|
|
128
|
+
servers: { "test-mcp": {} },
|
|
129
|
+
metadata: { "test-mcp": { tools: [{ name: "example" }] } },
|
|
130
|
+
toolPrefix,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
assert.deepEqual(grant.selections, [{ name: expectedName, selector: "test-mcp/example" }]);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
120
137
|
test("supports adapter-compatible question-mark patterns", () => {
|
|
121
138
|
const grant = planMcpDirectToolGrant({
|
|
122
139
|
selectors: ["demo"],
|
|
@@ -147,4 +147,31 @@ describe("tracked mutation evidence", () => {
|
|
|
147
147
|
assert.match(summary.message, /changed tracked files: tracked\.txt/);
|
|
148
148
|
assert.match(summary.message, /active tool: edit/);
|
|
149
149
|
});
|
|
150
|
+
|
|
151
|
+
it("classifies dirty timeouts with a missing requested report as recovery-needed", () => {
|
|
152
|
+
const changedFiles = Array.from({ length: 25 }, (_, index) => `src/file-${String(index + 1).padStart(2, "0")}.ts`);
|
|
153
|
+
const summary = buildTimeoutRecoverySummary({
|
|
154
|
+
termination: "timed-out",
|
|
155
|
+
evidence: {
|
|
156
|
+
source: "tracked-files",
|
|
157
|
+
trackedOnly: true,
|
|
158
|
+
changedFiles,
|
|
159
|
+
attemptedMutation: true,
|
|
160
|
+
},
|
|
161
|
+
requiredOutputMissing: true,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
assert.equal(summary.changedFiles.length, 20);
|
|
165
|
+
assert.deepEqual(summary.changedFiles.slice(0, 2), ["src/file-01.ts", "src/file-02.ts"]);
|
|
166
|
+
assert.equal(summary.truncated, true);
|
|
167
|
+
assert.match(summary.message, /\.\.\. \(5 more\)/);
|
|
168
|
+
assert.doesNotMatch(summary.message, /src\/file-25\.ts/);
|
|
169
|
+
assert.equal(summary.recoveryNeeded, true);
|
|
170
|
+
assert.equal(summary.reason, "timed-out-with-dirty-worktree");
|
|
171
|
+
assert.equal(summary.reportStatus, "missing");
|
|
172
|
+
assert.match(summary.message, /Recovery needed/i);
|
|
173
|
+
assert.match(summary.message, /requested report: missing/i);
|
|
174
|
+
assert.match(summary.message, /review (?:the )?diff and artifacts before resuming/i);
|
|
175
|
+
assert.match(summary.message, /dependent stages/i);
|
|
176
|
+
});
|
|
150
177
|
});
|