sneakoscope 3.1.0 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) 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/cli/install-helpers.js +6 -7
  8. package/dist/commands/zellij-slot-column-anchor.js +3 -1
  9. package/dist/commands/zellij-slot-pane.js +19 -2
  10. package/dist/core/agents/agent-janitor.js +10 -1
  11. package/dist/core/agents/agent-orchestrator.js +8 -2
  12. package/dist/core/agents/agent-proof-evidence.js +20 -0
  13. package/dist/core/agents/agent-runner-ollama.js +11 -4
  14. package/dist/core/agents/fast-mode-policy.js +7 -5
  15. package/dist/core/agents/intelligent-work-graph.js +93 -14
  16. package/dist/core/agents/native-cli-session-swarm.js +115 -9
  17. package/dist/core/agents/no-subagent-scaling-policy.js +10 -1
  18. package/dist/core/agents/official-subagent-helper-policy.js +62 -0
  19. package/dist/core/codex-app.js +0 -2
  20. package/dist/core/codex-control/codex-task-runner.js +9 -0
  21. package/dist/core/commands/fast-mode-command.js +1 -1
  22. package/dist/core/commands/loop-command.js +86 -13
  23. package/dist/core/commands/naruto-command.js +34 -21
  24. package/dist/core/commands/team-command.js +1 -0
  25. package/dist/core/commands/wiki-command.js +35 -1
  26. package/dist/core/fsx.js +1 -1
  27. package/dist/core/init.js +1 -2
  28. package/dist/core/locks/file-lock.js +88 -0
  29. package/dist/core/loops/loop-artifacts.js +54 -2
  30. package/dist/core/loops/loop-checkpoint.js +22 -0
  31. package/dist/core/loops/loop-concurrency-budget.js +55 -0
  32. package/dist/core/loops/loop-final-arbiter-contract.js +28 -0
  33. package/dist/core/loops/loop-finalizer.js +55 -7
  34. package/dist/core/loops/loop-fixture-policy.js +58 -0
  35. package/dist/core/loops/loop-gate-registry.js +96 -0
  36. package/dist/core/loops/loop-gate-runner.js +206 -17
  37. package/dist/core/loops/loop-gpt-final-arbiter.js +81 -0
  38. package/dist/core/loops/loop-integration-merge.js +80 -0
  39. package/dist/core/loops/loop-interrupt-registry.js +118 -0
  40. package/dist/core/loops/loop-lease.js +35 -20
  41. package/dist/core/loops/loop-merge-strategy.js +105 -0
  42. package/dist/core/loops/loop-mutation-ledger.js +103 -0
  43. package/dist/core/loops/loop-planner.js +36 -5
  44. package/dist/core/loops/loop-runtime-control.js +27 -0
  45. package/dist/core/loops/loop-runtime.js +254 -96
  46. package/dist/core/loops/loop-scheduler.js +14 -5
  47. package/dist/core/loops/loop-side-effect-scanner.js +91 -0
  48. package/dist/core/loops/loop-worker-prompts.js +43 -0
  49. package/dist/core/loops/loop-worker-runtime.js +281 -0
  50. package/dist/core/loops/loop-worktree-runtime.js +92 -0
  51. package/dist/core/naruto/naruto-finalizer.js +7 -2
  52. package/dist/core/naruto/naruto-loop-mesh.js +10 -1
  53. package/dist/core/proof/auto-finalize.js +3 -2
  54. package/dist/core/proof/proof-schema.js +6 -0
  55. package/dist/core/proof/proof-writer.js +5 -2
  56. package/dist/core/proof/root-cause-policy.js +70 -0
  57. package/dist/core/proof/route-adapter.js +18 -1
  58. package/dist/core/proof/route-finalizer.js +71 -6
  59. package/dist/core/proof/route-proof-gate.js +4 -0
  60. package/dist/core/release/release-gate-batch-runner.js +56 -10
  61. package/dist/core/release/release-gate-cache-v2.js +18 -3
  62. package/dist/core/release/release-gate-dag.js +121 -18
  63. package/dist/core/release/release-gate-node.js +2 -1
  64. package/dist/core/release/release-gate-resource-governor.js +27 -6
  65. package/dist/core/skills/core-skill-meta-update.js +24 -0
  66. package/dist/core/skills/core-skill-reflection.js +94 -0
  67. package/dist/core/skills/core-skill-trainer.js +103 -0
  68. package/dist/core/trust-kernel/completion-contract.js +4 -0
  69. package/dist/core/trust-kernel/route-contract.js +4 -1
  70. package/dist/core/version.js +1 -1
  71. package/dist/core/zellij/zellij-right-column-manager.js +13 -2
  72. package/dist/core/zellij/zellij-slot-column-anchor.js +40 -3
  73. package/dist/core/zellij/zellij-slot-pane-renderer.js +36 -11
  74. package/dist/core/zellij/zellij-slot-telemetry.js +96 -44
  75. package/dist/core/zellij/zellij-worker-pane-manager.js +42 -4
  76. package/dist/scripts/lib/native-cli-session-swarm-check-lib.js +14 -2
  77. package/dist/scripts/loop-directive-check-lib.js +225 -2
  78. package/dist/scripts/loop-hardening-check-lib.js +289 -0
  79. package/dist/scripts/loop-worker-fixture-child.js +53 -0
  80. package/dist/scripts/naruto-real-local-gpt-final-smoke.js +10 -1
  81. package/dist/scripts/prepublish-release-check-or-fast.js +38 -10
  82. package/dist/scripts/release-check-stamp.js +29 -4
  83. package/dist/scripts/release-gate-existence-audit.js +1 -0
  84. package/package.json +32 -2
