Files
converge/AGENTS.md
T
jbrechtel f546ee450e
Build / build (push) Failing after 12m9s
Add converge-wake.service and systemd-sleep hook for wake-from-sleep sync
Creates a oneshot user service (converge-wake.service) that runs
converge --sync after the network is online, and a systemd-sleep hook
(/usr/lib/systemd/system-sleep/converge-wake) that triggers the service
for each logged-in user on resume.

Uses After=network-online.target for proper network timing on
NetworkManager/systemd-networkd setups, with a 5-second ExecStartPre
delay as a safety net for netctl and other environments where
network-online.target may not gate on resume.

Updated PKGBUILD and AGENTS.md accordingly.
2026-05-23 22:38:55 -04:00

8.9 KiB

AGENTS.md — Project guide for AI coding agents

Project overview

Converge watches git repositories for file changes, automatically commits them, and pulls from a remote via rebase. It notifies the user via notify-send on merge conflicts.

  • Language: Haskell (GHC 9.10.3, Stackage lts-24.38)
  • Build system: Stack (config in stack.yaml, packages in package.yaml)
  • Cabal file: converge.cabal is generated from package.yaml via hpack

File layout

src/Converge.hs      # Single library module — all core logic
app/Main.hs          # CLI entry point (optparse-applicative)
test/Spec.hs         # Test runner (imports and runs all spec modules)
test/TestHelper.hs   # Shared test utilities (temp repos, sentinel files)
test/GitCommitSpec.hs   # Commit behaviour tests
test/GitPullSpec.hs     # Pull/rebase tests
test/GitSafetySpec.hs   # Operation-in-progress detection tests
SPEC.md                 # Human-readable specifications
converge.example.yaml    # Example config file
converge.service         # systemd user service (daemon mode)
converge-wake.service    # systemd user service (wake-from-sleep sync)
src/system-sleep/        # systemd-sleep hook scripts

Architecture

All core logic lives in the single Converge module (src/Converge.hs). Key sections:

  • Types: Options, RepoConfig, Config, Logger (an MVar () for serialized logging)
  • Config: YAML config file parsing via Data.Yaml, XDG path discovery
  • File watching: fsnotify via watchRepo — one per configured repo, each in its own thread (via async)
  • Debounce: An MVar pattern coalesces rapid events into one sync per quiet period
  • Sync cycle (syncRepo): guarded by isInMiddleOfOperation, then stage + commit + pull + conflict check
  • Git operations: shell out to git via System.Process.readProcessWithExitCode — no libgit2 dependency
  • Notifications: shell out to notify-send via System.Process.spawnProcess
  • Relay listener: connectRelay connects to a converge-relay SSE endpoint, parses push events, and triggers pulls on matching repos (by gitea_repo + branch). Runs as a background thread with exponential backoff reconnection.

Relay architecture

┌─────────────┐  POST /webhook   ┌──────────────────┐  GET /events (SSE)   ┌──────────────────┐
│   Gitea     │ ────────────────> │  converge-relay  │ ───────────────────> │  converge (local) │
│   Server    │   (push event)    │   (Go, hosted)   │  event: push        │   (Haskell)       │
└─────────────┘                   └──────────────────┘                     └─────────┬─────────┘
                                                                                    │
                                                                             matches gitea_repo
                                                                             → git pull

The Go relay is a zero-dependency pub/sub server. Key design:

  • POST /webhook — receives Gitea push events, optional HMAC-SHA256 verification
  • GET /events — SSE stream, clients connect and hold the connection open
  • Channel-based broker: Subscribe() → channel, Unsubscribe(ch), Publish(event) — non-blocking send to all subscribers
  • Slow subscribers are silently dropped (channel buffer = 16)

Git operations

Function Git command
gitCommitAll git add -A + git commit -m "converge: auto-commit from <host>"
gitPull git pull --rebase <remote> <branch>
hasConflicts git diff --check looking for <<<<<<< / ======= / >>>>>>>
isInMiddleOfOperation git rev-parse --git-dir then checks sentinel files/dirs

Operation detection sentinels

checkIndicatorFiles checks these inside .git/:

