Git-backed workspace sync (9/13) - #212
Open
alex-clickhouse wants to merge 1 commit into
Open
Conversation
There was a problem hiding this comment.
Pull request overview
Adds git-backed workspace synchronization to Nerve so instances can safely pull reviewed config changes from a remote repo, validate them before landing on disk, and hot-reload impacted subsystems.
Changes:
- Introduces
nerve.sync_servicewith a fetch → validate (via detached worktree) → ff-merge flow, plus a periodic daemon sync loop. - Adds operator entry points:
nerve config syncCLI andPOST /api/config/sync, and wires the periodic loop into the gateway lifespan when enabled. - Extends config + validation to support workspace sync settings and clearer reporting of which config layer references missing environment variables; serializes cron reload to avoid concurrent mutations.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/test_sync_service.py | Comprehensive unit + real-git + route + periodic-loop tests for guarded sync and never-raise behavior |
| tests/test_config_validate.py | Tests for layer-attribution messaging for missing ${VAR} references |
| nerve/sync_service.py | Implements guarded git sync, validation worktrees, local-change blocking, and periodic loop/apply logic |
| nerve/gateway/server.py | Starts/stops periodic workspace sync task during gateway lifespan (opt-in) |
| nerve/gateway/routes/config.py | Adds authenticated POST /api/config/sync route to sync + apply changes |
| nerve/gateway/routes/init.py | Registers the new config/sync router |
| nerve/cron/service.py | Serializes CronService.reload() with an async lock to prevent interleaved scheduler mutations |
| nerve/config.py | Adds WorkspaceSyncConfig and integrates it into NerveConfig construction |
| nerve/config_validate.py | Enhances missing-env-var messaging to attribute refs to machine vs workspace layers |
| nerve/cli.py | Adds nerve config sync command with --no-validate and --no-strict-env flags |
| docs/config.md | Documents workspace sync behavior, safety guarantees, and operational constraints |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+269
to
+273
| add = _git(["worktree", "add", "--detach", str(wt), rev], workspace) | ||
| if add.returncode != 0: | ||
| return ValidationResult( | ||
| errors=[f"could not create validation worktree: {add.stderr.strip()}"] | ||
| ) |
alex-clickhouse
force-pushed
the
config-refactor/09-git-sync
branch
from
July 28, 2026 12:11
2cac386 to
d31674f
Compare
alex-clickhouse
force-pushed
the
config-refactor/09-git-sync
branch
from
July 28, 2026 12:36
d31674f to
24c86b5
Compare
alex-clickhouse
force-pushed
the
config-refactor/09-git-sync
branch
from
July 28, 2026 13:14
24c86b5 to
ee4567f
Compare
alex-clickhouse
force-pushed
the
config-refactor/09-git-sync
branch
from
July 28, 2026 14:23
ee4567f to
948f638
Compare
alex-clickhouse
force-pushed
the
config-refactor/09-git-sync
branch
from
July 28, 2026 14:54
948f638 to
11001fc
Compare
alex-clickhouse
force-pushed
the
config-refactor/09-git-sync
branch
from
July 28, 2026 15:33
11001fc to
a7eff12
Compare
alex-clickhouse
force-pushed
the
config-refactor/09-git-sync
branch
from
July 28, 2026 16:03
a7eff12 to
f6bf5fd
Compare
alex-clickhouse
force-pushed
the
config-refactor/09-git-sync
branch
from
July 28, 2026 16:20
f6bf5fd to
116482b
Compare
alex-clickhouse
force-pushed
the
config-refactor/09-git-sync
branch
2 times, most recently
from
July 29, 2026 08:29
266996c to
cb5e04d
Compare
alex-clickhouse
force-pushed
the
config-refactor/09-git-sync
branch
2 times, most recently
from
July 29, 2026 09:31
7210d9e to
58f9bbc
Compare
alex-clickhouse
force-pushed
the
config-refactor/09-git-sync
branch
from
July 29, 2026 10:31
58f9bbc to
9b5077a
Compare
alex-clickhouse
force-pushed
the
config-refactor/09-git-sync
branch
from
July 29, 2026 13:28
9b5077a to
be4fa9b
Compare
alex-clickhouse
force-pushed
the
config-refactor/09-git-sync
branch
from
July 30, 2026 08:01
be4fa9b to
b50ad41
Compare
alex-clickhouse
marked this pull request as ready for review
July 30, 2026 08:17
alex-clickhouse
force-pushed
the
config-refactor/09-git-sync
branch
from
July 30, 2026 08:25
b50ad41 to
6cc6bf3
Compare
alex-clickhouse
force-pushed
the
config-refactor/09-git-sync
branch
from
July 30, 2026 10:06
6cc6bf3 to
8e4bcd7
Compare
alex-clickhouse
force-pushed
the
config-refactor/09-git-sync
branch
from
July 31, 2026 08:19
8e4bcd7 to
9e44f4e
Compare
The workspace is treated as a git repo synced with a remote: a manual command, a configurable periodic loop, and a trigger route. The order is fetch, validate, then merge, so a bundle that fails validation never lands on disk — validating after the merge would leave the bad config in the working tree with nothing but a log line to say so. Fast-forward only, and a dirty config subtree refuses the merge rather than overwriting local edits. Sync validates with strict env, because the daemon will need those variables at load time and a lenient pass would wave through a bundle it cannot actually run. It honours its never-raises contract, so a workspace value that is null or a list reports a 400 rather than a stack trace, and the periodic loop reads config fresh each pass instead of holding a reference from startup. An unresolved variable the bundle itself needs is not blamed on the machine layers, and a path is reported by its real name rather than C-escaped. Co-Authored-By: Claude <noreply@anthropic.com>
alex-clickhouse
force-pushed
the
config-refactor/09-git-sync
branch
from
July 31, 2026 08:36
9e44f4e to
08ede6b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
nerve syncplus an opt-in periodic loop. Fetch, validate, then merge, in that order, so a bundle that fails validation never reaches disk. On a successful merge the subsystems hot-reload.Fast-forward only. Refuses to merge into a config subtree with local modifications, and reports
ok: falsewith the apply error rather than claiming a change took effect when it did not.The loop pulls every
workspace_sync.interval_minutes, default 1. It is the only thing that applies a merged change on its own, so that interval is the upper bound on how stale an instance can be; a fetch that finds nothing new is one round trip. Raise it for a large fleet pointed at one repo.Shutdown goes through
stop_background_task(#209) rather than setting the stop event and cancelling in the same breath, which is the anti-pattern that helper exists to remove. Being straight about the size of it here: the git phase runs in a worker thread and is out of reach of cancellation either way, and a reload that never ran before the process exits is moot because start-up reads config fresh. What it buys is that this loop stops the same way the other supervised loop does, so the codebase does not keep one hand-rolled copy of a bug it fixed elsewhere — and it is already correct for the day something restarts this loop without restarting the process.Why
This is what the tracked layer is for: config lives in a git repo, is reviewed there, and arrives on the box by merge rather than by hand. Validating before the merge is the ordering that makes it safe — validating after would mean the bad config is already the config, and rolling back is a second operation that can also fail.
Fast-forward only, and refusing a dirty subtree, both exist so that sync never has to resolve a conflict. A merge commit or a stashed local edit means the config on the box is no longer the config that was reviewed, which defeats the point of reviewing it.
Scoring the result honestly matters more here than elsewhere, because sync runs unattended.
okanswers "did the merge take";appliedanswers "did every subsystem pick it up". Collapsing those into one boolean would either report failure for a merge that succeeded, or success for a box running the old configuration.The cadence is short because this loop is now the whole automatic path. An earlier revision of the stack also had a cron-directory file watcher and this loop deliberately skipped its own cron reload when that watcher was running, on the theory that the watcher would see the merge's file change. The watcher is gone (see #209), so the interlock is gone with it: a pull always reloads cron, and the two mechanisms can no longer disagree about which one applied a change.
🤖 Generated with Claude Code