@neriros/ralphy 2.17.2 → 2.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -173,6 +173,7 @@ A default `ralphy.config.json` is written on first run with every defaulted sett
173
173
  "getTodo": { "filter": [{ "type": "status", "value": "Todo" }] },
174
174
  "getInProgress": { "filter": [{ "type": "status", "value": "In Progress" }] },
175
175
  "getConflicted": { "filter": [{ "type": "label", "value": "ralph:conflicted" }] },
176
+ "getReview": { "filter": [{ "type": "label", "value": "ralph:review" }] },
176
177
  "setInProgress": { "type": "status", "value": "In Progress" },
177
178
  "setDone": {
178
179
  "apply": [
@@ -183,6 +184,7 @@ A default `ralphy.config.json` is written on first run with every defaulted sett
183
184
  "setError": { "type": "label", "value": "ralph:error" },
184
185
  "setConflicted": { "type": "label", "value": "ralph:conflicted" },
185
186
  "clearConflicted": { "type": "label", "value": "ralph:conflicted" },
187
+ "clearReview": { "type": "label", "value": "ralph:review" },
186
188
  },
187
189
  },
188
190
  "useWorktree": true,
@@ -205,9 +207,11 @@ A default `ralphy.config.json` is written on first run with every defaulted sett
205
207
 
206
208
  Linear is the source of truth for which issues Ralph has touched. Each `linear.indicators` key names a lifecycle event:
207
209
 
208
- - `getTodo` / `getInProgress` / `getConflicted` — `{ filter: [...] }` selectors used to find issues to pick up, resume, or repair.
210
+ - `getTodo` / `getInProgress` / `getConflicted` / `getReview` — `{ filter: [...] }` selectors used to find issues to pick up, resume, repair, or follow up on after review.
209
211
  - `setInProgress` / `setDone` / `setError` / `setConflicted` — single marker `{ type, value }` or `{ apply: [...] }` for multi-marker.
210
- - `clearConflicted` — labels to remove once a conflicted PR is fixed (status removal is not supported).
212
+ - `clearConflicted` / `clearReview` — labels to remove once a conflicted PR is fixed or a review-mode issue is picked back up (status removal is not supported).
213
+
214
+ **Review follow-ups.** When a Linear issue is in a "done" state and a reviewer adds the `getReview` marker (typically a label like `ralph:review` after leaving comments), Ralph picks it up, applies `setInProgress`, removes the `clearReview` label so the same trigger doesn't re-fire, fetches the comment thread, filters out Ralph's own comments, and prepends those reviewer comments as a new task at the top of `tasks.md`. The worker addresses them in the same change branch and `setDone` is re-applied on success.
211
215
 
212
216
  Marker types are `"label"` or `"status"`. Combine markers under `apply` when one event needs to set multiple — e.g. `setDone` flipping a status _and_ adding a "shipped" label.
213
217
 
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.2")
35033
- return "2.17.2";
35032
+ if ("2.18.0")
35033
+ return "2.18.0";
35034
35034
  } catch {}
35035
35035
  const dirsToTry = [];
