Coordinating Two Coding Agents with Git Refs and a CRDT — Agent Lab Journal
Agent Lab Journal
Guides
Glossary
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.
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.
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.
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:
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
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.
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:
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"
}
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.
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.
Concrete case: two agents change one task
Task task-42 begins as:
{
"id": "task-42",
"title": "Prevent duplicate payment retries",
"status": "open",
"requirements": []
}
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.
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
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.
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
Create base/task-42.json:
{
"id": "task-42",
"title": "Prevent duplicate payment retries",
"status": "open",
"requirements": []
}
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.
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"
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.
Step 2: implement the CRDT reducer
Create tools/reduce.py with the following code:
#!/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()
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)
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.
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.
Agent Alpha
Create a detached worktree from the common base:
git worktree add --detach .agent-alpha "$BASE_COMMIT"
mkdir -p .agent-alpha/ops/alpha
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"
}
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
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.
Agent Beta
Create another detached worktree from the same base:
git worktree add --detach .agent-beta "$BASE_COMMIT"
mkdir -p .agent-beta/ops/beta
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."
}
}
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
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
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.
Step 4: synchronize through Git refs
List the coordination namespace without switching branches:
git for-each-ref \
--format='%(refname) %(objectname)' \
refs/agents/
Inspect published operations directly from their commit trees:
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
At this point main has not moved. Confirm that the accepted working tree contains no agent operation:
test ! -e ops/alpha/alpha-task-42-1.json
test ! -e ops/beta/beta-task-42-1.json
git status --short
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:
git push origin \
HEAD:refs/agents/alpha/task-42
The coordinator can fetch all agent refs with:
git fetch origin \
'+refs/agents/*:refs/agents/*'
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.
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
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
cmp must exit successfully. This checks byte-for-byte equality, not merely semantic similarity.
Inspect the materialized task
python3 -m json.tool /tmp/task-ab.json
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"
}
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.
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"
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.
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:
git -C .agent-alpha status --porcelain
git -C .agent-beta status --porcelain
git worktree remove .agent-alpha
git worktree remove .agent-beta
Start a new shell in the repository. Do not restore ALPHA_COMMIT, BETA_COMMIT, or any agent prompt. Rebuild from durable state:
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
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.
Verification protocol
Run the following checks individually. A silent command with exit status zero is a passing assertion.
1. Both refs exist
git show-ref --verify --quiet refs/agents/alpha/task-42
git show-ref --verify --quiet refs/agents/beta/task-42
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
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
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
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
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
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.
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
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.
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.
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
}
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.
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.
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:
{
"op_id": "alpha:claim:payment-retry-handler:1",
"actor": "alpha",
"clock": 1,
"task": "task-42",
"kind": "claim_work",
"value": {
"work_unit": "payment-retry-handler"
}
}
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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:
git update-ref --stdin <<EOF
start
update refs/agents/alpha/task-42 NEW_SHA OLD_SHA
prepare
commit
EOF
Replace NEW_SHA and OLD_SHA with already validated full object identifiers. Do not pass unresolved user input directly into a ref transaction.
Separate convergence from acceptance
A robust pipeline has four gates:
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.
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.
Top comments (0)