@koda-sl/baker-bridge 0.172.0-dev.b570aa3f2 → 0.172.0-dev.f1dc1ec56
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/README.md +51 -67
- package/dist/builder/builder-turn.d.ts +28 -0
- package/dist/builder/builder-turn.d.ts.map +1 -0
- package/dist/builder/builder-turn.js +17 -0
- package/dist/builder/builder-turn.js.map +1 -0
- package/dist/hono/agent-session.d.ts.map +1 -1
- package/dist/hono/agent-session.js +4 -3
- package/dist/hono/agent-session.js.map +1 -1
- package/dist/hono/agent-session.test.js +4 -3
- package/dist/hono/agent-session.test.js.map +1 -1
- package/dist/hono/agent.d.ts.map +1 -1
- package/dist/hono/agent.js +5 -6
- package/dist/hono/agent.js.map +1 -1
- package/dist/hono/agent.test.js +91 -177
- package/dist/hono/agent.test.js.map +1 -1
- package/dist/hono/attachments.d.ts.map +1 -1
- package/dist/hono/attachments.js +9 -1
- package/dist/hono/attachments.js.map +1 -1
- package/dist/hono/attachments.test.js +22 -1
- package/dist/hono/attachments.test.js.map +1 -1
- package/dist/hono/bridge-job.d.ts +16 -28
- package/dist/hono/bridge-job.d.ts.map +1 -1
- package/dist/hono/bridge-job.js +60 -189
- package/dist/hono/bridge-job.js.map +1 -1
- package/dist/hono/bridge-job.test.js +99 -305
- package/dist/hono/bridge-job.test.js.map +1 -1
- package/dist/hono/flow-input-card-fields.test.js +1 -1
- package/dist/hono/flow-input.d.ts +1 -1
- package/dist/hono/flow-input.js +1 -1
- package/dist/hono/main-sync.d.ts +22 -0
- package/dist/hono/main-sync.d.ts.map +1 -1
- package/dist/hono/main-sync.js +110 -2
- package/dist/hono/main-sync.js.map +1 -1
- package/dist/hono/main-sync.test.js +127 -1
- package/dist/hono/main-sync.test.js.map +1 -1
- package/dist/hono/model-capabilities.d.ts +0 -1
- package/dist/hono/model-capabilities.d.ts.map +1 -1
- package/dist/hono/model-capabilities.js +0 -4
- package/dist/hono/model-capabilities.js.map +1 -1
- package/dist/hono/openrouter-session.d.ts +13 -19
- package/dist/hono/openrouter-session.d.ts.map +1 -1
- package/dist/hono/openrouter-session.js +13 -19
- package/dist/hono/openrouter-session.js.map +1 -1
- package/dist/hono/publish-handoff.d.ts +0 -1
- package/dist/hono/publish-handoff.d.ts.map +1 -1
- package/dist/hono/publish-handoff.js +0 -53
- package/dist/hono/publish-handoff.js.map +1 -1
- package/dist/hono/publish-handoff.test.js +0 -48
- package/dist/hono/publish-handoff.test.js.map +1 -1
- package/dist/hono/relay.d.ts +0 -8
- package/dist/hono/relay.d.ts.map +1 -1
- package/dist/hono/relay.js +0 -2
- package/dist/hono/relay.js.map +1 -1
- package/dist/hono/relay.test.js +0 -30
- package/dist/hono/relay.test.js.map +1 -1
- package/dist/hono/runtime-files.d.ts +0 -1
- package/dist/hono/runtime-files.d.ts.map +1 -1
- package/dist/hono/runtime-files.js +0 -3
- package/dist/hono/runtime-files.js.map +1 -1
- package/dist/hono/served-model.d.ts +0 -8
- package/dist/hono/served-model.d.ts.map +1 -1
- package/dist/hono/served-model.js +0 -41
- package/dist/hono/served-model.js.map +1 -1
- package/dist/hono/served-model.test.js +1 -19
- package/dist/hono/served-model.test.js.map +1 -1
- package/dist/hono/session-continuity.integration.test.js +0 -2
- package/dist/hono/session-continuity.integration.test.js.map +1 -1
- package/dist/hono/types.d.ts +7 -12
- package/dist/hono/types.d.ts.map +1 -1
- package/dist/review/digest.d.ts +0 -13
- package/dist/review/digest.d.ts.map +1 -1
- package/dist/review/digest.js +1 -1
- package/dist/review/digest.js.map +1 -1
- package/dist/review/structured-output-gate.d.ts +10 -0
- package/dist/review/structured-output-gate.d.ts.map +1 -1
- package/dist/review/structured-output-gate.js +15 -5
- package/dist/review/structured-output-gate.js.map +1 -1
- package/package.json +1 -2
- package/dist/hono/subagent-route.d.ts +0 -64
- package/dist/hono/subagent-route.d.ts.map +0 -1
- package/dist/hono/subagent-route.js +0 -147
- package/dist/hono/subagent-route.js.map +0 -1
- package/dist/hono/subagent-route.test.d.ts +0 -2
- package/dist/hono/subagent-route.test.d.ts.map +0 -1
- package/dist/hono/subagent-route.test.js +0 -68
- package/dist/hono/subagent-route.test.js.map +0 -1
- package/dist/hono/tool-result-ledger.d.ts +0 -32
- package/dist/hono/tool-result-ledger.d.ts.map +0 -1
- package/dist/hono/tool-result-ledger.js +0 -67
- package/dist/hono/tool-result-ledger.js.map +0 -1
- package/dist/hono/tool-result-ledger.test.d.ts +0 -2
- package/dist/hono/tool-result-ledger.test.d.ts.map +0 -1
- package/dist/hono/tool-result-ledger.test.js +0 -61
- package/dist/hono/tool-result-ledger.test.js.map +0 -1
- package/dist/hono/turn-route.d.ts +0 -84
- package/dist/hono/turn-route.d.ts.map +0 -1
- package/dist/hono/turn-route.js +0 -121
- package/dist/hono/turn-route.js.map +0 -1
- package/dist/hono/turn-route.test.d.ts +0 -2
- package/dist/hono/turn-route.test.d.ts.map +0 -1
- package/dist/hono/turn-route.test.js +0 -134
- package/dist/hono/turn-route.test.js.map +0 -1
- package/dist/hono/workspace-instructions.d.ts +0 -43
- package/dist/hono/workspace-instructions.d.ts.map +0 -1
- package/dist/hono/workspace-instructions.js +0 -68
- package/dist/hono/workspace-instructions.js.map +0 -1
- package/dist/hono/workspace-instructions.test.d.ts +0 -2
- package/dist/hono/workspace-instructions.test.d.ts.map +0 -1
- package/dist/hono/workspace-instructions.test.js +0 -68
- package/dist/hono/workspace-instructions.test.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { mkdir, mkdtemp,
|
|
2
|
+
import { mkdir, mkdtemp, rm, utimes, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
@@ -9,19 +9,11 @@ vi.mock("./env.ts", () => ({
|
|
|
9
9
|
// The skills allowlist is read off the workspace, so the project root has to be
|
|
10
10
|
// steerable to a fixture directory. Defaults to the real root for every other test.
|
|
11
11
|
const projectRootMock = vi.hoisted(() => ({ value: process.cwd() }));
|
|
12
|
-
// Where the per-company instructions import is written; kept out of the real ~/.baker.
|
|
13
|
-
const instructionsDirMock = vi.hoisted(() => ({
|
|
14
|
-
value: `${process.env.TMPDIR ?? "/tmp"}/baker-job-instructions-${process.pid}`,
|
|
15
|
-
}));
|
|
16
12
|
vi.mock("./runtime-files.ts", async (importOriginal) => {
|
|
17
13
|
const actual = await importOriginal();
|
|
18
14
|
return {
|
|
19
15
|
...actual,
|
|
20
|
-
bridgeRuntimeFiles: {
|
|
21
|
-
...actual.bridgeRuntimeFiles,
|
|
22
|
-
projectRoot: () => projectRootMock.value,
|
|
23
|
-
instructionsDir: () => instructionsDirMock.value,
|
|
24
|
-
},
|
|
16
|
+
bridgeRuntimeFiles: { ...actual.bridgeRuntimeFiles, projectRoot: () => projectRootMock.value },
|
|
25
17
|
};
|
|
26
18
|
});
|
|
27
19
|
import { AVATAR_CASTING_TOOL_NAME } from "./avatar-casting.js";
|
|
@@ -262,9 +254,7 @@ function createExecutor(options) {
|
|
|
262
254
|
...(options.catchUpWithMain ? { catchUpWithMain: options.catchUpWithMain } : {}),
|
|
263
255
|
...(options.keepCliInStep ? { keepCliInStep: options.keepCliInStep } : {}),
|
|
264
256
|
...(options.holdsNoWork ? { holdsNoWork: options.holdsNoWork } : {}),
|
|
265
|
-
|
|
266
|
-
routeJudge: options.routeJudge ?? (async () => null),
|
|
267
|
-
...(options.routeModels ? { routeModels: true } : {}),
|
|
257
|
+
...(options.settleAfterPublish ? { settleAfterPublish: options.settleAfterPublish } : {}),
|
|
268
258
|
});
|
|
269
259
|
}
|
|
270
260
|
function memoryInFlightTurns(initial) {
|
|
@@ -1017,39 +1007,6 @@ describe("BridgeJobExecutor", () => {
|
|
|
1017
1007
|
],
|
|
1018
1008
|
});
|
|
1019
1009
|
});
|
|
1020
|
-
// Tool results are most of what a turn writes to the prompt cache, and nothing
|
|
1021
|
-
// recorded their size: the turn now reports it next to its model usage.
|
|
1022
|
-
it("reports how much each tool's results put in the context this turn", async () => {
|
|
1023
|
-
const executor = createExecutor({
|
|
1024
|
-
queryResults: [
|
|
1025
|
-
scriptedMessages({
|
|
1026
|
-
type: "assistant",
|
|
1027
|
-
parent_tool_use_id: null,
|
|
1028
|
-
uuid: "assistant_tool",
|
|
1029
|
-
session_id: "session_tools_size",
|
|
1030
|
-
message: {
|
|
1031
|
-
id: "msg_tool",
|
|
1032
|
-
role: "assistant",
|
|
1033
|
-
content: [{ type: "tool_use", id: "tool_1", name: "Bash", input: { command: "baker ads meta ads" } }],
|
|
1034
|
-
},
|
|
1035
|
-
}, {
|
|
1036
|
-
type: "user",
|
|
1037
|
-
parent_tool_use_id: null,
|
|
1038
|
-
uuid: "user_tool_result",
|
|
1039
|
-
session_id: "session_tools_size",
|
|
1040
|
-
message: {
|
|
1041
|
-
role: "user",
|
|
1042
|
-
content: [{ type: "tool_result", tool_use_id: "tool_1", content: "r".repeat(800) }],
|
|
1043
|
-
},
|
|
1044
|
-
}, resultMessage(0.1, "session_tools_size")),
|
|
1045
|
-
],
|
|
1046
|
-
});
|
|
1047
|
-
const outcome = await executor.execute({ threadId: "thread_tools_size", turnId: "turn_tools_size", prompt: "How are the ads?", history: history([]) }, activeControls());
|
|
1048
|
-
expect(outcome).toMatchObject({
|
|
1049
|
-
kind: "completed",
|
|
1050
|
-
toolResults: [{ tool: "baker ads meta", agent: "main", calls: 1, bytes: 800, tokens: 200 }],
|
|
1051
|
-
});
|
|
1052
|
-
});
|
|
1053
1010
|
// OpenRouter answers under its own slug. A CLI resuming a transcript that names
|
|
1054
1011
|
// the slug drops every earlier thinking block from its first request, which
|
|
1055
1012
|
// breaks the prompt cache right after the system prompt (BAKER-331).
|
|
@@ -1104,6 +1061,25 @@ describe("BridgeJobExecutor", () => {
|
|
|
1104
1061
|
// would resume a transcript holding one `/publish` and nothing else.
|
|
1105
1062
|
expect(savedBoundaries).toEqual([]);
|
|
1106
1063
|
});
|
|
1064
|
+
it("settles the chat's Publish once a publish turn ends, and never after a chat turn", async () => {
|
|
1065
|
+
let settled = 0;
|
|
1066
|
+
const settleAfterPublish = () => {
|
|
1067
|
+
settled += 1;
|
|
1068
|
+
return Promise.resolve();
|
|
1069
|
+
};
|
|
1070
|
+
const publishing = createExecutor({
|
|
1071
|
+
queryResults: [scriptedMessages(resultMessage(0.3, "session_publish_own"))],
|
|
1072
|
+
settleAfterPublish,
|
|
1073
|
+
});
|
|
1074
|
+
const chatting = createExecutor({
|
|
1075
|
+
queryResults: [scriptedMessages(resultMessage(0.3, "session_chat"))],
|
|
1076
|
+
settleAfterPublish,
|
|
1077
|
+
});
|
|
1078
|
+
await publishing.execute({ threadId: "thread_publish", turnId: "turn_publish", prompt: "/publish", history: history([]) }, activeControls());
|
|
1079
|
+
const afterPublish = settled;
|
|
1080
|
+
await chatting.execute({ threadId: "thread_chat", turnId: "turn_chat", prompt: "hello" }, activeControls());
|
|
1081
|
+
expect([afterPublish, settled]).toEqual([1, 1]);
|
|
1082
|
+
});
|
|
1107
1083
|
it("leaves the chat's session alone when a publish fails", async () => {
|
|
1108
1084
|
const clearCalls = [];
|
|
1109
1085
|
const executor = createExecutor({
|
|
@@ -1332,19 +1308,16 @@ describe("BridgeJobExecutor", () => {
|
|
|
1332
1308
|
"x-session-id: thread_env",
|
|
1333
1309
|
].join("\n"));
|
|
1334
1310
|
});
|
|
1335
|
-
//
|
|
1336
|
-
//
|
|
1337
|
-
|
|
1338
|
-
// after the first tool exchange. Pinned on rather than left to the CLI's own
|
|
1339
|
-
// default, which a remote feature gate can switch off.
|
|
1340
|
-
it("turns on per-turn effort, so a turn on another effort level keeps the prompt cache", async () => {
|
|
1311
|
+
// OpenRouter has no Bedrock endpoint for a per-message effort, so while the CLI
|
|
1312
|
+
// sends one every Opus 5.5 request skips Bedrock and bills OpenRouter credits.
|
|
1313
|
+
it("keeps the agent's model requests servable by Bedrock", async () => {
|
|
1341
1314
|
const queryInputs = [];
|
|
1342
1315
|
const executor = createExecutor({
|
|
1343
1316
|
queryInputs,
|
|
1344
1317
|
queryResults: [scriptedMessages(resultMessage(0, "session_bedrock"))],
|
|
1345
1318
|
});
|
|
1346
1319
|
await executor.execute({ threadId: "thread_bedrock", turnId: "turn_bedrock", prompt: "Build a landing", history: history([]) }, activeControls());
|
|
1347
|
-
expect(queryInputs[0]?.options.env).toMatchObject({ CLAUDE_CODE_MODEL_CAPABILITIES: "per_turn_effort" });
|
|
1320
|
+
expect(queryInputs[0]?.options.env).toMatchObject({ CLAUDE_CODE_MODEL_CAPABILITIES: "-per_turn_effort" });
|
|
1348
1321
|
});
|
|
1349
1322
|
// The CLI names each session with a structured-output request OpenRouter's Bedrock
|
|
1350
1323
|
// cannot serve, so it went to Azure on OpenRouter credits; nothing reads the name.
|
|
@@ -1455,134 +1428,6 @@ describe("BridgeJobExecutor", () => {
|
|
|
1455
1428
|
expect(disallowed).not.toContain("Task");
|
|
1456
1429
|
expect(disallowed).not.toContain("SendMessage");
|
|
1457
1430
|
});
|
|
1458
|
-
// Developer tools a marketer's chat never calls (0 calls across 358
|
|
1459
|
-
// production chats, Oct 2026). Each definition rides every request a session
|
|
1460
|
-
// makes — DesignSync's alone is ~9k characters — whenever the CLI does not
|
|
1461
|
-
// defer it.
|
|
1462
|
-
it("removes the developer tools a marketer's chat never calls", async () => {
|
|
1463
|
-
const queryInputs = [];
|
|
1464
|
-
const executor = createExecutor({
|
|
1465
|
-
queryInputs,
|
|
1466
|
-
queryResults: [scriptedMessages(resultMessage(0, "session_dev_tools"))],
|
|
1467
|
-
});
|
|
1468
|
-
await executor.execute({ threadId: "thread_dev_tools", turnId: "turn_dev_tools", prompt: "Build a landing", history: history([]) }, activeControls());
|
|
1469
|
-
const disallowed = queryInputs[0]?.options.disallowedTools ?? [];
|
|
1470
|
-
expect(disallowed).toEqual(expect.arrayContaining(["DesignSync", "ReportFindings", "NotebookEdit"]));
|
|
1471
|
-
});
|
|
1472
|
-
// The scaffold's CLAUDE.md is the same text for every company on a scaffold
|
|
1473
|
-
// version, but loaded as a memory file it lands in the first user message with
|
|
1474
|
-
// no cache breakpoint after it, so every chat wrote its ~24k tokens cold. In the
|
|
1475
|
-
// system prompt it ends on a breakpoint and one chat reads another's copy.
|
|
1476
|
-
it("sends the workspace's shared instructions as the system prompt and loads COMPANY.md on its own", async () => {
|
|
1477
|
-
const workspace = await mkdtemp(join(tmpdir(), "baker-job-instructions-"));
|
|
1478
|
-
await writeFile(join(workspace, "CLAUDE.md"), "# Project Instructions\n\n## Client Context\n\nOne client per repo. Everything known about them:\n\n@COMPANY.md\n\n## Rules\n\nStage, don't ask.\n");
|
|
1479
|
-
projectRootMock.value = workspace;
|
|
1480
|
-
const queryInputs = [];
|
|
1481
|
-
const executor = createExecutor({
|
|
1482
|
-
queryInputs,
|
|
1483
|
-
queryResults: [scriptedMessages(resultMessage(0, "session_instructions"))],
|
|
1484
|
-
});
|
|
1485
|
-
await executor.execute({ threadId: "thread_instructions", turnId: "turn_instructions", prompt: "Build a landing", history: history([]) }, activeControls());
|
|
1486
|
-
const options = queryInputs[0]?.options;
|
|
1487
|
-
expect(options.systemPrompt).toEqual({
|
|
1488
|
-
type: "preset",
|
|
1489
|
-
preset: "claude_code",
|
|
1490
|
-
append: "# Project Instructions\n\n## Rules\n\nStage, don't ask.\n",
|
|
1491
|
-
snapshot: true,
|
|
1492
|
-
});
|
|
1493
|
-
expect(options.settings).toMatchObject({ claudeMdExcludes: [join(workspace, "CLAUDE.md")] });
|
|
1494
|
-
expect(options.additionalDirectories).toEqual([instructionsDirMock.value]);
|
|
1495
|
-
expect(options.env?.CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD).toBe("1");
|
|
1496
|
-
expect(await readFile(join(instructionsDirMock.value, "CLAUDE.md"), "utf8")).toBe(`@${join(workspace, "COMPANY.md")}\n`);
|
|
1497
|
-
});
|
|
1498
|
-
// Excluding CLAUDE.md from the memory files took it from every subagent that
|
|
1499
|
-
// loaded it there too; measured through the real CLI, `general-purpose` and
|
|
1500
|
-
// `research` lost it while still reading COMPANY.md.
|
|
1501
|
-
it("gives the shared instructions back to the subagents that loaded CLAUDE.md, and only to them", async () => {
|
|
1502
|
-
const workspace = await mkdtemp(join(tmpdir(), "baker-job-subagent-instructions-"));
|
|
1503
|
-
await writeFile(join(workspace, "CLAUDE.md"), "# Project Instructions\n\n## Client Context\n\nOne client per repo. Everything known about them:\n\n@COMPANY.md\n\n## Rules\n\nStage, don't ask.\n");
|
|
1504
|
-
await mkdir(join(workspace, ".claude", "agents"), { recursive: true });
|
|
1505
|
-
await writeFile(join(workspace, ".claude", "agents", "claim-checker.md"), "---\nname: claim-checker\nmodel: claude-sonnet-5-5\nomitClaudeMd: true\n---\nCheck claims.\n");
|
|
1506
|
-
projectRootMock.value = workspace;
|
|
1507
|
-
const queryInputs = [];
|
|
1508
|
-
const executor = createExecutor({
|
|
1509
|
-
queryInputs,
|
|
1510
|
-
queryResults: [scriptedMessages(resultMessage(0, "session_subagent_instructions"))],
|
|
1511
|
-
});
|
|
1512
|
-
await executor.execute({ threadId: "thread_sub_instr", turnId: "turn_sub_instr", prompt: "Build a landing", history: history([]) }, activeControls());
|
|
1513
|
-
const start = queryInputs[0]?.options.hooks?.SubagentStart?.[0]?.hooks[0];
|
|
1514
|
-
const contextFor = async (agentType) => (await start?.({
|
|
1515
|
-
hook_event_name: "SubagentStart",
|
|
1516
|
-
agent_type: agentType,
|
|
1517
|
-
agent_id: "a",
|
|
1518
|
-
session_id: "s",
|
|
1519
|
-
transcript_path: "",
|
|
1520
|
-
cwd: "",
|
|
1521
|
-
}, undefined, { signal: new AbortController().signal }))?.hookSpecificOutput?.additionalContext;
|
|
1522
|
-
expect(await contextFor("general-purpose")).toContain("Stage, don't ask.");
|
|
1523
|
-
expect(await contextFor("general-purpose")).not.toContain("@COMPANY.md");
|
|
1524
|
-
expect(await contextFor("claim-checker")).toBeUndefined();
|
|
1525
|
-
expect(await contextFor("fork")).toBeUndefined();
|
|
1526
|
-
});
|
|
1527
|
-
it("moves a built-in subagent that only reads to Sonnet, and records it with the turn", async () => {
|
|
1528
|
-
const queryInputs = [];
|
|
1529
|
-
let spawned;
|
|
1530
|
-
const executor = createExecutor({
|
|
1531
|
-
queryInputs,
|
|
1532
|
-
model: "claude-opus-5-5",
|
|
1533
|
-
routeModels: true,
|
|
1534
|
-
// The turn: finished work, so Opus. The spawn: only reading.
|
|
1535
|
-
routeJudge: (questions) => Promise.resolve(String(questions[0]?.instructions.message).includes("Check which step")
|
|
1536
|
-
? [0.2, 0.8, 0.9, 0.1]
|
|
1537
|
-
: [0.1, 0.9, 0.1, 0.9]),
|
|
1538
|
-
queryResults: [
|
|
1539
|
-
{
|
|
1540
|
-
async *[Symbol.asyncIterator]() {
|
|
1541
|
-
const preToolUse = queryInputs[0]?.options.hooks?.PreToolUse?.find((m) => m.matcher === "Agent")?.hooks[0];
|
|
1542
|
-
const result = (await preToolUse?.({
|
|
1543
|
-
hook_event_name: "PreToolUse",
|
|
1544
|
-
tool_name: "Agent",
|
|
1545
|
-
tool_input: {
|
|
1546
|
-
subagent_type: "general-purpose",
|
|
1547
|
-
description: "Walk the flow",
|
|
1548
|
-
prompt: "Check which step fires the conversion.",
|
|
1549
|
-
},
|
|
1550
|
-
tool_use_id: "toolu_1",
|
|
1551
|
-
session_id: "s",
|
|
1552
|
-
transcript_path: "",
|
|
1553
|
-
cwd: "",
|
|
1554
|
-
}, "toolu_1", { signal: new AbortController().signal }));
|
|
1555
|
-
spawned = result.hookSpecificOutput?.updatedInput;
|
|
1556
|
-
yield resultMessage(0, "session_subagent_route");
|
|
1557
|
-
},
|
|
1558
|
-
},
|
|
1559
|
-
],
|
|
1560
|
-
});
|
|
1561
|
-
const outcome = await executor.execute({ threadId: "thread_sub_route", turnId: "turn_sub_route", prompt: "Rebuild the landing", history: history([]) }, activeControls());
|
|
1562
|
-
expect(spawned).toMatchObject({ subagent_type: "general-purpose", model: "sonnet", effort: "high" });
|
|
1563
|
-
expect(outcome).toMatchObject({
|
|
1564
|
-
turnRoute: {
|
|
1565
|
-
model: "claude-opus-5-5",
|
|
1566
|
-
subagents: [{ agentType: "general-purpose", model: "sonnet", effort: "high", reason: "read_only" }],
|
|
1567
|
-
},
|
|
1568
|
-
});
|
|
1569
|
-
});
|
|
1570
|
-
it("lets the CLI load a CLAUDE.md it cannot split, exactly as before", async () => {
|
|
1571
|
-
const workspace = await mkdtemp(join(tmpdir(), "baker-job-instructions-old-"));
|
|
1572
|
-
await writeFile(join(workspace, "CLAUDE.md"), "# Project Instructions\n\nNo company import.\n");
|
|
1573
|
-
projectRootMock.value = workspace;
|
|
1574
|
-
const queryInputs = [];
|
|
1575
|
-
const executor = createExecutor({
|
|
1576
|
-
queryInputs,
|
|
1577
|
-
queryResults: [scriptedMessages(resultMessage(0, "session_instructions_old"))],
|
|
1578
|
-
});
|
|
1579
|
-
await executor.execute({ threadId: "thread_instructions_old", turnId: "turn_instructions_old", prompt: "Hi", history: history([]) }, activeControls());
|
|
1580
|
-
const options = queryInputs[0]?.options;
|
|
1581
|
-
expect(options.systemPrompt).toBeUndefined();
|
|
1582
|
-
expect(options.additionalDirectories).toBeUndefined();
|
|
1583
|
-
expect(options.settings ?? {}).not.toHaveProperty("claudeMdExcludes");
|
|
1584
|
-
expect(options.env?.CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD).toBeUndefined();
|
|
1585
|
-
});
|
|
1586
1431
|
it("allowlists the workspace's own skills so the CLI's built-in developer skills stay out of the session", async () => {
|
|
1587
1432
|
const workspace = await mkdtemp(join(tmpdir(), "baker-job-skills-"));
|
|
1588
1433
|
for (const skill of ["landing", "brand-build", "docx"]) {
|
|
@@ -1761,10 +1606,11 @@ describe("BridgeJobExecutor", () => {
|
|
|
1761
1606
|
await executor.execute({ threadId: "thread_nobeta", turnId: "turn_nobeta", prompt: "Hi", history: history([]) }, activeControls());
|
|
1762
1607
|
expect(queryInputs[0]?.options).not.toHaveProperty("betas");
|
|
1763
1608
|
});
|
|
1764
|
-
// Effort governs thinking depth, preamble and tool-call count
|
|
1765
|
-
//
|
|
1766
|
-
//
|
|
1767
|
-
|
|
1609
|
+
// Effort governs thinking depth, preamble and tool-call count, and the API
|
|
1610
|
+
// default is `high`. A silent drift back to the default costs money twice:
|
|
1611
|
+
// more tokens per turn, and — because the resolved effort is rendered into
|
|
1612
|
+
// the prompt — a cache-breakpoint invalidation on the turn it changes.
|
|
1613
|
+
it("sends a single constant effort so the prompt cache is never invalidated by it", async () => {
|
|
1768
1614
|
const queryInputs = [];
|
|
1769
1615
|
const executor = createExecutor({
|
|
1770
1616
|
queryInputs,
|
|
@@ -1777,105 +1623,8 @@ describe("BridgeJobExecutor", () => {
|
|
|
1777
1623
|
await executor.execute({ threadId: "thread_effort", turnId: "turn_effort_1", prompt: "Hi", history: history([]) }, activeControls());
|
|
1778
1624
|
await executor.execute({ threadId: "thread_effort", turnId: "turn_effort_2", prompt: "Again", history: history([]) }, activeControls());
|
|
1779
1625
|
expect(queryInputs[0]?.options.effort).toBe("medium");
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
it("runs a turn Jev clearly reads as small at low effort, and reports why", async () => {
|
|
1783
|
-
const queryInputs = [];
|
|
1784
|
-
const asked = [];
|
|
1785
|
-
const executor = createExecutor({
|
|
1786
|
-
queryInputs,
|
|
1787
|
-
model: "claude-opus-5-5",
|
|
1788
|
-
// small, complex, read-only, deliverable
|
|
1789
|
-
routeJudge: (questions) => {
|
|
1790
|
-
asked.push(...questions);
|
|
1791
|
-
return Promise.resolve([0.92, 0.05, 0.6, 0.1]);
|
|
1792
|
-
},
|
|
1793
|
-
queryResults: [scriptedMessages(resultMessage(0, "session_effort_low"))],
|
|
1794
|
-
});
|
|
1795
|
-
const outcome = await executor.execute({
|
|
1796
|
-
threadId: "thread_effort_low",
|
|
1797
|
-
turnId: "turn_effort_low",
|
|
1798
|
-
prompt: "¿Cuál fue el CPL de ayer?",
|
|
1799
|
-
history: history([
|
|
1800
|
-
{ role: "user", text: "Revisa la campaña de verano", createdAt: 1 },
|
|
1801
|
-
{ role: "assistant", text: "Listo: la campaña de verano va bien.", createdAt: 2 },
|
|
1802
|
-
]),
|
|
1803
|
-
}, activeControls());
|
|
1804
|
-
expect(queryInputs[0]?.options.effort).toBe("low");
|
|
1805
|
-
expect(asked[0]).toMatchObject({
|
|
1806
|
-
instructions: { message: "¿Cuál fue el CPL de ayer?", previous_reply: "Listo: la campaña de verano va bien." },
|
|
1807
|
-
});
|
|
1808
|
-
expect(outcome).toMatchObject({
|
|
1809
|
-
kind: "completed",
|
|
1810
|
-
turnRoute: { model: "claude-opus-5-5", modelReason: "fixed", effort: "low", reason: "small" },
|
|
1811
|
-
});
|
|
1812
|
-
});
|
|
1813
|
-
it("starts a chat that only reads on Sonnet, thinking harder, when the Runtime routes models", async () => {
|
|
1814
|
-
const queryInputs = [];
|
|
1815
|
-
const executor = createExecutor({
|
|
1816
|
-
queryInputs,
|
|
1817
|
-
model: "claude-opus-5-5",
|
|
1818
|
-
routeModels: true,
|
|
1819
|
-
routeJudge: () => Promise.resolve([0.2, 0.9, 0.85, 0.1]),
|
|
1820
|
-
queryResults: [scriptedMessages(resultMessage(0, "session_reading"))],
|
|
1821
|
-
});
|
|
1822
|
-
const outcome = await executor.execute({ threadId: "thread_reading", turnId: "turn_reading", prompt: "¿Cómo van los anuncios?", history: history([]) }, activeControls());
|
|
1823
|
-
expect(queryInputs[0]?.options).toMatchObject({ model: "claude-sonnet-5-5", effort: "high" });
|
|
1824
|
-
expect(outcome).toMatchObject({
|
|
1825
|
-
model: "claude-sonnet-5-5",
|
|
1826
|
-
turnRoute: { model: "claude-sonnet-5-5", modelReason: "read_only", effort: "high" },
|
|
1827
|
-
});
|
|
1828
|
-
});
|
|
1829
|
-
it("moves a chat that ran on Sonnet to Opus once a turn asks for finished work", async () => {
|
|
1830
|
-
const configDir = await mkdtemp(join(tmpdir(), "bridge-turn-route-"));
|
|
1831
|
-
try {
|
|
1832
|
-
await mkdir(join(configDir, "projects", "-home-user-repo"), { recursive: true });
|
|
1833
|
-
await writeFile(join(configDir, "projects", "-home-user-repo", "session_sonnet.jsonl"), `${JSON.stringify({ type: "assistant", requestedModel: "claude-sonnet-5-5", message: { id: "m", model: "claude-sonnet-5-5", content: [] } })}\n`);
|
|
1834
|
-
vi.stubEnv("CLAUDE_CONFIG_DIR", configDir);
|
|
1835
|
-
const queryInputs = [];
|
|
1836
|
-
const executor = createExecutor({
|
|
1837
|
-
queryInputs,
|
|
1838
|
-
model: "claude-opus-5-5",
|
|
1839
|
-
routeModels: true,
|
|
1840
|
-
loadedBoundary: cleanBoundary("session_sonnet"),
|
|
1841
|
-
routeJudge: () => Promise.resolve([0.1, 0.95, 0.2, 0.9]),
|
|
1842
|
-
queryResults: [scriptedMessages(resultMessage(0, "session_sonnet"))],
|
|
1843
|
-
});
|
|
1844
|
-
await executor.execute({
|
|
1845
|
-
threadId: "thread_upgrade",
|
|
1846
|
-
turnId: "turn_upgrade",
|
|
1847
|
-
prompt: "Perfecto, créame una landing con esto",
|
|
1848
|
-
history: history([
|
|
1849
|
-
{ role: "user", text: "¿Cómo van los anuncios?", createdAt: 1 },
|
|
1850
|
-
{ role: "assistant", text: "Van bien: el CPL bajó un 12%.", createdAt: 2 },
|
|
1851
|
-
]),
|
|
1852
|
-
}, activeControls());
|
|
1853
|
-
expect(queryInputs[0]?.options).toMatchObject({ model: "claude-opus-5-5", effort: "medium" });
|
|
1854
|
-
}
|
|
1855
|
-
finally {
|
|
1856
|
-
await rm(configDir, { recursive: true, force: true });
|
|
1857
|
-
}
|
|
1858
|
-
});
|
|
1859
|
-
it("keeps a plan-mode turn at medium without asking Jev", async () => {
|
|
1860
|
-
const queryInputs = [];
|
|
1861
|
-
let asked = false;
|
|
1862
|
-
const executor = createExecutor({
|
|
1863
|
-
queryInputs,
|
|
1864
|
-
routeJudge: () => {
|
|
1865
|
-
asked = true;
|
|
1866
|
-
return Promise.resolve([0.99, 0, 1, 0]);
|
|
1867
|
-
},
|
|
1868
|
-
queryResults: [scriptedMessages(resultMessage(0, "session_effort_plan"))],
|
|
1869
|
-
});
|
|
1870
|
-
await executor.execute({
|
|
1871
|
-
threadId: "thread_effort_plan",
|
|
1872
|
-
turnId: "turn_effort_plan",
|
|
1873
|
-
prompt: "Plan Q4",
|
|
1874
|
-
planMode: true,
|
|
1875
|
-
history: history([]),
|
|
1876
|
-
}, activeControls());
|
|
1877
|
-
expect(queryInputs[0]?.options.effort).toBe("medium");
|
|
1878
|
-
expect(asked).toBe(false);
|
|
1626
|
+
// Constant across turns — a per-turn value is what would break the cache.
|
|
1627
|
+
expect(queryInputs[1]?.options.effort).toBe(queryInputs[0]?.options.effort);
|
|
1879
1628
|
});
|
|
1880
1629
|
it("pins the auto-compact window to the context window it resolved for the model", async () => {
|
|
1881
1630
|
const queryInputs = [];
|
|
@@ -4162,8 +3911,6 @@ describe("BridgeJobExecutor stall recovery", () => {
|
|
|
4162
3911
|
kind: "completed",
|
|
4163
3912
|
costUsd: 0.05,
|
|
4164
3913
|
model: "claude-test",
|
|
4165
|
-
// The tool result the stalled query had already received still counts.
|
|
4166
|
-
toolResults: [expect.objectContaining({ calls: 1 })],
|
|
4167
3914
|
cleanBoundary: expect.objectContaining({ sessionId: "session_a" }),
|
|
4168
3915
|
});
|
|
4169
3916
|
expect(queryInputs).toHaveLength(2);
|
|
@@ -4902,9 +4649,6 @@ describe("BridgeJobExecutor turn end with background tasks", () => {
|
|
|
4902
4649
|
});
|
|
4903
4650
|
});
|
|
4904
4651
|
describe("BridgeJobExecutor review mode", () => {
|
|
4905
|
-
afterEach(() => {
|
|
4906
|
-
projectRootMock.value = process.cwd();
|
|
4907
|
-
});
|
|
4908
4652
|
const review = {
|
|
4909
4653
|
reviewId: "rev_1",
|
|
4910
4654
|
apiKey: "bk_review_key",
|
|
@@ -4962,21 +4706,6 @@ describe("BridgeJobExecutor review mode", () => {
|
|
|
4962
4706
|
await executor.execute(reviewJob(), activeControls());
|
|
4963
4707
|
expect(queryInputs[0]?.options.settings).toMatchObject({ permissions: { ask: ["Bash"] } });
|
|
4964
4708
|
});
|
|
4965
|
-
// The brief has the reviewer read the rulebook as the Session had it
|
|
4966
|
-
// (`git show <sha>:CLAUDE.md`); the working tree's copy may already be newer.
|
|
4967
|
-
// Auto-loading it put ~24k tokens of the marketer's instructions in front of
|
|
4968
|
-
// every review request, written cold and re-read on each one.
|
|
4969
|
-
it("does not load the workspace's CLAUDE.md into the reviewer, as a memory file or as the system prompt", async () => {
|
|
4970
|
-
const workspace = await mkdtemp(join(tmpdir(), "baker-review-instructions-"));
|
|
4971
|
-
await writeFile(join(workspace, "CLAUDE.md"), "# Project Instructions\n\n## Client Context\n\nOne client per repo. Everything known about them:\n\n@COMPANY.md\n\n## Rules\n");
|
|
4972
|
-
projectRootMock.value = workspace;
|
|
4973
|
-
const queryInputs = [];
|
|
4974
|
-
const executor = createExecutor({ queryInputs });
|
|
4975
|
-
await executor.execute(reviewJob(), activeControls());
|
|
4976
|
-
expect(queryInputs[0]?.options.settings).toMatchObject({ claudeMdExcludes: ["**/CLAUDE.md"] });
|
|
4977
|
-
expect(queryInputs[0]?.options.systemPrompt).toBeUndefined();
|
|
4978
|
-
expect(queryInputs[0]?.options.additionalDirectories).toBeUndefined();
|
|
4979
|
-
});
|
|
4980
4709
|
it("removes every editing tool on top of the list an ordinary turn is denied", async () => {
|
|
4981
4710
|
const queryInputs = [];
|
|
4982
4711
|
const executor = createExecutor({ queryInputs });
|
|
@@ -5040,7 +4769,6 @@ describe("BridgeJobExecutor review mode", () => {
|
|
|
5040
4769
|
expect(options.env?.BAKER_DEBUG_LOG).toBeUndefined();
|
|
5041
4770
|
expect(options.settings ?? {}).not.toHaveProperty("disableAllHooks");
|
|
5042
4771
|
expect(options.settings ?? {}).not.toHaveProperty("permissions");
|
|
5043
|
-
expect(options.settings ?? {}).not.toHaveProperty("claudeMdExcludes");
|
|
5044
4772
|
});
|
|
5045
4773
|
// BAKER-3462: after an English skill body, the next line went out in English to a Spanish chat.
|
|
5046
4774
|
const spanishTurn = "Prepara cuatro anuncios nuevos para la campaña de verano y súbelos hoy mismo";
|
|
@@ -5148,6 +4876,71 @@ describe("BridgeJobExecutor review mode", () => {
|
|
|
5148
4876
|
expect(outcome).toMatchObject({ kind: "completed", structuredOutput: findings });
|
|
5149
4877
|
});
|
|
5150
4878
|
});
|
|
4879
|
+
describe("BridgeJobExecutor builder mode", () => {
|
|
4880
|
+
const builder = {
|
|
4881
|
+
runId: "run_1",
|
|
4882
|
+
title: "Summer landing",
|
|
4883
|
+
outputSchema: { type: "object", properties: { summary: { type: "string" } }, required: ["summary"] },
|
|
4884
|
+
};
|
|
4885
|
+
function builderJob(overrides = {}) {
|
|
4886
|
+
return {
|
|
4887
|
+
threadId: "thread_task",
|
|
4888
|
+
turnId: "turn_1",
|
|
4889
|
+
prompt: "Build the summer sale landing.",
|
|
4890
|
+
unattended: true,
|
|
4891
|
+
builder,
|
|
4892
|
+
...overrides,
|
|
4893
|
+
};
|
|
4894
|
+
}
|
|
4895
|
+
it("runs the brief on the Session's own model, workspace and hooks, under a system prompt of its own", async () => {
|
|
4896
|
+
const queryInputs = [];
|
|
4897
|
+
const executor = createExecutor({ queryInputs, model: "claude-session-model" });
|
|
4898
|
+
await executor.execute(builderJob(), activeControls());
|
|
4899
|
+
const options = queryInputs[0]?.options;
|
|
4900
|
+
expect(options.model).toBe("claude-session-model");
|
|
4901
|
+
expect(options.systemPrompt).toEqual(expect.stringContaining("one brief"));
|
|
4902
|
+
expect(options.settings ?? {}).not.toHaveProperty("disableAllHooks");
|
|
4903
|
+
expect(options.disallowedTools).not.toContain("Write");
|
|
4904
|
+
});
|
|
4905
|
+
it("asks the SDK to enforce the task's result contract", async () => {
|
|
4906
|
+
const queryInputs = [];
|
|
4907
|
+
const executor = createExecutor({ queryInputs });
|
|
4908
|
+
await executor.execute(builderJob(), activeControls());
|
|
4909
|
+
const [taskQuery] = queryInputs;
|
|
4910
|
+
if (!taskQuery)
|
|
4911
|
+
throw new Error("expected the task to start one query");
|
|
4912
|
+
expect(taskQuery.options.outputFormat).toEqual({
|
|
4913
|
+
type: "json_schema",
|
|
4914
|
+
schema: builder.outputSchema,
|
|
4915
|
+
});
|
|
4916
|
+
});
|
|
4917
|
+
it("never offers a question to the person: it decides, or returns the question", async () => {
|
|
4918
|
+
const queryInputs = [];
|
|
4919
|
+
const executor = createExecutor({ queryInputs });
|
|
4920
|
+
await executor.execute(builderJob(), activeControls());
|
|
4921
|
+
expect(queryInputs[0]?.options.disallowedTools).toEqual(expect.arrayContaining(["AskUserQuestion", "ExitPlanMode"]));
|
|
4922
|
+
});
|
|
4923
|
+
it("sends a task that ends without returning its result back to StructuredOutput", async () => {
|
|
4924
|
+
const queryInputs = [];
|
|
4925
|
+
const executor = createExecutor({ queryInputs });
|
|
4926
|
+
await executor.execute(builderJob(), activeControls());
|
|
4927
|
+
const options = queryInputs[0]?.options;
|
|
4928
|
+
const stop = options.hooks?.Stop?.[0]?.hooks[0];
|
|
4929
|
+
const output = await stop?.({ hook_event_name: "Stop" }, undefined, {
|
|
4930
|
+
signal: new AbortController().signal,
|
|
4931
|
+
});
|
|
4932
|
+
expect(output).toMatchObject({ decision: "block", reason: expect.stringContaining("summary") });
|
|
4933
|
+
});
|
|
4934
|
+
it("leaves an ordinary turn's system prompt as it is", async () => {
|
|
4935
|
+
const queryInputs = [];
|
|
4936
|
+
const executor = createExecutor({ queryInputs });
|
|
4937
|
+
await executor.execute({ threadId: "t", turnId: "turn_1", prompt: "Build the page" }, activeControls());
|
|
4938
|
+
const [turnQuery] = queryInputs;
|
|
4939
|
+
if (!turnQuery)
|
|
4940
|
+
throw new Error("expected the turn to start one query");
|
|
4941
|
+
expect(turnQuery.options.systemPrompt).toBeUndefined();
|
|
4942
|
+
});
|
|
4943
|
+
});
|
|
5151
4944
|
describe("BridgeJobExecutor model refusals", () => {
|
|
5152
4945
|
function refusalNoFallback() {
|
|
5153
4946
|
return {
|
|
@@ -5292,6 +5085,7 @@ describe("BridgeJobExecutor model refusals", () => {
|
|
|
5292
5085
|
describe("agentFeature", () => {
|
|
5293
5086
|
it("tells OpenRouter a Session review, a plan and a chat turn apart", () => {
|
|
5294
5087
|
expect(agentFeature({ review: {} })).toBe("chatReview.session");
|
|
5088
|
+
expect(agentFeature({ builder: {} })).toBe("builder.task");
|
|
5295
5089
|
expect(agentFeature({ planMode: true })).toBe("agent.plan");
|
|
5296
5090
|
expect(agentFeature({})).toBe("agent.turn");
|
|
5297
5091
|
});
|