@neriros/ralphy 2.11.2 → 2.12.0
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/cli/index.js +343 -57
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -50837,7 +50837,7 @@ var require_axios = __commonJS((exports, module) => {
|
|
|
50837
50837
|
});
|
|
50838
50838
|
|
|
50839
50839
|
// apps/cli/src/index.ts
|
|
50840
|
-
import { resolve, join as
|
|
50840
|
+
import { resolve, join as join20, dirname as dirname5 } from "path";
|
|
50841
50841
|
import { exists as exists2, mkdir as mkdir4, rm } from "fs/promises";
|
|
50842
50842
|
|
|
50843
50843
|
// node_modules/.bun/ink@5.2.1+1f88f629f0141b18/node_modules/ink/build/render.js
|
|
@@ -56407,7 +56407,7 @@ function log(msg) {
|
|
|
56407
56407
|
// package.json
|
|
56408
56408
|
var package_default = {
|
|
56409
56409
|
name: "@neriros/ralphy",
|
|
56410
|
-
version: "2.
|
|
56410
|
+
version: "2.12.0",
|
|
56411
56411
|
description: "An iterative AI task execution framework. Orchestrates multi-phase autonomous work using Claude or Codex engines.",
|
|
56412
56412
|
keywords: [
|
|
56413
56413
|
"agent",
|
|
@@ -56879,7 +56879,7 @@ function createDefaultContext() {
|
|
|
56879
56879
|
|
|
56880
56880
|
// apps/cli/src/components/App.tsx
|
|
56881
56881
|
var import_react58 = __toESM(require_react(), 1);
|
|
56882
|
-
import { join as
|
|
56882
|
+
import { join as join19 } from "path";
|
|
56883
56883
|
|
|
56884
56884
|
// packages/core/src/state.ts
|
|
56885
56885
|
import { join as join2 } from "path";
|
|
@@ -70104,7 +70104,7 @@ function TaskLoop({ opts }) {
|
|
|
70104
70104
|
|
|
70105
70105
|
// apps/cli/src/components/AgentMode.tsx
|
|
70106
70106
|
var import_react57 = __toESM(require_react(), 1);
|
|
70107
|
-
import { join as
|
|
70107
|
+
import { join as join17 } from "path";
|
|
70108
70108
|
|
|
70109
70109
|
// apps/cli/src/agent/state.ts
|
|
70110
70110
|
import { join as join10 } from "path";
|
|
@@ -70249,6 +70249,9 @@ async function ensureRalphyConfig(projectRoot) {
|
|
|
70249
70249
|
return path;
|
|
70250
70250
|
}
|
|
70251
70251
|
|
|
70252
|
+
// apps/cli/src/agent/wire.ts
|
|
70253
|
+
import { join as join16 } from "path";
|
|
70254
|
+
|
|
70252
70255
|
// packages/core/src/layout.ts
|
|
70253
70256
|
import { join as join12 } from "path";
|
|
70254
70257
|
var STATE_FILE2 = ".ralph-state.json";
|
|
@@ -70864,8 +70867,32 @@ async function createPullRequest(input, runner) {
|
|
|
70864
70867
|
|
|
70865
70868
|
// apps/cli/src/agent/ci.ts
|
|
70866
70869
|
var PR_CHECKS_FIELDS = "name,bucket,link,workflow,event";
|
|
70867
|
-
|
|
70868
|
-
|
|
70870
|
+
var TRANSIENT_GH_RE = /HTTP 5\d\d|Gateway Timeout|Bad Gateway|Service Unavailable|connection reset|ECONNRESET|ETIMEDOUT|getaddrinfo|EAI_AGAIN|could not resolve host/i;
|
|
70871
|
+
var GH_RETRY_DELAYS = [5000, 15000, 45000];
|
|
70872
|
+
async function runGhWithRetry(cmd, runner, cwd2, onRetry, sleep2 = (ms) => new Promise((r) => setTimeout(r, ms))) {
|
|
70873
|
+
let lastErr;
|
|
70874
|
+
for (let i = 0;i <= GH_RETRY_DELAYS.length; i++) {
|
|
70875
|
+
try {
|
|
70876
|
+
return await runner.run(cmd, cwd2);
|
|
70877
|
+
} catch (err) {
|
|
70878
|
+
const e = err;
|
|
70879
|
+
const blob = `${e.message}
|
|
70880
|
+
${e.stderr ?? ""}
|
|
70881
|
+
${e.stdout ?? ""}`;
|
|
70882
|
+
if (!TRANSIENT_GH_RE.test(blob) || i === GH_RETRY_DELAYS.length)
|
|
70883
|
+
throw err;
|
|
70884
|
+
const delay2 = GH_RETRY_DELAYS[i];
|
|
70885
|
+
const firstLine = (e.stderr?.trim().split(`
|
|
70886
|
+
`)[0] ?? e.message).slice(0, 120);
|
|
70887
|
+
onRetry?.(i + 1, delay2, firstLine);
|
|
70888
|
+
await sleep2(delay2);
|
|
70889
|
+
lastErr = err;
|
|
70890
|
+
}
|
|
70891
|
+
}
|
|
70892
|
+
throw lastErr;
|
|
70893
|
+
}
|
|
70894
|
+
async function getPrChecksStatus(prRef, runner, cwd2, onTransientRetry) {
|
|
70895
|
+
const out = await runGhWithRetry(["gh", "pr", "checks", prRef, "--json", PR_CHECKS_FIELDS], runner, cwd2, onTransientRetry);
|
|
70869
70896
|
const checks = JSON.parse(out.stdout || "[]").filter((c) => c.bucket !== "skipping");
|
|
70870
70897
|
if (checks.some((c) => c.bucket === "pending")) {
|
|
70871
70898
|
return { bucket: "pending", failedRunIds: [] };
|
|
@@ -70902,17 +70929,28 @@ ${truncated}`);
|
|
|
70902
70929
|
}
|
|
70903
70930
|
async function fixCiUntilGreen(deps, opts) {
|
|
70904
70931
|
for (let attempt2 = 1;attempt2 <= opts.maxAttempts; attempt2++) {
|
|
70932
|
+
let pollN = 0;
|
|
70905
70933
|
while (true) {
|
|
70906
70934
|
if (deps.cancelled?.())
|
|
70907
70935
|
return { success: false, attempts: attempt2 - 1, reason: "cancelled" };
|
|
70908
|
-
|
|
70936
|
+
pollN += 1;
|
|
70937
|
+
deps.onPhase?.("ci-poll", `attempt ${attempt2}/${opts.maxAttempts} \xB7 poll ${pollN}`);
|
|
70938
|
+
let s;
|
|
70939
|
+
try {
|
|
70940
|
+
s = await deps.getStatus();
|
|
70941
|
+
} catch (err) {
|
|
70942
|
+
deps.log(`! gh pr checks failed permanently: ${err.message} \u2014 giving up CI watch`, "red");
|
|
70943
|
+
return { success: false, attempts: attempt2 - 1, reason: "gh-failed" };
|
|
70944
|
+
}
|
|
70909
70945
|
if (s.bucket === "pass") {
|
|
70910
70946
|
deps.log(`\u2713 CI green for PR (after ${attempt2 - 1} fix attempts)`, "green");
|
|
70911
70947
|
return { success: true, attempts: attempt2 - 1 };
|
|
70912
70948
|
}
|
|
70913
70949
|
if (s.bucket === "fail") {
|
|
70914
70950
|
deps.log(`\u2717 CI failing (attempt ${attempt2}/${opts.maxAttempts}) \u2014 fetching logs and re-running task`, "yellow");
|
|
70951
|
+
deps.onPhase?.("ci-fix", `attempt ${attempt2}/${opts.maxAttempts} \xB7 fetching logs`);
|
|
70915
70952
|
const logs = await deps.getFailedLogs(s.failedRunIds);
|
|
70953
|
+
deps.onPhase?.("ci-fix", `attempt ${attempt2}/${opts.maxAttempts} \xB7 re-running worker`);
|
|
70916
70954
|
const steering = `CI is failing on this PR. Investigate and fix:
|
|
70917
70955
|
|
|
70918
70956
|
\`\`\`
|
|
@@ -70923,6 +70961,7 @@ ${logs}
|
|
|
70923
70961
|
deps.log(`! task loop exited code ${code} during CI fix attempt ${attempt2}`, "red");
|
|
70924
70962
|
}
|
|
70925
70963
|
try {
|
|
70964
|
+
deps.onPhase?.("ci-fix", `attempt ${attempt2}/${opts.maxAttempts} \xB7 pushing fix`);
|
|
70926
70965
|
await deps.pushBranch();
|
|
70927
70966
|
} catch (err) {
|
|
70928
70967
|
deps.log(`! push failed during CI fix: ${err.message}`, "red");
|
|
@@ -70930,6 +70969,7 @@ ${logs}
|
|
|
70930
70969
|
}
|
|
70931
70970
|
break;
|
|
70932
70971
|
}
|
|
70972
|
+
deps.onPhase?.("ci-poll", `attempt ${attempt2}/${opts.maxAttempts} \xB7 pending, waiting`);
|
|
70933
70973
|
await deps.sleep(opts.pollIntervalSeconds * 1000);
|
|
70934
70974
|
}
|
|
70935
70975
|
}
|
|
@@ -70957,6 +70997,7 @@ async function reactivateState(stateFilePath, log2, changeName) {
|
|
|
70957
70997
|
}
|
|
70958
70998
|
async function runPostTask(input, deps) {
|
|
70959
70999
|
const { log: log2, cmd, git, runScript } = deps;
|
|
71000
|
+
const emit = (phase, detail) => deps.onPhase?.(phase, detail);
|
|
70960
71001
|
const {
|
|
70961
71002
|
changeName,
|
|
70962
71003
|
cwd: cwd2,
|
|
@@ -70973,6 +71014,7 @@ async function runPostTask(input, deps) {
|
|
|
70973
71014
|
respawnWorker
|
|
70974
71015
|
} = input;
|
|
70975
71016
|
if (cfg.teardownScript) {
|
|
71017
|
+
emit("teardown", cfg.teardownScript);
|
|
70976
71018
|
try {
|
|
70977
71019
|
await runScript("teardown", cfg.teardownScript, cwd2);
|
|
70978
71020
|
} catch {}
|
|
@@ -70998,6 +71040,7 @@ async function runPostTask(input, deps) {
|
|
|
70998
71040
|
let hookFixAttempt = 0;
|
|
70999
71041
|
let commitGaveUp = false;
|
|
71000
71042
|
while (true) {
|
|
71043
|
+
emit("committing", "git status");
|
|
71001
71044
|
let dirty = "";
|
|
71002
71045
|
try {
|
|
71003
71046
|
const status = await cmd.run(["git", "status", "--porcelain"], cwd2);
|
|
@@ -71009,7 +71052,9 @@ async function runPostTask(input, deps) {
|
|
|
71009
71052
|
if (!dirty)
|
|
71010
71053
|
break;
|
|
71011
71054
|
try {
|
|
71055
|
+
emit("committing", "git add -A");
|
|
71012
71056
|
await cmd.run(["git", "add", "-A"], cwd2);
|
|
71057
|
+
emit("committing", "git commit");
|
|
71013
71058
|
await cmd.run(["git", "commit", "-m", `chore(ralph): residual changes for ${changeName}`], cwd2);
|
|
71014
71059
|
log2(` committed residual changes for ${changeName}`, "gray");
|
|
71015
71060
|
break;
|
|
@@ -71028,6 +71073,7 @@ ${e.stderr ?? ""}`;
|
|
|
71028
71073
|
break;
|
|
71029
71074
|
}
|
|
71030
71075
|
hookFixAttempt += 1;
|
|
71076
|
+
emit("commit-retry", `${hookFixAttempt}/${maxHookFixAttempts}`);
|
|
71031
71077
|
log2(`! commit rejected for ${changeName} \u2014 prepending fix task and re-running loop (attempt ${hookFixAttempt}/${maxHookFixAttempts})`, "yellow");
|
|
71032
71078
|
log2(` detail: ${detail}`, "yellow");
|
|
71033
71079
|
const retryCode = await runWorkerWithFixTask("Fix host pre-commit hook rejection", `Committing residual changes was rejected by the host repo's pre-commit hook. ` + `Fix the underlying problem, then the commit will be retried.
|
|
@@ -71043,8 +71089,10 @@ ${e.stderr ?? ""}`;
|
|
|
71043
71089
|
}
|
|
71044
71090
|
let pr = null;
|
|
71045
71091
|
let prGaveUp = commitGaveUp;
|
|
71092
|
+
let nonFfRebaseAttempted = false;
|
|
71046
71093
|
while (!prGaveUp) {
|
|
71047
71094
|
try {
|
|
71095
|
+
emit("pr-create", "git push + gh pr create");
|
|
71048
71096
|
pr = await createPullRequest({ cwd: cwd2, branch, issue, base: cfg.prBaseBranch }, cmd);
|
|
71049
71097
|
break;
|
|
71050
71098
|
} catch (err) {
|
|
@@ -71052,8 +71100,69 @@ ${e.stderr ?? ""}`;
|
|
|
71052
71100
|
const detail = e.stderr?.trim() || e.message;
|
|
71053
71101
|
const combined = `${e.stdout ?? ""}
|
|
71054
71102
|
${e.stderr ?? ""}`;
|
|
71055
|
-
const
|
|
71056
|
-
|
|
71103
|
+
const isNonFastForward = /non-fast-forward|Updates were rejected because the (tip of your current branch is behind|remote contains work)/i.test(combined) && !/pre-push hook|hook declined/i.test(combined);
|
|
71104
|
+
const isHookReject = /pre-push hook|hook declined/i.test(combined);
|
|
71105
|
+
const pushRejected = isHookReject || /failed to push some refs/i.test(combined);
|
|
71106
|
+
if (isNonFastForward && !nonFfRebaseAttempted) {
|
|
71107
|
+
nonFfRebaseAttempted = true;
|
|
71108
|
+
emit("rebasing", `git pull --rebase origin ${branch}`);
|
|
71109
|
+
log2(` non-fast-forward push for ${changeName} \u2014 rebasing onto origin/${branch}`, "yellow");
|
|
71110
|
+
try {
|
|
71111
|
+
await cmd.run(["git", "fetch", "origin", branch], cwd2);
|
|
71112
|
+
await cmd.run(["git", "pull", "--rebase", "origin", branch], cwd2);
|
|
71113
|
+
continue;
|
|
71114
|
+
} catch (rebaseErr) {
|
|
71115
|
+
const re = rebaseErr;
|
|
71116
|
+
const reBlob = `${re.stdout ?? ""}
|
|
71117
|
+
${re.stderr ?? ""}`;
|
|
71118
|
+
const isConflict = /CONFLICT|Merge conflict|could not apply|both modified/i.test(reBlob);
|
|
71119
|
+
if (!isConflict) {
|
|
71120
|
+
log2(`! rebase failed for ${changeName}: ${rebaseErr.message} \u2014 giving up`, "red");
|
|
71121
|
+
effectiveCode = PR_FAILED_EXIT;
|
|
71122
|
+
prGaveUp = true;
|
|
71123
|
+
break;
|
|
71124
|
+
}
|
|
71125
|
+
emit("rebasing", "conflicts detected \u2014 aborting + queueing fix task");
|
|
71126
|
+
try {
|
|
71127
|
+
await cmd.run(["git", "rebase", "--abort"], cwd2);
|
|
71128
|
+
} catch {}
|
|
71129
|
+
let conflictedFiles = "";
|
|
71130
|
+
try {
|
|
71131
|
+
const r = await cmd.run(["git", "diff", "--name-only", `HEAD..origin/${branch}`], cwd2);
|
|
71132
|
+
conflictedFiles = r.stdout.trim();
|
|
71133
|
+
} catch {}
|
|
71134
|
+
if (hookFixAttempt >= maxHookFixAttempts) {
|
|
71135
|
+
log2(`! merge conflict on rebase of ${branch} after ${hookFixAttempt} attempts \u2014 worktree preserved at ${cwd2}`, "red");
|
|
71136
|
+
log2(` detail: ${reBlob.trim().split(`
|
|
71137
|
+
`).slice(0, 8).join(`
|
|
71138
|
+
`)}`, "red");
|
|
71139
|
+
effectiveCode = PR_FAILED_EXIT;
|
|
71140
|
+
prGaveUp = true;
|
|
71141
|
+
break;
|
|
71142
|
+
}
|
|
71143
|
+
hookFixAttempt += 1;
|
|
71144
|
+
emit("rebasing", `conflict-fix ${hookFixAttempt}/${maxHookFixAttempts}`);
|
|
71145
|
+
log2(`! merge conflict rebasing ${branch} \u2014 prepending fix task and re-running loop (attempt ${hookFixAttempt}/${maxHookFixAttempts})`, "yellow");
|
|
71146
|
+
const retryCode2 = await runWorkerWithFixTask("Resolve merge conflict with origin/" + branch, `Push to origin/${branch} was rejected as non-fast-forward, and rebasing ` + `onto origin/${branch} produced merge conflicts.
|
|
71147
|
+
|
|
71148
|
+
` + `Run \`git fetch origin ${branch}\` and \`git rebase origin/${branch}\`, ` + `resolve every conflict, \`git add\` the resolved files, and finish with ` + `\`git rebase --continue\`. The push will be retried after this loop ` + `iteration finishes.
|
|
71149
|
+
|
|
71150
|
+
` + (conflictedFiles ? `Files that differ between your branch and origin/${branch}:
|
|
71151
|
+
${conflictedFiles}
|
|
71152
|
+
|
|
71153
|
+
` : "") + `Rebase output:
|
|
71154
|
+
${reBlob.trim()}`);
|
|
71155
|
+
if (retryCode2 !== 0) {
|
|
71156
|
+
log2(`! worker re-run after merge conflict exited code ${retryCode2} \u2014 giving up`, "red");
|
|
71157
|
+
effectiveCode = PR_FAILED_EXIT;
|
|
71158
|
+
prGaveUp = true;
|
|
71159
|
+
break;
|
|
71160
|
+
}
|
|
71161
|
+
nonFfRebaseAttempted = false;
|
|
71162
|
+
continue;
|
|
71163
|
+
}
|
|
71164
|
+
}
|
|
71165
|
+
if (!isHookReject || hookFixAttempt >= maxHookFixAttempts) {
|
|
71057
71166
|
if (pushRejected) {
|
|
71058
71167
|
log2(`! push rejected for ${changeName} after ${hookFixAttempt} hook-fix attempts (host pre-push hook still failing) \u2014 worktree preserved at ${cwd2}`, "red");
|
|
71059
71168
|
log2(` detail: ${detail}`, "red");
|
|
@@ -71065,6 +71174,7 @@ ${e.stderr ?? ""}`;
|
|
|
71065
71174
|
break;
|
|
71066
71175
|
}
|
|
71067
71176
|
hookFixAttempt += 1;
|
|
71177
|
+
emit("push-retry", `${hookFixAttempt}/${maxHookFixAttempts}`);
|
|
71068
71178
|
log2(`! push rejected for ${changeName} \u2014 prepending fix task and re-running loop (attempt ${hookFixAttempt}/${maxHookFixAttempts})`, "yellow");
|
|
71069
71179
|
log2(` detail: ${detail}`, "yellow");
|
|
71070
71180
|
const retryCode = await runWorkerWithFixTask("Fix host pre-push hook rejection", `Push to origin/${branch} was rejected by the host repo's pre-push hook. ` + `Fix the underlying problem, then the push will be retried.
|
|
@@ -71084,8 +71194,10 @@ ${e.stderr ?? ""}`;
|
|
|
71084
71194
|
log2(` ${pr.created ? "opened" : "found existing"} PR: ${pr.url}`, "green");
|
|
71085
71195
|
if (wantFixCi) {
|
|
71086
71196
|
log2(` watching CI for ${pr.url} (max ${cfg.maxCiFixAttempts} fix attempts)`, "gray");
|
|
71197
|
+
emit("ci-poll", "starting");
|
|
71087
71198
|
const result2 = await fixCiUntilGreen({
|
|
71088
|
-
|
|
71199
|
+
onPhase: (p, d) => emit(p, d),
|
|
71200
|
+
getStatus: () => getPrChecksStatus(pr.url, cmd, cwd2, (n, ms, why) => log2(` gh transient (try ${n}) \u2014 retry in ${Math.round(ms / 1000)}s \xB7 ${why}`, "yellow")),
|
|
71089
71201
|
getFailedLogs: (ids) => fetchFailedRunLogs(ids, cmd, cwd2),
|
|
71090
71202
|
runTaskWithSteering: async (steering) => {
|
|
71091
71203
|
try {
|
|
@@ -71112,7 +71224,12 @@ ${e.stderr ?? ""}`;
|
|
|
71112
71224
|
}
|
|
71113
71225
|
}
|
|
71114
71226
|
}
|
|
71227
|
+
if (effectiveCode === 0)
|
|
71228
|
+
emit("done");
|
|
71229
|
+
else
|
|
71230
|
+
emit("gave-up", `exit ${effectiveCode}`);
|
|
71115
71231
|
if (useWorktree && cwd2 !== projectRoot) {
|
|
71232
|
+
emit("cleanup", "checking worktree safety");
|
|
71116
71233
|
if (effectiveCode === 0 && cfg.cleanupWorktreeOnSuccess) {
|
|
71117
71234
|
const check = await isWorktreeSafeToRemove(cwd2, cfg.prBaseBranch, git).catch((err) => ({
|
|
71118
71235
|
safe: false,
|
|
@@ -71176,6 +71293,22 @@ var bunCmdRunner = {
|
|
|
71176
71293
|
return { stdout, stderr };
|
|
71177
71294
|
}
|
|
71178
71295
|
};
|
|
71296
|
+
function traceCmdRunner(base2, onStart, onEnd) {
|
|
71297
|
+
return {
|
|
71298
|
+
run: async (cmd, cwd2) => {
|
|
71299
|
+
const t0 = Date.now();
|
|
71300
|
+
onStart(cmd);
|
|
71301
|
+
try {
|
|
71302
|
+
const r = await base2.run(cmd, cwd2);
|
|
71303
|
+
onEnd(cmd, Date.now() - t0, true);
|
|
71304
|
+
return r;
|
|
71305
|
+
} catch (err) {
|
|
71306
|
+
onEnd(cmd, Date.now() - t0, false);
|
|
71307
|
+
throw err;
|
|
71308
|
+
}
|
|
71309
|
+
}
|
|
71310
|
+
};
|
|
71311
|
+
}
|
|
71179
71312
|
function buildAgentCoordinator(input) {
|
|
71180
71313
|
const {
|
|
71181
71314
|
args,
|
|
@@ -71188,8 +71321,12 @@ function buildAgentCoordinator(input) {
|
|
|
71188
71321
|
onLog,
|
|
71189
71322
|
onWorkersChanged,
|
|
71190
71323
|
onWorkerStarted,
|
|
71191
|
-
onWorkerExited
|
|
71324
|
+
onWorkerExited,
|
|
71325
|
+
onWorkerPhase,
|
|
71326
|
+
onWorkerOutput,
|
|
71327
|
+
onWorkerCmd
|
|
71192
71328
|
} = input;
|
|
71329
|
+
const logsDir = join16(projectRoot, ".ralph", "logs");
|
|
71193
71330
|
const concurrency = args.concurrency || cfg.concurrency;
|
|
71194
71331
|
const pollInterval = args.pollInterval || cfg.pollIntervalSeconds;
|
|
71195
71332
|
const inProgressName = args.inProgressStatus || cfg.linear.inProgressStatus;
|
|
@@ -71300,24 +71437,83 @@ function buildAgentCoordinator(input) {
|
|
|
71300
71437
|
}
|
|
71301
71438
|
function spawnWorker(changeName) {
|
|
71302
71439
|
const cwd2 = cwdByChange.get(changeName) ?? projectRoot;
|
|
71303
|
-
const
|
|
71304
|
-
|
|
71440
|
+
const logFilePath = join16(logsDir, `${changeName}.log`);
|
|
71441
|
+
let logWriter = null;
|
|
71442
|
+
const ensureLogWriter = async () => {
|
|
71443
|
+
if (logWriter)
|
|
71444
|
+
return logWriter;
|
|
71445
|
+
try {
|
|
71446
|
+
await Bun.write(logFilePath, "");
|
|
71447
|
+
logWriter = Bun.file(logFilePath).writer();
|
|
71448
|
+
return logWriter;
|
|
71449
|
+
} catch (err) {
|
|
71450
|
+
onLog(`! could not open worker log ${logFilePath}: ${err.message}`, "yellow");
|
|
71451
|
+
return null;
|
|
71452
|
+
}
|
|
71453
|
+
};
|
|
71454
|
+
async function pump(stream, label) {
|
|
71455
|
+
if (!stream)
|
|
71456
|
+
return;
|
|
71457
|
+
const reader = stream.getReader();
|
|
71458
|
+
const decoder = new TextDecoder;
|
|
71459
|
+
let buf = "";
|
|
71460
|
+
const writer = await ensureLogWriter();
|
|
71461
|
+
try {
|
|
71462
|
+
while (true) {
|
|
71463
|
+
const { value, done } = await reader.read();
|
|
71464
|
+
if (done)
|
|
71465
|
+
break;
|
|
71466
|
+
const chunk2 = decoder.decode(value, { stream: true });
|
|
71467
|
+
buf += chunk2;
|
|
71468
|
+
let nl;
|
|
71469
|
+
while ((nl = buf.indexOf(`
|
|
71470
|
+
`)) >= 0) {
|
|
71471
|
+
const line = buf.slice(0, nl);
|
|
71472
|
+
buf = buf.slice(nl + 1);
|
|
71473
|
+
if (writer)
|
|
71474
|
+
writer.write(line + `
|
|
71475
|
+
`);
|
|
71476
|
+
if (line)
|
|
71477
|
+
onWorkerOutput?.(changeName, label === "err" ? `! ${line}` : line);
|
|
71478
|
+
}
|
|
71479
|
+
}
|
|
71480
|
+
if (buf) {
|
|
71481
|
+
if (writer)
|
|
71482
|
+
writer.write(buf + `
|
|
71483
|
+
`);
|
|
71484
|
+
onWorkerOutput?.(changeName, label === "err" ? `! ${buf}` : buf);
|
|
71485
|
+
}
|
|
71486
|
+
} catch {} finally {
|
|
71487
|
+
try {
|
|
71488
|
+
writer?.flush();
|
|
71489
|
+
} catch {}
|
|
71490
|
+
}
|
|
71491
|
+
}
|
|
71492
|
+
const launch = (note) => {
|
|
71493
|
+
const p = Bun.spawn({
|
|
71305
71494
|
cmd: buildTaskCmdFor(changeName),
|
|
71306
71495
|
cwd: cwd2,
|
|
71307
|
-
stdout: "
|
|
71308
|
-
stderr: "
|
|
71496
|
+
stdout: "pipe",
|
|
71497
|
+
stderr: "pipe",
|
|
71309
71498
|
stdin: "ignore"
|
|
71310
71499
|
});
|
|
71500
|
+
if (note && logWriter)
|
|
71501
|
+
logWriter.write(`
|
|
71502
|
+
--- ${note} ---
|
|
71503
|
+
`);
|
|
71504
|
+
pump(p.stdout, "out");
|
|
71505
|
+
pump(p.stderr, "err");
|
|
71506
|
+
return p;
|
|
71507
|
+
};
|
|
71508
|
+
const respawn = () => {
|
|
71509
|
+
onWorkerPhase?.(changeName, "working", "respawn");
|
|
71510
|
+
const rp = launch(`respawn at ${new Date().toISOString()}`);
|
|
71311
71511
|
return rp.exited;
|
|
71312
71512
|
};
|
|
71313
|
-
const proc =
|
|
71314
|
-
|
|
71315
|
-
|
|
71316
|
-
|
|
71317
|
-
stderr: "ignore",
|
|
71318
|
-
stdin: "ignore"
|
|
71319
|
-
});
|
|
71320
|
-
onWorkerStarted(changeName, statesDirByChange.get(changeName) ?? statesDir);
|
|
71513
|
+
const proc = launch(`spawn at ${new Date().toISOString()}`);
|
|
71514
|
+
onWorkerStarted(changeName, statesDirByChange.get(changeName) ?? statesDir, logFilePath);
|
|
71515
|
+
onWorkerPhase?.(changeName, "working");
|
|
71516
|
+
const tracedCmd = onWorkerCmd ? traceCmdRunner(bunCmdRunner, (cmd) => onWorkerCmd(changeName, cmd, "start"), (cmd, ms, ok) => onWorkerCmd(changeName, cmd, "end", ms, ok)) : bunCmdRunner;
|
|
71321
71517
|
const wantPr = args.createPr || cfg.createPrOnSuccess;
|
|
71322
71518
|
const wantFixCi = args.fixCi || cfg.fixCiOnFailure;
|
|
71323
71519
|
const wrapped = proc.exited.then(async (code) => {
|
|
@@ -71342,7 +71538,19 @@ function buildAgentCoordinator(input) {
|
|
|
71342
71538
|
cleanupWorktreeOnSuccess: cfg.cleanupWorktreeOnSuccess
|
|
71343
71539
|
},
|
|
71344
71540
|
respawnWorker: respawn
|
|
71345
|
-
}, {
|
|
71541
|
+
}, {
|
|
71542
|
+
cmd: tracedCmd,
|
|
71543
|
+
git: bunGitRunner,
|
|
71544
|
+
log: onLog,
|
|
71545
|
+
runScript,
|
|
71546
|
+
...onWorkerPhase && {
|
|
71547
|
+
onPhase: (phase, detail) => onWorkerPhase(changeName, phase, detail)
|
|
71548
|
+
}
|
|
71549
|
+
});
|
|
71550
|
+
try {
|
|
71551
|
+
logWriter?.flush();
|
|
71552
|
+
await logWriter?.end();
|
|
71553
|
+
} catch {}
|
|
71346
71554
|
cwdByChange.delete(changeName);
|
|
71347
71555
|
statesDirByChange.delete(changeName);
|
|
71348
71556
|
branchByChange.delete(changeName);
|
|
@@ -71418,6 +71626,12 @@ function nextId() {
|
|
|
71418
71626
|
lineCounter += 1;
|
|
71419
71627
|
return `${Date.now()}-${lineCounter}`;
|
|
71420
71628
|
}
|
|
71629
|
+
var TAIL_MAX_LINES = 5;
|
|
71630
|
+
var CMD_DISPLAY_MAX = 80;
|
|
71631
|
+
function fmtCmd(argv) {
|
|
71632
|
+
const joined = argv.join(" ");
|
|
71633
|
+
return joined.length > CMD_DISPLAY_MAX ? joined.slice(0, CMD_DISPLAY_MAX - 1) + "\u2026" : joined;
|
|
71634
|
+
}
|
|
71421
71635
|
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
71422
71636
|
function fmtElapsed(ms) {
|
|
71423
71637
|
const s = Math.floor(ms / 1000);
|
|
@@ -71467,15 +71681,50 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
71467
71681
|
store,
|
|
71468
71682
|
onLog: appendLog,
|
|
71469
71683
|
onWorkersChanged: () => setTick((t) => t + 1),
|
|
71470
|
-
onWorkerStarted: (changeName, dir) => {
|
|
71684
|
+
onWorkerStarted: (changeName, dir, logFile) => {
|
|
71471
71685
|
workerMetaRef.current.set(changeName, {
|
|
71472
71686
|
startedAt: Date.now(),
|
|
71473
71687
|
statesDir: dir,
|
|
71474
|
-
|
|
71688
|
+
logFile,
|
|
71689
|
+
iter: 0,
|
|
71690
|
+
phase: "working",
|
|
71691
|
+
phaseDetail: "",
|
|
71692
|
+
phaseStartedAt: Date.now(),
|
|
71693
|
+
currentCmd: null,
|
|
71694
|
+
lastCmd: null,
|
|
71695
|
+
tail: []
|
|
71475
71696
|
});
|
|
71476
71697
|
},
|
|
71477
71698
|
onWorkerExited: (changeName) => {
|
|
71478
71699
|
workerMetaRef.current.delete(changeName);
|
|
71700
|
+
},
|
|
71701
|
+
onWorkerPhase: (changeName, phase, detail) => {
|
|
71702
|
+
const m = workerMetaRef.current.get(changeName);
|
|
71703
|
+
if (!m)
|
|
71704
|
+
return;
|
|
71705
|
+
if (m.phase !== phase)
|
|
71706
|
+
m.phaseStartedAt = Date.now();
|
|
71707
|
+
m.phase = phase;
|
|
71708
|
+
m.phaseDetail = detail ?? "";
|
|
71709
|
+
},
|
|
71710
|
+
onWorkerOutput: (changeName, line) => {
|
|
71711
|
+
const m = workerMetaRef.current.get(changeName);
|
|
71712
|
+
if (!m)
|
|
71713
|
+
return;
|
|
71714
|
+
m.tail.push(line);
|
|
71715
|
+
if (m.tail.length > TAIL_MAX_LINES)
|
|
71716
|
+
m.tail.splice(0, m.tail.length - TAIL_MAX_LINES);
|
|
71717
|
+
},
|
|
71718
|
+
onWorkerCmd: (changeName, cmd, state, durationMs, ok) => {
|
|
71719
|
+
const m = workerMetaRef.current.get(changeName);
|
|
71720
|
+
if (!m)
|
|
71721
|
+
return;
|
|
71722
|
+
if (state === "start") {
|
|
71723
|
+
m.currentCmd = { argv: cmd, startedAt: Date.now() };
|
|
71724
|
+
} else {
|
|
71725
|
+
m.currentCmd = null;
|
|
71726
|
+
m.lastCmd = { argv: cmd, durationMs: durationMs ?? 0, ok: ok ?? true };
|
|
71727
|
+
}
|
|
71479
71728
|
}
|
|
71480
71729
|
});
|
|
71481
71730
|
appendLog(`concurrency=${concurrency} pollInterval=${pollInterval}s`, "gray");
|
|
@@ -71531,7 +71780,7 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
71531
71780
|
(async () => {
|
|
71532
71781
|
for (const [changeName, meta] of workerMetaRef.current) {
|
|
71533
71782
|
try {
|
|
71534
|
-
const file = Bun.file(
|
|
71783
|
+
const file = Bun.file(join17(meta.statesDir, changeName, ".ralph-state.json"));
|
|
71535
71784
|
if (await file.exists()) {
|
|
71536
71785
|
const json = await file.json();
|
|
71537
71786
|
meta.iter = json.iteration ?? meta.iter;
|
|
@@ -71589,19 +71838,56 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
71589
71838
|
const meta = workerMetaRef.current.get(w.changeName);
|
|
71590
71839
|
const elapsed = meta ? fmtElapsed(now2 - meta.startedAt) : "\u2013";
|
|
71591
71840
|
const iter = meta?.iter ?? 0;
|
|
71592
|
-
|
|
71593
|
-
|
|
71841
|
+
const phase = meta?.phase ?? "working";
|
|
71842
|
+
const phaseElapsed = meta ? fmtElapsed(now2 - meta.phaseStartedAt) : "\u2013";
|
|
71843
|
+
const phaseDetail = meta?.phaseDetail ? ` (${meta.phaseDetail})` : "";
|
|
71844
|
+
const cmd = meta?.currentCmd;
|
|
71845
|
+
const cmdElapsed = cmd ? fmtElapsed(now2 - cmd.startedAt) : null;
|
|
71846
|
+
const tail2 = meta?.tail ?? [];
|
|
71847
|
+
return /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Box_default, {
|
|
71848
|
+
flexDirection: "column",
|
|
71594
71849
|
children: [
|
|
71595
|
-
|
|
71596
|
-
|
|
71597
|
-
|
|
71598
|
-
|
|
71599
|
-
|
|
71600
|
-
|
|
71601
|
-
|
|
71602
|
-
|
|
71603
|
-
|
|
71604
|
-
|
|
71850
|
+
/* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
|
|
71851
|
+
color: "cyan",
|
|
71852
|
+
children: [
|
|
71853
|
+
" ",
|
|
71854
|
+
spinnerFrame,
|
|
71855
|
+
" ",
|
|
71856
|
+
w.issueIdentifier,
|
|
71857
|
+
" (",
|
|
71858
|
+
w.changeName,
|
|
71859
|
+
") \xB7 iter ",
|
|
71860
|
+
iter,
|
|
71861
|
+
" \xB7 ",
|
|
71862
|
+
elapsed
|
|
71863
|
+
]
|
|
71864
|
+
}, undefined, true, undefined, this),
|
|
71865
|
+
/* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
|
|
71866
|
+
dimColor: true,
|
|
71867
|
+
children: [
|
|
71868
|
+
" phase: ",
|
|
71869
|
+
phase,
|
|
71870
|
+
phaseDetail,
|
|
71871
|
+
" \xB7 ",
|
|
71872
|
+
phaseElapsed
|
|
71873
|
+
]
|
|
71874
|
+
}, undefined, true, undefined, this),
|
|
71875
|
+
cmd && /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
|
|
71876
|
+
color: "yellow",
|
|
71877
|
+
children: [
|
|
71878
|
+
" \u23F5 ",
|
|
71879
|
+
fmtCmd(cmd.argv),
|
|
71880
|
+
" \xB7 ",
|
|
71881
|
+
cmdElapsed
|
|
71882
|
+
]
|
|
71883
|
+
}, undefined, true, undefined, this),
|
|
71884
|
+
tail2.map((line, i) => /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
|
|
71885
|
+
dimColor: true,
|
|
71886
|
+
children: [
|
|
71887
|
+
" \u2502 ",
|
|
71888
|
+
line.length > 110 ? line.slice(0, 109) + "\u2026" : line
|
|
71889
|
+
]
|
|
71890
|
+
}, `${w.changeName}-tail-${i}`, true, undefined, this))
|
|
71605
71891
|
]
|
|
71606
71892
|
}, w.changeName, true, undefined, this);
|
|
71607
71893
|
})
|
|
@@ -71612,11 +71898,11 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
71612
71898
|
}
|
|
71613
71899
|
|
|
71614
71900
|
// packages/openspec/src/openspec-change-store.ts
|
|
71615
|
-
import { join as
|
|
71901
|
+
import { join as join18, dirname as dirname4 } from "path";
|
|
71616
71902
|
import { readdir, mkdir as mkdir3 } from "fs/promises";
|
|
71617
71903
|
function resolveOpenspecBin() {
|
|
71618
71904
|
const pkgJsonPath = Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir);
|
|
71619
|
-
return
|
|
71905
|
+
return join18(dirname4(pkgJsonPath), "bin", "openspec.js");
|
|
71620
71906
|
}
|
|
71621
71907
|
function runOpenspec(args, options = {}) {
|
|
71622
71908
|
const stdio = options.inherit ? ["inherit", "inherit", "inherit"] : ["ignore", "pipe", "pipe"];
|
|
@@ -71642,7 +71928,7 @@ class OpenSpecChangeStore {
|
|
|
71642
71928
|
}
|
|
71643
71929
|
}
|
|
71644
71930
|
getChangeDirectory(name) {
|
|
71645
|
-
return
|
|
71931
|
+
return join18("openspec", "changes", name);
|
|
71646
71932
|
}
|
|
71647
71933
|
async listChanges() {
|
|
71648
71934
|
const result2 = runOpenspec(["list", "--json"]);
|
|
@@ -71656,7 +71942,7 @@ class OpenSpecChangeStore {
|
|
|
71656
71942
|
}
|
|
71657
71943
|
} catch {}
|
|
71658
71944
|
}
|
|
71659
|
-
const changesDir =
|
|
71945
|
+
const changesDir = join18("openspec", "changes");
|
|
71660
71946
|
if (!await Bun.file(changesDir).exists())
|
|
71661
71947
|
return [];
|
|
71662
71948
|
try {
|
|
@@ -71667,18 +71953,18 @@ class OpenSpecChangeStore {
|
|
|
71667
71953
|
}
|
|
71668
71954
|
}
|
|
71669
71955
|
async readTaskList(name) {
|
|
71670
|
-
const file = Bun.file(
|
|
71956
|
+
const file = Bun.file(join18("openspec", "changes", name, "tasks.md"));
|
|
71671
71957
|
if (!await file.exists())
|
|
71672
71958
|
return "";
|
|
71673
71959
|
return await file.text();
|
|
71674
71960
|
}
|
|
71675
71961
|
async writeTaskList(name, content) {
|
|
71676
|
-
const path =
|
|
71962
|
+
const path = join18("openspec", "changes", name, "tasks.md");
|
|
71677
71963
|
await mkdir3(dirname4(path), { recursive: true });
|
|
71678
71964
|
await Bun.write(path, content);
|
|
71679
71965
|
}
|
|
71680
71966
|
async appendSteering(name, message) {
|
|
71681
|
-
const path =
|
|
71967
|
+
const path = join18("openspec", "changes", name, "steering.md");
|
|
71682
71968
|
const file = Bun.file(path);
|
|
71683
71969
|
const existing = await file.exists() ? await file.text() : null;
|
|
71684
71970
|
const updated = existing ? `${message}
|
|
@@ -71689,7 +71975,7 @@ ${existing.trimStart()}` : `${message}
|
|
|
71689
71975
|
await Bun.write(path, updated);
|
|
71690
71976
|
}
|
|
71691
71977
|
async readSection(name, artifact, heading) {
|
|
71692
|
-
const file = Bun.file(
|
|
71978
|
+
const file = Bun.file(join18("openspec", "changes", name, artifact));
|
|
71693
71979
|
if (!await file.exists())
|
|
71694
71980
|
return "";
|
|
71695
71981
|
const content = await file.text();
|
|
@@ -71771,8 +72057,8 @@ function App2({ args, statesDir, tasksDir, projectRoot }) {
|
|
|
71771
72057
|
message: "Error: --name is required for status mode"
|
|
71772
72058
|
}, undefined, false, undefined, this);
|
|
71773
72059
|
}
|
|
71774
|
-
const stateDir =
|
|
71775
|
-
if (getStorage().read(
|
|
72060
|
+
const stateDir = join19(statesDir, args.name);
|
|
72061
|
+
if (getStorage().read(join19(stateDir, ".ralph-state.json")) === null) {
|
|
71776
72062
|
return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(ErrorMessage, {
|
|
71777
72063
|
message: `Error: change '${args.name}' not found`
|
|
71778
72064
|
}, undefined, false, undefined, this);
|
|
@@ -71833,7 +72119,7 @@ if (typeof globalThis.Bun === "undefined") {
|
|
|
71833
72119
|
async function findProjectRoot() {
|
|
71834
72120
|
let dir = process.cwd();
|
|
71835
72121
|
while (dir !== "/") {
|
|
71836
|
-
if (await exists2(
|
|
72122
|
+
if (await exists2(join20(dir, "openspec")))
|
|
71837
72123
|
return dir;
|
|
71838
72124
|
dir = resolve(dir, "..");
|
|
71839
72125
|
}
|
|
@@ -71873,7 +72159,7 @@ try {
|
|
|
71873
72159
|
const tasksDir = layout.tasksDir;
|
|
71874
72160
|
if (args.mode === "init") {
|
|
71875
72161
|
await mkdir4(statesDir, { recursive: true });
|
|
71876
|
-
const openspecBin =
|
|
72162
|
+
const openspecBin = join20(dirname5(Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir)), "bin", "openspec.js");
|
|
71877
72163
|
Bun.spawnSync({
|
|
71878
72164
|
cmd: [process.execPath, openspecBin, "init", "--tools", "none", "--force"],
|
|
71879
72165
|
stdio: ["inherit", "inherit", "inherit"],
|
|
@@ -71886,9 +72172,9 @@ try {
|
|
|
71886
72172
|
`);
|
|
71887
72173
|
process.exit(1);
|
|
71888
72174
|
}
|
|
71889
|
-
const worktreeDir =
|
|
71890
|
-
const changeDir =
|
|
71891
|
-
const stateDir =
|
|
72175
|
+
const worktreeDir = join20(worktreesDir(projectRoot), args.name);
|
|
72176
|
+
const changeDir = join20(tasksDir, args.name);
|
|
72177
|
+
const stateDir = join20(statesDir, args.name);
|
|
71892
72178
|
const branch = `ralph/${args.name}`;
|
|
71893
72179
|
const removed = [];
|
|
71894
72180
|
if (await exists2(worktreeDir)) {
|
|
@@ -71948,13 +72234,13 @@ try {
|
|
|
71948
72234
|
process.exit(0);
|
|
71949
72235
|
}
|
|
71950
72236
|
if (args.mode === "task" && args.name) {
|
|
71951
|
-
await mkdir4(
|
|
71952
|
-
await mkdir4(
|
|
72237
|
+
await mkdir4(join20(statesDir, args.name), { recursive: true });
|
|
72238
|
+
await mkdir4(join20(tasksDir, args.name), { recursive: true });
|
|
71953
72239
|
}
|
|
71954
72240
|
if (args.mode === "agent") {
|
|
71955
72241
|
await mkdir4(statesDir, { recursive: true });
|
|
71956
72242
|
await mkdir4(tasksDir, { recursive: true });
|
|
71957
|
-
await mkdir4(
|
|
72243
|
+
await mkdir4(join20(projectRoot, ".ralph"), { recursive: true });
|
|
71958
72244
|
}
|
|
71959
72245
|
await runWithContext(createDefaultContext(), async () => {
|
|
71960
72246
|
const { waitUntilExit } = render_default(import_react59.createElement(App2, { args, statesDir, tasksDir, projectRoot }));
|