Skip to content

Repository files navigation

greentree

Test the tree, not every commit.

greentree content-addresses your dirty working tree, caches check verdicts by tree hash,
and refuses to create a commit from any tree that has not passed.

Built for coding agents: one verb, JSON on every command, stable exit codes, and a drop-in skill.
An open source primitive from reachpad · Apache-2.0 · Linux and macOS


$ greentree test
tree 8be03d1  test ✗ (11.2s)          # attempt 1: red

$ greentree test                       # attempt 2, after edits
tree 4f2a91c  test ✓ (10.8s)

$ greentree test                       # attempt 3 reverted attempt 2's edit
tree 4f2a91c  test ✓ (cached)          # same content, same tree, no re-run

$ greentree gate -m "implement add()"
  test ✓ (cached)
published commit a1c4e77 from verified tree 4f2a91c on main

The published commit is built with git commit-tree from the exact tree object the checks passed against. Not a re-checkout, not a re-run. What was tested is what ships, byte for byte.

The problem

A coding agent makes 30 attempts at a change inside a workspace. The verification loop for those attempts lives on the wrong side of the push:

  1. The agent thinks it is done and commits.
  2. It pushes. CI queues, boots a cold runner, reinstalls dependencies, and runs the full suite against a commit SHA.
  3. Fifteen minutes later a red X arrives on a commit the agent has already moved past.
  4. A reviewer comments. The agent starts a new environment, reproduces the state, edits, and the cycle repeats from step 1.

Every step re-derives state the workspace already had: the checkout, the warm caches, the running services, the test results for content that did not change. CI verifies commits; agents produce dozens of candidate trees per commit that lands.

greentree moves verification inside the workspace and makes the tree, not the commit, the unit of proof:

  • A test result belongs to a tree hash. Revert an experiment and the previous verdict is valid again, instantly. Rerun nothing that already passed on identical content.
  • Publishing is a gate, not a reminder. greentree publish exits 11 unless the current tree has a passing, fresh verdict for every required check. There is no "pushed before the tests finished".
  • One idempotent verb for agents. greentree gate runs whatever is not cached, then publishes if green. Safe to call in a loop.

How it works

flowchart LR
    A[agent edits] --> B["snapshot: shadow index + git write-tree<br>tree = 4f2a91c"]
    B --> C{verdict for<br>tree + check + env?}
    C -- hit --> E[verdict]
    C -- miss --> D["run check<br>/bin/sh -c, own process group"] --> E
    E -- pass --> F["gate: git commit-tree 4f2a91c<br>CAS ref update"]
    F --> G["push --force-with-lease<br>+ commit status"]
    E -- fail --> A
