sneakoscope 3.0.4 → 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/cli/command-registry.js +1 -0
- package/dist/cli/context7-command.js +29 -5
- package/dist/cli/install-helpers.js +15 -7
- package/dist/commands/zellij-slot-column-anchor.js +3 -1
- package/dist/commands/zellij-slot-pane.js +19 -2
- package/dist/core/agents/agent-janitor.js +10 -1
- package/dist/core/agents/agent-orchestrator.js +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/agents/runtime-proof-summary.js +4 -0
- package/dist/core/codex-control/codex-task-runner.js +9 -0
- package/dist/core/commands/goal-command.js +19 -1
- package/dist/core/commands/loop-command.js +176 -0
- 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/init.js +6 -1
- package/dist/core/locks/file-lock.js +88 -0
- package/dist/core/loops/goal-to-loop-compat.js +23 -0
- package/dist/core/loops/loop-artifacts.js +72 -0
- package/dist/core/loops/loop-checkpoint.js +22 -0
- package/dist/core/loops/loop-decomposer.js +56 -0
- package/dist/core/loops/loop-finalizer.js +54 -0
- package/dist/core/loops/loop-gate-ladder.js +16 -0
- package/dist/core/loops/loop-gate-registry.js +96 -0
- package/dist/core/loops/loop-gate-runner.js +177 -0
- package/dist/core/loops/loop-gate-selector.js +52 -0
- 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-iteration-runner.js +2 -0
- package/dist/core/loops/loop-lease.js +91 -0
- package/dist/core/loops/loop-observability.js +19 -0
- package/dist/core/loops/loop-owner-inference.js +57 -0
- package/dist/core/loops/loop-owner-ledger.js +2 -0
- package/dist/core/loops/loop-planner.js +170 -0
- package/dist/core/loops/loop-proof-summary.js +10 -0
- package/dist/core/loops/loop-proof.js +2 -0
- package/dist/core/loops/loop-risk-classifier.js +42 -0
- package/dist/core/loops/loop-runtime-control.js +25 -0
- package/dist/core/loops/loop-runtime.js +314 -0
- package/dist/core/loops/loop-scheduler.js +69 -0
- package/dist/core/loops/loop-schema.js +63 -0
- package/dist/core/loops/loop-state.js +61 -0
- 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 +39 -0
- package/dist/core/naruto/naruto-loop-worker-router.js +38 -0
- package/dist/core/pipeline-internals/runtime-core.js +82 -2
- package/dist/core/proof/proof-schema.js +6 -0
- package/dist/core/proof/proof-writer.js +5 -2
- package/dist/core/proof/root-cause-policy.js +70 -0
- package/dist/core/proof/route-adapter.js +18 -1
- package/dist/core/proof/route-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 +45 -5
- package/dist/core/zellij/zellij-slot-pane-renderer.js +37 -10
- 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 +388 -0
- 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 +38 -3
- package/schemas/loops/loop-node.schema.json +21 -0
- package/schemas/loops/loop-plan.schema.json +21 -0
- package/schemas/loops/loop-proof.schema.json +20 -0
- package/schemas/loops/loop-state.schema.json +19 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export function defaultLoopBudget(overrides = {}) {
|
|
2
|
+
return {
|
|
3
|
+
max_iterations: 2,
|
|
4
|
+
max_wall_ms: 15 * 60 * 1000,
|
|
5
|
+
max_model_calls: 8,
|
|
6
|
+
max_subagents: 4,
|
|
7
|
+
max_tokens_estimate: 120000,
|
|
8
|
+
max_changed_files: 16,
|
|
9
|
+
max_patch_bytes: 256000,
|
|
10
|
+
...overrides
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function validateLoopPlan(plan) {
|
|
14
|
+
const blockers = [
|
|
15
|
+
...(plan.schema !== 'sks.loop-plan.v1' ? ['loop_plan_schema_invalid'] : []),
|
|
16
|
+
...(!plan.mission_id ? ['loop_plan_mission_id_missing'] : []),
|
|
17
|
+
...(!plan.request ? ['loop_plan_request_missing'] : []),
|
|
18
|
+
...(!plan.integration_loop_id ? ['loop_plan_integration_loop_missing'] : []),
|
|
19
|
+
...(plan.graph.nodes.length === 0 ? ['loop_plan_nodes_missing'] : []),
|
|
20
|
+
...(!plan.graph.nodes.some((node) => node.loop_id === plan.integration_loop_id) ? ['loop_plan_integration_node_missing'] : [])
|
|
21
|
+
];
|
|
22
|
+
const ids = new Set();
|
|
23
|
+
for (const node of plan.graph.nodes) {
|
|
24
|
+
const result = validateLoopNode(node);
|
|
25
|
+
blockers.push(...result.blockers);
|
|
26
|
+
if (ids.has(node.loop_id))
|
|
27
|
+
blockers.push(`loop_node_duplicate:${node.loop_id}`);
|
|
28
|
+
ids.add(node.loop_id);
|
|
29
|
+
}
|
|
30
|
+
for (const edge of plan.graph.edges) {
|
|
31
|
+
if (!ids.has(edge.from) || !ids.has(edge.to))
|
|
32
|
+
blockers.push(`loop_edge_unknown:${edge.from}->${edge.to}`);
|
|
33
|
+
}
|
|
34
|
+
return { ok: blockers.length === 0, blockers };
|
|
35
|
+
}
|
|
36
|
+
export function validateLoopNode(node) {
|
|
37
|
+
const blockers = [
|
|
38
|
+
...(node.schema !== 'sks.loop-node.v1' ? [`loop_node_schema_invalid:${node.loop_id}`] : []),
|
|
39
|
+
...(!node.loop_id ? ['loop_node_id_missing'] : []),
|
|
40
|
+
...(!node.mission_id ? [`loop_node_mission_missing:${node.loop_id}`] : []),
|
|
41
|
+
...(!node.state_file ? [`loop_state_file_missing:${node.loop_id}`] : []),
|
|
42
|
+
...(!node.run_log_file ? [`loop_run_log_file_missing:${node.loop_id}`] : []),
|
|
43
|
+
...(!node.owner_scope ? [`loop_owner_scope_missing:${node.loop_id}`] : []),
|
|
44
|
+
...validateLoopBudget(node.budget).blockers.map((blocker) => `${node.loop_id}:${blocker}`),
|
|
45
|
+
...(node.level === 'L3-unattended' && ['high', 'critical'].includes(node.risk.level) ? [`loop_l3_risk_blocked:${node.loop_id}`] : []),
|
|
46
|
+
...(node.level === 'L2-action' && !node.checker.required_before_next_iteration ? [`loop_action_checker_missing:${node.loop_id}`] : [])
|
|
47
|
+
];
|
|
48
|
+
return { ok: blockers.length === 0, blockers };
|
|
49
|
+
}
|
|
50
|
+
export function validateLoopBudget(budget) {
|
|
51
|
+
const blockers = [];
|
|
52
|
+
for (const [key, value] of Object.entries(budget)) {
|
|
53
|
+
if (!Number.isFinite(value) || value < 0)
|
|
54
|
+
blockers.push(`loop_budget_invalid:${key}`);
|
|
55
|
+
}
|
|
56
|
+
if (budget.max_iterations < 1)
|
|
57
|
+
blockers.push('loop_budget_iterations_missing');
|
|
58
|
+
return { ok: blockers.length === 0, blockers };
|
|
59
|
+
}
|
|
60
|
+
export function allGateIds(gates) {
|
|
61
|
+
return [...new Set([...gates.triage, ...gates.local, ...gates.checker, ...gates.integration, ...gates.final])];
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=loop-schema.js.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { appendJsonl, readJson, writeJsonAtomic } from '../fsx.js';
|
|
2
|
+
import { loopRunLogPath, loopStatePath } from './loop-artifacts.js';
|
|
3
|
+
export async function readLoopState(root, missionId, loopId) {
|
|
4
|
+
return readJson(loopStatePath(root, missionId, loopId), null);
|
|
5
|
+
}
|
|
6
|
+
export async function writeLoopState(root, state) {
|
|
7
|
+
await writeJsonAtomic(loopStatePath(root, state.mission_id, state.loop_id), state);
|
|
8
|
+
return state;
|
|
9
|
+
}
|
|
10
|
+
export async function appendLoopRunLog(root, missionId, loopId, event) {
|
|
11
|
+
await appendJsonl(loopRunLogPath(root, missionId, loopId), { ts: event.ts || new Date().toISOString(), ...event });
|
|
12
|
+
}
|
|
13
|
+
export async function updateLoopState(root, missionId, loopId, patch) {
|
|
14
|
+
const current = await readLoopState(root, missionId, loopId);
|
|
15
|
+
if (!current)
|
|
16
|
+
throw new Error(`loop_state_missing:${loopId}`);
|
|
17
|
+
const next = {
|
|
18
|
+
...current,
|
|
19
|
+
...patch,
|
|
20
|
+
acting_on: { ...current.acting_on, ...(patch.acting_on || {}) },
|
|
21
|
+
handoff: { ...current.handoff, ...(patch.handoff || {}) },
|
|
22
|
+
budget_used: { ...current.budget_used, ...(patch.budget_used || {}) },
|
|
23
|
+
updated_at: new Date().toISOString()
|
|
24
|
+
};
|
|
25
|
+
await writeLoopState(root, next);
|
|
26
|
+
return next;
|
|
27
|
+
}
|
|
28
|
+
export function initialLoopState(input) {
|
|
29
|
+
return {
|
|
30
|
+
schema: 'sks.loop-state.v1',
|
|
31
|
+
mission_id: input.missionId,
|
|
32
|
+
loop_id: input.loopId,
|
|
33
|
+
status: 'planned',
|
|
34
|
+
iteration: 0,
|
|
35
|
+
acting_on: {
|
|
36
|
+
files: input.files,
|
|
37
|
+
worktree_id: input.worktreeId || null,
|
|
38
|
+
branch: input.branch || null
|
|
39
|
+
},
|
|
40
|
+
current_phase: 'triage',
|
|
41
|
+
last_action: null,
|
|
42
|
+
last_gate_result: null,
|
|
43
|
+
last_checker_result: null,
|
|
44
|
+
blockers: [],
|
|
45
|
+
handoff: {
|
|
46
|
+
required: false,
|
|
47
|
+
reason: null,
|
|
48
|
+
artifact: null
|
|
49
|
+
},
|
|
50
|
+
budget_used: {
|
|
51
|
+
wall_ms: 0,
|
|
52
|
+
model_calls: 0,
|
|
53
|
+
subagents: 0,
|
|
54
|
+
iterations: 0,
|
|
55
|
+
changed_files: 0,
|
|
56
|
+
patch_bytes: 0
|
|
57
|
+
},
|
|
58
|
+
updated_at: new Date().toISOString()
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=loop-state.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { allGateIds } from './loop-schema.js';
|
|
2
|
+
export function buildLoopMakerPrompt(input) {
|
|
3
|
+
const node = input.node;
|
|
4
|
+
return [
|
|
5
|
+
'You are the maker worker for an SKS Loop Mesh L2 action loop.',
|
|
6
|
+
`Mission: ${input.plan.mission_id}`,
|
|
7
|
+
`Loop: ${node.loop_id}`,
|
|
8
|
+
`Purpose: ${node.purpose}`,
|
|
9
|
+
`Owner files: ${node.owner_scope.files.join(', ') || '-'}`,
|
|
10
|
+
`Owner directories: ${node.owner_scope.directories.join(', ') || '-'}`,
|
|
11
|
+
`Allowed mutation scope: ${ownerScopeText(node)}`,
|
|
12
|
+
'Do not mutate outside the owner scope.',
|
|
13
|
+
`Selected local gates: ${allGateIds(node.gates).join(', ') || '-'}`,
|
|
14
|
+
`Budget: ${JSON.stringify(node.budget)}`,
|
|
15
|
+
`Worktree path: ${input.worktreePath || '-'}`,
|
|
16
|
+
'Write a patch candidate/runtime proof artifact with changed files and blockers.',
|
|
17
|
+
'No synthetic pass is allowed for production proof.'
|
|
18
|
+
].join('\n');
|
|
19
|
+
}
|
|
20
|
+
export function buildLoopCheckerPrompt(input) {
|
|
21
|
+
const node = input.node;
|
|
22
|
+
return [
|
|
23
|
+
'You are the checker worker for an SKS Loop Mesh action loop.',
|
|
24
|
+
'You must run in a fresh session and must not mutate source files.',
|
|
25
|
+
`Mission: ${input.plan.mission_id}`,
|
|
26
|
+
`Loop: ${node.loop_id}`,
|
|
27
|
+
`Purpose: ${node.purpose}`,
|
|
28
|
+
`Maker artifacts: ${input.makerArtifacts.join(', ') || '-'}`,
|
|
29
|
+
`Diff/patch summary: ${input.diffSummary || '-'}`,
|
|
30
|
+
`Selected gates: ${allGateIds(node.gates).join(', ') || '-'}`,
|
|
31
|
+
`Risk: ${node.risk.level} (${node.risk.reasons.join(', ') || '-'})`,
|
|
32
|
+
'Reject unrequested side effects and owner-scope violations.',
|
|
33
|
+
'Write checker-findings.json with fresh_session, reviewed_maker_artifacts, side_effects_detected, and approved.',
|
|
34
|
+
'No synthetic pass is allowed for production proof.'
|
|
35
|
+
].join('\n');
|
|
36
|
+
}
|
|
37
|
+
function ownerScopeText(node) {
|
|
38
|
+
return [
|
|
39
|
+
...node.owner_scope.files.map((file) => `file:${file}`),
|
|
40
|
+
...node.owner_scope.directories.map((dir) => `dir:${dir}`)
|
|
41
|
+
].join(', ') || 'none';
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=loop-worker-prompts.js.map
|
|
@@ -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
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { writeJsonAtomic } from '../fsx.js';
|
|
2
|
+
import { loopRoot } from '../loops/loop-artifacts.js';
|
|
3
|
+
import { runLoopPlan } from '../loops/loop-runtime.js';
|
|
4
|
+
import { routeNarutoLoopWorker } from './naruto-loop-worker-router.js';
|
|
5
|
+
export async function runNarutoLoopMesh(input) {
|
|
6
|
+
const routes = input.plan.graph.nodes.flatMap((node) => [routeNarutoLoopWorker(node, 'maker'), routeNarutoLoopWorker(node, 'checker')]);
|
|
7
|
+
const activeWorkerBudget = splitActiveWorkerBudget(input.plan, input.parallelism);
|
|
8
|
+
await writeJsonAtomic(`${loopRoot(input.root, input.plan.mission_id)}/naruto-loop-worker-routes.json`, {
|
|
9
|
+
schema: 'sks.naruto-loop-worker-routes.v1',
|
|
10
|
+
mission_id: input.plan.mission_id,
|
|
11
|
+
active_worker_budget: activeWorkerBudget,
|
|
12
|
+
routes
|
|
13
|
+
});
|
|
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
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export function splitActiveWorkerBudget(plan, parallelism) {
|
|
23
|
+
const cap = parallelism === 'safe' ? 8 : parallelism === 'extreme' ? 32 : 16;
|
|
24
|
+
const integrationReserved = 2;
|
|
25
|
+
const nonIntegration = plan.graph.nodes.filter((node) => node.route !== '$Integration');
|
|
26
|
+
const perLoopCap = Math.max(2, Math.floor((cap - integrationReserved) / Math.max(1, nonIntegration.length)));
|
|
27
|
+
const perLoop = nonIntegration.map((node) => ({
|
|
28
|
+
loop_id: node.loop_id,
|
|
29
|
+
maker_checker_workers: Math.min(perLoopCap, node.maker.worker_count + node.checker.worker_count)
|
|
30
|
+
}));
|
|
31
|
+
const used = perLoop.reduce((sum, row) => sum + row.maker_checker_workers, integrationReserved);
|
|
32
|
+
return {
|
|
33
|
+
global_active_workers: cap,
|
|
34
|
+
integration_reserved: integrationReserved,
|
|
35
|
+
per_loop: perLoop,
|
|
36
|
+
headroom: Math.max(0, cap - used)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=naruto-loop-mesh.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function routeNarutoLoopWorker(node, role) {
|
|
2
|
+
const domain = node.loop_id.replace(/^loop-/, '');
|
|
3
|
+
const roles = roleLabels(domain);
|
|
4
|
+
const gates = [...node.gates.triage, ...node.gates.local, ...node.gates.checker, ...node.gates.integration, ...node.gates.final];
|
|
5
|
+
return {
|
|
6
|
+
schema: 'sks.naruto-loop-worker-route.v1',
|
|
7
|
+
loop_id: node.loop_id,
|
|
8
|
+
maker_role: roles.maker,
|
|
9
|
+
checker_role: roles.checker,
|
|
10
|
+
prompt: [
|
|
11
|
+
`loop purpose: ${node.purpose}`,
|
|
12
|
+
`role: ${role === 'maker' ? roles.maker : roles.checker}`,
|
|
13
|
+
`owner files: ${node.owner_scope.files.join(', ') || '-'}`,
|
|
14
|
+
`owner directories: ${node.owner_scope.directories.join(', ') || '-'}`,
|
|
15
|
+
`gates: ${gates.join(', ') || '-'}`,
|
|
16
|
+
`state file: ${node.state_file}`,
|
|
17
|
+
`budget: ${JSON.stringify(node.budget)}`,
|
|
18
|
+
`collision policy: ${node.owner_scope.collision_policy}`,
|
|
19
|
+
'Do not mutate outside owner scope.'
|
|
20
|
+
].join('\n'),
|
|
21
|
+
allowed_files: node.owner_scope.files,
|
|
22
|
+
allowed_directories: node.owner_scope.directories,
|
|
23
|
+
gates,
|
|
24
|
+
mutation_outside_owner_scope_allowed: false
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function roleLabels(domain) {
|
|
28
|
+
if (domain.includes('zellij'))
|
|
29
|
+
return { maker: 'zellij implementer', checker: 'zellij QA/verifier' };
|
|
30
|
+
if (domain.includes('release'))
|
|
31
|
+
return { maker: 'release optimizer', checker: 'release gate verifier' };
|
|
32
|
+
if (domain.includes('research'))
|
|
33
|
+
return { maker: 'source shard/synthesis', checker: 'final reviewer' };
|
|
34
|
+
if (domain.includes('codex'))
|
|
35
|
+
return { maker: 'capability/probe implementer', checker: 'real probe verifier' };
|
|
36
|
+
return { maker: `${domain} implementer`, checker: `${domain} checker` };
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=naruto-loop-worker-router.js.map
|