bones.md
c137 Phase 1: the bones
Written: 2026-11-09. Reader: c131's executor (the types entry lands with c131 Phase 1 in ../qnative; nothing is written there by this cycle), c134's executor (the plane's types/api/messages.ts twin), c133 and c136 (their one type each), Mark at the review stop.
Status: a specification, written as the wire shape. Field names are snake_case on the wire; Rust and TypeScript follow their own casing. Timestamps are RFC 3339 in UTC; the app renders them in the machine's zone.
Rules it carries: R1 one schema for every message · R2 three sentences · R3 the citizen controls delivery, the overseer controls content · R4 nothing is alarming · R5 every message ends · R6 the record is honest and bounded · R7 use the OS.
1. The message
Message {
id: MessageId // ULID: sortable by creation time, unique across planes and local mode
type: MessageType // §2
severity: 'info' | 'attention' | 'required' // §3; `required` is also the one override (§6)
from: Sender // §7
to: Recipient // §7
batch?: BatchId // a send to a team fans out to one message per endpoint; they share a batch
subject: string // one line, 60 characters at most; the OS notification's title
body: { // R2: three sentences, in this order, each 200 characters at most
happened: string // what happened
matters: string // why it matters
next: string // what to do next
}
verbatim: boolean // D6: a censure is HQ's words, shown as written and labelled; never rewritten by us
actions: Action[] // 0 to 3; §4
links?: { // what the message points at, for `open-page` and for the operator's record
finding?: FindingId, policy?: PolicyId, training?: TrainingId, grant?: GrantId, endpoint?: EndpointId, page?: Route
}
why: string // "why am I seeing this": the policy or the person that caused it; fixed at send time
requires_ack: boolean // the person must press Acknowledge for the message to end
expires_at?: Timestamp // §5: none for `required`; per-type default otherwise
created_at: Timestamp // when the sender created it
delivered_at?: Timestamp // when this app received it (local mode: equals created_at)
state: State // §5
state_at: Timestamp
state_reason?: 'recalled' | 'expired' | 'declined'
thread: MessageId // the root message of the conversation this belongs to (itself for a root)
in_reply_to?: MessageId // set on replies
mute_key?: string // what a mute attaches to: `policy:<id>` or `thread:<id>` (§8)
local: boolean // true when no plane is involved: the record exists on this machine only
}
What is not in the message: a read time, a dwell time, a device fingerprint, the person's reply drafts, the delivery channel it took (the channel is the citizen's setting, not the record's business: R3, R6).
2. Types
The fifteen kept in inventory.md. The wire word is the type id.
| Type id | Direction | Default delivery | requires_ack | required allowed | Owner |
|---|---|---|---|---|---|
enforcement-notice | plane → person, or qcontrol → person (local) | notify | no | yes (D4) | c137 |
hq-notice | plane → person | inbox | no | only if the plane allows it | c137 |
identity-confirmation | plane → person | tray | no | no | c137, c131 (the V1 flow) |
censure | plane → person | notify | yes | yes (D4) | c137 |
responsibility | plane → person | tray | yes | no | c137 |
training-assignment | plane → person | tray | no | no (c133 D9) | c133 |
diagnostics-request | plane → person | tray | no (the actions end it) | no | c137 |
sensor-state | qcontrol → person | tray | no | no | c137, c131 (the tray) |
reply | either way, in a thread | tray (person); inbox (operator) | no | no | c137 |
acknowledgement | person → plane | n/a: a state carried back | — | — | c137 |
contact-it | person → plane | operator inbox | — | — | c137 |
training-completed | app → plane | operator inbox (quiet) + audit | — | — | c133 |
diagnostics-sent | person → plane (also carries declined) | operator inbox | — | — | c137 |
connect-request | app → plane, on approval planes only | operator inbox | — | — | c131, c134 |
grant-notice | plane → operator | operator inbox | no | — | c136 |
3. Severity
Three words, and only these three reach the citizen's UI (R4): info, attention, required. Never danger, critical, urgent or alert.
| Severity | Means | Delivery it asks for | Colour (the brand's rule: orange for work left, red only for blocked or failed) |
|---|---|---|---|
info | a fact: a receipt, a letter | the type's default | grey pill; no colour |
attention | something for the person to look at or do | the type's default | orange pill ("Attention") |
required | the one override: passes quiet hours and Focus, cannot be turned off, must be acknowledged, is counted (§6) | one OS notification, whatever the settings say | orange pill ("Required"), the count beside it |
A blocked action inside a body ("qcontrol blocked a paste") is the only place the error tone appears, on that word, because something was blocked. The message card itself is never red.
4. Actions
Action { label: string, kind: ActionKind, target?: string }
ActionKind = 'open-page' | 'acknowledge' | 'start-training' | 'contact-it' | 'allow-diagnostics' | 'decline' | 'dismiss'
- At most three, in the order shown; the first is the primary.
acknowledgeis added by the app whenrequires_ackis true, never by the sender. open-pagetargets a route in the native app (status,connect) or on the web app (a finding, a policy, a training); the web app opens in the browser, signed in through the plane connection (c131 D13).contact-itopens the reply form (§7) with the message's context attached. In local mode it opens the IT contact named in qcontrol's configuration (it_contact: a mailto or URL); absent, the form says so.allow-diagnosticsshows the package before anything is sent;declineends the message as done withstate_reason: declinedand tells the sender.dismissexists only forinfo; it ends the message as done.- "Why am I seeing this" is not an action; it is a line on every card (
why).
5. States and transitions
new ──opened──▶ seen ──acknowledge──▶ acknowledged ──action completes──▶ done
│ │ │
│ ├──action completes / dismiss (info)──▶ done
│ │
└────────────────┴──expires_at passes · operator recalls──▶ expired
| Event | From | To | Who |
|---|---|---|---|
| delivered (the app received it; the OS notification, if any, is posted now) | — | new | the app |
| opened (in the inbox, or the notification was clicked) | new | seen | the person |
| Acknowledge pressed | new, seen | acknowledged | the person |
the linked action completes (training-completed, diagnostics-sent, contact-it sent, open-page for a confirmation) | new, seen, acknowledged | done | the app |
| Dismiss (info only) | new, seen | done | the person |
expires_at passes | new, seen | expired (state_reason: expired) | the app |
| the operator recalls it (sent by mistake; §9) | new, seen | expired (state_reason: recalled); the card says so | the operator |
- Terminal states:
done,expired, andacknowledgedwhen the message has no completion action (a censure, a responsibility). R5: every message reaches one. expires_atdefaults:info30 days,attention90 days,requirednever (it stays, and the tray count holds it, until acknowledged). The sender may set a shorter one (a notice about Monday expires Monday).- One re-surface, not reminders: a message with
expires_atappears once more at the top of the tray's one line the day before it expires. Never a second OS notification (inventory row 21). - What the plane records (R6): created, delivered, acknowledged, done, expired with their timestamps;
seenonly for messages withrequires_ack(so the operator can tell "seen, not acknowledged" from "never opened"); nothing else. Default for plan Q2; Tyler or Peterson may draw it tighter. - Inbox order: open threads first (newest activity on top), then ended ones under a fold; ended messages are kept, not deleted. The inbox is a record, not a pile.
6. The required rule and its count
severity: required is the single override, and it is counted. Honesty in both directions.
- Who may set it: the sender, only on
enforcement-noticeandcensure(D4); onhq-noticeonly when the plane'smessages.required_on_noticessetting is on (off by default). A policy may set it for its enforcement notices; a person sets it per message. Any other type: the plane refuses the send. - What it does: one OS notification even during quiet hours and the OS's Focus (R7 permitting: §ports on time-sensitive delivery); ignores a type set to off; cannot be muted; implies
requires_ack; never expires; holds a place in the tray count until acknowledged. - What it does not do: repeat, escalate, sound different, or change colour beyond the "Required" pill. One notification, then the inbox.
- The count:
required_this_monthper recipient per calendar month, incremented at delivery. The citizen's settings page shows it: "HQ has sent this machine 2 required messages this month." The operator's send form shows it before sending: the fleet total this month and the highest count any chosen recipient already has. - The soft ceiling: the plane setting
messages.required_soft_limit(default 3 per recipient per month). Above it the send form asks for a one-line reason that is shown to the citizen on the card. Not a hard limit; an incident is an incident. - In local mode: the policy file sets
requiredper policy; the count is kept locally and shown the same way ("This machine's configuration has sent 1 required message this month").
7. Sender, recipient, thread
Sender = { kind: 'plane', label: string, operator?: Person } // label: the plane's sender label, default "HQ" (§7.1)
| { kind: 'qcontrol' } // local mode: "qcontrol on this machine"
| { kind: 'person', person: Person, endpoint: EndpointRef } // replies from the citizen
Recipient = { kind: 'endpoint', endpoint: EndpointRef, person?: Person } // the citizen; person when known
| { kind: 'plane' } // the operator's inbox
Person = { id, name } EndpointRef = { id, name }
7.1 The sender label
A plane-set string, messages.sender_label, default "HQ" (Mark's prompt; c0's proposal: each customer writes their own word, "Security", "IT", "HQ"). Shown alone for automatic messages ("HQ", with why naming the policy) and with the person when one wrote it ("HQ · Dana Whitfield"). Local mode shows "qcontrol on this machine". Overseer and citizen never appear (D7).
7.2 The thread
- A root message's
threadis its own id. A reply (contact-it,reply,diagnostics-sent,training-completed) carriesthread= the root andin_reply_to= the message it answers. - The citizen's Contact IT is a
contact-itmessage to{ kind: 'plane' }with the root's context attached automatically (policy, finding, endpoint, time): the person writes under three prompts (what were you doing · what do you need · anything else) in their own words; R2 does not bind the person. - The operator's answer is a
replyin the same thread, delivered per thereplytype's setting (default tray). The inbox shows one row per thread. - A thread ends when its root ends; replies inherit the root's
mute_key.
8. Mute keys
A mute is the citizen's, for a day (plan): mute_key is policy:<id> on automatic messages (so "mute this policy's notices for today" catches the next one) and thread:<id> on everything else. A required message ignores mutes but still carries the key, so the settings page can say what was muted and what came through anyway.
9. The send side (for c134)
The operator creates a message with type, recipients (a person, an endpoint, a team, everyone), subject, the three sentences, up to three actions, severity, expires_at, and for a censure verbatim: true. The plane validates: the lengths, the type's required permission, the soft ceiling, the sentence count; the brand voice rules run as a lint on the three sentences (the pattern from the plan: the ten examples below are the fixtures). A send to a team becomes one message per endpoint under one batch. Recall withdraws every message in the batch that has not ended. The operator's inbox lists the replies and the states; the audit log keeps message.sent, message.acknowledged, message.done, message.expired, message.recalled (R6).
10. Ten example messages
One per important type, each body in the brand's three sentences: what happened, why it matters, what to do next. Subjects are 60 characters or fewer. Times are the machine's local time; the fixture world is Acme's, with Dana Whitfield at HQ and Mark Parson on MBP-2291. These ten are the lab mock's data (app/data/messaging-fixture.ts) and the voice fixtures for every message type that follows.
E1 · enforcement-notice, from the plane · attention · V1 if ruled in
From HQ (policy Secrets in prompts) · Why "Policy Secrets in prompts, enforced for Engineering since 3 November" Subject A paste into ChatGPT was blocked
- At 14:02 qcontrol blocked a paste into chat.openai.com because the text held an AWS access key.
- The key stayed on this machine; your company's policy Secrets in prompts keeps credentials out of AI tools.
- If this was work you needed to do, contact IT from here; otherwise nothing more happens.
Actions Contact IT · See the policy · Links finding F-2291-0412, policy P-SECRETS
E2 · enforcement-notice, local mode (from: qcontrol) · attention · V1 if ruled in
From qcontrol on this machine · Why "Policy Personal data in prompts, from this machine's qcontrol configuration; no HQ connected" Subject An upload to Claude was blocked
- At 09:41 qcontrol blocked the upload of customer-export.csv to claude.ai because the file holds email addresses and phone numbers.
- The file stayed here; this machine's policy keeps personal data out of AI tools.
- If this upload was work you needed to do, write to it-help@acme.example, the contact named in this machine's qcontrol configuration.
Actions Write to IT (mailto) · See the policy (local) · local: true
E3 · hq-notice · info · later
From HQ · Dana Whitfield · Why "Dana Whitfield at HQ sent this to every team" Subject AI tools: what changes on Monday
- From Monday 17 November, pasting source code into AI tools other than GitHub Copilot and Claude for Work will be blocked on company laptops.
- The change comes out of October's security review and applies to every team.
- Nothing to do now; if your work needs another tool, ask IT before Monday.
Signed Dana Whitfield, HQ · Actions Ask IT · See the policy · expires_at Monday 17 November 09:00
E4 · identity-confirmation · info · the V1 flow
From HQ · Why "You connected this machine from Connect at 11:20" Subject This machine is connected to HQ
- MBP-2291 is now connected to HQ at hq.acme.example as mark@acme.example.
- HQ manages this machine's configuration from there, and what qcontrol finds here is reported to it.
- Nothing to do; Status shows what is reported, and Connect lets you disconnect if this is not your machine.
Actions Open Status · Open Connect · the same type carries "sign in again" (the credential expired) and "HQ disconnected this machine"
E5 · censure · required · later
From HQ · Dana Whitfield · verbatim: true · Why "Written by Dana Whitfield at HQ and shown as sent; Qpoint does not edit censures"
Subject From HQ: stop using a personal ChatGPT account for work
- Over the last two weeks, work documents were pasted into a personal ChatGPT account from this machine.
- Company data in a personal account sits outside our agreement with OpenAI and cannot be recalled.
- Stop, use the company's Claude for Work account instead, and acknowledge this notice.
Actions Acknowledge (added by the app) · Contact IT · the "Required" pill with "1 of 3 this month"
E6 · responsibility · attention · requires acknowledgement · later
From HQ · Dana Whitfield · Why "Dana Whitfield at HQ assigned this to you" Subject You now own the Jira MCP server
- HQ made you the owner of the Jira MCP server the Platform team uses, from today.
- The owner is asked before the server is blocked and reviews its tool list when it changes.
- Acknowledge to take it on, or reply if this should be someone else.
Actions Acknowledge · Reply · See the server · Links the MCP server's page
E7 · training-assignment (c133) · attention · later
From HQ (policy Secrets in prompts) · Why "Policy Secrets in prompts assigns this training after two blocked pastes" Subject A three-minute training: secrets in prompts
- Twice this week a credential was pasted into an AI tool from this machine, and both pastes were blocked.
- A three-minute training shows what to paste instead and how to get a key rotated when one slips.
- Take it when you have a moment; completion goes into the audit log as proof of correction.
Actions Start training · Links training T-SECRETS-1, findings F-2291-0412 and F-2291-0415 · ends as done on training-completed
E8 · diagnostics-request · attention · later
From HQ · Dana Whitfield · Why "Dana Whitfield at HQ asked, about the slow starts you reported on 8 November" Subject HQ asks for diagnostics from this machine
- Dana at HQ asked for this machine's qcontrol logs and effective configuration from the last 24 hours, to look into the slow starts you reported.
- The package holds qcontrol's own logs and settings and nothing of your own files or messages; you can read it before it goes.
- Review and send, or decline; Dana is told either way.
Actions Review and send · Decline · ends as done on diagnostics-sent (sent or declined)
E9 · sensor-state, degraded · attention · later (the tray is c131 Phase 3)
From qcontrol on this machine · Why "qcontrol lost its tap on Safari at 10:15 and it has stayed lost for 5 minutes" Subject qcontrol stopped seeing Safari
- Since 10:15 qcontrol has not seen traffic from Safari on this machine; other apps are still covered.
- Policies cannot act on what is not seen, so Safari is unprotected until this is fixed, and HQ sees the same gap.
- Quit and reopen Safari; if the Status line stays amber, contact IT from here.
Actions Open Status · Contact IT · ends as done when the tap returns
E10 · contact-it, the citizen's reply to E1 · later with E1
From Mark Parson · MBP-2291 · To the plane · in_reply_to E1 · the person's own words under three prompts; R2 does not bind the person
Subject Re: A paste into ChatGPT was blocked
- What were you doing? Pasting a config file to get help with a deploy script; I did not see the key in it.
- What do you need? A way to share config files safely, or an exception for this project.
- Anything else? The key belongs to a test account.
Attached automatically the policy, the finding, this machine and the time · lands in the operator's inbox as a reply on E1's thread
Two more, for the operator's inbox (not among the ten)
grant-notice (c136) · from the plane · to the operator · info · Subject Qpoint has access to this plane until Thursday 14:00 · A grant you approved opened at 09:00: fix, operate level, scoped to the ClickHouse layer, for Priya Nair at Qpoint. Every action under it carries grant G-142 in the audit log, and you can revoke it at any moment. Nothing to do; a summary arrives when it closes. · Actions See the grant · Revoke
reply, Dana's answer to E10 · from HQ · Dana Whitfield · to the person · tray · Thanks, that explains it. From today, config files in the deploy repo are excepted for the Platform team. If a key slips again you will still see a notice, and that is all that happens.
11. Voice check
Checked on 2026-11-09 with the brand MCP's check-brand-voice tool (this session had it): E1, E2 and E9 under the errors context, the rest under productUI. Twelve texts (the ten plus the two operator-side ones), zero violations across the 12 to 13 rules each (em dashes, rule-of-three patterns, false oppositions, AI-favoured vocabulary, superlatives, exclamation points in UI, and the rest). Two hand rules the tool does not check, applied while writing: no severity word stronger than "attention" or "required" (R4), and each body answers the three questions in order (R2). The lint the operator's send form runs (§9) is the same tool over the three sentences.
12. What lands where
../qnative,typescrate:messages.rswithMessage,MessageType,Severity,Sender,Recipient,Action,State; with c131 Phase 1, written from this document, serde names as the wire shape above. The inbox store (SQLite) and the OS plumbing follow the same shapes.- The plane (c134 Phase 2):
types/api/messages.ts, themessagesresource (list, send, recall, reply, state changes), the three settings (sender_label,required_on_notices,required_soft_limit), the audit events in §9. - The design site: the lab mock (Phase 2) renders these ten across the five states from
app/data/messaging-fixture.ts.