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.
- 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/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 +1 -0
- package/dist/core/agents/agent-runner-ollama.js +11 -4
- package/dist/core/agents/native-cli-session-swarm.js +69 -9
- package/dist/core/codex-control/codex-task-runner.js +9 -0
- package/dist/core/commands/loop-command.js +54 -13
- package/dist/core/commands/naruto-command.js +26 -17
- package/dist/core/commands/team-command.js +1 -0
- package/dist/core/fsx.js +1 -1
- package/dist/core/locks/file-lock.js +88 -0
- package/dist/core/loops/loop-artifacts.js +33 -2
- package/dist/core/loops/loop-checkpoint.js +22 -0
- package/dist/core/loops/loop-finalizer.js +33 -7
- package/dist/core/loops/loop-gate-registry.js +96 -0
- package/dist/core/loops/loop-gate-runner.js +165 -17
- package/dist/core/loops/loop-gpt-final-arbiter.js +61 -0
- package/dist/core/loops/loop-integration-merge.js +75 -0
- package/dist/core/loops/loop-lease.js +35 -20
- package/dist/core/loops/loop-planner.js +36 -5
- package/dist/core/loops/loop-runtime-control.js +25 -0
- package/dist/core/loops/loop-runtime.js +248 -93
- package/dist/core/loops/loop-scheduler.js +12 -3
- package/dist/core/loops/loop-worker-prompts.js +43 -0
- package/dist/core/loops/loop-worker-runtime.js +275 -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 +7 -1
- 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-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 +65 -17
- 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/loop-directive-check-lib.js +225 -2
- package/dist/scripts/loop-worker-fixture-child.js +53 -0
- package/dist/scripts/naruto-real-local-gpt-final-smoke.js +10 -1
- 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:
|
|
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:
|
|
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,
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
await appendLoopRunLog(input.root, node.mission_id, node.loop_id, { event_type: '
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
await
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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:
|
|
85
|
-
path:
|
|
86
|
-
branch:
|
|
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:
|
|
90
|
-
worker_count:
|
|
91
|
-
artifacts:
|
|
92
|
-
patch_candidates:
|
|
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:
|
|
96
|
-
worker_count:
|
|
97
|
-
artifacts:
|
|
98
|
-
|
|
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:
|
|
106
|
-
iterations:
|
|
107
|
-
changed_files: changedFiles.length,
|
|
108
|
-
patch_bytes: input.
|
|
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.
|
|
249
|
+
changed_files: input.changedFiles,
|
|
250
|
+
patch_bytes: input.patchBytes,
|
|
114
251
|
handoff: {
|
|
115
|
-
required:
|
|
116
|
-
reason:
|
|
117
|
-
artifact:
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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(
|
|
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
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
302
|
+
blockers: [],
|
|
303
|
+
runtime_proof_path: null,
|
|
304
|
+
worker_ids: [],
|
|
305
|
+
session_ids: []
|
|
154
306
|
};
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return
|
|
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
|
|
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,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
|