Runtime-manager compatibility characterization#77
Conversation
Empirically characterize how adj resolves language-runtime version managers (mise, asdf, rbenv, uv, nvm) across launch contexts, using a non-default-pin success signal so silent system-toolchain fallback fails loudly. Includes the Berkopec-profile fixture (fish + mise activate) as a real-world instance of the activation-camp failure case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Shell-driven harness (ci/runtime-compat) plus a GitHub Actions matrix that runs 8 manager/mode fixtures across inherited-shell and launchd-minimal contexts, with a mise-shim tracer that is locally verifiable before the rest fan out. Includes the Berkopec failure/remedy fixtures and a macOS launchd smoke job. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Boot failures now hard-fail instead of satisfying a fallback expectation; the macOS smoke job exports ADJACENT_HOME so the client reaches the daemon socket; setup.sh runs in-pipeline so install failures abort the job. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Without this, run-cell.sh always exited 143 (the SIGTERMed daemon's wait status leaking through the EXIT trap), failing every CI cell regardless of the pass/fail RESULT. The root cause had two layers: (1) capturing $? into a local var is correct, but (2) with set -e active in the EXIT trap, `wait` returning 143 aborted the function before `return "$status"` could execute. Adding `|| true` after `wait` lets the function reach its return statement. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Self-review (agent)Reviewed the full diff before handing off. Found and fixed four real bugs — recording them here for your eyes:
Verified locally
Known follow-up (by design)The Note for CI: mise's python-build-standalone attestation check fails on clean runners, so the mise-shim fixture sets |
mise-exec/mise-run/uv resolve their pin under launchd (workaround survives a bare PATH). asdf/nvm Node apps are unbootable under launchd — no /usr/bin/node to fall back to, unlike Ruby which falls back to /usr/bin/ruby. Adds an `unbootable` expectation (missing version satisfies it, present version violates it) so the finding is asserted rather than a false green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI green — characterization completeSecond run is fully green (all 8 cells + macOS real-launchd smoke + aggregate report). Observed matrix:
Takeaways (full write-up in the spec's "Observed results" section):
Ready for your review — left as a draft for you to mark ready / merge. |
Records the decision flowing from the PR #77 characterization: surface version-pin mismatches (detect-and-warn), document the mise exec/run / uv run pattern as supported, and defer transparent resolution (login-shell or per-manager adapters) as each trades away stack-agnosticism or determinism. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…l prior art Sharpen the decision from detect-and-warn to detect-and-fail: a declared pin that adj can't satisfy refuses the boot (the pin file is the declaration), per the declarative/loud/proud value. Records Puma-dev's login-shell trap (bash runner + zsh home shell = wrong env, no good override) as the prior art behind rejecting login-shell execution and env-var overrides. Mark Accepted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TL;DR — philosophy & design direction
Adjacent is declarative, loud, and proud: no zero-config magic that silently does the wrong thing, no invisible state. Applied to runtime version managers, that means:
.tool-versions/.mise.toml/.ruby-version/.python-version/.nvmrc) andadjcan't boot it with that exact version, it fails closed — same loud path as a crash — rather than quietly serving the wrong runtime. No pin = no opinion.mise run <task>/mise exec -- <cmd>/uv run. Verified to resolve the pin even under a bare launchd PATH. The fail-closed error names this remedy.adjacent.toml, committed and visible.Full rationale:
docs/adr/0001-runtime-manager-resolution.md(Accepted).This PR is the characterization that produced that decision — it does not change
adjbehavior yet. The fail-closed check + version probe is scoped follow-up work.What this is
An empirical CI harness that characterizes how
adjresolves language-runtime version managers (rbenv, asdf, mise, uv, nvm) when it boots an app, across the two launch contexts a real user lands in: an inherited-shell PATH vs a bare launchd-minimal PATH.Design:
docs/superpowers/specs/2026-06-28-runtime-manager-compatibility-design.mdPlan:
docs/superpowers/plans/2026-06-28-runtime-manager-compatibility.mdWhy the failure is dangerous (the thing the decision targets)
adjboots apps withsh -c <cmd>inheriting the daemon's env. So:mise activate, nvm) never fire undersh -c.The harness proves resolution rather than mere liveness: each fixture pins a version that differs from the runner's system default and its app echoes its own resolved version, so a silent fallback mismatches the pin and fails loudly. A boot failure hard-fails (never a false green).
Observed matrix (CI-green)
mise activate) — Berkopecuv run)mise exec/mise run/uv runresolve the pin even under a bare launchd PATH — the workaround to recommend.mise activateandnvmnever resolve per-directory undersh -c./usr/bin/node); Ruby/Python fall back to system/usr/bin/{ruby,python}. That asymmetry is recorded as a distinctunbootableexpectation (a missing version satisfies it; a present one violates it — so it can't false-green).Contents
ci/runtime-compat/— three version-echo servers,lib.sh+run-cell.sh(boots a real daemon per context, asserts observed-vs-pin), 8 fixtures, README..github/workflows/runtime-compat.yml— ubuntu matrix over 8 cells x 2 contexts + aggregate report + onemacos-14real-launchd smoke job.docs/adr/0001-runtime-manager-resolution.md— the decision.Follow-ups (own specs, not in this PR)
adjshould help with the Node-under-launchd gap beyond documenting it.🤖 Generated with Claude Code