gardener is a safety-gated Python CLI that dispatches Claude Code against
a fleet of software repos, in two distinct ways:
alignchecks one target repo at a time against a conventions repo you supply — your own engineering conventions and alignment checklists, in a git repo gardener only ever reads — and, if authorized, fixes what's missing.tend/garden/overnightmake real, broader progress on a repo (or a whole opt-in list of them, unattended overnight) by dispatching that repo's own<slug>-dev-loopClaude Code skill — triage, implement, test, PR — never merging without an explicit, separate per-repo opt-in. Those skills are generated bycreate-dev-loop, gardener's open-source companion project;tendbootstraps one automatically if the target repo doesn't have one yet.
gardener's own job is orchestration and safety-gating, in plain Python. The
actual reading/analysis/implementation judgment is delegated to a
dispatched, safety-gated claude CLI invocation in every mode — gardener
never itself decides what "aligned" means or what a fix should look like;
it only decides how much a dispatched Claude run is allowed to do about
it. See Usage below for the full command set.
gardener align --repo <owner/repo>: clones the target repo read-only, clones (or refreshes) a local cache of your conventions repo (the source of truth for what "aligned" means — gardener is only the tool that consumes it), builds a prompt combining that repo'sALIGNMENT_PROMPT.mdwith the target repo's identity and the requested mode's constraints, and dispatches one headlessclaude -prun to produce a gap checklist — or, if explicitly authorized, to act on it.gardener tend --repo <owner/repo>: dispatches the target repo's own<slug>-dev-loopskill instead — real triage/implement/test/PR work, not a conventions gap-check. Seegardener tendin the usage docs.gardener garden+gardener overnight: an opt-in list of repos and the unattended batch dispatcher that tends them one after another overnight. See docs/OVERNIGHT.md.gardener dashboard: a local, read-only web UI overgardener status's own run history plus everytend/overnightlog still being written to, so an unattended overnight run doesn't require polling the CLI by hand to see what it's doing. See docs/DASHBOARD.md.gardener doctor: a read-only pre-flight check over gardener's own local state — the CLIs it shells out to, the state directory, every cache clone's refresh-readiness, and whether each garden/allow-list entry still resolves to itself on GitHub. Every check comes from a failure that really did consume a garden slot on a real overnight run (a clone left dirty by a killed dev-loop run failstendevery night afterward; a renamed target repo fails in a way that looks fixed for exactly one run after you clear the cache). It reports findings and the exact command to fix each, and repairs nothing itself. Exits1on any error, so it works as a gate before the nightly run. Seegardener doctor.gardener update: fast-forwards gardener's own checkout toorigin—gardener overnightdoes this automatically before each run (opt out with--no-self-update), so a box running it unattended stays current without anyone needing to notice new commits andgit pullby hand. See Self-update.
Requires Python 3.10+, and the git, gh, and claude CLIs already
installed and authenticated (gh auth status, and a working claude
login) — gardener shells out to all three rather than reimplementing git
hosting, auth, or the agent loop itself.
git clone https://github.com/dmccoystephenson/gardener.git
cd gardener
pip install -e .This installs the gardener console script (via pyproject.toml's
[project.scripts] entry point) and leaves the source editable — which is
also what makes gardener update/overnight's self-update work at all
(see Self-update); a non-editable install has
no .git checkout to fast-forward, so it degrades to a no-op.
Only align needs this. tend, garden, and overnight dispatch each
repo's own dev-loop skill and work without one.
align audits a target repo against your engineering conventions, so you
have to tell it where those live. gardener deliberately ships no default
— a built-in one would mean silently auditing your repos against somebody
else's opinions — so align fails fast with setup instructions until one is
configured:
# Either, for every run:
export GARDENER_CONVENTIONS_URL=https://github.com/you/your-conventions.git
# Or, per invocation:
gardener align --repo <owner/repo> --conventions-repo <git-url>Any git repo works as long as it contains these files. gardener checks only that they exist — the contents are entirely yours:
| File | What it's for |
|---|---|
README.md |
Orientation for a reader of the conventions repo itself |
ALIGNMENT_PROMPT.md |
What a run should audit for — the substance of your conventions |
ALIGNMENT_CHECKLIST.md |
The checklist shape a run reports its findings back in |
docs/CLAUDE_MD_STRUCTURE.md |
What a CLAUDE.md should contain |
docs/README_STRUCTURE.md |
What a README.md should contain |
docs/CONTRIBUTING_STANDARDS.md |
What a CONTRIBUTING.md should contain |
docs/ISSUE_TEMPLATES.md |
Expected .github/ISSUE_TEMPLATE/ shape |
docs/CODEOWNERS.md |
Expected CODEOWNERS conventions |
docs/CI_STRUCTURE.md |
Expected CI workflow shape |
docs/COMMIT_PR_CONVENTIONS.md |
Branch naming, commit style, PR conventions |
docs/REVIEW_PROMPTING.md |
How code review should be requested and conducted |
docs/DEV_LOOP_PATTERNS.md |
Dev-loop conventions for repos that have one |
The dispatched run reads every one of them before forming a judgment, which is why all twelve are required: a missing file means auditing against an incomplete rubric, so gardener refuses rather than reporting a confidently-wrong result. Stub files are fine while you're building the set out.
The checkout is cached at ~/.cache/gardener/conventions and refreshed
each run — --no-refresh-conventions
reuses it as-is. Pointing gardener at a different conventions repo
re-points and refreshes that cache even under --no-refresh-conventions,
since reusing the previous repo's checkout would produce a wrong answer
rather than a stale one. gardener never commits or pushes into this cache.
$GARDENER_CACHE_DIR overrides the cache root — gardener's whole cache,
not just the conventions checkout. Setting it to /somewhere/else moves
both conventions/ above and the target-repo clones at
~/.cache/gardener/repos/<owner>__<repo> (see
docs/USAGE.md)
to /somewhere/else/conventions and /somewhere/else/repos. Worth setting
if ~/.cache isn't where this machine's free space is: a garden of any
size accumulates a full checkout per repo, dependency caches included (see
cli.py's PRESERVED_DEPENDENCY_DIRS).
By default gardener alerts nowhere except its own local run history — you
have to run gardener status or watch terminal output to see how a run
went. To get a Discord notification on every run's outcome instead,
configure a webhook one of two ways (checked in this order):
# 1. Environment variable (simplest — set it wherever gardener runs)
export GARDENER_DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/XXX/YYY"
# 2. A gitignored config file, for a persistent/cron context where
# exporting an env var per-invocation isn't practical — a plain
# KEY=VALUE dotenv file, mode 600):
mkdir -p ~/.local/state/gardener # or $GARDENER_STATE_DIR if overridden
umask 077
echo 'DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/XXX/YYY' \
> ~/.local/state/gardener/notify.envNo webhook configured (neither of the above) means notifications are a
clean no-op — gardener align still works exactly the same, nothing
prints or fails because of it. See docs/ALERTING.md
for how this is implemented.
Every alert carries the name of the device that sent it in the embed's
footer, so two machines tending overlapping gardens into the same channel
stay distinguishable. It defaults to the host's socket.gethostname(),
which is often not a name anyone recognizes (a UserLand session's
hostname, for instance) — override it the same two ways:
export GARDENER_DEVICE_NAME="pixel-userland"
# or, in the same notify.env file as above:
echo 'GARDENER_DEVICE_NAME=pixel-userland' >> ~/.local/state/gardener/notify.envgardener align --repo <owner/repo> [--implement] [--file-issue] [--conventions-repo <git-url>]
gardener tend --repo <owner/repo> [--allow-merge]
gardener allowlist list | add --repo <owner/repo> | remove --repo <owner/repo>
gardener garden list | add --repo <owner/repo> | remove --repo <owner/repo>
gardener overnight [--hours N] [--concurrency N] [--strategy round-robin|issue-count|random] [--no-self-update]
gardener ps [-a] [-q]
gardener stop <session>... | --all [-t SECONDS]
gardener kill <session>... | --all [-s SIGNAL]
gardener doctor [-v] [--offline]
gardener status [--repo <owner/repo>]
gardener tail-transcript <path> [-f]
gardener dashboard [--port N]
gardener update [--check]
See docs/USAGE.md for the full command reference:
every flag (--implement, --file-issue, --model, --timeout,
--conventions-repo, --no-refresh-*), how tend bootstraps and
dispatches a target repo's own dev-loop skill, orphaned-work recovery,
concurrent-dispatch safety, the merge allow-list, live session/transcript
visibility, run logs, and the docker-shaped ps/stop/kill commands for
listing a running session and stopping it together with everything it
dispatched.
For the unattended "tend to my garden while I sleep" flow (the garden
opt-in list, overnight's batching/budget/resume-cursor design, and the
per-device wiring recipes it's actually been deployed with), see
docs/OVERNIGHT.md.
For the dashboard's garden view (the table/plant-plot of every repo's health), the three progress bars an overnight run drives (garden cycle, time budget, current batch), and how the page reports that it has stopped being live, see docs/DASHBOARD.md.
Please file a bug report here.
See CONTRIBUTING.md for how to propose a change, branch naming, and running the test suite. Community participation is governed by the Code of Conduct.
PYTHONPATH=. python3 -m unittest discover -s tests -v
A passing run ends with OK. None of the automated tests hit the network,
invoke a real claude/git/gh process, or mutate a real repo. See
docs/TESTING.md for exactly what each test module
covers and for the manual/end-to-end verification steps required before
trusting a change to the dispatch layer.
gardener never invokes claude with bypassPermissions or any equivalent
auto-approve-everything mode, for any mode, under any flag combination —
enforced in dispatch.py, which raises rather than silently proceeding if
it's ever reached. See docs/SAFETY.md for the full
three-layer tool-scoping model, how headless tend dispatch handles the
"ask the user before merging" problem with nobody there to ask, and the
merge allow-list mechanics. See SECURITY.md to report a
vulnerability or to review the trust model before pointing gardener at a
repo you don't fully trust.
See docs/ALERTING.md for the Notifier
abstraction, DiscordNotifier/NullNotifier/CompositeNotifier, and how
_notify_run maps a run's outcome to a severity.
No build step — this is a stdlib-only Python CLI (see
Architecture below). Clone it, pip install -e ., edit,
re-run the tests.
Working end to end, real-verified against live repos in every dispatch
mode (align report-only, tend with and without --allow-merge,
overnight including its resume cursor and --concurrency, live
transcript visibility, and the create-dev-loop bootstrap path). See
docs/PROJECT_STATUS.md for the full history
of what was run, when, and what was confirmed afterward.
Stdlib-only Python — no third-party pip dependencies; gardener shells out
to git, gh, and claude rather than reimplementing git hosting,
GitHub API auth, or an agent loop. See docs/ARCHITECTURE.md
for the full module tree and gardener's relationship to the conventions
repo it consumes.
create-dev-loop— the open-source Claude Code skill that generates the per-repo<slug>-dev-loopskillsgardener tenddispatches. gardener orchestrates and safety-gates when those skills run across a fleet; create-dev-loop decides what each one knows about its repo. Either is usable without the other: create-dev-loop's skills run standalone as a/<slug>-dev-loopslash command, and gardener will bootstrap one on demand for any repo in its garden that lacks one.