Skip to content

Add Hexbus output transcripts#30

Merged
BurnedChris merged 5 commits into
mainfrom
hexbus-output-transcripts
May 31, 2026
Merged

Add Hexbus output transcripts#30
BurnedChris merged 5 commits into
mainfrom
hexbus-output-transcripts

Conversation

@BurnedChris

Copy link
Copy Markdown
Contributor

Capture CLI stdout and stderr with global transcript flags so downstream CLIs can persist human-readable or JSONL runs for agent review.

Capture CLI stdout and stderr with global transcript flags so downstream CLIs can persist human-readable or JSONL runs for agent review.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • CLI now supports --log-file flag to capture and save command execution transcripts
    • CLI now supports --log-format flag to control transcript output format (text or JSONL)
  • Tests

    • Added comprehensive test coverage for transcript logging functionality

Walkthrough

This PR adds process-level transcript capture to the Hexbus CLI. Users can now pass --log-file <path> and optionally --log-format jsonl (defaulting to plain text) to record all command output. The implementation intercepts stdout/stderr, writes to a transcript file with optional metadata, and integrates cleanly into the CLI startup and shutdown.

Changes

Transcript Capture System

Layer / File(s) Summary
Transcript Module: Types, Flag Parsing, and Stream Capture
packages/hexbus/src/transcript.ts
New module defines transcript types and formats, implements parseTranscriptFlags to extract flag values from raw CLI args in both space-separated and equals-form, and implements startOutputTranscript to create files, monkey-patch stdout/stderr to capture output (with optional ANSI stripping for JSONL), and restore writers on close with duration/exit-code metadata.
CLI Flags, Parser, and Runner Lifecycle
packages/hexbus/src/parser.ts, packages/hexbus/src/runner.ts
parser.ts adds --log-file and --log-format (default "text") to globalFlags. runner.ts imports transcript utilities, parses flags early from rawArgs, starts transcript before version handling, updates execution flow to ensure transcript closure, and guarantees transcript.close() is called with an appropriate exit code in a finally block.
Public API Re-exports
packages/hexbus/src/index.ts
Exports transcript types (TranscriptFormat, TranscriptFlagOptions, StartTranscriptOptions, OutputTranscript) and functions (parseTranscriptFlags, startOutputTranscript) for downstream use.
End-to-End Test Coverage
packages/hexbus/src/__tests__/spawned-cli.e2e.test.ts, packages/hexbus/src/__tests__/transcript.test.ts
Adds transcript test infrastructure with unique log file path generation, suite cleanup, unit tests for parseTranscriptFlags, and two e2e scenarios: one validating plain-text output via --log-file, the other validating JSONL output (start/end records with captured text) via --log-file --log-format jsonl.
Release Notes
.changeset/green-buses-log.md
Patch-level changeset documenting the addition of --log-file and --log-format transcript flags.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant runCli
  participant parseTranscriptFlags
  participant startOutputTranscript
  participant process_stdout as "process.stdout.write"
  participant FS as "filesystem"

  User->>runCli: invoke CLI with rawArgs
  runCli->>parseTranscriptFlags: parse rawArgs (--log-file / --log-format)
  parseTranscriptFlags-->>runCli: TranscriptFlagOptions | null
  runCli->>startOutputTranscript: startOutputTranscript(options)
  startOutputTranscript->>FS: create/truncate transcript file
  startOutputTranscript->>process_stdout: patch write to append captured output
  process_stdout->>FS: append captured output (text/jsonl)
  runCli->>startOutputTranscript: close(exitCode) on shutdown
  startOutputTranscript->>FS: write end record (jsonl) and restore writers
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • inthhq/hexbus#16: Extends the same spawned-cli e2e test suite with new transcript test scenarios and per-run transcript file handling infrastructure.
  • inthhq/hexbus#2: Both PRs modify packages/hexbus/src/parser.ts by extending the shared globalFlags list with additional CLI options.
  • inthhq/hexbus#6: Related changes to CLI flag parsing and handling that intersect with the new --log-file/--log-format flags.

"🐰 I hop and I write, every stdout line,
From plain text to JSONL, neat and fine,
I stamp a start, then every chunk I log,
Close with an end, no output lost in fog,
Hooray — your CLI transcripts now shine!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add Hexbus output transcripts' clearly and concisely describes the main change: adding transcript functionality to capture CLI output.
Description check ✅ Passed The description directly relates to the changeset, explaining the purpose of capturing CLI stdout/stderr with transcript flags for agent review.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch hexbus-output-transcripts

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Comment @coderabbitai help to get the list of available commands and usage tips.