@@ -1,29 +1,218 @@
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';
7
+ import { decideLoopFixturePolicy, writeLoopFixturePolicyDecision } from './loop-fixture-policy.js';
8
+ import { loopFinalArbiterGateContractRelativePath, writeLoopFinalArbiterGateContract } from './loop-final-arbiter-contract.js';
4
9
  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));
10
+ const selected = allGateIds(input.gates);
11
+ const failed = [];
12
+ const passed = [];
13
+ const skipped = [];
14
+ const blockers = [];
8
15
  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
- });
16
+ const result = await runOneGate(input, gate);
17
+ if (result.skipped)
18
+ skipped.push(gate);
19
+ else if (result.ok)
20
+ passed.push(gate);
21
+ else
22
+ failed.push(gate);
23
+ blockers.push(...result.blockers);
19
24
  }
20
25
  return {
21
26
  ok: failed.length === 0,
22
27
  selected_gates: selected,
23
28
  passed_gates: passed,
24
29
  failed_gates: failed,
25
- skipped_gates: selected.includes('release:check') ? ['release:check'] : [],
26
- blockers: failed.map((gate) => `gate_failed:${gate}`)
30
+ skipped_gates: skipped,
31
+ blockers
27
32
  };
28
33
  }
34
+ async function runOneGate(input, gateId) {
35
+ const started = Date.now();
36
+ const definition = await resolveLoopGate(input.root, gateId);
37
+ const fullReleaseCheckInsideLoop = gateId === 'release:check' && input.node.route !== '$Integration';
38
+ const unknown = !definition;
39
+ const packageJson = unknown ? await readJson(path.join(input.root, 'package.json'), null) : null;
40
+ const fixtureMode = process.env.SKS_LOOP_GATE_FIXTURE === '1';
41
+ const fixtureDecision = decideLoopFixturePolicy({
42
+ root: input.root,
43
+ missionId: input.missionId,
44
+ mode: 'gate',
45
+ requested: fixtureMode || (unknown && !packageJson)
46
+ });
47
+ if (fixtureDecision.requested)
48
+ await writeLoopFixturePolicyDecision(input.root, input.missionId, fixtureDecision).catch(() => undefined);
49
+ const skipUnknownFixtureGate = unknown && !packageJson && fixtureDecision.allowed;
50
+ const blockers = [
51
+ ...(unknown && !skipUnknownFixtureGate ? [`unknown_loop_gate:${gateId}`] : []),
52
+ ...(fullReleaseCheckInsideLoop ? ['full_release_check_inside_non_integration_loop'] : []),
53
+ ...(fixtureMode && !fixtureDecision.allowed ? [...fixtureDecision.blockers, 'loop_gate_fixture_forbidden_in_production'] : [])
54
+ ];
55
+ let ok = blockers.length === 0;
56
+ let skipped = skipUnknownFixtureGate;
57
+ let exitCode = null;
58
+ let stdoutTail = '';
59
+ let stderrTail = '';
60
+ let timedOut = false;
61
+ let handledBy;
62
+ let deferredContractPath;
63
+ let deferredReason;
64
+ if (definition && ok) {
65
+ if (fixtureMode && !fixtureDecision.allowed) {
66
+ ok = false;
67
+ }
68
+ else if (fixtureMode && definition.source !== 'builtin-pseudo') {
69
+ ok = true;
70
+ }
71
+ else if (definition.source === 'builtin-pseudo') {
72
+ const builtin = await runBuiltinGate(input.root, input.missionId, input.node.loop_id, definition, input.checkerArtifacts || []);
73
+ ok = builtin.ok;
74
+ skipped = builtin.skipped;
75
+ blockers.push(...builtin.blockers);
76
+ handledBy = builtin.handled_by;
77
+ deferredContractPath = builtin.deferred_contract_path;
78
+ deferredReason = builtin.deferred_reason;
79
+ }
80
+ else {
81
+ const command = definition.command;
82
+ const result = await runProcess(process.env.SHELL || '/bin/sh', ['-lc', command], {
83
+ cwd: input.root,
84
+ timeoutMs: input.timeoutMs || definition.timeout_ms,
85
+ maxOutputBytes: 512 * 1024,
86
+ env: {
87
+ SKS_LOOP_ID: input.node.loop_id,
88
+ SKS_MISSION_ID: input.missionId,
89
+ SKS_LOOP_GATE: gateId
90
+ }
91
+ });
92
+ exitCode = result.code;
93
+ stdoutTail = result.stdout.slice(-8000);
94
+ stderrTail = result.stderr.slice(-8000);
95
+ timedOut = result.timedOut;
96
+ ok = result.code === 0;
97
+ if (!ok)
98
+ blockers.push(`gate_command_failed:${gateId}:${result.code}`);
99
+ }
100
+ }
101
+ const artifact = {
102
+ schema: 'sks.loop-gate-result.v1',
103
+ ok,
104
+ gate_id: gateId,
105
+ loop_id: input.node.loop_id,
106
+ command: definition?.command || null,
107
+ source: definition?.source || null,
108
+ exit_code: exitCode,
109
+ duration_ms: Math.max(1, Date.now() - started),
110
+ stdout_tail: stdoutTail,
111
+ stderr_tail: stderrTail,
112
+ cached_allowed: definition?.cache_allowed ?? false,
113
+ fixture_mode: fixtureMode,
114
+ fixture_policy: fixtureDecision,
115
+ fixture_allowed_reason: fixtureDecision.allowed ? fixtureDecision.reason : null,
116
+ skipped,
117
+ handled_by: handledBy,
118
+ deferred_contract_path: deferredContractPath,
119
+ deferred_reason: deferredReason,
120
+ deferred_unknown_fixture_gate: skipUnknownFixtureGate,
121
+ timed_out: timedOut,
122
+ full_release_check_inside_loop: fullReleaseCheckInsideLoop,
123
+ generated_at: new Date().toISOString(),
124
+ blockers
125
+ };
126
+ await writeJsonAtomic(loopGatePath(input.root, input.missionId, input.node.loop_id, gateId), artifact);
127
+ return {
128
+ ok,
129
+ skipped,
130
+ blockers,
131
+ ...(handledBy ? { handled_by: handledBy } : {}),
132
+ ...(deferredContractPath ? { deferred_contract_path: deferredContractPath } : {}),
133
+ ...(deferredReason ? { deferred_reason: deferredReason } : {})
134
+ };
135
+ }
136
+ async function runBuiltinGate(root, missionId, loopId, definition, checkerArtifacts) {
137
+ if (definition.id === 'gpt:final-arbiter') {
138
+ await writeLoopFinalArbiterGateContract(root, missionId);
139
+ return {
140
+ ok: true,
141
+ skipped: true,
142
+ blockers: [],
143
+ handled_by: 'loop-finalizer',
144
+ deferred_contract_path: loopFinalArbiterGateContractRelativePath(missionId),
145
+ deferred_reason: 'gpt_final_arbiter_runs_after_integration_merge'
146
+ };
147
+ }
148
+ if (definition.id === 'human:handoff-required')
149
+ return { ok: false, skipped: false, blockers: ['human_handoff_required'] };
150
+ if (definition.id === 'loop:state-valid') {
151
+ const state = await readJson(loopStatePath(root, missionId, loopId), null);
152
+ return state?.schema === 'sks.loop-state.v1' ? { ok: true, skipped: false, blockers: [] } : { ok: false, skipped: false, blockers: ['loop_state_invalid'] };
153
+ }
154
+ if (definition.id === 'loop:budget-valid') {
155
+ const budget = await readJson(loopBudgetPath(root, missionId, loopId), null);
156
+ return budget && typeof budget === 'object' ? { ok: true, skipped: false, blockers: [] } : { ok: false, skipped: false, blockers: ['loop_budget_invalid'] };
157
+ }
158
+ if (definition.id === 'loop:checker-fresh-session') {
159
+ const artifacts = await Promise.all(checkerArtifacts.map((artifact) => readCheckerArtifact(root, missionId, artifact)));
160
+ const fresh = artifacts.some((artifact) => artifact?.fresh_session === true && artifact?.approved === true);
161
+ return fresh ? { ok: true, skipped: false, blockers: [] } : { ok: false, skipped: false, blockers: ['loop_checker_fresh_session_missing'] };
162
+ }
163
+ return { ok: false, skipped: false, blockers: [`unknown_builtin_gate:${definition.id}`] };
164
+ }
165
+ async function readCheckerArtifact(root, missionId, artifact) {
166
+ for (const candidate of checkerArtifactPathCandidates(root, missionId, artifact)) {
167
+ const readable = await checkerArtifactReadablePath(root, missionId, candidate);
168
+ if (!readable)
169
+ continue;
170
+ const row = await readJson(readable, null);
171
+ if (row)
172
+ return row;
173
+ }
174
+ return null;
175
+ }
176
+ function checkerArtifactPathCandidates(root, missionId, artifact) {
177
+ const raw = String(artifact || '').trim();
178
+ if (!raw)
179
+ return [];
180
+ const missionRoot = path.join(root, '.sneakoscope', 'missions', missionId);
181
+ const resolvedMissionRoot = path.resolve(missionRoot);
182
+ if (path.isAbsolute(raw)) {
183
+ return [path.resolve(raw)];
184
+ }
185
+ return uniqueStrings([
186
+ safeResolveWithin(path.join(resolvedMissionRoot, 'agents'), raw),
187
+ safeResolveWithin(resolvedMissionRoot, raw),
188
+ safeResolveWithin(path.join(resolvedMissionRoot, 'loops'), raw)
189
+ ].filter((value) => Boolean(value)));
190
+ }
191
+ function uniqueStrings(values) {
192
+ return [...new Set(values.filter(Boolean))];
193
+ }
194
+ async function checkerArtifactReadablePath(root, missionId, candidate) {
195
+ const resolvedMissionRoot = path.resolve(root, '.sneakoscope', 'missions', missionId);
196
+ const resolvedCandidate = path.resolve(candidate);
197
+ try {
198
+ const [realMissionRoot, realCandidate] = await Promise.all([
199
+ fsp.realpath(resolvedMissionRoot),
200
+ fsp.realpath(resolvedCandidate)
201
+ ]);
202
+ return isWithinPath(realMissionRoot, realCandidate) ? realCandidate : null;
203
+ }
204
+ catch {
205
+ return null;
206
+ }
207
+ }
208
+ function safeResolveWithin(base, target) {
209
+ const resolvedBase = path.resolve(base);
210
+ const resolvedTarget = path.resolve(resolvedBase, target);
211
+ return isWithinPath(resolvedBase, resolvedTarget) ? resolvedTarget : null;
212
+ }
213
+ function isWithinPath(base, target) {
214
+ const resolvedBase = path.resolve(base);
215
+ const resolvedTarget = path.resolve(target);
216
+ return resolvedTarget === resolvedBase || resolvedTarget.startsWith(`${resolvedBase}${path.sep}`);
217
+ }
29
218
  //# sourceMappingURL=loop-gate-runner.js.map
