npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gitkraken/sync-tools

v0.5.0

Published

Runtime-agnostic branch sync orchestration: rebase or merge a branch onto its target, cascade-sync a stack of dependent branches, and undo.

Readme

@gitkraken/sync-tools

Runtime-agnostic orchestration for keeping git branches in sync: rebase or merge a branch onto its target, cascade-sync a whole tree (“stack”) of dependent branches, skip already-applied commits, and undo — all in an isolated worktree so the user's working tree is never disturbed. That isolation covers the working tree and the branch ref, not the repository: the scratch worktree shares the repo's .git, so hooks run exactly as they would for a hand-run rebase (see Isolation).

The package is ports & adapters: it never talks to git directly. The consumer injects a git port (and optionally a conflict handler and an isolation provider); the library builds every operation on top.

Install

pnpm add @gitkraken/sync-tools

Requires git >= 2.31 (--end-of-options, and rev-parse --path-format=absolute for the default worktree isolation).

The git port

Everything runs through a SyncGitPort. A CLI consumer implements only exec:

import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import type { SyncGitPort } from '@gitkraken/sync-tools';

const execFileAsync = promisify(execFile);

const cwd = '/path/to/repo';

const git: SyncGitPort = {
  async exec(args, opts) {
    // Forward every option git is given: `signal` (cancellation), `stdin`, and `env`.
    // `env` matters — the library sets e.g. GIT_EDITOR so `rebase --continue` / `commit`
    // never block on an editor. Merge it over the ambient environment, don't replace it.
    const { stdout } = await execFileAsync('git', args, {
      cwd,
      signal: opts?.signal,
      env: { ...process.env, ...opts?.env },
    });
    return stdout;
  },
};

A libgit2 consumer (e.g. kepler) can implement the high-level ops (rebase, revParse, mergeHead, …) instead; a hybrid provides exec plus targeted overrides. Internal call sites prefer a provided op, fall back to exec, and raise SyncGitPortMissingOpError when neither is available.

Where exec is not optional

SyncGitOps only models the plumbing that has an op. Some entry points reach past it — stack metadata is a hand-built object graph (hash-object / mktree / commit-tree / cat-file), the undo store and the default worktree isolation are the same — so they need exec however complete the ops are:

| Entry point | exec | | --- | --- | | createStack, loadStackMeta | required — reading and writing the metadata blob is git plumbing | | syncStack | required — a cascade persists undo manifests | | undoSync, undoSession | required — same store | | syncWorktreePath, worktreeIsolationProvider, DisposableWorktree | required — but not in the type: these are reached through IsolationProvider, whose signature cannot carry the guarantee | | syncBranch | required unless an isolate provider is passed — and always required by skipAlreadyApplied other than 'off' | | takeOursHandler, takeTheirsHandler | required on ConflictContext.git | | createGitPortConformanceSuite | required — the cases build their fixtures with plumbing | | deleteStack, listStacks, getBranchState, getEffectiveParent, resumeOperation | not required — ops alone are enough |

So an ops-only port covers single-branch sync (with its own isolate provider and conflict handler), resume, deregistration, and inspection; it cannot register stacks, read their metadata, cascade-sync, or undo. The entry points that need exec unconditionally take ExecSyncGitPort — SyncGitPort with exec required — so the compiler rejects an ops-only port at those call sites. The two conditional rows are still prose: syncBranch depends on the options it is given, and ConflictContext.git is built by whichever isolation provider is active. A missing capability surfaces at runtime as SyncGitPortMissingOpError, including for skipAlreadyApplied: detection is best-effort about failing, but a port that cannot run it at all raises rather than quietly replaying every commit.

Quickstart — sync one branch

import { syncBranch, takeTheirsHandler } from '@gitkraken/sync-tools';

// Rebase `feature/api` onto `main` (default strategy), resolving conflicts via the handler.
const result = await syncBranch(git, 'feature/api', 'main', {
  conflictHandler: takeTheirsHandler, // omit to abort on the first conflict
  skipAlreadyApplied: 'squash', // drop commits already squash-merged into main
});
// result: { branch, status: 'synced' | 'up-to-date' | 'failed', previousSha?, newSha?, publishedTo?, reason? }

