@koda-sl/baker-bridge 0.171.0-dev.289ff1b9d → 0.171.0

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 (102) hide show
  1. package/README.md +51 -60
  2. package/dist/builder/builder-turn.d.ts +28 -0
  3. package/dist/builder/builder-turn.d.ts.map +1 -0
  4. package/dist/builder/builder-turn.js +16 -0
  5. package/dist/builder/builder-turn.js.map +1 -0
  6. package/dist/hono/agent-session.d.ts.map +1 -1
  7. package/dist/hono/agent-session.js +4 -3
  8. package/dist/hono/agent-session.js.map +1 -1
  9. package/dist/hono/agent-session.test.js +4 -3
  10. package/dist/hono/agent-session.test.js.map +1 -1
  11. package/dist/hono/agent.d.ts.map +1 -1
  12. package/dist/hono/agent.js +5 -6
  13. package/dist/hono/agent.js.map +1 -1
  14. package/dist/hono/agent.test.js +91 -177
  15. package/dist/hono/agent.test.js.map +1 -1
  16. package/dist/hono/attachments.d.ts.map +1 -1
  17. package/dist/hono/attachments.js +9 -1
  18. package/dist/hono/attachments.js.map +1 -1
  19. package/dist/hono/attachments.test.js +22 -1
  20. package/dist/hono/attachments.test.js.map +1 -1
  21. package/dist/hono/bridge-job.d.ts +16 -26
  22. package/dist/hono/bridge-job.d.ts.map +1 -1
  23. package/dist/hono/bridge-job.js +60 -162
  24. package/dist/hono/bridge-job.js.map +1 -1
  25. package/dist/hono/bridge-job.test.js +99 -233
  26. package/dist/hono/bridge-job.test.js.map +1 -1
  27. package/dist/hono/flow-input-card-fields.test.js +1 -1
  28. package/dist/hono/flow-input.d.ts +1 -1
  29. package/dist/hono/flow-input.js +1 -1
  30. package/dist/hono/main-sync.d.ts +22 -0
  31. package/dist/hono/main-sync.d.ts.map +1 -1
  32. package/dist/hono/main-sync.js +110 -2
  33. package/dist/hono/main-sync.js.map +1 -1
  34. package/dist/hono/main-sync.test.js +127 -1
  35. package/dist/hono/main-sync.test.js.map +1 -1
  36. package/dist/hono/model-capabilities.d.ts +0 -1
  37. package/dist/hono/model-capabilities.d.ts.map +1 -1
  38. package/dist/hono/model-capabilities.js +0 -4
  39. package/dist/hono/model-capabilities.js.map +1 -1
  40. package/dist/hono/openrouter-session.d.ts +13 -19
  41. package/dist/hono/openrouter-session.d.ts.map +1 -1
  42. package/dist/hono/openrouter-session.js +13 -19
  43. package/dist/hono/openrouter-session.js.map +1 -1
  44. package/dist/hono/publish-handoff.d.ts +0 -1
  45. package/dist/hono/publish-handoff.d.ts.map +1 -1
  46. package/dist/hono/publish-handoff.js +0 -53
  47. package/dist/hono/publish-handoff.js.map +1 -1
  48. package/dist/hono/publish-handoff.test.js +0 -48
  49. package/dist/hono/publish-handoff.test.js.map +1 -1
  50. package/dist/hono/relay.d.ts +0 -8
  51. package/dist/hono/relay.d.ts.map +1 -1
  52. package/dist/hono/relay.js +0 -2
  53. package/dist/hono/relay.js.map +1 -1
  54. package/dist/hono/relay.test.js +0 -30
  55. package/dist/hono/relay.test.js.map +1 -1
  56. package/dist/hono/runtime-files.d.ts +0 -1
  57. package/dist/hono/runtime-files.d.ts.map +1 -1
  58. package/dist/hono/runtime-files.js +0 -3
  59. package/dist/hono/runtime-files.js.map +1 -1
  60. package/dist/hono/served-model.d.ts +0 -8
  61. package/dist/hono/served-model.d.ts.map +1 -1
  62. package/dist/hono/served-model.js +0 -41
  63. package/dist/hono/served-model.js.map +1 -1
  64. package/dist/hono/served-model.test.js +1 -19
  65. package/dist/hono/served-model.test.js.map +1 -1
  66. package/dist/hono/session-continuity.integration.test.js +0 -2
  67. package/dist/hono/session-continuity.integration.test.js.map +1 -1
  68. package/dist/hono/types.d.ts +7 -12
  69. package/dist/hono/types.d.ts.map +1 -1
  70. package/dist/review/digest.d.ts +0 -13
  71. package/dist/review/digest.d.ts.map +1 -1
  72. package/dist/review/digest.js +1 -1
  73. package/dist/review/digest.js.map +1 -1
  74. package/dist/review/structured-output-gate.d.ts +10 -0
  75. package/dist/review/structured-output-gate.d.ts.map +1 -1
  76. package/dist/review/structured-output-gate.js +15 -5
  77. package/dist/review/structured-output-gate.js.map +1 -1
  78. package/package.json +1 -1
  79. package/dist/hono/tool-result-ledger.d.ts +0 -32
  80. package/dist/hono/tool-result-ledger.d.ts.map +0 -1
  81. package/dist/hono/tool-result-ledger.js +0 -67
  82. package/dist/hono/tool-result-ledger.js.map +0 -1
  83. package/dist/hono/tool-result-ledger.test.d.ts +0 -2
  84. package/dist/hono/tool-result-ledger.test.d.ts.map +0 -1
  85. package/dist/hono/tool-result-ledger.test.js +0 -61
  86. package/dist/hono/tool-result-ledger.test.js.map +0 -1
  87. package/dist/hono/turn-route.d.ts +0 -71
  88. package/dist/hono/turn-route.d.ts.map +0 -1
  89. package/dist/hono/turn-route.js +0 -112
  90. package/dist/hono/turn-route.js.map +0 -1
  91. package/dist/hono/turn-route.test.d.ts +0 -2
  92. package/dist/hono/turn-route.test.d.ts.map +0 -1
  93. package/dist/hono/turn-route.test.js +0 -134
  94. package/dist/hono/turn-route.test.js.map +0 -1
  95. package/dist/hono/workspace-instructions.d.ts +0 -43
  96. package/dist/hono/workspace-instructions.d.ts.map +0 -1
  97. package/dist/hono/workspace-instructions.js +0 -68
  98. package/dist/hono/workspace-instructions.js.map +0 -1
  99. package/dist/hono/workspace-instructions.test.d.ts +0 -2
  100. package/dist/hono/workspace-instructions.test.d.ts.map +0 -1
  101. package/dist/hono/workspace-instructions.test.js +0 -68
  102. package/dist/hono/workspace-instructions.test.js.map +0 -1
