@neriros/ralphy 2.11.2 → 2.13.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 +1034 -434
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -50838,7 +50838,7 @@ var require_axios = __commonJS((exports, module) => {
|
|
|
50838
50838
|
|
|
50839
50839
|
// apps/cli/src/index.ts
|
|
50840
50840
|
import { resolve, join as join19, dirname as dirname5 } from "path";
|
|
50841
|
-
import { exists as exists2, mkdir as
|
|
50841
|
+
import { exists as exists2, mkdir as mkdir5, rm } from "fs/promises";
|
|
50842
50842
|
|
|
50843
50843
|
// node_modules/.bun/ink@5.2.1+1f88f629f0141b18/node_modules/ink/build/render.js
|
|
50844
50844
|
import { Stream } from "stream";
|
|
@@ -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.13.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",
|
|
@@ -56502,6 +56502,17 @@ var package_default = {
|
|
|
56502
56502
|
var VERSION = package_default.version;
|
|
56503
56503
|
var VALID_MODES = new Set(["task", "list", "status", "init", "agent", "clean"]);
|
|
56504
56504
|
var VALID_MODELS = new Set(["haiku", "sonnet", "opus"]);
|
|
56505
|
+
var INDICATOR_KEYS = new Set([
|
|
56506
|
+
"getTodo",
|
|
56507
|
+
"getInProgress",
|
|
56508
|
+
"getConflicted",
|
|
56509
|
+
"setInProgress",
|
|
56510
|
+
"setDone",
|
|
56511
|
+
"setError",
|
|
56512
|
+
"setConflicted",
|
|
56513
|
+
"clearConflicted"
|
|
56514
|
+
]);
|
|
56515
|
+
var GET_KEYS = new Set(["getTodo", "getInProgress", "getConflicted"]);
|
|
56505
56516
|
var HELP_TEXT = [
|
|
56506
56517
|
`ralph v${VERSION}`,
|
|
56507
56518
|
"",
|
|
@@ -56534,23 +56545,25 @@ var HELP_TEXT = [
|
|
|
56534
56545
|
"Agent mode options (require LINEAR_API_KEY env var):",
|
|
56535
56546
|
" --linear-team <key> Linear team key (e.g. ENG)",
|
|
56536
56547
|
" --linear-assignee <id> Filter by assignee (user id, email, or 'me')",
|
|
56537
|
-
" --linear-status <name> Filter by status name (repeatable, e.g. Todo, In Progress)",
|
|
56538
|
-
" --linear-label <name> Filter by label name (repeatable, any-of)",
|
|
56539
56548
|
" --poll-interval <s> Seconds between Linear polls (default: 60)",
|
|
56540
56549
|
" --concurrency <n> Max concurrent task loops (default: 1)",
|
|
56541
|
-
" --worktree Run each task in its own git worktree
|
|
56542
|
-
" --
|
|
56543
|
-
"
|
|
56544
|
-
"
|
|
56550
|
+
" --worktree Run each task in its own git worktree",
|
|
56551
|
+
" --indicator <k>:<t>:<v> Override an indicator (repeatable). Examples:",
|
|
56552
|
+
" --indicator getTodo:status:Todo",
|
|
56553
|
+
" --indicator setDone:label:shipped",
|
|
56554
|
+
" --indicator setDone:status:Done (combined with above \u2192 multi-marker)",
|
|
56555
|
+
" Keys: getTodo, getInProgress, getConflicted,",
|
|
56556
|
+
" setInProgress, setDone, setError, setConflicted, clearConflicted",
|
|
56557
|
+
" Types: label, status",
|
|
56545
56558
|
" --create-pr Push the worker branch and open a GitHub PR on success (needs --worktree)",
|
|
56546
|
-
" --fix-ci After opening the PR, re-run
|
|
56559
|
+
" --fix-ci After opening the PR, re-run on CI failures until green (needs --create-pr)",
|
|
56547
56560
|
"",
|
|
56548
56561
|
" --help, -h Show this help message",
|
|
56549
56562
|
"",
|
|
56550
56563
|
"Examples:",
|
|
56551
56564
|
' ralph task --name my-feature --prompt "Add dark mode"',
|
|
56552
56565
|
" ralph task --name my-feature --claude sonnet --max-iterations 10",
|
|
56553
|
-
" ralph
|
|
56566
|
+
" ralph agent --indicator getTodo:status:Todo --indicator setDone:status:Done",
|
|
56554
56567
|
" ralph list",
|
|
56555
56568
|
" ralph status --name my-feature",
|
|
56556
56569
|
" ralph init"
|
|
@@ -56559,6 +56572,53 @@ var HELP_TEXT = [
|
|
|
56559
56572
|
function printHelp() {
|
|
56560
56573
|
log(HELP_TEXT);
|
|
56561
56574
|
}
|
|
56575
|
+
function parseIndicatorArg(raw) {
|
|
56576
|
+
const firstColon = raw.indexOf(":");
|
|
56577
|
+
if (firstColon < 0) {
|
|
56578
|
+
const err = new Error("--indicator expects key:type:value");
|
|
56579
|
+
err.input = raw;
|
|
56580
|
+
throw err;
|
|
56581
|
+
}
|
|
56582
|
+
const secondColon = raw.indexOf(":", firstColon + 1);
|
|
56583
|
+
if (secondColon < 0) {
|
|
56584
|
+
const err = new Error("--indicator expects key:type:value");
|
|
56585
|
+
err.input = raw;
|
|
56586
|
+
throw err;
|
|
56587
|
+
}
|
|
56588
|
+
const key = raw.slice(0, firstColon);
|
|
56589
|
+
const type = raw.slice(firstColon + 1, secondColon);
|
|
56590
|
+
const value = raw.slice(secondColon + 1);
|
|
56591
|
+
if (!INDICATOR_KEYS.has(key)) {
|
|
56592
|
+
const err = new Error("unknown indicator key");
|
|
56593
|
+
err.key = key;
|
|
56594
|
+
throw err;
|
|
56595
|
+
}
|
|
56596
|
+
if (type !== "label" && type !== "status") {
|
|
56597
|
+
const err = new Error("indicator type must be 'label' or 'status'");
|
|
56598
|
+
err.type = type;
|
|
56599
|
+
throw err;
|
|
56600
|
+
}
|
|
56601
|
+
if (!value)
|
|
56602
|
+
throw new Error("indicator value cannot be empty");
|
|
56603
|
+
return { key, marker: { type, value } };
|
|
56604
|
+
}
|
|
56605
|
+
function mergeIndicator(bag, key, marker) {
|
|
56606
|
+
if (GET_KEYS.has(key)) {
|
|
56607
|
+
const existing = bag[key];
|
|
56608
|
+
const filter2 = existing ? [...existing.filter, marker] : [marker];
|
|
56609
|
+
bag[key] = { filter: filter2 };
|
|
56610
|
+
} else {
|
|
56611
|
+
const existing = bag[key];
|
|
56612
|
+
let next;
|
|
56613
|
+
if (!existing)
|
|
56614
|
+
next = marker;
|
|
56615
|
+
else if ("apply" in existing)
|
|
56616
|
+
next = { apply: [...existing.apply, marker] };
|
|
56617
|
+
else
|
|
56618
|
+
next = { apply: [existing, marker] };
|
|
56619
|
+
bag[key] = next;
|
|
56620
|
+
}
|
|
56621
|
+
}
|
|
56562
56622
|
async function parseArgs(argv) {
|
|
56563
56623
|
const result2 = {
|
|
56564
56624
|
mode: "task",
|
|
@@ -56576,14 +56636,10 @@ async function parseArgs(argv) {
|
|
|
56576
56636
|
verbose: false,
|
|
56577
56637
|
linearTeam: "",
|
|
56578
56638
|
linearAssignee: "",
|
|
56579
|
-
linearStatus: [],
|
|
56580
|
-
linearLabel: [],
|
|
56581
56639
|
pollInterval: 60,
|
|
56582
56640
|
concurrency: 1,
|
|
56583
56641
|
worktree: false,
|
|
56584
|
-
|
|
56585
|
-
doneStatus: "",
|
|
56586
|
-
doneLabel: "",
|
|
56642
|
+
indicators: {},
|
|
56587
56643
|
createPr: false,
|
|
56588
56644
|
fixCi: false
|
|
56589
56645
|
};
|
|
@@ -56601,13 +56657,9 @@ async function parseArgs(argv) {
|
|
|
56601
56657
|
let expectPushInterval = false;
|
|
56602
56658
|
let expectLinearTeam = false;
|
|
56603
56659
|
let expectLinearAssignee = false;
|
|
56604
|
-
let expectLinearStatus = false;
|
|
56605
|
-
let expectLinearLabel = false;
|
|
56606
56660
|
let expectPollInterval = false;
|
|
56607
56661
|
let expectConcurrency = false;
|
|
56608
|
-
let
|
|
56609
|
-
let expectDoneStatus = false;
|
|
56610
|
-
let expectDoneLabel = false;
|
|
56662
|
+
let expectIndicator = false;
|
|
56611
56663
|
for (const arg of argv) {
|
|
56612
56664
|
if (expectModel) {
|
|
56613
56665
|
if (VALID_MODELS.has(arg)) {
|
|
@@ -56683,16 +56735,6 @@ async function parseArgs(argv) {
|
|
|
56683
56735
|
expectLinearAssignee = false;
|
|
56684
56736
|
continue;
|
|
56685
56737
|
}
|
|
56686
|
-
if (expectLinearStatus) {
|
|
56687
|
-
result2.linearStatus.push(arg);
|
|
56688
|
-
expectLinearStatus = false;
|
|
56689
|
-
continue;
|
|
56690
|
-
}
|
|
56691
|
-
if (expectLinearLabel) {
|
|
56692
|
-
result2.linearLabel.push(arg);
|
|
56693
|
-
expectLinearLabel = false;
|
|
56694
|
-
continue;
|
|
56695
|
-
}
|
|
56696
56738
|
if (expectPollInterval) {
|
|
56697
56739
|
result2.pollInterval = parseInt(arg, 10);
|
|
56698
56740
|
expectPollInterval = false;
|
|
@@ -56703,19 +56745,10 @@ async function parseArgs(argv) {
|
|
|
56703
56745
|
expectConcurrency = false;
|
|
56704
56746
|
continue;
|
|
56705
56747
|
}
|
|
56706
|
-
if (
|
|
56707
|
-
|
|
56708
|
-
|
|
56709
|
-
|
|
56710
|
-
}
|
|
56711
|
-
if (expectDoneStatus) {
|
|
56712
|
-
result2.doneStatus = arg;
|
|
56713
|
-
expectDoneStatus = false;
|
|
56714
|
-
continue;
|
|
56715
|
-
}
|
|
56716
|
-
if (expectDoneLabel) {
|
|
56717
|
-
result2.doneLabel = arg;
|
|
56718
|
-
expectDoneLabel = false;
|
|
56748
|
+
if (expectIndicator) {
|
|
56749
|
+
const { key, marker } = parseIndicatorArg(arg);
|
|
56750
|
+
mergeIndicator(result2.indicators, key, marker);
|
|
56751
|
+
expectIndicator = false;
|
|
56719
56752
|
continue;
|
|
56720
56753
|
}
|
|
56721
56754
|
switch (arg) {
|
|
@@ -56782,12 +56815,6 @@ async function parseArgs(argv) {
|
|
|
56782
56815
|
case "--linear-assignee":
|
|
56783
56816
|
expectLinearAssignee = true;
|
|
56784
56817
|
break;
|
|
56785
|
-
case "--linear-status":
|
|
56786
|
-
expectLinearStatus = true;
|
|
56787
|
-
break;
|
|
56788
|
-
case "--linear-label":
|
|
56789
|
-
expectLinearLabel = true;
|
|
56790
|
-
break;
|
|
56791
56818
|
case "--poll-interval":
|
|
56792
56819
|
expectPollInterval = true;
|
|
56793
56820
|
break;
|
|
@@ -56797,14 +56824,8 @@ async function parseArgs(argv) {
|
|
|
56797
56824
|
case "--worktree":
|
|
56798
56825
|
result2.worktree = true;
|
|
56799
56826
|
break;
|
|
56800
|
-
case "--
|
|
56801
|
-
|
|
56802
|
-
break;
|
|
56803
|
-
case "--done-status":
|
|
56804
|
-
expectDoneStatus = true;
|
|
56805
|
-
break;
|
|
56806
|
-
case "--done-label":
|
|
56807
|
-
expectDoneLabel = true;
|
|
56827
|
+
case "--indicator":
|
|
56828
|
+
expectIndicator = true;
|
|
56808
56829
|
break;
|
|
56809
56830
|
case "--create-pr":
|
|
56810
56831
|
result2.createPr = true;
|
|
@@ -60904,6 +60925,9 @@ var StateSchema = exports_external.object({
|
|
|
60904
60925
|
history: exports_external.array(HistoryEntrySchema).default([]),
|
|
60905
60926
|
metadata: exports_external.object({ branch: exports_external.string().optional() }).default({})
|
|
60906
60927
|
});
|
|
60928
|
+
function markersOf(set2) {
|
|
60929
|
+
return "apply" in set2 ? set2.apply : [set2];
|
|
60930
|
+
}
|
|
60907
60931
|
var PhaseFrontmatterSchema = exports_external.object({
|
|
60908
60932
|
name: exports_external.string(),
|
|
60909
60933
|
order: exports_external.number(),
|
|
@@ -70106,87 +70130,44 @@ function TaskLoop({ opts }) {
|
|
|
70106
70130
|
var import_react57 = __toESM(require_react(), 1);
|
|
70107
70131
|
import { join as join16 } from "path";
|
|
70108
70132
|
|
|
70109
|
-
// apps/cli/src/agent/
|
|
70133
|
+
// apps/cli/src/agent/config.ts
|
|
70110
70134
|
import { join as join10 } from "path";
|
|
70111
|
-
var
|
|
70112
|
-
|
|
70113
|
-
|
|
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()
|
|
70135
|
+
var MarkerSchema = exports_external.object({
|
|
70136
|
+
type: exports_external.enum(["label", "status"]),
|
|
70137
|
+
value: exports_external.string().min(1)
|
|
70121
70138
|
});
|
|
70122
|
-
var
|
|
70123
|
-
|
|
70124
|
-
lastPollAt: exports_external.string().nullable().default(null)
|
|
70139
|
+
var GetIndicatorSchema = exports_external.object({
|
|
70140
|
+
filter: exports_external.array(MarkerSchema).default([])
|
|
70125
70141
|
});
|
|
70126
|
-
|
|
70127
|
-
|
|
70128
|
-
}
|
|
70129
|
-
|
|
70130
|
-
|
|
70131
|
-
|
|
70132
|
-
|
|
70133
|
-
|
|
70134
|
-
|
|
70135
|
-
|
|
70136
|
-
|
|
70137
|
-
|
|
70138
|
-
|
|
70139
|
-
|
|
70140
|
-
|
|
70141
|
-
|
|
70142
|
-
|
|
70143
|
-
|
|
70144
|
-
|
|
70145
|
-
|
|
70146
|
-
|
|
70147
|
-
|
|
70148
|
-
|
|
70149
|
-
|
|
70150
|
-
|
|
70151
|
-
|
|
70152
|
-
if (!this.state) {
|
|
70153
|
-
throw new Error("AgentStateStore: load() must be called before snapshot()");
|
|
70142
|
+
var SetIndicatorSchema = exports_external.union([
|
|
70143
|
+
MarkerSchema,
|
|
70144
|
+
exports_external.object({ apply: exports_external.array(MarkerSchema).min(1) })
|
|
70145
|
+
]);
|
|
70146
|
+
var IndicatorsSchema = exports_external.object({
|
|
70147
|
+
getTodo: GetIndicatorSchema.optional(),
|
|
70148
|
+
getInProgress: GetIndicatorSchema.optional(),
|
|
70149
|
+
getConflicted: GetIndicatorSchema.optional(),
|
|
70150
|
+
setInProgress: SetIndicatorSchema.optional(),
|
|
70151
|
+
setDone: SetIndicatorSchema.optional(),
|
|
70152
|
+
setError: SetIndicatorSchema.optional(),
|
|
70153
|
+
setConflicted: SetIndicatorSchema.optional(),
|
|
70154
|
+
clearConflicted: SetIndicatorSchema.optional()
|
|
70155
|
+
}).superRefine((value, ctx) => {
|
|
70156
|
+
const clear = value.clearConflicted;
|
|
70157
|
+
if (!clear)
|
|
70158
|
+
return;
|
|
70159
|
+
const markers = "apply" in clear ? clear.apply : [clear];
|
|
70160
|
+
for (const m of markers) {
|
|
70161
|
+
if (m.type !== "label") {
|
|
70162
|
+
ctx.addIssue({
|
|
70163
|
+
code: exports_external.ZodIssueCode.custom,
|
|
70164
|
+
path: ["clearConflicted"],
|
|
70165
|
+
message: "clearConflicted markers must be label-typed (status removal is not supported)"
|
|
70166
|
+
});
|
|
70167
|
+
return;
|
|
70154
70168
|
}
|
|
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
|
}
|
|
70169
|
-
|
|
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";
|
|
70170
|
+
});
|
|
70190
70171
|
var RalphyConfigSchema = exports_external.object({
|
|
70191
70172
|
concurrency: exports_external.number().int().positive().default(1),
|
|
70192
70173
|
pollIntervalSeconds: exports_external.number().int().positive().default(60),
|
|
@@ -70212,14 +70193,26 @@ var RalphyConfigSchema = exports_external.object({
|
|
|
70212
70193
|
linear: exports_external.object({
|
|
70213
70194
|
team: exports_external.string().optional(),
|
|
70214
70195
|
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
70196
|
postComments: exports_external.boolean().default(true),
|
|
70221
|
-
updateEveryIterations: exports_external.number().int().nonnegative().default(10)
|
|
70222
|
-
|
|
70197
|
+
updateEveryIterations: exports_external.number().int().nonnegative().default(10),
|
|
70198
|
+
indicators: IndicatorsSchema.default({})
|
|
70199
|
+
}).passthrough().superRefine((value, ctx) => {
|
|
70200
|
+
const LEGACY_KEYS = [
|
|
70201
|
+
"statuses",
|
|
70202
|
+
"labels",
|
|
70203
|
+
"inProgressStatus",
|
|
70204
|
+
"doneStatus",
|
|
70205
|
+
"doneLabel"
|
|
70206
|
+
];
|
|
70207
|
+
const found = LEGACY_KEYS.filter((k) => (k in value));
|
|
70208
|
+
if (found.length === 0)
|
|
70209
|
+
return;
|
|
70210
|
+
ctx.addIssue({
|
|
70211
|
+
code: exports_external.ZodIssueCode.custom,
|
|
70212
|
+
path: ["linear"],
|
|
70213
|
+
message: `legacy linear keys [${found.join(", ")}] cannot be used together with the new ` + `\`linear.indicators\` map \u2014 they describe the same lifecycle and combining them is ` + `not possible. Migrate by moving each legacy key into linear.indicators (e.g. ` + `doneStatus: "Done" \u2192 indicators.setDone: {type: "status", value: "Done"}; ` + `statuses/labels \u2192 indicators.getTodo.filter; inProgressStatus \u2192 indicators.setInProgress; ` + `doneLabel \u2192 indicators.setDone {type: "label", ...}).`
|
|
70214
|
+
});
|
|
70215
|
+
}).default({ postComments: true, updateEveryIterations: 10, indicators: {} })
|
|
70223
70216
|
}).default({
|
|
70224
70217
|
concurrency: 1,
|
|
70225
70218
|
pollIntervalSeconds: 60,
|
|
@@ -70227,10 +70220,10 @@ var RalphyConfigSchema = exports_external.object({
|
|
|
70227
70220
|
maxCostUsdPerTask: 0,
|
|
70228
70221
|
engine: "claude",
|
|
70229
70222
|
model: "opus",
|
|
70230
|
-
linear: {
|
|
70223
|
+
linear: { postComments: true, updateEveryIterations: 10, indicators: {} }
|
|
70231
70224
|
});
|
|
70232
70225
|
async function loadRalphyConfig(projectRoot) {
|
|
70233
|
-
const path =
|
|
70226
|
+
const path = join10(projectRoot, "ralphy.config.json");
|
|
70234
70227
|
const file = Bun.file(path);
|
|
70235
70228
|
if (!await file.exists()) {
|
|
70236
70229
|
return RalphyConfigSchema.parse({});
|
|
@@ -70239,7 +70232,7 @@ async function loadRalphyConfig(projectRoot) {
|
|
|
70239
70232
|
return RalphyConfigSchema.parse(raw);
|
|
70240
70233
|
}
|
|
70241
70234
|
async function ensureRalphyConfig(projectRoot) {
|
|
70242
|
-
const path =
|
|
70235
|
+
const path = join10(projectRoot, "ralphy.config.json");
|
|
70243
70236
|
const file = Bun.file(path);
|
|
70244
70237
|
if (await file.exists())
|
|
70245
70238
|
return path;
|
|
@@ -70249,47 +70242,86 @@ async function ensureRalphyConfig(projectRoot) {
|
|
|
70249
70242
|
return path;
|
|
70250
70243
|
}
|
|
70251
70244
|
|
|
70245
|
+
// apps/cli/src/agent/wire.ts
|
|
70246
|
+
import { join as join15 } from "path";
|
|
70247
|
+
import { mkdir as mkdir3 } from "fs/promises";
|
|
70248
|
+
|
|
70252
70249
|
// packages/core/src/layout.ts
|
|
70253
|
-
import { join as
|
|
70250
|
+
import { join as join11 } from "path";
|
|
70254
70251
|
var STATE_FILE2 = ".ralph-state.json";
|
|
70255
70252
|
function projectLayout(root) {
|
|
70256
|
-
const statesDir =
|
|
70257
|
-
const tasksDir =
|
|
70253
|
+
const statesDir = join11(root, ".ralph", "tasks");
|
|
70254
|
+
const tasksDir = join11(root, "openspec", "changes");
|
|
70258
70255
|
return {
|
|
70259
70256
|
root,
|
|
70260
70257
|
statesDir,
|
|
70261
70258
|
tasksDir,
|
|
70262
|
-
agentStateFile:
|
|
70263
|
-
changeDir: (name) =>
|
|
70264
|
-
taskStateDir: (name) =>
|
|
70265
|
-
stateFile: (name) =>
|
|
70259
|
+
agentStateFile: join11(root, ".ralph", "agent-state.json"),
|
|
70260
|
+
changeDir: (name) => join11(tasksDir, name),
|
|
70261
|
+
taskStateDir: (name) => join11(statesDir, name),
|
|
70262
|
+
stateFile: (name) => join11(statesDir, name, STATE_FILE2)
|
|
70266
70263
|
};
|
|
70267
70264
|
}
|
|
70268
70265
|
|
|
70269
70266
|
// apps/cli/src/agent/linear.ts
|
|
70270
|
-
var OPEN_STATE_TYPES = ["unstarted", "started", "backlog"];
|
|
70271
70267
|
var LINEAR_API = "https://api.linear.app/graphql";
|
|
70272
|
-
|
|
70268
|
+
function partition2(markers) {
|
|
70269
|
+
const statuses = [];
|
|
70270
|
+
const labels = [];
|
|
70271
|
+
for (const m of markers) {
|
|
70272
|
+
if (m.type === "status")
|
|
70273
|
+
statuses.push(m.value);
|
|
70274
|
+
else
|
|
70275
|
+
labels.push(m.value);
|
|
70276
|
+
}
|
|
70277
|
+
return { statuses, labels };
|
|
70278
|
+
}
|
|
70279
|
+
function buildIssueFilter(spec) {
|
|
70273
70280
|
const where = {};
|
|
70274
|
-
if (
|
|
70275
|
-
where.team = { key: { eq:
|
|
70276
|
-
if (
|
|
70277
|
-
if (
|
|
70281
|
+
if (spec.team)
|
|
70282
|
+
where.team = { key: { eq: spec.team } };
|
|
70283
|
+
if (spec.assignee) {
|
|
70284
|
+
if (spec.assignee === "me")
|
|
70278
70285
|
where.assignee = { isMe: { eq: true } };
|
|
70279
|
-
|
|
70280
|
-
where.assignee = { email: { eq:
|
|
70281
|
-
|
|
70282
|
-
where.assignee = { id: { eq:
|
|
70283
|
-
|
|
70284
|
-
|
|
70285
|
-
if (
|
|
70286
|
-
|
|
70286
|
+
else if (spec.assignee.includes("@"))
|
|
70287
|
+
where.assignee = { email: { eq: spec.assignee } };
|
|
70288
|
+
else
|
|
70289
|
+
where.assignee = { id: { eq: spec.assignee } };
|
|
70290
|
+
}
|
|
70291
|
+
const inc = spec.include ?? [];
|
|
70292
|
+
if (inc.length > 0) {
|
|
70293
|
+
const { statuses, labels } = partition2(inc);
|
|
70294
|
+
const branches = [];
|
|
70295
|
+
if (statuses.length > 0)
|
|
70296
|
+
branches.push({ state: { name: { in: statuses } } });
|
|
70297
|
+
if (labels.length > 0)
|
|
70298
|
+
branches.push({ labels: { some: { name: { in: labels } } } });
|
|
70299
|
+
if (branches.length === 1)
|
|
70300
|
+
Object.assign(where, branches[0]);
|
|
70301
|
+
else
|
|
70302
|
+
where.or = branches;
|
|
70287
70303
|
} else {
|
|
70288
|
-
where.state = { type: { in: [
|
|
70289
|
-
}
|
|
70290
|
-
|
|
70291
|
-
|
|
70304
|
+
where.state = { type: { in: ["unstarted", "started", "backlog"] } };
|
|
70305
|
+
}
|
|
70306
|
+
const exc = spec.exclude ?? [];
|
|
70307
|
+
if (exc.length > 0) {
|
|
70308
|
+
const { statuses, labels } = partition2(exc);
|
|
70309
|
+
if (statuses.length > 0) {
|
|
70310
|
+
const current = where.state;
|
|
70311
|
+
const noStatus = { state: { name: { nin: statuses } } };
|
|
70312
|
+
if (current === undefined)
|
|
70313
|
+
Object.assign(where, noStatus);
|
|
70314
|
+
else
|
|
70315
|
+
where.and = [{ state: current }, noStatus];
|
|
70316
|
+
}
|
|
70317
|
+
if (labels.length > 0) {
|
|
70318
|
+
where.labels = { ...where.labels, every: { name: { nin: labels } } };
|
|
70319
|
+
}
|
|
70292
70320
|
}
|
|
70321
|
+
return where;
|
|
70322
|
+
}
|
|
70323
|
+
async function fetchOpenIssues(apiKey, spec) {
|
|
70324
|
+
const where = buildIssueFilter(spec);
|
|
70293
70325
|
const query = `query Issues($filter: IssueFilter) {
|
|
70294
70326
|
issues(filter: $filter, first: 50) {
|
|
70295
70327
|
nodes {
|
|
@@ -70406,6 +70438,15 @@ async function addLabelToIssue(apiKey, issueId, labelId) {
|
|
|
70406
70438
|
labelId
|
|
70407
70439
|
});
|
|
70408
70440
|
}
|
|
70441
|
+
async function removeLabelFromIssue(apiKey, issueId, labelId) {
|
|
70442
|
+
const mutation = `mutation RemoveLabel($id: String!, $labelId: String!) {
|
|
70443
|
+
issueRemoveLabel(id: $id, labelId: $labelId) { success }
|
|
70444
|
+
}`;
|
|
70445
|
+
await linearRequest(apiKey, mutation, {
|
|
70446
|
+
id: issueId,
|
|
70447
|
+
labelId
|
|
70448
|
+
});
|
|
70449
|
+
}
|
|
70409
70450
|
|
|
70410
70451
|
// apps/cli/src/agent/coordinator.ts
|
|
70411
70452
|
class AgentCoordinator {
|
|
@@ -70415,6 +70456,7 @@ class AgentCoordinator {
|
|
|
70415
70456
|
pendingIds = new Set;
|
|
70416
70457
|
queue = [];
|
|
70417
70458
|
stopped = false;
|
|
70459
|
+
conflictNotified = new Set;
|
|
70418
70460
|
constructor(deps, opts) {
|
|
70419
70461
|
this.deps = deps;
|
|
70420
70462
|
this.opts = opts;
|
|
@@ -70432,58 +70474,86 @@ class AgentCoordinator {
|
|
|
70432
70474
|
async pollOnce() {
|
|
70433
70475
|
if (this.stopped)
|
|
70434
70476
|
return { found: 0, added: 0 };
|
|
70435
|
-
let
|
|
70477
|
+
let todo = [];
|
|
70478
|
+
let inProgress = [];
|
|
70479
|
+
let conflicted = [];
|
|
70436
70480
|
try {
|
|
70437
|
-
|
|
70481
|
+
[todo, inProgress, conflicted] = await Promise.all([
|
|
70482
|
+
this.deps.fetchTodo(),
|
|
70483
|
+
this.deps.fetchInProgress(),
|
|
70484
|
+
this.deps.fetchConflicted()
|
|
70485
|
+
]);
|
|
70438
70486
|
} catch (err) {
|
|
70439
70487
|
this.deps.onLog(`! Linear poll failed: ${err.message}`, "red");
|
|
70440
70488
|
return { found: 0, added: 0 };
|
|
70441
70489
|
}
|
|
70442
|
-
const
|
|
70443
|
-
const
|
|
70444
|
-
|
|
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));
|
|
70490
|
+
const queuedIds = new Set(this.queue.map((q) => q.issue.id));
|
|
70491
|
+
const activeIds = new Set(this.workers.map((w) => w.issueId));
|
|
70492
|
+
const eligible = (id) => !queuedIds.has(id) && !activeIds.has(id) && !this.pendingIds.has(id);
|
|
70451
70493
|
let added = 0;
|
|
70452
|
-
for (const issue of
|
|
70453
|
-
if (
|
|
70454
|
-
continue;
|
|
70455
|
-
if (isFailed(issue.id))
|
|
70494
|
+
for (const issue of inProgress) {
|
|
70495
|
+
if (!eligible(issue.id))
|
|
70456
70496
|
continue;
|
|
70457
|
-
if (
|
|
70497
|
+
if (!this.dependenciesResolved(issue))
|
|
70458
70498
|
continue;
|
|
70459
|
-
|
|
70499
|
+
this.queue.push({ issue, mode: "resume" });
|
|
70500
|
+
queuedIds.add(issue.id);
|
|
70501
|
+
added += 1;
|
|
70502
|
+
}
|
|
70503
|
+
for (const issue of conflicted) {
|
|
70504
|
+
if (!eligible(issue.id))
|
|
70460
70505
|
continue;
|
|
70461
|
-
|
|
70506
|
+
this.queue.push({ issue, mode: "conflict-fix" });
|
|
70507
|
+
queuedIds.add(issue.id);
|
|
70508
|
+
added += 1;
|
|
70509
|
+
}
|
|
70510
|
+
for (const issue of todo) {
|
|
70511
|
+
if (!eligible(issue.id))
|
|
70462
70512
|
continue;
|
|
70463
|
-
|
|
70464
|
-
if (blocker !== undefined) {
|
|
70465
|
-
this.deps.onLog(` \u23F8 ${issue.identifier} skipped \u2014 blocked by unresolved dependency`, "yellow");
|
|
70513
|
+
if (!this.dependenciesResolved(issue))
|
|
70466
70514
|
continue;
|
|
70467
|
-
}
|
|
70468
|
-
|
|
70515
|
+
this.queue.push({ issue, mode: "fresh" });
|
|
70516
|
+
queuedIds.add(issue.id);
|
|
70469
70517
|
added += 1;
|
|
70470
70518
|
}
|
|
70471
70519
|
if (added > 0) {
|
|
70520
|
+
const modeRank = {
|
|
70521
|
+
resume: 0,
|
|
70522
|
+
"conflict-fix": 1,
|
|
70523
|
+
fresh: 2
|
|
70524
|
+
};
|
|
70472
70525
|
this.queue.sort((a, b) => {
|
|
70473
|
-
const pa = a.priority === 0 ? Infinity : a.priority;
|
|
70474
|
-
const pb = b.priority === 0 ? Infinity : b.priority;
|
|
70475
|
-
|
|
70526
|
+
const pa = a.issue.priority === 0 ? Infinity : a.issue.priority;
|
|
70527
|
+
const pb = b.issue.priority === 0 ? Infinity : b.issue.priority;
|
|
70528
|
+
if (pa !== pb)
|
|
70529
|
+
return pa - pb;
|
|
70530
|
+
return modeRank[a.mode] - modeRank[b.mode];
|
|
70476
70531
|
});
|
|
70477
70532
|
}
|
|
70478
|
-
await this.deps.store.setLastPollAt(new Date().toISOString());
|
|
70479
70533
|
this.spawnNext();
|
|
70534
|
+
await this.scanDoneForConflicts();
|
|
70480
70535
|
await this.reportProgress();
|
|
70481
|
-
|
|
70536
|
+
const found = todo.length + inProgress.length + conflicted.length;
|
|
70537
|
+
return { found, added };
|
|
70538
|
+
}
|
|
70539
|
+
dependenciesResolved(issue) {
|
|
70540
|
+
if (issue.blockedByIds.length === 0)
|
|
70541
|
+
return true;
|
|
70542
|
+
const openIds = new Set([
|
|
70543
|
+
...this.queue.map((q) => q.issue.id),
|
|
70544
|
+
...this.workers.map((w) => w.issueId)
|
|
70545
|
+
]);
|
|
70546
|
+
const blocker = issue.blockedByIds.find((bid) => openIds.has(bid));
|
|
70547
|
+
if (blocker !== undefined) {
|
|
70548
|
+
this.deps.onLog(` \u23F8 ${issue.identifier} skipped \u2014 blocked by unresolved dependency`, "yellow");
|
|
70549
|
+
return false;
|
|
70550
|
+
}
|
|
70551
|
+
this.deps.onLog(` \u23F8 ${issue.identifier} skipped \u2014 blocked by unresolved dependency`, "yellow");
|
|
70552
|
+
return false;
|
|
70482
70553
|
}
|
|
70483
70554
|
async reportProgress() {
|
|
70484
|
-
const updater = this.deps.updater;
|
|
70485
70555
|
const everyN = this.opts.commentEveryIterations ?? 0;
|
|
70486
|
-
if (everyN <= 0 ||
|
|
70556
|
+
if (everyN <= 0 || this.opts.postComments === false || !this.deps.getIterationCount) {
|
|
70487
70557
|
return;
|
|
70488
70558
|
}
|
|
70489
70559
|
for (const w of this.workers) {
|
|
@@ -70501,29 +70571,78 @@ class AgentCoordinator {
|
|
|
70501
70571
|
if (currMilestone <= lastMilestone)
|
|
70502
70572
|
continue;
|
|
70503
70573
|
try {
|
|
70504
|
-
await
|
|
70574
|
+
await this.deps.postComment(w.issue, `\uD83D\uDD04 Ralph progress update: iteration ${count} on \`${w.changeName}\``);
|
|
70505
70575
|
w.lastReportedIteration = count;
|
|
70506
70576
|
} catch (err) {
|
|
70507
70577
|
this.deps.onLog(`! Linear progress comment failed for ${w.issueIdentifier}: ${err.message}`, "red");
|
|
70508
70578
|
}
|
|
70509
70579
|
}
|
|
70510
70580
|
}
|
|
70581
|
+
async scanDoneForConflicts() {
|
|
70582
|
+
if (!this.opts.setConflicted)
|
|
70583
|
+
return;
|
|
70584
|
+
let candidates = [];
|
|
70585
|
+
try {
|
|
70586
|
+
candidates = await this.deps.fetchDoneCandidates();
|
|
70587
|
+
} catch (err) {
|
|
70588
|
+
this.deps.onLog(`! conflict scan fetch failed: ${err.message}`, "yellow");
|
|
70589
|
+
return;
|
|
70590
|
+
}
|
|
70591
|
+
if (candidates.length === 0)
|
|
70592
|
+
return;
|
|
70593
|
+
for (const issue of candidates) {
|
|
70594
|
+
if (this.workers.some((w) => w.issueId === issue.id))
|
|
70595
|
+
continue;
|
|
70596
|
+
if (this.pendingIds.has(issue.id))
|
|
70597
|
+
continue;
|
|
70598
|
+
if (this.queue.some((q) => q.issue.id === issue.id))
|
|
70599
|
+
continue;
|
|
70600
|
+
let pr;
|
|
70601
|
+
try {
|
|
70602
|
+
pr = await this.deps.checkPrConflict(issue);
|
|
70603
|
+
} catch (err) {
|
|
70604
|
+
this.deps.onLog(`! PR conflict check failed for ${issue.identifier}: ${err.message}`, "yellow");
|
|
70605
|
+
continue;
|
|
70606
|
+
}
|
|
70607
|
+
if (!pr || !pr.conflicting)
|
|
70608
|
+
continue;
|
|
70609
|
+
const alreadyNotified = this.conflictNotified.has(issue.id);
|
|
70610
|
+
if (alreadyNotified)
|
|
70611
|
+
continue;
|
|
70612
|
+
try {
|
|
70613
|
+
await this.deps.applyIndicator(issue, this.opts.setConflicted);
|
|
70614
|
+
} catch (err) {
|
|
70615
|
+
this.deps.onLog(`! Linear setConflicted failed for ${issue.identifier}: ${err.message}`, "red");
|
|
70616
|
+
continue;
|
|
70617
|
+
}
|
|
70618
|
+
this.conflictNotified.add(issue.id);
|
|
70619
|
+
if (this.opts.postComments !== false) {
|
|
70620
|
+
try {
|
|
70621
|
+
await this.deps.postComment(issue, `\u26A0 Ralph detected merge conflicts on this PR (${pr.url}) \u2014 re-running to resolve`);
|
|
70622
|
+
} catch (err) {
|
|
70623
|
+
this.deps.onLog(`! Linear conflict comment failed for ${issue.identifier}: ${err.message}`, "yellow");
|
|
70624
|
+
}
|
|
70625
|
+
}
|
|
70626
|
+
this.queue.push({ issue, mode: "conflict-fix" });
|
|
70627
|
+
}
|
|
70628
|
+
this.spawnNext();
|
|
70629
|
+
}
|
|
70511
70630
|
spawnNext() {
|
|
70512
70631
|
if (this.stopped)
|
|
70513
70632
|
return;
|
|
70514
70633
|
while (this.workers.length + this.pendingIds.size < this.opts.concurrency && this.queue.length > 0) {
|
|
70515
|
-
const
|
|
70516
|
-
this.pendingIds.add(issue.id);
|
|
70517
|
-
this.launchWorker(issue);
|
|
70634
|
+
const next = this.queue.shift();
|
|
70635
|
+
this.pendingIds.add(next.issue.id);
|
|
70636
|
+
this.launchWorker(next.issue, next.mode);
|
|
70518
70637
|
}
|
|
70519
70638
|
}
|
|
70520
|
-
async launchWorker(issue) {
|
|
70521
|
-
let
|
|
70639
|
+
async launchWorker(issue, mode) {
|
|
70640
|
+
let prep;
|
|
70522
70641
|
try {
|
|
70523
|
-
|
|
70642
|
+
prep = await this.deps.prepare(issue, mode);
|
|
70524
70643
|
} catch (err) {
|
|
70525
70644
|
this.pendingIds.delete(issue.id);
|
|
70526
|
-
this.deps.onLog(`!
|
|
70645
|
+
this.deps.onLog(`! prepare(${mode}) failed for ${issue.identifier}: ${err.message}`, "red");
|
|
70527
70646
|
this.spawnNext();
|
|
70528
70647
|
return;
|
|
70529
70648
|
}
|
|
@@ -70531,113 +70650,89 @@ class AgentCoordinator {
|
|
|
70531
70650
|
this.pendingIds.delete(issue.id);
|
|
70532
70651
|
return;
|
|
70533
70652
|
}
|
|
70534
|
-
{
|
|
70535
|
-
|
|
70536
|
-
|
|
70537
|
-
|
|
70538
|
-
|
|
70539
|
-
|
|
70540
|
-
|
|
70653
|
+
if (mode === "fresh" && this.opts.setInProgress) {
|
|
70654
|
+
try {
|
|
70655
|
+
await this.deps.applyIndicator(issue, this.opts.setInProgress);
|
|
70656
|
+
} catch (err) {
|
|
70657
|
+
this.deps.onLog(`! Linear setInProgress failed for ${issue.identifier}: ${err.message}`, "yellow");
|
|
70658
|
+
}
|
|
70659
|
+
}
|
|
70660
|
+
if (mode === "fresh" && this.opts.postComments !== false) {
|
|
70661
|
+
let alreadyPosted = false;
|
|
70662
|
+
try {
|
|
70663
|
+
const comments = await this.deps.fetchComments(issue.id);
|
|
70664
|
+
alreadyPosted = comments.some((c) => c.body.startsWith("\uD83E\uDD16 Ralph started working"));
|
|
70665
|
+
} catch (err) {
|
|
70666
|
+
this.deps.onLog(`! Linear comment fetch failed for ${issue.identifier}: ${err.message}`, "yellow");
|
|
70667
|
+
}
|
|
70668
|
+
if (!alreadyPosted) {
|
|
70669
|
+
try {
|
|
70670
|
+
await this.deps.postComment(issue, `\uD83E\uDD16 Ralph started working on this issue. Tracking change: \`${prep.changeName}\``);
|
|
70671
|
+
} catch (err) {
|
|
70672
|
+
this.deps.onLog(`! Linear comment failed for ${issue.identifier}: ${err.message}`, "red");
|
|
70673
|
+
}
|
|
70674
|
+
}
|
|
70541
70675
|
}
|
|
70542
|
-
this.deps.onLog(`\u25B6 ${issue.identifier} \u2192 ${changeName} (
|
|
70543
|
-
const handle = this.deps.spawnWorker(changeName, issue);
|
|
70676
|
+
this.deps.onLog(`\u25B6 ${issue.identifier} \u2192 ${prep.changeName} (${mode})`, mode === "conflict-fix" ? "yellow" : "cyan");
|
|
70677
|
+
const handle = this.deps.spawnWorker(prep.changeName, issue);
|
|
70544
70678
|
const worker = {
|
|
70545
|
-
changeName,
|
|
70679
|
+
changeName: prep.changeName,
|
|
70546
70680
|
issueId: issue.id,
|
|
70547
70681
|
issueIdentifier: issue.identifier,
|
|
70548
70682
|
issue,
|
|
70683
|
+
mode,
|
|
70549
70684
|
kill: handle.kill,
|
|
70550
70685
|
lastReportedIteration: 0
|
|
70551
70686
|
};
|
|
70552
70687
|
this.workers.push(worker);
|
|
70553
70688
|
this.pendingIds.delete(issue.id);
|
|
70554
70689
|
this.deps.onWorkersChanged();
|
|
70555
|
-
|
|
70556
|
-
handle.exited.then((code) => {
|
|
70690
|
+
handle.exited.then(async (code) => {
|
|
70557
70691
|
const idx = this.workers.indexOf(worker);
|
|
70558
70692
|
if (idx >= 0)
|
|
70559
70693
|
this.workers.splice(idx, 1);
|
|
70560
70694
|
const ok = code === 0;
|
|
70561
|
-
this.deps.onLog(`${ok ? "\u2713" : "\u2717"} ${issue.identifier} \u2192 ${changeName} exited (code ${code})`, ok ? "green" : "red");
|
|
70562
|
-
this.
|
|
70563
|
-
state: ok ? "processed" : "failed",
|
|
70564
|
-
finishedAt: new Date().toISOString(),
|
|
70565
|
-
exitCode: code
|
|
70566
|
-
});
|
|
70567
|
-
this.notifyExited(issue, changeName, code);
|
|
70695
|
+
this.deps.onLog(`${ok ? "\u2713" : "\u2717"} ${issue.identifier} \u2192 ${prep.changeName} exited (code ${code})`, ok ? "green" : "red");
|
|
70696
|
+
await this.notifyExited(issue, prep.changeName, code, mode);
|
|
70568
70697
|
this.deps.onWorkersChanged();
|
|
70569
70698
|
this.spawnNext();
|
|
70570
70699
|
});
|
|
70571
70700
|
}
|
|
70572
|
-
async
|
|
70573
|
-
const updater = this.deps.updater;
|
|
70574
|
-
if (!updater)
|
|
70575
|
-
return;
|
|
70576
|
-
const alreadyCommented = this.deps.store.snapshot().tasks[issue.identifier]?.commentPosted === true;
|
|
70577
|
-
if (this.opts.postComments !== false && !alreadyCommented) {
|
|
70578
|
-
try {
|
|
70579
|
-
await updater.postComment(issue, `\uD83E\uDD16 Ralph started working on this issue. Tracking change: \`${changeName}\``);
|
|
70580
|
-
await this.deps.store.upsertTask(issue, { commentPosted: true });
|
|
70581
|
-
} catch (err) {
|
|
70582
|
-
this.deps.onLog(`! Linear comment failed for ${issue.identifier}: ${err.message}`, "red");
|
|
70583
|
-
}
|
|
70584
|
-
}
|
|
70585
|
-
if (this.opts.inProgressStatus) {
|
|
70586
|
-
await this.moveIssue(issue, this.opts.inProgressStatus);
|
|
70587
|
-
}
|
|
70588
|
-
}
|
|
70589
|
-
async notifyExited(issue, changeName, code) {
|
|
70590
|
-
const updater = this.deps.updater;
|
|
70591
|
-
if (!updater)
|
|
70592
|
-
return;
|
|
70701
|
+
async notifyExited(issue, changeName, code, mode) {
|
|
70593
70702
|
const ok = code === 0;
|
|
70594
70703
|
if (this.opts.postComments !== false) {
|
|
70595
|
-
const body = ok ? `\u2705 Ralph completed work on this issue. Change: \`${changeName}\`` : `\u2717 Ralph exited with code ${code} on this issue. Change: \`${changeName}\`
|
|
70704
|
+
const body = ok ? mode === "conflict-fix" ? `\u2705 Ralph resolved merge conflicts on this issue. Change: \`${changeName}\`` : `\u2705 Ralph completed work on this issue. Change: \`${changeName}\`` : `\u2717 Ralph exited with code ${code} on this issue. Change: \`${changeName}\`
|
|
70596
70705
|
|
|
70597
|
-
` + `This issue has been quarantined and will not be auto-resumed on the next poll. ` + `Inspect the worktree at \`~/.ralph/<project>/worktrees/${changeName}\`, fix the
|
|
70706
|
+
` + `This issue has been quarantined and will not be auto-resumed on the next poll. ` + `Inspect the worktree at \`~/.ralph/<project>/worktrees/${changeName}\`, fix the ` + `underlying failure, then remove the error marker on this Linear issue (or run ` + `\`ralph clean --name ${changeName}\`) to clear the quarantine.`;
|
|
70598
70707
|
try {
|
|
70599
|
-
await
|
|
70708
|
+
await this.deps.postComment(issue, body);
|
|
70600
70709
|
} catch (err) {
|
|
70601
70710
|
this.deps.onLog(`! Linear comment failed for ${issue.identifier}: ${err.message}`, "red");
|
|
70602
70711
|
}
|
|
70603
70712
|
}
|
|
70604
|
-
if (ok
|
|
70605
|
-
|
|
70606
|
-
|
|
70607
|
-
|
|
70608
|
-
|
|
70609
|
-
|
|
70610
|
-
|
|
70611
|
-
|
|
70612
|
-
|
|
70613
|
-
|
|
70614
|
-
|
|
70615
|
-
|
|
70616
|
-
|
|
70617
|
-
|
|
70618
|
-
|
|
70619
|
-
|
|
70620
|
-
this.deps.onLog(`! Linear label '${labelName}' not found for ${issue.identifier}`, "yellow");
|
|
70621
|
-
return;
|
|
70713
|
+
if (ok) {
|
|
70714
|
+
if (mode === "conflict-fix") {
|
|
70715
|
+
if (this.opts.clearConflicted) {
|
|
70716
|
+
try {
|
|
70717
|
+
await this.deps.removeIndicator(issue, this.opts.clearConflicted);
|
|
70718
|
+
} catch (err) {
|
|
70719
|
+
this.deps.onLog(`! Linear clearConflicted failed for ${issue.identifier}: ${err.message}`, "red");
|
|
70720
|
+
}
|
|
70721
|
+
}
|
|
70722
|
+
this.conflictNotified.delete(issue.id);
|
|
70723
|
+
} else if (this.opts.setDone) {
|
|
70724
|
+
try {
|
|
70725
|
+
await this.deps.applyIndicator(issue, this.opts.setDone);
|
|
70726
|
+
} catch (err) {
|
|
70727
|
+
this.deps.onLog(`! Linear setDone failed for ${issue.identifier}: ${err.message}`, "red");
|
|
70728
|
+
}
|
|
70622
70729
|
}
|
|
70623
|
-
|
|
70624
|
-
|
|
70625
|
-
|
|
70626
|
-
|
|
70627
|
-
|
|
70628
|
-
}
|
|
70629
|
-
async moveIssue(issue, stateName) {
|
|
70630
|
-
const updater = this.deps.updater;
|
|
70631
|
-
try {
|
|
70632
|
-
const stateId = await updater.resolveStateId(issue, stateName);
|
|
70633
|
-
if (!stateId) {
|
|
70634
|
-
this.deps.onLog(`! Linear state '${stateName}' not found for ${issue.identifier}`, "yellow");
|
|
70635
|
-
return;
|
|
70730
|
+
} else if (this.opts.setError) {
|
|
70731
|
+
try {
|
|
70732
|
+
await this.deps.applyIndicator(issue, this.opts.setError);
|
|
70733
|
+
} catch (err) {
|
|
70734
|
+
this.deps.onLog(`! Linear setError failed for ${issue.identifier}: ${err.message}`, "red");
|
|
70636
70735
|
}
|
|
70637
|
-
await updater.setState(issue, stateId);
|
|
70638
|
-
this.deps.onLog(` \u2192 ${issue.identifier} moved to '${stateName}'`, "gray");
|
|
70639
|
-
} catch (err) {
|
|
70640
|
-
this.deps.onLog(`! Linear state move failed for ${issue.identifier}: ${err.message}`, "red");
|
|
70641
70736
|
}
|
|
70642
70737
|
}
|
|
70643
70738
|
stop() {
|
|
@@ -70651,7 +70746,7 @@ class AgentCoordinator {
|
|
|
70651
70746
|
}
|
|
70652
70747
|
|
|
70653
70748
|
// apps/cli/src/agent/scaffold.ts
|
|
70654
|
-
import { join as
|
|
70749
|
+
import { join as join12 } from "path";
|
|
70655
70750
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
70656
70751
|
function changeNameForIssue(issue) {
|
|
70657
70752
|
const slug = issue.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
@@ -70659,10 +70754,10 @@ function changeNameForIssue(issue) {
|
|
|
70659
70754
|
}
|
|
70660
70755
|
async function scaffoldChangeForIssue(tasksDir, statesDir, issue, comments = [], appendPrompt = "") {
|
|
70661
70756
|
const name = changeNameForIssue(issue);
|
|
70662
|
-
const changeDir =
|
|
70663
|
-
const stateDir =
|
|
70757
|
+
const changeDir = join12(tasksDir, name);
|
|
70758
|
+
const stateDir = join12(statesDir, name);
|
|
70664
70759
|
await mkdir2(changeDir, { recursive: true });
|
|
70665
|
-
await mkdir2(
|
|
70760
|
+
await mkdir2(join12(changeDir, "specs"), { recursive: true });
|
|
70666
70761
|
await mkdir2(stateDir, { recursive: true });
|
|
70667
70762
|
const commentsBlock = comments.length > 0 ? [
|
|
70668
70763
|
"",
|
|
@@ -70714,25 +70809,25 @@ async function scaffoldChangeForIssue(tasksDir, statesDir, issue, comments = [],
|
|
|
70714
70809
|
""
|
|
70715
70810
|
].join(`
|
|
70716
70811
|
`);
|
|
70717
|
-
await Bun.write(
|
|
70718
|
-
await Bun.write(
|
|
70719
|
-
await Bun.write(
|
|
70812
|
+
await Bun.write(join12(changeDir, "proposal.md"), proposal);
|
|
70813
|
+
await Bun.write(join12(changeDir, "tasks.md"), tasks);
|
|
70814
|
+
await Bun.write(join12(changeDir, "design.md"), design);
|
|
70720
70815
|
return name;
|
|
70721
70816
|
}
|
|
70722
70817
|
|
|
70723
70818
|
// apps/cli/src/agent/worktree.ts
|
|
70724
|
-
import { basename, join as
|
|
70819
|
+
import { basename, join as join13 } from "path";
|
|
70725
70820
|
import { homedir as homedir2 } from "os";
|
|
70726
70821
|
import { exists } from "fs/promises";
|
|
70727
70822
|
function worktreesDir(projectRoot) {
|
|
70728
|
-
return
|
|
70823
|
+
return join13(homedir2(), ".ralph", basename(projectRoot), "worktrees");
|
|
70729
70824
|
}
|
|
70730
70825
|
function branchForChange(changeName) {
|
|
70731
70826
|
return `ralph/${changeName}`;
|
|
70732
70827
|
}
|
|
70733
70828
|
async function createWorktree(projectRoot, changeName, runner) {
|
|
70734
70829
|
const dir = worktreesDir(projectRoot);
|
|
70735
|
-
const cwd2 =
|
|
70830
|
+
const cwd2 = join13(dir, changeName);
|
|
70736
70831
|
const branch = branchForChange(changeName);
|
|
70737
70832
|
const list = await runner.run(["worktree", "list", "--porcelain"], projectRoot);
|
|
70738
70833
|
if (list.stdout.includes(`worktree ${cwd2}
|
|
@@ -70789,8 +70884,8 @@ async function isWorktreeSafeToRemove(cwd2, base2, runner) {
|
|
|
70789
70884
|
return { safe: true, dirty, unpushedCommits };
|
|
70790
70885
|
}
|
|
70791
70886
|
async function seedWorktreeMcpConfig(projectRoot, worktreeCwd) {
|
|
70792
|
-
const dst =
|
|
70793
|
-
const src =
|
|
70887
|
+
const dst = join13(worktreeCwd, ".mcp.json");
|
|
70888
|
+
const src = join13(projectRoot, ".mcp.json");
|
|
70794
70889
|
const source = await exists(dst) ? dst : await exists(src) ? src : null;
|
|
70795
70890
|
if (!source)
|
|
70796
70891
|
return;
|
|
@@ -70804,7 +70899,7 @@ async function seedWorktreeMcpConfig(projectRoot, worktreeCwd) {
|
|
|
70804
70899
|
if (servers && typeof servers === "object") {
|
|
70805
70900
|
for (const cfg of Object.values(servers)) {
|
|
70806
70901
|
if (Array.isArray(cfg.args)) {
|
|
70807
|
-
cfg.args = cfg.args.map((a) => typeof a === "string" && a.startsWith(".ralph/") ?
|
|
70902
|
+
cfg.args = cfg.args.map((a) => typeof a === "string" && a.startsWith(".ralph/") ? join13(projectRoot, a) : a);
|
|
70808
70903
|
}
|
|
70809
70904
|
}
|
|
70810
70905
|
}
|
|
@@ -70813,7 +70908,7 @@ async function seedWorktreeMcpConfig(projectRoot, worktreeCwd) {
|
|
|
70813
70908
|
}
|
|
70814
70909
|
|
|
70815
70910
|
// apps/cli/src/agent/post-task.ts
|
|
70816
|
-
import { join as
|
|
70911
|
+
import { join as join14 } from "path";
|
|
70817
70912
|
|
|
70818
70913
|
// apps/cli/src/agent/pr.ts
|
|
70819
70914
|
function defaultTitle(issue) {
|
|
@@ -70864,8 +70959,32 @@ async function createPullRequest(input, runner) {
|
|
|
70864
70959
|
|
|
70865
70960
|
// apps/cli/src/agent/ci.ts
|
|
70866
70961
|
var PR_CHECKS_FIELDS = "name,bucket,link,workflow,event";
|
|
70867
|
-
|
|
70868
|
-
|
|
70962
|
+
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;
|
|
70963
|
+
var GH_RETRY_DELAYS = [5000, 15000, 45000];
|
|
70964
|
+
async function runGhWithRetry(cmd, runner, cwd2, onRetry, sleep2 = (ms) => new Promise((r) => setTimeout(r, ms))) {
|
|
70965
|
+
let lastErr;
|
|
70966
|
+
for (let i = 0;i <= GH_RETRY_DELAYS.length; i++) {
|
|
70967
|
+
try {
|
|
70968
|
+
return await runner.run(cmd, cwd2);
|
|
70969
|
+
} catch (err) {
|
|
70970
|
+
const e = err;
|
|
70971
|
+
const blob = `${e.message}
|
|
70972
|
+
${e.stderr ?? ""}
|
|
70973
|
+
${e.stdout ?? ""}`;
|
|
70974
|
+
if (!TRANSIENT_GH_RE.test(blob) || i === GH_RETRY_DELAYS.length)
|
|
70975
|
+
throw err;
|
|
70976
|
+
const delay2 = GH_RETRY_DELAYS[i];
|
|
70977
|
+
const firstLine = (e.stderr?.trim().split(`
|
|
70978
|
+
`)[0] ?? e.message).slice(0, 120);
|
|
70979
|
+
onRetry?.(i + 1, delay2, firstLine);
|
|
70980
|
+
await sleep2(delay2);
|
|
70981
|
+
lastErr = err;
|
|
70982
|
+
}
|
|
70983
|
+
}
|
|
70984
|
+
throw lastErr;
|
|
70985
|
+
}
|
|
70986
|
+
async function getPrChecksStatus(prRef, runner, cwd2, onTransientRetry) {
|
|
70987
|
+
const out = await runGhWithRetry(["gh", "pr", "checks", prRef, "--json", PR_CHECKS_FIELDS], runner, cwd2, onTransientRetry);
|
|
70869
70988
|
const checks = JSON.parse(out.stdout || "[]").filter((c) => c.bucket !== "skipping");
|
|
70870
70989
|
if (checks.some((c) => c.bucket === "pending")) {
|
|
70871
70990
|
return { bucket: "pending", failedRunIds: [] };
|
|
@@ -70902,17 +71021,28 @@ ${truncated}`);
|
|
|
70902
71021
|
}
|
|
70903
71022
|
async function fixCiUntilGreen(deps, opts) {
|
|
70904
71023
|
for (let attempt2 = 1;attempt2 <= opts.maxAttempts; attempt2++) {
|
|
71024
|
+
let pollN = 0;
|
|
70905
71025
|
while (true) {
|
|
70906
71026
|
if (deps.cancelled?.())
|
|
70907
71027
|
return { success: false, attempts: attempt2 - 1, reason: "cancelled" };
|
|
70908
|
-
|
|
71028
|
+
pollN += 1;
|
|
71029
|
+
deps.onPhase?.("ci-poll", `attempt ${attempt2}/${opts.maxAttempts} \xB7 poll ${pollN}`);
|
|
71030
|
+
let s;
|
|
71031
|
+
try {
|
|
71032
|
+
s = await deps.getStatus();
|
|
71033
|
+
} catch (err) {
|
|
71034
|
+
deps.log(`! gh pr checks failed permanently: ${err.message} \u2014 giving up CI watch`, "red");
|
|
71035
|
+
return { success: false, attempts: attempt2 - 1, reason: "gh-failed" };
|
|
71036
|
+
}
|
|
70909
71037
|
if (s.bucket === "pass") {
|
|
70910
71038
|
deps.log(`\u2713 CI green for PR (after ${attempt2 - 1} fix attempts)`, "green");
|
|
70911
71039
|
return { success: true, attempts: attempt2 - 1 };
|
|
70912
71040
|
}
|
|
70913
71041
|
if (s.bucket === "fail") {
|
|
70914
71042
|
deps.log(`\u2717 CI failing (attempt ${attempt2}/${opts.maxAttempts}) \u2014 fetching logs and re-running task`, "yellow");
|
|
71043
|
+
deps.onPhase?.("ci-fix", `attempt ${attempt2}/${opts.maxAttempts} \xB7 fetching logs`);
|
|
70915
71044
|
const logs = await deps.getFailedLogs(s.failedRunIds);
|
|
71045
|
+
deps.onPhase?.("ci-fix", `attempt ${attempt2}/${opts.maxAttempts} \xB7 re-running worker`);
|
|
70916
71046
|
const steering = `CI is failing on this PR. Investigate and fix:
|
|
70917
71047
|
|
|
70918
71048
|
\`\`\`
|
|
@@ -70923,6 +71053,7 @@ ${logs}
|
|
|
70923
71053
|
deps.log(`! task loop exited code ${code} during CI fix attempt ${attempt2}`, "red");
|
|
70924
71054
|
}
|
|
70925
71055
|
try {
|
|
71056
|
+
deps.onPhase?.("ci-fix", `attempt ${attempt2}/${opts.maxAttempts} \xB7 pushing fix`);
|
|
70926
71057
|
await deps.pushBranch();
|
|
70927
71058
|
} catch (err) {
|
|
70928
71059
|
deps.log(`! push failed during CI fix: ${err.message}`, "red");
|
|
@@ -70930,6 +71061,7 @@ ${logs}
|
|
|
70930
71061
|
}
|
|
70931
71062
|
break;
|
|
70932
71063
|
}
|
|
71064
|
+
deps.onPhase?.("ci-poll", `attempt ${attempt2}/${opts.maxAttempts} \xB7 pending, waiting`);
|
|
70933
71065
|
await deps.sleep(opts.pollIntervalSeconds * 1000);
|
|
70934
71066
|
}
|
|
70935
71067
|
}
|
|
@@ -70957,6 +71089,7 @@ async function reactivateState(stateFilePath, log2, changeName) {
|
|
|
70957
71089
|
}
|
|
70958
71090
|
async function runPostTask(input, deps) {
|
|
70959
71091
|
const { log: log2, cmd, git, runScript } = deps;
|
|
71092
|
+
const emit = (phase, detail) => deps.onPhase?.(phase, detail);
|
|
70960
71093
|
const {
|
|
70961
71094
|
changeName,
|
|
70962
71095
|
cwd: cwd2,
|
|
@@ -70973,6 +71106,7 @@ async function runPostTask(input, deps) {
|
|
|
70973
71106
|
respawnWorker
|
|
70974
71107
|
} = input;
|
|
70975
71108
|
if (cfg.teardownScript) {
|
|
71109
|
+
emit("teardown", cfg.teardownScript);
|
|
70976
71110
|
try {
|
|
70977
71111
|
await runScript("teardown", cfg.teardownScript, cwd2);
|
|
70978
71112
|
} catch {}
|
|
@@ -70987,7 +71121,7 @@ async function runPostTask(input, deps) {
|
|
|
70987
71121
|
const maxHookFixAttempts = cfg.maxCiFixAttempts;
|
|
70988
71122
|
const runWorkerWithFixTask = async (heading, failureOutput) => {
|
|
70989
71123
|
try {
|
|
70990
|
-
await prependFixTask(
|
|
71124
|
+
await prependFixTask(join14(changeDir, "tasks.md"), heading, failureOutput);
|
|
70991
71125
|
} catch (err) {
|
|
70992
71126
|
log2(`! could not prepend fix task: ${err.message}`, "red");
|
|
70993
71127
|
return 1;
|
|
@@ -70998,6 +71132,7 @@ async function runPostTask(input, deps) {
|
|
|
70998
71132
|
let hookFixAttempt = 0;
|
|
70999
71133
|
let commitGaveUp = false;
|
|
71000
71134
|
while (true) {
|
|
71135
|
+
emit("committing", "git status");
|
|
71001
71136
|
let dirty = "";
|
|
71002
71137
|
try {
|
|
71003
71138
|
const status = await cmd.run(["git", "status", "--porcelain"], cwd2);
|
|
@@ -71009,7 +71144,9 @@ async function runPostTask(input, deps) {
|
|
|
71009
71144
|
if (!dirty)
|
|
71010
71145
|
break;
|
|
71011
71146
|
try {
|
|
71147
|
+
emit("committing", "git add -A");
|
|
71012
71148
|
await cmd.run(["git", "add", "-A"], cwd2);
|
|
71149
|
+
emit("committing", "git commit");
|
|
71013
71150
|
await cmd.run(["git", "commit", "-m", `chore(ralph): residual changes for ${changeName}`], cwd2);
|
|
71014
71151
|
log2(` committed residual changes for ${changeName}`, "gray");
|
|
71015
71152
|
break;
|
|
@@ -71028,6 +71165,7 @@ ${e.stderr ?? ""}`;
|
|
|
71028
71165
|
break;
|
|
71029
71166
|
}
|
|
71030
71167
|
hookFixAttempt += 1;
|
|
71168
|
+
emit("commit-retry", `${hookFixAttempt}/${maxHookFixAttempts}`);
|
|
71031
71169
|
log2(`! commit rejected for ${changeName} \u2014 prepending fix task and re-running loop (attempt ${hookFixAttempt}/${maxHookFixAttempts})`, "yellow");
|
|
71032
71170
|
log2(` detail: ${detail}`, "yellow");
|
|
71033
71171
|
const retryCode = await runWorkerWithFixTask("Fix host pre-commit hook rejection", `Committing residual changes was rejected by the host repo's pre-commit hook. ` + `Fix the underlying problem, then the commit will be retried.
|
|
@@ -71043,8 +71181,10 @@ ${e.stderr ?? ""}`;
|
|
|
71043
71181
|
}
|
|
71044
71182
|
let pr = null;
|
|
71045
71183
|
let prGaveUp = commitGaveUp;
|
|
71184
|
+
let nonFfRebaseAttempted = false;
|
|
71046
71185
|
while (!prGaveUp) {
|
|
71047
71186
|
try {
|
|
71187
|
+
emit("pr-create", "git push + gh pr create");
|
|
71048
71188
|
pr = await createPullRequest({ cwd: cwd2, branch, issue, base: cfg.prBaseBranch }, cmd);
|
|
71049
71189
|
break;
|
|
71050
71190
|
} catch (err) {
|
|
@@ -71052,8 +71192,69 @@ ${e.stderr ?? ""}`;
|
|
|
71052
71192
|
const detail = e.stderr?.trim() || e.message;
|
|
71053
71193
|
const combined = `${e.stdout ?? ""}
|
|
71054
71194
|
${e.stderr ?? ""}`;
|
|
71055
|
-
const
|
|
71056
|
-
|
|
71195
|
+
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);
|
|
71196
|
+
const isHookReject = /pre-push hook|hook declined/i.test(combined);
|
|
71197
|
+
const pushRejected = isHookReject || /failed to push some refs/i.test(combined);
|
|
71198
|
+
if (isNonFastForward && !nonFfRebaseAttempted) {
|
|
71199
|
+
nonFfRebaseAttempted = true;
|
|
71200
|
+
emit("rebasing", `git pull --rebase origin ${branch}`);
|
|
71201
|
+
log2(` non-fast-forward push for ${changeName} \u2014 rebasing onto origin/${branch}`, "yellow");
|
|
71202
|
+
try {
|
|
71203
|
+
await cmd.run(["git", "fetch", "origin", branch], cwd2);
|
|
71204
|
+
await cmd.run(["git", "pull", "--rebase", "origin", branch], cwd2);
|
|
71205
|
+
continue;
|
|
71206
|
+
} catch (rebaseErr) {
|
|
71207
|
+
const re = rebaseErr;
|
|
71208
|
+
const reBlob = `${re.stdout ?? ""}
|
|
71209
|
+
${re.stderr ?? ""}`;
|
|
71210
|
+
const isConflict = /CONFLICT|Merge conflict|could not apply|both modified/i.test(reBlob);
|
|
71211
|
+
if (!isConflict) {
|
|
71212
|
+
log2(`! rebase failed for ${changeName}: ${rebaseErr.message} \u2014 giving up`, "red");
|
|
71213
|
+
effectiveCode = PR_FAILED_EXIT;
|
|
71214
|
+
prGaveUp = true;
|
|
71215
|
+
break;
|
|
71216
|
+
}
|
|
71217
|
+
emit("rebasing", "conflicts detected \u2014 aborting + queueing fix task");
|
|
71218
|
+
try {
|
|
71219
|
+
await cmd.run(["git", "rebase", "--abort"], cwd2);
|
|
71220
|
+
} catch {}
|
|
71221
|
+
let conflictedFiles = "";
|
|
71222
|
+
try {
|
|
71223
|
+
const r = await cmd.run(["git", "diff", "--name-only", `HEAD..origin/${branch}`], cwd2);
|
|
71224
|
+
conflictedFiles = r.stdout.trim();
|
|
71225
|
+
} catch {}
|
|
71226
|
+
if (hookFixAttempt >= maxHookFixAttempts) {
|
|
71227
|
+
log2(`! merge conflict on rebase of ${branch} after ${hookFixAttempt} attempts \u2014 worktree preserved at ${cwd2}`, "red");
|
|
71228
|
+
log2(` detail: ${reBlob.trim().split(`
|
|
71229
|
+
`).slice(0, 8).join(`
|
|
71230
|
+
`)}`, "red");
|
|
71231
|
+
effectiveCode = PR_FAILED_EXIT;
|
|
71232
|
+
prGaveUp = true;
|
|
71233
|
+
break;
|
|
71234
|
+
}
|
|
71235
|
+
hookFixAttempt += 1;
|
|
71236
|
+
emit("rebasing", `conflict-fix ${hookFixAttempt}/${maxHookFixAttempts}`);
|
|
71237
|
+
log2(`! merge conflict rebasing ${branch} \u2014 prepending fix task and re-running loop (attempt ${hookFixAttempt}/${maxHookFixAttempts})`, "yellow");
|
|
71238
|
+
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.
|
|
71239
|
+
|
|
71240
|
+
` + `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.
|
|
71241
|
+
|
|
71242
|
+
` + (conflictedFiles ? `Files that differ between your branch and origin/${branch}:
|
|
71243
|
+
${conflictedFiles}
|
|
71244
|
+
|
|
71245
|
+
` : "") + `Rebase output:
|
|
71246
|
+
${reBlob.trim()}`);
|
|
71247
|
+
if (retryCode2 !== 0) {
|
|
71248
|
+
log2(`! worker re-run after merge conflict exited code ${retryCode2} \u2014 giving up`, "red");
|
|
71249
|
+
effectiveCode = PR_FAILED_EXIT;
|
|
71250
|
+
prGaveUp = true;
|
|
71251
|
+
break;
|
|
71252
|
+
}
|
|
71253
|
+
nonFfRebaseAttempted = false;
|
|
71254
|
+
continue;
|
|
71255
|
+
}
|
|
71256
|
+
}
|
|
71257
|
+
if (!isHookReject || hookFixAttempt >= maxHookFixAttempts) {
|
|
71057
71258
|
if (pushRejected) {
|
|
71058
71259
|
log2(`! push rejected for ${changeName} after ${hookFixAttempt} hook-fix attempts (host pre-push hook still failing) \u2014 worktree preserved at ${cwd2}`, "red");
|
|
71059
71260
|
log2(` detail: ${detail}`, "red");
|
|
@@ -71065,6 +71266,7 @@ ${e.stderr ?? ""}`;
|
|
|
71065
71266
|
break;
|
|
71066
71267
|
}
|
|
71067
71268
|
hookFixAttempt += 1;
|
|
71269
|
+
emit("push-retry", `${hookFixAttempt}/${maxHookFixAttempts}`);
|
|
71068
71270
|
log2(`! push rejected for ${changeName} \u2014 prepending fix task and re-running loop (attempt ${hookFixAttempt}/${maxHookFixAttempts})`, "yellow");
|
|
71069
71271
|
log2(` detail: ${detail}`, "yellow");
|
|
71070
71272
|
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.
|
|
@@ -71082,14 +71284,17 @@ ${e.stderr ?? ""}`;
|
|
|
71082
71284
|
log2(` no commits ahead of ${cfg.prBaseBranch} \u2014 skipping PR`, "gray");
|
|
71083
71285
|
} else {
|
|
71084
71286
|
log2(` ${pr.created ? "opened" : "found existing"} PR: ${pr.url}`, "green");
|
|
71287
|
+
deps.registerPr?.(changeName, pr.url);
|
|
71085
71288
|
if (wantFixCi) {
|
|
71086
71289
|
log2(` watching CI for ${pr.url} (max ${cfg.maxCiFixAttempts} fix attempts)`, "gray");
|
|
71290
|
+
emit("ci-poll", "starting");
|
|
71087
71291
|
const result2 = await fixCiUntilGreen({
|
|
71088
|
-
|
|
71292
|
+
onPhase: (p, d) => emit(p, d),
|
|
71293
|
+
getStatus: () => getPrChecksStatus(pr.url, cmd, cwd2, (n, ms, why) => log2(` gh transient (try ${n}) \u2014 retry in ${Math.round(ms / 1000)}s \xB7 ${why}`, "yellow")),
|
|
71089
71294
|
getFailedLogs: (ids) => fetchFailedRunLogs(ids, cmd, cwd2),
|
|
71090
71295
|
runTaskWithSteering: async (steering) => {
|
|
71091
71296
|
try {
|
|
71092
|
-
await prependFixTask(
|
|
71297
|
+
await prependFixTask(join14(changeDir, "tasks.md"), "Fix failing CI checks", steering);
|
|
71093
71298
|
} catch (err) {
|
|
71094
71299
|
log2(`! could not prepend fix task: ${err.message}`, "red");
|
|
71095
71300
|
}
|
|
@@ -71112,7 +71317,12 @@ ${e.stderr ?? ""}`;
|
|
|
71112
71317
|
}
|
|
71113
71318
|
}
|
|
71114
71319
|
}
|
|
71320
|
+
if (effectiveCode === 0)
|
|
71321
|
+
emit("done");
|
|
71322
|
+
else
|
|
71323
|
+
emit("gave-up", `exit ${effectiveCode}`);
|
|
71115
71324
|
if (useWorktree && cwd2 !== projectRoot) {
|
|
71325
|
+
emit("cleanup", "checking worktree safety");
|
|
71116
71326
|
if (effectiveCode === 0 && cfg.cleanupWorktreeOnSuccess) {
|
|
71117
71327
|
const check = await isWorktreeSafeToRemove(cwd2, cfg.prBaseBranch, git).catch((err) => ({
|
|
71118
71328
|
safe: false,
|
|
@@ -71176,6 +71386,50 @@ var bunCmdRunner = {
|
|
|
71176
71386
|
return { stdout, stderr };
|
|
71177
71387
|
}
|
|
71178
71388
|
};
|
|
71389
|
+
function traceCmdRunner(base2, onStart, onEnd) {
|
|
71390
|
+
return {
|
|
71391
|
+
run: async (cmd, cwd2) => {
|
|
71392
|
+
const t0 = Date.now();
|
|
71393
|
+
onStart(cmd);
|
|
71394
|
+
try {
|
|
71395
|
+
const r = await base2.run(cmd, cwd2);
|
|
71396
|
+
onEnd(cmd, Date.now() - t0, true);
|
|
71397
|
+
return r;
|
|
71398
|
+
} catch (err) {
|
|
71399
|
+
onEnd(cmd, Date.now() - t0, false);
|
|
71400
|
+
throw err;
|
|
71401
|
+
}
|
|
71402
|
+
}
|
|
71403
|
+
};
|
|
71404
|
+
}
|
|
71405
|
+
function mergeIndicators(cfg, cli) {
|
|
71406
|
+
const out = {};
|
|
71407
|
+
for (const [k, v] of Object.entries(cfg)) {
|
|
71408
|
+
if (v !== undefined)
|
|
71409
|
+
out[k] = v;
|
|
71410
|
+
}
|
|
71411
|
+
for (const [k, v] of Object.entries(cli)) {
|
|
71412
|
+
if (v !== undefined)
|
|
71413
|
+
out[k] = v;
|
|
71414
|
+
}
|
|
71415
|
+
return out;
|
|
71416
|
+
}
|
|
71417
|
+
function unionMarkers(...sets) {
|
|
71418
|
+
const out = [];
|
|
71419
|
+
const seen = new Set;
|
|
71420
|
+
for (const s of sets) {
|
|
71421
|
+
if (!s)
|
|
71422
|
+
continue;
|
|
71423
|
+
for (const m of markersOf(s)) {
|
|
71424
|
+
const key = `${m.type}:${m.value}`;
|
|
71425
|
+
if (seen.has(key))
|
|
71426
|
+
continue;
|
|
71427
|
+
seen.add(key);
|
|
71428
|
+
out.push(m);
|
|
71429
|
+
}
|
|
71430
|
+
}
|
|
71431
|
+
return out;
|
|
71432
|
+
}
|
|
71179
71433
|
function buildAgentCoordinator(input) {
|
|
71180
71434
|
const {
|
|
71181
71435
|
args,
|
|
@@ -71184,33 +71438,104 @@ function buildAgentCoordinator(input) {
|
|
|
71184
71438
|
statesDir,
|
|
71185
71439
|
tasksDir,
|
|
71186
71440
|
apiKey,
|
|
71187
|
-
store,
|
|
71188
71441
|
onLog,
|
|
71189
71442
|
onWorkersChanged,
|
|
71190
71443
|
onWorkerStarted,
|
|
71191
|
-
onWorkerExited
|
|
71444
|
+
onWorkerExited,
|
|
71445
|
+
onWorkerPhase,
|
|
71446
|
+
onWorkerOutput,
|
|
71447
|
+
onWorkerCmd
|
|
71192
71448
|
} = input;
|
|
71449
|
+
const logsDir = join15(projectRoot, ".ralph", "logs");
|
|
71193
71450
|
const concurrency = args.concurrency || cfg.concurrency;
|
|
71194
71451
|
const pollInterval = args.pollInterval || cfg.pollIntervalSeconds;
|
|
71195
|
-
const
|
|
71196
|
-
const
|
|
71197
|
-
const
|
|
71198
|
-
const
|
|
71199
|
-
|
|
71200
|
-
|
|
71201
|
-
statuses: effectiveStatuses,
|
|
71202
|
-
labels: args.linearLabel.length ? args.linearLabel : cfg.linear.labels
|
|
71203
|
-
};
|
|
71452
|
+
const indicators = mergeIndicators(cfg.linear.indicators, args.indicators);
|
|
71453
|
+
const team = args.linearTeam || cfg.linear.team;
|
|
71454
|
+
const assignee = args.linearAssignee || cfg.linear.assignee;
|
|
71455
|
+
const excludeFromTodo = unionMarkers(indicators.setDone, indicators.setError, indicators.setConflicted);
|
|
71456
|
+
const gitRunner = input.runners?.git ?? bunGitRunner;
|
|
71457
|
+
const cmdRunner = input.runners?.cmd ?? bunCmdRunner;
|
|
71204
71458
|
const stateCache = new Map;
|
|
71205
71459
|
const labelCache = new Map;
|
|
71206
71460
|
const teamKeyOf = (issue) => issue.identifier.split("-")[0];
|
|
71207
|
-
|
|
71461
|
+
async function resolveStateId(issue, name) {
|
|
71462
|
+
const t = teamKeyOf(issue);
|
|
71463
|
+
let map2 = stateCache.get(t);
|
|
71464
|
+
if (!map2) {
|
|
71465
|
+
const states = await fetchWorkflowStates(apiKey, t);
|
|
71466
|
+
map2 = new Map(states.map((s) => [s.name.toLowerCase(), s.id]));
|
|
71467
|
+
stateCache.set(t, map2);
|
|
71468
|
+
}
|
|
71469
|
+
return map2.get(name.toLowerCase()) ?? null;
|
|
71470
|
+
}
|
|
71471
|
+
async function resolveLabelId(issue, name) {
|
|
71472
|
+
const t = teamKeyOf(issue);
|
|
71473
|
+
let map2 = labelCache.get(t);
|
|
71474
|
+
if (!map2) {
|
|
71475
|
+
const labels = await fetchIssueLabels(apiKey, t);
|
|
71476
|
+
map2 = new Map(labels.map((l) => [l.name.toLowerCase(), l.id]));
|
|
71477
|
+
labelCache.set(t, map2);
|
|
71478
|
+
}
|
|
71479
|
+
return map2.get(name.toLowerCase()) ?? null;
|
|
71480
|
+
}
|
|
71481
|
+
async function applyMarker(issue, m) {
|
|
71482
|
+
if (m.type === "status") {
|
|
71483
|
+
const id = await resolveStateId(issue, m.value);
|
|
71484
|
+
if (!id) {
|
|
71485
|
+
onLog(`! Linear status '${m.value}' not found for ${issue.identifier}`, "yellow");
|
|
71486
|
+
return;
|
|
71487
|
+
}
|
|
71488
|
+
await updateIssueState(apiKey, issue.id, id);
|
|
71489
|
+
onLog(` \u2192 ${issue.identifier} status='${m.value}'`, "gray");
|
|
71490
|
+
} else {
|
|
71491
|
+
const id = await resolveLabelId(issue, m.value);
|
|
71492
|
+
if (!id) {
|
|
71493
|
+
onLog(`! Linear label '${m.value}' not found for ${issue.identifier}`, "yellow");
|
|
71494
|
+
return;
|
|
71495
|
+
}
|
|
71496
|
+
await addLabelToIssue(apiKey, issue.id, id);
|
|
71497
|
+
onLog(` \u2192 ${issue.identifier} +label='${m.value}'`, "gray");
|
|
71498
|
+
}
|
|
71499
|
+
}
|
|
71500
|
+
async function applyIndicator(issue, ind) {
|
|
71501
|
+
for (const m of markersOf(ind))
|
|
71502
|
+
await applyMarker(issue, m);
|
|
71503
|
+
}
|
|
71504
|
+
async function removeIndicator(issue, ind) {
|
|
71505
|
+
for (const m of markersOf(ind)) {
|
|
71506
|
+
if (m.type !== "label")
|
|
71507
|
+
continue;
|
|
71508
|
+
const id = await resolveLabelId(issue, m.value);
|
|
71509
|
+
if (!id) {
|
|
71510
|
+
onLog(`! Linear label '${m.value}' not found for ${issue.identifier}`, "yellow");
|
|
71511
|
+
continue;
|
|
71512
|
+
}
|
|
71513
|
+
await removeLabelFromIssue(apiKey, issue.id, id);
|
|
71514
|
+
onLog(` \u2192 ${issue.identifier} -label='${m.value}'`, "gray");
|
|
71515
|
+
}
|
|
71516
|
+
}
|
|
71517
|
+
async function fetchByGet(inc, excl) {
|
|
71518
|
+
if (!inc)
|
|
71519
|
+
return [];
|
|
71520
|
+
const include = "filter" in inc ? inc.filter : [];
|
|
71521
|
+
if (include.length === 0)
|
|
71522
|
+
return [];
|
|
71523
|
+
const spec = {
|
|
71524
|
+
team,
|
|
71525
|
+
assignee,
|
|
71526
|
+
include,
|
|
71527
|
+
exclude: excl
|
|
71528
|
+
};
|
|
71529
|
+
return fetchOpenIssues(apiKey, spec);
|
|
71530
|
+
}
|
|
71208
71531
|
const cwdByChange = new Map;
|
|
71209
71532
|
const statesDirByChange = new Map;
|
|
71210
71533
|
const branchByChange = new Map;
|
|
71211
71534
|
const issueByChange = new Map;
|
|
71212
|
-
|
|
71213
|
-
|
|
71535
|
+
const prByChange = new Map;
|
|
71536
|
+
const prUnavailable = new Set;
|
|
71537
|
+
const useWorktree = args.worktree || cfg.useWorktree;
|
|
71538
|
+
const scriptRunner = input.runners?.runScript ?? (async (cmd, cwd2) => {
|
|
71214
71539
|
const proc = Bun.spawn({
|
|
71215
71540
|
cmd: ["sh", "-c", cmd],
|
|
71216
71541
|
cwd: cwd2,
|
|
@@ -71221,51 +71546,112 @@ function buildAgentCoordinator(input) {
|
|
|
71221
71546
|
const code = await proc.exited;
|
|
71222
71547
|
if (code !== 0) {
|
|
71223
71548
|
const stderr = await new Response(proc.stderr).text();
|
|
71224
|
-
onLog(`!
|
|
71549
|
+
onLog(`! script exited code ${code}${stderr ? `: ${stderr.trim().split(`
|
|
71225
71550
|
`)[0]}` : ""}`, "yellow");
|
|
71226
71551
|
}
|
|
71227
|
-
|
|
71228
|
-
|
|
71229
|
-
|
|
71230
|
-
|
|
71231
|
-
|
|
71232
|
-
|
|
71233
|
-
onLog(`!
|
|
71552
|
+
return code;
|
|
71553
|
+
});
|
|
71554
|
+
async function runScript(label, cmd, cwd2) {
|
|
71555
|
+
onLog(` ${label}: ${cmd}`, "gray");
|
|
71556
|
+
const code = await scriptRunner(cmd, cwd2);
|
|
71557
|
+
if (code !== 0) {
|
|
71558
|
+
onLog(`! ${label} exited code ${code}`, "yellow");
|
|
71234
71559
|
}
|
|
71560
|
+
}
|
|
71561
|
+
async function setupWorktree(issue) {
|
|
71235
71562
|
let workerCwd = projectRoot;
|
|
71236
71563
|
let scaffoldTasksDir = tasksDir;
|
|
71237
71564
|
let scaffoldStatesDir = statesDir;
|
|
71238
|
-
let
|
|
71565
|
+
let branch = null;
|
|
71566
|
+
if (!useWorktree)
|
|
71567
|
+
return { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch };
|
|
71239
71568
|
const probeName = issue.identifier.toLowerCase();
|
|
71240
|
-
|
|
71569
|
+
try {
|
|
71570
|
+
const wt = await createWorktree(projectRoot, probeName, gitRunner);
|
|
71571
|
+
workerCwd = wt.cwd;
|
|
71572
|
+
branch = wt.branch;
|
|
71573
|
+
const wtLayout = projectLayout(wt.cwd);
|
|
71574
|
+
scaffoldTasksDir = wtLayout.tasksDir;
|
|
71575
|
+
scaffoldStatesDir = wtLayout.statesDir;
|
|
71576
|
+
onLog(` ${issue.identifier} worktree: ${wt.cwd} (${wt.branch})`, "gray");
|
|
71241
71577
|
try {
|
|
71242
|
-
|
|
71243
|
-
|
|
71244
|
-
|
|
71245
|
-
|
|
71246
|
-
|
|
71247
|
-
|
|
71248
|
-
|
|
71249
|
-
|
|
71250
|
-
|
|
71251
|
-
|
|
71252
|
-
|
|
71253
|
-
|
|
71578
|
+
await seedWorktreeMcpConfig(projectRoot, wt.cwd);
|
|
71579
|
+
} catch (err) {
|
|
71580
|
+
onLog(`! seeding .mcp.json failed for ${issue.identifier}: ${err.message}`, "yellow");
|
|
71581
|
+
}
|
|
71582
|
+
} catch (err) {
|
|
71583
|
+
onLog(`! worktree create failed for ${issue.identifier}: ${err.message} \u2014 falling back to project root`, "yellow");
|
|
71584
|
+
}
|
|
71585
|
+
return { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch };
|
|
71586
|
+
}
|
|
71587
|
+
async function prepare(issue, mode) {
|
|
71588
|
+
const { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch } = await setupWorktree(issue);
|
|
71589
|
+
let changeName;
|
|
71590
|
+
if (mode === "fresh") {
|
|
71591
|
+
let comments = [];
|
|
71592
|
+
try {
|
|
71593
|
+
comments = await fetchIssueComments(apiKey, issue.id);
|
|
71254
71594
|
} catch (err) {
|
|
71255
|
-
onLog(`!
|
|
71595
|
+
onLog(`! Linear comment fetch failed for ${issue.identifier}: ${err.message}`, "yellow");
|
|
71256
71596
|
}
|
|
71597
|
+
const appendPrompt = args.prompt || cfg.appendPrompt || "";
|
|
71598
|
+
changeName = await scaffoldChangeForIssue(scaffoldTasksDir, scaffoldStatesDir, issue, comments, appendPrompt);
|
|
71599
|
+
} else {
|
|
71600
|
+
changeName = changeNameForIssue(issue);
|
|
71601
|
+
const wtLayout = projectLayout(workerCwd);
|
|
71602
|
+
await mkdir3(wtLayout.changeDir(changeName), { recursive: true });
|
|
71603
|
+
await mkdir3(wtLayout.taskStateDir(changeName), { recursive: true });
|
|
71257
71604
|
}
|
|
71258
|
-
const appendPrompt = args.prompt || cfg.appendPrompt || "";
|
|
71259
|
-
const changeName = await scaffoldChangeForIssue(scaffoldTasksDir, scaffoldStatesDir, issue, comments, appendPrompt);
|
|
71260
71605
|
cwdByChange.set(changeName, workerCwd);
|
|
71261
71606
|
statesDirByChange.set(changeName, scaffoldStatesDir);
|
|
71262
71607
|
issueByChange.set(changeName, issue);
|
|
71263
|
-
if (
|
|
71264
|
-
branchByChange.set(changeName,
|
|
71608
|
+
if (branch)
|
|
71609
|
+
branchByChange.set(changeName, branch);
|
|
71610
|
+
if (mode === "conflict-fix") {
|
|
71611
|
+
const wtLayout = projectLayout(workerCwd);
|
|
71612
|
+
const tasksFile = join15(wtLayout.changeDir(changeName), "tasks.md");
|
|
71613
|
+
const prUrl = prByChange.get(changeName);
|
|
71614
|
+
const body = [
|
|
71615
|
+
`The PR for this change has merge conflicts with \`${cfg.prBaseBranch}\`.`,
|
|
71616
|
+
"",
|
|
71617
|
+
"Steps:",
|
|
71618
|
+
`1. \`git fetch origin ${cfg.prBaseBranch}\` then rebase or merge \`${cfg.prBaseBranch}\` into the current branch.`,
|
|
71619
|
+
"2. Resolve conflicts in the files git lists.",
|
|
71620
|
+
"3. Stage and commit the resolution.",
|
|
71621
|
+
prUrl ? `
|
|
71622
|
+
PR: ${prUrl}` : ""
|
|
71623
|
+
].filter(Boolean).join(`
|
|
71624
|
+
`);
|
|
71625
|
+
try {
|
|
71626
|
+
await prependFixTask(tasksFile, "Resolve PR merge conflicts", body);
|
|
71627
|
+
} catch (err) {
|
|
71628
|
+
onLog(`! could not prepend conflict-fix task: ${err.message}`, "red");
|
|
71629
|
+
}
|
|
71630
|
+
await reactivateState2(wtLayout.stateFile(changeName), changeName);
|
|
71631
|
+
}
|
|
71265
71632
|
if (cfg.setupScript) {
|
|
71266
71633
|
await runScript("setup", cfg.setupScript, workerCwd);
|
|
71267
71634
|
}
|
|
71268
|
-
return
|
|
71635
|
+
return {
|
|
71636
|
+
changeName,
|
|
71637
|
+
...prByChange.has(changeName) ? { prUrl: prByChange.get(changeName) } : {}
|
|
71638
|
+
};
|
|
71639
|
+
}
|
|
71640
|
+
async function reactivateState2(stateFilePath, changeName) {
|
|
71641
|
+
const file = Bun.file(stateFilePath);
|
|
71642
|
+
if (!await file.exists())
|
|
71643
|
+
return;
|
|
71644
|
+
try {
|
|
71645
|
+
const stateObj = JSON.parse(await file.text());
|
|
71646
|
+
if (stateObj.status !== "active") {
|
|
71647
|
+
stateObj.status = "active";
|
|
71648
|
+
stateObj.lastModified = new Date().toISOString();
|
|
71649
|
+
await Bun.write(stateFilePath, JSON.stringify(stateObj, null, 2) + `
|
|
71650
|
+
`);
|
|
71651
|
+
}
|
|
71652
|
+
} catch (err) {
|
|
71653
|
+
onLog(`! could not reactivate state for ${changeName}: ${err.message}`, "yellow");
|
|
71654
|
+
}
|
|
71269
71655
|
}
|
|
71270
71656
|
function buildTaskCmdFor(changeName) {
|
|
71271
71657
|
const c = [
|
|
@@ -71298,29 +71684,102 @@ function buildAgentCoordinator(input) {
|
|
|
71298
71684
|
c.push("--verbose");
|
|
71299
71685
|
return c;
|
|
71300
71686
|
}
|
|
71301
|
-
function
|
|
71302
|
-
const
|
|
71303
|
-
|
|
71304
|
-
|
|
71305
|
-
|
|
71306
|
-
|
|
71307
|
-
|
|
71308
|
-
|
|
71309
|
-
|
|
71310
|
-
|
|
71311
|
-
|
|
71687
|
+
function defaultSpawn(changeName, cmd, cwd2, note) {
|
|
71688
|
+
const logFilePath = join15(logsDir, `${changeName}.log`);
|
|
71689
|
+
let logWriter = null;
|
|
71690
|
+
const ensureLogWriter = async () => {
|
|
71691
|
+
if (logWriter)
|
|
71692
|
+
return logWriter;
|
|
71693
|
+
try {
|
|
71694
|
+
await Bun.write(logFilePath, "");
|
|
71695
|
+
logWriter = Bun.file(logFilePath).writer();
|
|
71696
|
+
return logWriter;
|
|
71697
|
+
} catch (err) {
|
|
71698
|
+
onLog(`! could not open worker log ${logFilePath}: ${err.message}`, "yellow");
|
|
71699
|
+
return null;
|
|
71700
|
+
}
|
|
71312
71701
|
};
|
|
71313
|
-
|
|
71314
|
-
|
|
71702
|
+
async function pump(stream, label) {
|
|
71703
|
+
if (!stream)
|
|
71704
|
+
return;
|
|
71705
|
+
const reader = stream.getReader();
|
|
71706
|
+
const decoder = new TextDecoder;
|
|
71707
|
+
let buf = "";
|
|
71708
|
+
const writer = await ensureLogWriter();
|
|
71709
|
+
try {
|
|
71710
|
+
while (true) {
|
|
71711
|
+
const { value, done } = await reader.read();
|
|
71712
|
+
if (done)
|
|
71713
|
+
break;
|
|
71714
|
+
const chunk2 = decoder.decode(value, { stream: true });
|
|
71715
|
+
buf += chunk2;
|
|
71716
|
+
let nl;
|
|
71717
|
+
while ((nl = buf.indexOf(`
|
|
71718
|
+
`)) >= 0) {
|
|
71719
|
+
const line = buf.slice(0, nl);
|
|
71720
|
+
buf = buf.slice(nl + 1);
|
|
71721
|
+
if (writer)
|
|
71722
|
+
writer.write(line + `
|
|
71723
|
+
`);
|
|
71724
|
+
if (line)
|
|
71725
|
+
onWorkerOutput?.(changeName, label === "err" ? `! ${line}` : line);
|
|
71726
|
+
}
|
|
71727
|
+
}
|
|
71728
|
+
if (buf) {
|
|
71729
|
+
if (writer)
|
|
71730
|
+
writer.write(buf + `
|
|
71731
|
+
`);
|
|
71732
|
+
onWorkerOutput?.(changeName, label === "err" ? `! ${buf}` : buf);
|
|
71733
|
+
}
|
|
71734
|
+
} catch {} finally {
|
|
71735
|
+
try {
|
|
71736
|
+
writer?.flush();
|
|
71737
|
+
} catch {}
|
|
71738
|
+
}
|
|
71739
|
+
}
|
|
71740
|
+
const p = Bun.spawn({
|
|
71741
|
+
cmd,
|
|
71315
71742
|
cwd: cwd2,
|
|
71316
|
-
stdout: "
|
|
71317
|
-
stderr: "
|
|
71743
|
+
stdout: "pipe",
|
|
71744
|
+
stderr: "pipe",
|
|
71318
71745
|
stdin: "ignore"
|
|
71319
71746
|
});
|
|
71320
|
-
|
|
71747
|
+
(async () => {
|
|
71748
|
+
const writer = await ensureLogWriter();
|
|
71749
|
+
if (note && writer)
|
|
71750
|
+
writer.write(`
|
|
71751
|
+
--- ${note} ---
|
|
71752
|
+
`);
|
|
71753
|
+
})();
|
|
71754
|
+
pump(p.stdout, "out");
|
|
71755
|
+
pump(p.stderr, "err");
|
|
71756
|
+
return { exited: p.exited, kill: () => p.kill(), logFilePath };
|
|
71757
|
+
}
|
|
71758
|
+
function spawnWorker(changeName) {
|
|
71759
|
+
const cwd2 = cwdByChange.get(changeName) ?? projectRoot;
|
|
71760
|
+
const injected = input.runners?.spawnWorker;
|
|
71761
|
+
let logFilePath;
|
|
71762
|
+
let handle;
|
|
71763
|
+
if (injected) {
|
|
71764
|
+
logFilePath = join15(logsDir, `${changeName}.log`);
|
|
71765
|
+
handle = injected(buildTaskCmdFor(changeName), cwd2);
|
|
71766
|
+
} else {
|
|
71767
|
+
const r = defaultSpawn(changeName, buildTaskCmdFor(changeName), cwd2, `spawn at ${new Date().toISOString()}`);
|
|
71768
|
+
logFilePath = r.logFilePath;
|
|
71769
|
+
handle = { exited: r.exited, kill: r.kill };
|
|
71770
|
+
}
|
|
71771
|
+
const respawn = () => {
|
|
71772
|
+
onWorkerPhase?.(changeName, "working", "respawn");
|
|
71773
|
+
if (injected)
|
|
71774
|
+
return injected(buildTaskCmdFor(changeName), cwd2).exited;
|
|
71775
|
+
return defaultSpawn(changeName, buildTaskCmdFor(changeName), cwd2, `respawn at ${new Date().toISOString()}`).exited;
|
|
71776
|
+
};
|
|
71777
|
+
onWorkerStarted(changeName, statesDirByChange.get(changeName) ?? statesDir, logFilePath);
|
|
71778
|
+
onWorkerPhase?.(changeName, "working");
|
|
71779
|
+
const tracedCmd = onWorkerCmd ? traceCmdRunner(cmdRunner, (cmd) => onWorkerCmd(changeName, cmd, "start"), (cmd, ms, ok) => onWorkerCmd(changeName, cmd, "end", ms, ok)) : cmdRunner;
|
|
71321
71780
|
const wantPr = args.createPr || cfg.createPrOnSuccess;
|
|
71322
71781
|
const wantFixCi = args.fixCi || cfg.fixCiOnFailure;
|
|
71323
|
-
const wrapped =
|
|
71782
|
+
const wrapped = handle.exited.then(async (code) => {
|
|
71324
71783
|
const workerLayout = projectLayout(cwd2);
|
|
71325
71784
|
const effectiveCode = await runPostTask({
|
|
71326
71785
|
changeName,
|
|
@@ -71342,7 +71801,19 @@ function buildAgentCoordinator(input) {
|
|
|
71342
71801
|
cleanupWorktreeOnSuccess: cfg.cleanupWorktreeOnSuccess
|
|
71343
71802
|
},
|
|
71344
71803
|
respawnWorker: respawn
|
|
71345
|
-
}, {
|
|
71804
|
+
}, {
|
|
71805
|
+
cmd: tracedCmd,
|
|
71806
|
+
git: gitRunner,
|
|
71807
|
+
log: onLog,
|
|
71808
|
+
runScript,
|
|
71809
|
+
registerPr: (cn, url) => {
|
|
71810
|
+
prByChange.set(cn, url);
|
|
71811
|
+
prUnavailable.delete(cn);
|
|
71812
|
+
},
|
|
71813
|
+
...onWorkerPhase && {
|
|
71814
|
+
onPhase: (phase, detail) => onWorkerPhase(changeName, phase, detail)
|
|
71815
|
+
}
|
|
71816
|
+
});
|
|
71346
71817
|
cwdByChange.delete(changeName);
|
|
71347
71818
|
statesDirByChange.delete(changeName);
|
|
71348
71819
|
branchByChange.delete(changeName);
|
|
@@ -71350,13 +71821,73 @@ function buildAgentCoordinator(input) {
|
|
|
71350
71821
|
onWorkerExited(changeName);
|
|
71351
71822
|
return effectiveCode;
|
|
71352
71823
|
});
|
|
71353
|
-
return { exited: wrapped, kill: () =>
|
|
71824
|
+
return { exited: wrapped, kill: () => handle.kill() };
|
|
71825
|
+
}
|
|
71826
|
+
async function checkPrConflict(issue) {
|
|
71827
|
+
const changeName = changeNameForIssue(issue);
|
|
71828
|
+
if (prUnavailable.has(changeName))
|
|
71829
|
+
return null;
|
|
71830
|
+
const branch = branchForChange(changeName);
|
|
71831
|
+
let prUrl = prByChange.get(changeName);
|
|
71832
|
+
if (!prUrl) {
|
|
71833
|
+
try {
|
|
71834
|
+
const res = await cmdRunner.run([
|
|
71835
|
+
"gh",
|
|
71836
|
+
"pr",
|
|
71837
|
+
"list",
|
|
71838
|
+
"--head",
|
|
71839
|
+
branch,
|
|
71840
|
+
"--state",
|
|
71841
|
+
"open",
|
|
71842
|
+
"--json",
|
|
71843
|
+
"url",
|
|
71844
|
+
"--jq",
|
|
71845
|
+
".[0].url // empty"
|
|
71846
|
+
], projectRoot);
|
|
71847
|
+
const found = res.stdout.trim();
|
|
71848
|
+
if (!found) {
|
|
71849
|
+
prUnavailable.add(changeName);
|
|
71850
|
+
return null;
|
|
71851
|
+
}
|
|
71852
|
+
prUrl = found;
|
|
71853
|
+
prByChange.set(changeName, prUrl);
|
|
71854
|
+
} catch {
|
|
71855
|
+
prUnavailable.add(changeName);
|
|
71856
|
+
return null;
|
|
71857
|
+
}
|
|
71858
|
+
}
|
|
71859
|
+
try {
|
|
71860
|
+
const res = await cmdRunner.run(["gh", "pr", "view", prUrl, "--json", "mergeable", "--jq", ".mergeable"], projectRoot);
|
|
71861
|
+
const mergeable = res.stdout.trim();
|
|
71862
|
+
return { url: prUrl, conflicting: mergeable === "CONFLICTING" };
|
|
71863
|
+
} catch {
|
|
71864
|
+
return null;
|
|
71865
|
+
}
|
|
71866
|
+
}
|
|
71867
|
+
async function fetchDoneCandidates() {
|
|
71868
|
+
if (!indicators.setDone)
|
|
71869
|
+
return [];
|
|
71870
|
+
const include = markersOf(indicators.setDone);
|
|
71871
|
+
const exclude = indicators.setConflicted ? markersOf(indicators.setConflicted) : [];
|
|
71872
|
+
if (include.length === 0)
|
|
71873
|
+
return [];
|
|
71874
|
+
return fetchOpenIssues(apiKey, { team, assignee, include, exclude });
|
|
71354
71875
|
}
|
|
71355
71876
|
const coord = new AgentCoordinator({
|
|
71356
|
-
|
|
71357
|
-
|
|
71877
|
+
fetchTodo: () => fetchByGet(indicators.getTodo, excludeFromTodo),
|
|
71878
|
+
fetchInProgress: () => fetchByGet(indicators.getInProgress, []),
|
|
71879
|
+
fetchConflicted: () => fetchByGet(indicators.getConflicted, []),
|
|
71880
|
+
fetchDoneCandidates,
|
|
71881
|
+
prepare,
|
|
71358
71882
|
spawnWorker,
|
|
71359
|
-
|
|
71883
|
+
applyIndicator,
|
|
71884
|
+
removeIndicator,
|
|
71885
|
+
postComment: (issue, body) => addIssueComment(apiKey, issue.id, body),
|
|
71886
|
+
fetchComments: async (issueId) => {
|
|
71887
|
+
const c = await fetchIssueComments(apiKey, issueId);
|
|
71888
|
+
return c.map((x) => ({ body: x.body }));
|
|
71889
|
+
},
|
|
71890
|
+
checkPrConflict,
|
|
71360
71891
|
onLog,
|
|
71361
71892
|
onWorkersChanged,
|
|
71362
71893
|
getIterationCount: async (changeName) => {
|
|
@@ -71366,42 +71897,29 @@ function buildAgentCoordinator(input) {
|
|
|
71366
71897
|
return 0;
|
|
71367
71898
|
const json = await file.json();
|
|
71368
71899
|
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
71900
|
}
|
|
71395
71901
|
}, {
|
|
71396
71902
|
concurrency,
|
|
71397
|
-
|
|
71398
|
-
|
|
71399
|
-
|
|
71400
|
-
|
|
71903
|
+
...indicators.setInProgress !== undefined ? { setInProgress: indicators.setInProgress } : {},
|
|
71904
|
+
...indicators.setDone !== undefined ? { setDone: indicators.setDone } : {},
|
|
71905
|
+
...indicators.setError !== undefined ? { setError: indicators.setError } : {},
|
|
71906
|
+
...indicators.setConflicted !== undefined ? { setConflicted: indicators.setConflicted } : {},
|
|
71907
|
+
...indicators.clearConflicted !== undefined ? { clearConflicted: indicators.clearConflicted } : {},
|
|
71401
71908
|
postComments: cfg.linear.postComments,
|
|
71402
71909
|
commentEveryIterations: cfg.linear.updateEveryIterations
|
|
71403
71910
|
});
|
|
71404
|
-
|
|
71911
|
+
(async () => {
|
|
71912
|
+
const legacy = Bun.file(projectLayout(projectRoot).agentStateFile);
|
|
71913
|
+
if (await legacy.exists()) {
|
|
71914
|
+
onLog(" legacy .ralph/agent-state.json detected \u2014 Linear is now the source of truth; deleting", "gray");
|
|
71915
|
+
try {
|
|
71916
|
+
await legacy.delete();
|
|
71917
|
+
} catch (err) {
|
|
71918
|
+
onLog(`! failed to delete legacy agent-state.json: ${err.message}`, "yellow");
|
|
71919
|
+
}
|
|
71920
|
+
}
|
|
71921
|
+
})();
|
|
71922
|
+
const filterDesc = describeIndicators(indicators, team, assignee);
|
|
71405
71923
|
return {
|
|
71406
71924
|
coord,
|
|
71407
71925
|
filterDesc,
|
|
@@ -71410,6 +71928,21 @@ function buildAgentCoordinator(input) {
|
|
|
71410
71928
|
getWorkerCwd: (changeName) => cwdByChange.get(changeName)
|
|
71411
71929
|
};
|
|
71412
71930
|
}
|
|
71931
|
+
function describeIndicators(indicators, team, assignee) {
|
|
71932
|
+
const parts = [];
|
|
71933
|
+
parts.push(`team=${team ?? "*"}`);
|
|
71934
|
+
parts.push(`assignee=${assignee ?? "*"}`);
|
|
71935
|
+
if (indicators.getTodo) {
|
|
71936
|
+
parts.push(`todo=[${indicators.getTodo.filter.map((m) => `${m.type}:${m.value}`).join(",")}]`);
|
|
71937
|
+
}
|
|
71938
|
+
if (indicators.getInProgress) {
|
|
71939
|
+
parts.push(`inProgress=[${indicators.getInProgress.filter.map((m) => `${m.type}:${m.value}`).join(",")}]`);
|
|
71940
|
+
}
|
|
71941
|
+
if (indicators.getConflicted) {
|
|
71942
|
+
parts.push(`conflicted=[${indicators.getConflicted.filter.map((m) => `${m.type}:${m.value}`).join(",")}]`);
|
|
71943
|
+
}
|
|
71944
|
+
return parts.join(", ");
|
|
71945
|
+
}
|
|
71413
71946
|
|
|
71414
71947
|
// apps/cli/src/components/AgentMode.tsx
|
|
71415
71948
|
var jsx_dev_runtime9 = __toESM(require_jsx_dev_runtime(), 1);
|
|
@@ -71418,6 +71951,12 @@ function nextId() {
|
|
|
71418
71951
|
lineCounter += 1;
|
|
71419
71952
|
return `${Date.now()}-${lineCounter}`;
|
|
71420
71953
|
}
|
|
71954
|
+
var TAIL_MAX_LINES = 5;
|
|
71955
|
+
var CMD_DISPLAY_MAX = 80;
|
|
71956
|
+
function fmtCmd(argv) {
|
|
71957
|
+
const joined = argv.join(" ");
|
|
71958
|
+
return joined.length > CMD_DISPLAY_MAX ? joined.slice(0, CMD_DISPLAY_MAX - 1) + "\u2026" : joined;
|
|
71959
|
+
}
|
|
71421
71960
|
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
71422
71961
|
function fmtElapsed(ms) {
|
|
71423
71962
|
const s = Math.floor(ms / 1000);
|
|
@@ -71455,8 +71994,6 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
71455
71994
|
exit();
|
|
71456
71995
|
return;
|
|
71457
71996
|
}
|
|
71458
|
-
const store = new AgentStateStore(projectRoot);
|
|
71459
|
-
await store.load();
|
|
71460
71997
|
const { coord: coord2, filterDesc, concurrency, pollInterval } = buildAgentCoordinator({
|
|
71461
71998
|
args,
|
|
71462
71999
|
cfg,
|
|
@@ -71464,18 +72001,52 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
71464
72001
|
statesDir,
|
|
71465
72002
|
tasksDir,
|
|
71466
72003
|
apiKey,
|
|
71467
|
-
store,
|
|
71468
72004
|
onLog: appendLog,
|
|
71469
72005
|
onWorkersChanged: () => setTick((t) => t + 1),
|
|
71470
|
-
onWorkerStarted: (changeName, dir) => {
|
|
72006
|
+
onWorkerStarted: (changeName, dir, logFile) => {
|
|
71471
72007
|
workerMetaRef.current.set(changeName, {
|
|
71472
72008
|
startedAt: Date.now(),
|
|
71473
72009
|
statesDir: dir,
|
|
71474
|
-
|
|
72010
|
+
logFile,
|
|
72011
|
+
iter: 0,
|
|
72012
|
+
phase: "working",
|
|
72013
|
+
phaseDetail: "",
|
|
72014
|
+
phaseStartedAt: Date.now(),
|
|
72015
|
+
currentCmd: null,
|
|
72016
|
+
lastCmd: null,
|
|
72017
|
+
tail: []
|
|
71475
72018
|
});
|
|
71476
72019
|
},
|
|
71477
72020
|
onWorkerExited: (changeName) => {
|
|
71478
72021
|
workerMetaRef.current.delete(changeName);
|
|
72022
|
+
},
|
|
72023
|
+
onWorkerPhase: (changeName, phase, detail) => {
|
|
72024
|
+
const m = workerMetaRef.current.get(changeName);
|
|
72025
|
+
if (!m)
|
|
72026
|
+
return;
|
|
72027
|
+
if (m.phase !== phase)
|
|
72028
|
+
m.phaseStartedAt = Date.now();
|
|
72029
|
+
m.phase = phase;
|
|
72030
|
+
m.phaseDetail = detail ?? "";
|
|
72031
|
+
},
|
|
72032
|
+
onWorkerOutput: (changeName, line) => {
|
|
72033
|
+
const m = workerMetaRef.current.get(changeName);
|
|
72034
|
+
if (!m)
|
|
72035
|
+
return;
|
|
72036
|
+
m.tail.push(line);
|
|
72037
|
+
if (m.tail.length > TAIL_MAX_LINES)
|
|
72038
|
+
m.tail.splice(0, m.tail.length - TAIL_MAX_LINES);
|
|
72039
|
+
},
|
|
72040
|
+
onWorkerCmd: (changeName, cmd, state, durationMs, ok) => {
|
|
72041
|
+
const m = workerMetaRef.current.get(changeName);
|
|
72042
|
+
if (!m)
|
|
72043
|
+
return;
|
|
72044
|
+
if (state === "start") {
|
|
72045
|
+
m.currentCmd = { argv: cmd, startedAt: Date.now() };
|
|
72046
|
+
} else {
|
|
72047
|
+
m.currentCmd = null;
|
|
72048
|
+
m.lastCmd = { argv: cmd, durationMs: durationMs ?? 0, ok: ok ?? true };
|
|
72049
|
+
}
|
|
71479
72050
|
}
|
|
71480
72051
|
});
|
|
71481
72052
|
appendLog(`concurrency=${concurrency} pollInterval=${pollInterval}s`, "gray");
|
|
@@ -71589,19 +72160,56 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
71589
72160
|
const meta = workerMetaRef.current.get(w.changeName);
|
|
71590
72161
|
const elapsed = meta ? fmtElapsed(now2 - meta.startedAt) : "\u2013";
|
|
71591
72162
|
const iter = meta?.iter ?? 0;
|
|
71592
|
-
|
|
71593
|
-
|
|
72163
|
+
const phase = meta?.phase ?? "working";
|
|
72164
|
+
const phaseElapsed = meta ? fmtElapsed(now2 - meta.phaseStartedAt) : "\u2013";
|
|
72165
|
+
const phaseDetail = meta?.phaseDetail ? ` (${meta.phaseDetail})` : "";
|
|
72166
|
+
const cmd = meta?.currentCmd;
|
|
72167
|
+
const cmdElapsed = cmd ? fmtElapsed(now2 - cmd.startedAt) : null;
|
|
72168
|
+
const tail2 = meta?.tail ?? [];
|
|
72169
|
+
return /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Box_default, {
|
|
72170
|
+
flexDirection: "column",
|
|
71594
72171
|
children: [
|
|
71595
|
-
|
|
71596
|
-
|
|
71597
|
-
|
|
71598
|
-
|
|
71599
|
-
|
|
71600
|
-
|
|
71601
|
-
|
|
71602
|
-
|
|
71603
|
-
|
|
71604
|
-
|
|
72172
|
+
/* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
|
|
72173
|
+
color: "cyan",
|
|
72174
|
+
children: [
|
|
72175
|
+
" ",
|
|
72176
|
+
spinnerFrame,
|
|
72177
|
+
" ",
|
|
72178
|
+
w.issueIdentifier,
|
|
72179
|
+
" (",
|
|
72180
|
+
w.changeName,
|
|
72181
|
+
") \xB7 iter ",
|
|
72182
|
+
iter,
|
|
72183
|
+
" \xB7 ",
|
|
72184
|
+
elapsed
|
|
72185
|
+
]
|
|
72186
|
+
}, undefined, true, undefined, this),
|
|
72187
|
+
/* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
|
|
72188
|
+
dimColor: true,
|
|
72189
|
+
children: [
|
|
72190
|
+
" phase: ",
|
|
72191
|
+
phase,
|
|
72192
|
+
phaseDetail,
|
|
72193
|
+
" \xB7 ",
|
|
72194
|
+
phaseElapsed
|
|
72195
|
+
]
|
|
72196
|
+
}, undefined, true, undefined, this),
|
|
72197
|
+
cmd && /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
|
|
72198
|
+
color: "yellow",
|
|
72199
|
+
children: [
|
|
72200
|
+
" \u23F5 ",
|
|
72201
|
+
fmtCmd(cmd.argv),
|
|
72202
|
+
" \xB7 ",
|
|
72203
|
+
cmdElapsed
|
|
72204
|
+
]
|
|
72205
|
+
}, undefined, true, undefined, this),
|
|
72206
|
+
tail2.map((line, i) => /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
|
|
72207
|
+
dimColor: true,
|
|
72208
|
+
children: [
|
|
72209
|
+
" \u2502 ",
|
|
72210
|
+
line.length > 110 ? line.slice(0, 109) + "\u2026" : line
|
|
72211
|
+
]
|
|
72212
|
+
}, `${w.changeName}-tail-${i}`, true, undefined, this))
|
|
71605
72213
|
]
|
|
71606
72214
|
}, w.changeName, true, undefined, this);
|
|
71607
72215
|
})
|
|
@@ -71613,7 +72221,7 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
71613
72221
|
|
|
71614
72222
|
// packages/openspec/src/openspec-change-store.ts
|
|
71615
72223
|
import { join as join17, dirname as dirname4 } from "path";
|
|
71616
|
-
import { readdir, mkdir as
|
|
72224
|
+
import { readdir, mkdir as mkdir4 } from "fs/promises";
|
|
71617
72225
|
function resolveOpenspecBin() {
|
|
71618
72226
|
const pkgJsonPath = Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir);
|
|
71619
72227
|
return join17(dirname4(pkgJsonPath), "bin", "openspec.js");
|
|
@@ -71674,7 +72282,7 @@ class OpenSpecChangeStore {
|
|
|
71674
72282
|
}
|
|
71675
72283
|
async writeTaskList(name, content) {
|
|
71676
72284
|
const path = join17("openspec", "changes", name, "tasks.md");
|
|
71677
|
-
await
|
|
72285
|
+
await mkdir4(dirname4(path), { recursive: true });
|
|
71678
72286
|
await Bun.write(path, content);
|
|
71679
72287
|
}
|
|
71680
72288
|
async appendSteering(name, message) {
|
|
@@ -71685,7 +72293,7 @@ class OpenSpecChangeStore {
|
|
|
71685
72293
|
|
|
71686
72294
|
${existing.trimStart()}` : `${message}
|
|
71687
72295
|
`;
|
|
71688
|
-
await
|
|
72296
|
+
await mkdir4(dirname4(path), { recursive: true });
|
|
71689
72297
|
await Bun.write(path, updated);
|
|
71690
72298
|
}
|
|
71691
72299
|
async readSection(name, artifact, heading) {
|
|
@@ -71872,7 +72480,7 @@ try {
|
|
|
71872
72480
|
const statesDir = layout.statesDir;
|
|
71873
72481
|
const tasksDir = layout.tasksDir;
|
|
71874
72482
|
if (args.mode === "init") {
|
|
71875
|
-
await
|
|
72483
|
+
await mkdir5(statesDir, { recursive: true });
|
|
71876
72484
|
const openspecBin = join19(dirname5(Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir)), "bin", "openspec.js");
|
|
71877
72485
|
Bun.spawnSync({
|
|
71878
72486
|
cmd: [process.execPath, openspecBin, "init", "--tools", "none", "--force"],
|
|
@@ -71926,14 +72534,6 @@ try {
|
|
|
71926
72534
|
await rm(stateDir, { recursive: true, force: true });
|
|
71927
72535
|
removed.push(`task state ${stateDir}`);
|
|
71928
72536
|
}
|
|
71929
|
-
try {
|
|
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})`);
|
|
71935
|
-
}
|
|
71936
|
-
} catch {}
|
|
71937
72537
|
if (removed.length === 0) {
|
|
71938
72538
|
process.stdout.write(`Nothing to clean for '${args.name}'
|
|
71939
72539
|
`);
|
|
@@ -71948,13 +72548,13 @@ try {
|
|
|
71948
72548
|
process.exit(0);
|
|
71949
72549
|
}
|
|
71950
72550
|
if (args.mode === "task" && args.name) {
|
|
71951
|
-
await
|
|
71952
|
-
await
|
|
72551
|
+
await mkdir5(join19(statesDir, args.name), { recursive: true });
|
|
72552
|
+
await mkdir5(join19(tasksDir, args.name), { recursive: true });
|
|
71953
72553
|
}
|
|
71954
72554
|
if (args.mode === "agent") {
|
|
71955
|
-
await
|
|
71956
|
-
await
|
|
71957
|
-
await
|
|
72555
|
+
await mkdir5(statesDir, { recursive: true });
|
|
72556
|
+
await mkdir5(tasksDir, { recursive: true });
|
|
72557
|
+
await mkdir5(join19(projectRoot, ".ralph"), { recursive: true });
|
|
71958
72558
|
}
|
|
71959
72559
|
await runWithContext(createDefaultContext(), async () => {
|
|
71960
72560
|
const { waitUntilExit } = render_default(import_react59.createElement(App2, { args, statesDir, tasksDir, projectRoot }));
|