Task: T-0005 Pre-commit hook that runs litehive's smoke tests against staged heru changes
Stage: accepting
Stage owner: reviewer
Process profile: Python
Task type: -

Workspace context:
# heru — Litehive Workspace Context

Process profile: Python

You are editing **heru**, a unified headless CLI for coding agents
(codex, claude, copilot, gemini, opencode, goz). Read this whole file
before touching any file in this repository.

## Project

- **Purpose:** a thin adapter layer that wraps each supported agent CLI
  behind a uniform Python interface, with a unified `heru <engine>
  <prompt>` command, one JSONL event schema across engines, and one
  resume mechanism. heru is the engine-I/O layer; it knows nothing
  about task queues, stages, orchestration, or sandboxing — those are
  litehive concerns.
- **Main package:** `heru/` (at the repo root, below `.git/`). Public
  API lives in `heru/__init__.py`, `heru/base.py`, `heru/types.py`,
  and each `heru/adapters/<engine>.py`. Anything prefixed with `_` is
  internal.
- **Commands to know:**
  - `uv sync --extra dev` — install heru's own venv
  - `uv run pytest` — heru's standalone test suite
  - `uv run heru <engine> <prompt>` — the CLI (WIP per task T-0002)
  - `cd ~/git/litehive && uv run pytest -q tests/test_runner_workflow.py tests/test_engine_variants_and_timeline.py tests/test_heru_cli.py tests/test_codex_quota.py` — **litehive contract smoke tests; run before every commit**

## Directory layout

```
heru/
├── heru/                  # the Python package
│   ├── __init__.py        # get_engine, ENGINE_CHOICES — PUBLIC
│   ├── base.py            # ExternalCLIAdapter, CLIInvocation, run_live — PUBLIC
│   ├── types.py           # shared pydantic types — PUBLIC
│   ├── main.py            # `heru <engine> <prompt>` CLI entry — PUBLIC
│   ├── _engine_detection.py   # internal
│   ├── _continuation.py       # internal
│   ├── adapters/
│   │   ├── codex.py, _codex_impl.py         # CodexCLIAdapter PUBLIC, _impl internal
│   │   ├── claude.py, _claude_impl.py
│   │   ├── copilot.py, _copilot_impl.py
│   │   ├── gemini.py, _gemini_impl.py
│   │   ├── opencode.py, _opencode_impl.py
│   │   ├── goz.py, _goz_impl.py
│   │   └── common.py      # shared helpers
│   └── quota/             # per-provider quota check helpers — PUBLIC
├── tests/                 # heru's standalone unit tests (no litehive imports)
├── pyproject.toml
├── README.md
└── .litehive/             # this workspace (tasks, state)
```

## Public API surface — do not break without a semver bump

Anything **not** prefixed with `_` is public. Changing the signature,
return type, or behavior of any of the following is a **semver-major**
breaking change that requires a version bump in `pyproject.toml`, a
CHANGELOG entry, and a migration note for litehive:

