@neriros/ralphy 3.8.10 β†’ 3.8.13

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
@@ -2,13 +2,57 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@neriros/ralphy.svg)](https://www.npmjs.com/package/@neriros/ralphy)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/@neriros/ralphy.svg)](https://www.npmjs.com/package/@neriros/ralphy)
5
- [![license](https://img.shields.io/npm/l/@neriros/ralphy.svg)](https://github.com/NeriRos/ralphy/blob/main/LICENSE)
5
+ [![license](https://img.shields.io/npm/l/@neriros/ralphy.svg)](https://github.com/rosneri/ralphy/blob/main/LICENSE)
6
6
  [![Bun](https://img.shields.io/badge/runtime-Bun-fbf0df.svg)](https://bun.sh)
7
7
 
8
8
  An iterative AI task execution framework. Ralphy runs Claude or Codex in a checklist-driven loop with state on disk, cost safeguards, and a long-lived **agent** that polls Linear, opens PRs, and iterates with reviewers.
9
9
 
10
10
  > πŸ“˜ Full reference β€” Linear indicators, lifecycle, PR/CI flow, CLI flags, MCP β€” lives in **[GUIDE.md](./GUIDE.md)**.
11
11
 
12
+ ## Features
13
+
14
+ **Loop**
15
+
16
+ - **Checklist-driven** β€” one unchecked task per iteration; state persists on disk so any run can be resumed.
17
+ - **Engine choice** β€” Claude (haiku / sonnet / opus) or Codex, swappable per task.
18
+ - **Safeguards** β€” `--max-iterations`, `--max-cost`, `--max-runtime`, `--max-failures` cap any runaway run.
19
+ - **OpenSpec layout** β€” `proposal.md` (steering) + `design.md` + `tasks.md` + `specs/` per change.
20
+
21
+ **Agent mode (Linear-driven)**
22
+
23
+ - **Linear polling** β€” picks up Todo tickets, resumes In Progress, re-runs reviewer-flagged Done.
24
+ - **Indicators** β€” declarative `WORKFLOW.md` map for "which labels/statuses to watch and apply" at each lifecycle event.
25
+ - **Worktrees** β€” every task runs in its own `git worktree` so concurrent workers can't stomp on each other.
26
+ - **Confirmation gate** β€” optional human approval step between `tasks` and `implement`; revise via `@ralphy revise: <why>`.
27
+ - **Self-review phase** β€” once tasks are checked off, an in-process reviewer can append more work for another round.
28
+ - **Tmux session management** β€” `ralphy agent` re-execs into a managed tmux session so detaching the terminal doesn't kill the loop.
29
+ - **Pre-existing error check** β€” pauses pickups when the trunk is red so the agent doesn't chase failures it didn't cause.
30
+
31
+ **PR + CI**
32
+
33
+ - **Auto PR open** β€” push branch and `gh pr create` on clean exit; idempotent (surfaces existing PR if open).
34
+ - **Auto-merge opt-in** β€” `getAutoMerge` triggers `gh pr merge --auto --squash|merge|rebase` right after PR creation.
35
+ - **Stacked PRs** β€” `--stack-prs` opens against a blocker's head branch when a `blocked_by` Linear relation has exactly one open PR.
36
+ - **CI fix loop** β€” on red CI, pulls failed logs, appends to steering, re-spawns until green or `maxCiFixAttempts` hit.
37
+ - **Conflict re-fix** β€” `gh pr view`–driven; on `mergeable: CONFLICTING` enqueues a conflict-resolution task automatically.
38
+
39
+ **Reviewer interaction**
40
+
41
+ - **`@ralphy` mentions** β€” Linear comments _and_ GitHub PR comments trigger a fresh review run with the mention as the prompt.
42
+ - **Code-review iteration** β€” unresolved review-thread comments queue a digest; Ralph agrees-and-fixes (resolving the thread) or disagrees-and-replies.
43
+ - **Sticky task comment** β€” `tasks.md` mirrors into a single Linear comment that updates in place; a one-shot "πŸ“‹ Plan" comment summarises proposal + design when planning completes.
44
+
45
+ **Observability**
46
+
47
+ - **Ink dashboard** β€” engine/model, poll-bucket breakdown, per-worker cards with live phase, command-in-flight, and stdout tail.
48
+ - **Structured JSON event stream** β€” `--json-output` for CI; `--json-log-file` mirrors the same stream to disk.
49
+ - **Per-worker logs** β€” `~/.ralph/agent-mode.log` (global) + `.ralph/logs/<change>.log` (per-task) + per-change `LOG.jsonl`.
50
+
51
+ **Extensibility**
52
+
53
+ - **MCP server** β€” exposes `ralph_list_changes` / `get_change` / `create_change` / `append_steering` / `stop` to Claude-side agents (auto-wired on per-project install).
54
+ - **`WORKFLOW.md` template body** β€” Jinja-style prompt rendered per iteration, so project-specific rules / boundaries / labels flow into every task automatically.
55
+
12
56
  ## How it works
13
57
 
14
58
  ```mermaid
package/dist/mcp/index.js CHANGED
@@ -24066,6 +24066,7 @@ var StateSchema = exports_external.object({
24066
24066
  model: exports_external.string().default("opus"),
24067
24067
  manualTest: exports_external.boolean().default(false),
24068
24068
  createPr: exports_external.boolean().default(false),
24069
+ prDraft: exports_external.boolean().default(false),
24069
24070
  validateOnComplete: exports_external.boolean().default(false),
24070
24071
  usage: UsageSchema.default({}),
24071
24072
  history: exports_external.array(HistoryEntrySchema).default([]),
@@ -24183,6 +24184,7 @@ function buildInitialState(options) {
24183
24184
  model: options.model ?? "opus",
24184
24185
  manualTest: options.manualTest ?? false,
24185
24186
  createPr: options.createPr ?? false,
24187
+ prDraft: options.prDraft ?? false,
24186
24188
  createdAt: now,
24187
24189
  lastModified: now,
24188
24190
  metadata: { branch }
@@ -25120,8 +25122,6 @@ class OpenSpecChangeStore {
25120
25122
  } catch {}
25121
25123
  }
25122
25124
  const changesDir = join4("openspec", "changes");
25123
- if (!await Bun.file(changesDir).exists())
25124
- return [];
25125
25125
  try {
25126
25126
  const entries = await readdir(changesDir, { withFileTypes: true });
25127
25127
  return entries.filter((entry) => entry.isDirectory() && entry.name !== "archive").map((entry) => entry.name);
@@ -18928,8 +18928,8 @@ import { readFileSync } from "fs";
18928
18928
  import { resolve } from "path";
18929
18929
  function getVersion() {
18930
18930
  try {
18931
- if ("3.8.10")
18932
- return "3.8.10";
18931
+ if ("3.8.13")
18932
+ return "3.8.13";
18933
18933
  } catch {}
18934
18934
  const dirsToTry = [];
18935
18935
  try {
@@ -19302,6 +19302,7 @@ var init_posthog = __esm(() => {
19302
19302
  "agent_conflict_promoted",
19303
19303
  "agent_conflict_detected",
19304
19304
  "agent_ci_failed_detected",
19305
+ "agent_pr_tracker_bailed",
19305
19306
  "agent_prepare_failed",
19306
19307
  "agent_worker_spawned",
19307
19308
  "agent_worker_exited",
@@ -59875,8 +59876,6 @@ class OpenSpecChangeStore {
59875
59876
  } catch {}
59876
59877
  }
59877
59878
  const changesDir = join7("openspec", "changes");
59878
- if (!await Bun.file(changesDir).exists())
59879
- return [];
59880
59879
  try {
59881
59880
  const entries = await readdir(changesDir, { withFileTypes: true });
59882
59881
  return entries.filter((entry) => entry.isDirectory() && entry.name !== "archive").map((entry) => entry.name);
@@ -64364,6 +64363,7 @@ var init_types2 = __esm(() => {
64364
64363
  model: exports_external.string().default("opus"),
64365
64364
  manualTest: exports_external.boolean().default(false),
64366
64365
  createPr: exports_external.boolean().default(false),
64366
+ prDraft: exports_external.boolean().default(false),
64367
64367
  validateOnComplete: exports_external.boolean().default(false),
64368
64368
  usage: UsageSchema.default({}),
64369
64369
  history: exports_external.array(HistoryEntrySchema).default([]),
@@ -64503,6 +64503,7 @@ function buildInitialState(options) {
64503
64503
  model: options.model ?? "opus",
64504
64504
  manualTest: options.manualTest ?? false,
64505
64505
  createPr: options.createPr ?? false,
64506
+ prDraft: options.prDraft ?? false,
64506
64507
  createdAt: now2,
64507
64508
  lastModified: now2,
64508
64509
  metadata: { branch }
@@ -71298,7 +71299,8 @@ When all tasks are complete and all files are committed, push your branch and op
71298
71299
  `;
71299
71300
  prompt += ` git push -u origin HEAD
71300
71301
  `;
71301
- prompt += ` gh pr create --title "${state.name}" --body "Summary of changes for ${state.name}"
71302
+ const draftFlag = state.prDraft ? " --draft" : "";
71303
+ prompt += ` gh pr create${draftFlag} --title "${state.name}" --body "Summary of changes for ${state.name}"
71302
71304
  `;
71303
71305
  prompt += `Use the change name as the PR title and write a concise summary of the implementation in the body.
71304
71306
  `;
@@ -93558,6 +93560,7 @@ var init_schema2 = __esm(() => {
93558
93560
  teardownScript: exports_external2.string().optional(),
93559
93561
  appendPrompt: exports_external2.string().optional(),
93560
93562
  createPrOnSuccess: exports_external2.boolean().default(false),
93563
+ prDraft: exports_external2.boolean().default(false),
93561
93564
  prBaseBranch: exports_external2.string().default("main"),
93562
93565
  stackPrsOnDependencies: exports_external2.boolean().default(false),
93563
93566
  autoMergeStrategy: exports_external2.enum(["squash", "merge", "rebase"]).default("squash"),
@@ -93645,6 +93648,15 @@ var init_schema2 = __esm(() => {
93645
93648
  label: "ralph:pre-existing-error",
93646
93649
  outputCharLimit: 4000
93647
93650
  }),
93651
+ prTracker: exports_external2.object({
93652
+ enabled: exports_external2.boolean().default(true),
93653
+ maxRecoveryAttempts: exports_external2.number().int().positive().default(3),
93654
+ advanceMergedToDone: exports_external2.boolean().default(false)
93655
+ }).strict().default({
93656
+ enabled: true,
93657
+ maxRecoveryAttempts: 3,
93658
+ advanceMergedToDone: false
93659
+ }),
93648
93660
  openspec: exports_external2.object({
93649
93661
  reviewPhase: exports_external2.object({
93650
93662
  enabled: exports_external2.boolean().default(false),
@@ -94692,6 +94704,12 @@ async function parseAgentArgs(argv) {
94692
94704
  case "--no-tmux":
94693
94705
  result2.noTmux = true;
94694
94706
  break;
94707
+ case "--no-pr-tracker":
94708
+ result2.prTrackerEnabled = false;
94709
+ break;
94710
+ case "--pr-tracker":
94711
+ result2.prTrackerEnabled = true;
94712
+ break;
94695
94713
  default:
94696
94714
  if (VALID_MODES2.has(arg)) {
94697
94715
  result2.mode = arg;
@@ -94701,6 +94719,12 @@ async function parseAgentArgs(argv) {
94701
94719
  break;
94702
94720
  }
94703
94721
  }
94722
+ if (result2.fixCi && !result2.createPr) {
94723
+ throw new Error("--fix-ci requires --create-pr");
94724
+ }
94725
+ if (result2.stackPrs && !result2.createPr) {
94726
+ throw new Error("--stack-prs requires --create-pr");
94727
+ }
94704
94728
  return result2;
94705
94729
  }
94706
94730
  var VALID_MODES2, INDICATOR_KEYS, GET_KEYS, HELP_TEXT2;
@@ -94769,6 +94793,7 @@ var init_cli2 = __esm(() => {
94769
94793
  " --code-review Watch open tracked PRs for unresolved review comments",
94770
94794
  " --max-tickets <n> Stop picking up new issues after N have been started (0 = unlimited)",
94771
94795
  " --no-tmux Disable tmux session management; run agent in the foreground directly",
94796
+ " --no-pr-tracker Disable RLF-173 pr-tracker bail / recovery counter for this run",
94772
94797
  " --json-output Emit JSONL to stdout instead of the Ink dashboard (for scripting/CI)",
94773
94798
  " (auto-enabled when stdin is not a TTY, e.g. pipes / nohup / CI)",
94774
94799
  " --json-log-file <path> Mirror JSONL events to a file (works alongside TUI or --json-output)",
@@ -96399,9 +96424,18 @@ class AgentCoordinator {
96399
96424
  counts.conflicted += 1;
96400
96425
  else if (pr.status === "ci_failed")
96401
96426
  counts.ciFailed += 1;
96427
+ if (pr.status === "mergeable" && this.opts.prTracker) {
96428
+ try {
96429
+ await this.opts.prTracker.clear(issue2.identifier);
96430
+ } catch (err) {
96431
+ this.deps.onLog(`! pr-tracker clear failed for ${issue2.identifier}: ${err.message}`, "yellow");
96432
+ }
96433
+ }
96402
96434
  if (pr.status === "conflicted") {
96403
96435
  if (this.conflictNotified.has(issue2.id))
96404
96436
  continue;
96437
+ if (await this.prTrackerBail(issue2, pr.url, "conflicting"))
96438
+ continue;
96405
96439
  emitCapture(this.bus, "agent_conflict_detected", { issue_identifier: issue2.identifier });
96406
96440
  this.conflictNotified.add(issue2.id);
96407
96441
  this.deps.onLog(` ${issue2.identifier}: PR ${pr.url} conflicting \u2014 queued (conflict-fix)`, "yellow");
@@ -96422,6 +96456,8 @@ class AgentCoordinator {
96422
96456
  if (pr.status === "ci_failed") {
96423
96457
  if (this.ciFailedNotified.has(issue2.id))
96424
96458
  continue;
96459
+ if (await this.prTrackerBail(issue2, pr.url, "ci_failed"))
96460
+ continue;
96425
96461
  emitCapture(this.bus, "agent_ci_failed_detected", { issue_identifier: issue2.identifier });
96426
96462
  this.ciFailedNotified.add(issue2.id);
96427
96463
  this.deps.onLog(` ${issue2.identifier}: PR ${pr.url} CI failing \u2014 queued (ci-fix)`, "yellow");
@@ -96441,6 +96477,44 @@ class AgentCoordinator {
96441
96477
  }
96442
96478
  return counts;
96443
96479
  }
96480
+ async prTrackerBail(issue2, prUrl, reason) {
96481
+ const tracker = this.opts.prTracker;
96482
+ if (!tracker)
96483
+ return false;
96484
+ let decision;
96485
+ try {
96486
+ decision = await tracker.recordFailure(issue2.identifier, reason);
96487
+ } catch (err) {
96488
+ this.deps.onLog(`! pr-tracker record failed for ${issue2.identifier}: ${err.message}`, "yellow");
96489
+ return false;
96490
+ }
96491
+ if (decision.kind === "demote")
96492
+ return false;
96493
+ if (decision.firstBail) {
96494
+ this.deps.onLog(` ${issue2.identifier}: pr-tracker bailing after ${decision.attempts} recovery attempts (${reason}) \u2014 applying setError`, "red");
96495
+ emitCapture(this.bus, "agent_pr_tracker_bailed", {
96496
+ issue_identifier: issue2.identifier,
96497
+ reason,
96498
+ attempts: decision.attempts
96499
+ });
96500
+ if (this.opts.setError) {
96501
+ try {
96502
+ await this.deps.applyIndicator(issue2, this.opts.setError);
96503
+ } catch (err) {
96504
+ this.deps.onLog(`! Linear setError failed for ${issue2.identifier}: ${err.message}`, "yellow");
96505
+ }
96506
+ }
96507
+ if (this.opts.postComments !== false) {
96508
+ const human = reason === "conflicting" ? "merge conflicts" : "failing CI";
96509
+ try {
96510
+ await this.deps.postComment(issue2, `\u274C Ralph gave up auto-recovering this PR (${prUrl}) after ${decision.attempts} attempts \u2014 last failure: ${human}. The \`ralph:error\` label has been applied; clear it (or merge the PR) once a human has looked at it.`);
96511
+ } catch (err) {
96512
+ this.deps.onLog(`! Linear bail comment failed for ${issue2.identifier}: ${err.message}`, "yellow");
96513
+ }
96514
+ }
96515
+ }
96516
+ return true;
96517
+ }
96444
96518
  spawnNext() {
96445
96519
  if (this.stopped)
96446
96520
  return;
@@ -98204,17 +98278,20 @@ function createPrepareHelpers(input) {
98204
98278
  `The PR for this change has merge conflicts with \`${cfg.prBaseBranch}\`.`,
98205
98279
  "",
98206
98280
  "Steps:",
98207
- `1. \`git fetch origin ${cfg.prBaseBranch}\` then rebase or merge \`${cfg.prBaseBranch}\` into the current branch.`,
98281
+ `1. \`git fetch origin ${cfg.prBaseBranch}\` then merge \`${cfg.prBaseBranch}\` into the current branch (\`git merge origin/${cfg.prBaseBranch}\`). Do NOT rebase.`,
98208
98282
  "2. Resolve conflicts in the files git lists.",
98209
- "3. Stage and commit the resolution.",
98210
- `4. Push the resolved branch with \`git push --force-with-lease origin ${branchRef}\`.`,
98283
+ "3. Stage and commit the resolution as a new merge commit. Do NOT amend existing commits.",
98284
+ `4. Push the resolved branch with \`git push origin ${branchRef}\`. Never force-push.`,
98211
98285
  ` The post-task harness will NOT push for you in conflict-fix mode \u2014 you own the push.`,
98212
98286
  ` If the push is rejected, inspect the rejection output and react inline before retrying:`,
98213
- ` - **stale lease / non-fast-forward** (\`stale info\`, someone else pushed to \`${branchRef}\`):`,
98214
- ` \`git fetch origin ${branchRef}\` then rebase/merge their changes in, re-resolve any new`,
98215
- ` conflicts, and retry the push.`,
98287
+ ` - **non-fast-forward** (someone else pushed to \`${branchRef}\`):`,
98288
+ ` \`git fetch origin ${branchRef}\` then \`git merge origin/${branchRef}\` to bring their`,
98289
+ ` changes in as a new merge commit, re-resolve any new conflicts, and retry the push.`,
98290
+ ` Do NOT rebase and do NOT \`--force\` / \`--force-with-lease\` \u2014 work on the remote must`,
98291
+ ` never be overwritten.`,
98216
98292
  ` - **pre-push hook failure** (lint, typecheck, tests): fix the underlying problem locally,`,
98217
- ` \`git add\` + \`git commit --amend\` (or a new fixup commit), then retry the push.`,
98293
+ ` \`git add\` + \`git commit\` as a new commit (NEVER \`--amend\` an existing commit),`,
98294
+ ` then retry the push.`,
98218
98295
  ` - **ref-update policy rejection** (branch protection, required reviews): log the rejection`,
98219
98296
  ` message and stop \u2014 this requires human intervention; do not force past it.`,
98220
98297
  ` Only stop after exhausting the in-context fix. The push must succeed before this iteration ends.`,
@@ -98247,7 +98324,8 @@ PR: ${prUrl}` : ""
98247
98324
  `2. Fix the underlying failures in the worktree (tests, lint, typecheck, build).`,
98248
98325
  `3. Stage and commit the fixes.`,
98249
98326
  `4. Push with \`git push origin ${ciBranchRef}\`. If the push is rejected as`,
98250
- ` non-fast-forward, \`git fetch origin ${ciBranchRef}\` then rebase before retrying.`,
98327
+ ` non-fast-forward, \`git fetch origin ${ciBranchRef}\` then \`git merge origin/${ciBranchRef}\``,
98328
+ ` before retrying. Do NOT rebase, do NOT amend, and never force-push.`,
98251
98329
  `5. Wait for CI to re-run; if checks are still red, repeat from step 1.`,
98252
98330
  ` Stop only when CI is green or when the failure is clearly outside the change's scope`,
98253
98331
  ` (flaky infra, external service down) \u2014 in that case, log the rejection and exit.`,
@@ -99171,7 +99249,10 @@ async function createPullRequest(input, runner) {
99171
99249
  return { url: existingUrl, created: false };
99172
99250
  const title = defaultTitle(input.issue);
99173
99251
  const body = defaultBody(input.issue, input.branch);
99174
- const created = await runner.run(["gh", "pr", "create", "--base", base2, "--title", title, "--body", body], input.cwd);
99252
+ const createArgs = ["gh", "pr", "create", "--base", base2, "--title", title, "--body", body];
99253
+ if (input.draft)
99254
+ createArgs.push("--draft");
99255
+ const created = await runner.run(createArgs, input.cwd);
99175
99256
  const url2 = created.stdout.trim().split(`
99176
99257
  `).pop() ?? "";
99177
99258
  return { url: url2, created: true };
@@ -99326,9 +99407,37 @@ async function runWorkerWithFixTask(ctx, heading, body) {
99326
99407
  return 1;
99327
99408
  }
99328
99409
  await reactivateState(ctx.stateFilePath, ctx.log, ctx.changeName);
99329
- return ctx.respawnWorker();
99410
+ let preHead = "";
99411
+ try {
99412
+ const r = await ctx.cmd.run(["git", "rev-parse", "HEAD"], ctx.cwd);
99413
+ preHead = r.stdout.trim();
99414
+ } catch (err) {
99415
+ ctx.log(`! could not snapshot HEAD before fix task: ${err.message}`, "yellow");
99416
+ }
99417
+ const code = await ctx.respawnWorker();
99418
+ if (preHead) {
99419
+ try {
99420
+ const r = await ctx.cmd.run(["git", "rev-parse", "HEAD"], ctx.cwd);
99421
+ const postHead = r.stdout.trim();
99422
+ if (postHead !== preHead) {
99423
+ let isAncestor = true;
99424
+ try {
99425
+ await ctx.cmd.run(["git", "merge-base", "--is-ancestor", preHead, postHead], ctx.cwd);
99426
+ } catch {
99427
+ isAncestor = false;
99428
+ }
99429
+ if (!isAncestor) {
99430
+ ctx.log(`! fix worker for "${heading}" rewrote history \u2014 pre=${preHead.slice(0, 8)} ` + `is not an ancestor of post=${postHead.slice(0, 8)}. Aborting and preserving ` + `worktree at ${ctx.cwd}.`, "red");
99431
+ return 1;
99432
+ }
99433
+ }
99434
+ } catch (err) {
99435
+ ctx.log(`! could not verify append-only history after fix task: ${err.message}`, "yellow");
99436
+ }
99437
+ }
99438
+ return code;
99330
99439
  }
99331
- async function pushWithLeases(ctx) {
99440
+ async function pushBranchSafely(ctx) {
99332
99441
  try {
99333
99442
  ctx.emit("pushing", "after conflict resolution");
99334
99443
  await ctx.cmd.run(["git", "push", "origin", ctx.branch], ctx.cwd);
@@ -99342,10 +99451,12 @@ ${pe.stderr ?? ""}`;
99342
99451
  return false;
99343
99452
  }
99344
99453
  try {
99345
- await ctx.cmd.run(["git", "push", "--force-with-lease", "origin", ctx.branch], ctx.cwd);
99454
+ await ctx.cmd.run(["git", "fetch", "origin", ctx.branch], ctx.cwd);
99455
+ await ctx.cmd.run(["git", "merge", "--no-edit", `origin/${ctx.branch}`], ctx.cwd);
99456
+ await ctx.cmd.run(["git", "push", "origin", ctx.branch], ctx.cwd);
99346
99457
  return true;
99347
- } catch (forceErr) {
99348
- ctx.log(`! force-push after conflict fix failed: ${forceErr.message}`, "red");
99458
+ } catch (retryErr) {
99459
+ ctx.log(`! push after merging origin/${ctx.branch} failed: ${retryErr.message}`, "red");
99349
99460
  return false;
99350
99461
  }
99351
99462
  }
@@ -99364,7 +99475,8 @@ async function createPrWithRetry(ctx, issue2) {
99364
99475
  branch: ctx.branch,
99365
99476
  issue: issue2,
99366
99477
  base: base2,
99367
- metaOnlyFiles: ctx.cfg.metaOnlyFiles ?? []
99478
+ metaOnlyFiles: ctx.cfg.metaOnlyFiles ?? [],
99479
+ draft: ctx.cfg.prDraft ?? false
99368
99480
  }, ctx.cmd);
99369
99481
  return { pr: pr2, gaveUp: false };
99370
99482
  } catch (err) {
@@ -99377,26 +99489,26 @@ ${e.stderr ?? ""}`;
99377
99489
  const pushRejected = isHookReject || /failed to push some refs/i.test(combined);
99378
99490
  if (isNonFastForward && !nonFfRebaseAttempted) {
99379
99491
  nonFfRebaseAttempted = true;
99380
- ctx.emit("rebasing", `git pull --rebase origin ${ctx.branch}`);
99381
- ctx.log(` non-fast-forward push for ${ctx.changeName} \u2014 rebasing onto origin/${ctx.branch}`, "yellow");
99492
+ ctx.emit("merging", `git pull --no-rebase origin ${ctx.branch}`);
99493
+ ctx.log(` non-fast-forward push for ${ctx.changeName} \u2014 merging origin/${ctx.branch} into the branch`, "yellow");
99382
99494
  try {
99383
99495
  await ctx.cmd.run(["git", "fetch", "origin", ctx.branch], ctx.cwd);
99384
- await ctx.cmd.run(["git", "pull", "--rebase", "origin", ctx.branch], ctx.cwd);
99496
+ await ctx.cmd.run(["git", "pull", "--no-rebase", "--autostash", "--no-edit", "origin", ctx.branch], ctx.cwd);
99385
99497
  continue;
99386
- } catch (rebaseErr) {
99387
- const re = rebaseErr;
99498
+ } catch (mergeErr) {
99499
+ const re = mergeErr;
99388
99500
  const reBlob = `${re.stdout ?? ""}
99389
99501
  ${re.stderr ?? ""}`;
99390
- const isConflict = /CONFLICT|Merge conflict|could not apply|both modified/i.test(reBlob);
99502
+ const isConflict = /CONFLICT|Merge conflict|both modified/i.test(reBlob);
99391
99503
  if (!isConflict) {
99392
- ctx.log(`! rebase failed for ${ctx.changeName}: ${rebaseErr.message} \u2014 giving up`, "red");
99504
+ ctx.log(`! merge failed for ${ctx.changeName}: ${mergeErr.message} \u2014 giving up`, "red");
99393
99505
  return { pr: null, gaveUp: true };
99394
99506
  }
99395
- ctx.emit("rebasing", "conflicts detected \u2014 aborting + queueing fix task");
99507
+ ctx.emit("merging", "conflicts detected \u2014 aborting + queueing fix task");
99396
99508
  try {
99397
- await ctx.cmd.run(["git", "rebase", "--abort"], ctx.cwd);
99509
+ await ctx.cmd.run(["git", "merge", "--abort"], ctx.cwd);
99398
99510
  } catch (err2) {
99399
- ctx.log(`! git rebase --abort failed (worktree may already be clean): ${err2.message}`, "yellow");
99511
+ ctx.log(`! git merge --abort failed (worktree may already be clean): ${err2.message}`, "yellow");
99400
99512
  }
99401
99513
  let conflictedFiles = "";
99402
99514
  try {
@@ -99406,23 +99518,23 @@ ${re.stderr ?? ""}`;
99406
99518
  ctx.log(`! could not list conflicted files: ${err2.message}`, "yellow");
99407
99519
  }
99408
99520
  if (hookFixAttempt >= maxAttempts) {
99409
- ctx.log(`! merge conflict on rebase of ${ctx.branch} after ${hookFixAttempt} attempts \u2014 worktree preserved at ${ctx.cwd}`, "red");
99521
+ ctx.log(`! merge conflict merging origin/${ctx.branch} after ${hookFixAttempt} attempts \u2014 worktree preserved at ${ctx.cwd}`, "red");
99410
99522
  ctx.log(` detail: ${reBlob.trim().split(`
99411
99523
  `).slice(0, 8).join(`
99412
99524
  `)}`, "red");
99413
99525
  return { pr: null, gaveUp: true };
99414
99526
  }
99415
99527
  hookFixAttempt += 1;
99416
- ctx.emit("rebasing", `conflict-fix ${hookFixAttempt}/${maxAttempts}`);
99417
- ctx.log(`! merge conflict rebasing ${ctx.branch} \u2014 prepending fix task and re-running loop (attempt ${hookFixAttempt}/${maxAttempts})`, "yellow");
99418
- const retryCode2 = await runWorkerWithFixTask(ctx, "Resolve merge conflict with origin/" + ctx.branch, `Push to origin/${ctx.branch} was rejected as non-fast-forward, and rebasing ` + `onto origin/${ctx.branch} produced merge conflicts.
99528
+ ctx.emit("merging", `conflict-fix ${hookFixAttempt}/${maxAttempts}`);
99529
+ ctx.log(`! merge conflict merging origin/${ctx.branch} \u2014 prepending fix task and re-running loop (attempt ${hookFixAttempt}/${maxAttempts})`, "yellow");
99530
+ const retryCode2 = await runWorkerWithFixTask(ctx, "Resolve merge conflict with origin/" + ctx.branch, `Push to origin/${ctx.branch} was rejected as non-fast-forward, and merging ` + `origin/${ctx.branch} into the branch produced merge conflicts.
99419
99531
 
99420
- ` + `Run \`git fetch origin ${ctx.branch}\` and \`git rebase origin/${ctx.branch}\`, ` + `resolve every conflict, \`git add\` the resolved files, and finish with ` + `\`git rebase --continue\`. The push will be retried after this loop ` + `iteration finishes.
99532
+ ` + `Run \`git fetch origin ${ctx.branch}\` and \`git merge origin/${ctx.branch}\`, ` + `resolve every conflict, \`git add\` the resolved files, and finish with ` + `\`git commit\` (or \`git merge --continue\`). Do NOT rebase and do NOT ` + `amend existing commits \u2014 only add new commits. The push will be retried ` + `after this loop iteration finishes.
99421
99533
 
99422
99534
  ` + (conflictedFiles ? `Files that differ between your branch and origin/${ctx.branch}:
99423
99535
  ${conflictedFiles}
99424
99536
 
99425
- ` : "") + `Rebase output:
99537
+ ` : "") + `Merge output:
99426
99538
  ${reBlob.trim()}`);
99427
99539
  if (retryCode2 !== 0) {
99428
99540
  ctx.log(`! worker re-run after merge conflict exited code ${retryCode2} \u2014 giving up`, "red");
@@ -99447,6 +99559,8 @@ ${reBlob.trim()}`);
99447
99559
  ctx.log(` detail: ${detail}`, "yellow");
99448
99560
  const retryCode = await runWorkerWithFixTask(ctx, "Fix push rejection", `Push to origin/${ctx.branch} was rejected. Fix the underlying problem ` + `(e.g. failing pre-push hook checks), then the push will be retried.
99449
99561
 
99562
+ ` + `Do NOT delete, revert, amend, rebase, reorder, or squash existing commits. ` + `Only add new commits or edit working-tree files. If a pre-push check (test, ` + `lint, typecheck, etc.) fails on the change you just made, fix the test or ` + `the code under test \u2014 do not remove the change to silence the failure.
99563
+
99450
99564
  ` + combined.trim());
99451
99565
  if (retryCode !== 0) {
99452
99566
  ctx.log(`! worker re-run after push rejection exited code ${retryCode} \u2014 giving up`, "red");
@@ -99480,16 +99594,16 @@ async function fixConflictsAndCiLoop(ctx, prUrl, wantFixCi, checkPrConflict) {
99480
99594
  `The PR ${prUrl} has merge conflicts with \`${ctx.base}\`.`,
99481
99595
  "",
99482
99596
  "Steps:",
99483
- `1. \`git fetch origin ${ctx.base}\` then rebase or merge \`${ctx.base}\` into the current branch.`,
99597
+ `1. \`git fetch origin ${ctx.base}\` then merge \`${ctx.base}\` into the current branch (\`git merge origin/${ctx.base}\`). Do NOT rebase and do NOT amend existing commits.`,
99484
99598
  "2. Resolve conflicts in the files git lists.",
99485
- "3. Stage and commit the resolution."
99599
+ "3. Stage and commit the resolution as a new merge commit."
99486
99600
  ].join(`
99487
99601
  `));
99488
99602
  if (conflictCode !== 0) {
99489
99603
  ctx.log(`! conflict resolution worker exited code ${conflictCode} \u2014 giving up`, "red");
99490
99604
  return PR_FAILED_EXIT;
99491
99605
  }
99492
- const pushed = await pushWithLeases(ctx);
99606
+ const pushed = await pushBranchSafely(ctx);
99493
99607
  if (!pushed)
99494
99608
  return PR_FAILED_EXIT;
99495
99609
  continue;
@@ -99693,7 +99807,8 @@ ${indented}${suffix}`, "yellow");
99693
99807
  log2(` ${pr2.created ? "opened" : "found existing"} PR: ${prUrl}`, "green");
99694
99808
  registerPr?.(changeName, prUrl);
99695
99809
  let manualMergePending = false;
99696
- if (wantAutoMerge) {
99810
+ const prReadyNeeded = cfg.prDraft === true;
99811
+ if (!prReadyNeeded && wantAutoMerge) {
99697
99812
  const fallbackEnabled = cfg.manualMergeWhenAutoMergeDisabled !== false;
99698
99813
  const repoAllowsAutoMerge = await detectRepoAutoMergeAllowed(prUrl, cmd, cwd2, log2);
99699
99814
  if (repoAllowsAutoMerge === false && fallbackEnabled) {
@@ -99714,10 +99829,23 @@ ${indented}${suffix}`, "yellow");
99714
99829
  }
99715
99830
  }
99716
99831
  }
99832
+ } else if (prReadyNeeded && wantAutoMerge) {
99833
+ manualMergePending = true;
99717
99834
  }
99718
99835
  const ciResult = await fixConflictsAndCiLoop(ctx, prUrl, wantFixCi, checkPrConflict);
99719
99836
  if (ciResult !== 0)
99720
99837
  return ciResult;
99838
+ if (prReadyNeeded) {
99839
+ emit2("pr-ready");
99840
+ try {
99841
+ await cmd.run(["gh", "pr", "ready", prUrl], cwd2);
99842
+ log2(` converted ${prUrl} from draft to ready`, "green");
99843
+ } catch (err) {
99844
+ const e = err;
99845
+ log2(`! gh pr ready failed for ${prUrl}: ${e.stderr?.trim() || e.message}`, "yellow");
99846
+ manualMergePending = false;
99847
+ }
99848
+ }
99721
99849
  if (manualMergePending) {
99722
99850
  try {
99723
99851
  await cmd.run(["gh", "pr", "merge", prUrl, `--${cfg.autoMergeStrategy}`], cwd2);
@@ -100203,6 +100331,7 @@ function createSpawnWorker(input) {
100203
100331
  neverTouch: cfg.boundaries.never_touch,
100204
100332
  metaOnlyFiles: cfg.boundaries.meta_only_files,
100205
100333
  manualMergeWhenAutoMergeDisabled: cfg.manualMergeWhenAutoMergeDisabled,
100334
+ prDraft: cfg.prDraft,
100206
100335
  validateCommands: [cfg.commands.test, cfg.commands.lint, cfg.commands.typecheck].filter((c) => Boolean(c))
100207
100336
  },
100208
100337
  respawnWorker: respawn
@@ -253151,8 +253280,105 @@ var init_comment_sync2 = __esm(() => {
253151
253280
  init_linear();
253152
253281
  });
253153
253282
 
253154
- // apps/agent/src/agent/wire.ts
253283
+ // apps/agent/src/features/pr-tracker/state.ts
253155
253284
  import { join as join33 } from "path";
253285
+ async function readState2(projectRoot) {
253286
+ const path = join33(projectRoot, PR_TRACKER_STATE_RELPATH);
253287
+ const file2 = Bun.file(path);
253288
+ if (!await file2.exists())
253289
+ return {};
253290
+ try {
253291
+ const raw = await file2.text();
253292
+ const parsed = JSON.parse(raw);
253293
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
253294
+ return parsed;
253295
+ }
253296
+ return {};
253297
+ } catch {
253298
+ return {};
253299
+ }
253300
+ }
253301
+ async function writeState2(projectRoot, state) {
253302
+ const path = join33(projectRoot, PR_TRACKER_STATE_RELPATH);
253303
+ await Bun.write(path, JSON.stringify(state, null, 2));
253304
+ }
253305
+ var PR_TRACKER_STATE_RELPATH = ".ralph/pr-tracker-state.json";
253306
+ var init_state3 = () => {};
253307
+
253308
+ // apps/agent/src/features/pr-tracker/tracker.ts
253309
+ class PrTracker {
253310
+ opts;
253311
+ state = {};
253312
+ loaded = false;
253313
+ now;
253314
+ constructor(opts) {
253315
+ this.opts = opts;
253316
+ this.now = opts.now ?? (() => new Date);
253317
+ }
253318
+ async load() {
253319
+ if (this.loaded)
253320
+ return;
253321
+ this.state = await readState2(this.opts.projectRoot);
253322
+ this.loaded = true;
253323
+ }
253324
+ snapshot() {
253325
+ return JSON.parse(JSON.stringify(this.state));
253326
+ }
253327
+ isBailed(identifier) {
253328
+ return this.state[identifier]?.bailed === true;
253329
+ }
253330
+ getAttempts(identifier) {
253331
+ return this.state[identifier]?.attempts ?? 0;
253332
+ }
253333
+ async recordFailure(identifier, reason) {
253334
+ await this.load();
253335
+ const nowIso = this.now().toISOString();
253336
+ const existing = this.state[identifier];
253337
+ if (existing?.bailed) {
253338
+ existing.lastReason = reason;
253339
+ await this.flush();
253340
+ return { kind: "bail", attempts: existing.attempts, firstBail: false };
253341
+ }
253342
+ const attempts = (existing?.attempts ?? 0) + 1;
253343
+ const entry = {
253344
+ attempts,
253345
+ firstFailedAt: existing?.firstFailedAt ?? nowIso,
253346
+ lastDemotedAt: nowIso,
253347
+ lastReason: reason
253348
+ };
253349
+ if (attempts >= this.opts.maxRecoveryAttempts) {
253350
+ entry.bailed = true;
253351
+ this.state[identifier] = entry;
253352
+ await this.flush();
253353
+ return { kind: "bail", attempts, firstBail: true };
253354
+ }
253355
+ this.state[identifier] = entry;
253356
+ await this.flush();
253357
+ return { kind: "demote", attempts };
253358
+ }
253359
+ async clear(identifier) {
253360
+ await this.load();
253361
+ if (!(identifier in this.state))
253362
+ return;
253363
+ delete this.state[identifier];
253364
+ await this.flush();
253365
+ }
253366
+ async flush() {
253367
+ await writeState2(this.opts.projectRoot, this.state);
253368
+ }
253369
+ }
253370
+ var init_tracker = __esm(() => {
253371
+ init_state3();
253372
+ });
253373
+
253374
+ // apps/agent/src/features/pr-tracker/index.ts
253375
+ var init_pr_tracker = __esm(() => {
253376
+ init_tracker();
253377
+ init_state3();
253378
+ });
253379
+
253380
+ // apps/agent/src/agent/wire.ts
253381
+ import { join as join34 } from "path";
253156
253382
  function buildAgentCoordinator(input) {
253157
253383
  const {
253158
253384
  args,
@@ -253171,7 +253397,7 @@ function buildAgentCoordinator(input) {
253171
253397
  onWorkerCmd,
253172
253398
  onAwaitingTicket
253173
253399
  } = input;
253174
- const logsDir = join33(projectRoot, ".ralph", "logs");
253400
+ const logsDir = join34(projectRoot, ".ralph", "logs");
253175
253401
  const bus = createBus();
253176
253402
  subscribeAgentDiag(bus, onLog);
253177
253403
  const diag = (area, message, color) => {
@@ -253325,6 +253551,11 @@ function buildAgentCoordinator(input) {
253325
253551
  now: () => new Date
253326
253552
  };
253327
253553
  }
253554
+ const prTrackerEnabled = args.prTrackerEnabled === undefined ? cfg.prTracker.enabled : args.prTrackerEnabled;
253555
+ const prTracker = prTrackerEnabled ? new PrTracker({
253556
+ projectRoot,
253557
+ maxRecoveryAttempts: cfg.prTracker.maxRecoveryAttempts
253558
+ }) : null;
253328
253559
  const commentSync = createCommentSyncHooks({
253329
253560
  apiKey,
253330
253561
  cfg,
@@ -253339,7 +253570,7 @@ function buildAgentCoordinator(input) {
253339
253570
  pollContext = new PollContext;
253340
253571
  },
253341
253572
  fetchTodo: () => resolvers.fetchByGet(indicators.getTodo, excludeFromTodo),
253342
- fetchInProgress: () => resolvers.fetchByGet(indicators.getInProgress, []),
253573
+ fetchInProgress: () => resolvers.fetchByGet(indicators.getInProgress, unionMarkers(indicators.setError)),
253343
253574
  fetchReview: () => resolvers.fetchByGet(indicators.getReview, excludeFromReview),
253344
253575
  fetchMentions,
253345
253576
  fetchDoneCandidates: () => fetchDoneCandidatesWith(apiKey, team, assignee, indicators),
@@ -253378,7 +253609,8 @@ function buildAgentCoordinator(input) {
253378
253609
  ...indicators.getAutoMerge !== undefined ? { getAutoMerge: indicators.getAutoMerge } : {},
253379
253610
  postComments: cfg.linear.postComments,
253380
253611
  commentEveryIterations: cfg.linear.updateEveryIterations,
253381
- ...args.maxTickets > 0 ? { maxTickets: args.maxTickets } : {}
253612
+ ...args.maxTickets > 0 ? { maxTickets: args.maxTickets } : {},
253613
+ ...prTracker ? { prTracker } : {}
253382
253614
  });
253383
253615
  coordRef.current = coord;
253384
253616
  const filterDesc = describeIndicators(indicators, team, assignee);
@@ -253422,6 +253654,7 @@ var init_wire = __esm(() => {
253422
253654
  init_worker();
253423
253655
  init_baseline();
253424
253656
  init_comment_sync2();
253657
+ init_pr_tracker();
253425
253658
  });
253426
253659
 
253427
253660
  // apps/agent/src/agent/json-log/json-log-file.ts
@@ -253676,7 +253909,7 @@ var init_output_utils = __esm(() => {
253676
253909
  });
253677
253910
 
253678
253911
  // apps/agent/src/agent/state/worker-state-poll.ts
253679
- import { join as join34 } from "path";
253912
+ import { join as join35 } from "path";
253680
253913
  function parseSubtasks(tasksMd) {
253681
253914
  const out = [];
253682
253915
  let skipSection = false;
@@ -253709,7 +253942,7 @@ function initialWorkerSnapshot() {
253709
253942
  async function readWorkerSnapshot(input) {
253710
253943
  const next = { ...input.prev };
253711
253944
  try {
253712
- const file2 = Bun.file(join34(input.statesDir, input.changeName, ".ralph-state.json"));
253945
+ const file2 = Bun.file(join35(input.statesDir, input.changeName, ".ralph-state.json"));
253713
253946
  if (await file2.exists()) {
253714
253947
  const json2 = await file2.json();
253715
253948
  next.iter = json2.iteration ?? next.iter;
@@ -253718,10 +253951,10 @@ async function readWorkerSnapshot(input) {
253718
253951
  } catch {}
253719
253952
  if (input.changeDir) {
253720
253953
  try {
253721
- const tasksFile = Bun.file(join34(input.changeDir, "tasks.md"));
253722
- const proposalFile = Bun.file(join34(input.changeDir, "proposal.md"));
253723
- const designFile = Bun.file(join34(input.changeDir, "design.md"));
253724
- const reviewFindingsFile = Bun.file(join34(input.changeDir, "review-findings.md"));
253954
+ const tasksFile = Bun.file(join35(input.changeDir, "tasks.md"));
253955
+ const proposalFile = Bun.file(join35(input.changeDir, "proposal.md"));
253956
+ const designFile = Bun.file(join35(input.changeDir, "design.md"));
253957
+ const reviewFindingsFile = Bun.file(join35(input.changeDir, "review-findings.md"));
253725
253958
  const [tasksText, proposalText, designText, reviewFindingsText] = await Promise.all([
253726
253959
  tasksFile.exists().then((ok) => ok ? tasksFile.text() : null),
253727
253960
  proposalFile.exists().then((ok) => ok ? proposalFile.text() : null),
@@ -253784,7 +254017,7 @@ var init_worker_state_poll = __esm(() => {
253784
254017
  });
253785
254018
 
253786
254019
  // apps/agent/src/components/AgentMode.tsx
253787
- import { join as join35 } from "path";
254020
+ import { join as join36 } from "path";
253788
254021
  async function appendSteeringImpl(changeDir, message) {
253789
254022
  await runWithContext(createDefaultContext(), async () => {
253790
254023
  appendSteeringMessage(changeDir, message);
@@ -253801,6 +254034,16 @@ function orderSubtasksForCappedDisplay(subtasks) {
253801
254034
  (s.done ? done : pending).push(s);
253802
254035
  return [...pending, ...done];
253803
254036
  }
254037
+ function pickLatestGatedTicket(tickets) {
254038
+ if (tickets.size === 0)
254039
+ return { top: null, moreCount: 0 };
254040
+ const sorted = Array.from(tickets.entries()).sort(([, a], [, b2]) => {
254041
+ const aTime = a.since ? new Date(a.since).getTime() : 0;
254042
+ const bTime = b2.since ? new Date(b2.since).getTime() : 0;
254043
+ return bTime - aTime;
254044
+ });
254045
+ return { top: sorted[0], moreCount: sorted.length - 1 };
254046
+ }
253804
254047
  function fmtCmd(argv) {
253805
254048
  const joined = argv.join(" ");
253806
254049
  return joined.length > CMD_DISPLAY_MAX ? joined.slice(0, CMD_DISPLAY_MAX - 1) + "\u2026" : joined;
@@ -254795,7 +255038,11 @@ function AgentMode({
254795
255038
  })
254796
255039
  }, undefined, false, undefined, this)
254797
255040
  }, undefined, false, undefined, this),
254798
- Array.from(gatedTicketsRef.current.entries()).map(([changeName, g2]) => {
255041
+ (() => {
255042
+ const { top, moreCount } = pickLatestGatedTicket(gatedTicketsRef.current);
255043
+ if (!top)
255044
+ return null;
255045
+ const [changeName, g2] = top;
254799
255046
  const askedAgo = g2.since ? fmtElapsed(now2 - Date.parse(g2.since)) : "just now";
254800
255047
  const cardLabelWidth = g2.issueIdentifier.length + 2;
254801
255048
  const cardLabelNode = /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(jsx_dev_runtime10.Fragment, {
@@ -254815,63 +255062,71 @@ function AgentMode({
254815
255062
  }, undefined, false, undefined, this)
254816
255063
  ]
254817
255064
  }, undefined, true, undefined, this);
254818
- return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(LabeledBox, {
254819
- labelNode: cardLabelNode,
254820
- labelVisualWidth: cardLabelWidth,
254821
- borderColor: "yellow",
254822
- paddingX: 1,
254823
- gap: 2,
254824
- width: termWidth,
255065
+ return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(jsx_dev_runtime10.Fragment, {
254825
255066
  children: [
254826
- /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
254827
- color: "yellow",
254828
- bold: true,
254829
- children: "[GATE]"
254830
- }, undefined, false, undefined, this),
254831
- /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
254832
- color: "yellow",
254833
- children: "Awaiting confirmation"
254834
- }, undefined, false, undefined, this),
254835
- /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
254836
- dimColor: true,
254837
- children: "\xB7"
254838
- }, undefined, false, undefined, this),
254839
- /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
254840
- dimColor: true,
254841
- children: "round"
254842
- }, undefined, false, undefined, this),
254843
- /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
254844
- color: "white",
254845
- bold: true,
254846
- children: g2.round
254847
- }, undefined, false, undefined, this),
254848
- /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
254849
- dimColor: true,
254850
- children: "\xB7"
254851
- }, undefined, false, undefined, this),
254852
- /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
254853
- dimColor: true,
254854
- children: "asked"
254855
- }, undefined, false, undefined, this),
254856
- /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
254857
- color: "white",
254858
- children: askedAgo
254859
- }, undefined, false, undefined, this),
254860
- /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
254861
- dimColor: true,
254862
- children: "ago"
254863
- }, undefined, false, undefined, this),
254864
- /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
254865
- dimColor: true,
254866
- children: "\u2502"
254867
- }, undefined, false, undefined, this),
254868
- /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
255067
+ /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(LabeledBox, {
255068
+ labelNode: cardLabelNode,
255069
+ labelVisualWidth: cardLabelWidth,
255070
+ borderColor: "yellow",
255071
+ paddingX: 1,
255072
+ gap: 2,
255073
+ width: termWidth,
255074
+ children: [
255075
+ /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
255076
+ color: "yellow",
255077
+ bold: true,
255078
+ children: "[GATE]"
255079
+ }, undefined, false, undefined, this),
255080
+ /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
255081
+ color: "yellow",
255082
+ children: "Awaiting confirmation"
255083
+ }, undefined, false, undefined, this),
255084
+ /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
255085
+ dimColor: true,
255086
+ children: "\xB7"
255087
+ }, undefined, false, undefined, this),
255088
+ /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
255089
+ dimColor: true,
255090
+ children: "round"
255091
+ }, undefined, false, undefined, this),
255092
+ /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
255093
+ color: "white",
255094
+ bold: true,
255095
+ children: g2.round
255096
+ }, undefined, false, undefined, this),
255097
+ /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
255098
+ dimColor: true,
255099
+ children: "\xB7"
255100
+ }, undefined, false, undefined, this),
255101
+ /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
255102
+ dimColor: true,
255103
+ children: "asked"
255104
+ }, undefined, false, undefined, this),
255105
+ /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
255106
+ color: "white",
255107
+ children: askedAgo
255108
+ }, undefined, false, undefined, this),
255109
+ /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
255110
+ dimColor: true,
255111
+ children: "ago"
255112
+ }, undefined, false, undefined, this),
255113
+ /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
255114
+ dimColor: true,
255115
+ children: "\u2502"
255116
+ }, undefined, false, undefined, this),
255117
+ /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
255118
+ dimColor: true,
255119
+ children: trunc(g2.issueTitle, Math.max(20, termWidth - 70))
255120
+ }, undefined, false, undefined, this)
255121
+ ]
255122
+ }, `gated-${changeName}`, true, undefined, this),
255123
+ moreCount > 0 && /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(Text, {
254869
255124
  dimColor: true,
254870
- children: trunc(g2.issueTitle, Math.max(20, termWidth - 70))
255125
+ children: ` +${moreCount} more awaiting confirmation`
254871
255126
  }, undefined, false, undefined, this)
