Skip to content
Open
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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,19 @@ In your agent's bootstrap (CLAUDE.md / AGENTS.md):
agent-handover check # exit 1 → finish the interrupted handover first
```

`--checkpoint` defaults to `.agent-handover/checkpoint.json`. To set the path
once for every command — useful when one repo is driven by several agents or
worktrees — export `AGENT_HANDOVER_CHECKPOINT`:

```bash
export AGENT_HANDOVER_CHECKPOINT=.agent-handover/worktree-a.json
agent-handover check # uses the env var; --checkpoint still wins over it
```

Precedence: `--checkpoint` > `$AGENT_HANDOVER_CHECKPOINT` > the built-in
default (for `run`, the env var also beats `[handover].checkpoint` in the
config file, like any other CLI override).

## Declarative handover (`agent-handover run`)

Don't want to write Python? Describe the handover in a TOML file and run it from
Expand Down
18 changes: 15 additions & 3 deletions src/agent_handover/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from __future__ import annotations

import argparse
import os
import sys
from pathlib import Path

Expand All @@ -17,22 +18,33 @@
DEFAULT_CHECKPOINT = Path(".agent-handover/checkpoint.json")


def _default_checkpoint(fallback: Path | None = DEFAULT_CHECKPOINT) -> Path | None:
"""Checkpoint default: $AGENT_HANDOVER_CHECKPOINT, then ``fallback``.

Precedence is ``--checkpoint`` > ``$AGENT_HANDOVER_CHECKPOINT`` > default,
so one repo driven from several agents or worktrees can set the path once
via the environment.
"""
env = os.environ.get("AGENT_HANDOVER_CHECKPOINT")
return Path(env) if env else fallback


def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="agent-handover")
sub = parser.add_subparsers(dest="command", required=True)

p_check = sub.add_parser("check", help="session-start guard (exit 0/1/2)")
p_check.add_argument("--checkpoint", type=Path, default=DEFAULT_CHECKPOINT)
p_check.add_argument("--checkpoint", type=Path, default=_default_checkpoint())

p_status = sub.add_parser("status", help="print checkpoint state")
p_status.add_argument("--checkpoint", type=Path, default=DEFAULT_CHECKPOINT)
p_status.add_argument("--checkpoint", type=Path, default=_default_checkpoint())

p_run = sub.add_parser("run", help="run a declarative handover from a TOML config")
p_run.add_argument("--config", type=Path, required=True)
p_run.add_argument("--note", default=None, help="session note (overrides [note].session)")
p_run.add_argument("--current-state", default=None, help="overrides [note].current_state")
p_run.add_argument("--no-push", action="store_true", help="force local-only (no git push)")
p_run.add_argument("--checkpoint", type=Path, default=None,
p_run.add_argument("--checkpoint", type=Path, default=_default_checkpoint(fallback=None),
help="override [handover].checkpoint")
return parser

Expand Down
33 changes: 33 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,27 @@ def test_check_exit_codes(tmp_path):
assert main(["check", "--checkpoint", str(cp_path)]) == 2


def test_checkpoint_env_var_used_when_flag_absent(tmp_path, monkeypatch):
env_cp = tmp_path / "env-cp.json"
Checkpoint(env_cp).start(["a"]) # pending step -> resume needed
monkeypatch.setenv("AGENT_HANDOVER_CHECKPOINT", str(env_cp))
assert main(["check"]) == 1


def test_checkpoint_flag_overrides_env_var(tmp_path, monkeypatch):
env_cp = tmp_path / "env-cp.json"
Checkpoint(env_cp).start(["a"]) # env checkpoint would exit 1
monkeypatch.setenv("AGENT_HANDOVER_CHECKPOINT", str(env_cp))
flag_cp = tmp_path / "flag-cp.json" # missing -> no resume needed
assert main(["check", "--checkpoint", str(flag_cp)]) == 0


def test_checkpoint_default_when_no_flag_or_env(tmp_path, monkeypatch):
monkeypatch.delenv("AGENT_HANDOVER_CHECKPOINT", raising=False)
monkeypatch.chdir(tmp_path) # no .agent-handover/ here
assert main(["check"]) == 0


def test_status_runs(tmp_path, capsys):
cp_path = tmp_path / "cp.json"
Checkpoint(cp_path).start(["a"])
Expand Down Expand Up @@ -55,3 +76,15 @@ def test_run_note_from_config_when_flag_absent(tmp_path):
cfg = _write_config(tmp_path, mem, cp)
assert main(["run", "--config", str(cfg)]) == 0
assert "from file" in (mem / "layer2" / "current-state.md").read_text(encoding="utf-8")


def test_run_checkpoint_env_var_overrides_config(tmp_path, monkeypatch):
mem = tmp_path / "memory"
cfg_cp = tmp_path / "config-cp.json"
cfg = _write_config(tmp_path, mem, cfg_cp)
env_cp = tmp_path / "env-cp.json"
monkeypatch.setenv("AGENT_HANDOVER_CHECKPOINT", str(env_cp))
assert main(["run", "--config", str(cfg)]) == 0
# env var acts as the CLI-level checkpoint, beating [handover].checkpoint
assert env_cp.exists()
assert not cfg_cp.exists()