npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@opentrain-ai/cli

v0.41.0

Published

Agent-first command-line client for the OpenTrain public API: author and assign LMS retraining, author/publish adaptive job Task Checklists (file-first machine envelope), run employer-only Reviewer Feedback review/triage/access/settings, and manage jobs,

Readme

OpenTrain CLI

Command-line client for the OpenTrain public API. Built for coding agents and scripts: register an agent account, draft and publish jobs, review proposals, hire freelancers, manage contracts and milestones, message, poll updates or register webhooks, and manage credits and your team — all from the terminal. The same CLI also maintains the canonical Instructions pages for a job, builds and assigns Project To-dos and quizzes, runs the complete Training/LMS authoring and review lifecycle, and drives DEV-2741 selective quiz coaching for employers: todos quiz coaching … (queue/state/threads reads, draft feedback, redo selection, accept, planned + confirmed send-back), plus the distinct FULL-retake todos quiz remediate plan|apply lane. Every coaching subcommand answers --help with its focused contract; every mutation demands a caller-supplied stable --idempotency-key. Agents can also discover the live contract, filter/export coaching work, and attach private feedback images entirely from the CLI. todos quiz coaching image upload is the safe default; image download --out streams, verifies, and atomically places a worker image so the calling agent can inspect it without exposing a signed storage grant.

Freelancer API keys are deliberately read-only: they can search public jobs and read the owner's authorized messages. They cannot send messages, answer or submit quizzes, use coaching actions, or mutate account data.

Money never moves from the CLI. Ordinary hire, milestone funding/approval, and contract-end requests record a pending approval for a signed-in human. The separate default-off admin-balance-* family is available only after the server independently verifies every canonical OpenTrain-admin gate; it writes an internal funding marker with a $0 funding movement and never exposes payout release, which remains human-authorized.

Batch hire approvals (default-off preview)

proposals hire-batch [--job-id <id>] --idempotency-key <stable-value> accepts one of repeated --proposal-id, --proposal-ids <csv>, or --file <json>. Use 1–20 unique applicants on one job. The file contains defaults and items (with per-person milestone/rate overrides), never a confirmation or payer ID. Without --job-id, an authorized proposal read resolves the job. Dates require full ISO datetimes with timezone. See proposals hire-batch help for flags.

The response is one human review link, not a completed hire. A person reviews, edits or deselects applicants and confirms the priced revision. Each selected hire then executes independently; successes survive other failures. The initial milestone uses the bid; a different requested rate needs worker acceptance. Repeat uncertain requests with the same input/key. Read current per-item results with proposals hire-batch get --batch-id <id>, including after new requests are disabled. --pretty retains the full receipt; write commands refuse --fields and conditional-read options. Existing one-ID proposals hire is unchanged.

Restricted admin-balance milestone cancellation (0.27.3)

Verified canonical OpenTrain platform admins can cancel an eligible internally funded milestone without moving money:

opentrain admin-balance-milestones cancel \
  --contract-id <contract-id> --milestone-id <milestone-id> \
  --idempotency-key <stable-key> [--notify-counterparty] [--pretty]

The operation is request-idempotent and returns fundingDisposition=NO_MONEY_MOVED and moneyMoved=false; it never creates a Stripe refund or releases a payout. Notifications are suppressed unless --notify-counterparty is explicitly provided. Card- and credit-funded milestones stay on the signed-in human approval path and are refused by the server. This command requires CLI 0.27.3 and the matching serving API.

Reusable private video library

CLI 0.11+ exposes the rollout-gated opentrain videos family. Agents can list and inspect organization-owned videos, upload a local video directly through a one-time resumable grant, poll/cancel processing, record draft Instructions or LMS placement references, prepare/download an MP4 rendition, and fetch playback metadata. Run opentrain videos --help for the complete command list.

Video bytes never pass through OpenTrain JSON. videos download grant|save requires an API token explicitly minted with videos:download. Recording a reference does not insert a TipTap/LMS node or publish a worker-visible pin; those are separate, atomic product mutations.

OpenTrain Agreements documents (rollout-gated)

The opentrain agreements family prepares organization-owned PDF agreements for e-signature. It needs a token with agreements:read (reads) or agreements:write (mutations) and the server-side Agreements rollout. Run opentrain agreements help for the complete command list.

opentrain agreements documents create --title "Contractor NDA" --key contractor-nda
opentrain agreements documents upload --document-id <id> --file nda.pdf --expected-revision <n>
opentrain agreements fields set --document-id <id> --expected-revision <n> --fields-file fields.json
opentrain agreements preview --document-id <id> --pages 1,3 --out ./previews
opentrain agreements publish --document-id <id> --expected-revision <n> --confirm-live

Executed agreements (both providers). agreements requests lists the organization's signing requests: the native ones attach created (native:<id>) and legacy Dropbox Sign envelopes (dropbox_sign:<id>, provider: DROPBOX_SIGN). Each row says whether the exact executed PDF is stored (executedDocument.available, SHA-256, size). To read what one worker signed on a contract and save the executed PDF, hash-checked and audited:

opentrain agreements requests list --contract-id <contractId> --json
opentrain agreements requests get dropbox_sign:<attemptId> --json
opentrain agreements requests download dropbox_sign:<attemptId> --artifact signed --out ./ica-signed.pdf

For a whole job roster, page agreements requests list --job-id <jobId> and download --artifact signed for every row whose executedDocument.available is true. remind and audit take native:<id> requests only.

documents upload is the only upload command: it hashes the PDF locally, reserves a one-time signed PUT, streams the bytes straight to private storage and finalizes with the local SHA-256 so the server verifies what it stored. The signed URL, its headers and storage paths never appear in human output, the JSON envelope, or errors; re-run with the same --idempotency-key to resume an interrupted upload. preview returns short-lived image handles; --out <dir> downloads each page with your token to <dir>/page-<n>.png and refuses to overwrite unless --force. publish freezes an immutable version and requires --confirm-live. Every mutation sends an Idempotency-Key (default cli-<uuid>, announced on stderr) and accepts --dry-run to print the planned request without any network call. API failures keep the server's details.reason (for example REVISION_CONFLICT, CHECKSUM_MISMATCH, PUBLISH_WARNINGS_UNWAIVED).

Install

npm install -g @opentrain-ai/cli
opentrain --help

Or run without installing:

npx -y @opentrain-ai/cli@latest whoami

When running from a monorepo checkout that already has an @opentrain-ai/cli workspace, include @latest so npm resolves the published package instead of an unbuilt local workspace.

Updating while agents are running

opentrain version check reports whether a newer release exists and prints the install command; the CLI never updates itself. npm install -g replaces the package in place, so for a few seconds during an update the opentrain command can be missing (opentrain: No such file or directory) for any process that calls it. To keep long-running agent loops safe:

  • Update between runs, not while an agent is mid-task. If a call fails with "No such file or directory" right after an update started, wait a few seconds and retry the same command.
  • Or pin a version per project, which a global update never touches: npm install --save-dev @opentrain-ai/cli@<version> and run it with npx opentrain …, or call npx -y @opentrain-ai/cli@<version> … directly.

