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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.js +204 -65
  3. 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.budget.max_output_tokens - estimateTokens(system) - 100 - estimateTokens(ctxText)
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
- let json2 = t.slice(a, b + 1);
30495
+ const json2 = t.slice(a, b + 1);
30462
30496
  try {
30463
30497
  return triageOutputSchema.parse(JSON.parse(json2));
30464
- } catch (e) {
30465
- json2 = json2.replace(/,\s*([}\]])/g, "$1");
30466
- return triageOutputSchema.parse(JSON.parse(json2));
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) retried = true;
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
- raw = await provider.complete({
30640
+ const r = await completeWithMeta(provider, {
30540
30641
  system,
30541
30642
  user: repair ? `${prompt}
30542
30643
 
30543
- REPAIR: Your previous reply was not valid JSON (${lastErr}). Reply with ONLY a single JSON object matching the schema. No markdown fences, no prose.` : prompt,
30544
- maxOutputTokens: cfg.budget.max_output_tokens,
30644
+ ${retryInstruction(firstTruncated, lastErr)}` : prompt,
30645
+ maxOutputTokens: repair ? retryLimit : limit,
30545
30646
  jsonMode: true
30546
30647
  });
30547
- const llm = validateAgainstFiles(parseTriageOutput(raw), input2.files, ctx.criteria ?? []);
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) log("LLM JSON parse succeeded on repair attempt.");
30550
- return { llm, tokensEstimate: estimateTokens(system) + diffTokens + outTok, breakdown: mkBreakdown(outTok), retried };
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 turn = async (tools, jsonMode) => {
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: cfg.budget.max_output_tokens, jsonMode });
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 + cfg.budget.max_output_tokens + headroom > cfg.deep.max_tokens) stopped = "tokens";
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: `REPAIR: Your previous reply was not valid JSON (${why}). Reply with ONLY a single JSON object matching the schema. No markdown fences, no prose.` });
30737
- raw = (await turn(void 0, true)).text;
30738
- return result(validateAgainstFiles(parseTriageOutput(raw), input2.files, ctx.criteria ?? []));
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.0";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mergejudge-runner",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Run MergeJudge reviews on your own machine with your own Claude Code login (runner protocol /runner/v1)",