@rosthq/cli 0.7.39 → 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,11 +32,13 @@ 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>;
41
+ export declare function computeChangedPaths(ws: ForgeWorkspace): Promise<string[]>;
39
42
  export type OpenDraftPullRequestOptions = {
40
43
  repo: {
41
44
  owner: string;
@@ -48,6 +51,7 @@ export type OpenDraftPullRequestOptions = {
48
51
  credential: ForgeCredential;
49
52
  apiBaseUrl?: string;
50
53
  fetchImpl?: typeof fetch;
54
+ ledger?: StepLedger;
51
55
  };
52
56
  export type ForgePullRequest = {
53
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;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,16 +52141,46 @@ 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
  }
52154
+ async function computeChangedPaths(ws) {
52155
+ if (ws.readOnly || ws.branch.length === 0) {
52156
+ return [];
52157
+ }
52158
+ const raw = await runGit(
52159
+ ["diff", "--name-status", "-M", "-z", `${ws.baseSha}..HEAD`],
52160
+ ws.worktreeDir
52161
+ );
52162
+ const tokens = raw.split("\0").filter((token) => token.length > 0);
52163
+ const paths = /* @__PURE__ */ new Set();
52164
+ let i = 0;
52165
+ while (i < tokens.length) {
52166
+ const status = tokens[i] ?? "";
52167
+ i += 1;
52168
+ const isRenameOrCopy = status.startsWith("R") || status.startsWith("C");
52169
+ const src = tokens[i];
52170
+ if (src !== void 0) {
52171
+ paths.add(src);
52172
+ i += 1;
52173
+ }
52174
+ if (isRenameOrCopy) {
52175
+ const dst = tokens[i];
52176
+ if (dst !== void 0) {
52177
+ paths.add(dst);
52178
+ i += 1;
52179
+ }
52180
+ }
52181
+ }
52182
+ return [...paths];
52183
+ }
52063
52184
  var GITHUB_API_BASE_URL = "https://api.github.com";
52064
52185
  var PR_OPEN_RETRY_ATTEMPTS = 3;
52065
52186
  var PR_OPEN_RETRY_BASE_DELAY_MS = 500;
@@ -52088,19 +52209,28 @@ function draftPrBody(options) {
52088
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.`
52089
52210
  ].join("\n");
52090
52211
  }
52091
- async function withGithubRetries(op, fn) {
52212
+ async function withGithubRetries(op, fn, retryCounter) {
52092
52213
  let lastError;
52093
52214
  for (let attempt = 1; attempt <= PR_OPEN_RETRY_ATTEMPTS; attempt += 1) {
52094
52215
  try {
52095
52216
  const response = await fn();
52096
52217
  if ((response.status >= 500 || response.status === 429) && attempt < PR_OPEN_RETRY_ATTEMPTS) {
52097
52218
  lastError = new Error(`${op} transient status ${response.status}`);
52219
+ if (retryCounter) {
52220
+ retryCounter.count = attempt - 1;
52221
+ }
52098
52222
  await sleep2(PR_OPEN_RETRY_BASE_DELAY_MS * 2 ** (attempt - 1));
52099
52223
  continue;
52100
52224
  }
52225
+ if (retryCounter) {
52226
+ retryCounter.count = attempt - 1;
52227
+ }
52101
52228
  return response;
52102
52229
  } catch (error51) {
52103
52230
  lastError = error51;
52231
+ if (retryCounter) {
52232
+ retryCounter.count = attempt - 1;
52233
+ }
52104
52234
  if (attempt >= PR_OPEN_RETRY_ATTEMPTS) {
52105
52235
  break;
52106
52236
  }
@@ -52126,7 +52256,7 @@ function toForgePullRequest(json2, adopted) {
52126
52256
  }
52127
52257
  return { url: url2, number: number4, draft, adopted };
52128
52258
  }
52129
- async function adoptExistingPullRequest(baseUrl, fetchImpl, options) {
52259
+ async function adoptExistingPullRequest(baseUrl, fetchImpl, options, retryCounter) {
52130
52260
  const { owner, name } = options.repo;
52131
52261
  const head2 = `${owner}:${options.branch}`;
52132
52262
  const response = await withGithubRetries(
@@ -52134,7 +52264,8 @@ async function adoptExistingPullRequest(baseUrl, fetchImpl, options) {
52134
52264
  async () => fetchImpl(`${baseUrl}/repos/${owner}/${name}/pulls?state=open&head=${encodeURIComponent(head2)}`, {
52135
52265
  method: "GET",
52136
52266
  headers: githubHeaders(options.credential)
52137
- })
52267
+ }),
52268
+ retryCounter
52138
52269
  );
52139
52270
  if (response.status !== 200) {
52140
52271
  return null;
@@ -52146,6 +52277,9 @@ async function adoptExistingPullRequest(baseUrl, fetchImpl, options) {
52146
52277
  return toForgePullRequest(list[0], true);
52147
52278
  }
52148
52279
  async function openDraftPullRequest(options) {
52280
+ return withLedgerStep(options.ledger, "pr_open", (retryCounter) => openDraftPullRequestInner(options, retryCounter));
52281
+ }
52282
+ async function openDraftPullRequestInner(options, retryCounter) {
52149
52283
  const baseUrl = (options.apiBaseUrl ?? GITHUB_API_BASE_URL).replace(/\/+$/, "");
52150
52284
  const fetchImpl = options.fetchImpl ?? fetch;
52151
52285
  const { owner, name } = options.repo;
@@ -52162,7 +52296,8 @@ async function openDraftPullRequest(options) {
52162
52296
  draft: true,
52163
52297
  maintainer_can_modify: true
52164
52298
  })
52165
- })
52299
+ }),
52300
+ retryCounter
52166
52301
  );
52167
52302
  if (created.status === 201) {
52168
52303
  return toForgePullRequest(await created.json(), false);
@@ -52170,7 +52305,7 @@ async function openDraftPullRequest(options) {
52170
52305
  if (created.status === 422) {
52171
52306
  const detail = await readErrorText(created);
52172
52307
  if (/already exists/i.test(detail)) {
52173
- const existing = await adoptExistingPullRequest(baseUrl, fetchImpl, options);
52308
+ const existing = await adoptExistingPullRequest(baseUrl, fetchImpl, options, retryCounter);
52174
52309
  if (existing) {
52175
52310
  return existing;
52176
52311
  }
@@ -52878,6 +53013,7 @@ async function post2(fetchImpl, appUrl2, pathName, body, token) {
52878
53013
  return { status: response.status, json: json2 };
52879
53014
  }
52880
53015
  async function claimAndExecute(fetchImpl, appUrl2, state, config2, io, runtime, options = {}) {
53016
+ const claimStartedAt = (/* @__PURE__ */ new Date()).toISOString();
52881
53017
  const claimed = await post2(
52882
53018
  fetchImpl,
52883
53019
  appUrl2,
@@ -52885,9 +53021,18 @@ async function claimAndExecute(fetchImpl, appUrl2, state, config2, io, runtime,
52885
53021
  options.interactiveOnly ? { interactive_only: true } : {},
52886
53022
  state.runner_secret
52887
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
+ });
52888
53033
  const turnExecution = claimed.json.turn_execution;
52889
53034
  if (claimed.status === 200 && turnExecution && typeof turnExecution === "object") {
52890
- await executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtime, {
53035
+ await executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtime, ledger, {
52891
53036
  kind: "turn_execution",
52892
53037
  idField: "turn_execution_id",
52893
53038
  item: turnExecution,
@@ -52901,7 +53046,7 @@ async function claimAndExecute(fetchImpl, appUrl2, state, config2, io, runtime,
52901
53046
  if (claimed.status !== 200 || !workOrder || typeof workOrder !== "object") {
52902
53047
  return;
52903
53048
  }
52904
- await executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtime, {
53049
+ await executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtime, ledger, {
52905
53050
  kind: "work_order",
52906
53051
  idField: "work_order_id",
52907
53052
  item: workOrder,
@@ -52910,7 +53055,7 @@ async function claimAndExecute(fetchImpl, appUrl2, state, config2, io, runtime,
52910
53055
  logRef: (id) => `runner-local:${id}`
52911
53056
  });
52912
53057
  }
52913
- async function executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtime, input) {
53058
+ async function executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtime, ledger, input) {
52914
53059
  const id = input.item.id;
52915
53060
  if (typeof id !== "string") {
52916
53061
  return;
@@ -52920,7 +53065,7 @@ async function executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtim
52920
53065
  throw new Error(`${claimedUnitLabel(input.kind)} start failed ${started.status}: ${redactForLog(JSON.stringify(started.json))}`);
52921
53066
  }
52922
53067
  const turnRuntime = runtimeForClaimedUnit(input.item, runtime, input.kind);
52923
- 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) : {
52924
53069
  ok: false,
52925
53070
  summary: `Runner runtime ${runtime} cannot execute requested ${String(input.item.brain ?? "unknown")} turn.`
52926
53071
  };
@@ -52946,7 +53091,14 @@ async function executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtim
52946
53091
  ...result.prUrl ? { pr_url: result.prUrl } : {},
52947
53092
  ...result.branch ? { branch: result.branch } : {},
52948
53093
  ...result.commitSha ? { commit_sha: result.commitSha } : {},
52949
- ...result.evidence && result.evidence.length > 0 ? { evidence: result.evidence } : {}
53094
+ ...result.evidence && result.evidence.length > 0 ? { evidence: result.evidence } : {},
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 } : {}
52950
53102
  }, state.runner_secret);
52951
53103
  if (reported.status !== 200) {
52952
53104
  throw new Error(`${claimedUnitLabel(input.kind)} result failed ${reported.status}: ${redactForLog(JSON.stringify(reported.json))}`);
@@ -53060,6 +53212,7 @@ async function prepareForgeTurn(ctx, workOrder, execution) {
53060
53212
  if (!workOrderId || !buildRequestId) {
53061
53213
  return { ok: false, summary: "forge turn missing work order id or build request id" };
53062
53214
  }
53215
+ const credentialMintStartedAt = (/* @__PURE__ */ new Date()).toISOString();
53063
53216
  const brokered = await post2(
53064
53217
  ctx.fetchImpl,
53065
53218
  ctx.appUrl,
@@ -53067,6 +53220,14 @@ async function prepareForgeTurn(ctx, workOrder, execution) {
53067
53220
  {},
53068
53221
  ctx.state.runner_secret
53069
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
+ });
53070
53231
  if (brokered.status !== 200) {
53071
53232
  return { ok: false, summary: `forge credential broker failed ${brokered.status}: ${redactForLog(JSON.stringify(brokered.json))}` };
53072
53233
  }
@@ -53094,7 +53255,8 @@ async function prepareForgeTurn(ctx, workOrder, execution) {
53094
53255
  profile: isBuild ? "build" : "review",
53095
53256
  ...isBuild ? {} : { headSha: `origin/${defaultBranch}` },
53096
53257
  repo: { owner, name, default_branch: defaultBranch },
53097
- credential
53258
+ credential,
53259
+ ledger: ctx.ledger
53098
53260
  });
53099
53261
  } catch (error51) {
53100
53262
  return { ok: false, summary: `forge workspace preparation failed: ${redactForLog(error51 instanceof Error ? error51.message : String(error51))}` };
@@ -53132,11 +53294,17 @@ async function prepareForgeTurn(ctx, workOrder, execution) {
53132
53294
  }
53133
53295
  };
53134
53296
  }
53135
- async function finalizeForgeBuildPush(prep, result) {
53297
+ var FORGE_MAX_CHANGED_PATHS = 2e3;
53298
+ var FORGE_MAX_CHANGED_PATH_LENGTH = 500;
53299
+ function forgeChangedPathsField(changedPaths) {
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);
53301
+ return withinCaps ? { changedPaths: [...changedPaths] } : {};
53302
+ }
53303
+ async function finalizeForgeBuildPush(ctx, prep, result) {
53136
53304
  const credential = { token: prep.credential.token, expiresAt: prep.credential.expiresAt };
53137
53305
  let pushed;
53138
53306
  try {
53139
- pushed = await pushChangeset(prep.workspace, credential);
53307
+ pushed = await pushChangeset(prep.workspace, credential, ctx.ledger);
53140
53308
  } catch (error51) {
53141
53309
  return {
53142
53310
  ...result,
@@ -53149,13 +53317,21 @@ async function finalizeForgeBuildPush(prep, result) {
53149
53317
  return { ...result, summary: `${result.summary}
53150
53318
  [forge] no changes to push`.slice(0, 12e3) };
53151
53319
  }
53320
+ let changedPaths = [];
53321
+ try {
53322
+ changedPaths = await computeChangedPaths(prep.workspace);
53323
+ } catch {
53324
+ changedPaths = [];
53325
+ }
53326
+ const changedPathsField = forgeChangedPathsField(changedPaths);
53152
53327
  try {
53153
53328
  const pr = await openDraftPullRequest({
53154
53329
  repo: prep.repo,
53155
53330
  branch: prep.workspace.branch,
53156
53331
  buildRequestId: prep.buildRequestId,
53157
53332
  commitSha: pushed.commitSha,
53158
- credential
53333
+ credential,
53334
+ ledger: ctx.ledger
53159
53335
  });
53160
53336
  const evidence = [
53161
53337
  { kind: "note", summary: `pushed ${pushed.commitSha} to ${prep.workspace.branch}`.slice(0, 500) },
@@ -53167,6 +53343,7 @@ async function finalizeForgeBuildPush(prep, result) {
53167
53343
  branch: prep.workspace.branch,
53168
53344
  commitSha: pushed.commitSha,
53169
53345
  evidence,
53346
+ ...changedPathsField,
53170
53347
  summary: `${result.summary}
53171
53348
  [forge] pushed ${pushed.commitSha} to ${prep.workspace.branch}; ${pr.adopted ? "adopted" : "opened"} draft PR ${pr.url}`.slice(0, 12e3)
53172
53349
  };
@@ -53178,6 +53355,7 @@ async function finalizeForgeBuildPush(prep, result) {
53178
53355
  // rather than advancing a headless push. Re-entry re-pushes (no-op) and re-opens (adopt).
53179
53356
  branch: prep.workspace.branch,
53180
53357
  commitSha: pushed.commitSha,
53358
+ ...changedPathsField,
53181
53359
  summary: `${result.summary}
53182
53360
  [forge] pushed ${pushed.commitSha} to ${prep.workspace.branch}; draft PR open failed: ${redactForLog(error51 instanceof Error ? error51.message : String(error51))}`.slice(0, 12e3)
53183
53361
  };
@@ -53259,6 +53437,7 @@ async function spawnRunnerTurn(ctx, workOrder, runtime, kind) {
53259
53437
  }
53260
53438
  const { command, args } = wrapCommandWithSandbox(sandbox, built.command, built.args, sandboxProfilePath);
53261
53439
  const expectSessionEnvelope = runtime === "claude" && kind === "turn_execution";
53440
+ const spawnStartedAt = (/* @__PURE__ */ new Date()).toISOString();
53262
53441
  let result = await runModelProcess({
53263
53442
  command,
53264
53443
  args,
@@ -53269,13 +53448,32 @@ async function spawnRunnerTurn(ctx, workOrder, runtime, kind) {
53269
53448
  expectSessionEnvelope,
53270
53449
  resumeSessionId
53271
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
+ }
53272
53470
  void rm4(configPath, { force: true });
53273
53471
  if (sandboxProfilePath) {
53274
53472
  void rm4(sandboxProfilePath, { force: true });
53275
53473
  }
53276
53474
  if (forgePrep) {
53277
53475
  if (plan.profile === "forge_build" && result.ok && forgePrep.credential.canPush) {
53278
- result = await finalizeForgeBuildPush(forgePrep, result);
53476
+ result = await finalizeForgeBuildPush(ctx, forgePrep, result);
53279
53477
  }
53280
53478
  if (plan.profile === "forge_review") {
53281
53479
  await removeWorkspace(forgePrep.workspace).catch((error51) => {
@@ -53286,6 +53484,9 @@ async function spawnRunnerTurn(ctx, workOrder, runtime, kind) {
53286
53484
  }
53287
53485
  return result;
53288
53486
  }
53487
+ function scrubTranscriptSessionIds(value) {
53488
+ return value.replace(/("?(?:session_id|sessionId)"?\s*[:=]\s*"?)[A-Za-z0-9._-]+("?)/gi, "$1[redacted]$2");
53489
+ }
53289
53490
  function runModelProcess(input) {
53290
53491
  return new Promise((resolve2) => {
53291
53492
  let settled = false;
@@ -53305,7 +53506,12 @@ function runModelProcess(input) {
53305
53506
  let err = "";
53306
53507
  const timer = setTimeout(() => {
53307
53508
  child.kill("SIGKILL");
53308
- 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
+ });
53309
53515
  }, input.timeoutMs);
53310
53516
  child.stdout.on("data", (data) => {
53311
53517
  out += data.toString("utf8");
@@ -53316,9 +53522,11 @@ function runModelProcess(input) {
53316
53522
  child.on("close", (code) => {
53317
53523
  clearTimeout(timer);
53318
53524
  const rawOutput = (out || err || "completed").trim();
53525
+ const redacted = redactForLog(rawOutput);
53319
53526
  finish({
53320
53527
  ok: code === 0,
53321
- summary: extractRunnerSummary(redactForLog(rawOutput)).slice(0, 12e3),
53528
+ summary: extractRunnerSummary(redacted).slice(0, 12e3),
53529
+ transcript: scrubTranscriptSessionIds(redacted).slice(0, MAX_TRANSCRIPT_CHARS),
53322
53530
  ...input.expectSessionEnvelope ? extractClaudeSessionId(rawOutput) : {}
53323
53531
  });
53324
53532
  });
@@ -53328,6 +53536,18 @@ function runModelProcess(input) {
53328
53536
  });
53329
53537
  });
53330
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
+ }
53331
53551
  function extractRunnerSummary(output) {
53332
53552
  const trimmed = output.trim();
53333
53553
  if (!trimmed.startsWith("{")) {