Skip to content

Git-backed workspace sync (9/13) - #212

Open
alex-clickhouse wants to merge 1 commit into
config-refactor/08-migratefrom
config-refactor/09-git-sync
Open

Git-backed workspace sync (9/13)#212
alex-clickhouse wants to merge 1 commit into
config-refactor/08-migratefrom
config-refactor/09-git-sync

Conversation

@alex-clickhouse

@alex-clickhouse alex-clickhouse commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

What

nerve sync plus 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: false with 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. ok answers "did the merge take"; applied answers "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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_service with a fetch → validate (via detached worktree) → ff-merge flow, plus a periodic daemon sync loop.
  • Adds operator entry points: nerve config sync CLI and POST /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 thread nerve/sync_service.py
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
alex-clickhouse force-pushed the config-refactor/09-git-sync branch from 2cac386 to d31674f Compare July 28, 2026 12:11
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/09-git-sync branch from d31674f to 24c86b5 Compare July 28, 2026 12:36
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/09-git-sync branch from 24c86b5 to ee4567f Compare July 28, 2026 13:14
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/09-git-sync branch from ee4567f to 948f638 Compare July 28, 2026 14:23
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/09-git-sync branch from 948f638 to 11001fc Compare July 28, 2026 14:54
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/09-git-sync branch from 11001fc to a7eff12 Compare July 28, 2026 15:33
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/09-git-sync branch from a7eff12 to f6bf5fd Compare July 28, 2026 16:03
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/09-git-sync branch from f6bf5fd to 116482b Compare July 28, 2026 16:20
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/09-git-sync branch 2 times, most recently from 266996c to cb5e04d Compare July 29, 2026 08:29
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/09-git-sync branch 2 times, most recently from 7210d9e to 58f9bbc Compare July 29, 2026 09:31
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/09-git-sync branch from 58f9bbc to 9b5077a Compare July 29, 2026 10:31
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/09-git-sync branch from 9b5077a to be4fa9b Compare July 29, 2026 13:28
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/09-git-sync branch from be4fa9b to b50ad41 Compare July 30, 2026 08:01
@alex-clickhouse
alex-clickhouse marked this pull request as ready for review July 30, 2026 08:17
@alex-clickhouse
alex-clickhouse force-pushed the config-refactor/09-git-sync branch from b50ad41 to 6cc6bf3 Compare July 30, 2026 08:25
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
alex-clickhouse force-pushed the config-refactor/09-git-sync branch from 6cc6bf3 to 8e4bcd7 Compare July 30, 2026 10:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants