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.
- package/README.md +1 -1
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/crates/sks-core/src/main.rs +1 -1
- package/dist/.sks-build-stamp.json +4 -4
- package/dist/bin/sks.js +1 -1
- package/dist/cli/install-helpers.js +6 -7
- package/dist/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 +8 -2
- package/dist/core/agents/agent-proof-evidence.js +20 -0
- package/dist/core/agents/agent-runner-ollama.js +11 -4
- package/dist/core/agents/fast-mode-policy.js +7 -5
- package/dist/core/agents/intelligent-work-graph.js +93 -14
- package/dist/core/agents/native-cli-session-swarm.js +115 -9
- package/dist/core/agents/no-subagent-scaling-policy.js +10 -1
- package/dist/core/agents/official-subagent-helper-policy.js +62 -0
- package/dist/core/codex-app.js +0 -2
- package/dist/core/codex-control/codex-task-runner.js +9 -0
- package/dist/core/commands/fast-mode-command.js +1 -1
- package/dist/core/commands/loop-command.js +86 -13
- package/dist/core/commands/naruto-command.js +34 -21
- package/dist/core/commands/team-command.js +1 -0
- package/dist/core/commands/wiki-command.js +35 -1
- package/dist/core/fsx.js +1 -1
- package/dist/core/init.js +1 -2
- package/dist/core/locks/file-lock.js +88 -0
- package/dist/core/loops/loop-artifacts.js +54 -2
- package/dist/core/loops/loop-checkpoint.js +22 -0
- package/dist/core/loops/loop-concurrency-budget.js +55 -0
- package/dist/core/loops/loop-final-arbiter-contract.js +28 -0
- package/dist/core/loops/loop-finalizer.js +55 -7
- package/dist/core/loops/loop-fixture-policy.js +58 -0
- package/dist/core/loops/loop-gate-registry.js +96 -0
- package/dist/core/loops/loop-gate-runner.js +206 -17
- package/dist/core/loops/loop-gpt-final-arbiter.js +81 -0
- package/dist/core/loops/loop-integration-merge.js +80 -0
- package/dist/core/loops/loop-interrupt-registry.js +118 -0
- package/dist/core/loops/loop-lease.js +35 -20
- package/dist/core/loops/loop-merge-strategy.js +105 -0
- package/dist/core/loops/loop-mutation-ledger.js +103 -0
- package/dist/core/loops/loop-planner.js +36 -5
- package/dist/core/loops/loop-runtime-control.js +27 -0
- package/dist/core/loops/loop-runtime.js +254 -96
- package/dist/core/loops/loop-scheduler.js +14 -5
- package/dist/core/loops/loop-side-effect-scanner.js +91 -0
- package/dist/core/loops/loop-worker-prompts.js +43 -0
- package/dist/core/loops/loop-worker-runtime.js +281 -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 +10 -1
- package/dist/core/proof/auto-finalize.js +3 -2
- 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-finalizer.js +71 -6
- 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 +121 -18
- 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/lib/native-cli-session-swarm-check-lib.js +14 -2
- package/dist/scripts/loop-directive-check-lib.js +225 -2
- package/dist/scripts/loop-hardening-check-lib.js +289 -0
- package/dist/scripts/loop-worker-fixture-child.js +53 -0
- package/dist/scripts/naruto-real-local-gpt-final-smoke.js +10 -1
- package/dist/scripts/prepublish-release-check-or-fast.js +38 -10
- package/dist/scripts/release-check-stamp.js +29 -4
- package/dist/scripts/release-gate-existence-audit.js +1 -0
- package/package.json +32 -2
|
@@ -1,29 +1,218 @@
|
|
|
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';
|
|
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)
|
|
6
|
-
const failed =
|
|
7
|
-
const passed =
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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:
|
|
26
|
-
blockers
|
|
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
|
-
|
|
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',
|