Skip to content

feat(cli): add mastra run command for headless agent execution#15348

Closed
cmphilpot wants to merge 5 commits intomastra-ai:mainfrom
cmphilpot:output-format
Closed

feat(cli): add mastra run command for headless agent execution#15348
cmphilpot wants to merge 5 commits intomastra-ai:mainfrom
cmphilpot:output-format

Conversation

@cmphilpot
Copy link
Copy Markdown
Contributor

@cmphilpot cmphilpot commented Apr 14, 2026

Description

Related Issue(s)

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Code refactoring
  • Performance improvement
  • Test update

Checklist

  • I have made corresponding changes to the documentation (if applicable)
  • I have added tests that prove my fix is effective or that my feature works
  • I have addressed all Coderabbit comments on this PR

ELI5 Summary

This PR adds a new "mastra run" command so you can run a Mastra agent from the terminal without running a server: provide an agent ID and a prompt (or pipe input), choose an output format (text, JSON, or streaming JSON), and it runs the agent and prints the results. It also adds a headless runner and output-formatting helpers to support that command.

Overview

Adds a headless, one‑shot CLI workflow to execute Mastra agents locally with selectable output formats, optional structured output via JSON Schema, SIGINT handling, and clear exit-code semantics. Exposes a runHeadless orchestrator and output-formatting utilities from @mastra/core/harness and wires a top-level mastra run command in the CLI.

Key Changes

  • CLI

    • packages/cli/src/index.ts: adds top-level mastra run command wired to runAgent.
    • packages/cli/src/commands/actions/run-agent.ts: analytics-wrapped action forwarding args to run.
    • packages/cli/src/commands/run/run.ts: main CLI handler that resolves prompt (flag or piped stdin), validates inputs (output formats: text|json|stream-json), enforces --json-schema only for JSON modes, bundles the project entry, loads env, executes bundled output via execa, streams child stdout/stderr, forwards SIGINT, and maps exit codes (config error: 2, runtime error: 1, success: 0).
    • packages/cli/src/commands/run/RunBundler.ts: BuildBundler subclass generating an injected entry that calls runHeadless(mastra, ...), embeds runtime options, and discovers dotenv files (respects MASTRA_SKIP_DOTENV).
    • packages/cli/src/commands/run/RunBundler.test.ts and packages/cli/src/commands/run/run.test.ts: Vitest coverage for bundler entry generation, env-file discovery, argument validation, and error-exit behavior.
  • Core headless orchestrator & exports

    • packages/core/src/harness/run-headless.ts: new runHeadless function that validates args, resolves agent (printing available agents when resolution fails), parses optional jsonSchema, registers SIGINT via AbortController, invokes agent.stream (optionally with structuredOutput), formats output per selected mode, and enforces strict-mode/warnings semantics.
    • packages/core/src/harness/output-formatter.ts: adds OutputFormat type and three formatters:
      • formatText: streams text-delta chunks to stdout, routes error chunks to stderr, ensures trailing newline.
      • formatJson: emits the final FullOutput as a single newline-delimited JSON object (serializing Error objects).
      • formatStreamJson: emits NDJSON one line per streamed chunk.
      • hasWarnings: detects presence of warnings in FullOutput.
    • packages/core/src/harness/index.ts: re-exports runHeadless and formatter helpers/types.
    • packages/core/src/harness/test-utils.ts: test helpers for building mock stream/full outputs.
  • Tests

    • packages/core/src/harness/output-formatter.test.ts and packages/core/src/harness/run-headless.test.ts: comprehensive tests covering formatter behavior, full-output serialization, structured-output wiring, SIGINT handling, strict-mode, and exit-code semantics.
  • Releases / metadata

    • .changeset entries added indicating minor releases:
      • @mastra/core: exposes runHeadless and harness output-formatting helpers.
      • mastra: introduces mastra run CLI command and documents supported output modes and flags.

