ralphctl 0.4.4 → 0.4.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.
@@ -1626,6 +1626,14 @@ function resolveCheckScriptForRepo(repo) {
1626
1626
  }
1627
1627
 
1628
1628
  // src/business/pipelines/steps/run-check-scripts.ts
1629
+ var ERROR_OUTPUT_TAIL_LINES = 100;
1630
+ function tailOutput(output, maxLines = ERROR_OUTPUT_TAIL_LINES) {
1631
+ const lines = output.split("\n");
1632
+ if (lines.length <= maxLines) return output;
1633
+ const hidden = lines.length - maxLines;
1634
+ return `[${String(hidden)} earlier line${hidden !== 1 ? "s" : ""} omitted]
1635
+ ${lines.slice(-maxLines).join("\n")}`;
1636
+ }
1629
1637
  function runCheckScriptsStep(external, persistence, mode, options) {
1630
1638
  return step("run-check-scripts", async (ctx) => {
1631
1639
  const sprint = ctx.sprint;
@@ -1646,15 +1654,17 @@ function runCheckScriptsStep(external, persistence, mode, options) {
1646
1654
  const checkScript = resolveCheckScriptForRepo(resolved?.repo);
1647
1655
  if (!resolved || !checkScript) continue;
1648
1656
  const { repo } = resolved;
1649
- const result = external.runCheckScript(repo.path, checkScript, "sprintStart", repo.checkTimeout);
1657
+ const result = await external.runCheckScript(repo.path, checkScript, "sprintStart", repo.checkTimeout);
1650
1658
  if (!result.passed) {
1651
1659
  checkResults[repoId] = {
1652
1660
  projectPath: repo.path,
1653
1661
  success: false,
1654
1662
  output: result.output
1655
1663
  };
1656
- return Result.error(new StorageError(`Check failed for ${repo.path}: ${checkScript}
1657
- ${result.output}`));
1664
+ return Result.error(
1665
+ new StorageError(`Check failed for ${repo.path}: ${checkScript}
1666
+ ${tailOutput(result.output)}`)
1667
+ );
1658
1668
  }
1659
1669
  const ranAt = (/* @__PURE__ */ new Date()).toISOString();
1660
1670
  sprint.checkRanAt[repoId] = ranAt;
@@ -1680,7 +1690,7 @@ ${result.output}`));
1680
1690
  return Result.ok(partial2);
1681
1691
  }
1682
1692
  const { repo } = resolved;
1683
- const result = external.runCheckScript(repo.path, checkScript, "taskComplete", repo.checkTimeout);
1693
+ const result = await external.runCheckScript(repo.path, checkScript, "taskComplete", repo.checkTimeout);
1684
1694
  checkResults[targetRepoId] = {
1685
1695
  projectPath: repo.path,
1686
1696
  success: result.passed,
@@ -1689,7 +1699,7 @@ ${result.output}`));
1689
1699
  if (!result.passed) {
1690
1700
  return Result.error(
1691
1701
  new StorageError(`Post-task check failed for ${repo.path}: ${checkScript}
1692
- ${result.output}`)
1702
+ ${tailOutput(result.output)}`)
1693
1703
  );
1694
1704
  }
1695
1705
  }
@@ -1892,7 +1902,7 @@ ${instructions}`;
1892
1902
  if (!resolved || !checkScript) return true;
1893
1903
  this.logger.info(`Running post-task check: ${checkScript}`);
1894
1904
  const { repo } = resolved;
1895
- const result = this.external.runCheckScript(repo.path, checkScript, "taskComplete", repo.checkTimeout);
1905
+ const result = await this.external.runCheckScript(repo.path, checkScript, "taskComplete", repo.checkTimeout);
1896
1906
  if (result.passed) {
1897
1907
  this.logger.success("Post-task check: passed");
1898
1908
  }
@@ -4608,8 +4618,9 @@ function describeMcpHint(name) {
4608
4618
  }
4609
4619
 
4610
4620
  // src/integration/external/lifecycle.ts
4611
- import { spawnSync } from "child_process";
4621
+ import { spawn } from "child_process";
4612
4622
  var DEFAULT_HOOK_TIMEOUT_MS = 5 * 60 * 1e3;
4623
+ var MAX_OUTPUT_BYTES = 50 * 1024 * 1024;
4613
4624
  function getHookTimeoutMs() {
4614
4625
  const envVal = process.env["RALPHCTL_SETUP_TIMEOUT_MS"];
4615
4626
  if (envVal) {
@@ -4621,16 +4632,76 @@ function getHookTimeoutMs() {
4621
4632
  function runLifecycleHook(projectPath, script, event, timeoutOverrideMs) {
4622
4633
  assertSafeCwd(projectPath);
4623
4634
  const timeoutMs = timeoutOverrideMs ?? getHookTimeoutMs();
4624
- const result = spawnSync(script, {
4625
- cwd: projectPath,
4626
- shell: true,
4627
- stdio: ["pipe", "pipe", "pipe"],
4628
- encoding: "utf-8",
4629
- timeout: timeoutMs,
4630
- env: { ...process.env, RALPHCTL_LIFECYCLE_EVENT: event }
4635
+ return new Promise((resolve) => {
4636
+ const child = spawn(script, {
4637
+ cwd: projectPath,
4638
+ shell: true,
4639
+ detached: process.platform !== "win32",
4640
+ stdio: ["pipe", "pipe", "pipe"],
4641
+ env: { ...process.env, RALPHCTL_LIFECYCLE_EVENT: event }
4642
+ });
4643
+ const killTree = () => {
4644
+ if (process.platform !== "win32" && typeof child.pid === "number") {
4645
+ try {
4646
+ process.kill(-child.pid, "SIGTERM");
4647
+ return;
4648
+ } catch {
4649
+ }
4650
+ }
4651
+ try {
4652
+ child.kill("SIGTERM");
4653
+ } catch {
4654
+ }
4655
+ };
4656
+ const chunks = [];
4657
+ let totalBytes = 0;
4658
+ let timedOut = false;
4659
+ let capExceeded = false;
4660
+ let settled = false;
4661
+ const appendChunk = (chunk) => {
4662
+ if (capExceeded) return;
4663
+ totalBytes += chunk.length;
4664
+ if (totalBytes > MAX_OUTPUT_BYTES) {
4665
+ capExceeded = true;
4666
+ killTree();
4667
+ return;
4668
+ }
4669
+ chunks.push(chunk);
4670
+ };
4671
+ child.stdout.on("data", (chunk) => {
4672
+ appendChunk(chunk);
4673
+ });
4674
+ child.stderr.on("data", (chunk) => {
4675
+ appendChunk(chunk);
4676
+ });
4677
+ const timer = setTimeout(() => {
4678
+ timedOut = true;
4679
+ killTree();
4680
+ }, timeoutMs);
4681
+ const finish = (passed, suffix) => {
4682
+ if (settled) return;
4683
+ settled = true;
4684
+ clearTimeout(timer);
4685
+ const base = Buffer.concat(chunks).toString("utf-8").trim();
4686
+ const output = suffix ? base ? `${base}
4687
+ ${suffix}` : suffix : base;
4688
+ resolve({ passed, output });
4689
+ };
4690
+ child.on("error", (err) => {
4691
+ finish(false, `[spawn error: ${err.message}]`);
4692
+ });
4693
+ child.on("close", (code) => {
4694
+ if (timedOut) {
4695
+ finish(false, `[timeout exceeded after ${String(timeoutMs)}ms]`);
4696
+ return;
4697
+ }
4698
+ if (capExceeded) {
4699
+ finish(false, `[output exceeded ${String(MAX_OUTPUT_BYTES)} byte cap \u2014 truncated]`);
4700
+ return;
4701
+ }
4702
+ finish(code === 0);
4703
+ });
4631
4704
  });
4632
- const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
4633
- return { passed: result.status === 0, output };
4634
4705
  }
4635
4706
 
4636
4707
  // src/integration/ai/task-context.ts
@@ -4650,7 +4721,7 @@ function getRecentGitHistory(projectPath, count = 20) {
4650
4721
  }
4651
4722
 
4652
4723
  // src/integration/external/git.ts
4653
- import { spawnSync as spawnSync2 } from "child_process";
4724
+ import { spawnSync } from "child_process";
4654
4725
  var BRANCH_NAME_RE = /^[a-zA-Z0-9/_.-]+$/;
4655
4726
  var BRANCH_NAME_INVALID_PATTERNS = [/\.\./, /\.$/, /\/$/, /\.lock$/, /^-/, /\/\//];
4656
4727
  function isValidBranchName(name) {
@@ -4663,7 +4734,7 @@ function isValidBranchName(name) {
4663
4734
  }
4664
4735
  function getCurrentBranch(cwd) {
4665
4736
  assertSafeCwd(cwd);
4666
- const result = spawnSync2("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
4737
+ const result = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
4667
4738
  cwd,
4668
4739
  encoding: "utf-8",
4669
4740
  stdio: ["pipe", "pipe", "pipe"]
@@ -4678,7 +4749,7 @@ function branchExists(cwd, name) {
4678
4749
  if (!isValidBranchName(name)) {
4679
4750
  throw new Error(`Invalid branch name: ${name}`);
4680
4751
  }
4681
- const result = spawnSync2("git", ["show-ref", "--verify", `refs/heads/${name}`], {
4752
+ const result = spawnSync("git", ["show-ref", "--verify", `refs/heads/${name}`], {
4682
4753
  cwd,
4683
4754
  encoding: "utf-8",
4684
4755
  stdio: ["pipe", "pipe", "pipe"]
@@ -4695,7 +4766,7 @@ function createAndCheckoutBranch(cwd, name) {
4695
4766
  return;
4696
4767
  }
4697
4768
  if (branchExists(cwd, name)) {
4698
- const result = spawnSync2("git", ["checkout", name], {
4769
+ const result = spawnSync("git", ["checkout", name], {
4699
4770
  cwd,
4700
4771
  encoding: "utf-8",
4701
4772
  stdio: ["pipe", "pipe", "pipe"]
@@ -4704,7 +4775,7 @@ function createAndCheckoutBranch(cwd, name) {
4704
4775
  throw new Error(`Failed to checkout branch '${name}' in ${cwd}: ${result.stderr.trim()}`);
4705
4776
  }
4706
4777
  } else {
4707
- const result = spawnSync2("git", ["checkout", "-b", name], {
4778
+ const result = spawnSync("git", ["checkout", "-b", name], {
4708
4779
  cwd,
4709
4780
  encoding: "utf-8",
4710
4781
  stdio: ["pipe", "pipe", "pipe"]
@@ -4720,7 +4791,7 @@ function verifyCurrentBranch(cwd, expected) {
4720
4791
  }
4721
4792
  function getDefaultBranch(cwd) {
4722
4793
  assertSafeCwd(cwd);
4723
- const result = spawnSync2("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
4794
+ const result = spawnSync("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
4724
4795
  cwd,
4725
4796
  encoding: "utf-8",
4726
4797
  stdio: ["pipe", "pipe", "pipe"]
@@ -4741,7 +4812,7 @@ function getDefaultBranch(cwd) {
4741
4812
  function getHeadSha(cwd) {
4742
4813
  try {
4743
4814
  assertSafeCwd(cwd);
4744
- const result = spawnSync2("git", ["rev-parse", "HEAD"], {
4815
+ const result = spawnSync("git", ["rev-parse", "HEAD"], {
4745
4816
  cwd,
4746
4817
  encoding: "utf-8",
4747
4818
  stdio: ["pipe", "pipe", "pipe"]
@@ -4754,7 +4825,7 @@ function getHeadSha(cwd) {
4754
4825
  }
4755
4826
  function hasUncommittedChanges(cwd) {
4756
4827
  assertSafeCwd(cwd);
4757
- const result = spawnSync2("git", ["status", "--porcelain"], {
4828
+ const result = spawnSync("git", ["status", "--porcelain"], {
4758
4829
  cwd,
4759
4830
  encoding: "utf-8",
4760
4831
  stdio: ["pipe", "pipe", "pipe"]
@@ -4766,7 +4837,7 @@ function hasUncommittedChanges(cwd) {
4766
4837
  }
4767
4838
  function hardResetWorkingTree(cwd) {
4768
4839
  assertSafeCwd(cwd);
4769
- const reset = spawnSync2("git", ["reset", "--hard", "HEAD"], {
4840
+ const reset = spawnSync("git", ["reset", "--hard", "HEAD"], {
4770
4841
  cwd,
4771
4842
  encoding: "utf-8",
4772
4843
  stdio: ["pipe", "pipe", "pipe"]
@@ -4774,7 +4845,7 @@ function hardResetWorkingTree(cwd) {
4774
4845
  if (reset.status !== 0) {
4775
4846
  throw new StorageError(`Failed to reset working tree in ${cwd}: ${reset.stderr.trim() || reset.stdout.trim()}`);
4776
4847
  }
4777
- const clean = spawnSync2("git", ["clean", "-fd"], {
4848
+ const clean = spawnSync("git", ["clean", "-fd"], {
4778
4849
  cwd,
4779
4850
  encoding: "utf-8",
4780
4851
  stdio: ["pipe", "pipe", "pipe"]
@@ -4785,7 +4856,7 @@ function hardResetWorkingTree(cwd) {
4785
4856
  }
4786
4857
  function autoCommit(cwd, message) {
4787
4858
  assertSafeCwd(cwd);
4788
- const add = spawnSync2("git", ["add", "-A"], {
4859
+ const add = spawnSync("git", ["add", "-A"], {
4789
4860
  cwd,
4790
4861
  encoding: "utf-8",
4791
4862
  stdio: ["pipe", "pipe", "pipe"]
@@ -4793,7 +4864,7 @@ function autoCommit(cwd, message) {
4793
4864
  if (add.status !== 0) {
4794
4865
  throw new Error(`Failed to stage changes in ${cwd}: ${add.stderr.trim()}`);
4795
4866
  }
4796
- const commit = spawnSync2("git", ["commit", "-m", message], {
4867
+ const commit = spawnSync("git", ["commit", "-m", message], {
4797
4868
  cwd,
4798
4869
  encoding: "utf-8",
4799
4870
  stdio: ["pipe", "pipe", "pipe"]
@@ -4806,14 +4877,14 @@ function generateBranchName(sprintId) {
4806
4877
  return `ralphctl/${sprintId}`;
4807
4878
  }
4808
4879
  function isGhAvailable() {
4809
- const result = spawnSync2("gh", ["--version"], {
4880
+ const result = spawnSync("gh", ["--version"], {
4810
4881
  encoding: "utf-8",
4811
4882
  stdio: ["pipe", "pipe", "pipe"]
4812
4883
  });
4813
4884
  return result.status === 0;
4814
4885
  }
4815
4886
  function isGlabAvailable() {
4816
- const result = spawnSync2("glab", ["--version"], {
4887
+ const result = spawnSync("glab", ["--version"], {
4817
4888
  encoding: "utf-8",
4818
4889
  stdio: ["pipe", "pipe", "pipe"]
4819
4890
  });
@@ -41,7 +41,7 @@ import {
41
41
  updateTask,
42
42
  updateTaskStatus,
43
43
  validateImportTasks
44
- } from "./chunk-OFILN7QL.mjs";
44
+ } from "./chunk-B3RCOHW3.mjs";
45
45
  import {
46
46
  SignalParser,
47
47
  buildTicketRefinePrompt,
@@ -176,6 +176,7 @@ import {
176
176
  ProjectNotFoundError,
177
177
  SprintNotFoundError,
178
178
  SprintStatusError,
179
+ StepError,
179
180
  StorageError,
180
181
  TaskNotFoundError,
181
182
  TicketNotFoundError
@@ -184,7 +185,7 @@ import {
184
185
  // package.json
185
186
  var package_default = {
186
187
  name: "ralphctl",
187
- version: "0.4.4",
188
+ version: "0.4.6",
188
189
  description: "Agent harness for long-running AI coding tasks \u2014 orchestrates Claude Code & GitHub Copilot across repositories",
189
190
  homepage: "https://github.com/lukas-grigis/ralphctl",
190
191
  type: "module",
@@ -2068,12 +2069,20 @@ var InMemoryExecutionRegistry = class {
2068
2069
  this.transition(executionId, "completed", summary ?? void 0);
2069
2070
  }
2070
2071
  } catch (err) {
2071
- void err;
2072
2072
  if (abortSignal.aborted) {
2073
2073
  this.transition(executionId, "cancelled");
2074
- } else {
2075
- this.transition(executionId, "failed");
2074
+ return;
2076
2075
  }
2076
+ const errInfo = err instanceof StepError ? { message: err.message, stepName: err.stepName } : { message: err instanceof Error ? err.message : String(err) };
2077
+ const entry = this.entries.get(executionId);
2078
+ entry?.logEventBus.emit({
2079
+ kind: "log",
2080
+ level: "error",
2081
+ message: errInfo.stepName ? `[${errInfo.stepName}] ${errInfo.message}` : errInfo.message,
2082
+ context: {},
2083
+ timestamp: /* @__PURE__ */ new Date()
2084
+ });
2085
+ this.transition(executionId, "failed", void 0, errInfo);
2077
2086
  }
2078
2087
  }
2079
2088
  get(id) {
@@ -2100,7 +2109,7 @@ var InMemoryExecutionRegistry = class {
2100
2109
  getLogEventBus(id) {
2101
2110
  return this.entries.get(id)?.logEventBus ?? null;
2102
2111
  }
2103
- transition(executionId, status, summary) {
2112
+ transition(executionId, status, summary, error2) {
2104
2113
  const entry = this.entries.get(executionId);
2105
2114
  if (!entry) return;
2106
2115
  if (entry.execution.status === status) return;
@@ -2108,7 +2117,8 @@ var InMemoryExecutionRegistry = class {
2108
2117
  ...entry.execution,
2109
2118
  status,
2110
2119
  endedAt: /* @__PURE__ */ new Date(),
2111
- summary: summary ?? entry.execution.summary
2120
+ summary: summary ?? entry.execution.summary,
2121
+ error: error2 ?? entry.execution.error
2112
2122
  };
2113
2123
  entry.execution = next;
2114
2124
  this.notify(next);
package/dist/cli.mjs CHANGED
@@ -41,7 +41,7 @@ import {
41
41
  ticketRefineCommand,
42
42
  ticketRemoveCommand,
43
43
  ticketShowCommand
44
- } from "./chunk-ACRMBVEE.mjs";
44
+ } from "./chunk-O566EEDL.mjs";
45
45
  import {
46
46
  projectAddCommand
47
47
  } from "./chunk-PYZEQ2VK.mjs";
@@ -56,7 +56,7 @@ import {
56
56
  executePipeline,
57
57
  getTasks,
58
58
  sprintStartCommand
59
- } from "./chunk-OFILN7QL.mjs";
59
+ } from "./chunk-B3RCOHW3.mjs";
60
60
  import "./chunk-ZLWSPLWI.mjs";
61
61
  import {
62
62
  truncate
@@ -756,7 +756,7 @@ async function main() {
756
756
  const isBare = argv.length <= 2;
757
757
  const isInteractive = argv[2] === "interactive";
758
758
  if (isBare || isInteractive) {
759
- const { mountInkApp } = await import("./mount-VEV3TESX.mjs");
759
+ const { mountInkApp } = await import("./mount-B3MLHNVY.mjs");
760
760
  const { fallback } = await mountInkApp({ initialView: "repl" });
761
761
  if (!fallback) return;
762
762
  printBanner();
@@ -767,10 +767,10 @@ async function main() {
767
767
  return;
768
768
  }
769
769
  if (argv[2] === "sprint" && argv[3] === "start") {
770
- const { parseSprintStartArgs } = await import("./start-2WH4BTDB.mjs");
770
+ const { parseSprintStartArgs } = await import("./start-FP7MVN5P.mjs");
771
771
  const parsed = parseSprintStartArgs(argv.slice(4));
772
772
  if (parsed.ok) {
773
- const { mountInkApp } = await import("./mount-VEV3TESX.mjs");
773
+ const { mountInkApp } = await import("./mount-B3MLHNVY.mjs");
774
774
  const { getSharedDeps: getSharedDeps2 } = await import("./bootstrap-FMHG6DRY.mjs");
775
775
  let sprintId;
776
776
  try {
@@ -62,7 +62,7 @@ import {
62
62
  ticketRemoveCommand,
63
63
  ticketShowCommand,
64
64
  useCurrentPrompt
65
- } from "./chunk-ACRMBVEE.mjs";
65
+ } from "./chunk-O566EEDL.mjs";
66
66
  import {
67
67
  PromptCancelledError,
68
68
  detectCheckScriptCandidates,
@@ -99,7 +99,7 @@ import {
99
99
  reorderTask,
100
100
  sprintStartCommand,
101
101
  updateTaskStatus
102
- } from "./chunk-OFILN7QL.mjs";
102
+ } from "./chunk-B3RCOHW3.mjs";
103
103
  import {
104
104
  ProviderAiSessionAdapter,
105
105
  SignalParser,
@@ -189,7 +189,7 @@ import { render } from "ink";
189
189
 
190
190
  // src/integration/ui/tui/views/app.tsx
191
191
  import { useEffect as useEffect37, useState as useState38 } from "react";
192
- import { Box as Box37, useStdout } from "ink";
192
+ import { Box as Box37, useStdout as useStdout2 } from "ink";
193
193
 
194
194
  // src/integration/ui/tui/views/view-router.tsx
195
195
  import { useCallback as useCallback12, useMemo as useMemo34, useState as useState37 } from "react";
@@ -1606,7 +1606,7 @@ function SettingsView() {
1606
1606
 
1607
1607
  // src/integration/ui/tui/views/execute-view.tsx
1608
1608
  import { useEffect as useEffect8, useMemo as useMemo6, useRef as useRef2, useState as useState8 } from "react";
1609
- import { Box as Box19, Text as Text18, useInput as useInput6 } from "ink";
1609
+ import { Box as Box19, Text as Text18, useInput as useInput6, useStdout } from "ink";
1610
1610
 
1611
1611
  // src/integration/ui/tui/runtime/hooks.ts
1612
1612
  import { useCallback as useCallback4, useEffect as useEffect6, useRef, useState as useState6 } from "react";
@@ -1908,8 +1908,12 @@ function renderLine(event, index, isActiveSpinner) {
1908
1908
  }
1909
1909
  }
1910
1910
  }
1911
- function LogTail({ events, limit = 8 }) {
1912
- const tail = events.slice(-limit);
1911
+ function LogTail({ events, visibleLines = 8, scrollOffset = 0 }) {
1912
+ const maxOffset = Math.max(0, events.length - visibleLines);
1913
+ const clampedOffset = Math.min(Math.max(0, scrollOffset), maxOffset);
1914
+ const end = events.length - clampedOffset;
1915
+ const start = Math.max(0, end - visibleLines);
1916
+ const window = events.slice(start, end);
1913
1917
  const resolvedIds = useMemo5(() => {
1914
1918
  const ids = /* @__PURE__ */ new Set();
1915
1919
  for (const ev of events) {
@@ -1919,9 +1923,29 @@ function LogTail({ events, limit = 8 }) {
1919
1923
  }
1920
1924
  return ids;
1921
1925
  }, [events]);
1926
+ const scrolledUp = clampedOffset > 0;
1927
+ const hiddenAbove = start;
1928
+ const hiddenBelow = events.length - end;
1922
1929
  return /* @__PURE__ */ jsxs14(Box15, { flexDirection: "column", children: [
1923
- /* @__PURE__ */ jsx17(Text14, { dimColor: true, children: "\u2500\u2500 Log \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }),
1924
- tail.length === 0 ? /* @__PURE__ */ jsx17(Text14, { dimColor: true, children: "(no activity yet)" }) : tail.map((event, i) => {
1930
+ /* @__PURE__ */ jsxs14(Box15, { children: [
1931
+ /* @__PURE__ */ jsx17(Text14, { dimColor: true, children: "\u2500\u2500 Log \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }),
1932
+ scrolledUp ? /* @__PURE__ */ jsxs14(Text14, { dimColor: true, children: [
1933
+ " ",
1934
+ glyphs.arrowRight,
1935
+ " ",
1936
+ hiddenAbove,
1937
+ " line",
1938
+ hiddenAbove !== 1 ? "s" : "",
1939
+ " above",
1940
+ hiddenBelow > 0 ? ` \xB7 ${String(hiddenBelow)} below` : ""
1941
+ ] }) : events.length > visibleLines ? /* @__PURE__ */ jsxs14(Text14, { dimColor: true, children: [
1942
+ " ",
1943
+ "(",
1944
+ events.length - visibleLines,
1945
+ " hidden)"
1946
+ ] }) : null
1947
+ ] }),
1948
+ window.length === 0 ? /* @__PURE__ */ jsx17(Text14, { dimColor: true, children: "(no activity yet)" }) : window.map((event, i) => {
1925
1949
  const active = event.kind === "spinner-start" && !resolvedIds.has(event.id);
1926
1950
  return renderLine(event, i, active);
1927
1951
  })
@@ -1998,9 +2022,13 @@ function ResultCard({ kind, title, fields, nextSteps, lines }) {
1998
2022
  }
1999
2023
 
2000
2024
  // src/integration/ui/tui/views/execute-view.tsx
2001
- import { jsx as jsx20, jsxs as jsxs18 } from "react/jsx-runtime";
2025
+ import { Fragment, jsx as jsx20, jsxs as jsxs18 } from "react/jsx-runtime";
2002
2026
  var EXECUTE_HINTS_RUNNING = [{ key: "c", action: "cancel" }];
2003
2027
  var EXECUTE_HINTS_TERMINAL = [{ key: "Enter", action: "back" }];
2028
+ var LOG_TAIL_FIXED_OVERHEAD = 20;
2029
+ var LOG_TAIL_MIN_LINES = 3;
2030
+ var LOG_TAIL_DEFAULT_LINES = 8;
2031
+ var ERROR_MESSAGE_MAX_LINES = 20;
2004
2032
  function initialState() {
2005
2033
  return {
2006
2034
  sprint: null,
@@ -2026,6 +2054,7 @@ function ExecuteView({ sprintId, executionId, executionOptions }) {
2026
2054
  const registryEvents = useRegistryEvents(registry);
2027
2055
  const [attach, setAttach] = useState8(initialAttach);
2028
2056
  const [state, setState] = useState8(initialState);
2057
+ const { stdout } = useStdout();
2029
2058
  const processedCountRef = useRef2(0);
2030
2059
  const startedRef = useRef2(false);
2031
2060
  const attachedId = attach.execution?.id ?? null;
@@ -2122,6 +2151,7 @@ function ExecuteView({ sprintId, executionId, executionOptions }) {
2122
2151
  })();
2123
2152
  }
2124
2153
  }, [signalEvents, shared, sprintId]);
2154
+ const logVisibleLines = stdout.rows ? Math.max(LOG_TAIL_MIN_LINES, stdout.rows - LOG_TAIL_FIXED_OVERHEAD) : LOG_TAIL_DEFAULT_LINES;
2125
2155
  const status = liveExecution?.status ?? "running";
2126
2156
  const terminal = status === "completed" || status === "failed" || status === "cancelled";
2127
2157
  const [closePromptRun, setClosePromptRun] = useState8(false);
@@ -2190,6 +2220,7 @@ function ExecuteView({ sprintId, executionId, executionOptions }) {
2190
2220
  if (attach.kind === "attaching") {
2191
2221
  return /* @__PURE__ */ jsx20(ViewShell, { title: "Execute", children: /* @__PURE__ */ jsx20(Spinner, { label: "Attaching to execution\u2026" }) });
2192
2222
  }
2223
+ const errorCard = terminal && liveExecution?.status === "failed" && liveExecution.error ? buildErrorCard(liveExecution.error) : null;
2193
2224
  return /* @__PURE__ */ jsxs18(ViewShell, { title: "Execute", children: [
2194
2225
  /* @__PURE__ */ jsxs18(Box19, { children: [
2195
2226
  /* @__PURE__ */ jsx20(Text18, { bold: true, color: inkColors.primary, children: state.sprint?.name ?? "Sprint" }),
@@ -2216,8 +2247,8 @@ function ExecuteView({ sprintId, executionId, executionOptions }) {
2216
2247
  const taskName = task?.name ?? taskId.slice(0, 8);
2217
2248
  return /* @__PURE__ */ jsx20(Spinner, { label: `${taskName} ${glyphs.emDash} ${label}` }, taskId);
2218
2249
  }) }) : null,
2219
- /* @__PURE__ */ jsx20(Box19, { marginTop: spacing.section, children: /* @__PURE__ */ jsx20(LogTail, { events: logEvents }) }),
2220
- terminal && liveExecution ? /* @__PURE__ */ jsxs18(Box19, { marginTop: spacing.section, flexDirection: "column", children: [
2250
+ /* @__PURE__ */ jsx20(Box19, { marginTop: spacing.section, children: /* @__PURE__ */ jsx20(LogTail, { events: logEvents, visibleLines: logVisibleLines, scrollOffset: 0 }) }),
2251
+ terminal && liveExecution ? /* @__PURE__ */ jsx20(Box19, { marginTop: spacing.section, flexDirection: "column", children: errorCard ? /* @__PURE__ */ jsx20(ResultCard, { kind: "error", title: "Execution failed", fields: errorCard.fields, lines: errorCard.lines }) : /* @__PURE__ */ jsxs18(Fragment, { children: [
2221
2252
  /* @__PURE__ */ jsxs18(Text18, { color: terminalColor(liveExecution.status), bold: true, children: [
2222
2253
  terminalGlyph(liveExecution.status),
2223
2254
  " Execution ",
@@ -2229,8 +2260,8 @@ function ExecuteView({ sprintId, executionId, executionOptions }) {
2229
2260
  glyphs.inlineDot,
2230
2261
  " ",
2231
2262
  liveExecution.summary.remaining,
2232
- " remaining",
2233
2263
  " ",
2264
+ "remaining ",
2234
2265
  glyphs.inlineDot,
2235
2266
  " ",
2236
2267
  liveExecution.summary.blocked,
@@ -2239,7 +2270,7 @@ function ExecuteView({ sprintId, executionId, executionOptions }) {
2239
2270
  liveExecution.summary.stopReason,
2240
2271
  ")"
2241
2272
  ] }) : null
2242
- ] }) : null
2273
+ ] }) }) : null
2243
2274
  ] });
2244
2275
  }
2245
2276
  function terminalColor(status) {
@@ -2252,6 +2283,19 @@ function terminalGlyph(status) {
2252
2283
  if (status === "failed") return glyphs.cross;
2253
2284
  return glyphs.warningGlyph;
2254
2285
  }
2286
+ function buildErrorCard(error) {
2287
+ const fields = error.stepName ? [["Step", error.stepName]] : void 0;
2288
+ const rawLines = error.message.split("\n");
2289
+ if (rawLines.length <= ERROR_MESSAGE_MAX_LINES) {
2290
+ return { fields, lines: rawLines };
2291
+ }
2292
+ const hidden = rawLines.length - ERROR_MESSAGE_MAX_LINES;
2293
+ const visibleLines = [
2294
+ `(${String(hidden)} earlier line${hidden !== 1 ? "s" : ""} omitted)`,
2295
+ ...rawLines.slice(-ERROR_MESSAGE_MAX_LINES)
2296
+ ];
2297
+ return { fields, lines: visibleLines };
2298
+ }
2255
2299
  function CollisionRedirect({ registry, collisionId, fallbackSprintId }) {
2256
2300
  const router = useRouter();
2257
2301
  useInput6((_input, key) => {
@@ -6887,7 +6931,7 @@ import { Text as Text36 } from "ink";
6887
6931
  // src/integration/external/version-check.ts
6888
6932
  import { mkdir, readFile, rename, writeFile as writeFile2 } from "fs/promises";
6889
6933
  import { dirname, join as join3 } from "path";
6890
- var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
6934
+ var CACHE_TTL_MS = 60 * 60 * 1e3;
6891
6935
  var FETCH_TIMEOUT_MS = 3e3;
6892
6936
  var REGISTRY_URL = "https://registry.npmjs.org/ralphctl/latest";
6893
6937
  function getCachePath() {
@@ -7311,7 +7355,7 @@ function buildInitialStack(initialView, mountOptions) {
7311
7355
  return [{ id: "home" }];
7312
7356
  }
7313
7357
  function useTerminalWidth() {
7314
- const { stdout } = useStdout();
7358
+ const { stdout } = useStdout2();
7315
7359
  const [width, setWidth] = useState38(stdout.columns);
7316
7360
  useEffect37(() => {
7317
7361
  const onResize = () => {
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  parseSprintStartArgs,
4
4
  sprintStartCommand
5
- } from "./chunk-OFILN7QL.mjs";
5
+ } from "./chunk-B3RCOHW3.mjs";
6
6
  import "./chunk-ZLWSPLWI.mjs";
7
7
  import "./chunk-GQ2WFKBN.mjs";
8
8
  import "./chunk-CFUVE2BP.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralphctl",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
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",