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.
- 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/core/agents/runtime-proof-summary.js +4 -0
- package/dist/core/commands/goal-command.js +19 -1
- package/dist/core/commands/loop-command.js +135 -0
- package/dist/core/fsx.js +1 -1
- package/dist/core/init.js +6 -1
- package/dist/core/loops/goal-to-loop-compat.js +23 -0
- package/dist/core/loops/loop-artifacts.js +41 -0
- package/dist/core/loops/loop-decomposer.js +56 -0
- package/dist/core/loops/loop-finalizer.js +28 -0
- package/dist/core/loops/loop-gate-ladder.js +16 -0
- package/dist/core/loops/loop-gate-runner.js +29 -0
- package/dist/core/loops/loop-gate-selector.js +52 -0
- package/dist/core/loops/loop-iteration-runner.js +2 -0
- package/dist/core/loops/loop-lease.js +76 -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 +139 -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.js +159 -0
- package/dist/core/loops/loop-scheduler.js +60 -0
- package/dist/core/loops/loop-schema.js +63 -0
- package/dist/core/loops/loop-state.js +61 -0
- package/dist/core/naruto/naruto-loop-mesh.js +33 -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/version.js +1 -1
- package/dist/core/zellij/zellij-slot-column-anchor.js +5 -2
- package/dist/core/zellij/zellij-slot-pane-renderer.js +2 -0
- package/dist/scripts/loop-directive-check-lib.js +165 -0
- package/package.json +34 -2
- 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
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
|
|
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
|
|
|
@@ -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
|
|
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
|
|
5
|
-
"source_digest": "
|
|
6
|
-
"source_file_count":
|
|
7
|
-
"built_at_source_time":
|
|
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
|
@@ -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
|
-
|
|
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, {
|
|
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
|
|
2023
|
-
if (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|