35036
35036
  try {
@@ -35356,13 +35356,20 @@ var init_cli = __esm(() => {
35356
35356
  "getTodo",
35357
35357
  "getInProgress",
35358
35358
  "getConflicted",
35359
+ "getReview",
35359
35360
  "setInProgress",
35360
35361
  "setDone",
35361
35362
  "setError",
35362
35363
  "setConflicted",
35363
- "clearConflicted"
35364
+ "clearConflicted",
35365
+ "clearReview"
35366
+ ]);
35367
+ GET_KEYS = new Set([
35368
+ "getTodo",
35369
+ "getInProgress",
35370
+ "getConflicted",
35371
+ "getReview"
35364
35372
  ]);
35365
- GET_KEYS = new Set(["getTodo", "getInProgress", "getConflicted"]);
35366
35373
  HELP_TEXT = [
35367
35374
  `ralph v${VERSION}`,
35368
35375
  "",
@@ -35404,8 +35411,9 @@ var init_cli = __esm(() => {
35404
35411
  " --indicator getTodo:status:Todo",
35405
35412
  " --indicator setDone:label:shipped",
35406
35413
  " --indicator setDone:status:Done (combined with above \u2192 multi-marker)",
35407
- " Keys: getTodo, getInProgress, getConflicted,",
35408
- " setInProgress, setDone, setError, setConflicted, clearConflicted",
35414
+ " Keys: getTodo, getInProgress, getConflicted, getReview,",
35415
+ " setInProgress, setDone, setError, setConflicted,",
35416
+ " clearConflicted, clearReview",
35409
35417
  " Types: label, status",
35410
35418
  " --create-pr Push the worker branch and open a GitHub PR on success (needs --worktree)",
35411
35419
  " --fix-ci After opening the PR, re-run on CI failures until green (needs --create-pr)",
@@ -39468,6 +39476,99 @@ var init_types2 = __esm(() => {
39468
39476
  });
39469
39477
  });
39470
39478
 
39479
+ // apps/cli/src/agent/worktree.ts
39480
+ import { basename, join as join3 } from "path";
39481
+ import { homedir } from "os";
39482
+ import { exists } from "fs/promises";
39483
+ function worktreesDir(projectRoot) {
39484
+ return join3(homedir(), ".ralph", basename(projectRoot), "worktrees");
39485
+ }
39486
+ function branchForChange(changeName) {
39487
+ return `ralph/${changeName}`;
39488
+ }
39489
+ async function createWorktree(projectRoot, changeName, runner) {
39490
+ const dir = worktreesDir(projectRoot);
39491
+ const cwd2 = join3(dir, changeName);
39492
+ const branch = branchForChange(changeName);
39493
+ const list = await runner.run(["worktree", "list", "--porcelain"], projectRoot);
39494
+ if (list.stdout.includes(`worktree ${cwd2}
39495
+ `)) {
39496
+ return { cwd: cwd2, branch };
39497
+ }
39498
+ let branchExists = true;
39499
+ try {
39500
+ await runner.run(["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], projectRoot);
39501
+ } catch {
39502
+ branchExists = false;
39503
+ }
39504
+ const cmd = branchExists ? ["worktree", "add", cwd2, branch] : ["worktree", "add", "-b", branch, cwd2];
39505
+ await runner.run(cmd, projectRoot);
39506
+ return { cwd: cwd2, branch };
39507
+ }
39508
+ async function removeWorktree(projectRoot, cwd2, runner) {
39509
+ await runner.run(["worktree", "remove", "--force", cwd2], projectRoot);
39510
+ }
39511
+ async function isWorktreeSafeToRemove(cwd2, base2, runner) {
39512
+ const status = await runner.run(["status", "--porcelain"], cwd2);
39513
+ const dirty = status.stdout.trim();
39514
+ let unpushedCommits = "";
39515
+ try {
39516
+ const log2 = await runner.run(["log", "--oneline", `${base2}..HEAD`, "--no-merges"], cwd2);
39517
+ unpushedCommits = log2.stdout.trim();
39518
+ } catch {
39519
+ unpushedCommits = "<unknown: failed to compare against base>";
39520
+ }
39521
+ if (dirty && unpushedCommits) {
39522
+ return {
39523
+ safe: false,
39524
+ reason: "uncommitted changes AND unpushed commits present",
39525
+ dirty,
39526
+ unpushedCommits
39527
+ };
39528
+ }
39529
+ if (dirty) {
39530
+ return {
39531
+ safe: false,
39532
+ reason: "uncommitted or untracked files present",
39533
+ dirty,
39534
+ unpushedCommits
39535
+ };
39536
+ }
39537
+ if (unpushedCommits) {
39538
+ return {
39539
+ safe: false,
39540
+ reason: `commits ahead of ${base2} were not pushed/PR'd`,
39541
+ dirty,
39542
+ unpushedCommits
39543
+ };
39544
+ }
39545
+ return { safe: true, dirty, unpushedCommits };
39546
+ }
39547
+ async function seedWorktreeMcpConfig(projectRoot, worktreeCwd) {
39548
+ const dst = join3(worktreeCwd, ".mcp.json");
39549
+ const src = join3(projectRoot, ".mcp.json");
39550
+ const source = await exists(dst) ? dst : await exists(src) ? src : null;
39551
+ if (!source)
39552
+ return;
39553
+ let parsed;
39554
+ try {
39555
+ parsed = await Bun.file(source).json();
39556
+ } catch {
39557
+ return;
39558
+ }
39559
+ const servers = parsed.mcpServers;
39560
+ if (servers && typeof servers === "object") {
39561
+ for (const cfg of Object.values(servers)) {
39562
+ if (Array.isArray(cfg.args)) {
39563
+ cfg.args = cfg.args.map((a) => typeof a === "string" && a.startsWith(".ralph/") ? join3(projectRoot, a) : a);
39564
+ }
39565
+ }
39566
+ }
39567
+ await Bun.write(dst, JSON.stringify(parsed, null, 2) + `
39568
+ `);
39569
+ }
39570
+ var init_worktree = () => {};
39571
+
39471
39572
  // node_modules/.bun/react@18.3.1/node_modules/react/cjs/react-jsx-dev-runtime.development.js
39472
39573
  var require_react_jsx_dev_runtime_development = __commonJS((exports) => {
39473
39574
  var React10 = __toESM(require_react());
@@ -59333,8 +59434,8 @@ var init_node = __esm(() => {
59333
59434
  });
59334
59435
 
59335
59436
  // packages/telemetry/src/index.ts
59336
- import { homedir } from "os";
59337
- import { join as join6 } from "path";
59437
+ import { homedir as homedir2 } from "os";
59438
+ import { join as join7 } from "path";
59338
59439
  import { randomUUID } from "crypto";
59339
59440
  function setDefaultProperties(props) {
59340
59441
  defaultProps = { ...defaultProps, ...props };
@@ -59342,7 +59443,7 @@ function setDefaultProperties(props) {
59342
59443
  async function init() {
59343
59444
  if (!enabled)
59344
59445
  return;
59345
- const idPath = join6(homedir(), ".ralph", ".telemetry-id");
59446
+ const idPath = join7(homedir2(), ".ralph", ".telemetry-id");
59346
59447
  const idFile = Bun.file(idPath);
59347
59448
  if (await idFile.exists()) {
59348
59449
  distinctId = (await idFile.text()).trim();
@@ -59417,12 +59518,12 @@ ${fence}`;
59417
59518
  }
59418
59519
 
59419
59520
  // apps/cli/src/agent/config.ts
59420
- import { join as join10 } from "path";
59521
+ import { join as join11 } from "path";
59421
59522
  function stripJsonComments(text) {
59422
59523
  return text.replace(/\/\/[^\n]*/g, "");
59423
59524
  }
59424
59525
  async function loadRalphyConfig(projectRoot) {
59425
- const path = join10(projectRoot, "ralphy.config.json");
59526
+ const path = join11(projectRoot, "ralphy.config.json");
59426
59527
  const file = Bun.file(path);
59427
59528
  if (!await file.exists()) {
59428
59529
  return RalphyConfigSchema.parse({});
@@ -59432,7 +59533,7 @@ async function loadRalphyConfig(projectRoot) {
59432
59533
  return RalphyConfigSchema.parse(raw);
59433
59534
  }
59434
59535
  async function ensureRalphyConfig(projectRoot) {
59435
- const path = join10(projectRoot, "ralphy.config.json");
59536
+ const path = join11(projectRoot, "ralphy.config.json");
59436
59537
  const file = Bun.file(path);
59437
59538
  if (await file.exists())
59438
59539
  return path;
@@ -59533,6 +59634,10 @@ var MarkerSchema, GetIndicatorSchema, SetIndicatorSchema, IndicatorsSchema, Ralp
59533
59634
  // Issues whose PR has a merge conflict (Ralph will attempt a re-fix run).
59534
59635
  // "getConflicted": { "filter": [{ "type": "label", "value": "ralph:conflict" }] },
59535
59636
 
59637
+ // Done issues with new review comments to address (Ralph will re-open
59638
+ // and prepend a task that ingests the non-Ralph comments).
59639
+ // "getReview": { "filter": [{ "type": "label", "value": "ralph:review" }] },
59640
+
59536
59641
  // Applied when Ralph picks up an issue.
59537
59642
  // "setInProgress": { "type": "label", "value": "ralph:in-progress" },
59538
59643
 
@@ -59546,7 +59651,10 @@ var MarkerSchema, GetIndicatorSchema, SetIndicatorSchema, IndicatorsSchema, Ralp
59546
59651
  // "setConflicted": { "type": "label", "value": "ralph:conflict" },
59547
59652
 
59548
59653
  // Label removed once the conflict is fixed (status removal is not supported here).
59549
- // "clearConflicted": { "type": "label", "value": "ralph:conflict" }
59654
+ // "clearConflicted": { "type": "label", "value": "ralph:conflict" },
59655
+
59656
+ // Label removed when Ralph picks up a review-mode issue (status removal not supported).
59657
+ // "clearReview": { "type": "label", "value": "ralph:review" }
59550
59658
  }
59551
59659
  }
59552
59660
  }
@@ -59568,24 +59676,28 @@ var init_config = __esm(() => {
59568
59676
  getTodo: GetIndicatorSchema.optional(),
59569
59677
  getInProgress: GetIndicatorSchema.optional(),
59570
59678
  getConflicted: GetIndicatorSchema.optional(),
59679
+ getReview: GetIndicatorSchema.optional(),
59571
59680
  setInProgress: SetIndicatorSchema.optional(),
59572
59681
  setDone: SetIndicatorSchema.optional(),
59573
59682
  setError: SetIndicatorSchema.optional(),
59574
59683
  setConflicted: SetIndicatorSchema.optional(),
59575
- clearConflicted: SetIndicatorSchema.optional()
59684
+ clearConflicted: SetIndicatorSchema.optional(),
59685
+ clearReview: SetIndicatorSchema.optional()
59576
59686
  }).superRefine((value, ctx) => {
59577
- const clear = value.clearConflicted;
59578
- if (!clear)
59579
- return;
59580
- const markers = "apply" in clear ? clear.apply : [clear];
59581
- for (const m of markers) {
59582
- if (m.type !== "label") {
59583
- ctx.addIssue({
59584
- code: exports_external.ZodIssueCode.custom,
59585
- path: ["clearConflicted"],
59586
- message: "clearConflicted markers must be label-typed (status removal is not supported)"
59587
- });
59588
- return;
59687
+ for (const key of ["clearConflicted", "clearReview"]) {
59688
+ const clear = value[key];
59689
+ if (!clear)
59690
+ continue;
59691
+ const markers = "apply" in clear ? clear.apply : [clear];
59692
+ for (const m of markers) {
59693
+ if (m.type !== "label") {
59694
+ ctx.addIssue({
59695
+ code: exports_external.ZodIssueCode.custom,
59696
+ path: [key],
59697
+ message: `${key} markers must be label-typed (status removal is not supported)`
59698
+ });
59699
+ break;
59700
+ }
59589
59701
  }
59590
59702
  }
59591
59703
  });
@@ -59634,8 +59746,8 @@ var init_config = __esm(() => {
59634
59746
 
59635
59747
  // packages/log/src/log.ts
59636
59748
  import { appendFile } from "fs/promises";
59637
- import { join as join11, dirname as dirname4 } from "path";
59638
- import { homedir as homedir2 } from "os";
59749
+ import { join as join12, dirname as dirname4 } from "path";
59750
+ import { homedir as homedir3 } from "os";
59639
59751
  import { mkdir as mkdir2 } from "fs/promises";
59640
59752
  function fmt(type, text) {
59641
59753
  return `[${new Date().toISOString()}] [${type}] ${text}
@@ -59680,25 +59792,25 @@ async function initWorkerLog(logFile) {
59680
59792
  var ANSI_RE, AGENT_LOG_PATH;
59681
59793
  var init_log = __esm(() => {
59682
59794
  ANSI_RE = /\x1b(?:\[[0-9;]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|.)/g;
59683
- AGENT_LOG_PATH = join11(homedir2(), ".ralph", "agent-mode.log");
59795
+ AGENT_LOG_PATH = join12(homedir3(), ".ralph", "agent-mode.log");
59684
59796
  mkdir2(dirname4(AGENT_LOG_PATH), { recursive: true }).catch(() => {
59685
59797
  return;
59686
59798
  });
59687
59799
  });
59688
59800
 
59689
59801
  // packages/core/src/layout.ts
59690
- import { join as join12 } from "path";
59802
+ import { join as join13 } from "path";
59691
59803
  function projectLayout(root) {
59692
- const statesDir = join12(root, ".ralph", "tasks");
59693
- const tasksDir = join12(root, "openspec", "changes");
59804
+ const statesDir = join13(root, ".ralph", "tasks");
59805
+ const tasksDir = join13(root, "openspec", "changes");
59694
59806
  return {
59695
59807
  root,
59696
59808
  statesDir,
59697
59809
  tasksDir,
59698
- agentStateFile: join12(root, ".ralph", "agent-state.json"),
59699
- changeDir: (name) => join12(tasksDir, name),
59700
- taskStateDir: (name) => join12(statesDir, name),
59701
- stateFile: (name) => join12(statesDir, name, STATE_FILE2)
59810
+ agentStateFile: join13(root, ".ralph", "agent-state.json"),
59811
+ changeDir: (name) => join13(tasksDir, name),
59812
+ taskStateDir: (name) => join13(statesDir, name),
59813
+ stateFile: (name) => join13(statesDir, name, STATE_FILE2)
59702
59814
  };
59703
59815
  }
59704
59816
  var STATE_FILE2 = ".ralph-state.json";
@@ -59753,7 +59865,15 @@ function buildIssueFilter(spec) {
59753
59865
  where.and = [{ state: current }, noStatus];
59754
59866
  }
59755
59867
  if (labels.length > 0) {
59756
- where.labels = { ...where.labels, every: { name: { nin: labels } } };
59868
+ const includeLabels = where.labels;
59869
+ const excludeLabels = { every: { name: { nin: labels } } };
59870
+ if (includeLabels === undefined) {
59871
+ where.labels = excludeLabels;
59872
+ } else {
59873
+ const existingAnd = where.and ?? [];
59874
+ where.and = [...existingAnd, { labels: includeLabels }, { labels: excludeLabels }];
59875
+ delete where.labels;
59876
+ }
59757
59877
  }
59758
59878
  }
59759
59879
  return where;
@@ -59949,19 +60069,21 @@ class AgentCoordinator {
59949
60069
  let todo = [];
59950
60070
  let inProgress = [];
59951
60071
  let conflicted = [];
60072
+ let review = [];
59952
60073
  try {
59953
- [todo, inProgress, conflicted] = await Promise.all([
60074
+ [todo, inProgress, conflicted, review] = await Promise.all([
59954
60075
  this.deps.fetchTodo(),
59955
60076
  this.deps.fetchInProgress(),
59956
- this.deps.fetchConflicted()
60077
+ this.deps.fetchConflicted(),
60078
+ this.deps.fetchReview()
59957
60079
  ]);
59958
60080
  } catch (err) {
59959
60081
  this.deps.onLog(`! Linear poll failed: ${err.message}`, "red");
59960
60082
  capture("agent_linear_poll_failed", { error: err.message });
59961
60083
  return { found: 0, added: 0 };
59962
60084
  }
59963
- if (todo.length + inProgress.length + conflicted.length > 0) {
59964
- this.deps.onLog(` poll: ${todo.length} todo, ${inProgress.length} in-progress, ${conflicted.length} conflicted`, "gray");
60085
+ if (todo.length + inProgress.length + conflicted.length + review.length > 0) {
60086
+ this.deps.onLog(` poll: ${todo.length} todo, ${inProgress.length} in-progress, ${conflicted.length} conflicted, ${review.length} review`, "gray");
59965
60087
  }
59966
60088
  const queuedIds = new Set(this.queue.map((q) => q.issue.id));
59967
60089
  const activeIds = new Set(this.workers.map((w) => w.issueId));
@@ -59996,6 +60118,16 @@ class AgentCoordinator {
59996
60118
  added += 1;
59997
60119
  this.deps.onLog(` \u21B3 ${issue.identifier} queued (conflict-fix)`, "gray");
59998
60120
  }
60121
+ for (const issue of review) {
60122
+ if (atTicketLimit())
60123
+ break;
60124
+ if (!eligible(issue.id))
60125
+ continue;
60126
+ this.queue.push({ issue, mode: "review" });
60127
+ queuedIds.add(issue.id);
60128
+ added += 1;
60129
+ this.deps.onLog(` \u21B3 ${issue.identifier} queued (review)`, "gray");
60130
+ }
59999
60131
  for (const issue of todo) {
60000
60132
  if (atTicketLimit())
60001
60133
  break;
@@ -60012,7 +60144,8 @@ class AgentCoordinator {
60012
60144
  const modeRank = {
60013
60145
  resume: 0,
60014
60146
  "conflict-fix": 1,
60015
- fresh: 2
60147
+ review: 2,
60148
+ fresh: 3
60016
60149
  };
60017
60150
  this.queue.sort((a, b) => {
60018
60151
  const pa = a.issue.priority === 0 ? Infinity : a.issue.priority;
@@ -60025,7 +60158,7 @@ class AgentCoordinator {
60025
60158
  this.spawnNext();
60026
60159
  await this.scanDoneForConflicts();
60027
60160
  await this.reportProgress();
60028
- const found = todo.length + inProgress.length + conflicted.length;
60161
+ const found = todo.length + inProgress.length + conflicted.length + review.length;
60029
60162
  return { found, added };
60030
60163
  }
60031
60164
  dependenciesResolved(issue) {
@@ -60169,6 +60302,26 @@ class AgentCoordinator {
60169
60302
  });
60170
60303
  }
60171
60304
  }
60305
+ if (mode === "review" && this.opts.clearReview) {
60306
+ try {
60307
+ await this.deps.removeIndicator(issue, this.opts.clearReview);
60308
+ this.deps.onLog(` ${issue.identifier}: clearReview applied`, "gray");
60309
+ } catch (err) {
60310
+ this.deps.onLog(`! Linear clearReview failed for ${issue.identifier}: ${err.message}`, "yellow");
60311
+ capture("agent_indicator_failed", {
60312
+ indicator: "clearReview",
60313
+ issue_identifier: issue.identifier,
60314
+ error: err.message
60315
+ });
60316
+ }
60317
+ }
60318
+ if (mode === "review" && this.opts.postComments !== false) {
60319
+ try {
60320
+ await this.deps.postComment(issue, `\uD83D\uDD01 Ralph picked up new review comments. Tracking change: \`${prep.changeName}\``);
60321
+ } catch (err) {
60322
+ this.deps.onLog(`! Linear review comment failed for ${issue.identifier}: ${err.message}`, "yellow");
60323
+ }
60324
+ }
60172
60325
  if (mode === "fresh" && this.opts.postComments !== false) {
60173
60326
  let alreadyPosted = false;
60174
60327
  try {
@@ -60308,7 +60461,7 @@ var init_coordinator = __esm(() => {
60308
60461
  });
60309
60462
 
60310
60463
  // apps/cli/src/agent/scaffold.ts
60311
- import { join as join13 } from "path";
60464
+ import { join as join14 } from "path";
60312
60465
  import { mkdir as mkdir3 } from "fs/promises";
60313
60466
  function changeNameForIssue(issue) {
60314
60467
  const slug = issue.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
@@ -60316,10 +60469,10 @@ function changeNameForIssue(issue) {
60316
60469
  }
60317
60470
  async function scaffoldChangeForIssue(tasksDir, statesDir, issue, comments = [], appendPrompt = "") {
60318
60471
  const name = changeNameForIssue(issue);
60319
- const changeDir = join13(tasksDir, name);
60320
- const stateDir = join13(statesDir, name);
60472
+ const changeDir = join14(tasksDir, name);
60473
+ const stateDir = join14(statesDir, name);
60321
60474
  await mkdir3(changeDir, { recursive: true });
60322
- await mkdir3(join13(changeDir, "specs"), { recursive: true });
60475
+ await mkdir3(join14(changeDir, "specs"), { recursive: true });
60323
60476
  await mkdir3(stateDir, { recursive: true });
60324
60477
  const commentsBlock = comments.length > 0 ? [
60325
60478
  "",
@@ -60371,106 +60524,13 @@ async function scaffoldChangeForIssue(tasksDir, statesDir, issue, comments = [],
60371
60524
  ""
60372
60525
  ].join(`
60373
60526
  `);
60374
- await Bun.write(join13(changeDir, "proposal.md"), proposal);
60375
- await Bun.write(join13(changeDir, "tasks.md"), tasks);
60376
- await Bun.write(join13(changeDir, "design.md"), design);
60527
+ await Bun.write(join14(changeDir, "proposal.md"), proposal);
60528
+ await Bun.write(join14(changeDir, "tasks.md"), tasks);
60529
+ await Bun.write(join14(changeDir, "design.md"), design);
60377
60530
  return name;
60378
60531
  }
60379
60532
  var init_scaffold = () => {};
60380
60533
 
60381
- // apps/cli/src/agent/worktree.ts
60382
- import { basename, join as join14 } from "path";
60383
- import { homedir as homedir3 } from "os";
60384
- import { exists } from "fs/promises";
60385
- function worktreesDir(projectRoot) {
60386
- return join14(homedir3(), ".ralph", basename(projectRoot), "worktrees");
60387
- }
60388
- function branchForChange(changeName) {
60389
- return `ralph/${changeName}`;
60390
- }
60391
- async function createWorktree(projectRoot, changeName, runner) {
60392
- const dir = worktreesDir(projectRoot);
60393
- const cwd2 = join14(dir, changeName);
60394
- const branch = branchForChange(changeName);
60395
- const list = await runner.run(["worktree", "list", "--porcelain"], projectRoot);
60396
- if (list.stdout.includes(`worktree ${cwd2}
60397
- `)) {
60398
- return { cwd: cwd2, branch };
60399
- }
60400
- let branchExists = true;
60401
- try {
60402
- await runner.run(["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], projectRoot);
60403
- } catch {
60404
- branchExists = false;
60405
- }
60406
- const cmd = branchExists ? ["worktree", "add", cwd2, branch] : ["worktree", "add", "-b", branch, cwd2];
60407
- await runner.run(cmd, projectRoot);
60408
- return { cwd: cwd2, branch };
60409
- }
60410
- async function removeWorktree(projectRoot, cwd2, runner) {
60411
- await runner.run(["worktree", "remove", "--force", cwd2], projectRoot);
60412
- }
60413
- async function isWorktreeSafeToRemove(cwd2, base2, runner) {
60414
- const status = await runner.run(["status", "--porcelain"], cwd2);
60415
- const dirty = status.stdout.trim();
60416
- let unpushedCommits = "";
60417
- try {
60418
- const log2 = await runner.run(["log", "--oneline", `${base2}..HEAD`, "--no-merges"], cwd2);
60419
- unpushedCommits = log2.stdout.trim();
60420
- } catch {
60421
- unpushedCommits = "<unknown: failed to compare against base>";
60422
- }
60423
- if (dirty && unpushedCommits) {
60424
- return {
60425
- safe: false,
60426
- reason: "uncommitted changes AND unpushed commits present",
60427
- dirty,
60428
- unpushedCommits
60429
- };
60430
- }
60431
- if (dirty) {
60432
- return {
60433
- safe: false,
60434
- reason: "uncommitted or untracked files present",
60435
- dirty,
60436
- unpushedCommits
60437
- };
60438
- }
60439
- if (unpushedCommits) {
60440
- return {
60441
- safe: false,
60442
- reason: `commits ahead of ${base2} were not pushed/PR'd`,
60443
- dirty,
60444
- unpushedCommits
60445
- };
60446
- }
60447
- return { safe: true, dirty, unpushedCommits };
60448
- }
60449
- async function seedWorktreeMcpConfig(projectRoot, worktreeCwd) {
60450
- const dst = join14(worktreeCwd, ".mcp.json");
60451
- const src = join14(projectRoot, ".mcp.json");
60452
- const source = await exists(dst) ? dst : await exists(src) ? src : null;
60453
- if (!source)
60454
- return;
60455
- let parsed;
60456
- try {
60457
- parsed = await Bun.file(source).json();
60458
- } catch {
60459
- return;
60460
- }
60461
- const servers = parsed.mcpServers;
60462
- if (servers && typeof servers === "object") {
60463
- for (const cfg of Object.values(servers)) {
60464
- if (Array.isArray(cfg.args)) {
60465
- cfg.args = cfg.args.map((a) => typeof a === "string" && a.startsWith(".ralph/") ? join14(projectRoot, a) : a);
60466
- }
60467
- }
60468
- }
60469
- await Bun.write(dst, JSON.stringify(parsed, null, 2) + `
60470
- `);
60471
- }
60472
- var init_worktree = () => {};
60473
-
60474
60534
  // apps/cli/src/agent/pr.ts
60475
60535
  function defaultTitle(issue) {
60476
60536
  return `${issue.identifier}: ${issue.title}`;
@@ -61024,6 +61084,30 @@ function mergeIndicators(cfg, cli) {
61024
61084
  }
61025
61085
  return out;
61026
61086
  }
61087
+ function isRalphComment(body) {
61088
+ const trimmed = body.trimStart();
61089
+ return /^(\uD83E\uDD16|\uD83D\uDD04|\u2705|\u2717|\u26A0|\uD83D\uDD01)\s*Ralph\b/.test(trimmed);
61090
+ }
61091
+ function buildReviewTaskBody(comments, url) {
61092
+ if (comments.length === 0) {
61093
+ return `No non-Ralph reviewer comments were found on ${url}. Recheck the issue manually before continuing.`;
61094
+ }
61095
+ const blocks = comments.map((c) => {
61096
+ const author = c.user?.name ?? "unknown";
61097
+ return `**${author}** \u2014 ${c.createdAt}
61098
+
61099
+ ${c.body.trim()}`;
61100
+ });
61101
+ return [
61102
+ `Reviewer comments left on the Linear issue (${url}):`,
61103
+ "",
61104
+ ...blocks,
61105
+ "",
61106
+ "Address every concrete request above. If a comment is ambiguous, note",
61107
+ "your interpretation in proposal.md `## Steering` before acting."
61108
+ ].join(`
61109
+ `);
61110
+ }
61027
61111
  function unionMarkers(...sets) {
61028
61112
  const out = [];
61029
61113
  const seen = new Set;
@@ -61063,6 +61147,7 @@ function buildAgentCoordinator(input) {
61063
61147
  const team = args.linearTeam || cfg.linear.team;
61064
61148
  const assignee = args.linearAssignee || cfg.linear.assignee;
61065
61149
  const excludeFromTodo = unionMarkers(indicators.setDone, indicators.setError, indicators.setConflicted);
61150
+ const excludeFromReview = unionMarkers(indicators.setInProgress, indicators.setError, indicators.setConflicted);
61066
61151
  const gitRunner = input.runners?.git ?? bunGitRunner;
61067
61152
  const cmdRunner = input.runners?.cmd ?? bunCmdRunner;
61068
61153
  const stateCache = new Map;
@@ -61236,7 +61321,8 @@ function buildAgentCoordinator(input) {
61236
61321
  async function prepare(issue, mode) {
61237
61322
  const { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch } = await setupWorktree(issue);
61238
61323
  let changeName;
61239
- if (mode === "fresh") {
61324
+ const isFresh = mode === "fresh";
61325
+ if (isFresh) {
61240
61326
  let comments = [];
61241
61327
  try {
61242
61328
  comments = await fetchIssueComments(apiKey, issue.id);
@@ -61256,7 +61342,24 @@ function buildAgentCoordinator(input) {
61256
61342
  issueByChange.set(changeName, issue);
61257
61343
  if (branch)
61258
61344
  branchByChange.set(changeName, branch);
61259
- if (mode === "conflict-fix") {
61345
+ if (mode === "review") {
61346
+ const wtLayout = projectLayout(workerCwd);
61347
+ const tasksFile = join16(wtLayout.changeDir(changeName), "tasks.md");
61348
+ let comments = [];
61349
+ try {
61350
+ comments = await fetchIssueComments(apiKey, issue.id);
61351
+ } catch (err) {
61352
+ onLog(`! Linear comment fetch failed for ${issue.identifier}: ${err.message}`, "yellow");
61353
+ }
61354
+ const reviewerComments = comments.filter((c) => !isRalphComment(c.body));
61355
+ const body = buildReviewTaskBody(reviewerComments, issue.url);
61356
+ try {
61357
+ await prependFixTask(tasksFile, "Address reviewer comments", body);
61358
+ } catch (err) {
61359
+ onLog(`! could not prepend review task: ${err.message}`, "red");
61360
+ }
61361
+ await reactivateState2(wtLayout.stateFile(changeName), changeName);
61362
+ } else if (mode === "conflict-fix") {
61260
61363
  const wtLayout = projectLayout(workerCwd);
61261
61364
  const tasksFile = join16(wtLayout.changeDir(changeName), "tasks.md");
61262
61365
  const prUrl = prByChange.get(changeName);
@@ -61531,6 +61634,7 @@ PR: ${prUrl}` : ""
61531
61634
  fetchTodo: () => fetchByGet(indicators.getTodo, excludeFromTodo),
61532
61635
  fetchInProgress: () => fetchByGet(indicators.getInProgress, []),
61533
61636
  fetchConflicted: () => fetchByGet(indicators.getConflicted, []),
61637
+ fetchReview: () => fetchByGet(indicators.getReview, excludeFromReview),
61534
61638
  fetchDoneCandidates,
61535
61639
  prepare,
61536
61640
  spawnWorker,
@@ -61559,6 +61663,7 @@ PR: ${prUrl}` : ""
61559
61663
  ...indicators.setError !== undefined ? { setError: indicators.setError } : {},
61560
61664
  ...indicators.setConflicted !== undefined ? { setConflicted: indicators.setConflicted } : {},
61561
61665
  ...indicators.clearConflicted !== undefined ? { clearConflicted: indicators.clearConflicted } : {},
61666
+ ...indicators.clearReview !== undefined ? { clearReview: indicators.clearReview } : {},
61562
61667
  postComments: cfg.linear.postComments,
61563
61668
  commentEveryIterations: cfg.linear.updateEveryIterations,
61564
61669
  ...args.maxTickets > 0 ? { maxTickets: args.maxTickets } : {}
@@ -61585,6 +61690,9 @@ function describeIndicators(indicators, team, assignee) {
61585
61690
  if (indicators.getConflicted) {
61586
61691
  parts.push(`conflicted=[${indicators.getConflicted.filter.map((m) => `${m.type}:${m.value}`).join(",")}]`);
61587
61692
  }
61693
+ if (indicators.getReview) {
61694
+ parts.push(`review=[${indicators.getReview.filter.map((m) => `${m.type}:${m.value}`).join(",")}]`);
61695
+ }
61588
61696
  return parts.join(", ");
61589
61697
  }
61590
61698
  var bunGitRunner, bunCmdRunner;
@@ -66963,7 +67071,8 @@ function ensureState(changeDir) {
66963
67071
 
66964
67072
  // apps/cli/src/components/TaskList.tsx
66965
67073
  var import_react22 = __toESM(require_react(), 1);
66966
- import { join as join3 } from "path";
67074
+ import { join as join4 } from "path";
67075
+ init_worktree();
66967
67076
  var jsx_dev_runtime = __toESM(require_jsx_dev_runtime(), 1);
66968
67077
  function countTaskItems(content) {
66969
67078
  const checked = (content.match(/^- \[x\]/gm) ?? []).length;
@@ -66976,10 +67085,10 @@ function buildRows(statesDir, projectRoot) {
66976
67085
  const seenNames = new Set;
66977
67086
  const sources = [{ dir: statesDir, label: "main" }];
66978
67087
  if (projectRoot) {
66979
- const worktreesRoot = join3(projectRoot, ".ralph", "worktrees");
67088
+ const worktreesRoot = worktreesDir(projectRoot);
66980
67089
  for (const wt of storage.list(worktreesRoot)) {
66981
67090
  sources.push({
66982
- dir: join3(worktreesRoot, wt, ".ralph", "tasks"),
67091
+ dir: join4(worktreesRoot, wt, ".ralph", "tasks"),
66983
67092
  label: `wt:${wt}`
66984
67093
  });
66985
67094
  }
@@ -66988,7 +67097,7 @@ function buildRows(statesDir, projectRoot) {
66988
67097
  for (const entry of storage.list(dir)) {
66989
67098
  if (seenNames.has(entry))
66990
67099
  continue;
66991
- const raw = storage.read(join3(dir, entry, ".ralph-state.json"));
67100
+ const raw = storage.read(join4(dir, entry, ".ralph-state.json"));
66992
67101
  if (raw === null)
66993
67102
  continue;
66994
67103
  let state;
@@ -67004,7 +67113,7 @@ function buildRows(statesDir, projectRoot) {
67004
67113
  `).find((l) => l.trim() !== "") ?? "";
67005
67114
  let progress = "\u2014";
67006
67115
  let progressStyled = true;
67007
- const tasksContent = storage.read(join3(dir, entry, "tasks.md"));
67116
+ const tasksContent = storage.read(join4(dir, entry, "tasks.md"));
67008
67117
  if (tasksContent !== null) {
67009
67118
  const { checked, unchecked } = countTaskItems(tasksContent);
67010
67119
  const total = checked + unchecked;
@@ -67145,7 +67254,7 @@ function TaskList({ statesDir, projectRoot }) {
67145
67254
  }
67146
67255
 
67147
67256
  // apps/cli/src/components/TaskStatus.tsx
67148
- import { join as join4 } from "path";
67257
+ import { join as join5 } from "path";
67149
67258
  var jsx_dev_runtime2 = __toESM(require_jsx_dev_runtime(), 1);
67150
67259
  var HEAVY_RULE = "============================================";
67151
67260
  var LIGHT_RULE = "--------------------------------------------";
@@ -67156,7 +67265,7 @@ function TaskStatus({ state, stateDir }) {
67156
67265
  const time = Math.round(state.usage.total_duration_ms / 1000 * 10) / 10 + "s";
67157
67266
  const artifacts = OPENSPEC_ARTIFACTS.map((name) => ({
67158
67267
  name,
67159
- exists: storage.read(join4(stateDir, name)) !== null
67268
+ exists: storage.read(join5(stateDir, name)) !== null
67160
67269
  }));
67161
67270
  const recent = state.history.slice(-10);
67162
67271
  return /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Box_default, {
@@ -67299,7 +67408,7 @@ function TaskStatus({ state, stateDir }) {
67299
67408
 
67300
67409
  // apps/cli/src/components/TaskLoop.tsx
67301
67410
  var import_react56 = __toESM(require_react(), 1);
67302
- import { join as join9 } from "path";
67411
+ import { join as join10 } from "path";
67303
67412
 
67304
67413
  // node_modules/.bun/@inkjs+ui@2.0.0+5b84dde3d6cd3930/node_modules/@inkjs/ui/build/components/badge/badge.js
67305
67414
  var import_react24 = __toESM(require_react(), 1);
@@ -70863,7 +70972,7 @@ function StopMessage({
70863
70972
 
70864
70973
  // apps/cli/src/hooks/useLoop.ts
70865
70974
  var import_react55 = __toESM(require_react(), 1);
70866
- import { join as join8 } from "path";
70975
+ import { join as join9 } from "path";
70867
70976
 
70868
70977
  // packages/engine/src/spawn.ts
70869
70978
  var {spawn: bunSpawn } = globalThis.Bun;
@@ -70873,7 +70982,7 @@ var spawn = bunSpawn;
70873
70982
  import { createWriteStream } from "fs";
70874
70983
  import { mkdtemp, unlink, mkdir } from "fs/promises";
70875
70984
  import { dirname as dirname2 } from "path";
70876
- import { join as join5 } from "path";
70985
+ import { join as join6 } from "path";
70877
70986
  import { tmpdir } from "os";
70878
70987
 
70879
70988
  // packages/engine/src/feed-events.ts
@@ -71606,7 +71715,7 @@ function buildCodexArgs() {
71606
71715
  return ["exec", "--json", "--color", "never", "--dangerously-bypass-approvals-and-sandbox", "-"];
71607
71716
  }
71608
71717
  async function runInteractive(model, prompt, taskDir) {
71609
- const promptFile = taskDir ? join5(taskDir, "_interactive_prompt.md") : join5(await mkdtemp(join5(tmpdir(), "ralph-")), "prompt.md");
71718
+ const promptFile = taskDir ? join6(taskDir, "_interactive_prompt.md") : join6(await mkdtemp(join6(tmpdir(), "ralph-")), "prompt.md");
71610
71719
  await Bun.write(promptFile, prompt);
71611
71720
  try {
71612
71721
  const cmd = [
@@ -71632,7 +71741,7 @@ async function runInteractive(model, prompt, taskDir) {
71632
71741
  stderr: "inherit"
71633
71742
  });
71634
71743
  const exitCode = await proc.exited;
71635
- const doneFile = taskDir ? join5(taskDir, "_interactive_done") : null;
71744
+ const doneFile = taskDir ? join6(taskDir, "_interactive_done") : null;
71636
71745
  if (doneFile && await Bun.file(doneFile).exists()) {
71637
71746
  return { exitCode: 0, usage: null, sessionId: null, rateLimited: false };
71638
71747
  }
@@ -71849,12 +71958,12 @@ function commitTaskDir(taskDir, message) {
71849
71958
  init_src();
71850
71959
 
71851
71960
  // packages/core/src/loop.ts
71852
- import { join as join7 } from "path";
71961
+ import { join as join8 } from "path";
71853
71962
  var STEERING_MAX_LINES = 20;
71854
71963
  function buildTaskPrompt(state, taskDir) {
71855
71964
  const storage = getStorage();
71856
71965
  let prompt = "";
71857
- const steeringContent = storage.read(join7(taskDir, "steering.md"));
71966
+ const steeringContent = storage.read(join8(taskDir, "steering.md"));
71858
71967
  if (steeringContent !== null) {
71859
71968
  const steeringLines = steeringContent.split(`
71860
71969
  `).filter((line) => !line.startsWith("#")).filter((line) => line.trim()).slice(0, STEERING_MAX_LINES);
@@ -71873,7 +71982,7 @@ function buildTaskPrompt(state, taskDir) {
71873
71982
  `;
71874
71983
  }
71875
71984
  }
71876
- const tasksContent = storage.read(join7(taskDir, "tasks.md"));
71985
+ const tasksContent = storage.read(join8(taskDir, "tasks.md"));
71877
71986
  if (tasksContent !== null) {
71878
71987
  const section = firstUnchecked(tasksContent);
71879
71988
  if (section) {
@@ -71888,7 +71997,7 @@ function buildTaskPrompt(state, taskDir) {
71888
71997
  prompt += `---
71889
71998
 
71890
71999
  `;
71891
- 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.
72000
+ 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.
71892
72001
 
71893
72002
  `;
71894
72003
  }
@@ -71909,7 +72018,7 @@ function buildTaskPrompt(state, taskDir) {
71909
72018
  `;
71910
72019
  }
71911
72020
  if (state.manualTest) {
71912
- const tasksContent2 = storage.read(join7(taskDir, "tasks.md"));
72021
+ const tasksContent2 = storage.read(join8(taskDir, "tasks.md"));
71913
72022
  const hasUncheckedTasks = tasksContent2 !== null && /^- \[ \]/m.test(tasksContent2);
71914
72023
  if (!hasUncheckedTasks) {
71915
72024
  const hasManualTestSection = tasksContent2 !== null && /^## Manual Testing/m.test(tasksContent2);
@@ -71958,7 +72067,7 @@ When all tasks are complete and all files are committed, push your branch and op
71958
72067
  }
71959
72068
  function checkStopSignal(taskDir, stateDir) {
71960
72069
  const storage = getStorage();
71961
- const stopFile = join7(taskDir, "STOP");
72070
+ const stopFile = join8(taskDir, "STOP");
71962
72071
  const reason = storage.read(stopFile);
71963
72072
  if (reason === null)
71964
72073
  return null;
@@ -72033,7 +72142,7 @@ function updateStateIteration(stateDir, result2, startedAt, engine, model, usage
72033
72142
  }
72034
72143
  function appendSteeringMessage(taskDir, message) {
72035
72144
  const storage = getStorage();
72036
- const steeringPath = join7(taskDir, "steering.md");
72145
+ const steeringPath = join8(taskDir, "steering.md");
72037
72146
  const existing = storage.read(steeringPath);
72038
72147
  const updated = existing ? `${message}
72039
72148
 
@@ -72100,11 +72209,11 @@ function useLoop(opts) {
72100
72209
  setLogLines((prev) => [...prev, { id: nextId(), kind: "feed", event }]);
72101
72210
  };
72102
72211
  runWithContext(createDefaultContext(), async () => {
72103
- const stateDir = join8(opts.statesDir, opts.name);
72104
- const tasksDir = join8(opts.tasksDir, opts.name);
72212
+ const stateDir = join9(opts.statesDir, opts.name);
72213
+ const tasksDir = join9(opts.tasksDir, opts.name);
72105
72214
  const storage = getStorage();
72106
72215
  let currentState;
72107
- const existingStateRaw = storage.read(join8(stateDir, ".ralph-state.json"));
72216
+ const existingStateRaw = storage.read(join9(stateDir, ".ralph-state.json"));
72108
72217
  if (existingStateRaw !== null) {
72109
72218
  currentState = readState(stateDir);
72110
72219
  if (currentState.engine !== opts.engine || currentState.model !== opts.model) {
@@ -72151,7 +72260,7 @@ function useLoop(opts) {
72151
72260
  setStopReason(stop);
72152
72261
  break;
72153
72262
  }
72154
- const tasksContent = storage.read(join8(tasksDir, "tasks.md"));
72263
+ const tasksContent = storage.read(join9(tasksDir, "tasks.md"));
72155
72264
  if (tasksContent !== null) {
72156
72265
  const remaining = countUnchecked(tasksContent);
72157
72266
  addInfo(`tasks.md: ${remaining} unchecked item${remaining === 1 ? "" : "s"} remaining`);
@@ -72191,7 +72300,7 @@ function useLoop(opts) {
72191
72300
  model: opts.model,
72192
72301
  prompt,
72193
72302
  logFlag: opts.log,
72194
- logFile: join8(stateDir, "log.json"),
72303
+ logFile: join9(stateDir, "log.json"),
72195
72304
  taskDir: tasksDir,
72196
72305
  interactive: false,
72197
72306
  onFeedEvent: addFeedEvent,
@@ -72214,7 +72323,7 @@ function useLoop(opts) {
72214
72323
  model: opts.model,
72215
72324
  prompt: buildSteeringPrompt(steerMessage),
72216
72325
  logFlag: opts.log,
72217
- logFile: join8(stateDir, "log.json"),
72326
+ logFile: join9(stateDir, "log.json"),
72218
72327
  taskDir: tasksDir,
72219
72328
  onFeedEvent: addResumeFeedEvent,
72220
72329
  signal: resumeController.signal,
@@ -72409,7 +72518,7 @@ function TaskLoop({ opts }) {
72409
72518
  }, [loop.isRunning, exit]);
72410
72519
  if (!loop.state)
72411
72520
  return null;
72412
- const stateDir = join9(opts.statesDir, opts.name);
72521
+ const stateDir = join10(opts.statesDir, opts.name);
72413
72522
  return /* @__PURE__ */ jsx_dev_runtime8.jsxDEV(Box_default, {
72414
72523
  flexDirection: "column",
72415
72524
  children: [
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.2",
3
+ "version": "2.18.0",
4
4
  "description": "An iterative AI task execution framework. Orchestrates multi-phase autonomous work using Claude or Codex engines.",
5
5
  "keywords": [
6
6
  "agent",
@@ -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": {