@neriros/ralphy 2.7.9 → 2.8.1

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 +279 -61
  2. package/package.json +1 -1
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,13 @@ 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
+ failedIssueIds: exports_external.array(exports_external.string()).default([]),
69791
+ lastPollAt: exports_external.string().nullable().default(null),
69792
+ changeMeta: exports_external.record(exports_external.string(), exports_external.object({
69793
+ issueId: exports_external.string(),
69794
+ identifier: exports_external.string()
69795
+ })).default({})
69762
69796
  });
69763
69797
  function statePath(projectRoot) {
69764
69798
  return join10(projectRoot, ".ralph", "agent-state.json");
@@ -69944,12 +69978,15 @@ class AgentCoordinator {
69944
69978
  }
69945
69979
  const state = this.state;
69946
69980
  const seen = new Set(state.processedIssueIds);
69981
+ const failed = new Set(state.failedIssueIds);
69947
69982
  const queued = new Set(this.queue.map((i) => i.id));
69948
69983
  const active = new Set(this.workers.map((w) => w.issueId));
69949
69984
  let added = 0;
69950
69985
  for (const issue of issues) {
69951
69986
  if (seen.has(issue.id))
69952
69987
  continue;
69988
+ if (failed.has(issue.id))
69989
+ continue;
69953
69990
  if (queued.has(issue.id))
69954
69991
  continue;
69955
69992
  if (active.has(issue.id))
@@ -70040,6 +70077,10 @@ class AgentCoordinator {
70040
70077
  this.state.processedIssueIds.push(issue.id);
70041
70078
  this.deps.saveState(this.state);
70042
70079
  }
70080
+ if (!ok && this.state && !this.state.failedIssueIds.includes(issue.id)) {
70081
+ this.state.failedIssueIds.push(issue.id);
70082
+ this.deps.saveState(this.state);
70083
+ }
70043
70084
  this.notifyExited(issue, changeName, code);
70044
70085
  this.deps.onWorkersChanged();
70045
70086
  this.spawnNext();
@@ -70049,9 +70090,14 @@ class AgentCoordinator {
70049
70090
  const updater = this.deps.updater;
70050
70091
  if (!updater)
70051
70092
  return;
70052
- if (this.opts.postComments !== false) {
70093
+ const alreadyStarted = this.state?.startedIssueIds.includes(issue.id) ?? false;
70094
+ if (this.opts.postComments !== false && !alreadyStarted) {
70053
70095
  try {
70054
70096
  await updater.postComment(issue, `\uD83E\uDD16 Ralph started working on this issue. Tracking change: \`${changeName}\``);
70097
+ if (this.state && !this.state.startedIssueIds.includes(issue.id)) {
70098
+ this.state.startedIssueIds.push(issue.id);
70099
+ await this.deps.saveState(this.state);
70100
+ }
70055
70101
  } catch (err) {
70056
70102
  this.deps.onLog(`! Linear comment failed for ${issue.identifier}: ${err.message}`, "red");
70057
70103
  }
@@ -70066,7 +70112,9 @@ class AgentCoordinator {
70066
70112
  return;
70067
70113
  const ok = code === 0;
70068
70114
  if (this.opts.postComments !== false) {
70069
- const body = ok ? `\u2705 Ralph completed work on this issue. Change: \`${changeName}\`` : `\u2717 Ralph exited with code ${code} on this issue. Change: \`${changeName}\``;
70115
+ const body = ok ? `\u2705 Ralph completed work on this issue. Change: \`${changeName}\`` : `\u2717 Ralph exited with code ${code} on this issue. Change: \`${changeName}\`
70116
+
70117
+ ` + `This issue has been quarantined and will not be auto-resumed on the next poll. ` + `Inspect the worktree at \`.ralph/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.`;
70070
70118
  try {
70071
70119
  await updater.postComment(issue, body);
70072
70120
  } catch (err) {
@@ -70299,7 +70347,10 @@ var bunCmdRunner = {
70299
70347
  const stderr = await new Response(proc.stderr).text();
70300
70348
  const code = await proc.exited;
70301
70349
  if (code !== 0) {
70302
- const err = new Error(`command \`${cmd[0]}\` failed`);
70350
+ const firstStderrLine = stderr.trim().split(`
70351
+ `)[0] ?? "";
70352
+ const summary = firstStderrLine ? `: ${firstStderrLine}` : "";
70353
+ const err = new Error(`\`${cmd.join(" ")}\` exited ${code}${summary}`);
70303
70354
  err.stderr = stderr;
70304
70355
  err.code = code;
70305
70356
  throw err;
@@ -70312,11 +70363,28 @@ function nextId() {
70312
70363
  lineCounter += 1;
70313
70364
  return `${Date.now()}-${lineCounter}`;
70314
70365
  }
70366
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
70367
+ function fmtElapsed(ms) {
70368
+ const s = Math.floor(ms / 1000);
70369
+ if (s < 60)
70370
+ return `${s}s`;
70371
+ const m = Math.floor(s / 60);
70372
+ const rem = s % 60;
70373
+ if (m < 60)
70374
+ return `${m}m${rem.toString().padStart(2, "0")}s`;
70375
+ const h = Math.floor(m / 60);
70376
+ return `${h}h${(m % 60).toString().padStart(2, "0")}m`;
70377
+ }
70315
70378
  function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70316
70379
  const { exit } = use_app_default();
70317
70380
  const [logs, setLogs] = import_react57.useState([]);
70318
70381
  const [, setTick] = import_react57.useState(0);
70382
+ const [clock, setClock] = import_react57.useState(0);
70319
70383
  const coordRef = import_react57.useRef(null);
70384
+ const workerMetaRef = import_react57.useRef(new Map);
70385
+ const nextPollAtRef = import_react57.useRef(0);
70386
+ const pollIntervalRef = import_react57.useRef(0);
70387
+ const [pollStatus, setPollStatus] = import_react57.useState({ state: "idle", lastFound: null, lastAdded: null, lastAt: null, filterDesc: "" });
70320
70388
  function appendLog(text, color) {
70321
70389
  setLogs((prev) => [...prev, { id: nextId(), text, color }]);
70322
70390
  }
@@ -70329,6 +70397,7 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70329
70397
  appendLog(`agent mode \u2014 config: ${cfgPath}`, "gray");
70330
70398
  const concurrency = args.concurrency || cfg.concurrency;
70331
70399
  const pollInterval = args.pollInterval || cfg.pollIntervalSeconds;
70400
+ pollIntervalRef.current = pollInterval;
70332
70401
  appendLog(`concurrency=${concurrency} pollInterval=${pollInterval}s`, "gray");
70333
70402
  const apiKey = process.env["LINEAR_API_KEY"];
70334
70403
  if (!apiKey) {
@@ -70402,6 +70471,13 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70402
70471
  issueByChange.set(changeName, issue);
70403
70472
  if (workerBranch)
70404
70473
  branchByChange.set(changeName, workerBranch);
70474
+ try {
70475
+ const s = await readAgentState(projectRoot);
70476
+ s.changeMeta[changeName] = { issueId: issue.id, identifier: issue.identifier };
70477
+ await writeAgentState(projectRoot, s);
70478
+ } catch (err) {
70479
+ appendLog(`! failed to record agent meta for ${changeName}: ${err.message}`, "yellow");
70480
+ }
70405
70481
  if (cfg.setupScript) {
70406
70482
  await runScript("setup", cfg.setupScript, workerCwd);
70407
70483
  }
@@ -70447,8 +70523,14 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70447
70523
  stderr: "ignore",
70448
70524
  stdin: "ignore"
70449
70525
  });
70526
+ workerMetaRef.current.set(changeName, {
70527
+ startedAt: Date.now(),
70528
+ statesDir: statesDirByChange.get(changeName) ?? statesDir,
70529
+ iter: 0
70530
+ });
70450
70531
  const wantPr = args.createPr || cfg.createPrOnSuccess;
70451
70532
  const CI_FAILED_EXIT = 70;
70533
+ const PR_FAILED_EXIT = 71;
70452
70534
  const wrapped = proc.exited.then(async (code) => {
70453
70535
  if (cfg.teardownScript) {
70454
70536
  try {
@@ -70462,6 +70544,7 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70462
70544
  const prIssue = issueByChange.get(changeName);
70463
70545
  if (!branch || !prIssue) {
70464
70546
  appendLog(`! createPr requested but no worktree branch is tracked for ${changeName} (use --worktree)`, "yellow");
70547
+ effectiveCode = PR_FAILED_EXIT;
70465
70548
  } else {
70466
70549
  try {
70467
70550
  const pr = await createPullRequest({ cwd: cwd2, branch, issue: prIssue, base: cfg.prBaseBranch }, bunCmdRunner);
@@ -70521,7 +70604,10 @@ ${stamped}
70521
70604
  }
70522
70605
  }
70523
70606
  } catch (err) {
70524
- appendLog(`! PR create failed for ${changeName}: ${err.message}`, "red");
70607
+ const e = err;
70608
+ const detail = e.stderr?.trim() || e.message;
70609
+ appendLog(`! PR create failed for ${changeName}: ${detail}`, "red");
70610
+ effectiveCode = PR_FAILED_EXIT;
70525
70611
  }
70526
70612
  }
70527
70613
  }
@@ -70539,6 +70625,7 @@ ${stamped}
70539
70625
  statesDirByChange.delete(changeName);
70540
70626
  branchByChange.delete(changeName);
70541
70627
  issueByChange.delete(changeName);
70628
+ workerMetaRef.current.delete(changeName);
70542
70629
  return effectiveCode;
70543
70630
  });
70544
70631
  return { exited: wrapped, kill: () => proc.kill() };
@@ -70591,15 +70678,25 @@ ${stamped}
70591
70678
  });
70592
70679
  coordRef.current = coord2;
70593
70680
  await coord2.init();
70681
+ const filterDesc = `team=${filter2.team ?? "*"}, assignee=${filter2.assignee ?? "*"}, statuses=${filter2.statuses?.length ? filter2.statuses.join(",") : "open"}${filter2.labels?.length ? `, labels=${filter2.labels.join(",")}` : ""}`;
70594
70682
  const tick = async () => {
70595
70683
  if (cancelled)
70596
70684
  return;
70597
- const filterDesc = `team=${filter2.team ?? "*"}, assignee=${filter2.assignee ?? "*"}, statuses=${filter2.statuses?.length ? filter2.statuses.join(",") : "open"}${filter2.labels?.length ? `, labels=${filter2.labels.join(",")}` : ""}`;
70598
- appendLog(`\u2026 polling Linear (${filterDesc})`);
70685
+ setPollStatus((p) => ({ ...p, state: "polling", filterDesc }));
70599
70686
  const { found, added } = await coord2.pollOnce();
70600
- appendLog(` found ${found} open, ${added} new (queue=${coord2.queuedCount})`);
70601
70687
  if (cancelled)
70602
70688
  return;
70689
+ if (added > 0) {
70690
+ appendLog(` ${added} new issue${added === 1 ? "" : "s"} queued (found ${found} open)`);
70691
+ }
70692
+ setPollStatus({
70693
+ state: "idle",
70694
+ lastFound: found,
70695
+ lastAdded: added,
70696
+ lastAt: Date.now(),
70697
+ filterDesc
70698
+ });
70699
+ nextPollAtRef.current = Date.now() + pollInterval * 1000;
70603
70700
  pollTimer = setTimeout(tick, pollInterval * 1000);
70604
70701
  };
70605
70702
  tick();
@@ -70624,7 +70721,34 @@ ${stamped}
70624
70721
  process.off("SIGTERM", onSig);
70625
70722
  };
70626
70723
  }, []);
70724
+ import_react57.useEffect(() => {
70725
+ let cancelled = false;
70726
+ const interval = setInterval(() => {
70727
+ if (cancelled)
70728
+ return;
70729
+ (async () => {
70730
+ for (const [changeName, meta] of workerMetaRef.current) {
70731
+ try {
70732
+ const file = Bun.file(join14(meta.statesDir, changeName, ".ralph-state.json"));
70733
+ if (await file.exists()) {
70734
+ const json = await file.json();
70735
+ meta.iter = json.iteration ?? meta.iter;
70736
+ }
70737
+ } catch {}
70738
+ }
70739
+ if (!cancelled)
70740
+ setClock((c) => c + 1);
70741
+ })();
70742
+ }, 1000);
70743
+ return () => {
70744
+ cancelled = true;
70745
+ clearInterval(interval);
70746
+ };
70747
+ }, []);
70627
70748
  const coord = coordRef.current;
70749
+ const spinnerFrame = SPINNER_FRAMES[clock % SPINNER_FRAMES.length];
70750
+ const now2 = Date.now();
70751
+ const secsToNextPoll = nextPollAtRef.current ? Math.max(0, Math.ceil((nextPollAtRef.current - now2) / 1000)) : null;
70628
70752
  return /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Box_default, {
70629
70753
  flexDirection: "column",
70630
70754
  children: [
@@ -70644,23 +70768,41 @@ ${stamped}
70644
70768
  /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
70645
70769
  dimColor: true,
70646
70770
  children: [
70771
+ spinnerFrame,
70772
+ " ",
70773
+ 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"
70774
+ ]
70775
+ }, undefined, true, undefined, this),
70776
+ /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
70777
+ dimColor: true,
70778
+ children: [
70779
+ " ",
70647
70780
  "workers active: ",
70648
70781
  coord?.activeCount ?? 0,
70649
70782
  " \xB7 queued: ",
70650
70783
  coord?.queuedCount ?? 0
70651
70784
  ]
70652
70785
  }, undefined, true, undefined, this),
70653
- coord?.activeWorkers.map((w) => /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
70654
- color: "cyan",
70655
- children: [
70656
- " ",
70657
- "\u25CF ",
70658
- w.issueIdentifier,
70659
- " (",
70660
- w.changeName,
70661
- ")"
70662
- ]
70663
- }, w.changeName, true, undefined, this))
70786
+ coord?.activeWorkers.map((w) => {
70787
+ const meta = workerMetaRef.current.get(w.changeName);
70788
+ const elapsed = meta ? fmtElapsed(now2 - meta.startedAt) : "\u2013";
70789
+ const iter = meta?.iter ?? 0;
70790
+ return /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
70791
+ color: "cyan",
70792
+ children: [
70793
+ " ",
70794
+ spinnerFrame,
70795
+ " ",
70796
+ w.issueIdentifier,
70797
+ " (",
70798
+ w.changeName,
70799
+ ") \xB7 iter ",
70800
+ iter,
70801
+ " \xB7 ",
70802
+ elapsed
70803
+ ]
70804
+ }, w.changeName, true, undefined, this);
70805
+ })
70664
70806
  ]
