@leanandmean/coding-agent 0.74.1-scramjet.6 → 0.74.1-scramjet.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/LICENSE +202 -0
  3. package/README.md +21 -9
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +1 -1
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/cli/initial-message.d.ts +1 -0
  8. package/dist/cli/initial-message.d.ts.map +1 -1
  9. package/dist/cli/initial-message.js +3 -0
  10. package/dist/cli/initial-message.js.map +1 -1
  11. package/dist/cli/list-models.d.ts +2 -0
  12. package/dist/cli/list-models.d.ts.map +1 -1
  13. package/dist/cli/list-models.js +4 -1
  14. package/dist/cli/list-models.js.map +1 -1
  15. package/dist/config.d.ts +9 -2
  16. package/dist/config.d.ts.map +1 -1
  17. package/dist/config.js +33 -22
  18. package/dist/config.js.map +1 -1
  19. package/dist/core/agent-session-runtime.d.ts +12 -3
  20. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  21. package/dist/core/agent-session-runtime.js +64 -38
  22. package/dist/core/agent-session-runtime.js.map +1 -1
  23. package/dist/core/agent-session-services.d.ts +1 -0
  24. package/dist/core/agent-session-services.d.ts.map +1 -1
  25. package/dist/core/agent-session-services.js +2 -0
  26. package/dist/core/agent-session-services.js.map +1 -1
  27. package/dist/core/agent-session.d.ts +86 -16
  28. package/dist/core/agent-session.d.ts.map +1 -1
  29. package/dist/core/agent-session.js +865 -181
  30. package/dist/core/agent-session.js.map +1 -1
  31. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  32. package/dist/core/compaction/branch-summarization.js +21 -17
  33. package/dist/core/compaction/branch-summarization.js.map +1 -1
  34. package/dist/core/compaction/compaction.d.ts +3 -5
  35. package/dist/core/compaction/compaction.d.ts.map +1 -1
  36. package/dist/core/compaction/compaction.js +43 -10
  37. package/dist/core/compaction/compaction.js.map +1 -1
  38. package/dist/core/exec.d.ts +12 -0
  39. package/dist/core/exec.d.ts.map +1 -1
  40. package/dist/core/exec.js +74 -28
  41. package/dist/core/exec.js.map +1 -1
  42. package/dist/core/export-html/template.js +4 -0
  43. package/dist/core/extensions/index.d.ts +1 -1
  44. package/dist/core/extensions/index.d.ts.map +1 -1
  45. package/dist/core/extensions/index.js.map +1 -1
  46. package/dist/core/extensions/loader.d.ts.map +1 -1
  47. package/dist/core/extensions/loader.js +7 -0
  48. package/dist/core/extensions/loader.js.map +1 -1
  49. package/dist/core/extensions/runner.d.ts +9 -5
  50. package/dist/core/extensions/runner.d.ts.map +1 -1
  51. package/dist/core/extensions/runner.js +162 -3
  52. package/dist/core/extensions/runner.js.map +1 -1
  53. package/dist/core/extensions/types.d.ts +110 -6
  54. package/dist/core/extensions/types.d.ts.map +1 -1
  55. package/dist/core/extensions/types.js.map +1 -1
  56. package/dist/core/keybindings.d.ts +8 -0
  57. package/dist/core/keybindings.d.ts.map +1 -1
  58. package/dist/core/model-registry.d.ts +4 -0
  59. package/dist/core/model-registry.d.ts.map +1 -1
  60. package/dist/core/model-registry.js +179 -28
  61. package/dist/core/model-registry.js.map +1 -1
  62. package/dist/core/model-resolver.d.ts.map +1 -1
  63. package/dist/core/model-resolver.js +24 -6
  64. package/dist/core/model-resolver.js.map +1 -1
  65. package/dist/core/output-throughput-history.d.ts +25 -0
  66. package/dist/core/output-throughput-history.d.ts.map +1 -0
  67. package/dist/core/output-throughput-history.js +240 -0
  68. package/dist/core/output-throughput-history.js.map +1 -0
  69. package/dist/core/output-throughput.d.ts +43 -0
  70. package/dist/core/output-throughput.d.ts.map +1 -0
  71. package/dist/core/output-throughput.js +161 -0
  72. package/dist/core/output-throughput.js.map +1 -0
  73. package/dist/core/resource-loader.d.ts +6 -0
  74. package/dist/core/resource-loader.d.ts.map +1 -1
  75. package/dist/core/resource-loader.js +76 -21
  76. package/dist/core/resource-loader.js.map +1 -1
  77. package/dist/core/scramjet-command-parser.d.ts +4 -0
  78. package/dist/core/scramjet-command-parser.d.ts.map +1 -1
  79. package/dist/core/scramjet-command-parser.js +72 -1
  80. package/dist/core/scramjet-command-parser.js.map +1 -1
  81. package/dist/core/sdk.d.ts +2 -0
  82. package/dist/core/sdk.d.ts.map +1 -1
  83. package/dist/core/sdk.js +61 -10
  84. package/dist/core/sdk.js.map +1 -1
  85. package/dist/core/session-manager.d.ts +11 -3
  86. package/dist/core/session-manager.d.ts.map +1 -1
  87. package/dist/core/session-manager.js +46 -11
  88. package/dist/core/session-manager.js.map +1 -1
  89. package/dist/core/settings-manager.d.ts +18 -6
  90. package/dist/core/settings-manager.d.ts.map +1 -1
  91. package/dist/core/settings-manager.js +84 -16
  92. package/dist/core/settings-manager.js.map +1 -1
  93. package/dist/core/skills.d.ts.map +1 -1
  94. package/dist/core/skills.js +6 -2
  95. package/dist/core/skills.js.map +1 -1
  96. package/dist/core/system-prompt.d.ts +2 -0
  97. package/dist/core/system-prompt.d.ts.map +1 -1
  98. package/dist/core/system-prompt.js +5 -2
  99. package/dist/core/system-prompt.js.map +1 -1
  100. package/dist/core/tools/bash.d.ts.map +1 -1
  101. package/dist/core/tools/bash.js +4 -3
  102. package/dist/core/tools/bash.js.map +1 -1
  103. package/dist/index.d.ts +4 -3
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +3 -2
  106. package/dist/index.js.map +1 -1
  107. package/dist/main.d.ts +16 -0
  108. package/dist/main.d.ts.map +1 -1
  109. package/dist/main.js +38 -10
  110. package/dist/main.js.map +1 -1
  111. package/dist/modes/interactive/components/armin.d.ts +4 -2
  112. package/dist/modes/interactive/components/armin.d.ts.map +1 -1
  113. package/dist/modes/interactive/components/armin.js +14 -8
  114. package/dist/modes/interactive/components/armin.js.map +1 -1
  115. package/dist/modes/interactive/components/assistant-message.d.ts +3 -1
  116. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  117. package/dist/modes/interactive/components/assistant-message.js +19 -8
  118. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  119. package/dist/modes/interactive/components/daxnuts.d.ts +4 -1
  120. package/dist/modes/interactive/components/daxnuts.d.ts.map +1 -1
  121. package/dist/modes/interactive/components/daxnuts.js +14 -6
  122. package/dist/modes/interactive/components/daxnuts.js.map +1 -1
  123. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  124. package/dist/modes/interactive/components/extension-editor.js +5 -3
  125. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  126. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  127. package/dist/modes/interactive/components/extension-selector.js +4 -3
  128. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  129. package/dist/modes/interactive/components/footer.d.ts +4 -0
  130. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  131. package/dist/modes/interactive/components/footer.js +22 -18
  132. package/dist/modes/interactive/components/footer.js.map +1 -1
  133. package/dist/modes/interactive/components/model-selector.d.ts +3 -1
  134. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  135. package/dist/modes/interactive/components/model-selector.js +9 -3
  136. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  137. package/dist/modes/interactive/components/scoped-models-selector.d.ts +3 -1
  138. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  139. package/dist/modes/interactive/components/scoped-models-selector.js +8 -2
  140. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  141. package/dist/modes/interactive/components/settings-selector.d.ts +17 -3
  142. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  143. package/dist/modes/interactive/components/settings-selector.js +87 -16
  144. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  145. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  146. package/dist/modes/interactive/components/thinking-selector.js +2 -1
  147. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  148. package/dist/modes/interactive/components/tool-execution.d.ts +19 -1
  149. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  150. package/dist/modes/interactive/components/tool-execution.js +129 -36
  151. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  152. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  153. package/dist/modes/interactive/components/user-message.js +3 -2
  154. package/dist/modes/interactive/components/user-message.js.map +1 -1
  155. package/dist/modes/interactive/components/visual-truncate.d.ts.map +1 -1
  156. package/dist/modes/interactive/components/visual-truncate.js +2 -2
  157. package/dist/modes/interactive/components/visual-truncate.js.map +1 -1
  158. package/dist/modes/interactive/interactive-mode.d.ts +37 -1
  159. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  160. package/dist/modes/interactive/interactive-mode.js +876 -175
  161. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  162. package/dist/modes/interactive/spellcheck.d.ts +26 -0
  163. package/dist/modes/interactive/spellcheck.d.ts.map +1 -0
  164. package/dist/modes/interactive/spellcheck.js +144 -0
  165. package/dist/modes/interactive/spellcheck.js.map +1 -0
  166. package/dist/modes/interactive/theme/{dark.json → pi-dark.json} +3 -2
  167. package/dist/modes/interactive/theme/{light.json → pi-light.json} +27 -26
  168. package/dist/modes/interactive/theme/theme-schema.json +6 -1
  169. package/dist/modes/interactive/theme/theme.d.ts +24 -4
  170. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  171. package/dist/modes/interactive/theme/theme.js +123 -22
  172. package/dist/modes/interactive/theme/theme.js.map +1 -1
  173. package/dist/modes/print-mode.d.ts.map +1 -1
  174. package/dist/modes/print-mode.js +5 -0
  175. package/dist/modes/print-mode.js.map +1 -1
  176. package/dist/modes/rpc/rpc-client.d.ts +3 -1
  177. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  178. package/dist/modes/rpc/rpc-client.js.map +1 -1
  179. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  180. package/dist/modes/rpc/rpc-mode.js +6 -1
  181. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  182. package/dist/package-manager-cli.d.ts.map +1 -1
  183. package/dist/package-manager-cli.js +33 -14
  184. package/dist/package-manager-cli.js.map +1 -1
  185. package/dist/utils/clipboard-native.d.ts +1 -0
  186. package/dist/utils/clipboard-native.d.ts.map +1 -1
  187. package/dist/utils/clipboard-native.js.map +1 -1
  188. package/dist/utils/clipboard.d.ts +6 -0
  189. package/dist/utils/clipboard.d.ts.map +1 -1
  190. package/dist/utils/clipboard.js +278 -10
  191. package/dist/utils/clipboard.js.map +1 -1
  192. package/dist/utils/tools-manager.d.ts +1 -1
  193. package/dist/utils/tools-manager.d.ts.map +1 -1
  194. package/dist/utils/tools-manager.js +7 -6
  195. package/dist/utils/tools-manager.js.map +1 -1
  196. package/dist/utils/version-check.d.ts +17 -5
  197. package/dist/utils/version-check.d.ts.map +1 -1
  198. package/dist/utils/version-check.js +88 -21
  199. package/dist/utils/version-check.js.map +1 -1
  200. package/docs/compaction.md +9 -3
  201. package/docs/custom-provider.md +13 -4
  202. package/docs/extensions.md +139 -21
  203. package/docs/keybindings.md +25 -1
  204. package/docs/models.md +34 -2
  205. package/docs/packages.md +2 -0
  206. package/docs/prompt-templates.md +2 -0
  207. package/docs/quickstart.md +1 -4
  208. package/docs/rpc.md +8 -6
  209. package/docs/sdk.md +22 -3
  210. package/docs/session-format.md +2 -1
  211. package/docs/settings.md +13 -3
  212. package/docs/skills.md +7 -5
  213. package/docs/terminal-setup.md +79 -2
  214. package/docs/terminal-verification.md +71 -0
  215. package/docs/themes.md +63 -10
  216. package/docs/tui.md +120 -25
  217. package/docs/usage.md +6 -4
  218. package/examples/extensions/border-status-editor.ts +2 -3
  219. package/examples/extensions/doom-overlay/doom-component.ts +3 -3
  220. package/examples/extensions/mac-system-theme.ts +3 -2
  221. package/examples/extensions/modal-editor.ts +13 -8
  222. package/examples/extensions/overlay-qa-tests.ts +22 -9
  223. package/examples/extensions/overlay-test.ts +5 -3
  224. package/examples/extensions/snake.ts +3 -1
  225. package/examples/extensions/space-invaders.ts +6 -3
  226. package/examples/extensions/subagent/index.ts +20 -17
  227. package/package.json +69 -58
