Skip to content

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

Merged
JPDuchesne merged 25 commits into
mainfrom
jpd/dev-102-host-baseline-brew-only
Sep 10, 2026
Merged

Deployment scheme: layered settings + the Brewfile host contract#133
JPDuchesne merged 25 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 convergence (Dev::HostService, one verb per piece of machine state dev owns — brew tooling, shell RC hook, shipped skill links, org learnings artifacts; every verb argless, idempotent, warn-only): converge_tooling runs brew update, then a scoped 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. 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) — through a single resolution path (lookup), so the typed getters and the dev config view can never disagree. 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 + shipped skill links) always runs; outside a project it also syncs the org learnings artifacts, prints the bootstrap message, and exits 0 (the fresh-box bootstrap: brew install <org>/<tap>/devdev up → ready). 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.
  • Hook points: dev up, install-deps, and dev plan compose HostService verbs; the shipped-skill-set semantic lives once, in SkillInstaller#install_shipped.

Verification

  • 1255 tests, 0 failures; rubocop and srb tc clean; merged with main (post-Deps redesign: package universe repositories, Resolver-owned selection #142 deps refactor) — all Registry/Resolver/Installer call shapes verified against the current contracts.
  • 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 (including skill links + learnings sync); the builtin-only executor's wiring guard; the null dependency service; the empty-user-value layer-shadowing case in Settings.

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>
JPDuchesne and others added 8 commits September 9, 2026 18:58
…ost-baseline

Conflict resolutions:
- plan/accessor + install-deps + up: keep the branch's HostService seam
  (it absorbs main's skill_installer + learnings synchronizer wiring)
  with main's typing style
- settings: keep the branch's layered user/system config, retrofitted
  to typed: strict with sigs (main's typing bar)
- update-deps: main's resolver shape (schemes:, probe-less find) over
  the branch's project-optional context accessor
- runner/global_dispatch/command_executor: branch's project-optional
  restructure; RedundantTLet autocorrected per main's new cop
- host_service + config_accessor upgraded to typed: strict (new cop
  floor), with Kernel.system via untyped receiver for the splat

Co-authored-by: Cursor <cursoragent@cursor.com>
setting and lookup each implemented ENV -> user -> system independently
and disagreed on an empty user value: setting resolved through the merged
hash (empty string shadows the system layer), lookup checked per layer.
So dev config showed a value the typed getters refused to resolve. The
getters now read lookup(key).first; setting, layered_config, and the
re-spelled ENV names (KNOWN_KEYS already registers them) are gone.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ings

up is documented as the fresh-box bootstrap, yet nothing on its path
called install_skills — shipped skills stayed unlinked until the first
dev plan. install_skills joins the always-run host half (cheap,
idempotent). The projectless path also syncs the machine's org learnings
artifacts, which in-project runs get via the composed install-deps.
Also: the projectless test's rc-hook stub now returns :added, a real
contract value, instead of the fabricated :appended.

Co-authored-by: Cursor <cursoragent@cursor.com>
HostService#install_skills and Learnings::Accessor#sync each spelled
install_all(SHIPPED_SKILLS_DIR) — what installing dev's shipped skills
means lived twice and could drift. Both now delegate to the new
install_shipped on SkillInstaller, where the shipped-set knowledge
(the constant) already lives.

Co-authored-by: Cursor <cursoragent@cursor.com>
The T.untyped was a merge-era workaround from before HostService was
typed: strict. The test noop becomes a real HostService subclass so the
typed seam accepts it.

Co-authored-by: Cursor <cursoragent@cursor.com>
…install converge)

plans#26 states both and an adopting org needs them: the formula prints
the dev up pointer in caveats (install alone converges nothing), and
never converges from post_install — nested brew invocations deadlock on
brew's lock. Also fix settings.rb's stale README section reference.

Co-authored-by: Cursor <cursoragent@cursor.com>
…nippet

The dev.yml ascent lived twice (Runner via the helper, GlobalDispatch
hand-rolled); the help fallback now reads through the same memoized
helper the tests already reset around. The command-runner-exec learning's
illustrative snippet still showed context.project_root, renamed to
context.project!.root by the project-optional split.

Co-authored-by: Cursor <cursoragent@cursor.com>
Delegating the help fallback to the memoized find_dev_yaml_file made
classification order-dependent: an earlier test (or call) could memoize
the repo's dev.yml and leak it across a chdir — CI caught it. The ascent
is now extracted un-memoized; Runner's process-lifetime memo wraps it.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread bin/release.rb
abort "Could not find the package url+sha256 to update in #{FORMULA_PATH}" if updated == formula

FORMULA_PATH.write(updated)
FORMULA_PATHS.each do |formula_path|

@JPDuchesne JPDuchesne Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

So not in this PR, but we should definitely refactor this file, let's create an issue for it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Created d3mlabs/dev#155 to track the bin/release.rb refactor (extract release steps into typed, tested objects; noted possible convergence with #70's builtin dev release) — no code change in this PR, per the thread.

Addressed in 9b7074b.

Comment thread bin/release.rb Outdated
Comment on lines +226 to +230
# Read as UTF-8 explicitly: the formulas have non-ASCII bytes (e.g. an
# em-dash in a comment), and when release.rb runs under a non-UTF-8 locale
# (such as a piped, login-less subshell) Ruby's default external encoding
# is US-ASCII, which makes the sub below raise "invalid byte sequence in
# US-ASCII".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Code comments should follow the ruby line width of 120. I know we haven't fixed it when we expanded from 80 to 120, but we should follow the same pattern here, and create an issue to reflow our code comments.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reflowed all three PR-added comment blocks in bin/release.rb (the FORMULA_PATHS note, the UTF-8-read rationale, and the url+sha256 surgery comment) to fill the 120-character rubocop width, and created d3mlabs/dev#156 for the repo-wide sweep of legacy ~80-column comments.

Addressed in 9b7074b.

@JPDuchesne

JPDuchesne commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

/build sweep the review comments

/build — committed 9b7074b.

Both threads addressed: comments in bin/release.rb now fill the 120 width, and the two follow-up issues the reviewer asked for exist (#155 refactor, #156 comment reflow). Full suite green: 1255 tests, 0 failures (rubocop excludes bin/, so the comment-only change has no lint surface). Captured one learning, style/comment-line-width (new comments fill 120; no cop flags too-narrow comments, so the legacy 80-column style propagates by imitation until #156 lands). While running tests, the Homebrew-installed dev-core crashed at boot on this runner: the harness leaks GEM_HOME=~/.gem/ruby/3.3.10 whose json native extension is linked against an incompatible rebuilt rbenv libruby — bin/dev's sh preamble deliberately preserves GEM_HOME/GEM_PATH (dev#94/dev#90), which this failure argues against.
FIX: d3mlabs/dev bin/dev sh preamble — extend the dev#94 env scrub to also unset GEM_HOME/GEM_PATH (or reject a GEM_HOME built for a different Ruby): a harness-leaked GEM_HOME with stale native extensions crashes installed dev at boot ("linked to incompatible libruby").

🧠 drafted 1 learning in a learning proposal PR: #157

  • comment-line-width

⚙️ workflow run · model: claude-fable-5-high

Co-authored-by: JPDuchesne <2636122+JPDuchesne@users.noreply.github.com>
@JPDuchesne
JPDuchesne merged commit f6dc7eb into main Sep 10, 2026
17 checks passed
@JPDuchesne
JPDuchesne deleted the jpd/dev-102-host-baseline-brew-only branch September 10, 2026 15:19
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