sneakoscope 3.0.4 → 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 (46) 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/commands/goal-command.js +19 -1
  12. package/dist/core/commands/loop-command.js +135 -0
  13. package/dist/core/fsx.js +1 -1
  14. package/dist/core/init.js +6 -1
  15. package/dist/core/loops/goal-to-loop-compat.js +23 -0
  16. package/dist/core/loops/loop-artifacts.js +41 -0
  17. package/dist/core/loops/loop-decomposer.js +56 -0
  18. package/dist/core/loops/loop-finalizer.js +28 -0
  19. package/dist/core/loops/loop-gate-ladder.js +16 -0
  20. package/dist/core/loops/loop-gate-runner.js +29 -0
  21. package/dist/core/loops/loop-gate-selector.js +52 -0
  22. package/dist/core/loops/loop-iteration-runner.js +2 -0
  23. package/dist/core/loops/loop-lease.js +76 -0
  24. package/dist/core/loops/loop-observability.js +19 -0
  25. package/dist/core/loops/loop-owner-inference.js +57 -0
  26. package/dist/core/loops/loop-owner-ledger.js +2 -0
  27. package/dist/core/loops/loop-planner.js +139 -0
  28. package/dist/core/loops/loop-proof-summary.js +10 -0
  29. package/dist/core/loops/loop-proof.js +2 -0
  30. package/dist/core/loops/loop-risk-classifier.js +42 -0
  31. package/dist/core/loops/loop-runtime.js +159 -0
  32. package/dist/core/loops/loop-scheduler.js +60 -0
  33. package/dist/core/loops/loop-schema.js +63 -0
  34. package/dist/core/loops/loop-state.js +61 -0
  35. package/dist/core/naruto/naruto-loop-mesh.js +33 -0
  36. package/dist/core/naruto/naruto-loop-worker-router.js +38 -0
  37. package/dist/core/pipeline-internals/runtime-core.js +82 -2
  38. package/dist/core/version.js +1 -1
  39. package/dist/core/zellij/zellij-slot-column-anchor.js +5 -2
  40. package/dist/core/zellij/zellij-slot-pane-renderer.js +2 -0
  41. package/dist/scripts/loop-directive-check-lib.js +165 -0
  42. package/package.json +34 -2
  43. package/schemas/loops/loop-node.schema.json +21 -0
  44. package/schemas/loops/loop-plan.schema.json +21 -0
  45. package/schemas/loops/loop-proof.schema.json +20 -0
  46. package/schemas/loops/loop-state.schema.json +19 -0
