sneakoscope 4.0.7 → 4.0.9
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 +2 -2
- 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/bin/sks.js +1 -1
- package/dist/cli/command-registry.js +1 -0
- package/dist/core/commands/glm-command.js +8 -1
- package/dist/core/commands/naruto-command.js +25 -0
- package/dist/core/commands/stop-gate-command.js +63 -0
- package/dist/core/fsx.js +1 -1
- package/dist/core/pipeline-internals/runtime-gates.js +28 -4
- package/dist/core/providers/glm/glm-bench.js +4 -4
- package/dist/core/providers/glm/glm-direct-run.js +1 -1
- package/dist/core/providers/glm/glm-latency-trace.js +1 -1
- package/dist/core/providers/glm/glm-request-cache.js +10 -2
- package/dist/core/providers/glm/naruto/glm-naruto-artifacts.js +2 -0
- package/dist/core/providers/glm/naruto/glm-naruto-bench.js +68 -0
- package/dist/core/providers/glm/naruto/glm-naruto-budget.js +45 -0
- package/dist/core/providers/glm/naruto/glm-naruto-command.js +97 -0
- package/dist/core/providers/glm/naruto/glm-naruto-concurrency-governor.js +37 -0
- package/dist/core/providers/glm/naruto/glm-naruto-conflict-graph.js +74 -0
- package/dist/core/providers/glm/naruto/glm-naruto-decomposer.js +99 -0
- package/dist/core/providers/glm/naruto/glm-naruto-file-lease.js +23 -0
- package/dist/core/providers/glm/naruto/glm-naruto-finalizer.js +22 -0
- package/dist/core/providers/glm/naruto/glm-naruto-judge.js +84 -0
- package/dist/core/providers/glm/naruto/glm-naruto-merge-planner.js +57 -0
- package/dist/core/providers/glm/naruto/glm-naruto-orchestrator.js +277 -0
- package/dist/core/providers/glm/naruto/glm-naruto-patch-envelope.js +55 -0
- package/dist/core/providers/glm/naruto/glm-naruto-quorum.js +37 -0
- package/dist/core/providers/glm/naruto/glm-naruto-rate-limiter.js +18 -0
- package/dist/core/providers/glm/naruto/glm-naruto-repair-wave.js +21 -0
- package/dist/core/providers/glm/naruto/glm-naruto-shard-planner.js +32 -0
- package/dist/core/providers/glm/naruto/glm-naruto-trace.js +91 -0
- package/dist/core/providers/glm/naruto/glm-naruto-types.js +37 -0
- package/dist/core/providers/glm/naruto/glm-naruto-work-graph.js +2 -0
- package/dist/core/providers/glm/naruto/glm-naruto-worker-pool.js +79 -0
- package/dist/core/providers/glm/naruto/glm-naruto-worker-runtime.js +198 -0
- package/dist/core/providers/glm/naruto/glm-naruto-worker.js +2 -0
- package/dist/core/providers/glm/naruto/glm-naruto-worktree.js +48 -0
- package/dist/core/providers/openrouter/openrouter-provider-health.js +46 -0
- package/dist/core/providers/openrouter/openrouter-secret-store.js +33 -0
- package/dist/core/providers/openrouter/openrouter-stream.js +101 -8
- package/dist/core/stop-gate/stop-gate-check.js +208 -0
- package/dist/core/stop-gate/stop-gate-diagnostics.js +4 -0
- package/dist/core/stop-gate/stop-gate-resolver.js +122 -0
- package/dist/core/stop-gate/stop-gate-types.js +2 -0
- package/dist/core/stop-gate/stop-gate-writer.js +76 -0
- package/dist/core/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export function buildConflictGraph(envelopes, nodes) {
|
|
2
|
+
const edges = [];
|
|
3
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
4
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
5
|
+
const left = nodes[i];
|
|
6
|
+
const right = nodes[j];
|
|
7
|
+
const conflict = detectConflict(left, right, envelopes);
|
|
8
|
+
if (conflict)
|
|
9
|
+
edges.push(conflict);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
schema: 'sks.glm-naruto-conflict-graph.v1',
|
|
14
|
+
nodes,
|
|
15
|
+
edges
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function detectConflict(left, right, envelopes) {
|
|
19
|
+
if (left.patch_id === right.patch_id)
|
|
20
|
+
return null;
|
|
21
|
+
const leftEnv = envelopes.find((e) => e.worker_id === left.patch_id || e.patch_sha256 === left.patch_sha256);
|
|
22
|
+
const rightEnv = envelopes.find((e) => e.worker_id === right.patch_id || e.patch_sha256 === right.patch_sha256);
|
|
23
|
+
if (leftEnv && rightEnv && leftEnv.base_digest !== rightEnv.base_digest) {
|
|
24
|
+
return { left_patch_id: left.patch_id, right_patch_id: right.patch_id, reason: 'base_digest_mismatch' };
|
|
25
|
+
}
|
|
26
|
+
const leftPaths = new Set(left.target_paths);
|
|
27
|
+
const rightPaths = new Set(right.target_paths);
|
|
28
|
+
const sharedFiles = [...leftPaths].filter((p) => rightPaths.has(p));
|
|
29
|
+
if (sharedFiles.length > 0) {
|
|
30
|
+
if (left.shard_id === right.shard_id) {
|
|
31
|
+
return { left_patch_id: left.patch_id, right_patch_id: right.patch_id, reason: 'same_hunk' };
|
|
32
|
+
}
|
|
33
|
+
return { left_patch_id: left.patch_id, right_patch_id: right.patch_id, reason: 'same_file' };
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
export function hasConflict(graph, patchId) {
|
|
38
|
+
return graph.edges.some((edge) => edge.left_patch_id === patchId || edge.right_patch_id === patchId);
|
|
39
|
+
}
|
|
40
|
+
export function getNonConflictingSets(graph) {
|
|
41
|
+
const passed = graph.nodes.filter((n) => n.gate_passed);
|
|
42
|
+
if (passed.length === 0)
|
|
43
|
+
return [];
|
|
44
|
+
const conflictMap = new Map();
|
|
45
|
+
for (const node of passed)
|
|
46
|
+
conflictMap.set(node.patch_id, new Set());
|
|
47
|
+
for (const edge of graph.edges) {
|
|
48
|
+
if (conflictMap.has(edge.left_patch_id) && conflictMap.has(edge.right_patch_id)) {
|
|
49
|
+
conflictMap.get(edge.left_patch_id).add(edge.right_patch_id);
|
|
50
|
+
conflictMap.get(edge.right_patch_id).add(edge.left_patch_id);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const results = [];
|
|
54
|
+
const sorted = [...passed].sort((a, b) => b.score - a.score);
|
|
55
|
+
const used = new Set();
|
|
56
|
+
for (const node of sorted) {
|
|
57
|
+
if (used.has(node.patch_id))
|
|
58
|
+
continue;
|
|
59
|
+
const group = [node.patch_id];
|
|
60
|
+
used.add(node.patch_id);
|
|
61
|
+
for (const other of sorted) {
|
|
62
|
+
if (used.has(other.patch_id))
|
|
63
|
+
continue;
|
|
64
|
+
const conflicts = conflictMap.get(other.patch_id);
|
|
65
|
+
if (!group.some((id) => conflicts.has(id))) {
|
|
66
|
+
group.push(other.patch_id);
|
|
67
|
+
used.add(other.patch_id);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
results.push(group);
|
|
71
|
+
}
|
|
72
|
+
return results;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=glm-naruto-conflict-graph.js.map
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { GLM_NARUTO_DEFAULTS, NARUTO_PATCH_STRATEGIES } from './glm-naruto-types.js';
|
|
3
|
+
export function decomposeTask(input) {
|
|
4
|
+
const shards = [];
|
|
5
|
+
const dependencies = [];
|
|
6
|
+
const mutableShardIds = [];
|
|
7
|
+
const verificationShardIds = [];
|
|
8
|
+
const paths = input.mentionedPaths.length > 0
|
|
9
|
+
? input.mentionedPaths
|
|
10
|
+
: ['src/'];
|
|
11
|
+
let shardIndex = 0;
|
|
12
|
+
for (const targetPath of paths) {
|
|
13
|
+
const shardId = `shard-${shardIndex}`;
|
|
14
|
+
const strategy = NARUTO_PATCH_STRATEGIES[shardIndex % NARUTO_PATCH_STRATEGIES.length] || 'minimal_patch';
|
|
15
|
+
const isCritical = paths.length <= 2;
|
|
16
|
+
const shard = {
|
|
17
|
+
id: shardId,
|
|
18
|
+
kind: classifyShardKind(targetPath),
|
|
19
|
+
task: input.task,
|
|
20
|
+
target_paths: [targetPath],
|
|
21
|
+
forbidden_paths: ['.github/', 'dist/', 'node_modules/'],
|
|
22
|
+
base_digest: digestBase(input),
|
|
23
|
+
strategy,
|
|
24
|
+
patches_per_shard: isCritical ? GLM_NARUTO_DEFAULTS.critical_patches_per_shard : GLM_NARUTO_DEFAULTS.default_patches_per_shard,
|
|
25
|
+
max_tokens: GLM_NARUTO_DEFAULTS.default_max_tokens,
|
|
26
|
+
reasoning: 'none',
|
|
27
|
+
mutable: true
|
|
28
|
+
};
|
|
29
|
+
shards.push(shard);
|
|
30
|
+
mutableShardIds.push(shardId);
|
|
31
|
+
shardIndex++;
|
|
32
|
+
}
|
|
33
|
+
const verifyShard = {
|
|
34
|
+
id: 'shard-verify',
|
|
35
|
+
kind: 'verification',
|
|
36
|
+
task: `Verify all patches for: ${input.task}`,
|
|
37
|
+
target_paths: paths,
|
|
38
|
+
forbidden_paths: ['.github/', 'dist/', 'node_modules/'],
|
|
39
|
+
base_digest: digestBase(input),
|
|
40
|
+
strategy: 'minimal_patch',
|
|
41
|
+
patches_per_shard: 0,
|
|
42
|
+
max_tokens: 4096,
|
|
43
|
+
reasoning: 'low',
|
|
44
|
+
mutable: false
|
|
45
|
+
};
|
|
46
|
+
shards.push(verifyShard);
|
|
47
|
+
verificationShardIds.push(verifyShard.id);
|
|
48
|
+
for (const mutableId of mutableShardIds) {
|
|
49
|
+
dependencies.push({ from: mutableId, to: verifyShard.id, kind: 'verifies' });
|
|
50
|
+
}
|
|
51
|
+
const parallelGroup = {
|
|
52
|
+
id: 'parallel-patch-wave',
|
|
53
|
+
shard_ids: mutableShardIds,
|
|
54
|
+
parallel: true
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
schema: 'sks.glm-naruto-work-graph.v1',
|
|
58
|
+
mission_id: input.missionId,
|
|
59
|
+
task: input.task,
|
|
60
|
+
shards,
|
|
61
|
+
dependencies,
|
|
62
|
+
parallel_groups: [parallelGroup],
|
|
63
|
+
mutable_shards: mutableShardIds,
|
|
64
|
+
verification_shards: verificationShardIds
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function classifyShardKind(path) {
|
|
68
|
+
if (path.includes('test') || path.includes('__tests__') || path.includes('.test.'))
|
|
69
|
+
return 'test_fix';
|
|
70
|
+
if (path.endsWith('.md') || path.endsWith('.txt'))
|
|
71
|
+
return 'doc_patch';
|
|
72
|
+
if (path.endsWith('.json') || path.endsWith('.yaml') || path.endsWith('.yml') || path.endsWith('.toml'))
|
|
73
|
+
return 'config_patch';
|
|
74
|
+
if (path.endsWith('.ts') || path.endsWith('.js') || path.endsWith('.mjs'))
|
|
75
|
+
return 'file_patch';
|
|
76
|
+
return 'file_patch';
|
|
77
|
+
}
|
|
78
|
+
function digestBase(input) {
|
|
79
|
+
return crypto.createHash('sha256').update(JSON.stringify({
|
|
80
|
+
task: input.task,
|
|
81
|
+
gitStatus: input.gitStatus || '',
|
|
82
|
+
paths: input.mentionedPaths
|
|
83
|
+
})).digest('hex').slice(0, 16);
|
|
84
|
+
}
|
|
85
|
+
export function validateWorkGraph(graph, isVerifyOnly) {
|
|
86
|
+
if (isVerifyOnly)
|
|
87
|
+
return { ok: true };
|
|
88
|
+
const mutableCount = graph.mutable_shards.length;
|
|
89
|
+
if (mutableCount === 0)
|
|
90
|
+
return { ok: false, reason: 'glm_naruto_invalid_verify_only_plan' };
|
|
91
|
+
// Check ratio of mutable shards to total shards (excluding verification shards from the denominator)
|
|
92
|
+
const totalWorkShards = graph.shards.filter(s => s.mutable || s.kind !== 'verification').length;
|
|
93
|
+
const ratio = totalWorkShards > 0 ? mutableCount / totalWorkShards : 0;
|
|
94
|
+
if (ratio < GLM_NARUTO_DEFAULTS.patch_worker_ratio) {
|
|
95
|
+
return { ok: false, reason: 'glm_naruto_insufficient_patch_workers' };
|
|
96
|
+
}
|
|
97
|
+
return { ok: true };
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=glm-naruto-decomposer.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function planFileLeases(shardTargetPaths) {
|
|
2
|
+
const pathToShards = new Map();
|
|
3
|
+
for (const [shardId, paths] of shardTargetPaths) {
|
|
4
|
+
for (const p of paths) {
|
|
5
|
+
const list = pathToShards.get(p) || [];
|
|
6
|
+
list.push(shardId);
|
|
7
|
+
pathToShards.set(p, list);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const leases = [];
|
|
11
|
+
for (const [path, shardIds] of pathToShards) {
|
|
12
|
+
leases.push({
|
|
13
|
+
path,
|
|
14
|
+
shardIds,
|
|
15
|
+
exclusive: shardIds.length === 1
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return leases;
|
|
19
|
+
}
|
|
20
|
+
export function hasLeaseConflict(leases, shardId) {
|
|
21
|
+
return leases.some((lease) => !lease.exclusive && lease.shardIds.includes(shardId));
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=glm-naruto-file-lease.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { planMerge } from './glm-naruto-merge-planner.js';
|
|
2
|
+
import { buildConflictGraph } from './glm-naruto-conflict-graph.js';
|
|
3
|
+
export function finalizeMergePlan(input) {
|
|
4
|
+
const passedEnvelopes = input.envelopes.filter((e) => e.status === 'gate_passed');
|
|
5
|
+
const nodes = passedEnvelopes.map((env) => ({
|
|
6
|
+
patch_id: env.worker_id,
|
|
7
|
+
shard_id: env.shard_id,
|
|
8
|
+
target_paths: env.target_paths,
|
|
9
|
+
score: Math.max(0, 100 - Math.floor(env.patch.length / 100)),
|
|
10
|
+
gate_passed: true,
|
|
11
|
+
patch_sha256: env.patch_sha256
|
|
12
|
+
}));
|
|
13
|
+
const conflictGraph = buildConflictGraph(passedEnvelopes, nodes);
|
|
14
|
+
const strategy = input.useJudge && input.judgeResult ? 'judge' : 'deterministic';
|
|
15
|
+
return planMerge({
|
|
16
|
+
missionId: input.missionId,
|
|
17
|
+
graph: conflictGraph,
|
|
18
|
+
strategy,
|
|
19
|
+
...(input.judgeResult ? { judgeRanking: input.judgeResult.ranked_patch_ids } : {})
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=glm-naruto-finalizer.js.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { buildGlm52Request } from '../glm-52-request.js';
|
|
2
|
+
import { sendOpenRouterChatCompletionStream } from '../../openrouter/openrouter-stream.js';
|
|
3
|
+
import { assertGlm52ActualModel } from '../glm-52-response-guard.js';
|
|
4
|
+
import { GLM_52_OPENROUTER_MODEL } from '../glm-52-settings.js';
|
|
5
|
+
export async function runGlmJudge(input) {
|
|
6
|
+
const validEnvelopes = input.envelopes.filter((e) => e.status === 'gate_passed');
|
|
7
|
+
if (validEnvelopes.length === 0) {
|
|
8
|
+
return {
|
|
9
|
+
schema: 'sks.glm-naruto-judge.v1',
|
|
10
|
+
ranked_patch_ids: [],
|
|
11
|
+
reject_patch_ids: [],
|
|
12
|
+
mergeable_sets: [],
|
|
13
|
+
risks: ['no_gate_passed_candidates'],
|
|
14
|
+
requires_repair_wave: false
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const systemPrompt = `You are a GLM Naruto judge. Rank patch candidates by quality. Output strict JSON with schema: {"ranked_patch_ids":["id1","id2"],"reject_patch_ids":["id3"],"mergeable_sets":[["id1","id2"]],"risks":[],"requires_repair_wave":false}. Model: ${GLM_52_OPENROUTER_MODEL}. No GPT fallback.`;
|
|
18
|
+
const candidateDescriptions = validEnvelopes.map((e) => ({
|
|
19
|
+
patch_id: e.worker_id,
|
|
20
|
+
shard_id: e.shard_id,
|
|
21
|
+
target_paths: e.target_paths,
|
|
22
|
+
patch_sha256: e.patch_sha256.slice(0, 12),
|
|
23
|
+
strategy: e.strategy,
|
|
24
|
+
patch_size: e.patch.length
|
|
25
|
+
}));
|
|
26
|
+
const userContent = JSON.stringify({
|
|
27
|
+
mission_id: input.missionId,
|
|
28
|
+
candidates: candidateDescriptions,
|
|
29
|
+
instruction: 'Rank by: gate pass, minimal diff, correct target paths, no protected paths. Return mergeable non-conflicting sets.'
|
|
30
|
+
});
|
|
31
|
+
const messages = [
|
|
32
|
+
{ role: 'system', content: systemPrompt },
|
|
33
|
+
{ role: 'user', content: userContent }
|
|
34
|
+
];
|
|
35
|
+
const request = buildGlm52Request({
|
|
36
|
+
profile: 'deep',
|
|
37
|
+
messages,
|
|
38
|
+
maxTokens: 8192,
|
|
39
|
+
reasoningEffort: 'high',
|
|
40
|
+
toolChoice: 'none',
|
|
41
|
+
parallelToolCalls: false
|
|
42
|
+
});
|
|
43
|
+
const response = await sendOpenRouterChatCompletionStream({
|
|
44
|
+
apiKey: input.apiKey,
|
|
45
|
+
request: { ...request, session_id: `sks-glm-naruto-judge-${input.missionId}` },
|
|
46
|
+
timeoutMs: input.timeoutMs || 120_000
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
return fallbackJudgeResult(validEnvelopes, [`judge_request_failed:${response.error.code}`]);
|
|
50
|
+
}
|
|
51
|
+
const modelGuard = assertGlm52ActualModel(response.value.model);
|
|
52
|
+
if (!modelGuard.ok) {
|
|
53
|
+
return fallbackJudgeResult(validEnvelopes, [`judge_model_guard:${modelGuard.code}`]);
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const jsonMatch = response.value.content.match(/\{[\s\S]*\}/);
|
|
57
|
+
if (!jsonMatch)
|
|
58
|
+
return fallbackJudgeResult(validEnvelopes, ['judge_no_json_output']);
|
|
59
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
60
|
+
return {
|
|
61
|
+
schema: 'sks.glm-naruto-judge.v1',
|
|
62
|
+
ranked_patch_ids: Array.isArray(parsed.ranked_patch_ids) ? parsed.ranked_patch_ids : [],
|
|
63
|
+
reject_patch_ids: Array.isArray(parsed.reject_patch_ids) ? parsed.reject_patch_ids : [],
|
|
64
|
+
mergeable_sets: Array.isArray(parsed.mergeable_sets) ? parsed.mergeable_sets : [],
|
|
65
|
+
risks: Array.isArray(parsed.risks) ? parsed.risks : [],
|
|
66
|
+
requires_repair_wave: Boolean(parsed.requires_repair_wave)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return fallbackJudgeResult(validEnvelopes, ['judge_json_parse_failed']);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function fallbackJudgeResult(envelopes, risks) {
|
|
74
|
+
const sorted = [...envelopes].sort((a, b) => a.patch.length - b.patch.length);
|
|
75
|
+
return {
|
|
76
|
+
schema: 'sks.glm-naruto-judge.v1',
|
|
77
|
+
ranked_patch_ids: sorted.map((e) => e.worker_id),
|
|
78
|
+
reject_patch_ids: [],
|
|
79
|
+
mergeable_sets: sorted.length > 0 ? [[sorted[0].worker_id]] : [],
|
|
80
|
+
risks,
|
|
81
|
+
requires_repair_wave: false
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=glm-naruto-judge.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { getNonConflictingSets } from './glm-naruto-conflict-graph.js';
|
|
2
|
+
export function planMerge(input) {
|
|
3
|
+
const passedNodes = input.graph.nodes.filter((n) => n.gate_passed);
|
|
4
|
+
const nonConflictingSets = getNonConflictingSets(input.graph);
|
|
5
|
+
const candidates = nonConflictingSets.map((patchIds) => {
|
|
6
|
+
const nodes = passedNodes.filter((n) => patchIds.includes(n.patch_id));
|
|
7
|
+
const totalScore = nodes.reduce((sum, n) => sum + n.score, 0);
|
|
8
|
+
return { patch_ids: patchIds, total_score: totalScore, conflict_free: true };
|
|
9
|
+
});
|
|
10
|
+
candidates.sort((a, b) => b.total_score - a.total_score);
|
|
11
|
+
let selected = [];
|
|
12
|
+
let rationale = '';
|
|
13
|
+
if (input.strategy === 'judge' && input.judgeRanking && input.judgeRanking.length > 0) {
|
|
14
|
+
const ranked = input.judgeRanking.filter((id) => passedNodes.some((n) => n.patch_id === id));
|
|
15
|
+
const bestSet = candidates.find((set) => set.patch_ids.every((id) => ranked.includes(id))) || candidates[0];
|
|
16
|
+
selected = bestSet ? bestSet.patch_ids : [];
|
|
17
|
+
rationale = 'judge_ranked_deterministic_gated';
|
|
18
|
+
}
|
|
19
|
+
else if (input.strategy === 'quorum') {
|
|
20
|
+
const quorumMap = new Map();
|
|
21
|
+
for (const node of passedNodes) {
|
|
22
|
+
const key = node.shard_id;
|
|
23
|
+
quorumMap.set(key, (quorumMap.get(key) || 0) + 1);
|
|
24
|
+
}
|
|
25
|
+
const bestSet = candidates[0];
|
|
26
|
+
selected = bestSet ? bestSet.patch_ids : [];
|
|
27
|
+
rationale = 'quorum_consensus_deterministic_gated';
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
const bestSet = candidates[0];
|
|
31
|
+
selected = bestSet ? bestSet.patch_ids : [];
|
|
32
|
+
rationale = 'highest_score_non_conflicting';
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
schema: 'sks.glm-naruto-merge-plan.v1',
|
|
36
|
+
mission_id: input.missionId,
|
|
37
|
+
strategy: input.strategy,
|
|
38
|
+
selected_patches: selected,
|
|
39
|
+
candidates,
|
|
40
|
+
rationale
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function scoreCandidate(input) {
|
|
44
|
+
let score = 0;
|
|
45
|
+
if (input.node.gate_passed)
|
|
46
|
+
score += 100;
|
|
47
|
+
score -= input.patchSize / 100;
|
|
48
|
+
score -= input.touchedPathsCount * 5;
|
|
49
|
+
if (input.protectedPath)
|
|
50
|
+
score -= 200;
|
|
51
|
+
if (input.testFailure)
|
|
52
|
+
score -= 50;
|
|
53
|
+
if (input.judgeRank !== null && input.judgeRank !== undefined)
|
|
54
|
+
score += Math.max(0, 50 - input.judgeRank * 10);
|
|
55
|
+
return Math.round(score);
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=glm-naruto-merge-planner.js.map
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { nowIso, writeJsonAtomic } from '../../../fsx.js';
|
|
3
|
+
import { GLM_52_OPENROUTER_MODEL } from '../glm-52-settings.js';
|
|
4
|
+
import { resolveOpenRouterApiKey } from '../../openrouter/openrouter-secret-store.js';
|
|
5
|
+
import { checkAndApplyGlmPatch } from '../glm-patch-apply.js';
|
|
6
|
+
import { decomposeTask, validateWorkGraph } from './glm-naruto-decomposer.js';
|
|
7
|
+
import { planShardCandidates, computeInitialLaneMix } from './glm-naruto-shard-planner.js';
|
|
8
|
+
import { runPatchWorkerPool } from './glm-naruto-worker-pool.js';
|
|
9
|
+
import { runVerifierWorker } from './glm-naruto-worker-runtime.js';
|
|
10
|
+
import { buildConflictGraph } from './glm-naruto-conflict-graph.js';
|
|
11
|
+
import { planMerge } from './glm-naruto-merge-planner.js';
|
|
12
|
+
import { finalizeMergePlan } from './glm-naruto-finalizer.js';
|
|
13
|
+
import { planRepairWave } from './glm-naruto-repair-wave.js';
|
|
14
|
+
import { createBudget, checkBudget, recordRequest } from './glm-naruto-budget.js';
|
|
15
|
+
import { createProviderHealthTracker } from '../../openrouter/openrouter-provider-health.js';
|
|
16
|
+
import { createMissionTrace, recordWorkerTrace, writeMissionArtifacts, buildMissionSummary } from './glm-naruto-trace.js';
|
|
17
|
+
import { runGlmJudge } from './glm-naruto-judge.js';
|
|
18
|
+
import { writeFinalStopGate } from '../../../stop-gate/stop-gate-writer.js';
|
|
19
|
+
import { GLM_NARUTO_LIMITS } from './glm-naruto-types.js';
|
|
20
|
+
export async function runGlmNarutoMission(input) {
|
|
21
|
+
const missionId = input.missionId || `glm-naruto-${nowIso().replace(/[:.]/g, '-')}`;
|
|
22
|
+
const cwd = input.cwd;
|
|
23
|
+
const startedMs = Date.now();
|
|
24
|
+
const key = await resolveOpenRouterApiKey({ env: process.env });
|
|
25
|
+
if (!key.key) {
|
|
26
|
+
return missionResult(missionId, input.task, 'blocked', 'glm_missing_openrouter_key', 0, startedMs, [], [], ['glm_missing_openrouter_key'], []);
|
|
27
|
+
}
|
|
28
|
+
const mentionedPaths = extractMentionedPaths(input.task);
|
|
29
|
+
const gitStatus = await readGitStatus(cwd);
|
|
30
|
+
const graph = decomposeTask({
|
|
31
|
+
missionId,
|
|
32
|
+
task: input.task,
|
|
33
|
+
gitStatus,
|
|
34
|
+
mentionedPaths
|
|
35
|
+
});
|
|
36
|
+
const isVerifyOnly = input.task.trim().toLowerCase().startsWith('verify');
|
|
37
|
+
const validation = validateWorkGraph(graph, isVerifyOnly);
|
|
38
|
+
if (!validation.ok) {
|
|
39
|
+
return missionResult(missionId, input.task, 'blocked', validation.reason || 'invalid_work_graph', 0, startedMs, [], [], [validation.reason || 'invalid_work_graph'], []);
|
|
40
|
+
}
|
|
41
|
+
const budget = createBudget(missionId, input.deep || false);
|
|
42
|
+
const budgetCheck = checkBudget(budget);
|
|
43
|
+
if (!budgetCheck.ok) {
|
|
44
|
+
return missionResult(missionId, input.task, 'budget_exhausted', budgetCheck.reason, 0, startedMs, [], [], [budgetCheck.reason], []);
|
|
45
|
+
}
|
|
46
|
+
const laneMix = computeInitialLaneMix(graph);
|
|
47
|
+
const strategies = planShardCandidates(graph);
|
|
48
|
+
const strategyMap = new Map();
|
|
49
|
+
for (const entry of strategies) {
|
|
50
|
+
strategyMap.set(entry.shard.id, entry.strategies);
|
|
51
|
+
}
|
|
52
|
+
const healthTracker = createProviderHealthTracker();
|
|
53
|
+
let traceState = createMissionTrace(missionId);
|
|
54
|
+
// Wave 1: parallel patch candidate generation
|
|
55
|
+
const poolResult = await runPatchWorkerPool({
|
|
56
|
+
apiKey: key.key,
|
|
57
|
+
missionId,
|
|
58
|
+
cwd,
|
|
59
|
+
shards: graph.shards,
|
|
60
|
+
contextSummary: JSON.stringify({ task: input.task, git_status: gitStatus || '' }),
|
|
61
|
+
maxWorkers: input.maxWorkers || laneMix.patch_workers,
|
|
62
|
+
workerTimeoutMs: GLM_NARUTO_LIMITS.max_worker_runtime_ms,
|
|
63
|
+
strategies: strategyMap
|
|
64
|
+
});
|
|
65
|
+
for (const trace of poolResult.traces) {
|
|
66
|
+
traceState = recordWorkerTrace(traceState, trace);
|
|
67
|
+
}
|
|
68
|
+
healthTracker.record({ provider_slug: 'openrouter', model: GLM_52_OPENROUTER_MODEL, count_429: 0, count_5xx: 0 });
|
|
69
|
+
let envelopes = poolResult.envelopes;
|
|
70
|
+
let failedShardIds = poolResult.failedShardIds;
|
|
71
|
+
let repairWaves = 0;
|
|
72
|
+
// Repair wave if needed
|
|
73
|
+
if (failedShardIds.length > 0 && repairWaves < GLM_NARUTO_LIMITS.max_repair_waves) {
|
|
74
|
+
const repairPlan = planRepairWave({
|
|
75
|
+
failedEnvelopes: envelopes.filter((e) => e.status === 'gate_failed'),
|
|
76
|
+
shards: graph.shards,
|
|
77
|
+
repairWaveCount: repairWaves
|
|
78
|
+
});
|
|
79
|
+
if (repairPlan.canRepair && repairPlan.shardsToRepair.length > 0) {
|
|
80
|
+
repairWaves++;
|
|
81
|
+
const repairPool = await runPatchWorkerPool({
|
|
82
|
+
apiKey: key.key,
|
|
83
|
+
missionId,
|
|
84
|
+
cwd,
|
|
85
|
+
shards: repairPlan.shardsToRepair,
|
|
86
|
+
contextSummary: JSON.stringify({ task: input.task, repair: true }),
|
|
87
|
+
maxWorkers: input.maxWorkers || 3,
|
|
88
|
+
workerTimeoutMs: GLM_NARUTO_LIMITS.max_worker_runtime_ms,
|
|
89
|
+
strategies: new Map(repairPlan.shardsToRepair.map((s) => [s.id, [s.strategy]]))
|
|
90
|
+
});
|
|
91
|
+
envelopes = [...envelopes, ...repairPool.envelopes];
|
|
92
|
+
for (const trace of repairPool.traces) {
|
|
93
|
+
traceState = recordWorkerTrace(traceState, trace);
|
|
94
|
+
}
|
|
95
|
+
failedShardIds = [...failedShardIds, ...repairPool.failedShardIds];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// 4.0.9: Verifier wave — run parallel verifier workers over gate-passed candidates.
|
|
99
|
+
let passedEnvelopes = envelopes.filter((e) => e.status === 'gate_passed');
|
|
100
|
+
if (passedEnvelopes.length > 0 && !input.noApply) {
|
|
101
|
+
const verifyApiKey = key.key;
|
|
102
|
+
const verifyResults = await Promise.allSettled(passedEnvelopes.map((env) => runVerifierWorker({
|
|
103
|
+
apiKey: verifyApiKey,
|
|
104
|
+
missionId,
|
|
105
|
+
workerId: env.worker_id,
|
|
106
|
+
envelope: env,
|
|
107
|
+
timeoutMs: 120_000,
|
|
108
|
+
})));
|
|
109
|
+
const verifiedEnvelopes = [];
|
|
110
|
+
for (let vi = 0; vi < passedEnvelopes.length; vi++) {
|
|
111
|
+
const env = passedEnvelopes[vi];
|
|
112
|
+
const res = verifyResults[vi];
|
|
113
|
+
if (res.status === 'fulfilled' && res.value.ok) {
|
|
114
|
+
verifiedEnvelopes.push({ ...env, verification_passed: true, status: 'gate_passed' });
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
verifiedEnvelopes.push({ ...env, verification_passed: false, status: 'verification_failed' });
|
|
118
|
+
}
|
|
119
|
+
if (res.status === 'fulfilled') {
|
|
120
|
+
traceState = recordWorkerTrace(traceState, res.value.trace);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
envelopes = envelopes.map((e) => {
|
|
124
|
+
const verified = verifiedEnvelopes.find((v) => v.worker_id === e.worker_id);
|
|
125
|
+
return verified ?? e;
|
|
126
|
+
});
|
|
127
|
+
passedEnvelopes = envelopes.filter((e) => e.status === 'gate_passed');
|
|
128
|
+
}
|
|
129
|
+
// Build conflict graph and merge plan
|
|
130
|
+
const nodes = passedEnvelopes.map((env) => ({
|
|
131
|
+
patch_id: env.worker_id,
|
|
132
|
+
shard_id: env.shard_id,
|
|
133
|
+
target_paths: env.target_paths,
|
|
134
|
+
score: Math.max(0, 100 - Math.floor(env.patch.length / 100)),
|
|
135
|
+
gate_passed: true,
|
|
136
|
+
patch_sha256: env.patch_sha256
|
|
137
|
+
}));
|
|
138
|
+
const conflictGraph = buildConflictGraph(passedEnvelopes, nodes);
|
|
139
|
+
let judgeResult = null;
|
|
140
|
+
if (input.useJudge && passedEnvelopes.length > 1) {
|
|
141
|
+
judgeResult = await runGlmJudge({
|
|
142
|
+
apiKey: key.key,
|
|
143
|
+
missionId,
|
|
144
|
+
envelopes: passedEnvelopes,
|
|
145
|
+
timeoutMs: 120_000
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const mergePlan = finalizeMergePlan({
|
|
149
|
+
missionId,
|
|
150
|
+
envelopes: passedEnvelopes,
|
|
151
|
+
...(judgeResult ? { judgeResult } : {}),
|
|
152
|
+
useJudge: input.useJudge || false,
|
|
153
|
+
xhighFinalizer: input.xhighFinalizer || false
|
|
154
|
+
});
|
|
155
|
+
// Apply winning merge plan
|
|
156
|
+
let appliedPatches = 0;
|
|
157
|
+
let applyResult = null;
|
|
158
|
+
if (!input.noApply && mergePlan.selected_patches.length > 0) {
|
|
159
|
+
for (const patchId of mergePlan.selected_patches) {
|
|
160
|
+
const envelope = envelopes.find((e) => e.worker_id === patchId);
|
|
161
|
+
if (!envelope)
|
|
162
|
+
continue;
|
|
163
|
+
const applied = await checkAndApplyGlmPatch({ cwd, patch: envelope.patch, apply: true });
|
|
164
|
+
if (applied.ok) {
|
|
165
|
+
appliedPatches++;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
applyResult = { ok: appliedPatches > 0, applied: mergePlan.selected_patches };
|
|
169
|
+
}
|
|
170
|
+
const terminalState = appliedPatches > 0 ? 'completed' : passedEnvelopes.length > 0 ? 'partial_candidates' : 'blocked';
|
|
171
|
+
const terminationReason = appliedPatches > 0 ? 'completed_merge_applied' : passedEnvelopes.length > 0 ? 'partial_no_apply' : 'no_gate_passed_candidates';
|
|
172
|
+
const summary = buildMissionSummary({
|
|
173
|
+
missionId,
|
|
174
|
+
startedMs,
|
|
175
|
+
workerTraces: traceState.workerTraces,
|
|
176
|
+
patchCandidates: envelopes.length,
|
|
177
|
+
gatePassed: passedEnvelopes.length,
|
|
178
|
+
mergeable: mergePlan.candidates.length,
|
|
179
|
+
appliedPatches,
|
|
180
|
+
failedShards: failedShardIds.length,
|
|
181
|
+
repairWaves
|
|
182
|
+
});
|
|
183
|
+
const result = {
|
|
184
|
+
schema: 'sks.glm-naruto-mission-result.v1',
|
|
185
|
+
ok: terminalState === 'completed',
|
|
186
|
+
status: terminalState,
|
|
187
|
+
mission_id: missionId,
|
|
188
|
+
task: input.task,
|
|
189
|
+
model: GLM_52_OPENROUTER_MODEL,
|
|
190
|
+
gpt_fallback_allowed: false,
|
|
191
|
+
termination_reason: terminationReason,
|
|
192
|
+
workers_started: summary.workers_started,
|
|
193
|
+
workers_completed: summary.workers_completed,
|
|
194
|
+
patch_candidates: summary.patch_candidates,
|
|
195
|
+
gate_passed_candidates: summary.gate_passed_candidates,
|
|
196
|
+
mergeable_candidates: summary.mergeable_candidates,
|
|
197
|
+
applied_patches: summary.applied_patches,
|
|
198
|
+
failed_shards: summary.failed_shards,
|
|
199
|
+
repair_waves: summary.repair_waves,
|
|
200
|
+
budget_used_ms: summary.budget_used_ms,
|
|
201
|
+
blockers: terminalState === 'blocked' ? ['no_gate_passed_candidates'] : [],
|
|
202
|
+
warnings: []
|
|
203
|
+
};
|
|
204
|
+
const artifactDir = await writeMissionArtifacts({
|
|
205
|
+
root: cwd,
|
|
206
|
+
missionId,
|
|
207
|
+
workGraph: graph,
|
|
208
|
+
conflictGraph,
|
|
209
|
+
mergePlan,
|
|
210
|
+
...(judgeResult ? { judgeResult } : {}),
|
|
211
|
+
workerTraces: traceState.workerTraces,
|
|
212
|
+
providerHealth: healthTracker.snapshot(),
|
|
213
|
+
termination: { schema: 'sks.glm-naruto-termination.v1', mission_id: missionId, terminal_state: terminalState, reason: terminationReason, wall_clock_ms: summary.wall_clock_ms },
|
|
214
|
+
...(applyResult ? { applyResult: { ...applyResult, schema: 'sks.glm-naruto-apply-result.v1' } } : {}),
|
|
215
|
+
verificationSummary: { schema: 'sks.glm-naruto-verification.v1', verified: passedEnvelopes.length, total: envelopes.length },
|
|
216
|
+
missionResult: result,
|
|
217
|
+
envelopes
|
|
218
|
+
});
|
|
219
|
+
// 4.0.9: Write canonical stop-gate artifacts for hook resolution.
|
|
220
|
+
await writeFinalStopGate({
|
|
221
|
+
root: cwd,
|
|
222
|
+
missionId,
|
|
223
|
+
route: 'GLM_NARUTO',
|
|
224
|
+
routeCommand: '$Naruto',
|
|
225
|
+
status: result.ok ? 'passed' : (terminalState === 'blocked' ? 'blocked' : 'failed'),
|
|
226
|
+
terminal: terminalState === 'completed' || terminalState === 'blocked',
|
|
227
|
+
terminalState,
|
|
228
|
+
evidence: {
|
|
229
|
+
build_passed: result.ok,
|
|
230
|
+
tests_passed: result.ok,
|
|
231
|
+
route_evidence_passed: result.ok,
|
|
232
|
+
per_worker_artifacts: true,
|
|
233
|
+
verifier_wave_run: true,
|
|
234
|
+
model_guard_enforced: true,
|
|
235
|
+
},
|
|
236
|
+
blockers: result.blockers || [],
|
|
237
|
+
nativeGateFile: 'termination.json',
|
|
238
|
+
}).catch(() => null);
|
|
239
|
+
return { ...result, artifact_dir: artifactDir };
|
|
240
|
+
}
|
|
241
|
+
function missionResult(missionId, task, status, reason, patchCandidates, startedMs, envelopes, traces, blockers, warnings) {
|
|
242
|
+
return {
|
|
243
|
+
schema: 'sks.glm-naruto-mission-result.v1',
|
|
244
|
+
ok: status === 'completed',
|
|
245
|
+
status,
|
|
246
|
+
mission_id: missionId,
|
|
247
|
+
task,
|
|
248
|
+
model: GLM_52_OPENROUTER_MODEL,
|
|
249
|
+
gpt_fallback_allowed: false,
|
|
250
|
+
termination_reason: reason,
|
|
251
|
+
workers_started: traces.length,
|
|
252
|
+
workers_completed: traces.filter((t) => t.status === 'completed').length,
|
|
253
|
+
patch_candidates: envelopes.length,
|
|
254
|
+
gate_passed_candidates: envelopes.filter((e) => e.status === 'gate_passed').length,
|
|
255
|
+
mergeable_candidates: 0,
|
|
256
|
+
applied_patches: 0,
|
|
257
|
+
failed_shards: 0,
|
|
258
|
+
repair_waves: 0,
|
|
259
|
+
budget_used_ms: Date.now() - startedMs,
|
|
260
|
+
blockers,
|
|
261
|
+
warnings
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
async function readGitStatus(cwd) {
|
|
265
|
+
const { spawn } = await import('node:child_process');
|
|
266
|
+
return new Promise((resolve) => {
|
|
267
|
+
const child = spawn('git', ['status', '--short'], { cwd, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
268
|
+
let stdout = '';
|
|
269
|
+
child.stdout.on('data', (chunk) => { stdout += String(chunk); });
|
|
270
|
+
child.on('close', () => resolve(stdout.trim() || undefined));
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
function extractMentionedPaths(task) {
|
|
274
|
+
const matches = task.match(/(?:^|\s|[`"'])([A-Za-z0-9_.-]+\/[A-Za-z0-9_./-]+\.[A-Za-z0-9]+)(?:\s|[`"']|$)/g) || [];
|
|
275
|
+
return [...new Set(matches.map((value) => value.trim().replace(/^[`"']|[`"']$/g, '')))];
|
|
276
|
+
}
|
|
277
|
+
//# sourceMappingURL=glm-naruto-orchestrator.js.map
|