sneakoscope 4.0.9 → 4.0.10
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/dist/bin/sks.js +1 -1
- package/dist/core/fsx.js +1 -1
- package/dist/core/pipeline-internals/runtime-gates.js +5 -2
- package/dist/core/providers/glm/glm-request-cache.js +9 -4
- package/dist/core/providers/glm/naruto/glm-naruto-bench.js +47 -12
- package/dist/core/providers/glm/naruto/glm-naruto-combined-patch.js +49 -0
- package/dist/core/providers/glm/naruto/glm-naruto-conflict-graph.js +7 -1
- package/dist/core/providers/glm/naruto/glm-naruto-hunk-conflict.js +16 -0
- package/dist/core/providers/glm/naruto/glm-naruto-hunk-parser.js +36 -0
- package/dist/core/providers/glm/naruto/glm-naruto-orchestrator.js +24 -13
- package/dist/core/providers/glm/naruto/glm-naruto-patch-candidate-gate.js +42 -0
- package/dist/core/providers/glm/naruto/glm-naruto-patch-candidate-parser.js +62 -0
- package/dist/core/providers/glm/naruto/glm-naruto-secret-audit.js +39 -0
- package/dist/core/providers/glm/naruto/glm-naruto-verifier-output.js +26 -0
- package/dist/core/providers/glm/naruto/glm-naruto-worker-artifacts.js +42 -0
- package/dist/core/providers/glm/naruto/glm-naruto-worker-pool.js +30 -11
- package/dist/core/providers/glm/naruto/glm-naruto-worker-runtime.js +167 -15
- package/dist/core/providers/openrouter/openrouter-stream.js +26 -11
- package/dist/core/stop-gate/stop-gate-check.js +14 -2
- package/dist/core/stop-gate/stop-gate-writer.js +61 -11
- package/package.json +1 -1
package/dist/bin/sks.js
CHANGED
package/dist/core/fsx.js
CHANGED
|
@@ -5,7 +5,7 @@ import os from 'node:os';
|
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
export const PACKAGE_VERSION = '4.0.
|
|
8
|
+
export const PACKAGE_VERSION = '4.0.10';
|
|
9
9
|
export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
|
|
10
10
|
export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
|
|
11
11
|
export function nowIso() {
|
|
@@ -236,7 +236,9 @@ export async function evaluateStop(root, state, payload, opts = {}) {
|
|
|
236
236
|
if (state?.mission_id && state?.stop_gate && !['none', 'honest_mode', 'clarification-gate'].includes(state.stop_gate)) {
|
|
237
237
|
// 4.0.9: Use canonical stop-gate resolver first for NARUTO/GLM_NARUTO routes.
|
|
238
238
|
const modeUpper = String(state?.mode || '').toUpperCase();
|
|
239
|
-
|
|
239
|
+
const routeUpper = String(state?.route || state?.route_command || '').replace(/^\$/, '').toUpperCase();
|
|
240
|
+
const narutoFamily = modeUpper === 'NARUTO' || routeUpper === 'NARUTO' || routeUpper === 'GLM_NARUTO';
|
|
241
|
+
if (narutoFamily || state.stop_gate === 'stop-gate.json' || state.stop_gate === 'naruto-gate.json') {
|
|
240
242
|
const stopCheck = await checkStopGate({
|
|
241
243
|
root,
|
|
242
244
|
route: state.route || state.mode,
|
|
@@ -244,7 +246,8 @@ export async function evaluateStop(root, state, payload, opts = {}) {
|
|
|
244
246
|
explicitGatePath: typeof state.stop_gate_abs_path === 'string' && state.stop_gate_abs_path ? state.stop_gate_abs_path : undefined,
|
|
245
247
|
});
|
|
246
248
|
if (stopCheck.action === 'allow_stop') {
|
|
247
|
-
|
|
249
|
+
if (narutoFamily)
|
|
250
|
+
return { continue: true, systemMessage: `SKS: canonical stop-gate passed at ${stopCheck.gate_path}` };
|
|
248
251
|
}
|
|
249
252
|
else if (stopCheck.action === 'hard_blocked') {
|
|
250
253
|
return { continue: true, systemMessage: `SKS: ${stopCheck.feedback}` };
|
|
@@ -3,8 +3,10 @@ import { SksLruCache } from '../../perf/lru-cache.js';
|
|
|
3
3
|
export function createGlmEncodedRequestCache(maxEntries = 128) {
|
|
4
4
|
return new SksLruCache(maxEntries);
|
|
5
5
|
}
|
|
6
|
-
export function encodeGlmRequestWithCache(
|
|
7
|
-
const
|
|
6
|
+
export function encodeGlmRequestWithCache(input, cache = defaultEncodedRequestCache) {
|
|
7
|
+
const request = 'request' in input ? input.request : input;
|
|
8
|
+
const stringify = 'request' in input && input.stringify ? input.stringify : JSON.stringify;
|
|
9
|
+
const key = 'request' in input && input.cacheKeyParts ? digestRequestCacheKeyParts(input.cacheKeyParts) : digestRequestForCache(request);
|
|
8
10
|
const hit = cache.get(key);
|
|
9
11
|
// Fix 18.2: On cache hit, return stored body without JSON.stringify
|
|
10
12
|
if (hit) {
|
|
@@ -12,11 +14,11 @@ export function encodeGlmRequestWithCache(request, cache = defaultEncodedRequest
|
|
|
12
14
|
return { body: hit.body, entry: hit, cacheHit: true };
|
|
13
15
|
}
|
|
14
16
|
// Even for non-stored bodies, skip re-stringifying by computing from request
|
|
15
|
-
const body =
|
|
17
|
+
const body = stringify(request);
|
|
16
18
|
return { body, entry: hit, cacheHit: true };
|
|
17
19
|
}
|
|
18
20
|
// Cache miss: stringify once
|
|
19
|
-
const body =
|
|
21
|
+
const body = stringify(request);
|
|
20
22
|
if (containsSecretLikeContent(body)) {
|
|
21
23
|
const entry = {
|
|
22
24
|
key,
|
|
@@ -40,6 +42,9 @@ export function encodeGlmRequestWithCache(request, cache = defaultEncodedRequest
|
|
|
40
42
|
cache.set(key, entry);
|
|
41
43
|
return { body, entry, cacheHit: false };
|
|
42
44
|
}
|
|
45
|
+
export function digestRequestCacheKeyParts(parts) {
|
|
46
|
+
return crypto.createHash('sha256').update(stableStringify(parts)).digest('hex');
|
|
47
|
+
}
|
|
43
48
|
export function digestRequestForCache(request) {
|
|
44
49
|
const safe = {
|
|
45
50
|
model: request.model,
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { nowIso
|
|
1
|
+
import { nowIso } from '../../../fsx.js';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import fsp from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
3
5
|
import { GLM_52_OPENROUTER_MODEL } from '../glm-52-settings.js';
|
|
6
|
+
import { resolveOpenRouterApiKey } from '../../openrouter/openrouter-secret-store.js';
|
|
7
|
+
import { runGlmNarutoMission } from './glm-naruto-orchestrator.js';
|
|
4
8
|
export async function runGlmNarutoBench(root, args = []) {
|
|
5
9
|
const live = args.includes('--live');
|
|
6
10
|
const execute = args.includes('--execute');
|
|
@@ -11,7 +15,7 @@ export async function runGlmNarutoBench(root, args = []) {
|
|
|
11
15
|
if (!live) {
|
|
12
16
|
return {
|
|
13
17
|
schema: 'sks.glm-naruto-bench.v1',
|
|
14
|
-
version: '4.0.
|
|
18
|
+
version: '4.0.10',
|
|
15
19
|
generated_at: nowIso(),
|
|
16
20
|
status: 'dry_run',
|
|
17
21
|
model: GLM_52_OPENROUTER_MODEL,
|
|
@@ -27,29 +31,60 @@ export async function runGlmNarutoBench(root, args = []) {
|
|
|
27
31
|
warnings: ['dry_run_no_live_api_calls']
|
|
28
32
|
};
|
|
29
33
|
}
|
|
30
|
-
|
|
34
|
+
const key = await resolveOpenRouterApiKey({ env: process.env });
|
|
35
|
+
if (!key.key)
|
|
36
|
+
return blocked(root, ['live_bench_requires_openrouter_key']);
|
|
37
|
+
const fixture = await fsp.mkdtemp(path.join(os.tmpdir(), 'sks-glm-naruto-live-bench-'));
|
|
38
|
+
await fsp.mkdir(path.join(fixture, 'src'), { recursive: true });
|
|
39
|
+
await fsp.writeFile(path.join(fixture, 'src', 'bench-target.ts'), 'export const value = 1;\n', 'utf8');
|
|
40
|
+
const cases = [];
|
|
41
|
+
for (const workers of [1, 4, 8, 12]) {
|
|
42
|
+
const caseStarted = Date.now();
|
|
43
|
+
const result = await runGlmNarutoMission({
|
|
44
|
+
cwd: fixture,
|
|
45
|
+
task: 'Change src/bench-target.ts so value is 2. Return the smallest patch only.',
|
|
46
|
+
args: ['--bench', '--live', '--no-apply'],
|
|
47
|
+
missionId: `glm-naruto-live-bench-${workers}-${Date.now()}`,
|
|
48
|
+
maxWorkers: workers,
|
|
49
|
+
noApply: true
|
|
50
|
+
});
|
|
51
|
+
cases.push({
|
|
52
|
+
name: workers === 1 ? 'direct single GLM' : `GLM Naruto ${workers} workers`,
|
|
53
|
+
workers,
|
|
54
|
+
wall_clock_ms: Date.now() - caseStarted,
|
|
55
|
+
p50_ttft_ms: null,
|
|
56
|
+
p90_ttft_ms: null,
|
|
57
|
+
candidate_count: result.patch_candidates,
|
|
58
|
+
gate_pass_rate: result.patch_candidates ? result.gate_passed_candidates / result.patch_candidates : 0,
|
|
59
|
+
verifier_pass_rate: 0,
|
|
60
|
+
merge_success: result.mergeable_candidates > 0,
|
|
61
|
+
cached_tokens: 0,
|
|
62
|
+
cache_write_tokens: 0
|
|
63
|
+
});
|
|
64
|
+
}
|
|
31
65
|
return {
|
|
32
66
|
schema: 'sks.glm-naruto-bench.v1',
|
|
33
|
-
version: '4.0.
|
|
67
|
+
version: '4.0.10',
|
|
34
68
|
generated_at: nowIso(),
|
|
35
|
-
status: '
|
|
69
|
+
status: 'live',
|
|
36
70
|
model: GLM_52_OPENROUTER_MODEL,
|
|
37
71
|
gpt_fallback_allowed: false,
|
|
72
|
+
cases,
|
|
38
73
|
summary: {
|
|
39
|
-
simulated_workers:
|
|
40
|
-
simulated_waves:
|
|
41
|
-
simulated_patch_candidates: 0,
|
|
42
|
-
simulated_gate_passed: 0,
|
|
43
|
-
simulated_mergeable:
|
|
74
|
+
simulated_workers: Math.max(...cases.map((row) => row.workers)),
|
|
75
|
+
simulated_waves: cases.length,
|
|
76
|
+
simulated_patch_candidates: cases.reduce((sum, row) => sum + row.candidate_count, 0),
|
|
77
|
+
simulated_gate_passed: cases.reduce((sum, row) => sum + Math.round(row.candidate_count * row.gate_pass_rate), 0),
|
|
78
|
+
simulated_mergeable: cases.filter((row) => row.merge_success).length,
|
|
44
79
|
wall_clock_ms: Date.now() - started
|
|
45
80
|
},
|
|
46
|
-
warnings: ['
|
|
81
|
+
warnings: ['live_bench_no_apply_temp_repo']
|
|
47
82
|
};
|
|
48
83
|
}
|
|
49
84
|
function blocked(root, warnings) {
|
|
50
85
|
return {
|
|
51
86
|
schema: 'sks.glm-naruto-bench.v1',
|
|
52
|
-
version: '4.0.
|
|
87
|
+
version: '4.0.10',
|
|
53
88
|
generated_at: nowIso(),
|
|
54
89
|
status: 'blocked',
|
|
55
90
|
model: GLM_52_OPENROUTER_MODEL,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { checkAndApplyGlmPatch } from '../glm-patch-apply.js';
|
|
2
|
+
export function combineGlmNarutoPatches(envelopes, selectedPatchIds) {
|
|
3
|
+
const selected = selectedPatchIds
|
|
4
|
+
.map((patchId) => envelopes.find((env) => env.worker_id === patchId || env.patch_sha256 === patchId))
|
|
5
|
+
.filter((env) => Boolean(env))
|
|
6
|
+
.sort((a, b) => a.worker_id.localeCompare(b.worker_id));
|
|
7
|
+
return mergeDiffSections(selected.flatMap((env) => splitDiffSections(env.patch)));
|
|
8
|
+
}
|
|
9
|
+
export async function checkAndApplyCombinedGlmNarutoPatch(input) {
|
|
10
|
+
const patch = combineGlmNarutoPatches(input.envelopes, input.selectedPatchIds);
|
|
11
|
+
if (!patch.trim())
|
|
12
|
+
return { ok: false, patch, applied: [], blocker: 'combined_patch_empty' };
|
|
13
|
+
const checked = await checkAndApplyGlmPatch({ cwd: input.cwd, patch, apply: input.apply });
|
|
14
|
+
if (!checked.ok)
|
|
15
|
+
return { ok: false, patch, applied: [], blocker: checked.error.code };
|
|
16
|
+
return { ok: true, patch, applied: input.selectedPatchIds };
|
|
17
|
+
}
|
|
18
|
+
function mergeDiffSections(sections) {
|
|
19
|
+
const byFile = new Map();
|
|
20
|
+
for (const section of sections)
|
|
21
|
+
byFile.set(section.file, [...(byFile.get(section.file) || []), section]);
|
|
22
|
+
const merged = [];
|
|
23
|
+
for (const [file] of byFile) {
|
|
24
|
+
const group = byFile.get(file);
|
|
25
|
+
const first = group[0];
|
|
26
|
+
merged.push([...first.header, ...group.flatMap((section) => section.hunks)].join('\n').trimEnd());
|
|
27
|
+
}
|
|
28
|
+
return merged.filter(Boolean).join('\n\n') + (merged.length ? '\n' : '');
|
|
29
|
+
}
|
|
30
|
+
function splitDiffSections(patch) {
|
|
31
|
+
const rawSections = patch
|
|
32
|
+
.split(/(?=^diff --git )/m)
|
|
33
|
+
.map((section) => section.trim())
|
|
34
|
+
.filter(Boolean);
|
|
35
|
+
return rawSections.map((section) => {
|
|
36
|
+
const lines = section.split(/\r?\n/);
|
|
37
|
+
const diff = lines[0]?.match(/^diff --git a\/(.+?) b\/(.+)$/);
|
|
38
|
+
const file = diff?.[2] || diff?.[1] || lines[0] || 'unknown';
|
|
39
|
+
const firstHunk = lines.findIndex((line) => line.startsWith('@@ '));
|
|
40
|
+
if (firstHunk < 0)
|
|
41
|
+
return { file, header: lines, hunks: [] };
|
|
42
|
+
return {
|
|
43
|
+
file,
|
|
44
|
+
header: lines.slice(0, firstHunk),
|
|
45
|
+
hunks: lines.slice(firstHunk)
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=glm-naruto-combined-patch.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { envelopesHaveHunkConflict } from './glm-naruto-hunk-conflict.js';
|
|
1
2
|
export function buildConflictGraph(envelopes, nodes) {
|
|
2
3
|
const edges = [];
|
|
3
4
|
for (let i = 0; i < nodes.length; i++) {
|
|
@@ -23,11 +24,16 @@ function detectConflict(left, right, envelopes) {
|
|
|
23
24
|
if (leftEnv && rightEnv && leftEnv.base_digest !== rightEnv.base_digest) {
|
|
24
25
|
return { left_patch_id: left.patch_id, right_patch_id: right.patch_id, reason: 'base_digest_mismatch' };
|
|
25
26
|
}
|
|
27
|
+
if (left.shard_id === right.shard_id) {
|
|
28
|
+
return { left_patch_id: left.patch_id, right_patch_id: right.patch_id, reason: 'same_hunk' };
|
|
29
|
+
}
|
|
26
30
|
const leftPaths = new Set(left.target_paths);
|
|
27
31
|
const rightPaths = new Set(right.target_paths);
|
|
28
32
|
const sharedFiles = [...leftPaths].filter((p) => rightPaths.has(p));
|
|
29
33
|
if (sharedFiles.length > 0) {
|
|
30
|
-
if (
|
|
34
|
+
if (leftEnv && rightEnv) {
|
|
35
|
+
if (!envelopesHaveHunkConflict(leftEnv, rightEnv))
|
|
36
|
+
return null;
|
|
31
37
|
return { left_patch_id: left.patch_id, right_patch_id: right.patch_id, reason: 'same_hunk' };
|
|
32
38
|
}
|
|
33
39
|
return { left_patch_id: left.patch_id, right_patch_id: right.patch_id, reason: 'same_file' };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { hunksOverlap, parseUnifiedDiffHunks } from './glm-naruto-hunk-parser.js';
|
|
2
|
+
export function envelopesHaveHunkConflict(left, right) {
|
|
3
|
+
const leftHunks = parseUnifiedDiffHunks(left.patch);
|
|
4
|
+
const rightHunks = parseUnifiedDiffHunks(right.patch);
|
|
5
|
+
if (leftHunks.length === 0 || rightHunks.length === 0)
|
|
6
|
+
return sharesPath(left, right);
|
|
7
|
+
return leftHunks.some((leftHunk) => rightHunks.some((rightHunk) => hunksOverlap(leftHunk, rightHunk)));
|
|
8
|
+
}
|
|
9
|
+
export function envelopesShareFileButNotHunk(left, right) {
|
|
10
|
+
return sharesPath(left, right) && !envelopesHaveHunkConflict(left, right);
|
|
11
|
+
}
|
|
12
|
+
function sharesPath(left, right) {
|
|
13
|
+
const rightPaths = new Set(right.target_paths);
|
|
14
|
+
return left.target_paths.some((file) => rightPaths.has(file));
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=glm-naruto-hunk-conflict.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function parseUnifiedDiffHunks(patch) {
|
|
2
|
+
const hunks = [];
|
|
3
|
+
let currentFile = null;
|
|
4
|
+
for (const line of patch.split(/\r?\n/)) {
|
|
5
|
+
const diff = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
|
|
6
|
+
if (diff?.[2]) {
|
|
7
|
+
currentFile = diff[2];
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
const file = line.match(/^\+\+\+ b\/(.+)$/);
|
|
11
|
+
if (file?.[1] && file[1] !== '/dev/null')
|
|
12
|
+
currentFile = file[1];
|
|
13
|
+
const hunk = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/);
|
|
14
|
+
if (hunk && currentFile) {
|
|
15
|
+
hunks.push({
|
|
16
|
+
file: currentFile,
|
|
17
|
+
old_start: Number(hunk[1]),
|
|
18
|
+
old_lines: Number(hunk[2] || 1),
|
|
19
|
+
new_start: Number(hunk[3]),
|
|
20
|
+
new_lines: Number(hunk[4] || 1),
|
|
21
|
+
header: line
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return hunks;
|
|
26
|
+
}
|
|
27
|
+
export function hunksOverlap(left, right) {
|
|
28
|
+
if (left.file !== right.file)
|
|
29
|
+
return false;
|
|
30
|
+
return rangesOverlap(left.old_start, left.old_start + Math.max(1, left.old_lines) - 1, right.old_start, right.old_start + Math.max(1, right.old_lines) - 1)
|
|
31
|
+
|| rangesOverlap(left.new_start, left.new_start + Math.max(1, left.new_lines) - 1, right.new_start, right.new_start + Math.max(1, right.new_lines) - 1);
|
|
32
|
+
}
|
|
33
|
+
function rangesOverlap(aStart, aEnd, bStart, bEnd) {
|
|
34
|
+
return aStart <= bEnd && bStart <= aEnd;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=glm-naruto-hunk-parser.js.map
|
|
@@ -2,7 +2,6 @@ import path from 'node:path';
|
|
|
2
2
|
import { nowIso, writeJsonAtomic } from '../../../fsx.js';
|
|
3
3
|
import { GLM_52_OPENROUTER_MODEL } from '../glm-52-settings.js';
|
|
4
4
|
import { resolveOpenRouterApiKey } from '../../openrouter/openrouter-secret-store.js';
|
|
5
|
-
import { checkAndApplyGlmPatch } from '../glm-patch-apply.js';
|
|
6
5
|
import { decomposeTask, validateWorkGraph } from './glm-naruto-decomposer.js';
|
|
7
6
|
import { planShardCandidates, computeInitialLaneMix } from './glm-naruto-shard-planner.js';
|
|
8
7
|
import { runPatchWorkerPool } from './glm-naruto-worker-pool.js';
|
|
@@ -16,6 +15,8 @@ import { createProviderHealthTracker } from '../../openrouter/openrouter-provide
|
|
|
16
15
|
import { createMissionTrace, recordWorkerTrace, writeMissionArtifacts, buildMissionSummary } from './glm-naruto-trace.js';
|
|
17
16
|
import { runGlmJudge } from './glm-naruto-judge.js';
|
|
18
17
|
import { writeFinalStopGate } from '../../../stop-gate/stop-gate-writer.js';
|
|
18
|
+
import { checkAndApplyCombinedGlmNarutoPatch } from './glm-naruto-combined-patch.js';
|
|
19
|
+
import { auditGlmNarutoArtifactsForSecrets } from './glm-naruto-secret-audit.js';
|
|
19
20
|
import { GLM_NARUTO_LIMITS } from './glm-naruto-types.js';
|
|
20
21
|
export async function runGlmNarutoMission(input) {
|
|
21
22
|
const missionId = input.missionId || `glm-naruto-${nowIso().replace(/[:.]/g, '-')}`;
|
|
@@ -156,16 +157,14 @@ export async function runGlmNarutoMission(input) {
|
|
|
156
157
|
let appliedPatches = 0;
|
|
157
158
|
let applyResult = null;
|
|
158
159
|
if (!input.noApply && mergePlan.selected_patches.length > 0) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
applyResult = { ok: appliedPatches > 0, applied: mergePlan.selected_patches };
|
|
160
|
+
const combinedApply = await checkAndApplyCombinedGlmNarutoPatch({
|
|
161
|
+
cwd,
|
|
162
|
+
envelopes,
|
|
163
|
+
selectedPatchIds: mergePlan.selected_patches,
|
|
164
|
+
apply: true
|
|
165
|
+
});
|
|
166
|
+
appliedPatches = combinedApply.ok ? combinedApply.applied.length : 0;
|
|
167
|
+
applyResult = { ok: combinedApply.ok, applied: combinedApply.applied, ...(combinedApply.blocker ? { blocker: combinedApply.blocker } : {}) };
|
|
169
168
|
}
|
|
170
169
|
const terminalState = appliedPatches > 0 ? 'completed' : passedEnvelopes.length > 0 ? 'partial_candidates' : 'blocked';
|
|
171
170
|
const terminationReason = appliedPatches > 0 ? 'completed_merge_applied' : passedEnvelopes.length > 0 ? 'partial_no_apply' : 'no_gate_passed_candidates';
|
|
@@ -216,13 +215,21 @@ export async function runGlmNarutoMission(input) {
|
|
|
216
215
|
missionResult: result,
|
|
217
216
|
envelopes
|
|
218
217
|
});
|
|
218
|
+
const secretAudit = await auditGlmNarutoArtifactsForSecrets(path.join(cwd, '.sneakoscope', 'glm-naruto', missionId)).catch((err) => ({
|
|
219
|
+
schema: 'sks.glm-naruto-secret-audit.v1',
|
|
220
|
+
ok: false,
|
|
221
|
+
root: path.join(cwd, '.sneakoscope', 'glm-naruto', missionId),
|
|
222
|
+
scanned_files: 0,
|
|
223
|
+
findings: [`audit_failed:${err instanceof Error ? err.message : String(err)}`]
|
|
224
|
+
}));
|
|
225
|
+
await writeJsonAtomic(path.join(artifactDir, 'secret-audit.json'), secretAudit).catch(() => undefined);
|
|
219
226
|
// 4.0.9: Write canonical stop-gate artifacts for hook resolution.
|
|
220
227
|
await writeFinalStopGate({
|
|
221
228
|
root: cwd,
|
|
222
229
|
missionId,
|
|
223
230
|
route: 'GLM_NARUTO',
|
|
224
231
|
routeCommand: '$Naruto',
|
|
225
|
-
status: result.ok ? 'passed' : (terminalState === 'blocked' ? 'blocked' : 'failed'),
|
|
232
|
+
status: result.ok && secretAudit.ok ? 'passed' : (terminalState === 'blocked' || !secretAudit.ok ? 'blocked' : 'failed'),
|
|
226
233
|
terminal: terminalState === 'completed' || terminalState === 'blocked',
|
|
227
234
|
terminalState,
|
|
228
235
|
evidence: {
|
|
@@ -232,8 +239,12 @@ export async function runGlmNarutoMission(input) {
|
|
|
232
239
|
per_worker_artifacts: true,
|
|
233
240
|
verifier_wave_run: true,
|
|
234
241
|
model_guard_enforced: true,
|
|
242
|
+
proof_required: false,
|
|
243
|
+
proof_passed: true,
|
|
244
|
+
reflection_required: false,
|
|
245
|
+
reflection_passed: 'not_required',
|
|
235
246
|
},
|
|
236
|
-
blockers: result.blockers || [],
|
|
247
|
+
blockers: secretAudit.ok ? (result.blockers || []) : ['glm_naruto_secret_leak_detected'],
|
|
237
248
|
nativeGateFile: 'termination.json',
|
|
238
249
|
}).catch(() => null);
|
|
239
250
|
return { ...result, artifact_dir: artifactDir };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { checkAndApplyGlmPatch } from '../glm-patch-apply.js';
|
|
2
|
+
import { parseGlmNarutoPatchCandidate } from './glm-naruto-patch-candidate-parser.js';
|
|
3
|
+
const PROTECTED_PATH = /(^|\/)(\.github|dist|node_modules)(\/|$)/;
|
|
4
|
+
const SECRET_PATTERN = /\b(?:Bearer\s+[A-Za-z0-9._~+/-]+|sk-(?:or-)?[A-Za-z0-9_-]{12,}|OPENROUTER_API_KEY|SKS_OPENROUTER_API_KEY)\b/;
|
|
5
|
+
export async function evaluateGlmNarutoPatchCandidateGate(input) {
|
|
6
|
+
const checks = [];
|
|
7
|
+
const parsed = parseGlmNarutoPatchCandidate(input.envelope.patch);
|
|
8
|
+
checks.push(check('candidate_parse', parsed.ok, parsed.blockers.join(',') || undefined));
|
|
9
|
+
checks.push(check('patch_section', Boolean(parsed.patch), parsed.patch ? undefined : 'missing_patch_section'));
|
|
10
|
+
checks.push(check('unified_diff', /^diff --git /m.test(parsed.patch), parsed.patch ? undefined : 'no_diff_git'));
|
|
11
|
+
const blockedPath = parsed.touched_paths.find((file) => PROTECTED_PATH.test(file));
|
|
12
|
+
checks.push(check('protected_path_guard', !blockedPath, blockedPath));
|
|
13
|
+
const secretLeak = SECRET_PATTERN.test(parsed.patch);
|
|
14
|
+
checks.push(check('secret_leakage_guard', !secretLeak, secretLeak ? 'secret_like_content' : undefined));
|
|
15
|
+
if (parsed.ok && !blockedPath && !secretLeak) {
|
|
16
|
+
const applyCheck = await checkAndApplyGlmPatch({
|
|
17
|
+
cwd: input.cwd,
|
|
18
|
+
patch: parsed.patch,
|
|
19
|
+
apply: input.apply === true
|
|
20
|
+
});
|
|
21
|
+
checks.push(check('git_apply_check', applyCheck.ok, applyCheck.ok ? undefined : applyCheck.error.code));
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
checks.push({ id: 'git_apply_check', ok: false, reason: 'skipped_due_to_prior_blocker', ms: 0 });
|
|
25
|
+
}
|
|
26
|
+
const blockers = checks.filter((row) => !row.ok).map((row) => row.reason || row.id);
|
|
27
|
+
return {
|
|
28
|
+
schema: 'sks.glm-naruto-patch-candidate-gate.v1',
|
|
29
|
+
ok: blockers.length === 0,
|
|
30
|
+
worker_id: input.envelope.worker_id,
|
|
31
|
+
shard_id: input.envelope.shard_id,
|
|
32
|
+
patch_id: input.envelope.patch_sha256,
|
|
33
|
+
extracted_patch: parsed.patch,
|
|
34
|
+
touched_paths: parsed.touched_paths,
|
|
35
|
+
checks,
|
|
36
|
+
blockers
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function check(id, ok, reason) {
|
|
40
|
+
return { id, ok, ...(reason ? { reason } : {}), ms: 0 };
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=glm-naruto-patch-candidate-gate.js.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { parseUnifiedDiffPatch } from '../glm-patch-parser.js';
|
|
2
|
+
const CANDIDATE_OPEN = '<sks_patch_candidate>';
|
|
3
|
+
const CANDIDATE_CLOSE = '</sks_patch_candidate>';
|
|
4
|
+
const LEGACY_PATCH_OPEN = '<sks_patch>';
|
|
5
|
+
export function parseGlmNarutoPatchCandidate(text) {
|
|
6
|
+
const blockers = [];
|
|
7
|
+
if (text.includes(LEGACY_PATCH_OPEN) && !text.includes(CANDIDATE_OPEN)) {
|
|
8
|
+
return failed(text, ['legacy_sks_patch_envelope_rejected']);
|
|
9
|
+
}
|
|
10
|
+
const body = unwrapCandidateBody(text);
|
|
11
|
+
if (!body.ok)
|
|
12
|
+
return failed(text, body.blockers);
|
|
13
|
+
const extracted = extractPatchSection(body.body);
|
|
14
|
+
if (!extracted.ok)
|
|
15
|
+
return failed(body.body, extracted.blockers);
|
|
16
|
+
const parsed = parseUnifiedDiffPatch(extracted.patch);
|
|
17
|
+
if (parsed.empty)
|
|
18
|
+
blockers.push('patch_missing_unified_diff');
|
|
19
|
+
return {
|
|
20
|
+
ok: blockers.length === 0,
|
|
21
|
+
body: body.body,
|
|
22
|
+
patch: extracted.patch,
|
|
23
|
+
touched_paths: parsed.touchedPaths,
|
|
24
|
+
blockers
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function unwrapCandidateBody(text) {
|
|
28
|
+
const start = text.indexOf(CANDIDATE_OPEN);
|
|
29
|
+
const end = text.indexOf(CANDIDATE_CLOSE);
|
|
30
|
+
if (start >= 0 || end >= 0) {
|
|
31
|
+
if (start < 0 || end <= start)
|
|
32
|
+
return { ok: false, blockers: ['malformed_patch_candidate_envelope'] };
|
|
33
|
+
return { ok: true, body: text.slice(start + CANDIDATE_OPEN.length, end).trim() };
|
|
34
|
+
}
|
|
35
|
+
if (/^\s*patch\s*:/im.test(text))
|
|
36
|
+
return { ok: true, body: text.trim() };
|
|
37
|
+
return { ok: false, blockers: ['missing_patch_candidate_envelope'] };
|
|
38
|
+
}
|
|
39
|
+
function extractPatchSection(body) {
|
|
40
|
+
const lines = body.split(/\r?\n/);
|
|
41
|
+
const patchLine = lines.findIndex((line) => /^\s*patch\s*:/i.test(line));
|
|
42
|
+
if (patchLine < 0)
|
|
43
|
+
return { ok: false, blockers: ['missing_patch_section'] };
|
|
44
|
+
const firstLine = lines[patchLine] || '';
|
|
45
|
+
const inline = firstLine.replace(/^\s*patch\s*:\s*/i, '');
|
|
46
|
+
const tail = inline.trim() ? [inline, ...lines.slice(patchLine + 1)] : lines.slice(patchLine + 1);
|
|
47
|
+
const raw = tail.join('\n').replace(/^\s*```(?:diff|patch)?\s*/i, '').replace(/\s*```\s*$/i, '').trim();
|
|
48
|
+
const diffIndex = raw.search(/^diff --git /m);
|
|
49
|
+
if (diffIndex < 0)
|
|
50
|
+
return { ok: false, blockers: ['patch_missing_diff_git'] };
|
|
51
|
+
return { ok: true, patch: raw.slice(diffIndex).trimEnd() + '\n' };
|
|
52
|
+
}
|
|
53
|
+
function failed(body, blockers) {
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
body,
|
|
57
|
+
patch: '',
|
|
58
|
+
touched_paths: [],
|
|
59
|
+
blockers
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=glm-naruto-patch-candidate-parser.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fsp from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const SECRET_PATTERN = /\b(?:Bearer\s+[A-Za-z0-9._~+/-]+|sk-(?:or-)?[A-Za-z0-9_-]{12,}|OPENROUTER_API_KEY|SKS_OPENROUTER_API_KEY)\b/;
|
|
4
|
+
export async function auditGlmNarutoArtifactsForSecrets(root) {
|
|
5
|
+
const findings = [];
|
|
6
|
+
let scanned = 0;
|
|
7
|
+
await scan(root, async (file) => {
|
|
8
|
+
if (!/\.(json|jsonl|md|txt)$/i.test(file))
|
|
9
|
+
return;
|
|
10
|
+
scanned++;
|
|
11
|
+
const content = await fsp.readFile(file, 'utf8').catch(() => '');
|
|
12
|
+
if (SECRET_PATTERN.test(content))
|
|
13
|
+
findings.push(path.relative(root, file));
|
|
14
|
+
});
|
|
15
|
+
return {
|
|
16
|
+
schema: 'sks.glm-naruto-secret-audit.v1',
|
|
17
|
+
ok: findings.length === 0,
|
|
18
|
+
root,
|
|
19
|
+
scanned_files: scanned,
|
|
20
|
+
findings
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
async function scan(dir, visit) {
|
|
24
|
+
let entries;
|
|
25
|
+
try {
|
|
26
|
+
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
const p = path.join(dir, String(entry.name));
|
|
33
|
+
if (entry.isDirectory())
|
|
34
|
+
await scan(p, visit);
|
|
35
|
+
else if (entry.isFile())
|
|
36
|
+
await visit(p);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=glm-naruto-secret-audit.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function parseGlmNarutoVerifierOutput(content) {
|
|
2
|
+
try {
|
|
3
|
+
const parsed = JSON.parse(stripJsonFences(content));
|
|
4
|
+
const issues = [];
|
|
5
|
+
if (parsed.schema !== 'sks.glm-naruto-verifier-output.v1')
|
|
6
|
+
issues.push('schema');
|
|
7
|
+
if (typeof parsed.ok !== 'boolean')
|
|
8
|
+
issues.push('ok');
|
|
9
|
+
if (!Array.isArray(parsed.issues) || parsed.issues.some((issue) => typeof issue !== 'string'))
|
|
10
|
+
issues.push('issues');
|
|
11
|
+
if (typeof parsed.risk_score !== 'number' || parsed.risk_score < 0 || parsed.risk_score > 1)
|
|
12
|
+
issues.push('risk_score');
|
|
13
|
+
if (typeof parsed.confidence !== 'number' || parsed.confidence < 0 || parsed.confidence > 1)
|
|
14
|
+
issues.push('confidence');
|
|
15
|
+
if (issues.length)
|
|
16
|
+
return { ok: false, issues: issues.map((issue) => `invalid_${issue}`) };
|
|
17
|
+
return { ok: true, output: parsed, issues: [] };
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return { ok: false, issues: ['malformed_json'] };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function stripJsonFences(content) {
|
|
24
|
+
return content.trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```\s*$/i, '').trim();
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=glm-naruto-verifier-output.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { ensureDir, nowIso, writeJsonAtomic } from '../../../fsx.js';
|
|
3
|
+
export async function writeGlmNarutoWorkerArtifacts(input) {
|
|
4
|
+
const dir = path.join(input.root, '.sneakoscope', 'glm-naruto', input.missionId, 'workers', input.workerId);
|
|
5
|
+
await ensureDir(dir);
|
|
6
|
+
if (input.requestSummary) {
|
|
7
|
+
await writeJsonAtomic(path.join(dir, 'request-summary.json'), sanitizeArtifact({
|
|
8
|
+
schema: 'sks.glm-naruto-worker-request-summary.v1',
|
|
9
|
+
worker_id: input.workerId,
|
|
10
|
+
shard_id: input.shardId,
|
|
11
|
+
created_at: nowIso(),
|
|
12
|
+
...input.requestSummary
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
if (input.streamTrace)
|
|
16
|
+
await writeJsonAtomic(path.join(dir, 'stream-trace.json'), sanitizeArtifact(input.streamTrace));
|
|
17
|
+
if (input.patchEnvelope)
|
|
18
|
+
await writeJsonAtomic(path.join(dir, 'patch-envelope.json'), sanitizeArtifact(input.patchEnvelope));
|
|
19
|
+
if (input.gateResult)
|
|
20
|
+
await writeJsonAtomic(path.join(dir, 'gate-result.json'), sanitizeArtifact(input.gateResult));
|
|
21
|
+
if (input.termination) {
|
|
22
|
+
await writeJsonAtomic(path.join(dir, 'termination.json'), sanitizeArtifact({
|
|
23
|
+
schema: 'sks.glm-naruto-worker-termination.v1',
|
|
24
|
+
worker_id: input.workerId,
|
|
25
|
+
shard_id: input.shardId,
|
|
26
|
+
created_at: nowIso(),
|
|
27
|
+
...input.termination
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
return dir;
|
|
31
|
+
}
|
|
32
|
+
function sanitizeArtifact(value) {
|
|
33
|
+
return JSON.parse(JSON.stringify(value, (_key, raw) => {
|
|
34
|
+
if (typeof raw !== 'string')
|
|
35
|
+
return raw;
|
|
36
|
+
return raw
|
|
37
|
+
.replace(/Bearer\s+[A-Za-z0-9._~+/-]+/g, 'Bearer [REDACTED]')
|
|
38
|
+
.replace(/sk-or-[A-Za-z0-9_-]+/g, 'sk-or-[REDACTED]')
|
|
39
|
+
.replace(/sk-[A-Za-z0-9_-]{12,}/g, 'sk-[REDACTED]');
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=glm-naruto-worker-artifacts.js.map
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { runPatchWorker } from './glm-naruto-worker-runtime.js';
|
|
2
|
-
import { checkAndApplyGlmPatch } from '../glm-patch-apply.js';
|
|
3
|
-
import { evaluateGlmSpeedGate } from '../glm-speed-gate.js';
|
|
4
2
|
import { decideConcurrency } from './glm-naruto-concurrency-governor.js';
|
|
5
3
|
import { planFileLeases } from './glm-naruto-file-lease.js';
|
|
4
|
+
import { evaluateGlmNarutoPatchCandidateGate } from './glm-naruto-patch-candidate-gate.js';
|
|
5
|
+
import { createPatchEnvelope } from './glm-naruto-patch-envelope.js';
|
|
6
|
+
import { writeGlmNarutoWorkerArtifacts } from './glm-naruto-worker-artifacts.js';
|
|
6
7
|
export async function runPatchWorkerPool(input) {
|
|
7
8
|
const envelopes = [];
|
|
8
9
|
const traces = [];
|
|
@@ -34,6 +35,7 @@ export async function runPatchWorkerPool(input) {
|
|
|
34
35
|
apiKey: input.apiKey,
|
|
35
36
|
missionId: input.missionId,
|
|
36
37
|
workerId,
|
|
38
|
+
root: input.cwd,
|
|
37
39
|
shard: shardWithStrategy,
|
|
38
40
|
contextSummary: input.contextSummary,
|
|
39
41
|
timeoutMs: input.workerTimeoutMs
|
|
@@ -43,25 +45,42 @@ export async function runPatchWorkerPool(input) {
|
|
|
43
45
|
const results = await Promise.allSettled(workerTasks);
|
|
44
46
|
for (const result of results) {
|
|
45
47
|
if (result.status === 'fulfilled' && result.value.ok && result.value.envelope) {
|
|
46
|
-
const gate =
|
|
48
|
+
const gate = await evaluateGlmNarutoPatchCandidateGate({
|
|
49
|
+
cwd: input.cwd,
|
|
50
|
+
envelope: result.value.envelope,
|
|
51
|
+
apply: false
|
|
52
|
+
});
|
|
47
53
|
let envelope = result.value.envelope;
|
|
48
54
|
if (gate.ok) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
envelope = createPatchEnvelope({
|
|
56
|
+
missionId: envelope.mission_id,
|
|
57
|
+
workerId: envelope.worker_id,
|
|
58
|
+
shardId: envelope.shard_id,
|
|
59
|
+
baseDigest: envelope.base_digest,
|
|
60
|
+
patch: gate.extracted_patch,
|
|
61
|
+
strategy: envelope.strategy,
|
|
62
|
+
reasoningEffort: envelope.reasoning_effort,
|
|
63
|
+
status: 'gate_passed',
|
|
64
|
+
warnings: envelope.warnings
|
|
53
65
|
});
|
|
54
|
-
envelope = applyCheck.ok
|
|
55
|
-
? { ...envelope, status: 'gate_passed' }
|
|
56
|
-
: { ...envelope, status: 'gate_failed', blockers: [applyCheck.error.code] };
|
|
57
66
|
}
|
|
58
67
|
else {
|
|
59
68
|
envelope = {
|
|
60
69
|
...envelope,
|
|
61
70
|
status: 'gate_failed',
|
|
62
|
-
blockers: gate.
|
|
71
|
+
blockers: gate.blockers
|
|
63
72
|
};
|
|
64
73
|
}
|
|
74
|
+
await writeGlmNarutoWorkerArtifacts({
|
|
75
|
+
root: input.cwd,
|
|
76
|
+
missionId: input.missionId,
|
|
77
|
+
workerId: envelope.worker_id,
|
|
78
|
+
shardId: envelope.shard_id,
|
|
79
|
+
patchEnvelope: envelope,
|
|
80
|
+
gateResult: gate,
|
|
81
|
+
streamTrace: result.value.trace,
|
|
82
|
+
termination: { status: envelope.status, ok: gate.ok, blockers: envelope.blockers }
|
|
83
|
+
}).catch(() => undefined);
|
|
65
84
|
envelopes.push(envelope);
|
|
66
85
|
traces.push(result.value.trace);
|
|
67
86
|
}
|
|
@@ -6,9 +6,12 @@ import { sendOpenRouterChatCompletionStream } from '../../openrouter/openrouter-
|
|
|
6
6
|
import { assertGlm52ActualModel } from '../glm-52-response-guard.js';
|
|
7
7
|
import { encodeGlmRequestWithCache } from '../glm-request-cache.js';
|
|
8
8
|
import { parsePatchCandidateOutput, createPatchEnvelope, digestPatch } from './glm-naruto-patch-envelope.js';
|
|
9
|
+
import { parseGlmNarutoVerifierOutput } from './glm-naruto-verifier-output.js';
|
|
10
|
+
import { writeGlmNarutoWorkerArtifacts } from './glm-naruto-worker-artifacts.js';
|
|
9
11
|
const STABLE_SYSTEM_PREFIX = `You are a SKS GLM Naruto patch worker. Model lock: ${GLM_52_OPENROUTER_MODEL}. No GPT/OpenAI fallback allowed. Output only <sks_patch_candidate>, <sks_need_context>, or <sks_blocked> envelopes. Use unified diff format for patches. Never write to main workspace directly. Follow proof-first mutation rules.`;
|
|
10
12
|
export async function runPatchWorker(input) {
|
|
11
13
|
const started = Date.now();
|
|
14
|
+
const artifactRoot = input.root ?? process.cwd();
|
|
12
15
|
const sessionId = `sks-glm-naruto-${input.missionId}-${input.workerId}`;
|
|
13
16
|
const reasoningEffort = input.shard.reasoning;
|
|
14
17
|
const shardSuffix = JSON.stringify({
|
|
@@ -34,7 +37,38 @@ export async function runPatchWorker(input) {
|
|
|
34
37
|
providerSort: 'throughput'
|
|
35
38
|
});
|
|
36
39
|
const requestWithSession = { ...request, session_id: sessionId };
|
|
37
|
-
const
|
|
40
|
+
const stablePrefixDigest = crypto.createHash('sha256').update(STABLE_SYSTEM_PREFIX).digest('hex');
|
|
41
|
+
const shardSuffixDigest = crypto.createHash('sha256').update(shardSuffix).digest('hex');
|
|
42
|
+
const encoded = encodeGlmRequestWithCache({
|
|
43
|
+
request: requestWithSession,
|
|
44
|
+
cacheKeyParts: {
|
|
45
|
+
model: requestWithSession.model,
|
|
46
|
+
profile: 'glm-naruto-worker-speed',
|
|
47
|
+
stable_prefix_digest: stablePrefixDigest,
|
|
48
|
+
shard_suffix_digest: shardSuffixDigest,
|
|
49
|
+
tools_digest: requestWithSession.tools ? crypto.createHash('sha256').update(JSON.stringify(requestWithSession.tools)).digest('hex') : null,
|
|
50
|
+
response_format_digest: requestWithSession.response_format ? crypto.createHash('sha256').update(JSON.stringify(requestWithSession.response_format)).digest('hex') : null,
|
|
51
|
+
provider_digest: crypto.createHash('sha256').update(JSON.stringify(requestWithSession.provider ?? null)).digest('hex'),
|
|
52
|
+
session_id: sessionId
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
await writeGlmNarutoWorkerArtifacts({
|
|
56
|
+
root: artifactRoot,
|
|
57
|
+
missionId: input.missionId,
|
|
58
|
+
workerId: input.workerId,
|
|
59
|
+
shardId: input.shard.id,
|
|
60
|
+
requestSummary: {
|
|
61
|
+
model: requestWithSession.model,
|
|
62
|
+
provider: 'openrouter',
|
|
63
|
+
session_id: sessionId,
|
|
64
|
+
request_body_sha256: encoded.entry.bodySha256,
|
|
65
|
+
request_body_size: encoded.entry.byteLength,
|
|
66
|
+
request_body_stored: encoded.entry.bodyStored,
|
|
67
|
+
cache_hit: encoded.cacheHit,
|
|
68
|
+
stable_prefix_digest: stablePrefixDigest,
|
|
69
|
+
shard_suffix_digest: shardSuffixDigest
|
|
70
|
+
}
|
|
71
|
+
}).catch(() => undefined);
|
|
38
72
|
const traceBase = {
|
|
39
73
|
worker_id: input.workerId,
|
|
40
74
|
shard_id: input.shard.id,
|
|
@@ -54,35 +88,63 @@ export async function runPatchWorker(input) {
|
|
|
54
88
|
apiKey: input.apiKey,
|
|
55
89
|
request: requestWithSession,
|
|
56
90
|
timeoutMs: input.timeoutMs,
|
|
57
|
-
idleTimeoutMs: 60_000
|
|
91
|
+
idleTimeoutMs: 60_000,
|
|
92
|
+
...(input.fetchImpl ? { fetchImpl: input.fetchImpl } : {})
|
|
58
93
|
});
|
|
59
94
|
if (!response.ok) {
|
|
95
|
+
const trace = { ...traceBase, total_ms: Date.now() - started, status: 'failed' };
|
|
96
|
+
await writeGlmNarutoWorkerArtifacts({
|
|
97
|
+
root: artifactRoot,
|
|
98
|
+
missionId: input.missionId,
|
|
99
|
+
workerId: input.workerId,
|
|
100
|
+
shardId: input.shard.id,
|
|
101
|
+
streamTrace: trace,
|
|
102
|
+
termination: { status: 'failed', ok: false, error: response.error.code }
|
|
103
|
+
}).catch(() => undefined);
|
|
60
104
|
return {
|
|
61
105
|
envelope: null,
|
|
62
|
-
trace
|
|
106
|
+
trace,
|
|
63
107
|
ok: false,
|
|
64
108
|
error: response.error.code
|
|
65
109
|
};
|
|
66
110
|
}
|
|
67
111
|
const modelGuard = assertGlm52ActualModel(response.value.model);
|
|
68
112
|
if (!modelGuard.ok) {
|
|
113
|
+
const trace = { ...traceBase, total_ms: Date.now() - started, status: 'blocked' };
|
|
114
|
+
await writeGlmNarutoWorkerArtifacts({
|
|
115
|
+
root: artifactRoot,
|
|
116
|
+
missionId: input.missionId,
|
|
117
|
+
workerId: input.workerId,
|
|
118
|
+
shardId: input.shard.id,
|
|
119
|
+
streamTrace: trace,
|
|
120
|
+
termination: { status: 'blocked', ok: false, error: `model_guard:${modelGuard.code}` }
|
|
121
|
+
}).catch(() => undefined);
|
|
69
122
|
return {
|
|
70
123
|
envelope: null,
|
|
71
|
-
trace
|
|
124
|
+
trace,
|
|
72
125
|
ok: false,
|
|
73
126
|
error: `model_guard:${modelGuard.code}`
|
|
74
127
|
};
|
|
75
128
|
}
|
|
76
129
|
const parsed = parsePatchCandidateOutput(response.value.content);
|
|
77
130
|
if (parsed.kind !== 'patch') {
|
|
131
|
+
const trace = {
|
|
132
|
+
...traceBase,
|
|
133
|
+
ttft_ms: response.value.ttft_ms,
|
|
134
|
+
total_ms: Date.now() - started,
|
|
135
|
+
status: parsed.kind === 'blocked' ? 'blocked' : 'no_patch'
|
|
136
|
+
};
|
|
137
|
+
await writeGlmNarutoWorkerArtifacts({
|
|
138
|
+
root: artifactRoot,
|
|
139
|
+
missionId: input.missionId,
|
|
140
|
+
workerId: input.workerId,
|
|
141
|
+
shardId: input.shard.id,
|
|
142
|
+
streamTrace: trace,
|
|
143
|
+
termination: { status: trace.status, ok: false, error: parsed.kind }
|
|
144
|
+
}).catch(() => undefined);
|
|
78
145
|
return {
|
|
79
146
|
envelope: null,
|
|
80
|
-
trace
|
|
81
|
-
...traceBase,
|
|
82
|
-
ttft_ms: response.value.ttft_ms,
|
|
83
|
-
total_ms: Date.now() - started,
|
|
84
|
-
status: parsed.kind === 'blocked' ? 'blocked' : 'no_patch'
|
|
85
|
-
},
|
|
147
|
+
trace,
|
|
86
148
|
ok: false,
|
|
87
149
|
error: parsed.kind
|
|
88
150
|
};
|
|
@@ -103,12 +165,38 @@ export async function runPatchWorker(input) {
|
|
|
103
165
|
patch_digest: digestPatch(envelope.patch),
|
|
104
166
|
status: 'completed'
|
|
105
167
|
};
|
|
168
|
+
await writeGlmNarutoWorkerArtifacts({
|
|
169
|
+
root: artifactRoot,
|
|
170
|
+
missionId: input.missionId,
|
|
171
|
+
workerId: input.workerId,
|
|
172
|
+
shardId: input.shard.id,
|
|
173
|
+
streamTrace: trace,
|
|
174
|
+
patchEnvelope: envelope,
|
|
175
|
+
gateResult: {
|
|
176
|
+
schema: 'sks.glm-naruto-worker-gate-result.v1',
|
|
177
|
+
worker_id: input.workerId,
|
|
178
|
+
shard_id: input.shard.id,
|
|
179
|
+
status: 'pending_orchestrator_gate',
|
|
180
|
+
ok: false,
|
|
181
|
+
reason: 'deterministic_gate_runs_in_orchestrator_with_repo_cwd'
|
|
182
|
+
},
|
|
183
|
+
termination: { status: 'completed', ok: true }
|
|
184
|
+
}).catch(() => undefined);
|
|
106
185
|
return { envelope, trace, ok: true };
|
|
107
186
|
}
|
|
108
187
|
catch (err) {
|
|
188
|
+
const trace = { ...traceBase, total_ms: Date.now() - started, status: 'failed' };
|
|
189
|
+
await writeGlmNarutoWorkerArtifacts({
|
|
190
|
+
root: artifactRoot,
|
|
191
|
+
missionId: input.missionId,
|
|
192
|
+
workerId: input.workerId,
|
|
193
|
+
shardId: input.shard.id,
|
|
194
|
+
streamTrace: trace,
|
|
195
|
+
termination: { status: 'failed', ok: false, error: err instanceof Error ? err.message : String(err) }
|
|
196
|
+
}).catch(() => undefined);
|
|
109
197
|
return {
|
|
110
198
|
envelope: null,
|
|
111
|
-
trace
|
|
199
|
+
trace,
|
|
112
200
|
ok: false,
|
|
113
201
|
error: err instanceof Error ? err.message : String(err)
|
|
114
202
|
};
|
|
@@ -118,7 +206,7 @@ export async function runVerifierWorker(input) {
|
|
|
118
206
|
const started = Date.now();
|
|
119
207
|
const sessionId = `sks-glm-naruto-verify-${input.missionId}-${input.workerId}`;
|
|
120
208
|
const messages = [
|
|
121
|
-
{ role: 'system', content: `You are a SKS GLM Naruto verifier. Model: ${GLM_52_OPENROUTER_MODEL}. No GPT fallback.
|
|
209
|
+
{ role: 'system', content: `You are a SKS GLM Naruto verifier. Model: ${GLM_52_OPENROUTER_MODEL}. No GPT fallback. Return only JSON with schema "sks.glm-naruto-verifier-output.v1", ok boolean, issues string array, risk_score number 0..1, confidence number 0..1. Do not include markdown.` },
|
|
122
210
|
{ role: 'user', content: JSON.stringify({ patch_sha256: input.envelope.patch_sha256, target_paths: input.envelope.target_paths, patch: input.envelope.patch.slice(0, 4000) }) }
|
|
123
211
|
];
|
|
124
212
|
const request = buildGlm52Request({
|
|
@@ -126,14 +214,16 @@ export async function runVerifierWorker(input) {
|
|
|
126
214
|
messages,
|
|
127
215
|
maxTokens: 2048,
|
|
128
216
|
toolChoice: 'none',
|
|
129
|
-
parallelToolCalls: false
|
|
217
|
+
parallelToolCalls: false,
|
|
218
|
+
providerSort: 'throughput'
|
|
130
219
|
});
|
|
131
220
|
try {
|
|
132
221
|
const response = await sendOpenRouterChatCompletionStream({
|
|
133
222
|
apiKey: input.apiKey,
|
|
134
223
|
request: { ...request, session_id: sessionId },
|
|
135
224
|
timeoutMs: input.timeoutMs,
|
|
136
|
-
idleTimeoutMs: 60_000
|
|
225
|
+
idleTimeoutMs: 60_000,
|
|
226
|
+
...(input.fetchImpl ? { fetchImpl: input.fetchImpl } : {})
|
|
137
227
|
});
|
|
138
228
|
if (!response.ok) {
|
|
139
229
|
return {
|
|
@@ -155,6 +245,68 @@ export async function runVerifierWorker(input) {
|
|
|
155
245
|
issues: [response.error.code]
|
|
156
246
|
};
|
|
157
247
|
}
|
|
248
|
+
const modelGuard = assertGlm52ActualModel(response.value.model);
|
|
249
|
+
if (!modelGuard.ok) {
|
|
250
|
+
return {
|
|
251
|
+
ok: false,
|
|
252
|
+
trace: {
|
|
253
|
+
worker_id: input.workerId,
|
|
254
|
+
shard_id: input.envelope.shard_id,
|
|
255
|
+
strategy: 'minimal_patch',
|
|
256
|
+
model: GLM_52_OPENROUTER_MODEL,
|
|
257
|
+
provider: 'openrouter',
|
|
258
|
+
session_id: sessionId,
|
|
259
|
+
ttft_ms: response.value.ttft_ms,
|
|
260
|
+
total_ms: Date.now() - started,
|
|
261
|
+
request_cache_hit: false,
|
|
262
|
+
output_digest: crypto.createHash('sha256').update(response.value.content).digest('hex'),
|
|
263
|
+
patch_digest: input.envelope.patch_sha256,
|
|
264
|
+
status: 'verification_failed'
|
|
265
|
+
},
|
|
266
|
+
issues: [`model_guard:${modelGuard.code}`]
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
const parsed = parseGlmNarutoVerifierOutput(response.value.content);
|
|
270
|
+
if (!parsed.ok || !parsed.output) {
|
|
271
|
+
return {
|
|
272
|
+
ok: false,
|
|
273
|
+
trace: {
|
|
274
|
+
worker_id: input.workerId,
|
|
275
|
+
shard_id: input.envelope.shard_id,
|
|
276
|
+
strategy: 'minimal_patch',
|
|
277
|
+
model: GLM_52_OPENROUTER_MODEL,
|
|
278
|
+
provider: 'openrouter',
|
|
279
|
+
session_id: sessionId,
|
|
280
|
+
ttft_ms: response.value.ttft_ms,
|
|
281
|
+
total_ms: Date.now() - started,
|
|
282
|
+
request_cache_hit: false,
|
|
283
|
+
output_digest: crypto.createHash('sha256').update(response.value.content).digest('hex'),
|
|
284
|
+
patch_digest: input.envelope.patch_sha256,
|
|
285
|
+
status: 'verification_failed'
|
|
286
|
+
},
|
|
287
|
+
issues: parsed.issues
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
if (!parsed.output.ok) {
|
|
291
|
+
return {
|
|
292
|
+
ok: false,
|
|
293
|
+
trace: {
|
|
294
|
+
worker_id: input.workerId,
|
|
295
|
+
shard_id: input.envelope.shard_id,
|
|
296
|
+
strategy: 'minimal_patch',
|
|
297
|
+
model: GLM_52_OPENROUTER_MODEL,
|
|
298
|
+
provider: 'openrouter',
|
|
299
|
+
session_id: sessionId,
|
|
300
|
+
ttft_ms: response.value.ttft_ms,
|
|
301
|
+
total_ms: Date.now() - started,
|
|
302
|
+
request_cache_hit: false,
|
|
303
|
+
output_digest: crypto.createHash('sha256').update(response.value.content).digest('hex'),
|
|
304
|
+
patch_digest: input.envelope.patch_sha256,
|
|
305
|
+
status: 'verification_failed'
|
|
306
|
+
},
|
|
307
|
+
issues: parsed.output.issues
|
|
308
|
+
};
|
|
309
|
+
}
|
|
158
310
|
return {
|
|
159
311
|
ok: true,
|
|
160
312
|
trace: {
|
|
@@ -169,7 +321,7 @@ export async function runVerifierWorker(input) {
|
|
|
169
321
|
request_cache_hit: false,
|
|
170
322
|
output_digest: crypto.createHash('sha256').update(response.value.content).digest('hex'),
|
|
171
323
|
patch_digest: input.envelope.patch_sha256,
|
|
172
|
-
status: '
|
|
324
|
+
status: 'verification_passed'
|
|
173
325
|
},
|
|
174
326
|
issues: []
|
|
175
327
|
};
|
|
@@ -2,6 +2,15 @@ import { redactOpenRouterString } from '../../security/redact-secrets.js';
|
|
|
2
2
|
import { OPENROUTER_CHAT_COMPLETIONS_URL } from './openrouter-types.js';
|
|
3
3
|
import { normalizeOpenRouterError } from './openrouter-error.js';
|
|
4
4
|
import { encodeGlmRequestWithCache } from '../glm/glm-request-cache.js';
|
|
5
|
+
export class GlmStreamIdleTimeout extends Error {
|
|
6
|
+
code;
|
|
7
|
+
constructor(afterTtft) {
|
|
8
|
+
const code = afterTtft ? 'glm_stream_idle_timeout_after_ttft' : 'glm_stream_idle_timeout';
|
|
9
|
+
super(code);
|
|
10
|
+
this.name = 'GlmStreamIdleTimeout';
|
|
11
|
+
this.code = code;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
5
14
|
export async function sendOpenRouterChatCompletionStream(input) {
|
|
6
15
|
const started = Date.now();
|
|
7
16
|
const controller = input.timeoutMs ? new AbortController() : null;
|
|
@@ -37,12 +46,13 @@ export async function sendOpenRouterChatCompletionStream(input) {
|
|
|
37
46
|
catch (err) {
|
|
38
47
|
if (timeout)
|
|
39
48
|
clearTimeout(timeout);
|
|
40
|
-
if (err instanceof Error && (err.name === 'AbortError' || err.message === 'glm_stream_idle_timeout' || err.message === 'glm_stream_idle_timeout_after_ttft')) {
|
|
41
|
-
const
|
|
49
|
+
if (err instanceof Error && (err.name === 'AbortError' || err instanceof GlmStreamIdleTimeout || err.message === 'glm_stream_idle_timeout' || err.message === 'glm_stream_idle_timeout_after_ttft')) {
|
|
50
|
+
const code = err instanceof GlmStreamIdleTimeout ? err.code : err.message;
|
|
51
|
+
const isIdle = code.startsWith('glm_stream_idle');
|
|
42
52
|
return {
|
|
43
53
|
ok: false,
|
|
44
54
|
error: {
|
|
45
|
-
code: isIdle ?
|
|
55
|
+
code: isIdle ? code : 'glm_request_timeout',
|
|
46
56
|
message: isIdle ? `OpenRouter stream idle timeout after ${input.idleTimeoutMs || 0}ms.` : `OpenRouter stream aborted after ${input.timeoutMs || 'external'}ms.`,
|
|
47
57
|
severity: 'failed'
|
|
48
58
|
}
|
|
@@ -71,18 +81,23 @@ async function readRealStream(body, startedAtMs, idleTimeoutMs) {
|
|
|
71
81
|
let lastChunkMs = startedAtMs;
|
|
72
82
|
try {
|
|
73
83
|
while (true) {
|
|
74
|
-
// 4.0.9: Idle timeout between chunks — abort if stream stalls.
|
|
75
84
|
const readPromise = reader.read();
|
|
76
85
|
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
86
|
let result;
|
|
84
87
|
try {
|
|
85
|
-
result =
|
|
88
|
+
result = idleTimeoutMs && idleTimeoutMs > 0
|
|
89
|
+
? await Promise.race([
|
|
90
|
+
readPromise,
|
|
91
|
+
new Promise((_, reject) => {
|
|
92
|
+
idleTimer = setTimeout(() => reject(new GlmStreamIdleTimeout(ttft !== null)), idleTimeoutMs);
|
|
93
|
+
})
|
|
94
|
+
])
|
|
95
|
+
: await readPromise;
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
if (err instanceof GlmStreamIdleTimeout)
|
|
99
|
+
await reader.cancel(err.code).catch(() => undefined);
|
|
100
|
+
throw err;
|
|
86
101
|
}
|
|
87
102
|
finally {
|
|
88
103
|
if (idleTimer)
|
|
@@ -86,6 +86,7 @@ export async function checkStopGate(input) {
|
|
|
86
86
|
missing_fields: [],
|
|
87
87
|
blockers: [],
|
|
88
88
|
};
|
|
89
|
+
await writeDiagnostics(root, missionId, diagnostics);
|
|
89
90
|
return {
|
|
90
91
|
schema: 'sks.stop-gate-check.v1',
|
|
91
92
|
ok: true,
|
|
@@ -115,6 +116,7 @@ export async function checkStopGate(input) {
|
|
|
115
116
|
missing_fields: [],
|
|
116
117
|
blockers: [],
|
|
117
118
|
};
|
|
119
|
+
await writeDiagnostics(root, missionId, diagnostics);
|
|
118
120
|
return {
|
|
119
121
|
schema: 'sks.stop-gate-check.v1',
|
|
120
122
|
ok: false,
|
|
@@ -129,9 +131,19 @@ export async function checkStopGate(input) {
|
|
|
129
131
|
const missingFields = [];
|
|
130
132
|
if (normalizedGate.passed !== true)
|
|
131
133
|
missingFields.push('passed');
|
|
132
|
-
if (
|
|
134
|
+
if (normalizedGate.terminal !== true)
|
|
133
135
|
missingFields.push('terminal');
|
|
134
|
-
if (normalizedGate.
|
|
136
|
+
if (normalizedGate.status !== 'passed')
|
|
137
|
+
missingFields.push('status');
|
|
138
|
+
if (normalizedGate.blockers.length > 0)
|
|
139
|
+
missingFields.push('blockers');
|
|
140
|
+
if (normalizedGate.missing_fields.length > 0)
|
|
141
|
+
missingFields.push(...normalizedGate.missing_fields.map((field) => `missing_fields:${field}`));
|
|
142
|
+
if (normalizedGate.passed === true
|
|
143
|
+
&& normalizedGate.terminal === true
|
|
144
|
+
&& normalizedGate.status === 'passed'
|
|
145
|
+
&& normalizedGate.blockers.length === 0
|
|
146
|
+
&& normalizedGate.missing_fields.length === 0) {
|
|
135
147
|
const action = 'allow_stop';
|
|
136
148
|
const diagnostics = {
|
|
137
149
|
schema: 'sks.stop-gate-diagnostics.v1',
|
|
@@ -5,7 +5,11 @@ import { setCurrent } from '../mission.js';
|
|
|
5
5
|
export async function writeFinalStopGate(input) {
|
|
6
6
|
const dir = missionDir(input.root, input.missionId);
|
|
7
7
|
await ensureDir(dir);
|
|
8
|
-
const
|
|
8
|
+
const evidenceBlockers = input.status === 'passed' ? evidenceMissingBlockers(input.evidence) : [];
|
|
9
|
+
const status = evidenceBlockers.length ? 'blocked' : input.status;
|
|
10
|
+
const blockers = [...(input.blockers ?? []), ...evidenceBlockers];
|
|
11
|
+
const missingFields = [...(input.missingFields ?? []), ...evidenceBlockers.map((blocker) => `evidence:${blocker}`)];
|
|
12
|
+
const passed = status === 'passed';
|
|
9
13
|
const nativeGateFile = input.nativeGateFile ?? 'naruto-gate.json';
|
|
10
14
|
const nativeGatePath = path.join(dir, nativeGateFile);
|
|
11
15
|
const canonicalGatePath = path.join(dir, 'stop-gate.json');
|
|
@@ -18,17 +22,55 @@ export async function writeFinalStopGate(input) {
|
|
|
18
22
|
mission_id: input.missionId,
|
|
19
23
|
gate_file: nativeGateFile,
|
|
20
24
|
gate_abs_path: canonicalGatePath,
|
|
21
|
-
status
|
|
25
|
+
status,
|
|
22
26
|
passed,
|
|
23
|
-
terminal: input.terminal,
|
|
24
|
-
terminal_state: input.terminalState,
|
|
27
|
+
terminal: evidenceBlockers.length ? false : input.terminal,
|
|
28
|
+
terminal_state: evidenceBlockers.length ? 'blocked' : input.terminalState,
|
|
25
29
|
evidence: input.evidence,
|
|
26
|
-
blockers
|
|
27
|
-
missing_fields:
|
|
30
|
+
blockers,
|
|
31
|
+
missing_fields: missingFields,
|
|
28
32
|
created_at: nowIso(),
|
|
29
33
|
};
|
|
30
|
-
// 1. Write route-native gate file (backwards compat)
|
|
31
|
-
|
|
34
|
+
// 1. Write route-native gate file (backwards compat), preserving detailed native fields by default.
|
|
35
|
+
const preserveNativeGate = input.preserveNativeGate !== false;
|
|
36
|
+
if (preserveNativeGate && await exists(nativeGatePath)) {
|
|
37
|
+
const existing = await readJson(nativeGatePath, {});
|
|
38
|
+
await writeJsonAtomic(nativeGatePath, {
|
|
39
|
+
...existing,
|
|
40
|
+
...(input.nativeGatePatch ?? {}),
|
|
41
|
+
route: input.route,
|
|
42
|
+
route_command: input.routeCommand,
|
|
43
|
+
mission_id: input.missionId,
|
|
44
|
+
status,
|
|
45
|
+
passed,
|
|
46
|
+
terminal: gate.terminal,
|
|
47
|
+
terminal_state: gate.terminal_state,
|
|
48
|
+
evidence: {
|
|
49
|
+
...(existing.evidence || {}),
|
|
50
|
+
...input.evidence
|
|
51
|
+
},
|
|
52
|
+
blockers,
|
|
53
|
+
missing_fields: missingFields,
|
|
54
|
+
updated_at: nowIso()
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
await writeJsonAtomic(nativeGatePath, {
|
|
59
|
+
schema: nativeGateFile === 'termination.json' ? 'sks.glm-naruto-termination.v1' : 'sks.naruto-gate.v1',
|
|
60
|
+
route: input.route,
|
|
61
|
+
route_command: input.routeCommand,
|
|
62
|
+
mission_id: input.missionId,
|
|
63
|
+
status,
|
|
64
|
+
passed,
|
|
65
|
+
terminal: gate.terminal,
|
|
66
|
+
terminal_state: gate.terminal_state,
|
|
67
|
+
evidence: input.evidence,
|
|
68
|
+
blockers,
|
|
69
|
+
missing_fields: missingFields,
|
|
70
|
+
updated_at: nowIso(),
|
|
71
|
+
...(input.nativeGatePatch ?? {})
|
|
72
|
+
});
|
|
73
|
+
}
|
|
32
74
|
// 2. Write canonical stop-gate.json
|
|
33
75
|
await writeJsonAtomic(canonicalGatePath, gate);
|
|
34
76
|
// 3. Write stop-gate.latest.json
|
|
@@ -41,11 +83,11 @@ export async function writeFinalStopGate(input) {
|
|
|
41
83
|
mode: input.route === 'GLM_NARUTO' ? 'NARUTO' : (input.route === 'Naruto' ? 'NARUTO' : input.route),
|
|
42
84
|
stop_gate: 'stop-gate.json',
|
|
43
85
|
stop_gate_abs_path: canonicalGatePath,
|
|
44
|
-
stop_gate_status:
|
|
86
|
+
stop_gate_status: status,
|
|
45
87
|
stop_gate_passed: passed,
|
|
46
88
|
route_evidence_passed: input.evidence.route_evidence_passed ?? passed,
|
|
47
|
-
terminal:
|
|
48
|
-
terminal_state:
|
|
89
|
+
terminal: gate.terminal,
|
|
90
|
+
terminal_state: gate.terminal_state,
|
|
49
91
|
});
|
|
50
92
|
// 5. Re-read and verify
|
|
51
93
|
const verifyResult = {
|
|
@@ -73,4 +115,12 @@ export async function writeFinalStopGate(input) {
|
|
|
73
115
|
await writeJsonAtomic(verifyPath, verifyResult);
|
|
74
116
|
return gate;
|
|
75
117
|
}
|
|
118
|
+
function evidenceMissingBlockers(evidence) {
|
|
119
|
+
const blockers = [];
|
|
120
|
+
if (evidence.proof_required === true && evidence.proof_passed !== true)
|
|
121
|
+
blockers.push('proof_not_passed');
|
|
122
|
+
if (evidence.reflection_required === true && evidence.reflection_passed !== true && evidence.reflection_passed !== 'not_required')
|
|
123
|
+
blockers.push('reflection_not_passed');
|
|
124
|
+
return blockers;
|
|
125
|
+
}
|
|
76
126
|
//# sourceMappingURL=stop-gate-writer.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.10",
|
|
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",
|