forgedock 1.24.5 → 1.25.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/bin/engine/outcome-codes.mjs +11 -0
- package/bin/engine/phases.mjs +22 -6
- package/bin/engine/runlog.mjs +1 -1
- package/bin/engine-cli.mjs +206 -22
- package/bin/engine.mjs +3 -1
- package/bin/forgedock.mjs +17 -5
- package/bin/lean/batch.mjs +147 -0
- package/bin/lean/batch.test.mjs +184 -0
- package/bin/lean/lane.mjs +85 -0
- package/bin/lean/lane.test.mjs +91 -0
- package/bin/lean/lean.mjs +122 -9
- package/bin/lean/lean.test.mjs +370 -12
- package/bin/lean/orchestrate.mjs +229 -29
- package/bin/lean/prefilter.mjs +148 -0
- package/bin/lean/prefilter.test.mjs +228 -0
- package/bin/lean/prompts.mjs +80 -3
- package/bin/runner.mjs +25 -3
- package/commands/orchestrate.md +64 -21
- package/commands/review-pr-staging.md +28 -3
- package/docs/CONFIG.md +2 -0
- package/docs/OUTCOME-CODES.md +11 -4
- package/package.json +1 -1
- package/scripts/code-index.sh +61 -40
- package/scripts/code-index.test.sh +73 -0
- package/scripts/review-pr-staging-scope.test.sh +36 -1
- package/scripts/review-prepare.sh +16 -3
- package/scripts/review-prepare.test.sh +16 -0
package/bin/lean/lean.mjs
CHANGED
|
@@ -12,19 +12,48 @@ import { codeForTerminalReason, OUTCOME_CODES } from "../engine/outcome-codes.mj
|
|
|
12
12
|
import { ciPollDelayMs, ENGINE_VERSION, DEFAULT_MAX_SESSION_LIMIT_PAUSES, DEFAULT_SESSION_LIMIT_WAIT_MS, parseLeaseHolder, sanitizeLeaseHost } from "../engine.mjs";
|
|
13
13
|
import { openDependency } from "../engine/phases.mjs";
|
|
14
14
|
import { checkAdmission, ADMISSION_RETRY_SECONDS } from "./admission.mjs";
|
|
15
|
+
import { resolveLane, isDefaultLane } from "./lane.mjs";
|
|
15
16
|
import { retrying, PUSH_TRANSIENT_RE, PUSH_PERMANENT_RE, GH_TRANSIENT_RE, MERGE_TRANSIENT_RE, MERGE_CONFLICT_RE } from "./retry.mjs";
|
|
16
17
|
import { renderInvestigation, renderPlan, renderImplementation, renderReview, renderPrBody, renderTrajectory, capComment, sanitizeMd } from "./render.mjs";
|
|
17
|
-
import { REVIEWER_SYSTEM, LEAN_DIR, RESULT_FILE, INVESTIGATION_FILE, PLAN_FILE, IMPLEMENTATION_FILE, builderPrompt, resumeBuilderPrompt, reviewerPrompt, parseVerdict } from "./prompts.mjs";
|
|
18
|
+
import { REVIEWER_SYSTEM, LEAN_DIR, RESULT_FILE, INVESTIGATION_FILE, PLAN_FILE, IMPLEMENTATION_FILE, builderPrompt, resumeBuilderPrompt, reviewerPrompt, REVIEW_DIFF_MAX_BYTES, parseVerdict, blockCorrectionPrompt, HUMAN_BLOCK_REASONS } from "./prompts.mjs";
|
|
18
19
|
|
|
19
20
|
export const DEFAULT_REVIEWER_MODEL = "claude-sonnet-5-5";
|
|
20
21
|
export const REGISTER_MARKER = "FORGE:WORK-ON"; // alias marker carried by the FORGE:TRAJECTORY comment
|
|
21
22
|
const num = (v) => (Number.isFinite(v) ? v : null);
|
|
23
|
+
export const MAX_SUBISSUES = 8;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Typed builder block (#4124), from result.json `block`. Returns {kind:"dependency", issues} | {kind:"split", subissues}
|
|
27
|
+
* | {kind:"human", why} | {kind:"untyped", problem}. Only `human` with an allowed reason may end as needs-human.
|
|
28
|
+
*/
|
|
29
|
+
export function classifyBlock(result, issue) {
|
|
30
|
+
const b = result?.block;
|
|
31
|
+
if (!b || typeof b !== "object" || Array.isArray(b)) return { kind: "untyped", problem: "it has no typed \"block\" object" };
|
|
32
|
+
if (b.kind === "dependency") {
|
|
33
|
+
const issues = [...new Set((Array.isArray(b.issues) ? b.issues : []).map((x) => Number(String(x).replace(/^#/, ""))))].filter((n) => Number.isSafeInteger(n) && n > 0 && n !== Number(issue));
|
|
34
|
+
return issues.length ? { kind: "dependency", issues } : { kind: "untyped", problem: "the dependency block names no other issue number" };
|
|
35
|
+
}
|
|
36
|
+
if (b.kind === "split-required") {
|
|
37
|
+
const subs = (Array.isArray(b.subissues) ? b.subissues : []).filter((x) => x && typeof x.title === "string" && x.title.trim() && typeof x.body === "string");
|
|
38
|
+
if (!subs.length) return { kind: "untyped", problem: "the split-required block lists no sub-issues with a title and body" };
|
|
39
|
+
if (subs.length > MAX_SUBISSUES) return { kind: "untyped", problem: `the split-required block lists ${subs.length} sub-issues (at most ${MAX_SUBISSUES})` };
|
|
40
|
+
return { kind: "split", subissues: subs.map((x, i) => ({ title: x.title.trim(), body: x.body,
|
|
41
|
+
dependsOn: [...new Set((Array.isArray(x.depends_on) ? x.depends_on : []).filter((j) => Number.isInteger(j) && j >= 0 && j < i))] })) };
|
|
42
|
+
}
|
|
43
|
+
if (b.kind === "human") {
|
|
44
|
+
return HUMAN_BLOCK_REASONS.includes(b.why) ? { kind: "human", why: b.why }
|
|
45
|
+
: { kind: "untyped", problem: `"${String(b.why ?? "").slice(0, 80)}" is not a human-only reason (${HUMAN_BLOCK_REASONS.join(", ")})` };
|
|
46
|
+
}
|
|
47
|
+
return { kind: "untyped", problem: `block kind "${String(b.kind ?? "").slice(0, 40)}" is unknown` };
|
|
48
|
+
}
|
|
22
49
|
|
|
23
50
|
/**
|
|
24
51
|
* @param {object} o
|
|
25
52
|
* @param {number} o.issue
|
|
26
53
|
* @param {string} o.repo "owner/repo"
|
|
27
|
-
* @param {string} o.lane
|
|
54
|
+
* @param {string} [o.lane] requested lane: an assertion checked against the lane resolved from the issue (#4125)
|
|
55
|
+
* @param {boolean} [o.laneOverride] accept a requested lane that differs from the resolved one (never the default branch)
|
|
56
|
+
* @param {object} [o.branches] forge.yaml branches {default, staging, featurePattern} (lane.mjs readBranches)
|
|
28
57
|
* @param {string} o.cwd main checkout (worktrees are added from here)
|
|
29
58
|
* @param {string} o.dir run-log dir
|
|
30
59
|
* @param {object} o.io injected effects: gh, git, runner, waitCiGreen, transitionLabel, fs, projector?, now?, sleep?
|
|
@@ -33,11 +62,11 @@ const num = (v) => (Number.isFinite(v) ? v : null);
|
|
|
33
62
|
* @param {number} [o.ciMaxMinutes] remote-CI wait budget (default 360); exhaustion is NOT needs-human
|
|
34
63
|
*/
|
|
35
64
|
export async function runCore(o) {
|
|
36
|
-
const { issue, repo, lane, cwd, dir, io, builderModel = null, reviewerModel = DEFAULT_REVIEWER_MODEL,
|
|
65
|
+
const { issue, repo, lane: requestedLane = null, laneOverride = false, branches = {}, cwd, dir, io, builderModel = null, reviewerModel = DEFAULT_REVIEWER_MODEL,
|
|
37
66
|
ciMaxMinutes = 360, mergeWait = "required", maxSessionLimitPauses = DEFAULT_MAX_SESSION_LIMIT_PAUSES, cliIdleMinutes = 15, cliMaxMinutes = 180, laneLimits = null, maxMergeRetries = 5, sessionLimitMaxWaitMs = DEFAULT_SESSION_LIMIT_WAIT_MS, onProgress = () => {}, emitTelemetry = null, random = Math.random } = o;
|
|
38
67
|
const now = io.now ?? (() => Date.now());
|
|
39
68
|
const sleep = io.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
40
|
-
|
|
69
|
+
let lane = null, base = null, defaults = []; // set by lane resolution, before the lease
|
|
41
70
|
const branch = `feat/issue-${issue}-lean`;
|
|
42
71
|
const wt = join(o.worktreeRoot ?? join(cwd, ".claude", "worktrees"), `lean-${issue}`);
|
|
43
72
|
const t0 = now();
|
|
@@ -101,7 +130,7 @@ export async function runCore(o) {
|
|
|
101
130
|
const code = codeOverride ?? codeForTerminalReason(reason);
|
|
102
131
|
ev({ event: "RUN_TERMINAL", reason, code });
|
|
103
132
|
await emit({ event: "RUN_TERMINAL", reason, code });
|
|
104
|
-
if (["needs-human", "engine-error"].includes(reason)) {
|
|
133
|
+
if (["needs-human", "engine-error", "dependency-open", "dependency-blocked", "dependency-unreadable", "decomposed"].includes(reason)) {
|
|
105
134
|
const wf = ["investigating", "building", "in-review"].map((x) => `workflow:${x}`).join(",");
|
|
106
135
|
try { await ghc(["issue", "edit", String(issue), "-R", repo, "--remove-label", wf]); } catch { /* best effort */ }
|
|
107
136
|
}
|
|
@@ -285,11 +314,75 @@ export async function runCore(o) {
|
|
|
285
314
|
}
|
|
286
315
|
const readResult = async () => { try { const j = JSON.parse(await io.fs.read(`${wt}/${RESULT_FILE}`)); return j && typeof j === "object" ? j : null; } catch { return null; } };
|
|
287
316
|
|
|
317
|
+
/** Live-checks a builder block (#4124). Dependency issues are re-read, so a closed or unknown number is not a block. */
|
|
318
|
+
async function resolveBlock(r) {
|
|
319
|
+
const b = classifyBlock(r, issue);
|
|
320
|
+
if (b.kind !== "dependency") return b;
|
|
321
|
+
const open = [], blockers = [];
|
|
322
|
+
for (const d of b.issues) {
|
|
323
|
+
let v;
|
|
324
|
+
try { v = JSON.parse(String(await ghc(["issue", "view", String(d), "-R", repo, "--json", "state,labels"]))); } catch (e) {
|
|
325
|
+
return { kind: "unreadable", detail: `dependency #${d} named by the builder could not be read: ${String(e?.message ?? e).slice(0, 200)}` };
|
|
326
|
+
}
|
|
327
|
+
if (String(v?.state).toUpperCase() !== "OPEN") continue;
|
|
328
|
+
open.push(d);
|
|
329
|
+
if ((v.labels ?? []).some((l) => (l?.name ?? l) === "needs-human")) blockers.push(d);
|
|
330
|
+
}
|
|
331
|
+
if (!open.length) return { kind: "untyped", problem: `its dependencies ${b.issues.map((n) => `#${n}`).join(", ")} are not open` };
|
|
332
|
+
return { kind: "dependency", issues: open, blockers };
|
|
333
|
+
}
|
|
334
|
+
/** Ends a run whose builder stayed blocked: only a typed human reason is needs-human. */
|
|
335
|
+
async function endBlocked(b, summary) {
|
|
336
|
+
const s = summary ?? "(no summary)";
|
|
337
|
+
const refs = (ns) => ns.map((n) => `#${n}`).join(", ");
|
|
338
|
+
if (b.kind === "human") return await needsHuman(`Builder is blocked (${b.why}): ${s}`, OUTCOME_CODES.BUILDER_BLOCKED);
|
|
339
|
+
if (b.kind === "unreadable") return await finish("dependency-unreadable", b.detail);
|
|
340
|
+
if (b.kind === "dependency") {
|
|
341
|
+
const blocked = b.blockers.length > 0;
|
|
342
|
+
const detail = blocked
|
|
343
|
+
? `blocked by ${refs(b.blockers)} (needs-human); depends on ${refs(b.issues)}. ${s}`
|
|
344
|
+
: `depends on ${refs(b.issues)} which is still open; requeue behind it. ${s}`;
|
|
345
|
+
T.decisions.push(`Builder block: dependency on ${refs(b.issues)}${blocked ? ` (needs-human: ${refs(b.blockers)})` : ""}`);
|
|
346
|
+
try { await post("FORGE:DEPENDENCY", `## ${blocked ? "Blocked by" : "Waiting on"} ${refs(blocked ? b.blockers : b.issues)}\n${detail}\n\n_${usageLine()}_`); } catch { /* best effort */ }
|
|
347
|
+
return blocked
|
|
348
|
+
? await finish("dependency-blocked", detail, { code: OUTCOME_CODES.DEPENDENCY_BLOCKED, dependencies: b.issues, blockers: b.blockers }, OUTCOME_CODES.DEPENDENCY_BLOCKED)
|
|
349
|
+
: await finish("dependency-open", detail, { code: OUTCOME_CODES.DEPENDENCY_OPEN, dependencies: b.issues }, OUTCOME_CODES.DEPENDENCY_OPEN);
|
|
350
|
+
}
|
|
351
|
+
if (b.kind === "split") {
|
|
352
|
+
const filed = [];
|
|
353
|
+
try {
|
|
354
|
+
for (const sub of b.subissues) {
|
|
355
|
+
const lines = [`**Parent**: #${issue}`, ...sub.dependsOn.map((j) => `**Depends on**: #${filed[j]}`)];
|
|
356
|
+
const out = String(await ghw(["issue", "create", "-R", repo, "--title", sub.title, "--body", `${sub.body.trim()}\n\n${lines.join("\n")}`]));
|
|
357
|
+
const n = Number(/\/issues\/(\d+)/.exec(out)?.[1]);
|
|
358
|
+
if (!Number.isSafeInteger(n)) throw new Error(`cannot parse the sub-issue number from: ${out.slice(0, 200)}`);
|
|
359
|
+
filed.push(n);
|
|
360
|
+
}
|
|
361
|
+
} catch (e) { return await finish("engine-error", `filing sub-issues failed after ${refs(filed) || "none"}: ${e?.message ?? e}`, { subissues: filed }); }
|
|
362
|
+
T.decisions.push(`Builder block: split into ${refs(filed)}`);
|
|
363
|
+
try { await post("FORGE:DECOMPOSED", `## Split into sub-issues\n${filed.map((n, i) => `- #${n} ${b.subissues[i].title}`).join("\n")}\n\n${s}`); } catch { /* best effort */ }
|
|
364
|
+
await label("decomposed");
|
|
365
|
+
return await finish("decomposed", `split into ${refs(filed)}`, { code: OUTCOME_CODES.DECOMPOSED, subissues: filed });
|
|
366
|
+
}
|
|
367
|
+
return await needsHuman(`Builder is blocked without a valid typed reason after one correction (${b.problem}): ${s}`, OUTCOME_CODES.BUILDER_BLOCK_UNTYPED);
|
|
368
|
+
}
|
|
369
|
+
|
|
288
370
|
// ---- host admission (#4105): before the lease, the worktree or any GitHub call ----
|
|
289
371
|
if (io.host && o.admission?.enabled !== false) {
|
|
290
372
|
const a = await checkAdmission({ cfg: o.admission, host: io.host });
|
|
291
373
|
if (!a.ok) return { terminalReason: "deferred", code: OUTCOME_CODES.HOST_BUSY, detail: a.detail, retryInSeconds: ADMISSION_RETRY_SECONDS };
|
|
292
374
|
}
|
|
375
|
+
// ---- lane (#4125): worked out from the issue before any lease, comment or worktree; fail closed ----
|
|
376
|
+
{
|
|
377
|
+
const r = await resolveLane({ issue, repo, io, cwd, branches, requested: requestedLane, override: laneOverride });
|
|
378
|
+
if (!r.ok) return { terminalReason: "lane-refused", code: r.code, detail: r.detail };
|
|
379
|
+
lane = r.lane; base = `origin/${lane}`; defaults = r.defaults;
|
|
380
|
+
if (r.overridden) T.decisions.push(`Lane override: ${lane} instead of the resolved ${r.expected}`);
|
|
381
|
+
}
|
|
382
|
+
/** Last guard before a PR is created or merged: the default branch is never a lane. */
|
|
383
|
+
const laneGuard = (target, what) => {
|
|
384
|
+
if (isDefaultLane(target, defaults)) throw Object.assign(new Error(`refusing to ${what} into the default branch ${target}`), { laneCode: OUTCOME_CODES.LANE_DEFAULT_BRANCH });
|
|
385
|
+
};
|
|
293
386
|
// ---- admission: Depends-on refusal, then lease ----
|
|
294
387
|
{
|
|
295
388
|
let dep = null;
|
|
@@ -335,10 +428,15 @@ export async function runCore(o) {
|
|
|
335
428
|
try { await io.git(["fetch", "origin", lane], { cwd }); } catch { /* offline: use the local ref */ }
|
|
336
429
|
let resumed = null;
|
|
337
430
|
try {
|
|
338
|
-
const prs = JSON.parse(String(await ghc(["pr", "list", "-R", repo, "--head", branch, "--state", "open", "--json", "number,headRefOid"])) || "[]");
|
|
431
|
+
const prs = JSON.parse(String(await ghc(["pr", "list", "-R", repo, "--head", branch, "--state", "open", "--json", "number,headRefOid,baseRefName"])) || "[]");
|
|
339
432
|
const remote = String(await io.git(["ls-remote", "--heads", "origin", branch], { cwd })).trim();
|
|
340
|
-
if (prs.length || remote) resumed = { pr: prs[0]?.number ?? null };
|
|
433
|
+
if (prs.length || remote) resumed = { pr: prs[0]?.number ?? null, base: prs[0]?.baseRefName ?? null };
|
|
341
434
|
} catch { /* no resume info: start fresh */ }
|
|
435
|
+
// An open PR on another base (a hand-retargeted PR, or one opened before lane resolution) is never merged (#4125).
|
|
436
|
+
if (resumed?.base && resumed.base !== lane) {
|
|
437
|
+
return await needsHuman(`Open PR #${resumed.pr} targets \`${resumed.base}\`, but #${issue}'s lane is \`${lane}\`; retarget or close it before rerunning.`,
|
|
438
|
+
isDefaultLane(resumed.base, defaults) ? OUTCOME_CODES.LANE_DEFAULT_BRANCH : OUTCOME_CODES.LANE_MISMATCH);
|
|
439
|
+
}
|
|
342
440
|
try { await io.git(["worktree", "remove", "--force", wt], { cwd }); } catch { /* none */ }
|
|
343
441
|
try { await io.git(["worktree", "prune"], { cwd }); } catch { /* best effort */ }
|
|
344
442
|
// Concurrent lanes in one repo race on .git/config.lock / index.lock during `worktree add`; retry with jitter.
|
|
@@ -400,6 +498,13 @@ export async function runCore(o) {
|
|
|
400
498
|
await label("building");
|
|
401
499
|
await builder(++attemptN, builderPrompt(issue) + (fromCheckpoint ? CONTINUE_NOTE.replace("BASE", base) : ""), null, true);
|
|
402
500
|
result = await readResult();
|
|
501
|
+
let block = result?.status === "blocked" ? await resolveBlock(result) : null;
|
|
502
|
+
if (block?.kind === "untyped" && sessionId) { // #4124: one corrective resume before any untyped block can reach needs-human
|
|
503
|
+
T.decisions.push(`Builder block rejected (${block.problem}); resumed once`);
|
|
504
|
+
await builder(++attemptN, blockCorrectionPrompt(block.problem), sessionId);
|
|
505
|
+
result = await readResult();
|
|
506
|
+
block = result?.status === "blocked" ? await resolveBlock(result) : null;
|
|
507
|
+
}
|
|
403
508
|
const files0 = await changedFiles();
|
|
404
509
|
const evidence = (Array.isArray(result?.evidence) ? result.evidence : []).filter((x) => typeof x === "string" && x.trim());
|
|
405
510
|
const closing = ["invalid", "already-fixed"].includes(result?.status);
|
|
@@ -413,7 +518,7 @@ export async function runCore(o) {
|
|
|
413
518
|
await label("invalid");
|
|
414
519
|
return await finish("invalid", result.summary ?? result.status);
|
|
415
520
|
}
|
|
416
|
-
if (
|
|
521
|
+
if (block) return await endBlocked(block, result.summary);
|
|
417
522
|
if (!files0.length) return await needsHuman(`Builder made no commit${result ? ` (status "${result.status}")` : " and wrote no result.json"}.\n\n${result?.summary ?? ""}`, OUTCOME_CODES.BUILDER_NO_COMMIT);
|
|
418
523
|
await flushStages(result, ["plan", "implementation"]);
|
|
419
524
|
T.plan = "posted"; T.implement = [`${files0.length} file(s)`, branch];
|
|
@@ -428,9 +533,11 @@ export async function runCore(o) {
|
|
|
428
533
|
const doReview = async () => {
|
|
429
534
|
for (let t = 0; t < 2; t++) {
|
|
430
535
|
const diff = String(await git(["diff", `${base}...HEAD`]));
|
|
536
|
+
// #4129: an over-budget diff is bounded in the prompt; the stat keeps every changed file visible.
|
|
537
|
+
const stat = Buffer.byteLength(diff, "utf-8") > REVIEW_DIFF_MAX_BYTES ? String(await git(["diff", "--stat=200", `${base}...HEAD`]).catch(() => "")) : "";
|
|
431
538
|
await renew();
|
|
432
539
|
const rr = await callRunner({ role: "reviewer", issue, cwd: wt, model: reviewerModel, resume: null, system: REVIEWER_SYSTEM, readOnly: true,
|
|
433
|
-
prompt: reviewerPrompt({ issue: iss, diff }) });
|
|
540
|
+
prompt: reviewerPrompt({ issue: iss, diff, stat, base }) });
|
|
434
541
|
await renew();
|
|
435
542
|
await attempt("review", ++reviewN, rr);
|
|
436
543
|
const v = parseVerdict(rr?.text);
|
|
@@ -456,6 +563,7 @@ export async function runCore(o) {
|
|
|
456
563
|
// ---- 5. push + PR ----
|
|
457
564
|
const files = await changedFiles();
|
|
458
565
|
await gitPush(["push", "-u", "origin", branch]);
|
|
566
|
+
laneGuard(lane, "open a PR");
|
|
459
567
|
const prOut = resumed?.pr ? `/pull/${resumed.pr}` : String(await ghw(["pr", "create", "-R", repo, "--base", lane, "--head", branch,
|
|
460
568
|
"--title", `${iss.title} (#${issue})`, "--body", renderPrBody({ issue, investigation: await readStage(STAGES[0]), plan: await readStage(STAGES[1]), implementation: await readStage(STAGES[2]), result, gitFiles: files })])).trim();
|
|
461
569
|
const pr = Number(/\/pull\/(\d+)/.exec(prOut)?.[1] ?? /(\d+)\s*$/.exec(prOut)?.[1]);
|
|
@@ -583,6 +691,7 @@ export async function runCore(o) {
|
|
|
583
691
|
}
|
|
584
692
|
if (syncs > 0 && (await prConflicting()) === "conflict") continue; // the base moved again while waiting
|
|
585
693
|
T.merge = [policyText, `merged at ${localHead.slice(0, 12)}`];
|
|
694
|
+
laneGuard(resumed?.base ?? lane, "merge");
|
|
586
695
|
try {
|
|
587
696
|
await ghc(["pr", "merge", String(pr), "-R", repo, "--merge", "--match-head-commit", localHead]);
|
|
588
697
|
break;
|
|
@@ -608,6 +717,10 @@ export async function runCore(o) {
|
|
|
608
717
|
return finish("deferred", `usage limit hit; resets at ${when}`, { code: OUTCOME_CODES.RATE_LIMITED, resetAt: when, resetAtEpochMs: Number.isFinite(e.resetAtEpochMs) ? e.resetAtEpochMs : null }, OUTCOME_CODES.RATE_LIMITED);
|
|
609
718
|
}
|
|
610
719
|
if (e?.deferCode) return finish("deferred", e.deferDetail ?? String(e.message), { code: e.deferCode }, e.deferCode);
|
|
720
|
+
if (e?.laneCode) return await needsHuman(e.message, e.laneCode);
|
|
721
|
+
if (e?.code === "CLI_SPAWN_FAILED" || e?.spawnDeterministic === true) { // #4129: certain to fail again, so not engine-error (orchestrate would relaunch it)
|
|
722
|
+
return await needsHuman(`The claude CLI could not be started (${e.spawnErrno ?? "spawn error"}); a rerun fails the same way: ${String(e.message).slice(0, 400)}`, OUTCOME_CODES.CLI_SPAWN_FAILED);
|
|
723
|
+
}
|
|
611
724
|
if (e?.pushRejected) return await needsHuman(`git push was rejected (no retry): ${String(e?.stderr ?? e?.message ?? e).slice(0, 400)}`);
|
|
612
725
|
return finish("engine-error", e?.message ?? String(e));
|
|
613
726
|
}
|