// Or merge `main` INTO the branch (a merge commit, no history rewrite, no force-push):
await syncBranch(git, 'feature/api', 'main', { conflictHandler: takeTheirsHandler, strategy: 'merge' });

syncBranch works on any branch — no registration required. The rebase/merge runs in an isolated detached worktree and the branch ref is moved only on success, so it works even when the user is sitting on the branch being synced: their uncommitted changes are preserved, and their index and working tree are brought along to follow the moved ref. If a local change would have to be overwritten to do that, the branch is rewound and SyncError('CHECKOUT_REFRESH_BLOCKED') is thrown — the sync never leaves an advanced branch behind a stale checkout.

It writes no undo manifest, though: BranchSyncResult.undoManifestId is only ever set by syncStack, so a direct syncBranch call gets undoManifestId: undefined even when it rewrote the branch. Keep the previousSha it returns if you want to be able to rewind.

Inspect a branch relative to its parent:

import { getBranchState } from '@gitkraken/sync-tools';

const state = await getBranchState(git, 'feature/api', 'main');
// state.state: 'up-to-date' | 'stale' | 'merged' | 'orphaned'

Stacks — register and cascade-sync a tree of branches

A stack is a tree (“forest”) of dependent branches: each branch records its parent. Register it once, then sync the whole thing in topological order (parents before children).

import { createStack, syncStack, undoSession } from '@gitkraken/sync-tools';

await createStack(git, {
  name: 'my-feature',
  branches: [
    { name: 'feature/api',    parent: 'main' },
    { name: 'feature/api-v2', parent: 'feature/api' },
  ],
});

const sync = await syncStack(git, {
  root: 'my-feature',
  conflictHandler: takeTheirsHandler,
  mode: 'parallel',        // independent subtrees concurrently (default 'sequential')
  concurrency: 4,          // cap for 'parallel'
  skipAlreadyApplied: 'squash',
});
// sync: { status: 'completed' | 'partial' | 'cancelled', results: BranchSyncResult[], undoSessionId }

// Undo the entire cascade in one call:
if (sync.undoSessionId) await undoSession(git, sync.undoSessionId);

// Or one branch of it — a per-branch result carries a manifest id, not a session id:
const branch = sync.results.find((r) => r.branch === 'feature/api');
if (branch?.undoManifestId) await undoSync(git, branch.undoManifestId);

Failure semantics: a failed branch aborts itself and skips its subtree (recorded as skipped with a reason); independent subtrees keep going. There is no global rollback — already-synced branches are kept. A signal abort stops gracefully: in-flight branches are aborted, completed branches kept, status cancelled.

You don't have to persist a stack — pass an in-memory forest instead, and/or limit the cascade to a subtree:

// Ad-hoc forest, no refs/gitkraken/stacks/* entity written:
await syncStack(git, { meta: { version: 1, name: 'adhoc', baseBranch: 'main', branches: [...] } });

// Cascade only the subtree rooted at a branch (inclusive):
await syncStack(git, { root: 'my-feature', from: 'feature/api' });

Conflict handling

When a sync hits a conflict, the library calls the consumer's ConflictHandler with the unmerged files and lets it decide. The outcome is binary — the consumer owns everything inside (AI resolution, previews, retries, user prompts):

import type { ConflictHandler } from '@gitkraken/sync-tools';

const handler: ConflictHandler = {
  async handleConflicts(ctx) {
    // ctx: { git, worktreePath?, files, operation: 'rebase'|'merge'|'cherry-pick',
    //        sourceSha, targetSha, refs?, currentCommit?, sourceBranch, targetBranch, ... }
    // resolve files in ctx.git (scoped to wherever the conflict was materialised), then stage them
    return { resolved: true, report: { file: 'a.txt' } }; // or { resolved: false, reason: '...' } to abort this branch
  },
};

ctx.git carries only what the active isolation provider exposes — exec alone under the default detached worktree, possibly high-level ops and no exec under an in-memory provider. ctx.worktreePath is the absolute path the conflict is materialised at: the isolated worktree under syncBranch, the consumer's own checkout under resumeOperation. It is absent when the conflict has no filesystem location (an in-memory isolation provider) or when the port cannot report one, so a handler that reads files off disk should check it rather than assume.

