From 3ade6f87b4dda2fdb757d87c810bd4252c7209b9 Mon Sep 17 00:00:00 2001 From: Jay Scambler Date: Tue, 17 Mar 2026 17:33:18 -0500 Subject: [PATCH 1/3] fix(AC-331): print errors to console in non-JSON CLI mode autoctx run silently exited with code 1 when exceptions occurred and --json was not passed. Error handlers only wrote to stderr in JSON mode; in default (Rich console) mode, no output was produced. Fixed all three run paths (agent-task, generation runner, resume): - Exception: console.print(f"[red]Error: {exc}[/red]") - KeyboardInterrupt: console.print("[yellow]Run interrupted.[/yellow]") JSON mode continues writing to stderr as before. 3 tests: non-JSON error printed, non-JSON interrupt printed, JSON mode still works. --- autocontext/src/autocontext/cli.py | 12 ++++ autocontext/tests/test_cli_error_output.py | 76 ++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 autocontext/tests/test_cli_error_output.py diff --git a/autocontext/src/autocontext/cli.py b/autocontext/src/autocontext/cli.py index 52fa65fe..cea41bed 100644 --- a/autocontext/src/autocontext/cli.py +++ b/autocontext/src/autocontext/cli.py @@ -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)) @@ -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)) @@ -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)) diff --git a/autocontext/tests/test_cli_error_output.py b/autocontext/tests/test_cli_error_output.py new file mode 100644 index 00000000..5421fe80 --- /dev/null +++ b/autocontext/tests/test_cli_error_output.py @@ -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 From ae6eab483ec986be3a8d865c7e213c1b62bb6aeb Mon Sep 17 00:00:00 2001 From: Jay Scambler Date: Tue, 17 Mar 2026 17:39:35 -0500 Subject: [PATCH 2/3] Bump package versions to 0.2.1 --- autocontext/pyproject.toml | 2 +- autocontext/uv.lock | 2 +- ts/package-lock.json | 4 ++-- ts/package.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/autocontext/pyproject.toml b/autocontext/pyproject.toml index ef6e38e9..2c16ad9c 100644 --- a/autocontext/pyproject.toml +++ b/autocontext/pyproject.toml @@ -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" } diff --git a/autocontext/uv.lock b/autocontext/uv.lock index 547461fd..d4f41925 100644 --- a/autocontext/uv.lock +++ b/autocontext/uv.lock @@ -72,7 +72,7 @@ wheels = [ [[package]] name = "autoctx" -version = "0.2.0" +version = "0.2.1" source = { editable = "." } dependencies = [ { name = "anthropic" }, diff --git a/ts/package-lock.json b/ts/package-lock.json index b0e05ccf..0c8a6226 100644 --- a/ts/package-lock.json +++ b/ts/package-lock.json @@ -1,12 +1,12 @@ { "name": "autoctx", - "version": "0.2.0", + "version": "0.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "autoctx", - "version": "0.2.0", + "version": "0.2.1", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.27.1", diff --git a/ts/package.json b/ts/package.json index 5f4d81d7..a9943391 100644 --- a/ts/package.json +++ b/ts/package.json @@ -1,6 +1,6 @@ { "name": "autoctx", - "version": "0.2.0", + "version": "0.2.1", "description": "autocontext — always-on agent evaluation harness", "type": "module", "main": "dist/index.js", From 2ce9ec0f4262e23a49c39a6cfb2750bc790a9587 Mon Sep 17 00:00:00 2001 From: Jay Scambler Date: Tue, 17 Mar 2026 17:43:30 -0500 Subject: [PATCH 3/3] Add npm package metadata --- ts/package.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ts/package.json b/ts/package.json index a9943391..9875a6fe 100644 --- a/ts/package.json +++ b/ts/package.json @@ -6,9 +6,18 @@ "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" },