sneakoscope 10.5.6 → 10.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- 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/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
Sneakoscope Codex (`sks`) is an open-source trust layer for Codex CLI and ChatGPT Desktop. It coordinates bounded AI coding agents, records machine-verifiable evidence, preserves project memory, and blocks release claims that are not supported by current tests or artifacts. Search visibility outcomes are measured separately; SKS does not promise rankings or traffic.
|
|
18
18
|
<!-- END SKS SEARCH VISIBILITY MARKETING -->
|
|
19
19
|
|
|
20
|
-
Current package: **SKS 10.5.
|
|
20
|
+
Current package: **SKS 10.5.7**. Install the latest stable release from npm.
|
|
21
21
|
|
|
22
22
|
Sneakoscope Codex (`sks`) is a local trust layer for Codex CLI and ChatGPT Desktop. It keeps agent work bounded, preserves project context, records machine-verifiable evidence, and prevents release claims that current tests or artifacts cannot support.
|
|
23
23
|
|
|
@@ -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;
|
|
@@ -1,5 +1,110 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { exists, nowIso, readJson, writeJsonAtomic } from './fsx.js';
|
|
3
|
+
import { MEMORY_DISPOSITIONS, MEMORY_POLICY_REVISION } from './decisions/types.js';
|
|
4
|
+
import { containsPlaintextSecret } from './secret-redaction.js';
|
|
5
|
+
import { stableDigest } from './decisions/state.js';
|
|
6
|
+
export const JEV_MEMORY_LIMITS = Object.freeze({ textChars: 512, evidence: 16, images: 8, entryBytes: 8192, fileBytes: 131072, entries: 16 });
|
|
7
|
+
export const MEMORY_HASH = /^[a-f0-9]{64}$/;
|
|
8
|
+
export const MEMORY_ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,95}$/;
|
|
9
|
+
export function memoryRelativePath(value) {
|
|
10
|
+
return typeof value === 'string' && value.length > 0 && value.length <= 320
|
|
11
|
+
&& !value.includes('\\') && !value.includes('\0') && !path.posix.isAbsolute(value)
|
|
12
|
+
&& !/^[A-Za-z]:/.test(value) && !value.split('/').some((part) => part === '..' || part === '.' || !part);
|
|
13
|
+
}
|
|
14
|
+
export function validateJevMemoryIntake(data) {
|
|
15
|
+
const row = data;
|
|
16
|
+
const errors = [];
|
|
17
|
+
if (!row || row.schema !== 'sks.jev-memory-intake.v1')
|
|
18
|
+
return validationResult('sks.jev-memory-intake.v1', ['schema']);
|
|
19
|
+
const allowed = ['schema', 'memory_id', 'idempotency_key', 'workflow_run_id', 'candidate_digest', 'mission_id', 'turn_id', 'session_id_hash', 'disposition', 'scope', 'text_redacted', 'text_hash', 'source_kind', 'source_digest', 'evidence_refs', 'evidence_hashes', 'context_graph_hash', 'jev_decision', 'image_voxel_refs', 'lifecycle_state', 'ttl', 'created_at', 'policy_revision', 'redaction_status', 'promotion_status'];
|
|
20
|
+
if (Object.keys(row).some((key) => !allowed.includes(key)))
|
|
21
|
+
errors.push('unknown_field');
|
|
22
|
+
for (const key of ['memory_id', 'idempotency_key', 'candidate_digest', 'session_id_hash', 'text_hash', 'source_digest', 'context_graph_hash'])
|
|
23
|
+
if (typeof row[key] !== 'string' || !MEMORY_HASH.test(row[key]))
|
|
24
|
+
errors.push(key);
|
|
25
|
+
for (const key of ['mission_id', 'turn_id', 'workflow_run_id'])
|
|
26
|
+
if (typeof row[key] !== 'string' || !MEMORY_ID.test(row[key]))
|
|
27
|
+
errors.push(key);
|
|
28
|
+
if (!MEMORY_DISPOSITIONS.includes(row.disposition) || ['sensitive_no_store', 'ephemeral_turn', 'keep_baseline', 'needs_confirmation'].includes(row.disposition))
|
|
29
|
+
errors.push('disposition_not_storable');
|
|
30
|
+
if (!['mission', 'project', 'user', 'route'].includes(row.scope))
|
|
31
|
+
errors.push('scope');
|
|
32
|
+
if (!['user_prompt', 'mission_artifact', 'tool_evidence', 'image_evidence'].includes(row.source_kind))
|
|
33
|
+
errors.push('source_kind');
|
|
34
|
+
if (typeof row.text_redacted !== 'string' || !row.text_redacted.trim() || row.text_redacted.length > JEV_MEMORY_LIMITS.textChars)
|
|
35
|
+
errors.push('text_bound');
|
|
36
|
+
if (!Array.isArray(row.evidence_refs) || row.evidence_refs.length > JEV_MEMORY_LIMITS.evidence || row.evidence_refs.some((ref) => !memoryRelativePath(ref)))
|
|
37
|
+
errors.push('evidence_refs');
|
|
38
|
+
if (!row.evidence_hashes || typeof row.evidence_hashes !== 'object' || Array.isArray(row.evidence_hashes)
|
|
39
|
+
|| Object.keys(row.evidence_hashes).length > JEV_MEMORY_LIMITS.evidence
|
|
40
|
+
|| Object.entries(row.evidence_hashes).some(([ref, hash]) => !memoryRelativePath(ref) || !MEMORY_HASH.test(hash) || !row.evidence_refs?.includes(ref))
|
|
41
|
+
|| row.evidence_refs?.some(ref => !row.evidence_hashes?.[ref]))
|
|
42
|
+
errors.push('evidence_hashes');
|
|
43
|
+
if (!Array.isArray(row.image_voxel_refs) || row.image_voxel_refs.length > JEV_MEMORY_LIMITS.images)
|
|
44
|
+
errors.push('image_voxel_refs');
|
|
45
|
+
else
|
|
46
|
+
for (const ref of row.image_voxel_refs) {
|
|
47
|
+
if (!ref || typeof ref !== 'object') {
|
|
48
|
+
errors.push('image_ref');
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (!MEMORY_ID.test(ref.image_id || '') || !MEMORY_ID.test(ref.anchor_id || '') || !MEMORY_HASH.test(ref.sha256 || '') || !ref.relation_id || ref.relation_id.length > 96)
|
|
52
|
+
errors.push('image_ref_identity');
|
|
53
|
+
if (!Number.isInteger(ref.width) || ref.width <= 0 || !Number.isInteger(ref.height) || ref.height <= 0 || !Array.isArray(ref.bbox) || ref.bbox.length !== 4 || !ref.bbox.every(Number.isFinite) || ref.bbox[0] < 0 || ref.bbox[1] < 0 || ref.bbox[2] <= 0 || ref.bbox[3] <= 0 || ref.bbox[0] + ref.bbox[2] > ref.width || ref.bbox[1] + ref.bbox[3] > ref.height)
|
|
54
|
+
errors.push('image_ref_bbox');
|
|
55
|
+
}
|
|
56
|
+
if (!['ACTIVE', 'PINNED', 'DORMANT', 'STALE', 'CONFLICTED', 'QUARANTINED', 'DELETE_CANDIDATE', 'DELETED'].includes(row.lifecycle_state))
|
|
57
|
+
errors.push('lifecycle');
|
|
58
|
+
const zeroTtl = ['ephemeral_turn', 'sensitive_no_store', 'needs_confirmation', 'keep_baseline'].includes(row.disposition);
|
|
59
|
+
if (!Number.isSafeInteger(row.ttl) || row.ttl < 0 || row.ttl > 365 * 86400 || (!zeroTtl && row.ttl === 0) || (zeroTtl && row.ttl !== 0))
|
|
60
|
+
errors.push('ttl');
|
|
61
|
+
if (typeof row.created_at !== 'string' || !Number.isFinite(Date.parse(row.created_at)) || !row.created_at.endsWith('Z'))
|
|
62
|
+
errors.push('created_at');
|
|
63
|
+
if (row.policy_revision !== MEMORY_POLICY_REVISION)
|
|
64
|
+
errors.push('policy_revision');
|
|
65
|
+
if (row.redaction_status?.status !== 'pass' || !MEMORY_HASH.test(row.redaction_status?.rule_digest || ''))
|
|
66
|
+
errors.push('redaction_status');
|
|
67
|
+
if (row.jev_decision !== null && (!MEMORY_HASH.test(row.jev_decision?.digest || '') || Object.keys(row.jev_decision).length !== 1))
|
|
68
|
+
errors.push('jev_decision');
|
|
69
|
+
if (!['staged', 'needs_confirmation', 'promoted', 'rejected', 'tombstoned'].includes(row.promotion_status))
|
|
70
|
+
errors.push('promotion_status');
|
|
71
|
+
if (Buffer.byteLength(JSON.stringify(row)) > JEV_MEMORY_LIMITS.entryBytes)
|
|
72
|
+
errors.push('entry_bytes');
|
|
73
|
+
if (containsPlaintextSecret(row) || /data:image\/|[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}|(?:\/Users\/|\/home\/)[^/\s]+/.test(row.text_redacted || ''))
|
|
74
|
+
errors.push('sensitive_content');
|
|
75
|
+
if (row.text_hash !== stableDigest(row.text_redacted))
|
|
76
|
+
errors.push('text_hash_mismatch');
|
|
77
|
+
if (row.memory_id !== stableDigest({ text: row.text_hash, scope: row.scope, source: row.source_digest }))
|
|
78
|
+
errors.push('memory_id_mismatch');
|
|
79
|
+
if (row.idempotency_key !== stableDigest({ workflow: row.workflow_run_id, turn: row.turn_id, candidate: row.candidate_digest }))
|
|
80
|
+
errors.push('idempotency_key_mismatch');
|
|
81
|
+
return validationResult('sks.jev-memory-intake.v1', errors);
|
|
82
|
+
}
|
|
83
|
+
export function validateCanonicalMemoryMetadata(value) {
|
|
84
|
+
const row = value;
|
|
85
|
+
const errors = row ? [...validateJevMemoryIntake(row.intake).errors] : ['memory_metadata'];
|
|
86
|
+
if (row)
|
|
87
|
+
for (const key of ['effective_trust', 'evidence_score', 'priority']) {
|
|
88
|
+
if (typeof row[key] !== 'number' || !Number.isFinite(row[key]) || row[key] < 0 || row[key] > 1)
|
|
89
|
+
errors.push(key);
|
|
90
|
+
}
|
|
91
|
+
if (row && Object.keys(row).some(key => !['intake', 'effective_trust', 'evidence_score', 'priority'].includes(key)))
|
|
92
|
+
errors.push('unknown_memory_metadata');
|
|
93
|
+
return validationResult('sks.canonical-memory-metadata.v1', errors);
|
|
94
|
+
}
|
|
95
|
+
export function validateMemoryTombstone(data) {
|
|
96
|
+
const row = data;
|
|
97
|
+
const errors = [];
|
|
98
|
+
if (!row || row.schema !== 'sks.memory-tombstone.v1')
|
|
99
|
+
return validationResult('sks.memory-tombstone.v1', ['schema']);
|
|
100
|
+
if (!MEMORY_HASH.test(row.memory_id) || !MEMORY_HASH.test(row.previous_digest) || row.id !== row.memory_id)
|
|
101
|
+
errors.push('identity');
|
|
102
|
+
if (!['explicit_forget', 'policy_quarantine'].includes(row.reason) || !Number.isFinite(Date.parse(row.created_at)))
|
|
103
|
+
errors.push('reason_or_time');
|
|
104
|
+
if (Object.keys(row).length !== 6 || containsPlaintextSecret(row))
|
|
105
|
+
errors.push('unexpected_content');
|
|
106
|
+
return validationResult('sks.memory-tombstone.v1', errors);
|
|
107
|
+
}
|
|
3
108
|
export const EFFORTS = new Set(['none', 'low', 'medium', 'high', 'xhigh', 'forensic_vision', 'recovery']);
|
|
4
109
|
export const WORK_ORDER_STATUSES = new Set(['pending', 'in_progress', 'implemented', 'verified', 'blocked']);
|
|
5
110
|
export const ARTIFACT_FILES = {
|
|
@@ -4,6 +4,61 @@ import { flag } from './command-utils.js';
|
|
|
4
4
|
import { compileMistakeRules } from '../verification/mistake-rule-compiler.js';
|
|
5
5
|
export async function memoryCommand(sub, args = []) {
|
|
6
6
|
const action = String(sub || '').toLowerCase();
|
|
7
|
+
if (['promote', 'forget', 'recall'].includes(action)) {
|
|
8
|
+
const root = await sksRoot();
|
|
9
|
+
const json = flag(args, '--json');
|
|
10
|
+
if (action === 'promote') {
|
|
11
|
+
const missionId = String(readOption(args, '--mission', '') || '').trim();
|
|
12
|
+
if (!missionId || !flag(args, '--yes')) {
|
|
13
|
+
const blocked = { schema: 'sks.jev-memory-promotion.v1', ok: false, promoted: [], rejected: ['explicit_mission_and_yes_required'] };
|
|
14
|
+
if (json)
|
|
15
|
+
console.log(JSON.stringify(blocked, null, 2));
|
|
16
|
+
else {
|
|
17
|
+
console.log('Memory promotion blocked: pass --mission <id> --yes.');
|
|
18
|
+
process.exitCode = 1;
|
|
19
|
+
}
|
|
20
|
+
return blocked;
|
|
21
|
+
}
|
|
22
|
+
const { promoteJevMemoryIntake } = await import('../git-hygiene/shared-memory-publish.js');
|
|
23
|
+
const result = await promoteJevMemoryIntake(root, missionId, { explicit: true, requireConfirmation: true });
|
|
24
|
+
if (json)
|
|
25
|
+
console.log(JSON.stringify(result, null, 2));
|
|
26
|
+
else
|
|
27
|
+
console.log(result.ok ? `Memory promotion: ${result.promoted.length} record(s)` : `Memory promotion blocked: ${result.rejected.join(', ')}`);
|
|
28
|
+
if (!result.ok)
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
if (action === 'forget') {
|
|
33
|
+
const memoryId = String(readOption(args, '--memory-id', positionalMemoryId(args)) || '').trim();
|
|
34
|
+
const previousDigest = String(readOption(args, '--previous-digest', '') || '').trim();
|
|
35
|
+
if (!memoryId || !previousDigest) {
|
|
36
|
+
const blocked = { schema: 'sks.memory-tombstone.v1', ok: false, blockers: ['memory_id_and_previous_digest_required'] };
|
|
37
|
+
if (json)
|
|
38
|
+
console.log(JSON.stringify(blocked, null, 2));
|
|
39
|
+
else {
|
|
40
|
+
console.log('Memory forget blocked: pass --memory-id <id> --previous-digest <digest>.');
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
}
|
|
43
|
+
return blocked;
|
|
44
|
+
}
|
|
45
|
+
const { writeJevMemoryTombstone } = await import('../git-hygiene/shared-memory-publish.js');
|
|
46
|
+
const result = await writeJevMemoryTombstone(root, { memoryId, previousDigest });
|
|
47
|
+
const output = { ...result, ok: true };
|
|
48
|
+
if (json)
|
|
49
|
+
console.log(JSON.stringify(output, null, 2));
|
|
50
|
+
else
|
|
51
|
+
console.log(`Memory tombstone written: ${result.path}`);
|
|
52
|
+
return output;
|
|
53
|
+
}
|
|
54
|
+
const { readSharedMemoryOverlay } = await import('../git-hygiene/shared-memory-publish.js');
|
|
55
|
+
const result = await readSharedMemoryOverlay(root, { query: String(readOption(args, '--query', '') || ''), highRisk: flag(args, '--high-risk') });
|
|
56
|
+
if (json)
|
|
57
|
+
console.log(JSON.stringify(result, null, 2));
|
|
58
|
+
else
|
|
59
|
+
console.log(`Memory recall: ${result.available ? result.items.length : `unavailable (${result.reason || 'unknown'})`}`);
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
7
62
|
if (['build', 'project', 'agents', 'agents-md'].includes(action)) {
|
|
8
63
|
const root = await projectRoot();
|
|
9
64
|
const json = flag(args, '--json');
|
|
@@ -106,6 +161,9 @@ function readOption(args = [], name, fallback = null) {
|
|
|
106
161
|
const prefixed = args.find((arg) => String(arg).startsWith(`${name}=`));
|
|
107
162
|
return prefixed ? String(prefixed).slice(name.length + 1) : fallback;
|
|
108
163
|
}
|
|
164
|
+
function positionalMemoryId(args = []) {
|
|
165
|
+
return (args || []).find((arg) => !String(arg).startsWith('--')) || '';
|
|
166
|
+
}
|
|
109
167
|
export async function statsCommand(args = []) {
|
|
110
168
|
const root = await sksRoot();
|
|
111
169
|
const full = flag(args, '--full');
|
|
@@ -14,7 +14,7 @@ import { validateImageVoxelLedger } from '../wiki-image/validation.js';
|
|
|
14
14
|
import { maybeFinalizeRoute } from '../proof/auto-finalize.js';
|
|
15
15
|
import { wikiWrongnessCommand } from '../triwiki-wrongness/wrongness-cli.js';
|
|
16
16
|
import { recordImageWrongnessFromValidation } from '../triwiki-wrongness/image-wrongness.js';
|
|
17
|
-
import { publishSharedMemory, rebuildSharedIndexes, sharedMemorySummary, validateSharedMemory } from '../git-hygiene/shared-memory-publish.js';
|
|
17
|
+
import { promoteJevMemoryIntake, publishSharedMemory, rebuildSharedIndexes, sharedMemorySummary, validateSharedMemory } from '../git-hygiene/shared-memory-publish.js';
|
|
18
18
|
import { codePackPath, isCodePackProjectionBoundToSnapshot } from '../triwiki/code-pack.js';
|
|
19
19
|
import { runContextGraphLint } from '../triwiki/context-graph/lint/index.js';
|
|
20
20
|
import { contextIndexFreshness } from '../triwiki/context-graph/store/index-freshness.js';
|
|
@@ -57,14 +57,28 @@ export async function wikiCommand(sub, args = []) {
|
|
|
57
57
|
if (sub === 'publish') {
|
|
58
58
|
const root = await sksRoot();
|
|
59
59
|
if (!flag(args, '--shared'))
|
|
60
|
-
throw new Error('Usage: sks wiki publish latest --shared [--redact] [--json]');
|
|
60
|
+
throw new Error('Usage: sks wiki publish latest --shared [--redact] [--jev-memory --mission <id>] [--json]');
|
|
61
61
|
const target = positionalArgs(args)[0] || 'latest';
|
|
62
62
|
if (target !== 'latest')
|
|
63
63
|
throw new Error('Usage: sks wiki publish latest --shared [--redact] [--json]');
|
|
64
|
+
let promotion = null;
|
|
65
|
+
const missionId = readFlagValue(args, '--mission', '');
|
|
66
|
+
if (flag(args, '--jev-memory')) {
|
|
67
|
+
if (!missionId)
|
|
68
|
+
throw new Error('Usage: --jev-memory requires --mission <id>');
|
|
69
|
+
promotion = await promoteJevMemoryIntake(root, missionId, { explicit: true, requireConfirmation: true });
|
|
70
|
+
if (!promotion.ok) {
|
|
71
|
+
process.exitCode = 2;
|
|
72
|
+
if (flag(args, '--json'))
|
|
73
|
+
return console.log(JSON.stringify({ promotion }, null, 2));
|
|
74
|
+
console.log(`Jev memory promotion: blocked (${promotion.rejected.join(', ')})`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
64
78
|
const result = await publishSharedMemory(root, { target: 'wiki', redact: flag(args, '--redact') });
|
|
65
79
|
process.exitCode = result.ok ? 0 : 2;
|
|
66
80
|
if (flag(args, '--json'))
|
|
67
|
-
return console.log(JSON.stringify(result, null, 2));
|
|
81
|
+
return console.log(JSON.stringify({ ...result, ...(promotion ? { promotion } : {}) }, null, 2));
|
|
68
82
|
console.log(`Shared wiki publish: ${result.ok ? 'ok' : 'blocked'}`);
|
|
69
83
|
console.log(`Written: ${result.written.length}`);
|
|
70
84
|
for (const blocker of result.blockers)
|