sneakoscope 3.1.0 → 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 (60) 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/commands/zellij-slot-column-anchor.js +3 -1
  8. package/dist/commands/zellij-slot-pane.js +19 -2
  9. package/dist/core/agents/agent-janitor.js +10 -1
  10. package/dist/core/agents/agent-orchestrator.js +1 -0
  11. package/dist/core/agents/agent-runner-ollama.js +11 -4
  12. package/dist/core/agents/native-cli-session-swarm.js +69 -9
  13. package/dist/core/codex-control/codex-task-runner.js +9 -0
  14. package/dist/core/commands/loop-command.js +54 -13
  15. package/dist/core/commands/naruto-command.js +26 -17
  16. package/dist/core/commands/team-command.js +1 -0
  17. package/dist/core/fsx.js +1 -1
  18. package/dist/core/locks/file-lock.js +88 -0
  19. package/dist/core/loops/loop-artifacts.js +33 -2
  20. package/dist/core/loops/loop-checkpoint.js +22 -0
  21. package/dist/core/loops/loop-finalizer.js +33 -7
  22. package/dist/core/loops/loop-gate-registry.js +96 -0
  23. package/dist/core/loops/loop-gate-runner.js +165 -17
  24. package/dist/core/loops/loop-gpt-final-arbiter.js +61 -0
  25. package/dist/core/loops/loop-integration-merge.js +75 -0
  26. package/dist/core/loops/loop-lease.js +35 -20
  27. package/dist/core/loops/loop-planner.js +36 -5
  28. package/dist/core/loops/loop-runtime-control.js +25 -0
  29. package/dist/core/loops/loop-runtime.js +248 -93
  30. package/dist/core/loops/loop-scheduler.js +12 -3
  31. package/dist/core/loops/loop-worker-prompts.js +43 -0
  32. package/dist/core/loops/loop-worker-runtime.js +275 -0
  33. package/dist/core/loops/loop-worktree-runtime.js +92 -0
  34. package/dist/core/naruto/naruto-finalizer.js +7 -2
  35. package/dist/core/naruto/naruto-loop-mesh.js +7 -1
  36. package/dist/core/proof/proof-schema.js +6 -0
  37. package/dist/core/proof/proof-writer.js +5 -2
  38. package/dist/core/proof/root-cause-policy.js +70 -0
  39. package/dist/core/proof/route-adapter.js +18 -1
  40. package/dist/core/proof/route-proof-gate.js +4 -0
  41. package/dist/core/release/release-gate-batch-runner.js +56 -10
  42. package/dist/core/release/release-gate-cache-v2.js +18 -3
  43. package/dist/core/release/release-gate-dag.js +65 -17
  44. package/dist/core/release/release-gate-node.js +2 -1
  45. package/dist/core/release/release-gate-resource-governor.js +27 -6
  46. package/dist/core/skills/core-skill-meta-update.js +24 -0
  47. package/dist/core/skills/core-skill-reflection.js +94 -0
  48. package/dist/core/skills/core-skill-trainer.js +103 -0
  49. package/dist/core/trust-kernel/completion-contract.js +4 -0
  50. package/dist/core/trust-kernel/route-contract.js +4 -1
  51. package/dist/core/version.js +1 -1
  52. package/dist/core/zellij/zellij-right-column-manager.js +13 -2
  53. package/dist/core/zellij/zellij-slot-column-anchor.js +40 -3
  54. package/dist/core/zellij/zellij-slot-pane-renderer.js +36 -11
  55. package/dist/core/zellij/zellij-slot-telemetry.js +96 -44
  56. package/dist/core/zellij/zellij-worker-pane-manager.js +42 -4
  57. package/dist/scripts/loop-directive-check-lib.js +225 -2
  58. package/dist/scripts/loop-worker-fixture-child.js +53 -0
  59. package/dist/scripts/naruto-real-local-gpt-final-smoke.js +10 -1
  60. package/package.json +5 -2