@@ -288,8 +288,13 @@ user sends prompt ────────────────────
288
288
  │ ┌─── turn (repeats while LLM calls tools) ───┐ │
289
289
  │ │ │ │
290
290
  │ ├─► turn_start │ │
291
+ │ ├─► routing refresh │ │
291
292
  │ ├─► context (can modify messages) │ │
293
+ │ ├─► before_provider_call (can replace request-local system prompt)
294
+ │ ├─► provider serialization │ │
292
295
  │ ├─► before_provider_request (can inspect or replace payload)
296
+ │ ├─► provider_request_tool_inventory (observe final tool names)
297
+ │ ├─► transport │ │
293
298
  │ ├─► after_provider_response (status + headers, before stream consume)
294
299
  │ │ │ │
295
300
  │ │ LLM responds, may call tools: │ │
@@ -336,6 +341,16 @@ exit (Ctrl+C, Ctrl+D, SIGHUP, SIGTERM)
336
341
  └─► session_shutdown
337
342
  ```
338
343
 
344
+ ### Required Scramjet Builtin
345
+
346
+ Scramjet initializes as a **required builtin** through the resource loader (`builtinInit`), separately from optional disk-discovered extensions. Its successful initialization is a product invariant across **startup, reload, and every session replacement** (`/new`, resume, fork, clone, import):
347
+
348
+ - The candidate runtime's loader is validated **before** any irreversible live mutation. On reload the loader runs before `session_shutdown` and `resetApiProviders()`; on session replacement the candidate is prepared before the current session is torn down.
349
+ - If the Scramjet builtin fails to initialize, the loader throws a product-attributed `RequiredBuiltinInitError` (preserving the original failure as `cause`). The transition aborts, no `session_shutdown` fires, and the current runtime — runner, resources, providers — is left intact. At initial startup the same failure is fatal and exits non-zero.
350
+ - This is distinct from optional-extension failures: a throwing disk or inline extension remains a non-fatal diagnostic and never aborts the session. Only the required product builtin fails fast.
351
+
352
+ Extension authors do not interact with this directly, but it explains why a broken product init never leaves a session running without Scramjet.
353
+
339
354
  ### Resource Events
340
355
 
341
356
  #### resources_discover
@@ -477,7 +492,7 @@ pi.on("before_agent_start", async (event, ctx) => {
477
492
  // (includes changes from earlier before_agent_start handlers)
478
493
  // event.systemPromptSections - read-only view of the base system prompt
479
494
  // sections for this turn, before extension contributions; the volatile
480
- // environment tail (current date/cwd) is the section with
495
+ // environment tail (current date/cwd/session journal) is the section with
481
496
  // cacheRetention: "none"
482
497
  // event.systemPromptOptions - structured options used to build the system prompt
483
498
  // .customPrompt - any custom system prompt (from --system-prompt, SYSTEM.md, or custom templates)
@@ -486,6 +501,7 @@ pi.on("before_agent_start", async (event, ctx) => {
486
501
  // .promptGuidelines - custom guideline bullets
487
502
  // .appendSystemPrompt - text from --append-system-prompt flags
488
503
  // .cwd - working directory
504
+ // .sessionFile - current session file path (undefined when sessions are disabled)
489
505
  // .contextFiles - AGENTS.md files and other loaded context files
490
506
  // .skills - loaded skills
491
507
 
@@ -510,7 +526,7 @@ Inside `before_agent_start`, `event.systemPrompt` and `ctx.getSystemPrompt()` bo
510
526
 
511
527
  ##### System prompt sections
512
528
 
513
- Scramjet assembles the system prompt as ordered sections (`SystemPromptSection { id, text, cacheRetention?: "none" }` from `@leanandmean/ai`; ids are informational, and `"none"` is the only per-section retention — request-level retention applies otherwise) so providers can cache the stable prefix independently of the volatile tail. The last base section holds the current date and working directory and is marked `cacheRetention: "none"`; on Anthropic, everything before it is folded into a single cached system block and the volatile tail trails uncached.
529
+ Scramjet assembles the system prompt as ordered sections (`SystemPromptSection { id, text, cacheRetention?: "none" }` from `@leanandmean/ai`; ids are informational, and `"none"` is the only per-section retention — request-level retention applies otherwise) so providers can cache the stable prefix independently of the volatile tail. The last base section holds the current date, working directory, and session journal path and is marked `cacheRetention: "none"`; on Anthropic, everything before it is folded into a single cached system block and the volatile tail trails uncached.
514
530
 
515
531
  Returning `systemPromptSection` contributes a section for the current turn without replacing Scramjet's prompt assembly:
516
532
 
@@ -619,14 +635,29 @@ pi.on("context", async (event, ctx) => {
619
635
  });
620
636
  ```
