af: layered config (global / project-shared / project-local)#207
Merged
af: layered config (global / project-shared / project-local)#207
Conversation
splits af config across three scopes co-located with astro-cli's tree (~/.astro and project .astro/), so agents running `af instance discover` inside a project no longer pollute the user's global instance list, and team-shared deployment lists can be committed alongside .astro/config.yaml Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Path.cwd() raises FileNotFoundError when the cwd was deleted out from under the process. The walk-up needs to catch that — there's no filesystem position to walk from, so layering just gracefully skips. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
schnie
approved these changes
May 4, 2026
Member
schnie
left a comment
There was a problem hiding this comment.
Makes sense. Consolidate on astro config location and adds project and project local configs for better usage with Otto across many projects.
1 task
jlaneve
added a commit
that referenced
this pull request
May 4, 2026
## Summary - The layered-config PR (#207) moved the default global path from `~/.af/config.yaml` to `~/.astro/config.yaml` but two user-facing strings in `cli/main.py` still pointed at the old location. - This brings `af --help` in line with the README and skill docs. ## Test plan - [x] `af --help` shows `(default: ~/.astro/config.yaml)` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
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.
summary
afnow reads and writes config across three scopes — global, project-shared, project-local — colocated with the Astro CLI's existing~/.astro/directory. The model mirrorsgit config(system / global / local). This lets agents runaf instance discoverinside a specific astro project without polluting the user's global instance list, and lets a team commit their project's deployment list to.astro/config.yamlso a fresh clone +astro loginis enough to be productive.why
today, every
af instance ...operation reads and writes~/.af/config.yaml. That has two real problems:af instance discoverwill dump every deployment the user has access to (often dozens) into the user's global config, mixing with deployments from unrelated projects.the mental model
~/.astro/config.yaml<root>/.astro/config.yaml<root>/.astro/config.local.yamlcurrent-instanceand per-developer overrides<root>is found by walking up from cwd looking for a.astro/directory (same marker astro-cli already uses). Once you're in a project,af instance discoverpopulates the team's deployment list directly into the committable file. The instances are stored withastro_patauth (onlycontext+deployment_id, no token), so committing the file leaks nothing — every developer'sastro loginresolves their own PAT at request timeread precedence (most-specific wins):
current-instancefrom project-local, then global. Instance lookup: project-local → project-shared → global; same-named entries in narrower scopes shadowwrite routing (default, no flags):
add→ project-shared inside a project, else globaluse→ project-local (each developer can target a different deployment without touching the committed file)delete→ most-specific scope that has the name (rerun to peel further scopes)discover→ project-shared inside a project, else globalhow to use it
setting up a project (the main use case)
inside an astro project:
teammate clones the repo:
scope flags (override the default)
mutually-exclusive
--global/--project/--localflags onadd/use/delete/discover:af instance show <name>— answer "where is this defined?"mirrors
git config --show-originfor cases where the same name lives in multiple scopes (most-specific wins; the rest are shadowed)af migrate— move from~/.af/config.yamlto~/.astro/config.yamlif you've used
afbefore this PR, your config lives at~/.af/config.yaml. On first run after upgrade, af reads from the legacy path with a one-time stderr deprecation note. Runaf migrateto do the migration explicitly:idempotent. The migration preserves any astro-cli content already in
~/.astro/config.yamlvia the same merge logic save() uses. Your old file is renamed to.baksingle-file mode (escape hatch)
AF_CONFIG=<path>(or--config <path>) bypasses layering entirely. Theastro ottowrapper'sAF_CONFIG=/dev/nullneutralize-config sentinel works exactly as beforewhat changed under the hood
config/loader.py—ConfigManager.save()now reads existing file content and merges only af-owned top-level keys (instances,current-instance), preserving everything else (project:,cloud:,contexts:from astro-cli). Telemetry is sub-key merged so astro-cli'snotice_shownsurvives. Output issort_keys=Trueso cross-tool writes don't churn diffs. File mode is preserved on overwrites and tightened to0600on creation. Newcreate_default_if_missingflag for project layers (defaultTruepreserves old behavior for the global file)config/models.py— droppedvalidate_references(in layered world,current-instancecan legitimately point to a sibling-scope instance), relaxedTelemetrytoextra="ignore"config/scope.py(new) —Scopeenum +discover_project_root()walk-up logicconfig/layered.py(new) —LayeredConfigcomposes the three managers; handles merged-view reads, scope-routed writes, danglingcurrent-instancecleanup across scopescli/context.py—_load_from_config()switched toLayeredConfigcli/instances.py— every command moved toLayeredConfig. New--global/--project/--localflags. Newshowcommand. New SCOPE column inlist. Discover commands fail-fast on invalid scope before doing API/scan workcli/main.py— new top-levelmigratecommandaf no longer polices what credentials you put in which file. Convention is gitignore, not tool gating — same as git/terraform/kubectl/aws-cli/gcloud
test plan
test_scope.py,test_layered.py,test_cli_instances.py,test_cli_migrate.py, plus expansions totest_config.py)discover astro --project --dry-runpreviews 30 deployments without writing, realdiscover astro --projectwrites them with correct schema,af config versionagainst a HEALTHY discovered deployment returns the live response (proves PAT resolution from the merged~/.astro/config.yamlworks at request time)project,contexts, future astro-cli keys) injected mid-stream survive subsequentaf instance use/addwritesaf migratehappy path, idempotent (already-migrated/nothing-to-migrate),.baknumbering when.bakalready exists, malformed legacy YAML errors cleanlyknown follow-ups (not blocking)
af instance usewith no name in a non-tty raises asimple_term_menutraceback. Pre-existing; should detectsys.stdin.isatty()and error cleanlycli/main.py:init_context()doesn't lazy-init.gitignoretemplate addition for.astro/config.local.yaml(was deferred per discussion)🤖 Generated with Claude Code