254872
255127
  ]
254873
- }, `gated-${changeName}`, true, undefined, this);
254874
- }),
255128
+ }, undefined, true, undefined, this);
255129
+ })(),
254875
255130
  coord?.activeWorkers.map((w2, idx) => {
254876
255131
  const isFocused = idx === safeFocusedIdx;
254877
255132
  const meta3 = workerMetaRef.current.get(w2.changeName);
@@ -255245,7 +255500,7 @@ function AgentMode({
255245
255500
  },
255246
255501
  onSubmit: async (message) => {
255247
255502
  try {
255248
- await appendSteering2(join35(tasksDir, w2.changeName), message);
255503
+ await appendSteering2(join36(tasksDir, w2.changeName), message);
255249
255504
  fileEmit({ type: "steering_submitted", changeName: w2.changeName, message });
255250
255505
  } catch (err) {
255251
255506
  const text = err.message;
@@ -255493,7 +255748,7 @@ var exports_list = {};
255493
255748
  __export(exports_list, {
255494
255749
  runList: () => runList
255495
255750
  });
255496
- import { join as join36 } from "path";
255751
+ import { join as join37 } from "path";
255497
255752
  function countTaskItems(content) {
255498
255753
  const checked = (content.match(/^- \[x\]/gm) ?? []).length;
255499
255754
  const unchecked = (content.match(/^- \[ \]/gm) ?? []).length;
@@ -255506,13 +255761,13 @@ function buildLocalRows(statesDir, projectRoot) {
255506
255761
  const sources = [{ dir: statesDir, label: "main" }];
255507
255762
  const worktreesRoot = worktreesDir2(projectRoot);
255508
255763
  for (const wt of storage.list(worktreesRoot)) {
255509
- sources.push({ dir: join36(worktreesRoot, wt, ".ralph", "tasks"), label: `wt:${wt}` });
255764
+ sources.push({ dir: join37(worktreesRoot, wt, ".ralph", "tasks"), label: `wt:${wt}` });
255510
255765
  }
255511
255766
  for (const { dir, label } of sources) {
255512
255767
  for (const entry of storage.list(dir)) {
255513
255768
  if (seen.has(entry))
255514
255769
  continue;
255515
- const raw = storage.read(join36(dir, entry, ".ralph-state.json"));
255770
+ const raw = storage.read(join37(dir, entry, ".ralph-state.json"));
255516
255771
  if (raw === null)
255517
255772
  continue;
255518
255773
  let state;
@@ -255527,7 +255782,7 @@ function buildLocalRows(statesDir, projectRoot) {
255527
255782
  const firstLine = promptRaw.split(`
255528
255783
  `).find((l3) => l3.trim() !== "") ?? "";
255529
255784
  let progress = "\u2014";
255530
- const tasksContent = storage.read(join36(dir, entry, "tasks.md"));
255785
+ const tasksContent = storage.read(join37(dir, entry, "tasks.md"));
255531
255786
  if (tasksContent !== null) {
255532
255787
  const { checked, unchecked } = countTaskItems(tasksContent);
255533
255788
  const total = checked + unchecked;
@@ -255939,7 +256194,7 @@ var exports_json_runner = {};
255939
256194
  __export(exports_json_runner, {
255940
256195
  runAgentJson: () => runAgentJson
255941
256196
  });
255942
- import { join as join37 } from "path";
256197
+ import { join as join38 } from "path";
255943
256198
  import { mkdir as mkdir13 } from "fs/promises";
255944
256199
  import { homedir as homedir6 } from "os";
255945
256200
  function makeEmit(fileSink) {
@@ -255961,7 +256216,7 @@ async function runAgentJson({
255961
256216
  tasksDir,
255962
256217
  runPreflight: runPreflight2 = runPreflight
255963
256218
  }) {
255964
- await mkdir13(join37(homedir6(), ".ralph"), { recursive: true }).catch(() => {
256219
+ await mkdir13(join38(homedir6(), ".ralph"), { recursive: true }).catch(() => {
255965
256220
  return;
255966
256221
  });
255967
256222
  const fileSink = createJsonLogFileSink(args.jsonLogFile);
@@ -256169,7 +256424,7 @@ __export(exports_src2, {
256169
256424
  main: () => main2
256170
256425
  });
256171
256426
  import { mkdir as mkdir14 } from "fs/promises";
256172
- import { join as join38 } from "path";
256427
+ import { join as join39 } from "path";
256173
256428
  async function main2(argv) {
256174
256429
  if (argv.includes("--help") || argv.includes("-h")) {
256175
256430
  printAgentHelp();
@@ -256220,7 +256475,7 @@ async function main2(argv) {
256220
256475
  }
256221
256476
  await mkdir14(statesDir, { recursive: true });
256222
256477
  await mkdir14(tasksDir, { recursive: true });
256223
- await mkdir14(join38(projectRoot, ".ralph"), { recursive: true });
256478
+ await mkdir14(join39(projectRoot, ".ralph"), { recursive: true });
256224
256479
  if (shouldFallbackToJsonOutput(args, process.stdin.isTTY)) {
256225
256480
  process.stderr.write(`agent: stdin is not a TTY \u2014 falling back to --json-output mode.
256226
256481
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neriros/ralphy",
3
- "version": "3.8.10",
3
+ "version": "3.8.13",
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",
@@ -15,7 +15,7 @@
15
15
  "license": "MIT",
16
16
  "repository": {
17
17
  "type": "git",
18
- "url": "https://github.com/NeriRos/ralphy.git"
18
+ "url": "https://github.com/rosneri/ralphy.git"
19
19
  },
20
20
  "bin": {
21
21
  "ralphy": "./dist/shell/index.js",
@@ -95,7 +95,8 @@
95
95
  "@babel/plugin-transform-modules-systemjs": "^7.29.4",
96
96
  "axios": "^1.15.1",
97
97
  "fast-uri": "^3.1.2",
98
- "minimatch": "^10.2.3"
98
+ "minimatch": "^10.2.3",
99
+ "tmp": "^0.2.6"
99
100
  },
100
101
  "engines": {
101
102
  "bun": ">=1.0.0"