sneakoscope 4.0.8 → 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/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/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/naruto/glm-naruto-bench.js +3 -3
- package/dist/core/providers/glm/naruto/glm-naruto-judge.js +1 -1
- package/dist/core/providers/glm/naruto/glm-naruto-orchestrator.js +55 -2
- package/dist/core/providers/glm/naruto/glm-naruto-trace.js +41 -1
- package/dist/core/providers/glm/naruto/glm-naruto-worker-runtime.js +5 -3
- package/dist/core/providers/openrouter/openrouter-stream.js +31 -6
- 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
|
@@ -4,7 +4,7 @@ use std::io::{self, Read, Seek, SeekFrom};
|
|
|
4
4
|
fn main() {
|
|
5
5
|
let mut args = std::env::args().skip(1);
|
|
6
6
|
match args.next().as_deref() {
|
|
7
|
-
Some("--version") => println!("sks-rs 4.0.
|
|
7
|
+
Some("--version") => println!("sks-rs 4.0.9"),
|
|
8
8
|
Some("compact-info") => {
|
|
9
9
|
let mut input = String::new();
|
|
10
10
|
let _ = io::stdin().read_to_string(&mut input);
|
package/dist/bin/sks.js
CHANGED
|
@@ -123,6 +123,7 @@ export const COMMANDS = {
|
|
|
123
123
|
agent: entry('beta', 'Run native multi-session agent missions', 'dist/core/commands/agent-command.js', argsCommand(() => import('../core/commands/agent-command.js'), 'agentCommand', 'dist/core/commands/agent-command.js')),
|
|
124
124
|
'with-local-llm': entry('beta', 'Enable or inspect local Ollama worker backend', 'dist/core/commands/local-model-command.js', argsCommand(() => import('../core/commands/local-model-command.js'), 'localModelCommand', 'dist/core/commands/local-model-command.js')),
|
|
125
125
|
naruto: entry('labs', 'Run $Naruto shadow-clone swarm (up to 100 parallel sessions)', 'dist/core/commands/naruto-command.js', argsCommand(() => import('../core/commands/naruto-command.js'), 'narutoCommand', 'dist/core/commands/naruto-command.js')),
|
|
126
|
+
'stop-gate': entry('beta', 'Check canonical stop-gate resolution for a route/mission', 'dist/core/commands/stop-gate-command.js', commandArgsCommand(() => import('../core/commands/stop-gate-command.js'), 'stopGateCommand', 'dist/core/commands/stop-gate-command.js')),
|
|
126
127
|
loop: entry('labs', 'Dynamic Loop Runtime: plan/run/status/proof loop graphs.', 'dist/core/commands/loop-command.js', subcommand(() => import('../core/commands/loop-command.js'), 'loopCommand', 'dist/core/commands/loop-command.js', 'help')),
|
|
127
128
|
'qa-loop': entry('beta', 'Run QA loop missions', 'dist/core/commands/qa-loop-command.js', subcommand(() => import('../core/commands/qa-loop-command.js'), 'qaLoopCommand', 'dist/core/commands/qa-loop-command.js')),
|
|
128
129
|
research: entry('labs', 'Run research missions', 'dist/core/commands/research-command.js', subcommand(() => import('../core/commands/research-command.js'), 'researchCommand', 'dist/core/commands/research-command.js')),
|
|
@@ -25,6 +25,7 @@ import { evaluateGitWorktreeCapability } from '../git/git-worktree-capability.js
|
|
|
25
25
|
import { buildRuntimeProofSummary, renderRuntimeProofSummary } from '../agents/runtime-proof-summary.js';
|
|
26
26
|
import { writeCodex0138CapabilityArtifacts } from '../codex-control/codex-0138-capability.js';
|
|
27
27
|
import { writeCodex0139CapabilityArtifacts } from '../codex-control/codex-0139-capability.js';
|
|
28
|
+
import { writeFinalStopGate } from '../stop-gate/stop-gate-writer.js';
|
|
28
29
|
const NARUTO_RESULT_SCHEMA = 'sks.naruto-command-result.v1';
|
|
29
30
|
const NARUTO_ROUTE = '$Naruto';
|
|
30
31
|
// $Naruto — Shadow Clone Swarm (影分身 / Kage Bunshin no Jutsu).
|
|
@@ -34,6 +35,11 @@ const NARUTO_ROUTE = '$Naruto';
|
|
|
34
35
|
// writes). The standard 20-agent ceiling is lifted only for this route.
|
|
35
36
|
export async function narutoCommand(commandOrArgs = 'naruto', maybeArgs = []) {
|
|
36
37
|
const args = Array.isArray(commandOrArgs) ? commandOrArgs : maybeArgs;
|
|
38
|
+
// 4.0.9: `sks naruto --glm` delegates to GLM Naruto before legacy Naruto starts.
|
|
39
|
+
if (args.includes('--glm')) {
|
|
40
|
+
const { glmNarutoCommand } = await import('../providers/glm/naruto/glm-naruto-command.js');
|
|
41
|
+
return glmNarutoCommand(args.filter((arg) => arg !== '--glm'));
|
|
42
|
+
}
|
|
37
43
|
const parsed = parseNarutoArgs(args);
|
|
38
44
|
if (parsed.action === 'help')
|
|
39
45
|
return narutoHelp(parsed);
|
|
@@ -450,6 +456,25 @@ async function narutoRun(parsed) {
|
|
|
450
456
|
stop_gate: 'naruto-gate.json',
|
|
451
457
|
prompt: parsed.prompt
|
|
452
458
|
});
|
|
459
|
+
// 4.0.9: Write canonical stop-gate artifacts for hook resolution.
|
|
460
|
+
const narutoGatePassed = result.ok === true && nativeProofOk && finalAccepted && parallelRuntimeOk;
|
|
461
|
+
await writeFinalStopGate({
|
|
462
|
+
root,
|
|
463
|
+
missionId: mission.id,
|
|
464
|
+
route: 'Naruto',
|
|
465
|
+
routeCommand: '$Naruto',
|
|
466
|
+
status: summaryOk ? 'passed' : 'blocked',
|
|
467
|
+
terminal: summaryOk,
|
|
468
|
+
terminalState: summaryOk ? 'completed' : 'blocked',
|
|
469
|
+
evidence: {
|
|
470
|
+
build_passed: summaryOk,
|
|
471
|
+
tests_passed: summaryOk,
|
|
472
|
+
route_evidence_passed: nativeProofOk && finalAccepted,
|
|
473
|
+
native_session_split_evidence: nativeProofOk ? 'native_agent_proof' : null,
|
|
474
|
+
},
|
|
475
|
+
blockers: summaryOk ? [] : [...(result.proof?.blockers || []), ...(parallelRuntimeOk ? [] : ['naruto_parallel_runtime_proof_below_gate'])],
|
|
476
|
+
nativeGateFile: 'naruto-gate.json',
|
|
477
|
+
}).catch(() => null);
|
|
453
478
|
const summary = {
|
|
454
479
|
schema: NARUTO_RESULT_SCHEMA,
|
|
455
480
|
ok: summaryOk,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { sksRoot } from '../fsx.js';
|
|
2
|
+
import { checkStopGate } from '../stop-gate/stop-gate-check.js';
|
|
3
|
+
export async function stopGateCommand(command, args) {
|
|
4
|
+
const subcommand = args[0] === 'check' ? 'check' : (args[0] || 'check');
|
|
5
|
+
const rest = subcommand === 'check' ? args.slice(1) : args;
|
|
6
|
+
const json = rest.includes('--json');
|
|
7
|
+
const route = readOption(rest, '--route');
|
|
8
|
+
const missionId = readOption(rest, '--mission');
|
|
9
|
+
const gatePath = readOption(rest, '--gate');
|
|
10
|
+
if (subcommand !== 'check') {
|
|
11
|
+
const result = {
|
|
12
|
+
schema: 'sks.stop-gate-command.v1',
|
|
13
|
+
ok: false,
|
|
14
|
+
action: 'continue',
|
|
15
|
+
error: `Unknown subcommand: ${subcommand}. Available: check`,
|
|
16
|
+
};
|
|
17
|
+
if (json)
|
|
18
|
+
console.log(JSON.stringify(result, null, 2));
|
|
19
|
+
else
|
|
20
|
+
console.error(`Unknown stop-gate subcommand: ${subcommand}. Use: sks stop-gate check --route Naruto --json`);
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
const root = await sksRoot();
|
|
24
|
+
const result = await checkStopGate({
|
|
25
|
+
root,
|
|
26
|
+
...(route ? { route } : {}),
|
|
27
|
+
...(missionId ? { missionId } : {}),
|
|
28
|
+
...(gatePath ? { explicitGatePath: gatePath } : {}),
|
|
29
|
+
});
|
|
30
|
+
if (json) {
|
|
31
|
+
console.log(JSON.stringify(result, null, 2));
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
if (result.action === 'allow_stop') {
|
|
35
|
+
console.log(`stop-gate: allow_stop — gate passed at ${result.gate_path}`);
|
|
36
|
+
}
|
|
37
|
+
else if (result.action === 'hard_blocked') {
|
|
38
|
+
console.log(`stop-gate: hard_blocked — ${result.feedback}`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
console.error(`stop-gate: continue — ${result.feedback}`);
|
|
42
|
+
}
|
|
43
|
+
if (result.diagnostics.checked_paths.length > 0) {
|
|
44
|
+
console.log('Checked paths:');
|
|
45
|
+
for (const p of result.diagnostics.checked_paths)
|
|
46
|
+
console.log(` ${p}`);
|
|
47
|
+
}
|
|
48
|
+
if (result.diagnostics.selected_gate_path) {
|
|
49
|
+
console.log(`Selected gate: ${result.diagnostics.selected_gate_path}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (result.action === 'continue')
|
|
53
|
+
process.exitCode = 1;
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
function readOption(args, name) {
|
|
57
|
+
const index = args.indexOf(name);
|
|
58
|
+
if (index >= 0 && args[index + 1] && !String(args[index + 1]).startsWith('--'))
|
|
59
|
+
return args[index + 1];
|
|
60
|
+
const prefixed = args.find((arg) => String(arg).startsWith(name + '='));
|
|
61
|
+
return prefixed ? prefixed.slice(name.length + 1) : undefined;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=stop-gate-command.js.map
|
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.9';
|
|
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() {
|
|
@@ -17,6 +17,7 @@ import { readAgentGateStatus } from '../agents/agent-gate.js';
|
|
|
17
17
|
import { MISTAKE_RECALL_ARTIFACT, mistakeRecallGateStatus } from '../mistake-recall.js';
|
|
18
18
|
import { SSOT_GUARD_ARTIFACT, validateSsotGuardArtifact } from '../safety/ssot-guard.js';
|
|
19
19
|
import { validateTeamRuntimeArtifacts } from '../team-dag.js';
|
|
20
|
+
import { checkStopGate } from '../stop-gate/stop-gate-check.js';
|
|
20
21
|
import { clarificationStopReason, context7Evidence, hasContext7DocsEvidence, hasSubagentEvidence, subagentEvidence, } from './runtime-core.js';
|
|
21
22
|
const REFLECTION_ARTIFACT = 'reflection.md';
|
|
22
23
|
const REFLECTION_GATE = 'reflection-gate.json';
|
|
@@ -233,10 +234,33 @@ export async function evaluateStop(root, state, payload, opts = {}) {
|
|
|
233
234
|
return complianceBlock(root, state, `SKS no-question run is not done. Continue autonomously, fix failing checks, update ${gate.file || 'the active gate file'}, and do not ask the user.${missing}`, { gate: gate.file || 'active-gate', missing: gate.missing });
|
|
234
235
|
}
|
|
235
236
|
if (state?.mission_id && state?.stop_gate && !['none', 'honest_mode', 'clarification-gate'].includes(state.stop_gate)) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
237
|
+
// 4.0.9: Use canonical stop-gate resolver first for NARUTO/GLM_NARUTO routes.
|
|
238
|
+
const modeUpper = String(state?.mode || '').toUpperCase();
|
|
239
|
+
if (modeUpper === 'NARUTO' || state.stop_gate === 'stop-gate.json' || state.stop_gate === 'naruto-gate.json') {
|
|
240
|
+
const stopCheck = await checkStopGate({
|
|
241
|
+
root,
|
|
242
|
+
route: state.route || state.mode,
|
|
243
|
+
missionId: state.mission_id,
|
|
244
|
+
explicitGatePath: typeof state.stop_gate_abs_path === 'string' && state.stop_gate_abs_path ? state.stop_gate_abs_path : undefined,
|
|
245
|
+
});
|
|
246
|
+
if (stopCheck.action === 'allow_stop') {
|
|
247
|
+
// Gate passed via canonical resolver; fall through to remaining checks.
|
|
248
|
+
}
|
|
249
|
+
else if (stopCheck.action === 'hard_blocked') {
|
|
250
|
+
return { continue: true, systemMessage: `SKS: ${stopCheck.feedback}` };
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
const missing = stopCheck.diagnostics.missing_fields?.length ? ` Missing gate fields: ${stopCheck.diagnostics.missing_fields.join(', ')}.` : '';
|
|
254
|
+
const checkedPaths = stopCheck.diagnostics.checked_paths?.length ? ` Checked: ${stopCheck.diagnostics.checked_paths.join(', ')}.` : '';
|
|
255
|
+
return complianceBlock(root, state, `SKS ${state.route_command || state.mode} route cannot stop yet. Pass ${stopCheck.gate_path || state.stop_gate} or record a hard blocker with evidence before finishing.${missing}${checkedPaths}`, { gate: stopCheck.gate_path || state.stop_gate, missing: stopCheck.diagnostics.missing_fields });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
const gate = await passedActiveGate(root, state);
|
|
260
|
+
if (!gate.ok) {
|
|
261
|
+
const missing = gate.missing?.length ? ` Missing gate fields: ${gate.missing.join(', ')}.` : '';
|
|
262
|
+
return complianceBlock(root, state, `SKS ${state.route_command || state.mode} route cannot stop yet. Pass ${gate.file || state.stop_gate} or record a hard blocker with evidence before finishing.${missing}`, { gate: gate.file || state.stop_gate, missing: gate.missing });
|
|
263
|
+
}
|
|
240
264
|
}
|
|
241
265
|
}
|
|
242
266
|
const proofGate = await routeProofGateStatus(root, state);
|
|
@@ -14,7 +14,7 @@ export async function runGlmBench(root, args = []) {
|
|
|
14
14
|
if (execute && !live) {
|
|
15
15
|
const blocked = {
|
|
16
16
|
schema: 'sks.glm-bench-result.v1',
|
|
17
|
-
version: '4.0.
|
|
17
|
+
version: '4.0.9',
|
|
18
18
|
generated_at: nowIso(),
|
|
19
19
|
status: 'blocked',
|
|
20
20
|
dry_run: true,
|
|
@@ -32,7 +32,7 @@ export async function runGlmBench(root, args = []) {
|
|
|
32
32
|
if (live) {
|
|
33
33
|
const blocked = {
|
|
34
34
|
schema: 'sks.glm-bench-result.v1',
|
|
35
|
-
version: '4.0.
|
|
35
|
+
version: '4.0.9',
|
|
36
36
|
generated_at: nowIso(),
|
|
37
37
|
status: 'blocked',
|
|
38
38
|
dry_run: false,
|
|
@@ -50,7 +50,7 @@ export async function runGlmBench(root, args = []) {
|
|
|
50
50
|
if (execute) {
|
|
51
51
|
const blocked = {
|
|
52
52
|
schema: 'sks.glm-bench-result.v1',
|
|
53
|
-
version: '4.0.
|
|
53
|
+
version: '4.0.9',
|
|
54
54
|
generated_at: nowIso(),
|
|
55
55
|
status: 'blocked',
|
|
56
56
|
dry_run: true,
|
|
@@ -69,7 +69,7 @@ export async function runGlmBench(root, args = []) {
|
|
|
69
69
|
const deepTotals = SYNTHETIC_CASES.map((row) => row.deep.total_ms);
|
|
70
70
|
const result = {
|
|
71
71
|
schema: 'sks.glm-bench-result.v1',
|
|
72
|
-
version: '4.0.
|
|
72
|
+
version: '4.0.9',
|
|
73
73
|
generated_at: nowIso(),
|
|
74
74
|
status: 'dry_run',
|
|
75
75
|
dry_run: true,
|
|
@@ -60,7 +60,7 @@ export async function runGlmDirectSpeedRun(input) {
|
|
|
60
60
|
return result(reason === 'glm_request_timeout' ? 'timeout' : 'failed', controller.state().run_id, input.task, termination.reason, artifactDir, [], [response.error.code], []);
|
|
61
61
|
}
|
|
62
62
|
controller.transition('model_guard');
|
|
63
|
-
const modelGuard = assertGlm52ActualModel(response.value.model
|
|
63
|
+
const modelGuard = assertGlm52ActualModel(response.value.model);
|
|
64
64
|
if (!modelGuard.ok) {
|
|
65
65
|
const termination = controller.terminate('blocked', 'glm_model_mismatch', [modelGuard.code]);
|
|
66
66
|
const artifactDir = await writeGlmRunArtifacts({ cwd: input.cwd, state: controller.state(), termination, contextOmissions: context.omitted });
|
|
@@ -11,7 +11,7 @@ export async function runGlmNarutoBench(root, args = []) {
|
|
|
11
11
|
if (!live) {
|
|
12
12
|
return {
|
|
13
13
|
schema: 'sks.glm-naruto-bench.v1',
|
|
14
|
-
version: '4.0.
|
|
14
|
+
version: '4.0.9',
|
|
15
15
|
generated_at: nowIso(),
|
|
16
16
|
status: 'dry_run',
|
|
17
17
|
model: GLM_52_OPENROUTER_MODEL,
|
|
@@ -30,7 +30,7 @@ export async function runGlmNarutoBench(root, args = []) {
|
|
|
30
30
|
// Live bench would require OpenRouter key and real API calls
|
|
31
31
|
return {
|
|
32
32
|
schema: 'sks.glm-naruto-bench.v1',
|
|
33
|
-
version: '4.0.
|
|
33
|
+
version: '4.0.9',
|
|
34
34
|
generated_at: nowIso(),
|
|
35
35
|
status: 'blocked',
|
|
36
36
|
model: GLM_52_OPENROUTER_MODEL,
|
|
@@ -49,7 +49,7 @@ export async function runGlmNarutoBench(root, args = []) {
|
|
|
49
49
|
function blocked(root, warnings) {
|
|
50
50
|
return {
|
|
51
51
|
schema: 'sks.glm-naruto-bench.v1',
|
|
52
|
-
version: '4.0.
|
|
52
|
+
version: '4.0.9',
|
|
53
53
|
generated_at: nowIso(),
|
|
54
54
|
status: 'blocked',
|
|
55
55
|
model: GLM_52_OPENROUTER_MODEL,
|
|
@@ -48,7 +48,7 @@ export async function runGlmJudge(input) {
|
|
|
48
48
|
if (!response.ok) {
|
|
49
49
|
return fallbackJudgeResult(validEnvelopes, [`judge_request_failed:${response.error.code}`]);
|
|
50
50
|
}
|
|
51
|
-
const modelGuard = assertGlm52ActualModel(response.value.model
|
|
51
|
+
const modelGuard = assertGlm52ActualModel(response.value.model);
|
|
52
52
|
if (!modelGuard.ok) {
|
|
53
53
|
return fallbackJudgeResult(validEnvelopes, [`judge_model_guard:${modelGuard.code}`]);
|
|
54
54
|
}
|
|
@@ -6,6 +6,7 @@ import { checkAndApplyGlmPatch } from '../glm-patch-apply.js';
|
|
|
6
6
|
import { decomposeTask, validateWorkGraph } from './glm-naruto-decomposer.js';
|
|
7
7
|
import { planShardCandidates, computeInitialLaneMix } from './glm-naruto-shard-planner.js';
|
|
8
8
|
import { runPatchWorkerPool } from './glm-naruto-worker-pool.js';
|
|
9
|
+
import { runVerifierWorker } from './glm-naruto-worker-runtime.js';
|
|
9
10
|
import { buildConflictGraph } from './glm-naruto-conflict-graph.js';
|
|
10
11
|
import { planMerge } from './glm-naruto-merge-planner.js';
|
|
11
12
|
import { finalizeMergePlan } from './glm-naruto-finalizer.js';
|
|
@@ -14,6 +15,7 @@ import { createBudget, checkBudget, recordRequest } from './glm-naruto-budget.js
|
|
|
14
15
|
import { createProviderHealthTracker } from '../../openrouter/openrouter-provider-health.js';
|
|
15
16
|
import { createMissionTrace, recordWorkerTrace, writeMissionArtifacts, buildMissionSummary } from './glm-naruto-trace.js';
|
|
16
17
|
import { runGlmJudge } from './glm-naruto-judge.js';
|
|
18
|
+
import { writeFinalStopGate } from '../../../stop-gate/stop-gate-writer.js';
|
|
17
19
|
import { GLM_NARUTO_LIMITS } from './glm-naruto-types.js';
|
|
18
20
|
export async function runGlmNarutoMission(input) {
|
|
19
21
|
const missionId = input.missionId || `glm-naruto-${nowIso().replace(/[:.]/g, '-')}`;
|
|
@@ -93,8 +95,38 @@ export async function runGlmNarutoMission(input) {
|
|
|
93
95
|
failedShardIds = [...failedShardIds, ...repairPool.failedShardIds];
|
|
94
96
|
}
|
|
95
97
|
}
|
|
98
|
+
// 4.0.9: Verifier wave — run parallel verifier workers over gate-passed candidates.
|
|
99
|
+
let passedEnvelopes = envelopes.filter((e) => e.status === 'gate_passed');
|
|
100
|
+
if (passedEnvelopes.length > 0 && !input.noApply) {
|
|
101
|
+
const verifyApiKey = key.key;
|
|
102
|
+
const verifyResults = await Promise.allSettled(passedEnvelopes.map((env) => runVerifierWorker({
|
|
103
|
+
apiKey: verifyApiKey,
|
|
104
|
+
missionId,
|
|
105
|
+
workerId: env.worker_id,
|
|
106
|
+
envelope: env,
|
|
107
|
+
timeoutMs: 120_000,
|
|
108
|
+
})));
|
|
109
|
+
const verifiedEnvelopes = [];
|
|
110
|
+
for (let vi = 0; vi < passedEnvelopes.length; vi++) {
|
|
111
|
+
const env = passedEnvelopes[vi];
|
|
112
|
+
const res = verifyResults[vi];
|
|
113
|
+
if (res.status === 'fulfilled' && res.value.ok) {
|
|
114
|
+
verifiedEnvelopes.push({ ...env, verification_passed: true, status: 'gate_passed' });
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
verifiedEnvelopes.push({ ...env, verification_passed: false, status: 'verification_failed' });
|
|
118
|
+
}
|
|
119
|
+
if (res.status === 'fulfilled') {
|
|
120
|
+
traceState = recordWorkerTrace(traceState, res.value.trace);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
envelopes = envelopes.map((e) => {
|
|
124
|
+
const verified = verifiedEnvelopes.find((v) => v.worker_id === e.worker_id);
|
|
125
|
+
return verified ?? e;
|
|
126
|
+
});
|
|
127
|
+
passedEnvelopes = envelopes.filter((e) => e.status === 'gate_passed');
|
|
128
|
+
}
|
|
96
129
|
// Build conflict graph and merge plan
|
|
97
|
-
const passedEnvelopes = envelopes.filter((e) => e.status === 'gate_passed');
|
|
98
130
|
const nodes = passedEnvelopes.map((env) => ({
|
|
99
131
|
patch_id: env.worker_id,
|
|
100
132
|
shard_id: env.shard_id,
|
|
@@ -181,8 +213,29 @@ export async function runGlmNarutoMission(input) {
|
|
|
181
213
|
termination: { schema: 'sks.glm-naruto-termination.v1', mission_id: missionId, terminal_state: terminalState, reason: terminationReason, wall_clock_ms: summary.wall_clock_ms },
|
|
182
214
|
...(applyResult ? { applyResult: { ...applyResult, schema: 'sks.glm-naruto-apply-result.v1' } } : {}),
|
|
183
215
|
verificationSummary: { schema: 'sks.glm-naruto-verification.v1', verified: passedEnvelopes.length, total: envelopes.length },
|
|
184
|
-
missionResult: result
|
|
216
|
+
missionResult: result,
|
|
217
|
+
envelopes
|
|
185
218
|
});
|
|
219
|
+
// 4.0.9: Write canonical stop-gate artifacts for hook resolution.
|
|
220
|
+
await writeFinalStopGate({
|
|
221
|
+
root: cwd,
|
|
222
|
+
missionId,
|
|
223
|
+
route: 'GLM_NARUTO',
|
|
224
|
+
routeCommand: '$Naruto',
|
|
225
|
+
status: result.ok ? 'passed' : (terminalState === 'blocked' ? 'blocked' : 'failed'),
|
|
226
|
+
terminal: terminalState === 'completed' || terminalState === 'blocked',
|
|
227
|
+
terminalState,
|
|
228
|
+
evidence: {
|
|
229
|
+
build_passed: result.ok,
|
|
230
|
+
tests_passed: result.ok,
|
|
231
|
+
route_evidence_passed: result.ok,
|
|
232
|
+
per_worker_artifacts: true,
|
|
233
|
+
verifier_wave_run: true,
|
|
234
|
+
model_guard_enforced: true,
|
|
235
|
+
},
|
|
236
|
+
blockers: result.blockers || [],
|
|
237
|
+
nativeGateFile: 'termination.json',
|
|
238
|
+
}).catch(() => null);
|
|
186
239
|
return { ...result, artifact_dir: artifactDir };
|
|
187
240
|
}
|
|
188
241
|
function missionResult(missionId, task, status, reason, patchCandidates, startedMs, envelopes, traces, blockers, warnings) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { nowIso, writeJsonAtomic } from '../../../fsx.js';
|
|
2
|
+
import { ensureDir, nowIso, writeJsonAtomic } from '../../../fsx.js';
|
|
3
3
|
export function createMissionTrace(missionId) {
|
|
4
4
|
return {
|
|
5
5
|
missionId,
|
|
@@ -12,6 +12,7 @@ export function recordWorkerTrace(state, trace) {
|
|
|
12
12
|
}
|
|
13
13
|
export async function writeMissionArtifacts(input) {
|
|
14
14
|
const dir = path.join(input.root, '.sneakoscope', 'glm-naruto', input.missionId);
|
|
15
|
+
await ensureDir(dir);
|
|
15
16
|
if (input.workGraph)
|
|
16
17
|
await writeJsonAtomic(path.join(dir, 'work-graph.json'), input.workGraph);
|
|
17
18
|
if (input.conflictGraph)
|
|
@@ -32,6 +33,45 @@ export async function writeMissionArtifacts(input) {
|
|
|
32
33
|
await writeJsonAtomic(path.join(dir, 'verification-summary.json'), input.verificationSummary);
|
|
33
34
|
if (input.missionResult)
|
|
34
35
|
await writeJsonAtomic(path.join(dir, 'mission-result.json'), input.missionResult);
|
|
36
|
+
// 4.0.9: Write per-worker patch envelope / request-summary / stream-trace / gate-result artifacts.
|
|
37
|
+
if (input.envelopes && input.envelopes.length > 0) {
|
|
38
|
+
const workersDir = path.join(dir, 'workers');
|
|
39
|
+
await ensureDir(workersDir);
|
|
40
|
+
for (const env of input.envelopes) {
|
|
41
|
+
const workerId = String(env.worker_id || env.shard_id || 'unknown');
|
|
42
|
+
const workerDir = path.join(workersDir, workerId);
|
|
43
|
+
await ensureDir(workerDir);
|
|
44
|
+
await writeJsonAtomic(path.join(workerDir, 'patch-envelope.json'), env);
|
|
45
|
+
await writeJsonAtomic(path.join(workerDir, 'request-summary.json'), {
|
|
46
|
+
schema: 'sks.glm-naruto-worker-request-summary.v1',
|
|
47
|
+
worker_id: workerId,
|
|
48
|
+
shard_id: env.shard_id,
|
|
49
|
+
model: env.model || null,
|
|
50
|
+
provider: 'openrouter',
|
|
51
|
+
request_body_size: env.request_body_size ?? null,
|
|
52
|
+
cached: env.cached ?? false,
|
|
53
|
+
created_at: nowIso(),
|
|
54
|
+
});
|
|
55
|
+
await writeJsonAtomic(path.join(workerDir, 'stream-trace.json'), {
|
|
56
|
+
schema: 'sks.glm-naruto-worker-stream-trace.v1',
|
|
57
|
+
worker_id: workerId,
|
|
58
|
+
ttft_ms: env.ttft_ms ?? null,
|
|
59
|
+
chunk_count: env.chunk_count ?? null,
|
|
60
|
+
real_stream: env.real_stream ?? true,
|
|
61
|
+
idle_timeout_ms: env.idle_timeout_ms ?? null,
|
|
62
|
+
created_at: nowIso(),
|
|
63
|
+
});
|
|
64
|
+
await writeJsonAtomic(path.join(workerDir, 'gate-result.json'), {
|
|
65
|
+
schema: 'sks.glm-naruto-worker-gate-result.v1',
|
|
66
|
+
worker_id: workerId,
|
|
67
|
+
shard_id: env.shard_id,
|
|
68
|
+
status: env.status,
|
|
69
|
+
gate_passed: env.status === 'gate_passed',
|
|
70
|
+
verification_passed: env.verification_passed ?? (env.status === 'gate_passed'),
|
|
71
|
+
created_at: nowIso(),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
35
75
|
return dir;
|
|
36
76
|
}
|
|
37
77
|
export function buildMissionSummary(input) {
|
|
@@ -53,7 +53,8 @@ export async function runPatchWorker(input) {
|
|
|
53
53
|
const response = await sendOpenRouterChatCompletionStream({
|
|
54
54
|
apiKey: input.apiKey,
|
|
55
55
|
request: requestWithSession,
|
|
56
|
-
timeoutMs: input.timeoutMs
|
|
56
|
+
timeoutMs: input.timeoutMs,
|
|
57
|
+
idleTimeoutMs: 60_000
|
|
57
58
|
});
|
|
58
59
|
if (!response.ok) {
|
|
59
60
|
return {
|
|
@@ -63,7 +64,7 @@ export async function runPatchWorker(input) {
|
|
|
63
64
|
error: response.error.code
|
|
64
65
|
};
|
|
65
66
|
}
|
|
66
|
-
const modelGuard = assertGlm52ActualModel(response.value.model
|
|
67
|
+
const modelGuard = assertGlm52ActualModel(response.value.model);
|
|
67
68
|
if (!modelGuard.ok) {
|
|
68
69
|
return {
|
|
69
70
|
envelope: null,
|
|
@@ -131,7 +132,8 @@ export async function runVerifierWorker(input) {
|
|
|
131
132
|
const response = await sendOpenRouterChatCompletionStream({
|
|
132
133
|
apiKey: input.apiKey,
|
|
133
134
|
request: { ...request, session_id: sessionId },
|
|
134
|
-
timeoutMs: input.timeoutMs
|
|
135
|
+
timeoutMs: input.timeoutMs,
|
|
136
|
+
idleTimeoutMs: 60_000
|
|
135
137
|
});
|
|
136
138
|
if (!response.ok) {
|
|
137
139
|
return {
|
|
@@ -28,7 +28,7 @@ export async function sendOpenRouterChatCompletionStream(input) {
|
|
|
28
28
|
}
|
|
29
29
|
// Real streaming via ReadableStream reader
|
|
30
30
|
if (response.body && typeof response.body.getReader === 'function') {
|
|
31
|
-
return { ok: true, value: await readRealStream(response.body, started) };
|
|
31
|
+
return { ok: true, value: await readRealStream(response.body, started, input.idleTimeoutMs) };
|
|
32
32
|
}
|
|
33
33
|
// Fallback: non-streaming response
|
|
34
34
|
const text = await response.text();
|
|
@@ -37,12 +37,13 @@ export async function sendOpenRouterChatCompletionStream(input) {
|
|
|
37
37
|
catch (err) {
|
|
38
38
|
if (timeout)
|
|
39
39
|
clearTimeout(timeout);
|
|
40
|
-
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');
|
|
41
42
|
return {
|
|
42
43
|
ok: false,
|
|
43
44
|
error: {
|
|
44
|
-
code: 'glm_request_timeout',
|
|
45
|
-
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.`,
|
|
46
47
|
severity: 'failed'
|
|
47
48
|
}
|
|
48
49
|
};
|
|
@@ -57,7 +58,7 @@ export async function sendOpenRouterChatCompletionStream(input) {
|
|
|
57
58
|
};
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
|
-
async function readRealStream(body, startedAtMs) {
|
|
61
|
+
async function readRealStream(body, startedAtMs, idleTimeoutMs) {
|
|
61
62
|
const reader = body.getReader();
|
|
62
63
|
const decoder = new TextDecoder();
|
|
63
64
|
const events = [];
|
|
@@ -67,14 +68,33 @@ async function readRealStream(body, startedAtMs) {
|
|
|
67
68
|
let ttft = null;
|
|
68
69
|
let buffer = '';
|
|
69
70
|
let chunkCount = 0;
|
|
71
|
+
let lastChunkMs = startedAtMs;
|
|
70
72
|
try {
|
|
71
73
|
while (true) {
|
|
72
|
-
|
|
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;
|
|
73
92
|
if (done)
|
|
74
93
|
break;
|
|
75
94
|
buffer += decoder.decode(value, { stream: true });
|
|
76
95
|
const lines = buffer.split(/\r?\n/);
|
|
77
96
|
buffer = lines.pop() || '';
|
|
97
|
+
let hadChunk = false;
|
|
78
98
|
for (const line of lines) {
|
|
79
99
|
if (!line.startsWith('data:'))
|
|
80
100
|
continue;
|
|
@@ -93,6 +113,8 @@ async function readRealStream(body, startedAtMs) {
|
|
|
93
113
|
ttft = Math.max(0, Date.now() - startedAtMs);
|
|
94
114
|
content += delta;
|
|
95
115
|
chunkCount++;
|
|
116
|
+
lastChunkMs = Date.now();
|
|
117
|
+
hadChunk = true;
|
|
96
118
|
events.push({ type: 'chunk', content_delta: delta, ...(model ? { model } : {}), raw });
|
|
97
119
|
}
|
|
98
120
|
}
|
|
@@ -100,6 +122,8 @@ async function readRealStream(body, startedAtMs) {
|
|
|
100
122
|
events.push({ type: 'error', raw: data });
|
|
101
123
|
}
|
|
102
124
|
}
|
|
125
|
+
if (hadChunk)
|
|
126
|
+
lastChunkMs = Date.now();
|
|
103
127
|
}
|
|
104
128
|
}
|
|
105
129
|
finally {
|
|
@@ -111,6 +135,7 @@ async function readRealStream(body, startedAtMs) {
|
|
|
111
135
|
...(model ? { model } : {}),
|
|
112
136
|
...(usage ? { usage } : {}),
|
|
113
137
|
ttft_ms: ttft,
|
|
138
|
+
last_chunk_ms: lastChunkMs,
|
|
114
139
|
total_ms: Math.max(0, Date.now() - startedAtMs),
|
|
115
140
|
chunk_count: chunkCount,
|
|
116
141
|
events,
|
|
@@ -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",
|