sneakoscope 10.3.7 → 10.3.8

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 (63) hide show
  1. package/README.md +12 -181
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/commands/bridge.js +61 -1
  5. package/dist/commands/codex-app.js +4 -1
  6. package/dist/config/skills-manifest.json +1 -1
  7. package/dist/core/agents/native-worker-backend-router.js +55 -0
  8. package/dist/core/codex-app/codex-app-restart-policy.js +20 -0
  9. package/dist/core/codex-app/codex-context-window.js +61 -24
  10. package/dist/core/codex-app/menubar/constants.js +4 -0
  11. package/dist/core/codex-compat/codex-hook-output-builders.js +3 -1
  12. package/dist/core/codex-lb/bridge-cli-contract.js +2 -0
  13. package/dist/core/codex-lb/desktop-bridge/exclusive-provider-guard.js +59 -0
  14. package/dist/core/codex-lb/desktop-bridge/http-forward.js +23 -15
  15. package/dist/core/codex-lb/desktop-bridge/index.js +1 -0
  16. package/dist/core/codex-lb/desktop-bridge/responses-websocket.js +10 -3
  17. package/dist/core/codex-lb/desktop-bridge/security.js +63 -1
  18. package/dist/core/codex-lb/desktop-bridge/server.js +5 -0
  19. package/dist/core/codex-lb/desktop-bridge/state.js +3 -0
  20. package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +5 -1
  21. package/dist/core/codex-lb/desktop-controller-v3/catalog.js +31 -5
  22. package/dist/core/codex-lb/desktop-controller-v3/lifecycle-commands.js +23 -4
  23. package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-catalog.js +39 -0
  24. package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-commands.js +286 -0
  25. package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-main-model.js +101 -0
  26. package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-status.js +68 -0
  27. package/dist/core/codex-lb/desktop-controller-v3/status.js +8 -5
  28. package/dist/core/codex-lb/desktop-controller-v3.js +26 -2
  29. package/dist/core/codex-lb/desktop-service.js +13 -2
  30. package/dist/core/commands/naruto-command.js +1 -1
  31. package/dist/core/decisions/child-model-choice.js +107 -0
  32. package/dist/core/decisions/cli.js +19 -2
  33. package/dist/core/decisions/routing.js +5 -4
  34. package/dist/core/hooks-runtime/hook-context.js +21 -7
  35. package/dist/core/hooks-runtime/hook-io.js +3 -2
  36. package/dist/core/hooks-runtime/jev-spawn-routing.js +83 -5
  37. package/dist/core/hooks-runtime/official-subagent-lifecycle.js +3 -2
  38. package/dist/core/hooks-runtime/parent-orchestration-gate.js +18 -1
  39. package/dist/core/hooks-runtime/subagent-context.js +13 -2
  40. package/dist/core/hooks-runtime/subagent-spawn-policy.js +49 -6
  41. package/dist/core/hooks-runtime.js +12 -5
  42. package/dist/core/managed-assets/managed-assets-manifest.js +9 -4
  43. package/dist/core/subagents/child-model-allowlist.js +179 -0
  44. package/dist/core/subagents/child-model-plan.js +117 -0
  45. package/dist/core/subagents/naruto-host-credentials.js +82 -13
  46. package/dist/core/subagents/official-subagent-config.js +66 -15
  47. package/dist/core/subagents/official-subagent-preparation.js +45 -12
  48. package/dist/core/subagents/official-subagent-prompt.js +82 -11
  49. package/dist/core/subagents/official-subagent-runner.js +3 -0
  50. package/dist/core/subagents/read-only-list-role.js +73 -0
  51. package/dist/core/subagents/role-model-preferences.js +54 -17
  52. package/dist/core/version.js +1 -1
  53. package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +2 -0
  54. package/dist/native/sks-menubar/Sources/OpenRouterOnlyState.swift +314 -0
  55. package/dist/native/sks-menubar/Sources/ProcessClient.swift +2 -1
  56. package/dist/native/sks-menubar/Sources/ProvidersModelExposure.swift +15 -3
  57. package/dist/native/sks-menubar/Sources/ProvidersOpenRouterOnly.swift +162 -0
  58. package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +36 -18
  59. package/dist/native/sks-menubar/Sources/SettingsViewController.swift +10 -4
  60. package/dist/native/sks-menubar/Sources/SidebarItem.swift +2 -0
  61. package/dist/native/sks-menubar/Sources/SubagentModelsModels.swift +201 -0
  62. package/dist/native/sks-menubar/Sources/SubagentModelsViewController.swift +373 -0
  63. package/package.json +1 -1
package/README.md CHANGED
@@ -16,7 +16,7 @@
16
16
  Sneakoscope Codex (`sks`) is an open-source trust layer for Codex CLI and ChatGPT Desktop. It coordinates bounded AI coding agents, records machine-verifiable evidence, preserves project memory, and blocks release claims that are not supported by current tests or artifacts. Search visibility outcomes are measured separately; SKS does not promise rankings or traffic.
17
17
  <!-- END SKS SEARCH VISIBILITY MARKETING -->
18
18
 
19
- Current package: **SKS 10.3.7**. Install the latest stable release from npm.
19
+ Current package: **SKS 10.3.8**. Install the latest stable release from npm.
20
20
 
