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.
@@ -76,7 +76,7 @@ dependencies = [
76
76
 
77
77
  [[package]]
78
78
  name = "sks-core"
79
- version = "4.0.8"
79
+ version = "4.0.9"
80
80
  dependencies = [
81
81
  "serde_json",
82
82
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sks-core"
3
- version = "4.0.8"
3
+ version = "4.0.9"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -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.8"),
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
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const FAST_PACKAGE_VERSION = '4.0.8';
2
+ const FAST_PACKAGE_VERSION = '4.0.9';
3
3
  const args = process.argv.slice(2);
4
4
  try {
5
5
  if (args[0] === '--agent' && args[1] === 'worker') {
@@ -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';
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
- const gate = await passedActiveGate(root, state);
237
- if (!gate.ok) {
238
- const missing = gate.missing?.length ? ` Missing gate fields: ${gate.missing.join(', ')}.` : '';
239
- 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 });
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.8',
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.8',
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.8',
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.8',
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 || GLM_52_OPENROUTER_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 });
@@ -3,7 +3,7 @@ import { nowIso, writeJsonAtomic } from '../../fsx.js';
3
3
  export function createEmptyGlmLatencyTrace(mode) {
4
4
  return {
5
5
  schema: 'sks.glm-latency-trace.v1',
6
- version: '4.0.8',
6
+ version: '4.0.9',
7
7
  mode,
8
8
  total_ms: 0,
9
9
  preflight_ms: 0,
@@ -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.8',
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.8',
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.8',
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 || GLM_52_OPENROUTER_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 || GLM_52_OPENROUTER_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
- const { done, value } = await reader.read();
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,4 @@
1
+ export { checkStopGate } from './stop-gate-check.js';
2
+ export { resolveStopGate } from './stop-gate-resolver.js';
3
+ export { writeFinalStopGate } from './stop-gate-writer.js';
4
+ //# sourceMappingURL=stop-gate-diagnostics.js.map
@@ -0,0 +1,122 @@
1
+ import path from 'node:path';
2
+ import fsp from 'node:fs/promises';
3
+ import { exists, readJson, sha256 } from '../fsx.js';
4
+ import { missionDir, missionsDir, stateFile, findLatestMission } from '../mission.js';
5
+ const GATE_FILE_CANDIDATES = ['stop-gate.json', 'naruto-gate.json'];
6
+ const GLM_NARUTO_DIR = '.sneakoscope/glm-naruto';
7
+ async function statOrNull(filePath) {
8
+ try {
9
+ const stat = await fsp.stat(filePath);
10
+ const content = await fsp.readFile(filePath, 'utf8');
11
+ return { mtime: stat.mtime.toISOString(), sha: sha256(content), size: stat.size };
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ export async function resolveStopGate(input) {
18
+ const root = path.resolve(input.root);
19
+ const checkedPaths = [];
20
+ const route = input.route ?? null;
21
+ // 1. explicit absolute path
22
+ if (input.explicitGatePath) {
23
+ const abs = path.isAbsolute(input.explicitGatePath) ? input.explicitGatePath : path.resolve(root, input.explicitGatePath);
24
+ checkedPaths.push(abs);
25
+ if (await exists(abs)) {
26
+ const raw = await readJson(abs, null);
27
+ return makeResolution(root, route, null, abs, raw, checkedPaths, null, null, 'explicit_gate_path');
28
+ }
29
+ }
30
+ // 2. current.json → mission_id + stop_gate_abs_path
31
+ const statePath = stateFile(root);
32
+ let state = {};
33
+ let stateMissionId = null;
34
+ if (await exists(statePath)) {
35
+ checkedPaths.push(statePath);
36
+ state = await readJson(statePath, {});
37
+ stateMissionId = typeof state.mission_id === 'string' ? state.mission_id : null;
38
+ }
39
+ const missionId = input.missionId ?? stateMissionId;
40
+ // 2a. stop_gate_abs_path from current state
41
+ if (typeof state.stop_gate_abs_path === 'string' && state.stop_gate_abs_path) {
42
+ const abs = path.isAbsolute(state.stop_gate_abs_path) ? state.stop_gate_abs_path : path.resolve(root, state.stop_gate_abs_path);
43
+ checkedPaths.push(abs);
44
+ if (await exists(abs)) {
45
+ const raw = await readJson(abs, null);
46
+ return makeResolution(root, route, missionId, abs, raw, checkedPaths, statePath, stateMissionId, 'state.stop_gate_abs_path');
47
+ }
48
+ }
49
+ // 3. mission dir candidates
50
+ if (missionId) {
51
+ const dir = missionDir(root, missionId);
52
+ for (const file of GATE_FILE_CANDIDATES) {
53
+ const p = path.join(dir, file);
54
+ checkedPaths.push(p);
55
+ if (await exists(p)) {
56
+ const raw = await readJson(p, null);
57
+ return makeResolution(root, route, missionId, p, raw, checkedPaths, statePath, stateMissionId, 'mission_dir');
58
+ }
59
+ }
60
+ // GLM Naruto termination / mission-result
61
+ const glmDir = path.join(root, GLM_NARUTO_DIR, missionId);
62
+ for (const file of ['termination.json', 'mission-result.json']) {
63
+ const p = path.join(glmDir, file);
64
+ checkedPaths.push(p);
65
+ if (await exists(p)) {
66
+ const raw = await readJson(p, null);
67
+ return makeResolution(root, route, missionId, p, raw, checkedPaths, statePath, stateMissionId, 'glm_naruto_dir');
68
+ }
69
+ }
70
+ }
71
+ // 4. latest mission fallback
72
+ const latest = await findLatestMission(root);
73
+ if (latest) {
74
+ const dir = missionDir(root, latest);
75
+ for (const file of GATE_FILE_CANDIDATES) {
76
+ const p = path.join(dir, file);
77
+ checkedPaths.push(p);
78
+ if (await exists(p)) {
79
+ const raw = await readJson(p, null);
80
+ return makeResolution(root, route, latest, p, raw, checkedPaths, statePath, stateMissionId, 'latest_mission');
81
+ }
82
+ }
83
+ }
84
+ return makeResolution(root, route, missionId, null, null, checkedPaths, statePath, stateMissionId, 'no_gate_found');
85
+ }
86
+ function makeResolution(root, route, missionId, gatePath, gateRaw, checkedPaths, statePath, stateMissionId, reason) {
87
+ let gateSchema = null;
88
+ if (gateRaw) {
89
+ gateSchema = typeof gateRaw.schema === 'string' ? gateRaw.schema : guessSchemaFromPath(gatePath);
90
+ }
91
+ return {
92
+ root,
93
+ route,
94
+ mission_id: missionId,
95
+ gate_path: gatePath,
96
+ gate_schema: gateSchema,
97
+ gate_raw: gateRaw,
98
+ checked_paths: checkedPaths,
99
+ current_state_path: statePath,
100
+ current_state_mission_id: stateMissionId,
101
+ reason,
102
+ };
103
+ }
104
+ function guessSchemaFromPath(gatePath) {
105
+ if (!gatePath)
106
+ return null;
107
+ const base = path.basename(gatePath);
108
+ if (base === 'stop-gate.json' || base === 'stop-gate.latest.json')
109
+ return 'sks.stop-gate.v1';
110
+ if (base === 'naruto-gate.json')
111
+ return 'sks.naruto-gate';
112
+ if (base === 'termination.json')
113
+ return 'sks.glm-naruto-termination';
114
+ if (base === 'mission-result.json')
115
+ return 'sks.glm-naruto-mission-result';
116
+ return 'sks.gate';
117
+ }
118
+ export async function gateStatInfo(gatePath) {
119
+ const info = await statOrNull(gatePath);
120
+ return { mtime: info?.mtime ?? null, sha256: info?.sha ?? null };
121
+ }
122
+ //# sourceMappingURL=stop-gate-resolver.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=stop-gate-types.js.map
@@ -0,0 +1,76 @@
1
+ import path from 'node:path';
2
+ import { ensureDir, nowIso, readJson, writeJsonAtomic, exists } from '../fsx.js';
3
+ import { missionDir } from '../mission.js';
4
+ import { setCurrent } from '../mission.js';
5
+ export async function writeFinalStopGate(input) {
6
+ const dir = missionDir(input.root, input.missionId);
7
+ await ensureDir(dir);
8
+ const passed = input.status === 'passed';
9
+ const nativeGateFile = input.nativeGateFile ?? 'naruto-gate.json';
10
+ const nativeGatePath = path.join(dir, nativeGateFile);
11
+ const canonicalGatePath = path.join(dir, 'stop-gate.json');
12
+ const latestGatePath = path.join(dir, 'stop-gate.latest.json');
13
+ const verifyPath = path.join(dir, 'stop-gate-write-verify.json');
14
+ const gate = {
15
+ schema: 'sks.stop-gate.v1',
16
+ route: input.route,
17
+ route_command: input.routeCommand,
18
+ mission_id: input.missionId,
19
+ gate_file: nativeGateFile,
20
+ gate_abs_path: canonicalGatePath,
21
+ status: input.status,
22
+ passed,
23
+ terminal: input.terminal,
24
+ terminal_state: input.terminalState,
25
+ evidence: input.evidence,
26
+ blockers: input.blockers ?? [],
27
+ missing_fields: input.missingFields ?? [],
28
+ created_at: nowIso(),
29
+ };
30
+ // 1. Write route-native gate file (backwards compat)
31
+ await writeJsonAtomic(nativeGatePath, { ...gate, schema: 'sks.naruto-gate' });
32
+ // 2. Write canonical stop-gate.json
33
+ await writeJsonAtomic(canonicalGatePath, gate);
34
+ // 3. Write stop-gate.latest.json
35
+ await writeJsonAtomic(latestGatePath, gate);
36
+ // 4. Update current state with absolute path
37
+ await setCurrent(input.root, {
38
+ mission_id: input.missionId,
39
+ route: input.route,
40
+ route_command: input.routeCommand,
41
+ mode: input.route === 'GLM_NARUTO' ? 'NARUTO' : (input.route === 'Naruto' ? 'NARUTO' : input.route),
42
+ stop_gate: 'stop-gate.json',
43
+ stop_gate_abs_path: canonicalGatePath,
44
+ stop_gate_status: input.status,
45
+ stop_gate_passed: passed,
46
+ route_evidence_passed: input.evidence.route_evidence_passed ?? passed,
47
+ terminal: input.terminal,
48
+ terminal_state: input.terminalState,
49
+ });
50
+ // 5. Re-read and verify
51
+ const verifyResult = {
52
+ schema: 'sks.stop-gate-write-verify.v1',
53
+ verified: false,
54
+ checked_paths: [nativeGatePath, canonicalGatePath, latestGatePath],
55
+ created_at: nowIso(),
56
+ };
57
+ const errors = [];
58
+ for (const [label, p] of [['native', nativeGatePath], ['canonical', canonicalGatePath], ['latest', latestGatePath]]) {
59
+ if (!(await exists(p))) {
60
+ errors.push(`${label}:file_missing:${p}`);
61
+ continue;
62
+ }
63
+ const re = await readJson(p, null);
64
+ if (!re) {
65
+ errors.push(`${label}:unreadable`);
66
+ }
67
+ else if (re.passed !== passed) {
68
+ errors.push(`${label}:passed_mismatch:expected=${passed}:got=${re.passed}`);
69
+ }
70
+ }
71
+ verifyResult.errors = errors;
72
+ verifyResult.verified = errors.length === 0;
73
+ await writeJsonAtomic(verifyPath, verifyResult);
74
+ return gate;
75
+ }
76
+ //# sourceMappingURL=stop-gate-writer.js.map
@@ -1,2 +1,2 @@
1
- export const PACKAGE_VERSION = '4.0.8';
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.8",
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",