sneakoscope 3.0.4 → 3.1.1

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 (85) 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/command-registry.js +1 -0
  8. package/dist/cli/context7-command.js +29 -5
  9. package/dist/cli/install-helpers.js +15 -7
  10. package/dist/commands/zellij-slot-column-anchor.js +3 -1
  11. package/dist/commands/zellij-slot-pane.js +19 -2
  12. package/dist/core/agents/agent-janitor.js +10 -1
  13. package/dist/core/agents/agent-orchestrator.js +1 -0
  14. package/dist/core/agents/agent-runner-ollama.js +11 -4
  15. package/dist/core/agents/native-cli-session-swarm.js +69 -9
  16. package/dist/core/agents/runtime-proof-summary.js +4 -0
  17. package/dist/core/codex-control/codex-task-runner.js +9 -0
  18. package/dist/core/commands/goal-command.js +19 -1
  19. package/dist/core/commands/loop-command.js +176 -0
  20. package/dist/core/commands/naruto-command.js +26 -17
  21. package/dist/core/commands/team-command.js +1 -0
  22. package/dist/core/fsx.js +1 -1
  23. package/dist/core/init.js +6 -1
  24. package/dist/core/locks/file-lock.js +88 -0
  25. package/dist/core/loops/goal-to-loop-compat.js +23 -0
  26. package/dist/core/loops/loop-artifacts.js +72 -0
  27. package/dist/core/loops/loop-checkpoint.js +22 -0
  28. package/dist/core/loops/loop-decomposer.js +56 -0
  29. package/dist/core/loops/loop-finalizer.js +54 -0
  30. package/dist/core/loops/loop-gate-ladder.js +16 -0
  31. package/dist/core/loops/loop-gate-registry.js +96 -0
  32. package/dist/core/loops/loop-gate-runner.js +177 -0
  33. package/dist/core/loops/loop-gate-selector.js +52 -0
  34. package/dist/core/loops/loop-gpt-final-arbiter.js +61 -0
  35. package/dist/core/loops/loop-integration-merge.js +75 -0
  36. package/dist/core/loops/loop-iteration-runner.js +2 -0
  37. package/dist/core/loops/loop-lease.js +91 -0
  38. package/dist/core/loops/loop-observability.js +19 -0
  39. package/dist/core/loops/loop-owner-inference.js +57 -0
  40. package/dist/core/loops/loop-owner-ledger.js +2 -0
  41. package/dist/core/loops/loop-planner.js +170 -0
  42. package/dist/core/loops/loop-proof-summary.js +10 -0
  43. package/dist/core/loops/loop-proof.js +2 -0
  44. package/dist/core/loops/loop-risk-classifier.js +42 -0
  45. package/dist/core/loops/loop-runtime-control.js +25 -0
  46. package/dist/core/loops/loop-runtime.js +314 -0
  47. package/dist/core/loops/loop-scheduler.js +69 -0
  48. package/dist/core/loops/loop-schema.js +63 -0
  49. package/dist/core/loops/loop-state.js +61 -0
  50. package/dist/core/loops/loop-worker-prompts.js +43 -0
  51. package/dist/core/loops/loop-worker-runtime.js +275 -0
  52. package/dist/core/loops/loop-worktree-runtime.js +92 -0
  53. package/dist/core/naruto/naruto-finalizer.js +7 -2
  54. package/dist/core/naruto/naruto-loop-mesh.js +39 -0
  55. package/dist/core/naruto/naruto-loop-worker-router.js +38 -0
  56. package/dist/core/pipeline-internals/runtime-core.js +82 -2
  57. package/dist/core/proof/proof-schema.js +6 -0
  58. package/dist/core/proof/proof-writer.js +5 -2
  59. package/dist/core/proof/root-cause-policy.js +70 -0
  60. package/dist/core/proof/route-adapter.js +18 -1
  61. package/dist/core/proof/route-proof-gate.js +4 -0
  62. package/dist/core/release/release-gate-batch-runner.js +56 -10
  63. package/dist/core/release/release-gate-cache-v2.js +18 -3
  64. package/dist/core/release/release-gate-dag.js +65 -17
  65. package/dist/core/release/release-gate-node.js +2 -1
  66. package/dist/core/release/release-gate-resource-governor.js +27 -6
  67. package/dist/core/skills/core-skill-meta-update.js +24 -0
  68. package/dist/core/skills/core-skill-reflection.js +94 -0
  69. package/dist/core/skills/core-skill-trainer.js +103 -0
  70. package/dist/core/trust-kernel/completion-contract.js +4 -0
  71. package/dist/core/trust-kernel/route-contract.js +4 -1
  72. package/dist/core/version.js +1 -1
  73. package/dist/core/zellij/zellij-right-column-manager.js +13 -2
  74. package/dist/core/zellij/zellij-slot-column-anchor.js +45 -5
  75. package/dist/core/zellij/zellij-slot-pane-renderer.js +37 -10
  76. package/dist/core/zellij/zellij-slot-telemetry.js +96 -44
  77. package/dist/core/zellij/zellij-worker-pane-manager.js +42 -4
  78. package/dist/scripts/loop-directive-check-lib.js +388 -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/package.json +38 -3
  82. package/schemas/loops/loop-node.schema.json +21 -0
  83. package/schemas/loops/loop-plan.schema.json +21 -0
  84. package/schemas/loops/loop-proof.schema.json +20 -0
  85. package/schemas/loops/loop-state.schema.json +19 -0