@@ -6,6 +6,7 @@ import { inferLoopOwnerScope } from './loop-owner-inference.js';
6
6
  import { classifyLoopRisk } from './loop-risk-classifier.js';
7
7
  import { defaultLoopBudget, validateLoopPlan } from './loop-schema.js';
8
8
  export async function planLoopsFromRequest(input) {
9
+ const parallelism = input.parallelism || 'balanced';
9
10
  const maxLoops = Math.max(1, Math.min(32, input.maxLoops || 8));
10
11
  const domains = decomposeRequestIntoLoopDomains(input.request).slice(0, maxLoops);
11
12
  const actionNodes = domains.map((domain) => {
@@ -21,7 +22,8 @@ export async function planLoopsFromRequest(input) {
21
22
  dependencies: [],
22
23
  route: domain.id === 'docs' ? '$Loop' : '$Naruto',
23
24
  level: domain.id === 'docs' ? 'L1-assisted' : 'L2-action',
24
- risk
25
+ risk,
26
+ parallelism
25
27
  });
26
28
  return { ...nodeBase, gates: selectLoopGates({ node: nodeBase, changedFiles: [...ownerScope.files, ...ownerScope.directories], risk }) };
27
29
  });
@@ -36,7 +38,8 @@ export async function planLoopsFromRequest(input) {
36
38
  dependencies: actionNodes.map((node) => node.loop_id),
37
39
  route: '$Integration',
38
40
  level: 'L1-assisted',
39
- risk: integrationRisk
41
+ risk: integrationRisk,
42
+ parallelism
40
43
  });