@@ -0,0 +1,81 @@
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
+ import { decideLoopFixturePolicy, writeLoopFixturePolicyDecision } from './loop-fixture-policy.js';
5
+ export async function runLoopGptFinalArbiter(input) {
6
+ const artifactPath = loopGptFinalArbiterPath(input.root, input.plan.mission_id);
7
+ const changedFiles = [...new Set([
8
+ ...input.integrationMerge.changed_files,
9
+ ...input.proofs.flatMap((proof) => proof.changed_files)
10
+ ])];
11
+ const reviewedLoopIds = input.proofs.map((proof) => proof.loop_id);
12
+ const fixtureRequested = process.env.SKS_LOOP_GPT_FINAL_FIXTURE === '1' || Boolean(input.forceVerdict);
13
+ const fixtureDecision = decideLoopFixturePolicy({
14
+ root: input.root,
15
+ missionId: input.plan.mission_id,
16
+ mode: 'gpt-final',
17
+ requested: fixtureRequested
18
+ });
19
+ if (fixtureRequested)
20
+ await writeLoopFixturePolicyDecision(input.root, input.plan.mission_id, fixtureDecision).catch(() => undefined);
21
+ if (fixtureRequested && !fixtureDecision.allowed) {
22
+ const result = buildResult(input.plan.mission_id, reviewedLoopIds, changedFiles, 'reject', [], artifactPath, [...fixtureDecision.blockers, 'loop_gpt_final_fixture_forbidden_in_production']);
23
+ await writeJsonAtomic(artifactPath, { ...result, generated_at: nowIso(), backend: 'fixture-forbidden', fixture_policy: fixtureDecision });
24
+ return result;
25
+ }
26
+ if (fixtureRequested) {
27
+ const verdict = input.forceVerdict || (process.env.SKS_LOOP_GPT_FINAL_REJECT === '1' ? 'reject' : 'approve');
28
+ const result = buildResult(input.plan.mission_id, reviewedLoopIds, changedFiles, verdict, verdict === 'approve' ? [] : ['fixture_revision_required'], artifactPath, []);
29
+ await writeJsonAtomic(artifactPath, { ...result, generated_at: nowIso(), backend: 'fixture', fixture_policy: fixtureDecision, fixture_allowed_reason: fixtureDecision.reason, side_effect_report: input.sideEffectReport || null });
30
+ return result;
31
+ }
32
+ if (input.sideEffectReport && !input.sideEffectReport.ok) {
33
+ const result = buildResult(input.plan.mission_id, reviewedLoopIds, changedFiles, 'reject', [], artifactPath, input.sideEffectReport.blockers);
34
+ await writeJsonAtomic(artifactPath, { ...result, generated_at: nowIso(), backend: 'side-effect-block', side_effect_report: input.sideEffectReport });
35
+ return result;
36
+ }
37
+ const arbiter = await runGptFinalArbiter({
38
+ schema: 'sks.gpt-final-arbiter-input.v1',
39
+ route: '$Loop',
40
+ mission_id: input.plan.mission_id,
41
+ local_mode: 'local-parallel-gpt-final',
42
+ local_outputs: input.proofs.map((proof) => ({
43
+ id: proof.loop_id,
44
+ backend: proof.maker_result.backend || 'loop-worker',
45
+ status: proof.status,
46
+ summary: proof.blockers.join(', ') || 'loop proof completed',
47
+ changed_files: proof.changed_files,
48
+ blockers: proof.blockers
49
+ })),
50
+ candidate_diff: JSON.stringify({ changed_files: changedFiles, integration_merge: input.integrationMerge, side_effect_report: input.sideEffectReport || null }),
51
+ verification_results: input.proofs.map((proof) => ({ id: proof.loop_id, ok: proof.status === 'completed', blockers: proof.blockers })),
52
+ side_effect_report: input.sideEffectReport || { schema: 'sks.loop-side-effect-report.v1', ok: true, changed_files: changedFiles },
53
+ mutation_ledger: { schema: 'sks.loop-mutation-ledger.v1', proofs: input.proofs, path: input.sideEffectReport?.mutation_ledger_path || null },
54
+ rollback_plan: { schema: 'sks.loop-rollback-plan.v1', strategy: 'git-worktree-or-human-handoff' }
55
+ }, { cwd: input.root, mutationLedgerRoot: `${input.root}/.sneakoscope/missions/${input.plan.mission_id}/loops/gpt-final-arbiter` });
56
+ const status = String(arbiter.result?.status || '');
57
+ const verdict = status === 'approved' || status === 'modified' ? 'approve' : status === 'needs_more_work' ? 'revise' : 'reject';
58
+ const blockers = stringArray(arbiter.blockers);
59
+ const result = buildResult(input.plan.mission_id, reviewedLoopIds, changedFiles, verdict, stringArray(arbiter.result?.required_followup_work), artifactPath, blockers);
60
+ await writeJsonAtomic(artifactPath, { ...result, generated_at: nowIso(), backend: arbiter.backend || null, side_effect_report: input.sideEffectReport || null, arbiter });
61
+ return result;
62
+ }
63
+ function buildResult(missionId, reviewedLoopIds, changedFiles, verdict, revisions, artifactPath, blockers) {
64
+ return {
65
+ schema: 'sks.loop-gpt-final-arbiter.v1',
66
+ ok: verdict === 'approve' && blockers.length === 0,
67
+ mission_id: missionId,
68
+ reviewed_loop_ids: reviewedLoopIds,
69
+ changed_files: changedFiles,
70
+ verdict,
71
+ required_revisions: revisions,
72
+ blockers,
73
+ artifact_path: artifactPath
74
+ };
75
+ }
76
+ function stringArray(value) {
77
+ if (!Array.isArray(value))
78
+ return [];
79
+ return value.map((item) => typeof item === 'string' ? item : JSON.stringify(item)).filter(Boolean);
80
+ }
81
+ //# sourceMappingURL=loop-gpt-final-arbiter.js.map
@@ -0,0 +1,80 @@
1
+ import path from 'node:path';
2
+ import { nowIso, writeJsonAtomic } from '../fsx.js';
3
+ import { guardedWriteFile, guardContextForRoute } from '../safety/mutation-guard.js';
4
+ import { createRequestedScopeContract } from '../safety/requested-scope-contract.js';
5
+ import { loopIntegrationMergePath } from './loop-artifacts.js';
6
+ import { mergeSingleLoopWorktree } from './loop-merge-strategy.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
+ const mergeAttempts = {};
15
+ for (const proof of completed) {
16
+ for (const file of proof.changed_files) {
17
+ const previous = owners.get(file);
18
+ if (previous && previous !== proof.loop_id) {
19
+ blockers.push(`loop_integration_file_conflict:${file}:${previous}:${proof.loop_id}`);
20
+ conflictLoops.add(previous);
21
+ conflictLoops.add(proof.loop_id);
22
+ }
23
+ else {
24
+ owners.set(file, proof.loop_id);
25
+ }
26
+ }
27
+ }
28
+ if (!blockers.length) {
29
+ for (const proof of completed) {
30
+ const worktreePath = proof.worktree.path;
31
+ if (!worktreePath)
32
+ continue;
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}`);
37
+ conflictLoops.add(proof.loop_id);
38
+ await writeHandoff(input.root, proof.loop_id, merge.blockers.join('\n') || 'loop merge strategy failed');
39
+ continue;
40
+ }
41
+ appliedLoops.push(proof.loop_id);
42
+ for (const file of merge.changed_files)
43
+ changedFiles.add(file);
44
+ }
45
+ }
46
+ const result = {
47
+ schema: 'sks.loop-integration-merge.v1',
48
+ ok: blockers.length === 0,
49
+ applied_loops: appliedLoops,
50
+ conflict_loops: [...conflictLoops],
51
+ changed_files: [...changedFiles],
52
+ blockers: [...new Set(blockers)],
53
+ merge_attempts: mergeAttempts,
54
+ strategy_summary: summarizeStrategies(Object.values(mergeAttempts))
55
+ };
56
+ await writeJsonAtomic(loopIntegrationMergePath(input.root, input.plan.mission_id), { ...result, generated_at: nowIso() });
57
+ return result;
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
+ }
68
+ async function writeHandoff(root, loopId, detail) {
69
+ const contract = createRequestedScopeContract({
70
+ route: '$Loop',
71
+ userRequest: 'Write loop integration conflict handoff inside project .sneakoscope.',
72
+ projectRoot: root
73
+ });
74
+ const handoffPath = path.join(root, '.sneakoscope', `loop-integration-conflict-${safeArtifactId(loopId)}.txt`);
75
+ await guardedWriteFile(guardContextForRoute(root, contract, 'loop integration conflict handoff'), handoffPath, detail).catch(() => undefined);
76
+ }
77
+ function safeArtifactId(value) {
78
+ return String(value || 'unknown').replace(/[^A-Za-z0-9_.-]/g, '_').slice(0, 80) || 'unknown';
79
+ }
80
+ //# sourceMappingURL=loop-integration-merge.js.map
@@ -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
@@ -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',