Loading
  1. Snapshot. A persistent shadow index (never your real index, so there is no index.lock contention with your own git usage) is refreshed with git add -A and hashed with git write-tree. Cost is O(changed files) after the first run: 4 git subprocesses on a warm cache hit. The snapshot captures tracked files plus untracked files not ignored by .gitignore, minus configured excludes.
  2. Verdict cache. Results are keyed by (tree, check, command hash, environment fingerprint). Only pass and fail enter the cache. Timeouts, infrastructure errors, and runs during which the tree changed are never cached: a verdict that binds to no exact tree is worthless.
  3. Publish. git commit-tree <verified-tree>, a compare-and-swap ref update, and (with --push) a push whose --force-with-lease expectation is recorded explicitly. Every step is journaled: a publish killed at any point resumes exactly where it stopped, and the verification gate runs again on resume. Every commit carries a Greentree-Change-Id trailer, the stable identity that will let stacks of verified changes survive rebases (see the roadmap).
  4. Statuses. With a GitHub token available (an env var, or gh auth token if you're signed in with the gh CLI), a pushed publish posts a greentree/<check> commit status on the new SHA. Statuses satisfy branch protection required checks.

Install

cargo install --git https://github.com/reachpad/greentree

--no-default-features builds without the GitHub status client and its TLS stack. Prebuilt binaries are on the roadmap.

Quickstart

cd your-repo
greentree init      # detects pnpm/npm/yarn/cargo/go/uv, writes greentree.yaml
greentree test      # snapshot + run checks; instant when the tree is known
greentree gate      # verify (cache-aware), then commit the verified tree
greentree watch     # run watch-marked checks whenever the tree settles

No config file? greentree test auto-detects the project type and runs its conventional test command.

Capabilities

Commands

command what it does
init Detects the project, writes greentree.yaml, warms the snapshot index
test [check] Snapshots the tree and runs checks; cache hit = no process runs
status Reports the current tree, its verdicts, and whether publish would succeed
publish Creates a commit from the current tree; exit 11 unless verified
gate test for required checks, then publish if green; idempotent
watch Reruns watch-marked checks on every settle; kills runs on edit
attest Posts greentree/<check> statuses for HEAD if its tree is verified
gc Prunes snapshot anchors and trims logs

Every command takes --json (exactly one JSON object on stdout) and -C <dir>. Exit codes are a stable contract:

exit meaning
0 success: checks green, published, or no-op
10 a check ran and failed
11 publish refused: tree not verified
12 repository state blocks it: conflicted index, dirty submodule, or publishing mid-rebase
13 another greentree process holds the lock
14 configuration error
15 publish machinery failed: CAS refused, push rejected, no remote
16 free disk below min_free_disk: check refused, or aborted mid-run

Full JSON shapes and the verdict record schema are in docs/SPEC.md.

Configuration

version: 1

min_free_disk: 10G     # floor for every check; default 5G, "0" disables

checks:
  quick:
    run: pnpm lint && pnpm test
    watch: true          # run from `greentree watch` on each settle
  full:
    run: pnpm test && pnpm build
    required_for_publish: true
    fresh: 30m           # a pass older than this will not satisfy the gate
    timeout: 20m         # default 15m; SIGTERM, 5s grace, SIGKILL
    min_free_disk: 60G   # this check's own floor: refuse to start below it,
                         # and kill it if free space falls under it mid-run

snapshot:
  exclude: ["docs/generated/**"]   # paths checks may write without
                                   # invalidating their own verdict

inputs: [".env", "pnpm-lock.yaml"] # gitignored files that affect results:
                                   # hashed per-file into the verdict key,
                                   # so a cache miss names the file that
                                   # caused it

The watch loop

greentree watch runs watch: true checks whenever the tree settles. Two policies make it agent-proof:

  • Kill on edit. A file mutation during a run kills the check's whole process group immediately. The result could never be cached (it binds to no tree), and the CPU belongs to the agent's next attempt.
  • Adaptive settle window. 300ms, doubling after each cancelled cycle, capped at 5s. A continuously editing agent cannot starve verification, and a cancelled cycle reruns even if no further edit arrives.

The lock is held only during a cycle, so test and gate interleave freely with a running watcher.

Isolation and safety

  • Checks run under /bin/sh -c in their own process group with GIT_* redirection variables scrubbed: a git command inside your test suite sees your real repo, never greentree's shadow index.
  • Checks are bounded in time and in disk: below min_free_disk free space a check is refused before it starts (exit 16), and one that eats through the floor mid-run is killed like a timeout, so a build cannot fill the machine it runs on.
  • greentree never writes your index, your worktree, or your refs outside refs/greentree/*, except at publish, where the ref move is a compare-and-swap and the push records its lease explicitly.
  • Every tree a check ran against is anchored at refs/greentree/snapshots/<sha>, so the exact tested state can be diffed or restored later. gc bounds the accumulation.
  • Logs stream to .git/greentree/logs/ with a size cap and a digest computed over the full stream.

For coding agents

An agent is the primary user. The whole interface an agent needs is one verb and one exit-code table:

greentree gate --json -m "<message>"    # verify, then commit the tree; idempotent

Every command takes --json (exactly one object on stdout) and returns a stable exit code, so an agent branches on the code, never on parsed text: 0 committed, 10 a check failed (output at .log_tail), 11 not verified yet (run test first), 12 unsnapshotable, 13 locked. No prose to interpret, no re-running "to be sure" — a cache hit proves the content already passed.

Drop-in skill. skills/greentree/ is a Claude Code skill that teaches an agent the workflow and the exit codes. Copy it into a project so the agent loads it automatically:

cp -r skills/greentree .claude/skills/

The skill tells the agent to land changes with greentree gate instead of raw git commit/git push, how to react to each exit code, and not to re-run cached verdicts.

Enforce it. docs/AGENTS.md has a project-instructions snippet and a Claude Code hook that blocks git commit/git push so the gate is the only door, even if the agent forgets.

The gate before GitHub

greentree verifies where you work, before the push. GitHub records the result and enforces it, running no builds of its own:

  1. Verify while you work (test/watch/gate), in the workspace that already has the checkout and warm caches.
  2. Land the commit, either with greentree gate --push (builds the commit from the verified tree and pushes) or with plain git commit && git push followed by greentree attest.
  3. Both paths post a greentree/<check> commit status on the pushed SHA.
  4. A GitHub branch-protection rule that requires that status turns it into a merge gate. GitHub runs nothing; it checks that the box is green.

A commit that never went through a workspace has no status and cannot merge. To get green, route it through verification. That is the whole enforcement, with zero CI minutes.

greentree posts statuses with a GitHub token resolved from GREENTREE_GITHUB_TOKEN, then GITHUB_TOKEN, then (if neither is set and gh is on PATH) gh auth token. Checks never see it: the token is scrubbed from every check subprocess and reaches only the status API call. Use a fine-grained token scoped to the repo with "Commit statuses: write".

Verification runs wherever you invoke greentree. Pointing it at a persistent warm workspace instead of the local machine, so the cache and environment are shared and authoritative, is on the roadmap; reachpad is that workspace.

Comparisons

unit of proof runs revert costs rebase (same content) costs
greentree tree (content) before the push, in your warm workspace nothing: cache hit nothing: same trees
CI (Actions etc.) commit SHA after the push, cold runner full re-run full re-run
git-test commit's tree on existing commits cache hit cache hit
pre-commit / husky none (hooks) at every commit full re-run full re-run
Jujutsu none never (no test runner) n/a n/a
Graphite + CI commit SHA per branch after every restack force-push full re-run full stack re-run
Turborepo / Nx / Bazel task inputs wherever invoked task-level hit task-level hit

CI. Runs after the push, once per commit SHA, on a runner that starts from nothing. greentree runs before the push, in the workspace that already has the checkout, the dependency caches, and the services, and never re-runs a check whose exact content already passed. CI stays the authoritative re-check; greentree makes it arrive green.

git-test (mhagger/git-test) is the closest prior art and also caches results by tree. It tests ranges of existing commits; greentree tests the dirty tree before any commit exists, adds the environment fingerprint to the key, and gates publishing on the result. Same insight, opposite end of the commit's lifecycle.

pre-commit / husky. Hooks re-run on every commit regardless of content, slow the honest path, and are one --no-verify away from not existing. greentree memoizes by content and creates the commit itself, so there is no hook to skip. It also bypasses commit hooks by construction; its checks are the hook.

Jujutsu snapshots the working copy as a commit automatically, which is the same primitive greentree builds on, and its stable change IDs inspired the Greentree-Change-Id trailer. jj has no verdict cache and no publish gate; it manages history, not verification. greentree stays plain git, so every agent's existing tooling works unchanged. Running jj colocated on the same repo does not break greentree.

Graphite and stacked-PR tools re-push a stack of branches on every restack; CI, keyed by SHA, re-runs everything, and Graphite sells heuristics to suppress those runs. Tree-keyed verdicts make the same optimization exact rather than heuristic: a message edit or reorder keeps every tree and re-verifies nothing, and levels below an edit never re-run. Stack support is planned on the trailer already present in every published commit.

Build caches (Turborepo, Nx, Bazel). They memoize task results keyed by declared task inputs, inside the build graph. greentree memoizes whole-tree verdicts and controls what becomes a commit. They compose: run: turbo test gives task-level incrementality inside a run and tree-level memoization across runs.

Merge queues synthesize their own commits after approval, which no pre-push verifier can see. Out of scope; documented in the spec.

Limitations

Stated here because a verification tool that oversells is worse than none:

  • The verdict cache is machine-local and advisory. It attests that this tree passed on this machine, not a tamper-proof supply chain proof.
  • run: commands come from repo config and execute on the machine that runs greentree, with your privileges: the same trust model as npm test or make. greentree verifies code you already trust (your own work, your agents), before it is pushed. It is not a sandbox for untrusted code, and it does not pull in and run other people's commits. Verifying untrusted contributions (fork pull requests) is a job for an isolated, ephemeral runner, not greentree. See SECURITY.md.
  • Undeclared environment (toolchain versions, system libraries) is not in the verdict key. Declare what matters in inputs:.
  • An edit-and-revert during a single check run (ABA) is undetectable until the worktree executor lands (v0.4); watch narrows the window by killing on the first edit.
  • Dirty submodules are refused (exit 12): their state is invisible to the superproject tree hash. Symlink targets outside the repo are not captured.
  • Windows is unsupported for now.

Roadmap

v0.4 materializes any tree into an ephemeral worktree for verification, v0.5 builds stacks on the change-id trailer, v0.6 projects stacks onto GitHub PRs, v0.7 adds the GitHub App with real check runs and review-comment routing back to the workspace. Details and explicit non-goals: docs/ROADMAP.md.

License

Apache-2.0. Built by reachpad, infrastructure for coding agents.

Releases

Packages

Contributors

Languages