sneakoscope 3.1.1 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/crates/sks-core/src/main.rs +1 -1
  5. package/dist/.sks-build-stamp.json +4 -4
  6. package/dist/bin/sks.js +1 -1
  7. package/dist/cli/install-helpers.js +6 -7
  8. package/dist/core/agents/agent-orchestrator.js +7 -2
  9. package/dist/core/agents/agent-proof-evidence.js +20 -0
  10. package/dist/core/agents/fast-mode-policy.js +7 -5
  11. package/dist/core/agents/intelligent-work-graph.js +93 -14
  12. package/dist/core/agents/native-cli-session-swarm.js +46 -0
  13. package/dist/core/agents/no-subagent-scaling-policy.js +10 -1
  14. package/dist/core/agents/official-subagent-helper-policy.js +62 -0
  15. package/dist/core/codex-app.js +0 -2
  16. package/dist/core/commands/fast-mode-command.js +1 -1
  17. package/dist/core/commands/loop-command.js +35 -3
  18. package/dist/core/commands/naruto-command.js +10 -6
  19. package/dist/core/commands/wiki-command.js +35 -1
  20. package/dist/core/fsx.js +1 -1
  21. package/dist/core/init.js +1 -2
  22. package/dist/core/loops/loop-artifacts.js +21 -0
  23. package/dist/core/loops/loop-concurrency-budget.js +55 -0
  24. package/dist/core/loops/loop-final-arbiter-contract.js +28 -0
  25. package/dist/core/loops/loop-finalizer.js +25 -3
  26. package/dist/core/loops/loop-fixture-policy.js +58 -0
  27. package/dist/core/loops/loop-gate-runner.js +48 -7
  28. package/dist/core/loops/loop-gpt-final-arbiter.js +26 -6
  29. package/dist/core/loops/loop-integration-merge.js +20 -15
  30. package/dist/core/loops/loop-interrupt-registry.js +118 -0
  31. package/dist/core/loops/loop-merge-strategy.js +105 -0
  32. package/dist/core/loops/loop-mutation-ledger.js +103 -0
  33. package/dist/core/loops/loop-runtime-control.js +2 -0
  34. package/dist/core/loops/loop-runtime.js +6 -3
  35. package/dist/core/loops/loop-scheduler.js +2 -2
  36. package/dist/core/loops/loop-side-effect-scanner.js +91 -0
  37. package/dist/core/loops/loop-worker-runtime.js +35 -29
  38. package/dist/core/naruto/naruto-loop-mesh.js +3 -0
  39. package/dist/core/proof/auto-finalize.js +3 -2
  40. package/dist/core/proof/route-finalizer.js +71 -6
  41. package/dist/core/release/release-gate-dag.js +56 -1
  42. package/dist/core/version.js +1 -1
  43. package/dist/scripts/lib/native-cli-session-swarm-check-lib.js +14 -2
  44. package/dist/scripts/loop-directive-check-lib.js +1 -1
  45. package/dist/scripts/loop-hardening-check-lib.js +289 -0
  46. package/dist/scripts/prepublish-release-check-or-fast.js +38 -10
  47. package/dist/scripts/release-check-stamp.js +29 -4
  48. package/dist/scripts/release-gate-existence-audit.js +1 -0
  49. package/package.json +28 -1
@@ -2,7 +2,7 @@ import path from 'node:path';
2
2
  import { printJson } from '../../cli/output.js';
3
3
  import { createMission, findLatestMission, loadMission, setCurrent } from '../mission.js';
4
4
  import { readJson, sksRoot } from '../fsx.js';
5
- import { loopLatestCheckpointPath, loopPlanPath, loopProofPath, loopRoot } from '../loops/loop-artifacts.js';
5
+ import { loopActiveWorkerHandlesPath, loopIntegrationMergePath, loopLatestCheckpointPath, loopPlanPath, loopProofPath, loopRoot, loopSideEffectReportPath } from '../loops/loop-artifacts.js';
6
6
  import { finalizeLoopGraph } from '../loops/loop-finalizer.js';
7
7
  import { readLoopGraphProof } from '../loops/loop-observability.js';
8
8
  import { planLoopsFromRequest } from '../loops/loop-planner.js';
@@ -83,7 +83,10 @@ async function loopStatus(args) {
83
83
  const states = await Promise.all((plan?.graph.nodes || []).map((node) => readJson(path.join(loopRoot(root, missionId), node.loop_id, 'loop-state.json'), null)));
84
84
  const proofs = await Promise.all((plan?.graph.nodes || []).map((node) => readJson(loopProofPath(root, missionId, node.loop_id), null)));
85
85
  const checkpoints = await Promise.all((plan?.graph.nodes || []).map((node) => readJson(loopLatestCheckpointPath(root, missionId, node.loop_id), null)));
86
- const result = { schema: 'sks.loop-status-command.v1', mission_id: missionId, plan_ok: Boolean(plan && plan.blockers.length === 0), graph: proof, states, proofs, checkpoints };
86
+ const activeWorkerHandles = await readJsonl(loopActiveWorkerHandlesPath(root, missionId));
87
+ const merge = await readJson(loopIntegrationMergePath(root, missionId), null);
88
+ const sideEffects = await readJson(loopSideEffectReportPath(root, missionId), null);
89
+ const result = { schema: 'sks.loop-status-command.v1', mission_id: missionId, plan_ok: Boolean(plan && plan.blockers.length === 0), graph: proof, states, proofs, checkpoints, active_worker_handles: activeWorkerHandles, merge, side_effects: sideEffects };
87
90
  if (flag(args, '--json'))
88
91
  return printJson(result);
89
92
  console.log(`Loop status: ${missionId}`);
@@ -95,8 +98,16 @@ async function loopStatus(args) {
95
98
  const gates = nodeProof?.gate_result ? `${nodeProof.gate_result.passed_gates.length}/${nodeProof.gate_result.selected_gates.length}` : '-';
96
99
  const worktree = nodeProof?.worktree?.id || state.acting_on?.worktree_id || '-';
97
100
  const resumable = checkpoint?.resumable ? `resumable:${checkpoint.phase}` : 'resumable:-';
98
- console.log(` ${loopId.padEnd(18)} ${String(state.status).padEnd(10)} backend ${String(backend).padEnd(24)} gates ${gates.padEnd(5)} worktree ${String(worktree).padEnd(18)} ${resumable}`);
101
+ const active = activeWorkerHandles.filter((row) => row.loop_id === loopId && row.status === 'running').length;
102
+ const mergeStatus = merge?.merge_attempts?.[loopId]?.selected_strategy || '-';
103
+ console.log(` ${loopId.padEnd(18)} ${String(state.status).padEnd(10)} backend ${String(backend).padEnd(24)} workers ${String(active).padEnd(2)} gates ${gates.padEnd(5)} worktree ${String(worktree).padEnd(18)} merge ${String(mergeStatus).padEnd(14)} ${resumable}`);
99
104
  }
105
+ if (merge)
106
+ console.log(`Merge: ${merge.ok ? 'passed' : 'blocked'} strategy=${JSON.stringify(merge.strategy_summary || {})}`);
107
+ if (sideEffects)
108
+ console.log(`Side effects: ${sideEffects.ok ? 'passed' : 'blocked'} blockers=${sideEffects.blockers?.length || 0}`);
109
+ if (proof?.gpt_final_arbiter)
110
+ console.log(`Final arbiter: ${proof.gpt_final_arbiter.verdict || 'unknown'} handled_by=${proof.gpt_final_arbiter.handled_by || 'loop-finalizer'}`);
100
111
  }
