Metadata-Version: 2.5
Name: trampoline-delta
Version: 0.6.1
Summary: Command line interface for the Delta service
Requires-Python: >=3.13
Requires-Dist: boto3<1.43.1,>=1.42.24
Requires-Dist: botocore<1.43.1,>=1.42.24
Requires-Dist: croniter<7,>=6.0.0
Requires-Dist: cryptography>=48.0.1
Requires-Dist: google-auth>=2.38.0
Requires-Dist: google-cloud-storage<4.0.0,>=3.13.0
Requires-Dist: httpx<1.0,>=0.27.0
Requires-Dist: jsonschema==4.26.0
Requires-Dist: packaging>=25.0
Requires-Dist: pydantic>=2.13.4
Requires-Dist: python-ulid>=3.1.0
Requires-Dist: tomlkit<1,>=0.13
Requires-Dist: tzdata==2026.2
Requires-Dist: urllib3<3.0.0,>=2.7.0
Requires-Dist: zstandard>=0.23.0
Description-Content-Type: text/markdown

# Delta CLI

Durable per-step execution budgets are declared in `pyproject.toml`; see
[step timeouts](../../docs/step-timeouts.md) for precedence, runtime limits and rollout.

Delta CLI is a command line interface for the Delta service.

This repository contains the Delta command router, version output, and
browser-based CLI authentication.

## Features

- Built-in help via `delta --help` and per-command `--help`
- Version output via `delta version` and `delta --version`
- OAuth login with Authorization Code + PKCE
- One-shot `127.0.0.1:8400` callback server or an optional Console callback handoff
- Session storage in `~/.trampoline-ai/delta/auth.json` with owner-only permissions
- Automatic bearer authentication for service API commands

## Project selectors

`--project` accepts a stable ID beginning with `proj_` or an exact project name
in the selected organization. Quote names containing spaces, for example
`delta flows list --org ORG --project "Daily reports"`. Names are resolved through
the same paginated API as `delta projects list`; unknown names fail with
`no project named NAME in ORG; see delta projects list`. Duplicate names fail with
the matching IDs so you can select one explicitly. IDs bypass the lookup, including
legacy `proj_` IDs without a project record. Configuration and `DELTA_PROJECT`
selectors follow the same rule.

This applies to deploy, runs, flows, deployments, schedules, and webhooks. For env commands,
`--project` remains a boolean scope flag; `--project-id` accepts an ID or name.

## Terminal UI (`delta tui`, opt-in)

~~~bash
uv tool install trampoline-delta
# In the workflow project, add avalanche-ai>=0.15.0 and run uv sync.
delta tui --org ORG --project "Daily reports"
delta tui --org ORG --project PROJECT --flow hello --run RUN --read-only
~~~

The CLI has no `[tui]` extra and its base dependency closure contains no
Avalanche, PredictRLM, DSPy, LiteLLM or Textual. `delta tui` launches the
project's `<project root>/.venv/bin/python`, or the explicit `--python PATH`.
It requires **avalanche-ai>=0.15.0**, `TuiExtensions` v1 and the revamped store
contract in that interpreter. A subprocess preflight fails before authentication
or HTTP if these are missing; add the dependency to the project and run `uv sync`.
The CLI never installs, syncs or relocks on TUI launch. There is no legacy fallback.
The standalone provider/launcher ships in the Delta wheel and is loaded by absolute
path; Delta need not be installed in the project. Credentials pass only through the
child environment, never its argv. The terminal is inherited, child exit codes
propagate, and Ctrl-C/teardown terminates its process group.

Authentication, `DELTA_TOKEN`, upward `delta.toml` discovery, org selection and
project ID/exact-name resolution are shared with the other commands. Conflicting
flow/run links are refused. Use the command palette for provider panels and
explicit actions. Catalogs pin the finalized deployment used to discover them;
unknown historical deployment/flow/timestamps remain unknown in the inspector.

- Project history is paged; existing APIs without snapshot revisions expose
  **incomplete loaded windows**, not a fictitious stable full-history snapshot.
