sneakoscope 10.5.5 → 10.5.6

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.
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  [![CI](https://github.com/mandarange/Sneakoscope-Codex/actions/workflows/ci.yml/badge.svg)](https://github.com/mandarange/Sneakoscope-Codex/actions/workflows/ci.yml)
10
10
  [![npm version](https://img.shields.io/npm/v/sneakoscope?color=cb3837&logo=npm)](https://www.npmjs.com/package/sneakoscope)
11
- [![Node.js >=20.11](https://img.shields.io/badge/node-%3E%3D20.11-339933?logo=node.js&logoColor=white)](#quick-start)
11
+ [![Recommended Node.js 24 LTS](https://img.shields.io/badge/node-24%20LTS-339933?logo=node.js&logoColor=white)](#requirements)
12
12
  [![MIT license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
13
13
 
14
14
  </div>
@@ -17,50 +17,124 @@
17
17
  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.
18
18
  <!-- END SKS SEARCH VISIBILITY MARKETING -->
19
19
 
20
- Current package: **SKS 10.5.5**. Install the latest stable release from npm.
20
+ Current package: **SKS 10.5.6**. Install the latest stable release from npm.
21
21
 
22
22
  Sneakoscope Codex (`sks`) is a local trust layer for Codex CLI and ChatGPT Desktop. It keeps agent work bounded, preserves project context, records machine-verifiable evidence, and prevents release claims that current tests or artifacts cannot support.
23
23
 
24
24
  Use SKS when you want an AI coding session to end with something you can inspect: the change, the checks that ran, and the evidence behind the result. SKS is local-first and deterministic. It does not promise model quality, search rankings, or traffic.
25
25
 
26
+ ## SKS Center — one place for your Codex setup
27
+
28
+ On macOS, **SKS Center** puts connections, subagent model choices, updates,
29
+ and diagnostics in a native menu bar app. Open the SKS menu bar icon and choose
30
+ **Open SKS Control Center…** to manage your setup without memorizing CLI commands.
31
+
32
+ ![SKS Center showing a connection-specific subagent model list with separate reasoning efforts](docs/assets/sks-center-subagent-models.png)
33
+
34
+ *Native SKS Center UI with an example configuration. Models and available
35
+ reasoning efforts depend on your connection's current catalog.*
36
+
37
+ | Page | What you can manage |
38
+ | --- | --- |
39
+ | **Connections** | Codex-LB, OpenAI OAuth, OpenRouter, and model visibility. |
40
+ | **Subagent Models** | Separate lists per connection, a default option, reasoning effort, and criteria for Jev. The same model can have different efforts for different work. |
41
+ | **Decisions** | Jev's decision mode and cloud consent. |
42
+ | **Settings** | Naruto's execution preference and app settings. |
43
+ | **Image Generation** | Image mode, supported model choices, and provider setup. |
44
+ | **MCP Servers** | Connected tools and server configuration. |
45
+ | **Updates & Diagnostics** | Available updates, operation progress, logs, and health checks. |
46
+
47
+ After installing SKS, install or rebuild the macOS app with:
48
+
49
+ ```sh
50
+ sks menubar install
51
+ ```
52
+
53
+ Interactive macOS installs can build it automatically. Dependency, CI, and
54
+ piped installs skip the menu bar setup; scripts can opt in with
55
+ `SKS_POSTINSTALL_MENUBAR=1` or opt out with `SKS_POSTINSTALL_NO_MENUBAR=1`.
56
+ See [connection setup](docs/codex-lb.md), [subagent model lists](docs/naruto.md#connection-specific-subagent-lists),
57
+ and [image generation](docs/image-generation.md) for the underlying settings.
58
+
26
59
  ## Quick start
27
60
 
28
- Requirements: Node.js **20.11+**, npm, Git, and a current Codex CLI or supported Codex desktop host.
61
+ ### Requirements
62
+
63
+ - **Recommended: the latest patch release of Node.js 24 LTS.** Get it from the [official Node.js releases](https://nodejs.org/en/about/previous-releases).
64
+ - npm, Git, and a current Codex CLI or supported Codex desktop host.
65
+ - For source development and tests, see the locked dependency compatibility ranges in [Contributing](CONTRIBUTING.md#local-setup). CI uses the 24.x line.
66
+
67
+ The published CLI's declared `package.json` engine floor is still `>=20.11`.
68
+ That is separate from the development/test requirements above; Node 20 is now
69
+ end-of-life, so use a supported LTS release for a new installation.
70
+
71
+ ### Install SKS globally
72
+
73
+ Install SKS once for your user account so `sks` is available across projects:
74
+
75
+ ```sh
76
+ npm install --global sneakoscope@latest
77
+ sks doctor --fix --global-only --yes
78
+ sks --version
79
+ ```
80
+
81
+ Run these commands in your terminal. The global-only Doctor step reconciles
82
+ user-level SKS skills and configuration, and reports any setup blockers.
83
+ Make sure your npm global binary directory is on `PATH`.
84
+
85
+ Alternatively, the verified installer performs the global npm installation,
86
+ runs Doctor, and checks that `sks` on `PATH` resolves to the installed version:
29
87
 
30
88
  ```sh
31
89
  npm exec --yes --package=sneakoscope@latest -- sneakoscope install --yes
32
90
  ```
33
91
 
34
- From the root of a project you want SKS to understand:
92
+ ### Enable SKS for a project
93
+
94
+ From the root of each project you want SKS to understand:
35
95
 
36
96
  ```sh
37
97
  sks bootstrap
38
98
  sks doctor --json
39
99
  ```
40
100
 
41
- Then open the project in Codex. Start with the smallest useful loop:
101
+ Then open the project in Codex. Refresh its context and ask for ordinary work
102
+ directly in the conversation:
42
103
 
43
104
  ```text
44
- $sks-plan "Describe the change and the checks it needs"
45
- $sks-work
105
+ $sks-align
106
+ Fix the failing test and verify the changed behavior.
46
107
  $sks-review
47
108
  ```
48
109
 
49
- Use the terminal to inspect the same state:
110
+ `sks bootstrap` adds project-local SKS context and ignore rules. Ordinary work
111
+ stays with the main agent; choose `$sks-naruto` or `$sks-work` when you want
112
+ parallel child work.
113
+
114
+ ## Update an existing installation
115
+
116
+ Run this from your project root in a terminal:
50
117
 
51
118
  ```sh
52
- sks status --json
53
- sks review --staged
54
- sks update-check
119
+ sks update
120
+ sks --version
121
+ sks doctor --json
55
122
  ```
56
123
 
57
- The installer resolves the latest npm release, installs `sks`, runs setup and Doctor, and verifies that the command on your `PATH` is the version it installed. `sks bootstrap` adds the project-local SKS context and ignore rules; it does not replace your source of truth.
124
+ `sks update` updates the **global** SKS package, verifies the installed command,
125
+ and reconciles SKS-managed skills, hooks, configuration and SKS Center where
126
+ available. In 10.5.5 and later it also repairs the known ignored Codex settings
127
+ and refreshes managed guidance. User-authored text and explicit preferences
128
+ are preserved. If a step fails, follow the recovery action in its output.
129
+
130
+ To check available updates first, run `sks update check`. For a repair without
131
+ requesting a newer package, run `sks doctor --fix` yourself in the terminal.
58
132
 
59
133
  ## What you get
60
134
 
61
135
  | Need | SKS provides |
62
136
  | --- | --- |
63
- | Keep work focused | Answers, tiny edits, ordinary implementation, reviews, and DB work run directly in the main agent, the way Codex works by default. Naruto splits work across official Codex subagents only when you ask for it. |
137
+ | Keep work focused | Answers, tiny edits, ordinary implementation, reviews, and DB work run directly in the main agent. Naruto delegates through official Codex subagents when explicitly requested or selected by Jev. |
64
138
  | Keep context bounded | TriWiki indexes repository code into context that can be checked against source. |
65
139
  | Know what actually ran | Tests, diagnostics, and release evidence are recorded for completion claims. |
66
140
  | Recover safely | Doctor and update flows report a concrete recovery action when a check needs attention. |
@@ -73,23 +147,58 @@ and model choices are preserved. See [harness maintenance](docs/align-modernizat
73
147
 
74
148
  ## Everyday commands
75
149
 
76
- Inside a Codex conversation:
150
+ ### Core dollar commands
151
+
152
+ Type these into the **Codex desktop app or Codex CLI conversation**:
77
153
 
78
154
  | Command | Use it for |
79
155
  | --- | --- |
156
+ | `$sks-align` | Refresh official guidance and rebuild TriWiki from the current codebase. |
157
+ | `$sks-mad-sks "scope and task"` | Explicitly authorize a bounded high-risk task; combine with another dollar command when appropriate. |
80
158
  | `$sks-plan "task"` | Write a plan without editing product code. |
81
- | `$sks-work` | Execute the latest plan with Naruto child agents. |
159
+ | `$sks-work` | Run parallel child work through the Naruto execution alias. |
82
160
  | `$sks-review` | Review the current changes. |
83
161
  | `$sks-naruto "task"` | Explicitly split independent work across official subagents. |
84
162
  | `$sks-help` | Explore available SKS workflows. |
85
163
 
86
- In a terminal:
164
+ #### `$sks-align`: keep the harness and project context current
165
+
166
+ Use it when starting in a repository, after structural code changes, or when
167
+ you want to refresh the managed instructions against current official guidance:
168
+
169
+ ```text
170
+ $sks-align
171
+ ```
172
+
173
+ Align searches current official Codex/model guidance, refreshes only
174
+ SKS-managed instruction areas, and rebuilds the source-only TriWiki index.
175
+ Your own instructions and model choices are preserved. External references
176
+ stay separate from code facts; unavailable retrieval is reported while the
177
+ previous references are retained. See [harness maintenance](docs/align-modernization.md).
178
+
179
+ #### `$sks-mad-sks`: explicitly authorize a scoped high-risk task
180
+
181
+ Name the target, allowed changes and boundaries in the request. For example:
182
+
183
+ ```text
184
+ $sks-mad-sks Fix permissions only under ./dist so this project's build can run.
185
+ Preserve source files, credentials, and other projects.
186
+ ```
187
+
188
+ It temporarily widens only the approved scopes for that invocation and can
189
+ be combined with another dollar command, such as `$sks-db` for an explicitly
190
+ authorized database task. Database work uses the bound
191
+ project's SQL-plane capability, read-back verification and final read-only
192
+ restoration. Host approval boundaries and protected control-plane operations
193
+ remain enforced. See [MAD-SKS](docs/mad-sks.md) before authorizing such work.
194
+
195
+ ### Terminal commands
87
196
 
88
197
  ```sh
89
198
  sks --help
90
199
  sks status --json
91
200
  sks doctor --json
92
- sks update-check
201
+ sks update check
93
202
  sks update
94
203
  ```
95
204
 
@@ -106,16 +215,6 @@ SKS keeps these steps observable. It does not turn a failed check into a success
106
215
 
107
216
  ## Optional integrations
108
217
 
109
- ### SKS Center on macOS
110
-
111
- The menu bar app exposes connections, updates, MCP servers, image generation, and diagnostics in one place. Build it from an interactive macOS install or run:
112
-
113
- ```sh
114
- sks menubar install
115
- ```
116
-
117
- Dependency, CI, and piped installs do not touch your home directory. Set `SKS_POSTINSTALL_MENUBAR=1` to force the build in a script, or `SKS_POSTINSTALL_NO_MENUBAR=1` to skip it.
118
-
119
218
  ### Desktop Bridge
120
219
 
121
220
  The Desktop Bridge can route eligible models through Codex-LB or OpenRouter while keeping provider choice and session pins explicit:
@@ -144,7 +243,8 @@ See [Image generation](docs/image-generation.md) for provider boundaries and evi
144
243
 
145
244
  - [Product contract](docs/PRODUCT-CONTRACT.md) — supported surfaces and ownership.
146
245
  - [Essential Trust](docs/essential-trust.md) — verification profiles and safety boundaries.
147
- - [Astra guidance](docs/astra-guidance.md) — how SKS applies official model recommendations.
246
+ - [Harness maintenance and Align](docs/align-modernization.md) — current official references, thin managed instructions, and source-only TriWiki.
247
+ - [MAD-SKS](docs/mad-sks.md) — explicit permission scope, execution boundaries, and verification.
148
248
  - [Agent Bridge](docs/AGENT-BRIDGE.md) — integrate through the CLI or MCP interface.
149
249
  - [Context Graph](docs/architecture/context-graph.md) — bounded source lookup and freshness.
150
250
  - [Release readiness](docs/release-readiness.md) — build, verify, and publish a release.
@@ -259,7 +259,7 @@ dependencies = [
259
259
 
260
260
  [[package]]
261
261
  name = "sks-core"
262
- version = "10.5.5"
262
+ version = "10.5.6"
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.5.5"
3
+ version = "10.5.6"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema": "sks.skills-manifest.v1",
3
- "package_version": "10.5.5",
3
+ "package_version": "10.5.6",
4
4
  "skills": [
5
5
  {
6
6
  "canonical_name": "sks",
@@ -14,7 +14,7 @@ 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
16
  import { chooseChildModel, fallbackChildModel } from '../decisions/child-model-choice.js';
17
- import { allowlistedChildModel, effectiveChildModelAllowlist, childModelListProfile, listChildModelEffort, isSubagentModelEffort } from '../subagents/child-model-allowlist.js';
17
+ import { allowlistedChildModel, effectiveChildModelAllowlist, childModelListProfile, listChildModelEffort, isSubagentModelEffort, isAllowedChildModelEffort } from '../subagents/child-model-allowlist.js';
18
18
  export const NATIVE_WORKER_BACKEND_ROUTER_SCHEMA = 'sks.native-worker-backend-router.v1';
19
19
  export async function runNativeWorkerBackendRouter(input) {
20
20
  const root = path.resolve(input.agentRoot);
@@ -317,28 +317,31 @@ async function resolveOpenRouterOnlyWorkerRouting(input, deps, ctx) {
317
317
  ? allowlistedChildModel(input.agent?.routed_model, allowlist)
318
318
  : null;
319
319
  const requested = String(input.agent?.routed_model || input.agent?.model || '').trim() || null;
320
+ const requestedEffort = explicitReasoning || String(input.agent?.routed_model_reasoning_effort || input.agent?.model_reasoning_effort || '') || null;
320
321
  const explicitListed = explicitModel ? allowlistedChildModel(explicitModel, allowlist) : null;
321
322
  const task = `${ctx.taskKindText}\n${ctx.riskText}`.trim().slice(0, 1200);
322
323
  const choice = explicitModel
323
- ? (explicitListed ? fallbackChildModel(state, explicitListed, 'explicit') : null)
324
+ ? (explicitListed ? fallbackChildModel(state, explicitListed, 'explicit', requestedEffort) : null)
324
325
  : planned
325
- ? fallbackChildModel(state, planned, 'planned')
326
+ ? fallbackChildModel(state, planned, 'planned', requestedEffort)
326
327
  : deps.consultJev === false || !task
327
- ? fallbackChildModel(state, requested, deps.consultJev === false ? 'jev_skipped' : 'empty_task')
328
- : await chooseChildModel({ root: deps.root || process.cwd(), task, role: input.agent?.role || null, requestedModel: requested, state, env, profile: childModelListProfile(allowlist) })
329
- .catch(() => fallbackChildModel(state, requested, 'consult_failed'));
328
+ ? fallbackChildModel(state, requested, deps.consultJev === false ? 'jev_skipped' : 'empty_task', requestedEffort)
329
+ : await chooseChildModel({ root: deps.root || process.cwd(), task, role: input.agent?.role || null, requestedModel: requested, requestedEffort, state, env, profile: childModelListProfile(allowlist) })
330
+ .catch(() => fallbackChildModel(state, requested, 'consult_failed', requestedEffort));
330
331
  const taskPolicy = decideSubagentModel({ title: ctx.taskKindText, description: ctx.riskText, role: input.agent?.role });
331
332
  const choiceModel = choice?.entry.model || '';
332
333
  const routed = {
333
334
  model: choiceModel,
334
- reasoning: allowlist.mode === 'openrouter_only'
335
- ? explicitReasoning || choice?.entry.reasoning_effort || listReasoning(taskPolicy.modelReasoningEffort) || 'medium'
336
- : explicitReasoning || (choice ? listChildModelEffort(choice.entry, null) : null),
335
+ reasoning: choice ? listChildModelEffort(choice.entry, requestedEffort
336
+ || (allowlist.mode === 'openrouter_only' ? listReasoning(taskPolicy.modelReasoningEffort) || 'medium' : null)) : null,
337
337
  serviceTier: explicitTier || input.fastModePolicy.service_tier || 'fast'
338
338
  };
339
339
  const blockers = [
340
340
  ...(!allowlist.entries.length ? [allowlist.mode === 'openrouter_only' ? 'openrouter_only_subagent_list_empty' : 'configured_subagent_list_unavailable'] : []),
341
341
  ...(explicitModel && !explicitListed ? [allowlist.mode === 'openrouter_only' ? 'openrouter_only_subagent_model_not_listed' : 'configured_subagent_model_not_listed'] : []),
342
+ ...(explicitListed && choiceModel !== explicitListed ? ['subagent_model_effort_ambiguous'] : []),
343
+ ...(rawEffort && (!explicitReasoning || !isAllowedChildModelEffort(choiceModel, explicitReasoning, allowlist)
344
+ || explicitReasoning !== routed.reasoning) ? ['subagent_effort_conflicts_with_list'] : []),
342
345
  ...(allowlist.mode === 'configured' && rawEffort
343
346
  && (!explicitReasoning || !choice?.entry.supported_reasoning_efforts?.includes(explicitReasoning))
344
347
  ? ['configured_subagent_effort_unsupported'] : []),
@@ -10,7 +10,7 @@ import { parseNarutoArgs } from '../subagents/naruto-command-args.js';
10
10
  import { buildNarutoProofProjection } from '../subagents/naruto-proof-projection.js';
11
11
  import { attachNarutoLaunchDiagnostics, normalizedNarutoOperatorActions } from '../subagents/naruto-launch-diagnostics.js';
12
12
  import { withFileLock } from '../locks/file-lock.js';
13
- import { codexAppSessionKey, detectCodexAppSession, runOfficialSubagentWorkflow } from '../subagents/official-subagent-runner.js';
13
+ import { codexAppSessionKey, currentCodexThreadSessionKey, detectCodexAppSession, runOfficialSubagentWorkflow } from '../subagents/official-subagent-runner.js';
14
14
  import { NARUTO_GATE_FILENAME, NARUTO_RESULT_SCHEMA, NARUTO_SUMMARY_FILENAME, SUBAGENT_PLAN_FILENAME, buildNarutoSummary, officialSubagentPreparationInProgress, prepareOfficialSubagentMission, withOfficialSubagentLifecycleLock, withNarutoMissionRunAdmission, writeNarutoGate } from '../subagents/official-subagent-preparation.js';
15
15
  import { effectiveSubagentTarget, refreshSubagentWaveLifecycle } from '../subagents/wave-lifecycle.js';
16
16
  import { HOST_CAPABILITY_HOOK_RUNTIME_FILENAME, bindParentSummaryToHostCapabilityEvidence, createHostCapabilityHookRuntimeBinding, resolveHostCapabilityHookRuntimeBinding } from '../agent-bridge/host-capability-runtime.js';
@@ -115,9 +115,8 @@ async function narutoExecution(parsed) {
115
115
  }
116
116
  async function narutoParentSummary(parsed) {
117
117
  const root = await sksRoot();
118
- const appSession = detectCodexAppSession();
119
- const sessionKey = appSession ? codexAppSessionKey() : null;
120
- if (!appSession || !sessionKey) {
118
+ const sessionKey = currentCodexThreadSessionKey();
119
+ if (!sessionKey) {
121
120
  return blockedParentSummary(parsed, ['naruto_parent_summary_app_session_required']);
122
121
  }
123
122
  return withFileLock({
@@ -1,5 +1,6 @@
1
1
  import { consultJevOptions } from './integration.js';
2
- import { canonicalChildModelId, defaultSubagentEntry, subagentEntryForModel } from '../subagents/child-model-allowlist.js';
2
+ import { redactDecisionText } from './state.js';
3
+ import { canonicalChildModelId, defaultSubagentEntry, subagentEntryForModel, subagentEntryLabel } from '../subagents/child-model-allowlist.js';
3
4
  const QUESTION_BUDGET_BYTES = 16_000;
4
5
  const TASK_CHARS = 1200;
5
6
  function optionId(index) {
@@ -7,7 +8,7 @@ function optionId(index) {
7
8
  }
8
9
  function optionSummary(entry) {
9
10
  const criteria = entry.criteria || 'General work; no special criteria.';
10
- return `${entry.model}${entry.default ? ' (default)' : ''}: ${criteria}`;
11
+ return `${subagentEntryLabel(entry)}${entry.default ? ' (default)' : ''}: ${criteria}`;
11
12
  }
12
13
  function laneQuestionId(lane, index) {
13
14
  const slug = String(lane.id || '').toLowerCase().replace(/[^a-z0-9_]+/g, '_').replace(/^_+|_+$/g, '').slice(0, 24);
@@ -19,18 +20,23 @@ export function childModelQuestion(id, entries, lane) {
19
20
  const fallback = entries.findIndex((entry) => entry.default);
20
21
  return {
21
22
  id,
22
- instructions: 'Choose the model on the user\'s list that should run the child task in state.task. Each option is the user\'s rule for when to use that model; follow those rules. When several fit equally, choose the default.',
23
+ instructions: 'Choose the model and reasoning-effort option on the user\'s list that should run the child task in state.task. Options with the same model but different efforts are separate choices with separate criteria. Follow each option\'s criteria, including the task objective and role. The chosen option\'s effort is applied to the child. When several fit equally, choose the default.',
23
24
  options,
24
25
  state: {
25
26
  task: String(lane.task || '').slice(0, TASK_CHARS),
26
27
  role: lane.role || null,
27
28
  requested_model: lane.requestedModel || null,
28
- default_option: optionId(fallback === -1 ? 0 : fallback)
29
+ requested_effort: lane.requestedEffort || null,
30
+ default_option: optionId(fallback === -1 ? 0 : fallback),
31
+ entries: Object.fromEntries(entries.map((entry, index) => [optionId(index), {
32
+ model: entry.model, reasoning_effort: entry.reasoning_effort,
33
+ criteria: redactDecisionText(entry.criteria || 'General work; no special criteria.', 240)
34
+ }]))
29
35
  }
30
36
  };
31
37
  }
32
- export function fallbackChildModel(state, requestedModel, reason) {
33
- const requested = subagentEntryForModel(state, requestedModel);
38
+ export function fallbackChildModel(state, requestedModel, reason, requestedEffort) {
39
+ const requested = subagentEntryForModel(state, requestedModel, requestedEffort);
34
40
  if (requested)
35
41
  return { entry: requested, source: 'requested', reason };
36
42
  const fallback = defaultSubagentEntry(state);
@@ -43,7 +49,7 @@ export async function chooseChildModels(input) {
43
49
  return result;
44
50
  const fallbackAll = (reason) => {
45
51
  for (const lane of input.lanes) {
46
- const choice = fallbackChildModel(input.state, lane.requestedModel, reason);
52
+ const choice = fallbackChildModel(input.state, lane.requestedModel, reason, lane.requestedEffort);
47
53
  if (choice)
48
54
  result[lane.id] = choice;
49
55
  }
@@ -72,7 +78,7 @@ export async function chooseChildModels(input) {
72
78
  workflowId: input.workflowId,
73
79
  goal: input.goal,
74
80
  questions,
75
- facts: { openrouter_only: (input.profile ?? 'openrouter_only') === 'openrouter_only', subagent_model_profile: input.profile ?? 'openrouter_only', subagent_models: entries.map((entry) => canonicalChildModelId(entry.model)) },
81
+ facts: { openrouter_only: (input.profile ?? 'openrouter_only') === 'openrouter_only', subagent_model_profile: input.profile ?? 'openrouter_only', subagent_models: entries.map((entry) => canonicalChildModelId(entry.model)), subagent_options: entries.map((entry) => ({ model: entry.model, reasoning_effort: entry.reasoning_effort })) },
76
82
  ...(input.env ? { env: input.env } : {}),
77
83
  ...(input.deadlineMs === undefined ? {} : { deadlineMs: input.deadlineMs })
78
84
  })
@@ -87,7 +93,7 @@ export async function chooseChildModels(input) {
87
93
  continue;
88
94
  }
89
95
  const reason = !questionId ? 'budget' : decision.reason === 'applied' ? 'keep_baseline' : decision.reason;
90
- const choice = fallbackChildModel(input.state, lane.requestedModel, reason);
96
+ const choice = fallbackChildModel(input.state, lane.requestedModel, reason, lane.requestedEffort);
91
97
  if (choice)
92
98
  result[lane.id] = choice;
93
99
  }
@@ -98,7 +104,7 @@ export async function chooseChildModel(input) {
98
104
  root: input.root,
99
105
  workflowId: 'spawn-child-model',
100
106
  goal: input.task || 'Spawn a child agent.',
101
- lanes: [{ id: 'spawn', task: input.task, role: input.role ?? null, requestedModel: input.requestedModel ?? null }],
107
+ lanes: [{ id: 'spawn', task: input.task, role: input.role ?? null, requestedModel: input.requestedModel ?? null, requestedEffort: input.requestedEffort ?? null }],
102
108
  state: input.state,
103
109
  ...(input.profile ? { profile: input.profile } : {}),
104
110
  ...(input.env ? { env: input.env } : {}),
@@ -1,4 +1,4 @@
1
- import { CHOICE_MIN_CONFIDENCE, CHOICE_MIN_PROBABILITY, CONTEXT_DROP_NOUL_MAX, DISTRIBUTION_SUM_TOLERANCE, DELEGATION_CHOICES, KEEP_BASELINE_CHOICE, NEEDS_EVIDENCE_CHOICE, ROUTING_RISK_NOUL_MIN, ROUTING_ROLE_OMIT_NOUL_MAX, UNKNOWN_USAGE, ESCALATION_ROUTING_TIER, routingTier } from './types.js';
1
+ import { CHOICE_MIN_CONFIDENCE, CHOICE_MIN_PROBABILITY, CONTEXT_DROP_NOUL_MAX, DISTRIBUTION_SUM_TOLERANCE, DELEGATION_CHOICES, KEEP_BASELINE_CHOICE, NEEDS_EVIDENCE_CHOICE, ROUTING_RISK_NOUL_MIN, ROUTING_ROLE_OMIT_NOUL_MAX, UNKNOWN_USAGE, DESIGN_DEFAULTS, ESCALATION_ROUTING_TIER, routingTier } from './types.js';
2
2
  export function compileDecision(bundle, response) {
3
3
  const usage = usageFromWire(response.usage);
4
4
  const decoded = decodeWireResponse(bundle, response);
@@ -75,7 +75,9 @@ export function decodeWireResponse(bundle, response) {
75
75
  const model = text(response.model);
76
76
  if (!model)
77
77
  return { ok: false, reason: 'invalid_response' };
78
- if (model !== bundle.binding.requestedModel)
78
+ const knownSnapshot = bundle.binding.requestedModel === DESIGN_DEFAULTS.model
79
+ && model === 'typesafe/jev-1.13-20260917';
80
+ if (model !== bundle.binding.requestedModel && !knownSnapshot)
79
81
  return { ok: false, reason: 'unknown_model' };
80
82
  const answersRaw = response.answers;
81
83
  if (!isRecord(answersRaw))
@@ -1,7 +1,7 @@
1
1
  import { consultJevTurnModel } from '../decisions/integration.js';
2
2
  import { chooseChildModel, fallbackChildModel } from '../decisions/child-model-choice.js';
3
3
  import { managedOfficialSubagentRoleByName } from '../managed-assets/managed-assets-manifest.js';
4
- import { effectiveChildModelAllowlist, childModelListLabel, childModelListProfile, listChildModelEffort } from '../subagents/child-model-allowlist.js';
4
+ import { effectiveChildModelAllowlist, childModelListLabel, childModelListProfile, listChildModelEffort, subagentEntryKey } from '../subagents/child-model-allowlist.js';
5
5
  import { subagentModelProfile } from '../subagents/model-policy.js';
6
6
  import { effortForTier, latestModelForTier, latestTierModelSet } from '../subagents/model-tiers.js';
7
7
  import { readRoleModelPreferences } from '../subagents/role-model-preferences.js';
@@ -67,18 +67,21 @@ async function openRouterOnlySpawnRouting(root, parentState, input, allowlist) {
67
67
  return { input: null, route: null };
68
68
  const state = listState(allowlist);
69
69
  const requested = String(input.model || '').trim() || null;
70
+ const requestedEffort = String(input.reasoning_effort || '') || null;
70
71
  const agent = String(input.agent_type || input.agentType || '').trim() || null;
71
72
  const task = spawnTask(input, { items: true });
72
73
  const choice = task
73
- ? await chooseChildModel({ root, task, role: agent, requestedModel: requested, state, profile: childModelListProfile(allowlist) })
74
- .catch(() => fallbackChildModel(state, requested, 'consult_failed'))
75
- : fallbackChildModel(state, requested, 'empty_task');
74
+ ? await chooseChildModel({ root, task, role: agent, requestedModel: requested, requestedEffort, state, profile: childModelListProfile(allowlist) })
75
+ .catch(() => fallbackChildModel(state, requested, 'consult_failed', requestedEffort))
76
+ : fallbackChildModel(state, requested, 'empty_task', requestedEffort);
76
77
  if (!choice)
77
78
  return { input: null, route: null };
78
79
  const route = {
79
80
  mode: allowlist.mode,
80
81
  ...(allowlist.mode === 'configured' ? { profile: allowlist.profile } : {}),
81
82
  model: choice.entry.model,
83
+ reasoning_effort: listChildModelEffort(choice.entry, requestedEffort),
84
+ entry_key: subagentEntryKey(choice.entry),
82
85
  source: choice.source,
83
86
  reason: choice.reason
84
87
  };
@@ -175,10 +175,10 @@ export async function recordAndRefreshSubagentEvidence(root, state, payload, eve
175
175
  if (!event)
176
176
  return null;
177
177
  const events = [...priorEvents, event];
178
+ const lifecycle = await refreshSubagentWaveLifecycle(artifactDir, { plan, event, events });
178
179
  if (eventName !== 'SubagentResume') {
179
180
  await clearOfficialSubagentLifecycleCaptureFailure(artifactDir, state, eventPayload, eventName);
180
181
  }
181
- const lifecycle = await refreshSubagentWaveLifecycle(artifactDir, { plan, event, events });
182
182
  const refreshedPlan = lifecycle ? { ...plan, wave_lifecycle: lifecycle } : plan;
183
183
  const existing = await readJson(path.join(artifactDir, SUBAGENT_EVIDENCE_FILENAME), {});
184
184
  const parentSummary = await readJson(path.join(artifactDir, SUBAGENT_PARENT_SUMMARY_FILENAME), null);
@@ -35,7 +35,12 @@ function spawnRouteOf(raw) {
35
35
  return null;
36
36
  const row = raw;
37
37
  const text = (value) => String(value ?? '').trim().slice(0, 200);
38
- const route = { mode: text(row.mode), model: text(row.model), source: text(row.source), reason: text(row.reason) };
38
+ const route = {
39
+ mode: text(row.mode), model: text(row.model), source: text(row.source), reason: text(row.reason),
40
+ ...(Object.hasOwn(row, 'reasoning_effort') ? { reasoning_effort: text(row.reasoning_effort) || null } : {}),
41
+ ...(typeof row.entry_key === 'string' ? { entry_key: text(row.entry_key) } : {}),
42
+ ...(typeof row.profile === 'string' ? { profile: text(row.profile) } : {})
43
+ };
39
44
  return route.mode && route.model && route.source ? route : null;
40
45
  }
41
46
  export function isSpawnToolPayload(payload = {}) {
@@ -2,18 +2,18 @@ import { managedOfficialSubagentRoleByName } from '../managed-assets/managed-ass
2
2
  import { READ_ONLY_LIST_ROLE, readOnlyListRoleInstalled } from '../subagents/read-only-list-role.js';
3
3
  import { stalePinBlockReason, stalePinForAgentType } from '../subagents/role-model-pins.js';
4
4
  import { isSpawnAgentToolName, spawnPayloadToolName } from './spawn-tool-name.js';
5
- import { effectiveChildModelAllowlist, childModelListLabel, isAllowedChildModel } from '../subagents/child-model-allowlist.js';
5
+ import { effectiveChildModelAllowlist, childModelListLabel, isAllowedChildModel, isAllowedChildModelEffort, subagentEntryLabel } from '../subagents/child-model-allowlist.js';
6
6
  export const SUBAGENT_MODELS_SETTINGS_HINT = 'SKS Control Center > Subagent Models';
7
7
  export function renderChildModelList(allowlist) {
8
8
  if (allowlist.mode === 'tiers')
9
9
  return allowlist.models.join(', ');
10
- return allowlist.entries.map((entry) => `${entry.model}${entry.default ? ' (default)' : ''}`).join(', ');
10
+ return allowlist.entries.map((entry) => `${subagentEntryLabel(entry)}${entry.default ? ' (default)' : ''}`).join(', ');
11
11
  }
12
12
  export function renderChildModelCriteria(allowlist) {
13
13
  if (allowlist.mode === 'tiers')
14
14
  return allowlist.models.join(', ');
15
15
  return allowlist.entries
16
- .map((entry) => `${entry.model}${entry.default ? ' (default)' : ''}: ${entry.criteria || 'general work'}`)
16
+ .map((entry) => `${subagentEntryLabel(entry)}${entry.default ? ' (default)' : ''}: ${entry.criteria || 'general work'}`)
17
17
  .join('; ');
18
18
  }
19
19
  function openRouterOnlyBlockReason(allowlist) {
@@ -67,5 +67,8 @@ export function subagentSpawnPolicyBlockReason(payload = {}, opts = {}) {
67
67
  }
68
68
  if (!boundedForkTurns(input.fork_turns))
69
69
  return FORK_BLOCK_REASON;
70
+ if (allowlist.mode !== 'tiers' && !isAllowedChildModelEffort(input.model, input.reasoning_effort, allowlist)) {
71
+ return `SKS ${childModelListLabel(allowlist)} subagent list: the model and reasoning_effort must match a listed option: ${renderChildModelList(allowlist)}. Retry with the selected option's effort.`;
72
+ }
70
73
  return null;
71
74
  }
@@ -48,12 +48,12 @@ export function normalizeSubagentModelList(raw, profile = 'openrouter_only') {
48
48
  issues.push({ index, code: 'subagent_model_id_invalid' });
49
49
  return;
50
50
  }
51
- const key = canonicalChildModelId(model);
51
+ const effort = record.reasoning_effort ?? null;
52
+ const key = subagentEntryKey({ model, reasoning_effort: effort });
52
53
  if (seen.has(key)) {
53
54
  issues.push({ index, code: 'subagent_model_duplicate' });
54
55
  return;
55
56
  }
56
- const effort = record.reasoning_effort ?? null;
57
57
  if (effort !== null && effort !== '' && !isSubagentModelEffort(effort, profile)) {
58
58
  issues.push({ index, code: 'subagent_model_effort_invalid' });
59
59
  return;
@@ -64,7 +64,7 @@ export function normalizeSubagentModelList(raw, profile = 'openrouter_only') {
64
64
  }
65
65
  seen.add(key);
66
66
  entries.push({
67
- model: profile === 'openrouter_only' ? key : model,
67
+ model: profile === 'openrouter_only' ? canonicalChildModelId(model) : model,
68
68
  criteria: cleanCriteria(record.criteria),
69
69
  reasoning_effort: isSubagentModelEffort(effort, profile) ? effort : null,
70
70
  default: record.default === true
@@ -117,11 +117,22 @@ export async function writeOpenRouterOnlyState(update, input = {}) {
117
117
  export function defaultSubagentEntry(state) {
118
118
  return state.subagent_models.find((entry) => entry.default) || state.subagent_models[0] || null;
119
119
  }
120
- export function subagentEntryForModel(state, model) {
120
+ export function subagentEntryKey(entry) {
121
+ return JSON.stringify([canonicalChildModelId(entry.model), entry.reasoning_effort || null]);
122
+ }
123
+ export function subagentEntryLabel(entry) {
124
+ return `${entry.model} [${entry.reasoning_effort || 'default effort'}]`;
125
+ }
126
+ export function subagentEntryForModel(state, model, requestedEffort) {
121
127
  const key = canonicalChildModelId(model);
122
128
  if (!key)
123
129
  return null;
124
- return state.subagent_models.find((entry) => canonicalChildModelId(entry.model) === key) || null;
130
+ const entries = state.subagent_models.filter((entry) => canonicalChildModelId(entry.model) === key);
131
+ const effort = String(requestedEffort || '');
132
+ return (effort ? entries.find((entry) => entry.reasoning_effort === effort) : null)
133
+ || entries.find((entry) => !entry.reasoning_effort)
134
+ || entries.find((entry) => entry.default)
135
+ || (entries.length === 1 ? entries[0] : null);
125
136
  }
126
137
  export function childModelListProfile(list) {
127
138
  return list.mode === 'openrouter_only' ? 'openrouter_only' : list.profile;
@@ -134,6 +145,15 @@ export function listChildModelEffort(entry, requested) {
134
145
  const effort = entry.reasoning_effort || String(requested || '');
135
146
  return supported.includes(effort) && isSubagentModelEffort(effort, 'openai') ? effort : null;
136
147
  }
148
+ export function isAllowedChildModelEffort(model, effort, allowlist) {
149
+ if (allowlist.mode === 'tiers')
150
+ return isAllowedChildModel(model, allowlist);
151
+ const key = canonicalChildModelId(model);
152
+ const requested = String(effort || '');
153
+ return allowlist.entries.some((entry) => canonicalChildModelId(entry.model) === key
154
+ && (entry.reasoning_effort ? entry.reasoning_effort === requested
155
+ : !requested || listChildModelEffort(entry, requested) === requested));
156
+ }
137
157
  export function subagentModelListsPath(input = {}) {
138
158
  return path.join(path.dirname(openRouterOnlyStatePath(input)), 'sks-subagent-model-lists.json');
139
159
  }
@@ -253,7 +273,7 @@ export function effectiveChildModelAllowlist(input = {}) {
253
273
  return {
254
274
  mode: 'configured', profile, models: entries.map((entry) => entry.model), entries,
255
275
  default_model: defaultSubagentEntry({ subagent_models: entries })?.model ?? null,
256
- unroutable: selected.filter((row) => !entries.some((entry) => canonicalChildModelId(entry.model) === canonicalChildModelId(row.model))).map((row) => row.model),
276
+ unroutable: selected.filter((row) => !entries.some((entry) => subagentEntryKey(entry) === subagentEntryKey(row))).map(subagentEntryLabel),
257
277
  blockers: saved.blockers
258
278
  };
259
279
  }
@@ -1,6 +1,6 @@
1
1
  import { chooseChildModels } from '../decisions/child-model-choice.js';
2
2
  import { MAX_JEV_ROUTING_ROLES } from '../decisions/routing.js';
3
- import { defaultSubagentEntry, effectiveChildModelAllowlist, childModelListProfile, listChildModelEffort } from './child-model-allowlist.js';
3
+ import { defaultSubagentEntry, effectiveChildModelAllowlist, childModelListProfile, listChildModelEffort, subagentEntryKey } from './child-model-allowlist.js';
4
4
  import { readCodexMainModel } from './naruto-host-credentials.js';
5
5
  import { READ_ONLY_LIST_ROLE } from './read-only-list-role.js';
6
6
  export function readChildModelPlanContext(location = {}) {
@@ -24,7 +24,7 @@ function routedRow(row, choice, list) {
24
24
  routing_dynamic: true,
25
25
  role_model_preference_source: `ignored_${prefix}`,
26
26
  [list.mode === 'openrouter_only' ? 'openrouter_only_choice' : 'subagent_list_choice']: choice
27
- ? { model: choice.entry.model, source: choice.source, reason: choice.reason, default_entry: choice.entry.default }
27
+ ? { model: choice.entry.model, reasoning_effort: listEntryEffort(choice.entry, row.model_reasoning_effort), entry_key: subagentEntryKey(choice.entry), source: choice.source, reason: choice.reason, default_entry: choice.entry.default }
28
28
  : { model: null, source: 'default', reason: 'subagent_list_empty', default_entry: false }
29
29
  };
30
30
  }
@@ -3,7 +3,7 @@ import { codexUserConfigPath, readTopLevelTomlString } from '../codex-app/codex-
3
3
  import { isOpenRouterModelId } from '../imagegen/imagegen-config.js';
4
4
  import { defaultSubagentModel } from './model-policy.js';
5
5
  import { codexListedEfforts, latestTierModelSet } from './model-tiers.js';
6
- import { SUBAGENT_MODEL_EFFORTS, defaultSubagentEntry, effectiveChildModelAllowlist, allowlistedChildModel, readOpenRouterOnlyStateSync, subagentEntryForModel } from './child-model-allowlist.js';
6
+ import { SUBAGENT_MODEL_EFFORTS, defaultSubagentEntry, effectiveChildModelAllowlist, allowlistedChildModel, readOpenRouterOnlyStateSync, subagentEntryForModel, isAllowedChildModelEffort } from './child-model-allowlist.js';
7
7
  export const NARUTO_AUTH_MODES = ['managed', 'host'];
8
8
  const IDENTIFIER_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
9
9
  const RETIRED_DIRECT_MANAGED_PROVIDERS = new Set(['codex-lb', 'openrouter']);
@@ -134,13 +134,17 @@ export function resolveNarutoCredentialPolicy(input) {
134
134
  }
135
135
  else if (configured) {
136
136
  const model = allowlistedChildModel(resolvedModels.subagentModel, configured);
137
- const entry = configured.entries.find((row) => row.model === model);
137
+ const entry = subagentEntryForModel({ subagent_models: configured.entries }, model, sources.subagentEffort === 'default' ? null : resolvedModels.subagentEffort)
138
+ || (sources.subagentModel === 'default' && sources.subagentEffort === 'default' ? configuredDefault : null);
138
139
  if (!entry)
139
140
  blockers.push(configured.entries.length ? 'naruto_subagent_model_not_in_list' : 'naruto_subagent_model_list_empty');
140
141
  else {
141
142
  resolvedModels.subagentModel = entry.model;
142
143
  if (sources.subagentEffort === 'default')
143
144
  resolvedModels.subagentEffort = entry.reasoning_effort ?? '';
145
+ if (!isAllowedChildModelEffort(entry.model, resolvedModels.subagentEffort, configured)) {
146
+ blockers.push('naruto_subagent_effort_not_in_list');
147
+ }
144
148
  if (resolvedModels.subagentEffort && !entry.supported_reasoning_efforts?.includes(resolvedModels.subagentEffort)) {
145
149
  blockers.push(`naruto_subagent_effort_unsupported:${resolvedModels.subagentModel}:${resolvedModels.subagentEffort}`);
146
150
  }
@@ -218,13 +222,16 @@ function validateOpenRouterOnlyModels(context, resolved, sources, blockers) {
218
222
  blockers.push('naruto_subagent_model_list_empty');
219
223
  }
220
224
  else {
221
- const listed = subagentEntryForModel(context.state, resolved.subagentModel);
225
+ const listed = subagentEntryForModel(context.state, resolved.subagentModel, sources.subagentEffort === 'default' ? null : resolved.subagentEffort);
222
226
  if (!listed)
223
227
  blockers.push(`naruto_subagent_model_not_in_list:${String(resolved.subagentModel || '').slice(0, 64)}`);
224
228
  const entry = listed ?? listDefault;
225
229
  resolved.subagentModel = entry.model;
226
230
  if (sources.subagentEffort === 'default' && entry.reasoning_effort)
227
231
  resolved.subagentEffort = entry.reasoning_effort;
232
+ if (sources.subagentEffort !== 'default' && entry.reasoning_effort && entry.reasoning_effort !== resolved.subagentEffort) {
233
+ blockers.push('naruto_subagent_effort_not_in_list');
234
+ }
228
235
  }
229
236
  for (const scope of ['parent', 'subagent']) {
230
237
  const effort = String(resolved[`${scope}Effort`] || '');
@@ -2,7 +2,7 @@ import { latestTierModelSet } from './model-tiers.js';
2
2
  import { HARD_NARUTO_MAX_THREADS } from './thread-budget.js';
3
3
  import { coreEngineeringDirectiveReferenceText } from '../lean-engineering-policy.js';
4
4
  import { MAX_AUTOMATIC_REVIEWER_COUNT, MAX_AUTOMATIC_SUBAGENT_COUNT, MAX_CRITICAL_AUTOMATIC_REVIEWER_COUNT, officialSubagentOnDemandRoleCatalog, officialSubagentRoleCatalog, selectOfficialSubagentRole } from './agent-catalog.js';
5
- import { allowlistedChildModel, effectiveChildModelAllowlist, childModelListLabel, listChildModelEffort } from './child-model-allowlist.js';
5
+ import { allowlistedChildModel, effectiveChildModelAllowlist, childModelListLabel, listChildModelEffort, subagentEntryForModel, defaultSubagentEntry } from './child-model-allowlist.js';
6
6
  import { READ_ONLY_LIST_ROLE } from './read-only-list-role.js';
7
7
  import { EXCLUSIVE_SURFACE_RULE } from './exclusive-surface-rule.js';
8
8
  export function buildOfficialSubagentPrompt(input) {
@@ -169,7 +169,7 @@ Plan and capacity:
169
169
  - ${EXCLUSIVE_SURFACE_RULE}
170
170
  - reject duplicate slice fingerprints and homogeneous clone work; diversity may come from roles, disjoint shards, or non-exclusive tool surfaces (never from a second child on the same Computer Use or browser surface)
171
171
  - security, database, release, authorization, and irreversible-effect checks are protected strata; aggregate speed or accuracy never offsets a failed protected gate
172
- - after each SubagentStart/SubagentStop, update \`subagent-plan.json.wave_lifecycle\` under the same workflow_run_id
172
+ - after each SubagentStart/SubagentStop, read the runtime-maintained \`subagent-plan.json.wave_lifecycle\` under the same workflow_run_id; hooks rebuild it from official events. Never write planned slice rows into this derived field or edit the event ledger.
173
173
  - after each settled wave: collect results, close completed threads, refresh evidence/ledger, follow \`next_parent_actions\` / \`parent_guidance\`, rescan the ready DAG, then launch the next defensible direct-child wave when \`remaining_to_start > 0\`; capacity is reusable
174
174
  - when guidance says \`spawn_next_direct_child_wave_upto:N\`, immediately spawn that wave with sealed role profiles
175
175
  - automatic targets may resize between waves when the ready DAG changes, but update plan/evidence before spawning; explicit operator and route-owned counts remain exact
@@ -291,16 +291,17 @@ function renderListSpawnModelRouting(list, jevRouting) {
291
291
  }
292
292
  function renderListSliceContract(input) {
293
293
  const routedModel = allowlistedChildModel(input.routed?.routed_model, input.allowlist);
294
- const model = routedModel || input.allowlist.default_model;
295
- if (!model) {
294
+ const routedEffort = String(input.routed?.routed_model_reasoning_effort || '');
295
+ const state = { subagent_models: input.allowlist.entries };
296
+ const entry = subagentEntryForModel(state, routedModel, routedEffort) || defaultSubagentEntry(state);
297
+ if (!entry) {
296
298
  return {
297
299
  model: `none (the ${childModelListLabel(input.allowlist)} subagent list is empty)`,
298
300
  contract: 'stop before spawning: the user must add a model to the subagent list first'
299
301
  };
300
302
  }
301
- const entry = input.allowlist.entries.find((row) => row.model === model);
302
- const routedEffort = String(input.routed?.routed_model_reasoning_effort || '');
303
- const effort = entry ? listChildModelEffort(entry, routedModel ? routedEffort : null) : null;
303
+ const model = entry.model;
304
+ const effort = listChildModelEffort(entry, routedModel ? routedEffort : null);
304
305
  const brief = String(input.brief || '').replace(/\s+/g, ' ').trim().slice(0, 200);
305
306
  return {
306
307
  model: `${model} (${effort ? `effort ${effort}` : 'model default effort'}, ${input.allowlist.mode === 'openrouter_only' ? 'OpenRouter' : childModelListLabel(input.allowlist)} list)`,
@@ -83,6 +83,9 @@ export function detectCodexAppSession(env = process.env) {
83
83
  export function codexAppSessionKey(env = process.env) {
84
84
  if (!detectCodexAppSession(env))
85
85
  return null;
86
+ return currentCodexThreadSessionKey(env);
87
+ }
88
+ export function currentCodexThreadSessionKey(env = process.env) {
86
89
  const threadId = String(env.CODEX_THREAD_ID || '').trim();
87
90
  return threadId || null;
88
91
  }
@@ -195,8 +195,8 @@ function projectLifecycle(previous, input) {
195
195
  const starts = timeline.started;
196
196
  const open = new Set(timeline.open);
197
197
  const stops = new Set();
198
- const waves = previous.waves.map((wave) => ({ ...wave, thread_ids: [...wave.thread_ids], settled_thread_ids: [...wave.settled_thread_ids] }));
199
- const assigned = new Set(waves.flatMap((wave) => wave.thread_ids));
198
+ const waves = [];
199
+ const assigned = new Set();
200
200
  for (const event of input.events) {
201
201
  const threadId = event.thread_id;
202
202
  if (!threadId || event.event_name === 'SubagentResume')
@@ -1 +1 @@
1
- export const PACKAGE_VERSION = '10.5.5';
1
+ export const PACKAGE_VERSION = '10.5.6';
@@ -164,7 +164,7 @@ enum OpenRouterOnlyMessages {
164
164
  case "subagent_model_id_invalid":
165
165
  return row + "not a valid model id for this connection."
166
166
  case "subagent_model_duplicate":
167
- return row + "this model is already on the list."
167
+ return row + "this model and effort are already on the list. Choose a different effort."
168
168
  case "subagent_model_effort_invalid":
169
169
  return row + "choose Default or an effort supported by this model."
170
170
  case "subagent_model_effort_unsupported":
@@ -101,12 +101,26 @@ enum SubagentModelDraft {
101
101
  return available + missing
102
102
  }
103
103
 
104
- /// A new row takes the first available model not already on the list.
104
+ static func entryKey(_ entry: SubagentModelEntry) -> String {
105
+ entry.model.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + "|" + (entry.reasoningEffort ?? "")
106
+ }
107
+
108
+ /// Prefer a new model, then an unused effort of an existing model.
105
109
  static func adding(_ entries: [SubagentModelEntry], available: [SubagentModelOption]) -> [SubagentModelEntry]? {
106
110
  guard entries.count < SubagentModelRules.maxModels else { return nil }
107
111
  let used = Set(entries.map { $0.model.lowercased() })
108
- guard let next = available.first(where: { !used.contains($0.publicId.lowercased()) }) else { return nil }
109
- return entries + [SubagentModelEntry(model: next.publicId, criteria: "", reasoningEffort: nil, isDefault: entries.isEmpty, routable: nil)]
112
+ if let next = available.first(where: { !used.contains($0.publicId.lowercased()) }) {
113
+ return entries + [SubagentModelEntry(model: next.publicId, criteria: "", reasoningEffort: nil, isDefault: entries.isEmpty, routable: nil)]
114
+ }
115
+ let keys = Set(entries.map(entryKey))
116
+ for model in available {
117
+ let efforts: [String?] = [nil] + (model.reasoningEfforts ?? SubagentModelRules.efforts).map { Optional($0) }
118
+ for effort in efforts {
119
+ let next = SubagentModelEntry(model: model.publicId, criteria: "", reasoningEffort: effort, isDefault: entries.isEmpty, routable: nil)
120
+ if !keys.contains(entryKey(next)) { return entries + [next] }
121
+ }
122
+ }
123
+ return nil
110
124
  }
111
125
 
112
126
  static func removing(_ entries: [SubagentModelEntry], at index: Int) -> [SubagentModelEntry] {
@@ -189,7 +203,7 @@ enum SubagentModelDraft {
189
203
  let efforts = profile == "openrouter_only" ? SubagentModelRules.efforts : SubagentModelRules.nativeEfforts
190
204
  if !SubagentModelRules.isModelId(model, profile: profile) {
191
205
  issues.append("subagent_model_id_invalid:\(index)")
192
- } else if !seen.insert(model.lowercased()).inserted {
206
+ } else if !seen.insert(entryKey(entry)).inserted {
193
207
  issues.append("subagent_model_duplicate:\(index)")
194
208
  } else if let effort = entry.reasoningEffort, !efforts.contains(effort) {
195
209
  issues.append("subagent_model_effort_invalid:\(index)")
@@ -215,7 +229,7 @@ enum SubagentModelDraft {
215
229
  static func restoringCriteria(_ incoming: [SubagentModelEntry], known: [SubagentModelEntry], redact: (String) -> String) -> [SubagentModelEntry] {
216
230
  incoming.map { entry in
217
231
  guard entry.criteriaUnreadable, let match = known.first(where: {
218
- $0.model.lowercased() == entry.model.lowercased() && !$0.criteriaUnreadable
232
+ entryKey($0) == entryKey(entry) && !$0.criteriaUnreadable
219
233
  && (redact(cleanCriteria($0.criteria)) == entry.criteria || fitsRedacted(cleanCriteria($0.criteria), answer: entry.criteria))
220
234
  }) else { return entry }
221
235
  var copy = entry
@@ -65,7 +65,7 @@ final class SubagentModelsViewController: NSViewController, ControlCenterPage, N
65
65
  let modeCard = NativeView.card(title: "Connection", subtitle: "", views: [badge, modeDetail, modeIssues, hintRow])
66
66
  let listCard = NativeView.card(
67
67
  title: "Subagent model list",
68
- subtitle: "Save a separate list for Codex-LB, OpenAI OAuth, and OpenRouter Only. Subagents use this connection's listed models. With Jev on, Jev chooses by your criteria; otherwise a listed requested model or the default is used.",
68
+ subtitle: "Save a separate list for each connection. Add the same model at different efforts with separate criteria. With Jev on, Jev chooses a model and effort together; otherwise a listed requested option or the default is used.",
69
69
  views: [rowsStack, listStatus, ControlKit.actionRow([addButton, applyButton, spinner], trailing: [revertButton, refreshButton]), actionStatus]
70
70
  )
71
71
  view = NativeView.page([
@@ -252,15 +252,15 @@ final class SubagentModelsViewController: NSViewController, ControlCenterPage, N
252
252
  refreshButton.isEnabled = !busy
253
253
  if busy || loading { spinner.startAnimation(nil) } else { spinner.stopAnimation(nil) }
254
254
  guard snapshot != nil else { return show(listStatus, "") }
255
- var parts = ["\(draft.count) of \(SubagentModelRules.maxModels) models"]
256
- if let preferred = draft.first(where: \.isDefault) { parts.append("default \(preferred.model)") }
255
+ var parts = ["\(draft.count) of \(SubagentModelRules.maxModels) options"]
256
+ if let preferred = draft.first(where: \.isDefault) { parts.append("default \(preferred.model) [\(preferred.reasoningEffort ?? "Default")]") }
257
257
  if dirty { parts.append("unapplied changes") }
258
258
  if editable, available.isEmpty {
259
259
  parts.append(snapshot?.profile == "openrouter_only"
260
260
  ? "no OpenRouter models are in the bridge catalog yet; select some under Models in Codex on the Connections page"
261
261
  : "no models are available for this connection; open Codex and refresh the catalog")
262
262
  } else if editable, draft.count < SubagentModelRules.maxModels, SubagentModelDraft.adding(draft, available: available) == nil {
263
- parts.append("every catalog model is already on the list")
263
+ parts.append("every available model and effort is already on the list")
264
264
  }
265
265
  show(listStatus, parts.joined(separator: " · "), color: dirty ? .systemOrange : .secondaryLabelColor)
266
266
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "10.5.5",
4
+ "version": "10.5.6",
5
5
  "description": "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.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
@@ -57,6 +57,7 @@
57
57
  "crates/sks-core/Cargo.toml",
58
58
  "crates/sks-core/src",
59
59
  "docs/demo.tape",
60
+ "docs/assets/sks-center-subagent-models.png",
60
61
  "README.md",
61
62
  "LICENSE"
62
63
  ],