DEV Community

Михаил
Михаил

Posted on • Originally published at agentlabjournal.online

Coordinating Two Coding Agents with Git Refs and a CRDT

Coordinating Two Coding Agents with Git Refs and a CRDT — Agent Lab Journal

  Agent Lab Journal

    Guides
    Glossary
Enter fullscreen mode Exit fullscreen mode

Practice · Agent Engineering · Git

Coordinating Two Coding Agents with Git Refs and a CRDT

      Level: advanced
      Reading and laboratory time: 60 minutes
      Result: two independent agent changes preserved without merging their branches into the working branch



      Parallel coding agents do not mainly fail because Git cannot merge text. They fail because a branch is asked to represent three different things at once: private work, shared coordination state, and the accepted project state.
      In this laboratory, two coding agents begin from the same task, work without seeing each other, publish immutable operations through private Git refs, and recover after separate sessions. A small conflict-free replicated data type, or CRDT, combines their intent. The coordinator never merges either agent branch into the working branch, yet the final task contains both changes and can be regenerated from Git alone.
Enter fullscreen mode Exit fullscreen mode

Contents

  • What you will prove

  • Why branches alone are insufficient

  • Coordination model

  • Concrete case

  • Prerequisites and safety boundary

  • Step 1: create the repository

  • Step 2: implement the CRDT reducer

  • Step 3: publish two independent changes

  • Step 4: synchronize through Git refs

  • Step 5: materialize accepted state

  • Step 6: prove session recovery

  • Verification protocol

  • Connecting real coding agents

  • Failure cases

  • Limitations

  • Production hardening

What you will prove

      The experiment uses a task with two independently editable fields. Agent Alpha changes the task status from open to in_progress. Agent Beta adds a verification requirement. Each agent records its change as a separate immutable operation and advances only its own ref.



      A deterministic reducer reads both refs directly from the Git object database, validates their operations, and creates the same materialized task regardless of the order in which the refs are supplied. The accepted branch receives one generated file, not a merge of either agent's workspace.
Enter fullscreen mode Exit fullscreen mode

By the end, the laboratory must establish all of these properties:

  • the agents begin from the same base commit;

  • neither agent needs the other agent's checkout or conversation history;

  • each agent has a private publication ref;

  • the operation identifiers do not collide;

  • applying Alpha then Beta gives the same state as applying Beta then Alpha;

  • replaying the same operation does not apply it twice;

  • the final task contains both independent changes;

  • the working branch has no merge commit from either agent branch;

  • a new session can reconstruct the result from refs and committed objects alone.

What “without conflict” means here

        It does not mean that arbitrary concurrent source-code edits become semantically compatible. It means that agents publish structured intent outside the accepted working tree, and that the defined operation set has deterministic convergence rules. Human or automated review is still required before generated state becomes accepted project state.
Enter fullscreen mode Exit fullscreen mode

Why branches alone are insufficient

      A conventional branch records a succession of filesystem snapshots. That is useful for source code, but a branch does not explain whether two snapshots represent duplicate work, independent additions, or competing decisions. If two agents edit the same JSON task file, Git sees lines. The coordinator needs operations with identities, authors, logical ordering, and explicit semantics.



      The familiar “one branch per agent” design also leaves a coordination gap. An agent may commit correctly but disappear before opening a pull request. Another session may know the branch name but not the task assumptions. A third process may merge both branches and resolve a textual conflict while accidentally discarding one agent's intent.



      We separate three layers:
Enter fullscreen mode Exit fullscreen mode
  • Private execution state. Each agent may use its own directory, temporary files, prompts, and uncommitted experiments.

  • Published coordination state. Immutable operations are reachable through refs under refs/agents/.

  • Accepted project state. A coordinator materializes validated operations onto main without merging an agent branch.

      A Git commit becomes an immutable transport envelope. Its hash provides content addressing, its parent provides provenance, and a ref provides a movable pointer to the latest envelope published by an agent. Git is the transport and retention layer; the CRDT defines how concurrent intent converges.
    

Coordination model

Private refs

The laboratory reserves these names:

refs/heads/main
refs/agents/alpha/task-42
refs/agents/beta/task-42
Enter fullscreen mode Exit fullscreen mode
      Refs under refs/agents/ are ordinary Git refs, but they do not appear as local branches in normal git branch output. Agents update them with git update-ref. The coordinator reads their commit trees with git show and never checks them out over the working branch.
