@neriros/ralphy 2.11.1 → 2.11.2
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 +876 -780
- 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 join19, 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.11.
|
|
56410
|
+
version: "2.11.2",
|
|
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 join18 } 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,167 @@ 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 join16 } 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
|
+
// packages/core/src/layout.ts
|
|
70253
|
+
import { join as join12 } from "path";
|
|
70254
|
+
var STATE_FILE2 = ".ralph-state.json";
|
|
70255
|
+
function projectLayout(root) {
|
|
70256
|
+
const statesDir = join12(root, ".ralph", "tasks");
|
|
70257
|
+
const tasksDir = join12(root, "openspec", "changes");
|
|
70258
|
+
return {
|
|
70259
|
+
root,
|
|
70260
|
+
statesDir,
|
|
70261
|
+
tasksDir,
|
|
70262
|
+
agentStateFile: join12(root, ".ralph", "agent-state.json"),
|
|
70263
|
+
changeDir: (name) => join12(tasksDir, name),
|
|
70264
|
+
taskStateDir: (name) => join12(statesDir, name),
|
|
70265
|
+
stateFile: (name) => join12(statesDir, name, STATE_FILE2)
|
|
70266
|
+
};
|
|
70267
|
+
}
|
|
70072
70268
|
|
|
70073
70269
|
// apps/cli/src/agent/linear.ts
|
|
70074
70270
|
var OPEN_STATE_TYPES = ["unstarted", "started", "backlog"];
|
|
@@ -70211,300 +70407,78 @@ async function addLabelToIssue(apiKey, issueId, labelId) {
|
|
|
70211
70407
|
});
|
|
70212
70408
|
}
|
|
70213
70409
|
|
|
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
|
-
|
|
70410
|
+
// apps/cli/src/agent/coordinator.ts
|
|
70411
|
+
class AgentCoordinator {
|
|
70412
|
+
deps;
|
|
70413
|
+
opts;
|
|
70414
|
+
workers = [];
|
|
70415
|
+
pendingIds = new Set;
|
|
70416
|
+
queue = [];
|
|
70417
|
+
stopped = false;
|
|
70418
|
+
constructor(deps, opts) {
|
|
70419
|
+
this.deps = deps;
|
|
70420
|
+
this.opts = opts;
|
|
70421
|
+
}
|
|
70422
|
+
get activeCount() {
|
|
70423
|
+
return this.workers.length;
|
|
70424
|
+
}
|
|
70425
|
+
get queuedCount() {
|
|
70426
|
+
return this.queue.length;
|
|
70427
|
+
}
|
|
70428
|
+
get activeWorkers() {
|
|
70429
|
+
return this.workers;
|
|
70430
|
+
}
|
|
70431
|
+
async init() {}
|
|
70432
|
+
async pollOnce() {
|
|
70433
|
+
if (this.stopped)
|
|
70434
|
+
return { found: 0, added: 0 };
|
|
70435
|
+
let issues;
|
|
70436
|
+
try {
|
|
70437
|
+
issues = await this.deps.fetchIssues(this.opts.filter);
|
|
70438
|
+
} catch (err) {
|
|
70439
|
+
this.deps.onLog(`! Linear poll failed: ${err.message}`, "red");
|
|
70440
|
+
return { found: 0, added: 0 };
|
|
70441
|
+
}
|
|
70442
|
+
const state = this.deps.store.snapshot();
|
|
70443
|
+
const tasksByIssueId = new Map;
|
|
70444
|
+
for (const entry of Object.values(state.tasks)) {
|
|
70445
|
+
tasksByIssueId.set(entry.issueId, entry);
|
|
70446
|
+
}
|
|
70447
|
+
const isProcessed = (id) => tasksByIssueId.get(id)?.state === "processed";
|
|
70448
|
+
const isFailed = (id) => tasksByIssueId.get(id)?.state === "failed";
|
|
70449
|
+
const queued = new Set(this.queue.map((i) => i.id));
|
|
70450
|
+
const active = new Set(this.workers.map((w) => w.issueId));
|
|
70451
|
+
let added = 0;
|
|
70452
|
+
for (const issue of issues) {
|
|
70453
|
+
if (isProcessed(issue.id))
|
|
70252
70454
|
continue;
|
|
70253
|
-
|
|
70254
|
-
|
|
70255
|
-
|
|
70256
|
-
|
|
70257
|
-
|
|
70258
|
-
|
|
70455
|
+
if (isFailed(issue.id))
|
|
70456
|
+
continue;
|
|
70457
|
+
if (queued.has(issue.id))
|
|
70458
|
+
continue;
|
|
70459
|
+
if (active.has(issue.id))
|
|
70460
|
+
continue;
|
|
70461
|
+
if (this.pendingIds.has(issue.id))
|
|
70462
|
+
continue;
|
|
70463
|
+
const blocker = issue.blockedByIds.find((bid) => !isProcessed(bid));
|
|
70464
|
+
if (blocker !== undefined) {
|
|
70465
|
+
this.deps.onLog(` \u23F8 ${issue.identifier} skipped \u2014 blocked by unresolved dependency`, "yellow");
|
|
70466
|
+
continue;
|
|
70467
|
+
}
|
|
70468
|
+
this.queue.push(issue);
|
|
70469
|
+
added += 1;
|
|
70259
70470
|
}
|
|
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 };
|
|
70471
|
+
if (added > 0) {
|
|
70472
|
+
this.queue.sort((a, b) => {
|
|
70473
|
+
const pa = a.priority === 0 ? Infinity : a.priority;
|
|
70474
|
+
const pb = b.priority === 0 ? Infinity : b.priority;
|
|
70475
|
+
return pa - pb;
|
|
70476
|
+
});
|
|
70477
|
+
}
|
|
70478
|
+
await this.deps.store.setLastPollAt(new Date().toISOString());
|
|
70479
|
+
this.spawnNext();
|
|
70480
|
+
await this.reportProgress();
|
|
70481
|
+
return { found: issues.length, added };
|
|
70508
70482
|
}
|
|
70509
70483
|
async reportProgress() {
|
|
70510
70484
|
const updater = this.deps.updater;
|
|
@@ -70535,7 +70509,7 @@ class AgentCoordinator {
|
|
|
70535
70509
|
}
|
|
70536
70510
|
}
|
|
70537
70511
|
spawnNext() {
|
|
70538
|
-
if (this.stopped
|
|
70512
|
+
if (this.stopped)
|
|
70539
70513
|
return;
|
|
70540
70514
|
while (this.workers.length + this.pendingIds.size < this.opts.concurrency && this.queue.length > 0) {
|
|
70541
70515
|
const issue = this.queue.shift();
|
|
@@ -70543,18 +70517,6 @@ class AgentCoordinator {
|
|
|
70543
70517
|
this.launchWorker(issue);
|
|
70544
70518
|
}
|
|
70545
70519
|
}
|
|
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
70520
|
async launchWorker(issue) {
|
|
70559
70521
|
let changeName;
|
|
70560
70522
|
try {
|
|
@@ -70569,13 +70531,13 @@ class AgentCoordinator {
|
|
|
70569
70531
|
this.pendingIds.delete(issue.id);
|
|
70570
70532
|
return;
|
|
70571
70533
|
}
|
|
70572
|
-
|
|
70573
|
-
this.
|
|
70534
|
+
{
|
|
70535
|
+
const existing = this.deps.store.snapshot().tasks[issue.identifier];
|
|
70536
|
+
this.deps.store.upsertTask(issue, {
|
|
70574
70537
|
state: "started",
|
|
70575
70538
|
changeName,
|
|
70576
|
-
startedAt:
|
|
70539
|
+
startedAt: existing?.startedAt ?? new Date().toISOString()
|
|
70577
70540
|
});
|
|
70578
|
-
this.deps.saveState(this.state);
|
|
70579
70541
|
}
|
|
70580
70542
|
this.deps.onLog(`\u25B6 ${issue.identifier} \u2192 ${changeName} (worker started)`, "cyan");
|
|
70581
70543
|
const handle = this.deps.spawnWorker(changeName, issue);
|
|
@@ -70597,14 +70559,11 @@ class AgentCoordinator {
|
|
|
70597
70559
|
this.workers.splice(idx, 1);
|
|
70598
70560
|
const ok = code === 0;
|
|
70599
70561
|
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
|
-
}
|
|
70562
|
+
this.deps.store.upsertTask(issue, {
|
|
70563
|
+
state: ok ? "processed" : "failed",
|
|
70564
|
+
finishedAt: new Date().toISOString(),
|
|
70565
|
+
exitCode: code
|
|
70566
|
+
});
|
|
70608
70567
|
this.notifyExited(issue, changeName, code);
|
|
70609
70568
|
this.deps.onWorkersChanged();
|
|
70610
70569
|
this.spawnNext();
|
|
@@ -70614,14 +70573,11 @@ class AgentCoordinator {
|
|
|
70614
70573
|
const updater = this.deps.updater;
|
|
70615
70574
|
if (!updater)
|
|
70616
70575
|
return;
|
|
70617
|
-
const alreadyCommented = this.
|
|
70576
|
+
const alreadyCommented = this.deps.store.snapshot().tasks[issue.identifier]?.commentPosted === true;
|
|
70618
70577
|
if (this.opts.postComments !== false && !alreadyCommented) {
|
|
70619
70578
|
try {
|
|
70620
70579
|
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
|
-
}
|
|
70580
|
+
await this.deps.store.upsertTask(issue, { commentPosted: true });
|
|
70625
70581
|
} catch (err) {
|
|
70626
70582
|
this.deps.onLog(`! Linear comment failed for ${issue.identifier}: ${err.message}`, "red");
|
|
70627
70583
|
}
|
|
@@ -70694,18 +70650,89 @@ class AgentCoordinator {
|
|
|
70694
70650
|
}
|
|
70695
70651
|
}
|
|
70696
70652
|
|
|
70653
|
+
// apps/cli/src/agent/scaffold.ts
|
|
70654
|
+
import { join as join13 } from "path";
|
|
70655
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
70656
|
+
function changeNameForIssue(issue) {
|
|
70657
|
+
const slug = issue.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
70658
|
+
return slug ? `${issue.identifier.toLowerCase()}-${slug}` : issue.identifier.toLowerCase();
|
|
70659
|
+
}
|
|
70660
|
+
async function scaffoldChangeForIssue(tasksDir, statesDir, issue, comments = [], appendPrompt = "") {
|
|
70661
|
+
const name = changeNameForIssue(issue);
|
|
70662
|
+
const changeDir = join13(tasksDir, name);
|
|
70663
|
+
const stateDir = join13(statesDir, name);
|
|
70664
|
+
await mkdir2(changeDir, { recursive: true });
|
|
70665
|
+
await mkdir2(join13(changeDir, "specs"), { recursive: true });
|
|
70666
|
+
await mkdir2(stateDir, { recursive: true });
|
|
70667
|
+
const commentsBlock = comments.length > 0 ? [
|
|
70668
|
+
"",
|
|
70669
|
+
"## Linear comments",
|
|
70670
|
+
"",
|
|
70671
|
+
...comments.flatMap((c) => [
|
|
70672
|
+
`**${c.user?.name ?? "unknown"}** \u2014 ${c.createdAt}`,
|
|
70673
|
+
"",
|
|
70674
|
+
c.body.trim(),
|
|
70675
|
+
""
|
|
70676
|
+
])
|
|
70677
|
+
] : [];
|
|
70678
|
+
const proposal = [
|
|
70679
|
+
`# ${issue.identifier}: ${issue.title}`,
|
|
70680
|
+
"",
|
|
70681
|
+
`Source: [${issue.identifier}](${issue.url})`,
|
|
70682
|
+
`Status: ${issue.state.name}`,
|
|
70683
|
+
issue.assignee ? `Assignee: ${issue.assignee.name}` : "",
|
|
70684
|
+
issue.labels.length ? `Labels: ${issue.labels.join(", ")}` : "",
|
|
70685
|
+
"",
|
|
70686
|
+
"## Description",
|
|
70687
|
+
"",
|
|
70688
|
+
issue.description?.trim() || "_No description provided in Linear._",
|
|
70689
|
+
...commentsBlock,
|
|
70690
|
+
...appendPrompt.trim() ? ["", "## Additional instructions", "", appendPrompt.trim()] : [],
|
|
70691
|
+
"",
|
|
70692
|
+
"## Steering",
|
|
70693
|
+
"",
|
|
70694
|
+
"_Add steering notes here as the loop runs._",
|
|
70695
|
+
""
|
|
70696
|
+
].filter((l) => l !== "").join(`
|
|
70697
|
+
`);
|
|
70698
|
+
const tasks = [
|
|
70699
|
+
`# Tasks for ${issue.identifier}`,
|
|
70700
|
+
"",
|
|
70701
|
+
"## Subtasks",
|
|
70702
|
+
"",
|
|
70703
|
+
`- [ ] Read the Linear issue at ${issue.url} and break it into concrete subtasks`,
|
|
70704
|
+
`- [ ] Implement the changes described in proposal.md`,
|
|
70705
|
+
`- [ ] Add or update tests covering the new behavior`,
|
|
70706
|
+
`- [ ] Run \`bun run lint\` and \`bun run test\` and fix any failures`,
|
|
70707
|
+
""
|
|
70708
|
+
].join(`
|
|
70709
|
+
`);
|
|
70710
|
+
const design = [
|
|
70711
|
+
`# Design for ${issue.identifier}`,
|
|
70712
|
+
"",
|
|
70713
|
+
"_Fill in the technical design as you work through the issue._",
|
|
70714
|
+
""
|
|
70715
|
+
].join(`
|
|
70716
|
+
`);
|
|
70717
|
+
await Bun.write(join13(changeDir, "proposal.md"), proposal);
|
|
70718
|
+
await Bun.write(join13(changeDir, "tasks.md"), tasks);
|
|
70719
|
+
await Bun.write(join13(changeDir, "design.md"), design);
|
|
70720
|
+
return name;
|
|
70721
|
+
}
|
|
70722
|
+
|
|
70697
70723
|
// apps/cli/src/agent/worktree.ts
|
|
70698
|
-
import { basename, join as
|
|
70724
|
+
import { basename, join as join14 } from "path";
|
|
70699
70725
|
import { homedir as homedir2 } from "os";
|
|
70726
|
+
import { exists } from "fs/promises";
|
|
70700
70727
|
function worktreesDir(projectRoot) {
|
|
70701
|
-
return
|
|
70728
|
+
return join14(homedir2(), ".ralph", basename(projectRoot), "worktrees");
|
|
70702
70729
|
}
|
|
70703
70730
|
function branchForChange(changeName) {
|
|
70704
70731
|
return `ralph/${changeName}`;
|
|
70705
70732
|
}
|
|
70706
70733
|
async function createWorktree(projectRoot, changeName, runner) {
|
|
70707
70734
|
const dir = worktreesDir(projectRoot);
|
|
70708
|
-
const cwd2 =
|
|
70735
|
+
const cwd2 = join14(dir, changeName);
|
|
70709
70736
|
const branch = branchForChange(changeName);
|
|
70710
70737
|
const list = await runner.run(["worktree", "list", "--porcelain"], projectRoot);
|
|
70711
70738
|
if (list.stdout.includes(`worktree ${cwd2}
|
|
@@ -70761,6 +70788,32 @@ async function isWorktreeSafeToRemove(cwd2, base2, runner) {
|
|
|
70761
70788
|
}
|
|
70762
70789
|
return { safe: true, dirty, unpushedCommits };
|
|
70763
70790
|
}
|
|
70791
|
+
async function seedWorktreeMcpConfig(projectRoot, worktreeCwd) {
|
|
70792
|
+
const dst = join14(worktreeCwd, ".mcp.json");
|
|
70793
|
+
const src = join14(projectRoot, ".mcp.json");
|
|
70794
|
+
const source = await exists(dst) ? dst : await exists(src) ? src : null;
|
|
70795
|
+
if (!source)
|
|
70796
|
+
return;
|
|
70797
|
+
let parsed;
|
|
70798
|
+
try {
|
|
70799
|
+
parsed = await Bun.file(source).json();
|
|
70800
|
+
} catch {
|
|
70801
|
+
return;
|
|
70802
|
+
}
|
|
70803
|
+
const servers = parsed.mcpServers;
|
|
70804
|
+
if (servers && typeof servers === "object") {
|
|
70805
|
+
for (const cfg of Object.values(servers)) {
|
|
70806
|
+
if (Array.isArray(cfg.args)) {
|
|
70807
|
+
cfg.args = cfg.args.map((a) => typeof a === "string" && a.startsWith(".ralph/") ? join14(projectRoot, a) : a);
|
|
70808
|
+
}
|
|
70809
|
+
}
|
|
70810
|
+
}
|
|
70811
|
+
await Bun.write(dst, JSON.stringify(parsed, null, 2) + `
|
|
70812
|
+
`);
|
|
70813
|
+
}
|
|
70814
|
+
|
|
70815
|
+
// apps/cli/src/agent/post-task.ts
|
|
70816
|
+
import { join as join15 } from "path";
|
|
70764
70817
|
|
|
70765
70818
|
// apps/cli/src/agent/pr.ts
|
|
70766
70819
|
function defaultTitle(issue) {
|
|
@@ -70883,33 +70936,213 @@ ${logs}
|
|
|
70883
70936
|
return { success: false, attempts: opts.maxAttempts, reason: "max-attempts" };
|
|
70884
70937
|
}
|
|
70885
70938
|
|
|
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)
|
|
70939
|
+
// apps/cli/src/agent/post-task.ts
|
|
70940
|
+
var CI_FAILED_EXIT = 70;
|
|
70941
|
+
var PR_FAILED_EXIT = 71;
|
|
70942
|
+
async function reactivateState(stateFilePath, log2, changeName) {
|
|
70943
|
+
const file = Bun.file(stateFilePath);
|
|
70944
|
+
if (!await file.exists())
|
|
70895
70945
|
return;
|
|
70896
|
-
let parsed;
|
|
70897
70946
|
try {
|
|
70898
|
-
|
|
70899
|
-
|
|
70900
|
-
|
|
70947
|
+
const stateObj = JSON.parse(await file.text());
|
|
70948
|
+
if (stateObj.status !== "active") {
|
|
70949
|
+
stateObj.status = "active";
|
|
70950
|
+
stateObj.lastModified = new Date().toISOString();
|
|
70951
|
+
await Bun.write(stateFilePath, JSON.stringify(stateObj, null, 2) + `
|
|
70952
|
+
`);
|
|
70953
|
+
}
|
|
70954
|
+
} catch (err) {
|
|
70955
|
+
log2(`! could not reactivate state for ${changeName}: ${err.message}`, "yellow");
|
|
70901
70956
|
}
|
|
70902
|
-
|
|
70903
|
-
|
|
70904
|
-
|
|
70905
|
-
|
|
70906
|
-
|
|
70957
|
+
}
|
|
70958
|
+
async function runPostTask(input, deps) {
|
|
70959
|
+
const { log: log2, cmd, git, runScript } = deps;
|
|
70960
|
+
const {
|
|
70961
|
+
changeName,
|
|
70962
|
+
cwd: cwd2,
|
|
70963
|
+
projectRoot,
|
|
70964
|
+
changeDir,
|
|
70965
|
+
stateFilePath,
|
|
70966
|
+
branch,
|
|
70967
|
+
issue,
|
|
70968
|
+
exitCode,
|
|
70969
|
+
useWorktree,
|
|
70970
|
+
wantPr,
|
|
70971
|
+
wantFixCi,
|
|
70972
|
+
cfg,
|
|
70973
|
+
respawnWorker
|
|
70974
|
+
} = input;
|
|
70975
|
+
if (cfg.teardownScript) {
|
|
70976
|
+
try {
|
|
70977
|
+
await runScript("teardown", cfg.teardownScript, cwd2);
|
|
70978
|
+
} catch {}
|
|
70979
|
+
}
|
|
70980
|
+
let effectiveCode = exitCode;
|
|
70981
|
+
const ok = exitCode === 0;
|
|
70982
|
+
if (ok && wantPr) {
|
|
70983
|
+
if (!branch || !issue) {
|
|
70984
|
+
log2(`! createPr requested but no worktree branch is tracked for ${changeName} (use --worktree)`, "yellow");
|
|
70985
|
+
effectiveCode = PR_FAILED_EXIT;
|
|
70986
|
+
} else {
|
|
70987
|
+
const maxHookFixAttempts = cfg.maxCiFixAttempts;
|
|
70988
|
+
const runWorkerWithFixTask = async (heading, failureOutput) => {
|
|
70989
|
+
try {
|
|
70990
|
+
await prependFixTask(join15(changeDir, "tasks.md"), heading, failureOutput);
|
|
70991
|
+
} catch (err) {
|
|
70992
|
+
log2(`! could not prepend fix task: ${err.message}`, "red");
|
|
70993
|
+
return 1;
|
|
70994
|
+
}
|
|
70995
|
+
await reactivateState(stateFilePath, log2, changeName);
|
|
70996
|
+
return respawnWorker();
|
|
70997
|
+
};
|
|
70998
|
+
let hookFixAttempt = 0;
|
|
70999
|
+
let commitGaveUp = false;
|
|
71000
|
+
while (true) {
|
|
71001
|
+
let dirty = "";
|
|
71002
|
+
try {
|
|
71003
|
+
const status = await cmd.run(["git", "status", "--porcelain"], cwd2);
|
|
71004
|
+
dirty = status.stdout.trim();
|
|
71005
|
+
} catch (err) {
|
|
71006
|
+
log2(`! git status failed for ${changeName}: ${err.message}`, "yellow");
|
|
71007
|
+
break;
|
|
71008
|
+
}
|
|
71009
|
+
if (!dirty)
|
|
71010
|
+
break;
|
|
71011
|
+
try {
|
|
71012
|
+
await cmd.run(["git", "add", "-A"], cwd2);
|
|
71013
|
+
await cmd.run(["git", "commit", "-m", `chore(ralph): residual changes for ${changeName}`], cwd2);
|
|
71014
|
+
log2(` committed residual changes for ${changeName}`, "gray");
|
|
71015
|
+
break;
|
|
71016
|
+
} catch (err) {
|
|
71017
|
+
const e = err;
|
|
71018
|
+
const detail = e.stderr?.trim() || e.message;
|
|
71019
|
+
const combined = `${e.stdout ?? ""}
|
|
71020
|
+
${e.stderr ?? ""}`;
|
|
71021
|
+
if (/nothing to commit/i.test(combined))
|
|
71022
|
+
break;
|
|
71023
|
+
if (hookFixAttempt >= maxHookFixAttempts) {
|
|
71024
|
+
log2(`! commit rejected for ${changeName} after ${hookFixAttempt} hook-fix attempts (host pre-commit hook still failing) \u2014 worktree preserved at ${cwd2}`, "red");
|
|
71025
|
+
log2(` detail: ${detail}`, "red");
|
|
71026
|
+
effectiveCode = PR_FAILED_EXIT;
|
|
71027
|
+
commitGaveUp = true;
|
|
71028
|
+
break;
|
|
71029
|
+
}
|
|
71030
|
+
hookFixAttempt += 1;
|
|
71031
|
+
log2(`! commit rejected for ${changeName} \u2014 prepending fix task and re-running loop (attempt ${hookFixAttempt}/${maxHookFixAttempts})`, "yellow");
|
|
71032
|
+
log2(` detail: ${detail}`, "yellow");
|
|
71033
|
+
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.
|
|
71034
|
+
|
|
71035
|
+
` + combined.trim());
|
|
71036
|
+
if (retryCode !== 0) {
|
|
71037
|
+
log2(`! worker re-run after commit rejection exited code ${retryCode} \u2014 giving up`, "red");
|
|
71038
|
+
effectiveCode = PR_FAILED_EXIT;
|
|
71039
|
+
commitGaveUp = true;
|
|
71040
|
+
break;
|
|
71041
|
+
}
|
|
71042
|
+
}
|
|
71043
|
+
}
|
|
71044
|
+
let pr = null;
|
|
71045
|
+
let prGaveUp = commitGaveUp;
|
|
71046
|
+
while (!prGaveUp) {
|
|
71047
|
+
try {
|
|
71048
|
+
pr = await createPullRequest({ cwd: cwd2, branch, issue, base: cfg.prBaseBranch }, cmd);
|
|
71049
|
+
break;
|
|
71050
|
+
} catch (err) {
|
|
71051
|
+
const e = err;
|
|
71052
|
+
const detail = e.stderr?.trim() || e.message;
|
|
71053
|
+
const combined = `${e.stdout ?? ""}
|
|
71054
|
+
${e.stderr ?? ""}`;
|
|
71055
|
+
const pushRejected = /failed to push some refs|pre-push hook|hook declined/i.test(combined);
|
|
71056
|
+
if (!pushRejected || hookFixAttempt >= maxHookFixAttempts) {
|
|
71057
|
+
if (pushRejected) {
|
|
71058
|
+
log2(`! push rejected for ${changeName} after ${hookFixAttempt} hook-fix attempts (host pre-push hook still failing) \u2014 worktree preserved at ${cwd2}`, "red");
|
|
71059
|
+
log2(` detail: ${detail}`, "red");
|
|
71060
|
+
} else {
|
|
71061
|
+
log2(`! PR create failed for ${changeName}: ${detail}`, "red");
|
|
71062
|
+
}
|
|
71063
|
+
effectiveCode = PR_FAILED_EXIT;
|
|
71064
|
+
prGaveUp = true;
|
|
71065
|
+
break;
|
|
71066
|
+
}
|
|
71067
|
+
hookFixAttempt += 1;
|
|
71068
|
+
log2(`! push rejected for ${changeName} \u2014 prepending fix task and re-running loop (attempt ${hookFixAttempt}/${maxHookFixAttempts})`, "yellow");
|
|
71069
|
+
log2(` detail: ${detail}`, "yellow");
|
|
71070
|
+
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.
|
|
71071
|
+
|
|
71072
|
+
` + combined.trim());
|
|
71073
|
+
if (retryCode !== 0) {
|
|
71074
|
+
log2(`! worker re-run after push rejection exited code ${retryCode} \u2014 giving up`, "red");
|
|
71075
|
+
effectiveCode = PR_FAILED_EXIT;
|
|
71076
|
+
prGaveUp = true;
|
|
71077
|
+
break;
|
|
71078
|
+
}
|
|
71079
|
+
}
|
|
71080
|
+
}
|
|
71081
|
+
if (prGaveUp) {} else if (!pr) {
|
|
71082
|
+
log2(` no commits ahead of ${cfg.prBaseBranch} \u2014 skipping PR`, "gray");
|
|
71083
|
+
} else {
|
|
71084
|
+
log2(` ${pr.created ? "opened" : "found existing"} PR: ${pr.url}`, "green");
|
|
71085
|
+
if (wantFixCi) {
|
|
71086
|
+
log2(` watching CI for ${pr.url} (max ${cfg.maxCiFixAttempts} fix attempts)`, "gray");
|
|
71087
|
+
const result2 = await fixCiUntilGreen({
|
|
71088
|
+
getStatus: () => getPrChecksStatus(pr.url, cmd, cwd2),
|
|
71089
|
+
getFailedLogs: (ids) => fetchFailedRunLogs(ids, cmd, cwd2),
|
|
71090
|
+
runTaskWithSteering: async (steering) => {
|
|
71091
|
+
try {
|
|
71092
|
+
await prependFixTask(join15(changeDir, "tasks.md"), "Fix failing CI checks", steering);
|
|
71093
|
+
} catch (err) {
|
|
71094
|
+
log2(`! could not prepend fix task: ${err.message}`, "red");
|
|
71095
|
+
}
|
|
71096
|
+
return respawnWorker();
|
|
71097
|
+
},
|
|
71098
|
+
pushBranch: async () => {
|
|
71099
|
+
await cmd.run(["git", "push", "origin", branch], cwd2);
|
|
71100
|
+
},
|
|
71101
|
+
log: log2,
|
|
71102
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms))
|
|
71103
|
+
}, {
|
|
71104
|
+
maxAttempts: cfg.maxCiFixAttempts,
|
|
71105
|
+
pollIntervalSeconds: cfg.ciPollIntervalSeconds
|
|
71106
|
+
});
|
|
71107
|
+
if (!result2.success) {
|
|
71108
|
+
log2(`! CI fix loop gave up after ${result2.attempts} attempts (${result2.reason ?? "unknown"}) \u2014 withholding done-status until CI passes`, "red");
|
|
71109
|
+
effectiveCode = CI_FAILED_EXIT;
|
|
71110
|
+
}
|
|
71111
|
+
}
|
|
70907
71112
|
}
|
|
70908
71113
|
}
|
|
70909
71114
|
}
|
|
70910
|
-
|
|
70911
|
-
|
|
71115
|
+
if (useWorktree && cwd2 !== projectRoot) {
|
|
71116
|
+
if (effectiveCode === 0 && cfg.cleanupWorktreeOnSuccess) {
|
|
71117
|
+
const check = await isWorktreeSafeToRemove(cwd2, cfg.prBaseBranch, git).catch((err) => ({
|
|
71118
|
+
safe: false,
|
|
71119
|
+
reason: `safety check failed: ${err.message}`,
|
|
71120
|
+
dirty: "",
|
|
71121
|
+
unpushedCommits: ""
|
|
71122
|
+
}));
|
|
71123
|
+
if (!check.safe) {
|
|
71124
|
+
log2(`! preserving worktree for ${changeName}: ${check.reason}`, "yellow");
|
|
71125
|
+
if (check.dirty)
|
|
71126
|
+
log2(` uncommitted:
|
|
71127
|
+
${check.dirty}`, "yellow");
|
|
71128
|
+
if (check.unpushedCommits)
|
|
71129
|
+
log2(` commits:
|
|
71130
|
+
${check.unpushedCommits}`, "yellow");
|
|
71131
|
+
log2(` path: ${cwd2}`, "yellow");
|
|
71132
|
+
} else {
|
|
71133
|
+
try {
|
|
71134
|
+
await removeWorktree(projectRoot, cwd2, git);
|
|
71135
|
+
log2(` removed worktree ${cwd2}`, "gray");
|
|
71136
|
+
} catch (err) {
|
|
71137
|
+
log2(`! worktree remove failed for ${changeName}: ${err.message}`, "yellow");
|
|
71138
|
+
}
|
|
71139
|
+
}
|
|
71140
|
+
}
|
|
71141
|
+
}
|
|
71142
|
+
return effectiveCode;
|
|
70912
71143
|
}
|
|
71144
|
+
|
|
71145
|
+
// apps/cli/src/agent/wire.ts
|
|
70913
71146
|
var bunGitRunner = {
|
|
70914
71147
|
run: async (args, cwd2) => {
|
|
70915
71148
|
const proc = Bun.spawn({ cmd: ["git", ...args], cwd: cwd2, stdout: "pipe", stderr: "pipe" });
|
|
@@ -70943,35 +71176,249 @@ var bunCmdRunner = {
|
|
|
70943
71176
|
return { stdout, stderr };
|
|
70944
71177
|
}
|
|
70945
71178
|
};
|
|
71179
|
+
function buildAgentCoordinator(input) {
|
|
71180
|
+
const {
|
|
71181
|
+
args,
|
|
71182
|
+
cfg,
|
|
71183
|
+
projectRoot,
|
|
71184
|
+
statesDir,
|
|
71185
|
+
tasksDir,
|
|
71186
|
+
apiKey,
|
|
71187
|
+
store,
|
|
71188
|
+
onLog,
|
|
71189
|
+
onWorkersChanged,
|
|
71190
|
+
onWorkerStarted,
|
|
71191
|
+
onWorkerExited
|
|
71192
|
+
} = input;
|
|
71193
|
+
const concurrency = args.concurrency || cfg.concurrency;
|
|
71194
|
+
const pollInterval = args.pollInterval || cfg.pollIntervalSeconds;
|
|
71195
|
+
const inProgressName = args.inProgressStatus || cfg.linear.inProgressStatus;
|
|
71196
|
+
const baseStatuses = args.linearStatus.length ? args.linearStatus : cfg.linear.statuses;
|
|
71197
|
+
const effectiveStatuses = inProgressName && baseStatuses.length > 0 && !baseStatuses.includes(inProgressName) ? [...baseStatuses, inProgressName] : baseStatuses;
|
|
71198
|
+
const filter2 = {
|
|
71199
|
+
team: args.linearTeam || cfg.linear.team,
|
|
71200
|
+
assignee: args.linearAssignee || cfg.linear.assignee,
|
|
71201
|
+
statuses: effectiveStatuses,
|
|
71202
|
+
labels: args.linearLabel.length ? args.linearLabel : cfg.linear.labels
|
|
71203
|
+
};
|
|
71204
|
+
const stateCache = new Map;
|
|
71205
|
+
const labelCache = new Map;
|
|
71206
|
+
const teamKeyOf = (issue) => issue.identifier.split("-")[0];
|
|
71207
|
+
const useWorktree = args.worktree || cfg.useWorktree;
|
|
71208
|
+
const cwdByChange = new Map;
|
|
71209
|
+
const statesDirByChange = new Map;
|
|
71210
|
+
const branchByChange = new Map;
|
|
71211
|
+
const issueByChange = new Map;
|
|
71212
|
+
async function runScript(label, cmd, cwd2) {
|
|
71213
|
+
onLog(` ${label}: ${cmd}`, "gray");
|
|
71214
|
+
const proc = Bun.spawn({
|
|
71215
|
+
cmd: ["sh", "-c", cmd],
|
|
71216
|
+
cwd: cwd2,
|
|
71217
|
+
stdout: "ignore",
|
|
71218
|
+
stderr: "pipe",
|
|
71219
|
+
stdin: "ignore"
|
|
71220
|
+
});
|
|
71221
|
+
const code = await proc.exited;
|
|
71222
|
+
if (code !== 0) {
|
|
71223
|
+
const stderr = await new Response(proc.stderr).text();
|
|
71224
|
+
onLog(`! ${label} exited code ${code}${stderr ? `: ${stderr.trim().split(`
|
|
71225
|
+
`)[0]}` : ""}`, "yellow");
|
|
71226
|
+
}
|
|
71227
|
+
}
|
|
71228
|
+
async function scaffoldCallback(issue) {
|
|
71229
|
+
let comments = [];
|
|
71230
|
+
try {
|
|
71231
|
+
comments = await fetchIssueComments(apiKey, issue.id);
|
|
71232
|
+
} catch (err) {
|
|
71233
|
+
onLog(`! Linear comment fetch failed for ${issue.identifier}: ${err.message}`, "yellow");
|
|
71234
|
+
}
|
|
71235
|
+
let workerCwd = projectRoot;
|
|
71236
|
+
let scaffoldTasksDir = tasksDir;
|
|
71237
|
+
let scaffoldStatesDir = statesDir;
|
|
71238
|
+
let workerBranch = null;
|
|
71239
|
+
const probeName = issue.identifier.toLowerCase();
|
|
71240
|
+
if (useWorktree) {
|
|
71241
|
+
try {
|
|
71242
|
+
const wt = await createWorktree(projectRoot, probeName, bunGitRunner);
|
|
71243
|
+
workerCwd = wt.cwd;
|
|
71244
|
+
workerBranch = wt.branch;
|
|
71245
|
+
const wtLayout = projectLayout(wt.cwd);
|
|
71246
|
+
scaffoldTasksDir = wtLayout.tasksDir;
|
|
71247
|
+
scaffoldStatesDir = wtLayout.statesDir;
|
|
71248
|
+
onLog(` ${issue.identifier} worktree: ${wt.cwd} (${wt.branch})`, "gray");
|
|
71249
|
+
try {
|
|
71250
|
+
await seedWorktreeMcpConfig(projectRoot, wt.cwd);
|
|
71251
|
+
} catch (err) {
|
|
71252
|
+
onLog(`! seeding .mcp.json failed for ${issue.identifier}: ${err.message}`, "yellow");
|
|
71253
|
+
}
|
|
71254
|
+
} catch (err) {
|
|
71255
|
+
onLog(`! worktree create failed for ${issue.identifier}: ${err.message} \u2014 falling back to project root`, "yellow");
|
|
71256
|
+
}
|
|
71257
|
+
}
|
|
71258
|
+
const appendPrompt = args.prompt || cfg.appendPrompt || "";
|
|
71259
|
+
const changeName = await scaffoldChangeForIssue(scaffoldTasksDir, scaffoldStatesDir, issue, comments, appendPrompt);
|
|
71260
|
+
cwdByChange.set(changeName, workerCwd);
|
|
71261
|
+
statesDirByChange.set(changeName, scaffoldStatesDir);
|
|
71262
|
+
issueByChange.set(changeName, issue);
|
|
71263
|
+
if (workerBranch)
|
|
71264
|
+
branchByChange.set(changeName, workerBranch);
|
|
71265
|
+
if (cfg.setupScript) {
|
|
71266
|
+
await runScript("setup", cfg.setupScript, workerCwd);
|
|
71267
|
+
}
|
|
71268
|
+
return changeName;
|
|
71269
|
+
}
|
|
71270
|
+
function buildTaskCmdFor(changeName) {
|
|
71271
|
+
const c = [
|
|
71272
|
+
process.execPath,
|
|
71273
|
+
process.argv[1] ?? "",
|
|
71274
|
+
"task",
|
|
71275
|
+
"--name",
|
|
71276
|
+
changeName,
|
|
71277
|
+
"--" + (args.engineSet ? args.engine : cfg.engine),
|
|
71278
|
+
args.engineSet ? args.model : cfg.model
|
|
71279
|
+
];
|
|
71280
|
+
const maxIter = args.maxIterations || cfg.maxIterationsPerTask;
|
|
71281
|
+
if (maxIter > 0)
|
|
71282
|
+
c.push("--max-iterations", String(maxIter));
|
|
71283
|
+
const maxCost = args.maxCostUsd || cfg.maxCostUsdPerTask;
|
|
71284
|
+
if (maxCost > 0)
|
|
71285
|
+
c.push("--max-cost", String(maxCost));
|
|
71286
|
+
const maxRuntime = args.maxRuntimeMinutes || cfg.maxRuntimeMinutesPerTask;
|
|
71287
|
+
if (maxRuntime > 0)
|
|
71288
|
+
c.push("--max-runtime", String(maxRuntime));
|
|
71289
|
+
const maxFailures = args.maxConsecutiveFailures !== 5 ? args.maxConsecutiveFailures : cfg.maxConsecutiveFailuresPerTask;
|
|
71290
|
+
if (maxFailures !== 5)
|
|
71291
|
+
c.push("--max-failures", String(maxFailures));
|
|
71292
|
+
const delay2 = args.delay || cfg.iterationDelaySeconds;
|
|
71293
|
+
if (delay2 > 0)
|
|
71294
|
+
c.push("--delay", String(delay2));
|
|
71295
|
+
if (args.log || cfg.logRawStream)
|
|
71296
|
+
c.push("--log");
|
|
71297
|
+
if (args.verbose || cfg.taskVerbose)
|
|
71298
|
+
c.push("--verbose");
|
|
71299
|
+
return c;
|
|
71300
|
+
}
|
|
71301
|
+
function spawnWorker(changeName) {
|
|
71302
|
+
const cwd2 = cwdByChange.get(changeName) ?? projectRoot;
|
|
71303
|
+
const respawn = () => {
|
|
71304
|
+
const rp = Bun.spawn({
|
|
71305
|
+
cmd: buildTaskCmdFor(changeName),
|
|
71306
|
+
cwd: cwd2,
|
|
71307
|
+
stdout: "ignore",
|
|
71308
|
+
stderr: "ignore",
|
|
71309
|
+
stdin: "ignore"
|
|
71310
|
+
});
|
|
71311
|
+
return rp.exited;
|
|
71312
|
+
};
|
|
71313
|
+
const proc = Bun.spawn({
|
|
71314
|
+
cmd: buildTaskCmdFor(changeName),
|
|
71315
|
+
cwd: cwd2,
|
|
71316
|
+
stdout: "ignore",
|
|
71317
|
+
stderr: "ignore",
|
|
71318
|
+
stdin: "ignore"
|
|
71319
|
+
});
|
|
71320
|
+
onWorkerStarted(changeName, statesDirByChange.get(changeName) ?? statesDir);
|
|
71321
|
+
const wantPr = args.createPr || cfg.createPrOnSuccess;
|
|
71322
|
+
const wantFixCi = args.fixCi || cfg.fixCiOnFailure;
|
|
71323
|
+
const wrapped = proc.exited.then(async (code) => {
|
|
71324
|
+
const workerLayout = projectLayout(cwd2);
|
|
71325
|
+
const effectiveCode = await runPostTask({
|
|
71326
|
+
changeName,
|
|
71327
|
+
cwd: cwd2,
|
|
71328
|
+
projectRoot,
|
|
71329
|
+
changeDir: workerLayout.changeDir(changeName),
|
|
71330
|
+
stateFilePath: workerLayout.stateFile(changeName),
|
|
71331
|
+
branch: branchByChange.get(changeName) ?? null,
|
|
71332
|
+
issue: issueByChange.get(changeName) ?? null,
|
|
71333
|
+
exitCode: code,
|
|
71334
|
+
useWorktree,
|
|
71335
|
+
wantPr,
|
|
71336
|
+
wantFixCi,
|
|
71337
|
+
cfg: {
|
|
71338
|
+
teardownScript: cfg.teardownScript ?? null,
|
|
71339
|
+
prBaseBranch: cfg.prBaseBranch,
|
|
71340
|
+
maxCiFixAttempts: cfg.maxCiFixAttempts,
|
|
71341
|
+
ciPollIntervalSeconds: cfg.ciPollIntervalSeconds,
|
|
71342
|
+
cleanupWorktreeOnSuccess: cfg.cleanupWorktreeOnSuccess
|
|
71343
|
+
},
|
|
71344
|
+
respawnWorker: respawn
|
|
71345
|
+
}, { cmd: bunCmdRunner, git: bunGitRunner, log: onLog, runScript });
|
|
71346
|
+
cwdByChange.delete(changeName);
|
|
71347
|
+
statesDirByChange.delete(changeName);
|
|
71348
|
+
branchByChange.delete(changeName);
|
|
71349
|
+
issueByChange.delete(changeName);
|
|
71350
|
+
onWorkerExited(changeName);
|
|
71351
|
+
return effectiveCode;
|
|
71352
|
+
});
|
|
71353
|
+
return { exited: wrapped, kill: () => proc.kill() };
|
|
71354
|
+
}
|
|
71355
|
+
const coord = new AgentCoordinator({
|
|
71356
|
+
fetchIssues: (f2) => fetchOpenIssues(apiKey, f2),
|
|
71357
|
+
scaffold: scaffoldCallback,
|
|
71358
|
+
spawnWorker,
|
|
71359
|
+
store,
|
|
71360
|
+
onLog,
|
|
71361
|
+
onWorkersChanged,
|
|
71362
|
+
getIterationCount: async (changeName) => {
|
|
71363
|
+
const root = cwdByChange.get(changeName) ?? projectRoot;
|
|
71364
|
+
const file = Bun.file(projectLayout(root).stateFile(changeName));
|
|
71365
|
+
if (!await file.exists())
|
|
71366
|
+
return 0;
|
|
71367
|
+
const json = await file.json();
|
|
71368
|
+
return json.iteration ?? 0;
|
|
71369
|
+
},
|
|
71370
|
+
updater: {
|
|
71371
|
+
postComment: (issue, body) => addIssueComment(apiKey, issue.id, body),
|
|
71372
|
+
setState: (issue, stateId) => updateIssueState(apiKey, issue.id, stateId),
|
|
71373
|
+
resolveStateId: async (issue, stateName) => {
|
|
71374
|
+
const team = teamKeyOf(issue);
|
|
71375
|
+
let map2 = stateCache.get(team);
|
|
71376
|
+
if (!map2) {
|
|
71377
|
+
const states = await fetchWorkflowStates(apiKey, team);
|
|
71378
|
+
map2 = new Map(states.map((s) => [s.name.toLowerCase(), s.id]));
|
|
71379
|
+
stateCache.set(team, map2);
|
|
71380
|
+
}
|
|
71381
|
+
return map2.get(stateName.toLowerCase()) ?? null;
|
|
71382
|
+
},
|
|
71383
|
+
addLabel: (issue, labelId) => addLabelToIssue(apiKey, issue.id, labelId),
|
|
71384
|
+
resolveLabelId: async (issue, labelName) => {
|
|
71385
|
+
const team = teamKeyOf(issue);
|
|
71386
|
+
let map2 = labelCache.get(team);
|
|
71387
|
+
if (!map2) {
|
|
71388
|
+
const labels = await fetchIssueLabels(apiKey, team);
|
|
71389
|
+
map2 = new Map(labels.map((l) => [l.name.toLowerCase(), l.id]));
|
|
71390
|
+
labelCache.set(team, map2);
|
|
71391
|
+
}
|
|
71392
|
+
return map2.get(labelName.toLowerCase()) ?? null;
|
|
71393
|
+
}
|
|
71394
|
+
}
|
|
71395
|
+
}, {
|
|
71396
|
+
concurrency,
|
|
71397
|
+
filter: filter2,
|
|
71398
|
+
inProgressStatus: args.inProgressStatus || cfg.linear.inProgressStatus,
|
|
71399
|
+
doneStatus: args.doneStatus || cfg.linear.doneStatus,
|
|
71400
|
+
doneLabel: args.doneLabel || cfg.linear.doneLabel,
|
|
71401
|
+
postComments: cfg.linear.postComments,
|
|
71402
|
+
commentEveryIterations: cfg.linear.updateEveryIterations
|
|
71403
|
+
});
|
|
71404
|
+
const filterDesc = `team=${filter2.team ?? "*"}, assignee=${filter2.assignee ?? "*"}, statuses=` + `${filter2.statuses?.length ? filter2.statuses.join(",") : "open"}` + `${filter2.labels?.length ? `, labels=${filter2.labels.join(",")}` : ""}`;
|
|
71405
|
+
return {
|
|
71406
|
+
coord,
|
|
71407
|
+
filterDesc,
|
|
71408
|
+
concurrency,
|
|
71409
|
+
pollInterval,
|
|
71410
|
+
getWorkerCwd: (changeName) => cwdByChange.get(changeName)
|
|
71411
|
+
};
|
|
71412
|
+
}
|
|
71413
|
+
|
|
71414
|
+
// apps/cli/src/components/AgentMode.tsx
|
|
71415
|
+
var jsx_dev_runtime9 = __toESM(require_jsx_dev_runtime(), 1);
|
|
70946
71416
|
var lineCounter = 0;
|
|
70947
71417
|
function nextId() {
|
|
70948
71418
|
lineCounter += 1;
|
|
70949
71419
|
return `${Date.now()}-${lineCounter}`;
|
|
70950
71420
|
}
|
|
70951
71421
|
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
70952
|
-
async function injectFixSteering(changeDir, heading, steering) {
|
|
70953
|
-
const steeringFile = Bun.file(join14(changeDir, "steering.md"));
|
|
70954
|
-
const existing = await steeringFile.exists() ? await steeringFile.text() : "";
|
|
70955
|
-
const stamped = `## ${heading} (${new Date().toISOString()})
|
|
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);
|
|
70974
|
-
}
|
|
70975
71422
|
function fmtElapsed(ms) {
|
|
70976
71423
|
const s = Math.floor(ms / 1000);
|
|
70977
71424
|
if (s < 60)
|
|
@@ -70991,7 +71438,6 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
70991
71438
|
const coordRef = import_react57.useRef(null);
|
|
70992
71439
|
const workerMetaRef = import_react57.useRef(new Map);
|
|
70993
71440
|
const nextPollAtRef = import_react57.useRef(0);
|
|
70994
|
-
const pollIntervalRef = import_react57.useRef(0);
|
|
70995
71441
|
const [pollStatus, setPollStatus] = import_react57.useState({ state: "idle", lastFound: null, lastAdded: null, lastAt: null, filterDesc: "" });
|
|
70996
71442
|
function appendLog(text, color) {
|
|
70997
71443
|
setLogs((prev) => [...prev, { id: nextId(), text, color }]);
|
|
@@ -71002,389 +71448,39 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
71002
71448
|
async function init2() {
|
|
71003
71449
|
const cfgPath = await ensureRalphyConfig(projectRoot);
|
|
71004
71450
|
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");
|
|
71451
|
+
appendLog(`agent mode v${VERSION} \u2014 config: ${cfgPath}`, "gray");
|
|
71010
71452
|
const apiKey = process.env["LINEAR_API_KEY"];
|
|
71011
71453
|
if (!apiKey) {
|
|
71012
71454
|
appendLog("! LINEAR_API_KEY not set \u2014 cannot poll Linear", "red");
|
|
71013
71455
|
exit();
|
|
71014
71456
|
return;
|
|
71015
71457
|
}
|
|
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
|
-
});
|
|
71458
|
+
const store = new AgentStateStore(projectRoot);
|
|
71459
|
+
await store.load();
|
|
71460
|
+
const { coord: coord2, filterDesc, concurrency, pollInterval } = buildAgentCoordinator({
|
|
71461
|
+
args,
|
|
71462
|
+
cfg,
|
|
71463
|
+
projectRoot,
|
|
71464
|
+
statesDir,
|
|
71465
|
+
tasksDir,
|
|
71466
|
+
apiKey,
|
|
71467
|
+
store,
|
|
71468
|
+
onLog: appendLog,
|
|
71469
|
+
onWorkersChanged: () => setTick((t) => t + 1),
|
|
71470
|
+
onWorkerStarted: (changeName, dir) => {
|
|
71132
71471
|
workerMetaRef.current.set(changeName, {
|
|
71133
71472
|
startedAt: Date.now(),
|
|
71134
|
-
statesDir:
|
|
71473
|
+
statesDir: dir,
|
|
71135
71474
|
iter: 0
|
|
71136
71475
|
});
|
|
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
71476
|
},
|
|
71339
|
-
|
|
71340
|
-
|
|
71341
|
-
|
|
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;
|
|
71350
|
-
},
|
|
71351
|
-
updater: {
|
|
71352
|
-
postComment: (issue, body) => addIssueComment(apiKey, issue.id, body),
|
|
71353
|
-
setState: (issue, stateId) => updateIssueState(apiKey, issue.id, stateId),
|
|
71354
|
-
resolveStateId: async (issue, stateName) => {
|
|
71355
|
-
const team = teamKeyOf(issue);
|
|
71356
|
-
let map2 = stateCache.get(team);
|
|
71357
|
-
if (!map2) {
|
|
71358
|
-
const states = await fetchWorkflowStates(apiKey, team);
|
|
71359
|
-
map2 = new Map(states.map((s) => [s.name.toLowerCase(), s.id]));
|
|
71360
|
-
stateCache.set(team, map2);
|
|
71361
|
-
}
|
|
71362
|
-
return map2.get(stateName.toLowerCase()) ?? null;
|
|
71363
|
-
},
|
|
71364
|
-
addLabel: (issue, labelId) => addLabelToIssue(apiKey, issue.id, labelId),
|
|
71365
|
-
resolveLabelId: async (issue, labelName) => {
|
|
71366
|
-
const team = teamKeyOf(issue);
|
|
71367
|
-
let map2 = labelCache.get(team);
|
|
71368
|
-
if (!map2) {
|
|
71369
|
-
const labels = await fetchIssueLabels(apiKey, team);
|
|
71370
|
-
map2 = new Map(labels.map((l) => [l.name.toLowerCase(), l.id]));
|
|
71371
|
-
labelCache.set(team, map2);
|
|
71372
|
-
}
|
|
71373
|
-
return map2.get(labelName.toLowerCase()) ?? null;
|
|
71374
|
-
}
|
|
71375
|
-
}
|
|
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
|
|
71477
|
+
onWorkerExited: (changeName) => {
|
|
71478
|
+
workerMetaRef.current.delete(changeName);
|
|
71479
|
+
}
|
|
71384
71480
|
});
|
|
71481
|
+
appendLog(`concurrency=${concurrency} pollInterval=${pollInterval}s`, "gray");
|
|
71385
71482
|
coordRef.current = coord2;
|
|
71386
71483
|
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
71484
|
const tick = async () => {
|
|
71389
71485
|
if (cancelled)
|
|
71390
71486
|
return;
|
|
@@ -71435,7 +71531,7 @@ ${check.unpushedCommits}`, "yellow");
|
|
|
71435
71531
|
(async () => {
|
|
71436
71532
|
for (const [changeName, meta] of workerMetaRef.current) {
|
|
71437
71533
|
try {
|
|
71438
|
-
const file = Bun.file(
|
|
71534
|
+
const file = Bun.file(join16(meta.statesDir, changeName, ".ralph-state.json"));
|
|
71439
71535
|
if (await file.exists()) {
|
|
71440
71536
|
const json = await file.json();
|
|
71441
71537
|
meta.iter = json.iteration ?? meta.iter;
|
|
@@ -71516,11 +71612,11 @@ ${check.unpushedCommits}`, "yellow");
|
|
|
71516
71612
|
}
|
|
71517
71613
|
|
|
71518
71614
|
// packages/openspec/src/openspec-change-store.ts
|
|
71519
|
-
import { join as
|
|
71615
|
+
import { join as join17, dirname as dirname4 } from "path";
|
|
71520
71616
|
import { readdir, mkdir as mkdir3 } from "fs/promises";
|
|
71521
71617
|
function resolveOpenspecBin() {
|
|
71522
71618
|
const pkgJsonPath = Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir);
|
|
71523
|
-
return
|
|
71619
|
+
return join17(dirname4(pkgJsonPath), "bin", "openspec.js");
|
|
71524
71620
|
}
|
|
71525
71621
|
function runOpenspec(args, options = {}) {
|
|
71526
71622
|
const stdio = options.inherit ? ["inherit", "inherit", "inherit"] : ["ignore", "pipe", "pipe"];
|
|
@@ -71546,7 +71642,7 @@ class OpenSpecChangeStore {
|
|
|
71546
71642
|
}
|
|
71547
71643
|
}
|
|
71548
71644
|
getChangeDirectory(name) {
|
|
71549
|
-
return
|
|
71645
|
+
return join17("openspec", "changes", name);
|
|
71550
71646
|
}
|
|
71551
71647
|
async listChanges() {
|
|
71552
71648
|
const result2 = runOpenspec(["list", "--json"]);
|
|
@@ -71560,7 +71656,7 @@ class OpenSpecChangeStore {
|
|
|
71560
71656
|
}
|
|
71561
71657
|
} catch {}
|
|
71562
71658
|
}
|
|
71563
|
-
const changesDir =
|
|
71659
|
+
const changesDir = join17("openspec", "changes");
|
|
71564
71660
|
if (!await Bun.file(changesDir).exists())
|
|
71565
71661
|
return [];
|
|
71566
71662
|
try {
|
|
@@ -71571,18 +71667,18 @@ class OpenSpecChangeStore {
|
|
|
71571
71667
|
}
|
|
71572
71668
|
}
|
|
71573
71669
|
async readTaskList(name) {
|
|
71574
|
-
const file = Bun.file(
|
|
71670
|
+
const file = Bun.file(join17("openspec", "changes", name, "tasks.md"));
|
|
71575
71671
|
if (!await file.exists())
|
|
71576
71672
|
return "";
|
|
71577
71673
|
return await file.text();
|
|
71578
71674
|
}
|
|
71579
71675
|
async writeTaskList(name, content) {
|
|
71580
|
-
const path =
|
|
71676
|
+
const path = join17("openspec", "changes", name, "tasks.md");
|
|
71581
71677
|
await mkdir3(dirname4(path), { recursive: true });
|
|
71582
71678
|
await Bun.write(path, content);
|
|
71583
71679
|
}
|
|
71584
71680
|
async appendSteering(name, message) {
|
|
71585
|
-
const path =
|
|
71681
|
+
const path = join17("openspec", "changes", name, "steering.md");
|
|
71586
71682
|
const file = Bun.file(path);
|
|
71587
71683
|
const existing = await file.exists() ? await file.text() : null;
|
|
71588
71684
|
const updated = existing ? `${message}
|
|
@@ -71593,7 +71689,7 @@ ${existing.trimStart()}` : `${message}
|
|
|
71593
71689
|
await Bun.write(path, updated);
|
|
71594
71690
|
}
|
|
71595
71691
|
async readSection(name, artifact, heading) {
|
|
71596
|
-
const file = Bun.file(
|
|
71692
|
+
const file = Bun.file(join17("openspec", "changes", name, artifact));
|
|
71597
71693
|
if (!await file.exists())
|
|
71598
71694
|
return "";
|
|
71599
71695
|
const content = await file.text();
|
|
@@ -71675,8 +71771,8 @@ function App2({ args, statesDir, tasksDir, projectRoot }) {
|
|
|
71675
71771
|
message: "Error: --name is required for status mode"
|
|
71676
71772
|
}, undefined, false, undefined, this);
|
|
71677
71773
|
}
|
|
71678
|
-
const stateDir =
|
|
71679
|
-
if (getStorage().read(
|
|
71774
|
+
const stateDir = join18(statesDir, args.name);
|
|
71775
|
+
if (getStorage().read(join18(stateDir, ".ralph-state.json")) === null) {
|
|
71680
71776
|
return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(ErrorMessage, {
|
|
71681
71777
|
message: `Error: change '${args.name}' not found`
|
|
71682
71778
|
}, undefined, false, undefined, this);
|
|
@@ -71737,7 +71833,7 @@ if (typeof globalThis.Bun === "undefined") {
|
|
|
71737
71833
|
async function findProjectRoot() {
|
|
71738
71834
|
let dir = process.cwd();
|
|
71739
71835
|
while (dir !== "/") {
|
|
71740
|
-
if (await exists2(
|
|
71836
|
+
if (await exists2(join19(dir, "openspec")))
|
|
71741
71837
|
return dir;
|
|
71742
71838
|
dir = resolve(dir, "..");
|
|
71743
71839
|
}
|
|
@@ -71772,11 +71868,12 @@ try {
|
|
|
71772
71868
|
capture("command_run", { mode: args.mode, engine: args.engine, model: args.model });
|
|
71773
71869
|
try {
|
|
71774
71870
|
const projectRoot = await findProjectRoot();
|
|
71775
|
-
const
|
|
71776
|
-
const
|
|
71871
|
+
const layout = projectLayout(projectRoot);
|
|
71872
|
+
const statesDir = layout.statesDir;
|
|
71873
|
+
const tasksDir = layout.tasksDir;
|
|
71777
71874
|
if (args.mode === "init") {
|
|
71778
71875
|
await mkdir4(statesDir, { recursive: true });
|
|
71779
|
-
const openspecBin =
|
|
71876
|
+
const openspecBin = join19(dirname5(Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir)), "bin", "openspec.js");
|
|
71780
71877
|
Bun.spawnSync({
|
|
71781
71878
|
cmd: [process.execPath, openspecBin, "init", "--tools", "none", "--force"],
|
|
71782
71879
|
stdio: ["inherit", "inherit", "inherit"],
|
|
@@ -71789,9 +71886,9 @@ try {
|
|
|
71789
71886
|
`);
|
|
71790
71887
|
process.exit(1);
|
|
71791
71888
|
}
|
|
71792
|
-
const worktreeDir =
|
|
71793
|
-
const changeDir =
|
|
71794
|
-
const stateDir =
|
|
71889
|
+
const worktreeDir = join19(worktreesDir(projectRoot), args.name);
|
|
71890
|
+
const changeDir = join19(tasksDir, args.name);
|
|
71891
|
+
const stateDir = join19(statesDir, args.name);
|
|
71795
71892
|
const branch = `ralph/${args.name}`;
|
|
71796
71893
|
const removed = [];
|
|
71797
71894
|
if (await exists2(worktreeDir)) {
|
|
@@ -71830,12 +71927,11 @@ try {
|
|
|
71830
71927
|
removed.push(`task state ${stateDir}`);
|
|
71831
71928
|
}
|
|
71832
71929
|
try {
|
|
71833
|
-
const
|
|
71834
|
-
|
|
71835
|
-
|
|
71836
|
-
|
|
71837
|
-
|
|
71838
|
-
removed.push(`agent-state entry for ${entry.identifier} (${entry.issueId})`);
|
|
71930
|
+
const store = new AgentStateStore(projectRoot);
|
|
71931
|
+
await store.load();
|
|
71932
|
+
const removedEntry = await store.removeByChangeName(args.name);
|
|
71933
|
+
if (removedEntry) {
|
|
71934
|
+
removed.push(`agent-state entry for ${removedEntry.identifier} (${removedEntry.issueId})`);
|
|
71839
71935
|
}
|
|
71840
71936
|
} catch {}
|
|
71841
71937
|
if (removed.length === 0) {
|
|
@@ -71852,13 +71948,13 @@ try {
|
|
|
71852
71948
|
process.exit(0);
|
|
71853
71949
|
}
|
|
71854
71950
|
if (args.mode === "task" && args.name) {
|
|
71855
|
-
await mkdir4(
|
|
71856
|
-
await mkdir4(
|
|
71951
|
+
await mkdir4(join19(statesDir, args.name), { recursive: true });
|
|
71952
|
+
await mkdir4(join19(tasksDir, args.name), { recursive: true });
|
|
71857
71953
|
}
|
|
71858
71954
|
if (args.mode === "agent") {
|
|
71859
71955
|
await mkdir4(statesDir, { recursive: true });
|
|
71860
71956
|
await mkdir4(tasksDir, { recursive: true });
|
|
71861
|
-
await mkdir4(
|
|
71957
|
+
await mkdir4(join19(projectRoot, ".ralph"), { recursive: true });
|
|
71862
71958
|
}
|
|
71863
71959
|
await runWithContext(createDefaultContext(), async () => {
|
|
71864
71960
|
const { waitUntilExit } = render_default(import_react59.createElement(App2, { args, statesDir, tasksDir, projectRoot }));
|