sneakoscope 3.1.0 → 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 (84) 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/commands/zellij-slot-column-anchor.js +3 -1
  9. package/dist/commands/zellij-slot-pane.js +19 -2
  10. package/dist/core/agents/agent-janitor.js +10 -1
  11. package/dist/core/agents/agent-orchestrator.js +8 -2
  12. package/dist/core/agents/agent-proof-evidence.js +20 -0
  13. package/dist/core/agents/agent-runner-ollama.js +11 -4
  14. package/dist/core/agents/fast-mode-policy.js +7 -5
  15. package/dist/core/agents/intelligent-work-graph.js +93 -14
  16. package/dist/core/agents/native-cli-session-swarm.js +115 -9
  17. package/dist/core/agents/no-subagent-scaling-policy.js +10 -1
  18. package/dist/core/agents/official-subagent-helper-policy.js +62 -0
  19. package/dist/core/codex-app.js +0 -2
  20. package/dist/core/codex-control/codex-task-runner.js +9 -0
  21. package/dist/core/commands/fast-mode-command.js +1 -1
  22. package/dist/core/commands/loop-command.js +86 -13
  23. package/dist/core/commands/naruto-command.js +34 -21
  24. package/dist/core/commands/team-command.js +1 -0
  25. package/dist/core/commands/wiki-command.js +35 -1
  26. package/dist/core/fsx.js +1 -1
  27. package/dist/core/init.js +1 -2
  28. package/dist/core/locks/file-lock.js +88 -0
  29. package/dist/core/loops/loop-artifacts.js +54 -2
  30. package/dist/core/loops/loop-checkpoint.js +22 -0
  31. package/dist/core/loops/loop-concurrency-budget.js +55 -0
  32. package/dist/core/loops/loop-final-arbiter-contract.js +28 -0
  33. package/dist/core/loops/loop-finalizer.js +55 -7
  34. package/dist/core/loops/loop-fixture-policy.js +58 -0
  35. package/dist/core/loops/loop-gate-registry.js +96 -0
  36. package/dist/core/loops/loop-gate-runner.js +206 -17
  37. package/dist/core/loops/loop-gpt-final-arbiter.js +81 -0
  38. package/dist/core/loops/loop-integration-merge.js +80 -0
  39. package/dist/core/loops/loop-interrupt-registry.js +118 -0
  40. package/dist/core/loops/loop-lease.js +35 -20
  41. package/dist/core/loops/loop-merge-strategy.js +105 -0
  42. package/dist/core/loops/loop-mutation-ledger.js +103 -0
  43. package/dist/core/loops/loop-planner.js +36 -5
  44. package/dist/core/loops/loop-runtime-control.js +27 -0
  45. package/dist/core/loops/loop-runtime.js +254 -96
  46. package/dist/core/loops/loop-scheduler.js +14 -5
  47. package/dist/core/loops/loop-side-effect-scanner.js +91 -0
  48. package/dist/core/loops/loop-worker-prompts.js +43 -0
  49. package/dist/core/loops/loop-worker-runtime.js +281 -0
  50. package/dist/core/loops/loop-worktree-runtime.js +92 -0
  51. package/dist/core/naruto/naruto-finalizer.js +7 -2
  52. package/dist/core/naruto/naruto-loop-mesh.js +10 -1
  53. package/dist/core/proof/auto-finalize.js +3 -2
  54. package/dist/core/proof/proof-schema.js +6 -0
  55. package/dist/core/proof/proof-writer.js +5 -2
  56. package/dist/core/proof/root-cause-policy.js +70 -0
  57. package/dist/core/proof/route-adapter.js +18 -1
  58. package/dist/core/proof/route-finalizer.js +71 -6
  59. package/dist/core/proof/route-proof-gate.js +4 -0
  60. package/dist/core/release/release-gate-batch-runner.js +56 -10
  61. package/dist/core/release/release-gate-cache-v2.js +18 -3
  62. package/dist/core/release/release-gate-dag.js +121 -18
  63. package/dist/core/release/release-gate-node.js +2 -1
  64. package/dist/core/release/release-gate-resource-governor.js +27 -6
  65. package/dist/core/skills/core-skill-meta-update.js +24 -0
  66. package/dist/core/skills/core-skill-reflection.js +94 -0
  67. package/dist/core/skills/core-skill-trainer.js +103 -0
  68. package/dist/core/trust-kernel/completion-contract.js +4 -0
  69. package/dist/core/trust-kernel/route-contract.js +4 -1
  70. package/dist/core/version.js +1 -1
  71. package/dist/core/zellij/zellij-right-column-manager.js +13 -2
  72. package/dist/core/zellij/zellij-slot-column-anchor.js +40 -3
  73. package/dist/core/zellij/zellij-slot-pane-renderer.js +36 -11
  74. package/dist/core/zellij/zellij-slot-telemetry.js +96 -44
  75. package/dist/core/zellij/zellij-worker-pane-manager.js +42 -4
  76. package/dist/scripts/lib/native-cli-session-swarm-check-lib.js +14 -2
  77. package/dist/scripts/loop-directive-check-lib.js +225 -2
  78. package/dist/scripts/loop-hardening-check-lib.js +289 -0
  79. package/dist/scripts/loop-worker-fixture-child.js +53 -0
  80. package/dist/scripts/naruto-real-local-gpt-final-smoke.js +10 -1
  81. package/dist/scripts/prepublish-release-check-or-fast.js +38 -10
  82. package/dist/scripts/release-check-stamp.js +29 -4
  83. package/dist/scripts/release-gate-existence-audit.js +1 -0
  84. package/package.json +32 -2
