Git Integration with Generation Clock for Multi-Agent Conflict Resolution
Coditect Autonomous Development Platform
Version: 1.0
Status: Draft
Date: January 2026
1. Executive Summary
This document describes how Git version control integrates with the Generation Clock pattern to manage conflicting work products from multiple AI agents. By combining Git's content-addressable storage and branching model with generation-based coordination, Coditect achieves deterministic conflict resolution at both the task coordination layer and the code artifact layer.
The Two-Layer Conflict Problem
When multiple agents work on the same project, conflicts can occur at two distinct layers:
┌─────────────────────────────────────────────────────────────────────────┐
│ CONFLICT LAYERS │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ LAYER 1: TASK COORDINATION (Generation Clock) │
│ ───────────────────────────────────────────────────────────────────── │
│ Problem: Multiple agents claim/complete the same task │
│ Resolution: Higher generation wins, stale results rejected │
│ Mechanism: Monotonic generation counter + lease expiration │
│ │
│ LAYER 2: CODE ARTIFACTS (Git) │
│ ───────────────────────────────────────────────────────────────────── │
│ Problem: Multiple agents modify the same files │
│ Resolution: Git merge/rebase with generation-aware strategies │
│ Mechanism: Branch-per-generation + automated merge resolution │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Solution Overview
- Branch-per-Task-Generation: Each task claim creates a Git branch tagged with generation
- Generation-Ordered Merging: Branches merge to main in generation order
- Conflict Resolution Hierarchy: Higher generation code takes precedence
- Automated Reconciliation: AI-assisted merge for semantic conflicts
- Audit Trail: Git history preserves all generations, even rejected ones
2. Git Branch Naming Convention
2.1 Branch Name Structure
All agent work occurs on branches following a strict naming convention:
[TYPE]/[TASK_ID]/gen-[GENERATION]/[SESSION_SHORT]
Where:
TYPE := task | subtask | hotfix | experiment
TASK_ID := [TRACK]-[NNN]-[description]
GENERATION := [1-999]
SESSION_SHORT := First 8 chars of session_id
Examples:
task/A-001-setup-authentication/gen-1/sess-a1b2
task/A-001-setup-authentication/gen-2/sess-c3d4
task/B-003-implement-dashboard/gen-1/sess-e5f6
subtask/B-003-implement-dashboard::2/gen-1/sess-e5f6
2.2 Branch Lifecycle
BRANCH LIFECYCLE
═══════════════════════════════════════════════════════════════════════════
┌─────────────────────────────────────────────────────────────────┐
│ main │
│ ════════════════════════════════════════════════════════════ │
│ │ │ │ │
│ │ │ │ │
│ [A-001 merged] [B-001 merged] [A-002 merged] │
└─────────┼────────────────────┼────────────────────┼─────────────┘
│ │ │
│ │ │
══════════╪════════════════════╪════════════════════╪══════════════
│ │ │
┌─────────┴─────────┐ │ │
│ task/A-001-.../ │ │ │
│ gen-1/sess-a1b2 │ │ │
│ │ │ │
│ [commits...] │ │ │
│ [MERGED ✓] │ │ │
└───────────────────┘ │ │
│ │
┌─────────────────────────────┴─────────┐ │
│ task/B-001-schema-design/ │ │
│ gen-1/sess-c3d4 │ │
│ │ │
│ [commits...] │ │
│ [MERGED ✓] │ │
└───────────────────────────────────────┘ │
│
┌─────────────────────────────────────────────────┴─────────┐
│ task/A-002-di-container/gen-1/sess-a1b2 │
│ │
│ [commits...] │
│ [MERGED ✓] │
└───────────────────────────────────────────────────────────┘
CONFLICT SCENARIO: GENERATION RACE
═══════════════════════════════════════════════════════════════════════════
main ════════════════════════════════════════════════════════════
│
├─────────────────────────────────────┐
│ │
┌─────────┴─────────────────┐ ┌─────────────┴─────────────────┐
│ task/C-001-.../gen-1/ │ │ task/C-001-.../gen-2/ │
│ sess-alice │ │ sess-bob │
│ │ │ │
│ [Alice's work] │ │ [Bob's work] │
│ [Lease EXPIRED] │ │ [Claim ACQUIRED after expiry] │
│ [Result REJECTED] │ │ [Result ACCEPTED] │
│ │ │ │
│ ❌ NOT MERGED │ │ ✓ MERGED TO MAIN │
│ (preserved in refs/ │ │ │
│ rejected/...) │ │ │
└───────────────────────────┘ └───────────────────────────────┘
2.3 Special Reference Namespaces
refs/
├── heads/ # Active branches
│ ├── main
│ └── task/A-001-.../gen-1/...
├── generations/ # Generation tracking
│ └── [tenant]/[project]/[task_id]/
│ ├── gen-1 # Points to gen-1 branch tip
│ ├── gen-2 # Points to gen-2 branch tip
│ └── current -> gen-2 # Symref to accepted generation
├── rejected/ # Preserved rejected work
│ └── [tenant]/[project]/[task_id]/
│ └── gen-1/sess-alice # Alice's rejected work
├── work-products/ # Accepted work products
│ └── wp-A-001-...-gen1-abc123 # Tag pointing to accepted commit
└── tags/
└── release/...
3. Generation-Aware Git Operations
3.1 Claim Acquisition → Branch Creation
When an agent acquires a task claim, a corresponding Git branch is created:
class GitGenerationManager:
"""
Manages Git branches in coordination with Generation Clock.
"""
def __init__(self, repo_path: str, coordinator: TaskCoordinator):
self.repo = git.Repo(repo_path)
self.coordinator = coordinator
async def on_claim_acquired(
self,
claim: TaskClaim,
base_ref: str = "main"
) -> str:
"""
Called when Generation Clock grants a claim.
Creates a new branch for this generation's work.
Returns: Branch name
"""
branch_name = self._build_branch_name(claim)
# Create branch from base
base_commit = self.repo.commit(base_ref)
self.repo.create_head(branch_name, base_commit)
# Create generation ref
gen_ref = self._build_generation_ref(claim)
self.repo.create_head(gen_ref, base_commit)
# Record in generation tracking
await self._record_generation_branch(claim, branch_name)
logger.info(
f"Created branch {branch_name} for "
f"task={claim.key.task_id} gen={claim.generation.generation}"
)
return branch_name
def _build_branch_name(self, claim: TaskClaim) -> str:
"""Build standardized branch name from claim."""
session_short = claim.session_id[:8]
task_type = "subtask" if "::" in claim.key.task_id else "task"
return (
f"{task_type}/"
f"{claim.key.task_id}/"
f"gen-{claim.generation.generation}/"
f"{session_short}"
)
def _build_generation_ref(self, claim: TaskClaim) -> str:
"""Build generation tracking ref."""
return (
f"refs/generations/"
f"{claim.key.tenant_id}/"
f"{claim.key.project_id}/"
f"{claim.key.task_id}/"
f"gen-{claim.generation.generation}"
)
3.2 Work Commits → Generation-Tagged
All commits made during a task include generation metadata:
async def commit_work(
self,
claim: TaskClaim,
message: str,
files: List[str]
) -> str:
"""
Commit work with generation metadata.
The commit message includes structured metadata that enables
automated processing and conflict resolution.
"""
branch_name = self._build_branch_name(claim)
# Ensure we're on the right branch
self.repo.heads[branch_name].checkout()
# Stage files
self.repo.index.add(files)
# Build commit message with generation metadata
full_message = self._build_commit_message(claim, message)
# Create commit
commit = self.repo.index.commit(
full_message,
author=git.Actor(
f"agent-{claim.agent_id}",
f"{claim.agent_id}@coditect.local"
),
committer=git.Actor(
f"coditect-coordinator",
"coordinator@coditect.local"
)
)
# Update generation ref
gen_ref = self._build_generation_ref(claim)
self.repo.heads[gen_ref].set_commit(commit)
return commit.hexsha
def _build_commit_message(self, claim: TaskClaim, message: str) -> str:
"""
Build commit message with generation metadata.
Format:
[TASK_ID] Message
Generation-Clock: gen=N session=XXX agent=YYY
Task-Track: X
Task-Sequence: NNN
Work-Product: wp-...-genN-hash
Claimed-At: ISO-8601
"""
task_id = TaskIdentifier.parse(claim.key.task_id)
wp_ref = generate_work_product_ref(
claim.key.task_id,
claim.generation.generation
)
return f"""[{claim.key.task_id}] {message}
Generation-Clock: gen={claim.generation.generation} session={claim.session_id} agent={claim.agent_id}
Task-Track: {task_id.track}
Task-Sequence: {task_id.sequence}
Work-Product: {wp_ref}
Claimed-At: {claim.generation.claimed_at.isoformat()}
Tenant: {claim.key.tenant_id}
Project: {claim.key.project_id}
"""
3.3 Result Submission → Conditional Merge
When an agent submits a result, the merge to main is conditional on generation acceptance:
async def on_result_accepted(
self,
claim: TaskClaim,
result: TaskResult
) -> MergeResult:
"""
Called when Generation Clock accepts a result.
Merges the generation branch to main.
"""
branch_name = self._build_branch_name(claim)
# Verify branch exists and has commits
if branch_name not in self.repo.heads:
raise GitGenerationError(f"Branch {branch_name} not found")
branch = self.repo.heads[branch_name]
# Check for conflicts with main
main = self.repo.heads["main"]
merge_base = self.repo.merge_base(main, branch)
# Attempt merge
try:
# Checkout main
main.checkout()
# Merge with generation-aware strategy
merge_result = await self._generation_aware_merge(
branch_name,
claim.generation.generation
)
if merge_result.success:
# Tag the work product
self._create_work_product_tag(result)
# Update current generation ref
self._update_current_generation(claim)
# Archive the branch
self._archive_merged_branch(branch_name, claim)
return merge_result
except git.GitCommandError as e:
logger.error(f"Merge failed: {e}")
return MergeResult(
success=False,
conflict_files=self._get_conflict_files(),
resolution_required=True
)
async def on_result_rejected(
self,
claim: TaskClaim,
reason: RejectionReason
) -> None:
"""
Called when Generation Clock rejects a result.
Preserves the branch in rejected namespace for audit.
"""
branch_name = self._build_branch_name(claim)
if branch_name not in self.repo.heads:
return
branch = self.repo.heads[branch_name]
# Move to rejected namespace
rejected_ref = (
f"refs/rejected/"
f"{claim.key.tenant_id}/"
f"{claim.key.project_id}/"
f"{claim.key.task_id}/"
f"gen-{claim.generation.generation}/"
f"{claim.session_id[:8]}"
)
# Create ref in rejected namespace
self.repo.create_head(rejected_ref, branch.commit)
# Delete the active branch
self.repo.delete_head(branch_name, force=True)
logger.info(
f"Preserved rejected work at {rejected_ref} "
f"reason={reason.value}"
)
4. Conflict Resolution Strategies
4.1 Conflict Types and Resolution Hierarchy
┌─────────────────────────────────────────────────────────────────────────┐
│ CONFLICT RESOLUTION HIERARCHY │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ LEVEL 1: GENERATION PRECEDENCE (Automatic) │
│ ═══════════════════════════════════════════ │
│ Rule: Higher generation ALWAYS wins for the same task │
│ Scope: Entire task's work product │
│ Implementation: Branch replacement, no merge attempted │
│ │
│ Example: │
│ gen-1 completes task A-001 → branch task/A-001/.../gen-1 │
│ gen-2 completes task A-001 → branch task/A-001/.../gen-2 │
│ Result: gen-2 branch merged, gen-1 branch archived │
│ │
│ ───────────────────────────────────────────────────────────────────── │
│ │
│ LEVEL 2: FILE-LEVEL CONFLICT (Semi-Automatic) │
│ ═════════════════════════════════════════════ │
│ Rule: When different tasks modify same file │
│ Scope: Individual files │
│ Implementation: Git merge with custom merge drivers │
│ │
│ Strategies: │
│ a) DISJOINT CHANGES: Auto-merge (Git default) │
│ b) OVERLAPPING CHANGES: Higher-gen-wins OR semantic merge │
│ c) STRUCTURAL CONFLICTS: AI-assisted resolution │
│ │
│ ───────────────────────────────────────────────────────────────────── │
│ │
│ LEVEL 3: SEMANTIC CONFLICT (AI-Assisted) │
│ ════════════════════════════════════════ │
│ Rule: Changes that are textually compatible but semantically broken │
│ Scope: Code behavior and correctness │
│ Implementation: AI analysis + human review for critical paths │
│ │
│ Examples: │
│ - Function signature changed in one branch, calls updated in another │
│ - Shared state modified with incompatible assumptions │
│ - API contract changes affecting multiple consumers │
│ │
└─────────────────────────────────────────────────────────────────────────┘
4.2 Generation-Aware Merge Driver
Custom Git merge driver that uses generation metadata:
class GenerationAwareMergeDriver:
"""
Custom Git merge driver that resolves conflicts using
generation precedence when both changes are from the
same logical task.
"""
def merge(
self,
ancestor: str, # Common ancestor content
current: str, # Current branch (main) content
other: str, # Other branch (task) content
ancestor_meta: dict,
current_meta: dict,
other_meta: dict
) -> Tuple[str, bool]:
"""
Perform generation-aware merge.
Returns:
(merged_content, had_conflict)
"""
# Extract generation info from metadata
current_gen = current_meta.get("generation", 0)
other_gen = other_meta.get("generation", 0)
current_task = current_meta.get("task_id")
other_task = other_meta.get("task_id")
# RULE 1: Same task, different generations → higher wins
if current_task == other_task:
if other_gen > current_gen:
return other, False
elif current_gen > other_gen:
return current, False
# Same generation shouldn't happen, but handle it
else:
return self._structural_merge(ancestor, current, other)
# RULE 2: Different tasks → attempt structural merge
merged, conflict = self._structural_merge(ancestor, current, other)
if conflict:
# RULE 3: Structural conflict → AI-assisted resolution
merged = self._ai_assisted_merge(
ancestor, current, other,
current_meta, other_meta
)
conflict = False # AI resolved it
return merged, conflict
def _structural_merge(
self,
ancestor: str,
current: str,
other: str
) -> Tuple[str, bool]:
"""
Attempt standard 3-way merge.
Returns (result, had_conflict).
"""
import difflib
# Use diff3-style merge
ancestor_lines = ancestor.splitlines(keepends=True)
current_lines = current.splitlines(keepends=True)
other_lines = other.splitlines(keepends=True)
# Simple implementation - production would use libgit2
merger = difflib.Differ()
# Detect conflicts
current_diff = list(merger.compare(ancestor_lines, current_lines))
other_diff = list(merger.compare(ancestor_lines, other_lines))
# Check for overlapping changes
# (Simplified - real implementation more complex)
conflicts = self._detect_overlaps(current_diff, other_diff)
if conflicts:
return self._mark_conflicts(
ancestor_lines, current_lines, other_lines, conflicts
), True
# No conflicts - combine changes
return self._combine_changes(
ancestor_lines, current_lines, other_lines
), False
def _ai_assisted_merge(
self,
ancestor: str,
current: str,
other: str,
current_meta: dict,
other_meta: dict
) -> str:
"""
Use AI to resolve semantic conflicts.
The AI considers:
- What each change is trying to accomplish (from task description)
- Code semantics and dependencies
- Test coverage implications
- API compatibility
"""
prompt = f"""
Resolve this merge conflict between two agent work products.
ANCESTOR (common base):
```
{ancestor}
```
CURRENT (main branch, task {current_meta.get('task_id')}):
```
{current}
```
OTHER (incoming, task {other_meta.get('task_id')}, gen {other_meta.get('generation')}):
```
{other}
```
Context:
- Current task: {current_meta.get('task_description', 'unknown')}
- Other task: {other_meta.get('task_description', 'unknown')}
Provide the merged result that:
1. Preserves the intent of both changes
2. Maintains code correctness
3. Resolves any semantic conflicts
Return ONLY the merged code, no explanation.
"""
# Call AI service for resolution
merged = self.ai_service.complete(prompt)
return merged
4.3 Merge Strategy Selection
class MergeStrategySelector:
"""
Selects appropriate merge strategy based on conflict characteristics.
"""
def select_strategy(
self,
base_commit: str,
current_commit: str,
incoming_commit: str,
current_claim: Optional[TaskClaim],
incoming_claim: TaskClaim
) -> MergeStrategy:
"""
Analyze commits and select merge strategy.
"""
# Get changed files in each branch
current_files = self._get_changed_files(base_commit, current_commit)
incoming_files = self._get_changed_files(base_commit, incoming_commit)
# Check for file overlap
overlapping_files = current_files & incoming_files
if not overlapping_files:
# No file conflicts - simple merge
return MergeStrategy.FAST_FORWARD_OR_MERGE
# Check if same task (generation conflict)
if current_claim and current_claim.key.task_id == incoming_claim.key.task_id:
return MergeStrategy.GENERATION_PRECEDENCE
# Different tasks touching same files
conflict_severity = self._assess_conflict_severity(
base_commit, current_commit, incoming_commit, overlapping_files
)
if conflict_severity == "LOW":
# Disjoint changes within same files
return MergeStrategy.RECURSIVE_MERGE
elif conflict_severity == "MEDIUM":
# Overlapping but likely resolvable
return MergeStrategy.GENERATION_AWARE_MERGE
else:
# Complex conflicts requiring AI
return MergeStrategy.AI_ASSISTED_MERGE
def _assess_conflict_severity(
self,
base: str,
current: str,
incoming: str,
files: Set[str]
) -> str:
"""
Assess how severe the conflicts are.
LOW: Changes in different parts of files
MEDIUM: Changes in nearby lines
HIGH: Changes to same lines/functions
"""
for file in files:
base_content = self._get_file_content(base, file)
current_content = self._get_file_content(current, file)
incoming_content = self._get_file_content(incoming, file)
# Analyze change regions
current_regions = self._get_changed_regions(base_content, current_content)
incoming_regions = self._get_changed_regions(base_content, incoming_content)
# Check for overlap
overlap = self._regions_overlap(current_regions, incoming_regions)
if overlap == "DIRECT":
return "HIGH"
elif overlap == "ADJACENT":
return "MEDIUM"
return "LOW"
class MergeStrategy(Enum):
FAST_FORWARD_OR_MERGE = "ff_or_merge" # No conflicts expected
RECURSIVE_MERGE = "recursive" # Standard Git merge
GENERATION_PRECEDENCE = "gen_precedence" # Higher gen wins entirely
GENERATION_AWARE_MERGE = "gen_aware" # Merge with gen metadata
AI_ASSISTED_MERGE = "ai_assisted" # AI resolves conflicts
5. Conflict Scenarios and Resolution
5.1 Scenario: Same Task, Different Generations
SCENARIO: Alice and Bob both work on task A-001
═══════════════════════════════════════════════════════════════════════════
Timeline:
─────────────────────────────────────────────────────────────────────────
T=0 Alice claims A-001, gen=1
→ Branch: task/A-001-setup-auth/gen-1/sess-alice
T=5 Alice commits work
→ Commit: abc123 "Add OAuth2 provider"
T=10 Alice's network fails (lease renewal fails)
T=15 Lease expires
T=16 Bob claims A-001, gen=2
→ Branch: task/A-001-setup-auth/gen-2/sess-bob (from main)
T=20 Bob commits different implementation
→ Commit: def456 "Add SAML provider"
T=25 Bob submits result → ACCEPTED (gen=2)
→ Gen-2 branch merged to main
T=30 Alice submits result → REJECTED (gen=1 < gen=2)
→ Gen-1 branch moved to refs/rejected/...
GIT STATE AFTER RESOLUTION:
─────────────────────────────────────────────────────────────────────────
main ─────●─────●─────●─────●───────────────────────●
│ │ │ │ │
│ │ │ └── [B-001 merged] │
│ │ │ │
│ │ └── [previous work] │
│ │ │
│ └── [previous work] │
│ │
└── [initial] [A-001 gen-2 merged]
│
│
task/A-001-.../gen-2/sess-bob ─────●───────────┘
│
(Bob's SAML work)
refs/rejected/.../A-001/gen-1/sess-alice ─────●
│
(Alice's OAuth work)
(PRESERVED for audit)
RESULT:
- Bob's SAML implementation is in main
- Alice's OAuth work is preserved but not merged
- Alice can review what happened and potentially resubmit as new task
5.2 Scenario: Different Tasks, Same Files
SCENARIO: Tasks A-001 and B-002 both modify shared config file
═══════════════════════════════════════════════════════════════════════════
Timeline:
─────────────────────────────────────────────────────────────────────────
T=0 Agent-Alpha claims A-001 (Track A: Core setup)
Agent-Beta claims B-002 (Track B: Database config)
T=5 Both agents working in parallel (different tracks)
T=10 Agent-Alpha commits to config.yaml:
```yaml
# Added by A-001
auth:
provider: oauth2
client_id: ${AUTH_CLIENT_ID}
```
T=12 Agent-Beta commits to config.yaml:
```yaml
# Added by B-002
database:
host: ${DB_HOST}
pool_size: 10
```
T=15 Agent-Alpha submits A-001 result → ACCEPTED
→ Merged to main
T=18 Agent-Beta submits B-002 result → ACCEPTED
→ Merge attempt...
MERGE ANALYSIS:
─────────────────────────────────────────────────────────────────────────
main (after A-001)
│
│ config.yaml contains:
│ ```
│ auth:
│ provider: oauth2
│ client_id: ${AUTH_CLIENT_ID}
│ ```
│
├───────────────────────────────┐
│ │
│ task/B-002-.../gen-1
│ │
│ config.yaml contains:
│ ```
│ database:
│ host: ${DB_HOST}
│ pool_size: 10
│ ```
CONFLICT TYPE: DISJOINT ADDITIONS
RESOLUTION: AUTOMATIC MERGE (changes don't overlap)
MERGED config.yaml:
```yaml
auth:
provider: oauth2
client_id: ${AUTH_CLIENT_ID}
database:
host: ${DB_HOST}
pool_size: 10
No conflict - Git's recursive merge handles this automatically.
### 5.3 Scenario: Semantic Conflict Requiring AI Resolution
SCENARIO: Two tasks create incompatible API changes ═══════════════════════════════════════════════════════════════════════════
Timeline: ─────────────────────────────────────────────────────────────────────────
T=0 C-001: Add user authentication endpoint C-003: Add user profile endpoint
T=10 C-001 creates UserController:
python class UserController: async def authenticate(self, credentials: dict) -> AuthToken: user = await self.user_service.validate(credentials) return self.token_service.create(user.id)
T=12 C-003 creates UserController (didn't know C-001 was making one):
python class UserController: async def get_profile(self, user_id: str) -> UserProfile: return await self.user_service.get_profile(user_id)
T=15 C-001 merged to main first (lower sequence number)
T=18 C-003 attempts merge → CONFLICT
CONFLICT ANALYSIS: ─────────────────────────────────────────────────────────────────────────
File: controllers/user_controller.py
<<<<<<< main (C-001) class UserController: async def authenticate(self, credentials: dict) -> AuthToken: user = await self.user_service.validate(credentials) return self.token_service.create(user.id)
class UserController: async def get_profile(self, user_id: str) -> UserProfile: return await self.user_service.get_profile(user_id)
task/C-003-.../gen-1
CONFLICT TYPE: SEMANTIC (both define same class differently) RESOLUTION: AI-ASSISTED MERGE
AI RESOLUTION PROMPT: ─────────────────────────────────────────────────────────────────────────
"Two tasks created the same UserController class with different methods. Task C-001 (authentication) has sequence priority. Task C-003 (profile) should integrate with C-001's structure.
Merge to create a unified UserController with both methods."
AI MERGED RESULT: ─────────────────────────────────────────────────────────────────────────
class UserController:
"""
User-related HTTP endpoints.
Merged from:
- C-001-add-user-authentication (gen-1): authenticate method
- C-003-add-user-profile (gen-1): get_profile method
"""
def __init__(
self,
user_service: UserService,
token_service: TokenService
):
self.user_service = user_service
self.token_service = token_service
async def authenticate(self, credentials: dict) -> AuthToken:
"""Authenticate user and return token. (from C-001)"""
user = await self.user_service.validate(credentials)
return self.token_service.create(user.id)
async def get_profile(self, user_id: str) -> UserProfile:
"""Get user profile by ID. (from C-003)"""
return await self.user_service.get_profile(user_id)
Merge commit message: "Merge C-003 into main with AI-assisted conflict resolution
Conflicts resolved:
- controllers/user_controller.py: Merged two UserController definitions
Resolution strategy: Combined methods from both tasks into unified class. C-001 (authenticate) + C-003 (get_profile) → unified UserController"
---
## 6. Git Hooks for Generation Enforcement
### 6.1 Pre-Receive Hook
```bash
#!/bin/bash
# pre-receive hook: Enforce generation clock rules
while read oldrev newrev refname; do
# Skip non-task branches
if [[ ! "$refname" =~ ^refs/heads/task/ ]]; then
continue
fi
# Extract task info from branch name
# Format: refs/heads/task/[TASK_ID]/gen-[N]/[SESSION]
TASK_ID=$(echo "$refname" | cut -d'/' -f4)
GENERATION=$(echo "$refname" | cut -d'/' -f5 | sed 's/gen-//')
SESSION=$(echo "$refname" | cut -d'/' -f6)
# Verify with Generation Clock coordinator
CLAIM_STATUS=$(curl -s "http://coordinator:8080/api/claims/$TASK_ID" \
-H "X-Session-ID: $SESSION")
CURRENT_GEN=$(echo "$CLAIM_STATUS" | jq -r '.generation')
CLAIM_SESSION=$(echo "$CLAIM_STATUS" | jq -r '.session_id')
CLAIM_STATE=$(echo "$CLAIM_STATUS" | jq -r '.state')
# Rule 1: Generation must match current claim
if [[ "$GENERATION" != "$CURRENT_GEN" ]]; then
echo "ERROR: Generation mismatch. Branch gen=$GENERATION, claim gen=$CURRENT_GEN"
echo "Your claim may have been superseded."
exit 1
fi
# Rule 2: Session must match
if [[ "$SESSION" != "${CLAIM_SESSION:0:8}" ]]; then
echo "ERROR: Session mismatch. Branch session=$SESSION, claim session=$CLAIM_SESSION"
exit 1
fi
# Rule 3: Claim must be active
if [[ "$CLAIM_STATE" != "ACTIVE" ]]; then
echo "ERROR: Claim is not active. State=$CLAIM_STATE"
echo "Your lease may have expired."
exit 1
fi
# Verify commit messages have generation metadata
for commit in $(git rev-list $oldrev..$newrev); do
MSG=$(git log -1 --format=%B $commit)
if ! echo "$MSG" | grep -q "^Generation-Clock:"; then
echo "ERROR: Commit $commit missing Generation-Clock metadata"
exit 1
fi
done
echo "✓ Generation verification passed for $TASK_ID gen=$GENERATION"
done
exit 0
6.2 Post-Merge Hook
#!/bin/bash
# post-merge hook: Notify Generation Clock of successful merge
# Get the merge commit
MERGE_COMMIT=$(git rev-parse HEAD)
# Extract task info from merge commit message
TASK_ID=$(git log -1 --format=%B $MERGE_COMMIT | grep "Task-ID:" | cut -d' ' -f2)
GENERATION=$(git log -1 --format=%B $MERGE_COMMIT | grep "Generation-Clock:" | \
sed 's/.*gen=\([0-9]*\).*/\1/')
WORK_PRODUCT=$(git log -1 --format=%B $MERGE_COMMIT | grep "Work-Product:" | cut -d' ' -f2)
if [[ -n "$TASK_ID" && -n "$GENERATION" ]]; then
# Notify coordinator
curl -X POST "http://coordinator:8080/api/merges" \
-H "Content-Type: application/json" \
-d "{
\"task_id\": \"$TASK_ID\",
\"generation\": $GENERATION,
\"commit\": \"$MERGE_COMMIT\",
\"work_product_ref\": \"$WORK_PRODUCT\"
}"
echo "✓ Notified coordinator of merge: $TASK_ID gen=$GENERATION"
fi
6.3 Pre-Push Hook (Agent-Side)
#!/usr/bin/env python3
"""
Pre-push hook: Verify claim is still valid before pushing.
"""
import sys
import subprocess
import requests
import json
def get_claim_status(task_id: str, session_id: str) -> dict:
"""Query coordinator for claim status."""
response = requests.get(
f"http://coordinator:8080/api/claims/{task_id}",
headers={"X-Session-ID": session_id}
)
return response.json()
def main():
# Read push info from stdin
for line in sys.stdin:
local_ref, local_sha, remote_ref, remote_sha = line.strip().split()
# Only check task branches
if not remote_ref.startswith("refs/heads/task/"):
continue
# Parse branch name
parts = remote_ref.split("/")
task_id = parts[3]
generation = int(parts[4].replace("gen-", ""))
session_short = parts[5]
# Get current claim status
status = get_claim_status(task_id, session_short)
# Verify claim is still valid
if status.get("state") != "ACTIVE":
print(f"ERROR: Claim for {task_id} is no longer active")
print(f"State: {status.get('state')}")
print(f"Your lease may have expired.")
sys.exit(1)
current_gen = status.get("generation")
if current_gen != generation:
print(f"ERROR: Generation mismatch for {task_id}")
print(f"Your branch: gen-{generation}")
print(f"Current claim: gen-{current_gen}")
print(f"Another agent has taken over this task.")
sys.exit(1)
# Check remaining lease time
remaining = status.get("lease_remaining_seconds", 0)
if remaining < 30:
print(f"WARNING: Lease expires in {remaining}s")
print("Consider renewing before push completes.")
print(f"✓ Claim verified: {task_id} gen={generation}")
sys.exit(0)
if __name__ == "__main__":
main()
7. Recovering Rejected Work
7.1 Inspecting Rejected Generations
# List all rejected work for a task
git for-each-ref --format='%(refname) %(objectname:short) %(subject)' \
refs/rejected/acme-corp/project-123/A-001-setup-auth/
# Output:
# refs/rejected/.../A-001-setup-auth/gen-1/sess-alice abc1234 Add OAuth2 provider
# refs/rejected/.../A-001-setup-auth/gen-3/sess-charlie def5678 Add OIDC provider
# View the rejected work
git log refs/rejected/.../A-001-setup-auth/gen-1/sess-alice --oneline
# Compare rejected vs accepted
git diff main refs/rejected/.../A-001-setup-auth/gen-1/sess-alice
7.2 Salvaging Rejected Work
class RejectedWorkRecovery:
"""
Tools for recovering and reusing rejected agent work.
"""
async def list_rejected_for_task(
self,
tenant_id: str,
project_id: str,
task_id: str
) -> List[RejectedWork]:
"""List all rejected work products for a task."""
ref_pattern = f"refs/rejected/{tenant_id}/{project_id}/{task_id}/"
rejected = []
for ref in self.repo.refs:
if ref.path.startswith(ref_pattern):
parts = ref.path.split("/")
generation = int(parts[-2].replace("gen-", ""))
session = parts[-1]
rejected.append(RejectedWork(
ref=ref.path,
generation=generation,
session=session,
commit=ref.commit.hexsha,
message=ref.commit.message,
author=ref.commit.author.name,
date=ref.commit.committed_datetime
))
return sorted(rejected, key=lambda r: r.generation)
async def cherry_pick_to_new_task(
self,
rejected_ref: str,
new_task_id: str,
new_claim: TaskClaim
) -> str:
"""
Create a new task incorporating rejected work.
Useful when:
- Rejected work was good but superseded
- Want to create follow-up task with rejected approach
"""
# Create new branch for the new task
new_branch = self._build_branch_name(new_claim)
self.repo.create_head(new_branch, "main")
self.repo.heads[new_branch].checkout()
# Cherry-pick commits from rejected work
rejected_commits = list(self.repo.iter_commits(
f"main..{rejected_ref}"
))
for commit in reversed(rejected_commits):
# Cherry-pick with updated metadata
self.repo.git.cherry_pick(commit.hexsha, no_commit=True)
# Update commit message with new generation
new_message = self._update_commit_metadata(
commit.message,
new_claim
)
self.repo.index.commit(new_message)
return new_branch
async def diff_generations(
self,
tenant_id: str,
project_id: str,
task_id: str,
gen1: int,
gen2: int
) -> str:
"""
Show diff between two generations of the same task.
Useful for understanding why work diverged.
"""
ref1 = self._find_generation_ref(tenant_id, project_id, task_id, gen1)
ref2 = self._find_generation_ref(tenant_id, project_id, task_id, gen2)
return self.repo.git.diff(ref1, ref2)
8. Integration Architecture
8.1 System Components
┌─────────────────────────────────────────────────────────────────────────┐
│ GIT + GENERATION CLOCK INTEGRATION │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────┐ ┌────────────────┐ │
│ │ AGENT │ │ AGENT │ │
│ │ (Alice) │ │ (Bob) │ │
│ └───────┬────────┘ └───────┬────────┘ │
│ │ │ │
│ │ 1. Claim task │ │
│ ▼ │ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ TASK COORDINATOR │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Generation │ │ Claim │ │ Result │ │ │
│ │ │ Clock │ │ Manager │ │ Validator │ │ │
│ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │
│ │ │ │ │ │ │
│ └─────────┼────────────────┼────────────────┼───────────────────┘ │
│ │ │ │ │
│ │ 2. On claim │ │ 5. On result │
│ │ granted │ │ accepted │
│ ▼ ▼ ▼ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ GIT GENERATION MANAGER │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Branch │ │ Merge │ │ Conflict │ │ │
│ │ │ Creator │ │ Executor │ │ Resolver │ │ │
│ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │
│ │ │ │ │ │ │
│ └─────────┼────────────────┼────────────────┼───────────────────┘ │
│ │ │ │ │
│ │ 3. Create │ 6. Merge to │ 7. Resolve │
│ │ branch │ main │ conflicts │
│ ▼ ▼ ▼ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ GIT REPOSITORY │ │
│ │ │ │
│ │ main ════════════════════════════════════════════════════ │ │
│ │ │ │ │ │ │
│ │ │ │ │ │ │
│ │ task/A-001/.../gen-1 task/B-001/.../gen-1 task/A-002/... │ │
│ │ │ │
│ │ refs/generations/... refs/rejected/... refs/work-products │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ │ │
│ │ 4. Agent pushes commits │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ GIT HOOKS │ │
│ │ │ │
│ │ pre-receive: Verify generation claim is valid │ │
│ │ post-merge: Notify coordinator of successful merge │ │
│ │ pre-push: Check lease hasn't expired │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
8.2 Event Flow
COMPLETE EVENT FLOW: Claim → Work → Submit → Merge
═══════════════════════════════════════════════════════════════════════════
1. CLAIM ACQUISITION
Agent → Coordinator: claim_task(A-001)
Coordinator → Agent: ClaimResult(gen=1, granted=true)
Coordinator → GitManager: on_claim_acquired(claim)
GitManager → Git: create branch task/A-001-.../gen-1/sess-xxx
2. WORK PHASE
Agent → Git: checkout task/A-001-.../gen-1/sess-xxx
Agent → Git: commit "Add feature X" (with gen metadata)
Agent → Git: push (pre-push hook verifies claim)
Git Hooks → Coordinator: verify_claim(A-001, gen=1, sess-xxx)
Coordinator → Git Hooks: valid=true
Git → Agent: push successful
3. LEASE RENEWAL (background)
Agent → Coordinator: renew_lease(A-001, gen=1)
Coordinator → Agent: RenewalResult(renewed=true, new_expiry=...)
4. RESULT SUBMISSION
Agent → Coordinator: submit_result(A-001, gen=1, result_data)
Coordinator: validate generation matches current claim
Coordinator: validate session matches
Coordinator → Agent: SubmissionResult(accepted=true)
5. MERGE EXECUTION
Coordinator → GitManager: on_result_accepted(claim, result)
GitManager: analyze merge strategy
GitManager → Git: merge task/A-001-.../gen-1 to main
Git: execute merge (possibly with conflict resolution)
GitManager → Git: tag wp-A-001-...-gen1-hash
GitManager → Git: archive branch to refs/merged/...
GitManager → Coordinator: MergeResult(success=true)
6. SESSION MEMORY UPDATE
Coordinator → SessionMemory: log_event(RESULT_ACCEPTED, A-001, gen=1)
Coordinator → SessionMemory: remove_active_claim(A-001, completed=true)
9. Configuration
9.1 Git Configuration
# .gitconfig for Coditect repositories
[merge]
# Use generation-aware merge driver by default
conflictstyle = diff3
[merge "generation-aware"]
name = Generation Clock Aware Merge
driver = coditect-merge-driver %O %A %B %L %P
recursive = binary
[diff]
# Show generation metadata in diffs
algorithm = histogram
[receive]
# Enable generation enforcement hooks
denyCurrentBranch = refuse
denyNonFastForwards = false # Allow force-push for generation takeover
[gc]
# Don't garbage collect rejected refs
reflogExpire = never
reflogExpireUnreachable = never
[coditect]
# Coordinator endpoint
coordinator = http://coordinator:8080
# Branch naming
branchPrefix = task
# Conflict resolution
aiAssistedMerge = true
aiEndpoint = http://ai-service:8081/merge
# Rejected work retention
retainRejectedDays = 90
9.2 Gitattributes
# .gitattributes for Coditect repositories
# Use generation-aware merge for all code files
*.py merge=generation-aware
*.ts merge=generation-aware
*.js merge=generation-aware
*.java merge=generation-aware
*.go merge=generation-aware
*.rs merge=generation-aware
# Binary files - no merge
*.png binary
*.jpg binary
*.pdf binary
# Config files - union merge (combine all entries)
*.yaml merge=union
*.yml merge=union
*.json merge=union
# Lock files - ours strategy (keep main's version)
package-lock.json merge=ours
yarn.lock merge=ours
poetry.lock merge=ours
Cargo.lock merge=ours
10. Benefits Summary
| Aspect | Without Git Integration | With Git Integration |
|---|---|---|
| Conflict Detection | Only at task level | File and line level |
| Work Preservation | Results discarded | All work preserved in refs |
| Audit Trail | Database only | Database + Git history |
| Recovery | Manual recreation | Cherry-pick from rejected |
| Merge Strategy | Winner takes all | Intelligent merging |
| Semantic Conflicts | Undetected | AI-assisted resolution |
| Blame/History | Session logs only | Full Git blame |
| Rollback | Complex | Simple git revert |
11. References
- Joshi, Unmesh. Patterns of Distributed Systems. Chapter 9: Generation Clock.
- Chacon, Scott. Pro Git. 2nd Edition. Apress, 2014.
- Git Documentation: Custom Merge Drivers
- Coditect TDD: Generation Clock for Multi-Agent Coordination
Document Version: 1.0 | Last Updated: January 2026