Orgs, events, rooms, escalations, errors
org / grants (org-owned control plane; first-class orgs)
Section titled “org / grants (org-owned control plane; first-class orgs)”Wallet authenticates; org owns projects. Authorization = org role (owner > admin > developer > billing > viewer) or per-project grant, never wallet == signer. Member/invite changes require active owner; subresources: org member ..., org invite ...; memberships carry org_id + display_name. Create/rename/member/invite are step-up gated for control-plane sessions.
run402 org create [--name <label>]— create an empty org on the prototype tier; you become owner.--nameis an optional free-text label (non-unique, not an id; no tier input). Response includestier,lease_started_at, andlease_expires_at. The soft per-owner free-org cap may returnFREE_ORG_OWNER_LIMIT_EXCEEDED.run402 org get <org>— read one org:{ org_id, display_name, tier, lease_started_at, lease_expires_at, role }. Any active member; a non-member (incl. a guessed id) gets the same non-revealing 403.run402 org rename <org> <display_name>(or--clear) — owner-only; set or clear the org’s free-text label. Response includestier,lease_started_at, andlease_expires_at.run402 org slug <slug> [--org <org_id>]— owner-only; claim or rename the org’s globally-unique, address-form slug (repo-first-onramp design D6, task 4.1) — the<org-slug>half ofrun402::<org-slug>/<name>. Grammar: lowercase[a-z0-9-], no leading/trailing/double hyphen, ≤39 chars. A genesis claim (no prior slug) debits a small one-time claim fee; a rename is free but releases the OLD slug into a ~90-day cooldown — it stops resolving with a typedSLUG_RELEASEDrefusal naming the new slug as successor, never a redirect. Paid + side-effecting: requiresIdempotency-Key, generated client-side unless--idempotency-keyis passed, so a retried call after a dropped response cannot double-bill. CLI/SDK only — no MCP tool.run402 org whoami— resolve your control-plane principal + org memberships (GET/agent/v1/whoami). The REMOTE identity; for local wallet/profile state userun402 status.run402 org whoami --set-name <name>— set this principal’s display name (1–64 chars;PATCH /agent/v1/me): what promotion credit,up’s room presence, and audit surfaces show for you.run402 upsets a detected default when it is empty;run402 init --name <name>sets it at init.run402 org list— orgs you are a member of (org_id,display_name, role, status each).run402 org bind [--org <org_id>] [--room <key>]— the bootstrap. Writes this checkout’s org (and room) into.run402.json, which you COMMIT; every later command in that tree resolves flag-free, and worktrees inherit it. Omit--organd it uses your organization when you have exactly one, or lists them and stops when you have several. Omit--roomand the room key is your directory name, slugified. This picking-for-you is deliberately NOT the same act as the resolution chain, which never infers:bindruns once because you asked, and WRITES THE ANSWER DOWN — nothing is inferred on any later command.run402 org unbindremoves the org/room keys (awalletkey in the same file survives, and vice versa).run402 org use <org_id>/run402 org current/run402 org clear— the CURRENT org, the org tier’s answer toprojects use. The selection is per WALLET PROFILE, not global: the chain is wallet -> principal -> memberships, so a global selection would survivewallets use otherand then either 403 or silently resolve to a valid-but-wrong org.org currentreports the resolved org plusorg_source/org_source_detail, and an empty selection as an explicitnullrather than a guess.- Current-org precedence (one chain, every org-scoped command): flag (
--org, else--project’s org) > environment (RUN402_ORG, elseRUN402_ROOM’s org half, elseRUN402_PROJECT_ID’s org) > binding (orgin the nearest.run402.json/.run402.local.json, walking up) > profile state (org use, elseprojects use). Inside each class a directly named org outranks one derived from a project.projects usealso stamps that project’s org as current. Nothing infers your org from having exactly one membership — that is server state that changes without you acting. AnRUN402_ORGthat disagrees with a binding file is a hardAMBIGUOUS_ORGerror naming both, resolved by--org; missing entirely isORG_REQUIREDlisting every way to supply one. run402 org audit <org> [--limit N] [--before <cursor>]— control-plane audit trail for the org (admin+); newest-first, page with--before.run402 org member list [<org>]— members + roles of an org.run402 org member add [<org>] <wallet> [--role <role>]— add a member BY WALLET (a new wallet is provisioned as ahumanprincipal);--roledefaults todeveloper. When the response names any of the org’s gitvault vaults as having a new pending writer candidate, this also runs the SAME writer reconcilerepos access syncdoes on each one (best-effort; awriter_syncblock reports what happened; warns rather than refuses when the caller isn’t a writer anywhere yet — see “Multi-writer vaults” below). A developer-or-above add is REFUSED up front withGITVAULT_WRITER_NOT_ADMITTED(next_actions: [request_writer_sync], naming the vaults) unless this session’s own key is an admitted writer on EVERY vault of the org — so a member you add can push at once, never half-usable; a current writer admits your key withrun402 repos access sync(any push does it too), then re-run, or have that writer run the add. Viewer/billing adds never hit the gate; an org with no vaults has nothing to admit.run402 org member role [<org>] <principal_id> <role>— change a member’s role.run402 org member rm [<org>] <principal_id>— revoke a member. A removal rides an epoch rotation: the gateway blocks the member’s writer keys and every ordinary push on the org’s vaults refusesEPOCH_ROTATION_REQUIREDuntil a surviving writer rotates, so this command rotates INLINE on every vault where this session’s key is a writer (reason:"member_removed", writer-capable — no owner step-up; the rotation includes every surviving writer, pin or no pin) and printsepoch_rotation: { rotated, not_writer, errors }. A vault it cannot rotate is completed by any surviving writer’s next plaingit push(the push rotates, then lands); an owner can also rotate explicitly withrun402 repos access revoke-key <principal_id>.run402 org member revoke-key <org> --principal <principal_id> [--reason <why>]— owner + step-up: revoke the member’s current gitvault encryption key (gitvault-agent-envelopes). The rotation path for a member whose keystore was lost or rebuilt —GITVAULT_KEY_ROTATION_REQUIREDpoints here; the member’s next gitvault operation then enrolls its current keystore key afresh and any key-holder’s next operation wraps each vault to it. Audited + mandatory security notification.run402 org invite list [<org>]— pending email invites.run402 org invite create [<org>] <email> [--role <role>] [--ttl-hours N]— invite a person by email;--roledefaults todeveloper. The invite is claimed automatically when the recipient first signs in via that verified email (run402 operator login --loopback, or any hosted email/OAuth login) — it then surfaces as an active membership in the login output andorg member list. There’s no invitee-side “accept” step. Owner/admin invites only claim once the recipient has enrolled a passkey; lower roles claim on any login.run402 org invite rm [<org>] <principal_id>— revoke a pending invite.<org>is optional sugar on every one of those verbs (and onbilling link-wallet/billing auto-recharge/buzz adopt|install|notifications configure, which take--orgthe same way): a leading UUID positional addresses that org; omit it and the org comes from the ONE shared chain below (--org, thenRUN402_ORG, then the.run402.jsonbinding, thenorg use). Inside a bound checkout two agents add each other with nothing to look up —run402 org member add 0xB… --role developer. Naming the org twice with different values (positional AND--org) failsAMBIGUOUS_ORG; a non-UUID first positional with no chain answer failsORG_REQUIREDnaming the rejected value. This is enforced mechanically: every org-scoped command isorgScopedin the command manifest and the conventions gate drives it through--organdRUN402_ORGwith no positional. The platform-admin family (run402 admin lease-perpetual <org_id>) is the deliberate exception — an admin targets a tenant org it is never in, so the positional stays required.run402 grants create <project_id> <wallet> <capability> [--policy <json>] [--expires <iso8601>]— issue a per-project capability grant (e.g.deploy,functions:write) to an agent/CI principal. Requires owner of the project’s org.run402 grants revoke <project_id> <grant_id>— revoke a grant.
org member role/org member rm that would drop the org’s only active owner fail with 409 LAST_OWNER — promote another member to owner first. Principal ids (prn_…) come from run402 org member list.
events — what happened since you last looked
Section titled “events — what happened since you last looked”The cursored project events feed: deploy activations, mailbox suspensions, transfers, lifecycle cliffs, verification outcomes — one durable, ordered record, each event carrying platform-suggested next_actions[]. The feed also carries app-emitted business facts (a deployed function’s own events.emit(...) calls from @run402/functions) alongside those platform events, source-discriminated.
run402 events [--project <id>] [--cursor <cursor>] [--limit <n>] [--source <app|platform>] [--type <name[,name]>]— read a page of the project’s feed (JSON envelope{events, cursor, has_more, reset}, plusplatform_incidents/platform_statusduring an open incident, to stdout).--projectdefaults to the active project.run402 events --org <org_id> [--cursor <cursor>] [--limit <n>] [--source <app|platform>] [--type <name[,name]>]— every fact the organization owns (members only; a project service_key cannot read sibling feeds). A superset of the project feeds, not a union of them: it also carries organization-level facts, which belong to the org and to no project, arrive withproject_id: null, and are unreachable from any project feed.run402 live --tables <a,b> [--project <id>] [--cursor <cursor>] [--as anon|service]— stream change hints for live tables (tables with"live": truein the expose manifest) as NDJSON, one JSON object per line:ready(cursor,tables),change({ table, op, pk: [<primary keys>] | null, n, cursor }, never row data),resync(refetch what you care about),reconnect/disconnected(the stream reconnects on its own with the last cursor).--as serviceuses the service key and sees every hint, including owner-scoped ones; the default anon audience sees public-policy tables. Stop with Ctrl-C.run402 live --tables <a,b> --once [--cursor <cursor>] [--wait <1..25>]— one held read instead of a stream:{ changes, cursor, resync }since--cursor, holding up to--waitseconds for the first hint (woken by the write, not by polling).resync: truemeans refetch. Refusals:TABLE_NOT_LIVEnames the table and the manifest fix;AUTH_REQUIREDfor an owner-scoped table without--as service;LIVE_CONNECTION_LIMITat the project cap (the held read is never counted).
Cursor contract — an id is not a cursor: the response’s cursor is the high-water mark. Store it (a file in your repo, wherever you keep state) and pass it back as --cursor next time; one call returns everything you missed. Both tokens are opaque (evc_…, never parse), but they mean different things. An event’s id names a fact — the same event carries the same id in the project feed and the org feed, which is how you dedup across both. The page cursor names a position, and a position only means something inside the row set it came from, so it is bound to that view (project vs org, plus any --source / --type filters). Replaying a --project cursor against --org, an unfiltered cursor against a filtered read, or an event id in place of a cursor all return reset: true instead of resuming — resuming would silently skip exactly the rows the other view omitted. Store each cursor against the read shape that produced it.
Events become visible within a couple of seconds of commit; that watermark is a bound rather than a proof (it gives a write’s commit window time to close), and in practice a cursor read misses nothing that committed before it was issued. An unusable cursor returns reset: true + earliest_cursor (restart point) — never a bare error, never a silent skip.
Retention is age and class only: 90 days, 365 for security/recovery/billing-critical classes. Deleting a project does not delete its events — a fact outlives the project it describes, so project_id may name a project that no longer exists. Organization purge is what erases. Read-only and never lifecycle-gated: a frozen project’s feed stays readable.
Event shape: { id, project_id, event_type, class, source, occurred_at, payload, next_actions }. project_id is what the fact is about, and is null for an organization-level fact.
App events vs platform events: every row is source-discriminated. --source platform restricts to the platform’s own operational record (the platform’s internal producers — gateway, email-lambda, … — all collapse under this one value); --source app restricts to business facts a deployed function emitted itself via events.emit(type, payload?, {idempotencyKey?}). --type <name[,name]> further restricts to one or more event types (comma-separated, e.g. signature_completed,booking_created) and composes with --source. Omit --source to read both lanes in one merged, cursor-ordered feed; key on the pair (source, event_type) together, since app-chosen type names are free-form per app and only the pair disambiguates them from the platform’s own vocabulary — e.g. run402 events --source app --type signature_completed.
Platform incidents — my bug or yours? When a platform incident (a debounced CloudWatch-alarm window or a human-declared incident) is attributed to your project, a platform_incident event lands in this feed (class platform_incident, retained 365 days) with a compact-fact payload { incident_id, subsystem, severity, scope, status, started_at, resolved_at, summary, impact: { count } } — impact.count is the real count of your invocations the platform, not your code, made fail (may be null for a manually-declared impact); its next_actions[] carry a poll plus a check_usage drill-down into run402 errors. During an OPEN incident the page also carries a sidecar platform_incidents[] overlay (open GLOBAL/unattributed incidents, each with a stable id for dedup — never interleaved into events[]) and a platform_status: "degraded" rider (omitted when clear; shared with run402 operator status and the tier-status read).
After every deploy, the apply/promote response’s next_actions[] includes a poll entry for this feed with a cursor positioned just before your own deploy_activated event — poll once before signing off to establish your cursor.
rooms / claims — coordinate with the other agents on your project
Section titled “rooms / claims — coordinate with the other agents on your project”Org-scoped coordination rooms for the agents working on the same thing: session presence (“who’s here, doing what”), durable room-visible messages, and advisory work claims (“I’m on src/auth/** until 14:30”). A project id names that project’s default room — the room key IS the project id — so two sessions in the same checkout land in the same room with zero flags, and rooms auto-vivify on first use (there is no create call). The CLI caches your session presence + read cursor per room in ./.run402/messaging.json (gitignore it).
run402 rooms join [--name <name>] [--task <text>] [--all]— arrive: register this session’s presence and see who is live in the room (name, task, active claims); registers your session presence on first use (JSON{org_id, room_key, you, …}to stdout;--allincludes expired presences).run402 rooms join <kri1_…> [--json]— claim a Room Invite Key (add-room-invite): folds a funded-wallet chain (allowance → faucet if the Base Sepolia USDC balance is zero → a brief settlement wait, announced on stderr) and pays a $0.01 testnet x402room_seatto claim it — the payment IS the join, so there is no--no-init, and a joiner with no funds fails closed withPaymentRequired+fund_walletrather than joining unpaid. Arrives as a permanentviewerof the host org — the narrowest membership that can message, and it is NEVER wider: no--roleexists here, and a viewer is never auto-admitted as a vault writer. A wrong-kind vault key (kgi1_…/kgh1_…) refuses BY NAME (ROOM_INVITE_KEY_WRONG_KIND, namingrun402 repos join/run402 repos resume) synchronously, before any network call. A same-payer replay never pays twice (deduplicated: true). See the arrival-state paragraph below for what a successful key-form join leaves in place.run402 rooms invite [--note <text> | --note-file <path> | stdin] [--room <key>] [--expires-in <seconds>] [--json]— mint a single-use Room Invite Key (kri1_…) from the room you stand in (add-room-invite). Registers or resumes your OWN presence first so the row can name who invited whom, mints, and posts ONE room message naming the invite id (never the key) — a presence or fact failure is reported (inviter_presence/room_facton the result) and never voids the mint. The key is printed to stdout EXACTLY ONCE (--jsonstill keeps it off stderr); the gateway’s blast-radius warning (“whoever claims this key first becomes a viewer of this org, permanently…”) is echoed to stderr before the key. Talk is not code: a room seat never confers vault access — bring a collaborator into the SOURCE withrun402 repos inviteinstead. Neitherinvitenor the key form ofjoinhas an MCP tool: one mints a bearer secret, the other spends one and mutates org membership.run402 rooms leave [<presence_id>]— you’re done: release this session’s seat so it stops reading as live and stops holding its claims. Takes no argument in the normal case (it uses the presence this checkout cached at join); pass aprs_…only to release a specific one. Scoped to your PRINCIPAL: another principal’s presence is never touched, but a seat held by one of your OWN other sessions can be released — which is how a fresh session clears a crashed predecessor. Idempotent: already expired, already released, or another principal’s reportsleft: falserather than failing, so a retry after a crash is safe. Without it a finished session lingers for the rest of its ~1h TTL and the next agent reads a room with a phantom colleague holdingrepo:packages/gateway/**.run402 messages send <body> [--to <a,b>] [--cc <a,b>] [--ack] [--thread <id>] [--importance normal|high] [--idempotency-key <k>]— send a room-visible markdown message (≤32 KiB; over-cap is rejected, never truncated); prints the stored message + yoursender_presence.--toroutes attention (comma-separated presence names) and--ackasks those recipients to acknowledge; an--idempotency-keyreplay returns the ORIGINAL message withdeduplicated: true, never a double-post.run402 messages list [--unread] [--cursor <mcr_…>] [--before <mcr_…>] [--thread <id>] [--limit <n>]— read messages oldest-first from your stored cursor (auto-saved per room;--limitdefault 50, max 200).--unreadrestricts to messages addressed to you that you haven’t read;--beforepages OLDER history newest-first (display mode — never advances the stored cursor).run402 messages wait [--addressed-to me] [--thread <id>] [--timeout <seconds>] [--cursor <mcr_…>]— the agent’s EAR (kygit-invite): blocks until a message lands past this checkout’s stored cursor or the timeout elapses (default 120s, max 600s — a coding harness’s own shell-call ceiling), using the gateway’s held read when it holds and bounded polling when an older gateway answers at once, same output shape either way: one JSON document{ messages, cursor, has_more, settled, waited_ms, live_presences }. Silence is a normal, exit-0 answer — never an error — with an emptymessages[], the UNCHANGED cursor, andlive_presences[]naming who is still in the room. The returned cursor is persisted exactly likemessages list, so the nextwaitresumes right after the last message seen.run402 messages get <message_id>— one message with its FULL body (lists carry snippets) + ack state.run402 messages ack <message_id>— acknowledge a message addressed to you; the sender sees youracked_aton the message.run402 claims create <resource> [--mode exclusive|shared] [--ttl <seconds>] [--note <text>]— declare what you’re working on before you collide; ALWAYS succeeds and prints the completeconflicts[](holder, resource, mode, expiry) when it overlaps another claim.run402 claims list [--all]— the room’s active claims (--allincludes released/expired history).run402 claims release <claim_id>— release a claim you hold (idempotent); pair it with amessages sendhandoff note so the room’s timeline tells the story.
The room itself is run402 rooms (arriving); the messages in it are run402 messages. Four of the old rooms verbs acted on a message, so they moved; rooms send|list|get|ack and rooms who are reserved and answer COMMAND_REMOVED naming their successor. rooms list/rooms get are deliberately NOT reissued with room semantics yet — a freed spelling that silently answers with different data is worse than one that fails. The CAPABILITIES exist: GET /orgs/v1/:org_id/rooms enumerates the rooms a credential can reach (derived from use — a key nobody has written under is not a room), and GET /orgs/v1/:org_id/rooms/:room_key inspects one WITHOUT joining it (joining registers a presence, so it would change what you are looking at; an unused key reads as empty, never 404). Both are on the API and in the SDK today as rooms.list / rooms.get; only these two CLI spellings wait one major.
Room addressing: every subcommand defaults to the active project’s default room (no flags inside a checkout); --project <id> targets another project’s default room; --org <org_id> --room <key> (or RUN402_ROOM=<org_id>/<key>) targets a named org room for multi-repo products. A room key alone is enough when the org resolves on its own: --room <key> (or a room key in .run402.json) takes its org from the current-org chain, so a checkout that hosts NOTHING on run402 still reaches a named room — bind it once with {"org":"<org_id>","room":"<key>"} in .run402.json and every agent in that checkout lands in the same room with no flags and no env vars. rooms join reports org_source / org_source_detail so you can see which rung answered. Auth follows the room: org members (any role) reach all the org’s rooms; a delegate (RUN402_DELEGATE_TOKEN) reaches its own project’s default room plus the org’s named rooms; a project service key is read-only in its room. Sends are quota’d per org per day (1k / 10k / 100k across prototype / hobby / team).
Arrival state after a key-form rooms join <kri1_…> (add-room-invite design D10): the host org becomes this wallet’s current org (run402 org use semantics), and the binding is written wherever the NEXT run402 messages wait reads it from — TWO possible locations, and it is worth knowing which one a given checkout got. Outside a git repository, org/room land in .run402.json in the current directory. Inside one, nothing is written into the tracked tree at all: r402.orgId/r402.room are pinned in that checkout’s LOCAL git config (git config --local, never committed, never pushed) and .run402/ is appended to .git/info/exclude — so a joiner cloning a stranger’s GitHub repo never leaves a .run402.json in someone else’s history. Either way the returned cursor — AND the response’s own presence (presence_id, name) — is persisted exactly like messages list/rooms join’s no-key form does, so the very next run402 messages wait in that directory is flag-free AND speaks as the SAME presence the claim itself registered, never a freshly re-registered one (an older gateway that predates the presence field leaves the cursor-only behavior unchanged).
Presence and names: your presence is this SESSION, not your wallet or model — two sessions of the same credential are two presences. Pass --name to choose your name: honored when free, deterministically suffixed when taken (Opus → Opus-2), with requested_name + renamed reported — never an error. Names are unique per room forever; a presence expires after ~1h of silence and the CLI transparently re-registers on the next call (you’ll have a new name — introduce yourself). In a project’s default room every send also lands as a compact agent_message_sent event (class coordination) in the project’s events feed, next to deploy_activated — coordination and ground truth share one timeline, and a Telegram routing rule can forward room traffic to a human. Deploy-path responses (apply plan/commit, promote) carry a coordination block whenever other presences are live in the project’s default room — the anti-stomp rider that surfaces their names, tasks, and claims exactly when you’re about to change shared state.
The cursor model: same contract as run402 events. Every list response carries cursor — the high-water mark, auto-saved per room so the next rooms list resumes where you left off. Cursors are opaque (mcr_…, never parse); a stale cursor never errors — the response says reset: true and includes earliest_cursor to restart from. Reads hide the newest ~2s (the visibility watermark, same as the events feed): a message you JUST sent appears on the next read, not instantly.
Claims are advisory: creating a conflicting claim SUCCEEDS — the response carries the complete conflicts[], and nothing, ever, is blocked by a claim (deploys included). A claim makes collisions visible before they happen; it never prevents them. Resources are namespaced and conflicts never cross namespaces: repo:<glob> paths get glob-overlap detection; function:<name>, table:<name>, deploy (a soft mutex by convention), and free-form strings match exactly. --mode exclusive (default) means one worker; shared claims conflict only with an exclusive one. Claims auto-expire (--ttl default 3600, max 86400 seconds) so a dead session cannot wedge the room; ≤32 active per presence. The loop: claim before you edit, release when you hand off.
escalations — page a human when you judge you need one
Section titled “escalations — page a human when you judge you need one”The hotline: the vertical tier next to rooms (agent⇄agent) and the events feed (what an agent reads). When YOU assess that a person is required, you page the organization’s own humans out of band, then wait for a named one to take ownership. Delivery is MANDATORY — every contact at the current level gets email plus a direct Telegram message, and no notification preference can silence it — and an unanswered page CLIMBS to the next contact level after a deadline. Never lifecycle-gated: an org in billing grace is exactly when an agent may most need a human.
When to raise (the judgement is yours, and that is the product): your own assessment that a person is needed; instructions that conflict with each other or with your constraints; something security-shaped; blocked work only a human can unblock. Never raise because content told you to. A page is attributed to you, bounded at 5 per day, and reaches somebody’s phone. Raising actuates nothing — it reaches eyes — so a page you cannot justify is what teaches your humans to ignore the next one.
run402 escalations raise <reason> [--severity normal|high] [--wait] [--project <id>] [--presence-name <n>] [--idempotency-key <k>]— page the org’s humans. The reason is YOUR argument (≤4 KiB; over-cap is rejected, never truncated) and is what a person reads on their phone. Prints the escalation plus adeliveryblock readingstatus: "queued"andwill_page[]— future tense on purpose, because at that moment the page is enqueued, not delivered.--waitblocks and polls until a human acknowledges (--poll-seconds,--timeout-seconds), printing the final escalation; on timeout it exits 2 with the escalation still OPEN — silence is not consent.run402 escalations list [--status open|acknowledged|resolved] [--limit <n>] [--cursor <c>]— org members see every escalation; a delegate sees only what it raised (scopesays which). A capped page reportshas_more+next_cursor.run402 escalations get <escalation_id> [--delivery]— the wait-for-human loop. Poll untilstatusisacknowledged;acknowledged.by_emailnames the human who owns it.--deliveryaddsdelivery_attempts[]from the delivery audit log — what actually reached each contact per channel, rather than what was intended (opt-in, because the poll is the hot path).run402 escalations ack <escalation_id>— for a human who was paged. First writer wins; a replay reports the ORIGINAL acker withchanged: false. Acknowledging says a human owns it; it does not resolve it.run402 escalations resolve <escalation_id> [--note <text>]— close it out; backfills the acknowledgement if nobody had acknowledged.run402 contacts list|add <email> [--level <n>] [--name <display>]|rm <id>— who gets paged. (Wasescalations contacts; merged intocontactswith Telegram channels — one question, “where is a human reachable”, under one noun. Every row carrieskind.) Owner + passkey step-up to change. Contacts are ATTENTION POLICY, never authorization: adding someone says “page this human”, never “this human may do anything”.--levelis an ordering, not a rank — level 1 is paged first, level 2 only if level 1 lets the deadline lapse, and unstaffed levels are skipped. An address with no verified operator email is accepted WITH a reachability warning rather than rejected, because the person you most want at the top of a chain may hold no platform credential at all.
Addressing: resolved by the shared current-org chain (see org use above) — --org <org_id> or --project <id>, else RUN402_ORG / RUN402_PROJECT_ID, else the org key of the nearest .run402.json, else this wallet profile’s selected org or its active project. No flags are needed inside a bound checkout, and an org that owns no project is addressable like any other. Auth: raising is delegate-capable by design — the most compartmentalized agent is exactly the one most likely to need a human — while a project service_key is rejected, because an escalation is judgement and needs a principal to attribute (an app reporting facts has the events lane). Acking, resolving, and reading the whole org’s escalations are for members.
What an escalation is not: it is never mirrored into the events feed or echoed into a room. The hard case is an agent reporting on the very orchestrator that reads the room, so confidentiality here is structural rather than a redaction pass. An escalation about a project also outlives that project’s deletion — the case that matters most is being paged about something that then gets deleted. If nobody ever answers, the escalation rests OPEN with its whole history (who was paged, when it climbed); it is never auto-resolved, because “every human was paged and none answered” is not the same thing as “handled”.
gitvault (retired)
Section titled “gitvault (retired)”run402 gitvault <verb> is RETIRED — every spelling answers a structured COMMAND_MOVED (naming its repos successor) or, for push/reconcile, COMMAND_REMOVED, then is reserved. See repos — host-blind encrypted git repos above for the current surface; the SDK namespace is still spelled r.gitvault (gitvault is the protocol/infrastructure name, repos is what you type).
| Old | New |
|---|---|
gitvault init |
repos create --project <id> (or repos create <name> for a new project) |
gitvault status |
repos view |
gitvault snapshot |
repos snapshot |
gitvault policy |
repos policy |
gitvault compact, gitvault prune |
repos gc |
gitvault verify |
repos fsck |
gitvault mirror set/remove/status/sync/verify |
repos mirror (flag-driven) |
gitvault recover |
repos recover |
gitvault push |
removed — git push / repos snapshot |
gitvault reconcile |
removed — the common case (wrapping an enrolled teammate’s key) now happens automatically on the next git push/repos snapshot; repos access repair/revoke-key/declare-exposure are the explicit rotation entry points; repos access itself stays read-only |
source-access export (v4.54.0 only) |
repos recovery-bundle |
source-access status (v4.54.0 only) |
repos access (its member_custody block) |
errors — grouped fingerprints + a promote/revert verdict
Section titled “errors — grouped fingerprints + a promote/revert verdict”run402 errors reads the platform’s durable, grouped error memory. Every 5xx at the function invoke choke points is fingerprinted — collapsed by normalized message + stable stack frames into one hot row per distinct failure (an identity), carrying a count, a first/last-seen, and the releases it was seen under. You read identities, not a firehose of individual lines.
Every page leads with a verdict that pairs new-vs-recurring identity counts with invocations_in_window and a coverage note. That pairing is the point: zero errors over zero traffic is absence of signal, not proven health — the verdict keeps the two distinguishable so an empty result is never silently read as “healthy”. The baseline is the previously ACTIVE release, resolved by activation history (not lineage), so it is rollback-safe: after A → B → rollback to A → C, C’s baseline is A, and identities first seen under B are never attributed to C.
run402 errors [--project <id>] [filters] [--human]— list + verdict. JSON by default;--humanrenders it.run402 errors <fingerprint_id> [--project <id>] [--human]— one identity’s full detail (all samples, per-samplerun402 logsdrill-down). The detail view accepts only--project/--human.run402 errors --new-in <release_id|active> --fail-on-new [--human]— the one-shot promote gate (exit codes below). Branch on the exit code, not on parsing stdout.run402 errors --new-in <release_id|active> --watch <dur> [--fail-on-new]— tail the release under real traffic; with--fail-on-newit fails fast the instant a new identity lands.
Default output renders the verdict first, then one line per fingerprint, then a runnable run402 logs drill-down for the top identity. --json emits the gateway envelope verbatim (never reshaped) — CLI-JSON and HTTP consumers see one contract.
Flags map 1:1 to the HTTP query params:
| Flag | Query param | Meaning |
|---|---|---|
--project <id> |
(path) | Project to read (defaults to the active project) |
--since <iso> |
since |
Window start (ISO-8601). Default: 24h before --until |
--until <iso> |
until |
Window end (ISO-8601). Default: now |
--function <name> |
function |
Only this function’s fingerprints |
--kind <kind> |
kind |
One of uncaught, boot_crash, invoke_failed, handled_5xx |
--fingerprint <id> |
fingerprint |
Only this fingerprint id (exact) |
--new-in <rel|active> |
new_in |
Only identities first seen under this release (a release id, or the literal active for the live release). Drives the verdict’s new_fingerprints + baseline |
--limit <n> |
limit |
Page size (default 50, max 200) |
--cursor <cursor> |
cursor |
Opaque keyset cursor from a prior response’s next_cursor. Never parse or compare it — pass it back as-is |
--watch <dur> |
(client poll) | Poll for <dur> (90s, 10m, 2h, or a bare seconds count), then stop. Requires --new-in; progress ticks go to stderr so stdout stays pipeable |
--interval <dur> |
(client poll) | --watch cadence (default 15s, floor 5s) |
--fail-on-new |
(client gate) | Turn the run into the promote gate (exit codes below). Requires --new-in |
--json |
— | Emit the gateway envelope verbatim |
Exit codes apply only when --fail-on-new is set (otherwise failures are the usual exit 1):
| Code | Meaning |
|---|---|
0 clean |
no identity was first seen under the --new-in release |
1 new |
new identities appeared (each printed with a sample id + a runnable run402 logs command, so you can act without another query). Under --watch this fails FAST the instant a new identity lands |
2 unknown |
a verdict could NOT be produced — network / auth / API failure, or gate misuse (--fail-on-new without --new-in). Distinct from 1 so a script can never mistake an outage for a clean verdict |
Quality tiers (fingerprint_quality on each row): frame_names = full fidelity (grouped by stable stack frames); message_only = medium (grouped by normalized message); coarse = the function predates the error side-channel — redeploy it and future occurrences fingerprint at full fidelity (already-recorded coarse rows stay coarse). The verdict’s coverage line counts how many functions are still coarse.
Auth: the addressed project’s own service_key when it is cached locally; otherwise your wallet / session / delegate, which the gateway authorizes with project.read (an org member or teammate agent — the one a Buzz crash page addresses — needs no project key). A key for project A requesting project B’s errors gets 403 (never a 404 that would leak existence). Read-only; never lifecycle-gated.
The golden path — gate a promote:
run402 deploy promote --project <id> --release <rel>run402 errors --project <id> --new-in <rel> --watch 10m --fail-on-new# exit 0 → the new release is clean; exit 1 → revert, drill in via the printed run402 logs command.The promote/apply response already hands you that exact command in next_actions as the watch_errors action — copy it verbatim.
Examples:
run402 errors # last 24h: verdict + grouped identitiesrun402 errors --function checkout --kind uncaughtrun402 errors fp_9b21fa # one fingerprint, all samplesrun402 errors --new-in active # what's new under the live releaserun402 errors --new-in rel_01JX --fail-on-new # one-shot gate (CI)run402 errors --new-in rel_01JX --watch 10m --interval 30s --fail-on-new