@rosthq/cli 0.7.40 → 0.7.41

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.
@@ -1,3 +1,4 @@
1
+ import { type StepLedger } from "./step-ledger.js";
1
2
  export declare const MODEL_SPAWN_ENV_ALLOWLIST: readonly ["PATH", "HOME", "LANG", "LC_ALL", "TMPDIR", "SHELL", "NODE_OPTIONS"];
2
3
  export declare function buildModelSpawnEnv(base: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
3
4
  export type ForgeCredential = {
@@ -31,9 +32,10 @@ export type PrepareForgeWorkspaceOptions = {
31
32
  };
32
33
  credential: ForgeCredential;
33
34
  remoteUrl?: string;
35
+ ledger?: StepLedger;
34
36
  };
35
37
  export declare function prepareForgeWorkspace(options: PrepareForgeWorkspaceOptions): Promise<ForgeWorkspace>;
36
- export declare function pushChangeset(ws: ForgeWorkspace, credential: ForgeCredential): Promise<{
38
+ export declare function pushChangeset(ws: ForgeWorkspace, credential: ForgeCredential, ledger?: StepLedger): Promise<{
37
39
  commitSha: string;
38
40
  } | null>;
39
41
  export declare function computeChangedPaths(ws: ForgeWorkspace): Promise<string[]>;
@@ -49,6 +51,7 @@ export type OpenDraftPullRequestOptions = {
49
51
  credential: ForgeCredential;
50
52
  apiBaseUrl?: string;
51
53
  fetchImpl?: typeof fetch;
54
+ ledger?: StepLedger;
52
55
  };
53
56
  export type ForgePullRequest = {
54
57
  url: string;
@@ -1 +1 @@
1
- {"version":3,"file":"forge-workspace.d.ts","sourceRoot":"","sources":["../src/forge-workspace.ts"],"names":[],"mappings":"AAmDA,eAAO,MAAM,yBAAyB,gFAQ5B,CAAC;AAEX,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAW7E;AAID,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAG3B,QAAQ,EAAE,MAAM,CAAC;IAGjB,WAAW,EAAE,MAAM,CAAC;IAGpB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAKlB,OAAO,EAAE,MAAM,CAAC;IAIhB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEvD,MAAM,MAAM,4BAA4B,GAAG;IAEzC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1D,OAAO,EAAE,qBAAqB,CAAC;IAE/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9D,UAAU,EAAE,eAAe,CAAC;IAI5B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AA+QF,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,OAAO,CAAC,cAAc,CAAC,CAmC1G;AA8CD,wBAAsB,aAAa,CACjC,EAAE,EAAE,cAAc,EAClB,UAAU,EAAE,eAAe,GAC1B,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CA4BvC;AAsBD,wBAAsB,mBAAmB,CAAC,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAiC/E;AAcD,MAAM,MAAM,2BAA2B,GAAG;IACxC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,CAAC;IAE9D,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,eAAe,CAAC;IAG5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAE7B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IAGf,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAoGF,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,2BAA2B,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAkC1G;AAiBD,wBAAsB,eAAe,CAAC,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAOvE"}
1
+ {"version":3,"file":"forge-workspace.d.ts","sourceRoot":"","sources":["../src/forge-workspace.ts"],"names":[],"mappings":"AA2CA,OAAO,EAAqB,KAAK,UAAU,EAAiB,MAAM,kBAAkB,CAAC;AASrF,eAAO,MAAM,yBAAyB,gFAQ5B,CAAC;AAEX,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAW7E;AAID,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAG3B,QAAQ,EAAE,MAAM,CAAC;IAGjB,WAAW,EAAE,MAAM,CAAC;IAGpB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAKlB,OAAO,EAAE,MAAM,CAAC;IAIhB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEvD,MAAM,MAAM,4BAA4B,GAAG;IAEzC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1D,OAAO,EAAE,qBAAqB,CAAC;IAE/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9D,UAAU,EAAE,eAAe,CAAC;IAI5B,SAAS,CAAC,EAAE,MAAM,CAAC;IAGnB,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB,CAAC;AA2TF,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,OAAO,CAAC,cAAc,CAAC,CAoC1G;AAiDD,wBAAsB,aAAa,CACjC,EAAE,EAAE,cAAc,EAClB,UAAU,EAAE,eAAe,EAC3B,MAAM,CAAC,EAAE,UAAU,GAClB,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CA4BvC;AAsBD,wBAAsB,mBAAmB,CAAC,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAiC/E;AAcD,MAAM,MAAM,2BAA2B,GAAG;IACxC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,CAAC;IAE9D,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,eAAe,CAAC;IAG5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAGzB,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAE7B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IAGf,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AA+GF,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,2BAA2B,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAE1G;AAqDD,wBAAsB,eAAe,CAAC,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAOvE"}
package/dist/index.js CHANGED
@@ -51788,6 +51788,65 @@ import { tmpdir } from "node:os";
51788
51788
  import path3 from "node:path";
51789
51789
  import { setTimeout as sleep2 } from "node:timers/promises";
51790
51790
  import { promisify as promisify3 } from "node:util";
51791
+
51792
+ // src/step-ledger.ts
51793
+ var MAX_LEDGER_ENTRIES = 32;
51794
+ var MAX_TRANSCRIPT_CHARS = 4e4;
51795
+ var StepLedger = class {
51796
+ #entries = [];
51797
+ record(entry) {
51798
+ this.#entries.push(entry);
51799
+ if (this.#entries.length > MAX_LEDGER_ENTRIES) {
51800
+ this.#entries.shift();
51801
+ }
51802
+ }
51803
+ entries() {
51804
+ return [...this.#entries];
51805
+ }
51806
+ /** The first failed entry, if any — used to step-tag error_logs. */
51807
+ firstFailure() {
51808
+ return this.#entries.find((entry) => !entry.ok);
51809
+ }
51810
+ };
51811
+ function classifyStepError(message) {
51812
+ const lower = message.toLowerCase();
51813
+ if (/authentication failed|invalid username or password|permission denied|unauthorized|\b401\b|\b403\b/.test(lower)) {
51814
+ return "auth";
51815
+ }
51816
+ if (/already exists|\b409\b|\b422\b/.test(lower)) {
51817
+ return "conflict";
51818
+ }
51819
+ if (/timed out|timeout/.test(lower)) {
51820
+ return "timeout";
51821
+ }
51822
+ if (/no space left|enospc/.test(lower)) {
51823
+ return "disk";
51824
+ }
51825
+ if (/exhausted retries/.test(lower)) {
51826
+ return "retry_exhausted";
51827
+ }
51828
+ if (/could not resolve host|couldn't resolve|connection (refused|reset)|econnrefused|enotfound|network|early eof|rpc failed|the remote end hung up/.test(lower)) {
51829
+ return "network";
51830
+ }
51831
+ return "unknown";
51832
+ }
51833
+ function classifyHttpStatus(status) {
51834
+ if (status === 401 || status === 403) {
51835
+ return "auth";
51836
+ }
51837
+ if (status === 409 || status === 422) {
51838
+ return "conflict";
51839
+ }
51840
+ if (status === 429) {
51841
+ return "rate_limited";
51842
+ }
51843
+ if (status >= 500) {
51844
+ return "server_error";
51845
+ }
51846
+ return "unknown";
51847
+ }
51848
+
51849
+ // src/forge-workspace.ts
51791
51850
  var execFile3 = promisify3(execFileCallback3);
51792
51851
  var MODEL_SPAWN_ENV_ALLOWLIST = [
51793
51852
  "PATH",
@@ -51827,6 +51886,28 @@ var LOCAL_GIT_TIMEOUT_MS = 6e4;
51827
51886
  var NETWORK_RETRY_ATTEMPTS = 3;
51828
51887
  var NETWORK_RETRY_BASE_DELAY_MS = 500;
51829
51888
  var GIT_MAX_BUFFER = 64 * 1024 * 1024;
51889
+ async function withLedgerStep(ledger, step, fn) {
51890
+ if (!ledger) {
51891
+ return fn({ count: 0 });
51892
+ }
51893
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
51894
+ const retryCounter = { count: 0 };
51895
+ try {
51896
+ const result = await fn(retryCounter);
51897
+ ledger.record({ step, started_at: startedAt, ended_at: (/* @__PURE__ */ new Date()).toISOString(), ok: true, retry_count: retryCounter.count });
51898
+ return result;
51899
+ } catch (error51) {
51900
+ ledger.record({
51901
+ step,
51902
+ started_at: startedAt,
51903
+ ended_at: (/* @__PURE__ */ new Date()).toISOString(),
51904
+ ok: false,
51905
+ error_class: classifyStepError(error51 instanceof Error ? error51.message : String(error51)),
51906
+ retry_count: retryCounter.count
51907
+ });
51908
+ throw error51;
51909
+ }
51910
+ }
51830
51911
  var FORGE_GIT_AUTHOR_NAME = `${BRAND.name} ${RSF_ADDON_BRAND.shortName}`;
51831
51912
  var FORGE_GIT_AUTHOR_EMAIL = `${RSF_ADDON_BRAND.cliGroup}@${BRAND_DISTRIBUTION.cliBinName}.invalid`;
51832
51913
  function shortRequestId(buildRequestId) {
@@ -51894,13 +51975,20 @@ function isRetryableGitError(message) {
51894
51975
  }
51895
51976
  return /could not resolve host|couldn't resolve|connection (refused|reset|timed out)|connection was reset|timed out|timeout|early eof|rpc failed|unable to access|fetch-pack|the remote end hung up|ssl|tls|500|502|503|429|temporary failure/.test(lower);
51896
51977
  }
51897
- async function withNetworkRetries(op, fn) {
51978
+ async function withNetworkRetries(op, fn, retryCounter) {
51898
51979
  let lastError;
51899
51980
  for (let attempt = 1; attempt <= NETWORK_RETRY_ATTEMPTS; attempt += 1) {
51900
51981
  try {
51901
- return await fn();
51982
+ const result = await fn();
51983
+ if (retryCounter) {
51984
+ retryCounter.count = attempt - 1;
51985
+ }
51986
+ return result;
51902
51987
  } catch (error51) {
51903
51988
  lastError = error51;
51989
+ if (retryCounter) {
51990
+ retryCounter.count = attempt - 1;
51991
+ }
51904
51992
  const message = error51 instanceof Error ? error51.message : String(error51);
51905
51993
  if (attempt >= NETWORK_RETRY_ATTEMPTS || !isRetryableGitError(message)) {
51906
51994
  break;
@@ -51943,7 +52031,7 @@ async function openAskpassSession(credential) {
51943
52031
  }
51944
52032
  };
51945
52033
  }
51946
- async function runGitNetwork(op, args, env, cwd, timeoutMs = FETCH_TIMEOUT_MS) {
52034
+ async function runGitNetwork(op, args, env, cwd, timeoutMs = FETCH_TIMEOUT_MS, retryCounter) {
51947
52035
  return withNetworkRetries(op, async () => {
51948
52036
  try {
51949
52037
  const { stdout } = await execFile3("git", [...GIT_HARDENING_ARGS, ...args], {
@@ -51956,7 +52044,7 @@ async function runGitNetwork(op, args, env, cwd, timeoutMs = FETCH_TIMEOUT_MS) {
51956
52044
  } catch (error51) {
51957
52045
  throw new Error(`${op} failed: ${redactGitError(error51)}`);
51958
52046
  }
51959
- });
52047
+ }, retryCounter);
51960
52048
  }
51961
52049
  async function withAskpass(credential, fn) {
51962
52050
  const session = await openAskpassSession(credential);
@@ -51994,9 +52082,9 @@ async function prepareForgeWorkspace(options) {
51994
52082
  const baseRepo = baseRepoDir(options.baseDir, options.buildRequestId);
51995
52083
  const url2 = remoteUrlFor(options);
51996
52084
  await mkdir2(path3.dirname(baseRepo), { recursive: true, mode: 448 });
51997
- await withAskpass(options.credential, async (netEnv) => {
52085
+ await withLedgerStep(options.ledger, "clone", (retryCounter) => withAskpass(options.credential, async (netEnv) => {
51998
52086
  if (await isGitRepo(baseRepo)) {
51999
- await runGitNetwork("git fetch", ["fetch", "--prune", "--quiet", url2, "+refs/heads/*:refs/remotes/origin/*"], netEnv, baseRepo, FETCH_TIMEOUT_MS);
52087
+ await runGitNetwork("git fetch", ["fetch", "--prune", "--quiet", url2, "+refs/heads/*:refs/remotes/origin/*"], netEnv, baseRepo, FETCH_TIMEOUT_MS, retryCounter);
52000
52088
  } else {
52001
52089
  await runGitNetwork(
52002
52090
  "git clone",
@@ -52005,10 +52093,11 @@ async function prepareForgeWorkspace(options) {
52005
52093
  ["clone", "--no-checkout", "--quiet", url2, baseRepo],
52006
52094
  netEnv,
52007
52095
  void 0,
52008
- CLONE_TIMEOUT_MS
52096
+ CLONE_TIMEOUT_MS,
52097
+ retryCounter
52009
52098
  );
52010
52099
  }
52011
- });
52100
+ }));
52012
52101
  await configureForgeIdentity(baseRepo);
52013
52102
  if (options.profile === "review") {
52014
52103
  return await prepareReviewWorktree(options, baseRepo);
@@ -52019,13 +52108,15 @@ async function prepareBuildWorktree(options, baseRepo) {
52019
52108
  const branch = branchName(options.buildRequestId, options.changeset);
52020
52109
  const worktreeDir = buildWorktreeDir(options.baseDir, options.buildRequestId, options.changeset.ordinal);
52021
52110
  const originDefault = `origin/${options.repo.default_branch}`;
52022
- if (!await worktreeRegistered(baseRepo, worktreeDir)) {
52023
- if (await localBranchExists(baseRepo, branch)) {
52024
- await runGit(["worktree", "add", worktreeDir, branch], baseRepo);
52025
- } else {
52026
- await runGit(["worktree", "add", "-b", branch, worktreeDir, originDefault], baseRepo);
52111
+ await withLedgerStep(options.ledger, "branch", async () => {
52112
+ if (!await worktreeRegistered(baseRepo, worktreeDir)) {
52113
+ if (await localBranchExists(baseRepo, branch)) {
52114
+ await runGit(["worktree", "add", worktreeDir, branch], baseRepo);
52115
+ } else {
52116
+ await runGit(["worktree", "add", "-b", branch, worktreeDir, originDefault], baseRepo);
52117
+ }
52027
52118
  }
52028
- }
52119
+ });
52029
52120
  const baseSha = (await runGit(["merge-base", "HEAD", originDefault], worktreeDir)).trim();
52030
52121
  return { baseRepo, worktreeDir, branch, readOnly: false, baseSha, remoteUrl: remoteUrlFor(options) };
52031
52122
  }
@@ -52041,7 +52132,7 @@ async function prepareReviewWorktree(options, baseRepo) {
52041
52132
  await runGit(["worktree", "add", "--detach", worktreeDir, headSha], baseRepo);
52042
52133
  return { baseRepo, worktreeDir, branch: "", readOnly: true, baseSha: headSha, remoteUrl: remoteUrlFor(options) };
52043
52134
  }
52044
- async function pushChangeset(ws, credential) {
52135
+ async function pushChangeset(ws, credential, ledger) {
52045
52136
  if (ws.readOnly || ws.branch.length === 0) {
52046
52137
  return null;
52047
52138
  }
@@ -52050,14 +52141,14 @@ async function pushChangeset(ws, credential) {
52050
52141
  if (!Number.isFinite(ahead) || ahead <= 0) {
52051
52142
  return null;
52052
52143
  }
52053
- await withAskpass(credential, async (netEnv) => {
52054
- await runGitNetwork("git push", ["push", "--quiet", ws.remoteUrl, `HEAD:refs/heads/${ws.branch}`], netEnv, ws.worktreeDir, PUSH_TIMEOUT_MS);
52055
- const remote = await runGitNetwork("git ls-remote", ["ls-remote", ws.remoteUrl, `refs/heads/${ws.branch}`], netEnv, ws.worktreeDir, FETCH_TIMEOUT_MS);
52144
+ await withLedgerStep(ledger, "push", (retryCounter) => withAskpass(credential, async (netEnv) => {
52145
+ await runGitNetwork("git push", ["push", "--quiet", ws.remoteUrl, `HEAD:refs/heads/${ws.branch}`], netEnv, ws.worktreeDir, PUSH_TIMEOUT_MS, retryCounter);
52146
+ const remote = await runGitNetwork("git ls-remote", ["ls-remote", ws.remoteUrl, `refs/heads/${ws.branch}`], netEnv, ws.worktreeDir, FETCH_TIMEOUT_MS, retryCounter);
52056
52147
  const remoteSha = remote.split(/\s+/)[0]?.trim() ?? "";
52057
52148
  if (remoteSha !== head2) {
52058
52149
  throw new Error(`git push postcondition failed: remote ${ws.branch} is ${remoteSha || "absent"}, expected ${head2}`);
52059
52150
  }
52060
- });
52151
+ }));
52061
52152
  return { commitSha: head2 };
52062
52153
  }
52063
52154
  async function computeChangedPaths(ws) {
@@ -52118,19 +52209,28 @@ function draftPrBody(options) {
52118
52209
  `The approved plan and task checklist are tracked on the ${RSF_ADDON_BRAND.shortName} build request above. This PR is a draft; the runner never merges.`
52119
52210
  ].join("\n");
52120
52211
  }
52121
- async function withGithubRetries(op, fn) {
52212
+ async function withGithubRetries(op, fn, retryCounter) {
52122
52213
  let lastError;
52123
52214
  for (let attempt = 1; attempt <= PR_OPEN_RETRY_ATTEMPTS; attempt += 1) {
52124
52215
  try {
52125
52216
  const response = await fn();
52126
52217
  if ((response.status >= 500 || response.status === 429) && attempt < PR_OPEN_RETRY_ATTEMPTS) {
52127
52218
  lastError = new Error(`${op} transient status ${response.status}`);
52219
+ if (retryCounter) {
52220
+ retryCounter.count = attempt - 1;
52221
+ }
52128
52222
  await sleep2(PR_OPEN_RETRY_BASE_DELAY_MS * 2 ** (attempt - 1));
52129
52223
  continue;
52130
52224
  }
52225
+ if (retryCounter) {
52226
+ retryCounter.count = attempt - 1;
52227
+ }
52131
52228
  return response;
52132
52229
  } catch (error51) {
52133
52230
  lastError = error51;
52231
+ if (retryCounter) {
52232
+ retryCounter.count = attempt - 1;
52233
+ }
52134
52234
  if (attempt >= PR_OPEN_RETRY_ATTEMPTS) {
52135
52235
  break;
52136
52236
  }
@@ -52156,7 +52256,7 @@ function toForgePullRequest(json2, adopted) {
52156
52256
  }
52157
52257
  return { url: url2, number: number4, draft, adopted };
52158
52258
  }
52159
- async function adoptExistingPullRequest(baseUrl, fetchImpl, options) {
52259
+ async function adoptExistingPullRequest(baseUrl, fetchImpl, options, retryCounter) {
52160
52260
  const { owner, name } = options.repo;
52161
52261
  const head2 = `${owner}:${options.branch}`;
52162
52262
  const response = await withGithubRetries(
@@ -52164,7 +52264,8 @@ async function adoptExistingPullRequest(baseUrl, fetchImpl, options) {
52164
52264
  async () => fetchImpl(`${baseUrl}/repos/${owner}/${name}/pulls?state=open&head=${encodeURIComponent(head2)}`, {
52165
52265
  method: "GET",
52166
52266
  headers: githubHeaders(options.credential)
52167
- })
52267
+ }),
52268
+ retryCounter
52168
52269
  );
52169
52270
  if (response.status !== 200) {
52170
52271
  return null;
@@ -52176,6 +52277,9 @@ async function adoptExistingPullRequest(baseUrl, fetchImpl, options) {
52176
52277
  return toForgePullRequest(list[0], true);
52177
52278
  }
52178
52279
  async function openDraftPullRequest(options) {
52280
+ return withLedgerStep(options.ledger, "pr_open", (retryCounter) => openDraftPullRequestInner(options, retryCounter));
52281
+ }
52282
+ async function openDraftPullRequestInner(options, retryCounter) {
52179
52283
  const baseUrl = (options.apiBaseUrl ?? GITHUB_API_BASE_URL).replace(/\/+$/, "");
52180
52284
  const fetchImpl = options.fetchImpl ?? fetch;
52181
52285
  const { owner, name } = options.repo;
@@ -52192,7 +52296,8 @@ async function openDraftPullRequest(options) {
52192
52296
  draft: true,
52193
52297
  maintainer_can_modify: true
52194
52298
  })
52195
- })
52299
+ }),
52300
+ retryCounter
52196
52301
  );
52197
52302
  if (created.status === 201) {
52198
52303
  return toForgePullRequest(await created.json(), false);
@@ -52200,7 +52305,7 @@ async function openDraftPullRequest(options) {
52200
52305
  if (created.status === 422) {
52201
52306
  const detail = await readErrorText(created);
52202
52307
  if (/already exists/i.test(detail)) {
52203
- const existing = await adoptExistingPullRequest(baseUrl, fetchImpl, options);
52308
+ const existing = await adoptExistingPullRequest(baseUrl, fetchImpl, options, retryCounter);
52204
52309
  if (existing) {
52205
52310
  return existing;
52206
52311
  }
@@ -52908,6 +53013,7 @@ async function post2(fetchImpl, appUrl2, pathName, body, token) {
52908
53013
  return { status: response.status, json: json2 };
52909
53014
  }
52910
53015
  async function claimAndExecute(fetchImpl, appUrl2, state, config2, io, runtime, options = {}) {
53016
+ const claimStartedAt = (/* @__PURE__ */ new Date()).toISOString();
52911
53017
  const claimed = await post2(
52912
53018
  fetchImpl,
52913
53019
  appUrl2,
@@ -52915,9 +53021,18 @@ async function claimAndExecute(fetchImpl, appUrl2, state, config2, io, runtime,
52915
53021
  options.interactiveOnly ? { interactive_only: true } : {},
52916
53022
  state.runner_secret
52917
53023
  );
53024
+ const ledger = new StepLedger();
53025
+ ledger.record({
53026
+ step: "claim",
53027
+ started_at: claimStartedAt,
53028
+ ended_at: (/* @__PURE__ */ new Date()).toISOString(),
53029
+ ok: claimed.status === 200,
53030
+ retry_count: 0,
53031
+ ...claimed.status === 200 ? {} : { error_class: classifyHttpStatus(claimed.status) }
53032
+ });
52918
53033
  const turnExecution = claimed.json.turn_execution;
52919
53034
  if (claimed.status === 200 && turnExecution && typeof turnExecution === "object") {
52920
- await executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtime, {
53035
+ await executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtime, ledger, {
52921
53036
  kind: "turn_execution",
52922
53037
  idField: "turn_execution_id",
52923
53038
  item: turnExecution,
@@ -52931,7 +53046,7 @@ async function claimAndExecute(fetchImpl, appUrl2, state, config2, io, runtime,
52931
53046
  if (claimed.status !== 200 || !workOrder || typeof workOrder !== "object") {
52932
53047
  return;
52933
53048
  }
52934
- await executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtime, {
53049
+ await executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtime, ledger, {
52935
53050
  kind: "work_order",
52936
53051
  idField: "work_order_id",
52937
53052
  item: workOrder,
@@ -52940,7 +53055,7 @@ async function claimAndExecute(fetchImpl, appUrl2, state, config2, io, runtime,
52940
53055
  logRef: (id) => `runner-local:${id}`
52941
53056
  });
52942
53057
  }
52943
- async function executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtime, input) {
53058
+ async function executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtime, ledger, input) {
52944
53059
  const id = input.item.id;
52945
53060
  if (typeof id !== "string") {
52946
53061
  return;
@@ -52950,7 +53065,7 @@ async function executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtim
52950
53065
  throw new Error(`${claimedUnitLabel(input.kind)} start failed ${started.status}: ${redactForLog(JSON.stringify(started.json))}`);
52951
53066
  }
52952
53067
  const turnRuntime = runtimeForClaimedUnit(input.item, runtime, input.kind);
52953
- const result = turnRuntime ? await runLocalTurn({ fetchImpl, appUrl: appUrl2, state, config: config2, io }, input.item, turnRuntime, input.kind) : {
53068
+ const result = turnRuntime ? await runLocalTurn({ fetchImpl, appUrl: appUrl2, state, config: config2, io, ledger }, input.item, turnRuntime, input.kind) : {
52954
53069
  ok: false,
52955
53070
  summary: `Runner runtime ${runtime} cannot execute requested ${String(input.item.brain ?? "unknown")} turn.`
52956
53071
  };
@@ -52977,7 +53092,13 @@ async function executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtim
52977
53092
  ...result.branch ? { branch: result.branch } : {},
52978
53093
  ...result.commitSha ? { commit_sha: result.commitSha } : {},
52979
53094
  ...result.evidence && result.evidence.length > 0 ? { evidence: result.evidence } : {},
52980
- ...result.changedPaths && result.changedPaths.length > 0 ? { changed_paths: result.changedPaths } : {}
53095
+ ...result.changedPaths && result.changedPaths.length > 0 ? { changed_paths: result.changedPaths } : {},
53096
+ // DER-1331 (T1.9): the step ledger + the model's (already redacted, bounded) transcript.
53097
+ // Only the work-orders result route accepts these today (turn-executions is a separate,
53098
+ // narrower endpoint — T1.9 scope); its Zod schema strips unrecognized keys on a non-strict
53099
+ // parse either way, so sending them there is harmless, just unused.
53100
+ ...input.kind === "work_order" ? { step_ledger: ledger.entries() } : {},
53101
+ ...input.kind === "work_order" && result.transcript ? { transcript: result.transcript } : {}
52981
53102
  }, state.runner_secret);
52982
53103
  if (reported.status !== 200) {
52983
53104
  throw new Error(`${claimedUnitLabel(input.kind)} result failed ${reported.status}: ${redactForLog(JSON.stringify(reported.json))}`);
@@ -53091,6 +53212,7 @@ async function prepareForgeTurn(ctx, workOrder, execution) {
53091
53212
  if (!workOrderId || !buildRequestId) {
53092
53213
  return { ok: false, summary: "forge turn missing work order id or build request id" };
53093
53214
  }
53215
+ const credentialMintStartedAt = (/* @__PURE__ */ new Date()).toISOString();
53094
53216
  const brokered = await post2(
53095
53217
  ctx.fetchImpl,
53096
53218
  ctx.appUrl,
@@ -53098,6 +53220,14 @@ async function prepareForgeTurn(ctx, workOrder, execution) {
53098
53220
  {},
53099
53221
  ctx.state.runner_secret
53100
53222
  );
53223
+ ctx.ledger.record({
53224
+ step: "credential_mint",
53225
+ started_at: credentialMintStartedAt,
53226
+ ended_at: (/* @__PURE__ */ new Date()).toISOString(),
53227
+ ok: brokered.status === 200,
53228
+ retry_count: 0,
53229
+ ...brokered.status === 200 ? {} : { error_class: classifyHttpStatus(brokered.status) }
53230
+ });
53101
53231
  if (brokered.status !== 200) {
53102
53232
  return { ok: false, summary: `forge credential broker failed ${brokered.status}: ${redactForLog(JSON.stringify(brokered.json))}` };
53103
53233
  }
@@ -53125,7 +53255,8 @@ async function prepareForgeTurn(ctx, workOrder, execution) {
53125
53255
  profile: isBuild ? "build" : "review",
53126
53256
  ...isBuild ? {} : { headSha: `origin/${defaultBranch}` },
53127
53257
  repo: { owner, name, default_branch: defaultBranch },
53128
- credential
53258
+ credential,
53259
+ ledger: ctx.ledger
53129
53260
  });
53130
53261
  } catch (error51) {
53131
53262
  return { ok: false, summary: `forge workspace preparation failed: ${redactForLog(error51 instanceof Error ? error51.message : String(error51))}` };
@@ -53169,11 +53300,11 @@ function forgeChangedPathsField(changedPaths) {
53169
53300
  const withinCaps = changedPaths.length > 0 && changedPaths.length <= FORGE_MAX_CHANGED_PATHS && changedPaths.every((path7) => path7.length >= 1 && path7.length <= FORGE_MAX_CHANGED_PATH_LENGTH);
53170
53301
  return withinCaps ? { changedPaths: [...changedPaths] } : {};
53171
53302
  }
53172
- async function finalizeForgeBuildPush(prep, result) {
53303
+ async function finalizeForgeBuildPush(ctx, prep, result) {
53173
53304
  const credential = { token: prep.credential.token, expiresAt: prep.credential.expiresAt };
53174
53305
  let pushed;
53175
53306
  try {
53176
- pushed = await pushChangeset(prep.workspace, credential);
53307
+ pushed = await pushChangeset(prep.workspace, credential, ctx.ledger);
53177
53308
  } catch (error51) {
53178
53309
  return {
53179
53310
  ...result,
@@ -53199,7 +53330,8 @@ async function finalizeForgeBuildPush(prep, result) {
53199
53330
  branch: prep.workspace.branch,
53200
53331
  buildRequestId: prep.buildRequestId,
53201
53332
  commitSha: pushed.commitSha,
53202
- credential
53333
+ credential,
53334
+ ledger: ctx.ledger
53203
53335
  });
53204
53336
  const evidence = [
53205
53337
  { kind: "note", summary: `pushed ${pushed.commitSha} to ${prep.workspace.branch}`.slice(0, 500) },
@@ -53305,6 +53437,7 @@ async function spawnRunnerTurn(ctx, workOrder, runtime, kind) {
53305
53437
  }
53306
53438
  const { command, args } = wrapCommandWithSandbox(sandbox, built.command, built.args, sandboxProfilePath);
53307
53439
  const expectSessionEnvelope = runtime === "claude" && kind === "turn_execution";
53440
+ const spawnStartedAt = (/* @__PURE__ */ new Date()).toISOString();
53308
53441
  let result = await runModelProcess({
53309
53442
  command,
53310
53443
  args,
@@ -53315,13 +53448,32 @@ async function spawnRunnerTurn(ctx, workOrder, runtime, kind) {
53315
53448
  expectSessionEnvelope,
53316
53449
  resumeSessionId
53317
53450
  });
53451
+ const exitClass = classifyModelExit(result);
53452
+ ctx.ledger.record({
53453
+ step: "spawn",
53454
+ started_at: spawnStartedAt,
53455
+ ended_at: spawnStartedAt,
53456
+ ok: !exitClass.spawnFailed,
53457
+ retry_count: 0,
53458
+ ...exitClass.spawnFailed ? { error_class: "spawn_error" } : {}
53459
+ });
53460
+ if (!exitClass.spawnFailed) {
53461
+ ctx.ledger.record({
53462
+ step: "model_exit",
53463
+ started_at: spawnStartedAt,
53464
+ ended_at: (/* @__PURE__ */ new Date()).toISOString(),
53465
+ ok: result.ok,
53466
+ retry_count: 0,
53467
+ ...exitClass.errorClass ? { error_class: exitClass.errorClass } : {}
53468
+ });
53469
+ }
53318
53470
  void rm4(configPath, { force: true });
53319
53471
  if (sandboxProfilePath) {
53320
53472
  void rm4(sandboxProfilePath, { force: true });
53321
53473
  }
53322
53474
  if (forgePrep) {
53323
53475
  if (plan.profile === "forge_build" && result.ok && forgePrep.credential.canPush) {
53324
- result = await finalizeForgeBuildPush(forgePrep, result);
53476
+ result = await finalizeForgeBuildPush(ctx, forgePrep, result);
53325
53477
  }
53326
53478
  if (plan.profile === "forge_review") {
53327
53479
  await removeWorkspace(forgePrep.workspace).catch((error51) => {
@@ -53332,6 +53484,9 @@ async function spawnRunnerTurn(ctx, workOrder, runtime, kind) {
53332
53484
  }
53333
53485
  return result;
53334
53486
  }
53487
+ function scrubTranscriptSessionIds(value) {
53488
+ return value.replace(/("?(?:session_id|sessionId)"?\s*[:=]\s*"?)[A-Za-z0-9._-]+("?)/gi, "$1[redacted]$2");
53489
+ }
53335
53490
  function runModelProcess(input) {
53336
53491
  return new Promise((resolve2) => {
53337
53492
  let settled = false;
@@ -53351,7 +53506,12 @@ function runModelProcess(input) {
53351
53506
  let err = "";
53352
53507
  const timer = setTimeout(() => {
53353
53508
  child.kill("SIGKILL");
53354
- finish({ ok: false, summary: `turn timed out after ${Math.round(input.timeoutMs / 1e3)}s` });
53509
+ const partial2 = scrubTranscriptSessionIds(redactForLog((out || err || "").trim()));
53510
+ finish({
53511
+ ok: false,
53512
+ summary: `turn timed out after ${Math.round(input.timeoutMs / 1e3)}s`,
53513
+ ...partial2.length > 0 ? { transcript: partial2.slice(0, MAX_TRANSCRIPT_CHARS) } : {}
53514
+ });
53355
53515
  }, input.timeoutMs);
53356
53516
  child.stdout.on("data", (data) => {
53357
53517
  out += data.toString("utf8");
@@ -53362,9 +53522,11 @@ function runModelProcess(input) {
53362
53522
  child.on("close", (code) => {
53363
53523
  clearTimeout(timer);
53364
53524
  const rawOutput = (out || err || "completed").trim();
53525
+ const redacted = redactForLog(rawOutput);
53365
53526
  finish({
53366
53527
  ok: code === 0,
53367
- summary: extractRunnerSummary(redactForLog(rawOutput)).slice(0, 12e3),
53528
+ summary: extractRunnerSummary(redacted).slice(0, 12e3),
53529
+ transcript: scrubTranscriptSessionIds(redacted).slice(0, MAX_TRANSCRIPT_CHARS),
53368
53530
  ...input.expectSessionEnvelope ? extractClaudeSessionId(rawOutput) : {}
53369
53531
  });
53370
53532
  });
@@ -53374,6 +53536,18 @@ function runModelProcess(input) {
53374
53536
  });
53375
53537
  });
53376
53538
  }
53539
+ function classifyModelExit(result) {
53540
+ if (result.summary.startsWith("spawn error:")) {
53541
+ return { spawnFailed: true };
53542
+ }
53543
+ if (result.summary.startsWith("turn timed out after")) {
53544
+ return { spawnFailed: false, errorClass: "timeout" };
53545
+ }
53546
+ if (!result.ok) {
53547
+ return { spawnFailed: false, errorClass: "nonzero_exit" };
53548
+ }
53549
+ return { spawnFailed: false };
53550
+ }
53377
53551
  function extractRunnerSummary(output) {
53378
53552
  const trimmed = output.trim();
53379
53553
  if (!trimmed.startsWith("{")) {