41
44
  const integrationNode = {
42
45
  ...integrationBase,
@@ -86,8 +89,10 @@ export async function planLoopsFromRequest(input) {
86
89
  return plan;
87
90
  }
88
91
  function makeNode(input) {
92
+ const makerWorkerCount = dynamicMakerWorkerCount(input);
93
+ const checkerWorkerCount = dynamicCheckerWorkerCount(input);
89
94
  const budget = defaultLoopBudget({
90
- max_subagents: input.route === '$Integration' ? 2 : 4,
95
+ max_subagents: input.route === '$Integration' ? 2 : Math.max(4, makerWorkerCount + checkerWorkerCount + 1),
91
96
  max_changed_files: input.ownerScope.files.length ? Math.max(4, input.ownerScope.files.length + 2) : 12
92
97
  });
93
98
  return {
@@ -105,14 +110,14 @@ function makeNode(input) {
105
110
  maker: {
106
111
  route: '$Naruto',
107
112
  role: input.route === '$Integration' ? 'planner' : input.loopId.includes('docs') ? 'writer' : 'implementer',
108
- worker_count: input.route === '$Integration' ? 1 : 2,
113
+ worker_count: makerWorkerCount,
109
114
  backend_preference: ['codex-sdk', 'python-codex-sdk', 'local-llm'],
110
115
  local_draft_allowed: input.risk.level !== 'critical',
111
116
  gpt_final_required: input.risk.requires_gpt_final
112
117
  },
113
118
  checker: {
114
119
  route: input.loopId.includes('research') ? '$Research' : input.loopId.includes('docs') ? '$DFix' : '$QA-LOOP',
115
- worker_count: input.route === '$Integration' ? 1 : 1,
120
+ worker_count: checkerWorkerCount,
116
121
  fresh_session_required: true,
117
122
  stronger_model_required: input.risk.level === 'high' || input.risk.level === 'critical',
118
123
  required_before_next_iteration: input.level === 'L2-action'
@@ -133,6 +138,32 @@ function makeNode(input) {
133
138
  risk: input.risk
134
139
  };
135
140
  }
141
+ // Maker parallelism scales with the loop's owned scope instead of a flat 2:
142
+ // Naruto can fan out far wider, and a fixed count starved wide scopes while
143
+ // over-provisioning single-file loops. Risk still clamps the ceiling so
144
+ // critical work cannot stampede, and 'safe' mode keeps the old behavior.
145
+ function dynamicMakerWorkerCount(input) {
146
+ if (input.route === '$Integration')
147
+ return 1;
148
+ const scopeSize = input.ownerScope.files.length + input.ownerScope.directories.length * 3;
149
+ const modeCap = input.parallelism === 'safe' ? 2 : input.parallelism === 'extreme' ? 8 : 6;
150
+ const riskCap = input.risk.level === 'critical' ? 2 : modeCap;
151
+ const riskFloor = input.risk.level === 'high' ? 3 : 2;
152
+ const scopeScaled = Math.max(riskFloor, Math.ceil(scopeSize / 3));
153
+ return Math.max(1, Math.min(modeCap, riskCap, scopeScaled));
154
+ }
155
+ // Checker workers are read-only GPT review lanes. They scale more conservatively
156
+ // than makers, but wide/high-risk owner scopes get more than one fresh reviewer.
157
+ function dynamicCheckerWorkerCount(input) {
158
+ if (input.route === '$Integration')
159
+ return 1;
160
+ const scopeSize = input.ownerScope.files.length + input.ownerScope.directories.length * 3;
161
+ const modeCap = input.parallelism === 'safe' ? 1 : input.parallelism === 'extreme' ? 4 : 3;
162
+ const riskFloor = input.risk.level === 'high' || input.risk.level === 'critical' ? 2 : 1;
163
+ const riskCap = input.risk.level === 'critical' ? Math.min(2, modeCap) : modeCap;
164
+ const scopeScaled = Math.max(riskFloor, Math.ceil(scopeSize / 6));
165
+ return Math.max(1, Math.min(modeCap, riskCap, scopeScaled));
166
+ }
136
167
  function titleFromDomain(domainId) {
137
168
  return domainId === 'loop-general-coding' ? 'General coding loop' : `${domainId} loop`;
138
169
  }
@@ -0,0 +1,25 @@
1
+ import { readJson, writeJsonAtomic } from '../fsx.js';
2
+ import { loopKillRequestPath, loopProofPath, loopStatePath } from './loop-artifacts.js';
3
+ import { writeLoopCheckpoint } from './loop-checkpoint.js';
4
+ export async function writeLoopKillRequest(root, missionId, target) {
5
+ const request = { schema: 'sks.loop-kill-request.v1', mission_id: missionId, target, requested_at: new Date().toISOString() };
6
+ await writeJsonAtomic(loopKillRequestPath(root, missionId), request);
7
+ return request;
8
+ }
9
+ export async function shouldKillLoop(root, missionId, loopId) {
10
+ const request = await readJson(loopKillRequestPath(root, missionId), null);
11
+ return request?.target === 'all' || request?.target === loopId;
12
+ }
13
+ export async function checkpointCancelledLoop(root, node, iteration, phase) {
14
+ await writeLoopCheckpoint({
15
+ root,
16
+ mission_id: node.mission_id,
17
+ loop_id: node.loop_id,
18
+ iteration,
19
+ phase,
20
+ state_path: loopStatePath(root, node.mission_id, node.loop_id),
21
+ proof_path: loopProofPath(root, node.mission_id, node.loop_id),
22
+ resumable: true
23
+ });
24
+ }
25
+ //# sourceMappingURL=loop-runtime-control.js.map
@@ -1,15 +1,21 @@
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 { writeLoopCheckpoint } from './loop-checkpoint.js';
3
4
  import { finalizeLoopGraph } from './loop-finalizer.js';
4
5
  import { runLoopGates } from './loop-gate-runner.js';
5
6
  import { acquireLoopLease, releaseLoopLease } from './loop-lease.js';
7
+ import { checkpointCancelledLoop, shouldKillLoop } from './loop-runtime-control.js';
6
8
  import { scheduleLoopGraph } from './loop-scheduler.js';
7
9
  import { appendLoopRunLog, initialLoopState, updateLoopState, writeLoopState } from './loop-state.js';
10
+ import { runLoopCheckerWorkers, runLoopMakerWorkers } from './loop-worker-runtime.js';
11
+ import { allocateLoopWorktree, computeLoopDiff } from './loop-worktree-runtime.js';
8
12
  export async function runLoopPlan(input) {
9
13
  const started = Date.now();
10
14
  const schedule = scheduleLoopGraph(input.plan.graph.nodes, input.parallelism || 'balanced');
11
15
  const proofs = [];
12
16
  for (const batch of schedule.batches) {
17
+ if (await shouldKillLoop(input.root, input.plan.mission_id, 'all'))
18
+ break;
13
19
  const batchProofs = await Promise.all(batch.map((node) => runLoopNode({
14
20
  root: input.root,
15
21
  plan: input.plan,
@@ -37,123 +43,272 @@ export async function runLoopPlan(input) {
37
43
  export async function runLoopNode(input) {
38
44
  const started = Date.now();
39
45
  const node = input.node;
46
+ const iteration = input.iterationStart || 1;
40
47
  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;
48
+ let lease = null;
49
+ let worktree = null;
50
+ try {
51
+ await writeLoopState(input.root, initialLoopState({ missionId: node.mission_id, loopId: node.loop_id, files }));
52
+ await writeJsonAtomic(loopBudgetPath(input.root, node.mission_id, node.loop_id), node.budget);
53
+ await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_started', status: 'running', message: node.purpose });
54
+ await checkpoint(input.root, node, iteration, 'triage', false);
55
+ await updateLoopState(input.root, node.mission_id, node.loop_id, { status: 'running', iteration, current_phase: 'triage' });
56
+ await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_triage_completed', status: 'running' });
57
+ lease = await acquireLoopLease(input.root, input.plan, node);
58
+ if (lease.blockers.length)
59
+ return blockedProof(input.root, node, lease.blockers, started, 'owner_collision', 'handoff');
60
+ worktree = await allocateLoopWorktree({
61
+ root: input.root,
62
+ plan: input.plan,
63
+ node,
64
+ ...(input.noMutation === undefined ? {} : { noMutation: input.noMutation })
65
+ });
66
+ if (worktree.blockers.length)
67
+ return blockedProof(input.root, node, worktree.blockers, started, 'worktree_blocked', 'blocked', lease, worktree);
68
+ await updateLoopState(input.root, node.mission_id, node.loop_id, {
69
+ current_phase: 'maker',
70
+ acting_on: { files, worktree_id: worktree.worktree_id || lease.worktree_id, branch: worktree.branch }
71
+ });
72
+ if (await shouldCancel(input.root, node, iteration, 'maker'))
73
+ return cancelledProof(input.root, node, started, lease, worktree, 'maker');
74
+ await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_maker_started', status: 'running' });
75
+ const maker = await runLoopMakerWorkers({
76
+ root: input.root,
77
+ plan: input.plan,
78
+ node,
79
+ worktree: { id: worktree.worktree_id || lease.worktree_id, path: worktree.path, branch: worktree.branch },
80
+ ...(input.noMutation === undefined ? {} : { noMutation: input.noMutation })
81
+ });
82
+ await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_maker_completed', status: maker.ok ? 'running' : 'blocked' });
83
+ await checkpoint(input.root, node, iteration, 'maker', true);
84
+ if (!maker.ok)
85
+ return workerBlockedProof(input.root, node, maker, null, started, 'maker_failed', lease, worktree);
86
+ await updateLoopState(input.root, node.mission_id, node.loop_id, { current_phase: 'checker', last_action: 'maker_workers_completed' });
87
+ if (await shouldCancel(input.root, node, iteration, 'checker'))
88
+ return cancelledProof(input.root, node, started, lease, worktree, 'checker');
89
+ await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_checker_started', status: 'running' });
90
+ const checker = await runLoopCheckerWorkers({
91
+ root: input.root,
92
+ plan: input.plan,
93
+ node,
94
+ worktree: { id: worktree.worktree_id || lease.worktree_id, path: worktree.path, branch: worktree.branch },
95
+ // Checker is read-only by definition; fixture mode requires an explicit test-only runtime flag.
96
+ noMutation: true,
97
+ makerArtifacts: maker.artifacts
98
+ });
99
+ await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_checker_completed', status: checker.ok ? 'running' : 'blocked' });
100
+ await checkpoint(input.root, node, iteration, 'checker', true);
101
+ if (!checker.ok)
102
+ return workerBlockedProof(input.root, node, maker, checker, started, 'checker_failed', lease, worktree);
103
+ const diff = input.noMutation ? emptyDiff() : await computeLoopDiff({
104
+ root: input.root,
105
+ worktreePath: worktree.path,
106
+ ownerScope: node.owner_scope
107
+ });
108
+ const changedFiles = [...new Set([...maker.changed_files, ...diff.changed_files])];
109
+ const patchBytes = Math.max(diff.patch_bytes, ...maker.patch_candidates.map((artifact) => artifact.length), 0);
110
+ if (diff.blockers.length)
111
+ return completedOrBlockedProof({ root: input.root, node, maker, checker, gate: emptyGate(), lease, worktree, diff, changedFiles, patchBytes, started, extraBlockers: diff.blockers });
112
+ await updateLoopState(input.root, node.mission_id, node.loop_id, { current_phase: 'gates', last_checker_result: 'fresh_checker_passed' });
113
+ if (await shouldCancel(input.root, node, iteration, 'gates'))
114
+ return cancelledProof(input.root, node, started, lease, worktree, 'gates');
115
+ await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_gate_started', status: 'running' });
116
+ const gate = await runLoopGates({ root: input.root, missionId: node.mission_id, node, gates: node.gates, checkerArtifacts: checker.checker_findings });
117
+ await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: 'loop_gate_completed', status: gate.ok ? 'completed' : 'blocked' });
118
+ return completedOrBlockedProof({ root: input.root, node, maker, checker, gate, lease, worktree, diff, changedFiles, patchBytes, started, extraBlockers: [] });
51
119
  }
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 }
120
+ catch (err) {
121
+ return blockedProof(input.root, node, [`loop_runtime_exception:${err instanceof Error ? err.message : String(err)}`], started, 'runtime_exception', 'failed', lease, worktree);
122
+ }
123
+ finally {
124
+ if (lease?.status === 'active')
125
+ await releaseLoopLease(input.root, node.mission_id, node.loop_id).catch(() => undefined);
126
+ }
127
+ }
128
+ async function completedOrBlockedProof(input) {
129
+ const blockers = [
130
+ ...input.extraBlockers,
131
+ ...(input.gate.blockers || []),
132
+ ...(input.node.risk.requires_human_handoff ? ['human_handoff_required'] : [])
133
+ ];
134
+ const status = blockers.length ? (input.node.risk.requires_human_handoff ? 'handoff' : 'blocked') : 'completed';
135
+ const proof = buildProof({
136
+ node: input.node,
137
+ status,
138
+ started: input.started,
139
+ maker: input.maker,
140
+ checker: input.checker,
141
+ gate: input.gate,
142
+ lease: input.lease,
143
+ worktree: input.worktree,
144
+ changedFiles: input.changedFiles,
145
+ patchBytes: input.patchBytes,
146
+ blockers
55
147
  });
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()
148
+ await writeProofAndState(input.root, proof);
149
+ return proof;
150
+ }
151
+ async function workerBlockedProof(root, node, maker, checker, started, reason, lease, worktree) {
152
+ const blockers = [...maker.blockers, ...(checker?.blockers || []), reason];
153
+ const proof = buildProof({
154
+ node,
155
+ status: 'blocked',
156
+ started,
157
+ maker,
158
+ checker: checker || emptyWorker(node, 'checker'),
159
+ gate: emptyGate(),
160
+ lease,
161
+ worktree,
162
+ changedFiles: maker.changed_files,
163
+ patchBytes: 0,
164
+ blockers
64
165
  });
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,
166
+ await writeProofAndState(root, proof);
167
+ return proof;
168
+ }
169
+ async function cancelledProof(root, node, started, lease, worktree, phase) {
170
+ await checkpointCancelledLoop(root, node, 1, phase);
171
+ const proof = buildProof({
172
+ node,
173
+ status: 'cancelled',
174
+ started,
175
+ maker: emptyWorker(node, 'maker'),
176
+ checker: emptyWorker(node, 'checker'),
177
+ gate: emptyGate(),
178
+ lease,
179
+ worktree,
180
+ changedFiles: [],
181
+ patchBytes: 0,
182
+ blockers: [`loop_cancelled:${phase}`]
183
+ });
184
+ await writeProofAndState(root, proof);
185
+ return proof;
186
+ }
187
+ async function blockedProof(root, node, blockers, started, reason, status = 'handoff', lease, worktree) {
188
+ const proof = buildProof({
189
+ node,
80
190
  status,
81
- iterations: input.iterationStart || 1,
82
- owner_scope: node.owner_scope,
191
+ started,
192
+ maker: emptyWorker(node, 'maker'),
193
+ checker: emptyWorker(node, 'checker'),
194
+ gate: emptyGate(),
195
+ lease: lease || null,
196
+ worktree: worktree || null,
197
+ changedFiles: [],
198
+ patchBytes: 0,
199
+ blockers: [...blockers, reason]
200
+ });
201
+ await writeProofAndState(root, proof);
202
+ return proof;
203
+ }
204
+ function buildProof(input) {
205
+ const handoffRequired = input.status === 'handoff';
206
+ return {
207
+ schema: 'sks.loop-proof.v1',
208
+ mission_id: input.node.mission_id,
209
+ loop_id: input.node.loop_id,
210
+ status: input.status,
211
+ iterations: 1,
212
+ owner_scope: input.node.owner_scope,
83
213
  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
214
+ id: input.worktree?.worktree_id || input.lease?.worktree_id || null,
215
+ path: input.worktree?.path || null,
216
+ branch: input.worktree?.branch || null
87
217
  },
88
218
  maker_result: {
89
- ok: true,
90
- worker_count: node.maker.worker_count,
91
- artifacts: [patchCandidate],
92
- patch_candidates: [patchCandidate]
219
+ ok: input.maker.ok,
220
+ worker_count: input.maker.worker_count,
221
+ artifacts: input.maker.artifacts,
222
+ patch_candidates: input.maker.patch_candidates,
223
+ backend: input.maker.backend,
224
+ changed_files: input.maker.changed_files,
225
+ runtime_proof_path: input.maker.runtime_proof_path
93
226
  },
94
227
  checker_result: {
95
- ok: true,
96
- worker_count: node.checker.worker_count,
97
- artifacts: ['fresh-checker-session'],
98
- blockers: []
228
+ ok: input.checker.ok,
229
+ worker_count: input.checker.worker_count,
230
+ artifacts: input.checker.artifacts,
231
+ checker_findings: input.checker.checker_findings,
232
+ blockers: input.checker.blockers,
233
+ backend: input.checker.backend,
234
+ fresh_session: input.checker.session_ids.every((session) => !input.maker.session_ids.includes(session)),
235
+ runtime_proof_path: input.checker.runtime_proof_path
99
236
  },
100
- gate_result: gate,
237
+ gate_result: input.gate,
101
238
  budget: {
102
239
  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)
240
+ wall_ms: Math.max(1, Date.now() - input.started),
241
+ model_calls: input.node.route === '$Integration' ? 1 : 2,
242
+ subagents: input.maker.worker_count + input.checker.worker_count,
243
+ iterations: 1,
244
+ changed_files: input.changedFiles.length,
245
+ patch_bytes: input.patchBytes
109
246
  },
110
- max: node.budget
247
+ max: input.node.budget
111
248
  },
112
- changed_files: changedFiles,
113
- patch_bytes: input.noMutation ? 0 : Math.min(node.budget.max_patch_bytes, JSON.stringify(node.owner_scope).length),
249
+ changed_files: input.changedFiles,
250
+ patch_bytes: input.patchBytes,
114
251
  handoff: {
115
- required: status === 'handoff',
116
- reason: status === 'handoff' ? blockers.join(',') : null,
117
- artifact: status === 'handoff' ? `${node.loop_id}/handoff.md` : null
252
+ required: handoffRequired,
253
+ reason: handoffRequired ? input.blockers.join(',') : null,
254
+ artifact: handoffRequired ? `${input.node.loop_id}/handoff.md` : null
118
255
  },
119
- blockers
256
+ blockers: [...new Set(input.blockers)]
120
257
  };
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,
258
+ }
259
+ async function writeProofAndState(root, proof) {
260
+ await writeJsonAtomic(loopProofPath(root, proof.mission_id, proof.loop_id), proof);
261
+ await updateLoopState(root, proof.mission_id, proof.loop_id, {
262
+ status: proof.status,
263
+ current_phase: proof.status === 'completed' ? 'finalizer' : 'handoff',
264
+ last_gate_result: proof.gate_result.ok ? 'passed' : 'blocked',
265
+ blockers: proof.blockers,
127
266
  handoff: proof.handoff,
128
267
  budget_used: proof.budget.used
129
268
  });
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;
269
+ await appendLoopRunLog(root, proof.mission_id, proof.loop_id, { event_type: proof.status === 'completed' ? 'loop_completed' : 'loop_blocked', status: proof.status });
133
270
  }
134
- async function blockedProof(root, node, blockers, started, reason) {
135
- const proof = {
136
- schema: 'sks.loop-proof.v1',
271
+ async function shouldCancel(root, node, iteration, phase) {
272
+ if (!(await shouldKillLoop(root, node.mission_id, node.loop_id)))
273
+ return false;
274
+ await checkpointCancelledLoop(root, node, iteration, phase);
275
+ return true;
276
+ }
277
+ async function checkpoint(root, node, iteration, phase, resumable) {
278
+ await writeLoopCheckpoint({
279
+ root,
137
280
  mission_id: node.mission_id,
138
281
  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
- },
282
+ iteration,
283
+ phase,
284
+ state_path: loopStatePath(root, node.mission_id, node.loop_id),
285
+ proof_path: loopProofPath(root, node.mission_id, node.loop_id),
286
+ resumable
287
+ });
288
+ }
289
+ function emptyWorker(node, phase) {
290
+ return {
291
+ schema: 'sks.loop-worker-run-result.v1',
292
+ ok: false,
293
+ mission_id: node.mission_id,
294
+ loop_id: node.loop_id,
295
+ phase,
296
+ worker_count: 0,
297
+ backend: 'mock',
298
+ artifacts: [],
299
+ patch_candidates: [],
300
+ checker_findings: [],
150
301
  changed_files: [],
151
- patch_bytes: 0,
152
- handoff: { required: true, reason, artifact: `${node.loop_id}/handoff.md` },
153
- blockers
302
+ blockers: [],
303
+ runtime_proof_path: null,
304
+ worker_ids: [],
305
+ session_ids: []
154
306
  };
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;
307
+ }
308
+ function emptyGate() {
309
+ return { ok: false, selected_gates: [], passed_gates: [], failed_gates: [], skipped_gates: [], blockers: [] };
310
+ }
311
+ function emptyDiff() {
312
+ return { changed_files: [], patch_bytes: 0, diff_stat: '', blockers: [] };
158
313
  }
159
314
  //# sourceMappingURL=loop-runtime.js.map
@@ -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,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