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.
Files changed (60) hide show
  1. package/README.md +1 -1
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/crates/sks-core/src/main.rs +1 -1
  5. package/dist/.sks-build-stamp.json +4 -4
  6. package/dist/bin/sks.js +1 -1
  7. package/dist/commands/zellij-slot-column-anchor.js +3 -1
  8. package/dist/commands/zellij-slot-pane.js +19 -2
  9. package/dist/core/agents/agent-janitor.js +10 -1
  10. package/dist/core/agents/agent-orchestrator.js +1 -0
  11. package/dist/core/agents/agent-runner-ollama.js +11 -4
  12. package/dist/core/agents/native-cli-session-swarm.js +69 -9
  13. package/dist/core/codex-control/codex-task-runner.js +9 -0
  14. package/dist/core/commands/loop-command.js +54 -13
  15. package/dist/core/commands/naruto-command.js +26 -17
  16. package/dist/core/commands/team-command.js +1 -0
  17. package/dist/core/fsx.js +1 -1
  18. package/dist/core/locks/file-lock.js +88 -0
  19. package/dist/core/loops/loop-artifacts.js +33 -2
  20. package/dist/core/loops/loop-checkpoint.js +22 -0
  21. package/dist/core/loops/loop-finalizer.js +33 -7
  22. package/dist/core/loops/loop-gate-registry.js +96 -0
  23. package/dist/core/loops/loop-gate-runner.js +165 -17
  24. package/dist/core/loops/loop-gpt-final-arbiter.js +61 -0
  25. package/dist/core/loops/loop-integration-merge.js +75 -0
  26. package/dist/core/loops/loop-lease.js +35 -20
  27. package/dist/core/loops/loop-planner.js +36 -5
  28. package/dist/core/loops/loop-runtime-control.js +25 -0
  29. package/dist/core/loops/loop-runtime.js +248 -93
  30. package/dist/core/loops/loop-scheduler.js +12 -3
  31. package/dist/core/loops/loop-worker-prompts.js +43 -0
  32. package/dist/core/loops/loop-worker-runtime.js +275 -0
  33. package/dist/core/loops/loop-worktree-runtime.js +92 -0
  34. package/dist/core/naruto/naruto-finalizer.js +7 -2
  35. package/dist/core/naruto/naruto-loop-mesh.js +7 -1
  36. package/dist/core/proof/proof-schema.js +6 -0
  37. package/dist/core/proof/proof-writer.js +5 -2
  38. package/dist/core/proof/root-cause-policy.js +70 -0
  39. package/dist/core/proof/route-adapter.js +18 -1
  40. package/dist/core/proof/route-proof-gate.js +4 -0
  41. package/dist/core/release/release-gate-batch-runner.js +56 -10
  42. package/dist/core/release/release-gate-cache-v2.js +18 -3
  43. package/dist/core/release/release-gate-dag.js +65 -17
  44. package/dist/core/release/release-gate-node.js +2 -1
  45. package/dist/core/release/release-gate-resource-governor.js +27 -6
  46. package/dist/core/skills/core-skill-meta-update.js +24 -0
  47. package/dist/core/skills/core-skill-reflection.js +94 -0
  48. package/dist/core/skills/core-skill-trainer.js +103 -0
  49. package/dist/core/trust-kernel/completion-contract.js +4 -0
  50. package/dist/core/trust-kernel/route-contract.js +4 -1
  51. package/dist/core/version.js +1 -1
  52. package/dist/core/zellij/zellij-right-column-manager.js +13 -2
  53. package/dist/core/zellij/zellij-slot-column-anchor.js +40 -3
  54. package/dist/core/zellij/zellij-slot-pane-renderer.js +36 -11
  55. package/dist/core/zellij/zellij-slot-telemetry.js +96 -44
  56. package/dist/core/zellij/zellij-worker-pane-manager.js +42 -4
  57. package/dist/scripts/loop-directive-check-lib.js +225 -2
  58. package/dist/scripts/loop-worker-fixture-child.js +53 -0
  59. package/dist/scripts/naruto-real-local-gpt-final-smoke.js +10 -1
  60. package/package.json +5 -2
@@ -1,9 +1,10 @@
1
1
  import path from 'node:path';
2
2
  export function loopRoot(root, missionId) {
3
- return path.join(root, '.sneakoscope', 'missions', missionId, 'loops');
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 path.join(loopRoot(root, missionId), loopId);
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.length > 0);
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 && !anyHandoff && (!anySourceMutation || graph.gates.selected.includes('gpt:final-arbiter')),
19
- blockers: [
20
- ...graph.blockers,
21
- ...(anyHandoff ? ['loop_handoff_required'] : []),
22
- ...(anySourceMutation && !graph.gates.selected.includes('gpt:final-arbiter') ? ['gpt_final_arbiter_missing'] : [])
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 { writeJsonAtomic } from '../fsx.js';
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).filter((gate) => gate !== 'release:check');
6
- const failed = selected.filter((gate) => gate === 'human:handoff-required');
7
- const passed = selected.filter((gate) => !failed.includes(gate));
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 writeJsonAtomic(loopGatePath(input.root, input.missionId, input.node.loop_id, gate), {
10
- schema: 'sks.loop-gate-result.v1',
11
- ok: !failed.includes(gate),
12
- gate_id: gate,
13
- loop_id: input.node.loop_id,
14
- timeout_ms: input.timeoutMs || 120000,
15
- cached_allowed: true,
16
- full_release_check_inside_loop: false,
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: selected.includes('release:check') ? ['release:check'] : [],
26
- blockers: failed.map((gate) => `gate_failed:${gate}`)
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
- const blockers = await detectLoopLeaseConflicts(root, plan.mission_id, node);
5
- const lease = {
6
- schema: 'sks.loop-lease.v1',
7
- mission_id: plan.mission_id,
8
- loop_id: node.loop_id,
9
- owner_scope: node.owner_scope,
10
- acquired_at: nowIso(),
11
- expires_at: new Date(Date.now() + Math.max(60_000, node.budget.max_wall_ms)).toISOString(),
12
- status: blockers.length ? 'conflict' : 'active',
13
- worktree_id: node.worktree.required ? `sks-loop-${node.loop_id}` : null,
14
- blockers
15
- };
16
- const ledger = await readLoopOwnerLedger(root, plan.mission_id);
17
- const leases = ledger.leases.filter((row) => row.loop_id !== node.loop_id);
18
- leases.push(lease);
19
- await writeLoopOwnerLedger(root, plan.mission_id, leases);
20
- return lease;
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
- const ledger = await readLoopOwnerLedger(root, missionId);
24
- const leases = ledger.leases.map((lease) => lease.loop_id === loopId ? { ...lease, status: 'released' } : lease);
25
- await writeLoopOwnerLedger(root, missionId, leases);
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',