@@ -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.0';
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()) {
@@ -0,0 +1,88 @@
1
+ import fsp from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { ensureDir, nowIso, randomId, writeJsonAtomic } from '../fsx.js';
4
+ import { guardedRm, guardContextForRoute } from '../safety/mutation-guard.js';
5
+ import { CONFIRMATION_REQUIRED, REQUESTED_SCOPE_CONTRACT_SCHEMA } from '../safety/requested-scope-contract.js';
6
+ export async function withFileLock(input, fn) {
7
+ const lockPath = path.resolve(input.lockPath);
8
+ const timeoutMs = Math.max(1, input.timeoutMs);
9
+ const staleMs = Math.max(1, input.staleMs);
10
+ const started = Date.now();
11
+ const owner = `${process.pid}-${randomId(8)}`;
12
+ await ensureDir(path.dirname(lockPath));
13
+ while (true) {
14
+ try {
15
+ await fsp.mkdir(lockPath);
16
+ await writeJsonAtomic(path.join(lockPath, 'owner.json'), {
17
+ schema: 'sks.file-lock-owner.v1',
18
+ owner,
19
+ pid: process.pid,
20
+ acquired_at: nowIso(),
21
+ stale_ms: staleMs
22
+ });
23
+ break;
24
+ }
25
+ catch (err) {
26
+ const code = errorCode(err);
27
+ if (code !== 'EEXIST')
28
+ throw err;
29
+ await recoverStaleLock(lockPath, staleMs);
30
+ if (Date.now() - started > timeoutMs) {
31
+ throw new Error(`file_lock_timeout:${lockPath}`);
32
+ }
33
+ await sleep(jitterDelay());
34
+ }
35
+ }
36
+ try {
37
+ return await fn();
38
+ }
39
+ finally {
40
+ await removeLockDir(lockPath).catch(() => undefined);
41
+ }
42
+ }
43
+ async function recoverStaleLock(lockPath, staleMs) {
44
+ try {
45
+ const stat = await fsp.stat(lockPath);
46
+ if (Date.now() - stat.mtimeMs > staleMs) {
47
+ await removeLockDir(lockPath);
48
+ }
49
+ }
50
+ catch { }
51
+ }
52
+ async function removeLockDir(lockPath) {
53
+ await guardedRm(guardContextForRoute(process.cwd(), lockScopeContract(lockPath), 'remove SKS file lock directory'), lockPath, {
54
+ recursive: true,
55
+ force: true
56
+ });
57
+ }
58
+ function lockScopeContract(lockPath) {
59
+ const resolved = path.resolve(lockPath);
60
+ return {
61
+ schema: REQUESTED_SCOPE_CONTRACT_SCHEMA,
62
+ route: 'internal:file-lock',
63
+ user_request: 'Remove only the current SKS file-lock directory.',
64
+ allowed_mutations: {
65
+ project_files: true,
66
+ global_codex_config: false,
67
+ codex_app_process: false,
68
+ codex_lb_auth: false,
69
+ package_install: false,
70
+ zellij_install: false,
71
+ network: false,
72
+ skill_snapshot_promotion: false
73
+ },
74
+ allowed_paths: [resolved, `${resolved}/**`],
75
+ forbidden_paths: ['~/.codex/config.toml', '/Applications/**'],
76
+ requires_explicit_confirmation: [...CONFIRMATION_REQUIRED]
77
+ };
78
+ }
79
+ function jitterDelay() {
80
+ return 15 + Math.floor(Math.random() * 45);
81
+ }
82
+ function sleep(ms) {
83
+ return new Promise((resolve) => setTimeout(resolve, ms));
84
+ }
85
+ function errorCode(err) {
86
+ return err && typeof err === 'object' && 'code' in err ? String(err.code) : '';
87
+ }
88
+ //# sourceMappingURL=file-lock.js.map
@@ -1,9 +1,10 @@
1
1
  import path from 'node:path';
2
2
  export function loopRoot(root, missionId) {
3
- return path.join(root, '.sneakoscope', 'missions', missionId, 'loops');
3
+ const missionsRoot = path.resolve(root, '.sneakoscope', 'missions');
4
+ return containedJoin(missionsRoot, safeArtifactId('mission', missionId), 'loops');
4
5
  }
5
6
  export function loopNodeRoot(root, missionId, loopId) {
6
- return path.join(loopRoot(root, missionId), loopId);
7
+ return containedJoin(loopRoot(root, missionId), safeArtifactId('loop', loopId));
7
8
  }
8
9
  export function loopPlanPath(root, missionId) {
9
10
  return path.join(loopRoot(root, missionId), 'loop-plan.json');
@@ -20,9 +21,45 @@ export function loopProofPath(root, missionId, loopId) {
20
21
  export function loopBudgetPath(root, missionId, loopId) {
21
22
  return path.join(loopNodeRoot(root, missionId, loopId), 'loop-budget.json');
22
23
  }
24
+ export function loopCheckpointPath(root, missionId, loopId, iteration, phase) {
25
+ return path.join(loopNodeRoot(root, missionId, loopId), 'checkpoints', `${String(Math.max(1, Math.floor(iteration))).padStart(4, '0')}-${sanitizeArtifactPart(phase)}.json`);
26
+ }
27
+ export function loopLatestCheckpointPath(root, missionId, loopId) {
28
+ return path.join(loopNodeRoot(root, missionId, loopId), 'checkpoint-latest.json');
29
+ }
23
30
  export function loopGraphProofPath(root, missionId) {
24
31
  return path.join(loopRoot(root, missionId), 'loop-graph-proof.json');
25
32
  }
33
+ export function loopIntegrationMergePath(root, missionId) {
34
+ return path.join(loopRoot(root, missionId), 'integration-merge.json');
35
+ }
36
+ export function loopGptFinalArbiterPath(root, missionId) {
37
+ return path.join(loopRoot(root, missionId), 'loop-gpt-final-arbiter.json');
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
+ }
60
+ export function loopKillRequestPath(root, missionId) {
61
+ return path.join(loopRoot(root, missionId), 'kill-request.json');
62
+ }
26
63
  export function loopGatePath(root, missionId, loopId, gateId) {
27
64
  return path.join(loopNodeRoot(root, missionId, loopId), 'gates', `${sanitizeArtifactPart(gateId)}.json`);
28
65
  }
@@ -38,4 +75,19 @@ export function loopOwnerLedgerPath(root, missionId) {
38
75
  export function sanitizeArtifactPart(value) {
39
76
  return String(value || 'artifact').replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 96) || 'artifact';
40
77
  }
78
+ function safeArtifactId(kind, value) {
79
+ const text = String(value || '').trim();
80
+ const sanitized = sanitizeArtifactPart(text);
81
+ if (!text || sanitized !== text)
82
+ throw new Error(`invalid_loop_${kind}_id:${text || 'empty'}`);
83
+ return sanitized;
84
+ }
85
+ function containedJoin(base, ...parts) {
86
+ const resolvedBase = path.resolve(base);
87
+ const target = path.resolve(resolvedBase, ...parts);
88
+ if (target !== resolvedBase && !target.startsWith(`${resolvedBase}${path.sep}`)) {
89
+ throw new Error(`loop_artifact_path_escape:${target}`);
90
+ }
91
+ return target;
92
+ }
41
93
  //# sourceMappingURL=loop-artifacts.js.map
@@ -0,0 +1,22 @@
1
+ import { readJson, writeJsonAtomic } from '../fsx.js';
2
+ import { loopCheckpointPath, loopLatestCheckpointPath } from './loop-artifacts.js';
3
+ export async function writeLoopCheckpoint(input) {
4
+ const checkpoint = {
5
+ schema: 'sks.loop-checkpoint.v1',
6
+ mission_id: input.mission_id,
7
+ loop_id: input.loop_id,
8
+ iteration: input.iteration,
9
+ phase: input.phase,
10
+ state_path: input.state_path,
11
+ proof_path: input.proof_path,
12
+ resumable: input.resumable,
13
+ created_at: new Date().toISOString()
14
+ };
15
+ await writeJsonAtomic(loopCheckpointPath(input.root, input.mission_id, input.loop_id, input.iteration, input.phase), checkpoint);
16
+ await writeJsonAtomic(loopLatestCheckpointPath(input.root, input.mission_id, input.loop_id), checkpoint);
17
+ return checkpoint;
18
+ }
19
+ export async function readLatestLoopCheckpoint(root, missionId, loopId) {
20
+ return readJson(loopLatestCheckpointPath(root, missionId, loopId), null);
21
+ }
22
+ //# sourceMappingURL=loop-checkpoint.js.map
@@ -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,6 +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';
4
+ import { runLoopGptFinalArbiter } from './loop-gpt-final-arbiter.js';
5
+ import { mergeLoopWorktrees } from './loop-integration-merge.js';
3
6
  import { graphProofFromLoopProofs } from './loop-scheduler.js';
7
+ import { buildLoopSideEffectReport } from './loop-side-effect-scanner.js';
4
8
  export async function finalizeLoopGraph(input) {
5
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)));
6
10
  const realProofs = proofs.filter((proof) => Boolean(proof));
@@ -11,16 +15,60 @@ export async function finalizeLoopGraph(input) {
11
15
  maxActiveWorkers: input.maxActiveWorkers || Math.max(1, realProofs.reduce((sum, proof) => sum + proof.maker_result.worker_count + proof.checker_result.worker_count, 0)),
12
16
  wallMs: input.wallMs || 1
13
17
  });
