Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion autocontext/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "autoctx"
version = "0.2.0"
version = "0.2.1"
description = "autocontext control plane for iterative strategy evolution."
readme = "README.md"
license = { text = "Apache-2.0" }
Expand Down
12 changes: 12 additions & 0 deletions autocontext/src/autocontext/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,10 +302,14 @@ def run(
except KeyboardInterrupt:
if json_output:
_write_json_stderr("run interrupted")
else:
console.print("[yellow]Run interrupted.[/yellow]")
raise typer.Exit(code=1) from None
except Exception as exc:
if json_output:
_write_json_stderr(str(exc))
else:
console.print(f"[red]Error: {exc}[/red]")
raise typer.Exit(code=1) from exc
if json_output:
_write_json_stdout(dataclasses.asdict(task_summary))
Expand Down Expand Up @@ -352,10 +356,14 @@ def _loop_target() -> None:
except KeyboardInterrupt:
if json_output:
_write_json_stderr("run interrupted")
else:
console.print("[yellow]Run interrupted.[/yellow]")
raise typer.Exit(code=1) from None
except Exception as exc:
if json_output:
_write_json_stderr(str(exc))
else:
console.print(f"[red]Error: {exc}[/red]")
raise typer.Exit(code=1) from exc
if json_output:
_write_json_stdout(dataclasses.asdict(summary))
Expand Down Expand Up @@ -390,10 +398,14 @@ def resume(
except KeyboardInterrupt:
if json_output:
_write_json_stderr("resume interrupted")
else:
console.print("[yellow]Resume interrupted.[/yellow]")
raise typer.Exit(code=1) from None
except Exception as exc:
if json_output:
_write_json_stderr(str(exc))
else:
console.print(f"[red]Error: {exc}[/red]")
raise typer.Exit(code=1) from exc
if json_output:
_write_json_stdout(dataclasses.asdict(summary))
Expand Down
76 changes: 76 additions & 0 deletions autocontext/tests/test_cli_error_output.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""Tests for AC-331: CLI exits should print errors in non-JSON mode.

Verifies that autoctx run prints exceptions to stderr instead of
exiting silently when --json is not passed.
"""

from __future__ import annotations

from unittest.mock import MagicMock, patch

from typer.testing import CliRunner

from autocontext.cli import app

runner = CliRunner()


class TestCliErrorOutput:
def test_run_error_printed_in_non_json_mode(self) -> None:
"""AC-331: Non-JSON mode should print the error, not exit silently."""
with (
patch.dict("autocontext.cli.SCENARIO_REGISTRY", {"test_scenario": object}, clear=True),
patch(
"autocontext.scenarios.families.detect_family",
return_value=None,
),
patch("autocontext.cli._apply_preset_env"),
patch("autocontext.cli.load_settings", return_value=MagicMock()),
patch("autocontext.cli._runner", side_effect=RuntimeError("Something broke")),
):
result = runner.invoke(app, ["run", "--scenario", "test_scenario", "--gens", "1"])

assert result.exit_code == 1
# The error message should appear somewhere in the output
combined = (result.stdout or "") + (result.stderr or "") + (result.output or "")
assert "Something broke" in combined, (
f"Error message not printed. stdout={result.stdout!r}, stderr={result.stderr!r}"
)

def test_run_interrupt_printed_in_non_json_mode(self) -> None:
"""Keyboard interrupts should also produce visible output."""
with (
patch.dict("autocontext.cli.SCENARIO_REGISTRY", {"test_scenario": object}, clear=True),
patch(
"autocontext.scenarios.families.detect_family",
return_value=None,
),
patch("autocontext.cli._apply_preset_env"),
patch("autocontext.cli.load_settings", return_value=MagicMock()),
patch("autocontext.cli._runner", side_effect=KeyboardInterrupt()),
):
result = runner.invoke(app, ["run", "--scenario", "test_scenario", "--gens", "1"])

assert result.exit_code == 1
combined = (result.stdout or "") + (result.stderr or "") + (result.output or "")
assert "interrupt" in combined.lower(), (
f"Interrupt message not printed. stdout={result.stdout!r}"
)

def test_json_mode_still_writes_to_stderr(self) -> None:
"""JSON mode should continue writing errors to stderr."""
with (
patch.dict("autocontext.cli.SCENARIO_REGISTRY", {"test_scenario": object}, clear=True),
patch(
"autocontext.scenarios.families.detect_family",
return_value=None,
),
patch("autocontext.cli._apply_preset_env"),
patch("autocontext.cli.load_settings", return_value=MagicMock()),
patch("autocontext.cli._runner", side_effect=RuntimeError("JSON error")),
):
result = runner.invoke(app, ["run", "--scenario", "test_scenario", "--gens", "1", "--json"])

assert result.exit_code == 1
combined = (result.stdout or "") + (result.stderr or "") + (result.output or "")
assert "JSON error" in combined
2 changes: 1 addition & 1 deletion autocontext/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions ts/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion ts/package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
{
"name": "autoctx",
"version": "0.2.0",
"version": "0.2.1",
"description": "autocontext — always-on agent evaluation harness",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"README.md",
"dist",
"migrations"
],
"keywords": [
"agents",
"autocontext",
"evaluation",
"harness",
"llm",
"mcp"
],
"bin": {
"autoctx": "dist/cli/index.js"
},
Expand Down