sneakoscope 10.5.6 → 10.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +77 -15
  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/align/code-navigation-align.js +58 -3
  8. package/dist/core/artifact-schemas.js +105 -0
  9. package/dist/core/commands/gc-command.js +58 -0
  10. package/dist/core/commands/wiki-command.js +17 -3
  11. package/dist/core/decisions/cli.js +19 -3
  12. package/dist/core/decisions/config.js +25 -3
  13. package/dist/core/decisions/integration.js +26 -6
  14. package/dist/core/decisions/policy.js +7 -1
  15. package/dist/core/decisions/questions.js +43 -1
  16. package/dist/core/decisions/state.js +6 -2
  17. package/dist/core/decisions/types.js +9 -0
  18. package/dist/core/git-hygiene/git-policy.js +1 -0
  19. package/dist/core/git-hygiene/git-status.js +31 -22
  20. package/dist/core/git-hygiene/shared-memory-publish.js +536 -4
  21. package/dist/core/git-hygiene/validators.js +29 -1
  22. package/dist/core/hooks-runtime/jev-spawn-routing.js +22 -1
  23. package/dist/core/hooks-runtime/jev-turn-plan.js +116 -29
  24. package/dist/core/hooks-runtime/parent-orchestration-gate.js +10 -6
  25. package/dist/core/hooks-runtime/subagent-spawn-policy.js +2 -0
  26. package/dist/core/hooks-runtime.js +81 -5
  27. package/dist/core/memory/jev-memory-intake.js +179 -0
  28. package/dist/core/memory-governor.js +99 -0
  29. package/dist/core/perf/lru-cache.js +50 -1
  30. package/dist/core/pipeline-internals/pipeline-stage-builder.js +358 -0
  31. package/dist/core/pipeline-internals/runtime-core.js +21 -8
  32. package/dist/core/pipeline-internals/runtime-gates.js +22 -0
  33. package/dist/core/router/route-cache.js +38 -2
  34. package/dist/core/subagents/official-subagent-preparation.js +47 -3
  35. package/dist/core/triwiki/context-graph/compiler/cache-key.js +6 -1
  36. package/dist/core/triwiki-attention.js +24 -1
  37. package/dist/core/version.js +1 -1
  38. package/dist/core/wiki-image/image-voxel-ledger.js +84 -57
  39. package/dist/core/wiki-image/route-image-evidence.js +7 -3
  40. package/dist/core/wiki-image/validation.js +20 -12
  41. package/dist/core/wiki-image/visual-anchor.js +5 -1
  42. package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +2 -2
  43. package/dist/native/sks-menubar/Sources/NativeView.swift +4 -2
  44. package/package.json +1 -1
package/README.md CHANGED
@@ -14,14 +14,19 @@
14
14
  </div>
15
15
 
16
16
  <!-- BEGIN SKS SEARCH VISIBILITY MARKETING -->
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.
17
+ Sneakoscope Codex (`sks`) is an open-source local tool for the Codex CLI and Codex desktop app. It adds a macOS control center, bounded agent workflows, repository context, and evidence checks for SKS releases.
18
18
  <!-- END SKS SEARCH VISIBILITY MARKETING -->
19
19
 