- `heru.get_engine(name)`, `heru.ENGINE_CHOICES`
- `heru.base.ExternalCLIAdapter` (subclass, don't modify shape)
- `heru.base.CLIInvocation`, `heru.base.CLIExecutionResult`
- Each adapter class: `CodexCLIAdapter`, `ClaudeCLIAdapter`,
  `CopilotCLIAdapter`, `GeminiCLIAdapter`, `OpencodeCLIAdapter`,
  `GozCLIAdapter`
- Every pydantic model in `heru/types.py`
- `heru.main.main` (the CLI entry)

**Internal** (change freely, no semver implications): every
`_`-prefixed module, every private method on public classes, and the
exact text of rendered transcripts.

## Hard rules — do NOT break these

1. **No imports from `litehive`.** heru is a leaf package. One runtime
   dep: `pydantic`. If your diff adds `from litehive …`, stop — the
   code belongs in litehive, not heru.

2. **Do not break litehive.** litehive depends on heru via an editable
   path install, so every heru change propagates immediately. Before
   you finish any stage that touched code, you MUST run:
   ```
   cd ~/git/litehive && uv run pytest tests/test_runner_workflow.py \
       tests/test_engine_variants_and_timeline.py \
       tests/test_heru_cli.py tests/test_codex_quota.py -q
   ```
   If any fail, your change broke the contract. Either fix heru to
   preserve the contract, or explicitly call out the breakage in your
   stage report with a proposed litehive-side migration.

3. **Stage reports are a litehive concept, not heru's.** `StageReport`,
   `StageResultSubmission`, `StageResultTests`, and `TaskUpdateSubmission`
   currently live in `heru/types.py` as a temporary compromise — they
   are slated to move to litehive. Do not add new stage-report logic
   or fields inside heru. If your task requires you to edit stage
   report semantics, flag it back: it is misfiled and belongs in
   litehive.

4. **Tests must be standalone.** No test under `tests/` may import
   from `litehive`. Every heru test must run in a fresh venv with
   only `heru + pydantic + pytest` installed. For JSONL fixtures,
   hand-craft strings or files under `tests/fixtures/` — do NOT
   shell out to real engine CLIs during tests.

5. **Respect the contract tests** (once `tests/contract/` exists per
   T-0007). Changing an assertion in `tests/contract/` is by
   definition a breaking change and requires the full semver
   checklist.

6. **Every new adapter needs both a public class AND a `_impl` helper
   module.** Shape: `adapters/<engine>.py` for the public class,
   `adapters/_<engine>_impl.py` for parsing/quota internals. Keep the
   symmetry — the base class expects it.

## Before you write code — ask yourself

- **Is this a heru concern or a litehive concern?** If you need task
  queues, retries, worktrees, commit pipelines, sandbox profiles, or
  the `STAGE_RESULT:` agent protocol, this is probably litehive.
  Stop and surface the concern in grooming.
- **Does this change the public API?** If yes, note the intended
  semver bump in your report and explicitly list what litehive needs
  to update.
- **Can I write this as a fixture-driven unit test?** If not, your
  code is probably too coupled to the real CLI subprocess — refactor
  to take parsed inputs.
- **Have I run the litehive contract tests?** If not, the task is not
  done, no matter what your implementation report says.

## Relationship to litehive

heru is maintained **alongside** litehive at `~/git/litehive`. They
share a developer (you) and live on the same laptop. litehive depends
on heru via `uv add --editable ~/git/heru`, so every heru save is
immediately visible to litehive without any reinstall.

**If a heru task requires changes in litehive to stay consistent**
(e.g. adding a new adapter method that litehive needs to call), open
a companion task in the litehive workspace referencing this task,
and land the heru side FIRST. Litehive can then safely bump its own
expectations.

## Process overlay
- Source of truth: tasks and implementation state live under `.litehive/`.
- Task source of truth: issues or task records define scope; prompts and transcripts are supporting evidence.
- Orchestrator model: the local runner is the manager and owns stage routing.
- Routing model: routing stays deterministic and local; subagents execute assigned stages but do not self-route.
- Shared stages: grooming -> implementing -> testing -> accepting -> commit_to_git.
- Role model: `planner` frames the task, `reviewer` performs final PM-style acceptance, `swe` edits code, and `qa` runs focused verification.
- TDD expectations: add or update focused tests near the changed Python module before broad suites.
- Verification discipline: prefer targeted `pytest` evidence close to the changed module before broader smoke coverage.
- Acceptance flow: verify behavior with targeted `pytest` coverage and note any residual risk.
- Commit and recovery: keep checkpoint commits deterministic and easy to recover.

## Project overlay
- Python package or application workflow with pytest-oriented verification.
- Favor incremental, reviewable changes over broad refactors.
- Keep implementation, verification, and acceptance evidence explicit.
- Prefer focused `pytest` coverage for the changed modules.
- Keep dependency and packaging changes explicit and minimal.

## Init scaffold
- Scaffold `.litehive/context.md` from the generic base process template.
- Layer the project profile summary, workspace overlay, and stage overlay onto that base.
- Treat process profiles as overlays on the shared contract rather than separate workflows.
- Keep the task/issue source of truth, verification commands, and recovery policy visible in the scaffold.
- Seed Python workspaces with package layout, test entrypoints, and `uv` or virtualenv expectations.

## Prompt scaffold
- Start from the shared process contract, then add repository context and task data.
- Combine the generic base prompt with the selected project overlay instead of replacing the base.
- Apply stage defaults first, then append any project-specific stage overlay for that step.
- Keep stage prompts explicit about role, verification expectations, and final report format.

## Stage prompt scaffolding

### grooming
Act as the planner: clarify the user problem, inspect the repo if needed, and produce a concrete execution plan.
Focus on scope clarification, acceptance criteria quality, decomposition, follow-up tasks, and PM sizing.
Do not make code changes in this stage.

### implementing
Implement the task in this repository.
Keep changes tightly scoped and complete the work needed for the acceptance criteria.
Write tests so each assertion would fail if the feature is broken.
Do not spend test coverage on framework behavior or library guarantees.
Do not add tests that only restate defaults, constants, or static data.
Keep each test focused on one behavior.
Avoid duplicate coverage; extend an existing test only when it is the same behavior.
- Write or update focused tests alongside the code change when feasible.
- Use `pytest` for automated verification.
- Use `tmp_path` or pytest fixtures instead of manual tempfiles in repo code or `/tmp` setup.
- Mock external calls and integration edges, not the internal logic under test.
- Do not use `time.sleep` in tests; use deterministic synchronization or time control.

### testing
Validate the implementation.
Run focused checks or tests where possible and report failures precisely.
Only make minimal fixes if absolutely necessary.
Reject tests whose assertions would still pass if the feature were broken.
Reject tests that duplicate existing coverage instead of covering a new behavior.
Reject tests longer than 50 lines unless a shorter structure is genuinely impossible.
Reject monolithic tests that exercise 5 or more behaviors in one flow.
- Prefer targeted `pytest` invocations before broader test commands.
- Verify new or updated tests use `pytest` idioms and fixtures.
- Reject tests that use manual tempfiles where `tmp_path` would make isolation explicit.
- Reject tests that mock the unit's internal logic instead of external boundaries.
- Reject tests that rely on `time.sleep` instead of deterministic control.

### accepting
Act as the reviewer: validate the end-user outcome against the acceptance criteria and decide whether it should be accepted or sent back.
Be strict about regression detection, evidence quality, and final done versus not-done judgment.

## Python specifics
- Prefer `pytest` for automated verification.
- Keep module boundaries and import hygiene clear.
- Record virtualenv, `uv`, or toolchain expectations when they matter.

## Development rules
- Keep changes scoped to the current task.
- Prefer targeted tests over broad test suites.
- Record assumptions clearly in the final report.

## Tool usage
- Use `uv run pytest -q` for the current smoke test suite.
- Update litehive task artifacts instead of inventing external state stores.
- If you add a new command or workflow, document it here for future runs.

Shared process:
- Orchestrator model: the local runner is the manager and owns stage routing.
- Routing model: routing stays deterministic and local; subagents execute assigned stages but do not self-route.
- Shared stages: grooming -> implementing -> testing -> accepting -> commit_to_git.
- Role model: `planner` frames the task, `reviewer` performs final PM-style acceptance, `swe` edits code, and `qa` runs focused verification.
- Source of truth: tasks and implementation state live under `.litehive/`.
- Task source of truth: issues or task records define scope; prompts and transcripts are supporting evidence.
- TDD expectations: add or update focused tests near the changed Python module before broad suites.
- Verification discipline: prefer targeted `pytest` evidence close to the changed module before broader smoke coverage.
- Acceptance flow: verify behavior with targeted `pytest` coverage and note any residual risk.
- Commit and recovery: keep checkpoint commits deterministic and easy to recover.

Project overlay:
- Python package or application workflow with pytest-oriented verification.
- Favor incremental, reviewable changes over broad refactors.
- Keep implementation, verification, and acceptance evidence explicit.
- Prefer focused `pytest` coverage for the changed modules.
- Keep dependency and packaging changes explicit and minimal.

Prompt scaffold:
- Start from the shared process contract, then add repository context and task data.
- Combine the generic base prompt with the selected project overlay instead of replacing the base.
- Apply stage defaults first, then append any project-specific stage overlay for that step.
- Keep stage prompts explicit about role, verification expectations, and final report format.

Role focus:
- You are the reviewer, a PM-style role representing the user's and product's point of view.
- Validate the strict end-user outcome, look for regressions or missing evidence, and make a final done versus not-done judgment.
- Reject work that is incomplete, weakly verified, or misaligned with the promised outcome.
- If SWE shows the requested work was already implemented before this run and provides concrete verification evidence, accept the task to normal `done` rather than inventing a special closed status.
- Use `wont_do`, `duplicate`, or `deferred` only when the task is genuinely obsolete, superseded, or duplicated.
- You may close a task as duplicate, wont_do, or deferred by including `outcome: <status>` with optional `outcome_reason` in a TASK_UPDATE block. You may park a task with `action: park`.

Stage instructions:
Act as the reviewer: validate the end-user outcome against the acceptance criteria and decide whether it should be accepted or sent back.
Be strict about regression detection, evidence quality, and final done versus not-done judgment.

Goal:
heru is depended on by litehive via an editable path install. Any change to heru that breaks litehive's contract (e.g. renames a method, changes argv ordering, removes a class) silently breaks litehive on the next run. Install a git pre-commit hook in heru that, when files under heru/ or tests/ are staged, automatically runs litehive's focused smoke test suite against the staged state. If the smoke tests fail, the commit is rejected with a clear diagnostic. This is the biggest-ROI defense against heru→litehive regression because it enforces the contract at commit time rather than relying on developer discipline.

Acceptance criteria:
- A pre-commit hook script is committed under scripts/pre-commit.sh (so it can be versioned) and installed via a 'make install-hooks' or 'scripts/install-hooks.sh' helper that symlinks it into .git/hooks/pre-commit
- The hook detects when staged files include anything under heru/ or tests/ and, if so, runs a focused 'uv run pytest' against litehive's contract test set (test_runner_workflow.py, test_engine_variants_and_timeline.py, test_heru_cli.py, test_codex_quota.py, test_observability_and_status.py — whichever exercise heru's public API)
- The hook discovers litehive's path automatically: tries a sibling-dir heuristic (../litehive), falls back to an env var LITEHIVE_REPO, and skips with a warning if neither is found (so the hook is not a hard blocker for users who don't develop litehive alongside)
- The hook runs in under 2 minutes on a warm venv, producing clear output on which tests failed if any
- README has a 'Development' section explaining the hook, how to install it, how to skip it with --no-verify when intentional, and what the litehive contract is