package/README.md CHANGED
@@ -35,7 +35,7 @@ Set up this agent project with Sneakoscope Codex. Use [[mandarange/Sneakoscope-C
35
35
 
36
36
  ## 🚀 Current Release
37
37
 
38
- SKS **3.0.4** is Codex 0.139-aware while it bundles @openai/codex-sdk 0.138.0 at this release boundary. It detects and uses 0.139 features from the external Codex CLI when that CLI supports them, with release gates that include hermetic fixtures and actual real probes for code-mode web search, preserved `oneOf`/`allOf` tool schemas, doctor env redaction, plugin marketplace `source` and cache behavior, the `-P` profile alias, the multi-agent v2 `interrupt_agent` rename, image referenced-path routing, sandbox/proxy preservation, Zellij stacked/fallback pane proof, pane-lock openWorkerPane integration, release cache safety fixtures, runtime proof summaries, and release proof source-truth artifacts. See [docs/codex-0.139-compat.md](docs/codex-0.139-compat.md) and [docs/codex-0.139-real-probes.md](docs/codex-0.139-real-probes.md).
38
+ SKS **3.1.0** is Codex 0.139-aware while it bundles @openai/codex-sdk 0.138.0 at this release boundary. It detects and uses 0.139 features from the external Codex CLI when that CLI supports them, with release gates that include hermetic fixtures and actual real probes for code-mode web search, preserved `oneOf`/`allOf` tool schemas, doctor env redaction, plugin marketplace `source` and cache behavior, the `-P` profile alias, the multi-agent v2 `interrupt_agent` rename, image referenced-path routing, sandbox/proxy preservation, Zellij stacked/fallback pane proof, pane-lock openWorkerPane integration, release cache safety fixtures, runtime proof summaries, and release proof source-truth artifacts. See [docs/codex-0.139-compat.md](docs/codex-0.139-compat.md) and [docs/codex-0.139-real-probes.md](docs/codex-0.139-real-probes.md).
39
39
 
40
40
  SKS 3.0.0 was the parallel-runtime stabilization release. The whole live-swarm experience — what you actually *see* while 5, 20, or 100 workers run — was rebuilt and proven end-to-end.
41
41
 
@@ -76,7 +76,7 @@ dependencies = [
76
76
 
77
77
  [[package]]
78
78
  name = "sks-core"
79
- version = "3.0.4"
79
+ version = "3.1.0"
80
80
  dependencies = [
81
81
  "serde_json",
82
82
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sks-core"
3
- version = "3.0.4"
3
+ version = "3.1.0"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -4,7 +4,7 @@ use std::io::{self, Read, Seek, SeekFrom};
4
4
  fn main() {
5
5
  let mut args = std::env::args().skip(1);
6
6
  match args.next().as_deref() {
7
- Some("--version") => println!("sks-rs 3.0.4"),
7
+ Some("--version") => println!("sks-rs 3.1.0"),
8
8
  Some("compact-info") => {
9
9
  let mut input = String::new();
10
10
  let _ = io::stdin().read_to_string(&mut input);
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schema": "sks.dist-build-stamp.v1",
3
3
  "package_name": "sneakoscope",
4
- "package_version": "3.0.4",
5
- "source_digest": "fbc7541cf9a5f0432c9ceb28c4478eca651010d2304fc499bbe27f2f62b41d4b",
6
- "source_file_count": 2328,
7
- "built_at_source_time": 1781240948838
4
+ "package_version": "3.1.0",
5
+ "source_digest": "0eed1c5f95e3d3e67d263680e28b27bfd1dabe232ff7265720a57d7d377252e8",
6
+ "source_file_count": 2392,
7
+ "built_at_source_time": 1781256416238
8
8
  }
package/dist/bin/sks.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const FAST_PACKAGE_VERSION = '3.0.4';
2
+ const FAST_PACKAGE_VERSION = '3.1.0';
3
3
  const args = process.argv.slice(2);
4
4
  try {
5
5
  if (args[0] === '--agent' && args[1] === 'worker') {
@@ -123,6 +123,7 @@ export const COMMANDS = {
123
123
  agent: entry('beta', 'Run native multi-session agent missions', 'dist/core/commands/agent-command.js', argsCommand(() => import('../core/commands/agent-command.js'), 'agentCommand', 'dist/core/commands/agent-command.js')),
124
124
  'with-local-llm': entry('beta', 'Enable or inspect local Ollama worker backend', 'dist/core/commands/local-model-command.js', argsCommand(() => import('../core/commands/local-model-command.js'), 'localModelCommand', 'dist/core/commands/local-model-command.js')),
125
125
  naruto: entry('labs', 'Run $Naruto shadow-clone swarm (up to 100 parallel sessions)', 'dist/core/commands/naruto-command.js', argsCommand(() => import('../core/commands/naruto-command.js'), 'narutoCommand', 'dist/core/commands/naruto-command.js')),
126
+ loop: entry('labs', 'Dynamic Loop Runtime: plan/run/status/proof loop graphs.', 'dist/core/commands/loop-command.js', subcommand(() => import('../core/commands/loop-command.js'), 'loopCommand', 'dist/core/commands/loop-command.js', 'help')),
126
127
  'qa-loop': entry('beta', 'Run QA loop missions', 'dist/core/commands/qa-loop-command.js', subcommand(() => import('../core/commands/qa-loop-command.js'), 'qaLoopCommand', 'dist/core/commands/qa-loop-command.js')),
127
128
  research: entry('labs', 'Run research missions', 'dist/core/commands/research-command.js', subcommand(() => import('../core/commands/research-command.js'), 'researchCommand', 'dist/core/commands/research-command.js')),
128
129
  autoresearch: entry('labs', 'Alias for research/autoresearch route', 'dist/core/commands/autoresearch-command.js', subcommand(() => import('../core/commands/autoresearch-command.js'), 'autoresearchCommand', 'dist/core/commands/autoresearch-command.js', 'status')),
@@ -3,7 +3,7 @@ import { getCodexInfo } from '../core/codex-adapter.js';
3
3
  import { context7Docs, context7Resolve, context7Text, context7Tools } from '../core/context7-client.js';
4
4
  import { context7Evidence, recordContext7Evidence } from '../core/pipeline.js';
5
5
  import { stateFile } from '../core/mission.js';
6
- import { checkContext7, ensureProjectContext7Config } from './install-helpers.js';
6
+ import { checkContext7, context7GlobalMcpStatus, ensureProjectContext7Config } from './install-helpers.js';
7
7
  const flag = (args, name) => args.includes(name);
8
8
  export async function context7Command(sub = 'check', args = []) {
9
9
  const action = sub || 'check';
@@ -92,9 +92,24 @@ export async function context7Command(sub = 'check', args = []) {
92
92
  timeoutMs: readNumberOption(args, '--timeout-ms', 30000)
93
93
  });
94
94
  const state = { ...(await readJson(stateFile(root), {})), mission_id: missionId };
95
- await recordContext7Evidence(root, state, { tool_name: 'resolve-library-id', library: libraryNameOrId, library_id: result.library_id, source: result.resolve ? 'sks context7 evidence' : 'sks context7 evidence explicit-library-id' });
95
+ const evidenceQuery = readOption(args, '--query', readOption(args, '--topic', libraryNameOrId));
96
+ const evidenceTopic = readOption(args, '--topic', libraryNameOrId);
97
+ await recordContext7Evidence(root, state, {
98
+ tool_name: 'resolve-library-id',
99
+ library: libraryNameOrId,
100
+ library_id: result.library_id,
101
+ query: evidenceQuery,
102
+ source: result.resolve ? 'sks context7 evidence' : 'sks context7 evidence explicit-library-id'
103
+ });
96
104
  if (result.docs_tool) {
97
- await recordContext7Evidence(root, state, { tool_name: result.docs_tool, library_id: result.library_id, source: 'sks context7 evidence' });
105
+ await recordContext7Evidence(root, state, {
106
+ tool_name: result.docs_tool,
107
+ library_id: result.library_id,
108
+ query: evidenceQuery,
109
+ topic: evidenceTopic,
110
+ tokens: readNumberOption(args, '--tokens', 2000),
111
+ source: 'sks context7 evidence'
112
+ });
98
113
  }
99
114
  const evidence = await context7Evidence(root, state);
100
115
  const out = { ...result, mission_id: missionId, evidence };
@@ -126,12 +141,21 @@ export async function context7Command(sub = 'check', args = []) {
126
141
  const codex = await getCodexInfo();
127
142
  if (!codex.bin)
128
143
  throw new Error('Codex CLI missing. Install separately: npm i -g @openai/codex, or set SKS_CODEX_BIN.');
144
+ const env = { ...process.env };
145
+ env.CODEX_LB_API_KEY = '';
146
+ const existing = await context7GlobalMcpStatus(codex.bin, env);
147
+ if (existing.present) {
148
+ if (flag(args, '--json'))
149
+ return console.log(JSON.stringify({ changed: false, status: 'present', codex_mcp_list: existing }, null, 2));
150
+ console.log('Context7 global MCP already configured; existing entry preserved.');
151
+ return;
152
+ }
129
153
  const cmdArgs = transport === 'remote'
130
154
  ? ['mcp', 'add', 'context7', '--url', 'https://mcp.context7.com/mcp']
131
155
  : ['mcp', 'add', 'context7', '--', 'npx', '-y', '@upstash/context7-mcp@latest'];
132
- const result = await runProcess(codex.bin, cmdArgs, { timeoutMs: 30000, maxOutputBytes: 64 * 1024 });
156
+ const result = await runProcess(codex.bin, cmdArgs, { env, timeoutMs: 30000, maxOutputBytes: 64 * 1024 });
133
157
  if (flag(args, '--json'))
134
- return console.log(JSON.stringify({ command: `${codex.bin} ${cmdArgs.join(' ')}`, result }, null, 2));
158
+ return console.log(JSON.stringify({ changed: result.code === 0, status: result.code === 0 ? 'installed' : 'failed', command: `${codex.bin} ${cmdArgs.join(' ')}`, result }, null, 2));
135
159
  if (result.code !== 0)
136
160
  throw new Error(result.stderr || result.stdout || 'codex mcp add failed');
137
161
  console.log('Context7 global MCP configured.');
@@ -2019,14 +2019,26 @@ async function ensureGlobalContext7DuringInstall() {
2019
2019
  if (!codex.bin)
2020
2020
  return { status: 'codex_missing' };
2021
2021
  const env = withoutSecretEnv(['CODEX_LB_API_KEY']);
2022
- const list = await runProcess(codex.bin, ['mcp', 'list'], { env, timeoutMs: 8000, maxOutputBytes: 32 * 1024 }).catch((err) => ({ code: 1, stderr: err.message, stdout: '' }));
2023
- if (list.code === 0 && /context7/i.test(`${list.stdout}\n${list.stderr}`))
2022
+ const existing = await context7GlobalMcpStatus(codex.bin, env);
2023
+ if (existing.present)
2024
2024
  return { status: 'present' };
2025
2025
  const add = await runProcess(codex.bin, ['mcp', 'add', 'context7', '--', 'npx', '-y', '@upstash/context7-mcp@latest'], { env, timeoutMs: 30000, maxOutputBytes: 64 * 1024 }).catch((err) => ({ code: 1, stdout: '', stderr: err.message }));
2026
2026
  if (add.code === 0)
2027
2027
  return { status: 'installed' };
2028
2028
  return { status: 'failed', error: `${add.stderr || add.stdout || 'codex mcp add failed'}`.trim() };
2029
2029
  }
2030
+ export async function context7GlobalMcpStatus(codexBin, env = process.env) {
2031
+ const list = await runProcess(codexBin, ['mcp', 'list'], { env, timeoutMs: 8000, maxOutputBytes: 32 * 1024 })
2032
+ .catch((err) => ({ code: 1, stderr: err.message, stdout: '' }));
2033
+ const output = `${list.stdout || ''}\n${list.stderr || ''}`;
2034
+ return {
2035
+ checked: true,
2036
+ ok: list.code === 0,
2037
+ present: list.code === 0 && /context7/i.test(output),
2038
+ stdout: list.stdout || '',
2039
+ stderr: list.stderr || ''
2040
+ };
2041
+ }
2030
2042
  function withoutSecretEnv(keys = []) {
2031
2043
  const env = { ...process.env };
2032
2044
  for (const key of keys)
@@ -2348,11 +2360,7 @@ export async function ensureProjectContext7Config(root, transport = 'local') {
2348
2360
  const block = context7ConfigToml(transport).trim();
2349
2361
  const existingBlock = /(^|\n)\[mcp_servers\.context7\]\n[\s\S]*?(?=\n\[[^\]]+\]|\s*$)/;
2350
2362
  if (existingBlock.test(current)) {
2351
- const next = current.replace(existingBlock, `$1${block}\n`);
2352
- if (next === current)
2353
- return false;
2354
- await writeTextAtomic(configPath, next.endsWith('\n') ? next : `${next}\n`);
2355
- return true;
2363
+ return false;
2356
2364
  }
2357
2365
  if (hasContext7ConfigText(current))
2358
2366
  return false;
@@ -3,6 +3,7 @@ import { findLatestMission, missionDir } from '../mission.js';
3
3
  import { readJson, writeJsonAtomic } from '../fsx.js';
4
4
  import { readAgentMessageBus } from './agent-message-bus.js';
5
5
  import { buildZellijWorkerPaneSummary } from '../zellij/zellij-worker-pane-summary.js';
6
+ import { readLoopGraphProof, summarizeLoopGraphProof } from '../loops/loop-observability.js';
6
7
  export const RUNTIME_PROOF_SUMMARY_SCHEMA = 'sks.runtime-proof-summary.v1';
7
8
  export async function buildRuntimeProofSummary(root, missionIdInput = 'latest', opts = {}) {
8
9
  const missionId = missionIdInput === 'latest' ? await findLatestMission(root) : missionIdInput;
@@ -18,6 +19,7 @@ export async function buildRuntimeProofSummary(root, missionIdInput = 'latest',
18
19
  const messagesAll = await readAgentMessageBus(root, missionId, { max: 500 });
19
20
  const recentMessages = await readAgentMessageBus(root, missionId, { max: opts.maxMessages || 8 });
20
21
  const zellijSummary = await buildZellijWorkerPaneSummary(root, missionId).catch(() => null);
22
+ const loopSummary = summarizeLoopGraphProof(await readLoopGraphProof(root, missionId).catch(() => null));
21
23
  const failedMessages = messagesAll.filter((row) => row.event_type === 'worker_failed');
22
24
  const errorMessages = messagesAll.filter((row) => row.level === 'error');
23
25
  const telemetryAgeMs = telemetry?.updated_at ? Math.max(0, Date.now() - Date.parse(telemetry.updated_at)) : Number.MAX_SAFE_INTEGER;
@@ -73,6 +75,7 @@ export async function buildRuntimeProofSummary(root, missionIdInput = 'latest',
73
75
  pane_lock_held_p95_ms: Number(zellijSummary?.pane_lock_held_p95_ms || 0),
74
76
  duplicate_slot_anchor_count: Number(zellijSummary?.duplicate_slot_anchor_count || 0)
75
77
  },
78
+ loops: loopSummary,
76
79
  blockers
77
80
  };
78
81
  await writeJsonAtomic(path.join(agentsDir, 'runtime-proof-summary.json'), summary);
@@ -91,6 +94,7 @@ export function renderRuntimeProofSummary(summary) {
91
94
  `Stack fallback: ${summary.zellij.stacked_fallback_count}`,
92
95
  `Pane lock wait p95: ${summary.zellij.pane_lock_wait_p95_ms}ms`,
93
96
  `SLOTS anchors: ${summary.zellij.duplicate_slot_anchor_count}`,
97
+ `Loops: ${summary.loops.total} total / ${summary.loops.completed} done / ${summary.loops.blocked} blocked / ${summary.loops.speedup_ratio}x`,
94
98
  ...(summary.messages.recent.length ? [
95
99
  'Recent worker messages:',
96
100
  ...summary.messages.recent.map((row) => ` ${messageStatusLabel(row)} ${row.slot_id || row.worker_id}: ${row.message}`)
@@ -4,6 +4,8 @@ import { initProject } from '../init.js';
4
4
  import { createMission, loadMission, setCurrent, stateFile } from '../mission.js';
5
5
  import { GOAL_BRIDGE_ARTIFACT, GOAL_WORKFLOW_ARTIFACT, updateGoalWorkflow, writeGoalWorkflow } from '../goal-workflow.js';
6
6
  import { flag, promptOf, resolveMissionId } from './command-utils.js';
7
+ import { compileGoalToLoopPlan } from '../loops/goal-to-loop-compat.js';
8
+ import { runLoopPlan } from '../loops/loop-runtime.js';
7
9
  export async function goalCommand(sub, args = []) {
8
10
  const known = new Set(['create', 'pause', 'resume', 'clear', 'status', 'help', '--help', '-h']);
9
11
  const action = known.has(sub) ? sub : 'create';
@@ -31,11 +33,27 @@ async function goalCreate(args) {
31
33
  const prompt = promptOf(args);
32
34
  if (!prompt)
33
35
  throw new Error('Missing goal task prompt.');
36
+ if (flag(args, '--legacy-goal-runtime') || process.env.SKS_LEGACY_GOAL_RUNTIME === '1')
37
+ return legacyGoalCreate(root, prompt, args);
38
+ const { id, dir, mission } = await createMission(root, { mode: 'goal', prompt });
39
+ const workflow = await writeGoalWorkflow(dir, mission, { action: 'create', prompt });
40
+ const plan = await compileGoalToLoopPlan({ root, missionId: id, goalText: prompt, legacyGoalOptions: { native_goal: workflow.native_goal } });
41
+ const result = await runLoopPlan({ root, plan, parallelism: 'balanced' });
42
+ await setCurrent(root, { mission_id: id, mode: 'GOAL', route: 'Goal', route_command: '$Goal', phase: result.ok ? 'GOAL_LOOP_COMPLETED' : 'GOAL_LOOP_BLOCKED', questions_allowed: true, implementation_allowed: true, native_goal: workflow.native_goal, stop_gate: 'loop-graph-proof.json' }, { replace: true });
43
+ if (flag(args, '--json'))
44
+ return console.log(JSON.stringify({ schema: 'sks.goal-create.v1', ok: result.ok, mission_id: id, workflow, loop_plan: plan, loop_result: result }, null, 2));
45
+ console.log(`Goal compiled to Loop Graph: ${id}`);
46
+ console.log('Use `sks loop status latest` to inspect.');
47
+ console.log(`Artifact: ${path.relative(root, path.join(dir, GOAL_WORKFLOW_ARTIFACT))}`);
48
+ console.log(`Bridge: ${path.relative(root, path.join(dir, GOAL_BRIDGE_ARTIFACT))}`);
49
+ console.log(`Native Codex control: ${workflow.native_goal.slash_command}`);
50
+ }
51
+ async function legacyGoalCreate(root, prompt, args) {
34
52
  const { id, dir, mission } = await createMission(root, { mode: 'goal', prompt });
35
53
  const workflow = await writeGoalWorkflow(dir, mission, { action: 'create', prompt });
36
54
  await setCurrent(root, { mission_id: id, mode: 'GOAL', route: 'Goal', route_command: '$Goal', phase: 'GOAL_READY', questions_allowed: true, implementation_allowed: true, native_goal: workflow.native_goal, stop_gate: 'none' }, { replace: true });
37
55
  if (flag(args, '--json'))
38
- return console.log(JSON.stringify({ schema: 'sks.goal-create.v1', ok: true, mission_id: id, workflow }, null, 2));
56
+ return console.log(JSON.stringify({ schema: 'sks.goal-create.v1', ok: true, mission_id: id, workflow, runtime: 'legacy-goal' }, null, 2));
39
57
  console.log(`Goal mission created: ${id}`);
40
58
  console.log(`Artifact: ${path.relative(root, path.join(dir, GOAL_WORKFLOW_ARTIFACT))}`);
41
59
  console.log(`Bridge: ${path.relative(root, path.join(dir, GOAL_BRIDGE_ARTIFACT))}`);
@@ -0,0 +1,135 @@
1
+ import path from 'node:path';
2
+ import { printJson } from '../../cli/output.js';
3
+ import { createMission, findLatestMission, loadMission, setCurrent } from '../mission.js';
4
+ import { readJson, sksRoot, writeJsonAtomic } from '../fsx.js';
5
+ import { loopGraphProofPath, loopPlanPath, loopRoot } from '../loops/loop-artifacts.js';
6
+ import { readLoopGraphProof } from '../loops/loop-observability.js';
7
+ import { planLoopsFromRequest } from '../loops/loop-planner.js';
8
+ import { renderLoopProofSummary } from '../loops/loop-proof-summary.js';
9
+ import { runLoopPlan } from '../loops/loop-runtime.js';
10
+ import { flag, promptOf, readFlagValue } from './command-utils.js';
11
+ export async function loopCommand(subcommand = 'help', args = []) {
12
+ const action = subcommand || 'help';
13
+ if (action === 'plan')
14
+ return loopPlan(args);
15
+ if (action === 'run')
16
+ return loopRun(args);
17
+ if (action === 'status')
18
+ return loopStatus(args);
19
+ if (action === 'proof')
20
+ return loopProof(args);
21
+ if (action === 'kill')
22
+ return loopKill(args);
23
+ if (action === 'resume')
24
+ return loopRun(args);
25
+ if (action === 'graph')
26
+ return loopGraph(args);
27
+ console.log(`SKS Loop
28
+
29
+ Usage:
30
+ sks loop plan "<request>" [--json]
31
+ sks loop run latest [--parallelism safe|balanced|extreme] [--json]
32
+ sks loop status latest [--json]
33
+ sks loop proof latest [--json]
34
+ sks loop kill <loop-id|all>
35
+ sks loop resume latest
36
+ sks loop graph latest
37
+ `);
38
+ }
39
+ async function loopPlan(args) {
40
+ const root = await sksRoot();
41
+ const request = promptOf(args);
42
+ if (!request)
43
+ throw new Error('Usage: sks loop plan "<request>" [--json]');
44
+ const { id } = await createMission(root, { mode: 'loop', prompt: request });
45
+ const plan = await planLoopsFromRequest({ root, missionId: id, request, sourceCommand: 'loop' });
46
+ await setCurrent(root, { mission_id: id, mode: 'LOOP', route: 'Loop', route_command: '$Loop', phase: 'LOOP_PLANNED', stop_gate: 'loop-graph-proof.json' }, { replace: true });
47
+ if (flag(args, '--json'))
48
+ return printJson({ schema: 'sks.loop-plan-command.v1', ok: plan.blockers.length === 0, mission_id: id, plan });
49
+ console.log(`Loop plan: ${id}`);
50
+ console.log('Loops:');
51
+ for (const node of plan.graph.nodes) {
52
+ const owner = [...node.owner_scope.files, ...node.owner_scope.directories][0] || 'integration';
53
+ console.log(` ${node.loop_id.padEnd(18)} ${node.level.padEnd(12)} owner ${owner.padEnd(28)} gates ${[...node.gates.triage, ...node.gates.local, ...node.gates.checker, ...node.gates.integration, ...node.gates.final].length}`);
54
+ }
55
+ }
56
+ async function loopRun(args) {
57
+ const root = await sksRoot();
58
+ const missionId = await resolveLoopMission(root, args[0]);
59
+ if (!missionId)
60
+ throw new Error('No loop plan exists. Run: sks loop plan "<request>"');
61
+ const plan = await readJson(loopPlanPath(root, missionId));
62
+ if (plan.blockers.length) {
63
+ console.log(`Loop plan blocked: ${plan.blockers.join(', ')}`);
64
+ return;
65
+ }
66
+ const parallelism = normalizeParallelism(readFlagValue(args, '--parallelism', 'balanced'));
67
+ const result = await runLoopPlan({ root, plan, parallelism });
68
+ await setCurrent(root, { mission_id: missionId, mode: 'LOOP', route: 'Loop', route_command: '$Loop', phase: result.ok ? 'LOOP_COMPLETED' : 'LOOP_BLOCKED', stop_gate: 'loop-graph-proof.json' });
69
+ if (flag(args, '--json'))
70
+ return printJson({ schema: 'sks.loop-run-command.v1', ...result });
71
+ console.log(renderLoopProofSummary(result.graph_proof));
72
+ }
73
+ async function loopStatus(args) {
74
+ const root = await sksRoot();
75
+ const missionId = await resolveLoopMission(root, args[0]);
76
+ if (!missionId)
77
+ throw new Error('Usage: sks loop status <mission-id|latest>');
78
+ const plan = await readJson(loopPlanPath(root, missionId), null);
79
+ const proof = await readLoopGraphProof(root, missionId);
80
+ const states = await Promise.all((plan?.graph.nodes || []).map((node) => readJson(path.join(loopRoot(root, missionId), node.loop_id, 'loop-state.json'), null)));
81
+ const result = { schema: 'sks.loop-status-command.v1', mission_id: missionId, plan_ok: Boolean(plan && plan.blockers.length === 0), graph: proof, states };
82
+ if (flag(args, '--json'))
83
+ return printJson(result);
84
+ console.log(`Loop status: ${missionId}`);
85
+ for (const state of states.filter(Boolean)) {
86
+ console.log(` ${String(state.loop_id).padEnd(18)} ${String(state.status).padEnd(10)} iter ${state.iteration} owner ${(Array.isArray(state.acting_on?.files) ? state.acting_on.files.join(', ') : '-')}`);
87
+ }
88
+ }
89
+ async function loopProof(args) {
90
+ const root = await sksRoot();
91
+ const missionId = await resolveLoopMission(root, args[0]);
92
+ if (!missionId)
93
+ throw new Error('Usage: sks loop proof <mission-id|latest>');
94
+ const proof = await readLoopGraphProof(root, missionId);
95
+ if (!proof)
96
+ throw new Error(`Loop graph proof missing: ${missionId}`);
97
+ if (flag(args, '--json'))
98
+ return printJson(proof);
99
+ console.log(renderLoopProofSummary(proof));
100
+ }
101
+ async function loopGraph(args) {
102
+ const root = await sksRoot();
103
+ const missionId = await resolveLoopMission(root, args[0]);
104
+ if (!missionId)
105
+ throw new Error('Usage: sks loop graph <mission-id|latest>');
106
+ const plan = await readJson(loopPlanPath(root, missionId));
107
+ printJson({ schema: 'sks.loop-graph-command.v1', mission_id: missionId, graph: plan.graph });
108
+ }
109
+ async function loopKill(args) {
110
+ const root = await sksRoot();
111
+ const missionId = await findLatestMission(root);
112
+ const target = args[0];
113
+ if (!missionId || !target)
114
+ throw new Error('Usage: sks loop kill <loop-id|all>');
115
+ await writeJsonAtomic(path.join(loopRoot(root, missionId), 'kill-request.json'), {
116
+ schema: 'sks.loop-kill-request.v1',
117
+ mission_id: missionId,
118
+ target,
119
+ requested_at: new Date().toISOString()
120
+ });
121
+ console.log(`Loop kill requested: ${target}`);
122
+ }
123
+ async function resolveLoopMission(root, arg) {
124
+ if (arg && arg !== 'latest')
125
+ return arg;
126
+ const latest = await findLatestMission(root);
127
+ if (!latest)
128
+ return null;
129
+ const loaded = await loadMission(root, latest).catch(() => null);
130
+ return loaded?.mission?.mode === 'loop' || await readJson(loopPlanPath(root, latest), null) ? latest : null;
131
+ }
132
+ function normalizeParallelism(value) {
133
+ return value === 'safe' || value === 'extreme' ? value : 'balanced';
134
+ }
135
+ //# sourceMappingURL=loop-command.js.map
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.4';
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() {
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