A complete, runnable walkthrough of ezgitx on a realistic multi-repo workspace. Every command and block of output below is real. Run them and you'll get the same shape of result.
Captured 2026-06-13 against ezgitx 0.2.0; output updated for 0.4.0, which adds
the build column to status, plus the verify and sessions commands. The
upstream repos drift over time; if a step looks different from what you see, the
exact commits this was run against are listed under Notes.
This walkthrough uses Claude Code as the coding agent. The ezgitx
commands are identical everywhere. The pasted setup prompt in step 3 works
with any agent, but ezgitx init-skill (step 8) writes a Claude Code skill
file at .claude/skills/ezgitx/SKILL.md; other coding agents use different
instruction-file conventions, so swap that step for yours.
Picture the backend platform at your company. It isn't one repo, it's five, each on its own release cadence:
- two foundational utility libraries with no internal dependencies,
- two higher-level libraries built on top of those,
- and the service you actually ship, built on all of them.
You can't hand a stranger your company's private repos, so this walkthrough
stands them in with five real, public packages that happen to have exactly
that dependency shape: the aio-libs stack behind aiohttp. Treat the names
as if they were your own internal repos.
| Role in the "platform" | Real repo | Builds on (in this workspace) |
|---|---|---|
| foundational library | multidict |
none |
| foundational library | frozenlist |
none |
| higher-level library | yarl |
multidict |
| higher-level library | aiosignal |
frozenlist |
| the shipped service | aiohttp |
multidict, yarl, frozenlist, aiosignal |
graph TD
multidict --> yarl --> aiohttp
multidict --> aiohttp
frozenlist --> aiosignal --> aiohttp
frozenlist --> aiohttp
The point isn't aiohttp. It's that this is the shape of workspace ezgitx is built for: separate repos that build on each other locally. Unlike your private platform, you can run this one yourself.
mkdir acme && cd acme
git clone https://github.com/aio-libs/multidict.git
git clone https://github.com/aio-libs/frozenlist.git
git clone https://github.com/aio-libs/yarl.git
git clone https://github.com/aio-libs/aiosignal.git
git clone --recurse-submodules https://github.com/aio-libs/aiohttp.gitaiohttp vendors llhttp as a git submodule, so it needs
--recurse-submodules or its build fails later. That's an aiohttp quirk, not
an ezgitx one; your own repos may have none.
The repos install into a single environment so the higher-level ones build against the local copies of the lower-level ones, not published releases:
python3 -m venv .venv
source .venv/bin/activate
pip install -U pip setuptools wheelKeep this environment active in the shell you run ezgitx from. ezgitx's child build commands inherit it.
Don't hand-write the YAML. Open Claude Code at the workspace root and tell it to set ezgitx up. Since you know your own workspace, just describe the local relationships:
I'm adopting ezgitx (https://github.com/yuval-r/ezgitx) in this workspace. These five repos are a co-developed local stack, installed editable into the shared
.venv:yarlbuilds onmultidict,aiosignalbuilds onfrozenlist, andaiohttpbuilds on all four. Generate.ezgitx.ymlat the root with oneplatformgroup: each repo gets apip install -e .build command, anddepends_onedges for exactly those local relationships.aiohttpneedsAIOHTTP_NO_EXTENSIONS=1prefixed on its command. Then runezgitx statusto validate it.
The README has a fuller, evidence-first version of this prompt under Or let your agent write the config, which reads each repo's manifests and derives the commands itself.
The agent reads the repos and writes this:
version: 1
groups:
platform:
- path: ./multidict
default_cmd: "pip install -e ."
check_cmd: "python -c 'import multidict'"
- path: ./frozenlist
default_cmd: "pip install -e ."
check_cmd: "python -c 'import frozenlist'"
- path: ./yarl
default_cmd: "pip install -e ."
check_cmd: "python -c 'import yarl'"
depends_on: ["multidict"]
- path: ./aiosignal
default_cmd: "pip install -e ."
check_cmd: "python -c 'import aiosignal'"
depends_on: ["frozenlist"]
- path: ./aiohttp
default_cmd: "AIOHTTP_NO_EXTENSIONS=1 pip install -e ."
check_cmd: "python -c 'import aiohttp'"
depends_on: ["multidict", "yarl", "frozenlist", "aiosignal"]Two things to check in what it produced:
depends_onshould appear only where a repo consumes another from this workspace, which here means the local editable installs, never a plain PyPI dependency. That rule is what makes staleness mean something, so don't let the agent wire an edge for a package that would come from the registry.aiohttp's command carriesAIOHTTP_NO_EXTENSIONS=1, because building its C speedups from a git checkout needs a Cython step pip won't run for you. A repo's per-repodefault_cmdis where its build quirks belong.
ezgitx run --all --with-depsezgitx builds the two foundational libraries first, then the two that depend on them, then the service. One JSONL line per repo as it finishes, then a summary (stdout trimmed here):
{"repo":"multidict","exit_code":0,"duration_ms":1984,"stdout_tail":"...Successfully installed multidict-6.7.2.dev0"}
{"repo":"frozenlist","exit_code":0,"duration_ms":5009,"stdout_tail":"...Successfully installed frozenlist-1.8.1.dev0"}
{"repo":"aiosignal","exit_code":0,"duration_ms":933,"stdout_tail":"...Requirement already satisfied: frozenlist>=1.1.0 in …/.venv/… (1.8.1.dev0)..."}
{"repo":"yarl","exit_code":0,"duration_ms":4454,"stdout_tail":"...Requirement already satisfied: multidict>=4.0 in …/.venv/… (6.7.2.dev0)..."}
{"repo":"aiohttp","exit_code":0,"duration_ms":992,"stdout_tail":"...Requirement already satisfied: multidict<7.0,>=4.5 …(6.7.2.dev0); yarl<2.0,>=1.17.0 …(1.24.2)..."}
{"type":"summary","total":5,"passed":5,"failed":0,"duration_ms":10482}Look at yarl and aiohttp: Requirement already satisfied: multidict … (6.7.2.dev0). They resolved their dependency to the local checkout you just
built, not a PyPI download, because ezgitx installed multidict first.
Build yarl before multidict and pip pulls a published multidict instead.
The ordering is the whole point.
Genuinely external dependencies (idna, propcache, and friends) still come
from PyPI, as they should. ezgitx only orders what you declared.
ezgitx status --all --humanREPO BRANCH HEAD STATE AHEAD BEHIND STALE_DEPS BUILD
multidict master 8b7c4d8 clean 0 0 - fresh
frozenlist master 0334ec8 dirty 0 0 - fresh
aiosignal master 2a67bbd clean 0 0 fresh
yarl master 7b66654 dirty 0 0 fresh
aiohttp master 31702b2 clean 0 0 fresh
Nothing is stale: every repo sits at the commit ezgitx last built it at, so the
BUILD column reads fresh across the board. The STALE_DEPS column reads -
for a repo with no declared upstreams and is blank when its upstreams are all
fresh. (frozenlist and yarl show dirty
because building C extensions writes generated files into their trees. In your
own repos, .gitignore those build artifacts.)
You edit a foundational library and commit:
# ...edit multidict, then:
git -C multidict commit -am "tweak multidict internals"multidict has now moved past the commit it was last built at. ezgitx notices,
and flags everything downstream:
ezgitx status --all --humanREPO BRANCH HEAD STATE AHEAD BEHIND STALE_DEPS BUILD
multidict master 215c21e clean 1 0 - stale
frozenlist master 0334ec8 dirty 0 0 - fresh
aiosignal master 2a67bbd clean 0 0 fresh
yarl master 7b66654 dirty 0 0 multidict stale
aiohttp master 31702b2 clean 0 0 multidict stale
The BUILD column flips to stale for multidict (its own HEAD moved past the
recorded build) and for yarl/aiohttp (an upstream drifted); frozenlist and
aiosignal are untouched and stay fresh.
ezgitx check-impact --repo multidict --humanREPO DEPTH VIA
aiohttp 1 multidict
yarl 1 multidict
2 affected downstream of multidict
No doc to keep up to date, no remembering that the service sits on top of the collections library. ezgitx computed the blast radius from the declared graph plus the commit it recorded at build time.
ezgitx run --repo aiohttp --with-depsezgitx rebuilds the service and every upstream it sits on that has moved since
aiohttp was last built against it — here multidict (its own commit moved)
and yarl (it was built against the old multidict) — in dependency order,
and skips the rest:
{"repo":"multidict","exit_code":0,"duration_ms":1792,"stdout_tail":"...Successfully installed multidict-6.7.2.dev0"}
{"repo":"yarl","exit_code":0,"duration_ms":1233,"stdout_tail":"...Successfully installed yarl-1.22.0.dev0"}
{"repo":"aiohttp","exit_code":0,"duration_ms":1028,"stdout_tail":"...Successfully installed aiohttp-4.0.0a2.dev0"}
{"type":"summary","total":3,"passed":3,"failed":0,"duration_ms":4061}frozenlist and aiosignal are left alone — nothing they depend on moved.
yarl rebuilds even though its own source didn't change, because ezgitx
records the commit each repo was built against: yarl was built against the
old multidict, so it stays stale until rebuilt. That is the difference from a
model that tracks only each repo's own commit, and it is what keeps a compiled
or code-generated yarl from silently sitting on a stale multidict.
The flag is per-consumer. Had you rebuilt multidict for some other consumer,
status would still show yarl with stale_deps: multidict until you rebuilt
yarl itself — rebuilding a shared upstream for one consumer never clears
another consumer's flag.
To push a change the other way — rebuild everything that sits on top of a repo you just changed — use the forward counterpart:
ezgitx run --repo multidict --with-dependentsThat rebuilds multidict plus every stale dependent (yarl, aiohttp) in
dependency order.
Before you call a cross-repo change done, gate it. Say you've edited multidict
but not committed yet. ezgitx verify finds every dirty repo, adds everything
downstream of it, runs each one's check_cmd (fallback default_cmd) in
dependency order, and won't go green until they all pass:
ezgitx verify{"repo":"multidict","exit_code":0,"duration_ms":214,"stdout_tail":"","stderr_tail":"","truncated":false}
{"repo":"yarl","exit_code":0,"duration_ms":231,"stdout_tail":"","stderr_tail":"","truncated":false}
{"repo":"aiohttp","exit_code":0,"duration_ms":243,"stdout_tail":"","stderr_tail":"","truncated":false}
{"type":"verdict","verdict":"pass","checked":3,"failed":[]}multidict is dirty, so it and its downstream closure (yarl, aiohttp) are
checked; frozenlist and aiosignal are untouched and skipped. Had your edit
broken aiohttp's import check, its run line would carry a non-zero exit_code
and the final line would read
{"type":"verdict","verdict":"fail","checked":3,"failed":["aiohttp"]}, with the
command exiting 1. That single exit code is the gate: the agent isn't done until
verify is green.
In a workspace several agents share, ezgitx sessions lists who currently holds
a lock: one read-only line per live lock (repo, scope, pid, host, op,
since), so a second agent can see that a repo is mid-pull before piling on:
ezgitx sessions{"lock":"repo-aiohttp","scope":"repo","repo":"aiohttp","pid":48213,"host":"build-box","op":"pull","since":"2026-06-13T17:02:31Z"}Once you've seen ezgitx do its job, make every future session aware of it:
ezgitx init-skillThis writes .claude/skills/ezgitx/SKILL.md so a fresh Claude Code session in
this workspace reaches for ezgitx on its own, without you re-explaining the
layout.
- These are real public packages used as a stand-in for a private workspace. You wouldn't normally co-develop them, but the dependency shape is exactly what ezgitx targets, and a public stack is something you can actually run.
- ezgitx tracks staleness by commit: each repo records the commit it was built
against for every upstream, so a downstream stays flagged until it is itself
rebuilt. With the editable installs used here a downstream rebuild is often
redundant (the import is live); in a compiled or code-generated workspace it
is exactly what keeps artifacts honest.
--with-depswalks upstreams,--with-dependentswalks downstreams. statussurfaces that same freshness as abuild: fresh|stalecolumn for every repo (stale= no recorded build, own HEAD moved, or an upstream drifted).ezgitx verifyis a gate, not a build: it runscheck_cmd(fallbackdefault_cmd) and records no freshness, so running it never changes whatstatus/runconsider stale.- This walkthrough was captured against these upstream commits. If the repos
have moved since and a step diverges, check these out to reproduce it
exactly:
multidict8b7c4d8,frozenlist0334ec8,yarl7b66654,aiosignal2a67bbd,aiohttp31702b2.