Plan:
- No plan defined.

PM sizing:
- Current PM complexity: simple
- Current planned effort: s

Constraints:
- Keep changes scoped to the task.


Discussion thread:

--- [2026-04-10T20:01:10+00:00] planner (grooming) — pass ---
Passed grooming. I verified the repo currently has no existing hook installer or Development section in README, so the implementation scope is still cleanly bounded to this task. I also verified litehive currently contains the intended smoke targets: tests/test_runner_workflow.py, tests/test_engine_variants_and_timeline.py, tests/test_heru_cli.py, tests/test_codex_quota.py, and tests/test_observability_and_status.py. One important implementation detail surfaced during grooming: this task is being executed from a Git worktree, and git rev-parse --git-path hooks/pre-commit resolves to /home/alexey/git/heru/.git/hooks/pre-commit even though the worktree top-level is nested under .litehive/worktrees, so the installer should resolve the real hook path via Git rather than assuming .git/hooks under the current directory. No code changes were made in grooming and no pytest suite was run because this stage is planning-only. Acceptance criteria are clear enough to proceed. Recommended execution plan: 1. Add a versioned scripts/pre-commit.sh that inspects staged paths and no-ops unless staged files touch heru/ or tests/. 2. In that hook, discover the heru repo root and then locate litehive by trying the sibling of the canonical heru repo root first, then LITEHIVE_REPO, and warn-skip if neither exists. 3. Run the focused litehive smoke suite with clear failure output and a concise success/skip message. 4. Add scripts/install-hooks.sh or make install-hooks using git rev-parse --git-path hooks/pre-commit to install a symlink safely in normal repos and worktrees. 5. Update README with a Development section covering installation, automatic smoke enforcement, the litehive contract, and intentional bypass with --no-verify. PM sizing remains appropriate at complexity simple, effort s.