Enter fullscreen mode Exit fullscreen mode

Operation-set CRDT

      The shared data type is an add-only set of operations. Every operation has a globally unique identifier. Set union is commutative, associative, and idempotent, so duplicate delivery or a different discovery order does not change membership.



      The task itself has two field policies:
Enter fullscreen mode Exit fullscreen mode
  • status is a last-writer-wins register ordered by a logical tuple;

  • requirements is an add-only set keyed by stable requirement identifiers.

      The last-writer-wins register uses a Lamport clock plus the actor identifier as a deterministic tie-breaker. The clock establishes a reproducible order; it does not establish that the later decision is more correct.
    

Operation schema

{
  "op_id": "alpha:task-42:1",
  "actor": "alpha",
  "clock": 1,
  "task": "task-42",
  "kind": "set_status",
  "value": "in_progress"
}
Enter fullscreen mode Exit fullscreen mode
      Operation files live at ops/<actor>/<op-id>.json. One operation per file avoids concurrent append corruption and makes duplicate identifiers observable. Agents must never edit an operation after publication; a correction is a new operation.
Enter fullscreen mode Exit fullscreen mode

Why the reducer sorts input

      Set union determines which operations exist, but a materializer still needs stable output bytes. The reducer therefore deduplicates by op_id, rejects divergent payloads with the same identifier, and sorts operations before reduction. JSON keys and requirement lists are also serialized in a fixed order.
Enter fullscreen mode Exit fullscreen mode

Concrete case: two agents change one task

      Task task-42 begins as:
Enter fullscreen mode Exit fullscreen mode
{
  "id": "task-42",
  "title": "Prevent duplicate payment retries",
  "status": "open",
  "requirements": []
}
Enter fullscreen mode Exit fullscreen mode
      The agents receive the same base revision but different responsibilities:




          Actor
          Independent assignment
          Published operation




          Alpha
          Claim the task for implementation
          set_status("in_progress")


          Beta
          Add a verification condition
          add_requirement("retry-idempotency-test", ...)





      These operations touch different logical components. They can be composed without asking Git to merge two edited copies of tasks/task-42.json.
Enter fullscreen mode Exit fullscreen mode

Prerequisites and safety boundary

You need:

  • Git 2.30 or newer;

  • Python 3.9 or newer, using only the standard library;

  • a POSIX-compatible shell;

  • an empty disposable directory.

Check the tools:

git --version
python3 --version
Enter fullscreen mode Exit fullscreen mode
      Run the following laboratory in a new disposable directory, not inside an existing repository. The commands create a repository, commits, refs, and temporary work areas. Replace the example author identity if your environment already supplies one.



      In a production system, agents should not receive unrestricted access to every ref. Give each actor permission to update only its own namespace, and let a trusted coordinator own the accepted branch.
Enter fullscreen mode Exit fullscreen mode

Step 1: create the repository

Create and enter an empty directory:

mkdir agent-ref-crdt-lab
cd agent-ref-crdt-lab
git init -b main
git config user.name "Agent Lab"
git config user.email "lab@example.invalid"

mkdir -p base ops tools tasks
Enter fullscreen mode Exit fullscreen mode

Create base/task-42.json:

{
  "id": "task-42",
  "title": "Prevent duplicate payment retries",
  "status": "open",
  "requirements": []
}
Enter fullscreen mode Exit fullscreen mode

Create README.md:

# Agent ref and CRDT laboratory

Immutable agent operations are published under refs/agents/.
The accepted task in tasks/ is generated by tools/reduce.py.
Do not edit a published operation in place.
Enter fullscreen mode Exit fullscreen mode

Commit the common base:

git add README.md base/task-42.json
git commit -m "Initialize task coordination laboratory"
BASE_COMMIT=$(git rev-parse HEAD)
printf '%s\n' "$BASE_COMMIT"
Enter fullscreen mode Exit fullscreen mode
      Preserve the printed commit identifier in your terminal session. Both agent publications will use this exact commit as their parent, proving that neither was based on the other.
Enter fullscreen mode Exit fullscreen mode

Step 2: implement the CRDT reducer

      Create tools/reduce.py with the following code:
Enter fullscreen mode Exit fullscreen mode
#!/usr/bin/env python3
import argparse
import hashlib
import json
import subprocess
import sys
from pathlib import Path

