cryochamber 0.1.2

A hibernation chamber for AI agents — schedule, wake, and manage long-running agent tasks
Documentation
# Makefile for cryochamber

.PHONY: help build test fmt fmt-check clippy check clean example-clean coverage run-plan logo example example-cancel time check-agent check-round-trip check-gh check-service check-mock cli book book-serve book-deploy copilot-review release

# Default target
help:
	@echo "Available targets:"
	@echo "  build        - Build the project"
	@echo "  test         - Run all tests"
	@echo "  fmt          - Format code with rustfmt"
	@echo "  fmt-check    - Check code formatting"
	@echo "  clippy       - Run clippy lints"
	@echo "  check        - Quick check (fmt + clippy + test)"
	@echo "  coverage     - Generate coverage report (requires cargo-llvm-cov)"
	@echo "  clean        - Clean build artifacts (cargo clean)"
	@echo "  example-clean - Remove auto-generated files from examples"
	@echo "  logo         - Compile logo (requires typst)"
	@echo "  run-plan     - Execute a plan with Claude headless autorun"
	@echo "  example      - Run an example (DIR=examples/mr-lazy or DIR=examples/chess-by-mail)"
	@echo "  example-cancel - Stop a running example (DIR=examples/...)"
	@echo "  time         - Show current time or compute offset (OFFSET=\"+1 day\")"
	@echo "  check-agent  - Quick agent smoke test (runs agent once)"
	@echo "  check-round-trip - Full round-trip test with mr-lazy (daemon, Ctrl-C to stop)"
	@echo "  check-gh     - Verify GitHub Discussion sync (requires gh auth)"
	@echo "  check-service - Verify OS service install/uninstall (launchd/systemd)"
	@echo "  check-mock   - Run mock agent integration tests"
	@echo "  cli          - Install the cryo CLI locally"
	@echo "  book         - Build mdbook documentation"
	@echo "  book-serve   - Serve mdbook locally with live reload"
	@echo "  book-deploy  - Deploy mdbook to GitHub Pages (gh-pages branch)"
	@echo "  copilot-review - Request Copilot code review on current PR"
	@echo "  release V=x.y.z - Tag and push a release (triggers CI publish)"

# Build the project
build:
	cargo build

# Run all tests
test:
	cargo test

# Format code
fmt:
	cargo fmt --all

# Check formatting
fmt-check:
	cargo fmt --all -- --check

# Run clippy
clippy:
	cargo clippy --all-targets -- -D warnings

# Quick check before commit
check: fmt-check clippy test
	@echo "All checks passed!"

# Generate coverage report (requires: cargo install cargo-llvm-cov)
coverage:
	@command -v cargo-llvm-cov >/dev/null 2>&1 || { echo "Installing cargo-llvm-cov..."; cargo install cargo-llvm-cov; }
	cargo llvm-cov --workspace --html --open

# Compile logo (requires typst)
logo:
	typst compile docs/logo/logo.typ docs/logo/logo.svg
	typst compile docs/logo/logo.typ docs/logo/logo.png --ppi 300

# Clean build artifacts
clean:
	cargo clean

