@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.
- package/dist/forge-workspace.d.ts +4 -1
- package/dist/forge-workspace.d.ts.map +1 -1
- package/dist/index.js +210 -36
- package/dist/index.js.map +4 -4
- 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,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":"
|
|
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,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
|
-
|
|
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(
|
|
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("{")) {
|