hacifootprint
The map

Journeys & journey frames

A journey is a named multi-step flow the planner commits to; the frame narrows the served space to the journey's steps plus a guaranteed escape.

A journey is a named multi-step flow: does (the planner text), steps (action ids by qualified path or unambiguous suffix, resolved loudly at build time), and an optional when precondition. Journeys are what Mode B serves as fixed tools — one tool per journey, forever.

It is also the one relational thing you declare. A preferred order toward a goal is meaning, and meaning is yours. Everything else relational — a step's dependencies, what would free a greyed control, the route to a page — is DERIVED from declarations you already made for other reasons, so none of it can drift from your graph.

Frames — on-demand disclosure

Opening a journey (commitJourney, or calling its Mode B tool with no step) opens a frame: the served action space narrows to the frame's steps plus authored cancel/back plus the synthetic leave-journey escape — guaranteed for every frame that opens, so a committed planner is never locked in. The frame's result discloses readySteps (fireable now), laterSteps (with status), and awaitingState (steps whose fire has not yet been reported back — see the settlement rule).

journeyPlan(id) returns the derived dependency DAG with live per-step status — and THROWS on an unknown id, because every in-library caller passes an id the spec just yielded. tryJourneyPlan(id) is the same plan for an id you did not author (a model's, a URL's): { ok: true, plan } or { ok: false, reason: 'UNKNOWN_JOURNEY', known } — the identical failure shape commitJourney() returns, so a caller holding a model-supplied id handles the question one way.

Where a journey stands

journeyPlan answers what may I fire next. The question a reader actually has between turns is a different one: whose turn is it, and is this thing moving. session.journeyStanding(id) answers that with one word and the facts behind it.

const where = .('buy');
const where: JourneyStanding

JourneyStanding is a pure fold over the plan, the ask book, the decisions book, retained settlements and frame history: no state of its own, no cache, no timer, and it never fires. It computes fresh on every call, so the word is true about now. It throws on an unknown id, exactly as journeyPlan does and through that method's own refusal — serving layers resolve names first.

An open frame governs; otherwise a latest-closed 'completed' frame answers 'done', and a cancelled or demoted one contributes history and never a verdict. Then the steps are walked in chain order, and the first one not done — the governing step — names the standing:

standingwhat holds itevidence carries
'awaiting-human'the governing step's card is openaskId, step
'declined'the human answered no through their own dooraskId, step
'with-the-human'the decision belongs to a personstep, made, about?, madeBy?
'failed'its LAST attempt came to rest badlytransitionId — a pointer, never the receipt
'blocked'its guard was evaluated and failedblockedOn
'in-progress'nothing holds itguardUnevaluated when present
'done'every step is donethe counts

Every arm also carries stepsDone and stepsTotal, so a journey nobody has started reads 'in-progress' with stepsDone: 0 — the honest reading of open, and nothing holds it.

'failed' is never minted from a pause. Not from needs-confirm, not from a relayed decline, not from any approval refusal, not from a guard, disabled or materialization refusal. A refusal is not an execution: nothing ran, so nothing failed. It requires a fire that actually came to rest badly.

The strings live on this type alone — no existing union grew for them. Both Mode B doors serve the word from this same call (whats_here's journey rows and the journey tool's result), never a second derivation, so two doors cannot disagree about one chain. It sits beside judgment, which answers the other question: judgment is what is my move this turn, standing is where does this chain stand.

The per-step carrier

A step is a name — or the object element { step: 'place-order' }, which compiles identically.

steps: ['enter-address', { step: 'choose-shipping-speed' }, 'place-order']

It carries nothing beyond step today. It exists because per-step conditional metadata has to have exactly one authoring carrier: deciding it once means the next such feature lands as a new optional field on a shape that already exists, rather than as a second shape competing with this one. humanDecides is deliberately not one of them — ownership is a fact about the control.

The commit gates — five typed refusals

commitJourney() refuses, in order, with a typed reason:

  1. UNKNOWN_JOURNEY — with known, the list of real ids;
  2. STALE_CURSOR — the world moved since the caller looked;
  3. FRAME_ALREADY_OPEN — one committed flow at a time;
  4. PRECONDITION_FAILED — the journey's when fails against projected state;
  5. ENTRY_NOT_MATERIALIZEDthe never-trap gate. An agent commit outside a tour session is refused when the entry step could not act AT ALL right now: no registered handler under any key (instance-keyed wiring on a repeats container counts — the fire that follows carries the instance) and no navigate-derived gesture. The refusal carries the entry step's affordanceId and, when declared, its gesture. The frame that could never act is never opened.

The fifth gate asks the same widened materialisation question fire() asks — one code path, never a second lookup — so the commit gate and the fire gate can never disagree. See Actuation & materialisation.

If your agent flow commits journeys before the app's handlers mount: register the entry step's handler first (the same wiring 0.3.0 asked of fires), pass the navigate option when the entry is a pure navigation, commit with source: 'user' for human-driven flows, or create the session with allowUnmaterializedFires for touring.

Only the ENTRY_NOT_MATERIALIZED refusal lands a gap-ledger row — it is a capability gap (a binding to build). The other four are protocol events, not capability gaps, and stay un-ledgered.

The journey list you already own

If your app already keeps journey definitions, fromJourneys reads them in this same does / steps / when vocabulary — one authoring language, compiled by the same pass, refused in the same voice.

A note on the word

Before 1.0 this was called a skill, and the compiled shape was Skill. The agent ecosystem has since settled "skill" on a packaged capability an agent loads, and a library that keeps a private meaning for that word spends every conversation explaining which one it meant. What this describes is the path a person takes through an app — a journey. The old spellings are gone, not deprecated: skills: no longer type-checks, and a definition that reaches the runtime with one is refused by name.

On this page