Who sees it
- Scope: the Apps hub offers Mastermind only when the active workspace is
the root workspace, the root permission set contains
view_infrastructure, and the build has a hub source configured. - Default: hidden. It is not an experimental module and cannot be enabled from Settings.
- Authority: this check is cosmetic. The hub Worker’s owner allowlist is the real gate, and a 403 from the hub hides the entry until the next access sync.
- Deep links and push taps: any
/mastermind/**location is held on a neutral skeleton while the access check runs, then redirected to the Apps hub unless access is loaded and enabled. A 403 keeps the user on the screen so it can explain itself; the entry disappears from the Apps hub. - Account changes: logout and account switch erase the cached snapshot, memory and disk, and re-check access for the selected workspace.
- Access errors: a failed access check hides the entry, so it is re-run when the app resumes and when the Apps hub is pulled to refresh; neither blinks a healthy entry while it checks.
Data source
Fixture builds (development flavor withMASTERMIND_FIXTURE) read the bundled
synthetic sample. Staging reads mastermind-hub-staging.tutur3u.com, and any build reads
MASTERMIND_HUB_URL when it is set. Production stays unconfigured, and the
entry hidden, until the beta rollout names its host. A build with neither stays
disconnected, as does any value that is not an HTTPS host. The app-level runtime owns
the source, the cache and the refresh scheduler and closes them with the app.
Each Mastermind screen only attaches to and detaches from the scheduler, so
refresh runs only while a screen is open and the app is in the foreground. It
also runs at once when the app returns from the background (hidden or
paused); a brief inactive blip such as Control Centre or a biometric prompt
is not a resume and fetches nothing. The scheduler follows device connectivity,
and the freshness line (Offline versus Couldn’t refresh) updates when
connectivity alone changes.
Tabs
Home, Inbox, Work, Ship and Insights share one mini-app shell: the top navbar shows the destination title and no Back button, and the floating dock owns Back. Home and Inbox are implemented; the other tabs show a “coming soon” empty state behind the same access gate and freshness strip.Freshness strip
Every tab starts with one line, inside the scroll view. Data age is informational because publishing is manual at first; only a failed refresh or a degraded source is a warning. Times are absolute GMT+7.Before data arrives
A 401 can come from the Worker (audience or clock skew) rather than the
session, and signing out removes every saved account on the device, so it is
never the primary action and only runs after the user confirms.
Home
Needs you comes first, then lanes at a glance, ship, quota, recent decisions and activity. Needs you lists up to three normal items (decision, then action, then FYI, then publisher order); “See all (N)” counts normal items only. Recent-decision rows and Ship rows are not tappable until Inbox and Ship have screens to open. “See all”, the lane chips and “Usage” link to their tabs; a lane chip opens Work already filtered to that state. Security rows render as a single non-tappable “<Product> · security fix”
line in a trailing “Security” group with no count. They have no severity, title
or detail anywhere, are never counted, and never reorder anything. Event
summaries apply the same collapse.
Inbox
The Inbox tab lists the open Needs-you items grouped by kind: “Decisions for you”, “Actions”, then “For your information”, each with its count, in publisher order. Each card shows the title, the body (three lines until “More”), kind and product chips and the options as read-only chips; the recommended option carries the word “Recommended” and a star, never colour alone. A search field (“Search Needs you”) and product chips narrow the list. Pull to refresh.- Scope: normal Needs-you items only. The Decisions journal is a later change; this tab has no segmented control until it exists.
- Security rows keep the Home rule: one non-tappable line in a trailing “Security” group with no count, never counted, searched or filtered, with no severity, id or detail.
- Detail:
/mastermind/inbox/item/:itemIdopens one item in full. The id is an opaque snapshot id and only ever a route parameter; deep links carry tab roots only and never ids. An id that is not an open item shows “This item is no longer in Mastermind” (or “This was answered or withdrawn” when it left while open) with a way back, never an error. A changedversion_hashshows “This question was updated”. - The “Reply on Mac” option is a placeholder the publisher adds when an item has nothing to pick. It is never offered as a choice and the app never sends it.
Replies
Replies are gated at runtime byGET /v1/capabilities (replies, with schema
1.4), checked when an Inbox screen opens and at most every five minutes.
Where replies are off, or the hub cannot be asked, the detail shows the options
read-only and “Reply on your Mac”; there is no composer. The Worker owns the
switch, so it flips without a store release.
-
Gate in code: submit, retry and flush all refuse to send unless the hub
says replies are on, not only the composer’s visibility. Entries queued while
replies are off stay queued: neither sent nor dropped, until capabilities say
replies: trueagain. Coming back online re-asks the hub before flushing. -
Known answer kept offline: a known “supported” answer survives a failed
re-check. Going offline never flips it, so the outbox keeps working with no
connection; only an answer from the hub turns replies off:
replies: falsefrom the capabilities check, or a 403replies_disabledon a send (any other 403 is a lost-access refusal, handled like a 403 on the snapshot). - One check at a time: overlapping capability checks, forced or not, share a single request, and a reply sent during the first-ever check waits for its answer instead of being refused. Connectivity changes are de-duplicated, so several interfaces flipping together cause one re-check and flush.
-
Contract:
POST /v1/replieswith exactly{schema_version: "1.4", item_id, version_hash, option_index | text}: one of an option index (into the item’s ownoptions[]) or free text of at most 500 characters, counted in code points, and at most 10 lines (the hub’s limit; the composer caps both as you type). Choosing an option clears the text and typing clears the option. The text field has a placeholder and a counter. A confirmation asks before anything is sent. -
Idempotency: the
Idempotency-Keyis a random v4 UUID from a secure generator, made once per reply and stored in the outbox before the first send. Every retry reuses it, so an unknown outcome never becomes a second reply. A 200 replay is treated exactly like 201. One reply per question version. - Offline outbox: a reply that cannot be delivered (offline, timeout, 5xx) stays queued on the device, per user, in the snapshot cache directory, and is sent again when an Inbox screen opens, when the app resumes and when the device comes back online. It can be retried or discarded. Entries expire after seven days and are dropped when a live snapshot no longer has the item open. Queued replies live in the OS cache directory, not the documents directory: that keeps them out of device backups, at the cost that the OS may purge the cache under storage pressure and silently lose a queued reply. We accept that trade-off, because a reply is short-lived and a lost one is re-asked; a reply already accepted by the hub is unaffected. Logout, account switch and lost access erase the outbox with the snapshot cache, behind the same generation fences, so an in-flight send cannot revive it.
-
409
stale_item: the item is refreshed, the typed words are kept, and the screen says “This question changed or was already answered. Review it again.” An option stays selected only if it is still the same option at the same place.idempotency_conflictdrops the queued entry with “Something went wrong with this reply. Review it and send it again.” -
Other answers: 400 (
unsafe_textexplains the text policy) and 413 keep the words to edit; 429 waits outRetry-Afterand disables the retry until then; 403replies_disabledhides the composer and 403forbiddenis lost access. - 401: the reply stays queued. The first failure offers “Try again”; only after a repeat does “Sign out and sign in again” appear, and it asks first, because signing out removes every saved account on the device.
-
Waiting rows: the Inbox shows “N replies waiting to send” while replies
are queued; with exactly one, the row opens that item. A queued reply binds to
its item id and
version_hash, so a reply to an older version of a question is never shown as the answer to the current one. A 429 counts down on its own and re-enables “Try again” whenRetry-Afterhas passed. - Search: it never matches the “Reply on Mac” placeholder option.
- Accessibility: the reply status line is a live region, and the character counter has a spoken label (“12 of 500 characters used”).
Work
The Work tab lists lanes grouped by state, the one that needs attention first: failed, waiting, running, queued, idle, stale, completed, cancelled, then the rare open, answered, withdrawn and unknown. Each group shows its count and is ordered oldest first, a lane with no age last. A row shows the title, role and provider/model (display text, shown verbatim), “Running for 2 h” (or “Running · age unknown”), “About 20 min left” only when there is an estimate, and product, area and kind chips. State is always a glyph and a word.- Filters: a search field (“Search lanes”), one chip per state, and product and area chips while two or more of each are present. A state chip’s count follows the product, area and search filters, so it always matches its group header. A product or area filter resets when its value leaves the snapshot, because its chip would be gone.
- Filters in the URL: the state, product and area filters live in the
location as
?state=,?product=and?area=, and every chip change rewrites it. The Home lane chips open/mastermind/work?state=<state>. An unknown value is ignored and “All” clears the state. A lane opens by push, so Back from it (the dock included) returns to the same filtered list, search text and all. External deep links open the tab root and never carry a filter or a lane id. - No matches: a filter combination with no lanes (including a state with none) shows “No matching lanes” with a “Clear filters” row that resets state, product, area and search.
- Scope: lanes only. The Backlog and Staff segments are deferred to a follow-up layer; this tab has no segmented control until they exist.
- Security rows keep the Home rule: one non-tappable line in a trailing “Security” group with no count, never counted, searched or filtered.
- Detail:
/mastermind/work/lane/:laneIdshows the same fields in full and has no action. The id is an opaque snapshot id and only ever a route parameter. An id that is not in the snapshot shows “This lane is no longer in Mastermind” (or “This lane finished or was removed” when it left while open) with a way back, never an error. A lane whose state changed while open shows “This lane was updated”.
Regression coverage
apps/mobile/test/features/mastermind/view:
mastermind_entry_test.dart: entry gating, route gate, mini-app shell, runtime reset.mastermind_section_test.dart: access gate, scheduler lifetime, resume refresh gated on a real background (including a section mounted while the app is backgrounded), connectivity-driven freshness strip, whole-section states, the sign-out confirmation.mastermind_access_resync_test.dart: access recovery on resume and Apps hub pull-to-refresh.runtime/mastermind_auth_reset_test.dart: the app-level logout and account-switch listener over a realAuthCubit.mastermind_freshness_test.dart: strip states.mastermind_home_page_test.dartandhome_model_test.dart: ordering, security redaction, dock clearance, tab links.mastermind_work_page_test.dart: grouping, the state, product and area filters and their URL, chip counts following the filters, Clear filters, Back from a lane keeping the Home chip filter, security redaction, whole-section states, dock clearance, deep links to the tab root.mastermind_lane_page_test.dart(detail, gone, updated) andwork_model_test.dart.
/mastermind/** route
through the production router.
apps/mobile/test/features/mastermind/replies covers the reply path:
reply_models_test.dart (key format, exact body, strict stored form, limits),
reply_outbox_store_test.dart (per-user persistence, corruption, clear),
hub_reply_transport_test.dart (headers, every status, the 401 retry with the
same key), mastermind_replies_cubit_test.dart (capability gate, persist before
send, idempotent retry, 409/400/401/403/413/429, logout fencing, snapshot sync),
mastermind_replies_gate_test.dart (nothing is sent while replies are off,
including a reconnect flush; a logout racing a refused or stale reply; entries
bound to item id and version), mastermind_capability_load_test.dart (one
shared capability request, a send waiting on the first check) and
mastermind_runtime_replies_test.dart (logout, switch and lost access clear
the outbox). runtime/mastermind_runtime_online_test.dart covers the
connectivity stream (reaches the scheduler, de-duplicated, cancelled on close).
The screens are in view/mastermind_inbox_page_test.dart,
view/mastermind_item_page_test.dart and
view/reply_line_limit_formatter_test.dart (the 10-line cap and its refusals).