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,170 @@
1
+ import { writeJsonAtomic } from '../fsx.js';
2
+ import { decomposeRequestIntoLoopDomains } from './loop-decomposer.js';
3
+ import { loopPlanPath, loopRunLogPath, loopStatePath } from './loop-artifacts.js';
4
+ import { selectLoopGates } from './loop-gate-selector.js';
5
+ import { inferLoopOwnerScope } from './loop-owner-inference.js';
6
+ import { classifyLoopRisk } from './loop-risk-classifier.js';
7
+ import { defaultLoopBudget, validateLoopPlan } from './loop-schema.js';
8
+ export async function planLoopsFromRequest(input) {
9
+ const parallelism = input.parallelism || 'balanced';
10
+ const maxLoops = Math.max(1, Math.min(32, input.maxLoops || 8));
11
+ const domains = decomposeRequestIntoLoopDomains(input.request).slice(0, maxLoops);
12
+ const actionNodes = domains.map((domain) => {
13
+ const loopId = `loop-${domain.id}`;
14
+ const ownerScope = inferLoopOwnerScope({ domain });
15
+ const risk = classifyLoopRisk({ loop_id: loopId, owner_scope: ownerScope, level: 'L2-action' });
16
+ const nodeBase = makeNode({
17
+ missionId: input.missionId,
18
+ loopId,
19
+ title: titleFromDomain(domain.id),
20
+ purpose: `Execute ${domain.id} slice for: ${input.request}`,
21
+ ownerScope,
22
+ dependencies: [],
23
+ route: domain.id === 'docs' ? '$Loop' : '$Naruto',
24
+ level: domain.id === 'docs' ? 'L1-assisted' : 'L2-action',
25
+ risk,
26
+ parallelism
27
+ });
28
+ return { ...nodeBase, gates: selectLoopGates({ node: nodeBase, changedFiles: [...ownerScope.files, ...ownerScope.directories], risk }) };
29
+ });
30
+ const integrationOwner = inferLoopOwnerScope({ domain: { id: 'integration', dirs: [], files: [], gates: ['release:dag-full-coverage'] }, integration: true });
31
+ const integrationRisk = classifyLoopRisk({ loop_id: 'loop-integration', owner_scope: integrationOwner, level: 'L1-assisted' });
32
+ const integrationBase = makeNode({
33
+ missionId: input.missionId,
34
+ loopId: 'loop-integration',
35
+ title: 'Integration loop finalizer',
36
+ purpose: 'Merge loop proofs, run integration gates, and require GPT final arbitration when source mutation exists.',
37
+ ownerScope: integrationOwner,
38
+ dependencies: actionNodes.map((node) => node.loop_id),
39
+ route: '$Integration',
40
+ level: 'L1-assisted',
41
+ risk: integrationRisk,
42
+ parallelism
43
+ });
44
+ const integrationNode = {
45
+ ...integrationBase,
46
+ gates: selectLoopGates({
47
+ node: integrationBase,
48
+ changedFiles: ['package.json', 'release-gates.v2.json', 'CHANGELOG.md'],
49
+ risk: integrationRisk,
50
+ packageScriptsChanged: ['loop:runtime'],
51
+ releaseGateIdsChanged: ['release:dag-full-coverage']
52
+ })
53
+ };
54
+ const nodes = [...actionNodes, integrationNode];
55
+ const plan = {
56
+ schema: 'sks.loop-plan.v1',
57
+ mission_id: input.missionId,
58
+ request: input.request,
59
+ generated_at: new Date().toISOString(),
60
+ planner: {
61
+ route: '$Loop',
62
+ model_policy: input.mode === 'codex-assisted' ? 'codex-sdk' : 'deterministic',
63
+ confidence: actionNodes.length ? 'high' : 'medium'
64
+ },
65
+ graph: {
66
+ nodes,
67
+ edges: actionNodes.map((node) => ({ from: node.loop_id, to: integrationNode.loop_id, reason: 'integration_after_loop_proof' }))
68
+ },
69
+ global_budget: defaultLoopBudget({
70
+ max_iterations: Math.max(...nodes.map((node) => node.budget.max_iterations)),
71
+ max_subagents: nodes.reduce((sum, node) => sum + node.budget.max_subagents, 0)
72
+ }),
73
+ safety: {
74
+ no_unrequested_fallback_code: true,
75
+ require_owner_lease: true,
76
+ require_checker_for_action: true,
77
+ require_gpt_final_for_source_mutation: true
78
+ },
79
+ integration_loop_id: integrationNode.loop_id,
80
+ compatibility: {
81
+ goal_compat_artifact: input.sourceCommand === 'goal' ? `.sneakoscope/missions/${input.missionId}/goal-compat.json` : null,
82
+ source_command: input.sourceCommand
83
+ },
84
+ blockers: []
85
+ };
86
+ const validation = validateLoopPlan(plan);
87
+ plan.blockers = validation.blockers;
88
+ await writeJsonAtomic(loopPlanPath(input.root, input.missionId), plan);
89
+ return plan;
90
+ }
91
+ function makeNode(input) {
92
+ const makerWorkerCount = dynamicMakerWorkerCount(input);
93
+ const checkerWorkerCount = dynamicCheckerWorkerCount(input);
94
+ const budget = defaultLoopBudget({
95
+ max_subagents: input.route === '$Integration' ? 2 : Math.max(4, makerWorkerCount + checkerWorkerCount + 1),
96
+ max_changed_files: input.ownerScope.files.length ? Math.max(4, input.ownerScope.files.length + 2) : 12
97
+ });
98
+ return {
99
+ schema: 'sks.loop-node.v1',
100
+ loop_id: input.loopId,
101
+ mission_id: input.missionId,
102
+ title: input.title,
103
+ purpose: input.purpose,
104
+ level: input.level,
105
+ route: input.route,
106
+ owner_scope: input.ownerScope,
107
+ state_file: loopStatePath('', input.missionId, input.loopId).replace(/^\/?/, ''),
108
+ run_log_file: loopRunLogPath('', input.missionId, input.loopId).replace(/^\/?/, ''),
109
+ budget,
110
+ maker: {
111
+ route: '$Naruto',
112
+ role: input.route === '$Integration' ? 'planner' : input.loopId.includes('docs') ? 'writer' : 'implementer',
113
+ worker_count: makerWorkerCount,
114
+ backend_preference: ['codex-sdk', 'python-codex-sdk', 'local-llm'],
115
+ local_draft_allowed: input.risk.level !== 'critical',
116
+ gpt_final_required: input.risk.requires_gpt_final
117
+ },
118
+ checker: {
119
+ route: input.loopId.includes('research') ? '$Research' : input.loopId.includes('docs') ? '$DFix' : '$QA-LOOP',
120
+ worker_count: checkerWorkerCount,
121
+ fresh_session_required: true,
122
+ stronger_model_required: input.risk.level === 'high' || input.risk.level === 'critical',
123
+ required_before_next_iteration: input.level === 'L2-action'
124
+ },
125
+ gates: { triage: [], local: [], checker: [], integration: [], final: [] },
126
+ dependencies: input.dependencies,
127
+ handoff_policy: {
128
+ allow_handoff: true,
129
+ reasons: input.risk.requires_human_handoff ? ['critical_risk_requires_handoff'] : [],
130
+ artifact: null
131
+ },
132
+ worktree: {
133
+ required: input.risk.requires_worktree,
134
+ mode: input.risk.requires_worktree ? 'new-worktree' : 'none',
135
+ branch_prefix: `sks/loop/${input.missionId}`,
136
+ cleanup: input.risk.level === 'low' ? 'on-success' : 'keep-on-failure'
137
+ },
138
+ risk: input.risk
139
+ };
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
+ }
167
+ function titleFromDomain(domainId) {
168
+ return domainId === 'loop-general-coding' ? 'General coding loop' : `${domainId} loop`;
169
+ }
170
+ //# sourceMappingURL=loop-planner.js.map
@@ -0,0 +1,10 @@
1
+ export function renderLoopProofSummary(proof) {
2
+ return [
3
+ `Loop graph: ${proof.ok ? 'passed' : 'blocked'}`,
4
+ `Loops: ${proof.total_loops} total / ${proof.completed_loops} done / ${proof.handoff_loops} handoff`,
5
+ `Parallelism: ${proof.parallelism.max_active_loops} active loops / ${proof.parallelism.max_active_workers} max workers / ${proof.parallelism.speedup_ratio}x speedup`,
6
+ `Gates: ${proof.gates.selected.length} selected / ${proof.gates.passed.length} passed`,
7
+ `Blocked: ${proof.blockers.length ? proof.blockers.join(', ') : 'none'}`
8
+ ].join('\n');
9
+ }
10
+ //# sourceMappingURL=loop-proof-summary.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=loop-proof.js.map
@@ -0,0 +1,42 @@
1
+ export function classifyLoopRisk(node) {
2
+ const scope = [
3
+ ...node.owner_scope.files,
4
+ ...node.owner_scope.directories,
5
+ ...node.owner_scope.package_scripts,
6
+ ...node.owner_scope.release_gate_ids
7
+ ].join(' ').toLowerCase();
8
+ const reasons = [];
9
+ let level = 'low';
10
+ if (/(db|mad-db|mcp|token|auth|postinstall|publish|global config)/.test(scope)) {
11
+ level = 'critical';
12
+ reasons.push('critical_scope');
13
+ }
14
+ else if (/(release-gates|worktree|scheduler|zellij|codex-control|agent|native-swarm)/.test(scope)) {
15
+ level = 'high';
16
+ reasons.push('runtime_or_scheduler_scope');
17
+ }
18
+ else if (/(qa-loop|research|image|docs)/.test(scope)) {
19
+ level = 'medium';
20
+ reasons.push('domain_scope');
21
+ }
22
+ else {
23
+ reasons.push('bounded_scope');
24
+ }
25
+ const requiresHuman = level === 'critical';
26
+ return {
27
+ level,
28
+ reasons,
29
+ requires_worktree: level === 'medium' || level === 'high' || level === 'critical',
30
+ requires_gpt_final: level !== 'low',
31
+ requires_human_handoff: requiresHuman
32
+ };
33
+ }
34
+ export function loopLevelAllowedUnattended(node) {
35
+ return node.level === 'L3-unattended'
36
+ && (node.risk.level === 'low' || node.risk.level === 'medium')
37
+ && node.owner_scope.exclusive
38
+ && node.budget.max_changed_files <= 8
39
+ && node.gates.local.length > 0
40
+ && !node.risk.requires_human_handoff;
41
+ }
42
+ //# sourceMappingURL=loop-risk-classifier.js.map
@@ -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
@@ -0,0 +1,314 @@
1
+ import { writeJsonAtomic } from '../fsx.js';
2
+ import { loopBudgetPath, loopProofPath, loopStatePath } from './loop-artifacts.js';
3
+ import { writeLoopCheckpoint } from './loop-checkpoint.js';
4
+ import { finalizeLoopGraph } from './loop-finalizer.js';
5
+ import { runLoopGates } from './loop-gate-runner.js';
6
+ import { acquireLoopLease, releaseLoopLease } from './loop-lease.js';
7
+ import { checkpointCancelledLoop, shouldKillLoop } from './loop-runtime-control.js';
8
+ import { scheduleLoopGraph } from './loop-scheduler.js';
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';
12
+ export async function runLoopPlan(input) {
13
+ const started = Date.now();
14
+ const schedule = scheduleLoopGraph(input.plan.graph.nodes, input.parallelism || 'balanced');
15
+ const proofs = [];
16
+ for (const batch of schedule.batches) {
17
+ if (await shouldKillLoop(input.root, input.plan.mission_id, 'all'))
18
+ break;
19
+ const batchProofs = await Promise.all(batch.map((node) => runLoopNode({
20
+ root: input.root,
21
+ plan: input.plan,
22
+ node,
23
+ noMutation: Boolean(input.noMutation || input.dryRun)
24
+ })));
25
+ proofs.push(...batchProofs);
26
+ }
27
+ const graphProof = await finalizeLoopGraph({
28
+ root: input.root,
29
+ plan: input.plan,
30
+ proofs,
31
+ maxActiveLoops: schedule.max_active_loops,
32
+ maxActiveWorkers: Math.max(1, proofs.reduce((sum, proof) => sum + proof.maker_result.worker_count + proof.checker_result.worker_count, 0)),
33
+ wallMs: Math.max(1, Date.now() - started)
34
+ });
35
+ return {
36
+ ok: schedule.ok && graphProof.ok,
37
+ mission_id: input.plan.mission_id,
38
+ proofs,
39
+ graph_proof: graphProof,
40
+ blockers: [...schedule.blockers, ...graphProof.blockers]
41
+ };
42
+ }
43
+ export async function runLoopNode(input) {
44
+ const started = Date.now();
45
+ const node = input.node;
46
+ const iteration = input.iterationStart || 1;
47
+ const files = [...node.owner_scope.files, ...node.owner_scope.directories];
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: [] });
119
+ }
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
147
+ });
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
165
+ });
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,
190
+ status,
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,
213
+ worktree: {
214
+ id: input.worktree?.worktree_id || input.lease?.worktree_id || null,
215
+ path: input.worktree?.path || null,
216
+ branch: input.worktree?.branch || null
217
+ },
218
+ maker_result: {
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
226
+ },
227
+ checker_result: {
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
236
+ },
237
+ gate_result: input.gate,
238
+ budget: {
239
+ used: {
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
246
+ },
247
+ max: input.node.budget
248
+ },
249
+ changed_files: input.changedFiles,
250
+ patch_bytes: input.patchBytes,
251
+ handoff: {
252
+ required: handoffRequired,
253
+ reason: handoffRequired ? input.blockers.join(',') : null,
254
+ artifact: handoffRequired ? `${input.node.loop_id}/handoff.md` : null
255
+ },
256
+ blockers: [...new Set(input.blockers)]
257
+ };
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,
266
+ handoff: proof.handoff,
267
+ budget_used: proof.budget.used
268
+ });
269
+ await appendLoopRunLog(root, proof.mission_id, proof.loop_id, { event_type: proof.status === 'completed' ? 'loop_completed' : 'loop_blocked', status: proof.status });
270
+ }
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,
280
+ mission_id: node.mission_id,
281
+ loop_id: node.loop_id,
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: [],
301
+ changed_files: [],
302
+ blockers: [],
303
+ runtime_proof_path: null,
304
+ worker_ids: [],
305
+ session_ids: []
306
+ };
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: [] };
313
+ }
314
+ //# sourceMappingURL=loop-runtime.js.map
@@ -0,0 +1,69 @@
1
+ import os from 'node:os';
2
+ export function scheduleLoopGraph(nodes, parallelism = 'balanced') {
3
+ const pending = new Map(nodes.map((node) => [node.loop_id, node]));
4
+ const completed = new Set();
5
+ const batches = [];
6
+ const maxParallel = maxConcurrentLoops(nodes, parallelism);
7
+ const blockers = [];
8
+ while (pending.size) {
9
+ const ready = [...pending.values()].filter((node) => node.dependencies.every((dep) => completed.has(dep)));
10
+ if (!ready.length) {
11
+ blockers.push(`loop_dependency_cycle:${[...pending.keys()].join(',')}`);
12
+ break;
13
+ }
14
+ const batch = ready.slice(0, maxParallel);
15
+ batches.push(batch);
16
+ for (const node of batch) {
17
+ pending.delete(node.loop_id);
18
+ if (node.route !== '$Integration')
19
+ completed.add(node.loop_id);
20
+ else
21
+ completed.add(node.loop_id);
22
+ }
23
+ }
24
+ return { ok: blockers.length === 0, batches, max_active_loops: Math.max(0, ...batches.map((batch) => batch.length)), blockers };
25
+ }
26
+ export function maxConcurrentLoops(nodes, parallelism = 'balanced') {
27
+ const cores = Math.max(1, os.cpus().length || 1);
28
+ const base = parallelism === 'safe' ? 2 : parallelism === 'extreme' ? Math.min(16, cores) : Math.min(8, cores);
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);
41
+ }
42
+ export function graphProofFromLoopProofs(input) {
43
+ const selected = [...new Set(input.proofs.flatMap((proof) => proof.gate_result.selected_gates))];
44
+ const passed = [...new Set(input.proofs.flatMap((proof) => proof.gate_result.passed_gates))];
45
+ const failed = [...new Set(input.proofs.flatMap((proof) => proof.gate_result.failed_gates))];
46
+ const skipped = [...new Set(input.proofs.flatMap((proof) => proof.gate_result.skipped_gates))];
47
+ const blockers = [...new Set(input.proofs.flatMap((proof) => proof.blockers))];
48
+ const sequential = Math.max(input.wallMs, input.proofs.length * Math.max(1, Math.floor(input.wallMs / Math.max(1, input.maxActiveLoops))));
49
+ return {
50
+ schema: 'sks.loop-graph-proof.v1',
51
+ mission_id: input.missionId,
52
+ ok: blockers.length === 0 && failed.length === 0,
53
+ total_loops: input.proofs.length,
54
+ completed_loops: input.proofs.filter((proof) => proof.status === 'completed').length,
55
+ blocked_loops: input.proofs.filter((proof) => proof.status === 'blocked').length,
56
+ failed_loops: input.proofs.filter((proof) => proof.status === 'failed').length,
57
+ handoff_loops: input.proofs.filter((proof) => proof.status === 'handoff').length,
58
+ parallelism: {
59
+ max_active_loops: input.maxActiveLoops,
60
+ max_active_workers: input.maxActiveWorkers,
61
+ wall_ms: input.wallMs,
62
+ sequential_estimate_ms: sequential,
63
+ speedup_ratio: Number((sequential / Math.max(1, input.wallMs)).toFixed(2))
64
+ },
65
+ gates: { selected, passed, failed, skipped },
66
+ blockers
67
+ };
68
+ }
69
+ //# sourceMappingURL=loop-scheduler.js.map