A handler may attach an arbitrary report to its outcome — resolved or not — and sync-tools carries every one it receives, in call order, on the operation's result as conflictReports: BranchSyncResult (per branch, including under a syncStack cascade) and ResumeResult. The value is opaque to sync-tools — never inspected, cloned, or interpreted — so a handler that used to stash reports in its own closure and read them back after the call can return them here instead. conflictReports is absent when no outcome supplied a report.

sourceSha / targetSha are named after the sync, not after git: which one is git's --ours depends on the operation. A rebase replays the source onto the target, so the target is --ours; a merge runs on the source's HEAD, so the source is --ours. They are the branch tips as they stood before the operation started, and stay fixed for every conflict the sync hits.

refs and currentCommit describe the stop itself instead: refs.ours is the stop's HEAD, refs.theirs is the operation's opposing commit, and refs.base is their merge-base when it could be read. Both are recomputed before every handleConflicts call, including a stop reached after continue — so, unlike sourceSha/targetSha, they track a rebase or cherry-pick as it moves from one replayed commit to the next. currentCommit names the commit being replayed at this stop (sha, plus message when the port can read it); a merge has no such commit, so currentCommit is absent there. Any field this call could not resolve — an unreadable ref, no merge-base, no way to read a commit message — is simply omitted rather than causing a throw. If you pass sync-tools' ConflictHandler on to @gitkraken/conflict's resolver, ctx.refs is exactly the shape ResolutionContext.refs expects. For a rebase, feed @gitkraken/conflict's reference-integrity detection ResolutionContext.detectionRefs = { ours: targetSha, theirs: sourceSha } — the stable pre-rebase tips — rather than ctx.refs: ctx.refs moves stop to stop as the rebase replays each commit, while detection needs the two endpoints the whole rebase is bridging. A merge has no such distinction, since its refs already are those endpoints.

Pass it as conflictHandler in the options bag — the same option name and the same abortOnConflict default on syncBranch, syncStack and resumeOperation.

Built-in handlers: abortOnConflict (default), takeOursHandler, takeTheirsHandler — the last two stage git's --ours / --theirs side respectively, so which branch they keep follows the rule above. A “review before apply” flow is just a handler that resolves, shows a preview, and waits for the user's decision before returning.

Isolation

By default each rebase/merge runs in a detached worktree the library creates and disposes (worktreeIsolationProvider, requires exec). A libgit2 consumer that rebases in-memory passes its own isolate provider so no worktree is created:

import { syncWorktreePath, worktreeIsolationProvider } from '@gitkraken/sync-tools';
// syncBranch(..., { isolate: myInMemoryProvider })   // default is worktreeIsolationProvider

// The path the default provider uses for a branch — same value on every call and across
// processes, for pointing at or reasoning about the directory a sync works in:
const path = await syncWorktreePath(git, 'feature/api');
// <git-common-dir>/sync-tools-worktrees/feature-api-<hash>

The scratch worktree is an ordinary git worktree in the same repository: it lives under --git-common-dir and shares .git, and therefore .git/hooks. Nothing passes --no-verify, so pre-rebase, post-rewrite, pre-commit and commit-msg fire during a sync just as they would for a hand-run rebase or merge. Isolation is about the working tree and the branch ref, not about suppressing repository-wide side effects.

