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 +128 -28
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/config/skills-manifest.json +1 -1
- package/dist/core/agents/native-worker-backend-router.js +12 -9
- package/dist/core/commands/naruto-command.js +3 -4
- package/dist/core/decisions/child-model-choice.js +16 -10
- package/dist/core/decisions/policy.js +4 -2
- package/dist/core/hooks-runtime/jev-spawn-routing.js +7 -4
- package/dist/core/hooks-runtime/official-subagent-lifecycle.js +1 -1
- package/dist/core/hooks-runtime/parent-orchestration-gate.js +6 -1
- package/dist/core/hooks-runtime/subagent-spawn-policy.js +6 -3
- package/dist/core/subagents/child-model-allowlist.js +26 -6
- package/dist/core/subagents/child-model-plan.js +2 -2
- package/dist/core/subagents/naruto-host-credentials.js +10 -3
- package/dist/core/subagents/official-subagent-prompt.js +8 -7
- package/dist/core/subagents/official-subagent-runner.js +3 -0
- package/dist/core/subagents/wave-lifecycle.js +2 -2
- package/dist/core/version.js +1 -1
- package/dist/native/sks-menubar/Sources/OpenRouterOnlyState.swift +1 -1
- package/dist/native/sks-menubar/Sources/SubagentModelsModels.swift +19 -5
- package/dist/native/sks-menubar/Sources/SubagentModelsViewController.swift +4 -4
- package/docs/assets/sks-center-subagent-models.png +0 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
[](https://github.com/mandarange/Sneakoscope-Codex/actions/workflows/ci.yml)
|
|
10
10
|
[](https://www.npmjs.com/package/sneakoscope)
|
|
11
|
-
[](#requirements)
|
|
12
12
|
[](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.
|
|
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
|
+

|
|
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
|
|
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
|
-
|
|
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.
|
|
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-
|
|
45
|
-
|
|
105
|
+
$sks-align
|
|
106
|
+
Fix the failing test and verify the changed behavior.
|
|
46
107
|
$sks-review
|
|
47
108
|
```
|
|
48
109
|
|
|
49
|
-
|
|
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
|
|
53
|
-
sks
|
|
54
|
-
sks
|
|
119
|
+
sks update
|
|
120
|
+
sks --version
|
|
121
|
+
sks doctor --json
|
|
55
122
|
```
|
|
56
123
|
|
|
57
|
-
|
|
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
|
|
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
|
-
|
|
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` |
|
|
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
|
-
|
|
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
|
|
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
|
-
- [
|
|
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.
|
|
@@ -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:
|
|
335
|
-
|
|
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
|
|
119
|
-
|
|
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 {
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 = {
|
|
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
|
|
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
|
|
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
|
|
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' ?
|
|
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
|
|
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
|
-
|
|
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) =>
|
|
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
|
|
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,
|
|
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
|
|
295
|
-
|
|
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
|
|
302
|
-
const
|
|
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 =
|
|
199
|
-
const assigned = new Set(
|
|
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')
|
package/dist/core/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '10.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
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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(
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
}
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sneakoscope",
|
|
3
3
|
"displayName": "ㅅㅋㅅ",
|
|
4
|
-
"version": "10.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
|
],
|