Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 8 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,27 +41,24 @@ runtime:
python -m pip install --upgrade openadapt
```

Run the bundled synthetic workflow. It needs no account, target application,
API key, or operating-system automation permissions:
Run the complete bundled tutorial with one command. It needs no account,
target application, API key, or operating-system automation permissions:

```bash
openadapt flow demo-record --out rec
openadapt flow compile rec --out bundle --name mockmed-triage
openadapt flow certify bundle --policy permissive
openadapt flow replay bundle --run-dir run
openadapt quickstart
```

You now have:

- `rec/`: the demonstrated interaction and retained target evidence
- `bundle/`: the inspectable compiled workflow
- `run/REPORT.md`: the ordered actions, evidence, outcome, and any halt reason
- `openadapt-quickstart/recording/`: the demonstrated interaction and retained target evidence
- `openadapt-quickstart/bundle/`: the inspectable compiled workflow
- `openadapt-quickstart/run/REPORT.md`: the ordered actions, evidence, outcome, and any halt reason

Inspect the program and its deployment gaps:

```bash
openadapt flow visualize bundle --out graph.html
openadapt flow lint bundle
openadapt flow visualize openadapt-quickstart/bundle --out graph.html
openadapt flow lint openadapt-quickstart/bundle
```

The bundled workflow is a tutorial, not a production certification. Qualifying
Expand Down
111 changes: 103 additions & 8 deletions openadapt/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
openadapt doctor
"""

import os
import sys
from pathlib import Path
from typing import Optional

import click
Expand Down Expand Up @@ -133,6 +135,19 @@ def main():
# =============================================================================


def _invoke_flow(argv: list[str]) -> int:
"""Invoke the canonical engine once and return its exit code."""
try:
from openadapt_flow.__main__ import main as flow_main
except ImportError:
click.echo("Error: openadapt-flow not installed.", err=True)
click.echo("Reinstall with: pip install --upgrade openadapt", err=True)
click.echo("Engine only: pip install openadapt-flow", err=True)
return 1

return int(flow_main(argv))


def _run_flow(argv: list[str]) -> None:
"""Delegate to the openadapt-flow CLI, preserving its exit code.

Expand All @@ -142,15 +157,95 @@ def _run_flow(argv: list[str]) -> None:
Imported lazily so ``openadapt`` (and ``openadapt flow --help``) work
even when openadapt-flow isn't installed.
"""
try:
from openadapt_flow.__main__ import main as flow_main
except ImportError:
click.echo("Error: openadapt-flow not installed.", err=True)
click.echo("Reinstall with: pip install --upgrade openadapt", err=True)
click.echo("Engine only: pip install openadapt-flow", err=True)
sys.exit(1)
sys.exit(_invoke_flow(argv))


@main.command("quickstart")
@click.option(
"--out",
type=click.Path(path_type=Path),
default=Path("openadapt-quickstart"),
show_default=True,
help="New directory for the recording, bundle, and run report.",
)
@click.option(
"--headed", is_flag=True, help="Show the browser while the tutorial runs."
)
def quickstart(out: Path, headed: bool) -> None:
"""Run the complete local tutorial: record, compile, check, and replay.

This is the shortest path to a real OpenAdapt run. It uses the bundled
synthetic tutorial, keeps every artifact on this computer, and enables no
model or Cloud call. The output directory is never overwritten.
"""
root = out.expanduser().resolve()
if root.exists():
raise click.UsageError(
f"Output already exists: {root}. Pass --out with a new directory."
)

recording = root / "recording"
bundle = root / "bundle"
run_dir = root / "run"
steps = [
("Record the tutorial", ["demo-record", "--out", str(recording)]),
(
"Compile the recording",
[
"compile",
str(recording),
"--out",
str(bundle),
"--name",
"local-quickstart",
],
),
(
"Certify the tutorial",
["certify", str(bundle), "--policy", "permissive"],
),
(
"Replay locally",
["replay", str(bundle), "--run-dir", str(run_dir)],
),
]
if headed:
steps[0][1].append("--headed")
steps[-1][1].append("--headed")

for index, (label, argv) in enumerate(steps, start=1):
click.echo(f"\n[{index}/{len(steps)}] {label}")
# The bundled tutorial contains synthetic identity-like strings. Keep
# the ordinary report warning meaningful for real workflows by making
# this one known-safe fixture explicit, then restore the operator's
# environment immediately after replay.
scrub = os.environ.get("OPENADAPT_FLOW_SCRUB")
if argv[0] == "replay" and scrub in (None, "auto"):
os.environ["OPENADAPT_FLOW_SCRUB"] = "off"
try:
code = _invoke_flow(argv)
finally:
if argv[0] == "replay" and scrub in (None, "auto"):
if scrub is None:
os.environ.pop("OPENADAPT_FLOW_SCRUB", None)
else:
os.environ["OPENADAPT_FLOW_SCRUB"] = scrub
if code:
raise click.ClickException(
f"{label} stopped with exit code {code}. "
f"Any completed artifacts remain in {root}."
)

sys.exit(flow_main(argv))
click.echo("\nLocal quickstart complete.")
click.echo(f"Bundle: {bundle}")
click.echo(f"Run evidence: {run_dir}")
click.echo("No model or Cloud call was enabled.")
click.echo(f"Inspect qualification gaps: openadapt flow lint {bundle}")
click.echo(
"Connect this computer when you want Cloud history and collaboration: "
"https://app.openadapt.ai/dashboard/settings/ingest"
)
click.echo("Qualify a consequential workflow: https://openadapt.ai/qualify")


@main.group(cls=_FlowPassthroughGroup)
Expand Down
16 changes: 8 additions & 8 deletions platform-manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_kind": "openadapt-platform-release-manifest",
"schema_version": "1.0.0",
"generated_at": "2026-07-26T20:55:04+00:00",
"generated_at": "2026-07-26T23:49:48+00:00",
"release_channel": "beta",
"components": {
"launcher": {
Expand Down Expand Up @@ -46,21 +46,21 @@
},
"capture": {
"package": "openadapt-capture",
"version": "1.1.1",
"version": "1.2.0",
"source": "pypi",
"requires_python": ">=3.10",
"artifacts": [
{
"type": "bdist_wheel",
"filename": "openadapt_capture-1.1.1-py3-none-any.whl",
"url": "https://files.pythonhosted.org/packages/3b/66/f6ea4cb9a5e9515370e59574c7caf8f827807af877d251db17047010c871/openadapt_capture-1.1.1-py3-none-any.whl",
"sha256": "76f6fdc9bd81dd3a72445fd8ebd918654a732765d5aae848d5f56dc62d1bf97d"
"filename": "openadapt_capture-1.2.0-py3-none-any.whl",
"url": "https://files.pythonhosted.org/packages/ee/4c/6b5c0ab21b7300c03d22d8096b85e279def7ff7552b207ff4239e1af3108/openadapt_capture-1.2.0-py3-none-any.whl",
"sha256": "da3b2ef52b3eb24c0a565ec4c6f0eda1dcebf30be78a48b704a28d3e1316f053"
},
{
"type": "sdist",
"filename": "openadapt_capture-1.1.1.tar.gz",
"url": "https://files.pythonhosted.org/packages/6f/65/11c5c49eeee83b10595c87269f83d1d1dbc6f4e2e04f51885441d064e295/openadapt_capture-1.1.1.tar.gz",
"sha256": "7022d41b37f3a8fbf30f5e89df2373d3bfacc0e13ab8c9b6c1770f76126fbd6b"
"filename": "openadapt_capture-1.2.0.tar.gz",
"url": "https://files.pythonhosted.org/packages/fe/98/ea22edf242430b9a15689d22c8608852db49265a356f603ec3d5bdd64595/openadapt_capture-1.2.0.tar.gz",
"sha256": "5eb5f7df329e58a15df27caba4cd3b76cf1d5779515c4c0f0cdd439631736390"
}
]
},
Expand Down
37 changes: 37 additions & 0 deletions tests/test_cli_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,43 @@ def test_version_command():
assert result.exit_code == 0


def test_quickstart_runs_one_local_lifecycle_without_overwriting(monkeypatch):
calls = []
monkeypatch.setattr(
"openadapt.cli._invoke_flow",
lambda argv: calls.append(list(argv)) or 0,
)

runner = CliRunner()
with runner.isolated_filesystem():
result = runner.invoke(cli_main, ["quickstart", "--out", "first-run"])
assert result.exit_code == 0, result.output

assert [call[0] for call in calls] == [
"demo-record",
"compile",
"certify",
"replay",
]
assert calls[2][-1] == "permissive"
assert "No model or Cloud call was enabled" in result.output


def test_quickstart_refuses_an_existing_output(monkeypatch):
monkeypatch.setattr(
"openadapt.cli._invoke_flow",
lambda _argv: pytest.fail("engine must not run before output refusal"),
)

runner = CliRunner()
with runner.isolated_filesystem():
Path("existing").mkdir()
result = runner.invoke(cli_main, ["quickstart", "--out", "existing"])

assert result.exit_code != 0
assert "Output already exists" in result.output


def test_version_flag_matches_installed_metadata():
"""`openadapt --version` must print the real installed distribution
version (importlib.metadata), never a hardcoded string that can drift
Expand Down