18
+ const integrationMerge = await mergeLoopWorktrees({
19
+ root: input.root,
20
+ plan: input.plan,
21
+ proofs: realProofs
22
+ });
14
23
  const anyHandoff = realProofs.some((proof) => proof.handoff.required);
15
- const anySourceMutation = realProofs.some((proof) => proof.changed_files.length > 0);
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
+ });
35
+ const arbiter = anySourceMutation
36
+ ? await runLoopGptFinalArbiter({ root: input.root, plan: input.plan, proofs: realProofs, integrationMerge, sideEffectReport })
37
+ : null;
38
+ const blockers = [
39
+ ...graph.blockers,
40
+ ...(anyHandoff ? ['loop_handoff_required'] : []),
41
+ ...(integrationMerge.ok ? [] : integrationMerge.blockers),
42
+ ...(sideEffectReport.ok ? [] : sideEffectReport.blockers),
43
+ ...(anySourceMutation && !arbiter ? ['gpt_final_arbiter_missing'] : []),
44
+ ...(selectedGptFinal && anySourceMutation && (!contract || !arbiter) ? ['gpt_final_arbiter_contract_unfulfilled'] : []),
45
+ ...(arbiter && !arbiter.ok ? ['gpt_final_arbiter_not_approved', ...arbiter.blockers] : [])
46
+ ];
16
47
  const finalGraph = {
17
48
  ...graph,
18
- ok: graph.ok && !anyHandoff && (!anySourceMutation || graph.gates.selected.includes('gpt:final-arbiter')),
19
- blockers: [
20
- ...graph.blockers,
21
- ...(anyHandoff ? ['loop_handoff_required'] : []),
22
- ...(anySourceMutation && !graph.gates.selected.includes('gpt:final-arbiter') ? ['gpt_final_arbiter_missing'] : [])
23
- ]
49
+ ok: graph.ok && blockers.length === 0,
50
+ blockers: [...new Set(blockers)],
51
+ integration_merge: {
52
+ ok: integrationMerge.ok,
53
+ artifact_path: `.sneakoscope/missions/${input.plan.mission_id}/loops/integration-merge.json`,
54
+ applied_loops: integrationMerge.applied_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
62
+ },
63
+ ...(arbiter ? {
64
+ gpt_final_arbiter: {
65
+ ok: arbiter.ok,
66
+ artifact_path: arbiter.artifact_path,
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'
70
+ }
71
+ } : {})
24
72
  };
