Echo/Docs

Immutable journeys, one contract, many runtimes

Echo's onboarding control plane publishes journey versions immutably, hashes their canonical JSON, and serves the same contract to TypeScript, Swift, and Rust runtimes.

Journey publication#

A journey draft is published from Onboarding Studio. The version is stored immutably with a SHA-256 of its canonical JSON and bound to its adaptive experiment; publish, activate, and rollback are the only transitions. A bad journey is recovered by activating a previous version rather than editing one.

parseJourneyDefinition rejects any definition that is not schema version 1 before it can be stored.

Bundle and events endpoints#

Clients fetch the published definition with a bearer ONBOARDING_BUNDLE_TOKEN and report runtime events back to the same surface. Only the ten enumerated onboarding event names are accepted, and bodies above the configured limit are refused.

HTTP
GET  /api/onboarding/bundle?product_id=…&journey_id=…   # ETag, revalidated on every read (private, no-cache)
POST /api/onboarding/events                              # attempt, answers, exposures, outcomes

Runtimes#

  • Rust: wisent-onboarding-client (crates/onboarding-client in echo-web), executing the journey contract against wisent-integrations.
  • Swift: WisentOnboarding from the wisent-ai/echo package, compiled into Wisent's desktop and iOS products.
  • TypeScript: the runtime in wisent-ai/onboarding-web.
  • All three validate the same closed journey contract: guardrails, sticky assignment, canonical offline fallback, first-action and first-success semantics. Consumers keep their own renderer and action registry.

The GitHub star request#

Every Wisent product asks a person for a GitHub star once, after their fifth successful action, through one journey of these runtimes: star-request. Its definition ships with the Rust runtime as crates/onboarding-client/journeys/star_request.json in echo-web, and the threshold of five is the operator's number, kept with his words in journeys/numeric-provenance.json beside it. The Swift and TypeScript runtimes build the same definition, with the same SHA-256 and the same journey version id.

A product builds the journey from three values: its catalog id, the name a person reads, and star_repository, the public OWNER/REPO its Stado catalog record names for the request to link to (a product whose own repository is private names a public one). Rust star_request_bundle(product_id, product_name, star_repository), Swift StarRequest.bundle(productId:productName:starRepository:) and TypeScript starRequestBundle(productId, productName, starRepository) refuse a star_repository that is not OWNER/REPO: star_repository "<value>" is not OWNER/REPO; it is the catalog record's star_repository (`stado product registry set <product> --star-repository OWNER/REPO`).

After each successful action the product calls observe_action(successful_actions) (observeAction in Swift and TypeScript). The count is stored as the answer to that fact, and the journey moves from its counting screen to its star_request screen when the count reaches the threshold. The call answers that screen once, on the action that reached it, and nothing on every other call. The screen's presentation holds title, body and repository (https://github.com/<star_repository>), and its actions are open_repository, later and dismiss. Dismissing is skip: the journey is no longer in progress, and no later action shows the request. reset counts again from zero. A stored count that is not a whole number is refused: the recorded count of successful_actions is <value>, not a whole number.

The journey version id is the definition's SHA-256 written as a version-5 UUID. Stored progress is resumed only for the same version id, so a product that builds the bundle in every process keeps its count across runs, and a changed definition (another repository, name or threshold) counts afresh. Command-line programs on Node import FileJourneyStorage from @wisent-ai/onboarding-web/node, which keeps the journal at $XDG_STATE_HOME/<product>/onboarding.json (default ~/.local/state/<product>/onboarding.json), and OfflineJourneyTransport when no control plane is declared.

Each runtime's tests/star suite runs the real client on real storage (a file for Rust and Node, an isolated UserDefaults suite for Swift): the request appears at the fifth action and only then, a dismissed request never returns, a new process reads the same count and dismissal, a reset counts from zero, the refusals above are their exact sentences, and the definition equals the Rust runtime's file named by ECHO_WEB_STAR_REQUEST_TEMPLATE.

Wisent App package acquisition#

Wisent App declares @wisent-ai/onboarding-web as file:../onboarding-web in both npm manifests. Local development uses the canonical sibling checkout. Release workers receive the source archive pinned in inputs.onboarding-web, not GitHub credentials or the removed package path inside Echo Web.

Publish a selected committed revision with stado release catalog input pin "$WISENT_APP_CHECKOUT" --name onboarding-web --source "$ONBOARDING_SOURCE" --revision "$ONBOARDING_REVISION" --json, then commit and push the updated manifest. Both web quality and build use --link-input ../onboarding-web=onboarding-web/onboarding-web because the archive carries its root directory. Stado Desktop exposes the same operation under Releases → Publish an immutable build input.

From Wisent App, run STADO_BIN="$STADO_BIN" ONBOARDING_SOURCE="$ONBOARDING_SOURCE" ONBOARDING_REVISION="$ONBOARDING_REVISION" WISENT_APP_TEST_SUCCESS_EXIT=0 node tests/release/onboarding-package.mjs. It downloads and checks the real pinned archive, installs the package with npm ci --offline, compares its entry with the selected source commit, and checks npm's EUSAGE refusal for a missing local source.

Reports in .build/onboarding-package/run-*/ retain source and binary identities, input metadata, commands, exit statuses and output. Missing objects report not_found; digest mismatches, missing packages and failed installs fail qualification. This checks package acquisition, not the complete application build, deployment, Desktop screen or browser onboarding journey.