@dotobokuri/fleet-console 1.48.0 → 1.50.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.
@@ -19031,6 +19031,7 @@ async function resolveCodexCredentials(deps) {
19031
19031
  return null;
19032
19032
  }
19033
19033
  }
19034
+ new TextEncoder().encode(": keep-alive\n\n");
19034
19035
  var OPENCODE_AUTH_PROVIDER_ID = "Claude Code with OpenCode Go";
19035
19036
  var FLEET_DATA_DIR_NAME = ".fleet";
19036
19037
  function getFleetDataDir() {
@@ -36945,6 +36945,16 @@ function clampReasoningEffort(effort, supportedEfforts, modelId) {
36945
36945
  if (lower) return lower.supported;
36946
36946
  throw new UnsupportedReasoningEffortError(effort, supportedEfforts, modelId);
36947
36947
  }
36948
+ var ContextWindowExceededError = class extends Error {
36949
+ constructor(requestTokens, contextWindow) {
36950
+ super(`Prompt is too long: ${requestTokens} tokens > ${contextWindow} maximum context window`);
36951
+ this.requestTokens = requestTokens;
36952
+ this.contextWindow = contextWindow;
36953
+ this.name = "ContextWindowExceededError";
36954
+ }
36955
+ requestTokens;
36956
+ contextWindow;
36957
+ };
36948
36958
  var DEFAULT_CHARS_PER_TOKEN = 4;
36949
36959
  var CODE_MODEL_CHARS_PER_TOKEN = 3.5;
36950
36960
  var CJK_CHARS_PER_TOKEN = 2.5;
@@ -38262,14 +38272,22 @@ function translateMessage(message) {
38262
38272
  }
38263
38273
  const items = [];
38264
38274
  const parts = [];
38265
- const flushParts = () => {
38275
+ let reasoningContent = "";
38276
+ const takeReasoningContent = () => {
38277
+ if (role !== "assistant" || reasoningContent.length === 0) return {};
38278
+ const reasoning = reasoningContent;
38279
+ reasoningContent = "";
38280
+ return { reasoning_content: reasoning };
38281
+ };
38282
+ const flushParts = (includeReasoning = true) => {
38266
38283
  if (parts.length === 0) {
38267
38284
  return;
38268
38285
  }
38269
38286
  items.push({
38270
38287
  type: "message",
38271
38288
  role,
38272
- content: normalizeCanonicalMessageContent(parts.splice(0, parts.length))
38289
+ content: normalizeCanonicalMessageContent(parts.splice(0, parts.length)),
38290
+ ...includeReasoning ? takeReasoningContent() : {}
38273
38291
  });
38274
38292
  };
38275
38293
  for (const block of message.content) {
@@ -38283,12 +38301,13 @@ function translateMessage(message) {
38283
38301
  parts.push(translateImageBlock(block));
38284
38302
  break;
38285
38303
  case "tool_use":
38286
- flushParts();
38304
+ flushParts(false);
38287
38305
  items.push({
38288
38306
  type: "function_call",
38289
38307
  call_id: block.id,
38290
38308
  name: block.name,
38291
- arguments: JSON.stringify(block.input)
38309
+ arguments: JSON.stringify(block.input),
38310
+ ...takeReasoningContent()
38292
38311
  });
38293
38312
  break;
38294
38313
  case "tool_result": {
@@ -38304,6 +38323,10 @@ function translateMessage(message) {
38304
38323
  break;
38305
38324
  }
38306
38325
  case "thinking":
38326
+ if (role === "assistant" && typeof block.thinking === "string") {
38327
+ reasoningContent += block.thinking;
38328
+ }
38329
+ break;
38307
38330
  case "redacted_thinking":
38308
38331
  break;
38309
38332
  default:
@@ -38455,6 +38478,7 @@ async function collectAnthropicMessage(events, fallbackModel, options = {}) {
38455
38478
  let id = "msg_gateway";
38456
38479
  let model = fallbackModel;
38457
38480
  let text2 = "";
38481
+ const thinking = /* @__PURE__ */ new Map();
38458
38482
  let stopReason = "end_turn";
38459
38483
  let outputTokens = 0;
38460
38484
  let inputTokens = 0;
@@ -38474,6 +38498,9 @@ async function collectAnthropicMessage(events, fallbackModel, options = {}) {
38474
38498
  case "response.output_text.delta":
38475
38499
  text2 += event.delta;
38476
38500
  break;
38501
+ case "response.reasoning_summary_text.delta":
38502
+ thinking.set(event.item_id, (thinking.get(event.item_id) ?? "") + event.delta);
38503
+ break;
38477
38504
  case "response.output_item.added":
38478
38505
  if (event.item.type === "function_call") {
38479
38506
  stopReason = "tool_use";
@@ -38526,6 +38553,11 @@ async function collectAnthropicMessage(events, fallbackModel, options = {}) {
38526
38553
  if (block) block.input = safeJson(entry.text);
38527
38554
  }
38528
38555
  if (text2.length > 0) content.unshift({ type: "text", text: text2 });
38556
+ content.unshift(...[...thinking].map(([itemId, value]) => ({
38557
+ type: "thinking",
38558
+ thinking: value,
38559
+ signature: `gateway_${reasoningBlockKey(itemId)}`
38560
+ })));
38529
38561
  return {
38530
38562
  id,
38531
38563
  type: "message",
@@ -38559,38 +38591,49 @@ async function* encodeAnthropicSse(events, options = {}) {
38559
38591
  let messageStarted = false;
38560
38592
  let messageStopped = false;
38561
38593
  let sawToolUse = false;
38594
+ let activeContentKey;
38562
38595
  const encode3 = (event, data) => encoder2.encode(`event: ${event}
38563
38596
  data: ${JSON.stringify(data)}
38564
38597
 
38565
38598
  `);
38566
- const startTextBlock = (key) => {
38599
+ const startContentBlock = (key, kind) => {
38567
38600
  if (blocks.has(key)) {
38568
38601
  return void 0;
38569
38602
  }
38570
38603
  const block = {
38571
38604
  index: nextBlockIndex++,
38572
- kind: "text",
38605
+ kind,
38573
38606
  accumulatedJson: "",
38574
38607
  closed: false
38575
38608
  };
38576
38609
  blocks.set(key, block);
38610
+ activeContentKey = key;
38577
38611
  return encode3("content_block_start", {
38578
38612
  type: "content_block_start",
38579
38613
  index: block.index,
38580
- content_block: { type: "text", text: "" }
38614
+ content_block: kind === "text" ? { type: "text", text: "" } : { type: "thinking", thinking: "", signature: "" }
38581
38615
  });
38582
38616
  };
38583
38617
  const closeBlock = (key) => {
38584
38618
  const block = blocks.get(key);
38585
38619
  if (block === void 0 || block.closed) {
38586
- return void 0;
38620
+ return [];
38587
38621
  }
38588
38622
  block.closed = true;
38589
- return encode3("content_block_stop", {
38590
- type: "content_block_stop",
38591
- index: block.index
38592
- });
38623
+ if (activeContentKey === key) activeContentKey = void 0;
38624
+ return [
38625
+ ...block.kind === "thinking" ? [encode3("content_block_delta", {
38626
+ type: "content_block_delta",
38627
+ index: block.index,
38628
+ delta: { type: "signature_delta", signature: `gateway_${key}` }
38629
+ })] : [],
38630
+ encode3("content_block_stop", {
38631
+ type: "content_block_stop",
38632
+ index: block.index
38633
+ })
38634
+ ];
38593
38635
  };
38636
+ const closeActiveContent = () => activeContentKey === void 0 ? [] : closeBlock(activeContentKey);
38594
38637
  for await (const event of events) {
38595
38638
  switch (event.type) {
38596
38639
  case "response.created":
@@ -38615,17 +38658,19 @@ data: ${JSON.stringify(data)}
38615
38658
  break;
38616
38659
  case "response.content_part.added": {
38617
38660
  const key = textBlockKey(event.item_id, event.content_index);
38618
- const start = startTextBlock(key);
38619
- if (start !== void 0) {
38620
- yield start;
38661
+ if (!blocks.has(key)) {
38662
+ yield* closeActiveContent();
38663
+ const start = startContentBlock(key, "text");
38664
+ if (start !== void 0) yield start;
38621
38665
  }
38622
38666
  break;
38623
38667
  }
38624
38668
  case "response.output_text.delta": {
38625
38669
  const key = textBlockKey(event.item_id, event.content_index);
38626
- const start = startTextBlock(key);
38627
- if (start !== void 0) {
38628
- yield start;
38670
+ if (!blocks.has(key)) {
38671
+ yield* closeActiveContent();
38672
+ const start = startContentBlock(key, "text");
38673
+ if (start !== void 0) yield start;
38629
38674
  }
38630
38675
  const block = blocks.get(key);
38631
38676
  if (block !== void 0) {
@@ -38637,15 +38682,29 @@ data: ${JSON.stringify(data)}
38637
38682
  }
38638
38683
  break;
38639
38684
  }
38640
- case "response.output_text.done": {
38641
- const stop = closeBlock(textBlockKey(event.item_id, event.content_index));
38642
- if (stop !== void 0) {
38643
- yield stop;
38685
+ case "response.output_text.done":
38686
+ yield* closeBlock(textBlockKey(event.item_id, event.content_index));
38687
+ break;
38688
+ case "response.reasoning_summary_text.delta": {
38689
+ const key = reasoningBlockKey(event.item_id);
38690
+ if (!blocks.has(key)) {
38691
+ yield* closeActiveContent();
38692
+ const start = startContentBlock(key, "thinking");
38693
+ if (start !== void 0) yield start;
38694
+ }
38695
+ const block = blocks.get(key);
38696
+ if (block !== void 0) {
38697
+ yield encode3("content_block_delta", {
38698
+ type: "content_block_delta",
38699
+ index: block.index,
38700
+ delta: { type: "thinking_delta", thinking: event.delta }
38701
+ });
38644
38702
  }
38645
38703
  break;
38646
38704
  }
38647
38705
  case "response.output_item.added":
38648
38706
  if (event.item.type === "function_call") {
38707
+ yield* closeActiveContent();
38649
38708
  sawToolUse = true;
38650
38709
  const block = {
38651
38710
  index: nextBlockIndex++,
@@ -38687,16 +38746,14 @@ data: ${JSON.stringify(data)}
38687
38746
  delta: { type: "input_json_delta", partial_json: remainder }
38688
38747
  });
38689
38748
  }
38690
- const stop = closeBlock(event.item_id);
38691
- if (stop !== void 0) {
38692
- yield stop;
38693
- }
38749
+ yield* closeBlock(event.item_id);
38694
38750
  break;
38695
38751
  }
38696
38752
  case "response.output_item.done":
38697
38753
  if (event.item.type === "function_call") {
38698
38754
  let block = blocks.get(event.item.id);
38699
38755
  if (block === void 0) {
38756
+ yield* closeActiveContent();
38700
38757
  const syntheticStart = functionCallStart(event.item, nextBlockIndex++);
38701
38758
  block = syntheticStart.block;
38702
38759
  blocks.set(event.item.id, block);
@@ -38712,11 +38769,9 @@ data: ${JSON.stringify(data)}
38712
38769
  delta: { type: "input_json_delta", partial_json: remainder }
38713
38770
  });
38714
38771
  }
38715
- const stop = closeBlock(event.item.id);
38716
- if (stop !== void 0) {
38717
- yield stop;
38718
- }
38772
+ yield* closeBlock(event.item.id);
38719
38773
  } else if (event.item.type === "web_search_call") {
38774
+ yield* closeActiveContent();
38720
38775
  const query = derivedWebSearchQuery(event.item.action);
38721
38776
  const searchIndex = nextBlockIndex++;
38722
38777
  yield encode3("content_block_start", {
@@ -38750,10 +38805,7 @@ data: ${JSON.stringify(data)}
38750
38805
  break;
38751
38806
  case "response.completed":
38752
38807
  for (const [key] of blocks) {
38753
- const stop = closeBlock(key);
38754
- if (stop !== void 0) {
38755
- yield stop;
38756
- }
38808
+ yield* closeBlock(key);
38757
38809
  }
38758
38810
  yield encode3("message_delta", {
38759
38811
  type: "message_delta",
@@ -38770,10 +38822,12 @@ data: ${JSON.stringify(data)}
38770
38822
  messageStopped = true;
38771
38823
  break;
38772
38824
  case "response.failed":
38825
+ yield* closeActiveContent();
38773
38826
  yield encode3("error", { type: "error", error: event.response.error });
38774
38827
  messageStopped = true;
38775
38828
  break;
38776
38829
  case "error":
38830
+ yield* closeActiveContent();
38777
38831
  yield encode3("error", { type: "error", error: event.error });
38778
38832
  messageStopped = true;
38779
38833
  break;
@@ -38786,6 +38840,9 @@ data: ${JSON.stringify(data)}
38786
38840
  function textBlockKey(itemId, contentIndex) {
38787
38841
  return `${itemId}:${contentIndex}`;
38788
38842
  }
38843
+ function reasoningBlockKey(itemId) {
38844
+ return `reasoning:${itemId}`;
38845
+ }
38789
38846
  function requiredToolBlock(blocks, itemId) {
38790
38847
  const block = blocks.get(itemId);
38791
38848
  if (block === void 0 || block.kind !== "tool_use") {
@@ -40711,6 +40768,28 @@ var CursorAdapter = class {
40711
40768
  throw error51;
40712
40769
  }
40713
40770
  wireLog("cursor.wire.plan", plan);
40771
+ const contextRecall = recallCursorContextCheckpoint(
40772
+ identity.conversationId,
40773
+ plan.wireModelId,
40774
+ credentialFingerprint,
40775
+ plan.estimatedInputTokens
40776
+ );
40777
+ const contextRefusal = cursorContextWindowRefusal(
40778
+ contextRecall.checkpoint,
40779
+ options.modelContextWindow
40780
+ );
40781
+ if (contextRefusal || contextRecall.compacted) {
40782
+ const stale = this.pendingLiveRuns.get(conversationStateKey);
40783
+ if (stale && this.claimPendingLiveRun(stale)) {
40784
+ const outcome = contextRefusal ? "context_window_exceeded" : "conversation_compacted";
40785
+ stale.run.report("bridge.mismatch", {
40786
+ model: cursorDiagnosticLabel(request.model),
40787
+ outcome
40788
+ });
40789
+ stale.run.dispose(`bridge_mismatch_${outcome}`);
40790
+ }
40791
+ if (contextRefusal) throw contextRefusal;
40792
+ }
40714
40793
  const descriptor = {
40715
40794
  conversationId: identity.conversationId,
40716
40795
  sessionId: identity.sessionId,
@@ -40756,7 +40835,7 @@ var CursorAdapter = class {
40756
40835
  pending.run.dispose(`bridge_mismatch_${mismatch ?? "concurrent_claim"}`);
40757
40836
  }
40758
40837
  }
40759
- return this.openRun(request, options, identity, plan, descriptor);
40838
+ return this.openRun(request, options, identity, plan, descriptor, contextRecall.checkpoint);
40760
40839
  }
40761
40840
  /** Close every adapter-owned parked Run. Safe to call more than once. */
40762
40841
  dispose() {
@@ -40829,7 +40908,7 @@ var CursorAdapter = class {
40829
40908
  const pending = this.pendingLiveRuns.get(key);
40830
40909
  if (pending?.run === run) this.claimPendingLiveRun(pending);
40831
40910
  }
40832
- async openRun(request, options, identity, plan, descriptor) {
40911
+ async openRun(request, options, identity, plan, descriptor, previousContextCheckpoint) {
40833
40912
  if (options.signal?.aborted) throw new Error("cancelled by caller");
40834
40913
  const report = createCursorDiagnosticReporter(
40835
40914
  options.diagnosticsEnabled === false ? void 0 : this.diagnostics
@@ -40841,12 +40920,6 @@ var CursorAdapter = class {
40841
40920
  descriptor.credentialFingerprint,
40842
40921
  plan.wireModelId
40843
40922
  );
40844
- const previousContextCheckpoint = recallCursorContextCheckpoint(
40845
- identity.conversationId,
40846
- plan.wireModelId,
40847
- descriptor.credentialFingerprint,
40848
- plan.estimatedInputTokens
40849
- );
40850
40923
  report("turn.start", {
40851
40924
  model,
40852
40925
  wireModel,
@@ -41063,13 +41136,22 @@ function rememberCursorWireModel(conversationId, credentialFingerprint, wireMode
41063
41136
  function recallCursorContextCheckpoint(conversationId, wireModelId, credentialFingerprint, requestInputTokens) {
41064
41137
  const key = cursorConversationStateKey(credentialFingerprint, conversationId);
41065
41138
  const checkpoint = CURSOR_CONTEXT_CHECKPOINT_BY_STATE.get(key);
41066
- if (!checkpoint) return void 0;
41139
+ if (!checkpoint) return { checkpoint: void 0, compacted: false };
41067
41140
  CURSOR_CONTEXT_CHECKPOINT_BY_STATE.delete(key);
41068
- if (checkpoint.wireModelId !== wireModelId || checkpoint.credentialFingerprint !== credentialFingerprint || requestInputTokens < checkpoint.requestInputTokens) {
41069
- return void 0;
41141
+ if (checkpoint.wireModelId !== wireModelId || checkpoint.credentialFingerprint !== credentialFingerprint) {
41142
+ return { checkpoint: void 0, compacted: false };
41143
+ }
41144
+ if (requestInputTokens < checkpoint.requestInputTokens) {
41145
+ return { checkpoint: void 0, compacted: true };
41070
41146
  }
41071
41147
  CURSOR_CONTEXT_CHECKPOINT_BY_STATE.set(key, checkpoint);
41072
- return checkpoint;
41148
+ return { checkpoint, compacted: false };
41149
+ }
41150
+ function cursorContextWindowRefusal(checkpoint, modelContextWindow) {
41151
+ if (checkpoint === void 0 || typeof modelContextWindow !== "number" || !Number.isFinite(modelContextWindow) || modelContextWindow <= 0 || checkpoint.contextTokens < modelContextWindow) {
41152
+ return void 0;
41153
+ }
41154
+ return new ContextWindowExceededError(checkpoint.contextTokens, modelContextWindow);
41073
41155
  }
41074
41156
  function rememberCursorContextCheckpoint(conversationId, wireModelId, credentialFingerprint, requestInputTokens, checkpoint) {
41075
41157
  const key = cursorConversationStateKey(credentialFingerprint, conversationId);
@@ -41573,6 +41655,12 @@ ${argumentsText}`;
41573
41655
  }
41574
41656
  if (isRecord42(update.thinkingDelta) && typeof update.thinkingDelta.text === "string") {
41575
41657
  activeSegment.outputText += update.thinkingDelta.text;
41658
+ emit({
41659
+ type: "response.reasoning_summary_text.delta",
41660
+ item_id: `${activeSegment.itemId}_reasoning`,
41661
+ output_index: 0,
41662
+ delta: update.thinkingDelta.text
41663
+ });
41576
41664
  return;
41577
41665
  }
41578
41666
  if (isRecord42(update.tokenDelta)) {
@@ -42596,6 +42684,14 @@ function canonicalEvent(value) {
42596
42684
  content_index: number4(value.content_index, "content_index"),
42597
42685
  text: string4(value.text, "text")
42598
42686
  };
42687
+ case "response.reasoning_summary_text.delta":
42688
+ case "response.reasoning_text.delta":
42689
+ return {
42690
+ type: "response.reasoning_summary_text.delta",
42691
+ item_id: string4(value.item_id, "item_id"),
42692
+ output_index: number4(value.output_index, "output_index"),
42693
+ delta: string4(value.delta, "delta")
42694
+ };
42599
42695
  case "response.output_item.added":
42600
42696
  case "response.output_item.done": {
42601
42697
  const item = outputItem(value.item);
@@ -42924,16 +43020,62 @@ function optionalNonNegativeNumber(value, name) {
42924
43020
  }
42925
43021
  return value;
42926
43022
  }
42927
- var ContextWindowExceededError = class extends Error {
42928
- constructor(requestTokens, contextWindow) {
42929
- super(`Prompt is too long: ${requestTokens} tokens > ${contextWindow} maximum context window`);
42930
- this.requestTokens = requestTokens;
42931
- this.contextWindow = contextWindow;
42932
- this.name = "ContextWindowExceededError";
43023
+ var ANTHROPIC_SSE_KEEPALIVE_INTERVAL_MS = 1e4;
43024
+ var KEEPALIVE_FRAME = new TextEncoder().encode(": keep-alive\n\n");
43025
+ async function* withSseKeepAlive(chunks, intervalMs = ANTHROPIC_SSE_KEEPALIVE_INTERVAL_MS) {
43026
+ const iterator = chunks[Symbol.asyncIterator]();
43027
+ let next = iterator.next();
43028
+ let boundaryTail = "";
43029
+ let atFrameBoundary = true;
43030
+ try {
43031
+ for (; ; ) {
43032
+ const result = atFrameBoundary ? await waitForChunk(next, intervalMs) : { kind: "chunk", result: await next };
43033
+ if (result.kind === "keepalive") {
43034
+ yield KEEPALIVE_FRAME;
43035
+ continue;
43036
+ }
43037
+ if (result.result.done) return;
43038
+ if (result.result.value.byteLength > 0) {
43039
+ boundaryTail = appendBoundaryTail(boundaryTail, result.result.value);
43040
+ atFrameBoundary = endsAtSseFrameBoundary(boundaryTail);
43041
+ }
43042
+ yield result.result.value;
43043
+ next = iterator.next();
43044
+ }
43045
+ } finally {
43046
+ await iterator.return?.();
42933
43047
  }
42934
- requestTokens;
42935
- contextWindow;
42936
- };
43048
+ }
43049
+ function appendBoundaryTail(tail, chunk) {
43050
+ return (tail + new TextDecoder().decode(chunk)).slice(-4);
43051
+ }
43052
+ function endsAtSseFrameBoundary(tail) {
43053
+ return tail.endsWith("\n\n") || tail.endsWith("\r\r") || tail.endsWith("\r\n\r\n");
43054
+ }
43055
+ async function waitForChunk(next, intervalMs) {
43056
+ return await new Promise((resolve3, reject) => {
43057
+ let settled = false;
43058
+ const timer = setTimeout(() => {
43059
+ settled = true;
43060
+ resolve3({ kind: "keepalive" });
43061
+ }, intervalMs);
43062
+ timer.unref?.();
43063
+ next.then(
43064
+ (result) => {
43065
+ if (settled) return;
43066
+ settled = true;
43067
+ clearTimeout(timer);
43068
+ resolve3({ kind: "chunk", result });
43069
+ },
43070
+ (error51) => {
43071
+ if (settled) return;
43072
+ settled = true;
43073
+ clearTimeout(timer);
43074
+ reject(error51);
43075
+ }
43076
+ );
43077
+ });
43078
+ }
42937
43079
  var AnthropicMessagesGateway = class {
42938
43080
  constructor(adapter = new OpenAIResponsesAdapter()) {
42939
43081
  this.adapter = adapter;
@@ -42964,6 +43106,7 @@ var AnthropicMessagesGateway = class {
42964
43106
  guardModelContextWindow(canonical, options.modelContextWindow, this.adapter);
42965
43107
  const upstream = await this.adapter.stream(canonical, {
42966
43108
  apiKey: options.apiKey,
43109
+ ...options.modelContextWindow === void 0 ? {} : { modelContextWindow: options.modelContextWindow },
42967
43110
  ...options.diagnosticsEnabled === void 0 ? {} : { diagnosticsEnabled: options.diagnosticsEnabled },
42968
43111
  signal: options.signal
42969
43112
  });
@@ -42992,10 +43135,10 @@ var AnthropicMessagesGateway = class {
42992
43135
  "cache-control": "no-cache",
42993
43136
  "content-type": "text/event-stream; charset=utf-8"
42994
43137
  }),
42995
- body: encodeAnthropicSse(events, {
43138
+ body: withSseKeepAlive(encodeAnthropicSse(events, {
42996
43139
  contextWindow: options.contextWindow,
42997
43140
  model: request.model
42998
- })
43141
+ }))
42999
43142
  };
43000
43143
  }
43001
43144
  const message = await collectAnthropicMessage(events, request.model, {
@@ -43146,18 +43289,22 @@ function forChatCompletionsBackend(request) {
43146
43289
  if (request.instructions !== void 0 && request.instructions.length > 0) {
43147
43290
  messages.push({ role: "system", content: request.instructions });
43148
43291
  }
43292
+ const replayReasoning = request.model.startsWith("deepseek-v4-");
43149
43293
  let pendingToolCalls = [];
43150
43294
  let pendingAssistantText;
43295
+ let pendingAssistantReasoning;
43151
43296
  let deferredMessages = [];
43152
43297
  const flushToolCalls = () => {
43153
43298
  if (pendingToolCalls.length === 0) return;
43154
43299
  messages.push({
43155
43300
  role: "assistant",
43156
43301
  content: pendingAssistantText ?? null,
43157
- tool_calls: pendingToolCalls
43302
+ tool_calls: pendingToolCalls,
43303
+ ...replayReasoning && pendingAssistantReasoning ? { reasoning_content: pendingAssistantReasoning } : {}
43158
43304
  });
43159
43305
  pendingToolCalls = [];
43160
43306
  pendingAssistantText = void 0;
43307
+ pendingAssistantReasoning = void 0;
43161
43308
  };
43162
43309
  const flushDeferredMessages = () => {
43163
43310
  if (deferredMessages.length === 0) return;
@@ -43172,6 +43319,9 @@ function forChatCompletionsBackend(request) {
43172
43319
  type: "function",
43173
43320
  function: { name: item.name, arguments: item.arguments }
43174
43321
  });
43322
+ if (replayReasoning && item.reasoning_content) {
43323
+ pendingAssistantReasoning ??= item.reasoning_content;
43324
+ }
43175
43325
  continue;
43176
43326
  }
43177
43327
  if (item.type === "function_call_output") {
@@ -43188,12 +43338,12 @@ function forChatCompletionsBackend(request) {
43188
43338
  ${text2}`;
43189
43339
  }
43190
43340
  } else {
43191
- deferredMessages.push(chatWireMessage(item));
43341
+ deferredMessages.push(chatWireMessage(item, replayReasoning));
43192
43342
  }
43193
43343
  continue;
43194
43344
  }
43195
43345
  flushDeferredMessages();
43196
- messages.push(chatWireMessage(item));
43346
+ messages.push(chatWireMessage(item, replayReasoning));
43197
43347
  }
43198
43348
  flushToolCalls();
43199
43349
  flushDeferredMessages();
@@ -43226,12 +43376,19 @@ ${text2}`;
43226
43376
  }
43227
43377
  return payload;
43228
43378
  }
43229
- function chatWireMessage(item) {
43379
+ function chatWireMessage(item, replayReasoning) {
43230
43380
  const role = item.role === "developer" ? "system" : item.role;
43381
+ if (role === "assistant") {
43382
+ return {
43383
+ role,
43384
+ content: canonicalMessageText(item.content),
43385
+ ...replayReasoning && item.reasoning_content ? { reasoning_content: item.reasoning_content } : {}
43386
+ };
43387
+ }
43231
43388
  if (typeof item.content === "string") {
43232
43389
  return { role, content: item.content };
43233
43390
  }
43234
- if (role !== "user") {
43391
+ if (role === "system") {
43235
43392
  return { role, content: canonicalMessageText(item.content) };
43236
43393
  }
43237
43394
  const parts = item.content.map((part) => {
@@ -43301,6 +43458,14 @@ async function* translateChatCompletionsStream(body, options) {
43301
43458
  for (const choice of choices) {
43302
43459
  if (!isRecord7(choice)) continue;
43303
43460
  const delta = isRecord7(choice.delta) ? choice.delta : {};
43461
+ if (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0) {
43462
+ yield {
43463
+ type: "response.reasoning_summary_text.delta",
43464
+ item_id: `${MESSAGE_ITEM_ID}_reasoning`,
43465
+ output_index: 0,
43466
+ delta: delta.reasoning_content
43467
+ };
43468
+ }
43304
43469
  if (typeof delta.content === "string" && delta.content.length > 0) {
43305
43470
  textSeen = true;
43306
43471
  accumulatedText += delta.content;
@@ -43749,14 +43914,17 @@ var GATEWAY_MODELS_DOCTRINE = {
43749
43914
  // 이 목록은 필드 사전이 아니라 판정 규칙이다. 응답을 보면 알 수 있는 것은 빼고,
43750
43915
  // 틀리면 조용히 실패하는 것만 남긴다 — 길어질수록 읽히지 않고, 읽히지 않으면 없는 것과 같다.
43751
43916
  usageGuidelines: [
43752
- `Two spellings, never interchangeable. agentTypes names this identity once per reasoning rung, so selecting by name already carries the level and nothing further pins effort; modelId is the model as a value. Neither derives from the other \u2014 the transform behind a name collapses ".", "[1m]", and "--" all into "-". Prefer a name: a wrong name fails loudly, while modelId reaches whatever the catalog holds, including a model the user turned off, in silence.`,
43753
- `Names are registered once at session start; this roster is re-read live. A model exposed mid-session therefore appears here under a name that will not resolve until a new session \u2014 that, and not a stale roster, is what an unknown-name failure means.`,
43754
- `Each provider entry pairs one allowance with the exposed models it serves, so the window to read against a model is in the same entry. Every entry reports its allowance, the parent subscription included; what differs is what you can select. claude reports a window but serves no model by design, so a session running a built-in Claude model spends an allowance you can read and never select \u2014 spare it by pinning away from it; a session launched on a gateway default instead spends the entry that serves that model, which further runs can pile back onto. A model in no entry is one the user turned off.`,
43755
- `effortLadder is what this session offers as identities, not what the model can do: the user exposes a model at some or all of its levels, so a level absent here may still exist upstream. A level outside the model's own ladder is clamped down with no signal and refused when nothing is below it, and some models have no effort control at all. The registration caveat above covers levels too: widening a model's exposure mid-session adds names here that this session cannot resolve.`,
43756
- `roleFit null means unmeasured, never unsuitable: quality then gives no reason to prefer an identity, so the choice falls to allowance and never back to this session's own model. homolineage and the provider entry answer different questions and never collapse into one: homolineage marks a Claude-family model, derived from its id alone and silent about what this session runs on, while the entry it sits under marks whose allowance it spends. A Claude-family model under another provider moves spend without buying independence from a Claude-family session, and independence is judged against the subject under examination, not against this session.`,
43757
- `Read pressure before doing arithmetic of your own; it is the verdict on one window, combining headroom with burn pace against that window's own clock, and it never says which model to choose. Compare usedPercent only within one cadence \u2014 a shared window id does not mean a shared length. Where a provider splits into pools, quotaScope picks the window that applies and the isAggregate one stays out of headroom math. recoveryHalfLifeMs prices the drain: weeks of lockout for a monthly window, hours for a session one.`,
43917
+ `Two spellings, never interchangeable. agentTypes names an identity once per reasoning rung, so a name already carries the level and nothing further pins effort; modelId is the model as a value. Prefer a name: a wrong one fails loudly, while modelId reaches whatever the catalog holds, including a model the user turned off, in silence.`,
43918
+ `Names are registered once at session start; this roster is re-read live. A model \u2014 or a level \u2014 exposed mid-session appears here under a name that will not resolve until a new session. That, not a stale roster, is what an unknown-name failure means.`,
43919
+ `effortLadder is what this session offers, not what the model can do. A level outside the model's own ladder is clamped down with no signal and refused when nothing is below it, and some models have no effort control at all.`,
43920
+ `A model's window is in the provider entry that serves it, and a model in no entry is one the user turned off. Every entry reports its allowance, the parent subscription included; what differs is what you can select.`,
43921
+ `claude reports a window but serves no model by design, so a session on a built-in Claude model spends an allowance you can read and never select; a session launched on a gateway default instead spends the entry that serves that model, which further runs can pile back onto.`,
43922
+ `Read pressure before doing arithmetic of your own; it is the verdict on one window, combining headroom with burn pace against that window's own clock, and it never says which model to choose. Compare usedPercent only within one cadence \u2014 a shared window id does not mean a shared length.`,
43923
+ `Where a provider splits into pools, quotaScope picks the window that applies and the isAggregate one stays out of headroom math. recoveryHalfLifeMs prices the drain: weeks of lockout for a monthly window, hours for a session one.`,
43924
+ `Three fields, three questions, and none implies another. homolineage marks a Claude-family model, derived from its id alone and silent about what this session runs on; the entry it sits under marks whose allowance it spends; roleFit.tokenEfficiency counts tokens and tool calls, which no allowance is derived from.`,
43925
+ `roleFit null means unmeasured, never unsuitable: quality then gives no reason to prefer an identity, so the choice falls to allowance and never back to this session's own model.`,
43758
43926
  `Absence is never safety. A missing derived field means the reading could not support it, and status "unsupported" means the allowance could not be read at all.`,
43759
- `The roster cannot tell which provider this session itself runs on \u2014 it is registered once per runtime \u2014 so make that match yourself and read that window. isSessionDefault reflects Settings as it stands now, not what an already-running session launched with.`
43927
+ `The roster cannot tell which provider this session itself runs on \u2014 make that match yourself and read that window. isSessionDefault reflects Settings as it stands now, not what an already-running session launched with.`
43760
43928
  ]
43761
43929
  };
43762
43930
  function buildGatewayModelsToolSpec(deps) {
@@ -43996,9 +44164,14 @@ function buildClaudeNativeArgs(context) {
43996
44164
  ]),
43997
44165
  ...context.mcpServers.length > 0 ? ["--mcp-config", buildClaudeMcpConfig(context.mcpServers)] : [],
43998
44166
  ...buildCustomAgentsArgs(context.customAgents),
44167
+ ...buildSettingsArgs(context.skillOverrides),
43999
44168
  "--dangerously-skip-permissions"
44000
44169
  ];
44001
44170
  }
44171
+ function buildSettingsArgs(skillOverrides) {
44172
+ if (skillOverrides === void 0 || Object.keys(skillOverrides).length === 0) return [];
44173
+ return ["--settings", JSON.stringify({ skillOverrides })];
44174
+ }
44002
44175
  function buildCustomAgentsArgs(agents) {
44003
44176
  if (agents === void 0 || Object.keys(agents).length === 0) return [];
44004
44177
  return ["--agents", JSON.stringify(agents)];
@@ -44024,6 +44197,13 @@ function buildClaudeMcpConfig(servers) {
44024
44197
  });
44025
44198
  }
44026
44199
 
44200
+ // ../../packages/fleet-admiral/src/agent-cli/gateway-skills.ts
44201
+ var GATEWAY_DISABLED_CLAUDE_SKILLS = ["claude-api"];
44202
+ function buildDisabledSkillOverrides(skillNames) {
44203
+ if (skillNames.length === 0) return void 0;
44204
+ return Object.fromEntries(skillNames.map((name) => [name, "off"]));
44205
+ }
44206
+
44027
44207
  // ../../packages/fleet-admiral/src/agent-cli/assets.generated.ts
44028
44208
  var EMBEDDED_AGENT_CLI_SKILL_ASSETS = [
44029
44209
  { relativePath: "assumption-audit/SKILL.md", content: "---\nname: assumption-audit\ndescription: Resolve decision-shaped blocking gaps one at a time \u2014 Context Confidence gate failures during an active protocol, or pre-engagement requirements ambiguity routed by the Command Integrity Standing Order.\n---\n\nUse this auxiliary skill only when a decision-shaped blocking gap has been found: either the active protocol or Context Confidence re-entry path surfaced it, or the Command Integrity Standing Order routed a pre-engagement requirements ambiguity here before a protocol mode loads. This skill is not a protocol mode, does not replace the active protocol, and cannot declare the planning boundary passed by itself.\n\nFor each unresolved blocking gap, triage the gap before questioning:\n\n- **Scout-shaped**: the answer should come from direct file reads, focused reconnaissance, carrier scouting, or another verifiable evidence source. Send the workflow back to that evidence-gathering path instead of asking the user to decide.\n- **Decision-shaped**: the answer depends on preference, scope, risk appetite, product intent, or authority that evidence alone cannot settle. Ask exactly one question for this gap.\n- **Escalation-shaped**: the answer requires authority beyond the current operator, changes the mission boundary, repeatedly fails to resolve, or would weaken the active protocol's required gate. Escalate to the user.\n\nWhen a gap is decision-shaped, ask one question at a time. Present your recommended answer first, then give one or two concrete alternatives when useful. Walk decision dependencies one branch at a time until the current gap is resolved; do not bundle unrelated gaps into the same question.\n\nAfter the gap is answered, report the resolved decision in one short line and return control to the caller: the active protocol or Context Confidence Standing Order on re-entry, or the Protocol Gate when invoked pre-engagement. An active workflow must re-evaluate confidence and re-apply the required planning boundary gate before planning proceeds.\n" },
@@ -44100,7 +44280,7 @@ If the intended Carrier is unavailable or carrier_dispatch rejects the requested
44100
44280
  { relativePath: "gateway/codebase-research/SKILL.md", content: "---\nname: codebase-research\ndescription: Answer a question about a codebase or an external subject by fanning out independent searches, reading sources directly, and separating what was verified from what was only claimed. Load before orchestrating reconnaissance across many files, subsystems, or external sources. Skip for a single lookup you can perform directly.\n---\n\n# Codebase Research\n\nReconnaissance whose product is **evidence, not a summary**. The run's value comes from covering angles a single reader would miss and from being explicit about what it failed to establish.\n\nExecuting this skeleton \u2014 the surface it runs on, the wiring between stages, and model and effort assignment \u2014 belongs to `workflow`; this skill owns the shape of the run.\n\n## When Not To Use\n\n- A fact one grep or one file read settles. Fanning out costs more than the answer is worth.\n- Work that will change files. Use `implementation-run`.\n- Judging code that already exists against a standard. Use `quality-review`.\n\n## Stage Skeleton\n\n| Stage | Role | Fan | Returns |\n|---|---|---|---|\n| Scope | decompose | 1 | 3-6 angles, each a distinct search strategy \u2014 not paraphrases of one query |\n| Sweep | scan | one per angle | Located candidates with a path or URL and why each is relevant |\n| Read | extract | one per surviving candidate | Claims, each with a verbatim quote and its exact source |\n| Reconcile | synthesize | 1 | Merged findings, ranked, with contradictions kept visible |\n\nRun Sweep and Read as a pipeline. A barrier between them buys nothing: each candidate can be read the moment its angle finds it. Insert a barrier only before Reconcile, which genuinely needs the whole set.\n\n## Rules\n\n- **Angles must differ in method, not wording.** By-name, by-caller, by-test, by-history, by-config are different angles. Three rephrasings of one query is one angle run three times.\n- **A claim without a quote is a lead, not a finding.** Require the source and the literal text; report the count of leads that never became findings.\n- **Deduplicate before reading, not after.** Deduplicate on a normalized identity (path, or host plus path for a URL) so the same source is not read once per angle.\n- **Contradictions survive to the report.** When two sources disagree, say so and name both. Collapsing them into whichever sounds more confident destroys the run's most valuable output.\n- **Name what you failed to reach.** Blocked networks, unreadable files, and truncated searches are results. A report that omits them reads as exhaustive when it is not.\n\n## Stopping\n\nStop when a full sweep round adds no source you had not already read. Do not keep spawning searchers because the subject is large \u2014 spawn them because the last round found something new.\n\n## Gotchas\n\n- **Symptom:** The report is confident and short, and every finding traces to one or two sources.\n **Action:** Check whether the angles actually differed. Re-run with methods, not phrasings.\n **Why:** Similar queries return the same top results, so the fan-out produced redundancy that reads as corroboration.\n\n- **Symptom:** A cited file path or symbol does not exist.\n **Action:** Treat the whole finding as unverified and re-read the source before keeping it.\n **Why:** A stage that could not reach a source may still produce a plausible path; requiring a verbatim quote is what makes this detectable.\n" },
44101
44281
  { relativePath: "gateway/implementation-run/SKILL.md", content: '---\nname: implementation-run\ndescription: Apply one decided change across many files, packages, or call sites by discovering the sites, transforming each in isolation, and inspecting the artifacts rather than the reports. Load before a migration, a sweeping refactor, or a multi-package edit. Skip when the change fits in a few files you will edit directly, or when the approach is not yet decided.\n---\n\n# Implementation Run\n\nThe only stage shape here that **writes**. Its risk is not failure \u2014 a failed edit is visible \u2014 but convergence: many branches each producing something reasonable that together do not match the codebase.\n\nExecuting this skeleton \u2014 the surface it runs on, the wiring between stages, and model and effort assignment \u2014 belongs to `workflow`; this skill owns the shape of the run.\n\n## When Not To Use\n\n- The approach is undecided. Decide first with `architecture-review`; a stage handed an open decision will close it for you, differently in each branch.\n- A handful of files you can edit directly. The per-stage overhead exceeds the work.\n- Judging existing code. Use `quality-review`.\n\n## Stage Skeleton\n\n| Stage | Role | Fan | Returns |\n|---|---|---|---|\n| Discover | map | 1-3 | Every site that must change, each with a path and why it qualifies |\n| **Decide** | \u2014 | **host only** | The literal values every site will use. Decided here, never in a stage. |\n| Apply | implement | one per site or coherent group, `isolation: \'worktree\'` | Files changed, and which existing conventions were matched |\n| Inspect | verify | host reads the diff | Accept or reject per site |\n\nDiscover and Apply pipeline naturally, but **Decide is a barrier by necessity** \u2014 the literals must exist before any site is touched, or each branch invents its own.\n\n## Decisions Travel as Literals\n\nBefore starting any branch, close every judgment gap. Ask both:\n\n1. Must the stage choose a concrete value?\n2. Does it lack the doctrine or convention context to justify that choice?\n\nIf both are yes, **the host chooses the value and passes it verbatim**. This covers design tokens, API paths, setting keys, protocol tokens, names, error message text, thresholds, and constants \u2014 not an exhaustive list.\n\nNever leave a choice to a stage behind phrases like "match the existing style", "pick a consistent name", "follow the convention", or "\uC801\uC808\uD788". A stage on another model has no feel for this repository and will produce something defensible but foreign.\n\n## Rules\n\n- **Isolate every writing branch.** Parallel edits to a shared tree corrupt each other. Worktree isolation costs setup time and disk; pay it whenever more than one branch writes.\n- **Inspect artifacts, never narratives.** Read the actual diff for each site. A stage\'s summary of what it did is evidence of what it believed, not of what it wrote.\n- **Verbatim match or defect.** A literal you sent must appear exactly. An equivalent-looking substitution \u2014 a synonym token, a reformatted path, a renamed key \u2014 is a defect, not a variation.\n- **A site that needs a new decision stops.** When Apply discovers a case Decide did not cover, it returns that fact instead of choosing. Resolve it on the host and start that branch again with the value; do not let one branch set precedent for the rest.\n- **Reject rather than patch.** A branch whose output drifted is re-run with a sharper prompt. Fixing its output by hand hides that the prompt was insufficient, and the next site will drift the same way.\n\n## Scope Warning\n\nMeasurement covered only **local, well-precedented edits** \u2014 a couple of files with an obvious existing pattern to follow. Every model tested handled those correctly. Nothing establishes that this holds for sweeping or cross-package work, where convention drift compounds and each branch sees only its own slice. Treat wide runs as unproven: keep groups small, inspect every diff, and keep a structural change on the host rather than spreading it across branches that each see one slice.\n\n## Stopping\n\nStop when every discovered site is either accepted or explicitly deferred with a reason. Do not accept a run with unexamined sites because the count is large \u2014 an unexamined site is an unknown edit.\n\n## Gotchas\n\n- **Symptom:** Tests pass and the build is green, but the change reads as foreign to the surrounding code.\n **Action:** Diff the produced values against the literals you sent. Re-run the drifted sites with the literal spelled out.\n **Why:** Green checks confirm the code runs, not that it belongs; convention is invisible to a compiler.\n\n- **Symptom:** Different sites solved the same sub-problem differently.\n **Action:** That sub-problem belonged in Decide. Choose once on the host and re-run the affected sites with the value.\n **Why:** Each branch resolved an open decision independently, which is exactly what the Decide barrier exists to prevent.\n\n- **Symptom:** A branch reports success but changed nothing.\n **Action:** Check the returned file list against the actual diff before accepting.\n **Why:** A branch that could not find its target may report the intent as done; only the artifact settles it.\n' },
44102
44282
  { relativePath: "gateway/quality-review/SKILL.md", content: "---\nname: quality-review\ndescription: Review existing code or a change set by splitting the work into independent dimensions, hunting within each, then adversarially verifying every finding before it is reported. Load before a correctness, security, or quality pass over a diff or subsystem. Skip when you already know the defect and only need it fixed.\n---\n\n# Quality Review\n\nThe output is a **judged finding list, not a fix list**. A reviewer that also repairs what it finds loses the independence that made the finding worth having, and repairs things that were never broken.\n\nExecuting this skeleton \u2014 the surface it runs on, the wiring between stages, and model and effort assignment \u2014 belongs to `workflow`; this skill owns the shape of the run.\n\n## When Not To Use\n\n- The defect is known and only the repair remains. Use `implementation-run`.\n- Deciding between designs. Use `architecture-review`.\n- Establishing facts with no standard to judge against. Use `codebase-research`.\n\n## Stage Skeleton\n\n| Stage | Role | Fan | Returns |\n|---|---|---|---|\n| Split | decompose | 1 | The dimensions this review will cover, each with its own standard |\n| Hunt | scan | one per dimension | Candidate findings, each with a file, a line, and a concrete failing scenario |\n| Verify | verify | 2-3 per finding, mixed lineage, prompted to refute | Refuted or survived, with the specific evidence |\n| Adjudicate | \u2014 | **host only** | Confirmed / declined / deferred, with the reason |\n\nPipeline Hunt into Verify \u2014 a dimension's findings can be verified while another dimension is still hunting. Nothing here needs a global barrier.\n\n## Dimensions Stay Separate\n\nNever combine security auditing with functional or end-to-end review in one hunt. Measured outcome: the combined run drops the functional pass \u2014 security findings are more legible, so the agent spends its budget there and reports the run as complete. Give each dimension its own hunter with its own standard.\n\nTypical dimensions, chosen per target rather than run wholesale: correctness, security and input trust, boundary and ownership rules, error and failure handling, test coverage, and convention conformance.\n\n## Verify Is Adversarial\n\nVerifiers are prompted to **refute**, not to confirm. A finding survives only when the refutation attempt fails.\n\n- Default to refuted when uncertain. An unreproduced finding is a hypothesis.\n- Require a concrete failing scenario: inputs or state, and the wrong result. \"This could break\" is not a finding.\n- Distinguish three outcomes. Survived, refuted on merit, and **unverifiable because the verifier errored** are different; collapsing the third into \"refuted\" silently discards real findings when infrastructure fails.\n- Mix lineage across a finding's verifiers. Identical models produce correlated verdicts, which reads as agreement.\n\n## Adjudication Stays on the Host\n\nA surviving finding is evidence, not an instruction. For each one the host decides:\n\n- **Confirm** when it occurs on a path a real workflow reaches, is in scope, and the repair costs less than the defect.\n- **Decline** when it is hypothetical, overfit to the reviewer's reading, outside scope, or contradicts an intended trade-off. Record the reason; a silent skip is indistinguishable from an oversight.\n- **Defer** when it is real but belongs to different work. Say why it is real and why not here.\n\nSeverity never decides disposition. A reviewer's P1 on a path nothing reaches is still a decline.\n\n## Stopping\n\nStop when a hunting round produces no finding that survives verification. Two consecutive dry rounds end the run. A reviewer can always generate another suggestion, so waiting for it to fall silent is an unbounded loop.\n\n## Gotchas\n\n- **Symptom:** The run reports many findings and all of them survived.\n **Action:** Check that verifiers were prompted to refute rather than to assess. A confirming verifier confirms.\n **Why:** Adversarial framing is the entire mechanism; without it the verify stage is a second opinion that agrees by default.\n\n- **Symptom:** Fixing one finding produced the next round's findings.\n **Action:** Roll back the fix rather than widening it. That is evidence the repair was over-scoped.\n **Why:** A repair that breeds findings changed more than the defect required.\n\n- **Symptom:** The security dimension is thorough and the functional one is a sentence.\n **Action:** Re-run the functional dimension on its own hunter.\n **Why:** Combined dimensions do not split budget evenly; the more legible one absorbs it.\n" },
44103
- { relativePath: "gateway/workflow/SKILL.md", content: "---\nname: workflow\ndescription: Choose the surface a handoff runs on and pin the identity it runs as, then wire a staged run's stages to each other and keep its failures visible. Load before any run leaves the host \u2014 one Agent, a named teammate, or a staged workflow \u2014 and before executing a stage skeleton from architecture-review, codebase-research, implementation-run, or quality-review. Skip only when the work stays on the host.\n---\n\n# Workflow\n\nThe other gateway skills each own the *shape* of a run \u2014 which stages exist, what each returns, where the judgment stays on the host. This skill owns **turning that shape into an actual run**: the surface it executes on, the identity it runs as, how stages are wired to each other, and what each stage runs on.\n\nTwo gates open before anything leaves the host, in order. Neither decides *whether* to hand work off \u2014 that is the Orchestration Policy Standing Order's Proportionality call, already made before you arrive here. Work that belongs on the host stays on the host, and nothing in this skill is a reason to create a run you would not otherwise have made. Equally, avoiding these gates is not a reason to absorb a run you would have made.\n\n## Gate 1 \u2014 Execution Surface\n\nThree surfaces, and they are not interchangeable.\n\n- **One Agent** \u2014 a single run whose result comes back whole. This is the default. Work whose parts need no wiring between them belongs here.\n- **A named teammate** \u2014 an Agent you can address again later with its context intact. Reach for it when the same worker must carry several exchanges rather than one.\n- **The staged workflow surface** \u2014 a script of stages wired to each other, with data flowing between them, barriers, fan-out, and a fleet of different models working the same problem at once. That wiring is the only thing it buys, and it is what the user asks for on top of the default.\n\nReach past the default only when the wiring is the point. A skeleton that is never executed as stages is not a cheaper version of the run \u2014 it is a single reader doing every job in one context, which is the failure mode the skeleton exists to prevent. The inverse is equally wrong: a staged run for work that needed one Agent pays the coordination cost and collects none of it back.\n\n**A surface gated behind user opt-in is unavailable until that opt-in exists.** The staged workflow surface refuses to run unless the user explicitly asked for a multi-model run \u2014 as of this writing by naming `ultracode`, or through a standing session-level opt-in the harness confirms. That trigger belongs to the harness, not to Fleet: read the live tool description for what it accepts now, and treat the keyword named here as the last thing anyone checked rather than as the contract. It is a session opt-in and never a reasoning-effort rung \u2014 requesting it as one is off-ladder and clamped upstream without a signal.\n\nWhen the gate is closed, that refusal is not a defect. Report the gate, say what the staged run would cost and what it would buy, and wait \u2014 the same way you would report any unavailable surface. Do not quietly do the work yourself in one context instead.\n\nInspect the live tool surface before concluding anything about any of the three; tools may be lazy-loaded.\n\n**Call mechanics stay out of this skill on purpose.** Argument names, script syntax, return shapes, and which values a field accepts live in the live tool description. Read them there every time. Anything restated here would be a copy that goes stale silently.\n\n## Gate 2 \u2014 Model Pin Gate\n\nEvery run that leaves the host carries a pinned identity. An unpinned run is not the neutral choice: it inherits the session's own model and spends the session's own allowance, reached by omission rather than by selection. Closing that omission is this gate's whole job.\n\n**Call `gateway_models` first, every time.** Not once per session: allowances move while work is in flight, and a roster entry can be enabled or disabled between two runs. A gate cleared against a remembered roster is not cleared.\n\nRead the response on two axes and never collapse them.\n\n- **Lineage** \u2014 whose blind spots an identity inherits. `homolineage: true` marks a Claude-family model, derived from the model id alone and silent about what this session runs on; it is a \"same as me\" flag only when this session is itself Claude-family. This axis decides independence, never cost.\n- **Allowance** \u2014 whose subscription a run bills to. Every model sits under the provider entry it spends. This axis decides cost, never independence.\n\nThe two come apart. An identity can carry Claude lineage while billing to another provider's subscription, and that combination is a legitimate way to move spend. The rule below binds the allowance axis only.\n\n**The parent session's own allowance is the last one to spend, not the first.** Identify which allowance that is before applying the rule, because the roster cannot tell you: it reports what this session exposes, never what this session itself runs on. Read your own model id and find the provider that bills it.\n\nTwo cases, and they differ in what you can do about it, not in what you can see \u2014 every provider's allowance is reported, the parent subscription included. A session running a built-in Claude model spends the `claude` entry, which reports its window like any other but serves no roster model by design, so it can never be selected, only inherited: read its pressure to know where you stand, and spare it by pinning away from it rather than by choosing it. A session launched on a gateway default spends an entry that both reports *and* serves; there the failure is recursion \u2014 routing more runs to the entry already carrying this session drains one allowance twice while the rest sit idle. `isSessionDefault` does not settle which case you are in: it reflects Settings as they stand now, not what an already-running session launched with.\n\nPrefer any other provider with room: whatever this session runs on is the most expensive way to obtain what any identity produces equally well.\n\nThree exceptions, and only these three. Each is recorded by its label in the split record.\n\n- **E1 \u2014 cross-lineage verification.** All three must hold: the role is `verify`, `judge`, or `adjudicate`; disagreement is that stage's actual product; and the lineage this run would inherit differs from the subject's. That last one is a check, never an assumption \u2014 an unpinned run takes whatever this session was launched on, and a session launched on a non-Claude gateway default inherits *that* lineage, which can be the subject's own. Reading `homolineage` off the roster does not answer it either: the flag describes a model, not this session. When the condition does hold, the voice you already are is the independent one and pinning elsewhere to obtain the same lineage is ceremony. Cap it there \u2014 one lineage must not hold a majority of the quorum, or the independence it was admitted for is gone.\n- **E2 \u2014 last resort.** Every candidate identity's own window reads `critical`, or runs against them keep returning empty after a retry. A provider whose allowance could not be read is **not** evidence of exhaustion \u2014 absence is never safety \u2014 so it can neither open this exception nor close it. When E2 opens, run one alternative identity alongside and compare the two results; a last resort nobody checked is an unpinned run with a label on it.\n- **E3 \u2014 empty roster.** No model is exposed at all, so there is nothing to pin to.\n\nWhen none of the three applies, the session's own model is out and the choice falls to the procedure below. `roleFit: null` is not a fourth exception: unmeasured means quality gave no reason to prefer anyone, so the choice falls to allowance and never back to this session's model.\n\n## Reading a Stage Skeleton\n\nEvery gateway skeleton is a table of `Stage | Role | Fan | Returns`. Read it as an execution plan:\n\n- **Role** is the one-word job \u2014 map, propose, implement, verify, synthesize, transform. It is also the input to model assignment below.\n- **Fan** is how many parallel branches that stage runs. `1` is one branch. `one per <item>` is a fan-out sized by the previous stage's output, not by a number you pick. **`host only` is not a stage you hand off** \u2014 it is a barrier where you do the work yourself, and handing it off defeats the skeleton.\n- **Returns** is the contract. When a stage returns structured data, declare the schema rather than parsing prose; a stage that must fill a shape will retry against it, while a stage asked to write prose will improvise.\n\n## Pipeline by Default\n\nBetween two stages, the choice is pipeline or barrier, and **pipeline is the default**.\n\nA barrier \u2014 waiting for every branch of stage N before starting stage N+1 \u2014 is correct only when stage N+1 genuinely needs the whole set at once: deduplicating across all results before expensive downstream work, deciding literals every later branch must share, early-exit when the total is zero, or a prompt that compares one result against the others.\n\nA barrier is **not** justified by needing to flatten, map, or filter between stages \u2014 do that inside a stage \u2014 nor by the stages feeling conceptually separate, nor by the script reading more cleanly. Each unjustified barrier costs the difference between the slowest branch and the fastest, on every item, for nothing.\n\nEach skill's skeleton already names its own barriers, and they are the load-bearing part of that shape. `implementation-run`'s Decide barrier and `quality-review`'s Adjudicate barrier are the two places the run stops being parallel because a single decision must exist before anything downstream. Do not optimize them away.\n\n## Failures Must Be Loud\n\nFan-out helpers routinely turn a failed branch into an empty result rather than an error. A run that lost three of eight branches then looks like a run that found less, which is indistinguishable from a thorough run over a quiet subject.\n\n- Have each stage **return its failure as a value** \u2014 a result that says it failed and why \u2014 instead of throwing into the helper.\n- Before synthesizing, check the branch count against what you started. A missing branch is a finding.\n- Never report coverage you did not verify. If the run capped, sampled, or dropped anything, say so in the report; silent truncation reads as completeness.\n\n## Model and Effort Assignment\n\nDistribution is the default. Concentrating a run on the model this session happens to run on is the exception, and the exception carries the burden of proof \u2014 Gate 2 above is where that burden is discharged, and it names the only three forms the proof can take. What follows is how the remaining choice is made once the gate is clear.\n\nWork through these in order.\n\n1. **Name the role.** Take it from the skeleton's Role column. If you cannot name it in one word, the stage boundary is wrong; fix the split before choosing a model.\n2. **Name the dominant risk.** What would ruin *this* stage: too little context, unreliable tool use, correlated judgment, drift from repository convention, or incomplete coverage. One risk, not a list.\n3. **Look for a measured fit.** Read `roleFit` for that risk. A declared `fit` is a reason to prefer an identity and a declared `unfit` a reason to avoid it. `null` means unmeasured: it says nothing about quality, and it is never a reason to fall back to the session model.\n4. **Spread the rest by allowance.** For every stage with no measured fit, choose by cost. Read the window that belongs to the model \u2014 the one whose `scope` matches `constraints.quotaScope` when the model declares one, and the provider's scope-less window when it does not \u2014 and let the roster's own verdict lead: prefer windows at `pressure: \"ok\"`, treat `\"elevated\"` as a reason to route elsewhere, and send nothing to `\"critical\"` unless every alternative is worse. Break a tie between windows that share a `cadence` by the lower `usedPercent`, and never compare percentages across cadences \u2014 a weekly window at 49% early in its week burns hotter than a monthly one at 78% near its reset, and `paceRatio` above 1.0 says so directly. On an older reading that carries none of the derived fields, treat percentages as comparable only within a single provider's windows \u2014 a shared id like `cycle` does not mean a shared length \u2014 and across providers trust only the extreme: a window near 100 is spent whatever its clock. A scope is declared only where one subscription splits into pools; there the scope-less figure is marked `isAggregate` \u2014 a sum that can read healthy while the model's own pool is spent, and one that stays out of headroom math. Move off a provider as its windows go elevated instead of discovering exhaustion mid-run.\n5. **Re-pick effort for the model you chose.** Ladders differ between identities. A level a model does not advertise is clamped down to the next rung below it with no signal to you, and rejected outright when nothing is below. Take a rung the target's `effortLadder` actually lists \u2014 the user can expose a model at fewer levels than it supports, and the ladder reports what this session registered, not the catalog \u2014 and check the stage's input against the target's `contextWindow`.\n6. **Diversify where disagreement is the product.** A majority-vote or judging stage wants different lineages \u2014 a verifier sharing its subject's lineage inherits the same blind spots. Judge that against the **subject**, not against this session: a Claude-family identity billed to another provider is useful for moving spend, useless for independence from a Claude-family session, and silent about independence from a subject that ran elsewhere. An unpinned stage has no lineage of its own \u2014 it takes whatever this session was launched on \u2014 so its independence from the subject is knowable only once you have read your own model id, which is what Gate 2's E1 makes you check.\n7. **Confirm the name exists on both sides.** Roster membership resolves live, but Agent names were fixed when the session started. Pick only a name present in both; a model enabled mid-session is unreachable until restart. `400 unknown model` means re-read the roster.\n8. **Do not choose the load-bearing stage by allowance alone.** When everything downstream rests on one stage \u2014 the contract survey, the final synthesis, the integrating judgment \u2014 let measured fit and lineage independence decide it, and let cost break ties only after those.\n9. **Record the split.** One line per run: which identities carried which stages, what decided it, and the `E1` / `E2` / `E3` label wherever the session's own model carried one. A distribution nobody can audit is indistinguishable from a random one, and an exception nobody labelled is indistinguishable from a lapse.\n\n### What Measurement Actually Showed\n\nTwo measurements, both on 2026-08-02.\n\nThree models against seven stage roles were **indistinguishable on five of them**: structured output, repository search, adversarial judgment, mechanical transformation, and a small implementation task.\n\nTwelve identities were then given one identical mapping task \u2014 twelve files, exact line counts, exact export symbols. **All twelve answered it perfectly**: full coverage, no fabricated file, and every one caught the trap entry whose correct answer was an empty list. What separated them was spend. The cheapest finished on 176k total tokens over 5 tool calls; the most expensive spent 5.20M over 29 for the same answer. Output tokens alone ran 1.7k to 20.3k, so this is not a cache-read artifact.\n\nRead the two together. Quality parity is the prior \u2014 and parity is exactly what makes cost the deciding axis. **Indistinguishable never meant \"inherit\"; it means the expensive choice buys nothing.** The roster declares fit only where a measurement separated the models, and reports `null` everywhere else \u2014 `null` means unmeasured, never unsuitable.\n\n### Rules That Measurement Refuted\n\n- **A larger context window does not mean better reading.** Asked to map a 22-file subsystem, the 1M-window model opened 16 files and a 372k-window model opened all 22. What separated them was thoroughness in tool use, which no catalog field predicts. Use the window as a floor \u2014 can this model hold the input at all \u2014 not as a ranking.\n- **Raising effort does not reliably improve judgment.** The same verification task at the lowest and highest rungs produced the same verdict with equal reasoning quality. Effort pays only once a task is hard enough to need it; raising it by habit buys nothing and costs throughput.\n- **A local, well-precedented edit does not need the session model.** Every model tested landed the change in the right files, found the package's existing export pattern instead of inventing one, and matched the surrounding comment language. This does **not** generalize to sweeping or multi-package work, where convention drift compounds and goes unseen.\n\n### Handing Work to a Different Model\n\nA stage running on another model has no feel for this repository's conventions, so decisions must travel as literal values, not as descriptions. Name the exact token, path, setting key, or constant; never write \"match the existing style\" or \"pick something consistent\". On return, check the artifacts against the literals you sent \u2014 an equivalent-looking substitution is a defect, not a variation.\n\n## Gotchas\n\n- **Symptom:** A run left the host on the session's own model and nothing in the report says why.\n **Action:** Treat it as a gate that never opened rather than as a choice. Re-read Gate 2, name the exception that applied, and if none did, repeat the run pinned.\n **Why:** The session's allowance is reached by omission rather than by selection, so this failure leaves no trace of its own \u2014 an unlabelled inheritance and a deliberate `E1` look identical afterwards.\n\n- **Symptom:** A run that pinned several models produced uniform-looking results, or one stage's output is missing with no error.\n **Action:** Check whether that branch failed rather than ran. Confirm each pinned id is still in the roster and return branch failures as values instead of letting the helper collapse them.\n **Why:** A de-selected or mistyped id fails at the gateway, but the fan-out helper turns a failed branch into an empty slot, so a heterogeneous run silently becomes a partial one.\n\n- **Symptom:** A stage ran at a different reasoning level than the one requested.\n **Action:** Read that model's ladder from the roster and request a level it actually advertises.\n **Why:** Ladders are not uniform \u2014 some models have no `medium`, others no effort control at all \u2014 and an off-ladder level is clamped upstream without any signal.\n\n- **Symptom:** A provider looked like it had room, but its requests began failing.\n **Action:** Read the window whose `scope` matches the model's `quotaScope`, not the provider's combined figure.\n **Why:** One subscription can bill through separate pools; the sum can read comfortable while the pool a given model draws from is nearly spent.\n\n- **Symptom:** A stage returned nothing at all \u2014 no result, no error you can quote \u2014 while other stages on the same provider succeeded.\n **Action:** Treat a `\"critical\"` pressure \u2014 or a `usedPercent` near 100 \u2014 on that model's own window as the explanation and move those stages to another provider. Do not wait for a message that says exhausted.\n **Why:** There is no exhaustion status. `status` distinguishes *reading* failures \u2014 not connected, signed out, expired, no subscription, stale, error \u2014 and a spent pool is visible only in its own window's figures. A stage dying after retries with an empty return is what exhaustion actually looks like from here.\n\n- **Symptom:** A model you just enabled is in `gateway_models` but every attempt to run a stage on it fails as an unknown Agent.\n **Action:** Use only names present in both the live roster and the Agent names this session started with. Reaching a newly enabled model requires a new session.\n **Why:** The roster re-reads the user's selection on every call, but Agent names were serialized once at session start. The two drift apart the moment settings change mid-session.\n\n- **Symptom:** The run took as long as doing it yourself, with the same total cost.\n **Action:** Count the barriers. Each one that no stage actually needed becomes wall-clock spent waiting for the slowest branch.\n **Why:** Staging buys overlap; a skeleton executed as a sequence of barriers pays the coordination cost and collects none of it back.\n\n- **Symptom:** A stage came back asking what to do, or made a choice the skeleton reserved for the host.\n **Action:** Move that decision to the preceding host-only barrier and run the stage again with the value spelled out.\n **Why:** A stage handed an open decision always closes it, differently in each branch \u2014 which is the failure the barrier was placed there to prevent.\n" },
44283
+ { relativePath: "gateway/workflow/SKILL.md", content: "---\nname: workflow\ndescription: Choose the surface a handoff runs on and pin the identity it runs as, then wire a staged run's stages to each other and keep its failures visible. Load before any run leaves the host \u2014 one Agent, a named teammate, or a staged workflow \u2014 and before executing a stage skeleton from architecture-review, codebase-research, implementation-run, or quality-review. Skip only when the work stays on the host.\n---\n\n# Workflow\n\nThe other gateway skills own the *shape* of a run. This skill turns that shape into an actual run: the surface it executes on, the identity it runs as, and how its stages are wired.\n\nTwo gates open before anything leaves the host, in order. Neither decides *whether* to hand work off \u2014 Proportionality already did. Nothing here is a reason to create a run you would not otherwise have made, and avoiding these gates is not a reason to absorb a run you would have made.\n\n## Gate 1 \u2014 Execution Surface\n\nThree surfaces, and they are not interchangeable.\n\n| Surface | What it buys | Reach for it when |\n|---|---|---|\n| **One Agent** | one result, returned whole | **the default** \u2014 parts need no wiring between them |\n| **A named teammate** | an Agent addressable again with its context intact | one worker must carry several exchanges |\n| **The staged workflow surface** | wiring: data between stages, barriers, fan-out, and a fleet of different models working the same problem at once | that wiring is the point |\n\n- **Wiring is the only thing the staged surface buys.** A skeleton never executed as stages is one reader doing every job in one context \u2014 the failure the skeleton exists to prevent. A staged run for work that needed one Agent pays the coordination cost and collects none of it back.\n- **A surface gated behind user opt-in is unavailable until that opt-in exists.** As of this writing the staged surface wants `ultracode` or a standing session opt-in. That trigger belongs to the harness, not to Fleet \u2014 read the live tool description for what it accepts now. It is a session opt-in and never a reasoning-effort rung; requesting it as one is clamped upstream without a signal.\n- **A closed gate is not a defect.** Report the gate, say what the staged run would cost and buy, and wait. Do not quietly do the work yourself in one context instead.\n- **Call mechanics stay out of this skill on purpose.** Argument names, script syntax, and accepted values live in the live tool description \u2014 read them there every time, and inspect the live surface before concluding anything, since tools may be lazy-loaded.\n\n## Gate 2 \u2014 Model Pin Gate\n\nEvery run that leaves the host carries a pinned identity. **An unpinned run is not the neutral choice** \u2014 it inherits the session's own model and spends the session's own allowance, reached by omission rather than by selection.\n\n**Call `gateway_models` first, every time.** Not once per session: allowances move while work is in flight, and a gate cleared against a remembered roster is not cleared.\n\n### Two axes, never collapsed\n\n| Axis | What it reads | What it decides |\n|---|---|---|\n| **Lineage** | `homolineage: true` marks a Claude-family model, derived from the model id alone and silent about what this session runs on | This axis decides independence, never cost. |\n| **Allowance** | the provider entry a model sits under \u2014 whose subscription the run bills to | This axis decides cost, never independence. |\n\nThey come apart: an identity can carry Claude lineage while billing elsewhere, which is a legitimate way to move spend. The rule below binds the allowance axis only.\n\n### The session's own allowance is the last one to spend\n\nIdentify which allowance that is first, because the roster cannot tell you \u2014 it reports what this session exposes, never what this session itself runs on. Read your own model id and find the provider that bills it. Both cases below are visible; every provider's allowance is reported, the parent subscription included. What differs is what you can do about it.\n\n| This session runs on | The failure to avoid |\n|---|---|\n| a built-in Claude model | It spends the `claude` entry, which reports a window but serves no roster model, so **it can never be selected, only inherited** \u2014 spare it by pinning away, not by choosing it. |\n| a gateway default | **A session launched on a gateway default spends an entry that both reports *and* serves**, so routing more runs there **drains one allowance twice** while the rest sit idle. |\n\n`isSessionDefault` does not settle which case you are in: it reflects Settings as they stand now, not what an already-running session launched with. Prefer any other provider with room \u2014 whatever this session runs on is the most expensive way to obtain what any identity produces equally well.\n\n### Three exceptions, and only these three.\n\nEach is recorded by its label in the split record.\n\n- **E1 \u2014 cross-lineage verification.** All three must hold: the role is `verify`, `judge`, or `adjudicate`; disagreement is that stage's actual product; and the lineage this run would inherit differs from the subject's. That last one is a check, never an assumption \u2014 an unpinned run takes whatever this session launched on, and the flag describes a model, not this session. **Cap the session's lineage at one verifier seat per verify stage**, fixed by the stage's need before you read the roster. Among the *other* lineages one lineage must not hold a majority of the quorum; when too few remain, shrink the quorum rather than add session-lineage seats. The seat is a verification exception, not a scarcity response.\n- **E2 \u2014 last resort.** Every candidate's own window reads `critical`, or runs keep returning empty after a retry. An allowance that could not be read is **not** evidence of exhaustion, so it can neither open this exception nor close it. When E2 opens, run one alternative identity alongside and compare \u2014 a last resort nobody checked is an unpinned run with a label on it.\n- **E3 \u2014 empty roster.** No model is exposed at all.\n\n`roleFit: null` is not a fourth exception: unmeasured means quality gave no reason to prefer anyone, so the choice falls to allowance and never back to this session's model.\n\n## Reading a Stage Skeleton\n\nEvery gateway skeleton is a table of `Stage | Role | Fan | Returns`.\n\n- **Role** \u2014 the one-word job: map, propose, implement, verify, synthesize, transform. It is the input to model assignment.\n- **Fan** \u2014 parallel branches. `one per <item>` is sized by the previous stage's output, not by a number you pick. **`host only` is not a stage you hand off** \u2014 it is a barrier where you do the work yourself.\n- **Returns** \u2014 the contract. Declare a schema rather than parsing prose: a stage that must fill a shape retries against it, while a stage asked for prose improvises.\n\n## Pipeline by Default\n\n**Pipeline unless stage N+1 genuinely needs the whole set at once** \u2014 deduplicating before expensive downstream work, deciding literals every branch shares, early-exit on zero, or comparing one result against the others.\n\nA barrier is **not** justified by needing to flatten, map, or filter between stages (do that inside a stage), by stages feeling conceptually separate, or by the script reading cleaner. Each unjustified barrier costs the gap between slowest and fastest branch, on every item, for nothing. The barriers a skeleton already names \u2014 `implementation-run`'s Decide, `quality-review`'s Adjudicate \u2014 are load-bearing; do not optimize them away.\n\n## Failures Must Be Loud\n\nA fan-out helper turns a failed branch into an empty result, so a run that lost three of eight branches reads as a thorough run over a quiet subject.\n\n- Have each stage **return its failure as a value**, not throw into the helper.\n- Check the branch count against what you started before synthesizing. A missing branch is a finding.\n- Never report coverage you did not verify. Say so when the run capped, sampled, or dropped anything.\n\n## Model and Effort Assignment\n\nDistribution is the default. The session's own allowance is the last one to spend, and its first-priority use is orchestration on the host itself, never bulk fan-out. Concentrating a run on this session's model is the exception, and the exception carries the burden of proof \u2014 Gate 2 above is where that burden is discharged.\n\n1. **Name the role.** Take it from the Role column. If you cannot name it in one word, fix the stage split first.\n2. **Name the dominant risk.** One risk, not a list: too little context, unreliable tool use, correlated judgment, convention drift, or incomplete coverage.\n3. **Look for a measured fit.** Read `roleFit` for that risk. `fit` is a reason to prefer, `unfit` a reason to avoid, `null` says nothing about quality and never sends you back to the session model.\n4. **Spread the rest by allowance**, using the two subsections below.\n5. **Re-pick effort for the model you chose.** A level a model does not advertise is clamped down with no signal and refused when nothing is below. Take a rung the target's `effortLadder` actually lists \u2014 it reports what this session registered, not the catalog \u2014 and check the stage's input against its `contextWindow`.\n6. **Diversify where disagreement is the product.** A verifier sharing its subject's lineage inherits the same blind spots. Judge that against the **subject**, not against this session: a Claude-family identity billed elsewhere is useful for moving spend, useless for independence from a Claude-family session, and silent about independence from a subject that ran elsewhere. An unpinned stage has no lineage of its own. Diversity sizes the quorum, never the bulk fan-out.\n7. **Confirm the name exists on both sides.** The roster resolves live; Agent names were fixed at session start. `400 unknown model` means re-read the roster. Reaching a newly enabled model requires a new session.\n8. **Do not choose the load-bearing stage by allowance alone.** For the contract survey, the final synthesis, or the integrating judgment, let measured fit and lineage independence decide, and let allowance break ties only after those.\n9. **Record the split.** Which identities carried which stages, what decided it, and the `E1` / `E2` / `E3` label wherever the session's model carried one. An unlabelled exception is indistinguishable from a lapse.\n\n### Reading an allowance\n\n- **Allowance is not efficiency.** `roleFit.tokenEfficiency` counts tokens and tool calls \u2014 a model's appetite, not a subscription's drain. The two are not proportional and quota is never inferred from them.\n- **Read the window that belongs to the model** \u2014 the one whose `scope` matches `constraints.quotaScope` when the model declares one, and the provider's scope-less window when it does not.\n- **The roster's verdict outranks arithmetic of your own.** Prefer `pressure: \"ok\"`, treat `\"elevated\"` as a reason to rebalance toward a lighter provider rather than a prohibition, and send nothing to `\"critical\"` unless every alternative is worse. A window the roster calls `ok` is usable at any percentage; re-deriving risk from `usedPercent` or `paceRatio` to overrule it is how a healthy provider gets abandoned \u2014 one payload can carry a 35% window marked `elevated` beside a 64% window marked `ok`.\n- **Percentages compare only within one clock.** Break a tie between windows that share a `cadence` by the lower `usedPercent`, and never compare percentages across cadences \u2014 a weekly window at 49% early in its week burns hotter than a monthly one at 78% near its reset, and `paceRatio` above 1.0 says so directly.\n- **On an older reading with no derived fields**, treat percentages as comparable only within a single provider's windows \u2014 a shared id like `cycle` does not mean a shared length \u2014 and across providers trust only the extreme: a window near 100 is spent whatever its clock.\n- **A scope is declared only where one subscription splits into pools.** There the scope-less figure is marked `isAggregate` \u2014 a sum that can read healthy while the model's own pool is spent, and one that stays out of headroom math.\n\n### Sizing a bulk fan-out\n\n- **The task sets the branch count and an allowance reading never trims it.** A window still called `ok` is not a reason to run fewer branches than the work needs.\n- **Split evenly across eligible non-session providers** \u2014 those whose applicable window is readable and not `critical` \u2014 no provider more than one branch above another.\n- **Count providers, not identities.** A provider exposing two models does not draw twice the share.\n- **A measured fit may pull one stage off the split**, but it sizes that stage only and never redistributes the rest.\n- **One eligible provider left carries the whole fan-out**, however high its `usedPercent` reads and whether its pressure is `ok` or `elevated`. A sole remaining provider is where \"rebalance off elevated\" stops applying, because the only place left to move is the session's own allowance.\n- **An unreadable allowance joins no even split** \u2014 absence is not headroom \u2014 but it is not exhausted either: give it a bounded share and promote it once runs return. When the even split comes out empty those bounded shares *are* the fan-out; an unreadable allowance never opens E2.\n\n## What Measurement Actually Showed\n\nTwo measurements, both on 2026-08-02.\n\n| Measurement | Result | What it means |\n|---|---|---|\n| Three models against seven stage roles | **indistinguishable on five of them** \u2014 structured output, repository search, adversarial judgment, mechanical transformation, a small implementation task | quality parity is the prior |\n| Twelve identities, one identical 12-file mapping task | **all twelve answered it perfectly**, trap entry included; cheapest **176k total tokens over 5 tool calls**, dearest **5.20M over 29**; output alone 1.7k\u201320.3k, so **not a cache-read artifact** | what separated them was measured efficiency, not provider quota |\n\nParity is exactly what makes measured token and tool-call efficiency the deciding axis when allowance pressure is equal. **Indistinguishable never meant \"inherit\"; it means the less efficient choice buys nothing.** Quota pressure remains a separate roster verdict, never inferred from these counts.\n\nThree rules the same measurements refuted:\n\n- **A larger context window does not mean better reading.** Mapping a 22-file subsystem, the 1M-window model opened 16 files and a 372k-window model opened all 22. Use the window as a floor, not a ranking.\n- **Raising effort does not reliably improve judgment.** The same verification task at the lowest and highest rungs produced the same verdict. Effort pays only once a task is hard enough to need it.\n- **A local, well-precedented edit does not need the session model.** Every model tested landed it in the right files and matched the surrounding conventions. This does **not** generalize to sweeping or multi-package work.\n\n## Handing Work to a Different Model\n\nDecisions must travel as literal values, not descriptions: name the exact token, path, setting key, or constant, and never write \"match the existing style\". On return, check the artifacts against the literals you sent \u2014 an equivalent-looking substitution is a defect, not a variation.\n\n## Gotchas\n\n- **Symptom:** A run left the host on the session's own model and nothing in the report says why.\n **Action:** Treat it as a gate that never opened rather than as a choice. Re-read Gate 2, name the exception that applied, and if none did, repeat the run pinned.\n **Why:** The session's allowance is reached by omission rather than by selection, so this failure leaves no trace of its own \u2014 an unlabelled inheritance and a deliberate `E1` look identical afterwards.\n\n- **Symptom:** A run that pinned several models produced uniform-looking results, or one stage's output is missing with no error.\n **Action:** Check whether that branch failed rather than ran. Confirm each pinned id is still in the roster and return branch failures as values instead of letting the helper collapse them.\n **Why:** A de-selected or mistyped id fails at the gateway, but the fan-out helper turns a failed branch into an empty slot, so a heterogeneous run silently becomes a partial one.\n\n- **Symptom:** A stage ran at a different reasoning level than the one requested.\n **Action:** Read that model's ladder from the roster and request a level it actually advertises.\n **Why:** Ladders are not uniform \u2014 some models have no `medium`, others no effort control at all \u2014 and an off-ladder level is clamped upstream without any signal.\n\n- **Symptom:** A provider looked like it had room, but its requests began failing.\n **Action:** Read the window whose `scope` matches the model's `quotaScope`, not the provider's combined figure.\n **Why:** One subscription can bill through separate pools; the sum can read comfortable while the pool a given model draws from is nearly spent.\n\n- **Symptom:** A stage returned nothing at all \u2014 no result, no error you can quote \u2014 while other stages on the same provider succeeded.\n **Action:** Treat a `\"critical\"` pressure \u2014 or a `usedPercent` near 100 \u2014 on that model's own window as the explanation and move those stages to another provider. Do not wait for a message that says exhausted.\n **Why:** There is no exhaustion status. `status` distinguishes *reading* failures \u2014 not connected, signed out, expired, no subscription, stale, error \u2014 and a spent pool is visible only in its own window's figures. A stage dying after retries with an empty return is what exhaustion actually looks like from here.\n\n- **Symptom:** A model you just enabled is in `gateway_models` but every attempt to run a stage on it fails as an unknown Agent.\n **Action:** Use only names present in both the live roster and the Agent names this session started with. Reaching a newly enabled model requires a new session.\n **Why:** The roster re-reads the user's selection on every call, but Agent names were serialized once at session start. The two drift apart the moment settings change mid-session.\n\n- **Symptom:** The run took as long as doing it yourself, with the same total cost.\n **Action:** Count the barriers. Each one that no stage actually needed becomes wall-clock spent waiting for the slowest branch.\n **Why:** Staging buys overlap; a skeleton executed as a sequence of barriers pays the coordination cost and collects none of it back.\n\n- **Symptom:** A stage came back asking what to do, or made a choice the skeleton reserved for the host.\n **Action:** Move that decision to the preceding host-only barrier and run the stage again with the value spelled out.\n **Why:** A stage handed an open decision always closes it, differently in each branch \u2014 which is the failure the barrier was placed there to prevent.\n" },
44104
44284
  { relativePath: "protocol-baseline/SKILL.md", content: "---\nname: protocol-baseline\ndescription: Use the compact Fleet protocol mode for simple, reversible, single-surface work.\n---\n\n# Fleet Protocol: Baseline\n\nUse this mode only for simple, reversible, single-surface operational work.\n\nAt any point during the work, if a Downward Guard trigger appears, stop and re-classify.\n\n## Checkpoints\n\nNone. Selecting baseline implies Mission Anchor Compact Mode.\n\n## Reporting Cadence\n\nAs you move through this protocol, report progress to the user in order.\n\n1. Brief in one line how the Procedure will proceed. \u2192 report `brief: <\u2026>`\n2. State that execution is beginning and run the Procedure. \u2192 report `status: executing`\n\n## General Quarters\n\nConfirm each readiness check below before the Procedure. Work through them in order and report each as you confirm it, then proceed to the Objective anchor. These checks prepare the work; they do not gate entry.\n\n- [ ] **Common** \u2014 objective stated (Mission Anchor), mode-fit holds (Mode Gate), Standing Orders binding. \u2192 report `common: ready`\n- [ ] **Single surface** \u2014 the exact file, command, or fact is identified. \u2192 report `surface: <x>`\n- [ ] **Reversibility** \u2014 the change is trivially reversible. \u2192 report `reversible: yes`\n\n## Procedure\n\n1. Objective statement: state the Mission Anchor objective in one line.\n2. Exact fact/file verification: verify the exact file, command, or fact needed for the request.\n3. Execution: make the smallest reversible change or run the exact requested command.\n4. Result verification: check the touched surface or command result.\n5. One-line report: report what changed, verification, and any skipped escalation trigger.\n" },
44105
44285
  { relativePath: "protocol-frontline/SKILL.md", content: "---\nname: protocol-frontline\ndescription: Use the coordinated Fleet protocol mode for multi-carrier or parallel ownership work.\n---\n\n# Fleet Protocol: Frontline\n\nUse this mode when operational work requires multiple Carriers, independent parallel workstreams, cross-carrier review loops, or file ownership coordination. If the work is high risk but single-owner, use `protocol-redline` instead.\n\n## Checkpoints\n\nDecomposition, Dispatch, Integration, Verification.\n\n## Reporting Cadence\n\nAs you move through this protocol, report progress to the user in order \u2014 each step on its own line with its report token.\n\n1. Brief how the Procedure will proceed \u2014 name (a) the Procedure steps that will run, (b) each carrier's file or responsibility ownership, and (c) the dispatch wave sequencing. \u2192 report `brief: <\u2026>`\n2. State that execution is beginning and run the Procedure. \u2192 report `status: executing`\n\n## General Quarters\n\nConfirm each readiness check below before the Procedure. Work through them in order and report each as you confirm it, then proceed to reconnaissance and decomposition. These checks prepare the work; they do not gate entry.\n\n- [ ] **Common** \u2014 objective stated (Mission Anchor), mode-fit holds (Mode Gate), Standing Orders binding. \u2192 report `common: ready`\n- [ ] **Impact radius** \u2014 flag public-surface or API impact, irreversibility, and any security-sensitive surface. \u2192 report `impact: <\u2026>`\n- [ ] **Rollback** \u2014 identify a rollback-safe checkpoint and any user approval point before execution begins. \u2192 report `rollback: <\u2026>`\n- [ ] **Carrier availability** \u2014 confirm the intended carriers are actually exposed and available this session. \u2192 report `carriers: <\u2026>`\n- [ ] **Ownership** \u2014 pre-sketch each carrier's file or responsibility boundary. \u2192 report `ownership: <\u2026>`\n- [ ] **Shared resources** \u2014 flag shared mutable resources (same files, lock files, or a singleton test environment). \u2192 report `shared: <\u2026|none>`\n- [ ] **Dependencies** \u2014 pre-classify parallel versus sequential work before decomposition and dispatch. \u2192 report `dependencies: <parallel|sequenced: \u2026>`\n\n## Procedure\n\n1. Reconnaissance and decomposition: audit known facts, identify gaps, map affected surfaces, and split work into independently verifiable missions.\n2. Ownership graph: assign each Carrier a clear file or responsibility boundary, note dependencies, and identify shared mutable resources.\n3. Host-authored structured planning boundary: `Apply the Context Confidence Standing Order \u2014 entry requires complete`. Resolve all blocking and confirmatory gaps before the host authors the dispatch plan.\n4. Parallel dispatch: use the `carrier-operations` skill's sequencing rules to launch independent Carrier work in parallel; sequence only for explicit dependencies or shared resources.\n5. Integration: re-read files before editing or accepting Carrier output, reconcile overlaps, and preserve unrelated user or Carrier changes.\n6. Cross-carrier review loop: route implementation outputs to review Carriers, send actionable findings back to owners, and re-review changed surfaces.\n7. Verification: run integrated tests and apply Deep Dive to speculative or conflicting Carrier claims.\n8. Documentation and completion report: update directly affected docs and report executed waves, Carrier ownership, QA, unresolved risks, and final Result Integrity checks.\n\n## Cross-Carrier Feedback Patterns\n\nWhen composing waves and review loops, select the structured feedback pattern that fits the task:\n\n| Pattern | Flow | When |\n|---------|------|------|\n| **Build \u2192 Review** | implementation carrier \u2192 review carrier \u2192 findings back to implementation carrier \u2192 re-review | Standard implementation cycle |\n| **Analyze \u2192 Execute** | implementation or refactoring carrier \u2192 review carrier verifies | Refactoring workflow |\n| **Decide \u2192 Host Planning \u2192 Execute** | optional judgment carrier \u2192 host-authored plan \u2192 execution carrier | Complex features |\n| **Research \u2192 Act** | reconnaissance carrier \u2192 appropriate follow-up carrier from the active roster | Unknown scope tasks |\n" },
44106
44286
  { relativePath: "protocol-midline/SKILL.md", content: "---\nname: protocol-midline\ndescription: Use the normal Fleet protocol mode for bounded operational work without downward-guard triggers.\n---\n\n# Fleet Protocol: Midline\n\nUse this mode for ordinary bounded operational work.\n\nAt any point during the work, if a Downward Guard trigger appears, stop and re-classify.\n\n## Checkpoints\n\nReconnaissance, Plan, Execution, Verification.\n\n## Reporting Cadence\n\nAs you move through this protocol, report progress to the user in order \u2014 each step on its own line with its report token.\n\n1. Brief how the Procedure will proceed \u2014 name (a) the Procedure steps that will run, (b) the target surfaces, and (c) the verification command. \u2192 report `brief: <\u2026>`\n2. State that execution is beginning and run the Procedure. \u2192 report `status: executing`\n\n## General Quarters\n\nConfirm each readiness check below before the Procedure. Work through them in order and report each as you confirm it, then proceed to focused reconnaissance. These checks prepare the work; they do not gate entry.\n\n- [ ] **Common** \u2014 objective stated (Mission Anchor), mode-fit holds (Mode Gate), Standing Orders binding. \u2192 report `common: ready`\n- [ ] **Target surfaces** \u2014 provisionally name the minimal modules or files reconnaissance will touch; confirm or revise in the brief after reconnaissance. \u2192 report `surfaces: <\u2026>`\n- [ ] **Verification** \u2014 provisionally pre-load the test, build, or check command that will prove the work done; confirm or revise in the brief after reconnaissance. \u2192 report `verify: <cmd>`\n- [ ] **Carrier** \u2014 declare whether a carrier dispatch is needed. \u2192 report `carrier: <none|\u2026>`\n\n## Procedure\n\n1. Focused reconnaissance: audit known facts, identify blocking and confirmatory gaps, and inspect the minimal relevant surfaces.\n2. Host-authored planning boundary: `Apply the Context Confidence Standing Order \u2014 entry requires sufficient`. Resolve all blocking gaps before the host plans.\n3. Host-authored inline plan: state objective, targets, execution steps, and done criteria.\n4. Execution: implement the plan in narrow batches, using Carrier Operations Policy when delegation is appropriate.\n5. Verification and review: run targeted checks, apply Deep Dive to speculative results, and fix actionable issues.\n6. Documentation and final report: update directly affected docs only when behavior or operator guidance changed, then summarize changes and QA.\n" },
@@ -44587,7 +44767,8 @@ async function injectAgentCliProfile(profile, options) {
44587
44767
  customAgents: buildGatewayCustomAgents(
44588
44768
  options.gatewayExposedModels ?? [],
44589
44769
  options.gatewayEffortExposure
44590
- )
44770
+ ),
44771
+ skillOverrides: buildDisabledSkillOverrides(GATEWAY_DISABLED_CLAUDE_SKILLS)
44591
44772
  } : {};
44592
44773
  const context = {
44593
44774
  cliId: profile.id,
@@ -56464,11 +56645,13 @@ async function proxyAnthropicMessages(res, body, options) {
56464
56645
  return;
56465
56646
  }
56466
56647
  const rawBody = readResponseBody(upstream.body);
56467
- const responseBody = options.contextWindow === void 0 && options.responseModel === void 0 ? rawBody : projectAnthropicResponseUsage(rawBody, {
56468
- contentType: upstream.headers.get("content-type"),
56648
+ const contentType = upstream.headers.get("content-type");
56649
+ const projectedBody = options.contextWindow === void 0 && options.responseModel === void 0 ? rawBody : projectAnthropicResponseUsage(rawBody, {
56650
+ contentType,
56469
56651
  contextWindow: options.contextWindow,
56470
56652
  responseModel: options.responseModel
56471
56653
  });
56654
+ const responseBody = options.keepAlive === true && contentType?.split(";", 1)[0]?.trim().toLowerCase() === "text/event-stream" ? withSseKeepAlive(projectedBody) : projectedBody;
56472
56655
  for await (const chunk of responseBody) {
56473
56656
  if (!res.write(chunk)) await drain(res);
56474
56657
  }
@@ -56560,6 +56743,7 @@ async function proxyToOpencode(requestHeaders, res, body, model, contextWindow,
56560
56743
  await proxyAnthropicMessages(res, opencodeRequestBody(body, model), {
56561
56744
  contextWindow,
56562
56745
  responseModel,
56746
+ keepAlive: true,
56563
56747
  fetchImpl,
56564
56748
  headers,
56565
56749
  signal,
@@ -56793,6 +56977,7 @@ async function proxyToAnthropic(requestHeaders, res, body, fetchImpl, signal) {
56793
56977
  if (typeof value === "string") headers[name] = value;
56794
56978
  }
56795
56979
  await proxyAnthropicMessages(res, body, {
56980
+ keepAlive: true,
56796
56981
  fetchImpl,
56797
56982
  headers,
56798
56983
  signal,
@@ -56813,6 +56998,7 @@ async function proxyToKimi(requestHeaders, res, body, model, contextWindow, apiK
56813
56998
  await proxyAnthropicMessages(res, kimiRequestBody(body, model), {
56814
56999
  contextWindow,
56815
57000
  responseModel,
57001
+ keepAlive: true,
56816
57002
  fetchImpl,
56817
57003
  headers,
56818
57004
  signal,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotobokuri/fleet-console",
3
- "version": "1.48.0",
3
+ "version": "1.50.0",
4
4
  "description": "Fleet Console - standalone web surface for observing Fleet CLI workspaces, carrier jobs, live output streams, and terminals.",
5
5
  "repository": {
6
6
  "type": "git",