sneakoscope 4.0.7 → 4.0.9

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 (49) hide show
  1. package/README.md +2 -2
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/crates/sks-core/src/main.rs +1 -1
  5. package/dist/bin/sks.js +1 -1
  6. package/dist/cli/command-registry.js +1 -0
  7. package/dist/core/commands/glm-command.js +8 -1
  8. package/dist/core/commands/naruto-command.js +25 -0
  9. package/dist/core/commands/stop-gate-command.js +63 -0
  10. package/dist/core/fsx.js +1 -1
  11. package/dist/core/pipeline-internals/runtime-gates.js +28 -4
  12. package/dist/core/providers/glm/glm-bench.js +4 -4
  13. package/dist/core/providers/glm/glm-direct-run.js +1 -1
  14. package/dist/core/providers/glm/glm-latency-trace.js +1 -1
  15. package/dist/core/providers/glm/glm-request-cache.js +10 -2
  16. package/dist/core/providers/glm/naruto/glm-naruto-artifacts.js +2 -0
  17. package/dist/core/providers/glm/naruto/glm-naruto-bench.js +68 -0
  18. package/dist/core/providers/glm/naruto/glm-naruto-budget.js +45 -0
  19. package/dist/core/providers/glm/naruto/glm-naruto-command.js +97 -0
  20. package/dist/core/providers/glm/naruto/glm-naruto-concurrency-governor.js +37 -0
  21. package/dist/core/providers/glm/naruto/glm-naruto-conflict-graph.js +74 -0
  22. package/dist/core/providers/glm/naruto/glm-naruto-decomposer.js +99 -0
  23. package/dist/core/providers/glm/naruto/glm-naruto-file-lease.js +23 -0
  24. package/dist/core/providers/glm/naruto/glm-naruto-finalizer.js +22 -0
  25. package/dist/core/providers/glm/naruto/glm-naruto-judge.js +84 -0
  26. package/dist/core/providers/glm/naruto/glm-naruto-merge-planner.js +57 -0
  27. package/dist/core/providers/glm/naruto/glm-naruto-orchestrator.js +277 -0
  28. package/dist/core/providers/glm/naruto/glm-naruto-patch-envelope.js +55 -0
  29. package/dist/core/providers/glm/naruto/glm-naruto-quorum.js +37 -0
  30. package/dist/core/providers/glm/naruto/glm-naruto-rate-limiter.js +18 -0
  31. package/dist/core/providers/glm/naruto/glm-naruto-repair-wave.js +21 -0
  32. package/dist/core/providers/glm/naruto/glm-naruto-shard-planner.js +32 -0
  33. package/dist/core/providers/glm/naruto/glm-naruto-trace.js +91 -0
  34. package/dist/core/providers/glm/naruto/glm-naruto-types.js +37 -0
  35. package/dist/core/providers/glm/naruto/glm-naruto-work-graph.js +2 -0
  36. package/dist/core/providers/glm/naruto/glm-naruto-worker-pool.js +79 -0
  37. package/dist/core/providers/glm/naruto/glm-naruto-worker-runtime.js +198 -0
  38. package/dist/core/providers/glm/naruto/glm-naruto-worker.js +2 -0
  39. package/dist/core/providers/glm/naruto/glm-naruto-worktree.js +48 -0
  40. package/dist/core/providers/openrouter/openrouter-provider-health.js +46 -0
  41. package/dist/core/providers/openrouter/openrouter-secret-store.js +33 -0
  42. package/dist/core/providers/openrouter/openrouter-stream.js +101 -8
  43. package/dist/core/stop-gate/stop-gate-check.js +208 -0
  44. package/dist/core/stop-gate/stop-gate-diagnostics.js +4 -0
  45. package/dist/core/stop-gate/stop-gate-resolver.js +122 -0
  46. package/dist/core/stop-gate/stop-gate-types.js +2 -0
  47. package/dist/core/stop-gate/stop-gate-writer.js +76 -0
  48. package/dist/core/version.js +1 -1
  49. package/package.json +1 -1
