@neriros/ralphy 2.16.4 → 2.16.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +16 -1
  2. package/dist/cli/index.js +202 -140
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -231,9 +231,24 @@ Each worker card shows:
231
231
  - `▶ TASK` — first unchecked task from `tasks.md`, updated every second
232
232
  - `PHASE` with color (cyan = working, yellow = git ops, blue = CI, green = done, red = gave-up) + time in phase
233
233
  - `⏵ CMD` when a shell command is in flight (shows the command and how long it's been running)
234
- - `LOG` — path to the worker's log file for `tail -f`
234
+ - `LOG` — path to the worker's log file for `tail -f` (format: `[ISO] [type] message`)
235
235
  - `─ OUTPUT ─` section with live stdout/stderr (scales to fill remaining terminal height for the focused worker)
236
236
 
237
+ ### Log files
238
+
239
+ All log entries use the format `[2024-01-15T12:00:00.000Z] [type] message`. Four log types:
240
+
241
+ | Type | Meaning | Destination |
242
+ | --------- | -------------------------------------------------- | -------------------------------------------------- |
243
+ | `session` | Worker start / stop boundaries | `agent-mode.log` + worker log |
244
+ | `phase` | Phase transitions (working → pushing → ci-poll …) | `agent-mode.log` + worker log |
245
+ | `coord` | Coordinator events (Linear poll, worktree, labels) | `agent-mode.log` + worker log (when task-specific) |
246
+ | `output` | Raw subprocess stdout/stderr lines | Worker log only |
247
+
248
+ - **`~/.ralph/agent-mode.log`** — global session log, appended each agent run
249
+ - **`<projectRoot>/.ralph/logs/<change>.log`** — per-worker unified log; includes output, phases, and coordinator events for that task. `tail -f` this for live progress.
250
+ - **`<taskDir>/LOG.jsonl`** — structured JSON event log for the web UI (each line has a `ts` field)
251
+
237
252
  ## OpenSpec Flow
238
253
 
239
254
  There are no phases. One loop, one prompt, one `tasks.md` checklist.
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.16.4")
35033
- return "2.16.4";
35032
+ if ("2.16.6")
35033
+ return "2.16.6";
35034
35034
  } catch {}
35035
35035
  const dirsToTry = [];
35036
35036
  try {
@@ -59616,19 +59616,73 @@ var init_config = __esm(() => {
59616
59616
  });
59617
59617
  });
59618
59618
 