The branch ref half of that is a guarantee, for the rebases this library runs itself on an exec-backed port: syncBranch and syncStack update no refs/heads/* ref other than the ones they publish, whatever rebase.updateRefs is set to in repository, global or system config. Each such rebase is run with rebase.updateRefs=false pinned on the git process — not a side effect suppressed out of caution, but because git's version of that setting collides with what a sync is doing. With it on, git force-updates every branch pointing into the replayed range, including the branch being synced, so syncBranch's compare-and-swap publish loses a race against git itself and syncStack ends up with branches moved outside its undo manifest. A consumer-supplied rebase op is called instead of that, not around it, so nothing is pinned on its behalf and it owes the guarantee itself: update no ref other than the one named by source. That is one of the obligations createGitPortConformanceSuite() checks. The exception is resumeOperation (below), which finishes a rebase some other tool began — git settled that rebase's ref updates when it wrote the todo list, and nothing the library pins at --continue time takes them back.

The scratch path is derived from the branch name alone, so it is the same on every run and in every process. A sync that was killed mid-run leaves that directory on disk and registered in git worktree list, and worktree add has no force level that overrides an existing directory — so the branch cannot be synced again until somebody clears it. The library names that case instead of passing git's fatal: '<path>' already exists through: the sync fails with SyncError('WORKTREE_PATH_OCCUPIED'), and the message carries the path.

Clearing it is your decision, not the library's — the same path is what a live sync of that branch in another process is using, and nothing here can tell an abandoned directory apart from a busy one:

import { listSyncWorktrees, removeSyncWorktree } from '@gitkraken/sync-tools';

// Pass the same options to both calls — e.g. { worktreeDir } if you configured one — so
// removeSyncWorktree judges against the same scratch base directory listSyncWorktrees did:
const opts = { worktreeDir };
const scratch = await listSyncWorktrees(git, opts);   // [{ path, branch, prunable }, ...]

// Once you have established that no other sync owns it:
await removeSyncWorktree(git, scratch[0].path, opts); // uncommitted work included

removeSyncWorktree refuses any path isSyncWorktreePath(git, path) does not recognise with SyncError('INVALID_OPTIONS') and removes nothing: only a direct child of the scratch base, spelled without any traversal segment along the way, is one of this package's, so the repository root, the base directory itself, a sibling of it and anything nested deeper are all rejected — it is a path-shape rule, not a registry lookup, so it also removes a worktree a consumer created themselves under a shared worktreeDir. removeSyncWorktree then goes further than the path-shape rule and resolves the path itself, refusing it when it does not really sit directly under the scratch base — so a symlink planted at the leaf, pointing somewhere else, is rejected rather than followed. isSyncWorktreePath stays a path-shape predicate only: it has to answer for a path nothing has been created at yet, so it cannot resolve anything, which makes removeSyncWorktree deliberately the stricter of the two. prunable is git's own verdict on a registration: usually it means the directory is already gone, which worktree add --force repairs in passing, but it also fires for a directory that merely lost its own .git marker while its content is intact — that one still blocks a sync and still surfaces as SyncError('WORKTREE_PATH_OCCUPIED').

Writing your own provider needs one tsconfig setting: IsolatedRebase extends the global AsyncDisposable, which requires TypeScript >= 5.2 and esnext.disposable in your own lib (this package compiles with "lib": ["ES2022", "ESNext.Disposable"]). Without it your provider fails with Cannot find name 'AsyncDisposable'.

Replaying a caller-chosen revision (source)

syncBranch(git, branch, onto, { source, isolate }) replays source instead of refs/heads/<branch>, while branch stays the human-facing label used for progress events, the default merge message, and scratch-worktree naming. This is for a review-before-apply flow that already has a revision to replay (say, a prior review result) and wants it to look like branch's tip without creating a throwaway branch for it.

source is resolved once, before the isolated worktree is created, with the same branch-first-then-tag policy as onto. It is only accepted when opts.isolate declares publishesToCustomTarget: true (for example, createWorktreeIsolationProvider({ publishTo: ... })), so source without a qualifying isolate raises SyncError('INVALID_OPTIONS') before any revision is resolved or the repository is touched. That declaration is a trust boundary, not a check syncBranch itself performs — it trusts the provider to actually keep refs/heads/<branch> out of the write path. createWorktreeIsolationProvider earns that trust by refusing, when invoked, a publishTo (fixed string or function form) that resolves to refs/heads/<branch> for the branch it was invoked with; a hand-written provider that declares the marker owes the same guarantee itself. Because refs/heads/<branch> is never read, branch need not exist as a ref at all. BranchSyncResult.previousSha is always the revision actually replayed — the resolved source when one was supplied, refs/heads/<branch>'s tip otherwise.

syncStack does not accept source; it always replays each branch's own tip so a cascade's next branch has something to rebase onto.

Review before apply

By default syncBranch publishes straight to refs/heads/<branch>. Pass createWorktreeIsolationProvider({ publishTo }) as isolate instead to park the result on a review ref and leave the branch untouched, so a reviewer can inspect the commit before it becomes the branch's history. The result reports exactly where it landed as publishedTo — a caller never has to re-evaluate publishTo itself to find the ref:

import {
  applyPublishedSync,
  createWorktreeIsolationProvider,
  discardPublishedSync,
  syncBranch,
  undoSync,
} from '@gitkraken/sync-tools';

const isolate = createWorktreeIsolationProvider({
  publishTo: (branch) => `refs/sync-tools/review/${branch}`,
});

const result = await syncBranch(git, 'feature/api', 'main', { isolate });
// result.status === 'synced': feature/api is still at result.previousSha, and
// result.publishedTo (refs/sync-tools/review/feature/api) now holds result.newSha.

if (result.status === 'synced' && result.publishedTo) {
  // Inspect result.publishedTo however you like — diff it against the branch, show it to a
  // reviewer — then apply it: fast-forwards feature/api to the reviewed commit, refreshing a
  // checkout that is on it, and records an undo manifest.
  const { undoManifestId } = await applyPublishedSync(git, result);

  // Changed your mind after applying? Undo the branch move (and any checkout it refreshed) like
  // any other sync:
  await undoSync(git, undoManifestId);

  // The review ref itself outlives both apply and undo — clean it up once you're done with it:
  await discardPublishedSync(git, result);
}

Both operations refuse a stale result: applyPublishedSync compare-and-swaps refs/heads/<branch> from result.previousSha to result.newSha only after confirming result.publishedTo still resolves to result.newSha, and discardPublishedSync deletes result.publishedTo only while it still does. Either a branch someone else moved — reported as SyncError('BRANCH_HAS_NEW_COMMITS'), the same code a plain sync's publish raises for the same race — or a review ref someone else changed or removed, fails the call instead of silently doing something the caller never asked for. That pre-check tells a genuinely absent ref (still SyncError('REVIEW_REF_STALE')) apart from one it could not read at all, which propagates as the underlying failure instead of being reported as staleness. Both operations also refuse a result whose publishedTo is refs/heads/<branch> itself, with SyncError('INVALID_OPTIONS'): that is the real branch, which an ordinary sync with no publishTo (or a provider that resolves back to it) reports the exact same way, so such a result was never published for review, and applying or discarding it would move or delete the branch's own history rather than a parked review commit — a publishTo naming a different branch's ref is unaffected and still accepted. applyPublishedSync requires exec (ExecSyncGitPort) — it persists the undo manifest and measures worktree cleanliness the same way syncStack does for a cascade — and checks for it before touching any ref, so a port missing it fails up front rather than after moving the branch. Neither operation deletes the review ref for you: discardPublishedSync is the only thing that does, so a reviewed-but-not-yet-decided result stays inspectable until you explicitly apply or discard it.

syncStack refuses an isolate configured with publishTo outright (see Isolation) — review-before-apply is a syncBranch flow.

Resume an in-progress operation

resumeOperation is the explicit exception to the isolation invariant above. syncBranch always runs in an isolated worktree and only moves the real branch ref on success; resumeOperation instead drives a rebase, merge, or cherry-pick that was already started outside the library — by plain git or an IDE, and currently paused at a conflict — to completion in place, on the real working tree and branch. There is no isolated worktree and no ref-move/publish step: git itself finalizes the operation (rebase --continue, commit --no-edit, or cherry-pick --continue) directly on the branch the consumer already has checked out.

Because of this, the consumer must already know an operation is in progress and tell the library which kind ('rebase' | 'merge' | 'cherry-pick'), plus any sourceSha/targetSha/sourceBranch/targetBranch the handler needs — the library does not detect or reconstruct these from git state. ConflictContext.refs/currentCommit are the exception: naming operation is enough for the library to read the matching pseudo-ref itself (REBASE_HEAD, MERGE_HEAD, or CHERRY_PICK_HEAD) and populate them the same way syncBranch does.

import { resumeOperation, takeTheirsHandler } from '@gitkraken/sync-tools';

// A rebase/merge/cherry-pick is already paused on a conflict in the real repo.
const result = await resumeOperation(git, { operation: 'rebase', conflictHandler: takeTheirsHandler });
// result: { status: 'completed' | 'failed', newSha?, reason? }

By default the same "never left mid-operation" guarantee applies: any failure — an unresolved conflict, a handler that leaves conflicts staged, or an unexpected error — aborts the in-progress operation before the result (or error) is returned, leaving a clean working tree.

Pass abortOnFailure: false to trade that guarantee for the operation itself: an unresolved conflict then returns { status: 'failed', reason } with the operation still in progress and the working tree untouched, so the consumer can inspect the conflict, finish it by hand, or abort on its own terms. Nothing else cleans up after it. The flag only covers conflict-resolution outcomes — a missing port op, an invalid option, or an error thrown by the handler still throws, because no ResumeResult can describe those honestly.

Files already staged when resumeOperation takes over are respected: an operation paused with no unmerged paths left (the user resolved the conflict and ran git add, but never --continue) is driven straight to completion, without consulting the handler.

Progress events

Pass onProgress to receive a SyncProgressEvent stream — operation-level (operation:started, operation:conflict-detected, operation:conflict-resolved, operation:conflict-failed, operation:commits-skipped, operation:completed, operation:aborted), cascade-level (sync:started, sync:branch-started, sync:branch-completed, sync:branch-up-to-date, sync:branch-skipped, sync:branch-failed, sync:completed), and stack lifecycle (stack:registered, stack:deregistered).

operation:started is closed by exactly one of operation:completed or operation:aborted, so a progress UI can pair them: a branch that turned out to need no replay completes, a refused conflict or an unexpected error aborts, and anything that rejects the sync before it starts (an unusable port, an invalid ref, a branch already up to date) emits neither event. The single exception is resumeOperation returning failed under abortOnFailure: false, where the operation is deliberately still in progress — nothing closes the stream because nothing closed the operation. A throw on that path still closes it.

Validating a custom port

import { createGitPortConformanceSuite } from '@gitkraken/sync-tools/testing';

for (const testCase of createGitPortConformanceSuite()) {
  // run testCase against your port in your own test runner; it throws on failure
}

Cases are added as guarantees are added, so a port that passed against an earlier version can fail against a later one. The current suite holds rebase to the ref guarantee described under Isolation — it configures rebase.updateRefs=true and fails an implementation that lets a branch pointing inside the replayed range move.

The suite lives on its own @gitkraken/sync-tools/testing entry point, so it stays out of the module graph of a production import. Each case needs a fresh repo with at least one commit and a working tree — the checkout, worktree and refreshCheckout cases operate on one — and the port under test must provide exec — the cases build their fixtures with plumbing the ops do not cover. An ops-only port therefore has to add exec for conformance purposes only, and the suite cannot tell you whether the rest of the library will accept the port without it (see Where exec is not optional).

Options reference

  • conflictHandler — conflict resolution; defaults to abortOnConflict on all three entry points.
  • strategy: 'rebase' | 'merge' — single-branch only; rebase (default) rewrites history, merge adds a merge commit (--no-ff).
  • mergeMessage — the merge commit's message (subject, plus a body after a blank line); defaults to Merge branch '<onto>' into <branch>. Pass it when branch is not the name the merge should record, such as a scratch ref the sync runs on. Requires strategy: 'merge' and must not be blank; either is SyncError('INVALID_OPTIONS') raised before any work starts.
  • skipAlreadyApplied: 'off' | 'squash' | 'squash+subject' — skip feature commits already applied to the target (rebase only).
  • mode: 'sequential' | 'parallel' + concurrency — cascade scheduling (syncStack). concurrency (default 4) requires mode: 'parallel' and an integer >= 1; anything else is SyncError('INVALID_OPTIONS') rather than a silent fallback.
  • from — limit a cascade to a subtree.
  • isolate — isolation mechanism (default: detached worktree).
  • source — syncBranch only: revision to replay instead of refs/heads/<branch>; requires isolate to declare publishesToCustomTarget: true. See Replaying a caller-chosen revision.
  • abortOnFailure — resumeOperation only; false leaves a failed operation in progress instead of aborting it.
  • previousResolutions — per-file resolutions the caller carries in and hands to its own handler; sync-tools passes them through untouched.
  • sessionId, signal, onProgress.