sneakoscope 3.1.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/commands/zellij-slot-column-anchor.js +3 -1
- package/dist/commands/zellij-slot-pane.js +19 -2
- package/dist/core/agents/agent-janitor.js +10 -1
- package/dist/core/agents/agent-orchestrator.js +1 -0
- package/dist/core/agents/agent-runner-ollama.js +11 -4
- package/dist/core/agents/native-cli-session-swarm.js +69 -9
- package/dist/core/codex-control/codex-task-runner.js +9 -0
- package/dist/core/commands/loop-command.js +54 -13
- package/dist/core/commands/naruto-command.js +26 -17
- package/dist/core/commands/team-command.js +1 -0
- package/dist/core/fsx.js +1 -1
- package/dist/core/locks/file-lock.js +88 -0
- package/dist/core/loops/loop-artifacts.js +33 -2
- package/dist/core/loops/loop-checkpoint.js +22 -0
- package/dist/core/loops/loop-finalizer.js +33 -7
- package/dist/core/loops/loop-gate-registry.js +96 -0
- package/dist/core/loops/loop-gate-runner.js +165 -17
- package/dist/core/loops/loop-gpt-final-arbiter.js +61 -0
- package/dist/core/loops/loop-integration-merge.js +75 -0
- package/dist/core/loops/loop-lease.js +35 -20
- package/dist/core/loops/loop-planner.js +36 -5
- package/dist/core/loops/loop-runtime-control.js +25 -0
- package/dist/core/loops/loop-runtime.js +248 -93
- package/dist/core/loops/loop-scheduler.js +12 -3
- package/dist/core/loops/loop-worker-prompts.js +43 -0
- package/dist/core/loops/loop-worker-runtime.js +275 -0
- package/dist/core/loops/loop-worktree-runtime.js +92 -0
- package/dist/core/naruto/naruto-finalizer.js +7 -2
- package/dist/core/naruto/naruto-loop-mesh.js +7 -1
- package/dist/core/proof/proof-schema.js +6 -0
- package/dist/core/proof/proof-writer.js +5 -2
- package/dist/core/proof/root-cause-policy.js +70 -0
- package/dist/core/proof/route-adapter.js +18 -1
- package/dist/core/proof/route-proof-gate.js +4 -0
- package/dist/core/release/release-gate-batch-runner.js +56 -10
- package/dist/core/release/release-gate-cache-v2.js +18 -3
- package/dist/core/release/release-gate-dag.js +65 -17
- package/dist/core/release/release-gate-node.js +2 -1
- package/dist/core/release/release-gate-resource-governor.js +27 -6
- package/dist/core/skills/core-skill-meta-update.js +24 -0
- package/dist/core/skills/core-skill-reflection.js +94 -0
- package/dist/core/skills/core-skill-trainer.js +103 -0
- package/dist/core/trust-kernel/completion-contract.js +4 -0
- package/dist/core/trust-kernel/route-contract.js +4 -1
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-right-column-manager.js +13 -2
- package/dist/core/zellij/zellij-slot-column-anchor.js +40 -3
- package/dist/core/zellij/zellij-slot-pane-renderer.js +36 -11
- package/dist/core/zellij/zellij-slot-telemetry.js +96 -44
- package/dist/core/zellij/zellij-worker-pane-manager.js +42 -4
- package/dist/scripts/loop-directive-check-lib.js +225 -2
- package/dist/scripts/loop-worker-fixture-child.js +53 -0
- package/dist/scripts/naruto-real-local-gpt-final-smoke.js +10 -1
- package/package.json +5 -2
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.1.
|
|
38
|
+
SKS **3.1.1** 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.1.
|
|
7
|
+
Some("--version") => println!("sks-rs 3.1.1"),
|
|
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.1.
|
|
5
|
-
"source_digest": "
|
|
6
|
-
"source_file_count":
|
|
7
|
-
"built_at_source_time":
|
|
4
|
+
"package_version": "3.1.1",
|
|
5
|
+
"source_digest": "ea95ac89533e71474a3f68a4dbf879d966eb64159ca24da208d6bb066fb7c573",
|
|
6
|
+
"source_file_count": 2410,
|
|
7
|
+
"built_at_source_time": 1781345242524
|
|
8
8
|
}
|
package/dist/bin/sks.js
CHANGED
|
@@ -7,7 +7,9 @@ export async function run(_command = 'zellij-slot-column-anchor', args = []) {
|
|
|
7
7
|
const intervalMs = Math.max(250, Number(readOption(args, '--interval-ms', '1000') || 1000));
|
|
8
8
|
for (;;) {
|
|
9
9
|
const text = await renderZellijSlotColumnAnchorFromArtifacts({ artifactRoot, missionId, mode });
|
|
10
|
-
|
|
10
|
+
// Cursor-home + clear-to-end redraw; `\x1Bc` (RIS) resets the pane's
|
|
11
|
+
// scrollback/modes every tick and intermittently breaks scrolling.
|
|
12
|
+
process.stdout.write('\x1b[H' + text + '\n\x1b[0J');
|
|
11
13
|
if (!watch)
|
|
12
14
|
break;
|
|
13
15
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { renderZellijSlotPaneFromArtifacts, renderZellijSlotPaneStatusFromArtifacts } from '../core/zellij/zellij-slot-pane-renderer.js';
|
|
1
|
+
import { renderZellijSlotPaneFromArtifacts, renderZellijSlotPaneStatusFromArtifacts, resolveZellijSlotPaneExit } from '../core/zellij/zellij-slot-pane-renderer.js';
|
|
2
2
|
export async function run(_command = 'zellij-slot-pane', args = []) {
|
|
3
3
|
const artifactDir = readOption(args, '--artifact-dir', process.cwd()) || process.cwd();
|
|
4
4
|
const artifactRoot = readOption(args, '--artifact-root', artifactDir) || artifactDir;
|
|
@@ -18,9 +18,19 @@ export async function run(_command = 'zellij-slot-pane', args = []) {
|
|
|
18
18
|
}
|
|
19
19
|
for (;;) {
|
|
20
20
|
const text = await renderZellijSlotPaneFromArtifacts({ artifactDir, artifactRoot, missionId, slotId, generationIndex, backend, role, mode });
|
|
21
|
-
process.stdout.write(
|
|
21
|
+
process.stdout.write(redrawFrame(text));
|
|
22
22
|
if (!watch)
|
|
23
23
|
break;
|
|
24
|
+
// Root-cause-3 fix: exit the pane once the worker has reached a terminal state and written its
|
|
25
|
+
// result, so the pane closes (or shows the final exited frame) instead of looping forever and
|
|
26
|
+
// perpetually re-reporting telemetry staleness.
|
|
27
|
+
const shouldExit = await resolveZellijSlotPaneExit({ artifactDir, artifactRoot, missionId, slotId, generationIndex }).catch(() => false);
|
|
28
|
+
if (shouldExit) {
|
|
29
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
30
|
+
const finalText = await renderZellijSlotPaneFromArtifacts({ artifactDir, artifactRoot, missionId, slotId, generationIndex, backend, role, mode });
|
|
31
|
+
process.stdout.write(redrawFrame(finalText));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
24
34
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
25
35
|
}
|
|
26
36
|
}
|
|
@@ -28,6 +38,13 @@ function readOption(args, name, fallback) {
|
|
|
28
38
|
const index = args.indexOf(name);
|
|
29
39
|
return index >= 0 && args[index + 1] ? String(args[index + 1]) : fallback;
|
|
30
40
|
}
|
|
41
|
+
// Redraw in place with cursor-home + clear-to-end instead of `\x1Bc` (RIS).
|
|
42
|
+
// RIS performs a full terminal reset every tick, which wipes the Zellij pane's
|
|
43
|
+
// scrollback and resets scroll position/modes — the cause of intermittent
|
|
44
|
+
// "scrolling stops working" while a watch pane is refreshing.
|
|
45
|
+
function redrawFrame(text) {
|
|
46
|
+
return '\x1b[H' + text + '\n\x1b[0J';
|
|
47
|
+
}
|
|
31
48
|
function hasFlag(args, flag) {
|
|
32
49
|
return args.includes(flag);
|
|
33
50
|
}
|
|
@@ -164,7 +164,16 @@ async function listFiles(dir) {
|
|
|
164
164
|
const out = [];
|
|
165
165
|
if (!(await exists(dir)))
|
|
166
166
|
return out;
|
|
167
|
-
|
|
167
|
+
let entries;
|
|
168
|
+
try {
|
|
169
|
+
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR')
|
|
173
|
+
return out;
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
for (const entry of entries) {
|
|
168
177
|
const file = path.join(dir, entry.name);
|
|
169
178
|
if (entry.isDirectory())
|
|
170
179
|
out.push(...await listFiles(file));
|
|
@@ -477,6 +477,7 @@ export async function runNativeAgentOrchestrator(opts = {}) {
|
|
|
477
477
|
visiblePanes: visualLaneCount,
|
|
478
478
|
expectedWorkerRuntimeMs: targetActiveSlots >= 10 ? 8000 : targetActiveSlots >= 2 ? 2000 : 25,
|
|
479
479
|
minActiveWorkers: Math.min(targetActiveSlots, desiredWorkItemCount),
|
|
480
|
+
...(backend === 'codex-sdk' && opts.real === true ? { minSpeedupRatio: 3 } : {}),
|
|
480
481
|
proofMode: opts.mock === true ? 'mock-process' : 'production',
|
|
481
482
|
requireWorkerPids: opts.nativeCliSwarm !== false && targetActiveSlots >= 16
|
|
482
483
|
});
|
|
@@ -132,19 +132,25 @@ export function classifyOllamaWorkerSlice(slice, input = {}) {
|
|
|
132
132
|
input.agent?.persona_id,
|
|
133
133
|
slice?.role,
|
|
134
134
|
slice?.domain,
|
|
135
|
+
slice?.kind,
|
|
135
136
|
slice?.title,
|
|
136
137
|
slice?.description,
|
|
137
138
|
...(Array.isArray(slice?.target_paths) ? slice.target_paths : [])
|
|
138
139
|
].map((value) => String(value || '')).join('\n');
|
|
139
|
-
const bannedRole = /(?:^|\b)(architect|verifier|safety|integrator|schema|release|ux|db)(?:\b|$)/i.test(String(input.agent?.role || slice?.role || ''));
|
|
140
|
+
const bannedRole = /(?:^|\b)(architect|verifier|checker|reviewer|researcher|safety|integrator|schema|release|ux|db)(?:\b|$)/i.test(String(input.agent?.role || slice?.role || ''));
|
|
140
141
|
const collection = /\b(collect|gather|extract|inventory|list|scan|grep|tail|summarize|catalog)\b|수집|추출|목록|스캔|인벤토리/i.test(text);
|
|
141
142
|
const coding = /\b(code|implement|patch|write|edit|fix|mechanical|simple)\b|코드|작성|수정|구현|패치|단순/i.test(text);
|
|
142
|
-
const banned = /\b(strategy|strategize|planning|plan|architecture|architect|design|review|verify|verification|safety|risk|consensus|debate|orchestrate|policy|decide|decision|migration|database|schema)\b
|
|
143
|
-
|
|
143
|
+
const banned = /\b(strategy|strategize|planning|plan|architecture|architect|design|review|verify|verification|audit|inspect|safety|risk|consensus|debate|orchestrate|policy|decide|decision|migration|database|schema)\b|전략|기획|설계|디자인|검증|검수|리뷰|감사|안전|위험|합의|토론|결정|마이그레이션|데이터베이스/i.test(text);
|
|
144
|
+
// Web research / external lookup must run on GPT, never on the local model:
|
|
145
|
+
// local LLMs hallucinate sources and cannot browse. This wins even over the
|
|
146
|
+
// collection allowlist (e.g. "web research and collect docs" stays on GPT).
|
|
147
|
+
const research = /\b(web|research|browse|browser|crawl|fetch docs|websearch|web search|search the web|investigate|context7)\b|웹|리서치|조사|웹서치|웹 ?검색|검색/i.test(text);
|
|
148
|
+
const allowed = !bannedRole && !banned && !research && (writePaths.length > 0 || collection || coding);
|
|
144
149
|
const blockers = [
|
|
145
150
|
...(allowed ? [] : ['ollama_worker_task_not_simple_code_or_collection']),
|
|
146
151
|
...(bannedRole ? ['ollama_worker_role_blocked'] : []),
|
|
147
|
-
...(banned ? ['ollama_worker_strategy_planning_design_blocked'] : [])
|
|
152
|
+
...(banned ? ['ollama_worker_strategy_planning_design_blocked'] : []),
|
|
153
|
+
...(research ? ['ollama_worker_web_research_blocked'] : [])
|
|
148
154
|
];
|
|
149
155
|
return {
|
|
150
156
|
schema: OLLAMA_WORKER_POLICY_SCHEMA,
|
|
@@ -156,6 +162,7 @@ export function classifyOllamaWorkerSlice(slice, input = {}) {
|
|
|
156
162
|
write_path_count: writePaths.length,
|
|
157
163
|
collection_detected: collection,
|
|
158
164
|
coding_detected: coding,
|
|
165
|
+
research_detected: research,
|
|
159
166
|
blockers
|
|
160
167
|
};
|
|
161
168
|
}
|
|
@@ -379,6 +379,7 @@ class NativeCliSessionSwarmRecorder {
|
|
|
379
379
|
serviceTier: this.input.fastModePolicy.service_tier,
|
|
380
380
|
worktree: worktree ? { id: worktree.id, path: worktree.path, branch: worktree.branch } : null,
|
|
381
381
|
backend: this.input.backend,
|
|
382
|
+
taskTitle: String(input.ctx.slice?.title || input.ctx.slice?.description || input.ctx.slice?.id || '') || null,
|
|
382
383
|
uiMode,
|
|
383
384
|
projectRoot: input.ctx.opts.projectRoot || this.input.projectRoot || input.ctx.opts.cwd,
|
|
384
385
|
rightColumnMode: 'spawn-on-first-worker',
|
|
@@ -484,7 +485,23 @@ class NativeCliSessionSwarmRecorder {
|
|
|
484
485
|
session_id: input.ctx.agent.session_id,
|
|
485
486
|
worker_artifact_dir: input.workerDirRel
|
|
486
487
|
});
|
|
487
|
-
const parsed = await
|
|
488
|
+
const parsed = await this.waitForWorkerResultWithActivity({
|
|
489
|
+
resultPath: path.join(this.root, input.resultRel),
|
|
490
|
+
activityPaths: [
|
|
491
|
+
path.join(this.root, input.heartbeatRel),
|
|
492
|
+
path.join(this.root, input.stdoutRel),
|
|
493
|
+
path.join(this.root, input.stderrRel),
|
|
494
|
+
path.join(this.root, input.workerDirRel, 'codex-sdk-events.jsonl'),
|
|
495
|
+
path.join(this.root, input.workerDirRel, 'python-codex-sdk-events.jsonl'),
|
|
496
|
+
path.join(this.root, input.workerDirRel, 'local-llm-events.jsonl')
|
|
497
|
+
],
|
|
498
|
+
stdoutPath: path.join(this.root, input.stdoutRel),
|
|
499
|
+
ctx: input.ctx,
|
|
500
|
+
heartbeatRel: input.heartbeatRel,
|
|
501
|
+
resultRel: input.resultRel,
|
|
502
|
+
stdoutRel: input.stdoutRel,
|
|
503
|
+
stderrRel: input.stderrRel
|
|
504
|
+
});
|
|
488
505
|
const compactExit = processRun ? await processRun.wait(parsed ? 10000 : 1000) : null;
|
|
489
506
|
this.active.delete(activeToken);
|
|
490
507
|
input.record.closed_at = nowIso();
|
|
@@ -593,6 +610,45 @@ class NativeCliSessionSwarmRecorder {
|
|
|
593
610
|
artifacts: [...new Set([...(Array.isArray(parsed.artifacts) ? parsed.artifacts : []), input.stdoutRel, input.stderrRel, path.join(input.workerDirRel, 'zellij-worker-pane.json')])]
|
|
594
611
|
});
|
|
595
612
|
}
|
|
613
|
+
// Root-cause-2 fix: a fixed 2-minute wall-clock result timeout killed live codex-sdk
|
|
614
|
+
// workers (real runs exceed 2 min), marking false zellij_worker_result_timeout failures and
|
|
615
|
+
// freezing the UI while the worker kept running. Replace it with an activity-aware wait: keep
|
|
616
|
+
// waiting as long as ANY worker artifact (heartbeat, stdout/stderr, sdk event jsonl) was touched
|
|
617
|
+
// recently. Only give up after SKS_ZELLIJ_WORKER_INACTIVITY_TIMEOUT_MS of silence (default 5min)
|
|
618
|
+
// OR an absolute cap SKS_ZELLIJ_WORKER_RESULT_TIMEOUT_MS (default 1h; 0 = no cap). While waiting,
|
|
619
|
+
// emit a heartbeat telemetry event every ~10s so the SLOTS snapshot updated_at stays fresh from
|
|
620
|
+
// the orchestrator side too.
|
|
621
|
+
async waitForWorkerResultWithActivity(input) {
|
|
622
|
+
const inactivityTimeoutMs = Math.max(1000, Number(process.env.SKS_ZELLIJ_WORKER_INACTIVITY_TIMEOUT_MS || 300000));
|
|
623
|
+
const absoluteCapRaw = Number(process.env.SKS_ZELLIJ_WORKER_RESULT_TIMEOUT_MS ?? 3600000);
|
|
624
|
+
const absoluteCapMs = Number.isFinite(absoluteCapRaw) ? absoluteCapRaw : 3600000;
|
|
625
|
+
const start = Date.now();
|
|
626
|
+
let lastActivityMs = start;
|
|
627
|
+
let lastHeartbeatEmit = 0;
|
|
628
|
+
for (;;) {
|
|
629
|
+
const result = await readJson(input.resultPath, null).catch(() => null);
|
|
630
|
+
if (result)
|
|
631
|
+
return result;
|
|
632
|
+
const now = Date.now();
|
|
633
|
+
const newestActivity = await newestMtimeMs(input.activityPaths);
|
|
634
|
+
if (newestActivity != null && newestActivity > lastActivityMs)
|
|
635
|
+
lastActivityMs = newestActivity;
|
|
636
|
+
if (absoluteCapMs > 0 && now - start >= absoluteCapMs)
|
|
637
|
+
return null;
|
|
638
|
+
if (now - lastActivityMs >= inactivityTimeoutMs)
|
|
639
|
+
return null;
|
|
640
|
+
if (now - lastHeartbeatEmit >= 10000) {
|
|
641
|
+
lastHeartbeatEmit = now;
|
|
642
|
+
await this.telemetry(input.ctx, {
|
|
643
|
+
eventType: 'heartbeat',
|
|
644
|
+
status: 'running',
|
|
645
|
+
artifacts: [input.heartbeatRel, input.resultRel, input.stdoutRel, input.stderrRel],
|
|
646
|
+
logTail: await tailFile(input.stdoutPath, 600)
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
650
|
+
}
|
|
651
|
+
}
|
|
596
652
|
async finalize() {
|
|
597
653
|
await this.persist();
|
|
598
654
|
return this.summary();
|
|
@@ -777,15 +833,19 @@ function buildPaneWorkerHeader(input) {
|
|
|
777
833
|
'status: running'
|
|
778
834
|
].join('\n');
|
|
779
835
|
}
|
|
780
|
-
async function
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
836
|
+
async function newestMtimeMs(files) {
|
|
837
|
+
let newest = null;
|
|
838
|
+
for (const file of files) {
|
|
839
|
+
try {
|
|
840
|
+
const mtime = (await fs.promises.stat(file)).mtimeMs;
|
|
841
|
+
if (newest == null || mtime > newest)
|
|
842
|
+
newest = mtime;
|
|
843
|
+
}
|
|
844
|
+
catch {
|
|
845
|
+
// missing file: no activity signal from it
|
|
846
|
+
}
|
|
787
847
|
}
|
|
788
|
-
return
|
|
848
|
+
return newest;
|
|
789
849
|
}
|
|
790
850
|
async function waitForWorkerHeartbeat(file, timeoutMs) {
|
|
791
851
|
const deadline = Date.now() + Math.max(1000, timeoutMs);
|
|
@@ -290,6 +290,15 @@ async function runLocalControlTask(root, task, schema, routerDecision) {
|
|
|
290
290
|
...(validation.ok ? [] : ['local_llm_structured_output_invalid', ...validation.issues.map((issue) => `schema:${issue}`)])
|
|
291
291
|
];
|
|
292
292
|
const workerResult = normalizeWorkerResult(structuredOutput, task, finalBlockers, validation.ok, 'local-llm');
|
|
293
|
+
// Stamp the local-llm request id as backend proof on model-authored patch
|
|
294
|
+
// envelopes; without it agent-patch-schema rejects every local-llm patch
|
|
295
|
+
// with model_authored_backend_proof_missing (the model cannot know the id).
|
|
296
|
+
if (Array.isArray(workerResult.patch_envelopes)) {
|
|
297
|
+
workerResult.patch_envelopes = workerResult.patch_envelopes.map((envelope) => ({
|
|
298
|
+
...envelope,
|
|
299
|
+
backend_ollama_request_id: envelope?.backend_ollama_request_id || adapterResult.requestId
|
|
300
|
+
}));
|
|
301
|
+
}
|
|
293
302
|
const workerResultPath = path.join(root, 'local-llm-worker-result.json');
|
|
294
303
|
await writeJsonAtomic(workerResultPath, workerResult);
|
|
295
304
|
const patchEnvelopePath = Array.isArray(workerResult.patch_envelopes) && workerResult.patch_envelopes.length
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { printJson } from '../../cli/output.js';
|
|
3
3
|
import { createMission, findLatestMission, loadMission, setCurrent } from '../mission.js';
|
|
4
|
-
import { readJson, sksRoot
|
|
5
|
-
import {
|
|
4
|
+
import { readJson, sksRoot } from '../fsx.js';
|
|
5
|
+
import { loopLatestCheckpointPath, loopPlanPath, loopProofPath, loopRoot } from '../loops/loop-artifacts.js';
|
|
6
|
+
import { finalizeLoopGraph } from '../loops/loop-finalizer.js';
|
|
6
7
|
import { readLoopGraphProof } from '../loops/loop-observability.js';
|
|
7
8
|
import { planLoopsFromRequest } from '../loops/loop-planner.js';
|
|
8
9
|
import { renderLoopProofSummary } from '../loops/loop-proof-summary.js';
|
|
9
|
-
import { runLoopPlan } from '../loops/loop-runtime.js';
|
|
10
|
+
import { runLoopNode, runLoopPlan } from '../loops/loop-runtime.js';
|
|
11
|
+
import { scheduleLoopGraph } from '../loops/loop-scheduler.js';
|
|
12
|
+
import { writeLoopKillRequest } from '../loops/loop-runtime-control.js';
|
|
10
13
|
import { flag, promptOf, readFlagValue } from './command-utils.js';
|
|
11
14
|
export async function loopCommand(subcommand = 'help', args = []) {
|
|
12
15
|
const action = subcommand || 'help';
|
|
@@ -21,7 +24,7 @@ export async function loopCommand(subcommand = 'help', args = []) {
|
|
|
21
24
|
if (action === 'kill')
|
|
22
25
|
return loopKill(args);
|
|
23
26
|
if (action === 'resume')
|
|
24
|
-
return
|
|
27
|
+
return loopResume(args);
|
|
25
28
|
if (action === 'graph')
|
|
26
29
|
return loopGraph(args);
|
|
27
30
|
console.log(`SKS Loop
|
|
@@ -32,7 +35,7 @@ Usage:
|
|
|
32
35
|
sks loop status latest [--json]
|
|
33
36
|
sks loop proof latest [--json]
|
|
34
37
|
sks loop kill <loop-id|all>
|
|
35
|
-
sks loop resume latest
|
|
38
|
+
sks loop resume latest [--rerun-completed]
|
|
36
39
|
sks loop graph latest
|
|
37
40
|
`);
|
|
38
41
|
}
|
|
@@ -78,12 +81,21 @@ async function loopStatus(args) {
|
|
|
78
81
|
const plan = await readJson(loopPlanPath(root, missionId), null);
|
|
79
82
|
const proof = await readLoopGraphProof(root, missionId);
|
|
80
83
|
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
|
|
84
|
+
const proofs = await Promise.all((plan?.graph.nodes || []).map((node) => readJson(loopProofPath(root, missionId, node.loop_id), null)));
|
|
85
|
+
const checkpoints = await Promise.all((plan?.graph.nodes || []).map((node) => readJson(loopLatestCheckpointPath(root, missionId, node.loop_id), null)));
|
|
86
|
+
const result = { schema: 'sks.loop-status-command.v1', mission_id: missionId, plan_ok: Boolean(plan && plan.blockers.length === 0), graph: proof, states, proofs, checkpoints };
|
|
82
87
|
if (flag(args, '--json'))
|
|
83
88
|
return printJson(result);
|
|
84
89
|
console.log(`Loop status: ${missionId}`);
|
|
85
90
|
for (const state of states.filter(Boolean)) {
|
|
86
|
-
|
|
91
|
+
const loopId = String(state.loop_id);
|
|
92
|
+
const nodeProof = proofs.find((row) => row?.loop_id === loopId);
|
|
93
|
+
const checkpoint = checkpoints.find((row) => row?.loop_id === loopId);
|
|
94
|
+
const backend = nodeProof?.maker_result?.backend || 'blocked';
|
|
95
|
+
const gates = nodeProof?.gate_result ? `${nodeProof.gate_result.passed_gates.length}/${nodeProof.gate_result.selected_gates.length}` : '-';
|
|
96
|
+
const worktree = nodeProof?.worktree?.id || state.acting_on?.worktree_id || '-';
|
|
97
|
+
const resumable = checkpoint?.resumable ? `resumable:${checkpoint.phase}` : 'resumable:-';
|
|
98
|
+
console.log(` ${loopId.padEnd(18)} ${String(state.status).padEnd(10)} backend ${String(backend).padEnd(24)} gates ${gates.padEnd(5)} worktree ${String(worktree).padEnd(18)} ${resumable}`);
|
|
87
99
|
}
|
|
88
100
|
}
|
|
89
101
|
async function loopProof(args) {
|
|
@@ -112,14 +124,43 @@ async function loopKill(args) {
|
|
|
112
124
|
const target = args[0];
|
|
113
125
|
if (!missionId || !target)
|
|
114
126
|
throw new Error('Usage: sks loop kill <loop-id|all>');
|
|
115
|
-
await
|
|
116
|
-
schema: 'sks.loop-kill-request.v1',
|
|
117
|
-
mission_id: missionId,
|
|
118
|
-
target,
|
|
119
|
-
requested_at: new Date().toISOString()
|
|
120
|
-
});
|
|
127
|
+
await writeLoopKillRequest(root, missionId, target);
|
|
121
128
|
console.log(`Loop kill requested: ${target}`);
|
|
122
129
|
}
|
|
130
|
+
async function loopResume(args) {
|
|
131
|
+
const root = await sksRoot();
|
|
132
|
+
const missionId = await resolveLoopMission(root, args[0]);
|
|
133
|
+
if (!missionId)
|
|
134
|
+
throw new Error('Usage: sks loop resume <mission-id|latest> [--rerun-completed]');
|
|
135
|
+
const plan = await readJson(loopPlanPath(root, missionId));
|
|
136
|
+
const rerunCompleted = flag(args, '--rerun-completed');
|
|
137
|
+
const existingProofs = await Promise.all(plan.graph.nodes.map((node) => readJson(loopProofPath(root, missionId, node.loop_id), null)));
|
|
138
|
+
const completed = new Set(existingProofs.filter((proof) => proof !== null && proof.status === 'completed').map((proof) => proof.loop_id));
|
|
139
|
+
const runnable = rerunCompleted ? plan.graph.nodes : plan.graph.nodes.filter((node) => !completed.has(node.loop_id));
|
|
140
|
+
const schedule = scheduleLoopGraph(runnable, normalizeParallelism(readFlagValue(args, '--parallelism', 'balanced')));
|
|
141
|
+
const started = Date.now();
|
|
142
|
+
const resumedProofs = [];
|
|
143
|
+
for (const batch of schedule.batches) {
|
|
144
|
+
const batchProofs = await Promise.all(batch.map((node) => runLoopNode({ root, plan, node })));
|
|
145
|
+
resumedProofs.push(...batchProofs);
|
|
146
|
+
}
|
|
147
|
+
const mergedProofs = [
|
|
148
|
+
...existingProofs.filter((proof) => proof !== null && proof.status === 'completed' && !rerunCompleted),
|
|
149
|
+
...resumedProofs
|
|
150
|
+
];
|
|
151
|
+
const graphProof = await finalizeLoopGraph({
|
|
152
|
+
root,
|
|
153
|
+
plan,
|
|
154
|
+
proofs: mergedProofs,
|
|
155
|
+
maxActiveLoops: schedule.max_active_loops,
|
|
156
|
+
maxActiveWorkers: Math.max(1, mergedProofs.reduce((sum, proof) => sum + proof.maker_result.worker_count + proof.checker_result.worker_count, 0)),
|
|
157
|
+
wallMs: Math.max(1, Date.now() - started)
|
|
158
|
+
});
|
|
159
|
+
await setCurrent(root, { mission_id: missionId, mode: 'LOOP', route: 'Loop', route_command: '$Loop', phase: graphProof.ok ? 'LOOP_COMPLETED' : 'LOOP_BLOCKED', stop_gate: 'loop-graph-proof.json' });
|
|
160
|
+
if (flag(args, '--json'))
|
|
161
|
+
return printJson({ schema: 'sks.loop-resume-command.v1', ok: graphProof.ok, mission_id: missionId, resumed_loops: resumedProofs.map((proof) => proof.loop_id), skipped_completed: [...completed], graph_proof: graphProof });
|
|
162
|
+
console.log(renderLoopProofSummary(graphProof));
|
|
163
|
+
}
|
|
123
164
|
async function resolveLoopMission(root, arg) {
|
|
124
165
|
if (arg && arg !== 'latest')
|
|
125
166
|
return arg;
|
|
@@ -385,6 +385,9 @@ async function narutoRun(parsed) {
|
|
|
385
385
|
serviceTier: 'fast',
|
|
386
386
|
noFast: false,
|
|
387
387
|
writeMode: writeCapable ? parsed.writeMode || 'parallel' : 'off',
|
|
388
|
+
applyPatches: parsed.applyPatches,
|
|
389
|
+
dryRunPatches: parsed.dryRunPatches,
|
|
390
|
+
maxWriteAgents: parsed.maxWriteAgents,
|
|
388
391
|
gitWorktreePolicy: worktreePolicy,
|
|
389
392
|
narutoWorkGraph: workGraph,
|
|
390
393
|
narutoAllocationPolicy: allocationPolicy,
|
|
@@ -418,18 +421,6 @@ async function narutoRun(parsed) {
|
|
|
418
421
|
blockers: [...(result.proof?.blockers || []), ...(parallelRuntimeOk ? [] : ['naruto_parallel_runtime_proof_below_gate'])],
|
|
419
422
|
updated_at: nowIso()
|
|
420
423
|
});
|
|
421
|
-
await setCurrent(root, {
|
|
422
|
-
mission_id: mission.id,
|
|
423
|
-
route: 'Naruto',
|
|
424
|
-
route_command: '$Naruto',
|
|
425
|
-
mode: 'NARUTO',
|
|
426
|
-
phase: result.ok === true ? 'NARUTO_COMPLETE_OR_REVIEW' : 'NARUTO_BLOCKED',
|
|
427
|
-
native_sessions_verified: nativeProofOk,
|
|
428
|
-
subagents_verified: nativeProofOk,
|
|
429
|
-
naruto_gate_file: 'naruto-gate.json',
|
|
430
|
-
stop_gate: 'naruto-gate.json',
|
|
431
|
-
prompt: parsed.prompt
|
|
432
|
-
});
|
|
433
424
|
const clones = result.roster?.agent_count ?? roster.agent_count;
|
|
434
425
|
const localWorkerSummary = summarizeNarutoLocalWorkerResult(localWorker, result);
|
|
435
426
|
// Finalizer policy: when local LLM workers contributed patches, the GPT
|
|
@@ -437,16 +428,29 @@ async function narutoRun(parsed) {
|
|
|
437
428
|
const finalizer = evaluateNarutoFinalizer({
|
|
438
429
|
localParticipated: Number(localWorkerSummary?.selected_worker_count || 0) > 0,
|
|
439
430
|
gptFinalStatus: result.proof?.gpt_final_status || null,
|
|
440
|
-
applyPatches:
|
|
431
|
+
applyPatches: parsed.applyPatches
|
|
441
432
|
});
|
|
442
433
|
await writeJsonAtomic(path.join(mission.dir, 'naruto-finalizer.json'), {
|
|
443
434
|
...finalizer,
|
|
444
435
|
generated_at: nowIso(),
|
|
445
436
|
mission_id: mission.id
|
|
446
437
|
});
|
|
438
|
+
const summaryOk = result.ok === true && (parsed.applyPatches === true ? finalizer.ok === true : finalizer.run_ok === true);
|
|
439
|
+
await setCurrent(root, {
|
|
440
|
+
mission_id: mission.id,
|
|
441
|
+
route: 'Naruto',
|
|
442
|
+
route_command: '$Naruto',
|
|
443
|
+
mode: 'NARUTO',
|
|
444
|
+
phase: summaryOk ? 'NARUTO_COMPLETE_OR_REVIEW' : 'NARUTO_BLOCKED',
|
|
445
|
+
native_sessions_verified: nativeProofOk,
|
|
446
|
+
subagents_verified: nativeProofOk,
|
|
447
|
+
naruto_gate_file: 'naruto-gate.json',
|
|
448
|
+
stop_gate: 'naruto-gate.json',
|
|
449
|
+
prompt: parsed.prompt
|
|
450
|
+
});
|
|
447
451
|
const summary = {
|
|
448
452
|
schema: NARUTO_RESULT_SCHEMA,
|
|
449
|
-
ok:
|
|
453
|
+
ok: summaryOk,
|
|
450
454
|
mode: 'NARUTO',
|
|
451
455
|
jutsu: 'kage_bunshin_no_jutsu',
|
|
452
456
|
mission_id: result.mission_id,
|
|
@@ -502,6 +506,7 @@ async function narutoRun(parsed) {
|
|
|
502
506
|
headless_workers: parallelRuntime.headless_workers,
|
|
503
507
|
passed: parallelRuntime.passed
|
|
504
508
|
} : null,
|
|
509
|
+
parallel_write_policy: result.parallel_write_policy || null,
|
|
505
510
|
local_worker: localWorkerSummary,
|
|
506
511
|
finalizer,
|
|
507
512
|
proof: result.proof?.status || 'missing',
|
|
@@ -538,6 +543,7 @@ function compactNarutoRunResult(result) {
|
|
|
538
543
|
mission_id: result?.mission_id || null,
|
|
539
544
|
route: result?.route || NARUTO_ROUTE,
|
|
540
545
|
backend: result?.backend || null,
|
|
546
|
+
parallel_write_policy: result?.parallel_write_policy || null,
|
|
541
547
|
target_active_slots: result?.target_active_slots ?? null,
|
|
542
548
|
proof: result?.proof ? {
|
|
543
549
|
ok: result.proof.ok === true,
|
|
@@ -754,7 +760,7 @@ async function narutoHelp(parsed) {
|
|
|
754
760
|
mode: 'NARUTO',
|
|
755
761
|
description: 'Shadow Clone Swarm: fan out up to ' + MAX_NARUTO_AGENT_COUNT + ' parallel clone sessions.',
|
|
756
762
|
usage: [
|
|
757
|
-
'sks naruto run "<task>" [--clones N] [--backend codex-sdk|fake|ollama] [--local-model|--no-ollama] [--work-items N] [--real] [--readonly] [--json]',
|
|
763
|
+
'sks naruto run "<task>" [--clones N] [--backend codex-sdk|fake|ollama] [--local-model|--no-ollama] [--work-items N] [--write-mode parallel|serial|off] [--apply-patches] [--dry-run-patches] [--real] [--readonly] [--json]',
|
|
758
764
|
'sks naruto status [--mission <id>] [--json]',
|
|
759
765
|
'sks naruto proof latest [--messages 20] [--json]'
|
|
760
766
|
],
|
|
@@ -788,6 +794,9 @@ function parseNarutoArgs(args = []) {
|
|
|
788
794
|
const readonly = hasFlag(args, '--readonly') || hasFlag(args, '--read-only');
|
|
789
795
|
const writeModeRaw = String(readOption(args, '--write-mode', hasFlag(args, '--parallel-write') ? 'parallel' : '') || '');
|
|
790
796
|
const writeMode = (['proof-safe', 'parallel', 'serial', 'off'].includes(writeModeRaw) ? writeModeRaw : null);
|
|
797
|
+
const applyPatches = hasFlag(args, '--apply-patches');
|
|
798
|
+
const dryRunPatches = hasFlag(args, '--dry-run-patches') || hasFlag(args, '--dry-run-patch');
|
|
799
|
+
const maxWriteAgents = Math.max(0, Math.floor(Number(readOption(args, '--max-write-agents', '0')) || 0));
|
|
791
800
|
const positionalMission = action === 'dashboard' || action === 'workers' || action === 'status' || action === 'proof'
|
|
792
801
|
? positionalArgs(rest, new Set()).find((arg) => /^latest$|^M-/.test(arg))
|
|
793
802
|
: null;
|
|
@@ -799,9 +808,9 @@ function parseNarutoArgs(args = []) {
|
|
|
799
808
|
const smoke = hasFlag(args, '--smoke');
|
|
800
809
|
const parallelism = normalizeParallelism(readOption(args, '--parallelism', 'extreme'));
|
|
801
810
|
const messages = normalizeMessages(readOption(args, '--messages', '8'));
|
|
802
|
-
const valueFlags = new Set(['--clones', '--agents', '--work-items', '--concurrency', '--target-active-slots', '--backend', '--write-mode', '--mission', '--mission-id', '--ollama-model', '--local-model-model', '--ollama-base-url', '--local-model-base-url', '--parallelism', '--messages']);
|
|
811
|
+
const valueFlags = new Set(['--clones', '--agents', '--work-items', '--concurrency', '--target-active-slots', '--backend', '--write-mode', '--max-write-agents', '--mission', '--mission-id', '--ollama-model', '--local-model-model', '--ollama-base-url', '--local-model-base-url', '--parallelism', '--messages']);
|
|
803
812
|
const prompt = positionalArgs(rest, valueFlags).join(' ').trim() || 'Naruto shadow clone swarm run';
|
|
804
|
-
return { action, prompt, clones, workItems, concurrency, backend, backendExplicit, mock, real, readonly, ollamaEnabled: useOllama && !noOllama, noOllama, ollamaModel, ollamaBaseUrl, writeMode, json, missionId, noOpenZellij, attach, smoke, parallelism, messages };
|
|
813
|
+
return { action, prompt, clones, workItems, concurrency, backend, backendExplicit, mock, real, readonly, ollamaEnabled: useOllama && !noOllama, noOllama, ollamaModel, ollamaBaseUrl, writeMode, applyPatches, dryRunPatches, maxWriteAgents, json, missionId, noOpenZellij, attach, smoke, parallelism, messages };
|
|
805
814
|
}
|
|
806
815
|
function normalizeParallelism(value) {
|
|
807
816
|
const text = String(value || 'extreme').toLowerCase();
|
|
@@ -25,6 +25,7 @@ async function redirectTeamCreateToNaruto(args = []) {
|
|
|
25
25
|
redirected_to: 'sks naruto run',
|
|
26
26
|
route_command: '$Naruto',
|
|
27
27
|
deprecated_route: '$Team',
|
|
28
|
+
parallel_write_policy: result?.parallel_write_policy || result?.run?.parallel_write_policy || null,
|
|
28
29
|
created_at: nowIso(),
|
|
29
30
|
args: list
|
|
30
31
|
});
|
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.1.
|
|
8
|
+
export const PACKAGE_VERSION = '3.1.1';
|
|
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() {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import fsp from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { ensureDir, nowIso, randomId, writeJsonAtomic } from '../fsx.js';
|
|
4
|
+
import { guardedRm, guardContextForRoute } from '../safety/mutation-guard.js';
|
|
5
|
+
import { CONFIRMATION_REQUIRED, REQUESTED_SCOPE_CONTRACT_SCHEMA } from '../safety/requested-scope-contract.js';
|
|
6
|
+
export async function withFileLock(input, fn) {
|
|
7
|
+
const lockPath = path.resolve(input.lockPath);
|
|
8
|
+
const timeoutMs = Math.max(1, input.timeoutMs);
|
|
9
|
+
const staleMs = Math.max(1, input.staleMs);
|
|
10
|
+
const started = Date.now();
|
|
11
|
+
const owner = `${process.pid}-${randomId(8)}`;
|
|
12
|
+
await ensureDir(path.dirname(lockPath));
|
|
13
|
+
while (true) {
|
|
14
|
+
try {
|
|
15
|
+
await fsp.mkdir(lockPath);
|
|
16
|
+
await writeJsonAtomic(path.join(lockPath, 'owner.json'), {
|
|
17
|
+
schema: 'sks.file-lock-owner.v1',
|
|
18
|
+
owner,
|
|
19
|
+
pid: process.pid,
|
|
20
|
+
acquired_at: nowIso(),
|
|
21
|
+
stale_ms: staleMs
|
|
22
|
+
});
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
const code = errorCode(err);
|
|
27
|
+
if (code !== 'EEXIST')
|
|
28
|
+
throw err;
|
|
29
|
+
await recoverStaleLock(lockPath, staleMs);
|
|
30
|
+
if (Date.now() - started > timeoutMs) {
|
|
31
|
+
throw new Error(`file_lock_timeout:${lockPath}`);
|
|
32
|
+
}
|
|
33
|
+
await sleep(jitterDelay());
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
return await fn();
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
await removeLockDir(lockPath).catch(() => undefined);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function recoverStaleLock(lockPath, staleMs) {
|
|
44
|
+
try {
|
|
45
|
+
const stat = await fsp.stat(lockPath);
|
|
46
|
+
if (Date.now() - stat.mtimeMs > staleMs) {
|
|
47
|
+
await removeLockDir(lockPath);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch { }
|
|
51
|
+
}
|
|
52
|
+
async function removeLockDir(lockPath) {
|
|
53
|
+
await guardedRm(guardContextForRoute(process.cwd(), lockScopeContract(lockPath), 'remove SKS file lock directory'), lockPath, {
|
|
54
|
+
recursive: true,
|
|
55
|
+
force: true
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function lockScopeContract(lockPath) {
|
|
59
|
+
const resolved = path.resolve(lockPath);
|
|
60
|
+
return {
|
|
61
|
+
schema: REQUESTED_SCOPE_CONTRACT_SCHEMA,
|
|
62
|
+
route: 'internal:file-lock',
|
|
63
|
+
user_request: 'Remove only the current SKS file-lock directory.',
|
|
64
|
+
allowed_mutations: {
|
|
65
|
+
project_files: true,
|
|
66
|
+
global_codex_config: false,
|
|
67
|
+
codex_app_process: false,
|
|
68
|
+
codex_lb_auth: false,
|
|
69
|
+
package_install: false,
|
|
70
|
+
zellij_install: false,
|
|
71
|
+
network: false,
|
|
72
|
+
skill_snapshot_promotion: false
|
|
73
|
+
},
|
|
74
|
+
allowed_paths: [resolved, `${resolved}/**`],
|
|
75
|
+
forbidden_paths: ['~/.codex/config.toml', '/Applications/**'],
|
|
76
|
+
requires_explicit_confirmation: [...CONFIRMATION_REQUIRED]
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function jitterDelay() {
|
|
80
|
+
return 15 + Math.floor(Math.random() * 45);
|
|
81
|
+
}
|
|
82
|
+
function sleep(ms) {
|
|
83
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
84
|
+
}
|
|
85
|
+
function errorCode(err) {
|
|
86
|
+
return err && typeof err === 'object' && 'code' in err ? String(err.code) : '';
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=file-lock.js.map
|