@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.
Files changed (2) hide show
  1. package/dist/cli/index.js +757 -443
  2. 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 join20, dirname as dirname5 } from "path";
50841
- import { exists as exists2, mkdir as mkdir4, rm } from "fs/promises";
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.12.0",
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 (~/.ralph/<project>/worktrees/<name>)",
56542
- " --in-progress-status <name> Linear status to set when work starts on an issue",
56543
- " --done-status <name> Linear status to set when work completes successfully",
56544
- " --done-label <name> Linear label to add when work completes successfully",
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 the task on CI failures until green (needs --create-pr)",
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 task --name my-feature",
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
- inProgressStatus: "",
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 expectInProgressStatus = false;
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 (expectInProgressStatus) {
56707
- result2.inProgressStatus = arg;
56708
- expectInProgressStatus = false;
56709
- continue;
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 "--in-progress-status":
56801
- expectInProgressStatus = true;
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 join19 } from "path";
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 join17 } from "path";
70131
+ import { join as join16 } from "path";
70108
70132
 
70109
- // apps/cli/src/agent/state.ts
70133
+ // apps/cli/src/agent/config.ts
70110
70134
  import { join as join10 } from "path";
70111
- var TaskStateSchema = exports_external.enum(["started", "processed", "failed"]);
70112
- var TaskEntrySchema = exports_external.object({
70113
- issueId: exports_external.string(),
70114
- identifier: exports_external.string(),
70115
- state: TaskStateSchema,
70116
- changeName: exports_external.string().optional(),
70117
- startedAt: exports_external.string().optional(),
70118
- finishedAt: exports_external.string().optional(),
70119
- exitCode: exports_external.number().optional(),
70120
- commentPosted: exports_external.boolean().optional()
70135
+ var MarkerSchema = exports_external.object({
70136
+ type: exports_external.enum(["label", "status"]),
70137
+ value: exports_external.string().min(1)
70121
70138
  });
70122
- var AgentStateSchema = exports_external.object({
70123
- tasks: exports_external.record(exports_external.string(), TaskEntrySchema).default({}),
70124
- lastPollAt: exports_external.string().nullable().default(null)
70139
+ var GetIndicatorSchema = exports_external.object({
70140
+ filter: exports_external.array(MarkerSchema).default([])
70125
70141
  });
70126
- function statePath(projectRoot) {
70127
- return join10(projectRoot, ".ralph", "agent-state.json");
70128
- }
70129
- async function readState2(projectRoot) {
70130
- const file = Bun.file(statePath(projectRoot));
70131
- if (!await file.exists()) {
70132
- return AgentStateSchema.parse({});
70133
- }
70134
- const raw = await file.json();
70135
- return AgentStateSchema.parse(raw);
70136
- }
70137
- async function writeState2(projectRoot, state) {
70138
- await Bun.write(statePath(projectRoot), JSON.stringify(state, null, 2) + `
70139
- `);
70140
- }
70141
-
70142
- class AgentStateStore {
70143
- projectRoot;
70144
- state = null;
70145
- constructor(projectRoot) {
70146
- this.projectRoot = projectRoot;
70147
- }
70148
- async load() {
70149
- this.state = await readState2(this.projectRoot);
70150
- }
70151
- snapshot() {
70152
- if (!this.state) {
70153
- throw new Error("AgentStateStore: load() must be called before snapshot()");
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
- 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
- }).default({ statuses: [], labels: [], postComments: true })
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: { statuses: [], labels: [], postComments: true }
70223
+ linear: { postComments: true, updateEveryIterations: 10, indicators: {} }
70231
70224
  });
70232
70225
  async function loadRalphyConfig(projectRoot) {
70233
- const path = join11(projectRoot, "ralphy.config.json");
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 = join11(projectRoot, "ralphy.config.json");
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 join16 } from "path";
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 join12 } from "path";
70250
+ import { join as join11 } from "path";
70257
70251
  var STATE_FILE2 = ".ralph-state.json";
70258
70252
  function projectLayout(root) {
70259
- const statesDir = join12(root, ".ralph", "tasks");
70260
- const tasksDir = join12(root, "openspec", "changes");
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: join12(root, ".ralph", "agent-state.json"),
70266
- changeDir: (name) => join12(tasksDir, name),
70267
- taskStateDir: (name) => join12(statesDir, name),
70268
- stateFile: (name) => join12(statesDir, name, STATE_FILE2)
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
- async function fetchOpenIssues(apiKey, filter2) {
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 (filter2.team)
70278
- where.team = { key: { eq: filter2.team } };
70279
- if (filter2.assignee) {
70280
- if (filter2.assignee === "me") {
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
- } else if (filter2.assignee.includes("@")) {
70283
- where.assignee = { email: { eq: filter2.assignee } };
70284
- } else {
70285
- where.assignee = { id: { eq: filter2.assignee } };
70286
- }
70287
- }
70288
- if (filter2.statuses && filter2.statuses.length > 0) {
70289
- where.state = { name: { in: filter2.statuses } };
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: [...OPEN_STATE_TYPES] } };
70292
- }
70293
- if (filter2.labels && filter2.labels.length > 0) {
70294
- where.labels = { some: { name: { in: filter2.labels } } };
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 issues;
70477
+ let todo = [];
70478
+ let inProgress = [];
70479
+ let conflicted = [];
70439
70480
  try {
70440
- issues = await this.deps.fetchIssues(this.opts.filter);
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 state = this.deps.store.snapshot();
70446
- const tasksByIssueId = new Map;
70447
- for (const entry of Object.values(state.tasks)) {
70448
- tasksByIssueId.set(entry.issueId, entry);
70449
- }
70450
- const isProcessed = (id) => tasksByIssueId.get(id)?.state === "processed";
70451
- const isFailed = (id) => tasksByIssueId.get(id)?.state === "failed";
70452
- const queued = new Set(this.queue.map((i) => i.id));
70453
- const active = new Set(this.workers.map((w) => w.issueId));
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 issues) {
70456
- if (isProcessed(issue.id))
70457
- continue;
70458
- if (isFailed(issue.id))
70494
+ for (const issue of inProgress) {
70495
+ if (!eligible(issue.id))
70459
70496
  continue;
70460
- if (queued.has(issue.id))
70497
+ if (!this.dependenciesResolved(issue))
70461
70498
  continue;
70462
- if (active.has(issue.id))
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
- if (this.pendingIds.has(issue.id))
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
- const blocker = issue.blockedByIds.find((bid) => !isProcessed(bid));
70467
- if (blocker !== undefined) {
70468
- this.deps.onLog(` \u23F8 ${issue.identifier} skipped \u2014 blocked by unresolved dependency`, "yellow");
70513
+ if (!this.dependenciesResolved(issue))
70469
70514
  continue;
70470
- }
70471
- this.queue.push(issue);
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
- return pa - pb;
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
- return { found: issues.length, added };
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 || !updater || this.opts.postComments === false || !this.deps.getIterationCount) {
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 updater.postComment(w.issue, `\uD83D\uDD04 Ralph progress update: iteration ${count} on \`${w.changeName}\``);
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 issue = this.queue.shift();
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 changeName;
70639
+ async launchWorker(issue, mode) {
70640
+ let prep;
70525
70641
  try {
70526
- changeName = await this.deps.scaffold(issue);
70642
+ prep = await this.deps.prepare(issue, mode);
70527
70643
  } catch (err) {
70528
70644
  this.pendingIds.delete(issue.id);
70529
- this.deps.onLog(`! scaffold failed for ${issue.identifier}: ${err.message}`, "red");
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
- const existing = this.deps.store.snapshot().tasks[issue.identifier];
70539
- this.deps.store.upsertTask(issue, {
70540
- state: "started",
70541
- changeName,
70542
- startedAt: existing?.startedAt ?? new Date().toISOString()
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
- this.deps.onLog(`\u25B6 ${issue.identifier} \u2192 ${changeName} (worker started)`, "cyan");
70546
- const handle = this.deps.spawnWorker(changeName, issue);
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
- this.notifyStarted(issue, changeName);
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.deps.store.upsertTask(issue, {
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 notifyStarted(issue, changeName) {
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 underlying ` + `failure (e.g. lint/typecheck), then run \`ralph clean --name ${changeName}\` to ` + `clear the quarantine and let the next poll re-pick the issue.`;
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 updater.postComment(issue, body);
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 && this.opts.doneStatus) {
70608
- await this.moveIssue(issue, this.opts.doneStatus);
70609
- }
70610
- if (ok && this.opts.doneLabel) {
70611
- await this.tagIssue(issue, this.opts.doneLabel);
70612
- }
70613
- }
70614
- async tagIssue(issue, labelName) {
70615
- const updater = this.deps.updater;
70616
- if (!updater.resolveLabelId || !updater.addLabel) {
70617
- this.deps.onLog(`! Linear updater does not support labels (cannot tag ${issue.identifier} with '${labelName}')`, "yellow");
70618
- return;
70619
- }
70620
- try {
70621
- const labelId = await updater.resolveLabelId(issue, labelName);
70622
- if (!labelId) {
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
- await updater.addLabel(issue, labelId);
70627
- this.deps.onLog(` \u2192 ${issue.identifier} tagged with '${labelName}'`, "gray");
70628
- } catch (err) {
70629
- this.deps.onLog(`! Linear label add failed for ${issue.identifier}: ${err.message}`, "red");
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 join13 } from "path";
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 = join13(tasksDir, name);
70666
- const stateDir = join13(statesDir, name);
70757
+ const changeDir = join12(tasksDir, name);
70758
+ const stateDir = join12(statesDir, name);
70667
70759
  await mkdir2(changeDir, { recursive: true });
70668
- await mkdir2(join13(changeDir, "specs"), { recursive: true });
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(join13(changeDir, "proposal.md"), proposal);
70721
- await Bun.write(join13(changeDir, "tasks.md"), tasks);
70722
- await Bun.write(join13(changeDir, "design.md"), design);
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 join14 } from "path";
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 join14(homedir2(), ".ralph", basename(projectRoot), "worktrees");
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 = join14(dir, changeName);
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 = join14(worktreeCwd, ".mcp.json");
70796
- const src = join14(projectRoot, ".mcp.json");
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/") ? join14(projectRoot, a) : a);
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 join15 } from "path";
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(join15(changeDir, "tasks.md"), heading, failureOutput);
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(join15(changeDir, "tasks.md"), "Fix failing CI checks", steering);
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 = join16(projectRoot, ".ralph", "logs");
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 inProgressName = args.inProgressStatus || cfg.linear.inProgressStatus;
71333
- const baseStatuses = args.linearStatus.length ? args.linearStatus : cfg.linear.statuses;
71334
- const effectiveStatuses = inProgressName && baseStatuses.length > 0 && !baseStatuses.includes(inProgressName) ? [...baseStatuses, inProgressName] : baseStatuses;
71335
- const filter2 = {
71336
- team: args.linearTeam || cfg.linear.team,
71337
- assignee: args.linearAssignee || cfg.linear.assignee,
71338
- statuses: effectiveStatuses,
71339
- labels: args.linearLabel.length ? args.linearLabel : cfg.linear.labels
71340
- };
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
- const useWorktree = args.worktree || cfg.useWorktree;
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
- async function runScript(label, cmd, cwd2) {
71350
- onLog(` ${label}: ${cmd}`, "gray");
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(`! ${label} exited code ${code}${stderr ? `: ${stderr.trim().split(`
71549
+ onLog(`! script exited code ${code}${stderr ? `: ${stderr.trim().split(`
71362
71550
  `)[0]}` : ""}`, "yellow");
71363
71551
  }
71364
- }
71365
- async function scaffoldCallback(issue) {
71366
- let comments = [];
71367
- try {
71368
- comments = await fetchIssueComments(apiKey, issue.id);
71369
- } catch (err) {
71370
- onLog(`! Linear comment fetch failed for ${issue.identifier}: ${err.message}`, "yellow");
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 workerBranch = null;
71565
+ let branch = null;
71566
+ if (!useWorktree)
71567
+ return { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch };
71376
71568
  const probeName = issue.identifier.toLowerCase();
71377
- if (useWorktree) {
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
- const wt = await createWorktree(projectRoot, probeName, bunGitRunner);
71380
- workerCwd = wt.cwd;
71381
- workerBranch = wt.branch;
71382
- const wtLayout = projectLayout(wt.cwd);
71383
- scaffoldTasksDir = wtLayout.tasksDir;
71384
- scaffoldStatesDir = wtLayout.statesDir;
71385
- onLog(` ${issue.identifier} worktree: ${wt.cwd} (${wt.branch})`, "gray");
71386
- try {
71387
- await seedWorktreeMcpConfig(projectRoot, wt.cwd);
71388
- } catch (err) {
71389
- onLog(`! seeding .mcp.json failed for ${issue.identifier}: ${err.message}`, "yellow");
71390
- }
71578
+ await seedWorktreeMcpConfig(projectRoot, wt.cwd);
71391
71579
  } catch (err) {
71392
- onLog(`! worktree create failed for ${issue.identifier}: ${err.message} \u2014 falling back to project root`, "yellow");
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 (workerBranch)
71401
- branchByChange.set(changeName, workerBranch);
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 changeName;
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 spawnWorker(changeName) {
71439
- const cwd2 = cwdByChange.get(changeName) ?? projectRoot;
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 launch = (note) => {
71493
- const p = Bun.spawn({
71494
- cmd: buildTaskCmdFor(changeName),
71495
- cwd: cwd2,
71496
- stdout: "pipe",
71497
- stderr: "pipe",
71498
- stdin: "ignore"
71499
- });
71500
- if (note && logWriter)
71501
- logWriter.write(`
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
- pump(p.stdout, "out");
71505
- pump(p.stderr, "err");
71506
- return p;
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
- const rp = launch(`respawn at ${new Date().toISOString()}`);
71511
- return rp.exited;
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(bunCmdRunner, (cmd) => onWorkerCmd(changeName, cmd, "start"), (cmd, ms, ok) => onWorkerCmd(changeName, cmd, "end", ms, ok)) : bunCmdRunner;
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 = proc.exited.then(async (code) => {
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: bunGitRunner,
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: () => proc.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
- fetchIssues: (f2) => fetchOpenIssues(apiKey, f2),
71565
- scaffold: scaffoldCallback,
71877
+ fetchTodo: () => fetchByGet(indicators.getTodo, excludeFromTodo),
71878
+ fetchInProgress: () => fetchByGet(indicators.getInProgress, []),
71879
+ fetchConflicted: () => fetchByGet(indicators.getConflicted, []),
71880
+ fetchDoneCandidates,
71881
+ prepare,
71566
71882
  spawnWorker,
71567
- store,
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
- filter: filter2,
71606
- inProgressStatus: args.inProgressStatus || cfg.linear.inProgressStatus,
71607
- doneStatus: args.doneStatus || cfg.linear.doneStatus,
71608
- doneLabel: args.doneLabel || cfg.linear.doneLabel,
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
- const filterDesc = `team=${filter2.team ?? "*"}, assignee=${filter2.assignee ?? "*"}, statuses=` + `${filter2.statuses?.length ? filter2.statuses.join(",") : "open"}` + `${filter2.labels?.length ? `, labels=${filter2.labels.join(",")}` : ""}`;
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(join17(meta.statesDir, changeName, ".ralph-state.json"));
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 join18, dirname as dirname4 } from "path";
71902
- import { readdir, mkdir as mkdir3 } from "fs/promises";
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 join18(dirname4(pkgJsonPath), "bin", "openspec.js");
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 join18("openspec", "changes", name);
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 = join18("openspec", "changes");
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(join18("openspec", "changes", name, "tasks.md"));
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 = join18("openspec", "changes", name, "tasks.md");
71963
- await mkdir3(dirname4(path), { recursive: true });
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 = join18("openspec", "changes", name, "steering.md");
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 mkdir3(dirname4(path), { recursive: true });
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(join18("openspec", "changes", name, artifact));
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 = join19(statesDir, args.name);
72061
- if (getStorage().read(join19(stateDir, ".ralph-state.json")) === null) {
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(join20(dir, "openspec")))
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 mkdir4(statesDir, { recursive: true });
72162
- const openspecBin = join20(dirname5(Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir)), "bin", "openspec.js");
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 = join20(worktreesDir(projectRoot), args.name);
72176
- const changeDir = join20(tasksDir, args.name);
72177
- const stateDir = join20(statesDir, args.name);
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 mkdir4(join20(statesDir, args.name), { recursive: true });
72238
- await mkdir4(join20(tasksDir, args.name), { recursive: true });
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 mkdir4(statesDir, { recursive: true });
72242
- await mkdir4(tasksDir, { recursive: true });
72243
- await mkdir4(join20(projectRoot, ".ralph"), { recursive: true });
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 }));