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.
Files changed (56) hide show
  1. package/README.md +128 -28
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/cli/command-manifest-lite.js +3 -3
  5. package/dist/config/skills-manifest.json +1 -1
  6. package/dist/core/agents/agent-message-bus.js +180 -4
  7. package/dist/core/agents/native-worker-backend-router.js +12 -9
  8. package/dist/core/align/code-navigation-align.js +58 -3
  9. package/dist/core/artifact-schemas.js +105 -0
  10. package/dist/core/commands/gc-command.js +58 -0
  11. package/dist/core/commands/naruto-command.js +3 -4
  12. package/dist/core/commands/wiki-command.js +17 -3
  13. package/dist/core/decisions/child-model-choice.js +16 -10
  14. package/dist/core/decisions/cli.js +19 -3
  15. package/dist/core/decisions/config.js +25 -3
  16. package/dist/core/decisions/integration.js +26 -6
  17. package/dist/core/decisions/policy.js +10 -2
  18. package/dist/core/decisions/questions.js +43 -1
  19. package/dist/core/decisions/state.js +6 -2
  20. package/dist/core/decisions/types.js +9 -0
  21. package/dist/core/git-hygiene/git-policy.js +1 -0
  22. package/dist/core/git-hygiene/git-status.js +31 -22
  23. package/dist/core/git-hygiene/shared-memory-publish.js +536 -4
  24. package/dist/core/git-hygiene/validators.js +29 -1
  25. package/dist/core/hooks-runtime/jev-spawn-routing.js +29 -5
  26. package/dist/core/hooks-runtime/jev-turn-plan.js +116 -29
  27. package/dist/core/hooks-runtime/official-subagent-lifecycle.js +1 -1
  28. package/dist/core/hooks-runtime/parent-orchestration-gate.js +16 -7
  29. package/dist/core/hooks-runtime/subagent-spawn-policy.js +8 -3
  30. package/dist/core/hooks-runtime.js +81 -5
  31. package/dist/core/memory/jev-memory-intake.js +179 -0
  32. package/dist/core/memory-governor.js +99 -0
  33. package/dist/core/perf/lru-cache.js +50 -1
  34. package/dist/core/pipeline-internals/pipeline-stage-builder.js +358 -0
  35. package/dist/core/pipeline-internals/runtime-core.js +21 -8
  36. package/dist/core/pipeline-internals/runtime-gates.js +22 -0
  37. package/dist/core/router/route-cache.js +38 -2
  38. package/dist/core/subagents/child-model-allowlist.js +26 -6
  39. package/dist/core/subagents/child-model-plan.js +2 -2
  40. package/dist/core/subagents/naruto-host-credentials.js +10 -3
  41. package/dist/core/subagents/official-subagent-preparation.js +47 -3
  42. package/dist/core/subagents/official-subagent-prompt.js +8 -7
  43. package/dist/core/subagents/official-subagent-runner.js +3 -0
  44. package/dist/core/subagents/wave-lifecycle.js +2 -2
  45. package/dist/core/triwiki/context-graph/compiler/cache-key.js +6 -1
  46. package/dist/core/triwiki-attention.js +24 -1
  47. package/dist/core/version.js +1 -1
  48. package/dist/core/wiki-image/image-voxel-ledger.js +84 -57
  49. package/dist/core/wiki-image/route-image-evidence.js +7 -3
  50. package/dist/core/wiki-image/validation.js +20 -12
  51. package/dist/core/wiki-image/visual-anchor.js +5 -1
  52. package/dist/native/sks-menubar/Sources/OpenRouterOnlyState.swift +1 -1
  53. package/dist/native/sks-menubar/Sources/SubagentModelsModels.swift +19 -5
  54. package/dist/native/sks-menubar/Sources/SubagentModelsViewController.swift +4 -4
  55. package/docs/assets/sks-center-subagent-models.png +0 -0
  56. package/package.json +2 -1
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.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
+ ![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.7"
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.7"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -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 into managed AGENTS.md blocks or run memory GC', maturity: 'beta' },
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 into managed AGENTS.md blocks or run bounded memory cleanup.'
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,6 +1,6 @@
1
1
  {
2
2
  "schema": "sks.skills-manifest.v1",
3
- "package_version": "10.5.5",
3
+ "package_version": "10.5.7",
4
4
  "skills": [
5
5
  {
6
6
  "canonical_name": "sks",
@@ -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: { to: entry.to, type: entry.type } });
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: 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'] : []),