@koda-sl/baker-bridge 0.106.0 → 0.107.0-dev.a5e33d211

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +22 -27
  2. package/dist/hono/agent-session.test.js +6 -6
  3. package/dist/hono/agent.d.ts.map +1 -1
  4. package/dist/hono/agent.js +1 -4
  5. package/dist/hono/agent.js.map +1 -1
  6. package/dist/hono/agent.test.js +9 -9
  7. package/dist/hono/avatar-casting.js +2 -2
  8. package/dist/hono/avatar-casting.js.map +1 -1
  9. package/dist/hono/bridge-job.d.ts +2 -14
  10. package/dist/hono/bridge-job.d.ts.map +1 -1
  11. package/dist/hono/bridge-job.js +57 -123
  12. package/dist/hono/bridge-job.js.map +1 -1
  13. package/dist/hono/bridge-job.test.js +39 -175
  14. package/dist/hono/bridge-job.test.js.map +1 -1
  15. package/dist/hono/convex.d.ts +0 -1
  16. package/dist/hono/convex.d.ts.map +1 -1
  17. package/dist/hono/convex.js +0 -4
  18. package/dist/hono/convex.js.map +1 -1
  19. package/dist/hono/convex.test.js +3 -3
  20. package/dist/hono/mcp-servers.d.ts +2 -0
  21. package/dist/hono/mcp-servers.d.ts.map +1 -1
  22. package/dist/hono/mcp-servers.js +17 -16
  23. package/dist/hono/mcp-servers.js.map +1 -1
  24. package/dist/hono/mcp-servers.test.js +10 -38
  25. package/dist/hono/mcp-servers.test.js.map +1 -1
  26. package/dist/hono/model-capabilities.d.ts.map +1 -1
  27. package/dist/hono/model-capabilities.js +28 -19
  28. package/dist/hono/model-capabilities.js.map +1 -1
  29. package/dist/hono/model-capabilities.test.js +37 -15
  30. package/dist/hono/model-capabilities.test.js.map +1 -1
  31. package/dist/hono/relay.d.ts +1 -2
  32. package/dist/hono/relay.d.ts.map +1 -1
  33. package/dist/hono/relay.js +0 -3
  34. package/dist/hono/relay.js.map +1 -1
  35. package/dist/hono/relay.test.js +6 -22
  36. package/dist/hono/relay.test.js.map +1 -1
  37. package/dist/hono/session-continuity.integration.test.js +4 -4
  38. package/dist/hono/tag-fields.js +1 -1
  39. package/dist/hono/tag-fields.js.map +1 -1
  40. package/dist/hono/tag-input.js +1 -1
  41. package/dist/hono/tag-input.js.map +1 -1
  42. package/dist/hono/turn-events.d.ts +1 -1
  43. package/dist/hono/turn-events.d.ts.map +1 -1
  44. package/dist/hono/turn-events.js.map +1 -1
  45. package/package.json +1 -1
@@ -16,7 +16,7 @@ vi.mock("./runtime-files.ts", async (importOriginal) => {
16
16
  };
17
17
  });
18
18
  import { AVATAR_CASTING_TOOL_NAME } from "./avatar-casting.js";
19
- import { BridgeJobExecutor, createStallDetector, isContextOverflowRefusal, isControlChannelDeny, parsePlanDecision, remapAnswersToQuestionText, } from "./bridge-job.js";
19
+ import { BridgeJobExecutor, createStallDetector, isContextOverflowRefusal, isControlChannelDeny, OUTPUT_STYLE, parsePlanDecision, remapAnswersToQuestionText, } from "./bridge-job.js";
20
20
  import { CONNECTION_INPUT_TOOL_NAME } from "./connection-input.js";
21
21
  import { FLOW_INPUT_TOOL_NAME } from "./flow-input.js";
22
22
  import { bridgeRuntimeFiles } from "./runtime-files.js";