- Selecting a run loads its detail once, then reduces its contiguous fact SSE.
  Resume uses that run's `Last-Event-ID`, never another run's maximum sequence.
  There is no periodic resource GET or network health/ping loop. A stream reset
  rereads selected detail and installs its hydrated nodes/error in the baseline
  before ACK; a terminal frame reuses the last fact ID, not a new data position.
  Covered pre-cut states are not replayed over that baseline, and queued detail
  updates are released once. Missing optional project events keeps project scope
  stale but does not prevent selected-run fact reconciliation.
- Observation reads are auxiliary: any non-2xx (including 503
  `observation_integrity`, `observation_unavailable` or `observation_pending`)
  leaves the fact stream and baseline intact. Log/trace detail shows
  `observation unavailable: <reason>` without inventing trace/evidence or changing
  execution status. Failed attempt identities are cached; only node selection or
  explicit refresh permits a reread. There are no automatic observation retries,
  including when `Retry-After` is supplied. A known CONSOLE-owned read-before-archive
  race can briefly return `observation_integrity`; this client does not read spill
  storage or hide a failed completion-log/trace proof.
- Project notices, live activity, original inputs, effective capabilities and
  org discovery are negotiated. Unsupported panels/streams are hidden; **Server
  availability** reports “not available on this server.” The shared CONSOLE
  project-feed adapter follows its frozen contract, including opaque cursors.
  M1 notices concern runs only. Catalog/schedule/env reads refresh on selection,
  reset/reconnect or explicit panel refresh, never on a timer.
- `--read-only`, or lack of server-verified `run:create`, disables all backend
  mutations. Without a capabilities endpoint, actions remain unavailable with a
  reason; decoded JWT claims never grant permission. Downloads separately need
  verified `storage:read`, an explicit destination and confirmation.
- Start previews schema-validated input and pins deployment + SID. Retry the same
  intent and exact payload after an uncertain result or `429 run_rate_limited`;
  `Retry-After` is shown, with no automatic replacement intent. Cancel retains its
  command ID; **202 means requested, not terminated**. Only genuinely resolvable
  legacy waits are offered. Schedules have disable only, never enable/toggle.
- Env inspection retains names/flags/provenance only and immediately discards
  values. Observations pin node/attempt/auth; absent/truncated traces are not
  manufactured. Live activity and retained terminal traces are separate surfaces.

### Local headless proof

`make dev`, then `make dev-hello ARGS='--tui'`, extends the existing real Compose
hello fixture with Textual Pilot navigation, a live DAG through fact SSE, a strict
single selected-detail GET assertion, resize, and read-only checks. The Pilot runs
through the wheel's absolute-path `tui_entry.py` loader in the fixture's
project `.venv`, with `-I` so neither the project nor `scripts/dev/runtime.py`
can shadow Avalanche's packages. The fixture uses published Avalanche 0.15.0
without wheel/index overrides. Check other lanes'
Compose stacks and use an owned project name before starting it. The fixture also
deploys `tui-agent`: two deterministic JSON DummyLM actions (`plan`/`code`), with
no external LM calls. The Pilot opens its native agent trace inspector and checks
hydrated iteration code/output and retained usage. `tui-failing-agent` deliberately
uses incompatible DummyLM output to exhaust three real agent attempts. The Pilot
forces one provider reset at the first retry, lets the run seal while baseline
loading is paused, then checks a complete hydrated failed baseline, exact ACK,
once-only buffered detail delivery, failed DAG/history, and the native node-detail
error. This is a controlled reset cut, not a claim to reproduce every spontaneous
transport failure. Existing success-path request-count assertions are unchanged.
Cancel uses the provider when
capabilities permit it; otherwise the authorized fixture client performs the write
and the Pilot asserts its cancel control is disabled. New-stream checks that lack
server support are reported NOT RUN, not counted as passing. Allow-all Compose is
not a Clerk permission proof.

## Schedules (`/v1`)

CLI 0.3.9 builds containing scheduled dispatch add `delta schedules create`,
`list`, `get` and `disable`. The server must also contain the dispatch capability;
older versions only store schedule metadata. See [scheduled flows](../../docs/scheduled-flows.md)
for pinned-deployment examples, idempotency keys, cron/timezone semantics,
permissions and restart/missed-window behavior.

