Skip to content

Git-native claims

A work claim can be scoped to the git branch the work happens on. The hub stays git-agnostic — it never runs git and never reads a filesystem — so all git resolution happens client-side and the branch travels as opaque metadata on an ordinary claim. The hub stores it, replays it from the durable log on restart, and shows it in the state view, but it never acts on it. That keeps the local-first, single-dependency guarantee intact while making claims branch-aware.

Claim on the current branch

synapse git-claim TASK-1 --paths src/auth.py --base main
# equivalent for structured argv builders:
synapse git-claim --task-id TASK-1 --paths src/auth.py --base main

The branch is resolved locally with git rev-parse --abbrev-ref HEAD and sent on an ordinary claim, carrying the branch, the base it will merge into, and the declared auto-release policy. The repository root is resolved too (with git rev-parse --show-toplevel followed by strict OS canonicalisation) and set as the claim's worktree. A git-claim is therefore isolated to its own repository: two repositories that declare identically named paths never contend, while two claims in the same repository still detect an overlap. synapse state then shows the branch alongside the claim:

Active claims (1):
  TASK-1 [claimed] owner=USER paths=src/auth.py checkpoint=- git=feature/x->main

Options

Option Meaning
--paths File-scope path the claim intends to touch (repeatable).
--module, --symbol, --api, --source, --test, --generated, --migration Resolve semantic selectors locally. Symbol and API selectors use a synthetic descendant scope; the other selectors and companion tests/generated outputs remain whole-file paths.
--diff-base, --diff-head, --diff-path Infer conservative symbol scopes from a tracked Git diff. --diff-head is optional; without it the base is compared with the working tree. Repeat --diff-path to limit the diff.
--semantic-evidence-json Write receipt-ready selector and diff evidence JSON under the git root, or to an absolute path.
--base The branch the work merges back into (default: main).
--auto-release-on The release trigger recorded on the claim: manual, commit, or merge (default merge).

Use normal repository-relative display paths such as src/auth.py or docs/cli.md for narrow claims. git-claim binds each display to a local, versioned identity before sending it: Git-index component spelling, the strictly resolved filesystem-relative path, Unicode NFC, the worktree's actual case policy, and a device/object key when the target already exists. Symlinks, junctions, hard links, and Windows 8.3 aliases therefore cannot create a second claim for the same object. The human-readable paths remain unchanged for state views and denial messages.

Device/object keys are local values, so the resolver also sends an opaque hashed host namespace and the canonical worktree root's object key. Object equality is trusted only when both values match; coincident inode numbers from different hosts cannot widen conflicts. Object equality is deliberately conflict-only: it can deny a competing hard-link claim, but cannot authorize an edit or release a lease after the filesystem object may have changed.

Identity derivation is fail closed. Absolute or parent-escaping paths, broken or unreadable aliases, aliases outside the worktree, ambiguous case-insensitive Git index entries, non-canonical display spelling, an unprovable case policy, and identity/display mismatches are refused before the claim is sent. Missing final components remain claimable after their nearest existing ancestor has resolved inside the worktree. Case is folded only when the detected filesystem is case-insensitive; Linux/ext4 claims remain case-sensitive.

Auto-release repeats the same local identity derivation. If that derivation fails, or a snapshot carries a present-invalid or scope-misaligned identity, the non-blocking hook returns success without releasing anything.

The hub validates and compares identity values but never reads the worktree. Identity-aware claims compare conservatively with a legacy peer by projecting that peer's display paths under the known filesystem policy. Two legacy peers retain their historical literal-path behavior, so upgrade all Git-aware claim producers to close alias gaps across an entire fleet.

First-party ordinary file claims participate in the same identity model. When synapse lock --paths ... or MCP synapse_claim(..., paths=[...]) runs inside a Git checkout, the client resolves the canonical worktree and aligned path identity before contacting the hub. An overlapping ordinary, MCP, or git-claim scope therefore contends in one physical checkout, while linked Git worktrees remain isolated. Outside Git, ordinary claims retain the historical shared worktree namespace. A keyless synapse lock remains a deliberately separate named mutex keyed by its task id and never probes Git.

