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.
- package/README.md +1 -1
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/crates/sks-core/src/main.rs +1 -1
- package/dist/.sks-build-stamp.json +4 -4
- package/dist/bin/sks.js +1 -1
- package/dist/cli/install-helpers.js +6 -7
- package/dist/commands/zellij-slot-column-anchor.js +3 -1
- package/dist/commands/zellij-slot-pane.js +19 -2
- package/dist/core/agents/agent-janitor.js +10 -1
- package/dist/core/agents/agent-orchestrator.js +8 -2
- package/dist/core/agents/agent-proof-evidence.js +20 -0
- package/dist/core/agents/agent-runner-ollama.js +11 -4
- package/dist/core/agents/fast-mode-policy.js +7 -5
- package/dist/core/agents/intelligent-work-graph.js +93 -14
- package/dist/core/agents/native-cli-session-swarm.js +115 -9
- package/dist/core/agents/no-subagent-scaling-policy.js +10 -1
- package/dist/core/agents/official-subagent-helper-policy.js +62 -0
- package/dist/core/codex-app.js +0 -2
- package/dist/core/codex-control/codex-task-runner.js +9 -0
- package/dist/core/commands/fast-mode-command.js +1 -1
- package/dist/core/commands/loop-command.js +86 -13
- package/dist/core/commands/naruto-command.js +34 -21
- package/dist/core/commands/team-command.js +1 -0
- package/dist/core/commands/wiki-command.js +35 -1
- package/dist/core/fsx.js +1 -1
- package/dist/core/init.js +1 -2
- package/dist/core/locks/file-lock.js +88 -0
- package/dist/core/loops/loop-artifacts.js +54 -2
- package/dist/core/loops/loop-checkpoint.js +22 -0
- package/dist/core/loops/loop-concurrency-budget.js +55 -0
- package/dist/core/loops/loop-final-arbiter-contract.js +28 -0
- package/dist/core/loops/loop-finalizer.js +55 -7
- package/dist/core/loops/loop-fixture-policy.js +58 -0
- package/dist/core/loops/loop-gate-registry.js +96 -0
- package/dist/core/loops/loop-gate-runner.js +206 -17
- package/dist/core/loops/loop-gpt-final-arbiter.js +81 -0
- package/dist/core/loops/loop-integration-merge.js +80 -0
- package/dist/core/loops/loop-interrupt-registry.js +118 -0
- package/dist/core/loops/loop-lease.js +35 -20
- package/dist/core/loops/loop-merge-strategy.js +105 -0
- package/dist/core/loops/loop-mutation-ledger.js +103 -0
- package/dist/core/loops/loop-planner.js +36 -5
- package/dist/core/loops/loop-runtime-control.js +27 -0
- package/dist/core/loops/loop-runtime.js +254 -96
- package/dist/core/loops/loop-scheduler.js +14 -5
- package/dist/core/loops/loop-side-effect-scanner.js +91 -0
- package/dist/core/loops/loop-worker-prompts.js +43 -0
- package/dist/core/loops/loop-worker-runtime.js +281 -0
- package/dist/core/loops/loop-worktree-runtime.js +92 -0
- package/dist/core/naruto/naruto-finalizer.js +7 -2
- package/dist/core/naruto/naruto-loop-mesh.js +10 -1
- package/dist/core/proof/auto-finalize.js +3 -2
- package/dist/core/proof/proof-schema.js +6 -0
- package/dist/core/proof/proof-writer.js +5 -2
- package/dist/core/proof/root-cause-policy.js +70 -0
- package/dist/core/proof/route-adapter.js +18 -1
- package/dist/core/proof/route-finalizer.js +71 -6
- package/dist/core/proof/route-proof-gate.js +4 -0
- package/dist/core/release/release-gate-batch-runner.js +56 -10
- package/dist/core/release/release-gate-cache-v2.js +18 -3
- package/dist/core/release/release-gate-dag.js +121 -18
- package/dist/core/release/release-gate-node.js +2 -1
- package/dist/core/release/release-gate-resource-governor.js +27 -6
- package/dist/core/skills/core-skill-meta-update.js +24 -0
- package/dist/core/skills/core-skill-reflection.js +94 -0
- package/dist/core/skills/core-skill-trainer.js +103 -0
- package/dist/core/trust-kernel/completion-contract.js +4 -0
- package/dist/core/trust-kernel/route-contract.js +4 -1
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-right-column-manager.js +13 -2
- package/dist/core/zellij/zellij-slot-column-anchor.js +40 -3
- package/dist/core/zellij/zellij-slot-pane-renderer.js +36 -11
- package/dist/core/zellij/zellij-slot-telemetry.js +96 -44
- package/dist/core/zellij/zellij-worker-pane-manager.js +42 -4
- package/dist/scripts/lib/native-cli-session-swarm-check-lib.js +14 -2
- package/dist/scripts/loop-directive-check-lib.js +225 -2
- package/dist/scripts/loop-hardening-check-lib.js +289 -0
- package/dist/scripts/loop-worker-fixture-child.js +53 -0
- package/dist/scripts/naruto-real-local-gpt-final-smoke.js +10 -1
- package/dist/scripts/prepublish-release-check-or-fast.js +38 -10
- package/dist/scripts/release-check-stamp.js +29 -4
- package/dist/scripts/release-gate-existence-audit.js +1 -0
- package/package.json +32 -2
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { writeJsonAtomic } from '../fsx.js';
|
|
2
|
-
import { loopBudgetPath,
|
|
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
|
|
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:
|
|
26
|
-
maxActiveWorkers:
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: '
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
await
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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:
|
|
85
|
-
path:
|
|
86
|
-
branch:
|
|
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:
|
|
90
|
-
worker_count:
|
|
91
|
-
artifacts:
|
|
92
|
-
patch_candidates:
|
|
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:
|
|
96
|
-
worker_count:
|
|
97
|
-
artifacts:
|
|
98
|
-
|
|
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:
|
|
106
|
-
iterations:
|
|
107
|
-
changed_files: changedFiles.length,
|
|
108
|
-
patch_bytes: input.
|
|
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.
|
|
252
|
+
changed_files: input.changedFiles,
|
|
253
|
+
patch_bytes: input.patchBytes,
|
|
114
254
|
handoff: {
|
|
115
|
-
required:
|
|
116
|
-
reason:
|
|
117
|
-
artifact:
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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(
|
|
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
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
305
|
+
blockers: [],
|
|
306
|
+
runtime_proof_path: null,
|
|
307
|
+
worker_ids: [],
|
|
308
|
+
session_ids: []
|
|
154
309
|
};
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
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
|