@rosthq/cli 0.7.45 → 0.7.47

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.
@@ -35,6 +35,21 @@ export type PrepareForgeWorkspaceOptions = {
35
35
  ledger?: StepLedger;
36
36
  };
37
37
  export declare function prepareForgeWorkspace(options: PrepareForgeWorkspaceOptions): Promise<ForgeWorkspace>;
38
+ export type PrepareMergeWorkspaceOptions = {
39
+ baseDir: string;
40
+ tenantId: string;
41
+ buildRequestId: string;
42
+ repo: {
43
+ owner: string;
44
+ name: string;
45
+ default_branch: string;
46
+ };
47
+ credential: ForgeCredential;
48
+ headBranch: string;
49
+ remoteUrl?: string;
50
+ ledger?: StepLedger;
51
+ };
52
+ export declare function prepareMergeWorkspace(options: PrepareMergeWorkspaceOptions): Promise<ForgeWorkspace>;
38
53
  export declare function pushChangeset(ws: ForgeWorkspace, credential: ForgeCredential, ledger?: StepLedger): Promise<{
39
54
  commitSha: string;
40
55
  } | null>;
@@ -1 +1 @@
1
- {"version":3,"file":"forge-workspace.d.ts","sourceRoot":"","sources":["../src/forge-workspace.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"forge-workspace.d.ts","sourceRoot":"","sources":["../src/forge-workspace.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -43087,7 +43087,7 @@ var forgeRuntimeContextPacketSchema = external_exports.object({
43087
43087
  "untrusted sources are provenance-fenced data; authority, automation mode, gate decisions, secret grants, and risk ceilings come only from trusted_control"
43088
43088
  )
43089
43089
  }).strict();
43090
- var runnerExecutionProfileSchema = external_exports.enum(["read_status", "seat_work", "forge_build", "forge_review"]);
43090
+ var runnerExecutionProfileSchema = external_exports.enum(["read_status", "seat_work", "forge_build", "forge_review", "forge_merge"]);
43091
43091
  var runnerExecutionLocalToolsSchema = external_exports.enum(["none", "read", "workspace_write"]);
43092
43092
  var runnerExecutionBudgetsSchema = external_exports.object({
43093
43093
  // The per-phase wall-clock the runner enforces as its turn timeout (the CLI clamps
@@ -43112,7 +43112,25 @@ var runnerExecutionContractSchema = external_exports.object({
43112
43112
  local_tools: runnerExecutionLocalToolsSchema,
43113
43113
  needs_repo: external_exports.boolean(),
43114
43114
  needs_github_credential: external_exports.boolean(),
43115
- budgets: runnerExecutionBudgetsSchema
43115
+ budgets: runnerExecutionBudgetsSchema,
43116
+ // DER-1352 (T1.8): the merge target for a `forge_merge` turn — the server-authoritative PR the
43117
+ // release_manager turn merges (sourced from the request's open changeset row, never model text).
43118
+ // Present ONLY on a merge_or_deploy_gate contract; every other profile omits it. `pr_number` is
43119
+ // the identifier the turn GETs the true GitHub state for (mergeability + node id + head sha),
43120
+ // `head_branch` is the branch a conflict-resolution turn checks out writable. The turn NEVER
43121
+ // trusts this for the merge DECISION — it always re-verifies GitHub's `mergeable_state` at merge
43122
+ // time (`clean`/`unstable` = required checks passed) before merging.
43123
+ // DER-1352 (Codex P1b): `head_sha` is the REVIEWED / risk-assessed head — the implementation
43124
+ // commit the review + risk phases ran against (server-sourced, never model text). The merge turn
43125
+ // merges ONLY if GitHub's CURRENT head still equals it; if the PR head advanced since review, the
43126
+ // new commits are unreviewed + un-risk-assessed, so it parks for re-review instead of merging.
43127
+ // Omitted only for a transitional changeset with no recorded implementation head, in which case
43128
+ // the merge turn fails CLOSED (parks — it cannot prove the head was risk-assessed).
43129
+ merge: external_exports.object({
43130
+ pr_number: external_exports.number().int().positive(),
43131
+ head_branch: external_exports.string().trim().min(1).max(255),
43132
+ head_sha: external_exports.string().trim().regex(/^[0-9a-f]{7,40}$/i, "head_sha must be a git SHA (7\u201340 hex chars)").optional()
43133
+ }).strict().optional()
43116
43134
  }).strict();
43117
43135
  var turnReportText = external_exports.string().trim().min(1).max(2e3);
43118
43136
  var runnerTurnReportIssueSchema = external_exports.object({
@@ -52127,22 +52145,18 @@ async function configureForgeIdentity(baseRepo) {
52127
52145
  await runGit(["config", "user.name", FORGE_GIT_AUTHOR_NAME], baseRepo);
52128
52146
  await runGit(["config", "user.email", FORGE_GIT_AUTHOR_EMAIL], baseRepo);
52129
52147
  }
52130
- async function prepareForgeWorkspace(options) {
52131
- if (options.profile === "review" && !options.headSha) {
52132
- throw new Error("prepareForgeWorkspace: review profile requires headSha");
52133
- }
52134
- const baseRepo = baseRepoDir(options.baseDir, options.buildRequestId);
52135
- const url2 = remoteUrlFor(options);
52148
+ async function ensureBaseClone(input) {
52149
+ const baseRepo = baseRepoDir(input.baseDir, input.buildRequestId);
52136
52150
  await mkdir2(path3.dirname(baseRepo), { recursive: true, mode: 448 });
52137
- await withLedgerStep(options.ledger, "clone", (retryCounter) => withAskpass(options.credential, async (netEnv) => {
52151
+ await withLedgerStep(input.ledger, "clone", (retryCounter) => withAskpass(input.credential, async (netEnv) => {
52138
52152
  if (await isGitRepo(baseRepo)) {
52139
- await runGitNetwork("git fetch", ["fetch", "--prune", "--quiet", url2, "+refs/heads/*:refs/remotes/origin/*"], netEnv, baseRepo, FETCH_TIMEOUT_MS, retryCounter);
52153
+ await runGitNetwork("git fetch", ["fetch", "--prune", "--quiet", input.url, "+refs/heads/*:refs/remotes/origin/*"], netEnv, baseRepo, FETCH_TIMEOUT_MS, retryCounter);
52140
52154
  } else {
52141
52155
  await runGitNetwork(
52142
52156
  "git clone",
52143
52157
  // Non-shallow (no --depth) so merge-base/ancestry survive for later phases;
52144
52158
  // --no-checkout keeps the base a bare-ish object store the worktrees hang off.
52145
- ["clone", "--no-checkout", "--quiet", url2, baseRepo],
52159
+ ["clone", "--no-checkout", "--quiet", input.url, baseRepo],
52146
52160
  netEnv,
52147
52161
  void 0,
52148
52162
  CLONE_TIMEOUT_MS,
@@ -52151,11 +52165,52 @@ async function prepareForgeWorkspace(options) {
52151
52165
  }
52152
52166
  }));
52153
52167
  await configureForgeIdentity(baseRepo);
52168
+ return baseRepo;
52169
+ }
52170
+ async function prepareForgeWorkspace(options) {
52171
+ if (options.profile === "review" && !options.headSha) {
52172
+ throw new Error("prepareForgeWorkspace: review profile requires headSha");
52173
+ }
52174
+ const baseRepo = await ensureBaseClone({
52175
+ baseDir: options.baseDir,
52176
+ buildRequestId: options.buildRequestId,
52177
+ url: remoteUrlFor(options),
52178
+ credential: options.credential,
52179
+ ledger: options.ledger
52180
+ });
52154
52181
  if (options.profile === "review") {
52155
52182
  return await prepareReviewWorktree(options, baseRepo);
52156
52183
  }
52157
52184
  return await prepareBuildWorktree(options, baseRepo);
52158
52185
  }
52186
+ async function prepareMergeWorkspace(options) {
52187
+ if (!options.headBranch || options.headBranch.trim().length === 0) {
52188
+ throw new Error("prepareMergeWorkspace: headBranch is required");
52189
+ }
52190
+ const url2 = options.remoteUrl ?? `https://github.com/${options.repo.owner}/${options.repo.name}.git`;
52191
+ const baseRepo = await ensureBaseClone({
52192
+ baseDir: options.baseDir,
52193
+ buildRequestId: options.buildRequestId,
52194
+ url: url2,
52195
+ credential: options.credential,
52196
+ ledger: options.ledger
52197
+ });
52198
+ const branch = options.headBranch;
52199
+ const worktreeDir = path3.join(requestRoot(options.baseDir, options.buildRequestId), "merge");
52200
+ const originDefault = `origin/${options.repo.default_branch}`;
52201
+ const originHead = `origin/${branch}`;
52202
+ await withLedgerStep(options.ledger, "branch", async () => {
52203
+ if (await worktreeRegistered(baseRepo, worktreeDir)) {
52204
+ await removeWorktree(baseRepo, worktreeDir);
52205
+ }
52206
+ if (await localBranchExists(baseRepo, branch)) {
52207
+ await runGit(["branch", "-D", branch], baseRepo);
52208
+ }
52209
+ await runGit(["worktree", "add", "-b", branch, worktreeDir, originHead], baseRepo);
52210
+ });
52211
+ const baseSha = (await runGit(["merge-base", "HEAD", originDefault], worktreeDir)).trim();
52212
+ return { baseRepo, worktreeDir, branch, readOnly: false, baseSha, remoteUrl: url2 };
52213
+ }
52159
52214
  async function prepareBuildWorktree(options, baseRepo) {
52160
52215
  const branch = branchName(options.buildRequestId, options.changeset);
52161
52216
  const worktreeDir = buildWorktreeDir(options.baseDir, options.buildRequestId, options.changeset.ordinal);
@@ -52366,6 +52421,147 @@ async function openDraftPullRequestInner(options, retryCounter) {
52366
52421
  }
52367
52422
  throw new Error(`github pr create failed ${created.status}: ${redactSecrets(await readErrorText(created)).slice(0, 500)}`);
52368
52423
  }
52424
+ function classifyMergeReadiness(state) {
52425
+ if (state.merged) {
52426
+ return "already_merged";
52427
+ }
52428
+ if (state.draft) {
52429
+ return "draft";
52430
+ }
52431
+ if (state.mergeable === false || state.mergeableState === "dirty") {
52432
+ return "conflicted";
52433
+ }
52434
+ switch (state.mergeableState) {
52435
+ // clean = all required checks passed, no conflicts. unstable = only NON-required checks are
52436
+ // failing/pending (GitHub still permits the merge) — treat as mergeable.
52437
+ case "clean":
52438
+ case "unstable":
52439
+ return "mergeable";
52440
+ case "blocked":
52441
+ return "blocked";
52442
+ case "behind":
52443
+ return "behind";
52444
+ case "unknown":
52445
+ case "":
52446
+ return "unknown";
52447
+ default:
52448
+ return "blocked";
52449
+ }
52450
+ }
52451
+ function toPullRequestState(json2) {
52452
+ const record2 = json2 && typeof json2 === "object" && !Array.isArray(json2) ? json2 : {};
52453
+ const head2 = record2.head && typeof record2.head === "object" ? record2.head : {};
52454
+ const number4 = typeof record2.number === "number" ? record2.number : Number.NaN;
52455
+ const nodeId = typeof record2.node_id === "string" ? record2.node_id : "";
52456
+ if (!Number.isFinite(number4) || !nodeId) {
52457
+ throw new Error("github pr response missing number or node_id");
52458
+ }
52459
+ return {
52460
+ number: number4,
52461
+ nodeId,
52462
+ merged: record2.merged === true,
52463
+ draft: record2.draft === true,
52464
+ mergeable: typeof record2.mergeable === "boolean" ? record2.mergeable : null,
52465
+ mergeableState: typeof record2.mergeable_state === "string" ? record2.mergeable_state : "unknown",
52466
+ headSha: typeof head2.sha === "string" ? head2.sha : "",
52467
+ mergedSha: typeof record2.merge_commit_sha === "string" && record2.merged === true ? record2.merge_commit_sha : null
52468
+ };
52469
+ }
52470
+ async function fetchPullRequestState(options) {
52471
+ const baseUrl = (options.apiBaseUrl ?? GITHUB_API_BASE_URL).replace(/\/+$/u, "");
52472
+ const fetchImpl = options.fetchImpl ?? fetch;
52473
+ const response = await withGithubRetries(
52474
+ "github pr get",
52475
+ async () => fetchImpl(`${baseUrl}/repos/${options.repo.owner}/${options.repo.name}/pulls/${options.prNumber}`, {
52476
+ method: "GET",
52477
+ headers: githubHeaders(options.credential)
52478
+ })
52479
+ );
52480
+ if (response.status !== 200) {
52481
+ throw new Error(`github pr get failed ${response.status}: ${redactSecrets(await readErrorText(response)).slice(0, 500)}`);
52482
+ }
52483
+ return toPullRequestState(await response.json());
52484
+ }
52485
+ async function mergePullRequest(options) {
52486
+ const baseUrl = (options.apiBaseUrl ?? GITHUB_API_BASE_URL).replace(/\/+$/u, "");
52487
+ const fetchImpl = options.fetchImpl ?? fetch;
52488
+ const { owner, name } = options.repo;
52489
+ if (options.state.merged) {
52490
+ return { status: "merged", mergedSha: options.state.mergedSha };
52491
+ }
52492
+ const merge2 = await withGithubRetries(
52493
+ "github pr merge",
52494
+ async () => fetchImpl(`${baseUrl}/repos/${owner}/${name}/pulls/${options.state.number}/merge`, {
52495
+ method: "PUT",
52496
+ headers: githubHeaders(options.credential),
52497
+ // SHA guard: merge ONLY the head we verified — never pass a merge_method (the repo default
52498
+ // wins; a queue owns the strategy). A stale sha → 409, re-checked by the caller.
52499
+ body: JSON.stringify({ sha: options.state.headSha })
52500
+ })
52501
+ );
52502
+ if (merge2.status === 405) {
52503
+ const detail = await readErrorText(merge2);
52504
+ if (/merge queue/i.test(detail)) {
52505
+ await enablePullRequestAutoMerge({ repo: options.repo, nodeId: options.state.nodeId, credential: options.credential, fetchImpl, apiBaseUrl: baseUrl });
52506
+ return { status: "queued" };
52507
+ }
52508
+ throw new Error(`github pr merge failed 405: ${redactSecrets(detail).slice(0, 500)}`);
52509
+ }
52510
+ if (merge2.status !== 200) {
52511
+ throw new Error(`github pr merge failed ${merge2.status}: ${redactSecrets(await readErrorText(merge2)).slice(0, 500)}`);
52512
+ }
52513
+ const confirmed = await fetchPullRequestState({ repo: options.repo, prNumber: options.state.number, credential: options.credential, fetchImpl, apiBaseUrl: baseUrl });
52514
+ if (!confirmed.merged) {
52515
+ throw new Error("github pr merge postcondition failed: PR is not merged after the merge call");
52516
+ }
52517
+ return { status: "merged", mergedSha: confirmed.mergedSha };
52518
+ }
52519
+ async function enablePullRequestAutoMerge(options) {
52520
+ const baseUrl = (options.apiBaseUrl ?? GITHUB_API_BASE_URL).replace(/\/+$/u, "");
52521
+ const fetchImpl = options.fetchImpl ?? fetch;
52522
+ const response = await withGithubRetries(
52523
+ "github enable auto-merge",
52524
+ async () => fetchImpl(`${baseUrl}/graphql`, {
52525
+ method: "POST",
52526
+ headers: githubHeaders(options.credential),
52527
+ body: JSON.stringify({
52528
+ query: "mutation($id:ID!){enablePullRequestAutoMerge(input:{pullRequestId:$id}){clientMutationId}}",
52529
+ variables: { id: options.nodeId }
52530
+ })
52531
+ })
52532
+ );
52533
+ if (response.status !== 200) {
52534
+ throw new Error(`github enable auto-merge failed ${response.status}: ${redactSecrets(await readErrorText(response)).slice(0, 500)}`);
52535
+ }
52536
+ const body = await response.json();
52537
+ if (Array.isArray(body.errors) && body.errors.length > 0) {
52538
+ throw new Error("github enable auto-merge returned errors");
52539
+ }
52540
+ }
52541
+ async function forcePushWithLease(ws, credential) {
52542
+ if (ws.readOnly || ws.branch.length === 0) {
52543
+ throw new Error("force-push requires a writable build worktree on a branch");
52544
+ }
52545
+ const head2 = (await runGit(["rev-parse", "HEAD"], ws.worktreeDir)).trim();
52546
+ await withAskpass(credential, async (netEnv) => {
52547
+ const before = await runGitNetwork("git ls-remote", ["ls-remote", ws.remoteUrl, `refs/heads/${ws.branch}`], netEnv, ws.worktreeDir, FETCH_TIMEOUT_MS);
52548
+ const beforeSha = before.split(/\s+/)[0]?.trim() ?? "";
52549
+ const forceArg = beforeSha ? `--force-with-lease=refs/heads/${ws.branch}:${beforeSha}` : null;
52550
+ await runGitNetwork(
52551
+ "git push --force-with-lease",
52552
+ ["push", ...forceArg ? [forceArg] : [], "--quiet", ws.remoteUrl, `HEAD:refs/heads/${ws.branch}`],
52553
+ netEnv,
52554
+ ws.worktreeDir,
52555
+ PUSH_TIMEOUT_MS
52556
+ );
52557
+ const remote = await runGitNetwork("git ls-remote", ["ls-remote", ws.remoteUrl, `refs/heads/${ws.branch}`], netEnv, ws.worktreeDir, FETCH_TIMEOUT_MS);
52558
+ const remoteSha = remote.split(/\s+/)[0]?.trim() ?? "";
52559
+ if (remoteSha !== head2) {
52560
+ throw new Error(`git force-push postcondition failed: remote ${ws.branch} is ${remoteSha || "absent"}, expected ${head2}`);
52561
+ }
52562
+ });
52563
+ return { commitSha: head2 };
52564
+ }
52369
52565
  async function removeWorktree(baseRepo, worktreeDir) {
52370
52566
  try {
52371
52567
  await runGit(["worktree", "remove", "--force", worktreeDir], baseRepo);
@@ -52548,6 +52744,9 @@ function isSandboxExecAvailable() {
52548
52744
  return false;
52549
52745
  }
52550
52746
  }
52747
+ function claudeBashTempRoot(uid) {
52748
+ return `/private/tmp/claude-${uid}`;
52749
+ }
52551
52750
  function resolveSandboxSpec(options) {
52552
52751
  if (options.mode === "off") {
52553
52752
  return { kind: "none", reason: "RUNNER_SANDBOX=off (loosened fallback - OS confinement disabled)" };
@@ -52570,12 +52769,17 @@ function resolveSandboxSpec(options) {
52570
52769
  const denyWritePaths = (options.denyWritePaths ?? []).map(
52571
52770
  (p) => p.endsWith("/") ? `${canonical(p.replace(/\/+$/, ""))}/` : canonicalFile(p)
52572
52771
  );
52772
+ const uid = typeof process.getuid === "function" ? process.getuid() : null;
52773
+ const allowWritePaths = [
52774
+ ...(options.allowWritePaths ?? []).map(canonical),
52775
+ ...uid !== null ? [canonicalFile(claudeBashTempRoot(uid))] : []
52776
+ ];
52573
52777
  const profile = buildSeatbeltProfile({
52574
52778
  workspaceDir,
52575
52779
  tmpDir,
52576
52780
  homeDir,
52577
52781
  denyReadPaths,
52578
- allowWritePaths: (options.allowWritePaths ?? []).map(canonical),
52782
+ allowWritePaths,
52579
52783
  denyWritePaths,
52580
52784
  allowNetwork,
52581
52785
  strict: options.mode === "strict"
@@ -52993,7 +53197,7 @@ async function detectCapabilities(cliVersion, env, homeDir) {
52993
53197
  claude: { ...claude, account_alias: accountAlias, rate_limited_until: null },
52994
53198
  codex: { ...codex, account_alias: accountAlias, rate_limited_until: null },
52995
53199
  turn_executions: { report_supported: true },
52996
- ...cliVersion ? { forge_build: cliVersion, seat_work: cliVersion } : {},
53200
+ ...cliVersion ? { forge_build: cliVersion, seat_work: cliVersion, forge_merge: cliVersion } : {},
52997
53201
  git,
52998
53202
  gh,
52999
53203
  scheduler: { max_parallel_sessions_estimate: null },
@@ -53149,6 +53353,11 @@ async function executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtim
53149
53353
  ...result.commitSha ? { commit_sha: result.commitSha } : {},
53150
53354
  ...result.evidence && result.evidence.length > 0 ? { evidence: result.evidence } : {},
53151
53355
  ...result.changedPaths && result.changedPaths.length > 0 ? { changed_paths: result.changedPaths } : {},
53356
+ // DER-1352 (T1.8): the merge-turn postcondition. Only sent when the harness GET-verified the PR
53357
+ // merged on GitHub — the driver's completion writer marks the changeset merged + writes the
53358
+ // decisions row ONLY on this. Gated on result.ok so a failed turn can never carry it.
53359
+ ...result.ok && result.merged === true ? { merged: true } : {},
53360
+ ...result.ok && result.merged === true && result.mergedCommitSha ? { merged_commit_sha: result.mergedCommitSha } : {},
53152
53361
  // DER-1331 (T1.9): the step ledger + the model's (already redacted, bounded) transcript.
53153
53362
  // Only the work-orders result route accepts these today (turn-executions is a separate,
53154
53363
  // narrower endpoint — T1.9 scope); its Zod schema strips unrecognized keys on a non-strict
@@ -53492,6 +53701,9 @@ function buildTurnPrompt(input) {
53492
53701
  }
53493
53702
  async function spawnRunnerTurn(ctx, workOrder, runtime, kind) {
53494
53703
  const execution = parseRunnerExecutionContract(workOrder);
53704
+ if (execution?.profile === "forge_merge") {
53705
+ return runForgeMergeTurn(ctx, workOrder, execution, runtime);
53706
+ }
53495
53707
  let forgePrep = null;
53496
53708
  if (execution?.needs_repo) {
53497
53709
  const prepared = await prepareForgeTurn(ctx, workOrder, execution);
@@ -53574,6 +53786,271 @@ async function spawnRunnerTurn(ctx, workOrder, runtime, kind) {
53574
53786
  }
53575
53787
  return result;
53576
53788
  }
53789
+ var MERGE_MERGEABILITY_POLLS = 5;
53790
+ var MERGE_MERGEABILITY_POLL_MS = 2e3;
53791
+ var MERGE_QUEUE_POLLS = 10;
53792
+ var MERGE_QUEUE_POLL_MS = 3e3;
53793
+ var MERGE_CONFLICT_SUMMARY_MAX = 12e3;
53794
+ function mergeParkResult(note, extra) {
53795
+ return { ok: true, summary: `[forge-merge] ${note}`.slice(0, MERGE_CONFLICT_SUMMARY_MAX), ...extra };
53796
+ }
53797
+ function mergeMergedResult(mergedSha, note) {
53798
+ return {
53799
+ ok: true,
53800
+ merged: true,
53801
+ ...mergedSha ? { mergedCommitSha: mergedSha } : {},
53802
+ summary: `[forge-merge] ${note}${mergedSha ? ` (${mergedSha})` : ""}`.slice(0, MERGE_CONFLICT_SUMMARY_MAX)
53803
+ };
53804
+ }
53805
+ async function brokerForgeMergeCredential(ctx, workOrder) {
53806
+ const workOrderId = typeof workOrder.id === "string" ? workOrder.id : "";
53807
+ const forge = typeof workOrder.forge === "object" && workOrder.forge !== null ? workOrder.forge : null;
53808
+ const buildRequestId = typeof forge?.build_request_id === "string" ? forge.build_request_id : "";
53809
+ if (!workOrderId || !buildRequestId) {
53810
+ return { ok: false, summary: "[forge-merge] work order missing id or build request id" };
53811
+ }
53812
+ const credentialMintStartedAt = (/* @__PURE__ */ new Date()).toISOString();
53813
+ const brokered = await post2(
53814
+ ctx.fetchImpl,
53815
+ ctx.appUrl,
53816
+ `/api/runner/work-orders/${encodeURIComponent(workOrderId)}/github-credential`,
53817
+ {},
53818
+ ctx.state.runner_secret
53819
+ );
53820
+ ctx.ledger.record({
53821
+ step: "credential_mint",
53822
+ started_at: credentialMintStartedAt,
53823
+ ended_at: (/* @__PURE__ */ new Date()).toISOString(),
53824
+ ok: brokered.status === 200,
53825
+ retry_count: 0,
53826
+ ...brokered.status === 200 ? {} : { error_class: classifyHttpStatus(brokered.status) }
53827
+ });
53828
+ if (brokered.status !== 200) {
53829
+ return { ok: false, summary: `[forge-merge] credential broker failed ${brokered.status}: ${redactForLog(JSON.stringify(brokered.json))}` };
53830
+ }
53831
+ const repoRaw = typeof brokered.json.repo === "object" && brokered.json.repo !== null ? brokered.json.repo : null;
53832
+ const token = typeof brokered.json.token === "string" ? brokered.json.token : "";
53833
+ const expiresAt = typeof brokered.json.expires_at === "string" ? brokered.json.expires_at : "";
53834
+ const owner = typeof repoRaw?.owner === "string" ? repoRaw.owner : "";
53835
+ const name = typeof repoRaw?.name === "string" ? repoRaw.name : "";
53836
+ const defaultBranch = typeof repoRaw?.default_branch === "string" ? repoRaw.default_branch : "";
53837
+ if (!token || !expiresAt || !owner || !name || !defaultBranch) {
53838
+ return { ok: false, summary: "[forge-merge] credential broker returned an incomplete credential" };
53839
+ }
53840
+ return { ok: true, credential: { token, expiresAt }, repo: { owner, name, default_branch: defaultBranch }, buildRequestId };
53841
+ }
53842
+ async function fetchSettledPullRequestState(ctx, input) {
53843
+ let state = await fetchPullRequestState({ repo: input.repo, prNumber: input.prNumber, credential: input.credential, fetchImpl: ctx.fetchImpl });
53844
+ for (let attempt = 0; attempt < MERGE_MERGEABILITY_POLLS && state.mergeable === null && !state.merged; attempt += 1) {
53845
+ await sleep3(MERGE_MERGEABILITY_POLL_MS);
53846
+ state = await fetchPullRequestState({ repo: input.repo, prNumber: input.prNumber, credential: input.credential, fetchImpl: ctx.fetchImpl });
53847
+ }
53848
+ return state;
53849
+ }
53850
+ async function pollForQueueMerge(ctx, input) {
53851
+ for (let attempt = 0; attempt < MERGE_QUEUE_POLLS; attempt += 1) {
53852
+ await sleep3(MERGE_QUEUE_POLL_MS);
53853
+ const state = await fetchPullRequestState({ repo: input.repo, prNumber: input.prNumber, credential: input.credential, fetchImpl: ctx.fetchImpl });
53854
+ if (state.merged) {
53855
+ return { mergedSha: state.mergedSha };
53856
+ }
53857
+ }
53858
+ return null;
53859
+ }
53860
+ async function resolveMergeConflictTurn(ctx, input) {
53861
+ const tenantId = typeof input.workOrder.tenant_id === "string" ? input.workOrder.tenant_id : ctx.state.tenant_id ?? "runner";
53862
+ let workspace;
53863
+ try {
53864
+ workspace = await prepareMergeWorkspace({
53865
+ baseDir: path5.join(ctx.config.homeDir, ".rost", "forge", ctx.state.runner_id),
53866
+ tenantId,
53867
+ buildRequestId: input.buildRequestId,
53868
+ repo: input.repo,
53869
+ credential: input.credential,
53870
+ headBranch: input.headBranch,
53871
+ ledger: ctx.ledger
53872
+ });
53873
+ } catch (error51) {
53874
+ return { pushed: false, result: { ok: false, summary: `[forge-merge] conflict workspace preparation failed: ${redactForLog(error51 instanceof Error ? error51.message : String(error51))}`.slice(0, MERGE_CONFLICT_SUMMARY_MAX) } };
53875
+ }
53876
+ const sandbox = resolveSandboxSpec({
53877
+ mode: ctx.config.sandboxMode,
53878
+ platform: process.platform,
53879
+ workspaceDir: workspace.worktreeDir,
53880
+ tmpDir: tmpdir2(),
53881
+ homeDir: ctx.config.homeDir,
53882
+ stateFile: ctx.config.stateFile,
53883
+ runnerBaseDir: path5.join(ctx.config.homeDir, ".rost"),
53884
+ allowWritePaths: [workspace.baseRepo],
53885
+ denyWritePaths: [
53886
+ `${path5.join(workspace.baseRepo, ".git", "hooks")}/`,
53887
+ path5.join(workspace.baseRepo, ".git", "config"),
53888
+ path5.join(workspace.worktreeDir, ".git")
53889
+ ],
53890
+ allowNetwork: ctx.config.sandboxAllowNetwork
53891
+ });
53892
+ const modelResult = await spawnMergeConflictModel(ctx, {
53893
+ workOrder: input.workOrder,
53894
+ execution: input.execution,
53895
+ runtime: input.runtime,
53896
+ workspace,
53897
+ sandbox,
53898
+ defaultBranch: input.repo.default_branch
53899
+ });
53900
+ if (!modelResult.ok) {
53901
+ await removeWorkspace(workspace).catch(() => void 0);
53902
+ return { pushed: false, result: mergeParkResult(`could not resolve the merge conflict (${modelResult.summary.slice(0, 400)}); parking for a human.`) };
53903
+ }
53904
+ try {
53905
+ const pushed = await forcePushWithLease(workspace, input.credential);
53906
+ await removeWorkspace(workspace).catch(() => void 0);
53907
+ return { pushed: true, result: mergeParkResult(`resolved the conflict and force-pushed ${pushed.commitSha}`, { commitSha: pushed.commitSha }) };
53908
+ } catch (error51) {
53909
+ await removeWorkspace(workspace).catch(() => void 0);
53910
+ return { pushed: false, result: { ok: false, summary: `[forge-merge] conflict resolution force-push failed: ${redactForLog(error51 instanceof Error ? error51.message : String(error51))}`.slice(0, MERGE_CONFLICT_SUMMARY_MAX) } };
53911
+ }
53912
+ }
53913
+ async function spawnMergeConflictModel(ctx, input) {
53914
+ const mcp = typeof input.workOrder.mcp === "object" && input.workOrder.mcp !== null ? input.workOrder.mcp : {};
53915
+ const token = typeof mcp.token === "string" ? mcp.token : "";
53916
+ const url2 = typeof mcp.url === "string" ? `${ctx.appUrl.replace(/\/+$/, "")}${mcp.url}` : `${ctx.appUrl.replace(/\/+$/, "")}/mcp`;
53917
+ const mcpConfig = JSON.stringify({ mcpServers: { rost: { type: "http", url: url2, headers: { Authorization: `Bearer ${token}` } } } });
53918
+ const configPath = path5.join(tmpdir2(), `rost-runner-mcp-${createHash2("sha256").update(`${url2}:${Date.now()}:${Math.random()}`).digest("hex").slice(0, 16)}.json`);
53919
+ await writeFile3(configPath, `${mcpConfig}
53920
+ `, { mode: 384 });
53921
+ await chmod2(configPath, 384);
53922
+ const plan = resolveTurnPlan({ kind: "work_order", execution: input.execution, repoDir: input.workspace.worktreeDir });
53923
+ const prompt = [
53924
+ "You are resolving a merge conflict on a pull request branch so it can merge into the base branch.",
53925
+ `A WRITABLE checkout of the PR's head branch is at your current working directory. Run \`git merge origin/${input.defaultBranch}\` to merge the base branch in, resolve EVERY conflict so the result is correct and builds, then \`git add\` the resolved files and \`git commit\` the merge. If the project has a fast build or test, run it to confirm the resolution.`,
53926
+ "Do NOT run git push, git remote, or any network git command, and never request or handle GitHub tokens \u2014 the harness pushes and merges after you finish. If the conflict cannot be resolved safely, do NOT force a bad merge: leave the branch uncommitted and stop with a short explanation.",
53927
+ "End with a single concise final message summarizing exactly what you resolved."
53928
+ ].join("\n");
53929
+ let sandboxProfilePath = null;
53930
+ if (input.sandbox.kind === "seatbelt") {
53931
+ sandboxProfilePath = path5.join(tmpdir2(), `rost-runner-sandbox-${createHash2("sha256").update(`${configPath}:${Math.random()}`).digest("hex").slice(0, 16)}.sb`);
53932
+ await writeFile3(sandboxProfilePath, input.sandbox.profile, { mode: 384 });
53933
+ await chmod2(sandboxProfilePath, 384);
53934
+ }
53935
+ const built = buildTurnCommand({ runtime: input.runtime, prompt, configPath, plan });
53936
+ const { command, args } = wrapCommandWithSandbox(input.sandbox, built.command, built.args, sandboxProfilePath);
53937
+ const spawnStartedAt = (/* @__PURE__ */ new Date()).toISOString();
53938
+ const result = await runModelProcess({
53939
+ command,
53940
+ args,
53941
+ cwd: input.workspace.worktreeDir,
53942
+ timeoutMs: plan.timeoutMs,
53943
+ // The model gets the EXPLICIT allowlist env — never the merge token, GIT_ASKPASS, or the bearer.
53944
+ env: buildModelSpawnEnv(process.env),
53945
+ expectSessionEnvelope: false
53946
+ });
53947
+ const exitClass = classifyModelExit(result);
53948
+ ctx.ledger.record({
53949
+ step: "model_exit",
53950
+ started_at: spawnStartedAt,
53951
+ ended_at: (/* @__PURE__ */ new Date()).toISOString(),
53952
+ ok: result.ok,
53953
+ retry_count: 0,
53954
+ ...exitClass.errorClass ? { error_class: exitClass.errorClass } : {}
53955
+ });
53956
+ void rm4(configPath, { force: true });
53957
+ if (sandboxProfilePath) {
53958
+ void rm4(sandboxProfilePath, { force: true });
53959
+ }
53960
+ return result;
53961
+ }
53962
+ async function runForgeMergeTurn(ctx, workOrder, execution, runtime) {
53963
+ const merge2 = execution.merge;
53964
+ if (!merge2) {
53965
+ return mergeParkResult("no merge target bound on the execution contract; parking for a human.");
53966
+ }
53967
+ const prepared = await brokerForgeMergeCredential(ctx, workOrder);
53968
+ if (!prepared.ok) {
53969
+ return { ok: false, summary: prepared.summary };
53970
+ }
53971
+ const { credential, repo, buildRequestId } = prepared;
53972
+ const mergeStartedAt = (/* @__PURE__ */ new Date()).toISOString();
53973
+ try {
53974
+ const outcome = await executeForgeMerge(ctx, {
53975
+ workOrder,
53976
+ execution,
53977
+ runtime,
53978
+ repo,
53979
+ credential,
53980
+ buildRequestId,
53981
+ prNumber: merge2.pr_number,
53982
+ headBranch: merge2.head_branch
53983
+ });
53984
+ ctx.ledger.record({
53985
+ step: "merge",
53986
+ started_at: mergeStartedAt,
53987
+ ended_at: (/* @__PURE__ */ new Date()).toISOString(),
53988
+ ok: outcome.ok,
53989
+ retry_count: 0,
53990
+ ...outcome.ok ? {} : { error_class: "conflict" }
53991
+ });
53992
+ return outcome;
53993
+ } catch (error51) {
53994
+ ctx.ledger.record({
53995
+ step: "merge",
53996
+ started_at: mergeStartedAt,
53997
+ ended_at: (/* @__PURE__ */ new Date()).toISOString(),
53998
+ ok: false,
53999
+ retry_count: 0,
54000
+ error_class: "server_error"
54001
+ });
54002
+ return { ok: false, summary: `[forge-merge] merge turn failed: ${redactForLog(error51 instanceof Error ? error51.message : String(error51))}`.slice(0, MERGE_CONFLICT_SUMMARY_MAX) };
54003
+ }
54004
+ }
54005
+ async function executeForgeMerge(ctx, input) {
54006
+ const { repo, credential, prNumber } = input;
54007
+ let state = await fetchSettledPullRequestState(ctx, { repo, prNumber, credential });
54008
+ let readiness = classifyMergeReadiness(state);
54009
+ if (readiness === "already_merged") {
54010
+ return mergeMergedResult(state.mergedSha, "PR already merged on GitHub");
54011
+ }
54012
+ const reviewedHeadSha = input.execution.merge?.head_sha;
54013
+ if (!reviewedHeadSha) {
54014
+ return mergeParkResult("no reviewed head SHA bound on the merge contract; parking for a human (cannot verify the PR head was risk-assessed).");
54015
+ }
54016
+ if (state.headSha !== reviewedHeadSha) {
54017
+ return mergeParkResult(`PR head advanced since review (reviewed ${reviewedHeadSha.slice(0, 12)}, GitHub now ${state.headSha.slice(0, 12) || "unknown"}); the new commits need re-review/re-risk \u2014 parking.`);
54018
+ }
54019
+ if (readiness === "blocked" || readiness === "draft" || readiness === "unknown") {
54020
+ return mergeParkResult(`PR is not ready to merge (state=${state.mergeableState}); parking for a human.`);
54021
+ }
54022
+ if (readiness === "conflicted" || readiness === "behind") {
54023
+ const resolved = await resolveMergeConflictTurn(ctx, {
54024
+ workOrder: input.workOrder,
54025
+ execution: input.execution,
54026
+ runtime: input.runtime,
54027
+ repo: input.repo,
54028
+ credential: input.credential,
54029
+ buildRequestId: input.buildRequestId,
54030
+ headBranch: input.headBranch
54031
+ });
54032
+ if (!resolved.pushed) {
54033
+ return resolved.result ?? mergeParkResult("could not resolve the merge conflict; parking for a human.");
54034
+ }
54035
+ state = await fetchSettledPullRequestState(ctx, { repo, prNumber, credential });
54036
+ readiness = classifyMergeReadiness(state);
54037
+ if (readiness === "already_merged") {
54038
+ return mergeMergedResult(state.mergedSha, "PR merged during conflict resolution");
54039
+ }
54040
+ if (readiness !== "mergeable") {
54041
+ return mergeParkResult(`conflict resolved and pushed, but the PR is not yet mergeable (state=${state.mergeableState}); parking for a re-check.`, resolved.result?.commitSha ? { commitSha: resolved.result.commitSha } : void 0);
54042
+ }
54043
+ }
54044
+ const outcome = await mergePullRequest({ repo, state, credential, fetchImpl: ctx.fetchImpl });
54045
+ if (outcome.status === "merged") {
54046
+ return mergeMergedResult(outcome.mergedSha, "PR merged");
54047
+ }
54048
+ const merged = await pollForQueueMerge(ctx, { repo, prNumber, credential });
54049
+ if (merged) {
54050
+ return mergeMergedResult(merged.mergedSha, "PR merged via the merge queue");
54051
+ }
54052
+ return mergeParkResult("auto-merge enabled (merge queue); the merge was not confirmed within the turn \u2014 parking for a re-check.");
54053
+ }
53577
54054
  function scrubTranscriptSessionIds(value) {
53578
54055
  return value.replace(/("?(?:session_id|sessionId)"?\s*[:=]\s*"?)[A-Za-z0-9._-]+("?)/gi, "$1[redacted]$2");
53579
54056
  }