The --auto-release-on value is the policy stored with the claim; a client-side git hook enacts it so a finished branch frees its claim without a manual step.

Use either the positional TASK-1 form or --task-id TASK-1, not both. The named form exists for scripts and agent adapters that assemble command arguments from structured fields. synapse git-release is reserved for the installed hooks and auto-detects releasable claims from the git diff; for a manual release, use synapse release <task> --name <owner>.

Claim a semantic selector

Use the semantic flags when the work is naturally described by a module, symbol, API object, source path, owning test, generated artefact, or migration:

synapse git-claim TASK-RECEIPTS \
  --symbol synapse_channel.core.receipts.build_release_receipt \
  --semantic-evidence-json semantic-evidence.json

The command resolves the current git root first and runs the same deterministic resolver as python tools/semantic_claims.py. A symbol or API selector encodes the source and qualified symbol as a synthetic descendant such as src/pkg/worker.py/.synapse-symbol/Worker/run; likely owning tests and generated outputs remain ordinary whole-file companion paths. Module, source, test, generated, and migration selectors also remain whole-file paths. All derived paths are merged with explicit --paths. Selector text and evidence stay local unless you choose --semantic-evidence-json and later attach that JSON to a release receipt.

Claim a Git diff at symbol scope

Install the local parser bindings, then compare a base revision with the working tree:

pip install 'synapse-channel[semantic]'
python tools/semantic_diff_claims.py --base main --claim-args
synapse git-claim TASK-WORKER \
  --diff-base main \
  --diff-path src/pkg/worker.py \
  --semantic-evidence-json semantic-evidence.json

Use --diff-head HEAD for a committed comparison. The standalone tool accepts the equivalent --head and repeatable --path flags. Python/PYI, JavaScript/JSX, TypeScript/TSX, Rust, and Go are supported by locally installed upstream grammar wheels. Parser imports are lazy, and neither command downloads a grammar or contacts a service at runtime.

For an ordinary modified file, zero-context hunks are mapped on both the old and new source side. Every changed line must fall inside a named declaration. The smallest enclosing declaration becomes the claim path; renaming a declaration claims both old and new names. The existing path ancestry rule then provides the enforcement:

  • two different function descendants in one source file can coexist in hub state and can be edited safely from isolated worktrees;
  • a class scope conflicts with its methods;
  • a whole-file or parent-directory claim conflicts with every symbol below it.

Provider guards do not pretend a shared physical file is isolated. A precise edit tool may start from one semantic claim only when every semantic claim for that source in the exact worktree and branch belongs to the same editable owner. A sibling claim held by another owner in the same shared worktree makes the pre-edit decision ambiguous and is denied. Whole-file writers and patch tools always require a literal whole-file or parent claim. The staged index check is the authoritative post-edit proof described below.

Claim the smallest coherent ownership boundary, but do not under-claim a tool's actual mutation surface: whole-file writers require the file or an ancestor, while a verified precise edit may use the exact semantic declaration scope.

Incomplete evidence always widens. Additions, deletions, file renames or copies, mode-only changes, unsupported languages, syntax-error trees, oversized or non-regular sources, module-level changed lines, and unsafe declaration names produce a whole-file claim. Owning tests and generated outputs also remain whole-file companions. This may block more work, but it cannot silently omit a known scope or miss a real conflict.

Semantic Git reads are local and fail closed. Each command has a ten-second deadline, retains at most eight MiB of stdout and 64 KiB of stderr, runs without a pager, prompt, global/system configuration, external diff, or textconv, and explicitly disables any repository-configured filesystem monitor. It uses an isolated child process with terminate/kill escalation. Git blob and working-tree source reads stop after the two-MiB semantic source ceiling. Diagnostics are reduced to one bounded printable line. A pipe-reader failure or unavailable file enumeration denies the operation; if a known file's patch or blob cannot be obtained safely, that file widens to a whole-file claim.

