@neriros/ralphy 2.12.0 → 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 +757 -443
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -50837,8 +50837,8 @@ var require_axios = __commonJS((exports, module) => {
|
|
|
50837
50837
|
});
|
|
50838
50838
|
|
|
50839
50839
|
// apps/cli/src/index.ts
|
|
50840
|
-
import { resolve, join as
|
|
50841
|
-
import { exists as exists2, mkdir as
|
|
50840
|
+
import { resolve, join as join19, dirname as dirname5 } from "path";
|
|
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;
|
|
@@ -56879,7 +56900,7 @@ function createDefaultContext() {
|
|
|
56879
56900
|
|
|
56880
56901
|
// apps/cli/src/components/App.tsx
|
|
56881
56902
|
var import_react58 = __toESM(require_react(), 1);
|
|
56882
|
-
import { join as
|
|
56903
|
+
import { join as join18 } from "path";
|
|
56883
56904
|
|
|
56884
56905
|
// packages/core/src/state.ts
|
|
56885
56906
|
import { join as join2 } from "path";
|
|
@@ -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(),
|
|
@@ -70104,89 +70128,46 @@ function TaskLoop({ opts }) {
|
|
|
70104
70128
|
|
|
70105
70129
|
// apps/cli/src/components/AgentMode.tsx
|
|
70106
70130
|
var import_react57 = __toESM(require_react(), 1);
|
|
70107
|
-
import { join as
|
|
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
|
-
async setLastPollAt(when) {
|
|
70170
|
-
const s = this.snapshot();
|
|
70171
|
-
s.lastPollAt = when;
|
|
70172
|
-
await this.flush();
|
|
70173
70169
|
}
|
|
70174
|
-
|
|
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;
|
|
@@ -70250,49 +70243,85 @@ async function ensureRalphyConfig(projectRoot) {
|
|
|
70250
70243
|
}
|
|
70251
70244
|
|
|
70252
70245
|
// apps/cli/src/agent/wire.ts
|
|
70253
|
-
import { join as
|
|
70246
|
+
import { join as join15 } from "path";
|
|
70247
|
+
import { mkdir as mkdir3 } from "fs/promises";
|
|
70254
70248
|
|
|
70255
70249
|
// packages/core/src/layout.ts
|
|
70256
|
-
import { join as
|
|
70250
|
+
import { join as join11 } from "path";
|
|
70257
70251
|
var STATE_FILE2 = ".ralph-state.json";
|
|
70258
70252
|
function projectLayout(root) {
|
|
70259
|
-
const statesDir =
|
|
70260
|
-
const tasksDir =
|
|
70253
|
+
const statesDir = join11(root, ".ralph", "tasks");
|
|
70254
|
+
const tasksDir = join11(root, "openspec", "changes");
|
|
70261
70255
|
return {
|
|
70262
70256
|
root,
|
|
70263
70257
|
statesDir,
|
|
70264
70258
|
tasksDir,
|
|
70265
|
-
agentStateFile:
|
|
70266
|
-
changeDir: (name) =>
|
|
70267
|
-
taskStateDir: (name) =>
|
|
70268
|
-
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)
|
|
70269
70263
|
};
|
|
70270
70264
|
}
|
|
70271
70265
|
|
|
70272
70266
|
// apps/cli/src/agent/linear.ts
|
|
70273
|
-
var OPEN_STATE_TYPES = ["unstarted", "started", "backlog"];
|
|
70274
70267
|
var LINEAR_API = "https://api.linear.app/graphql";
|
|
70275
|
-
|
|
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) {
|
|
70276
70280
|
const where = {};
|
|
70277
|
-
if (
|
|
70278
|
-
where.team = { key: { eq:
|
|
70279
|
-
if (
|
|
70280
|
-
if (
|
|
70281
|
+
if (spec.team)
|
|
70282
|
+
where.team = { key: { eq: spec.team } };
|
|
70283
|
+
if (spec.assignee) {
|
|
70284
|
+
if (spec.assignee === "me")
|
|
70281
70285
|
where.assignee = { isMe: { eq: true } };
|
|
70282
|
-
|
|
70283
|
-
where.assignee = { email: { eq:
|
|
70284
|
-
|
|
70285
|
-
where.assignee = { id: { eq:
|
|
70286
|
-
|
|
70287
|
-
|
|
70288
|
-
if (
|
|
70289
|
-
|
|
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;
|
|
70290
70303
|
} else {
|
|
70291
|
-
where.state = { type: { in: [
|
|
70292
|
-
}
|
|
70293
|
-
|
|
70294
|
-
|
|
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
|
+
}
|
|
70295
70320
|
}
|
|
70321
|
+
return where;
|
|
70322
|
+
}
|
|
70323
|
+
async function fetchOpenIssues(apiKey, spec) {
|
|
70324
|
+
const where = buildIssueFilter(spec);
|
|
70296
70325
|
const query = `query Issues($filter: IssueFilter) {
|
|
70297
70326
|
issues(filter: $filter, first: 50) {
|
|
70298
70327
|
nodes {
|
|
@@ -70409,6 +70438,15 @@ async function addLabelToIssue(apiKey, issueId, labelId) {
|
|
|
70409
70438
|
labelId
|
|
70410
70439
|
});
|
|
70411
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
|
+
}
|
|
70412
70450
|
|
|
70413
70451
|
// apps/cli/src/agent/coordinator.ts
|
|
70414
70452
|
class AgentCoordinator {
|
|
@@ -70418,6 +70456,7 @@ class AgentCoordinator {
|
|
|
70418
70456
|
pendingIds = new Set;
|
|
70419
70457
|
queue = [];
|
|
70420
70458
|
stopped = false;
|
|
70459
|
+
conflictNotified = new Set;
|
|
70421
70460
|
constructor(deps, opts) {
|
|
70422
70461
|
this.deps = deps;
|
|
70423
70462
|
this.opts = opts;
|
|
@@ -70435,58 +70474,86 @@ class AgentCoordinator {
|
|
|
70435
70474
|
async pollOnce() {
|
|
70436
70475
|
if (this.stopped)
|
|
70437
70476
|
return { found: 0, added: 0 };
|
|
70438
|
-
let
|
|
70477
|
+
let todo = [];
|
|
70478
|
+
let inProgress = [];
|
|
70479
|
+
let conflicted = [];
|
|
70439
70480
|
try {
|
|
70440
|
-
|
|
70481
|
+
[todo, inProgress, conflicted] = await Promise.all([
|
|
70482
|
+
this.deps.fetchTodo(),
|
|
70483
|
+
this.deps.fetchInProgress(),
|
|
70484
|
+
this.deps.fetchConflicted()
|
|
70485
|
+
]);
|
|
70441
70486
|
} catch (err) {
|
|
70442
70487
|
this.deps.onLog(`! Linear poll failed: ${err.message}`, "red");
|
|
70443
70488
|
return { found: 0, added: 0 };
|
|
70444
70489
|
}
|
|
70445
|
-
const
|
|
70446
|
-
const
|
|
70447
|
-
|
|
70448
|
-
tasksByIssueId.set(entry.issueId, entry);
|
|
70449
|
-
}
|
|
70450
|
-
const isProcessed = (id) => tasksByIssueId.get(id)?.state === "processed";
|
|
70451
|
-
const isFailed = (id) => tasksByIssueId.get(id)?.state === "failed";
|
|
70452
|
-
const queued = new Set(this.queue.map((i) => i.id));
|
|
70453
|
-
const active = new Set(this.workers.map((w) => w.issueId));
|
|
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);
|
|
70454
70493
|
let added = 0;
|
|
70455
|
-
for (const issue of
|
|
70456
|
-
if (
|
|
70457
|
-
continue;
|
|
70458
|
-
if (isFailed(issue.id))
|
|
70494
|
+
for (const issue of inProgress) {
|
|
70495
|
+
if (!eligible(issue.id))
|
|
70459
70496
|
continue;
|
|
70460
|
-
if (
|
|
70497
|
+
if (!this.dependenciesResolved(issue))
|
|
70461
70498
|
continue;
|
|
70462
|
-
|
|
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))
|
|
70463
70505
|
continue;
|
|
70464
|
-
|
|
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))
|
|
70465
70512
|
continue;
|
|
70466
|
-
|
|
70467
|
-
if (blocker !== undefined) {
|
|
70468
|
-
this.deps.onLog(` \u23F8 ${issue.identifier} skipped \u2014 blocked by unresolved dependency`, "yellow");
|
|
70513
|
+
if (!this.dependenciesResolved(issue))
|
|
70469
70514
|
continue;
|
|
70470
|
-
}
|
|
70471
|
-
|
|
70515
|
+
this.queue.push({ issue, mode: "fresh" });
|
|
70516
|
+
queuedIds.add(issue.id);
|
|
70472
70517
|
added += 1;
|
|
70473
70518
|
}
|
|
70474
70519
|
if (added > 0) {
|
|
70520
|
+
const modeRank = {
|
|
70521
|
+
resume: 0,
|
|
70522
|
+
"conflict-fix": 1,
|
|
70523
|
+
fresh: 2
|
|
70524
|
+
};
|
|
70475
70525
|
this.queue.sort((a, b) => {
|
|
70476
|
-
const pa = a.priority === 0 ? Infinity : a.priority;
|
|
70477
|
-
const pb = b.priority === 0 ? Infinity : b.priority;
|
|
70478
|
-
|
|
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];
|
|
70479
70531
|
});
|
|
70480
70532
|
}
|
|
70481
|
-
await this.deps.store.setLastPollAt(new Date().toISOString());
|
|
70482
70533
|
this.spawnNext();
|
|
70534
|
+
await this.scanDoneForConflicts();
|
|
70483
70535
|
await this.reportProgress();
|
|
70484
|
-
|
|
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;
|
|
70485
70553
|
}
|
|
70486
70554
|
async reportProgress() {
|
|
70487
|
-
const updater = this.deps.updater;
|
|
70488
70555
|
const everyN = this.opts.commentEveryIterations ?? 0;
|
|
70489
|
-
if (everyN <= 0 ||
|
|
70556
|
+
if (everyN <= 0 || this.opts.postComments === false || !this.deps.getIterationCount) {
|
|
70490
70557
|
return;
|
|
70491
70558
|
}
|
|
70492
70559
|
for (const w of this.workers) {
|
|
@@ -70504,29 +70571,78 @@ class AgentCoordinator {
|
|
|
70504
70571
|
if (currMilestone <= lastMilestone)
|
|
70505
70572
|
continue;
|
|
70506
70573
|
try {
|
|
70507
|
-
await
|
|
70574
|
+
await this.deps.postComment(w.issue, `\uD83D\uDD04 Ralph progress update: iteration ${count} on \`${w.changeName}\``);
|
|
70508
70575
|
w.lastReportedIteration = count;
|
|
70509
70576
|
} catch (err) {
|
|
70510
70577
|
this.deps.onLog(`! Linear progress comment failed for ${w.issueIdentifier}: ${err.message}`, "red");
|
|
70511
70578
|
}
|
|
70512
70579
|
}
|
|
70513
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
|
+
}
|
|
70514
70630
|
spawnNext() {
|
|
70515
70631
|
if (this.stopped)
|
|
70516
70632
|
return;
|
|
70517
70633
|
while (this.workers.length + this.pendingIds.size < this.opts.concurrency && this.queue.length > 0) {
|
|
70518
|
-
const
|
|
70519
|
-
this.pendingIds.add(issue.id);
|
|
70520
|
-
this.launchWorker(issue);
|
|
70634
|
+
const next = this.queue.shift();
|
|
70635
|
+
this.pendingIds.add(next.issue.id);
|
|
70636
|
+
this.launchWorker(next.issue, next.mode);
|
|
70521
70637
|
}
|
|
70522
70638
|
}
|
|
70523
|
-
async launchWorker(issue) {
|
|
70524
|
-
let
|
|
70639
|
+
async launchWorker(issue, mode) {
|
|
70640
|
+
let prep;
|
|
70525
70641
|
try {
|
|
70526
|
-
|
|
70642
|
+
prep = await this.deps.prepare(issue, mode);
|
|
70527
70643
|
} catch (err) {
|
|
70528
70644
|
this.pendingIds.delete(issue.id);
|
|
70529
|
-
this.deps.onLog(`!
|
|
70645
|
+
this.deps.onLog(`! prepare(${mode}) failed for ${issue.identifier}: ${err.message}`, "red");
|
|
70530
70646
|
this.spawnNext();
|
|
70531
70647
|
return;
|
|
70532
70648
|
}
|
|
@@ -70534,113 +70650,89 @@ class AgentCoordinator {
|
|
|
70534
70650
|
this.pendingIds.delete(issue.id);
|
|
70535
70651
|
return;
|
|
70536
70652
|
}
|
|
70537
|
-
{
|
|
70538
|
-
|
|
70539
|
-
|
|
70540
|
-
|
|
70541
|
-
|
|
70542
|
-
|
|
70543
|
-
});
|
|
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
|
+
}
|
|
70544
70659
|
}
|
|
70545
|
-
|
|
70546
|
-
|
|
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
|
+
}
|
|
70675
|
+
}
|
|
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);
|
|
70547
70678
|
const worker = {
|
|
70548
|
-
changeName,
|
|
70679
|
+
changeName: prep.changeName,
|
|
70549
70680
|
issueId: issue.id,
|
|
70550
70681
|
issueIdentifier: issue.identifier,
|
|
70551
70682
|
issue,
|
|
70683
|
+
mode,
|
|
70552
70684
|
kill: handle.kill,
|
|
70553
70685
|
lastReportedIteration: 0
|
|
70554
70686
|
};
|
|
70555
70687
|
this.workers.push(worker);
|
|
70556
70688
|
this.pendingIds.delete(issue.id);
|
|
70557
70689
|
this.deps.onWorkersChanged();
|
|
70558
|
-
|
|
70559
|
-
handle.exited.then((code) => {
|
|
70690
|
+
handle.exited.then(async (code) => {
|
|
70560
70691
|
const idx = this.workers.indexOf(worker);
|
|
70561
70692
|
if (idx >= 0)
|
|
70562
70693
|
this.workers.splice(idx, 1);
|
|
70563
70694
|
const ok = code === 0;
|
|
70564
|
-
this.deps.onLog(`${ok ? "\u2713" : "\u2717"} ${issue.identifier} \u2192 ${changeName} exited (code ${code})`, ok ? "green" : "red");
|
|
70565
|
-
this.
|
|
70566
|
-
state: ok ? "processed" : "failed",
|
|
70567
|
-
finishedAt: new Date().toISOString(),
|
|
70568
|
-
exitCode: code
|
|
70569
|
-
});
|
|
70570
|
-
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);
|
|
70571
70697
|
this.deps.onWorkersChanged();
|
|
70572
70698
|
this.spawnNext();
|
|
70573
70699
|
});
|
|
70574
70700
|
}
|
|
70575
|
-
async
|
|
70576
|
-
const updater = this.deps.updater;
|
|
70577
|
-
if (!updater)
|
|
70578
|
-
return;
|
|
70579
|
-
const alreadyCommented = this.deps.store.snapshot().tasks[issue.identifier]?.commentPosted === true;
|
|
70580
|
-
if (this.opts.postComments !== false && !alreadyCommented) {
|
|
70581
|
-
try {
|
|
70582
|
-
await updater.postComment(issue, `\uD83E\uDD16 Ralph started working on this issue. Tracking change: \`${changeName}\``);
|
|
70583
|
-
await this.deps.store.upsertTask(issue, { commentPosted: true });
|
|
70584
|
-
} catch (err) {
|
|
70585
|
-
this.deps.onLog(`! Linear comment failed for ${issue.identifier}: ${err.message}`, "red");
|
|
70586
|
-
}
|
|
70587
|
-
}
|
|
70588
|
-
if (this.opts.inProgressStatus) {
|
|
70589
|
-
await this.moveIssue(issue, this.opts.inProgressStatus);
|
|
70590
|
-
}
|
|
70591
|
-
}
|
|
70592
|
-
async notifyExited(issue, changeName, code) {
|
|
70593
|
-
const updater = this.deps.updater;
|
|
70594
|
-
if (!updater)
|
|
70595
|
-
return;
|
|
70701
|
+
async notifyExited(issue, changeName, code, mode) {
|
|
70596
70702
|
const ok = code === 0;
|
|
70597
70703
|
if (this.opts.postComments !== false) {
|
|
70598
|
-
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}\`
|
|
70599
70705
|
|
|
70600
|
-
` + `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.`;
|
|
70601
70707
|
try {
|
|
70602
|
-
await
|
|
70708
|
+
await this.deps.postComment(issue, body);
|
|
70603
70709
|
} catch (err) {
|
|
70604
70710
|
this.deps.onLog(`! Linear comment failed for ${issue.identifier}: ${err.message}`, "red");
|
|
70605
70711
|
}
|
|
70606
70712
|
}
|
|
70607
|
-
if (ok
|
|
70608
|
-
|
|
70609
|
-
|
|
70610
|
-
|
|
70611
|
-
|
|
70612
|
-
|
|
70613
|
-
|
|
70614
|
-
|
|
70615
|
-
|
|
70616
|
-
|
|
70617
|
-
|
|
70618
|
-
|
|
70619
|
-
|
|
70620
|
-
|
|
70621
|
-
|
|
70622
|
-
|
|
70623
|
-
this.deps.onLog(`! Linear label '${labelName}' not found for ${issue.identifier}`, "yellow");
|
|
70624
|
-
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
|
+
}
|
|
70625
70729
|
}
|
|
70626
|
-
|
|
70627
|
-
|
|
70628
|
-
|
|
70629
|
-
|
|
70630
|
-
|
|
70631
|
-
}
|
|
70632
|
-
async moveIssue(issue, stateName) {
|
|
70633
|
-
const updater = this.deps.updater;
|
|
70634
|
-
try {
|
|
70635
|
-
const stateId = await updater.resolveStateId(issue, stateName);
|
|
70636
|
-
if (!stateId) {
|
|
70637
|
-
this.deps.onLog(`! Linear state '${stateName}' not found for ${issue.identifier}`, "yellow");
|
|
70638
|
-
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");
|
|
70639
70735
|
}
|
|
70640
|
-
await updater.setState(issue, stateId);
|
|
70641
|
-
this.deps.onLog(` \u2192 ${issue.identifier} moved to '${stateName}'`, "gray");
|
|
70642
|
-
} catch (err) {
|
|
70643
|
-
this.deps.onLog(`! Linear state move failed for ${issue.identifier}: ${err.message}`, "red");
|
|
70644
70736
|
}
|
|
70645
70737
|
}
|
|
70646
70738
|
stop() {
|
|
@@ -70654,7 +70746,7 @@ class AgentCoordinator {
|
|
|
70654
70746
|
}
|
|
70655
70747
|
|
|
70656
70748
|
// apps/cli/src/agent/scaffold.ts
|
|
70657
|
-
import { join as
|
|
70749
|
+
import { join as join12 } from "path";
|
|
70658
70750
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
70659
70751
|
function changeNameForIssue(issue) {
|
|
70660
70752
|
const slug = issue.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
@@ -70662,10 +70754,10 @@ function changeNameForIssue(issue) {
|
|
|
70662
70754
|
}
|
|
70663
70755
|
async function scaffoldChangeForIssue(tasksDir, statesDir, issue, comments = [], appendPrompt = "") {
|
|
70664
70756
|
const name = changeNameForIssue(issue);
|
|
70665
|
-
const changeDir =
|
|
70666
|
-
const stateDir =
|
|
70757
|
+
const changeDir = join12(tasksDir, name);
|
|
70758
|
+
const stateDir = join12(statesDir, name);
|
|
70667
70759
|
await mkdir2(changeDir, { recursive: true });
|
|
70668
|
-
await mkdir2(
|
|
70760
|
+
await mkdir2(join12(changeDir, "specs"), { recursive: true });
|
|
70669
70761
|
await mkdir2(stateDir, { recursive: true });
|
|
70670
70762
|
const commentsBlock = comments.length > 0 ? [
|
|
70671
70763
|
"",
|
|
@@ -70717,25 +70809,25 @@ async function scaffoldChangeForIssue(tasksDir, statesDir, issue, comments = [],
|
|
|
70717
70809
|
""
|
|
70718
70810
|
].join(`
|
|
70719
70811
|
`);
|
|
70720
|
-
await Bun.write(
|
|
70721
|
-
await Bun.write(
|
|
70722
|
-
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);
|
|
70723
70815
|
return name;
|
|
70724
70816
|
}
|
|
70725
70817
|
|
|
70726
70818
|
// apps/cli/src/agent/worktree.ts
|
|
70727
|
-
import { basename, join as
|
|
70819
|
+
import { basename, join as join13 } from "path";
|
|
70728
70820
|
import { homedir as homedir2 } from "os";
|
|
70729
70821
|
import { exists } from "fs/promises";
|
|
70730
70822
|
function worktreesDir(projectRoot) {
|
|
70731
|
-
return
|
|
70823
|
+
return join13(homedir2(), ".ralph", basename(projectRoot), "worktrees");
|
|
70732
70824
|
}
|
|
70733
70825
|
function branchForChange(changeName) {
|
|
70734
70826
|
return `ralph/${changeName}`;
|
|
70735
70827
|
}
|
|
70736
70828
|
async function createWorktree(projectRoot, changeName, runner) {
|
|
70737
70829
|
const dir = worktreesDir(projectRoot);
|
|
70738
|
-
const cwd2 =
|
|
70830
|
+
const cwd2 = join13(dir, changeName);
|
|
70739
70831
|
const branch = branchForChange(changeName);
|
|
70740
70832
|
const list = await runner.run(["worktree", "list", "--porcelain"], projectRoot);
|
|
70741
70833
|
if (list.stdout.includes(`worktree ${cwd2}
|
|
@@ -70792,8 +70884,8 @@ async function isWorktreeSafeToRemove(cwd2, base2, runner) {
|
|
|
70792
70884
|
return { safe: true, dirty, unpushedCommits };
|
|
70793
70885
|
}
|
|
70794
70886
|
async function seedWorktreeMcpConfig(projectRoot, worktreeCwd) {
|
|
70795
|
-
const dst =
|
|
70796
|
-
const src =
|
|
70887
|
+
const dst = join13(worktreeCwd, ".mcp.json");
|
|
70888
|
+
const src = join13(projectRoot, ".mcp.json");
|
|
70797
70889
|
const source = await exists(dst) ? dst : await exists(src) ? src : null;
|
|
70798
70890
|
if (!source)
|
|
70799
70891
|
return;
|
|
@@ -70807,7 +70899,7 @@ async function seedWorktreeMcpConfig(projectRoot, worktreeCwd) {
|
|
|
70807
70899
|
if (servers && typeof servers === "object") {
|
|
70808
70900
|
for (const cfg of Object.values(servers)) {
|
|
70809
70901
|
if (Array.isArray(cfg.args)) {
|
|
70810
|
-
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);
|
|
70811
70903
|
}
|
|
70812
70904
|
}
|
|
70813
70905
|
}
|
|
@@ -70816,7 +70908,7 @@ async function seedWorktreeMcpConfig(projectRoot, worktreeCwd) {
|
|
|
70816
70908
|
}
|
|
70817
70909
|
|
|
70818
70910
|
// apps/cli/src/agent/post-task.ts
|
|
70819
|
-
import { join as
|
|
70911
|
+
import { join as join14 } from "path";
|
|
70820
70912
|
|
|
70821
70913
|
// apps/cli/src/agent/pr.ts
|
|
70822
70914
|
function defaultTitle(issue) {
|
|
@@ -71029,7 +71121,7 @@ async function runPostTask(input, deps) {
|
|
|
71029
71121
|
const maxHookFixAttempts = cfg.maxCiFixAttempts;
|
|
71030
71122
|
const runWorkerWithFixTask = async (heading, failureOutput) => {
|
|
71031
71123
|
try {
|
|
71032
|
-
await prependFixTask(
|
|
71124
|
+
await prependFixTask(join14(changeDir, "tasks.md"), heading, failureOutput);
|
|
71033
71125
|
} catch (err) {
|
|
71034
71126
|
log2(`! could not prepend fix task: ${err.message}`, "red");
|
|
71035
71127
|
return 1;
|
|
@@ -71192,6 +71284,7 @@ ${reBlob.trim()}`);
|
|
|
71192
71284
|
log2(` no commits ahead of ${cfg.prBaseBranch} \u2014 skipping PR`, "gray");
|
|
71193
71285
|
} else {
|
|
71194
71286
|
log2(` ${pr.created ? "opened" : "found existing"} PR: ${pr.url}`, "green");
|
|
71287
|
+
deps.registerPr?.(changeName, pr.url);
|
|
71195
71288
|
if (wantFixCi) {
|
|
71196
71289
|
log2(` watching CI for ${pr.url} (max ${cfg.maxCiFixAttempts} fix attempts)`, "gray");
|
|
71197
71290
|
emit("ci-poll", "starting");
|
|
@@ -71201,7 +71294,7 @@ ${reBlob.trim()}`);
|
|
|
71201
71294
|
getFailedLogs: (ids) => fetchFailedRunLogs(ids, cmd, cwd2),
|
|
71202
71295
|
runTaskWithSteering: async (steering) => {
|
|
71203
71296
|
try {
|
|
71204
|
-
await prependFixTask(
|
|
71297
|
+
await prependFixTask(join14(changeDir, "tasks.md"), "Fix failing CI checks", steering);
|
|
71205
71298
|
} catch (err) {
|
|
71206
71299
|
log2(`! could not prepend fix task: ${err.message}`, "red");
|
|
71207
71300
|
}
|
|
@@ -71309,6 +71402,34 @@ function traceCmdRunner(base2, onStart, onEnd) {
|
|
|
71309
71402
|
}
|
|
71310
71403
|
};
|
|
71311
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
|
+
}
|
|
71312
71433
|
function buildAgentCoordinator(input) {
|
|
71313
71434
|
const {
|
|
71314
71435
|
args,
|
|
@@ -71317,7 +71438,6 @@ function buildAgentCoordinator(input) {
|
|
|
71317
71438
|
statesDir,
|
|
71318
71439
|
tasksDir,
|
|
71319
71440
|
apiKey,
|
|
71320
|
-
store,
|
|
71321
71441
|
onLog,
|
|
71322
71442
|
onWorkersChanged,
|
|
71323
71443
|
onWorkerStarted,
|
|
@@ -71326,28 +71446,96 @@ function buildAgentCoordinator(input) {
|
|
|
71326
71446
|
onWorkerOutput,
|
|
71327
71447
|
onWorkerCmd
|
|
71328
71448
|
} = input;
|
|
71329
|
-
const logsDir =
|
|
71449
|
+
const logsDir = join15(projectRoot, ".ralph", "logs");
|
|
71330
71450
|
const concurrency = args.concurrency || cfg.concurrency;
|
|
71331
71451
|
const pollInterval = args.pollInterval || cfg.pollIntervalSeconds;
|
|
71332
|
-
const
|
|
71333
|
-
const
|
|
71334
|
-
const
|
|
71335
|
-
const
|
|
71336
|
-
|
|
71337
|
-
|
|
71338
|
-
statuses: effectiveStatuses,
|
|
71339
|
-
labels: args.linearLabel.length ? args.linearLabel : cfg.linear.labels
|
|
71340
|
-
};
|
|
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;
|
|
71341
71458
|
const stateCache = new Map;
|
|
71342
71459
|
const labelCache = new Map;
|
|
71343
71460
|
const teamKeyOf = (issue) => issue.identifier.split("-")[0];
|
|
71344
|
-
|
|
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
|
+
}
|
|
71345
71531
|
const cwdByChange = new Map;
|
|
71346
71532
|
const statesDirByChange = new Map;
|
|
71347
71533
|
const branchByChange = new Map;
|
|
71348
71534
|
const issueByChange = new Map;
|
|
71349
|
-
|
|
71350
|
-
|
|
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) => {
|
|
71351
71539
|
const proc = Bun.spawn({
|
|
71352
71540
|
cmd: ["sh", "-c", cmd],
|
|
71353
71541
|
cwd: cwd2,
|
|
@@ -71358,51 +71546,112 @@ function buildAgentCoordinator(input) {
|
|
|
71358
71546
|
const code = await proc.exited;
|
|
71359
71547
|
if (code !== 0) {
|
|
71360
71548
|
const stderr = await new Response(proc.stderr).text();
|
|
71361
|
-
onLog(`!
|
|
71549
|
+
onLog(`! script exited code ${code}${stderr ? `: ${stderr.trim().split(`
|
|
71362
71550
|
`)[0]}` : ""}`, "yellow");
|
|
71363
71551
|
}
|
|
71364
|
-
|
|
71365
|
-
|
|
71366
|
-
|
|
71367
|
-
|
|
71368
|
-
|
|
71369
|
-
|
|
71370
|
-
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");
|
|
71371
71559
|
}
|
|
71560
|
+
}
|
|
71561
|
+
async function setupWorktree(issue) {
|
|
71372
71562
|
let workerCwd = projectRoot;
|
|
71373
71563
|
let scaffoldTasksDir = tasksDir;
|
|
71374
71564
|
let scaffoldStatesDir = statesDir;
|
|
71375
|
-
let
|
|
71565
|
+
let branch = null;
|
|
71566
|
+
if (!useWorktree)
|
|
71567
|
+
return { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch };
|
|
71376
71568
|
const probeName = issue.identifier.toLowerCase();
|
|
71377
|
-
|
|
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");
|
|
71378
71577
|
try {
|
|
71379
|
-
|
|
71380
|
-
workerCwd = wt.cwd;
|
|
71381
|
-
workerBranch = wt.branch;
|
|
71382
|
-
const wtLayout = projectLayout(wt.cwd);
|
|
71383
|
-
scaffoldTasksDir = wtLayout.tasksDir;
|
|
71384
|
-
scaffoldStatesDir = wtLayout.statesDir;
|
|
71385
|
-
onLog(` ${issue.identifier} worktree: ${wt.cwd} (${wt.branch})`, "gray");
|
|
71386
|
-
try {
|
|
71387
|
-
await seedWorktreeMcpConfig(projectRoot, wt.cwd);
|
|
71388
|
-
} catch (err) {
|
|
71389
|
-
onLog(`! seeding .mcp.json failed for ${issue.identifier}: ${err.message}`, "yellow");
|
|
71390
|
-
}
|
|
71578
|
+
await seedWorktreeMcpConfig(projectRoot, wt.cwd);
|
|
71391
71579
|
} catch (err) {
|
|
71392
|
-
onLog(`!
|
|
71580
|
+
onLog(`! seeding .mcp.json failed for ${issue.identifier}: ${err.message}`, "yellow");
|
|
71393
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);
|
|
71594
|
+
} catch (err) {
|
|
71595
|
+
onLog(`! Linear comment fetch failed for ${issue.identifier}: ${err.message}`, "yellow");
|
|
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 });
|
|
71394
71604
|
}
|
|
71395
|
-
const appendPrompt = args.prompt || cfg.appendPrompt || "";
|
|
71396
|
-
const changeName = await scaffoldChangeForIssue(scaffoldTasksDir, scaffoldStatesDir, issue, comments, appendPrompt);
|
|
71397
71605
|
cwdByChange.set(changeName, workerCwd);
|
|
71398
71606
|
statesDirByChange.set(changeName, scaffoldStatesDir);
|
|
71399
71607
|
issueByChange.set(changeName, issue);
|
|
71400
|
-
if (
|
|
71401
|
-
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
|
+
}
|
|
71402
71632
|
if (cfg.setupScript) {
|
|
71403
71633
|
await runScript("setup", cfg.setupScript, workerCwd);
|
|
71404
71634
|
}
|
|
71405
|
-
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
|
+
}
|
|
71406
71655
|
}
|
|
71407
71656
|
function buildTaskCmdFor(changeName) {
|
|
71408
71657
|
const c = [
|
|
@@ -71435,9 +71684,8 @@ function buildAgentCoordinator(input) {
|
|
|
71435
71684
|
c.push("--verbose");
|
|
71436
71685
|
return c;
|
|
71437
71686
|
}
|
|
71438
|
-
function
|
|
71439
|
-
const
|
|
71440
|
-
const logFilePath = join16(logsDir, `${changeName}.log`);
|
|
71687
|
+
function defaultSpawn(changeName, cmd, cwd2, note) {
|
|
71688
|
+
const logFilePath = join15(logsDir, `${changeName}.log`);
|
|
71441
71689
|
let logWriter = null;
|
|
71442
71690
|
const ensureLogWriter = async () => {
|
|
71443
71691
|
if (logWriter)
|
|
@@ -71489,34 +71737,49 @@ function buildAgentCoordinator(input) {
|
|
|
71489
71737
|
} catch {}
|
|
71490
71738
|
}
|
|
71491
71739
|
}
|
|
71492
|
-
const
|
|
71493
|
-
|
|
71494
|
-
|
|
71495
|
-
|
|
71496
|
-
|
|
71497
|
-
|
|
71498
|
-
|
|
71499
|
-
|
|
71500
|
-
|
|
71501
|
-
|
|
71740
|
+
const p = Bun.spawn({
|
|
71741
|
+
cmd,
|
|
71742
|
+
cwd: cwd2,
|
|
71743
|
+
stdout: "pipe",
|
|
71744
|
+
stderr: "pipe",
|
|
71745
|
+
stdin: "ignore"
|
|
71746
|
+
});
|
|
71747
|
+
(async () => {
|
|
71748
|
+
const writer = await ensureLogWriter();
|
|
71749
|
+
if (note && writer)
|
|
71750
|
+
writer.write(`
|
|
71502
71751
|
--- ${note} ---
|
|
71503
71752
|
`);
|
|
71504
|
-
|
|
71505
|
-
|
|
71506
|
-
|
|
71507
|
-
};
|
|
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
|
+
}
|
|
71508
71771
|
const respawn = () => {
|
|
71509
71772
|
onWorkerPhase?.(changeName, "working", "respawn");
|
|
71510
|
-
|
|
71511
|
-
|
|
71773
|
+
if (injected)
|
|
71774
|
+
return injected(buildTaskCmdFor(changeName), cwd2).exited;
|
|
71775
|
+
return defaultSpawn(changeName, buildTaskCmdFor(changeName), cwd2, `respawn at ${new Date().toISOString()}`).exited;
|
|
71512
71776
|
};
|
|
71513
|
-
const proc = launch(`spawn at ${new Date().toISOString()}`);
|
|
71514
71777
|
onWorkerStarted(changeName, statesDirByChange.get(changeName) ?? statesDir, logFilePath);
|
|
71515
71778
|
onWorkerPhase?.(changeName, "working");
|
|
71516
|
-
const tracedCmd = onWorkerCmd ? traceCmdRunner(
|
|
71779
|
+
const tracedCmd = onWorkerCmd ? traceCmdRunner(cmdRunner, (cmd) => onWorkerCmd(changeName, cmd, "start"), (cmd, ms, ok) => onWorkerCmd(changeName, cmd, "end", ms, ok)) : cmdRunner;
|
|
71517
71780
|
const wantPr = args.createPr || cfg.createPrOnSuccess;
|
|
71518
71781
|
const wantFixCi = args.fixCi || cfg.fixCiOnFailure;
|
|
71519
|
-
const wrapped =
|
|
71782
|
+
const wrapped = handle.exited.then(async (code) => {
|
|
71520
71783
|
const workerLayout = projectLayout(cwd2);
|
|
71521
71784
|
const effectiveCode = await runPostTask({
|
|
71522
71785
|
changeName,
|
|
@@ -71540,17 +71803,17 @@ function buildAgentCoordinator(input) {
|
|
|
71540
71803
|
respawnWorker: respawn
|
|
71541
71804
|
}, {
|
|
71542
71805
|
cmd: tracedCmd,
|
|
71543
|
-
git:
|
|
71806
|
+
git: gitRunner,
|
|
71544
71807
|
log: onLog,
|
|
71545
71808
|
runScript,
|
|
71809
|
+
registerPr: (cn, url) => {
|
|
71810
|
+
prByChange.set(cn, url);
|
|
71811
|
+
prUnavailable.delete(cn);
|
|
71812
|
+
},
|
|
71546
71813
|
...onWorkerPhase && {
|
|
71547
71814
|
onPhase: (phase, detail) => onWorkerPhase(changeName, phase, detail)
|
|
71548
71815
|
}
|
|
71549
71816
|
});
|
|
71550
|
-
try {
|
|
71551
|
-
logWriter?.flush();
|
|
71552
|
-
await logWriter?.end();
|
|
71553
|
-
} catch {}
|
|
71554
71817
|
cwdByChange.delete(changeName);
|
|
71555
71818
|
statesDirByChange.delete(changeName);
|
|
71556
71819
|
branchByChange.delete(changeName);
|
|
@@ -71558,13 +71821,73 @@ function buildAgentCoordinator(input) {
|
|
|
71558
71821
|
onWorkerExited(changeName);
|
|
71559
71822
|
return effectiveCode;
|
|
71560
71823
|
});
|
|
71561
|
-
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 });
|
|
71562
71875
|
}
|
|
71563
71876
|
const coord = new AgentCoordinator({
|
|
71564
|
-
|
|
71565
|
-
|
|
71877
|
+
fetchTodo: () => fetchByGet(indicators.getTodo, excludeFromTodo),
|
|
71878
|
+
fetchInProgress: () => fetchByGet(indicators.getInProgress, []),
|
|
71879
|
+
fetchConflicted: () => fetchByGet(indicators.getConflicted, []),
|
|
71880
|
+
fetchDoneCandidates,
|
|
71881
|
+
prepare,
|
|
71566
71882
|
spawnWorker,
|
|
71567
|
-
|
|
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,
|
|
71568
71891
|
onLog,
|
|
71569
71892
|
onWorkersChanged,
|
|
71570
71893
|
getIterationCount: async (changeName) => {
|
|
@@ -71574,42 +71897,29 @@ function buildAgentCoordinator(input) {
|
|
|
71574
71897
|
return 0;
|
|
71575
71898
|
const json = await file.json();
|
|
71576
71899
|
return json.iteration ?? 0;
|
|
71577
|
-
},
|
|
71578
|
-
updater: {
|
|
71579
|
-
postComment: (issue, body) => addIssueComment(apiKey, issue.id, body),
|
|
71580
|
-
setState: (issue, stateId) => updateIssueState(apiKey, issue.id, stateId),
|
|
71581
|
-
resolveStateId: async (issue, stateName) => {
|
|
71582
|
-
const team = teamKeyOf(issue);
|
|
71583
|
-
let map2 = stateCache.get(team);
|
|
71584
|
-
if (!map2) {
|
|
71585
|
-
const states = await fetchWorkflowStates(apiKey, team);
|
|
71586
|
-
map2 = new Map(states.map((s) => [s.name.toLowerCase(), s.id]));
|
|
71587
|
-
stateCache.set(team, map2);
|
|
71588
|
-
}
|
|
71589
|
-
return map2.get(stateName.toLowerCase()) ?? null;
|
|
71590
|
-
},
|
|
71591
|
-
addLabel: (issue, labelId) => addLabelToIssue(apiKey, issue.id, labelId),
|
|
71592
|
-
resolveLabelId: async (issue, labelName) => {
|
|
71593
|
-
const team = teamKeyOf(issue);
|
|
71594
|
-
let map2 = labelCache.get(team);
|
|
71595
|
-
if (!map2) {
|
|
71596
|
-
const labels = await fetchIssueLabels(apiKey, team);
|
|
71597
|
-
map2 = new Map(labels.map((l) => [l.name.toLowerCase(), l.id]));
|
|
71598
|
-
labelCache.set(team, map2);
|
|
71599
|
-
}
|
|
71600
|
-
return map2.get(labelName.toLowerCase()) ?? null;
|
|
71601
|
-
}
|
|
71602
71900
|
}
|
|
71603
71901
|
}, {
|
|
71604
71902
|
concurrency,
|
|
71605
|
-
|
|
71606
|
-
|
|
71607
|
-
|
|
71608
|
-
|
|
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 } : {},
|
|
71609
71908
|
postComments: cfg.linear.postComments,
|
|
71610
71909
|
commentEveryIterations: cfg.linear.updateEveryIterations
|
|
71611
71910
|
});
|
|
71612
|
-
|
|
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);
|
|
71613
71923
|
return {
|
|
71614
71924
|
coord,
|
|
71615
71925
|
filterDesc,
|
|
@@ -71618,6 +71928,21 @@ function buildAgentCoordinator(input) {
|
|
|
71618
71928
|
getWorkerCwd: (changeName) => cwdByChange.get(changeName)
|
|
71619
71929
|
};
|
|
71620
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
|
+
}
|
|
71621
71946
|
|
|
71622
71947
|
// apps/cli/src/components/AgentMode.tsx
|
|
71623
71948
|
var jsx_dev_runtime9 = __toESM(require_jsx_dev_runtime(), 1);
|
|
@@ -71669,8 +71994,6 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
71669
71994
|
exit();
|
|
71670
71995
|
return;
|
|
71671
71996
|
}
|
|
71672
|
-
const store = new AgentStateStore(projectRoot);
|
|
71673
|
-
await store.load();
|
|
71674
71997
|
const { coord: coord2, filterDesc, concurrency, pollInterval } = buildAgentCoordinator({
|
|
71675
71998
|
args,
|
|
71676
71999
|
cfg,
|
|
@@ -71678,7 +72001,6 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
71678
72001
|
statesDir,
|
|
71679
72002
|
tasksDir,
|
|
71680
72003
|
apiKey,
|
|
71681
|
-
store,
|
|
71682
72004
|
onLog: appendLog,
|
|
71683
72005
|
onWorkersChanged: () => setTick((t) => t + 1),
|
|
71684
72006
|
onWorkerStarted: (changeName, dir, logFile) => {
|
|
@@ -71780,7 +72102,7 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
71780
72102
|
(async () => {
|
|
71781
72103
|
for (const [changeName, meta] of workerMetaRef.current) {
|
|
71782
72104
|
try {
|
|
71783
|
-
const file = Bun.file(
|
|
72105
|
+
const file = Bun.file(join16(meta.statesDir, changeName, ".ralph-state.json"));
|
|
71784
72106
|
if (await file.exists()) {
|
|
71785
72107
|
const json = await file.json();
|
|
71786
72108
|
meta.iter = json.iteration ?? meta.iter;
|
|
@@ -71898,11 +72220,11 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
|
|
|
71898
72220
|
}
|
|
71899
72221
|
|
|
71900
72222
|
// packages/openspec/src/openspec-change-store.ts
|
|
71901
|
-
import { join as
|
|
71902
|
-
import { readdir, mkdir as
|
|
72223
|
+
import { join as join17, dirname as dirname4 } from "path";
|
|
72224
|
+
import { readdir, mkdir as mkdir4 } from "fs/promises";
|
|
71903
72225
|
function resolveOpenspecBin() {
|
|
71904
72226
|
const pkgJsonPath = Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir);
|
|
71905
|
-
return
|
|
72227
|
+
return join17(dirname4(pkgJsonPath), "bin", "openspec.js");
|
|
71906
72228
|
}
|
|
71907
72229
|
function runOpenspec(args, options = {}) {
|
|
71908
72230
|
const stdio = options.inherit ? ["inherit", "inherit", "inherit"] : ["ignore", "pipe", "pipe"];
|
|
@@ -71928,7 +72250,7 @@ class OpenSpecChangeStore {
|
|
|
71928
72250
|
}
|
|
71929
72251
|
}
|
|
71930
72252
|
getChangeDirectory(name) {
|
|
71931
|
-
return
|
|
72253
|
+
return join17("openspec", "changes", name);
|
|
71932
72254
|
}
|
|
71933
72255
|
async listChanges() {
|
|
71934
72256
|
const result2 = runOpenspec(["list", "--json"]);
|
|
@@ -71942,7 +72264,7 @@ class OpenSpecChangeStore {
|
|
|
71942
72264
|
}
|
|
71943
72265
|
} catch {}
|
|
71944
72266
|
}
|
|
71945
|
-
const changesDir =
|
|
72267
|
+
const changesDir = join17("openspec", "changes");
|
|
71946
72268
|
if (!await Bun.file(changesDir).exists())
|
|
71947
72269
|
return [];
|
|
71948
72270
|
try {
|
|
@@ -71953,29 +72275,29 @@ class OpenSpecChangeStore {
|
|
|
71953
72275
|
}
|
|
71954
72276
|
}
|
|
71955
72277
|
async readTaskList(name) {
|
|
71956
|
-
const file = Bun.file(
|
|
72278
|
+
const file = Bun.file(join17("openspec", "changes", name, "tasks.md"));
|
|
71957
72279
|
if (!await file.exists())
|
|
71958
72280
|
return "";
|
|
71959
72281
|
return await file.text();
|
|
71960
72282
|
}
|
|
71961
72283
|
async writeTaskList(name, content) {
|
|
71962
|
-
const path =
|
|
71963
|
-
await
|
|
72284
|
+
const path = join17("openspec", "changes", name, "tasks.md");
|
|
72285
|
+
await mkdir4(dirname4(path), { recursive: true });
|
|
71964
72286
|
await Bun.write(path, content);
|
|
71965
72287
|
}
|
|
71966
72288
|
async appendSteering(name, message) {
|
|
71967
|
-
const path =
|
|
72289
|
+
const path = join17("openspec", "changes", name, "steering.md");
|
|
71968
72290
|
const file = Bun.file(path);
|
|
71969
72291
|
const existing = await file.exists() ? await file.text() : null;
|
|
71970
72292
|
const updated = existing ? `${message}
|
|
71971
72293
|
|
|
71972
72294
|
${existing.trimStart()}` : `${message}
|
|
71973
72295
|
`;
|
|
71974
|
-
await
|
|
72296
|
+
await mkdir4(dirname4(path), { recursive: true });
|
|
71975
72297
|
await Bun.write(path, updated);
|
|
71976
72298
|
}
|
|
71977
72299
|
async readSection(name, artifact, heading) {
|
|
71978
|
-
const file = Bun.file(
|
|
72300
|
+
const file = Bun.file(join17("openspec", "changes", name, artifact));
|
|
71979
72301
|
if (!await file.exists())
|
|
71980
72302
|
return "";
|
|
71981
72303
|
const content = await file.text();
|
|
@@ -72057,8 +72379,8 @@ function App2({ args, statesDir, tasksDir, projectRoot }) {
|
|
|
72057
72379
|
message: "Error: --name is required for status mode"
|
|
72058
72380
|
}, undefined, false, undefined, this);
|
|
72059
72381
|
}
|
|
72060
|
-
const stateDir =
|
|
72061
|
-
if (getStorage().read(
|
|
72382
|
+
const stateDir = join18(statesDir, args.name);
|
|
72383
|
+
if (getStorage().read(join18(stateDir, ".ralph-state.json")) === null) {
|
|
72062
72384
|
return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(ErrorMessage, {
|
|
72063
72385
|
message: `Error: change '${args.name}' not found`
|
|
72064
72386
|
}, undefined, false, undefined, this);
|
|
@@ -72119,7 +72441,7 @@ if (typeof globalThis.Bun === "undefined") {
|
|
|
72119
72441
|
async function findProjectRoot() {
|
|
72120
72442
|
let dir = process.cwd();
|
|
72121
72443
|
while (dir !== "/") {
|
|
72122
|
-
if (await exists2(
|
|
72444
|
+
if (await exists2(join19(dir, "openspec")))
|
|
72123
72445
|
return dir;
|
|
72124
72446
|
dir = resolve(dir, "..");
|
|
72125
72447
|
}
|
|
@@ -72158,8 +72480,8 @@ try {
|
|
|
72158
72480
|
const statesDir = layout.statesDir;
|
|
72159
72481
|
const tasksDir = layout.tasksDir;
|
|
72160
72482
|
if (args.mode === "init") {
|
|
72161
|
-
await
|
|
72162
|
-
const openspecBin =
|
|
72483
|
+
await mkdir5(statesDir, { recursive: true });
|
|
72484
|
+
const openspecBin = join19(dirname5(Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir)), "bin", "openspec.js");
|
|
72163
72485
|
Bun.spawnSync({
|
|
72164
72486
|
cmd: [process.execPath, openspecBin, "init", "--tools", "none", "--force"],
|
|
72165
72487
|
stdio: ["inherit", "inherit", "inherit"],
|
|
@@ -72172,9 +72494,9 @@ try {
|
|
|
72172
72494
|
`);
|
|
72173
72495
|
process.exit(1);
|
|
72174
72496
|
}
|
|
72175
|
-
const worktreeDir =
|
|
72176
|
-
const changeDir =
|
|
72177
|
-
const stateDir =
|
|
72497
|
+
const worktreeDir = join19(worktreesDir(projectRoot), args.name);
|
|
72498
|
+
const changeDir = join19(tasksDir, args.name);
|
|
72499
|
+
const stateDir = join19(statesDir, args.name);
|
|
72178
72500
|
const branch = `ralph/${args.name}`;
|
|
72179
72501
|
const removed = [];
|
|
72180
72502
|
if (await exists2(worktreeDir)) {
|
|
@@ -72212,14 +72534,6 @@ try {
|
|
|
72212
72534
|
await rm(stateDir, { recursive: true, force: true });
|
|
72213
72535
|
removed.push(`task state ${stateDir}`);
|
|
72214
72536
|
}
|
|
72215
|
-
try {
|
|
72216
|
-
const store = new AgentStateStore(projectRoot);
|
|
72217
|
-
await store.load();
|
|
72218
|
-
const removedEntry = await store.removeByChangeName(args.name);
|
|
72219
|
-
if (removedEntry) {
|
|
72220
|
-
removed.push(`agent-state entry for ${removedEntry.identifier} (${removedEntry.issueId})`);
|
|
72221
|
-
}
|
|
72222
|
-
} catch {}
|
|
72223
72537
|
if (removed.length === 0) {
|
|
72224
72538
|
process.stdout.write(`Nothing to clean for '${args.name}'
|
|
72225
72539
|
`);
|
|
@@ -72234,13 +72548,13 @@ try {
|
|
|
72234
72548
|
process.exit(0);
|
|
72235
72549
|
}
|
|
72236
72550
|
if (args.mode === "task" && args.name) {
|
|
72237
|
-
await
|
|
72238
|
-
await
|
|
72551
|
+
await mkdir5(join19(statesDir, args.name), { recursive: true });
|
|
72552
|
+
await mkdir5(join19(tasksDir, args.name), { recursive: true });
|
|
72239
72553
|
}
|
|
72240
72554
|
if (args.mode === "agent") {
|
|
72241
|
-
await
|
|
72242
|
-
await
|
|
72243
|
-
await
|
|
72555
|
+
await mkdir5(statesDir, { recursive: true });
|
|
72556
|
+
await mkdir5(tasksDir, { recursive: true });
|
|
72557
|
+
await mkdir5(join19(projectRoot, ".ralph"), { recursive: true });
|
|
72244
72558
|
}
|
|
72245
72559
|
await runWithContext(createDefaultContext(), async () => {
|
|
72246
72560
|
const { waitUntilExit } = render_default(import_react59.createElement(App2, { args, statesDir, tasksDir, projectRoot }));
|