sneakoscope 3.1.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +1 -1
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/crates/sks-core/src/main.rs +1 -1
  5. package/dist/.sks-build-stamp.json +4 -4
  6. package/dist/bin/sks.js +1 -1
  7. package/dist/commands/zellij-slot-column-anchor.js +3 -1
  8. package/dist/commands/zellij-slot-pane.js +19 -2
  9. package/dist/core/agents/agent-janitor.js +10 -1
  10. package/dist/core/agents/agent-orchestrator.js +1 -0
  11. package/dist/core/agents/agent-runner-ollama.js +11 -4
  12. package/dist/core/agents/native-cli-session-swarm.js +69 -9
  13. package/dist/core/codex-control/codex-task-runner.js +9 -0
  14. package/dist/core/commands/loop-command.js +54 -13
  15. package/dist/core/commands/naruto-command.js +26 -17
  16. package/dist/core/commands/team-command.js +1 -0
  17. package/dist/core/fsx.js +1 -1
  18. package/dist/core/locks/file-lock.js +88 -0
  19. package/dist/core/loops/loop-artifacts.js +33 -2
  20. package/dist/core/loops/loop-checkpoint.js +22 -0
  21. package/dist/core/loops/loop-finalizer.js +33 -7
  22. package/dist/core/loops/loop-gate-registry.js +96 -0
  23. package/dist/core/loops/loop-gate-runner.js +165 -17
  24. package/dist/core/loops/loop-gpt-final-arbiter.js +61 -0
  25. package/dist/core/loops/loop-integration-merge.js +75 -0
  26. package/dist/core/loops/loop-lease.js +35 -20
  27. package/dist/core/loops/loop-planner.js +36 -5
  28. package/dist/core/loops/loop-runtime-control.js +25 -0
  29. package/dist/core/loops/loop-runtime.js +248 -93
  30. package/dist/core/loops/loop-scheduler.js +12 -3
  31. package/dist/core/loops/loop-worker-prompts.js +43 -0
  32. package/dist/core/loops/loop-worker-runtime.js +275 -0
  33. package/dist/core/loops/loop-worktree-runtime.js +92 -0
  34. package/dist/core/naruto/naruto-finalizer.js +7 -2
  35. package/dist/core/naruto/naruto-loop-mesh.js +7 -1
  36. package/dist/core/proof/proof-schema.js +6 -0
  37. package/dist/core/proof/proof-writer.js +5 -2
  38. package/dist/core/proof/root-cause-policy.js +70 -0
  39. package/dist/core/proof/route-adapter.js +18 -1
  40. package/dist/core/proof/route-proof-gate.js +4 -0
  41. package/dist/core/release/release-gate-batch-runner.js +56 -10
  42. package/dist/core/release/release-gate-cache-v2.js +18 -3
  43. package/dist/core/release/release-gate-dag.js +65 -17
  44. package/dist/core/release/release-gate-node.js +2 -1
  45. package/dist/core/release/release-gate-resource-governor.js +27 -6
  46. package/dist/core/skills/core-skill-meta-update.js +24 -0
  47. package/dist/core/skills/core-skill-reflection.js +94 -0
  48. package/dist/core/skills/core-skill-trainer.js +103 -0
  49. package/dist/core/trust-kernel/completion-contract.js +4 -0
  50. package/dist/core/trust-kernel/route-contract.js +4 -1
  51. package/dist/core/version.js +1 -1
  52. package/dist/core/zellij/zellij-right-column-manager.js +13 -2
  53. package/dist/core/zellij/zellij-slot-column-anchor.js +40 -3
  54. package/dist/core/zellij/zellij-slot-pane-renderer.js +36 -11
  55. package/dist/core/zellij/zellij-slot-telemetry.js +96 -44
  56. package/dist/core/zellij/zellij-worker-pane-manager.js +42 -4
  57. package/dist/scripts/loop-directive-check-lib.js +225 -2
  58. package/dist/scripts/loop-worker-fixture-child.js +53 -0
  59. package/dist/scripts/naruto-real-local-gpt-final-smoke.js +10 -1
  60. package/package.json +5 -2
@@ -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: blockers.length ? 'blocked' : input.applyPatches === true ? 'accepted' : 'draft',
14
+ final_status: finalStatus,
13
15
  final_patch_source: gptFinalRequired ? 'gpt_final_arbiter' : 'deterministic_no_local',
14
16
  blockers,
15
- ok: blockers.length === 0
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({ root: input.root, plan: input.plan, parallelism: input.parallelism });
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,