621
637
 
638
+ #### before_provider_call
639
+
640
+ Fired after request routing and message-context transformation, immediately before provider serialization. `event.model` is an immutable, handler-isolated snapshot of the exact model routed for this call, and `event.systemPrompt` is its request-local provider-neutral prompt (`string` or `SystemPromptSection[]`). Return `{ systemPrompt }` to replace only this call's prompt. Replacements chain in extension load order and never accumulate in session state.
641
+
642
+ ```typescript
643
+ pi.on("before_provider_call", (event) => {
644
+ return {
645
+ systemPrompt: typeof event.systemPrompt === "string"
646
+ ? `${event.systemPrompt}\n\nRequest routed to ${event.model.provider}/${event.model.id}`
647
+ : [...event.systemPrompt, { id: "routed-model", text: `\n\nRequest routed to ${event.model.provider}/${event.model.id}` }],
648
+ };
649
+ });
650
+ ```
651
+
622
652
  #### before_provider_request
623
653
 
624
- Fired after the provider-specific payload is built, right before the request is sent. Handlers run in extension load order. Returning `undefined` keeps the payload unchanged. Returning any other value replaces the payload for later handlers and for the actual request.
654
+ Fired after the provider-specific payload is built, right before transport. `event.model` is a fresh immutable snapshot of the same exact routed model exposed by `before_provider_call`; mutating one handler's snapshot cannot affect later handlers or transport routing. Handlers run in extension load order. Returning `undefined` keeps the payload unchanged. Returning any other value replaces the payload for later handlers and for the actual request.
625
655
 
626
- This hook can rewrite provider-level system instructions or remove them entirely. Those payload-level changes are not reflected by `ctx.getSystemPrompt()`, which reports Scramjet's system prompt string rather than the final serialized provider payload.
656
+ This hook can rewrite provider-level system instructions or remove them entirely. Those payload-level changes are not reflected by `ctx.getSystemPrompt()`, which reports Scramjet's stored system prompt string rather than request-local `before_provider_call` changes or the final serialized provider payload.
627
657
 
628
658
  ```typescript
629
- pi.on("before_provider_request", (event, ctx) => {
659
+ pi.on("before_provider_request", (event) => {
660
+ console.log(`${event.model.provider}/${event.model.id}`);
630
661
  console.log(JSON.stringify(event.payload, null, 2));
631
662
 
632
663
  // Optional: replace payload
@@ -634,7 +665,34 @@ pi.on("before_provider_request", (event, ctx) => {
634
665
  });
635
666
  ```
636
667
 
637
- This is mainly useful for debugging provider serialization and cache behavior.
668
+ The complete request order is routing refresh → `context` transformation → `before_provider_call` → provider serialization → public `Agent.onPayload` callback → `before_provider_request` → `provider_request_tool_inventory` → transport. The public callback runs exactly once; `undefined` keeps its input unchanged, while any other return value becomes the input to extension payload handlers. One extension runner is bound when `before_provider_call` begins and owns the request through preparation, payload hooks, and observation. If reload supersedes it during provider-neutral preparation or before payload dispatch begins, the public callback still runs but the old request skips extension rewriting and observation instead of entering the replacement runtime. This is mainly useful for request-local prompt preparation and debugging provider serialization or cache behavior.
669
+
670
+ #### provider_request_tool_inventory
671
+
672
+ Fired after the complete `before_provider_request` replacement chain and before the final payload is returned to provider transport. This event is observation-only: handlers have no return value and cannot change the outgoing request.
673
+
674
+ ```typescript
675
+ pi.on("provider_request_tool_inventory", (event) => {
676
+ // Immutable, bounded identity of the exact routed model.
677
+ console.log(`${event.model.provider}/${event.model.id} (${event.model.api})`);
678
+ // Frozen copy from this request's provider-neutral context.
679
+ console.log(event.requestContextToolNames);
680
+
681
+ if (event.inventory.status === "observed") {
682
+ // Sorted, deduplicated names from the final serialized payload.
683
+ console.log(event.inventory.toolNames);
684
+ } else {
685
+ // "malformed" includes a bounded reason; "unsupported" does not inspect payload fields.
686
+ console.log(event.inventory.status);
687
+ }
688
+ });
689
+ ```
690
+
691
+ The event and all nested fields are frozen. The model snapshot contains only `provider`, `id`, and `api`; it excludes model headers, credentials, URLs, and other configuration. Supported built-in shape families are OpenAI Responses/Azure/Codex, OpenAI completions, Mistral conversations, Anthropic messages, Google Generative AI/Vertex function declarations, and Bedrock tool specifications. A missing optional tool container or valid empty list is observed as an empty inventory; malformed known shapes report only a bounded reason, never raw payload content. Custom API identifiers report `unsupported` when their provider invokes `onPayload`; a custom provider that does not invoke `onPayload` produces no final-schema observation.
692
+
693
+ This boundary proves only what was present in the final pre-transport payload value after the public callback and extension rewrite chain. It does not prove transport success, response parsing, assistant completion, persistence, or model tool selection. If successful reload replaces the request's extension runner during provider-neutral preparation or before payload dispatch begins, extension rewriting and observation are skipped while the independent public callback still applies. If replacement occurs while the old runner's rewrite chain is already in flight, its completed replacement is preserved and the observation is suppressed. Observer failures use normal extension error isolation and do not alter the outgoing payload.
694
+
695
+ **TypeScript compatibility:** Adding this event extends the exported `ExtensionEvent` union. Consumers with exhaustive switches over that union must add this member or a default/unknown-event branch; ordinary `pi.on(...)` registrations remain compatible.
638
696
 
639
697
  #### after_provider_response
640
698
 
