@neriros/ralphy 2.7.8 → 2.8.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/README.md CHANGED
@@ -140,6 +140,8 @@ Use `setupScript` (run inside the worktree right after scaffolding) to install d
140
140
 
141
141
  **`fixCiOnFailure`** (or `--fix-ci`) watches the PR's checks via `gh pr checks` and, on failure, fetches the failed-run logs (`gh run view --log-failed`), appends them to `proposal.md` under `## Steering`, re-spawns the task loop in the worktree, and pushes the new commits — repeating until checks go green or `maxCiFixAttempts` is hit (default 5, polling interval `ciPollIntervalSeconds` defaults to 30s). Requires `--create-pr`.
142
142
 
143
+ When `fixCiOnFailure` is enabled, the issue is **not** moved to `doneStatus` (and `doneLabel` is not applied, and the issue is not marked processed in `.ralph/agent-state.json`) until CI actually goes green. If the fix loop exhausts its attempts the worker is treated as failed for completion-marking purposes and the issue will be re-picked-up on the next poll (the resume-in-progress filter ensures that).
144
+
143
145
  Every CLI flag is also configurable in `ralphy.config.json`; CLI values override config when both are set. The agent forwards `maxRuntimeMinutesPerTask` / `maxConsecutiveFailuresPerTask` / `iterationDelaySeconds` / `logRawStream` / `taskVerbose` to each spawned `ralph task` worker.
144
146
 
145
147
  Failed workers (non-zero exit) are not marked processed, so they'll be retried on the next poll. SIGINT/SIGTERM cleanly stops polling and kills active workers. All Linear side effects are best-effort — failures log a warning but never block the task loop.
