Keep many long-lived pull requests clean, rebuildable, and publishable from one manifest.
Current version: 0.45.5
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
- want to send several pull requests while working in a single checkout, without a worktree per branch
- need to own a commit before you know which pull request it belongs to
- need a repeatable branch that combines selected feature stacks for a shared or temporary deployment flow
- 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
- a rebuild restores and records a deterministic, manifest-only control commit
when its projected tree carries an older manifest;
int rebuild --reasonis required forai-managedrepositories - deployment channels pin selected stack revisions into ordered, rebuildable branch compositions
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 entrypointcheck --strictis the CI/readiness gate: warnings, undeclared stack-branch commits, or any planned action produce a non-zero exitresumeis 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 - legacy manifests use
--force-with-leaseby default, because managed branches are often rewritten by deterministic rebuilds - active-active version 2 or 3 manifests publish managed refs and state together with atomic, exact leases; unsupported remotes fail closed
- public coordination state uses typed canonical and publication remote roles, rejects ambiguous aliases that cannot be normalized safely, and makes a reused managed branch immediately supersede its old cleanup tombstone
repo 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
For legacy manifests, 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: without active-active coordination the integration
branch gets no control commit, and no Syncwheel push carries the manifest to a
stack, pull-request or delivery branch. New managed worktrees default to repo-relative
.syncwheel/wt/.
authority declares how far agents may take a change on their own. A repo
with mode=ai-managed and source_change allowed lets an agent edit, test,
commit, push, open and merge the PR without asking at each step; runtime_change
must be granted separately; destructive_rewrite can never be granted. An
absent block means human-gated. Inspect and set it with
syncwheel repo authority status and syncwheel repo authority set.
Manifest versions 2 and 3 can safely coordinate the same integration branch from
multiple independent devices or agents. New git-tracked manifests enable it
when their publication remote is configured:
syncwheel init --syncwheel-tracking git-tracked --publication-remote origin
syncwheel handoff
syncwheel publishExisting version 1 manifests remain legacy. Upgrade deliberately with
syncwheel coordination init --remote origin --apply; opt out with
syncwheel coordination disable --apply. Active coordination requires atomic
push support and never falls back to serial pushes. See
the active-active protocol for the
state model, lease handling, explicit merge acceptance, privacy contract, and
local cleanup safeguards.
Every managed source ref has one CAS authority at
refs/heads/syncwheel/claim/heads/.... Each coordinated publication advances
that claim in the same atomic push as the source and state refs; an unchanged
claim is not accepted as lease evidence. Existing manifests normalize
coordination.claims to advisory. In that mode Syncwheel still creates claims
and reports gaps. Prepare an explicit switch to required with:
syncwheel coordination claims backfill
syncwheel coordination claims backfill --apply --reason "claim migration"Backfill uses create-only leases and never overwrites a foreign claim.
required refuses published state that lists an owned source without a claim.
Syncwheel covers concurrent Syncwheel commands, process death, ordinary Git on
unowned refs, and arbitrary readers. It fails closed on a detectable CAS,
ordering, intent, or recovery violation, and it never leaves state that a retry
of the recorded operation, or the next coordinated command, cannot resolve;
forced/raw mutation of Syncwheel-owned refs and hostile remotes remain outside
that guarantee.
Every mutating coordinated publisher records a durable operation token before the atomic push. If process death occurs after remote success, the retry accepts only state and claims carrying that token and completes without republishing, including when later publications have moved the shared state on. Draft create uses the same token in its create intent and remote claim, and cleans up its token-derived temporary worktree registration on retry.
One publication cycle runs at a time in a clone. Every coordinated command takes
<git-common-dir>/syncwheel/coordination-publication.lock around its whole
intent, push, local remainder and terminal event, and a second command refuses
with the live owner's pid rather than deciding anything about its intent; a
plain retry after it finishes succeeds. Process death releases the lock, so a
dead owner's intent is recovered by the next command. Dry runs and handoff
take no lock and are never refused.
A publication intent is always terminal, and a token has exactly one terminal
record; a second terminal is refused when it is written, never when the ledger
is read. Landing is proved by a state commit inside the intent's own window that
declares this operation's token, scope, changed refs, projection status and
manifest digest, together with a claim carrying that token for every touched
source ref; never by current ref tips, so another clone legitimately advancing
the same ref cannot turn a landed operation into an abandoned one, and a token
reused for another operation cannot prove this one. An intent with no recorded
expected state tip is never proved landed by the chain. Recovery, and only
recovery, accepts a second form after that one fails: every changed ref, and no
other, carrying a claim that declares this operation's token, scope and whole
ref set. That form has no window and no manifest digest, so it still holds when
a state ref is rewritten backwards, and a claim carrying the token under another
scope or another ref set proves a different operation, not this one. The digest
in both forms ignores integration.derived_provenance, which the two sides
resolve from different sources by design. An intent absent from that evidence
never reached the remote and is recorded as
coordination_publish_abandoned by the next publication, reconcile, or
resume, with reason not_landed when nothing else published meanwhile and
superseded when the reviewed state tip was overtaken; the local rename of an
abandoned promotion is undone only when the remote carries neither the promoted
ref nor a claim bearing its token. A promotion whose push landed before its
manifest save owes only that save: the next coordinated command, stack rebuild, stack sync and stack add included, completes it from the published
state and rebuilds the promoted branch, so an intervening sync cannot strand
it, and a draft branch that came back with commits of its own is anchored under
refs/syncwheel/recovery/drafts/ before it is dropped. A clone that lost a
promotion race can also rerun stack promote: when the published stack,
managed ref, and promotion tombstone exactly match its draft, Syncwheel adopts
that published branch locally without republishing coordination state. A clone
whose publication was refused by the remote, or died before its push, keeps
publishing after a plain retry. A coordination remote that is unreachable, or
that refuses the atomic push without changing anything, fails closed and names
the retry command for stack push, int push, stack promote, stack create --draft, publish, reconcile --apply --push, and coordination claims backfill.
When a managed branch is correct but coordination state recorded the wrong tip, generate a digest-bound repair plan. The default backend remains non-mutating:
syncwheel coordination repair --ref refs/heads/main-integration > repair-plan.json
syncwheel coordination repair --apply --plan-file repair-plan.jsonApply rechecks the exact plan, ownership, pending merges, and every guarded ref.
Ordinary Git drops an unchanged no-op refspec, so there is no Git push fallback.
GitHub branch locks are not sufficient: administrators can bypass or change
them concurrently, so github-lock stops as unsupported before any state
update.
For the narrower case where the recorded and observed commits have exactly the
same tree, use --freeze-backend tree-equivalent-state-cas for both plan and
apply. This backend fetches and proves both commit trees, rechecks the state tip
and every managed ref, then pushes only an append-only state child under an
exact state-ref lease. It never updates the managed branch. Any different tree,
ownership uncertainty, ref drift, lease loss, or post-CAS drift stops
fail-closed; an accepted state child remains durable evidence even when the
post-verification reports failure.
When an actively managed ref has instead advanced through an exact, reviewed
fast-forward, use --freeze-backend fast-forward-state-cas. The plan binds the
recorded and observed tips and trees plus the complete bounded commit interval.
Apply repeats the ancestry and drift checks, then appends only state evidence
under the same exact state-ref lease. It never updates the managed branch and
refuses non-descendants, intervals above 1024 commits, plan drift, ownership
uncertainty, or concurrent ref changes.
When coordination state still owns a ref that was deleted remotely, use
--freeze-backend missing-ref-create-cas. The reviewed plan requires the ref
to remain actively declared and absent and the recorded commit object to exist
locally. Apply records a durable intent, then atomically creates that exact ref,
publishes a successor ownership claim, and appends a child state under exact
leases. A retry can complete an interrupted successful publication from its
operation token. Inactive or tombstoned refs are rejected; if any guarded ref
or claim appears or changes, nothing is overwritten.
A coordination state records the digest of .syncwheel/manifest.json on its
integration tip. States published before 0.42.2 recorded the digest of the
normalized public snapshot of that same manifest instead, so the guard accepts
either form and reports which one matched; anything else still fails closed.
The first publish, push, or compose over a legacy state rewrites the state with
the control-manifest digest and records a coordination_state_digest_migrated
ledger event. To migrate without publishing anything else, plan and apply with
--freeze-backend state-digest-migration: the plan reports
digest-migration-required instead of noop, binds both digests, and apply
appends a state child that changes only the recorded digest under the same
exact state-ref lease.
When the remote state and a stale local manifest independently added stacks,
compose the proposals explicitly instead of weakening stack push:
syncwheel coordination compose \
--stack new-stack \
--known-base-state <state-sha> \
--known-base-manifest-digest <integration-control-manifest-digest> > compose-plan.json
syncwheel coordination compose --apply --plan-file compose-plan.jsonCompose accepts only one local additive stack and additive remote stacks from the exact known base. Existing records, shared defaults, integration strategy, and membership order must be unchanged. Apply rederives the complete plan, then atomically publishes only the new stack ref and an append-only partial state child under exact leases. The integration ref is observed but never updated; unmapped integration commits remain unmapped diagnostics. If remote publication succeeds but local manifest adoption fails, replan produces an adopt-only operation and never repeats the push.
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 -nThe safest default is:
- keep the primary working checkout on the shared integration branch
- rebuild and publish every PR branch from there, without checking it out
- optionally keep a separate administrative checkout for manifest-only work
- begin routine implementation, dependency installation, builds, and tests on integration
- create a desk only to resolve a conflict or validate a non-empty materialized stack when integration cannot safely run it
stack rebuild and int rebuild leave no worktree behind by default. Their
auto replay mode uses Git plumbing where Git supports it (2.38 or newer) and a
self-removing temporary worktree below that, the current checkout when it is
already on the target branch, and an existing worktree when the branch has one.
Pin a mode with --replay-mode, with syncwheel replay-mode <mode> for a
repo-local default, or with defaults.replay_mode in the manifest.
When another agent has explicitly locked or owns the primary checkout, continue authoring only by asking for a governed lane. This is an explicit fallback, not an automatic reaction to a lock:
syncwheel worktree open concise-change
syncwheel worktree open dependency-repair --fullopen creates one clone-local, registered worktree below the configured
syncwheel_worktree_root and prints its path. A light lane is for authoring and
committing only; Syncwheel does not provision dependencies there. --full is
the explicit, bounded choice when dependency installation, builds, tests, or
debugging are necessary. It is a lifecycle declaration, not a sandbox: a raw
shell can still bypass it, so agents must keep that boundary in their procedure.
Each clone permits four active lanes by default. A repository can change the limit, or
make reaching it a warning instead of a refusal, with the manifest block
"governed_worktree_capacity": {"limit": 4, "enforcement": "error"} (enforcement:
error or warn). The local registry records the owner,
lease, base, branch, target stack, mode, and any recovery ref in Git's common
directory, never in the shared manifest. Use --into <existing-stack> when the
destination is already known. Otherwise, after committing, use the existing
stack create, stack add, or stack capture-integration workflow from a
different clean checkout. Once a stack owns every lane commit, Syncwheel anchors
the lane tip under refs/syncwheel/recovery/lanes/... before reaping the clean
worktree and local lane branch. A dirty, unavailable, or current-directory lane
is retained and reported; it is never removed automatically. A missing lane
whose lease expired, or whose local owner PID is known to be dead, is reaped on
the next applicable mutation even when an old registry path is outside the
current configured root. If Git reports that the branch's worktree moved,
Syncwheel resolves and checks the current path before deciding whether the lane
is eligible, and retains moved dirty or externally locked worktrees.
The clone-local registry mutex records its PID, process start time, and a unique
token. A successor first obtains the old inode's non-blocking flock, then
atomically renames and logs it when the metadata proves owner death, PID reuse,
or an unreaped zombie. An empty or truncated lock is recovered only after a
brief initialization grace, so a plain retry can resume even when SIGKILL
lands between exclusive creation and metadata fsync. The creator verifies that
its inode still owns the lock path before entering the critical section, so a
stolen uninitialized lock never puts two processes inside it. That recovery is
reported as an uninitialized lock rather than a dead owner, because its creator
may be alive and merely descheduled, and the renamed inode is pruned by the next
cleanup once nobody holds it; the durable recovery log keeps the evidence. While
holding that mutex, cleanup takes a Git worktree lock with a deterministic
Syncwheel token before classifying the lane or changing a ref. A lock failure is
reported as a lane in use and stops the operation.
Under both locks, Syncwheel verifies the exact Git admin directory and its
gitdir, fsyncs a ledger governed_worktree_cleanup_intent, and saves the
pending registry state through a temp-file and parent-directory fsync guarded by
the observed pre-image digest. Stack create, add, and capture keep that intent
and its terminal event in the ledger selected by the effective shared,
personal, or external manifest. Only then may cleanup create the recovery ref
and commit one expected-old ref transaction that verifies the recovery ref
while deleting the lane branch. A restart rebuilds missing or rolled-back
registry state from the durable intent. A final tracked/untracked probe precedes
removal of the exact verified registration; cleanup never uses a global git worktree prune.
A reappearing path, changed registration, or moved recovery ref fails closed
with a retryable record. If a clean lane branch advanced after its first anchor,
the recorded gc --apply remedy anchors the new tip and completes while
retaining the earlier recovery ref. The locks cover concurrent Syncwheel cleanup
and ordinary non-forced Git worktree operations. Raw mutation of
Syncwheel-owned refs, double-force worktree operations, and direct non-owner
writes into a lane during cleanup are outside the supported concurrency model
and fail closed when their effects are detectable.
To retire a known dead or abandoned lane deliberately, preview the operation first and provide a durable reason:
syncwheel worktree release abandoned-lane --reason "superseded by pr/example"
syncwheel worktree release abandoned-lane --reason "superseded by pr/example" --applyrelease is dry-run by default. With --apply, it creates a recovery ref for
an existing lane-branch tip, removes the registry record, and appends a ledger
event. An explicitly abandoned record whose path is already missing can still
be released; any remaining Git worktree registration is removed before the
registry record. It refuses an existing dirty lane and names the recovery
remedy instead of removing it. Pending cleanup, including a branch that advanced
before its expected-old transaction, and ledger writes are retryable and
idempotent. An explicit release may supersede the automatic branch_advanced
intent, retain its earlier recovery ref, anchor the new tip, and close with the
operator's release reason; a release-originated retry retains its original
reason. A release also completes any other pending reap state, including one
left by SIGKILL, terminalizing it under the intent already fsynced for it. If
another Syncwheel command already terminalized the lane, or the first release
completed but its response was lost, the release reports that terminal ledger
event instead of an unknown lane. Whenever the recorded terminal is not this
operator's own release reason, --apply records that reason as a
governed_worktree_release_noted ledger event, once per terminal; the dry run
records nothing. gc reselects eligible lanes while holding the registry lock,
avoids stale lane-id reuse, and also works when active-active coordination is
disabled.
Automatic lane reaping runs only before an explicitly mutating lifecycle
operation. Status, check, handoff, gc without --apply, reconcile or
resume without --apply, and every other preview leave the registry,
branches, recovery refs, and ledger unchanged. stack git and int git join
that mutation allowlist only when --auto-worktree or --worktree explicitly
authorizes worktree creation; passthrough Git commands in an existing worktree
do not implicitly trigger lane reaping.
status, check, handoff, and gc include structured governed-worktree
diagnostics in JSON. Repo-aware terminal commands show actionable yellow
warnings for unfinished lanes when stderr is a TTY; NO_COLOR removes ANSI
color and JSON output remains free of ANSI sequences.
When an occupied primary checkout stops a mutating command, Syncwheel names
manifest-derived capture and queue commands in the error. Capture committed
primary work with syncwheel stack capture-integration <stack> HEAD, or leave
another agent's primary checkout unchanged and use
syncwheel worktree open <lane> --into <stack>. Capacity and expired-lane
diagnostics name the corresponding syncwheel stack add <stack> <base>..<lane>
queue command.
A commit made on the integration branch has to belong to a stack to reach the base branch. Until it
does, validate reports it as owned by nobody and the next integration rebuild drops it.
A draft stack closes that gap. It owns its commits like any other stack, but is forbidden from becoming a pull request until you promote it:
syncwheel stack create --draft caching-experiment
syncwheel stack capture-integration caching-experiment HEAD
syncwheel stack promote caching-experiment --branch pr/cachingA draft refuses stack push to the target remote and names its state as the reason. Under
active-active coordination its source ref does publish to the coordination remote, so another clone
can rebuild the draft from the manifest alone. Draft creation uses a create-only ref CAS and rechecks
cross-domain ownership at publication, so a concurrently created local ref or coordination claim is
preserved and reported instead of replaced. A retry adopts an equivalent completed remote create
before probing whether a new atomic push is possible. stack demote reverses the promotion, and
refuses when the stack already records an open pull request.
Closing a never-published draft is remote-first under active coordination. It
records stack_close_intent, atomically publishes a tombstone claim plus state,
saves the manifest, and only then records stack_closed. A retry after a crash
between push and save recognizes its operation token in the tombstone and
completes idempotently; an unreachable remote fails closed without changing the
manifest, including during recovery. If a later create has advanced the claim,
the old close is terminalized as close_superseded and cannot close the new
generation. A close whose tombstone already landed completes from its intent
even when unrelated publications have advanced the state, instead of publishing
a second tombstone.
A coordinated close, of a draft or of a published stack, publishes the
published coordination state minus the closed stack. Unpublished local changes
to other stacks stay local and no longer block the close, and a stack that
another clone published but this manifest lacks stays published. The saved
local manifest is the local manifest minus the closed stack. A retried published
close looks for its token in the state history and in the closed ref's claim
history before any delivery or ancestry proof. A close that already landed
completes locally without publishing again, whatever changed locally or on the
delivery branch since, and refuses only a different --reason. When this clone declared
the same stack id again after that close, or its published entry matches the
local one, the old intent is recorded as recovered and the command closes the
stack as a new operation. A close that never landed is
abandoned and the retry writes a new intent against the current state, unless
the stack's published entry changed in between: then the retry stops with
close_superseded and the next run starts fresh. Choose the close proof from
the delivery method:
syncwheel stack close feature-a --reason merged # normal merge or fast-forward
syncwheel stack close feature-a --reason absorbed # squash or rebaseabsorbed first fetches and records the observed delivery SHA, then compares
the fully composed stack result with that delivery tip for every touched path.
Squash-equivalent delivery is accepted; a historical patch later reverted at
the tip is not. A verified absorbed close does not need --force.
When plan finds integration commits belonging to no stack, it now names capture-integration into a
new draft as the remedy.
When the owning stack already exists and its source branch must remain unchanged, classify the commit declaratively instead. Preview is the default; apply requires the exact digest printed by the preview:
plan=$(syncwheel stack classify-integration caching-experiment HEAD)
digest=$(printf '%s' "$plan" | jq -r .planDigest)
syncwheel stack classify-integration caching-experiment HEAD --apply --plan-digest "$digest"This writes only the manifest and ledger. The commit is recorded in
integration_only_commits and remains part of future integration rebuilds,
including merge-stacks, without being projected onto the stack branch. These
commits replay after the combined stack projection so they retain the context in
which integration-first work was created.
stack land is the bounded alternative to a PR when a maintainer explicitly
asks to land an already-validated stack. It does not create a lane type, open a
PR, close a stack, deploy a channel, rewrite history, or mutate
main-integration. A preview rechecks the exact declared source projection,
combined integration projection, clean worktrees, dependency ancestry, remote
delivery tip, and (when enabled) active-active coordination. Its planDigest
binds a second --apply invocation; apply re-runs the same checks and pushes
only through an exact lease.
plan="$(syncwheel stack land caching-experiment --allow-direct --operation-id cache-land-01)"
digest="$(printf '%s' "$plan" | jq -r .planDigest)"
syncwheel stack land caching-experiment --allow-direct --operation-id cache-land-01 \
--plan-digest "$digest" --applyRepositories can opt in with a root landing policy. With no policy,
--allow-direct is required for the specific request. mode: "direct" permits
the normal path; mode: "disabled" retains the explicit bypass requirement.
The optional checks field is a small all/any tree of local commands,
receipt attestations, and PR-check route markers. A failed PR marker or a
protected remote ref stops and suggests stack promote; Syncwheel never opens
the PR itself. Every check override needs a reason and is retained in the
digest-bound plan and ledger receipt.
A channel pins an exact base revision plus a selected, ordered composition of
stacks pinned to exact branch revisions, commit lists, base provenance, and
depends_on closure/order. It is intentionally separate from:
- a stack, which owns one change stream;
- the integration branch, which represents the full current integration projection;
- a deployment, which is external environment state managed by CI/CD or another system.
Channels may be shared (for example a stable test input) or ephemeral (for
example a short-lived feature input with explicit expiry metadata). They do not
follow base or stack tips automatically: channel refresh updates pins
deliberately.
Explicit depends_on metadata is version 3-only. The reviewed v2-to-v3 channel
migration derives direct dependencies from existing stack base chains.
Every mutation previews a channelPlan; --apply requires its exact
planDigest. Durable operations record started/prepared/receipt evidence, and
publishing uses an exact lease. A channel-local resolution snapshot can capture
a resolved composition without rewriting its stacks; composition edits
invalidate it. Expiry never deletes a branch automatically; use channel close
for explicit cleanup.
Shared channels refuse publication when they contain draft stacks. Ephemeral channels may include drafts for temporary testing. Under active-active coordination, every channel uses the coordination remote so its branch and coordination state publish atomically.
Publishing channel/test proves only that a specific Git composition is
available on that branch. It does not prove that an environment has deployed
or is healthy. See deployment channels for the
complete CLI, failure, and CI/CD boundary.
syncwheel has six 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 - optional deployment channels for pinned, selected compositions
- ledger (
.syncwheel/ledger/for the shared manifest, or a sibling<manifest-name>-ledger/directory for personal and external manifests) 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 classify-integrationrecords integration-only ownership through a digest-bound manifest plan without rebuilding a stack or integration ref.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.channel planpreviews channel materialization or publication; every channel mutation requires its exact digest, andchannel publishuses an exact lease.channel operation list/show/reconcileexposes durable operation intent and observes uncertain outcomes without retrying a mutation.stack pushandint pushare targeted publication commands. On active version 2 or 3 manifests they publish a partial atomic state snapshot; legacy manifests retain the direct Git push wrapper and its optional arguments.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.- Stack-scoped
reconcileandresumereport unrelated dirty governed lanes without reaping them. A live lane targeting a selected stack still blocks that stack's mutation. - 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.
For repositories that explicitly use ai-managed source delivery, the
optional digest-bound GitHub PR merge path is documented in
docs/github-pr-merge.md. Its private policy stays
clone-local and its admin bypass is limited to required reviews. CI is required
by default; repositories without CI can explicitly select clone-local
checks: "none", which remains visible as a merge-plan warning.
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. Its operational history is isolated in
.syncwheel/manifests/alice.local-ledger/. 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
python3 scripts/syncwheel.py s set -p alice feature-a origin/main..HEADLong names are still available: stack create --personal alice is equivalent,
and spoke is a readable alias for stack. New manifests include every declared
stack in integration by default; use ordinary Git worktrees for work that should
not enter the Syncwheel lifecycle.
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. syncwheel replay-mode <mode> stores the repo-local default replay execution mode in the same file.
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
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.
Under active-active coordination, stack set <id> --published drops an
unpublished local change to a published stack: it resets the stack's commits to
its entry in the published coordination state, records the previous commits and
the actor in the ledger, and prints the exact stack set <id> <commits...>
command that restores them. It refuses for a stack that is not published and for
a local entry that differs in anything other than its commits. Published commits
missing locally are fetched once from the coordination remote; if they are still
missing, it refuses and names the owner.
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 shared manifest's ledger lives under .syncwheel/ledger/. Every other
manifest gets a sibling ledger derived from its filename, so parallel personal
or external manifests cannot overwrite one another's checkpoint. For example,
.syncwheel/manifests/alice.local.json uses
.syncwheel/manifests/alice.local-ledger/, while
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-remoteLegacy publish and reconcile --push use --force-with-lease by default
because rebuilt managed branches commonly replace older remote history in
multi-device workflows. Active version 2 or 3 manifests instead use the coordinated
atomic publisher and reject manual force, lease, or remote overrides.
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
python3 scripts/syncwheel.py stack git feature-a --worktree ../wt-pr-feature-a -- status
python3 scripts/syncwheel.py int rebuild --worktree ../wt-integration --reason "refresh integration projection"
python3 scripts/syncwheel.py int push
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.
Inspect or explicitly manage the repository-local, composable hook bundle:
syncwheel hooks status
syncwheel hooks install
syncwheel hooks install --apply
syncwheel hooks remove
syncwheel hooks remove --disable --reason "external contribution clone"
syncwheel hooks remove --disable --reason "external contribution clone" --applyThe pre-push guard derives guarded refs from the manifest and published coordination state,
including integration, stack and draft sources, channels, coordination state, an
owned journal branch, and the delivery branches that only stack land may publish.
It blocks direct, aliased, multi-ref, delete, force, and HEAD:<managed> pushes,
then names the corresponding Syncwheel publisher. Existing
hooks are chained and restored on removal; core.hooksPath is honored.
The same bundle installs post-checkout, pre-commit, and reference-transaction guards
for the primary checkout (0.42.4+). post-checkout compares the worktree with the declared
branch and reports a visible failure if a checkout moved it elsewhere — Git has already
completed the switch, so restore a mismatched checkout losslessly rather than resetting
dirty work. pre-commit blocks both that mismatch and a manual commit while the primary is
correctly on the integration branch. reference-transaction blocks every unauthorized
update to the integration ref, including a fast-forward, rewind, creation, or deletion, that
did not go through Syncwheel. Syncwheel's own control and in-place rebuild commits pass
through a short-lived, single-use, per-process nonce; dedicated feature worktrees and
plumbing-materialized branches are unaffected. A refusal names syncwheel worktree open <lane> --into <stack> for new work or syncwheel stack capture-integration <stack> HEAD
for work already committed on the primary. Every built-in mutation also refuses to start
while the primary has tracked changes, naming the same remedies.
For git-tracked repositories the bundle is required by default. Every normal
repo-aware Syncwheel command, including repo tracking status, validate, and
status, checks and converges the bundle before continuing. Initialization and a
transition to git-tracked converge it in the same command. Explicit hooks status|install|remove lifecycle commands remain observational or plan-first so they
can inspect and administer an absent bundle; the generated hook callbacks are also
excluded to prevent recursion. Existing non-Syncwheel hooks are chained and restored
on removal. local-only contribution clones remain opt-in. The only escape hatch is
a persisted clone-local disable with a non-empty reason, which stays visible in
validation.
Syncwheel publishers use a short-lived, single-use authorization scoped to the remote and allowed destination refset.
These hooks are local safety rails, not a security boundary. --no-verify, deleting
the hooks, or operating from a clone before its first normal Syncwheel command can
bypass them.
See the managed repository guard design
for server-side hardening options.
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, the hook renders the two website version labels from VERSION and
stops once when docs/index.html needs to be reviewed and staged. Commits that
stage release-relevant changes under scripts/, tests/, or githooks/ must
also stage VERSION, CHANGELOG.md, and the README current-version line. CI
checks that the committed static site matches VERSION.
Run the same deterministic website render directly with:
python3 docs/sync_version.pyself 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_revision_provider.py: strict Agentwheel revision-provider protocolscripts/syncwheel-status.sh: small compatibility wrapperdocs/: human-readable workflow docs and guidesdocs/sync_version.py: renders the checked-in website version fromVERSIONexamples/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/revision-provider.md: Agentwheel revisioning protocol and recovery contract, including blob-exact manifest-base/derived routing, NUL-safe Git paths, Git-common-dir and coordinated content-bound provenance, snapshot precedence over the clone-local cache, stale/narrowed projection repair, the single-clone limit of uncoordinatedderived_paths, and the accepted derived-route delivery costdocs/branch-model.md: branch role model and safety defaultsdocs/deterministic-model.md: manifest semantics and validation contractdocs/design/active-active-coordination.md: active-active publication and recovery protocoldocs/deployment-channels.md: pinned channel model, CLI, receipts, and CI/CD boundarydocs/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 coordination --help
python3 scripts/syncwheel.py handoff --help
python3 scripts/syncwheel.py revision-provider --help
python3 scripts/syncwheel.py gc --help
python3 scripts/syncwheel.py worktree --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 journal status --help
python3 scripts/syncwheel.py journal snapshot --help
python3 scripts/syncwheel.py journal publish --help
python3 scripts/syncwheel.py journal pull --help
python3 scripts/syncwheel.py journal schedule --help
python3 scripts/syncwheel.py channel --help
python3 scripts/syncwheel.py channel contract
python3 scripts/syncwheel.py channel operation --help
python3 scripts/syncwheel.py stack --help
python3 scripts/syncwheel.py stack close --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 --helpSet repository_mode to journal for a single-branch repository that records
an allowlisted working tree without PR stacks or integration. The manifest's
journal object declares branch, remote, include, exclude,
max_file_bytes, and an optional interval (default 30m).
syncwheel journal status
syncwheel journal snapshot # plan
syncwheel journal snapshot --apply # locked temporary-index commit
syncwheel journal publish # plan snapshot and exact-lease push
syncwheel journal publish --apply
syncwheel journal pull # fetch and plan a fast-forward to the remote tip
syncwheel journal pull --apply
syncwheel journal pull --apply --park-conflicts # save, fast-forward, three-way merge
syncwheel journal schedule install # plan a Linux systemd user timer
syncwheel journal schedule install --applyjournal publish prints excluded_count and pull.kept_local_count in both
plan and apply results. Its other result fields remain available. Use
journal status --json to inspect the full excluded path list; journal snapshot
and journal pull retain their full diagnostic lists.
Journal snapshots refuse a dirty real index, sensitive paths, oversized files, and high-confidence secrets. Publication never merges, resets, rebases, or force-updates a remote.
Several clones may write to the same journal. journal pull fetches the
journal branch, reports aligned, ahead, or behind, and stops on
divergence. When behind, it keeps local changes to paths the remote did not
touch, accepts changes that are byte-identical to the remote tip (including
the residue of a fast-forward the ref guard refused), and fast-forwards under a
Syncwheel ref-move authorization. journal publish --apply runs the same pull
first, so a publisher that fell behind catches up instead of stopping. If its
push loses the lease, the unpublished snapshot returns to the working tree and
the next run retries. Raw git merge or git pull on the journal branch stays
refused by the reference-transaction guard.
A path changed differently on both sides is a conflict. The default pull stops
and names it. journal pull --apply --park-conflicts saves each local version
under .git/syncwheel/journal-conflicts/<stamp>/, restores the path,
fast-forwards, and runs a three-way merge. A clean merge lands in the working
tree. An overlapping or binary edit stays parked with ours/, base/,
theirs/, and a conflict/ file with markers, listed in conflicts.json, and
the working tree keeps the remote version. Conflict markers never enter the
working tree, so a scheduled publish cannot record them.
AI agents resolve journal conflicts themselves rather than handing them to a human:
- Run
syncwheel journal pull --apply --park-conflicts. - For each
conflictentry, read the parked versions and write one merged file into the working tree that keeps the intent of both sides. For append-only logs and notes, keep both entries in chronological order. - Run
syncwheel journal publish --apply, then delete the parked directory.
Stop and ask only when the two sides contradict each other in a way that needs a human decision, such as two different values for the same setting.
Common aliases:
check->ckstatus->stvalidate->vplan->plreconcile->recchannel->chstack->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.
Four ratified working rules (MGT-0206), in full in the skill and docs/ai-agents.md:
- never author or commit in the primary checkout — open a governed lane with
syncwheel worktree open <lane> [--into <stack> [--base <stack-branch>]] [--full] - never resolve a replay conflict with raw git — retry with
--replay-mode desk, then resolve throughstack absorb/stack resolve-integration - integration composition is declared and visible — check
syncwheel int showbefore testing there or blaming your own code - every mutating command carries
--reason, mandatory inai-managedrepositories, surfaced insyncwheel ledger showwith the actor and command
Recommended sequence:
repo tracking statushandofffor an active-active manifest, otherwisereconcile- 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