dracon-sync
Invisible git sync for AI-powered development. An auto-commit, multi-mirror daemon that watches your repos, commits every change with deterministic, facts-based messages, and pushes to GitHub, GitLab, and Codeberg simultaneously.
Why This Exists
Other tools solve parts of the problem:
- git-auto-sync: Auto-commits a single repo, no mirroring
- gitea-mirror: One-way mirror to a single Forgejo instance, no auto-commit
- git-bridge: Multi-provider sync, but no auto-commit or AI
- swarf: Invisible sync for AI agents, but only a side-band directory
dracon-sync is the only tool that combines all of these into one daemon:
| Capability | git-auto-sync | gitea-mirror | git-bridge | swarf | dracon-sync |
|---|---|---|---|---|---|
| Auto-commit on change | ✅ | ❌ | ❌ | ✅ | ✅ |
| Multi-repo watch | ❌ | ✅ | ✅ | ❌ | ✅ |
| Multi-provider mirror | ❌ | ✅ (→1) | ✅ | ❌ | ✅ (3+) |
| Deterministic commit messages | ❌ | ❌ | ❌ | ❌ | ✅ |
| Version bump + release | ❌ | ❌ | ❌ | ❌ | ✅ |
| Safety guards | ❌ | ❌ | ❌ | ❌ | ✅ |
| Visibility sync | ❌ | ❌ | ❌ | ❌ | ✅ |
| Broken tracking repair | ❌ | ❌ | ❌ | ❌ | ✅ |
Features
Invisible Infrastructure
The AI (or human) works on one repo at a time, makes changes, and sync handles the rest — the AI never needs to think about commits, pushes, or cross-repo coordination.
- You edit files
- Sync detects changes within seconds
- After a brief inactivity delay (5s default), sync commits with a deterministic message
- Pushes to origin (GitHub) and all mirror remotes (GitLab, Codeberg)
- Done — no manual git commands needed
HTTPS + PAT Transport (GitHub)
GitHub origin uses HTTPS with Personal Access Tokens — more reliable than SSH (no agent timeouts, no key rotation). GitLab and Codeberg mirrors use SSH by default, with HTTPS PAT fallback on SSH failures.
Automatic Remote Creation
When auto_github_private = true, newly initialized repos without an origin remote automatically get a private GitHub repository created via gh, with the remote added and initial commit pushed.
Deterministic Sync
- Monitors repositories for changes across watched roots
- Commits, pulls, and pushes automatically based on policy
- Respects freeze markers (e.g., during deployments)
Self-Healing
- Detects and repairs common git issues (conflicted remotes, stuck pushes)
- Repairs broken upstream tracking refs (e.g.
origin/master: gone) - Consolidates dual main/master branch repos to main
- Manages permanently stuck repos
- Prunes stale operational state on daemon restart (stuck repos, incident ledger, visibility cache)
Commit Messages
Deterministic facts extracted from the diff. No AI, no LLM, no prose. Routing
keys are grep-searchable via git log --grep=.
Installation
Quick Install (User Service)
Run the repository installer from the repository root:
This will:
- Build the release binary
- Install to
~/.local/bin/dracon-sync - Set up and start the systemd user service
The per-utility directories do not contain standalone installers; use the root install.sh for all utilities.
Manual Install
# Build
# Copy binary
# Install systemd service
Usage
Commands
# Show policy path, watched roots, and discovered repos
# One-shot sync across all discovered repos
# Run continuous sync daemon (default: 1s pulse interval)
# Override the pulse interval from CLI
# Sync a specific repository now
# Sync repos currently reported as WARN (dirty-only triage)
# Edit the sync policy
# Validate the sync policy
# Report across all repos
# Repair concern repos (dry-run by default)
# Repair warn repos
# Manage stuck repos
# Manage dual-branch repos
# Repair orphan origin URLs (e.g. after remote rename)
# Scaffold standard files (LICENSE, optional .github/FUNDING.yml, ...)
# FUNDING.yml is Dracon-specific; external users must opt in explicitly.
# Manually publish to registries
# Check daemon health and metrics
Systemd Service Management
# Check status
# View logs
# Restart after config changes
Configuration
Create ~/.dracon/utilities/sync/dracon-sync.toml:
[]
# Watch directories for git repositories
= ["/home/user/Dev", "/home/user/work"]
# Pulse interval in seconds (how often to scan for changes)
= 1
# Delay after last change before auto-push (seconds)
= 5
# Auto git operations
= true
= true
= true
= true
# Auto-repair concerns and warnings
= true
= true
# Automatic private GitHub remote creation
# When a repo has no origin remote, creates a private GitHub repo,
# adds the HTTPS remote, and pushes the initial commit.
= true
= "YourOrgOrUsername"
# Exclude specific repos or directories
= ["/home/user/Dev/archived"]
= ["node_modules", "target", ".venv"]
# Sync freeze (for use with dracon-system disk guard)
= true
Automatic Remote Creation
If auto_github_private = true in your policy, any git init in a watched root will automatically:
- Create a private GitHub repo via
gh repo create --private - Add the HTTPS remote:
git remote add origin https://github.com/account/repo.git - Push the initial commit:
git push -u origin HEAD
Requirements:
ghCLI must be installed and authenticated (gh auth login)auto_github_private_accountmust match your GitHub username or org
Multi-Provider Mirrors
Push to multiple providers simultaneously. GitHub uses HTTPS + PAT; others use SSH with HTTPS fallback:
[[]]
= "github"
= "https://github.com/DraconDev/{repo}.git"
[[]]
= "gitlab"
= "git@gitlab.com:dracondev/{repo}.git"
= true
[[]]
= "codeberg"
= "git@codeberg.org:dracondev/{repo}.git"
= false # Codeberg/Forgejo doesn't support push-to-create
Store PATs for HTTPS fallback and API operations:
# GitLab
# Codeberg
Commit Messages
Commit messages are deterministic facts extracted from the diff — no AI, no inference.
Format
[INTENT] | N file(s) in DIRS [files] DELTA:+A/-B [METRICS]
- INTENT — task state transitions from markdown (
CLOSED: task1, task2orWIP: task1), capped at 10 tasks - DIRS — top-level directories touched (root files show no
inclause) - DELTA — lines added/removed
- METRICS —
TEST:,NEW:,DEL:,DEPS:,BIN:,TESTONLY:,ENV:,TAG:,MERGE:,REVERT:
Why Mechanical Messages?
LLM-scribed commit messages were removed — they hallucinated context and the AI reads the diff anyway. Mechanical facts are searchable (git log --grep="JWT"), honest, and compact.
Startup Cleanup
On daemon start/restart, sync prunes stale operational state:
- Stuck repos: Removes entries from stuck-push tracking for repos no longer stuck
- Incident ledger: Enforces retention policy (keeps last N entries per
incident_retention) - Alert ledger: Keeps a JSONL history of desktop-sync alert attempts under
~/.local/state/dracon/dracon-sync-alerts.jsonl - Visibility cache: Removes orphan
.lastfiles for repos no longer watched - Broken tracking: Repairs
origin/master: gonerefs → re-points toorigin/{branch} - Stale index.lock: Removes
.git/index.lockfiles with no holding process (left by crashed git operations). Without this, a stale lock blocks all git operations in that repo.
Broken tracking repair also runs every ~300 cycles (~5 min) in the daemon loop, since new :gone tracking breaks can appear at runtime.
Daemon Reliability
Push timeouts: Default push_op_timeout_secs=60 (was 300). A hanging mirror push blocks the entire daemon — no other repos get synced until it times out. With 3 mirrors at 300s each, a single repo could block the daemon for 15 minutes. 60s per push / 120s per repo keeps the daemon responsive.
Filter-only cooldown: Repos with clean/smudge filter changes (e.g. dracon-warden encryption) show as dirty in git status but have no diff after staging. The daemon detects this, resets the staging area, and applies a cooldown to prevent tight re-check loops.
Fingerprint-based scheduling: The daemon uses a fingerprint (branch + effective_dirty + staged + ahead + behind) to determine if a repo needs syncing. Only after the fingerprint stays stable for inactivity_push_delay_secs (default 5s) does the daemon attempt a sync.
Push Failure Decision Tree
When a push fails, dracon-sync follows this decision tree to recover:
Push Attempt
├── SSH push with hardening (ConnectTimeout=10, ConnectionAttempts=2)
│ ├── Success → Done ✅
│ └── Failure → Continue
├── Retry loop (configurable retries, linear backoff 1-5s)
│ ├── Success → Done ✅
│ └── Failure → Continue
├── Transport fallback (SSH → HTTPS)
│ ├── GitHub HTTPS (no token needed for public repos)
│ ├── GitLab HTTPS (requires GITLAB_TOKEN)
│ ├── Codeberg HTTPS (requires CODEBERG_TOKEN)
│ ├── Success → Done ✅
│ └── All fail → Continue
└── Final failure handling
├── Diverged (ahead > 0 AND behind > 0) → Mark as stuck, skip
├── Clean + ahead > 0 + 3 failures → Mark as stuck, skip
└── Other → Log incident, continue to next repo
Failure Modes and Recovery
| Failure Type | Cause | Recovery |
|---|---|---|
| SSH timeout | Network issue, SSH agent not running | HTTPS fallback, retry |
| SSH auth failed | Expired key, permission denied | HTTPS fallback with PAT |
| HTTPS auth failed | Expired/missing PAT | Check token in secrets/ |
| Non-fast-forward | Remote has diverged | Merge/pull or manual resolution; stuck tracking prevents repeated failed pushes |
| Rejected | Branch protection, permission denied | Manual intervention needed |
| Network unreachable | DNS failure, firewall | Retry with backoff |
| Timeout | Hanging connection, large repo | Progress-aware push/pull timeout; failed operations retry or surface in repos/health |
Stuck Push Detection
A repo is marked as "stuck" when:
- Diverged:
ahead > 0 AND behind > 0— requires manual resolution - Clean + ahead + failures: Repo has no uncommitted changes, has unpushed commits, and push has failed 3+ times — indicates a permanent issue (deleted remote, permission denied, etc.)
Stuck repos are tracked in ~/.local/state/dracon/dracon-sync-stuck-push-repos.json and skipped until manually unstuck:
Push Timeouts
| Setting | Default | Purpose |
|---|---|---|
push_op_timeout_secs |
60s | Per-push timeout for SSH or HTTPS; active pack-transfer progress extends the idle deadline |
pull_op_timeout_secs |
10s | Per-pull timeout with the same progress-aware behavior |
push_retries |
3 | Number of SSH retry attempts |
repo_sync_timeout_secs |
120s | Compatibility/status field retained for policy visibility; per-operation progress-aware timeouts control network work |
With defaults, each network operation uses a progress-aware timeout so active transfers are not killed while idle/stalled operations are. Mirror push failures are retried and surfaced through repo reports, health checks, and incident logs.
Report Accuracy
The repos command shows real dirty file counts from libgit2's get_status(), not filtered counts. The OK/WARN/CONCERN status uses has_sync_relevant_dirty_entries() (which excludes target/, node_modules/, oversized files, etc.), but the MOD/STG columns always show the actual number of modified/staged files. Previously, when effective_dirty was false (all changes excluded by policy), the report showed 0 — making repos with dozens of uncommitted files appear clean.
Version