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 outcomes and recovery

A missing reply does not prove denial: the hub may have durably granted the lease before the reply was lost or delayed. git-claim waits up to 30 seconds per send/reply exchange by default, then requests one correlated state snapshot before deciding the outcome. --reply-timeout accepts a finite positive duration up to 300 seconds. Confirmation has its own equally bounded exchange; this is not an overall command deadline. Local Git/semantic resolution happens first, and connection readiness has a separate five-second wait plus a short post-welcome refusal check.

Exit Meaning Permission to edit
0 Exact grant or confirmed live lease Only within the verified scope and lease
1 Explicit claim denial or local/connection failure None
2 Invalid command/deadline None
3 Claim outcome unknown; no confirmed live lease None

For exit 3, repeat the original identity, task, resolved paths, base and release policy with --confirm-only:

synapse git-claim TASK --name=PROJECT/seat --paths=src/owned.py \
  --base=main --auto-release-on=commit --reply-timeout=30 --confirm-only

Confirmation sends a read-only state request; it never issues another claim, renews a lease, advances its epoch, releases it, or bypasses staged claim checks. It requires exactly one active claim with the same owner, worktree, ordered canonical paths, path identities and Git context, a positive integer fencing epoch, a nonterminal status and a finite unexpired lease. The snapshot must echo a fresh opaque request id and report a valid generation timestamp. The verified epoch is persisted and read back when persistence is enabled, for a later release/checkpoint/hook process. Unavailable fence storage leaves recovery unknown. Missing, mismatched, expired, ambiguous or unavailable state remains unknown.

MCP synapse_git_claim exposes equivalent reply_timeout (default 30.0) and confirm_only (default false) arguments. Its text distinguishes claim granted, claim confirmed, claim denied and claim outcome unknown. Other ordinary MCP queries keep the bridge request timeout. A dashboard operator must preserve these distinctions and verify a live lease before enabling scoped work; a viewer state display alone does not grant authority.

Compatibility: exit 3 replaces the former exit 1 for unresolved outcomes. Automations must handle it without editing or blindly replaying a claim. The native attempts/poll_interval arguments remain a deadline override for existing callers; they no longer cause polling. Snapshot confirmation requires a hub that echoes the optional request_id on state_snapshot. An older hub can still grant normally but cannot confirm an uncertain outcome, which stays unknown. No wire version bump is needed for the optional correlation field.

Natural-language claim drafts

synapse claim-parse asks an explicitly selected provider for a draft:

synapse claim-parse --provider=ollama-api --model=your-model \
  --name=PROJECT/seat --from-text='Refactor src/auth.py' --json

This experimental command prints submitted: false and an argument array for the normal git-claim command. It never calls the hub or executes that array. Without --json, it prints a POSIX-shell-quoted command for review. Use --endpoint only with ollama-api to select an explicit generate URL. --uri selects the hub in the proposed command, not a connection by the drafter.

Provider and owner identity are required; no paid provider is selected by default. The selected provider retains its configured credentials, tools, filesystem and network permissions. This adapter is not a provider sandbox. Prompt framing is not a prompt-injection defence; run only an appropriately restricted provider. No repository files are automatically included in the request by this adapter.

Requests are limited to 8192 characters and parsed answers to 32768. Answers must be one JSON object with paths, task, optional base (default main) and optional scope_note. Duplicate and unknown keys, invalid types and prose-wrapped JSON are rejected. One to 64 paths use a conservative portable relative-path subset: no absolute/drive paths, backslashes, traversal segments, terminal controls, Windows reserved names or trailing dots/spaces. Legal but nonportable filenames, including POSIX newline names, require the ordinary manually reviewed claim flow. Base names must be literal branch names; the ambiguous @ alias is refused.

These are lexical checks, not evidence that paths exist, symlinks stay inside the repository, or the proposed scope matches your intent. Review those facts before running the printed command. Values bind to options with = so model text cannot become additional CLI options. No file existence or reservation guarantee is inferred from a successfully validated draft.

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, one path per flag). A comma-joined value such as a.py,b.py that names no existing path is refused: it would register one literal path that no staged file can match.
--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. MCP synapse_claim without paths covers the whole resolved worktree inside Git and is refused outside it. Its task_only=true form is the same task-keyed named mutex as a keyless synapse lock, with no file scope.

For lock, repeat --paths once per path, for example synapse lock --paths src/a.py --paths src/b.py build -- make. A comma-joined value that names no existing path is refused with exit code 2 before connecting or running the command. An existing path whose filename contains a comma remains a literal path; the CLI never splits it.

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, Go, Java, C#, and Ruby 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.

When wrapping a commit with synapse lock, use the same owner identity as the installed hooks. The lock disconnects after acquiring its durable task claim so the hook can connect under that identity, then reconnects to release the mutex after the command. The mutex remains subject to its claim TTL throughout.

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. The non-blocking git-release hook prints release requested on ... after its send attempt; exit 0 is not a confirmed hub release. For an operator-confirmed closeout, use the manual release and exact read-only recovery workflow.

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 --check-semantic

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.

With the optional semantic extra, --check-semantic compares the named declarations changed by each branch relative to its unique merge base. It pins commit identities before reading filenames and syntax, and preserves literal Git filenames. Enclosing declarations overlap their nested members, so a class edit cannot be declared independent of a method edit. Disjoint declarations can remove a predicted overlap; missing grammars, unavailable branches, ambiguous merge bases or incomplete syntax retain conservative warnings.

When both flags are set, the semantic pass performs its own file filtering. This is a committed-diff advisory view: it does not observe future edits or uncommitted work in other checkouts, reserve paths, establish behavioural independence, or replace a real merge and its tests.

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 or no state snapshot arrives within the polling budget. This applies to every mode, including the default without either flag. A received empty inventory is distinct from a missing response and can return 0. 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.