Output & Signal Behavior (summary)

  • text: streams text-delta to stdout, error chunks to stderr, appends trailing newline; SIGINT emits a newline and exits with runtime error code.
  • json: emits one JSON envelope for final FullOutput (errors serialized); SIGINT emits an interrupted envelope; errors -> exit code 1.
  • stream-json: emits NDJSON per chunk; SIGINT emits an abort chunk with payload.reason === "interrupted".

Validation & Errors

  • outputFormat must be one of text | json | stream-json (configuration error, exit 2).
  • --json-schema is allowed only with json or stream-json (configuration error, exit 2); schema JSON is parsed and parse errors produce clear stderr and exit 2.
  • Missing agent or failed agent resolution prints guidance (including available agents when possible) and exits with configuration error code 2.

@dane-ai-mastra
Copy link
Copy Markdown
Contributor

Thank you for your contribution!

Please ensure that your PR fixes an existing issue and that you have linked it in the description (e.g. with Fixes #1234).

We use CodeRabbit for automated code reviews. Please address all feedback from CodeRabbit by either making changes to your PR or leaving a comment explaining why you disagree with the feedback. Since CodeRabbit is an AI, it may occasionally provide incorrect feedback.

Addressing CodeRabbit's feedback will greatly increase the chances of your PR being merged. We appreciate your understanding and cooperation in helping us maintain high code quality standards.

Comment @coderabbitai review in case you want to trigger a review.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 14, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a headless mastra run CLI command and action, bundler support to generate a runtime entry that calls a new runHeadless orchestrator, new harness output-formatters (text, json, stream-json) and exports, plus comprehensive tests across CLI and core packages.

Changes

Cohort / File(s) Summary
Changesets
\.changeset/five-guests-play.md, \.changeset/thirty-regions-rush.md
New changeset metadata announcing minor releases describing runHeadless, the mastra run CLI, and harness output-format helpers and formats.
CLI: Command wiring & action
packages/cli/src/index.ts, packages/cli/src/commands/actions/run-agent.ts
Adds top-level mastra run command and exported runAgent action that normalizes args, applies defaults, and tracks execution via analytics.
CLI: Command implementation
packages/cli/src/commands/run/run.ts, packages/cli/src/commands/run/run.test.ts
New run command with RunArgs, input validation (stdin fallback), isOutputFormat checks, --json-schema gating, bundling and child-process execution, signal forwarding, error/exit semantics, and validation tests.
CLI: Bundler
packages/cli/src/commands/run/RunBundler.ts, packages/cli/src/commands/run/RunBundler.test.ts
Introduces RunBundler and RunEntryOptions to generate an entry importing runHeadless, embedding run options, wiring stdio/exit/SIGINT, and discovering optional env files; includes bundler unit tests.
Core: Harness entry
packages/core/src/harness/run-headless.ts, packages/core/src/harness/run-headless.test.ts
Adds exported runHeadless and types (RunHeadlessOptions, RunHeadlessIO): agent lookup, optional JSON Schema parsing, AbortController + SIGINT handling, agent.stream invocation, format-based output flow, strict-warning handling, and exhaustive tests including SIGINT behavior.
Core: Output formatter & utils
packages/core/src/harness/output-formatter.ts, packages/core/src/harness/output-formatter.test.ts, packages/core/src/harness/test-utils.ts
New OutputFormat type and functions formatText, formatJson, formatStreamJson, hasWarnings, plus an Error replacer and NDJSON semantics; accompanied by unit tests and test helpers (createMockFullOutput, createMockStreamOutput, collectStream).
Core: Harness exports
packages/core/src/harness/index.ts
Re-exports runHeadless, RunHeadlessOptions, RunHeadlessIO, formatText, formatJson, formatStreamJson, hasWarnings, and OutputFormat from harness modules.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.37% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(cli): add mastra run command for headless agent execution' is concise, descriptive, and directly aligns with the main changeset objective of introducing a new 'mastra run' CLI command.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
packages/cli/src/commands/run/run.ts (1)

1-10: Minor inconsistency: FileService import path differs from RunBundler.ts.

RunBundler.ts imports from '@mastra/deployer/build' while this file imports from '@mastra/deployer'. Both may resolve correctly, but consider using consistent import paths across the codebase for clarity.

♻️ Suggested change for consistency
-import { FileService } from '@mastra/deployer';
+import { FileService } from '@mastra/deployer/build';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/run/run.ts` around lines 1 - 10, The import for
FileService in run.ts is inconsistent with RunBundler.ts (one uses
'@mastra/deployer' while the other uses '@mastra/deployer/build'); update the
FileService import in packages/cli/src/commands/run/run.ts to match the same
module path used in RunBundler (use the '@mastra/deployer/build' path if that's
what's used in RunBundler.js) so imports are consistent across the codebase;
locate the import statement referencing FileService and adjust it to the same
module string as in RunBundler to avoid ambiguity.
packages/cli/src/commands/run/RunBundler.test.ts (1)

147-163: Consider adding test coverage for MASTRA_SKIP_DOTENV='1'.

The shouldSkipDotenvLoading() utility (from packages/cli/src/commands/utils.ts) accepts both 'true' and '1' values, but only 'true' is tested here.

🧪 Optional: Add test for '1' value
     it('should return empty array when MASTRA_SKIP_DOTENV is set', async () => {
       process.env.MASTRA_SKIP_DOTENV = 'true';

       const bundler = new RunBundler(defaultOptions);
       const envFiles = await bundler.getEnvFiles();

       expect(envFiles).toEqual([]);
     });
+
+    it('should return empty array when MASTRA_SKIP_DOTENV is "1"', async () => {
+      process.env.MASTRA_SKIP_DOTENV = '1';
+
+      const bundler = new RunBundler(defaultOptions);
+      const envFiles = await bundler.getEnvFiles();
+
+      expect(envFiles).toEqual([]);
+    });
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/run/RunBundler.test.ts` around lines 147 - 163, Add
a test case in RunBundler.test.ts that mirrors the existing
MASTRA_SKIP_DOTENV='true' test but sets process.env.MASTRA_SKIP_DOTENV = '1' to
assert getEnvFiles() returns an empty array; this ensures the
shouldSkipDotenvLoading() behavior (used by RunBundler.getEnvFiles) correctly
handles the '1' value. Reuse the same setup as the existing test (instantiate
new RunBundler(defaultOptions), call await bundler.getEnvFiles(), and
expect(envFiles).toEqual([])) and clean up/reset the environment variable after
the test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.changeset/thirty-regions-rush.md:
- Around line 5-19: The changeset entry titled "Added `mastra run` command for
headless one-shot agent invocation." is too long and reads like CLI docs;
replace the current long example block and flag-by-flag breakdown with a brief
1–2 sentence release-note summarizing the change (e.g., "Add `mastra run`
command to invoke agents headlessly with JSON output support."), and move the
detailed usage example and flags out of .changeset/thirty-regions-rush.md into
the CLI help or project docs; ensure the changeset only contains the concise
summary sentence(s) and nothing else.

In `@packages/cli/src/commands/actions/run-agent.ts`:
- Around line 15-17: The analytics call using analytics.trackCommandExecution
currently sends the raw args object (variable args) which contains sensitive
fields like the free-form prompt, JSON schema, and path/env overrides; update
the call so you redact those fields before sending: build a sanitized payload
from args that omits or replaces prompt, schema, and any path/env override keys
with redacted flags, and instead include only non-sensitive metadata such as
output format and boolean presence flags (e.g., hasPrompt, hasSchema,
hasLocalOverrides); update the call site where analytics.trackCommandExecution
is invoked to pass this sanitized payload rather than the original args.

In `@packages/core/src/harness/run-headless.ts`:
- Around line 82-105: The code currently relies on RunHeadlessIO.exit (a void
callback) to stop execution after SIGINT; instead add an explicit aborted
state/cancellation flow: modify registerSigint to set a shared aborted flag or
produce an AbortSignal/AbortController (and call io.exit as before), then
propagate that signal into the async formatting calls (formatText, formatJson,
formatStreamJson) or at minimum check the shared aborted flag immediately after
each await (e.g., after the chosen format* call) before inspecting fullOutput,
writing warnings or calling io.exit; ensure any subsequent branches (the strict
warnings branch and the normal exit path) short-circuit when aborted to avoid
emitting further output or calling io.exit again.
- Around line 65-80: The harness must reject a provided jsonSchema when
outputFormat is 'text' before calling the agent: in runHeadless (where
jsonSchema and structuredOutput are handled) add a guard that if jsonSchema is
set and outputFormat === 'text' then write an error to io.stderr and call
io.exit(EXIT_CONFIG_ERROR) (same code path used for invalid JSON parsing) so we
fail fast; only build structuredOutput and call agent.stream(...) when
outputFormat permits structured outputs, and keep references to jsonSchema,
structuredOutput, outputFormat, agent.stream and formatText to locate the
change.

---

Nitpick comments:
In `@packages/cli/src/commands/run/run.ts`:
- Around line 1-10: The import for FileService in run.ts is inconsistent with
RunBundler.ts (one uses '@mastra/deployer' while the other uses
'@mastra/deployer/build'); update the FileService import in
packages/cli/src/commands/run/run.ts to match the same module path used in
RunBundler (use the '@mastra/deployer/build' path if that's what's used in
RunBundler.js) so imports are consistent across the codebase; locate the import
statement referencing FileService and adjust it to the same module string as in
RunBundler to avoid ambiguity.

In `@packages/cli/src/commands/run/RunBundler.test.ts`:
- Around line 147-163: Add a test case in RunBundler.test.ts that mirrors the
existing MASTRA_SKIP_DOTENV='true' test but sets process.env.MASTRA_SKIP_DOTENV
= '1' to assert getEnvFiles() returns an empty array; this ensures the
shouldSkipDotenvLoading() behavior (used by RunBundler.getEnvFiles) correctly
handles the '1' value. Reuse the same setup as the existing test (instantiate
new RunBundler(defaultOptions), call await bundler.getEnvFiles(), and
expect(envFiles).toEqual([])) and clean up/reset the environment variable after
the test.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9dfab51a-5b3a-499b-95a3-aafea2c1f7bd

📥 Commits

Reviewing files that changed from the base of the PR and between a6fbf2d and 2bd7939.

📒 Files selected for processing (13)
  • .changeset/five-guests-play.md
  • .changeset/thirty-regions-rush.md
  • packages/cli/src/commands/actions/run-agent.ts
  • packages/cli/src/commands/run/RunBundler.test.ts
  • packages/cli/src/commands/run/RunBundler.ts
  • packages/cli/src/commands/run/run.test.ts
  • packages/cli/src/commands/run/run.ts
  • packages/cli/src/index.ts
  • packages/core/src/harness/index.ts
  • packages/core/src/harness/output-formatter.test.ts
  • packages/core/src/harness/output-formatter.ts
  • packages/core/src/harness/run-headless.test.ts
  • packages/core/src/harness/run-headless.ts

Comment thread .changeset/thirty-regions-rush.md Outdated
Comment thread packages/cli/src/commands/actions/run-agent.ts Outdated
Comment thread packages/core/src/harness/run-headless.ts
Comment thread packages/core/src/harness/run-headless.ts Outdated
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 14, 2026

🦋 Changeset detected

Latest commit: 15bdd2e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 21 packages
Name Type
@mastra/core Minor
mastra Minor
mastracode Patch
@mastra/mcp-docs-server Patch
@internal/playground Minor
@mastra/client-js Patch
@mastra/opencode Patch
@mastra/longmemeval Patch
create-mastra Minor
@mastra/deployer-cloud Minor
@mastra/deployer-vercel Patch
@mastra/playground-ui Patch
@mastra/react Patch
@mastra/server Minor
@mastra/deployer Minor
@mastra/express Patch
@mastra/fastify Patch
@mastra/hono Patch
@mastra/koa Patch
@mastra/deployer-cloudflare Patch
@mastra/deployer-netlify Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
packages/core/src/harness/run-headless.ts (1)

81-99: ⚠️ Potential issue | 🟠 Major

SIGINT still doesn't unblock a hung formatter.

The new aborted checks only run after formatText() / formatJson() / formatStreamJson() resolve. For text mode, packages/core/src/harness/output-formatter.ts:114-143 waits on reader.read() until the stream closes, so a stalled stream still leaves runHeadless() pending forever after Ctrl-C when io.exit() is mocked or embedded instead of terminating the process.

Possible fix
-  if (outputFormat === 'text') {
-    fullOutput = await formatText(streamOutput, io.stdout, io.stderr);
-  } else if (outputFormat === 'json') {
-    fullOutput = await formatJson(streamOutput, io.stdout, startTime);
-  } else {
-    fullOutput = await formatStreamJson(streamOutput, io.stdout, io.stderr);
-  }
+  if (outputFormat === 'text') {
+    fullOutput = await formatText(streamOutput, io.stdout, io.stderr, abortController.signal);
+  } else if (outputFormat === 'json') {
+    fullOutput = await formatJson(streamOutput, io.stdout, startTime, abortController.signal);
+  } else {
+    fullOutput = await formatStreamJson(streamOutput, io.stdout, io.stderr, abortController.signal);
+  }
// packages/core/src/harness/output-formatter.ts
export async function formatText<OUTPUT>(
  streamOutput: MastraModelOutput<OUTPUT>,
  stdout: Writable,
  stderr: Writable,
  signal?: AbortSignal,
): Promise<FullOutput<OUTPUT>> {
  const reader = streamOutput.fullStream.getReader();
  const onAbort = () => {
    void reader.cancel('interrupted');
  };

  signal?.addEventListener('abort', onAbort, { once: true });

  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      // ...
    }
  } finally {
    signal?.removeEventListener('abort', onAbort);
    reader.releaseLock();
  }

  return await streamOutput.getFullOutput();
}