--- [2026-04-10T20:07:39+00:00] swe (implementing) — pass ---
Implemented a versioned git hook workflow for heru->litehive contract enforcement. Added scripts/pre-commit.sh to detect staged changes under heru/ or tests/, locate litehive via sibling ../litehive or LITEHIVE_REPO, temporarily isolate unstaged worktree/untracked changes so the smoke run sees staged state, and run: uv run pytest -q tests/test_runner_workflow.py tests/test_engine_variants_and_timeline.py tests/test_heru_cli.py tests/test_codex_quota.py tests/test_observability_and_status.py. The hook exits 1 with clear diagnostics when the smoke suite fails, exits 0 with a warning when litehive is not available, and remains silent for unrelated staged paths. Added scripts/install-hooks.sh to symlink the versioned hook into the real hook path resolved by git rev-parse --git-path hooks/pre-commit, which works from a normal checkout and from a git worktree. Added focused pytest coverage in tests/test_pre_commit_hook.py covering staged-state execution, sibling discovery, LITEHIVE_REPO fallback, warning skip when litehive is missing, failure rejection, and hook installation into a worktree-backed repo hook dir. Updated README with a Development section documenting installation, the enforced litehive contract smoke suite, automatic repo discovery, and intentional bypass with git commit --no-verify. Verification run: uv run pytest -q tests/test_pre_commit_hook.py tests/test_cli_smoke.py -> 8 passed. Required litehive contract run: cd /home/alexey/git/litehive && uv run pytest -q tests/test_runner_workflow.py tests/test_engine_variants_and_timeline.py tests/test_heru_cli.py tests/test_codex_quota.py tests/test_observability_and_status.py -> 286 passed, 3 warnings. Note: the public `litehive report` CLI currently crashes with AttributeError: Namespace has no attribute files_changed, so this report was submitted through litehive.cli.report._cmd_report with files_changed=[].