## Runs (`/v1`)

`delta runs` starts and inspects runs of deployed flows using the committed merged
OpenAPI contract. Use `delta runs --help` for the seven run commands.

Start a deployed flow, then follow the returned run ID:

~~~bash
delta deploy . --org ORG --project PROJECT
RUN=$(delta runs start --flow hello --input '{"name":"Ada"}' \
  --org ORG --project PROJECT)
delta runs events "$RUN" --org ORG --project PROJECT
delta runs outputs "$RUN" --org ORG --project PROJECT --json
~~~

`start` selects the newest finalized deployment containing the flow; use
`--deployment D` to pin one. `--input` must be a JSON object. The supplied `--sid S`
or a fresh `cli-<hex>` SID is printed to stderr before submission. Retry with the
**same `--sid` and input/deployment** after a lost response; no automatic retry is
made. Human stdout is only the `run_id`; `--json` prints the exact generated create
response (201 accepted or 200 existing replay).

~~~bash
delta runs get RUN --org ORG --project PROJECT --json
delta runs outputs RUN --org ORG --project PROJECT
delta runs artifacts RUN --org ORG --project PROJECT
delta runs start --flow process_document --input '{}' --file document=./report.pdf \
  --org ORG --project PROJECT
delta runs download RUN --out ./downloads --org ORG --project PROJECT
delta runs download RUN --path .outputs/STEP/ATTEMPT/INVOCATION/FIELD/result.pdf \
  --out ./downloads --org ORG --project PROJECT
# Legacy single-file form remains available:
delta runs download RUN INV reports/result.txt --dest ./result.txt \
  --org ORG --project PROJECT
delta runs cancel RUN --cmd-id retry-stable-id --org ORG --project PROJECT
delta runs events RUN --last-event-id 12 --max-reconnects 3 \
  --org ORG --project PROJECT --json
~~~

All commands use existing `delta login`, `DELTA_TOKEN`/`--token`, local project/user
config and `DELTA_ORG`/`DELTA_PROJECT` resolution. Explicit org/project flags work
outside a local checkout. The default API host is
`https://api.avalanche.run`, the authenticated `/v1` release host. Until it is
bound to the `/v1` gateway, point commands at `https://v2-gateway-staging.avalanche.run`
with `--api-url` or `DELTA_API_URL`.
API URL precedence (highest first): `--api-url`, `DELTA_API_URL`, project
`delta.toml` `api_url`, user `~/.config/trampoline-ai/delta/config.toml` `api_url`,
then the default.
`--api-url` accepts an origin or its `/v1` base. `--json`
prints generated model JSON without filling absent fields; events are JSON lines.

Starting a run through the selected edge project establishes its binding. Reads of a
run not bound to that project report `404 run_not_found`, including cancel/events
before the rollout gate. Outputs/artifacts/download are live. Until runtime R16-d,
cancel/events report `503 durable_runs_upstream_unavailable` as **not yet available
on this deployment**, with exit status 1. Other errors preserve the edge `{detail}`
and runtime/new-route `{reason}` families.

Cancel prints its supplied or once-generated `cmd_id` to stderr, does not retry,
and tells you to retry with the **SAME `--cmd-id`** on ambiguous outcome 503. A 202
records the command; it does not prove child termination. SSE ignores heartbeat
comments, checks contiguous fact IDs, resumes exclusively after the last delivered
ID, and stops on terminal/stream_end/stream_error. Reconnects are bounded by a total
count (default 3); stream_error exits 1. Requests have 15-second HTTPX budgets;
SSE reads allow 60 seconds, above the 45-second upstream idle bound.

Download gets a JSON descriptor from the edge, then uses its exact signed URL and
headers directly against storage, without the edge bearer. Bytes must match the
immutable file size and SHA-256 before exclusive atomic publication. Existing
files/symlinks and racing destination creators are never overwritten. Destination
parents must exist. JSON download output is the descriptor and can contain a signed
URL: treat that output as sensitive.

