diff --git a/.github/workflows/cli-distribution-check.yml b/.github/workflows/cli-distribution-check.yml index a9ab3cf..14b5bcc 100644 --- a/.github/workflows/cli-distribution-check.yml +++ b/.github/workflows/cli-distribution-check.yml @@ -57,6 +57,37 @@ jobs: python3 -m pip install --require-hashes --only-binary=:all: -r cli/ci/requirements-test.txt cd cli/bun bun install --frozen-lockfile --ignore-scripts + - name: Check the public surface + # prose-cli is a public user client: no internal service detail or + # developer-only surface in tracked files, release help or binaries. + # Same check as the public-surface gate in cli/ci/run_local.py. + if: matrix.os == 'ubuntu-22.04' + run: | + python3 -m unittest discover -s cli/ci -p test_check_public_surface.py + python3 cli/ci/check_public_surface.py --build-release + - name: Check hosted service parity between the Rust and Bun ports + # One shared corpus drives both ports through the test-seam fixture + # transport; no network. Same checks as the service-* and + # registry-service-* gates in cli/ci/run_local.py. + if: matrix.os == 'ubuntu-22.04' + run: >- + python3 cli/ci/run_local.py + --only shared-contracts + --only service-operations-corpus + --only service-coverage + --only service-help + --only service-operations-rust-build + --only service-operations-rust + --only registry-service-rust + --only service-operations-bun-build + --only service-operations-bun + --only registry-service-bun + - name: Build and test the developer endpoint build + # The dev-endpoint cargo feature is never in a public build; keep it + # compiling and tested. Same checks as the rust-build-dev-endpoint and + # rust-tests-dev-endpoint gates in cli/ci/run_local.py. + if: matrix.os == 'ubuntu-22.04' + run: python3 cli/ci/run_local.py --only rust-build-dev-endpoint --only rust-tests-dev-endpoint - name: Test distribution plan adapter run: | python3 -m unittest discover -s cli/conformance/runner -p test_runner.py diff --git a/README.md b/README.md index 1a5da60..ff919b6 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,10 @@ Exact admitted versions and platforms are checked at readiness; see the product Native Claude delegation is available through the opt-in `claude-workspace-tools` native profile. It uses nonbare mode with explicit tools, separate caller-supplied permission rules and directory access, and fresh native configuration for API authentication. The default API profile retains its bare/tool coupling. Native task progress and completion are transported without interpreting their purpose. +## OpenProse service + +`prose cli` is also a user client for the hosted OpenProse service: sign in with `prose cli auth login` (or set `OPENPROSE_API_KEY`), then quote, submit, watch and download hosted runs, and manage programs, results, jobs, the wallet and organizations. Public builds talk only to the production service. Start with `prose cli service triage --json`; see [the service client guide](docs/hosted-service-client.md). + ## Development and provenance [Build Rust](cli/rust/README.md) · [Build Bun](cli/bun/README.md) · [Native output semantics](docs/native-output.md) · [Native task events](docs/native-task-events.md) diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 6b640a6..c3fc1c8 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -21,6 +21,90 @@ the tagged release. authentication boundaries. - Provider-free package admission and opt-in, cost-acknowledged live transport evidence for the nonsemantic `echo-v0` image. +- OpenProse hosted service commands in both products: + `cli service status|triage|capabilities|operations|guide`, `cli model list`, + `cli example list|show`, `cli repo list`, + `cli run quote|submit|watch|input|cancel|list|show|download|share`, + `cli program list|show|save|visibility|delete|revisions|draft`, + `cli result list|show|publish|unpublish`, `cli job …`, + `cli wallet balance|events|usage|redeem|topup` and `cli org …` (no + organization deletion). One embedded manifest + (`shared/service/operations.v1.json`, printed by `cli service operations`) + drives parsing, help, confirmation and transport limits. Every command emits + versioned JSON or JSONL and never prompts. Commands that spend money, publish + outward, delete or cannot be undone require `--yes`, and `--preview` shows + the planned request without sending it. The service commands talk only to + the production OpenProse service. +- Hosted runs always use a live session that is journaled before submission, + recover from a duplicate submission, and detach on Ctrl-C or the deadline. + Only `cli run cancel --yes` cancels. New taxonomy codes include + `HOSTED_RUN_DETACHED` (exit 21), `HOSTED_RUN_FAILED` and + `RUN_SUBMISSION_AMBIGUOUS` (exit 22), `HOSTED_RUN_CANCELLED` (exit 24), + `CONFIRMATION_REQUIRED`, `SERVICE_RESOURCE_NOT_FOUND` and + `GITHUB_LINK_REQUIRED`. +- `cli service status` reports reachability and the models you can use; + `cli service triage` reports the service, credential, wallet, organization, + recent runs and jobs with next commands. +- Customer-facing results carry prices only; human output shows money in + dollars. Result schemas refuse any property whose name matches `cost`. +- Help and the agent guide: `prose --help` lists the hosted command groups, + every command and group has `--help` with examples and exit codes (including + `cli auth`, `cli org list` and `cli package`), and `cli --help` lists the + everyday groups first. `cli service guide` covers a first program, a daily + job, scripting, pricing, headless keys and sharing. +- `SERVICE_AUTH_REQUIRED` names `credentialSource`, `credentialVariable` and + `credentialProblem` in `details`. +- On Linux, the Rust product stores credentials through `secret-tool` in the + same item the Bun product uses. +- `cli model list` shows each model's status when the service sends a + `catalog`: the models you can run, premium models that unlock with any wallet + top-up (with the service's summary and a top-up preview) and older ids still + accepted with the model to use instead. `--json` adds `result.catalog` + (`id`, `status`, `tier`, `summary`, `successor` only). +- A premium model refused before a top-up (402 `paid_top_up_required`, or 403 + `paid_model_required` from older servers) is `SERVICE_PREMIUM_MODEL_LOCKED` + (exit 10) with the model's summary in `details.reason`, a + `cli wallet topup --amount-cents 500 --preview` Action and + `details.suggestedArgv`. + +### Breaking + +- Service environment selection is removed. Every account, organization, + package and service command reaches the production OpenProse service with + `OPENPROSE_API_KEY` or the stored production credential. + - `cli environment show`, `cli environment use` and `cli environment reset` + are gone and fail with `INVOCATION_INVALID` (exit 2, unknown command). + - The global `--service-environment` option is gone. Placed before `cli`, it + fails with `INVOCATION_INVALID` (exit 2) and says the option was removed. + - `OPENPROSE_STAGING_API_KEY` is no longer read. + - A saved `service_environment` key in the user `cli.toml` is ignored, + whatever its value. A user who had selected the alternate environment now + reaches production; check which credential is in use with + `prose cli auth status`. + - The `openprose.service-environment/1` schema + (`shared/schemas/service-environment.schema.json`) is deleted. +- `cli auth login|status|logout`, `cli org list` and `cli package + publish|fetch|list|withdraw` print the `openprose.service-operation/1` + envelope (`schema`, `operation`, `interaction`, `result`, `problem`, with + keys sorted) like every other service command, instead of the + `openprose.service-account/1`, `openprose.organization-list/1` and + `openprose.package-operation/1` documents. `operation` is the dotted + operation id (`auth.status`, `org.list`, `package.fetch`); the `environment` + member is gone. The schema files of those names now describe the envelope's + `result`: `authenticated` and `credentialSource` (whose stored-key value is + now `store`, not `os-credential-store`), `organizations`, and the package + receipt, page or withdrawal. +- A malformed API key (from `OPENPROSE_API_KEY` or the store) is + `SERVICE_AUTH_REQUIRED` with `details.credentialProblem: "malformed"`, not + `SERVICE_PROTOCOL_INVALID`; the exit code stays 10. +- A registry 404 in `cli package fetch|withdraw|list` is + `SERVICE_RESOURCE_NOT_FOUND` (exit 10, not retryable) naming the missing + package version or organization, not the retryable `SERVICE_UNAVAILABLE`. +- An invalid `cli package` reference, slug, version, `--sha256` or `--cursor` + value is `INVOCATION_INVALID` (exit 2) with the cause in `details.reason`, + not `CONFIG_INVALID`, and every invalid `cli package` invocation is reported + in the `openprose.service-operation/1` envelope. +- No npm package or release binary changes until the next published alpha. ### Compatibility and release boundary diff --git a/cli/CONTRIBUTING.md b/cli/CONTRIBUTING.md index 517e146..87d5653 100644 --- a/cli/CONTRIBUTING.md +++ b/cli/CONTRIBUTING.md @@ -221,6 +221,51 @@ Do not skip a failed gate by loosening a version allowlist, changing evidence in place, dropping failed trials, retrying without recording the attempt, or using one product's output as the expected result for the other. +## Keep the client public and user-only + +This repository is the public OpenProse user client. It is not developer +tooling for the hosted service. Public builds talk only to the production +OpenProse service, and nothing in this tree may name another service +deployment, a developer credential variable, internal feature flags, +deployment or runtime identities, or real run ids. The `public-surface` gate +enforces this: + +```sh +python3 cli/ci/check_public_surface.py # tracked files only +python3 cli/ci/check_public_surface.py --build-release # plus both ports' release --help and the Rust binary strings +python3 cli/ci/run_local.py --only public-surface +``` + +Use neutral hosts such as `https://example.invalid` in tests and docs, and +short synthetic run ids such as `run_aaaaaaaaaaaaaaaa`. + +The denylist (`cli/ci/public_surface_denylist.py`) writes out only generic +patterns. Named internal terms, feature flags and captured identifiers are +matched by salted digest, so the public file does not publish what it guards; +the plaintext list stays with the service maintainers. When the gate reports +an `internal service term`, reword the line rather than the denylist. + +### Developer endpoint build + +OpenProse developers who need to point the CLI at another service origin use a +separate developer build. The override is compiled out of every default and +release build, which ignore it entirely. + +```sh +# Rust +cargo build --release --locked -p prose-cli --features dev-endpoint +# Bun (writes cli/bun/dist/prose-dev) +cd cli/bun && bun run build:dev +``` + +In a developer build only, `OPENPROSE_API_URL` (an `https` origin with no path, +query or credentials) replaces the production origin. The key is still +`OPENPROSE_API_KEY`, and `prose cli auth login` stores it under a +credential-store entry scoped to that origin, so it never overwrites your +production key. Human output is labeled `OpenProse (custom endpoint )` +and JSON envelopes report `"environment": "custom"`. Supply the origin at run +time; never commit one. + ## Pull request checklist - The change stays inside the CLI/language boundary. @@ -235,6 +280,7 @@ using one product's output as the expected result for the other. authentication assumptions. - Evidence and release claims do not exceed the authority actually observed. - No secret, private path, or raw provider response is present in the diff. +- The `public-surface` gate passes. For the current implementation status and known blockers, read `cli/protocol/STATUS.md`. For release construction and local package rehearsal, diff --git a/cli/README.md b/cli/README.md index 5747249..bfeaa5f 100644 --- a/cli/README.md +++ b/cli/README.md @@ -29,23 +29,21 @@ Release operators use the [functional-alpha readiness contract](release/ALPHA_READINESS.md) to distinguish candidate, promotion, and post-publication authority. -For the candidate service-account connection, see [Connect the CLI to staging](../docs/staging-account.md). The candidate registry commands below reuse that account connection; they do not enable hosted execution. Source implementation and hermetic fixtures do not establish deployment, release availability, or live qualification. +The `prose cli` account, registry and service commands connect to the hosted +OpenProse service. Source implementation and hermetic fixtures do not establish +release availability or live qualification. ## Registry package commands (candidate) -Package commands move bounded, data-only files through the selected OpenProse -account service. Production is the default. Select staging persistently when -working with its separate credentials, then inspect the selection: +Package commands move bounded, data-only files through your OpenProse account. +Sign in once, or set `OPENPROSE_API_KEY`: ```sh -prose cli environment use staging -prose cli environment show prose cli auth login prose cli package publish ./hello.md --organization example --name hello --version 1.0.0 prose cli package list example --json prose cli package fetch example/hello@1.0.0 --output-dir ./hello-copy prose cli package withdraw example/hello@1.0.0 -prose cli environment reset ``` A single file uses its basename as the default export and has no dependencies. @@ -78,9 +76,9 @@ neither downloaded nor executed. File contents are not scanned for secrets. See the [package byte format](shared/fixtures/registry/FORMAT.md) for exact path, encoding, hash, and receipt rules. -Fetch obtains a receipt and canonical artifact from the same selected service, -then verifies artifact and file hashes, manifest, inventory, and canonical bytes -before writing. Supply `--sha256 <64-lowercase-hex-digest>` to require a known +Fetch obtains a receipt and canonical artifact from the service, then verifies +artifact and file hashes, manifest, inventory, and canonical bytes before +writing. Supply `--sha256 <64-lowercase-hex-digest>` to require a known artifact identity. The output directory must be fresh, its parent must exist, and its ancestors must not be symlinks. Existing destinations are never replaced. The receipt is written as `.prose-package-receipt.json`. @@ -94,14 +92,50 @@ Cleanup preserves content whose ownership no longer matches the operation. These checks do not sandbox another process running with the same filesystem privileges. Native platform qualification remains separate from fixture results. -All four commands accept trailing `--json` or global `--output json`. Reports -include the selected environment; staging human output is labeled. Use global -`--service-environment production|staging` before `cli` for a one-command override. -Only user configuration can persist the selection. Production and staging use -separate OS credentials and `OPENPROSE_API_KEY` / `OPENPROSE_STAGING_API_KEY` -respectively; switching never copies credentials. Public reads can be anonymous -when no credential is available, but malformed selected credentials fail closed. -Publish and withdraw require a credential. No command starts a model or harness. +All four commands accept trailing `--json` or global `--output json`. The key +comes from `OPENPROSE_API_KEY` when it is set, otherwise from the OS credential +store written by `prose cli auth login`. Public reads can be anonymous when no +credential is available, but a malformed credential fails closed. Publish and +withdraw require a credential. No command starts a model or harness. + +## OpenProse service commands + +The `cli` service commands are a thin, noninteractive client of the hosted +OpenProse service (overview in +[the service client guide](../docs/hosted-service-client.md)). They use the same +account connection as the registry commands above. Unlike every other command +in this README, `cli run submit` starts a hosted model run and spends from your +wallet. + +```sh +prose --output json cli service status +prose --output json cli service operations # every command, option, exit code +prose --output json cli wallet balance +prose --output json cli program save hello hello.prose.md +prose --output json cli run quote +prose --output jsonl cli run submit hello.prose.md --yes +prose --output jsonl cli run watch --after 0 +prose --output json cli run download --output-dir ./out +``` + +`cli service operations` prints the embedded operation manifest. That manifest +drives parsing, help, confirmation, and transport limits in both products, so +it is the complete machine-readable reference. Every command emits +`openprose.service-operation/1` JSON, or `openprose.service-event/1` JSONL lines +ending in exactly one terminal line, and never prompts. Commands that spend +money, publish outward, delete, or cannot be undone stop with +`CONFIRMATION_REQUIRED` (exit 2), which shows the planned request, until you +repeat them with `--yes`. `--preview` shows that plan without sending anything. +Results show prices only. + +A submitted run is journaled before it is sent, and `run watch`, `input`, and +`cancel` use that local journal's live session (or `--session UUID`); a run +started elsewhere can still be read with `run show`. `--detach` returns as soon +as the run id is known. Ctrl-C (`HOSTED_RUN_DETACHED`) and the `--wait` +deadline, 30 minutes by default (`SERVICE_WATCH_DEADLINE`), both exit 21 and +leave the run going; their `details.resumeArgv` is the exact command that +resumes watching. Only `cli run cancel --yes` cancels. Organization +deletion and browser-only flows are not available. ## First five minutes @@ -413,8 +447,9 @@ independent harness and hosted fixtures, Rust and Bun suites, differential black-box behavior, process settlement, architecture/dependency boundaries, deterministic dependency inventory and release-note evidence, Windows resolution plus static/host-neutral checks, isolated archive/npm installation, -exact functional-alpha package admission, and the benchmark and release- -workflow contracts. Passing them is local mechanical admission only; it cannot +exact functional-alpha package admission, the benchmark and release- +workflow contracts, and the `public-surface` leak check that keeps internal +service detail out of this public client. Passing them is local mechanical admission only; it cannot grant semantic, native-Windows, strict semantic-adapter, or public-release eligibility. diff --git a/cli/SPEC.md b/cli/SPEC.md index 84fe9cb..9c63356 100644 --- a/cli/SPEC.md +++ b/cli/SPEC.md @@ -364,12 +364,37 @@ install upgrade help examples Unknown future commands follow the same path without a runner release. `prose help` is language help and is forwarded; `prose --help` describes the -shell runner. +shell runner. `prose help cli [COMMAND...]` is the one exception: `cli` is +reserved, so it is never a language help topic, and the runner prints the +matching `cli` help topic. The bare first token `cli` is the one permanent operational reservation. `prose -- cli ...` strips the delimiter and forces `cli ...` through the language path, preserving a deliberate escape hatch. +One narrow exception protects agents that omit `cli` ("Missing `cli`" below). The first words after the +runner globals may spell a service or runner command path (`run submit`, +`job list`, `wallet balance`, `org member list`, `doctor`). If neither +of the first two words names an existing file or directory relative to the +working directory, the runner exits 2 with the `prose cli ...` command instead +of forwarding. The same applies to a lone word that a `nounSynonyms` entry +maps to exactly one command (`login`, `whoami`, `models`), to a verb before +its group (`list jobs`) and to a `commandRewrites` word (`stop`, `delete`, +`share`, `cron`). A language command that also names a service command +(`status`, `help`, `examples`) is forwarded unless the default hosted harness, +which runs no language command, would refuse it; `run FILE` is always +forwarded, and `prose -- ` always forwards. + +A second exception protects agents that put a command option first. When the first token the runner-global parser does not +consume is a command-local option (a manifest `commonOptions` flag, an +operation option, or an `optionAliases` spelling of one such as `-j` or `-y`), +and only such options and runner globals precede `cli`, the runner exits 2 +(`INVOCATION_INVALID`) with `details.suggestedArgv` moving the options after +the command path. `prose --json cli run list` is therefore never forwarded. +An unknown option, or options not followed by `cli`, still freeze runner +parsing as the first opaque language token. `prose help cli ... --json` prints +the same topic as without `--json`. + The forwarded `argv` is the literal `prose` introducer followed by the remaining argument vector after runner-global options; no display string or shell reconstruction is authoritative. @@ -401,11 +426,11 @@ the skill-owned unresolved-input or unresolved-approval terminal status. The runner never opens a prompt to continue the run. Users who want conversational questions, approvals, or a TUI use the direct skill path. -### 7.2.1 Proposed local weave host bridge (IMP-026) +### 7.2.1 Proposed local weave host bridge This isolated branch proposes `prose cli weave --host-binding ABS OP CONFIG ...`. It is not a released command. The exact admission, operation grammar, byte-stream, -exit and interruption contract is [the IMP-026 decision](protocol/decisions/imp026-weave-host.md), +exit and interruption contract is [the weave host decision](protocol/decisions/weave-host.md), with its self-contained [black-box fixture](shared/fixtures/weave-host-v1.json). Implementation must follow that corpus in both products before qualification. @@ -483,6 +508,21 @@ Version 1 consumes only `PROSE_HARNESS`, `PROSE_TRANSPORT`, `PROSE_MODEL`, Unknown configuration-file keys and invalid values fail closed with their source location; unknown environment variables are ignored by the runner. +Both products apply one value grammar. Environment booleans are exactly +`true`, `false`, `1` or `0`; durations match `^[1-9][0-9]*(ms|s|m|h)$` and are +at most 24 hours (`maxTimeoutMs` in +`shared/capabilities/transport-limits.v1.json`); an empty value is rejected, +`PROSE_NATIVE_LOG` included; and a value holding U+FFFD (the replacement for +bytes that are not UTF-8) is rejected. Values are checked in one order, the +order of `values` in `shared/schemas/configuration-explanation.schema.json` +(with `nativeLog` before `authProfile`), within each source: a file's values +after all of its lines parse, then the environment, then flags. Every +`CONFIG_INVALID` names its source in `details.source` (`path:line`, the +variable, the flag, or `process cwd`), and reasons are fixed text without +operating-system error strings. On Windows, variable names match without +regard to case. The shared vectors are +`shared/fixtures/config/values-v1.json`. + Configuration files use a deliberately closed, portable subset of TOML. Each nonblank physical line contains either one comment or one assignment to a known top-level bare key. Spaces and tabs may appear around the key, equals @@ -739,6 +779,11 @@ transport/prompt/image/recursion rejection → `20`; `STARTUP_TIMEOUT` → `21`; protocol and harness failure → `22`; `SEMANTIC_STATUS_UNKNOWN` → `23`; `CANCELLED` → `24`; and `PROCESS_CLEANUP_FAILED` → `25`. +Both products cancel on `SIGINT`, `SIGTERM` and (POSIX) `SIGHUP`, the signal +of a closed terminal: the run is cancelled cooperatively and ends with +`CANCELLED` (`24`). Neither product handles Windows Ctrl+Break, whose default +action ends the process. + ## 9. Adapter architecture ### 9.1 Runtime classes @@ -1779,31 +1824,39 @@ parity with explicitly implementation-specific research variants; one isolated repository subtree; local functionality before public automation; and evidence-backed portability claims only. -## IMP-034: explicit staging account commands - -The opt-in global `--service-environment staging` admits only `cli auth login`, -`cli auth status`, `cli auth logout`, and `cli org list`. Other values or uses -are invocation errors. Without this option existing unavailable account behavior -and all harness routing remain unchanged. This does not enable hosted execution. -The only network origin is `https://run-prose-staging.openprose.workers.dev`; -redirects and user-configurable token destinations are forbidden. - -The shared black-box corpus is `conformance/runner/staging-service-corpus.json`. -Staging account and organization results use the closed `service-account` and -`organization-list` schemas. Service errors are contained in `problem`, with -exit code 10; cancellation uses existing `CANCELLED` and exit 24. No raw remote -errors, device codes, API keys, or unknown response fields enter output. -Organization projection contains only id, slug, name, and optional role. - -`OPENPROSE_STAGING_API_KEY` overrides only the staging local credential. It is -never forwarded to a harness. Login/logout reject this variable when nonempty, -because they cannot replace or clear the parent environment. Local credentials -use OS credential storage, service `org.openprose.cli.staging`, account -`api-key`; unavailable native storage fails closed with no plaintext fallback. -Status without a token succeeds as signed out. Status with a token verifies it -using GET `/organizations`; this endpoint can lazily create the account's default -organization. Logout removes only the local credential; it does not revoke a -server key. No provider credential is consulted or modified. +## Account commands + +`cli auth login`, `cli auth status`, `cli auth logout` and `cli org list` +connect the CLI to an OpenProse account. They do not enable harness execution. +Public builds reach exactly one origin, the production service origin +(`environments.production.origin` in `shared/service/operations.v1.json`); redirects and +user-configurable token destinations are forbidden, and no environment +variable, option or configuration value changes the origin (see +"Service origin" under hosted service operations for the separate developer build). + +The shared black-box cases are `conformance/cases/service/account/` and +`conformance/cases/service/organizations/`. Account and organization commands +print the `openprose.service-operation/1` envelope like every other `cli` +command (`operation` `auth.status`, `auth.login`, `auth.logout` or +`org.list`); their results are the closed `service-account` (`authenticated`, +`credentialSource`) and `organization-list` (`organizations`) schemas. Service +errors are contained in `problem`, with exit code 10; cancellation uses existing +`CANCELLED` and exit 24. No raw remote errors, device codes, API keys, or +unknown response fields enter output. Organization projection contains only +id, slug, name, and optional role. + +`OPENPROSE_API_KEY` overrides the stored credential. It is never forwarded to +a harness, even when explicitly re-allowed. Login/logout reject this variable +when nonempty, because they cannot replace or clear the parent environment. +Stored credentials use OS credential storage, service +`org.openprose.cli.production`, account `api-key`; unavailable native storage +fails closed with no plaintext fallback. Status without a token succeeds as +signed out. Status with a token verifies it using GET `/organizations`; this +endpoint can lazily create the account's default organization. Logout removes +only the local credential; it does not revoke a server key. No provider +credential is consulted or modified. A user `cli.toml` that still carries a +`service_environment` key (written by `cli environment use` in earlier +versions) is accepted and the key is ignored. Device login POSTs `/auth/device` without authorization, prints the returned user code and exact `https://github.com/login/device` verification URI on stderr, @@ -1820,31 +1873,725 @@ SERVICE_PROTOCOL_INVALID. Requests have bounded time and response sizes and never retry by changing origins or credentials. Only compiled test-seam builds may honor `PROSE_TEST_SERVICE_FIXTURE`. The file -contains `credential`, `storeAvailable`, ordered `exchanges` with exact `method`, -`path`, HTTP `status`, and JSON `body`, plus optional `cancelBeforePoll`. This -transport consumes the transcript without network, uses a virtual monotonic -clock, and replaces the credential store in memory. It is not an endpoint -override. Requests must match transcript methods and paths and use a bearer -credential only for organization requests. Release builds ignore this seam. - -The staging credential predicate is exactly `rr_test_[0-9a-f]{32}`; invalid -credentials normalize to SERVICE_PROTOCOL_INVALID. Device user codes are +contains `credentials` (`{production: token|null}`), `storeAvailable`, ordered +`exchanges` with exact `method`, `path`, HTTP `status`, and JSON `body`, plus +optional `cancelBeforePoll`. This transport consumes the transcript without +network, uses a virtual monotonic clock, and replaces the credential store in +memory. It is not an endpoint override. Requests must match transcript methods +and paths and use a bearer credential only for organization requests. Release +builds ignore this seam. + +The credential predicate is exactly `rr_test_[0-9a-f]{32}`. A malformed or +rejected key is SERVICE_AUTH_REQUIRED naming the variable (see +"Credential teaching and account options"). Device user codes are 1..32 characters from `[A-Z0-9-]`, and must not contain the private device code. Projected organization strings are nonempty, at most 4096 Unicode scalar values, contain no ASCII control characters or DEL, and cannot contain the bearer key. Extra organization fields, including nested private fields, are discarded. -Malformed transcript roots fail SERVICE_PROTOCOL_INVALID: credential must be -null or a string, storeAvailable a boolean, exchanges an array of at most 182. - -## IMP-034 persistent service environments (supersedes staging-only admission) - -Production-default service environment contract, supersedes staging-only paragraph: -- Grammar: prose cli environment show [--json]; prose cli environment use staging|production [--json]; prose cli environment reset [--json]. Global --output json supported. Reset removes stored selection =>production. Use production stores explicit production. Commands require no auth/network/keychain. -- Persistent flat service_environment="staging"|"production" in existing USER cli.toml only. Use existing safe atomic writer preserving other values/comments and validate same protections. No project setting may redirect service. Service resolver reads user only, ignores project; normal project config with service_environment rejects CONFIG_INVALID. Keep service setting out of harness EffectiveValues/config-explain schema, expose via environment show. -- Unset=>production. All auth login/status/logout and org list work by default, no flag needed. Retain --service-environment production|staging as ephemeral account/org override only; reject with environment management or unrelated ops. No generic endpoint env override. -- Production origin https://run-prose-production.openprose.workers.dev; staging https://run-prose-staging.openprose.workers.dev. All OpenProse account/service requests route through selected fixed origin. Model/provider/kernel artifact endpoints unchanged. -- Credentials: OPENPROSE_API_KEY production; OPENPROSE_STAGING_API_KEY staging. Only selected variable consulted. Both filtered from ALL harness/probe env even explicit reallow. Both namespaces independent org.openprose.cli.production and org.openprose.cli.staging account api-key. Backend actually issues rr_test_32hex on BOTH deployments, do not use rr_live_. No fallback across env. Login/logout reject only selected envtoken. Switching never reads/writes/deletes creds. -- Human account/service and environment command output for staging clearly includes 'OpenProse staging'. JSON stays single parseable object with environment production|staging (no banner); device verification stderr remains necessary but contains no credential. Other local/harness human outputs need not change because they make no OpenProse service request. -- Environment command closed output: schema openprose.service-environment/1, environment production|staging, source default|user-config, problem null|runner-error. Success show/use/reset returns selected env+source. Failure invalid config normal existing runner-error envelope allowed; never default silently if malformed user config. service-account/organization-list schemas environment enum both. -- Test seam may optionally contain environment:'production'|'staging', credentials:{production: token|null,staging:token|null}; old credential applies only expected environment when provided. Assert selected env equals fixture.environment if supplied and exchange optional origin equals selected origin. Never reach network when fixture present or credential real stores. -- Shared sequences will test persisted use/show/status/reset, fresh process, precedence, wrong env tokens, fixed origins, malformed/project config, JSON/human indicator. Update existing no-flag account tests to hermetic fixture behavior BEFORE full tests to avoid actual network/keychain. +Malformed transcript roots fail SERVICE_PROTOCOL_INVALID: credentials must be +an object, storeAvailable a boolean, exchanges an array of at most 182. + +## Hosted service operations + +Status: see `protocol/STATUS.md`. +The normative data is `shared/service/operations.v1.json` (the operation +manifest), the schemas it names, and `shared/errors/taxonomy.v1.json`. + +**Scope.** `prose cli` gains runner-owned service operations that reach the +OpenProse hosted service: discovery, pricing, hosted runs, run +records, saved programs, published results, jobs, the wallet and +organizations. They are not harness execution. `prose ` with the +default `openprose` harness still reports `HOSTED_UNAVAILABLE` with unchanged +text; hosted runs are `prose cli run submit`. Phase 5 (§12) is unchanged. + +**Grammar.** `prose [--output human|json|jsonl] cli [ARGUMENTS] +[OPTIONS]`. Nouns are singular. Every operation, its +arguments, options, requests, confirmation class, transport class and +result schema are declared by the manifest, and both products derive parsing, +help and confirmation from the embedded manifest. Each operation's `command` +is its argv after `prose` (`["cli", "run", "submit"]`), as in the capabilities +document. `prose cli service operations --json` prints the published manifest +as the envelope's `result` (one line) and makes no request; the published +manifest is the embedded manifest's public projection +(`shared/service/operations-public.v1.json`, an allowlist of members): each +operation's command, arguments, options, spec, effect, confirmation, output +schema names, exit codes and examples, plus the grammar, exit dictionary, +environment variables and status classification. Service routes and their +catalog entries, service error strings, the service origin, the key format, +the vendored export and this client's own transport, journal and identity +settings are read internally and never printed; the projection records a size +budget and strings that must never appear in it. +Human mode prints a summary table (operation, effect, confirm, usage) and +`--output jsonl` prints one `openprose.service-record/1` line per operation, +in manifest order, then the `openprose.service-page/1` trailer. A service +`--help` in a JSON mode (`prose --output json cli run --help`) prints the +envelope whose result is `{help, operations}`: the help text and the manifest +records of the commands it describes. The manifest also declares `exitCodes`, the exit dictionary keyed by +error code (`{exit, retryable, meaning}`, each equal to the taxonomy entry), +`envVars` (every variable service operations read), and per-operation +`examples` (runnable command lines). `prose cli service capabilities [--json]` +prints the grammar, variables, exit dictionary and the noun/verb +index with examples on one page, or as one canonical +`openprose.service-capabilities/1` line (`shared/schemas/service-capabilities.schema.json`); +it makes no request. Both views are rendered once by +`ci/render_service_help.py` into `shared/service/help.v1.json` (`capabilities`, +`views`) and embedded by both products. `prose cli service guide [--json]` prints the agent guide `shared/service/guide.v1.md` byte for +byte, or its `## ` sections as the result `{sections: [{id, title, body}]}` of +one `openprose.service-operation/1` envelope; it makes no request and needs no +key. Both products embed the file; the `service-help` gate fails when a +`prose cli ...` command in it does not parse against the manifest, when a +required section is missing, or when the generated cases pinning both views +are stale. `prose cli service triage [--json]` is one +read of the session state: it sends `GET /health` anonymously, then, with a usable key, `GET /wallet/balance`, +`GET /organizations/default`, `GET /runs?limit=5` and `GET /triggers`. The +result (`service/discovery.schema.json#/$defs/serviceTriage`) has one section +per read, each `{problem}` or its projected fields with `problem: null`, a +`credential` section (`variable`, `source`, `state`, `problem`) and +`nextCommands` (`{why, argv, env}`) derived from state. It exits 0 whenever +the report is produced; only cancellation and invocation errors end it early. +Without a usable key the account sections are null and the credential +problem names the variable to set; after an unanswered `/health` nothing else +is sent. Sections carry price fields only. Human output is at most 25 lines. The `service-help` gate fails when the +dictionary lacks a code that a shared corpus case emits or expects under a +different exit, lacks a code an operation or the error classification names, +disagrees with the taxonomy, or lists an unexplained code; and when an example +names another command, an unknown or missing required option or argument, or +a confirm-class command without `--yes` or `--preview`. The +`service-operations-*` gates also run every example against the product and +fail when its grammar parser rejects one. When stdout is a closed pipe +(`| head -1`), both products stop writing stdout and exit with the command's +own code without a diagnostic. The global `--model`, +`--harness` and `--dry-run` options are `INVOCATION_INVALID` in the global +position before a service operation. Operation options follow the command +path, and an operation may define an option of the same name (`cli run submit +--model`). Trailing `--json` equals `--output json`. No new environment +variable is read for output; `PROSE_OUTPUT` keeps its §7.4 meaning. `--help` +prints the text in `shared/service/help.v1.json` for that topic exactly. +Every manifest command path has a topic, including the account `auth`, +`org list` and `package` commands. `cli --help`, and +`--help` or `-h` after an account verb, print that topic and never the runner +help. Each operation's "Exit codes:" line is rendered from its manifest +`exitCodes` (`{exit, meaning, codes?}`, ascending). The `service-help` gate +fails when that list disagrees with the error taxonomy, lists a +route-override code for an operation that does not send the route, or +disagrees with the exits and codes that the shared corpora expect from the +operation. + +**No prompts; confirmation.** No service operation prompts. `--yes` is required +when any non-GET/HEAD request's catalog interaction has agent policy `confirm` +or `never`, is `DELETE`, has effect `money`, `outward` or `destructive`, or is +not reversible; the manifest may only tighten this. Without `--yes` the +operation sends no mutation and exits 2 with `CONFIRMATION_REQUIRED`, whose +`details.plannedRequest` describes the planned request: its method, what it +does in words (`description`, the first sentence of the operation's summary), +the body digest, the effect, the non-secret `summary` of the caller's inputs +and, for `run submit` and `job create`, the service hold quote. The service +route, its query and the price policy reference are never shown. `--preview` prints the same plan for any +mutation as a successful result, exits 0 and sends no mutation. The account +operations (`auth`, `org list`, `package`) keep their grammar; the manifest records their +confirmation waivers. + +**Output.** JSON output is one `openprose.service-operation/1` object followed +by LF. It contains `schema`, `operation`, the CLI id, and `interaction`, the +primary service catalog id or null. It names no service environment (a +developer build's human output names its endpoint, below). `result` is the +closed per-operation projection; a paged operation's result carries +`nextBefore`. `problem` is a runner error; when it is non-null, `result` is +null. A `cli` command's `INVOCATION_INVALID` says what was wrong in its +`message`: `Unknown command.`, `Unknown option.` or `That command isn't quite +right.`; the language and the runner commands keep `Runner invocation is +invalid.`. One JSON error shape: in JSON and JSONL modes this +envelope also carries every invocation error of a service command line that +was rejected before an operation ran: a near miss (`cli models`, +`cli run delete`), a command option before `cli`, a missing `cli`, an invalid +or conflicting `--output`, and an account-path refusal +(`cli package frob`). +`operation` is the operation the argv names, meaning the longest run of +command words after `cli` that equals an operation's command, else the literal +`cli`, whose `interaction` is null. JSONL prints the envelope as one line, +except that a streaming operation prints its one `service.failed` line. +Human output is unchanged. A bare `openprose.runner-error/1` is printed only +for language commands and the local runner commands (`cli doctor`, +`cli harness`, `cli config`, `cli cleanup`). A transport failure +(`SERVICE_UNAVAILABLE` with no service response) carries `details.reason`, so +every non-zero exit of a service command line has `problem.details`; the +service corpus runner asserts both over every case. JSONL streams emit `openprose.service-event/1` lines. The service's +`run_complete` frame becomes exactly one terminal `service.completed`, +`service.failed` or `service.detached` line carrying the envelope, and nothing +follows it; `service.detached` is the exit-21 end (detached or the `--wait` +deadline: the run continues), `service.failed` every other failure. End of +stream after an `error` frame is terminal. Unknown event types become +`unrecognized` events carrying only a sanitized name. Heartbeat comments are +ignored. Human mode writes results and text chunks to stdout, and status, +activity and warnings to stderr. A status line carries the status word only +(the service's status message stays in the JSONL event). An agent's structured +final answer (`{status, reason, semantic_diff}`) prints as its words, never as +JSON. Human errors name no HTTP status (`details.serviceStatus` keeps it); an +allowlisted service code prints as `Service code:`. Results keep service field +names verbatim and are built from allowlists: unknown fields are dropped at +every depth. A run's `environment` is its public id (`builtin`, `linux`); the +service's runtime name, runtime contract and environment version are not +output. Signed file URLs and `customer_id` are never output. + +**Lists, times and human output.** A list operation names its +item array in the manifest's `output.records`. With `--output jsonl` it prints +one `openprose.service-record/1` line per item and then one +`openprose.service-page/1` trailer carrying `count`, `nextBefore` (null when +there is no next page) and `meta`, the result without its collection; a failed +list operation prints the envelope. In JSON the cursor is +`result.nextBefore`; error envelopes carry none. Every epoch-ms result field `X` has an additive `X_iso` +(RFC 3339 UTC, or null). Human output uses dollars for money, prints +`No .` for an empty list, flattens nested records into `label: value` +lines (`run show`, the job renderer), prints `Next page:` as a full command and ends the run, job and +model views with copyable `Next:` commands from the one follow-up renderer. + +**Key order.** Every JSON or JSONL stdout line, including the +account, organization-list and package envelopes and every +runner error, is canonical: compact, with object keys sorted recursively as +strings at every depth (so `"10"` sorts before `"2"`), UTF-8 without ASCII +escaping, and one LF per document. Both products print identical bytes for +identical documents; Rust gets this from `serde_json::Value`, Bun from +`output.canonicalJson`. `--output-file` JSON uses the same order with a two-space indent. The one exception is +`cli service operations --json`, whose result is the public projection of +the embedded manifest. +The shared corpora compare stdout bytes, not only parsed values. +Share URLs, Checkout URLs, invitation tokens, webhook endpoints and signing +secrets appear only in the result of the operation that creates them (a +webhook's `endpoint_url` also appears in `job show` when its endpoint is the +secret-free job-id path). Output +contains server-provided prices only and no field matching `/cost/i`. + +**Allowlisted output.** Every result is built by copying the fields its closed +schema (`shared/schemas/service/`) lists; nothing a service record carries +beyond them reaches stdout or stderr, in any output mode. The shared corpus +reruns every case with an unknown member on every service object and requires +identical output. Job output uses the user noun and one casing (`job`, `jobs`, +`max_jobs`, `job_limit`, snake_case fields, the opaque `revision_token`, run +counts in the states `queued`, `running`, `completed`, `failed`, `cancelled`, +`awaiting_billing`). A run's `billing_status` is `settled`, `settling` (a +charge not yet final) or `unknown`. Stream status text names the model +(`Running on MODEL`) and activity carries no tool call reference. A run's +error text prints mapped words (`shared/fixtures/service/run-errors.v1.json`) +with a generic fallback, never the service's stage names; `PROSE_DEBUG=1` +prints the service's text instead. An example that `example show` cannot +fetch is `viewOnWeb` with its `webUrl` when the service names one. Organization +members are numbered (`member N`, by when they joined, with a `handle` when +the service sends one); account and organization ids are not printed, and +`org member role|remove` take `N`, `member N` or an account id. Cursors +(`nextBefore`) are opaque. + +**Errors and exits.** A service response is classified in this order: + +1. its body `code`; +2. then a route override: `/repos` 401 and `PUT /programs/{slug}` 403 map to + `GITHUB_LINK_REQUIRED`. `program save` narrows the 403 by the service + `error` text: only the linked-login refusal keeps `GITHUB_LINK_REQUIRED`, + a reserved handle or reassigned slug is `SERVICE_REQUEST_REJECTED`, and any + other 403 (such as `Invalid API key.`) is `SERVICE_AUTH_REQUIRED`; +3. then its HTTP status, using the manifest's `errorClassification` table. + +`details` carries `serviceStatus` and an allowlisted `serviceCode`. For 400, +409 and 422 on service operations only (not the account operations), it also carries `serviceMessage`: the +service `error` text after the account text validator, at most 512 code +points, with no control characters and with the key redacted. Account +operations keep their frozen mapping and emit no remote text. Exits follow +§7.5. The additional taxonomy codes are: + +- `CONFIRMATION_REQUIRED` (2); +- service errors (10); +- `SERVICE_WATCH_DEADLINE` and `HOSTED_RUN_DETACHED` (21, not retryable: the + run continues, so both carry `details.resumable: true` and + `details.resumeArgv`; retrying the command would start a second run); +- `HOSTED_RUN_FAILED` and `RUN_SUBMISSION_AMBIGUOUS` (22); +- `HOSTED_RUN_CANCELLED` (24, not retryable). + +A hosted cancellation reports the terminal `HOSTED_RUN_CANCELLED` (24). +Exit 30 stays reserved for structured semantic failure. + +**Invocation errors and copyable commands.** Every +`INVOCATION_INVALID` of a service invocation carries an Action for its own +cause, never the generic runner syntax advice. When the Action names one +command, `details.suggestedArgv` holds its arguments after the product name, +and the Action shows the same argv as a shell-quoted `prose ...` line. The +suggestion is one of: + +- a corrected copy of the rejected argv (a did-you-mean noun, verb or option, + a dropped duplicate or extra token, `--dry-run` replaced by `--preview`, a + global option moved before `cli`); +- a follow-up command: the listing that shows valid values for a missing + argument (`run list` for ``, `program list` for a program, and so + on), or the command's `--help`. + +Follow-up commands, `resumeArgv`, `cancelArgv`, and every `prose cli ...` +command quoted in a reason or human hint come from one renderer. It writes +`--output ` when the resolved mode is `json` or `jsonl`, then `cli` and +the words. A copied command therefore never switches to human output. The +`--output` and `--json` tokens that follow a rejected token are still read to +choose the output mode and the suggested commands. The phrase "place global +options before cli" appears only when a global option right after `cli` +(`cli --output json run list`) was rejected. A misspelled service noun, a bare +or unknown verb and a global option after `cli` are service invocation errors. +They render as `OpenProse: INVOCATION_INVALID: ...` in human mode, or as a bare +runner error in JSON, and they never name the executable path. Public builds +print no banner; a developer build prints its custom-endpoint label at most +once, before the first streamed or result byte. A failure that streamed +nothing prints only the error, whose first line already carries the label. +Both products render these bytes identically. The account `auth`, +`package` and `org list` grammars keep their runner rendering for their own +argument errors; an unknown verb in those groups is an intent-inference error +(below). + +**Missing `cli`.** Service words given without `cli` +are an invocation error. They are never forwarded to the language runner and +never billed. After the runner globals, the runner checks the first +unconsumed tokens: + +- **Command words.** Two words that form a command-path prefix of the + manifest or the runner commands (`run submit`, `job list`, + `org member list`, `harness list`), or one word that is a complete one-word + path (`doctor`), are a service command. So is a lone word whose + `nounSynonyms` entry names exactly one command: a complete path, or a + group with one verb, which is appended (`login` → `auth login`, `whoami` → + `auth status`, `models` → `model list`, `money` → `wallet balance`); a + group synonym followed by one of its verbs names that command. A verb before + its group (`list jobs` → `job list`) and a `commandRewrites` phrase (`stop` + → `run cancel`, `delete` → `program delete`, `share` → `run share`, + `cron` → `job create`, `show` → `run list --limit 1`) are service words + too; a rewrite's explanation follows the suggested command in the reason. + Distance is never used before `cli`. When neither word names an + existing file or directory (resolved against `--cwd` or the process working + directory), the result is `INVOCATION_INVALID` (exit 2) rendered by the + service renderer. The reason reads `` is a service command; did you + mean `prose ... cli ...`? Nothing was forwarded or sent. To pass these words + to the OpenProse language instead, put `--` before them`` (``is not a + command`` for a synonym). The Action reads ``Insert cli before the service + command: `prose ... cli ...`.`` (``Use `cli `: ...`` for a + synonym). `details.suggestedArgv` is the original argv with `cli` and the + command words in place of the typed words, with every global kept in place. +- **Language commands.** `grammar.intentInference.languageCommands` is the + SPEC 7.1 list (a contract test compares them). A language command that also + names a service command (`status` → `service triage`, `help [COMMAND]` → + `--help`, `examples` → `example list`) is forwarded when a local harness is + selected. Under the default `openprose` harness, which runs no language + command, it is the `INVOCATION_INVALID` rejection instead (reason + `` is a language command, which runs only with a local harness``). + `run FILE` is always forwarded: its `HOSTED_UNAVAILABLE` refusal keeps the + frozen Action, and `details.suggestedArgv` is `prose ... cli run submit FILE + --preview`. +- **A file of that name.** When one of the words names an existing file or + directory, the argv is a language command and is forwarded unchanged. If + the default `openprose` harness then refuses with `HOSTED_UNAVAILABLE`, the + Action gains ``If you meant the hosted service command, use `prose cli + ...`.`` and `details.suggestedArgv` carries that argv. +- **Global aliases before `cli`.** An `optionAliases` entry whose target is a + `grammar.globalOptions` name (`--format` → `--output`) may appear in the + global position. When `cli` or service command words follow, the alias is + suggested and never applied. Without `cli` or command words after it, the + token stays opaque language input. Any other unknown option before `cli` is + an ordinary unknown-option error. +- **Command options before `cli`.** A command-local + option in the global position (a `commonOptions` flag such as `--json`, + `--yes` or `--preview`, any operation option such as `--limit` or + `--output-file`, or an `optionAliases` spelling of one such as `-j` or + `-y`), followed by `cli` with only such options and runner globals in + between, is `INVOCATION_INVALID` rendered by the service renderer. The + reason reads ``<`flag`[ (canonical)]...> belongs after the command path, not + before `cli`; nothing was forwarded or sent``; the Action reads ``Move + after the command path: `prose ...`.``; + `details.suggestedArgv` keeps the runner globals in place and appends the + options, by canonical name and with their values, after the command path + (before a literal `--`). `--json` among them selects JSON output for the + error. +- **Pre-parse global values.** Before a manifest + command, an `--output` value that is not a mode suggests the mode equal in + letter case or nearest by distance, else `json`. This is a + service-renderer error in both ports and never names the executable path. +- `prose -- ` always forwards, with no hint. + +`prose --help` (`runner-help.txt`) lists every manifest command path and the +exit-code dictionary (every exit code in `shared/errors/taxonomy.v1.json`). +It also lists the service environment variables and a +"For agents" section naming `prose cli service capabilities --json`, +`prose cli service triage --json`, +`prose cli service status --json`, `prose cli service operations --json`, +`prose cli service guide` and `details.suggestedArgv`. +`ci/render_service_help.py --check` (the `service-help` gate) fails when a +manifest command, an exit code or one of those entries is missing. It also +fails when the help names a command that does not exist. The shared case +`framework/help-runner-top-level` pins the bytes in both ports. + +**Intent inference.** A word or option the grammar +does not know is never guessed into execution: it is `INVOCATION_INVALID` +(exit 2) with the corrected argv in `details.suggestedArgv`, even for reads. +The tables live in the manifest at `grammar.intentInference`, and both +products read them: + +- `verbSynonyms` maps a word in verb position to candidate verbs in + preference order; the first candidate that the group has wins (`run ls` → + `run list`, `run get` → `run show`, `wallet get` → `wallet balance`, + `org member rm` → `org member remove`). `nounSynonyms` maps a word right + after `cli` to command words (`organization` → `org`, `whoami` → + `auth status`). `optionAliases` maps a foreign option spelling to candidate + options; the first one the operation accepts wins (`-y` → `--yes`, + `-j` → `--json`, `--format` → `--output`, `--dry-run` → `--preview`). A synonym is tried + only when the word is not itself a command or option in that position. +- Otherwise the suggestion is the unique nearest known word by + optimal-string-alignment distance, in which an adjacent transposition is + one edit (`lsit` → `list`, `shwo` → `show`, `--jsno` → `--json`). The + limit is `distance.shortWordMax` (1) for words of up to + `distance.shortWordLength` (4) characters and `distance.max` (2) beyond. + A tie at the smallest distance suggests nothing. +- The reason reads ``unknown command `cli `; did you mean `cli + `? Commands: `` (or ``unknown command `cli `. Commands: + `` without a match). After `cli`, the list names every first word of + a manifest command and the runner commands `doctor`, `harness`, `cleanup` + and `config`. In a group it names the group's verbs, including the runner + groups `harness`, `cleanup` and `config` and the account groups `auth` and `package`. An unknown + option reads ``unknown option for `cli `; did you mean + ?``. Every such error is rendered by the service renderer, so both + products print the same bytes and never name the executable path. +- Suggestions are complete: `details.suggestedArgv`, + run as given with `details.suggestedStdin` (when present) on standard + input, is never itself `INVOCATION_INVALID`. A corrected noun or verb that + leaves a group without a verb gains the next typed verb, the verb a + misspelled next word means, or the group's only verb (`cli models` and + `cli model` → `cli model list`); a group with several verbs suggests its + `--help`. A corrected bare command whose arguments are all optional + suggests its `--help` (`cli run submt` → `cli run submit --help`). When the + corrected argv names a service command the grammar would still reject, + the correction is settled: the Action reads ``, then `` and `suggestedArgv` is the second fix (`cli run shw` → `cli run + list`, the listing for the missing RUN_ID). `optionConversions` change + units: `--amount 5` (dollars, before or after `cli`) suggests + `--amount-cents 500` and says the dollars were converted; a value that is + not a positive whole number is not converted, the reason states the unit, + and the suggestion is the help. `commandRewrites` map a phrase to a + command plus options (`cli run result RUN_ID` → `cli run show RUN_ID + --file outputs/result.json`; without RUN_ID, `cli run show --help`). An + unexpected argument fills the one missing required value option + (`cli wallet topup 500` → `--amount-cents 500`, `cli job create spec.json` + → `--spec-file spec.json` when the file exists; an `N` option takes only + digits). The argument of an operation with a `secretFileOptions` option + (`cli wallet redeem CODE`, `cli org invitation accept TOKEN`) is presumed + to be the secret: the reason and Action never echo it, and the suggestion + reads the option from standard input (`--code-file -`) with a + ``printf '%s\n' "$CODE" | ...`` example. An unknown option of a + `--spec-file` operation that names a text, interval or enum key of its + `spec` (`cli job update ID --name x`) suggests `--spec-file -`, and + `details.suggestedStdin` holds the JSON object of every such option + (`{"name":"x"}` and a newline); `*_secret` keys are never moved. A + whole-number `--limit` out of range suggests the nearest accepted value + (`--limit 500` → `--limit 200`, `--limit 0` → `--limit 1`). A did-you-mean + in a handler's reason carries the corrected command (`VISIBILITY pubic` + → `public`), and an existing `run download` directory suggests the first + free `DIR-2` … `DIR-99` as `--output-dir`. The service corpus runner reruns + every JSON case's `suggestedArgv` offline in the case's fixture and fails + on `INVOCATION_INVALID` (`service_operations.py --round-trip-only` runs + that check alone). +- Discovery: every `cli ... --help` topic has an + `Examples:` section rendered from the manifest `examples` (an operation's + own; one per child command, in command order, for a group), after the + options or command list and before the global options or `Output:` line. + An option whose value the client or service fills in when it is omitted + carries a manifest `default`, printed as `Default: ...` (`wallet usage + --start` 30 days ago and `--end` today, UTC; `run submit --model`, + `--environment`, `--runtime`); `program save` says a new program is + private. `prose --help` starts its "For agents" section within its first + 25 lines, before the runner commands and options. + `render_service_help.py --check` fails when a topic lacks its examples, or + when "For agents" moves below line 25. +- Help: `-h` is `--help` wherever `--help` is accepted. Bare `prose cli`, + `prose cli help`, `prose cli help ` and a trailing `help` or + `-h` after a command group or path print that topic and exit 0, in every + output mode. + +**Runs.** `run submit` is always live. It mints a session UUID and records +`{session, runId, createdAt, lastSequence, sourceSha256}` in the run journal +before sending `POST /run?live=1&session=` with `X-Session-Id` and +`Accept: text/event-stream`. The journal lives at +`$XDG_STATE_HOME/openprose/cli/production/runs/`, with directories 0700, +files 0600 and no key. The CLI never sends a non-live submission. It never +retries `POST /run` blindly. It resubmits at most once with the same session, +and only when the connection dropped before the first event. A 409 reporting +that the session already has a run proves the first submission was admitted. +The CLI then takes the run id from an `X-Run-Id` header when present, and +otherwise exits `RUN_SUBMISSION_AMBIGUOUS` and keeps the journal entry. Only +the duplicate-session 409 counts (it names a run, or carries the service's +"This session already has a run." text without a `code`); any other 409 is +classified normally, or is `RUN_SUBMISSION_AMBIGUOUS` after a resubmission. +Interrupting `run submit` or `run watch`, or reaching `--wait`, detaches and +never cancels. The `--wait` default is 30 minutes and the maximum is 6 hours. +An interrupt or a lost stream exits `HOSTED_RUN_DETACHED` (21, not +retryable: retrying `run submit` would start a second run) and the deadline +exits `SERVICE_WATCH_DEADLINE` (21). Both carry `runId`, `afterSequence`, +`resumeArgv` and `cancelArgv`. A `--wait` that passes before the run id is +known keeps reading the stream until the id arrives, for at most the stream +connect timeout, then exits 21 naming the run. `RUN_SUBMISSION_AMBIGUOUS` +(22) is left for a submission lost without a run id; its +`details.resumeArgv` is the same `run submit` with `--session S --detach`. +`run submit --session S` of a session that already has a run (this +machine's journal, or the service's duplicate-session 409 naming the run +without a resubmission) exits 0 with that run and `result.reused: true`. +`run submit` and `run watch` results carry `run_id` beside `runId`, and a +detached (or reused) result's `run` is `{run_id, status}`. When the service closes a `run watch` stream +cleanly with nothing past `--after`, the run has already finished, and watch +replays once from 0 to report its outcome. Only `run cancel --yes` cancels, and it reports the wallet's +`reserved_*` beside `available_*`. `run watch`, `run input` and `run cancel` +take the session from the journal unless `--session` is given; with neither, +they read the run record first. + +**Ended runs.** `run cancel` of a run the service +reports as ended (409 `This run has already ended.`) exits 0 with +`{runId, status: "already_ended", runStatus}`: `runStatus` is the run record's +status from `GET /runs/{id}`, or null when the record is not written yet +(404); the wallet is not read. `run input` refused with 409 `This run is no +longer accepting instructions.` stays `SERVICE_WRITE_CONFLICT` (not +retryable), with `details.reason`, `details.runId`, an Action saying not to +retry and `details.suggestedArgv` `cli run show RUN_ID`. `run watch` without +`--session` or a journal entry reads `GET /runs/{id}`: an ended record +(`completed`, `error`, `failed`, `timeout`, `cancelled`) is reported like a +replay (exit 0 with `session: null`, `afterSequence: 0`, `source: "record"` +and `run` projected from the record without `response`; `HOSTED_RUN_FAILED` +22 or `HOSTED_RUN_CANCELLED` 24 with `details.files` and `source`), and a 404 +or another status is `INVOCATION_INVALID` whose reason says the live session +is in the submitting machine's journal, with `suggestedArgv` `cli run show +RUN_ID`. `run download` without `--output-dir` writes to `./RUN_ID` and +still refuses an existing directory. + +**Not found and idempotent deletes.** Every operation +that addresses a resource by an argument has a manifest `notFound` entry +(`resource`, an `id` template over its arguments, an optional `list` command, +an optional `hint`, and `serviceCodes` overrides such as +`organization_not_found`). A `SERVICE_RESOURCE_NOT_FOUND` on that operation +carries `details.resource` `{kind, id}`, a `details.reason` naming the id +(``job was not found``) unless the handler gave +a more specific one, `details.suggestedArgv` for the listing command, and the +Action ``Run `` to find the right , then retry with it.`` (or +``Check the identifier named in Detail, then retry with the right one.`` +when no command lists it). A 404 on one of a run's files names the file +(`kind` `file`, `id` `RUN_ID/PATH`, `suggestedArgv` `cli run show RUN_ID`). A +`SERVICE_REQUEST_REJECTED` Action names only the details present (``Correct the +request as details.serviceMessage describes, then retry.``); with none it +points at the operation's `--help`, also as `suggestedArgv`. Human output +never names a JSON field in an Action: the rejected Action names the printed +lines instead (``Correct the request as the Detail and Service message lines +above describe, then retry.``), and the catalog Actions of +`SERVICE_WATCH_DEADLINE`, `HOSTED_RUN_DETACHED`, `HOSTED_RUN_CANCELLED` and +`RUN_SUBMISSION_AMBIGUOUS` point at the command their `Detail:` line names. +JSON keeps the catalog Action. A confirmed +`program delete` or `job delete` whose DELETE is a 404 exits 0 with +`{..., deleted: true, alreadyAbsent: true}` (`already_absent` for a job): +the goal state holds, so a retry after a lost response is not a failure. + +**Idempotence only where it is true.** `alreadyAbsent` +is reported only for a 404 on a well-formed identifier the caller owns: +- `job delete` requires `JOB_ID` to be a lowercase UUID + (`8-4-4-4-12` hex digits, as `job list` prints it). Anything else, including + a path-like value such as `..`, is `INVOCATION_INVALID` (exit 2) before any + request, with a reason naming the value (and the lower-case form when that + is a UUID), the Action ``List your jobs with `` and pass one of + their ids.`` and `suggestedArgv` `cli job list`. +- An own-scope `OWNER/SLUG` (`program save|visibility|delete|revisions`, + `result publish|unpublish`) whose `OWNER` cannot be confirmed as the caller + (the caller's `GET /programs/{slug}/revisions` is a 404 or an empty list) is + `INVOCATION_INVALID` (exit 2) before any write, `--preview` included: the + reason says ``OWNER "X" is not confirmed as you`` and names the bare slug to + pass, the Action names the bare slug and `cli program list`, and + `suggestedArgv` is `cli program list`. That 404 is never mapped to + `alreadyAbsent`. (A first `program save` keeps its own reason asking for the + bare slug.) When `OWNER` is confirmed, the plan (`--preview` result and + `CONFIRMATION_REQUIRED` `details.plannedRequest`) carries + `owner: {handle, verified: true}` and human output prints + `Owner: HANDLE (verified as you)`; a bare-`SLUG` plan has no `owner`. +- `run cancel` and `run input` without `--session` or a journal entry read + `GET /runs/{id}` (the manifest's `run.read` request; `run input` gains it as + request 1) before the confirmation gate. An ended record (`completed`, + `error`, `failed`, `timeout`, `cancelled`) makes `cancel` exit 0 with + `{runId, status: "already_ended", runStatus}` (nothing is POSTed, the wallet + is not read, `--yes` is not needed because nothing changes) and makes + `input` `SERVICE_WRITE_CONFLICT` (exit 10, not retryable) with + `details.reason` naming the status, `details.runId`, `details.source: + "record"`, the ended-run Action and `suggestedArgv` `cli run show RUN_ID`. + A 404 or a status that is not ended is `INVOCATION_INVALID` whose Action + names the verb (``Cancel the run from the machine that submitted it (its run + journal holds the live session) or pass that session with --session UUID; + nothing was cancelled. ...``, and the `input` equivalent), with + `suggestedArgv` `cli run show RUN_ID`. Cancel is therefore idempotent from + any machine. + +**Registry errors.** A registry 404 is +`SERVICE_RESOURCE_NOT_FOUND` (exit 10, not retryable), never +`SERVICE_UNAVAILABLE`: fetch and withdraw name `{kind: package, id: +ORG/NAME@VERSION}` with `suggestedArgv` `cli package list ORG`; list and +publish name the organization with `cli org list`. 413 and 429 stay +`SERVICE_UNAVAILABLE`. An invalid `cli package ` invocation is +`INVOCATION_INVALID` in the `openprose.service-operation/1` envelope in both +ports, before any request, with a per-cause reason (missing `ORG` or +`ORG/NAME@VERSION`, an option the command does not take with the ones it does, +a duplicate or valueless option, a missing required option, a value that is not +a slug, a version, a cursor or a digest) and `suggestedArgv` `cli package + --help`; a missing or unknown command is a bare `INVOCATION_INVALID` +naming the four commands. A human listing with no public packages prints +`No public packages in ORG.` Human registry errors use the service error +layout (`