package/dist/cli/index.js CHANGED
@@ -50549,7 +50549,7 @@ var require_axios = __commonJS((exports, module) => {
50549
50549
 
50550
50550
  // apps/cli/src/index.ts
50551
50551
  import { resolve, join as join17, dirname as dirname4 } from "path";
50552
- import { exists, mkdir as mkdir3 } from "fs/promises";
50552
+ import { exists, mkdir as mkdir3, rm } from "fs/promises";
50553
50553
 
50554
50554
  // node_modules/.bun/ink@5.2.1+1f88f629f0141b18/node_modules/ink/build/render.js
50555
50555
  import { Stream } from "stream";
@@ -56117,7 +56117,7 @@ function log(msg) {
56117
56117
  }
56118
56118
 
56119
56119
  // apps/cli/src/cli.ts
56120
- var VALID_MODES = new Set(["task", "list", "status", "init", "agent"]);
56120
+ var VALID_MODES = new Set(["task", "list", "status", "init", "agent", "clean"]);
56121
56121
  var VALID_MODELS = new Set(["haiku", "sonnet", "opus"]);
56122
56122
  var HELP_TEXT = [
56123
56123
  "Usage: ralph <command> [options]",
@@ -56128,6 +56128,7 @@ var HELP_TEXT = [
56128
56128
  " status Show detailed change status",
56129
56129
  " init Initialize OpenSpec in current directory",
56130
56130
  " agent Poll Linear for new tasks and run loops concurrently",
56131
+ " clean Remove worktree, branch, openspec change, and task state for --name",
56131
56132
  "",
56132
56133
  "Options:",
56133
56134
  " --name <name> Change name (required for most commands)",
@@ -60611,54 +60612,70 @@ function countTaskItems(content) {
60611
60612
  const unchecked = (content.match(/^- \[ \]/gm) ?? []).length;
60612
60613
  return { checked, unchecked };
60613
60614
  }
60614
- function buildRows(statesDir) {
60615
+ function buildRows(statesDir, projectRoot) {
60615
60616
  const storage = getStorage();
60616
- const entries = storage.list(statesDir);
60617
60617
  const rows = [];
60618
- for (const entry of entries) {
60619
- const raw = storage.read(join3(statesDir, entry, ".ralph-state.json"));
60620
- if (raw === null)
60621
- continue;
60622
- let state;
60623
- try {
60624
- state = JSON.parse(raw);
60625
- } catch {
60626
- continue;
60618
+ const seenNames = new Set;
60619
+ const sources = [{ dir: statesDir, label: "main" }];
60620
+ if (projectRoot) {
60621
+ const worktreesRoot = join3(projectRoot, ".ralph", "worktrees");
60622
+ for (const wt of storage.list(worktreesRoot)) {
60623
+ sources.push({
60624
+ dir: join3(worktreesRoot, wt, ".ralph", "tasks"),
60625
+ label: `wt:${wt}`
60626
+ });
60627
60627
  }
60628
- if (String(state.status ?? "") === "completed")
60629
- continue;
60630
- const promptRaw = String(state.prompt ?? "");
60631
- const firstLine = promptRaw.split(`
60628
+ }
60629
+ for (const { dir, label } of sources) {
60630
+ for (const entry of storage.list(dir)) {
60631
+ if (seenNames.has(entry))
60632
+ continue;
60633
+ const raw = storage.read(join3(dir, entry, ".ralph-state.json"));
60634
+ if (raw === null)
60635
+ continue;
60636
+ let state;
60637
+ try {
60638
+ state = JSON.parse(raw);
60639
+ } catch {
60640
+ continue;
60641
+ }
60642
+ if (String(state.status ?? "") === "completed")
60643
+ continue;
60644
+ const promptRaw = String(state.prompt ?? "");
60645
+ const firstLine = promptRaw.split(`
60632
60646
  `).find((l) => l.trim() !== "") ?? "";
60633
- let progress = "\u2014";
60634
- let progressStyled = true;
60635
- const tasksContent = storage.read(join3(statesDir, entry, "tasks.md"));
60636
- if (tasksContent !== null) {
60637
- const { checked, unchecked } = countTaskItems(tasksContent);
60638
- const total = checked + unchecked;
60639
- if (total > 0) {
60640
- progress = `${checked}/${total}`;
60641
- progressStyled = false;
60642
- }
60643
- }
60644
- rows.push({
60645
- name: String(state.name ?? entry),
60646
- phase: String(state.status ?? "active"),
60647
- status: String(state.status ?? "unknown"),
60648
- iters: String(state.iteration ?? 0),
60649
- progress,
60650
- progressStyled,
60651
- prompt: firstLine.replace(/^#+\s*/, "").trim().slice(0, 60)
60652
- });
60647
+ let progress = "\u2014";
60648
+ let progressStyled = true;
60649
+ const tasksContent = storage.read(join3(dir, entry, "tasks.md"));
60650
+ if (tasksContent !== null) {
60651
+ const { checked, unchecked } = countTaskItems(tasksContent);
60652
+ const total = checked + unchecked;
60653
+ if (total > 0) {
60654
+ progress = `${checked}/${total}`;
60655
+ progressStyled = false;
60656
+ }
60657
+ }
60658
+ seenNames.add(entry);
60659
+ rows.push({
60660
+ name: String(state.name ?? entry),
60661
+ phase: String(state.status ?? "active"),
60662
+ status: String(state.status ?? "unknown"),
60663
+ iters: String(state.iteration ?? 0),
60664
+ progress,
60665
+ progressStyled,
60666
+ prompt: firstLine.replace(/^#+\s*/, "").trim().slice(0, 60),
60667
+ source: label
60668
+ });
60669
+ }
60653
60670
  }
60654
60671
  return rows;
60655
60672
  }
60656
- function TaskList({ statesDir }) {
60673
+ function TaskList({ statesDir, projectRoot }) {
60657
60674
  const { exit } = use_app_default();
60658
60675
  import_react22.useEffect(() => {
60659
60676
  exit();
60660
60677
  }, [exit]);
60661
- const rows = buildRows(statesDir);
60678
+ const rows = buildRows(statesDir, projectRoot);
60662
60679
  if (rows.length === 0) {
60663
60680
  return /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Box_default, {
60664
60681
  flexDirection: "column",
@@ -60681,9 +60698,10 @@ function TaskList({ statesDir }) {
60681
60698
  phase: Math.max(5, ...rows.map((r) => r.phase.length)),
60682
60699
  status: Math.max(6, ...rows.map((r) => r.status.length)),
60683
60700
  iters: 5,
60684
- progress: 8
60701
+ progress: 8,
60702
+ source: Math.max(6, ...rows.map((r) => r.source.length))
60685
60703
  };
60686
- const ruleWidth = cols.name + cols.phase + cols.status + cols.iters + cols.progress + 60 + 10;
60704
+ const ruleWidth = cols.name + cols.phase + cols.status + cols.iters + cols.progress + cols.source + 60 + 12;
60687
60705
  return /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Box_default, {
60688
60706
  flexDirection: "column",
60689
60707
  children: [
@@ -60717,6 +60735,11 @@ function TaskList({ statesDir }) {
60717
60735
  children: "Progress".padEnd(cols.progress)
60718
60736
  }, undefined, false, undefined, this),
60719
60737
  " ",
60738
+ /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
60739
+ bold: true,
60740
+ children: "Source".padEnd(cols.source)
60741
+ }, undefined, false, undefined, this),
60742
+ " ",
60720
60743
  /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
60721
60744
  bold: true,
60722
60745
  children: "Description"
@@ -60745,6 +60768,11 @@ function TaskList({ statesDir }) {
60745
60768
  children: row.progress.padStart(cols.progress)
60746
60769
  }, undefined, false, undefined, this) : row.progress.padStart(cols.progress),
60747
60770
  " ",
60771
+ /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
60772
+ dimColor: true,
60773
+ children: row.source.padEnd(cols.source)
60774
+ }, undefined, false, undefined, this),
60775
+ " ",
60748
60776
  /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Text, {
60749
60777
  dimColor: true,
60750
60778
  children: row.prompt
@@ -69758,7 +69786,12 @@ async function addLabelToIssue(apiKey, issueId, labelId) {
69758
69786
  import { join as join10 } from "path";
69759
69787
  var AgentStateSchema = exports_external.object({
69760
69788
  processedIssueIds: exports_external.array(exports_external.string()).default([]),
69761
- lastPollAt: exports_external.string().nullable().default(null)
69789
+ startedIssueIds: exports_external.array(exports_external.string()).default([]),
69790
+ lastPollAt: exports_external.string().nullable().default(null),
69791
+ changeMeta: exports_external.record(exports_external.string(), exports_external.object({
69792
+ issueId: exports_external.string(),
69793
+ identifier: exports_external.string()
69794
+ })).default({})
69762
69795
  });
69763
69796
  function statePath(projectRoot) {
69764
69797
  return join10(projectRoot, ".ralph", "agent-state.json");
@@ -70049,9 +70082,14 @@ class AgentCoordinator {
70049
70082
  const updater = this.deps.updater;
70050
70083
  if (!updater)
70051
70084
  return;
70052
- if (this.opts.postComments !== false) {
70085
+ const alreadyStarted = this.state?.startedIssueIds.includes(issue.id) ?? false;
70086
+ if (this.opts.postComments !== false && !alreadyStarted) {
70053
70087
  try {
70054
70088
  await updater.postComment(issue, `\uD83E\uDD16 Ralph started working on this issue. Tracking change: \`${changeName}\``);
70089
+ if (this.state && !this.state.startedIssueIds.includes(issue.id)) {
70090
+ this.state.startedIssueIds.push(issue.id);
70091
+ await this.deps.saveState(this.state);
70092
+ }
70055
70093
  } catch (err) {
70056
70094
  this.deps.onLog(`! Linear comment failed for ${issue.identifier}: ${err.message}`, "red");
70057
70095
  }
@@ -70299,7 +70337,10 @@ var bunCmdRunner = {
70299
70337
  const stderr = await new Response(proc.stderr).text();
70300
70338
  const code = await proc.exited;
70301
70339
  if (code !== 0) {
70302
- const err = new Error(`command \`${cmd[0]}\` failed`);
70340
+ const firstStderrLine = stderr.trim().split(`
70341
+ `)[0] ?? "";
70342
+ const summary = firstStderrLine ? `: ${firstStderrLine}` : "";
70343
+ const err = new Error(`\`${cmd.join(" ")}\` exited ${code}${summary}`);
70303
70344
  err.stderr = stderr;
70304
70345
  err.code = code;
70305
70346
  throw err;
@@ -70312,11 +70353,28 @@ function nextId() {
70312
70353
  lineCounter += 1;
70313
70354
  return `${Date.now()}-${lineCounter}`;
70314
70355
  }
70356
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
70357
+ function fmtElapsed(ms) {
70358
+ const s = Math.floor(ms / 1000);
70359
+ if (s < 60)
70360
+ return `${s}s`;
70361
+ const m = Math.floor(s / 60);
70362
+ const rem = s % 60;
70363
+ if (m < 60)
70364
+ return `${m}m${rem.toString().padStart(2, "0")}s`;
70365
+ const h = Math.floor(m / 60);
70366
+ return `${h}h${(m % 60).toString().padStart(2, "0")}m`;
70367
+ }
70315
70368
  function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70316
70369
  const { exit } = use_app_default();
70317
70370
  const [logs, setLogs] = import_react57.useState([]);
70318
70371
  const [, setTick] = import_react57.useState(0);
70372
+ const [clock, setClock] = import_react57.useState(0);
70319
70373
  const coordRef = import_react57.useRef(null);
70374
+ const workerMetaRef = import_react57.useRef(new Map);
70375
+ const nextPollAtRef = import_react57.useRef(0);
70376
+ const pollIntervalRef = import_react57.useRef(0);
70377
+ const [pollStatus, setPollStatus] = import_react57.useState({ state: "idle", lastFound: null, lastAdded: null, lastAt: null, filterDesc: "" });
70320
70378
  function appendLog(text, color) {
70321
70379
  setLogs((prev) => [...prev, { id: nextId(), text, color }]);
70322
70380
  }
@@ -70329,6 +70387,7 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70329
70387
  appendLog(`agent mode \u2014 config: ${cfgPath}`, "gray");
70330
70388
  const concurrency = args.concurrency || cfg.concurrency;
70331
70389
  const pollInterval = args.pollInterval || cfg.pollIntervalSeconds;
70390
+ pollIntervalRef.current = pollInterval;
70332
70391
  appendLog(`concurrency=${concurrency} pollInterval=${pollInterval}s`, "gray");
70333
70392
  const apiKey = process.env["LINEAR_API_KEY"];
70334
70393
  if (!apiKey) {
@@ -70402,6 +70461,13 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70402
70461
  issueByChange.set(changeName, issue);
70403
70462
  if (workerBranch)
70404
70463
  branchByChange.set(changeName, workerBranch);
70464
+ try {
70465
+ const s = await readAgentState(projectRoot);
70466
+ s.changeMeta[changeName] = { issueId: issue.id, identifier: issue.identifier };
70467
+ await writeAgentState(projectRoot, s);
70468
+ } catch (err) {
70469
+ appendLog(`! failed to record agent meta for ${changeName}: ${err.message}`, "yellow");
70470
+ }
70405
70471
  if (cfg.setupScript) {
70406
70472
  await runScript("setup", cfg.setupScript, workerCwd);
70407
70473
  }
@@ -70447,19 +70513,28 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70447
70513
  stderr: "ignore",
70448
70514
  stdin: "ignore"
70449
70515
  });
70516
+ workerMetaRef.current.set(changeName, {
70517
+ startedAt: Date.now(),
70518
+ statesDir: statesDirByChange.get(changeName) ?? statesDir,
70519
+ iter: 0
70520
+ });
70450
70521
  const wantPr = args.createPr || cfg.createPrOnSuccess;
70522
+ const CI_FAILED_EXIT = 70;
70523
+ const PR_FAILED_EXIT = 71;
70451
70524
  const wrapped = proc.exited.then(async (code) => {
70452
70525
  if (cfg.teardownScript) {
70453
70526
  try {
70454
70527
  await runScript("teardown", cfg.teardownScript, cwd2);
70455
70528
  } catch {}
70456
70529
  }
70530
+ let effectiveCode = code;
70457
70531
  const ok = code === 0;
70458
70532
  if (ok && wantPr) {
70459
70533
  const branch = branchByChange.get(changeName);
70460
70534
  const prIssue = issueByChange.get(changeName);
70461
70535
  if (!branch || !prIssue) {
70462
70536
  appendLog(`! createPr requested but no worktree branch is tracked for ${changeName} (use --worktree)`, "yellow");
70537
+ effectiveCode = PR_FAILED_EXIT;
70463
70538
  } else {
70464
70539
  try {
70465
70540
  const pr = await createPullRequest({ cwd: cwd2, branch, issue: prIssue, base: cfg.prBaseBranch }, bunCmdRunner);
@@ -70513,17 +70588,21 @@ ${stamped}
70513
70588
  pollIntervalSeconds: cfg.ciPollIntervalSeconds
70514
70589
  });
70515
70590
  if (!result2.success) {
70516
- appendLog(`! CI fix loop gave up after ${result2.attempts} attempts (${result2.reason ?? "unknown"})`, "red");
70591
+ appendLog(`! CI fix loop gave up after ${result2.attempts} attempts (${result2.reason ?? "unknown"}) \u2014 withholding done-status until CI passes`, "red");
70592
+ effectiveCode = CI_FAILED_EXIT;
70517
70593
  }
70518
70594
  }
70519
70595
  }
70520
70596
  } catch (err) {
70521
- appendLog(`! PR create failed for ${changeName}: ${err.message}`, "red");
70597
+ const e = err;
70598
+ const detail = e.stderr?.trim() || e.message;
70599
+ appendLog(`! PR create failed for ${changeName}: ${detail}`, "red");
70600
+ effectiveCode = PR_FAILED_EXIT;
70522
70601
  }
70523
70602
  }
70524
70603
  }
70525
70604
  if (useWorktree && cwd2 !== projectRoot) {
70526
- if (ok && cfg.cleanupWorktreeOnSuccess) {
70605
+ if (effectiveCode === 0 && cfg.cleanupWorktreeOnSuccess) {
70527
70606
  try {
70528
70607
  await removeWorktree(projectRoot, cwd2, bunGitRunner);
70529
70608
  appendLog(` removed worktree ${cwd2}`, "gray");
@@ -70536,7 +70615,8 @@ ${stamped}
70536
70615
  statesDirByChange.delete(changeName);
70537
70616
  branchByChange.delete(changeName);
70538
70617
  issueByChange.delete(changeName);
70539
- return code;
70618
+ workerMetaRef.current.delete(changeName);
70619
+ return effectiveCode;
70540
70620
  });
70541
70621
  return { exited: wrapped, kill: () => proc.kill() };
70542
70622
  },
@@ -70588,15 +70668,25 @@ ${stamped}
70588
70668
  });
70589
70669
  coordRef.current = coord2;
70590
70670
  await coord2.init();
70671
+ const filterDesc = `team=${filter2.team ?? "*"}, assignee=${filter2.assignee ?? "*"}, statuses=${filter2.statuses?.length ? filter2.statuses.join(",") : "open"}${filter2.labels?.length ? `, labels=${filter2.labels.join(",")}` : ""}`;
70591
70672
  const tick = async () => {
70592
70673
  if (cancelled)
70593
70674
  return;
70594
- const filterDesc = `team=${filter2.team ?? "*"}, assignee=${filter2.assignee ?? "*"}, statuses=${filter2.statuses?.length ? filter2.statuses.join(",") : "open"}${filter2.labels?.length ? `, labels=${filter2.labels.join(",")}` : ""}`;
70595
- appendLog(`\u2026 polling Linear (${filterDesc})`);
70675
+ setPollStatus((p) => ({ ...p, state: "polling", filterDesc }));
70596
70676
  const { found, added } = await coord2.pollOnce();
70597
- appendLog(` found ${found} open, ${added} new (queue=${coord2.queuedCount})`);
70598
70677
  if (cancelled)
70599
70678
  return;
70679
+ if (added > 0) {
70680
+ appendLog(` ${added} new issue${added === 1 ? "" : "s"} queued (found ${found} open)`);
70681
+ }
70682
+ setPollStatus({
70683
+ state: "idle",
70684
+ lastFound: found,
70685
+ lastAdded: added,
70686
+ lastAt: Date.now(),
70687
+ filterDesc
70688
+ });
70689
+ nextPollAtRef.current = Date.now() + pollInterval * 1000;
70600
70690
  pollTimer = setTimeout(tick, pollInterval * 1000);
70601
70691
  };
70602
70692
  tick();
@@ -70621,7 +70711,34 @@ ${stamped}
70621
70711
  process.off("SIGTERM", onSig);
70622
70712
  };
70623
70713
  }, []);
70714
+ import_react57.useEffect(() => {
70715
+ let cancelled = false;
70716
+ const interval = setInterval(() => {
70717
+ if (cancelled)
70718
+ return;
70719
+ (async () => {
70720
+ for (const [changeName, meta] of workerMetaRef.current) {
70721
+ try {
70722
+ const file = Bun.file(join14(meta.statesDir, changeName, ".ralph-state.json"));
70723
+ if (await file.exists()) {
70724
+ const json = await file.json();
70725
+ meta.iter = json.iteration ?? meta.iter;
70726
+ }
70727
+ } catch {}
70728
+ }
70729
+ if (!cancelled)
70730
+ setClock((c) => c + 1);
70731
+ })();
70732
+ }, 1000);
70733
+ return () => {
70734
+ cancelled = true;
70735
+ clearInterval(interval);
70736
+ };
70737
+ }, []);
70624
70738
  const coord = coordRef.current;
70739
+ const spinnerFrame = SPINNER_FRAMES[clock % SPINNER_FRAMES.length];
70740
+ const now2 = Date.now();
70741
+ const secsToNextPoll = nextPollAtRef.current ? Math.max(0, Math.ceil((nextPollAtRef.current - now2) / 1000)) : null;
70625
70742
  return /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Box_default, {
70626
70743
  flexDirection: "column",
70627
70744
  children: [
@@ -70641,23 +70758,41 @@ ${stamped}
70641
70758
  /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
70642
70759
  dimColor: true,
70643
70760
  children: [
70761
+ spinnerFrame,
70762
+ " ",
70763
+ pollStatus.state === "polling" ? `polling Linear (${pollStatus.filterDesc})` : pollStatus.lastAt !== null ? `last poll: ${pollStatus.lastFound} open, ${pollStatus.lastAdded} new${secsToNextPoll !== null ? ` \xB7 next in ${secsToNextPoll}s` : ""}` : "starting\u2026"
70764
+ ]
70765
+ }, undefined, true, undefined, this),
70766
+ /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
70767
+ dimColor: true,
70768
+ children: [
70769
+ " ",
70644
70770
  "workers active: ",
70645
70771
  coord?.activeCount ?? 0,
70646
70772
  " \xB7 queued: ",
70647
70773
  coord?.queuedCount ?? 0
70648
70774
  ]
70649
70775
  }, undefined, true, undefined, this),
70650
- coord?.activeWorkers.map((w) => /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
70651
- color: "cyan",
70652
- children: [
70653
- " ",
70654
- "\u25CF ",
70655
- w.issueIdentifier,
70656
- " (",
70657
- w.changeName,
70658
- ")"
70659
- ]
70660
- }, w.changeName, true, undefined, this))
70776
+ coord?.activeWorkers.map((w) => {
70777
+ const meta = workerMetaRef.current.get(w.changeName);
70778
+ const elapsed = meta ? fmtElapsed(now2 - meta.startedAt) : "\u2013";
70779
+ const iter = meta?.iter ?? 0;
70780
+ return /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
70781
+ color: "cyan",
70782
+ children: [
70783
+ " ",
70784
+ spinnerFrame,
70785
+ " ",
70786
+ w.issueIdentifier,
70787
+ " (",
70788
+ w.changeName,
70789
+ ") \xB7 iter ",
70790
+ iter,
70791
+ " \xB7 ",
70792
+ elapsed
70793
+ ]
70794
+ }, w.changeName, true, undefined, this);
70795
+ })
70661
70796
  ]