# Remove auto-generated files from examples (cancels running daemons first)
example-clean:
	@for dir in examples/*/; do \
		if [ -f "$(CURDIR)/$$dir/timer.json" ]; then \
			cd "$(CURDIR)/$$dir" && $(CURDIR)/target/debug/cryo cancel 2>/dev/null; \
		fi; \
	done; true
	rm -f examples/*/CLAUDE.md examples/*/AGENTS.md
	rm -f examples/*/*.log examples/*/*.json
	rm -rf examples/*/messages examples/*/.cryo

# Run a plan with Claude in headless mode
# Usage: make run-plan [INSTRUCTIONS="..."] [OUTPUT=output.log] [AGENT_TYPE=claude]
# PLAN_FILE defaults to the most recently modified file in docs/plans/
INSTRUCTIONS ?=
OUTPUT ?= claude-output.log
AGENT_TYPE ?= claude
PLAN_FILE ?= $(shell ls -t docs/plans/*.md 2>/dev/null | head -1)

run-plan:
	@NL=$$'\n'; \
	BRANCH=$$(git branch --show-current); \
	if [ "$(AGENT_TYPE)" = "claude" ]; then \
		PROCESS="1. Read the plan file$${NL}2. Use /subagent-driven-development to execute tasks$${NL}3. Push: git push origin $$BRANCH$${NL}4. Create a pull request"; \
	else \
		PROCESS="1. Read the plan file$${NL}2. Execute the tasks step by step. For each task, implement and test before moving on.$${NL}3. Push: git push origin $$BRANCH$${NL}4. Create a pull request"; \
	fi; \
	PROMPT="Execute the plan in '$(PLAN_FILE)'."; \
	if [ -n "$(INSTRUCTIONS)" ]; then \
		PROMPT="$${PROMPT}$${NL}$${NL}## Additional Instructions$${NL}$(INSTRUCTIONS)"; \
	fi; \
	PROMPT="$${PROMPT}$${NL}$${NL}## Process$${NL}$${PROCESS}$${NL}$${NL}## Rules$${NL}- Tests should be strong enough to catch regressions.$${NL}- Do not modify tests to make them pass.$${NL}- Test failure must be reported."; \
	echo "=== Prompt ===" && echo "$$PROMPT" && echo "===" ; \
	claude --dangerously-skip-permissions \
		--model opus \
		--verbose \
		--max-turns 500 \
		-p "$$PROMPT" 2>&1 | tee "$(OUTPUT)"

# Install the cryo CLI
cli:
	cargo install --path .

# Run an example
# Usage: make example DIR=examples/mr-lazy
#        make example DIR=examples/chess-by-mail AGENT=claude
example: build
	@if [ -z "$(DIR)" ]; then echo "Usage: make example DIR=examples/mr-lazy"; exit 1; fi
	@if [ -f "$(DIR)/timer.json" ]; then (cd "$(DIR)" && $(CURDIR)/target/debug/cryo cancel 2>/dev/null); fi; \
	cd "$(DIR)" && rm -rf .cryo timer.json cryo.log cryo-agent.log messages AGENTS.md CLAUDE.md && \
	$(CURDIR)/target/debug/cryo init --agent "$(AGENT)" && $(CURDIR)/target/debug/cryo start --agent "$(AGENT)" && \
	$(CURDIR)/target/debug/cryo web

# Stop a running example
# Usage: make example-cancel DIR=examples/chess-by-mail
example-cancel:
	cd "$(DIR)" && $(CURDIR)/target/debug/cryo cancel

# Quick smoke test: force one agent wakeup cycle
# Usage: make check-agent                 # check default (opencode)
#        make check-agent AGENT=claude    # check claude
AGENT ?= opencode
CHECK_TIMEOUT ?= 3000

check-agent: build
	@TMPDIR=$$(mktemp -d /tmp/cryo-check-XXXXXX); \
	cp examples/mr-lazy/plan.md "$$TMPDIR/plan.md"; \
	cd "$$TMPDIR" && $(CURDIR)/target/debug/cryo init --agent "$(AGENT)"; \
	echo "=== Agent Health Check ==="; \
	echo "Agent: $(AGENT)"; \
	echo ""; \
	cd "$$TMPDIR" && $(CURDIR)/target/debug/cryo start \
		--agent "$(AGENT)" \
		--max-session-duration $(CHECK_TIMEOUT) 2>&1; \
	RC=$$?; \
	if [ $$RC -ne 0 ]; then \
		echo "FAIL: cryo start failed (exit code $$RC)"; \
		rm -rf "$$TMPDIR"; \
		exit 1; \
	fi; \
	echo ""; \
	echo "=== Session Log (Ctrl-C to stop) ==="; \
	trap 'cd "'"$$TMPDIR"'" && '"$(CURDIR)"'/target/debug/cryo cancel 2>/dev/null; rm -rf "'"$$TMPDIR"'"; exit 0' INT TERM; \
	cd "$$TMPDIR" && $(CURDIR)/target/debug/cryo watch --all; \
	cd "$$TMPDIR" && $(CURDIR)/target/debug/cryo cancel 2>/dev/null; \
	rm -rf "$$TMPDIR"

# Full round-trip test with mr-lazy example (daemon mode)
# Runs until plan completes or Ctrl-C, then cleans up.
# Usage: make check-round-trip                 # check default (opencode)
#        make check-round-trip AGENT=claude    # check claude
check-round-trip: build
	@echo "=== Round-Trip Test (mr-lazy) ==="
	@PROG=$$(echo "$(AGENT)" | awk '{print $$1}'); \
	echo "Agent:   $(AGENT)"; \
	echo "Timeout: $(CHECK_TIMEOUT)s"; \
	echo ""; \
	echo "1. Checking if $$PROG is in PATH..."; \
	if command -v "$$PROG" >/dev/null 2>&1; then \
		echo "   OK: $$(command -v $$PROG)"; \
	else \
		echo "   FAIL: '$$PROG' not found in PATH"; exit 1; \
	fi; \
	echo ""; \
	echo "2. Starting mr-lazy daemon..."; \
	TMPDIR=$$(mktemp -d /tmp/cryo-check-XXXXXX); \
	cp examples/mr-lazy/plan.md "$$TMPDIR/plan.md"; \
	cd "$$TMPDIR" && $(CURDIR)/target/debug/cryo init --agent "$(AGENT)"; \
	cd "$$TMPDIR" && $(CURDIR)/target/debug/cryo start \
		--agent "$(AGENT)" \
		--max-session-duration $(CHECK_TIMEOUT) 2>&1; \
	RC=$$?; \
	echo ""; \
	if [ $$RC -ne 0 ]; then \
		echo "   FAIL: cryo daemon failed to start (exit code $$RC)"; \
		echo "   Last 10 lines of log:"; \
		tail -10 "$$TMPDIR/cryo.log" 2>/dev/null | sed 's/^/   | /' || echo "   (no log)"; \
		rm -rf "$$TMPDIR"; \
		exit 1; \
	fi; \
	echo "   OK: Daemon started. Watching log (Ctrl-C to stop)..."; \
	echo ""; \
	trap 'echo ""; echo "Stopping daemon..."; cd "'"$$TMPDIR"'" && '"$(CURDIR)"'/target/debug/cryo cancel 2>/dev/null; rm -rf "'"$$TMPDIR"'"; echo "=== Done ==="; exit 0' INT TERM; \
	cd "$$TMPDIR" && $(CURDIR)/target/debug/cryo watch --all; \
	echo ""; \
	cd "$$TMPDIR" && $(CURDIR)/target/debug/cryo cancel 2>/dev/null; \
	rm -rf "$$TMPDIR"; \
	echo "=== Round-trip test done ==="

# Verify GitHub Discussion sync (requires: gh auth login)
# Usage: make check-gh REPO="owner/repo"
REPO ?= GiggleLiu/cryochamber

check-gh: build
	@echo "=== GitHub Sync Check ==="
	@echo "1. Checking gh CLI..."; \
	if command -v gh >/dev/null 2>&1; then \
		echo "   OK: $$(command -v gh)"; \
	else \
		echo "   FAIL: 'gh' not found. Install: https://cli.github.com"; exit 1; \
	fi; \
	echo ""; \
	echo "2. Checking gh authentication..."; \
	if gh auth status >/dev/null 2>&1; then \
		echo "   OK: authenticated as $$(gh api user -q .login)"; \
	else \
		echo "   FAIL: not authenticated. Run: gh auth login"; exit 1; \
	fi; \
	echo ""; \
	echo "3. Creating test Discussion in $(REPO)..."; \
	TMPDIR=$$(mktemp -d /tmp/cryo-check-gh-XXXXXX); \
	printf '# Health Check\n\nThis is an automated test.\n' > "$$TMPDIR/plan.md"; \
	cd "$$TMPDIR" && \
	$(CURDIR)/target/debug/cryo-gh init --repo "$(REPO)" --title "[Cryo] Health Check $$(date +%Y%m%d-%H%M%S)"; \
	RC=$$?; \
	if [ $$RC -ne 0 ]; then \
		echo "   FAIL: could not create Discussion"; \
		rm -rf "$$TMPDIR"; \
		exit 1; \
	fi; \
	echo "   OK: Discussion created"; \
	echo ""; \
	echo "4. Posting test comment..."; \
	mkdir -p "$$TMPDIR/messages/inbox"; \
	printf '--- CRYO SESSION 1 ---\ntask: health check\nagent: gh-check\ninbox: 0 messages\n[00:00:01] agent started (pid 1)\n[00:00:02] hibernate: complete, exit=0, summary="Health check passed"\n[00:00:02] agent exited (code 0)\n--- CRYO END ---\n' > "$$TMPDIR/cryo.log"; \
	printf '{"plan_path":"plan.md","session_number":1,"last_command":null,"pid":null,"max_retries":1,"retry_count":0,"max_session_duration":300,"watch_inbox":false,"daemon_mode":false}' > "$$TMPDIR/timer.json"; \
	$(CURDIR)/target/debug/cryo-gh push; \
	RC=$$?; \
	if [ $$RC -ne 0 ]; then \
		echo "   FAIL: could not post comment"; \
		rm -rf "$$TMPDIR"; \
		exit 1; \
	fi; \
	echo "   OK: comment posted"; \
	rm -rf "$$TMPDIR"; \
	echo ""; \
	echo "=== GitHub sync check passed ==="

# Verify OS service install/uninstall lifecycle (launchd on macOS, systemd on Linux)
# This test installs a real service, verifies it runs, cancels it, and cleans up.
# Usage: make check-service
#        make check-service AGENT="opencode run"
check-service: build
	@echo "=== Service Lifecycle Check ==="
	@echo "Platform: $$(uname -s)"
	@echo ""
	@echo "1. Setting up test project..."
	@TMPDIR=$$(mktemp -d /tmp/cryo-check-svc-XXXXXX); \
	cp examples/mr-lazy/plan.md "$$TMPDIR/plan.md"; \
	cd "$$TMPDIR" && $(CURDIR)/target/debug/cryo init --agent "$(AGENT)"; \
	echo "   OK: $$TMPDIR"; \
	echo ""; \
	echo "2. Installing daemon service (cryo start)..."; \
	cd "$$TMPDIR" && $(CURDIR)/target/debug/cryo start \
		--agent "/bin/sh -c 'sleep 600'" \
		--max-session-duration 600 2>&1; \
	RC=$$?; \
	if [ $$RC -ne 0 ]; then \
		echo "   FAIL: cryo start failed (exit $$RC)"; \
		rm -rf "$$TMPDIR"; exit 1; \
	fi; \
	echo "   OK: service installed"; \
	echo ""; \
	echo "3. Verifying service is running..."; \
	sleep 2; \
	if [ "$$(uname -s)" = "Darwin" ]; then \
		SVC_FILE=$$(ls -t ~/Library/LaunchAgents/com.cryo.daemon.*.plist 2>/dev/null | head -1); \
		if [ -n "$$SVC_FILE" ]; then \
			echo "   OK: plist found: $$(basename $$SVC_FILE)"; \
		else \
			echo "   FAIL: no launchd plist found"; \
			cd "$$TMPDIR" && $(CURDIR)/target/debug/cryo cancel 2>/dev/null; \
			rm -rf "$$TMPDIR"; exit 1; \
		fi; \
	else \
		SVC_FILE=$$(ls -t ~/.config/systemd/user/com.cryo.daemon.*.service 2>/dev/null | head -1); \
		if [ -n "$$SVC_FILE" ]; then \
			echo "   OK: unit found: $$(basename $$SVC_FILE)"; \
		else \
			echo "   FAIL: no systemd unit found"; \
			cd "$$TMPDIR" && $(CURDIR)/target/debug/cryo cancel 2>/dev/null; \
			rm -rf "$$TMPDIR"; exit 1; \
		fi; \
	fi; \
	PID=$$(cd "$$TMPDIR" && cat timer.json 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('pid',''))" 2>/dev/null); \
	if [ -n "$$PID" ] && kill -0 "$$PID" 2>/dev/null; then \
		echo "   OK: daemon process alive (PID $$PID)"; \
	else \
		echo "   WARN: daemon PID not found in timer.json (may still be starting)"; \
	fi; \
	echo ""; \
	echo "4. Cancelling (cryo cancel)..."; \
	cd "$$TMPDIR" && $(CURDIR)/target/debug/cryo cancel 2>&1; \
	RC=$$?; \
	if [ $$RC -ne 0 ]; then \
		echo "   FAIL: cryo cancel failed (exit $$RC)"; \
		rm -rf "$$TMPDIR"; exit 1; \
	fi; \
	echo "   OK: cancelled"; \
	echo ""; \
	echo "5. Verifying service removed..."; \
	if [ -e "$$SVC_FILE" ]; then \
		echo "   FAIL: service file still exists: $$SVC_FILE"; \
		rm -rf "$$TMPDIR"; exit 1; \
	else \
		echo "   OK: service file removed ($$SVC_FILE)"; \
	fi; \
	rm -rf "$$TMPDIR"; \
	echo ""; \
	echo "=== Service lifecycle check passed ==="; \
	echo ""; \
	echo "To test reboot persistence, run manually:"; \
	echo "  cd /tmp/cryo-reboot-test && cryo init && cryo start --agent '/bin/sh -c sleep 999'"; \
	echo "  # Reboot your machine"; \
	echo "  # After reboot, verify:"; \
	echo "  #   macOS:  launchctl list | grep com.cryo"; \
	echo "  #   Linux:  systemctl --user status com.cryo.daemon.*"

# Run mock agent scenario tests (no external agent required)
check-mock:
	cargo test --test mock_agent_tests -- --nocapture --test-threads=1

# Build mdbook documentation
book:
	@command -v mdbook >/dev/null 2>&1 || { echo "Installing mdbook..."; cargo install mdbook; }
	mdbook build

# Serve mdbook locally with live reload
book-serve:
	@command -v mdbook >/dev/null 2>&1 || { echo "Installing mdbook..."; cargo install mdbook; }
	mdbook serve --open

# Deploy mdbook to GitHub Pages (gh-pages branch)
book-deploy: book
	@echo "=== Deploying to gh-pages ==="
	@TMPDIR=$$(mktemp -d); \
	cp -r book/* "$$TMPDIR/"; \
	cd "$$TMPDIR" && \
	git init && \
	git checkout -b gh-pages && \
	git add -A && \
	git commit -m "Deploy mdbook" && \
	git remote add origin "$$(cd "$(CURDIR)" && git remote get-url origin)" && \
	git push --force origin gh-pages; \
	rm -rf "$$TMPDIR"; \
	echo "=== Deployed to gh-pages ==="

# Tag and push a release (triggers CI publish to crates.io)
# Usage: make release V=x.y.z
release:
ifndef V
	$(error Usage: make release V=x.y.z)
endif
	@echo "Releasing v$(V)..."
	sed -i 's/^version = ".*"/version = "$(V)"/' Cargo.toml
	cargo check
	git add Cargo.toml
	git commit -m "release: v$(V)"
	git tag -a "v$(V)" -m "Release v$(V)"
	git push origin main --tags
	@echo "v$(V) pushed — CI will publish to crates.io"

# Request Copilot code review on the current PR
# Requires: gh extension install ChrisCarini/gh-copilot-review
copilot-review:
	@PR=$$(gh pr view --json number --jq .number 2>/dev/null) || { echo "No PR found for current branch"; exit 1; }; \
	echo "Requesting Copilot review on PR #$$PR..."; \
	gh copilot-review $$PR