sneakoscope 10.5.5 → 10.5.7
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/cli/command-manifest-lite.js +3 -3
- package/dist/config/skills-manifest.json +1 -1
- package/dist/core/agents/agent-message-bus.js +180 -4
- package/dist/core/agents/native-worker-backend-router.js +12 -9
- package/dist/core/align/code-navigation-align.js +58 -3
- package/dist/core/artifact-schemas.js +105 -0
- package/dist/core/commands/gc-command.js +58 -0
- package/dist/core/commands/naruto-command.js +3 -4
- package/dist/core/commands/wiki-command.js +17 -3
- package/dist/core/decisions/child-model-choice.js +16 -10
- package/dist/core/decisions/cli.js +19 -3
- package/dist/core/decisions/config.js +25 -3
- package/dist/core/decisions/integration.js +26 -6
- package/dist/core/decisions/policy.js +10 -2
- package/dist/core/decisions/questions.js +43 -1
- package/dist/core/decisions/state.js +6 -2
- package/dist/core/decisions/types.js +9 -0
- package/dist/core/git-hygiene/git-policy.js +1 -0
- package/dist/core/git-hygiene/git-status.js +31 -22
- package/dist/core/git-hygiene/shared-memory-publish.js +536 -4
- package/dist/core/git-hygiene/validators.js +29 -1
- package/dist/core/hooks-runtime/jev-spawn-routing.js +29 -5
- package/dist/core/hooks-runtime/jev-turn-plan.js +116 -29
- package/dist/core/hooks-runtime/official-subagent-lifecycle.js +1 -1
- package/dist/core/hooks-runtime/parent-orchestration-gate.js +16 -7
- package/dist/core/hooks-runtime/subagent-spawn-policy.js +8 -3
- package/dist/core/hooks-runtime.js +81 -5
- package/dist/core/memory/jev-memory-intake.js +179 -0
- package/dist/core/memory-governor.js +99 -0
- package/dist/core/perf/lru-cache.js +50 -1
- package/dist/core/pipeline-internals/pipeline-stage-builder.js +358 -0
- package/dist/core/pipeline-internals/runtime-core.js +21 -8
- package/dist/core/pipeline-internals/runtime-gates.js +22 -0
- package/dist/core/router/route-cache.js +38 -2
- 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-preparation.js +47 -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/triwiki/context-graph/compiler/cache-key.js +6 -1
- package/dist/core/triwiki-attention.js +24 -1
- package/dist/core/version.js +1 -1
- package/dist/core/wiki-image/image-voxel-ledger.js +84 -57
- package/dist/core/wiki-image/route-image-evidence.js +7 -3
- package/dist/core/wiki-image/validation.js +20 -12
- package/dist/core/wiki-image/visual-anchor.js +5 -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.7**. 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.
|
|
@@ -70,7 +70,7 @@ const COMMAND_MANIFEST_LITE_BASE = [
|
|
|
70
70
|
{ name: 'gx', summary: 'Render/validate GX cartridges', maturity: 'labs' },
|
|
71
71
|
{ name: 'eval', summary: 'Run eval reports', maturity: 'labs' },
|
|
72
72
|
{ name: 'wiki', summary: 'Manage TriWiki and image voxel ledgers', maturity: 'beta', skipMigrationGate: true, allowedDuringActiveRoute: true, diagnostic: true },
|
|
73
|
-
{ name: 'memory', summary: 'Project TriWiki memory
|
|
73
|
+
{ name: 'memory', summary: 'Project TriWiki memory, explicit Jev promotion, digest-bound forgetting, or bounded recall', maturity: 'beta' },
|
|
74
74
|
{ name: 'gc', summary: 'Compact/prune runtime state', maturity: 'labs', skipMigrationGate: true, allowedDuringActiveRoute: true, diagnostic: true },
|
|
75
75
|
{ name: 'stats', summary: 'Show storage stats', maturity: 'labs', readonly: true, skipMigrationGate: true, allowedDuringActiveRoute: true, diagnostic: true },
|
|
76
76
|
{ name: 'mcp-server', summary: 'Run a stdio MCP server exposing SKS commands as tools for MCP-capable agent hosts', maturity: 'beta', skipMigrationGate: true, allowedDuringActiveRoute: true },
|
|
@@ -373,8 +373,8 @@ const COMMAND_HELP_LITE = {
|
|
|
373
373
|
description: 'Build, refresh, publish shared shards, rebuild ignored indexes, validate, and attach wrongness-memory context to RGBA/trig LLM Wiki packs with attention.use_first and attention.hydrate_first for compact recall plus source hydration.'
|
|
374
374
|
},
|
|
375
375
|
memory: {
|
|
376
|
-
usage: 'sks memory build [--json] | sks memory gc [--dry-run]',
|
|
377
|
-
description: 'Project TriWiki context-pack memory
|
|
376
|
+
usage: 'sks memory build [--json] | sks memory gc [--dry-run] | sks memory promote --mission <id> --yes [--json] | sks memory forget --memory-id <id> --previous-digest <digest> [--json] | sks memory recall [--query <text>] [--high-risk] [--json]',
|
|
377
|
+
description: 'Project TriWiki context-pack memory, explicitly promote a Jev mission intake, write a digest-bound tombstone, or read bounded canonical memory.'
|
|
378
378
|
},
|
|
379
379
|
naruto: {
|
|
380
380
|
usage: 'sks naruto run "task" [--agents N] [--max-threads N] [--trusted-project] [--json] | sks naruto status|subagents|proof [latest|M-...] [--json] | sks naruto execution status [--json] | sks naruto execution set --mode auto|current-session|standalone [--restart] [--json] | sks naruto parent-summary --mission M-... --stdin [--json]',
|
|
@@ -1,7 +1,153 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
|
-
import { appendJsonl, nowIso } from '../fsx.js';
|
|
3
|
+
import { appendJsonl, nowIso, sha256 } from '../fsx.js';
|
|
4
4
|
import { appendAgentLedgerEvent } from './agent-central-ledger.js';
|
|
5
|
+
export const SIBLING_MESSAGE_MAX_BYTES = 4_096;
|
|
6
|
+
export const SIBLING_MESSAGE_MAX_COUNT = 8;
|
|
7
|
+
export const SIBLING_MESSAGE_MAX_REPLIES = 4;
|
|
8
|
+
export const SIBLING_MESSAGE_MAX_TTL_MS = 300_000;
|
|
9
|
+
export const SIBLING_MESSAGE_MAX_HOPS = 1;
|
|
10
|
+
export const SIBLING_MESSAGE_KINDS = Object.freeze(['need_sibling_analysis', 'evidence_request', 'handoff_clarification', 'conflict_notice']);
|
|
11
|
+
const SIBLING_RELAY_LOCKS = new Map();
|
|
12
|
+
async function withSiblingRelayLock(root, fn) {
|
|
13
|
+
const previous = SIBLING_RELAY_LOCKS.get(root) || Promise.resolve();
|
|
14
|
+
const next = previous.catch(() => undefined).then(fn);
|
|
15
|
+
SIBLING_RELAY_LOCKS.set(root, next.catch(() => undefined));
|
|
16
|
+
return next;
|
|
17
|
+
}
|
|
18
|
+
export function buildSiblingMessage(input) {
|
|
19
|
+
const created = input.created_at || nowIso();
|
|
20
|
+
const expires = input.expires_at || new Date(Date.parse(created) + 120_000).toISOString();
|
|
21
|
+
const body = String(input.body || '').trim();
|
|
22
|
+
const bodyBytes = Buffer.byteLength(body, 'utf8');
|
|
23
|
+
if (bodyBytes === 0 || bodyBytes > SIBLING_MESSAGE_MAX_BYTES)
|
|
24
|
+
throw new Error('sibling_message_body_size');
|
|
25
|
+
if (/(?:spawn|merge|publish|align|apply|permission|elevat|credential|secret|api[ _-]?key|password|token|session.?id)/i.test(body)) {
|
|
26
|
+
throw new Error('sibling_message_action_or_sensitive_content');
|
|
27
|
+
}
|
|
28
|
+
const ttlMs = Date.parse(expires) - Date.parse(created);
|
|
29
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0 || ttlMs > SIBLING_MESSAGE_MAX_TTL_MS)
|
|
30
|
+
throw new Error('sibling_message_ttl');
|
|
31
|
+
const message = {
|
|
32
|
+
schema: 'sks.sibling-message.v1',
|
|
33
|
+
message_id: String(input.message_id),
|
|
34
|
+
sender_task_id: String(input.sender_task_id),
|
|
35
|
+
recipient_task_id: String(input.recipient_task_id),
|
|
36
|
+
mission_id: String(input.mission_id),
|
|
37
|
+
plan_id: String(input.plan_id),
|
|
38
|
+
correlation_task_key: String(input.correlation_task_key),
|
|
39
|
+
kind: input.kind,
|
|
40
|
+
body_digest: sha256(body),
|
|
41
|
+
artifact_refs: [...new Set((input.artifact_refs || []).map(String))].filter((ref) => !ref.includes('..') && !/[\\\0]/.test(ref)).slice(0, 16),
|
|
42
|
+
reply_to: input.reply_to || null,
|
|
43
|
+
created_at: created,
|
|
44
|
+
expires_at: expires,
|
|
45
|
+
hop_count: Math.max(0, Math.floor(input.hop_count || 0)),
|
|
46
|
+
status: 'pending'
|
|
47
|
+
};
|
|
48
|
+
const validation = validateSiblingMessage(message);
|
|
49
|
+
if (!validation.ok)
|
|
50
|
+
throw new Error(`sibling_message_invalid:${validation.issues.join('|')}`);
|
|
51
|
+
return message;
|
|
52
|
+
}
|
|
53
|
+
export function validateSiblingMessage(value, now = Date.now(), options = {}) {
|
|
54
|
+
const row = value;
|
|
55
|
+
const issues = [];
|
|
56
|
+
if (!row || row.schema !== 'sks.sibling-message.v1')
|
|
57
|
+
issues.push('schema');
|
|
58
|
+
for (const key of ['message_id', 'sender_task_id', 'recipient_task_id', 'mission_id', 'plan_id', 'correlation_task_key', 'body_digest'])
|
|
59
|
+
if (!/^[A-Za-z0-9_.:-]{1,128}$/.test(String(row?.[key] || '')))
|
|
60
|
+
issues.push(key);
|
|
61
|
+
if (!SIBLING_MESSAGE_KINDS.includes(row?.kind))
|
|
62
|
+
issues.push('kind');
|
|
63
|
+
if (!Array.isArray(row?.artifact_refs) || row.artifact_refs.length > 16)
|
|
64
|
+
issues.push('artifact_refs');
|
|
65
|
+
if (Number(row?.hop_count) !== 0 && Number(row?.hop_count) !== SIBLING_MESSAGE_MAX_HOPS)
|
|
66
|
+
issues.push('hop_count');
|
|
67
|
+
if (!Number.isFinite(Date.parse(String(row?.created_at || ''))) || !Number.isFinite(Date.parse(String(row?.expires_at || ''))))
|
|
68
|
+
issues.push('time');
|
|
69
|
+
const ttlMs = Date.parse(String(row?.expires_at || '')) - Date.parse(String(row?.created_at || ''));
|
|
70
|
+
if (Date.parse(String(row?.expires_at || '')) <= now)
|
|
71
|
+
issues.push('expired');
|
|
72
|
+
if (ttlMs <= 0 || ttlMs > (options.maxTtlMs || SIBLING_MESSAGE_MAX_TTL_MS))
|
|
73
|
+
issues.push('ttl');
|
|
74
|
+
if (options.expectedPlanId && row?.plan_id !== options.expectedPlanId)
|
|
75
|
+
issues.push('cross_plan');
|
|
76
|
+
if (options.allowedRecipients && !options.allowedRecipients.includes(String(row?.recipient_task_id || '')))
|
|
77
|
+
issues.push('recipient');
|
|
78
|
+
if (options.mainTaskId && row?.kind === 'need_sibling_analysis' && row?.recipient_task_id !== options.mainTaskId)
|
|
79
|
+
issues.push('decomposition_recipient');
|
|
80
|
+
if (typeof row?.status !== 'string' || !['pending', 'replied', 'resolved', 'denied', 'expired'].includes(row.status))
|
|
81
|
+
issues.push('status');
|
|
82
|
+
return { ok: issues.length === 0, issues };
|
|
83
|
+
}
|
|
84
|
+
export async function appendSiblingMessage(root, input, options = {}) {
|
|
85
|
+
return withSiblingRelayLock(root, async () => {
|
|
86
|
+
const row = buildSiblingMessage(input);
|
|
87
|
+
const now = options.now || Date.now();
|
|
88
|
+
if (options.maxBytes !== undefined && Buffer.byteLength(String(input.body || '').trim(), 'utf8') > Math.min(SIBLING_MESSAGE_MAX_BYTES, Math.max(1, Math.floor(options.maxBytes))))
|
|
89
|
+
throw new Error('sibling_message_size_cap');
|
|
90
|
+
if (row.sender_task_id === row.recipient_task_id)
|
|
91
|
+
throw new Error('sibling_message_self_recipient');
|
|
92
|
+
if (options.planId && row.plan_id !== options.planId)
|
|
93
|
+
throw new Error('sibling_message_cross_plan');
|
|
94
|
+
if (options.parentTaskId && (options.senderParentTaskId !== options.parentTaskId || options.recipientParentTaskId !== options.parentTaskId)) {
|
|
95
|
+
throw new Error('sibling_message_parent_scope');
|
|
96
|
+
}
|
|
97
|
+
const allowedRecipients = options.allowedRecipients ? [...new Set(options.allowedRecipients.map(String))] : null;
|
|
98
|
+
if (allowedRecipients && !allowedRecipients.includes(row.recipient_task_id))
|
|
99
|
+
throw new Error('sibling_message_recipient_denied');
|
|
100
|
+
if (options.mainTaskId && row.kind === 'need_sibling_analysis' && row.recipient_task_id !== options.mainTaskId)
|
|
101
|
+
throw new Error('sibling_message_decomposition_main_only');
|
|
102
|
+
const requiredCapability = options.requiredCapability || 'sibling_discussion';
|
|
103
|
+
for (const side of [options.senderCapabilities, options.recipientCapabilities]) {
|
|
104
|
+
if (side && !side.includes(requiredCapability))
|
|
105
|
+
throw new Error('sibling_message_capability_denied');
|
|
106
|
+
}
|
|
107
|
+
const validation = validateSiblingMessage(row, now, {
|
|
108
|
+
...(options.planId === undefined ? {} : { expectedPlanId: options.planId }),
|
|
109
|
+
...(allowedRecipients === null ? {} : { allowedRecipients }),
|
|
110
|
+
...(options.maxTtlMs === undefined ? {} : { maxTtlMs: options.maxTtlMs }),
|
|
111
|
+
...(options.mainTaskId === undefined ? {} : { mainTaskId: options.mainTaskId })
|
|
112
|
+
});
|
|
113
|
+
if (!validation.ok)
|
|
114
|
+
throw new Error(`sibling_message_invalid:${validation.issues.join('|')}`);
|
|
115
|
+
const existing = await readRawAgentMessages(root);
|
|
116
|
+
const duplicate = existing.find((entry) => entry?.message_id === row.message_id && entry?.plan_id === row.plan_id);
|
|
117
|
+
if (duplicate)
|
|
118
|
+
return { ...row, duplicate: true, bus_entry: normalizeAgentMessageBusEntry(duplicate, row.mission_id) };
|
|
119
|
+
const planCount = existing.filter((entry) => entry?.plan_id === row.plan_id && entry?.mission_id === row.mission_id).length;
|
|
120
|
+
if (planCount >= Math.min(SIBLING_MESSAGE_MAX_COUNT, Math.max(1, Math.floor(options.maxCount || SIBLING_MESSAGE_MAX_COUNT))))
|
|
121
|
+
throw new Error('sibling_message_count_cap');
|
|
122
|
+
if (row.reply_to) {
|
|
123
|
+
const parent = existing.find((entry) => entry?.message_id === row.reply_to && entry?.plan_id === row.plan_id);
|
|
124
|
+
if (!parent)
|
|
125
|
+
throw new Error('sibling_message_reply_target_missing');
|
|
126
|
+
const replies = existing.filter((entry) => entry?.plan_id === row.plan_id && entry?.reply_to === row.reply_to).length;
|
|
127
|
+
if (replies >= Math.min(SIBLING_MESSAGE_MAX_REPLIES, Math.max(1, Math.floor(options.maxReplies || SIBLING_MESSAGE_MAX_REPLIES))))
|
|
128
|
+
throw new Error('sibling_message_reply_cap');
|
|
129
|
+
}
|
|
130
|
+
const entry = await appendAgentMessage(root, {
|
|
131
|
+
from: row.sender_task_id,
|
|
132
|
+
session_id: row.sender_task_id,
|
|
133
|
+
to: row.recipient_task_id,
|
|
134
|
+
body: `body_digest:${row.body_digest}`,
|
|
135
|
+
type: row.kind,
|
|
136
|
+
mission_id: row.mission_id,
|
|
137
|
+
worker_id: row.sender_task_id,
|
|
138
|
+
event_type: 'status',
|
|
139
|
+
artifact_paths: row.artifact_refs,
|
|
140
|
+
message_id: row.message_id,
|
|
141
|
+
plan_id: row.plan_id,
|
|
142
|
+
correlation_task_key: row.correlation_task_key,
|
|
143
|
+
reply_to: row.reply_to,
|
|
144
|
+
expires_at: row.expires_at,
|
|
145
|
+
hop_count: row.hop_count,
|
|
146
|
+
body_digest: row.body_digest
|
|
147
|
+
});
|
|
148
|
+
return { ...row, duplicate: false, bus_entry: entry };
|
|
149
|
+
});
|
|
150
|
+
}
|
|
5
151
|
export async function appendAgentMessage(root, message) {
|
|
6
152
|
const eventType = normalizeAgentMessageEventType(message.event_type || message.type);
|
|
7
153
|
const entry = {
|
|
@@ -18,10 +164,21 @@ export async function appendAgentMessage(root, message) {
|
|
|
18
164
|
from: message.from,
|
|
19
165
|
to: message.to || 'orchestrator',
|
|
20
166
|
type: message.type || 'note',
|
|
21
|
-
body: message.body
|
|
167
|
+
body: message.body,
|
|
168
|
+
...(message.message_id === undefined ? {} : { message_id: message.message_id }),
|
|
169
|
+
...(message.plan_id === undefined ? {} : { plan_id: message.plan_id }),
|
|
170
|
+
...(message.correlation_task_key === undefined ? {} : { correlation_task_key: message.correlation_task_key }),
|
|
171
|
+
...(message.reply_to === undefined ? {} : { reply_to: message.reply_to }),
|
|
172
|
+
...(message.expires_at === undefined ? {} : { expires_at: message.expires_at }),
|
|
173
|
+
...(message.hop_count === undefined ? {} : { hop_count: message.hop_count }),
|
|
174
|
+
...(message.body_digest === undefined ? {} : { body_digest: message.body_digest })
|
|
22
175
|
};
|
|
23
176
|
await appendJsonl(path.join(root, 'agent-messages.jsonl'), entry);
|
|
24
|
-
await appendAgentLedgerEvent(root, { agent_id: message.from, session_id: message.session_id, event_type: 'message_appended', payload: {
|
|
177
|
+
await appendAgentLedgerEvent(root, { agent_id: message.from, session_id: message.session_id, event_type: 'message_appended', payload: {
|
|
178
|
+
to: entry.to, type: entry.type, message_id: entry.message_id || null, plan_id: entry.plan_id || null,
|
|
179
|
+
correlation_task_key: entry.correlation_task_key || null, reply_to: entry.reply_to || null,
|
|
180
|
+
expires_at: entry.expires_at || null, hop_count: entry.hop_count ?? null, body_digest: entry.body_digest || null
|
|
181
|
+
} });
|
|
25
182
|
return entry;
|
|
26
183
|
}
|
|
27
184
|
export async function readAgentMessageBus(root, missionId, opts = {}) {
|
|
@@ -74,9 +231,28 @@ function normalizeAgentMessageBusEntry(value, missionId) {
|
|
|
74
231
|
from: value.from == null ? undefined : String(value.from),
|
|
75
232
|
to: value.to == null ? undefined : String(value.to),
|
|
76
233
|
type: value.type == null ? undefined : String(value.type),
|
|
77
|
-
body: value.body == null ? undefined : String(value.body)
|
|
234
|
+
body: value.body == null ? undefined : String(value.body),
|
|
235
|
+
message_id: value.message_id == null ? undefined : String(value.message_id),
|
|
236
|
+
plan_id: value.plan_id == null ? undefined : String(value.plan_id),
|
|
237
|
+
correlation_task_key: value.correlation_task_key == null ? undefined : String(value.correlation_task_key),
|
|
238
|
+
reply_to: value.reply_to == null ? value.reply_to : String(value.reply_to),
|
|
239
|
+
expires_at: value.expires_at == null ? undefined : String(value.expires_at),
|
|
240
|
+
hop_count: value.hop_count == null ? undefined : Number(value.hop_count),
|
|
241
|
+
body_digest: value.body_digest == null ? undefined : String(value.body_digest)
|
|
78
242
|
};
|
|
79
243
|
}
|
|
244
|
+
async function readRawAgentMessages(root) {
|
|
245
|
+
const file = path.join(root, 'agent-messages.jsonl');
|
|
246
|
+
const text = await fs.readFile(file, 'utf8').catch(() => '');
|
|
247
|
+
return text.split(/\n+/).filter(Boolean).flatMap((line) => {
|
|
248
|
+
try {
|
|
249
|
+
return [JSON.parse(line)];
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return [];
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
80
256
|
function normalizeAgentMessageEventType(value) {
|
|
81
257
|
const text = String(value || '').toLowerCase();
|
|
82
258
|
if (text === 'worker_completed' || text === 'completed' || text === 'done')
|
|
@@ -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'] : []),
|