Deployment scheme: layered settings + the Brewfile host contract - #133
Conversation
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
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>
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>
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>
…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>
| 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| |
There was a problem hiding this comment.
So not in this PR, but we should definitely refactor this file, let's create an issue for it.
There was a problem hiding this comment.
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.
| # 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". |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
/build sweep the review comments
|
Co-authored-by: JPDuchesne <2636122+JPDuchesne@users.noreply.github.com>
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 upon a fresh box (no dev.yml anywhere) converges the host and succeeds.dev-coreis the public core formula; each org ships a thindevformula depending on it that installsconfig.ymland an orgBrewfileinto$(brew --prefix)/etc/dev/(pkgetc).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_toolingrunsbrew update, then a scopedbrew upgrade <deployment_formula>— the setting validated against brew's canonical token shape (bare or fully tap-qualified, lowercase,@/+allowed) — thenbrew bundleagainst the org Brewfile. A failed host step never blocks project provisioning. No self-update throttle; brew is its own rate limiter.Dev::Settingsresolves 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 thedev configview can never disagree. The newdev configglobal builtin manages the user layer.ExecutionContextsplits into a host half (always present) and a nilableProjectContext;Runnerconstructs without a dev.yml and serves a projectless catalog of exactlyup, wired with a builtin-only executor and a null dependency service.upis a hybrid command — its host half (converge +dev cdRC 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>/dev→dev up→ ready).bin/devrescues nothing: the no-dev.yml refusal and the removed-ruby:-key error both map insideRunner#exit_for, at the layer that owns the run.dev up,install-deps, anddev plancomposeHostServiceverbs; the shipped-skill-set semantic lives once, inSkillInstaller#install_shipped.Verification
srb tcclean; merged with main (post-Deps redesign: package universe repositories, Resolver-owned selection #142 deps refactor) — allRegistry/Resolver/Installercall shapes verified against the current contracts.Wheretable (tap-qualified@/+accepted, two-segment and uppercase rejected); projectless Runner paths (context assembly, up-only catalog, no-dev.yml refusal,ruby:-key mapping insiderun); the hybridupoutside 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 inSettings.