@neriros/ralphy 2.17.1 → 2.17.3

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/dist/cli/index.js CHANGED
@@ -35029,8 +35029,8 @@ import { readFileSync as readFileSync2 } from "fs";
35029
35029
  import { resolve } from "path";
35030
35030
  function getVersion() {
35031
35031
  try {
35032
- if ("2.17.1")
35033
- return "2.17.1";
35032
+ if ("2.17.3")
35033
+ return "2.17.3";
35034
35034
  } catch {}
35035
35035
  const dirsToTry = [];
35036
35036
  try {
@@ -35131,6 +35131,7 @@ async function parseArgs(argv) {
35131
35131
  createPr: false,
35132
35132
  fixCi: false,
35133
35133
  maxTickets: 0,
35134
+ projectRoot: undefined,
35134
35135
  jsonOutput: false
35135
35136
  };
35136
35137
  let expectModel = false;
@@ -35149,6 +35150,7 @@ async function parseArgs(argv) {
35149
35150
  let expectConcurrency = false;
35150
35151
  let expectMaxTickets = false;
35151
35152
  let expectIndicator = false;
35153
+ let expectProjectRoot = false;
35152
35154
  for (const arg of argv) {
35153
35155
  if (expectModel) {
35154
35156
  if (VALID_MODELS.has(arg)) {
@@ -35237,6 +35239,11 @@ async function parseArgs(argv) {
35237
35239
  expectIndicator = false;
35238
35240
  continue;
35239
35241
  }
35242
+ if (expectProjectRoot) {
35243
+ result2.projectRoot = arg;
35244
+ expectProjectRoot = false;
35245
+ continue;
35246
+ }
35240
35247
  switch (arg) {
35241
35248
  case "--claude":
35242
35249
  if (result2.engineSet && result2.engine !== "claude") {
@@ -35325,6 +35332,9 @@ async function parseArgs(argv) {
35325
35332
  case "--from-agent":
35326
35333
  result2.fromAgent = true;
35327
35334
  break;
35335
+ case "--project-root":
35336
+ expectProjectRoot = true;
35337
+ break;
35328
35338
  default:
35329
35339
  if (VALID_MODES.has(arg)) {
35330
35340
  result2.mode = arg;
@@ -39458,6 +39468,99 @@ var init_types2 = __esm(() => {
39458
39468
  });
39459
39469
  });
39460
39470
 
39471
+ // apps/cli/src/agent/worktree.ts
39472
+ import { basename, join as join3 } from "path";
39473
+ import { homedir } from "os";
39474
+ import { exists } from "fs/promises";
39475
+ function worktreesDir(projectRoot) {
39476
+ return join3(homedir(), ".ralph", basename(projectRoot), "worktrees");
39477
+ }
39478
+ function branchForChange(changeName) {
39479
+ return `ralph/${changeName}`;
39480
+ }
39481
+ async function createWorktree(projectRoot, changeName, runner) {
39482
+ const dir = worktreesDir(projectRoot);
39483
+ const cwd2 = join3(dir, changeName);
39484
+ const branch = branchForChange(changeName);
39485
+ const list = await runner.run(["worktree", "list", "--porcelain"], projectRoot);
39486
+ if (list.stdout.includes(`worktree ${cwd2}
39487
+ `)) {
39488
+ return { cwd: cwd2, branch };
39489
+ }
39490
+ let branchExists = true;
39491
+ try {
39492
+ await runner.run(["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], projectRoot);
39493
+ } catch {
39494
+ branchExists = false;
39495
+ }
39496
+ const cmd = branchExists ? ["worktree", "add", cwd2, branch] : ["worktree", "add", "-b", branch, cwd2];
39497
+ await runner.run(cmd, projectRoot);
39498
+ return { cwd: cwd2, branch };
39499
+ }
39500
+ async function removeWorktree(projectRoot, cwd2, runner) {
39501
+ await runner.run(["worktree", "remove", "--force", cwd2], projectRoot);
39502
+ }
39503
+ async function isWorktreeSafeToRemove(cwd2, base2, runner) {
39504
+ const status = await runner.run(["status", "--porcelain"], cwd2);
39505
+ const dirty = status.stdout.trim();
39506
+ let unpushedCommits = "";
39507
+ try {
39508
+ const log2 = await runner.run(["log", "--oneline", `${base2}..HEAD`, "--no-merges"], cwd2);
39509
+ unpushedCommits = log2.stdout.trim();
39510
+ } catch {
39511
+ unpushedCommits = "<unknown: failed to compare against base>";
39512
+ }
39513
+ if (dirty && unpushedCommits) {
39514
+ return {
39515
+ safe: false,
39516
+ reason: "uncommitted changes AND unpushed commits present",
39517
+ dirty,
39518
+ unpushedCommits
39519
+ };
39520
+ }
39521
+ if (dirty) {
39522
+ return {
39523
+ safe: false,
39524
+ reason: "uncommitted or untracked files present",
39525
+ dirty,
39526
+ unpushedCommits
39527
+ };
39528
+ }
39529
+ if (unpushedCommits) {
39530
+ return {
39531
+ safe: false,
39532
+ reason: `commits ahead of ${base2} were not pushed/PR'd`,
39533
+ dirty,
39534
+ unpushedCommits
39535
+ };
39536
+ }
39537
+ return { safe: true, dirty, unpushedCommits };
39538
+ }
39539
+ async function seedWorktreeMcpConfig(projectRoot, worktreeCwd) {
39540
+ const dst = join3(worktreeCwd, ".mcp.json");
39541
+ const src = join3(projectRoot, ".mcp.json");
39542
+ const source = await exists(dst) ? dst : await exists(src) ? src : null;
39543
+ if (!source)
39544
+ return;
39545
+ let parsed;
39546
+ try {
39547
+ parsed = await Bun.file(source).json();
39548
+ } catch {
39549
+ return;
39550
+ }
39551
+ const servers = parsed.mcpServers;
39552
+ if (servers && typeof servers === "object") {
39553
+ for (const cfg of Object.values(servers)) {
39554
+ if (Array.isArray(cfg.args)) {
39555
+ cfg.args = cfg.args.map((a) => typeof a === "string" && a.startsWith(".ralph/") ? join3(projectRoot, a) : a);
39556
+ }
39557
+ }
39558
+ }
39559
+ await Bun.write(dst, JSON.stringify(parsed, null, 2) + `
39560
+ `);
39561
+ }
39562
+ var init_worktree = () => {};
39563
+
39461
39564
  // node_modules/.bun/react@18.3.1/node_modules/react/cjs/react-jsx-dev-runtime.development.js
39462
39565
  var require_react_jsx_dev_runtime_development = __commonJS((exports) => {
39463
39566
  var React10 = __toESM(require_react());
@@ -59323,8 +59426,8 @@ var init_node = __esm(() => {
59323
59426
  });
59324
59427
 
59325
59428
  // packages/telemetry/src/index.ts
59326
- import { homedir } from "os";
59327
- import { join as join6 } from "path";
59429
+ import { homedir as homedir2 } from "os";
59430
+ import { join as join7 } from "path";
59328
59431
  import { randomUUID } from "crypto";
59329
59432
  function setDefaultProperties(props) {
59330
59433
  defaultProps = { ...defaultProps, ...props };
@@ -59332,7 +59435,7 @@ function setDefaultProperties(props) {
59332
59435
  async function init() {
59333
59436
  if (!enabled)
59334
59437
  return;
59335
- const idPath = join6(homedir(), ".ralph", ".telemetry-id");
59438
+ const idPath = join7(homedir2(), ".ralph", ".telemetry-id");
59336
59439
  const idFile = Bun.file(idPath);
59337
59440
  if (await idFile.exists()) {
59338
59441
  distinctId = (await idFile.text()).trim();
@@ -59407,12 +59510,12 @@ ${fence}`;
59407
59510
  }
59408
59511
 
59409
59512
  // apps/cli/src/agent/config.ts
59410
- import { join as join10 } from "path";
59513
+ import { join as join11 } from "path";
59411
59514
  function stripJsonComments(text) {
59412
59515
  return text.replace(/\/\/[^\n]*/g, "");
59413
59516
  }
59414
59517
  async function loadRalphyConfig(projectRoot) {
59415
- const path = join10(projectRoot, "ralphy.config.json");
59518
+ const path = join11(projectRoot, "ralphy.config.json");
59416
59519
  const file = Bun.file(path);
59417
59520
  if (!await file.exists()) {
59418
59521
  return RalphyConfigSchema.parse({});
@@ -59422,7 +59525,7 @@ async function loadRalphyConfig(projectRoot) {
59422
59525
  return RalphyConfigSchema.parse(raw);
59423
59526
  }
59424
59527
  async function ensureRalphyConfig(projectRoot) {
59425
- const path = join10(projectRoot, "ralphy.config.json");
59528
+ const path = join11(projectRoot, "ralphy.config.json");
59426
59529
  const file = Bun.file(path);
59427
59530
  if (await file.exists())
59428
59531
  return path;
@@ -59624,8 +59727,8 @@ var init_config = __esm(() => {
59624
59727
 
59625
59728
  // packages/log/src/log.ts
59626
59729
  import { appendFile } from "fs/promises";
59627
- import { join as join11, dirname as dirname4 } from "path";
59628
- import { homedir as homedir2 } from "os";
59730
+ import { join as join12, dirname as dirname4 } from "path";
59731
+ import { homedir as homedir3 } from "os";
59629
59732
  import { mkdir as mkdir2 } from "fs/promises";
59630
59733
  function fmt(type, text) {
59631
59734
  return `[${new Date().toISOString()}] [${type}] ${text}
@@ -59670,25 +59773,25 @@ async function initWorkerLog(logFile) {
59670
59773
  var ANSI_RE, AGENT_LOG_PATH;
59671
59774
  var init_log = __esm(() => {
59672
59775
  ANSI_RE = /\x1b(?:\[[0-9;]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|.)/g;
59673
- AGENT_LOG_PATH = join11(homedir2(), ".ralph", "agent-mode.log");
59776
+ AGENT_LOG_PATH = join12(homedir3(), ".ralph", "agent-mode.log");
59674
59777
  mkdir2(dirname4(AGENT_LOG_PATH), { recursive: true }).catch(() => {
59675
59778
  return;
59676
59779
  });
59677
59780
  });
59678
59781
 
59679
59782
  // packages/core/src/layout.ts
59680
- import { join as join12 } from "path";
59783
+ import { join as join13 } from "path";
59681
59784
  function projectLayout(root) {
59682
- const statesDir = join12(root, ".ralph", "tasks");
59683
- const tasksDir = join12(root, "openspec", "changes");
59785
+ const statesDir = join13(root, ".ralph", "tasks");
59786
+ const tasksDir = join13(root, "openspec", "changes");
59684
59787
  return {
59685
59788
  root,
59686
59789
  statesDir,
59687
59790
  tasksDir,
59688
- agentStateFile: join12(root, ".ralph", "agent-state.json"),
59689
- changeDir: (name) => join12(tasksDir, name),
59690
- taskStateDir: (name) => join12(statesDir, name),
59691
- stateFile: (name) => join12(statesDir, name, STATE_FILE2)
59791
+ agentStateFile: join13(root, ".ralph", "agent-state.json"),
59792
+ changeDir: (name) => join13(tasksDir, name),
59793
+ taskStateDir: (name) => join13(statesDir, name),
59794
+ stateFile: (name) => join13(statesDir, name, STATE_FILE2)
59692
59795
  };
59693
59796
  }
59694
59797
  var STATE_FILE2 = ".ralph-state.json";
@@ -59743,7 +59846,15 @@ function buildIssueFilter(spec) {
59743
59846
  where.and = [{ state: current }, noStatus];
59744
59847
  }
59745
59848
  if (labels.length > 0) {
59746
- where.labels = { ...where.labels, every: { name: { nin: labels } } };
59849
+ const includeLabels = where.labels;
59850
+ const excludeLabels = { every: { name: { nin: labels } } };
59851
+ if (includeLabels === undefined) {
59852
+ where.labels = excludeLabels;
59853
+ } else {
59854
+ const existingAnd = where.and ?? [];
59855
+ where.and = [...existingAnd, { labels: includeLabels }, { labels: excludeLabels }];
59856
+ delete where.labels;
59857
+ }
59747
59858
  }
59748
59859
  }
59749
59860
  return where;
@@ -60298,7 +60409,7 @@ var init_coordinator = __esm(() => {
60298
60409
  });
60299
60410
 
60300
60411
  // apps/cli/src/agent/scaffold.ts
60301
- import { join as join13 } from "path";
60412
+ import { join as join14 } from "path";
60302
60413
  import { mkdir as mkdir3 } from "fs/promises";
60303
60414
  function changeNameForIssue(issue) {
60304
60415
  const slug = issue.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
@@ -60306,10 +60417,10 @@ function changeNameForIssue(issue) {
60306
60417
  }
60307
60418
  async function scaffoldChangeForIssue(tasksDir, statesDir, issue, comments = [], appendPrompt = "") {
60308
60419
  const name = changeNameForIssue(issue);
60309
- const changeDir = join13(tasksDir, name);
60310
- const stateDir = join13(statesDir, name);
60420
+ const changeDir = join14(tasksDir, name);
60421
+ const stateDir = join14(statesDir, name);
60311
60422
  await mkdir3(changeDir, { recursive: true });
60312
- await mkdir3(join13(changeDir, "specs"), { recursive: true });
60423
+ await mkdir3(join14(changeDir, "specs"), { recursive: true });
60313
60424
  await mkdir3(stateDir, { recursive: true });
60314
60425
  const commentsBlock = comments.length > 0 ? [
60315
60426
  "",
@@ -60361,106 +60472,13 @@ async function scaffoldChangeForIssue(tasksDir, statesDir, issue, comments = [],
60361
60472
  ""
60362
60473
  ].join(`
60363
60474
  `);
60364
- await Bun.write(join13(changeDir, "proposal.md"), proposal);
60365
- await Bun.write(join13(changeDir, "tasks.md"), tasks);
60366
- await Bun.write(join13(changeDir, "design.md"), design);
60475
+ await Bun.write(join14(changeDir, "proposal.md"), proposal);
60476
+ await Bun.write(join14(changeDir, "tasks.md"), tasks);
60477
+ await Bun.write(join14(changeDir, "design.md"), design);
60367
60478
  return name;
60368
60479
  }
60369
60480
  var init_scaffold = () => {};
60370
60481
 
60371
- // apps/cli/src/agent/worktree.ts
60372
- import { basename, join as join14 } from "path";
60373
- import { homedir as homedir3 } from "os";
60374
- import { exists } from "fs/promises";
60375
- function worktreesDir(projectRoot) {
60376
- return join14(homedir3(), ".ralph", basename(projectRoot), "worktrees");
60377
- }
60378
- function branchForChange(changeName) {
60379
- return `ralph/${changeName}`;
60380
- }
60381
- async function createWorktree(projectRoot, changeName, runner) {
60382
- const dir = worktreesDir(projectRoot);
60383
- const cwd2 = join14(dir, changeName);
60384
- const branch = branchForChange(changeName);
60385
- const list = await runner.run(["worktree", "list", "--porcelain"], projectRoot);
60386
- if (list.stdout.includes(`worktree ${cwd2}
60387
- `)) {
60388
- return { cwd: cwd2, branch };
60389
- }
60390
- let branchExists = true;
60391
- try {
60392
- await runner.run(["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], projectRoot);
60393
- } catch {
60394
- branchExists = false;
60395
- }
60396
- const cmd = branchExists ? ["worktree", "add", cwd2, branch] : ["worktree", "add", "-b", branch, cwd2];
60397
- await runner.run(cmd, projectRoot);
60398
- return { cwd: cwd2, branch };
60399
- }
60400
- async function removeWorktree(projectRoot, cwd2, runner) {
60401
- await runner.run(["worktree", "remove", "--force", cwd2], projectRoot);
60402
- }
60403
- async function isWorktreeSafeToRemove(cwd2, base2, runner) {
60404
- const status = await runner.run(["status", "--porcelain"], cwd2);
60405
- const dirty = status.stdout.trim();
60406
- let unpushedCommits = "";
60407
- try {
60408
- const log2 = await runner.run(["log", "--oneline", `${base2}..HEAD`, "--no-merges"], cwd2);
60409
- unpushedCommits = log2.stdout.trim();
60410
- } catch {
60411
- unpushedCommits = "<unknown: failed to compare against base>";
60412
- }
60413
- if (dirty && unpushedCommits) {
60414
- return {
60415
- safe: false,
60416
- reason: "uncommitted changes AND unpushed commits present",
60417
- dirty,
60418
- unpushedCommits
60419
- };
60420
- }
60421
- if (dirty) {
60422
- return {
60423
- safe: false,
60424
- reason: "uncommitted or untracked files present",
60425
- dirty,
60426
- unpushedCommits
60427
- };
60428
- }
60429
- if (unpushedCommits) {
60430
- return {
60431
- safe: false,
60432
- reason: `commits ahead of ${base2} were not pushed/PR'd`,
60433
- dirty,
60434
- unpushedCommits
60435
- };
60436
- }
60437
- return { safe: true, dirty, unpushedCommits };
60438
- }
60439
- async function seedWorktreeMcpConfig(projectRoot, worktreeCwd) {
60440
- const dst = join14(worktreeCwd, ".mcp.json");
60441
- const src = join14(projectRoot, ".mcp.json");
60442
- const source = await exists(dst) ? dst : await exists(src) ? src : null;
60443
- if (!source)
60444
- return;
60445
- let parsed;
60446
- try {
60447
- parsed = await Bun.file(source).json();
60448
- } catch {
60449
- return;
60450
- }
60451
- const servers = parsed.mcpServers;
60452
- if (servers && typeof servers === "object") {
60453
- for (const cfg of Object.values(servers)) {
60454
- if (Array.isArray(cfg.args)) {
60455
- cfg.args = cfg.args.map((a) => typeof a === "string" && a.startsWith(".ralph/") ? join14(projectRoot, a) : a);
60456
- }
60457
- }
60458
- }
60459
- await Bun.write(dst, JSON.stringify(parsed, null, 2) + `
60460
- `);
60461
- }
60462
- var init_worktree = () => {};
60463
-
60464
60482
  // apps/cli/src/agent/pr.ts
60465
60483
  function defaultTitle(issue) {
60466
60484
  return `${issue.identifier}: ${issue.title}`;
@@ -66953,7 +66971,8 @@ function ensureState(changeDir) {
66953
66971
 
66954
66972
  // apps/cli/src/components/TaskList.tsx
66955
66973
  var import_react22 = __toESM(require_react(), 1);
66956
- import { join as join3 } from "path";
66974
+ import { join as join4 } from "path";
66975
+ init_worktree();
66957
66976
  var jsx_dev_runtime = __toESM(require_jsx_dev_runtime(), 1);
66958
66977
  function countTaskItems(content) {
66959
66978
  const checked = (content.match(/^- \[x\]/gm) ?? []).length;
@@ -66966,10 +66985,10 @@ function buildRows(statesDir, projectRoot) {
66966
66985
  const seenNames = new Set;
66967
66986
  const sources = [{ dir: statesDir, label: "main" }];
66968
66987
  if (projectRoot) {
66969
- const worktreesRoot = join3(projectRoot, ".ralph", "worktrees");
66988
+ const worktreesRoot = worktreesDir(projectRoot);
66970
66989
  for (const wt of storage.list(worktreesRoot)) {
66971
66990
  sources.push({
66972
- dir: join3(worktreesRoot, wt, ".ralph", "tasks"),
66991
+ dir: join4(worktreesRoot, wt, ".ralph", "tasks"),
66973
66992
  label: `wt:${wt}`
66974
66993
  });
66975
66994
  }
@@ -66978,7 +66997,7 @@ function buildRows(statesDir, projectRoot) {
66978
66997
  for (const entry of storage.list(dir)) {
66979
66998
  if (seenNames.has(entry))
66980
66999
  continue;
66981
- const raw = storage.read(join3(dir, entry, ".ralph-state.json"));
67000
+ const raw = storage.read(join4(dir, entry, ".ralph-state.json"));
66982
67001
  if (raw === null)
66983
67002
  continue;
66984
67003
  let state;
@@ -66994,7 +67013,7 @@ function buildRows(statesDir, projectRoot) {
66994
67013
  `).find((l) => l.trim() !== "") ?? "";
66995
67014
  let progress = "\u2014";
66996
67015
  let progressStyled = true;
66997
- const tasksContent = storage.read(join3(dir, entry, "tasks.md"));
67016
+ const tasksContent = storage.read(join4(dir, entry, "tasks.md"));
66998
67017
  if (tasksContent !== null) {
66999
67018
  const { checked, unchecked } = countTaskItems(tasksContent);
67000
67019
  const total = checked + unchecked;
@@ -67135,7 +67154,7 @@ function TaskList({ statesDir, projectRoot }) {
67135
67154
  }
67136
67155
 
67137
67156
  // apps/cli/src/components/TaskStatus.tsx
67138
- import { join as join4 } from "path";
67157
+ import { join as join5 } from "path";
67139
67158
  var jsx_dev_runtime2 = __toESM(require_jsx_dev_runtime(), 1);
67140
67159
  var HEAVY_RULE = "============================================";
67141
67160
  var LIGHT_RULE = "--------------------------------------------";
@@ -67146,7 +67165,7 @@ function TaskStatus({ state, stateDir }) {
67146
67165
  const time = Math.round(state.usage.total_duration_ms / 1000 * 10) / 10 + "s";
67147
67166
  const artifacts = OPENSPEC_ARTIFACTS.map((name) => ({
67148
67167
  name,
67149
- exists: storage.read(join4(stateDir, name)) !== null
67168
+ exists: storage.read(join5(stateDir, name)) !== null
67150
67169
  }));
67151
67170
  const recent = state.history.slice(-10);
67152
67171
  return /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Box_default, {
@@ -67289,7 +67308,7 @@ function TaskStatus({ state, stateDir }) {
67289
67308
 
67290
67309
  // apps/cli/src/components/TaskLoop.tsx
67291
67310
  var import_react56 = __toESM(require_react(), 1);
67292
- import { join as join9 } from "path";
67311
+ import { join as join10 } from "path";
67293
67312
 
67294
67313
  // node_modules/.bun/@inkjs+ui@2.0.0+5b84dde3d6cd3930/node_modules/@inkjs/ui/build/components/badge/badge.js
67295
67314
  var import_react24 = __toESM(require_react(), 1);
@@ -70853,7 +70872,7 @@ function StopMessage({
70853
70872
 
70854
70873
  // apps/cli/src/hooks/useLoop.ts
70855
70874
  var import_react55 = __toESM(require_react(), 1);
70856
- import { join as join8 } from "path";
70875
+ import { join as join9 } from "path";
70857
70876
 
70858
70877
  // packages/engine/src/spawn.ts
70859
70878
  var {spawn: bunSpawn } = globalThis.Bun;
@@ -70863,7 +70882,7 @@ var spawn = bunSpawn;
70863
70882
  import { createWriteStream } from "fs";
70864
70883
  import { mkdtemp, unlink, mkdir } from "fs/promises";
70865
70884
  import { dirname as dirname2 } from "path";
70866
- import { join as join5 } from "path";
70885
+ import { join as join6 } from "path";
70867
70886
  import { tmpdir } from "os";
70868
70887
 
70869
70888
  // packages/engine/src/feed-events.ts
@@ -71596,7 +71615,7 @@ function buildCodexArgs() {
71596
71615
  return ["exec", "--json", "--color", "never", "--dangerously-bypass-approvals-and-sandbox", "-"];
71597
71616
  }
71598
71617
  async function runInteractive(model, prompt, taskDir) {
71599
- const promptFile = taskDir ? join5(taskDir, "_interactive_prompt.md") : join5(await mkdtemp(join5(tmpdir(), "ralph-")), "prompt.md");
71618
+ const promptFile = taskDir ? join6(taskDir, "_interactive_prompt.md") : join6(await mkdtemp(join6(tmpdir(), "ralph-")), "prompt.md");
71600
71619
  await Bun.write(promptFile, prompt);
71601
71620
  try {
71602
71621
  const cmd = [
@@ -71622,7 +71641,7 @@ async function runInteractive(model, prompt, taskDir) {
71622
71641
  stderr: "inherit"
71623
71642
  });
71624
71643
  const exitCode = await proc.exited;
71625
- const doneFile = taskDir ? join5(taskDir, "_interactive_done") : null;
71644
+ const doneFile = taskDir ? join6(taskDir, "_interactive_done") : null;
71626
71645
  if (doneFile && await Bun.file(doneFile).exists()) {
71627
71646
  return { exitCode: 0, usage: null, sessionId: null, rateLimited: false };
71628
71647
  }
@@ -71839,12 +71858,12 @@ function commitTaskDir(taskDir, message) {
71839
71858
  init_src();
71840
71859
 
71841
71860
  // packages/core/src/loop.ts
71842
- import { join as join7 } from "path";
71861
+ import { join as join8 } from "path";
71843
71862
  var STEERING_MAX_LINES = 20;
71844
71863
  function buildTaskPrompt(state, taskDir) {
71845
71864
  const storage = getStorage();
71846
71865
  let prompt = "";
71847
- const steeringContent = storage.read(join7(taskDir, "steering.md"));
71866
+ const steeringContent = storage.read(join8(taskDir, "steering.md"));
71848
71867
  if (steeringContent !== null) {
71849
71868
  const steeringLines = steeringContent.split(`
71850
71869
  `).filter((line) => !line.startsWith("#")).filter((line) => line.trim()).slice(0, STEERING_MAX_LINES);
@@ -71863,7 +71882,7 @@ function buildTaskPrompt(state, taskDir) {
71863
71882
  `;
71864
71883
  }
71865
71884
  }
71866
- const tasksContent = storage.read(join7(taskDir, "tasks.md"));
71885
+ const tasksContent = storage.read(join8(taskDir, "tasks.md"));
71867
71886
  if (tasksContent !== null) {
71868
71887
  const section = firstUnchecked(tasksContent);
71869
71888
  if (section) {
@@ -71878,7 +71897,7 @@ function buildTaskPrompt(state, taskDir) {
71878
71897
  prompt += `---
71879
71898
 
71880
71899
  `;
71881
- prompt += `**Tracking progress**: as you finish each item above, edit ` + `\`${join7(taskDir, "tasks.md")}\` and change its \`- [ ]\` to ` + `\`- [x]\` in the same commit. The loop reads this file between ` + `iterations and stops when no \`- [ ]\` items remain \u2014 if you do ` + `not tick the box, the next iteration will repeat this task.
71900
+ prompt += `**Tracking progress**: as you finish each item above, edit ` + `\`${join8(taskDir, "tasks.md")}\` and change its \`- [ ]\` to ` + `\`- [x]\` in the same commit. The loop reads this file between ` + `iterations and stops when no \`- [ ]\` items remain \u2014 if you do ` + `not tick the box, the next iteration will repeat this task.
71882
71901
 
71883
71902
  `;
71884
71903
  }
@@ -71899,7 +71918,7 @@ function buildTaskPrompt(state, taskDir) {
71899
71918
  `;
71900
71919
  }
71901
71920
  if (state.manualTest) {
71902
- const tasksContent2 = storage.read(join7(taskDir, "tasks.md"));
71921
+ const tasksContent2 = storage.read(join8(taskDir, "tasks.md"));
71903
71922
  const hasUncheckedTasks = tasksContent2 !== null && /^- \[ \]/m.test(tasksContent2);
71904
71923
  if (!hasUncheckedTasks) {
71905
71924
  const hasManualTestSection = tasksContent2 !== null && /^## Manual Testing/m.test(tasksContent2);
@@ -71948,7 +71967,7 @@ When all tasks are complete and all files are committed, push your branch and op
71948
71967
  }
71949
71968
  function checkStopSignal(taskDir, stateDir) {
71950
71969
  const storage = getStorage();
71951
- const stopFile = join7(taskDir, "STOP");
71970
+ const stopFile = join8(taskDir, "STOP");
71952
71971
  const reason = storage.read(stopFile);
71953
71972
  if (reason === null)
71954
71973
  return null;
@@ -72023,7 +72042,7 @@ function updateStateIteration(stateDir, result2, startedAt, engine, model, usage
72023
72042
  }
72024
72043
  function appendSteeringMessage(taskDir, message) {
72025
72044
  const storage = getStorage();
72026
- const steeringPath = join7(taskDir, "steering.md");
72045
+ const steeringPath = join8(taskDir, "steering.md");
72027
72046
  const existing = storage.read(steeringPath);
72028
72047
  const updated = existing ? `${message}
72029
72048
 
@@ -72090,11 +72109,11 @@ function useLoop(opts) {
72090
72109
  setLogLines((prev) => [...prev, { id: nextId(), kind: "feed", event }]);
72091
72110
  };
72092
72111
  runWithContext(createDefaultContext(), async () => {
72093
- const stateDir = join8(opts.statesDir, opts.name);
72094
- const tasksDir = join8(opts.tasksDir, opts.name);
72112
+ const stateDir = join9(opts.statesDir, opts.name);
72113
+ const tasksDir = join9(opts.tasksDir, opts.name);
72095
72114
  const storage = getStorage();
72096
72115
  let currentState;
72097
- const existingStateRaw = storage.read(join8(stateDir, ".ralph-state.json"));
72116
+ const existingStateRaw = storage.read(join9(stateDir, ".ralph-state.json"));
72098
72117
  if (existingStateRaw !== null) {
72099
72118
  currentState = readState(stateDir);
72100
72119
  if (currentState.engine !== opts.engine || currentState.model !== opts.model) {
@@ -72141,7 +72160,7 @@ function useLoop(opts) {
72141
72160
  setStopReason(stop);
72142
72161
  break;
72143
72162
  }
72144
- const tasksContent = storage.read(join8(tasksDir, "tasks.md"));
72163
+ const tasksContent = storage.read(join9(tasksDir, "tasks.md"));
72145
72164
  if (tasksContent !== null) {
72146
72165
  const remaining = countUnchecked(tasksContent);
72147
72166
  addInfo(`tasks.md: ${remaining} unchecked item${remaining === 1 ? "" : "s"} remaining`);
@@ -72181,7 +72200,7 @@ function useLoop(opts) {
72181
72200
  model: opts.model,
72182
72201
  prompt,
72183
72202
  logFlag: opts.log,
72184
- logFile: join8(stateDir, "log.json"),
72203
+ logFile: join9(stateDir, "log.json"),
72185
72204
  taskDir: tasksDir,
72186
72205
  interactive: false,
72187
72206
  onFeedEvent: addFeedEvent,
@@ -72204,7 +72223,7 @@ function useLoop(opts) {
72204
72223
  model: opts.model,
72205
72224
  prompt: buildSteeringPrompt(steerMessage),
72206
72225
  logFlag: opts.log,
72207
- logFile: join8(stateDir, "log.json"),
72226
+ logFile: join9(stateDir, "log.json"),
72208
72227
  taskDir: tasksDir,
72209
72228
  onFeedEvent: addResumeFeedEvent,
72210
72229
  signal: resumeController.signal,
@@ -72399,7 +72418,7 @@ function TaskLoop({ opts }) {
72399
72418
  }, [loop.isRunning, exit]);
72400
72419
  if (!loop.state)
72401
72420
  return null;
72402
- const stateDir = join9(opts.statesDir, opts.name);
72421
+ const stateDir = join10(opts.statesDir, opts.name);
72403
72422
  return /* @__PURE__ */ jsx_dev_runtime8.jsxDEV(Box_default, {
72404
72423
  flexDirection: "column",
72405
72424
  children: [
@@ -73753,8 +73772,11 @@ init_worktree();
73753
73772
  // apps/cli/src/debug.ts
73754
73773
  init_log();
73755
73774
  import { join as join20 } from "path";
73775
+ function fmtTs(d) {
73776
+ return d.toISOString().replace("T", " ").slice(0, 23);
73777
+ }
73756
73778
  var LOG_LINE_RE = /^\[(.+?)\] \[(.+?)\] (.+)$/;
73757
- function parseLog(content) {
73779
+ function parseTextLog(content) {
73758
73780
  return content.split(`
73759
73781
  `).filter(Boolean).flatMap((line) => {
73760
73782
  const m = LOG_LINE_RE.exec(line);
@@ -73766,23 +73788,154 @@ function parseLog(content) {
73766
73788
  return [{ ts, type: m[2], text: m[3] }];
73767
73789
  });
73768
73790
  }
73769
- function fmtTs(d) {
73770
- return d.toISOString().replace("T", " ").slice(0, 23);
73791
+ function parseJsonlLog(content, filterChangeName) {
73792
+ return content.split(`
73793
+ `).filter(Boolean).flatMap((line) => {
73794
+ let entry;
73795
+ try {
73796
+ entry = JSON.parse(line);
73797
+ } catch {
73798
+ return [];
73799
+ }
73800
+ const ts = new Date(entry.ts);
73801
+ if (isNaN(ts.getTime()))
73802
+ return [];
73803
+ if (filterChangeName && entry.changeName && entry.changeName !== filterChangeName) {
73804
+ return [];
73805
+ }
73806
+ const cn = entry.changeName ?? "";
73807
+ switch (entry.type) {
73808
+ case "started":
73809
+ return [{ ts, type: "agent", text: `agent started v${entry.version ?? "?"}` }];
73810
+ case "stopped":
73811
+ return [{ ts, type: "agent", text: "agent stopped" }];
73812
+ case "worker_started":
73813
+ return [{ ts, type: "spawn", text: `${cn}: worker spawned` }];
73814
+ case "worker_phase": {
73815
+ const detail = entry.detail ? ` (${entry.detail})` : "";
73816
+ return [{ ts, type: "phase", text: `${cn}: ${entry.phase}${detail}` }];
73817
+ }
73818
+ case "worker_cmd_start":
73819
+ return [
73820
+ {
73821
+ ts,
73822
+ type: "cmd",
73823
+ text: `${cn}: \u2192 ${(entry.cmd ?? []).slice(0, 4).join(" ")}`
73824
+ }
73825
+ ];
73826
+ case "worker_cmd_end":
73827
+ return [
73828
+ {
73829
+ ts,
73830
+ type: "cmd",
73831
+ text: `${cn}: \u2190 ${(entry.cmd ?? []).slice(0, 2).join(" ")} (${entry.durationMs}ms, ${entry.ok ? "ok" : "err"})`
73832
+ }
73833
+ ];
73834
+ case "worker_pr":
73835
+ return [{ ts, type: "pr", text: `${cn}: PR \u2192 ${entry.prUrl}` }];
73836
+ case "worker_exited":
73837
+ return [
73838
+ {
73839
+ ts,
73840
+ type: "exit",
73841
+ text: `${cn}: exited (code ${entry.exitCode ?? "?"})`
73842
+ }
73843
+ ];
73844
+ case "log":
73845
+ return [{ ts, type: "coord", text: entry.text ?? "" }];
73846
+ case "poll_done":
73847
+ return [
73848
+ {
73849
+ ts,
73850
+ type: "poll",
73851
+ text: `poll: found=${entry.found} added=${entry.added}`
73852
+ }
73853
+ ];
73854
+ default:
73855
+ return [];
73856
+ }
73857
+ });
73858
+ }
73859
+ function detectStuck(lines) {
73860
+ if (lines.length < 10)
73861
+ return null;
73862
+ const recent = lines.slice(-20).filter((l) => l.type === "phase");
73863
+ if (recent.length < 5)
73864
+ return null;
73865
+ const phaseNames = recent.map((l) => l.text.split(": ").slice(1).join(": "));
73866
+ const unique = new Set(phaseNames);
73867
+ if (unique.size !== 1)
73868
+ return null;
73869
+ const phase = phaseNames[0];
73870
+ const all = lines.filter((l) => l.type === "phase" && l.text.includes(phase));
73871
+ const first = all[0];
73872
+ const last2 = all[all.length - 1];
73873
+ const minutesStuck = (last2.ts.getTime() - first.ts.getTime()) / 60000;
73874
+ const prEntry = lines.find((l) => l.type === "pr");
73875
+ const cmdEntry = lines.find((l) => l.type === "cmd" && l.text.includes("gh") && l.text.includes("mergeable"));
73876
+ const watchingPrUrl = prEntry?.text.split("PR \u2192 ")[1] ?? cmdEntry?.text.match(/https:\/\/github\.com\/[^\s)]+/)?.[0];
73877
+ return {
73878
+ phase,
73879
+ count: all.length,
73880
+ firstSeen: first.ts,
73881
+ lastSeen: last2.ts,
73882
+ minutesStuck,
73883
+ watchingPrUrl
73884
+ };
73885
+ }
73886
+ async function inspectBinary(projectRoot) {
73887
+ const binPath = join20(projectRoot, ".ralph", "bin", "cli.js");
73888
+ const file = Bun.file(binPath);
73889
+ if (!await file.exists())
73890
+ return null;
73891
+ let embeddedVersion;
73892
+ try {
73893
+ const slice2 = await file.slice(0, 50000).text();
73894
+ const m = /"(\d+\.\d+\.\d+)"/.exec(slice2);
73895
+ if (m)
73896
+ embeddedVersion = m[1];
73897
+ } catch {}
73898
+ let builtAt;
73899
+ try {
73900
+ const r = Bun.spawnSync(["stat", "-f", "%Sm", "-t", "%Y-%m-%dT%H:%M:%S", binPath], {
73901
+ stderr: "ignore"
73902
+ });
73903
+ const s = r.stdout.toString().trim();
73904
+ if (s)
73905
+ builtAt = new Date(s);
73906
+ } catch {}
73907
+ return { path: binPath, embeddedVersion, builtAt };
73771
73908
  }
73772
73909
  var SPAWN_RE = /\u25B6 (\S+) \u2192 (\S+)/;
73773
- async function resolveDebugTarget(opts) {
73910
+ async function resolveDebugTarget(projectRoot, opts) {
73774
73911
  const agentLogFile = Bun.file(AGENT_LOG_PATH);
73775
- const agentLines = await agentLogFile.exists() ? parseLog(await agentLogFile.text()) : [];
73912
+ const textLines = await agentLogFile.exists() ? parseTextLog(await agentLogFile.text()) : [];
73913
+ const jsonlLogFile = Bun.file(join20(projectRoot, ".ralph", "agent.log"));
73914
+ const jsonlLines = await jsonlLogFile.exists() ? parseJsonlLog(await jsonlLogFile.text()) : [];
73915
+ const allLines = [...textLines, ...jsonlLines];
73776
73916
  if (opts.name && !opts.issue) {
73777
- for (const line of agentLines) {
73917
+ for (const line of allLines) {
73778
73918
  const m = SPAWN_RE.exec(line.text);
73779
73919
  if (m && m[2] === opts.name)
73780
73920
  return { changeName: opts.name, identifier: m[1] };
73781
73921
  }
73922
+ for (const line of allLines) {
73923
+ if (line.text.includes(opts.name) && line.text.includes("COD-")) {
73924
+ const id = /COD-\d+/.exec(line.text)?.[0];
73925
+ if (id)
73926
+ return { changeName: opts.name, identifier: id };
73927
+ }
73928
+ }
73782
73929
  return { changeName: opts.name, identifier: undefined };
73783
73930
  }
73784
73931
  if (opts.issue && !opts.name) {
73785
- for (const line of agentLines) {
73932
+ const pattern = new RegExp(`(cod-${opts.issue.toLowerCase().replace("cod-", "")}[\\w-]+)`);
73933
+ for (const line of allLines) {
73934
+ const m = pattern.exec(line.text);
73935
+ if (m)
73936
+ return { changeName: m[1], identifier: opts.issue };
73937
+ }
73938
+ for (const line of allLines) {
73786
73939
  const m = SPAWN_RE.exec(line.text);
73787
73940
  if (m && m[1] === opts.issue)
73788
73941
  return { changeName: m[2], identifier: opts.issue };
@@ -73852,34 +74005,33 @@ async function fetchGithubPr(changeName) {
73852
74005
  ]) ?? [];
73853
74006
  return { ...pr, checks };
73854
74007
  }
74008
+ function fetchMergeableNow(prUrl) {
74009
+ const result2 = Bun.spawnSync(["gh", "pr", "view", prUrl, "--json", "mergeable", "--jq", ".mergeable"], { stderr: "ignore" });
74010
+ return result2.exitCode === 0 ? result2.stdout.toString().trim() : null;
74011
+ }
73855
74012
  async function runDebug(opts) {
73856
74013
  const { projectRoot } = opts;
74014
+ const out = (s) => process.stdout.write(s + `
74015
+ `);
73857
74016
  const agentLogFile = Bun.file(AGENT_LOG_PATH);
73858
- const agentLogContent = await agentLogFile.exists() ? await agentLogFile.text() : "";
73859
- const agentLines = parseLog(agentLogContent);
73860
- let { changeName, identifier: issueIdentifier } = await resolveDebugTarget({
74017
+ const textLines = await agentLogFile.exists() ? parseTextLog(await agentLogFile.text()) : [];
74018
+ const jsonlLogPath = join20(projectRoot, ".ralph", "agent.log");
74019
+ const jsonlLogFile = Bun.file(jsonlLogPath);
74020
+ const hasJsonlLog = await jsonlLogFile.exists();
74021
+ let { changeName, identifier: issueIdentifier } = await resolveDebugTarget(projectRoot, {
73861
74022
  ...opts.name !== undefined ? { name: opts.name } : {},
73862
74023
  ...opts.issue !== undefined ? { issue: opts.issue } : {}
73863
74024
  });
73864
74025
  if (!changeName) {
73865
- process.stderr.write(`! Could not resolve a change name for ${opts.issue ?? opts.name}. Has this issue been started?
74026
+ process.stderr.write(`! Could not resolve change name for ${opts.issue ?? opts.name}.
73866
74027
  `);
73867
74028
  process.exit(1);
73868
74029
  }
73869
- const relevant = agentLines.filter((l) => l.text.includes(changeName) || issueIdentifier !== undefined && l.text.includes(issueIdentifier));
73870
- if (!issueIdentifier) {
73871
- for (const line of relevant) {
73872
- const m = SPAWN_RE.exec(line.text);
73873
- if (m && m[2] === changeName) {
73874
- issueIdentifier = m[1];
73875
- break;
73876
- }
73877
- }
73878
- }
73879
- const workerLogPath = join20(projectRoot, ".ralph", "logs", `${changeName}.log`);
73880
- const workerLogFile = Bun.file(workerLogPath);
73881
- const workerLines = await workerLogFile.exists() ? parseLog(await workerLogFile.text()) : [];
73882
- const merged = [...relevant, ...workerLines].sort((a, b) => +a.ts - +b.ts);
74030
+ const jsonlLines = hasJsonlLog ? parseJsonlLog(await jsonlLogFile.text(), changeName) : [];
74031
+ const relevantText = textLines.filter((l) => l.text.includes(changeName) || issueIdentifier !== undefined && l.text.includes(issueIdentifier));
74032
+ const workerLogFile = Bun.file(join20(projectRoot, ".ralph", "logs", `${changeName}.log`));
74033
+ const workerLines = await workerLogFile.exists() ? parseTextLog(await workerLogFile.text()) : [];
74034
+ const merged = [...relevantText, ...jsonlLines, ...workerLines].sort((a, b) => +a.ts - +b.ts);
73883
74035
  const seen = new Set;
73884
74036
  const timeline = merged.filter((l) => {
73885
74037
  const key = `${l.ts.getTime()}:${l.type}:${l.text}`;
@@ -73888,8 +74040,24 @@ async function runDebug(opts) {
73888
74040
  seen.add(key);
73889
74041
  return true;
73890
74042
  });
73891
- const out = (s) => process.stdout.write(s + `
73892
- `);
74043
+ if (!issueIdentifier) {
74044
+ for (const line of timeline) {
74045
+ const m = SPAWN_RE.exec(line.text);
74046
+ if (m && m[2] === changeName) {
74047
+ issueIdentifier = m[1];
74048
+ break;
74049
+ }
74050
+ if (line.text.includes(changeName)) {
74051
+ const id = /(COD|ENG|DEV)-\d+/.exec(line.text)?.[0];
74052
+ if (id) {
74053
+ issueIdentifier = id;
74054
+ break;
74055
+ }
74056
+ }
74057
+ }
74058
+ }
74059
+ const stuck = detectStuck(timeline);
74060
+ const binary = await inspectBinary(projectRoot);
73893
74061
  out(`
73894
74062
  === Ralph Debug: ${changeName}${issueIdentifier ? ` (${issueIdentifier})` : ""} ===
73895
74063
  `);
@@ -73897,12 +74065,30 @@ async function runDebug(opts) {
73897
74065
  if (!timeline.length) {
73898
74066
  out(" (no log entries found)");
73899
74067
  } else {
73900
- for (const line of timeline) {
73901
- const prefix = line.type === "output" ? " \u2502" : " \xB7";
73902
- out(`${prefix} ${fmtTs(line.ts)} [${line.type.padEnd(7)}] ${line.text}`);
74068
+ if (stuck && timeline.length > 20) {
74069
+ const phaseLines = timeline.filter((l) => l.type === "phase" && l.text.includes(stuck.phase));
74070
+ const nonPhase = timeline.filter((l) => !(l.type === "phase" && l.text.includes(stuck.phase)));
74071
+ for (const line of nonPhase) {
74072
+ const prefix = line.type === "output" ? " \u2502" : " \xB7";
74073
+ out(`${prefix} ${fmtTs(line.ts)} [${line.type.padEnd(7)}] ${line.text}`);
74074
+ }
74075
+ out(` \u21BA ... ${phaseLines.length}\xD7 ${stuck.phase} (${stuck.minutesStuck.toFixed(1)} min) ...`);
74076
+ } else {
74077
+ for (const line of timeline) {
74078
+ const prefix = line.type === "output" ? " \u2502" : " \xB7";
74079
+ out(`${prefix} ${fmtTs(line.ts)} [${line.type.padEnd(7)}] ${line.text}`);
74080
+ }
73903
74081
  }
73904
74082
  }
73905
74083
  out("");
74084
+ if (binary) {
74085
+ out("\u2500\u2500 Installed binary \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
74086
+ out(` Path : ${binary.path}`);
74087
+ out(` Embedded version : ${binary.embeddedVersion ?? "(unknown)"}`);
74088
+ if (binary.builtAt)
74089
+ out(` Built at : ${fmtTs(binary.builtAt)}`);
74090
+ out("");
74091
+ }
73906
74092
  out("\u2500\u2500 Current Linear state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
73907
74093
  if (!issueIdentifier) {
73908
74094
  out(" (unknown identifier \u2014 pass --issue to query Linear directly)");
@@ -73924,7 +74110,13 @@ async function runDebug(opts) {
73924
74110
  out("\u2500\u2500 Current GitHub PR \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
73925
74111
  const pr = await fetchGithubPr(changeName);
73926
74112
  if (!pr) {
73927
- out(` (no PR found for branch ralph/${changeName})`);
74113
+ if (stuck?.watchingPrUrl) {
74114
+ const m = fetchMergeableNow(stuck.watchingPrUrl);
74115
+ out(` Watching : ${stuck.watchingPrUrl}`);
74116
+ out(` Mergeable: ${m ?? "(error fetching)"}`);
74117
+ } else {
74118
+ out(` (no PR found for branch ralph/${changeName})`);
74119
+ }
73928
74120
  } else {
73929
74121
  const failing = pr.checks.filter((c) => c.conclusion === "FAILURE" || c.conclusion === "failure");
73930
74122
  const pending = pr.checks.filter((c) => c.state === "PENDING" || c.state === "IN_PROGRESS");
@@ -73946,32 +74138,58 @@ async function runDebug(opts) {
73946
74138
  const lastEvent = timeline.at(-1);
73947
74139
  if (lastEvent)
73948
74140
  out(` Last event : ${fmtTs(lastEvent.ts)} ${lastEvent.text}`);
73949
- const exitLine = relevant.find((l) => /exited \(code \d+\)/.test(l.text));
74141
+ const exitLine = timeline.find((l) => /exited \(code \d+\)/.test(l.text));
73950
74142
  if (exitLine) {
73951
74143
  const code = Number(/code (\d+)/.exec(exitLine.text)?.[1]);
73952
- const meaning = code === 0 ? "success" : code === 70 ? "CI fix loop exhausted its attempt budget" : code === 71 ? "push or PR creation failed (pre-push hook or remote rejection)" : "worker subprocess failed";
74144
+ const meaning = code === 0 ? "success" : code === 70 ? "CI fix loop exhausted its attempt budget" : code === 71 ? "push or PR creation failed" : "worker subprocess failed";
73953
74145
  out(` Exit code : ${code} \u2014 ${meaning}`);
73954
74146
  }
73955
- const logHas = (s) => relevant.some((l) => l.text.includes(s));
74147
+ if (stuck) {
74148
+ out(` \u26A0 STUCK in ${stuck.phase} \u2014 ${stuck.count} iterations over ${stuck.minutesStuck.toFixed(1)} min`);
74149
+ if (stuck.watchingPrUrl) {
74150
+ const mergeable = fetchMergeableNow(stuck.watchingPrUrl);
74151
+ out(` Watching : ${stuck.watchingPrUrl}`);
74152
+ out(` Mergeable : ${mergeable ?? "(error)"} (live fetch)`);
74153
+ if (mergeable === "MERGEABLE") {
74154
+ out(` \u2192 PR is MERGEABLE \u2014 loop should have exited. Likely cause:`);
74155
+ if (binary?.embeddedVersion && binary.embeddedVersion < "2.17.1") {
74156
+ out(` Local binary is v${binary.embeddedVersion} (fix shipped in v2.17.1). Update with:`);
74157
+ out(` cd ${projectRoot} && bunx @neriros/ralphy@latest make-install`);
74158
+ } else {
74159
+ out(` This is the conflict-check infinite loop bug (fixed in v2.17.1).`);
74160
+ out(` Restart the agent after updating to v2.17.1.`);
74161
+ }
74162
+ }
74163
+ }
74164
+ }
74165
+ if (binary) {
74166
+ const embV = binary.embeddedVersion ?? "?";
74167
+ const logV = timeline.find((l) => l.text.includes("agent started"))?.text.match(/v([\d.]+)/)?.[1];
74168
+ if (logV && embV !== logV) {
74169
+ out(` \u26A0 Version mismatch: binary says v${embV}, agent reported v${logV}`);
74170
+ out(` The binary reads version from a package.json at runtime \u2014 the actual`);
74171
+ out(` running code is v${embV}, not v${logV}. Update the local install.`);
74172
+ }
74173
+ }
74174
+ const logHas = (s) => timeline.some((l) => l.text.includes(s));
73956
74175
  if (logHas("setError applied"))
73957
74176
  out(" \u26A0 setError applied \u2014 issue is quarantined in Linear");
73958
74177
  if (logHas("setDone applied"))
73959
74178
  out(" \u2713 setDone applied \u2014 issue marked done in Linear");
73960
74179
  if (logHas("clearConflicted applied"))
73961
- out(" \u2713 clearConflicted applied \u2014 conflicts resolved");
74180
+ out(" \u2713 clearConflicted applied");
73962
74181
  if (logHas("setConflicted applied"))
73963
74182
  out(" \u26A0 setConflicted applied \u2014 merge conflicts detected");
73964
74183
  if (logHas("skipping PR phase"))
73965
74184
  out(" \u21A9 PR phase skipped \u2014 worker exited non-zero");
73966
74185
  if (pr?.mergeable === "CONFLICTING")
73967
74186
  out(" \u26A0 PR currently has merge conflicts");
73968
- if (pr?.checks.some((c) => c.conclusion === "FAILURE" || c.conclusion === "failure")) {
74187
+ if (pr?.checks.some((c) => c.conclusion === "FAILURE"))
73969
74188
  out(" \u26A0 PR has failing CI checks");
73970
- }
73971
74189
  const worktreePath = join20(projectRoot, ".ralph", "worktrees", changeName);
73972
- const worktreeExists = await Bun.file(join20(worktreePath, ".git")).exists();
73973
- if (worktreeExists)
74190
+ if (await Bun.file(join20(worktreePath, ".git")).exists()) {
73974
74191
  out(` Worktree : ${worktreePath}`);
74192
+ }
73975
74193
  if (!timeline.length)
73976
74194
  out(" (no log entries \u2014 has this change been started yet?)");
73977
74195
  out("");
@@ -74041,7 +74259,7 @@ try {
74041
74259
  `);
74042
74260
  process.exit(1);
74043
74261
  }
74044
- await runDebug({ name: args.name, projectRoot });
74262
+ await runDebug({ name: args.name, projectRoot: args.projectRoot ?? projectRoot });
74045
74263
  await shutdown();
74046
74264
  process.exit(0);
74047
74265
  }
package/dist/mcp/index.js CHANGED
@@ -3047,10 +3047,13 @@ var require_data = __commonJS((exports, module) => {
3047
3047
  };
3048
3048
  });
3049
3049
 
3050
- // node_modules/.bun/fast-uri@3.1.0/node_modules/fast-uri/lib/utils.js
3050
+ // node_modules/.bun/fast-uri@3.1.2/node_modules/fast-uri/lib/utils.js
3051
3051
  var require_utils = __commonJS((exports, module) => {
3052
3052
  var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu);
3053
3053
  var isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u);
3054
+ var isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu);
3055
+ var isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu);
3056
+ var isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu);
3054
3057
  function stringArrayToHexStripped(input) {
3055
3058
  let acc = "";
3056
3059
  let code = 0;
@@ -3244,27 +3247,77 @@ var require_utils = __commonJS((exports, module) => {
3244
3247
  }
3245
3248
  return output.join("");
3246
3249
  }
3247
- function normalizeComponentEncoding(component, esc2) {
3248
- const func = esc2 !== true ? escape : unescape;
3249
- if (component.scheme !== undefined) {
3250
- component.scheme = func(component.scheme);
3251
- }
3252
- if (component.userinfo !== undefined) {
3253
- component.userinfo = func(component.userinfo);
3254
- }
3255
- if (component.host !== undefined) {
3256
- component.host = func(component.host);
3250
+ var HOST_DELIMS = { "@": "%40", "/": "%2F", "?": "%3F", "#": "%23", ":": "%3A" };
3251
+ var HOST_DELIM_RE = /[@/?#:]/g;
3252
+ var HOST_DELIM_NO_COLON_RE = /[@/?#]/g;
3253
+ function reescapeHostDelimiters(host, isIP) {
3254
+ const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
3255
+ re.lastIndex = 0;
3256
+ return host.replace(re, (ch) => HOST_DELIMS[ch]);
3257
+ }
3258
+ function normalizePercentEncoding(input, decodeUnreserved = false) {
3259
+ if (input.indexOf("%") === -1) {
3260
+ return input;
3257
3261
  }
3258
- if (component.path !== undefined) {
3259
- component.path = func(component.path);
3262
+ let output = "";
3263
+ for (let i = 0;i < input.length; i++) {
3264
+ if (input[i] === "%" && i + 2 < input.length) {
3265
+ const hex = input.slice(i + 1, i + 3);
3266
+ if (isHexPair(hex)) {
3267
+ const normalizedHex = hex.toUpperCase();
3268
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
3269
+ if (decodeUnreserved && isUnreserved(decoded)) {
3270
+ output += decoded;
3271
+ } else {
3272
+ output += "%" + normalizedHex;
3273
+ }
3274
+ i += 2;
3275
+ continue;
3276
+ }
3277
+ }
3278
+ output += input[i];
3260
3279
  }
3261
- if (component.query !== undefined) {
3262
- component.query = func(component.query);
3280
+ return output;
3281
+ }
3282
+ function normalizePathEncoding(input) {
3283
+ let output = "";
3284
+ for (let i = 0;i < input.length; i++) {
3285
+ if (input[i] === "%" && i + 2 < input.length) {
3286
+ const hex = input.slice(i + 1, i + 3);
3287
+ if (isHexPair(hex)) {
3288
+ const normalizedHex = hex.toUpperCase();
3289
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
3290
+ if (decoded !== "." && isUnreserved(decoded)) {
3291
+ output += decoded;
3292
+ } else {
3293
+ output += "%" + normalizedHex;
3294
+ }
3295
+ i += 2;
3296
+ continue;
3297
+ }
3298
+ }
3299
+ if (isPathCharacter(input[i])) {
3300
+ output += input[i];
3301
+ } else {
3302
+ output += escape(input[i]);
3303
+ }
3263
3304
  }
3264
- if (component.fragment !== undefined) {
3265
- component.fragment = func(component.fragment);
3305
+ return output;
3306
+ }
3307
+ function escapePreservingEscapes(input) {
3308
+ let output = "";
3309
+ for (let i = 0;i < input.length; i++) {
3310
+ if (input[i] === "%" && i + 2 < input.length) {
3311
+ const hex = input.slice(i + 1, i + 3);
3312
+ if (isHexPair(hex)) {
3313
+ output += "%" + hex.toUpperCase();
3314
+ i += 2;
3315
+ continue;
3316
+ }
3317
+ }
3318
+ output += escape(input[i]);
3266
3319
  }
3267
- return component;
3320
+ return output;
3268
3321
  }
3269
3322
  function recomposeAuthority(component) {
3270
3323
  const uriTokens = [];
@@ -3279,7 +3332,7 @@ var require_utils = __commonJS((exports, module) => {
3279
3332
  if (ipV6res.isIPV6 === true) {
3280
3333
  host = `[${ipV6res.escapedHost}]`;
3281
3334
  } else {
3282
- host = component.host;
3335
+ host = reescapeHostDelimiters(host, false);
3283
3336
  }
3284
3337
  }
3285
3338
  uriTokens.push(host);
@@ -3293,7 +3346,10 @@ var require_utils = __commonJS((exports, module) => {
3293
3346
  module.exports = {
3294
3347
  nonSimpleDomain,
3295
3348
  recomposeAuthority,
3296
- normalizeComponentEncoding,
3349
+ reescapeHostDelimiters,
3350
+ normalizePercentEncoding,
3351
+ normalizePathEncoding,
3352
+ escapePreservingEscapes,
3297
3353
  removeDotSegments,
3298
3354
  isIPv4,
3299
3355
  isUUID,
@@ -3302,7 +3358,7 @@ var require_utils = __commonJS((exports, module) => {
3302
3358
  };
3303
3359
  });
3304
3360
 
3305
- // node_modules/.bun/fast-uri@3.1.0/node_modules/fast-uri/lib/schemes.js
3361
+ // node_modules/.bun/fast-uri@3.1.2/node_modules/fast-uri/lib/schemes.js
3306
3362
  var require_schemes = __commonJS((exports, module) => {
3307
3363
  var { isUUID } = require_utils();
3308
3364
  var URN_REG = /([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-.:;=@]|%[\da-f]{2})+)/iu;
@@ -3476,13 +3532,13 @@ var require_schemes = __commonJS((exports, module) => {
3476
3532
  };
3477
3533
  });
3478
3534
 
3479
- // node_modules/.bun/fast-uri@3.1.0/node_modules/fast-uri/index.js
3535
+ // node_modules/.bun/fast-uri@3.1.2/node_modules/fast-uri/index.js
3480
3536
  var require_fast_uri = __commonJS((exports, module) => {
3481
- var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizeComponentEncoding, isIPv4, nonSimpleDomain } = require_utils();
3537
+ var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
3482
3538
  var { SCHEMES, getSchemeHandler } = require_schemes();
3483
3539
  function normalize(uri, options) {
3484
3540
  if (typeof uri === "string") {
3485
- uri = serialize(parse6(uri, options), options);
3541
+ uri = normalizeString(uri, options);
3486
3542
  } else if (typeof uri === "object") {
3487
3543
  uri = parse6(serialize(uri, options), options);
3488
3544
  }
@@ -3548,19 +3604,9 @@ var require_fast_uri = __commonJS((exports, module) => {
3548
3604
  return target;
3549
3605
  }
3550
3606
  function equal(uriA, uriB, options) {
3551
- if (typeof uriA === "string") {
3552
- uriA = unescape(uriA);
3553
- uriA = serialize(normalizeComponentEncoding(parse6(uriA, options), true), { ...options, skipEscape: true });
3554
- } else if (typeof uriA === "object") {
3555
- uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true });
3556
- }
3557
- if (typeof uriB === "string") {
3558
- uriB = unescape(uriB);
3559
- uriB = serialize(normalizeComponentEncoding(parse6(uriB, options), true), { ...options, skipEscape: true });
3560
- } else if (typeof uriB === "object") {
3561
- uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true });
3562
- }
3563
- return uriA.toLowerCase() === uriB.toLowerCase();
3607
+ const normalizedA = normalizeComparableURI(uriA, options);
3608
+ const normalizedB = normalizeComparableURI(uriB, options);
3609
+ return normalizedA !== undefined && normalizedB !== undefined && normalizedA.toLowerCase() === normalizedB.toLowerCase();
3564
3610
  }
3565
3611
  function serialize(cmpts, opts) {
3566
3612
  const component = {
@@ -3586,12 +3632,12 @@ var require_fast_uri = __commonJS((exports, module) => {
3586
3632
  schemeHandler.serialize(component, options);
3587
3633
  if (component.path !== undefined) {
3588
3634
  if (!options.skipEscape) {
3589
- component.path = escape(component.path);
3635
+ component.path = escapePreservingEscapes(component.path);
3590
3636
  if (component.scheme !== undefined) {
3591
3637
  component.path = component.path.split("%3A").join(":");
3592
3638
  }
3593
3639
  } else {
3594
- component.path = unescape(component.path);
3640
+ component.path = normalizePercentEncoding(component.path);
3595
3641
  }
3596
3642
  }
3597
3643
  if (options.reference !== "suffix" && component.scheme) {
@@ -3626,7 +3672,16 @@ var require_fast_uri = __commonJS((exports, module) => {
3626
3672
  return uriTokens.join("");
3627
3673
  }
3628
3674
  var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
3629
- function parse6(uri, opts) {
3675
+ function getParseError(parsed, matches) {
3676
+ if (matches[2] !== undefined && parsed.path && parsed.path[0] !== "/") {
3677
+ return 'URI path must start with "/" when authority is present.';
3678
+ }
3679
+ if (typeof parsed.port === "number" && (parsed.port < 0 || parsed.port > 65535)) {
3680
+ return "URI port is malformed.";
3681
+ }
3682
+ return;
3683
+ }
3684
+ function parseWithStatus(uri, opts) {
3630
3685
  const options = Object.assign({}, opts);
3631
3686
  const parsed = {
3632
3687
  scheme: undefined,
@@ -3637,6 +3692,7 @@ var require_fast_uri = __commonJS((exports, module) => {
3637
3692
  query: undefined,
3638
3693
  fragment: undefined
3639
3694
  };
3695
+ let malformedAuthorityOrPort = false;
3640
3696
  let isIP = false;
3641
3697
  if (options.reference === "suffix") {
3642
3698
  if (options.scheme) {
@@ -3657,6 +3713,11 @@ var require_fast_uri = __commonJS((exports, module) => {
3657
3713
  if (isNaN(parsed.port)) {
3658
3714
  parsed.port = matches[5];
3659
3715
  }
3716
+ const parseError = getParseError(parsed, matches);
3717
+ if (parseError !== undefined) {
3718
+ parsed.error = parsed.error || parseError;
3719
+ malformedAuthorityOrPort = true;
3720
+ }
3660
3721
  if (parsed.host) {
3661
3722
  const ipv4result = isIPv4(parsed.host);
3662
3723
  if (ipv4result === false) {
@@ -3695,14 +3756,18 @@ var require_fast_uri = __commonJS((exports, module) => {
3695
3756
  parsed.scheme = unescape(parsed.scheme);
3696
3757
  }
3697
3758
  if (parsed.host !== undefined) {
3698
- parsed.host = unescape(parsed.host);
3759
+ parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP);
3699
3760
  }
3700
3761
  }
3701
3762
  if (parsed.path) {
3702
- parsed.path = escape(unescape(parsed.path));
3763
+ parsed.path = normalizePathEncoding(parsed.path);
3703
3764
  }
3704
3765
  if (parsed.fragment) {
3705
- parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
3766
+ try {
3767
+ parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
3768
+ } catch {
3769
+ parsed.error = parsed.error || "URI malformed";
3770
+ }
3706
3771
  }
3707
3772
  }
3708
3773
  if (schemeHandler && schemeHandler.parse) {
@@ -3711,7 +3776,29 @@ var require_fast_uri = __commonJS((exports, module) => {
3711
3776
  } else {
3712
3777
  parsed.error = parsed.error || "URI can not be parsed.";
3713
3778
  }
3714
- return parsed;
3779
+ return { parsed, malformedAuthorityOrPort };
3780
+ }
3781
+ function parse6(uri, opts) {
3782
+ return parseWithStatus(uri, opts).parsed;
3783
+ }
3784
+ function normalizeString(uri, opts) {
3785
+ return normalizeStringWithStatus(uri, opts).normalized;
3786
+ }
3787
+ function normalizeStringWithStatus(uri, opts) {
3788
+ const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts);
3789
+ return {
3790
+ normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
3791
+ malformedAuthorityOrPort
3792
+ };
3793
+ }
3794
+ function normalizeComparableURI(uri, opts) {
3795
+ if (typeof uri === "string") {
3796
+ const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts);
3797
+ return malformedAuthorityOrPort ? undefined : normalized;
3798
+ }
3799
+ if (typeof uri === "object") {
3800
+ return serialize(uri, opts);
3801
+ }
3715
3802
  }
3716
3803
  var fastUri = {
3717
3804
  SCHEMES,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neriros/ralphy",
3
- "version": "2.17.1",
3
+ "version": "2.17.3",
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",
@@ -57,33 +57,35 @@
57
57
  "prepublishOnly": "bun run build:publish"
58
58
  },
59
59
  "devDependencies": {
60
- "@commitlint/cli": "^20.4.3",
61
- "@commitlint/config-conventional": "^20.4.3",
60
+ "@commitlint/cli": "^20.5.3",
61
+ "@commitlint/config-conventional": "^20.5.3",
62
62
  "@fission-ai/openspec": "latest",
63
- "@modelcontextprotocol/sdk": "^1.12.0",
63
+ "@modelcontextprotocol/sdk": "^1.29.0",
64
64
  "@nx/devkit": "^22.7.1",
65
65
  "@nx/js": "^22.7.1",
66
- "@secretlint/secretlint-rule-preset-recommend": "^11.3.1",
66
+ "@secretlint/secretlint-rule-preset-recommend": "^11.7.1",
67
67
  "@swc-node/register": "^1.11.1",
68
- "@swc/core": "^1.15.18",
68
+ "@swc/core": "^1.15.33",
69
69
  "@total-typescript/ts-reset": "^0.6.1",
70
- "@types/node": "^22.0.0",
71
- "bun-types": "^1.3.0",
72
- "chalk": "^5.4.0",
73
- "dependency-cruiser": "^17.3.8",
70
+ "@types/node": "^22.19.18",
71
+ "bun-types": "^1.3.13",
72
+ "chalk": "^5.6.2",
73
+ "dependency-cruiser": "^17.4.0",
74
74
  "husky": "^9.1.7",
75
- "knip": "^5.85.0",
76
- "lint-staged": "^16.3.2",
75
+ "knip": "^5.88.1",
76
+ "lint-staged": "^16.4.0",
77
77
  "nx": "22.5.3",
78
78
  "oxc-parser": "^0.126.0",
79
79
  "oxfmt": "^0.36.0",
80
- "oxlint": "^1.51.0",
81
- "secretlint": "^11.3.1",
82
- "typescript": "^5.8.0",
83
- "zod": "^3.24.0"
80
+ "oxlint": "^1.63.0",
81
+ "secretlint": "^11.7.1",
82
+ "typescript": "^5.9.3",
83
+ "zod": "^3.25.76"
84
84
  },
85
85
  "overrides": {
86
+ "@babel/plugin-transform-modules-systemjs": "^7.29.4",
86
87
  "axios": "^1.15.1",
88
+ "fast-uri": "^3.1.2",
87
89
  "minimatch": "^10.2.3"
88
90
  },
89
91
  "engines": {