Also applies to: 116-134

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/harness/run-headless.ts` around lines 81 - 99, runHeadless
registers an AbortController but only checks aborted after
formatText/formatJson/formatStreamJson return, so a hung formatter blocks
SIGINT; modify the formatters (formatText, formatJson, formatStreamJson) to
accept an optional AbortSignal parameter and inside obtain the reader from
streamOutput.fullStream.getReader(), add an onAbort handler that calls
reader.cancel('interrupted') and register it via signal.addEventListener(..., {
once: true }), ensure the handler is removed in finally and releaseLock on the
reader; then pass abortController.signal from runHeadless (where registerSigint
is already wired) into the chosen formatter so Ctrl-C cancels the reader and
unblocks runHeadless.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/cli/src/commands/run/run.ts`:
- Around line 41-47: mastraDir is being resolved against process.cwd() instead
of using the computed rootDir (so --root is ignored); update the mastraDir
computation to base relative paths on rootDir (use rootDir in the join and in
the startsWith/resolve logic) so that the default and provided relative args.dir
are resolved from rootDir rather than process.cwd(); adjust the code that sets
mastraDir (the variable named mastraDir, referencing args.dir and join) to
reference rootDir for both the relative branch and the default branch.

---

Duplicate comments:
In `@packages/core/src/harness/run-headless.ts`:
- Around line 81-99: runHeadless registers an AbortController but only checks
aborted after formatText/formatJson/formatStreamJson return, so a hung formatter
blocks SIGINT; modify the formatters (formatText, formatJson, formatStreamJson)
to accept an optional AbortSignal parameter and inside obtain the reader from
streamOutput.fullStream.getReader(), add an onAbort handler that calls
reader.cancel('interrupted') and register it via signal.addEventListener(..., {
once: true }), ensure the handler is removed in finally and releaseLock on the
reader; then pass abortController.signal from runHeadless (where registerSigint
is already wired) into the chosen formatter so Ctrl-C cancels the reader and
unblocks runHeadless.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0623c1c4-8b26-4ea0-a099-ac37a8e191bf

