@neriros/ralphy 2.9.3 → 2.10.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 +133 -56
  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.3",
56410
+ version: "2.10.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",
@@ -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) {
@@ -70806,22 +70887,28 @@ function nextId() {
70806
70887
  return `${Date.now()}-${lineCounter}`;
70807
70888
  }
70808
70889
  var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
70809
- async function appendSteering(proposalPath, steering) {
70810
- const file = Bun.file(proposalPath);
70811
- const prev = await file.exists() ? await file.text() : "";
70812
- const stamped = `
70813
-
70814
- ### Steering feedback (${new Date().toISOString()})
70890
+ async function injectFixSteering(changeDir, heading, steering) {
70891
+ const steeringFile = Bun.file(join14(changeDir, "steering.md"));
70892
+ const existing = await steeringFile.exists() ? await steeringFile.text() : "";
70893
+ const stamped = `## ${heading} (${new Date().toISOString()})
70815
70894
 
70816
70895
  ${steering}
70817
70896
  `;
70818
- const next = prev.includes("## Steering") ? prev.replace(/## Steering\n+([\s\S]*?)$/, (_m, rest2) => `## Steering
70819
- ${stamped}
70820
- ${rest2}`) : prev + `
70821
- ## Steering
70822
- ${stamped}
70897
+ const nextSteering = existing ? `${stamped}
70898
+ ${existing.trimStart()}` : `${stamped}
70899
+ `;
70900
+ await Bun.write(join14(changeDir, "steering.md"), nextSteering);
70901
+ const tasksFile = Bun.file(join14(changeDir, "tasks.md"));
70902
+ const tasks = await tasksFile.exists() ? await tasksFile.text() : "";
70903
+ const taskSection = `
70904
+ ## ${heading} (${new Date().toISOString()})
70905
+
70906
+ ` + `- [ ] ${heading}. The error output is recorded in steering.md \u2014 read it first, ` + `then fix the underlying problem (do not just retry the failing command).
70823
70907
  `;
70824
- await Bun.write(proposalPath, next);
70908
+ const nextTasks = tasks.endsWith(`
70909
+ `) ? tasks + taskSection : tasks + `
70910
+ ` + taskSection;
70911
+ await Bun.write(join14(changeDir, "tasks.md"), nextTasks);
70825
70912
  }
70826
70913
  function fmtElapsed(ms) {
70827
70914
  const s = Math.floor(ms / 1000);
@@ -70930,13 +71017,6 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
70930
71017
  issueByChange.set(changeName, issue);
70931
71018
  if (workerBranch)
70932
71019
  branchByChange.set(changeName, workerBranch);
70933
- try {
70934
- const s = await readAgentState(projectRoot);
70935
- s.changeMeta[changeName] = { issueId: issue.id, identifier: issue.identifier };
70936
- await writeAgentState(projectRoot, s);
70937
- } catch (err) {
70938
- appendLog(`! failed to record agent meta for ${changeName}: ${err.message}`, "yellow");
70939
- }
70940
71020
  if (cfg.setupScript) {
70941
71021
  await runScript("setup", cfg.setupScript, workerCwd);
70942
71022
  }
@@ -71005,13 +71085,13 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
71005
71085
  appendLog(`! createPr requested but no worktree branch is tracked for ${changeName} (use --worktree)`, "yellow");
71006
71086
  effectiveCode = PR_FAILED_EXIT;
71007
71087
  } else {
71008
- const proposalPath = join14(statesDirByChange.get(changeName) ?? statesDir, "..", "..", "openspec", "changes", changeName, "proposal.md");
71088
+ const changeDir = join14(statesDirByChange.get(changeName) ?? statesDir, "..", "..", "openspec", "changes", changeName);
71009
71089
  const maxHookFixAttempts = cfg.maxCiFixAttempts;
71010
- const runWorkerWithSteering = async (steering) => {
71090
+ const runWorkerWithFixSteering = async (heading, steering) => {
71011
71091
  try {
71012
- await appendSteering(proposalPath, steering);
71092
+ await injectFixSteering(changeDir, heading, steering);
71013
71093
  } catch (steerErr) {
71014
- appendLog(`! could not append steering: ${steerErr.message}`, "red");
71094
+ appendLog(`! could not inject steering: ${steerErr.message}`, "red");
71015
71095
  return 1;
71016
71096
  }
71017
71097
  const rp = Bun.spawn({
@@ -71058,7 +71138,7 @@ ${e.stderr ?? ""}`;
71058
71138
  commitFixAttempt += 1;
71059
71139
  appendLog(`! commit rejected for ${changeName} \u2014 feeding error back to worker (attempt ${commitFixAttempt}/${maxHookFixAttempts})`, "yellow");
71060
71140
  appendLog(` detail: ${detail}`, "yellow");
71061
- const retryCode = await runWorkerWithSteering(`Committing residual changes was rejected by the host repo's pre-commit hook. ` + `Fix the underlying problem reported below, then the commit will be retried.
71141
+ const retryCode = await runWorkerWithFixSteering("Fix host pre-commit hook rejection", `Committing residual changes was rejected by the host repo's pre-commit hook. ` + `Fix the underlying problem reported below, then the commit will be retried.
71062
71142
 
71063
71143
  ` + "```\n" + combined.trim() + "\n```");
71064
71144
  if (retryCode !== 0) {
@@ -71096,7 +71176,7 @@ ${e.stderr ?? ""}`;
71096
71176
  pushFixAttempt += 1;
71097
71177
  appendLog(`! push rejected for ${changeName} \u2014 feeding error back to worker (attempt ${pushFixAttempt}/${maxHookFixAttempts})`, "yellow");
71098
71178
  appendLog(` detail: ${detail}`, "yellow");
71099
- const retryCode = await runWorkerWithSteering(`Push to origin/${branch} was rejected by the host repo's pre-push hook. ` + `Fix the underlying problem reported below, then the push will be retried.
71179
+ const retryCode = await runWorkerWithFixSteering("Fix host pre-push hook rejection", `Push to origin/${branch} was rejected by the host repo's pre-push hook. ` + `Fix the underlying problem reported below, then the push will be retried.
71100
71180
 
71101
71181
  ` + "```\n" + combined.trim() + "\n```");
71102
71182
  if (retryCode !== 0) {
@@ -71119,11 +71199,11 @@ ${e.stderr ?? ""}`;
71119
71199
  getFailedLogs: (ids) => fetchFailedRunLogs(ids, bunCmdRunner, cwd2),
71120
71200
  runTaskWithSteering: async (steering) => {
71121
71201
  try {
71122
- await appendSteering(proposalPath, `CI feedback:
71202
+ await injectFixSteering(changeDir, "Fix failing CI checks", `CI feedback:
71123
71203
 
71124
71204
  ${steering}`);
71125
71205
  } catch (err) {
71126
- appendLog(`! could not append steering: ${err.message}`, "red");
71206
+ appendLog(`! could not inject steering: ${err.message}`, "red");
71127
71207
  }
71128
71208
  const p = Bun.spawn({
71129
71209
  cmd: buildTaskCmd(),
@@ -71684,14 +71764,11 @@ try {
71684
71764
  }
71685
71765
  try {
71686
71766
  const agentState = await readAgentState(projectRoot);
71687
- const meta = agentState.changeMeta[args.name];
71688
- if (meta) {
71689
- agentState.processedIssueIds = agentState.processedIssueIds.filter((id) => id !== meta.issueId);
71690
- agentState.startedIssueIds = agentState.startedIssueIds.filter((id) => id !== meta.issueId);
71691
- agentState.failedIssueIds = agentState.failedIssueIds.filter((id) => id !== meta.issueId);
71692
- delete agentState.changeMeta[args.name];
71767
+ const entry = Object.values(agentState.tasks).find((t) => t.changeName === args.name);
71768
+ if (entry) {
71769
+ delete agentState.tasks[entry.identifier];
71693
71770
  await writeAgentState(projectRoot, agentState);
71694
- removed.push(`agent-state entry for ${meta.identifier} (${meta.issueId})`);
71771
+ removed.push(`agent-state entry for ${entry.identifier} (${entry.issueId})`);
71695
71772
  }
71696
71773
  } catch {}
71697
71774
  if (removed.length === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neriros/ralphy",
3
- "version": "2.9.3",
3
+ "version": "2.10.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",