@@ -1,6 +1,7 @@
1
1
  import { readdir, readFile, stat } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { isAbsolute, join, relative, resolve } from "node:path";
4
+ import { BUILDER_DISALLOWED_TOOLS, BUILDER_SYSTEM_PROMPT, builderOutputHooks, } from "../builder/builder-turn.js";
4
5
  import { capDigestSections } from "../review/digest-bundle.js";
5
6
  import { buildReviewDigest, digestPaths } from "../review/evidence.js";
6
7
  import { renderEvidenceBlock } from "../review/evidence-block.js";
@@ -23,11 +24,11 @@ import { composioScopesToInvalidate, failedCustomServers, reportMcpInvalid, serv
23
24
  import { agentModelOverride, autoCompactWindowForModel, betasForModel, contextWindowForModel, } from "./model-capabilities.js";
24
25
  import { agentShellPrefixEnv, killFirstWrapper } from "./oom-priority.js";
25
26
  import { anthropicCustomHeaders, runtimeOpenRouterTags } from "./openrouter-attribution.js";
26
- import { buildAgentExtraBody, EXTRA_BODY_ENV, MODEL_CAPABILITIES_ENV, PER_TURN_EFFORT_CAPABILITIES, } from "./openrouter-session.js";
27
+ import { BEDROCK_SERVABLE_CAPABILITIES, buildAgentExtraBody, EXTRA_BODY_ENV, MODEL_CAPABILITIES_ENV, } from "./openrouter-session.js";
27
28
  import { credentialReminder, currentReferenceReminder, kickoffBriefReminder } from "./prompt-context.js";
28
29
  import { publishLanguageReminder, publishRunReminder, publishStandingReminder } from "./publish-handoff.js";
29
30
  import { bridgeRuntimeFiles } from "./runtime-files.js";
30
- import { alignServedModels, readConversationModel } from "./served-model.js";
31
+ import { alignServedModels } from "./served-model.js";
31
32
  import { applyCurrentSenderAttribution, buildCancelBoundary, buildCleanBoundary, buildRehydratedPrompt, compactionRetryAttempt, emptyInProgressSession, interruptedResumeAttempt, isCliResumeRefusal, isContextOverflowError, isResumeSessionError, observeInProgressSession, prepareSessionAttempt, stallContinuationAttempt, } from "./session-continuity.js";
32
33
  import { readSessionSkills } from "./session-skills.js";
33
34
  import { ownQueryUsage, readUsageBaseline } from "./session-usage-baseline.js";
@@ -35,11 +36,8 @@ import { skillLanguageHooks } from "./skill-language.js";
35
36
  import { BridgeSlashCommands, leadingSkillCommand, verbatimSlashCommand } from "./slash-commands.js";
36
37
  import { createBakerUiServer, normalizeTagInputToolInput, sanitizeTagChanges, TAG_INPUT_TOOL_NAME, unknownPrefillMessage, } from "./tag-input.js";
37
38
  import { blockingToolDelegated, toolRunAsCommand } from "./tool-in-shell.js";
38
- import { ToolResultLedger } from "./tool-result-ledger.js";
39
39
  import { recordTurnEvent } from "./turn-events.js";
40
40
  import { addQueryModelUsage, queryModelUsage, turnModelUsageList } from "./turn-model-usage.js";
41
- import { askTurnSignals, needsSignals, routeTurn, } from "./turn-route.js";
42
- import { prepareSharedInstructions } from "./workspace-instructions.js";
43
41
  import { settleWorkspaceAfterTasks } from "./workspace-settle.js";
44
42
  export function parsePlanDecision(value) {
45
43
  if (!value || typeof value !== "object") {
@@ -220,13 +218,6 @@ const BASE_DISALLOWED_TOOLS = [
220
218
  // not asked to fund a research fleet. Ordinary fan-out is unaffected:
221
219
  // `Task` stays, so the scaffold's subagents still run.
222
220
  "Workflow",
223
- // Developer tools: design-system sync, code-review findings, Jupyter edits.
224
- // None was called once in 358 production chats (Oct 2026), and each
225
- // definition rides every request whenever the CLI does not defer it —
226
- // DesignSync's alone is ~9k characters.
227
- "DesignSync",
228
- "ReportFindings",
229
- "NotebookEdit",
230
221
  // NOT listed: `SendMessage`. It is how the model follows up with a
231
222
  // subagent it already ran, and the CLI tells it to on every Agent result.
232
223
  // The continued subagent runs in the background, but it announces itself
@@ -244,7 +235,12 @@ function disallowedToolsFor(job) {
244
235
  // calls go through `reviewBashPolicy` — because each fails differently: a
245
236
  // route could be mis-scoped, a policy could miss a spelling, and a tool the
246
237
  // model cannot see cannot be reached at all.
247
- return job.review ? [...BASE_DISALLOWED_TOOLS, ...REVIEW_DISALLOWED_TOOLS] : [...BASE_DISALLOWED_TOOLS];
238
+ if (job.review)
239
+ return [...BASE_DISALLOWED_TOOLS, ...REVIEW_DISALLOWED_TOOLS];
240
+ // A task has nobody to answer a question: it decides, or returns the question.
241
+ if (job.builder)
242
+ return [...BASE_DISALLOWED_TOOLS, ...BUILDER_DISALLOWED_TOOLS];
243
+ return [...BASE_DISALLOWED_TOOLS];
248
244
  }
249
245
  /**
250
246
  * A review is pinned to one model in the backend's constants, not to whatever
@@ -255,7 +251,7 @@ function disallowedToolsFor(job) {
255
251
  function modelForJob(job, sessionModel) {
256
252
  return job.review?.model ?? sessionModel;
257
253
  }
258
- export const REVIEW_DISALLOWED_TOOLS = ["Edit", "Write", "MultiEdit", "ExitPlanMode", "Skill"];
254
+ export const REVIEW_DISALLOWED_TOOLS = ["Edit", "Write", "MultiEdit", "NotebookEdit", "ExitPlanMode", "Skill"];
259
255
  /**
260
256
  * The findings contract, enforced by the SDK rather than parsed back out of
261
257
  * prose. `structured_output` on the result message is what reaches Convex, and
@@ -266,10 +262,29 @@ export function reviewQueryOptions(job) {
266
262
  ? { outputFormat: { type: "json_schema", schema: job.review.outputSchema }, hooks: reviewOutputHooks() }
267
263
  : {};
268
264
  }
265
+ /**
266
+ * A task's result contract, enforced by the SDK like the review's findings, under the task's own system
267
+ * prompt (`../builder/builder-turn.ts`). An ordinary turn keeps the empty one the SDK sends by default.
268
+ */
269
+ export function builderQueryOptions(job) {
270
+ return job.builder
271
+ ? {
272
+ systemPrompt: BUILDER_SYSTEM_PROMPT,
273
+ outputFormat: { type: "json_schema", schema: job.builder.outputSchema },
274
+ hooks: builderOutputHooks(),
275
+ }
276
+ : {};
277
+ }
278
+ /** What shapes a turn's answer: a task's contract and prompt, the review's findings, or nothing. */
279
+ function answerOptions(job) {
280
+ return job.builder ? builderQueryOptions(job) : reviewQueryOptions(job);
281
+ }
269
282
  /** What the agent's model calls are for, as OpenRouter is told (`openrouter-attribution.ts`). */
270
283
  export function agentFeature(job) {
271
284
  if (job.review)
272
285
  return "chatReview.session";
286
+ if (job.builder)
287
+ return "builder.task";
273
288
  return job.planMode ? "agent.plan" : "agent.turn";
274
289
  }
275
290
  /**
@@ -442,19 +457,6 @@ function createBridgeJobState() {
442
457
  detachedSession: false,
443
458
  modelUsageTotals: new Map(),
444
459
  queryModelUsage: new Map(),
445
- toolResults: new ToolResultLedger(),
446
- };
447
- }
448
- /** What a finished turn reports about its spend: per-model usage and per-tool result sizes. */
449
- function turnUsage(state) {
450
- const toolResults = state.toolResults.summary();
451
- return {
452
- modelUsage: turnModelUsageList(state.modelUsageTotals),
453
- ...(toolResults.length > 0 ? { toolResults } : {}),
454
- // Left off when nothing was decided: Jev never answered and the model is the Runtime's.
455
- ...(state.turnRoute && !(state.turnRoute.reason === "unjudged" && state.turnRoute.modelReason === "fixed")
456
- ? { turnRoute: state.turnRoute }
457
- : {}),
458
460
  };
459
461
  }
460
462
  function errorMessage(error) {
@@ -531,66 +533,6 @@ function hookSettingsForJob(job) {
531
533
  }
532
534
  return {};
533
535
  }
534
- /**
535
- * Which instruction files a job loads as memory. A Session review loads none:
536
- * its brief has it read the rulebook as the Session had it (`git show
537
- * <sha>:CLAUDE.md`), since Publish may already have moved the working tree past
538
- * it, and the auto-loaded copy is ~24k tokens of the marketer's instructions
539
- * written cold and re-read on every review request. The client's agent loads
540
- * the workspace's `CLAUDE.md` itself only when it could not be split into the
541
- * shared system prompt (`prepareSharedInstructions`).
542
- */
543
- /** A Session review reads no CLAUDE.md, so it has nothing to share. */
544
- async function sharedInstructionsFor(job) {
545
- if (job.review) {
546
- return null;
547
- }
548
- return await prepareSharedInstructions({
549
- projectRoot: bridgeRuntimeFiles.projectRoot(),
550
- contextDir: bridgeRuntimeFiles.instructionsDir(),
551
- });
552
- }
553
- function sharedInstructionOptions(sharedInstructions) {
554
- if (!sharedInstructions) {
555
- return {};
556
- }
557
- return {
558
- systemPrompt: {
559
- type: "preset",
560
- preset: "claude_code",
561
- append: sharedInstructions.shared,
562
- // Recorded once per conversation, so a scaffold sync mid-chat cannot
563
- // move the cached prefix under it; it lands at the next compaction.
564
- snapshot: true,
565
- },
566
- additionalDirectories: [bridgeRuntimeFiles.instructionsDir()],
567
- };
568
- }
569
- function bridgeJudge(feature) {
570
- return jevJudge(process.env.OPENROUTER_API_KEY, runtimeOpenRouterTags(process.env, { service: "bridge", feature }));
571
- }
572
- function judgesOf(dependencies) {
573
- return {
574
- judge: dependencies.judge ?? bridgeJudge("bridge.toolCallGuard"),
575
- routeJudge: dependencies.routeJudge ?? bridgeJudge("bridge.turnRoute"),
576
- };
577
- }
578
- function sharedInstructionEnv(sharedInstructions) {
579
- return sharedInstructions ? { CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD: "1" } : {};
580
- }
581
- function turnEffortOf(state) {
582
- return state.turnRoute?.effort ?? "medium";
583
- }
584
- /** The agent's last message in the chat, empty on its first turn. */
585
- function previousReplyOf(job) {
586
- return (job.history?.entries ?? []).filter((entry) => entry.role === "assistant").at(-1)?.text ?? "";
587
- }
588
- function instructionSettingsForJob(job, sharedInstructions) {
589
- if (job.review) {
590
- return { claudeMdExcludes: ["**/CLAUDE.md"] };
591
- }
592
- return sharedInstructions ? { claudeMdExcludes: [join(bridgeRuntimeFiles.projectRoot(), "CLAUDE.md")] } : {};
593
- }
594
536
  // Carry a Runtime-wide model override down to the fan-out workers.
595
537
  //
596
538
  // The shared subagents pin themselves to Sonnet 5.5 in their own frontmatter (see
@@ -1250,10 +1192,9 @@ export class BridgeJobExecutor {
1250
1192
  settleWorkspace;
1251
1193
  catchUpWithMain;
1252
1194
  holdsNoWork;
1195
+ settleAfterPublish;
1253
1196
  keepCliInStep;
1254
1197
  judge;
1255
- routeJudge;
1256
- routeModels;
1257
1198
  constructor(dependencies) {
1258
1199
  this.sdk = dependencies.sdk;
1259
1200
  this.relay = dependencies.relay;
@@ -1276,11 +1217,13 @@ export class BridgeJobExecutor {
1276
1217
  this.settleWorkspace = dependencies.settleWorkspace ?? (() => settleWorkspaceAfterTasks());
1277
1218
  this.catchUpWithMain = dependencies.catchUpWithMain;
1278
1219
  this.holdsNoWork = dependencies.holdsNoWork;
1220
+ this.settleAfterPublish = dependencies.settleAfterPublish;
1279
1221
  this.keepCliInStep = dependencies.keepCliInStep;
1280
1222
  this.readFlowTree =
1281
1223
  dependencies.readFlowTree ?? ((flowSlug) => readRawFlowTree(bridgeRuntimeFiles.flowsDir(), flowSlug));
1282
- ({ judge: this.judge, routeJudge: this.routeJudge } = judgesOf(dependencies));
1283
- this.routeModels = dependencies.routeModels === true;
1224
+ this.judge =
1225
+ dependencies.judge ??
1226
+ jevJudge(process.env.OPENROUTER_API_KEY, runtimeOpenRouterTags(process.env, { service: "bridge", feature: "bridge.toolCallGuard" }));
1284
1227
  }
1285
1228
  async execute(job, controls) {
1286
1229
  try {
@@ -1292,45 +1235,6 @@ export class BridgeJobExecutor {
1292
1235
  await this.inFlightTurns.clear(job.threadId, job.turnId).catch(() => undefined);
1293
1236
  }
1294
1237
  }
1295
- /**
1296
- * What Jev reads in this turn, asked while the boundary loads (~0.3 s). Null
1297
- * for a Session review, which keeps its own model and effort, and for a turn
1298
- * that decides without it. Never throws: an unanswered judge is no signal.
1299
- */
1300
- async askSignals(job, request) {
1301
- if (job.review || !needsSignals(this.turnFacts(job, request))) {
1302
- return null;
1303
- }
1304
- return await askTurnSignals({
1305
- message: job.prompt,
1306
- previousReply: previousReplyOf(job),
1307
- judge: this.routeJudge,
1308
- }).catch(() => null);
1309
- }
1310
- /** This turn's model and effort (see `turn-route.ts`); undefined for a Session review. */
1311
- async routeFor(job, request, attempt, signals) {
1312
- if (job.review) {
1313
- return undefined;
1314
- }
1315
- const resumed = attempt.resumeOptions.resume;
1316
- const sessionModel = resumed ? await readConversationModel(resumed, claudeConfigDir()) : undefined;
1317
- return routeTurn({
1318
- ...this.turnFacts(job, request),
1319
- signals: await signals,
1320
- defaultModel: this.model,
1321
- routeModels: this.routeModels,
1322
- ...(sessionModel ? { sessionModel } : {}),
1323
- startedChat: previousReplyOf(job) === "",
1324
- });
1325
- }
1326
- turnFacts(job, request) {
1327
- return {
1328
- planMode: job.planMode === true,
1329
- leadingCommand: request.leadingCommand,
1330
- unattended: job.unattended === true,
1331
- hasAttachments: (job.attachments?.length ?? 0) > 0,
1332
- };
1333
- }
1334
1238
  async executeTurn(job, controls) {
1335
1239
  const state = createBridgeJobState();
1336
1240
  let result = { costUsd: 0, attemptedRehydration: false };
@@ -1342,8 +1246,6 @@ export class BridgeJobExecutor {
1342
1246
  if (!request?.prompt || !controls.isActive()) {
1343
1247
  return { kind: "cancelled", costUsd: 0, model: state.model };
1344
1248
  }
1345
- // Asked while the boundary loads, so the answer (~0.3 s) costs the turn no time.
1346
- const signals = this.askSignals(job, request);
1347
1249
  const loaded = await this.loadTrustedBoundary(job, controls);
1348
1250
  if (loaded?.kind !== "clean") {
1349
1251
  request = await this.withContinuedAttachments(job, request);
@@ -1362,7 +1264,6 @@ export class BridgeJobExecutor {
1362
1264
  state.detachedSession = attempt.detachedSession === true;
1363
1265
  result.attemptedRehydration = attempt.attemptedRehydration;
1364
1266
  this.recordAttemptContextSource(job, attempt);
1365
- state.turnRoute = await this.routeFor(job, request, attempt, signals);
1366
1267
  result = await this.runAttempt(job, controls, state, attempt);
1367
1268
  return await this.finishSuccessfulAttempt(job, controls, state, result.costUsd);
1368
1269
  }
@@ -1371,8 +1272,6 @@ export class BridgeJobExecutor {
1371
1272
  if (repaired && controls.isActive()) {
1372
1273
  const repairState = createBridgeJobState();
1373
1274
  repairState.detachedSession = repaired.detachedSession === true;
1374
- // The same turn, so the same model and level: a change would re-write the cache.
1375
- repairState.turnRoute = state.turnRoute;
1376
1275
  let repairResult = { costUsd: 0, attemptedRehydration: repaired.attemptedRehydration };
1377
1276
  try {
1378
1277
  repairResult = await this.runAttempt(job, controls, repairState, repaired);
@@ -1522,7 +1421,7 @@ export class BridgeJobExecutor {
1522
1421
  return `${block}\n\n${job.prompt}`;
1523
1422
  }
1524
1423
  async runAttempt(job, controls, state, attempt) {
1525
- state.model = state.turnRoute?.model ?? this.model;
1424
+ state.model = this.model;
1526
1425
  // A stalled stream closes the wedged query and resumes the CLI session in
1527
1426
  // place — the turn survives a hung upstream request instead of dying at the
1528
1427
  // 30-minute session watchdog with all mid-turn work stranded.
@@ -1530,7 +1429,7 @@ export class BridgeJobExecutor {
1530
1429
  let current = attempt;
1531
1430
  for (let stallRetries = 0;;) {
1532
1431
  try {
1533
- costUsd += await this.runQueryOnce(job, controls, state, current, state.model);
1432
+ costUsd += await this.runQueryOnce(job, controls, state, current, this.model);
1534
1433
  break;
1535
1434
  }
1536
1435
  catch (error) {
@@ -1575,22 +1474,22 @@ export class BridgeJobExecutor {
1575
1474
  return { costUsd, attemptedRehydration: attempt.attemptedRehydration };
1576
1475
  }
1577
1476
  async finishSuccessfulAttempt(job, controls, state, costUsd) {
1578
- const usage = turnUsage(state);
1477
+ const modelUsage = turnModelUsageList(state.modelUsageTotals);
1579
1478
  if (!controls.isActive()) {
1580
1479
  await this.clearIfDirty(job, controls, state, "abort");
1581
- return { kind: "cancelled", costUsd, model: state.model, ...usage };
1480
+ return { kind: "cancelled", costUsd, model: state.model, modelUsage };
1582
1481
  }
1583
1482
  // No boundary is handed back, so the refused exchange never becomes part of
1584
1483
  // the conversation the next message resumes.
1585
1484
  if (state.refusedWithoutFallback) {
1586
- return { kind: "errored", costUsd, model: state.model, ...usage, errors: [MODEL_REFUSAL_MESSAGE] };
1485
+ return { kind: "errored", costUsd, model: state.model, modelUsage, errors: [MODEL_REFUSAL_MESSAGE] };
1587
1486
  }
1588
1487
  if (!state.postedAssistantEvent && state.turnText.length > 0) {
1589
1488
  await this.postSyntheticAssistantEvent(job, state);
1590
1489
  }
1591
1490
  if (!controls.isActive()) {
1592
1491
  await this.clearIfDirty(job, controls, state, "abort");
1593
- return { kind: "cancelled", costUsd, model: state.model, ...usage };
1492
+ return { kind: "cancelled", costUsd, model: state.model, modelUsage };
1594
1493
  }
1595
1494
  // A detached turn (publish) ran in a session of its own. Handing that session
1596
1495
  // back as the thread's boundary would make the next chat message resume a
@@ -1603,11 +1502,17 @@ export class BridgeJobExecutor {
1603
1502
  // `closeRunThatChangedNothing`), and only the Runtime can vouch for its files.
1604
1503
  // Never after a publish turn: that one settles the chat itself.
1605
1504
  const workspaceUnchanged = !state.detachedSession && !job.review && this.holdsNoWork !== undefined && (await this.holdsNoWork());
1505
+ // A publish turn's push is settled before the chat goes on: its next turn starts from what went live.
1506
+ if (state.detachedSession) {
1507
+ await this.settleAfterPublish?.().catch((error) => {
1508
+ console.error(`[BridgeJob] could not settle the publish for ${job.threadId}`, error);
1509
+ });
1510
+ }
1606
1511
  return {
1607
1512
  kind: "completed",
1608
1513
  costUsd,
1609
1514
  model: state.model,
1610
- ...usage,
1515
+ modelUsage,
1611
1516
  ...(workspaceUnchanged ? { workspaceUnchanged: true } : {}),
1612
1517
  cleanBoundary,
1613
1518
  ...(state.structuredOutput === undefined ? {} : { structuredOutput: state.structuredOutput }),
@@ -1615,7 +1520,7 @@ export class BridgeJobExecutor {
1615
1520
  }
1616
1521
  errorOutcome(job, controls, state, result, error) {
1617
1522
  const model = state.model;
1618
- const usage = turnUsage(state);
1523
+ const modelUsage = turnModelUsageList(state.modelUsageTotals);
1619
1524
  if (result.attemptedRehydration) {
1620
1525
  void this.relay.contextSource({ threadId: job.threadId, turnId: job.turnId }, "rehydration_failed", {
1621
1526
  historyEntryCount: job.history?.entries.length ?? 0,
@@ -1625,7 +1530,7 @@ export class BridgeJobExecutor {
1625
1530
  });
1626
1531
  }
1627
1532
  if (!controls.isActive()) {
1628
- return { kind: "cancelled", costUsd: result.costUsd, model, ...usage };
1533
+ return { kind: "cancelled", costUsd: result.costUsd, model, modelUsage };
1629
1534
  }
1630
1535
  const shown = userFacingTurnError(error);
1631
1536
  if (shown !== errorMessage(error)) {
@@ -1635,7 +1540,7 @@ export class BridgeJobExecutor {
1635
1540
  kind: "errored",
1636
1541
  costUsd: result.costUsd,
1637
1542
  model,
1638
- ...usage,
1543
+ modelUsage,
1639
1544
  errors: [shown],
1640
1545
  };
1641
1546
  }
@@ -1910,7 +1815,6 @@ export class BridgeJobExecutor {
1910
1815
  await this.alignResumedTranscript(attempt);
1911
1816
  const mcpServers = await this.resolveMcpServers(job);
1912
1817
  const skills = await readSessionSkills(bridgeRuntimeFiles.projectRoot());
1913
- const sharedInstructions = await sharedInstructionsFor(job);
1914
1818
  const autoCompactWindow = autoCompactWindowForModel(model);
1915
1819
  const betas = betasForModel(model);
1916
1820
  // No `outputStyle`: the built-in `Concise` style was pinned here against Opus 5's
@@ -1946,7 +1850,6 @@ export class BridgeJobExecutor {
1946
1850
  }
1947
1851
  : {}),
1948
1852
  ...hookSettingsForJob(job),
1949
- ...instructionSettingsForJob(job, sharedInstructions),
1950
1853
  };
1951
1854
  const agentTags = runtimeOpenRouterTags(process.env, {
1952
1855
  service: "agent",
@@ -1966,17 +1869,15 @@ export class BridgeJobExecutor {
1966
1869
  // more per level than Opus 5, so compare `low` on real Sessions before
1967
1870
  // treating this as tuned.
1968
1871
  //
1969
- // A changed top-level effort alone re-writes the cached conversation from
1970
- // its first tool exchange onward. It may differ between turns only because
1971
- // the CLI also restates it per message (`PER_TURN_EFFORT_CAPABILITIES`),
1972
- // which keeps the cache; prose is no substitute.
1973
- effort: turnEffortOf(state),
1872
+ // MUST stay a single constant. The resolved effort is rendered into the
1873
+ // prompt, so changing it between requests invalidates every cache
1874
+ // breakpoint above it — on a session carrying hundreds of thousands of
1875
+ // tokens that re-write costs far more than any per-turn tuning saves.
1876
+ // Vary it per message only once the route accepts per-message effort
1877
+ // (see `BEDROCK_SERVABLE_CAPABILITIES`); prose is no substitute.
1878
+ effort: "medium",
1974
1879
  cwd: process.cwd(),
1975
1880
  settingSources: ["project"],
1976
- // The scaffold's CLAUDE.md, minus its `@COMPANY.md` import, as the system
1977
- // prompt, so every chat reads one cached copy instead of writing its own;
1978
- // the import loads from `instructionsDir` (see workspace-instructions.ts).
1979
- ...sharedInstructionOptions(sharedInstructions),
1980
1881
  // `baker_ui` hosts in-process interactive dashboard tools (secure tag
1981
1882
  // form). Always present; HTTP (Composio) servers merge alongside it.
1982
1883
  mcpServers: { ...(mcpServers ?? {}), baker_ui: createBakerUiServer({ credentials: credentialsEnabled() }) },
@@ -1989,7 +1890,7 @@ export class BridgeJobExecutor {
1989
1890
  // the workspace could not be read; omit rather than allowlist nothing.
1990
1891
  ...(skills ? { skills } : {}),
1991
1892
  disallowedTools: disallowedToolsFor(job),
1992
- ...withCredentialScrub(reviewQueryOptions(job), job),
1893
+ ...withCredentialScrub(answerOptions(job), job),
1993
1894
  // Never omitted: since SDK 0.3.286 an omitted mode starts a third-party-provider
1994
1895
  // session (OpenRouter) in auto mode, where a classifier rather than `canUseTool`
1995
1896
  // decides tool calls.
@@ -2028,8 +1929,6 @@ export class BridgeJobExecutor {
2028
1929
  // they are never TTY-gated, so every help read the agent does inside
2029
1930
  // the sandbox pays ~5-10% of its tokens on colour codes it can't see.
2030
1931
  NO_COLOR: "1",
2031
- // Loads the `CLAUDE.md` in `instructionsDir`, which imports COMPANY.md.
2032
- ...sharedInstructionEnv(sharedInstructions),
2033
1932
  // Pin the conversation to one OpenRouter provider instance so the
2034
1933
  // instance-scoped prompt cache is actually re-read across the turn's
2035
1934
  // many tool-call requests. Merged into every model request body by the
@@ -2041,8 +1940,8 @@ export class BridgeJobExecutor {
2041
1940
  ANTHROPIC_CUSTOM_HEADERS: [process.env.ANTHROPIC_CUSTOM_HEADERS, anthropicCustomHeaders(agentTags.headers)]
2042
1941
  .filter(Boolean)
2043
1942
  .join("\n"),
2044
- // Lets a turn change effort without losing the prompt cache; see openrouter-session.ts.
2045
- [MODEL_CAPABILITIES_ENV]: PER_TURN_EFFORT_CAPABILITIES,
1943
+ // Without it no Opus 5.5 request can reach Bedrock; see openrouter-session.ts.
1944
+ [MODEL_CAPABILITIES_ENV]: BEDROCK_SERVABLE_CAPABILITIES,
2046
1945
  // The CLI names each session with a second, structured-output request on the
2047
1946
  // turn's model. OpenRouter's Bedrock cannot serve that, so it was billed to
2048
1947
  // OpenRouter credits on Azure or Vertex, and nothing in Baker reads the name.
@@ -2124,7 +2023,6 @@ export class BridgeJobExecutor {
2124
2023
  for await (const msg of query) {
2125
2024
  stallDetector.observeMessage(msg);
2126
2025
  turnEnd.observeMessage(msg);
2127
- state.toolResults.observe(msg);
2128
2026
  controls.setWorkInFlight?.(stallDetector.hasPendingToolUses() || turnEnd.hasInFlightWork());
2129
2027
  try {
2130
2028
  costUsd = await this.consumeSdkMessage(job, controls, state, msg, costUsd);