From 005f44f63867fbcaa8ea5480bc50308711063c84 Mon Sep 17 00:00:00 2001 From: anay Date: Wed, 29 Jul 2026 12:25:48 -0700 Subject: [PATCH] feat(cli): default checkpoint path from AGENT_HANDOVER_CHECKPOINT When --checkpoint is not given, fall back to the AGENT_HANDOVER_CHECKPOINT environment variable, then the current default. Precedence: --checkpoint > $AGENT_HANDOVER_CHECKPOINT > default. Closes #6 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019cFhMyHwVAQ9GieYbi3aUb --- README.md | 13 +++++++++++++ src/agent_handover/cli.py | 18 +++++++++++++++--- tests/test_cli.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9119cff..1b990dc 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/agent_handover/cli.py b/src/agent_handover/cli.py index 79d22b5..4020ee2 100644 --- a/src/agent_handover/cli.py +++ b/src/agent_handover/cli.py @@ -9,6 +9,7 @@ from __future__ import annotations import argparse +import os import sys from pathlib import Path @@ -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 diff --git a/tests/test_cli.py b/tests/test_cli.py index f6c5663..bc53191 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -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"]) @@ -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()