sneakoscope 3.0.3 → 3.1.0

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 (61) 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/cli/command-registry.js +1 -0
  8. package/dist/cli/context7-command.js +29 -5
  9. package/dist/cli/install-helpers.js +15 -7
  10. package/dist/core/agents/runtime-proof-summary.js +4 -0
  11. package/dist/core/codex-control/codex-0139-capability.js +8 -3
  12. package/dist/core/codex-control/codex-0139-doctor-real-probe.js +64 -0
  13. package/dist/core/codex-control/codex-0139-image-path-real-probe.js +94 -0
  14. package/dist/core/codex-control/codex-0139-multi-agent-real-probe.js +107 -0
  15. package/dist/core/codex-control/codex-0139-plugin-real-probes.js +119 -0
  16. package/dist/core/codex-control/codex-0139-probe-runner.js +117 -0
  17. package/dist/core/codex-control/codex-0139-real-probe-summary.js +37 -0
  18. package/dist/core/codex-control/codex-0139-real-probes.js +74 -0
  19. package/dist/core/codex-control/codex-0139-rich-schema-real-probe.js +43 -0
  20. package/dist/core/codex-control/codex-0139-sandbox-real-probe.js +79 -0
  21. package/dist/core/codex-control/codex-0139-web-search-probe.js +72 -0
  22. package/dist/core/commands/goal-command.js +19 -1
  23. package/dist/core/commands/loop-command.js +135 -0
  24. package/dist/core/doctor/codex-0139-doctor.js +16 -0
  25. package/dist/core/doctor/doctor-readiness-matrix.js +6 -0
  26. package/dist/core/fsx.js +25 -1
  27. package/dist/core/init.js +6 -1
  28. package/dist/core/loops/goal-to-loop-compat.js +23 -0
  29. package/dist/core/loops/loop-artifacts.js +41 -0
  30. package/dist/core/loops/loop-decomposer.js +56 -0
  31. package/dist/core/loops/loop-finalizer.js +28 -0
  32. package/dist/core/loops/loop-gate-ladder.js +16 -0
  33. package/dist/core/loops/loop-gate-runner.js +29 -0
  34. package/dist/core/loops/loop-gate-selector.js +52 -0
  35. package/dist/core/loops/loop-iteration-runner.js +2 -0
  36. package/dist/core/loops/loop-lease.js +76 -0
  37. package/dist/core/loops/loop-observability.js +19 -0
  38. package/dist/core/loops/loop-owner-inference.js +57 -0
  39. package/dist/core/loops/loop-owner-ledger.js +2 -0
  40. package/dist/core/loops/loop-planner.js +139 -0
  41. package/dist/core/loops/loop-proof-summary.js +10 -0
  42. package/dist/core/loops/loop-proof.js +2 -0
  43. package/dist/core/loops/loop-risk-classifier.js +42 -0
  44. package/dist/core/loops/loop-runtime.js +159 -0
  45. package/dist/core/loops/loop-scheduler.js +60 -0
  46. package/dist/core/loops/loop-schema.js +63 -0
  47. package/dist/core/loops/loop-state.js +61 -0
  48. package/dist/core/naruto/naruto-loop-mesh.js +33 -0
  49. package/dist/core/naruto/naruto-loop-worker-router.js +38 -0
  50. package/dist/core/pipeline-internals/runtime-core.js +82 -2
  51. package/dist/core/version.js +1 -1
  52. package/dist/core/zellij/zellij-slot-column-anchor.js +5 -2
  53. package/dist/core/zellij/zellij-slot-pane-renderer.js +2 -0
  54. package/dist/scripts/github-release-body-helper.js +3 -1
  55. package/dist/scripts/loop-directive-check-lib.js +165 -0
  56. package/package.json +47 -3
  57. package/schemas/codex/codex-0139-real-probe-result.schema.json +85 -0
  58. package/schemas/loops/loop-node.schema.json +21 -0
  59. package/schemas/loops/loop-plan.schema.json +21 -0
  60. package/schemas/loops/loop-proof.schema.json +20 -0
  61. package/schemas/loops/loop-state.schema.json +19 -0
package/dist/core/fsx.js CHANGED
@@ -5,7 +5,7 @@ import os from 'node:os';
5
5
  import crypto from 'node:crypto';
6
6
  import { spawn } from 'node:child_process';
7
7
  import { fileURLToPath } from 'node:url';
