Skip to content

Dry-run mode

Dry-run is a diagnostic path. By default it prints a safe simulation for any configured codename, without touching the scheduler, GitHub, Slack, AWS, Playwright, an LLM, or a real worktree. Runners that declare native dry-run support can also execute their lifecycle hooks with outside-world calls stubbed.

A developer with nothing configured (no gh auth, no AWS, no Slack, no Claude) can run a dry-run and see the sequence Alfred would follow. The output is a narrated, step-numbered trace.

Condensed companion to docs/DRY_RUN.md.

ALFRED_DOCTOR=1 short-circuits a runner to a preflight-only check: it verifies host configuration and exits before the lifecycle starts.

Dry-run is the opposite: it shows the firing path and, for native dry-run runners, executes that path with outside calls stubbed. Use doctor mode to answer “is this host configured correctly?”; use dry-run to answer “what does a firing do, step by step?”.

From a fresh checkout (no install needed), put lib/ on PYTHONPATH:

Terminal window
git clone https://github.com/luminik-io/alfred-os.git ~/code/alfred-os
cd ~/code/alfred-os
PYTHONPATH=lib python3 examples/bin/echo_summarise.py --dry-run

You get a step-numbered trace of the full lifecycle and an exit code of 0. The same works for examples/bin/hello.py (the minimal agent) and bin/lucius.py (the feature-dev agent).

After install, use the Alfred CLI:

Terminal window
alfred dry-run lucius
alfred dry-run drake
alfred dry-run all
alfred dry-run lucius --native

Every configured codename resolves through this command. By default Alfred prints a safe simulation that never touches the scheduler, GitHub, Slack, AWS, Playwright, an LLM, or a real worktree. Pass --native when you want a runner that declares native dry-run support to execute with every side-effecting seam stubbed.

Two equivalent switches:

  • The ALFRED_DRY_RUN environment variable, set to any truthy value (1, true, yes, on).
  • The --dry-run CLI flag, accepted by the example runners and bin/lucius.py.

A runner that sees --dry-run calls agent_runner.set_dry_run(), which writes ALFRED_DRY_RUN=1 back into the process environment so every downstream code path, and any subprocess-spawned child, agrees on the mode.

Everything inside Alfred runs for real: the lock, preflight narration, event log, prompt construction, and the runner’s own result-branching logic. Calls to the outside world stay stubbed.

Every side-effecting boundary is stubbed behind a single is_dry_run() helper in the agent_runner package:

BoundaryDry-run behaviour
claude_invoke, codex_invoke, invoke_agent_engineReturn a clearly-marked synthetic result (cost_usd=0.0, result_text labelled [dry-run] synthetic ...). No LLM is ever invoked.
SpendStateWrite a separate spend-dryrun-<date>.json ledger. The real per-day counters are never touched, so a dry-run can’t trip a daily cap.
set_global_blockLog the provider-limit block it would set; the real scheduler block file is never written.
slack_postLog the line it would post (severity included) and return success. The webhook is never hit.
claim_issue, release_issue, gh_pr_create, gh_issue_edit, and the other gh helpersLog the gh call that would run and return success. No gh subprocess is spawned.
make_worktree / remove_worktreeCreate a self-contained throwaway git repo in a temp dir, coherent enough for a runner to inspect, then remove it. Nothing is fetched from or pushed to a real remote.

With nothing configured, the runners also substitute clearly-labelled fake data: a synthetic issue from pick_issue, a dry-run-org/<repo> placeholder when GH_ORG is unset, and a dry-run-repo slug when the repo env vars are missing. Preflight still runs and still reports what is missing. In dry-run, the runner narrates the gap and continues instead of exiting.

See docs/DRY_RUN.md for the full seam table and for how to add dry-run support to your own runner.