syncwheel is meant to reduce ambiguity for AI-driven Git maintenance.
The script owns:
- repo state discovery
- manifest validation
- branch reconstruction commands
The AI agent owns:
- deciding when to update the manifest
- keeping authoring commits out of the shared primary checkout
- running project-specific validation after branch rebuilds
- communicating risks and blockers clearly
- deciding which exact stack revisions belong in a deployment channel and obtaining separate evidence for any external deployment
Ratified operating rules (MGT-0206). Rule 2 is the one a real incident violated by resolving a rebuild conflict with raw git instead of the documented path — the mechanism existed, it just was not prominent enough. Follow all four exactly.
- Never author or commit in the primary checkout; it stays on
manifest.integration.branch. Open a governed lane:syncwheel worktree open <lane> [--into <stack>] [--full]. As of 0.42.4, where hooks are installed (syncwheel hooks install --apply), a manual commit or unauthorized integration-ref move in the primary is refused, and mutating commands refuse while it is dirty. The only opt-out is a reasoned, ledgered disable:syncwheel hooks remove --disable --reason "<why>" --apply. ExportSYNCWHEEL_LANE_OWNER=<agent-name>for the whole session: it owns the lanes you open and is recorded asactoron stack create, add, set, capture, promote, close-intent, and push ledger events, so a refusal blocked by your unpublished stack names you. Without it the actor is<user>@<host>:<parent-pid>. A lane with a named owner is reaped only after its lease expires, never early because the opening process died. - Never resolve a replay conflict with raw git. Take the retry the conflict names:
syncwheel stack rebuild <id> --replay-mode desk, then resolve through the manifest withsyncwheel stack absorb <stack> [<path>...|--staged]orsyncwheel stack resolve-integration <stack> <resolved-commit>.... A manualgit merge/git commiton integration is invisible to Syncwheel: the next rebuild reconstructs the branch from the manifest's own commit projection and never consults that resolution, so the work is lost andreconcilekeeps refusing with the same conflict. - Before the first use of each mutating subcommand in a session, or after an
upgrade/error, run its exact nested
--help; never infer flags, close reasons, defaults, or force requirements from memory. Integration composition is declared and visible — inspect it withsyncwheel int showbefore testing there or blaming your own code. Add a stack withsyncwheel stack create <id> [<commit-or-range>...] [--draft]thensyncwheel int rebuild --reason "<why>"; close a normally merged stack with--reason merged, or a squash/rebase delivery with--reason absorbed, then validate and rebuild.absorbedverifies final delivered content and does not require--force. - Every mutating command carries
--reason; it is mandatory inai-managedrepositories and already enforced on several commands individually (int rebuildwhen ai-managed,hooks remove --disable,worktree release,coordination provenance reset). Pass it always. It lands in the ledger with the actor and the exact command:syncwheel ledger show.
When something looks wrong: do not force a push, do not hand-edit the manifest or the
coordination state, do not fall back to raw git. Re-observe and use the exact remedy the
failing command names. For a managed ref that disagrees with the coordination state, the
named repair classes are all syncwheel coordination repair (plan-first, then --apply --plan-file <plan>): default for a wrong recorded tip on an otherwise-correct ref (ref
repair), --freeze-backend tree-equivalent-state-cas for same-tree/different-shape,
--freeze-backend fast-forward-state-cas for an exact reviewed fast-forward,
--freeze-backend missing-ref-create-cas to atomically recreate an absent owned
ref at its recorded tip, and --freeze-backend state-digest-migration for a
pre-0.42.2 legacy-digest state.
A human should be able to write:
syncwheel this reporebuild integration and all PR branchesvalidate stack drift and tell me what is missingreconcile this shared integration branch with the manifestshow how channel test differs, plan it, and stop before applying
An AI agent should then:
- run
syncwheel reconcile - if the manifest is missing or stale, update it first with
initandstack create/stack set/stack add - run
reconcile --apply --worktree-root <path>only when the dry-run plan is understood - add
--pushonly when the shared remote branches should be updated - rerun
checkorreconcile - summarize what changed and what still needs a human
For an active-active version 2 or 3 manifest, insert syncwheel handoff before planning or publication. It is a read-only diagnostic of the
published state, ownership boundary, local locks, pending merge decision, and
eligible cleanup. Use publish rather than a raw Git push so all managed refs
and the coordination state receive one atomic, leased publication.
The lease authority for a managed source is its
refs/heads/syncwheel/claim/heads/... claim. A valid publish advances the claim
in the same atomic refspec; merely observing or leasing an unchanged ref is not
proof. Treat coordination.claims: advisory as a migration state, inspect
syncwheel coordination claims backfill, and never flip to required until
the dry-run reports zero unclaimed owned refs. Backfill with --apply --reason
never replaces a claim owned by another coordination domain.
Every coordinated remote mutation must have a fsynced caller intent and stable
operation token. On retry, exact token-bearing claim/state evidence is adopted;
do not repeat the push. Create propagates its stack_create_intent token into
the claim and removes only the deterministic temporary worktree owned by that
generation.
Take the clone's publication lock around the whole intent, push, local remainder and terminal event. The lock, not the intent payload, says what is in flight: a command that cannot take it exits 2 naming the live owner and touches nothing, and one that takes it knows no other cycle is running here, because process death releases the lock. The owner record in the intent names a dead owner in the abandonment event; it never authorizes a transition.
Landing has two proofs, and every caller declares which it accepts. The state
proof is a state commit inside the intent's recorded window carrying its token,
scope, changed refs, projection status and manifest digest, plus a claim
carrying that token for every touched source ref; it is the only form a fresh
publication may use to decide it needs no push. The claim proof belongs to the
recovery paths alone and only after the state proof failed: every changed ref,
and no other, carries a claim declaring this operation's token, scope and whole
ref set. It has no window, which is why it survives a state ref rewritten
backwards, and it excludes the manifest digest, which no claim records; a claim
written before 0.43 records no scope or ref set and therefore proves nothing.
Only the most recent 500 claims of a source ref are read. Neither proof reads
current ref tips: in an active-active fleet another clone may legitimately move
a published ref, push the same tip, or reuse a caller-supplied identifier. When
no evidence holds, record coordination_publish_abandoned with reason
not_landed or superseded and continue rather than freezing on it, and undo
the local rename only when the remote carries neither the promoted ref nor a
claim bearing that token.
A reconcile retry with a non-empty newly planned refset adopts an existing intent only when the whole changed-ref tuple matches. Otherwise the same authenticated landing resolver must settle it before a fresh intent can be created. A landed operation is never abandoned to make a different tuple publish; uncertain evidence stops the command. Empty-action retries retain their existing recovery contract.
The manifest digest that identifies an operation is computed over the public
manifest snapshot without integration.derived_provenance: the intent and the
published state resolve that projection from different sources by design, so it
cannot be part of an identity. An intent recorded before 0.43.0 carries the
older digest, is therefore never proved landed by the state proof, and is
terminalized under the same rule as any other intent whose evidence is absent.
Write exactly one terminal record per operation token, refusing a second when it
is written rather than when the ledger is read. A landed promotion still owes
only its manifest save; stack promote, stack push, int push,
stack create, stack close, stack rebuild, stack sync, stack add, and
reconcile --apply complete it from the published state and rebuild the
promoted branch instead of requiring a particular local branch layout, anchoring
a diverged draft branch under refs/syncwheel/recovery/drafts/ before dropping
it. When a different clone completed the same promotion without leaving a local
intent, stack promote may adopt it only from exact published stack, ref, and
tombstone evidence; adoption must not publish a new remote state. Never leave a
publish intent that no command can terminalize, and never let
a foreign pending intent be the reason an unrelated publication cannot run.
For draft close, the order is intent, remote tombstone claim plus state CAS,
local manifest save, terminal event. Do not reintroduce remote observations
around the filesystem save: they cannot make that boundary atomic. A retry may
complete only when the remote tombstone contains its exact operation token.
If the claim has advanced, record close_superseded and stop without applying
the old close to the current generation; decide that from the claim of that
generation, never from a state tip that unrelated publications also move.
Remote failure during either the first attempt or recovery must name the same
remote-first retry command.
For channels, inspect channel contract, channel list, channel show, and
channel diff first. Every mutation previews a channelPlan; repeat the same
command only with its exact --plan-digest ... --apply and optional stable
--operation-id. Use channel plan for materialization/publication. Publish
with channel publish; never replace its exact lease with a raw Git push. Under
active-active coordination the channel ref and state publish atomically. A
published branch is only a deployment input, not proof that an environment was
deployed.
The channel base is an exact pin. Treat baseDrifted as information, not an
instruction to follow the symbolic base; only an explicit channel refresh
advances it. Preserve every stack's exact depends_on closure and order. A
channel-local resolution snapshot is bound to forPinDigest; composition edits
invalidate it and promotion copies it with the source pins. Shared channels
refuse draft-stack publication, while ephemeral channels may carry drafts. An
active-active channel must use the coordination remote.
- do not author or commit in the shared primary checkout, even when it is clean;
open a governed lane first with
syncwheel worktree open <lane> --into <stack> - when the calling checkout is behind the stack, use
worktree open <lane> --into <stack> --base <stack-branch>; the explicit base must resolve to the stack's exact current projected tip, and the lane records that commit as its base - do not run a built-in mutation while the primary is dirty: it stops before side
effects and names
worktree openorstack capture-integrationfor the work - read-only diagnostics remain usable when the primary is dirty; treat their yellow dirty-primary warning as foreign work, not permission to repair it
- use
syncwheel stack capture-integration <stack> HEADonly for already committed primary work named by a refusal; do not create a new primary commit to use it - use
syncwheel hooks remove --disable --reason "..." --applyonly as a deliberate, visible clone-local recovery opt-out; re-enable the bundle afterwards - when a primary-checkout stop names a manifest-derived remedy, use the named
stack capture-integration <stack> HEADcommand for your committed primary work, orworktree open <lane> --into <stack>without changing work owned by another agent; capacity and expiry messages name the applicablestack addqueue command - treat a light lane as an operational boundary, not a sandbox: do not install dependencies there, and return to a clean checkout before stack ownership operations
- review governed-worktree warnings on normal commands; a missing lane with an expired lease or known dead local owner is recovered to a local recovery ref before reaping even if its registry path is outside the current root, while an existing dirty or unknown lane must remain visible until explicitly handled
- preview
worktree release <lane> --reason <why>before using--applyfor a dead or abandoned lane; apply records the reason in the ledger and refuses an existing dirty worktree with its recovery remedy - use
--dry-runwhen inspecting rebuild/push commands - prefer
reconcilefor the normal multi-device lifecycle; use raw Git only as inspection or fallback - for an active-active manifest, use
handoffbefore taking over from another device or agent; never bypass the coordinated publisher withgit push - when a publication or revision-provider check is blocked by another stack's
unpublished declaration, contact the owner it names and run
syncwheel handoff; the only command these refusals suggest issyncwheel stack set <id> --published, offered for a stale local commit change whose published commits are present locally (it prints the command that restores them) - if a coordinated publish reports a mergeable race, do not retry silently;
review the handoff and use
publish --accept-mergeonly for that explicit disjoint-stack decision - keep local worktrees that need investigation with
worktree lock <stack>;gc --applyreaps eligible expired governed lanes whether or not active-active coordination is enabled, removes only eligible local tombstoned artifacts, and never deletes a remote branch - if manifest and Git disagree, fix the manifest or call out the conflict explicitly
- do not claim a repo is aligned if integration and PR branches still disagree
- stop on a stale channel plan, replay conflict, unknown required observation, post-plan remote change, or lease loss; re-observe instead of forcing the ref
- after an uncertain ref or remote outcome, inspect
channel operation showand use digest-boundchannel operation reconcile; it only observes and appends a terminal receipt, never retries the mutation - treat
cancelledbefore the authoritative boundary as terminal; an interrupt at or after that boundary isunknownuntil reconciliation - close expired or obsolete channels explicitly; expiry is not automatic branch deletion
- fail closed when a source claim is absent in required mode, belongs to another domain, or fails to advance; use handoff or claims backfill, never a raw push
Before branch, push, PR, or recovery work, inspect the repo-local tracking policy:
syncwheel repo tracking statussyncwheel_tracking=git-tracked means .syncwheel/manifest.json is part of the
shared Git contract. syncwheel_tracking=local-only means Syncwheel metadata
must stay local through .git/info/exclude: without active-active coordination
no control commit is created on the integration branch, and no Syncwheel push
carries the manifest to a stack, pull-request or delivery branch. If the policy is missing,
ask for the intended mode and persist it with
syncwheel repo tracking set ... --apply before continuing. See manifest-tracking.md for the
full policy and migration behavior. An installable agent skill that encodes this
lives in skills/syncwheel/SKILL.md.
The manifest can also say how much of the delivery pipeline you may run without a human gate:
syncwheel repo authority statushuman-gated(or no block at all): ask before commit, push, PR, and merge, as the repository's own instructions require.ai-managed: the repository's configured Syncwheel pipeline runs unattended. The manifest defines the pipeline (stacks, PRs, landing, channels, or journal publish);authorityonly says whether you may run it without a human at each stage. Still stop for anything outside the named scope, for external side effects, and for anything destructive.source_changecovers source delivery up to and including merge orjournal publish.runtime_changecovers the rollout that pipeline leads to (deployment channels, install, service restarts) and must be listed on its own; otherwise rollout remains gated.destructive_rewriteis never allowed, whatever the block says: force pushes, history rewrites, and deleting work always need a human.
Only a maintainer sets this policy (syncwheel repo authority set ... --apply).
Never set it, widen it, or infer it for yourself.
An ai-managed delivery repository may opt into the deterministic GitHub PR
merge path with a private clone-local policy. Configure it with
repo pr-merge-policy set github, inspect the dry-run, and apply only after
reviewing the exact local diff. Then use stack merge-pr <stack> to produce a
digest-bound JSON plan. See github-pr-merge.md for the
full contract.
The policy is fail-closed by default: it requires an allowlisted actor with
repository admin permission, an allowlisted PR/commit/source repository, an
exact stack head, green CI, no unresolved review work, and recognized GitHub
rules. Repositories without CI may explicitly set clone-local checks: "none";
the plan reports that opt-out as a warning and still honors required GitHub
checks. The
admin bypass is limited to required reviews and always includes
--match-head-commit; no branch deletion is performed.
When Agentwheel is available, install the Syncwheel agent skill into the local Codex runtime for the repo:
agentwheel install github:NestDevLab/syncwheel --adapter codex --local --target-root /path/to/repo --skill syncwheelsyncwheel self status reports a missing-skill recommendation when Agentwheel is
installed and the skill is absent. If the syncwheel executable is not on PATH
yet, use the checkout script directly:
python3 /path/to/syncwheel/scripts/syncwheel.py self status