Skip to content

Deployment scheme: layered settings + the Brewfile host contract - #133

Open
JPDuchesne wants to merge 16 commits into
mainfrom
jpd/dev-102-host-baseline-brew-only
Open

Deployment scheme: layered settings + the Brewfile host contract#133
JPDuchesne wants to merge 16 commits into
mainfrom
jpd/dev-102-host-baseline-brew-only

Conversation

@JPDuchesne

@JPDuchesne JPDuchesne commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Host layer for dev#102 (plans#26), final shape: the host baseline is no longer a manifest dev fetches and converges — it is a Homebrew contract shipped by the org's deployment formula. And dev's dispatch is now project-optional, so dev up on a fresh box (no dev.yml anywhere) converges the host and succeeds.

  • Deployment scheme: dev-core is the public core formula; each org ships a thin dev formula depending on it that installs config.yml and an org Brewfile into $(brew --prefix)/etc/dev/ (pkgetc).
  • Host converge (Dev::Host::Converge, first step of every dev up): scoped self-update — brew upgrade <deployment_formula>, the setting validated against brew's canonical token shape (bare or fully tap-qualified, lowercase, @/+ allowed) — then brew bundle against the org Brewfile. Warn-only: a failed host step never blocks project provisioning. No self-update throttle; brew is its own rate limiter.
  • Layered settings: Dev::Settings resolves each key gitconfig-style — ENV → user file (~/.config/dev/config.yml) → system file (pkgetc, shipped by the deployment formula). The new dev config global builtin manages the user layer.
  • Project-optional dispatch: ExecutionContext splits into a host half (always present) and a nilable ProjectContext; Runner constructs without a dev.yml and serves a projectless catalog of exactly up, wired with a builtin-only executor and a null dependency service. up is a hybrid command — its host half (converge + dev cd RC hook) always runs; outside a project it prints the bootstrap message and exits 0. bin/dev rescues nothing: the no-dev.yml refusal and the removed-ruby:-key error both map inside Runner#exit_for, at the layer that owns the run.

Verification

  • 981 tests, 0 failures; rubocop and srb tc clean.
  • New coverage: converge failure branches (offline update, failed upgrade/bundle) and the real executor's status mapping; a formula-token Where table (tap-qualified @/+ accepted, two-segment and uppercase rejected); projectless Runner paths (context assembly, up-only catalog, no-dev.yml refusal, ruby:-key mapping inside run); the hybrid up outside a project; the builtin-only executor's wiring guard; the null dependency service.

JPDuchesne and others added 2 commits August 23, 2026 09:38
@codecov

codecov Bot commented Aug 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

JPDuchesne and others added 2 commits August 23, 2026 09:52
Co-authored-by: Cursor <cursoragent@cursor.com>
dev is public and hardcodes no org content, so the baseline manifest
moves out of dev's distribution: the org names its repo in the new
baseline_repo setting and dev reads baseline/dependencies.rb from it,
caching a machine-local copy next to the stamp. dev up refreshes the
cache (falling back to it offline) and converges on digest drift; the
per-command nag stays O(1) against the cache, never the network.
Settings resolution is now layered gitconfig-style — ENV over the user
file over the brew-prefix system file an org deployment formula ships.

Co-authored-by: Cursor <cursoragent@cursor.com>
@JPDuchesne JPDuchesne changed the title Host baseline, brew-only take: lockless manifest, upstream cursor-cli cask Host baseline: org-sourced, lockless, converged by dev up Aug 23, 2026
JPDuchesne and others added 2 commits August 23, 2026 16:48
The tap now ships dev-core (the org-blank tool) and a slim dev
deployment formula (org config + dependency edge). Every org's
install becomes `brew install <org>/<tap>/dev`; individuals install
dev-core and hand-write the user config. release.rb rewrites both
formulas' url+sha in lockstep.