@@ -63,7 +63,6 @@ function recordingRelay() {
63
63
  const agentActivityUpdates = [];
64
64
  const compactionUpdates = [];
65
65
  const reviewDigests = [];
66
- const retractedEvents = [];
67
66
  const contextUsageUpdates = [];
68
67
  return {
69
68
  assistantEvents,
@@ -72,7 +71,6 @@ function recordingRelay() {
72
71
  compactionUpdates,
73
72
  contextUsageUpdates,
74
73
  reviewDigests,
75
- retractedEvents,
76
74
  startPreview: () => undefined,
77
75
  previewText: () => undefined,
78
76
  previewTools: () => undefined,
@@ -88,10 +86,6 @@ function recordingRelay() {
88
86
  assistantEvents.push({ target, event, model });
89
87
  return Promise.resolve();
90
88
  },
91
- retractEvents: (target, eventIds) => {
92
- retractedEvents.push({ target, eventIds });
93
- return Promise.resolve();
94
- },
95
89
  complete: () => Promise.resolve(),
96
90
  inputRequest: () => Promise.resolve(true),
97
91
  inputResolved: () => Promise.resolve(),
@@ -314,8 +308,8 @@ describe("isControlChannelDeny", () => {
314
308
  })).toBe(false);
315
309
  });
