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
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
export function loopRoot(root, missionId) {
|
|
3
|
-
|
|
3
|
+
const missionsRoot = path.resolve(root, '.sneakoscope', 'missions');
|
|
4
|
+
return containedJoin(missionsRoot, safeArtifactId('mission', missionId), 'loops');
|
|
4
5
|
}
|
|
5
6
|
export function loopNodeRoot(root, missionId, loopId) {
|
|
6
|
-
return
|
|
7
|
+
return containedJoin(loopRoot(root, missionId), safeArtifactId('loop', loopId));
|
|
7
8
|
}
|
|
8
9
|
export function loopPlanPath(root, missionId) {
|
|
9
10
|
return path.join(loopRoot(root, missionId), 'loop-plan.json');
|
|
@@ -20,9 +21,24 @@ export function loopProofPath(root, missionId, loopId) {
|
|
|
20
21
|
export function loopBudgetPath(root, missionId, loopId) {
|
|
21
22
|
return path.join(loopNodeRoot(root, missionId, loopId), 'loop-budget.json');
|
|
22
23
|
}
|
|
24
|
+
export function loopCheckpointPath(root, missionId, loopId, iteration, phase) {
|
|
25
|
+
return path.join(loopNodeRoot(root, missionId, loopId), 'checkpoints', `${String(Math.max(1, Math.floor(iteration))).padStart(4, '0')}-${sanitizeArtifactPart(phase)}.json`);
|
|
26
|
+
}
|
|
27
|
+
export function loopLatestCheckpointPath(root, missionId, loopId) {
|
|
28
|
+
return path.join(loopNodeRoot(root, missionId, loopId), 'checkpoint-latest.json');
|
|
29
|
+
}
|
|
23
30
|
export function loopGraphProofPath(root, missionId) {
|
|
24
31
|
return path.join(loopRoot(root, missionId), 'loop-graph-proof.json');
|
|
25
32
|
}
|
|
33
|
+
export function loopIntegrationMergePath(root, missionId) {
|
|
34
|
+
return path.join(loopRoot(root, missionId), 'integration-merge.json');
|
|
35
|
+
}
|
|
36
|
+
export function loopGptFinalArbiterPath(root, missionId) {
|
|
37
|
+
return path.join(loopRoot(root, missionId), 'loop-gpt-final-arbiter.json');
|
|
38
|
+
}
|
|
39
|
+
export function loopKillRequestPath(root, missionId) {
|
|
40
|
+
return path.join(loopRoot(root, missionId), 'kill-request.json');
|
|
41
|
+
}
|
|
26
42
|
export function loopGatePath(root, missionId, loopId, gateId) {
|
|
27
43
|
return path.join(loopNodeRoot(root, missionId, loopId), 'gates', `${sanitizeArtifactPart(gateId)}.json`);
|
|
28
44
|
}
|
|
@@ -38,4 +54,19 @@ export function loopOwnerLedgerPath(root, missionId) {
|
|
|
38
54
|
export function sanitizeArtifactPart(value) {
|
|
39
55
|
return String(value || 'artifact').replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 96) || 'artifact';
|
|
40
56
|
}
|
|
57
|
+
function safeArtifactId(kind, value) {
|
|
58
|
+
const text = String(value || '').trim();
|
|
59
|
+
const sanitized = sanitizeArtifactPart(text);
|
|
60
|
+
if (!text || sanitized !== text)
|
|
61
|
+
throw new Error(`invalid_loop_${kind}_id:${text || 'empty'}`);
|
|
62
|
+
return sanitized;
|
|
63
|
+
}
|
|
64
|
+
function containedJoin(base, ...parts) {
|
|
65
|
+
const resolvedBase = path.resolve(base);
|
|
66
|
+
const target = path.resolve(resolvedBase, ...parts);
|
|
67
|
+
if (target !== resolvedBase && !target.startsWith(`${resolvedBase}${path.sep}`)) {
|
|
68
|
+
throw new Error(`loop_artifact_path_escape:${target}`);
|
|
69
|
+
}
|
|
70
|
+
return target;
|
|
71
|
+
}
|
|
41
72
|
//# sourceMappingURL=loop-artifacts.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readJson, writeJsonAtomic } from '../fsx.js';
|
|
2
|
+
import { loopCheckpointPath, loopLatestCheckpointPath } from './loop-artifacts.js';
|
|
3
|
+
export async function writeLoopCheckpoint(input) {
|
|
4
|
+
const checkpoint = {
|
|
5
|
+
schema: 'sks.loop-checkpoint.v1',
|
|
6
|
+
mission_id: input.mission_id,
|
|
7
|
+
loop_id: input.loop_id,
|
|
8
|
+
iteration: input.iteration,
|
|
9
|
+
phase: input.phase,
|
|
10
|
+
state_path: input.state_path,
|
|
11
|
+
proof_path: input.proof_path,
|
|
12
|
+
resumable: input.resumable,
|
|
13
|
+
created_at: new Date().toISOString()
|
|
14
|
+
};
|
|
15
|
+
await writeJsonAtomic(loopCheckpointPath(input.root, input.mission_id, input.loop_id, input.iteration, input.phase), checkpoint);
|
|
16
|
+
await writeJsonAtomic(loopLatestCheckpointPath(input.root, input.mission_id, input.loop_id), checkpoint);
|
|
17
|
+
return checkpoint;
|
|
18
|
+
}
|
|
19
|
+
export async function readLatestLoopCheckpoint(root, missionId, loopId) {
|
|
20
|
+
return readJson(loopLatestCheckpointPath(root, missionId, loopId), null);
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=loop-checkpoint.js.map
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { readJson, writeJsonAtomic } from '../fsx.js';
|
|
2
2
|
import { loopGraphProofPath, loopProofPath } from './loop-artifacts.js';
|
|
3
|
+
import { runLoopGptFinalArbiter } from './loop-gpt-final-arbiter.js';
|
|
4
|
+
import { mergeLoopWorktrees } from './loop-integration-merge.js';
|
|
3
5
|
import { graphProofFromLoopProofs } from './loop-scheduler.js';
|
|
4
6
|
export async function finalizeLoopGraph(input) {
|
|
5
7
|
const proofs = input.proofs || await Promise.all(input.plan.graph.nodes.map((node) => readJson(loopProofPath(input.root, input.plan.mission_id, node.loop_id), null)));
|
|
@@ -11,16 +13,40 @@ export async function finalizeLoopGraph(input) {
|
|
|
11
13
|
maxActiveWorkers: input.maxActiveWorkers || Math.max(1, realProofs.reduce((sum, proof) => sum + proof.maker_result.worker_count + proof.checker_result.worker_count, 0)),
|
|
12
14
|
wallMs: input.wallMs || 1
|
|
13
15
|
});
|
|
16
|
+
const integrationMerge = await mergeLoopWorktrees({
|
|
17
|
+
root: input.root,
|
|
18
|
+
plan: input.plan,
|
|
19
|
+
proofs: realProofs
|
|
20
|
+
});
|
|
14
21
|
const anyHandoff = realProofs.some((proof) => proof.handoff.required);
|
|
15
|
-
const anySourceMutation = realProofs.some((proof) => proof.changed_files.
|
|
22
|
+
const anySourceMutation = realProofs.some((proof) => proof.changed_files.some((file) => !file.startsWith('.sneakoscope/')));
|
|
23
|
+
const arbiter = anySourceMutation
|
|
24
|
+
? await runLoopGptFinalArbiter({ root: input.root, plan: input.plan, proofs: realProofs, integrationMerge })
|
|
25
|
+
: null;
|
|
26
|
+
const blockers = [
|
|
27
|
+
...graph.blockers,
|
|
28
|
+
...(anyHandoff ? ['loop_handoff_required'] : []),
|
|
29
|
+
...(integrationMerge.ok ? [] : integrationMerge.blockers),
|
|
30
|
+
...(anySourceMutation && !arbiter ? ['gpt_final_arbiter_missing'] : []),
|
|
31
|
+
...(arbiter && !arbiter.ok ? ['gpt_final_arbiter_not_approved', ...arbiter.blockers] : [])
|
|
32
|
+
];
|
|
16
33
|
const finalGraph = {
|
|
17
34
|
...graph,
|
|
18
|
-
ok: graph.ok &&
|
|
19
|
-
blockers: [
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
35
|
+
ok: graph.ok && blockers.length === 0,
|
|
36
|
+
blockers: [...new Set(blockers)],
|
|
37
|
+
integration_merge: {
|
|
38
|
+
ok: integrationMerge.ok,
|
|
39
|
+
artifact_path: `.sneakoscope/missions/${input.plan.mission_id}/loops/integration-merge.json`,
|
|
40
|
+
applied_loops: integrationMerge.applied_loops,
|
|
41
|
+
conflict_loops: integrationMerge.conflict_loops
|
|
42
|
+
},
|
|
43
|
+
...(arbiter ? {
|
|
44
|
+
gpt_final_arbiter: {
|
|
45
|
+
ok: arbiter.ok,
|
|
46
|
+
artifact_path: arbiter.artifact_path,
|
|
47
|
+
verdict: arbiter.verdict
|
|
48
|
+
}
|
|
49
|
+
} : {})
|
|
24
50
|
};
|
|
25
51
|
await writeJsonAtomic(loopGraphProofPath(input.root, input.plan.mission_id), finalGraph);
|
|
26
52
|
return finalGraph;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readJson } from '../fsx.js';
|
|
3
|
+
export async function resolveLoopGate(root, gateId) {
|
|
4
|
+
const builtin = builtinLoopGate(gateId);
|
|
5
|
+
if (builtin)
|
|
6
|
+
return builtin;
|
|
7
|
+
const releaseGate = await resolveReleaseGate(root, gateId);
|
|
8
|
+
if (releaseGate)
|
|
9
|
+
return releaseGate;
|
|
10
|
+
const packageGate = await resolvePackageScriptGate(root, gateId);
|
|
11
|
+
if (packageGate)
|
|
12
|
+
return packageGate;
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
export async function listLoopGateDefinitions(root) {
|
|
16
|
+
const packageJson = await readJson(path.join(root, 'package.json'), {});
|
|
17
|
+
const release = await readJson(path.join(root, 'release-gates.v2.json'), {});
|
|
18
|
+
const packageScripts = packageJson && typeof packageJson === 'object' && packageJson.scripts && typeof packageJson.scripts === 'object'
|
|
19
|
+
? Object.keys(packageJson.scripts)
|
|
20
|
+
: [];
|
|
21
|
+
const releaseGates = Array.isArray(release.gates) ? release.gates : [];
|
|
22
|
+
const definitions = [
|
|
23
|
+
...['gpt:final-arbiter', 'human:handoff-required', 'loop:checker-fresh-session', 'loop:state-valid', 'loop:budget-valid'].map((id) => builtinLoopGate(id)).filter((row) => Boolean(row)),
|
|
24
|
+
...releaseGates.map((gate) => normalizeReleaseGate(gate)).filter((row) => Boolean(row)),
|
|
25
|
+
...packageScripts.map((id) => ({
|
|
26
|
+
id,
|
|
27
|
+
command: `npm run ${shellQuote(id)} --silent`,
|
|
28
|
+
source: 'package-json',
|
|
29
|
+
side_effect: 'hermetic',
|
|
30
|
+
timeout_ms: 300000,
|
|
31
|
+
cache_allowed: true
|
|
32
|
+
}))
|
|
33
|
+
];
|
|
34
|
+
const byId = new Map();
|
|
35
|
+
for (const definition of definitions)
|
|
36
|
+
if (!byId.has(definition.id))
|
|
37
|
+
byId.set(definition.id, definition);
|
|
38
|
+
return [...byId.values()];
|
|
39
|
+
}
|
|
40
|
+
function builtinLoopGate(gateId) {
|
|
41
|
+
if (gateId === 'gpt:final-arbiter') {
|
|
42
|
+
return { id: gateId, command: 'builtin:gpt-final-arbiter', source: 'builtin-pseudo', side_effect: 'read-only', timeout_ms: 300000, cache_allowed: false };
|
|
43
|
+
}
|
|
44
|
+
if (gateId === 'human:handoff-required') {
|
|
45
|
+
return { id: gateId, command: 'builtin:human-handoff-required', source: 'builtin-pseudo', side_effect: 'human', timeout_ms: 0, cache_allowed: false };
|
|
46
|
+
}
|
|
47
|
+
if (gateId === 'loop:checker-fresh-session') {
|
|
48
|
+
return { id: gateId, command: 'builtin:loop-checker-fresh-session', source: 'builtin-pseudo', side_effect: 'read-only', timeout_ms: 30000, cache_allowed: false };
|
|
49
|
+
}
|
|
50
|
+
if (gateId === 'loop:state-valid') {
|
|
51
|
+
return { id: gateId, command: 'builtin:loop-state-valid', source: 'builtin-pseudo', side_effect: 'read-only', timeout_ms: 30000, cache_allowed: true };
|
|
52
|
+
}
|
|
53
|
+
if (gateId === 'loop:budget-valid') {
|
|
54
|
+
return { id: gateId, command: 'builtin:loop-budget-valid', source: 'builtin-pseudo', side_effect: 'read-only', timeout_ms: 30000, cache_allowed: true };
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
async function resolveReleaseGate(root, gateId) {
|
|
59
|
+
const release = await readJson(path.join(root, 'release-gates.v2.json'), {});
|
|
60
|
+
const gate = Array.isArray(release.gates) ? release.gates.find((row) => row.id === gateId) : null;
|
|
61
|
+
return gate ? normalizeReleaseGate(gate) : null;
|
|
62
|
+
}
|
|
63
|
+
function normalizeReleaseGate(gate) {
|
|
64
|
+
const id = String(gate.id || '');
|
|
65
|
+
const command = String(gate.command || '');
|
|
66
|
+
if (!id || !command)
|
|
67
|
+
return null;
|
|
68
|
+
return {
|
|
69
|
+
id,
|
|
70
|
+
command,
|
|
71
|
+
source: 'release-gates-v2',
|
|
72
|
+
side_effect: normalizeSideEffect(gate.side_effect),
|
|
73
|
+
timeout_ms: Number.isFinite(Number(gate.timeout_ms)) ? Math.max(1, Number(gate.timeout_ms)) : 300000,
|
|
74
|
+
cache_allowed: gate.cache?.enabled !== false
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async function resolvePackageScriptGate(root, gateId) {
|
|
78
|
+
const packageJson = await readJson(path.join(root, 'package.json'), {});
|
|
79
|
+
if (!packageJson?.scripts || typeof packageJson.scripts !== 'object' || !(gateId in packageJson.scripts))
|
|
80
|
+
return null;
|
|
81
|
+
return {
|
|
82
|
+
id: gateId,
|
|
83
|
+
command: `npm run ${shellQuote(gateId)} --silent`,
|
|
84
|
+
source: 'package-json',
|
|
85
|
+
side_effect: 'hermetic',
|
|
86
|
+
timeout_ms: 300000,
|
|
87
|
+
cache_allowed: true
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function normalizeSideEffect(value) {
|
|
91
|
+
return value === 'read-only' || value === 'mutation' || value === 'human' ? value : 'hermetic';
|
|
92
|
+
}
|
|
93
|
+
function shellQuote(value) {
|
|
94
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=loop-gate-registry.js.map
|
|
@@ -1,29 +1,177 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fsp from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readJson, runProcess, writeJsonAtomic } from '../fsx.js';
|
|
2
4
|
import { allGateIds } from './loop-schema.js';
|
|
3
|
-
import { loopGatePath } from './loop-artifacts.js';
|
|
5
|
+
import { loopBudgetPath, loopGatePath, loopStatePath } from './loop-artifacts.js';
|
|
6
|
+
import { resolveLoopGate } from './loop-gate-registry.js';
|
|
4
7
|
export async function runLoopGates(input) {
|
|
5
|
-
const selected = allGateIds(input.gates)
|
|
6
|
-
const failed =
|
|
7
|
-
const passed =
|
|
8
|
+
const selected = allGateIds(input.gates);
|
|
9
|
+
const failed = [];
|
|
10
|
+
const passed = [];
|
|
11
|
+
const skipped = [];
|
|
12
|
+
const blockers = [];
|
|
8
13
|
for (const gate of selected) {
|
|
9
|
-
await
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
generated_at: new Date().toISOString()
|
|
18
|
-
});
|
|
14
|
+
const result = await runOneGate(input, gate);
|
|
15
|
+
if (result.skipped)
|
|
16
|
+
skipped.push(gate);
|
|
17
|
+
else if (result.ok)
|
|
18
|
+
passed.push(gate);
|
|
19
|
+
else
|
|
20
|
+
failed.push(gate);
|
|
21
|
+
blockers.push(...result.blockers);
|
|
19
22
|
}
|
|
20
23
|
return {
|
|
21
24
|
ok: failed.length === 0,
|
|
22
25
|
selected_gates: selected,
|
|
23
26
|
passed_gates: passed,
|
|
24
27
|
failed_gates: failed,
|
|
25
|
-
skipped_gates:
|
|
26
|
-
blockers
|
|
28
|
+
skipped_gates: skipped,
|
|
29
|
+
blockers
|
|
27
30
|
};
|
|
28
31
|
}
|
|
32
|
+
async function runOneGate(input, gateId) {
|
|
33
|
+
const started = Date.now();
|
|
34
|
+
const definition = await resolveLoopGate(input.root, gateId);
|
|
35
|
+
const fullReleaseCheckInsideLoop = gateId === 'release:check' && input.node.route !== '$Integration';
|
|
36
|
+
const unknown = !definition;
|
|
37
|
+
const packageJson = unknown ? await readJson(path.join(input.root, 'package.json'), null) : null;
|
|
38
|
+
const skipUnknownFixtureGate = unknown && !packageJson;
|
|
39
|
+
const blockers = [
|
|
40
|
+
...(unknown && !skipUnknownFixtureGate ? [`unknown_loop_gate:${gateId}`] : []),
|
|
41
|
+
...(fullReleaseCheckInsideLoop ? ['full_release_check_inside_non_integration_loop'] : [])
|
|
42
|
+
];
|
|
43
|
+
let ok = blockers.length === 0;
|
|
44
|
+
let skipped = skipUnknownFixtureGate;
|
|
45
|
+
let exitCode = null;
|
|
46
|
+
let stdoutTail = '';
|
|
47
|
+
let stderrTail = '';
|
|
48
|
+
let timedOut = false;
|
|
49
|
+
const fixtureMode = process.env.SKS_LOOP_GATE_FIXTURE === '1';
|
|
50
|
+
if (definition && ok) {
|
|
51
|
+
if (fixtureMode && definition.source !== 'builtin-pseudo') {
|
|
52
|
+
ok = true;
|
|
53
|
+
}
|
|
54
|
+
else if (definition.source === 'builtin-pseudo') {
|
|
55
|
+
const builtin = await runBuiltinGate(input.root, input.missionId, input.node.loop_id, definition, input.checkerArtifacts || []);
|
|
56
|
+
ok = builtin.ok;
|
|
57
|
+
skipped = builtin.skipped;
|
|
58
|
+
blockers.push(...builtin.blockers);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const command = definition.command;
|
|
62
|
+
const result = await runProcess(process.env.SHELL || '/bin/sh', ['-lc', command], {
|
|
63
|
+
cwd: input.root,
|
|
64
|
+
timeoutMs: input.timeoutMs || definition.timeout_ms,
|
|
65
|
+
maxOutputBytes: 512 * 1024,
|
|
66
|
+
env: {
|
|
67
|
+
SKS_LOOP_ID: input.node.loop_id,
|
|
68
|
+
SKS_MISSION_ID: input.missionId,
|
|
69
|
+
SKS_LOOP_GATE: gateId
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
exitCode = result.code;
|
|
73
|
+
stdoutTail = result.stdout.slice(-8000);
|
|
74
|
+
stderrTail = result.stderr.slice(-8000);
|
|
75
|
+
timedOut = result.timedOut;
|
|
76
|
+
ok = result.code === 0;
|
|
77
|
+
if (!ok)
|
|
78
|
+
blockers.push(`gate_command_failed:${gateId}:${result.code}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const artifact = {
|
|
82
|
+
schema: 'sks.loop-gate-result.v1',
|
|
83
|
+
ok,
|
|
84
|
+
gate_id: gateId,
|
|
85
|
+
loop_id: input.node.loop_id,
|
|
86
|
+
command: definition?.command || null,
|
|
87
|
+
source: definition?.source || null,
|
|
88
|
+
exit_code: exitCode,
|
|
89
|
+
duration_ms: Math.max(1, Date.now() - started),
|
|
90
|
+
stdout_tail: stdoutTail,
|
|
91
|
+
stderr_tail: stderrTail,
|
|
92
|
+
cached_allowed: definition?.cache_allowed ?? false,
|
|
93
|
+
fixture_mode: fixtureMode,
|
|
94
|
+
skipped,
|
|
95
|
+
deferred_unknown_fixture_gate: skipUnknownFixtureGate,
|
|
96
|
+
timed_out: timedOut,
|
|
97
|
+
full_release_check_inside_loop: fullReleaseCheckInsideLoop,
|
|
98
|
+
generated_at: new Date().toISOString(),
|
|
99
|
+
blockers
|
|
100
|
+
};
|
|
101
|
+
await writeJsonAtomic(loopGatePath(input.root, input.missionId, input.node.loop_id, gateId), artifact);
|
|
102
|
+
return { ok, skipped, blockers };
|
|
103
|
+
}
|
|
104
|
+
async function runBuiltinGate(root, missionId, loopId, definition, checkerArtifacts) {
|
|
105
|
+
if (definition.id === 'gpt:final-arbiter')
|
|
106
|
+
return { ok: true, skipped: true, blockers: [] };
|
|
107
|
+
if (definition.id === 'human:handoff-required')
|
|
108
|
+
return { ok: false, skipped: false, blockers: ['human_handoff_required'] };
|
|
109
|
+
if (definition.id === 'loop:state-valid') {
|
|
110
|
+
const state = await readJson(loopStatePath(root, missionId, loopId), null);
|
|
111
|
+
return state?.schema === 'sks.loop-state.v1' ? { ok: true, skipped: false, blockers: [] } : { ok: false, skipped: false, blockers: ['loop_state_invalid'] };
|
|
112
|
+
}
|
|
113
|
+
if (definition.id === 'loop:budget-valid') {
|
|
114
|
+
const budget = await readJson(loopBudgetPath(root, missionId, loopId), null);
|
|
115
|
+
return budget && typeof budget === 'object' ? { ok: true, skipped: false, blockers: [] } : { ok: false, skipped: false, blockers: ['loop_budget_invalid'] };
|
|
116
|
+
}
|
|
117
|
+
if (definition.id === 'loop:checker-fresh-session') {
|
|
118
|
+
const artifacts = await Promise.all(checkerArtifacts.map((artifact) => readCheckerArtifact(root, missionId, artifact)));
|
|
119
|
+
const fresh = artifacts.some((artifact) => artifact?.fresh_session === true && artifact?.approved === true);
|
|
120
|
+
return fresh ? { ok: true, skipped: false, blockers: [] } : { ok: false, skipped: false, blockers: ['loop_checker_fresh_session_missing'] };
|
|
121
|
+
}
|
|
122
|
+
return { ok: false, skipped: false, blockers: [`unknown_builtin_gate:${definition.id}`] };
|
|
123
|
+
}
|
|
124
|
+
async function readCheckerArtifact(root, missionId, artifact) {
|
|
125
|
+
for (const candidate of checkerArtifactPathCandidates(root, missionId, artifact)) {
|
|
126
|
+
const readable = await checkerArtifactReadablePath(root, missionId, candidate);
|
|
127
|
+
if (!readable)
|
|
128
|
+
continue;
|
|
129
|
+
const row = await readJson(readable, null);
|
|
130
|
+
if (row)
|
|
131
|
+
return row;
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
function checkerArtifactPathCandidates(root, missionId, artifact) {
|
|
136
|
+
const raw = String(artifact || '').trim();
|
|
137
|
+
if (!raw)
|
|
138
|
+
return [];
|
|
139
|
+
const missionRoot = path.join(root, '.sneakoscope', 'missions', missionId);
|
|
140
|
+
const resolvedMissionRoot = path.resolve(missionRoot);
|
|
141
|
+
if (path.isAbsolute(raw)) {
|
|
142
|
+
return [path.resolve(raw)];
|
|
143
|
+
}
|
|
144
|
+
return uniqueStrings([
|
|
145
|
+
safeResolveWithin(path.join(resolvedMissionRoot, 'agents'), raw),
|
|
146
|
+
safeResolveWithin(resolvedMissionRoot, raw),
|
|
147
|
+
safeResolveWithin(path.join(resolvedMissionRoot, 'loops'), raw)
|
|
148
|
+
].filter((value) => Boolean(value)));
|
|
149
|
+
}
|
|
150
|
+
function uniqueStrings(values) {
|
|
151
|
+
return [...new Set(values.filter(Boolean))];
|
|
152
|
+
}
|
|
153
|
+
async function checkerArtifactReadablePath(root, missionId, candidate) {
|
|
154
|
+
const resolvedMissionRoot = path.resolve(root, '.sneakoscope', 'missions', missionId);
|
|
155
|
+
const resolvedCandidate = path.resolve(candidate);
|
|
156
|
+
try {
|
|
157
|
+
const [realMissionRoot, realCandidate] = await Promise.all([
|
|
158
|
+
fsp.realpath(resolvedMissionRoot),
|
|
159
|
+
fsp.realpath(resolvedCandidate)
|
|
160
|
+
]);
|
|
161
|
+
return isWithinPath(realMissionRoot, realCandidate) ? realCandidate : null;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function safeResolveWithin(base, target) {
|
|
168
|
+
const resolvedBase = path.resolve(base);
|
|
169
|
+
const resolvedTarget = path.resolve(resolvedBase, target);
|
|
170
|
+
return isWithinPath(resolvedBase, resolvedTarget) ? resolvedTarget : null;
|
|
171
|
+
}
|
|
172
|
+
function isWithinPath(base, target) {
|
|
173
|
+
const resolvedBase = path.resolve(base);
|
|
174
|
+
const resolvedTarget = path.resolve(target);
|
|
175
|
+
return resolvedTarget === resolvedBase || resolvedTarget.startsWith(`${resolvedBase}${path.sep}`);
|
|
176
|
+
}
|
|
29
177
|
//# sourceMappingURL=loop-gate-runner.js.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { runGptFinalArbiter } from '../codex-control/gpt-final-arbiter.js';
|
|
2
|
+
import { nowIso, writeJsonAtomic } from '../fsx.js';
|
|
3
|
+
import { loopGptFinalArbiterPath } from './loop-artifacts.js';
|
|
4
|
+
export async function runLoopGptFinalArbiter(input) {
|
|
5
|
+
const artifactPath = loopGptFinalArbiterPath(input.root, input.plan.mission_id);
|
|
6
|
+
const changedFiles = [...new Set([
|
|
7
|
+
...input.integrationMerge.changed_files,
|
|
8
|
+
...input.proofs.flatMap((proof) => proof.changed_files)
|
|
9
|
+
])];
|
|
10
|
+
const reviewedLoopIds = input.proofs.map((proof) => proof.loop_id);
|
|
11
|
+
if (process.env.SKS_LOOP_GPT_FINAL_FIXTURE === '1' || input.forceVerdict) {
|
|
12
|
+
const verdict = input.forceVerdict || (process.env.SKS_LOOP_GPT_FINAL_REJECT === '1' ? 'reject' : 'approve');
|
|
13
|
+
const result = buildResult(input.plan.mission_id, reviewedLoopIds, changedFiles, verdict, verdict === 'approve' ? [] : ['fixture_revision_required'], artifactPath, []);
|
|
14
|
+
await writeJsonAtomic(artifactPath, { ...result, generated_at: nowIso(), backend: 'fixture' });
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
const arbiter = await runGptFinalArbiter({
|
|
18
|
+
schema: 'sks.gpt-final-arbiter-input.v1',
|
|
19
|
+
route: '$Loop',
|
|
20
|
+
mission_id: input.plan.mission_id,
|
|
21
|
+
local_mode: 'local-parallel-gpt-final',
|
|
22
|
+
local_outputs: input.proofs.map((proof) => ({
|
|
23
|
+
id: proof.loop_id,
|
|
24
|
+
backend: proof.maker_result.backend || 'loop-worker',
|
|
25
|
+
status: proof.status,
|
|
26
|
+
summary: proof.blockers.join(', ') || 'loop proof completed',
|
|
27
|
+
changed_files: proof.changed_files,
|
|
28
|
+
blockers: proof.blockers
|
|
29
|
+
})),
|
|
30
|
+
candidate_diff: JSON.stringify({ changed_files: changedFiles, integration_merge: input.integrationMerge }),
|
|
31
|
+
verification_results: input.proofs.map((proof) => ({ id: proof.loop_id, ok: proof.status === 'completed', blockers: proof.blockers })),
|
|
32
|
+
side_effect_report: { schema: 'sks.loop-side-effect-report.v1', ok: true, changed_files: changedFiles },
|
|
33
|
+
mutation_ledger: { schema: 'sks.loop-mutation-ledger.v1', proofs: input.proofs },
|
|
34
|
+
rollback_plan: { schema: 'sks.loop-rollback-plan.v1', strategy: 'git-worktree-or-human-handoff' }
|
|
35
|
+
}, { cwd: input.root, mutationLedgerRoot: `${input.root}/.sneakoscope/missions/${input.plan.mission_id}/loops/gpt-final-arbiter` });
|
|
36
|
+
const status = String(arbiter.result?.status || '');
|
|
37
|
+
const verdict = status === 'approved' || status === 'modified' ? 'approve' : status === 'needs_more_work' ? 'revise' : 'reject';
|
|
38
|
+
const blockers = stringArray(arbiter.blockers);
|
|
39
|
+
const result = buildResult(input.plan.mission_id, reviewedLoopIds, changedFiles, verdict, stringArray(arbiter.result?.required_followup_work), artifactPath, blockers);
|
|
40
|
+
await writeJsonAtomic(artifactPath, { ...result, generated_at: nowIso(), backend: arbiter.backend || null, arbiter });
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
function buildResult(missionId, reviewedLoopIds, changedFiles, verdict, revisions, artifactPath, blockers) {
|
|
44
|
+
return {
|
|
45
|
+
schema: 'sks.loop-gpt-final-arbiter.v1',
|
|
46
|
+
ok: verdict === 'approve' && blockers.length === 0,
|
|
47
|
+
mission_id: missionId,
|
|
48
|
+
reviewed_loop_ids: reviewedLoopIds,
|
|
49
|
+
changed_files: changedFiles,
|
|
50
|
+
verdict,
|
|
51
|
+
required_revisions: revisions,
|
|
52
|
+
blockers,
|
|
53
|
+
artifact_path: artifactPath
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function stringArray(value) {
|
|
57
|
+
if (!Array.isArray(value))
|
|
58
|
+
return [];
|
|
59
|
+
return value.map((item) => typeof item === 'string' ? item : JSON.stringify(item)).filter(Boolean);
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=loop-gpt-final-arbiter.js.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { nowIso, writeJsonAtomic } from '../fsx.js';
|
|
3
|
+
import { runGitCommand } from '../git/git-worktree-runner.js';
|
|
4
|
+
import { guardedWriteFile, guardContextForRoute } from '../safety/mutation-guard.js';
|
|
5
|
+
import { createRequestedScopeContract } from '../safety/requested-scope-contract.js';
|
|
6
|
+
import { loopIntegrationMergePath } from './loop-artifacts.js';
|
|
7
|
+
export async function mergeLoopWorktrees(input) {
|
|
8
|
+
const completed = input.proofs.filter((proof) => proof.status === 'completed' && proof.loop_id !== input.plan.integration_loop_id);
|
|
9
|
+
const blockers = [];
|
|
10
|
+
const appliedLoops = [];
|
|
11
|
+
const conflictLoops = new Set();
|
|
12
|
+
const changedFiles = new Set();
|
|
13
|
+
const owners = new Map();
|
|
14
|
+
for (const proof of completed) {
|
|
15
|
+
for (const file of proof.changed_files) {
|
|
16
|
+
const previous = owners.get(file);
|
|
17
|
+
if (previous && previous !== proof.loop_id) {
|
|
18
|
+
blockers.push(`loop_integration_file_conflict:${file}:${previous}:${proof.loop_id}`);
|
|
19
|
+
conflictLoops.add(previous);
|
|
20
|
+
conflictLoops.add(proof.loop_id);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
owners.set(file, proof.loop_id);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (!blockers.length) {
|
|
28
|
+
for (const proof of completed) {
|
|
29
|
+
const worktreePath = proof.worktree.path;
|
|
30
|
+
if (!worktreePath)
|
|
31
|
+
continue;
|
|
32
|
+
const diff = await runGitCommand(worktreePath, ['diff', '--binary', '--full-index', 'HEAD'], { timeoutMs: 60000 }).catch(() => null);
|
|
33
|
+
if (!diff?.ok) {
|
|
34
|
+
blockers.push(`loop_integration_diff_failed:${proof.loop_id}`);
|
|
35
|
+
conflictLoops.add(proof.loop_id);
|
|
36
|
+
continue;
|
|
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');
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
appliedLoops.push(proof.loop_id);
|
|
48
|
+
for (const file of proof.changed_files)
|
|
49
|
+
changedFiles.add(file);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const result = {
|
|
53
|
+
schema: 'sks.loop-integration-merge.v1',
|
|
54
|
+
ok: blockers.length === 0,
|
|
55
|
+
applied_loops: appliedLoops,
|
|
56
|
+
conflict_loops: [...conflictLoops],
|
|
57
|
+
changed_files: [...changedFiles],
|
|
58
|
+
blockers: [...new Set(blockers)]
|
|
59
|
+
};
|
|
60
|
+
await writeJsonAtomic(loopIntegrationMergePath(input.root, input.plan.mission_id), { ...result, generated_at: nowIso() });
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
async function writeHandoff(root, loopId, detail) {
|
|
64
|
+
const contract = createRequestedScopeContract({
|
|
65
|
+
route: '$Loop',
|
|
66
|
+
userRequest: 'Write loop integration conflict handoff inside project .sneakoscope.',
|
|
67
|
+
projectRoot: root
|
|
68
|
+
});
|
|
69
|
+
const handoffPath = path.join(root, '.sneakoscope', `loop-integration-conflict-${safeArtifactId(loopId)}.txt`);
|
|
70
|
+
await guardedWriteFile(guardContextForRoute(root, contract, 'loop integration conflict handoff'), handoffPath, detail).catch(() => undefined);
|
|
71
|
+
}
|
|
72
|
+
function safeArtifactId(value) {
|
|
73
|
+
return String(value || 'unknown').replace(/[^A-Za-z0-9_.-]/g, '_').slice(0, 80) || 'unknown';
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=loop-integration-merge.js.map
|
|
@@ -1,30 +1,38 @@
|
|
|
1
1
|
import { loopOwnerLedgerPath } from './loop-artifacts.js';
|
|
2
2
|
import { nowIso, readJson, writeJsonAtomic } from '../fsx.js';
|
|
3
|
+
import { withFileLock } from '../locks/file-lock.js';
|
|
3
4
|
export async function acquireLoopLease(root, plan, node) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
5
|
+
return withLoopOwnerLedgerLock(root, plan.mission_id, async () => {
|
|
6
|
+
const blockers = await detectLoopLeaseConflictsUnderLock(root, plan.mission_id, node);
|
|
7
|
+
const lease = {
|
|
8
|
+
schema: 'sks.loop-lease.v1',
|
|
9
|
+
mission_id: plan.mission_id,
|
|
10
|
+
loop_id: node.loop_id,
|
|
11
|
+
owner_scope: node.owner_scope,
|
|
12
|
+
acquired_at: nowIso(),
|
|
13
|
+
expires_at: new Date(Date.now() + Math.max(60_000, node.budget.max_wall_ms)).toISOString(),
|
|
14
|
+
status: blockers.length ? 'conflict' : 'active',
|
|
15
|
+
worktree_id: node.worktree.required ? `sks-loop-${node.loop_id}` : null,
|
|
16
|
+
blockers
|
|
17
|
+
};
|
|
18
|
+
const ledger = await readLoopOwnerLedger(root, plan.mission_id);
|
|
19
|
+
const leases = ledger.leases.filter((row) => row.loop_id !== node.loop_id);
|
|
20
|
+
leases.push(lease);
|
|
21
|
+
await writeLoopOwnerLedger(root, plan.mission_id, leases);
|
|
22
|
+
return lease;
|
|
23
|
+
});
|
|
21
24
|
}
|
|
22
25
|
export async function releaseLoopLease(root, missionId, loopId) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
await withLoopOwnerLedgerLock(root, missionId, async () => {
|
|
27
|
+
const ledger = await readLoopOwnerLedger(root, missionId);
|
|
28
|
+
const leases = ledger.leases.map((lease) => lease.loop_id === loopId ? { ...lease, status: 'released' } : lease);
|
|
29
|
+
await writeLoopOwnerLedger(root, missionId, leases);
|
|
30
|
+
});
|
|
26
31
|
}
|
|
27
32
|
export async function detectLoopLeaseConflicts(root, missionId, node) {
|
|
33
|
+
return withLoopOwnerLedgerLock(root, missionId, () => detectLoopLeaseConflictsUnderLock(root, missionId, node));
|
|
34
|
+
}
|
|
35
|
+
async function detectLoopLeaseConflictsUnderLock(root, missionId, node) {
|
|
28
36
|
const ledger = await readLoopOwnerLedger(root, missionId);
|
|
29
37
|
const active = ledger.leases.filter((lease) => lease.status === 'active' && Date.parse(lease.expires_at) > Date.now());
|
|
30
38
|
const blockers = [];
|
|
@@ -49,6 +57,13 @@ export async function detectLoopLeaseConflicts(root, missionId, node) {
|
|
|
49
57
|
}
|
|
50
58
|
return [...new Set(blockers)];
|
|
51
59
|
}
|
|
60
|
+
async function withLoopOwnerLedgerLock(root, missionId, fn) {
|
|
61
|
+
return withFileLock({
|
|
62
|
+
lockPath: `${root}/.sneakoscope/locks/loop-owner-ledger-${missionId}.lock`,
|
|
63
|
+
timeoutMs: 30000,
|
|
64
|
+
staleMs: 5 * 60 * 1000
|
|
65
|
+
}, fn);
|
|
66
|
+
}
|
|
52
67
|
async function readLoopOwnerLedger(root, missionId) {
|
|
53
68
|
return readJson(loopOwnerLedgerPath(root, missionId), {
|
|
54
69
|
schema: 'sks.loop-owner-ledger.v1',
|