Metadata-Version: 2.4
Name: heru
Version: 2.1.1
Summary: Unified headless CLI for coding agents (codex, claude, copilot, gemini, opencode, goz). One interface, one JSONL output format, one resume mechanism.
Author: Alexey Grigorev
License: MIT
Requires-Python: >=3.11
Requires-Dist: pydantic>=2.11.0
Requires-Dist: typer>=0.16.0
Provides-Extra: dev
Requires-Dist: pytest>=8.4.0; extra == 'dev'
Requires-Dist: quse>=0.0.4; extra == 'dev'
Description-Content-Type: text/markdown

# heru

**Unified headless CLI for coding agents.** One interface, one JSONL event
envelope, one resume mechanism — regardless of which underlying agent you use:

- [codex](https://github.com/openai/codex) (OpenAI)
- [claude](https://github.com/anthropics/claude-code) (Anthropic)
- [copilot](https://github.com/github/copilot-cli) (GitHub)
- [gemini](https://github.com/google-gemini/gemini-cli) (Google)
- [opencode](https://github.com/sst/opencode) (any OpenAI-compatible)
- [goz](https://github.com/z-ai/goz) (Z.AI)

## Why

Every coding-agent CLI has its own flags, its own event format, its own
notion of "session", its own quota endpoint, and its own quirks. If you
want to drive them headlessly from a script or higher-level orchestrator,
you end up writing a separate wrapper for each — and when a new one ships
you start over.

heru is that wrapper, once. Point it at any supported CLI and get the
same shape out.

## Usage

```bash
# Fresh run
heru codex "summarize the diff between main and my branch"

# Resume a prior session
heru codex --resume <session-id> "now also run the tests"

# Different engine, same CLI shape
heru claude "find the bug in src/foo.py"

# Apply a launch profile without changing the engine/parser
heru codex --profile zodex "run the subagent smoke test"

```

All output is streamed as unified JSONL by default. Pass `--raw` to get
the engine's native JSON/JSONL stream back for debugging.

The engine name is positional. The legacy `--engine` CLI form was removed
in `v2.0.0`; see [CHANGELOG.md](CHANGELOG.md) and
[`docs/migrations/2.0.0-remove-legacy-engine-flag.md`](docs/migrations/2.0.0-remove-legacy-engine-flag.md).

Cross-provider usage/quota reporting lives in
[quse](https://github.com/alexeygrigorev/quse). Use `quse` directly for
normalized quota windows.

## Launch Profiles

Use `--profile` (`-p`) when an engine should launch with a different wrapper,
config directory, environment cleanup, or preflight command while keeping the
same engine parser and unified event output.

Profiles are read from `~/.config/heru/profiles.toml`, or from the path in
`HERU_PROFILES_FILE`.

Example for a Z.AI-routed Codex profile managed by
[`~/git/.agents`](https://github.com/alexeygrigorev/.agents):

```toml
[profiles.zodex]
engine = "codex"
command = "codex"
unset_env_file = "~/git/.agents/config/codex/zodex_env_unset.txt"
preflight = [["~/git/.agents/scripts/zodex_start_proxy.sh"]]

[profiles.zodex.env]
CODEX_HOME = "~/.zodex"
```

If you have a real wrapper executable, use it as `command = "zodex"` instead.

Then run:

```bash
heru codex --profile zodex "spawn one Python subagent and summarize the result"
```

Claude-style profile wrappers work the same way:

```toml
[profiles.zlaude]
engine = "claude"
command = "claude"
unset_env_file = "~/git/.agents/config/claude/zlaude_env_unset.txt"

[profiles.zlaude.env]
CLAUDE_CONFIG_DIR = "~/.zlaude"
```

## Unified Event Schema

Each stdout line is a `UnifiedEvent` JSON object with these common fields:

- `kind`: `message`, `tool_call`, `tool_result`, `usage`, `error`, `status`, or `continuation`
- `engine`: the emitting engine name (`codex`, `claude`, `copilot`, `gemini`, `opencode`, `goz`)
- `sequence`: zero-based event order within the run
- `timestamp`: ISO-8601 timestamp for the emitted event
- `role`: optional role (`assistant`, `user`, `system`)
- `content`: assistant or status text when present
- `tool_name`: tool identifier for tool-related events
- `tool_input`: serialized tool input when present
- `tool_output`: serialized tool output when present
- `error`: error text when present
- `usage_delta`: per-event usage fields extracted from the provider payload
- `continuation_id`: session/thread identifier when the engine emits one
- `raw`: the original provider-native payload

Example:

```json
{"kind":"message","engine":"claude","sequence":0,"timestamp":"2026-04-10T19:00:00+00:00","role":"assistant","content":"Done.","raw":{"type":"assistant","message":{"content":"Done."}}}
```

## Install

```bash
pip install heru
# or
uv add heru
```

## Status

**v2.0.0 is a physical extraction from
[litehive](https://github.com/alexeygrigorev/litehive),** which uses
heru as its engine execution layer. Several things from the vision
above are still in flight:

- [x] Unified JSONL output format — `heru` now emits one documented
      event envelope across all supported engines by default.
- [x] Unified `--resume` across all engines.
- [x] Positional engine argument (`heru codex <prompt>`).
- [ ] Standalone sandboxing — heru currently executes agents directly;
      sandbox integration is a follow-up.

## Layout

```
heru/
  adapters/    per-engine CLI wrappers (codex, claude, copilot, gemini, opencode, goz)
  base.py      ExternalCLIAdapter base class + CLIInvocation
  types.py     shared engine/stream/usage types
  main.py      entrypoint for `heru <engine> <prompt>`

tests/         unit tests for adapters, stream parsing, inactivity timeout
```

`tests/contract/` is the standalone public-API contract suite. Treat it as the
"thou shalt not break" directory: changing assertions in that folder is a
semver-major change and requires the full breaking-change checklist below.

## API Contract

The names below are heru's stable public contract. Changes to their
signature, return shape, required fields, or documented behavior are
breaking changes and require a semver-major release.

### Public entrypoints

- `heru.get_engine(name)`: resolves a stable engine name such as `codex` or `claude` to the adapter instance heru will execute.
- `heru.ENGINE_CHOICES`: lists the stable engine names accepted by the CLI and `get_engine`.
- `heru.main.main(argv=None)`: provides the public `heru` CLI entrypoint and preserves the supported argument contract.
- `heru <engine> --profile <name> <prompt>`: applies a launch profile from `~/.config/heru/profiles.toml` or `HERU_PROFILES_FILE` without changing engine identity.

### Base adapter contract

- `heru.base.ExternalCLIAdapter`: base class callers may subclass against; heru preserves its constructor shape and override points as the adapter contract.
- `heru.base.CLIInvocation`: immutable invocation description containing argv, cwd, env, and optional stdin payload for a launch.
- `heru.base.CLIExecutionResult`: immutable execution record containing exit status, stdout/stderr, pid, and transcript accessors for a finished run.

### Engine adapters

- `heru.adapters.codex.CodexCLIAdapter`: stable adapter for Codex CLI command building, transcript rendering, usage extraction, and continuation discovery.
- `heru.adapters.claude.ClaudeCLIAdapter`: stable adapter for Claude Code invocation and unified stream parsing.
- `heru.adapters.copilot.CopilotCLIAdapter`: stable adapter for GitHub Copilot CLI invocation and stream parsing.
- `heru.adapters.gemini.GeminiCLIAdapter`: stable adapter for Gemini CLI invocation, usage extraction, and continuation parsing.
- `heru.adapters.opencode.OpenCodeAdapter`: stable adapter for OpenCode CLI invocation and stream parsing under the current exported class name.
- `heru.adapters.goz.GozCLIAdapter`: stable adapter for Goz CLI invocation, transcript extraction, and continuation parsing.

### `heru.types` models

- `EngineUsageWindow`: normalized usage-window counters and reset metadata extracted from provider output.
- `EngineUsageObservation`: normalized per-run usage or quota observation reported by adapters.
- `UnifiedEvent`: stable JSONL event schema emitted across engines.
- `LiveEvent`: a `UnifiedEvent` instance used inside live timelines.
- `LiveTimeline`: ordered live-event container with summary counts and task/subagent metadata.
- `ResourceLimitEvent`: normalized process-resource failure details attached to stage reports.
- `RuntimeEngineContinuation`: stable continuation/session handle for resume flows across engines.
- `SubagentRef`: stable reference to a spawned subagent as reported in pipeline payloads.

### Internal modules

The modules below are internal implementation details and may change
without notice in any release:

- `heru._continuation`
- `heru.adapters._codex_impl`
- `heru.adapters._claude_impl`
- `heru.adapters._copilot_impl`
- `heru.adapters._gemini_impl`
- `heru.adapters._opencode_impl`
- `heru.adapters._goz_impl`

The same rule applies to private helpers and any `_`-prefixed name in a
public module unless this README explicitly lists it as part of the
stable contract.

### Stability Matrix

| heru version | Public API breakage |
| --- | --- |
| `v0.1.0` | Initial extracted public contract. No documented breaking public API changes yet. |
| `v1.0.0` | Breaking change: provider-specific quota dataclasses and CLI fields replaced by unified `short_term` / `long_term` usage windows. |
| `v2.0.0` | Breaking change: removed the legacy prompt-first `--engine` form; positional `heru <engine> <prompt>` is now required. |
| `v2.1.0` | Breaking change: removed `heru usage`; use `quse` for normalized quota reporting. |

### Submitting A Breaking Change

If you need to break any public API listed above:

1. Bump heru's semver major version in `pyproject.toml`.
2. Add a CHANGELOG entry that names the broken public API and the replacement.
3. Add a migration note for litehive and any other known consumer that relies on the changed contract.

## Tests

```bash
uv run pytest
```

`tests/contract/` is included in the default `uv run pytest` discovery. Those
tests are fixture-only and are intended to pass in a fresh heru-only virtualenv
without litehive, engine CLIs, or network access.

If a PR changes any assertion under `tests/contract/`, that PR is changing the
documented public contract and must also:

1. Bump heru's major version in `pyproject.toml`.
2. Add a `CHANGELOG` entry for the break.
3. Add a migration note describing what downstream users, including litehive, must change.

## Development

Install the versioned git hook with:

```bash
./scripts/install-hooks.sh
```

The installer symlinks `scripts/pre-commit.sh` into your real git hook directory using `git rev-parse --git-path hooks/pre-commit`, so it works from normal checkouts and git worktrees.

When staged files touch `heru/` or `tests/`, the hook treats that commit as a potential change to heru's public contract with litehive and runs this focused smoke suite in the litehive repo:

```bash
cd ../litehive && uv run pytest -q \
  tests/test_runner_workflow.py \
  tests/test_engine_variants_and_timeline.py \
  tests/test_heru_cli.py \
  tests/test_observability_and_status.py
```

The hook looks for litehive in a sibling checkout at `../litehive`, then falls back to `LITEHIVE_REPO`. If neither exists, it prints a warning and skips the smoke run so standalone heru users are not blocked.

The smoke suite is intended to protect the editable-install contract between heru and litehive: litehive imports heru directly from your local checkout, so breaking `heru` method names, CLI argv shape, or event schema can regress litehive immediately. If you need to bypass the hook intentionally, commit with `git commit --no-verify`, but that should be the exception rather than the default workflow.
