← Design · c134

kamal-prep.md

Kamal prep: the native app and the plane

Read before: Mark's call with Kamal Nasser, who owns the plane's backend (Pulse, Go) and its new data model. Tyler, session 1b21a3ce (2026-11-05): "Kamal, in pretty short order, would come off, and he would start to repurpose Pulse with a new data model"; and "we shouldn't hold Kamal up for this, he has a few things he needs to get done first". Kamal also built the terminal UI Tyler showed in that session (an overview, qcontrol's own CPU and memory, the monitor, workers, events): the nearest thing to the native app's Status page that exists today. Tyler checks the Windows installer with him too (an earlier call). Written: 2026-11-10 from the records: c130's plan and primer, c131, c134, c136, c137, the spike in ../qnative, the bible. Measured numbers are the spike's; everything else is read or ruled. Names: "the native app" and "the plane" (Qplane in public); c0 in ../qnative picks the real ones, so coin none on the call. Mark's three questions, one line each. Preparatory work: this brief, the demo set up before the call (§2), and the two proposals (§4), which can go to Kamal ahead of time. Demo-able: yes, a ten-minute slice (the pages on :3030, one native page with the tray and three data sources, the inbox mock, the gap list); no real native pages yet. Prototyping the pages: §5.

Update 2026-11-11 (c138 Developer Mode). Question 6's second half is answered by a survey of the code (../c138.developer-mode/research/monitor-tui.md): his TUI is the monitor process. It reads an in-process snapshot (workers, health, targets, behaviors) and the event route, and samples CPU and memory itself; apart from the event stream and the installation and process snapshots, none of it can be read from another process today. The ask therefore has a concrete form: the Guide for Kamal, seven proposal pages in ../qnative/cycles/c3.developer-mode/guide/ (on this site at /cycles/qnative/c3/doc/guide/README), with the stream contract, the Monitor's shapes as proposed routes under c131 D17, and ten questions. It is the thing to send him ahead of the call, beside the two proposals in §4. One correction to §1: the app's local source looks in the wrong data directory on macOS and Windows today (Guide page 2 has the workaround). Reconciled 2026-11-12 (c143): the Monitor's health, resources, log and loaded config are optional fields on D17's /api/sensor and /api/config, so the ask is D17's three routes plus two of the Monitor's own (/api/monitor/workers, /api/monitor/processes), not D17's six shapes plus four more.

1. What the native app needs from the plane

The app reads through one switch with three sources (c130 D6, c131 D3): synthetic (the bundled world), local (this machine's qcontrol over qdash's loopback API, found through qdash.sock.lock) and plane (a base URL plus a standardised credential; Authorization: Bearer in the spike). Every page reads one type per served shape (bible R13: ../qcontrol/crates/qdash/ui/app/types/api/*, 28 files written from web.rs), and the plane serves the same shapes at the same routes until a schema generates both (§4.2). Connected to a plane the app is read-only (c131 R4): it edits nothing and shows what the plane holds. Standing in the last column: today = one of the 38 routes (inventory.md §4); gap N = row N of the 30-row gap list (inventory.md §6); new = served nowhere and listed nowhere. Polling is the bible's primitives at 3 s; the one stream is SSE.

Status (on / off, local / connected, CPU and RAM, workers, plugins, logs, effective config; c131 Phase 2)

ResourceShapeStream or pollWindowRead-only when connectedStanding
The headline numbers and the world seamstats.ts: agents, active, sessions, events, coverage {injection_failures, adapter_errors, uninspectable_conns}, world, buildpollcost and tokens 24 h, fixedyestoday; gap 2 (one window), gap 26 (hub {connected, subscribers})
This machine's rowendpoint.ts EndpointDetail (/api/endpoints?host=: installs_detail, agents_detail, os, monitored, workspaces)poll?period= presetsyestoday; gap 1 (from / to)
Sensor version, heartbeat, healthnonepoll; the heartbeat the app sends is where it comes fromn/ayesgap 27
qcontrol's own CPU, RAM, workers, logsnone on the API; Kamal's TUI reads them from qcontrol itselflocal, live; the plane sees only the heartbeat's summaryliven/a (local)new, local; ask where the TUI reads them (§3 Q6)
The grant linesee "The grant line" belowpolln/ayesgap 11

Inventory (harnesses, MCP servers, skills, tools, sessions and workspaces, models, providers, accounts seen on this machine)

ResourceShapeStream or pollWindowRead-only when connectedStanding
The nine category listingsharness.ts, mcpServer.ts, skill.ts, tool.ts, session.ts, workspace.ts, model.ts, provider.ts, account.ts; detail by each category's identity parampoll?period= 24h / 7d / 30d, else all; cut back from the fleet anchor, not the wall clockyestoday, fleet-wide; the spike's types crate mirrors Harness, McpServer and Skill field for field
The one-machine slice of each listingthe listings take ?period= and ?agent= (some ?team=), none ?host=; only /api/endpoints?host= is per machinepollas aboveyesnot on the gap list: an endpoint scope on every listing (the alternative, filtering client-side, is wrong at fleet size); candidate row 31
What an actor touchessurface.ts (/api/surface?<scope>=&period=)poll?period=yestoday
Installs at restinstallation.ts (instrumented, fp_confirmed)pollnoneyestoday; gap 2

Events (the live stream and the audit log, with the detail sidebar; Mark's "Live view")

ResourceShapeStream or pollWindowRead-only when connectedStanding
The live taillive.ts StreamEvent: SSE, one qevents record per frame, no replay, no cursor; the static qdash serve answers 503streamliveyestoday, local only (the hub is in-process); gap 26 for the plane: a stream from its store with a cursor
A session's eventssession.ts SessionEvent (/api/sessions?run=)polldetail: all-timeyestoday; gap 24 (skill invocations, judged tools, summary)
Findings on this machinefinding.ts (/api/security?period=&team=)poll?period=yes; decisions are the operator's, on the web apptoday; gap 23 (host on the row, sla_due written)
The audit log as a resource/api/activity: served, no type file, read by no pagepollnoneyestoday; gap 2; the grant and message events join it (c136 D5, c137 bones §9)

Config (the file found and the effective result; editable locally, read-only when connected)

ResourceShapeStream or pollWindowRead-only when connectedStanding
The file found and the effective confignone on the API; qconfig locallylocal readn/athe page says the plane manages it and shows what appliesnew, local; c130 shape
The controls that applycontrol.ts (/api/controls, /api/control-vocab)polln/ayes; edited and published on the web app (c118)today
The policies that applyGET /api/policiespolln/ayesgap 14 (staged on local() today, bible R25)
Org settings that reach an endpoint (the IT contact for local Contact IT, c137)GET /api/settingspolln/ayesgap 9

Connect (sign in, pick the plane, confirm, connected; the one V1 message flow)

ResourceShapeStream or pollWindowRead-only when connectedStanding
The exchange: base URL plus credentialthe app sends Authorization: Bearer <token>; the fixture plane takes --token and answers 401 to a wrong one, drawn as a failed staterequestn/an/anew: gap 10 (enrolment, credential format, heartbeat); c131 D13; c130 Q6
How the app finds its planeconfiguration, the Connect page, or a deep link: qpoint://connect?… needs an installed, signed bundle; a loopback redirect with PKCE (RFC 8252) works in dev buildsn/an/an/anew; primer question 6
The receiptthe identity-confirmation message (c137 bones E4: machine, account, plane)messagen/ayesnew; gap 12
Disconnect, sign in again, "HQ disconnected this machine"the same message type; the credential revoked on the planerequest and messagen/an/anew

The message inbox (c137; the plumbing ships in V1, c130 D9)

ResourceShapeStream or pollWindowRead-only when connectedStanding
Messages to this endpointbones.md §1 Message: ULID id, type, severity info / attention / required, three sentences, up to three actions, why, states new → seen → acknowledged → done / expired, thread, localpoll; a stream later with gap 26none, it is a record; expires_at per typethe person acts (acknowledge, reply, decline); content is the overseer's (R3)new: gap 12; the plane's types/api/messages.ts twin lands with c134 Phase 2
State carried backacknowledged, done, declined with timestamps; seen only where requires_ack (bones §5)writen/an/anew
Replies from the personcontact-it, reply, diagnostics-sent, training-completed, in the root's threadwriten/an/anew
The plane's three settingsmessages.sender_label (default "HQ"), required_on_notices, required_soft_limit (3)polln/ayesnew

The grant line (c136; read-only in the app, c131 D12)

ResourceShapeStream or pollWindowRead-only when connectedStanding
Active grants on the plane this endpoint reports topurpose (diagnose / fix / implement / hot-fix), level (observe / operate / change), scope, window, named people, actions so far (c136 "The shape")pollstart, duration, expiryyes: one line on Status while active, nothing otherwisenew: gap 11; grants are plane objects (c136 D1); access.grant.* events in the audit stream (D5); a grant-notice message to the operators

Count. Of the 38 routes the app reads 18 as they are (the nine listings, stats, endpoints, surface, installations, events/stream, security, activity, controls, control-vocab), and the two host-local actions agent/focus and agent/reveal leave the plane for the app (inventory.md §5). The gap list adds rows 1, 2, 9, 10, 11, 12, 14, 23, 24, 26 and 27. New and nowhere listed: qcontrol's own process figures (local), the endpoint scope on the listings, and the Connect exchange's exact form.

2. The demo, ten minutes

Before the call (fifteen minutes, once). Start qdash-seed (:7882, the Meridian world) and then qdash-app (:3030) from design's launch configs; neither was running at writing, :3000 was. In ../qnative, cargo build --release -p app -p fixture-plane so the run commands are instant (a clean release build is 1 min 42 s, debug about 3 min; Rust 1.95 or later, 1.99.1 is installed). Open /cycles/design/c134/doc/inventory.md on :3000 in a tab. Keep a terminal and the browser side by side; the native window opens at 1120×721. Do not reload the inbox page between the send and the read (the mock's state is shared within the visit).

MinStepDoIt proves
0–2The pages being portedOn :3030: / (the index of micros), Inventory › Harnesses, Organization › Endpoints › one host (the machine's slice: installs, harnesses, workspaces), Live (against the static seed it answers 503 and the page says so). Point at the STAGED WORLD · Meridian pill and the when: pill.The 44 pages exist on one data discipline: 28 contract types written from the handlers (R13), honest states (R31), staged values marked (R23, R25). The native app takes the one-machine slice of these; the plane takes all of them, with a frame around them.
2–4The native window and the trayFrom ../qnative: cargo run -p app -- --data synthetic. The kit's title bar, a sidebar (Harnesses · MCP servers · Skills), a data table per listing over a 12-row bundled world, a footer reading "data: synthetic". Close the window: the process stays in the menu bar; "Open Qpoint" from the tray brings the window back; Quit ends it.GPUI Kit 0.9.1 draws our shell with no custom component; the tray lives in the same process on GPUI's loop (c131 D7, path B; the two-process fallback not needed on macOS); Inter is bundled; every lifecycle call the primer listed as unverified exists.
4–6The third source: a plane at a URLSecond terminal: cargo run -p fixture-plane (its URL is the first line; --port N fixes it, --token T demands the credential). Then cargo run -p app -- --data plane --plane-url http://127.0.0.1:<port>: the same rows, footer "data: plane (http://127.0.0.1:)". With --token on the plane and QPOINT_PLANE_TOKEN wrong on the app, the page draws "failed: … answered 401". --data local draws "qcontrol's dashboard is not running on this machine".One switch per process, never per page (c131 R2). The plane source is a base URL plus a bearer, so Kamal's Go app replaces the fixture plane at the same routes with nothing changed in the app; a wrong credential and an absent sensor are drawn, not blank (R5).
6–7The budget, measuredSay the numbers from ../qnative/README.md under Footprint: strobe 0.26.0, 2026-11-09 and 2026-11-10, M2 Max, macOS 15.6, release build, idle for 60 s: windowed 52–80 MB private (RSS 66–68 MB; the spread is GPU surface memory that follows the display), tray-only 16 MB, CPU 0.005 % of one core at 0.3–1.2 wakes/s, first render 264 ms, binary 17.9 MB of which 1.8 MB is Inter.The footprint is a number with a method, not a quote from a blog; it sits beside the sensor's own figures on the same machine; any agent can repeat the recipe in the README.
7–9A message from the plane lands in the inboxOn :3000, /pages/phase4/mock/messages (the operator's inbox and Send, in the qdash shell): send an hq-notice to MBP-2291; type an em dash or "urgent" and the lint flags it. Follow the in-app link to /pages/phase4/messaging-inbox: the message is there as new; open it (seen); acknowledge the censure and the tray line's count drops.The message is specified to the wire (bones.md §1): one schema, three severities, five states, the required rule with its monthly count. Messages are a resource on the plane (list, send, recall, reply, state; gap 12), not a side channel; the inbox is a record, not a pile.
9–10Where the plane has to reach/cycles/design/c134/doc/inventory.md: §4 (the 38 routes with their window and writes), §6 (the 30 gaps; the window and identity groups block the most pages), §9 (eleven questions, most of them his).The contract gap is enumerated, each row with where it was recorded; c134 Phase 2 writes it as the document he implements.

What is honest to say. The native app has one page, an Inventory table, not the five; the stories, the messages crate and the Meridian world in fixtures land with c131 Phase 1. local is a stub: it reads qdash.sock.lock and compiles without qcontrol, and this Mac has no qcontrol data dir, so it has only ever said "not running". There is no signed .app, so system notifications and qpoint:// are unexercised (the spike's "Still open"). Windows and Linux are wired and unbuilt: the CI workflow exists and the repository has no remote. c130's shape.md is not written (G1 is "D1–D10 ruled, shape pending"). How native pages are mocked is undecided (c132, §5); the inbox page is a web rendering standing in. qdash has no auth and no org column; the plane's data model is Kamal's, and nothing here presumes his tables.

3. Questions for Kamal, in order

Ten; each with why it matters and the default we would accept if he has no view yet. Brackets name the record the question comes from.

#QuestionWhy it mattersDefault we would accept
1When are you free, and where does the code live? (c134 Q1; c130 Q2, Q10)Every c134 phase after the inventory waits on this; the Phase 2 contract is written for him to implement, so his start date sets ours.A new control-plane repo with the Go app and the UI (c134 D4); the native app a sibling until G2; design writes the contract now and he reads it when he lands.
2Can design read Pulse's source, and which of its patterns survive? The server-side filter grammar ("the JavaScript filter"), /contexts (member-scoped views), the four alert rule kinds. (c134 Q2)The global filter and the when: pill are the shell; if the filter grammar is their backend half, every list route is shaped by it. Views decide gap 8; the rule kinds decide D7.Read access; the filter grammar as the server side of the global filter; views dropped for the global filter unless Tyler keeps them; the rule kinds as the alert engine's first spec.
3Will the API publish a schema we generate from, and does every row carry org_id from day one? (c134 Q8, Q3; c131 R3)Three consumers (the web app, the native app, his tests) hand-write the same types today; one schema makes drift a build error (§4.2). org_id everywhere keeps SaaS multi-org a flag, not a migration (c134 D3).OpenAPI 3.1 from the Go handlers, TypeScript and Rust clients generated in CI; org_id on every row, one org per install.
4Identity: OIDC plus local accounts, SAML kept; who issues the session, and what does the native app hold? (c134 Q3, D2; gaps 5–7)Self-hosted has no Firebase and may have no IdP; the native app holds no browser cookie; /api/me and invitations are the frame.OIDC plus local accounts; a cookie session for the web app, a bearer for the native app and the endpoint, GET /api/me for both.
5The Connect exchange: how does an endpoint enrol, what credential does it get, how often does it heartbeat and what does the heartbeat carry? Also: a qpoint:// scheme or loopback OAuth with PKCE, and whether the sensor sees loopback traffic. (c131 D13; c134 Q7; gaps 10, 27; primer question 6)It is the one V1 flow and the plane's half of it; the receipt is the first message; version and health per endpoint come from nowhere else.An OAuth-style sign-in in the browser with PKCE; a loopback redirect for unbundled builds and qpoint:// once installed; a per-endpoint bearer in the OS keychain, revocable on the plane; a heartbeat every few minutes carrying sensor version, health and the app's version.
6When the dashboard leaves qcontrol, what does the app read locally: the socket API, the SQLite, or the crates? And where does his TUI read qcontrol's CPU, memory, workers and events? (c131 Q2; c130 Q3)The spike's local source reads qdash's loopback API. If the qdash crate goes with the UI, that API goes too, and the Status page's process figures have no feed; his TUI already has one.The loopback API survives as qcontrol's local read surface (the 38 routes, one machine) or a slimmed one; the TUI's feed becomes the Status page's; SQLite direct only if he prefers it.
7Grants as plane objects, and the mechanic's identity. A grant: purpose, level, scope, window, named people; access.grant.* events in the same audit stream; the mechanic signs in with a named Qpoint identity his plane accepts only while a grant names it; a one-time invitation link for a self-hosted plane with no reachability. (c136 D1, D3, D5)It is the door we will use on his customers' planes; without it the first fix is a screen share or a shared credential.Grants in the data model and the contract; OIDC from Qpoint's IdP as the mechanic's identity; the invitation link for sealed installs.
8Messages as a resource, and does the overseer see seen? List, send, recall, reply, state changes; the three settings; message.* audit events. (c137 D1; bones §5, §9, §12; c137 inventory question 4)The operator's inbox and the native inbox are one record seen from two sides; a side channel would fork it. seen is a privacy line: the record is honest and bounded (c137 R6).A messages resource in the contract; seen recorded only where requires_ack; nothing else about reading.
9The window and the live tail. from / to on every list and rollup with the presets kept; cut from the wall clock or from the data's anchor; a stream from ClickHouse with a cursor, or polling first? (c134 Q4, Q6; gaps 1, 2, 26)The window group blocks the most pages (reports, the range picker); the baked worlds depend on the anchor; the native Events page is local today and needs a plane-side replay to show what the plane saw.from / to everywhere with period as presets; the wall clock in production, the anchor for staged worlds; polling first, a cursor stream in the first revision.
10The demo world: can his model import and export the Meridian world, and do c113's baked worlds survive the new routes? (c134 Q9; c131 D4)§4.1 depends on it; the baked worlds are keyed by URL and die with the routes otherwise.An import and an export of a world in the Go app; the demo re-baked against the new API.

The three he is most likely to push back on: 3 (a schema and generated clients is a discipline on his handlers), 6 (keeping a read API on the endpoint after the dashboard leaves is the opposite of the roadmap's motion) and 9 (from / to and a cursor stream are the most work in the list).

4. Two proposals

4.1 The Meridian world as an artifact his data model emits. The world exists three times today: the seed (../qcontrol/scripts/qdash-seed/generate.py) writes it into qdash's SQLite; c113 bakes it into static JSON per URL for demo.qpoint.io; the spike's fixtures crate hand-writes 12 rows and c131 D4 plans to replace them with an export of the seed. Proposal: one world file, defined as the routes' responses (one entry per route and query, the shapes of §1), that (a) the seed emits, (b) the Go app imports as a demo org and exports from any org, redacted, (c) the native app bundles as synthetic, (d) the fixture plane serves, (e) both apps' stories and specs read as fixtures. Then web and native agree by construction: the same world through the same contract, and a contract change that breaks the export breaks the build. What Kamal adds: a world import and a world export in the Go app, and his word on the format (the response shapes as they are, or his tables with a view onto the shapes). What we add: the export from the seed and the loader in fixtures. c131 D4 and c134 D5 already say this in halves; the proposal joins them.

4.2 One schema source, so the types are generated rather than written twice. Bible R13 says every served shape is a TypeScript type written from the backend handler: 28 files, each header citing its handler and the commit it was checked against. The spike mirrors three of them in Rust by hand, with a comparison check "until one generates the other" (c131 R3). Three records name three sources of truth (the TypeScript, the Rust, the Go); pick one: the Go handlers publish a schema and the rest is generated. OpenAPI 3.1 fits (REST and JSON routes, SSE for the stream, tools on every side: oapi-codegen or huma on Go, openapi-typescript for the web app, progenitor for the Rust types crate); protobuf only if Pulse already speaks it, since the price is a second encoding on the wire. R13 extended: the schema is the contract, the types are its output, and types/api/* keeps its header comments as the review trail. What Kamal adds: the schema published from the handlers and versioned with the API. What we add: the generators in both repos' CI, and the comparison check until they land. Ask which he would run; either is better than three hand-written copies.

5. Prototyping the pages (c132, the current thinking)

c132's bake-off has not run; this is where the records point.

  • The kit has no browser renderer we can use. Its wasm build is "Showcase only" on nightly (primer surprise 6), so medium C (the real UI embedded in a lab page) is weak, and the loop cannot be capture.mts against a URL.
  • The realistic pair is A plus B. A: Vue mocks in a native skin on the design site, a theme that mirrors the kit's tokens and chrome, so every existing tool works and a reviewer needs nothing; c137's inbox page (/pages/phase4/messaging-inbox) is the precedent, a web rendering standing in for the native app. B: a stories crate in ../qnative, one story per page state (empty, minimal, typical, overflow, error), run as headless tests (c131 R6) and screenshotted into the cycle atlas with surface: native (c132 D3), so the real thing is reviewable without a toolchain on the reviewer's machine. A for ideation and early review, B as the proof and the test; the token bridge (brand/design_tokens.md to a generated kit theme, Inter bundled) is the seam between them.
  • Figma is an input, not the medium (c132 medium D): a kit-shaped library if one turns up; /figma-to-vue feeds A. Illustrator works from captures only (c132 D5).
  • Both Mark and agents mock (c132 question 1, answered 2026-11-09), so the medium has to serve a person without Rust and an agent with it; a captured, reviewable artifact per page state is non-negotiable whichever wins.
  • For Kamal: every mock runs on the fixture world (§4.1), so a mocked page is that page's data needs made visible, and the Phase 2 contract is written from them. The bake-off (c132 Phase 1: Inventory in A, B and C, three states each, side by side) runs once the primer is accepted; Mark picks at Phase 2.

6. Where the records disagree

  • local reads an API the roadmap removes. The roadmap takes the dashboard, the qdash crate, out of qcontrol; c131 D5 and the spike read qdash's loopback API as the local source. Nobody has ruled that the API stays on the endpoint when the UI leaves (Q6).
  • Three sources of truth for the types. c130's plan makes the TypeScript the contract; c131's patterns generate Rust first, then TypeScript from it; c134's patterns generate from the Go handlers. §4.2 picks one.
  • One switch, two truths when connected. c131 R2 says one source per process; a connected app still reads this machine's live stream and process figures locally (the plane has them only after the fact, gap 26) while reading config, messages and grants from the plane. The nozzle needs a rule for "connected": the plane for what the plane owns, local for what only this machine sees. Design's to settle, not Kamal's; it changes what §1 asks of him for Events.
  • The endpoint scope is missing from the gap list. The listings take ?agent= and ?team=, never ?host=; the connected app's Inventory has no server-side slice (§1). Add it to inventory.md §6 at the review.
  • The default window. qdash defaults to 7d; c128 ruled 30d at the port. One of them is the contract's default.

Qpoint Brand Style Guide