mergejudge-runner 0.2.0 → 0.2.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 +2 -2
- package/dist/cli.js +204 -65
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Status: MVP. Design: [docs/RUNNERS_DESIGN.md](../../docs/RUNNERS_DESIGN.md).
|
|
|
10
10
|
npx mergejudge-runner setup <code> # code from the panel: Runners > Add runner
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Registers the runner, detects which of `claude` and `codex` are installed and signed in (`claude auth status`, `codex login status`; Codex counts only when signed in **with ChatGPT**, an API-key login is warned about and codex is not offered), asks yes/no about a GLM (z.ai) API key, and offers to start it at login (launchd / systemd user unit). Flags: `--yes`, `--no-autostart`, `--print-unit`, `--engine <claude-code|codex|glm>`, `--model [engine=]<model>` (repeatable), `--glm-key-env NAME`, `--no-glm`; `MERGEJUDGE_URL` overrides the server. See the [guide](https://mergejudge.si/guide/run-on-your-machine).
|
|
13
|
+
Registers the runner, detects which of `claude` and `codex` are installed and signed in (`claude auth status`, `codex login status`; Codex counts only when signed in **with ChatGPT**, an API-key login is warned about and codex is not offered), asks yes/no about a GLM (z.ai) API key, and offers to start it at login (launchd / systemd user unit). Flags: `--yes`, `--no-autostart`, `--print-unit`, `--engine <claude-code|codex|glm>`, `--model [engine=]<model>` (repeatable), `--codex-effort <minimal|low|medium|high|xhigh>` (Codex reasoning effort, default `low`; stored as `engines.codex.effort` in the config), `--glm-key-env NAME`, `--no-glm`; `MERGEJUDGE_URL` overrides the server. See the [guide](https://mergejudge.si/guide/run-on-your-machine).
|
|
14
14
|
|
|
15
15
|
## Install (from source)
|
|
16
16
|
|
|
@@ -40,7 +40,7 @@ This stores the runner id and token in `$XDG_CONFIG_HOME/mergejudge-runner/confi
|
|
|
40
40
|
| Engine | What runs | Billing |
|
|
41
41
|
|---|---|---|
|
|
42
42
|
| `claude-code` | `claude -p --tools ""` on your subscription | subscription |
|
|
43
|
-
| `codex` | `codex exec --sandbox read-only --skip-git-repo-check --ephemeral --color never --ignore-user-config [-m MODEL] --output-schema <file> -o <file> -` (prompt on stdin, ChatGPT login) | subscription |
|
|
43
|
+
| `codex` | `codex exec --sandbox read-only --skip-git-repo-check --ephemeral --color never --ignore-user-config [-m MODEL] -c model_reasoning_effort=EFFORT --output-schema <file> -o <file> -` (prompt on stdin, ChatGPT login) | subscription |
|
|
44
44
|
| `glm` | `claude -p` with `ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic` and `ANTHROPIC_AUTH_TOKEN=<your key>` set only in the child env | **API key (billed per use)** |
|
|
45
45
|
|
|
46
46
|
- The runner declares its engines, billing and models when it registers and on the runner heartbeat at every start. The config keeps `engine` (default), `engines: { name: { model, models } }` and, for GLM, `glmApiKey` (config is `0600`; `ZAI_API_KEY` in the environment works too). `models: ["*"]` offers any model.
|
package/dist/cli.js
CHANGED
|
@@ -27291,37 +27291,6 @@ function buildEngineSettings(enabled, defaultEngine, modelFlags) {
|
|
|
27291
27291
|
return out;
|
|
27292
27292
|
}
|
|
27293
27293
|
|
|
27294
|
-
// src/redact.ts
|
|
27295
|
-
var PATTERNS = [
|
|
27296
|
-
/mjr_[ea]_[A-Za-z0-9_-]{6,}/g,
|
|
27297
|
-
// MergeJudge runner / enrollment tokens
|
|
27298
|
-
/sk-ant-[A-Za-z0-9_-]{6,}/g,
|
|
27299
|
-
// Anthropic keys and OAuth tokens
|
|
27300
|
-
/Bearer\s+[A-Za-z0-9._~+/=-]{8,}/gi,
|
|
27301
|
-
/\b(ANTHROPIC_[A-Z_]+|MERGEJUDGE_[A-Z_]*(?:TOKEN|KEY|SECRET)[A-Z_]*|CLAUDE_CODE_OAUTH_TOKEN)=\S+/g,
|
|
27302
|
-
/\bgh[pousr]_[A-Za-z0-9]{20,}/g,
|
|
27303
|
-
/\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}/g,
|
|
27304
|
-
// OpenAI-style keys
|
|
27305
|
-
/\b[0-9a-f]{32}\.[A-Za-z0-9]{8,}\b/g,
|
|
27306
|
-
// z.ai API keys (<32 hex>.<secret>)
|
|
27307
|
-
/\b(ZAI_[A-Z_]*|OPENAI_[A-Z_]*|CODEX_[A-Z_]*(?:KEY|TOKEN)[A-Z_]*)=\S+/g
|
|
27308
|
-
];
|
|
27309
|
-
var REDACTED = "[redacted]";
|
|
27310
|
-
function redact(text, secrets = []) {
|
|
27311
|
-
let out = text;
|
|
27312
|
-
for (const s of secrets) if (s && s.length >= 6) out = out.split(s).join(REDACTED);
|
|
27313
|
-
for (const re of PATTERNS) out = out.replace(re, REDACTED);
|
|
27314
|
-
return out;
|
|
27315
|
-
}
|
|
27316
|
-
var tokenPrefix = (t) => t ? `${t.slice(0, 10)}\u2026` : "(none)";
|
|
27317
|
-
function createLogger(write, secrets = () => [], now = () => /* @__PURE__ */ new Date()) {
|
|
27318
|
-
const emit = (level, msg, f) => {
|
|
27319
|
-
const kv = f ? Object.entries(f).filter(([, v]) => v !== void 0).map(([k, v]) => ` ${k}=${typeof v === "string" && /[\s"]/.test(v) ? JSON.stringify(v) : String(v)}`).join("") : "";
|
|
27320
|
-
write(redact(`${now().toISOString()} ${level} ${msg}${kv}`, secrets()));
|
|
27321
|
-
};
|
|
27322
|
-
return { info: (m, f) => emit("INFO", m, f), warn: (m, f) => emit("WARN", m, f), error: (m, f) => emit("ERROR", m, f) };
|
|
27323
|
-
}
|
|
27324
|
-
|
|
27325
27294
|
// src/engine.ts
|
|
27326
27295
|
import { spawn } from "node:child_process";
|
|
27327
27296
|
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
@@ -27407,6 +27376,13 @@ var safeModel = (model) => {
|
|
|
27407
27376
|
if (!MODEL_RE.test(model)) throw new EngineError("spawn", "model name has unsupported characters");
|
|
27408
27377
|
return model;
|
|
27409
27378
|
};
|
|
27379
|
+
var CODEX_EFFORTS = ["minimal", "low", "medium", "high", "xhigh"];
|
|
27380
|
+
var DEFAULT_CODEX_EFFORT = "low";
|
|
27381
|
+
var isCodexEffort = (s) => CODEX_EFFORTS.includes(s);
|
|
27382
|
+
var safeEffort = (e) => {
|
|
27383
|
+
if (!isCodexEffort(e)) throw new EngineError("spawn", `unsupported codex reasoning effort (use ${CODEX_EFFORTS.join(", ")})`);
|
|
27384
|
+
return e;
|
|
27385
|
+
};
|
|
27410
27386
|
function claudeArgs(model) {
|
|
27411
27387
|
const m = safeModel(model);
|
|
27412
27388
|
return [
|
|
@@ -27423,6 +27399,7 @@ function claudeArgs(model) {
|
|
|
27423
27399
|
}
|
|
27424
27400
|
function codexArgs(o) {
|
|
27425
27401
|
const m = safeModel(o.model);
|
|
27402
|
+
const e = o.effort ? safeEffort(o.effort) : null;
|
|
27426
27403
|
return [
|
|
27427
27404
|
"exec",
|
|
27428
27405
|
"--sandbox",
|
|
@@ -27433,6 +27410,7 @@ function codexArgs(o) {
|
|
|
27433
27410
|
"never",
|
|
27434
27411
|
"--ignore-user-config",
|
|
27435
27412
|
...m ? ["-m", m] : [],
|
|
27413
|
+
...e ? ["-c", `model_reasoning_effort=${e}`] : [],
|
|
27436
27414
|
"--output-schema",
|
|
27437
27415
|
o.schemaFile,
|
|
27438
27416
|
"-o",
|
|
@@ -27606,7 +27584,7 @@ async function runCodex(prompt, signal, o = {}) {
|
|
|
27606
27584
|
try {
|
|
27607
27585
|
const schemaFile = join2(cwd, "schema.json"), outFile = join2(cwd, "last-message.txt");
|
|
27608
27586
|
await writeFile(schemaFile, JSON.stringify(CODEX_OUTPUT_SCHEMA), { mode: 384 });
|
|
27609
|
-
const args = codexArgs({ model: o.model, schemaFile, outFile });
|
|
27587
|
+
const args = codexArgs({ model: o.model, effort: o.effort ?? DEFAULT_CODEX_EFFORT, schemaFile, outFile });
|
|
27610
27588
|
await spawnCli({ bin: o.bin ?? "codex", args, env: engineEnv(o.env, "codex"), cwd, stdin: prompt, signal, killGraceMs: o.killGraceMs });
|
|
27611
27589
|
let last;
|
|
27612
27590
|
try {
|
|
@@ -27654,6 +27632,37 @@ ${req.user}`;
|
|
|
27654
27632
|
}
|
|
27655
27633
|
};
|
|
27656
27634
|
|
|
27635
|
+
// src/redact.ts
|
|
27636
|
+
var PATTERNS = [
|
|
27637
|
+
/mjr_[ea]_[A-Za-z0-9_-]{6,}/g,
|
|
27638
|
+
// MergeJudge runner / enrollment tokens
|
|
27639
|
+
/sk-ant-[A-Za-z0-9_-]{6,}/g,
|
|
27640
|
+
// Anthropic keys and OAuth tokens
|
|
27641
|
+
/Bearer\s+[A-Za-z0-9._~+/=-]{8,}/gi,
|
|
27642
|
+
/\b(ANTHROPIC_[A-Z_]+|MERGEJUDGE_[A-Z_]*(?:TOKEN|KEY|SECRET)[A-Z_]*|CLAUDE_CODE_OAUTH_TOKEN)=\S+/g,
|
|
27643
|
+
/\bgh[pousr]_[A-Za-z0-9]{20,}/g,
|
|
27644
|
+
/\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}/g,
|
|
27645
|
+
// OpenAI-style keys
|
|
27646
|
+
/\b[0-9a-f]{32}\.[A-Za-z0-9]{8,}\b/g,
|
|
27647
|
+
// z.ai API keys (<32 hex>.<secret>)
|
|
27648
|
+
/\b(ZAI_[A-Z_]*|OPENAI_[A-Z_]*|CODEX_[A-Z_]*(?:KEY|TOKEN)[A-Z_]*)=\S+/g
|
|
27649
|
+
];
|
|
27650
|
+
var REDACTED = "[redacted]";
|
|
27651
|
+
function redact(text, secrets = []) {
|
|
27652
|
+
let out = text;
|
|
27653
|
+
for (const s of secrets) if (s && s.length >= 6) out = out.split(s).join(REDACTED);
|
|
27654
|
+
for (const re of PATTERNS) out = out.replace(re, REDACTED);
|
|
27655
|
+
return out;
|
|
27656
|
+
}
|
|
27657
|
+
var tokenPrefix = (t) => t ? `${t.slice(0, 10)}\u2026` : "(none)";
|
|
27658
|
+
function createLogger(write, secrets = () => [], now = () => /* @__PURE__ */ new Date()) {
|
|
27659
|
+
const emit = (level, msg, f) => {
|
|
27660
|
+
const kv = f ? Object.entries(f).filter(([, v]) => v !== void 0).map(([k, v]) => ` ${k}=${typeof v === "string" && /[\s"]/.test(v) ? JSON.stringify(v) : String(v)}`).join("") : "";
|
|
27661
|
+
write(redact(`${now().toISOString()} ${level} ${msg}${kv}`, secrets()));
|
|
27662
|
+
};
|
|
27663
|
+
return { info: (m, f) => emit("INFO", m, f), warn: (m, f) => emit("WARN", m, f), error: (m, f) => emit("ERROR", m, f) };
|
|
27664
|
+
}
|
|
27665
|
+
|
|
27657
27666
|
// ../../node_modules/balanced-match/dist/esm/index.js
|
|
27658
27667
|
var balanced = (a, b, str2) => {
|
|
27659
27668
|
const ma = a instanceof RegExp ? maybeMatch(a, str2) : a;
|
|
@@ -30212,6 +30221,31 @@ function chunkById(snap, id) {
|
|
|
30212
30221
|
var embeddingText = (c) => `${c.path}
|
|
30213
30222
|
${c.text}`.slice(0, 1500);
|
|
30214
30223
|
|
|
30224
|
+
// ../../src/budget.ts
|
|
30225
|
+
var DEFAULT_OUTPUT_TOKENS = 2500;
|
|
30226
|
+
var REASONING_OUTPUT_TOKENS = 8e3;
|
|
30227
|
+
var REASONING_MODEL = /(?:^|[/:@])(?:glm-?(?:5|4\.[5-9])|gpt-5|o[1-9](?:-|$)|deepseek-(?:r1|reasoner)|qwq|[^/]*thinking)/i;
|
|
30228
|
+
var isReasoningModel = (model) => !!model && REASONING_MODEL.test(model);
|
|
30229
|
+
var MIN_OUTPUT = 256;
|
|
30230
|
+
var MIN_INPUT = 1e3;
|
|
30231
|
+
function resolveOutputLimit(cfg) {
|
|
30232
|
+
const total = cfg.budget.max_tokens_per_pr;
|
|
30233
|
+
const explicit = cfg.budget.max_output_tokens;
|
|
30234
|
+
if (explicit !== void 0) return Math.max(MIN_OUTPUT, Math.min(explicit, total - MIN_INPUT));
|
|
30235
|
+
const want = isReasoningModel(cfg.provider.model) ? REASONING_OUTPUT_TOKENS : DEFAULT_OUTPUT_TOKENS;
|
|
30236
|
+
return Math.max(MIN_OUTPUT, Math.min(want, Math.floor(total / 2)));
|
|
30237
|
+
}
|
|
30238
|
+
function retryOutputLimit(limit, room) {
|
|
30239
|
+
return Math.max(limit, Math.min(limit * 2, room));
|
|
30240
|
+
}
|
|
30241
|
+
var COMPACT_JSON_INSTRUCTION = "Respond with compact JSON only, no reasoning text, keep findings short.";
|
|
30242
|
+
|
|
30243
|
+
// ../../src/llm/provider.ts
|
|
30244
|
+
async function completeWithMeta(p, req) {
|
|
30245
|
+
return p.completeEx ? p.completeEx(req) : { text: await p.complete(req), truncated: false };
|
|
30246
|
+
}
|
|
30247
|
+
var canUseTools = (p) => !!p && p.supportsTools === true && typeof p.chat === "function";
|
|
30248
|
+
|
|
30215
30249
|
// ../../src/rubrics.ts
|
|
30216
30250
|
var import_yaml = __toESM(require_dist());
|
|
30217
30251
|
var criterionSchema = external_exports.object({
|
|
@@ -30384,7 +30418,7 @@ function buildUserPrompt(input2, cfg, system = SYSTEM_PROMPT) {
|
|
|
30384
30418
|
const ctxText = renderContext(input2.context ?? []);
|
|
30385
30419
|
const budgetChars = Math.max(
|
|
30386
30420
|
0,
|
|
30387
|
-
cfg.budget.max_tokens_per_pr - cfg
|
|
30421
|
+
cfg.budget.max_tokens_per_pr - resolveOutputLimit(cfg) - estimateTokens(system) - 100 - estimateTokens(ctxText)
|
|
30388
30422
|
) * 4;
|
|
30389
30423
|
const title = truncate(sanitizeUntrusted(input2.title), 300);
|
|
30390
30424
|
const body = truncate(sanitizeUntrusted(input2.body ?? ""), Math.min(2e3, Math.floor(budgetChars * 0.15)));
|
|
@@ -30458,14 +30492,68 @@ function parseTriageOutput(raw) {
|
|
|
30458
30492
|
const a = t.indexOf("{");
|
|
30459
30493
|
const b = t.lastIndexOf("}");
|
|
30460
30494
|
if (a < 0 || b <= a) throw new Error("no JSON object in LLM output");
|
|
30461
|
-
|
|
30495
|
+
const json2 = t.slice(a, b + 1);
|
|
30462
30496
|
try {
|
|
30463
30497
|
return triageOutputSchema.parse(JSON.parse(json2));
|
|
30464
|
-
} catch (
|
|
30465
|
-
|
|
30466
|
-
|
|
30498
|
+
} catch (first) {
|
|
30499
|
+
const repaired = repairJson(t.slice(a));
|
|
30500
|
+
if (repaired !== void 0) {
|
|
30501
|
+
try {
|
|
30502
|
+
return triageOutputSchema.parse(JSON.parse(repaired));
|
|
30503
|
+
} catch {
|
|
30504
|
+
}
|
|
30505
|
+
}
|
|
30506
|
+
throw first;
|
|
30467
30507
|
}
|
|
30468
30508
|
}
|
|
30509
|
+
function repairJson(s) {
|
|
30510
|
+
let out = "";
|
|
30511
|
+
const stack = [];
|
|
30512
|
+
let inStr = false;
|
|
30513
|
+
for (let i = 0; i < s.length; i++) {
|
|
30514
|
+
const c = s[i];
|
|
30515
|
+
if (inStr) {
|
|
30516
|
+
if (c === "\\") {
|
|
30517
|
+
const n = s[i + 1];
|
|
30518
|
+
if (n !== void 0 && /["\\/bfnrtu]/.test(n)) {
|
|
30519
|
+
out += c + n;
|
|
30520
|
+
i++;
|
|
30521
|
+
} else out += "\\\\";
|
|
30522
|
+
} else if (c === '"') {
|
|
30523
|
+
const rest = s.slice(i + 1).match(/^\s*(.)?/)?.[1];
|
|
30524
|
+
const closes = rest === void 0 || rest === "," || rest === "}" || rest === "]" || rest === ":";
|
|
30525
|
+
if (closes) {
|
|
30526
|
+
out += c;
|
|
30527
|
+
inStr = false;
|
|
30528
|
+
} else out += '\\"';
|
|
30529
|
+
} else if (c === "\n") out += "\\n";
|
|
30530
|
+
else if (c === "\r") out += "\\r";
|
|
30531
|
+
else if (c === " ") out += "\\t";
|
|
30532
|
+
else if (c < " ") out += " ";
|
|
30533
|
+
else out += c;
|
|
30534
|
+
continue;
|
|
30535
|
+
}
|
|
30536
|
+
if (c === '"') {
|
|
30537
|
+
inStr = true;
|
|
30538
|
+
out += c;
|
|
30539
|
+
} else if (c === "{" || c === "[") {
|
|
30540
|
+
stack.push(c === "{" ? "}" : "]");
|
|
30541
|
+
out += c;
|
|
30542
|
+
} else if (c === "}" || c === "]") {
|
|
30543
|
+
out = out.replace(/,\s*$/, "");
|
|
30544
|
+
if (stack[stack.length - 1] === c) {
|
|
30545
|
+
stack.pop();
|
|
30546
|
+
out += c;
|
|
30547
|
+
if (!stack.length) return out;
|
|
30548
|
+
}
|
|
30549
|
+
} else out += c;
|
|
30550
|
+
}
|
|
30551
|
+
if (!stack.length) return out;
|
|
30552
|
+
if (inStr) out += '"';
|
|
30553
|
+
out = out.replace(/,\s*$/, "").replace(/:\s*$/, ": null");
|
|
30554
|
+
out = out.replace(/,\s*"(?:[^"\\]|\\.)*"\s*$/, "");
|
|
30555
|
+
return out + stack.reverse().join("");
|
|
30556
|
+
}
|
|
30469
30557
|
var MAX_FINDINGS = 20;
|
|
30470
30558
|
function validateAgainstFiles(out, files, criteria = []) {
|
|
30471
30559
|
const known = new Set(files.map((f) => f.path));
|
|
@@ -30515,8 +30603,15 @@ function tokenBreakdown(opts) {
|
|
|
30515
30603
|
skipped_diff_files: opts.skipped
|
|
30516
30604
|
};
|
|
30517
30605
|
}
|
|
30606
|
+
function outputLimitWarning(o) {
|
|
30607
|
+
return `LLM output limit: ${o.ref ?? "?"} model=${o.model ?? "?"} limit=${o.limit} output_tokens=${o.outputTokens} truncated=${o.truncated} retry=${o.retry}${o.retryLimit ? ` (limit ${o.retryLimit}, compact JSON)` : ""}`;
|
|
30608
|
+
}
|
|
30609
|
+
function retryInstruction(truncated, why) {
|
|
30610
|
+
const reason = truncated ? "Your previous reply was cut off at the output limit" : `Your previous reply was not valid JSON (${why})`;
|
|
30611
|
+
return `REPAIR: ${reason}. ${COMPACT_JSON_INSTRUCTION} Reply with ONLY a single JSON object matching the schema. No markdown fences, no prose.`;
|
|
30612
|
+
}
|
|
30518
30613
|
async function runTriage(provider, input2, cfg, ctx = {}, log = () => {
|
|
30519
|
-
}) {
|
|
30614
|
+
}, warn = log) {
|
|
30520
30615
|
const system = buildSystemPrompt(ctx) + (input2.context?.length ? `
|
|
30521
30616
|
|
|
30522
30617
|
${CONTEXT_RULES}` : "");
|
|
@@ -30526,6 +30621,9 @@ ${CONTEXT_RULES}` : "");
|
|
|
30526
30621
|
let raw = "";
|
|
30527
30622
|
let retried = false;
|
|
30528
30623
|
let lastErr = "unknown parse error";
|
|
30624
|
+
const limit = resolveOutputLimit(cfg);
|
|
30625
|
+
let retryLimit = limit;
|
|
30626
|
+
let firstTruncated = false, firstOut = 0, lastTruncated = false;
|
|
30529
30627
|
const mkBreakdown = (outTok) => ({
|
|
30530
30628
|
...tokenBreakdown({ systemBase: system, policyText: ctx.policyText, criteria: ctx.criteria, diffTokens: diffOnly, outputTokens: outTok, skipped }),
|
|
30531
30629
|
context: contextTokens,
|
|
@@ -30534,20 +30632,38 @@ ${CONTEXT_RULES}` : "");
|
|
|
30534
30632
|
try {
|
|
30535
30633
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
30536
30634
|
const repair = attempt === 1;
|
|
30537
|
-
if (repair)
|
|
30635
|
+
if (repair) {
|
|
30636
|
+
retried = true;
|
|
30637
|
+
retryLimit = retryOutputLimit(limit, cfg.budget.max_tokens_per_pr - estimateTokens(system) - estimateTokens(prompt) - 100);
|
|
30638
|
+
}
|
|
30538
30639
|
try {
|
|
30539
|
-
|
|
30640
|
+
const r = await completeWithMeta(provider, {
|
|
30540
30641
|
system,
|
|
30541
30642
|
user: repair ? `${prompt}
|
|
30542
30643
|
|
|
30543
|
-
|
|
30544
|
-
maxOutputTokens:
|
|
30644
|
+
${retryInstruction(firstTruncated, lastErr)}` : prompt,
|
|
30645
|
+
maxOutputTokens: repair ? retryLimit : limit,
|
|
30545
30646
|
jsonMode: true
|
|
30546
30647
|
});
|
|
30547
|
-
|
|
30648
|
+
raw = r.text;
|
|
30649
|
+
lastTruncated = r.truncated;
|
|
30650
|
+
if (!repair) {
|
|
30651
|
+
firstTruncated = r.truncated;
|
|
30652
|
+
firstOut = r.outputTokens ?? estimateTokens(raw);
|
|
30653
|
+
}
|
|
30654
|
+
let llm;
|
|
30655
|
+
try {
|
|
30656
|
+
llm = validateAgainstFiles(parseTriageOutput(raw), input2.files, ctx.criteria ?? []);
|
|
30657
|
+
} catch (pe) {
|
|
30658
|
+
if (!r.truncated) throw pe;
|
|
30659
|
+
throw new Error(`output truncated at ${repair ? retryLimit : limit} tokens (${pe.message})`);
|
|
30660
|
+
}
|
|
30548
30661
|
const outTok = estimateTokens(raw);
|
|
30549
|
-
if (retried)
|
|
30550
|
-
|
|
30662
|
+
if (retried) {
|
|
30663
|
+
log("LLM JSON parse succeeded on repair attempt.");
|
|
30664
|
+
warn(outputLimitWarning({ ref: ctx.ref, model: cfg.provider.model, limit, retryLimit, outputTokens: firstOut, truncated: firstTruncated, retry: "succeeded" }));
|
|
30665
|
+
}
|
|
30666
|
+
return { llm, tokensEstimate: estimateTokens(system) + diffTokens + outTok, breakdown: mkBreakdown(outTok), retried, ...firstTruncated ? { truncated: true } : {}, outputLimit: limit };
|
|
30551
30667
|
} catch (e) {
|
|
30552
30668
|
lastErr = e.message.slice(0, 160);
|
|
30553
30669
|
log(`LLM JSON attempt ${attempt + 1} failed: ${lastErr}`);
|
|
@@ -30558,12 +30674,15 @@ REPAIR: Your previous reply was not valid JSON (${lastErr}). Reply with ONLY a s
|
|
|
30558
30674
|
} catch (e) {
|
|
30559
30675
|
const outTok = estimateTokens(raw);
|
|
30560
30676
|
log(`LLM triage falling back to rules-only after JSON failure: ${e.message.slice(0, 160)}`);
|
|
30677
|
+
if (retried) warn(outputLimitWarning({ ref: ctx.ref, model: cfg.provider.model, limit, retryLimit, outputTokens: firstOut, truncated: firstTruncated || lastTruncated, retry: "failed" }));
|
|
30561
30678
|
return {
|
|
30562
30679
|
llm: null,
|
|
30563
30680
|
error: e.message.slice(0, 200),
|
|
30564
30681
|
tokensEstimate: estimateTokens(system) + diffTokens + outTok,
|
|
30565
30682
|
breakdown: mkBreakdown(outTok),
|
|
30566
|
-
retried
|
|
30683
|
+
retried,
|
|
30684
|
+
...firstTruncated || lastTruncated ? { truncated: true } : {},
|
|
30685
|
+
outputLimit: limit
|
|
30567
30686
|
};
|
|
30568
30687
|
}
|
|
30569
30688
|
}
|
|
@@ -30652,7 +30771,7 @@ function deepRules(cfg, tools) {
|
|
|
30652
30771
|
}
|
|
30653
30772
|
var turnTokens = (m) => estimateTokens(m.content + (m.role === "assistant" && m.toolCalls ? JSON.stringify(m.toolCalls) : ""));
|
|
30654
30773
|
async function runDeepTriage(provider, input2, cfg, ctx = {}, opts, log = () => {
|
|
30655
|
-
}) {
|
|
30774
|
+
}, warn = log) {
|
|
30656
30775
|
const now = opts.now ?? Date.now;
|
|
30657
30776
|
const started = now();
|
|
30658
30777
|
const system = buildSystemPrompt(ctx) + `
|
|
@@ -30668,19 +30787,26 @@ ${deepRules(cfg, opts.tools)}`;
|
|
|
30668
30787
|
let stopped;
|
|
30669
30788
|
let retried = false;
|
|
30670
30789
|
let raw = "";
|
|
30671
|
-
const
|
|
30790
|
+
const limit = resolveOutputLimit(cfg);
|
|
30791
|
+
let retryLimit = limit;
|
|
30792
|
+
let lastTruncated = false, lastOut = 0, firstTruncated = false, firstOut = 0;
|
|
30793
|
+
const turn = async (tools, jsonMode, maxOutputTokens = limit) => {
|
|
30672
30794
|
const inEst = sysTok + messages.reduce((n, m) => n + turnTokens(m), 0);
|
|
30673
|
-
const t = await provider.chat({ system, messages, tools, maxOutputTokens
|
|
30795
|
+
const t = await provider.chat({ system, messages, tools, maxOutputTokens, jsonMode });
|
|
30674
30796
|
modelCalls++;
|
|
30675
30797
|
const o = t.usage?.output ?? estimateTokens(t.text + JSON.stringify(t.toolCalls));
|
|
30676
30798
|
used += (t.usage?.input ?? inEst) + o;
|
|
30677
30799
|
outTok += o;
|
|
30800
|
+
lastTruncated = !!t.truncated;
|
|
30801
|
+
lastOut = o;
|
|
30678
30802
|
return t;
|
|
30679
30803
|
};
|
|
30680
30804
|
const result = (llm, error62) => ({
|
|
30681
30805
|
llm,
|
|
30682
30806
|
...error62 ? { error: error62 } : {},
|
|
30683
30807
|
retried,
|
|
30808
|
+
outputLimit: limit,
|
|
30809
|
+
...firstTruncated || lastTruncated ? { truncated: true } : {},
|
|
30684
30810
|
tokensEstimate: used,
|
|
30685
30811
|
breakdown: {
|
|
30686
30812
|
...tokenBreakdown({ systemBase: system, policyText: ctx.policyText, criteria: ctx.criteria, diffTokens, outputTokens: outTok, skipped }),
|
|
@@ -30697,7 +30823,7 @@ ${deepRules(cfg, opts.tools)}`;
|
|
|
30697
30823
|
const convTok = sysTok + messages.reduce((n, m) => n + turnTokens(m), 0);
|
|
30698
30824
|
if (steps >= cfg.deep.max_steps) stopped = "steps";
|
|
30699
30825
|
else if (now() - started >= cfg.deep.max_seconds * 1e3) stopped = "time";
|
|
30700
|
-
else if (used + convTok +
|
|
30826
|
+
else if (used + convTok + limit + headroom > cfg.deep.max_tokens) stopped = "tokens";
|
|
30701
30827
|
if (stopped) break;
|
|
30702
30828
|
const t = await turn(opts.tools.defs, false);
|
|
30703
30829
|
if (!t.toolCalls.length) {
|
|
@@ -30732,13 +30858,20 @@ ${deepRules(cfg, opts.tools)}`;
|
|
|
30732
30858
|
const why = e.message.slice(0, 160);
|
|
30733
30859
|
log(`LLM JSON attempt 1 failed: ${why}`);
|
|
30734
30860
|
retried = true;
|
|
30861
|
+
firstTruncated = lastTruncated;
|
|
30862
|
+
firstOut = lastOut;
|
|
30863
|
+
const convNow = sysTok + messages.reduce((n, m) => n + turnTokens(m), 0);
|
|
30864
|
+
retryLimit = retryOutputLimit(limit, cfg.deep.max_tokens - used - convNow);
|
|
30735
30865
|
messages.push({ role: "assistant", content: raw });
|
|
30736
|
-
messages.push({ role: "user", content:
|
|
30737
|
-
raw = (await turn(void 0, true)).text;
|
|
30738
|
-
|
|
30866
|
+
messages.push({ role: "user", content: retryInstruction(firstTruncated, why) });
|
|
30867
|
+
raw = (await turn(void 0, true, retryLimit)).text;
|
|
30868
|
+
const llm = validateAgainstFiles(parseTriageOutput(raw), input2.files, ctx.criteria ?? []);
|
|
30869
|
+
warn(outputLimitWarning({ ref: ctx.ref, model: cfg.provider.model, limit, retryLimit, outputTokens: firstOut, truncated: firstTruncated, retry: "succeeded" }));
|
|
30870
|
+
return result(llm);
|
|
30739
30871
|
}
|
|
30740
30872
|
} catch (e) {
|
|
30741
30873
|
log(`LLM triage falling back to rules-only after deep-mode failure: ${e.message.slice(0, 160)}`);
|
|
30874
|
+
if (retried) warn(outputLimitWarning({ ref: ctx.ref, model: cfg.provider.model, limit, retryLimit, outputTokens: firstOut, truncated: firstTruncated || lastTruncated, retry: "failed" }));
|
|
30742
30875
|
return result(null, e.message.slice(0, 200));
|
|
30743
30876
|
}
|
|
30744
30877
|
}
|
|
@@ -30847,9 +30980,6 @@ function semanticSearch(e, base, snap, changedPaths) {
|
|
|
30847
30980
|
};
|
|
30848
30981
|
}
|
|
30849
30982
|
|
|
30850
|
-
// ../../src/llm/provider.ts
|
|
30851
|
-
var canUseTools = (p) => !!p && p.supportsTools === true && typeof p.chat === "function";
|
|
30852
|
-
|
|
30853
30983
|
// ../../src/policy.ts
|
|
30854
30984
|
var POLICY_MAX_EACH = 8 * 1024;
|
|
30855
30985
|
var POLICY_MAX_TOTAL = 8 * 1024;
|
|
@@ -30968,8 +31098,8 @@ async function produceReviewResult(o) {
|
|
|
30968
31098
|
context = g.pieces;
|
|
30969
31099
|
}
|
|
30970
31100
|
const prompt = { title: pr.title ?? "", body: pr.body ?? "", files: kept, context };
|
|
30971
|
-
const tctx = { policyText: policies.text, criteria, skippedDiffFiles: skipped };
|
|
30972
|
-
const res = mode === "deep" && tools && canUseTools(provider) ? await runDeepTriage(provider, prompt, cfg, tctx, { tools }, log.info) : await runTriage(provider, prompt, cfg, tctx, log.info);
|
|
31101
|
+
const tctx = { policyText: policies.text, criteria, skippedDiffFiles: skipped, ref: o.ref ?? `#${pr.number}` };
|
|
31102
|
+
const res = mode === "deep" && tools && canUseTools(provider) ? await runDeepTriage(provider, prompt, cfg, tctx, { tools }, log.info, log.warning) : await runTriage(provider, prompt, cfg, tctx, log.info, log.warning);
|
|
30973
31103
|
usedMode = mode;
|
|
30974
31104
|
tokens = res.tokensEstimate;
|
|
30975
31105
|
llm = res.llm;
|
|
@@ -30977,7 +31107,7 @@ async function produceReviewResult(o) {
|
|
|
30977
31107
|
log.info(
|
|
30978
31108
|
`Tokens ~${res.breakdown.total} (system ${res.breakdown.system} \xB7 policy ${res.breakdown.policy} \xB7 rubric ${res.breakdown.rubric} \xB7 diff ${res.breakdown.diff}${res.breakdown.context ? ` \xB7 context ${res.breakdown.context}` : ""}${res.breakdown.tools ? ` \xB7 tools ${res.breakdown.tools} (${res.breakdown.tool_calls} calls)` : ""} \xB7 out ${res.breakdown.output}) \xB7 mode ${mode}` + (res.retried ? " \xB7 JSON repair retry used" : "")
|
|
30979
31109
|
);
|
|
30980
|
-
if (!llm) notes.push(`LLM triage failed (${res.error}); rules-only.`);
|
|
31110
|
+
if (!llm) notes.push(`LLM triage failed (${res.error}${res.truncated ? `; output cut off at the ${res.outputLimit}-token limit, retry failed` : ""}); rules-only.`);
|
|
30981
31111
|
else if (o.llmCache) {
|
|
30982
31112
|
await o.llmCache.put({ llm, tokens, breakdown: res.breakdown, policiesUsed: policies.used }).catch((e) => log.warning(`Could not cache LLM result: ${e.message}`));
|
|
30983
31113
|
}
|
|
@@ -31232,7 +31362,7 @@ function installAutostart(d, bundle) {
|
|
|
31232
31362
|
}
|
|
31233
31363
|
|
|
31234
31364
|
// src/version.ts
|
|
31235
|
-
var VERSION = "0.2.
|
|
31365
|
+
var VERSION = "0.2.1";
|
|
31236
31366
|
|
|
31237
31367
|
// src/run.ts
|
|
31238
31368
|
var MAX_RESULT_BYTES = 512 * 1024;
|
|
@@ -31321,7 +31451,7 @@ async function runLoop(o) {
|
|
|
31321
31451
|
const caps = runnerCaps(cfg, engEnv, new Set(Object.keys(blocked)));
|
|
31322
31452
|
const engineOptions = (engine) => {
|
|
31323
31453
|
const base = { env: engEnv, killGraceMs: o.claude?.killGraceMs };
|
|
31324
|
-
if (engine === "codex") return { ...base, bin: o.codex?.bin ?? cfg.codexBin ?? void 0, ...o.codex?.killGraceMs !== void 0 ? { killGraceMs: o.codex.killGraceMs } : {} };
|
|
31454
|
+
if (engine === "codex") return { ...base, effort: cfg.engines?.codex?.effort ?? void 0, bin: o.codex?.bin ?? cfg.codexBin ?? void 0, ...o.codex?.killGraceMs !== void 0 ? { killGraceMs: o.codex.killGraceMs } : {} };
|
|
31325
31455
|
return { ...base, bin: o.claude?.bin ?? cfg.claudeBin ?? void 0, ...engine === "glm" ? { apiKey: glmKeyOf(cfg, engEnv) } : {} };
|
|
31326
31456
|
};
|
|
31327
31457
|
let nextAnnounce = 0;
|
|
@@ -31549,8 +31679,8 @@ async function runLoop(o) {
|
|
|
31549
31679
|
var USAGE = `mergejudge-runner ${VERSION}
|
|
31550
31680
|
|
|
31551
31681
|
Usage:
|
|
31552
|
-
mergejudge-runner register --code <enrollment-code> --url <https://app.mergejudge.si> [--name <name>] [--engine claude-code|codex|glm] [--model [engine=]<model>]... [--glm-key-env NAME] [--claude-bin <path>] [--codex-bin <path>] [--concurrency 1] [--force]
|
|
31553
|
-
mergejudge-runner setup [code] [--yes] [--no-autostart] [--print-unit] [--name <name>] [--url <url>] [--engine <name>] [--model [engine=]<model>]... [--glm-key-env NAME | --no-glm] [--force]
|
|
31682
|
+
mergejudge-runner register --code <enrollment-code> --url <https://app.mergejudge.si> [--name <name>] [--engine claude-code|codex|glm] [--model [engine=]<model>]... [--codex-effort minimal|low|medium|high|xhigh] [--glm-key-env NAME] [--claude-bin <path>] [--codex-bin <path>] [--concurrency 1] [--force]
|
|
31683
|
+
mergejudge-runner setup [code] [--yes] [--no-autostart] [--print-unit] [--name <name>] [--url <url>] [--engine <name>] [--model [engine=]<model>]... [--codex-effort minimal|low|medium|high|xhigh] [--glm-key-env NAME | --no-glm] [--force]
|
|
31554
31684
|
mergejudge-runner run [--concurrency 1] [--once] [--claude-bin <path>] [--codex-bin <path>] [--job-timeout-min 10]
|
|
31555
31685
|
mergejudge-runner status
|
|
31556
31686
|
|
|
@@ -31559,8 +31689,8 @@ setup registers this machine (code from the panel, or prompted), detects which o
|
|
|
31559
31689
|
Common: --config <path> (default: $XDG_CONFIG_HOME or ~/.config /mergejudge-runner/config.json)
|
|
31560
31690
|
`;
|
|
31561
31691
|
var COMMAND_USAGE = {
|
|
31562
|
-
register: "mergejudge-runner register --code <enrollment-code> --url <https://app.mergejudge.si> [--name <name>] [--engine claude-code|codex|glm] [--model [engine=]<model>]... [--glm-key-env NAME] [--claude-bin <path>] [--codex-bin <path>] [--concurrency 1] [--force]",
|
|
31563
|
-
setup: "mergejudge-runner setup [code] [--yes] [--no-autostart] [--print-unit] [--name <name>] [--url <url>] [--engine <name>] [--model [engine=]<model>]... [--glm-key-env NAME | --no-glm] [--force]\n\nRegisters this machine (code from the panel, or prompted), detects which of claude and codex are installed and signed in, asks whether to add a GLM (z.ai, billed per use) API key (--glm-key-env NAME reads it from that environment variable; --no-glm skips the question), and offers to start the runner at login. MERGEJUDGE_URL overrides the default server.",
|
|
31692
|
+
register: "mergejudge-runner register --code <enrollment-code> --url <https://app.mergejudge.si> [--name <name>] [--engine claude-code|codex|glm] [--model [engine=]<model>]... [--codex-effort minimal|low|medium|high|xhigh] [--glm-key-env NAME] [--claude-bin <path>] [--codex-bin <path>] [--concurrency 1] [--force]",
|
|
31693
|
+
setup: "mergejudge-runner setup [code] [--yes] [--no-autostart] [--print-unit] [--name <name>] [--url <url>] [--engine <name>] [--model [engine=]<model>]... [--codex-effort minimal|low|medium|high|xhigh] [--glm-key-env NAME | --no-glm] [--force]\n\nRegisters this machine (code from the panel, or prompted), detects which of claude and codex are installed and signed in, asks whether to add a GLM (z.ai, billed per use) API key (--glm-key-env NAME reads it from that environment variable; --no-glm skips the question), and offers to start the runner at login. MERGEJUDGE_URL overrides the default server.",
|
|
31564
31694
|
run: "mergejudge-runner run [--concurrency 1] [--once] [--claude-bin <path>] [--codex-bin <path>] [--job-timeout-min 10]",
|
|
31565
31695
|
status: "mergejudge-runner status"
|
|
31566
31696
|
};
|
|
@@ -31595,6 +31725,7 @@ async function main(argv, deps = {}) {
|
|
|
31595
31725
|
engine: { type: "string" },
|
|
31596
31726
|
"claude-bin": { type: "string" },
|
|
31597
31727
|
"codex-bin": { type: "string" },
|
|
31728
|
+
"codex-effort": { type: "string" },
|
|
31598
31729
|
model: { type: "string", multiple: true },
|
|
31599
31730
|
"glm-key-env": { type: "string" },
|
|
31600
31731
|
"no-glm": { type: "boolean" },
|
|
@@ -31631,6 +31762,12 @@ ${USAGE}`);
|
|
|
31631
31762
|
return e instanceof ConfigError || e.code?.startsWith("ERR_PARSE_ARGS") ? 2 : 1;
|
|
31632
31763
|
}
|
|
31633
31764
|
}
|
|
31765
|
+
function applyCodexEffort(settings, effort) {
|
|
31766
|
+
if (effort === void 0) return;
|
|
31767
|
+
if (!isCodexEffort(effort)) throw new ConfigError(`--codex-effort must be one of ${CODEX_EFFORTS.join(", ")}`);
|
|
31768
|
+
if (!settings.codex) throw new ConfigError("--codex-effort needs the codex engine (codex installed and signed in, or --engine codex)");
|
|
31769
|
+
settings.codex.effort = effort;
|
|
31770
|
+
}
|
|
31634
31771
|
async function register(v, configPath, deps, out) {
|
|
31635
31772
|
const code = v.code, urlRaw = v.url;
|
|
31636
31773
|
if (!code || !urlRaw) throw new ConfigError("register needs --code and --url");
|
|
@@ -31638,6 +31775,7 @@ async function register(v, configPath, deps, out) {
|
|
|
31638
31775
|
const engine = v.engine ?? "claude-code";
|
|
31639
31776
|
const glmApiKey = readGlmKeyEnv(v["glm-key-env"], env);
|
|
31640
31777
|
const settings = buildEngineSettings([engine], pickDefaultEngine(engine, engine === "glm" && !glmApiKey && !env[GLM_KEY_ENV] ? [] : [engine]), v.model ?? []);
|
|
31778
|
+
applyCodexEffort(settings, v["codex-effort"]);
|
|
31641
31779
|
const cfg = await registerRunner({
|
|
31642
31780
|
code,
|
|
31643
31781
|
url: urlRaw,
|
|
@@ -31784,6 +31922,7 @@ async function setup(v, positionals, configPath, deps, out, err) {
|
|
|
31784
31922
|
const requested = v.engine;
|
|
31785
31923
|
const engine = requested && !enabled.includes(requested) && enabled.length === 0 && requested !== "glm" ? requested : pickDefaultEngine(requested, enabled);
|
|
31786
31924
|
const settings = buildEngineSettings(enabled.includes(engine) ? enabled : [...enabled, engine], engine, v.model ?? []);
|
|
31925
|
+
applyCodexEffort(settings, v["codex-effort"]);
|
|
31787
31926
|
cfg = await registerRunner({
|
|
31788
31927
|
code: code2,
|
|
31789
31928
|
url: url2,
|
package/package.json
CHANGED