Skip to content

fix: make doctor, deploy and printed commands match what runs - #1155

Merged
abrichr merged 17 commits into
mainfrom
fix/ledger-wave2-2026-10-14
Oct 14, 2026
Merged

abrichr merged 17 commits into
mainfrom
fix/ledger-wave2-2026-10-14

Conversation

@abrichr

@abrichr abrichr commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

This branch fixes 13 launcher problems found in a review of what openadapt prints and checks. In each case a check said [OK] or "passed" when it hadn't checked, a printed command failed when pasted, or the docs described an action as safer than it is. Each fix has a regression test that fails on origin/main (60938ae) and passes here.

Fixes

Problem Change Regression test Ref
The README called openadapt flow replay bundle --url ... a "permissive local rehearsal". Replay performs every recorded action against that app, including saves that can't be undone, and doesn't ask first. The README says replay writes to the app at --url, isn't a dry run, doesn't certify the bundle, and belongs on a test or staging copy. The example URLs use a staging placeholder. tests/test_readme_replay_warning.py B011
PyPI keeps a release's description forever. The next wheel would have frozen the README's generated lifecycle block, which states an admission, onto PyPI, where it could outlive a revocation. A hatchling metadata hook (hatch_build.py) builds the package description from the README with that block replaced by a link to the live signed record. scripts/verify_release_artifacts.py refuses a wheel or sdist whose description states a lifecycle state. The GitHub README doesn't change. tests/test_pypi_description.py, tests/test_release_artifacts.py::test_release_artifacts_reject_a_frozen_lifecycle_description B007
The quickstart's "See a fail-safe halt" command replayed the tutorial bundle with --drift modal. That bundle doesn't match plain MockMed, so it halted at step_000 on the login screen with or without drift. The hint prints demo-record, compile, and replay --drift modal for a separate demo on plain MockMed. That replay stops at step_010 when the screen changes. tests/test_cli_quickstart_polish.py::test_drift_hint_replays_a_bundle_recorded_on_plain_mockmed B008
openadapt doctor --backend with macos, windows, linux, or citrix printed [OK] and exited 0 on a base install, where the modules those backends import are missing. doctor and deploy look up the modules each Flow backend imports (find_spec, nothing is imported). A missing module prints [MISSING] with the install command. A host that can't open the surface prints [UNSUPPORTED]. Both fail the check. tests/test_cli_smoke.py::test_doctor_native_backend_*, ::test_deploy_native_backend_* B009
Flow 1.33 and 1.34 look up four X11 libraries in the wrong case and refuse Chromium on Linux. The launcher accepted Flow >=1.29.0, so pip install --upgrade openadapt kept 1.34. Every openadapt-flow requirement, the range that doctor and deploy report, and the platform manifest's copy of the pins start at 1.35.0. uv.lock is refreshed. tests/test_flow_floor.py B012
openadapt --help and the quickstart printed openadapt-agent serve .... After uv tool install openadapt or pipx, only openadapt is on PATH, so that command failed with exit 127. A new openadapt agent command passes every argument to openadapt-agent and returns its exit code. It writes nothing to stdout first, so an MCP client can launch it. Help, quickstart, and the README use it. tests/test_agent_command.py B042
Step 3 of the help's Manual Demo Lifecycle was lint --strict, which exits 1 on the demo bundle, so a set -e script stopped there. Step 3 is openadapt flow lint bundle. A note says --strict exits 1 on the demo. tests/test_help_demo_lifecycle.py (the end-to-end test runs with OPENADAPT_E2E=1) B006
openadapt replay suggested deploy. openadapt quickstart --brek-it printed Flow's 43-verb usage and no hint. A Flow verb typed without flow fails with "Did you mean 'openadapt flow replay'?". Quickstart checks each forwarded long option against Flow's tutorial parser and suggests the closest name. Abbreviations are refused, because an abbreviation Flow would expand could change what runs. tests/test_cli_dead_ends.py B013
doctor listed PyTorch, ANTHROPIC_API_KEY, and OPENAI_API_KEY as missing on every base install. Compiled workflows make no model calls. Without a research extra, doctor prints "Model access: not required." With one, the same checks appear under "Research extras" and name the command that needs each item. tests/test_doctor_research_checks.py B014
openadapt version printed five research packages as "not installed". capture stop was advertised, though it can't stop a capture. Two module usage lines didn't parse. version lists the product packages and shows research packages only when installed. capture stop is hidden from help and still exits 1 with the Ctrl+C note. The usage lines are fixed and parsed by a test. tests/test_cli_leftovers.py B015
The README and docs/LEGACY_FREEZE.md offered pip install openadapt==0.46.0, which fails because openai-whisper imports pkg_resources. The freeze doc said to run poetry install on main, which has no Poetry project. The freeze doc gives a recipe that upgrades pip and caps setuptools below 81 with --build-constraint, and clones the v0.46.0 tag. The runs are in docs/legacy-install-evidence.md. tests/test_legacy_install_docs.py B017
Printed commands broke in cmd and PowerShell (curl ... | sh, single-quoted specs), in zsh (unquoted openadapt[ml]), or when the output path had a space. Windows gets uv's PowerShell installer. Package specs are double-quoted. Paths in quickstart hints are quoted for the host's shell. tests/test_cli_shell_hints.py B019
deploy --secret-ref env:FOO printed [OK] with FOO unset, and the preflight printed "Preflight passed" right after a [SETUP] line that says readiness isn't checked. An env: reference is [OK] only when the variable is set and non-empty; otherwise it's [MISSING] and the preflight fails. The value is never printed. A keychain: reference is [SETUP]. Any [SETUP] item turns the last line into "Preflight incomplete", with a count. tests/test_deploy_preflight_readiness.py B021

