sneakoscope 4.0.9 → 4.0.11
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 +9 -1
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/bin/sks.js +1 -1
- package/dist/core/fsx.js +1 -1
- package/dist/core/pipeline-internals/runtime-gates.js +5 -2
- package/dist/core/providers/glm/glm-request-cache.js +9 -4
- package/dist/core/providers/glm/naruto/glm-naruto-apply-transaction.js +90 -0
- package/dist/core/providers/glm/naruto/glm-naruto-bench.js +65 -12
- package/dist/core/providers/glm/naruto/glm-naruto-combined-patch.js +49 -0
- package/dist/core/providers/glm/naruto/glm-naruto-command.js +9 -22
- package/dist/core/providers/glm/naruto/glm-naruto-concurrency-governor.js +10 -9
- package/dist/core/providers/glm/naruto/glm-naruto-conflict-graph.js +7 -1
- package/dist/core/providers/glm/naruto/glm-naruto-decomposer.js +54 -4
- package/dist/core/providers/glm/naruto/glm-naruto-finalizer.js +1 -0
- package/dist/core/providers/glm/naruto/glm-naruto-hunk-conflict.js +16 -0
- package/dist/core/providers/glm/naruto/glm-naruto-hunk-parser.js +36 -0
- package/dist/core/providers/glm/naruto/glm-naruto-isolation-policy.js +38 -0
- package/dist/core/providers/glm/naruto/glm-naruto-merge-planner.js +9 -4
- package/dist/core/providers/glm/naruto/glm-naruto-metrics.js +34 -0
- package/dist/core/providers/glm/naruto/glm-naruto-orchestrator.js +116 -28
- package/dist/core/providers/glm/naruto/glm-naruto-patch-candidate-gate.js +42 -0
- package/dist/core/providers/glm/naruto/glm-naruto-patch-candidate-parser.js +62 -0
- package/dist/core/providers/glm/naruto/glm-naruto-scoreboard.js +75 -0
- package/dist/core/providers/glm/naruto/glm-naruto-secret-audit.js +91 -0
- package/dist/core/providers/glm/naruto/glm-naruto-session-id.js +10 -0
- package/dist/core/providers/glm/naruto/glm-naruto-terminal.js +44 -0
- package/dist/core/providers/glm/naruto/glm-naruto-trace.js +47 -18
- package/dist/core/providers/glm/naruto/glm-naruto-usage-extractor.js +31 -0
- package/dist/core/providers/glm/naruto/glm-naruto-verifier-output.js +26 -0
- package/dist/core/providers/glm/naruto/glm-naruto-worker-artifacts.js +56 -0
- package/dist/core/providers/glm/naruto/glm-naruto-worker-pool.js +81 -12
- package/dist/core/providers/glm/naruto/glm-naruto-worker-runtime.js +217 -19
- package/dist/core/providers/glm/naruto/glm-naruto-worktree-cleanup.js +20 -0
- package/dist/core/providers/glm/naruto/glm-naruto-worktree-manager.js +57 -0
- package/dist/core/providers/glm/naruto/glm-naruto-worktree-worker.js +44 -0
- package/dist/core/providers/openrouter/openrouter-client.js +1 -1
- package/dist/core/providers/openrouter/openrouter-provider-health.js +3 -2
- package/dist/core/providers/openrouter/openrouter-stream.js +31 -14
- package/dist/core/stop-gate/stop-gate-check.js +14 -2
- package/dist/core/stop-gate/stop-gate-writer.js +61 -11
- package/dist/core/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,7 +35,15 @@ Set up this agent project with Sneakoscope Codex. Use [[mandarange/Sneakoscope-C
|
|
|
35
35
|
|
|
36
36
|
## 🚀 Current Release
|
|
37
37
|
|
|
38
|
-
SKS **4.0.
|
|
38
|
+
SKS **4.0.11** makes GLM Naruto operationally measurable: `--worktree` is honest, live bench records real worker/verifier/cache metrics, candidates are scored before merge planning, final apply writes rollback-aware transaction evidence, and early terminal paths write canonical stop-gates. Ordinary direct GLM remains available for single-path edits, while GLM Naruto is the measured parallel runtime.
|
|
39
|
+
|
|
40
|
+
What changed in 4.0.11:
|
|
41
|
+
|
|
42
|
+
- **Honest worktree isolation.** `sks --mad --glm --naruto --worktree "<task>"` uses per-worker git worktrees when available or blocks unless `--allow-patch-envelope-fallback` is explicit.
|
|
43
|
+
- **Measured live bench.** `--bench --live --no-apply` still runs verifier/scoring and reports TTFT, total latency, verifier pass rate, cache tokens, reasoning tokens, and worker completion/failure counts.
|
|
44
|
+
- **Scoreboard-driven merge planning.** `candidate-scoreboard.json` captures gate, verifier, risk, confidence, path, conflict, latency, cache, diversity, and secret-safety components.
|
|
45
|
+
- **Rollback-aware apply.** Final mutation is single-threaded and records `apply-transaction.json`, selected combined patch, diff hashes, and rollback evidence.
|
|
46
|
+
- **Terminal evidence and artifact safety.** Missing-key, invalid-graph, budget, and no-candidate terminal paths write canonical stop-gates, and GLM Naruto artifacts use key-aware secret audit/redaction.
|
|
39
47
|
|
|
40
48
|
What changed in 4.0.8:
|
|
41
49
|
|
package/dist/bin/sks.js
CHANGED
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 = '4.0.
|
|
8
|
+
export const PACKAGE_VERSION = '4.0.11';
|
|
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() {
|
|
@@ -236,7 +236,9 @@ export async function evaluateStop(root, state, payload, opts = {}) {
|
|
|
236
236
|
if (state?.mission_id && state?.stop_gate && !['none', 'honest_mode', 'clarification-gate'].includes(state.stop_gate)) {
|
|
237
237
|
// 4.0.9: Use canonical stop-gate resolver first for NARUTO/GLM_NARUTO routes.
|
|
238
238
|
const modeUpper = String(state?.mode || '').toUpperCase();
|
|
239
|
-
|
|
239
|
+
const routeUpper = String(state?.route || state?.route_command || '').replace(/^\$/, '').toUpperCase();
|
|
240
|
+
const narutoFamily = modeUpper === 'NARUTO' || routeUpper === 'NARUTO' || routeUpper === 'GLM_NARUTO';
|
|
241
|
+
if (narutoFamily || state.stop_gate === 'stop-gate.json' || state.stop_gate === 'naruto-gate.json') {
|
|
240
242
|
const stopCheck = await checkStopGate({
|
|
241
243
|
root,
|
|
242
244
|
route: state.route || state.mode,
|
|
@@ -244,7 +246,8 @@ export async function evaluateStop(root, state, payload, opts = {}) {
|
|
|
244
246
|
explicitGatePath: typeof state.stop_gate_abs_path === 'string' && state.stop_gate_abs_path ? state.stop_gate_abs_path : undefined,
|
|
245
247
|
});
|
|
246
248
|
if (stopCheck.action === 'allow_stop') {
|
|
247
|
-
|
|
249
|
+
if (narutoFamily)
|
|
250
|
+
return { continue: true, systemMessage: `SKS: canonical stop-gate passed at ${stopCheck.gate_path}` };
|
|
248
251
|
}
|
|
249
252
|
else if (stopCheck.action === 'hard_blocked') {
|
|
250
253
|
return { continue: true, systemMessage: `SKS: ${stopCheck.feedback}` };
|
|
@@ -3,8 +3,10 @@ import { SksLruCache } from '../../perf/lru-cache.js';
|
|
|
3
3
|
export function createGlmEncodedRequestCache(maxEntries = 128) {
|
|
4
4
|
return new SksLruCache(maxEntries);
|
|
5
5
|
}
|
|
6
|
-
export function encodeGlmRequestWithCache(
|
|
7
|
-
const
|
|
6
|
+
export function encodeGlmRequestWithCache(input, cache = defaultEncodedRequestCache) {
|
|
7
|
+
const request = 'request' in input ? input.request : input;
|
|
8
|
+
const stringify = 'request' in input && input.stringify ? input.stringify : JSON.stringify;
|
|
9
|
+
const key = 'request' in input && input.cacheKeyParts ? digestRequestCacheKeyParts(input.cacheKeyParts) : digestRequestForCache(request);
|
|
8
10
|
const hit = cache.get(key);
|
|
9
11
|
// Fix 18.2: On cache hit, return stored body without JSON.stringify
|
|
10
12
|
if (hit) {
|
|
@@ -12,11 +14,11 @@ export function encodeGlmRequestWithCache(request, cache = defaultEncodedRequest
|
|
|
12
14
|
return { body: hit.body, entry: hit, cacheHit: true };
|
|
13
15
|
}
|
|
14
16
|
// Even for non-stored bodies, skip re-stringifying by computing from request
|
|
15
|
-
const body =
|
|
17
|
+
const body = stringify(request);
|
|
16
18
|
return { body, entry: hit, cacheHit: true };
|
|
17
19
|
}
|
|
18
20
|
// Cache miss: stringify once
|
|
19
|
-
const body =
|
|
21
|
+
const body = stringify(request);
|
|
20
22
|
if (containsSecretLikeContent(body)) {
|
|
21
23
|
const entry = {
|
|
22
24
|
key,
|
|
@@ -40,6 +42,9 @@ export function encodeGlmRequestWithCache(request, cache = defaultEncodedRequest
|
|
|
40
42
|
cache.set(key, entry);
|
|
41
43
|
return { body, entry, cacheHit: false };
|
|
42
44
|
}
|
|
45
|
+
export function digestRequestCacheKeyParts(parts) {
|
|
46
|
+
return crypto.createHash('sha256').update(stableStringify(parts)).digest('hex');
|
|
47
|
+
}
|
|
43
48
|
export function digestRequestForCache(request) {
|
|
44
49
|
const safe = {
|
|
45
50
|
model: request.model,
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { sha256, writeJsonAtomic, writeTextAtomic } from '../../../fsx.js';
|
|
4
|
+
import { parseUnifiedDiffPatch } from '../glm-patch-parser.js';
|
|
5
|
+
import { combineGlmNarutoPatches } from './glm-naruto-combined-patch.js';
|
|
6
|
+
export async function runGlmNarutoApplyTransaction(input) {
|
|
7
|
+
const preStatus = await gitText(input.cwd, ['status', '--short']);
|
|
8
|
+
const preDiff = await gitText(input.cwd, ['diff', '--binary']);
|
|
9
|
+
const patch = combineGlmNarutoPatches(input.envelopes, input.selectedPatchIds);
|
|
10
|
+
const parsed = parseUnifiedDiffPatch(patch);
|
|
11
|
+
const patchPath = path.join(input.artifactDir, 'selected-combined.patch');
|
|
12
|
+
await writeTextAtomic(patchPath, patch);
|
|
13
|
+
const blockers = [];
|
|
14
|
+
let applyCheckPassed = false;
|
|
15
|
+
let applyPassed = false;
|
|
16
|
+
let rollbackAttempted = false;
|
|
17
|
+
let rollbackPassed = null;
|
|
18
|
+
let finalStatus = 'blocked';
|
|
19
|
+
if (!patch.trim()) {
|
|
20
|
+
blockers.push('combined_patch_empty');
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
const checked = await gitApply(input.cwd, patch, ['apply', '--check', '--whitespace=nowarn', '-']);
|
|
24
|
+
applyCheckPassed = checked.code === 0;
|
|
25
|
+
if (!applyCheckPassed)
|
|
26
|
+
blockers.push(checked.stderr || checked.stdout || 'git_apply_check_failed');
|
|
27
|
+
if (applyCheckPassed) {
|
|
28
|
+
const applied = await gitApply(input.cwd, patch, ['apply', '--whitespace=nowarn', '-']);
|
|
29
|
+
applyPassed = applied.code === 0;
|
|
30
|
+
if (applyPassed) {
|
|
31
|
+
finalStatus = 'applied';
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
blockers.push(applied.stderr || applied.stdout || 'git_apply_failed');
|
|
35
|
+
rollbackAttempted = true;
|
|
36
|
+
const rollback = await gitApply(input.cwd, patch, ['apply', '-R', '--whitespace=nowarn', '-']);
|
|
37
|
+
rollbackPassed = rollback.code === 0;
|
|
38
|
+
finalStatus = rollbackPassed ? 'rolled_back' : 'blocked';
|
|
39
|
+
if (!rollbackPassed)
|
|
40
|
+
blockers.push(rollback.stderr || rollback.stdout || 'rollback_reverse_patch_failed');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const postDiff = await gitText(input.cwd, ['diff', '--binary']);
|
|
45
|
+
const transaction = {
|
|
46
|
+
schema: 'sks.glm-naruto-apply-transaction.v1',
|
|
47
|
+
mission_id: input.missionId,
|
|
48
|
+
selected_patch_ids: input.selectedPatchIds,
|
|
49
|
+
touched_paths: parsed.touchedPaths,
|
|
50
|
+
pre_status: preStatus,
|
|
51
|
+
pre_diff_sha256: sha256(preDiff),
|
|
52
|
+
combined_patch_sha256: sha256(patch),
|
|
53
|
+
apply_check_passed: applyCheckPassed,
|
|
54
|
+
apply_passed: applyPassed,
|
|
55
|
+
targeted_checks_passed: null,
|
|
56
|
+
rollback_attempted: rollbackAttempted,
|
|
57
|
+
rollback_passed: rollbackPassed,
|
|
58
|
+
final_status: finalStatus,
|
|
59
|
+
blockers
|
|
60
|
+
};
|
|
61
|
+
await writeJsonAtomic(path.join(input.artifactDir, 'apply-transaction.json'), transaction);
|
|
62
|
+
await writeJsonAtomic(path.join(input.artifactDir, 'apply-transaction-diff-hashes.json'), {
|
|
63
|
+
schema: 'sks.glm-naruto-apply-transaction-diff-hashes.v1',
|
|
64
|
+
mission_id: input.missionId,
|
|
65
|
+
pre_diff_sha256: sha256(preDiff),
|
|
66
|
+
post_diff_sha256: sha256(postDiff),
|
|
67
|
+
combined_patch_sha256: sha256(patch)
|
|
68
|
+
});
|
|
69
|
+
return { ok: finalStatus === 'applied', applied: finalStatus === 'applied' ? input.selectedPatchIds : [], patch, transaction };
|
|
70
|
+
}
|
|
71
|
+
function gitText(cwd, args) {
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
const child = spawn('git', [...args], { cwd, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
74
|
+
let stdout = '';
|
|
75
|
+
child.stdout.on('data', (chunk) => { stdout += String(chunk); });
|
|
76
|
+
child.on('close', () => resolve(stdout));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function gitApply(cwd, patch, args) {
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
const child = spawn('git', [...args], { cwd, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
82
|
+
let stdout = '';
|
|
83
|
+
let stderr = '';
|
|
84
|
+
child.stdout.on('data', (chunk) => { stdout += String(chunk); });
|
|
85
|
+
child.stderr.on('data', (chunk) => { stderr += String(chunk); });
|
|
86
|
+
child.on('close', (code) => resolve({ code, stdout, stderr }));
|
|
87
|
+
child.stdin.end(patch);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=glm-naruto-apply-transaction.js.map
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import { nowIso
|
|
1
|
+
import { nowIso } from '../../../fsx.js';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import fsp from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
3
5
|
import { GLM_52_OPENROUTER_MODEL } from '../glm-52-settings.js';
|
|
6
|
+
import { resolveOpenRouterApiKey } from '../../openrouter/openrouter-secret-store.js';
|
|
7
|
+
import { runGlmNarutoMission } from './glm-naruto-orchestrator.js';
|
|
8
|
+
import { summarizeGlmNarutoWorkerMetrics } from './glm-naruto-metrics.js';
|
|
4
9
|
export async function runGlmNarutoBench(root, args = []) {
|
|
5
10
|
const live = args.includes('--live');
|
|
6
11
|
const execute = args.includes('--execute');
|
|
@@ -11,7 +16,7 @@ export async function runGlmNarutoBench(root, args = []) {
|
|
|
11
16
|
if (!live) {
|
|
12
17
|
return {
|
|
13
18
|
schema: 'sks.glm-naruto-bench.v1',
|
|
14
|
-
version: '4.0.
|
|
19
|
+
version: '4.0.11',
|
|
15
20
|
generated_at: nowIso(),
|
|
16
21
|
status: 'dry_run',
|
|
17
22
|
model: GLM_52_OPENROUTER_MODEL,
|
|
@@ -27,29 +32,67 @@ export async function runGlmNarutoBench(root, args = []) {
|
|
|
27
32
|
warnings: ['dry_run_no_live_api_calls']
|
|
28
33
|
};
|
|
29
34
|
}
|
|
30
|
-
|
|
35
|
+
const key = await resolveOpenRouterApiKey({ env: process.env });
|
|
36
|
+
if (!key.key)
|
|
37
|
+
return blocked(root, ['live_bench_requires_openrouter_key']);
|
|
38
|
+
const fixture = await fsp.mkdtemp(path.join(os.tmpdir(), 'sks-glm-naruto-live-bench-'));
|
|
39
|
+
await fsp.mkdir(path.join(fixture, 'src'), { recursive: true });
|
|
40
|
+
await fsp.writeFile(path.join(fixture, 'src', 'bench-target.ts'), 'export const value = 1;\n', 'utf8');
|
|
41
|
+
const cases = [];
|
|
42
|
+
for (const workers of [1, 4, 8, 12]) {
|
|
43
|
+
const caseStarted = Date.now();
|
|
44
|
+
const result = await runGlmNarutoMission({
|
|
45
|
+
cwd: fixture,
|
|
46
|
+
task: 'Change src/bench-target.ts so value is 2. Return the smallest patch only.',
|
|
47
|
+
args: ['--bench', '--live', '--no-apply'],
|
|
48
|
+
missionId: `glm-naruto-live-bench-${workers}-${Date.now()}`,
|
|
49
|
+
maxWorkers: workers,
|
|
50
|
+
noApply: true
|
|
51
|
+
});
|
|
52
|
+
const traces = await readWorkerTraces(result.artifact_dir);
|
|
53
|
+
const metrics = summarizeGlmNarutoWorkerMetrics(traces);
|
|
54
|
+
cases.push({
|
|
55
|
+
name: workers === 1 ? 'direct single GLM' : `GLM Naruto ${workers} workers`,
|
|
56
|
+
workers,
|
|
57
|
+
wall_clock_ms: Date.now() - caseStarted,
|
|
58
|
+
p50_ttft_ms: metrics.p50_ttft_ms,
|
|
59
|
+
p90_ttft_ms: metrics.p90_ttft_ms,
|
|
60
|
+
p50_total_ms: metrics.p50_total_ms,
|
|
61
|
+
p90_total_ms: metrics.p90_total_ms,
|
|
62
|
+
candidate_count: result.patch_candidates,
|
|
63
|
+
gate_pass_rate: result.patch_candidates ? result.gate_passed_candidates / result.patch_candidates : 0,
|
|
64
|
+
verifier_pass_rate: metrics.verifier_pass_rate,
|
|
65
|
+
merge_success: result.mergeable_candidates > 0,
|
|
66
|
+
cached_tokens_sum: metrics.cached_tokens_sum,
|
|
67
|
+
cache_write_tokens_sum: metrics.cache_write_tokens_sum,
|
|
68
|
+
reasoning_tokens_sum: metrics.reasoning_tokens_sum,
|
|
69
|
+
workers_completed: metrics.workers_completed,
|
|
70
|
+
workers_failed: metrics.workers_failed
|
|
71
|
+
});
|
|
72
|
+
}
|
|
31
73
|
return {
|
|
32
74
|
schema: 'sks.glm-naruto-bench.v1',
|
|
33
|
-
version: '4.0.
|
|
75
|
+
version: '4.0.11',
|
|
34
76
|
generated_at: nowIso(),
|
|
35
|
-
status: '
|
|
77
|
+
status: 'live',
|
|
36
78
|
model: GLM_52_OPENROUTER_MODEL,
|
|
37
79
|
gpt_fallback_allowed: false,
|
|
80
|
+
cases,
|
|
38
81
|
summary: {
|
|
39
|
-
simulated_workers:
|
|
40
|
-
simulated_waves:
|
|
41
|
-
simulated_patch_candidates: 0,
|
|
42
|
-
simulated_gate_passed: 0,
|
|
43
|
-
simulated_mergeable:
|
|
82
|
+
simulated_workers: Math.max(...cases.map((row) => row.workers)),
|
|
83
|
+
simulated_waves: cases.length,
|
|
84
|
+
simulated_patch_candidates: cases.reduce((sum, row) => sum + row.candidate_count, 0),
|
|
85
|
+
simulated_gate_passed: cases.reduce((sum, row) => sum + Math.round(row.candidate_count * row.gate_pass_rate), 0),
|
|
86
|
+
simulated_mergeable: cases.filter((row) => row.merge_success).length,
|
|
44
87
|
wall_clock_ms: Date.now() - started
|
|
45
88
|
},
|
|
46
|
-
warnings: ['
|
|
89
|
+
warnings: ['live_bench_no_apply_temp_repo']
|
|
47
90
|
};
|
|
48
91
|
}
|
|
49
92
|
function blocked(root, warnings) {
|
|
50
93
|
return {
|
|
51
94
|
schema: 'sks.glm-naruto-bench.v1',
|
|
52
|
-
version: '4.0.
|
|
95
|
+
version: '4.0.11',
|
|
53
96
|
generated_at: nowIso(),
|
|
54
97
|
status: 'blocked',
|
|
55
98
|
model: GLM_52_OPENROUTER_MODEL,
|
|
@@ -65,4 +108,14 @@ function blocked(root, warnings) {
|
|
|
65
108
|
warnings
|
|
66
109
|
};
|
|
67
110
|
}
|
|
111
|
+
async function readWorkerTraces(artifactDir) {
|
|
112
|
+
if (!artifactDir)
|
|
113
|
+
return [];
|
|
114
|
+
try {
|
|
115
|
+
return JSON.parse(await fsp.readFile(path.join(artifactDir, 'worker-traces.json'), 'utf8'));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
68
121
|
//# sourceMappingURL=glm-naruto-bench.js.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { checkAndApplyGlmPatch } from '../glm-patch-apply.js';
|
|
2
|
+
export function combineGlmNarutoPatches(envelopes, selectedPatchIds) {
|
|
3
|
+
const selected = selectedPatchIds
|
|
4
|
+
.map((patchId) => envelopes.find((env) => env.worker_id === patchId || env.patch_sha256 === patchId))
|
|
5
|
+
.filter((env) => Boolean(env))
|
|
6
|
+
.sort((a, b) => a.worker_id.localeCompare(b.worker_id));
|
|
7
|
+
return mergeDiffSections(selected.flatMap((env) => splitDiffSections(env.patch)));
|
|
8
|
+
}
|
|
9
|
+
export async function checkAndApplyCombinedGlmNarutoPatch(input) {
|
|
10
|
+
const patch = combineGlmNarutoPatches(input.envelopes, input.selectedPatchIds);
|
|
11
|
+
if (!patch.trim())
|
|
12
|
+
return { ok: false, patch, applied: [], blocker: 'combined_patch_empty' };
|
|
13
|
+
const checked = await checkAndApplyGlmPatch({ cwd: input.cwd, patch, apply: input.apply });
|
|
14
|
+
if (!checked.ok)
|
|
15
|
+
return { ok: false, patch, applied: [], blocker: checked.error.code };
|
|
16
|
+
return { ok: true, patch, applied: input.selectedPatchIds };
|
|
17
|
+
}
|
|
18
|
+
function mergeDiffSections(sections) {
|
|
19
|
+
const byFile = new Map();
|
|
20
|
+
for (const section of sections)
|
|
21
|
+
byFile.set(section.file, [...(byFile.get(section.file) || []), section]);
|
|
22
|
+
const merged = [];
|
|
23
|
+
for (const [file] of byFile) {
|
|
24
|
+
const group = byFile.get(file);
|
|
25
|
+
const first = group[0];
|
|
26
|
+
merged.push([...first.header, ...group.flatMap((section) => section.hunks)].join('\n').trimEnd());
|
|
27
|
+
}
|
|
28
|
+
return merged.filter(Boolean).join('\n\n') + (merged.length ? '\n' : '');
|
|
29
|
+
}
|
|
30
|
+
function splitDiffSections(patch) {
|
|
31
|
+
const rawSections = patch
|
|
32
|
+
.split(/(?=^diff --git )/m)
|
|
33
|
+
.map((section) => section.trim())
|
|
34
|
+
.filter(Boolean);
|
|
35
|
+
return rawSections.map((section) => {
|
|
36
|
+
const lines = section.split(/\r?\n/);
|
|
37
|
+
const diff = lines[0]?.match(/^diff --git a\/(.+?) b\/(.+)$/);
|
|
38
|
+
const file = diff?.[2] || diff?.[1] || lines[0] || 'unknown';
|
|
39
|
+
const firstHunk = lines.findIndex((line) => line.startsWith('@@ '));
|
|
40
|
+
if (firstHunk < 0)
|
|
41
|
+
return { file, header: lines, hunks: [] };
|
|
42
|
+
return {
|
|
43
|
+
file,
|
|
44
|
+
header: lines.slice(0, firstHunk),
|
|
45
|
+
hunks: lines.slice(firstHunk)
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=glm-naruto-combined-patch.js.map
|
|
@@ -2,7 +2,6 @@ import { flag, readOption, positionalArgs } from '../../../../cli/args.js';
|
|
|
2
2
|
import { printJson } from '../../../../cli/output.js';
|
|
3
3
|
import { runGlmNarutoMission } from './glm-naruto-orchestrator.js';
|
|
4
4
|
import { runGlmNarutoBench } from './glm-naruto-bench.js';
|
|
5
|
-
import { resolveOpenRouterApiKey } from '../../openrouter/openrouter-secret-store.js';
|
|
6
5
|
export async function glmNarutoCommand(args = []) {
|
|
7
6
|
if (flag(args, '--bench')) {
|
|
8
7
|
const result = await runGlmNarutoBench(process.cwd(), args);
|
|
@@ -36,34 +35,17 @@ export async function glmNarutoCommand(args = []) {
|
|
|
36
35
|
process.exitCode = 1;
|
|
37
36
|
return result;
|
|
38
37
|
}
|
|
39
|
-
const key = await resolveOpenRouterApiKey({ env: process.env });
|
|
40
|
-
if (!key.key) {
|
|
41
|
-
const result = {
|
|
42
|
-
schema: 'sks.glm-naruto-result.v1',
|
|
43
|
-
ok: false,
|
|
44
|
-
status: 'blocked',
|
|
45
|
-
mission_id: 'none',
|
|
46
|
-
task,
|
|
47
|
-
model: 'z-ai/glm-5.2',
|
|
48
|
-
gpt_fallback_allowed: false,
|
|
49
|
-
termination_reason: 'glm_missing_openrouter_key',
|
|
50
|
-
blockers: ['glm_missing_openrouter_key'],
|
|
51
|
-
warnings: ['set_OPENROUTER_API_KEY_or_run_sks_--mad_--glm_--repair']
|
|
52
|
-
};
|
|
53
|
-
if (flag(args, '--json'))
|
|
54
|
-
printJson(result);
|
|
55
|
-
else
|
|
56
|
-
console.error('GLM Naruto blocked: missing OpenRouter API key. Run: sks --mad --glm --repair');
|
|
57
|
-
process.exitCode = 1;
|
|
58
|
-
return result;
|
|
59
|
-
}
|
|
60
38
|
const maxWorkers = parseInt(readOption(args, '--clones', readOption(args, '--workers', '12')), 10) || 12;
|
|
61
39
|
const deep = flag(args, '--deep');
|
|
62
40
|
const useJudge = flag(args, '--judge');
|
|
63
41
|
const xhighFinalizer = flag(args, '--xhigh-finalizer');
|
|
64
42
|
const useWorktree = flag(args, '--worktree');
|
|
65
43
|
const patchEnvelopeOnly = flag(args, '--patch-envelope-only');
|
|
44
|
+
const keepWorktrees = flag(args, '--keep-worktrees');
|
|
45
|
+
const cleanupWorktrees = flag(args, '--cleanup-worktrees') || !keepWorktrees;
|
|
46
|
+
const allowPatchEnvelopeFallback = flag(args, '--allow-patch-envelope-fallback');
|
|
66
47
|
const noApply = flag(args, '--no-apply');
|
|
48
|
+
const skipVerifier = flag(args, '--skip-verifier');
|
|
67
49
|
const mergeStrategy = readOption(args, '--merge-strategy', 'deterministic');
|
|
68
50
|
const result = await runGlmNarutoMission({
|
|
69
51
|
cwd: process.cwd(),
|
|
@@ -74,7 +56,12 @@ export async function glmNarutoCommand(args = []) {
|
|
|
74
56
|
useJudge,
|
|
75
57
|
xhighFinalizer,
|
|
76
58
|
useWorktree: useWorktree && !patchEnvelopeOnly,
|
|
59
|
+
patchEnvelopeOnly,
|
|
60
|
+
allowPatchEnvelopeFallback,
|
|
61
|
+
keepWorktrees,
|
|
62
|
+
cleanupWorktrees,
|
|
77
63
|
noApply: noApply || flag(args, '--dry-run'),
|
|
64
|
+
skipVerifier,
|
|
78
65
|
mergeStrategy
|
|
79
66
|
});
|
|
80
67
|
if (flag(args, '--json')) {
|
|
@@ -2,24 +2,25 @@ import { GLM_NARUTO_DEFAULTS } from './glm-naruto-types.js';
|
|
|
2
2
|
export function decideConcurrency(input) {
|
|
3
3
|
const maxClones = Math.min(input.operatorMax || GLM_NARUTO_DEFAULTS.max_clones, GLM_NARUTO_DEFAULTS.max_clones);
|
|
4
4
|
const requested = Math.min(input.requestedClones || GLM_NARUTO_DEFAULTS.default_clones, maxClones);
|
|
5
|
-
|
|
5
|
+
const active = input.activeWorkers > 0 ? input.activeWorkers : Math.min(requested, GLM_NARUTO_DEFAULTS.safe_active_start);
|
|
6
|
+
if (input.failureRate > 0.10) {
|
|
6
7
|
return {
|
|
7
|
-
target_active_workers:
|
|
8
|
+
target_active_workers: 0,
|
|
8
9
|
burst_workers: 0,
|
|
9
10
|
backpressure: true,
|
|
10
|
-
reason: '
|
|
11
|
+
reason: 'pause_high_5xx_or_failure_rate'
|
|
11
12
|
};
|
|
12
13
|
}
|
|
13
|
-
if (input.
|
|
14
|
+
if (input.rateLimited429 > 0 || input.ttftP90Ms > 15_000) {
|
|
14
15
|
return {
|
|
15
|
-
target_active_workers: Math.max(1, Math.floor(
|
|
16
|
+
target_active_workers: Math.max(1, Math.floor(active * 0.5)),
|
|
16
17
|
burst_workers: 0,
|
|
17
18
|
backpressure: true,
|
|
18
|
-
reason: '
|
|
19
|
+
reason: 'scale_down_high_latency_or_rate_limit'
|
|
19
20
|
};
|
|
20
21
|
}
|
|
21
|
-
if (input.ttftP90Ms < 5_000 && input.rateLimited429 === 0 && input.
|
|
22
|
-
const target = Math.min(requested,
|
|
22
|
+
if (input.ttftP90Ms < 5_000 && input.rateLimited429 === 0 && input.failureRate < 0.05 && active < requested) {
|
|
23
|
+
const target = Math.min(requested, active + 2);
|
|
23
24
|
return {
|
|
24
25
|
target_active_workers: target,
|
|
25
26
|
burst_workers: Math.min(2, requested - target),
|
|
@@ -28,7 +29,7 @@ export function decideConcurrency(input) {
|
|
|
28
29
|
};
|
|
29
30
|
}
|
|
30
31
|
return {
|
|
31
|
-
target_active_workers: Math.min(
|
|
32
|
+
target_active_workers: Math.min(active, requested),
|
|
32
33
|
burst_workers: 0,
|
|
33
34
|
backpressure: false,
|
|
34
35
|
reason: 'steady_state'
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { envelopesHaveHunkConflict } from './glm-naruto-hunk-conflict.js';
|
|
1
2
|
export function buildConflictGraph(envelopes, nodes) {
|
|
2
3
|
const edges = [];
|
|
3
4
|
for (let i = 0; i < nodes.length; i++) {
|
|
@@ -23,11 +24,16 @@ function detectConflict(left, right, envelopes) {
|
|
|
23
24
|
if (leftEnv && rightEnv && leftEnv.base_digest !== rightEnv.base_digest) {
|
|
24
25
|
return { left_patch_id: left.patch_id, right_patch_id: right.patch_id, reason: 'base_digest_mismatch' };
|
|
25
26
|
}
|
|
27
|
+
if (left.shard_id === right.shard_id) {
|
|
28
|
+
return { left_patch_id: left.patch_id, right_patch_id: right.patch_id, reason: 'same_hunk' };
|
|
29
|
+
}
|
|
26
30
|
const leftPaths = new Set(left.target_paths);
|
|
27
31
|
const rightPaths = new Set(right.target_paths);
|
|
28
32
|
const sharedFiles = [...leftPaths].filter((p) => rightPaths.has(p));
|
|
29
33
|
if (sharedFiles.length > 0) {
|
|
30
|
-
if (
|
|
34
|
+
if (leftEnv && rightEnv) {
|
|
35
|
+
if (!envelopesHaveHunkConflict(leftEnv, rightEnv))
|
|
36
|
+
return null;
|
|
31
37
|
return { left_patch_id: left.patch_id, right_patch_id: right.patch_id, reason: 'same_hunk' };
|
|
32
38
|
}
|
|
33
39
|
return { left_patch_id: left.patch_id, right_patch_id: right.patch_id, reason: 'same_file' };
|
|
@@ -5,9 +5,32 @@ export function decomposeTask(input) {
|
|
|
5
5
|
const dependencies = [];
|
|
6
6
|
const mutableShardIds = [];
|
|
7
7
|
const verificationShardIds = [];
|
|
8
|
-
const paths = input
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
const paths = discoverTargetPaths(input);
|
|
9
|
+
if (paths.length === 0) {
|
|
10
|
+
const scoutShard = {
|
|
11
|
+
id: 'shard-scout-paths',
|
|
12
|
+
kind: 'verification',
|
|
13
|
+
task: `Discover target paths before mutation for: ${input.task}`,
|
|
14
|
+
target_paths: [],
|
|
15
|
+
forbidden_paths: ['.github/', 'dist/', 'node_modules/'],
|
|
16
|
+
base_digest: digestBase(input),
|
|
17
|
+
strategy: 'minimal_patch',
|
|
18
|
+
patches_per_shard: 0,
|
|
19
|
+
max_tokens: 2048,
|
|
20
|
+
reasoning: 'low',
|
|
21
|
+
mutable: false
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
schema: 'sks.glm-naruto-work-graph.v1',
|
|
25
|
+
mission_id: input.missionId,
|
|
26
|
+
task: input.task,
|
|
27
|
+
shards: [scoutShard],
|
|
28
|
+
dependencies,
|
|
29
|
+
parallel_groups: [],
|
|
30
|
+
mutable_shards: [],
|
|
31
|
+
verification_shards: [scoutShard.id]
|
|
32
|
+
};
|
|
33
|
+
}
|
|
11
34
|
let shardIndex = 0;
|
|
12
35
|
for (const targetPath of paths) {
|
|
13
36
|
const shardId = `shard-${shardIndex}`;
|
|
@@ -64,6 +87,33 @@ export function decomposeTask(input) {
|
|
|
64
87
|
verification_shards: verificationShardIds
|
|
65
88
|
};
|
|
66
89
|
}
|
|
90
|
+
function discoverTargetPaths(input) {
|
|
91
|
+
const candidates = new Set();
|
|
92
|
+
for (const mentioned of input.mentionedPaths)
|
|
93
|
+
candidates.add(mentioned);
|
|
94
|
+
for (const line of (input.gitStatus || '').split(/\r?\n/)) {
|
|
95
|
+
const match = line.match(/^\s*(?:[AMDRCU?!]{1,2})\s+(.+)$/);
|
|
96
|
+
if (!match)
|
|
97
|
+
continue;
|
|
98
|
+
const file = match[1].split(/\s+->\s+/).pop().trim();
|
|
99
|
+
if (isMutableCandidate(file))
|
|
100
|
+
candidates.add(file);
|
|
101
|
+
}
|
|
102
|
+
for (const source of [input.task, input.lastError || '']) {
|
|
103
|
+
for (const match of source.matchAll(/\b([A-Za-z0-9_.-]+\/[A-Za-z0-9_./-]+\.(?:ts|tsx|js|mjs|cjs|json|md|yml|yaml|toml))\b/g)) {
|
|
104
|
+
if (isMutableCandidate(match[1]))
|
|
105
|
+
candidates.add(match[1]);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return [...candidates];
|
|
109
|
+
}
|
|
110
|
+
function isMutableCandidate(file) {
|
|
111
|
+
return Boolean(file)
|
|
112
|
+
&& !file.startsWith('.github/')
|
|
113
|
+
&& !file.startsWith('dist/')
|
|
114
|
+
&& !file.startsWith('node_modules/')
|
|
115
|
+
&& !file.endsWith('/');
|
|
116
|
+
}
|
|
67
117
|
function classifyShardKind(path) {
|
|
68
118
|
if (path.includes('test') || path.includes('__tests__') || path.includes('.test.'))
|
|
69
119
|
return 'test_fix';
|
|
@@ -87,7 +137,7 @@ export function validateWorkGraph(graph, isVerifyOnly) {
|
|
|
87
137
|
return { ok: true };
|
|
88
138
|
const mutableCount = graph.mutable_shards.length;
|
|
89
139
|
if (mutableCount === 0)
|
|
90
|
-
return { ok: false, reason: '
|
|
140
|
+
return { ok: false, reason: 'glm_naruto_needs_target_path_context' };
|
|
91
141
|
// Check ratio of mutable shards to total shards (excluding verification shards from the denominator)
|
|
92
142
|
const totalWorkShards = graph.shards.filter(s => s.mutable || s.kind !== 'verification').length;
|
|
93
143
|
const ratio = totalWorkShards > 0 ? mutableCount / totalWorkShards : 0;
|
|
@@ -15,6 +15,7 @@ export function finalizeMergePlan(input) {
|
|
|
15
15
|
return planMerge({
|
|
16
16
|
missionId: input.missionId,
|
|
17
17
|
graph: conflictGraph,
|
|
18
|
+
...(input.scoreboard ? { scoreboard: input.scoreboard } : {}),
|
|
18
19
|
strategy,
|
|
19
20
|
...(input.judgeResult ? { judgeRanking: input.judgeResult.ranked_patch_ids } : {})
|
|
20
21
|
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { hunksOverlap, parseUnifiedDiffHunks } from './glm-naruto-hunk-parser.js';
|
|
2
|
+
export function envelopesHaveHunkConflict(left, right) {
|
|
3
|
+
const leftHunks = parseUnifiedDiffHunks(left.patch);
|
|
4
|
+
const rightHunks = parseUnifiedDiffHunks(right.patch);
|
|
5
|
+
if (leftHunks.length === 0 || rightHunks.length === 0)
|
|
6
|
+
return sharesPath(left, right);
|
|
7
|
+
return leftHunks.some((leftHunk) => rightHunks.some((rightHunk) => hunksOverlap(leftHunk, rightHunk)));
|
|
8
|
+
}
|
|
9
|
+
export function envelopesShareFileButNotHunk(left, right) {
|
|
10
|
+
return sharesPath(left, right) && !envelopesHaveHunkConflict(left, right);
|
|
11
|
+
}
|
|
12
|
+
function sharesPath(left, right) {
|
|
13
|
+
const rightPaths = new Set(right.target_paths);
|
|
14
|
+
return left.target_paths.some((file) => rightPaths.has(file));
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=glm-naruto-hunk-conflict.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function parseUnifiedDiffHunks(patch) {
|
|
2
|
+
const hunks = [];
|
|
3
|
+
let currentFile = null;
|
|
4
|
+
for (const line of patch.split(/\r?\n/)) {
|
|
5
|
+
const diff = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
|
|
6
|
+
if (diff?.[2]) {
|
|
7
|
+
currentFile = diff[2];
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
const file = line.match(/^\+\+\+ b\/(.+)$/);
|
|
11
|
+
if (file?.[1] && file[1] !== '/dev/null')
|
|
12
|
+
currentFile = file[1];
|
|
13
|
+
const hunk = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/);
|
|
14
|
+
if (hunk && currentFile) {
|
|
15
|
+
hunks.push({
|
|
16
|
+
file: currentFile,
|
|
17
|
+
old_start: Number(hunk[1]),
|
|
18
|
+
old_lines: Number(hunk[2] || 1),
|
|
19
|
+
new_start: Number(hunk[3]),
|
|
20
|
+
new_lines: Number(hunk[4] || 1),
|
|
21
|
+
header: line
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return hunks;
|
|
26
|
+
}
|
|
27
|
+
export function hunksOverlap(left, right) {
|
|
28
|
+
if (left.file !== right.file)
|
|
29
|
+
return false;
|
|
30
|
+
return rangesOverlap(left.old_start, left.old_start + Math.max(1, left.old_lines) - 1, right.old_start, right.old_start + Math.max(1, right.old_lines) - 1)
|
|
31
|
+
|| rangesOverlap(left.new_start, left.new_start + Math.max(1, left.new_lines) - 1, right.new_start, right.new_start + Math.max(1, right.new_lines) - 1);
|
|
32
|
+
}
|
|
33
|
+
function rangesOverlap(aStart, aEnd, bStart, bEnd) {
|
|
34
|
+
return aStart <= bEnd && bStart <= aEnd;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=glm-naruto-hunk-parser.js.map
|