59619
+ // packages/log/src/log.ts
59620
+ import { appendFile } from "fs/promises";
59621
+ import { join as join11, dirname as dirname4 } from "path";
59622
+ import { homedir as homedir2 } from "os";
59623
+ import { mkdir as mkdir2 } from "fs/promises";
59624
+ function fmt(type, text) {
59625
+ return `[${new Date().toISOString()}] [${type}] ${text}
59626
+ `;
59627
+ }
59628
+ function write(path, line) {
59629
+ appendFile(path, line).catch(() => {
59630
+ return;
59631
+ });
59632
+ }
59633
+ function logSession(text, workerLogFile) {
59634
+ const clean = text.replace(ANSI_RE, "").trim();
59635
+ if (!clean)
59636
+ return;
59637
+ const line = fmt("session", clean);
59638
+ write(AGENT_LOG_PATH, line);
59639
+ if (workerLogFile)
59640
+ write(workerLogFile, line);
59641
+ }
59642
+ function logCoord(text, workerLogFile) {
59643
+ const clean = text.replace(ANSI_RE, "").trim();
59644
+ if (!clean)
59645
+ return;
59646
+ const line = fmt("coord", clean);
59647
+ write(AGENT_LOG_PATH, line);
59648
+ if (workerLogFile)
59649
+ write(workerLogFile, line);
59650
+ }
59651
+ function logPhase(changeName, workerLogFile, phase, detail) {
59652
+ const msg = `${changeName}: ${phase}${detail ? ` (${detail})` : ""}`;
59653
+ const line = fmt("phase", msg);
59654
+ write(AGENT_LOG_PATH, line);
59655
+ write(workerLogFile, line);
59656
+ }
59657
+ function logOutput(workerLogFile, text) {
59658
+ write(workerLogFile, fmt("output", text));
59659
+ }
59660
+ async function initWorkerLog(logFile) {
59661
+ await mkdir2(dirname4(logFile), { recursive: true });
59662
+ await Bun.write(logFile, "");
59663
+ }
59664
+ var ANSI_RE, AGENT_LOG_PATH;
59665
+ var init_log = __esm(() => {
59666
+ ANSI_RE = /\x1b(?:\[[0-9;]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|.)/g;
59667
+ AGENT_LOG_PATH = join11(homedir2(), ".ralph", "agent-mode.log");
59668
+ mkdir2(dirname4(AGENT_LOG_PATH), { recursive: true }).catch(() => {
59669
+ return;
59670
+ });
59671
+ });
59672
+
59619
59673
  // packages/core/src/layout.ts
59620
- import { join as join11 } from "path";
59674
+ import { join as join12 } from "path";
59621
59675
  function projectLayout(root) {
59622
- const statesDir = join11(root, ".ralph", "tasks");
59623
- const tasksDir = join11(root, "openspec", "changes");
59676
+ const statesDir = join12(root, ".ralph", "tasks");
59677
+ const tasksDir = join12(root, "openspec", "changes");
59624
59678
  return {
59625
59679
  root,
59626
59680
  statesDir,
59627
59681
  tasksDir,
59628
- agentStateFile: join11(root, ".ralph", "agent-state.json"),
59629
- changeDir: (name) => join11(tasksDir, name),
59630
- taskStateDir: (name) => join11(statesDir, name),
59631
- stateFile: (name) => join11(statesDir, name, STATE_FILE2)
59682
+ agentStateFile: join12(root, ".ralph", "agent-state.json"),
59683
+ changeDir: (name) => join12(tasksDir, name),
59684
+ taskStateDir: (name) => join12(statesDir, name),
59685
+ stateFile: (name) => join12(statesDir, name, STATE_FILE2)
59632
59686
  };
59633
59687
  }
59634
59688
  var STATE_FILE2 = ".ralph-state.json";
@@ -60221,19 +60275,19 @@ var init_coordinator = __esm(() => {
60221
60275
  });
60222
60276
 
60223
60277
  // apps/cli/src/agent/scaffold.ts
60224
- import { join as join12 } from "path";
60225
- import { mkdir as mkdir2 } from "fs/promises";
60278
+ import { join as join13 } from "path";
60279
+ import { mkdir as mkdir3 } from "fs/promises";
60226
60280
  function changeNameForIssue(issue) {
60227
60281
  const slug = issue.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
60228
60282
  return slug ? `${issue.identifier.toLowerCase()}-${slug}` : issue.identifier.toLowerCase();
60229
60283
  }
60230
60284
  async function scaffoldChangeForIssue(tasksDir, statesDir, issue, comments = [], appendPrompt = "") {
60231
60285
  const name = changeNameForIssue(issue);
60232
- const changeDir = join12(tasksDir, name);
60233
- const stateDir = join12(statesDir, name);
60234
- await mkdir2(changeDir, { recursive: true });
60235
- await mkdir2(join12(changeDir, "specs"), { recursive: true });
60236
- await mkdir2(stateDir, { recursive: true });
60286
+ const changeDir = join13(tasksDir, name);
60287
+ const stateDir = join13(statesDir, name);
60288
+ await mkdir3(changeDir, { recursive: true });
60289
+ await mkdir3(join13(changeDir, "specs"), { recursive: true });
60290
+ await mkdir3(stateDir, { recursive: true });
60237
60291
  const commentsBlock = comments.length > 0 ? [
60238
60292
  "",
60239
60293
  "## Linear comments",
@@ -60284,26 +60338,26 @@ async function scaffoldChangeForIssue(tasksDir, statesDir, issue, comments = [],
60284
60338
  ""
60285
60339
  ].join(`
60286
60340
  `);
60287
- await Bun.write(join12(changeDir, "proposal.md"), proposal);
60288
- await Bun.write(join12(changeDir, "tasks.md"), tasks);
60289
- await Bun.write(join12(changeDir, "design.md"), design);
60341
+ await Bun.write(join13(changeDir, "proposal.md"), proposal);
60342
+ await Bun.write(join13(changeDir, "tasks.md"), tasks);
60343
+ await Bun.write(join13(changeDir, "design.md"), design);
60290
60344
  return name;
60291
60345
  }
60292
60346
  var init_scaffold = () => {};
60293
60347
 
60294
60348
  // apps/cli/src/agent/worktree.ts
60295
- import { basename, join as join13 } from "path";
60296
- import { homedir as homedir2 } from "os";
60349
+ import { basename, join as join14 } from "path";
60350
+ import { homedir as homedir3 } from "os";
60297
60351
  import { exists } from "fs/promises";
60298
60352
  function worktreesDir(projectRoot) {
60299
- return join13(homedir2(), ".ralph", basename(projectRoot), "worktrees");
60353
+ return join14(homedir3(), ".ralph", basename(projectRoot), "worktrees");
60300
60354
  }
60301
60355
  function branchForChange(changeName) {
60302
60356
  return `ralph/${changeName}`;
60303
60357
  }
60304
60358
  async function createWorktree(projectRoot, changeName, runner) {
60305
60359
  const dir = worktreesDir(projectRoot);
60306
- const cwd2 = join13(dir, changeName);
60360
+ const cwd2 = join14(dir, changeName);
60307
60361
  const branch = branchForChange(changeName);
60308
60362
  const list = await runner.run(["worktree", "list", "--porcelain"], projectRoot);
60309
60363
  if (list.stdout.includes(`worktree ${cwd2}
@@ -60360,8 +60414,8 @@ async function isWorktreeSafeToRemove(cwd2, base2, runner) {
60360
60414
  return { safe: true, dirty, unpushedCommits };
60361
60415
  }
60362
60416
  async function seedWorktreeMcpConfig(projectRoot, worktreeCwd) {
60363
- const dst = join13(worktreeCwd, ".mcp.json");
60364
- const src = join13(projectRoot, ".mcp.json");
60417
+ const dst = join14(worktreeCwd, ".mcp.json");
60418
+ const src = join14(projectRoot, ".mcp.json");
60365
60419
  const source = await exists(dst) ? dst : await exists(src) ? src : null;
60366
60420
  if (!source)
60367
60421
  return;
@@ -60375,7 +60429,7 @@ async function seedWorktreeMcpConfig(projectRoot, worktreeCwd) {
60375
60429
  if (servers && typeof servers === "object") {
60376
60430
  for (const cfg of Object.values(servers)) {
60377
60431
  if (Array.isArray(cfg.args)) {
60378
- cfg.args = cfg.args.map((a) => typeof a === "string" && a.startsWith(".ralph/") ? join13(projectRoot, a) : a);
60432
+ cfg.args = cfg.args.map((a) => typeof a === "string" && a.startsWith(".ralph/") ? join14(projectRoot, a) : a);
60379
60433
  }
60380
60434
  }
60381
60435
  }
@@ -60558,7 +60612,7 @@ var init_ci = __esm(() => {
60558
60612
  });
60559
60613
 
60560
60614
  // apps/cli/src/agent/post-task.ts
60561
- import { join as join14 } from "path";
60615
+ import { join as join15 } from "path";
60562
60616
  async function reactivateState(stateFilePath, log2, changeName) {
60563
60617
  const file = Bun.file(stateFilePath);
60564
60618
  if (!await file.exists())
@@ -60577,7 +60631,7 @@ async function reactivateState(stateFilePath, log2, changeName) {
60577
60631
  }
60578
60632
  async function runWorkerWithFixTask(ctx, heading, body) {
60579
60633
  try {
60580
- await prependFixTask(join14(ctx.changeDir, "tasks.md"), heading, body);
60634
+ await prependFixTask(join15(ctx.changeDir, "tasks.md"), heading, body);
60581
60635
  } catch (err) {
60582
60636
  ctx.log(`! could not prepend fix task: ${err.message}`, "red");
60583
60637
  return 1;
@@ -60889,8 +60943,8 @@ var init_post_task = __esm(() => {
60889
60943
  });
60890
60944
 
60891
60945
  // apps/cli/src/agent/wire.ts
60892
- import { join as join15 } from "path";
60893
- import { mkdir as mkdir3 } from "fs/promises";
60946
+ import { join as join16 } from "path";
60947
+ import { mkdir as mkdir4 } from "fs/promises";
60894
60948
  function traceCmdRunner(base2, onStart, onEnd) {
60895
60949
  return {
60896
60950
  run: async (cmd, cwd2) => {
@@ -60951,7 +61005,7 @@ function buildAgentCoordinator(input) {
60951
61005
  onWorkerOutput,
60952
61006
  onWorkerCmd
60953
61007
  } = input;
60954
- const logsDir = join15(projectRoot, ".ralph", "logs");
61008
+ const logsDir = join16(projectRoot, ".ralph", "logs");
60955
61009
  const concurrency = args.concurrency || cfg.concurrency;
60956
61010
  const pollInterval = args.pollInterval || cfg.pollIntervalSeconds;
60957
61011
  const indicators = mergeIndicators(cfg.linear.indicators, args.indicators);
@@ -61143,8 +61197,8 @@ function buildAgentCoordinator(input) {
61143
61197
  } else {
61144
61198
  changeName = changeNameForIssue(issue);
61145
61199
  const wtLayout = projectLayout(workerCwd);
61146
- await mkdir3(wtLayout.changeDir(changeName), { recursive: true });
61147
- await mkdir3(wtLayout.taskStateDir(changeName), { recursive: true });
61200
+ await mkdir4(wtLayout.changeDir(changeName), { recursive: true });
61201
+ await mkdir4(wtLayout.taskStateDir(changeName), { recursive: true });
61148
61202
  }
61149
61203
  cwdByChange.set(changeName, workerCwd);
61150
61204
  statesDirByChange.set(changeName, scaffoldStatesDir);
@@ -61153,7 +61207,7 @@ function buildAgentCoordinator(input) {
61153
61207
  branchByChange.set(changeName, branch);
61154
61208
  if (mode === "conflict-fix") {
61155
61209
  const wtLayout = projectLayout(workerCwd);
61156
- const tasksFile = join15(wtLayout.changeDir(changeName), "tasks.md");
61210
+ const tasksFile = join16(wtLayout.changeDir(changeName), "tasks.md");
61157
61211
  const prUrl = prByChange.get(changeName);
61158
61212
  const body = [
61159
61213
  `The PR for this change has merge conflicts with \`${cfg.prBaseBranch}\`.`,
@@ -61231,21 +61285,8 @@ PR: ${prUrl}` : ""
61231
61285
  return c;
61232
61286
  }
61233
61287
  function defaultSpawn(changeName, cmd, cwd2, note) {
61234
- const logFilePath = join15(logsDir, `${changeName}.log`);
61235
- let logWriter = null;
61236
- const ensureLogWriter = async () => {
61237
- if (logWriter)
61238
- return logWriter;
61239
- try {
61240
- await Bun.write(logFilePath, "");
61241
- logWriter = Bun.file(logFilePath).writer();
61242
- return logWriter;
61243
- } catch (err) {
61244
- onLog(`! could not open worker log ${logFilePath}: ${err.message}`, "yellow");
61245
- return null;
61246
- }
61247
- };
61248
- const ANSI_RE = /\x1b(?:\[[0-9;]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|.)/g;
61288
+ const logFilePath = join16(logsDir, `${changeName}.log`);
61289
+ const ANSI_RE2 = /\x1b(?:\[[0-9;]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|.)/g;
61249
61290
  const BOX_ONLY_RE = /^[\s\u2500\u2502\u256D\u256E\u2570\u256F\u254C\u2504\u2501\u2503]+$/;
61250
61291
  const STATUS_BAR_LINE_RE = /^[\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F\u2713\u2717]\s+iter\s+\d+/;
61251
61292
  const ITER_HEADER_LINE_RE = /^\u2500\u2500/;
@@ -61258,7 +61299,6 @@ PR: ${prUrl}` : ""
61258
61299
  const reader = stream.getReader();
61259
61300
  const decoder = new TextDecoder;
61260
61301
  let buf = "";
61261
- const writer = await ensureLogWriter();
61262
61302
  try {
61263
61303
  while (true) {
61264
61304
  const { value, done } = await reader.read();
@@ -61271,26 +61311,20 @@ PR: ${prUrl}` : ""
61271
61311
  `)) >= 0) {
61272
61312
  const line = buf.slice(0, nl);
61273
61313
  buf = buf.slice(nl + 1);
61274
- const clean = line.replace(ANSI_RE, "").trim();
61275
- if (writer && clean && isLogWorthy(clean))
61276
- writer.write(clean + `
61277
- `);
61314
+ const clean = line.replace(ANSI_RE2, "").trim();
61315
+ if (clean && isLogWorthy(clean))
61316
+ logOutput(logFilePath, clean);
61278
61317
  if (line)
61279
61318
  onWorkerOutput?.(changeName, label === "err" ? `! ${line}` : line);
61280
61319
  }
61281
61320
  }
61282
61321
  if (buf) {
61283
- const clean = buf.replace(ANSI_RE, "").trim();
61284
- if (writer && clean && isLogWorthy(clean))
61285
- writer.write(clean + `
61286
- `);
61322
+ const clean = buf.replace(ANSI_RE2, "").trim();
61323
+ if (clean && isLogWorthy(clean))
61324
+ logOutput(logFilePath, clean);
61287
61325
  onWorkerOutput?.(changeName, label === "err" ? `! ${buf}` : buf);
61288
61326
  }
61289
- } catch {} finally {
61290
- try {
61291
- writer?.flush();
61292
- } catch {}
61293
- }
61327
+ } catch {}
61294
61328
  }
