diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d282ae9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,131 @@ +# 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 unit file +``` + +## 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` + +### Git operations + +| Function | Git command | +|---|---| +| `gitCommitAll` | `git add -A` + `git commit -m "converge: auto-commit from "` | +| `gitPull` | `git pull --rebase ` | +| `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 + +```bash +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) +``` + +## 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 `. +- `rawGit :: [String] -> IO (ExitCode, Text, Text)` — runs `git ` 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`: + +| 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 | + +## 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 + +| Package | Purpose | +|---|---| +| `async` | Concurrent repo watching | +| `fsnotify` | File system event monitoring | +| `optparse-applicative` | CLI argument parsing | +| `yaml` | Config file parsing | +| `temporary` (test only) | Temp directories for test repos | +| `hspec` (test only) | Test framework | + +## External requirements + +- `git` must be on PATH (no libgit2 dependency) +- `notify-send` for desktop notifications (optional — only called on conflict) +- `hostname` command for commit message generation