ralphctl 0.7.2 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -4,7 +4,7 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/application/ui/tui/launch.ts
7
- import React65 from "react";
7
+ import React66 from "react";
8
8
 
9
9
  // src/application/bootstrap/storage-paths.ts
10
10
  import { promises as fs } from "fs";
@@ -4360,7 +4360,7 @@ var createNpmVersionChecker = (deps) => {
4360
4360
  // package.json
4361
4361
  var package_default = {
4362
4362
  name: "ralphctl",
4363
- version: "0.7.2",
4363
+ version: "0.7.3",
4364
4364
  description: "Agent harness for long-running AI coding tasks \u2014 orchestrates Claude Code & GitHub Copilot across repositories",
4365
4365
  homepage: "https://github.com/lukas-grigis/ralphctl",
4366
4366
  type: "module",
@@ -5219,7 +5219,7 @@ var getRunInTerminal = () => (fn) => ref.current(fn);
5219
5219
 
5220
5220
  // src/application/ui/tui/App.tsx
5221
5221
  import "react";
5222
- import { Box as Box49 } from "ink";
5222
+ import { Box as Box50 } from "ink";
5223
5223
 
5224
5224
  // src/application/ui/tui/runtime/deps-context.tsx
5225
5225
  import { createContext, useContext } from "react";
@@ -6497,10 +6497,17 @@ var spinnerGlyph = (frame) => glyphs.spinner[frame % glyphs.spinner.length] ?? "
6497
6497
 
6498
6498
  // src/application/ui/tui/components/spinner.tsx
6499
6499
  import { jsxs as jsxs6 } from "react/jsx-runtime";
6500
- var Spinner = ({ label, color = inkColors.info }) => {
6501
- const frame = useSpinnerFrame();
6500
+ var Spinner = ({ label, color = inkColors.info, active = true, dim }) => {
6501
+ const frame = useSpinnerFrame(active);
6502
+ const glyph = spinnerGlyph(frame);
6503
+ if (dim === true) {
6504
+ return /* @__PURE__ */ jsxs6(Text7, { dimColor: true, children: [
6505
+ glyph,
6506
+ label !== void 0 && label.length > 0 ? ` ${label}` : ""
6507
+ ] });
6508
+ }
6502
6509
  return /* @__PURE__ */ jsxs6(Text7, { color, children: [
6503
- spinnerGlyph(frame),
6510
+ glyph,
6504
6511
  label !== void 0 && label.length > 0 ? ` ${label}` : ""
6505
6512
  ] });
6506
6513
  };
@@ -17819,20 +17826,20 @@ import { Box as Box34, Text as Text36, useInput as useInput15 } from "ink";
17819
17826
  import { useMemo as useMemo10 } from "react";
17820
17827
  import { Box as Box30, Text as Text32 } from "ink";
17821
17828
  import { jsx as jsx42, jsxs as jsxs31 } from "react/jsx-runtime";
17822
- var glyphFor = (status, frame) => {
17829
+ var glyphFor = (status) => {
17823
17830
  switch (status) {
17824
17831
  case "completed":
17825
- return [glyphs.phaseDone, inkColors.success];
17832
+ return { kind: "static", glyph: glyphs.phaseDone, color: inkColors.success };
17826
17833
  case "failed":
17827
- return [glyphs.cross, inkColors.error];
17834
+ return { kind: "static", glyph: glyphs.cross, color: inkColors.error };
17828
17835
  case "aborted":
17829
- return [glyphs.warningGlyph, inkColors.warning];
17836
+ return { kind: "static", glyph: glyphs.warningGlyph, color: inkColors.warning };
17830
17837
  case "skipped":
17831
- return [glyphs.phaseDisabled, inkColors.muted];
17838
+ return { kind: "static", glyph: glyphs.phaseDisabled, color: inkColors.muted };
17832
17839
  case "running":
17833
- return [spinnerGlyph(frame), inkColors.info];
17840
+ return { kind: "spinner", color: inkColors.info };
17834
17841
  case "pending":
17835
- return [glyphs.phasePending, inkColors.muted];
17842
+ return { kind: "static", glyph: glyphs.phasePending, color: inkColors.muted };
17836
17843
  }
17837
17844
  };
17838
17845
  var trailingLabelFor = (status) => {
@@ -17882,7 +17889,6 @@ var StepTrace = ({
17882
17889
  inFlightLabel,
17883
17890
  plan
17884
17891
  }) => {
17885
- const frame = useSpinnerFrame(running);
17886
17892
  const traceLastEntry = trace[trace.length - 1];
17887
17893
  const merged = useMemo10(
17888
17894
  () => plan !== void 0 ? mergePlanWithTrace(plan, trace, running) : traceToRows(trace),
@@ -17896,11 +17902,11 @@ var StepTrace = ({
17896
17902
  const rows = runningIdx >= 0 ? filtered.slice(Math.max(0, runningIdx - Math.floor(maxRows / 2))).slice(0, maxRows) : filtered.slice(-maxRows);
17897
17903
  return /* @__PURE__ */ jsxs31(Box30, { flexDirection: "column", children: [
17898
17904
  rows.map((row, i) => {
17899
- const [g, color] = glyphFor(row.status, frame);
17905
+ const instruction = glyphFor(row.status);
17900
17906
  const trailing = trailingLabelFor(row.status);
17901
17907
  const dimRow = row.status === "pending";
17902
17908
  return /* @__PURE__ */ jsxs31(Box30, { paddingX: spacing.indent, children: [
17903
- /* @__PURE__ */ jsx42(Text32, { color, bold: true, children: g }),
17909
+ instruction.kind === "spinner" ? /* @__PURE__ */ jsx42(Spinner, { active: running, color: instruction.color }) : /* @__PURE__ */ jsx42(Text32, { color: instruction.color, bold: true, children: instruction.glyph }),
17904
17910
  /* @__PURE__ */ jsxs31(Text32, { dimColor: dimRow, children: [
17905
17911
  " ",
17906
17912
  row.name
@@ -17911,7 +17917,7 @@ var StepTrace = ({
17911
17917
  " ",
17912
17918
  fmtDuration(row.durationMs)
17913
17919
  ] }),
17914
- trailing !== void 0 && /* @__PURE__ */ jsxs31(Text32, { color, children: [
17920
+ trailing !== void 0 && /* @__PURE__ */ jsxs31(Text32, { color: instruction.color, children: [
17915
17921
  " ",
17916
17922
  glyphs.emDash,
17917
17923
  " ",
@@ -17926,7 +17932,7 @@ var StepTrace = ({
17926
17932
  ] }, `${row.name}-${String(i)}`);
17927
17933
  }),
17928
17934
  plan === void 0 && running && inFlightLabel !== void 0 && /* @__PURE__ */ jsxs31(Box30, { paddingX: spacing.indent, children: [
17929
- /* @__PURE__ */ jsx42(Text32, { color: inkColors.info, bold: true, children: spinnerGlyph(frame) }),
17935
+ /* @__PURE__ */ jsx42(Spinner, { active: running, color: inkColors.info }),
17930
17936
  /* @__PURE__ */ jsxs31(Text32, { dimColor: true, children: [
17931
17937
  " ",
17932
17938
  inFlightLabel
@@ -18062,14 +18068,9 @@ var EvaluationLine = ({ evaluation }) => {
18062
18068
  evaluation.dimensions.length > 0 && /* @__PURE__ */ jsx43(Box31, { paddingLeft: 6, children: /* @__PURE__ */ jsx43(Text33, { dimColor: true, children: evaluation.dimensions.map((d) => `${d.dimension}: ${String(d.score)}/5 ${d.passed ? glyphs.check : glyphs.cross}`).join(` ${glyphs.bullet} `) }) })
18063
18069
  ] });
18064
18070
  };
18065
- var SubStepLine = ({
18066
- sub,
18067
- running,
18068
- spinner
18069
- }) => {
18071
+ var SubStepLine = ({ sub, running }) => {
18070
18072
  const presentation = SUB_STEP_PRESENTATION[sub.status];
18071
18073
  const glyph = running && sub.status === "completed" ? presentation.glyph : presentation.glyph;
18072
- void spinner;
18073
18074
  return /* @__PURE__ */ jsxs32(Box31, { children: [
18074
18075
  /* @__PURE__ */ jsxs32(Text33, { color: presentation.color, bold: true, children: [
18075
18076
  glyphs.activityArrow,
@@ -18098,15 +18099,20 @@ var TaskBlock = ({
18098
18099
  task,
18099
18100
  running,
18100
18101
  display,
18101
- maxSignals
18102
+ maxSignals,
18103
+ maxSubSteps,
18104
+ maxEvaluations
18102
18105
  }) => {
18103
18106
  const presentation = STATUS_PRESENTATION[task.status];
18104
- const frame = useSpinnerFrame(running && task.status === "running");
18105
- const glyph = task.status === "running" ? spinnerGlyph(frame) : presentation.glyph;
18107
+ const isSpinning = task.status === "running";
18106
18108
  const signalRows = task.signals.slice(-maxSignals);
18109
+ const subStepRows = task.subSteps.slice(-maxSubSteps);
18110
+ const subStepElided = task.subSteps.length - subStepRows.length;
18111
+ const evalRows = task.evaluations.slice(-maxEvaluations);
18112
+ const evalElided = task.evaluations.length - evalRows.length;
18107
18113
  return /* @__PURE__ */ jsxs32(Box31, { flexDirection: "column", marginBottom: spacing.section, children: [
18108
18114
  /* @__PURE__ */ jsxs32(Box31, { children: [
18109
- /* @__PURE__ */ jsx43(Text33, { color: presentation.color, bold: true, children: glyph }),
18115
+ isSpinning ? /* @__PURE__ */ jsx43(Spinner, { active: running, color: presentation.color }) : /* @__PURE__ */ jsx43(Text33, { color: presentation.color, bold: true, children: presentation.glyph }),
18110
18116
  /* @__PURE__ */ jsxs32(Text33, { bold: true, children: [
18111
18117
  " ",
18112
18118
  display
@@ -18132,8 +18138,14 @@ var TaskBlock = ({
18132
18138
  ] })
18133
18139
  ] }),
18134
18140
  task.errorMessage !== void 0 && /* @__PURE__ */ jsx43(Box31, { paddingLeft: 2, children: /* @__PURE__ */ jsx43(Text33, { color: inkColors.error, children: task.errorMessage }) }),
18135
- task.subSteps.length > 0 && /* @__PURE__ */ jsx43(Box31, { flexDirection: "column", paddingLeft: 2, children: task.subSteps.map((s, i) => /* @__PURE__ */ jsx43(SubStepLine, { sub: s, running, spinner: spinnerGlyph(frame) }, `${task.id}-sub-${String(i)}`)) }),
18136
- task.evaluations.length > 0 && /* @__PURE__ */ jsx43(Box31, { flexDirection: "column", paddingLeft: 2, marginTop: 1, children: task.evaluations.map((e, i) => /* @__PURE__ */ jsx43(EvaluationLine, { evaluation: e }, `${task.id}-eval-${String(i)}`)) }),
18141
+ subStepRows.length > 0 && /* @__PURE__ */ jsxs32(Box31, { flexDirection: "column", paddingLeft: 2, children: [
18142
+ subStepElided > 0 && /* @__PURE__ */ jsx43(Text33, { dimColor: true, children: `\u2026 ${String(subStepElided)} earlier sub-steps` }),
18143
+ subStepRows.map((s, i) => /* @__PURE__ */ jsx43(SubStepLine, { sub: s, running }, `${task.id}-sub-${String(i)}`))
18144
+ ] }),
18145
+ evalRows.length > 0 && /* @__PURE__ */ jsxs32(Box31, { flexDirection: "column", paddingLeft: 2, marginTop: 1, children: [
18146
+ evalElided > 0 && /* @__PURE__ */ jsx43(Text33, { dimColor: true, children: `\u2026 ${String(evalElided)} earlier evaluations` }),
18147
+ evalRows.map((e, i) => /* @__PURE__ */ jsx43(EvaluationLine, { evaluation: e }, `${task.id}-eval-${String(i)}`))
18148
+ ] }),
18137
18149
  signalRows.length > 0 && /* @__PURE__ */ jsxs32(Box31, { flexDirection: "column", paddingLeft: 2, marginTop: 1, children: [
18138
18150
  /* @__PURE__ */ jsx43(Text33, { dimColor: true, children: "signals" }),
18139
18151
  /* @__PURE__ */ jsx43(Box31, { flexDirection: "column", paddingLeft: 2, children: signalRows.map((s, i) => /* @__PURE__ */ jsx43(SignalLine, { signal: s }, `${task.id}-sig-${String(i)}`)) })
@@ -18159,7 +18171,9 @@ var TasksPanel = ({
18159
18171
  running,
18160
18172
  nameById,
18161
18173
  maxSignalsPerTask = 8,
18162
- maxOrphanSignals = 6
18174
+ maxOrphanSignals = 6,
18175
+ maxSubStepsPerTask = 12,
18176
+ maxEvaluationsPerTask = 6
18163
18177
  }) => {
18164
18178
  if (bucketed.tasks.length === 0 && bucketed.orphanSignals.length === 0) {
18165
18179
  return /* @__PURE__ */ jsx43(Box31, { paddingX: spacing.indent, children: /* @__PURE__ */ jsx43(Text33, { dimColor: true, children: "(no tasks yet)" }) });
@@ -18169,7 +18183,18 @@ var TasksPanel = ({
18169
18183
  /* @__PURE__ */ jsx43(OrphanSignals, { signals: bucketed.orphanSignals, max: maxOrphanSignals }),
18170
18184
  bucketed.tasks.map((task) => {
18171
18185
  const display = nameById?.get(task.id) ?? `${task.id.slice(0, 8)}\u2026`;
18172
- return /* @__PURE__ */ jsx43(TaskBlock, { task, running, display, maxSignals: maxSignalsPerTask }, task.id);
18186
+ return /* @__PURE__ */ jsx43(
18187
+ TaskBlock,
18188
+ {
18189
+ task,
18190
+ running,
18191
+ display,
18192
+ maxSignals: maxSignalsPerTask,
18193
+ maxSubSteps: maxSubStepsPerTask,
18194
+ maxEvaluations: maxEvaluationsPerTask
18195
+ },
18196
+ task.id
18197
+ );
18173
18198
  })
18174
18199
  ] });
18175
18200
  };
@@ -18480,7 +18505,6 @@ var ExecuteView = () => {
18480
18505
  filter: (e) => "chainId" in e && e.chainId === sessionId2,
18481
18506
  limit: 2e3
18482
18507
  });
18483
- const frame = useSpinnerFrame(session?.descriptor.status === "running");
18484
18508
  const term = useTerminalSize();
18485
18509
  const isRunning = session?.descriptor.status === "running";
18486
18510
  useViewHints(
@@ -18573,10 +18597,7 @@ var ExecuteView = () => {
18573
18597
  String(tasksTotal)
18574
18598
  ] })
18575
18599
  ] }),
18576
- isRunning && /* @__PURE__ */ jsx46(Box34, { marginLeft: 2, children: /* @__PURE__ */ jsxs35(Text36, { color: inkColors.info, children: [
18577
- spinnerGlyph(frame),
18578
- " live"
18579
- ] }) })
18600
+ isRunning && /* @__PURE__ */ jsx46(Box34, { marginLeft: 2, children: /* @__PURE__ */ jsx46(Spinner, { active: isRunning, color: inkColors.info, label: "live" }) })
18580
18601
  ] }),
18581
18602
  currentTask !== void 0 && currentTaskName !== void 0 && /* @__PURE__ */ jsxs35(Box34, { children: [
18582
18603
  /* @__PURE__ */ jsxs35(Text36, { dimColor: true, children: [
@@ -21120,8 +21141,40 @@ var useGlobalKeys = (opts = {}) => {
21120
21141
  });
21121
21142
  };
21122
21143
 
21144
+ // src/application/ui/tui/components/memory-pressure-banner.tsx
21145
+ import { useEffect as useEffect34, useState as useState41 } from "react";
21146
+ import { Box as Box49, Text as Text51 } from "ink";
21147
+ import { jsxs as jsxs50 } from "react/jsx-runtime";
21148
+ var formatMb = (bytes) => `${(bytes / 1048576).toFixed(0)} MB`;
21149
+ var formatPercent = (ratio) => `${Math.round(ratio * 100)}%`;
21150
+ var MemoryPressureBanner = () => {
21151
+ const deps = useDeps();
21152
+ const [latest, setLatest] = useState41(void 0);
21153
+ useEffect34(() => {
21154
+ const unsub = deps.eventBus.subscribe((event) => {
21155
+ if (event.type === "memory-pressure") setLatest(event);
21156
+ });
21157
+ return unsub;
21158
+ }, [deps.eventBus]);
21159
+ if (latest === void 0 || latest.severity === "recovered") return null;
21160
+ const tone = latest.severity === "critical" ? inkColors.error : inkColors.warning;
21161
+ const headline = latest.severity === "critical" ? `memory critical \u2014 ${formatPercent(latest.ratio)} of heap; auto-cleared in-memory buffers` : `memory pressure \u2014 ${formatPercent(latest.ratio)} of heap used; consider aborting and restarting`;
21162
+ const detail = `(${formatMb(latest.heapUsed)} / ${formatMb(latest.heapLimit)})`;
21163
+ return /* @__PURE__ */ jsxs50(Box49, { paddingX: spacing.indent, flexDirection: "row", children: [
21164
+ /* @__PURE__ */ jsxs50(Text51, { bold: true, color: tone, children: [
21165
+ glyphs.warningGlyph,
21166
+ " ",
21167
+ headline
21168
+ ] }),
21169
+ /* @__PURE__ */ jsxs50(Text51, { dimColor: true, children: [
21170
+ " ",
21171
+ detail
21172
+ ] })
21173
+ ] });
21174
+ };
21175
+
21123
21176
  // src/application/ui/tui/App.tsx
21124
- import { jsx as jsx62 } from "react/jsx-runtime";
21177
+ import { jsx as jsx62, jsxs as jsxs51 } from "react/jsx-runtime";
21125
21178
  var App = ({
21126
21179
  deps,
21127
21180
  storage: storage2,
@@ -21144,7 +21197,10 @@ var Layout = ({ children }) => {
21144
21197
  const ui = useUiState();
21145
21198
  const { rows } = useTerminalSize();
21146
21199
  useGlobalKeys({ disabled: ui.promptActive });
21147
- return /* @__PURE__ */ jsx62(Box49, { flexDirection: "column", height: rows, children });
21200
+ return /* @__PURE__ */ jsxs51(Box50, { flexDirection: "column", height: rows, children: [
21201
+ /* @__PURE__ */ jsx62(MemoryPressureBanner, {}),
21202
+ children
21203
+ ] });
21148
21204
  };
21149
21205
 
21150
21206
  // src/application/ui/tui/launch-routing.ts
@@ -21228,6 +21284,69 @@ var createLogLevelGate = (initial) => {
21228
21284
  };
21229
21285
  };
21230
21286
 
21287
+ // src/integration/observability/heap-watchdog.ts
21288
+ import * as v8 from "v8";
21289
+ var MIN_INTERVAL_MS = 1e3;
21290
+ var DEFAULT_INTERVAL_MS = 1e4;
21291
+ var DEFAULT_WARNING_RATIO = 0.8;
21292
+ var DEFAULT_CRITICAL_RATIO = 0.95;
21293
+ var defaultReadHeap = () => {
21294
+ const stats = v8.getHeapStatistics();
21295
+ return { heapUsed: stats.used_heap_size, heapLimit: stats.heap_size_limit };
21296
+ };
21297
+ var classify = (ratio, warning, critical) => {
21298
+ if (ratio >= critical) return "critical";
21299
+ if (ratio >= warning) return "warning";
21300
+ return "ok";
21301
+ };
21302
+ var startHeapWatchdog = (deps) => {
21303
+ const intervalMs = Math.max(MIN_INTERVAL_MS, deps.intervalMs ?? DEFAULT_INTERVAL_MS);
21304
+ const warning = deps.warningRatio ?? DEFAULT_WARNING_RATIO;
21305
+ const critical = deps.criticalRatio ?? DEFAULT_CRITICAL_RATIO;
21306
+ const clock = deps.clock ?? IsoTimestamp.now;
21307
+ const readHeap = deps.readHeap ?? defaultReadHeap;
21308
+ let stopped = false;
21309
+ let previousBand = "ok";
21310
+ const emit = (severity, reading, ratio) => {
21311
+ deps.eventBus.publish({
21312
+ type: "memory-pressure",
21313
+ severity,
21314
+ ratio,
21315
+ heapUsed: reading.heapUsed,
21316
+ heapLimit: reading.heapLimit,
21317
+ at: clock()
21318
+ });
21319
+ };
21320
+ const sample = () => {
21321
+ if (stopped) return;
21322
+ const reading = readHeap();
21323
+ const ratio = reading.heapLimit > 0 ? reading.heapUsed / reading.heapLimit : 0;
21324
+ const band = classify(ratio, warning, critical);
21325
+ if (band === previousBand) return;
21326
+ if (band === "warning") emit("warning", reading, ratio);
21327
+ else if (band === "critical") {
21328
+ emit("critical", reading, ratio);
21329
+ try {
21330
+ deps.onCritical?.();
21331
+ } catch (err) {
21332
+ console.warn("[heap-watchdog] onCritical threw:", err);
21333
+ }
21334
+ } else {
21335
+ emit("recovered", reading, ratio);
21336
+ }
21337
+ previousBand = band;
21338
+ };
21339
+ const handle = setInterval(sample, intervalMs);
21340
+ handle.unref?.();
21341
+ return {
21342
+ stop() {
21343
+ if (stopped) return;
21344
+ stopped = true;
21345
+ clearInterval(handle);
21346
+ }
21347
+ };
21348
+ };
21349
+
21231
21350
  // src/application/ui/tui/launch.ts
21232
21351
  var bootstrap = async () => {
21233
21352
  const paths = resolveStoragePaths();
@@ -21251,6 +21370,13 @@ var bootstrap = async () => {
21251
21370
  const unsubLogForward = deps.eventBus.subscribe((event) => {
21252
21371
  if (event.type === "log" && passesLogLevel(event.level, logLevelGate.get())) logBus.emit(event);
21253
21372
  });
21373
+ const heapWatchdog = startHeapWatchdog({
21374
+ eventBus: deps.eventBus,
21375
+ onCritical: () => {
21376
+ harnessBus.clear();
21377
+ logBus.clear();
21378
+ }
21379
+ });
21254
21380
  const sessions = createSessionManager();
21255
21381
  const queue = createPromptQueue();
21256
21382
  void createInkInteractivePrompt(queue);
@@ -21287,6 +21413,7 @@ var bootstrap = async () => {
21287
21413
  drain: () => {
21288
21414
  queue.drain(new Error("TUI shutting down"));
21289
21415
  unsubLogForward();
21416
+ heapWatchdog.stop();
21290
21417
  }
21291
21418
  };
21292
21419
  };
@@ -21301,7 +21428,7 @@ var launchTui = async () => {
21301
21428
  process.exitCode = 1;
21302
21429
  return;
21303
21430
  }
21304
- const appElement = React65.createElement(App, booted.app);
21431
+ const appElement = React66.createElement(App, booted.app);
21305
21432
  const host = createInkHost({ appElement });
21306
21433
  setRunInTerminal(host.runInTerminal);
21307
21434
  try {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "generatedAt": "2026-05-19T19:29:09.887Z",
3
+ "generatedAt": "2026-05-20T06:49:10.467Z",
4
4
  "assets": [
5
5
  "prompts/_partials/harness-context.md",
6
6
  "prompts/_partials/signals-evaluation.md",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralphctl",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "Agent harness for long-running AI coding tasks — orchestrates Claude Code & GitHub Copilot across repositories",
5
5
  "homepage": "https://github.com/lukas-grigis/ralphctl",
6
6
  "type": "module",