User stories · Inventory
Route: /inventory (sidebar: Checks › User stories)
The User stories page in Mushi Mushi is the v2 positive side of the loop. Where Reports / Graph / Fixes catch what your users felt break, the inventory describes what should exist — every page, every action, every expected outcome — so the gates can fail when the two diverge.
User stories needs a plan that includes it. On such a plan it is listed under Checks in the sidebar; otherwise its card on Checks shows an Upgrade pill.
What you see
With no map yet, the page is one step: Turn on discovery (it opens the Discovery tab with the SDK snippet). Writing the map file by hand is the other way in.
Once the map exists:
- Status sentence across the top: how many actions are broken (stopped working, or no code behind them yet) and how many a test proves, with Check every action and Crawl the app.
- Your app, page by page: one card per page with what a user can do on it. Broken actions are red and sort first; pages with a broken action come first.
- Story cards: one per user story (
signup,pricing, …) with per-story counts and a ⋯ menu for runs scoped to that story. - Action drawer: click any action to inspect its
expected_outcome, the last walk and its history.
The tabs are User stories, All actions (the page × element table),
Code checks (open findings, ?tab=gates), Live probes
(?tab=synthetic), Out of sync (?tab=drift), Discovery and
Map file (?tab=yaml). The ?tab= ids are unchanged, so old links work.
Check every action / Crawl the app
The status line has one Check every action and one Crawl the app
for the whole project. They call the same server routes the GitHub
Action uses (@mushi-mushi/mcp-ci):
- Check every action runs the server-side checks: status claims, API contract, unused backend endpoints and calls to unknown endpoints. Results show on the Code checks tab. It posts nothing to a pull request: the lint checks (dead handlers, mock data) need your ESLint report, which only the GitHub Action sends.
- Crawl the app crawls every page in the map and reconciles it with
inventory.yaml. Differences land in the Out of sync tab.
To run just one story, open the ⋯ menu on its card:
- Crawl this story’s pages — crawls only the pages linked to that story.
- Run gates on this story — runs the gates on that story’s actions only.
SDK-driven discovery
Most teams will never hand-author inventory.yaml. Turn on
capture.discoverInventory in the SDK config:
import { MushiProvider } from '@mushi-mushi/react'
<MushiProvider config={{
projectId: '…',
apiKey: '…',
capture: { discoverInventory: true },
}}>
<App />
</MushiProvider>The SDK quietly observes routes, data-testids, and outbound API paths
in production. Claude drafts a candidate inventory.yaml from the
stream every 24 h and the Discovery tab surfaces the diff for
review. You accept, edit, or skip — the production payload itself
never contains user data.
Surface mode in the graph
The same inventory powers a Surface toggle on the
Knowledge graph — every Page / Element / Action node
overlaid on the live bug graph so the dead corners (high-traffic
elements with no expected_outcome) light up.
The gates
The GitHub Action rolls these into one composite check. The Code checks tab lists their findings under the names in the first column:
| Gate (console name) | Id | What it checks |
|---|---|---|
| Dead handlers | dead_handler | ESLint mushi-mushi/no-dead-handler: buttons and links whose handler does nothing. Needs your ESLint report from CI |
| Mock data in production | mock_leak | ESLint mushi-mushi/no-mock-leak: faker or placeholder data in non-test paths. Needs your ESLint report from CI |
| API contract | api_contract | Frontend calls whose request or response does not match the API (server side) |
| Live route crawl | crawl | Every page in the map loaded live; runs through Crawl the app, not as part of the composite check |
| Status claims | status_claim | Actions marked verified or wired in the map whose derived status disagrees |
| Unused backend endpoints | orphan_endpoint | Backend endpoints nothing calls |
| Calls to unknown endpoints | unknown_call | Frontend calls to endpoints the backend does not have |
Watch your live site
Discovery → Watch your live site asks Firecrawl to crawl your live site once a day, following your site’s own links from the address you enter. Mushi Mushi files a bug report, before a user finds the problem, for each page that:
- returns a server error (5xx);
- returns a 404 or another 4xx from a link on your own site (sign-in walls and rate limits don’t count);
- fails to load;
- changed in a way Firecrawl’s checker calls broken: an error message, a blank page, or missing main content. Normal wording and layout edits are ignored.
Pages your robots.txt blocks, links that only differ by ?utm_… parameters, and pages you removed are not reported.
A page has to fail twice before it becomes a report. When a check finds a broken page, Mushi Mushi loads that page again, waits about 10 seconds for it to finish loading, and files only if it is still broken. A page caught mid-load (a loading screen, only a “Skip to content” link, a status line waiting on a request) is not a broken page. A server error or a page that won’t load also files when two checks in a row see it. If your whole site is down, Mushi Mushi files one report within the hour. Two cases file at low severity: a page Firecrawl’s checker keeps calling broken that Mushi Mushi can’t load again to confirm (marked “unconfirmed”), and a server error that keeps coming back but loads fine each time Mushi Mushi looks again (3 times in 7 days, marked “intermittent”). These second loads use your app’s own Firecrawl key: at most 6 per hourly read, shared across your apps, 1 credit each.
Each broken page files one report. When 5 or more pages break at once (the site is down, or a deploy broke a shared layout), they file one report that lists them all. While a page stays broken, Mushi Mushi doesn’t file again. When it is no longer broken, it leaves the “Broken right now” list; its report stays as it is for you to close. If it breaks again later, it has to fail twice again before it files.
Check now runs a check straight away, and results show in 1 to 3 minutes. Each app can run 6 checks an hour this way; the daily check always runs.
The watch uses your Firecrawl key, or a key shared with all your apps. It costs 1 credit per page per day, about 750 a month for 25 pages. Firecrawl’s own estimate is up to twice that, because it holds back 1 credit per page for checking changed pages. Turn off deletes the crawl in Firecrawl and needs the key to do so; reports already filed stay. Only public pages are checked; flows behind sign-in belong in QA stories.
The crawler sends MushiSiteWatch in its User-Agent header (MushiSiteWatch/1.0 (+https://kensaur.us/mushi-mushi), after a normal Chrome user agent). Filter that string out of Sentry and your analytics so the daily check doesn’t count as visitors or errors, for example by dropping events in Sentry’s beforeSend when the request’s user agent contains MushiSiteWatch. The header is on the HTTP request; don’t rely on navigator.userAgent showing it. A watch turned on before this header was added starts sending it the next time you save it or turn it off and on.
Synthetic monitor
Mushi Mushi re-walks the inventory’s expected_outcome checks against your
staging URL every 15 minutes once you enable it. The monitor is
fail-closed by default: write paths (POST / PUT / DELETE actions)
are skipped unless you turn on Allow mutations for the project in
Map file → Crawler & probes (the synthetic_monitor_allow_mutations
project setting; it is not an inventory.yaml key).
Set this only when staging is fully isolated from production and the write endpoints have an idempotency key or roll back cleanly.
Spec traceability — every fix carries the originating Action
As of the 2026-05-09 release every fix dispatched against a report that
has been linked to an inventory Action (via the reports_against
graph edge classify-report writes) carries that link end-to-end:
- The dispatch row (
fix_dispatch_jobs.inventory_action_node_id) and the resulting attempt (fix_attempts.inventory_action_node_id) both store the FK to thegraph_nodesrow for the originating Action. - The fix-worker LLM prompt includes a Markdown spec-context block with the action description, page, story, and every assertion in the contract — see Concepts → Spec traceability — read AND write side.
- A deterministic
validateAgainstSpecgate runs before the PR opens. Soft warnings (no changed file references the contract’s required DB table or page route) land onfix_attempts.spec_validation_warnings JSONBand surface in the fix’s detail panel under Spec validation warnings. - A targeted post-PR synthetic probe is queued for the originating
Action immediately after the PR opens. The synthetic-monitor cron
drains the queue with priority on its next tick — so a fix that
immediately makes the action
regressedshows up here within minutes of merge, not on the next 15-min reconciler sweep.
External AI agents (Cursor, Claude Code, OpenAI Agents SDK, LangGraph, CrewAI, A2A v1.0.0) get the same anchor — see Connecting your AI agent for the per-surface details.
See also
- Concepts → Inventory and gates — the conceptual model.
@mushi-mushi/inventory-schema— the Zod / JSON Schema forinventory.yaml.@mushi-mushi/mcp-ci— the GitHub Action.eslint-plugin-mushi-mushi— the lint rules.@mushi-mushi/inventory-auth-runner— bootstrap an authenticated session for the crawler / monitor.