25
73
  await writeJsonAtomic(loopGraphProofPath(input.root, input.plan.mission_id), finalGraph);
26
74
  return finalGraph;
@@ -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
@@ -0,0 +1,96 @@
1
+ import path from 'node:path';
2
+ import { readJson } from '../fsx.js';
3
+ export async function resolveLoopGate(root, gateId) {
4
+ const builtin = builtinLoopGate(gateId);
5
+ if (builtin)
6
+ return builtin;
7
+ const releaseGate = await resolveReleaseGate(root, gateId);
8
+ if (releaseGate)
9
+ return releaseGate;
10
+ const packageGate = await resolvePackageScriptGate(root, gateId);
11
+ if (packageGate)
12
+ return packageGate;
13
+ return null;
14
+ }
15
+ export async function listLoopGateDefinitions(root) {
16
+ const packageJson = await readJson(path.join(root, 'package.json'), {});
17
+ const release = await readJson(path.join(root, 'release-gates.v2.json'), {});
18
+ const packageScripts = packageJson && typeof packageJson === 'object' && packageJson.scripts && typeof packageJson.scripts === 'object'
19
+ ? Object.keys(packageJson.scripts)
20
+ : [];
21
+ const releaseGates = Array.isArray(release.gates) ? release.gates : [];
22
+ const definitions = [
23
+ ...['gpt:final-arbiter', 'human:handoff-required', 'loop:checker-fresh-session', 'loop:state-valid', 'loop:budget-valid'].map((id) => builtinLoopGate(id)).filter((row) => Boolean(row)),
24
+ ...releaseGates.map((gate) => normalizeReleaseGate(gate)).filter((row) => Boolean(row)),
25
+ ...packageScripts.map((id) => ({
26
+ id,
27
+ command: `npm run ${shellQuote(id)} --silent`,
28
+ source: 'package-json',
29
+ side_effect: 'hermetic',
30
+ timeout_ms: 300000,
31
+ cache_allowed: true
32
+ }))
33
+ ];
34
+ const byId = new Map();
35
+ for (const definition of definitions)
36
+ if (!byId.has(definition.id))
37
+ byId.set(definition.id, definition);
38
+ return [...byId.values()];
39
+ }
40
+ function builtinLoopGate(gateId) {
41
+ if (gateId === 'gpt:final-arbiter') {
42
+ return { id: gateId, command: 'builtin:gpt-final-arbiter', source: 'builtin-pseudo', side_effect: 'read-only', timeout_ms: 300000, cache_allowed: false };
43
+ }
44
+ if (gateId === 'human:handoff-required') {
45
+ return { id: gateId, command: 'builtin:human-handoff-required', source: 'builtin-pseudo', side_effect: 'human', timeout_ms: 0, cache_allowed: false };
46
+ }
47
+ if (gateId === 'loop:checker-fresh-session') {
48
+ return { id: gateId, command: 'builtin:loop-checker-fresh-session', source: 'builtin-pseudo', side_effect: 'read-only', timeout_ms: 30000, cache_allowed: false };
49
+ }
50
+ if (gateId === 'loop:state-valid') {
51
+ return { id: gateId, command: 'builtin:loop-state-valid', source: 'builtin-pseudo', side_effect: 'read-only', timeout_ms: 30000, cache_allowed: true };
52
+ }
53
+ if (gateId === 'loop:budget-valid') {
54
+ return { id: gateId, command: 'builtin:loop-budget-valid', source: 'builtin-pseudo', side_effect: 'read-only', timeout_ms: 30000, cache_allowed: true };
55
+ }
56
+ return null;
57
+ }
58
+ async function resolveReleaseGate(root, gateId) {
59
+ const release = await readJson(path.join(root, 'release-gates.v2.json'), {});
60
+ const gate = Array.isArray(release.gates) ? release.gates.find((row) => row.id === gateId) : null;
61
+ return gate ? normalizeReleaseGate(gate) : null;
62
+ }
63
+ function normalizeReleaseGate(gate) {
64
+ const id = String(gate.id || '');
65
+ const command = String(gate.command || '');
66
+ if (!id || !command)
67
+ return null;
68
+ return {
69
+ id,
70
+ command,
71
+ source: 'release-gates-v2',
72
+ side_effect: normalizeSideEffect(gate.side_effect),
73
+ timeout_ms: Number.isFinite(Number(gate.timeout_ms)) ? Math.max(1, Number(gate.timeout_ms)) : 300000,
74
+ cache_allowed: gate.cache?.enabled !== false
75
+ };
76
+ }
77
+ async function resolvePackageScriptGate(root, gateId) {
78
+ const packageJson = await readJson(path.join(root, 'package.json'), {});
79
+ if (!packageJson?.scripts || typeof packageJson.scripts !== 'object' || !(gateId in packageJson.scripts))
80
+ return null;
81
+ return {
82
+ id: gateId,
83
+ command: `npm run ${shellQuote(gateId)} --silent`,
84
+ source: 'package-json',
85
+ side_effect: 'hermetic',
86
+ timeout_ms: 300000,
87
+ cache_allowed: true
88
+ };
89
+ }
90
+ function normalizeSideEffect(value) {
91
+ return value === 'read-only' || value === 'mutation' || value === 'human' ? value : 'hermetic';
92
+ }
93
+ function shellQuote(value) {
94
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
95
+ }
96
+ //# sourceMappingURL=loop-gate-registry.js.map