Co-authored-by: Cursor <cursoragent@cursor.com>
Brew converges brew: the org tooling list lives in the deployment
formula's Brewfile (etc/dev/Brewfile) and dev up only triggers brew —
throttled brew update, scoped upgrade of the self-named
deployment_formula, then brew bundle install. Deletes the baseline
fetch/cache/stamp/nag machinery; settings drop baseline_repo for
deployment_formula.

Co-authored-by: Cursor <cursoragent@cursor.com>
@JPDuchesne JPDuchesne changed the title Host baseline: org-sourced, lockless, converged by dev up Deployment scheme: layered settings + the Brewfile host contract Aug 24, 2026
JPDuchesne and others added 10 commits August 23, 2026 20:43
config list / get <key> / set <key> <value> over the layered settings,
with the source layer shown gitconfig --show-origin style. Known-keys
only, from the Settings::KNOWN_KEYS registry — the one list both the
command and the resolver read. set merge-writes the user file as plain
string-keyed YAML. Global command, dispatched before any dev.yml lookup.

Co-authored-by: Cursor <cursoragent@cursor.com>
Measured no-op costs on a converged host (brew update ~0.5s, scoped
upgrade ~0.4s, brew bundle ~0.9s) don't justify a daily stamp that
delays deployment fixes by up to 24h. Every dev up now runs the full
chain; brew's own HOMEBREW_AUTO_UPDATE_SECS stays the only network
rate limiter, tunable through brew rather than dev.

Co-authored-by: Cursor <cursoragent@cursor.com>
Codecov's patch check flagged the warn-only branches (failed upgrade,
failed bundle), the production Executor bodies, and ConfigCommand#call
as untested.

Co-authored-by: Cursor <cursoragent@cursor.com>
Tap-qualified segments were missing @ and + — a versioned deployment
formula (org/tap/dev@2) was falsely rejected as malformed, silently
stopping self-updates in exactly the tap-qualified case the guard
serves. Also reject the invalid two-segment form and require brew's
canonical lowercase spelling (brew stores taps downcased).

Co-authored-by: Cursor <cursoragent@cursor.com>
The domain model dev actually has: every run carries a host half (ui),
and a project half only when a dev.yml encloses the cwd. ProjectContext
groups root/ruby/python/build_container/runner; project! makes the
registration invariant explicit (project commands only exist when the
project does). Groundwork for a project-optional Runner — no behavior
change yet.

Co-authored-by: Cursor <cursoragent@cursor.com>
up is a hybrid command: the host half (converge + cd RC hook) always
runs; the project half provisions only when a project context exists,
otherwise up prints the fresh-box bootstrap message and succeeds. This
is the business logic bin/dev's rescue was homing at the wrong layer.

Co-authored-by: Cursor <cursoragent@cursor.com>
The Runner now constructs without a dev.yml: manifest resolution moves
into run (inside the exit_for mapping, where project-input errors like
the removed ruby: key already map to clean exits), the projectless
catalog registers only up (backed by a builtin-only executor and a
null dependency service), and a lookup miss maps to the no-dev.yml
refusal. bin/dev's rescue block — host converge for up, the refusal
for everything else — was this exact logic homed at the wrong layer;
it is deleted, not moved.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Host::Converge was a single-#run method object, and "what keeping a host
converged consists of" lived in no class — up hand-assembled converge +
RC hook while plan/learnings hand-assembled skills + learnings sync.
HostService names that responsibility with four domain verbs
(converge_tooling, install_rc_hook, install_skills, sync_learnings); the
brew layer becomes converge_tooling's private implementation.

Co-authored-by: Cursor <cursoragent@cursor.com>
The skills + learnings refresh pair was hand-assembled from individual
collaborators at each hook point. Plan::Accessor and InstallDepsCommand
now inject the one host service and call its verbs, which also replaces
install-deps' untyped synchronizer param with a typed collaborator.
Learnings::Accessor keeps its direct collaborators: its sync subcommand
is the explicit blocking refresh, a different contract from the
warn-only hooks.

Co-authored-by: Cursor <cursoragent@cursor.com>
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.

1 participant