sneakoscope 3.0.4 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/crates/sks-core/src/main.rs +1 -1
- package/dist/.sks-build-stamp.json +4 -4
- package/dist/bin/sks.js +1 -1
- package/dist/cli/command-registry.js +1 -0
- package/dist/cli/context7-command.js +29 -5
- package/dist/cli/install-helpers.js +15 -7
- package/dist/commands/zellij-slot-column-anchor.js +3 -1
- package/dist/commands/zellij-slot-pane.js +19 -2
- package/dist/core/agents/agent-janitor.js +10 -1
- package/dist/core/agents/agent-orchestrator.js +1 -0
- package/dist/core/agents/agent-runner-ollama.js +11 -4
- package/dist/core/agents/native-cli-session-swarm.js +69 -9
- package/dist/core/agents/runtime-proof-summary.js +4 -0
- package/dist/core/codex-control/codex-task-runner.js +9 -0
- package/dist/core/commands/goal-command.js +19 -1
- package/dist/core/commands/loop-command.js +176 -0
- package/dist/core/commands/naruto-command.js +26 -17
- package/dist/core/commands/team-command.js +1 -0
- package/dist/core/fsx.js +1 -1
- package/dist/core/init.js +6 -1
- package/dist/core/locks/file-lock.js +88 -0
- package/dist/core/loops/goal-to-loop-compat.js +23 -0
- package/dist/core/loops/loop-artifacts.js +72 -0
- package/dist/core/loops/loop-checkpoint.js +22 -0
- package/dist/core/loops/loop-decomposer.js +56 -0
- package/dist/core/loops/loop-finalizer.js +54 -0
- package/dist/core/loops/loop-gate-ladder.js +16 -0
- package/dist/core/loops/loop-gate-registry.js +96 -0
- package/dist/core/loops/loop-gate-runner.js +177 -0
- package/dist/core/loops/loop-gate-selector.js +52 -0
- package/dist/core/loops/loop-gpt-final-arbiter.js +61 -0
- package/dist/core/loops/loop-integration-merge.js +75 -0
- package/dist/core/loops/loop-iteration-runner.js +2 -0
- package/dist/core/loops/loop-lease.js +91 -0
- package/dist/core/loops/loop-observability.js +19 -0
- package/dist/core/loops/loop-owner-inference.js +57 -0
- package/dist/core/loops/loop-owner-ledger.js +2 -0
- package/dist/core/loops/loop-planner.js +170 -0
- package/dist/core/loops/loop-proof-summary.js +10 -0
- package/dist/core/loops/loop-proof.js +2 -0
- package/dist/core/loops/loop-risk-classifier.js +42 -0
- package/dist/core/loops/loop-runtime-control.js +25 -0
- package/dist/core/loops/loop-runtime.js +314 -0
- package/dist/core/loops/loop-scheduler.js +69 -0
- package/dist/core/loops/loop-schema.js +63 -0
- package/dist/core/loops/loop-state.js +61 -0
- package/dist/core/loops/loop-worker-prompts.js +43 -0
- package/dist/core/loops/loop-worker-runtime.js +275 -0
- package/dist/core/loops/loop-worktree-runtime.js +92 -0
- package/dist/core/naruto/naruto-finalizer.js +7 -2
- package/dist/core/naruto/naruto-loop-mesh.js +39 -0
- package/dist/core/naruto/naruto-loop-worker-router.js +38 -0
- package/dist/core/pipeline-internals/runtime-core.js +82 -2
- package/dist/core/proof/proof-schema.js +6 -0
- package/dist/core/proof/proof-writer.js +5 -2
- package/dist/core/proof/root-cause-policy.js +70 -0
- package/dist/core/proof/route-adapter.js +18 -1
- package/dist/core/proof/route-proof-gate.js +4 -0
- package/dist/core/release/release-gate-batch-runner.js +56 -10
- package/dist/core/release/release-gate-cache-v2.js +18 -3
- package/dist/core/release/release-gate-dag.js +65 -17
- package/dist/core/release/release-gate-node.js +2 -1
- package/dist/core/release/release-gate-resource-governor.js +27 -6
- package/dist/core/skills/core-skill-meta-update.js +24 -0
- package/dist/core/skills/core-skill-reflection.js +94 -0
- package/dist/core/skills/core-skill-trainer.js +103 -0
- package/dist/core/trust-kernel/completion-contract.js +4 -0
- package/dist/core/trust-kernel/route-contract.js +4 -1
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-right-column-manager.js +13 -2
- package/dist/core/zellij/zellij-slot-column-anchor.js +45 -5
- package/dist/core/zellij/zellij-slot-pane-renderer.js +37 -10
- package/dist/core/zellij/zellij-slot-telemetry.js +96 -44
- package/dist/core/zellij/zellij-worker-pane-manager.js +42 -4
- package/dist/scripts/loop-directive-check-lib.js +388 -0
- package/dist/scripts/loop-worker-fixture-child.js +53 -0
- package/dist/scripts/naruto-real-local-gpt-final-smoke.js +10 -1
- package/package.json +38 -3
- package/schemas/loops/loop-node.schema.json +21 -0
- package/schemas/loops/loop-plan.schema.json +21 -0
- package/schemas/loops/loop-proof.schema.json +20 -0
- package/schemas/loops/loop-state.schema.json +19 -0
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.
|
|
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.
|
|
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.
|
|
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
|
@@ -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;
|
|
@@ -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);
|
|
@@ -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}`)
|
|
@@ -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
|
|
@@ -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))}`);
|