Omniscio documentation
Browse all documentation
  1. Getting Started19
  2. Sessions & Agents134
  3. Inbox & Notifications69
  4. Projects & Tasks100
  5. Automation & Scheduling76
  6. Knowledge & Memory28
  7. AI Features72
  8. Integrations114
  9. Plugins & Marketplace42
  10. Cloud & Teams52
  11. Settings & Customization79
  12. Account & Billing28
  13. Troubleshooting81
  14. CLI & API Reference28
  15. Legal & Policies5
  16. Uncategorised17

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:

  1. 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.
  2. 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.
  3. 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:new with the same key is the check.
  • When a promotion is needed, the definition's title, category, style, hub, surfaces and channels are 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

Last verified 2026-10-19