--- [2026-04-10T20:09:19+00:00] qa (testing) — reject ---
EXPECTED behavior: the pre-commit hook should run litehive's focused smoke suite whenever staged files include anything under heru/ or tests/, including deletions. OBSERVED behavior: scripts/pre-commit.sh filters staged paths with 'git diff --cached --name-only -z --diff-filter=ACMR' at lines 108-116, so staged deletions are excluded and the hook exits 0 without invoking uv. I reproduced this in a fresh temp repo by committing heru/__init__.py, deleting it with 'git rm heru/__init__.py', placing a fake 'uv' on PATH, and running 'bash scripts/pre-commit.sh'; result: exit status 0, fake uv was not invoked, and stderr was empty. Steps to reproduce: 1) create a git repo with scripts/pre-commit.sh plus a tracked file under heru/, 2) commit, 3) run 'git rm heru/__init__.py', 4) put a fake uv executable first on PATH, 5) run 'bash scripts/pre-commit.sh', 6) observe that it exits successfully without running uv. Acceptance criteria status: satisfied - versioned hook exists under scripts/pre-commit.sh, installer exists under scripts/install-hooks.sh and works in a worktree, README Development section documents installation/discovery/--no-verify, targeted pytest passes (uv run pytest -q tests/test_pre_commit_hook.py -> 5 passed), and the required litehive smoke suite passes (286 passed, 3 warnings). Not met - the hook does not detect all staged files under heru/ or tests/ because deletions are skipped, so commit-time contract enforcement is incomplete.