70665
70807
  }, undefined, true, undefined, this)
70666
70808
  ]
@@ -70811,7 +70953,8 @@ function App2({ args, statesDir, tasksDir, projectRoot }) {
70811
70953
  switch (args.mode) {
70812
70954
  case "list":
70813
70955
  return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(TaskList, {
70814
- statesDir
70956
+ statesDir,
70957
+ projectRoot
70815
70958
  }, undefined, false, undefined, this);
70816
70959
  case "agent":
70817
70960
  return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(AgentMode, {
@@ -70847,6 +70990,10 @@ function App2({ args, statesDir, tasksDir, projectRoot }) {
70847
70990
  children: "Initialized openspec directory"
70848
70991
  }, undefined, false, undefined, this)
70849
70992
  }, undefined, false, undefined, this);
70993
+ case "clean":
70994
+ return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(ExitAfterRender, {
70995
+ children: /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {}, undefined, false, undefined, this)
70996
+ }, undefined, false, undefined, this);
70850
70997
  case "task": {
70851
70998
  if (!args.name) {
70852
70999
  return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(ErrorMessage, {
@@ -70930,6 +71077,77 @@ try {
70930
71077
  cwd: process.cwd()
70931
71078
  });
70932
71079
  }
71080
+ if (args.mode === "clean") {
71081
+ if (!args.name) {
71082
+ process.stderr.write(`Error: --name is required for clean mode
71083
+ `);
71084
+ process.exit(1);
71085
+ }
71086
+ const worktreeDir = join17(projectRoot, ".ralph", "worktrees", args.name);
71087
+ const changeDir = join17(tasksDir, args.name);
71088
+ const stateDir = join17(statesDir, args.name);
71089
+ const branch = `ralph/${args.name}`;
71090
+ const removed = [];
71091
+ if (await exists(worktreeDir)) {
71092
+ const proc = Bun.spawn({
71093
+ cmd: ["git", "worktree", "remove", "--force", worktreeDir],
71094
+ cwd: projectRoot,
71095
+ stdout: "pipe",
71096
+ stderr: "pipe"
71097
+ });
71098
+ const code = await proc.exited;
71099
+ if (code !== 0) {
71100
+ await rm(worktreeDir, { recursive: true, force: true });
71101
+ await Bun.spawn({
71102
+ cmd: ["git", "worktree", "prune"],
71103
+ cwd: projectRoot,
71104
+ stdout: "ignore",
71105
+ stderr: "ignore"
71106
+ }).exited;
71107
+ }
71108
+ removed.push(`worktree ${worktreeDir}`);
71109
+ }
71110
+ const branchProc = Bun.spawn({
71111
+ cmd: ["git", "branch", "-D", branch],
71112
+ cwd: projectRoot,
71113
+ stdout: "ignore",
71114
+ stderr: "ignore"
71115
+ });
71116
+ if (await branchProc.exited === 0)
71117
+ removed.push(`branch ${branch}`);
71118
+ if (await exists(changeDir)) {
71119
+ await rm(changeDir, { recursive: true, force: true });
71120
+ removed.push(`openspec change ${changeDir}`);
71121
+ }
71122
+ if (await exists(stateDir)) {
71123
+ await rm(stateDir, { recursive: true, force: true });
71124
+ removed.push(`task state ${stateDir}`);
71125
+ }
71126
+ try {
71127
+ const agentState = await readAgentState(projectRoot);
71128
+ const meta = agentState.changeMeta[args.name];
71129
+ if (meta) {
71130
+ agentState.processedIssueIds = agentState.processedIssueIds.filter((id) => id !== meta.issueId);
71131
+ agentState.startedIssueIds = agentState.startedIssueIds.filter((id) => id !== meta.issueId);
71132
+ agentState.failedIssueIds = agentState.failedIssueIds.filter((id) => id !== meta.issueId);
71133
+ delete agentState.changeMeta[args.name];
71134
+ await writeAgentState(projectRoot, agentState);
71135
+ removed.push(`agent-state entry for ${meta.identifier} (${meta.issueId})`);
71136
+ }
71137
+ } catch {}
71138
+ if (removed.length === 0) {
71139
+ process.stdout.write(`Nothing to clean for '${args.name}'
71140
+ `);
71141
+ } else {
71142
+ process.stdout.write(`Cleaned '${args.name}':
71143
+ `);
71144
+ for (const r of removed)
71145
+ process.stdout.write(` \u2713 removed ${r}
71146
+ `);
71147
+ }
71148
+ await shutdown();
71149
+ process.exit(0);
71150
+ }
70933
71151
  if (args.mode === "task" && args.name) {
70934
71152
  await mkdir3(join17(statesDir, args.name), { recursive: true });
70935
71153
  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.9",
3
+ "version": "2.8.1",
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",