Establish workspace/config/ and multi-source config resolution (3/13) - #206
Open
alex-clickhouse wants to merge 1 commit into
Open
Establish workspace/config/ and multi-source config resolution (3/13)#206alex-clickhouse wants to merge 1 commit into
alex-clickhouse wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new git-tracked workspace config layer (<workspace>/config/settings.yaml) beneath the existing machine-local config.yaml and config.local.yaml, and updates init/backup/reload paths so the new layered config is created, loaded, validated, and included in backups without breaking legacy installs.
Changes:
- Implement multi-layer config loading:
workspace/config/settings.yaml<config.yaml<config.local.yaml, with env interpolation after merge. - Update
nerve initscaffolding/writing to populate the workspace settings layer and keep re-init behavior safe via backups and ownership rules. - Ensure backups and MCP reload paths include/read the tracked workspace config subtree.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
tests/test_config_sources.py |
New tests covering multi-layer merge semantics, strictness of workspace settings, env refs, and repairability via CLI commands. |
tests/test_bootstrap.py |
Adjusts bootstrap expectations for the split between machine-local config and workspace-tracked settings; adds prompt wording test. |
tests/test_backup.py |
Adds regression test ensuring backups include workspace/config/** (settings + cron). |
nerve/workspace.py |
Adds scaffold installer for workspace/config/ from packaged templates. |
nerve/templates/config/settings.yaml |
Introduces default commented scaffold for shareable workspace settings. |
nerve/gateway/routes/mcp_servers.py |
Returns clean 400s on config errors during MCP reload instead of 500s. |
nerve/config.py |
Adds YAML mapping reader with strict mode and implements multi-source config resolution + shared helpers. |
nerve/cli.py |
Allows doctor/init to run even when config load fails; updates doctor to validate merged multi-layer view. |
nerve/bootstrap.py |
Updates setup wizard to create/write the workspace settings layer and manage backups + re-init semantics. |
nerve/backup.py |
Includes workspace/config in backup allowlist so tracked settings/cron survive restores. |
nerve/agent/engine.py |
Reload MCP config from the daemon’s config dir (not process CWD) so reload matches startup behavior. |
docs/config.md |
Documents the new 3-layer precedence model, init split/ownership, and env refs across all layers. |
Comments suppressed due to low confidence (1)
nerve/bootstrap.py:2199
- Write workspace/config/settings.yaml using an explicit UTF-8 encoding. This file is intended to safely contain non-ASCII content (prompt hints, names, etc.); relying on the process locale for encoding can cause
UnicodeEncodeErrorunderLC_ALL=Cand similar environments.
with open(settings_path, "w") as f:
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
alex-clickhouse
force-pushed
the
config-refactor/03-config-subdir
branch
from
July 28, 2026 12:11
727d3b6 to
a6022f5
Compare
alex-clickhouse
force-pushed
the
config-refactor/03-config-subdir
branch
2 times, most recently
from
July 28, 2026 13:14
d4ed397 to
eeeb051
Compare
alex-clickhouse
force-pushed
the
config-refactor/03-config-subdir
branch
from
July 28, 2026 14:23
eeeb051 to
99d0346
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
nerve/bootstrap.py:2666
- _expand_workspace() claims to expand paths the same way the config loader does, but it currently doesn’t strip whitespace and it expands user/home before env vars.
nerve.config._expand_path()strips first, then expandvars→expanduser, and strips again. With leading/trailing spaces (or values like " ${WS}"), the wizard can write settings.yaml into a different directory than the loader will later read, silently losing portable settings.
def _expand_workspace(raw: str) -> Path:
"""Expand a workspace path the way nerve.config resolves it."""
if "${" in raw:
raw = _interpolate_str(raw, [])
return Path(os.path.expandvars(os.path.expanduser(raw)))
alex-clickhouse
marked this pull request as ready for review
July 28, 2026 14:44
alex-clickhouse
force-pushed
the
config-refactor/03-config-subdir
branch
from
July 28, 2026 14:54
99d0346 to
68ef88a
Compare
alex-clickhouse
force-pushed
the
config-refactor/03-config-subdir
branch
from
July 28, 2026 15:33
68ef88a to
b1e1ea5
Compare
alex-clickhouse
force-pushed
the
config-refactor/03-config-subdir
branch
from
July 29, 2026 10:31
b1e1ea5 to
0eb465c
Compare
alex-clickhouse
force-pushed
the
config-refactor/03-config-subdir
branch
from
July 30, 2026 08:01
0eb465c to
3936790
Compare
Shareable configuration moves into <workspace>/config/settings.yaml, a git-tracked subtree, and is merged beneath the two machine-local layers: settings.yaml < config.yaml < config.local.yaml. An absent subtree keeps the previous behaviour. A settings.yaml that is not a mapping now fails loudly instead of being merged as junk, a file that exists but cannot be read is reported as such rather than read as empty, and the YAML encoding is pinned so a mis-encoded file cannot decode differently on another machine. nerve doctor stays alive on a bad config so it can report it, and nerve backup captures the new subtree. Co-Authored-By: Claude <noreply@anthropic.com>
alex-clickhouse
force-pushed
the
config-refactor/03-config-subdir
branch
from
July 30, 2026 10:06
3936790 to
94e01b4
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
Adds a third config layer:
<workspace>/config/settings.yaml, git-tracked and merged beneathconfig.yamlandconfig.local.yaml, so machine-local values still override it. With no such subtree present, resolution is byte-for-byte what it was.nerve initno longer writes everything toconfig.yaml. Its answers are split by whether the value would be wrong on another machine:config.yamlworkspace,deployment,provider.aws_profile,gateway.ssl.*,proxy,docker,telegram.enabled,sync.gmail.accounts,external_agents,mcp_endpointsettings.yamltimezone,gateway.host/port,provider.type/aws_region(with the region-scoped Bedrock model IDs),agent.*,memory.*,sessions.*,sync.*, quiet hours,telegram.dm_policy/stream_modeA key goes in exactly one layer. Re-running
nerve initrewritesconfig.yamlwholesale but mergessettings.yaml, preserving keys the wizard does not own — a team policy setting survives. Where a re-run stops emitting a key it previously wrote, the key is deleted rather than omitted, since the merge would otherwise leave it at its old value forever.Also fixes
nerve backup, which enumerates workspace directories explicitly and so stopped capturing the tracked config once it moved: bundles kept every skill and identity file and no settings.Why
The layer is the foundation for sharing config between machines, but adding it is not enough on its own, because
config.yamlshadows it. If the wizard kept writing every key toconfig.yaml,settings.yamlwould be dead on arrival — you could edit it, commit it, sync it to a second machine, and nothing would change, because the local file still wins. The split is what makes the new layer load-bearing rather than decorative.The practical test for where a key belongs is lockdown, which drops
config.yamlandconfig.local.yamlentirely. Anything left in the machine layer then falls back to its declared default on every locked box. So a key belongs there only when that default is an acceptable answer — which is whygateway.host/portandprovider.type/aws_regionare shared rather than local. Both were previously machine-local, and both are the deployment's shape rather than a per-machine fact:0.0.0.0and8900, identical to the declared defaults, so the tracked layer could not state a bind address at all and a fleet had no way to agree on a non-default port.providerblock lived only inconfig.yamllost it, silently reverted to theanthropicdefault, and then failed for a missing API key it would not otherwise have needed. The geo-scoped Bedrock model IDs (us./eu./apac.) are a function of the region alone, so once the region is tracked they can be tracked with it.provider.aws_profilestays machine-local — it names an entry in one box's AWS credentials file — as dogateway.ssl.cert/key, which are local filesystem paths. Sharing a certificate path would stop the gateway binding on any machine that does not have that file.external_agentsalso stays local for a different reason: the gateway rewrites that block at runtime on every enable/disable, and it must not be writing into a git-tracked file.🤖 Generated with Claude Code