Not fixed here

  • Already fixed on main, waiting for a release: the base dependency on openadapt-agent (B002; PyPI's 1.16.0 doesn't include it, main's wheel does) and the README lifecycle block naming Flow 1.35.1 (B004; the scheduled check passes again, but README Production lifecycle source has drifted #1123 is still open).
  • In openadapt-flow: its README still says "permissive rehearsal path", certify --policy permissive prints "certified safe", openadapt flow replya prints openadapt-flow usage, and lint --strict still prints Next command: openadapt-flow certify ... through the launcher, which rewrites only the tutorial's output.
  • doctor and deploy still print python -m pip ..., which is the wrong interpreter under uv tool install.
  • examples/capture_train_eval.py still prints openadapt train --capture and openadapt eval view.
  • docs/LEGACY_FREEZE.md keeps unrelated stale statements, such as "Python 3.12+" for the new packages. The legacy recipe was run on macOS only.
  • deploy exits 0 when only [SETUP] items remain, because the native backends always leave the target check to Flow. The last line says "Preflight incomplete" in that case.

How it was tested

  • Each new or changed test file was copied onto origin/main in a separate worktree. Every fix has tests that fail there on an assertion about the old behavior and pass here. Control tests (forwarding real flags, Click's own suggestions, a fully checked web host) pass on both.
  • Full suite: 454 passed, 7 skipped on Python 3.12 and on 3.10. The skips are the openadapt-ml seam tests, the opt-in browser test, and a CI-only sibling-install check. ruff check and ruff format --check pass on openadapt/.
  • uv build --wheel --sdist (uv 0.11.29, as in the release workflow) passes verify_release_artifacts.py, and a wheel rebuilt from the sdist is byte-identical. A wheel built from origin/main is refused by the new check. uv lock --check and validate_platform_manifest.py --offline pass.
  • On macOS (Apple silicon), with real Chromium and MockMed:
    • The help's five lifecycle commands ran end to end (OPENADAPT_E2E=1); on main they stop at lint --strict.
    • Quickstart ran from a directory whose path has a space. Its printed lint line and the three drift commands were pasted into sh and ran. The drift replay stopped at step_010; the same bundle without drift ended COMPLETED_UNVERIFIED. The old hint's command halted at step_000.
    • openadapt agent serve --bundles <bundle> answered an MCP initialize and tools/list on stdout.
    • doctor --backend macos reported [MISSING] with python -m pip install "openadapt[macos]", and --backend linux reported [UNSUPPORTED]. deploy --secret-ref env:FOO with FOO unset failed.
  • Legacy install on Python 3.11 with pip 26.2.1: the plain dry run exits 1 on pkg_resources, and the constrained dry run exits 0.
  • The CI install set (pip install -e ".[dev]" plus the research packages) resolves with Flow 1.35.1 on Python 3.10 and 3.12, for Linux and macOS. In a local environment with those packages, the first CI run's failure reproduced: every test passed and pytest then exited 139. Doctor tests that stub importlib.util.find_spec made doctor import torch under the stub, which left torch half-loaded. tests/conftest.py now imports torch once before any stub; three full runs then exited 0.
  • gitleaks found no leaks in the branch commits.

Before merging

  • The "Validate platform manifest against published artifacts" check fails here and on main for the same reason: openadapt-privacy 1.1.2 is newer than the manifest's 1.1.1 (platform-manifest.json has drifted from the published releases #1110). The offline manifest check passes.
  • Review the uv.lock change: openadapt-flow 1.29.0 to 1.35.1, openadapt-types 0.17.1 added, opencv-python-headless removed.
  • These changes alter what scripts see. doctor --backend <native> exits 1 on a base install. deploy fails on an unset env: reference. Quickstart refuses unknown or abbreviated tutorial flags. deploy prints "Preflight incomplete" instead of "Preflight passed" while [SETUP] items remain.
  • PyPI users get B002, B007, B012, and B042 only after a launcher release through the reviewed release workflow. The bundle-free openadapt agent serve --allow-run also needs openadapt-agent 2.0.2.

🤖 Generated with Claude Code

abrichr and others added 16 commits October 14, 2026 09:56
The "Author a workflow" section ended with `openadapt flow replay bundle
--url https://your-app.example` and then called it a "permissive local
rehearsal". Replay drives the app at --url and performs every recorded
step, including an irreversible save, with no prompt. A reader who
followed the README against a real system of record made real writes
that the text described as a rehearsal.

The section now says that replay performs every recorded action against
the real app, doesn't ask first, isn't a dry run and doesn't certify the
bundle, and that --url belongs on a test or staging copy. The example
URLs point at a staging placeholder. A new test fails if the prose after
a `flow replay ... --url` block calls it a rehearsal or leaves out the
write warning.

Ledger: B011
Signed-off-by: abrichr <richard.abrich@mldsai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PyPI keeps a release's description forever. The 1.16.0 page still says
"Not actively admitted" while the GitHub README names seven active
admissions, and the next release would have frozen today's block
("is the admitted runtime (launcher 1.16.0 ...)") the same way, where it
could outlive a revocation. The README block stays true only because a
daily job rerenders and checks it against the live signed record.

The package description now comes from a small hatch metadata hook
(hatch_build.py) that copies README.md and replaces the generated
lifecycle block with a link to the live signed record. The GitHub
README is unchanged. The release artifact verifier now refuses a wheel
or source archive whose description still states a lifecycle state,
next to its existing refusal of a static lifecycle classifier. The
source archive includes the hook so a wheel built from it gets the same
description.

Ledger: B007
Signed-off-by: abrichr <richard.abrich@mldsai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`openadapt quickstart` ended with "See a fail-safe halt: openadapt flow
replay <qs>/bundle --drift modal". The tutorial bundle is recorded
against MockMed's fault server and enters already signed in, while
`replay --drift` serves plain MockMed, which opens at its login screen.
The printed command halted at step_000 ("Could not resolve target")
with or without --drift, so the demo labeled an app mismatch as drift
safety.

The hint now prints three commands that record and compile a separate
demo on plain MockMed and replay that bundle with modal drift. Run as
printed, the replay stops at step_010 (Save Encounter) on the screen
change, and the same bundle without drift runs to the end. The hint
says "See it stop when the screen changes" and makes no claim about
whether anything was written, because that halt ends
RECONCILIATION_REQUIRED.

Ledger: B008
Signed-off-by: abrichr <richard.abrich@mldsai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`openadapt doctor --backend macos|windows|linux|citrix` printed a fixed
"[OK]" and exited 0 on a base install, where the AppKit, Quartz,
ApplicationServices, gi or requests modules those surfaces import are
absent, so the replay failed later. Only rdp probed its dependency.
`deploy` printed "[SETUP]" for the same backends, passed, and its
failure text called [SETUP] items blocking.

A small table now lists, per backend and host, the modules each surface
imports and the extra that installs them. doctor and deploy locate them
without importing them. A missing module prints [MISSING] with the
`pip install 'openadapt[<extra>]'` command and fails the check. A host
that can't open the surface (macos off macOS, linux off Linux, citrix
on Linux) prints [UNSUPPORTED] and fails. Otherwise [OK] says that only
the Python dependencies were checked. deploy keeps its [SETUP] note on
target readiness, and its failure text no longer calls it blocking.

Ledger: B009
Signed-off-by: abrichr <richard.abrich@mldsai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Flow 1.33 and 1.34 look up four X11 libraries as "xcomposite",
"xdamage", "xfixes" and "xrandr". find_library matches case-sensitively,
so on Linux those releases report installed libraries as missing and
refuse to download Chromium. The launcher accepted any Flow >=1.29.0,
so `pip install --upgrade openadapt` kept an installed 1.34 and
quickstart kept refusing. The runtime shim on main corrects the names
only for commands that go through `openadapt flow`, and it isn't
released.

Every openadapt-flow requirement in pyproject.toml, the range that
doctor and deploy report, and the version check behind it now start at
1.35.0. With the new wheel, an environment holding Flow 1.34.0 moves to
1.35.1; with the old one it stays on 1.34.0. uv.lock follows the floor
(Flow 1.29.0 to 1.35.1, adding openadapt-types and dropping
opencv-python-headless). The shim stays as a no-op guard for installs
made without dependency resolution. A new test fails if any Flow
requirement or the reported range admits a release below 1.35.0.

Ledger: B012
Signed-off-by: abrichr <richard.abrich@mldsai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`openadapt --help` and the quickstart footer printed `openadapt-agent
serve ...`. `uv tool install openadapt`, which the openadapt.ai
installer runs, and pipx put only the launcher's `openadapt` executable
on PATH, so that command failed with "command not found" (exit 127).
Flow's printed commands were already rewritten to `openadapt flow`; the
agent bridge had no launcher form.

A new `openadapt agent` command passes every argument to
openadapt-agent's entry point and returns its exit code. It writes
nothing to stdout before the bridge starts, so an MCP client can launch
it directly. The help, the quickstart footer and the README now name
`openadapt agent serve ...`. Tests fail if the launcher prints a bare
`openadapt-agent <verb>` command or the passthrough drops an argument.

Ledger: B042
Signed-off-by: abrichr <richard.abrich@mldsai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`openadapt --help` listed `openadapt flow lint bundle --strict` as step
3 of the Manual Demo Lifecycle. Strict lint exits 1 on the demo bundle
by design, because its irreversible save has warn-level gaps (no effect
contract, a same-channel check). A script that chained the documented
steps with `&&` or `set -e` stopped there, and the help didn't say so.

Step 3 is now `openadapt flow lint bundle`, which prints the same
warnings and exits 0. A line under the block says the demo has
warnings, so `lint --strict` exits 1 on it. A test fails if a lifecycle
step uses `--strict`. An opt-in test (OPENADAPT_E2E=1) runs the five
printed commands in order and requires exit 0 from each; it failed at
step 3 before this change and passes after it.

Ledger: B006
Signed-off-by: abrichr <richard.abrich@mldsai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`openadapt run`, `openadapt record` and `openadapt replay` printed only
"No such command", and `replay` suggested `deploy`. They are Flow
commands, spelled `openadapt flow <verb>` through the launcher.
`openadapt quickstart --brek-it` forwarded the typo to the engine, whose
parser printed its 43-verb `openadapt-flow` usage and no hint.

An unknown top-level name that is a Flow verb now fails with "Did you
mean 'openadapt flow <verb>'?". The verbs come from the installed Flow
parser, with a fixed list when it can't be read. quickstart checks each
forwarded long option against the installed Flow tutorial's options
before it announces or creates an output directory, and suggests the
closest name ("Did you mean --break-it?"). Names must match exactly, so
an abbreviation Flow would expand can't change what runs without the
launcher knowing. When Flow's options can't be read, flags forward
unchanged as before.

Ledger: B013
Signed-off-by: abrichr <richard.abrich@mldsai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`openadapt doctor` always printed "PyTorch not installed",
"ANTHROPIC_API_KEY not set" and "OPENAI_API_KEY not set", even on a
base install. Healthy runs of compiled workflows make no model calls,
so those lines suggested that an AI key and a GPU stack were missing
when neither is used.

On an install without openadapt-ml or openadapt-evals, doctor now
prints one line instead: "Model access: not required. Healthy runs of
compiled workflows make no model calls." When a research extra is
installed, the same checks appear under "Research extras (not used by
compiled workflows)", and each missing item names the command that
needs it. Exit codes don't change.