61295
61329
  const p = Bun.spawn({
61296
61330
  cmd,
@@ -61299,13 +61333,10 @@ PR: ${prUrl}` : ""
61299
61333
  stderr: "pipe",
61300
61334
  stdin: "ignore"
61301
61335
  });
61302
- (async () => {
61303
- const writer = await ensureLogWriter();
61304
- if (note && writer)
61305
- writer.write(`
61306
- --- ${note} ---
61307
- `);
61308
- })();
61336
+ initWorkerLog(logFilePath).then(() => {
61337
+ if (note)
61338
+ logSession(note, logFilePath);
61339
+ });
61309
61340
  pump(p.stdout, "out");
61310
61341
  pump(p.stderr, "err");
61311
61342
  return { exited: p.exited, kill: () => p.kill(), logFilePath };
@@ -61316,7 +61347,7 @@ PR: ${prUrl}` : ""
61316
61347
  let logFilePath;
61317
61348
  let handle;
61318
61349
  if (injected) {
61319
- logFilePath = join15(logsDir, `${changeName}.log`);
61350
+ logFilePath = join16(logsDir, `${changeName}.log`);
61320
61351
  handle = injected(buildTaskCmdFor(changeName), cwd2);
61321
61352
  } else {
61322
61353
  const r = defaultSpawn(changeName, buildTaskCmdFor(changeName), cwd2, `spawn at ${new Date().toISOString()}`);
@@ -61500,6 +61531,7 @@ function describeIndicators(indicators, team, assignee) {
61500
61531
  }
61501
61532
  var bunGitRunner, bunCmdRunner;
61502
61533
  var init_wire = __esm(() => {
61534
+ init_log();
61503
61535
  init_layout();
61504
61536
  init_types2();
61505
61537
  init_coordinator();
@@ -61546,7 +61578,7 @@ var exports_json_runner = {};
61546
61578
  __export(exports_json_runner, {
61547
61579
  runAgentJson: () => runAgentJson
61548
61580
  });
61549
- import { join as join19 } from "path";
61581
+ import { join as join20 } from "path";
61550
61582
  import { mkdir as mkdir6 } from "fs/promises";
61551
61583
  import { homedir as homedir4 } from "os";
61552
61584
  function cleanOutputLine2(raw) {
@@ -61571,7 +61603,7 @@ async function runAgentJson({
61571
61603
  statesDir,
61572
61604
  tasksDir
61573
61605
  }) {
61574
- await mkdir6(join19(homedir4(), ".ralph"), { recursive: true }).catch(() => {
61606
+ await mkdir6(join20(homedir4(), ".ralph"), { recursive: true }).catch(() => {
61575
61607
  return;
61576
61608
  });
61577
61609
  const cfgPath = await ensureRalphyConfig(projectRoot);
@@ -61677,7 +61709,7 @@ var init_json_runner = __esm(() => {
61677
61709
  });
61678
61710
 
61679
61711
  // apps/cli/src/index.ts
61680
- import { resolve as resolve2, join as join20, dirname as dirname6 } from "path";
61712
+ import { resolve as resolve2, join as join21, dirname as dirname6 } from "path";
61681
61713
  import { exists as exists2, mkdir as mkdir7, rm } from "fs/promises";
61682
61714
 
61683
61715
  // node_modules/.bun/ink@5.2.1+1f88f629f0141b18/node_modules/ink/build/render.js
@@ -66800,7 +66832,7 @@ function createDefaultContext() {
66800
66832
 
66801
66833
  // apps/cli/src/components/App.tsx
66802
66834
  var import_react58 = __toESM(require_react(), 1);
66803
- import { join as join18 } from "path";
66835
+ import { join as join19 } from "path";
66804
66836
 
66805
66837
  // packages/core/src/state.ts
66806
66838
  init_types2();
@@ -72377,11 +72409,19 @@ var import_react57 = __toESM(require_react(), 1);
72377
72409
  init_cli();
72378
72410
  init_config();
72379
72411
  init_wire();
72380
- var jsx_dev_runtime9 = __toESM(require_jsx_dev_runtime(), 1);
72381
- import { join as join16, dirname as dirname4 } from "path";
72412
+ import { join as join17 } from "path";
72382
72413
  import { pathToFileURL } from "url";
72383
- import { homedir as homedir3 } from "os";
72384
- import { appendFile, mkdir as mkdir4 } from "fs/promises";
72414
+
72415
+ // packages/core/src/progress.ts
72416
+ function countProgress(content) {
72417
+ const checked = (content.match(/^- \[x\]/gm) ?? []).length;
72418
+ const unchecked = (content.match(/^- \[ \]/gm) ?? []).length;
72419
+ return { checked, unchecked, total: checked + unchecked };
72420
+ }
72421
+
72422
+ // apps/cli/src/components/AgentMode.tsx
72423
+ init_log();
72424
+ var jsx_dev_runtime9 = __toESM(require_jsx_dev_runtime(), 1);
72385
72425
  var lineCounter = 0;
72386
72426
  function nextId() {
72387
72427
  lineCounter += 1;
@@ -72408,6 +72448,18 @@ function fmtElapsed(ms) {
72408
72448
  function trunc(s, max2) {
72409
72449
  return s.length > max2 ? s.slice(0, max2 - 1) + "\u2026" : s;
72410
72450
  }
72451
+ function calcProgressBar(checked, total, width) {
72452
+ const countStr = `${checked}/${total}`;
72453
+ const inner = width - 2;
72454
+ if (inner < countStr.length + 2)
72455
+ return null;
72456
+ const leftSlot = Math.floor((inner - countStr.length) / 2);
72457
+ const rightSlot = Math.max(0, inner - countStr.length - leftSlot);
72458
+ const filled = total > 0 ? Math.round(checked / total * inner) : 0;
72459
+ const filledLeft = Math.min(filled, leftSlot);
72460
+ const filledRight = Math.max(0, Math.min(filled - leftSlot - countStr.length, rightSlot));
72461
+ return { countStr, filledLeft, leftSlot, filledRight, rightSlot };
72462
+ }
72411
72463
  function prLabel(prUrl) {
72412
72464
  const m = prUrl.match(/\/pull\/(\d+)/);
72413
72465
  return m ? `#${m[1]}` : "PR";
@@ -72571,28 +72623,7 @@ function displayTailLines(activeCount) {
72571
72623
  return 8;
72572
72624
  return 5;
72573
72625
  }
72574
- var AGENT_LOG_PATH = join16(homedir3(), ".ralph", "agent-mode.log");
72575
72626
  var SESSION_START = new Date().toISOString();
72576
- mkdir4(dirname4(AGENT_LOG_PATH), { recursive: true }).catch(() => {
72577
- return;
72578
- });
72579
- function writeAgentLog(text) {
72580
- const clean = text.replace(ANSI_STRIP_RE, "").trim();
72581
- if (!clean)
72582
- return;
72583
- const line = `[${new Date().toISOString()}] ${clean}
72584
- `;
72585
- appendFile(AGENT_LOG_PATH, line).catch(() => {
72586
- return;
72587
- });
72588
- }
72589
- function writePhaseLog(phaseLogFile, text) {
72590
- const line = `[${new Date().toISOString()}] ${text}
72591
- `;
72592
- appendFile(phaseLogFile, line).catch(() => {
72593
- return;
72594
- });
72595
- }
72596
72627
  function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
72597
72628
  const { exit } = use_app_default();
72598
72629
  const { stdout } = use_stdout_default();
@@ -72606,15 +72637,15 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
72606
72637
  const nextPollAtRef = import_react57.useRef(0);
72607
72638
  const cfgRef = import_react57.useRef(null);
72608
72639
  const [pollStatus, setPollStatus] = import_react57.useState({ state: "idle", lastFound: null, lastAdded: null, lastAt: null, filterDesc: "" });
72609
- function appendLog(text, color) {
72640
+ function appendLog(text, color, workerLogFile) {
72610
72641
  setLogs((prev) => [...prev, { id: nextId(), text, color }]);
72611
- writeAgentLog(text);
72642
+ logCoord(text, workerLogFile);
72612
72643
  }
72613
72644
  import_react57.useEffect(() => {
72614
72645
  let pollTimer = null;
72615
72646
  let cancelled = false;
72616
72647
  async function init2() {
72617
- writeAgentLog(`=== session start ${SESSION_START} ===`);
72648
+ logSession(`=== session start ${SESSION_START} ===`);
72618
72649
  const cfgPath = await ensureRalphyConfig(projectRoot);
72619
72650
  const cfg2 = await loadRalphyConfig(projectRoot);
72620
72651
  cfgRef.current = cfg2;
@@ -72635,49 +72666,37 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
72635
72666
  onLog: appendLog,
72636
72667
  onWorkersChanged: () => setTick((t) => t + 1),
72637
72668
  onWorkerStarted: (changeName, dir, logFile, changeDir) => {
72638
- writeAgentLog(`worker-started ${changeName} log=${logFile}`);
72639
- const phaseLogFile = logFile.replace(/\.log$/, "-phases.log");
72640
- mkdir4(dirname4(phaseLogFile), { recursive: true }).then(() => appendFile(phaseLogFile, `=== session ${SESSION_START} | worker-started ${new Date().toISOString()} ===
72641
- `)).catch(() => {
72642
- return;
72643
- });
72669
+ logSession(`worker-started ${changeName} log=${logFile}`, logFile);
72644
72670
  workerMetaRef.current.set(changeName, {
72645
72671
  startedAt: Date.now(),
72646
72672
  statesDir: dir,
72647
72673
  logFile,
72648
- phaseLogFile,
72649
72674
  changeDir,
72650
72675
  iter: 0,
72651
72676
  phase: "working",
72652
72677
  phaseDetail: "",
72653
72678
  phaseStartedAt: Date.now(),
72654
72679
  currentTask: null,
72680
+ taskProgress: null,
72655
72681
  prUrl: null,
72656
72682
  currentCmd: null,
72657
72683
  tail: []
72658
72684
  });
72659
72685
  },
72660
72686
  onWorkerExited: (changeName) => {
72661
- writeAgentLog(`worker-exited ${changeName}`);
72662
72687
  const m = workerMetaRef.current.get(changeName);
72663
- if (m?.phaseLogFile) {
72664
- writePhaseLog(m.phaseLogFile, `=== worker-exited ===`);
72665
- }
72688
+ logSession(`worker-exited ${changeName}`, m?.logFile);
72666
72689
  workerMetaRef.current.delete(changeName);
72667
72690
  },
72668
72691
  onWorkerPhase: (changeName, phase, detail) => {
72669
72692
  const m = workerMetaRef.current.get(changeName);
72670
72693
  if (!m)
72671
72694
  return;
72672
- if (m.phase !== phase) {
72673
- writeAgentLog(`phase ${changeName}: ${phase}${detail ? ` (${detail})` : ""}`);
72695
+ if (m.phase !== phase)
72674
72696
  m.phaseStartedAt = Date.now();
72675
- }
72676
72697
  m.phase = phase;
72677
72698
  m.phaseDetail = detail ?? "";
72678
- if (m.phaseLogFile) {
72679
- writePhaseLog(m.phaseLogFile, `${phase}${detail ? ` (${detail})` : ""}`);
72680
- }
72699
+ logPhase(changeName, m.logFile, phase, detail);
72681
72700
  },
72682
72701
  onWorkerOutput: (changeName, line) => {
72683
72702
  const m = workerMetaRef.current.get(changeName);
@@ -72758,7 +72777,7 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
72758
72777
  (async () => {
72759
72778
  for (const [changeName, meta] of workerMetaRef.current) {
72760
72779
  try {
72761
- const file = Bun.file(join16(meta.statesDir, changeName, ".ralph-state.json"));
72780
+ const file = Bun.file(join17(meta.statesDir, changeName, ".ralph-state.json"));
72762
72781
  if (await file.exists()) {
72763
72782
  const json = await file.json();
72764
72783
  meta.iter = json.iteration ?? meta.iter;
@@ -72768,11 +72787,13 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
72768
72787
  }
72769
72788
  if (meta.changeDir) {
72770
72789
  try {
72771
- const tasksFile = Bun.file(join16(meta.changeDir, "tasks.md"));
72790
+ const tasksFile = Bun.file(join17(meta.changeDir, "tasks.md"));
72772
72791
  if (await tasksFile.exists()) {
72773
72792
  const text = await tasksFile.text();
72774
72793
  const match = text.match(/^- \[ \] (.+)$/m);
72775
72794
  meta.currentTask = match?.[1]?.trim() ?? null;
72795
+ const { checked, total } = countProgress(text);
72796
+ meta.taskProgress = total > 0 ? { checked, total } : null;
72776
72797
  }
72777
72798
  } catch (err) {
72778
72799
  console.error(`Failed to read tasks.md for worker '${changeName}' (may not exist yet):`, err);
@@ -73119,6 +73140,7 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
73119
73140
  const tail2 = meta?.tail ?? [];
73120
73141
  const prUrl = meta?.prUrl ?? null;
73121
73142
  const currentTask = meta?.currentTask ?? null;
73143
+ const taskProgress = meta?.taskProgress ?? null;
73122
73144
  const pBadge = priorityBadge(w.issue.priority);
73123
73145
  const mBadge = modeBadge(w.mode);
73124
73146
  const pColor = phaseColor(phase);
@@ -73322,6 +73344,46 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
73322
73344
  }, undefined, false, undefined, this)
73323
73345
  ]
73324
73346
  }, undefined, true, undefined, this),
73347
+ taskProgress && (() => {
73348
+ const bar = calcProgressBar(taskProgress.checked, taskProgress.total, termWidth - 4);
73349
+ if (!bar)
73350
+ return null;
73351
+ const { countStr, filledLeft, leftSlot, filledRight, rightSlot } = bar;
73352
+ return /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Box_default, {
73353
+ marginTop: 0,
73354
+ children: [
73355
+ /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
73356
+ dimColor: true,
73357
+ children: "["
73358
+ }, undefined, false, undefined, this),
73359
+ /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
73360
+ color: "green",
73361
+ children: "\u2588".repeat(filledLeft)
73362
+ }, undefined, false, undefined, this),
73363
+ /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
73364
+ dimColor: true,
73365
+ children: "\u2591".repeat(leftSlot - filledLeft)
73366
+ }, undefined, false, undefined, this),
73367
+ /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
73368
+ color: "white",
73369
+ bold: true,
73370
+ children: countStr
73371
+ }, undefined, false, undefined, this),
73372
+ /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
73373
+ color: "green",
73374
+ children: "\u2588".repeat(filledRight)
73375
+ }, undefined, false, undefined, this),
73376
+ /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
73377
+ dimColor: true,
73378
+ children: "\u2591".repeat(rightSlot - filledRight)
73379
+ }, undefined, false, undefined, this),
73380
+ /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Text, {
73381
+ dimColor: true,
73382
+ children: "]"
73383
+ }, undefined, false, undefined, this)
73384
+ ]
73385
+ }, undefined, true, undefined, this);
73386
+ })(),
73325
73387
  currentTask && /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(Box_default, {
73326
73388
  gap: 1,
73327
73389
  marginTop: 0,
@@ -73385,11 +73447,11 @@ function AgentMode({ args, projectRoot, statesDir, tasksDir }) {
73385
73447
  }
73386
73448
 
73387
73449
  // packages/openspec/src/openspec-change-store.ts
73388
- import { join as join17, dirname as dirname5 } from "path";
73450
+ import { join as join18, dirname as dirname5 } from "path";
73389
73451
  import { readdir, mkdir as mkdir5 } from "fs/promises";
73390
73452
  function resolveOpenspecBin() {
73391
73453
  const pkgJsonPath = Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir);
73392
- return join17(dirname5(pkgJsonPath), "bin", "openspec.js");
73454
+ return join18(dirname5(pkgJsonPath), "bin", "openspec.js");
73393
73455
  }
73394
73456
  function runOpenspec(args, options = {}) {
73395
73457
  const stdio = options.inherit ? ["inherit", "inherit", "inherit"] : ["ignore", "pipe", "pipe"];
@@ -73415,7 +73477,7 @@ class OpenSpecChangeStore {
73415
73477
  }
73416
73478
  }
73417
73479
  getChangeDirectory(name) {
73418
- return join17("openspec", "changes", name);
73480
+ return join18("openspec", "changes", name);
73419
73481
  }
73420
73482
  async listChanges() {
73421
73483
  const result2 = runOpenspec(["list", "--json"]);
@@ -73429,7 +73491,7 @@ class OpenSpecChangeStore {
73429
73491
  }
73430
73492
  } catch {}
73431
73493
  }
73432
- const changesDir = join17("openspec", "changes");
73494
+ const changesDir = join18("openspec", "changes");
73433
73495
  if (!await Bun.file(changesDir).exists())
73434
73496
  return [];
73435
73497
  try {
@@ -73440,18 +73502,18 @@ class OpenSpecChangeStore {
73440
73502
  }
73441
73503
  }
73442
73504
  async readTaskList(name) {
73443
- const file = Bun.file(join17("openspec", "changes", name, "tasks.md"));
73505
+ const file = Bun.file(join18("openspec", "changes", name, "tasks.md"));
73444
73506
  if (!await file.exists())
73445
73507
  return "";
73446
73508
  return await file.text();
73447
73509
  }
73448
73510
  async writeTaskList(name, content) {
73449
- const path = join17("openspec", "changes", name, "tasks.md");
73511
+ const path = join18("openspec", "changes", name, "tasks.md");
73450
73512
  await mkdir5(dirname5(path), { recursive: true });
73451
73513
  await Bun.write(path, content);
73452
73514
  }
73453
73515
  async appendSteering(name, message) {
73454
- const path = join17("openspec", "changes", name, "steering.md");
73516
+ const path = join18("openspec", "changes", name, "steering.md");
73455
73517
  const file = Bun.file(path);
73456
73518
  const existing = await file.exists() ? await file.text() : null;
73457
73519
  const updated = existing ? `${message}
@@ -73462,7 +73524,7 @@ ${existing.trimStart()}` : `${message}
73462
73524
  await Bun.write(path, updated);
73463
73525
  }
73464
73526
  async readSection(name, artifact, heading) {
73465
- const file = Bun.file(join17("openspec", "changes", name, artifact));
73527
+ const file = Bun.file(join18("openspec", "changes", name, artifact));
73466
73528
  if (!await file.exists())
73467
73529
  return "";
73468
73530
  const content = await file.text();
@@ -73580,8 +73642,8 @@ function App2({ args, statesDir, tasksDir, projectRoot }) {
73580
73642
  message: "Error: --name is required for status mode"
73581
73643
  }, undefined, false, undefined, this);
73582
73644
  }
73583
- const stateDir = join18(statesDir, args.name);
73584
- if (getStorage().read(join18(stateDir, ".ralph-state.json")) === null) {
73645
+ const stateDir = join19(statesDir, args.name);
73646
+ if (getStorage().read(join19(stateDir, ".ralph-state.json")) === null) {
73585
73647
  return /* @__PURE__ */ jsx_dev_runtime10.jsxDEV(ErrorMessage, {
73586
73648
  message: `Error: change '${args.name}' not found`
73587
73649
  }, undefined, false, undefined, this);
@@ -73633,7 +73695,7 @@ if (typeof globalThis.Bun === "undefined") {
73633
73695
  async function findProjectRoot() {
73634
73696
  let dir = process.cwd();
73635
73697
  while (dir !== "/") {
73636
- if (await exists2(join20(dir, "openspec")))
73698
+ if (await exists2(join21(dir, "openspec")))
73637
73699
  return dir;
73638
73700
  dir = resolve2(dir, "..");
73639
73701
  }
@@ -73674,7 +73736,7 @@ try {
73674
73736
  const tasksDir = layout.tasksDir;
73675
73737
  if (args.mode === "init") {
73676
73738
  await mkdir7(statesDir, { recursive: true });
73677
- const openspecBin = join20(dirname6(Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir)), "bin", "openspec.js");
73739
+ const openspecBin = join21(dirname6(Bun.resolveSync("@fission-ai/openspec/package.json", import.meta.dir)), "bin", "openspec.js");
73678
73740
  Bun.spawnSync({
73679
73741
  cmd: [process.execPath, openspecBin, "init", "--tools", "none", "--force"],
73680
73742
  stdio: ["inherit", "inherit", "inherit"],
@@ -73687,9 +73749,9 @@ try {
73687
73749
  `);
73688
73750
  process.exit(1);
73689
73751
  }
73690
- const worktreeDir = join20(worktreesDir(projectRoot), args.name);
73691
- const changeDir = join20(tasksDir, args.name);
73692
- const stateDir = join20(statesDir, args.name);
73752
+ const worktreeDir = join21(worktreesDir(projectRoot), args.name);
73753
+ const changeDir = join21(tasksDir, args.name);
73754
+ const stateDir = join21(statesDir, args.name);
73693
73755
  const branch = `ralph/${args.name}`;
73694
73756
  const removed = [];
73695
73757
  if (await exists2(worktreeDir)) {
@@ -73741,13 +73803,13 @@ try {
73741
73803
  process.exit(0);
73742
73804
  }
73743
73805
  if (args.mode === "task" && args.name) {
73744
- await mkdir7(join20(statesDir, args.name), { recursive: true });
73745
- await mkdir7(join20(tasksDir, args.name), { recursive: true });
73806
+ await mkdir7(join21(statesDir, args.name), { recursive: true });
73807
+ await mkdir7(join21(tasksDir, args.name), { recursive: true });
73746
73808
  }
73747
73809
  if (args.mode === "agent") {
73748
73810
  await mkdir7(statesDir, { recursive: true });
73749
73811
  await mkdir7(tasksDir, { recursive: true });
73750
- await mkdir7(join20(projectRoot, ".ralph"), { recursive: true });
73812
+ await mkdir7(join21(projectRoot, ".ralph"), { recursive: true });
73751
73813
  }
73752
73814
  if (args.mode === "agent" && args.jsonOutput) {
73753
73815
  const { runAgentJson: runAgentJson2 } = await Promise.resolve().then(() => (init_json_runner(), exports_json_runner));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neriros/ralphy",
3
- "version": "2.16.4",
3
+ "version": "2.16.6",
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",