@@ -0,0 +1,46 @@
1
+ import { nowIso, writeJsonAtomic } from '../../fsx.js';
2
+ import path from 'node:path';
3
+ export function createProviderHealthTracker(providerSlug = 'openrouter', model = 'z-ai/glm-5.2') {
4
+ let health = {
5
+ schema: 'sks.openrouter-provider-health.v1',
6
+ provider_slug: providerSlug,
7
+ model,
8
+ p50_ttft_ms: 0,
9
+ p90_ttft_ms: 0,
10
+ p50_throughput: 0,
11
+ p90_throughput: 0,
12
+ count_429: 0,
13
+ count_5xx: 0,
14
+ last_success: null,
15
+ last_failure: null,
16
+ updated_at: nowIso()
17
+ };
18
+ const ttftSamples = [];
19
+ return {
20
+ record: (entry) => {
21
+ if (typeof entry.p50_ttft_ms === 'number')
22
+ ttftSamples.push(entry.p50_ttft_ms);
23
+ const p50 = ttftSamples.length > 0 ? (ttftSamples[Math.floor(ttftSamples.length * 0.5)] ?? 0) : 0;
24
+ const p90 = ttftSamples.length > 0 ? (ttftSamples[Math.floor(ttftSamples.length * 0.9)] ?? 0) : 0;
25
+ health = {
26
+ ...health,
27
+ ...entry,
28
+ p50_ttft_ms: p50,
29
+ p90_ttft_ms: p90,
30
+ count_429: health.count_429 + (entry.count_429 || 0),
31
+ count_5xx: health.count_5xx + (entry.count_5xx || 0),
32
+ last_success: entry.last_success || health.last_success,
33
+ last_failure: entry.last_failure || health.last_failure,
34
+ updated_at: nowIso()
35
+ };
36
+ },
37
+ getHealth: () => health,
38
+ snapshot: () => health
39
+ };
40
+ }
41
+ export async function writeProviderHealth(root, health) {
42
+ const out = path.join(root, '.sneakoscope', 'glm-naruto', 'provider-health.json');
43
+ await writeJsonAtomic(out, health);
44
+ return out;
45
+ }
46
+ //# sourceMappingURL=openrouter-provider-health.js.map
@@ -110,4 +110,37 @@ async function ensureSecretDir(secretDir) {
110
110
  await fs.mkdir(secretDir, { recursive: true, mode: 0o700 });
111
111
  await fs.chmod(secretDir, 0o700).catch(() => undefined);
112
112
  }
113
+ export async function promptForOpenRouterKeyHidden() {
114
+ const readline = await import('node:readline/promises');
115
+ const { stdin, stdout } = await import('node:process');
116
+ const rl = readline.createInterface({ input: stdin, output: stdout, terminal: true });
117
+ try {
118
+ // Use muted input for hidden key entry
119
+ let key = '';
120
+ const escaped = stdout.isTTY
121
+ ? await new Promise((resolve) => {
122
+ const onData = (char) => {
123
+ const c = char.toString();
124
+ if (c === '\r' || c === '\n' || c === '\u0004') {
125
+ stdin.removeListener('data', onData);
126
+ resolve(key);
127
+ }
128
+ else if (c === '\u0003') {
129
+ stdin.removeListener('data', onData);
130
+ resolve('');
131
+ }
132
+ else {
133
+ key += c;
134
+ }
135
+ };
136
+ stdin.on('data', onData);
137
+ stdout.write('Enter OpenRouter API key (input hidden): ');
138
+ })
139
+ : await rl.question('Enter OpenRouter API key: ');
140
+ return escaped.trim() || null;
141
+ }
142
+ finally {
143
+ rl.close();
144
+ }
145
+ }
113
146
  //# sourceMappingURL=openrouter-secret-store.js.map
@@ -22,20 +22,28 @@ export async function sendOpenRouterChatCompletionStream(input) {
22
22
  });
23
23
  if (timeout)
24
24
  clearTimeout(timeout);
25
- const text = await response.text();
26
- if (!response.ok)
25
+ if (!response.ok) {
26
+ const text = await response.text();
27
27
  return { ok: false, error: normalizeOpenRouterError(response.status, text) };
28
- return { ok: true, value: parseOpenRouterStreamText(text, started) };
28
+ }
29
+ // Real streaming via ReadableStream reader
30
+ if (response.body && typeof response.body.getReader === 'function') {
31
+ return { ok: true, value: await readRealStream(response.body, started, input.idleTimeoutMs) };
32
+ }
33
+ // Fallback: non-streaming response
34
+ const text = await response.text();
35
+ return { ok: true, value: parseOpenRouterStreamText(text, started, false) };
29
36
  }