📥 Commits

Reviewing files that changed from the base of the PR and between 2bd7939 and bce69c7.

📒 Files selected for processing (5)
  • .changeset/thirty-regions-rush.md
  • packages/cli/src/commands/actions/run-agent.ts
  • packages/cli/src/commands/run/run.ts
  • packages/core/src/harness/run-headless.test.ts
  • packages/core/src/harness/run-headless.ts
✅ Files skipped from review due to trivial changes (1)
  • .changeset/thirty-regions-rush.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/cli/src/commands/actions/run-agent.ts

Comment thread packages/cli/src/commands/run/run.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/core/src/harness/run-headless.test.ts (1)

10-62: Consider extracting shared test helpers.

The createMockFullOutput, createMockStreamOutput, and collectStream functions are duplicated from output-formatter.test.ts. These could be extracted to a shared test utilities file (e.g., packages/core/src/harness/__tests__/helpers.ts or packages/core/src/harness/test-utils.ts) to reduce duplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/harness/run-headless.test.ts` around lines 10 - 62, Tests
duplicate helper functions createMockFullOutput, createMockStreamOutput, and
collectStream; extract these into a shared test utilities module (e.g.,
harness/test-utils.ts) that exports those functions, then replace the local
definitions in run-headless.test.ts and output-formatter.test.ts with imports
from the shared module and update any types (FullOutput, ChunkType) imports as
needed so both tests reference the single implementation of
createMockFullOutput, createMockStreamOutput, and collectStream.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/core/src/harness/run-headless.test.ts`:
- Around line 10-62: Tests duplicate helper functions createMockFullOutput,
createMockStreamOutput, and collectStream; extract these into a shared test
utilities module (e.g., harness/test-utils.ts) that exports those functions,
then replace the local definitions in run-headless.test.ts and
output-formatter.test.ts with imports from the shared module and update any
types (FullOutput, ChunkType) imports as needed so both tests reference the
single implementation of createMockFullOutput, createMockStreamOutput, and
collectStream.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1e1c85ea-e248-4267-b8f0-dc355f7b4130

