forgedock 1.30.0 → 1.32.0

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 (162) hide show
  1. package/bin/engine/outcome-codes.mjs +3 -2
  2. package/bin/engine/runlog.mjs +82 -12
  3. package/bin/engine/telemetry-seam.mjs +11 -4
  4. package/bin/engine/telemetry-seam.test.mjs +18 -0
  5. package/bin/engine-cli.mjs +409 -38
  6. package/bin/engine.mjs +47 -37
  7. package/bin/forgedock.mjs +35 -3
  8. package/bin/gc.mjs +50 -17
  9. package/bin/hooks/reread-nudge.mjs +65 -0
  10. package/bin/labels.json +5 -0
  11. package/bin/lean/apishape.mjs +170 -0
  12. package/bin/lean/apishape.test.mjs +187 -0
  13. package/bin/lean/batch.mjs +32 -0
  14. package/bin/lean/batch.test.mjs +338 -2
  15. package/bin/lean/budget.mjs +89 -0
  16. package/bin/lean/budget.test.mjs +106 -0
  17. package/bin/lean/checks.mjs +32 -9
  18. package/bin/lean/checks.test.mjs +78 -1
  19. package/bin/lean/conformance.e2e.test.mjs +27 -5
  20. package/bin/lean/conformance.mjs +150 -15
  21. package/bin/lean/conformance.test.mjs +175 -3
  22. package/bin/lean/context.mjs +128 -44
  23. package/bin/lean/context.test.mjs +157 -5
  24. package/bin/lean/decompose.mjs +90 -0
  25. package/bin/lean/decompose.test.mjs +105 -0
  26. package/bin/lean/depcache.mjs +11 -7
  27. package/bin/lean/depcache.test.mjs +81 -0
  28. package/bin/lean/e2e.mjs +102 -0
  29. package/bin/lean/e2e.test.mjs +86 -0
  30. package/bin/lean/effects.mjs +48 -15
  31. package/bin/lean/explore.mjs +47 -0
  32. package/bin/lean/explore.test.mjs +61 -0
  33. package/bin/lean/facts.mjs +84 -0
  34. package/bin/lean/facts.test.mjs +66 -0
  35. package/bin/lean/fixround.mjs +90 -0
  36. package/bin/lean/fixround.test.mjs +117 -0
  37. package/bin/lean/heavy.mjs +35 -2
  38. package/bin/lean/heavy.test.mjs +32 -2
  39. package/bin/lean/lean.mjs +712 -134
  40. package/bin/lean/lean.test.mjs +1626 -37
  41. package/bin/lean/ledger.mjs +5 -3
  42. package/bin/lean/ledger.test.mjs +18 -0
  43. package/bin/lean/light.mjs +135 -0
  44. package/bin/lean/light.test.mjs +173 -0
  45. package/bin/lean/lightlane.mjs +219 -0
  46. package/bin/lean/lightlane.test.mjs +237 -0
  47. package/bin/lean/limits.test.mjs +36 -1
  48. package/bin/lean/orchestrate.mjs +303 -41
  49. package/bin/lean/pitfalls.mjs +136 -0
  50. package/bin/lean/pitfalls.test.mjs +117 -0
  51. package/bin/lean/postmerge.mjs +85 -0
  52. package/bin/lean/postmerge.test.mjs +79 -0
  53. package/bin/lean/precheck.mjs +138 -13
  54. package/bin/lean/precheck.test.mjs +118 -1
  55. package/bin/lean/prefilter.mjs +220 -5
  56. package/bin/lean/prefilter.test.mjs +260 -4
  57. package/bin/lean/preflight.mjs +108 -3
  58. package/bin/lean/preflight.test.mjs +48 -1
  59. package/bin/lean/probe.mjs +297 -0
  60. package/bin/lean/probe.test.mjs +210 -0
  61. package/bin/lean/prompts.mjs +104 -13
  62. package/bin/lean/render.mjs +7 -3
  63. package/bin/lean/resume.mjs +67 -0
  64. package/bin/lean/resume.test.mjs +84 -0
  65. package/bin/lean/review-trigger.mjs +12 -0
  66. package/bin/lean/review-trigger.test.mjs +17 -1
  67. package/bin/lean/routing.mjs +74 -0
  68. package/bin/lean/routing.test.mjs +67 -0
  69. package/bin/lean/sched.mjs +16 -1
  70. package/bin/lean/sched.test.mjs +175 -1
  71. package/bin/lean/schedd.mjs +55 -9
  72. package/bin/lean/schedd.test.mjs +170 -3
  73. package/bin/lean/selfreview.mjs +37 -0
  74. package/bin/lean/selfreview.test.mjs +51 -0
  75. package/bin/lean/seqlease.mjs +40 -0
  76. package/bin/lean/seqlease.test.mjs +101 -0
  77. package/bin/lean/sequence.mjs +296 -13
  78. package/bin/lean/sequence.test.mjs +263 -2
  79. package/bin/lean/toolchain.mjs +60 -15
  80. package/bin/lean/toolchain.test.mjs +120 -3
  81. package/bin/lean/toolstats.mjs +72 -0
  82. package/bin/lean/toolstats.test.mjs +47 -0
  83. package/bin/lean/verifier.mjs +232 -0
  84. package/bin/lean/verifier.test.mjs +163 -0
  85. package/bin/lean/verify.mjs +72 -10
  86. package/bin/lean/verify.test.mjs +68 -3
  87. package/bin/review-staging.mjs +533 -0
  88. package/bin/runner.mjs +82 -13
  89. package/bin/stats/aggregate.mjs +87 -2
  90. package/bin/stats/efficiency.mjs +335 -0
  91. package/bin/stats/efficiency.test.mjs +266 -0
  92. package/bin/stats/github-source.mjs +8 -3
  93. package/bin/stats/index.mjs +91 -6
  94. package/bin/stats/local-source.mjs +145 -8
  95. package/bin/stats/query.mjs +181 -3
  96. package/bin/stats/render.mjs +22 -0
  97. package/bin/stats/stats.test.mjs +403 -8
  98. package/commands/orchestrate.md +3 -1
  99. package/commands/review-pr-agents/protocols.md +1 -1
  100. package/commands/review-pr-staging.md +84 -831
  101. package/commands/review-pr.md +1 -1
  102. package/commands/work-on/build/implement.md +1 -0
  103. package/commands/work-on/build/validate.md +11 -1
  104. package/docs/CONFIG.md +80 -5
  105. package/docs/OUTCOME-CODES.md +2 -1
  106. package/docs/STATS.md +66 -1
  107. package/docs/TELEMETRY.md +1 -1
  108. package/docs/eval/corpus/lightlane-v1.json +55 -0
  109. package/docs/eval/lightlane-bench.md +91 -0
  110. package/docs/spec/review-protocol.md +1 -1
  111. package/package.json +1 -1
  112. package/scripts/bench-lightlane.mjs +286 -0
  113. package/scripts/ci-local/lib.mjs +20 -0
  114. package/scripts/ci-local/plan.mjs +2 -12
  115. package/scripts/ci-local/scoping.mjs +10 -1
  116. package/scripts/ci-local.test.sh +2 -0
  117. package/scripts/fixtures/apishape/other/go-change/base/x.go +3 -0
  118. package/scripts/fixtures/apishape/other/go-change/head/x.go +3 -0
  119. package/scripts/fixtures/apishape/python/generator-function/base/seq.py +3 -0
  120. package/scripts/fixtures/apishape/python/generator-function/head/seq.py +2 -0
  121. package/scripts/fixtures/apishape/python/ichunked/base/more.py +17 -0
  122. package/scripts/fixtures/apishape/python/ichunked/head/more.py +20 -0
  123. package/scripts/fixtures/apishape/python/private-edit/base/calc.py +6 -0
  124. package/scripts/fixtures/apishape/python/private-edit/head/calc.py +9 -0
  125. package/scripts/fixtures/apishape/python/removed/base/ops.py +6 -0
  126. package/scripts/fixtures/apishape/python/removed/head/ops.py +2 -0
  127. package/scripts/fixtures/apishape/python/renamed/base/text.py +2 -0
  128. package/scripts/fixtures/apishape/python/renamed/head/text.py +2 -0
  129. package/scripts/fixtures/apishape/python/signature/base/shapes.py +2 -0
  130. package/scripts/fixtures/apishape/python/signature/head/shapes.py +2 -0
  131. package/scripts/fixtures/apishape/python/sync-async/base/client.py +7 -0
  132. package/scripts/fixtures/apishape/python/sync-async/head/client.py +7 -0
  133. package/scripts/fixtures/apishape/ts/generator-function/base/seq.js +3 -0
  134. package/scripts/fixtures/apishape/ts/generator-function/head/seq.js +5 -0
  135. package/scripts/fixtures/apishape/ts/private-edit/base/calc.ts +7 -0
  136. package/scripts/fixtures/apishape/ts/private-edit/head/calc.ts +9 -0
  137. package/scripts/fixtures/apishape/ts/removed/base/ops.ts +7 -0
  138. package/scripts/fixtures/apishape/ts/removed/head/ops.ts +3 -0
  139. package/scripts/fixtures/apishape/ts/renamed/base/text.ts +3 -0
  140. package/scripts/fixtures/apishape/ts/renamed/head/text.ts +3 -0
  141. package/scripts/fixtures/apishape/ts/signature/base/shapes.ts +3 -0
  142. package/scripts/fixtures/apishape/ts/signature/head/shapes.ts +3 -0
  143. package/scripts/fixtures/apishape/ts/sync-async/base/client.mjs +5 -0
  144. package/scripts/fixtures/apishape/ts/sync-async/head/client.mjs +5 -0
  145. package/scripts/forge-root.test.sh +2 -2
  146. package/scripts/publish-staging-sync.test.sh +62 -4
  147. package/scripts/review-finalize.sh +17 -6
  148. package/scripts/review-finalize.test.sh +47 -0
  149. package/scripts/review-pr-arguments.test.sh +13 -1
  150. package/scripts/review-pr-delta-wiring.test.sh +2 -2
  151. package/scripts/review-pr-gate2.test.sh +6 -14
  152. package/scripts/review-pr-staging-finding-filter.test.sh +4 -4
  153. package/scripts/review-pr-staging-scope.test.sh +39 -33
  154. package/scripts/review-prepare.sh +73 -6
  155. package/scripts/review-prepare.test.sh +85 -0
  156. package/scripts/review-staging-post.sh +293 -0
  157. package/scripts/review-staging-post.test.sh +143 -0
  158. package/scripts/review-staging-pre.sh +401 -0
  159. package/scripts/review-staging-pre.test.sh +150 -0
  160. package/scripts/symbol-index.mjs +26 -7
  161. package/scripts/symbol-index.py +55 -9
  162. package/scripts/trusted-comments.test.sh +1 -1
@@ -4,16 +4,21 @@ import { describe, it } from "node:test";
4
4
  import { laneBackendId } from "./limits.mjs";
5
5
  import assert from "node:assert/strict";
6
6
  import { tmpDir } from "../tests/_tmp.mjs";
7
- import { writeFileSync, existsSync, readFileSync, mkdirSync, symlinkSync } from "node:fs";
7
+ import { writeFileSync, existsSync, readFileSync, mkdirSync, symlinkSync, readdirSync } from "node:fs";
8
8
  import { join } from "node:path";
9
- import { runCore, contractHash, isProtectedLaneBranch } from "./lean.mjs";
9
+ import { runCore, contractHash, isProtectedLaneBranch, classifyBlock } from "./lean.mjs";
10
+ import { parseFactsComment, normaliseFacts, renderFactsRecord, FACTS_MARKER } from "./facts.mjs";
10
11
  import { makeLedger } from "./ledger.mjs";
12
+ import { parseVerify } from "./verify.mjs";
13
+ import { worktreeFingerprint } from "./precheck.mjs";
11
14
  import { renderInvestigation, renderPlan, renderImplementation, renderReview, renderPrBody, renderTrajectory, renderStructure, renderContract, contractJson, parseContractComment, sanitizeMd, capComment, section, cell, NR, MAX_COMMENT_CHARS } from "./render.mjs";
12
15
  import { orchestrate, parseDeps, formatTable, pickNext } from "./orchestrate.mjs";
13
- import { REVIEWER_SYSTEM, approxTokens, builderPlanPrompt, builderImplementPrompt, reviewerPrompt, hotspotPlanNote, hotspotBuildRule } from "./prompts.mjs";
16
+ import { REVIEWER_SYSTEM, REVIEW_RUBRIC, approxTokens, builderPlanPrompt, builderImplementPrompt, reviewerPrompt, hotspotPlanNote, hotspotBuildRule } from "./prompts.mjs";
14
17
  import { parseReview, unnamedEdges, structureNote, applyAmendments, validateContract, scopeText, DECISION_SOURCES } from "./conformance.mjs";
15
18
  import { mapRecord } from "../engine/telemetry-seam.mjs";
16
- import { readLog } from "../engine/runlog.mjs";
19
+ import { readLog as readRepoLog } from "../engine/runlog.mjs";
20
+ // #4432: the run log is keyed by repo + issue; every lane in this file runs in repo o/r.
21
+ const readLog = (dir, issue, repo = "o/r") => readRepoLog(dir, issue, { repo });
17
22
  import { makeRunTmp, wrapLaneLimits, systemdRunAvailable, makeLeanRunner, requiredChecksFor, procsInDir, makeLeanIo } from "./effects.mjs";
18
23
  import { retrying, PUSH_TRANSIENT_RE } from "./retry.mjs";
19
24
  import { checkAdmission, hostParallelism } from "./admission.mjs";
@@ -59,7 +64,7 @@ function world(o = {}) {
59
64
  contract: o.contract ?? CONTRACT, issueComments: o.issueComments, issueStates: o.issueStates,
60
65
  changed: o.changed ?? ["src/foo.js", "test/foo.test.js"], reviews: [...(o.reviews ?? [])], waits: [...(o.waits ?? [])], clock: 0,
61
66
  result: o.result ?? { status: "fixed", summary: "fixed foo", evidence: [], tests: ["test/foo.test.js"] },
62
- prList: o.prList ?? [], remote: o.remote ?? "", resumeResult: o.resumeResult, seq: [], mtimes: {}, envs: [], pushes: 0, pushErrors: [...(o.pushErrors ?? [])], commentErrors: [...(o.commentErrors ?? [])], dirty: o.dirty ?? false, lastSubject: o.lastSubject ?? "", prView: [...(o.prView ?? [])], mergeResults: [...(o.mergeResults ?? [])], mergeErrors: [...(o.mergeErrors ?? [])], conflictFiles: [], mergeInProgress: false, markers: false, mergeAttempts: 0, branchDeletes: [],
67
+ prList: o.prList ?? [], remote: o.remote ?? "", resumeResult: o.resumeResult, seq: [], mtimes: {}, envs: [], pushes: 0, pushErrors: [...(o.pushErrors ?? [])], commentErrors: [...(o.commentErrors ?? [])], dirty: o.dirty ?? false, lastSubject: o.lastSubject ?? "", prView: [...(o.prView ?? [])], mergeResults: [...(o.mergeResults ?? [])], mergeErrors: [...(o.mergeErrors ?? [])], conflictFiles: [], baseHeads: [...(o.baseHeads ?? ["ba5e000000000000"])], mergeInProgress: false, markers: false, mergeAttempts: 0, branchDeletes: [],
63
68
  stages: { ...STAGE_MD, ...(o.stages ?? {}) }, builderN: 0,
64
69
  salvageTrees: [...(o.salvageTrees ?? [])], alive: new Set((o.procs ?? []).map((p) => p.pid)), kills: [],
65
70
  };