@@ -0,0 +1,63 @@
1
+ export function defaultLoopBudget(overrides = {}) {
2
+ return {
3
+ max_iterations: 2,
4
+ max_wall_ms: 15 * 60 * 1000,
5
+ max_model_calls: 8,
6
+ max_subagents: 4,
7
+ max_tokens_estimate: 120000,
8
+ max_changed_files: 16,
9
+ max_patch_bytes: 256000,
10
+ ...overrides
11
+ };
12
+ }
13
+ export function validateLoopPlan(plan) {
14
+ const blockers = [
15
+ ...(plan.schema !== 'sks.loop-plan.v1' ? ['loop_plan_schema_invalid'] : []),
16
+ ...(!plan.mission_id ? ['loop_plan_mission_id_missing'] : []),
17
+ ...(!plan.request ? ['loop_plan_request_missing'] : []),
18
+ ...(!plan.integration_loop_id ? ['loop_plan_integration_loop_missing'] : []),
19
+ ...(plan.graph.nodes.length === 0 ? ['loop_plan_nodes_missing'] : []),
20
+ ...(!plan.graph.nodes.some((node) => node.loop_id === plan.integration_loop_id) ? ['loop_plan_integration_node_missing'] : [])
21
+ ];
22
+ const ids = new Set();
23
+ for (const node of plan.graph.nodes) {
24
+ const result = validateLoopNode(node);
25
+ blockers.push(...result.blockers);
26
+ if (ids.has(node.loop_id))
27
+ blockers.push(`loop_node_duplicate:${node.loop_id}`);
28
+ ids.add(node.loop_id);
29
+ }
30
+ for (const edge of plan.graph.edges) {
31
+ if (!ids.has(edge.from) || !ids.has(edge.to))
32
+ blockers.push(`loop_edge_unknown:${edge.from}->${edge.to}`);
33
+ }
34
+ return { ok: blockers.length === 0, blockers };
35
+ }
36
+ export function validateLoopNode(node) {
37
+ const blockers = [
38
+ ...(node.schema !== 'sks.loop-node.v1' ? [`loop_node_schema_invalid:${node.loop_id}`] : []),
39
+ ...(!node.loop_id ? ['loop_node_id_missing'] : []),
40
+ ...(!node.mission_id ? [`loop_node_mission_missing:${node.loop_id}`] : []),
41
+ ...(!node.state_file ? [`loop_state_file_missing:${node.loop_id}`] : []),
42
+ ...(!node.run_log_file ? [`loop_run_log_file_missing:${node.loop_id}`] : []),
43
+ ...(!node.owner_scope ? [`loop_owner_scope_missing:${node.loop_id}`] : []),
44
+ ...validateLoopBudget(node.budget).blockers.map((blocker) => `${node.loop_id}:${blocker}`),
45
+ ...(node.level === 'L3-unattended' && ['high', 'critical'].includes(node.risk.level) ? [`loop_l3_risk_blocked:${node.loop_id}`] : []),
46
+ ...(node.level === 'L2-action' && !node.checker.required_before_next_iteration ? [`loop_action_checker_missing:${node.loop_id}`] : [])
47
+ ];
48
+ return { ok: blockers.length === 0, blockers };
49
+ }
50
+ export function validateLoopBudget(budget) {
51
+ const blockers = [];
52
+ for (const [key, value] of Object.entries(budget)) {
53
+ if (!Number.isFinite(value) || value < 0)
54
+ blockers.push(`loop_budget_invalid:${key}`);
55
+ }
56
+ if (budget.max_iterations < 1)
57
+ blockers.push('loop_budget_iterations_missing');
58
+ return { ok: blockers.length === 0, blockers };
59
+ }
60
+ export function allGateIds(gates) {
61
+ return [...new Set([...gates.triage, ...gates.local, ...gates.checker, ...gates.integration, ...gates.final])];
62
+ }
63
+ //# sourceMappingURL=loop-schema.js.map
@@ -0,0 +1,61 @@
1
+ import { appendJsonl, readJson, writeJsonAtomic } from '../fsx.js';
2
+ import { loopRunLogPath, loopStatePath } from './loop-artifacts.js';
3
+ export async function readLoopState(root, missionId, loopId) {
4
+ return readJson(loopStatePath(root, missionId, loopId), null);
5
+ }
6
+ export async function writeLoopState(root, state) {
7
+ await writeJsonAtomic(loopStatePath(root, state.mission_id, state.loop_id), state);
8
+ return state;
9
+ }
10
+ export async function appendLoopRunLog(root, missionId, loopId, event) {
11
+ await appendJsonl(loopRunLogPath(root, missionId, loopId), { ts: event.ts || new Date().toISOString(), ...event });
12
+ }
13
+ export async function updateLoopState(root, missionId, loopId, patch) {
14
+ const current = await readLoopState(root, missionId, loopId);
15
+ if (!current)
16
+ throw new Error(`loop_state_missing:${loopId}`);
17
+ const next = {
18
+ ...current,
19
+ ...patch,
20
+ acting_on: { ...current.acting_on, ...(patch.acting_on || {}) },
21
+ handoff: { ...current.handoff, ...(patch.handoff || {}) },
22
+ budget_used: { ...current.budget_used, ...(patch.budget_used || {}) },
23
+ updated_at: new Date().toISOString()
24
+ };
25
+ await writeLoopState(root, next);
26
+ return next;
27
+ }
28
+ export function initialLoopState(input) {
29
+ return {
30
+ schema: 'sks.loop-state.v1',
31
+ mission_id: input.missionId,
32
+ loop_id: input.loopId,
33
+ status: 'planned',
34
+ iteration: 0,
35
+ acting_on: {
36
+ files: input.files,
37
+ worktree_id: input.worktreeId || null,
38
+ branch: input.branch || null
39
+ },
40
+ current_phase: 'triage',
41
+ last_action: null,
42
+ last_gate_result: null,
43
+ last_checker_result: null,
44
+ blockers: [],
45
+ handoff: {
46
+ required: false,
47
+ reason: null,
48
+ artifact: null
49
+ },
50
+ budget_used: {
51
+ wall_ms: 0,
52
+ model_calls: 0,
53
+ subagents: 0,
54
+ iterations: 0,
55
+ changed_files: 0,
56
+ patch_bytes: 0
57
+ },
58
+ updated_at: new Date().toISOString()
59
+ };
60
+ }
61
+ //# sourceMappingURL=loop-state.js.map
@@ -0,0 +1,43 @@
1
+ import { allGateIds } from './loop-schema.js';
2
+ export function buildLoopMakerPrompt(input) {
3
+ const node = input.node;
4
+ return [
5
+ 'You are the maker worker for an SKS Loop Mesh L2 action loop.',
6
+ `Mission: ${input.plan.mission_id}`,
7
+ `Loop: ${node.loop_id}`,
8
+ `Purpose: ${node.purpose}`,
9
+ `Owner files: ${node.owner_scope.files.join(', ') || '-'}`,
10
+ `Owner directories: ${node.owner_scope.directories.join(', ') || '-'}`,
11
+ `Allowed mutation scope: ${ownerScopeText(node)}`,
12
+ 'Do not mutate outside the owner scope.',
13
+ `Selected local gates: ${allGateIds(node.gates).join(', ') || '-'}`,
14
+ `Budget: ${JSON.stringify(node.budget)}`,
15
+ `Worktree path: ${input.worktreePath || '-'}`,
16
+ 'Write a patch candidate/runtime proof artifact with changed files and blockers.',
17
+ 'No synthetic pass is allowed for production proof.'
18
+ ].join('\n');
19
+ }
20
+ export function buildLoopCheckerPrompt(input) {
21
+ const node = input.node;
22
+ return [
23
+ 'You are the checker worker for an SKS Loop Mesh action loop.',
24
+ 'You must run in a fresh session and must not mutate source files.',
25
+ `Mission: ${input.plan.mission_id}`,
26
+ `Loop: ${node.loop_id}`,
27
+ `Purpose: ${node.purpose}`,
28
+ `Maker artifacts: ${input.makerArtifacts.join(', ') || '-'}`,
29
+ `Diff/patch summary: ${input.diffSummary || '-'}`,
30
+ `Selected gates: ${allGateIds(node.gates).join(', ') || '-'}`,
31
+ `Risk: ${node.risk.level} (${node.risk.reasons.join(', ') || '-'})`,
32
+ 'Reject unrequested side effects and owner-scope violations.',
33
+ 'Write checker-findings.json with fresh_session, reviewed_maker_artifacts, side_effects_detected, and approved.',
34
+ 'No synthetic pass is allowed for production proof.'
35
+ ].join('\n');
36
+ }
37
+ function ownerScopeText(node) {
38
+ return [
39
+ ...node.owner_scope.files.map((file) => `file:${file}`),
40
+ ...node.owner_scope.directories.map((dir) => `dir:${dir}`)
41
+ ].join(', ') || 'none';
42
+ }
43
+ //# sourceMappingURL=loop-worker-prompts.js.map
@@ -0,0 +1,275 @@
1
+ import path from 'node:path';
2
+ import os from 'node:os';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { runNativeAgentOrchestrator } from '../agents/agent-orchestrator.js';
5
+ import { ensureDir, nowIso, readJson, runProcess, writeJsonAtomic } from '../fsx.js';
6
+ import { loopNodeRoot } from './loop-artifacts.js';
7
+ import { buildLoopCheckerPrompt, buildLoopMakerPrompt } from './loop-worker-prompts.js';
8
+ export async function runLoopMakerWorkers(input) {
9
+ return runLoopWorkers({ ...input, phase: 'maker' });
10
+ }
11
+ export async function runLoopCheckerWorkers(input) {
12
+ return runLoopWorkers({ ...input, phase: 'checker', noMutation: true });
13
+ }
14
+ async function runLoopWorkers(input) {
15
+ if (shouldUseFixture(input))
16
+ return runLoopWorkerFixture(input);
17
+ return runLoopWorkerNative(input);
18
+ }
19
+ // `noMutation` used to force fixture mode here, which silently turned EVERY
20
+ // checker run into a deterministic fixture (checkers always pass
21
+ // noMutation: true for read-only semantics) — real model verification never
22
+ // happened. Fixture mode is now an explicit, separate test-only signal.
23
+ function shouldUseFixture(input) {
24
+ const requested = input.fixture === true || process.env.SKS_LOOP_RUNTIME_FIXTURE === '1';
25
+ if (!requested)
26
+ return false;
27
+ const allowed = loopFixtureAllowed(input);
28
+ if (!allowed.ok) {
29
+ throw new Error(`loop_fixture_runtime_forbidden:${allowed.reason}`);
30
+ }
31
+ return true;
32
+ }
33
+ async function runLoopWorkerNative(input) {
34
+ const prompt = input.phase === 'maker'
35
+ ? buildLoopMakerPrompt({ plan: input.plan, node: input.node, worktreePath: input.worktree?.path || null })
36
+ : buildLoopCheckerPrompt({ plan: input.plan, node: input.node, makerArtifacts: input.makerArtifacts || [] });
37
+ const workerCount = input.phase === 'maker' ? input.node.maker.worker_count : input.node.checker.worker_count;
38
+ const workGraph = buildLoopNarutoWorkGraph(input, workerCount);
39
+ // Root-cause-1 fix: keep the ORCHESTRATOR root on the MAIN repo (input.root), not the
40
+ // loop worktree. All zellij/right-column/slot-telemetry state derives from the orchestrator
41
+ // root, so anchoring it on input.root makes the SLOTS snapshot land under
42
+ // <main repo>/.sneakoscope/missions/<missionId>/... where the main session's anchor + slot
43
+ // renderer panes watch it (previously it landed under the worktree and went permanently stale).
44
+ // The loop worktree is still where workers cwd + write: it is threaded through the per-worker
45
+ // `worktree` opt below, which launchWorker reads as ctx.opts.worktree -> workerCwd.
46
+ const insideZellij = Boolean(process.env.SKS_ZELLIJ_SESSION_NAME || process.env.ZELLIJ);
47
+ const visiblePaneCap = Math.min(resolveLoopVisiblePaneCap(workerCount), Math.max(1, workerCount));
48
+ const zellijPlacementOpts = insideZellij ? {
49
+ workerPlacement: 'zellij-pane',
50
+ ...(process.env.SKS_ZELLIJ_SESSION_NAME ? { zellijSessionName: process.env.SKS_ZELLIJ_SESSION_NAME } : {}),
51
+ zellijVisiblePaneCap: visiblePaneCap
52
+ } : {};
53
+ const orchestrator = await runNativeAgentOrchestrator({
54
+ root: input.root,
55
+ missionId: input.plan.mission_id,
56
+ prompt,
57
+ route: '$Naruto',
58
+ backend: 'codex-sdk',
59
+ readonly: input.phase === 'checker',
60
+ workspaceWrite: input.phase === 'maker',
61
+ desiredWorkItemCount: workGraph.total_work_items,
62
+ minimumWorkItems: 1,
63
+ maxAgentCount: Math.max(1, workerCount),
64
+ targetActiveSlots: Math.max(1, workerCount),
65
+ visualLaneCount: visiblePaneCap,
66
+ narutoMode: true,
67
+ narutoWorkGraph: workGraph,
68
+ ...zellijPlacementOpts,
69
+ ...(input.worktree?.path ? {
70
+ worktree: {
71
+ id: input.worktree.id || `loop-${input.node.loop_id}-${input.phase}`,
72
+ path: input.worktree.path,
73
+ branch: input.worktree.branch || 'unknown',
74
+ main_repo_root: input.root
75
+ }
76
+ } : {}),
77
+ gitWorktreePolicy: input.worktree?.path ? {
78
+ mode: 'patch-envelope-only',
79
+ required: false,
80
+ main_repo_root: input.root,
81
+ worktree_root: input.worktree.path,
82
+ fallback_reason: null
83
+ } : null
84
+ });
85
+ return normalizeNativeResult(input, orchestrator);
86
+ }
87
+ async function normalizeNativeResult(input, result) {
88
+ const artifacts = collectArtifactPaths(result);
89
+ const changedFiles = stringArray(result?.changed_files || result?.proof?.changed_files || result?.results?.flatMap?.((row) => row?.changed_files || []));
90
+ const blockers = [
91
+ ...(result?.ok === true ? [] : ['loop_worker_native_orchestrator_not_ok']),
92
+ ...stringArray(result?.blockers || result?.proof?.blockers)
93
+ ];
94
+ const proofPath = path.join(loopNodeRoot(input.root, input.plan.mission_id, input.node.loop_id), input.phase, 'worker-runtime-result.json');
95
+ const normalized = {
96
+ schema: 'sks.loop-worker-run-result.v1',
97
+ ok: blockers.length === 0,
98
+ mission_id: input.plan.mission_id,
99
+ loop_id: input.node.loop_id,
100
+ phase: input.phase,
101
+ worker_count: input.phase === 'maker' ? input.node.maker.worker_count : input.node.checker.worker_count,
102
+ backend: 'native-agent-orchestrator',
103
+ artifacts,
104
+ patch_candidates: input.phase === 'maker' ? artifacts.filter((artifact) => artifact.includes('patch')) : [],
105
+ checker_findings: input.phase === 'checker' ? artifacts.filter((artifact) => artifact.includes('checker') || artifact.includes('finding')) : [],
106
+ changed_files: changedFiles,
107
+ blockers: [...new Set(blockers)],
108
+ runtime_proof_path: proofPath,
109
+ worker_ids: stringArray(result?.results?.map?.((row) => row?.agent_id || row?.id)),
110
+ session_ids: stringArray(result?.results?.map?.((row) => row?.session_id))
111
+ };
112
+ await writeJsonAtomic(proofPath, { ...normalized, native_result_summary: summarizeNativeResult(result), generated_at: nowIso() });
113
+ return normalized;
114
+ }
115
+ async function runLoopWorkerFixture(input) {
116
+ const dir = path.join(loopNodeRoot(input.root, input.plan.mission_id, input.node.loop_id), input.phase);
117
+ await ensureDir(dir);
118
+ const resultPath = path.join(dir, 'worker-runtime-result.json');
119
+ const childInputPath = path.join(dir, 'worker-fixture-intake.json');
120
+ await writeJsonAtomic(childInputPath, {
121
+ schema: 'sks.loop-worker-fixture-intake.v1',
122
+ root: input.root,
123
+ mission_id: input.plan.mission_id,
124
+ loop_id: input.node.loop_id,
125
+ phase: input.phase,
126
+ worker_count: input.phase === 'maker' ? input.node.maker.worker_count : input.node.checker.worker_count,
127
+ result_path: resultPath,
128
+ owner_scope: input.node.owner_scope,
129
+ maker_artifacts: input.makerArtifacts || []
130
+ });
131
+ const child = await runProcess(process.execPath, [fixtureChildEntrypoint(), childInputPath], {
132
+ cwd: input.root,
133
+ timeoutMs: input.timeoutMs || 30000,
134
+ maxOutputBytes: 64 * 1024
135
+ });
136
+ const result = await readJson(resultPath, null);
137
+ if (!result) {
138
+ return {
139
+ schema: 'sks.loop-worker-run-result.v1',
140
+ ok: false,
141
+ mission_id: input.plan.mission_id,
142
+ loop_id: input.node.loop_id,
143
+ phase: input.phase,
144
+ worker_count: 0,
145
+ backend: 'deterministic-fixture',
146
+ artifacts: [],
147
+ patch_candidates: [],
148
+ checker_findings: [],
149
+ changed_files: [],
150
+ blockers: [`loop_worker_fixture_child_missing_result:${child.code}`],
151
+ runtime_proof_path: resultPath,
152
+ worker_ids: [],
153
+ session_ids: []
154
+ };
155
+ }
156
+ return {
157
+ ...result,
158
+ ok: result.ok && child.code === 0,
159
+ blockers: [
160
+ ...result.blockers,
161
+ ...(child.code === 0 ? [] : [`loop_worker_fixture_child_exit:${child.code}`])
162
+ ]
163
+ };
164
+ }
165
+ function buildLoopNarutoWorkGraph(input, workerCount) {
166
+ const workItems = Array.from({ length: Math.max(1, workerCount) }, (_, index) => {
167
+ const id = `${input.node.loop_id}-${input.phase}-${index + 1}`;
168
+ const writeAllowed = input.phase === 'maker';
169
+ return {
170
+ id,
171
+ kind: writeAllowed ? 'code_modification' : 'verification',
172
+ title: `${input.phase} worker ${index + 1} for ${input.node.loop_id}`,
173
+ target_paths: [...input.node.owner_scope.files, ...input.node.owner_scope.directories],
174
+ readonly_paths: input.phase === 'checker' ? [...input.node.owner_scope.files, ...input.node.owner_scope.directories] : [],
175
+ write_paths: writeAllowed ? [...input.node.owner_scope.files, ...input.node.owner_scope.directories] : [],
176
+ required_role: input.phase,
177
+ write_allowed: writeAllowed,
178
+ verification_required: input.phase === 'checker',
179
+ dependencies: [],
180
+ can_run_in_parallel_with: [],
181
+ conflicts_with: [],
182
+ estimated_cost: { tokens: 8000, latency_ms: 30000, cpu_weight: 1, memory_mb: 512, gpu_weight: 0 },
183
+ lease_requirements: input.node.owner_scope.files.map((file) => ({ path: file, kind: writeAllowed ? 'write' : 'read' })),
184
+ acceptance: { requires_patch_envelope: writeAllowed, requires_verification: !writeAllowed, requires_gpt_final: input.node.risk.requires_gpt_final },
185
+ owner: input.node.loop_id,
186
+ allocation_reason: 'loop-worker-runtime',
187
+ allocation_score: 1,
188
+ allocation_hints: null,
189
+ lane: input.phase,
190
+ worktree: {
191
+ mode: input.worktree?.path ? 'patch-envelope-only' : 'git-worktree',
192
+ required: input.node.worktree.required,
193
+ allocation_required: false
194
+ }
195
+ };
196
+ });
197
+ return {
198
+ schema: 'sks.naruto-work-graph.v1',
199
+ route: '$Naruto',
200
+ requested_clones: workerCount,
201
+ total_work_items: workItems.length,
202
+ readonly: input.phase === 'checker',
203
+ write_capable: input.phase === 'maker',
204
+ work_items: workItems,
205
+ active_waves: [{ wave_id: `${input.node.loop_id}-${input.phase}`, work_item_ids: workItems.map((item) => item.id), write_paths: workItems.flatMap((item) => item.write_paths), conflict_count: 0 }],
206
+ mixed_work_kinds: [...new Set(workItems.map((item) => item.kind))],
207
+ write_allowed_count: workItems.filter((item) => item.write_allowed).length,
208
+ worktree_policy: {
209
+ mode: input.worktree?.path ? 'patch-envelope-only' : 'git-worktree',
210
+ required: input.node.worktree.required,
211
+ main_repo_root: input.root,
212
+ worktree_root: null,
213
+ fallback_reason: input.worktree?.path ? 'loop_worktree_already_allocated' : null
214
+ },
215
+ blockers: [],
216
+ ok: true
217
+ };
218
+ }
219
+ function collectArtifactPaths(result) {
220
+ return stringArray([
221
+ result?.ledger_root,
222
+ result?.proof?.artifact,
223
+ ...(Array.isArray(result?.results) ? result.results.flatMap((row) => row?.artifacts || row?.patch_queue_refs || []) : [])
224
+ ]);
225
+ }
226
+ function summarizeNativeResult(result) {
227
+ return {
228
+ ok: result?.ok === true,
229
+ status: result?.status || null,
230
+ mission_id: result?.mission_id || null,
231
+ backend: result?.backend || null,
232
+ result_count: Array.isArray(result?.results) ? result.results.length : 0,
233
+ blockers: stringArray(result?.blockers || result?.proof?.blockers).slice(0, 20)
234
+ };
235
+ }
236
+ function stringArray(value) {
237
+ if (!Array.isArray(value))
238
+ return [];
239
+ return [...new Set(value.flat().map((item) => String(item || '').trim()).filter(Boolean))];
240
+ }
241
+ // Visible pane cap for loop workers: defaults to min(4, workers) so the right
242
+ // column stays readable; SKS_ZELLIJ_VISIBLE_PANE_CAP overrides for tall
243
+ // terminals (overflow workers run headless and stay visible in SLOTS rows).
244
+ function resolveLoopVisiblePaneCap(workerCount) {
245
+ const fromEnv = Number(process.env.SKS_ZELLIJ_VISIBLE_PANE_CAP || 0);
246
+ if (Number.isFinite(fromEnv) && fromEnv >= 1)
247
+ return Math.floor(fromEnv);
248
+ return Math.min(4, Math.max(1, workerCount));
249
+ }
250
+ function fixtureChildEntrypoint() {
251
+ return fileURLToPath(new URL('../../scripts/loop-worker-fixture-child.js', import.meta.url));
252
+ }
253
+ function loopFixtureAllowed(input) {
254
+ const argv = process.argv.join(' ');
255
+ const scriptIsCheck = /(?:^|\s)(?:.*[\/\\])?(?:dist|src)[\/\\]scripts[\/\\][^\s]*(?:check|blackbox)\.(?:js|ts)(?:\s|$)/.test(argv);
256
+ const explicitTestEnv = process.env.NODE_ENV === 'test'
257
+ || process.env.SKS_TEST_RUNTIME_FIXTURE_ALLOWED === '1'
258
+ || process.env.VITEST_WORKER_ID !== undefined
259
+ || process.env.JEST_WORKER_ID !== undefined
260
+ || process.env.NODE_V8_COVERAGE !== undefined;
261
+ const tempRoot = isUnderTempRoot(input.root) && /^M-check-/.test(input.plan.mission_id);
262
+ if (scriptIsCheck)
263
+ return { ok: true, reason: 'release_check_script' };
264
+ if (explicitTestEnv)
265
+ return { ok: true, reason: 'test_environment' };
266
+ if (tempRoot)
267
+ return { ok: true, reason: 'hermetic_temp_loop_check' };
268
+ return { ok: false, reason: 'fixture_requires_test_context' };
269
+ }
270
+ function isUnderTempRoot(root) {
271
+ const normalizedRoot = path.resolve(root);
272
+ const tempRoot = path.resolve(os.tmpdir());
273
+ return normalizedRoot === tempRoot || normalizedRoot.startsWith(`${tempRoot}${path.sep}`);
274
+ }
275
+ //# sourceMappingURL=loop-worker-runtime.js.map
@@ -0,0 +1,92 @@
1
+ import path from 'node:path';
2
+ import { exists, nowIso, writeJsonAtomic } from '../fsx.js';
3
+ import { allocateWorkerWorktree } from '../git/git-worktree-manager.js';
4
+ import { gitOutputLine, runGitCommand } from '../git/git-worktree-runner.js';
5
+ import { loopNodeRoot } from './loop-artifacts.js';
6
+ export async function allocateLoopWorktree(input) {
7
+ const blockers = [];
8
+ let worktreeId = null;
9
+ let worktreePath = null;
10
+ let branch = null;
11
+ let baseRef = null;
12
+ if (input.node.worktree.required && !input.noMutation) {
13
+ const gitPresent = await exists(path.join(input.root, '.git'));
14
+ if (!gitPresent) {
15
+ blockers.push('loop_worktree_required_but_git_missing');
16
+ }
17
+ else {
18
+ const allocation = await allocateWorkerWorktree({
19
+ repoRoot: input.root,
20
+ missionId: input.plan.mission_id,
21
+ workerId: input.node.loop_id,
22
+ slotId: input.node.loop_id,
23
+ generationIndex: 1,
24
+ branchPrefix: input.node.worktree.branch_prefix
25
+ }).catch((err) => ({ ok: false, blockers: [`loop_worktree_allocate_exception:${err instanceof Error ? err.message : String(err)}`] }));
26
+ if (allocation.ok) {
27
+ worktreeId = allocation.worker_id || input.node.loop_id;
28
+ worktreePath = allocation.worktree_path || null;
29
+ branch = allocation.branch || null;
30
+ baseRef = allocation.base_ref || null;
31
+ }
32
+ else {
33
+ blockers.push(...stringArray(allocation.blockers));
34
+ }
35
+ }
36
+ }
37
+ const record = {
38
+ schema: 'sks.loop-worktree.v1',
39
+ loop_id: input.node.loop_id,
40
+ worktree_id: worktreeId,
41
+ path: worktreePath,
42
+ branch,
43
+ base_ref: baseRef,
44
+ allocated_at: nowIso(),
45
+ cleanup_policy: input.node.worktree.cleanup,
46
+ blockers
47
+ };
48
+ await writeJsonAtomic(path.join(loopNodeRoot(input.root, input.plan.mission_id, input.node.loop_id), 'worktree.json'), record);
49
+ return record;
50
+ }
51
+ export async function computeLoopDiff(input) {
52
+ const cwd = input.worktreePath || input.root;
53
+ const blockers = [];
54
+ const names = await runGitCommand(cwd, ['diff', '--name-only', 'HEAD'], { timeoutMs: 30000 }).catch(() => null);
55
+ const stat = await runGitCommand(cwd, ['diff', '--stat', 'HEAD'], { timeoutMs: 30000 }).catch(() => null);
56
+ const diff = await runGitCommand(cwd, ['diff', '--binary', '--full-index', 'HEAD'], { timeoutMs: 60000 }).catch(() => null);
57
+ if (!names?.ok)
58
+ blockers.push('loop_git_diff_name_only_failed');
59
+ if (!diff?.ok)
60
+ blockers.push('loop_git_diff_failed');
61
+ const changedFiles = [...new Set((names?.stdout || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean))];
62
+ blockers.push(...enforceLoopOwnerScope(changedFiles, input.ownerScope));
63
+ return {
64
+ changed_files: changedFiles,
65
+ patch_bytes: Buffer.byteLength(diff?.stdout || ''),
66
+ diff_stat: stat ? gitOutputLine(stat) || stat.stdout.slice(-4000) : '',
67
+ blockers: [...new Set(blockers)]
68
+ };
69
+ }
70
+ export function enforceLoopOwnerScope(changedFiles, ownerScope) {
71
+ const blockers = [];
72
+ for (const file of changedFiles) {
73
+ if (!isInOwnerScope(file, ownerScope))
74
+ blockers.push(`loop_owner_scope_violation:${file}`);
75
+ }
76
+ return blockers;
77
+ }
78
+ function isInOwnerScope(file, ownerScope) {
79
+ const normalized = normalizePath(file);
80
+ if (ownerScope.files.map(normalizePath).includes(normalized))
81
+ return true;
82
+ return ownerScope.directories.map(normalizePath).some((dir) => normalized === dir || normalized.startsWith(`${dir}/`));
83
+ }
84
+ function normalizePath(value) {
85
+ return String(value || '').replace(/\\/g, '/').replace(/^\.\/+/, '');
86
+ }
87
+ function stringArray(value) {
88
+ if (!Array.isArray(value))
89
+ return [];
90
+ return value.map((item) => String(item || '').trim()).filter(Boolean);
91
+ }
92
+ //# sourceMappingURL=loop-worktree-runtime.js.map
@@ -5,14 +5,19 @@ export function evaluateNarutoFinalizer(input = {}) {
5
5
  const blockers = [
6
6
  ...(gptFinalRequired && !gptFinalAccepted ? ['naruto_local_worker_output_needs_gpt_final_arbiter'] : [])
7
7
  ];
8
+ const finalStatus = blockers.length ? 'blocked' : input.applyPatches === true ? 'accepted' : 'draft';
9
+ const applyFinalized = finalStatus === 'accepted';
8
10
  return {
9
11
  schema: 'sks.naruto-finalizer.v1',
10
12
  local_participated: localParticipated,
11
13
  gpt_final_required: gptFinalRequired,
12
- final_status: blockers.length ? 'blocked' : input.applyPatches === true ? 'accepted' : 'draft',
14
+ final_status: finalStatus,
13
15
  final_patch_source: gptFinalRequired ? 'gpt_final_arbiter' : 'deterministic_no_local',
14
16
  blockers,
15
- ok: blockers.length === 0
17
+ run_ok: blockers.length === 0,
18
+ release_proof_allowed: applyFinalized,
19
+ apply_finalized: applyFinalized,
20
+ ok: applyFinalized
16
21
  };
17
22
  }