ALLOWED_STATUS = {"open", "in_progress", "blocked", "done"}
ALLOWED_KINDS = {"set_status", "add_requirement"}

def git(*args):
    completed = subprocess.run(
        ["git", *args],
        check=True,
        text=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
    return completed.stdout

def canonical(value):
    return json.dumps(
        value,
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
    ).encode("utf-8")

def validate(operation):
    required = {"op_id", "actor", "clock", "task", "kind", "value"}
    if set(operation) != required:
        raise ValueError(
            f"{operation.get('op_id', '<unknown>')}: invalid keys"
        )
    if not isinstance(operation["op_id"], str) or not operation["op_id"]:
        raise ValueError("op_id must be a non-empty string")
    if not isinstance(operation["actor"], str) or not operation["actor"]:
        raise ValueError(f"{operation['op_id']}: invalid actor")
    if not isinstance(operation["clock"], int) or operation["clock"] < 1:
        raise ValueError(f"{operation['op_id']}: invalid clock")
    if operation["task"] != "task-42":
        raise ValueError(f"{operation['op_id']}: unexpected task")
    if operation["kind"] not in ALLOWED_KINDS:
        raise ValueError(f"{operation['op_id']}: unsupported kind")

    value = operation["value"]
    if operation["kind"] == "set_status":
        if value not in ALLOWED_STATUS:
            raise ValueError(f"{operation['op_id']}: invalid status")
    else:
        if not isinstance(value, dict) or set(value) != {"id", "text"}:
            raise ValueError(f"{operation['op_id']}: invalid requirement")
        if not all(isinstance(value[key], str) and value[key]
                   for key in ("id", "text")):
            raise ValueError(f"{operation['op_id']}: empty requirement field")

def read_base():
    with Path("base/task-42.json").open(encoding="utf-8") as stream:
        return json.load(stream)

def operations_from_ref(ref):
    paths = git("ls-tree", "-r", "--name-only", ref, "--", "ops").splitlines()
    operations = []
    for path in paths:
        if not path.endswith(".json"):
            continue
        raw = git("show", f"{ref}:{path}")
        operation = json.loads(raw)
        validate(operation)
        operations.append(operation)
    return operations

def collect(refs):
    by_id = {}
    encoded_by_id = {}

    for ref in refs:
        for operation in operations_from_ref(ref):
            op_id = operation["op_id"]
            encoded = canonical(operation)
            if op_id in by_id and encoded_by_id[op_id] != encoded:
                raise ValueError(f"divergent payloads for {op_id}")
            by_id[op_id] = operation
            encoded_by_id[op_id] = encoded

    return sorted(
        by_id.values(),
        key=lambda item: (
            item["clock"],
            item["actor"],
            item["op_id"],
        ),
    )

def reduce_operations(base, operations):
    status = base["status"]
    status_version = (0, "base", "base:0")
    requirements = {
        item["id"]: item for item in base.get("requirements", [])
    }

    for operation in operations:
        version = (
            operation["clock"],
            operation["actor"],
            operation["op_id"],
        )
        if operation["kind"] == "set_status":
            if version > status_version:
                status = operation["value"]
                status_version = version
        elif operation["kind"] == "add_requirement":
            requirement = operation["value"]
            existing = requirements.get(requirement["id"])
            if existing is not None and existing != requirement:
                raise ValueError(
                    "divergent requirement payload for "
                    + requirement["id"]
                )
            requirements[requirement["id"]] = requirement

    result = dict(base)
    result["status"] = status
    result["requirements"] = [
        requirements[key] for key in sorted(requirements)
    ]
    return result

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--ref", action="append", dest="refs", required=True)
    parser.add_argument("--output")
    parser.add_argument("--print-digest", action="store_true")
    args = parser.parse_args()

    operations = collect(args.refs)
    state = reduce_operations(read_base(), operations)
    rendered = json.dumps(
        state,
        ensure_ascii=False,
        sort_keys=True,
        indent=2,
    ) + "\n"

    if args.output:
        output = Path(args.output)
        output.parent.mkdir(parents=True, exist_ok=True)
        output.write_text(rendered, encoding="utf-8")
    else:
        sys.stdout.write(rendered)

    if args.print_digest:
        print(
            hashlib.sha256(rendered.encode("utf-8")).hexdigest(),
            file=sys.stderr,
        )

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Commit the reducer before creating agent publications:

chmod +x tools/reduce.py
git add tools/reduce.py
git commit -m "Add deterministic task operation reducer"
BASE_COMMIT=$(git rev-parse HEAD)
Enter fullscreen mode Exit fullscreen mode
      Updating BASE_COMMIT here is important: both agent commits must contain the reducer's parent revision, even though their trees will add only their own operation.
Enter fullscreen mode Exit fullscreen mode

Step 3: publish two independent changes

      A Git worktree gives each agent a separate filesystem while sharing one object database. Worktrees are convenient for the laboratory, but they are not part of the convergence algorithm. Separate clones can publish the same refs to a shared bare repository.
Enter fullscreen mode Exit fullscreen mode

Agent Alpha

Create a detached worktree from the common base:

git worktree add --detach .agent-alpha "$BASE_COMMIT"
mkdir -p .agent-alpha/ops/alpha
Enter fullscreen mode Exit fullscreen mode

Create .agent-alpha/ops/alpha/alpha-task-42-1.json:

{
  "op_id": "alpha:task-42:1",
  "actor": "alpha",
  "clock": 1,
  "task": "task-42",
  "kind": "set_status",
  "value": "in_progress"
}
Enter fullscreen mode Exit fullscreen mode

Commit the operation and publish the private ref:

git -C .agent-alpha add ops/alpha/alpha-task-42-1.json
git -C .agent-alpha commit -m "Alpha claims task-42"
ALPHA_COMMIT=$(git -C .agent-alpha rev-parse HEAD)
git update-ref refs/agents/alpha/task-42 \
  "$ALPHA_COMMIT" \
  0000000000000000000000000000000000000000
Enter fullscreen mode Exit fullscreen mode
      The all-zero old value means “create only if absent.” This is a compare-and-swap guard: an unexpected existing ref causes the publication to fail instead of silently overwriting another session's pointer.
Enter fullscreen mode Exit fullscreen mode

Agent Beta

Create another detached worktree from the same base:

git worktree add --detach .agent-beta "$BASE_COMMIT"
mkdir -p .agent-beta/ops/beta
Enter fullscreen mode Exit fullscreen mode

Create .agent-beta/ops/beta/beta-task-42-1.json:

{
  "op_id": "beta:task-42:1",
  "actor": "beta",
  "clock": 1,
  "task": "task-42",
  "kind": "add_requirement",
  "value": {
    "id": "retry-idempotency-test",
    "text": "Verify that replaying the same payment retry key creates one charge."
  }
}
Enter fullscreen mode Exit fullscreen mode

Commit and publish Beta's ref:

git -C .agent-beta add ops/beta/beta-task-42-1.json
git -C .agent-beta commit -m "Beta adds retry verification requirement"
BETA_COMMIT=$(git -C .agent-beta rev-parse HEAD)
git update-ref refs/agents/beta/task-42 \
  "$BETA_COMMIT" \
  0000000000000000000000000000000000000000
Enter fullscreen mode Exit fullscreen mode

Confirm independence

test "$(git rev-parse refs/agents/alpha/task-42^)" = "$BASE_COMMIT"
test "$(git rev-parse refs/agents/beta/task-42^)" = "$BASE_COMMIT"

git merge-base --is-ancestor \
  refs/agents/alpha/task-42 \
  refs/agents/beta/task-42 && exit 1 || true

git merge-base --is-ancestor \
  refs/agents/beta/task-42 \
  refs/agents/alpha/task-42 && exit 1 || true
Enter fullscreen mode Exit fullscreen mode
      The first two assertions prove that both publications have the same parent. The next two require that neither agent commit is an ancestor of the other.
Enter fullscreen mode Exit fullscreen mode

Step 4: synchronize through Git refs

      List the coordination namespace without switching branches:
Enter fullscreen mode Exit fullscreen mode
git for-each-ref \
  --format='%(refname) %(objectname)' \
  refs/agents/
Enter fullscreen mode Exit fullscreen mode
      Inspect published operations directly from their commit trees:
Enter fullscreen mode Exit fullscreen mode
git show \
  refs/agents/alpha/task-42:ops/alpha/alpha-task-42-1.json

git show \
  refs/agents/beta/task-42:ops/beta/beta-task-42-1.json
Enter fullscreen mode Exit fullscreen mode
      At this point main has not moved. Confirm that the accepted working tree contains no agent operation:
Enter fullscreen mode Exit fullscreen mode
test ! -e ops/alpha/alpha-task-42-1.json
test ! -e ops/beta/beta-task-42-1.json
git status --short
Enter fullscreen mode Exit fullscreen mode

When agents use separate clones

      A shared repository must explicitly transfer the custom namespace because a default fetch usually follows only refs/heads/*. An agent may push its private ref with:
Enter fullscreen mode Exit fullscreen mode
git push origin \
  HEAD:refs/agents/alpha/task-42
Enter fullscreen mode Exit fullscreen mode

The coordinator can fetch all agent refs with:

git fetch origin \
  '+refs/agents/*:refs/agents/*'
Enter fullscreen mode Exit fullscreen mode
      The leading plus permits non-fast-forward replacement and is therefore too permissive for a hardened deployment. Prefer server-side policies that require fast-forward publication or immutable, generation-specific ref names.
Enter fullscreen mode Exit fullscreen mode

Step 5: materialize accepted state

Reduce Alpha then Beta

python3 tools/reduce.py \
  --ref refs/agents/alpha/task-42 \
  --ref refs/agents/beta/task-42 \
  --output /tmp/task-ab.json \
  --print-digest
Enter fullscreen mode Exit fullscreen mode

Reduce Beta then Alpha

python3 tools/reduce.py \
  --ref refs/agents/beta/task-42 \
  --ref refs/agents/alpha/task-42 \
  --output /tmp/task-ba.json \
  --print-digest

cmp /tmp/task-ab.json /tmp/task-ba.json
Enter fullscreen mode Exit fullscreen mode
      cmp must exit successfully. This checks byte-for-byte equality, not merely semantic similarity.
Enter fullscreen mode Exit fullscreen mode

Inspect the materialized task

python3 -m json.tool /tmp/task-ab.json
Enter fullscreen mode Exit fullscreen mode

The generated document should have this structure:

{
  "id": "task-42",
  "requirements": [
    {
      "id": "retry-idempotency-test",
      "text": "Verify that replaying the same payment retry key creates one charge."
    }
  ],
  "status": "in_progress",
  "title": "Prevent duplicate payment retries"
}
Enter fullscreen mode Exit fullscreen mode
      This is not a claimed benchmark result. It is the deterministic expected state implied by the two operation fixtures: Alpha supplies the winning status operation, while Beta contributes the only requirement.
Enter fullscreen mode Exit fullscreen mode

Commit only the generated state

mkdir -p tasks
cp /tmp/task-ab.json tasks/task-42.json
git add tasks/task-42.json
git commit -m "Materialize coordinated task-42 state"
Enter fullscreen mode Exit fullscreen mode
      This ordinary commit has one parent: the previous main commit. It does not merge either private agent ref. The accepted branch contains the result selected by the coordinator, while the refs preserve the provenance needed to regenerate it.
Enter fullscreen mode Exit fullscreen mode

Step 6: prove recovery across sessions

      Conversation transcripts and worktree directories are intentionally excluded from the recovery contract. Remove the disposable worktrees after checking that they are clean:
Enter fullscreen mode Exit fullscreen mode
git -C .agent-alpha status --porcelain
git -C .agent-beta status --porcelain
git worktree remove .agent-alpha
git worktree remove .agent-beta
Enter fullscreen mode Exit fullscreen mode
      Start a new shell in the repository. Do not restore ALPHA_COMMIT, BETA_COMMIT, or any agent prompt. Rebuild from durable state:
Enter fullscreen mode Exit fullscreen mode
git for-each-ref --format='%(refname)' refs/agents/

python3 tools/reduce.py \
  --ref refs/agents/alpha/task-42 \
  --ref refs/agents/beta/task-42 \
  --output /tmp/task-recovered.json

cmp tasks/task-42.json /tmp/task-recovered.json
Enter fullscreen mode Exit fullscreen mode
      A successful comparison proves that the materialized state does not depend on either temporary checkout or in-memory session context. Git retains the operations because the custom refs keep their commits reachable.
Enter fullscreen mode Exit fullscreen mode

Verification protocol

      Run the following checks individually. A silent command with exit status zero is a passing assertion.
Enter fullscreen mode Exit fullscreen mode

1. Both refs exist

git show-ref --verify --quiet refs/agents/alpha/task-42
git show-ref --verify --quiet refs/agents/beta/task-42
Enter fullscreen mode Exit fullscreen mode

2. Both operations are reachable

git cat-file -e \
  refs/agents/alpha/task-42:ops/alpha/alpha-task-42-1.json

git cat-file -e \
  refs/agents/beta/task-42:ops/beta/beta-task-42-1.json
Enter fullscreen mode Exit fullscreen mode

3. Delivery order does not matter

python3 tools/reduce.py \
  --ref refs/agents/alpha/task-42 \
  --ref refs/agents/beta/task-42 \
  --output /tmp/order-one.json

python3 tools/reduce.py \
  --ref refs/agents/beta/task-42 \
  --ref refs/agents/alpha/task-42 \
  --output /tmp/order-two.json

cmp /tmp/order-one.json /tmp/order-two.json
Enter fullscreen mode Exit fullscreen mode

4. Duplicate delivery is harmless

python3 tools/reduce.py \
  --ref refs/agents/alpha/task-42 \
  --ref refs/agents/alpha/task-42 \
  --ref refs/agents/beta/task-42 \
  --output /tmp/duplicate.json

cmp /tmp/order-one.json /tmp/duplicate.json
Enter fullscreen mode Exit fullscreen mode

5. Both logical changes survive

python3 - <<'PY'
import json
from pathlib import Path

task = json.loads(Path("tasks/task-42.json").read_text())
assert task["status"] == "in_progress"
assert {
    item["id"] for item in task["requirements"]
} == {"retry-idempotency-test"}
PY
Enter fullscreen mode Exit fullscreen mode

6. The working branch has no agent merge

test "$(git rev-list --parents -n 1 HEAD | wc -w)" -eq 2
test "$(git rev-list --merges "$BASE_COMMIT"..HEAD | wc -l)" -eq 0
Enter fullscreen mode Exit fullscreen mode
      A normal one-parent commit produces two words in the first command's output: the commit itself and its parent. This check assumes the laboratory history after BASE_COMMIT contains only the materialization commit.
Enter fullscreen mode Exit fullscreen mode

7. Committed state is reproducible

python3 tools/reduce.py \
  --ref refs/agents/alpha/task-42 \
  --ref refs/agents/beta/task-42 \
  --output /tmp/rebuilt.json

cmp tasks/task-42.json /tmp/rebuilt.json
git status --short
Enter fullscreen mode Exit fullscreen mode

Record evidence, not a prewritten conclusion

        Preserve the Git version, Python version, base commit, ref object identifiers, reducer commit, commands, exit statuses, and generated digest from your own run. Do not report the laboratory as passed until every assertion succeeds in your environment.
Enter fullscreen mode Exit fullscreen mode

Connecting real coding agents

      The shell commands above simulate agents deterministically so that the coordination mechanism can be tested without credentials or a model provider. When replacing the fixtures with real agents, constrain their output to the same operation schema.
Enter fullscreen mode Exit fullscreen mode

Agent input contract

Give each agent a session manifest such as:

{
  "actor": "alpha",
  "task": "task-42",
  "base_ref": "refs/heads/main",
  "publish_ref": "refs/agents/alpha/task-42",
  "allowed_operations": ["set_status"],
  "next_clock": 1
}
Enter fullscreen mode Exit fullscreen mode
      The manifest is durable coordination context, not a prose summary. The next session can validate that the current private ref still points to the expected commit before appending another operation.
Enter fullscreen mode Exit fullscreen mode

Agent output contract

Require the agent to return:

  • exactly one structured operation or an explicit abstention;

  • the base commit it inspected;

  • the actor and logical clock allocated by the controller;

  • the verification command it proposes for the resulting project change;

  • no direct mutation of another actor's namespace.

Controller responsibilities

      A trusted controller, rather than the model, should allocate operation identifiers, validate schemas, create commits, perform compare-and-swap ref updates, run the reducer, and enforce acceptance policy. This reduces the amount of repository authority placed inside the model's tool boundary.
Enter fullscreen mode Exit fullscreen mode

Preventing duplicate work

      CRDT convergence preserves concurrent changes; it does not stop two agents from implementing the same assignment. Add a claim operation keyed by a stable work-unit identifier:
Enter fullscreen mode Exit fullscreen mode
{
  "op_id": "alpha:claim:payment-retry-handler:1",
  "actor": "alpha",
  "clock": 1,
  "task": "task-42",
  "kind": "claim_work",
  "value": {
    "work_unit": "payment-retry-handler"
  }
}
Enter fullscreen mode Exit fullscreen mode
      The coordinator can reduce competing claims with a deterministic rule, but should expose the loser as a rejected claim before expensive execution begins. A deterministic winner is not the same as a fair scheduler. For costly jobs, reserve work centrally before agents start and use the CRDT log as the durable audit trail.
Enter fullscreen mode Exit fullscreen mode

Failure cases and how to diagnose them

The custom refs disappear after cloning

      Default clone and fetch settings normally transfer branch and tag namespaces, not arbitrary refs/agents/*. Add an explicit fetch refspec, or publish coordination refs under a namespace supported by your hosting policy.
Enter fullscreen mode Exit fullscreen mode

An agent overwrites its previous ref

      A blind git update-ref can replace a pointer after another session has advanced it. Always provide the expected old object identifier. If compare-and-swap fails, fetch the current ref, inspect its operations, allocate a new clock, create a descendant commit, and retry.
Enter fullscreen mode Exit fullscreen mode

Garbage collection removes a publication

      A commit reachable through a real ref is retained. A commit known only through a temporary variable or expired reflog may be collected. Publish the ref before deleting a worktree and verify it with git show-ref --verify.
Enter fullscreen mode Exit fullscreen mode

Two operations reuse one identifier

      The reducer accepts duplicate delivery only when the canonical payloads are identical. The same op_id with different content is corruption and must stop materialization. Generate identifiers from a controlled actor namespace and monotonic counter, or use collision-resistant random identifiers.
Enter fullscreen mode Exit fullscreen mode

Two agents add different text under one requirement ID

      The example deliberately rejects this case. Choosing one value with lexical ordering would converge, but it could hide a semantic disagreement. For requirements and security constraints, surfacing the conflict is safer than silently selecting text.
Enter fullscreen mode Exit fullscreen mode

Logical clocks go backward

      The reducer validates only that clocks are positive. A production publisher must remember the actor's highest committed clock and require the next operation to be greater. When one actor uses several devices, allocate clock ranges centrally or use a richer causal scheme such as a vector clock.
Enter fullscreen mode Exit fullscreen mode

The materialized file changes between runs

Check for nondeterministic inputs:

  • filesystem traversal without sorting;

  • JSON serialization using incidental dictionary order;

  • timestamps inserted during reduction;

  • locale-dependent comparison;

  • model calls inside the reducer;

  • different base task revisions.

      Reduction should be a pure, deterministic program over a pinned base and a validated operation set. Model judgment belongs before publication or after materialization, never inside the convergence function.
    

The Git merge is clean but the program is wrong

      Textual cleanliness says nothing about semantic compatibility. Two agents may change different files while violating the same invariant. Run project tests, static analysis, schema validation, and domain-specific checks after materialization. Treat CRDT convergence as a coordination guarantee, not a correctness proof.
Enter fullscreen mode Exit fullscreen mode

An operation passes schema validation but is unauthorized

      Schema validation proves shape, not permission. Verify that the publishing identity is allowed to perform the operation kind, task, and field transition. Signing a commit is useful only when the coordinator also validates the signature against an authorization policy.
Enter fullscreen mode Exit fullscreen mode

Limitations

      This laboratory intentionally uses a small operation vocabulary. Arbitrary source-code patches are not naturally CRDT operations. Concurrent edits to syntax trees can converge mechanically and still produce uncompilable or unsafe code.
Enter fullscreen mode Exit fullscreen mode
  • Add-only data grows forever. Production systems need snapshots, compaction, retention rules, and a way to prove which operations a snapshot includes.

  • Last-writer-wins can discard intent. It is suitable only where a deterministic winner is acceptable and losing values remain auditable.

  • Git refs are pointers, not queues. Polling them does not provide delivery latency, backpressure, or consumer acknowledgements.

  • Refs do not authenticate meaning. Repository authorization and signature verification must be implemented separately.

  • Atomicity is limited. Updating several refs individually can expose an intermediate view. Use git update-ref --stdin transactions when multiple ref changes must become visible together.

  • One repository is one administrative boundary. Cross-repository operations require an external transaction or reconciliation protocol.

  • Clock order is not causal understanding. A Lamport clock can order events but cannot prove that one agent observed another's operation unless the publication protocol records that dependency.

  • Human intent remains underspecified. A schema can validate an operation while the underlying task description remains ambiguous.

      Use CRDTs for narrow coordination objects—claims, task state, findings, requirements, approvals, evidence references, and append-only observations. Continue using normal review and integration techniques for source code.
    

Production hardening

Define the operation algebra first

      For every operation, document its identifier, validation rules, authorization, causal assumptions, merge behavior, rejection behavior, and compaction policy. If two operations cannot be composed safely, represent that as an explicit review state rather than inventing a silent tie-breaker.
Enter fullscreen mode Exit fullscreen mode

Protect namespaces

A practical permission model is:

          Identity
          May read
          May update




          Agent Alpha
          Base branch and authorized coordination refs
          refs/agents/alpha/*


          Agent Beta
          Base branch and authorized coordination refs
          refs/agents/beta/*


          Coordinator
          All agent refs
          Accepted-state branch and snapshot refs
Enter fullscreen mode Exit fullscreen mode

Pin every acceptance input

      An acceptance record should name the base commit, exact agent ref object identifiers, reducer version, policy version, generated-state digest, and verification evidence. Ref names alone are insufficient because refs move.
Enter fullscreen mode Exit fullscreen mode

Make publication transactional

      Create the operation commit first, validate it by object identifier, and then advance the ref with the expected old value. Never expose a ref before its tree has passed structural checks. For a batch:
Enter fullscreen mode Exit fullscreen mode
git update-ref --stdin <<EOF
start
update refs/agents/alpha/task-42 NEW_SHA OLD_SHA
prepare
commit
EOF
Enter fullscreen mode Exit fullscreen mode
      Replace NEW_SHA and OLD_SHA with already validated full object identifiers. Do not pass unresolved user input directly into a ref transaction.
Enter fullscreen mode Exit fullscreen mode

Separate convergence from acceptance

      A robust pipeline has four gates:
Enter fullscreen mode Exit fullscreen mode
  • collect immutable operations from pinned commits;

  • validate identity, authorization, schema, and causal rules;

  • reduce to a deterministic candidate state;

  • run project verification and accept the candidate through a one-parent coordinator commit.

Compact without losing provenance

      When the operation set becomes large, create a snapshot that records the included operation frontier and its digest. Keep a signed or protected snapshot ref. New reductions begin from that snapshot and apply only later operations. Test snapshot-plus-tail reduction against full replay before pruning any history.
Enter fullscreen mode Exit fullscreen mode

Observe coordination quality

Useful measurements include:

  • duplicate work claims per task;

  • compare-and-swap publication failures;

  • operations rejected by schema or policy;

  • time between publication and materialization;

  • semantic conflicts surfaced for review;

  • replay mismatches between generated and committed state;

  • orphaned refs and operations not included in any accepted snapshot.

      These are measurements to collect from your own system, not universal target values. Establish thresholds only after observing representative workloads.
    

Repeatable laboratory checklist

  • Create an empty disposable Git repository.

  • Commit the base task and deterministic reducer.

  • Pin the common base commit.

  • Create two detached worktrees from that commit.

  • Write one immutable operation per agent.

  • Commit each operation independently.

  • Publish each commit under its own refs/agents/ namespace.

  • Verify that neither agent commit descends from the other.

  • Read operations directly from Git objects.

  • Reduce the refs in both input orders.

  • Compare generated bytes.

  • Repeat one ref to test idempotent delivery.

  • Assert that both logical changes exist.

  • Commit only the materialized task to main.

  • Verify that the accepted commit has one parent.

  • Delete temporary worktrees.

  • Reconstruct the task in a fresh session.

  • Save versions, commit IDs, digests, and exit statuses from the actual run.

Final takeaway

        Git refs solve durable publication and discovery; the CRDT solves deterministic composition; the coordinator solves policy and acceptance. Keeping those responsibilities separate prevents private agent work from becoming shared mutable state and makes recovery depend on inspectable repository objects instead of vanished session context.






    ← Browse all guides
    ·
    Open the glossary →





Agent Lab Journal
Real experiments. Verifiable conclusions.
Enter fullscreen mode Exit fullscreen mode

Original article: https://agentlabjournal.online/en/git-refs-crdt-coordination-for-coding-agents.html?utm_source=devto&utm_medium=referral&utm_campaign=agentlabjournal-en-global-all&utm_content=article&utm_term=git-refs-crdt-coordination-for-coding-agents

Top comments (0)