/** * v2 MVP M3 — merge(b), the hard bone ([ref] §7, V2-MVP-PLAN.md, search [25]/[26], council round-1). * * The deterministic Coordinator integrates N completed workers' branches under FULL isolation: * 1. pull each worker's patch OUT of its container (M2 `pullDiff` — no creds go in). * 2. provision an EPHEMERAL no-net/no-creds integration sandbox (base pre-provisioned by the dep). * 3. `git apply --3way` each patch in sequence (conflict ⇒ TERMINAL: capture `.rej` + the conflict PATH list and * hand them to the USER as {@link MergeConflict} — S-113 / clay [ref] ④; council#3's quarantine unchanged). * 4. run the full test suite IN the sandbox (the gate; untrusted diff never touches the control plane — [26]§16). * 5. green → the **Coordinator** (trusted control plane, holds creds — NOT the sandbox, council#1) takes the * integrated patch and pushes with CAS `--force-with-lease=:` (push race → retry — council#2). * 6. the sandbox is destroyed on ALL exit paths (council#15). * * This module is the ORCHESTRATION/control-flow (deps injected → mock-tested). The real git mechanics * (apply/rebase/test/push in a real E2B container) are validated by the M5 real-E2B joint dogfood. */ import type { WorkerReport } from "./fanout.js"; import type { PullDiffResult } from "./diffout.js"; import { type RepairTerminal } from "@sema-agent/core"; import { type GraderHandle } from "./grader-env-factory.js"; /** A buffered command env for the integration sandbox (real E2B adapter adapts to this; mock in tests). */ export interface IntegrationEnv { /** Run a command; never throws — failures surface as a non-zero exitCode. */ exec(command: string, opts?: { cwd?: string; }): Promise<{ exitCode: number; stdout: string; stderr: string; }>; writeFile(path: string, content: string): Promise; } /** * Bounded integration-repair (design: search 2026-06-14, Codex-reviewed). When the merged tree applies * cleanly but the integration `testCmd` (the oracle gate) FAILS, the leader used to return terminal — a human * read the build error and re-ran (the "11 runs / 6 fixes" loop was human-driven). This closes that gap: a * bounded repair AGENT runs IN the already-merged sandbox (keeps the workers' good output + sees BOTH sides of * a broken cross-worker contract, e.g. the ChannelShelf edge), edits the working tree (NOT commit/push), then * mergeBranches re-runs `testCmd` to verify. Safe degradation: if repair exhausts rounds / errors, the result * is today's `phase:"test"` failure plus repair metadata — never worse than terminal-fail. ⚠️ repair on a * COMPILE-only `testCmd` can push green-but-incorrect code (oracle-quality, not repair, problem) — pair with a * real hidden-oracle `measureCmd`/L3 when correctness (not just compile) must close. */ export interface IntegrationRepair { /** Max repair attempts before giving up (each = one agent run + one testCmd re-run). */ maxRounds: number; /** Run the repair agent against the live merged tree. Edits the working tree to make `testCmd` pass; MUST * NOT commit or push (mergeBranches squashes worker+repair edits into one commit at the end). Returns its * spend + a short note; mergeBranches re-runs `testCmd` to decide if the fix actually worked. */ run(ctx: { round: number; errorText: string; testCmd: string; repoDir: string; }): Promise<{ costUsd?: number; note?: string; }>; } /** * S-113 / clay [ref] ④ — **合不上 = 交给用户**(对齐 CC 形)。`git apply --3way` 冲突时的产出:冲突**路径** * (不含内容)+ 哪几只 worker 的补丁已经进树,由 leader 铸成 `LeaderResult.conflict` 段交到用户手上。 * 本段刻意**只有路径与身份**:CC 259 语料对 collapse / conflict resolution 零命中,CC 的并行是同机 teammates * 各 worktree、结束由 git 重放到用户 HEAD、冲突列给用户(cli259.js @2303113)——没有任何模型去读别的 worker * 的 diff。7.83.0 删掉的那条「冲突解决模型」通道(它的类型面与 `LEADER_CONFLICT_ROUNDS` 旋钮)正是此意。 */ export interface MergeConflict { /** 有冲突标记或 `.rej` 的路径(**不含内容**;`.rej` 后缀已剥,指向源文件)。 */ files: string[]; /** 对抗复审 F5 [medium]:清单**被截断**(命中 {@link CONFLICT_FILES_MAX})时在场 —— 缺席 = 这就是全部。 * 「完整清单」是本终局对用户的补偿,一次静默的 200 封顶会让用户以为冲突只有 200 处。 */ filesTruncated?: true; /** 冲突发生**之前**已经干净 apply 进树的 workerId(顺序 apply,失败即停)。 */ applied: string[]; /** 补丁没能 apply 的那只 worker。 */ conflicted: string; /** `.rej` 头(经既有**工件**脱敏面 `redactArtifactText` —— 与 `rej` 同一批 diff 字节同一口 —— 再切 ≤ 2 KiB) * ——给用户看「冲突长什么样」,不是给模型的输入。 */ rejHead?: string; } export interface MergeDeps { /** Pull one worker's patch from ITS container (M2 pullDiff bound to the worker's env). */ pullWorkerDiff(report: WorkerReport): Promise; /** Provision a fresh ephemeral no-net/no-creds sandbox with the base repo at `baseSha` already in place. * `repair` (optional) enables bounded integration-repair on a `testCmd` failure (see {@link IntegrationRepair}); * unset → a failing integration test is terminal (the pre-2026-06-14 behavior, unchanged). */ provisionIntegrationSandbox(): Promise<{ env: IntegrationEnv; destroy: () => Promise; repair?: IntegrationRepair; }>; /** * The CREDENTIALED push, done by the Coordinator (control plane), NOT the sandbox (council#1). Takes the * integrated patch (the sandbox's combined diff over base) + the base it forked from; applies to the * Coordinator's own trusted clone and pushes `--force-with-lease=:`. `raced` = lease reject. */ push(integratedPatch: string, baseSha: string): Promise<{ ok: true; ref: string; } | { ok: false; raced?: boolean; error: string; }>; /** Repo dir inside the sandbox (default /repo). */ repoDir?: string; /** Full test command run in the sandbox (the merge gate). */ testCmd: string; /** Optional truecorrect MEASURE ([ref]): a thorough hidden-oracle command run IN the integration sandbox * (E2B) right after the gate passes — offloads the measure from the CPU-contended control plane (the * frontier-multi oracle-timeout fix). NOT a gate (does not affect `ok`); the result lands in MergeResult.measure * for the caller to read as the objective truly-correct signal. Same place + env as the gate, just a thornier test. */ measureCmd?: string; /** * LEADER-REPAIRLOOP-INTEGRATION §10.3 — push-hold-on-SIGNAL master flag (the wire sets it from * `LEADER_MEASURE_GATES`, default OFF). When OFF (the default) the push path is BYTE-IDENTICAL to today: the * `measureCmd` stays observe-only and a compile-only-green merge auto-pushes. When ON, a configured strong * oracle that is NOT green HOLDS the push (→ `candidate_only`, no push): the hold keys on the SIGNAL, not a * caller field — `strongOracleSeeded` (an oracleFiles-backed hidden held-out oracle is in the sandbox) OR * (`measureCmd` present AND its measure did not pass). Auto-push only when every configured strong signal is * GREEN; absent BOTH → legacy compile-only auto-push (§10.3). */ measureGates?: boolean; /** * §10.3 — set by the wire when an oracleFiles-backed hidden held-out oracle was seeded into the integration * sandbox (`wire.ts` `injectOracles`). Under `measureGates`, a seeded-but-only-`testCmd`-green merge is a * strong-oracle-UNCONFIRMED result → the push is HELD (`candidate_only`). The merge module cannot see the * oracleFiles itself (a wire concern), so the wire reports their presence here. Default unset/false → today's * behavior. */ strongOracleSeeded?: boolean; /** * §10.4 measure-drives-repair — the out-of-process measure grader (R2) + the changed-files allowlist. When set * (and `measureGates` ON) a `measure`-FAIL re-seeds this SINGLE minted grader (`git reset --hard ` + * `git clean -fdx`, re-apply the integrated patch) and re-runs the trusted `measureCmd` THERE — never in the * repair-edited (possibly poisoned) merge sandbox (§10.4(i)). Before re-measuring, the integrated patch's * changed-file list is mechanically checked against `allowlist` (the workers' planned `files` ∪ trusted spec * dirs): an out-of-allowlist / build-config touch downgrades the candidate to `needs_human_oracle` (§10.4(ii)). * The grader handle is bound by the wire (a re-seedable integration-equivalent sandbox). Unset → no measure * grader; the measure stays observe-only (today's path). */ measureDrivesRepair?: { /** The out-of-process grader handle (re-seedable; mirrors the integration sandbox, oracle-seeded). */ grader: GraderHandle; /** The trusted, spec-derived measure steps run in the grader (NEVER worker-authored). */ measureSteps: import("@sema-agent/core").ExecStep[]; /** §10.4(ii): the changed-files allowlist = the workers' planned `files` ∪ trusted spec dirs. */ allowlist: string[]; /** Bounded measure-repair rounds (re-seed + re-measure). Each round optionally drives the existing * integration `repair` agent first; 0/unset → grade once, no repair. */ maxRounds?: number; /** Parse the integrated patch into a changed-file list (the wire binds `git apply --numstat` / name-only). */ changedFiles: (integratedPatch: string) => Promise; }; logger?: { warn?: (m: string, x?: Record) => void; info?: (m: string, x?: Record) => void; }; } /** Integration-repair outcome (when `MergeDeps.provisionIntegrationSandbox` supplied a `repair`): how many * rounds ran, whether the post-repair `testCmd` finally passed, and the repair spend. `outsidePlannedFiles` * is honest accounting — repaired files are NOT attributed to the original disjoint workers. */ export interface RepairMeta { rounds: number; fixed: boolean; costUsd?: number; } export type MergeResult = { ok: true; ref: string; merged: string[]; measure?: { pass: boolean; reason?: string; }; repair?: RepairMeta; terminal?: RepairTerminal; candidatePatch?: string; } | { ok: false; reason: string; phase: "no-workers" | "diff-out" | "provision" | "apply" | "test" | "push"; quarantine?: string[]; rej?: string; conflict?: MergeConflict; retry?: boolean; details?: string; repair?: RepairMeta; terminal?: RepairTerminal; candidatePatch?: string; measure?: { pass: boolean; reason?: string; }; }; /** Integrate completed workers' branches. Never throws — failures are typed MergeResult. */ export declare function mergeBranches(reports: WorkerReport[], baseSha: string, deps: MergeDeps): Promise; /** 探测脚本里某一半失败时打到 stdout 的标记前缀(`grep` / `find` 各一);只由 {@link conflictFilesOf} 认。 * ⚠️ 判序承重:`conflictFilesOf` **先**测本前缀、**后**剥 `./` —— `grep -rl` / `find .` 输出的真路径恒带 `./`, * 所以一个真叫 `__PROBE_FAIL_grep` 的文件到达时是 `./__PROBE_FAIL_grep`,不与标记二义;把剥 `./` 挪到前面就静默破功。 */ export declare const PROBE_FAIL_PREFIX = "__PROBE_FAIL_"; /** * S-113 / [ref] ④ — 把 `grep -rIl` + `find` 的输出变成**冲突路径清单**(给用户,不含内容)。 * 逐行:去掉 `./` 前缀、剥掉 `.rej` 后缀(`pkg/a.ts.rej` 与 `pkg/a.ts` 是同一处冲突的两种痕迹)、去重、 * 按 {@link CONFLICT_FILES_MAX} 封顶并**交回是否截断**(F5:静默封顶会让用户把 200 当成全部)。两条命令合成 * 一次 exec,所以 stdout 里两族路径混在一起 —— 本函数是它唯一的解释者。任一半探测失败 ⇒ 那一半打出 * {@link PROBE_FAIL_PREFIX} 标记、另一半的清单照常回来 ⇒ `probeFailed` 点名失败的那半、`files` 只是**不完整**而不是 * 「没有冲突」:`conflict` 段仍在场(用户仍拿得到分支与冲突者身份),调用点另记一行响亮日志(合并复审 车HB H-7)。 */ export declare function conflictFilesOf(stdout: string): { files: string[]; truncated: boolean; probeFailed: string[]; }; //# sourceMappingURL=merge.d.ts.map