30
37
  catch (err) {
31
38
  if (timeout)
32
39
  clearTimeout(timeout);
33
- if (err instanceof Error && err.name === 'AbortError') {
40
+ if (err instanceof Error && (err.name === 'AbortError' || err.message === 'glm_stream_idle_timeout' || err.message === 'glm_stream_idle_timeout_after_ttft')) {
41
+ const isIdle = err.message.startsWith('glm_stream_idle');
34
42
  return {
35
43
  ok: false,
36
44
  error: {
37
- code: 'glm_request_timeout',
38
- message: `OpenRouter stream aborted after ${input.timeoutMs || 'external'}ms.`,
45
+ code: isIdle ? err.message : 'glm_request_timeout',
46
+ message: isIdle ? `OpenRouter stream idle timeout after ${input.idleTimeoutMs || 0}ms.` : `OpenRouter stream aborted after ${input.timeoutMs || 'external'}ms.`,
39
47
  severity: 'failed'
40
48
  }
41
49
  };
@@ -50,7 +58,91 @@ export async function sendOpenRouterChatCompletionStream(input) {
50
58
  };
51
59
  }
52
60
  }
53
- export function parseOpenRouterStreamText(text, startedAtMs = Date.now()) {
61
+ async function readRealStream(body, startedAtMs, idleTimeoutMs) {
62
+ const reader = body.getReader();
63
+ const decoder = new TextDecoder();
64
+ const events = [];
65
+ let content = '';
66
+ let model;
67
+ let usage;
68
+ let ttft = null;
69
+ let buffer = '';
70
+ let chunkCount = 0;
71
+ let lastChunkMs = startedAtMs;
72
+ try {
73
+ while (true) {
74
+ // 4.0.9: Idle timeout between chunks — abort if stream stalls.
75
+ const readPromise = reader.read();
76
+ let idleTimer = null;
77
+ if (idleTimeoutMs && idleTimeoutMs > 0) {
78
+ idleTimer = setTimeout(() => {
79
+ const code = ttft === null ? 'glm_stream_idle_timeout' : 'glm_stream_idle_timeout_after_ttft';
80
+ reader.cancel(code).catch(() => undefined);
81
+ }, idleTimeoutMs);
82
+ }
83
+ let result;
84
+ try {
85
+ result = await readPromise;
86
+ }
87
+ finally {
88
+ if (idleTimer)
89
+ clearTimeout(idleTimer);
90
+ }
91
+ const { done, value } = result;
92
+ if (done)
93
+ break;
94
+ buffer += decoder.decode(value, { stream: true });
95
+ const lines = buffer.split(/\r?\n/);
96
+ buffer = lines.pop() || '';
97
+ let hadChunk = false;
98
+ for (const line of lines) {
99
+ if (!line.startsWith('data:'))
100
+ continue;
101
+ const data = line.slice('data:'.length).trim();
102
+ if (!data || data === '[DONE]')
103
+ continue;
104
+ try {
105
+ const raw = JSON.parse(data);
106
+ const delta = raw?.choices?.[0]?.delta?.content;
107
+ if (typeof raw?.model === 'string')
108
+ model = raw.model;
109
+ if (raw?.usage)
110
+ usage = raw.usage;
111
+ if (typeof delta === 'string' && delta) {
112
+ if (ttft === null)
113
+ ttft = Math.max(0, Date.now() - startedAtMs);
114
+ content += delta;
115
+ chunkCount++;
116
+ lastChunkMs = Date.now();
117
+ hadChunk = true;
118
+ events.push({ type: 'chunk', content_delta: delta, ...(model ? { model } : {}), raw });
119
+ }
120
+ }
121
+ catch {
122
+ events.push({ type: 'error', raw: data });
123
+ }
124
+ }
125
+ if (hadChunk)
126
+ lastChunkMs = Date.now();
127
+ }
128
+ }
129
+ finally {
130
+ reader.releaseLock();
131
+ }
132
+ events.push({ type: 'done', ...(model ? { model } : {}), ...(usage ? { usage } : {}) });
133
+ return {
134
+ content,
135
+ ...(model ? { model } : {}),
136
+ ...(usage ? { usage } : {}),
137
+ ttft_ms: ttft,
138
+ last_chunk_ms: lastChunkMs,
139
+ total_ms: Math.max(0, Date.now() - startedAtMs),
140
+ chunk_count: chunkCount,
141
+ events,
142
+ real_stream: true
143
+ };
144
+ }
145
+ export function parseOpenRouterStreamText(text, startedAtMs = Date.now(), realStream = false) {
54
146
  const events = [];
55
147
  let content = '';
56
148
  let model;
@@ -88,7 +180,8 @@ export function parseOpenRouterStreamText(text, startedAtMs = Date.now()) {
88
180
  ttft_ms: ttft,
89
181
  total_ms: Math.max(0, Date.now() - startedAtMs),
90
182
  chunk_count: events.filter((event) => event.type === 'chunk').length,
91
- events
183
+ events,
184
+ real_stream: realStream
92
185
  };
93
186
  }
94
187
  //# sourceMappingURL=openrouter-stream.js.map
@@ -0,0 +1,208 @@
1
+ import path from 'node:path';
2
+ import { ensureDir, exists, nowIso, readJson, writeJsonAtomic } from '../fsx.js';
3
+ import { missionDir } from '../mission.js';
4
+ import { resolveStopGate, gateStatInfo } from './stop-gate-resolver.js';
5
+ const HARD_BLOCKER_FILE = 'hard-blocker.json';
6
+ function normalizeRoute(route) {
7
+ if (!route)
8
+ return null;
9
+ const upper = route.toUpperCase().replace(/^\$/, '');
10
+ if (upper === 'GLM_NARUTO' || upper === 'NARUTO')
11
+ return 'Naruto';
12
+ return route;
13
+ }
14
+ function rawGateToV1(raw, gatePath, route) {
15
+ if (!raw)
16
+ return null;
17
+ // If already canonical schema, cast
18
+ if (raw.schema === 'sks.stop-gate.v1') {
19
+ return raw;
20
+ }
21
+ // Normalize from legacy naruto-gate.json / glm-naruto termination
22
+ const passed = raw.passed === true;
23
+ const status = passed ? 'passed' : (raw.status || 'blocked');
24
+ const terminalState = raw.terminal_state || (passed ? 'completed' : 'blocked');
25
+ const evidence = raw.evidence || {};
26
+ return {
27
+ schema: 'sks.stop-gate.v1',
28
+ route: route || String(raw.route || 'Naruto'),
29
+ route_command: String(raw.route_command || '$Naruto'),
30
+ mission_id: String(raw.mission_id || ''),
31
+ gate_file: path.basename(gatePath),
32
+ gate_abs_path: gatePath,
33
+ status: status,
34
+ passed,
35
+ terminal: raw.terminal === true || passed,
36
+ terminal_state: terminalState,
37
+ evidence: evidence,
38
+ blockers: Array.isArray(raw.blockers) ? raw.blockers : [],
39
+ missing_fields: Array.isArray(raw.missing_fields) ? raw.missing_fields : [],
40
+ created_at: String(raw.created_at || raw.updated_at || nowIso()),
41
+ };
42
+ }
43
+ async function checkHardBlocker(root, missionId) {
44
+ if (!missionId)
45
+ return { ok: false, file: null, reason: null, evidence: [] };
46
+ const file = path.join(missionDir(root, missionId), HARD_BLOCKER_FILE);
47
+ if (!(await exists(file)))
48
+ return { ok: false, file: null, reason: null, evidence: [] };
49
+ const blocker = await readJson(file, null);
50
+ if (!blocker)
51
+ return { ok: false, file, reason: null, evidence: [] };
52
+ const ok = blocker.passed === true
53
+ && String(blocker.reason || '').trim().length > 0
54
+ && Array.isArray(blocker.evidence)
55
+ && blocker.evidence.length > 0;
56
+ return { ok, file, reason: String(blocker.reason || ''), evidence: blocker.evidence || [] };
57
+ }
58
+ export async function checkStopGate(input) {
59
+ const root = path.resolve(input.root);
60
+ const resolution = await resolveStopGate({
61
+ root,
62
+ ...(input.route ? { route: input.route } : {}),
63
+ ...(input.missionId ? { missionId: input.missionId } : {}),
64
+ ...(input.explicitGatePath ? { explicitGatePath: input.explicitGatePath } : {}),
65
+ });
66
+ const route = normalizeRoute(resolution.route) ?? normalizeRoute(input.route ?? null) ?? 'Naruto';
67
+ const missionId = resolution.mission_id;
68
+ const statInfo = resolution.gate_path ? await gateStatInfo(resolution.gate_path) : { mtime: null, sha256: null };
69
+ // Check hard blocker first
70
+ const hardBlocker = await checkHardBlocker(root, missionId);
71
+ if (hardBlocker.ok) {
72
+ const action = 'hard_blocked';
73
+ const diagnostics = {
74
+ schema: 'sks.stop-gate-diagnostics.v1',
75
+ resolved_root: root,
76
+ route,
77
+ mission_id: missionId,
78
+ checked_paths: resolution.checked_paths,
79
+ selected_gate_path: hardBlocker.file,
80
+ selected_gate_schema: 'sks.hard-blocker.v1',
81
+ selected_gate_sha256: null,
82
+ selected_gate_mtime: null,
83
+ current_state_path: resolution.current_state_path,
84
+ current_state_mission_id: resolution.current_state_mission_id,
85
+ reason: `hard_blocker: ${hardBlocker.reason}`,
86
+ missing_fields: [],
87
+ blockers: [],
88
+ };
89
+ return {
90
+ schema: 'sks.stop-gate-check.v1',
91
+ ok: true,
92
+ action,
93
+ route,
94
+ mission_id: missionId,
95
+ gate_path: hardBlocker.file,
96
+ diagnostics,
97
+ feedback: `Stop allowed: hard blocker recorded with evidence. Reason: ${hardBlocker.reason}`,
98
+ };
99
+ }
100
+ const normalizedGate = rawGateToV1(resolution.gate_raw, resolution.gate_path || '', route);
101
+ if (!normalizedGate || !resolution.gate_path) {
102
+ const diagnostics = {
103
+ schema: 'sks.stop-gate-diagnostics.v1',
104
+ resolved_root: root,
105
+ route,
106
+ mission_id: missionId,
107
+ checked_paths: resolution.checked_paths,
108
+ selected_gate_path: null,
109
+ selected_gate_schema: null,
110
+ selected_gate_sha256: null,
111
+ selected_gate_mtime: null,
112
+ current_state_path: resolution.current_state_path,
113
+ current_state_mission_id: resolution.current_state_mission_id,
114
+ reason: 'no_gate_file_found',
115
+ missing_fields: [],
116
+ blockers: [],
117
+ };
118
+ return {
119
+ schema: 'sks.stop-gate-check.v1',
120
+ ok: false,
121
+ action: 'continue',
122
+ route,
123
+ mission_id: missionId,
124
+ gate_path: null,
125
+ diagnostics,
126
+ feedback: `Stop blocked: no gate file found. Checked paths: ${resolution.checked_paths.join(', ')}`,
127
+ };
128
+ }
129
+ const missingFields = [];
130
+ if (normalizedGate.passed !== true)
131
+ missingFields.push('passed');
132
+ if (!normalizedGate.terminal)
133
+ missingFields.push('terminal');
134
+ if (normalizedGate.passed === true && missingFields.length === 0) {
135
+ const action = 'allow_stop';
136
+ const diagnostics = {
137
+ schema: 'sks.stop-gate-diagnostics.v1',
138
+ resolved_root: root,
139
+ route,
140
+ mission_id: missionId,
141
+ checked_paths: resolution.checked_paths,
142
+ selected_gate_path: resolution.gate_path,
143
+ selected_gate_schema: resolution.gate_schema,
144
+ selected_gate_sha256: statInfo.sha256,
145
+ selected_gate_mtime: statInfo.mtime,
146
+ current_state_path: resolution.current_state_path,
147
+ current_state_mission_id: resolution.current_state_mission_id,
148
+ reason: 'gate_passed',
149
+ missing_fields: [],
150
+ blockers: [],
151
+ };
152
+ await writeDiagnostics(root, missionId, diagnostics);
153
+ return {
154
+ schema: 'sks.stop-gate-check.v1',
155
+ ok: true,
156
+ action,
157
+ route,
158
+ mission_id: missionId,
159
+ gate_path: resolution.gate_path,
160
+ normalized_gate: normalizedGate,
161
+ diagnostics,
162
+ feedback: `Stop allowed: gate passed at ${resolution.gate_path}`,
163
+ };
164
+ }
165
+ // Gate not passed
166
+ const action = 'continue';
167
+ const diagnostics = {
168
+ schema: 'sks.stop-gate-diagnostics.v1',
169
+ resolved_root: root,
170
+ route,
171
+ mission_id: missionId,
172
+ checked_paths: resolution.checked_paths,
173
+ selected_gate_path: resolution.gate_path,
174
+ selected_gate_schema: resolution.gate_schema,
175
+ selected_gate_sha256: statInfo.sha256,
176
+ selected_gate_mtime: statInfo.mtime,
177
+ current_state_path: resolution.current_state_path,
178
+ current_state_mission_id: resolution.current_state_mission_id,
179
+ reason: `gate_not_passed:${normalizedGate.status}`,
180
+ missing_fields: missingFields,
181
+ blockers: normalizedGate.blockers,
182
+ };
183
+ await writeDiagnostics(root, missionId, diagnostics);
184
+ return {
185
+ schema: 'sks.stop-gate-check.v1',
186
+ ok: false,
187
+ action,
188
+ route,
189
+ mission_id: missionId,
190
+ gate_path: resolution.gate_path,
191
+ normalized_gate: normalizedGate,
192
+ diagnostics,
193
+ feedback: `Stop blocked: gate not passed. Selected: ${resolution.gate_path}. Missing fields: ${missingFields.join(', ') || 'none'}. Checked: ${resolution.checked_paths.join(', ')}`,
194
+ };
195
+ }
196
+ async function writeDiagnostics(root, missionId, diagnostics) {
197
+ // Global report
198
+ const reportsDir = path.join(root, '.sneakoscope', 'reports');
199
+ await ensureDir(reportsDir);
200
+ await writeJsonAtomic(path.join(reportsDir, 'stop-gate-last-check.json'), diagnostics);
201
+ // Mission-local
202
+ if (missionId) {
203
+ const dir = missionDir(root, missionId);
204
+ await ensureDir(dir);
205
+ await writeJsonAtomic(path.join(dir, 'stop-gate-last-check.json'), diagnostics);
206
+ }
207
+ }
208
+ //# sourceMappingURL=stop-gate-check.js.map
@@ -0,0 +1,4 @@
1
+ export { checkStopGate } from './stop-gate-check.js';
2
+ export { resolveStopGate } from './stop-gate-resolver.js';
3
+ export { writeFinalStopGate } from './stop-gate-writer.js';
4
+ //# sourceMappingURL=stop-gate-diagnostics.js.map
@@ -0,0 +1,122 @@
1
+ import path from 'node:path';
2
+ import fsp from 'node:fs/promises';
3
+ import { exists, readJson, sha256 } from '../fsx.js';
4
+ import { missionDir, missionsDir, stateFile, findLatestMission } from '../mission.js';
5
+ const GATE_FILE_CANDIDATES = ['stop-gate.json', 'naruto-gate.json'];
6
+ const GLM_NARUTO_DIR = '.sneakoscope/glm-naruto';
7
+ async function statOrNull(filePath) {
8
+ try {
9
+ const stat = await fsp.stat(filePath);
10
+ const content = await fsp.readFile(filePath, 'utf8');
11
+ return { mtime: stat.mtime.toISOString(), sha: sha256(content), size: stat.size };
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ export async function resolveStopGate(input) {
18
+ const root = path.resolve(input.root);
19
+ const checkedPaths = [];
20
+ const route = input.route ?? null;
21
+ // 1. explicit absolute path
22
+ if (input.explicitGatePath) {
23
+ const abs = path.isAbsolute(input.explicitGatePath) ? input.explicitGatePath : path.resolve(root, input.explicitGatePath);
24
+ checkedPaths.push(abs);
25
+ if (await exists(abs)) {
26
+ const raw = await readJson(abs, null);
27
+ return makeResolution(root, route, null, abs, raw, checkedPaths, null, null, 'explicit_gate_path');
28
+ }
29
+ }
30
+ // 2. current.json → mission_id + stop_gate_abs_path
31
+ const statePath = stateFile(root);
32
+ let state = {};
33
+ let stateMissionId = null;
34
+ if (await exists(statePath)) {
35
+ checkedPaths.push(statePath);
36
+ state = await readJson(statePath, {});
37
+ stateMissionId = typeof state.mission_id === 'string' ? state.mission_id : null;
38
+ }
39
+ const missionId = input.missionId ?? stateMissionId;
40
+ // 2a. stop_gate_abs_path from current state
41
+ if (typeof state.stop_gate_abs_path === 'string' && state.stop_gate_abs_path) {
42
+ const abs = path.isAbsolute(state.stop_gate_abs_path) ? state.stop_gate_abs_path : path.resolve(root, state.stop_gate_abs_path);
43
+ checkedPaths.push(abs);
44
+ if (await exists(abs)) {
45
+ const raw = await readJson(abs, null);
46
+ return makeResolution(root, route, missionId, abs, raw, checkedPaths, statePath, stateMissionId, 'state.stop_gate_abs_path');
47
+ }
48
+ }
49
+ // 3. mission dir candidates
50
+ if (missionId) {
51
+ const dir = missionDir(root, missionId);
52
+ for (const file of GATE_FILE_CANDIDATES) {
53
+ const p = path.join(dir, file);
54
+ checkedPaths.push(p);
55
+ if (await exists(p)) {
56
+ const raw = await readJson(p, null);
57
+ return makeResolution(root, route, missionId, p, raw, checkedPaths, statePath, stateMissionId, 'mission_dir');
58
+ }
59
+ }
60
+ // GLM Naruto termination / mission-result
61
+ const glmDir = path.join(root, GLM_NARUTO_DIR, missionId);
62
+ for (const file of ['termination.json', 'mission-result.json']) {
63
+ const p = path.join(glmDir, file);
64
+ checkedPaths.push(p);
65
+ if (await exists(p)) {
66
+ const raw = await readJson(p, null);
67
+ return makeResolution(root, route, missionId, p, raw, checkedPaths, statePath, stateMissionId, 'glm_naruto_dir');
68
+ }
69
+ }
70
+ }
71
+ // 4. latest mission fallback
72
+ const latest = await findLatestMission(root);
73
+ if (latest) {
74
+ const dir = missionDir(root, latest);
75
+ for (const file of GATE_FILE_CANDIDATES) {
76
+ const p = path.join(dir, file);
77
+ checkedPaths.push(p);
78
+ if (await exists(p)) {
79
+ const raw = await readJson(p, null);
80
+ return makeResolution(root, route, latest, p, raw, checkedPaths, statePath, stateMissionId, 'latest_mission');
81
+ }
82
+ }
83
+ }
84
+ return makeResolution(root, route, missionId, null, null, checkedPaths, statePath, stateMissionId, 'no_gate_found');
85
+ }
86
+ function makeResolution(root, route, missionId, gatePath, gateRaw, checkedPaths, statePath, stateMissionId, reason) {
87
+ let gateSchema = null;
88
+ if (gateRaw) {
89
+ gateSchema = typeof gateRaw.schema === 'string' ? gateRaw.schema : guessSchemaFromPath(gatePath);
90
+ }
91
+ return {
92
+ root,
93
+ route,
94
+ mission_id: missionId,
95
+ gate_path: gatePath,
96
+ gate_schema: gateSchema,
97
+ gate_raw: gateRaw,
98
+ checked_paths: checkedPaths,
99
+ current_state_path: statePath,
100
+ current_state_mission_id: stateMissionId,
101
+ reason,
102
+ };
103
+ }
104
+ function guessSchemaFromPath(gatePath) {
105
+ if (!gatePath)
106
+ return null;
107
+ const base = path.basename(gatePath);
108
+ if (base === 'stop-gate.json' || base === 'stop-gate.latest.json')
109
+ return 'sks.stop-gate.v1';
110
+ if (base === 'naruto-gate.json')
111
+ return 'sks.naruto-gate';
112
+ if (base === 'termination.json')
113
+ return 'sks.glm-naruto-termination';
114
+ if (base === 'mission-result.json')
115
+ return 'sks.glm-naruto-mission-result';
116
+ return 'sks.gate';
117
+ }
118
+ export async function gateStatInfo(gatePath) {
119
+ const info = await statOrNull(gatePath);
120
+ return { mtime: info?.mtime ?? null, sha256: info?.sha ?? null };
121
+ }
122
+ //# sourceMappingURL=stop-gate-resolver.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=stop-gate-types.js.map
@@ -0,0 +1,76 @@
1
+ import path from 'node:path';
2
+ import { ensureDir, nowIso, readJson, writeJsonAtomic, exists } from '../fsx.js';
3
+ import { missionDir } from '../mission.js';
4
+ import { setCurrent } from '../mission.js';
5
+ export async function writeFinalStopGate(input) {
6
+ const dir = missionDir(input.root, input.missionId);
7
+ await ensureDir(dir);
8
+ const passed = input.status === 'passed';
9
+ const nativeGateFile = input.nativeGateFile ?? 'naruto-gate.json';
10
+ const nativeGatePath = path.join(dir, nativeGateFile);
11
+ const canonicalGatePath = path.join(dir, 'stop-gate.json');
12
+ const latestGatePath = path.join(dir, 'stop-gate.latest.json');
13
+ const verifyPath = path.join(dir, 'stop-gate-write-verify.json');
14
+ const gate = {
15
+ schema: 'sks.stop-gate.v1',
16
+ route: input.route,
17
+ route_command: input.routeCommand,
18
+ mission_id: input.missionId,
19
+ gate_file: nativeGateFile,
20
+ gate_abs_path: canonicalGatePath,
21
+ status: input.status,
22
+ passed,
23
+ terminal: input.terminal,
24
+ terminal_state: input.terminalState,
25
+ evidence: input.evidence,
26
+ blockers: input.blockers ?? [],
27
+ missing_fields: input.missingFields ?? [],
28
+ created_at: nowIso(),
29
+ };
30
+ // 1. Write route-native gate file (backwards compat)
31
+ await writeJsonAtomic(nativeGatePath, { ...gate, schema: 'sks.naruto-gate' });
32
+ // 2. Write canonical stop-gate.json
33
+ await writeJsonAtomic(canonicalGatePath, gate);
34
+ // 3. Write stop-gate.latest.json
35
+ await writeJsonAtomic(latestGatePath, gate);
36
+ // 4. Update current state with absolute path
37
+ await setCurrent(input.root, {
38
+ mission_id: input.missionId,
39
+ route: input.route,
40
+ route_command: input.routeCommand,
41
+ mode: input.route === 'GLM_NARUTO' ? 'NARUTO' : (input.route === 'Naruto' ? 'NARUTO' : input.route),
42
+ stop_gate: 'stop-gate.json',
43
+ stop_gate_abs_path: canonicalGatePath,
44
+ stop_gate_status: input.status,
45
+ stop_gate_passed: passed,
46
+ route_evidence_passed: input.evidence.route_evidence_passed ?? passed,
47
+ terminal: input.terminal,
48
+ terminal_state: input.terminalState,
49
+ });
50
+ // 5. Re-read and verify
51
+ const verifyResult = {
52
+ schema: 'sks.stop-gate-write-verify.v1',
53
+ verified: false,
54
+ checked_paths: [nativeGatePath, canonicalGatePath, latestGatePath],
55
+ created_at: nowIso(),
56
+ };
57
+ const errors = [];
58
+ for (const [label, p] of [['native', nativeGatePath], ['canonical', canonicalGatePath], ['latest', latestGatePath]]) {
59
+ if (!(await exists(p))) {
60
+ errors.push(`${label}:file_missing:${p}`);
61
+ continue;
62
+ }
63
+ const re = await readJson(p, null);
64
+ if (!re) {
65
+ errors.push(`${label}:unreadable`);
66
+ }
67
+ else if (re.passed !== passed) {
68
+ errors.push(`${label}:passed_mismatch:expected=${passed}:got=${re.passed}`);
69
+ }
70
+ }
71
+ verifyResult.errors = errors;
72
+ verifyResult.verified = errors.length === 0;
73
+ await writeJsonAtomic(verifyPath, verifyResult);
74
+ return gate;
75
+ }
76
+ //# sourceMappingURL=stop-gate-writer.js.map
@@ -1,2 +1,2 @@
1
- export const PACKAGE_VERSION = '4.0.6';
1
+ export const PACKAGE_VERSION = '4.0.9';
2
2
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "4.0.7",
4
+ "version": "4.0.9",
5
5
  "description": "Sneakoscope Codex: fast proof-first Codex trust layer with image-based Voxel TriWiki.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",