@neriros/ralphy 2.9.4 → 2.10.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 +113 -41
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -56407,7 +56407,7 @@ function log(msg) {
56407
56407
  // package.json
56408
56408
  var package_default = {
56409
56409
  name: "@neriros/ralphy",
56410
- version: "2.9.4",
56410
+ version: "2.10.1",
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",
@@ -56538,7 +56538,7 @@ var HELP_TEXT = [
56538
56538
  " --linear-label <name> Filter by label name (repeatable, any-of)",
56539
56539
  " --poll-interval <s> Seconds between Linear polls (default: 60)",
56540
56540
  " --concurrency <n> Max concurrent task loops (default: 1)",
56541
- " --worktree Run each task in its own git worktree (.ralph/worktrees/<name>)",
56541
+ " --worktree Run each task in its own git worktree (~/.ralph/<project>/worktrees/<name>)",
56542
56542
  " --in-progress-status <name> Linear status to set when work starts on an issue",
56543
56543
  " --done-status <name> Linear status to set when work completes successfully",
56544
56544
  " --done-label <name> Linear label to add when work completes successfully",
@@ -70178,25 +70178,82 @@ async function addLabelToIssue(apiKey, issueId, labelId) {
70178
70178
 
70179
70179
  // apps/cli/src/agent/state.ts
70180
70180
  import { join as join10 } from "path";
70181
+ var TaskStateSchema = exports_external.enum(["started", "processed", "failed"]);
70182
+ var TaskEntrySchema = exports_external.object({
70183
+ issueId: exports_external.string(),
70184
+ identifier: exports_external.string(),
70185
+ state: TaskStateSchema,
70186
+ changeName: exports_external.string().optional(),
70187
+ startedAt: exports_external.string().optional(),
70188
+ finishedAt: exports_external.string().optional(),
70189
+ exitCode: exports_external.number().optional(),
70190
+ commentPosted: exports_external.boolean().optional()
70191
+ });
70181
70192
  var AgentStateSchema = exports_external.object({
70193
+ tasks: exports_external.record(exports_external.string(), TaskEntrySchema).default({}),
70194
+ lastPollAt: exports_external.string().nullable().default(null)
70195
+ });
70196
+ var LegacyAgentStateSchema = exports_external.object({
70182
70197
  processedIssueIds: exports_external.array(exports_external.string()).default([]),
70183
70198
  startedIssueIds: exports_external.array(exports_external.string()).default([]),
70184
70199
  failedIssueIds: exports_external.array(exports_external.string()).default([]),
70185
70200
  lastPollAt: exports_external.string().nullable().default(null),
70186
- changeMeta: exports_external.record(exports_external.string(), exports_external.object({
70187
- issueId: exports_external.string(),
70188
- identifier: exports_external.string()
70189
- })).default({})
70190
- });
70201
+ changeMeta: exports_external.record(exports_external.string(), exports_external.object({ issueId: exports_external.string(), identifier: exports_external.string() })).default({})
70202
+ }).partial();
70203
+ function migrateLegacy(raw) {
70204
+ const parsed = LegacyAgentStateSchema.safeParse(raw);
70205
+ if (!parsed.success)
70206
+ return AgentStateSchema.parse({});
70207
+ const legacy = parsed.data;
70208
+ const tasks = {};
70209
+ const byIssueId = new Map;
70210
+ for (const [changeName, meta] of Object.entries(legacy.changeMeta ?? {})) {
70211
+ byIssueId.set(meta.issueId, { identifier: meta.identifier, changeName });
70212
+ }
70213
+ const fold = (ids, state) => {
70214
+ for (const issueId of ids ?? []) {
70215
+ const found = byIssueId.get(issueId);
70216
+ if (!found)
70217
+ continue;
70218
+ tasks[found.identifier] = {
70219
+ issueId,
70220
+ identifier: found.identifier,
70221
+ state,
70222
+ changeName: found.changeName
70223
+ };
70224
+ }
70225
+ };
70226
+ fold(legacy.startedIssueIds, "started");
70227
+ fold(legacy.processedIssueIds, "processed");
70228
+ fold(legacy.failedIssueIds, "failed");
70229
+ for (const issueId of legacy.startedIssueIds ?? []) {
70230
+ const found = byIssueId.get(issueId);
70231
+ if (!found)
70232
+ continue;
70233
+ const entry = tasks[found.identifier];
70234
+ if (entry)
70235
+ entry.commentPosted = true;
70236
+ }
70237
+ return { tasks, lastPollAt: legacy.lastPollAt ?? null };
70238
+ }
70191
70239
  function statePath(projectRoot) {
70192
70240
  return join10(projectRoot, ".ralph", "agent-state.json");
70193
70241
  }
70242
+ function looksLegacy(raw) {
70243
+ if (typeof raw !== "object" || raw === null)
70244
+ return false;
70245
+ const r = raw;
70246
+ return "processedIssueIds" in r || "startedIssueIds" in r || "failedIssueIds" in r || "changeMeta" in r;
70247
+ }
70194
70248
  async function readAgentState(projectRoot) {
70195
70249
  const file = Bun.file(statePath(projectRoot));
70196
70250
  if (!await file.exists()) {
70197
70251
  return AgentStateSchema.parse({});
70198
70252
  }
70199
- return AgentStateSchema.parse(await file.json());
70253
+ const raw = await file.json();
70254
+ if (looksLegacy(raw))
70255
+ return migrateLegacy(raw);
70256
+ return AgentStateSchema.parse(raw);
70200
70257
  }
70201
70258
  async function writeAgentState(projectRoot, state) {
70202
70259
  await Bun.write(statePath(projectRoot), JSON.stringify(state, null, 2) + `
@@ -70371,15 +70428,19 @@ class AgentCoordinator {
70371
70428
  return { found: 0, added: 0 };
70372
70429
  }
70373
70430
  const state = this.state;
70374
- const seen = new Set(state.processedIssueIds);
70375
- const failed = new Set(state.failedIssueIds);
70431
+ const tasksByIssueId = new Map;
70432
+ for (const entry of Object.values(state.tasks)) {
70433
+ tasksByIssueId.set(entry.issueId, entry);
70434
+ }
70435
+ const isProcessed = (id) => tasksByIssueId.get(id)?.state === "processed";
70436
+ const isFailed = (id) => tasksByIssueId.get(id)?.state === "failed";
70376
70437
  const queued = new Set(this.queue.map((i) => i.id));
70377
70438
  const active = new Set(this.workers.map((w) => w.issueId));
70378
70439
  let added = 0;
70379
70440
  for (const issue of issues) {
70380
- if (seen.has(issue.id))
70441
+ if (isProcessed(issue.id))
70381
70442
  continue;
70382
- if (failed.has(issue.id))
70443
+ if (isFailed(issue.id))
70383
70444
  continue;
70384
70445
  if (queued.has(issue.id))
70385
70446
  continue;
@@ -70387,7 +70448,7 @@ class AgentCoordinator {
70387
70448
  continue;
70388
70449
  if (this.pendingIds.has(issue.id))
70389
70450
  continue;
70390
- const blocker = issue.blockedByIds.find((bid) => !seen.has(bid));
70451
+ const blocker = issue.blockedByIds.find((bid) => !isProcessed(bid));
70391
70452
  if (blocker !== undefined) {
70392
70453
  this.deps.onLog(` \u23F8 ${issue.identifier} skipped \u2014 blocked by unresolved dependency`, "yellow");
70393
70454
  continue;
@@ -70445,6 +70506,18 @@ class AgentCoordinator {
70445
70506
  this.launchWorker(issue);
70446
70507
  }
70447
70508
  }
70509
+ upsertTask(issue, patch) {
70510
+ if (!this.state)
70511
+ return;
70512
+ const existing = this.state.tasks[issue.identifier];
70513
+ this.state.tasks[issue.identifier] = {
70514
+ issueId: issue.id,
70515
+ identifier: issue.identifier,
70516
+ state: existing?.state ?? "started",
70517
+ ...existing,
70518
+ ...patch
70519
+ };
70520
+ }
70448
70521
  async launchWorker(issue) {
70449
70522
  let changeName;
70450
70523
  try {
@@ -70459,6 +70532,14 @@ class AgentCoordinator {
70459
70532
  this.pendingIds.delete(issue.id);
70460
70533
  return;
70461
70534
  }
70535
+ if (this.state) {
70536
+ this.upsertTask(issue, {
70537
+ state: "started",
70538
+ changeName,
70539
+ startedAt: this.state.tasks[issue.identifier]?.startedAt ?? new Date().toISOString()
70540
+ });
70541
+ this.deps.saveState(this.state);
70542
+ }
70462
70543
  this.deps.onLog(`\u25B6 ${issue.identifier} \u2192 ${changeName} (worker started)`, "cyan");
70463
70544
  const handle = this.deps.spawnWorker(changeName, issue);
70464
70545
  const worker = {
@@ -70479,12 +70560,12 @@ class AgentCoordinator {
70479
70560
  this.workers.splice(idx, 1);
70480
70561
  const ok = code === 0;
70481
70562
  this.deps.onLog(`${ok ? "\u2713" : "\u2717"} ${issue.identifier} \u2192 ${changeName} exited (code ${code})`, ok ? "green" : "red");
70482
- if (ok && this.state && !this.state.processedIssueIds.includes(issue.id)) {
70483
- this.state.processedIssueIds.push(issue.id);
70484
- this.deps.saveState(this.state);
70485
- }
70486
- if (!ok && this.state && !this.state.failedIssueIds.includes(issue.id)) {
70487
- this.state.failedIssueIds.push(issue.id);
70563
+ if (this.state) {
70564
+ this.upsertTask(issue, {
70565
+ state: ok ? "processed" : "failed",
70566
+ finishedAt: new Date().toISOString(),
70567
+ exitCode: code
70568
+ });
70488
70569
  this.deps.saveState(this.state);
70489
70570
  }
70490
70571
  this.notifyExited(issue, changeName, code);
@@ -70496,12 +70577,12 @@ class AgentCoordinator {
70496
70577
  const updater = this.deps.updater;
70497
70578
  if (!updater)
70498
70579
  return;
70499
- const alreadyStarted = this.state?.startedIssueIds.includes(issue.id) ?? false;
70500
- if (this.opts.postComments !== false && !alreadyStarted) {
70580
+ const alreadyCommented = this.state?.tasks[issue.identifier]?.commentPosted === true;
70581
+ if (this.opts.postComments !== false && !alreadyCommented) {
70501
70582
  try {
70502
70583
  await updater.postComment(issue, `\uD83E\uDD16 Ralph started working on this issue. Tracking change: \`${changeName}\``);
70503
- if (this.state && !this.state.startedIssueIds.includes(issue.id)) {
70504
- this.state.startedIssueIds.push(issue.id);
70584
+ if (this.state) {
70585
+ this.upsertTask(issue, { commentPosted: true });
70505
70586
  await this.deps.saveState(this.state);
70506
70587
  }
70507
70588
  } catch (err) {
@@ -70520,7 +70601,7 @@ class AgentCoordinator {
70520
70601
  if (this.opts.postComments !== false) {
70521
70602
  const body = ok ? `\u2705 Ralph completed work on this issue. Change: \`${changeName}\`` : `\u2717 Ralph exited with code ${code} on this issue. Change: \`${changeName}\`
70522
70603
 
70523
- ` + `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.`;
70604
+ ` + `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.`;
70524
70605
  try {
70525
70606
  await updater.postComment(issue, body);
70526
70607
  } catch (err) {
@@ -70577,9 +70658,10 @@ class AgentCoordinator {
70577
70658
  }
70578
70659
 
70579
70660
  // apps/cli/src/agent/worktree.ts
70580
- import { join as join13 } from "path";
70661
+ import { basename, join as join13 } from "path";
70662
+ import { homedir as homedir2 } from "os";
70581
70663
  function worktreesDir(projectRoot) {
70582
- return join13(projectRoot, ".ralph", "worktrees");
70664
+ return join13(homedir2(), ".ralph", basename(projectRoot), "worktrees");
70583
70665
  }
70584
70666
  function branchForChange(changeName) {
70585
70667
  return `ralph/${changeName}`;
@@ -70936,13 +71018,6 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70936
71018
  issueByChange.set(changeName, issue);
70937
71019
  if (workerBranch)
70938
71020
  branchByChange.set(changeName, workerBranch);
70939
- try {
70940
- const s = await readAgentState(projectRoot);
70941
- s.changeMeta[changeName] = { issueId: issue.id, identifier: issue.identifier };
70942
- await writeAgentState(projectRoot, s);
70943
- } catch (err) {
70944
- appendLog(`! failed to record agent meta for ${changeName}: ${err.message}`, "yellow");
70945
- }
70946
71021
  if (cfg.setupScript) {
70947
71022
  await runScript("setup", cfg.setupScript, workerCwd);
70948
71023
  }
@@ -71648,7 +71723,7 @@ try {
71648
71723
  `);
71649
71724
  process.exit(1);
71650
71725
  }
71651
- const worktreeDir = join17(projectRoot, ".ralph", "worktrees", args.name);
71726
+ const worktreeDir = join17(worktreesDir(projectRoot), args.name);
71652
71727
  const changeDir = join17(tasksDir, args.name);
71653
71728
  const stateDir = join17(statesDir, args.name);
71654
71729
  const branch = `ralph/${args.name}`;
@@ -71690,14 +71765,11 @@ try {
71690
71765
  }
71691
71766
  try {
71692
71767
  const agentState = await readAgentState(projectRoot);
71693
- const meta = agentState.changeMeta[args.name];
71694
- if (meta) {
71695
- agentState.processedIssueIds = agentState.processedIssueIds.filter((id) => id !== meta.issueId);
71696
- agentState.startedIssueIds = agentState.startedIssueIds.filter((id) => id !== meta.issueId);
71697
- agentState.failedIssueIds = agentState.failedIssueIds.filter((id) => id !== meta.issueId);
71698
- delete agentState.changeMeta[args.name];
71768
+ const entry = Object.values(agentState.tasks).find((t) => t.changeName === args.name);
71769
+ if (entry) {
71770
+ delete agentState.tasks[entry.identifier];
71699
71771
  await writeAgentState(projectRoot, agentState);
71700
- removed.push(`agent-state entry for ${meta.identifier} (${meta.issueId})`);
71772
+ removed.push(`agent-state entry for ${entry.identifier} (${entry.issueId})`);
71701
71773
  }
71702
71774
  } catch {}
71703
71775
  if (removed.length === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neriros/ralphy",
3
- "version": "2.9.4",
3
+ "version": "2.10.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",