70662
70797
  }, undefined, true, undefined, this)
70663
70798
  ]
@@ -70808,7 +70943,8 @@ function App2({ args, statesDir, tasksDir, projectRoot }) {
70808
70943
  switch (args.mode) {
70809
70944
  case "list":
70810
70945
  return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(TaskList, {
70811
- statesDir
70946
+ statesDir,
70947
+ projectRoot
70812
70948
  }, undefined, false, undefined, this);
70813
70949
  case "agent":
70814
70950
  return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(AgentMode, {
@@ -70844,6 +70980,10 @@ function App2({ args, statesDir, tasksDir, projectRoot }) {
70844
70980
  children: "Initialized openspec directory"
70845
70981
  }, undefined, false, undefined, this)
70846
70982
  }, undefined, false, undefined, this);
70983
+ case "clean":
70984
+ return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(ExitAfterRender, {
70985
+ children: /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {}, undefined, false, undefined, this)
70986
+ }, undefined, false, undefined, this);
70847
70987
  case "task": {
70848
70988
  if (!args.name) {
70849
70989
  return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(ErrorMessage, {
@@ -70927,6 +71067,76 @@ try {
70927
71067
  cwd: process.cwd()
70928
71068
  });
70929
71069
  }
71070
+ if (args.mode === "clean") {
71071
+ if (!args.name) {
71072
+ process.stderr.write(`Error: --name is required for clean mode
71073
+ `);
71074
+ process.exit(1);
71075
+ }
71076
+ const worktreeDir = join17(projectRoot, ".ralph", "worktrees", args.name);
71077
+ const changeDir = join17(tasksDir, args.name);
71078
+ const stateDir = join17(statesDir, args.name);
71079
+ const branch = `ralph/${args.name}`;
71080
+ const removed = [];
71081
+ if (await exists(worktreeDir)) {
71082
+ const proc = Bun.spawn({
71083
+ cmd: ["git", "worktree", "remove", "--force", worktreeDir],
71084
+ cwd: projectRoot,
71085
+ stdout: "pipe",
71086
+ stderr: "pipe"
71087
+ });
71088
+ const code = await proc.exited;
71089
+ if (code !== 0) {
71090
+ await rm(worktreeDir, { recursive: true, force: true });
71091
+ await Bun.spawn({
71092
+ cmd: ["git", "worktree", "prune"],
71093
+ cwd: projectRoot,
71094
+ stdout: "ignore",
71095
+ stderr: "ignore"
71096
+ }).exited;
71097
+ }
71098
+ removed.push(`worktree ${worktreeDir}`);
71099
+ }
71100
+ const branchProc = Bun.spawn({
71101
+ cmd: ["git", "branch", "-D", branch],
71102
+ cwd: projectRoot,
71103
+ stdout: "ignore",
71104
+ stderr: "ignore"
71105
+ });
71106
+ if (await branchProc.exited === 0)
71107
+ removed.push(`branch ${branch}`);
71108
+ if (await exists(changeDir)) {
71109
+ await rm(changeDir, { recursive: true, force: true });
71110
+ removed.push(`openspec change ${changeDir}`);
71111
+ }
71112
+ if (await exists(stateDir)) {
71113
+ await rm(stateDir, { recursive: true, force: true });
71114
+ removed.push(`task state ${stateDir}`);
71115
+ }
71116
+ try {
71117
+ const agentState = await readAgentState(projectRoot);
71118
+ const meta = agentState.changeMeta[args.name];
71119
+ if (meta) {
71120
+ agentState.processedIssueIds = agentState.processedIssueIds.filter((id) => id !== meta.issueId);
71121
+ agentState.startedIssueIds = agentState.startedIssueIds.filter((id) => id !== meta.issueId);
71122
+ delete agentState.changeMeta[args.name];
71123
+ await writeAgentState(projectRoot, agentState);
71124
+ removed.push(`agent-state entry for ${meta.identifier} (${meta.issueId})`);
71125
+ }
71126
+ } catch {}
71127
+ if (removed.length === 0) {
71128
+ process.stdout.write(`Nothing to clean for '${args.name}'
71129
+ `);
71130
+ } else {
71131
+ process.stdout.write(`Cleaned '${args.name}':
71132
+ `);
71133
+ for (const r of removed)
71134
+ process.stdout.write(` \u2713 removed ${r}
71135
+ `);
71136
+ }
71137
+ await shutdown();
71138
+ process.exit(0);
71139
+ }
70930
71140
  if (args.mode === "task" && args.name) {
70931
71141
  await mkdir3(join17(statesDir, args.name), { recursive: true });
70932
71142
  await mkdir3(join17(tasksDir, args.name), { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neriros/ralphy",
3
- "version": "2.7.8",
3
+ "version": "2.8.0",
4
4
  "description": "An iterative AI task execution framework. Orchestrates multi-phase autonomous work using Claude or Codex engines.",
5
5
  "keywords": [
6
6
  "agent",