### Generation and opt-in evidence

From the monorepo root:

~~~bash
uv run --frozen --package trampoline-delta python apps/cli/tools/generate_v1_client.py --write
uv run --frozen --package trampoline-delta python apps/cli/tools/generate_v1_client.py --check
uv run --frozen python apps/cli/tools/e2e_durable_run.py --help
~~~

`datamodel-code-generator==0.83.0` generates the reachable component closure for
Pydantic v2/Python 3.13. Narrow, schema-derived post-processing preserves conditional
and composition constraints with Draft 2020-12 entrypoint validators, PEP 695
aliases for hashable dictionary keys, Pydantic recursive JsonValue, compiled regex
patterns, and unchanged URI/date-time strings. No DTO fields are handwritten.
`--check` regenerates in memory and verifies frozen/merged/overlay hashes against
spec bytes, info fields and `apps/api/openapi/SHA256SUMS`. Builds verify committed
module digests and spec identities without requiring codegen at wheel runtime;
the generated models, constants and digest inventory ship in the wheel.

The E2E driver requires `--base`, `--org`, `--project`, `--run`, `--wrong-project`
and `--evidence`; supply `--binding`/`--path` together or auto-select the first file.
It uses only `DELTA_TOKEN`, the generated models and the CLI client, compares
outputs at equal watermarks, verifies direct-storage bytes, and requires the same
run to return `404 run_not_found` under the wrong project for all six exact routes.
Default evidence expects gated cancel/events. `--runtime-enabled` mutates the
selected owned fixture by cancelling it and observes incremental SSE (30-second /
20-frame smoke bound). Evidence includes candidate identity, all three spec hashes,
rerun arguments, status observations and cleanup, without payloads, bearer tokens,
signed URLs/query strings or headers. **Do not run against a live host without
explicit fixture authorization.**

## Requirements

- Python 3.13+
- uv

## Installation

From this directory, install the CLI in editable mode:

~~~bash
uv tool install -e .
~~~

You can also run the CLI directly during development:

~~~bash
uv run --with-editable . delta --help
uv run --with-editable . delta version
~~~

## Authentication Setup

The CLI uses a public OAuth application. Do not use secret keys for interactive
CLI user login.

The local session is stored in `~/.trampoline-ai/delta/auth.json`. Delta creates the
containing directory with mode `0700` and writes the credential file atomically with
mode `0600`.

Create an OAuth application in the same auth project as `../console`:

- Default redirect URI: `http://127.0.0.1:8400/callback`
- Console callback URI for `--manual-callback`:
  `https://cloud.avalanche.run/oauth/callback`
- Public client: enabled
- PKCE required: enabled
- Scopes: `profile email offline_access user:org:read`

A Clerk administrator must register the applicable redirect URI before its login mode
can succeed.

The production issuer, public OAuth client ID, and callback port are built in.
Sign in without configuration:

~~~bash
delta login
~~~

For another deployment, override the public auth configuration explicitly:

~~~bash
export DELTA_AUTH_ISSUER_URL="https://your-auth-issuer.example.com"
export DELTA_AUTH_CLIENT_ID="your_oauth_client_id"
~~~

For local development in this monorepo, the CLI can also derive the issuer from
`DELTA_AUTH_PUBLISHABLE_KEY`.

## Command Overview

### `delta deploy`

Deployment discovery runs **only in the project's interpreter**, not the CLI's
installation. Install `avalanche-ai==0.15.0` and your workflow dependencies in the
project, then run `uv sync` there before deploying. The CLI never syncs, installs,
or relocks the project. The CLI wheel itself has no Avalanche, DSPy, PredictRLM,
or LiteLLM dependency.

The default interpreter is `.venv/bin/python` (`.venv\Scripts\python.exe` on
Windows). For a nonstandard environment, set `python = "path/to/venv/bin/python"`
in the root of `delta.toml`, or `DELTA_PROJECT_PYTHON` (takes precedence). Relative
paths are project-root-relative. `DELTA_DISCOVERY_TIMEOUT` sets the positive,
finite timeout in seconds (default 30), including imports, builders and metadata.
User workflow code executes in a subprocess, **not a security sandbox**.