@@ -75,8 +80,8 @@ function world(o = {}) {
75
80
  if (k === "pr list") return JSON.stringify(w.prList);
76
81
  if (k === "repo view") return `${o.defaultBranch ?? "main"}\n`;
77
82
  if (k === "issue view" && a.includes("milestone")) { w.seq.push("lane"); return JSON.stringify({ milestone: o.milestone ? { title: o.milestone } : null }); }
78
- if (k === "issue view" && o.issueBodies && a[2] === "7" && a.some((x) => String(x).startsWith("number,title,body,state,comments"))) { const b = o.issueBodies.length > 1 ? o.issueBodies.shift() : o.issueBodies[0]; return JSON.stringify({ number: 7, title: "foo breaks", body: b, state: "OPEN", comments: [] }); }
79
- if (k === "issue view") return JSON.stringify({ number: Number(a[2]), title: "foo breaks", body: ISSUE_BODY, state: w.issueStates?.[a[2]] ?? "OPEN", comments: [], ...(o.labels && a[2] === "7" ? { labels: o.labels } : {}) });
83
+ if (k === "issue view" && o.issueBodies && a[2] === "7" && a.some((x) => String(x).startsWith("number,title,body,state,comments"))) { const b = o.issueBodies.length > 1 ? o.issueBodies.shift() : o.issueBodies[0]; return JSON.stringify({ number: 7, title: o.title ?? "foo breaks", body: b, state: "OPEN", comments: o.viewComments ?? [] }); }
84
+ if (k === "issue view") return JSON.stringify({ number: Number(a[2]), title: a[2] === "7" && o.title ? o.title : "foo breaks", body: a[2] === "7" && o.body ? o.body : ISSUE_BODY, state: w.issueStates?.[a[2]] ?? "OPEN", comments: [], ...(o.labels && a[2] === "7" ? { labels: o.labels } : {}) });
80
85
  if (k === "pr edit") { w.prEdits.push(a); return ""; }
81
86
  if (k === "label create") { w.labelCreates = [...(w.labelCreates ?? []), a]; return ""; }
82
87
  if (k === "issue comment" && w.commentErrors.length) throw new Error(w.commentErrors.shift());
@@ -115,6 +120,7 @@ function world(o = {}) {
115
120
  if (a[0] === "diff" && a[1] === "--check") return "";
116
121
  if (a[0] === "grep") return w.markers ? "src/foo.js:3:<<<<<<< HEAD" : "";
117
122
  if (a[0] === "merge" && a[1] === "--abort") { w.mergeInProgress = false; return ""; }
123
+ if (a[0] === "rev-parse" && a.length === 2 && /^origin\//.test(String(a[1]))) return w.baseHeads.length > 1 ? w.baseHeads.shift() : w.baseHeads[0]; // #4386: the base head moves only when a test says so
118
124
  if (a[0] === "rev-parse") return w.head;
119
125
  if (a[0] === "diff" && a[1] === "--name-only" && !a.some((x) => String(x).includes("..."))) return w.changed.filter((f) => !(o.equalToBase ?? []).includes(f)).join("\n");
120
126
  if (a[0] === "diff" && a[1] === "--name-only") return w.changed.join("\n");
@@ -132,10 +138,12 @@ function world(o = {}) {
132
138
  w.seq.push(c.role + (c.resume ? "-resume" : ""));
133
139
  w.envs.push(c.env ?? null);
134
140
  if (c.role === "builder") {
135
- const r = c.resume ? (w.resumeResult ?? w.result) : w.result;
141
+ const cost = c.resume ? (w.sessCost = Math.round(((w.sessCost ?? 0) + 0.1) * 1e9) / 1e9) : (w.sessCost = 0.1); // #4407: a resumed CLI session reports its running total
142
+ const fix = Boolean(c.resume) || /no earlier session/.test(c.prompt); // #4363: a fresh fix session acts like the old resume
143
+ const r = fix ? (w.resumeResult ?? w.result) : w.result;
136
144
  const kind = /PLANNING ONLY/.test(c.prompt) ? "plan" : /contract.json is not valid/.test(c.prompt) ? "contract-fix" : "work";
137
145
  const n = ++w.builderN;
138
- if (o.builder) { await o.builder(w, put, kind, n, c); return { text: "done", sessionId: "sess-1", usage: { input_tokens: 10, output_tokens: 5, cache_read_input_tokens: 100 }, costUsd: 0.1, durationMs: 1000, turns: 3, model: "default" }; }
146
+ if (o.builder) { await o.builder(w, put, kind, n, c); return { text: "done", sessionId: "sess-1", usage: { input_tokens: 10, output_tokens: 5, cache_read_input_tokens: 100 }, costUsd: cost, durationMs: 1000, turns: 3, model: "default" }; }
139
147
  if (kind === "plan") {
140
148
  if (o.live) await o.live(w, put);
141
149
  else if (!o.noStages) writeStages(["investigation"]);
@@ -146,14 +154,14 @@ function world(o = {}) {
146
154
  } else {
147
155
  if (!(`${WT}/.forge-lean/contract.json` in w.files) && w.contract && r?.status !== "blocked") put("contract.json", JSON.stringify(w.contract));
148
156
  if (!o.noStages && !w.implWritten && !o.live) { writeStages(["implementation"]); w.implWritten = true; }
149
- if (c.resume && n > 2 && /does not conform/.test(c.prompt) && o.resumeChanged) w.changed = o.resumeChanged;
157
+ if (fix && n > 2 && /does not conform/.test(c.prompt) && o.resumeChanged) w.changed = o.resumeChanged;
150
158
  if (/Merging origin\//.test(c.prompt) && o.syncChanged) w.changed = o.syncChanged;
151
159
  if (r?.status === "blocked" || ["invalid", "already-fixed"].includes(r?.status)) put("result.json", JSON.stringify(r));
152
160
  else if (r) put("result.json", JSON.stringify({ ...r, requirements: o.claims ?? w.contract.requirements.filter((x) => x.disposition === "deliver").map((x) => ({ id: x.id, status: "implemented", evidence: "src/foo.js:3" })),
153
- validation: (c.resume && n > 2 ? o.resumeValidation : null) ?? o.validation ?? (w.contract.validate ?? []).map((v) => ({ id: v.id, command: v.command, exit: 0 })), amendments: (c.resume && n > 2 ? o.resumeAmendments : null) ?? o.amendments ?? [] }));
161
+ validation: (fix && n > 2 ? o.resumeValidation : null) ?? o.validation ?? (w.contract.validate ?? []).map((v) => ({ id: v.id, command: v.command, exit: 0 })), amendments: (fix && n > 2 ? o.resumeAmendments : null) ?? o.amendments ?? [] }));
154
162
  }
155
- if (c.resume || /Merging origin\//.test(c.prompt)) { w.head = `bbbb${w.calls.length}`.padEnd(16, "0"); if (!o.leaveConflicts) { w.conflictFiles = []; w.mergeInProgress = false; } }
156
- return { text: "done", sessionId: "sess-1", usage: { input_tokens: 10, output_tokens: 5, cache_read_input_tokens: 100 }, costUsd: 0.1, durationMs: 1000, turns: 3, model: "default" };
163
+ if ((fix && !o.freshNoCommit) || /Merging origin\//.test(c.prompt)) { w.head = `bbbb${w.calls.length}`.padEnd(16, "0"); if (!o.leaveConflicts) { w.conflictFiles = []; w.mergeInProgress = false; } }
164
+ return { text: "done", sessionId: "sess-1", usage: { input_tokens: 10, output_tokens: 5, cache_read_input_tokens: 100 }, costUsd: cost, durationMs: 1000, turns: 3, model: "default" };
157
165
  }
158
166
  return { text: w.reviews.length ? w.reviews.shift() : ok(), sessionId: "rev", usage: { input_tokens: 4, output_tokens: 2 }, costUsd: 0.05, durationMs: 500, turns: 2, model: c.model };
159
167
  },
@@ -179,7 +187,7 @@ describe("core work-on", () => {
179
187
  const r = await run(w, { dir });
180
188
  assert.equal(r.terminalReason, "merged");
181
189
  assert.equal(r.pr, 77);
182
- assert.deepEqual(w.comments.map(markerOf), ["FORGE:STARTED", "FORGE:INVESTIGATION", "FORGE:CONTRACT", "FORGE:IMPLEMENTATION", "FORGE:REVIEW", "FORGE:TRAJECTORY"]);
190
+ assert.deepEqual(w.comments.map(markerOf), ["FORGE:STARTED", "FORGE:INVESTIGATION", "FORGE:CONTRACT", "FORGE:IMPLEMENTATION", "FORGE:REVIEW", "FORGE:FACTS", "FORGE:TRAJECTORY"]);
183
191
  assert.match(w.comments[1], /<!-- FORGE:VALIDATE -->/, "VALIDATE stays as an alias marker");
184
192
  assert.match(w.comments[1], /^<!-- FORGE:INVESTIGATION -->\n<!-- FORGE:VALIDATE -->\n## Investigation Report\n\*\*Verdict\*\*: CONFIRMED/);
185
193
  assert.match(w.comments[2], /^<!-- FORGE:CONTRACT -->\n<!-- FORGE:PLAN -->\n## Builder Contract \(frozen `[0-9a-f]{12}`\)/, "PLAN stays as an alias marker");
@@ -282,7 +290,7 @@ describe("core work-on", () => {
282
290
  assert.equal(r.terminalReason, "merged");
283
291
  const b = builders(w);
284
292
  assert.equal(b.length, 3);
285
- assert.equal(b[2][2], "sess-1");
293
+ assert.equal(b[2][2], null, "#4363: a FAIL citing file:line runs in a fresh session");
286
294
  assert.match(b[2][4], /conformance\.unimplemented: R1 partial: src\/foo.js:9 still wrong/);
287
295
  assert.equal(reviewers(w).length, 2);
288
296
  assert.equal(w.prs.length, 1);
@@ -428,7 +436,7 @@ describe("core work-on", () => {
428
436
 
429
437
  it("#4124 split-required files sub-issues with Parent / Depends-on lines and ends decomposed", async () => {
430
438
  const w = depWorld({ changed: [], result: { status: "blocked", summary: "epic", block: { kind: "split-required", subissues: [
431
- { title: "part A", body: "do A" }, { title: "part B", body: "do B", depends_on: [0] }] } } }, {});
439
+ { title: "part A", body: "do A", paths: ["src/a"] }, { title: "part B", body: "do B", paths: ["src/b"], depends_on: [0] }] } } }, {});
432
440
  const r = await run(w);
433
441
  assert.equal(r.terminalReason, "decomposed");
434
442
  assert.deepEqual(r.subissues, [201, 202]);
@@ -436,11 +444,70 @@ describe("core work-on", () => {
436
444
  assert.match(bodyB, /^\*\*Parent\*\*: #7$/m);
437
445
  assert.match(bodyB, /^\*\*Depends on\*\*: #201$/m);
438
446
  assert.ok(w.labels.includes("decomposed"));
447
+ assert.equal(r.decomposition.independent, true);
448
+ assert.ok(w.comments.some((c) => /FORGE:DECOMPOSED/.test(c) && /Rationale: independent: yes; size: no estimate vs budget 25 files \/ 1500 lines -> oversize no/.test(c)));
439
449
  noNeedsHuman(w);
440
450
  });
441
451
 
452
+ it("#4396 a split of overlapping parts within the size budget is rejected once toward one checklist lane; repeated, it is needs-human", async () => {
453
+ const over = { status: "blocked", summary: "epic", block: { kind: "split-required", size: { files: 6, lines: 300 }, subissues: [
454
+ { title: "part A", body: "do A", paths: ["src/alerts"] }, { title: "part B", body: "do B", paths: ["src/alerts/routing.ts"] }] } };
455
+ const w = depWorld({ changed: [], result: over }, {});
456
+ const r = await run(w);
457
+ assert.equal(r.terminalReason, "needs-human");
458
+ assert.equal(r.code, "builder.block_untyped");
459
+ assert.equal(w.created.length, 0, "no sub-issue is filed");
460
+ const resumed = builders(w).filter((c) => c[2]);
461
+ assert.equal(resumed.length, 1);
462
+ assert.match(resumed[0][4], /split is not justified/);
463
+ assert.match(resumed[0][4], /ordered checklist/);
464
+ });
465
+
466
+ it("#4396 an oversize split of overlapping parts is admitted and records the size rationale", async () => {
467
+ const big = { status: "blocked", summary: "epic", block: { kind: "split-required", size: { files: 40, lines: 4000 }, subissues: [
468
+ { title: "part A", body: "do A", paths: ["src/alerts"] }, { title: "part B", body: "do B", paths: ["src/alerts/x.ts"] }] } };
469
+ const w = depWorld({ changed: [], result: big }, {});
470
+ const r = await run(w);
471
+ assert.equal(r.terminalReason, "decomposed");
472
+ assert.deepEqual({ independent: r.decomposition.independent, oversize: r.decomposition.oversize }, { independent: false, oversize: true });
473
+ assert.ok(w.comments.some((c) => /FORGE:DECOMPOSED/.test(c) && /size: 40 files, 4000 lines .* oversize yes/.test(c)));
474
+ });
475
+
476
+ it("#4385 split siblings that declare a shared hub file are serialised (chained, no duplicate transitive edge); disjoint or undeclared files stay parallel", () => {
477
+ const hub = ["src/domain/index.ts", "./src/domain/events.ts"];
478
+ const BIG = { files: 40, lines: 2000 }; // #4396: shared or undeclared files are admitted only over the size budget
479
+ const b = classifyBlock({ block: { kind: "split-required", size: BIG, subissues: [
480
+ { title: "state machine", body: "a", files: [...hub, "src/domain/state.ts"] },
481
+ { title: "next-date", body: "b", files: [...hub, "src/domain/next.ts"] },
482
+ { title: "timeline", body: "c", files: ["/src/domain/index.ts", "src/domain/events.ts", "src/domain/timeline.ts"] }] } }, 7);
483
+ assert.equal(b.kind, "split");
484
+ assert.deepEqual(b.subissues.map((x) => x.dependsOn), [[], [0], [1]]);
485
+ assert.deepEqual(b.serialized.map((e) => [e.from, e.to, e.files]), [[1, 0, ["src/domain/index.ts", "src/domain/events.ts"]], [2, 1, ["src/domain/index.ts", "src/domain/events.ts"]]]);
486
+ const disjoint = classifyBlock({ block: { kind: "split-required", subissues: [
487
+ { title: "a", body: "a", files: ["src/a.ts"] }, { title: "b", body: "b", files: ["src/b.ts"] }] } }, 7);
488
+ assert.deepEqual(disjoint.subissues.map((x) => x.dependsOn), [[], []]);
489
+ assert.deepEqual(disjoint.serialized, []);
490
+ const legacy = classifyBlock({ block: { kind: "split-required", size: BIG, subissues: [
491
+ { title: "a", body: "a" }, { title: "b", body: "b" }, { title: "c", body: "c", depends_on: [0] }] } }, 7);
492
+ assert.deepEqual(legacy.subissues.map((x) => x.dependsOn), [[], [], [0]]);
493
+ assert.ok(legacy.subissues.every((x) => !("files" in x)));
494
+ const declared = classifyBlock({ block: { kind: "split-required", size: BIG, subissues: [
495
+ { title: "a", body: "a", files: ["x.ts"] }, { title: "b", body: "b", files: ["y.ts"], depends_on: [0] }, { title: "c", body: "c", files: ["x.ts"], depends_on: [1] }] } }, 7);
496
+ assert.deepEqual(declared.subissues.map((x) => x.dependsOn), [[], [0], [1]]);
497
+ assert.deepEqual(declared.serialized, []);
498
+ });
499
+
500
+ it("#4385 a serialised sibling is filed with the engine-added Depends-on line", async () => {
501
+ const w = depWorld({ changed: [], result: { status: "blocked", summary: "epic", block: { kind: "split-required", size: { files: 40, lines: 2000 }, subissues: [
502
+ { title: "part A", body: "do A", files: ["src/domain/index.ts"] }, { title: "part B", body: "do B", files: ["src/domain/index.ts", "src/b.ts"] }] } } }, {});
503
+ const r = await run(w);
504
+ assert.equal(r.terminalReason, "decomposed");
505
+ const bodyB = w.created[1][w.created[1].indexOf("--body") + 1];
506
+ assert.match(bodyB, /^\*\*Depends on\*\*: #201$/m);
507
+ });
508
+
442
509
  it("#4153 a split of a milestone issue files every sub-issue with the parent's milestone; no milestone passes none", async () => {
443
- const split = { status: "blocked", summary: "epic", block: { kind: "split-required", subissues: [{ title: "part A", body: "do A" }, { title: "part B", body: "do B", depends_on: [0] }] } };
510
+ const split = { status: "blocked", summary: "epic", block: { kind: "split-required", subissues: [{ title: "part A", body: "do A", paths: ["src/a"] }, { title: "part B", body: "do B", paths: ["src/b"], depends_on: [0] }] } };
444
511
  const w = depWorld({ changed: [], result: split, milestone: "Remote MCP Server", branchesOnOrigin: ["staging", "milestone/remote-mcp-server"] }, {});
445
512
  const r = await run(w, { lane: null });
446
513
  assert.equal(r.terminalReason, "decomposed");
@@ -462,7 +529,7 @@ describe("core work-on", () => {
462
529
  noNeedsHuman(w);
463
530
  });
464
531
 
465
- const splitBlock = { status: "blocked", summary: "epic", block: { kind: "split-required", subissues: [{ title: "part A", body: "do A" }, { title: "part B", body: "do B", depends_on: [0] }] } };
532
+ const splitBlock = { status: "blocked", summary: "epic", block: { kind: "split-required", subissues: [{ title: "part A", body: "do A", paths: ["src/a"] }, { title: "part B", body: "do B", paths: ["src/b"], depends_on: [0] }] } };
466
533
  it("#4337 without decomposition.close_parent a split keeps the parent open (opt-in, default false)", async () => {
467
534
  const w = depWorld({ changed: [], result: splitBlock }, {});
468
535
  assert.equal((await run(w)).terminalReason, "decomposed");
@@ -859,7 +926,7 @@ describe("in-batch recovery of mechanical needs-human (#4267)", () => {
859
926
  };
860
927
  const quiet = {
861
928
  invalid: () => world({ changed: [], result: { status: "invalid", summary: "not a bug", evidence: ["src/foo.js:3 already guards null"], tests: [] } }),
862
- decomposed: () => withDeps(world({ changed: [], result: { status: "blocked", summary: "epic", block: { kind: "split-required", subissues: [{ title: "part A", body: "do A" }] } } }), {}),
929
+ decomposed: () => withDeps(world({ changed: [], result: { status: "blocked", summary: "epic", block: { kind: "split-required", subissues: [{ title: "part A", body: "do A", paths: ["src/a"] }] } } }), {}),
863
930
  "dependency-open": () => withDeps(world({ changed: [], result: { status: "blocked", summary: "needs #3", block: { kind: "dependency", issues: ["#3"] } } }), { 3: { state: "OPEN", labels: [] } }),
864
931
  "engine-error": () => failBuilder(world(), new Error("boom")),
865
932
  deferred: () => failBuilder(world(), limitErr),
@@ -1168,6 +1235,84 @@ describe("conformance shadow mode (the default)", () => {
1168
1235
  assert.equal((await run(w)).code, "contract.invalid"); assert.equal(builders(w).length, 2, "plan + one correction");
1169
1236
  assert.match(builders(w)[1][4], /R1: quote is not verbatim[\s\S]*V1: needs command 'jq'/);
1170
1237
  });
1238
+ it("#4393 replaying the AlterLab#36619 contract flags V4 at preflight, before the builder starts", async () => {
1239
+ const V4 = "uv run --no-project --python 3.12 python -m shared.detection.corpus.tools.check_manifest";
1240
+ const c = { ...CONTRACT, validate: [{ id: "V1", command: "node --test test/foo.test.js" }, { id: "V2", command: "python3 -m pytest -q tests/test_corpus.py" }, { id: "V3", command: "uv run pytest -q tests/test_manifest.py" }, { id: "V4", command: V4 }] };
1241
+ const fixed = { ...c, validate: [...c.validate.slice(0, 3), { id: "V4", command: "uv run python -m shared.detection.corpus.tools.check_manifest" }] };
1242
+ const dir = tmpDir("preflight-run-");
1243
+ const w = world({ contract: c, fixedContract: fixed, validation: fixed.validate.map((v) => ({ id: v.id, command: v.command, exit: 0 })) });
1244
+ const ran = [];
1245
+ w.io.probeCommand = async () => ({ rc: 0, output: "" }); // uv, python3 and pytest are on the host
1246
+ w.io.preflightRun = async (a) => { ran.push([a.command, a.cwd, w.seq.length]); return a.command === V4 ? { rc: 1, output: "Traceback (most recent call last):\nModuleNotFoundError: No module named 'httpx'\n" } : { rc: 1, output: "AssertionError: expected RED on base" }; };
1247
+ assert.equal((await run(w, { dir })).terminalReason, "merged");
1248
+ const b = builders(w);
1249
+ assert.equal(b.length, 3, "plan + one contract correction + implement");
1250
+ assert.match(b[1][4], /V4: running it on the base worktree stopped before any test ran: needs Python module 'httpx'[\s\S]*repo's own toolchain/);
1251
+ assert.doesNotMatch(b[1][4], /V[123]:/, "RED on base is not a miss");
1252
+ assert.match(b[2][4], /The engine froze/, "the correction lands before the implement builder");
1253
+ assert.ok(ran.every(([, cwd]) => cwd === WT));
1254
+ const firstImpl = w.seq.indexOf("builder-resume", w.seq.indexOf("builder-resume") + 1);
1255
+ assert.ok(ran.every(([, , at]) => at < firstImpl), "every run happens before the implement session");
1256
+ const pf = readLog(dir, 7).filter((e) => e.event === "VALIDATE_PREFLIGHT");
1257
+ assert.deepEqual(pf.map((e) => [e.corrected, e.found.map((m) => [m.ids, m.tool, m.kind, m.source])]), [[true, [[["V4"], "httpx", "python-module", "run"]]]]);
1258
+ // a timed-out run is no evidence; an unset $VAR is a miss without running the command
1259
+ const t = world({ contract: { ...CONTRACT, validate: [{ id: "V1", command: "node --test test/foo.test.js" }, { id: "V2", command: 'node scripts/x.mjs "$NO_SUCH_VAR_4393"' }] } });
1260
+ const tRan = []; t.io.preflightRun = async (a) => { tRan.push(a.command); return { rc: 124, output: "ModuleNotFoundError: No module named 'httpx'\n(timed out after 30s)" }; };
1261
+ assert.equal((await run(t)).terminalReason, "merged");
1262
+ assert.deepEqual(tRan, ["node --test test/foo.test.js", "node --test test/foo.test.js"], "V2 skipped (env miss), V1 run again after the correction");
1263
+ assert.match(builders(t)[1][4], /V2: uses \$NO_SUCH_VAR_4393, which is unset or empty/);
1264
+ });
1265
+ it("#4393 files the preflight runs create or modify are reverted; paths dirty before are not", async () => {
1266
+ const w = world({ dirty: true });
1267
+ let st = " M src/foo.js\n";
1268
+ const git0 = w.io.git, reverts = [];
1269
+ w.io.git = async (a, o) => { if (a[0] === "status" && a[2] === "--untracked-files=all") return st; if (a[0] === "clean" || (a[0] === "checkout" && a[1] === "--")) { reverts.push(a.join(" ")); return ""; } return git0(a, o); };
1270
+ w.io.preflightRun = async () => { st += "?? .pytest_cache_x/log\n M src/bar.js\n"; return { rc: 0, output: "" }; };
1271
+ await run(w);
1272
+ assert.deepEqual(reverts, ["clean -fdq -- .pytest_cache_x/log", "checkout -- src/bar.js"]);
1273
+ assert.ok(traj(w).some((x) => /Validate preflight reverted 2 path\(s\) its runs changed/.test(x)));
1274
+ });
1275
+ it("#4353 the #4348 contract shape: an allowlist validate is dropped at freeze (CONTRACT_SCOPE_VALIDATE_DROPPED), the amended lane merges, and a real out-of-scope edit is still scope_creep", async () => {
1276
+ const ALLOW = 'test -z "$(git diff --name-only origin/staging...HEAD | grep -v -x -e src/foo.js -e test/foo.test.js)"';
1277
+ const c = { ...CONTRACT, validate: [...CONTRACT.validate, { id: "V2", command: ALLOW }] }, files = ["src/foo.js", "test/foo.test.js", "docs/foo.md"];
1278
+ const shape = (changed) => ({ contract: c, changed, amendments: [{ kind: "add-deliverable", path: "docs/foo.md", why: "the docs-freshness rule needs the module doc" }],
1279
+ validation: [{ id: "V1", command: CONTRACT.validate[0].command, exit: 0 }, { id: "V2", command: ALLOW, exit: 1 }], reviews: [conf({}, CONTRACT, files)] });
1280
+ const dir = tmpDir("scope-drop-");
1281
+ const w = world(shape(files));
1282
+ w.io.runCommand = async () => ({ rc: 1, output: "" });
1283
+ assert.equal((await run(w, { dir })).terminalReason, "merged", "the lane is not ended by scope");
1284
+ assert.equal(builders(w).length, 2, "plan + implement: the drop costs no contract correction");
1285
+ const log = readLog(dir, 7);
1286
+ assert.deepEqual(log.filter((e) => e.event === "CONTRACT_SCOPE_VALIDATE_DROPPED").map((e) => e.runs), [[{ id: "V2", command: ALLOW }]]);
1287
+ assert.equal(log.filter((e) => e.event === "VALIDATE_SUPERSEDED").length, 0, "nothing left for the #4350 path to supersede");
1288
+ const posted = w.comments.find((x) => x.includes("FORGE:CONTRACT"));
1289
+ assert.ok(posted.includes("V1") && !posted.includes("grep -v -x"), "the frozen contract has no allowlist validate");
1290
+ assert.ok(traj(w).some((t) => /Scope-check validate V2 dropped at contract freeze/.test(t)));
1291
+ const creep = world(shape([...files, "src/other.js"]));
1292
+ creep.io.runCommand = async () => ({ rc: 1, output: "" });
1293
+ assert.equal((await run(creep)).code, "conformance.scope_creep");
1294
+ assert.ok(builders(creep).some((b) => /changed outside the contract's deliverables: src\/other\.js/.test(b[4])));
1295
+ });
1296
+
1297
+ it("#4404 a Lighthouse requirement is tagged out_of_band at freeze; with instrumentation the lane merges and the PR body names the job", async () => {
1298
+ const LH = "Lighthouse performance score >= 95 on the landing page.";
1299
+ const body = `${ISSUE_BODY}\n- ${LH}`;
1300
+ const c = { ...CONTRACT, requirements: [...CONTRACT.requirements, { id: "R2", quote: LH, disposition: "deliver", evidence: "lighthouse-nightly" }] };
1301
+ const tagged = { ...c, requirements: [c.requirements[0], { ...c.requirements[1], verify: "out_of_band" }] };
1302
+ const review = (instrumented) => conf({ issue_coverage: [...JSON.parse(conf().slice(8, -4)).issue_coverage, { quote: LH, contract_ids: ["R2"], status: "covered" }],
1303
+ requirements: [{ id: "R1", status: "implemented", evidence: "src/foo.js:3", validated_by: "V1" }, { id: "R2", status: "partial", evidence: instrumented ? "src/foo.js:3" : "", validated_by: "V1", instrumented, note: "measured by lighthouse-nightly" }] });
1304
+ const dir = tmpDir("oob-");
1305
+ const w = world({ contract: c, issueBodies: [body], claims: [{ id: "R1", status: "implemented", evidence: "src/foo.js:3" }, { id: "R2", status: "partial", instrumented: true, evidence: "src/foo.js:3" }], reviews: [review(true)] });
1306
+ assert.equal((await run(w, { dir })).terminalReason, "merged");
1307
+ assert.deepEqual(readLog(dir, 7).filter((e) => e.event === "CONTRACT_OUT_OF_BAND_TAGGED").map((e) => e.ids), [["R2"]]);
1308
+ assert.ok(w.comments.find((x) => x.includes("FORGE:CONTRACT")).includes('"verify": "out_of_band"'), "the frozen contract carries the tag");
1309
+ assert.ok(traj(w).some((t) => /R2 tagged verify out_of_band at contract freeze/.test(t)));
1310
+ assert.match(w.prs[0][w.prs[0].indexOf("--body") + 1], /## Verified out of band\n- R2: Lighthouse performance score >= 95 on the landing page\. \(job `lighthouse-nightly`\)/);
1311
+ // without the instrumentation it is still unimplemented
1312
+ const no = world({ contract: tagged, issueBodies: [body], claims: [{ id: "R1", status: "implemented", evidence: "src/foo.js:3" }, { id: "R2", status: "partial", instrumented: true, evidence: "src/foo.js:3" }], reviews: [review(false), review(false)] });
1313
+ const r = await run(no);
1314
+ assert.equal(r.code, "conformance.unimplemented");
1315
+ });
1171
1316
 
1172
1317
  it("a failed validate fetches the lane and the resume says how to catch up with a moved base (merge, never rebase)", async () => {
1173
1318
  const w = world({ validation: [{ id: "V1", command: "node --test test/foo.test.js", exit: 1 }], resumeValidation: [{ id: "V1", command: "node --test test/foo.test.js", exit: 0 }] });
@@ -1238,6 +1383,62 @@ describe("conformance shadow mode (the default)", () => {
1238
1383
  assert.ok(w.seq.indexOf("pr") > -1 && traj(w)[0].includes("Tracked R2 (defer) in #900"));
1239
1384
  });
1240
1385
 
1386
+ it("#4398 a diff on a declared review.risk_paths glob makes the shadow gate enforce: a nonconforming risk-path PR never merges", async () => {
1387
+ const dir = tmpDir("risk-");
1388
+ const w = world({ contract: { ...CONTRACT, preserve: [] }, reviews: [omission(), omission()] });
1389
+ const r = await runShadow(w, { dir, reviewTrigger: { mode: "shadow", riskPaths: ["src/**"] } });
1390
+ assert.equal(r.terminalReason, "needs-human"); assert.equal(w.merges.length, 0);
1391
+ assert.equal(r.code, "conformance.omission"); assert.equal(builders(w).length, 3, "one builder resume, as in enforce");
1392
+ assert.deepEqual(r.conformance, { mode: "enforce", would: null, risk: true });
1393
+ const log = readLog(dir, 7);
1394
+ assert.deepEqual(log.filter((e) => e.event === "CONFORMANCE_RISK_ENFORCED").map((e) => e.paths), [["src/foo.js"]], "logged once (sticky across the resume)");
1395
+ assert.equal(log.find((e) => e.event === "RUN_TERMINAL").conformance_mode, "enforce");
1396
+ assert.ok(traj(w)[0].includes("Conformance enforced (review.risk_paths): src/foo.js"));
1397
+ });
1398
+
1399
+ it("#4398 risk-enforced lanes merge once conforming; built-in risk words and unmatched globs stay shadow", async () => {
1400
+ const w = world({ contract: { ...CONTRACT, preserve: [] }, reviews: [omission(), ok({}, { ...CONTRACT, preserve: [] })] });
1401
+ const r = await runShadow(w, { reviewTrigger: { mode: "shadow", riskPaths: ["src/foo.js"] } });
1402
+ assert.equal(r.terminalReason, "merged"); assert.equal(builders(w).length, 3);
1403
+ assert.match(w.comments.find((c) => c.includes("<!-- FORGE:REVIEW -->")), /\*\*Verdict:\*\* CONFORMS/);
1404
+ const s = world({ contract: { ...CONTRACT, preserve: [] }, changed: ["src/auth/foo.js", "test/foo.test.js"], reviews: [omission()] });
1405
+ const rs = await runShadow(s, { reviewTrigger: { mode: "shadow", riskPaths: ["db/**"] } });
1406
+ assert.equal(rs.terminalReason, "merged", "an auth path with no declared glob stays shadow");
1407
+ assert.deepEqual(rs.conformance, { mode: "shadow", would: "nonconforming", risk: false });
1408
+ });
1409
+
1410
+ it("#4398 an investigation whose question is deferred closes partial (not planned), carried forward in the tracking issue", async () => {
1411
+ const k = { ...CONTRACT, preserve: [], requirements: [CONTRACT.requirements[0], { id: "R2", quote: "Existing callers of foo keep working.", disposition: "defer", why: "w" }] };
1412
+ const w = world({ title: "investigate(stealth): which signal is rejected", contract: k, claims: [{ id: "R1", status: "implemented" }], reviews: [conf({}, k)] });
1413
+ const r = await runShadow(w);
1414
+ assert.equal(r.terminalReason, "merged"); assert.equal(r.close, "partial");
1415
+ assert.deepEqual(w.closed.map((a) => a[a.indexOf("--reason") + 1]), ["not planned"]);
1416
+ assert.ok(w.comments.some((c) => c.includes("<!-- FORGE:TRACKED -->") && /Closed as partial[\s\S]*carried forward in #900/.test(c)));
1417
+ assert.ok(traj(w)[0].includes("| Close | issue closed (partial) |"));
1418
+ });
1419
+
1420
+ it("#4398 an investigation whose answer the review marks deferred is tracked before the PR and closes partial, even though shadow merges it", async () => {
1421
+ const k = { ...CONTRACT, preserve: [] };
1422
+ const w = world({ title: "investigate: foo", contract: k, reviews: [conf({ requirements: [{ id: "R1", status: "deferred", evidence: "", validated_by: "", note: "question open" }] }, k)] });
1423
+ const r = await runShadow(w);
1424
+ assert.equal(r.terminalReason, "merged"); assert.equal(r.close, "partial");
1425
+ assert.equal(w.followups.length, 1); assert.match(w.followups[0].items[0].key, /^R1: /); assert.match(w.followups[0].items[0].why, /investigation question not answered/);
1426
+ assert.match(w.prs[0].join(" "), /#900/);
1427
+ assert.deepEqual(w.closed.map((a) => a[a.indexOf("--reason") + 1]), ["not planned"]);
1428
+ });
1429
+
1430
+ it("#4398 an answered investigation and a non-investigation with a deferral both close completed", async () => {
1431
+ const a = world({ title: "investigate(x): why foo breaks", contract: { ...CONTRACT, preserve: [] }, reviews: [conf({}, { ...CONTRACT, preserve: [] })] });
1432
+ const ra = await runShadow(a);
1433
+ assert.equal(ra.terminalReason, "merged"); assert.equal(ra.close, undefined);
1434
+ assert.deepEqual(a.closed.map((x) => x[x.indexOf("--reason") + 1]), ["completed"]);
1435
+ const k = { ...CONTRACT, preserve: [], requirements: [CONTRACT.requirements[0], { id: "R2", quote: "Existing callers of foo keep working.", disposition: "defer", why: "w" }] };
1436
+ const b = world({ contract: k, claims: [{ id: "R1", status: "implemented" }], reviews: [conf({}, k)] });
1437
+ await runShadow(b);
1438
+ assert.deepEqual(b.closed.map((x) => x[x.indexOf("--reason") + 1]), ["completed"]);
1439
+ assert.ok(traj(b)[0].includes("| Close | issue closed (completed) |"));
1440
+ });
1441
+
1241
1442
  it("#4160 base-sync merge files (equal to the base) drop out of the scope check", async () => {
1242
1443
  const w = world({ changed: ["src/foo.js", "src/merged-from-base.js", "test/foo.test.js"], equalToBase: ["src/merged-from-base.js"] });
1243
1444
  assert.equal((await run(w)).terminalReason, "merged");
@@ -1294,7 +1495,7 @@ describe("structure delta (forge#4159): undeclared package edges block, import g
1294
1495
  assert.equal(r.terminalReason, "merged");
1295
1496
  assert.equal(builders(w).length, 3); assert.equal(reviewers(w).length, 1);
1296
1497
  assert.match(builders(w)[2][4], /@s\/db -> @s\/core \(dependencies, packages\/db\/package\.json\)[\s\S]*add it to the contract with a reason, or remove it/);
1297
- assert.equal(builders(w)[2][2], "sess-1", "resumed in the builder's own session");
1498
+ assert.equal(builders(w)[2][2], null, "#4363: a fresh fix session");
1298
1499
  assert.deepEqual(w.structureCalls[0], { cwd: "/wt/lean-7", base: "origin/staging" });
1299
1500
  const e = readLog(dir, 7).find((x) => x.event === "STRUCTURE_DELTA");
1300
1501
  assert.equal(e.edges_added, 1);
@@ -1377,6 +1578,7 @@ describe("structure delta (forge#4159): undeclared package edges block, import g
1377
1578
  assert.equal(reviewerPrompt(args), reviewerPrompt({ ...args, structure: "" }));
1378
1579
  assert.ok(reviewerPrompt({ ...args, structure: "x\ny" }).endsWith("</diff>\n<structure>x y</structure>"));
1379
1580
  assert.match(builderPlanPrompt(7), /"the lint allows it" or "there is a precedent" is not a reason on its own/);
1581
+ assert.match(builderPlanPrompt(7), /Scope is enforced by the engine[^\n]*do not write a file-allowlist or denylist validate[^\n]*the engine drops such a validate at freeze/, "#4353");
1380
1582
  assert.match(builderPlanPrompt(7), /must be a deliverable on that package\.json whose change names the package and gives the reason/);
1381
1583
  assert.match(builderImplementPrompt(), /blocked by the engine: add it through add-deliverable with a why naming the package/);
1382
1584
  });
@@ -1407,7 +1609,8 @@ describe("hotspot prevention (forge#4195): plan facts, build rule, unexempted gr
1407
1609
  assert.equal(hotspotBuildRule(facts([], { file_lines: 1000, growth_lines: 50 })).trim(), "Don't grow a file over 1000 lines by more than 50 lines. Put new code next to it.");
1408
1610
  assert.equal(hotspotBuildRule(null), ""); assert.equal(hotspotBuildRule(facts([], { enabled: false })), "");
1409
1611
  const plan = builderPlanPrompt(7) + hotspotPlanNote(facts([HOT, { ...HOT, path: "a/b.py" }, { ...HOT, path: "c/d.py" }]));
1410
- assert.ok(approxTokens(plan) <= 1800, `plan prompt with hotspots is ${approxTokens(plan)} tokens`);
1612
+ // 1850: #4404 (verify out_of_band) and #4396 (split size rule) grew the plan prompt to ~1803 tokens on CI paths.
1613
+ assert.ok(approxTokens(plan) <= 1850, `plan prompt with hotspots is ${approxTokens(plan)} tokens`);
1411
1614
  assert.ok(approxTokens(builderImplementPrompt() + hotspotBuildRule(facts([]))) <= 1500);
1412
1615
  });
1413
1616
 
@@ -1719,6 +1922,17 @@ describe("comment templates", () => {
1719
1922
  assert.match(renderPrBody({ issue: 1, result: null }), /## Root cause\nnot reported[\s\S]*Closes #1$/);
1720
1923
  });
1721
1924
 
1925
+ it("#4404 out-of-band requirements are listed in the PR body with their job and marked in the contract and review tables", () => {
1926
+ const c = { requirements: [{ id: "R1", quote: "Fix the leak.", disposition: "deliver" }, { id: "R2", quote: "Lighthouse >= 95 on /home.", disposition: "deliver", verify: "out_of_band", evidence: "lighthouse-nightly" }], deliverables: [{ id: "D1", path: "a.js", change: "x", requirements: ["R1"] }] };
1927
+ const body = renderPrBody({ issue: 4, contract: c, result: null });
1928
+ assert.match(body, /## Verified out of band\n- R2: Lighthouse >= 95 on \/home\. \(job `lighthouse-nightly`\)\n\nCloses #4$/);
1929
+ assert.doesNotMatch(renderPrBody({ issue: 4, contract: { ...c, requirements: [c.requirements[0]] }, result: null }), /out of band/);
1930
+ assert.match(renderContract(c, "abc"), /\| R2 \| Lighthouse >= 95 on \/home\. \| deliver, verified out of band \(lighthouse-nightly\) \|/);
1931
+ const rv = { requirements: [{ id: "R1", status: "implemented" }, { id: "R2", status: "partial" }], issue_coverage: [], scope: [], preserve: [] };
1932
+ assert.match(renderReview(rv, c), /\| R2 \| partial \(out of band: lighthouse-nightly\) \|/);
1933
+ assert.match(renderReview(rv, c), /\| R1 \| implemented \|/);
1934
+ });
1935
+
1722
1936
  it("trajectory rows: merged, invalid, needs-human; missing rows read 'not reached'", () => {
1723
1937
  const base = { issue: 9, validate: ["CONFIRMED", "2 evidence item(s)"], plan: "posted", implement: ["2 file(s)", "b"], review: ["PASS", "1 round"], pr: ["#5", "opened"], merge: ["all", "merged at abc"], close: ["issue closed (completed)", "label workflow:merged"], decisions: ["d1"], anomalies: [], usage: "cost $1" };
1724
1938
  const m = renderTrajectory(base);
@@ -1741,10 +1955,21 @@ describe("comment templates", () => {
1741
1955
  assert.match(t, /does not conform to the issue and its frozen contract after one builder resume/);
1742
1956
  });
1743
1957
 
1958
+ it("#4401 the lane posts FORGE:FACTS from result.json facts, its validation and changed files; never quotable as a requirement", async () => {
1959
+ const w = world({ result: { status: "fixed", summary: "s", validation: [{ id: "V1", command: "node --test test/foo.test.js", exit: 0 }],
1960
+ facts: { rejected: [{ approach: "detection-only guard", why: "hides the cause" }], hotspots: [{ at: "src/foo.js:3", why: "null path" }] } } });
1961
+ assert.equal((await run(w)).terminalReason, "merged");
1962
+ const f = parseFactsComment(w.comments.find((c) => markerOf(c) === "FORGE:FACTS"));
1963
+ assert.deepEqual(f.rejected, [{ approach: "detection-only guard", why: "hides the cause" }]);
1964
+ assert.deepEqual(f.baselines, [{ command: "node --test test/foo.test.js", exit: 0 }]);
1965
+ assert.ok(f.files.length > 0 && !f.files.some((x) => x.startsWith(".forge-lean/")), JSON.stringify(f.files));
1966
+ assert.ok(!scopeText({ title: "t", body: "b" }, [{ body: w.comments.find((c) => markerOf(c) === "FORGE:FACTS") }]).includes("detection-only"));
1967
+ });
1968
+
1744
1969
  it("missing stage files in a real run are rendered from result.json and noted as anomalies", async () => {
1745
1970
  const w = world({ noStages: true, result: { status: "fixed", summary: "fixed foo", evidence: ["src/foo.js:3 x"], root_cause: "bad at src/foo.js:3", changes: [{ file: "src/foo.js", change: "guard", why: "w" }], tests: [{ command: "t", result: "pass", fails_without_fix: false }] } });
1746
1971
  await run(w);
1747
- assert.deepEqual(w.comments.map(markerOf), ["FORGE:STARTED", "FORGE:INVESTIGATION", "FORGE:CONTRACT", "FORGE:IMPLEMENTATION", "FORGE:REVIEW", "FORGE:TRAJECTORY"]);
1972
+ assert.deepEqual(w.comments.map(markerOf), ["FORGE:STARTED", "FORGE:INVESTIGATION", "FORGE:CONTRACT", "FORGE:IMPLEMENTATION", "FORGE:REVIEW", "FORGE:FACTS", "FORGE:TRAJECTORY"]);
1748
1973
  assert.match(w.comments[1], /Verdict\*\*: CONFIRMED[\s\S]*Root Cause\nnot reported/, "the plan call has no result.json: a placeholder, still posted");
1749
1974
  assert.match(w.comments[3], /fails without fix: no/);
1750
1975
  assert.match(traj(w)[0], /investigation.md missing or empty: rendered from result.json/);
@@ -1844,6 +2069,10 @@ describe("prompts", () => {
1844
2069
  it("the conformance reviewer asks for rows, not an opinion, and stays within budget", () => {
1845
2070
  assert.ok(approxTokens(REVIEWER_SYSTEM) <= 1500);
1846
2071
  for (const k of ["issue_coverage", "requirements", "scope", "preserve", "regressions", "engine computes the verdict", "reply without rows is rejected", "untrusted data"]) assert.ok(REVIEWER_SYSTEM.includes(k), k);
2072
+ // #4404: out-of-band requirements are judged by their instrumentation, in the reviewer, the shared rubric and the plan prompt
2073
+ assert.match(REVIEWER_SYSTEM, /out_of_band row[\s\S]*instrumented: true only when its budget test or bench is committed/);
2074
+ assert.match(REVIEW_RUBRIC, /out-of-band requirement is delivered by its instrumentation/);
2075
+ assert.match(builderPlanPrompt(7), /"verify":"out_of_band" with "evidence" naming the job or workflow/);
1847
2076
  assert.doesNotMatch(REVIEWER_SYSTEM, /Does this change fix the issue as described\?/);
1848
2077
  });
1849
2078
 
@@ -1959,6 +2188,27 @@ describe("orchestrate", () => {
1959
2188
  assert.deepEqual(rows.map((r) => [r.issue, r.outcome]), [[1, "merged"], [2, "merged"], [9, "merged"]]);
1960
2189
  });
1961
2190
 
2191
+ it("#4387 an out-of-set dependency labelled needs-human is not pulled in; the issue and its dependents end skipped", async () => {
2192
+ const log = [], asked = [];
2193
+ const rows = await orchestrate({ issues: [1, 2], parallel: 2,
2194
+ readBody: async (n) => (n === 2 ? "**Depends on**: #1" : ""),
2195
+ issueLabels: async (n) => { asked.push(n); return n === 9 ? ["bug", "needs-human"] : []; },
2196
+ runOne: async (n) => { log.push(n); return n === 1 ? { terminalReason: "dependency-open", dependencies: [9], detail: "depends on #9" } : { terminalReason: "merged" }; } });
2197
+ assert.deepEqual(log, [1], "no lane for #9 and none for the dependent #2");
2198
+ assert.deepEqual(asked, [9]);
2199
+ assert.deepEqual(rows.map((r) => [r.issue, r.outcome]), [[1, "skipped"], [2, "skipped"]]);
2200
+ assert.equal(rows[0].detail, "dependency #9 needs-human");
2201
+ assert.match(rows[1].detail, /#1/);
2202
+ });
2203
+
2204
+ it("#4387 a dependency that already ended needs-human in this batch is not run again", async () => {
2205
+ const log = [];
2206
+ const rows = await orchestrate({ issues: [9, 1], parallel: 1, readBody: async () => "",
2207
+ runOne: async (n) => { log.push(n); return n === 9 ? { terminalReason: "needs-human", detail: "ci.red" } : { terminalReason: "dependency-open", dependencies: [9] }; } });
2208
+ assert.deepEqual(log, [9, 1]);
2209
+ assert.deepEqual(rows.map((r) => [r.issue, r.outcome, r.detail]), [[9, "needs-human", "ci.red"], [1, "skipped", "dependency #9 needs-human"]]);
2210
+ });
2211
+
1962
2212
  it("#4124 a repeated dependency-open stops after the requeue cap", async () => {
1963
2213
  const h = harness({}, { 1: "dependency-open" });
1964
2214
  const clock = { t: 0 }, starts = [];
@@ -2101,6 +2351,24 @@ describe("orchestrate", () => {
2101
2351
  assert.equal(res.ok, true);
2102
2352
  });
2103
2353
 
2354
+ it("#4361 a preflight GC that ran out of budget resumes at the next admission interval even off the ceiling", async () => {
2355
+ const leanIo = { gh: async (a) => (a[0] === "repo" ? "main\n" : a.includes("milestone") ? JSON.stringify({ milestone: null }) : JSON.stringify({ body: "" })),
2356
+ git: async (a) => (a[0] === "ls-remote" && a.at(-1) === "refs/heads/staging" ? "abc\tref\n" : "") };
2357
+ const gcCalls = [], logs = [];
2358
+ let t = 0;
2359
+ const deferred = [5, 0];
2360
+ const l = console.log; console.log = (s) => logs.push(String(s));
2361
+ try {
2362
+ await orchestrateFromCli(["1", "2", "3", "--parallel", "1", "--repo", "o/r", "--no-prefilter"], { dir: tmpDir("core-gc-"), batchRoot: tmpDir("core-gc-batch-"), allowTmpfs: true, emitTelemetry: async () => {}, io: { gh: async () => "o/r\n" }, leanIo,
2363
+ leanModule: { runCore: async (o) => { t += 6 * 60 * 1000; return { terminalReason: "merged", pr: o.issue, cost_usd: 0, wall_ms: 1 }; } },
2364
+ gc: async ({ phase }) => { gcCalls.push(phase); return { reclaimed: 1, deferred: deferred.shift() ?? 0, bytes: 0 }; },
2365
+ host: { loadAvg1: () => 0, cores: () => 4, memAvailableMb: () => 64000 }, now: () => t, hygiene: { auto_gc: true, max_host_load_ratio: 0.8 } });
2366
+ } finally { console.log = l; }
2367
+ // preflight deferred 5 items: the next due admission finishes them; once nothing is pending, no more GC off the ceiling
2368
+ assert.deepEqual(gcCalls, ["preflight", "admission"]);
2369
+ assert.ok(logs.includes("gc (preflight): 5 item(s) deferred to the next admission"), logs.join("\n"));
2370
+ });
2371
+
2104
2372
  it("#4125 a lane-refused issue starts no run and its dependents are skipped, not reported as a cycle", async () => {
2105
2373
  const runs = [];
2106
2374
  const rows = await orchestrate({ issues: [1, 2, 3], parallel: 2, readBody: async (n) => (n === 2 ? "**Depends on**: #1" : ""),
@@ -2212,7 +2480,9 @@ describe("orchestrate maintenance budget (#4197)", () => {
2212
2480
  await orchestrateFromCli(["1", "--repo", "o/r", "--no-prefilter"], { ...mk(), maintenance: null });
2213
2481
  assert.deepEqual(ran, [1]);
2214
2482
  // No maintenance scan without the budget; labels (host priority, #4191) ride on the body call, never a call of their own.
2215
- assert.ok(!calls.some((c) => c.startsWith("issue list") || c.endsWith("--json labels")), calls.join("\n"));
2483
+ // #4399: the batch-start acceptance-probe sweep (`forge-probe` search) is not a maintenance scan.
2484
+ assert.ok(calls.some((c) => c.startsWith("issue list") && c.includes("forge-probe")), calls.join("\n"));
2485
+ assert.ok(!calls.some((c) => (c.startsWith("issue list") && !c.includes("forge-probe")) || c.endsWith("--json labels")), calls.join("\n"));
2216
2486
  ran.length = 0; calls.length = 0;
2217
2487
  const res = await orchestrateFromCli(["1", "--repo", "o/r", "--no-prefilter"], { ...mk(), maintenance: { share: 0.5, whenIdle: true, labels: ["maintenance"] } });
2218
2488
  assert.deepEqual(ran, [1, 9]);
@@ -2239,6 +2509,22 @@ describe("runCliBackendAsync", () => {
2239
2509
  assert.equal(a.sessionId, "s-1"); assert.equal(a.text, "hello"); assert.equal(a.costUsd, 0.25);
2240
2510
  });
2241
2511
 
2512
+ it("#4390: contextMax stops a stream session whose turn context reaches it, with aggregated usage and turns", async () => {
2513
+ const { writeFileSync, chmodSync } = await import("node:fs");
2514
+ const { runCliBackendAsync } = await import("../runner.mjs");
2515
+ const d = tmpDir("core-async-");
2516
+ const line = (id, ctx) => JSON.stringify({ type: "assistant", message: { id, content: [{ type: "tool_use", name: "Bash", input: { command: "ls" } }], usage: { input_tokens: 10, output_tokens: 7, cache_read_input_tokens: ctx, cache_creation_input_tokens: 0 } } });
2517
+ writeFileSync(`${d}/fake`, `#!/bin/sh\necho '${line("m1", 500)}'\necho '${line("m1", 500)}'\necho '${line("m2", 2000)}'\nexec sleep 30\n`); chmodSync(`${d}/fake`, 0o755);
2518
+ const t0 = Date.now();
2519
+ await assert.rejects(runCliBackendAsync({ spec: { name: "t" }, userMessage: "go", args: ["1"], cwd: d, bin: `${d}/fake`, logger: { log() {} }, stream: true, contextMax: 1000 }), (e) => {
2520
+ assert.equal(e.code, "CLI_CONTEXT"); assert.equal(e.context, 2010); assert.equal(e.turns, 2, "lines of one message count once");
2521
+ assert.deepEqual(e.usage, { input_tokens: 20, output_tokens: 14, cache_creation_input_tokens: 0, cache_read_input_tokens: 2500 });
2522
+ assert.equal(e.lastTool.name, "Bash");
2523
+ return true;
2524
+ });
2525
+ assert.ok(Date.now() - t0 < 15000, "killed, not waited out");
2526
+ });
2527
+
2242
2528
  it("surfaces a failing CLI as the same CLI_BACKEND_FAILED error", async () => {
2243
2529
  const { writeFileSync, chmodSync } = await import("node:fs");
2244
2530
  const { runCliBackendAsync } = await import("../runner.mjs");
@@ -2355,6 +2641,58 @@ describe("base-sync (#4100)", () => {
2355
2641
  assert.ok(w.edits.some((e) => e.includes("needs-human")));
2356
2642
  });
2357
2643
 
2644
+ /** #4406: io.postMergeChecks reports the scoped typecheck; `tsc` fails `fails` times on the merged tree, then passes. */
2645
+ const pmWorld = (fails, o = {}) => {
2646
+ const w = world({ prView: [CONFLICTING, OKSTATE], mergeResults: ["clean"], reviews: [ok(), ok()], ...o });
2647
+ w.pmRuns = [];
2648
+ w.io.postMergeChecks = async (x) => { w.pmArgs = x; return [{ kind: "typecheck", command: "tsc -p apps/web", cwd: ".", mode: "scoped" }, { kind: "lint", command: "eslint .", cwd: ".", mode: "scoped" }]; };
2649
+ w.io.runCommand = async ({ command }) => {
2650
+ if (command !== "tsc -p apps/web") return { rc: 0, output: "" };
2651
+ w.pmRuns.push(w.calls.length);
2652
+ return w.pmRuns.length <= fails ? { rc: 2, output: "apps/web/__tests__/cross-tenant-actions.test.ts(41,3): error TS1117: An object literal cannot have multiple properties with the same name." } : { rc: 0, output: "" };
2653
+ };
2654
+ return w;
2655
+ };
2656
+
2657
+ it("#4406 a base-sync whose merged tree fails typecheck resumes the builder with the errors before pushing", async () => {
2658
+ const w = pmWorld(1);
2659
+ const r = await run(w);
2660
+ assert.equal(r.terminalReason, "merged");
2661
+ assert.equal(w.pmRuns.length, 2, "typecheck on the merged tree, then again after the fix");
2662
+ const fix = w.calls.findIndex((c) => c[0] === "runner" && c[1] === "builder" && /TS1117/.test(c[4]));
2663
+ assert.ok(fix > 0, "builder resumed with the typecheck errors");
2664
+ assert.match(w.calls[fix][4], /After merging the base into this branch[\s\S]*tsc -p apps\/web/);
2665
+ const pushes = w.calls.map((c, i) => [c, i]).filter(([c]) => c[0] === "git" && c[1] === "push" && !c.includes("--delete")).map(([, i]) => i);
2666
+ assert.equal(pushes.length, 2, "initial push plus the sync push");
2667
+ assert.ok(fix < pushes[1] && w.pmRuns[0] < pushes[1], "fixed before the base-sync push");
2668
+ assert.equal(reviewers(w).length, 2, "the post-merge fix is re-reviewed");
2669
+ assert.match(syncComment(w), /builder fixed post-merge check failures[\s\S]*Tests run:\*\* tsc -p apps\/web: pass/);
2670
+ assert.equal(w.pmArgs.cwd, WT);
2671
+ noRebaseNoForce(w);
2672
+ });
2673
+
2674
+ it("#4406 a passing post-merge typecheck keeps the clean-merge path: no builder, no re-review; ci mode runs no host typecheck", async () => {
2675
+ let w = pmWorld(0);
2676
+ assert.equal((await run(w)).terminalReason, "merged");
2677
+ assert.equal(w.pmRuns.length, 1);
2678
+ assert.equal(builders(w).length, 2);
2679
+ assert.equal(reviewers(w).length, 1);
2680
+ w = pmWorld(1);
2681
+ await run(w, { verify: { mode: "ci" } }).catch(() => null);
2682
+ assert.equal(w.pmRuns.length, 0, "ci mode: the scoped typecheck is left to CI");
2683
+ w = pmWorld(9);
2684
+ await run(w, { verify: { postMerge: "make check" } });
2685
+ assert.equal(w.pmRuns.length, 0, "verify.post_merge replaces the scoped typecheck");
2686
+ });
2687
+
2688
+ it("#4406 a merged tree that still fails after verify.max_rounds is an anomaly; the sync still pushes", async () => {
2689
+ const w = pmWorld(9, { reviews: [ok(), ok(), ok()] });
2690
+ const r = await run(w, { verify: { maxRounds: 1 } });
2691
+ assert.equal(r.terminalReason, "merged");
2692
+ assert.equal(w.pmRuns.length, 2);
2693
+ assert.match(traj(w)[0], /Post-merge check\(s\) still fail after merging origin\/staging/);
2694
+ });
2695
+
2358
2696
  describe("#4266 PR turns conflicting during the CI wait", () => {
2359
2697
  const syncCount = (w) => gitCalls(w).filter((x) => x[0] === "merge" && x[1] === "--no-edit").length;
2360
2698
  /** required check `build` reports nothing (never started) until a base-sync merged, then `then`. */
@@ -2828,12 +3166,13 @@ describe("batch robustness (#4112-#4115)", () => {
2828
3166
  assert.equal(r.heavyQueueMax, 4);
2829
3167
  });
2830
3168
 
2831
- it("#4331 pre-PR checks: scoped by the test map, concurrent up to FORGE_HOST_PARALLELISM under one heavy slot, timed", async () => {
3169
+ it("#4331/#4344 pre-PR checks: scoped by the test map, concurrent up to FORGE_HOST_PARALLELISM with one heavy slot and a worker share each, timed", async () => {
2832
3170
  const w = heavyWorld(); w.changed = ["commands/foo.md"];
2833
3171
  w.baseFiles = { "origin/staging:.forge/test-map.json": JSON.stringify({ version: 1, entries: [
2834
3172
  { globs: ["commands/**"], command: "bash scripts/check-a.sh", cwd: "." }, { globs: ["bin/**"], command: "bash scripts/check-b.sh", cwd: "." }] }) };
2835
3173
  const ran = []; let live = 0, peak = 0;
2836
- w.io.runCommand = async ({ command }) => { ran.push(command); peak = Math.max(peak, ++live); await new Promise((r) => setImmediate(r)); w.clock += 1000; live--; return { rc: 0, output: "" }; };
3174
+ const envs = [];
3175
+ w.io.runCommand = async ({ command, env }) => { ran.push(command); envs.push(env.FORGE_HOST_PARALLELISM); peak = Math.max(peak, ++live); await new Promise((r) => setImmediate(r)); w.clock += 1000; live--; return { rc: 0, output: "" }; };
2837
3176
  const prev = process.env.FORGE_HOST_PARALLELISM; process.env.FORGE_HOST_PARALLELISM = "2";
2838
3177
  const dir = tmpDir("core-");
2839
3178
  let r;
@@ -2841,9 +3180,10 @@ describe("batch robustness (#4112-#4115)", () => {
2841
3180
  finally { if (prev === undefined) delete process.env.FORGE_HOST_PARALLELISM; else process.env.FORGE_HOST_PARALLELISM = prev; }
2842
3181
  assert.deepEqual(ran, ["bash scripts/check-a.sh", "make lint", "make fmt"], "mapped check-b misses commands/foo.md; unmapped ones run");
2843
3182
  assert.equal(peak, 2);
2844
- assert.equal(w.acq.filter((a) => a.phase === "precheck").length, 1, "one heavy slot for the round");
3183
+ assert.equal(w.acq.filter((a) => a.phase === "precheck").length, 3, "one heavy slot per command");
3184
+ assert.deepEqual(envs, ["1", "1", "1"], "N=2 shared by 2 concurrent commands, not multiplied");
2845
3185
  const [pre] = readLog(dir, 7).filter((e) => e.event === "PRECHECK_COMMANDS");
2846
- assert.equal(pre.parallelism, 2); assert.ok(pre.ms > 0); assert.ok(pre.results.every((x) => Number.isFinite(x.ms)));
3186
+ assert.equal(pre.parallelism, 2); assert.equal(pre.workers, 1); assert.ok(pre.ms > 0); assert.ok(pre.results.every((x) => Number.isFinite(x.ms)));
2847
3187
  assert.ok(r.precheckMs > 0); assert.equal(r.precheckDeferred, 0);
2848
3188
  });
2849
3189
 
@@ -3065,6 +3405,84 @@ describe("batch robustness (#4112-#4115)", () => {
3065
3405
  }
3066
3406
  });
3067
3407
 
3408
+ // ---- #4405 ----
3409
+ const HEAVY_CHILD = (d) => `printf '#!/bin/sh\\nsleep 1.5\\n' > ${d}/vitest; chmod +x ${d}/vitest`;
3410
+ it("#4405 a silent session with a running heavy-step child is not idle-killed within the extended window", async () => {
3411
+ const { runCliBackendAsync } = await import("../runner.mjs");
3412
+ const { isHeavyProcess } = await import("./heavy.mjs");
3413
+ const d0 = tmpDir("core-cli-");
3414
+ const { d, bin } = await fakeCli("waits", `${TOOL}\n${HEAVY_CHILD(d0)}\n${d0}/vitest run\n${RESULT}`);
3415
+ const extended = [];
3416
+ const busy = (children) => { const work = children.filter((c) => isHeavyProcess(c.cmd)); return work.length ? { children: work, heavyQueue: { queued: 0, running: 1 } } : null; };
3417
+ const r = await runCliBackendAsync({ spec: { name: "t" }, userMessage: "go", args: ["1"], cwd: d, bin, logger: { log() {} }, stream: true, idleMs: 300, maxMs: 20000,
3418
+ busy, busyMs: 10000, onIdleExtend: (i) => extended.push(i) });
3419
+ assert.equal(r.text, "all done");
3420
+ assert.equal(extended.length, 1, "the extension is reported once per silent stretch");
3421
+ assert.ok(extended[0].children.some((c) => /vitest run/.test(c.cmd)), JSON.stringify(extended[0]));
3422
+ });
3423
+
3424
+ it("#4405 a busy session past idle + busy window is killed as CLI_IDLE with extended and heavyQueue", async () => {
3425
+ const { runCliBackendAsync } = await import("../runner.mjs");
3426
+ const { d, bin } = await fakeCli("stuck", `${TOOL}\nsleep 30 &\nwait`);
3427
+ const t0 = Date.now();
3428
+ await assert.rejects(runCliBackendAsync({ spec: { name: "t" }, userMessage: "go", args: ["1"], cwd: d, bin, logger: { log() {} }, stream: true, idleMs: 300, maxMs: 20000,
3429
+ busy: () => ({ children: [], heavyQueue: { queued: 1, running: 0 } }), busyMs: 600 }), (e) => {
3430
+ assert.equal(e.code, "CLI_IDLE"); assert.equal(e.extended, true); assert.deepEqual(e.heavyQueue, { queued: 1, running: 0 });
3431
+ return true;
3432
+ });
3433
+ const took = Date.now() - t0;
3434
+ assert.ok(took >= 850 && took < 8000, `killed after idle + busy window, not before (${took} ms)`);
3435
+ });
3436
+
3437
+ it("#4405 the CLI_TIMEOUT ceiling still applies during an idle extension", async () => {
3438
+ const { runCliBackendAsync } = await import("../runner.mjs");
3439
+ const { d, bin } = await fakeCli("busyceil", `${TOOL}\nsleep 30 &\nwait`);
3440
+ const t0 = Date.now();
3441
+ await assert.rejects(runCliBackendAsync({ spec: { name: "t" }, userMessage: "go", args: ["1"], cwd: d, bin, logger: { log() {} }, stream: true, idleMs: 300, maxMs: 1200,
3442
+ busy: () => ({ children: [], heavyQueue: { queued: 0, running: 1 } }), busyMs: 60000 }), (e) => { assert.equal(e.code, "CLI_TIMEOUT", e.message); return true; });
3443
+ assert.ok(Date.now() - t0 < 6000, "killed at maxMs, not after idle + busy window");
3444
+ });
3445
+
3446
+ it("#4405 a silent session with no work children is still killed at the idle timeout when busy is wired", async () => {
3447
+ const { runCliBackendAsync } = await import("../runner.mjs");
3448
+ const { isHeavyProcess } = await import("./heavy.mjs");
3449
+ const { d, bin } = await fakeCli("idle2", `${TOOL}\nsleep 30 &\nwait`);
3450
+ const t0 = Date.now(); let asked = 0;
3451
+ await assert.rejects(runCliBackendAsync({ spec: { name: "t" }, userMessage: "go", args: ["1"], cwd: d, bin, logger: { log() {} }, stream: true, idleMs: 400, maxMs: 20000,
3452
+ busy: (ch) => { asked++; return ch.some((c) => isHeavyProcess(c.cmd)) ? { children: ch } : null; }, busyMs: 60000 }), (e) => {
3453
+ assert.equal(e.code, "CLI_IDLE"); assert.equal(e.extended, false); assert.equal(e.heavyQueue, null);
3454
+ assert.ok(e.children.some((c) => /sleep 30/.test(c.cmd)));
3455
+ return true;
3456
+ });
3457
+ assert.ok(asked >= 1); assert.ok(Date.now() - t0 < 3000, "killed at the idle timeout, not the busy window");
3458
+ });
3459
+
3460
+ it("#4405 the builder call wires busy (heavy children or this session's heavy-queue entries) and the kill event carries heavyQueue", async () => {
3461
+ const w = world({ dirty: true }); const dir = tmpDir("core-busy-");
3462
+ const lanes = [];
3463
+ w.io.heavy = { acquire: async () => ({ waitMs: 0, queued: 0, release() {} }), lane: (pids) => { lanes.push(pids); return pids.includes(42) ? { queued: 1, running: 0 } : { queued: 0, running: 0 }; } };
3464
+ const orig = w.io.runner; let n = 0, seen = null;
3465
+ w.io.runner = async (c) => {
3466
+ if (c.role === "builder" && n++ === 1) {
3467
+ seen = c;
3468
+ c.onIdleExtend(c.busy([{ pid: 42, cmd: "node heavy.mjs run --name npm -- npm test" }]));
3469
+ throw Object.assign(new Error("idle"), { code: "CLI_IDLE", lastTool: null, children: [], extended: true, heavyQueue: { queued: 1, running: 0 } });
3470
+ }
3471
+ return orig(c);
3472
+ };
3473
+ const r = await run(w, { dir, cliBusyMinutes: 30 });
3474
+ assert.equal(r.terminalReason, "merged");
3475
+ assert.equal(seen.busyMs, 30 * 60000);
3476
+ assert.equal(seen.busy([{ pid: 5, cmd: "sleep 10" }]), null, "a poller alone is not activity");
3477
+ assert.deepEqual(seen.busy([{ pid: 42, cmd: "sleep 10" }]), { children: [], heavyQueue: { queued: 1, running: 0 } }, "this session's queued heavy entry is activity");
3478
+ assert.equal(seen.busy([{ pid: 6, cmd: "/bin/sh /x/vitest run" }]).children.length, 1, "a test runner child is activity");
3479
+ assert.deepEqual(lanes.at(-1), [6]);
3480
+ const log = readLog(dir, 7);
3481
+ assert.deepEqual(log.find((e) => e.event === "CLI_IDLE_EXTENDED").heavyQueue, { queued: 1, running: 0 });
3482
+ const kill = log.find((e) => e.event === "CLI_IDLE_KILL");
3483
+ assert.deepEqual(kill.heavyQueue, { queued: 1, running: 0 }); assert.equal(kill.extended, true);
3484
+ });
3485
+
3068
3486
  it("#4112 a rerun resumes from the checkpoint branch and tells the builder to continue", async () => {
3069
3487
  const w = world({ prList: [], remote: "abc\trefs/heads/feat/issue-7-lean", lastSubject: "wip(forge): checkpoint after idle timeout" });
3070
3488
  const r = await run(w);
@@ -3350,7 +3768,7 @@ describe("verify-by-CI (#4167): ci mode runs the heavy checks on CI and feeds fa
3350
3768
  const r = await run(w, { verify: CI });
3351
3769
  assert.equal(r.terminalReason, "merged");
3352
3770
  const fix = builders(w).find((b) => /CI failed on the pushed branch/.test(b[4]));
3353
- assert.ok(fix, "a CI-fix resume ran"); assert.equal(fix[2], "sess-1");
3771
+ assert.ok(fix, "a CI-fix round ran"); assert.equal(fix[2], null);
3354
3772
  assert.match(fix[4], /poetry\.lock is not up to date/); assert.match(fix[4], /actions\/runs\/100/);
3355
3773
  assert.deepEqual(w.ci.logsFor, [100]);
3356
3774
  assert.equal(w.ci.dispatched.length, 2, "the fixed head is verified again");
@@ -3405,8 +3823,47 @@ describe("verify-by-CI (#4167): ci mode runs the heavy checks on CI and feeds fa
3405
3823
  const d = tmpDir("verify-yaml-");
3406
3824
  writeFileSync(`${d}/forge.yaml`, "engine:\n conformance: shadow\n");
3407
3825
  assert.equal(readVerify(d), undefined);
3408
- writeFileSync(`${d}/forge.yaml`, "verify:\n mode: ci\n workflow: verify.yml\n skip_paths: \"*.md,docs/**\"\n max_rounds: 3\n precheck_timeout_seconds: 60\n");
3409
- assert.deepEqual(readVerify(d), { mode: "ci", workflow: "verify.yml", pathsInput: null, skipPaths: ["*.md", "docs/**"], maxRounds: 3, precheckTimeoutSeconds: 60, precheckTotalSeconds: 600, checkRun: false });
3826
+ writeFileSync(`${d}/forge.yaml`, "verify:\n mode: ci\n workflow: verify.yml\n skip_paths: \"*.md,docs/**\"\n max_rounds: 3\n precheck_timeout_seconds: 60\n e2e_label: full-ci\n e2e_checks: \"e2e (1),e2e (2)\"\n");
3827
+ const e2e = { mode: "auto", label: "full-ci", workflow: null, pathsInput: null, checks: ["e2e (1)", "e2e (2)"], paths: ["e2e/**", "**/e2e/**", "*.e2e.*", "*.e2e-spec.*", "cypress/**", "playwright/**"], blocking: "changed" };
3828
+ assert.deepEqual(readVerify(d), { mode: "ci", workflow: "verify.yml", pathsInput: null, skipPaths: ["*.md", "docs/**"], maxRounds: 3, precheckTimeoutSeconds: 60, precheckTotalSeconds: 600, checkRun: false, checks: null, postMerge: null, duplicateExports: true, e2e });
3829
+ });
3830
+
3831
+ describe("#4387 a no-commit CI fix round reruns the failed jobs once", () => {
3832
+ const PUSH = { mode: "ci", skipPaths: ["*.md"], maxRounds: 2 };
3833
+ const RUN = "https://github.com/o/r/actions/runs/77/job/1";
3834
+ /** Commit checks: `test` red (id 1) until a rerun, then `after` (id 2); a non-required storybook stays red; the builder never commits a CI fix. */
3835
+ const flaky = (after) => {
3836
+ const w = world({ validation: ciValidation });
3837
+ w.reran = [];
3838
+ w.io.requiredChecks = async () => ["test"];
3839
+ w.io.commitChecks = async () => [{ name: "storybook (web)", state: "failure", id: 3, url: "https://github.com/o/r/actions/runs/78/job/1" },
3840
+ w.reran.length ? { name: "test", state: after, id: 4, url: RUN } : { name: "test", state: "failure", id: 1, url: RUN }];
3841
+ w.io.failedLogs = async () => "expected 26, got 25";
3842
+ w.io.rerunFailedJobs = async (id) => { w.reran.push(id); };
3843
+ w.io.checkStates = async () => [{ name: "test", state: after, url: RUN, head: w.head }];
3844
+ const inner = w.io.runner;
3845
+ w.io.runner = async (c) => { const h = w.head; const res = await inner(c); if (/CI failed on the pushed branch/.test(c.prompt)) w.head = h; return res; };
3846
+ return w;
3847
+ };
3848
+ const flakeEvents = (dir) => readLog(dir, 7).filter((e) => e.event === "CI_FLAKE_RERUN");
3849
+
3850
+ it("green after the rerun: the lane continues to review and merges; CI_FLAKE_RERUN is logged", async () => {
3851
+ const w = flaky("success"), dir = tmpDir("core-ci-");
3852
+ const r = await run(w, { verify: PUSH, dir });
3853
+ assert.equal(r.terminalReason, "merged");
3854
+ assert.deepEqual(w.reran, ["77"], "only the required check's run is rerun, once");
3855
+ assert.ok(reviewers(w).length >= 1);
3856
+ assert.deepEqual(flakeEvents(dir).map((e) => [e.result, e.jobs]), [["pass", ["test"]]]);
3857
+ assert.match(traj(w)[0], /after a rerun of the failed jobs/);
3858
+ });
3859
+
3860
+ it("still red after the rerun: ci.red as before, with CI_FLAKE_RERUN in the run log", async () => {
3861
+ const w = flaky("failure"), dir = tmpDir("core-ci-");
3862
+ const r = await run(w, { verify: PUSH, dir });
3863
+ assert.equal(r.terminalReason, "needs-human"); assert.equal(r.code, "ci.red");
3864
+ assert.deepEqual(w.reran, ["77"]); assert.equal(w.merges.length, 0);
3865
+ assert.deepEqual(flakeEvents(dir).map((e) => e.result), ["fail"]);
3866
+ });
3410
3867
  });
3411
3868
  });
3412
3869
 
@@ -3420,7 +3877,7 @@ describe("lane resource ledger + teardown on every exit path (#4179)", () => {
3420
3877
  w.io.docker = async (a) => { w.docker.push(a); return ""; };
3421
3878
  w.io.killGroup = async (pid, marker) => { w.killed.push([pid, marker]); return true; };
3422
3879
  w.io.runTmp = { prune: async () => {}, path: (id) => `/home/x/.forge/tmp/runs/${id}`, make: async (_i, id) => `/home/x/.forge/tmp/runs/${id}`, remove: async (d) => { w.tmpRemoved.push(d); return true; } };
3423
- w.ledgerFile = join(root, o.batch ?? "adhoc", "lean-7.json");
3880
+ w.ledgerFile = join(root, o.batch ?? "adhoc", "o__r__lean-7.json"); // #4432: repo-scoped lane file
3424
3881
  const r0 = w.io.runner;
3425
3882
  w.io.runner = async (c) => {
3426
3883
  if (c.role === "builder") {
@@ -3554,6 +4011,35 @@ describe("lane resource ledger + teardown on every exit path (#4179)", () => {
3554
4011
  });
3555
4012
  });
3556
4013
 
4014
+ describe("two repos with the same issue number on one host (#4432)", () => {
4015
+ /** A lane of `repo` whose ledger lives under the shared `root` (paths recorded) and whose FORGE:STATE holds `lease`. */
4016
+ const lane = (root, lease) => {
4017
+ const w = world(); w.ledgerPaths = []; w.leaseWrites = [];
4018
+ w.io.ledger = (opts) => { const l = makeLedger({ root, ...opts }); w.ledgerPaths.push(l.path); return l; };
4019
+ w.io.projector = { readState: async () => (lease ? { v: 1, lease } : null), writeState: async (_i, st) => { w.leaseWrites.push(st); } };
4020
+ return w;
4021
+ };
4022
+
4023
+ it("separate run logs and ledger files; a live lease in one repo never defers the other", async () => {
4024
+ const dir = tmpDir("core-shared-"), root = tmpDir("ledger-shared-");
4025
+ const held = lane(root, { by: "core_9999", until: 1e15, host: "other-host", pid: 9999 });
4026
+ const r0 = await run(held, { repo: "acme/one", dir, hostName: "h1" });
4027
+ assert.equal(r0.terminalReason, "deferred");
4028
+ assert.equal(r0.code, "lease-held");
4029
+ const two = lane(root, null);
4030
+ assert.equal((await run(two, { repo: "acme/two", dir, hostName: "h1" })).terminalReason, "merged", "repo one's lease is not repo two's");
4031
+ const one = lane(root, null);
4032
+ assert.equal((await run(one, { repo: "acme/one", dir, hostName: "h1" })).terminalReason, "merged");
4033
+ assert.deepEqual(readdirSync(dir).filter((f) => f.endsWith(".jsonl")).sort(), ["acme__one__7.jsonl", "acme__two__7.jsonl"]);
4034
+ for (const repo of ["acme/one", "acme/two"]) {
4035
+ const log = readLog(dir, 7, repo);
4036
+ assert.deepEqual(log.filter((e) => e.event === "RUN_START").map((e) => e.repo), [repo]);
4037
+ assert.equal(log.filter((e) => e.event === "RUN_TERMINAL").length, 1);
4038
+ }
4039
+ assert.deepEqual([one.ledgerPaths, two.ledgerPaths], [[join(root, "adhoc", "acme__one__lean-7.json")], [join(root, "adhoc", "acme__two__lean-7.json")]]);
4040
+ });
4041
+ });
4042
+
3557
4043
  describe("host-aware verification concurrency (#4182)", () => {
3558
4044
  const host = (o = {}) => ({ memAvailableMb: () => 8192, freeMb: async () => 8192, loadAvg1: () => o.load ?? 1, cores: () => o.cores ?? 4, activeLanes: o.activeLanes ?? (() => o.lanes ?? 2), tmpPaths: () => ["/tmp"] });
3559
4045
  const withEnv = async (v, fn) => { const prev = process.env.FORGE_HOST_PARALLELISM; if (v === undefined) delete process.env.FORGE_HOST_PARALLELISM; else process.env.FORGE_HOST_PARALLELISM = v;
@@ -3618,7 +4104,7 @@ describe("shared dependency cache wiring (#4220)", () => {
3618
4104
  const d = { calls: [] };
3619
4105
  w.io.deps = {
3620
4106
  env: () => { if (o.envThrows) throw new Error("boom"); return { UV_CACHE_DIR: "/c/stores/uv-cache", npm_config_store_dir: "/c/stores/pnpm-store", TMPDIR: "/should-not-win" }; },
3621
- prepare: async (a) => { d.calls.push({ ...a, runnerCallsBefore: w.calls.filter((c) => c[0] === "runner").length, worktreeAdded: w.calls.some((c) => c[1] === "worktree" && c[2] === "add") });
4107
+ prepare: async (a) => { d.calls.push({ ...a, toolchainCalled: !!w.toolchainCalled, runnerCallsBefore: w.calls.filter((c) => c[0] === "runner").length, worktreeAdded: w.calls.some((c) => c[1] === "worktree" && c[2] === "add") });
3622
4108
  if (o.prepareThrows) throw new Error("disk on fire");
3623
4109
  return o.results ?? [{ status: "hit", eco: "pnpm", key: "0123456789abcdef", ms: 1200, reason: null }]; },
3624
4110
  };
@@ -3661,6 +4147,24 @@ describe("shared dependency cache wiring (#4220)", () => {
3661
4147
  for (const e of w.envs) assert.equal(e.UV_CACHE_DIR, undefined);
3662
4148
  });
3663
4149
 
4150
+ it("#4347 prepare runs after the toolchain step with its env; a failed or shimless toolchain step leaves the host PATH and the run merges", async () => {
4151
+ const cases = [
4152
+ [async () => { throw new Error("toolchain boom"); }, undefined],
4153
+ [async () => ({ env: null, summary: "pnpm@9.12.0" }), undefined],
4154
+ [async () => ({ env: { PATH: "/shim/bin:/host/bin", COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" }, summary: "pnpm@9.12.0" }), "/shim/bin:/host/bin"],
4155
+ ];
4156
+ for (const [tc, path] of cases) {
4157
+ const dir = tmpDir("deps-tc-");
4158
+ const w = world(); const d = depsIo(w);
4159
+ w.io.prepareToolchain = async (a) => { w.toolchainCalled = true; return tc(a); };
4160
+ assert.equal((await run(w, { dir })).terminalReason, "merged");
4161
+ assert.equal(d.calls.length, 1);
4162
+ assert.equal(d.calls[0].toolchainCalled, true, "toolchain prepared before dependency prep");
4163
+ assert.equal(d.calls[0].env?.PATH, path);
4164
+ if (!path) assert.equal(d.calls[0].env, undefined);
4165
+ }
4166
+ });
4167
+
3664
4168
  it("#4220 both builder prompts tell the builder how to treat a shared read-only node_modules/.venv (within budget)", () => {
3665
4169
  for (const p of [builderPlanPrompt(7), builderImplementPrompt()]) {
3666
4170
  for (const needle of ["hardlinked read-only from a shared cache", "rm -rf", "EACCES"]) assert.ok(p.includes(needle), needle);
@@ -3687,7 +4191,7 @@ describe("local-mode CI fix rounds at merge and declared pre-PR check commands (
3687
4191
  const r = await run(w, { dir });
3688
4192
  assert.equal(r.terminalReason, "merged");
3689
4193
  const fix = ciFixes(w);
3690
- assert.equal(fix.length, 1); assert.equal(fix[0][2], "sess-1");
4194
+ assert.equal(fix.length, 1); assert.equal(fix[0][2], null);
3691
4195
  assert.match(fix[0][4], /mkdtempSync, not tmpDir/); assert.match(fix[0][4], /actions\/runs\/555/);
3692
4196
  assert.deepEqual(w.logsFor, ["555"]);
3693
4197
  assert.equal(reviewers(w).length, 2, "the fixed head is reviewed again before merge");
@@ -3714,7 +4218,7 @@ describe("local-mode CI fix rounds at merge and declared pre-PR check commands (
3714
4218
  w.io.runner = async (c) => { const h = w.head; const res = await inner(c); if (/CI failed on the pushed branch/.test(c.prompt)) w.head = h; return res; };
3715
4219
  const r = await run(w, { verify: { maxRounds: 5 } });
3716
4220
  assert.equal(r.terminalReason, "needs-human"); assert.equal(r.code, "ci.red");
3717
- assert.equal(ciFixes(w).length, 1); assert.equal(w.merges.length, 0);
4221
+ assert.equal(ciFixes(w).length, 2, "#4363: fresh, then the one fallback resume, in one round"); assert.equal(w.merges.length, 0);
3718
4222
  });
3719
4223
 
3720
4224
  it("a rollup still reporting the pre-fix head counts as pending, not as a second failure", async () => {
@@ -3741,8 +4245,8 @@ describe("local-mode CI fix rounds at merge and declared pre-PR check commands (
3741
4245
  assert.deepEqual(ran.map((x) => x.slice(0, 2)), [["bash scripts/check-test-tmp-cleanup.sh", WT], ["bash scripts/check-test-tmp-cleanup.sh", WT]]);
3742
4246
  assert.equal(ran[0][2], 300000);
3743
4247
  const fix = builders(w).filter((b) => /declared check commands failed/.test(b[4]));
3744
- assert.equal(fix.length, 1); assert.equal(fix[0][2], "sess-1"); assert.match(fix[0][4], /mkdtempSync in x\.test\.mjs/); assert.match(fix[0][4], /exit 1/);
3745
- assert.ok(w.seq.indexOf("builder-resume", w.seq.indexOf("check:1")) < w.seq.indexOf("pr"), "the fix runs before the PR is opened");
4248
+ assert.equal(fix.length, 1); assert.equal(fix[0][2], null); assert.match(fix[0][4], /mkdtempSync in x\.test\.mjs/); assert.match(fix[0][4], /exit 1/);
4249
+ assert.ok(w.seq.indexOf("builder", w.seq.indexOf("check:1")) < w.seq.indexOf("pr"), "the fix runs before the PR is opened");
3746
4250
  const log = readLog(dir, 7);
3747
4251
  const pre = log.filter((e) => e.event === "PRECHECK_COMMANDS");
3748
4252
  assert.deepEqual(pre.map((e) => e.results), [[{ command: "bash scripts/check-test-tmp-cleanup.sh", rc: 1, ms: 0 }], [{ command: "bash scripts/check-test-tmp-cleanup.sh", rc: 0, ms: 0 }]]);
@@ -3765,7 +4269,7 @@ describe("local-mode CI fix rounds at merge and declared pre-PR check commands (
3765
4269
  w.io.runner = async (c) => { const h = w.head; const res = await inner(c); if (/declared check commands failed/.test(c.prompt)) w.head = h; return res; };
3766
4270
  const r = await run(w, { precheck: { commands: ["make lint"] }, verify: { maxRounds: 3 } });
3767
4271
  assert.equal(r.terminalReason, "merged");
3768
- assert.equal(builders(w).filter((b) => /declared check commands failed/.test(b[4])).length, 1);
4272
+ assert.equal(builders(w).filter((b) => /declared check commands failed/.test(b[4])).length, 2, "#4363: fresh, then one fallback resume");
3769
4273
  assert.match(traj(w)[0], /builder made no new commit for them/);
3770
4274
  });
3771
4275
 
@@ -3780,6 +4284,39 @@ describe("local-mode CI fix rounds at merge and declared pre-PR check commands (
3780
4284
  });
3781
4285
  });
3782
4286
 
4287
+ describe("#4364 in-session forgedock check: plan hand-off and self-check record", () => {
4288
+ const SELF = `${WT}/.forge-lean/self-check.json`;
4289
+ const fp = (w) => worktreeFingerprint({ git: (a) => w.io.git(a, { cwd: WT }), base: "origin/staging" });
4290
+
4291
+ it("writes check-plan.json beside the contract and the implement prompt asks for forgedock check", async () => {
4292
+ const w = world(); w.io.runCommand = async () => ({ rc: 0, output: "" });
4293
+ assert.equal((await run(w, { precheck: { commands: [" make lint ", ""], maxMinutes: 5 }, verify: { mode: "hybrid" } })).terminalReason, "merged");
4294
+ assert.deepEqual(JSON.parse(w.files[`${WT}/.forge-lean/check-plan.json`]), { v: 1, base: "origin/staging", mode: "hybrid", learned: ["make lint"], maxMinutes: 5, precheckTotalSeconds: 600, precheckTimeoutSeconds: 300 });
4295
+ assert.match(builders(w)[1][4], /forgedock\.mjs check` in the worktree[\s\S]*report the problem as blocked/);
4296
+ });
4297
+
4298
+ it("fixed without a forgedock check run logs SELF_CHECK_MISSING skipped and still merges (not a failure)", async () => {
4299
+ const w = world(); const dir = tmpDir("core-");
4300
+ assert.equal((await run(w, { dir })).terminalReason, "merged");
4301
+ assert.deepEqual(readLog(dir, 7).filter((e) => e.event === "SELF_CHECK_MISSING").map((e) => e.state), ["skipped"]);
4302
+ });
4303
+
4304
+ it("a green run on the final tree logs nothing; a record of another tree is stale on SELF_CHECK_MISSING and a failed pre-PR round", async () => {
4305
+ const w = world(); const dir = tmpDir("core-");
4306
+ w.files[SELF] = JSON.stringify({ v: 1, fingerprint: await fp(w), ok: true });
4307
+ assert.equal((await run(w, { dir })).terminalReason, "merged");
4308
+ assert.equal(readLog(dir, 7).filter((e) => e.event === "SELF_CHECK_MISSING").length, 0);
4309
+ const w2 = world(); const dir2 = tmpDir("core-");
4310
+ w2.files[SELF] = JSON.stringify({ v: 1, fingerprint: "other-tree", ok: true });
4311
+ let rc = 1; w2.io.runCommand = async () => ({ rc, output: "boom" });
4312
+ const inner = w2.io.runner; w2.io.runner = async (c) => { if (/declared check commands failed/.test(c.prompt)) rc = 0; return inner(c); };
4313
+ assert.equal((await run(w2, { dir: dir2, precheck: { commands: ["make lint"] } })).terminalReason, "merged");
4314
+ const log = readLog(dir2, 7);
4315
+ assert.deepEqual(log.filter((e) => e.event === "SELF_CHECK_MISSING").map((e) => e.state), ["stale"]);
4316
+ assert.deepEqual(log.filter((e) => e.event === "PRECHECK_COMMANDS").map((e) => [e.results[0].rc, e.self_check]), [[1, "stale"], [0, undefined]]);
4317
+ });
4318
+ });
4319
+
3783
4320
  describe("test map and repo facts caches (#4272)", () => {
3784
4321
  const MAPF = "origin/staging:.forge/test-map.json", FACTSF = `${WT}/.forge/repo-facts.json`;
3785
4322
  const map = (entries) => JSON.stringify({ version: 1, entries });
@@ -4282,6 +4819,22 @@ describe("context pack modes (forge#4275)", () => {
4282
4819
  assert.deepEqual([m.leaseWrites.at(-1).terminalCode, m.leaseWrites.at(-1).failureClasses], [OUTCOME_CODES.COMPLETED, []]);
4283
4820
  });
4284
4821
 
4822
+ it("#4444 ab: the lane runs its issue's arm, logs it as context_pack and tags context_ab", async () => {
4823
+ const { contextArm } = await import("./context.mjs");
4824
+ const arm = contextArm({ mode: "ab", repo: "o/r", issue: 7 }).mode;
4825
+ const w = withPack(), dir = tmpDir("core-");
4826
+ const r = await run(w, { dir, contextPack: { mode: "ab" } });
4827
+ assert.equal(r.terminalReason, "merged");
4828
+ const plan = builders(w)[0][4];
4829
+ assert.equal(/Context pack \(hints, not proof/.test(plan), arm === "on");
4830
+ const log = readLog(dir, 7);
4831
+ const cp = log.find((e) => e.event === "CONTEXT_PACK"), term = log.find((e) => e.event === "RUN_TERMINAL");
4832
+ assert.deepEqual([cp.mode, cp.context_ab, term.context_pack, term.context_ab], [arm, true, arm, true]);
4833
+ const plain = withPack(), d2 = tmpDir("core-");
4834
+ await run(plain, { dir: d2, contextPack: { mode: "shadow" } });
4835
+ assert.equal("context_ab" in readLog(d2, 7).find((e) => e.event === "RUN_TERMINAL"), false);
4836
+ });
4837
+
4285
4838
  it("forge.yaml engine.context_pack* is read flat; invalid mode is ignored", () => {
4286
4839
  const d = tmpDir("ctxcfg-");
4287
4840
  writeFileSync(join(d, "forge.yaml"), "engine:\n context_pack: on\n context_pack_sources: graph,attempts\n context_pack_bytes: 4096\n");
@@ -4367,6 +4920,29 @@ describe("enumerating tests + sequence numbers at hand-off (#4322)", () => {
4367
4920
  return w;
4368
4921
  };
4369
4922
 
4923
+ const DUP = [{ symbol: "undoActivityAction", path: "apps/web/feed/actions.ts", others: ["apps/web/inbox/actions.ts"] }];
4924
+ it("#4406 a lane export that another module of the same app declares resumes the builder with both paths", async () => {
4925
+ const w = hworld(); const d = [DUP, []]; w.dupCalls = 0;
4926
+ w.io.duplicateExports = async (x) => { w.dupCalls++; w.dupArgs = x; return d.length > 1 ? d.shift() : d[0]; };
4927
+ assert.equal((await run(w)).terminalReason, "merged");
4928
+ const b = builders(w);
4929
+ assert.equal(b.length, 3);
4930
+ assert.match(b[2][4], /undoActivityAction: apps\/web\/feed\/actions\.ts \(also exported by apps\/web\/inbox\/actions\.ts\)[\s\S]*Rename the new export or reuse the existing one/);
4931
+ assert.ok(w.seq.indexOf("pr") > w.seq.lastIndexOf("builder-resume"), "fixed before the PR");
4932
+ assert.equal(w.dupCalls, 2);
4933
+ assert.equal(w.dupArgs.cwd, WT);
4934
+ });
4935
+
4936
+ it("#4406 a duplicate that remains after the round is an anomaly; verify.duplicate_exports false skips the check", async () => {
4937
+ let w = hworld(); w.io.duplicateExports = async () => DUP;
4938
+ assert.equal((await run(w)).terminalReason, "merged");
4939
+ assert.match(traj(w)[0], /Duplicate export\(s\) remain \(hand-off\): undoActivityAction in apps\/web\/feed\/actions\.ts/);
4940
+ w = hworld(); let called = 0; w.io.duplicateExports = async () => { called++; return DUP; };
4941
+ await run(w, { verify: { duplicateExports: false } });
4942
+ assert.equal(called, 0);
4943
+ assert.equal(builders(w).length, 2);
4944
+ });
4945
+
4370
4946
  it("no enumerating tests and no collisions: no new plan step, no extra builder call (prompts unchanged)", async () => {
4371
4947
  const w = hworld();
4372
4948
  assert.equal((await run(w)).terminalReason, "merged");
@@ -4421,4 +4997,1017 @@ describe("enumerating tests + sequence numbers at hand-off (#4322)", () => {
4421
4997
  assert.equal((await run(w)).terminalReason, "merged");
4422
4998
  assert.match(builders(w)[2][4], /conflicts in: db\/meta\/_journal\.json[\s\S]*do not text-merge it\. Renumber the lane's files/);
4423
4999
  });
5000
+
5001
+ describe("#4386 deterministic renumber, base-sync and merge-gate late binding", () => {
5002
+ const MOVES = [{ from: "db/0007_usage.sql", to: "db/0008_usage.sql" }];
5003
+ const syncs = (w) => w.calls.filter((c) => c[0] === "git" && c[1] === "merge" && c[2] === "--no-edit").length;
5004
+ /** `det`: sequenceRenumber results per call; `resolve`: sequenceResolve result; `mine`: sequenceSnapshot (the lane's numbered files). */
5005
+ const dworld = ({ det = [{ ok: true, moves: MOVES, commit: "d".repeat(40) }], resolve = { ok: true }, mine = [{ path: "db/0007_usage.sql", blob: "b1" }], ...o } = {}) => {
5006
+ const w = hworld(o), d = [...det];
5007
+ w.renumbers = []; w.leases = { acquired: [], released: 0 };
5008
+ w.io.sequenceRenumber = async (x) => { w.renumbers.push(x); w.head = `dddd${w.calls.length}`.padEnd(16, "0"); return d.length > 1 ? d.shift() : d[0]; };
5009
+ w.io.sequenceResolve = async (x) => { w.resolveArgs = x; w.conflictFiles = []; w.mergeInProgress = false; return resolve; };
5010
+ w.io.sequenceSnapshot = async () => mine;
5011
+ w.io.sequenceLease = { acquire: async (x) => { w.leases.acquired.push(x); return { waitMs: 0, timedOut: false, release: () => { w.leases.released++; } }; } };
5012
+ return w;
5013
+ };
5014
+
5015
+ it("hand-off: a taken number is renumbered by the engine; no builder session, the trajectory says so", async () => {
5016
+ const w = dworld({ seq: [COLL, []] });
5017
+ assert.equal((await run(w)).terminalReason, "merged");
5018
+ assert.equal(builders(w).length, 2, "plan + implement only");
5019
+ assert.equal(w.renumbers.length, 1);
5020
+ assert.deepEqual(w.verifyArgs.moves, MOVES);
5021
+ assert.match(traj(w).at(-1), /renumbered deterministically \(no builder\): db\/0007_usage\.sql → db\/0008_usage\.sql/);
5022
+ });
5023
+
5024
+ it("base-sync with conflicts only in sequence files: base side taken and renumbered by the engine, zero builder sessions, zero re-reviews", async () => {
5025
+ const w = dworld({ seq: [[], COLL, []], prView: [CONFLICTING, OKSTATE], mergeResults: ["conflict"], conflictFiles: ["db/meta/_journal.json", "db/meta/0007_snapshot.json"] });
5026
+ assert.equal((await run(w)).terminalReason, "merged");
5027
+ assert.equal(builders(w).length, 2, "no builder for the conflict or the renumber");
5028
+ assert.equal(reviewers(w).length, 1, "no re-review");
5029
+ assert.deepEqual(w.resolveArgs.files, ["db/meta/_journal.json", "db/meta/0007_snapshot.json"]);
5030
+ assert.ok(w.renumbers[0].laneRef, "renumbered from the pre-merge lane head");
5031
+ assert.match(w.comments.find((c) => c.includes("<!-- FORGE:BASE_SYNC -->")), /deterministic: base side taken[\s\S]*renumbered deterministically[\s\S]*not required \(deterministic renumber \+ base merge\)/);
5032
+ });
5033
+
5034
+ it("a schema-changing migration without lean.sequence.regenerate falls back to the builder renumber and the trajectory records why", async () => {
5035
+ const w = dworld({ seq: [COLL, []], det: [{ unsupported: "schema-changing migration db/0007_usage.sql needs lean.sequence.regenerate (its snapshot cannot be cloned)" }] });
5036
+ assert.equal((await run(w, { sequenceRegenerate: null })).terminalReason, "merged");
5037
+ assert.equal(builders(w).length, 3, "the builder renumbers");
5038
+ assert.match(builders(w)[2][4], /already uses sequence numbers/);
5039
+ assert.match(traj(w).at(-1), /Deterministic renumber not possible \(schema-changing migration db\/0007_usage\.sql needs lean\.sequence\.regenerate/);
5040
+ assert.equal(w.renumbers[0].regenerate, null);
5041
+ });
5042
+
5043
+ it("syncs overtaken by a new base head do not count toward MAX_SYNCS: overtaken 3 times still merges; 2 syncs against one base head is base.conflict", async () => {
5044
+ let w = dworld({ prView: [CONFLICTING, CONFLICTING, CONFLICTING, CONFLICTING, CONFLICTING, OKSTATE], mergeResults: ["clean", "clean", "clean"], baseHeads: ["base0", "base1", "base2", "base3"], mine: [] }); // the loop re-reads mergeability after each sync
5045
+ assert.equal((await run(w)).terminalReason, "merged");
5046
+ assert.equal(syncs(w), 3);
5047
+ w = dworld({ prView: [CONFLICTING], mergeResults: ["clean", "clean", "clean"], baseHeads: ["base1"], mine: [] });
5048
+ const r = await run(w);
5049
+ assert.equal(r.code, OUTCOME_CODES.BASE_CONFLICT);
5050
+ assert.equal(syncs(w), 2);
5051
+ });
5052
+
5053
+ it("merge gate: under the sequence lease a number taken since review is renumbered, CI waited again, merged; a lane without numbered files never takes the lease", async () => {
5054
+ let w = dworld({ seq: [[], COLL, COLL, []] }); // hand-off clean; the late check and the gate's base-sync see the taken number
5055
+ assert.equal((await run(w)).terminalReason, "merged");
5056
+ assert.equal(w.leases.acquired.length, 1);
5057
+ assert.deepEqual(w.leases.acquired[0].dirs, ["db"]);
5058
+ assert.equal(w.leases.acquired[0].lane, "staging");
5059
+ assert.equal(w.renumbers.length, 1, "late renumber at the gate");
5060
+ assert.equal(builders(w).length, 2);
5061
+ assert.equal(reviewers(w).length, 1, "no re-review for the late renumber");
5062
+ assert.match(w.comments.find((c) => c.includes("<!-- FORGE:BASE_SYNC -->")), /merge gate: sequence number taken/);
5063
+ assert.ok(w.leases.released >= 1);
5064
+ assert.deepEqual(w.merges[0].slice(-2), ["--match-head-commit", w.head]);
5065
+ w = dworld({ mine: [] });
5066
+ assert.equal((await run(w)).terminalReason, "merged");
5067
+ assert.equal(w.leases.acquired.length, 0);
5068
+ });
5069
+ });
5070
+ });
5071
+
5072
+ describe("#4363 fresh fix sessions", () => {
5073
+ const FRESH = /no earlier session/;
5074
+ const prechecked = (o = {}) => {
5075
+ const w = world(o); let rc = 1;
5076
+ w.io.runCommand = async () => ({ rc, output: rc ? `${"x".repeat(300000)}\nlint: src/foo.js:3 unused var` : "" });
5077
+ const inner = w.io.runner;
5078
+ w.io.runner = async (c) => { const res = await inner(c); if (/declared check commands failed/.test(c.prompt) && !o.freshNoCommit) rc = 0; return res; };
5079
+ return w;
5080
+ };
5081
+ const usage = (dir) => readLog(dir, 7).filter((e) => e.event === "PHASE_ATTEMPT_USAGE" && e.phase === "implement");
5082
+
5083
+ it("a precheck failure runs a fresh session: no resume id, the reviewer model by default, a prompt under 60K with no transcript; plan/build keep the builder model", async () => {
5084
+ const w = prechecked(); const dir = tmpDir("core-");
5085
+ const r = await run(w, { dir, builderModel: "opus-b", builderRouting: { mode: "off" }, precheck: { commands: ["make lint"] } });
5086
+ assert.equal(r.terminalReason, "merged");
5087
+ const fix = builders(w).filter((b) => /declared check commands failed/.test(b[4]));
5088
+ assert.equal(fix.length, 1);
5089
+ assert.equal(fix[0][2], null, "no resume id"); assert.equal(fix[0][3], "claude-sonnet-5-5");
5090
+ assert.ok(fix[0][4].length < 60000, `prompt ${fix[0][4].length} chars`);
5091
+ for (const re of [FRESH, /## Issue: foo breaks/, /Frozen contract/, /src\/foo\.js:3 unused var/]) assert.ok(re.test(fix[0][4]), String(re));
5092
+ assert.ok(!/PLANNING ONLY|The engine froze/.test(fix[0][4]), "no plan or build prompt text");
5093
+ for (const b of builders(w).filter((b) => !FRESH.test(b[4]))) assert.equal(b[3], "opus-b", "plan and build keep the builder model");
5094
+ const u = usage(dir);
5095
+ assert.deepEqual(u.map((e) => e.mode ?? null), [null, null, "fresh"]);
5096
+ });
5097
+
5098
+ it("lean.fix_model (fixModel) picks the fix-session model", async () => {
5099
+ const w = prechecked();
5100
+ await run(w, { builderModel: "opus-b", fixModel: "haiku-fix", builderRouting: { mode: "off" }, precheck: { commands: ["make lint"] } });
5101
+ assert.deepEqual(builders(w).filter((b) => FRESH.test(b[4])).map((b) => b[3]), ["haiku-fix"]);
5102
+ });
5103
+
5104
+ it("a fresh session that makes no commit falls back to one resume of the build session in the same round; max_rounds is unchanged", async () => {
5105
+ const w = prechecked({ freshNoCommit: true }); const dir = tmpDir("core-");
5106
+ const inner = w.io.runner;
5107
+ w.io.runner = async (c) => { const h = w.head; const res = await inner(c); if (/declared check commands failed/.test(c.prompt)) w.head = h; return res; };
5108
+ const r = await run(w, { dir, precheck: { commands: ["make lint"] }, verify: { maxRounds: 3 } });
5109
+ assert.equal(r.terminalReason, "merged");
5110
+ const fix = builders(w).filter((b) => /declared check commands failed/.test(b[4]));
5111
+ assert.deepEqual(fix.map((b) => b[2]), [null, "sess-1"], "fresh, then exactly one resume");
5112
+ assert.doesNotMatch(fix[1][4], FRESH, "the resume gets the plain task prompt");
5113
+ assert.deepEqual(usage(dir).map((e) => e.mode ?? null), [null, null, "fresh", "resume"]);
5114
+ assert.match(traj(w)[0], /Fresh precheck fix session made no commit; builder session resumed once/);
5115
+ assert.match(traj(w)[0], /builder made no new commit for them/);
5116
+ });
5117
+
5118
+ it("a conformance FAIL citing file:line is fresh; one without a file:line resumes; the one review retry is unchanged", async () => {
5119
+ const w1 = world({ reviews: [bad("src/foo.js:9 still wrong"), ok()] }); const d1 = tmpDir("core-");
5120
+ assert.equal((await run(w1, { dir: d1 })).terminalReason, "merged");
5121
+ assert.deepEqual(usage(d1).map((e) => e.mode ?? null), [null, null, "fresh"]);
5122
+ const vague = conf({ verdict: "NONCONFORMING", requirements: [{ id: "R1", status: "partial", evidence: "the foo module", validated_by: "V1", note: "null still throws" }] });
5123
+ const w2 = world({ reviews: [vague, ok()] }); const d2 = tmpDir("core-");
5124
+ assert.equal((await run(w2, { dir: d2 })).terminalReason, "merged");
5125
+ assert.equal(builders(w2)[2][2], "sess-1");
5126
+ assert.deepEqual(usage(d2).map((e) => e.mode ?? null), [null, null, "resume"]);
5127
+ const w3 = world({ reviews: [bad(), bad(), bad()] });
5128
+ const r3 = await run(w3);
5129
+ assert.equal(r3.terminalReason, "needs-human"); assert.equal(reviewers(w3).length, 2, "still one review retry");
5130
+ });
5131
+ });
5132
+
5133
+ describe("#4374 builder self-review against the shared rubric", () => {
5134
+ const SR = [1, 2, 3, 4].map((n) => ({ question: String(n), answer: "yes", evidence: "src/foo.js:3" }));
5135
+ const fixed = { status: "fixed", summary: "fixed foo", evidence: [], tests: ["test/foo.test.js"], self_review: SR };
5136
+ const selfEvents = (dir) => readLog(dir, 7).filter((e) => /^SELF_REVIEW/.test(e.event));
5137
+
5138
+ it("one rubric constant, verbatim in reviewerPrompt and in the implement prompt with the git diff self-check", () => {
5139
+ assert.ok(REVIEW_RUBRIC.length > 0);
5140
+ assert.ok(reviewerPrompt({ issue: { number: 7, title: "t", body: "b" }, contract: CONTRACT, diff: "" }).includes(`<rubric>\n${REVIEW_RUBRIC}\n</rubric>`));
5141
+ for (const mode of ["local", "ci", "hybrid"]) {
5142
+ const p = builderImplementPrompt(mode);
5143
+ assert.ok(p.includes(`<rubric>\n${REVIEW_RUBRIC}\n</rubric>`), mode);
5144
+ assert.match(p, /Last step before writing \.forge-lean\/result\.json: self-review[\s\S]*git diff <base>\.\.\.HEAD[\s\S]*evidence for each[\s\S]*fix any "no" in this session/);
5145
+ assert.match(p, /"self_review":\[\{"question":"1","answer":"yes\|no","evidence":""\}\]/);
5146
+ assert.ok(p.indexOf("<rubric>") < p.indexOf("Finally write"), "the self-review comes before the result");
5147
+ }
5148
+ });
5149
+
5150
+ it("a fixed result without self_review is logged SELF_REVIEW_MISSING, never a failure or an extra runner call", async () => {
5151
+ const dir = tmpDir("selfrev-"), w = world({ reviews: [ok()] });
5152
+ assert.equal((await run(w, { dir })).terminalReason, "merged");
5153
+ assert.equal(builders(w).length, 2); assert.equal(reviewers(w).length, 1);
5154
+ assert.deepEqual(selfEvents(dir).map((e) => [e.event, e.state]), [["SELF_REVIEW_MISSING", "missing"]]);
5155
+ });
5156
+
5157
+ it("the run log records the self-review and whether the first reviewer judgement agreed with it", async () => {
5158
+ const dir = tmpDir("selfrev-"), w = world({ result: fixed, reviews: [ok()] });
5159
+ assert.equal((await run(w, { dir })).terminalReason, "merged");
5160
+ assert.equal(builders(w).length, 2); assert.equal(reviewers(w).length, 1);
5161
+ assert.deepEqual(selfEvents(dir).map((e) => e.event), ["SELF_REVIEW", "SELF_REVIEW_AGREED"]);
5162
+ const dir2 = tmpDir("selfrev-"), w2 = world({ result: fixed, resumeResult: fixed, reviews: [bad(), ok()] });
5163
+ await run(w2, { dir: dir2 });
5164
+ const ev2 = selfEvents(dir2);
5165
+ assert.deepEqual(ev2.map((e) => e.event), ["SELF_REVIEW", "SELF_REVIEW_DISAGREED"], "compared once, on the first judgement");
5166
+ assert.equal(ev2[1].reviewer, "nonconforming"); assert.deepEqual(ev2[1].self_no, []);
5167
+ });
5168
+ });
5169
+
5170
+ describe("#4362 attempt triggers", () => {
5171
+ const usage = (dir) => readLog(dir, 7).filter((e) => e.event === "PHASE_ATTEMPT_USAGE");
5172
+
5173
+ it("every builder/fixRound/attempt call site in lean.mjs passes a string-literal trigger from TRIGGERS", async () => {
5174
+ const { TRIGGERS } = await import("./fixround.mjs");
5175
+ const src = readFileSync(new URL("./lean.mjs", import.meta.url), "utf8");
5176
+ const lines = src.split("\n");
5177
+ const at = (re) => lines.map((l, i) => [l, i + 1]).filter(([l]) => re.test(l));
5178
+ const builderSites = at(/builder\(\+\+attemptN,/), fixSites = at(/fixRound\("/), attemptSites = at(/attempt\("(?!implement)/);
5179
+ assert.ok(builderSites.length >= 9 && fixSites.length >= 9 && attemptSites.length >= 2, "the call sites were found");
5180
+ const known = (lit, l, n) => assert.ok(lit && TRIGGERS.includes(lit), `lean.mjs:${n} passes no known trigger: ${l.trim().slice(0, 120)}`);
5181
+ const fixLines = at(/async function fixRound\(/)[0][1];
5182
+ for (const [l, n] of builderSites) {
5183
+ if (n > fixLines && n < fixLines + 10 && /trigger \}\)/.test(l)) continue; // fixRound forwards its own trigger, checked below
5184
+ known(/trigger: "([a-z_]+)"/.exec(l)?.[1], l, n);
5185
+ }
5186
+ // #4375: a fixRound call may pass the failure text after its trigger
5187
+ for (const [l, n] of fixSites) known(/\), (?:[\w.]+|\[\]), "([a-z_]+)"(?:, .*)?\);$/.exec(l)?.[1], l, n);
5188
+ for (const [l, n] of attemptSites) known(/, "([a-z_]+)"\);/.exec(l)?.[1], l, n);
5189
+ assert.match(src, /opts\.trigger = "timeout_restart"/, "a timeout restart is tagged");
5190
+ });
5191
+
5192
+ it("a precheck fix run tags every attempt (plan, build, precheck; review) and is not first pass; a clean run is", async () => {
5193
+ const w = world(); let rc = 1;
5194
+ w.io.runCommand = async () => ({ rc, output: rc ? "lint: src/foo.js:3 unused var" : "" });
5195
+ const inner = w.io.runner;
5196
+ w.io.runner = async (c) => { const res = await inner(c); if (/declared check commands failed/.test(c.prompt)) rc = 0; return res; };
5197
+ const dir = tmpDir("core-");
5198
+ const r = await run(w, { dir, precheck: { commands: ["make lint"] } });
5199
+ assert.equal(r.terminalReason, "merged");
5200
+ const u = usage(dir);
5201
+ const { TRIGGERS } = await import("./fixround.mjs");
5202
+ for (const e of u) assert.ok(TRIGGERS.includes(e.trigger), `${e.phase} attempt ${e.attempt}: ${e.trigger}`);
5203
+ assert.deepEqual(u.filter((e) => e.phase === "implement").map((e) => e.trigger), ["plan", "build", "precheck"]);
5204
+ assert.ok(u.filter((e) => e.phase === "review").every((e) => e.trigger === "review"));
5205
+ assert.equal(r.firstPass, false);
5206
+ const clean = await run(world(), { dir: tmpDir("core-") });
5207
+ assert.equal(clean.terminalReason, "merged"); assert.equal(clean.firstPass, true);
5208
+ });
5209
+
5210
+ it("an idle-killed build retries as timeout_restart (restart_of build) and still counts as reaching the build", async () => {
5211
+ const w = world({ dirty: true }); const dir = tmpDir("core-idle-");
5212
+ const orig = w.io.runner; let n = 0;
5213
+ w.io.runner = async (c) => { if (c.role === "builder" && n++ === 1) throw Object.assign(new Error("idle"), { code: "CLI_IDLE" }); return orig(c); };
5214
+ const r = await run(w, { dir });
5215
+ assert.equal(r.terminalReason, "merged");
5216
+ assert.deepEqual(usage(dir).filter((e) => e.phase === "implement").map((e) => [e.attempt, e.trigger, e.restart_of ?? null]), [[1, "plan", null], [2, "timeout_restart", "build"]]);
5217
+ assert.equal(r.firstPass, true);
5218
+ });
5219
+
5220
+ it("#4390: a build stopped at the context ceiling is checkpointed and restarted fresh from contract, diff and last validate", async () => {
5221
+ const w = world({ dirty: true }); const dir = tmpDir("core-ctx-");
5222
+ const orig = w.io.runner, seen = []; let n = 0, contractAtKill = null;
5223
+ w.io.runner = async (c) => {
5224
+ if (c.role === "builder") seen.push({ contextMax: c.contextMax ?? 0, resume: c.resume ?? null, prompt: c.prompt });
5225
+ if (c.role === "builder" && n++ === 1) {
5226
+ contractAtKill = w.files[`${WT}/.forge-lean/contract.json`];
5227
+ w.files[`${WT}/.forge-lean/self-check.json`] = '{"v":1,"ok":false,"validate":[{"id":"V1","rc":1}]}';
5228
+ throw Object.assign(new Error("ctx"), { code: "CLI_CONTEXT", context: 130000, turns: 40, usage: { input_tokens: 40, output_tokens: 900, cache_read_input_tokens: 4000000, cache_creation_input_tokens: 5000 } });
5229
+ }
5230
+ return orig(c);
5231
+ };
5232
+ const r = await run(w, { dir, contextCeilingTokens: 120000 });
5233
+ assert.equal(r.terminalReason, "merged");
5234
+ assert.equal(seen[0].contextMax, 0, "the plan call runs uncapped");
5235
+ assert.equal(seen[1].contextMax, 120000, "the build call carries the ceiling");
5236
+ const restart = seen[2];
5237
+ assert.equal(restart.resume, null, "the restart is a fresh session");
5238
+ assert.match(restart.prompt, /The engine froze \.forge-lean\/contract\.json/);
5239
+ assert.match(restart.prompt, /reached the 120000-token context ceiling/);
5240
+ assert.match(restart.prompt, /<diff>\ndiff --git a\/src\/foo\.js/, "changed hunks carried");
5241
+ assert.match(restart.prompt, /"validate":\[\{"id":"V1","rc":1\}\]/, "last validate result carried");
5242
+ assert.equal(w.files[`${WT}/.forge-lean/contract.json`], contractAtKill, "contract unchanged by the restart");
5243
+ assert.ok(w.calls.some((c) => c[0] === "git" && c[1] === "commit" && c.some((x) => /checkpoint after context ceiling/.test(x))), "dirty work checkpointed on the branch");
5244
+ assert.ok(readLog(dir, 7).some((e) => e.event === "CONTEXT_CEILING" && e.context_tokens === 130000 && e.turns === 40));
5245
+ const impl = usage(dir).filter((e) => e.phase === "implement");
5246
+ assert.deepEqual(impl.map((e) => [e.attempt, e.trigger, e.restart_of ?? null]), [[1, "plan", null], [2, "build", null], [2, "context_restart", "build"]]);
5247
+ assert.equal(impl[1].usage.cache_read_input_tokens, 4000000, "the stopped session's tokens are recorded");
5248
+ assert.equal(r.firstPass, true);
5249
+ });
5250
+
5251
+ it("#4390: context restarts are bounded; after three the call runs uncapped, and 0 disables the ceiling", async () => {
5252
+ const w = world(); const dir = tmpDir("core-ctx-");
5253
+ const orig = w.io.runner, caps = [];
5254
+ w.io.runner = async (c) => {
5255
+ if (c.role === "builder") caps.push(c.contextMax ?? 0);
5256
+ if (c.role === "builder" && c.contextMax > 0) throw Object.assign(new Error("ctx"), { code: "CLI_CONTEXT", context: 999, turns: 1, usage: null });
5257
+ return orig(c);
5258
+ };
5259
+ const r = await run(w, { dir, contextCeilingTokens: 500 });
5260
+ assert.equal(r.terminalReason, "merged");
5261
+ assert.deepEqual(caps.slice(0, 5), [0, 500, 500, 500, 0]);
5262
+ const off = world(); const offCaps = []; const o2 = off.io.runner;
5263
+ off.io.runner = async (c) => { if (c.role === "builder") offCaps.push(c.contextMax ?? 0); return o2(c); };
5264
+ assert.equal((await run(off, { dir: tmpDir("core-ctx-"), contextCeilingTokens: 0 })).terminalReason, "merged");
5265
+ assert.ok(offCaps.every((x) => x === 0));
5266
+ });
5267
+ });
5268
+
5269
+ describe("#4407 per-attempt cost deltas", () => {
5270
+ const bad1 = { ...CONTRACT, requirements: [{ id: "R1", quote: "make foo nicer", disposition: "deliver" }] };
5271
+ const usage = (dir) => readLog(dir, 7).filter((e) => e.event === "PHASE_ATTEMPT_USAGE");
5272
+ const sum = (xs) => Math.round(xs.reduce((a, x) => a + x, 0) * 1e9) / 1e9;
5273
+
5274
+ it("the deltas of a resumed builder session sum to its final total and the lane cost is their sum", async () => {
5275
+ // plan (fresh) -> contract correction (resume) -> build (resume): the CLI reports 0.1, 0.2, 0.3 for the one session
5276
+ const w = world({ contract: bad1, fixedContract: CONTRACT }); const dir = tmpDir("core-4407-");
5277
+ const r = await run(w, { dir });
5278
+ assert.equal(r.terminalReason, "merged");
5279
+ const impl = usage(dir).filter((e) => e.phase === "implement");
5280
+ assert.deepEqual(impl.map((e) => e.trigger), ["plan", "contract_correction", "build"]);
5281
+ for (const e of impl) assert.ok(Number.isFinite(e.cost_usd_delta) && Number.isFinite(e.cost_usd_session), `attempt ${e.attempt} lacks the cost fields`);
5282
+ assert.deepEqual(impl.map((e) => e.cost_usd_session), [0.1, 0.2, 0.3]);
5283
+ assert.equal(sum(impl.map((e) => e.cost_usd_delta)), impl.at(-1).cost_usd_session, "sum of deltas = final session total");
5284
+ const all = usage(dir);
5285
+ assert.equal(Math.round(r.cost_usd * 1e9) / 1e9, sum(all.map((e) => e.cost_usd_delta)), "the lane cost (batch/orchestrate rows) sums the deltas");
5286
+ assert.ok(r.cost_usd < sum(all.map((e) => e.cost_usd)), "not the sum of running totals");
5287
+ });
5288
+
5289
+ it("the trajectory cost line and the run telemetry carry the deltas", async () => {
5290
+ const w = world({ contract: bad1, fixedContract: CONTRACT });
5291
+ const r = await run(w);
5292
+ assert.equal(r.terminalReason, "merged");
5293
+ assert.ok(traj(w)[0].includes("cost $0.3500"), traj(w)[0]);
5294
+ const tel = w.telemetry.filter((e) => e.event === "PHASE_ATTEMPT_USAGE" && e.phase === "implement");
5295
+ assert.deepEqual(tel.map((e) => e.cost_usd_delta), [0.1, 0.1, 0.1]);
5296
+ });
5297
+ });
5298
+
5299
+ describe("#4392: implement turn budget", () => {
5300
+ /** The build is stopped at its turn budget; `wrap(w, put)` acts as the wrap-up session (after the normal fake). */
5301
+ const budgetWorld = (wrap, { overrun = false } = {}) => {
5302
+ const w = world({ dirty: true }); const orig = w.io.runner; w.seen = []; let stopped = false;
5303
+ w.io.runner = async (c) => {
5304
+ if (c.role === "builder") w.seen.push({ turnMax: c.turnMax ?? 0, resume: c.resume ?? null, prompt: c.prompt });
5305
+ if (c.role === "builder" && c.turnMax > 0 && !stopped) { stopped = true; throw Object.assign(new Error("turns"), { code: "CLI_TURNS", turns: c.turnMax, turnMax: c.turnMax, usage: null }); }
5306
+ if (c.role === "builder" && /Turn budget spent/.test(c.prompt)) {
5307
+ if (overrun) throw Object.assign(new Error("turns"), { code: "CLI_TURNS", turns: c.turnMax, turnMax: c.turnMax, usage: null });
5308
+ const r = await orig(c); wrap?.(w); return r;
5309
+ }
5310
+ return orig(c);
5311
+ };
5312
+ return w;
5313
+ };
5314
+ const usage = (dir) => readLog(dir, 7).filter((e) => e.event === "PHASE_ATTEMPT_USAGE");
5315
+ const TB = { S: 25, M: 60, L: 100 }; // the CLI defaults (readCoreLimits); runCore alone is unbudgeted
5316
+ const stopWith = (digest) => (w) => { w.files[`${WT}/.forge-lean/result.json`] = JSON.stringify({ status: "budget-exceeded", summary: "too big", digest }); };
5317
+
5318
+ it("an overrun ends needs-human budget.exceeded with the builder's digest, checkpointed, recoverable once", async () => {
5319
+ const w = budgetWorld(stopWith("done: parser. remaining: 3 adapters. recommend split")); const dir = tmpDir("core-tb-");
5320
+ const r = await run(w, { dir, turnBudgets: TB });
5321
+ assert.equal(r.terminalReason, "needs-human"); assert.equal(r.code, OUTCOME_CODES.BUDGET_EXCEEDED);
5322
+ assert.match(r.detail, /size S, 25 turns/); assert.match(r.detail, /remaining: 3 adapters/);
5323
+ assert.equal(classifyNeedsHuman({ outcome: r.terminalReason, code: r.code }), "recoverable");
5324
+ assert.deepEqual(r.turns, { size: "S", budget: 25, build: 28, total: 31, exceeded: true });
5325
+ assert.deepEqual(w.seen.map((x) => x.turnMax), [0, 25, 5], "plan uncapped, build gets the budget, wrap-up the grant");
5326
+ assert.equal(w.seen[2].resume, null, "the wrap-up is a fresh session");
5327
+ assert.match(w.seen[2].prompt, /The engine froze \.forge-lean\/contract\.json/); assert.match(w.seen[2].prompt, /at most 5 turns/);
5328
+ assert.ok(w.calls.some((c) => c[0] === "git" && c[1] === "commit" && c.some((x) => /checkpoint after turn budget/.test(x))), "work checkpointed");
5329
+ assert.ok(w.comments.some((c) => c.includes("FORGE:NEEDS_HUMAN") && c.includes("remaining: 3 adapters")));
5330
+ const log = readLog(dir, 7);
5331
+ assert.ok(log.some((e) => e.event === "TURN_BUDGET_EXCEEDED" && e.size === "S" && e.budget === 25 && e.turns === 25 && e.grant === 5));
5332
+ assert.deepEqual(log.find((e) => e.event === "RUN_TERMINAL").turns, r.turns);
5333
+ assert.deepEqual(usage(dir).filter((e) => e.phase === "implement").map((e) => [e.trigger, e.restart_of ?? null]), [["plan", null], ["build", null], ["budget_wrapup", "build"]]);
5334
+ });
5335
+
5336
+ it("a wrap-up that finishes flows on to merge; an overrun wrap-up stops with the diff stat as digest", async () => {
5337
+ const fin = budgetWorld(null);
5338
+ const r = await run(fin, { dir: tmpDir("core-tb-"), turnBudgets: TB });
5339
+ assert.equal(r.terminalReason, "merged"); assert.equal(r.turns.exceeded, true); assert.equal(r.firstPass, true);
5340
+ const over = budgetWorld(null, { overrun: true });
5341
+ const o2 = await run(over, { dir: tmpDir("core-tb-"), turnBudgets: TB });
5342
+ assert.equal(o2.code, OUTCOME_CODES.BUDGET_EXCEEDED); assert.match(o2.detail, /No builder digest; changes so far/);
5343
+ });
5344
+
5345
+ it("size comes from a size: label over the contract; 0 disables the budget and nothing is capped", async () => {
5346
+ const w = world({ labels: [{ name: "size:L" }] }); const caps = []; const orig = w.io.runner;
5347
+ w.io.runner = async (c) => { if (c.role === "builder") caps.push(c.turnMax ?? 0); return orig(c); };
5348
+ const r = await run(w, { dir: tmpDir("core-tb-"), turnBudgets: { L: 70 } });
5349
+ assert.equal(r.terminalReason, "merged"); assert.deepEqual(caps, [0, 70]);
5350
+ assert.deepEqual(r.turns, { size: "L", budget: 70, build: 3, total: 6, exceeded: false });
5351
+ const off = world(); const offCaps = []; const o2 = off.io.runner;
5352
+ off.io.runner = async (c) => { if (c.role === "builder") offCaps.push(c.turnMax ?? 0); return o2(c); };
5353
+ const r2 = await run(off, { dir: tmpDir("core-tb-"), turnBudgets: { S: 0, M: 0, L: 0 } });
5354
+ assert.equal(r2.terminalReason, "merged"); assert.ok(offCaps.every((x) => x === 0)); assert.equal(r2.turns.budget, 0);
5355
+ });
5356
+ });
5357
+
5358
+ describe("learned pitfalls (#4375)", () => {
5359
+ const memStore = () => { const recs = []; return { recs, read: async () => recs.map((r) => JSON.parse(JSON.stringify(r))), append: async (_repo, r) => { recs.push(r); } }; };
5360
+ const packFacts = async (a) => (await import("./context.mjs")).gatherContextFacts({ ...a, io: {}, sources: ["pitfalls"] });
5361
+ const failingPrecheck = (w, passAfterFix = true) => {
5362
+ let rc = 1;
5363
+ w.io.runCommand = async () => ({ rc, output: rc ? "> eslint .\nsrc/foo.js:3:7 error 'tmp' is assigned a value but never used no-unused-vars" : "" });
5364
+ const inner = w.io.runner;
5365
+ w.io.runner = async (c) => { const res = await inner(c); if (passAfterFix && /declared check commands failed/.test(c.prompt)) rc = 0; return res; };
5366
+ };
5367
+
5368
+ it("a precheck rework round that fixes the failure records one pitfall; the next lane's pack carries it, never posted", async () => {
5369
+ const store = memStore();
5370
+ const w = world(); w.io.pitfalls = store; failingPrecheck(w);
5371
+ const dir = tmpDir("core-pit-");
5372
+ assert.equal((await run(w, { dir, precheck: { commands: ["npm run lint"] } })).terminalReason, "merged");
5373
+ const pits = store.recs.filter((r) => r.t === "pitfall");
5374
+ assert.equal(pits.length, 1);
5375
+ assert.equal(pits[0].trigger, "precheck");
5376
+ assert.match(pits[0].sig, /^precheck:[0-9a-f]{12}$/);
5377
+ assert.deepEqual(pits[0].globs, ["src/*.js", "test/*.js"]);
5378
+ assert.match(pits[0].lesson, /^precheck: src\/foo\.js:3:7 error 'tmp' is assigned a value but never used no-unused-vars$/);
5379
+ assert.equal(store.recs.filter((r) => r.t === "lane").length, 1, "one lane row per lane");
5380
+ assert.deepEqual(readLog(dir, 7).filter((e) => e.event === "PITFALL_RECORDED").map((e) => [e.trigger, e.sig]), [["precheck", pits[0].sig]]);
5381
+
5382
+ // the next lane naming the same file gets it in its pack (mode on), logs it, and posts only the count
5383
+ const w2 = world({ issueBodies: [`${ISSUE_BODY}\n\nFix it in src/foo.js.`] }); w2.io.pitfalls = store; w2.io.contextFacts = packFacts;
5384
+ const dir2 = tmpDir("core-pit2-");
5385
+ assert.equal((await run(w2, { dir: dir2, contextPack: { mode: "on" } })).terminalReason, "merged");
5386
+ assert.match(builders(w2)[0][4], /Recurring pitfalls in this repo[\s\S]*no-unused-vars/);
5387
+ const cp = readLog(dir2, 7).find((e) => e.event === "CONTEXT_PACK");
5388
+ assert.deepEqual(cp.pitfalls, [{ sig: pits[0].sig, match: "files" }]);
5389
+ assert.equal(readLog(dir2, 7).find((e) => e.event === "RUN_TERMINAL").pitfalls_matched, 1);
5390
+ for (const c of [...w.comments, ...w2.comments]) assert.ok(!c.includes("no-unused-vars") || !c.startsWith("<!-- FORGE:CONTEXT"), "pitfall text never in FORGE:CONTEXT");
5391
+ assert.ok(!w2.comments.some((c) => c.includes("'tmp' is assigned a value")), "no posted comment carries the lesson");
5392
+ const mapped = [...w.telemetry, ...w2.telemetry].map((x) => JSON.stringify(mapRecord(x) ?? null));
5393
+ assert.ok(!mapped.some((x) => /never used|pitfall|precheck:[0-9a-f]{12}/.test(x)), "nothing pitfall-shaped reaches the FORGE:TELEMETRY record");
5394
+ });
5395
+
5396
+ it("a rework round whose gate never passes records nothing; no io.pitfalls is today's run", async () => {
5397
+ const store = memStore();
5398
+ const w = world(); w.io.pitfalls = store; failingPrecheck(w, false);
5399
+ const dir = tmpDir("core-pit3-");
5400
+ assert.equal((await run(w, { dir, precheck: { commands: ["npm run lint"] } })).terminalReason, "merged");
5401
+ assert.equal(store.recs.filter((r) => r.t === "pitfall").length, 0);
5402
+ assert.ok(!readLog(dir, 7).some((e) => e.event === "PITFALL_RECORDED"));
5403
+ const plain = world(); failingPrecheck(plain);
5404
+ const d2 = tmpDir("core-pit4-");
5405
+ assert.equal((await run(plain, { dir: d2, precheck: { commands: ["npm run lint"] } })).terminalReason, "merged");
5406
+ assert.ok(!readLog(d2, 7).some((e) => e.event === "PITFALL_RECORDED"));
5407
+ assert.equal(readLog(d2, 7).find((e) => e.event === "RUN_TERMINAL").pitfalls_matched, undefined);
5408
+ });
5409
+
5410
+ it("a ledger that throws never fails the lane", async () => {
5411
+ const w = world({ issueBodies: [`${ISSUE_BODY}\n\nsrc/foo.js`] });
5412
+ w.io.pitfalls = { read: async () => { throw new Error("EACCES"); }, append: async () => { throw new Error("EROFS"); } };
5413
+ w.io.contextFacts = packFacts; failingPrecheck(w);
5414
+ assert.equal((await run(w, { contextPack: { mode: "on" }, precheck: { commands: ["npm run lint"] } })).terminalReason, "merged");
5415
+ });
5416
+ });
5417
+
5418
+ describe("explorer subagent (#4391)", () => {
5419
+ const capture = (w) => { const seen = []; const inner = w.io.runner; w.io.runner = async (c) => { seen.push({ role: c.role, agents: c.agents ?? null }); return inner(c); }; return seen; };
5420
+
5421
+ it("on: builder calls get the explorer agent, plan/implement prompts the rule, reviewer none; RUN_TERMINAL tags explore", async () => {
5422
+ const w = world(), seen = capture(w), dir = tmpDir("core-");
5423
+ assert.equal((await run(w, { dir, explore: { mode: "on", model: "claude-haiku-5-5" } })).terminalReason, "merged");
5424
+ const b = seen.filter((s) => s.role === "builder");
5425
+ assert.ok(b.length >= 2 && b.every((s) => s.agents?.explorer?.model === "claude-haiku-5-5"));
5426
+ assert.ok(seen.filter((s) => s.role !== "builder").every((s) => s.agents === null));
5427
+ const [plan, impl] = builders(w).map((c) => c[4]);
5428
+ assert.ok(plan.startsWith(builderPlanPrompt(7)));
5429
+ assert.match(plan, /subagent_type "explorer"[\s\S]*Open a file yourself only to edit it or to verify a digest claim/);
5430
+ assert.match(impl, /subagent_type "explorer"/);
5431
+ assert.equal(readLog(dir, 7).find((e) => e.event === "RUN_TERMINAL").explore, "on");
5432
+ assert.match(traj(w).at(-1), /Explorer subagent \(on\): model claude-haiku-5-5/);
5433
+ });
5434
+
5435
+ it("off (default): no agents on any call and prompts identical to an explicit off", async () => {
5436
+ const w = world(), seen = capture(w), off = world(), dir = tmpDir("core-");
5437
+ await run(w, { dir });
5438
+ await run(off, { explore: { mode: "off" } });
5439
+ assert.ok(seen.every((s) => s.agents === null));
5440
+ assert.deepEqual(builders(w).map((c) => c[4]), builders(off).map((c) => c[4]));
5441
+ assert.ok(!builders(w).some((c) => c[4].includes("explorer")));
5442
+ assert.equal(readLog(dir, 7).find((e) => e.event === "RUN_TERMINAL").explore, "off");
5443
+ });
5444
+ });
5445
+
5446
+ describe("e2e merge gate (#4403)", () => {
5447
+ const SPECS = ["src/foo.js", "test/foo.test.js", "e2e/foo.spec.ts"];
5448
+ const RUN = "https://github.com/o/r/actions/runs/777/job/1";
5449
+ /** commitChecks: the e2e job is skipped until the PR carries the label; then `seq` is consumed (the last repeats). */
5450
+ const withE2e = (w, seq, log = "FAIL e2e/foo.spec.ts > handles null") => {
5451
+ w.labelled = () => w.prEdits.some((a) => a.includes("--add-label"));
5452
+ w.io.commitChecks = async () => {
5453
+ if (!w.labelled()) return [{ name: "e2e", state: "success", conclusion: "skipped", url: RUN }];
5454
+ const st = seq.length > 1 ? seq.shift() : seq[0];
5455
+ return [{ name: "e2e", state: "success", conclusion: "skipped", url: RUN }, { name: "e2e", state: st, conclusion: st === "pending" ? null : st, url: RUN }];
5456
+ };
5457
+ w.io.failedLogs = async () => log;
5458
+ return w;
5459
+ };
5460
+ const cfg = (extra = {}) => parseVerify({ e2e_label: "full-ci", e2e_checks: "e2e", ...extra });
5461
+ const go = (w, extra = {}) => run(w, { conformance: "shadow", verify: cfg(), ...extra });
5462
+ const e2eEvents = (dir) => readLog(dir, 7).filter((e) => e.event === "E2E_VERIFY");
5463
+
5464
+ it("#4403 repro: a lane that changes an e2e spec labels the PR, waits on the e2e check and merges only after it passes", async () => {
5465
+ const w = withE2e(world({ changed: SPECS, reviews: [conf({}, CONTRACT, SPECS)] }), ["pending", "pending", "success"]);
5466
+ const dir = tmpDir("core-");
5467
+ const r = await go(w, { dir });
5468
+ assert.equal(r.terminalReason, "merged");
5469
+ const label = w.prEdits.find((a) => a.includes("--add-label"));
5470
+ assert.ok(label, "the PR is labelled for the e2e run"); assert.equal(label[label.indexOf("--add-label") + 1], "full-ci");
5471
+ const ev = e2eEvents(dir);
5472
+ assert.deepEqual(ev.map((e) => [e.result, e.specs]), [["pass", ["e2e/foo.spec.ts"]]]);
5473
+ assert.ok(w.seq.indexOf("merge") > -1);
5474
+ });
5475
+
5476
+ it("the lane's own spec failing in e2e: builder fix round, re-review, then merged once e2e is green; no rounds left -> ci.red, nothing merged", async () => {
5477
+ const w = withE2e(world({ changed: SPECS }), ["failure", "success"]);
5478
+ const r = await go(w);
5479
+ assert.equal(r.terminalReason, "merged");
5480
+ const fix = builders(w).filter((b) => /CI failed on the pushed branch/.test(b[4]));
5481
+ assert.equal(fix.length, 1); assert.match(fix[0][4], /e2e\/foo\.spec\.ts/);
5482
+ assert.equal(reviewers(w).length, 2, "the fixed head is reviewed again");
5483
+ const w2 = withE2e(world({ changed: SPECS }), ["failure"]);
5484
+ const r2 = await go(w2, { verify: { ...cfg(), maxRounds: 0 } });
5485
+ assert.equal(r2.terminalReason, "needs-human"); assert.equal(r2.code, OUTCOME_CODES.CI_RED); assert.equal(w2.merges.length, 0);
5486
+ });
5487
+
5488
+ it("an unrelated pre-existing e2e failure is reported, not blocking; e2e_blocking: all blocks it", async () => {
5489
+ const w = withE2e(world({ changed: SPECS }), ["failure"], "FAIL e2e/rosters.spec.ts > lists rosters");
5490
+ const dir = tmpDir("core-");
5491
+ const r = await go(w, { dir });
5492
+ assert.equal(r.terminalReason, "merged");
5493
+ assert.deepEqual(e2eEvents(dir).map((e) => [e.result, e.own]), [["fail", []]]);
5494
+ assert.ok(traj(w).some((t) => /Pre-existing e2e failure\(s\) reported, not blocking: e2e/.test(t)));
5495
+ const w2 = withE2e(world({ changed: SPECS }), ["failure"], "FAIL e2e/rosters.spec.ts > lists rosters");
5496
+ const r2 = await go(w2, { verify: { ...cfg({ e2e_blocking: "all" }), maxRounds: 0 } });
5497
+ assert.equal(r2.terminalReason, "needs-human"); assert.equal(w2.merges.length, 0);
5498
+ });
5499
+
5500
+ it("verify.e2e: never (or no label/workflow configured) keeps today's behaviour: no label, no e2e wait, no event", async () => {
5501
+ for (const verify of [cfg({ e2e: "never" }), parseVerify({})]) {
5502
+ const w = withE2e(world({ changed: SPECS }), ["failure"]);
5503
+ const dir = tmpDir("core-");
5504
+ const r = await go(w, { dir, verify });
5505
+ assert.equal(r.terminalReason, "merged");
5506
+ assert.equal(w.prEdits.filter((a) => a.includes("--add-label")).length, 0);
5507
+ assert.equal(e2eEvents(dir).length, 0);
5508
+ }
5509
+ });
5510
+
5511
+ it("a lane without e2e specs or [type:e2e] criteria never runs e2e; e2e that never starts is needs-human, nothing merged", async () => {
5512
+ const w = withE2e(world(), ["failure"]);
5513
+ assert.equal((await go(w)).terminalReason, "merged");
5514
+ assert.equal(w.prEdits.filter((a) => a.includes("--add-label")).length, 0);
5515
+ const w2 = world({ changed: SPECS });
5516
+ w2.io.commitChecks = async () => [{ name: "e2e", state: "success", conclusion: "skipped", url: RUN }];
5517
+ const r2 = await go(w2);
5518
+ assert.equal(r2.terminalReason, "needs-human"); assert.equal(w2.merges.length, 0);
5519
+ });
5520
+ });
5521
+
5522
+ describe("#4397 independent adversarial verifier", () => {
5523
+ const finding = { file: "src/foo.js", line: 3, input: "foo(null)", expected: "0", actual: "throws TypeError", why: "no guard" };
5524
+ /** Verifier replies in order (an Error throws); every verifier call is recorded in w.vcalls and never reaches the reviewer queue. */
5525
+ const withVerifier = (w, replies = []) => {
5526
+ const orig = w.io.runner; w.vcalls = [];
5527
+ w.io.runner = async (c) => {
5528
+ if (c.role !== "verifier") return orig(c);
5529
+ w.vcalls.push(c);
5530
+ const r = replies.length ? replies.shift() : fence({ findings: [] });
5531
+ if (r instanceof Error) throw r;
5532
+ return { text: r, sessionId: "ver", usage: { input_tokens: 3, output_tokens: 1 }, costUsd: 0.01, durationMs: 300, turns: 2, model: c.model };
5533
+ };
5534
+ return w;
5535
+ };
5536
+ const vEvents = (dir) => readLog(dir, 7).filter((e) => e.event === "VERIFIER");
5537
+ const review = (w) => w.comments.find((c) => c.includes("<!-- FORGE:REVIEW -->"));
5538
+
5539
+ it("verifier off (default): no verifier call, no VERIFIER event, review comment unchanged", async () => {
5540
+ const w = withVerifier(world()); const dir = tmpDir("core-");
5541
+ assert.equal((await run(w, { dir })).terminalReason, "merged");
5542
+ assert.equal(w.vcalls.length, 0);
5543
+ assert.deepEqual(vEvents(dir), []);
5544
+ assert.doesNotMatch(review(w), /Independent verifier/);
5545
+ });
5546
+
5547
+ it("shadow: one fresh read-only call on a model other than the builder's, without the builder's result; findings are advisory", async () => {
5548
+ const w = withVerifier(world(), [fence({ findings: [finding, { file: "src/nope.js", line: 1 }] })]); const dir = tmpDir("core-");
5549
+ const r = await run(w, { dir, conformance: "shadow", builderModel: "claude-sonnet-5-5", verifier: { mode: "shadow" } });
5550
+ assert.equal(r.terminalReason, "merged", "shadow never blocks");
5551
+ assert.equal(w.vcalls.length, 1);
5552
+ const c = w.vcalls[0];
5553
+ assert.equal(c.resume, null); assert.equal(c.readOnly, true);
5554
+ assert.equal(c.model, "claude-opus-5-5", "the configured default is the builder's model, so it is swapped");
5555
+ assert.ok(c.maxMs > 0 && c.maxTurns > 0);
5556
+ assert.match(c.system, /BREAK/);
5557
+ assert.match(c.prompt, /foo crashes on null input\./); assert.match(c.prompt, /<contract>/); assert.match(c.prompt, /\+fix/);
5558
+ assert.doesNotMatch(c.prompt, /fixed foo|builder_result|<rubric>/, "no builder summary or rubric");
5559
+ assert.equal(builders(w).length, 2, "no fix round in shadow");
5560
+ const [e] = vEvents(dir);
5561
+ assert.equal(e.mode, "shadow"); assert.equal(e.model, "claude-opus-5-5"); assert.equal(e.swapped, true); assert.equal(e.findings, 1, "a finding at a missing path is dropped");
5562
+ assert.equal(e.cost_usd, 0.01); assert.ok(e.implement_cost_usd > 0); assert.equal(e.ratio, Math.round((0.01 / e.implement_cost_usd) * 1000) / 1000);
5563
+ assert.match(review(w), /### Independent verifier \(shadow, advisory\)[\s\S]*- `src\/foo\.js:3` input foo\(null\): throws TypeError \(expected 0\)/);
5564
+ const usage = readLog(dir, 7).filter((x) => x.event === "PHASE_ATTEMPT_USAGE" && x.phase === "verify");
5565
+ assert.equal(usage.length, 1); assert.equal(usage[0].trigger, "verifier");
5566
+ });
5567
+
5568
+ it("enforce: a finding feeds the existing conformance fix round (fresh session); the new head is verified again and merges", async () => {
5569
+ const w = withVerifier(world(), [fence({ findings: [finding] }), fence({ findings: [] })]); const dir = tmpDir("core-");
5570
+ const r = await run(w, { dir, verifier: { mode: "enforce", model: "claude-haiku-5-5" } });
5571
+ assert.equal(r.terminalReason, "merged");
5572
+ const b = builders(w);
5573
+ assert.equal(b.length, 3, "plan, implement, one fix round");
5574
+ assert.equal(b[2][2], null, "file:line findings run a fresh fix session");
5575
+ assert.match(b[2][4], /conformance\.regression: independent verifier: src\/foo\.js:3 input foo\(null\): throws TypeError/);
5576
+ assert.equal(w.vcalls.length, 2);
5577
+ assert.ok(w.vcalls.every((c) => c.model === "claude-haiku-5-5"));
5578
+ const ev = vEvents(dir);
5579
+ assert.deepEqual(ev.map((e) => [e.mode, e.findings]), [["enforce", 1], ["enforce", 0]]);
5580
+ assert.notEqual(ev[0].head, ev[1].head);
5581
+ assert.match(review(w), /### Independent verifier \(enforce\)[\s\S]*No concrete defect found\./);
5582
+ assert.ok(readLog(dir, 7).some((e) => e.event === "CONFORMANCE" && e.state === "conforms"));
5583
+ });
5584
+
5585
+ it("enforce: a head is verified once; a fix round that made no commit reuses the verdict and ends needs-human", async () => {
5586
+ const w = withVerifier(world({ freshNoCommit: true }), [fence({ findings: [finding] })]); const dir = tmpDir("core-");
5587
+ const r = await run(w, { dir, verifier: { mode: "enforce" } });
5588
+ assert.equal(r.terminalReason, "needs-human");
5589
+ assert.equal(w.vcalls.length, 1);
5590
+ assert.equal(vEvents(dir).length, 1);
5591
+ assert.equal(w.merges.length, 0);
5592
+ });
5593
+
5594
+ it("enforce with no findings merges after one verifier call; a verifier error is an anomaly, never a failure", async () => {
5595
+ const ok1 = withVerifier(world());
5596
+ assert.equal((await run(ok1, { verifier: { mode: "enforce" } })).terminalReason, "merged");
5597
+ assert.equal(ok1.vcalls.length, 1);
5598
+ assert.equal(builders(ok1).length, 2);
5599
+ const w = withVerifier(world(), [Object.assign(new Error("idle"), { code: "CLI_IDLE" })]); const dir = tmpDir("core-");
5600
+ assert.equal((await run(w, { dir, verifier: { mode: "enforce" } })).terminalReason, "merged");
5601
+ assert.equal(vEvents(dir)[0].error, "CLI_IDLE");
5602
+ assert.match(traj(w)[0], /Independent verifier failed at [0-9a-f]{12}: CLI_IDLE/);
5603
+ });
5604
+
5605
+ it("an empty diff skips the call; a failing review never reaches the verifier", async () => {
5606
+ const w = withVerifier(world()); const dir = tmpDir("core-");
5607
+ const g = w.io.git; w.io.git = async (a, o) => (a[0] === "diff" && a.length === 2 ? "" : g(a, o));
5608
+ await run(w, { dir, verifier: { mode: "shadow" } });
5609
+ assert.equal(w.vcalls.length, 0);
5610
+ assert.equal(vEvents(dir)[0].skipped, "empty_diff");
5611
+ const bad1 = withVerifier(world({ reviews: [bad(), bad()] }));
5612
+ await run(bad1, { verifier: { mode: "enforce" } });
5613
+ assert.equal(bad1.vcalls.length, 0);
5614
+ });
5615
+ });
5616
+
5617
+ describe("#4394 builder model routing", () => {
5618
+ const prechecked = () => { // the first precheck fails, the fix round makes it pass
5619
+ const w = world(); let rc = 1;
5620
+ w.io.runCommand = async () => ({ rc, output: rc ? "lint: src/foo.js:3 unused var" : "" });
5621
+ const inner = w.io.runner;
5622
+ w.io.runner = async (c) => { const res = await inner(c); if (/declared check commands failed/.test(c.prompt)) rc = 0; return res; };
5623
+ return w;
5624
+ };
5625
+ const events = (dir, name) => readLog(dir, 7).filter((e) => e.event === name);
5626
+ const isPlan = (b) => /PLANNING ONLY/.test(b[4]);
5627
+ const docsContract = { ...CONTRACT, deliverables: [{ id: "D1", path: "docs/foo.md", change: "document null", requirements: ["R1"] }, { id: "D2", path: "test/foo.test.js", change: "test", requirements: ["R1"] }] };
5628
+
5629
+ it("a docs/test-only contract builds on the mid model; the plan stays on builderModel; RUN_TERMINAL and the result carry the route", async () => {
5630
+ const w = world({ contract: docsContract }); const dir = tmpDir("core-");
5631
+ const r = await run(w, { dir, builderModel: "opus-b" });
5632
+ const b = builders(w);
5633
+ assert.ok(isPlan(b[0])); assert.equal(b[0][3], "opus-b", "the plan keeps the top model");
5634
+ assert.equal(b[1][3], "claude-sonnet-5-5", "the build runs on the mid model");
5635
+ const [route] = events(dir, "BUILDER_ROUTE");
5636
+ assert.deepEqual({ mode: route.mode, class: route.class, tier: route.tier, model: route.model }, { mode: "on", class: "docs-config-test", tier: "mid", model: "claude-sonnet-5-5" });
5637
+ const term = events(dir, "RUN_TERMINAL").at(-1);
5638
+ assert.deepEqual([term.builder_tier, term.builder_class, term.builder_escalated, term.builder_model], ["mid", "docs-config-test", false, "claude-sonnet-5-5"]);
5639
+ assert.deepEqual(r.builderRoute, { tier: "mid", class: "docs-config-test", escalated: false, model: "claude-sonnet-5-5" });
5640
+ assert.match(traj(w)[0], /Builder routed to the mid tier \(docs-config-test; claude-sonnet-5-5\)/);
5641
+ const t = w.telemetry.map((x) => mapRecord(x, 1)).find((x) => x?.event === "run_terminal");
5642
+ assert.equal(t.payload.builder_tier, "mid"); assert.equal(t.payload.builder_escalated, 0);
5643
+ });
5644
+
5645
+ it("lean.builder_mid_model picks the mid model; a declared risk path routes the build to the top model", async () => {
5646
+ const w1 = world(); await run(w1, { builderModel: "opus-b", builderRouting: { midModel: "haiku-mid" } });
5647
+ assert.deepEqual(builders(w1).slice(0, 2).map((b) => b[3]), ["opus-b", "haiku-mid"]);
5648
+ const w2 = world(); const dir = tmpDir("core-");
5649
+ await run(w2, { dir, builderModel: "opus-b", reviewTrigger: { mode: "shadow", riskPaths: ["src/**"] } });
5650
+ assert.ok(builders(w2).every((b) => b[3] === "opus-b"), "every builder call on the top model");
5651
+ const [route] = events(dir, "BUILDER_ROUTE");
5652
+ assert.equal(route.class, "risk"); assert.equal(route.tier, "top"); assert.deepEqual(route.risk, ["src/foo.js"]);
5653
+ });
5654
+
5655
+ it("a failed precheck on the mid tier escalates: BUILDER_ESCALATED, the fix round runs on the top model, RUN_TERMINAL records it", async () => {
5656
+ const w = prechecked(); const dir = tmpDir("core-");
5657
+ const r = await run(w, { dir, builderModel: "opus-b", fixModel: "haiku-fix", precheck: { commands: ["make lint"] } });
5658
+ assert.equal(r.terminalReason, "merged");
5659
+ const fix = builders(w).filter((b) => /declared check commands failed/.test(b[4]));
5660
+ assert.equal(fix.length, 1, "no extra fix round");
5661
+ assert.equal(fix[0][3], "opus-b", "the fresh fix session runs on the top model, not lean.fix_model");
5662
+ const esc = events(dir, "BUILDER_ESCALATED");
5663
+ assert.equal(esc.length, 1); assert.deepEqual([esc[0].from, esc[0].to, esc[0].trigger], ["claude-sonnet-5-5", "opus-b", "precheck"]);
5664
+ const term = events(dir, "RUN_TERMINAL").at(-1);
5665
+ assert.deepEqual([term.builder_tier, term.builder_class, term.builder_escalated, term.builder_model], ["mid", "small", true, "claude-sonnet-5-5"]);
5666
+ assert.equal(r.builderRoute.escalated, true);
5667
+ });
5668
+
5669
+ it("a conformance fix round on the mid tier does not escalate and keeps the fix model", async () => {
5670
+ const w = world({ reviews: [bad("src/foo.js:9 still wrong"), ok()] }); const dir = tmpDir("core-");
5671
+ assert.equal((await run(w, { dir, builderModel: "opus-b", fixModel: "haiku-fix" })).terminalReason, "merged");
5672
+ assert.equal(builders(w)[2][3], "haiku-fix");
5673
+ assert.deepEqual(events(dir, "BUILDER_ESCALATED"), []);
5674
+ });
5675
+
5676
+ it("builder_routing off: plan, build and the resume keep builderModel; the class is still recorded on the top tier", async () => {
5677
+ const w = world({ reviews: [conf({ verdict: "NONCONFORMING", requirements: [{ id: "R1", status: "partial", evidence: "the foo module", validated_by: "V1", note: "x" }] }), ok()] }); const dir = tmpDir("core-");
5678
+ await run(w, { dir, builderModel: "opus-b", builderRouting: { mode: "off" } });
5679
+ assert.ok(builders(w).length >= 3);
5680
+ assert.ok(builders(w).every((b) => b[3] === "opus-b"));
5681
+ const term = events(dir, "RUN_TERMINAL").at(-1);
5682
+ assert.deepEqual([term.builder_tier, term.builder_class, term.builder_model], ["top", "small", "opus-b"]);
5683
+ });
5684
+
5685
+ it("the verifier never runs on the routed mid model", async () => {
5686
+ const w = world(); w.vcalls = [];
5687
+ const orig = w.io.runner;
5688
+ w.io.runner = async (c) => {
5689
+ if (c.role !== "verifier") return orig(c);
5690
+ w.vcalls.push(c);
5691
+ return { text: fence({ findings: [] }), sessionId: "ver", usage: { input_tokens: 3, output_tokens: 1 }, costUsd: 0.01, durationMs: 300, turns: 2, model: c.model };
5692
+ };
5693
+ await run(w, { conformance: "shadow", builderModel: "claude-opus-5-5", verifier: { mode: "shadow", model: "claude-sonnet-5-5" } });
5694
+ assert.equal(builders(w)[1][3], "claude-sonnet-5-5");
5695
+ assert.equal(w.vcalls.length, 1);
5696
+ assert.notEqual(w.vcalls[0].model, "claude-sonnet-5-5");
5697
+ });
5698
+ });
5699
+
5700
+ describe("resume digest in builder prompts (#4400)", () => {
5701
+ const BOT = { author: { login: "forge[bot]" }, authorAssociation: "NONE" };
5702
+ const factsRec = `<!-- ${FACTS_MARKER} -->\n${renderFactsRecord(normaliseFacts({ validation: [{ command: "node --test test/foo.test.js", exit: 1 }] }, { files: ["src/foo.js"] }))}`;
5703
+ const needs = "<!-- FORGE:NEEDS_HUMAN -->\n## Needs human\nCI red: test/foo.test.js fails on the null case";
5704
+ const body = `${ISSUE_BODY}\n\n<!-- FORGE:STATE\n${JSON.stringify({ v: 1, issue: 7, terminal: true, terminalReason: "needs-human", terminalCode: "ci.red", failureClasses: ["ci.red"] })}\n-->`;
5705
+ const frozen = () => {
5706
+ const h = contractHash(contractJson(CONTRACT));
5707
+ return { body: `<!-- FORGE:CONTRACT -->\n<!-- FORGE:PLAN -->\n${renderContract(CONTRACT, h)}`, author_association: "OWNER", user: { type: "User", login: "op" } };
5708
+ };
5709
+ const resumedWorld = () => world({ prList: [], remote: "abc\trefs/heads/feat/issue-7-lean", issueComments: [frozen()], issueBodies: [body], viewComments: [{ body: factsRec, ...BOT }, { body: needs, ...BOT }] });
5710
+
5711
+ it("a resumed lane with a frozen contract gets files changed, the failing validate, the end reason and the requirements; RESUME_DIGEST is logged", async () => {
5712
+ const w = resumedWorld(), dir = tmpDir("lean-resume-");
5713
+ const r = await run(w, { dir });
5714
+ assert.equal(r.terminalReason, "merged");
5715
+ const p = builders(w)[0][4];
5716
+ for (const s of ["Resume digest of the last run", "- needs-human (ci.red); failures ci.red", "FORGE:NEEDS_HUMAN: CI red: test/foo.test.js fails on the null case",
5717
+ "- V1 `node --test test/foo.test.js`: exit 1 (failing)", "- R1: foo must handle a null argument without throwing.", "- src/foo.js", "Start from this state: do not re-derive it."]) assert.ok(p.includes(s), `missing ${s}\n${p}`);
5718
+ const e = readLog(dir, 7).find((x) => x.event === "RESUME_DIGEST");
5719
+ assert.ok(e && e.bytes > 0 && e.validates === 1 && e.files === 2 && e.reason === "needs-human", JSON.stringify(e));
5720
+ assert.match(traj(w)[0], /Resume digest given to the builder/);
5721
+ });
5722
+
5723
+ it("a recovery lane gets the digest after the recovery note", async () => {
5724
+ const w = resumedWorld();
5725
+ await run(w, { recover: { code: "ci.red", attempt: 1 } });
5726
+ const p = builders(w)[0][4];
5727
+ assert.ok(p.indexOf("Recovery pass (ci.red)") >= 0 && p.indexOf("Recovery pass (ci.red)") < p.indexOf("Resume digest of the last run"), p);
5728
+ });
5729
+
5730
+ it("a first run gets no digest; context_pack off gives none; the contract quotes are unchanged", async () => {
5731
+ const first = world({ issueBodies: [body], viewComments: [{ body: factsRec, ...BOT }] });
5732
+ await run(first);
5733
+ assert.ok(!builders(first).some((b) => b[4].includes("Resume digest")));
5734
+ const off = resumedWorld(), dir = tmpDir("lean-resume-off-");
5735
+ await run(off, { dir, contextPack: { mode: "off" } });
5736
+ assert.ok(!builders(off).some((b) => b[4].includes("Resume digest")));
5737
+ assert.ok(!readLog(dir, 7).some((x) => x.event === "RESUME_DIGEST"));
5738
+ assert.ok(!off.comments.some((c) => c.includes("FORGE:CONTRACT")), "the frozen contract is reused, not re-planned");
5739
+ });
5740
+ });
5741
+
5742
+ describe("#4395 lane profile", () => {
5743
+ const LANE = "# Hard rules\nNever push to main.";
5744
+ const capture = (w) => {
5745
+ const seen = [], orig = w.io.runner;
5746
+ w.io.runner = async (c) => { seen.push({ role: c.role, resume: c.resume ?? null, system: c.system, env: c.env ?? {}, noTools: c.noTools ?? false }); return orig(c); };
5747
+ return seen;
5748
+ };
5749
+ const usage = (dir) => readLog(dir, 7).filter((e) => e.event === "PHASE_ATTEMPT_USAGE");
5750
+
5751
+ it("without lane_instructions every session has auto-memory off, CLAUDE.md kept and system prompts unchanged", async () => {
5752
+ const w = world(), seen = capture(w), dir = tmpDir("core-lp-");
5753
+ const r = await run(w, { dir });
5754
+ assert.equal(r.terminalReason, "merged");
5755
+ assert.ok(seen.length >= 2);
5756
+ for (const c of seen) { assert.equal(c.env.CLAUDE_CODE_DISABLE_AUTO_MEMORY, "1"); assert.equal("CLAUDE_CODE_DISABLE_CLAUDE_MDS" in c.env, false); }
5757
+ assert.equal(seen.find((c) => c.role === "builder").system, "");
5758
+ assert.equal(seen.find((c) => c.role === "reviewer").system, REVIEWER_SYSTEM);
5759
+ assert.ok(usage(dir).every((e) => e.lane_profile === "memory-off"));
5760
+ assert.match(traj(w)[0], /Lane profile: auto-memory off, CLAUDE\.md loaded/);
5761
+ });
5762
+
5763
+ it("a valid lane file replaces CLAUDE.md: both switches, file appended to fresh sessions only", async () => {
5764
+ const w = world(), seen = capture(w), dir = tmpDir("core-lp-");
5765
+ w.files[`${WT}/devdocs/lane.md`] = `${LANE}\n`;
5766
+ const r = await run(w, { dir, laneInstructions: "devdocs/lane.md" });
5767
+ assert.equal(r.terminalReason, "merged");
5768
+ for (const c of seen) { assert.equal(c.env.CLAUDE_CODE_DISABLE_AUTO_MEMORY, "1"); assert.equal(c.env.CLAUDE_CODE_DISABLE_CLAUDE_MDS, "1"); }
5769
+ const fresh = seen.filter((c) => c.role === "builder" && !c.resume), resumed = seen.filter((c) => c.role === "builder" && c.resume);
5770
+ assert.ok(fresh.length >= 1 && resumed.length >= 1);
5771
+ for (const c of fresh) assert.equal(c.system, LANE);
5772
+ for (const c of resumed) assert.equal(c.system, "", "a resume sends no system prompt");
5773
+ assert.equal(seen.find((c) => c.role === "reviewer").system, `${REVIEWER_SYSTEM}\n\n${LANE}`);
5774
+ assert.ok(usage(dir).every((e) => e.lane_profile === "lane"));
5775
+ assert.match(traj(w)[0], /Lane profile: devdocs\/lane\.md \(\d+ bytes\) instead of CLAUDE\.md/);
5776
+ });
5777
+
5778
+ for (const [path, why] of [["devdocs/missing.md", /not readable/], ["../outside.md", /not a repo-relative path/], ["/etc/passwd", /not a repo-relative path/]]) {
5779
+ it(`a bad lane file (${path}) is an anomaly and falls back to CLAUDE.md with memory off`, async () => {
5780
+ const w = world(), seen = capture(w), dir = tmpDir("core-lp-");
5781
+ const r = await run(w, { dir, laneInstructions: path });
5782
+ assert.equal(r.terminalReason, "merged");
5783
+ for (const c of seen) { assert.equal(c.env.CLAUDE_CODE_DISABLE_AUTO_MEMORY, "1"); assert.equal("CLAUDE_CODE_DISABLE_CLAUDE_MDS" in c.env, false); }
5784
+ assert.equal(seen.find((c) => c.role === "builder").system, "");
5785
+ assert.match(traj(w)[0], new RegExp(`engine\\.lane_instructions .*${why.source}`));
5786
+ });
5787
+ }
5788
+
5789
+ it("an oversized lane file falls back", async () => {
5790
+ const w = world(), seen = capture(w);
5791
+ w.files[`${WT}/big.md`] = "x".repeat(16385);
5792
+ await run(w, { laneInstructions: "big.md" });
5793
+ assert.ok(seen.every((c) => !("CLAUDE_CODE_DISABLE_CLAUDE_MDS" in c.env)));
5794
+ assert.match(traj(w)[0], /over 16384 bytes/);
5795
+ });
5796
+
5797
+ it("FORGEDOCK_FULL_SESSION=1 leaves sessions untouched", async () => {
5798
+ const w = world(), seen = capture(w), dir = tmpDir("core-lp-");
5799
+ w.files[`${WT}/devdocs/lane.md`] = LANE;
5800
+ const prev = process.env.FORGEDOCK_FULL_SESSION; process.env.FORGEDOCK_FULL_SESSION = "1";
5801
+ try { await run(w, { dir, laneInstructions: "devdocs/lane.md" }); } finally { if (prev === undefined) delete process.env.FORGEDOCK_FULL_SESSION; else process.env.FORGEDOCK_FULL_SESSION = prev; }
5802
+ for (const c of seen) { assert.equal("CLAUDE_CODE_DISABLE_AUTO_MEMORY" in c.env, false); assert.equal("CLAUDE_CODE_DISABLE_CLAUDE_MDS" in c.env, false); }
5803
+ assert.equal(seen.find((c) => c.role === "builder").system, "");
5804
+ assert.ok(usage(dir).every((e) => e.lane_profile === "full"));
5805
+ });
5806
+
5807
+ it("PHASE_ATTEMPT_USAGE carries the runner's baseline_tokens", async () => {
5808
+ const w = world(), dir = tmpDir("core-lp-"), orig = w.io.runner;
5809
+ w.io.runner = async (c) => ({ ...(await orig(c)), baselineTokens: c.resume ? null : 21000 });
5810
+ await run(w, { dir });
5811
+ const u = usage(dir);
5812
+ assert.equal(u[0].baseline_tokens, 21000, "the fresh plan call reports its baseline");
5813
+ assert.ok(u.filter((e) => e.phase === "implement" && e.trigger === "build").every((e) => !("baseline_tokens" in e)), "a resumed build reports none");
5814
+ });
5815
+
5816
+ it("#4410 PHASE_ATTEMPT_USAGE carries the runner's tools stats only when present; makeLeanRunner derives them from toolUses", async () => {
5817
+ const tools = { calls: 4, turns: 2, per_turn: 2, rereads: 0, top: [] };
5818
+ const w = world(), dir = tmpDir("core-tools-"), orig = w.io.runner;
5819
+ w.io.runner = async (c) => ({ ...(await orig(c)), tools: c.role === "builder" ? tools : null });
5820
+ await run(w, { dir });
5821
+ const u = usage(dir);
5822
+ assert.ok(u.some((e) => e.phase === "implement" && e.trigger === "build" && e.tools?.per_turn === 2), "a builder attempt carries tools");
5823
+ const rev = readLog(dir, 7).filter((e) => e.event === "PHASE_ATTEMPT_USAGE" && e.phase === "review");
5824
+ assert.ok(rev.length && rev.every((e) => !("tools" in e)), "null tools leave the key absent");
5825
+ const uses = [{ msg: "m1", name: "Read", path: "a", cmd: null }, { msg: "m1", name: "Bash", path: null, cmd: "cat a" }, { msg: "m2", name: "Bash", path: null, cmd: "sed -n 1,2p a" }];
5826
+ const r = await makeLeanRunner({ runCli: async () => ({ text: "", toolUses: uses }) })({ role: "builder", issue: 7, cwd: dir, resume: null, system: "", prompt: "go" });
5827
+ assert.deepEqual(r.tools, { calls: 3, turns: 2, per_turn: 1.5, rereads: 1, top: [{ path: "a", reads: 3 }] });
5828
+ assert.equal((await makeLeanRunner({ runCli: async () => ({ text: "" }) })({ role: "builder", issue: 7, cwd: dir, resume: null, system: "", prompt: "go" })).tools, null);
5829
+ });
5830
+
5831
+ it("#4410 both builder prompts ask for same-turn read-only calls and read-once notes (within budget)", () => {
5832
+ for (const p of [builderPlanPrompt(7), builderImplementPrompt(), builderImplementPrompt("ci")]) {
5833
+ assert.match(p, /Issue independent read-only calls \(reads, greps, git log\/blame, gh\) in the same turn or one shell call\./);
5834
+ assert.match(p, /Read each file's ranges once .* and keep notes; don't re-read it\./);
5835
+ }
5836
+ assert.ok(approxTokens(builderImplementPrompt()) <= 1500 && approxTokens(builderPlanPrompt(7)) <= 1800);
5837
+ });
5838
+
5839
+ it("runCliBackendAsync reports the first stream turn's context as baselineTokens; makeLeanRunner drops it on resume", async () => {
5840
+ const { chmodSync } = await import("node:fs");
5841
+ const { runCliBackendAsync } = await import("../runner.mjs");
5842
+ const d = tmpDir("core-async-");
5843
+ const msg = (id, read) => JSON.stringify({ type: "assistant", message: { id, content: [], usage: { input_tokens: 10, output_tokens: 7, cache_read_input_tokens: read, cache_creation_input_tokens: 300 } } });
5844
+ const result = JSON.stringify({ type: "result", subtype: "success", is_error: false, result: "ok", session_id: "s-1", usage: { input_tokens: 20, output_tokens: 14 } });
5845
+ writeFileSync(`${d}/fake`, `#!/bin/sh\necho '${msg("m1", 5000)}'\necho '${msg("m2", 9000)}'\necho '${result}'\n`); chmodSync(`${d}/fake`, 0o755);
5846
+ const r = await runCliBackendAsync({ spec: { name: "t" }, userMessage: "go", args: ["1"], cwd: d, bin: `${d}/fake`, logger: { log() {} }, stream: true });
5847
+ assert.equal(r.baselineTokens, 5310);
5848
+ const fresh = await makeLeanRunner({ runCli: async () => ({ text: "", baselineTokens: 5310 }) })({ role: "builder", issue: 7, cwd: d, resume: null, system: "", prompt: "go" });
5849
+ assert.equal(fresh.baselineTokens, 5310);
5850
+ const resumed = await makeLeanRunner({ runCli: async () => ({ text: "", baselineTokens: 90000 }) })({ role: "builder", issue: 7, cwd: d, resume: "s-1", system: "", prompt: "go" });
5851
+ assert.equal(resumed.baselineTokens, null);
5852
+ });
5853
+ });
5854
+
5855
+ describe("#4450 light lane: one builder session, derived contract, upgrade to the full lane", () => {
5856
+ const RT = { path: "test/foo.test.js", id: "foo handles null", command: "node --test test/foo.test.js" };
5857
+ const BODY = "foo crashes on null input in src/foo.js.\n\nExpected behaviour: foo returns null.\n\n- foo must handle a null argument without throwing.\n- Existing callers of foo keep working.";
5858
+ const RESULT = { status: "fixed", summary: "fixed foo", requirement: "foo must handle a null argument without throwing.", repro_test: RT, tests: [{ command: RT.command, result: "pass", fails_without_fix: true }] };
5859
+ const lworld = (o = {}) => {
5860
+ const w = world({ labels: [{ name: "bug" }], body: BODY, result: RESULT, contract: { ...CONTRACT, repro_test: RT }, ...o });
5861
+ w.numstat = o.numstat ?? "5\t1\tsrc/foo.js\n20\t0\ttest/foo.test.js\n";
5862
+ w.rgCalls = []; w.ran = [];
5863
+ w.io.redGreen = async (x) => { w.rgCalls.push(x.command); return o.repro ?? { base: "FAILED", head: "RAN" }; };
5864
+ w.io.runCommand = async (a) => { w.ran.push(a.command); return { rc: 0, output: "" }; };
5865
+ w.io.apiShape = async () => o.api ?? { status: "none", changes: [], unknown: [] };
5866
+ return w;
5867
+ };
5868
+ const events = (dir, name) => readLog(dir, 7).filter((e) => e.event === name);
5869
+ const ON = { light: { mode: "on" } };
5870
+
5871
+ it("nothing triggers: exactly one builder session (no plan) plus the review; the derived contract is frozen and posted", async () => {
5872
+ const w = lworld(), dir = tmpDir("light-");
5873
+ const r = await run(w, { dir, ...ON });
5874
+ assert.equal(r.terminalReason, "merged");
5875
+ assert.equal(builders(w).length, 1, "one builder session");
5876
+ assert.ok(!/PLANNING ONLY/.test(builders(w)[0][4]));
5877
+ assert.match(builders(w)[0][4], /failing regression test first/);
5878
+ assert.equal(reviewers(w).length, 1, "plus the review call");
5879
+ const contract = w.comments.find((c) => c.includes("<!-- FORGE:CONTRACT -->"));
5880
+ assert.match(contract, /\| R1 \| foo must handle a null argument without throwing\. \| deliver \|/);
5881
+ assert.match(contract, /"repro_test"/);
5882
+ assert.equal(events(dir, "LIGHT_ELIGIBILITY")[0].verdict, "eligible");
5883
+ assert.equal(events(dir, "LIGHT_DONE").length, 1);
5884
+ assert.equal(events(dir, "LIGHT_UPGRADE").length, 0);
5885
+ assert.deepEqual(events(dir, "CONTRACT_FROZEN").map((e) => e.derived), ["light"]);
5886
+ assert.ok(w.ran.includes(RT.command), "the engine ran the targeted tests");
5887
+ assert.ok(w.rgCalls.length >= 1, "red/green repro gate ran");
5888
+ const lite = w.comments.filter((c) => c.includes("<!-- FORGE:LITE -->"));
5889
+ assert.equal(lite.length, 1, "#4452: one FORGE:LITE receipt");
5890
+ assert.match(lite[0], /\*\*Outcome:\*\* light/);
5891
+ assert.match(lite[0], /\*\*Diff:\*\* 2 file\(s\), \+25\/-1/);
5892
+ assert.match(lite[0], /\*\*Red\/green:\*\* base FAILED → head RAN/);
5893
+ assert.match(lite[0], /targeted tests: 1 command\(s\), passed/);
5894
+ assert.match(lite[0], /\*\*Cost:\*\* \$\d+\.\d{4} \(true per-session cost/);
5895
+ assert.match(lite[0], /\*\*Eligibility:\*\* eligible \(mode on\)/);
5896
+ assert.equal(typeof events(dir, "LIGHT_DONE")[0].cost_usd, "number");
5897
+ });
5898
+
5899
+ for (const [name, code, o] of [
5900
+ ["oversized diff", "diff_lines", { numstat: "80\t3\tsrc/foo.js\n20\t0\ttest/foo.test.js\n" }],
5901
+ ["non-red repro", "repro_not_red", { repro: { base: "PASSED", head: "RAN" } }],
5902
+ ["API change", "api_shape", { api: { status: "changed", changes: [{ file: "src/foo.js", symbol: "foo", change: "sync-async", before: "def", after: "async def" }], unknown: [] } }],
5903
+ ["risk path", "risk_path", { changed: ["src/auth/session.js", "test/foo.test.js"], numstat: "4\t0\tsrc/auth/session.js\n" }],
5904
+ ]) {
5905
+ it(`${name}: upgrades to the full lane with the digest, keeping the light commits`, async () => {
5906
+ const w = lworld(o), dir = tmpDir("light-up-");
5907
+ await run(w, { dir, ...ON });
5908
+ const up = events(dir, "LIGHT_UPGRADE");
5909
+ assert.equal(up.length, 1);
5910
+ assert.ok(up[0].reasons.some((x) => x.code === code), JSON.stringify(up[0].reasons));
5911
+ const b = builders(w);
5912
+ assert.ok(b.length >= 3, "light session, then the full lane's plan and implement");
5913
+ assert.ok(!/PLANNING ONLY/.test(b[0][4]));
5914
+ assert.match(b[1][4], /PLANNING ONLY/);
5915
+ assert.match(b[1][4], /This is a rerun: the branch already holds commits/);
5916
+ assert.match(b[1][4], /Resume digest of the last run on this branch/);
5917
+ assert.match(b[1][4], new RegExp(`Light-lane upgrade[\\s\\S]*- ${code}:`));
5918
+ const later = w.calls.slice(w.calls.indexOf(b[0]), w.calls.indexOf(b[2])).filter((c) => c[0] === "git"); // light session -> full implement
5919
+ assert.ok(!later.some((c) => ["reset", "rebase", "clean"].includes(c[1]) || (c[1] === "checkout" && c.includes("-B")) || (c[1] === "branch" && c.includes("-D")) || (c[1] === "push" && c.some((x) => /^--force|^-f$/.test(String(x))))), "the light work is never discarded");
5920
+ assert.equal(events(dir, "LIGHT_DONE").length, 0);
5921
+ const lite = w.comments.filter((c) => c.includes("<!-- FORGE:LITE -->"));
5922
+ assert.equal(lite.length, 1, "#4452: the upgraded light lane still posts its receipt");
5923
+ assert.match(lite[0], new RegExp(`\\*\\*Upgrade reasons:\\*\\*[\\s\\S]*- ${code}:`));
5924
+ });
5925
+ }
5926
+
5927
+ it("#4453 the run result carries light, the eligibility verdict and the upgrade reason codes (for the three-way bench)", async () => {
5928
+ const lit = await run(lworld(), { dir: tmpDir("light-res-"), ...ON });
5929
+ assert.equal(lit.light, true); assert.equal(lit.light_eligibility, "eligible"); assert.ok(!("light_upgrade" in lit));
5930
+ const up = await run(lworld({ repro: { base: "PASSED", head: "RAN" } }), { dir: tmpDir("light-res-up-"), ...ON });
5931
+ assert.equal(up.light, false); assert.equal(up.light_eligibility, "eligible"); assert.ok(up.light_upgrade.includes("repro_not_red"), JSON.stringify(up.light_upgrade));
5932
+ const off = await run(lworld(), { dir: tmpDir("light-res-off-"), light: { mode: "off" } });
5933
+ assert.equal(off.light, false); assert.equal(off.light_eligibility, "off"); assert.ok(!("light_upgrade" in off));
5934
+ assert.equal(typeof off.cost_usd, "number"); assert.ok(Number.isFinite(off.wall_ms) && Number.isFinite(off.model_ms), "#4451 fields unchanged");
5935
+ });
5936
+
5937
+ it("shadow (the default) only reports the verdict: the lane keeps plan -> implement", async () => {
5938
+ const w = lworld(), dir = tmpDir("light-sh-");
5939
+ assert.equal((await run(w, { dir })).terminalReason, "merged");
5940
+ const e = events(dir, "LIGHT_ELIGIBILITY")[0];
5941
+ assert.equal(e.mode, "shadow"); assert.equal(e.verdict, "eligible"); assert.equal(e.light, false);
5942
+ assert.equal(builders(w).length, 2);
5943
+ assert.match(builders(w)[0][4], /PLANNING ONLY/);
5944
+ assert.ok(!/Light-lane upgrade/.test(builders(w)[0][4]));
5945
+ assert.ok(!w.comments.some((c) => c.includes("<!-- FORGE:LITE -->")), "#4452: no receipt outside a light lane");
5946
+ });
5947
+
5948
+ /** #4451: lworld plus counting fakes for the overhead the light lane skips; each call notes its position in w.calls */
5949
+ const overhead = (o = {}) => {
5950
+ const w = lworld(o);
5951
+ w.over = { pack: [], hot: [], structure: [] };
5952
+ w.io.contextFacts = async () => { w.over.pack.push(w.calls.length); return { sources: {}, items: [] }; };
5953
+ w.io.hotspots = async (x) => { w.over.hot.push(w.calls.length); return { enabled: true, files: [], base: x.base ?? null }; };
5954
+ w.io.structureDelta = async () => { w.over.structure.push(w.calls.length); return { package_edges_added: [], package_edges_removed: [], import_pairs: [] }; };
5955
+ return w;
5956
+ };
5957
+ it("#4451 a light lane that is not upgraded makes no context-pack, structure or hotspot calls", async () => {
5958
+ const w = overhead(), dir = tmpDir("light-over-");
5959
+ const r = await run(w, { dir, ...ON, contextPack: { mode: "on" } });
5960
+ assert.equal(r.terminalReason, "merged");
5961
+ assert.equal(events(dir, "LIGHT_DONE").length, 1);
5962
+ assert.deepEqual(w.over, { pack: [], hot: [], structure: [] });
5963
+ assert.equal(events(dir, "CONTEXT_PACK").length, 0);
5964
+ // only the merge-time record (local numstat, no structure-delta call) remains; no implement-time delta
5965
+ assert.deepEqual(events(dir, "STRUCTURE_DELTA").map((e) => e.phase), ["merge"]);
5966
+ assert.equal(events(dir, "RUN_TERMINAL")[0].light, true);
5967
+ });
5968
+ it("#4451 an upgraded light lane computes the hotspots and the context pack before the plan call and runs the structure delta", async () => {
5969
+ const w = overhead({ numstat: "80\t3\tsrc/foo.js\n20\t0\ttest/foo.test.js\n" }), dir = tmpDir("light-over-up-");
5970
+ await run(w, { dir, ...ON, contextPack: { mode: "on" } });
5971
+ assert.equal(events(dir, "LIGHT_UPGRADE").length, 1);
5972
+ const plan = w.calls.indexOf(builders(w)[1]);
5973
+ // each fake notes w.calls.length when called, i.e. the index the next call takes: <= plan means before the plan call
5974
+ assert.ok(w.over.hot.some((i) => i <= plan) && w.over.pack.some((i) => i <= plan), JSON.stringify({ plan, ...w.over }));
5975
+ assert.ok(w.over.hot.every((i) => i > w.calls.indexOf(builders(w)[0])), "not before the light session");
5976
+ assert.ok(w.over.structure.length >= 1);
5977
+ assert.equal(events(dir, "RUN_TERMINAL")[0].light, false);
5978
+ });
5979
+ it("#4451 a full lane (light off) keeps its context-pack, structure and hotspot calls", async () => {
5980
+ const w = overhead(), dir = tmpDir("light-over-off-");
5981
+ await run(w, { dir, light: { mode: "off" }, contextPack: { mode: "on" } });
5982
+ assert.ok(w.over.pack.length >= 1 && w.over.hot.length >= 1 && w.over.structure.length >= 1, JSON.stringify(w.over));
5983
+ });
5984
+ it("#4451 the light targeted tests run concurrently, never more than FORGE_HOST_PARALLELISM in flight", async () => {
5985
+ const cmds = ["node --test t1.js", "node --test t2.js", "node --test t3.js", "node --test t4.js"];
5986
+ const w = lworld({ result: { ...RESULT, tests: cmds.map((command) => ({ command, result: "pass" })) } }), dir = tmpDir("light-par-");
5987
+ let live = 0, max = 0;
5988
+ const seen = [];
5989
+ w.io.runCommand = async (a) => { live++; max = Math.max(max, live); seen.push(a.env?.FORGE_HOST_PARALLELISM); await new Promise((r) => setTimeout(r, 5)); live--; return { rc: 0, output: "" }; };
5990
+ const prev = process.env.FORGE_HOST_PARALLELISM;
5991
+ process.env.FORGE_HOST_PARALLELISM = "2";
5992
+ try { await run(w, { dir, ...ON }); } finally { if (prev === undefined) delete process.env.FORGE_HOST_PARALLELISM; else process.env.FORGE_HOST_PARALLELISM = prev; }
5993
+ assert.equal(events(dir, "LIGHT_DONE").length, 1);
5994
+ assert.equal(max, 2, "two in flight at once, capped at the host parallelism");
5995
+ assert.ok(seen.every((x) => x === undefined || x === "1"), `each command gets its worker share: ${seen}`);
5996
+ });
5997
+ it("#4451 lane telemetry records wall time and model time separately", async () => {
5998
+ const w = lworld(), dir = tmpDir("light-time-");
5999
+ const r = await run(w, { dir, ...ON });
6000
+ const model = events(dir, "PHASE_ATTEMPT_USAGE").reduce((n, e) => n + e.duration_ms, 0);
6001
+ assert.ok(model > 0);
6002
+ assert.equal(r.model_ms, model);
6003
+ assert.ok(Number.isFinite(r.wall_ms));
6004
+ const term = w.telemetry.find((t) => t.event === "RUN_TERMINAL");
6005
+ assert.equal(term.model_ms, model); assert.ok(Number.isFinite(term.wall_ms));
6006
+ });
6007
+
6008
+ it("a resumed lane never goes light, even in mode on", async () => {
6009
+ const w = lworld({ remote: "abc\trefs/heads/feat/issue-7-lean\n" }), dir = tmpDir("light-res-");
6010
+ await run(w, { dir, ...ON });
6011
+ assert.equal(events(dir, "LIGHT_DONE").length + events(dir, "LIGHT_UPGRADE").length, 0);
6012
+ });
4424
6013
  });