Skip to content

NestDevLab/syncwheel

Repository files navigation

syncwheel

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

Why Syncwheel Exists

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
  • reconcile compares 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.

30-Second Workflow

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 publish

Default behavior is conservative:

  • reconcile is the dry-run diagnostic entrypoint
  • resume is the dry-run recovery entrypoint for cross-device resume when integration contains unmapped commits that can be classified deterministically
  • ledger show exposes Syncwheel's append-only event ledger and the current replayed state used for cross-machine recovery
  • sync applies the safe local lifecycle without pushing
  • publish applies the lifecycle and pushes managed branches
  • lower-level reconcile --apply and reconcile --apply --push remain available for explicit scripting
  • publish and reconcile --push use --force-with-lease by default, because managed branches are often rewritten by deterministic rebuilds
  • repo tracking status shows whether the manifest is git-tracked, local-only, or missing a persisted policy
  • if a remote managed branch already matches the manifest projection, sync, publish, and reconcile --apply align the local branch to that remote instead of rebuilding new replacement commits
  • pass --no-align-local-to-remote when 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/.

Common Short Flags

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 -n

Worktree-first model

Syncwheel 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.

System flow (visual)

syncwheel has four pieces:

  • base branch (upstream/main or similar)
  • PR stacks mapped to pr/* branches
  • manifest (.syncwheel/manifest.json) as source of truth
  • integration branch (main-integration by 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
Loading

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

How it works in practice

  • A PR stack is one logical change stream mapped to one pr/* branch with an explicit commit list.
  • stack sync, stack set, and stack add update commit ownership without hand-editing SHA lists.
  • stack absorb moves dirty or staged integration-branch changes into a stack branch, updates the manifest, and removes the absorbed patch from the integration checkout.
  • stack rebuild rebuilds one PR branch from the manifest.
  • int rebuild rebuilds integration from ordered stacks.
  • stack push and int push wrap git push, with arbitrary Git arguments after --.
  • reconcile is 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 --apply and --push.
  • In multi-device workflows, reconcile converges toward a remote branch that already matches the manifest projection instead of rebuilding the same logical state into new SHAs on every device.
  • validate and plan detect 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.strategy controls how integration is rebuilt:
    • cherry-pick replays every declared commit into one linear history.
    • merge-stacks merges each declared stack branch in manifest order with --no-ff, preserving an integration history made of merge commits.

Who this is for

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

Who this is not for

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

Three ways to use syncwheel

  1. 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.

  2. 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.

  3. 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.

Install Methods

CLI install

uv tool install "git+https://github.com/NestDevLab/syncwheel"
syncwheel self status

AI 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.md

Companion 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 syncwheel

Install

Requirements:

  • 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/syncwheel

If 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 --help

Self update, notifications, and AI-safe visibility

Syncwheel 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 fetch plus fast-forward merge flow
  • uv tool installs update with uv tool upgrade syncwheel
  • manual inspection:
syncwheel self status
syncwheel self check-update --fetch

When 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 syncwheel

If 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 off

auto 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.

Installation and adoption modes

  1. uv production tool (recommended for normal hosts)

    • Run uv tool install "git+https://github.com/NestDevLab/syncwheel".
    • The syncwheel executable is placed on PATH when uv's tool bin directory is configured in the shell.
    • syncwheel self update uses uv to upgrade the installed tool.
  2. uv editable development tool (recommended for syncwheel development)

    • Clone syncwheel once in a stable location.
    • Run uv tool install --editable /path/to/syncwheel.
    • The syncwheel executable reflects local source edits immediately.
    • syncwheel self update treats the checkout as a git install and uses the existing fast-forward flow against the clone's upstream.
  3. Git submodule

    • Add syncwheel as 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.
  4. Vendored checkout or script

    • Copy scripts/syncwheel.py into a project.
    • Fastest for experiments, but updates are manual.
    • self status reports install_kind: script when no git checkout or uv tool environment is detected.

Repo aliases

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 --clear

You can also set SYNCWHEEL_REPO when wrapping syncwheel from another project:

SYNCWHEEL_REPO=/path/to/repo python3 scripts/syncwheel.py check

Manifest creation

Create the shared manifest with init:

python3 scripts/syncwheel.py init

Create a personal local manifest without copying or hand-writing JSON:

python3 scripts/syncwheel.py init --personal alice

This 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..HEAD

Long 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 --shared

use alice writes .syncwheel/profile.local.json, which should be ignored by the host repository because it is local operator state.

Stack metadata (optional)

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"
  }
}

Quick start

1. Bootstrap or inspect a manifest

python3 scripts/syncwheel.py init
python3 scripts/syncwheel.py check

For a custom integration branch:

python3 scripts/syncwheel.py init --integration-branch integration/team-stack

Use --stdout only when you need to pipe the generated manifest instead of writing it to .syncwheel/manifest.json.

2. Declare stack ownership

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 HEAD

Use 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.

3. Absorb integration-first work into stacks

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 --staged

By 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 sync

4. Reconcile managed branches

Use 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 publish

reconcile 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 resume

In 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 segments
  • checkpoints/latest.json contains 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 --json

When 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-remote

publish 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.

5. Use lower-level commands when needed

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 --json

Use --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.

6. Compare different integration compositions

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.json

The comparison reports shared stacks, stacks only present in one composition, and shared stacks whose branch/base/commit list diverges.

6. Install local Git hooks

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-hooks

After 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.

Files

  • scripts/syncwheel.py: main CLI
  • scripts/syncwheel-status.sh: small compatibility wrapper
  • docs/: human-readable workflow docs and guides
  • examples/manifest.example.json: starter manifest
  • tests/: unit tests and fixture repositories
  • VERSION: current release version
  • CHANGELOG.md: release notes

Documentation map

  • install.md: AI-agent handoff for installing Syncwheel and the companion skill
  • AGENT.md: concise operating guide for AI agents
  • llms.txt: LLM-oriented map of the public docs
  • docs/workflow.md: concise workflow model
  • docs/core-procedure.md: deterministic recovery procedure
  • docs/manual-git-flow.md: raw Git equivalent of the Syncwheel lifecycle
  • docs/branch-model.md: branch role model and safety defaults
  • docs/deterministic-model.md: manifest semantics and validation contract
  • docs/ai-agents.md: short AI behavior contract
  • docs/agent-procedure.md: extended AI execution guidance
  • docs/workflow-longform.md: long-form practical workflow guide
  • docs/public-article.md: narrative article version for broader audiences

CLI summary

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 --help

Common aliases:

  • check -> ck
  • status -> st
  • validate -> v
  • plan -> pl
  • reconcile -> rec
  • stack -> s, spoke
  • int -> i
  • stack create -> s new
  • stack rebuild -> s rb
  • int rebuild -> i rb
  • git subcommands -> g
  • --personal -> -p

AI agent usage

Agents should not infer stack ownership from memory when the repository is meant to be maintained via syncwheel.

Recommended sequence:

  1. repo tracking status
  2. reconcile
  3. update the manifest with stack sync, stack set, or stack add if the dry-run report shows real ownership changes
  4. sync
  5. publish when the rebuilt managed branches should become the shared remote state
  6. rerun reconcile or check and report remaining drift honestly

See docs/ai-agents.md.

Manifest maintenance rule

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.

License

MIT

About

Deterministic Git maintenance for PR stacks, worktrees, integration branches, and multi-agent repositories.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors