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
@@ -1,15 +1,24 @@
1
1
  import { writeJsonAtomic } from '../fsx.js';
2
- import { loopBudgetPath, loopPatchPath, loopProofPath } from './loop-artifacts.js';
2
+ import { loopBudgetPath, loopProofPath, loopStatePath } from './loop-artifacts.js';
3
+ import { computeLoopConcurrencyBudget, writeLoopConcurrencyBudget } from './loop-concurrency-budget.js';
4
+ import { writeLoopCheckpoint } from './loop-checkpoint.js';
3
5
  import { finalizeLoopGraph } from './loop-finalizer.js';
4
6
  import { runLoopGates } from './loop-gate-runner.js';
5
7
  import { acquireLoopLease, releaseLoopLease } from './loop-lease.js';
8
+ import { checkpointCancelledLoop, shouldKillLoop } from './loop-runtime-control.js';
6
9
  import { scheduleLoopGraph } from './loop-scheduler.js';
7
10
  import { appendLoopRunLog, initialLoopState, updateLoopState, writeLoopState } from './loop-state.js';
11
+ import { runLoopCheckerWorkers, runLoopMakerWorkers } from './loop-worker-runtime.js';
12
+ import { allocateLoopWorktree, computeLoopDiff } from './loop-worktree-runtime.js';
8
13
  export async function runLoopPlan(input) {
9
14
  const started = Date.now();
10
- const schedule = scheduleLoopGraph(input.plan.graph.nodes, input.parallelism || 'balanced');
15
+ const concurrencyBudget = computeLoopConcurrencyBudget({ plan: input.plan, parallelism: input.parallelism || 'balanced' });
16
+ await writeLoopConcurrencyBudget(input.root, concurrencyBudget);
17
+ const schedule = scheduleLoopGraph(input.plan.graph.nodes, input.parallelism || 'balanced', concurrencyBudget);
11
18
  const proofs = [];
12
19
  for (const batch of schedule.batches) {
20
+ if (await shouldKillLoop(input.root, input.plan.mission_id, 'all'))
21
+ break;
13
22
  const batchProofs = await Promise.all(batch.map((node) => runLoopNode({
14
23
  root: input.root,
15
24
  plan: input.plan,
@@ -22,8 +31,8 @@ export async function runLoopPlan(input) {
22
31
  root: input.root,
23
32
  plan: input.plan,
24
33
  proofs,
25
- maxActiveLoops: schedule.max_active_loops,
26
- maxActiveWorkers: Math.max(1, proofs.reduce((sum, proof) => sum + proof.maker_result.worker_count + proof.checker_result.worker_count, 0)),
34
+ maxActiveLoops: concurrencyBudget.max_active_loops,
35
+ maxActiveWorkers: concurrencyBudget.max_active_workers,
27
36
  wallMs: Math.max(1, Date.now() - started)
28
37
  });
29
38
  return {
@@ -37,123 +46,272 @@ export async function runLoopPlan(input) {
37
46
  export async function runLoopNode(input) {
38
47
  const started = Date.now();
39
48
  const node = input.node;
49
+ const iteration = input.iterationStart || 1;
40
50
  const files = [...node.owner_scope.files, ...node.owner_scope.directories];
41
- await writeLoopState(input.root, initialLoopState({ missionId: node.mission_id, loopId: node.loop_id, files }));
42
- await writeJsonAtomic(loopBudgetPath(input.root, node.mission_id, node.loop_id), node.budget);
43
- await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_started', status: 'running', message: node.purpose });
44
- await updateLoopState(input.root, node.mission_id, node.loop_id, { status: 'running', iteration: input.iterationStart || 1, current_phase: 'triage' });
45
- await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_triage_completed', status: 'running' });
46
- const lease = await acquireLoopLease(input.root, input.plan, node);
47
- if (lease.blockers.length) {
48
- const proof = await blockedProof(input.root, node, lease.blockers, started, 'owner_collision');
49
- await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_handoff_required', status: proof.status, message: lease.blockers.join(', ') });
50
- return proof;
51
+ let lease = null;
52
+ let worktree = null;
53
+ try {
54
+ await writeLoopState(input.root, initialLoopState({ missionId: node.mission_id, loopId: node.loop_id, files }));
55
+ await writeJsonAtomic(loopBudgetPath(input.root, node.mission_id, node.loop_id), node.budget);
56
+ await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_started', status: 'running', message: node.purpose });
57
+ await checkpoint(input.root, node, iteration, 'triage', false);
58
+ await updateLoopState(input.root, node.mission_id, node.loop_id, { status: 'running', iteration, current_phase: 'triage' });
59
+ await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_triage_completed', status: 'running' });
60
+ lease = await acquireLoopLease(input.root, input.plan, node);
61
+ if (lease.blockers.length)
62
+ return blockedProof(input.root, node, lease.blockers, started, 'owner_collision', 'handoff');
63
+ worktree = await allocateLoopWorktree({
64
+ root: input.root,
65
+ plan: input.plan,
66
+ node,
67
+ ...(input.noMutation === undefined ? {} : { noMutation: input.noMutation })
68
+ });
69
+ if (worktree.blockers.length)
70
+ return blockedProof(input.root, node, worktree.blockers, started, 'worktree_blocked', 'blocked', lease, worktree);
71
+ await updateLoopState(input.root, node.mission_id, node.loop_id, {
72
+ current_phase: 'maker',
73
+ acting_on: { files, worktree_id: worktree.worktree_id || lease.worktree_id, branch: worktree.branch }
74
+ });
75
+ if (await shouldCancel(input.root, node, iteration, 'maker'))
76
+ return cancelledProof(input.root, node, started, lease, worktree, 'maker');
77
+ await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_maker_started', status: 'running' });
78
+ const maker = await runLoopMakerWorkers({
79
+ root: input.root,
80
+ plan: input.plan,
81
+ node,
82
+ worktree: { id: worktree.worktree_id || lease.worktree_id, path: worktree.path, branch: worktree.branch },
83
+ ...(input.noMutation === undefined ? {} : { noMutation: input.noMutation })
84
+ });
85
+ await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_maker_completed', status: maker.ok ? 'running' : 'blocked' });
86
+ await checkpoint(input.root, node, iteration, 'maker', true);
87
+ if (!maker.ok)
88
+ return workerBlockedProof(input.root, node, maker, null, started, 'maker_failed', lease, worktree);
89
+ await updateLoopState(input.root, node.mission_id, node.loop_id, { current_phase: 'checker', last_action: 'maker_workers_completed' });
90
+ if (await shouldCancel(input.root, node, iteration, 'checker'))
91
+ return cancelledProof(input.root, node, started, lease, worktree, 'checker');
92
+ await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_checker_started', status: 'running' });
93
+ const checker = await runLoopCheckerWorkers({
94
+ root: input.root,
95
+ plan: input.plan,
96
+ node,
97
+ worktree: { id: worktree.worktree_id || lease.worktree_id, path: worktree.path, branch: worktree.branch },
98
+ // Checker is read-only by definition; fixture mode requires an explicit test-only runtime flag.
99
+ noMutation: true,
100
+ makerArtifacts: maker.artifacts
101
+ });
102
+ await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_checker_completed', status: checker.ok ? 'running' : 'blocked' });
103
+ await checkpoint(input.root, node, iteration, 'checker', true);
104
+ if (!checker.ok)
105
+ return workerBlockedProof(input.root, node, maker, checker, started, 'checker_failed', lease, worktree);
106
+ const diff = input.noMutation ? emptyDiff() : await computeLoopDiff({
107
+ root: input.root,
108
+ worktreePath: worktree.path,
109
+ ownerScope: node.owner_scope
110
+ });
111
+ const changedFiles = [...new Set([...maker.changed_files, ...diff.changed_files])];
112
+ const patchBytes = Math.max(diff.patch_bytes, ...maker.patch_candidates.map((artifact) => artifact.length), 0);
113
+ if (diff.blockers.length)
114
+ return completedOrBlockedProof({ root: input.root, node, maker, checker, gate: emptyGate(), lease, worktree, diff, changedFiles, patchBytes, started, extraBlockers: diff.blockers });
115
+ await updateLoopState(input.root, node.mission_id, node.loop_id, { current_phase: 'gates', last_checker_result: 'fresh_checker_passed' });
116
+ if (await shouldCancel(input.root, node, iteration, 'gates'))
117
+ return cancelledProof(input.root, node, started, lease, worktree, 'gates');
118
+ await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_gate_started', status: 'running' });
119
+ const gate = await runLoopGates({ root: input.root, missionId: node.mission_id, node, gates: node.gates, checkerArtifacts: checker.checker_findings });
120
+ await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_gate_completed', status: gate.ok ? 'completed' : 'blocked' });
121
+ return completedOrBlockedProof({ root: input.root, node, maker, checker, gate, lease, worktree, diff, changedFiles, patchBytes, started, extraBlockers: [] });
51
122
  }
52
- await updateLoopState(input.root, node.mission_id, node.loop_id, {
53
- current_phase: 'maker',
54
- acting_on: { files, worktree_id: lease.worktree_id, branch: node.worktree.required ? `${node.worktree.branch_prefix}/${node.loop_id}` : null }
123
+ catch (err) {
124
+ return blockedProof(input.root, node, [`loop_runtime_exception:${err instanceof Error ? err.message : String(err)}`], started, 'runtime_exception', 'failed', lease, worktree);
125
+ }
126
+ finally {
127
+ if (lease?.status === 'active')
128
+ await releaseLoopLease(input.root, node.mission_id, node.loop_id).catch(() => undefined);
129
+ }
130
+ }
131
+ async function completedOrBlockedProof(input) {
132
+ const blockers = [
133
+ ...input.extraBlockers,
134
+ ...(input.gate.blockers || []),
135
+ ...(input.node.risk.requires_human_handoff ? ['human_handoff_required'] : [])
136
+ ];
137
+ const status = blockers.length ? (input.node.risk.requires_human_handoff ? 'handoff' : 'blocked') : 'completed';
138
+ const proof = buildProof({
139
+ node: input.node,
140
+ status,
141
+ started: input.started,
142
+ maker: input.maker,
143
+ checker: input.checker,
144
+ gate: input.gate,
145
+ lease: input.lease,
146
+ worktree: input.worktree,
147
+ changedFiles: input.changedFiles,
148
+ patchBytes: input.patchBytes,
149
+ blockers
55
150
  });
56
- await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_maker_started', status: 'running' });
57
- const patchCandidate = loopPatchPath(input.root, node.mission_id, node.loop_id, 'maker-patch-candidate');
58
- await writeJsonAtomic(patchCandidate, {
59
- schema: 'sks.loop-patch-candidate.v1',
60
- loop_id: node.loop_id,
61
- no_mutation: Boolean(input.noMutation),
62
- owner_scope: node.owner_scope,
63
- generated_at: new Date().toISOString()
151
+ await writeProofAndState(input.root, proof);
152
+ return proof;
153
+ }
154
+ async function workerBlockedProof(root, node, maker, checker, started, reason, lease, worktree) {
155
+ const blockers = [...maker.blockers, ...(checker?.blockers || []), reason];
156
+ const proof = buildProof({
157
+ node,
158
+ status: 'blocked',
159
+ started,
160
+ maker,
161
+ checker: checker || emptyWorker(node, 'checker'),
162
+ gate: emptyGate(),
163
+ lease,
164
+ worktree,
165
+ changedFiles: maker.changed_files,
166
+ patchBytes: 0,
167
+ blockers
64
168
  });
65
- await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_maker_completed', status: 'running' });
66
- await updateLoopState(input.root, node.mission_id, node.loop_id, { current_phase: 'checker', last_action: 'maker_patch_candidate_recorded' });
67
- await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_checker_started', status: 'running' });
68
- await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_checker_completed', status: 'running' });
69
- await updateLoopState(input.root, node.mission_id, node.loop_id, { current_phase: 'gates', last_checker_result: 'fresh_checker_passed' });
70
- await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_gate_started', status: 'running' });
71
- const gate = await runLoopGates({ root: input.root, missionId: node.mission_id, node, gates: node.gates });
72
- await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_gate_completed', status: gate.ok ? 'completed' : 'blocked' });
73
- const changedFiles = input.noMutation ? [] : files.filter((file) => !file.startsWith('.sneakoscope'));
74
- const blockers = [...gate.blockers, ...(node.risk.requires_human_handoff ? ['human_handoff_required'] : [])];
75
- const status = blockers.length ? (node.risk.requires_human_handoff ? 'handoff' : 'blocked') : 'completed';
76
- const proof = {
77
- schema: 'sks.loop-proof.v1',
78
- mission_id: node.mission_id,
79
- loop_id: node.loop_id,
169
+ await writeProofAndState(root, proof);
170
+ return proof;
171
+ }
172
+ async function cancelledProof(root, node, started, lease, worktree, phase) {
173
+ await checkpointCancelledLoop(root, node, 1, phase);
174
+ const proof = buildProof({
175
+ node,
176
+ status: 'cancelled',
177
+ started,
178
+ maker: emptyWorker(node, 'maker'),
179
+ checker: emptyWorker(node, 'checker'),
180
+ gate: emptyGate(),
181
+ lease,
182
+ worktree,
183
+ changedFiles: [],
184
+ patchBytes: 0,
185
+ blockers: [`loop_cancelled:${phase}`]
186
+ });
187
+ await writeProofAndState(root, proof);
188
+ return proof;
189
+ }
190
+ async function blockedProof(root, node, blockers, started, reason, status = 'handoff', lease, worktree) {
191
+ const proof = buildProof({
192
+ node,
80
193
  status,
81
- iterations: input.iterationStart || 1,
82
- owner_scope: node.owner_scope,
194
+ started,
195
+ maker: emptyWorker(node, 'maker'),
196
+ checker: emptyWorker(node, 'checker'),
197
+ gate: emptyGate(),
198
+ lease: lease || null,
199
+ worktree: worktree || null,
200
+ changedFiles: [],
201
+ patchBytes: 0,
202
+ blockers: [...blockers, reason]
203
+ });
204
+ await writeProofAndState(root, proof);
205
+ return proof;
206
+ }
207
+ function buildProof(input) {
208
+ const handoffRequired = input.status === 'handoff';
209
+ return {
210
+ schema: 'sks.loop-proof.v1',
211
+ mission_id: input.node.mission_id,
212
+ loop_id: input.node.loop_id,
213
+ status: input.status,
214
+ iterations: 1,
215
+ owner_scope: input.node.owner_scope,
83
216
  worktree: {
84
- id: lease.worktree_id,
85
- path: node.worktree.required ? `.sneakoscope/worktrees/${node.loop_id}` : null,
86
- branch: node.worktree.required ? `${node.worktree.branch_prefix}/${node.loop_id}` : null
217
+ id: input.worktree?.worktree_id || input.lease?.worktree_id || null,
218
+ path: input.worktree?.path || null,
219
+ branch: input.worktree?.branch || null
87
220
  },
88
221
  maker_result: {
89
- ok: true,
90
- worker_count: node.maker.worker_count,
91
- artifacts: [patchCandidate],
92
- patch_candidates: [patchCandidate]
222
+ ok: input.maker.ok,
223
+ worker_count: input.maker.worker_count,
224
+ artifacts: input.maker.artifacts,
225
+ patch_candidates: input.maker.patch_candidates,
226
+ backend: input.maker.backend,
227
+ changed_files: input.maker.changed_files,
228
+ runtime_proof_path: input.maker.runtime_proof_path
93
229
  },
94
230
  checker_result: {
95
- ok: true,
96
- worker_count: node.checker.worker_count,
97
- artifacts: ['fresh-checker-session'],
98
- blockers: []
231
+ ok: input.checker.ok,
232
+ worker_count: input.checker.worker_count,
233
+ artifacts: input.checker.artifacts,
234
+ checker_findings: input.checker.checker_findings,
235
+ blockers: input.checker.blockers,
236
+ backend: input.checker.backend,
237
+ fresh_session: input.checker.session_ids.every((session) => !input.maker.session_ids.includes(session)),
238
+ runtime_proof_path: input.checker.runtime_proof_path
99
239
  },
100
- gate_result: gate,
240
+ gate_result: input.gate,
101
241
  budget: {
102
242
  used: {
103
- wall_ms: Math.max(1, Date.now() - started),
104
- model_calls: node.route === '$Integration' ? 1 : 2,
105
- subagents: node.maker.worker_count + node.checker.worker_count,
106
- iterations: input.iterationStart || 1,
107
- changed_files: changedFiles.length,
108
- patch_bytes: input.noMutation ? 0 : Math.min(node.budget.max_patch_bytes, JSON.stringify(node.owner_scope).length)
243
+ wall_ms: Math.max(1, Date.now() - input.started),
244
+ model_calls: input.node.route === '$Integration' ? 1 : 2,
245
+ subagents: input.maker.worker_count + input.checker.worker_count,
246
+ iterations: 1,
247
+ changed_files: input.changedFiles.length,
248
+ patch_bytes: input.patchBytes
109
249
  },
110
- max: node.budget
250
+ max: input.node.budget
111
251
  },
112
- changed_files: changedFiles,
113
- patch_bytes: input.noMutation ? 0 : Math.min(node.budget.max_patch_bytes, JSON.stringify(node.owner_scope).length),
252
+ changed_files: input.changedFiles,
253
+ patch_bytes: input.patchBytes,
114
254
  handoff: {
115
- required: status === 'handoff',
116
- reason: status === 'handoff' ? blockers.join(',') : null,
117
- artifact: status === 'handoff' ? `${node.loop_id}/handoff.md` : null
255
+ required: handoffRequired,
256
+ reason: handoffRequired ? input.blockers.join(',') : null,
257
+ artifact: handoffRequired ? `${input.node.loop_id}/handoff.md` : null
118
258
  },
119
- blockers
259
+ blockers: [...new Set(input.blockers)]
120
260
  };
121
- await writeJsonAtomic(loopProofPath(input.root, node.mission_id, node.loop_id), proof);
122
- await updateLoopState(input.root, node.mission_id, node.loop_id, {
123
- status,
124
- current_phase: status === 'completed' ? 'finalizer' : 'handoff',
125
- last_gate_result: gate.ok ? 'passed' : 'blocked',
126
- blockers,
261
+ }
262
+ async function writeProofAndState(root, proof) {
263
+ await writeJsonAtomic(loopProofPath(root, proof.mission_id, proof.loop_id), proof);
264
+ await updateLoopState(root, proof.mission_id, proof.loop_id, {
265
+ status: proof.status,
266
+ current_phase: proof.status === 'completed' ? 'finalizer' : 'handoff',
267
+ last_gate_result: proof.gate_result.ok ? 'passed' : 'blocked',
268
+ blockers: proof.blockers,
127
269
  handoff: proof.handoff,
128
270
  budget_used: proof.budget.used
129
271
  });
130
- await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: status === 'completed' ? 'loop_completed' : 'loop_blocked', status });
131
- await releaseLoopLease(input.root, node.mission_id, node.loop_id);
132
- return proof;
272
+ await appendLoopRunLog(root, proof.mission_id, proof.loop_id, { event_type: proof.status === 'completed' ? 'loop_completed' : 'loop_blocked', status: proof.status });
133
273
  }
134
- async function blockedProof(root, node, blockers, started, reason) {
135
- const proof = {
136
- schema: 'sks.loop-proof.v1',
274
+ async function shouldCancel(root, node, iteration, phase) {
275
+ if (!(await shouldKillLoop(root, node.mission_id, node.loop_id)))
276
+ return false;
277
+ await checkpointCancelledLoop(root, node, iteration, phase);
278
+ return true;
279
+ }
280
+ async function checkpoint(root, node, iteration, phase, resumable) {
281
+ await writeLoopCheckpoint({
282
+ root,
137
283
  mission_id: node.mission_id,
138
284
  loop_id: node.loop_id,
139
- status: 'handoff',
140
- iterations: 1,
141
- owner_scope: node.owner_scope,
142
- worktree: { id: null, path: null, branch: null },
143
- maker_result: { ok: false, worker_count: 0, artifacts: [], patch_candidates: [] },
144
- checker_result: { ok: false, worker_count: 0, artifacts: [], blockers },
145
- gate_result: { ok: false, selected_gates: [], passed_gates: [], failed_gates: [], skipped_gates: [] },
146
- budget: {
147
- used: { wall_ms: Math.max(1, Date.now() - started), model_calls: 0, subagents: 0, iterations: 1, changed_files: 0, patch_bytes: 0 },
148
- max: node.budget
149
- },
285
+ iteration,
286
+ phase,
287
+ state_path: loopStatePath(root, node.mission_id, node.loop_id),
288
+ proof_path: loopProofPath(root, node.mission_id, node.loop_id),
289
+ resumable
290
+ });
291
+ }
292
+ function emptyWorker(node, phase) {
293
+ return {
294
+ schema: 'sks.loop-worker-run-result.v1',
295
+ ok: false,
296
+ mission_id: node.mission_id,
297
+ loop_id: node.loop_id,
298
+ phase,
299
+ worker_count: 0,
300
+ backend: 'mock',
301
+ artifacts: [],
302
+ patch_candidates: [],
303
+ checker_findings: [],
150
304
  changed_files: [],
151
- patch_bytes: 0,
152
- handoff: { required: true, reason, artifact: `${node.loop_id}/handoff.md` },
153
- blockers
305
+ blockers: [],
306
+ runtime_proof_path: null,
307
+ worker_ids: [],
308
+ session_ids: []
154
309
  };
155
- await writeJsonAtomic(loopProofPath(root, node.mission_id, node.loop_id), proof);
156
- await updateLoopState(root, node.mission_id, node.loop_id, { status: 'handoff', current_phase: 'handoff', blockers, handoff: proof.handoff });
157
- return proof;
310
+ }
311
+ function emptyGate() {
312
+ return { ok: false, selected_gates: [], passed_gates: [], failed_gates: [], skipped_gates: [], blockers: [] };
313
+ }
314
+ function emptyDiff() {
315
+ return { changed_files: [], patch_bytes: 0, diff_stat: '', blockers: [] };
158
316
  }
159
317
  //# sourceMappingURL=loop-runtime.js.map
@@ -1,9 +1,9 @@
1
1
  import os from 'node:os';
2
- export function scheduleLoopGraph(nodes, parallelism = 'balanced') {
2
+ export function scheduleLoopGraph(nodes, parallelism = 'balanced', budget) {
3
3
  const pending = new Map(nodes.map((node) => [node.loop_id, node]));
4
4
  const completed = new Set();
5
5
  const batches = [];
6
- const maxParallel = maxConcurrentLoops(nodes, parallelism);
6
+ const maxParallel = budget?.max_active_loops || maxConcurrentLoops(nodes, parallelism);
7
7
  const blockers = [];
8
8
  while (pending.size) {
9
9
  const ready = [...pending.values()].filter((node) => node.dependencies.every((dep) => completed.has(dep)));
@@ -26,9 +26,18 @@ export function scheduleLoopGraph(nodes, parallelism = 'balanced') {
26
26
  export function maxConcurrentLoops(nodes, parallelism = 'balanced') {
27
27
  const cores = Math.max(1, os.cpus().length || 1);
28
28
  const base = parallelism === 'safe' ? 2 : parallelism === 'extreme' ? Math.min(16, cores) : Math.min(8, cores);
29
- return nodes.some((node) => node.risk.level === 'critical' || node.risk.level === 'high') && parallelism !== 'extreme'
30
- ? Math.max(1, Math.min(base, 3))
31
- : Math.max(1, base);
29
+ return Math.max(1, Math.min(base, riskAwareLoopCap(nodes, parallelism, cores)));
30
+ }
31
+ function riskAwareLoopCap(nodes, parallelism, cores) {
32
+ if (parallelism === 'extreme')
33
+ return Math.min(16, cores);
34
+ const hasCritical = nodes.some((node) => node.risk.level === 'critical');
35
+ const hasHigh = nodes.some((node) => node.risk.level === 'high');
36
+ if (hasCritical)
37
+ return parallelism === 'safe' ? 1 : Math.max(2, Math.floor(cores / 4));
38
+ if (hasHigh)
39
+ return parallelism === 'safe' ? 2 : Math.max(4, Math.floor(cores / 2));
40
+ return parallelism === 'safe' ? 2 : Math.min(8, cores);
32
41
  }
33
42
  export function graphProofFromLoopProofs(input) {
34
43
  const selected = [...new Set(input.proofs.flatMap((proof) => proof.gate_result.selected_gates))];
@@ -0,0 +1,91 @@
1
+ import { readJson, writeJsonAtomic } from '../fsx.js';
2
+ import { loopGatePath, loopMutationLedgerPath, loopSideEffectReportPath } from './loop-artifacts.js';
3
+ import { mutationLedgerFromLoopProofs, readLoopMutationLedger } from './loop-mutation-ledger.js';
4
+ import { enforceLoopOwnerScope } from './loop-worktree-runtime.js';
5
+ export async function buildLoopSideEffectReport(input) {
6
+ await mutationLedgerFromLoopProofs({
7
+ root: input.root,
8
+ missionId: input.missionId,
9
+ proofs: input.proofs,
10
+ integrationMerge: input.integrationMerge || null
11
+ });
12
+ const ledger = await readLoopMutationLedger(input.root, input.missionId);
13
+ const changedFiles = [...new Set([
14
+ ...input.proofs.flatMap((proof) => proof.changed_files),
15
+ ...(input.integrationMerge?.changed_files || []),
16
+ ...ledger.map((event) => event.file_path).filter((file) => Boolean(file))
17
+ ])];
18
+ const integrationLoopIds = new Set(input.proofs.filter((proof) => proof.loop_id.includes('integration')).map((proof) => proof.loop_id));
19
+ const ownerScopeViolations = collectOwnerScopeViolations(input.proofs, ledger);
20
+ const unexpectedPackageChanges = changedFiles.filter((file) => isPackageOrReleaseFile(file) && !changedByIntegration(input.proofs, file, integrationLoopIds));
21
+ const globalConfigMutations = changedFiles.filter(isGlobalConfigPath);
22
+ const gateSideEffects = await collectGateSideEffects(input.root, input.missionId, input.proofs);
23
+ const networkOrInstallSideEffects = gateSideEffects.filter((item) => /(install|network|npm|pnpm|yarn|curl|brew)/i.test(item));
24
+ const blockers = [
25
+ ...ownerScopeViolations.map((file) => `loop_side_effect_owner_scope_violation:${file}`),
26
+ ...unexpectedPackageChanges.map((file) => `loop_side_effect_unexpected_package_change:${file}`),
27
+ ...globalConfigMutations.map((file) => `loop_side_effect_global_config_mutation:${file}`),
28
+ ...networkOrInstallSideEffects.map((item) => `loop_side_effect_network_or_install:${item}`),
29
+ ...gateSideEffects.filter((item) => item.includes('gate_side_effect_not_hermetic')).map((item) => `loop_side_effect_gate:${item}`)
30
+ ];
31
+ const report = {
32
+ schema: 'sks.loop-side-effect-report.v1',
33
+ ok: blockers.length === 0,
34
+ mission_id: input.missionId,
35
+ changed_files: changedFiles,
36
+ owner_scope_violations: [...new Set(ownerScopeViolations)],
37
+ unexpected_package_changes: [...new Set(unexpectedPackageChanges)],
38
+ global_config_mutations: [...new Set(globalConfigMutations)],
39
+ network_or_install_side_effects: [...new Set(networkOrInstallSideEffects)],
40
+ gate_side_effects: [...new Set(gateSideEffects)],
41
+ mutation_ledger_path: `.sneakoscope/missions/${input.missionId}/loops/mutation-ledger.jsonl`,
42
+ blockers: [...new Set(blockers)]
43
+ };
44
+ await writeJsonAtomic(loopSideEffectReportPath(input.root, input.missionId), { ...report, generated_at: new Date().toISOString() });
45
+ return report;
46
+ }
47
+ function collectOwnerScopeViolations(proofs, ledger) {
48
+ const fromLedger = ledger
49
+ .filter((event) => event.event_type === 'owner_scope_violation' || event.allowed_by_owner_scope === false)
50
+ .map((event) => event.file_path)
51
+ .filter((file) => Boolean(file));
52
+ const fromProofs = proofs.flatMap((proof) => proof.changed_files.filter((file) => enforceLoopOwnerScope([file], proof.owner_scope).length > 0));
53
+ return [...fromLedger, ...fromProofs];
54
+ }
55
+ async function collectGateSideEffects(root, missionId, proofs) {
56
+ const results = [];
57
+ for (const proof of proofs) {
58
+ for (const gateId of proof.gate_result.selected_gates || []) {
59
+ const artifact = await readJson(loopGatePath(root, missionId, proof.loop_id, gateId), null);
60
+ const sideEffect = String(artifact?.side_effect || artifact?.definition_side_effect || '');
61
+ if (proof.loop_id !== 'loop-integration' && sideEffect === 'mutation') {
62
+ results.push(`gate_side_effect_not_hermetic:${proof.loop_id}:${gateId}`);
63
+ }
64
+ if (Array.isArray(artifact?.side_effects)) {
65
+ results.push(...artifact.side_effects.map((value) => `${proof.loop_id}:${gateId}:${String(value)}`));
66
+ }
67
+ }
68
+ }
69
+ return results;
70
+ }
71
+ function changedByIntegration(proofs, file, integrationLoopIds) {
72
+ return proofs.some((proof) => integrationLoopIds.has(proof.loop_id) && proof.changed_files.includes(file));
73
+ }
74
+ function isPackageOrReleaseFile(file) {
75
+ return ['package.json', 'package-lock.json', 'release-gates.v2.json'].includes(normalize(file));
76
+ }
77
+ function isGlobalConfigPath(file) {
78
+ const normalized = normalize(file);
79
+ return normalized.startsWith('.codex/')
80
+ || normalized.startsWith('.agents/')
81
+ || normalized.startsWith('.sneakoscope/policy')
82
+ || normalized.includes('/.codex/')
83
+ || normalized.includes('/.agents/');
84
+ }
85
+ function normalize(file) {
86
+ return String(file || '').replace(/\\/g, '/').replace(/^\.\/+/, '');
87
+ }
88
+ export function loopSideEffectLedgerPath(root, missionId) {
89
+ return loopMutationLedgerPath(root, missionId);
90
+ }
91
+ //# sourceMappingURL=loop-side-effect-scanner.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