The hub receives only canonical path strings; .synapse-symbol is a reserved coordination segment, not a filesystem lookup or a new wire field. Evidence JSON records each narrowing or widening decision, but tree-sitter output is planning evidence, not proof that a change is correct or complete.

Filesystem-canonical identity resolves a semantic descendant through its physical source before rebuilding the synthetic suffix. Existing sources carry their host-namespaced object key plus an object-relative semantic sub-scope. A whole-file alias therefore conflicts with every declaration on that object, declaration ancestry still contends across aliases, and sibling declarations remain separate even when a hard link is created after the first claim. Object comparison is conflict material only and never becomes a repository path or edit capability.

Auto-release on commit or merge

Install the git hooks once per repository so finished work frees its claims with no manual step:

synapse git-hook install

This writes a post-commit and a post-merge hook that call synapse git-release. After each commit or merge, git-release resolves the changed files locally (git diff-tree for a commit, git diff ORIG_HEAD HEAD for a merge), then filters claims by exact owner, canonical worktree, attached branch, and --auto-release-on trigger. Whole-file claims release from the physical change. Semantic claims release only when the committed HEAD^..HEAD (or ORIG_HEAD..HEAD merge) diff proves a matching .synapse-symbol path. Ambiguous, unsupported, unreadable, or parser-unavailable semantic evidence retains the symbol claim for explicit manual release instead of releasing it from a mere physical-file match. Add --name (and --token-file for a secured hub) to match the identity your agent claims under; a pre-existing hook from anything else is left untouched.

The hub never parses Git or source: it receives only an ordinary release. The hook never blocks a commit — an unreachable hub or no matching claim is simply a no-op.

Block commits whose staged paths are not claimed

Run the read-only staged gate directly:

synapse git-init --name project/agent
synapse git-claim-check --staged

The checker reads the authoritative index with git diff --cached --name-status -z --find-renames --find-copies; it does not trust filenames supplied by a hook. Adds, modifications, deletions, type changes, and unmerged paths are checked. Copies and renames check both the old and new names. Malformed, absolute, parent-escaping, or truncated records fail closed.

When at least one active claim in the exact worktree and branch uses a .synapse-symbol path for a staged source, the checker also resolves HEAD versus the index with the optional local semantic parser. An ordinary modification is projected to the exact named declarations touched on both old and new sides. The projected symbol paths — not the physical source name — are then matched against claims. A change to an unclaimed sibling symbol is denied. Any incomplete mapping widens to the physical source, so a symbol-only claim cannot authorise module-level edits, additions, deletions, renames, unsupported languages, invalid syntax, or ambiguous hunks. Parser/import/Git failures deny; they never downgrade a semantic claim check to a permissive file match.

An empty staged index returns success without resolving identity, reading a token, or connecting to a hub. For a non-empty index, every covering claim must:

  • match the canonical repository root and current non-detached branch;
  • belong to one exact identity and be in claimed or working state; and
  • cover every projected staged path, either by exact semantic or literal scope, by directory ancestry, or through the existing empty-path whole-worktree meaning.
  • cover every staged path by its display-bound canonical Git identity, directory ancestry, or the empty-path whole-worktree meaning. Filesystem aliases and object ids remain conflict-denial evidence, never authorization.

A PROJECT:git serialization lock cannot satisfy this check: it has no canonical worktree, branch, and path ownership. The checker never acquires, widens, renews, or releases a claim. Its bounded denial lists all ordinary uncovered paths so the operator can acquire the exact claim and retry.

Identity resolution is deliberately strict. Populated sources must agree in this order: explicit --name, worktree-scoped synapse.identity, then an agreeing SYN_PROJECT plus SYN_IDENTITY pair. A bare ambient identity, placeholder such as USER, disagreement, or detached HEAD is refused. git-init enables Git's official extensions.worktreeConfig support and persists synapse.identity and synapse.uri in the current worktree's config; --token-file persists only its canonical path as synapse.tokenFile, never token content. Legacy repository-local values remain readable until git-init is rerun, then they are removed so another linked worktree fails closed instead of inheriting the wrong seat.