Ledger: B014
Signed-off-by: abrichr <richard.abrich@mldsai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`openadapt version` printed openadapt-ml, -evals, -viewer, -grounding
and -retrieval as "not installed" on every base install, which looks
like a broken install, and never listed openadapt-types, which the base
install brings in. `openadapt capture stop` can't stop anything and
exits 1, yet the module usage and the `capture` help listed it with the
working commands. The module usage also named `train --capture` and
`eval --checkpoint`, which don't parse; the commands are `train start`
and `eval run`.

`version` now lists openadapt, openadapt-flow, openadapt-agent,
openadapt-types and openadapt-capture (marked as an optional extra),
says that OpenAdapt Desktop is a separate app, and shows research
packages under "Research (optional)" only when they're installed.
`capture stop` is hidden from help and still explains how to stop a
capture, with a non-zero exit because nothing was stopped. The usage
lines are corrected, and a test parses each one against the real
command tree.

Ledger: B015
Signed-off-by: abrichr <richard.abrich@mldsai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README.md said v0.46.0 was "still installable with `pip install
openadapt==0.46.0`", and docs/LEGACY_FREEZE.md gave the same command.
It fails on every Python that 0.46.0 supports. Its openai-whisper pin
ships only as a source archive whose setup.py imports pkg_resources, and
pip builds it with the newest setuptools, which no longer has it. The
freeze doc also said to clone main and run `poetry install`, but main
now holds the 1.x launcher and has no Poetry project.

The freeze doc now gives a marked recipe that upgrades pip to 25.3 or
later and installs with `--build-constraint` capping setuptools below
81. On 2026-10-14 it installed v0.46.0 with Python 3.11 on macOS (Apple
silicon), and a dry run succeeded with Python 3.10; the runs are in
docs/legacy-install-evidence.md. The clone instructions now check out
the v0.46.0 tag. The README says the plain command fails and points to
the freeze doc. A test fails if either file offers the bare command in
a code block or the recipe loses the setuptools cap.

Ledger: B017
Signed-off-by: abrichr <richard.abrich@mldsai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The launcher printed commands that broke when pasted outside a POSIX
shell or from a path with a space. The Python remedy offered only
`curl ... | sh` and a POSIX install.sh, which don't run in cmd or
PowerShell. Package specs were single-quoted (`'openadapt[rdp]'`),
which cmd passes to pip with the quotes, or unquoted
(`openadapt[ml]`), which zsh rejects as a glob. The quickstart lint and
--break-it hints printed the output path unquoted, so a path such as
C:\Users\Jane Doe\... split into several arguments.

On Windows the Python remedy now gives uv's PowerShell installer and
drops the POSIX script link. Every bracketed package spec is
double-quoted, which cmd, PowerShell, bash and zsh all accept. Every
quickstart hint that carries a path quotes it with shlex on POSIX and
with double quotes on Windows. Tests fail if a spaced path splits, a
spec is single-quoted or unquoted, or the Windows remedy pipes to sh.

Ledger: B019
Signed-off-by: abrichr <richard.abrich@mldsai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`openadapt deploy` printed `[OK] env:FOO` for any secret reference that
matched the env:NAME or keychain:service/item syntax, even when FOO
wasn't set. It also printed "Preflight passed" right after a `[SETUP]`
line saying the guide doesn't claim the target is ready. An operator
read both as a ready host.

An env:NAME reference is now `[OK]` only when NAME is set and non-empty
in this environment, which is what Flow requires. Otherwise it's
`[MISSING]` and the preflight fails. The value is never printed. The
launcher can't look inside the OS keychain, so a keychain reference is
now a `[SETUP]` item. When any `[SETUP]` item remains, the last line
says the preflight is incomplete and counts those items instead of
saying it passed. The exit code stays 0 in that case, because the
native backends always leave the target check to Flow. A host with
nothing left unchecked still gets "Preflight passed".

The native-backend deploy test asserted the old "Preflight passed"
line, and the rdp guide test passed an unset env reference. Both now
match the new behavior.

Ledger: B021
Signed-off-by: abrichr <richard.abrich@mldsai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Several new tests patched launcher helpers (`_flow_tutorial_options`,
`_flow_subparsers`, `_host_platform`) with monkeypatch's default
raising=True. Against a launcher without those helpers they errored in
setup instead of failing an assertion, so a red run said "missing
attribute" rather than showing the old behavior. With raising=False, the
same tests now fail on main for the reason they guard (no `openadapt flow
<verb>` hint, a typo forwarded to the engine, `[OK]` for an unset env
reference), and the forwarding controls pass there too.

Signed-off-by: abrichr <richard.abrich@mldsai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`compatibility.launcher_requires` in platform-manifest.json mirrors the
openadapt-* pins in pyproject.toml, and `validate_platform_manifest.py
--offline`, a pull request check, fails when they differ. The Flow floor
moved to 1.35.0 in pyproject.toml, so the manifest's copy moves with it.
The `components.launcher` entries still describe the published 1.16.0
metadata and are left alone; the release workflow regenerates them.

Ledger: B012
Signed-off-by: abrichr <richard.abrich@mldsai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI installs the research extras, and on this branch every Python CI lane
passed all tests and then crashed at exit with a segmentation fault
(exit 139). Several doctor tests replace importlib.util.find_spec with a
stub that reports every module as installed. Doctor now runs its GPU
check only when a research extra is found, so the first `import torch`
of the session moved into one of those stubbed tests. Under the stub,
torch's import fails partway (it looks for optional modules the stub
claims exist), doctor reports PyTorch as missing, and the half-loaded
extension crashes the interpreter at shutdown. On main the first import
happened to run under a narrower stub.

A session fixture in tests/conftest.py imports torch once with the real
finder when it's installed. Reproduced and checked in a local virtual
environment with the same packages as CI: exit 139 before, exit 0 on
three runs after.

Signed-off-by: abrichr <richard.abrich@mldsai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@abrichr
abrichr merged commit e1db089 into main Oct 14, 2026
16 checks passed
@abrichr
abrichr deleted the fix/ledger-wave2-2026-10-14 branch October 14, 2026 17:26
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