`delta init` adds `[tool.uv] exclude-dependencies = ["litellm"]` to
`pyproject.toml`, preserving existing exclusions and formatting. Commit that
change and re-sync the project yourself; it does not change pip wheel metadata.


Build and upload the committed source and wheelhouse, then finalize the deployment
through `/v1`. Deploy prints the deployment ID and finalized state; `--json` keeps
the deployment, source, flows, bundle, finalize request, diagnostics and git metadata.

~~~bash
delta deploy . --org ORG --project PROJECT
delta deploy . --org ORG --project PROJECT --env-file .env.production --json
delta deployments list --org ORG --project PROJECT
delta flows list --org ORG --project PROJECT
delta flows get FLOW --org ORG --project PROJECT --deployment-id DPL
~~~

Flow list/get use `--deployment-id` when supplied; otherwise the CLI reads all
finalized deployment pages and selects the newest `created_at` (deployment ID
breaks ties). Flow requests always send that explicit deployment ID. Human flow
continuations include it; keep the same `--deployment-id` when using a JSON
`next_cursor`. An empty deployment catalog reports an error asking you to deploy.

Runs target finalized deployments; `/v1` has no "live" deployment or set-live step.

### Dependencies

Deploy includes the project's frozen `uv.lock` dependency closure as verified
runtime-target wheels. Runtime-provided packages are omitted only at matching
versions; conflicts print the complete `[tool.uv] constraint-dependencies` fix.
Use **the version your runtime reports**, then `uv lock` and commit the result.
See [Flow dependencies](../../docs/dependencies.md) for the constraint block,
binary-wheel/system-library limits and the `imageio-ffmpeg==0.6.0` example.

### `delta version`

Shows the installed Delta CLI version.

### `delta auth login`

Signs in to Delta through your browser.

~~~bash
delta login
~~~

The CLI uses the registered callback port `8400`. Pass `--callback-port 0` for a
random loopback port when another provider supports RFC 8252 loopback redirects.

Use `--no-browser` to print the sign-in URL instead of opening it:

~~~bash
delta auth login --no-browser
~~~

Use the Console callback when the browser cannot reach the local loopback listener:

~~~bash
delta auth login --manual-callback
~~~

Manual callback sign-in requires Delta CLI 0.1.6 or newer. Older CLI releases cannot
complete this handoff; upgrade the CLI and start sign-in again if the Console reports
that an upgrade is required.

Complete sign-in in the browser, select **Copy response** at the Console callback
page, then paste only that response into Delta's hidden terminal prompt. Never copy,
paste, log, or share the raw browser callback URL. The response is bound to the
in-memory PKCE sign-in attempt; do not paste it into logs or chat.

### `delta auth status`

Shows whether you are signed in.

### `delta auth whoami`

Shows the signed-in account.

### `delta auth logout`

Signs out of Delta.

### `delta --help`

Shows available commands and global options.

Service API commands automatically load and refresh the signed-in session from
`~/.trampoline-ai/delta/auth.json`. An explicit `--token` wins, then a nonblank
`DELTA_TOKEN`, then the saved session; the user TOML configuration rejects plaintext
tokens.

The API enforces the selected Clerk organization and its role permissions. The
signed-in user must belong to the requested organization, and that role must
include the Delta action used by the service command.

## Listing pages

Collection commands accept `--limit` (default 100, maximum 500) and `--cursor`.
Human output prints the next continuation; list JSON is now an
`{items,next_cursor}` envelope. `HttpDeploymentApi` returns a list-compatible
`ApiPage` exposing `next_cursor`; iterating it makes no additional requests.

Live paging continues after the last returned item. Refresh without a cursor to
see newer items that sort before that position. Filtered pages may contain no
items and still have a continuation.

Webhook URLs now include organization, project and flow scope:
`/v2/hooks/orgs/ORG/projects/PROJECT/flows/FLOW/[CONFIGURED_PATH/]TOKEN`.
Use the complete URL returned by the API; opaque-only URLs are removed.