101
112
  async function loopProof(args) {
102
113
  const root = await sksRoot();
@@ -109,6 +120,12 @@ async function loopProof(args) {
109
120
  if (flag(args, '--json'))
110
121
  return printJson(proof);
111
122
  console.log(renderLoopProofSummary(proof));
123
+ if (proof.integration_merge)
124
+ console.log(`Merge: ${proof.integration_merge.ok ? 'passed' : 'blocked'} ${JSON.stringify(proof.integration_merge.strategy_summary || {})}`);
125
+ if (proof.side_effect_report)
126
+ console.log(`Side effects: ${proof.side_effect_report.ok ? 'passed' : 'blocked'} ${proof.side_effect_report.blockers?.join(', ') || 'none'}`);
127
+ if (proof.gpt_final_arbiter)
128
+ console.log(`Final arbiter: ${proof.gpt_final_arbiter.verdict || 'unknown'} contract=${proof.gpt_final_arbiter.gate_contract_path || 'missing'}`);
112
129
  }
113
130
  async function loopGraph(args) {
114
131
  const root = await sksRoot();
@@ -173,4 +190,19 @@ async function resolveLoopMission(root, arg) {
173
190
  function normalizeParallelism(value) {
174
191
  return value === 'safe' || value === 'extreme' ? value : 'balanced';
175
192
  }
193
+ async function readJsonl(file) {
194
+ const text = await import('node:fs/promises').then((fs) => fs.readFile(file, 'utf8')).catch(() => '');
195
+ return String(text).split(/\r?\n/)
196
+ .map((line) => line.trim())
197
+ .filter(Boolean)
198
+ .map((line) => {
199
+ try {
200
+ return JSON.parse(line);
201
+ }
202
+ catch {
203
+ return null;
204
+ }
205
+ })
206
+ .filter(Boolean);
207
+ }
176
208
  //# sourceMappingURL=loop-command.js.map
@@ -5,6 +5,7 @@ import { runNativeAgentOrchestrator } from '../agents/agent-orchestrator.js';
5
5
  import { classifyOllamaWorkerSlice } from '../agents/agent-runner-ollama.js';
6
6
  import { buildNarutoCloneRoster, systemSafeNarutoConcurrency } from '../agents/agent-roster.js';
7
7
  import { DEFAULT_NARUTO_CLONES, MAX_NARUTO_AGENT_COUNT } from '../agents/agent-schema.js';
8
+ import { normalizeServiceTier } from '../agents/fast-mode-policy.js';
8
9
  import { resolveOllamaWorkerConfig } from '../agents/ollama-worker-config.js';
9
10
  import { attachZellijSessionInteractive, launchZellijLayout } from '../zellij/zellij-launcher.js';
10
11
  import { maybePromptZellijUpdateForLaunch } from '../zellij/zellij-update.js';
@@ -380,10 +381,9 @@ async function narutoRun(parsed) {
380
381
  workerPlacement: parsed.json || parsed.noOpenZellij ? 'process' : 'zellij-pane',
381
382
  zellijPaneWorker: true,
382
383
  zellijVisiblePaneCap: zellijVisiblePanes,
383
- // Shadow clones ALWAYS run in fast service tier — never honor --no-fast/standard.
384
- fastMode: true,
385
- serviceTier: 'fast',
386
- noFast: false,
384
+ ...(parsed.fastMode === undefined ? {} : { fastMode: parsed.fastMode }),
385
+ ...(parsed.serviceTier === undefined ? {} : { serviceTier: parsed.serviceTier }),
386
+ noFast: parsed.noFast,
387
387
  writeMode: writeCapable ? parsed.writeMode || 'parallel' : 'off',
388
388
  applyPatches: parsed.applyPatches,
389
389
  dryRunPatches: parsed.dryRunPatches,
@@ -797,6 +797,10 @@ function parseNarutoArgs(args = []) {
797
797
  const applyPatches = hasFlag(args, '--apply-patches');
798
798
  const dryRunPatches = hasFlag(args, '--dry-run-patches') || hasFlag(args, '--dry-run-patch');
799
799
  const maxWriteAgents = Math.max(0, Math.floor(Number(readOption(args, '--max-write-agents', '0')) || 0));
800
+ const explicitServiceTier = String(readOption(args, '--service-tier', '') || '');
801
+ const serviceTier = normalizeServiceTier(explicitServiceTier, null) || undefined;
802
+ const fastMode = hasFlag(args, '--no-fast') || serviceTier === 'standard' ? false : hasFlag(args, '--fast') ? true : undefined;
803
+ const noFast = hasFlag(args, '--no-fast');
800
804
  const positionalMission = action === 'dashboard' || action === 'workers' || action === 'status' || action === 'proof'
801
805
  ? positionalArgs(rest, new Set()).find((arg) => /^latest$|^M-/.test(arg))
802
806
  : null;
@@ -808,9 +812,9 @@ function parseNarutoArgs(args = []) {
808
812
  const smoke = hasFlag(args, '--smoke');
809
813
  const parallelism = normalizeParallelism(readOption(args, '--parallelism', 'extreme'));
810
814
  const messages = normalizeMessages(readOption(args, '--messages', '8'));
811
- const valueFlags = new Set(['--clones', '--agents', '--work-items', '--concurrency', '--target-active-slots', '--backend', '--write-mode', '--max-write-agents', '--mission', '--mission-id', '--ollama-model', '--local-model-model', '--ollama-base-url', '--local-model-base-url', '--parallelism', '--messages']);
815
+ const valueFlags = new Set(['--clones', '--agents', '--work-items', '--concurrency', '--target-active-slots', '--backend', '--write-mode', '--max-write-agents', '--service-tier', '--mission', '--mission-id', '--ollama-model', '--local-model-model', '--ollama-base-url', '--local-model-base-url', '--parallelism', '--messages']);
812
816
  const prompt = positionalArgs(rest, valueFlags).join(' ').trim() || 'Naruto shadow clone swarm run';
813
- return { action, prompt, clones, workItems, concurrency, backend, backendExplicit, mock, real, readonly, ollamaEnabled: useOllama && !noOllama, noOllama, ollamaModel, ollamaBaseUrl, writeMode, applyPatches, dryRunPatches, maxWriteAgents, json, missionId, noOpenZellij, attach, smoke, parallelism, messages };
817
+ return { action, prompt, clones, workItems, concurrency, backend, backendExplicit, mock, real, readonly, ollamaEnabled: useOllama && !noOllama, noOllama, ollamaModel, ollamaBaseUrl, writeMode, applyPatches, dryRunPatches, maxWriteAgents, fastMode, serviceTier, noFast, json, missionId, noOpenZellij, attach, smoke, parallelism, messages };
814
818
  }
815
819
  function normalizeParallelism(value) {
816
820
  const text = String(value || 'extreme').toLowerCase();
@@ -120,7 +120,17 @@ export async function wikiCommand(sub, args = []) {
120
120
  const { id, dir } = await createMission(root, { mode: 'wiki', prompt: 'sks wiki refresh' });
121
121
  const gate = { schema_version: 1, passed: validation.result.ok, ok: validation.result.ok, context_pack: '.sneakoscope/wiki/context-pack.json', anchors: wikiAnchorCount(pack.wiki), voxels: wikiVoxelRowCount(pack.wiki) };
122
122
  await writeJsonAtomic(path.join(dir, 'wiki-gate.json'), gate);
123
- await maybeFinalizeRoute(root, { missionId: id, route: '$Wiki', gateFile: 'wiki-gate.json', gate, artifacts: ['wiki-gate.json', 'completion-proof.json'], statusHint: validation.result.ok ? 'verified_partial' : 'blocked', blockers: validation.result.ok ? [] : validation.result.issues, command: { cmd: 'sks wiki refresh', status: exitCode } });
123
+ await maybeFinalizeRoute(root, {
124
+ missionId: id,
125
+ route: '$Wiki',
126
+ gateFile: 'wiki-gate.json',
127
+ gate,
128
+ artifacts: ['wiki-gate.json', 'completion-proof.json'],
129
+ statusHint: validation.result.ok ? 'verified_partial' : 'blocked',
130
+ blockers: validation.result.ok ? [] : validation.result.issues,
131
+ command: { cmd: 'sks wiki refresh', status: exitCode },
132
+ failureAnalysis: wikiRefreshFailureAnalysis(validation.result, gate)
133
+ });
124
134
  }
125
135
  if (flag(args, '--json')) {
126
136
  process.exitCode = exitCode;
@@ -197,6 +207,30 @@ export async function wikiCommand(sub, args = []) {
197
207
  console.error('Usage: sks wiki coords|pack|refresh|publish|rebuild-index|rebuild-summary|validate|validate-shared|wrongness|image-ingest|anchor-add|relation-add|image-validate|image-summary');
198
208
  process.exitCode = 1;
199
209
  }
210
+ function wikiRefreshFailureAnalysis(validationResult, gate = {}) {
211
+ if (validationResult?.ok) {
212
+ return {
213
+ status: 'complete',
214
+ root_cause: 'Wiki refresh intentionally finalizes as verified_partial because a context-pack refresh can verify TriWiki schema and anchors, but active wrongness memory may still prevent full trust verification.',
215
+ corrective_action: 'The Wiki route records the fresh context-pack validation, wiki gate counts, and completion-proof evidence while keeping the route below full verified status until wrongness memory is separately resolved.',
216
+ evidence: [
217
+ '.sneakoscope/wiki/context-pack.json',
218
+ 'wiki-gate.json',
219
+ { anchors: gate.anchors || 0, voxels: gate.voxels || 0 }
220
+ ]
221
+ };
222
+ }
223
+ return {
224
+ status: 'complete',
225
+ root_cause: `Wiki context-pack validation failed during refresh: ${(validationResult?.issues || []).join(', ') || 'unknown validation issue'}.`,
226
+ corrective_action: 'The Wiki route finalized as blocked and preserved validation issues in the completion proof so the context pack can be corrected before any completion claim.',
227
+ evidence: [
228
+ '.sneakoscope/wiki/context-pack.json',
229
+ 'wiki-gate.json',
230
+ ...(validationResult?.issues || [])
231
+ ]
232
+ };
233
+ }
200
234
  async function wikiImageIngest(args = []) {
201
235
  const root = await sksRoot();
202
236
  const imagePath = args.find((arg, i) => i >= 0 && !String(arg).startsWith('--'));
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 = '3.1.1';
8
+ export const PACKAGE_VERSION = '3.1.2';
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() {
package/dist/core/init.js CHANGED
@@ -591,7 +591,6 @@ export async function initProject(root, opts = {}) {
591
591
  // upgrade never clobbers a user's chosen model/service_tier. model_reasoning_effort
592
592
  // is intentionally left untouched at the top level.
593
593
  next = upsertTopLevelTomlStringIfAbsent(next, 'model', 'gpt-5.5');
594
- next = upsertTopLevelTomlStringIfAbsent(next, 'service_tier', 'fast');
595
594
  next = upsertTopLevelTomlBooleanIfAbsent(next, 'suppress_unstable_features_warning', true);
596
595
  // Codex App feature flags: SET-IF-ABSENT only (see note above).
597
596
  for (const flag of MANAGED_CODEX_FEATURE_FLAGS) {
@@ -599,7 +598,7 @@ export async function initProject(root, opts = {}) {
599
598
  }
600
599
  next = upsertTomlTableKeyIfAbsent(next, 'user.fast_mode', 'visible = true');
601
600
  next = upsertTomlTableKeyIfAbsent(next, 'user.fast_mode', 'enabled = true');
602
- next = upsertTomlTableKeyIfAbsent(next, 'user.fast_mode', 'default_profile = "sks-fast-high"');
601
+ next = removeTomlTableKey(next, 'user.fast_mode', 'default_profile');
603
602
  next = upsertTomlTableKeyIfAbsent(next, 'agents', 'max_threads = 6');
604
603
  next = upsertTomlTableKeyIfAbsent(next, 'agents', 'max_depth = 1');
605
604
  for (const block of managedCodexConfigBlocks()) {
@@ -36,6 +36,27 @@ export function loopIntegrationMergePath(root, missionId) {
36
36
  export function loopGptFinalArbiterPath(root, missionId) {
37
37
  return path.join(loopRoot(root, missionId), 'loop-gpt-final-arbiter.json');
38
38
  }
39
+ export function loopFinalArbiterGateContractPath(root, missionId) {
40
+ return path.join(loopRoot(root, missionId), 'gpt-final-arbiter-gate-contract.json');
41
+ }
42
+ export function loopFixturePolicyPath(root, missionId) {
43
+ return path.join(loopRoot(root, missionId), 'fixture-policy.json');
44
+ }
45
+ export function loopMutationLedgerPath(root, missionId) {
46
+ return path.join(loopRoot(root, missionId), 'mutation-ledger.jsonl');
47
+ }
48
+ export function loopSideEffectReportPath(root, missionId) {
49
+ return path.join(loopRoot(root, missionId), 'loop-side-effect-report.json');
50
+ }
51
+ export function loopActiveWorkerHandlesPath(root, missionId) {
52
+ return path.join(loopRoot(root, missionId), 'active-worker-handles.jsonl');
53
+ }
54
+ export function loopInterruptResultPath(root, missionId) {
55
+ return path.join(loopRoot(root, missionId), 'interrupt-result.json');
56
+ }
57
+ export function loopConcurrencyBudgetPath(root, missionId) {
58
+ return path.join(loopRoot(root, missionId), 'concurrency-budget.json');
59
+ }
39
60
  export function loopKillRequestPath(root, missionId) {
40
61
  return path.join(loopRoot(root, missionId), 'kill-request.json');
41
62
  }
@@ -0,0 +1,55 @@
1
+ import os from 'node:os';
2
+ import { writeJsonAtomic } from '../fsx.js';
3
+ import { loopConcurrencyBudgetPath } from './loop-artifacts.js';
4
+ export function computeLoopConcurrencyBudget(input) {
5
+ const env = input.env || process.env;
6
+ const cores = Math.max(1, os.cpus().length || 1);
7
+ const requestedLoops = input.parallelism === 'safe' ? 2 : input.parallelism === 'extreme' ? Math.min(16, cores) : Math.min(8, cores);
8
+ const envLoops = positiveInt(env.SKS_LOOP_MAX_ACTIVE_LOOPS);
9
+ const envWorkers = positiveInt(env.SKS_LOOP_MAX_ACTIVE_WORKERS);
10
+ const envModelCalls = positiveInt(env.SKS_LOOP_MAX_MODEL_CALLS);
11
+ const maxActiveLoops = envLoops || requestedLoops;
12
+ const maxActiveWorkers = envWorkers || (input.parallelism === 'safe' ? Math.min(8, cores) : input.parallelism === 'extreme' ? Math.min(32, Math.max(4, cores * 2)) : Math.min(16, Math.max(4, cores)));
13
+ const maxModelCalls = envModelCalls || Math.max(1, Math.min(maxActiveWorkers, input.plan.global_budget.max_model_calls || maxActiveWorkers));
14
+ let remainingWorkers = maxActiveWorkers;
15
+ let remainingModelCalls = maxModelCalls;
16
+ const perLoop = input.plan.graph.nodes.map((node) => {
17
+ const requested = Math.max(1, node.maker.worker_count + node.checker.worker_count);
18
+ const fairShare = Math.max(1, Math.floor(maxActiveWorkers / Math.max(1, input.plan.graph.nodes.length)));
19
+ const allocation = Math.min(requested, Math.max(1, fairShare), Math.max(1, remainingWorkers));
20
+ const maker = Math.min(node.maker.worker_count, Math.max(1, Math.ceil(allocation / 2)));
21
+ const checker = Math.min(node.checker.worker_count, Math.max(0, allocation - maker));
22
+ const modelCalls = Math.min(Math.max(1, node.budget.max_model_calls), Math.max(1, remainingModelCalls));
23
+ remainingWorkers = Math.max(0, remainingWorkers - maker - checker);
24
+ remainingModelCalls = Math.max(0, remainingModelCalls - modelCalls);
25
+ return {
26
+ loop_id: node.loop_id,
27
+ maker_workers: maker,
28
+ checker_workers: checker,
29
+ model_call_budget: modelCalls
30
+ };
31
+ });
32
+ return {
33
+ schema: 'sks.loop-concurrency-budget.v1',
34
+ mission_id: input.plan.mission_id,
35
+ max_active_loops: maxActiveLoops,
36
+ max_active_workers: maxActiveWorkers,
37
+ max_model_calls: maxModelCalls,
38
+ per_loop_worker_budget: perLoop,
39
+ headroom_workers: Math.max(0, maxActiveWorkers - perLoop.reduce((sum, row) => sum + row.maker_workers + row.checker_workers, 0)),
40
+ blockers: []
41
+ };
42
+ }
43
+ export async function writeLoopConcurrencyBudget(root, budget) {
44
+ await writeJsonAtomic(loopConcurrencyBudgetPath(root, budget.mission_id), { ...budget, generated_at: new Date().toISOString() });
45
+ }
46
+ export function loopWorkerBudgetFor(budget, loopId, phase, requested) {
47
+ const row = budget.per_loop_worker_budget.find((item) => item.loop_id === loopId);
48
+ const allowed = phase === 'maker' ? row?.maker_workers : row?.checker_workers;
49
+ return Math.max(1, Math.min(Math.max(1, requested), Math.max(1, allowed || requested)));
50
+ }
51
+ function positiveInt(value) {
52
+ const number = Number(value);
53
+ return Number.isFinite(number) && number >= 1 ? Math.floor(number) : null;
54
+ }
55
+ //# sourceMappingURL=loop-concurrency-budget.js.map
@@ -0,0 +1,28 @@
1
+ import { writeJsonAtomic } from '../fsx.js';
2
+ import { loopFinalArbiterGateContractPath, loopGptFinalArbiterPath } from './loop-artifacts.js';
3
+ export async function writeLoopFinalArbiterGateContract(root, missionId) {
4
+ const contract = buildLoopFinalArbiterGateContract(root, missionId);
5
+ await writeJsonAtomic(loopFinalArbiterGateContractPath(root, missionId), { ...contract, generated_at: new Date().toISOString() });
6
+ return contract;
7
+ }
8
+ export function buildLoopFinalArbiterGateContract(root, missionId) {
9
+ return {
10
+ schema: 'sks.loop-final-arbiter-gate-contract.v1',
11
+ mission_id: missionId,
12
+ gate_id: 'gpt:final-arbiter',
13
+ handled_by: 'loop-finalizer',
14
+ gate_runner_status: 'deferred',
15
+ finalizer_artifact_path: relativeMissionArtifact(root, loopGptFinalArbiterPath(root, missionId)),
16
+ required_when: ['source_mutation_exists', 'selected_gates_include_gpt_final_arbiter'],
17
+ production_fixture_allowed: false
18
+ };
19
+ }
20
+ export function loopFinalArbiterGateContractRelativePath(missionId) {
21
+ return `.sneakoscope/missions/${missionId}/loops/gpt-final-arbiter-gate-contract.json`;
22
+ }
23
+ function relativeMissionArtifact(root, absolute) {
24
+ const normalizedRoot = root.replace(/\\/g, '/').replace(/\/+$/, '');
25
+ const normalized = absolute.replace(/\\/g, '/');
26
+ return normalized.startsWith(`${normalizedRoot}/`) ? normalized.slice(normalizedRoot.length + 1) : normalized;
27
+ }
28
+ //# sourceMappingURL=loop-final-arbiter-contract.js.map
@@ -1,8 +1,10 @@
1
1
  import { readJson, writeJsonAtomic } from '../fsx.js';
2
2
  import { loopGraphProofPath, loopProofPath } from './loop-artifacts.js';
3
+ import { writeLoopFinalArbiterGateContract } from './loop-final-arbiter-contract.js';
3
4
  import { runLoopGptFinalArbiter } from './loop-gpt-final-arbiter.js';
4
5
  import { mergeLoopWorktrees } from './loop-integration-merge.js';
5
6
  import { graphProofFromLoopProofs } from './loop-scheduler.js';
7
+ import { buildLoopSideEffectReport } from './loop-side-effect-scanner.js';
6
8
  export async function finalizeLoopGraph(input) {
7
9
  const proofs = input.proofs || await Promise.all(input.plan.graph.nodes.map((node) => readJson(loopProofPath(input.root, input.plan.mission_id, node.loop_id), null)));
8
10
  const realProofs = proofs.filter((proof) => Boolean(proof));
@@ -20,14 +22,26 @@ export async function finalizeLoopGraph(input) {
20
22
  });
21
23
  const anyHandoff = realProofs.some((proof) => proof.handoff.required);
22
24
  const anySourceMutation = realProofs.some((proof) => proof.changed_files.some((file) => !file.startsWith('.sneakoscope/')));
25
+ const selectedGptFinal = graph.gates.selected.includes('gpt:final-arbiter');
26
+ const contract = anySourceMutation || selectedGptFinal
27
+ ? await writeLoopFinalArbiterGateContract(input.root, input.plan.mission_id)
28
+ : null;
29
+ const sideEffectReport = await buildLoopSideEffectReport({
30
+ root: input.root,
31
+ missionId: input.plan.mission_id,
32
+ proofs: realProofs,
33
+ integrationMerge
34
+ });
23
35
  const arbiter = anySourceMutation
24
- ? await runLoopGptFinalArbiter({ root: input.root, plan: input.plan, proofs: realProofs, integrationMerge })
36
+ ? await runLoopGptFinalArbiter({ root: input.root, plan: input.plan, proofs: realProofs, integrationMerge, sideEffectReport })
25
37
  : null;
26
38
  const blockers = [
27
39
  ...graph.blockers,
28
40
  ...(anyHandoff ? ['loop_handoff_required'] : []),
29
41
  ...(integrationMerge.ok ? [] : integrationMerge.blockers),
42
+ ...(sideEffectReport.ok ? [] : sideEffectReport.blockers),
30
43
  ...(anySourceMutation && !arbiter ? ['gpt_final_arbiter_missing'] : []),
44
+ ...(selectedGptFinal && anySourceMutation && (!contract || !arbiter) ? ['gpt_final_arbiter_contract_unfulfilled'] : []),
31
45
  ...(arbiter && !arbiter.ok ? ['gpt_final_arbiter_not_approved', ...arbiter.blockers] : [])
32
46
  ];
33
47
  const finalGraph = {
@@ -38,13 +52,21 @@ export async function finalizeLoopGraph(input) {
38
52
  ok: integrationMerge.ok,
39
53
  artifact_path: `.sneakoscope/missions/${input.plan.mission_id}/loops/integration-merge.json`,
40
54
  applied_loops: integrationMerge.applied_loops,
41
- conflict_loops: integrationMerge.conflict_loops
55
+ conflict_loops: integrationMerge.conflict_loops,
56
+ ...(integrationMerge.strategy_summary ? { strategy_summary: integrationMerge.strategy_summary } : {})
57
+ },
58
+ side_effect_report: {
59
+ ok: sideEffectReport.ok,
60
+ artifact_path: `.sneakoscope/missions/${input.plan.mission_id}/loops/loop-side-effect-report.json`,
61
+ blockers: sideEffectReport.blockers
42
62
  },
43
63
  ...(arbiter ? {
44
64
  gpt_final_arbiter: {
45
65
  ok: arbiter.ok,
46
66
  artifact_path: arbiter.artifact_path,
47
- verdict: arbiter.verdict
67
+ verdict: arbiter.verdict,
68
+ gate_contract_path: `.sneakoscope/missions/${input.plan.mission_id}/loops/gpt-final-arbiter-gate-contract.json`,
69
+ handled_by: 'loop-finalizer'
48
70
  }
49
71
  } : {})
50
72
  };
@@ -0,0 +1,58 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import { writeJsonAtomic } from '../fsx.js';
4
+ import { loopFixturePolicyPath } from './loop-artifacts.js';
5
+ export function decideLoopFixturePolicy(input) {
6
+ const env = input.env || process.env;
7
+ const argv = input.argv || process.argv;
8
+ const scriptPath = argv.find((arg) => /(?:^|[\\/])(?:dist|src)[\\/]scripts[\\/]/.test(arg)) || argv[1] || '';
9
+ const scriptName = path.basename(scriptPath);
10
+ const scriptIsCheck = /(?:^|[\\/])(?:dist|src)[\\/]scripts[\\/]/.test(scriptPath)
11
+ && /(check|blackbox)/.test(scriptName);
12
+ const missionIsCheck = /^M-check-/.test(input.missionId);
13
+ const tempRoot = isUnderTempRoot(input.root);
14
+ const explicitTestEnv = env.NODE_ENV === 'test'
15
+ || env.SKS_TEST_RUNTIME_FIXTURE_ALLOWED === '1'
16
+ || env.VITEST_WORKER_ID !== undefined
17
+ || env.JEST_WORKER_ID !== undefined
18
+ || env.NODE_V8_COVERAGE !== undefined;
19
+ const commandText = argv.join(' ');
20
+ const productionCommand = /\bsks\s+(?:loop\s+run|goal|naruto)\b/.test(commandText);
21
+ const requestedByEnv = env.SKS_LOOP_GATE_FIXTURE === '1'
22
+ || env.SKS_LOOP_RUNTIME_FIXTURE === '1'
23
+ || env.SKS_LOOP_GPT_FINAL_FIXTURE === '1';
24
+ const allowReasons = [
25
+ scriptIsCheck ? 'release_check_script' : null,
26
+ missionIsCheck ? 'check_mission_id' : null,
27
+ tempRoot ? 'temp_project_root' : null,
28
+ explicitTestEnv ? 'test_environment' : null
29
+ ].filter((value) => Boolean(value));
30
+ const allowed = input.requested && allowReasons.length > 0 && !productionCommand;
31
+ const productionLike = !scriptIsCheck && !missionIsCheck && !tempRoot && !explicitTestEnv;
32
+ const blockers = input.requested && !allowed
33
+ ? [
34
+ 'loop_fixture_forbidden_in_production',
35
+ `loop_${input.mode.replace(/-/g, '_')}_fixture_forbidden_in_production`,
36
+ ...(productionCommand ? ['loop_fixture_forbidden_for_production_command'] : []),
37
+ ...(requestedByEnv && productionLike ? ['loop_fixture_env_without_allowed_reason'] : [])
38
+ ]
39
+ : [];
40
+ return {
41
+ schema: 'sks.loop-fixture-policy-decision.v1',
42
+ allowed,
43
+ mode: input.mode,
44
+ requested: input.requested,
45
+ production_like: productionLike || productionCommand,
46
+ reason: allowed ? allowReasons.join('+') : input.requested ? 'fixture_requires_check_or_test_context' : 'fixture_not_requested',
47
+ blockers: [...new Set(blockers)]
48
+ };
49
+ }
50
+ export async function writeLoopFixturePolicyDecision(root, missionId, decision) {
51
+ await writeJsonAtomic(loopFixturePolicyPath(root, missionId), { ...decision, generated_at: new Date().toISOString() });
52
+ }
53
+ export function isUnderTempRoot(root) {
54
+ const normalizedRoot = path.resolve(root);
55
+ const tempRoot = path.resolve(os.tmpdir());
56
+ return normalizedRoot === tempRoot || normalizedRoot.startsWith(`${tempRoot}${path.sep}`);
57
+ }
58
+ //# sourceMappingURL=loop-fixture-policy.js.map
@@ -4,6 +4,8 @@ import { readJson, runProcess, writeJsonAtomic } from '../fsx.js';
4
4
  import { allGateIds } from './loop-schema.js';
5
5
  import { loopBudgetPath, loopGatePath, loopStatePath } from './loop-artifacts.js';
6
6
  import { resolveLoopGate } from './loop-gate-registry.js';
7
+ import { decideLoopFixturePolicy, writeLoopFixturePolicyDecision } from './loop-fixture-policy.js';
8
+ import { loopFinalArbiterGateContractRelativePath, writeLoopFinalArbiterGateContract } from './loop-final-arbiter-contract.js';
7
9
  export async function runLoopGates(input) {
8
10
  const selected = allGateIds(input.gates);
9
11
  const failed = [];
@@ -35,10 +37,20 @@ async function runOneGate(input, gateId) {
35
37
  const fullReleaseCheckInsideLoop = gateId === 'release:check' && input.node.route !== '$Integration';
36
38
  const unknown = !definition;
37
39
  const packageJson = unknown ? await readJson(path.join(input.root, 'package.json'), null) : null;
38
- const skipUnknownFixtureGate = unknown && !packageJson;
40
+ const fixtureMode = process.env.SKS_LOOP_GATE_FIXTURE === '1';
41
+ const fixtureDecision = decideLoopFixturePolicy({
42
+ root: input.root,
43
+ missionId: input.missionId,
44
+ mode: 'gate',
45
+ requested: fixtureMode || (unknown && !packageJson)
46
+ });
47
+ if (fixtureDecision.requested)
48
+ await writeLoopFixturePolicyDecision(input.root, input.missionId, fixtureDecision).catch(() => undefined);
49
+ const skipUnknownFixtureGate = unknown && !packageJson && fixtureDecision.allowed;
39
50
  const blockers = [
40
51
  ...(unknown && !skipUnknownFixtureGate ? [`unknown_loop_gate:${gateId}`] : []),
41
- ...(fullReleaseCheckInsideLoop ? ['full_release_check_inside_non_integration_loop'] : [])
52
+ ...(fullReleaseCheckInsideLoop ? ['full_release_check_inside_non_integration_loop'] : []),
53
+ ...(fixtureMode && !fixtureDecision.allowed ? [...fixtureDecision.blockers, 'loop_gate_fixture_forbidden_in_production'] : [])
42
54
  ];
43
55
  let ok = blockers.length === 0;
44
56
  let skipped = skipUnknownFixtureGate;
@@ -46,9 +58,14 @@ async function runOneGate(input, gateId) {
46
58
  let stdoutTail = '';
47
59
  let stderrTail = '';
48
60
  let timedOut = false;
49
- const fixtureMode = process.env.SKS_LOOP_GATE_FIXTURE === '1';
61
+ let handledBy;
62
+ let deferredContractPath;
63
+ let deferredReason;
50
64
  if (definition && ok) {
51
- if (fixtureMode && definition.source !== 'builtin-pseudo') {
65
+ if (fixtureMode && !fixtureDecision.allowed) {
66
+ ok = false;
67
+ }
68
+ else if (fixtureMode && definition.source !== 'builtin-pseudo') {
52
69
  ok = true;
53
70
  }
54
71
  else if (definition.source === 'builtin-pseudo') {
@@ -56,6 +73,9 @@ async function runOneGate(input, gateId) {
56
73
  ok = builtin.ok;
57
74
  skipped = builtin.skipped;
58
75
  blockers.push(...builtin.blockers);
76
+ handledBy = builtin.handled_by;
77
+ deferredContractPath = builtin.deferred_contract_path;
78
+ deferredReason = builtin.deferred_reason;
59
79
  }
60
80
  else {
61
81
  const command = definition.command;
@@ -91,7 +111,12 @@ async function runOneGate(input, gateId) {
91
111
  stderr_tail: stderrTail,
92
112
  cached_allowed: definition?.cache_allowed ?? false,
93
113
  fixture_mode: fixtureMode,
114
+ fixture_policy: fixtureDecision,
115
+ fixture_allowed_reason: fixtureDecision.allowed ? fixtureDecision.reason : null,
94
116
  skipped,
117
+ handled_by: handledBy,
118
+ deferred_contract_path: deferredContractPath,
119
+ deferred_reason: deferredReason,
95
120
  deferred_unknown_fixture_gate: skipUnknownFixtureGate,
96
121
  timed_out: timedOut,
97
122
  full_release_check_inside_loop: fullReleaseCheckInsideLoop,
@@ -99,11 +124,27 @@ async function runOneGate(input, gateId) {
99
124
  blockers
100
125
  };
101
126
  await writeJsonAtomic(loopGatePath(input.root, input.missionId, input.node.loop_id, gateId), artifact);
102
- return { ok, skipped, blockers };
127
+ return {
128
+ ok,
129
+ skipped,
130
+ blockers,
131
+ ...(handledBy ? { handled_by: handledBy } : {}),
132
+ ...(deferredContractPath ? { deferred_contract_path: deferredContractPath } : {}),
133
+ ...(deferredReason ? { deferred_reason: deferredReason } : {})
134
+ };
103
135
  }
104
136
  async function runBuiltinGate(root, missionId, loopId, definition, checkerArtifacts) {
105
- if (definition.id === 'gpt:final-arbiter')
106
- return { ok: true, skipped: true, blockers: [] };
137
+ if (definition.id === 'gpt:final-arbiter') {
138
+ await writeLoopFinalArbiterGateContract(root, missionId);
139
+ return {
140
+ ok: true,
141
+ skipped: true,
142
+ blockers: [],
143
+ handled_by: 'loop-finalizer',
144
+ deferred_contract_path: loopFinalArbiterGateContractRelativePath(missionId),
145
+ deferred_reason: 'gpt_final_arbiter_runs_after_integration_merge'
146
+ };
147
+ }
107
148
  if (definition.id === 'human:handoff-required')
108
149
  return { ok: false, skipped: false, blockers: ['human_handoff_required'] };
109
150
  if (definition.id === 'loop:state-valid') {
@@ -1,6 +1,7 @@
1
1
  import { runGptFinalArbiter } from '../codex-control/gpt-final-arbiter.js';
2
2
  import { nowIso, writeJsonAtomic } from '../fsx.js';
3
3
  import { loopGptFinalArbiterPath } from './loop-artifacts.js';
4
+ import { decideLoopFixturePolicy, writeLoopFixturePolicyDecision } from './loop-fixture-policy.js';
4
5
  export async function runLoopGptFinalArbiter(input) {
5
6
  const artifactPath = loopGptFinalArbiterPath(input.root, input.plan.mission_id);
6
7
  const changedFiles = [...new Set([
@@ -8,10 +9,29 @@ export async function runLoopGptFinalArbiter(input) {
8
9
  ...input.proofs.flatMap((proof) => proof.changed_files)
9
10
  ])];
10
11
  const reviewedLoopIds = input.proofs.map((proof) => proof.loop_id);
11
- if (process.env.SKS_LOOP_GPT_FINAL_FIXTURE === '1' || input.forceVerdict) {
12
+ const fixtureRequested = process.env.SKS_LOOP_GPT_FINAL_FIXTURE === '1' || Boolean(input.forceVerdict);
13
+ const fixtureDecision = decideLoopFixturePolicy({
14
+ root: input.root,
15
+ missionId: input.plan.mission_id,
16
+ mode: 'gpt-final',
17
+ requested: fixtureRequested
18
+ });
19
+ if (fixtureRequested)
20
+ await writeLoopFixturePolicyDecision(input.root, input.plan.mission_id, fixtureDecision).catch(() => undefined);
21
+ if (fixtureRequested && !fixtureDecision.allowed) {
22
+ const result = buildResult(input.plan.mission_id, reviewedLoopIds, changedFiles, 'reject', [], artifactPath, [...fixtureDecision.blockers, 'loop_gpt_final_fixture_forbidden_in_production']);
23
+ await writeJsonAtomic(artifactPath, { ...result, generated_at: nowIso(), backend: 'fixture-forbidden', fixture_policy: fixtureDecision });
24
+ return result;
25
+ }
26
+ if (fixtureRequested) {
12
27
  const verdict = input.forceVerdict || (process.env.SKS_LOOP_GPT_FINAL_REJECT === '1' ? 'reject' : 'approve');
13
28
  const result = buildResult(input.plan.mission_id, reviewedLoopIds, changedFiles, verdict, verdict === 'approve' ? [] : ['fixture_revision_required'], artifactPath, []);
14
- await writeJsonAtomic(artifactPath, { ...result, generated_at: nowIso(), backend: 'fixture' });
29
+ await writeJsonAtomic(artifactPath, { ...result, generated_at: nowIso(), backend: 'fixture', fixture_policy: fixtureDecision, fixture_allowed_reason: fixtureDecision.reason, side_effect_report: input.sideEffectReport || null });
30
+ return result;
31
+ }
32
+ if (input.sideEffectReport && !input.sideEffectReport.ok) {
33
+ const result = buildResult(input.plan.mission_id, reviewedLoopIds, changedFiles, 'reject', [], artifactPath, input.sideEffectReport.blockers);
34
+ await writeJsonAtomic(artifactPath, { ...result, generated_at: nowIso(), backend: 'side-effect-block', side_effect_report: input.sideEffectReport });
15
35
  return result;
16
36
  }
17
37
  const arbiter = await runGptFinalArbiter({
@@ -27,17 +47,17 @@ export async function runLoopGptFinalArbiter(input) {
27
47
  changed_files: proof.changed_files,
28
48
  blockers: proof.blockers
29
49
  })),
30
- candidate_diff: JSON.stringify({ changed_files: changedFiles, integration_merge: input.integrationMerge }),
50
+ candidate_diff: JSON.stringify({ changed_files: changedFiles, integration_merge: input.integrationMerge, side_effect_report: input.sideEffectReport || null }),
31
51
  verification_results: input.proofs.map((proof) => ({ id: proof.loop_id, ok: proof.status === 'completed', blockers: proof.blockers })),
32
- side_effect_report: { schema: 'sks.loop-side-effect-report.v1', ok: true, changed_files: changedFiles },
33
- mutation_ledger: { schema: 'sks.loop-mutation-ledger.v1', proofs: input.proofs },
52
+ side_effect_report: input.sideEffectReport || { schema: 'sks.loop-side-effect-report.v1', ok: true, changed_files: changedFiles },
53
+ mutation_ledger: { schema: 'sks.loop-mutation-ledger.v1', proofs: input.proofs, path: input.sideEffectReport?.mutation_ledger_path || null },
34
54
  rollback_plan: { schema: 'sks.loop-rollback-plan.v1', strategy: 'git-worktree-or-human-handoff' }
35
55
  }, { cwd: input.root, mutationLedgerRoot: `${input.root}/.sneakoscope/missions/${input.plan.mission_id}/loops/gpt-final-arbiter` });
36
56
  const status = String(arbiter.result?.status || '');
37
57
  const verdict = status === 'approved' || status === 'modified' ? 'approve' : status === 'needs_more_work' ? 'revise' : 'reject';
38
58
  const blockers = stringArray(arbiter.blockers);
39
59
  const result = buildResult(input.plan.mission_id, reviewedLoopIds, changedFiles, verdict, stringArray(arbiter.result?.required_followup_work), artifactPath, blockers);
40
- await writeJsonAtomic(artifactPath, { ...result, generated_at: nowIso(), backend: arbiter.backend || null, arbiter });
60
+ await writeJsonAtomic(artifactPath, { ...result, generated_at: nowIso(), backend: arbiter.backend || null, side_effect_report: input.sideEffectReport || null, arbiter });
41
61
  return result;
42
62
  }
43
63
  function buildResult(missionId, reviewedLoopIds, changedFiles, verdict, revisions, artifactPath, blockers) {