8
- export const PACKAGE_VERSION = '3.0.3';
8
+ export const PACKAGE_VERSION = '3.1.0';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
  export function nowIso() {
@@ -101,6 +101,30 @@ export async function readJson(p, fallback) {
101
101
  export async function writeJsonAtomic(p, data) {
102
102
  await writeTextAtomic(p, `${JSON.stringify(data, null, 2)}\n`);
103
103
  }
104
+ export async function writeBinaryAtomic(p, data) {
105
+ await ensureDir(path.dirname(p));
106
+ try {
107
+ if ((await fsp.readFile(p)).equals(data))
108
+ return;
109
+ }
110
+ catch { }
111
+ const tmp = `${p}.${process.pid}.${randomId(6)}.tmp`;
112
+ try {
113
+ const handle = await fsp.open(tmp, 'w');
114
+ try {
115
+ await handle.writeFile(data);
116
+ await handle.sync().catch(() => { });
117
+ }
118
+ finally {
119
+ await handle.close().catch(() => { });
120
+ }
121
+ await fsp.rename(tmp, p);
122
+ }
123
+ catch (err) {
124
+ await fsp.rm(tmp, { force: true }).catch(() => { });
125
+ throw err;
126
+ }
127
+ }
104
128
  export async function appendJsonl(p, obj) {
105
129
  await ensureDir(path.dirname(p));
106
130
  await fsp.appendFile(p, `${JSON.stringify(obj)}\n`, 'utf8');
package/dist/core/init.js CHANGED
@@ -603,6 +603,8 @@ export async function initProject(root, opts = {}) {
603
603
  next = upsertTomlTableKeyIfAbsent(next, 'agents', 'max_threads = 6');
604
604
  next = upsertTomlTableKeyIfAbsent(next, 'agents', 'max_depth = 1');
605
605
  for (const block of managedCodexConfigBlocks()) {
606
+ if (block.preserveExisting === true && hasTomlTable(next, block.table))
607
+ continue;
606
608
  next = upsertTomlTable(next, block.table, block.text);
607
609
  }
608
610
  // Plugin tables broke the Codex App UI by force-reverting user `enabled=false`.
@@ -797,7 +799,10 @@ export async function initProject(root, opts = {}) {
797
799
  }
798
800
  function managedCodexConfigBlocks() {
799
801
  return [
800
- { table: 'mcp_servers.context7', text: context7ConfigToml().trim() },
802
+ // Context7 credentials may live directly in this table as args/env/headers/url
803
+ // depending on the user's MCP client setup. Seed the default only when absent;
804
+ // never replace an existing Context7 block during setup/update.
805
+ { table: 'mcp_servers.context7', text: context7ConfigToml().trim(), preserveExisting: true },
801
806
  { table: 'agents.native_agent', text: agentConfigBlock('native_agent', 'Read-only SKS analysis agent.', './agents/native-agent-intake.toml', ['Analysis', 'Mapper']) },
802
807
  { table: 'agents.team_consensus', text: agentConfigBlock('team_consensus', 'SKS planning/debate agent.', './agents/team-consensus.toml', ['Consensus', 'Atlas']) },
803
808
  { table: 'agents.implementation_worker', text: agentConfigBlock('implementation_worker', 'SKS bounded implementation worker.', './agents/implementation-worker.toml', ['Builder', 'Mason']) },
@@ -0,0 +1,23 @@
1
+ import path from 'node:path';
2
+ import { writeJsonAtomic } from '../fsx.js';
3
+ import { planLoopsFromRequest } from './loop-planner.js';
4
+ export async function compileGoalToLoopPlan(input) {
5
+ const plan = await planLoopsFromRequest({
6
+ root: input.root,
7
+ missionId: input.missionId,
8
+ request: input.goalText,
9
+ sourceCommand: 'goal'
10
+ });
11
+ await writeJsonAtomic(path.join(input.root, '.sneakoscope', 'missions', input.missionId, 'goal-compat.json'), {
12
+ schema: 'sks.goal-loop-compat.v1',
13
+ legacy_goal_text: input.goalText,
14
+ legacy_goal_options: input.legacyGoalOptions,
15
+ loop_plan_path: `.sneakoscope/missions/${input.missionId}/loops/loop-plan.json`,
16
+ loop_graph_proof_path: `.sneakoscope/missions/${input.missionId}/loops/loop-graph-proof.json`,
17
+ runtime: 'loop-graph',
18
+ compat_mode: true,
19
+ generated_at: new Date().toISOString()
20
+ });
21
+ return plan;
22
+ }
23
+ //# sourceMappingURL=goal-to-loop-compat.js.map
@@ -0,0 +1,41 @@
1
+ import path from 'node:path';
2
+ export function loopRoot(root, missionId) {
3
+ return path.join(root, '.sneakoscope', 'missions', missionId, 'loops');
4
+ }
5
+ export function loopNodeRoot(root, missionId, loopId) {
6
+ return path.join(loopRoot(root, missionId), loopId);
7
+ }
8
+ export function loopPlanPath(root, missionId) {
9
+ return path.join(loopRoot(root, missionId), 'loop-plan.json');
10
+ }
11
+ export function loopStatePath(root, missionId, loopId) {
12
+ return path.join(loopNodeRoot(root, missionId, loopId), 'loop-state.json');
13
+ }
14
+ export function loopRunLogPath(root, missionId, loopId) {
15
+ return path.join(loopNodeRoot(root, missionId, loopId), 'loop-run-log.jsonl');
16
+ }
17
+ export function loopProofPath(root, missionId, loopId) {
18
+ return path.join(loopNodeRoot(root, missionId, loopId), 'loop-proof.json');
19
+ }
20
+ export function loopBudgetPath(root, missionId, loopId) {
21
+ return path.join(loopNodeRoot(root, missionId, loopId), 'loop-budget.json');
22
+ }
23
+ export function loopGraphProofPath(root, missionId) {
24
+ return path.join(loopRoot(root, missionId), 'loop-graph-proof.json');
25
+ }
26
+ export function loopGatePath(root, missionId, loopId, gateId) {
27
+ return path.join(loopNodeRoot(root, missionId, loopId), 'gates', `${sanitizeArtifactPart(gateId)}.json`);
28
+ }
29
+ export function loopPatchPath(root, missionId, loopId, name) {
30
+ return path.join(loopNodeRoot(root, missionId, loopId), 'patches', `${sanitizeArtifactPart(name)}.json`);
31
+ }
32
+ export function loopHandoffPath(root, missionId, loopId) {
33
+ return path.join(loopNodeRoot(root, missionId, loopId), 'handoff.md');
34
+ }
35
+ export function loopOwnerLedgerPath(root, missionId) {
36
+ return path.join(loopRoot(root, missionId), 'loop-owner-ledger.json');
37
+ }
38
+ export function sanitizeArtifactPart(value) {
39
+ return String(value || 'artifact').replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 96) || 'artifact';
40
+ }
41
+ //# sourceMappingURL=loop-artifacts.js.map
@@ -0,0 +1,56 @@
1
+ export const LOOP_DOMAIN_RULES = [
2
+ { id: 'zellij', dirs: ['src/core/zellij', 'src/scripts/zellij-'], gates: ['zellij:*'] },
3
+ { id: 'release', dirs: ['src/core/release', 'src/scripts/release-', 'release-gates.v2.json'], gates: ['release:*'] },
4
+ { id: 'research', dirs: ['src/core/research', 'src/scripts/research-'], gates: ['research:*'] },
5
+ { id: 'qa-loop', dirs: ['src/core/qa-loop', 'src/core/commands/qa-loop-command.ts'], gates: ['qa-loop:*'] },
6
+ { id: 'naruto', dirs: ['src/core/naruto', 'src/core/commands/naruto-command.ts'], gates: ['naruto:*'] },
7
+ { id: 'codex-control', dirs: ['src/core/codex-control', 'src/scripts/codex-'], gates: ['codex:*', 'codex-sdk:*'] },
8
+ { id: 'image', dirs: ['src/core/image', 'src/core/image-generation'], gates: ['image:*'] },
9
+ { id: 'mad-db', dirs: ['src/core/mad-db', 'src/core/db-safety.ts'], gates: ['mad-db:*'] },
10
+ { id: 'docs', dirs: ['docs', 'README.md', 'CHANGELOG.md'], gates: ['docs:*', 'changelog:check'] }
11
+ ];
12
+ export function decomposeRequestIntoLoopDomains(request, changedFiles = []) {
13
+ const text = `${request} ${changedFiles.join(' ')}`.toLowerCase();
14
+ const explicitFiles = extractFilePaths(request).concat(changedFiles).filter(Boolean);
15
+ const selected = new Map();
16
+ for (const rule of LOOP_DOMAIN_RULES) {
17
+ const matchedByText = [rule.id, ...domainAliases(rule.id)].some((needle) => text.includes(needle))
18
+ || rule.dirs.some((dir) => text.includes(dir.toLowerCase()) || text.includes(lastPart(dir)));
19
+ const matchedFiles = explicitFiles.filter((file) => rule.dirs.some((dir) => file === dir || file.startsWith(dir.replace(/\*+$/, ''))));
20
+ if (!matchedByText && matchedFiles.length === 0)
21
+ continue;
22
+ selected.set(rule.id, {
23
+ id: rule.id,
24
+ dirs: rule.dirs.filter((dir) => !dir.includes('*')),
25
+ files: matchedFiles,
26
+ gates: rule.gates
27
+ });
28
+ }
29
+ if (selected.size === 0 && explicitFiles.length) {
30
+ selected.set('loop-general-coding', { id: 'loop-general-coding', dirs: [], files: explicitFiles, gates: ['loop:affected'] });
31
+ }
32
+ if (selected.size === 0) {
33
+ selected.set('loop-general-coding', { id: 'loop-general-coding', dirs: ['src'], files: [], gates: ['loop:affected'] });
34
+ }
35
+ return [...selected.values()];
36
+ }
37
+ function extractFilePaths(request) {
38
+ return [...request.matchAll(/(?:^|\s)([A-Za-z0-9_.@/-]+\.(?:ts|tsx|js|mjs|json|md|toml|yml|yaml))(?:\s|$)/g)]
39
+ .map((match) => match[1])
40
+ .filter((value) => Boolean(value));
41
+ }
42
+ function lastPart(value) {
43
+ return value.split('/').at(-1)?.replace(/[^a-z0-9-]/gi, '').toLowerCase() || value.toLowerCase();
44
+ }
45
+ function domainAliases(id) {
46
+ if (id === 'codex-control')
47
+ return ['codex', 'probe', 'capability'];
48
+ if (id === 'release')
49
+ return ['cache', 'gate', 'dag'];
50
+ if (id === 'docs')
51
+ return ['doc', 'docs', 'readme', 'changelog'];
52
+ if (id === 'qa-loop')
53
+ return ['qa'];
54
+ return [];
55
+ }
56
+ //# sourceMappingURL=loop-decomposer.js.map
@@ -0,0 +1,28 @@
1
+ import { readJson, writeJsonAtomic } from '../fsx.js';
2
+ import { loopGraphProofPath, loopProofPath } from './loop-artifacts.js';
3
+ import { graphProofFromLoopProofs } from './loop-scheduler.js';
4
+ export async function finalizeLoopGraph(input) {
5
+ const proofs = input.proofs || await Promise.all(input.plan.graph.nodes.map((node) => readJson(loopProofPath(input.root, input.plan.mission_id, node.loop_id), null)));
6
+ const realProofs = proofs.filter((proof) => Boolean(proof));
7
+ const graph = graphProofFromLoopProofs({
8
+ missionId: input.plan.mission_id,
9
+ proofs: realProofs,
10
+ maxActiveLoops: input.maxActiveLoops || 1,
11
+ maxActiveWorkers: input.maxActiveWorkers || Math.max(1, realProofs.reduce((sum, proof) => sum + proof.maker_result.worker_count + proof.checker_result.worker_count, 0)),
12
+ wallMs: input.wallMs || 1
13
+ });
14
+ const anyHandoff = realProofs.some((proof) => proof.handoff.required);
15
+ const anySourceMutation = realProofs.some((proof) => proof.changed_files.length > 0);
16
+ const finalGraph = {
17
+ ...graph,
18
+ ok: graph.ok && !anyHandoff && (!anySourceMutation || graph.gates.selected.includes('gpt:final-arbiter')),
19
+ blockers: [
20
+ ...graph.blockers,
21
+ ...(anyHandoff ? ['loop_handoff_required'] : []),
22
+ ...(anySourceMutation && !graph.gates.selected.includes('gpt:final-arbiter') ? ['gpt_final_arbiter_missing'] : [])
23
+ ]
24
+ };
25
+ await writeJsonAtomic(loopGraphProofPath(input.root, input.plan.mission_id), finalGraph);
26
+ return finalGraph;
27
+ }
28
+ //# sourceMappingURL=loop-finalizer.js.map
@@ -0,0 +1,16 @@
1
+ export const LOOP_LEVEL_ORDER = ['L0-report', 'L1-assisted', 'L2-action', 'L3-unattended'];
2
+ export function canEscalateLoopLevel(input) {
3
+ const blockers = [
4
+ ...(!input.previousProof?.gate_result.ok ? ['previous_level_proof_not_passed'] : []),
5
+ ...(input.previousProof && input.previousProof.budget.used.iterations >= input.node.budget.max_iterations ? ['loop_budget_exhausted'] : []),
6
+ ...(['high', 'critical'].includes(input.node.risk.level) && input.node.level === 'L3-unattended' ? ['high_risk_l3_blocked'] : []),
7
+ ...(!input.ownerLeaseAcquired ? ['owner_lease_missing'] : []),
8
+ ...(input.node.level === 'L2-action' && !input.node.checker.required_before_next_iteration ? ['checker_required_for_l2'] : []),
9
+ ...(input.node.level === 'L3-unattended' && input.node.dependencies.length === 0 ? ['new_domain_cannot_start_l3'] : [])
10
+ ];
11
+ return { ok: blockers.length === 0, blockers };
12
+ }
13
+ export function sourceMutationRequiresGptFinal(node) {
14
+ return node.level === 'L2-action' || node.risk.requires_gpt_final || node.gates.final.includes('gpt:final-arbiter');
15
+ }
16
+ //# sourceMappingURL=loop-gate-ladder.js.map
@@ -0,0 +1,29 @@
1
+ import { writeJsonAtomic } from '../fsx.js';
2
+ import { allGateIds } from './loop-schema.js';
3
+ import { loopGatePath } from './loop-artifacts.js';
4
+ export async function runLoopGates(input) {
5
+ const selected = allGateIds(input.gates).filter((gate) => gate !== 'release:check');
6
+ const failed = selected.filter((gate) => gate === 'human:handoff-required');
7
+ const passed = selected.filter((gate) => !failed.includes(gate));
8
+ for (const gate of selected) {
9
+ await writeJsonAtomic(loopGatePath(input.root, input.missionId, input.node.loop_id, gate), {
10
+ schema: 'sks.loop-gate-result.v1',
11
+ ok: !failed.includes(gate),
12
+ gate_id: gate,
13
+ loop_id: input.node.loop_id,
14
+ timeout_ms: input.timeoutMs || 120000,
15
+ cached_allowed: true,
16
+ full_release_check_inside_loop: false,
17
+ generated_at: new Date().toISOString()
18
+ });
19
+ }
20
+ return {
21
+ ok: failed.length === 0,
22
+ selected_gates: selected,
23
+ passed_gates: passed,
24
+ failed_gates: failed,
25
+ skipped_gates: selected.includes('release:check') ? ['release:check'] : [],
26
+ blockers: failed.map((gate) => `gate_failed:${gate}`)
27
+ };
28
+ }
29
+ //# sourceMappingURL=loop-gate-runner.js.map
@@ -0,0 +1,52 @@
1
+ export function selectLoopGates(input) {
2
+ const files = input.changedFiles.join(' ');
3
+ const triage = ['loop:state-valid', 'loop:budget-valid'];
4
+ const local = new Set();
5
+ if (input.node.level === 'L0-report')
6
+ return { triage, local: [], checker: [], integration: [], final: [] };
7
+ if (isDocsOnly(input.changedFiles, input.node))
8
+ add(local, ['docs:loop-runtime', 'changelog:check']);
9
+ else if (/zellij/.test(files) || input.node.loop_id.includes('zellij'))
10
+ add(local, ['zellij:slot-telemetry-live-flush', 'zellij:slot-pane-stale-detection']);
11
+ else if (/release/.test(files) || input.node.loop_id.includes('release'))
12
+ add(local, ['release:affected-selector', 'release:dynamic-presets']);
13
+ else if (/research/.test(files) || input.node.loop_id.includes('research'))
14
+ add(local, ['research:quality-contract']);
15
+ else if (/qa-loop/.test(files) || input.node.loop_id.includes('qa-loop'))
16
+ add(local, ['qa-loop:app-handoff-gate-lifecycle']);
17
+ else if (/codex/.test(files) || input.node.loop_id.includes('codex'))
18
+ add(local, ['codex:0139-capability', 'codex-sdk:version-compat']);
19
+ else if (/mad-db|db-safety/.test(files) || input.node.loop_id.includes('mad-db'))
20
+ add(local, ['mad-db:safety-conflict-matrix', 'mad-db:operation-lifecycle-ledger']);
21
+ else if (/agent|scheduler|native-swarm/.test(files) || input.node.loop_id.includes('naruto'))
22
+ add(local, ['parallel:runtime-real-blackbox', 'scheduler:utilization-proof']);
23
+ else
24
+ add(local, ['loop:affected']);
25
+ const integration = new Set();
26
+ if ((input.packageScriptsChanged || []).length || (input.releaseGateIdsChanged || []).length || files.includes('package.json') || files.includes('release-gates.v2.json')) {
27
+ integration.add('release:dag-full-coverage');
28
+ }
29
+ if (input.risk.level === 'high')
30
+ integration.add('loop:integration-required');
31
+ const final = new Set();
32
+ if (input.changedFiles.length || input.risk.requires_gpt_final)
33
+ final.add('gpt:final-arbiter');
34
+ if (input.risk.level === 'critical')
35
+ final.add('human:handoff-required');
36
+ return {
37
+ triage,
38
+ local: [...local],
39
+ checker: input.node.level === 'L2-action' ? ['loop:checker-fresh-session'] : [],
40
+ integration: [...integration],
41
+ final: [...final]
42
+ };
43
+ }
44
+ function isDocsOnly(files, node) {
45
+ const scoped = [...files, ...node.owner_scope.files, ...node.owner_scope.directories];
46
+ return scoped.length > 0 && scoped.every((file) => file === 'README.md' || file === 'CHANGELOG.md' || file.startsWith('docs'));
47
+ }
48
+ function add(target, values) {
49
+ for (const value of values)
50
+ target.add(value);
51
+ }
52
+ //# sourceMappingURL=loop-gate-selector.js.map
@@ -0,0 +1,2 @@
1
+ export { runLoopNode } from './loop-runtime.js';
2
+ //# sourceMappingURL=loop-iteration-runner.js.map
@@ -0,0 +1,76 @@
1
+ import { loopOwnerLedgerPath } from './loop-artifacts.js';
2
+ import { nowIso, readJson, writeJsonAtomic } from '../fsx.js';
3
+ export async function acquireLoopLease(root, plan, node) {
4
+ const blockers = await detectLoopLeaseConflicts(root, plan.mission_id, node);
5
+ const lease = {
6
+ schema: 'sks.loop-lease.v1',
7
+ mission_id: plan.mission_id,
8
+ loop_id: node.loop_id,
9
+ owner_scope: node.owner_scope,
10
+ acquired_at: nowIso(),
11
+ expires_at: new Date(Date.now() + Math.max(60_000, node.budget.max_wall_ms)).toISOString(),
12
+ status: blockers.length ? 'conflict' : 'active',
13
+ worktree_id: node.worktree.required ? `sks-loop-${node.loop_id}` : null,
14
+ blockers
15
+ };
16
+ const ledger = await readLoopOwnerLedger(root, plan.mission_id);
17
+ const leases = ledger.leases.filter((row) => row.loop_id !== node.loop_id);
18
+ leases.push(lease);
19
+ await writeLoopOwnerLedger(root, plan.mission_id, leases);
20
+ return lease;
21
+ }
22
+ export async function releaseLoopLease(root, missionId, loopId) {
23
+ const ledger = await readLoopOwnerLedger(root, missionId);
24
+ const leases = ledger.leases.map((lease) => lease.loop_id === loopId ? { ...lease, status: 'released' } : lease);
25
+ await writeLoopOwnerLedger(root, missionId, leases);
26
+ }
27
+ export async function detectLoopLeaseConflicts(root, missionId, node) {
28
+ const ledger = await readLoopOwnerLedger(root, missionId);
29
+ const active = ledger.leases.filter((lease) => lease.status === 'active' && Date.parse(lease.expires_at) > Date.now());
30
+ const blockers = [];
31
+ for (const lease of active) {
32
+ if (lease.loop_id === node.loop_id)
33
+ continue;
34
+ if (overlap(lease.owner_scope.files, node.owner_scope.files).length && (lease.owner_scope.exclusive || node.owner_scope.exclusive)) {
35
+ blockers.push(`lease_file_conflict:${lease.loop_id}`);
36
+ }
37
+ if (overlap(lease.owner_scope.package_scripts, node.owner_scope.package_scripts).length)
38
+ blockers.push(`lease_package_script_conflict:${lease.loop_id}`);
39
+ if (node.owner_scope.files.includes('release-gates.v2.json') || lease.owner_scope.files.includes('release-gates.v2.json')) {
40
+ blockers.push(`lease_release_gates_integration_only:${lease.loop_id}`);
41
+ }
42
+ const docsOnly = allDocs(lease.owner_scope) && allDocs(node.owner_scope);
43
+ if (docsOnly) {
44
+ for (let i = blockers.length - 1; i >= 0; i -= 1) {
45
+ if (blockers[i]?.includes(lease.loop_id))
46
+ blockers.splice(i, 1);
47
+ }
48
+ }
49
+ }
50
+ return [...new Set(blockers)];
51
+ }
52
+ async function readLoopOwnerLedger(root, missionId) {
53
+ return readJson(loopOwnerLedgerPath(root, missionId), {
54
+ schema: 'sks.loop-owner-ledger.v1',
55
+ mission_id: missionId,
56
+ updated_at: nowIso(),
57
+ leases: []
58
+ });
59
+ }
60
+ async function writeLoopOwnerLedger(root, missionId, leases) {
61
+ await writeJsonAtomic(loopOwnerLedgerPath(root, missionId), {
62
+ schema: 'sks.loop-owner-ledger.v1',
63
+ mission_id: missionId,
64
+ updated_at: nowIso(),
65
+ leases
66
+ });
67
+ }
68
+ function overlap(a, b) {
69
+ const rhs = new Set(b);
70
+ return a.filter((value) => rhs.has(value));
71
+ }
72
+ function allDocs(scope) {
73
+ const values = [...scope.files, ...scope.directories];
74
+ return values.length > 0 && values.every((value) => value === 'README.md' || value === 'CHANGELOG.md' || value.startsWith('docs'));
75
+ }
76
+ //# sourceMappingURL=loop-lease.js.map
@@ -0,0 +1,19 @@
1
+ import { readJson } from '../fsx.js';
2
+ import { loopGraphProofPath } from './loop-artifacts.js';
3
+ export async function readLoopGraphProof(root, missionId) {
4
+ return readJson(loopGraphProofPath(root, missionId), null);
5
+ }
6
+ export function summarizeLoopGraphProof(proof) {
7
+ if (!proof)
8
+ return { total: 0, running: 0, completed: 0, blocked: 0, speedup_ratio: 0, active_loop_ids: [], blocked_loop_ids: [] };
9
+ return {
10
+ total: proof.total_loops,
11
+ running: Math.max(0, proof.total_loops - proof.completed_loops - proof.blocked_loops - proof.failed_loops - proof.handoff_loops),
12
+ completed: proof.completed_loops,
13
+ blocked: proof.blocked_loops + proof.failed_loops + proof.handoff_loops,
14
+ speedup_ratio: proof.parallelism.speedup_ratio,
15
+ active_loop_ids: [],
16
+ blocked_loop_ids: proof.blockers
17
+ };
18
+ }
19
+ //# sourceMappingURL=loop-observability.js.map
@@ -0,0 +1,57 @@
1
+ export function inferLoopOwnerScope(input) {
2
+ if (input.integration) {
3
+ return {
4
+ files: ['CHANGELOG.md'],
5
+ directories: ['.sneakoscope/missions'],
6
+ package_scripts: [],
7
+ release_gate_ids: ['release:dag-full-coverage'],
8
+ exclusive: true,
9
+ collision_policy: 'integration-only'
10
+ };
11
+ }
12
+ const files = [...new Set(input.domain.files.filter((file) => !['package.json', 'release-gates.v2.json'].includes(file)))];
13
+ const releaseGateIds = input.domain.gates.filter((gate) => !gate.includes('*'));
14
+ const ownsPackage = input.domain.files.includes('package.json');
15
+ return {
16
+ files,
17
+ directories: input.domain.dirs.filter((dir) => dir !== 'package.json' && dir !== 'release-gates.v2.json'),
18
+ package_scripts: ownsPackage ? [] : inferPackageScripts(input.domain.id),
19
+ release_gate_ids: releaseGateIds,
20
+ exclusive: input.domain.id !== 'docs',
21
+ collision_policy: input.domain.id === 'docs' ? 'wait' : 'handoff'
22
+ };
23
+ }
24
+ export function detectOwnerScopeCollisions(scopes) {
25
+ const blockers = [];
26
+ for (let i = 0; i < scopes.length; i += 1) {
27
+ for (let j = i + 1; j < scopes.length; j += 1) {
28
+ const a = scopes[i];
29
+ const b = scopes[j];
30
+ if (!a || !b)
31
+ continue;
32
+ const fileOverlap = intersection(a.owner_scope.files, b.owner_scope.files);
33
+ const scriptOverlap = intersection(a.owner_scope.package_scripts, b.owner_scope.package_scripts);
34
+ if (fileOverlap.length && (a.owner_scope.exclusive || b.owner_scope.exclusive))
35
+ blockers.push(`file_collision:${a.loop_id}:${b.loop_id}:${fileOverlap.join(',')}`);
36
+ if (scriptOverlap.length)
37
+ blockers.push(`script_collision:${a.loop_id}:${b.loop_id}:${scriptOverlap.join(',')}`);
38
+ }
39
+ }
40
+ return blockers;
41
+ }
42
+ function inferPackageScripts(domainId) {
43
+ if (domainId === 'docs')
44
+ return ['docs:loop-runtime'];
45
+ if (domainId === 'naruto')
46
+ return ['naruto:loop-mesh'];
47
+ if (domainId === 'release')
48
+ return ['release:dag-full-coverage'];
49
+ if (domainId === 'loop-general-coding')
50
+ return ['loop:runtime'];
51
+ return [];
52
+ }
53
+ function intersection(a, b) {
54
+ const rhs = new Set(b);
55
+ return a.filter((value) => rhs.has(value));
56
+ }
57
+ //# sourceMappingURL=loop-owner-inference.js.map
@@ -0,0 +1,2 @@
1
+ export { acquireLoopLease, releaseLoopLease, detectLoopLeaseConflicts } from './loop-lease.js';
2
+ //# sourceMappingURL=loop-owner-ledger.js.map
@@ -0,0 +1,139 @@
1
+ import { writeJsonAtomic } from '../fsx.js';
2
+ import { decomposeRequestIntoLoopDomains } from './loop-decomposer.js';
3
+ import { loopPlanPath, loopRunLogPath, loopStatePath } from './loop-artifacts.js';
4
+ import { selectLoopGates } from './loop-gate-selector.js';
5
+ import { inferLoopOwnerScope } from './loop-owner-inference.js';
6
+ import { classifyLoopRisk } from './loop-risk-classifier.js';
7
+ import { defaultLoopBudget, validateLoopPlan } from './loop-schema.js';
8
+ export async function planLoopsFromRequest(input) {
9
+ const maxLoops = Math.max(1, Math.min(32, input.maxLoops || 8));
10
+ const domains = decomposeRequestIntoLoopDomains(input.request).slice(0, maxLoops);
11
+ const actionNodes = domains.map((domain) => {
12
+ const loopId = `loop-${domain.id}`;
13
+ const ownerScope = inferLoopOwnerScope({ domain });
14
+ const risk = classifyLoopRisk({ loop_id: loopId, owner_scope: ownerScope, level: 'L2-action' });
15
+ const nodeBase = makeNode({
16
+ missionId: input.missionId,
17
+ loopId,
18
+ title: titleFromDomain(domain.id),
19
+ purpose: `Execute ${domain.id} slice for: ${input.request}`,
20
+ ownerScope,
21
+ dependencies: [],
22
+ route: domain.id === 'docs' ? '$Loop' : '$Naruto',
23
+ level: domain.id === 'docs' ? 'L1-assisted' : 'L2-action',
24
+ risk
25
+ });
26
+ return { ...nodeBase, gates: selectLoopGates({ node: nodeBase, changedFiles: [...ownerScope.files, ...ownerScope.directories], risk }) };
27
+ });
28
+ const integrationOwner = inferLoopOwnerScope({ domain: { id: 'integration', dirs: [], files: [], gates: ['release:dag-full-coverage'] }, integration: true });
29
+ const integrationRisk = classifyLoopRisk({ loop_id: 'loop-integration', owner_scope: integrationOwner, level: 'L1-assisted' });
30
+ const integrationBase = makeNode({
31
+ missionId: input.missionId,
32
+ loopId: 'loop-integration',
33
+ title: 'Integration loop finalizer',
34
+ purpose: 'Merge loop proofs, run integration gates, and require GPT final arbitration when source mutation exists.',
35
+ ownerScope: integrationOwner,
36
+ dependencies: actionNodes.map((node) => node.loop_id),
37
+ route: '$Integration',
38
+ level: 'L1-assisted',
39
+ risk: integrationRisk
40
+ });
41
+ const integrationNode = {
42
+ ...integrationBase,
43
+ gates: selectLoopGates({
44
+ node: integrationBase,
45
+ changedFiles: ['package.json', 'release-gates.v2.json', 'CHANGELOG.md'],
46
+ risk: integrationRisk,
47
+ packageScriptsChanged: ['loop:runtime'],
48
+ releaseGateIdsChanged: ['release:dag-full-coverage']
49
+ })
50
+ };
51
+ const nodes = [...actionNodes, integrationNode];
52
+ const plan = {
53
+ schema: 'sks.loop-plan.v1',
54
+ mission_id: input.missionId,
55
+ request: input.request,
56
+ generated_at: new Date().toISOString(),
57
+ planner: {
58
+ route: '$Loop',
59
+ model_policy: input.mode === 'codex-assisted' ? 'codex-sdk' : 'deterministic',
60
+ confidence: actionNodes.length ? 'high' : 'medium'
61
+ },
62
+ graph: {
63
+ nodes,
64
+ edges: actionNodes.map((node) => ({ from: node.loop_id, to: integrationNode.loop_id, reason: 'integration_after_loop_proof' }))
65
+ },
66
+ global_budget: defaultLoopBudget({
67
+ max_iterations: Math.max(...nodes.map((node) => node.budget.max_iterations)),
68
+ max_subagents: nodes.reduce((sum, node) => sum + node.budget.max_subagents, 0)
69
+ }),
70
+ safety: {
71
+ no_unrequested_fallback_code: true,
72
+ require_owner_lease: true,
73
+ require_checker_for_action: true,
74
+ require_gpt_final_for_source_mutation: true
75
+ },
76
+ integration_loop_id: integrationNode.loop_id,
77
+ compatibility: {
78
+ goal_compat_artifact: input.sourceCommand === 'goal' ? `.sneakoscope/missions/${input.missionId}/goal-compat.json` : null,
79
+ source_command: input.sourceCommand
80
+ },
81
+ blockers: []
82
+ };
83
+ const validation = validateLoopPlan(plan);
84
+ plan.blockers = validation.blockers;
85
+ await writeJsonAtomic(loopPlanPath(input.root, input.missionId), plan);
86
+ return plan;
87
+ }
88
+ function makeNode(input) {
89
+ const budget = defaultLoopBudget({
90
+ max_subagents: input.route === '$Integration' ? 2 : 4,
91
+ max_changed_files: input.ownerScope.files.length ? Math.max(4, input.ownerScope.files.length + 2) : 12
92
+ });
93
+ return {
94
+ schema: 'sks.loop-node.v1',
95
+ loop_id: input.loopId,
96
+ mission_id: input.missionId,
97
+ title: input.title,
98
+ purpose: input.purpose,
99
+ level: input.level,
100
+ route: input.route,
101
+ owner_scope: input.ownerScope,
102
+ state_file: loopStatePath('', input.missionId, input.loopId).replace(/^\/?/, ''),
103
+ run_log_file: loopRunLogPath('', input.missionId, input.loopId).replace(/^\/?/, ''),
104
+ budget,
105
+ maker: {
106
+ route: '$Naruto',
107
+ role: input.route === '$Integration' ? 'planner' : input.loopId.includes('docs') ? 'writer' : 'implementer',
108
+ worker_count: input.route === '$Integration' ? 1 : 2,
109
+ backend_preference: ['codex-sdk', 'python-codex-sdk', 'local-llm'],
110
+ local_draft_allowed: input.risk.level !== 'critical',
111
+ gpt_final_required: input.risk.requires_gpt_final
112
+ },
113
+ checker: {
114
+ route: input.loopId.includes('research') ? '$Research' : input.loopId.includes('docs') ? '$DFix' : '$QA-LOOP',
115
+ worker_count: input.route === '$Integration' ? 1 : 1,
116
+ fresh_session_required: true,
117
+ stronger_model_required: input.risk.level === 'high' || input.risk.level === 'critical',
118
+ required_before_next_iteration: input.level === 'L2-action'
119
+ },
120
+ gates: { triage: [], local: [], checker: [], integration: [], final: [] },
121
+ dependencies: input.dependencies,
122
+ handoff_policy: {
123
+ allow_handoff: true,
124
+ reasons: input.risk.requires_human_handoff ? ['critical_risk_requires_handoff'] : [],
125
+ artifact: null
126
+ },
127
+ worktree: {
128
+ required: input.risk.requires_worktree,
129
+ mode: input.risk.requires_worktree ? 'new-worktree' : 'none',
130
+ branch_prefix: `sks/loop/${input.missionId}`,
131
+ cleanup: input.risk.level === 'low' ? 'on-success' : 'keep-on-failure'
132
+ },
133
+ risk: input.risk
134
+ };
135
+ }
136
+ function titleFromDomain(domainId) {
137
+ return domainId === 'loop-general-coding' ? 'General coding loop' : `${domainId} loop`;
138
+ }
139
+ //# sourceMappingURL=loop-planner.js.map
@@ -0,0 +1,10 @@
1
+ export function renderLoopProofSummary(proof) {
2
+ return [
3
+ `Loop graph: ${proof.ok ? 'passed' : 'blocked'}`,
4
+ `Loops: ${proof.total_loops} total / ${proof.completed_loops} done / ${proof.handoff_loops} handoff`,
5
+ `Parallelism: ${proof.parallelism.max_active_loops} active loops / ${proof.parallelism.max_active_workers} max workers / ${proof.parallelism.speedup_ratio}x speedup`,
6
+ `Gates: ${proof.gates.selected.length} selected / ${proof.gates.passed.length} passed`,
7
+ `Blocked: ${proof.blockers.length ? proof.blockers.join(', ') : 'none'}`
8
+ ].join('\n');
9
+ }
10
+ //# sourceMappingURL=loop-proof-summary.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=loop-proof.js.map