@neriros/ralphy 2.11.1 → 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 +1231 -849
- 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";
|
|
@@ -65702,11 +65702,16 @@ async function runEngine(opts) {
|
|
|
65702
65702
|
stderr: isClaude ? "inherit" : "pipe",
|
|
65703
65703
|
...opts.cwd ? { cwd: opts.cwd } : {}
|
|
65704
65704
|
});
|
|
65705
|
+
let intentionalKill = false;
|
|
65706
|
+
const killProc = () => {
|
|
65707
|
+
intentionalKill = true;
|
|
65708
|
+
proc.kill();
|
|
65709
|
+
};
|
|
65705
65710
|
if (opts.signal) {
|
|
65706
65711
|
if (opts.signal.aborted) {
|
|
65707
|
-
|
|
65712
|
+
killProc();
|
|
65708
65713
|
} else {
|
|
65709
|
-
opts.signal.addEventListener("abort",
|
|
65714
|
+
opts.signal.addEventListener("abort", killProc, { once: true });
|
|
65710
65715
|
}
|
|
65711
65716
|
}
|
|
65712
65717
|
const stdin = proc.stdin;
|
|
@@ -65742,7 +65747,7 @@ async function runEngine(opts) {
|
|
|
65742
65747
|
if (opts.signal) {
|
|
65743
65748
|
const onAbort = () => {
|
|
65744
65749
|
aborted = true;
|
|
65745
|
-
|
|
65750
|
+
killProc();
|
|
65746
65751
|
};
|
|
65747
65752
|
if (opts.signal.aborted) {
|
|
65748
65753
|
onAbort();
|
|
@@ -65778,7 +65783,7 @@ async function runEngine(opts) {
|
|
|
65778
65783
|
emitEvent(event);
|
|
65779
65784
|
}
|
|
65780
65785
|
if (claudeState.gotResult) {
|
|
65781
|
-
|
|
65786
|
+
killProc();
|
|
65782
65787
|
break;
|
|
65783
65788
|
}
|
|
65784
65789
|
}
|
|
@@ -65807,7 +65812,7 @@ async function runEngine(opts) {
|
|
|
65807
65812
|
}
|
|
65808
65813
|
await closeRaw();
|
|
65809
65814
|
const exitCode = await proc.exited;
|
|
65810
|
-
const wasIntentionalKill = (exitCode === 143 || exitCode === 137)
|
|
65815
|
+
const wasIntentionalKill = intentionalKill && (exitCode === 143 || exitCode === 137);
|
|
65811
65816
|
const normalizedExitCode = wasIntentionalKill ? 0 : exitCode;
|
|
65812
65817
|
return { exitCode: normalizedExitCode, usage, sessionId, rateLimited: detectedRateLimit };
|
|
65813
65818
|
}
|
|
@@ -69477,8 +69482,9 @@ async function shutdown() {
|
|
|
69477
69482
|
|
|
69478
69483
|
// packages/core/src/loop.ts
|
|
69479
69484
|
import { join as join7 } from "path";
|
|
69480
|
-
|
|
69481
|
-
|
|
69485
|
+
|
|
69486
|
+
// packages/core/src/tasks-md.ts
|
|
69487
|
+
function firstUnchecked(tasksContent) {
|
|
69482
69488
|
const sections = tasksContent.split(/(?=^## )/m);
|
|
69483
69489
|
for (const section of sections) {
|
|
69484
69490
|
if (/^## /m.test(section) && /^- \[ \]/m.test(section))
|
|
@@ -69488,12 +69494,41 @@ function extractFirstUncheckedSection(tasksContent) {
|
|
|
69488
69494
|
return tasksContent.trim();
|
|
69489
69495
|
return null;
|
|
69490
69496
|
}
|
|
69491
|
-
function
|
|
69497
|
+
function countUnchecked(tasksContent) {
|
|
69492
69498
|
return (tasksContent.match(/^- \[ \]/gm) ?? []).length;
|
|
69493
69499
|
}
|
|
69494
|
-
function
|
|
69500
|
+
function allCompleted(tasksContent) {
|
|
69495
69501
|
return !/^- \[ \]/m.test(tasksContent);
|
|
69496
69502
|
}
|
|
69503
|
+
function prependSection(existing, heading, body) {
|
|
69504
|
+
const section = `## ${heading}
|
|
69505
|
+
|
|
69506
|
+
${body.trimEnd()}
|
|
69507
|
+
|
|
69508
|
+
`;
|
|
69509
|
+
const headingIdx = existing.search(/^## /m);
|
|
69510
|
+
if (headingIdx === -1) {
|
|
69511
|
+
return existing.trimEnd() + (existing ? `
|
|
69512
|
+
|
|
69513
|
+
` : "") + section;
|
|
69514
|
+
}
|
|
69515
|
+
return existing.slice(0, headingIdx) + section + existing.slice(headingIdx);
|
|
69516
|
+
}
|
|
69517
|
+
async function prependFixTask(tasksPath, heading, failureOutput) {
|
|
69518
|
+
const file = Bun.file(tasksPath);
|
|
69519
|
+
const existing = await file.exists() ? await file.text() : "";
|
|
69520
|
+
const stamped = `${heading} (${new Date().toISOString()})`;
|
|
69521
|
+
const fence = "```";
|
|
69522
|
+
const body = `- [ ] ${heading}. Read the error block below, fix the underlying ` + `problem (do not just retry the failing command), then check this box.
|
|
69523
|
+
|
|
69524
|
+
` + `${fence}
|
|
69525
|
+
${failureOutput.trim()}
|
|
69526
|
+
${fence}`;
|
|
69527
|
+
await Bun.write(tasksPath, prependSection(existing, stamped, body));
|
|
69528
|
+
}
|
|
69529
|
+
|
|
69530
|
+
// packages/core/src/loop.ts
|
|
69531
|
+
var STEERING_MAX_LINES = 20;
|
|
69497
69532
|
function buildTaskPrompt(state, taskDir) {
|
|
69498
69533
|
const storage = getStorage();
|
|
69499
69534
|
let prompt = "";
|
|
@@ -69518,7 +69553,7 @@ function buildTaskPrompt(state, taskDir) {
|
|
|
69518
69553
|
}
|
|
69519
69554
|
const tasksContent = storage.read(join7(taskDir, "tasks.md"));
|
|
69520
69555
|
if (tasksContent !== null) {
|
|
69521
|
-
const section =
|
|
69556
|
+
const section = firstUnchecked(tasksContent);
|
|
69522
69557
|
if (section) {
|
|
69523
69558
|
prompt += `---
|
|
69524
69559
|
|
|
@@ -69753,10 +69788,10 @@ function useLoop(opts) {
|
|
|
69753
69788
|
}
|
|
69754
69789
|
const tasksContent = storage.read(join8(tasksDir, "tasks.md"));
|
|
69755
69790
|
if (tasksContent !== null) {
|
|
69756
|
-
const remaining =
|
|
69791
|
+
const remaining = countUnchecked(tasksContent);
|
|
69757
69792
|
addInfo(`tasks.md: ${remaining} unchecked item${remaining === 1 ? "" : "s"} remaining`);
|
|
69758
69793
|
}
|
|
69759
|
-
if (tasksContent !== null &&
|
|
69794
|
+
if (tasksContent !== null && allCompleted(tasksContent)) {
|
|
69760
69795
|
addInfo("All tasks completed \u2014 archiving change.");
|
|
69761
69796
|
currentState = {
|
|
69762
69797
|
...currentState,
|
|
@@ -70069,6 +70104,170 @@ function TaskLoop({ opts }) {
|
|
|
70069
70104
|
|
|
70070
70105
|
// apps/cli/src/components/AgentMode.tsx
|
|
70071
70106
|
var import_react57 = __toESM(require_react(), 1);
|
|
70107
|
+
import { join as join17 } from "path";
|
|
70108
|
+
|
|
70109
|
+
// apps/cli/src/agent/state.ts
|
|
70110
|
+
import { join as join10 } from "path";
|
|
70111
|
+
var TaskStateSchema = exports_external.enum(["started", "processed", "failed"]);
|
|
70112
|
+
var TaskEntrySchema = exports_external.object({
|
|
70113
|
+
issueId: exports_external.string(),
|
|
70114
|
+
identifier: exports_external.string(),
|
|
70115
|
+
state: TaskStateSchema,
|
|
70116
|
+
changeName: exports_external.string().optional(),
|
|
70117
|
+
startedAt: exports_external.string().optional(),
|
|
70118
|
+
finishedAt: exports_external.string().optional(),
|
|
70119
|
+
exitCode: exports_external.number().optional(),
|
|
70120
|
+
commentPosted: exports_external.boolean().optional()
|
|
70121
|
+
});
|
|
70122
|
+
var AgentStateSchema = exports_external.object({
|
|
70123
|
+
tasks: exports_external.record(exports_external.string(), TaskEntrySchema).default({}),
|
|
70124
|
+
lastPollAt: exports_external.string().nullable().default(null)
|
|
70125
|
+
});
|
|
70126
|
+
function statePath(projectRoot) {
|
|
70127
|
+
return join10(projectRoot, ".ralph", "agent-state.json");
|
|
70128
|
+
}
|
|
70129
|
+
async function readState2(projectRoot) {
|
|
70130
|
+
const file = Bun.file(statePath(projectRoot));
|
|
70131
|
+
if (!await file.exists()) {
|
|
70132
|
+
return AgentStateSchema.parse({});
|
|
70133
|
+
}
|
|
70134
|
+
const raw = await file.json();
|
|
70135
|
+
return AgentStateSchema.parse(raw);
|
|
70136
|
+
}
|
|
70137
|
+
async function writeState2(projectRoot, state) {
|
|
70138
|
+
await Bun.write(statePath(projectRoot), JSON.stringify(state, null, 2) + `
|
|
70139
|
+
`);
|
|
70140
|
+
}
|
|
70141
|
+
|
|
70142
|
+
class AgentStateStore {
|
|
70143
|
+
projectRoot;
|
|
70144
|
+
state = null;
|
|
70145
|
+
constructor(projectRoot) {
|
|
70146
|
+
this.projectRoot = projectRoot;
|
|
70147
|
+
}
|
|
70148
|
+
async load() {
|
|
70149
|
+
this.state = await readState2(this.projectRoot);
|
|
70150
|
+
}
|
|
70151
|
+
snapshot() {
|
|
70152
|
+
if (!this.state) {
|
|
70153
|
+
throw new Error("AgentStateStore: load() must be called before snapshot()");
|
|
70154
|
+
}
|
|
70155
|
+
return this.state;
|
|
70156
|
+
}
|
|
70157
|
+
async upsertTask(issue, patch) {
|
|
70158
|
+
const s = this.snapshot();
|
|
70159
|
+
const existing = s.tasks[issue.identifier];
|
|
70160
|
+
s.tasks[issue.identifier] = {
|
|
70161
|
+
issueId: issue.id,
|
|
70162
|
+
identifier: issue.identifier,
|
|
70163
|
+
state: existing?.state ?? "started",
|
|
70164
|
+
...existing,
|
|
70165
|
+
...patch
|
|
70166
|
+
};
|
|
70167
|
+
await this.flush();
|
|
70168
|
+
}
|
|
70169
|
+
async setLastPollAt(when) {
|
|
70170
|
+
const s = this.snapshot();
|
|
70171
|
+
s.lastPollAt = when;
|
|
70172
|
+
await this.flush();
|
|
70173
|
+
}
|
|
70174
|
+
async removeByChangeName(changeName) {
|
|
70175
|
+
const s = this.snapshot();
|
|
70176
|
+
const entry = Object.values(s.tasks).find((t) => t.changeName === changeName);
|
|
70177
|
+
if (!entry)
|
|
70178
|
+
return null;
|
|
70179
|
+
delete s.tasks[entry.identifier];
|
|
70180
|
+
await this.flush();
|
|
70181
|
+
return { identifier: entry.identifier, issueId: entry.issueId };
|
|
70182
|
+
}
|
|
70183
|
+
async flush() {
|
|
70184
|
+
await writeState2(this.projectRoot, this.snapshot());
|
|
70185
|
+
}
|
|
70186
|
+
}
|
|
70187
|
+
|
|
70188
|
+
// apps/cli/src/agent/config.ts
|
|
70189
|
+
import { join as join11 } from "path";
|
|
70190
|
+
var RalphyConfigSchema = exports_external.object({
|
|
70191
|
+
concurrency: exports_external.number().int().positive().default(1),
|
|
70192
|
+
pollIntervalSeconds: exports_external.number().int().positive().default(60),
|
|
70193
|
+
maxIterationsPerTask: exports_external.number().int().nonnegative().default(0),
|
|
70194
|
+
maxCostUsdPerTask: exports_external.number().nonnegative().default(0),
|
|
70195
|
+
maxRuntimeMinutesPerTask: exports_external.number().nonnegative().default(0),
|
|
70196
|
+
maxConsecutiveFailuresPerTask: exports_external.number().int().nonnegative().default(5),
|
|
70197
|
+
iterationDelaySeconds: exports_external.number().int().nonnegative().default(0),
|
|
70198
|
+
logRawStream: exports_external.boolean().default(false),
|
|
70199
|
+
taskVerbose: exports_external.boolean().default(false),
|
|
70200
|
+
useWorktree: exports_external.boolean().default(false),
|
|
70201
|
+
cleanupWorktreeOnSuccess: exports_external.boolean().default(false),
|
|
70202
|
+
setupScript: exports_external.string().optional(),
|
|
70203
|
+
teardownScript: exports_external.string().optional(),
|
|
70204
|
+
appendPrompt: exports_external.string().optional(),
|
|
70205
|
+
createPrOnSuccess: exports_external.boolean().default(false),
|
|
70206
|
+
prBaseBranch: exports_external.string().default("main"),
|
|
70207
|
+
fixCiOnFailure: exports_external.boolean().default(false),
|
|
70208
|
+
maxCiFixAttempts: exports_external.number().int().positive().default(5),
|
|
70209
|
+
ciPollIntervalSeconds: exports_external.number().int().positive().default(30),
|
|
70210
|
+
engine: exports_external.enum(["claude", "codex"]).default("claude"),
|
|
70211
|
+
model: exports_external.enum(["haiku", "sonnet", "opus"]).default("opus"),
|
|
70212
|
+
linear: exports_external.object({
|
|
70213
|
+
team: exports_external.string().optional(),
|
|
70214
|
+
assignee: exports_external.string().optional(),
|
|
70215
|
+
statuses: exports_external.array(exports_external.string()).default([]),
|
|
70216
|
+
labels: exports_external.union([exports_external.array(exports_external.string()), exports_external.string()]).transform((v) => typeof v === "string" ? [v] : v).default([]),
|
|
70217
|
+
inProgressStatus: exports_external.string().optional(),
|
|
70218
|
+
doneStatus: exports_external.string().optional(),
|
|
70219
|
+
doneLabel: exports_external.string().optional(),
|
|
70220
|
+
postComments: exports_external.boolean().default(true),
|
|
70221
|
+
updateEveryIterations: exports_external.number().int().nonnegative().default(10)
|
|
70222
|
+
}).default({ statuses: [], labels: [], postComments: true })
|
|
70223
|
+
}).default({
|
|
70224
|
+
concurrency: 1,
|
|
70225
|
+
pollIntervalSeconds: 60,
|
|
70226
|
+
maxIterationsPerTask: 0,
|
|
70227
|
+
maxCostUsdPerTask: 0,
|
|
70228
|
+
engine: "claude",
|
|
70229
|
+
model: "opus",
|
|
70230
|
+
linear: { statuses: [], labels: [], postComments: true }
|
|
70231
|
+
});
|
|
70232
|
+
async function loadRalphyConfig(projectRoot) {
|
|
70233
|
+
const path = join11(projectRoot, "ralphy.config.json");
|
|
70234
|
+
const file = Bun.file(path);
|
|
70235
|
+
if (!await file.exists()) {
|
|
70236
|
+
return RalphyConfigSchema.parse({});
|
|
70237
|
+
}
|
|
70238
|
+
const raw = await file.json();
|
|
70239
|
+
return RalphyConfigSchema.parse(raw);
|
|
70240
|
+
}
|
|
70241
|
+
async function ensureRalphyConfig(projectRoot) {
|
|
70242
|
+
const path = join11(projectRoot, "ralphy.config.json");
|
|
70243
|
+
const file = Bun.file(path);
|
|
70244
|
+
if (await file.exists())
|
|
70245
|
+
return path;
|
|
70246
|
+
const defaults2 = RalphyConfigSchema.parse({});
|
|
70247
|
+
await Bun.write(path, JSON.stringify(defaults2, null, 2) + `
|
|
70248
|
+
`);
|
|
70249
|
+
return path;
|
|
70250
|
+
}
|
|
70251
|
+
|
|
70252
|
+
// apps/cli/src/agent/wire.ts
|
|
70253
|
+
import { join as join16 } from "path";
|
|
70254
|
+
|
|
70255
|
+
// packages/core/src/layout.ts
|
|
70256
|
+
import { join as join12 } from "path";
|
|
70257
|
+
var STATE_FILE2 = ".ralph-state.json";
|
|
70258
|
+
function projectLayout(root) {
|
|
70259
|
+
const statesDir = join12(root, ".ralph", "tasks");
|
|
70260
|
+
const tasksDir = join12(root, "openspec", "changes");
|
|
70261
|
+
return {
|
|
70262
|
+
root,
|
|
70263
|
+
statesDir,
|
|
70264
|
+
tasksDir,
|
|
70265
|
+
agentStateFile: join12(root, ".ralph", "agent-state.json"),
|
|
70266
|
+
changeDir: (name) => join12(tasksDir, name),
|
|
70267
|
+
taskStateDir: (name) => join12(statesDir, name),
|
|
70268
|
+
stateFile: (name) => join12(statesDir, name, STATE_FILE2)
|
|
70269
|
+
};
|
|
70270
|
+
}
|
|
70072
70271
|
|
|
70073
70272
|
// apps/cli/src/agent/linear.ts
|
|
70074
70273
|
var OPEN_STATE_TYPES = ["unstarted", "started", "backlog"];
|
|
@@ -70211,300 +70410,78 @@ async function addLabelToIssue(apiKey, issueId, labelId) {
|
|
|
70211
70410
|
});
|
|
70212
70411
|
}
|
|
70213
70412
|
|
|
70214
|
-
// apps/cli/src/agent/
|
|
70215
|
-
|
|
70216
|
-
|
|
70217
|
-
|
|
70218
|
-
|
|
70219
|
-
|
|
70220
|
-
|
|
70221
|
-
|
|
70222
|
-
|
|
70223
|
-
|
|
70224
|
-
|
|
70225
|
-
|
|
70226
|
-
|
|
70227
|
-
|
|
70228
|
-
|
|
70229
|
-
|
|
70230
|
-
|
|
70231
|
-
|
|
70232
|
-
|
|
70233
|
-
|
|
70234
|
-
|
|
70235
|
-
|
|
70236
|
-
|
|
70237
|
-
|
|
70238
|
-
|
|
70239
|
-
|
|
70240
|
-
|
|
70241
|
-
|
|
70242
|
-
|
|
70243
|
-
|
|
70244
|
-
|
|
70245
|
-
|
|
70246
|
-
|
|
70247
|
-
|
|
70248
|
-
|
|
70249
|
-
|
|
70250
|
-
|
|
70251
|
-
|
|
70413
|
+
// apps/cli/src/agent/coordinator.ts
|
|
70414
|
+
class AgentCoordinator {
|
|
70415
|
+
deps;
|
|
70416
|
+
opts;
|
|
70417
|
+
workers = [];
|
|
70418
|
+
pendingIds = new Set;
|
|
70419
|
+
queue = [];
|
|
70420
|
+
stopped = false;
|
|
70421
|
+
constructor(deps, opts) {
|
|
70422
|
+
this.deps = deps;
|
|
70423
|
+
this.opts = opts;
|
|
70424
|
+
}
|
|
70425
|
+
get activeCount() {
|
|
70426
|
+
return this.workers.length;
|
|
70427
|
+
}
|
|
70428
|
+
get queuedCount() {
|
|
70429
|
+
return this.queue.length;
|
|
70430
|
+
}
|
|
70431
|
+
get activeWorkers() {
|
|
70432
|
+
return this.workers;
|
|
70433
|
+
}
|
|
70434
|
+
async init() {}
|
|
70435
|
+
async pollOnce() {
|
|
70436
|
+
if (this.stopped)
|
|
70437
|
+
return { found: 0, added: 0 };
|
|
70438
|
+
let issues;
|
|
70439
|
+
try {
|
|
70440
|
+
issues = await this.deps.fetchIssues(this.opts.filter);
|
|
70441
|
+
} catch (err) {
|
|
70442
|
+
this.deps.onLog(`! Linear poll failed: ${err.message}`, "red");
|
|
70443
|
+
return { found: 0, added: 0 };
|
|
70444
|
+
}
|
|
70445
|
+
const state = this.deps.store.snapshot();
|
|
70446
|
+
const tasksByIssueId = new Map;
|
|
70447
|
+
for (const entry of Object.values(state.tasks)) {
|
|
70448
|
+
tasksByIssueId.set(entry.issueId, entry);
|
|
70449
|
+
}
|
|
70450
|
+
const isProcessed = (id) => tasksByIssueId.get(id)?.state === "processed";
|
|
70451
|
+
const isFailed = (id) => tasksByIssueId.get(id)?.state === "failed";
|
|
70452
|
+
const queued = new Set(this.queue.map((i) => i.id));
|
|
70453
|
+
const active = new Set(this.workers.map((w) => w.issueId));
|
|
70454
|
+
let added = 0;
|
|
70455
|
+
for (const issue of issues) {
|
|
70456
|
+
if (isProcessed(issue.id))
|
|
70252
70457
|
continue;
|
|
70253
|
-
|
|
70254
|
-
|
|
70255
|
-
|
|
70256
|
-
|
|
70257
|
-
|
|
70258
|
-
|
|
70458
|
+
if (isFailed(issue.id))
|
|
70459
|
+
continue;
|
|
70460
|
+
if (queued.has(issue.id))
|
|
70461
|
+
continue;
|
|
70462
|
+
if (active.has(issue.id))
|
|
70463
|
+
continue;
|
|
70464
|
+
if (this.pendingIds.has(issue.id))
|
|
70465
|
+
continue;
|
|
70466
|
+
const blocker = issue.blockedByIds.find((bid) => !isProcessed(bid));
|
|
70467
|
+
if (blocker !== undefined) {
|
|
70468
|
+
this.deps.onLog(` \u23F8 ${issue.identifier} skipped \u2014 blocked by unresolved dependency`, "yellow");
|
|
70469
|
+
continue;
|
|
70470
|
+
}
|
|
70471
|
+
this.queue.push(issue);
|
|
70472
|
+
added += 1;
|
|
70259
70473
|
}
|
|
70260
|
-
|
|
70261
|
-
|
|
70262
|
-
|
|
70263
|
-
|
|
70264
|
-
|
|
70265
|
-
|
|
70266
|
-
|
|
70267
|
-
|
|
70268
|
-
|
|
70269
|
-
|
|
70270
|
-
|
|
70271
|
-
}
|
|
70272
|
-
return { tasks, lastPollAt: legacy.lastPollAt ?? null };
|
|
70273
|
-
}
|
|
70274
|
-
function statePath(projectRoot) {
|
|
70275
|
-
return join10(projectRoot, ".ralph", "agent-state.json");
|
|
70276
|
-
}
|
|
70277
|
-
function looksLegacy(raw) {
|
|
70278
|
-
if (typeof raw !== "object" || raw === null)
|
|
70279
|
-
return false;
|
|
70280
|
-
const r = raw;
|
|
70281
|
-
return "processedIssueIds" in r || "startedIssueIds" in r || "failedIssueIds" in r || "changeMeta" in r;
|
|
70282
|
-
}
|
|
70283
|
-
async function readAgentState(projectRoot) {
|
|
70284
|
-
const file = Bun.file(statePath(projectRoot));
|
|
70285
|
-
if (!await file.exists()) {
|
|
70286
|
-
return AgentStateSchema.parse({});
|
|
70287
|
-
}
|
|
70288
|
-
const raw = await file.json();
|
|
70289
|
-
if (looksLegacy(raw))
|
|
70290
|
-
return migrateLegacy(raw);
|
|
70291
|
-
return AgentStateSchema.parse(raw);
|
|
70292
|
-
}
|
|
70293
|
-
async function writeAgentState(projectRoot, state) {
|
|
70294
|
-
await Bun.write(statePath(projectRoot), JSON.stringify(state, null, 2) + `
|
|
70295
|
-
`);
|
|
70296
|
-
}
|
|
70297
|
-
|
|
70298
|
-
// apps/cli/src/agent/scaffold.ts
|
|
70299
|
-
import { join as join11 } from "path";
|
|
70300
|
-
import { mkdir as mkdir2 } from "fs/promises";
|
|
70301
|
-
function changeNameForIssue(issue) {
|
|
70302
|
-
const slug = issue.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
70303
|
-
return slug ? `${issue.identifier.toLowerCase()}-${slug}` : issue.identifier.toLowerCase();
|
|
70304
|
-
}
|
|
70305
|
-
async function scaffoldChangeForIssue(tasksDir, statesDir, issue, comments = [], appendPrompt = "") {
|
|
70306
|
-
const name = changeNameForIssue(issue);
|
|
70307
|
-
const changeDir = join11(tasksDir, name);
|
|
70308
|
-
const stateDir = join11(statesDir, name);
|
|
70309
|
-
await mkdir2(changeDir, { recursive: true });
|
|
70310
|
-
await mkdir2(join11(changeDir, "specs"), { recursive: true });
|
|
70311
|
-
await mkdir2(stateDir, { recursive: true });
|
|
70312
|
-
const commentsBlock = comments.length > 0 ? [
|
|
70313
|
-
"",
|
|
70314
|
-
"## Linear comments",
|
|
70315
|
-
"",
|
|
70316
|
-
...comments.flatMap((c) => [
|
|
70317
|
-
`**${c.user?.name ?? "unknown"}** \u2014 ${c.createdAt}`,
|
|
70318
|
-
"",
|
|
70319
|
-
c.body.trim(),
|
|
70320
|
-
""
|
|
70321
|
-
])
|
|
70322
|
-
] : [];
|
|
70323
|
-
const proposal = [
|
|
70324
|
-
`# ${issue.identifier}: ${issue.title}`,
|
|
70325
|
-
"",
|
|
70326
|
-
`Source: [${issue.identifier}](${issue.url})`,
|
|
70327
|
-
`Status: ${issue.state.name}`,
|
|
70328
|
-
issue.assignee ? `Assignee: ${issue.assignee.name}` : "",
|
|
70329
|
-
issue.labels.length ? `Labels: ${issue.labels.join(", ")}` : "",
|
|
70330
|
-
"",
|
|
70331
|
-
"## Description",
|
|
70332
|
-
"",
|
|
70333
|
-
issue.description?.trim() || "_No description provided in Linear._",
|
|
70334
|
-
...commentsBlock,
|
|
70335
|
-
...appendPrompt.trim() ? ["", "## Additional instructions", "", appendPrompt.trim()] : [],
|
|
70336
|
-
"",
|
|
70337
|
-
"## Steering",
|
|
70338
|
-
"",
|
|
70339
|
-
"_Add steering notes here as the loop runs._",
|
|
70340
|
-
""
|
|
70341
|
-
].filter((l) => l !== "").join(`
|
|
70342
|
-
`);
|
|
70343
|
-
const tasks = [
|
|
70344
|
-
`# Tasks for ${issue.identifier}`,
|
|
70345
|
-
"",
|
|
70346
|
-
"## Subtasks",
|
|
70347
|
-
"",
|
|
70348
|
-
`- [ ] Read the Linear issue at ${issue.url} and break it into concrete subtasks`,
|
|
70349
|
-
`- [ ] Implement the changes described in proposal.md`,
|
|
70350
|
-
`- [ ] Add or update tests covering the new behavior`,
|
|
70351
|
-
`- [ ] Run \`bun run lint\` and \`bun run test\` and fix any failures`,
|
|
70352
|
-
""
|
|
70353
|
-
].join(`
|
|
70354
|
-
`);
|
|
70355
|
-
const design = [
|
|
70356
|
-
`# Design for ${issue.identifier}`,
|
|
70357
|
-
"",
|
|
70358
|
-
"_Fill in the technical design as you work through the issue._",
|
|
70359
|
-
""
|
|
70360
|
-
].join(`
|
|
70361
|
-
`);
|
|
70362
|
-
await Bun.write(join11(changeDir, "proposal.md"), proposal);
|
|
70363
|
-
await Bun.write(join11(changeDir, "tasks.md"), tasks);
|
|
70364
|
-
await Bun.write(join11(changeDir, "design.md"), design);
|
|
70365
|
-
return name;
|
|
70366
|
-
}
|
|
70367
|
-
|
|
70368
|
-
// apps/cli/src/agent/config.ts
|
|
70369
|
-
import { join as join12 } from "path";
|
|
70370
|
-
var RalphyConfigSchema = exports_external.object({
|
|
70371
|
-
concurrency: exports_external.number().int().positive().default(1),
|
|
70372
|
-
pollIntervalSeconds: exports_external.number().int().positive().default(60),
|
|
70373
|
-
maxIterationsPerTask: exports_external.number().int().nonnegative().default(0),
|
|
70374
|
-
maxCostUsdPerTask: exports_external.number().nonnegative().default(0),
|
|
70375
|
-
maxRuntimeMinutesPerTask: exports_external.number().nonnegative().default(0),
|
|
70376
|
-
maxConsecutiveFailuresPerTask: exports_external.number().int().nonnegative().default(5),
|
|
70377
|
-
iterationDelaySeconds: exports_external.number().int().nonnegative().default(0),
|
|
70378
|
-
logRawStream: exports_external.boolean().default(false),
|
|
70379
|
-
taskVerbose: exports_external.boolean().default(false),
|
|
70380
|
-
useWorktree: exports_external.boolean().default(false),
|
|
70381
|
-
cleanupWorktreeOnSuccess: exports_external.boolean().default(false),
|
|
70382
|
-
setupScript: exports_external.string().optional(),
|
|
70383
|
-
teardownScript: exports_external.string().optional(),
|
|
70384
|
-
appendPrompt: exports_external.string().optional(),
|
|
70385
|
-
createPrOnSuccess: exports_external.boolean().default(false),
|
|
70386
|
-
prBaseBranch: exports_external.string().default("main"),
|
|
70387
|
-
fixCiOnFailure: exports_external.boolean().default(false),
|
|
70388
|
-
maxCiFixAttempts: exports_external.number().int().positive().default(5),
|
|
70389
|
-
ciPollIntervalSeconds: exports_external.number().int().positive().default(30),
|
|
70390
|
-
engine: exports_external.enum(["claude", "codex"]).default("claude"),
|
|
70391
|
-
model: exports_external.enum(["haiku", "sonnet", "opus"]).default("opus"),
|
|
70392
|
-
linear: exports_external.object({
|
|
70393
|
-
team: exports_external.string().optional(),
|
|
70394
|
-
assignee: exports_external.string().optional(),
|
|
70395
|
-
statuses: exports_external.array(exports_external.string()).default([]),
|
|
70396
|
-
labels: exports_external.union([exports_external.array(exports_external.string()), exports_external.string()]).transform((v) => typeof v === "string" ? [v] : v).default([]),
|
|
70397
|
-
inProgressStatus: exports_external.string().optional(),
|
|
70398
|
-
doneStatus: exports_external.string().optional(),
|
|
70399
|
-
doneLabel: exports_external.string().optional(),
|
|
70400
|
-
postComments: exports_external.boolean().default(true),
|
|
70401
|
-
updateEveryIterations: exports_external.number().int().nonnegative().default(10)
|
|
70402
|
-
}).default({ statuses: [], labels: [], postComments: true })
|
|
70403
|
-
}).default({
|
|
70404
|
-
concurrency: 1,
|
|
70405
|
-
pollIntervalSeconds: 60,
|
|
70406
|
-
maxIterationsPerTask: 0,
|
|
70407
|
-
maxCostUsdPerTask: 0,
|
|
70408
|
-
engine: "claude",
|
|
70409
|
-
model: "opus",
|
|
70410
|
-
linear: { statuses: [], labels: [], postComments: true }
|
|
70411
|
-
});
|
|
70412
|
-
async function loadRalphyConfig(projectRoot) {
|
|
70413
|
-
const path = join12(projectRoot, "ralphy.config.json");
|
|
70414
|
-
const file = Bun.file(path);
|
|
70415
|
-
if (!await file.exists()) {
|
|
70416
|
-
return RalphyConfigSchema.parse({});
|
|
70417
|
-
}
|
|
70418
|
-
const raw = await file.json();
|
|
70419
|
-
return RalphyConfigSchema.parse(raw);
|
|
70420
|
-
}
|
|
70421
|
-
async function ensureRalphyConfig(projectRoot) {
|
|
70422
|
-
const path = join12(projectRoot, "ralphy.config.json");
|
|
70423
|
-
const file = Bun.file(path);
|
|
70424
|
-
if (await file.exists())
|
|
70425
|
-
return path;
|
|
70426
|
-
const defaults2 = RalphyConfigSchema.parse({});
|
|
70427
|
-
await Bun.write(path, JSON.stringify(defaults2, null, 2) + `
|
|
70428
|
-
`);
|
|
70429
|
-
return path;
|
|
70430
|
-
}
|
|
70431
|
-
|
|
70432
|
-
// apps/cli/src/agent/coordinator.ts
|
|
70433
|
-
class AgentCoordinator {
|
|
70434
|
-
deps;
|
|
70435
|
-
opts;
|
|
70436
|
-
workers = [];
|
|
70437
|
-
pendingIds = new Set;
|
|
70438
|
-
queue = [];
|
|
70439
|
-
state = null;
|
|
70440
|
-
stopped = false;
|
|
70441
|
-
constructor(deps, opts) {
|
|
70442
|
-
this.deps = deps;
|
|
70443
|
-
this.opts = opts;
|
|
70444
|
-
}
|
|
70445
|
-
get activeCount() {
|
|
70446
|
-
return this.workers.length;
|
|
70447
|
-
}
|
|
70448
|
-
get queuedCount() {
|
|
70449
|
-
return this.queue.length;
|
|
70450
|
-
}
|
|
70451
|
-
get activeWorkers() {
|
|
70452
|
-
return this.workers;
|
|
70453
|
-
}
|
|
70454
|
-
async init() {
|
|
70455
|
-
this.state = await this.deps.loadState();
|
|
70456
|
-
}
|
|
70457
|
-
async pollOnce() {
|
|
70458
|
-
if (this.stopped)
|
|
70459
|
-
return { found: 0, added: 0 };
|
|
70460
|
-
let issues;
|
|
70461
|
-
try {
|
|
70462
|
-
issues = await this.deps.fetchIssues(this.opts.filter);
|
|
70463
|
-
} catch (err) {
|
|
70464
|
-
this.deps.onLog(`! Linear poll failed: ${err.message}`, "red");
|
|
70465
|
-
return { found: 0, added: 0 };
|
|
70466
|
-
}
|
|
70467
|
-
const state = this.state;
|
|
70468
|
-
const tasksByIssueId = new Map;
|
|
70469
|
-
for (const entry of Object.values(state.tasks)) {
|
|
70470
|
-
tasksByIssueId.set(entry.issueId, entry);
|
|
70471
|
-
}
|
|
70472
|
-
const isProcessed = (id) => tasksByIssueId.get(id)?.state === "processed";
|
|
70473
|
-
const isFailed = (id) => tasksByIssueId.get(id)?.state === "failed";
|
|
70474
|
-
const queued = new Set(this.queue.map((i) => i.id));
|
|
70475
|
-
const active = new Set(this.workers.map((w) => w.issueId));
|
|
70476
|
-
let added = 0;
|
|
70477
|
-
for (const issue of issues) {
|
|
70478
|
-
if (isProcessed(issue.id))
|
|
70479
|
-
continue;
|
|
70480
|
-
if (isFailed(issue.id))
|
|
70481
|
-
continue;
|
|
70482
|
-
if (queued.has(issue.id))
|
|
70483
|
-
continue;
|
|
70484
|
-
if (active.has(issue.id))
|
|
70485
|
-
continue;
|
|
70486
|
-
if (this.pendingIds.has(issue.id))
|
|
70487
|
-
continue;
|
|
70488
|
-
const blocker = issue.blockedByIds.find((bid) => !isProcessed(bid));
|
|
70489
|
-
if (blocker !== undefined) {
|
|
70490
|
-
this.deps.onLog(` \u23F8 ${issue.identifier} skipped \u2014 blocked by unresolved dependency`, "yellow");
|
|
70491
|
-
continue;
|
|
70492
|
-
}
|
|
70493
|
-
this.queue.push(issue);
|
|
70494
|
-
added += 1;
|
|
70495
|
-
}
|
|
70496
|
-
if (added > 0) {
|
|
70497
|
-
this.queue.sort((a, b) => {
|
|
70498
|
-
const pa = a.priority === 0 ? Infinity : a.priority;
|
|
70499
|
-
const pb = b.priority === 0 ? Infinity : b.priority;
|
|
70500
|
-
return pa - pb;
|
|
70501
|
-
});
|
|
70502
|
-
}
|
|
70503
|
-
state.lastPollAt = new Date().toISOString();
|
|
70504
|
-
await this.deps.saveState(state);
|
|
70505
|
-
this.spawnNext();
|
|
70506
|
-
await this.reportProgress();
|
|
70507
|
-
return { found: issues.length, added };
|
|
70474
|
+
if (added > 0) {
|
|
70475
|
+
this.queue.sort((a, b) => {
|
|
70476
|
+
const pa = a.priority === 0 ? Infinity : a.priority;
|
|
70477
|
+
const pb = b.priority === 0 ? Infinity : b.priority;
|
|
70478
|
+
return pa - pb;
|
|
70479
|
+
});
|
|
70480
|
+
}
|
|
70481
|
+
await this.deps.store.setLastPollAt(new Date().toISOString());
|
|
70482
|
+
this.spawnNext();
|
|
70483
|
+
await this.reportProgress();
|
|
70484
|
+
return { found: issues.length, added };
|
|
70508
70485
|
}
|
|
70509
70486
|
async reportProgress() {
|
|
70510
70487
|
const updater = this.deps.updater;
|
|
@@ -70535,7 +70512,7 @@ class AgentCoordinator {
|
|
|
70535
70512
|
}
|
|
70536
70513
|
}
|
|
70537
70514
|
spawnNext() {
|
|
70538
|
-
if (this.stopped
|
|
70515
|
+
if (this.stopped)
|
|
70539
70516
|
return;
|
|
70540
70517
|
while (this.workers.length + this.pendingIds.size < this.opts.concurrency && this.queue.length > 0) {
|
|
70541
70518
|
const issue = this.queue.shift();
|
|
@@ -70543,18 +70520,6 @@ class AgentCoordinator {
|
|
|
70543
70520
|
this.launchWorker(issue);
|
|
70544
70521
|
}
|
|
70545
70522
|
}
|
|
70546
|
-
upsertTask(issue, patch) {
|
|
70547
|
-
if (!this.state)
|
|
70548
|
-
return;
|
|
70549
|
-
const existing = this.state.tasks[issue.identifier];
|
|
70550
|
-
this.state.tasks[issue.identifier] = {
|
|
70551
|
-
issueId: issue.id,
|
|
70552
|
-
identifier: issue.identifier,
|
|
70553
|
-
state: existing?.state ?? "started",
|
|
70554
|
-
...existing,
|
|
70555
|
-
...patch
|
|
70556
|
-
};
|
|
70557
|
-
}
|
|
70558
70523
|
async launchWorker(issue) {
|
|
70559
70524
|
let changeName;
|
|
70560
70525
|
try {
|
|
@@ -70569,13 +70534,13 @@ class AgentCoordinator {
|
|
|
70569
70534
|
this.pendingIds.delete(issue.id);
|
|
70570
70535
|
return;
|
|
70571
70536
|
}
|
|
70572
|
-
|
|
70573
|
-
this.
|
|
70537
|
+
{
|
|
70538
|
+
const existing = this.deps.store.snapshot().tasks[issue.identifier];
|
|
70539
|
+
this.deps.store.upsertTask(issue, {
|
|
70574
70540
|
state: "started",
|
|
70575
70541
|
changeName,
|
|
70576
|
-
startedAt:
|
|
70542
|
+
startedAt: existing?.startedAt ?? new Date().toISOString()
|
|
70577
70543
|
});
|
|
70578
|
-
this.deps.saveState(this.state);
|
|
70579
70544
|
}
|
|
70580
70545
|
this.deps.onLog(`\u25B6 ${issue.identifier} \u2192 ${changeName} (worker started)`, "cyan");
|
|
70581
70546
|
const handle = this.deps.spawnWorker(changeName, issue);
|
|
@@ -70597,14 +70562,11 @@ class AgentCoordinator {
|
|
|
70597
70562
|
this.workers.splice(idx, 1);
|
|
70598
70563
|
const ok = code === 0;
|
|
70599
70564
|
this.deps.onLog(`${ok ? "\u2713" : "\u2717"} ${issue.identifier} \u2192 ${changeName} exited (code ${code})`, ok ? "green" : "red");
|
|
70600
|
-
|
|
70601
|
-
|
|
70602
|
-
|
|
70603
|
-
|
|
70604
|
-
|
|
70605
|
-
});
|
|
70606
|
-
this.deps.saveState(this.state);
|
|
70607
|
-
}
|
|
70565
|
+
this.deps.store.upsertTask(issue, {
|
|
70566
|
+
state: ok ? "processed" : "failed",
|
|
70567
|
+
finishedAt: new Date().toISOString(),
|
|
70568
|
+
exitCode: code
|
|
70569
|
+
});
|
|
70608
70570
|
this.notifyExited(issue, changeName, code);
|
|
70609
70571
|
this.deps.onWorkersChanged();
|
|
70610
70572
|
this.spawnNext();
|
|
@@ -70614,14 +70576,11 @@ class AgentCoordinator {
|
|
|
70614
70576
|
const updater = this.deps.updater;
|
|
70615
70577
|
if (!updater)
|
|
70616
70578
|
return;
|
|
70617
|
-
const alreadyCommented = this.
|
|
70579
|
+
const alreadyCommented = this.deps.store.snapshot().tasks[issue.identifier]?.commentPosted === true;
|
|
70618
70580
|
if (this.opts.postComments !== false && !alreadyCommented) {
|
|
70619
70581
|
try {
|
|
70620
70582
|
await updater.postComment(issue, `\uD83E\uDD16 Ralph started working on this issue. Tracking change: \`${changeName}\``);
|
|
70621
|
-
|
|
70622
|
-
this.upsertTask(issue, { commentPosted: true });
|
|
70623
|
-
await this.deps.saveState(this.state);
|
|
70624
|
-
}
|
|
70583
|
+
await this.deps.store.upsertTask(issue, { commentPosted: true });
|
|
70625
70584
|
} catch (err) {
|
|
70626
70585
|
this.deps.onLog(`! Linear comment failed for ${issue.identifier}: ${err.message}`, "red");
|
|
70627
70586
|
}
|
|
@@ -70694,66 +70653,137 @@ class AgentCoordinator {
|
|
|
70694
70653
|
}
|
|
70695
70654
|
}
|
|
70696
70655
|
|
|
70697
|
-
// apps/cli/src/agent/
|
|
70698
|
-
import {
|
|
70699
|
-
import {
|
|
70700
|
-
function
|
|
70701
|
-
|
|
70702
|
-
}
|
|
70703
|
-
function branchForChange(changeName) {
|
|
70704
|
-
return `ralph/${changeName}`;
|
|
70705
|
-
}
|
|
70706
|
-
async function createWorktree(projectRoot, changeName, runner) {
|
|
70707
|
-
const dir = worktreesDir(projectRoot);
|
|
70708
|
-
const cwd2 = join13(dir, changeName);
|
|
70709
|
-
const branch = branchForChange(changeName);
|
|
70710
|
-
const list = await runner.run(["worktree", "list", "--porcelain"], projectRoot);
|
|
70711
|
-
if (list.stdout.includes(`worktree ${cwd2}
|
|
70712
|
-
`)) {
|
|
70713
|
-
return { cwd: cwd2, branch };
|
|
70714
|
-
}
|
|
70715
|
-
let branchExists = true;
|
|
70716
|
-
try {
|
|
70717
|
-
await runner.run(["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], projectRoot);
|
|
70718
|
-
} catch {
|
|
70719
|
-
branchExists = false;
|
|
70720
|
-
}
|
|
70721
|
-
const cmd = branchExists ? ["worktree", "add", cwd2, branch] : ["worktree", "add", "-b", branch, cwd2];
|
|
70722
|
-
await runner.run(cmd, projectRoot);
|
|
70723
|
-
return { cwd: cwd2, branch };
|
|
70724
|
-
}
|
|
70725
|
-
async function removeWorktree(projectRoot, cwd2, runner) {
|
|
70726
|
-
await runner.run(["worktree", "remove", "--force", cwd2], projectRoot);
|
|
70656
|
+
// apps/cli/src/agent/scaffold.ts
|
|
70657
|
+
import { join as join13 } from "path";
|
|
70658
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
70659
|
+
function changeNameForIssue(issue) {
|
|
70660
|
+
const slug = issue.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
70661
|
+
return slug ? `${issue.identifier.toLowerCase()}-${slug}` : issue.identifier.toLowerCase();
|
|
70727
70662
|
}
|
|
70728
|
-
async function
|
|
70729
|
-
const
|
|
70730
|
-
const
|
|
70731
|
-
|
|
70732
|
-
|
|
70733
|
-
|
|
70734
|
-
|
|
70735
|
-
|
|
70736
|
-
|
|
70737
|
-
|
|
70738
|
-
|
|
70739
|
-
|
|
70740
|
-
|
|
70741
|
-
|
|
70742
|
-
|
|
70743
|
-
|
|
70744
|
-
|
|
70745
|
-
|
|
70746
|
-
|
|
70747
|
-
|
|
70748
|
-
|
|
70749
|
-
|
|
70750
|
-
|
|
70751
|
-
|
|
70752
|
-
}
|
|
70753
|
-
|
|
70754
|
-
|
|
70755
|
-
|
|
70756
|
-
|
|
70663
|
+
async function scaffoldChangeForIssue(tasksDir, statesDir, issue, comments = [], appendPrompt = "") {
|
|
70664
|
+
const name = changeNameForIssue(issue);
|
|
70665
|
+
const changeDir = join13(tasksDir, name);
|
|
70666
|
+
const stateDir = join13(statesDir, name);
|
|
70667
|
+
await mkdir2(changeDir, { recursive: true });
|
|
70668
|
+
await mkdir2(join13(changeDir, "specs"), { recursive: true });
|
|
70669
|
+
await mkdir2(stateDir, { recursive: true });
|
|
70670
|
+
const commentsBlock = comments.length > 0 ? [
|
|
70671
|
+
"",
|
|
70672
|
+
"## Linear comments",
|
|
70673
|
+
"",
|
|
70674
|
+
...comments.flatMap((c) => [
|
|
70675
|
+
`**${c.user?.name ?? "unknown"}** \u2014 ${c.createdAt}`,
|
|
70676
|
+
"",
|
|
70677
|
+
c.body.trim(),
|
|
70678
|
+
""
|
|
70679
|
+
])
|
|
70680
|
+
] : [];
|
|
70681
|
+
const proposal = [
|
|
70682
|
+
`# ${issue.identifier}: ${issue.title}`,
|
|
70683
|
+
"",
|
|
70684
|
+
`Source: [${issue.identifier}](${issue.url})`,
|
|
70685
|
+
`Status: ${issue.state.name}`,
|
|
70686
|
+
issue.assignee ? `Assignee: ${issue.assignee.name}` : "",
|
|
70687
|
+
issue.labels.length ? `Labels: ${issue.labels.join(", ")}` : "",
|
|
70688
|
+
"",
|
|
70689
|
+
"## Description",
|
|
70690
|
+
"",
|
|
70691
|
+
issue.description?.trim() || "_No description provided in Linear._",
|
|
70692
|
+
...commentsBlock,
|
|
70693
|
+
...appendPrompt.trim() ? ["", "## Additional instructions", "", appendPrompt.trim()] : [],
|
|
70694
|
+
"",
|
|
70695
|
+
"## Steering",
|
|
70696
|
+
"",
|
|
70697
|
+
"_Add steering notes here as the loop runs._",
|
|
70698
|
+
""
|
|
70699
|
+
].filter((l) => l !== "").join(`
|
|
70700
|
+
`);
|
|
70701
|
+
const tasks = [
|
|
70702
|
+
`# Tasks for ${issue.identifier}`,
|
|
70703
|
+
"",
|
|
70704
|
+
"## Subtasks",
|
|
70705
|
+
"",
|
|
70706
|
+
`- [ ] Read the Linear issue at ${issue.url} and break it into concrete subtasks`,
|
|
70707
|
+
`- [ ] Implement the changes described in proposal.md`,
|
|
70708
|
+
`- [ ] Add or update tests covering the new behavior`,
|
|
70709
|
+
`- [ ] Run \`bun run lint\` and \`bun run test\` and fix any failures`,
|
|
70710
|
+
""
|
|
70711
|
+
].join(`
|
|
70712
|
+
`);
|
|
70713
|
+
const design = [
|
|
70714
|
+
`# Design for ${issue.identifier}`,
|
|
70715
|
+
"",
|
|
70716
|
+
"_Fill in the technical design as you work through the issue._",
|
|
70717
|
+
""
|
|
70718
|
+
].join(`
|
|
70719
|
+
`);
|
|
70720
|
+
await Bun.write(join13(changeDir, "proposal.md"), proposal);
|
|
70721
|
+
await Bun.write(join13(changeDir, "tasks.md"), tasks);
|
|
70722
|
+
await Bun.write(join13(changeDir, "design.md"), design);
|
|
70723
|
+
return name;
|
|
70724
|
+
}
|
|
70725
|
+
|
|
70726
|
+
// apps/cli/src/agent/worktree.ts
|
|
70727
|
+
import { basename, join as join14 } from "path";
|
|
70728
|
+
import { homedir as homedir2 } from "os";
|
|
70729
|
+
import { exists } from "fs/promises";
|
|
70730
|
+
function worktreesDir(projectRoot) {
|
|
70731
|
+
return join14(homedir2(), ".ralph", basename(projectRoot), "worktrees");
|
|
70732
|
+
}
|
|
70733
|
+
function branchForChange(changeName) {
|
|
70734
|
+
return `ralph/${changeName}`;
|
|
70735
|
+
}
|
|
70736
|
+
async function createWorktree(projectRoot, changeName, runner) {
|
|
70737
|
+
const dir = worktreesDir(projectRoot);
|
|
70738
|
+
const cwd2 = join14(dir, changeName);
|
|
70739
|
+
const branch = branchForChange(changeName);
|
|
70740
|
+
const list = await runner.run(["worktree", "list", "--porcelain"], projectRoot);
|
|
70741
|
+
if (list.stdout.includes(`worktree ${cwd2}
|
|
70742
|
+
`)) {
|
|
70743
|
+
return { cwd: cwd2, branch };
|
|
70744
|
+
}
|
|
70745
|
+
let branchExists = true;
|
|
70746
|
+
try {
|
|
70747
|
+
await runner.run(["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], projectRoot);
|
|
70748
|
+
} catch {
|
|
70749
|
+
branchExists = false;
|
|
70750
|
+
}
|
|
70751
|
+
const cmd = branchExists ? ["worktree", "add", cwd2, branch] : ["worktree", "add", "-b", branch, cwd2];
|
|
70752
|
+
await runner.run(cmd, projectRoot);
|
|
70753
|
+
return { cwd: cwd2, branch };
|
|
70754
|
+
}
|
|
70755
|
+
async function removeWorktree(projectRoot, cwd2, runner) {
|
|
70756
|
+
await runner.run(["worktree", "remove", "--force", cwd2], projectRoot);
|
|
70757
|
+
}
|
|
70758
|
+
async function isWorktreeSafeToRemove(cwd2, base2, runner) {
|
|
70759
|
+
const status = await runner.run(["status", "--porcelain"], cwd2);
|
|
70760
|
+
const dirty = status.stdout.trim();
|
|
70761
|
+
let unpushedCommits = "";
|
|
70762
|
+
try {
|
|
70763
|
+
const log2 = await runner.run(["log", "--oneline", `${base2}..HEAD`, "--no-merges"], cwd2);
|
|
70764
|
+
unpushedCommits = log2.stdout.trim();
|
|
70765
|
+
} catch {
|
|
70766
|
+
unpushedCommits = "<unknown: failed to compare against base>";
|
|
70767
|
+
}
|
|
70768
|
+
if (dirty && unpushedCommits) {
|
|
70769
|
+
return {
|
|
70770
|
+
safe: false,
|
|
70771
|
+
reason: "uncommitted changes AND unpushed commits present",
|
|
70772
|
+
dirty,
|
|
70773
|
+
unpushedCommits
|
|
70774
|
+
};
|
|
70775
|
+
}
|
|
70776
|
+
if (dirty) {
|
|
70777
|
+
return {
|
|
70778
|
+
safe: false,
|
|
70779
|
+
reason: "uncommitted or untracked files present",
|
|
70780
|
+
dirty,
|
|
70781
|
+
unpushedCommits
|
|
70782
|
+
};
|
|
70783
|
+
}
|
|
70784
|
+
if (unpushedCommits) {
|
|
70785
|
+
return {
|
|
70786
|
+
safe: false,
|
|
70757
70787
|
reason: `commits ahead of ${base2} were not pushed/PR'd`,
|
|
70758
70788
|
dirty,
|
|
70759
70789
|
unpushedCommits
|
|
@@ -70761,6 +70791,32 @@ async function isWorktreeSafeToRemove(cwd2, base2, runner) {
|
|
|
70761
70791
|
}
|
|
70762
70792
|
return { safe: true, dirty, unpushedCommits };
|
|
70763
70793
|
}
|
|
70794
|
+
async function seedWorktreeMcpConfig(projectRoot, worktreeCwd) {
|
|
70795
|
+
const dst = join14(worktreeCwd, ".mcp.json");
|
|
70796
|
+
const src = join14(projectRoot, ".mcp.json");
|
|
70797
|
+
const source = await exists(dst) ? dst : await exists(src) ? src : null;
|
|
70798
|
+
if (!source)
|
|
70799
|
+
return;
|
|
70800
|
+
let parsed;
|
|
70801
|
+
try {
|
|
70802
|
+
parsed = await Bun.file(source).json();
|
|
70803
|
+
} catch {
|
|
70804
|
+
return;
|
|
70805
|
+
}
|
|
70806
|
+
const servers = parsed.mcpServers;
|
|
70807
|
+
if (servers && typeof servers === "object") {
|
|
70808
|
+
for (const cfg of Object.values(servers)) {
|
|
70809
|
+
if (Array.isArray(cfg.args)) {
|
|
70810
|
+
cfg.args = cfg.args.map((a) => typeof a === "string" && a.startsWith(".ralph/") ? join14(projectRoot, a) : a);
|
|
70811
|
+
}
|
|
70812
|
+
}
|
|
70813
|
+
}
|
|
70814
|
+
await Bun.write(dst, JSON.stringify(parsed, null, 2) + `
|
|
70815
|
+
`);
|
|
70816
|
+
}
|
|
70817
|
+
|
|
70818
|
+
// apps/cli/src/agent/post-task.ts
|
|
70819
|
+
import { join as join15 } from "path";
|
|
70764
70820
|
|
|
70765
70821
|
// apps/cli/src/agent/pr.ts
|
|
70766
70822
|
function defaultTitle(issue) {
|
|
@@ -70811,8 +70867,32 @@ async function createPullRequest(input, runner) {
|
|
|
70811
70867
|
|
|
70812
70868
|
// apps/cli/src/agent/ci.ts
|
|
70813
70869
|
var PR_CHECKS_FIELDS = "name,bucket,link,workflow,event";
|
|
70814
|
-
|
|
70815
|
-
|
|
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);
|
|
70816
70896
|
const checks = JSON.parse(out.stdout || "[]").filter((c) => c.bucket !== "skipping");
|
|
70817
70897
|
if (checks.some((c) => c.bucket === "pending")) {
|
|
70818
70898
|
return { bucket: "pending", failedRunIds: [] };
|
|
@@ -70849,17 +70929,28 @@ ${truncated}`);
|
|
|
70849
70929
|
}
|
|
70850
70930
|
async function fixCiUntilGreen(deps, opts) {
|
|
70851
70931
|
for (let attempt2 = 1;attempt2 <= opts.maxAttempts; attempt2++) {
|
|
70932
|
+
let pollN = 0;
|
|
70852
70933
|
while (true) {
|
|
70853
70934
|
if (deps.cancelled?.())
|
|
70854
70935
|
return { success: false, attempts: attempt2 - 1, reason: "cancelled" };
|
|
70855
|
-
|
|
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
|
+
}
|
|
70856
70945
|
if (s.bucket === "pass") {
|
|
70857
70946
|
deps.log(`\u2713 CI green for PR (after ${attempt2 - 1} fix attempts)`, "green");
|
|
70858
70947
|
return { success: true, attempts: attempt2 - 1 };
|
|
70859
70948
|
}
|
|
70860
70949
|
if (s.bucket === "fail") {
|
|
70861
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`);
|
|
70862
70952
|
const logs = await deps.getFailedLogs(s.failedRunIds);
|
|
70953
|
+
deps.onPhase?.("ci-fix", `attempt ${attempt2}/${opts.maxAttempts} \xB7 re-running worker`);
|
|
70863
70954
|
const steering = `CI is failing on this PR. Investigate and fix:
|
|
70864
70955
|
|
|
70865
70956
|
\`\`\`
|
|
@@ -70870,6 +70961,7 @@ ${logs}
|
|
|
70870
70961
|
deps.log(`! task loop exited code ${code} during CI fix attempt ${attempt2}`, "red");
|
|
70871
70962
|
}
|
|
70872
70963
|
try {
|
|
70964
|
+
deps.onPhase?.("ci-fix", `attempt ${attempt2}/${opts.maxAttempts} \xB7 pushing fix`);
|
|
70873
70965
|
await deps.pushBranch();
|
|
70874
70966
|
} catch (err) {
|
|
70875
70967
|
deps.log(`! push failed during CI fix: ${err.message}`, "red");
|
|
@@ -70877,39 +70969,297 @@ ${logs}
|
|
|
70877
70969
|
}
|
|
70878
70970
|
break;
|
|
70879
70971
|
}
|
|
70972
|
+
deps.onPhase?.("ci-poll", `attempt ${attempt2}/${opts.maxAttempts} \xB7 pending, waiting`);
|
|
70880
70973
|
await deps.sleep(opts.pollIntervalSeconds * 1000);
|
|
70881
70974
|
}
|
|
70882
70975
|
}
|
|
70883
70976
|
return { success: false, attempts: opts.maxAttempts, reason: "max-attempts" };
|
|
70884
70977
|
}
|
|
70885
70978
|
|
|
70886
|
-
// apps/cli/src/
|
|
70887
|
-
var
|
|
70888
|
-
|
|
70889
|
-
|
|
70890
|
-
|
|
70891
|
-
|
|
70892
|
-
const src = join14(projectRoot, ".mcp.json");
|
|
70893
|
-
const source = await exists(dst) ? dst : await exists(src) ? src : null;
|
|
70894
|
-
if (!source)
|
|
70979
|
+
// apps/cli/src/agent/post-task.ts
|
|
70980
|
+
var CI_FAILED_EXIT = 70;
|
|
70981
|
+
var PR_FAILED_EXIT = 71;
|
|
70982
|
+
async function reactivateState(stateFilePath, log2, changeName) {
|
|
70983
|
+
const file = Bun.file(stateFilePath);
|
|
70984
|
+
if (!await file.exists())
|
|
70895
70985
|
return;
|
|
70896
|
-
let parsed;
|
|
70897
70986
|
try {
|
|
70898
|
-
|
|
70899
|
-
|
|
70900
|
-
|
|
70987
|
+
const stateObj = JSON.parse(await file.text());
|
|
70988
|
+
if (stateObj.status !== "active") {
|
|
70989
|
+
stateObj.status = "active";
|
|
70990
|
+
stateObj.lastModified = new Date().toISOString();
|
|
70991
|
+
await Bun.write(stateFilePath, JSON.stringify(stateObj, null, 2) + `
|
|
70992
|
+
`);
|
|
70993
|
+
}
|
|
70994
|
+
} catch (err) {
|
|
70995
|
+
log2(`! could not reactivate state for ${changeName}: ${err.message}`, "yellow");
|
|
70901
70996
|
}
|
|
70902
|
-
|
|
70903
|
-
|
|
70904
|
-
|
|
70905
|
-
|
|
70906
|
-
|
|
70997
|
+
}
|
|
70998
|
+
async function runPostTask(input, deps) {
|
|
70999
|
+
const { log: log2, cmd, git, runScript } = deps;
|
|
71000
|
+
const emit = (phase, detail) => deps.onPhase?.(phase, detail);
|
|
71001
|
+
const {
|
|
71002
|
+
changeName,
|
|
71003
|
+
cwd: cwd2,
|
|
71004
|
+
projectRoot,
|
|
71005
|
+
changeDir,
|
|
71006
|
+
stateFilePath,
|
|
71007
|
+
branch,
|
|
71008
|
+
issue,
|
|
71009
|
+
exitCode,
|
|
71010
|
+
useWorktree,
|
|
71011
|
+
wantPr,
|
|
71012
|
+
wantFixCi,
|
|
71013
|
+
cfg,
|
|
71014
|
+
respawnWorker
|
|
71015
|
+
} = input;
|
|
71016
|
+
if (cfg.teardownScript) {
|
|
71017
|
+
emit("teardown", cfg.teardownScript);
|
|
71018
|
+
try {
|
|
71019
|
+
await runScript("teardown", cfg.teardownScript, cwd2);
|
|
71020
|
+
} catch {}
|
|
71021
|
+
}
|
|
71022
|
+
let effectiveCode = exitCode;
|
|
71023
|
+
const ok = exitCode === 0;
|
|
71024
|
+
if (ok && wantPr) {
|
|
71025
|
+
if (!branch || !issue) {
|
|
71026
|
+
log2(`! createPr requested but no worktree branch is tracked for ${changeName} (use --worktree)`, "yellow");
|
|
71027
|
+
effectiveCode = PR_FAILED_EXIT;
|
|
71028
|
+
} else {
|
|
71029
|
+
const maxHookFixAttempts = cfg.maxCiFixAttempts;
|
|
71030
|
+
const runWorkerWithFixTask = async (heading, failureOutput) => {
|
|
71031
|
+
try {
|
|
71032
|
+
await prependFixTask(join15(changeDir, "tasks.md"), heading, failureOutput);
|
|
71033
|
+
} catch (err) {
|
|
71034
|
+
log2(`! could not prepend fix task: ${err.message}`, "red");
|
|
71035
|
+
return 1;
|
|
71036
|
+
}
|
|
71037
|
+
await reactivateState(stateFilePath, log2, changeName);
|
|
71038
|
+
return respawnWorker();
|
|
71039
|
+
};
|
|
71040
|
+
let hookFixAttempt = 0;
|
|
71041
|
+
let commitGaveUp = false;
|
|
71042
|
+
while (true) {
|
|
71043
|
+
emit("committing", "git status");
|
|
71044
|
+
let dirty = "";
|
|
71045
|
+
try {
|
|
71046
|
+
const status = await cmd.run(["git", "status", "--porcelain"], cwd2);
|
|
71047
|
+
dirty = status.stdout.trim();
|
|
71048
|
+
} catch (err) {
|
|
71049
|
+
log2(`! git status failed for ${changeName}: ${err.message}`, "yellow");
|
|
71050
|
+
break;
|
|
71051
|
+
}
|
|
71052
|
+
if (!dirty)
|
|
71053
|
+
break;
|
|
71054
|
+
try {
|
|
71055
|
+
emit("committing", "git add -A");
|
|
71056
|
+
await cmd.run(["git", "add", "-A"], cwd2);
|
|
71057
|
+
emit("committing", "git commit");
|
|
71058
|
+
await cmd.run(["git", "commit", "-m", `chore(ralph): residual changes for ${changeName}`], cwd2);
|
|
71059
|
+
log2(` committed residual changes for ${changeName}`, "gray");
|
|
71060
|
+
break;
|
|
71061
|
+
} catch (err) {
|
|
71062
|
+
const e = err;
|
|
71063
|
+
const detail = e.stderr?.trim() || e.message;
|
|
71064
|
+
const combined = `${e.stdout ?? ""}
|
|
71065
|
+
${e.stderr ?? ""}`;
|
|
71066
|
+
if (/nothing to commit/i.test(combined))
|
|
71067
|
+
break;
|
|
71068
|
+
if (hookFixAttempt >= maxHookFixAttempts) {
|
|
71069
|
+
log2(`! commit rejected for ${changeName} after ${hookFixAttempt} hook-fix attempts (host pre-commit hook still failing) \u2014 worktree preserved at ${cwd2}`, "red");
|
|
71070
|
+
log2(` detail: ${detail}`, "red");
|
|
71071
|
+
effectiveCode = PR_FAILED_EXIT;
|
|
71072
|
+
commitGaveUp = true;
|
|
71073
|
+
break;
|
|
71074
|
+
}
|
|
71075
|
+
hookFixAttempt += 1;
|
|
71076
|
+
emit("commit-retry", `${hookFixAttempt}/${maxHookFixAttempts}`);
|
|
71077
|
+
log2(`! commit rejected for ${changeName} \u2014 prepending fix task and re-running loop (attempt ${hookFixAttempt}/${maxHookFixAttempts})`, "yellow");
|
|
71078
|
+
log2(` detail: ${detail}`, "yellow");
|
|
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.
|
|
71080
|
+
|
|
71081
|
+
` + combined.trim());
|
|
71082
|
+
if (retryCode !== 0) {
|
|
71083
|
+
log2(`! worker re-run after commit rejection exited code ${retryCode} \u2014 giving up`, "red");
|
|
71084
|
+
effectiveCode = PR_FAILED_EXIT;
|
|
71085
|
+
commitGaveUp = true;
|
|
71086
|
+
break;
|
|
71087
|
+
}
|
|
71088
|
+
}
|
|
71089
|
+
}
|
|
71090
|
+
let pr = null;
|
|
71091
|
+
let prGaveUp = commitGaveUp;
|
|
71092
|
+
let nonFfRebaseAttempted = false;
|
|
71093
|
+
while (!prGaveUp) {
|
|
71094
|
+
try {
|
|
71095
|
+
emit("pr-create", "git push + gh pr create");
|
|
71096
|
+
pr = await createPullRequest({ cwd: cwd2, branch, issue, base: cfg.prBaseBranch }, cmd);
|
|
71097
|
+
break;
|
|
71098
|
+
} catch (err) {
|
|
71099
|
+
const e = err;
|
|
71100
|
+
const detail = e.stderr?.trim() || e.message;
|
|
71101
|
+
const combined = `${e.stdout ?? ""}
|
|
71102
|
+
${e.stderr ?? ""}`;
|
|
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) {
|
|
71166
|
+
if (pushRejected) {
|
|
71167
|
+
log2(`! push rejected for ${changeName} after ${hookFixAttempt} hook-fix attempts (host pre-push hook still failing) \u2014 worktree preserved at ${cwd2}`, "red");
|
|
71168
|
+
log2(` detail: ${detail}`, "red");
|
|
71169
|
+
} else {
|
|
71170
|
+
log2(`! PR create failed for ${changeName}: ${detail}`, "red");
|
|
71171
|
+
}
|
|
71172
|
+
effectiveCode = PR_FAILED_EXIT;
|
|
71173
|
+
prGaveUp = true;
|
|
71174
|
+
break;
|
|
71175
|
+
}
|
|
71176
|
+
hookFixAttempt += 1;
|
|
71177
|
+
emit("push-retry", `${hookFixAttempt}/${maxHookFixAttempts}`);
|
|
71178
|
+
log2(`! push rejected for ${changeName} \u2014 prepending fix task and re-running loop (attempt ${hookFixAttempt}/${maxHookFixAttempts})`, "yellow");
|
|
71179
|
+
log2(` detail: ${detail}`, "yellow");
|
|
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.
|
|
71181
|
+
|
|
71182
|
+
` + combined.trim());
|
|
71183
|
+
if (retryCode !== 0) {
|
|
71184
|
+
log2(`! worker re-run after push rejection exited code ${retryCode} \u2014 giving up`, "red");
|
|
71185
|
+
effectiveCode = PR_FAILED_EXIT;
|
|
71186
|
+
prGaveUp = true;
|
|
71187
|
+
break;
|
|
71188
|
+
}
|
|
71189
|
+
}
|
|
71190
|
+
}
|
|
71191
|
+
if (prGaveUp) {} else if (!pr) {
|
|
71192
|
+
log2(` no commits ahead of ${cfg.prBaseBranch} \u2014 skipping PR`, "gray");
|
|
71193
|
+
} else {
|
|
71194
|
+
log2(` ${pr.created ? "opened" : "found existing"} PR: ${pr.url}`, "green");
|
|
71195
|
+
if (wantFixCi) {
|
|
71196
|
+
log2(` watching CI for ${pr.url} (max ${cfg.maxCiFixAttempts} fix attempts)`, "gray");
|
|
71197
|
+
emit("ci-poll", "starting");
|
|
71198
|
+
const result2 = await fixCiUntilGreen({
|
|
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")),
|
|
71201
|
+
getFailedLogs: (ids) => fetchFailedRunLogs(ids, cmd, cwd2),
|
|
71202
|
+
runTaskWithSteering: async (steering) => {
|
|
71203
|
+
try {
|
|
71204
|
+
await prependFixTask(join15(changeDir, "tasks.md"), "Fix failing CI checks", steering);
|
|
71205
|
+
} catch (err) {
|
|
71206
|
+
log2(`! could not prepend fix task: ${err.message}`, "red");
|
|
71207
|
+
}
|
|
71208
|
+
return respawnWorker();
|
|
71209
|
+
},
|
|
71210
|
+
pushBranch: async () => {
|
|
71211
|
+
await cmd.run(["git", "push", "origin", branch], cwd2);
|
|
71212
|
+
},
|
|
71213
|
+
log: log2,
|
|
71214
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms))
|
|
71215
|
+
}, {
|
|
71216
|
+
maxAttempts: cfg.maxCiFixAttempts,
|
|
71217
|
+
pollIntervalSeconds: cfg.ciPollIntervalSeconds
|
|
71218
|
+
});
|
|
71219
|
+
if (!result2.success) {
|
|
71220
|
+
log2(`! CI fix loop gave up after ${result2.attempts} attempts (${result2.reason ?? "unknown"}) \u2014 withholding done-status until CI passes`, "red");
|
|
71221
|
+
effectiveCode = CI_FAILED_EXIT;
|
|
71222
|
+
}
|
|
71223
|
+
}
|
|
70907
71224
|
}
|
|
70908
71225
|
}
|
|
70909
71226
|
}
|
|
70910
|
-
|
|
70911
|
-
|
|
71227
|
+
if (effectiveCode === 0)
|
|
71228
|
+
emit("done");
|
|
71229
|
+
else
|
|
71230
|
+
emit("gave-up", `exit ${effectiveCode}`);
|
|
71231
|
+
if (useWorktree && cwd2 !== projectRoot) {
|
|
71232
|
+
emit("cleanup", "checking worktree safety");
|
|
71233
|
+
if (effectiveCode === 0 && cfg.cleanupWorktreeOnSuccess) {
|
|
71234
|
+
const check = await isWorktreeSafeToRemove(cwd2, cfg.prBaseBranch, git).catch((err) => ({
|
|
71235
|
+
safe: false,
|
|
71236
|
+
reason: `safety check failed: ${err.message}`,
|
|
71237
|
+
dirty: "",
|
|
71238
|
+
unpushedCommits: ""
|
|
71239
|
+
}));
|
|
71240
|
+
if (!check.safe) {
|
|
71241
|
+
log2(`! preserving worktree for ${changeName}: ${check.reason}`, "yellow");
|
|
71242
|
+
if (check.dirty)
|
|
71243
|
+
log2(` uncommitted:
|
|
71244
|
+
${check.dirty}`, "yellow");
|
|
71245
|
+
if (check.unpushedCommits)
|
|
71246
|
+
log2(` commits:
|
|
71247
|
+
${check.unpushedCommits}`, "yellow");
|
|
71248
|
+
log2(` path: ${cwd2}`, "yellow");
|
|
71249
|
+
} else {
|
|
71250
|
+
try {
|
|
71251
|
+
await removeWorktree(projectRoot, cwd2, git);
|
|
71252
|
+
log2(` removed worktree ${cwd2}`, "gray");
|
|
71253
|
+
} catch (err) {
|
|
71254
|
+
log2(`! worktree remove failed for ${changeName}: ${err.message}`, "yellow");
|
|
71255
|
+
}
|
|
71256
|
+
}
|
|
71257
|
+
}
|
|
71258
|
+
}
|
|
71259
|
+
return effectiveCode;
|
|
70912
71260
|
}
|
|
71261
|
+
|
|
71262
|
+
// apps/cli/src/agent/wire.ts
|
|
70913
71263
|
var bunGitRunner = {
|
|
70914
71264
|
run: async (args, cwd2) => {
|
|
70915
71265
|
const proc = Bun.spawn({ cmd: ["git", ...args], cwd: cwd2, stdout: "pipe", stderr: "pipe" });
|
|
@@ -70943,35 +71293,346 @@ var bunCmdRunner = {
|
|
|
70943
71293
|
return { stdout, stderr };
|
|
70944
71294
|
}
|
|
70945
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
|
+
}
|
|
71312
|
+
function buildAgentCoordinator(input) {
|
|
71313
|
+
const {
|
|
71314
|
+
args,
|
|
71315
|
+
cfg,
|
|
71316
|
+
projectRoot,
|
|
71317
|
+
statesDir,
|
|
71318
|
+
tasksDir,
|
|
71319
|
+
apiKey,
|
|
71320
|
+
store,
|
|
71321
|
+
onLog,
|
|
71322
|
+
onWorkersChanged,
|
|
71323
|
+
onWorkerStarted,
|
|
71324
|
+
onWorkerExited,
|
|
71325
|
+
onWorkerPhase,
|
|
71326
|
+
onWorkerOutput,
|
|
71327
|
+
onWorkerCmd
|
|
71328
|
+
} = input;
|
|
71329
|
+
const logsDir = join16(projectRoot, ".ralph", "logs");
|
|
71330
|
+
const concurrency = args.concurrency || cfg.concurrency;
|
|
71331
|
+
const pollInterval = args.pollInterval || cfg.pollIntervalSeconds;
|
|
71332
|
+
const inProgressName = args.inProgressStatus || cfg.linear.inProgressStatus;
|
|
71333
|
+
const baseStatuses = args.linearStatus.length ? args.linearStatus : cfg.linear.statuses;
|
|
71334
|
+
const effectiveStatuses = inProgressName && baseStatuses.length > 0 && !baseStatuses.includes(inProgressName) ? [...baseStatuses, inProgressName] : baseStatuses;
|
|
71335
|
+
const filter2 = {
|
|
71336
|
+
team: args.linearTeam || cfg.linear.team,
|
|
71337
|
+
assignee: args.linearAssignee || cfg.linear.assignee,
|
|
71338
|
+
statuses: effectiveStatuses,
|
|
71339
|
+
labels: args.linearLabel.length ? args.linearLabel : cfg.linear.labels
|
|
71340
|
+
};
|
|
71341
|
+
const stateCache = new Map;
|
|
71342
|
+
const labelCache = new Map;
|
|
71343
|
+
const teamKeyOf = (issue) => issue.identifier.split("-")[0];
|
|
71344
|
+
const useWorktree = args.worktree || cfg.useWorktree;
|
|
71345
|
+
const cwdByChange = new Map;
|
|
71346
|
+
const statesDirByChange = new Map;
|
|
71347
|
+
const branchByChange = new Map;
|
|
71348
|
+
const issueByChange = new Map;
|
|
71349
|
+
async function runScript(label, cmd, cwd2) {
|
|
71350
|
+
onLog(` ${label}: ${cmd}`, "gray");
|
|
71351
|
+
const proc = Bun.spawn({
|
|
71352
|
+
cmd: ["sh", "-c", cmd],
|
|
71353
|
+
cwd: cwd2,
|
|
71354
|
+
stdout: "ignore",
|
|
71355
|
+
stderr: "pipe",
|
|
71356
|
+
stdin: "ignore"
|
|
71357
|
+
});
|
|
71358
|
+
const code = await proc.exited;
|
|
71359
|
+
if (code !== 0) {
|
|
71360
|
+
const stderr = await new Response(proc.stderr).text();
|
|
71361
|
+
onLog(`! ${label} exited code ${code}${stderr ? `: ${stderr.trim().split(`
|
|
71362
|
+
`)[0]}` : ""}`, "yellow");
|
|
71363
|
+
}
|
|
71364
|
+
}
|
|
71365
|
+
async function scaffoldCallback(issue) {
|
|
71366
|
+
let comments = [];
|
|
71367
|
+
try {
|
|
71368
|
+
comments = await fetchIssueComments(apiKey, issue.id);
|
|
71369
|
+
} catch (err) {
|
|
71370
|
+
onLog(`! Linear comment fetch failed for ${issue.identifier}: ${err.message}`, "yellow");
|
|
71371
|
+
}
|
|
71372
|
+
let workerCwd = projectRoot;
|
|
71373
|
+
let scaffoldTasksDir = tasksDir;
|
|
71374
|
+
let scaffoldStatesDir = statesDir;
|
|
71375
|
+
let workerBranch = null;
|
|
71376
|
+
const probeName = issue.identifier.toLowerCase();
|
|
71377
|
+
if (useWorktree) {
|
|
71378
|
+
try {
|
|
71379
|
+
const wt = await createWorktree(projectRoot, probeName, bunGitRunner);
|
|
71380
|
+
workerCwd = wt.cwd;
|
|
71381
|
+
workerBranch = wt.branch;
|
|
71382
|
+
const wtLayout = projectLayout(wt.cwd);
|
|
71383
|
+
scaffoldTasksDir = wtLayout.tasksDir;
|
|
71384
|
+
scaffoldStatesDir = wtLayout.statesDir;
|
|
71385
|
+
onLog(` ${issue.identifier} worktree: ${wt.cwd} (${wt.branch})`, "gray");
|
|
71386
|
+
try {
|
|
71387
|
+
await seedWorktreeMcpConfig(projectRoot, wt.cwd);
|
|
71388
|
+
} catch (err) {
|
|
71389
|
+
onLog(`! seeding .mcp.json failed for ${issue.identifier}: ${err.message}`, "yellow");
|
|
71390
|
+
}
|
|
71391
|
+
} catch (err) {
|
|
71392
|
+
onLog(`! worktree create failed for ${issue.identifier}: ${err.message} \u2014 falling back to project root`, "yellow");
|
|
71393
|
+
}
|
|
71394
|
+
}
|
|
71395
|
+
const appendPrompt = args.prompt || cfg.appendPrompt || "";
|
|
71396
|
+
const changeName = await scaffoldChangeForIssue(scaffoldTasksDir, scaffoldStatesDir, issue, comments, appendPrompt);
|
|
71397
|
+
cwdByChange.set(changeName, workerCwd);
|
|
71398
|
+
statesDirByChange.set(changeName, scaffoldStatesDir);
|
|
71399
|
+
issueByChange.set(changeName, issue);
|
|
71400
|
+
if (workerBranch)
|
|
71401
|
+
branchByChange.set(changeName, workerBranch);
|
|
71402
|
+
if (cfg.setupScript) {
|
|
71403
|
+
await runScript("setup", cfg.setupScript, workerCwd);
|
|
71404
|
+
}
|
|
71405
|
+
return changeName;
|
|
71406
|
+
}
|
|
71407
|
+
function buildTaskCmdFor(changeName) {
|
|
71408
|
+
const c = [
|
|
71409
|
+
process.execPath,
|
|
71410
|
+
process.argv[1] ?? "",
|
|
71411
|
+
"task",
|
|
71412
|
+
"--name",
|
|
71413
|
+
changeName,
|
|
71414
|
+
"--" + (args.engineSet ? args.engine : cfg.engine),
|
|
71415
|
+
args.engineSet ? args.model : cfg.model
|
|
71416
|
+
];
|
|
71417
|
+
const maxIter = args.maxIterations || cfg.maxIterationsPerTask;
|
|
71418
|
+
if (maxIter > 0)
|
|
71419
|
+
c.push("--max-iterations", String(maxIter));
|
|
71420
|
+
const maxCost = args.maxCostUsd || cfg.maxCostUsdPerTask;
|
|
71421
|
+
if (maxCost > 0)
|
|
71422
|
+
c.push("--max-cost", String(maxCost));
|
|
71423
|
+
const maxRuntime = args.maxRuntimeMinutes || cfg.maxRuntimeMinutesPerTask;
|
|
71424
|
+
if (maxRuntime > 0)
|
|
71425
|
+
c.push("--max-runtime", String(maxRuntime));
|
|
71426
|
+
const maxFailures = args.maxConsecutiveFailures !== 5 ? args.maxConsecutiveFailures : cfg.maxConsecutiveFailuresPerTask;
|
|
71427
|
+
if (maxFailures !== 5)
|
|
71428
|
+
c.push("--max-failures", String(maxFailures));
|
|
71429
|
+
const delay2 = args.delay || cfg.iterationDelaySeconds;
|
|
71430
|
+
if (delay2 > 0)
|
|
71431
|
+
c.push("--delay", String(delay2));
|
|
71432
|
+
if (args.log || cfg.logRawStream)
|
|
71433
|
+
c.push("--log");
|
|
71434
|
+
if (args.verbose || cfg.taskVerbose)
|
|
71435
|
+
c.push("--verbose");
|
|
71436
|
+
return c;
|
|
71437
|
+
}
|
|
71438
|
+
function spawnWorker(changeName) {
|
|
71439
|
+
const cwd2 = cwdByChange.get(changeName) ?? projectRoot;
|
|
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({
|
|
71494
|
+
cmd: buildTaskCmdFor(changeName),
|
|
71495
|
+
cwd: cwd2,
|
|
71496
|
+
stdout: "pipe",
|
|
71497
|
+
stderr: "pipe",
|
|
71498
|
+
stdin: "ignore"
|
|
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()}`);
|
|
71511
|
+
return rp.exited;
|
|
71512
|
+
};
|
|
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;
|
|
71517
|
+
const wantPr = args.createPr || cfg.createPrOnSuccess;
|
|
71518
|
+
const wantFixCi = args.fixCi || cfg.fixCiOnFailure;
|
|
71519
|
+
const wrapped = proc.exited.then(async (code) => {
|
|
71520
|
+
const workerLayout = projectLayout(cwd2);
|
|
71521
|
+
const effectiveCode = await runPostTask({
|
|
71522
|
+
changeName,
|
|
71523
|
+
cwd: cwd2,
|
|
71524
|
+
projectRoot,
|
|
71525
|
+
changeDir: workerLayout.changeDir(changeName),
|
|
71526
|
+
stateFilePath: workerLayout.stateFile(changeName),
|
|
71527
|
+
branch: branchByChange.get(changeName) ?? null,
|
|
71528
|
+
issue: issueByChange.get(changeName) ?? null,
|
|
71529
|
+
exitCode: code,
|
|
71530
|
+
useWorktree,
|
|
71531
|
+
wantPr,
|
|
71532
|
+
wantFixCi,
|
|
71533
|
+
cfg: {
|
|
71534
|
+
teardownScript: cfg.teardownScript ?? null,
|
|
71535
|
+
prBaseBranch: cfg.prBaseBranch,
|
|
71536
|
+
maxCiFixAttempts: cfg.maxCiFixAttempts,
|
|
71537
|
+
ciPollIntervalSeconds: cfg.ciPollIntervalSeconds,
|
|
71538
|
+
cleanupWorktreeOnSuccess: cfg.cleanupWorktreeOnSuccess
|
|
71539
|
+
},
|
|
71540
|
+
respawnWorker: respawn
|
|
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 {}
|
|
71554
|
+
cwdByChange.delete(changeName);
|
|
71555
|
+
statesDirByChange.delete(changeName);
|
|
71556
|
+
branchByChange.delete(changeName);
|
|
71557
|
+
issueByChange.delete(changeName);
|
|
71558
|
+
onWorkerExited(changeName);
|
|
71559
|
+
return effectiveCode;
|
|
71560
|
+
});
|
|
71561
|
+
return { exited: wrapped, kill: () => proc.kill() };
|
|
71562
|
+
}
|
|
71563
|
+
const coord = new AgentCoordinator({
|
|
71564
|
+
fetchIssues: (f2) => fetchOpenIssues(apiKey, f2),
|
|
71565
|
+
scaffold: scaffoldCallback,
|
|
71566
|
+
spawnWorker,
|
|
71567
|
+
store,
|
|
71568
|
+
onLog,
|
|
71569
|
+
onWorkersChanged,
|
|
71570
|
+
getIterationCount: async (changeName) => {
|
|
71571
|
+
const root = cwdByChange.get(changeName) ?? projectRoot;
|
|
71572
|
+
const file = Bun.file(projectLayout(root).stateFile(changeName));
|
|
71573
|
+
if (!await file.exists())
|
|
71574
|
+
return 0;
|
|
71575
|
+
const json = await file.json();
|
|
71576
|
+
return json.iteration ?? 0;
|
|
71577
|
+
},
|
|
71578
|
+
updater: {
|
|
71579
|
+
postComment: (issue, body) => addIssueComment(apiKey, issue.id, body),
|
|
71580
|
+
setState: (issue, stateId) => updateIssueState(apiKey, issue.id, stateId),
|
|
71581
|
+
resolveStateId: async (issue, stateName) => {
|
|
71582
|
+
const team = teamKeyOf(issue);
|
|
71583
|
+
let map2 = stateCache.get(team);
|
|
71584
|
+
if (!map2) {
|
|
71585
|
+
const states = await fetchWorkflowStates(apiKey, team);
|
|
71586
|
+
map2 = new Map(states.map((s) => [s.name.toLowerCase(), s.id]));
|
|
71587
|
+
stateCache.set(team, map2);
|
|
71588
|
+
}
|
|
71589
|
+
return map2.get(stateName.toLowerCase()) ?? null;
|
|
71590
|
+
},
|
|
71591
|
+
addLabel: (issue, labelId) => addLabelToIssue(apiKey, issue.id, labelId),
|
|
71592
|
+
resolveLabelId: async (issue, labelName) => {
|
|
71593
|
+
const team = teamKeyOf(issue);
|
|
71594
|
+
let map2 = labelCache.get(team);
|
|
71595
|
+
if (!map2) {
|
|
71596
|
+
const labels = await fetchIssueLabels(apiKey, team);
|
|
71597
|
+
map2 = new Map(labels.map((l) => [l.name.toLowerCase(), l.id]));
|
|
71598
|
+
labelCache.set(team, map2);
|
|
71599
|
+
}
|
|
71600
|
+
return map2.get(labelName.toLowerCase()) ?? null;
|
|
71601
|
+
}
|
|
71602
|
+
}
|
|
71603
|
+
}, {
|
|
71604
|
+
concurrency,
|
|
71605
|
+
filter: filter2,
|
|
71606
|
+
inProgressStatus: args.inProgressStatus || cfg.linear.inProgressStatus,
|
|
71607
|
+
doneStatus: args.doneStatus || cfg.linear.doneStatus,
|
|
71608
|
+
doneLabel: args.doneLabel || cfg.linear.doneLabel,
|
|
71609
|
+
postComments: cfg.linear.postComments,
|
|
71610
|
+
commentEveryIterations: cfg.linear.updateEveryIterations
|
|
71611
|
+
});
|
|
71612
|
+
const filterDesc = `team=${filter2.team ?? "*"}, assignee=${filter2.assignee ?? "*"}, statuses=` + `${filter2.statuses?.length ? filter2.statuses.join(",") : "open"}` + `${filter2.labels?.length ? `, labels=${filter2.labels.join(",")}` : ""}`;
|
|
71613
|
+
return {
|
|
71614
|
+
coord,
|
|
71615
|
+
filterDesc,
|
|
71616
|
+
concurrency,
|
|
71617
|
+
pollInterval,
|
|
71618
|
+
getWorkerCwd: (changeName) => cwdByChange.get(changeName)
|
|
71619
|
+
};
|
|
71620
|
+
}
|
|
71621
|
+
|
|
71622
|
+
// apps/cli/src/components/AgentMode.tsx
|
|
71623
|
+
var jsx_dev_runtime9 = __toESM(require_jsx_dev_runtime(), 1);
|
|
70946
71624
|
var lineCounter = 0;
|
|
70947
71625
|
function nextId() {
|
|
70948
71626
|
lineCounter += 1;
|
|
70949
71627
|
return `${Date.now()}-${lineCounter}`;
|
|
70950
71628
|
}
|
|
70951
|
-
var
|
|
70952
|
-
|
|
70953
|
-
|
|
70954
|
-
const
|
|
70955
|
-
|
|
70956
|
-
|
|
70957
|
-
${steering}
|
|
70958
|
-
`;
|
|
70959
|
-
const nextSteering = existing ? `${stamped}
|
|
70960
|
-
${existing.trimStart()}` : `${stamped}
|
|
70961
|
-
`;
|
|
70962
|
-
await Bun.write(join14(changeDir, "steering.md"), nextSteering);
|
|
70963
|
-
const tasksFile = Bun.file(join14(changeDir, "tasks.md"));
|
|
70964
|
-
const tasks = await tasksFile.exists() ? await tasksFile.text() : "";
|
|
70965
|
-
const taskSection = `
|
|
70966
|
-
## ${heading} (${new Date().toISOString()})
|
|
70967
|
-
|
|
70968
|
-
` + `- [ ] ${heading}. The error output is recorded in steering.md \u2014 read it first, ` + `then fix the underlying problem (do not just retry the failing command).
|
|
70969
|
-
`;
|
|
70970
|
-
const nextTasks = tasks.endsWith(`
|
|
70971
|
-
`) ? tasks + taskSection : tasks + `
|
|
70972
|
-
` + taskSection;
|
|
70973
|
-
await Bun.write(join14(changeDir, "tasks.md"), nextTasks);
|
|
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;
|
|
70974
71634
|
}
|
|
71635
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
70975
71636
|
function fmtElapsed(ms) {
|
|
70976
71637
|
const s = Math.floor(ms / 1000);
|
|
70977
71638
|
if (s < 60)
|
|
@@ -70991,7 +71652,6 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
70991
71652
|
const coordRef = import_react57.useRef(null);
|
|
70992
71653
|
const workerMetaRef = import_react57.useRef(new Map);
|
|
70993
71654
|
const nextPollAtRef = import_react57.useRef(0);
|
|
70994
|
-
const pollIntervalRef = import_react57.useRef(0);
|
|
70995
71655
|
const [pollStatus, setPollStatus] = import_react57.useState({ state: "idle", lastFound: null, lastAdded: null, lastAt: null, filterDesc: "" });
|
|
70996
71656
|
function appendLog(text, color) {
|
|
70997
71657
|
setLogs((prev) => [...prev, { id: nextId(), text, color }]);
|
|
@@ -71002,389 +71662,74 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
71002
71662
|
async function init2() {
|
|
71003
71663
|
const cfgPath = await ensureRalphyConfig(projectRoot);
|
|
71004
71664
|
const cfg = await loadRalphyConfig(projectRoot);
|
|
71005
|
-
appendLog(`agent mode \u2014 config: ${cfgPath}`, "gray");
|
|
71006
|
-
const concurrency = args.concurrency || cfg.concurrency;
|
|
71007
|
-
const pollInterval = args.pollInterval || cfg.pollIntervalSeconds;
|
|
71008
|
-
pollIntervalRef.current = pollInterval;
|
|
71009
|
-
appendLog(`concurrency=${concurrency} pollInterval=${pollInterval}s`, "gray");
|
|
71665
|
+
appendLog(`agent mode v${VERSION} \u2014 config: ${cfgPath}`, "gray");
|
|
71010
71666
|
const apiKey = process.env["LINEAR_API_KEY"];
|
|
71011
71667
|
if (!apiKey) {
|
|
71012
71668
|
appendLog("! LINEAR_API_KEY not set \u2014 cannot poll Linear", "red");
|
|
71013
71669
|
exit();
|
|
71014
71670
|
return;
|
|
71015
71671
|
}
|
|
71016
|
-
const
|
|
71017
|
-
|
|
71018
|
-
const
|
|
71019
|
-
|
|
71020
|
-
|
|
71021
|
-
|
|
71022
|
-
|
|
71023
|
-
|
|
71024
|
-
|
|
71025
|
-
|
|
71026
|
-
|
|
71027
|
-
|
|
71028
|
-
|
|
71029
|
-
const cwdByChange = new Map;
|
|
71030
|
-
const statesDirByChange = new Map;
|
|
71031
|
-
const branchByChange = new Map;
|
|
71032
|
-
const issueByChange = new Map;
|
|
71033
|
-
async function runScript(label, cmd, cwd2) {
|
|
71034
|
-
appendLog(` ${label}: ${cmd}`, "gray");
|
|
71035
|
-
const proc = Bun.spawn({
|
|
71036
|
-
cmd: ["sh", "-c", cmd],
|
|
71037
|
-
cwd: cwd2,
|
|
71038
|
-
stdout: "ignore",
|
|
71039
|
-
stderr: "pipe",
|
|
71040
|
-
stdin: "ignore"
|
|
71041
|
-
});
|
|
71042
|
-
const code = await proc.exited;
|
|
71043
|
-
if (code !== 0) {
|
|
71044
|
-
const stderr = await new Response(proc.stderr).text();
|
|
71045
|
-
appendLog(`! ${label} exited code ${code}${stderr ? `: ${stderr.trim().split(`
|
|
71046
|
-
`)[0]}` : ""}`, "yellow");
|
|
71047
|
-
}
|
|
71048
|
-
}
|
|
71049
|
-
const coord2 = new AgentCoordinator({
|
|
71050
|
-
fetchIssues: (f2) => fetchOpenIssues(apiKey, f2),
|
|
71051
|
-
scaffold: async (issue) => {
|
|
71052
|
-
let comments = [];
|
|
71053
|
-
try {
|
|
71054
|
-
comments = await fetchIssueComments(apiKey, issue.id);
|
|
71055
|
-
} catch (err) {
|
|
71056
|
-
appendLog(`! Linear comment fetch failed for ${issue.identifier}: ${err.message}`, "yellow");
|
|
71057
|
-
}
|
|
71058
|
-
let workerCwd = projectRoot;
|
|
71059
|
-
let scaffoldTasksDir = tasksDir;
|
|
71060
|
-
let scaffoldStatesDir = statesDir;
|
|
71061
|
-
let workerBranch = null;
|
|
71062
|
-
const probeName = issue.identifier.toLowerCase();
|
|
71063
|
-
if (useWorktree) {
|
|
71064
|
-
try {
|
|
71065
|
-
const wt = await createWorktree(projectRoot, probeName, bunGitRunner);
|
|
71066
|
-
workerCwd = wt.cwd;
|
|
71067
|
-
workerBranch = wt.branch;
|
|
71068
|
-
scaffoldTasksDir = join14(wt.cwd, "openspec", "changes");
|
|
71069
|
-
scaffoldStatesDir = join14(wt.cwd, ".ralph", "tasks");
|
|
71070
|
-
appendLog(` ${issue.identifier} worktree: ${wt.cwd} (${wt.branch})`, "gray");
|
|
71071
|
-
try {
|
|
71072
|
-
await seedWorktreeMcpConfig(projectRoot, wt.cwd);
|
|
71073
|
-
} catch (err) {
|
|
71074
|
-
appendLog(`! seeding .mcp.json failed for ${issue.identifier}: ${err.message}`, "yellow");
|
|
71075
|
-
}
|
|
71076
|
-
} catch (err) {
|
|
71077
|
-
appendLog(`! worktree create failed for ${issue.identifier}: ${err.message} \u2014 falling back to project root`, "yellow");
|
|
71078
|
-
}
|
|
71079
|
-
}
|
|
71080
|
-
const appendPrompt = args.prompt || cfg.appendPrompt || "";
|
|
71081
|
-
const changeName = await scaffoldChangeForIssue(scaffoldTasksDir, scaffoldStatesDir, issue, comments, appendPrompt);
|
|
71082
|
-
cwdByChange.set(changeName, workerCwd);
|
|
71083
|
-
statesDirByChange.set(changeName, scaffoldStatesDir);
|
|
71084
|
-
issueByChange.set(changeName, issue);
|
|
71085
|
-
if (workerBranch)
|
|
71086
|
-
branchByChange.set(changeName, workerBranch);
|
|
71087
|
-
if (cfg.setupScript) {
|
|
71088
|
-
await runScript("setup", cfg.setupScript, workerCwd);
|
|
71089
|
-
}
|
|
71090
|
-
return changeName;
|
|
71091
|
-
},
|
|
71092
|
-
spawnWorker: (changeName) => {
|
|
71093
|
-
const buildTaskCmd = () => {
|
|
71094
|
-
const c = [
|
|
71095
|
-
process.execPath,
|
|
71096
|
-
process.argv[1] ?? "",
|
|
71097
|
-
"task",
|
|
71098
|
-
"--name",
|
|
71099
|
-
changeName,
|
|
71100
|
-
"--" + (args.engineSet ? args.engine : cfg.engine),
|
|
71101
|
-
args.engineSet ? args.model : cfg.model
|
|
71102
|
-
];
|
|
71103
|
-
const maxIter = args.maxIterations || cfg.maxIterationsPerTask;
|
|
71104
|
-
if (maxIter > 0)
|
|
71105
|
-
c.push("--max-iterations", String(maxIter));
|
|
71106
|
-
const maxCost = args.maxCostUsd || cfg.maxCostUsdPerTask;
|
|
71107
|
-
if (maxCost > 0)
|
|
71108
|
-
c.push("--max-cost", String(maxCost));
|
|
71109
|
-
const maxRuntime = args.maxRuntimeMinutes || cfg.maxRuntimeMinutesPerTask;
|
|
71110
|
-
if (maxRuntime > 0)
|
|
71111
|
-
c.push("--max-runtime", String(maxRuntime));
|
|
71112
|
-
const maxFailures = args.maxConsecutiveFailures !== 5 ? args.maxConsecutiveFailures : cfg.maxConsecutiveFailuresPerTask;
|
|
71113
|
-
if (maxFailures !== 5)
|
|
71114
|
-
c.push("--max-failures", String(maxFailures));
|
|
71115
|
-
const delay2 = args.delay || cfg.iterationDelaySeconds;
|
|
71116
|
-
if (delay2 > 0)
|
|
71117
|
-
c.push("--delay", String(delay2));
|
|
71118
|
-
if (args.log || cfg.logRawStream)
|
|
71119
|
-
c.push("--log");
|
|
71120
|
-
if (args.verbose || cfg.taskVerbose)
|
|
71121
|
-
c.push("--verbose");
|
|
71122
|
-
return c;
|
|
71123
|
-
};
|
|
71124
|
-
const cwd2 = cwdByChange.get(changeName) ?? projectRoot;
|
|
71125
|
-
const proc = Bun.spawn({
|
|
71126
|
-
cmd: buildTaskCmd(),
|
|
71127
|
-
cwd: cwd2,
|
|
71128
|
-
stdout: "ignore",
|
|
71129
|
-
stderr: "ignore",
|
|
71130
|
-
stdin: "ignore"
|
|
71131
|
-
});
|
|
71672
|
+
const store = new AgentStateStore(projectRoot);
|
|
71673
|
+
await store.load();
|
|
71674
|
+
const { coord: coord2, filterDesc, concurrency, pollInterval } = buildAgentCoordinator({
|
|
71675
|
+
args,
|
|
71676
|
+
cfg,
|
|
71677
|
+
projectRoot,
|
|
71678
|
+
statesDir,
|
|
71679
|
+
tasksDir,
|
|
71680
|
+
apiKey,
|
|
71681
|
+
store,
|
|
71682
|
+
onLog: appendLog,
|
|
71683
|
+
onWorkersChanged: () => setTick((t) => t + 1),
|
|
71684
|
+
onWorkerStarted: (changeName, dir, logFile) => {
|
|
71132
71685
|
workerMetaRef.current.set(changeName, {
|
|
71133
71686
|
startedAt: Date.now(),
|
|
71134
|
-
statesDir:
|
|
71135
|
-
|
|
71687
|
+
statesDir: dir,
|
|
71688
|
+
logFile,
|
|
71689
|
+
iter: 0,
|
|
71690
|
+
phase: "working",
|
|
71691
|
+
phaseDetail: "",
|
|
71692
|
+
phaseStartedAt: Date.now(),
|
|
71693
|
+
currentCmd: null,
|
|
71694
|
+
lastCmd: null,
|
|
71695
|
+
tail: []
|
|
71136
71696
|
});
|
|
71137
|
-
const wantPr = args.createPr || cfg.createPrOnSuccess;
|
|
71138
|
-
const CI_FAILED_EXIT = 70;
|
|
71139
|
-
const PR_FAILED_EXIT = 71;
|
|
71140
|
-
const wrapped = proc.exited.then(async (code) => {
|
|
71141
|
-
if (cfg.teardownScript) {
|
|
71142
|
-
try {
|
|
71143
|
-
await runScript("teardown", cfg.teardownScript, cwd2);
|
|
71144
|
-
} catch {}
|
|
71145
|
-
}
|
|
71146
|
-
let effectiveCode = code;
|
|
71147
|
-
const ok = code === 0;
|
|
71148
|
-
if (ok && wantPr) {
|
|
71149
|
-
const branch = branchByChange.get(changeName);
|
|
71150
|
-
const prIssue = issueByChange.get(changeName);
|
|
71151
|
-
if (!branch || !prIssue) {
|
|
71152
|
-
appendLog(`! createPr requested but no worktree branch is tracked for ${changeName} (use --worktree)`, "yellow");
|
|
71153
|
-
effectiveCode = PR_FAILED_EXIT;
|
|
71154
|
-
} else {
|
|
71155
|
-
const changeDir = join14(statesDirByChange.get(changeName) ?? statesDir, "..", "..", "openspec", "changes", changeName);
|
|
71156
|
-
const maxHookFixAttempts = cfg.maxCiFixAttempts;
|
|
71157
|
-
const runWorkerWithFixSteering = async (heading, steering) => {
|
|
71158
|
-
try {
|
|
71159
|
-
await injectFixSteering(changeDir, heading, steering);
|
|
71160
|
-
} catch (steerErr) {
|
|
71161
|
-
appendLog(`! could not inject steering: ${steerErr.message}`, "red");
|
|
71162
|
-
return 1;
|
|
71163
|
-
}
|
|
71164
|
-
const rp = Bun.spawn({
|
|
71165
|
-
cmd: buildTaskCmd(),
|
|
71166
|
-
cwd: cwd2,
|
|
71167
|
-
stdout: "ignore",
|
|
71168
|
-
stderr: "ignore",
|
|
71169
|
-
stdin: "ignore"
|
|
71170
|
-
});
|
|
71171
|
-
return rp.exited;
|
|
71172
|
-
};
|
|
71173
|
-
let commitFixAttempt = 0;
|
|
71174
|
-
let commitGaveUp = false;
|
|
71175
|
-
while (true) {
|
|
71176
|
-
let dirty = "";
|
|
71177
|
-
try {
|
|
71178
|
-
const status = await bunCmdRunner.run(["git", "status", "--porcelain"], cwd2);
|
|
71179
|
-
dirty = status.stdout.trim();
|
|
71180
|
-
} catch (err) {
|
|
71181
|
-
appendLog(`! git status failed for ${changeName}: ${err.message}`, "yellow");
|
|
71182
|
-
break;
|
|
71183
|
-
}
|
|
71184
|
-
if (!dirty)
|
|
71185
|
-
break;
|
|
71186
|
-
try {
|
|
71187
|
-
await bunCmdRunner.run(["git", "add", "-A"], cwd2);
|
|
71188
|
-
await bunCmdRunner.run(["git", "commit", "-m", `ralph: residual changes for ${changeName}`], cwd2);
|
|
71189
|
-
appendLog(` committed residual changes for ${changeName}`, "gray");
|
|
71190
|
-
break;
|
|
71191
|
-
} catch (err) {
|
|
71192
|
-
const e = err;
|
|
71193
|
-
const detail = e.stderr?.trim() || e.message;
|
|
71194
|
-
const combined = `${e.stdout ?? ""}
|
|
71195
|
-
${e.stderr ?? ""}`;
|
|
71196
|
-
if (/nothing to commit/i.test(combined))
|
|
71197
|
-
break;
|
|
71198
|
-
if (commitFixAttempt >= maxHookFixAttempts) {
|
|
71199
|
-
appendLog(`! commit rejected for ${changeName} after ${commitFixAttempt} fix attempts (host pre-commit hook still failing) \u2014 worktree preserved at ${cwd2}`, "red");
|
|
71200
|
-
appendLog(` detail: ${detail}`, "red");
|
|
71201
|
-
effectiveCode = PR_FAILED_EXIT;
|
|
71202
|
-
commitGaveUp = true;
|
|
71203
|
-
break;
|
|
71204
|
-
}
|
|
71205
|
-
commitFixAttempt += 1;
|
|
71206
|
-
appendLog(`! commit rejected for ${changeName} \u2014 feeding error back to worker (attempt ${commitFixAttempt}/${maxHookFixAttempts})`, "yellow");
|
|
71207
|
-
appendLog(` detail: ${detail}`, "yellow");
|
|
71208
|
-
const retryCode = await runWorkerWithFixSteering("Fix host pre-commit hook rejection", `Committing residual changes was rejected by the host repo's pre-commit hook. ` + `Fix the underlying problem reported below, then the commit will be retried.
|
|
71209
|
-
|
|
71210
|
-
` + "```\n" + combined.trim() + "\n```");
|
|
71211
|
-
if (retryCode !== 0) {
|
|
71212
|
-
appendLog(`! worker re-run after commit rejection exited code ${retryCode} \u2014 giving up`, "red");
|
|
71213
|
-
effectiveCode = PR_FAILED_EXIT;
|
|
71214
|
-
commitGaveUp = true;
|
|
71215
|
-
break;
|
|
71216
|
-
}
|
|
71217
|
-
}
|
|
71218
|
-
}
|
|
71219
|
-
let pushFixAttempt = 0;
|
|
71220
|
-
let pr = null;
|
|
71221
|
-
let prGaveUp = commitGaveUp;
|
|
71222
|
-
while (!prGaveUp) {
|
|
71223
|
-
try {
|
|
71224
|
-
pr = await createPullRequest({ cwd: cwd2, branch, issue: prIssue, base: cfg.prBaseBranch }, bunCmdRunner);
|
|
71225
|
-
break;
|
|
71226
|
-
} catch (err) {
|
|
71227
|
-
const e = err;
|
|
71228
|
-
const detail = e.stderr?.trim() || e.message;
|
|
71229
|
-
const combined = `${e.stdout ?? ""}
|
|
71230
|
-
${e.stderr ?? ""}`;
|
|
71231
|
-
const pushRejected = /failed to push some refs|pre-push hook|hook declined/i.test(combined);
|
|
71232
|
-
if (!pushRejected || pushFixAttempt >= maxHookFixAttempts) {
|
|
71233
|
-
if (pushRejected) {
|
|
71234
|
-
appendLog(`! push rejected for ${changeName} after ${pushFixAttempt} fix attempts (host pre-push hook still failing) \u2014 worktree preserved at ${cwd2}`, "red");
|
|
71235
|
-
appendLog(` detail: ${detail}`, "red");
|
|
71236
|
-
} else {
|
|
71237
|
-
appendLog(`! PR create failed for ${changeName}: ${detail}`, "red");
|
|
71238
|
-
}
|
|
71239
|
-
effectiveCode = PR_FAILED_EXIT;
|
|
71240
|
-
prGaveUp = true;
|
|
71241
|
-
break;
|
|
71242
|
-
}
|
|
71243
|
-
pushFixAttempt += 1;
|
|
71244
|
-
appendLog(`! push rejected for ${changeName} \u2014 feeding error back to worker (attempt ${pushFixAttempt}/${maxHookFixAttempts})`, "yellow");
|
|
71245
|
-
appendLog(` detail: ${detail}`, "yellow");
|
|
71246
|
-
const retryCode = await runWorkerWithFixSteering("Fix host pre-push hook rejection", `Push to origin/${branch} was rejected by the host repo's pre-push hook. ` + `Fix the underlying problem reported below, then the push will be retried.
|
|
71247
|
-
|
|
71248
|
-
` + "```\n" + combined.trim() + "\n```");
|
|
71249
|
-
if (retryCode !== 0) {
|
|
71250
|
-
appendLog(`! worker re-run after push rejection exited code ${retryCode} \u2014 giving up`, "red");
|
|
71251
|
-
effectiveCode = PR_FAILED_EXIT;
|
|
71252
|
-
prGaveUp = true;
|
|
71253
|
-
break;
|
|
71254
|
-
}
|
|
71255
|
-
}
|
|
71256
|
-
}
|
|
71257
|
-
if (prGaveUp) {} else if (!pr) {
|
|
71258
|
-
appendLog(` no commits ahead of ${cfg.prBaseBranch} \u2014 skipping PR`, "gray");
|
|
71259
|
-
} else {
|
|
71260
|
-
appendLog(` ${pr.created ? "opened" : "found existing"} PR: ${pr.url}`, "green");
|
|
71261
|
-
const wantFixCi = args.fixCi || cfg.fixCiOnFailure;
|
|
71262
|
-
if (wantFixCi) {
|
|
71263
|
-
appendLog(` watching CI for ${pr.url} (max ${cfg.maxCiFixAttempts} fix attempts)`, "gray");
|
|
71264
|
-
const result2 = await fixCiUntilGreen({
|
|
71265
|
-
getStatus: () => getPrChecksStatus(pr.url, bunCmdRunner, cwd2),
|
|
71266
|
-
getFailedLogs: (ids) => fetchFailedRunLogs(ids, bunCmdRunner, cwd2),
|
|
71267
|
-
runTaskWithSteering: async (steering) => {
|
|
71268
|
-
try {
|
|
71269
|
-
await injectFixSteering(changeDir, "Fix failing CI checks", `CI feedback:
|
|
71270
|
-
|
|
71271
|
-
${steering}`);
|
|
71272
|
-
} catch (err) {
|
|
71273
|
-
appendLog(`! could not inject steering: ${err.message}`, "red");
|
|
71274
|
-
}
|
|
71275
|
-
const p = Bun.spawn({
|
|
71276
|
-
cmd: buildTaskCmd(),
|
|
71277
|
-
cwd: cwd2,
|
|
71278
|
-
stdout: "ignore",
|
|
71279
|
-
stderr: "ignore",
|
|
71280
|
-
stdin: "ignore"
|
|
71281
|
-
});
|
|
71282
|
-
return p.exited;
|
|
71283
|
-
},
|
|
71284
|
-
pushBranch: async () => {
|
|
71285
|
-
await bunCmdRunner.run(["git", "push", "origin", branch], cwd2);
|
|
71286
|
-
},
|
|
71287
|
-
log: appendLog,
|
|
71288
|
-
sleep: (ms) => new Promise((r) => setTimeout(r, ms))
|
|
71289
|
-
}, {
|
|
71290
|
-
maxAttempts: cfg.maxCiFixAttempts,
|
|
71291
|
-
pollIntervalSeconds: cfg.ciPollIntervalSeconds
|
|
71292
|
-
});
|
|
71293
|
-
if (!result2.success) {
|
|
71294
|
-
appendLog(`! CI fix loop gave up after ${result2.attempts} attempts (${result2.reason ?? "unknown"}) \u2014 withholding done-status until CI passes`, "red");
|
|
71295
|
-
effectiveCode = CI_FAILED_EXIT;
|
|
71296
|
-
}
|
|
71297
|
-
}
|
|
71298
|
-
}
|
|
71299
|
-
}
|
|
71300
|
-
}
|
|
71301
|
-
if (useWorktree && cwd2 !== projectRoot) {
|
|
71302
|
-
if (effectiveCode === 0 && cfg.cleanupWorktreeOnSuccess) {
|
|
71303
|
-
const check = await isWorktreeSafeToRemove(cwd2, cfg.prBaseBranch, bunGitRunner).catch((err) => ({
|
|
71304
|
-
safe: false,
|
|
71305
|
-
reason: `safety check failed: ${err.message}`,
|
|
71306
|
-
dirty: "",
|
|
71307
|
-
unpushedCommits: ""
|
|
71308
|
-
}));
|
|
71309
|
-
if (!check.safe) {
|
|
71310
|
-
appendLog(`! preserving worktree for ${changeName}: ${check.reason}`, "yellow");
|
|
71311
|
-
if (check.dirty) {
|
|
71312
|
-
appendLog(` uncommitted:
|
|
71313
|
-
${check.dirty}`, "yellow");
|
|
71314
|
-
}
|
|
71315
|
-
if (check.unpushedCommits) {
|
|
71316
|
-
appendLog(` commits:
|
|
71317
|
-
${check.unpushedCommits}`, "yellow");
|
|
71318
|
-
}
|
|
71319
|
-
appendLog(` path: ${cwd2}`, "yellow");
|
|
71320
|
-
} else {
|
|
71321
|
-
try {
|
|
71322
|
-
await removeWorktree(projectRoot, cwd2, bunGitRunner);
|
|
71323
|
-
appendLog(` removed worktree ${cwd2}`, "gray");
|
|
71324
|
-
} catch (err) {
|
|
71325
|
-
appendLog(`! worktree remove failed for ${changeName}: ${err.message}`, "yellow");
|
|
71326
|
-
}
|
|
71327
|
-
}
|
|
71328
|
-
}
|
|
71329
|
-
}
|
|
71330
|
-
cwdByChange.delete(changeName);
|
|
71331
|
-
statesDirByChange.delete(changeName);
|
|
71332
|
-
branchByChange.delete(changeName);
|
|
71333
|
-
issueByChange.delete(changeName);
|
|
71334
|
-
workerMetaRef.current.delete(changeName);
|
|
71335
|
-
return effectiveCode;
|
|
71336
|
-
});
|
|
71337
|
-
return { exited: wrapped, kill: () => proc.kill() };
|
|
71338
71697
|
},
|
|
71339
|
-
|
|
71340
|
-
|
|
71341
|
-
onLog: appendLog,
|
|
71342
|
-
onWorkersChanged: () => setTick((t) => t + 1),
|
|
71343
|
-
getIterationCount: async (changeName) => {
|
|
71344
|
-
const dir = statesDirByChange.get(changeName) ?? statesDir;
|
|
71345
|
-
const file = Bun.file(join14(dir, changeName, ".ralph-state.json"));
|
|
71346
|
-
if (!await file.exists())
|
|
71347
|
-
return 0;
|
|
71348
|
-
const json = await file.json();
|
|
71349
|
-
return json.iteration ?? 0;
|
|
71698
|
+
onWorkerExited: (changeName) => {
|
|
71699
|
+
workerMetaRef.current.delete(changeName);
|
|
71350
71700
|
},
|
|
71351
|
-
|
|
71352
|
-
|
|
71353
|
-
|
|
71354
|
-
|
|
71355
|
-
|
|
71356
|
-
|
|
71357
|
-
|
|
71358
|
-
|
|
71359
|
-
|
|
71360
|
-
|
|
71361
|
-
|
|
71362
|
-
|
|
71363
|
-
|
|
71364
|
-
|
|
71365
|
-
|
|
71366
|
-
|
|
71367
|
-
|
|
71368
|
-
|
|
71369
|
-
|
|
71370
|
-
|
|
71371
|
-
|
|
71372
|
-
|
|
71373
|
-
|
|
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 };
|
|
71374
71727
|
}
|
|
71375
71728
|
}
|
|
71376
|
-
}, {
|
|
71377
|
-
concurrency,
|
|
71378
|
-
filter: filter2,
|
|
71379
|
-
inProgressStatus: args.inProgressStatus || cfg.linear.inProgressStatus,
|
|
71380
|
-
doneStatus: args.doneStatus || cfg.linear.doneStatus,
|
|
71381
|
-
doneLabel: args.doneLabel || cfg.linear.doneLabel,
|
|
71382
|
-
postComments: cfg.linear.postComments,
|
|
71383
|
-
commentEveryIterations: cfg.linear.updateEveryIterations
|
|
71384
71729
|
});
|
|
71730
|
+
appendLog(`concurrency=${concurrency} pollInterval=${pollInterval}s`, "gray");
|
|
71385
71731
|
coordRef.current = coord2;
|
|
71386
71732
|
await coord2.init();
|
|
71387
|
-
const filterDesc = `team=${filter2.team ?? "*"}, assignee=${filter2.assignee ?? "*"}, statuses=${filter2.statuses?.length ? filter2.statuses.join(",") : "open"}${filter2.labels?.length ? `, labels=${filter2.labels.join(",")}` : ""}`;
|
|
71388
71733
|
const tick = async () => {
|
|
71389
71734
|
if (cancelled)
|
|
71390
71735
|
return;
|
|
@@ -71435,7 +71780,7 @@ ${check.unpushedCommits}`, "yellow");
|
|
|
71435
71780
|
(async () => {
|
|
71436
71781
|
for (const [changeName, meta] of workerMetaRef.current) {
|
|
71437
71782
|
try {
|
|
71438
|
-
const file = Bun.file(
|
|
71783
|
+
const file = Bun.file(join17(meta.statesDir, changeName, ".ralph-state.json"));
|
|
71439
71784
|
if (await file.exists()) {
|
|
71440
71785
|
const json = await file.json();
|
|
71441
71786
|
meta.iter = json.iteration ?? meta.iter;
|
|
@@ -71493,19 +71838,56 @@ ${check.unpushedCommits}`, "yellow");
|
|
|
71493
71838
|
const meta = workerMetaRef.current.get(w.changeName);
|
|
71494
71839
|
const elapsed = meta ? fmtElapsed(now2 - meta.startedAt) : "\u2013";
|
|
71495
71840
|
const iter = meta?.iter ?? 0;
|
|
71496
|
-
|
|
71497
|
-
|
|
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",
|
|
71498
71849
|
children: [
|
|
71499
|
-
|
|
71500
|
-
|
|
71501
|
-
|
|
71502
|
-
|
|
71503
|
-
|
|
71504
|
-
|
|
71505
|
-
|
|
71506
|
-
|
|
71507
|
-
|
|
71508
|
-
|
|
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))
|
|
71509
71891
|
]
|
|
71510
71892
|
}, w.changeName, true, undefined, this);
|
|
71511
71893
|
})
|
|
@@ -71516,11 +71898,11 @@ ${check.unpushedCommits}`, "yellow");
|
|
|
71516
71898
|
}
|
|
71517
71899
|
|
|
71518
71900
|
// packages/openspec/src/openspec-change-store.ts
|
|
71519
|
-
import { join as
|
|
71901
|
+
import { join as join18, dirname as dirname4 } from "path";
|
|
71520
71902
|
import { readdir, mkdir as mkdir3 } from "fs/promises";
|
|
71521
71903
|
function resolveOpenspecBin() {
|
|
71522
71904
|
const pkgJsonPath = Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir);
|
|
71523
|
-
return
|
|
71905
|
+
return join18(dirname4(pkgJsonPath), "bin", "openspec.js");
|
|
71524
71906
|
}
|
|
71525
71907
|
function runOpenspec(args, options = {}) {
|
|
71526
71908
|
const stdio = options.inherit ? ["inherit", "inherit", "inherit"] : ["ignore", "pipe", "pipe"];
|
|
@@ -71546,7 +71928,7 @@ class OpenSpecChangeStore {
|
|
|
71546
71928
|
}
|
|
71547
71929
|
}
|
|
71548
71930
|
getChangeDirectory(name) {
|
|
71549
|
-
return
|
|
71931
|
+
return join18("openspec", "changes", name);
|
|
71550
71932
|
}
|
|
71551
71933
|
async listChanges() {
|
|
71552
71934
|
const result2 = runOpenspec(["list", "--json"]);
|
|
@@ -71560,7 +71942,7 @@ class OpenSpecChangeStore {
|
|
|
71560
71942
|
}
|
|
71561
71943
|
} catch {}
|
|
71562
71944
|
}
|
|
71563
|
-
const changesDir =
|
|
71945
|
+
const changesDir = join18("openspec", "changes");
|
|
71564
71946
|
if (!await Bun.file(changesDir).exists())
|
|
71565
71947
|
return [];
|
|
71566
71948
|
try {
|
|
@@ -71571,18 +71953,18 @@ class OpenSpecChangeStore {
|
|
|
71571
71953
|
}
|
|
71572
71954
|
}
|
|
71573
71955
|
async readTaskList(name) {
|
|
71574
|
-
const file = Bun.file(
|
|
71956
|
+
const file = Bun.file(join18("openspec", "changes", name, "tasks.md"));
|
|
71575
71957
|
if (!await file.exists())
|
|
71576
71958
|
return "";
|
|
71577
71959
|
return await file.text();
|
|
71578
71960
|
}
|
|
71579
71961
|
async writeTaskList(name, content) {
|
|
71580
|
-
const path =
|
|
71962
|
+
const path = join18("openspec", "changes", name, "tasks.md");
|
|
71581
71963
|
await mkdir3(dirname4(path), { recursive: true });
|
|
71582
71964
|
await Bun.write(path, content);
|
|
71583
71965
|
}
|
|
71584
71966
|
async appendSteering(name, message) {
|
|
71585
|
-
const path =
|
|
71967
|
+
const path = join18("openspec", "changes", name, "steering.md");
|
|
71586
71968
|
const file = Bun.file(path);
|
|
71587
71969
|
const existing = await file.exists() ? await file.text() : null;
|
|
71588
71970
|
const updated = existing ? `${message}
|
|
@@ -71593,7 +71975,7 @@ ${existing.trimStart()}` : `${message}
|
|
|
71593
71975
|
await Bun.write(path, updated);
|
|
71594
71976
|
}
|
|
71595
71977
|
async readSection(name, artifact, heading) {
|
|
71596
|
-
const file = Bun.file(
|
|
71978
|
+
const file = Bun.file(join18("openspec", "changes", name, artifact));
|
|
71597
71979
|
if (!await file.exists())
|
|
71598
71980
|
return "";
|
|
71599
71981
|
const content = await file.text();
|
|
@@ -71675,8 +72057,8 @@ function App2({ args, statesDir, tasksDir, projectRoot }) {
|
|
|
71675
72057
|
message: "Error: --name is required for status mode"
|
|
71676
72058
|
}, undefined, false, undefined, this);
|
|
71677
72059
|
}
|
|
71678
|
-
const stateDir =
|
|
71679
|
-
if (getStorage().read(
|
|
72060
|
+
const stateDir = join19(statesDir, args.name);
|
|
72061
|
+
if (getStorage().read(join19(stateDir, ".ralph-state.json")) === null) {
|
|
71680
72062
|
return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(ErrorMessage, {
|
|
71681
72063
|
message: `Error: change '${args.name}' not found`
|
|
71682
72064
|
}, undefined, false, undefined, this);
|
|
@@ -71737,7 +72119,7 @@ if (typeof globalThis.Bun === "undefined") {
|
|
|
71737
72119
|
async function findProjectRoot() {
|
|
71738
72120
|
let dir = process.cwd();
|
|
71739
72121
|
while (dir !== "/") {
|
|
71740
|
-
if (await exists2(
|
|
72122
|
+
if (await exists2(join20(dir, "openspec")))
|
|
71741
72123
|
return dir;
|
|
71742
72124
|
dir = resolve(dir, "..");
|
|
71743
72125
|
}
|
|
@@ -71772,11 +72154,12 @@ try {
|
|
|
71772
72154
|
capture("command_run", { mode: args.mode, engine: args.engine, model: args.model });
|
|
71773
72155
|
try {
|
|
71774
72156
|
const projectRoot = await findProjectRoot();
|
|
71775
|
-
const
|
|
71776
|
-
const
|
|
72157
|
+
const layout = projectLayout(projectRoot);
|
|
72158
|
+
const statesDir = layout.statesDir;
|
|
72159
|
+
const tasksDir = layout.tasksDir;
|
|
71777
72160
|
if (args.mode === "init") {
|
|
71778
72161
|
await mkdir4(statesDir, { recursive: true });
|
|
71779
|
-
const openspecBin =
|
|
72162
|
+
const openspecBin = join20(dirname5(Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir)), "bin", "openspec.js");
|
|
71780
72163
|
Bun.spawnSync({
|
|
71781
72164
|
cmd: [process.execPath, openspecBin, "init", "--tools", "none", "--force"],
|
|
71782
72165
|
stdio: ["inherit", "inherit", "inherit"],
|
|
@@ -71789,9 +72172,9 @@ try {
|
|
|
71789
72172
|
`);
|
|
71790
72173
|
process.exit(1);
|
|
71791
72174
|
}
|
|
71792
|
-
const worktreeDir =
|
|
71793
|
-
const changeDir =
|
|
71794
|
-
const stateDir =
|
|
72175
|
+
const worktreeDir = join20(worktreesDir(projectRoot), args.name);
|
|
72176
|
+
const changeDir = join20(tasksDir, args.name);
|
|
72177
|
+
const stateDir = join20(statesDir, args.name);
|
|
71795
72178
|
const branch = `ralph/${args.name}`;
|
|
71796
72179
|
const removed = [];
|
|
71797
72180
|
if (await exists2(worktreeDir)) {
|
|
@@ -71830,12 +72213,11 @@ try {
|
|
|
71830
72213
|
removed.push(`task state ${stateDir}`);
|
|
71831
72214
|
}
|
|
71832
72215
|
try {
|
|
71833
|
-
const
|
|
71834
|
-
|
|
71835
|
-
|
|
71836
|
-
|
|
71837
|
-
|
|
71838
|
-
removed.push(`agent-state entry for ${entry.identifier} (${entry.issueId})`);
|
|
72216
|
+
const store = new AgentStateStore(projectRoot);
|
|
72217
|
+
await store.load();
|
|
72218
|
+
const removedEntry = await store.removeByChangeName(args.name);
|
|
72219
|
+
if (removedEntry) {
|
|
72220
|
+
removed.push(`agent-state entry for ${removedEntry.identifier} (${removedEntry.issueId})`);
|
|
71839
72221
|
}
|
|
71840
72222
|
} catch {}
|
|
71841
72223
|
if (removed.length === 0) {
|
|
@@ -71852,13 +72234,13 @@ try {
|
|
|
71852
72234
|
process.exit(0);
|
|
71853
72235
|
}
|
|
71854
72236
|
if (args.mode === "task" && args.name) {
|
|
71855
|
-
await mkdir4(
|
|
71856
|
-
await mkdir4(
|
|
72237
|
+
await mkdir4(join20(statesDir, args.name), { recursive: true });
|
|
72238
|
+
await mkdir4(join20(tasksDir, args.name), { recursive: true });
|
|
71857
72239
|
}
|
|
71858
72240
|
if (args.mode === "agent") {
|
|
71859
72241
|
await mkdir4(statesDir, { recursive: true });
|
|
71860
72242
|
await mkdir4(tasksDir, { recursive: true });
|
|
71861
|
-
await mkdir4(
|
|
72243
|
+
await mkdir4(join20(projectRoot, ".ralph"), { recursive: true });
|
|
71862
72244
|
}
|
|
71863
72245
|
await runWithContext(createDefaultContext(), async () => {
|
|
71864
72246
|
const { waitUntilExit } = render_default(import_react59.createElement(App2, { args, statesDir, tasksDir, projectRoot }));
|