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.
- package/README.md +77 -15
- 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/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/wiki-command.js +17 -3
- 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 +7 -1
- 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 +22 -1
- package/dist/core/hooks-runtime/jev-turn-plan.js +116 -29
- package/dist/core/hooks-runtime/parent-orchestration-gate.js +10 -6
- package/dist/core/hooks-runtime/subagent-spawn-policy.js +2 -0
- 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/official-subagent-preparation.js +47 -3
- 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/ControlCenterWindowController.swift +2 -2
- package/dist/native/sks-menubar/Sources/NativeView.swift +4 -2
- 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
|
|
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.
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
###
|
|
99
|
+
### Add project context
|
|
93
100
|
|
|
94
|
-
|
|
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`
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
|
@@ -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')
|
|
@@ -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: '
|
|
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
|
-
|
|
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;
|