316
310
  it("does not match a PreToolUse hook block that quotes the envelope in its explanation", () => {
317
- // Live-run finding: the scaffold's guard-background-commands hook explains
318
- // WHY it blocks a backgrounded subagent by quoting the failure it prevents.
311
+ // Live-run finding: the scaffold's guard-background-commands hook, when it
312
+ // still blocked backgrounded subagents, explained why by quoting the failure.
319
313
  // Its block arrives as an is_error tool_result, so an envelope-substring
320
314
  // match counted it as a deny — three blocked spawns in one turn would have
321
315
  // tripped the breaker and errored a perfectly healthy turn.
@@ -751,13 +745,13 @@ describe("BridgeJobExecutor", () => {
751
745
  const relay = recordingRelay();
752
746
  const executor = createExecutor({
753
747
  relay,
754
- model: "claude-opus-5-5",
748
+ model: "claude-opus-4-6",
755
749
  queryResults: [
756
750
  scriptedMessages(assistantMessage("assistant_1", "Done.", "session_next", {
757
751
  usage: { input_tokens: 4000, cache_read_input_tokens: 96000 },
758
752
  }),
759
753
  // The runtime reports Opus's catalog window as 200k through the
760
- // OpenRouter passthrough — wrong. Opus 5.5 is 1M on OpenRouter.
754
+ // OpenRouter passthrough — wrong. Opus 4.6 is 1M on OpenRouter.
761
755
  resultMessage(0.02, "session_next", {
762
756
  "claude-main": {
763
757
  inputTokens: 4000,
@@ -1169,8 +1163,7 @@ describe("BridgeJobExecutor", () => {
1169
1163
  // Read off a session's own `init` tool list, the CLI offers these to every
1170
1164
  // Session. Each one reaches past what a marketer's chat is for: the worktree
1171
1165
  // pair rearranges the branches Publish owns, `Workflow` fans out a fleet on
1172
- // the organization's OpenRouter key, and `SendMessage` addresses peers a
1173
- // single-session Runtime does not have.
1166
+ // the organization's OpenRouter key.
1174
1167
  it("removes the tools that reach outside what a Session is", async () => {
1175
1168
  const queryInputs = [];
1176
1169
  const executor = createExecutor({
@@ -1179,10 +1172,12 @@ describe("BridgeJobExecutor", () => {
1179
1172
  });
1180
1173
  await executor.execute({ threadId: "thread_reach", turnId: "turn_reach", prompt: "Build a landing", history: history([]) }, activeControls());
1181
1174
  const disallowed = queryInputs[0]?.options.disallowedTools ?? [];
1182
- expect(disallowed).toEqual(expect.arrayContaining(["EnterWorktree", "ExitWorktree", "Workflow", "SendMessage"]));
1175
+ expect(disallowed).toEqual(expect.arrayContaining(["EnterWorktree", "ExitWorktree", "Workflow"]));
1183
1176
  // `Task` is the subagent spawn itself — the scaffold's own fan-out workers
1184
- // ride it, so blocking multi-agent orchestration must not catch it.
1177
+ // ride it, so blocking multi-agent orchestration must not catch it. And
1178
+ // `SendMessage` is how the agent follows up with a subagent it already ran.
1185
1179
  expect(disallowed).not.toContain("Task");
1180
+ expect(disallowed).not.toContain("SendMessage");
1186
1181
  });
1187
1182
  it("allowlists the workspace's own skills so the CLI's built-in developer skills stay out of the session", async () => {
1188
1183
  const workspace = await mkdtemp(join(tmpdir(), "baker-job-skills-"));
@@ -1370,31 +1365,42 @@ describe("BridgeJobExecutor", () => {
1370
1365
  await executor.execute({ threadId: "thread_uw", turnId: "turn_uw", prompt: "Hi", history: history([]) }, activeControls());
1371
1366
  expect((queryInputs[0]?.options.settings ?? {})).not.toHaveProperty("autoCompactWindow");
1372
1367
  });
1373
- // No output style is sent. `Concise` was pinned against Opus 5's preamble; on
1374
- // Opus 5.5 it suppressed the one-line progress notes a marketer reads while a
1375
- // long turn works. The voice rules live in the scaffold CLAUDE.md instead.
1376
- it("sends no output style, so the default voice and its progress notes survive", async () => {
1368
+ // The output style is pinned on every turn, so the settings tier now always has
1369
+ // something in it — even for a model whose window we cannot resolve and with no
1370
+ // chat id to enable the memory directory, which used to leave it empty.
1371
+ it("always sends the settings tier, because the output style is pinned every turn", async () => {
1372
+ vi.stubEnv("BAKER_CHAT_ID", "");
1377
1373
  const queryInputs = [];
1378
1374
  const executor = createExecutor({
1379
1375
  queryInputs,
1380
- queryResults: [scriptedMessages(resultMessage(0, "session_style"))],
1376
+ model: "claude-unknown",
1377
+ queryResults: [scriptedMessages(resultMessage(0, "session_no_settings"))],
1381
1378
  });
1382
- await executor.execute({ threadId: "thread_style", turnId: "turn_style_1", prompt: "Hi", history: history([]) }, activeControls());
1383
- expect((queryInputs[0]?.options.settings ?? {})).not.toHaveProperty("outputStyle");
1379
+ await executor.execute({ threadId: "thread_ns", turnId: "turn_ns", prompt: "Hi", history: history([]) }, activeControls());
1380
+ expect(queryInputs[0]?.options.settings).toEqual({ outputStyle: OUTPUT_STYLE });
1384
1381
  });
1385
- // Nothing to put in the settings tier (unknown model window, no chat id for the
1386
- // memory directory) means no `settings` key at all: the SDK forwards even `{}`
1387
- // to the CLI as a `--settings` flag tier.
1388
- it("omits the settings tier when nothing in it applies", async () => {
1389
- vi.stubEnv("BAKER_CHAT_ID", "");
1382
+ // The CLI accepts an unrecognized output-style name silently — a misspelling
1383
+ // returns a clean 200 on the default voice, with nothing at runtime to notice
1384
+ // it. Verified against the CLI: `{"outputStyle":"NotARealStyle"}` succeeded.
1385
+ // So the literal is pinned here rather than left to review.
1386
+ it("names the built-in Concise style exactly", () => {
1387
+ expect(OUTPUT_STYLE).toBe("Concise");
1388
+ });
1389
+ it("runs every turn under the pinned output style", async () => {
1390
1390
  const queryInputs = [];
1391
1391
  const executor = createExecutor({
1392
1392
  queryInputs,
1393
- model: "claude-unknown",
1394
- queryResults: [scriptedMessages(resultMessage(0, "session_no_settings"))],
1393
+ queryResults: [
1394
+ scriptedMessages(resultMessage(0, "session_style")),
1395
+ scriptedMessages(resultMessage(0, "session_style")),
1396
+ ],
1395
1397
  });
1396
- await executor.execute({ threadId: "thread_ns", turnId: "turn_ns", prompt: "Hi", history: history([]) }, activeControls());
1397
- expect(queryInputs[0]?.options).not.toHaveProperty("settings");
1398
+ await executor.execute({ threadId: "thread_style", turnId: "turn_style_1", prompt: "Hi", history: history([]) }, activeControls());
1399
+ await executor.execute({ threadId: "thread_style", turnId: "turn_style_2", prompt: "Again", history: history([]) }, activeControls());
1400
+ expect(queryInputs[0]?.options.settings).toMatchObject({ outputStyle: "Concise" });
1401
+ // Constant across turns — the style is part of the system prompt, so a
1402
+ // per-turn value would re-write the cache prefix on every request.
1403
+ expect(queryInputs[1]?.options.settings).toMatchObject({ outputStyle: "Concise" });
1398
1404
  });
1399
1405
  it("runs exact publish commands with the prompt passed through verbatim", async () => {
1400
1406
  const queryInputs = [];
@@ -1923,8 +1929,8 @@ describe("BridgeJobExecutor", () => {
1923
1929
  expect(castingRequests[0]).toMatchObject({ avatarHandle: "elena" });
1924
1930
  });
1925
1931
  it("hands the agent's own casting call the face the user picked, instead of a second picker", async () => {
1926
- // An agent may still call it right after `baker avatars create` (the tool now says the
1927
- // picker opens by itself, but the call is tolerated). The auto-cast owed for that create used
1932
+ // The tool tells the agent to call it IMMEDIATELY after `baker avatars create`, so
1933
+ // this is the documented path, not an edge. The auto-cast owed for that create used
1928
1934
  // to run under its own tool-use id and then let the explicit call through unhandled —
1929
1935
  // which returns the tool's `{ status: "declined" }` default. The agent was told the
1930
1936
  // user had rejected all four faces moments after they picked one, and the remedy in
@@ -3174,35 +3180,6 @@ describe("BridgeJobExecutor turn end with background tasks", () => {
3174
3180
  expect(outcome).toMatchObject({ kind: "completed", costUsd: 0.1 });
3175
3181
  expect(endInputAt.count).toBe(3);
3176
3182
  });
3177
- // The production shape: the model refused, the CLI answered with an error
3178
- // result and then waited on stdin like after any other result. The error was
3179
- // no longer thrown, and only a clean result ended input, so the turn sat idle
3180
- // until the 30-minute session watchdog reported a lost connection.
3181
- it("ends input after the error result of a refused turn", async () => {
3182
- const endInputAt = { count: -1 };
3183
- const refusal = {
3184
- type: "system",
3185
- subtype: "model_refusal_no_fallback",
3186
- original_model: "claude-opus-5-5",
3187
- api_refusal_category: "reasoning_extraction",
3188
- session_id: "session_refused",
3189
- };
3190
- const erroredResult = {
3191
- type: "result",
3192
- subtype: "error_during_execution",
3193
- is_error: true,
3194
- errors: ["API Error: Claude Code is unable to respond to this request."],
3195
- total_cost_usd: 0.01,
3196
- session_id: "session_refused",
3197
- };
3198
- const executor = createExecutor({ queryResults: [cliLikeQuery(endInputAt, refusal, erroredResult)] });
3199
- const outcome = await executor.execute({ threadId: "thread_refused", turnId: "turn_refused", prompt: "Hi", history: history([]) }, activeControls());
3200
- expect(outcome).toMatchObject({
3201
- kind: "errored",
3202
- errors: ["I couldn't help with that request. Try rephrasing what you need."],
3203
- });
3204
- expect(endInputAt.count).toBe(2);
3205
- });
3206
3183
  it("does not hold the turn open for ambient skip_transcript tasks", async () => {
3207
3184
  const endInputAt = { count: -1 };
3208
3185
  const executor = createExecutor({
@@ -3401,117 +3378,4 @@ describe("BridgeJobExecutor review mode", () => {
3401
3378
  expect(outcome).toMatchObject({ kind: "completed", structuredOutput: findings });
3402
3379
  });
3403
3380
  });
3404
- describe("BridgeJobExecutor model refusals", () => {
3405
- function refusalNoFallback() {
3406
- return {
3407
- type: "system",
3408
- subtype: "model_refusal_no_fallback",
3409
- original_model: "claude-opus-5-5",
3410
- request_id: "req_1",
3411
- api_refusal_category: "cyber",
3412
- content: "Claude Code is unable to respond to this request.",
3413
- uuid: "refusal_notice",
3414
- session_id: "session_refused",
3415
- };
3416
- }
3417
- it("ends a refused turn with a product-language error instead of a success", async () => {
3418
- const executor = createExecutor({
3419
- queryResults: [scriptedMessages(refusalNoFallback(), resultMessage(0.01, "session_refused"))],
3420
- });
3421
- const outcome = await executor.execute({ threadId: "thread_refused", turnId: "turn_refused", prompt: "Hi", history: history([]) }, activeControls());
3422
- expect(outcome).toMatchObject({
3423
- kind: "errored",
3424
- errors: ["I couldn't help with that request. Try rephrasing what you need."],
3425
- });
3426
- });
3427
- it("records the refusal category for whoever reads the turn afterwards", async () => {
3428
- const turnEvents = [];
3429
- const executor = createExecutor({
3430
- turnEvents,
3431
- queryResults: [scriptedMessages(refusalNoFallback(), resultMessage(0.01, "session_refused"))],
3432
- });
3433
- await executor.execute({ threadId: "thread_refused", turnId: "turn_refused", prompt: "Hi", history: history([]) }, activeControls());
3434
- expect(turnEvents).toContainEqual(expect.objectContaining({ event: "model_refusal", detail: expect.objectContaining({ category: "cyber" }) }));
3435
- });
3436
- // The CLI renders the refusal as an assistant error frame written for a
3437
- // terminal user. The turn already ends on a product-language error, so that
3438
- // frame never reaches the chat.
3439
- it("does not relay the CLI's refusal frame to the chat", async () => {
3440
- const relay = recordingRelay();
3441
- const refusalFrame = {
3442
- type: "assistant",
3443
- uuid: "refusal_frame",
3444
- message: {
3445
- role: "assistant",
3446
- content: [{ type: "text", text: "API Error: Claude Code is unable to respond to this request." }],
3447
- stop_reason: "refusal",
3448
- },
3449
- };
3450
- const executor = createExecutor({
3451
- relay,
3452
- queryResults: [scriptedMessages(refusalFrame, refusalNoFallback(), resultMessage(0.01, "session_refused"))],
3453
- });
3454
- await executor.execute({ threadId: "thread_refused", turnId: "turn_refused", prompt: "Hi", history: history([]) }, activeControls());
3455
- expect(relay.assistantEvents).toEqual([]);
3456
- });
3457
- // A refused subagent is reported the same way, but the main agent gets the
3458
- // failed task back and carries on — the turn is still its to finish.
3459
- it("lets the turn complete when the main conversation carries on after a refusal", async () => {
3460
- const executor = createExecutor({
3461
- queryResults: [
3462
- scriptedMessages(refusalNoFallback(), assistantMessage("assistant_after", "Done another way.", "session_refused"), resultMessage(0.01, "session_refused")),
3463
- ],
3464
- });
3465
- const outcome = await executor.execute({ threadId: "thread_refused", turnId: "turn_refused", prompt: "Hi", history: history([]) }, activeControls());
3466
- expect(outcome).toMatchObject({ kind: "completed" });
3467
- });
3468
- it("keeps the product-language error when the refused turn's result is an error", async () => {
3469
- const erroredResult = {
3470
- type: "result",
3471
- subtype: "error_during_execution",
3472
- is_error: true,
3473
- errors: ["API Error: Claude Code is unable to respond to this request."],
3474
- total_cost_usd: 0.01,
3475
- session_id: "session_refused",
3476
- };
3477
- const executor = createExecutor({ queryResults: [scriptedMessages(refusalNoFallback(), erroredResult)] });
3478
- const outcome = await executor.execute({ threadId: "thread_refused", turnId: "turn_refused", prompt: "Hi", history: history([]) }, activeControls());
3479
- expect(outcome).toMatchObject({
3480
- kind: "errored",
3481
- errors: ["I couldn't help with that request. Try rephrasing what you need."],
3482
- });
3483
- });
3484
- // A fallback model retried the refused request. What the refused leg already
3485
- // posted is superseded, so it leaves the chat instead of sitting above the
3486
- // answer that replaced it.
3487
- it("retracts the messages a refusal fallback superseded", async () => {
3488
- const relay = recordingRelay();
3489
- const fallbackNotice = {
3490
- type: "system",
3491
- subtype: "model_refusal_fallback",
3492
- trigger: "refusal",
3493
- direction: "retry",
3494
- scope: "session",
3495
- original_model: "claude-opus-5-5",
3496
- fallback_model: "claude-sonnet-5",
3497
- request_id: "req_1",
3498
- api_refusal_category: "cyber",
3499
- retracted_message_uuids: ["refused_partial_1", "refused_partial_2"],
3500
- content: "Retried on a different model.",
3501
- uuid: "fallback_notice",
3502
- session_id: "session_fallback",
3503
- };
3504
- const executor = createExecutor({
3505
- relay,
3506
- queryResults: [
3507
- scriptedMessages(assistantMessage("refused_partial_1", "Sure, here is", "session_fallback"), fallbackNotice, assistantMessage("fallback_answer", "Here is the plan.", "session_fallback"), resultMessage(0.01, "session_fallback")),
3508
- ],
3509
- });
3510
- const outcome = await executor.execute({ threadId: "thread_fb", turnId: "turn_fb", prompt: "Hi", history: history([]) }, activeControls());
3511
- expect(relay.retractedEvents).toEqual([
3512
- { target: { threadId: "thread_fb", turnId: "turn_fb" }, eventIds: ["refused_partial_1", "refused_partial_2"] },
3513
- ]);
3514
- expect(outcome).toMatchObject({ kind: "completed" });
3515
- });
3516
- });
3517
3381
  //# sourceMappingURL=bridge-job.test.js.map