File/Dir Operation
MERGE_HEAD Merge in progress
REVERT_HEAD Revert in progress
CHERRY_PICK_HEAD Cherry-pick in progress
BISECT_LOG Bisect in progress
rebase-apply/ Rebase in progress
rebase-merge/ Rebase in progress
sequencer/ Sequencer-based operation in progress

REBASE_HEAD is intentionally excluded — git does not remove it when a rebase completes, causing false positives.

Build / test / run

Haskell (converge)

stack build                          # Build library + executable + tests
stack test                           # Build and run the test suite
stack exec converge -- [options]     # Run the tool directly
./scripts/build                      # Format + build + compress (uses Docker via ./hs)
./scripts/test                       # Run tests (uses Docker via ./hs)
./scripts/run                        # Run the tool (uses Docker via ./hs)

Go (converge-relay)

cd converge-relay
./scripts/build                      # Format + test + build (uses Docker via ./goctl)
./scripts/test                       # Run tests (uses Docker via ./goctl)
./scripts/run --listen :8080         # Run the relay (uses Docker via ./goctl)

Test conventions

Tests use hspec and create real temporary git repositories — no mocking of git commands. This avoids complexity and tests actual git semantics.

Helper module (test/TestHelper.hs)

  • withTempGitRepo :: (FilePath -> IO a) -> IO a — creates a temp dir, runs git init -b main, configures user, makes an initial commit, runs the action, cleans up.
  • withTempGitRepoWithRemote :: (FilePath -> IO a) -> IO a — like above but also creates a bare remote, adds it as origin, and pushes the initial commit.
  • runGitIn :: FilePath -> [String] -> IO (ExitCode, Text, Text) — runs git -C <path> <args>.
  • rawGit :: [String] -> IO (ExitCode, Text, Text) — runs git <args> without -C (for git clone, etc.).
  • testLogger :: IO Logger — creates a test Logger backed by a fresh MVar.
  • Sentinel helpers (createMergeHead, createRebaseApply, etc.) — create the known sentinel files/dirs inside .git/.

Writing new tests

  1. Create a new spec module in test/ (e.g., test/MyNewSpec.hs)
  2. Export a top-level spec :: Spec value
  3. Import it in test/Spec.hs and add it to the hspec block
  4. Use withTempGitRepo for tests that need a git repo
  5. If the test needs new exports from Converge, add them to the module's export list

Spec coverage

Tests are organized to match SPEC.md:

Anytime new behavior is added the SPEC.md should be updated and a corresponding hspec test written.

Spec Test module Key tests
Creates commit on file change GitCommitSpec Commit creation, content inclusion, no duplicate commits
One commit per debounce window GitCommitSpec Idempotency of gitCommitAll
Pulls from remote origin GitPullSpec Fast-forward pull scenario
Rebases on pull GitPullSpec Divergent history → linear history, no merge commits
No-op during operations GitSafetySpec All 7 sentinel types + clean repo
RelaySpec SSE line parsing, event folding, JSON parsing, repo matching, operation-in-progress skip

Key conventions

  • Default extensions (from package.yaml): OverloadedStrings, RecordWildCards, ScopedTypeVariables, TupleSections
  • GHC options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints
  • Strings are Text (via OverloadedStrings), not String
  • Logging goes through the Logger newtype wrapping MVar () — all logging is serialized to stderr
  • Hostname for commit messages is obtained via readProcess "hostname" [] ""
  • Git commands always use readProcessWithExitCode (not readProcess except for hostname)

Dependencies

Haskell

Package Purpose
async Concurrent repo watching
fsnotify File system event monitoring
optparse-applicative CLI argument parsing
yaml Config file parsing
http-client HTTP client for SSE relay connection
http-client-tls TLS support for HTTPS relay endpoints
http-types HTTP status codes
aeson JSON parsing (push events from relay)
bytestring Efficient binary data for SSE stream parsing
temporary (test only) Temp directories for test repos
hspec (test only) Test framework

Go (converge-relay)

Zero external dependencies — stdlib only (net/http, log/slog, crypto/hmac, encoding/json, sync).

External requirements

  • git must be on PATH (no libgit2 dependency)
  • notify-send for desktop notifications (optional — only called on conflict/pull)
  • hostname command for commit message generation
  • For realtime pull: a running converge-relay instance reachable via HTTP(S)