Adding an alert (one command, one file, no conflicts)
A new kind of alert is one new file, created by `npm run alert:new`. Nothing shared is edited, so two people adding two alerts never touch the same file, and the app finds the new alert by itself.
What it is
Every alert type Omniscio can raise is declared once, in its own definition file, inside its family's folder:
src/shared/alert-definitions/<family>/<name>.alert.ts
The file's default export is defineAlert({ ... }). That single object is the whole decision about the type: the deduplication key that identifies it, the title a person sees in Settings, the category it is filed under, the hub it lands in, how a card of this type looks, which on-screen surfaces it uses, which off-screen channels it may reach, and which crew owns it.
Discovery is automatic in all three places the code runs. In a Vite build — the app's main process, the window and the test runner — the files are found by a build-time glob. Under plain Node, which is how the scripts and generators run, the folder is read directly. No index, barrel or registry names the files, which is exactly why adding one is a zero-conflict job.
The old way is still present but frozen: a central alert-type registry holds every type that has not migrated yet. Adding a new alert there is refused by a build guard. Moving an existing type into its own file is a separate change that alters nothing a person sees.
Where to find it
The command
npm run alert:new -- --family <folder> --name <file> --title "Title a person reads"
Run it with no other flags and it prompts for anything missing. Every flag is optional except --family and --name; the title derives from the name when you leave it out.
| Flag | What it decides |
|---|---|
--family |
the folder the file goes in, kebab-case (cloud-fleet, dictation, …) |
--name |
the file name, kebab-case (disk-almost-full → disk-almost-full.alert.ts) |
--key |
the deduplication key; defaults to the name, and to <name>: in prefix mode |
--title |
the name in Settings; defaults to the name in sentence case |
--category |
one of the closed category list; defaults to product |
--hub |
alerts or omniscio; defaults to alerts |
--style |
info, success, warning, critical or question; defaults to info |
--icon |
a lucide component name in PascalCase, such as TriangleAlert |
--match-mode |
exact (default), prefix for a key family, or segment |
--surfaces |
comma-separated; inbox is always present |
--channels |
comma-separated; push is the page to the owner's phone |
--owner-crew |
the kebab-case role slug of the crew that owns the type |
The command writes one file and prints its path. It refuses, before writing anything, when:
- the file already exists — a new alert is a new file, so edit that one deliberately instead;
- the key is already declared anywhere, in the central registry or in any definition file;
- a value is outside a closed vocabulary, with a message that names the field and the allowed values.
The file it writes
import { defineAlert } from '../define-alert'
export const DISK_ALMOST_FULL_DEDUP_KEY = 'disk-almost-full'
export default defineAlert({
key: DISK_ALMOST_FULL_DEDUP_KEY,
title: 'Disk almost full',
category: 'system-health',
style: 'warning',
hub: 'alerts',
icon: 'TriangleAlert'
})
A prefix family exports *_DEDUP_PREFIX and passes matchMode: 'prefix', so one declaration covers every key that starts with it.
How it behaves
What happens after the file exists
Nothing else has to happen. The next build, test run or script picks the file up, and the type joins the same list every reader already consults — the Settings list, the hub a card lands in, the mute switches, the audience rules and the routing.
A key declared twice — once in a definition file and once in the central registry — keeps the definition and fails the build, so a half-finished move can never make behaviour depend on which list was read first.
The guards that keep it one file
Three checks fail the build rather than warn:
- A new alert never edits an existing definition file. Each file present when the foundation landed is frozen with the exact keys it declares; adding a second alert to one of them is refused, and a deliberately removed file deletes its row in the same commit.
- A new alert never adds a row to the central registry. The registry's keys are frozen too; a key it does not already know must arrive as a definition file.
- One bad file never hides the rest. Every file loads inside its own try; a failure is recorded and fails the build, while the app keeps running so a card of that type still arrives rather than vanishing.
Raising a card for the new type
POST /alert accepts the same declaration fields through the request schema, so an agent can state which type its card belongs to while it raises it:
curl -s -X POST http://127.0.0.1:19519/alert \
-H "Authorization: Bearer $AMC_CLI_TOKEN" \
-H "X-AMC-Source-Session-Id: $AMC_SESSION_ID" \
-H 'Content-Type: application/json' \
--data '{"title":"Disk almost full","contentType":"text","text":"Free space is under 5 GB.","definition":{"key":"disk-almost-full","title":"Disk almost full","category":"system-health","style":"warning"}}'
The block is checked, never stored: its key must resolve to a declared alert type, its dedupKey must belong to that type, and any field it states must agree with the declaration. A contradiction is refused with a sentence naming the field. Callers that send no definition block behave exactly as before, and a field the body does not accept is refused as unknown field "<name>" rather than a schema dump.
Two fields are deliberately not accepted on the wire. actions is a button whose handler is code the app ships, and lifecycle is derived from behaviour, so neither can be declared by a caller.
For agents
- The whole job is
npm run alert:new -- --family <family> --name <name> --title "<title>", then commit the one new file. Do not add a line to any registry, barrel or index. - The answer to "how many existing files must an agent edit to add an alert" is zero. The scaffold writes one file and the discovery picks it up; the only other change is whatever producer raises the card, which is the agent's own code.
- Before writing, check the key is free. The command does it for you; if you are writing the file by hand,
npm run alert:newwith the same key is the check. - When a promotion is needed, the definition's
title,category,style,hub,surfacesandchannelsare the only places those decisions live. Never duplicate them in a producer. - Read the contract before changing an invariant. The rules behind the file shape, the load behaviour and the guards are in the alert-definition contract named below.
Related
- Inbox alerts — what an alert is and how one reaches you.
- Inbox hubs — which pile an alert lands in.
- Alert session hub — naming a session or crew per alert type.
Last verified 2026-10-19