Existing account: browser login (0.27.0+, rollout-gated)

Each browser-auth command has focused, offline --help (for example, opentrain auth login --help and opentrain auth login wait --help). Help never creates a pending login or contacts the server. During polling, temporary 503/429 responses honor server-directed backoff without creating a new login; resume the same loginId after a wait timeout, until its reported expiry.

Browser-login commands are included in CLI 0.27.0+. The server migration and agent_browser_login rollout must be delivered before login can succeed. A disabled server refuses the request; do not create another account as a workaround. Existing API-key login remains supported. Package publication does not enable the separate browser-login rollout.

# The agent starts a login, then gives the returned HTTPS link/code to the person.
opentrain auth login --no-wait --json

# After the person signs in and explicitly approves, resume the SAME handle.
opentrain auth login wait --login-id <loginId> --timeout 120 --json
opentrain auth status --json

The person can use a browser on a different computer or phone. The normal OpenTrain approval page displays the matching short code, identity and requested permissions. No local callback, Vercel login, copied API key or password sharing is needed. The agent must not approve on the person's behalf. Browser login does not grant new roles, bypass restricted-admin policy, or approve money movement.

auth login without --no-wait prints the link on stderr and waits up to --timeout seconds (default 600); stdout carries one final result. --open explicitly asks to open the link on the CLI machine, which is usually unnecessary for a remote agent. PENDING is a pending human decision, not authentication. Resume the same loginId; after DENIED or EXPIRED, a new login needs a new explicit request. auth login cancel --login-id <loginId> removes that local pending handle; the server-side approval expires independently.

The login receipt verifies the new browser session, but an environment API key can still take precedence for later commands. Read the reported credential source and shadowing warnings before using the account. auth logout revokes and removes the selected saved browser session; it preserves a separately saved API key and a new login completed meanwhile. Static-key-only logout retains its existing local removal behavior. Offline logout reports local removal separately from unconfirmed server revocation. It never revokes an environment key.

Employer and freelancer Settings also expose Connected agents alongside API keys for inspecting and revoking your own browser-login connections. Revoking one ends that grant's access; it does not revoke unrelated API keys or another user's connection. If the list cannot be loaded, Settings reports it as unavailable and offers a read-only retry rather than claiming there are no connections.

New account only: anonymous agent onboarding

# 1. Create an anonymous agent account with a pre-claim API token
#    (job drafting/publishing + read access). Saved to local config.
opentrain auth register --agent-name "My Agent"

# 2. Draft a job from a plain-text description — OpenTrain parses it
opentrain jobs draft create --description-file ./job-description.txt

# 3. Fill any missing fields the draft output asks for
opentrain jobs draft update --job-id <id> --set pricePerHour=25

# 4. Publish when the draft is publish ready
opentrain jobs publish --job-id <id>

# 5. To unlock hiring, messaging, and team invites, hand the account to a human:
opentrain auth claim --email [email protected]
opentrain auth claim-status --wait

API keys remain supported for CI and existing integrations. Already have a personal API token (ot_pat_…) from the OpenTrain app? Create it from Employer Settings -> Developer -> API keys, then copy it once:

opentrain auth login --api-key ot_pat_<publicId>_<secret>
opentrain whoami

Commands

Execution commands accept --pretty for prose instead of the machine envelope. Registry-declared read-only commands also accept --fields <path,path,…>: dot paths of data, with element-wise array traversal (e.g. --fields messages.id,messages.content). Writes, mixed-action commands, JSONL streams, watch mode and --pretty refuse projection before dispatch. Malformed paths exit 2. Paths that matched nothing in this result appear in meta.projection.unmatchedFields with ok: true; empty arrays and nulls satisfy traversal. meta.projection also names selected paths, omitted top-level keys (worked out from the request) and retainedPaths — what was kept without being asked. Continuation, truncation and completeness siblings (nextCursor, hasMore, truncated, *Truncated, incomplete, complete, *Complete, partial, valuesExhaustive) are retained, even nested and even when false — a marker is a boolean, a number, null, a cursor string (nextCursor / resumeCursor) or a flat map of such flags (the contracts history truncated block); a marker-named key holding any other string, an array or a nested object is walked like any subtree, so only the markers inside it are kept — and the JobOps sync / syncRecovery blocks are always kept whole (the next covered call acknowledges them by presenting the token); the API response is unchanged and rerunning a read without the flag retrieves everything (when the invocation wrote its payload to a local file — checklists checkout --dir, a download --out, an export --output — read the written file instead; fullRetrieval says which, per invocation). Discover fields with a full read first, and retain whole documents when editing them: text-only projection cannot safely preserve marks or attrs. Projection adds overhead on small responses; first-use field discovery can cost more than it saves. See the cold/warm measurements in output-fidelity.json.

Repeated sweeps use conditional reads: --digest adds meta.digest (a stable content digest of the final data, after --fields), and --if-none-match <digest> re-runs the same read and, when the fresh data's digest equals the one you hold, writes data: null with meta.notModified: true and the continuation meta intact; when something changed it writes the complete data and its new digest. Only data you proved you already hold is withheld, and the API call still happens, so a polling loop is read --fields … --digest once, then read --fields … --if-none-match <meta.digest> until notModified is false. An invocation that received a JobOps sync / syncRecovery delivery on any page — the transport records it, whether or not the block survives in the final data — is never withheld (its digest is still reported): the token the CLI presents on the next covered call acknowledges only deliveries you were shown. Sync-covered reads (every manager … read, the bridged messaging/LMS reads) therefore never benefit; the conditional form is for the plain reads you sweep repeatedly. It is refused for an invocation that writes its payload to a local file (--out / --output / --dir): the receipt's digest does not cover the file — and when the path arrives inside --input instead, the receipt is never withheld (the write is recorded as it happens; notModified stays false). Both are machine-envelope controls (not with --pretty).

When a page DID change, --delta (with --if-none-match) sends only what moved: every scalar of the page (continuation included) plus the complete collection records that are new or changed, and meta.delta — the collection key, the fresh order and the unchanged / changed / added / removed ids — so you rebuild the exact fresh page from the one you hold (take changed/added records from data, the rest from your copy, in meta.delta.order); meta.digest is the fresh page's digest for the next sweep. It works for pages with one collection of id-bearing records whose previous shape this CLI remembered (ids and record digests only, in a 0600 state file, for 24 hours); any other case — a page this CLI never digested, a different collection, no collection — writes the complete data with no meta.delta, so nothing is ever lost.