Run synapse git-init --name <exact-seat> once inside every linked worktree. Git requires core.worktree and core.bare=true to be moved out of the shared config before enabling per-worktree config; Synapse detects that uncommon layout and refuses with the upstream migration instruction instead of guessing. The staged gate is worktree-safe after this migration. The auto-release hook scripts are repository-wide — git worktrees share one hooks directory — but they now pass git-release --resolve-identity, so at commit time each reads the synapse.identity / synapse.uri / synapse.tokenFile recorded for the worktree that produced the commit and releases that seat's claims. Mixed-identity linked worktrees therefore auto-release correctly; the identity baked at install time is only the fallback for a worktree with no recorded identity, and --auto-release-on manual remains available whenever explicit release is preferred.

This repository dogfoods the gate through the pre-commit framework:

- id: staged-claim-coverage
  name: every staged path has an owned Synapse claim
  entry: python -m synapse_channel.cli git-claim-check --staged
  language: system
  stages: [pre-commit]
  always_run: true
  pass_filenames: false

git-init does not splice or overwrite a pre-commit hook. It installs only the non-blocking post-commit and post-merge auto-release hooks and writes the local guide/config. Repositories using pre-commit can add the stanza above; repositories with another hook manager must compose the standalone command themselves.

Because a missing hook — or one whose baked-in synapse path has since moved — fails silently at commit time, confirm the setup with:

synapse git-hook test

It reports whether each post-commit/post-merge hook is installed and whether the executable it invokes still resolves, exiting non-zero on any gap. Gate your deployment on it: this project's own CI installs the hooks in a scratch repo and runs git-hook test on every push, so a regression in the install-or-resolve path is caught before release. (synapse git-init installs the hooks and writes the conventions guide in one step.)

Predict merge conflicts

See a collision before it happens:

synapse conflicts
synapse conflicts --check-diff

synapse conflicts reads the hub's live claims and flags every pair held on different branches with the same merge base whose declared paths overlap — two agents about to edit the same files on branches that will merge into one target. Claims with different bases are ignored because their branch-integration risk is not the same merge point. --check-diff refines the prediction against each branch's actual git diff base...branch, so only files both branches have really changed are reported. A directory-scoped claim such as --paths src matches changed files below that directory, and a whole-worktree claim is refined to the common changed files when both branch diffs are available. A branch that is not checked out locally is kept as a conservative warning rather than dropped.

Predicted conflicts (1):
  A@feature/x vs B@feature/y (both -> main): src/auth.py

synapse conflicts exits 0 when nothing is predicted, 2 when a conflict is, and 1 if the hub is unreachable — so a gate like synapse conflicts && git merge feature/x proceeds only on a clean, successfully checked result.

The prediction is computed entirely on the client from the ordinary state snapshot; the hub runs no git.

For semantic merge-risk beyond direct path overlap, run the import graph merge-risk radar against changed files or a branch diff:

python tools/import_merge_risk.py --changed src/auth.py --claimed src/session.py --check
python tools/import_merge_risk.py --base main --head HEAD --claims-json claims.json --json

The radar combines package-local Python import edges, CODEOWNERS, and mapped test owners with the changed and claimed paths. It is advisory and client-side only; use it to decide whether to coordinate, expand tests, or include more evidence in the release receipt.

What stays out of the hub

A git-scoped claim is an ordinary claim with opaque git metadata plus an optional additive path_identity. The hub validates, persists, replays, and compares that identity but runs no git and reads no filesystem. Even symbol claims are ordinary canonical paths interpreted by the existing ancestry algebra; parsing, diff resolution, and filesystem identity derivation stay entirely client-side. Resist any temptation to move git execution into the hub: the git-agnostic hub is the whole local-first guarantee.