20
- Current package: **SKS 10.5.6**. Install the latest stable release from npm.
20
+ Current package: **SKS 10.5.8**, prepared for npm publication.
21
+ [Release notes](https://github.com/mandarange/Sneakoscope-Codex/releases/tag/v10.5.8)
22
+ · [Changelog](CHANGELOG.md). Install the latest stable release from npm.
21
23
 
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
-
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.
24
+ Ordinary work stays with the main agent. Optional Jev decisions use OpenRouter
25
+ with cloud consent; source indexing and safety checks run locally. The default
26
+ `essential` verification profile keeps safety checks active without requiring
27
+ completion-proof rituals on every turn. `strict` adds those checks. SKS's npm
28
+ release workflow requires its own current test and artifact evidence in either
29
+ profile. See [verification profiles](docs/essential-trust.md).
25
30
 
26
31
  ## SKS Center — one place for your Codex setup
27
32
 
@@ -50,7 +55,7 @@ After installing SKS, install or rebuild the macOS app with:
50
55
  sks menubar install
51
56
  ```
52
57
 
53
- Interactive macOS installs can build it automatically. Dependency, CI, and
58
+ Interactive global macOS installs can build it automatically. Dependency, CI, and
54
59
  piped installs skip the menu bar setup; scripts can opt in with
55
60
  `SKS_POSTINSTALL_MENUBAR=1` or opt out with `SKS_POSTINSTALL_NO_MENUBAR=1`.
56
61
  See [connection setup](docs/codex-lb.md), [subagent model lists](docs/naruto.md#connection-specific-subagent-lists),
@@ -62,6 +67,8 @@ and [image generation](docs/image-generation.md) for the underlying settings.
62
67
 
63
68
  - **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
69
  - npm, Git, and a current Codex CLI or supported Codex desktop host.
70
+ - Building SKS Center on macOS also requires Xcode Command Line Tools and Swift;
71
+ the installer reports `xcode-select --install` if the tools are missing.
65
72
  - For source development and tests, see the locked dependency compatibility ranges in [Contributing](CONTRIBUTING.md#local-setup). CI uses the 24.x line.
66
73
 
67
74
  The published CLI's declared `package.json` engine floor is still `>=20.11`.
@@ -89,9 +96,10 @@ runs Doctor, and checks that `sks` on `PATH` resolves to the installed version:
89
96
  npm exec --yes --package=sneakoscope@latest -- sneakoscope install --yes
90
97
  ```
91
98
 
92
- ### Enable SKS for a project
99
+ ### Add project context
93
100
 
94
- From the root of each project you want SKS to understand:
101
+ Global SKS hooks and skills apply across projects. From a project root, create
102
+ its local context and ignore rules with:
95
103
 
96
104
  ```sh
97
105
  sks bootstrap
@@ -107,9 +115,10 @@ Fix the failing test and verify the changed behavior.
107
115
  $sks-review
108
116
  ```
109
117
 
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.
118
+ `sks bootstrap` initializes the project's SKS files. `$sks-align` builds the
119
+ source index and refreshes managed guidance. Choose `$sks-naruto` or `$sks-work`
120
+ when you explicitly want child agents; enabled Jev can also select parallel
121
+ work when it judges that the task has independent parts.
113
122
 
114
123
  ## Update an existing installation
115
124
 
@@ -136,7 +145,8 @@ requesting a newer package, run `sks doctor --fix` yourself in the terminal.
136
145
  | --- | --- |
137
146
  | 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. |
138
147
  | Keep context bounded | TriWiki indexes repository code into context that can be checked against source. |
139
- | Know what actually ran | Tests, diagnostics, and release evidence are recorded for completion claims. |
148
+ | Reuse project memory | Opt-in Jev intake stages mission candidates; explicit promotion validates durable records separately from the code index. |
149
+ | Know what actually ran | Diagnostics report measured checks; release gates bind tests and package evidence to the candidate commit. |
140
150
  | Recover safely | Doctor and update flows report a concrete recovery action when a check needs attention. |
141
151
  | Operate locally | The CLI works without a hosted control plane; macOS users can also use SKS Center. |
142
152
 
@@ -145,6 +155,20 @@ references and rebuilds TriWiki. Update and user-run `sks doctor --fix` share
145
155
  that maintenance and repair known ignored configuration settings. User text
146
156
  and model choices are preserved. See [harness maintenance](docs/align-modernization.md).
147
157
 
158
+ ### New in 10.5.7
159
+
160
+ - **Jev execution profiles:** a decision can propose a bounded execution plan.
161
+ The default policy is `baseline`; fast profiles require `optimize` and an
162
+ explicit profile list. Required safety and mutation stages stay code-owned.
163
+ - **TriWiki memory:** eligible mission candidates can be staged and explicitly
164
+ promoted into durable records after provenance, scope, lifecycle, and
165
+ evidence checks. Intake and promotion are separate opt-ins.
166
+ - **Reliable index refresh:** align preserves canonical memory, and rebuilding
167
+ the generated memory indexes no longer makes a freshly aligned graph stale.
168
+
169
+ These are implementation and validation results, not a measured speed or model
170
+ quality improvement. See the [Jev and memory guide](docs/jev-triwiki-memory.md).
171
+
148
172
  ## Everyday commands
149
173
 
150
174
  ### Core dollar commands
@@ -211,10 +235,40 @@ Naruto runs only when you invoke `$sks-naruto` or `$sks-work`, pass `--agents N`
211
235
  3. **Verify** — run the project checks and SKS diagnostics.
212
236
  4. **Review** — inspect the diff and the evidence before claiming completion.
213
237
 
214
- SKS keeps these steps observable. It does not turn a failed check into a success, invent test output, or silently substitute a provider.
238
+ Inspect the actual checks and their scope before relying on a result. A passing
239
+ fixture, configured integration, or successful build does not establish a live
240
+ provider result or deployment.
215
241
 
216
242
  ## Optional integrations
217
243
 
244
+ ### Jev decisions and memory
245
+
246
+ Jev is **off by default**. It requires an OpenRouter key and explicit cloud
247
+ consent. Configure the connection in SKS Center, then use **Decisions** or the
248
+ CLI to enable it:
249
+
250
+ ```sh
251
+ sks decision status --json
252
+ sks decision enable --provider openrouter --model typesafe/jev-1.13 --consent-cloud --json
253
+ ```
254
+
255
+ `status` makes no OpenRouter request. Enabling Jev permits bounded decision
256
+ requests, which may contain redacted task text and selected source excerpts;
257
+ the `baseline` execution policy and disabled memory options remain the defaults.
258
+
259
+ Advanced settings are CLI options: `--execution-policy observe` keeps baseline
260
+ execution; `--execution-policy optimize --profiles <ids>` permits selected fast
261
+ profiles when their preconditions pass. `--memory-intake` enables eligible
262
+ mission candidates; `--memory-promotion` additionally permits an explicit
263
+ `sks memory promote --mission <id> --yes` under the `optimize` policy.
264
+ User-scoped memory additionally requires `--allow-user-memory`.
265
+ Re-running `decision enable` without these flags resets them to their defaults.
266
+
267
+ Use `sks memory recall --json` to inspect available records. Disabling Jev with
268
+ `sks decision disable` stops its decision and intake paths without deleting
269
+ canonical memory. See [Jev setup](docs/jev-decisions.md) and
270
+ [memory boundaries](docs/jev-triwiki-memory.md) before enabling advanced options.
271
+
218
272
  ### Desktop Bridge
219
273
 
220
274
  The Desktop Bridge can route eligible models through Codex-LB or OpenRouter while keeping provider choice and session pins explicit:
@@ -247,6 +301,8 @@ See [Image generation](docs/image-generation.md) for provider boundaries and evi
247
301
  - [MAD-SKS](docs/mad-sks.md) — explicit permission scope, execution boundaries, and verification.
248
302
  - [Agent Bridge](docs/AGENT-BRIDGE.md) — integrate through the CLI or MCP interface.
249
303
  - [Context Graph](docs/architecture/context-graph.md) — bounded source lookup and freshness.
304
+ - [Jev decisions](docs/jev-decisions.md) — optional cloud decisions, setup, and fallback behavior.
305
+ - [Jev and TriWiki memory](docs/jev-triwiki-memory.md) — execution profiles, memory intake, explicit promotion, and bounded recall.
250
306
  - [Release readiness](docs/release-readiness.md) — build, verify, and publish a release.
251
307
  - [Release evidence](docs/release-proof-truth.md) — what each verification result proves.
252
308
  - [FAQ](docs/faq.md) — common setup questions.
@@ -257,7 +313,11 @@ See [Image generation](docs/image-generation.md) for provider boundaries and evi
257
313
 
258
314
  ### Does SKS send my source code to a hosted service?
259
315
 
260
- SKS is local-first. Provider integrations are explicit opt-in routes; inspect the provider and bridge status before using them. Do not paste credentials or private source into an issue.
316
+ TriWiki builds its code index locally. Codex sends the context needed for its
317
+ work to the configured model provider; optional Jev can send bounded, redacted
318
+ task text and selected source excerpts to OpenRouter after cloud consent.
319
+ Local indexing is not a promise that the whole session stays offline. Inspect
320
+ your provider and decision settings before using sensitive repositories.
261
321
 
262
322
  ### Why does `sks doctor` report a problem after an update?
263
323
 
@@ -279,7 +339,9 @@ The native Center is macOS-only. Run `sks menubar install` from an interactive t
279
339
 
280
340
  ### Does SKS guarantee better AI output or search visibility?
281
341
 
282
- No. SKS verifies process evidence and release claims. Model behavior and search outcomes remain separate measurements.
342
+ No. SKS checks defined safety and evidence contracts. It does not guarantee
343
+ model quality, faster execution, rankings, traffic, or correctness beyond the
344
+ checks that actually ran.
283
345
 
284
346
  For a reproducible issue, use the [bug report form](https://github.com/mandarange/Sneakoscope-Codex/issues/new?template=bug_report.yml). For a question, start a discussion or open an issue with the smallest redacted reproduction.
285
347
 
@@ -259,7 +259,7 @@ dependencies = [
259
259
 
260
260
  [[package]]
261
261
  name = "sks-core"
262
- version = "10.5.6"
262
+ version = "10.5.8"
263
263
  dependencies = [
264
264
  "globset",
265
265
  "grep-matcher",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sks-core"
3
- version = "10.5.6"
3
+ version = "10.5.8"
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.6",
3
+ "package_version": "10.5.8",
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')
@@ -17,6 +17,7 @@ import { walkCodeInventory } from '../triwiki/context-graph/extractors/code/inve
17
17
  import { codeInventoryInputHashes } from '../triwiki/code-navigation-policy.js';
18
18
  import { clearContextGraphSnapshotCache, clearWorkspaceContextIndex } from '../triwiki/context-graph/query/index.js';
19
19
  import { withTriWikiStateLock } from '../triwiki/triwiki-cleanup.js';
20
+ import { preserveCanonicalMemory, rebuildSharedIndexes } from '../git-hygiene/shared-memory-publish.js';
20
21
  import { publishArchitectureMapToStage } from '../triwiki/context-graph/store/architecture-map-store.js';
21
22
  import { alignPendingRoot, alignPendingWiki, alignSourceFingerprint, projectAlignCodePack, publishAlignContextIndex } from './align-context-index.js';
22
23
  import { ALIGN_LEDGER_ARTIFACT, ALIGN_OUTPUT_ARTIFACTS, ALIGN_PLAN_ARTIFACT, ALIGN_STAGING_ROOT_REL, buildInitialAlignLedger, refreshAlignGate } from './align-route.js';
@@ -43,7 +44,7 @@ if (new Set(ALIGN_OUTPUT_ARTIFACTS).size !== ALIGN_OUTPUT_ARTIFACTS.length) {
43
44
  }
44
45
  const ALIGN_REPLACED_SURFACES = Object.freeze([
45
46
  { key: 'wiki', rel: '.sneakoscope/wiki' },
46
- { key: 'memory', rel: '.sneakoscope/memory' },
47
+ { key: 'legacy_memory_runtime', rel: '.sneakoscope/memory' },
47
48
  { key: 'context_graph_cache', rel: '.sneakoscope/cache/context-graph' },
48
49
  { key: 'code_pack_freshness_cache', rel: '.sneakoscope/cache/code-pack-head-freshness.json' },
49
50
  { key: 'generated_agents_projection', rel: '.sneakoscope/context/AGENTS.generated.md' },
@@ -167,7 +168,15 @@ async function validateStaging(input) {
167
168
  if (!packValidation.ok || !isCodePackProjectionBoundToSnapshot(input.snapshot.snapshotHash, pack))
168
169
  return false;
169
170
  const contextPack = await readJson(path.join(input.stageWiki, 'context-pack.json'), null);
170
- return validateCodeNavigationContextPack(contextPack, input.root).ok;
171
+ if (!validateCodeNavigationContextPack(contextPack, input.root).ok)
172
+ return false;
173
+ for (const [relative, expected] of Object.entries(input.canonicalHashes || {})) {
174
+ const file = path.join(input.stageWiki, relative);
175
+ const stat = await fsp.stat(file).catch(() => null);
176
+ if (!stat?.isFile() || await fileSha256(file) !== expected)
177
+ return false;
178
+ }
179
+ return true;
171
180
  }
172
181
  async function runLocked(root, dir, missionId, plan, ledger, beforeFinalSourceCas) {
173
182
  const startedAt = Date.now();
@@ -177,6 +186,28 @@ async function runLocked(root, dir, missionId, plan, ledger, beforeFinalSourceCa
177
186
  if (staleStage?.isSymbolicLink())
178
187
  throw new Error('code_navigation_staging_symlink_refused');
179
188
  if (staleStage) {
189
+ const entries = await fsp.readdir(stageBase, { withFileTypes: true });
190
+ for (const entry of entries) {
191
+ if (!entry.isDirectory() || entry.isSymbolicLink())
192
+ throw new Error('align_recovery_stage_invalid');
193
+ const journal = await readJson(path.join(stageBase, entry.name, 'swap-journal.json'), null);
194
+ const priorRoot = path.join(stageBase, entry.name, 'previous');
195
+ for (const surface of ALIGN_REPLACED_SURFACES) {
196
+ const prior = path.join(priorRoot, surface.key);
197
+ if (!(await lstatOrNull(prior)))
198
+ continue;
199
+ if ((await lstatOrNull(prior))?.isSymbolicLink())
200
+ throw new Error('align_recovery_symlink');
201
+ const active = path.join(root, surface.rel);
202
+ if (!await lstatOrNull(active) && journal?.phase === 'prior_moved') {
203
+ await ensureDir(path.dirname(active));
204
+ await guardedRename(guard, prior, active);
205
+ }
206
+ else if (await lstatOrNull(prior)) {
207
+ await guardedRm(guard, prior, { recursive: true, force: true });
208
+ }
209
+ }
210
+ }
180
211
  await guardedRm(guard, stageBase, { recursive: true, force: true });
181
212
  if (await lstatOrNull(stageBase))
182
213
  throw new Error('code_navigation_stale_staging_not_removed');
@@ -311,8 +342,11 @@ async function runLocked(root, dir, missionId, plan, ledger, beforeFinalSourceCa
311
342
  await writeJsonAtomic(path.join(stageWiki, 'code-pack.json'), pack);
312
343
  await writeJsonAtomic(path.join(stageWiki, 'context-pack.json'), contextPack);
313
344
  await publishArchitectureMapToStage(stageWiki, snapshot, { root, missionId });
345
+ const canonicalHashes = await preserveCanonicalMemory(root, stageWiki);
346
+ await rebuildSharedIndexes(pendingRoot);
347
+ ledger.publication.canonical_memory_sha256 = canonicalHashes;
314
348
  ledger.publication.staged = true;
315
- ledger.validation.staged_readback = await validateStaging({ stageWiki, root, snapshot, pack, contextPack, manifest });
349
+ ledger.validation.staged_readback = await validateStaging({ stageWiki, root, snapshot, pack, contextPack, manifest, canonicalHashes });
316
350
  if (!ledger.validation.staged_readback)
317
351
  throw new Error('code_navigation_staging_validation_failed');
318
352
  const prior = await inspectPriorSurfaces(root);
@@ -321,15 +355,36 @@ async function runLocked(root, dir, missionId, plan, ledger, beforeFinalSourceCa
321
355
  ledger.input_state.prior_state_digest = prior.length
322
356
  ? crypto.createHash('sha256').update(prior.map((surface) => surface.descriptor).sort().join('\n')).digest('hex')
323
357
  : null;
358
+ await writeJsonAtomic(path.join(stageRoot, 'swap-journal.json'), {
359
+ schema: 'sks.align-swap-journal.v1',
360
+ phase: 'prepared',
361
+ mission_id: missionId,
362
+ active_surfaces: prior.map((surface) => surface.rel).sort(),
363
+ created_at: nowIso()
364
+ });
324
365
  for (const surface of prior) {
325
366
  const temporary = path.join(previousRoot, surface.key);
326
367
  await ensureDir(path.dirname(temporary));
327
368
  await guardedRename(guard, surface.absolute, temporary);
328
369
  movedPrior.push({ surface, temporary });
329
370
  }
371
+ await writeJsonAtomic(path.join(stageRoot, 'swap-journal.json'), {
372
+ schema: 'sks.align-swap-journal.v1',
373
+ phase: 'prior_moved',
374
+ mission_id: missionId,
375
+ active_surfaces: prior.map((surface) => surface.rel).sort(),
376
+ created_at: nowIso()
377
+ });
330
378
  await ensureDir(path.dirname(activeWiki));
331
379
  await guardedRename(guard, stageWiki, activeWiki);
332
380
  promoted = true;
381
+ await writeJsonAtomic(path.join(stageRoot, 'swap-journal.json'), {
382
+ schema: 'sks.align-swap-journal.v1',
383
+ phase: 'promoted',
384
+ mission_id: missionId,
385
+ active_surfaces: prior.map((surface) => surface.rel).sort(),
386
+ created_at: nowIso()
387
+ });
333
388
  clearContextGraphSnapshotCache();
334
389
  clearWorkspaceContextIndex(root);
335
390
  ledger.publication.transactional_directory_replaced = true;