Large artifacts go to a file, not through your context. An export with an output path (lms assignments export --output, lms attempts export --output, todos quiz coaching export --out) places the COMPLETE payload atomically (a torn or partial file never appears at the path: the bytes go to a sibling <path>.partial-<pid>-<hex> renamed into place only once every byte is written — a short write, a full disk, a quota or a size limit fails with OUTPUT_WRITE_FAILED rather than placing a truncated file — and an existing regular file is replaced in place with its exact mode kept; a symbolic link at the path is refused before any request (OUTPUT_SYMLINK_REFUSED, naming the link's target so you can pass it directly — the CLI never writes through a link), as are a directory (OUTPUT_IS_DIRECTORY), a device/FIFO/socket (OUTPUT_NOT_REGULAR_FILE) and a file this process cannot write (OUTPUT_NOT_WRITABLE), all exit 2; a file-size limit (ulimit -f) fails as OUTPUT_WRITE_FAILED rather than killing the CLI; a signal that kills the CLI mid-write can leave that partial file behind, safe to delete, with the target untouched; because the file is replaced by rename, an existing target's hard links stop sharing its content and you become the file's owner) and prints a compact receipt — the payload's own keys minus the content, plus savedBytes, the sha256 of exactly the bytes written and contentType — never the artifact. lms assignments export --output walks every page from --cursor on into that one file (json = one array of rows, jsonl = one row per line, csv = the header once), page by page, and reports rowCount, pages, startCursor, complete: true (complete from startCursor), nextCursor: null. Under --output, --limit is the walk's page size (1–5000), not a row cap — --limit 10 --output f used to write ten rows and now writes the whole export in pages of ten. A walk the bounded page cap (200 pages) interrupts writes nothing and fails with LMS_EXPORT_PAGE_CAP, whose details.resumeCursor resumes into a second file. Query the file locally (jq, rg, code) and verify it by the receipt's sha256; the measured workflow (three pages, 27 KB through stdout versus a 455-byte receipt) is in output-fidelity.json under large-artifact.

Every other command that writes a path you supply goes through the same writer with the same refusals, checked before any request (for agreements preview, before the first page download — the page list comes from the preview read): open-label exports download --out (and its server-named default, which is only ever a plain file name in the current directory), manager setup plan --out, todos quiz export --out and todos quiz results-export --out, agreements preview --out <dir> (every page's path, before the first page download) and agreements requests download --out, videos download save --output, instructions plan --out and the apply journal beside the plan (<plan>.journal.json, checked before any request under --confirm-live), the checklist bundle files checklists init|checkout --dir place, the LMS preview evidence and audit files staged for publication, and todos quiz coaching image download --out. agreements requests download, agreements preview and videos download save keep their no-overwrite default as a typed refusal — an existing file is OUTPUT_EXISTS (exit 2) until you pass --force — and place the file exclusively, so a file or link that appears between the check and the placement is never clobbered either. checklists init|checkout --dir refuses an existing bundle or base file as a usage error until you pass --force, and LMS preview evidence publication never replaces an existing destination (use an empty --out directory). The other writers (including open-label exports download and todos quiz coaching image download) replace an existing regular file in place, keeping its mode, and still never write through a link. Where the envelope reports the file it now carries savedBytes, sha256 and contentType beside its earlier fields (additive).

Contract list/get/history responses support the additive dashboard-link feature negotiation header. CLI 0.27.x sends it automatically; other HTTP clients can opt in with X-OpenTrain-Features: contract-dashboard-url-v1. Clients that omit the header receive the legacy response shape.

opentrain --version
opentrain whoami [--pretty]
opentrain auth register [--agent-name <name>] [--org-name <name>] [--base-url <url>] [--force] [--pretty]
opentrain auth claim --email <human-email> [--claim-token <token>] [--pretty]
opentrain auth claim-status [--wait] [--timeout <seconds>] [--claim-token <token>] [--pretty]
opentrain auth login [--no-wait] [--timeout <seconds>] [--open] [--device-label <label>] [--lane EMPLOYER|WORKER] [--scope <scopes>] [--base-url <origin>] [--json]
opentrain auth login status --login-id <loginId> [--json]
opentrain auth login wait --login-id <loginId> [--timeout <seconds>] [--json]
opentrain auth login cancel --login-id <loginId> [--json]
opentrain auth login --api-key <token> [--base-url <url>] [--json]
opentrain auth status [--pretty]
opentrain auth logout
opentrain jobs draft create --description <text> [--title <title>] [--external-id <id>] [--idempotency-key <key>] [--json]
opentrain jobs draft create --description-file <path> [--title <title>] [--external-id <id>] [--idempotency-key <key>] [--json]
opentrain jobs draft create --canonical-file <path> [--external-id <id>] [--idempotency-key <key>] [--json]
opentrain jobs draft create --payload-file <path> [--external-id <id>] [--idempotency-key <key>] [--json]
opentrain jobs draft update --job-id <id> --set <field=value> [--set <field=value> ...] [--json]
opentrain jobs draft update --job-id <id> --patch-file <path> | --patch-json <json> [--json]
opentrain jobs list [--status <status>] [--limit <n>] [--cursor <cursor>] [--json]
opentrain jobs search [--q <text>] [--category <slug>] [--language <lang>] [--country <iso>] [--pay-type PAY_PER_HOUR|FIXED_PRICE|PAY_PER_LABEL] [--limit <n>] [--cursor <cursor>] [--json]
opentrain jobs publish --job-id <id> [--json]
opentrain jobs invite --job-id <id> --freelancer-id <id> [--json]
opentrain jobs close --job-id <id> [--json]
opentrain jobs update-published --job-id <id> --set <field=value> [--set <field=value> ...] [--json]
opentrain jobs update-published --job-id <id> --patch-file <path> | --patch-json <json> [--json]
opentrain jobs interview set --job-id <id> (--required | --not-required | --default) [--modality voice|text] [--json]
opentrain contracts list [--job-id <id>] [--status active|ended] [--participation-state ACTIVE|OFFBOARDING|OFFBOARDED_PAYABLE|CLOSED|REACTIVATED] [--json]
opentrain contracts get --contract-id <id> [--json]
opentrain contracts history (--contract-id <id> | --proposal-id <id>) [--view current|timeline] [--json]
opentrain contracts end --contract-id <id> [--json]
opentrain milestones create --contract-id <id> --description <text> [--name <name>] [--amount <usd>] [--volume <n>] [--due-date <iso>] [--json]
opentrain milestones fund --milestone-id <id> [--json]
opentrain milestones approve --milestone-id <id> [--json]
opentrain approvals get --approval-id <id> [--json]
opentrain proposals list --job-id <id> [--status <status>] [--sort recent|screening-score] [--limit <n>] [--cursor <cursor>] [--json]
opentrain proposals get --proposal-id <id> [--interview] [--json]
opentrain proposals interview-audio --proposal-id <id> [--output <path>] [--url-only] [--force] [--json]
opentrain proposals hire --proposal-id <id> (--amount <usd> | --hours <n>) [--rate <usd/hour>] [--milestone-name <name>] [--milestone-description <text>] [--due-date <iso>] [--confirm-not-fit-override] [--json]
opentrain contracts propose-rate --contract-id <id> --rate <usd/hour> [--reason <text>] [--idempotency-key <key>] [--json]
opentrain freelancers get --id <user-id-or-slug> [--json]
opentrain talent search [--q <text>] [--skill <s>] [--language <l>] [--country <ISO>] [--badge VERIFIED_TRAINER] [--talent-type Individual|Agency] [--availability <text>] [--min-reputation <n>] [--tier PROVEN|ESTABLISHED|PROVISIONAL] [--available-now] [--sort relevance|reputation|rate_asc|rate_desc|recent] [--limit <n>] [--offset <n>] [--json]
opentrain talent reputation --slug <profile-slug-or-user-id> [--json]
opentrain messages list [--filter all|job|proposal] [--unread-only] [--limit <n>] [--cursor <cursor>] [--json]
opentrain messages unread [--filter all|job|proposal] [--limit <n>] [--cursor <cursor>] [--json]
opentrain messages read --conversation-id <id> [--limit <n>] [--direction older|newer] [--cursor <cursor>] [--json]
opentrain messages send --conversation-id <id> --content <text> [--json]
opentrain messages send --conversation-id <id> --content-file <path> [--json]
opentrain messages edit --message-id <id> --expected-revision <n> --idempotency-key <key> (--content <text> | --content-file <path|->) [--json]
opentrain messages delete --message-id <id> --expected-revision <n> --idempotency-key <key> [--json]
opentrain messages start-proposal-thread --proposal-id <id> [--json]
opentrain updates poll [--cursor <cursor>] [--limit <n>] [--json]
opentrain credits show [--json]
opentrain credits ledger [--cursor <cursor>] [--limit <n>] [--json]
opentrain credits top-up --amount <usd> [--json]
opentrain credits top-up-status --top-up-id <id> [--json]
opentrain webhooks create --url <url> --events <type,...> [--json]
opentrain webhooks list [--json]
opentrain webhooks get <webhook-id> [--json]
opentrain webhooks delete <webhook-id> [--json]
opentrain tokens list [--json]
opentrain tokens revoke --token-id <id> [--json]
opentrain team show [--json]
opentrain team invite --email <address> [--json]
opentrain payments pending [--json]
opentrain capabilities [--json]
opentrain jobs draft create --description <text> [--title <title>] [--external-id <id>] [--idempotency-key <key>] [--pretty]
opentrain jobs draft create --description-file <path> [--title <title>] [--external-id <id>] [--idempotency-key <key>] [--pretty]
opentrain jobs draft create --canonical-file <path> [--external-id <id>] [--idempotency-key <key>] [--pretty]
opentrain jobs draft create --payload-file <path> [--external-id <id>] [--idempotency-key <key>] [--pretty]
opentrain jobs draft update --job-id <id> --set <field=value> [--set <field=value> ...] [--pretty]
opentrain jobs draft update --job-id <id> --patch-file <path> | --patch-json <json> [--pretty]
opentrain jobs list [--status <status>] [--limit <n>] [--cursor <cursor>] [--pretty]
opentrain jobs search [--q <text>] [--category <slug>] [--language <lang>] [--country <iso>] [--pay-type PAY_PER_HOUR|FIXED_PRICE|PAY_PER_LABEL] [--limit <n>] [--cursor <cursor>] [--pretty]
opentrain jobs publish --job-id <id> [--pretty]
opentrain jobs invite --job-id <id> --freelancer-id <id> [--pretty]
opentrain jobs invitations list --job-id <id> [--freelancer-id <id>] [--state INVITED|DECLINED|APPLIED|HIRED] [--limit <n>] [--cursor <cursor>] [--all] [--pretty]
opentrain jobs invitations get --job-id <id> --freelancer-id <id> [--pretty]
opentrain jobs close --job-id <id> [--pretty]
opentrain jobs draft discard --job-id <id> --confirm [--pretty]
opentrain jobs draft clients [--pretty]   (admin accounts: valid msClientId values)
opentrain jobs update-published --job-id <id> --set <field=value> [--set <field=value> ...] [--pretty]
opentrain jobs update-published --job-id <id> --patch-file <path> | --patch-json <json> [--pretty]
opentrain jobs interview set --job-id <id> (--required | --not-required | --default) [--modality voice|text] [--pretty]
opentrain jobs interview help   (what each setting means; --default = required unless an Open Label work assessment replaces it)
opentrain contracts list [--job-id <id>] [--status active|ended] [--limit <n>] [--cursor <cursor>] [--all] [--pretty]
opentrain contracts get --contract-id <id> [--pretty]
opentrain contracts history (--contract-id <id> | --proposal-id <id>) [--view current|timeline] [--pretty]
opentrain contracts end --contract-id <id> [--pretty]
opentrain milestones create --contract-id <id> --description <text> [--name <name>] [--amount <usd>] [--volume <n>] [--due-date <iso>] [--pretty]
opentrain milestones fund --milestone-id <id> [--pretty]
opentrain milestones approve --milestone-id <id> [--pretty]
opentrain admin-balance-milestones create --contract-id <id> --idempotency-key <key> --description <text> --amount <usd> [--volume <hours-or-labels>] [--name <name>] [--due-date <iso>] [--pretty]
opentrain admin-balance-milestones fund --contract-id <id> --milestone-id <id> --idempotency-key <key> [--pretty]
opentrain approvals get --approval-id <id> [--pretty]
opentrain proposals list --job-id <id> [--status <status>] [--sort recent|screening-score] [--limit <n>] [--cursor <cursor>] [--pretty]
opentrain proposals get --proposal-id <id> [--interview] [--pretty]
opentrain proposals interview-audio --proposal-id <id> [--output <path>] [--url-only] [--force] [--pretty]
opentrain proposals hire --proposal-id <id> (--amount <usd> | --hours <n>) [--rate <usd/hour>] [--milestone-name <name>] [--milestone-description <text>] [--due-date <iso>] [--confirm-not-fit-override] [--pretty]
opentrain contracts propose-rate --contract-id <id> --rate <usd/hour> [--reason <text>] [--idempotency-key <key>] [--pretty]
opentrain freelancers get --id <user-id-or-slug> [--pretty]
opentrain talent search [--q <text>] [--skill <s>] [--language <l>] [--country <ISO>] [--badge VERIFIED_TRAINER] [--talent-type Individual|Agency] [--availability <text>] [--min-reputation <n>] [--tier PROVEN|ESTABLISHED|PROVISIONAL] [--available-now] [--sort relevance|reputation|rate_asc|rate_desc|recent] [--limit <n>] [--offset <n>] [--pretty]
opentrain talent reputation --slug <profile-slug-or-user-id> [--pretty]
opentrain messages list [--filter all|job|proposal] [--unread-only] [--limit <n>] [--cursor <cursor>] [--pretty]
opentrain messages unread [--filter all|job|proposal] [--limit <n>] [--cursor <cursor>] [--pretty]
opentrain messages read --conversation-id <id> [--limit <n>] [--direction older|newer] [--cursor <cursor>] [--pretty]
opentrain messages send --conversation-id <id> --content <text> [--pretty]
opentrain messages send --conversation-id <id> --content-file <path> [--pretty]
opentrain messages edit --message-id <id> --expected-revision <n> --idempotency-key <key> (--content <text> | --content-file <path|->) [--pretty]
opentrain messages delete --message-id <id> --expected-revision <n> --idempotency-key <key> [--pretty]
opentrain messages start-proposal-thread --proposal-id <id> [--pretty]
opentrain updates poll [--cursor <cursor>] [--limit <n>] [--pretty]
opentrain credits show [--pretty]
opentrain credits ledger [--cursor <cursor>] [--limit <n>] [--pretty]
opentrain credits top-up --amount <usd> [--pretty]
opentrain credits top-up-status --top-up-id <id> [--pretty]
opentrain webhooks create --url <url> --events <type,...> [--pretty]
opentrain webhooks list [--pretty]
opentrain webhooks get <webhook-id> [--pretty]
opentrain webhooks delete <webhook-id> [--pretty]
opentrain tokens list [--pretty]
opentrain tokens revoke --token-id <id> [--pretty]
opentrain team show [--pretty]
opentrain team invite --email <address> [--pretty]
opentrain payments pending [--pretty]
opentrain capabilities [--pretty]
opentrain instructions help
opentrain todos help
opentrain lms help
opentrain manager help

Every non-stream command returns one versioned JSON success or failure envelope by default. Pass --pretty for the human-readable view. Existing --json calls remain valid, but the flag is a deprecated no-op during this transition release.

Shared job operations (manager)

opentrain manager help (CLI 0.11.0+) is the context-first control surface for managing an existing employer job alongside other people and agents. One manager context get --view compact call automatically carries the top-10 fresh priority attention signals with their derived claim state, signal counts, per-rule evaluator health, the effective typed policies, and the Agent Guide digest — there is no second bootstrap command.

Automatic by design — the human never needs to know to ask for this setup. The first job-scoped manager call starts or resumes the durable per-agent sync session, and its bootstrap envelope directs exactly one read of the composite Agent Guide (sync.nextOperations ends with job_operations.agent_guide.get — manager agent-guide get: the canonical typed projection plus the human-published Playbook); afterwards, whenever the Playbook head moves past what the agent could have held, the covered response's recommendation.agent_guide_changed names the exact manager playbook delta read (added/changed sections, removedSectionIds tombstones) so it refreshes only what changed, and the agent acknowledges only a revision that was actually delivered to it. Agents define this job's own work types and workflow in its Work Catalog (manager work …) instead of fixed grading/retraining enums, and propose missing or outdated guidance as Playbook proposals (manager playbook proposals submit; RECOMMENDED, never enforced) — only the human job manager publishes (manager playbook publish). The composite Agent Guide, Playbook reads and deltas, proposals, and publication need CLI 0.26.0+.

Every manager mutation (CLI 0.16.0+) has command-specific discovery: leaf --help (e.g. opentrain manager actions create --help) prints that operation's exact required/optional body fields with types, enums, and bounds, the dedicated-flag partition, evidence guidance, and a copy-paste example accepted by the real server validator; --print-schema on the same commands prints the machine-readable opentrain-cli-schema/1 envelope with zero authentication and zero network. Schema-discovery-by-production-400 is over.

CLI 0.11.0 automatically keeps every covered job-scoped call synchronized. It stores a server-minted session credential and the latest per-job sync tokens in a private, per-agent-instance state file, then threads them without prompting. Every covered JSON response includes sync and syncCoverage; inspect sync.conflicts, sync.recommendations, and sync.nextOperations before the next action. Raw credentials and signed tokens never appear in CLI output. Deleting local sync state is safe—it only causes an evidence-backed bootstrap. syncCoverage.coverage is exhaustive: native and bridged calls refresh shared context, while none explains why that operation does not. A durable agent instance is taken only from PASEO_AGENT_ID or OPENTRAIN_AGENT_INSTANCE; without one the CLI safely stays ephemeral instead of accidentally sharing state through a working directory. Ephemeral covered calls additionally receive the advisory-only recommendation.establish_durable_agent_session envelope entry naming the exact fix (export a unique per-agent-instance value BEFORE running commands); never reuse one instance value across concurrent agents — that would share a cursor.

  • manager sync poll waits up to 25 seconds for a fresh shared-context update; --watch emits one bounded JSON response per poll.

  • manager intents declare|list|resolve is the write-ahead coordination lane. Declare before spontaneous work so another manager sees ownership and a duplicate agent receives the live winner instead of racing.

  • manager proposals submit|list|review|withdraw records a non-enforced alternative when the requested workflow conflicts with current team policy, claims, or intent. Reviewing a proposal never executes its suggested change.

  • manager overlays get|set keeps guidance at its correct scope: TEAM is owner-controlled shared guidance, PERSONAL is one manager's durable memory, and SESSION is one agent instance's scratch memory. Overlays never change canonical gates or policies.

  • manager messages send is the synchronized live-job send path. It requires a stable --idempotency-key and the complete claimed-action guard, including the exact latest authorized root/thread message id (or literal none only when the conversation was observed empty). The server resolves the canonical Job Message or Proposal Message job, commits the message and context advance atomically, and refuses stale or cross-job sends. It requires both messages:manage and messages:write; ordinary messages send remains the backward-compatible unsynchronized path.

  • manager signals list|get|acknowledge|snooze|dismiss|claim — deterministic attention signals, highest priority first. claim is the ONLY path that turns a signal into owned work (triage never claims) and requires the exact --expected-signal-revision; eligibility is ACTIVE + FRESH + effective OPEN/ACKNOWLEDGED, non-shadow.

  • manager policies effective|list|record|verify-enforcement — typed, attributed, superseding durable policies (never chat or free text).

  • manager guide get [--format json|markdown] — the canonical typed projection of the Agent Guide alone (without the Playbook; guideVersion + SHA-256 checksum), kept for published clients — new work reads manager agent-guide get. The same Markdown is the MCP resource opentrain://jobs/{jobId}/operations/guide.

  • manager setup status|plan|apply (CLI 0.15.0+) — the guided operational setup flow. Shared setup is product-driven: while a job's operating brief, typed workflow, or an applicable required operating choice is missing, every covered response's sync envelope carries the deterministic recommendation.job_setup_incomplete entry pointing here. The DRIVEN multi-agent flow runs in order: status (machine packet + inProgress view) → manager intents declare … --intent-key setup.guided (a DUPLICATE names the live winner but does not transfer ownership) → plan … --adopt-intent <yourIntentId> → human approval → apply … --confirm-live → verify, resolve your intent, hand off. plan composes the exact canonical writes ZERO-WRITE with a deterministic planReceipt, never guesses an unanswered required choice, and fails closed on a truncated inProgress view, foreign active setup.* intents/proposals, or INDETERMINATE evidence (follow the packet's evidenceRecovery read). apply strictly re-validates the whole plan against the closed guided-setup grammar before ANY request leaves the process — exactly the canonical brief publish and guided typed-policy records for this job, exact paths, content-bound idempotency keys, no unknown fields or duplicates; the planReceipt is integrity/approval evidence, never authority. It also fails closed without --confirm-live, on shared-context drift, on INDETERMINATE evidence, and on foreign setup.* coordination. Both plan and apply prove ownership by exact durable-session identity: the intent's sessionId must equal the current response's sync.session.sessionId with a matching principal. A winner ID visible in a DUPLICATE result is never adoptable, and a copied plan cannot be applied from another session. The command then executes and prints the verified shared state. Once setup reports COMPLETE or DEFERRED, stop prompting — completion is shared across the employer team. An approved postponement is an ordinary OPERATING_RULE.setup_deferral_* typed policy.

  • manager work definitions|executions|migrations … (CLI 0.23.0+, DEV-3191) — the dynamic per-job Work Catalog. Work is PER-JOB catalog data, never a platform enum: every covered response's sync.workCatalog teaches the catalog head/status, the winner-only setup lease, pending human approvals, and this job's work-coordination mode. Definition-bound mutations present the FULL binding (versionNumber + contentHash + coordinationSignature); every execution mints ONE coordinating action that is the single claim authority. An INFERRED proposal never activates without the typed WORK_DEFINITION_APPROVAL.<definitionKey> policy recorded by a non-agent human job manager, after every possible-duplicate candidate needing a decision is resolved (WORK_DUPLICATE_RESOLUTION.<first>.<second>; advisory same-shape evidence never blocks).

  • Work-coordination mode (sync.workCatalog.coordination): the singleton typed policy WORK_COORDINATION_MODE — recorded only by a non-agent human job manager — declares how strictly this job requires declared work: OPEN (default when no row exists; the catalog is advisory), COORDINATED (every guarded mutation such as manager messages send must present a claimed action bound to an OPEN work execution, defined or provisional), MANAGED (that execution must be DEFINED work under an ACTIVE definition). A null mode is fail-closed evidence (no current head, a branched lineage, or an unreadable head) — never OPEN. A refused guarded mutation is a 409 whose details carry reason: JOB_OPS_WORK_COORDINATION_REQUIRED, requirement (WORK_EXECUTION_REQUIRED | DEFINED_WORK_REQUIRED | MODE_UNREADABLE), and mode; the JSON error envelope passes them through unchanged.

  • External attestations (manager observations record with authority OBSERVED and a top-level externalAttestation): the catalog cannot see browser edits, chat/ticket/spreadsheet tools, or out-of-band mutations, so that state is UNKNOWN unless a CURRENT attestation covers it. ATTEST carries a bounded per-job source.system slug, optional locator/subject, a claim, a non-future attestedAt, a REQUIRED expiresAt (≤ 30 days), and an optional supersedesObservationId; INVALIDATE names the attestation a webhook, UI, agent, or human retired. Attribution is canonical: pass the top-level actorAgentLabel (stored as createdByAgentText); a nested label is refused. The server proves the same-context lineage before writing (readable non-future target, same source + subject, one live ender per target, ONE current head per source + subject — a second root is refused and told which head to supersede). manager guide get renders externalCoverage: per-entry status, attribution, bounded evidence, and the ending receipt (who ended it, why, when, evidence).

When a message suggests repeated confusion, that is an agent inference—not a deterministic OpenTrain alert. Record it as INFERRED, show a zero-write LMS retraining outline, and obtain explicit human approval before creating or assigning retraining. Recommendations and proposals are never write authority.

These commands require an OpenTrain server exposing the job_operations.signals.* / job_operations.policies.* / job_operations.guide.get operations (opentrain capabilities lists them); the base manager family needs CLI 0.8.0+, the attention/policy/guide commands need CLI 0.10.0+, automatic sync/intents/proposals/overlays need CLI 0.11.0+, guided setup (manager setup) needs CLI 0.15.0+, the Work Catalog, coordination modes, and external attestations need CLI 0.23.0+, and the composite Agent Guide + Playbook family (manager agent-guide get, manager playbook …) needs CLI 0.26.0+.

Shared folder Instructions (0.27.0+)

Use the same canonical editor pages across every eligible child job. Discover and review before activation; ordinary reads never hide job guidance:

opentrain instructions folder inspect --folder <folderId> --include-content
opentrain instructions folder preview --folder <folderId> --request request.json
opentrain instructions folder confirm --folder <folderId> --request request.json --plan-digest <reviewedDigest> --confirm
opentrain instructions folder status --folder <folderId> --operation <operationId>
# Wait until status.quiesceReadyAt, then apply the same operation.
opentrain instructions folder apply --folder <folderId> --operation <operationId> --confirm
opentrain instructions folder recovery --folder <folderId> --operation <operationId>

Preview lists every affected job, copied/hidden page, destination order and expanded audience. Activation preserves originals/checkpoints/media and commits the complete destination atomically. Require VERIFIED_READBACK; after an uncertain response inspect the same operation, never create another key. Cancel/resume and guarded reversal are described in instructions folder help.

After activation, use exactly one selector, --folder <folderId> or --job <jobId>, for page/tree, import/assets, links, history, schema, bundles and verify. Existing job clients remain compatible. Folder authoring refuses an empty target until reviewed activation and refuses incompatible published/unknown delivery without changing release/read/ack flags. Last-page archival does not restore hidden job pages. See the folder workflow.

Canonical job instructions

opentrain instructions reads and edits the same pages shown in the actual job Instructions tab. A job ID is mandatory; there are no standalone workspaces, manuals, bindings, or publication copies.

opentrain instructions inspect --job <job-id>
opentrain instructions page get --job <job-id> --page <page-id>
opentrain instructions page link --job <job-id> --page <page-id> \
  --anchor <stable-node-id>
opentrain instructions page preview --job <job-id> \
  --content-file ./handbook.tiptap.json
opentrain instructions page create --job <job-id> --title "Reviewer handbook" \
  --key reviewer-handbook-v1 --content-file ./handbook.tiptap.json \
  --confirm-live
opentrain instructions import docx --job <job-id> --file ./handbook.docx \
  --title "Reviewer handbook" --key reviewer-handbook-docx-v1 --confirm-live
opentrain instructions page patch --job <job-id> --page <page-id> \
  --expected-checksum <sha256> --ops-file ./patch.json --confirm-live
opentrain instructions verify --job <job-id>
opentrain instructions visual-check --job <job-id>

# Controlled multi-page work: checkout → edit files → diff/validate → immutable
# plan → review plan JSON → one atomic canonical-draft tree swap.
opentrain instructions checkout --job <job-id> --dir ./job-instructions
opentrain instructions diff --dir ./job-instructions
opentrain instructions validate --dir ./job-instructions
opentrain instructions plan --dir ./job-instructions --out ./job-instructions.plan.json
opentrain instructions apply --plan ./job-instructions.plan.json --confirm-live
opentrain instructions verify --job <job-id> --against-bundle ./job-instructions

Every page reports its contentSchemaVersion. Schema v1 retains the original job-document node set; schema v2 adds details, callouts, YouTube, audio, and attachment blocks. Upgrade an existing page explicitly, then re-check it out before authoring v2 content:

# No-write status and impact plan
opentrain instructions page upgrade-schema --job <job-id> --page <page-id>

# One-way v1 → v2 upgrade. --wait follows the quiescence window to completion.
opentrain instructions page upgrade-schema --job <job-id> --page <page-id> \
  --target 2 --confirm-live --wait

# Convert a supported YouTube URL to the canonical safe TipTap node offline.
opentrain instructions youtube-node --url "https://youtu.be/jNQXAC9IVRw" \
  --title "Project walkthrough"

Fresh checkouts use bundle format opentrain-job-instructions-bundle/2 and record the schema version of every page. Older version-blind bundles fail closed; run a fresh checkout instead of hand-editing their manifest. YouTube URLs are normalized to the privacy-enhanced player and arbitrary iframe HTML, playlists, tracking parameters, and non-YouTube hosts are rejected.

Job instruction writes change the canonical employer draft. Create, replace, and DOCX import preview without writing until you add --confirm-live; every other draft mutation requires that flag. Before a shared-draft write, the CLI obtains a short-lived signed preflight token bound to the exact job, actor, operation, content, and canonical state. The server revalidates it, then the CLI reads the canonical page back and verifies its checksum or removal. Every successful operation returns the exact employer URL where the result is visible.

Checkout directories are non-authoritative working copies. plan fails on remote drift and records the exact archive set and normalized content reviewed by the operator. apply stages deterministic TipTap documents and swaps the entire page tree in one database transaction, so workers never see a partially assembled multi-page manual. Interrupted retries converge using the same idempotency key; regenerate the plan instead of editing plan JSON.

Pages support multiple roots plus one subpage level, matching the job editor: for example, Start Here, Annotators, QC, and Language-Specific Pages can each be level-one roots, with direct children beneath them. A child cannot parent another page. Do not put every page under Start Here; use a separate root when a section is independent.

For page creation, --parent <pageId> makes a new page a direct child of an existing root; omit --parent to create a root. For an existing page, page move --parent <rootPageId> reparents it under that root, while page move --root promotes it to a root and page move --direction up|down changes sibling order. The one-level guard refuses a parent that is already a child, so creating a second nested level requires a root/child rewrite. A page that already has children cannot be moved beneath another page; move its children first.

# Independent level-one roots
opentrain instructions page create --job <jobId> --title "Start Here" --key <key> --confirm-live
opentrain instructions page create --job <jobId> --title "Annotators" --key <key> --confirm-live

# One direct child beneath an existing root
opentrain instructions page create --job <jobId> --title "Annotation Rules" \
  --parent <annotators-page-id> --key <key> --confirm-live

# Reorganize an existing page
opentrain instructions page move --job <jobId> --page <pageId> \
  --parent <annotators-page-id> --confirm-live
opentrain instructions page move --job <jobId> --page <pageId> --root --confirm-live

Folder ACTIVATE and APPEND share the same reference: each newPages[].parentRef is either null for another new root or the ref of a root in the same newPages request. It is not an existing folder page ID. Use page create --folder <folderId> / page move --folder <folderId> for existing pages. A page with children cannot be moved beneath another page until its children move first. Page content is worker-visible, so keep editor-only setup or internal notes out of published pages; in an internal authoring brief, a clearly labelled EDITOR-ONLY — PAGE SETUP codeBlock or callout can distinguish setup from ordinary guide prose and real cross-links below.

DOCX import converts headings, lists, tables, formatting, links, and embedded images deterministically. Preview returns the embedded-image manifest and stable final URL plan without uploading; confirmed import uploads those exact bytes and inserts the canonical URLs. Direct image uploads accept JPEG, PNG, WebP, GIF, and AVIF up to 4 MB. Whole-document replacement additionally requires the last-read checksum and --confirm-replace; normal rich edits use checksum-guarded node patches. Run opentrain instructions page patch help for the six stable-node operations and examples.

visual-check returns the authenticated employer URL, exact sidebar order, page checksums, expected images, viewports, and screenshot assertions. It does not pretend an employer token proves the worker reader: release verification of that surface requires a real test worker/contract instruction session.

Search runs the employer editor's ranked full-text search (typo-tolerant fallback) over the job's effective draft scopes — its own set plus the folder set it inherits, exactly what inspect and page get read; never a worker's released view. Results are page groups with up to five block hits each (stable block ids, anchor ids for deep links, the nearest heading, inert snippets with code-point highlight ranges). truncated: true means the ranked window filled — narrow the query rather than page on; hasMore/nextCursor page the ranked page groups (the cursor is bound to the job, the query and the ranked window: a stale one is INVALID_CURSOR). --fields keeps the three markers.

opentrain instructions search --job <job-id> --q "lockout tagout" --limit 5
opentrain instructions search --job <job-id> --q "lockout tagout" --cursor <next-cursor>

Read Required compliance is the employer matrix the dashboard shows: one row per participating contract with the worker's state against the job's published releases (NO_REVIEW_REQUIRED / SYNCING / ACTION_REQUIRED / MARKED_READ / UNDERSTOOD), the outstanding count and the last read / understood timestamps. Workers are referenced by workerUserId with a masked display name; worker acknowledgments themselves never cross the public API. The read is gated by the Read Required rollout for the actor (outside it: 404, like the release commands) and pages by hasMore/nextCursor.

opentrain instructions compliance --job <job-id> --limit 50
opentrain instructions compliance --job <job-id> --cursor <next-cursor> --pretty

The revision read is the cheap change poll: the job set's revision counter and the inherited folder set's, folded into one comparable, job-bound revisionToken (also the strong HTTP ETag, so raw callers get 304 with If-None-Match until something changed; the body carries no clock, so the CLI's --digest / --if-none-match are stable across unchanged polls too). A different token means a committed structure or content change — it never says what; read inspect, page get, history or release list. There is no durable event log or webhook.

opentrain instructions revision --job <job-id>
opentrain instructions revision --job <job-id> --if-none-match <digest>   # data withheld while unchanged

Draft history exposes the employer-only page checkpoints captured automatically on every canonical draft write (and by sweep recovery), plus explicit manual checkpoints and a destructive restore:

opentrain instructions history --job <job-id> --page <page-id>
opentrain instructions history show --job <job-id> --page <page-id> \
  --checkpoint <checkpoint-id>
opentrain instructions history compare --job <job-id> --page <page-id> \
  --checkpoint <checkpoint-id> --to current
opentrain instructions history checkpoint --job <job-id> --page <page-id> \
  --key <idempotency-key>
opentrain instructions history restore --job <job-id> --page <page-id> \
  --checkpoint <checkpoint-id> --expected-checksum <sha256> \
  --key <fresh-idempotency-key> --confirm-restore --confirm-live

Listing uses opaque page-bound cursors (--cursor, from nextCursor). Compare diffs the checkpoint against the current draft (or another checkpoint): ADDED blocks exist only in the current draft and would be removed by a restore; REMOVED blocks would be brought back. Restore is destructive: it replaces the live page content, so it refuses to run without --expected-checksum from a fresh compare plus both --confirm-restore and --confirm-live, always records a durable undo baseline checkpoint first, and returns 409 if the live page changed since the checksum was read — compare again, take the fresh checksum, and retry with a NEW --key. After success the CLI verifies storage + checksum readback only — never collaboration sync or browser rendering; confirm visually in the job Instructions tab.

For accounts in the immutable-release rollout, workers read snapshots captured from the canonical job draft. Prepare, preview, and publish are explicit sequential operations; publishing requires a delivery policy on every surface:

opentrain instructions release prepare --job <job-id>
opentrain instructions release preview --job <job-id> --candidate <candidate-id>
opentrain instructions release publish --job <job-id> --candidate <candidate-id> \
  --operation-key <key> --delivery silent|notify|read-required --confirm
opentrain instructions release status --job <job-id>

Candidates expire after 30 minutes. silent changes the effective release without a worker notification, notify sends a lightweight notification, and read-required creates an acknowledgement obligation plus reminders. The release records the server recommendation and any employer override. Actors outside the rollout receive 404 from release commands. Whether a confirmed write reaches workers at once or only on publish is stated per read by opentrain instructions capabilities (mutations.workerVisibility).

Training/LMS

opentrain lms works on the same canonical course draft as the dashboard. A dashboard-owned and CLI-owned copy never exist. Capability discovery tells an agent the active shared TipTap profile, registry version, per-block author/edit/render support, private-asset semantics, active schema, asset limits, assessment settings, job linking support, and Training controls before it generates a bundle. Lesson contentDoc values are canonical TipTap JSON; agents must not generate HTML or a CLI-only content model:

opentrain lms capabilities
opentrain lms courses create --title "Reviewer onboarding" \
  --origin-job <job-id> --key reviewer-onboarding-v1
opentrain lms courses recover-create --key reviewer-onboarding-v1

LMS reads also return canonical human-navigation links[]. The --pretty view prints them under Canonical links:; the default JSON envelope preserves each link's kind, audience, URL, and scoped identifiers. Send EMPLOYER links to employers (job Training, job-scoped course/preview/version/module/lesson/review/release impact) and only returned LEARNER links to assigned workers (their Manage Job → Project To-dos shell). Do not reconstruct routes from IDs or replace the server-returned origin. URLs contain no credentials and are reauthorized when opened; locked future learner lessons are not returned.

The safe hybrid authoring loop is checkout → edit → diff → reconcile when the dashboard changed → validate → push. Pull only fast-forwards a locally clean bundle; reconcile merges non-overlapping local and dashboard edits and refuses true conflicts. Push checks the remote draft revision before any asset upload and again before the draft mutation. The dashboard can reopen and edit every current-profile block emitted by the CLI, and checkout/push preserves those blocks, marks, attributes, ordering, and asset IDs without conversion through the compact Native Forms editor.

Courses can persist an advisory authoringIntent in course.yaml and the canonical draft tree. It controls authoring validation and worker-equivalent Preview defaults without becoming a second delivery engine. Legacy courses default to STANDARD + SYSTEM; INDIVIDUAL_MASTERY requires SYSTEM. Checkout, diff, reconcile, push, publish, and immutable versions preserve the declaration. Assignment and retraining records remain authoritative for what a worker actually receives.

opentrain lms checkout --course <course-id> --dir ./course
opentrain lms diff --dir ./course
opentrain lms pull --dir ./course
opentrain lms reconcile --dir ./course
opentrain lms validate --dir ./course --remote --key validate-course-v1
opentrain lms push --dir ./course --key push-course-v1

Private assets use prepare → direct-to-storage upload → verified finalize. Videos above one 6 MiB chunk use resumable TUS automatically; the current effective direct-upload video ceiling is reported by lms capabilities (256 MiB at this release). Upload grants are transient secrets and are never printed.

opentrain lms assets upload --course <course-id> --kind VIDEO \
  --file ./walkthrough.mp4 --key walkthrough-video-v1
opentrain lms assets upload --course <course-id> --kind AUDIO \
  --file ./listen-first.mp3 --key listen-first-audio-v1
opentrain lms assets list --course <course-id>
opentrain lms assets get --course <course-id> --asset <asset-id>

Quiz questions can carry media too (image, audio, or video on the prompt, a title/instructions block, or a section description). Upload the clip against the assessment with the same prepare → upload → finalize contract, then reference the returned asset id from the question's promptDoc as an image, audio, or video node. The kind is derived from the file extension (.png .jpg .gif .webp / .mp3 .m4a .ogg .wav / .mp4 .webm .mov); ceilings are 5 MiB images, 32 MiB audio, 256 MiB video. The server re-verifies the stored bytes (container signature + SHA-256) before the asset becomes READY, and publishing pins referenced assets to the version.

opentrain lms assessments assets upload --assessment <form-id> \
  --file ./listen-and-answer.mp3 --key q3-audio-v1
opentrain lms assessments assets list --assessment <form-id>

Publishing, job linking, and assigning learners are separate confirmed actions. None silently performs another, with one convenience: linking a TRAINING course also turns on the job's Training and Project To-dos settings (Project To-dos only when the token also has project_todos:write; the response's jobSettingsEnabled says which ones it turned on), so lms training set is only needed to turn Training off or on by itself. lms courses create --origin-job <job-id> only records which job a course is for: the course is not on that job until it is linked. lms training status reports training.readiness: ready, or warnings (ORIGIN_COURSES_NOT_LINKED, TRAINING_SETTINGS_OFF) with the exact commands to run. Check it before telling anyone the training is live. Omitting --stage when the course is already linked keeps the link as it is, so re-running a link never demotes a SCREENING course:

opentrain lms publish --dir ./course --confirm-publish --key publish-course-v1
opentrain lms training link --job <job-id> --course <course-id> \
  --confirm-link --key link-course-v1
# Application-stage screening: every NEW proposal on the job is enrolled
# automatically (published course required; at most one per job).
opentrain lms training link --job <job-id> --course <screening-course-id> \
  --stage SCREENING --max-attempts 2 --confirm-link --key screen-course-v1
opentrain lms training set --job <job-id> --enabled true \
  --confirm-training --key enable-training-v1
opentrain lms assign --job <job-id> --course <course-id> --version <version-id> \
  --audience SELECTED --selected-contracts <contract-id> --policy SYSTEM \
  --confirm-assign --key assign-course-v1

opentrain lms assign help, lms status help, and lms versions help (or --help) print the focused leaf usage before any validation or network call. Assignment status is paged and truthful: every page carries hasMore plus nextCursor, --pretty says PARTIAL RESULT with the exact next-page command (never treat a partial page as the complete learner population), and --all collects every page — effective page size sent on each request, de-duplicated by assignment id, repeated-cursor safe, with collection metadata — and fails the command on any page error:

opentrain lms status --job <job-id>                            # first page (50)
opentrain lms status --job <job-id> --cursor <nextCursor>      # next page
opentrain lms status --job <job-id> --limit 200                # custom page size
opentrain lms status --job <job-id> --all                      # complete retrieval
opentrain lms status --assignment <assignment-id> --include-runs   # one record
opentrain lms versions get --course <course-id> --course-version <version-id>

To see which quiz questions people find hard, read the per-question results for a job. Counts use each person's first submitted attempt at each quiz version (employer review remediation is excluded), grouped by course, lesson and quiz version: right / wrong / partly right / skipped / awaiting-grade counts, choice counts with the correct choice marked, and who missed each question with their answer. Names are always masked as "First L.". Hired workers are the default audience; --audience APPLICANT reads screening quizzes. --pretty prints one line per quiz with its hardest question:

opentrain lms question-results --job <job-id> --pretty
opentrain lms question-results --job <job-id> --audience APPLICANT --course <course-id>

Course and version reads carry a privacy-safe attribution block (creator, last material updater, version author, publisher) with stable actor ids and an expli