sneakoscope 3.1.1 → 3.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/crates/sks-core/src/main.rs +1 -1
- package/dist/.sks-build-stamp.json +4 -4
- package/dist/bin/sks.js +1 -1
- package/dist/cli/install-helpers.js +6 -7
- package/dist/core/agents/agent-orchestrator.js +7 -2
- package/dist/core/agents/agent-proof-evidence.js +20 -0
- package/dist/core/agents/fast-mode-policy.js +7 -5
- package/dist/core/agents/intelligent-work-graph.js +93 -14
- package/dist/core/agents/native-cli-session-swarm.js +46 -0
- package/dist/core/agents/no-subagent-scaling-policy.js +10 -1
- package/dist/core/agents/official-subagent-helper-policy.js +62 -0
- package/dist/core/codex-app.js +0 -2
- package/dist/core/commands/fast-mode-command.js +1 -1
- package/dist/core/commands/loop-command.js +35 -3
- package/dist/core/commands/naruto-command.js +10 -6
- package/dist/core/commands/wiki-command.js +35 -1
- package/dist/core/fsx.js +1 -1
- package/dist/core/init.js +1 -2
- package/dist/core/loops/loop-artifacts.js +21 -0
- package/dist/core/loops/loop-concurrency-budget.js +55 -0
- package/dist/core/loops/loop-final-arbiter-contract.js +28 -0
- package/dist/core/loops/loop-finalizer.js +25 -3
- package/dist/core/loops/loop-fixture-policy.js +58 -0
- package/dist/core/loops/loop-gate-runner.js +48 -7
- package/dist/core/loops/loop-gpt-final-arbiter.js +26 -6
- package/dist/core/loops/loop-integration-merge.js +20 -15
- package/dist/core/loops/loop-interrupt-registry.js +118 -0
- package/dist/core/loops/loop-merge-strategy.js +105 -0
- package/dist/core/loops/loop-mutation-ledger.js +103 -0
- package/dist/core/loops/loop-runtime-control.js +2 -0
- package/dist/core/loops/loop-runtime.js +6 -3
- package/dist/core/loops/loop-scheduler.js +2 -2
- package/dist/core/loops/loop-side-effect-scanner.js +91 -0
- package/dist/core/loops/loop-worker-runtime.js +35 -29
- package/dist/core/naruto/naruto-loop-mesh.js +3 -0
- package/dist/core/proof/auto-finalize.js +3 -2
- package/dist/core/proof/route-finalizer.js +71 -6
- package/dist/core/release/release-gate-dag.js +56 -1
- package/dist/core/version.js +1 -1
- package/dist/scripts/lib/native-cli-session-swarm-check-lib.js +14 -2
- package/dist/scripts/loop-directive-check-lib.js +1 -1
- package/dist/scripts/loop-hardening-check-lib.js +289 -0
- package/dist/scripts/prepublish-release-check-or-fast.js +38 -10
- package/dist/scripts/release-check-stamp.js +29 -4
- package/dist/scripts/release-gate-existence-audit.js +1 -0
- package/package.json +28 -1
|
@@ -2,7 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import { printJson } from '../../cli/output.js';
|
|
3
3
|
import { createMission, findLatestMission, loadMission, setCurrent } from '../mission.js';
|
|
4
4
|
import { readJson, sksRoot } from '../fsx.js';
|
|
5
|
-
import { loopLatestCheckpointPath, loopPlanPath, loopProofPath, loopRoot } from '../loops/loop-artifacts.js';
|
|
5
|
+
import { loopActiveWorkerHandlesPath, loopIntegrationMergePath, loopLatestCheckpointPath, loopPlanPath, loopProofPath, loopRoot, loopSideEffectReportPath } from '../loops/loop-artifacts.js';
|
|
6
6
|
import { finalizeLoopGraph } from '../loops/loop-finalizer.js';
|
|
7
7
|
import { readLoopGraphProof } from '../loops/loop-observability.js';
|
|
8
8
|
import { planLoopsFromRequest } from '../loops/loop-planner.js';
|
|
@@ -83,7 +83,10 @@ async function loopStatus(args) {
|
|
|
83
83
|
const states = await Promise.all((plan?.graph.nodes || []).map((node) => readJson(path.join(loopRoot(root, missionId), node.loop_id, 'loop-state.json'), null)));
|
|
84
84
|
const proofs = await Promise.all((plan?.graph.nodes || []).map((node) => readJson(loopProofPath(root, missionId, node.loop_id), null)));
|
|
85
85
|
const checkpoints = await Promise.all((plan?.graph.nodes || []).map((node) => readJson(loopLatestCheckpointPath(root, missionId, node.loop_id), null)));
|
|
86
|
-
const
|
|
86
|
+
const activeWorkerHandles = await readJsonl(loopActiveWorkerHandlesPath(root, missionId));
|
|
87
|
+
const merge = await readJson(loopIntegrationMergePath(root, missionId), null);
|
|
88
|
+
const sideEffects = await readJson(loopSideEffectReportPath(root, missionId), null);
|
|
89
|
+
const result = { schema: 'sks.loop-status-command.v1', mission_id: missionId, plan_ok: Boolean(plan && plan.blockers.length === 0), graph: proof, states, proofs, checkpoints, active_worker_handles: activeWorkerHandles, merge, side_effects: sideEffects };
|
|
87
90
|
if (flag(args, '--json'))
|
|
88
91
|
return printJson(result);
|
|
89
92
|
console.log(`Loop status: ${missionId}`);
|
|
@@ -95,8 +98,16 @@ async function loopStatus(args) {
|
|
|
95
98
|
const gates = nodeProof?.gate_result ? `${nodeProof.gate_result.passed_gates.length}/${nodeProof.gate_result.selected_gates.length}` : '-';
|
|
96
99
|
const worktree = nodeProof?.worktree?.id || state.acting_on?.worktree_id || '-';
|
|
97
100
|
const resumable = checkpoint?.resumable ? `resumable:${checkpoint.phase}` : 'resumable:-';
|
|
98
|
-
|
|
101
|
+
const active = activeWorkerHandles.filter((row) => row.loop_id === loopId && row.status === 'running').length;
|
|
102
|
+
const mergeStatus = merge?.merge_attempts?.[loopId]?.selected_strategy || '-';
|
|
103
|
+
console.log(` ${loopId.padEnd(18)} ${String(state.status).padEnd(10)} backend ${String(backend).padEnd(24)} workers ${String(active).padEnd(2)} gates ${gates.padEnd(5)} worktree ${String(worktree).padEnd(18)} merge ${String(mergeStatus).padEnd(14)} ${resumable}`);
|
|
99
104
|
}
|
|
105
|
+
if (merge)
|
|
106
|
+
console.log(`Merge: ${merge.ok ? 'passed' : 'blocked'} strategy=${JSON.stringify(merge.strategy_summary || {})}`);
|
|
107
|
+
if (sideEffects)
|
|
108
|
+
console.log(`Side effects: ${sideEffects.ok ? 'passed' : 'blocked'} blockers=${sideEffects.blockers?.length || 0}`);
|
|
109
|
+
if (proof?.gpt_final_arbiter)
|
|
110
|
+
console.log(`Final arbiter: ${proof.gpt_final_arbiter.verdict || 'unknown'} handled_by=${proof.gpt_final_arbiter.handled_by || 'loop-finalizer'}`);
|
|
100
111
|
}
|
|
101
112
|
async function loopProof(args) {
|
|
102
113
|
const root = await sksRoot();
|
|
@@ -109,6 +120,12 @@ async function loopProof(args) {
|
|
|
109
120
|
if (flag(args, '--json'))
|
|
110
121
|
return printJson(proof);
|
|
111
122
|
console.log(renderLoopProofSummary(proof));
|
|
123
|
+
if (proof.integration_merge)
|
|
124
|
+
console.log(`Merge: ${proof.integration_merge.ok ? 'passed' : 'blocked'} ${JSON.stringify(proof.integration_merge.strategy_summary || {})}`);
|
|
125
|
+
if (proof.side_effect_report)
|
|
126
|
+
console.log(`Side effects: ${proof.side_effect_report.ok ? 'passed' : 'blocked'} ${proof.side_effect_report.blockers?.join(', ') || 'none'}`);
|
|
127
|
+
if (proof.gpt_final_arbiter)
|
|
128
|
+
console.log(`Final arbiter: ${proof.gpt_final_arbiter.verdict || 'unknown'} contract=${proof.gpt_final_arbiter.gate_contract_path || 'missing'}`);
|
|
112
129
|
}
|
|
113
130
|
async function loopGraph(args) {
|
|
114
131
|
const root = await sksRoot();
|
|
@@ -173,4 +190,19 @@ async function resolveLoopMission(root, arg) {
|
|
|
173
190
|
function normalizeParallelism(value) {
|
|
174
191
|
return value === 'safe' || value === 'extreme' ? value : 'balanced';
|
|
175
192
|
}
|
|
193
|
+
async function readJsonl(file) {
|
|
194
|
+
const text = await import('node:fs/promises').then((fs) => fs.readFile(file, 'utf8')).catch(() => '');
|
|
195
|
+
return String(text).split(/\r?\n/)
|
|
196
|
+
.map((line) => line.trim())
|
|
197
|
+
.filter(Boolean)
|
|
198
|
+
.map((line) => {
|
|
199
|
+
try {
|
|
200
|
+
return JSON.parse(line);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
.filter(Boolean);
|
|
207
|
+
}
|
|
176
208
|
//# sourceMappingURL=loop-command.js.map
|
|
@@ -5,6 +5,7 @@ import { runNativeAgentOrchestrator } from '../agents/agent-orchestrator.js';
|
|
|
5
5
|
import { classifyOllamaWorkerSlice } from '../agents/agent-runner-ollama.js';
|
|
6
6
|
import { buildNarutoCloneRoster, systemSafeNarutoConcurrency } from '../agents/agent-roster.js';
|
|
7
7
|
import { DEFAULT_NARUTO_CLONES, MAX_NARUTO_AGENT_COUNT } from '../agents/agent-schema.js';
|
|
8
|
+
import { normalizeServiceTier } from '../agents/fast-mode-policy.js';
|
|
8
9
|
import { resolveOllamaWorkerConfig } from '../agents/ollama-worker-config.js';
|
|
9
10
|
import { attachZellijSessionInteractive, launchZellijLayout } from '../zellij/zellij-launcher.js';
|
|
10
11
|
import { maybePromptZellijUpdateForLaunch } from '../zellij/zellij-update.js';
|
|
@@ -380,10 +381,9 @@ async function narutoRun(parsed) {
|
|
|
380
381
|
workerPlacement: parsed.json || parsed.noOpenZellij ? 'process' : 'zellij-pane',
|
|
381
382
|
zellijPaneWorker: true,
|
|
382
383
|
zellijVisiblePaneCap: zellijVisiblePanes,
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
noFast: false,
|
|
384
|
+
...(parsed.fastMode === undefined ? {} : { fastMode: parsed.fastMode }),
|
|
385
|
+
...(parsed.serviceTier === undefined ? {} : { serviceTier: parsed.serviceTier }),
|
|
386
|
+
noFast: parsed.noFast,
|
|
387
387
|
writeMode: writeCapable ? parsed.writeMode || 'parallel' : 'off',
|
|
388
388
|
applyPatches: parsed.applyPatches,
|
|
389
389
|
dryRunPatches: parsed.dryRunPatches,
|
|
@@ -797,6 +797,10 @@ function parseNarutoArgs(args = []) {
|
|
|
797
797
|
const applyPatches = hasFlag(args, '--apply-patches');
|
|
798
798
|
const dryRunPatches = hasFlag(args, '--dry-run-patches') || hasFlag(args, '--dry-run-patch');
|
|
799
799
|
const maxWriteAgents = Math.max(0, Math.floor(Number(readOption(args, '--max-write-agents', '0')) || 0));
|
|
800
|
+
const explicitServiceTier = String(readOption(args, '--service-tier', '') || '');
|
|
801
|
+
const serviceTier = normalizeServiceTier(explicitServiceTier, null) || undefined;
|
|
802
|
+
const fastMode = hasFlag(args, '--no-fast') || serviceTier === 'standard' ? false : hasFlag(args, '--fast') ? true : undefined;
|
|
803
|
+
const noFast = hasFlag(args, '--no-fast');
|
|
800
804
|
const positionalMission = action === 'dashboard' || action === 'workers' || action === 'status' || action === 'proof'
|
|
801
805
|
? positionalArgs(rest, new Set()).find((arg) => /^latest$|^M-/.test(arg))
|
|
802
806
|
: null;
|
|
@@ -808,9 +812,9 @@ function parseNarutoArgs(args = []) {
|
|
|
808
812
|
const smoke = hasFlag(args, '--smoke');
|
|
809
813
|
const parallelism = normalizeParallelism(readOption(args, '--parallelism', 'extreme'));
|
|
810
814
|
const messages = normalizeMessages(readOption(args, '--messages', '8'));
|
|
811
|
-
const valueFlags = new Set(['--clones', '--agents', '--work-items', '--concurrency', '--target-active-slots', '--backend', '--write-mode', '--max-write-agents', '--mission', '--mission-id', '--ollama-model', '--local-model-model', '--ollama-base-url', '--local-model-base-url', '--parallelism', '--messages']);
|
|
815
|
+
const valueFlags = new Set(['--clones', '--agents', '--work-items', '--concurrency', '--target-active-slots', '--backend', '--write-mode', '--max-write-agents', '--service-tier', '--mission', '--mission-id', '--ollama-model', '--local-model-model', '--ollama-base-url', '--local-model-base-url', '--parallelism', '--messages']);
|
|
812
816
|
const prompt = positionalArgs(rest, valueFlags).join(' ').trim() || 'Naruto shadow clone swarm run';
|
|
813
|
-
return { action, prompt, clones, workItems, concurrency, backend, backendExplicit, mock, real, readonly, ollamaEnabled: useOllama && !noOllama, noOllama, ollamaModel, ollamaBaseUrl, writeMode, applyPatches, dryRunPatches, maxWriteAgents, json, missionId, noOpenZellij, attach, smoke, parallelism, messages };
|
|
817
|
+
return { action, prompt, clones, workItems, concurrency, backend, backendExplicit, mock, real, readonly, ollamaEnabled: useOllama && !noOllama, noOllama, ollamaModel, ollamaBaseUrl, writeMode, applyPatches, dryRunPatches, maxWriteAgents, fastMode, serviceTier, noFast, json, missionId, noOpenZellij, attach, smoke, parallelism, messages };
|
|
814
818
|
}
|
|
815
819
|
function normalizeParallelism(value) {
|
|
816
820
|
const text = String(value || 'extreme').toLowerCase();
|
|
@@ -120,7 +120,17 @@ export async function wikiCommand(sub, args = []) {
|
|
|
120
120
|
const { id, dir } = await createMission(root, { mode: 'wiki', prompt: 'sks wiki refresh' });
|
|
121
121
|
const gate = { schema_version: 1, passed: validation.result.ok, ok: validation.result.ok, context_pack: '.sneakoscope/wiki/context-pack.json', anchors: wikiAnchorCount(pack.wiki), voxels: wikiVoxelRowCount(pack.wiki) };
|
|
122
122
|
await writeJsonAtomic(path.join(dir, 'wiki-gate.json'), gate);
|
|
123
|
-
await maybeFinalizeRoute(root, {
|
|
123
|
+
await maybeFinalizeRoute(root, {
|
|
124
|
+
missionId: id,
|
|
125
|
+
route: '$Wiki',
|
|
126
|
+
gateFile: 'wiki-gate.json',
|
|
127
|
+
gate,
|
|
128
|
+
artifacts: ['wiki-gate.json', 'completion-proof.json'],
|
|
129
|
+
statusHint: validation.result.ok ? 'verified_partial' : 'blocked',
|
|
130
|
+
blockers: validation.result.ok ? [] : validation.result.issues,
|
|
131
|
+
command: { cmd: 'sks wiki refresh', status: exitCode },
|
|
132
|
+
failureAnalysis: wikiRefreshFailureAnalysis(validation.result, gate)
|
|
133
|
+
});
|
|
124
134
|
}
|
|
125
135
|
if (flag(args, '--json')) {
|
|
126
136
|
process.exitCode = exitCode;
|
|
@@ -197,6 +207,30 @@ export async function wikiCommand(sub, args = []) {
|
|
|
197
207
|
console.error('Usage: sks wiki coords|pack|refresh|publish|rebuild-index|rebuild-summary|validate|validate-shared|wrongness|image-ingest|anchor-add|relation-add|image-validate|image-summary');
|
|
198
208
|
process.exitCode = 1;
|
|
199
209
|
}
|
|
210
|
+
function wikiRefreshFailureAnalysis(validationResult, gate = {}) {
|
|
211
|
+
if (validationResult?.ok) {
|
|
212
|
+
return {
|
|
213
|
+
status: 'complete',
|
|
214
|
+
root_cause: 'Wiki refresh intentionally finalizes as verified_partial because a context-pack refresh can verify TriWiki schema and anchors, but active wrongness memory may still prevent full trust verification.',
|
|
215
|
+
corrective_action: 'The Wiki route records the fresh context-pack validation, wiki gate counts, and completion-proof evidence while keeping the route below full verified status until wrongness memory is separately resolved.',
|
|
216
|
+
evidence: [
|
|
217
|
+
'.sneakoscope/wiki/context-pack.json',
|
|
218
|
+
'wiki-gate.json',
|
|
219
|
+
{ anchors: gate.anchors || 0, voxels: gate.voxels || 0 }
|
|
220
|
+
]
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
status: 'complete',
|
|
225
|
+
root_cause: `Wiki context-pack validation failed during refresh: ${(validationResult?.issues || []).join(', ') || 'unknown validation issue'}.`,
|
|
226
|
+
corrective_action: 'The Wiki route finalized as blocked and preserved validation issues in the completion proof so the context pack can be corrected before any completion claim.',
|
|
227
|
+
evidence: [
|
|
228
|
+
'.sneakoscope/wiki/context-pack.json',
|
|
229
|
+
'wiki-gate.json',
|
|
230
|
+
...(validationResult?.issues || [])
|
|
231
|
+
]
|
|
232
|
+
};
|
|
233
|
+
}
|
|
200
234
|
async function wikiImageIngest(args = []) {
|
|
201
235
|
const root = await sksRoot();
|
|
202
236
|
const imagePath = args.find((arg, i) => i >= 0 && !String(arg).startsWith('--'));
|
package/dist/core/fsx.js
CHANGED
|
@@ -5,7 +5,7 @@ import os from 'node:os';
|
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
export const PACKAGE_VERSION = '3.1.
|
|
8
|
+
export const PACKAGE_VERSION = '3.1.2';
|
|
9
9
|
export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
|
|
10
10
|
export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
|
|
11
11
|
export function nowIso() {
|
package/dist/core/init.js
CHANGED
|
@@ -591,7 +591,6 @@ export async function initProject(root, opts = {}) {
|
|
|
591
591
|
// upgrade never clobbers a user's chosen model/service_tier. model_reasoning_effort
|
|
592
592
|
// is intentionally left untouched at the top level.
|
|
593
593
|
next = upsertTopLevelTomlStringIfAbsent(next, 'model', 'gpt-5.5');
|
|
594
|
-
next = upsertTopLevelTomlStringIfAbsent(next, 'service_tier', 'fast');
|
|
595
594
|
next = upsertTopLevelTomlBooleanIfAbsent(next, 'suppress_unstable_features_warning', true);
|
|
596
595
|
// Codex App feature flags: SET-IF-ABSENT only (see note above).
|
|
597
596
|
for (const flag of MANAGED_CODEX_FEATURE_FLAGS) {
|
|
@@ -599,7 +598,7 @@ export async function initProject(root, opts = {}) {
|
|
|
599
598
|
}
|
|
600
599
|
next = upsertTomlTableKeyIfAbsent(next, 'user.fast_mode', 'visible = true');
|
|
601
600
|
next = upsertTomlTableKeyIfAbsent(next, 'user.fast_mode', 'enabled = true');
|
|
602
|
-
next =
|
|
601
|
+
next = removeTomlTableKey(next, 'user.fast_mode', 'default_profile');
|
|
603
602
|
next = upsertTomlTableKeyIfAbsent(next, 'agents', 'max_threads = 6');
|
|
604
603
|
next = upsertTomlTableKeyIfAbsent(next, 'agents', 'max_depth = 1');
|
|
605
604
|
for (const block of managedCodexConfigBlocks()) {
|
|
@@ -36,6 +36,27 @@ export function loopIntegrationMergePath(root, missionId) {
|
|
|
36
36
|
export function loopGptFinalArbiterPath(root, missionId) {
|
|
37
37
|
return path.join(loopRoot(root, missionId), 'loop-gpt-final-arbiter.json');
|
|
38
38
|
}
|
|
39
|
+
export function loopFinalArbiterGateContractPath(root, missionId) {
|
|
40
|
+
return path.join(loopRoot(root, missionId), 'gpt-final-arbiter-gate-contract.json');
|
|
41
|
+
}
|
|
42
|
+
export function loopFixturePolicyPath(root, missionId) {
|
|
43
|
+
return path.join(loopRoot(root, missionId), 'fixture-policy.json');
|
|
44
|
+
}
|
|
45
|
+
export function loopMutationLedgerPath(root, missionId) {
|
|
46
|
+
return path.join(loopRoot(root, missionId), 'mutation-ledger.jsonl');
|
|
47
|
+
}
|
|
48
|
+
export function loopSideEffectReportPath(root, missionId) {
|
|
49
|
+
return path.join(loopRoot(root, missionId), 'loop-side-effect-report.json');
|
|
50
|
+
}
|
|
51
|
+
export function loopActiveWorkerHandlesPath(root, missionId) {
|
|
52
|
+
return path.join(loopRoot(root, missionId), 'active-worker-handles.jsonl');
|
|
53
|
+
}
|
|
54
|
+
export function loopInterruptResultPath(root, missionId) {
|
|
55
|
+
return path.join(loopRoot(root, missionId), 'interrupt-result.json');
|
|
56
|
+
}
|
|
57
|
+
export function loopConcurrencyBudgetPath(root, missionId) {
|
|
58
|
+
return path.join(loopRoot(root, missionId), 'concurrency-budget.json');
|
|
59
|
+
}
|
|
39
60
|
export function loopKillRequestPath(root, missionId) {
|
|
40
61
|
return path.join(loopRoot(root, missionId), 'kill-request.json');
|
|
41
62
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import { writeJsonAtomic } from '../fsx.js';
|
|
3
|
+
import { loopConcurrencyBudgetPath } from './loop-artifacts.js';
|
|
4
|
+
export function computeLoopConcurrencyBudget(input) {
|
|
5
|
+
const env = input.env || process.env;
|
|
6
|
+
const cores = Math.max(1, os.cpus().length || 1);
|
|
7
|
+
const requestedLoops = input.parallelism === 'safe' ? 2 : input.parallelism === 'extreme' ? Math.min(16, cores) : Math.min(8, cores);
|
|
8
|
+
const envLoops = positiveInt(env.SKS_LOOP_MAX_ACTIVE_LOOPS);
|
|
9
|
+
const envWorkers = positiveInt(env.SKS_LOOP_MAX_ACTIVE_WORKERS);
|
|
10
|
+
const envModelCalls = positiveInt(env.SKS_LOOP_MAX_MODEL_CALLS);
|
|
11
|
+
const maxActiveLoops = envLoops || requestedLoops;
|
|
12
|
+
const maxActiveWorkers = envWorkers || (input.parallelism === 'safe' ? Math.min(8, cores) : input.parallelism === 'extreme' ? Math.min(32, Math.max(4, cores * 2)) : Math.min(16, Math.max(4, cores)));
|
|
13
|
+
const maxModelCalls = envModelCalls || Math.max(1, Math.min(maxActiveWorkers, input.plan.global_budget.max_model_calls || maxActiveWorkers));
|
|
14
|
+
let remainingWorkers = maxActiveWorkers;
|
|
15
|
+
let remainingModelCalls = maxModelCalls;
|
|
16
|
+
const perLoop = input.plan.graph.nodes.map((node) => {
|
|
17
|
+
const requested = Math.max(1, node.maker.worker_count + node.checker.worker_count);
|
|
18
|
+
const fairShare = Math.max(1, Math.floor(maxActiveWorkers / Math.max(1, input.plan.graph.nodes.length)));
|
|
19
|
+
const allocation = Math.min(requested, Math.max(1, fairShare), Math.max(1, remainingWorkers));
|
|
20
|
+
const maker = Math.min(node.maker.worker_count, Math.max(1, Math.ceil(allocation / 2)));
|
|
21
|
+
const checker = Math.min(node.checker.worker_count, Math.max(0, allocation - maker));
|
|
22
|
+
const modelCalls = Math.min(Math.max(1, node.budget.max_model_calls), Math.max(1, remainingModelCalls));
|
|
23
|
+
remainingWorkers = Math.max(0, remainingWorkers - maker - checker);
|
|
24
|
+
remainingModelCalls = Math.max(0, remainingModelCalls - modelCalls);
|
|
25
|
+
return {
|
|
26
|
+
loop_id: node.loop_id,
|
|
27
|
+
maker_workers: maker,
|
|
28
|
+
checker_workers: checker,
|
|
29
|
+
model_call_budget: modelCalls
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
return {
|
|
33
|
+
schema: 'sks.loop-concurrency-budget.v1',
|
|
34
|
+
mission_id: input.plan.mission_id,
|
|
35
|
+
max_active_loops: maxActiveLoops,
|
|
36
|
+
max_active_workers: maxActiveWorkers,
|
|
37
|
+
max_model_calls: maxModelCalls,
|
|
38
|
+
per_loop_worker_budget: perLoop,
|
|
39
|
+
headroom_workers: Math.max(0, maxActiveWorkers - perLoop.reduce((sum, row) => sum + row.maker_workers + row.checker_workers, 0)),
|
|
40
|
+
blockers: []
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export async function writeLoopConcurrencyBudget(root, budget) {
|
|
44
|
+
await writeJsonAtomic(loopConcurrencyBudgetPath(root, budget.mission_id), { ...budget, generated_at: new Date().toISOString() });
|
|
45
|
+
}
|
|
46
|
+
export function loopWorkerBudgetFor(budget, loopId, phase, requested) {
|
|
47
|
+
const row = budget.per_loop_worker_budget.find((item) => item.loop_id === loopId);
|
|
48
|
+
const allowed = phase === 'maker' ? row?.maker_workers : row?.checker_workers;
|
|
49
|
+
return Math.max(1, Math.min(Math.max(1, requested), Math.max(1, allowed || requested)));
|
|
50
|
+
}
|
|
51
|
+
function positiveInt(value) {
|
|
52
|
+
const number = Number(value);
|
|
53
|
+
return Number.isFinite(number) && number >= 1 ? Math.floor(number) : null;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=loop-concurrency-budget.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { writeJsonAtomic } from '../fsx.js';
|
|
2
|
+
import { loopFinalArbiterGateContractPath, loopGptFinalArbiterPath } from './loop-artifacts.js';
|
|
3
|
+
export async function writeLoopFinalArbiterGateContract(root, missionId) {
|
|
4
|
+
const contract = buildLoopFinalArbiterGateContract(root, missionId);
|
|
5
|
+
await writeJsonAtomic(loopFinalArbiterGateContractPath(root, missionId), { ...contract, generated_at: new Date().toISOString() });
|
|
6
|
+
return contract;
|
|
7
|
+
}
|
|
8
|
+
export function buildLoopFinalArbiterGateContract(root, missionId) {
|
|
9
|
+
return {
|
|
10
|
+
schema: 'sks.loop-final-arbiter-gate-contract.v1',
|
|
11
|
+
mission_id: missionId,
|
|
12
|
+
gate_id: 'gpt:final-arbiter',
|
|
13
|
+
handled_by: 'loop-finalizer',
|
|
14
|
+
gate_runner_status: 'deferred',
|
|
15
|
+
finalizer_artifact_path: relativeMissionArtifact(root, loopGptFinalArbiterPath(root, missionId)),
|
|
16
|
+
required_when: ['source_mutation_exists', 'selected_gates_include_gpt_final_arbiter'],
|
|
17
|
+
production_fixture_allowed: false
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function loopFinalArbiterGateContractRelativePath(missionId) {
|
|
21
|
+
return `.sneakoscope/missions/${missionId}/loops/gpt-final-arbiter-gate-contract.json`;
|
|
22
|
+
}
|
|
23
|
+
function relativeMissionArtifact(root, absolute) {
|
|
24
|
+
const normalizedRoot = root.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
25
|
+
const normalized = absolute.replace(/\\/g, '/');
|
|
26
|
+
return normalized.startsWith(`${normalizedRoot}/`) ? normalized.slice(normalizedRoot.length + 1) : normalized;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=loop-final-arbiter-contract.js.map
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { readJson, writeJsonAtomic } from '../fsx.js';
|
|
2
2
|
import { loopGraphProofPath, loopProofPath } from './loop-artifacts.js';
|
|
3
|
+
import { writeLoopFinalArbiterGateContract } from './loop-final-arbiter-contract.js';
|
|
3
4
|
import { runLoopGptFinalArbiter } from './loop-gpt-final-arbiter.js';
|
|
4
5
|
import { mergeLoopWorktrees } from './loop-integration-merge.js';
|
|
5
6
|
import { graphProofFromLoopProofs } from './loop-scheduler.js';
|
|
7
|
+
import { buildLoopSideEffectReport } from './loop-side-effect-scanner.js';
|
|
6
8
|
export async function finalizeLoopGraph(input) {
|
|
7
9
|
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)));
|
|
8
10
|
const realProofs = proofs.filter((proof) => Boolean(proof));
|
|
@@ -20,14 +22,26 @@ export async function finalizeLoopGraph(input) {
|
|
|
20
22
|
});
|
|
21
23
|
const anyHandoff = realProofs.some((proof) => proof.handoff.required);
|
|
22
24
|
const anySourceMutation = realProofs.some((proof) => proof.changed_files.some((file) => !file.startsWith('.sneakoscope/')));
|
|
25
|
+
const selectedGptFinal = graph.gates.selected.includes('gpt:final-arbiter');
|
|
26
|
+
const contract = anySourceMutation || selectedGptFinal
|
|
27
|
+
? await writeLoopFinalArbiterGateContract(input.root, input.plan.mission_id)
|
|
28
|
+
: null;
|
|
29
|
+
const sideEffectReport = await buildLoopSideEffectReport({
|
|
30
|
+
root: input.root,
|
|
31
|
+
missionId: input.plan.mission_id,
|
|
32
|
+
proofs: realProofs,
|
|
33
|
+
integrationMerge
|
|
34
|
+
});
|
|
23
35
|
const arbiter = anySourceMutation
|
|
24
|
-
? await runLoopGptFinalArbiter({ root: input.root, plan: input.plan, proofs: realProofs, integrationMerge })
|
|
36
|
+
? await runLoopGptFinalArbiter({ root: input.root, plan: input.plan, proofs: realProofs, integrationMerge, sideEffectReport })
|
|
25
37
|
: null;
|
|
26
38
|
const blockers = [
|
|
27
39
|
...graph.blockers,
|
|
28
40
|
...(anyHandoff ? ['loop_handoff_required'] : []),
|
|
29
41
|
...(integrationMerge.ok ? [] : integrationMerge.blockers),
|
|
42
|
+
...(sideEffectReport.ok ? [] : sideEffectReport.blockers),
|
|
30
43
|
...(anySourceMutation && !arbiter ? ['gpt_final_arbiter_missing'] : []),
|
|
44
|
+
...(selectedGptFinal && anySourceMutation && (!contract || !arbiter) ? ['gpt_final_arbiter_contract_unfulfilled'] : []),
|
|
31
45
|
...(arbiter && !arbiter.ok ? ['gpt_final_arbiter_not_approved', ...arbiter.blockers] : [])
|
|
32
46
|
];
|
|
33
47
|
const finalGraph = {
|
|
@@ -38,13 +52,21 @@ export async function finalizeLoopGraph(input) {
|
|
|
38
52
|
ok: integrationMerge.ok,
|
|
39
53
|
artifact_path: `.sneakoscope/missions/${input.plan.mission_id}/loops/integration-merge.json`,
|
|
40
54
|
applied_loops: integrationMerge.applied_loops,
|
|
41
|
-
conflict_loops: integrationMerge.conflict_loops
|
|
55
|
+
conflict_loops: integrationMerge.conflict_loops,
|
|
56
|
+
...(integrationMerge.strategy_summary ? { strategy_summary: integrationMerge.strategy_summary } : {})
|
|
57
|
+
},
|
|
58
|
+
side_effect_report: {
|
|
59
|
+
ok: sideEffectReport.ok,
|
|
60
|
+
artifact_path: `.sneakoscope/missions/${input.plan.mission_id}/loops/loop-side-effect-report.json`,
|
|
61
|
+
blockers: sideEffectReport.blockers
|
|
42
62
|
},
|
|
43
63
|
...(arbiter ? {
|
|
44
64
|
gpt_final_arbiter: {
|
|
45
65
|
ok: arbiter.ok,
|
|
46
66
|
artifact_path: arbiter.artifact_path,
|
|
47
|
-
verdict: arbiter.verdict
|
|
67
|
+
verdict: arbiter.verdict,
|
|
68
|
+
gate_contract_path: `.sneakoscope/missions/${input.plan.mission_id}/loops/gpt-final-arbiter-gate-contract.json`,
|
|
69
|
+
handled_by: 'loop-finalizer'
|
|
48
70
|
}
|
|
49
71
|
} : {})
|
|
50
72
|
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { writeJsonAtomic } from '../fsx.js';
|
|
4
|
+
import { loopFixturePolicyPath } from './loop-artifacts.js';
|
|
5
|
+
export function decideLoopFixturePolicy(input) {
|
|
6
|
+
const env = input.env || process.env;
|
|
7
|
+
const argv = input.argv || process.argv;
|
|
8
|
+
const scriptPath = argv.find((arg) => /(?:^|[\\/])(?:dist|src)[\\/]scripts[\\/]/.test(arg)) || argv[1] || '';
|
|
9
|
+
const scriptName = path.basename(scriptPath);
|
|
10
|
+
const scriptIsCheck = /(?:^|[\\/])(?:dist|src)[\\/]scripts[\\/]/.test(scriptPath)
|
|
11
|
+
&& /(check|blackbox)/.test(scriptName);
|
|
12
|
+
const missionIsCheck = /^M-check-/.test(input.missionId);
|
|
13
|
+
const tempRoot = isUnderTempRoot(input.root);
|
|
14
|
+
const explicitTestEnv = env.NODE_ENV === 'test'
|
|
15
|
+
|| env.SKS_TEST_RUNTIME_FIXTURE_ALLOWED === '1'
|
|
16
|
+
|| env.VITEST_WORKER_ID !== undefined
|
|
17
|
+
|| env.JEST_WORKER_ID !== undefined
|
|
18
|
+
|| env.NODE_V8_COVERAGE !== undefined;
|
|
19
|
+
const commandText = argv.join(' ');
|
|
20
|
+
const productionCommand = /\bsks\s+(?:loop\s+run|goal|naruto)\b/.test(commandText);
|
|
21
|
+
const requestedByEnv = env.SKS_LOOP_GATE_FIXTURE === '1'
|
|
22
|
+
|| env.SKS_LOOP_RUNTIME_FIXTURE === '1'
|
|
23
|
+
|| env.SKS_LOOP_GPT_FINAL_FIXTURE === '1';
|
|
24
|
+
const allowReasons = [
|
|
25
|
+
scriptIsCheck ? 'release_check_script' : null,
|
|
26
|
+
missionIsCheck ? 'check_mission_id' : null,
|
|
27
|
+
tempRoot ? 'temp_project_root' : null,
|
|
28
|
+
explicitTestEnv ? 'test_environment' : null
|
|
29
|
+
].filter((value) => Boolean(value));
|
|
30
|
+
const allowed = input.requested && allowReasons.length > 0 && !productionCommand;
|
|
31
|
+
const productionLike = !scriptIsCheck && !missionIsCheck && !tempRoot && !explicitTestEnv;
|
|
32
|
+
const blockers = input.requested && !allowed
|
|
33
|
+
? [
|
|
34
|
+
'loop_fixture_forbidden_in_production',
|
|
35
|
+
`loop_${input.mode.replace(/-/g, '_')}_fixture_forbidden_in_production`,
|
|
36
|
+
...(productionCommand ? ['loop_fixture_forbidden_for_production_command'] : []),
|
|
37
|
+
...(requestedByEnv && productionLike ? ['loop_fixture_env_without_allowed_reason'] : [])
|
|
38
|
+
]
|
|
39
|
+
: [];
|
|
40
|
+
return {
|
|
41
|
+
schema: 'sks.loop-fixture-policy-decision.v1',
|
|
42
|
+
allowed,
|
|
43
|
+
mode: input.mode,
|
|
44
|
+
requested: input.requested,
|
|
45
|
+
production_like: productionLike || productionCommand,
|
|
46
|
+
reason: allowed ? allowReasons.join('+') : input.requested ? 'fixture_requires_check_or_test_context' : 'fixture_not_requested',
|
|
47
|
+
blockers: [...new Set(blockers)]
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export async function writeLoopFixturePolicyDecision(root, missionId, decision) {
|
|
51
|
+
await writeJsonAtomic(loopFixturePolicyPath(root, missionId), { ...decision, generated_at: new Date().toISOString() });
|
|
52
|
+
}
|
|
53
|
+
export function isUnderTempRoot(root) {
|
|
54
|
+
const normalizedRoot = path.resolve(root);
|
|
55
|
+
const tempRoot = path.resolve(os.tmpdir());
|
|
56
|
+
return normalizedRoot === tempRoot || normalizedRoot.startsWith(`${tempRoot}${path.sep}`);
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=loop-fixture-policy.js.map
|
|
@@ -4,6 +4,8 @@ import { readJson, runProcess, writeJsonAtomic } from '../fsx.js';
|
|
|
4
4
|
import { allGateIds } from './loop-schema.js';
|
|
5
5
|
import { loopBudgetPath, loopGatePath, loopStatePath } from './loop-artifacts.js';
|
|
6
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';
|
|
7
9
|
export async function runLoopGates(input) {
|
|
8
10
|
const selected = allGateIds(input.gates);
|
|
9
11
|
const failed = [];
|
|
@@ -35,10 +37,20 @@ async function runOneGate(input, gateId) {
|
|
|
35
37
|
const fullReleaseCheckInsideLoop = gateId === 'release:check' && input.node.route !== '$Integration';
|
|
36
38
|
const unknown = !definition;
|
|
37
39
|
const packageJson = unknown ? await readJson(path.join(input.root, 'package.json'), null) : null;
|
|
38
|
-
const
|
|
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;
|
|
39
50
|
const blockers = [
|
|
40
51
|
...(unknown && !skipUnknownFixtureGate ? [`unknown_loop_gate:${gateId}`] : []),
|
|
41
|
-
...(fullReleaseCheckInsideLoop ? ['full_release_check_inside_non_integration_loop'] : [])
|
|
52
|
+
...(fullReleaseCheckInsideLoop ? ['full_release_check_inside_non_integration_loop'] : []),
|
|
53
|
+
...(fixtureMode && !fixtureDecision.allowed ? [...fixtureDecision.blockers, 'loop_gate_fixture_forbidden_in_production'] : [])
|
|
42
54
|
];
|
|
43
55
|
let ok = blockers.length === 0;
|
|
44
56
|
let skipped = skipUnknownFixtureGate;
|
|
@@ -46,9 +58,14 @@ async function runOneGate(input, gateId) {
|
|
|
46
58
|
let stdoutTail = '';
|
|
47
59
|
let stderrTail = '';
|
|
48
60
|
let timedOut = false;
|
|
49
|
-
|
|
61
|
+
let handledBy;
|
|
62
|
+
let deferredContractPath;
|
|
63
|
+
let deferredReason;
|
|
50
64
|
if (definition && ok) {
|
|
51
|
-
if (fixtureMode &&
|
|
65
|
+
if (fixtureMode && !fixtureDecision.allowed) {
|
|
66
|
+
ok = false;
|
|
67
|
+
}
|
|
68
|
+
else if (fixtureMode && definition.source !== 'builtin-pseudo') {
|
|
52
69
|
ok = true;
|
|
53
70
|
}
|
|
54
71
|
else if (definition.source === 'builtin-pseudo') {
|
|
@@ -56,6 +73,9 @@ async function runOneGate(input, gateId) {
|
|
|
56
73
|
ok = builtin.ok;
|
|
57
74
|
skipped = builtin.skipped;
|
|
58
75
|
blockers.push(...builtin.blockers);
|
|
76
|
+
handledBy = builtin.handled_by;
|
|
77
|
+
deferredContractPath = builtin.deferred_contract_path;
|
|
78
|
+
deferredReason = builtin.deferred_reason;
|
|
59
79
|
}
|
|
60
80
|
else {
|
|
61
81
|
const command = definition.command;
|
|
@@ -91,7 +111,12 @@ async function runOneGate(input, gateId) {
|
|
|
91
111
|
stderr_tail: stderrTail,
|
|
92
112
|
cached_allowed: definition?.cache_allowed ?? false,
|
|
93
113
|
fixture_mode: fixtureMode,
|
|
114
|
+
fixture_policy: fixtureDecision,
|
|
115
|
+
fixture_allowed_reason: fixtureDecision.allowed ? fixtureDecision.reason : null,
|
|
94
116
|
skipped,
|
|
117
|
+
handled_by: handledBy,
|
|
118
|
+
deferred_contract_path: deferredContractPath,
|
|
119
|
+
deferred_reason: deferredReason,
|
|
95
120
|
deferred_unknown_fixture_gate: skipUnknownFixtureGate,
|
|
96
121
|
timed_out: timedOut,
|
|
97
122
|
full_release_check_inside_loop: fullReleaseCheckInsideLoop,
|
|
@@ -99,11 +124,27 @@ async function runOneGate(input, gateId) {
|
|
|
99
124
|
blockers
|
|
100
125
|
};
|
|
101
126
|
await writeJsonAtomic(loopGatePath(input.root, input.missionId, input.node.loop_id, gateId), artifact);
|
|
102
|
-
return {
|
|
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
|
+
};
|
|
103
135
|
}
|
|
104
136
|
async function runBuiltinGate(root, missionId, loopId, definition, checkerArtifacts) {
|
|
105
|
-
if (definition.id === 'gpt:final-arbiter')
|
|
106
|
-
|
|
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
|
+
}
|
|
107
148
|
if (definition.id === 'human:handoff-required')
|
|
108
149
|
return { ok: false, skipped: false, blockers: ['human_handoff_required'] };
|
|
109
150
|
if (definition.id === 'loop:state-valid') {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { runGptFinalArbiter } from '../codex-control/gpt-final-arbiter.js';
|
|
2
2
|
import { nowIso, writeJsonAtomic } from '../fsx.js';
|
|
3
3
|
import { loopGptFinalArbiterPath } from './loop-artifacts.js';
|
|
4
|
+
import { decideLoopFixturePolicy, writeLoopFixturePolicyDecision } from './loop-fixture-policy.js';
|
|
4
5
|
export async function runLoopGptFinalArbiter(input) {
|
|
5
6
|
const artifactPath = loopGptFinalArbiterPath(input.root, input.plan.mission_id);
|
|
6
7
|
const changedFiles = [...new Set([
|
|
@@ -8,10 +9,29 @@ export async function runLoopGptFinalArbiter(input) {
|
|
|
8
9
|
...input.proofs.flatMap((proof) => proof.changed_files)
|
|
9
10
|
])];
|
|
10
11
|
const reviewedLoopIds = input.proofs.map((proof) => proof.loop_id);
|
|
11
|
-
|
|
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) {
|
|
12
27
|
const verdict = input.forceVerdict || (process.env.SKS_LOOP_GPT_FINAL_REJECT === '1' ? 'reject' : 'approve');
|
|
13
28
|
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' });
|
|
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 });
|
|
15
35
|
return result;
|
|
16
36
|
}
|
|
17
37
|
const arbiter = await runGptFinalArbiter({
|
|
@@ -27,17 +47,17 @@ export async function runLoopGptFinalArbiter(input) {
|
|
|
27
47
|
changed_files: proof.changed_files,
|
|
28
48
|
blockers: proof.blockers
|
|
29
49
|
})),
|
|
30
|
-
candidate_diff: JSON.stringify({ changed_files: changedFiles, integration_merge: input.integrationMerge }),
|
|
50
|
+
candidate_diff: JSON.stringify({ changed_files: changedFiles, integration_merge: input.integrationMerge, side_effect_report: input.sideEffectReport || null }),
|
|
31
51
|
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 },
|
|
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 },
|
|
34
54
|
rollback_plan: { schema: 'sks.loop-rollback-plan.v1', strategy: 'git-worktree-or-human-handoff' }
|
|
35
55
|
}, { cwd: input.root, mutationLedgerRoot: `${input.root}/.sneakoscope/missions/${input.plan.mission_id}/loops/gpt-final-arbiter` });
|
|
36
56
|
const status = String(arbiter.result?.status || '');
|
|
37
57
|
const verdict = status === 'approved' || status === 'modified' ? 'approve' : status === 'needs_more_work' ? 'revise' : 'reject';
|
|
38
58
|
const blockers = stringArray(arbiter.blockers);
|
|
39
59
|
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 });
|
|
60
|
+
await writeJsonAtomic(artifactPath, { ...result, generated_at: nowIso(), backend: arbiter.backend || null, side_effect_report: input.sideEffectReport || null, arbiter });
|
|
41
61
|
return result;
|
|
42
62
|
}
|
|
43
63
|
function buildResult(missionId, reviewedLoopIds, changedFiles, verdict, revisions, artifactPath, blockers) {
|