@checkstack/incident-backend 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,601 @@
1
+ /**
2
+ * Incident triggers + actions registered with the Automation platform.
3
+ *
4
+ * Triggers re-expose the existing incident hooks as automation entry
5
+ * points; actions wrap the existing `IncidentService` methods so
6
+ * operators can compose them into automation flows (e.g. "when an
7
+ * incident is created, file a Jira ticket and post an update").
8
+ *
9
+ * Each trigger declares a `contextKey` extractor returning the
10
+ * `incidentId` — the dispatch engine uses it to scope artifact lookups
11
+ * and to match `wait_for_trigger` waits against the same incident.
12
+ */
13
+ import { z } from "zod";
14
+ import { Versioned } from "@checkstack/backend-api";
15
+ import type {
16
+ ActionDefinition,
17
+ ArtifactTypeDefinition,
18
+ EntityHandle,
19
+ TriggerDefinition,
20
+ } from "@checkstack/automation-backend";
21
+ import { makeEntityDrivenTriggerSetup } from "@checkstack/automation-backend";
22
+ import {
23
+ IncidentSeverityEnum,
24
+ IncidentStatusEnum,
25
+ } from "@checkstack/incident-common";
26
+
27
+ import type { IncidentService } from "./service";
28
+ import {
29
+ toIncidentEntityState,
30
+ writeIncidentEntity,
31
+ type IncidentEntityState,
32
+ } from "./incident-entity";
33
+
34
+ // ─── Payload schemas — match the hook payloads exactly ─────────────────
35
+
36
+ // NOTE (reactive entity payload mapper): the reactive `incident` entity state
37
+ // is only `{ status, severity, systemIds }`. The descriptive fields below
38
+ // (`title`, `description`, `createdAt`, `resolvedAt`) are NOT derivable from an
39
+ // entity change, so they are OPTIONAL — the entity-driven `trigger.payload`
40
+ // (see `incidentChangeToPayload`) carries `incidentId` / `systemIds` /
41
+ // `severity` / `status` / `statusChange` only. They stay in the schema for
42
+ // editor introspection / forward compatibility.
43
+ const incidentCreatedPayloadSchema = z.object({
44
+ incidentId: z.string(),
45
+ systemIds: z.array(z.string()),
46
+ title: z.string().optional(),
47
+ description: z.string().optional(),
48
+ severity: IncidentSeverityEnum,
49
+ status: IncidentStatusEnum,
50
+ createdAt: z.string().optional(),
51
+ });
52
+
53
+ const incidentUpdatedPayloadSchema = z.object({
54
+ incidentId: z.string(),
55
+ systemIds: z.array(z.string()),
56
+ title: z.string().optional(),
57
+ description: z.string().optional(),
58
+ severity: IncidentSeverityEnum,
59
+ status: IncidentStatusEnum,
60
+ statusChange: IncidentStatusEnum.optional(),
61
+ });
62
+
63
+ const incidentResolvedPayloadSchema = z.object({
64
+ incidentId: z.string(),
65
+ systemIds: z.array(z.string()),
66
+ title: z.string().optional(),
67
+ severity: IncidentSeverityEnum,
68
+ resolvedAt: z.string().optional(),
69
+ });
70
+
71
+ // ─── Triggers ──────────────────────────────────────────────────────────
72
+
73
+ // These triggers are ENTITY-DRIVEN (§10.1): the `incident` entity's change
74
+ // deriver fires `incident.created/.updated/.resolved` via Stage-1 routing,
75
+ // so they no longer subscribe to a hook. A no-op `setup` keeps them in the
76
+ // editor's trigger catalog without re-introducing a hook.
77
+ export const incidentCreatedTrigger: TriggerDefinition<
78
+ z.infer<typeof incidentCreatedPayloadSchema>
79
+ > = {
80
+ id: "created",
81
+ displayName: "Incident Created",
82
+ description: "Fires when a new incident is created",
83
+ category: "Incidents",
84
+ icon: "CircleAlert",
85
+ payloadSchema: incidentCreatedPayloadSchema,
86
+ setup: makeEntityDrivenTriggerSetup<
87
+ z.infer<typeof incidentCreatedPayloadSchema>
88
+ >(),
89
+ contextKey: (p) => p.incidentId,
90
+ };
91
+
92
+ export const incidentUpdatedTrigger: TriggerDefinition<
93
+ z.infer<typeof incidentUpdatedPayloadSchema>
94
+ > = {
95
+ id: "updated",
96
+ displayName: "Incident Updated",
97
+ description:
98
+ "Fires when an incident's reactive state changes (status, severity, or affected systems). A comment-only update does not fire this trigger.",
99
+ category: "Incidents",
100
+ icon: "CircleAlert",
101
+ payloadSchema: incidentUpdatedPayloadSchema,
102
+ setup: makeEntityDrivenTriggerSetup<
103
+ z.infer<typeof incidentUpdatedPayloadSchema>
104
+ >(),
105
+ contextKey: (p) => p.incidentId,
106
+ };
107
+
108
+ export const incidentResolvedTrigger: TriggerDefinition<
109
+ z.infer<typeof incidentResolvedPayloadSchema>
110
+ > = {
111
+ id: "resolved",
112
+ displayName: "Incident Resolved",
113
+ description: "Fires when an incident is marked as resolved",
114
+ category: "Incidents",
115
+ icon: "CircleCheck",
116
+ payloadSchema: incidentResolvedPayloadSchema,
117
+ setup: makeEntityDrivenTriggerSetup<
118
+ z.infer<typeof incidentResolvedPayloadSchema>
119
+ >(),
120
+ contextKey: (p) => p.incidentId,
121
+ };
122
+
123
+ /**
124
+ * All incident triggers as a heterogeneous list. Typed as
125
+ * `TriggerDefinition<unknown>[]` so the array can be iterated in the
126
+ * plugin entry without TypeScript collapsing the union to a single
127
+ * payload shape.
128
+ */
129
+ export const incidentTriggers: TriggerDefinition<unknown>[] = [
130
+ incidentCreatedTrigger as TriggerDefinition<unknown>,
131
+ incidentUpdatedTrigger as TriggerDefinition<unknown>,
132
+ incidentResolvedTrigger as TriggerDefinition<unknown>,
133
+ ];
134
+
135
+ // ─── incident artifact type ────────────────────────────────────────────
136
+
137
+ /**
138
+ * The `incident` artifact represents an incident opened by an upstream
139
+ * action (e.g. `incident.create`). Downstream actions in the same run
140
+ * (`incident.resolve`, `incident.add_update`, `incident.update_status`)
141
+ * can consume it to act on that incident without the operator repeating
142
+ * the id — the open-then-wait-then-resolve flow the default auto-incident
143
+ * automations rely on.
144
+ */
145
+ const incidentDataSchema = z.object({
146
+ incidentId: z.string(),
147
+ status: z.string(),
148
+ severity: z.string(),
149
+ systemIds: z.array(z.string()),
150
+ });
151
+
152
+ export const incidentArtifactType: ArtifactTypeDefinition<
153
+ z.infer<typeof incidentDataSchema>
154
+ > = {
155
+ id: "incident",
156
+ displayName: "Incident",
157
+ description: "An incident opened by an upstream automation action",
158
+ schema: incidentDataSchema,
159
+ };
160
+
161
+ // ─── Action configs ────────────────────────────────────────────────────
162
+
163
+ const incidentCreateConfigSchema = z.object({
164
+ title: z.string().min(1),
165
+ description: z.string().optional(),
166
+ severity: IncidentSeverityEnum,
167
+ systemIds: z.array(z.string()).min(1),
168
+ initialMessage: z.string().optional(),
169
+ suppressNotifications: z.boolean().optional().default(false),
170
+ /**
171
+ * When true, reuse an existing OPEN incident on the first target
172
+ * system instead of opening a duplicate (the old auto-incident
173
+ * `findActiveAutoIncident(systemId)` semantic). The reused incident is
174
+ * returned as the produced `incident` artifact so downstream
175
+ * resolve/update actions still work. Default false — existing and
176
+ * custom automations always create.
177
+ */
178
+ dedupe_open_for_system: z.boolean().optional().default(false),
179
+ });
180
+
181
+ // `incidentId` is optional on the close/update actions: when omitted the
182
+ // action falls back to the upstream `incident` artifact in run scope
183
+ // (config takes priority, else artifact). Mirrors Jira's resolveIssueKey.
184
+ const incidentResolveConfigSchema = z.object({
185
+ incidentId: z
186
+ .string()
187
+ .optional()
188
+ .describe("Defaults to the upstream incident artifact in the run."),
189
+ message: z.string().optional(),
190
+ });
191
+
192
+ const incidentAddUpdateConfigSchema = z.object({
193
+ incidentId: z
194
+ .string()
195
+ .optional()
196
+ .describe("Defaults to the upstream incident artifact in the run."),
197
+ message: z.string().min(1),
198
+ statusChange: IncidentStatusEnum.optional(),
199
+ });
200
+
201
+ const incidentUpdateStatusConfigSchema = z.object({
202
+ incidentId: z
203
+ .string()
204
+ .optional()
205
+ .describe("Defaults to the upstream incident artifact in the run."),
206
+ status: IncidentStatusEnum,
207
+ /**
208
+ * Optional accompanying message. Defaults to a generic transition note
209
+ * so the audit trail is never empty.
210
+ */
211
+ message: z.string().optional(),
212
+ });
213
+
214
+ // ─── Action artifact shapes ────────────────────────────────────────────
215
+
216
+ interface IncidentArtifact {
217
+ incidentId: string;
218
+ status: string;
219
+ severity: string;
220
+ systemIds: string[];
221
+ }
222
+
223
+ interface IncidentUpdateArtifact {
224
+ updateId: string;
225
+ incidentId: string;
226
+ }
227
+
228
+ /**
229
+ * Resolve an incident id from explicit config or fall back to the
230
+ * upstream `incident` artifact in the run scope (config takes priority).
231
+ * Mirrors Jira's `resolveIssueKey` pattern.
232
+ */
233
+ function resolveIncidentId(
234
+ configId: string | undefined,
235
+ consumed: Record<string, unknown>,
236
+ ): string | undefined {
237
+ if (configId && configId.trim().length > 0) return configId;
238
+ const incident = consumed["incident"];
239
+ if (
240
+ incident &&
241
+ typeof incident === "object" &&
242
+ "incidentId" in incident &&
243
+ typeof (incident as { incidentId: unknown }).incidentId === "string"
244
+ ) {
245
+ return (incident as { incidentId: string }).incidentId;
246
+ }
247
+ return;
248
+ }
249
+
250
+ // ─── Actions ───────────────────────────────────────────────────────────
251
+
252
+ export interface IncidentActionDeps {
253
+ service: IncidentService;
254
+ /**
255
+ * Resolver for the reactive `incident` entity (§10.1). When present, the
256
+ * `incident.create` action drives its write through `handle.mutate` so an
257
+ * action-created incident becomes reactive like any other (6(a)). Undefined
258
+ * in tests / before the handle is wired — the create still runs, just
259
+ * non-reactively.
260
+ */
261
+ getIncidentEntity?: () => EntityHandle<IncidentEntityState> | undefined;
262
+ }
263
+
264
+ export function createIncidentActions(
265
+ deps: IncidentActionDeps,
266
+ ): ActionDefinition<unknown, unknown>[] {
267
+ const { service, getIncidentEntity } = deps;
268
+
269
+ const createAction: ActionDefinition<
270
+ z.infer<typeof incidentCreateConfigSchema>,
271
+ IncidentArtifact
272
+ > = {
273
+ id: "create",
274
+ displayName: "Create Incident",
275
+ description: "Open a new incident affecting one or more systems",
276
+ category: "Incidents",
277
+ icon: "CircleAlert",
278
+ config: new Versioned({
279
+ version: 1,
280
+ schema: incidentCreateConfigSchema,
281
+ }),
282
+ produces: "incident",
283
+ execute: async ({ config, logger, runId }) => {
284
+ const createInput = {
285
+ title: config.title,
286
+ description: config.description,
287
+ severity: config.severity,
288
+ systemIds: config.systemIds,
289
+ initialMessage: config.initialMessage,
290
+ suppressNotifications: config.suppressNotifications,
291
+ };
292
+ const handle = getIncidentEntity?.();
293
+
294
+ // Per-system dedup (opt-in): if an open incident already exists on
295
+ // the first target system, reuse it instead of opening a duplicate.
296
+ // Reproduces the old auto-incident `findActiveAutoIncident` semantic
297
+ // and keeps at most one open auto-incident per system across all the
298
+ // default sustained/flapping automations. The check + create are
299
+ // serialized per system inside the service (advisory lock), so two
300
+ // concurrent triggers (e.g. sustained + flapping) for the same system
301
+ // can't both find none and both create.
302
+ //
303
+ // 6(a): an action-created incident is now reactive — the create runs
304
+ // through `handle.mutate`, so the deriver fires `incident.created` and
305
+ // automations can trigger on it. A REUSED incident drives NO entity
306
+ // write (it already exists and is unchanged, so no duplicate
307
+ // `incident.created`). The id is generated up front so a real create's
308
+ // `prev` snapshot reads the not-yet-existing row as absent.
309
+ if (config.dedupe_open_for_system) {
310
+ const newId = crypto.randomUUID();
311
+ // The create (when not reused) runs INSIDE the dedup lock via
312
+ // `onCreate`, driven through `handle.mutate` so `prev` is snapshotted
313
+ // (absent → null) BEFORE the insert and `incident.created` fires. A
314
+ // REUSE never calls `onCreate`, so no entity write and no duplicate
315
+ // `incident.created` — matching the pre-reactive dedupe behavior.
316
+ const { incident, reused } =
317
+ await service.createIncidentDedupedForSystem(
318
+ createInput,
319
+ config.systemIds[0]!,
320
+ undefined,
321
+ newId,
322
+ async (create) => {
323
+ let created!: Awaited<ReturnType<typeof create>>;
324
+ await writeIncidentEntity({
325
+ handle,
326
+ incidentId: newId,
327
+ opts: { runId },
328
+ apply: async () => {
329
+ created = await create();
330
+ return toIncidentEntityState(created);
331
+ },
332
+ });
333
+ return created;
334
+ },
335
+ );
336
+ if (reused) {
337
+ logger.info(
338
+ `Automation reused open incident ${incident.id} for system ${config.systemIds[0]} (dedupe)`,
339
+ );
340
+ } else {
341
+ logger.info(`Automation created incident ${incident.id}`);
342
+ }
343
+ return {
344
+ success: true,
345
+ externalId: incident.id,
346
+ artifact: {
347
+ incidentId: incident.id,
348
+ status: incident.status,
349
+ severity: incident.severity,
350
+ systemIds: incident.systemIds,
351
+ },
352
+ };
353
+ }
354
+
355
+ const newId = crypto.randomUUID();
356
+ let incident!: Awaited<ReturnType<typeof service.createIncident>>;
357
+ await writeIncidentEntity({
358
+ handle,
359
+ incidentId: newId,
360
+ opts: { runId },
361
+ apply: async () => {
362
+ incident = await service.createIncident(createInput, undefined, newId);
363
+ return toIncidentEntityState(incident);
364
+ },
365
+ });
366
+ logger.info(`Automation created incident ${incident.id}`);
367
+ return {
368
+ success: true,
369
+ externalId: incident.id,
370
+ artifact: {
371
+ incidentId: incident.id,
372
+ status: incident.status,
373
+ severity: incident.severity,
374
+ systemIds: incident.systemIds,
375
+ },
376
+ };
377
+ },
378
+ };
379
+
380
+ const resolveAction: ActionDefinition<
381
+ z.infer<typeof incidentResolveConfigSchema>,
382
+ IncidentArtifact
383
+ > = {
384
+ id: "resolve",
385
+ displayName: "Resolve Incident",
386
+ description: "Mark an existing incident as resolved",
387
+ category: "Incidents",
388
+ icon: "CircleCheck",
389
+ config: new Versioned({
390
+ version: 1,
391
+ schema: incidentResolveConfigSchema,
392
+ }),
393
+ consumes: ["incident"],
394
+ execute: async ({ config, consumedArtifacts, logger, runId }) => {
395
+ const incidentId = resolveIncidentId(config.incidentId, consumedArtifacts);
396
+ if (!incidentId) {
397
+ return {
398
+ success: false,
399
+ error: "No incidentId given and no upstream incident artifact found",
400
+ };
401
+ }
402
+ // Guard existence BEFORE the driven write so `apply` never throws on a
403
+ // missing incident (a graceful action failure, not a run error).
404
+ if (!(await service.getIncident(incidentId))) {
405
+ return {
406
+ success: false,
407
+ error: `Incident ${incidentId} not found`,
408
+ };
409
+ }
410
+ // 6(a): route the resolve through the reactive `incident` entity (like
411
+ // the RPC router) so the status flip appends an `entity_transitions` row,
412
+ // emits `ENTITY_CHANGED` (waking any `wait_until`), and fires the
413
+ // `incident.resolved` deriver. `apply` performs the REAL resolve and
414
+ // re-reads the post-write state inside the mutate window. `opts.runId`
415
+ // masks any run-resolved secret that lands in the reactive state.
416
+ const captured: {
417
+ incident: NonNullable<
418
+ Awaited<ReturnType<typeof service.resolveIncident>>
419
+ > | null;
420
+ } = { incident: null };
421
+ await writeIncidentEntity({
422
+ handle: getIncidentEntity?.(),
423
+ incidentId,
424
+ opts: { runId },
425
+ apply: async () => {
426
+ const resolved = await service.resolveIncident(
427
+ incidentId,
428
+ config.message,
429
+ );
430
+ if (!resolved) {
431
+ throw new Error(`Incident ${incidentId} not found`);
432
+ }
433
+ captured.incident = resolved;
434
+ return toIncidentEntityState(resolved);
435
+ },
436
+ });
437
+ const incident = captured.incident;
438
+ if (!incident) {
439
+ return {
440
+ success: false,
441
+ error: `Incident ${incidentId} not found`,
442
+ };
443
+ }
444
+ logger.info(`Automation resolved incident ${incident.id}`);
445
+ return {
446
+ success: true,
447
+ externalId: incident.id,
448
+ artifact: {
449
+ incidentId: incident.id,
450
+ status: incident.status,
451
+ severity: incident.severity,
452
+ systemIds: incident.systemIds,
453
+ },
454
+ };
455
+ },
456
+ };
457
+
458
+ const addUpdateAction: ActionDefinition<
459
+ z.infer<typeof incidentAddUpdateConfigSchema>,
460
+ IncidentUpdateArtifact
461
+ > = {
462
+ id: "add_update",
463
+ displayName: "Add Incident Update",
464
+ description: "Post a status update to an existing incident",
465
+ category: "Incidents",
466
+ icon: "MessageSquare",
467
+ config: new Versioned({
468
+ version: 1,
469
+ schema: incidentAddUpdateConfigSchema,
470
+ }),
471
+ consumes: ["incident"],
472
+ execute: async ({ config, consumedArtifacts, logger, runId }) => {
473
+ const incidentId = resolveIncidentId(config.incidentId, consumedArtifacts);
474
+ if (!incidentId) {
475
+ return {
476
+ success: false,
477
+ error: "No incidentId given and no upstream incident artifact found",
478
+ };
479
+ }
480
+ // 6(a): route the update through the reactive `incident` entity (like the
481
+ // RPC router). `apply` posts the update row + (optionally) flips status,
482
+ // then re-reads the post-write reactive state. When `statusChange` flips
483
+ // the status to resolved the deriver fires `incident.resolved`; any other
484
+ // status change fires `incident.updated`; an update with no status change
485
+ // is a no-op diff (no event). `opts.runId` masks run-resolved secrets.
486
+ const captured: {
487
+ update: Awaited<ReturnType<typeof service.addUpdate>> | null;
488
+ } = { update: null };
489
+ await writeIncidentEntity({
490
+ handle: getIncidentEntity?.(),
491
+ incidentId,
492
+ opts: { runId },
493
+ apply: async () => {
494
+ captured.update = await service.addUpdate({
495
+ incidentId,
496
+ message: config.message,
497
+ statusChange: config.statusChange,
498
+ });
499
+ const incident = await service.getIncident(incidentId);
500
+ if (!incident) {
501
+ throw new Error(`Incident ${incidentId} not found`);
502
+ }
503
+ return toIncidentEntityState(incident);
504
+ },
505
+ });
506
+ const update = captured.update;
507
+ if (!update) {
508
+ return {
509
+ success: false,
510
+ error: `Incident ${incidentId} not found`,
511
+ };
512
+ }
513
+ logger.info(
514
+ `Automation added update ${update.id} to incident ${incidentId}`,
515
+ );
516
+ return {
517
+ success: true,
518
+ externalId: update.id,
519
+ artifact: {
520
+ updateId: update.id,
521
+ incidentId: update.incidentId,
522
+ },
523
+ };
524
+ },
525
+ };
526
+
527
+ const updateStatusAction: ActionDefinition<
528
+ z.infer<typeof incidentUpdateStatusConfigSchema>,
529
+ IncidentUpdateArtifact
530
+ > = {
531
+ id: "update_status",
532
+ displayName: "Update Incident Status",
533
+ description: "Change an incident's status and post an audit update",
534
+ category: "Incidents",
535
+ icon: "Activity",
536
+ config: new Versioned({
537
+ version: 1,
538
+ schema: incidentUpdateStatusConfigSchema,
539
+ }),
540
+ consumes: ["incident"],
541
+ execute: async ({ config, consumedArtifacts, logger, runId }) => {
542
+ const incidentId = resolveIncidentId(config.incidentId, consumedArtifacts);
543
+ if (!incidentId) {
544
+ return {
545
+ success: false,
546
+ error: "No incidentId given and no upstream incident artifact found",
547
+ };
548
+ }
549
+ // 6(a): route the status flip through the reactive `incident` entity
550
+ // (like the RPC router) so it appends an `entity_transitions` row, emits
551
+ // `ENTITY_CHANGED` (waking any `wait_until`), and fires `incident.resolved`
552
+ // (→ resolved) or `incident.updated`. `apply` performs the REAL write +
553
+ // re-reads post-write state. `opts.runId` masks run-resolved secrets.
554
+ const captured: {
555
+ update: Awaited<ReturnType<typeof service.addUpdate>> | null;
556
+ } = { update: null };
557
+ await writeIncidentEntity({
558
+ handle: getIncidentEntity?.(),
559
+ incidentId,
560
+ opts: { runId },
561
+ apply: async () => {
562
+ captured.update = await service.addUpdate({
563
+ incidentId,
564
+ message: config.message ?? `Status changed to ${config.status}`,
565
+ statusChange: config.status,
566
+ });
567
+ const incident = await service.getIncident(incidentId);
568
+ if (!incident) {
569
+ throw new Error(`Incident ${incidentId} not found`);
570
+ }
571
+ return toIncidentEntityState(incident);
572
+ },
573
+ });
574
+ const update = captured.update;
575
+ if (!update) {
576
+ return {
577
+ success: false,
578
+ error: `Incident ${incidentId} not found`,
579
+ };
580
+ }
581
+ logger.info(
582
+ `Automation set incident ${incidentId} status → ${config.status}`,
583
+ );
584
+ return {
585
+ success: true,
586
+ externalId: update.id,
587
+ artifact: {
588
+ updateId: update.id,
589
+ incidentId: update.incidentId,
590
+ },
591
+ };
592
+ },
593
+ };
594
+
595
+ return [
596
+ createAction as ActionDefinition<unknown, unknown>,
597
+ resolveAction as ActionDefinition<unknown, unknown>,
598
+ addUpdateAction as ActionDefinition<unknown, unknown>,
599
+ updateStatusAction as ActionDefinition<unknown, unknown>,
600
+ ];
601
+ }
package/src/hooks.ts CHANGED
@@ -1,47 +1,11 @@
1
- import { createHook } from "@checkstack/backend-api";
2
-
3
1
  /**
4
- * Incident hooks for cross-plugin communication.
5
- * Other plugins can subscribe to these hooks to react to incident lifecycle events.
2
+ * Incident cross-plugin hooks.
3
+ *
4
+ * The `incident.created` / `.updated` / `.resolved` hooks were removed in
5
+ * Phase 4 (§10.1): incidents are now the reactive `incident` entity, whose
6
+ * change deriver fires the matching `incident.created` / `.updated` /
7
+ * `.resolved` trigger events through Stage-1 routing. No cross-plugin hook
8
+ * remains, so this object is intentionally empty (kept for the stable
9
+ * `export { incidentHooks }` surface).
6
10
  */
7
- export const incidentHooks = {
8
- /**
9
- * Emitted when a new incident is created.
10
- * Plugins can subscribe (work-queue mode) to react to new incidents.
11
- */
12
- incidentCreated: createHook<{
13
- incidentId: string;
14
- systemIds: string[];
15
- title: string;
16
- description?: string;
17
- severity: string;
18
- status: string;
19
- createdAt: string;
20
- }>("incident.created"),
21
-
22
- /**
23
- * Emitted when an incident is updated (info or status change).
24
- * Plugins can subscribe (work-queue mode) to react to updates.
25
- */
26
- incidentUpdated: createHook<{
27
- incidentId: string;
28
- systemIds: string[];
29
- title: string;
30
- description?: string;
31
- severity: string;
32
- status: string;
33
- statusChange?: string;
34
- }>("incident.updated"),
35
-
36
- /**
37
- * Emitted when an incident is resolved.
38
- * Plugins can subscribe to clean up or log incident resolutions.
39
- */
40
- incidentResolved: createHook<{
41
- incidentId: string;
42
- systemIds: string[];
43
- title: string;
44
- severity: string;
45
- resolvedAt: string;
46
- }>("incident.resolved"),
47
- } as const;
11
+ export const incidentHooks = {} as const;