📥 Commits

Reviewing files that changed from the base of the PR and between 05b2804 and b7577b5.

📒 Files selected for processing (6)
  • .changeset/five-guests-play.md
  • packages/core/src/harness/index.ts
  • packages/core/src/harness/output-formatter.test.ts
  • packages/core/src/harness/output-formatter.ts
  • packages/core/src/harness/run-headless.test.ts
  • packages/core/src/harness/run-headless.ts
✅ Files skipped from review due to trivial changes (2)
  • .changeset/five-guests-play.md
  • packages/core/src/harness/output-formatter.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/core/src/harness/index.ts
  • packages/core/src/harness/run-headless.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/core/src/harness/test-utils.ts (1)

37-50: Keep the shared stream-output helper strongly typed.

as any here removes the compile-time contract for a helper that multiple harness suites now depend on. If the real stream-output shape changes, these tests will keep compiling and only fail at runtime. Please return the concrete stream-output type instead of erasing it.

As per coding guidelines, **/*.{ts,tsx}: All packages use TypeScript with strict type checking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/harness/test-utils.ts` around lines 37 - 50, The helper
createMockStreamOutput currently returns an any which erases the compile-time
contract; change its return type to the concrete stream-output interface used by
consumers (e.g., the type that exposes fullStream:
ReadableStream<ChunkType<any>> and getFullOutput: () =>
Promise<FullOutput<any>>), update the function signature to return that exact
type instead of `any`, and ensure imports/use of ChunkType and FullOutput in the
signature match the existing types so callers get strong typing for fullStream
and getFullOutput.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/harness/run-headless.test.ts`:
- Around line 307-328: The test should actually await the headless run and make
the mocked agent/stream respond to abort so cancellation is exercised: change
the test to capture the promise returned by runHeadless(mastra, ..., h.io)
instead of discarding it, update the mock streamOutput
(fullStream/getFullOutput) to resolve/finish when the harness SIGINT handler is
invoked (e.g., have getFullOutput return a promise that can be rejected/resolved
by calling a stored abort callback), then after calling h.sigintHandlers[0]()
await the captured runHeadless promise before asserting exits and parsing
stdout; update other similar tests that call runHeadless (the ones referenced
around the other ranges) the same way.

---

Nitpick comments:
In `@packages/core/src/harness/test-utils.ts`:
- Around line 37-50: The helper createMockStreamOutput currently returns an any
which erases the compile-time contract; change its return type to the concrete
stream-output interface used by consumers (e.g., the type that exposes
fullStream: ReadableStream<ChunkType<any>> and getFullOutput: () =>
Promise<FullOutput<any>>), update the function signature to return that exact
type instead of `any`, and ensure imports/use of ChunkType and FullOutput in the
signature match the existing types so callers get strong typing for fullStream
and getFullOutput.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3972660a-1df3-4fbd-813d-ead6bc0cf28a

📥 Commits

Reviewing files that changed from the base of the PR and between b7577b5 and 15bdd2e.

📒 Files selected for processing (3)
  • packages/core/src/harness/output-formatter.test.ts
  • packages/core/src/harness/run-headless.test.ts
  • packages/core/src/harness/test-utils.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/harness/output-formatter.test.ts

Comment on lines +307 to +328
it('json sigint handler should emit abort chunk and exit 1', async () => {
const h = createHarness();
// Use a stream that doesn't resolve immediately
const never = new ReadableStream({ start() {} });
const streamOutput = { fullStream: never, getFullOutput: () => new Promise(() => {}) } as any;
const agent = { stream: vi.fn().mockResolvedValue(streamOutput) };
const mastra = createMockMastra(() => agent);

// Don't await — kick it off, then fire sigint
void runHeadless(mastra, { ...baseOptions, outputFormat: 'json' }, h.io);
await new Promise(r => setImmediate(r));

expect(h.sigintHandlers).toHaveLength(1);
h.sigintHandlers[0]!();
h.stdout.end();
h.stderr.end();

expect(h.exits).toEqual([1]);
const chunk = JSON.parse((await h.stdoutPromise).trim());
expect(chunk.type).toBe('abort');
expect(chunk.payload.reason).toBe('interrupted');
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Await the interrupted run in these SIGINT cases.

These tests currently only assert the handler’s immediate stdout/exit side effects. Because the mocked fullStream/getFullOutput never react to abort and the kicked-off runHeadless() promise is discarded, a regression where Ctrl-C leaves the headless run hung would still pass here. Please make the mock settle on abort and await the run promise so cancellation actually gets exercised.

Also applies to: 330-366, 406-424

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/harness/run-headless.test.ts` around lines 307 - 328, The
test should actually await the headless run and make the mocked agent/stream
respond to abort so cancellation is exercised: change the test to capture the
promise returned by runHeadless(mastra, ..., h.io) instead of discarding it,
update the mock streamOutput (fullStream/getFullOutput) to resolve/finish when
the harness SIGINT handler is invoked (e.g., have getFullOutput return a promise
that can be rejected/resolved by calling a stored abort callback), then after
calling h.sigintHandlers[0]() await the captured runHeadless promise before
asserting exits and parsing stdout; update other similar tests that call
runHeadless (the ones referenced around the other ranges) the same way.

@cmphilpot
Copy link
Copy Markdown
Contributor Author

Closing — on reflection this work belonged in mastracode/src/headless.ts, not a new framework-CLI command. Will open a narrower PR.

@cmphilpot cmphilpot closed this Apr 15, 2026
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