--- [2026-04-10T20:11:21+00:00] recovery (implementing) — pass ---
Root cause: scripts/pre-commit.sh only considered staged paths from git diff --cached --name-only -z --diff-filter=ACMR, so staged deletions under heru/ or tests/ never triggered the litehive smoke suite and commits could bypass contract enforcement. Fix made: updated the hook to include deletions by changing the staged-path filter to ACMRD, and added a focused regression test that stages git rm heru/__init__.py and verifies the hook still invokes uv against the litehive smoke suite. Verification: uv run pytest -q tests/test_pre_commit_hook.py -> 6 passed, including the new staged-deletion case; cd /home/alexey/git/litehive && uv run pytest -q tests/test_runner_workflow.py tests/test_engine_variants_and_timeline.py tests/test_heru_cli.py tests/test_codex_quota.py tests/test_observability_and_status.py -> 286 passed, 3 warnings. Evidence now matches the acceptance criteria: versioned hook and installer remain in place, README documentation was already present from the prior attempt, staged changes under heru/ or tests/ including deletions now trigger the contract suite, litehive path discovery behavior is unchanged, and failing smoke tests still reject the commit with clear diagnostics. The implementing stage should be retried from this updated state.

--- [2026-04-10T20:13:07+00:00] qa (testing) — pass ---
Verified the pre-commit hook workflow end-to-end. I inspected scripts/pre-commit.sh and confirmed staged-path detection now includes deletions via --diff-filter=ACMRD, only triggers for staged paths under heru/ or tests/, discovers litehive via sibling ../litehive before LITEHIVE_REPO, warns and skips when litehive is unavailable, and rejects the commit on smoke-test failure with clear diagnostics. I inspected scripts/install-hooks.sh and confirmed it installs the versioned hook by symlinking scripts/pre-commit.sh into the real hook path from git rev-parse --git-path hooks/pre-commit, which is the right behavior for normal repos and worktrees. I inspected README.md and confirmed the new Development section documents hook installation, the enforced litehive smoke suite, sibling/env-var discovery, the editable-install contract with litehive, and intentional bypass via git commit --no-verify. Verification run in heru: uv run pytest -q tests/test_pre_commit_hook.py -> 6 passed in 0.32s. That suite includes the regression for staged deletions and covers staged-state execution, sibling discovery, LITEHIVE_REPO fallback, warning-skip behavior, failure rejection, and worktree hook installation. Required litehive contract verification: cd /home/alexey/git/litehive && uv run pytest -q tests/test_runner_workflow.py tests/test_engine_variants_and_timeline.py tests/test_heru_cli.py tests/test_codex_quota.py tests/test_observability_and_status.py -> 286 passed, 3 warnings in 24.85s. This evidence satisfies the acceptance criteria.

IMPORTANT: When you are done, you MUST submit your verdict by running:
  litehive report --verdict <pass|reject> --role reviewer --step accepting --message "<your report>"
The environment variable LITEHIVE_TASK_ID is set to T-0005 for this session. Workspace and task resolution should use the injected environment automatically.

Your --message is the PRIMARY way the next agent understands what happened.
Do NOT rely on your raw transcript being read — write the report as if it is the only thing the next agent will see.

Report requirements:
- On PASS/ACCEPT: explain what you verified, what tests you ran, what evidence confirms the acceptance criteria are met.
- On REJECT: you MUST include ALL of the following:
  1. EXPECTED behavior: what should happen according to the acceptance criteria
  2. OBSERVED behavior: what actually happens (exact error messages, test output, wrong values)
  3. Steps to reproduce: the exact command or test that demonstrates the gap
  4. Which acceptance criteria are not met and which ones are already satisfied

A vague rejection like 'tests fail' or 'missing evidence' is useless and causes infinite loops.
A good rejection looks like: 'Expected: `litehive engine gemini` switches the default engine and prints confirmation. Observed: command exits 0 but config.yaml still shows the old engine. Reproduce: run `litehive engine gemini` then `cat .litehive/config.yaml`. Criteria 1-3 are met, criterion 4 (persistence) is not.'
