sneakoscope 3.1.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/crates/sks-core/src/main.rs +1 -1
- package/dist/.sks-build-stamp.json +4 -4
- package/dist/bin/sks.js +1 -1
- package/dist/commands/zellij-slot-column-anchor.js +3 -1
- package/dist/commands/zellij-slot-pane.js +19 -2
- package/dist/core/agents/agent-janitor.js +10 -1
- package/dist/core/agents/agent-orchestrator.js +1 -0
- package/dist/core/agents/agent-runner-ollama.js +11 -4
- package/dist/core/agents/native-cli-session-swarm.js +69 -9
- package/dist/core/codex-control/codex-task-runner.js +9 -0
- package/dist/core/commands/loop-command.js +54 -13
- package/dist/core/commands/naruto-command.js +26 -17
- package/dist/core/commands/team-command.js +1 -0
- package/dist/core/fsx.js +1 -1
- package/dist/core/locks/file-lock.js +88 -0
- package/dist/core/loops/loop-artifacts.js +33 -2
- package/dist/core/loops/loop-checkpoint.js +22 -0
- package/dist/core/loops/loop-finalizer.js +33 -7
- package/dist/core/loops/loop-gate-registry.js +96 -0
- package/dist/core/loops/loop-gate-runner.js +165 -17
- package/dist/core/loops/loop-gpt-final-arbiter.js +61 -0
- package/dist/core/loops/loop-integration-merge.js +75 -0
- package/dist/core/loops/loop-lease.js +35 -20
- package/dist/core/loops/loop-planner.js +36 -5
- package/dist/core/loops/loop-runtime-control.js +25 -0
- package/dist/core/loops/loop-runtime.js +248 -93
- package/dist/core/loops/loop-scheduler.js +12 -3
- package/dist/core/loops/loop-worker-prompts.js +43 -0
- package/dist/core/loops/loop-worker-runtime.js +275 -0
- package/dist/core/loops/loop-worktree-runtime.js +92 -0
- package/dist/core/naruto/naruto-finalizer.js +7 -2
- package/dist/core/naruto/naruto-loop-mesh.js +7 -1
- package/dist/core/proof/proof-schema.js +6 -0
- package/dist/core/proof/proof-writer.js +5 -2
- package/dist/core/proof/root-cause-policy.js +70 -0
- package/dist/core/proof/route-adapter.js +18 -1
- package/dist/core/proof/route-proof-gate.js +4 -0
- package/dist/core/release/release-gate-batch-runner.js +56 -10
- package/dist/core/release/release-gate-cache-v2.js +18 -3
- package/dist/core/release/release-gate-dag.js +65 -17
- package/dist/core/release/release-gate-node.js +2 -1
- package/dist/core/release/release-gate-resource-governor.js +27 -6
- package/dist/core/skills/core-skill-meta-update.js +24 -0
- package/dist/core/skills/core-skill-reflection.js +94 -0
- package/dist/core/skills/core-skill-trainer.js +103 -0
- package/dist/core/trust-kernel/completion-contract.js +4 -0
- package/dist/core/trust-kernel/route-contract.js +4 -1
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-right-column-manager.js +13 -2
- package/dist/core/zellij/zellij-slot-column-anchor.js +40 -3
- package/dist/core/zellij/zellij-slot-pane-renderer.js +36 -11
- package/dist/core/zellij/zellij-slot-telemetry.js +96 -44
- package/dist/core/zellij/zellij-worker-pane-manager.js +42 -4
- package/dist/scripts/loop-directive-check-lib.js +225 -2
- package/dist/scripts/loop-worker-fixture-child.js +53 -0
- package/dist/scripts/naruto-real-local-gpt-final-smoke.js +10 -1
- package/package.json +5 -2
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { runNativeAgentOrchestrator } from '../agents/agent-orchestrator.js';
|
|
5
|
+
import { ensureDir, nowIso, readJson, runProcess, writeJsonAtomic } from '../fsx.js';
|
|
6
|
+
import { loopNodeRoot } from './loop-artifacts.js';
|
|
7
|
+
import { buildLoopCheckerPrompt, buildLoopMakerPrompt } from './loop-worker-prompts.js';
|
|
8
|
+
export async function runLoopMakerWorkers(input) {
|
|
9
|
+
return runLoopWorkers({ ...input, phase: 'maker' });
|
|
10
|
+
}
|
|
11
|
+
export async function runLoopCheckerWorkers(input) {
|
|
12
|
+
return runLoopWorkers({ ...input, phase: 'checker', noMutation: true });
|
|
13
|
+
}
|
|
14
|
+
async function runLoopWorkers(input) {
|
|
15
|
+
if (shouldUseFixture(input))
|
|
16
|
+
return runLoopWorkerFixture(input);
|
|
17
|
+
return runLoopWorkerNative(input);
|
|
18
|
+
}
|
|
19
|
+
// `noMutation` used to force fixture mode here, which silently turned EVERY
|
|
20
|
+
// checker run into a deterministic fixture (checkers always pass
|
|
21
|
+
// noMutation: true for read-only semantics) — real model verification never
|
|
22
|
+
// happened. Fixture mode is now an explicit, separate test-only signal.
|
|
23
|
+
function shouldUseFixture(input) {
|
|
24
|
+
const requested = input.fixture === true || process.env.SKS_LOOP_RUNTIME_FIXTURE === '1';
|
|
25
|
+
if (!requested)
|
|
26
|
+
return false;
|
|
27
|
+
const allowed = loopFixtureAllowed(input);
|
|
28
|
+
if (!allowed.ok) {
|
|
29
|
+
throw new Error(`loop_fixture_runtime_forbidden:${allowed.reason}`);
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
async function runLoopWorkerNative(input) {
|
|
34
|
+
const prompt = input.phase === 'maker'
|
|
35
|
+
? buildLoopMakerPrompt({ plan: input.plan, node: input.node, worktreePath: input.worktree?.path || null })
|
|
36
|
+
: buildLoopCheckerPrompt({ plan: input.plan, node: input.node, makerArtifacts: input.makerArtifacts || [] });
|
|
37
|
+
const workerCount = input.phase === 'maker' ? input.node.maker.worker_count : input.node.checker.worker_count;
|
|
38
|
+
const workGraph = buildLoopNarutoWorkGraph(input, workerCount);
|
|
39
|
+
// Root-cause-1 fix: keep the ORCHESTRATOR root on the MAIN repo (input.root), not the
|
|
40
|
+
// loop worktree. All zellij/right-column/slot-telemetry state derives from the orchestrator
|
|
41
|
+
// root, so anchoring it on input.root makes the SLOTS snapshot land under
|
|
42
|
+
// <main repo>/.sneakoscope/missions/<missionId>/... where the main session's anchor + slot
|
|
43
|
+
// renderer panes watch it (previously it landed under the worktree and went permanently stale).
|
|
44
|
+
// The loop worktree is still where workers cwd + write: it is threaded through the per-worker
|
|
45
|
+
// `worktree` opt below, which launchWorker reads as ctx.opts.worktree -> workerCwd.
|
|
46
|
+
const insideZellij = Boolean(process.env.SKS_ZELLIJ_SESSION_NAME || process.env.ZELLIJ);
|
|
47
|
+
const visiblePaneCap = Math.min(resolveLoopVisiblePaneCap(workerCount), Math.max(1, workerCount));
|
|
48
|
+
const zellijPlacementOpts = insideZellij ? {
|
|
49
|
+
workerPlacement: 'zellij-pane',
|
|
50
|
+
...(process.env.SKS_ZELLIJ_SESSION_NAME ? { zellijSessionName: process.env.SKS_ZELLIJ_SESSION_NAME } : {}),
|
|
51
|
+
zellijVisiblePaneCap: visiblePaneCap
|
|
52
|
+
} : {};
|
|
53
|
+
const orchestrator = await runNativeAgentOrchestrator({
|
|
54
|
+
root: input.root,
|
|
55
|
+
missionId: input.plan.mission_id,
|
|
56
|
+
prompt,
|
|
57
|
+
route: '$Naruto',
|
|
58
|
+
backend: 'codex-sdk',
|
|
59
|
+
readonly: input.phase === 'checker',
|
|
60
|
+
workspaceWrite: input.phase === 'maker',
|
|
61
|
+
desiredWorkItemCount: workGraph.total_work_items,
|
|
62
|
+
minimumWorkItems: 1,
|
|
63
|
+
maxAgentCount: Math.max(1, workerCount),
|
|
64
|
+
targetActiveSlots: Math.max(1, workerCount),
|
|
65
|
+
visualLaneCount: visiblePaneCap,
|
|
66
|
+
narutoMode: true,
|
|
67
|
+
narutoWorkGraph: workGraph,
|
|
68
|
+
...zellijPlacementOpts,
|
|
69
|
+
...(input.worktree?.path ? {
|
|
70
|
+
worktree: {
|
|
71
|
+
id: input.worktree.id || `loop-${input.node.loop_id}-${input.phase}`,
|
|
72
|
+
path: input.worktree.path,
|
|
73
|
+
branch: input.worktree.branch || 'unknown',
|
|
74
|
+
main_repo_root: input.root
|
|
75
|
+
}
|
|
76
|
+
} : {}),
|
|
77
|
+
gitWorktreePolicy: input.worktree?.path ? {
|
|
78
|
+
mode: 'patch-envelope-only',
|
|
79
|
+
required: false,
|
|
80
|
+
main_repo_root: input.root,
|
|
81
|
+
worktree_root: input.worktree.path,
|
|
82
|
+
fallback_reason: null
|
|
83
|
+
} : null
|
|
84
|
+
});
|
|
85
|
+
return normalizeNativeResult(input, orchestrator);
|
|
86
|
+
}
|
|
87
|
+
async function normalizeNativeResult(input, result) {
|
|
88
|
+
const artifacts = collectArtifactPaths(result);
|
|
89
|
+
const changedFiles = stringArray(result?.changed_files || result?.proof?.changed_files || result?.results?.flatMap?.((row) => row?.changed_files || []));
|
|
90
|
+
const blockers = [
|
|
91
|
+
...(result?.ok === true ? [] : ['loop_worker_native_orchestrator_not_ok']),
|
|
92
|
+
...stringArray(result?.blockers || result?.proof?.blockers)
|
|
93
|
+
];
|
|
94
|
+
const proofPath = path.join(loopNodeRoot(input.root, input.plan.mission_id, input.node.loop_id), input.phase, 'worker-runtime-result.json');
|
|
95
|
+
const normalized = {
|
|
96
|
+
schema: 'sks.loop-worker-run-result.v1',
|
|
97
|
+
ok: blockers.length === 0,
|
|
98
|
+
mission_id: input.plan.mission_id,
|
|
99
|
+
loop_id: input.node.loop_id,
|
|
100
|
+
phase: input.phase,
|
|
101
|
+
worker_count: input.phase === 'maker' ? input.node.maker.worker_count : input.node.checker.worker_count,
|
|
102
|
+
backend: 'native-agent-orchestrator',
|
|
103
|
+
artifacts,
|
|
104
|
+
patch_candidates: input.phase === 'maker' ? artifacts.filter((artifact) => artifact.includes('patch')) : [],
|
|
105
|
+
checker_findings: input.phase === 'checker' ? artifacts.filter((artifact) => artifact.includes('checker') || artifact.includes('finding')) : [],
|
|
106
|
+
changed_files: changedFiles,
|
|
107
|
+
blockers: [...new Set(blockers)],
|
|
108
|
+
runtime_proof_path: proofPath,
|
|
109
|
+
worker_ids: stringArray(result?.results?.map?.((row) => row?.agent_id || row?.id)),
|
|
110
|
+
session_ids: stringArray(result?.results?.map?.((row) => row?.session_id))
|
|
111
|
+
};
|
|
112
|
+
await writeJsonAtomic(proofPath, { ...normalized, native_result_summary: summarizeNativeResult(result), generated_at: nowIso() });
|
|
113
|
+
return normalized;
|
|
114
|
+
}
|
|
115
|
+
async function runLoopWorkerFixture(input) {
|
|
116
|
+
const dir = path.join(loopNodeRoot(input.root, input.plan.mission_id, input.node.loop_id), input.phase);
|
|
117
|
+
await ensureDir(dir);
|
|
118
|
+
const resultPath = path.join(dir, 'worker-runtime-result.json');
|
|
119
|
+
const childInputPath = path.join(dir, 'worker-fixture-intake.json');
|
|
120
|
+
await writeJsonAtomic(childInputPath, {
|
|
121
|
+
schema: 'sks.loop-worker-fixture-intake.v1',
|
|
122
|
+
root: input.root,
|
|
123
|
+
mission_id: input.plan.mission_id,
|
|
124
|
+
loop_id: input.node.loop_id,
|
|
125
|
+
phase: input.phase,
|
|
126
|
+
worker_count: input.phase === 'maker' ? input.node.maker.worker_count : input.node.checker.worker_count,
|
|
127
|
+
result_path: resultPath,
|
|
128
|
+
owner_scope: input.node.owner_scope,
|
|
129
|
+
maker_artifacts: input.makerArtifacts || []
|
|
130
|
+
});
|
|
131
|
+
const child = await runProcess(process.execPath, [fixtureChildEntrypoint(), childInputPath], {
|
|
132
|
+
cwd: input.root,
|
|
133
|
+
timeoutMs: input.timeoutMs || 30000,
|
|
134
|
+
maxOutputBytes: 64 * 1024
|
|
135
|
+
});
|
|
136
|
+
const result = await readJson(resultPath, null);
|
|
137
|
+
if (!result) {
|
|
138
|
+
return {
|
|
139
|
+
schema: 'sks.loop-worker-run-result.v1',
|
|
140
|
+
ok: false,
|
|
141
|
+
mission_id: input.plan.mission_id,
|
|
142
|
+
loop_id: input.node.loop_id,
|
|
143
|
+
phase: input.phase,
|
|
144
|
+
worker_count: 0,
|
|
145
|
+
backend: 'deterministic-fixture',
|
|
146
|
+
artifacts: [],
|
|
147
|
+
patch_candidates: [],
|
|
148
|
+
checker_findings: [],
|
|
149
|
+
changed_files: [],
|
|
150
|
+
blockers: [`loop_worker_fixture_child_missing_result:${child.code}`],
|
|
151
|
+
runtime_proof_path: resultPath,
|
|
152
|
+
worker_ids: [],
|
|
153
|
+
session_ids: []
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
...result,
|
|
158
|
+
ok: result.ok && child.code === 0,
|
|
159
|
+
blockers: [
|
|
160
|
+
...result.blockers,
|
|
161
|
+
...(child.code === 0 ? [] : [`loop_worker_fixture_child_exit:${child.code}`])
|
|
162
|
+
]
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function buildLoopNarutoWorkGraph(input, workerCount) {
|
|
166
|
+
const workItems = Array.from({ length: Math.max(1, workerCount) }, (_, index) => {
|
|
167
|
+
const id = `${input.node.loop_id}-${input.phase}-${index + 1}`;
|
|
168
|
+
const writeAllowed = input.phase === 'maker';
|
|
169
|
+
return {
|
|
170
|
+
id,
|
|
171
|
+
kind: writeAllowed ? 'code_modification' : 'verification',
|
|
172
|
+
title: `${input.phase} worker ${index + 1} for ${input.node.loop_id}`,
|
|
173
|
+
target_paths: [...input.node.owner_scope.files, ...input.node.owner_scope.directories],
|
|
174
|
+
readonly_paths: input.phase === 'checker' ? [...input.node.owner_scope.files, ...input.node.owner_scope.directories] : [],
|
|
175
|
+
write_paths: writeAllowed ? [...input.node.owner_scope.files, ...input.node.owner_scope.directories] : [],
|
|
176
|
+
required_role: input.phase,
|
|
177
|
+
write_allowed: writeAllowed,
|
|
178
|
+
verification_required: input.phase === 'checker',
|
|
179
|
+
dependencies: [],
|
|
180
|
+
can_run_in_parallel_with: [],
|
|
181
|
+
conflicts_with: [],
|
|
182
|
+
estimated_cost: { tokens: 8000, latency_ms: 30000, cpu_weight: 1, memory_mb: 512, gpu_weight: 0 },
|
|
183
|
+
lease_requirements: input.node.owner_scope.files.map((file) => ({ path: file, kind: writeAllowed ? 'write' : 'read' })),
|
|
184
|
+
acceptance: { requires_patch_envelope: writeAllowed, requires_verification: !writeAllowed, requires_gpt_final: input.node.risk.requires_gpt_final },
|
|
185
|
+
owner: input.node.loop_id,
|
|
186
|
+
allocation_reason: 'loop-worker-runtime',
|
|
187
|
+
allocation_score: 1,
|
|
188
|
+
allocation_hints: null,
|
|
189
|
+
lane: input.phase,
|
|
190
|
+
worktree: {
|
|
191
|
+
mode: input.worktree?.path ? 'patch-envelope-only' : 'git-worktree',
|
|
192
|
+
required: input.node.worktree.required,
|
|
193
|
+
allocation_required: false
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
});
|
|
197
|
+
return {
|
|
198
|
+
schema: 'sks.naruto-work-graph.v1',
|
|
199
|
+
route: '$Naruto',
|
|
200
|
+
requested_clones: workerCount,
|
|
201
|
+
total_work_items: workItems.length,
|
|
202
|
+
readonly: input.phase === 'checker',
|
|
203
|
+
write_capable: input.phase === 'maker',
|
|
204
|
+
work_items: workItems,
|
|
205
|
+
active_waves: [{ wave_id: `${input.node.loop_id}-${input.phase}`, work_item_ids: workItems.map((item) => item.id), write_paths: workItems.flatMap((item) => item.write_paths), conflict_count: 0 }],
|
|
206
|
+
mixed_work_kinds: [...new Set(workItems.map((item) => item.kind))],
|
|
207
|
+
write_allowed_count: workItems.filter((item) => item.write_allowed).length,
|
|
208
|
+
worktree_policy: {
|
|
209
|
+
mode: input.worktree?.path ? 'patch-envelope-only' : 'git-worktree',
|
|
210
|
+
required: input.node.worktree.required,
|
|
211
|
+
main_repo_root: input.root,
|
|
212
|
+
worktree_root: null,
|
|
213
|
+
fallback_reason: input.worktree?.path ? 'loop_worktree_already_allocated' : null
|
|
214
|
+
},
|
|
215
|
+
blockers: [],
|
|
216
|
+
ok: true
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function collectArtifactPaths(result) {
|
|
220
|
+
return stringArray([
|
|
221
|
+
result?.ledger_root,
|
|
222
|
+
result?.proof?.artifact,
|
|
223
|
+
...(Array.isArray(result?.results) ? result.results.flatMap((row) => row?.artifacts || row?.patch_queue_refs || []) : [])
|
|
224
|
+
]);
|
|
225
|
+
}
|
|
226
|
+
function summarizeNativeResult(result) {
|
|
227
|
+
return {
|
|
228
|
+
ok: result?.ok === true,
|
|
229
|
+
status: result?.status || null,
|
|
230
|
+
mission_id: result?.mission_id || null,
|
|
231
|
+
backend: result?.backend || null,
|
|
232
|
+
result_count: Array.isArray(result?.results) ? result.results.length : 0,
|
|
233
|
+
blockers: stringArray(result?.blockers || result?.proof?.blockers).slice(0, 20)
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function stringArray(value) {
|
|
237
|
+
if (!Array.isArray(value))
|
|
238
|
+
return [];
|
|
239
|
+
return [...new Set(value.flat().map((item) => String(item || '').trim()).filter(Boolean))];
|
|
240
|
+
}
|
|
241
|
+
// Visible pane cap for loop workers: defaults to min(4, workers) so the right
|
|
242
|
+
// column stays readable; SKS_ZELLIJ_VISIBLE_PANE_CAP overrides for tall
|
|
243
|
+
// terminals (overflow workers run headless and stay visible in SLOTS rows).
|
|
244
|
+
function resolveLoopVisiblePaneCap(workerCount) {
|
|
245
|
+
const fromEnv = Number(process.env.SKS_ZELLIJ_VISIBLE_PANE_CAP || 0);
|
|
246
|
+
if (Number.isFinite(fromEnv) && fromEnv >= 1)
|
|
247
|
+
return Math.floor(fromEnv);
|
|
248
|
+
return Math.min(4, Math.max(1, workerCount));
|
|
249
|
+
}
|
|
250
|
+
function fixtureChildEntrypoint() {
|
|
251
|
+
return fileURLToPath(new URL('../../scripts/loop-worker-fixture-child.js', import.meta.url));
|
|
252
|
+
}
|
|
253
|
+
function loopFixtureAllowed(input) {
|
|
254
|
+
const argv = process.argv.join(' ');
|
|
255
|
+
const scriptIsCheck = /(?:^|\s)(?:.*[\/\\])?(?:dist|src)[\/\\]scripts[\/\\][^\s]*(?:check|blackbox)\.(?:js|ts)(?:\s|$)/.test(argv);
|
|
256
|
+
const explicitTestEnv = process.env.NODE_ENV === 'test'
|
|
257
|
+
|| process.env.SKS_TEST_RUNTIME_FIXTURE_ALLOWED === '1'
|
|
258
|
+
|| process.env.VITEST_WORKER_ID !== undefined
|
|
259
|
+
|| process.env.JEST_WORKER_ID !== undefined
|
|
260
|
+
|| process.env.NODE_V8_COVERAGE !== undefined;
|
|
261
|
+
const tempRoot = isUnderTempRoot(input.root) && /^M-check-/.test(input.plan.mission_id);
|
|
262
|
+
if (scriptIsCheck)
|
|
263
|
+
return { ok: true, reason: 'release_check_script' };
|
|
264
|
+
if (explicitTestEnv)
|
|
265
|
+
return { ok: true, reason: 'test_environment' };
|
|
266
|
+
if (tempRoot)
|
|
267
|
+
return { ok: true, reason: 'hermetic_temp_loop_check' };
|
|
268
|
+
return { ok: false, reason: 'fixture_requires_test_context' };
|
|
269
|
+
}
|
|
270
|
+
function isUnderTempRoot(root) {
|
|
271
|
+
const normalizedRoot = path.resolve(root);
|
|
272
|
+
const tempRoot = path.resolve(os.tmpdir());
|
|
273
|
+
return normalizedRoot === tempRoot || normalizedRoot.startsWith(`${tempRoot}${path.sep}`);
|
|
274
|
+
}
|
|
275
|
+
//# sourceMappingURL=loop-worker-runtime.js.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { exists, nowIso, writeJsonAtomic } from '../fsx.js';
|
|
3
|
+
import { allocateWorkerWorktree } from '../git/git-worktree-manager.js';
|
|
4
|
+
import { gitOutputLine, runGitCommand } from '../git/git-worktree-runner.js';
|
|
5
|
+
import { loopNodeRoot } from './loop-artifacts.js';
|
|
6
|
+
export async function allocateLoopWorktree(input) {
|
|
7
|
+
const blockers = [];
|
|
8
|
+
let worktreeId = null;
|
|
9
|
+
let worktreePath = null;
|
|
10
|
+
let branch = null;
|
|
11
|
+
let baseRef = null;
|
|
12
|
+
if (input.node.worktree.required && !input.noMutation) {
|
|
13
|
+
const gitPresent = await exists(path.join(input.root, '.git'));
|
|
14
|
+
if (!gitPresent) {
|
|
15
|
+
blockers.push('loop_worktree_required_but_git_missing');
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
const allocation = await allocateWorkerWorktree({
|
|
19
|
+
repoRoot: input.root,
|
|
20
|
+
missionId: input.plan.mission_id,
|
|
21
|
+
workerId: input.node.loop_id,
|
|
22
|
+
slotId: input.node.loop_id,
|
|
23
|
+
generationIndex: 1,
|
|
24
|
+
branchPrefix: input.node.worktree.branch_prefix
|
|
25
|
+
}).catch((err) => ({ ok: false, blockers: [`loop_worktree_allocate_exception:${err instanceof Error ? err.message : String(err)}`] }));
|
|
26
|
+
if (allocation.ok) {
|
|
27
|
+
worktreeId = allocation.worker_id || input.node.loop_id;
|
|
28
|
+
worktreePath = allocation.worktree_path || null;
|
|
29
|
+
branch = allocation.branch || null;
|
|
30
|
+
baseRef = allocation.base_ref || null;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
blockers.push(...stringArray(allocation.blockers));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const record = {
|
|
38
|
+
schema: 'sks.loop-worktree.v1',
|
|
39
|
+
loop_id: input.node.loop_id,
|
|
40
|
+
worktree_id: worktreeId,
|
|
41
|
+
path: worktreePath,
|
|
42
|
+
branch,
|
|
43
|
+
base_ref: baseRef,
|
|
44
|
+
allocated_at: nowIso(),
|
|
45
|
+
cleanup_policy: input.node.worktree.cleanup,
|
|
46
|
+
blockers
|
|
47
|
+
};
|
|
48
|
+
await writeJsonAtomic(path.join(loopNodeRoot(input.root, input.plan.mission_id, input.node.loop_id), 'worktree.json'), record);
|
|
49
|
+
return record;
|
|
50
|
+
}
|
|
51
|
+
export async function computeLoopDiff(input) {
|
|
52
|
+
const cwd = input.worktreePath || input.root;
|
|
53
|
+
const blockers = [];
|
|
54
|
+
const names = await runGitCommand(cwd, ['diff', '--name-only', 'HEAD'], { timeoutMs: 30000 }).catch(() => null);
|
|
55
|
+
const stat = await runGitCommand(cwd, ['diff', '--stat', 'HEAD'], { timeoutMs: 30000 }).catch(() => null);
|
|
56
|
+
const diff = await runGitCommand(cwd, ['diff', '--binary', '--full-index', 'HEAD'], { timeoutMs: 60000 }).catch(() => null);
|
|
57
|
+
if (!names?.ok)
|
|
58
|
+
blockers.push('loop_git_diff_name_only_failed');
|
|
59
|
+
if (!diff?.ok)
|
|
60
|
+
blockers.push('loop_git_diff_failed');
|
|
61
|
+
const changedFiles = [...new Set((names?.stdout || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean))];
|
|
62
|
+
blockers.push(...enforceLoopOwnerScope(changedFiles, input.ownerScope));
|
|
63
|
+
return {
|
|
64
|
+
changed_files: changedFiles,
|
|
65
|
+
patch_bytes: Buffer.byteLength(diff?.stdout || ''),
|
|
66
|
+
diff_stat: stat ? gitOutputLine(stat) || stat.stdout.slice(-4000) : '',
|
|
67
|
+
blockers: [...new Set(blockers)]
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function enforceLoopOwnerScope(changedFiles, ownerScope) {
|
|
71
|
+
const blockers = [];
|
|
72
|
+
for (const file of changedFiles) {
|
|
73
|
+
if (!isInOwnerScope(file, ownerScope))
|
|
74
|
+
blockers.push(`loop_owner_scope_violation:${file}`);
|
|
75
|
+
}
|
|
76
|
+
return blockers;
|
|
77
|
+
}
|
|
78
|
+
function isInOwnerScope(file, ownerScope) {
|
|
79
|
+
const normalized = normalizePath(file);
|
|
80
|
+
if (ownerScope.files.map(normalizePath).includes(normalized))
|
|
81
|
+
return true;
|
|
82
|
+
return ownerScope.directories.map(normalizePath).some((dir) => normalized === dir || normalized.startsWith(`${dir}/`));
|
|
83
|
+
}
|
|
84
|
+
function normalizePath(value) {
|
|
85
|
+
return String(value || '').replace(/\\/g, '/').replace(/^\.\/+/, '');
|
|
86
|
+
}
|
|
87
|
+
function stringArray(value) {
|
|
88
|
+
if (!Array.isArray(value))
|
|
89
|
+
return [];
|
|
90
|
+
return value.map((item) => String(item || '').trim()).filter(Boolean);
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=loop-worktree-runtime.js.map
|
|
@@ -5,14 +5,19 @@ export function evaluateNarutoFinalizer(input = {}) {
|
|
|
5
5
|
const blockers = [
|
|
6
6
|
...(gptFinalRequired && !gptFinalAccepted ? ['naruto_local_worker_output_needs_gpt_final_arbiter'] : [])
|
|
7
7
|
];
|
|
8
|
+
const finalStatus = blockers.length ? 'blocked' : input.applyPatches === true ? 'accepted' : 'draft';
|
|
9
|
+
const applyFinalized = finalStatus === 'accepted';
|
|
8
10
|
return {
|
|
9
11
|
schema: 'sks.naruto-finalizer.v1',
|
|
10
12
|
local_participated: localParticipated,
|
|
11
13
|
gpt_final_required: gptFinalRequired,
|
|
12
|
-
final_status:
|
|
14
|
+
final_status: finalStatus,
|
|
13
15
|
final_patch_source: gptFinalRequired ? 'gpt_final_arbiter' : 'deterministic_no_local',
|
|
14
16
|
blockers,
|
|
15
|
-
|
|
17
|
+
run_ok: blockers.length === 0,
|
|
18
|
+
release_proof_allowed: applyFinalized,
|
|
19
|
+
apply_finalized: applyFinalized,
|
|
20
|
+
ok: applyFinalized
|
|
16
21
|
};
|
|
17
22
|
}
|
|
18
23
|
//# sourceMappingURL=naruto-finalizer.js.map
|
|
@@ -11,7 +11,13 @@ export async function runNarutoLoopMesh(input) {
|
|
|
11
11
|
active_worker_budget: activeWorkerBudget,
|
|
12
12
|
routes
|
|
13
13
|
});
|
|
14
|
-
return runLoopPlan({
|
|
14
|
+
return runLoopPlan({
|
|
15
|
+
root: input.root,
|
|
16
|
+
plan: input.plan,
|
|
17
|
+
parallelism: input.parallelism,
|
|
18
|
+
...(input.dryRun === undefined ? {} : { dryRun: input.dryRun }),
|
|
19
|
+
...(input.noMutation === undefined ? {} : { noMutation: input.noMutation })
|
|
20
|
+
});
|
|
15
21
|
}
|
|
16
22
|
export function splitActiveWorkerBudget(plan, parallelism) {
|
|
17
23
|
const cap = parallelism === 'safe' ? 8 : parallelism === 'extreme' ? 32 : 16;
|
|
@@ -38,6 +38,12 @@ export function emptyCompletionProof(overrides = {}) {
|
|
|
38
38
|
claims: [],
|
|
39
39
|
unverified: [],
|
|
40
40
|
blockers: [],
|
|
41
|
+
failure_analysis: {
|
|
42
|
+
status: 'not_required',
|
|
43
|
+
root_cause: null,
|
|
44
|
+
corrective_action: null,
|
|
45
|
+
evidence: []
|
|
46
|
+
},
|
|
41
47
|
next_human_actions: [],
|
|
42
48
|
...overrides
|
|
43
49
|
};
|
|
@@ -76,10 +76,13 @@ export function renderProofMarkdown(proof = {}, validation = validateCompletionP
|
|
|
76
76
|
`- Wrongness: ${proof.evidence?.wrongness?.active_count ?? 0} active (${proof.evidence?.wrongness?.high_severity_active ?? 0} high)`,
|
|
77
77
|
`- Evidence router: ${proof.evidence?.evidence_router?.records ?? 0} record(s)`,
|
|
78
78
|
`- Trust report: ${proof.evidence?.trust_report || 'not_recorded'}`,
|
|
79
|
-
'',
|
|
80
|
-
'## Unverified',
|
|
81
79
|
''
|
|
82
80
|
];
|
|
81
|
+
const failureAnalysis = proof.failure_analysis;
|
|
82
|
+
if (failureAnalysis && (failureAnalysis.status !== 'not_required' || failureAnalysis.root_cause || failureAnalysis.corrective_action)) {
|
|
83
|
+
lines.push('## Failure Analysis', '', `- Status: ${failureAnalysis.status || 'unknown'}`, `- Root cause: ${failureAnalysis.root_cause || 'not_recorded'}`, `- Corrective action: ${failureAnalysis.corrective_action || 'not_recorded'}`, `- Evidence: ${Array.isArray(failureAnalysis.evidence) ? failureAnalysis.evidence.length : failureAnalysis.evidence ? 1 : 0}`, '');
|
|
84
|
+
}
|
|
85
|
+
lines.push('## Unverified', '');
|
|
83
86
|
const unverified = proof.unverified?.length ? proof.unverified : ['No unverified claims recorded.'];
|
|
84
87
|
for (const item of unverified)
|
|
85
88
|
lines.push(`- ${typeof item === 'string' ? item : JSON.stringify(item)}`);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const PROBLEM_PATTERN = /\b(fallback|workaround|bypass|temporary|synthetic|stale|missing|failed|failure|error|blocked|not_ok|not ok|fixture_child_missing|native_agent_proof_false)\b/i;
|
|
2
|
+
const COMPLETE_STATUSES = new Set(['complete', 'completed', 'corrected', 'resolved', 'fixed']);
|
|
3
|
+
const BLOCKING_STATUSES = new Set(['blocked', 'failed', 'not_verified']);
|
|
4
|
+
function asRecord(value) {
|
|
5
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
6
|
+
}
|
|
7
|
+
function asList(value) {
|
|
8
|
+
return Array.isArray(value) ? value : [];
|
|
9
|
+
}
|
|
10
|
+
function meaningfulString(value, minLength = 12) {
|
|
11
|
+
return typeof value === 'string' && value.trim().length >= minLength;
|
|
12
|
+
}
|
|
13
|
+
function hasEvidence(value) {
|
|
14
|
+
if (typeof value === 'string')
|
|
15
|
+
return value.trim().length >= 6;
|
|
16
|
+
if (Array.isArray(value))
|
|
17
|
+
return value.length > 0;
|
|
18
|
+
if (value && typeof value === 'object')
|
|
19
|
+
return Object.keys(value).length > 0;
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
export function rootCauseAnalysisRequired(proof = {}, validationIssues = []) {
|
|
23
|
+
const proofRecord = asRecord(proof);
|
|
24
|
+
if (!Object.keys(proofRecord).length)
|
|
25
|
+
return false;
|
|
26
|
+
const evidence = asRecord(proofRecord.evidence);
|
|
27
|
+
const agents = asRecord(evidence.agents);
|
|
28
|
+
const routeGate = asRecord(evidence.route_gate);
|
|
29
|
+
if (BLOCKING_STATUSES.has(String(proofRecord.status || '')))
|
|
30
|
+
return true;
|
|
31
|
+
if (asList(proofRecord.blockers).length > 0)
|
|
32
|
+
return true;
|
|
33
|
+
if (validationIssues.some((issue) => String(issue) !== 'root_cause_analysis_missing'))
|
|
34
|
+
return true;
|
|
35
|
+
const problemSurface = {
|
|
36
|
+
status: proofRecord.status,
|
|
37
|
+
unverified: proofRecord.unverified,
|
|
38
|
+
blockers: proofRecord.blockers,
|
|
39
|
+
claims: proofRecord.claims,
|
|
40
|
+
route_gate: routeGate,
|
|
41
|
+
agents: {
|
|
42
|
+
ok: agents.ok,
|
|
43
|
+
status: agents.status,
|
|
44
|
+
blockers: agents.blockers,
|
|
45
|
+
issues: agents.issues
|
|
46
|
+
},
|
|
47
|
+
wrongness: evidence.wrongness,
|
|
48
|
+
trust_report: evidence.trust_report
|
|
49
|
+
};
|
|
50
|
+
return PROBLEM_PATTERN.test(JSON.stringify(problemSurface));
|
|
51
|
+
}
|
|
52
|
+
export function rootCauseAnalysisComplete(proof = {}) {
|
|
53
|
+
const proofRecord = asRecord(proof);
|
|
54
|
+
const analysis = asRecord(proofRecord.failure_analysis || asRecord(proofRecord.evidence).root_cause_analysis);
|
|
55
|
+
if (!Object.keys(analysis).length)
|
|
56
|
+
return false;
|
|
57
|
+
const status = String(analysis.status || '').toLowerCase();
|
|
58
|
+
if (!COMPLETE_STATUSES.has(status))
|
|
59
|
+
return false;
|
|
60
|
+
const rootCause = analysis.root_cause ?? analysis.cause;
|
|
61
|
+
const correctiveAction = analysis.corrective_action ?? analysis.fix ?? analysis.correction;
|
|
62
|
+
const evidence = analysis.evidence ?? analysis.proof ?? analysis.references;
|
|
63
|
+
return meaningfulString(rootCause) && meaningfulString(correctiveAction) && hasEvidence(evidence);
|
|
64
|
+
}
|
|
65
|
+
export function rootCauseAnalysisIssue(proof = {}, validationIssues = []) {
|
|
66
|
+
if (!rootCauseAnalysisRequired(proof, validationIssues))
|
|
67
|
+
return null;
|
|
68
|
+
return rootCauseAnalysisComplete(proof) ? null : 'root_cause_analysis_missing';
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=root-cause-policy.js.map
|
|
@@ -5,7 +5,7 @@ import { normalizeProofRoute, routeRequiresImageVoxelAnchors } from './route-pro
|
|
|
5
5
|
import { linkProofClaimsToEvidence, proofEvidenceSummary } from '../evidence/evidence-proof-linker.js';
|
|
6
6
|
import { writeTrustArtifactsForProof } from '../trust-kernel/trust-report.js';
|
|
7
7
|
import { enforceRetention } from '../retention.js';
|
|
8
|
-
export async function writeRouteCompletionProof(root, { missionId = null, route = null, status = 'verified_partial', gate = null, summary = {}, artifacts = [], evidence = {}, claims = [], unverified = [], blockers = [], nextHumanActions = [] } = {}) {
|
|
8
|
+
export async function writeRouteCompletionProof(root, { missionId = null, route = null, status = 'verified_partial', gate = null, summary = {}, artifacts = [], evidence = {}, claims = [], unverified = [], blockers = [], failureAnalysis = null, nextHumanActions = [] } = {}) {
|
|
9
9
|
const collected = await collectProofEvidence(root);
|
|
10
10
|
const normalizedRoute = normalizeProofRoute(route);
|
|
11
11
|
const mergedEvidence = {
|
|
@@ -36,6 +36,7 @@ export async function writeRouteCompletionProof(root, { missionId = null, route
|
|
|
36
36
|
claims,
|
|
37
37
|
unverified,
|
|
38
38
|
blockers,
|
|
39
|
+
failure_analysis: normalizeFailureAnalysis(failureAnalysis || evidence.failure_analysis || evidence.root_cause_analysis),
|
|
39
40
|
next_human_actions: nextHumanActions
|
|
40
41
|
}, {
|
|
41
42
|
command: {
|
|
@@ -68,6 +69,22 @@ export async function writeRouteCompletionProof(root, { missionId = null, route
|
|
|
68
69
|
const retention = await runPostRouteRetention(root, missionId);
|
|
69
70
|
return { ...enriched, trust, retention };
|
|
70
71
|
}
|
|
72
|
+
function normalizeFailureAnalysis(value) {
|
|
73
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
74
|
+
return {
|
|
75
|
+
status: 'not_required',
|
|
76
|
+
root_cause: null,
|
|
77
|
+
corrective_action: null,
|
|
78
|
+
evidence: []
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
status: value.status || 'complete',
|
|
83
|
+
root_cause: value.root_cause || value.cause || null,
|
|
84
|
+
corrective_action: value.corrective_action || value.fix || value.correction || null,
|
|
85
|
+
evidence: value.evidence || value.proof || value.references || []
|
|
86
|
+
};
|
|
87
|
+
}
|
|
71
88
|
function normalizeRouteProofStatus(status, { route, evidence, blockers, unverified }) {
|
|
72
89
|
if (blockers?.length)
|
|
73
90
|
return status === 'failed' ? 'failed' : 'blocked';
|
|
@@ -3,6 +3,7 @@ import { readRouteProof } from './proof-reader.js';
|
|
|
3
3
|
import { validateCompletionProof } from './validation.js';
|
|
4
4
|
import { normalizeProofRoute, proofStatusBlocks, routeRequiresCompletionProof, routeRequiresImageVoxelAnchors } from './route-proof-policy.js';
|
|
5
5
|
import { routeRequiresAgentIntake } from '../agents/agent-plan.js';
|
|
6
|
+
import { rootCauseAnalysisIssue } from './root-cause-policy.js';
|
|
6
7
|
export async function validateRouteCompletionProof(root, { missionId = null, route = null, state = {}, visualClaim = undefined } = {}) {
|
|
7
8
|
const proofRequired = state.proof_required === true || routeRequiresCompletionProof(route);
|
|
8
9
|
if (!proofRequired)
|
|
@@ -61,6 +62,9 @@ export async function validateRouteCompletionProof(root, { missionId = null, rou
|
|
|
61
62
|
issues.push('active_wrongness_high');
|
|
62
63
|
if (proof.status === 'verified' && Number(wrongness?.active_count || 0) > 0)
|
|
63
64
|
issues.push('active_wrongness_requires_partial');
|
|
65
|
+
const rootCauseIssue = rootCauseAnalysisIssue(proof, issues);
|
|
66
|
+
if (rootCauseIssue)
|
|
67
|
+
issues.push(rootCauseIssue);
|
|
64
68
|
return {
|
|
65
69
|
ok: issues.length === 0,
|
|
66
70
|
required: true,
|