@@ -880,7 +938,7 @@ UI methods for user interaction. See [Custom UI](#custom-ui) for full details.
880
938
 
881
939
  ### ctx.hasUI
882
940
 
883
- `false` in print mode (`-p`) and JSON mode. `true` in interactive and RPC mode. In RPC mode, dialog methods (`select`, `confirm`, `input`, `editor`) work via the extension UI sub-protocol, and fire-and-forget methods (`notify`, `setStatus`, `setWidget`, `setTitle`, `setEditorText`) emit requests to the client. Some TUI-specific methods are no-ops or return defaults (see [rpc.md](rpc.md#extension-ui-protocol)).
941
+ `false` in print mode (`-p`) and JSON mode. `true` in interactive and RPC mode. This is a transport-level signal, not a guarantee that every UI method is supported. In RPC mode, dialog methods (`select`, `confirm`, `input`, `editor`) work via the extension UI sub-protocol, and fire-and-forget methods (`notify`, `setStatus`, `setWidget`, `setTitle`, `setEditorText`) emit requests to the client, but TUI-only `custom()` resolves `undefined`. Other TUI-specific methods are no-ops or return defaults (see [rpc.md](rpc.md#extension-ui-protocol)). Extensions that require a specific UI method must handle that method's documented unsupported result.
884
942
 
885
943
  ### ctx.cwd
886
944
 
@@ -955,11 +1013,13 @@ Returns current context usage for the active model. Uses last assistant usage wh
955
1013
 
956
1014
  ```typescript
957
1015
  const usage = ctx.getContextUsage();
958
- if (usage && usage.tokens > 100_000) {
1016
+ if (usage && usage.tokens !== null && usage.tokens > usage.contextWindow * 0.8) {
959
1017
  // ...
960
1018
  }
961
1019
  ```
962
1020
 
1021
+ `usage.contextWindow` is maximum supported total context and the denominator of `usage.percent`. Tokens and percent remain `null` when usage is unknown after compaction. The former `contextWindowBudget` field and `getContextWindowBudget()` export are removed; replace those reads with `contextWindow`. Model registrations may separately represent a genuine provider input constraint with `maxInputTokens`, not a discretionary budget. See [models.md](models.md#breaking-migration-from-context-budgets) for the breaking migration, validation, and preserved configuration/OAuth fallback behavior.
1022
+
963
1023
  ### ctx.compact()
964
1024
 
965
1025
  Trigger compaction without awaiting completion. Use `onComplete` and `onError` for follow-up actions.
@@ -982,6 +1042,7 @@ Returns Scramjet's current system prompt string.
982
1042
 
983
1043
  - During `before_agent_start`, this reflects chained system-prompt changes made so far for the current turn.
984
1044
  - It does not include later `context` message mutations.
1045
+ - It does not include request-local `before_provider_call` prompt replacements.
985
1046
  - It does not include `before_provider_request` payload rewrites.
986
1047
  - If later-loaded extensions run after yours, they can still change what is ultimately sent.
987
1048
 
@@ -1516,10 +1577,17 @@ if (pi.getFlag("plan")) {
1516
1577
  Execute a shell command.
1517
1578
 
1518
1579
  ```typescript
1519
- const result = await pi.exec("git", ["status"], { signal, timeout: 5000 });
1520
- // result.stdout, result.stderr, result.code, result.killed
1580
+ const result = await pi.exec("gh", ["api", "--method", "POST", "--input", "-", endpoint], {
1581
+ signal,
1582
+ timeout: 5000,
1583
+ stdin: JSON.stringify(payload),
1584
+ });
1585
+ // result.stdout, result.stderr, result.code, result.killed,
1586
+ // result.spawnError, result.stdinError, result.stdoutError, result.stderrError
1521
1587
  ```
1522
1588
 
1589
+ When `stdin` is supplied, `pi.exec` writes that exact UTF-8 string once and closes standard input without adding a newline. `spawnError` is present only when the command failed before spawning; `stdinError` reports a write or close failure; `stdoutError` and `stderrError` report output-pipe failures after spawn. Each error contains a safe `message` and optional system `code`, never stdin or process output content. Once the child has spawned, callers must use command-specific semantics to decide whether a failed execution may have produced side effects.
1590
+
1523
1591
  ### pi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(names)
1524
1592
 
1525
1593
  Manage active tools. This works for both built-in tools and dynamically registered tools.
@@ -1539,6 +1607,8 @@ const extensionTools = all.filter((t) => t.sourceInfo.source !== "builtin" && t.
1539
1607
  pi.setActiveTools(["read", "bash"]); // Switch to read-only
1540
1608
  ```
1541
1609
 
1610
+ Changes made by an awaited `turn_end` handler settle before the next provider request snapshots active tools and generated guidance.
1611
+
1542
1612
  `pi.getAllTools()` returns `name`, `description`, `parameters`, and `sourceInfo`.
1543
1613
 
1544
1614
  Typical `sourceInfo.source` values:
@@ -1546,6 +1616,22 @@ Typical `sourceInfo.source` values:
1546
1616
  - `sdk` for tools passed via `createAgentSession({ customTools })`
1547
1617
  - extension source metadata for tools registered by extensions
1548
1618
 
1619
+ ### pi.invokeHarnessTool(name, args, options?)
1620
+
1621
+ Execute a registered tool as a **harness-originated** call — one the harness makes itself, not one the model requested. The tool runs through the identical prepare/execute/finalize pipeline as a model tool call: it produces the same live `tool_execution_*`/message events, persisted session entries, and `tool_call`/`tool_result` hooks, but emits **no** run/turn framing (`agent_start`/`turn_*`/`agent_end`), so it is invisible to `agent_end`-keyed logic. The tool is resolved from the full registry, so a `"harness-only"` tool (never provider-visible) can still be executed. When the agent is idle the call executes immediately; mid-run it is queued and drained before the next intra-run LLM call.
1622
+
1623
+ ```typescript
1624
+ // Record a harness-originated event as a real, replayable tool row, then proceed only once it exists.
1625
+ await pi.invokeHarnessTool("my_notice_tool", { text: "model changed to X" });
1626
+ // The tool-result row is now persisted in the session transcript.
1627
+ ```
1628
+
1629
+ The returned promise **resolves only after the resulting tool-result message has been persisted** to the session — a stronger guarantee than "the tool ran". A consumer that must not replace or tear down the session (e.g. before starting a fresh session) until the record row exists can safely `await` it. The promise **rejects** if no tool with `name` is registered, if a matching event fails to process/persist, if the session is disposed before it settles or is called after disposal, or if an explicit `options.toolCallId` is malformed or already pending. Rejection means the pipeline did not complete — it does **not** prove the artifact is absent (state and persistence may have partially completed), so do not blindly retry. A tool's own `execute()` error is not a rejection: it resolves normally as an `isError` result.
1630
+
1631
+ `options.toolCallId` (optional) sets an explicit tool-call id (must be provider-safe: 1-64 characters of `[a-zA-Z0-9_-]`); a provider-safe id is generated when omitted.
1632
+
1633
+ Do not `await` this from work whose return is required for the agent to reach the mid-run drain point or for the event queue to persist the result (a model-callable `execute`, `beforeToolBatch`/`beforeToolCall`/`afterToolCall`/`prepareNextTurn`, or an awaited event handler) — that would deadlock. Start the promise, return from the gating callback, and await it from an independent continuation.
1634
+
1549
1635
  ### pi.setModel(model)
1550
1636
 
1551
1637
  Set the current model. Returns `false` if no API key is available for the model. See [models.md](models.md) for configuring custom models.
@@ -2151,6 +2237,16 @@ Extensions can interact with users via `ctx.ui` methods and customize how messag
2151
2237
  - Autocomplete providers layered on top of built-in slash/path completion (addAutocompleteProvider)
2152
2238
  - Custom footers (setFooter)
2153
2239
 
2240
+ ### Retained viewport and input ownership
2241
+
2242
+ InteractiveMode uses the existing component presentation in an alternate-screen retained viewport. Ordinary `render(width)`, tool `renderCall`/`renderResult`, result/session schemas and HTML export contracts are unchanged. Render the complete logical text rather than clipping it to terminal height. Components receive terminal width minus the reserved scrollbar column; images alone may use the optional `setViewportHeight` bound. Standard Container/Box propagate it; custom image wrappers must forward it. Unrecognized custom graphics envelopes display a placeholder rather than an unsafe clipped placement.
2243
+
2244
+ `ctx.ui.onTerminalInput()` listeners run after the viewport listener. They do not receive consumed pointer packets, selection-copy keys, Ctrl+Home/Ctrl+End transcript navigation or detached PageUp/PageDown/Escape when the input owner permits viewport navigation. Ordinary Home/End pass through for editor line-start/end behavior. Do not use raw listeners to compete for transcript pointer ownership. Focused overlays keep keyboard precedence, and conflicting selector bindings precede configurable Alt+PageUp/Alt+PageDown transcript actions. Focus-in/out reports are consumed rather than delivered as ordinary input. The viewport keeps no session-journal state; session reconstruction discards browsing/selection state.
2245
+
2246
+ The input area is docked by default, with widgets preserving their above/below-editor placement. Docking can change live without replacing component instances. Non-fitting extension content remains reachable in an explained undocked retained flow. Custom editors can implement `setHeightLimit(limits)`, where the callback returns `{ rows, text }` for the current total input budget and maximum wrapped text rows. Respect the budget without losing draft content, keep the cursor and active completion choice visible, and use the effective text-window height for paging. This is separate from the image-only `setViewportHeight` API. Editors with completion menus can also expose optional `isShowingAutocomplete(): boolean`; InteractiveMode then reveals a hidden editor slot independently of its hardware cursor and consumes acceptance until the slot is visible and its current paint has flushed. Built-in `CustomEditor` inherits this capability. See [tui.md](tui.md#retained-viewport-api) for projection, selection and geometry contracts.
2247
+
2248
+ Kitty-capable viewport terminals explicitly encode printable keys as well as modified keys. Match actions against raw input with `matchesKey()`/the injected keybinding manager before using `decodeKittyPrintable(data) ?? data` for printable-mode mappings; decoding first can obscure special-key identities such as keypad Enter. Do not assume a printable key is one raw byte. Raw listeners must ignore `isKeyRelease(data)` unless they intentionally handle releases; focused components filter releases by default. The shipped [modal editor](../examples/extensions/modal-editor.ts) demonstrates the printable-key migration. No blanket compatibility claim is made for external raw-byte listeners or graphics wrappers; see [tui.md](tui.md) for exact viewport APIs and [terminal-setup.md](terminal-setup.md#native-compatibility-evidence) for tested configurations.
2249
+
2154
2250
  ### Dialogs
2155
2251
 
2156
2252
  ```typescript
@@ -2369,18 +2465,18 @@ See [github-issue-autocomplete.ts](../examples/extensions/github-issue-autocompl
2369
2465
  For complex UI, use `ctx.ui.custom()`. This temporarily replaces the editor with your component until `done()` is called:
2370
2466
 
2371
2467
  ```typescript
2372
- import { Text, Component } from "@leanandmean/tui";
2468
+ import { Text } from "@leanandmean/tui";
2373
2469
 
2374
- const result = await ctx.ui.custom<boolean>((tui, theme, keybindings, done) => {
2470
+ const result = await ctx.ui.custom<boolean>((_tui, _theme, keybindings, done) => {
2375
2471
  const text = new Text("Press Enter to confirm, Escape to cancel", 1, 1);
2376
-
2377
- text.onKey = (key) => {
2378
- if (key === "return") done(true);
2379
- if (key === "escape") done(false);
2380
- return true;
2472
+ return {
2473
+ render: (width) => text.render(width),
2474
+ invalidate: () => text.invalidate(),
2475
+ handleInput(data) {
2476
+ if (keybindings.matches(data, "tui.select.confirm")) done(true);
2477
+ else if (keybindings.matches(data, "tui.select.cancel")) done(false);
2478
+ },
2381
2479
  };
2382
-
2383
- return text;
2384
2480
  });
2385
2481
 
2386
2482
  if (result) {
@@ -2394,6 +2490,28 @@ The callback receives:
2394
2490
  - `keybindings` - App keybinding manager (for checking shortcuts)
2395
2491
  - `done(value)` - Call to close component and return value
2396
2492
 
2493
+ Ordinary editor-slot custom UI can opt into actual layout allocation with `ctx.ui.custom(factory, { onAvailableHeight(rows) { maximumRows = rows; } })`, where `maximumRows` is invocation-local state read during rendering. The callback receives `undefined` at mounting/release and a numeric row budget before each allocated render; it can run more than once per frame. It must not request another render or change focus, and allocation does not certify docking, visibility or flushing. Retained undocked rendering can allocate rows too; committed rendering has no numeric allocation. Below-minimum geometry skips rendering rather than delivering a temporary budget, and recovery supplies a fresh allocation. Other components are unchanged unless they explicitly opt in. The option is incompatible with overlays and tool-attached context and is rejected before the factory runs; it is separate from image-only `setViewportHeight` and editor text-height preferences. Allocation-notification errors reject the owning custom UI request and release/dispose its control; release-notification errors are reported without stranding completion. See [allocated custom input height](tui.md#allocated-custom-input-height).
2494
+
2495
+ A pending sequential tool can attach immutable long-form context to its own tool row before compact controls become interactive. Pass the current `toolCallId` from the tool's `execute()` method:
2496
+
2497
+ ```typescript
2498
+ const result = await ctx.ui.custom(
2499
+ (_tui, _theme, _keybindings, done) => new ApprovalSelector(done),
2500
+ {
2501
+ toolAttachedContext: {
2502
+ toolCallId,
2503
+ render: (_tui, theme) => new Text(theme.fg("muted", completeContext), 0, 0),
2504
+ },
2505
+ },
2506
+ );
2507
+ ```
2508
+
2509
+ The complete immutable context is installed at that pending tool's transcript position. In both renderer modes, the editor is defocused and a required initial flush must succeed before controls receive focus. Missing/non-leading/settled tool rows, context replaced before that flush settles, and missing or failed flushing reject the request through attachment cleanup. Context is visual-only, not persisted or sent to the model. Use this only from the sequential tool whose id is supplied.
2510
+
2511
+ In default **retained mode**, the context remains browseable through Scramjet's scrollbar even when taller than the screen; it need not all be simultaneously visible or emitted into native scrollback. Controls require current-paint flush evidence, complete rendering of the full context and controls, and whole-control visibility. Clipped context (including offscreen rows) or controls that cannot fit fail closed. If browsing hides controls, the first activation reveals and flushes them without approving; only subsequent input can authorize. Attachment currentness and these presentation guards are rechecked on input.
2512
+
2513
+ With startup-only **`tuiMode: "committed"`**, `commitNow({ requireFlush: true })` emits the complete context into committed native history and waits for the initial flush before focusing controls. The mutable canvas remains tail-windowed; this mode does **not** provide Scramjet scrollbar access, retained render-completeness/whole-control-visibility checks, or reveal-before-subsequent-activation protection. Do not rely on those retained-only guarantees when supporting committed compatibility mode.
2514
+
2397
2515
  See [tui.md](tui.md) for the full component API.
2398
2516
 
2399
2517
  #### Overlay Mode (Experimental)
@@ -2438,7 +2556,7 @@ class VimEditor extends CustomEditor {
2438
2556
  this.mode = "normal";
2439
2557
  return;
2440
2558
  }
2441
- if (this.mode === "normal" && data === "i") {
2559
+ if (this.mode === "normal" && matchesKey(data, "i")) {
2442
2560
  this.mode = "insert";
2443
2561
  return;
2444
2562
  }
@@ -2548,7 +2666,7 @@ const highlighted = highlightCode(code, lang, theme);
2548
2666
  | Mode | UI Methods | Notes |
2549
2667
  |------|-----------|-------|
2550
2668
  | Interactive | Full TUI | Normal operation |
2551
- | RPC (`--mode rpc`) | JSON protocol | Host handles UI, see [rpc.md](rpc.md) |
2669
+ | RPC (`--mode rpc`) | JSON protocol | Host handles supported UI methods; TUI-only `custom()` resolves `undefined`, see [rpc.md](rpc.md) |
2552
2670
  | JSON (`--mode json`) | No-op | Event stream to stdout, see [json.md](json.md) |
2553
2671
  | Print (`-p`) | No-op | Extensions run but can't prompt |
2554
2672
 
@@ -1,6 +1,6 @@
1
1
  # Keybindings
2
2
 
3
- All keyboard shortcuts can be customized via `~/.scramjet/agent/keybindings.json`. Each action can be bound to one or more keys.
3
+ Action keybindings can be customized via `~/.scramjet/agent/keybindings.json`. Each action can be bound to one or more keys.
4
4
 
5
5
  The config file uses the same namespaced keybinding ids that Scramjet uses internally and that extension authors use in `keyHint()` and injected `keybindings` managers.
6
6
 
@@ -20,8 +20,30 @@ After editing `keybindings.json`, run `/reload` in Scramjet to apply the changes
20
20
 
21
21
  Modifier combinations: `ctrl+shift+x`, `alt+ctrl+x`, `ctrl+shift+alt+x`, `ctrl+1`, etc.
22
22
 
23
+ ## Transcript browsing precedence
24
+
25
+ Interactive mode's retained viewport owns mouse wheel/trackpad scrolling, the rightmost draggable scrollbar and ordinary drag selection. Right-click a nonempty selection or use `tui.input.copy` (Ctrl+C by default) to copy displayed text. Without a transcript selection, the same key keeps its focused-component/application behavior. Escape clears selection and returns to the live tail.
26
+
27
+ While detached from the tail with the editor owning input, the viewport consumes this fixed set before editor bindings; focused selectors retain their own navigation and cancellation bindings:
28
+
29
+ | Key | Browsing action |
30
+ |-----|-----------------|
31
+ | PageUp / PageDown | Scroll one viewport page |
32
+ | Escape | Return to the live tail |
33
+
34
+ Home/End retain editor line-start/end behavior at every transcript position. Ctrl+Home goes to the transcript beginning and Ctrl+End goes to its bottom and resumes following, whether currently following or detached. Docked input and configured tool/thinking presentation toggles preserve the transcript anchor; undocked ordinary editing reveals the editor cursor. Submitting nonempty editor input resumes following before command routing or queueing, including Enter and Alt+Enter. New user-message delivery also resumes following; passive output does not. At the tail, unmodified paging retains its editor/list meanings. Focused overlays take precedence, and focused selector bindings take precedence over both fixed and configurable viewport actions. In particular, Escape cancels a focused settings selector rather than merely returning its transcript to the tail. If tool-attached approval controls are hidden by browsing, the first activation reveals and flushes them without approving; a later activation can approve.
35
+
36
+ The fixed detached-only keys above remain separate from `tui.viewport.pageUp` and `tui.viewport.pageDown`, which default to Alt+PageUp and Alt+PageDown and work from the tail as well as while browsing. Terminal/desktop interception can require remapping these configurable actions. See [terminal-setup.md](terminal-setup.md#transcript-browsing-and-copying) for terminal-menu and clipboard requirements.
37
+
23
38
  ## All Actions
24
39
 
40
+ ### Transcript Navigation
41
+
42
+ | Keybinding id | Default | Description |
43
+ |--------|---------|-------------|
44
+ | `tui.viewport.pageUp` | `alt+pageUp` | Browse one transcript page upward, including from the tail |
45
+ | `tui.viewport.pageDown` | `alt+pageDown` | Browse one transcript page downward |
46
+
25
47
  ### TUI Editor Cursor Movement
26
48
 
27
49
  | Keybinding id | Default | Description |
@@ -114,6 +136,8 @@ Modifier combinations: `ctrl+shift+x`, `alt+ctrl+x`, `ctrl+shift+alt+x`, `ctrl+1
114
136
  | `app.thinking.cycle` | `shift+tab` | Cycle thinking level |
115
137
  | `app.thinking.toggle` | `ctrl+t` | Collapse or expand thinking blocks |
116
138
 
139
+ `app.thinking.cycle` also works while Scramjet next-step and structured confirm/select dialogs are open. Those dialogs display the usable configured shortcut, apply effort changes immediately, and retain them when cancelled. Dialog navigation, confirmation, cancellation, and next-step model arrows take precedence if bindings conflict; freetext and headless interactions are unchanged.
140
+
117
141
  ### Display and Message Queue
118
142
 
119
143
  | Keybinding id | Default | Description |
package/docs/models.md CHANGED
@@ -91,6 +91,8 @@ Override defaults when you need specific values:
91
91
 
92
92
  The file reloads each time you open `/model`. Edit during session; no restart needed.
93
93
 
94
+ When valid history exists, `/model` and `/scoped-models` append a recent median output-rate estimate to the requested provider/model row. The estimate uses normalized final output usage over first-to-last generated output, excluding time to first token and trailing completion latency; no placeholder is shown for models without history.
95
+
94
96
  ## Google AI Studio Example
95
97
 
96
98
  Use `google-generative-ai` with a `baseUrl` to add models from Google AI Studio, including custom Gemma 4 entries:
@@ -194,11 +196,41 @@ If your command is slow, expensive, rate-limited, or should keep using a previou
194
196
  | `reasoning` | No | `false` | Supports extended thinking |
195
197
  | `thinkingLevelMap` | No | omitted | Maps scramjet thinking levels to provider values and marks unsupported levels (see below) |
196
198
  | `input` | No | `["text"]` | Input types: `["text"]` or `["text", "image"]` |
197
- | `contextWindow` | No | `128000` | Context window size in tokens |
199
+ | `contextWindow` | No | `128000` | Maximum supported total context for this provider/model, in tokens |
200
+ | `maxInputTokens` | No | omitted | Genuine independently enforced provider input limit, only when evidenced for this route |
201
+ | `requestLimits` | No | omitted | Non-empty array of provider-declared joint endpoint token constraints (see below) |
198
202
  | `maxTokens` | No | `16384` | Maximum output tokens |
199
203
  | `cost` | No | all zeros | `{"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0}` (per million tokens) |
200
204
  | `compat` | No | provider `compat` | Provider compatibility overrides. Merged with provider-level `compat` when both are set. |
201
205
 
206
+ `contextWindow` is the sole total-context authority for usage percentages, the footer, listings, and compaction. Do not reduce it for pricing, presumed quality, or speculative conservatism. `maxTokens` is an independent output allowance; configurable compaction reserves remain separate. Custom-model defaults are client defaults, not evidence of your backend's maximum—set the actual deployment value.
207
+
208
+ Final `contextWindow` and any `maxInputTokens` must be positive finite numbers after configuration merges, dynamic registration, and OAuth transformations; positive fractional context values remain accepted. An input constraint above total context is non-binding, not a reason to increase total context. Only assign input constraints from applicable provider evidence, never by subtracting maximum output or copying an old budget. Aggregate endpoint maxima may not be jointly available under custom routing or account restrictions. OpenRouter generation reads endpoint input constraints and retains the largest permitted input across all reported routes, only when that is independently below total context. A route without a separate input limit contributes its total context, not a guessed output reservation. Empty endpoint lists provide no input-limit evidence. This does not guarantee that the maximum input, output, tools, and other requested capabilities are jointly available on one endpoint.
209
+
210
+ ### Joint endpoint constraints
211
+
212
+ `requestLimits` preserves input/output combinations that independent scalar maxima cannot express. Each entry has required positive finite `maxTotalTokens` and boolean `supportsTools`, plus optional positive finite `maxInputTokens` and `maxOutputTokens`. These are endpoint request constraints, not another model context denominator. Omitted input/output fields add no independent bound; they are not output reservations or proof of unlimited backend capacity. An absent array adds no joint check; an empty or malformed array is rejected at configuration, dynamic/OAuth registration, and Faux boundaries.
213
+
214
+ For example, a long-context endpoint can accept 98,304 input tokens but only 8,192 output tokens, while another endpoint allows longer output only within a 40,960-token total. Coding-agent sessions and both built-in summarizers filter these declarations by estimated input and required tool support, then cap explicit output at the largest remaining allowance from a compatible entry. They reject requests with no compatible entry rather than selecting a provider, truncating the prompt, or globally reducing `contextWindow` or `maxTokens`. Ordinary OpenRouter output remains omitted when not explicitly requested. Direct AI streams and ordinary standalone Agent/AgentHarness streams do not automatically run this allocation; their callers own request constraints.
215
+
216
+ OpenRouter and Vercel generation deduplicates equivalent declarations and omits joint metadata when a tool-capable endpoint already admits the entire scalar envelope. Empty or unavailable discovery is not a verified envelope. These declarations do not encode custom routing preferences, account restrictions, modalities, or every requested capability, and do not guarantee provider acceptance. If changing the route, supply applicable constraints through an override or replace the model definition rather than assuming inherited declarations describe the new backend. Overrides replace the entire array, not individual entries.
217
+
218
+ ### Breaking migration from context budgets
219
+
220
+ `contextWindowBudget` and the exported `getContextWindowBudget()` resolver have been removed, without aliases. Replace resolver calls and usage-budget reads with `contextWindow`; `ContextUsage` is now `{ tokens, percent, contextWindow }`. Remove the obsolete key from model definitions, `models.json` overrides, dynamic providers, Faux definitions, and OAuth-produced models—even when it equals total context. Registration/configuration diagnostics identify the provider/model and explain removal. No user files are rewritten and no old value is automatically copied into `contextWindow` or `maxInputTokens`.
221
+
222
+ Invalid configuration retains built-ins while rejecting its request settings; an invalid OAuth transformation is diagnosed and its untransformed candidate models remain. This preserves existing fallback behavior, not a guarantee that requests are blocked until configuration is repaired.
223
+
224
+ Built-in metadata is not proof of endpoint acceptance. The provider audit in [issue #525](https://github.com/LeanAndMean/scramjet/issues/525) retains unresolved numerical/route dispositions, including some Codex maxima and aggregate input/output combinations; retained values are not newly verified by this migration. Tests use static metadata and synthetic usage, not paid maximum-context requests. Built-in `cost` values are scalar usage **estimates**, not billing guarantees: the September 2026 refresh reused same-route Pi or prior catalog values where source cache prices were absent. A zero estimate is not proof that a provider bills nothing for cache activity; check the applicable provider/account pricing when monetary accuracy matters. Some built-in model IDs retired or disappeared from both the live source and upstream Pi; a missing feed entry does not prove a custom deployment or account has retired it. Removed IDs no longer appear in selectors: saved selections may fall back to another available model, a configured default that no longer resolves reports the missing ID and chosen fallback at startup, unmatched scopes are skipped, and `modelOverrides` cannot recreate an absent built-in. If a custom deployment still serves one, define it in `models.json` with applicable API, limits and cost metadata, then update saved selections and scopes rather than assuming an alias. An Azure Responses listing for `gpt-realtime-2.1` was removed from an intermediate PR head (it was absent from the committed baseline). Azure Responses GPT-6 Astra, Luna and Sol are now built-ins because Microsoft's own Responses model list, model-limit table and deployment-tier pricing supply route-specific evidence; direct OpenAI limits and prices were not used as Azure proof. Azure region and deployment entitlement still vary. See the [built-in catalog provenance](../../ai/catalog-provenance.md) for dated sources, scoped removal groups, retentions, and exclusions.
225
+
226
+ GPT-6 Astra through the public OpenAI API stores the standard scalar rates of $10 input, $50 output, $1 cache read, and $12.50 cache write per million tokens. Requests above 272,000 input tokens use higher rates for the whole request, which the current scalar estimator cannot represent, so its monetary estimate may be too low while its token counts remain valid. The September 2026 refresh likewise uses the current [OpenAI base-tier prices](https://developers.openai.com/api/docs/pricing) for public GPT-5.6 Sol ($4/$20 input/output), Terra ($2/$12) and Luna ($0.20/$1.20) per million tokens; their long-context tiers can cost more without reducing their 1.05M total context. The `openai-codex` and `github-copilot` records carry scalar values only as usage estimates; those subscription providers do not imply marginal per-request API charges.
227
+
228
+ `openai-codex/gpt-6-sol` and `/gpt-6-luna` use the Codex OAuth route and its published 872K maximum context, not the direct API's 1.05M. Codex's public model catalog does not declare their maximum output; their 128K `maxTokens` values are an explicitly authorized, **provisional** reuse of the direct-API allowance, not a verified Codex-route ceiling. Scalar dollar costs likewise proxy public API usage, not Codex subscription credits or account entitlement. `gpt-5.4` and `gpt-5.4-mini` retired from Codex with ChatGPT sign-in; saved Codex selections of either ID may fall back, with a warning where enabled, but direct OpenAI remains a distinct route. Google's Vertex-specific audit added six tool-capable 3.x models and removed its two documented-retired 2.0 models; the 3.6–3.8 Flash introductory rates expire after December 2026. See the [route audit](../../ai/catalog-provenance.md#hand-maintained-and-derived-route-audit-2026-09-24) for source and retention qualifications.
229
+
230
+ The GitHub Copilot records for `gpt-6-sol`, `gpt-6-luna`, and `claude-opus-5.5` use models.dev and an authenticated individual-account Copilot `GET /models` response observed on 2026-09-23. Where those sources conflicted, the account-scoped Copilot route governed: each record uses 1,000,000 total context, an independent 872,000 input limit, and a 128,000 output limit. These are metadata observations, not paid invocation or maximum-boundary acceptance tests.
231
+
232
+ GitHub Copilot catalog visibility requires configured Copilot authentication, but a listed model is not proof that the current account is entitled to use it or that the endpoint will accept a request. Copilot prices are scalar default/low-context usage estimates for a subscription route, not guaranteed marginal charges, and cannot encode separately reported long-context tiers. Grok's separate long-context rates likewise cannot be represented by the scalar estimator, so estimates for long requests may be too low while token counts remain valid.
233
+
202
234
  Current behavior:
203
235
  - `/model` and `--list-models` list entries by model `id`.
204
236
  - The configured `name` is used for model matching and detail/status text.
@@ -305,7 +337,7 @@ Use `modelOverrides` to customize specific built-in models without replacing the
305
337
  }
306
338
  ```
307
339
 
308
- `modelOverrides` supports these fields per model: `name`, `reasoning`, `input`, `cost` (partial), `contextWindow`, `maxTokens`, `headers`, `compat`.
340
+ `modelOverrides` supports these fields per model: `name`, `reasoning`, `input`, `cost` (partial), `contextWindow`, `maxInputTokens`, `requestLimits`, `maxTokens`, `headers`, `compat`.
309
341
 
310
342
  Behavior notes:
311
343
  - `modelOverrides` are applied to built-in provider models.
package/docs/packages.md CHANGED
@@ -38,6 +38,8 @@ scramjet update --extension npm:@foo/bar
38
38
 
39
39
  By default, `install` and `remove` write to global settings (`~/.scramjet/agent/settings.json`). Use `-l` to write to project settings (`.scramjet/settings.json`) instead. Project settings can be shared with your team, and scramjet installs any missing packages automatically on startup.
40
40
 
41
+ For a globally package-manager-managed CLI, self-update resolves one current npm release, installs that exact package version with the owning package manager, and verifies the managed package metadata before reporting success. This verification does not inspect or repair duplicate installations selected through `PATH`.
42
+
41
43
  To try a package without installing it, use `--extension` or `-e`. This installs to a temporary directory for the current run only:
42
44
 
43
45
  ```bash
@@ -4,6 +4,8 @@
4
4
 
5
5
  Prompt templates are Markdown snippets that expand into full prompts. Type `/name` in the editor to invoke a template, where `name` is the filename without `.md`.
6
6
 
7
+ A template expansion is an ordinary user prompt, not a Scramjet command. It does not receive `<scramjet-command>` framing, command lifecycle association, declared next-step behavior, or a guarantee that the agent must execute it as an active command. Skills are on-demand capability packages rather than ordinary prompt expansions, but they likewise do not carry the Scramjet command guarantee. When template-derived information supports an active command, that command's explicit Goals (or inferred outcomes for a legacy command), user decisions, trust boundaries, consumer contracts, and required side effects remain controlling. See the [Scramjet command authoring guide](../../scramjet/docs/command-authoring.md) for the command contract.
8
+
7
9
  ## Locations
8
10
 
9
11
  Scramjet loads prompt templates from:
@@ -73,10 +73,7 @@ Scramjet loads context files at startup. Add an `AGENTS.md` file to tell it how
73
73
  - Keep responses concise.
74
74
  ```
75
75
 
76
- Scramjet loads:
77
-
78
- - `~/.scramjet/agent/AGENTS.md` for global instructions
79
- - `AGENTS.md` or `CLAUDE.md` from parent directories and the current directory
76
+ Scramjet loads `CLAUDE.md` and `AGENTS.md` from the global agent directory, parent directories, and the current directory. Within each directory, `CLAUDE.md` loads first. If both files there decode to exactly equal text, only `CLAUDE.md` is loaded; equal text in different directories remains independently loaded.
80
77
 
81
78
  Restart scramjet, or run `/reload`, after changing context files.
82
79
 
package/docs/rpc.md CHANGED
@@ -61,7 +61,7 @@ With images:
61
61
  - `"steer"`: Queue the message while the agent is running. It is delivered after the current assistant turn finishes executing its tool calls, before the next LLM call.
62
62
  - `"followUp"`: Wait until the agent finishes. Message is delivered only when agent stops.
63
63
 
64
- If the agent is streaming and no `streamingBehavior` is specified, the command returns an error.
64
+ If the agent is streaming and no `streamingBehavior` is specified, the command returns an error. During automatic-retry backoff, when the agent is not streaming but the retry chain is still active, a new prompt is rejected; send `abort_retry` before starting unrelated work.
65
65
 
66
66
  **Extension commands**: If the message is an extension command (e.g., `/mycommand`), it executes immediately even during streaming. Extension commands manage their own LLM interaction via `pi.sendMessage()`.
67
67
 
@@ -263,7 +263,7 @@ List all configured models.
263
263
  {"type": "get_available_models"}
264
264
  ```
265
265
 
266
- Response contains an array of full [Model](#model) objects:
266
+ Response contains an array of full [Model](#model) objects. `RpcClient.getAvailableModels()` types these as `ModelInfo`, including optional `maxInputTokens`; there is no runtime projection:
267
267
  ```json
268
268
  {
269
269
  "type": "response",
@@ -323,8 +323,8 @@ Control how steering messages (from `steer`) are delivered.
323
323
  ```
324
324
 
325
325
  Modes:
326
- - `"all"`: Deliver all steering messages after the current assistant turn finishes executing its tool calls
327
- - `"one-at-a-time"`: Deliver one steering message per completed assistant turn (default)
326
+ - `"all"`: Deliver all steering messages after the current assistant turn finishes executing its tool calls (default)
327
+ - `"one-at-a-time"`: Deliver one steering message per completed assistant turn
328
328
 
329
329
  Response:
330
330
  ```json
@@ -408,7 +408,7 @@ Response:
408
408
 
409
409
  #### abort_retry
410
410
 
411
- Abort an in-progress retry (cancel the delay and stop retrying).
411
+ Abort an in-progress retry, whether it is waiting in backoff or running the retry continuation.
412
412
 
413
413
  ```json
414
414
  {"type": "abort_retry"}
@@ -533,7 +533,7 @@ Response:
533
533
  }
534
534
  ```
535
535
 
536
- `tokens` contains assistant usage totals for the current session state. `contextUsage` contains the actual current context-window estimate used for compaction and footer display.
536
+ `tokens` contains assistant usage totals for the current session state. `contextUsage.contextWindow` is maximum supported total context and the denominator of `percent`. Compaction uses this same context value with the configured reserve. `contextWindowBudget` has been removed from both model and usage transport contracts; clients must read `contextWindow` instead. See [models.md](models.md#breaking-migration-from-context-budgets).
537
537
 
538
538
  `contextUsage` is omitted when no model or context window is available. `contextUsage.tokens` and `contextUsage.percent` are `null` immediately after compaction until a fresh post-compaction assistant response provides valid usage data.
539
539
 
@@ -1227,6 +1227,8 @@ Source files:
1227
1227
  }
1228
1228
  ```
1229
1229
 
1230
+ `contextWindow` is maximum supported total context. An optional `maxInputTokens` represents only an independently evidenced provider input constraint; it is not another context denominator. The removed budget field is not emitted.
1231
+
1230
1232
  ### UserMessage
1231
1233
 
1232
1234
  ```json
package/docs/sdk.md CHANGED
@@ -93,6 +93,9 @@ interface AgentSession {
93
93
  cycleModel(): Promise<ModelCycleResult | undefined>;
94
94
  cycleThinkingLevel(): ThinkingLevel | undefined;
95
95
 
96
+ // Harness-originated tool execution (resolves after the tool-result is persisted)
97
+ invokeHarnessTool(name: string, args: unknown, options?: { toolCallId?: string }): Promise<void>;
98
+
96
99
  // State access
97
100
  agent: Agent;
98
101
  model: Model | undefined;
@@ -117,6 +120,8 @@ interface AgentSession {
117
120
 
118
121
  Session replacement APIs such as new-session, resume, fork, and import live on `AgentSessionRuntime`, not on `AgentSession`.
119
122
 
123
+ `invokeHarnessTool(name, args, options?)` runs a registered tool as a harness-originated call (not model-requested) through the real execution pipeline — same events, persistence, and hooks, but no run/turn framing. Its promise resolves only after the resulting tool-result message has been persisted to the session, so a consumer can `await` it before replacing or tearing down the session. It rejects on unknown tool, a matching event failing to persist, disposal before settlement or a call after disposal, or a malformed or duplicate explicit `toolCallId`; rejection does not prove the artifact is absent, so do not blindly retry. A tool's own `execute()` error resolves as an `isError` result rather than rejecting.
124
+
120
125
  ### createAgentSessionRuntime() and AgentSessionRuntime
121
126
 
122
127
  Use the runtime API when you need to replace the active session and rebuild cwd-bound runtime state.
@@ -360,7 +365,7 @@ const { session } = await createAgentSession({
360
365
  - `.scramjet/skills/`
361
366
  - `.agents/skills/` in `cwd` and ancestor directories (up to git repo root, or filesystem root when not in a repo)
362
367
  - Project prompts (`.scramjet/prompts/`)
363
- - Context files (`AGENTS.md` walking up from cwd)
368
+ - Context files (`CLAUDE.md` and `AGENTS.md` walking up from cwd)
364
369
  - Session directory naming
365
370
 
366
371
  `agentDir` is used by `DefaultResourceLoader` for:
@@ -369,7 +374,7 @@ const { session } = await createAgentSession({
369
374
  - `skills/` under `agentDir` (for example `~/.scramjet/agent/skills/`)
370
375
  - `~/.agents/skills/`
371
376
  - Global prompts (`prompts/`)
372
- - Global context file (`AGENTS.md`)
377
+ - Global context files (`CLAUDE.md` and `AGENTS.md`)
373
378
  - Settings (`settings.json`)
374
379
  - Custom models (`models.json`)
375
380
  - Credentials (`auth.json`)
@@ -420,12 +425,24 @@ If no model is provided:
420
425
 
421
426
  `cacheRetention` controls prompt-cache retention on providers that support it (Anthropic: `long` -> 1h cache TTL where the model supports it, `short` -> the default 5m TTL, `none` -> no caching). Precedence: the `cacheRetention` option (or the `--cache-retention` CLI flag), then the `PI_CACHE_RETENTION` environment variable, then the built-in default `long`. Short-lived sessions such as subagents should pass `short` since a 1h cache write costs more than a 5m one and rarely pays off for a single run.
422
427
 
423
- Scramjet builds the system prompt as ordered sections and isolates the volatile environment tail (current date, working directory) into a final uncached section. On Anthropic, the stable sections are folded into a single system block carrying the cache breakpoint, so the cached prefix survives across turns even though the tail varies between sessions. Extensions can contribute stable sections via `before_agent_start` — see [extensions.md](extensions.md#system-prompt-sections).
428
+ Scramjet builds the system prompt as ordered sections and isolates the volatile environment tail (current date, working directory, session journal path) into a final uncached section. On Anthropic, the stable sections are folded into a single system block carrying the cache breakpoint, so the cached prefix survives across turns even though the tail varies between sessions. Extensions can contribute stable sections via `before_agent_start` — see [extensions.md](extensions.md#system-prompt-sections).
429
+
430
+ Within one accepted top-level prompt, changing the active tools regenerates the generated tool guidance while preserving that run's `before_agent_start` composition. Contributed sections are reapplied once against the regenerated base; an authoritative `systemPrompt` string remains byte-identical and receives no generated guidance. The same composition remains in effect for intra-run continuations and automatic retries. Each later accepted top-level prompt runs `before_agent_start` against a fresh generated base, and a successful same-session reload clears the prior extension instance's composition before rebuilding the runtime. Tree navigation changes messages only; the next accepted prompt performs the normal fresh composition.
431
+
432
+ For each built-in provider request, `createAgentSession()` binds the current extension runner when `before_provider_call` begins, then carries that runner through provider-neutral preparation, serialization, payload rewriting, and observation. It also captures a request-local copy of provider-neutral tool names, a bounded routed-model identity (`provider`, `id`, and `api` only), and the run-captured public `Agent.onPayload` callback without changing that callback's signature. Payload processing order is public callback → captured runner's `before_provider_request` chain → immutable `provider_request_tool_inventory` observation → transport. The public callback runs exactly once; `undefined` keeps its input unchanged, while any other return value becomes the input to extension handlers. If successful reload supersedes the captured runner before payload dispatch, the public callback still runs but the old request skips extension rewriting and observation rather than entering the replacement runtime; if replacement occurs while the old runner's chain is already in flight, its completed replacement reaches transport and only observation is suppressed. The wrapper inspects only tool names from the actual final payload and returns that exact same value to transport. Known malformed shapes expose only a bounded reason, custom APIs are `unsupported`, and custom providers that never invoke `onPayload` provide no final-schema proof. The observation does not prove transport or model behavior; see [extensions.md](extensions.md#provider_request_tool_inventory). Because this adds a member to the exported `ExtensionEvent` union, exhaustive TypeScript switches must handle it or include a default/unknown-event branch.
424
433
 
425
434
  > Before sectioned system prompts landed, the effective default was `short` (the env var could only upgrade it to `long`). Sessions created through the SDK or CLI now default to `long`; set `PI_CACHE_RETENTION=short` or pass `cacheRetention: "short"` to restore the old behavior.
426
435
 
427
436
  > See [examples/sdk/02-custom-model.ts](../examples/sdk/02-custom-model.ts)
428
437
 
438
+ ### Context and output allocation
439
+
440
+ Models and usage expose one authoritative total `contextWindow`; `contextWindowBudget` and `getContextWindowBudget()` are removed. See [the migration guide](models.md#breaking-migration-from-context-budgets) before updating external model definitions or consumers.
441
+
442
+ The `createAgentSession()` stream path checks the current request's estimated input against any genuine `maxInputTokens`, then bounds output by remaining total context and the applicable output maximum. It does not subtract compaction reserve again. The same accounting applies to both built-in summarizer implementations. These are approximate checks, not exact serialized-payload tokenization; custom stream functions and later payload rewrites must honor their own constraints.
443
+
444
+ OpenRouter's implicit aggregate output maximum is omitted because it can exclude the long-context endpoint; explicit summary output limits are retained. Vercel's Anthropic route still requires an output limit. Codex still omits `max_output_tokens`. Aggregate route compatibility is not guaranteed by scalar metadata. Anthropic/Bedrock simple adapters treat explicit `maxTokens` as a combined output ceiling including thinking, rather than increasing it for reasoning.
445
+
429
446
  ### API Keys and OAuth
430
447
 
431
448
  API key resolution priority (handled by AuthStorage):
@@ -652,6 +669,8 @@ const { session } = await createAgentSession({ resourceLoader: loader });
652
669
 
653
670
  ### Context Files
654
671
 
672
+ Default discovery loads context broadest-to-most-specific and orders `CLAUDE.md` before `AGENTS.md` within each directory. When both same-directory files decode to exactly equal text, only `CLAUDE.md` is retained; equality suppression never crosses directory boundaries.
673
+
655
674
  ```typescript
656
675
  import { createAgentSession, DefaultResourceLoader } from "@leanandmean/coding-agent";
657
676
 
@@ -81,6 +81,7 @@ interface UserMessage {
81
81
  interface AssistantMessage {
82
82
  role: "assistant";
83
83
  content: (TextContent | ThinkingContent | ToolCall)[];
84
+ origin?: "provider" | "harness"; // Explicit for new messages; absent in legacy sessions
84
85
  api: string;
85
86
  provider: string;
86
87
  model: string;
@@ -203,7 +204,7 @@ A message in the conversation. The `message` field contains an `AgentMessage`.
203
204
 
204
205
  ```json
205
206
  {"type":"message","id":"a1b2c3d4","parentId":"prev1234","timestamp":"2024-12-03T14:00:01.000Z","message":{"role":"user","content":"Hello"}}
206
- {"type":"message","id":"b2c3d4e5","parentId":"a1b2c3d4","timestamp":"2024-12-03T14:00:02.000Z","message":{"role":"assistant","content":[{"type":"text","text":"Hi!"}],"provider":"anthropic","model":"claude-sonnet-4-5","usage":{...},"stopReason":"stop"}}
207
+ {"type":"message","id":"b2c3d4e5","parentId":"a1b2c3d4","timestamp":"2024-12-03T14:00:02.000Z","message":{"role":"assistant","content":[{"type":"text","text":"Hi!"}],"origin":"provider","provider":"anthropic","model":"claude-sonnet-4-5","usage":{...},"stopReason":"stop"}}
207
208
  {"type":"message","id":"c3d4e5f6","parentId":"b2c3d4e5","timestamp":"2024-12-03T14:00:03.000Z","message":{"role":"toolResult","toolCallId":"call_123","toolName":"bash","content":[{"type":"text","text":"output"}],"isError":false}}
208
209
  ```
209
210