18
23
  //# sourceMappingURL=naruto-finalizer.js.map
@@ -0,0 +1,39 @@
1
+ import { writeJsonAtomic } from '../fsx.js';
2
+ import { loopRoot } from '../loops/loop-artifacts.js';
3
+ import { runLoopPlan } from '../loops/loop-runtime.js';
4
+ import { routeNarutoLoopWorker } from './naruto-loop-worker-router.js';
5
+ export async function runNarutoLoopMesh(input) {
6
+ const routes = input.plan.graph.nodes.flatMap((node) => [routeNarutoLoopWorker(node, 'maker'), routeNarutoLoopWorker(node, 'checker')]);
7
+ const activeWorkerBudget = splitActiveWorkerBudget(input.plan, input.parallelism);
8
+ await writeJsonAtomic(`${loopRoot(input.root, input.plan.mission_id)}/naruto-loop-worker-routes.json`, {
9
+ schema: 'sks.naruto-loop-worker-routes.v1',
10
+ mission_id: input.plan.mission_id,
11
+ active_worker_budget: activeWorkerBudget,
12
+ routes
13
+ });
14
+ return runLoopPlan({
15
+ root: input.root,
16
+ plan: input.plan,
17
+ parallelism: input.parallelism,
18
+ ...(input.dryRun === undefined ? {} : { dryRun: input.dryRun }),
19
+ ...(input.noMutation === undefined ? {} : { noMutation: input.noMutation })
20
+ });
21
+ }
22
+ export function splitActiveWorkerBudget(plan, parallelism) {
23
+ const cap = parallelism === 'safe' ? 8 : parallelism === 'extreme' ? 32 : 16;
24
+ const integrationReserved = 2;
25
+ const nonIntegration = plan.graph.nodes.filter((node) => node.route !== '$Integration');
26
+ const perLoopCap = Math.max(2, Math.floor((cap - integrationReserved) / Math.max(1, nonIntegration.length)));
27
+ const perLoop = nonIntegration.map((node) => ({
28
+ loop_id: node.loop_id,
29
+ maker_checker_workers: Math.min(perLoopCap, node.maker.worker_count + node.checker.worker_count)
30
+ }));
31
+ const used = perLoop.reduce((sum, row) => sum + row.maker_checker_workers, integrationReserved);
32
+ return {
33
+ global_active_workers: cap,
34
+ integration_reserved: integrationReserved,
35
+ per_loop: perLoop,
36
+ headroom: Math.max(0, cap - used)
37
+ };
38
+ }
39
+ //# sourceMappingURL=naruto-loop-mesh.js.map
@@ -0,0 +1,38 @@
1
+ export function routeNarutoLoopWorker(node, role) {
2
+ const domain = node.loop_id.replace(/^loop-/, '');
3
+ const roles = roleLabels(domain);
4
+ const gates = [...node.gates.triage, ...node.gates.local, ...node.gates.checker, ...node.gates.integration, ...node.gates.final];
5
+ return {
6
+ schema: 'sks.naruto-loop-worker-route.v1',
7
+ loop_id: node.loop_id,
8
+ maker_role: roles.maker,
9
+ checker_role: roles.checker,
10
+ prompt: [
11
+ `loop purpose: ${node.purpose}`,
12
+ `role: ${role === 'maker' ? roles.maker : roles.checker}`,
13
+ `owner files: ${node.owner_scope.files.join(', ') || '-'}`,
14
+ `owner directories: ${node.owner_scope.directories.join(', ') || '-'}`,
15
+ `gates: ${gates.join(', ') || '-'}`,
16
+ `state file: ${node.state_file}`,
17
+ `budget: ${JSON.stringify(node.budget)}`,
18
+ `collision policy: ${node.owner_scope.collision_policy}`,
19
+ 'Do not mutate outside owner scope.'
20
+ ].join('\n'),
21
+ allowed_files: node.owner_scope.files,
22
+ allowed_directories: node.owner_scope.directories,
23
+ gates,
24
+ mutation_outside_owner_scope_allowed: false
25
+ };
26
+ }
27
+ function roleLabels(domain) {
28
+ if (domain.includes('zellij'))
29
+ return { maker: 'zellij implementer', checker: 'zellij QA/verifier' };
30
+ if (domain.includes('release'))
31
+ return { maker: 'release optimizer', checker: 'release gate verifier' };
32
+ if (domain.includes('research'))
33
+ return { maker: 'source shard/synthesis', checker: 'final reviewer' };
34
+ if (domain.includes('codex'))
35
+ return { maker: 'capability/probe implementer', checker: 'real probe verifier' };
36
+ return { maker: `${domain} implementer`, checker: `${domain} checker` };
37
+ }
38
+ //# sourceMappingURL=naruto-loop-worker-router.js.map