autofix-ci Bot and others added 2 commits May 31, 2026 02:33
Keep the transcript logging feature scoped to Hexbus' existing Clack output layer so the branch can ship without unrelated terminal UI changes.

Co-authored-by: Cursor <cursoragent@cursor.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/hexbus/src/errors.ts (1)

302-317: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

withErrorHandling uses process.exit(1) directly, bypassing OpenTUI exit flow.

createErrorHandlers.handleError routes through openTuiExit(messages, 1) for consistent rendering, but withErrorHandling still calls cliError.display(logger) followed by process.exit(1). This inconsistency means errors caught by withErrorHandling won't be captured in transcripts or rendered through the unified OpenTUI path.

Consider aligning withErrorHandling with the new pattern, or document the intentional divergence.

🔧 Potential alignment with OpenTUI exit
 export function withErrorHandling<
   T extends (...args: unknown[]) => Promise<unknown>,
 >(fn: T, logger: CliLogger, context?: { command?: string }): T {
   return (async (...args: Parameters<T>) => {
     try {
       return await fn(...args);
     } catch (error) {
       const cliError = CliError.from(error);
-      cliError.display(logger);
+      let { message } = cliError.entry;
+      if (cliError.context?.details) {
+        message += `: ${cliError.context.details}`;
+      }
+      const messages = [formatLogMessage("error", message)];
+      if (cliError.entry.hint) {
+        messages.push(formatLogMessage("info", `Hint: ${cliError.entry.hint}`));
+      }
+      if (cliError.entry.docs) {
+        messages.push(formatLogMessage("info", `Docs: ${cliError.entry.docs}`));
+      }
       if (context?.command) {
-        logger.info(`Command: ${context.command}`);
+        messages.push(formatLogMessage("info", `Command: ${context.command}`));
       }
-      process.exit(1);
+      return openTuiExit(messages, 1);
     }
   }) as T;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/hexbus/src/errors.ts` around lines 302 - 317, The withErrorHandling
wrapper currently calls CliError.from/error.display and then process.exit(1),
bypassing the OpenTUI exit flow; update it to delegate to the centralized exit
handler (e.g., use createErrorHandlers.handleError or call openTuiExit(messages,
1) via the same pathway used by createErrorHandlers) so errors go through the
unified rendering/transcript logic—locate the withErrorHandling function, remove
the direct process.exit(1) call, and invoke the shared error/exit helper with
the CliError (or its message payload and exit code) to preserve consistent
OpenTUI behavior.
packages/hexbus/src/intro.ts (1)

36-45: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Unused context parameter in displayIntro

displayIntro still takes context: Pick<CliContext, "logger">, but it’s not referenced anywhere; openTuiIntro only accepts (banner, content, title?), while runner.ts and the runner unit tests continue to pass commandContext anyway. Either rename the parameter to _context and update the JSDoc (since logger isn’t used anymore), or remove it as a breaking API change by updating all call sites (runner.ts, tests, and the exported signature from src/index.ts).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/hexbus/src/intro.ts` around lines 36 - 45, The function displayIntro
has an unused parameter context; rename it to _context in the function signature
(keep the type Pick<CliContext, "logger">) and update its JSDoc to indicate the
logger is intentionally unused to silence linters and clarify intent; leave
openTuiIntro calls and caller sites (runner.ts and tests) unchanged since this
is not a breaking API change. Ensure the identifier change is applied to the
export in src/index.ts if it re-exports the same symbol so the signature remains
consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/hexbus/src/logger.ts`:
- Around line 93-119: The switch over logLevel in packages/hexbus/src/logger.ts
is duplicating openTuiMessage(formattedMessage) in every non-error branch;
simplify by making the "error" case handle its TTY fallback (if
process.stdin.isTTY && process.stdout.isTTY then call
openTuiMessage(formattedMessage) else write to process.stderr), and collapse all
other cases (warn, info, debug, success, failed, default) to a single
fall-through that just calls openTuiMessage(formattedMessage); update the switch
or replace it with an if/else using the existing symbols logLevel,
formattedMessage, openTuiMessage and the TTY check to remove the duplicated
calls.

In `@packages/hexbus/src/opentui.ts`:
- Around line 289-299: getSession retains a rejected sessionPromise so the TUI
is permanently broken; wrap the await of sessionPromise (created via
sessionPromise ??= createSession()) in a try/catch: assign sessionPromise before
awaiting, then in the catch set sessionPromise = null (and leave activeSession
null) and rethrow the error so callers see the failure, ensuring subsequent
calls to getSession will retry createSession; reference sessionPromise,
getSession, createSession, activeSession, and cancelScheduledClose when making
the change.
- Around line 801-838: The non-openTui branch of openTuiExit currently calls
process.exit synchronously and can prevent the runner's finally block (which
calls transcript?.close(...)) from running; change openTuiExit to ensure the
transcript is closed/flushes before exiting when !canUseOpenTui() by awaiting
the shared transcript close hook (or invoking a closeTranscript/flushTranscript
function) prior to calling process.exit — locate openTuiExit and canUseOpenTui
and add an awaited call to the same transcript?.close(...) logic used by
runner.ts (or a centrally exported close function) so the transcript is
guaranteed to be closed before process.exit is invoked.
- Around line 136-140: clearChildren: no change required—box.getChildren()
already returns a snapshot so you can leave clearChildren(box: BoxRenderable)
as-is. getSession: avoid leaving a permanently rejected sessionPromise by
changing the sessionPromise assignment to use a temporary promise (e.g., const p
= createSession()), set sessionPromise = p, then await p.catch(err => { if
(sessionPromise === p) sessionPromise = undefined; throw err; }) so a rejection
clears sessionPromise and allows retries; reference sessionPromise and
createSession. openTuiExit: do not call process.exit(exitCode) synchronously in
the non-TTY (!canUseOpenTui()) branch because it can bypass runCli()'s
finally/await transcript.close; instead either return the exitCode to the caller
(so the caller calls process.exit after cleanup) or schedule the exit
asynchronously (e.g., setImmediate(() => process.exit(exitCode))) so
transcript.close/runCli() finally handlers can run; reference openTuiExit,
runCli, and transcript.close.

In `@packages/hexbus/src/runner.ts`:
- Around line 815-817: The version check calls handleVersionRequest(options,
rawArgs, state.context) before state.context is initialized, so context (and its
logger) is always undefined; either initialize state.context (so logger is
available) before invoking handleVersionRequest or remove the new context
parameter and logger forwarding from handleVersionRequest/printVersionInfo; to
fix, move the context creation (assignment to state.context) to before the if
(await handleVersionRequest(...)) call so handleVersionRequest and
printVersionInfo receive a valid context.logger, or alternatively remove the
context argument from handleVersionRequest and the logger: context?.logger usage
in printVersionInfo to keep behavior unchanged.
- Around line 848-852: handleRunnerError -> state.context.error.handleError ->
handleError -> openTuiExit currently calls process.exit synchronously on
non-TTY, which bypasses the finally block that calls transcript.close in
runner.ts; to fix, change openTuiExit (and its callers like handleError) to not
call process.exit directly but instead surface the intended exit code by
throwing a dedicated ExitError (or returning the code) so the caller
(handleRunnerError/runner.ts) can let the exception propagate and run the
finally block where transcript?.close(...) executes; ensure the thrown ExitError
carries the exit code and update handleError/openTuiExit docs/comments to
reflect the new behavior.

In `@packages/hexbus/src/transcript.ts`:
- Around line 217-232: close() is declared to return Promise<void> but only
performs synchronous I/O and returns Promise.resolve(), which misleads callers;
make it truly asynchronous by converting the body to async and using
non-blocking file writes: mark close(...) as async, replace the synchronous
appendJsonLine/appendFileSync calls with their Promise-based equivalents (e.g.,
use appendJsonLine that returns a Promise or fs.promises.appendFile for
options.format === "jsonl"), await those calls, and then return/allow implicit
resolution; keep restoring originalStdoutWrite/originalStderrWrite and
preserving durationMs/exitCode/timestamp logic referencing options.format,
options.filePath and startedAt.
- Around line 130-156: The argument parsing loop in transcript.ts currently
unconditionally consumes the next token for spaced flags (--log-file,
--log-format) by doing index++ and using rawArgs[index+1], which can skip a
following real argument if the next token is missing or itself a flag. Update
the handling for the "--log-file" and "--log-format" cases inside the for loop
to first check that rawArgs[index + 1] exists and does not start with "--"
before assigning filePath or format and incrementing index; otherwise treat the
flag as having no value (leave filePath/format unchanged or null) so later code
that calls normalizeFormat or valueAfterEquals behaves correctly. Use the
existing valueAfterEquals and normalizeFormat helpers for the equals-form
branches unchanged.
- Around line 214-215: Add a one-time process exit handler to ensure the
original stdout/stderr write functions are restored and the transcript is closed
on unexpected exits: register process.once listeners for 'exit' and signal
events like 'SIGINT' and 'SIGTERM' that call the module/class close() (or an
internal restore method) and restore process.stdout.write and
process.stderr.write back to the saved originals
(captureStdoutWrite/captureStderrWrite should have saved the originals) before
returning; ensure handlers are idempotent so repeated signals don't double-close
and remove or ignore handlers after normal close() completes.

---

Outside diff comments:
In `@packages/hexbus/src/errors.ts`:
- Around line 302-317: The withErrorHandling wrapper currently calls
CliError.from/error.display and then process.exit(1), bypassing the OpenTUI exit
flow; update it to delegate to the centralized exit handler (e.g., use
createErrorHandlers.handleError or call openTuiExit(messages, 1) via the same
pathway used by createErrorHandlers) so errors go through the unified
rendering/transcript logic—locate the withErrorHandling function, remove the
direct process.exit(1) call, and invoke the shared error/exit helper with the
CliError (or its message payload and exit code) to preserve consistent OpenTUI
behavior.

In `@packages/hexbus/src/intro.ts`:
- Around line 36-45: The function displayIntro has an unused parameter context;
rename it to _context in the function signature (keep the type Pick<CliContext,
"logger">) and update its JSDoc to indicate the logger is intentionally unused
to silence linters and clarify intent; leave openTuiIntro calls and caller sites
(runner.ts and tests) unchanged since this is not a breaking API change. Ensure
the identifier change is applied to the export in src/index.ts if it re-exports
the same symbol so the signature remains consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 05ba3a4c-9143-4948-886a-949a143202e2

📥 Commits

Reviewing files that changed from the base of the PR and between 2aefbe9 and 2919352.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (15)
  • .changeset/green-buses-log.md
  • packages/hexbus/package.json
  • packages/hexbus/src/__tests__/dispatch.test.ts
  • packages/hexbus/src/__tests__/prompts.test.ts
  • packages/hexbus/src/__tests__/spawned-cli.e2e.test.ts
  • packages/hexbus/src/errors.ts
  • packages/hexbus/src/index.ts
  • packages/hexbus/src/intro.ts
  • packages/hexbus/src/logger.ts
  • packages/hexbus/src/opentui.ts
  • packages/hexbus/src/parser.ts
  • packages/hexbus/src/prompts.ts
  • packages/hexbus/src/runner.ts
  • packages/hexbus/src/spinner.ts
  • packages/hexbus/src/transcript.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Write code that is accessible, performant, type-safe, and maintainable with focus on clarity and explicit intent over brevity
Use meaningful variable names instead of magic numbers - extract constants with descriptive names
Use arrow functions for callbacks and short functions
Prefer for...of loops over .forEach() and indexed for loops
Use optional chaining (?.) and nullish coalescing (??) for safer property access
Prefer template literals over string concatenation
Use destructuring for object and array assignments
Use const by default, let only when reassignment is needed, never var
Always await promises in async functions - don't forget to use the return value
Use async/await syntax instead of promise chains for better readability
Handle errors appropriately in async code with try-catch blocks
Don't use async functions as Promise executors
Remove console.log, debugger, and alert statements from production code
Throw Error objects with descriptive messages, not strings or other values
Use try-catch blocks meaningfully - don't catch errors just to rethrow them
Prefer early returns over nested conditionals for error cases
Keep functions focused and under reasonable cognitive complexity limits
Extract complex conditions into well-named boolean variables
Use early returns to reduce nesting
Prefer simple conditionals over nested ternary operators
Group related code together and separate concerns
Don't use eval() or assign directly to document.cookie
Validate and sanitize user input
Avoid spread syntax in accumulators within loops
Use top-level regex literals instead of creating them in loops
Prefer specific imports over namespace imports
Use next/head or App Router metadata API for head elements
Use descriptive names for functions, variables, and types
Handle boundary conditions and error states for edge cases
Add comments for complex logic, but prefer self-documenting code
Run bun x ultracite fix before comm...

Files:

  • packages/hexbus/src/index.ts
  • packages/hexbus/src/intro.ts
  • packages/hexbus/src/logger.ts
  • packages/hexbus/src/spinner.ts
  • packages/hexbus/src/errors.ts
  • packages/hexbus/src/transcript.ts
  • packages/hexbus/src/parser.ts
  • packages/hexbus/src/__tests__/spawned-cli.e2e.test.ts
  • packages/hexbus/src/runner.ts
  • packages/hexbus/src/__tests__/prompts.test.ts
  • packages/hexbus/src/__tests__/dispatch.test.ts
  • packages/hexbus/src/opentui.ts
  • packages/hexbus/src/prompts.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use explicit types for function parameters and return values when they enhance clarity
Prefer unknown over any when the type is genuinely unknown
Use const assertions (as const) for immutable values and literal types
Leverage TypeScript's type narrowing instead of type assertions

Files:

  • packages/hexbus/src/index.ts
  • packages/hexbus/src/intro.ts
  • packages/hexbus/src/logger.ts
  • packages/hexbus/src/spinner.ts
  • packages/hexbus/src/errors.ts
  • packages/hexbus/src/transcript.ts
  • packages/hexbus/src/parser.ts
  • packages/hexbus/src/__tests__/spawned-cli.e2e.test.ts
  • packages/hexbus/src/runner.ts
  • packages/hexbus/src/__tests__/prompts.test.ts
  • packages/hexbus/src/__tests__/dispatch.test.ts
  • packages/hexbus/src/opentui.ts
  • packages/hexbus/src/prompts.ts
**/index.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Avoid barrel files (index files that re-export everything)

Files:

  • packages/hexbus/src/index.ts
**/package.json

📄 CodeRabbit inference engine (AGENTS.md)

Use Bun for installs, scripts, tests, and one-off TypeScript execution

Files:

  • packages/hexbus/package.json
packages/*/package.json

📄 CodeRabbit inference engine (AGENTS.md)

Put package-specific task logic in each package's package.json

Files:

  • packages/hexbus/package.json
**/*.{test,spec}.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{test,spec}.{ts,tsx,js,jsx}: Write assertions inside it() or test() blocks
Avoid done callbacks in async tests - use async/await instead
Don't use .only or .skip in committed code
Keep test suites reasonably flat - avoid excessive describe nesting

Files:

  • packages/hexbus/src/__tests__/spawned-cli.e2e.test.ts
  • packages/hexbus/src/__tests__/prompts.test.ts
  • packages/hexbus/src/__tests__/dispatch.test.ts
🪛 ast-grep (0.42.3)
packages/hexbus/src/transcript.ts

[warning] 44-44: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(${ESCAPE_CHARACTER}\\[[0-?]*[ -/]*[@-~], "g")
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

packages/hexbus/src/opentui.ts

[warning] 29-29: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(${ESCAPE_CHARACTER}\\[[0-?]*[ -/]*[@-~], "g")
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🪛 markdownlint-cli2 (0.22.1)
.changeset/green-buses-log.md

[warning] 5-5: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

🔇 Additional comments (15)
packages/hexbus/src/parser.ts (1)

32-44: LGTM!

Also applies to: 208-214

packages/hexbus/src/runner.ts (1)

803-812: LGTM!

packages/hexbus/src/index.ts (1)

175-182: LGTM!

packages/hexbus/src/__tests__/spawned-cli.e2e.test.ts (1)

51-57: LGTM!

Also applies to: 209-260

.changeset/green-buses-log.md (1)

1-5: LGTM!

packages/hexbus/package.json (1)

38-38: ⚡ Quick win

Confirm @opentui/core@0.2.15 pin and removal of @clack/prompts.

packages/hexbus/package.json now pins @opentui/core to 0.2.15 (npm registry has this version), and the GitHub security advisories lookup for @opentui/core returned no results. @clack/prompts has no remaining TypeScript references in packages/hexbus.

packages/hexbus/src/prompts.ts (1)

249-261: LGTM!

Also applies to: 275-291, 303-317, 335-343

packages/hexbus/src/__tests__/prompts.test.ts (1)

13-24: LGTM!

Also applies to: 33-33, 55-59, 75-75, 96-96, 164-164

packages/hexbus/src/__tests__/dispatch.test.ts (1)

10-18: LGTM!

Also applies to: 22-22, 74-89, 101-111, 115-115, 273-288, 335-335, 477-492

packages/hexbus/src/transcript.ts (1)

43-45: LGTM!

The static analysis tool flagged a ReDoS concern, but this is a false positive. The ESCAPE_CHARACTER variable is a constant (\x1b), not user input. The resulting regex \x1b\[[0-?]*[ -/]*[@-~] is a standard ANSI escape sequence pattern with no pathological backtracking risk.

packages/hexbus/src/logger.ts (1)

183-194: LGTM!

packages/hexbus/src/errors.ts (2)

248-254: LGTM!


269-284: LGTM!

packages/hexbus/src/spinner.ts (2)

28-49: LGTM!

The spinner now emits static progress messages rather than animated frames—a reasonable tradeoff for transcript compatibility and non-TTY environments.


63-82: LGTM!

Comment thread packages/hexbus/src/logger.ts
Comment thread packages/hexbus/src/opentui.ts Outdated
Comment on lines +136 to +140
function clearChildren(box: BoxRenderable): void {
for (const child of box.getChildren()) {
box.remove(child.id);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does @opentui/core BoxRenderable getChildren() return a live internal array or a new copy?

💡 Result:

@opentui/core BoxRenderable getChildren returns a new array (a snapshot), not a live internal array. Evidence: OpenTUI’s own Tree Navigation docs define getChildren to “Get children” and show it used as an array of direct children for iteration (and for operations like remove-by-id), without any indication that mutating the returned array would affect the underlying render tree [1]. Additionally, internal OpenTUI docs treat child access as an ephemeral “render list” built during layout/render traversal rather than exposing internal storage directly [2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/hexbus/src/opentui.ts"

echo "== clearChildren area =="
sed -n '110,170p' "$FILE" | nl -ba

echo
echo "== getSession area =="
sed -n '250,340p' "$FILE" | nl -ba

echo
echo "== openTuiExit area =="
sed -n '760,880p' "$FILE" | nl -ba

echo
echo "== Any transcript close / process.exit call sites =="
rg -n "process\.exit|transcript|renderLines" "$FILE"

Repository: inthhq/hexbus

Length of output: 143


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/hexbus/src/opentui.ts"

echo "== clearChildren area =="
sed -n '110,170p' "$FILE" | cat -n

echo
echo "== getSession area =="
sed -n '250,340p' "$FILE" | cat -n

echo
echo "== openTuiExit area =="
sed -n '760,880p' "$FILE" | cat -n

echo
echo "== Any transcript/process.exit/renderLines call sites =="
rg -n "process\.exit|transcript|renderLines" "$FILE"

Repository: inthhq/hexbus

Length of output: 8347


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/hexbus/src/opentui.ts"

echo "== sessionPromise / createSession =="
rg -n "sessionPromise|activeSession|createSession" "$FILE"
echo
echo "== openTuiExit signature and surrounding definitions =="
rg -n "export function openTuiExit|function openTuiExit" "$FILE" -n
sed -n '180,360p' "$FILE" | cat -n
echo
sed -n '720,870p' "$FILE" | cat -n

echo
echo "== openTuiExit call sites =="
rg -n "openTuiExit\\(" -S packages | head -n 200

Repository: inthhq/hexbus

Length of output: 11761


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="packages/hexbus/src/errors.ts"

sed -n '220,320p' "$FILE" | cat -n
echo
rg -n "transcript|finally|try\\s*\\{|openTuiExit" "$FILE"

Repository: inthhq/hexbus

Length of output: 3760


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "transcript" . | head -n 200
echo
rg -n "createErrorHandlers\\(" packages | head -n 200
echo
rg -n "withErrorHandling\\(" packages | head -n 200
echo
rg -n "openTuiExit\\(" packages | head -n 200

Repository: inthhq/hexbus

Length of output: 3221


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="packages/hexbus/src/runner.ts"
sed -n '740,900p' "$FILE" | cat -n

echo
echo "== transcript.ts close implementation =="
TS="packages/hexbus/src/transcript.ts"
rg -n "close\\(" "$TS"
sed -n '130,240p' "$TS" | cat -n

Repository: inthhq/hexbus

Length of output: 8058


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="packages/hexbus/src/opentui.ts"
rg -n "function canUseOpenTui|canUseOpenTui\\(" "$FILE"
sed -n '1,120p' "$FILE" | cat -n

Repository: inthhq/hexbus

Length of output: 4005


Fix getSession rejection stickiness; adjust openTuiExit exit/cleanup behavior; drop clearChildren mutation concern

  • clearChildren(): box.getChildren() comes back as a snapshot/new array, so iterating while calling box.remove() should not skip elements—no defensive copy needed.
  • getSession(): sessionPromise ??= createSession() leaves a rejected sessionPromise in place forever if createSession() rejects (no reset on rejection), permanently breaking OpenTUI for subsequent calls.
  • openTuiExit(): the process.exit(exitCode) in the !canUseOpenTui() branch is synchronous, so it can bypass runCli()’s finally { await transcript?.close(...) } and leave transcripts unclosed/incomplete. In the TTY branch, process.exit is delayed, so transcript close may still run before exit.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/hexbus/src/opentui.ts` around lines 136 - 140, clearChildren: no
change required—box.getChildren() already returns a snapshot so you can leave
clearChildren(box: BoxRenderable) as-is. getSession: avoid leaving a permanently
rejected sessionPromise by changing the sessionPromise assignment to use a
temporary promise (e.g., const p = createSession()), set sessionPromise = p,
then await p.catch(err => { if (sessionPromise === p) sessionPromise =
undefined; throw err; }) so a rejection clears sessionPromise and allows
retries; reference sessionPromise and createSession. openTuiExit: do not call
process.exit(exitCode) synchronously in the non-TTY (!canUseOpenTui()) branch
because it can bypass runCli()'s finally/await transcript.close; instead either
return the exitCode to the caller (so the caller calls process.exit after
cleanup) or schedule the exit asynchronously (e.g., setImmediate(() =>
process.exit(exitCode))) so transcript.close/runCli() finally handlers can run;
reference openTuiExit, runCli, and transcript.close.

Comment thread packages/hexbus/src/opentui.ts Outdated
Comment thread packages/hexbus/src/opentui.ts Outdated
Comment thread packages/hexbus/src/runner.ts Outdated
Comment on lines +848 to +852
try {
handleRunnerError(state);
} finally {
await transcript?.close(state.errorToHandle === undefined ? 0 : 1);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how handleError / openTuiExit terminate the process
ast-grep --pattern 'export function openTuiExit($$$) { $$$ }'
rg -nP --type=ts -C5 '\bprocess\.exit\s*\('
rg -nP --type=ts -C3 '\bhandleError\b'

Repository: inthhq/hexbus

Length of output: 6247


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the exact runner try/finally region mentioned in the review comment
sed -n '820,870p' packages/hexbus/src/runner.ts

# Inspect runner's handleRunnerError implementation and how it invokes error handling
rg -n "function handleRunnerError|handleRunnerError\\(" packages/hexbus/src/runner.ts
rg -n "handleRunnerError\\(" packages/hexbus/src/runner.ts -n

# Show the relevant part of errors.ts where handleError terminates
sed -n '220,330p' packages/hexbus/src/errors.ts

# Show the full openTuiExit implementation area (including where process.exit is called)
rg -n "function openTuiExit\\b|export function openTuiExit\\b" packages/hexbus/src/opentui.ts
sed -n '740,870p' packages/hexbus/src/opentui.ts

Repository: inthhq/hexbus

Length of output: 7131


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show handleRunnerError implementation and the exact call into context.error.handleError
rg -n "function handleRunnerError|const handleRunnerError|handleRunnerError\\s*=" packages/hexbus/src/runner.ts
# Print a focused window around the call site and the function definition
sed -n '730,830p' packages/hexbus/src/runner.ts
sed -n '830,880p' packages/hexbus/src/runner.ts

# Also inspect transcript.close usage if present
rg -n "transcript\\?\\.close|transcript\\.close" packages/hexbus/src/runner.ts

Repository: inthhq/hexbus

Length of output: 3761


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Find canUseOpenTui() definition and its conditions
rg -n "function canUseOpenTui\\b|canUseOpenTui\\s*\\(" packages/hexbus/src/opentui.ts
# Show around the definition
# (If rg reports multiple hits, grab the closest window around each definition)
rg -n "canUseOpenTui\\b" packages/hexbus/src/opentui.ts -n -C3

Repository: inthhq/hexbus

Length of output: 2204


Fix transcript closure on non-TTY error exits (process.exit bypasses finally)

handleRunnerError(state) calls state.context.error.handleError(...) synchronously (packages/hexbus/src/runner.ts), and handleError(...) (packages/hexbus/src/errors.ts) routes to openTuiExit(messages, 1). In packages/hexbus/src/opentui.ts, openTuiExit() calls process.exit(exitCode) synchronously when canUseOpenTui() is false (process.stdin.isTTY && process.stdout.isTTY). In that non-TTY case, the finally { await transcript?.close(...) } block in runner.ts (lines 848-852) will not run, so the JSONL end record/flush can be lost for error runs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/hexbus/src/runner.ts` around lines 848 - 852, handleRunnerError ->
state.context.error.handleError -> handleError -> openTuiExit currently calls
process.exit synchronously on non-TTY, which bypasses the finally block that
calls transcript.close in runner.ts; to fix, change openTuiExit (and its callers
like handleError) to not call process.exit directly but instead surface the
intended exit code by throwing a dedicated ExitError (or returning the code) so
the caller (handleRunnerError/runner.ts) can let the exception propagate and run
the finally block where transcript?.close(...) executes; ensure the thrown
ExitError carries the exit code and update handleError/openTuiExit docs/comments
to reflect the new behavior.

Comment thread packages/hexbus/src/transcript.ts
Comment thread packages/hexbus/src/transcript.ts
Comment thread packages/hexbus/src/transcript.ts
BurnedChris and others added 2 commits May 30, 2026 19:50
Keep version handling on the fast path and make transcript cleanup more robust around missing flag values and process exits.

Co-authored-by: Cursor <cursoragent@cursor.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
packages/hexbus/src/transcript.ts (1)

130-136: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle short-form flags as missing values in nextFlagValue.

Line 135 only excludes --..., so inputs like --log-file -h or --log-format -v are consumed as values and can start transcript capture unexpectedly. Treat short option-looking tokens as missing values too.

🔧 Suggested patch
 function nextFlagValue(
   rawArgs: readonly string[],
   index: number
 ): string | undefined {
   const value = rawArgs[index + 1];
-  return value !== undefined && !value.startsWith("--") ? value : undefined;
+  if (value === undefined || value === "--") {
+    return undefined;
+  }
+  return /^-{1,2}[A-Za-z]/.test(value) ? undefined : value;
 }

As per coding guidelines, "Handle boundary conditions and error states for edge cases".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/hexbus/src/transcript.ts` around lines 130 - 136, The helper
nextFlagValue currently treats only tokens starting with "--" as flags, so
short-form flags like "-h" are mistakenly accepted as values; update the check
in nextFlagValue (params rawArgs, index) to treat any token that begins with "-"
as a flag (e.g., use value !== undefined && !value.startsWith("-") or a regex
like /^-/) so short options are considered missing values; keep the same return
semantics (return the next token only if it does not look like a flag).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@packages/hexbus/src/transcript.ts`:
- Around line 130-136: The helper nextFlagValue currently treats only tokens
starting with "--" as flags, so short-form flags like "-h" are mistakenly
accepted as values; update the check in nextFlagValue (params rawArgs, index) to
treat any token that begins with "-" as a flag (e.g., use value !== undefined &&
!value.startsWith("-") or a regex like /^-/) so short options are considered
missing values; keep the same return semantics (return the next token only if it
does not look like a flag).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f884fa4a-9d02-4ce0-96ba-dd0d440ef588

📥 Commits

Reviewing files that changed from the base of the PR and between 2919352 and 60be4cb.

📒 Files selected for processing (4)
  • packages/hexbus/src/__tests__/transcript.test.ts
  • packages/hexbus/src/parser.ts
  • packages/hexbus/src/runner.ts
  • packages/hexbus/src/transcript.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Write code that is accessible, performant, type-safe, and maintainable with focus on clarity and explicit intent over brevity
Use meaningful variable names instead of magic numbers - extract constants with descriptive names
Use arrow functions for callbacks and short functions
Prefer for...of loops over .forEach() and indexed for loops
Use optional chaining (?.) and nullish coalescing (??) for safer property access
Prefer template literals over string concatenation
Use destructuring for object and array assignments
Use const by default, let only when reassignment is needed, never var
Always await promises in async functions - don't forget to use the return value
Use async/await syntax instead of promise chains for better readability
Handle errors appropriately in async code with try-catch blocks
Don't use async functions as Promise executors
Remove console.log, debugger, and alert statements from production code
Throw Error objects with descriptive messages, not strings or other values
Use try-catch blocks meaningfully - don't catch errors just to rethrow them
Prefer early returns over nested conditionals for error cases
Keep functions focused and under reasonable cognitive complexity limits
Extract complex conditions into well-named boolean variables
Use early returns to reduce nesting
Prefer simple conditionals over nested ternary operators
Group related code together and separate concerns
Don't use eval() or assign directly to document.cookie
Validate and sanitize user input
Avoid spread syntax in accumulators within loops
Use top-level regex literals instead of creating them in loops
Prefer specific imports over namespace imports
Use next/head or App Router metadata API for head elements
Use descriptive names for functions, variables, and types
Handle boundary conditions and error states for edge cases
Add comments for complex logic, but prefer self-documenting code
Run bun x ultracite fix before comm...

Files:

  • packages/hexbus/src/__tests__/transcript.test.ts
  • packages/hexbus/src/parser.ts
  • packages/hexbus/src/runner.ts
  • packages/hexbus/src/transcript.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use explicit types for function parameters and return values when they enhance clarity
Prefer unknown over any when the type is genuinely unknown
Use const assertions (as const) for immutable values and literal types
Leverage TypeScript's type narrowing instead of type assertions

Files:

  • packages/hexbus/src/__tests__/transcript.test.ts
  • packages/hexbus/src/parser.ts
  • packages/hexbus/src/runner.ts
  • packages/hexbus/src/transcript.ts
**/*.{test,spec}.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{test,spec}.{ts,tsx,js,jsx}: Write assertions inside it() or test() blocks
Avoid done callbacks in async tests - use async/await instead
Don't use .only or .skip in committed code
Keep test suites reasonably flat - avoid excessive describe nesting

Files:

  • packages/hexbus/src/__tests__/transcript.test.ts
🔇 Additional comments (3)
packages/hexbus/src/parser.ts (1)

33-45: LGTM!

Also applies to: 209-214

packages/hexbus/src/runner.ts (1)

516-529: LGTM!

Also applies to: 801-810, 813-815, 846-850

packages/hexbus/src/__tests__/transcript.test.ts (1)

1-24: LGTM!

@BurnedChris BurnedChris merged commit 8dd441c into main May 31, 2026
3 checks passed
@BurnedChris BurnedChris deleted the hexbus-output-transcripts branch May 31, 2026 02:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant