@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.
- package/dist/forge-workspace.d.ts +5 -1
- package/dist/forge-workspace.d.ts.map +1 -1
- package/dist/index.js +256 -36
- package/dist/index.js.map +4 -4
- package/dist/runner-serve.d.ts +5 -0
- package/dist/runner-serve.d.ts.map +1 -1
- package/dist/step-ledger.d.ts +21 -0
- package/dist/step-ledger.d.ts.map +1 -0
- package/package.json +1 -1
|
@@ -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":"
|
|
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
|
-
|
|
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
|
-
|
|
52023
|
-
if (await
|
|
52024
|
-
await
|
|
52025
|
-
|
|
52026
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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("{")) {
|