sneakoscope 3.1.1 → 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/core/agents/agent-orchestrator.js +7 -2
- package/dist/core/agents/agent-proof-evidence.js +20 -0
- 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 +46 -0
- 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/commands/fast-mode-command.js +1 -1
- package/dist/core/commands/loop-command.js +35 -3
- package/dist/core/commands/naruto-command.js +10 -6
- 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/loops/loop-artifacts.js +21 -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 +25 -3
- package/dist/core/loops/loop-fixture-policy.js +58 -0
- package/dist/core/loops/loop-gate-runner.js +48 -7
- package/dist/core/loops/loop-gpt-final-arbiter.js +26 -6
- package/dist/core/loops/loop-integration-merge.js +20 -15
- package/dist/core/loops/loop-interrupt-registry.js +118 -0
- 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-runtime-control.js +2 -0
- package/dist/core/loops/loop-runtime.js +6 -3
- package/dist/core/loops/loop-scheduler.js +2 -2
- package/dist/core/loops/loop-side-effect-scanner.js +91 -0
- package/dist/core/loops/loop-worker-runtime.js +35 -29
- package/dist/core/naruto/naruto-loop-mesh.js +3 -0
- package/dist/core/proof/auto-finalize.js +3 -2
- package/dist/core/proof/route-finalizer.js +71 -6
- package/dist/core/release/release-gate-dag.js +56 -1
- package/dist/core/version.js +1 -1
- package/dist/scripts/lib/native-cli-session-swarm-check-lib.js +14 -2
- package/dist/scripts/loop-directive-check-lib.js +1 -1
- package/dist/scripts/loop-hardening-check-lib.js +289 -0
- 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 +28 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { nowIso, writeJsonAtomic } from '../fsx.js';
|
|
3
|
-
import { runGitCommand } from '../git/git-worktree-runner.js';
|
|
4
3
|
import { guardedWriteFile, guardContextForRoute } from '../safety/mutation-guard.js';
|
|
5
4
|
import { createRequestedScopeContract } from '../safety/requested-scope-contract.js';
|
|
6
5
|
import { loopIntegrationMergePath } from './loop-artifacts.js';
|
|
6
|
+
import { mergeSingleLoopWorktree } from './loop-merge-strategy.js';
|
|
7
7
|
export async function mergeLoopWorktrees(input) {
|
|
8
8
|
const completed = input.proofs.filter((proof) => proof.status === 'completed' && proof.loop_id !== input.plan.integration_loop_id);
|
|
9
9
|
const blockers = [];
|
|
@@ -11,6 +11,7 @@ export async function mergeLoopWorktrees(input) {
|
|
|
11
11
|
const conflictLoops = new Set();
|
|
12
12
|
const changedFiles = new Set();
|
|
13
13
|
const owners = new Map();
|
|
14
|
+
const mergeAttempts = {};
|
|
14
15
|
for (const proof of completed) {
|
|
15
16
|
for (const file of proof.changed_files) {
|
|
16
17
|
const previous = owners.get(file);
|
|
@@ -29,23 +30,16 @@ export async function mergeLoopWorktrees(input) {
|
|
|
29
30
|
const worktreePath = proof.worktree.path;
|
|
30
31
|
if (!worktreePath)
|
|
31
32
|
continue;
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
const merge = await mergeSingleLoopWorktree({ root: input.root, proof, worktreePath, allowBranchMerge: true });
|
|
34
|
+
mergeAttempts[proof.loop_id] = merge;
|
|
35
|
+
if (!merge.ok) {
|
|
36
|
+
blockers.push(...merge.blockers, `loop_integration_merge_conflict:${proof.loop_id}`);
|
|
35
37
|
conflictLoops.add(proof.loop_id);
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
if (!diff.stdout.trim())
|
|
39
|
-
continue;
|
|
40
|
-
const apply = await runGitCommand(input.root, ['apply', '--whitespace=nowarn', '-'], { input: diff.stdout, timeoutMs: 60000 }).catch(() => null);
|
|
41
|
-
if (!apply?.ok) {
|
|
42
|
-
blockers.push(`loop_integration_apply_conflict:${proof.loop_id}`);
|
|
43
|
-
conflictLoops.add(proof.loop_id);
|
|
44
|
-
await writeHandoff(input.root, proof.loop_id, apply?.stderr_tail || apply?.stdout_tail || 'git apply failed');
|
|
38
|
+
await writeHandoff(input.root, proof.loop_id, merge.blockers.join('\n') || 'loop merge strategy failed');
|
|
45
39
|
continue;
|
|
46
40
|
}
|
|
47
41
|
appliedLoops.push(proof.loop_id);
|
|
48
|
-
for (const file of
|
|
42
|
+
for (const file of merge.changed_files)
|
|
49
43
|
changedFiles.add(file);
|
|
50
44
|
}
|
|
51
45
|
}
|
|
@@ -55,11 +49,22 @@ export async function mergeLoopWorktrees(input) {
|
|
|
55
49
|
applied_loops: appliedLoops,
|
|
56
50
|
conflict_loops: [...conflictLoops],
|
|
57
51
|
changed_files: [...changedFiles],
|
|
58
|
-
blockers: [...new Set(blockers)]
|
|
52
|
+
blockers: [...new Set(blockers)],
|
|
53
|
+
merge_attempts: mergeAttempts,
|
|
54
|
+
strategy_summary: summarizeStrategies(Object.values(mergeAttempts))
|
|
59
55
|
};
|
|
60
56
|
await writeJsonAtomic(loopIntegrationMergePath(input.root, input.plan.mission_id), { ...result, generated_at: nowIso() });
|
|
61
57
|
return result;
|
|
62
58
|
}
|
|
59
|
+
function summarizeStrategies(results) {
|
|
60
|
+
return {
|
|
61
|
+
apply_count: results.filter((row) => row.selected_strategy === 'apply').length,
|
|
62
|
+
apply_3way_count: results.filter((row) => row.selected_strategy === 'apply-3way').length,
|
|
63
|
+
cherry_pick_count: results.filter((row) => row.selected_strategy === 'cherry-pick').length,
|
|
64
|
+
merge_no_commit_count: results.filter((row) => row.selected_strategy === 'merge-no-commit').length,
|
|
65
|
+
handoff_count: results.filter((row) => row.selected_strategy === 'handoff' || !row.ok).length
|
|
66
|
+
};
|
|
67
|
+
}
|
|
63
68
|
async function writeHandoff(root, loopId, detail) {
|
|
64
69
|
const contract = createRequestedScopeContract({
|
|
65
70
|
route: '$Loop',
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { appendJsonl, readText, writeJsonAtomic } from '../fsx.js';
|
|
3
|
+
import { guardContextForRoute, guardedProcessKill } from '../safety/mutation-guard.js';
|
|
4
|
+
import { createRequestedScopeContract } from '../safety/requested-scope-contract.js';
|
|
5
|
+
import { loopActiveWorkerHandlesPath, loopInterruptResultPath } from './loop-artifacts.js';
|
|
6
|
+
export async function registerLoopActiveWorker(root, handle) {
|
|
7
|
+
const row = {
|
|
8
|
+
schema: 'sks.loop-active-worker-handle.v1',
|
|
9
|
+
mission_id: handle.mission_id,
|
|
10
|
+
loop_id: handle.loop_id,
|
|
11
|
+
phase: handle.phase,
|
|
12
|
+
worker_id: handle.worker_id,
|
|
13
|
+
session_id: handle.session_id,
|
|
14
|
+
pid: handle.pid,
|
|
15
|
+
started_at: handle.started_at || new Date().toISOString(),
|
|
16
|
+
interrupt_supported: handle.interrupt_supported,
|
|
17
|
+
status: handle.status || 'running'
|
|
18
|
+
};
|
|
19
|
+
await appendJsonl(loopActiveWorkerHandlesPath(root, handle.mission_id), row);
|
|
20
|
+
return row;
|
|
21
|
+
}
|
|
22
|
+
export async function markLoopWorkerInterrupted(root, missionId, workerId, status = 'interrupted') {
|
|
23
|
+
const handles = await readLoopActiveWorkers(root, missionId);
|
|
24
|
+
await appendJsonl(loopActiveWorkerHandlesPath(root, missionId), {
|
|
25
|
+
...(handles.find((handle) => handle.worker_id === workerId) || {
|
|
26
|
+
schema: 'sks.loop-active-worker-handle.v1',
|
|
27
|
+
mission_id: missionId,
|
|
28
|
+
loop_id: 'unknown',
|
|
29
|
+
phase: 'maker',
|
|
30
|
+
session_id: null,
|
|
31
|
+
pid: null,
|
|
32
|
+
started_at: new Date().toISOString(),
|
|
33
|
+
interrupt_supported: false
|
|
34
|
+
}),
|
|
35
|
+
worker_id: workerId,
|
|
36
|
+
status
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export async function readLoopActiveWorkers(root, missionId) {
|
|
40
|
+
const text = await readText(loopActiveWorkerHandlesPath(root, missionId), '');
|
|
41
|
+
const byWorker = new Map();
|
|
42
|
+
for (const line of String(text).split(/\r?\n/).map((row) => row.trim()).filter(Boolean)) {
|
|
43
|
+
try {
|
|
44
|
+
const row = JSON.parse(line);
|
|
45
|
+
if (row?.schema === 'sks.loop-active-worker-handle.v1' && row.worker_id)
|
|
46
|
+
byWorker.set(row.worker_id, row);
|
|
47
|
+
}
|
|
48
|
+
catch { }
|
|
49
|
+
}
|
|
50
|
+
return [...byWorker.values()];
|
|
51
|
+
}
|
|
52
|
+
export async function interruptLoopWorkers(input) {
|
|
53
|
+
const handles = (await readLoopActiveWorkers(input.root, input.missionId))
|
|
54
|
+
.filter((handle) => handle.status === 'running' && (input.target === 'all' || handle.loop_id === input.target || handle.worker_id === input.target));
|
|
55
|
+
const killContract = createRequestedScopeContract({
|
|
56
|
+
route: 'loop:interrupt-registry',
|
|
57
|
+
userRequest: 'Terminate only registered loop worker processes for an explicit loop interrupt request.',
|
|
58
|
+
projectRoot: input.root,
|
|
59
|
+
overrides: { codex_app_process: true }
|
|
60
|
+
});
|
|
61
|
+
const killGuard = guardContextForRoute(input.root, killContract, `loop worker interrupt:${input.missionId}:${input.target}`);
|
|
62
|
+
const interrupted = [];
|
|
63
|
+
const failed = [];
|
|
64
|
+
const blockers = [];
|
|
65
|
+
for (const handle of handles) {
|
|
66
|
+
if (handle.pid && handle.interrupt_supported) {
|
|
67
|
+
try {
|
|
68
|
+
await guardedProcessKill(killGuard, handle.pid, { signal: 'SIGTERM', confirmed: true });
|
|
69
|
+
await sleep(input.graceMs ?? 250);
|
|
70
|
+
if (processStillExists(handle.pid)) {
|
|
71
|
+
try {
|
|
72
|
+
await guardedProcessKill(killGuard, handle.pid, { signal: 'SIGKILL', confirmed: true });
|
|
73
|
+
}
|
|
74
|
+
catch { }
|
|
75
|
+
}
|
|
76
|
+
await markLoopWorkerInterrupted(input.root, input.missionId, handle.worker_id, 'interrupted');
|
|
77
|
+
interrupted.push(handle.worker_id);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
failed.push(handle.worker_id);
|
|
81
|
+
blockers.push(`loop_worker_interrupt_failed:${handle.worker_id}:${err instanceof Error ? err.message : String(err)}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else if (handle.session_id) {
|
|
85
|
+
await markLoopWorkerInterrupted(input.root, input.missionId, handle.worker_id, 'interrupted');
|
|
86
|
+
interrupted.push(handle.worker_id);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
failed.push(handle.worker_id);
|
|
90
|
+
blockers.push(`loop_worker_interrupt_unsupported:${handle.worker_id}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const result = {
|
|
94
|
+
schema: 'sks.loop-interrupt-result.v1',
|
|
95
|
+
ok: blockers.length === 0,
|
|
96
|
+
mission_id: input.missionId,
|
|
97
|
+
target: input.target,
|
|
98
|
+
interrupted,
|
|
99
|
+
failed,
|
|
100
|
+
handles,
|
|
101
|
+
blockers
|
|
102
|
+
};
|
|
103
|
+
await writeJsonAtomic(loopInterruptResultPath(input.root, input.missionId), { ...result, generated_at: new Date().toISOString() });
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
function processStillExists(pid) {
|
|
107
|
+
try {
|
|
108
|
+
process.kill(pid, 0);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function sleep(ms) {
|
|
116
|
+
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=loop-interrupt-registry.js.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { exists, runProcess } from '../fsx.js';
|
|
3
|
+
import { gitBlocker, runGitCommand } from '../git/git-worktree-runner.js';
|
|
4
|
+
export async function mergeSingleLoopWorktree(input) {
|
|
5
|
+
const attempts = [];
|
|
6
|
+
const changedFiles = [...new Set(input.proof.changed_files)];
|
|
7
|
+
const diff = await runGitCommand(input.worktreePath, ['diff', '--binary', '--full-index', 'HEAD'], { timeoutMs: 60000 }).catch(() => null);
|
|
8
|
+
if (!diff?.ok) {
|
|
9
|
+
return result(input.proof.loop_id, false, null, attempts, changedFiles, [`loop_merge_diff_failed:${input.proof.loop_id}`]);
|
|
10
|
+
}
|
|
11
|
+
if (!diff.stdout.trim())
|
|
12
|
+
return result(input.proof.loop_id, true, 'already_applied', attempts, changedFiles, []);
|
|
13
|
+
const applyCheck = await gitAttempt('apply-check', input.root, ['apply', '--check', '--whitespace=nowarn', '-'], diff.stdout);
|
|
14
|
+
attempts.push(applyCheck);
|
|
15
|
+
if (applyCheck.ok) {
|
|
16
|
+
const apply = await gitAttempt('apply', input.root, ['apply', '--whitespace=nowarn', '-'], diff.stdout);
|
|
17
|
+
attempts.push(apply);
|
|
18
|
+
if (apply.ok)
|
|
19
|
+
return result(input.proof.loop_id, true, 'apply', attempts, changedFiles, []);
|
|
20
|
+
await rollbackApply(input.root, diff.stdout);
|
|
21
|
+
}
|
|
22
|
+
const alreadyApplied = await gitAttempt('apply-check', input.root, ['apply', '--reverse', '--check', '--whitespace=nowarn', '-'], diff.stdout);
|
|
23
|
+
if (alreadyApplied.ok) {
|
|
24
|
+
attempts.push({ ...alreadyApplied, strategy: 'apply-check', blockers: [] });
|
|
25
|
+
return result(input.proof.loop_id, true, 'already_applied', attempts, changedFiles, []);
|
|
26
|
+
}
|
|
27
|
+
const apply3Check = await gitAttempt('apply-3way', input.root, ['apply', '--3way', '--check', '--whitespace=nowarn', '-'], diff.stdout);
|
|
28
|
+
attempts.push(apply3Check);
|
|
29
|
+
if (apply3Check.ok) {
|
|
30
|
+
const apply3 = await gitAttempt('apply-3way', input.root, ['apply', '--3way', '--whitespace=nowarn', '-'], diff.stdout);
|
|
31
|
+
attempts.push(apply3);
|
|
32
|
+
if (apply3.ok)
|
|
33
|
+
return result(input.proof.loop_id, true, 'apply-3way', attempts, changedFiles, []);
|
|
34
|
+
await abortMergeLikeState(input.root);
|
|
35
|
+
}
|
|
36
|
+
const head = await runGitCommand(input.worktreePath, ['rev-parse', '--verify', 'HEAD'], { timeoutMs: 10000 }).catch(() => null);
|
|
37
|
+
const commit = head?.ok ? head.stdout.trim() : '';
|
|
38
|
+
if (commit) {
|
|
39
|
+
const cherry = await gitAttempt('cherry-pick', input.root, ['cherry-pick', '--no-commit', commit], undefined);
|
|
40
|
+
attempts.push(cherry);
|
|
41
|
+
if (cherry.ok)
|
|
42
|
+
return result(input.proof.loop_id, true, 'cherry-pick', attempts, changedFiles, []);
|
|
43
|
+
await runGitCommand(input.root, ['cherry-pick', '--abort'], { timeoutMs: 30000 }).catch(() => null);
|
|
44
|
+
await abortMergeLikeState(input.root);
|
|
45
|
+
}
|
|
46
|
+
if (input.allowBranchMerge && input.proof.worktree.branch) {
|
|
47
|
+
const branch = input.proof.worktree.branch;
|
|
48
|
+
const merge = await gitAttempt('merge-no-commit', input.root, ['merge', '--no-ff', '--no-commit', branch], undefined);
|
|
49
|
+
attempts.push(merge);
|
|
50
|
+
if (merge.ok)
|
|
51
|
+
return result(input.proof.loop_id, true, 'merge-no-commit', attempts, changedFiles, []);
|
|
52
|
+
await runGitCommand(input.root, ['merge', '--abort'], { timeoutMs: 30000 }).catch(() => null);
|
|
53
|
+
await abortMergeLikeState(input.root);
|
|
54
|
+
}
|
|
55
|
+
const handoff = {
|
|
56
|
+
strategy: 'handoff',
|
|
57
|
+
ok: false,
|
|
58
|
+
exit_code: null,
|
|
59
|
+
stdout_tail: '',
|
|
60
|
+
stderr_tail: 'all merge strategies failed',
|
|
61
|
+
duration_ms: 1,
|
|
62
|
+
blockers: [`loop_merge_conflict_handoff:${input.proof.loop_id}`]
|
|
63
|
+
};
|
|
64
|
+
attempts.push(handoff);
|
|
65
|
+
return result(input.proof.loop_id, false, 'handoff', attempts, changedFiles, handoff.blockers);
|
|
66
|
+
}
|
|
67
|
+
async function gitAttempt(strategy, cwd, args, input) {
|
|
68
|
+
const started = Date.now();
|
|
69
|
+
const res = await runGitCommand(cwd, args, { timeoutMs: 60000, ...(input === undefined ? {} : { input }) }).catch((err) => null);
|
|
70
|
+
if (!res) {
|
|
71
|
+
return { strategy, ok: false, exit_code: null, stdout_tail: '', stderr_tail: '', duration_ms: Math.max(1, Date.now() - started), blockers: [`loop_merge_${strategy}_exception`] };
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
strategy,
|
|
75
|
+
ok: res.ok,
|
|
76
|
+
exit_code: res.code,
|
|
77
|
+
stdout_tail: res.stdout_tail,
|
|
78
|
+
stderr_tail: res.stderr_tail,
|
|
79
|
+
duration_ms: Math.max(1, Date.now() - started),
|
|
80
|
+
blockers: res.ok ? [] : [gitBlocker(`loop_merge_${strategy}_failed`, res)]
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
async function rollbackApply(root, diff) {
|
|
84
|
+
await runGitCommand(root, ['apply', '--reverse', '--whitespace=nowarn', '-'], { input: diff, timeoutMs: 60000 }).catch(() => null);
|
|
85
|
+
await abortMergeLikeState(root);
|
|
86
|
+
}
|
|
87
|
+
async function abortMergeLikeState(root) {
|
|
88
|
+
if (await exists(path.join(root, '.git', 'MERGE_HEAD')))
|
|
89
|
+
await runGitCommand(root, ['merge', '--abort'], { timeoutMs: 30000 }).catch(() => null);
|
|
90
|
+
if (await exists(path.join(root, '.git', 'CHERRY_PICK_HEAD')))
|
|
91
|
+
await runGitCommand(root, ['cherry-pick', '--abort'], { timeoutMs: 30000 }).catch(() => null);
|
|
92
|
+
await runProcess('git', ['reset', '--merge'], { cwd: root, timeoutMs: 30000, maxOutputBytes: 64 * 1024 }).catch(() => null);
|
|
93
|
+
}
|
|
94
|
+
function result(loopId, ok, selectedStrategy, attempts, changedFiles, blockers) {
|
|
95
|
+
return {
|
|
96
|
+
schema: 'sks.loop-merge-strategy-result.v1',
|
|
97
|
+
loop_id: loopId,
|
|
98
|
+
ok,
|
|
99
|
+
selected_strategy: selectedStrategy,
|
|
100
|
+
attempts,
|
|
101
|
+
changed_files: changedFiles,
|
|
102
|
+
blockers: [...new Set(blockers)]
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=loop-merge-strategy.js.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { appendJsonl, readText } from '../fsx.js';
|
|
2
|
+
import { loopMutationLedgerPath } from './loop-artifacts.js';
|
|
3
|
+
import { enforceLoopOwnerScope } from './loop-worktree-runtime.js';
|
|
4
|
+
export async function appendLoopMutationEvent(root, missionId, event) {
|
|
5
|
+
await appendJsonl(loopMutationLedgerPath(root, missionId), {
|
|
6
|
+
schema: 'sks.loop-mutation-ledger-event.v1',
|
|
7
|
+
ts: event.ts || new Date().toISOString(),
|
|
8
|
+
mission_id: missionId,
|
|
9
|
+
loop_id: event.loop_id,
|
|
10
|
+
event_type: event.event_type,
|
|
11
|
+
file_path: event.file_path,
|
|
12
|
+
source: event.source,
|
|
13
|
+
allowed_by_owner_scope: event.allowed_by_owner_scope,
|
|
14
|
+
details: event.details
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export async function readLoopMutationLedger(root, missionId) {
|
|
18
|
+
const text = await readText(loopMutationLedgerPath(root, missionId), '');
|
|
19
|
+
return String(text).split(/\r?\n/)
|
|
20
|
+
.map((line) => line.trim())
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.map((line) => {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(line);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
.filter((row) => Boolean(row));
|
|
31
|
+
}
|
|
32
|
+
export async function mutationLedgerFromLoopProofs(input) {
|
|
33
|
+
const events = [];
|
|
34
|
+
for (const proof of input.proofs) {
|
|
35
|
+
const workerChanged = [...new Set([...(proof.maker_result.changed_files || []), ...proof.changed_files])];
|
|
36
|
+
for (const file of workerChanged) {
|
|
37
|
+
const violations = enforceLoopOwnerScope([file], proof.owner_scope);
|
|
38
|
+
const eventType = violations.length ? 'owner_scope_violation' : 'file_changed';
|
|
39
|
+
const event = {
|
|
40
|
+
schema: 'sks.loop-mutation-ledger-event.v1',
|
|
41
|
+
ts: new Date().toISOString(),
|
|
42
|
+
mission_id: input.missionId,
|
|
43
|
+
loop_id: proof.loop_id,
|
|
44
|
+
event_type: eventType,
|
|
45
|
+
file_path: file,
|
|
46
|
+
source: 'git-diff',
|
|
47
|
+
allowed_by_owner_scope: violations.length === 0,
|
|
48
|
+
details: { status: proof.status, blockers: violations }
|
|
49
|
+
};
|
|
50
|
+
events.push(event);
|
|
51
|
+
await appendJsonl(loopMutationLedgerPath(input.root, input.missionId), event);
|
|
52
|
+
}
|
|
53
|
+
if (proof.gate_result.blockers?.some((blocker) => blocker.includes('side_effect') || blocker.includes('mutation'))) {
|
|
54
|
+
const event = {
|
|
55
|
+
schema: 'sks.loop-mutation-ledger-event.v1',
|
|
56
|
+
ts: new Date().toISOString(),
|
|
57
|
+
mission_id: input.missionId,
|
|
58
|
+
loop_id: proof.loop_id,
|
|
59
|
+
event_type: 'gate_side_effect',
|
|
60
|
+
file_path: null,
|
|
61
|
+
source: 'gate-result',
|
|
62
|
+
allowed_by_owner_scope: false,
|
|
63
|
+
details: { blockers: proof.gate_result.blockers || [] }
|
|
64
|
+
};
|
|
65
|
+
events.push(event);
|
|
66
|
+
await appendJsonl(loopMutationLedgerPath(input.root, input.missionId), event);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (input.integrationMerge) {
|
|
70
|
+
for (const loopId of input.integrationMerge.applied_loops) {
|
|
71
|
+
const event = {
|
|
72
|
+
schema: 'sks.loop-mutation-ledger-event.v1',
|
|
73
|
+
ts: new Date().toISOString(),
|
|
74
|
+
mission_id: input.missionId,
|
|
75
|
+
loop_id: loopId,
|
|
76
|
+
event_type: 'merge_applied',
|
|
77
|
+
file_path: null,
|
|
78
|
+
source: 'integration-merge',
|
|
79
|
+
allowed_by_owner_scope: true,
|
|
80
|
+
details: { changed_files: input.integrationMerge.changed_files }
|
|
81
|
+
};
|
|
82
|
+
events.push(event);
|
|
83
|
+
await appendJsonl(loopMutationLedgerPath(input.root, input.missionId), event);
|
|
84
|
+
}
|
|
85
|
+
for (const loopId of input.integrationMerge.conflict_loops) {
|
|
86
|
+
const event = {
|
|
87
|
+
schema: 'sks.loop-mutation-ledger-event.v1',
|
|
88
|
+
ts: new Date().toISOString(),
|
|
89
|
+
mission_id: input.missionId,
|
|
90
|
+
loop_id: loopId,
|
|
91
|
+
event_type: 'merge_conflict',
|
|
92
|
+
file_path: null,
|
|
93
|
+
source: 'integration-merge',
|
|
94
|
+
allowed_by_owner_scope: false,
|
|
95
|
+
details: { blockers: input.integrationMerge.blockers }
|
|
96
|
+
};
|
|
97
|
+
events.push(event);
|
|
98
|
+
await appendJsonl(loopMutationLedgerPath(input.root, input.missionId), event);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return events;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=loop-mutation-ledger.js.map
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { readJson, writeJsonAtomic } from '../fsx.js';
|
|
2
2
|
import { loopKillRequestPath, loopProofPath, loopStatePath } from './loop-artifacts.js';
|
|
3
3
|
import { writeLoopCheckpoint } from './loop-checkpoint.js';
|
|
4
|
+
import { interruptLoopWorkers } from './loop-interrupt-registry.js';
|
|
4
5
|
export async function writeLoopKillRequest(root, missionId, target) {
|
|
5
6
|
const request = { schema: 'sks.loop-kill-request.v1', mission_id: missionId, target, requested_at: new Date().toISOString() };
|
|
6
7
|
await writeJsonAtomic(loopKillRequestPath(root, missionId), request);
|
|
8
|
+
await interruptLoopWorkers({ root, missionId, target }).catch(() => undefined);
|
|
7
9
|
return request;
|
|
8
10
|
}
|
|
9
11
|
export async function shouldKillLoop(root, missionId, loopId) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { writeJsonAtomic } from '../fsx.js';
|
|
2
2
|
import { loopBudgetPath, loopProofPath, loopStatePath } from './loop-artifacts.js';
|
|
3
|
+
import { computeLoopConcurrencyBudget, writeLoopConcurrencyBudget } from './loop-concurrency-budget.js';
|
|
3
4
|
import { writeLoopCheckpoint } from './loop-checkpoint.js';
|
|
4
5
|
import { finalizeLoopGraph } from './loop-finalizer.js';
|
|
5
6
|
import { runLoopGates } from './loop-gate-runner.js';
|
|
@@ -11,7 +12,9 @@ import { runLoopCheckerWorkers, runLoopMakerWorkers } from './loop-worker-runtim
|
|
|
11
12
|
import { allocateLoopWorktree, computeLoopDiff } from './loop-worktree-runtime.js';
|
|
12
13
|
export async function runLoopPlan(input) {
|
|
13
14
|
const started = Date.now();
|
|
14
|
-
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);
|
|
15
18
|
const proofs = [];
|
|
16
19
|
for (const batch of schedule.batches) {
|
|
17
20
|
if (await shouldKillLoop(input.root, input.plan.mission_id, 'all'))
|
|
@@ -28,8 +31,8 @@ export async function runLoopPlan(input) {
|
|
|
28
31
|
root: input.root,
|
|
29
32
|
plan: input.plan,
|
|
30
33
|
proofs,
|
|
31
|
-
maxActiveLoops:
|
|
32
|
-
maxActiveWorkers:
|
|
34
|
+
maxActiveLoops: concurrencyBudget.max_active_loops,
|
|
35
|
+
maxActiveWorkers: concurrencyBudget.max_active_workers,
|
|
33
36
|
wallMs: Math.max(1, Date.now() - started)
|
|
34
37
|
});
|
|
35
38
|
return {
|
|
@@ -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)));
|
|
@@ -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
|