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.
- package/README.md +2 -2
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/crates/sks-core/src/main.rs +1 -1
- package/dist/bin/sks.js +1 -1
- package/dist/cli/command-registry.js +1 -0
- package/dist/core/commands/glm-command.js +8 -1
- package/dist/core/commands/naruto-command.js +25 -0
- package/dist/core/commands/stop-gate-command.js +63 -0
- package/dist/core/fsx.js +1 -1
- package/dist/core/pipeline-internals/runtime-gates.js +28 -4
- package/dist/core/providers/glm/glm-bench.js +4 -4
- package/dist/core/providers/glm/glm-direct-run.js +1 -1
- package/dist/core/providers/glm/glm-latency-trace.js +1 -1
- package/dist/core/providers/glm/glm-request-cache.js +10 -2
- package/dist/core/providers/glm/naruto/glm-naruto-artifacts.js +2 -0
- package/dist/core/providers/glm/naruto/glm-naruto-bench.js +68 -0
- package/dist/core/providers/glm/naruto/glm-naruto-budget.js +45 -0
- package/dist/core/providers/glm/naruto/glm-naruto-command.js +97 -0
- package/dist/core/providers/glm/naruto/glm-naruto-concurrency-governor.js +37 -0
- package/dist/core/providers/glm/naruto/glm-naruto-conflict-graph.js +74 -0
- package/dist/core/providers/glm/naruto/glm-naruto-decomposer.js +99 -0
- package/dist/core/providers/glm/naruto/glm-naruto-file-lease.js +23 -0
- package/dist/core/providers/glm/naruto/glm-naruto-finalizer.js +22 -0
- package/dist/core/providers/glm/naruto/glm-naruto-judge.js +84 -0
- package/dist/core/providers/glm/naruto/glm-naruto-merge-planner.js +57 -0
- package/dist/core/providers/glm/naruto/glm-naruto-orchestrator.js +277 -0
- package/dist/core/providers/glm/naruto/glm-naruto-patch-envelope.js +55 -0
- package/dist/core/providers/glm/naruto/glm-naruto-quorum.js +37 -0
- package/dist/core/providers/glm/naruto/glm-naruto-rate-limiter.js +18 -0
- package/dist/core/providers/glm/naruto/glm-naruto-repair-wave.js +21 -0
- package/dist/core/providers/glm/naruto/glm-naruto-shard-planner.js +32 -0
- package/dist/core/providers/glm/naruto/glm-naruto-trace.js +91 -0
- package/dist/core/providers/glm/naruto/glm-naruto-types.js +37 -0
- package/dist/core/providers/glm/naruto/glm-naruto-work-graph.js +2 -0
- package/dist/core/providers/glm/naruto/glm-naruto-worker-pool.js +79 -0
- package/dist/core/providers/glm/naruto/glm-naruto-worker-runtime.js +198 -0
- package/dist/core/providers/glm/naruto/glm-naruto-worker.js +2 -0
- package/dist/core/providers/glm/naruto/glm-naruto-worktree.js +48 -0
- package/dist/core/providers/openrouter/openrouter-provider-health.js +46 -0
- package/dist/core/providers/openrouter/openrouter-secret-store.js +33 -0
- package/dist/core/providers/openrouter/openrouter-stream.js +101 -8
- package/dist/core/stop-gate/stop-gate-check.js +208 -0
- package/dist/core/stop-gate/stop-gate-diagnostics.js +4 -0
- package/dist/core/stop-gate/stop-gate-resolver.js +122 -0
- package/dist/core/stop-gate/stop-gate-types.js +2 -0
- package/dist/core/stop-gate/stop-gate-writer.js +76 -0
- package/dist/core/version.js +1 -1
- 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
|
-
|
|
26
|
-
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
const text = await response.text();
|
|
27
27
|
return { ok: false, error: normalizeOpenRouterError(response.status, text) };
|
|
28
|
-
|
|
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
|
-
|
|
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,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,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
|
package/dist/core/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '4.0.
|
|
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.
|
|
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",
|