Keep many long-lived pull requests clean, rebuildable, and publishable from one manifest.
Current version: 0.20.0
syncwheel is a small CLI and workflow model for maintainers who carry several
PR branches against an upstream repository and need those branches to stay
under control over time.
It is especially useful when you:
- keep many open PRs alive while upstream keeps moving
- maintain a fork with clean review branches and a combined local runtime branch
- need to rebuild PR branches deterministically instead of hand-rebasing them
- work across multiple devices or AI agents without one checkout becoming the hidden source of truth
- want branch recovery to follow a manifest, not memory
integration/* is recommended, not mandatory.
You can use syncwheel in two modes:
- PR-only mode: manage and validate PR stacks without an integration branch
- Integration mode: also maintain a combined branch to test multiple in-flight PRs together
Git can tell you what happened. It cannot reliably tell you which commits belong
to logical PR feature-a, which commits are temporary integration-only work,
or how to rebuild ten open PRs after upstream changed.
Syncwheel adds that missing control plane:
- one manifest declares commit ownership
- each stack maps to one PR branch
- integration is a disposable projection of the manifest
reconcilecompares local branches, remote tips, and manifest projections- humans, scripts, and AI agents can all run the same lifecycle
The practical result is that a maintainer can keep many PRs open without turning branch history into tribal knowledge.
python3 scripts/syncwheel.py repo tracking status
python3 scripts/syncwheel.py reconcile
python3 scripts/syncwheel.py resume
python3 scripts/syncwheel.py sync
python3 scripts/syncwheel.py publishDefault behavior is conservative:
reconcileis the dry-run diagnostic entrypointresumeis the dry-run recovery entrypoint for cross-device resume when integration contains unmapped commits that can be classified deterministicallyledger showexposes Syncwheel's append-only event ledger and the current replayed state used for cross-machine recoverysyncapplies the safe local lifecycle without pushingpublishapplies the lifecycle and pushes managed branches- lower-level
reconcile --applyandreconcile --apply --pushremain available for explicit scripting publishandreconcile --pushuse--force-with-leaseby default, because managed branches are often rewritten by deterministic rebuildsrepo tracking statusshows whether the manifest isgit-tracked,local-only, or missing a persisted policy- if a remote managed branch already matches the manifest projection,
sync,publish, andreconcile --applyalign the local branch to that remote instead of rebuilding new replacement commits - pass
--no-align-local-to-remotewhen you intentionally want to preserve a different local history for manual inspection
Use --no-force-with-lease only when a normal push is intentionally required.
syncwheel_tracking=git-tracked means .syncwheel/manifest.json should be
tracked by Git. syncwheel_tracking=local-only keeps Syncwheel metadata local
through .git/info/exclude. New managed worktrees default to repo-relative
.syncwheel/wt/.
Long options remain stable; short flags are available for daily workflows:
| Short | Long | Where |
|---|---|---|
-r |
--repo |
most repo commands |
-M |
--manifest |
most repo commands |
-p |
--personal |
most repo commands |
-j |
--json |
status, plan, check, ledger, reconcile |
-F |
--no-fetch |
check, reconcile, update/alignment flows |
-n |
--dry-run |
rebuild, push, self update/hooks, align-remote |
-a |
--apply |
reconcile and repo tracking set |
-P |
--push |
reconcile |
-R |
--remote |
reconcile, push, integration remote flows |
-W |
--worktree-root |
init, reconcile, stack absorb |
Examples:
syncwheel status -f -j
syncwheel repo tracking set git-tracked -a
syncwheel reconcile -a -P -W .syncwheel/wt
syncwheel stack rebuild feature-a -nSyncwheel is fundamentally built around Git worktrees. The safest default is:
- keep the primary working checkout on the shared integration branch
- use one worktree per PR branch when rebuilding or validating PR state
- optionally keep a separate administrative checkout for manifest-only work
This keeps branch mutation explicit and avoids losing your place in a normal
working checkout. For simpler human-operated workflows, stack rebuild and
int rebuild also support --in-place; in-place mode requires the current
checkout to already be on the target branch and to be clean before anything is
reset or replayed.
syncwheel has four pieces:
- base branch (
upstream/mainor similar) - PR stacks mapped to
pr/*branches - manifest (
.syncwheel/manifest.json) as source of truth - integration branch (
main-integrationby default) for combined testing - ledger (
.syncwheel/ledger/by default, or a sibling<manifest-name>-ledger/directory when the manifest lives outside the repo) as append-only operational history plus a replay checkpoint for cross-machine recovery
flowchart LR
U[upstream/main]
I[main-integration]
P1[pr/feature-a]
P2[pr/feature-b]
P3[pr/hotfix-c]
M[.syncwheel/manifest.json]
U --> P1
U --> P2
U --> P3
M --> P1
M --> P2
M --> P3
M -. optional .-> I
P1 -. sync .-> I
P2 -. sync .-> I
P3 -. sync .-> I
I -. sync .-> P1
I -. sync .-> P2
I -. sync .-> P3
Practical meaning:
- PR branches are rebuilt from declared commit ownership
- integration (if used) is rebuilt from declared stack order
- one manifest keeps both sides aligned
- branch rebuilds create a backup branch first when the target branch already exists
- A PR stack is one logical change stream mapped to one
pr/*branch with an explicit commit list. stack sync,stack set, andstack addupdate commit ownership without hand-editing SHA lists.stack absorbmoves dirty or staged integration-branch changes into a stack branch, updates the manifest, and removes the absorbed patch from the integration checkout.stack rebuildrebuilds one PR branch from the manifest.int rebuildrebuilds integration from ordered stacks.stack pushandint pushwrapgit push, with arbitrary Git arguments after--.reconcileis the preferred multi-device maintenance entrypoint: it compares manifest ownership, stack branches, integration, and remote tips; reports a dry-run plan by default; and can rebuild, update manifest SHAs, and push when explicitly run with--applyand--push.- In multi-device workflows,
reconcileconverges toward a remote branch that already matches the manifest projection instead of rebuilding the same logical state into new SHAs on every device. validateandplandetect drift before branch mutation.- validation also reports non-merge commits on integration that are not declared in any stack, so integration-only work cannot hide silently.
- update detection also works for normal branch checkouts and detached submodule-style installs.
integration.strategycontrols how integration is rebuilt:cherry-pickreplays every declared commit into one linear history.merge-stacksmerges each declared stack branch in manifest order with--no-ff, preserving an integration history made of merge commits.
syncwheel is for teams or maintainers who have at least one of these conditions:
- active upstream + fork workflow, especially in open source
- multiple PR branches that must stay clean while development continues
- long-lived PRs that need regular rebuilds on top of a moving base branch
- an
integration/*branch used as day-to-day runnable state - multi-device or AI-agent workflows where no single checkout should be considered authoritative
- need for repeatable branch recovery that does not depend on memory
syncwheel is usually overkill when:
- you ship directly from one branch with short-lived PRs only
- your repo has no integration branch and no stacked branch maintenance
- your process does not need deterministic rebuilds from a declared manifest
-
Guide-first (manual execution)
Use docs/manual-git-flow.md as an operating playbook and run the underlying Git steps manually. This is possible, but cognitively heavier and easier to get wrong in complex branch graphs. -
Script-assisted (human-operated)
Use the CLI for discovery, validation, manifest updates, branch rebuilds, Git wrappers, and push wrappers while a human decides what to run and when. This is a strong middle ground once the team knows the model well. -
AI-operated (recommended)
Let an AI agent run the syncwheel flow through prompts, with a human supervising intent and approval boundaries. In practice this gives the best speed/consistency balance for ongoing maintenance.
CLI install
uv tool install "git+https://github.com/NestDevLab/syncwheel"
syncwheel self statusAI agent handoff
Give an agent install.md when you want it to install Syncwheel, verify the CLI, inspect
a repository, and install the companion skill through Agentwheel.
curl -fsSL https://raw.githubusercontent.com/NestDevLab/syncwheel/main/install.mdCompanion skill
When Agentwheel is available, install the Syncwheel skill into the runtime you are using:
agentwheel doctor --adapter codex --local --skill syncwheel --source github:NestDevLab/syncwheel
agentwheel install github:NestDevLab/syncwheel --adapter codex --local --skill syncwheelRequirements:
- Python 3.11+
- Git
- uv 0.10+ for PATH-based installs
Recommended production install:
uv tool install "git+https://github.com/NestDevLab/syncwheel"Development editable install from a local checkout:
uv tool install --editable .Installer script:
scripts/install.sh
scripts/install.sh --editable /path/to/syncwheelIf uv is not installed, scripts/install.sh exits with instructions by
default. To explicitly let the installer bootstrap uv with the official
astral.sh installer, pass --with-uv.
Legacy checkout execution remains supported for pinned submodules, vendored checkouts, and existing scripts:
python3 scripts/syncwheel.py --helpSyncwheel now includes a built-in install/update channel so humans and AI agents can notice new releases instead of silently drifting.
- default mode:
notify - automatic notice is emitted on normal syncwheel usage when the local install is behind the configured update source
- git-checkout installs update with the existing
git fetchplus fast-forward merge flow - uv tool installs update with
uv tool upgrade syncwheel - manual inspection:
syncwheel self status
syncwheel self check-update --fetchWhen Agentwheel is on PATH, self status also checks whether the local Codex
runtime has the Syncwheel agent skill installed for the current repo. Install it
with:
agentwheel install github:NestDevLab/syncwheel --adapter codex --local --target-root /path/to/repo --skill syncwheelIf the syncwheel executable is not installed yet, run the same status checks
through the checkout script:
python3 /path/to/syncwheel/scripts/syncwheel.py self status
python3 /path/to/syncwheel/scripts/syncwheel.py self check-update --fetch- manual update:
syncwheel self update- update policy:
python3 scripts/syncwheel.py self mode notify
python3 scripts/syncwheel.py self mode auto
python3 scripts/syncwheel.py self mode offauto tries a safe fast-forward self-update when a newer upstream version is
detected for git-checkout installs and runs the uv tool updater for uv installs.
If a git checkout is dirty or detached, syncwheel falls back to a visible notice
instead of mutating it unsafely.
For uv installs, self check-update reads the upstream VERSION file directly
instead of requiring a local git checkout. Advanced wrappers can override the
version source with SYNCWHEEL_REMOTE_VERSION_URL and the installer/update
source label with SYNCWHEEL_UV_TOOL_SOURCE.
-
uv production tool (recommended for normal hosts)
- Run
uv tool install "git+https://github.com/NestDevLab/syncwheel". - The
syncwheelexecutable is placed on PATH when uv's tool bin directory is configured in the shell. syncwheel self updateuses uv to upgrade the installed tool.
- Run
-
uv editable development tool (recommended for syncwheel development)
- Clone
syncwheelonce in a stable location. - Run
uv tool install --editable /path/to/syncwheel. - The
syncwheelexecutable reflects local source edits immediately. syncwheel self updatetreats the checkout as a git install and uses the existing fast-forward flow against the clone's upstream.
- Clone
-
Git submodule
- Add
syncwheelas a submodule inside each target repo. - Good when each project must pin an explicit syncwheel version.
- Invoke it with
python3 path/to/syncwheel/scripts/syncwheel.py .... - Self-update status works for detached submodule-style checkouts; updating remains controlled by the parent repository's submodule policy.
- Add
-
Vendored checkout or script
- Copy
scripts/syncwheel.pyinto a project. - Fastest for experiments, but updates are manual.
self statusreportsinstall_kind: scriptwhen no git checkout or uv tool environment is detected.
- Copy
You can register repo aliases and keep commands short.
python3 scripts/syncwheel.py repo add project ~/code/sample-project
python3 scripts/syncwheel.py repo ls
python3 scripts/syncwheel.py self status
python3 scripts/syncwheel.py self check-update --fetch
python3 scripts/syncwheel.py self update
python3 scripts/syncwheel.py self mode notify
python3 scripts/syncwheel.py status -r project --fetch
python3 scripts/syncwheel.py repo rm project-r/--repo accepts both:
- a filesystem path
- a registered alias
Alias entries can also carry a default manifest path (useful for private/local manifests on public repos):
python3 scripts/syncwheel.py repo add service ~/code/sample-service \
--manifest ~/.config/syncwheel/manifests/sample-service.json
python3 scripts/syncwheel.py repo set-manifest service ~/.config/syncwheel/manifests/sample-service.json
python3 scripts/syncwheel.py repo set-manifest service --clearYou can also set SYNCWHEEL_REPO when wrapping syncwheel from another project:
SYNCWHEEL_REPO=/path/to/repo python3 scripts/syncwheel.py checkCreate the shared manifest with init:
python3 scripts/syncwheel.py initCreate a personal local manifest without copying or hand-writing JSON:
python3 scripts/syncwheel.py init --personal aliceThis writes .syncwheel/manifests/alice.local.json and defaults its integration
branch to integration/alice/main. Use -p alice on later commands
when you want to target that personal manifest:
python3 scripts/syncwheel.py check -p alice
python3 scripts/syncwheel.py s new -p alice feature-a --branch pr/alice/feature-a -u
python3 scripts/syncwheel.py s set -p alice feature-a origin/main..HEADLong names are still available: stack create --personal alice is equivalent,
spoke is a readable alias for stack, and -u is the short form of
--include-in-integration.
To make a personal manifest the default for the current clone:
python3 scripts/syncwheel.py use alice
python3 scripts/syncwheel.py check
python3 scripts/syncwheel.py use --shareduse alice writes .syncwheel/profile.local.json, which should be ignored by
the host repository because it is local operator state.
Each stack can include optional meta fields so humans and AI can understand intent better.
Example:
{
"id": "endpoint-resolution-policy",
"branch": "pr/endpoint-resolution-policy",
"commits": ["abc1234"],
"meta": {
"purpose": "Endpoint policy and routing",
"status": "active",
"priority": "p1",
"dependencies": [],
"integrationPolicy": "required",
"notes": "Keep in integration for runtime validation"
}
}python3 scripts/syncwheel.py init
python3 scripts/syncwheel.py checkFor a custom integration branch:
python3 scripts/syncwheel.py init --integration-branch integration/team-stackUse --stdout only when you need to pipe the generated manifest instead of
writing it to .syncwheel/manifest.json.
python3 scripts/syncwheel.py stack create feature-a --branch pr/feature-a -u
python3 scripts/syncwheel.py stack sync feature-a
python3 scripts/syncwheel.py stack set feature-a origin/main..HEAD
python3 scripts/syncwheel.py stack add feature-a HEADUse stack sync when the branch already represents the intended PR stack. Use
stack set or stack add when you want to declare an explicit revision range
or append a new commit.
When the main checkout is on the integration branch, you can make and test changes there first, then assign those changes to the PR stack that owns them:
python3 scripts/syncwheel.py stack absorb feature-a path/to/file.ts
python3 scripts/syncwheel.py stack absorb feature-a --stagedBy default, stack absorb amends the stack branch tip, refreshes that stack's
manifest commits, and reverse-applies the absorbed patch from the integration
checkout. Pass --no-amend -m "message" when the absorbed change should become
a new stack commit. Use --staged after git add -p when one file contains
changes for multiple PR stacks.
Example: two files belong to feature-a, one file belongs to feature-b, and
one mixed file has hunks for both stacks:
python3 scripts/syncwheel.py stack absorb feature-a a1.ts a2.ts
python3 scripts/syncwheel.py stack absorb feature-b b1.ts
git add -p shared.ts
python3 scripts/syncwheel.py stack absorb feature-a --staged
git add -p shared.ts
python3 scripts/syncwheel.py stack absorb feature-b --staged
python3 scripts/syncwheel.py syncUse reconcile as the normal maintenance entrypoint:
python3 scripts/syncwheel.py repo tracking status
python3 scripts/syncwheel.py reconcile
python3 scripts/syncwheel.py resume
python3 scripts/syncwheel.py sync
python3 scripts/syncwheel.py publishreconcile fetches by default, classifies stack and integration drift, and
prints a dry-run plan. sync runs the same lifecycle locally: it rebuilds only
branches that differ from the manifest projection unless --rebuild all is
passed, refreshes stack commit SHAs after rebuilds, and rebuilds integration
from the current manifest. publish does the same local work and then uses
Syncwheel push wrappers for managed branches. The report also prints the
current working tree status, including uncommitted files, before validation and
drift details so dirty checkouts are visible without running a separate
git status.
resume is a thin recovery layer on top of reconcile. It pre-classifies
unmapped integration commits when ownership is deterministic, then runs the
normal reconcile planner on the resulting manifest. Use either form:
python3 scripts/syncwheel.py reconcile --mode resume
python3 scripts/syncwheel.py resumeIn resume mode Syncwheel can:
- add an unmapped integration commit to an existing owning stack when exactly one owner is detected
- restore a previously known historical stack from the ledger when the branch still exists locally or remotely and ownership is unambiguous
- leave the commit in manual review when ownership is ambiguous
The ledger lives under .syncwheel/ledger/ when the manifest is repo-local.
When --manifest points outside the repository, Syncwheel stores the ledger in
a sibling directory next to that manifest, derived from its filename. For
example, docs/syncwheel/glow-portals-manifest.json uses
docs/syncwheel/glow-portals-ledger/.
Each ledger root contains:
events/000001.jsonl,000002.jsonl, ... contain append-only event segmentscheckpoints/latest.jsoncontains the replayed current state for fast reads
Use this to inspect the current replayed ledger state:
python3 scripts/syncwheel.py ledger show
python3 scripts/syncwheel.py ledger show --jsonWhen integration contains non-merge commits that are not declared in any stack,
check and reconcile print commit-level guidance: short SHA, subject, touched
files, local and remote branches containing the commit, likely stack owners, and
suggested next commands such as syncwheel stack add <stack> <sha> followed by
syncwheel reconcile. This keeps the common integration-first repair path
inside Syncwheel instead of requiring separate git log, git show, and
git branch --contains commands.
When the remote branch already matches the manifest projection, sync,
publish, and reconcile --apply align the local branch to the remote and do
not update the manifest or push new replacement commits. Pass
--no-align-local-to-remote when you intentionally want to preserve a different
local history for manual inspection:
python3 scripts/syncwheel.py sync --no-align-local-to-remotepublish and reconcile --push use --force-with-lease by default because
rebuilt managed branches commonly replace older remote history in multi-device
workflows. Pass --no-force-with-lease only when a normal push is
intentionally required.
Use --json for automation, --stack <id> to limit stack work, --remote to
override the publication remote, and --in-place-integration only when the
current checkout is already on the clean integration branch and should be reset
as part of the reconcile.
reconcile is the preferred lifecycle command. The object/action commands are
still useful for targeted repair and inspection:
python3 scripts/syncwheel.py validate
python3 scripts/syncwheel.py plan --json
python3 scripts/syncwheel.py stack absorb feature-a path/to/file.ts
python3 scripts/syncwheel.py stack rebuild feature-a --worktree ../wt-pr-feature-a
python3 scripts/syncwheel.py stack push feature-a -- --force-with-lease
python3 scripts/syncwheel.py stack git feature-a --worktree ../wt-pr-feature-a -- status
python3 scripts/syncwheel.py int rebuild --worktree ../wt-integration
python3 scripts/syncwheel.py int push -- --force-with-lease
python3 scripts/syncwheel.py int git --auto-worktree -- status
python3 scripts/syncwheel.py int sync-status --jsonUse --dry-run on rebuild and push commands to print commands without applying
them. If the remote integration branch already matches the manifest projection
and the local checkout is stale, int align-remote can align a clean local
integration checkout to the remote with a backup branch first.
When two devices or workstreams use different manifests and integration branches, compare the manifests instead of merging their integration branches:
python3 scripts/syncwheel.py manifest compare --other-personal laptop --json
python3 scripts/syncwheel.py manifest compare --other-manifest ../other-manifest.jsonThe comparison reports shared stacks, stacks only present in one composition, and shared stacks whose branch/base/commit list diverges.
Syncwheel includes a pre-commit hook that runs the version-bump guard against staged files. Enable the tracked hooks once per clone:
python3 scripts/syncwheel.py self install-hooksAfter that, commits that stage release-relevant changes under scripts/,
tests/, or githooks/ must also stage VERSION, CHANGELOG.md, and the
README current-version line.
self status reports whether the hooks are active in the current Syncwheel
installation. See docs/manual-git-flow.md for the
raw Git equivalent of the Syncwheel lifecycle.
scripts/syncwheel.py: main CLIscripts/syncwheel-status.sh: small compatibility wrapperdocs/: human-readable workflow docs and guidesexamples/manifest.example.json: starter manifesttests/: unit tests and fixture repositoriesVERSION: current release versionCHANGELOG.md: release notes
install.md: AI-agent handoff for installing Syncwheel and the companion skillAGENT.md: concise operating guide for AI agentsllms.txt: LLM-oriented map of the public docsdocs/workflow.md: concise workflow modeldocs/core-procedure.md: deterministic recovery proceduredocs/manual-git-flow.md: raw Git equivalent of the Syncwheel lifecycledocs/branch-model.md: branch role model and safety defaultsdocs/deterministic-model.md: manifest semantics and validation contractdocs/ai-agents.md: short AI behavior contractdocs/agent-procedure.md: extended AI execution guidancedocs/workflow-longform.md: long-form practical workflow guidedocs/public-article.md: narrative article version for broader audiences
python3 scripts/syncwheel.py --help
python3 scripts/syncwheel.py --version
python3 scripts/syncwheel.py init --help
python3 scripts/syncwheel.py check --help
python3 scripts/syncwheel.py status --help
python3 scripts/syncwheel.py validate --help
python3 scripts/syncwheel.py plan --help
python3 scripts/syncwheel.py reconcile --help
python3 scripts/syncwheel.py ledger show --help
python3 scripts/syncwheel.py resume --help
python3 scripts/syncwheel.py sync --help
python3 scripts/syncwheel.py publish --help
python3 scripts/syncwheel.py stack --help
python3 scripts/syncwheel.py int --help
python3 scripts/syncwheel.py stack rebuild --help
python3 scripts/syncwheel.py stack push --help
python3 scripts/syncwheel.py stack git --help
python3 scripts/syncwheel.py int rebuild --help
python3 scripts/syncwheel.py int push --help
python3 scripts/syncwheel.py int git --helpCommon aliases:
check->ckstatus->stvalidate->vplan->plreconcile->recstack->s,spokeint->istack create->s newstack rebuild->s rbint rebuild->i rbgitsubcommands ->g--personal->-p
Agents should not infer stack ownership from memory when the repository is meant to be maintained via syncwheel.
Recommended sequence:
repo tracking statusreconcile- update the manifest with
stack sync,stack set, orstack addif the dry-run report shows real ownership changes syncpublishwhen the rebuilt managed branches should become the shared remote state- rerun
reconcileorcheckand report remaining drift honestly
See docs/ai-agents.md.
When .syncwheel/manifest.json is the source of truth for exact stack commits,
do not try to make a manifest-editing commit describe itself inside that same
manifest revision.
Use this rule instead:
- stack commits describe product/runtime changes
- manifest edits and syncwheel-version bumps are control-plane metadata
- rebuild
pr/*branches and integration from the manifest - keep manifest-maintenance commits out of
integration.stacks
For the operational flow, see docs/core-procedure.md.
MIT