21
21
  [Quick start](#install-in-one-command) · [Commands](#everyday-commands) · [SKS Center](#sks-center-macos) · [Documentation](#documentation) · [Changelog](CHANGELOG.md)
22
22
 
@@ -101,7 +101,8 @@ home directory; set `SKS_POSTINSTALL_MENUBAR=1` to force the build in a script,
101
101
  | Page | Use it to |
102
102
  | --- | --- |
103
103
  | Overview | Check local health and find the next action. |
104
- | Connections | Connect Codex-LB or OpenRouter, choose authentication priority, and manage exposed models. |
104
+ | Connections | Connect Codex-LB or OpenRouter, choose Prefer Codex-LB or OpenRouter Only, and manage exposed models. |
105
+ | Subagent Models | Keep the OpenRouter models subagents may use in OpenRouter Only Mode, with the criteria Jev routes by. |
105
106
  | Updates | Check and update SKS and Codex CLI, with operation progress and recovery details. |
106
107
  | MCP Servers | Manage server configuration, health checks, and authentication. |
107
108
  | Image Generation | Keep Codex's default image generation, or route it through the Desktop Bridge to an OpenRouter image model you pick. |
@@ -123,184 +124,11 @@ connection first for eligible models. The switch distinguishes a saved preferenc
123
124
  from connection readiness: **on but unavailable** means setup needs attention.
124
125
  Turning it off restores the configured official-model routing preference.
125
126
 
126
- The same controls are available from the CLI:
127
-
128
- ```sh
129
- sks bridge auth-priority status --json
130
- sks bridge auth-priority on --json
131
- sks bridge auth-priority off --json
132
- sks bridge status --json
133
- sks bridge route explain gpt-6-astra --json
134
- ```
135
-
136
- Enter credentials through the native connection dialog or the CLI's
137
- `--api-key-stdin` option. See the [provider guide](docs/codex-lb.md) for
138
- configuration, transport checks, and recovery commands.
139
-
140
- ## Image generation
141
-
142
- SKS image routes (image generation, UX-Review callouts, PPT assets, slide
143
- reviews) follow one image mode. By default they use Codex's own image
144
- generation. Turn on a custom image model on the **Image Generation** page to
145
- send every SKS image through the Desktop Bridge to an OpenRouter image model
146
- you pick. SKS pins no image model and records which model made each image.
147
-
148
- ```sh
149
- sks imagegen status --json
150
- sks imagegen models --json
151
- sks imagegen enable --model black-forest-labs/flux.2-klein-4b
152
- sks imagegen generate --prompt "App icon, flat, blue" --out icon.png
153
- sks imagegen disable
154
- ```
155
-
156
- See [Image generation](docs/image-generation.md) for both modes, evidence
157
- sidecars, and the Jev choices.
158
-
159
- ## Naruto workflow
160
-
161
- The parent orchestrates: it decomposes the task, spawns a child for each
162
- disjoint slice, waits, and owns integration and final verification. It does not
163
- implement slices itself. The SKS PreToolUse hook denies parent source edits
164
- until the first child starts and while children are still running. Children
165
- receive bounded tasks and do not spawn children.
166
-
167
- No model family is pinned. Every child runs the newest model of the tier its
168
- work needs, read from the Codex models cache, so a new model family is used as
169
- soon as Codex lists it:
170
-
171
- | Work | Tier | Effort | Today |
172
- | --- | --- | --- | --- |
173
- | Tiny mechanical tasks | fast | low | `gpt-6-luna` |
174
- | Instructed implementation | balanced | low | `gpt-6-sol` |
175
- | Exploration, large-context reads, and direct tool operation | context | medium | `gpt-6-sol` |
176
- | Planning, review, debugging, and focused judgment | deep | max | `gpt-6-astra` |
177
-
178
- With Jev mode on, Jev picks the tier for every spawn, every gated parent edit,
179
- the plan, and the context, so the parent spends no time on those choices. It
180
- also picks the pipeline for each prompt; an explicit SKS `<div align="center">
181
-
182
- <img src="docs/assets/sks-logo.svg" alt="Sneakoscope Codex logo" width="120" height="120" />
183
-
184
- # Sneakoscope Codex
185
-
186
- **Plan, build, and verify with Codex.**
187
-
188
- [![npm version](https://img.shields.io/npm/v/sneakoscope?color=cb3837&logo=npm)](https://www.npmjs.com/package/sneakoscope)
189
- [![node](https://img.shields.io/badge/node-%3E%3D20.11-339933?logo=node.js&logoColor=white)](#requirements)
190
- [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
191
-
192
- </div>
193
-
194
- <!-- BEGIN SKS SEARCH VISIBILITY MARKETING -->
195
- Sneakoscope Codex (`sks`) is an open-source trust layer for Codex CLI and ChatGPT Desktop. It coordinates bounded AI coding agents, records machine-verifiable evidence, preserves project memory, and blocks release claims that are not supported by current tests or artifacts. Search visibility outcomes are measured separately; SKS does not promise rankings or traffic.
196
- <!-- END SKS SEARCH VISIBILITY MARKETING -->
197
-
198
- Current package: **SKS 10.3.7**. Install the latest stable release from npm.
199
-
200
- [Quick start](#install-in-one-command) · [Commands](#everyday-commands) · [SKS Center](#sks-center-macos) · [Documentation](#documentation) · [Changelog](CHANGELOG.md)
201
-
202
- ## Install in one command
203
-
204
- ```sh
205
- npm exec --yes --package=sneakoscope@latest -- sneakoscope install --yes
206
- ```
207
-
208
- The installer resolves the latest release, installs it globally, runs setup and
209
- Doctor, and checks that `sks` on your PATH points to the installed version.
210
-
211
- To set up a project, run this from its root, then open it in Codex:
212
-
213
- ```sh
214
- sks bootstrap --yes
215
- ```
216
-
217
- ## What SKS adds
218
-
219
- | Capability | What you get |
220
- | --- | --- |
221
- | Focused execution | Answers and tiny edits stay lightweight; implementation work runs through official Codex subagents while the parent orchestrates and integrates. |
222
- | Project context | TriWiki indexes repository code and supplies bounded context that can be checked against source. |
223
- | Verification | Tests, diagnostics, and release evidence support completion claims. Security and data-integrity checks stay in place. |
224
- | Native controls | SKS Center brings connections, updates, MCP servers, and diagnostics together on macOS. |
225
- | Consistent setup | `sks update` installs the latest release, runs Doctor, and reconciles SKS-managed files and legacy assets. |
226
-
227
- The default `essential` profile avoids repetitive completion rituals. `strict`
228
- adds stronger completion requirements. See [Essential Trust](docs/essential-trust.md).
229
-
230
- With Codex-LB connected, `sks agent-bridge async --prompt "Check SKS status while
231
- explaining what the check covers" --tools status --json` runs native Astra Async
232
- tool calling with selected read-only SKS tools, using WebSocket first and safe
233
- HTTP fallback. See [Astra guidance](docs/astra-guidance.md)
234
- for setup boundaries and reported execution evidence.
235
-
236
- ## Everyday commands
237
-
238
- SKS enables experimental Astra context management by default during setup and
239
- repair. Turn it off in **SKS Center → Settings → Astra context management**, or
240
- use `sks codex-app context-management off`. Updates preserve an explicit opt-out.
241
- Start a new task after changing the setting. Availability depends on a supported
242
- Codex client and eligible ChatGPT sign-in; API-key and custom-provider sessions
243
- may not activate it. See [OpenAI's context management guidance](https://learn.chatgpt.com/docs/models#experimental-context-management).
244
-
245
- Use these inside a Codex conversation:
246
-
247
- | Command | Purpose |
248
- | --- | --- |
249
- | `$sks-plan "task"` | Create a plan without editing product code. |
250
- | `$sks-work` | Execute the latest plan. |
251
- | `$sks-naruto "task"` | Run an official subagent workflow. |
252
- | `$sks-review` | Review the current changes. |
253
- | `$sks-help` | Explore available SKS workflows. |
254
-
255
- Use these in your terminal:
256
-
257
- ```sh
258
- sks --help
259
- sks status --json
260
- sks review --staged
261
- sks doctor --json
262
- sks update-check
263
- sks update
264
- ```
265
-
266
- `sks update` runs setup reconciliation even when the package is already current.
267
- It preserves user-authored configuration and cleans up recognized SKS-owned
268
- legacy assets. If a check needs attention, follow its reported recovery action
269
- in your terminal.
270
-
271
- ## SKS Center (macOS)
272
-
273
- Open **SKS Control Center** from the SKS menu bar.
274
- The Menu Bar app (with the Control Center) is built from source on macOS when you run
275
- `npm i -g sneakoscope` interactively in a terminal, and by `sks install`, `sks doctor --fix`,
276
- `sks update`, or `sks menubar install`. Dependency, CI, and piped installs never touch your
277
- home directory; set `SKS_POSTINSTALL_MENUBAR=1` to force the build in a script, or
278
- `SKS_POSTINSTALL_NO_MENUBAR=1` to skip it.
279
-
280
- | Page | Use it to |
281
- | --- | --- |
282
- | Overview | Check local health and find the next action. |
283
- | Connections | Connect Codex-LB or OpenRouter, choose authentication priority, and manage exposed models. |
284
- | Updates | Check and update SKS and Codex CLI, with operation progress and recovery details. |
285
- | MCP Servers | Manage server configuration, health checks, and authentication. |
286
- | Image Generation | Keep Codex's default image generation, or route it through the Desktop Bridge to an OpenRouter image model you pick. |
287
- | Diagnostics | Inspect issues and run targeted checks. |
288
- | Settings | Configure lifecycle behavior, notifications, and advanced options. |
289
- | Remote Coding | Find the independent [Paseo companion](https://paseo.sh/docs) and its setup guidance. |
290
-
291
- Connection controls appear first. Model catalogs, bridge diagnostics, and
292
- advanced settings expand when you need them.
293
-
294
- ## Desktop Bridge
295
-
296
- One local bridge manages routing for independent **Codex-LB** and **OpenRouter**
297
- profiles. ChatGPT sign-in remains owned by Codex. Provider choices and existing
298
- session pins remain authoritative; unavailable routes are reported explicitly.
299
-
300
- In **Connections**, turn on **Prefer Codex-LB** to use the saved Codex-LB
301
- connection first for eligible models. The switch distinguishes a saved preference
302
- from connection readiness: **on but unavailable** means setup needs attention.
303
- Turning it off restores the configured official-model routing preference.
127
+ Turn on **OpenRouter Only** instead to run the main thread and every subagent on
128
+ OpenRouter models. The two switches are mutually exclusive: turning one on turns
129
+ the other off. Subagents may use only the models on the **Subagent Models** page,
130
+ and with Jev mode on, Jev picks one for each child from the criteria you wrote.
131
+ See [OpenRouter Only Mode](docs/openrouter-only-mode.md).
304
132
 
305
133
  The same controls are available from the CLI:
306
134
 
@@ -308,6 +136,8 @@ The same controls are available from the CLI:
308
136
  sks bridge auth-priority status --json
309
137
  sks bridge auth-priority on --json
310
138
  sks bridge auth-priority off --json
139
+ sks bridge openrouter-only on --json
140
+ sks bridge subagent-models list --json
311
141
  sks bridge status --json
312
142
  sks bridge route explain gpt-6-astra --json
313
143
  ```
@@ -356,7 +186,7 @@ soon as Codex lists it:
356
186
 
357
187
  With Jev mode on, Jev picks the tier for every spawn, every gated parent edit,
358
188
  the plan, and the context, so the parent spends no time on those choices. It
359
- command always
189
+ also picks the pipeline for each prompt; an explicit SKS dollar command always
360
190
  wins. A stored role-model preference on a current model wins in both modes.
361
191
 
362
192
  An active Codex task keeps the user's selected main model, effort, and service
@@ -393,6 +223,7 @@ freshness semantics.
393
223
  - [Astra guidance](docs/astra-guidance.md) — how SKS applies the official model recommendations.
394
224
  - [Agent Bridge](docs/AGENT-BRIDGE.md) — integrate through the CLI or MCP interface.
395
225
  - [Codex-LB priority](docs/codex-lb-priority.md) — how Codex App WebSockets follow the Center priority setting.
226
+ - [OpenRouter Only Mode](docs/openrouter-only-mode.md) — run everything on OpenRouter with a subagent model list Jev routes by.
396
227
  - [Release readiness](docs/release-readiness.md) — build, verify, and publish a release.
397
228
  - [Release evidence](docs/release-proof-truth.md) — what each verification result proves.
398
229
  - [Changelog](CHANGELOG.md) — changes by version.
@@ -259,7 +259,7 @@ dependencies = [
259
259
 
260
260
  [[package]]
261
261
  name = "sks-core"
262
- version = "10.3.7"
262
+ version = "10.3.8"
263
263
  dependencies = [
264
264
  "globset",
265
265
  "grep-matcher",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sks-core"
3
- version = "10.3.7"
3
+ version = "10.3.8"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -5,6 +5,7 @@ import { serveDesktopBridge } from '../core/codex-lb/desktop-service.js';
5
5
  import { BridgeCliError, errorOutput, mergeMetadata, ordinaryOutput, sanitizeBridgeValue, textSummary, verificationOutput } from './bridge-command-output.js';
6
6
  const CONTROLLER_FACADE_EXPORT = 'executeDesktopBridgeCommand';
7
7
  const MAX_STDIN_SECRET_BYTES = 64 * 1024;
8
+ const MAX_STDIN_JSON_BYTES = 64 * 1024;
8
9
  const DEFAULT_IO = {
9
10
  readStdin,
10
11
  stdout: (text) => process.stdout.write(text),
@@ -18,6 +19,9 @@ export function usage(command = 'bridge') {
18
19
  `Usage: sks ${command} status [--json]`,
19
20
  ` sks ${command} ensure|repair [--json]`,
20
21
  ` sks ${command} auth-priority status|on|off [--json]`,
22
+ ` sks ${command} openrouter-only status|on|off [--no-restart] [--json]`,
23
+ ` sks ${command} subagent-models list [--json]`,
24
+ ` sks ${command} subagent-models set --stdin [--no-restart] [--json]`,
21
25
  ` sks ${command} verify --level shallow|transport|deep [--strict|--require-ready] [--json]`,
22
26
  ` sks ${command} provider list [--json]`,
23
27
  ` sks ${command} provider configure codex-lb --host <host> --api-key-stdin [--json]`,
@@ -34,7 +38,9 @@ export function usage(command = 'bridge') {
34
38
  ` sks ${command} unmanage --confirm [--json]`,
35
39
  ` sks ${command} rollback <receipt-id> --confirm [--json]`,
36
40
  '',
37
- 'Provider secrets are accepted only through --api-key-stdin. Readiness changes the exit code only with --strict or --require-ready.'
41
+ 'Provider secrets are accepted only through --api-key-stdin. Readiness changes the exit code only with --strict or --require-ready.',
42
+ 'OpenRouter Only Mode and Codex-LB authentication priority are mutually exclusive; turning one on turns the other off.',
43
+ 'subagent-models set reads {"subagent_models":[{"model","criteria","reasoning_effort","default"}]} (max 16) from stdin.'
38
44
  ].join('\n');
39
45
  }
40
46
  export async function run(_command, args = []) {
@@ -120,6 +126,39 @@ async function parseInvocation(args, io) {
120
126
  ? { operation: 'auth-priority.status' }
121
127
  : { operation: 'auth-priority.set', enabled: action === 'on' }, label: 'Codex-LB authentication priority' };
122
128
  }
129
+ if (area === 'openrouter-only' && target === undefined && (action === 'status' || action === 'on' || action === 'off')) {
130
+ if (action === 'status') {
131
+ allowOnly(parsed, ['--json'], []);
132
+ return { ...base, request: { operation: 'openrouter-only.status' }, label: 'OpenRouter Only Mode' };
133
+ }
134
+ allowOnly(parsed, ['--json', '--no-restart'], []);
135
+ return {
136
+ ...base,
137
+ request: { operation: 'openrouter-only.set', enabled: action === 'on', no_restart: parsed.flags.has('--no-restart') },
138
+ label: 'OpenRouter Only Mode'
139
+ };
140
+ }
141
+ if (area === 'subagent-models' && target === undefined) {
142
+ if (action === 'list') {
143
+ allowOnly(parsed, ['--json'], []);
144
+ return { ...base, request: { operation: 'subagent-models.list' }, label: 'Subagent model list' };
145
+ }
146
+ if (action === 'set') {
147
+ allowOnly(parsed, ['--json', '--stdin', '--no-restart'], []);
148
+ if (!parsed.flags.has('--stdin')) {
149
+ throw new BridgeCliError('bridge_subagent_models_stdin_required', 'retry_with_stdin');
150
+ }
151
+ return {
152
+ ...base,
153
+ request: {
154
+ operation: 'subagent-models.set',
155
+ subagent_models: subagentModelsFromStdin(await io.readStdin()),
156
+ no_restart: parsed.flags.has('--no-restart')
157
+ },
158
+ label: 'Subagent model list update'
159
+ };
160
+ }
161
+ }
123
162
  if (area === 'status' && action === undefined) {
124
163
  allowOnly(parsed, ['--json'], []);
125
164
  return { ...base, request: { operation: 'status' }, label: 'Desktop Bridge status' };
@@ -318,6 +357,27 @@ function parseArgs(args) {
318
357
  }
319
358
  return { positionals, flags, values };
320
359
  }
360
+ function subagentModelsFromStdin(text) {
361
+ const raw = String(text || '').trim();
362
+ if (!raw)
363
+ throw new BridgeCliError('bridge_subagent_models_stdin_empty', 'retry_with_stdin');
364
+ if (Buffer.byteLength(raw, 'utf8') > MAX_STDIN_JSON_BYTES) {
365
+ throw new BridgeCliError('bridge_subagent_models_stdin_too_large', 'retry_with_stdin');
366
+ }
367
+ let parsed;
368
+ try {
369
+ parsed = JSON.parse(raw);
370
+ }
371
+ catch {
372
+ throw new BridgeCliError('bridge_subagent_models_stdin_invalid_json', 'retry_with_stdin');
373
+ }
374
+ const models = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
375
+ ? parsed.subagent_models
376
+ : undefined;
377
+ if (!Array.isArray(models))
378
+ throw new BridgeCliError('bridge_subagent_models_stdin_invalid', 'retry_with_stdin');
379
+ return models;
380
+ }
321
381
  function rejectSecretArgv(args) {
322
382
  for (const arg of args) {
323
383
  const option = String(arg).split('=', 1)[0]?.toLowerCase() || '';
@@ -38,7 +38,10 @@ export async function run(_command, args = []) {
38
38
  }
39
39
  console.log(`Codex 1M Context: ${result.enabled ? 'enabled' : 'disabled'}`);
40
40
  console.log(`Config: ${result.config_path}`);
41
- console.log(`Model: ${result.model || 'not set'} (documented for ${result.expected_model})`);
41
+ const window = result.model_window;
42
+ console.log(`Model: ${result.model || 'not set'}${window.effective_window ? ` · window ${window.effective_window} (default ${window.default_window ?? 'unknown'}, max ${window.max_window ?? 'uncapped'})` : ' · window unknown'}`);
43
+ if (result.larger_window_models.length)
44
+ console.log(`Models with a larger window: ${result.larger_window_models.join(', ')}`);
42
45
  for (const [key, target] of Object.entries(result.target)) {
43
46
  const state = result.keys[key];
44
47
  const detail = state?.present ? `${state.value ?? 'unparsed'}${state.managed ? ' (SKS-managed)' : ''}` : 'not set';
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema": "sks.skills-manifest.v1",
3
- "package_version": "10.3.7",
3
+ "package_version": "10.3.8",
4
4
  "skills": [
5
5
  {
6
6
  "canonical_name": "sks",
@@ -13,6 +13,8 @@ import { codexTimeoutClassForRoute } from '../codex-control/codex-reliability-sh
13
13
  import { decideSubagentModel } from '../subagents/model-policy.js';
14
14
  import { latestTierModelSet } from '../subagents/model-tiers.js';
15
15
  import { consultJevTurnModel } from '../decisions/integration.js';
16
+ import { chooseChildModel, fallbackChildModel } from '../decisions/child-model-choice.js';
17
+ import { OPENROUTER_ONLY_SCHEMA, allowlistedChildModel, effectiveChildModelAllowlist, isSubagentModelEffort } from '../subagents/child-model-allowlist.js';
16
18
  export const NATIVE_WORKER_BACKEND_ROUTER_SCHEMA = 'sks.native-worker-backend-router.v1';
17
19
  export async function runNativeWorkerBackendRouter(input) {
18
20
  const root = path.resolve(input.agentRoot);
@@ -229,6 +231,10 @@ export async function resolveWorkerModelRouting(input, deps = {}) {
229
231
  const riskText = [taskKindText, input.slice?.risk, input.slice?.risk_focus, input.slice?.acceptance, input.slice?.parent_prompt, input.slice?.description, input.intake?.prompt].map((value) => String(value || '')).join(' ');
230
232
  const category = categoryForWorkerRole(String(input.agent?.role || 'executor'), taskKindText);
231
233
  const env = deps.env || process.env;
234
+ const allowlist = effectiveChildModelAllowlist({ env });
235
+ if (allowlist.mode === 'openrouter_only') {
236
+ return resolveOpenRouterOnlyWorkerRouting(input, deps, { allowlist, category, env, taskKindText, riskText });
237
+ }
232
238
  const lbHealth = Object.prototype.hasOwnProperty.call(deps, 'lbHealth') ? deps.lbHealth : await readLbHealth().catch(() => null);
233
239
  const lbCatalog = narutoOnly
234
240
  ? Object.prototype.hasOwnProperty.call(deps, 'lbCatalog')
@@ -305,6 +311,55 @@ export async function resolveWorkerModelRouting(input, deps = {}) {
305
311
  })
306
312
  };
307
313
  }
314
+ async function resolveOpenRouterOnlyWorkerRouting(input, deps, ctx) {
315
+ const { allowlist, category, env } = ctx;
316
+ const state = { schema: OPENROUTER_ONLY_SCHEMA, enabled: true, subagent_models: allowlist.entries, restore: null, updated_at: null };
317
+ const explicitModel = String(env.SKS_WORKER_MODEL || env.SKS_AGENT_MODEL || '').trim();
318
+ const explicitReasoning = listReasoning(env.SKS_WORKER_REASONING || env.SKS_WORKER_MODEL_REASONING);
319
+ const explicitTier = normalizeServiceTier(String(env.SKS_WORKER_SERVICE_TIER || env.SKS_SERVICE_TIER || '').trim());
320
+ const planned = /^openrouter_only_/.test(String(input.agent?.routed_model_policy || ''))
321
+ ? allowlistedChildModel(input.agent?.routed_model, allowlist)
322
+ : null;
323
+ const requested = String(input.agent?.routed_model || input.agent?.model || '').trim() || null;
324
+ const explicitListed = explicitModel ? allowlistedChildModel(explicitModel, allowlist) : null;
325
+ const task = `${ctx.taskKindText}\n${ctx.riskText}`.trim().slice(0, 1200);
326
+ const choice = explicitModel
327
+ ? (explicitListed ? fallbackChildModel(state, explicitListed, 'explicit') : null)
328
+ : planned
329
+ ? fallbackChildModel(state, planned, 'planned')
330
+ : deps.consultJev === false || !task
331
+ ? fallbackChildModel(state, requested, deps.consultJev === false ? 'jev_skipped' : 'empty_task')
332
+ : await chooseChildModel({ root: deps.root || process.cwd(), task, role: input.agent?.role || null, requestedModel: requested, state, env })
333
+ .catch(() => fallbackChildModel(state, requested, 'consult_failed'));
334
+ const taskPolicy = decideSubagentModel({ title: ctx.taskKindText, description: ctx.riskText, role: input.agent?.role });
335
+ const choiceModel = choice?.entry.model || '';
336
+ const routed = {
337
+ model: choiceModel,
338
+ reasoning: explicitReasoning || choice?.entry.reasoning_effort || listReasoning(taskPolicy.modelReasoningEffort) || 'medium',
339
+ serviceTier: explicitTier || input.fastModePolicy.service_tier || 'fast'
340
+ };
341
+ const blockers = [
342
+ ...(!allowlist.entries.length ? ['openrouter_only_subagent_list_empty'] : []),
343
+ ...(explicitModel && !explicitListed ? ['openrouter_only_subagent_model_not_listed'] : []),
344
+ ...(!choiceModel ? ['naruto_worker_model_unavailable'] : [])
345
+ ];
346
+ const why = choice ? `${choice.source}${choice.reason === 'applied' ? '' : `:${choice.reason}`}` : 'blocked';
347
+ return {
348
+ category,
349
+ choice: routed,
350
+ explicit: Boolean(explicitModel),
351
+ lb_health: Object.prototype.hasOwnProperty.call(deps, 'lbHealth') ? deps.lbHealth : null,
352
+ lb_catalog: null,
353
+ blockers: [...new Set(blockers)],
354
+ reason: `${category}->${choiceModel || 'blocked'}@${routed.reasoning} (openrouter only list: ${why})`
355
+ };
356
+ }
357
+ function listReasoning(value) {
358
+ const text = String(value || '').trim().toLowerCase();
359
+ if (text === 'max' || text === 'ultra')
360
+ return 'xhigh';
361
+ return isSubagentModelEffort(text) ? text : null;
362
+ }
308
363
  function normalizeModelReasoning(value) {
309
364
  const text = String(value || '').toLowerCase();
310
365
  return text === 'low' || text === 'medium' || text === 'high' || text === 'xhigh' || text === 'max' || text === 'ultra' ? text : null;
@@ -0,0 +1,20 @@
1
+ import { isCodexAppRunningByBundleId } from './menubar/config.js';
2
+ import { restartCodexApp } from './codex-app-restart.js';
3
+ export async function maybeRestartRunningCodexApp(input) {
4
+ const skipped = (reason) => ({ attempted: false, running: null, status: 'skipped', reason, ok: true, blockers: [] });
5
+ if (input.noRestart)
6
+ return skipped('no_restart_flag');
7
+ if (!input.changed)
8
+ return skipped('config_unchanged');
9
+ if (input.env.SKS_SKIP_CODEX_APP_RESTART === '1')
10
+ return skipped('SKS_SKIP_CODEX_APP_RESTART');
11
+ if ((input.platform || process.platform) !== 'darwin')
12
+ return skipped('not_macos');
13
+ const bundleId = String(input.env.SKS_CODEX_APP_BUNDLE_ID || 'com.openai.codex');
14
+ const running = await (input.isRunningImpl || isCodexAppRunningByBundleId)(bundleId, input.env);
15
+ if (!running) {
16
+ return { attempted: false, running: false, status: 'skipped', reason: 'codex_not_running', ok: true, blockers: [] };
17
+ }
18
+ const result = await (input.restartImpl || restartCodexApp)({ env: input.env, ...(input.root ? { root: input.root } : {}) });
19
+ return { attempted: true, running: true, status: result.status, reason: null, ok: result.ok, blockers: [...result.blockers] };
20
+ }
@@ -1,16 +1,17 @@
1
+ import path from 'node:path';
1
2
  import { exists, readText } from '../fsx.js';
2
3
  import { escapeRegExp } from '../text/regex.js';
3
4
  import { safeWriteCodexConfigToml } from '../codex-runtime/codex-desktop-config-policy.js';
4
- import { codexUserConfigPath, readTopLevelTomlString } from './codex-model-catalog.js';
5
+ import { codexHomePath, codexUserConfigPath, readTopLevelTomlString } from './codex-model-catalog.js';
5
6
  import { isCodexAppRunningByBundleId } from './menubar/config.js';
6
7
  import { restartCodexApp } from './codex-app-restart.js';
8
+ import { maybeRestartRunningCodexApp } from './codex-app-restart-policy.js';
7
9
  export const CODEX_CONTEXT_1M_SCHEMA = 'sks.codex-context-1m.v1';
8
10
  export const CODEX_CONTEXT_1M_MARKER = 'sks-codex-context-1m';
9
11
  export const CODEX_CONTEXT_1M_TARGETS = {
10
12
  model_context_window: 1_000_000,
11
13
  model_auto_compact_token_limit: 900_000
12
14
  };
13
- export const CODEX_CONTEXT_1M_MODEL = 'gpt-5.6-sol';
14
15
  const MANAGED_KEYS = Object.keys(CODEX_CONTEXT_1M_TARGETS);
15
16
  function topLevelRegionEnd(lines) {
16
17
  const first = lines.findIndex((line) => /^\s*\[.+\]\s*$/.test(line));
@@ -64,6 +65,50 @@ export function inspectCodexContext1m(text) {
64
65
  const enabled = MANAGED_KEYS.every((key) => keys[key].managed && keys[key].value === CODEX_CONTEXT_1M_TARGETS[key]);
65
66
  return { enabled, model: readTopLevelTomlString(String(text || ''), 'model'), keys, warnings };
66
67
  }
68
+ function positiveInt(value) {
69
+ return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : null;
70
+ }
71
+ export async function readCodexModelWindows(input = {}) {
72
+ const text = await readText(path.join(codexHomePath(input), 'models_cache.json'), '');
73
+ let parsed = null;
74
+ try {
75
+ parsed = JSON.parse(String(text || 'null'));
76
+ }
77
+ catch {
78
+ return [];
79
+ }
80
+ const models = parsed?.models;
81
+ if (!Array.isArray(models))
82
+ return [];
83
+ return models.flatMap((row) => {
84
+ if (!row || typeof row !== 'object')
85
+ return [];
86
+ const record = row;
87
+ const slug = typeof record.slug === 'string' ? record.slug.trim() : '';
88
+ if (!slug)
89
+ return [];
90
+ return [{
91
+ slug,
92
+ listed: record.visibility !== 'hide',
93
+ default_window: positiveInt(record.context_window),
94
+ max_window: positiveInt(record.max_context_window)
95
+ }];
96
+ });
97
+ }
98
+ export function codexContextModelWindow(rows, model) {
99
+ const row = model ? rows.find((candidate) => candidate.slug === model) : undefined;
100
+ if (!row)
101
+ return { source: 'unknown', default_window: null, max_window: null, effective_window: null, extends: null };
102
+ const target = CODEX_CONTEXT_1M_TARGETS.model_context_window;
103
+ const effective = row.max_window === null ? target : Math.min(target, row.max_window);
104
+ return {
105
+ source: 'codex_models_cache',
106
+ default_window: row.default_window,
107
+ max_window: row.max_window,
108
+ effective_window: effective,
109
+ extends: row.default_window === null ? null : effective > row.default_window
110
+ };
111
+ }
67
112
  export function enableCodexContext1m(text) {
68
113
  const source = String(text || '');
69
114
  const lines = source.split('\n');
@@ -160,15 +205,21 @@ export async function codexContext1mCommand(args = [], opts = {}) {
160
205
  }
161
206
  const inspection = inspectCodexContext1m(afterText);
162
207
  warnings.push(...inspection.warnings);
208
+ const windowRows = await readCodexModelWindows({ env, ...(opts.home ? { home: opts.home } : {}) });
209
+ const modelWindow = codexContextModelWindow(windowRows, inspection.model);
163
210
  if (action === 'on' || inspection.enabled) {
164
211
  if (!inspection.model)
165
212
  warnings.push('codex_context_model_line_missing');
166
- else if (inspection.model !== CODEX_CONTEXT_1M_MODEL)
167
- warnings.push(`codex_context_active_model_not_${CODEX_CONTEXT_1M_MODEL}:${inspection.model}`);
213
+ else if (modelWindow.source === 'unknown')
214
+ warnings.push(`codex_context_model_window_unknown:${inspection.model}`);
215
+ else if (modelWindow.max_window === null)
216
+ warnings.push(`codex_context_model_window_uncapped:${inspection.model}`);
217
+ else if (modelWindow.extends === false)
218
+ warnings.push(`codex_context_model_window_fixed:${inspection.model}:${modelWindow.max_window}`);
168
219
  }
169
220
  let restart = null;
170
221
  if ((action === 'on' || action === 'off') && !blockers.length) {
171
- restart = await maybeRestartCodexApp({
222
+ restart = await maybeRestartRunningCodexApp({
172
223
  env,
173
224
  changed,
174
225
  noRestart,
@@ -186,7 +237,10 @@ export async function codexContext1mCommand(args = [], opts = {}) {
186
237
  enabled: inspection.enabled,
187
238
  config_path: configPath,
188
239
  model: inspection.model,
189
- expected_model: CODEX_CONTEXT_1M_MODEL,
240
+ model_window: modelWindow,
241
+ larger_window_models: windowRows
242
+ .filter((row) => row.listed && row.max_window !== null && row.default_window !== null && row.max_window > row.default_window)
243
+ .map((row) => row.slug),
190
244
  target: { ...CODEX_CONTEXT_1M_TARGETS },
191
245
  keys: inspection.keys,
192
246
  previous: mutation && action === 'on' ? mutation.previous : null,
@@ -198,6 +252,7 @@ export async function codexContext1mCommand(args = [], opts = {}) {
198
252
  warnings,
199
253
  notes: [
200
254
  'Only new Codex sessions pick up context-window changes; existing conversations keep their previous limits.',
255
+ 'Codex caps the window at each model\'s maximum, so every model gets its largest supported window up to 1M.',
201
256
  'Requests with more than 272K input tokens are billed at the long-context rate (2x input / 1.5x output) for the entire request.'
202
257
  ],
203
258
  cli_commands: {
@@ -207,21 +262,3 @@ export async function codexContext1mCommand(args = [], opts = {}) {
207
262
  }
208
263
  };
209
264
  }
210
- async function maybeRestartCodexApp(input) {
211
- const skippedOutcome = (reason) => ({ attempted: false, running: null, status: 'skipped', reason, ok: true, blockers: [] });
212
- if (input.noRestart)
213
- return skippedOutcome('no_restart_flag');
214
- if (!input.changed)
215
- return skippedOutcome('config_unchanged');
216
- if (input.env.SKS_SKIP_CODEX_APP_RESTART === '1')
217
- return skippedOutcome('SKS_SKIP_CODEX_APP_RESTART');
218
- if (process.platform !== 'darwin')
219
- return skippedOutcome('not_macos');
220
- const bundleId = String(input.env.SKS_CODEX_APP_BUNDLE_ID || 'com.openai.codex');
221
- const running = await (input.isRunningImpl || isCodexAppRunningByBundleId)(bundleId, input.env);
222
- if (!running) {
223
- return { attempted: false, running: false, status: 'skipped', reason: 'codex_not_running', ok: true, blockers: [] };
224
- }
225
- const result = await (input.restartImpl || restartCodexApp)({ env: input.env, ...(input.root ? { root: input.root } : {}) });
226
- return { attempted: true, running: true, status: result.status, reason: null, ok: result.ok, blockers: result.blockers };
227
- }
@@ -29,6 +29,10 @@ export const NATIVE_SOURCE_FILES = [
29
29
  'ProvidersOpenRouter.swift',
30
30
  'ProvidersModelExposure.swift',
31
31
  'ProvidersBridgeCatalog.swift',
32
+ 'OpenRouterOnlyState.swift',
33
+ 'ProvidersOpenRouterOnly.swift',
34
+ 'SubagentModelsModels.swift',
35
+ 'SubagentModelsViewController.swift',
32
36
  'RemoteCodingViewController.swift',
33
37
  'DiagnosticsViewController.swift',
34
38
  'SettingsViewController.swift',
@@ -24,12 +24,14 @@ export function buildPreToolUseAllowRewrite(updatedInput, options = {}) {
24
24
  if (updatedInput === undefined || updatedInput === null) {
25
25
  throw new Error('PreToolUse allow rewrite requires updatedInput');
26
26
  }
27
+ const additionalContext = optionalText(options.additionalContext);
27
28
  return withOptionalSystemMessage({
28
29
  continue: true,
29
30
  hookSpecificOutput: {
30
31
  hookEventName: 'PreToolUse',
31
32
  permissionDecision: 'allow',
32
- updatedInput
33
+ updatedInput,
34
+ ...(additionalContext ? { additionalContext } : {})
33
35
  }
34
36
  }, options.systemMessage);
35
37
  }
@@ -6,6 +6,8 @@ export const BRIDGE_CLI_BOOLEAN_OPTIONS = [
6
6
  '--require-ready',
7
7
  '--api-key-stdin',
8
8
  '--confirm',
9
+ '--no-restart',
10
+ '--stdin',
9
11
  DESKTOP_BRIDGE_SUPERVISED_FLAG
10
12
  ];
11
13
  export const BRIDGE_CLI_VALUE_OPTIONS = ['--level', '--host', '--settings', '--set'];