@shipeasy/openapi 3.2.0 → 3.3.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.
Files changed (39) hide show
  1. package/README.md +1 -1
  2. package/dist/{chunk-RTN572TI.js → chunk-75Z4SQOM.js} +75 -1
  3. package/dist/chunk-75Z4SQOM.js.map +1 -0
  4. package/dist/{chunk-DW33MG4B.js → chunk-ZYK74ZIW.js} +426 -121
  5. package/dist/chunk-ZYK74ZIW.js.map +1 -0
  6. package/dist/{client-BPVn4hMC.d.ts → client-DAGqRpF1.d.ts} +1362 -238
  7. package/dist/client.d.ts +1 -1
  8. package/dist/client.js +19 -1
  9. package/dist/custom.d.ts +5 -5
  10. package/dist/custom.js +128 -84
  11. package/dist/custom.js.map +1 -1
  12. package/dist/index.d.ts +2 -2
  13. package/dist/index.js +20 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/schemas.d.ts +1648 -907
  16. package/dist/schemas.js +113 -1
  17. package/openapi-sdk.json +31 -9
  18. package/openapi.json +4040 -2083
  19. package/openapi.yaml +1775 -408
  20. package/package.json +1 -1
  21. package/spec/components/schemas/alert-rules.yaml +405 -7
  22. package/spec/components/schemas/alerts.yaml +9 -7
  23. package/spec/components/schemas/common.yaml +2 -2
  24. package/spec/components/schemas/connectors.yaml +69 -6
  25. package/spec/components/schemas/events.yaml +20 -9
  26. package/spec/components/schemas/keys.yaml +30 -0
  27. package/spec/components/schemas/metrics.yaml +131 -251
  28. package/spec/components/schemas/ops.yaml +377 -29
  29. package/spec/components/schemas/projects.yaml +18 -0
  30. package/spec/components/schemas/tags.yaml +91 -0
  31. package/spec/openapi.yaml +43 -0
  32. package/spec/paths/alerts.yaml +5 -3
  33. package/spec/paths/i18n.yaml +2 -1
  34. package/spec/paths/keys.yaml +79 -0
  35. package/spec/paths/metrics.yaml +26 -17
  36. package/spec/paths/ops.yaml +388 -11
  37. package/spec/paths/tags.yaml +270 -0
  38. package/dist/chunk-DW33MG4B.js.map +0 -1
  39. package/dist/chunk-RTN572TI.js.map +0 -1
package/openapi.yaml CHANGED
@@ -216,6 +216,24 @@ tags:
216
216
  the AI-write seam the cockpit's detail panel renders: `list` reads what a
217
217
  previous run found, `create` appends a new record, and `update` fills in the
218
218
  `working` run record you were handed (findings, PR, confidence) as you work.
219
+ - name: Questions
220
+ parent: Ops
221
+ description: |-
222
+ Structured questions an agent asks the team about a queue item — the
223
+ `AskUserQuestion` shape: a short `header`, the `question`, 2–4 `options`
224
+ (label + description), and `multiSelect` for checkboxes instead of radios.
225
+ Every question also accepts a free-text "Other" answer. Open questions sit
226
+ at the very top of the item's detail panel and mark the row in the queue.
227
+
228
+ **Asking.** `ask` posts 1–4 questions at once. Several agents (or several
229
+ runs) may have questions open on the same item at the same time.
230
+
231
+ **Getting the answer back.** `delivery: resume` (the default for a run
232
+ launched through a trigger connector) re-fires that same agent on the
233
+ item once every question its run asked is answered, continuing the same
234
+ run record with the answers in the prompt. `delivery: poll` (the default
235
+ for a live, connector-less session such as Claude Code in a terminal)
236
+ fires nothing — call `list` until the status reads `completed`.
219
237
  - name: Projects
220
238
  description: |-
221
239
  Projects: the account-level container every other resource is scoped to.
@@ -258,6 +276,19 @@ tags:
258
276
  Continue) schedule on their own surface — typically a GitHub Actions
259
277
  `schedule:` cron running the platform's headless CLI with the shared
260
278
  trigger prompt.
279
+ - name: Tags
280
+ description: |-
281
+ Tags: free-form, project-scoped labels any item can carry — queue items
282
+ (bugs, feature requests, error/alert tickets), flags, configs, kill
283
+ switches, metrics, tracked errors, alert rules, and experiments all share
284
+ one tag roster.
285
+
286
+ **Identity.** A tag is its name, unique case-insensitively per project. A
287
+ tag is born the first time it's attached — there is no separate create.
288
+
289
+ **Addressing.** An item is named by `entityType` + its handle (an ops
290
+ item's number, a resource's name, or any id). `set` replaces the full tag
291
+ set; `update` adds/removes names incrementally.
261
292
  - name: Connectors
262
293
  description: |-
263
294
  Outbound integrations: the per-project destinations feedback + trigger
@@ -4561,12 +4592,15 @@ paths:
4561
4592
  name: checkouts
4562
4593
  folder: null
4563
4594
  eventName: checkout_completed
4564
- query: count_users(checkout_completed)
4595
+ query: count(checkout_completed)
4565
4596
  queryIr:
4566
- agg:
4567
- kind: count_users
4568
- metric: checkout_completed
4569
- filters: []
4597
+ v: 2
4598
+ expr:
4599
+ node: agg
4600
+ event: checkout_completed
4601
+ agg:
4602
+ kind: count
4603
+ filters: []
4570
4604
  direction: higher_better
4571
4605
  version: 1
4572
4606
  '400':
@@ -4600,7 +4634,7 @@ paths:
4600
4634
 
4601
4635
  **Use cases**
4602
4636
 
4603
- - **Track an event** — `count_users(<event>)` for unique-user counts.
4637
+ - **Track an event** — `count(<event>)` for event counts.
4604
4638
  - **Sum a value** — `sum(<event>, <label>)` for revenue / quantity metrics.
4605
4639
  - **Experiment success metric** — create the metric, then attach its id to an experiment.
4606
4640
  tags:
@@ -4614,13 +4648,13 @@ paths:
4614
4648
  schema:
4615
4649
  $ref: '#/components/schemas/CreateMetricRequest'
4616
4650
  examples:
4617
- countUsers:
4618
- summary: Count unique users (DSL)
4619
- description: Unique users who completed checkout, expressed with the query DSL.
4651
+ countEvents:
4652
+ summary: Count an event (DSL)
4653
+ description: Completed checkouts, expressed with the query DSL.
4620
4654
  value:
4621
4655
  name: checkouts
4622
4656
  event_name: checkout_completed
4623
- query: count_users(checkout_completed)
4657
+ query: count(checkout_completed)
4624
4658
  sumRevenue:
4625
4659
  summary: Sum a value label (DSL)
4626
4660
  description: Total revenue, summing the `amount` property of the `purchase` event.
@@ -4637,10 +4671,13 @@ paths:
4637
4671
  name: checkouts
4638
4672
  event_name: checkout_completed
4639
4673
  query_ir:
4640
- agg:
4641
- kind: count_users
4642
- metric: checkout_completed
4643
- filters: []
4674
+ v: 2
4675
+ expr:
4676
+ node: agg
4677
+ event: checkout_completed
4678
+ agg:
4679
+ kind: count
4680
+ filters: []
4644
4681
  responses:
4645
4682
  '201':
4646
4683
  description: Create a metric
@@ -4721,11 +4758,14 @@ paths:
4721
4758
  eventName: purchase
4722
4759
  query: sum(purchase, amount)
4723
4760
  queryIr:
4724
- agg:
4725
- kind: sum
4726
- metric: purchase
4727
- valueLabel: amount
4728
- filters: []
4761
+ v: 2
4762
+ expr:
4763
+ node: agg
4764
+ event: purchase
4765
+ agg:
4766
+ kind: sum
4767
+ valueLabel: amount
4768
+ filters: []
4729
4769
  direction: higher_better
4730
4770
  winsorizePct: 99
4731
4771
  version: 1
@@ -5431,7 +5471,11 @@ paths:
5431
5471
  operationId: listOpsItems
5432
5472
  summary: List the operational queue
5433
5473
  description: |-
5434
- Returns the unified ops queue (bugs, feature requests, errors, alerts) in work order — highest priority first, oldest first within a priority — so consumers work it top-down. Filter by `type` and/or `status`, and cap with `limit`. Human-gated holding states (items awaiting human sign-off in the dashboard) are never returned by `all`/default status.
5474
+ Returns the unified ops queue (bugs, feature requests, errors, alerts, measurement plans) in work order — highest priority first, oldest first within a priority — so consumers work it top-down. Every filter is applied in SQL before paging, and filters combine with AND. Human-gated holding states (items awaiting human sign-off in the dashboard) are never returned by `all`/default status.
5475
+
5476
+ **Filters:** `type` (one item type), `status` (one lifecycle status, or `unresolved` — everything not yet `resolved`/`wont_fix`), `priority` (one triage priority), `owner` (one person or agent), and `scope=ready` — the items ready to be picked up right now: `open`, with nobody working them (no open run), and either unowned or reopened (sent back to `open` after an earlier run finished).
5477
+
5478
+ **Paging:** `limit` is the page size and `offset` the number of items to skip. The order is stable, so walk the queue with `offset += limit`; a page shorter than `limit` is the last one.
5435
5479
 
5436
5480
  **Use case:** Pull the open queue to triage — e.g. every `bug` still `open` — before working items down one by one.
5437
5481
  tags:
@@ -5441,8 +5485,9 @@ paths:
5441
5485
  - name: type
5442
5486
  in: query
5443
5487
  required: false
5444
- description: Filter by item type (`bug`/`feature_request`/`error`/`alert`), or `all`.
5488
+ description: Filter by item type, or `all` (the default). Every type a returned item can carry is filterable, including the auto-filed ones.
5445
5489
  schema:
5490
+ default: all
5446
5491
  anyOf:
5447
5492
  - type: string
5448
5493
  enum:
@@ -5450,30 +5495,70 @@ paths:
5450
5495
  - feature_request
5451
5496
  - error
5452
5497
  - alert
5453
- description: Queue item type. `bug` and `feature_request` are user-fileable; `error` and `alert` tickets are auto-filed by the platform.
5498
+ - measure_plan
5499
+ description: Queue item type. `bug` and `feature_request` are user-fileable; `error`, `alert`, and `measure_plan` tickets are auto-filed by the platform.
5454
5500
  - type: string
5455
5501
  const: all
5456
5502
  - name: status
5457
5503
  in: query
5458
5504
  required: false
5459
- description: Filter by lifecycle status, or `all`. The human-gated holding state (`pending_approval`) is excluded from `all`/default and returned only when requested as the exact status.
5505
+ description: Filter by one lifecycle status, `unresolved` for every status that is not `resolved` or `wont_fix`, or `all` for every status. Defaults to `all` over HTTP; the CLI and MCP tool default to `unresolved`, so pass `all` there to include resolved and won't-fix items. The human-gated holding state (`pending_approval`) is excluded from `unresolved` and `all`, and returned only when requested as the exact status.
5460
5506
  schema:
5507
+ default: all
5461
5508
  anyOf:
5462
5509
  - $ref: '#/components/schemas/OpsItemStatus'
5510
+ - type: string
5511
+ const: unresolved
5463
5512
  - type: string
5464
5513
  const: all
5514
+ x-cli:
5515
+ default: unresolved
5516
+ x-mcp:
5517
+ default: unresolved
5518
+ - name: priority
5519
+ in: query
5520
+ required: false
5521
+ description: Filter by triage priority, or `all` (the default). Items with no priority set are returned only under `all`.
5522
+ schema:
5523
+ default: all
5524
+ anyOf:
5525
+ - $ref: '#/components/schemas/OpsItemPriority'
5526
+ - type: string
5527
+ const: all
5528
+ - name: scope
5529
+ in: query
5530
+ required: false
5531
+ description: '`ready` narrows to the items ready to be picked up right now: status `open`, no open run (nobody has acked it and is still working), and either unowned (no person and no agent) or reopened (sent back to `open` after an earlier run finished). Leave `status` unset with it. `all` (the default) applies no such narrowing.'
5532
+ schema:
5533
+ type: string
5534
+ enum:
5535
+ - all
5536
+ - ready
5537
+ default: all
5465
5538
  - name: limit
5466
5539
  in: query
5467
5540
  required: false
5468
- description: Max items to return (1–500).
5541
+ description: Page size — max items to return (1–500). A page shorter than `limit` is the last one.
5469
5542
  schema:
5470
5543
  type: integer
5471
5544
  minimum: 1
5472
5545
  maximum: 500
5546
+ default: 200
5547
+ x-mcp:
5548
+ required: true
5549
+ - name: offset
5550
+ in: query
5551
+ required: false
5552
+ description: Number of items to skip before the page starts. Defaults to 0. The order is stable, so `offset += limit` walks the queue page by page.
5553
+ schema:
5554
+ type: integer
5555
+ minimum: 0
5556
+ maximum: 100000
5557
+ default: 0
5473
5558
  - name: owner
5474
5559
  in: query
5475
5560
  required: false
5476
- description: Narrow to items owned by one person OR one agent. Matches a person by `users.id`, email, or display name, and an agent by connector id, display name, or kebab-case handle — e.g. `owner=Claude` or `owner=alice@acme.dev`. Case-insensitive exact match, applied over the returned page.
5561
+ description: Narrow to items owned by one person OR one agent. Matches a person by `users.id`, email, or display name, and an agent by connector id, display name, or kebab-case handle — e.g. `owner=Claude` or `owner=alice@acme.dev`. Case-insensitive exact match.
5477
5562
  schema:
5478
5563
  type: string
5479
5564
  responses:
@@ -5519,6 +5604,8 @@ paths:
5519
5604
  - FORBIDDEN
5520
5605
  x-cli:
5521
5606
  name: list
5607
+ x-mcp:
5608
+ description: 'List the operational queue (bugs, feature requests, errors, alerts, measurement plans) in work order — highest priority first, oldest first within a priority. PAGING: `limit` (page size, 1–500) is REQUIRED — the call fails without it. Start at `offset: 0` and add `limit` to `offset` for the next page; a page shorter than `limit` is the last one. FILTERS (all optional, combined with AND, applied before paging): `type` = bug | feature_request | error | alert | measure_plan; `status` = open | in_progress | investigating_by_ai | blocked | ready_for_qa | resolved | wont_fix | pending_approval | unresolved | all — DEFAULTS TO `unresolved` (omitted = items still being worked: open, in_progress, blocked, ready_for_qa…; no resolved or wont_fix ones). Pass `status: "all"` to include resolved and wont_fix too (pending_approval is only returned when asked for exactly); `priority` = critical | high | medium | nice_to_have; `owner` = a person (id, email or name) or an agent (connector id, name or handle, e.g. "Claude"); `scope: "ready"` = what to work on now — open items nobody is working (no open run) that are unowned or were reopened after an earlier run. Use `scope: "ready"` to pick the next item, then `ops_ack` it.'
5522
5609
  post:
5523
5610
  operationId: createOpsItem
5524
5611
  summary: File a queue item
@@ -5615,6 +5702,8 @@ paths:
5615
5702
 
5616
5703
  The project is the key's own project; there is no `X-Project-Id` to pass and no way to file into someone else's queue. Repeat submissions of the same title dedupe against the open ticket already tracking it, which returns `200` with `deduped: true` instead of filing again.
5617
5704
 
5705
+ Send a `dedupKey` to own that grain yourself — the same key from a monitor, a job, or a retried install re-triggers its open ticket: the ticket's fields are refreshed from the new payload and a "re-triggered" comment is appended (`200` with `deduped: true, updated: true`), so one recurring failure stays one ticket with its history intact.
5706
+
5618
5707
  This endpoint is served by the Shipeasy **edge worker** (`api.shipeasy.ai`), not the admin API — see `servers` below.
5619
5708
 
5620
5709
  **Use case:** `shipeasy setup` fails on a customer's machine and self-reports the failure with the user's consent — `{ "title": "Setup failed at Feature installs", "stepsToReproduce": "…", "actualResult": "…" }`.
@@ -5694,7 +5783,7 @@ paths:
5694
5783
  description: |-
5695
5784
  Files one feature request onto a project's queue, awaiting human approval. The feature-request counterpart to `POST /ops/bug`, with the same three gates: a `client` key carrying `tickets:public_create`, a project that has opted in, and a `pending_approval` state forced server-side.
5696
5785
 
5697
- The project is the key's own project; there is no `X-Project-Id` to pass. Repeat submissions of the same title dedupe against the open ticket already tracking it, which returns `200` with `deduped: true` instead of filing again.
5786
+ The project is the key's own project; there is no `X-Project-Id` to pass. Repeat submissions of the same title dedupe against the open ticket already tracking it, which returns `200` with `deduped: true` instead of filing again. A `dedupKey` works exactly as on `POST /ops/bug`: a repeat carrying the same key refreshes the open ticket and comments that it was re-triggered.
5698
5787
 
5699
5788
  This endpoint is served by the Shipeasy **edge worker** (`api.shipeasy.ai`), not the admin API — see `servers` below.
5700
5789
 
@@ -6013,6 +6102,11 @@ paths:
6013
6102
  list` and use one of those (or connect the agent under Settings →
6014
6103
  Triggers).
6015
6104
 
6105
+ **Launching.** When the named agent has a connected trigger and you pass
6106
+ no `sessionId`, the ack also fires that trigger — a new provider run
6107
+ starts working the item. Pass `launch: false` to only claim the item and
6108
+ record the run (you are doing the work yourself), without starting one.
6109
+
6016
6110
  **Completion.** The run closes automatically on the loop's final actions —
6017
6111
  linking the fixing PR (`link-pr`), an ops-notify escalation, or a
6018
6112
  completion status change (`ready_for_qa`/`resolved`) — and the dashboard
@@ -6134,7 +6228,9 @@ paths:
6134
6228
  (`kind: investigated`), a blocking question for the team (`kind:
6135
6229
  question`), or how to verify the fix (`kind: ready_for_qa` with
6136
6230
  `qaNotes`). Append-only and create-only, so it is safe for restricted ops
6137
- keys.
6231
+ keys. For a decision between options you can name, ask a structured
6232
+ question instead (`ops questions ask`) — it is answerable in one tap and
6233
+ the answer comes back to your run.
6138
6234
 
6139
6235
  **Use case:** After working an item, leave a findings write-up (summary,
6140
6236
  markdown findings, sources inspected, confidence) so the team — and the
@@ -6277,6 +6373,275 @@ paths:
6277
6373
  positional:
6278
6374
  - handle
6279
6375
  - investigationId
6376
+ /api/admin/ops/{handle}/questions:
6377
+ get:
6378
+ operationId: listOpsQuestions
6379
+ summary: List an item's questions
6380
+ description: |-
6381
+ The structured questions asked on a queue item, with their answers —
6382
+ newest first (max 100). Pull this to read the answers to questions you
6383
+ asked: an answered question reads `status: completed` and carries
6384
+ `answer.selected` (the option labels picked) and `answer.other` (the
6385
+ free-text "Other" reply, or `null`).
6386
+
6387
+ **Use case:** You asked with `delivery: poll` from a live session — poll
6388
+ this until your questions read `completed`, then continue with the answers.
6389
+ tags:
6390
+ - Questions
6391
+ parameters:
6392
+ - $ref: '#/components/parameters/ProjectId'
6393
+ - name: handle
6394
+ in: path
6395
+ required: true
6396
+ description: Per-project item number (e.g. `7`) or the full ops item id.
6397
+ schema:
6398
+ $ref: '#/components/schemas/ResourceId'
6399
+ - name: status
6400
+ in: query
6401
+ required: false
6402
+ description: Filter by question status, or `all` (the default).
6403
+ schema:
6404
+ default: all
6405
+ anyOf:
6406
+ - $ref: '#/components/schemas/OpsQuestionStatus'
6407
+ - type: string
6408
+ const: all
6409
+ responses:
6410
+ '200':
6411
+ description: List an item's questions
6412
+ content:
6413
+ application/json:
6414
+ schema:
6415
+ $ref: '#/components/schemas/ListOpsQuestionsResponse'
6416
+ '400':
6417
+ $ref: '#/components/responses/BadRequest'
6418
+ '401':
6419
+ $ref: '#/components/responses/Unauthorized'
6420
+ '403':
6421
+ $ref: '#/components/responses/Forbidden'
6422
+ '404':
6423
+ $ref: '#/components/responses/NotFound'
6424
+ '422':
6425
+ $ref: '#/components/responses/UnprocessableEntity'
6426
+ x-error-codes:
6427
+ - BAD_REQUEST
6428
+ - UNAUTHORIZED
6429
+ - FORBIDDEN
6430
+ - NOT_FOUND
6431
+ x-cli:
6432
+ name: list
6433
+ positional:
6434
+ - handle
6435
+ post:
6436
+ operationId: askOpsQuestions
6437
+ summary: Ask the team questions about an item
6438
+ description: |-
6439
+ Ask 1–4 structured questions about a queue item — the way to get a
6440
+ decision from a person when the implementation needs one. Each question
6441
+ is the `AskUserQuestion` shape: a short `header` chip, the full
6442
+ `question`, 2–4 `options` (`label` + `description`), and `multiSelect`
6443
+ (checkboxes instead of radios). The person can always answer "Other" in
6444
+ free text, so never add an "Other" option yourself. The questions render
6445
+ at the very top of the item's detail panel and mark the row in the queue.
6446
+
6447
+ **Delivery.** `resume` re-fires your trigger-connector run on the item
6448
+ once every question it asked is answered, continuing the same run record
6449
+ with the answers in the prompt — stop after asking. `poll` fires nothing;
6450
+ poll `GET .../questions` instead. Omitted, it is `resume` when `runId`
6451
+ names a run launched through a trigger connector, else `poll`.
6452
+
6453
+ **Use case:** Blocked on a product decision (which behaviour to keep,
6454
+ which of two fixes to ship) — ask instead of guessing, then wait for the
6455
+ answer.
6456
+ tags:
6457
+ - Questions
6458
+ parameters:
6459
+ - $ref: '#/components/parameters/ProjectId'
6460
+ - name: handle
6461
+ in: path
6462
+ required: true
6463
+ description: Per-project item number (e.g. `7`) or the full ops item id.
6464
+ schema:
6465
+ $ref: '#/components/schemas/ResourceId'
6466
+ requestBody:
6467
+ required: true
6468
+ content:
6469
+ application/json:
6470
+ schema:
6471
+ $ref: '#/components/schemas/AskOpsQuestionsRequest'
6472
+ examples:
6473
+ single:
6474
+ summary: Ask one radio question
6475
+ value:
6476
+ runId: 0d3f4c8e-7a51-4c2e-9b1f-1e2d3c4b5a69
6477
+ questions:
6478
+ - header: Legacy param
6479
+ question: Should the fix keep honouring the legacy `?v=1` query param?
6480
+ multiSelect: false
6481
+ options:
6482
+ - label: Keep it
6483
+ description: Old links keep working; the redirect stays
6484
+ - label: Drop it
6485
+ description: Simpler code; old links land on the default view
6486
+ responses:
6487
+ '200':
6488
+ description: The created questions, in the order asked
6489
+ content:
6490
+ application/json:
6491
+ schema:
6492
+ $ref: '#/components/schemas/AskOpsQuestionsResponse'
6493
+ '400':
6494
+ $ref: '#/components/responses/BadRequest'
6495
+ '401':
6496
+ $ref: '#/components/responses/Unauthorized'
6497
+ '403':
6498
+ $ref: '#/components/responses/Forbidden'
6499
+ '404':
6500
+ $ref: '#/components/responses/NotFound'
6501
+ '422':
6502
+ $ref: '#/components/responses/UnprocessableEntity'
6503
+ x-error-codes:
6504
+ - BAD_REQUEST
6505
+ - UNAUTHORIZED
6506
+ - FORBIDDEN
6507
+ - NOT_FOUND
6508
+ - VALIDATION
6509
+ x-cli:
6510
+ name: ask
6511
+ positional:
6512
+ - handle
6513
+ /api/admin/ops/{handle}/questions/{questionId}/answer:
6514
+ post:
6515
+ operationId: answerOpsQuestion
6516
+ summary: Answer a question
6517
+ description: |-
6518
+ Answer one open question on a queue item — pick option labels in
6519
+ `selected` (exactly one unless the question is `multiSelect`) and/or
6520
+ write a free-text `other`. The question moves to `completed`. When it was
6521
+ the last open question of a `resume` run, that agent is re-fired on the
6522
+ item with every answer. A person's action: never allow-listed for
6523
+ restricted ops keys.
6524
+
6525
+ **Use case:** Unblock an agent from the CLI instead of the dashboard.
6526
+ tags:
6527
+ - Questions
6528
+ parameters:
6529
+ - $ref: '#/components/parameters/ProjectId'
6530
+ - name: handle
6531
+ in: path
6532
+ required: true
6533
+ description: Per-project item number (e.g. `7`) or the full ops item id.
6534
+ schema:
6535
+ $ref: '#/components/schemas/ResourceId'
6536
+ - name: questionId
6537
+ in: path
6538
+ required: true
6539
+ description: The question id (from `ask` or `list`).
6540
+ schema:
6541
+ $ref: '#/components/schemas/ResourceId'
6542
+ requestBody:
6543
+ required: true
6544
+ content:
6545
+ application/json:
6546
+ schema:
6547
+ $ref: '#/components/schemas/AnswerOpsQuestionRequest'
6548
+ examples:
6549
+ option:
6550
+ summary: Pick an option
6551
+ value:
6552
+ selected:
6553
+ - Keep it
6554
+ other:
6555
+ summary: Answer in free text
6556
+ value:
6557
+ other: Keep it for a release, then drop it
6558
+ responses:
6559
+ '200':
6560
+ description: The answered question
6561
+ content:
6562
+ application/json:
6563
+ schema:
6564
+ $ref: '#/components/schemas/OpsQuestion'
6565
+ '400':
6566
+ $ref: '#/components/responses/BadRequest'
6567
+ '401':
6568
+ $ref: '#/components/responses/Unauthorized'
6569
+ '403':
6570
+ $ref: '#/components/responses/Forbidden'
6571
+ '404':
6572
+ $ref: '#/components/responses/NotFound'
6573
+ '409':
6574
+ $ref: '#/components/responses/Conflict'
6575
+ '422':
6576
+ $ref: '#/components/responses/UnprocessableEntity'
6577
+ x-error-codes:
6578
+ - BAD_REQUEST
6579
+ - UNAUTHORIZED
6580
+ - FORBIDDEN
6581
+ - NOT_FOUND
6582
+ - INVALID_TRANSITION
6583
+ - VALIDATION
6584
+ x-cli:
6585
+ name: answer
6586
+ positional:
6587
+ - handle
6588
+ - questionId
6589
+ /api/admin/ops/{handle}/questions/{questionId}/cancel:
6590
+ post:
6591
+ operationId: cancelOpsQuestion
6592
+ summary: Withdraw a question
6593
+ description: |-
6594
+ Withdraw one open question you asked — it no longer needs an answer
6595
+ (you found it out yourself, or the item moved on). The question moves to
6596
+ `cancelled` and leaves the detail panel. Withdrawing the last open
6597
+ question of a run delivers nothing.
6598
+
6599
+ **Use case:** You asked, then answered it yourself from the code — take
6600
+ it off the person's plate.
6601
+ tags:
6602
+ - Questions
6603
+ parameters:
6604
+ - $ref: '#/components/parameters/ProjectId'
6605
+ - name: handle
6606
+ in: path
6607
+ required: true
6608
+ description: Per-project item number (e.g. `7`) or the full ops item id.
6609
+ schema:
6610
+ $ref: '#/components/schemas/ResourceId'
6611
+ - name: questionId
6612
+ in: path
6613
+ required: true
6614
+ description: The question id (from `ask` or `list`).
6615
+ schema:
6616
+ $ref: '#/components/schemas/ResourceId'
6617
+ responses:
6618
+ '200':
6619
+ description: The withdrawn question
6620
+ content:
6621
+ application/json:
6622
+ schema:
6623
+ $ref: '#/components/schemas/OpsQuestion'
6624
+ '400':
6625
+ $ref: '#/components/responses/BadRequest'
6626
+ '401':
6627
+ $ref: '#/components/responses/Unauthorized'
6628
+ '403':
6629
+ $ref: '#/components/responses/Forbidden'
6630
+ '404':
6631
+ $ref: '#/components/responses/NotFound'
6632
+ '409':
6633
+ $ref: '#/components/responses/Conflict'
6634
+ x-error-codes:
6635
+ - BAD_REQUEST
6636
+ - UNAUTHORIZED
6637
+ - FORBIDDEN
6638
+ - NOT_FOUND
6639
+ - INVALID_TRANSITION
6640
+ x-cli:
6641
+ name: cancel
6642
+ positional:
6643
+ - handle
6644
+ - questionId
6280
6645
  /api/admin/agent-profiles:
6281
6646
  get:
6282
6647
  operationId: listOpsAgents
@@ -6790,7 +7155,9 @@ paths:
6790
7155
  description: |-
6791
7156
  Returns the project's FIRED alerts as a **bare JSON array** (no pagination envelope), ordered by `createdAt desc`. Defaults to the currently-firing ones (`status=active`); pass a `status` to widen to resolved/dismissed history or `all`.
6792
7157
 
6793
- Fired alerts are raised only by the platform — the UI's killswitch handlers plus the worker's analysis consumer and alerts cron — never filed by hand, so this surface is list + triage (via PATCH), with no create. The rules that *define* metric-threshold alerts live at `/api/admin/alert-rules`.
7158
+ A fired alert is an **instance** of an alert rule, and an instance IS its ops queue item — the same row `/api/admin/ops` returns with `type: "alert"`, viewed through the alert lens. One rule produces many instances over its life and at most one is open at a time: when the condition clears the instance closes, and the next firing is a NEW instance with its own id and history. That is why `severity` reads off the rule (the queue item carries it as `priority`) and why resolving the queue item and clearing the alert are one act, not two rows to keep in step.
7159
+
7160
+ Instances are opened only by the platform — the UI's killswitch handlers plus the worker's analysis consumer and alerts cron — never filed by hand, so this surface is list + triage (via PATCH), with no create. The rules that *define* metric-threshold alerts live at `/api/admin/alert-rules`.
6794
7161
 
6795
7162
  **Use case:** Snapshot what is currently firing for an on-call view or the home Alerts block, or pull `status=all` to audit how a noisy rule has behaved over time.
6796
7163
  tags:
@@ -6874,7 +7241,7 @@ paths:
6874
7241
  description: |-
6875
7242
  Triage writes on one fired alert — the only mutations this surface allows. All body fields are optional (at least one required); only the fields present are changed.
6876
7243
 
6877
- - **`status`** — flip between `active` / `resolved` / `dismissed`. `resolved` and `dismissed` stamp `resolvedAt` / `dismissedAt`; `active` re-opens and clears both. A resolved alert re-fires (as the same row, re-activated) if its condition is raised again.
7244
+ - **`status`** — flip between `active` / `resolved` / `dismissed`. `resolved` and `dismissed` stamp `resolvedAt` / `dismissedAt`; `active` re-opens and clears both. These write the underlying queue item's own status (`resolved` / `wont_fix` / `open`), so clearing an alert here and resolving it in the ops queue are the same act. A cleared instance is never revived: if the condition fires again it opens a NEW instance with its own id.
6878
7245
  - **`assigneeId`** — the PERSON owner (a `users.id`), or `null` to unassign.
6879
7246
  - **`agent`** — the AGENT owner: a connected trigger connector's id, or the built-in `"jarvis"` (**Enterprise plan only** — `403` otherwise), or `null` to clear. Person and agent halves are independent.
6880
7247
 
@@ -6891,7 +7258,7 @@ paths:
6891
7258
  - name: id
6892
7259
  in: path
6893
7260
  required: true
6894
- description: Stable opaque alert id (`al_…`).
7261
+ description: Stable opaque instance id — the same id as the `alert` ops item this instance is.
6895
7262
  schema:
6896
7263
  $ref: '#/components/schemas/ResourceId'
6897
7264
  requestBody:
@@ -7365,11 +7732,12 @@ paths:
7365
7732
  - name: limit
7366
7733
  in: query
7367
7734
  required: false
7368
- description: Max keys to return (1–500).
7735
+ description: Max keys to return (1–500). Defaults to 200.
7369
7736
  schema:
7370
7737
  type: integer
7371
7738
  minimum: 1
7372
7739
  maximum: 500
7740
+ default: 200
7373
7741
  - name: offset
7374
7742
  in: query
7375
7743
  required: false
@@ -9406,6 +9774,7 @@ paths:
9406
9774
  expires_at: null
9407
9775
  created_by_email: ana@example.com
9408
9776
  name: production back-end
9777
+ description: Held by the checkout service; rotate with the quarterly credential sweep
9409
9778
  scopes:
9410
9779
  - gates:evaluate
9411
9780
  - events:write
@@ -9505,16 +9874,16 @@ paths:
9505
9874
  - PLAN_LIMIT
9506
9875
  x-cli:
9507
9876
  name: create
9508
- /api/admin/keys/{id}/revoke:
9509
- post:
9510
- operationId: revokeKey
9511
- summary: Revoke an API key
9877
+ /api/admin/keys/{id}:
9878
+ patch:
9879
+ operationId: updateKey
9880
+ summary: Update an API key's label
9512
9881
  description: |-
9513
- Revokes a key by id — stamps its `revoked_at` and deletes the hot-path KV entry so the token stops authenticating immediately. Takes no body.
9882
+ Renames a key or re-notes what it is for. **Only `name` and `description` are editable** — everything that gives the key its authority (`type`, `env`, `scopes`, `expires_at`) is fixed at mint, because the worker derives the read env and permissions from the key itself. To change any of those, rotate: mint a replacement and revoke this one.
9514
9883
 
9515
- Idempotent: revoking an already-revoked key is a no-op and returns the same `{ id, revoked: true }`. Returns `404` if no such key exists in the project.
9884
+ Omitted fields are left untouched; send `null` to clear one. Returns the updated key row. Revoked keys can still be relabelled (the audit trail outlives the credential). Returns `404` if no such key exists in the project.
9516
9885
 
9517
- **Use case:** Rotate a leaked or stale credential — mint the replacement, then revoke the old key.
9886
+ **Use case:** A key minted by `shipeasy setup` carries an auto-composed provenance label — rename it to the service that actually holds it and note the rotation owner.
9518
9887
  tags:
9519
9888
  - API Keys
9520
9889
  parameters:
@@ -9525,16 +9894,43 @@ paths:
9525
9894
  description: Stable opaque key id (UUID) returned by `create` / `list`.
9526
9895
  schema:
9527
9896
  $ref: '#/components/schemas/ResourceId'
9897
+ requestBody:
9898
+ required: true
9899
+ content:
9900
+ application/json:
9901
+ schema:
9902
+ $ref: '#/components/schemas/UpdateKeyRequest'
9903
+ examples:
9904
+ rename:
9905
+ summary: Rename and note the key
9906
+ value:
9907
+ name: checkout-service · production
9908
+ description: Held by the checkout service; rotation owner is the payments team
9909
+ clearDescription:
9910
+ summary: Clear the note
9911
+ value:
9912
+ description: null
9528
9913
  responses:
9529
9914
  '200':
9530
- description: Revoke an API key
9915
+ description: Update an API key's label
9531
9916
  content:
9532
9917
  application/json:
9533
9918
  schema:
9534
- $ref: '#/components/schemas/RevokeKeyResponse'
9919
+ $ref: '#/components/schemas/KeyRecord'
9535
9920
  example:
9536
9921
  id: 3f2a9b1c-7d4e-4a8f-9c2b-1e5d6f7a8b90
9537
- revoked: true
9922
+ type: server
9923
+ env: prod
9924
+ created_at: '2026-05-09T16:01:22.000Z'
9925
+ revoked_at: null
9926
+ expires_at: null
9927
+ created_by_email: ana@example.com
9928
+ name: checkout-service · production
9929
+ description: Held by the checkout service; rotation owner is the payments team
9930
+ scopes:
9931
+ - gates:evaluate
9932
+ - events:write
9933
+ last4: a1b2
9538
9934
  '400':
9539
9935
  $ref: '#/components/responses/BadRequest'
9540
9936
  '401':
@@ -9551,18 +9947,69 @@ paths:
9551
9947
  - UNAUTHORIZED
9552
9948
  - FORBIDDEN
9553
9949
  - NOT_FOUND
9950
+ - VALIDATION
9554
9951
  x-cli:
9555
- name: revoke
9952
+ name: update
9556
9953
  positional:
9557
9954
  - id
9558
- /api/admin/search:
9559
- get:
9560
- operationId: searchResources
9561
- summary: Search project resources
9955
+ /api/admin/keys/{id}/revoke:
9956
+ post:
9957
+ operationId: revokeKey
9958
+ summary: Revoke an API key
9562
9959
  description: |-
9563
- Universal substring search across the project's primary resources — feature gates, experiments, dynamic configs, killswitches, metrics, and team members. Matches the trimmed `q` against each resource's immutable `name` and its human title/display name (members match on display name or email), newest-first, capped per resource family so one noisy type can't crowd out the rest.
9960
+ Revokes a key by id — stamps its `revoked_at` and deletes the hot-path KV entry so the token stops authenticating immediately. Takes no body.
9564
9961
 
9565
- Returns a flat, ranked-by-type list of hits (members first); an empty or whitespace-only `q` returns no hits. Each hit carries a project-relative `href` that deep-links to the row.
9962
+ Idempotent: revoking an already-revoked key is a no-op and returns the same `{ id, revoked: true }`. Returns `404` if no such key exists in the project.
9963
+
9964
+ **Use case:** Rotate a leaked or stale credential — mint the replacement, then revoke the old key.
9965
+ tags:
9966
+ - API Keys
9967
+ parameters:
9968
+ - $ref: '#/components/parameters/ProjectId'
9969
+ - name: id
9970
+ in: path
9971
+ required: true
9972
+ description: Stable opaque key id (UUID) returned by `create` / `list`.
9973
+ schema:
9974
+ $ref: '#/components/schemas/ResourceId'
9975
+ responses:
9976
+ '200':
9977
+ description: Revoke an API key
9978
+ content:
9979
+ application/json:
9980
+ schema:
9981
+ $ref: '#/components/schemas/RevokeKeyResponse'
9982
+ example:
9983
+ id: 3f2a9b1c-7d4e-4a8f-9c2b-1e5d6f7a8b90
9984
+ revoked: true
9985
+ '400':
9986
+ $ref: '#/components/responses/BadRequest'
9987
+ '401':
9988
+ $ref: '#/components/responses/Unauthorized'
9989
+ '403':
9990
+ $ref: '#/components/responses/Forbidden'
9991
+ '404':
9992
+ $ref: '#/components/responses/NotFound'
9993
+ '409':
9994
+ $ref: '#/components/responses/Conflict'
9995
+ '422':
9996
+ $ref: '#/components/responses/UnprocessableEntity'
9997
+ x-error-codes:
9998
+ - UNAUTHORIZED
9999
+ - FORBIDDEN
10000
+ - NOT_FOUND
10001
+ x-cli:
10002
+ name: revoke
10003
+ positional:
10004
+ - id
10005
+ /api/admin/search:
10006
+ get:
10007
+ operationId: searchResources
10008
+ summary: Search project resources
10009
+ description: |-
10010
+ Universal substring search across the project's primary resources — feature gates, experiments, dynamic configs, killswitches, metrics, and team members. Matches the trimmed `q` against each resource's immutable `name` and its human title/display name (members match on display name or email), newest-first, capped per resource family so one noisy type can't crowd out the rest.
10011
+
10012
+ Returns a flat, ranked-by-type list of hits (members first); an empty or whitespace-only `q` returns no hits. Each hit carries a project-relative `href` that deep-links to the row.
9566
10013
 
9567
10014
  **Use case:** Back a command-palette / quick-switcher in the dashboard, or look up the id of a resource by a fragment of its name before driving another API call.
9568
10015
  tags:
@@ -9617,6 +10064,276 @@ paths:
9617
10064
  - FORBIDDEN
9618
10065
  x-cli:
9619
10066
  name: search
10067
+ /api/admin/tags:
10068
+ get:
10069
+ operationId: listTags
10070
+ summary: List the project's tags
10071
+ description: |-
10072
+ Returns every tag in the project — the shared roster any item (queue items, flags, configs, kill switches, metrics, tracked errors, alert rules, experiments) can carry. Sorted by name.
10073
+
10074
+ **Use case:** See which tags already exist before tagging an item, so you reuse `ios` instead of minting `iOS-app`.
10075
+ tags:
10076
+ - Tags
10077
+ parameters:
10078
+ - $ref: '#/components/parameters/ProjectId'
10079
+ - name: q
10080
+ in: query
10081
+ required: false
10082
+ description: Case-insensitive substring filter on the tag name. Omit to return every tag.
10083
+ schema:
10084
+ type: string
10085
+ maxLength: 64
10086
+ responses:
10087
+ '200':
10088
+ description: List the project's tags
10089
+ content:
10090
+ application/json:
10091
+ schema:
10092
+ $ref: '#/components/schemas/ListTagsResponse'
10093
+ example:
10094
+ - id: tag_01j7w8a1b2c3d4e5f6g7h8i9j0
10095
+ name: checkout
10096
+ - id: tag_01j7w8a1b2c3d4e5f6g7h8i9j1
10097
+ name: ios
10098
+ '400':
10099
+ $ref: '#/components/responses/BadRequest'
10100
+ '401':
10101
+ $ref: '#/components/responses/Unauthorized'
10102
+ '403':
10103
+ $ref: '#/components/responses/Forbidden'
10104
+ '404':
10105
+ $ref: '#/components/responses/NotFound'
10106
+ '409':
10107
+ $ref: '#/components/responses/Conflict'
10108
+ '422':
10109
+ $ref: '#/components/responses/UnprocessableEntity'
10110
+ x-error-codes:
10111
+ - BAD_REQUEST
10112
+ - UNAUTHORIZED
10113
+ - FORBIDDEN
10114
+ x-cli:
10115
+ name: list
10116
+ /api/admin/tags/{entityType}/{entityId}:
10117
+ get:
10118
+ operationId: getEntityTags
10119
+ summary: Read an item's tags
10120
+ description: |-
10121
+ Returns the tags attached to one item. The item is addressed by kind (`entityType`) plus its handle, so the same call reads tags off a queue item, a flag, a config, a kill switch, a metric, a tracked error, an alert rule, or an experiment.
10122
+
10123
+ **Use case:** Check an item's current tags before editing them.
10124
+ tags:
10125
+ - Tags
10126
+ parameters:
10127
+ - $ref: '#/components/parameters/ProjectId'
10128
+ - name: entityType
10129
+ in: path
10130
+ required: true
10131
+ description: The kind of item — `ops`, `flag`, `config`, `killswitch`, `metric`, `error`, `alert_rule`, or `experiment`.
10132
+ schema:
10133
+ $ref: '#/components/schemas/TaggableEntityType'
10134
+ - name: entityId
10135
+ in: path
10136
+ required: true
10137
+ description: The item's handle — an ops item's number (e.g. `7`) or id; a flag/config/killswitch/metric/alert rule/experiment's name or id; a tracked error's id.
10138
+ schema:
10139
+ $ref: '#/components/schemas/ResourceId'
10140
+ responses:
10141
+ '200':
10142
+ description: Read an item's tags
10143
+ content:
10144
+ application/json:
10145
+ schema:
10146
+ $ref: '#/components/schemas/EntityTagsResponse'
10147
+ example:
10148
+ entityType: ops
10149
+ entityId: fb_01j7w8a1b2c3d4e5f6g7h8i9j0
10150
+ tags:
10151
+ - checkout
10152
+ - ios
10153
+ '400':
10154
+ $ref: '#/components/responses/BadRequest'
10155
+ '401':
10156
+ $ref: '#/components/responses/Unauthorized'
10157
+ '403':
10158
+ $ref: '#/components/responses/Forbidden'
10159
+ '404':
10160
+ $ref: '#/components/responses/NotFound'
10161
+ '409':
10162
+ $ref: '#/components/responses/Conflict'
10163
+ '422':
10164
+ $ref: '#/components/responses/UnprocessableEntity'
10165
+ x-error-codes:
10166
+ - BAD_REQUEST
10167
+ - UNAUTHORIZED
10168
+ - FORBIDDEN
10169
+ - NOT_FOUND
10170
+ x-cli:
10171
+ name: get
10172
+ positional:
10173
+ - entityType
10174
+ - entityId
10175
+ put:
10176
+ operationId: setEntityTags
10177
+ summary: Replace an item's tags
10178
+ description: |-
10179
+ Replaces the item's whole tag set with `tags`: names not yet in the project are created, attached names not in the list are detached, and `[]` clears every tag. Names are deduped case-insensitively, reusing an existing tag's casing (sending `iOS` when `ios` exists attaches `ios`).
10180
+
10181
+ **Use cases**
10182
+
10183
+ - **Tag an item** — `{ "tags": ["checkout", "ios"] }`.
10184
+ - **Clear tags** — `{ "tags": [] }`.
10185
+
10186
+ To add or remove a few tags without restating the rest, use the incremental `PATCH` instead.
10187
+ tags:
10188
+ - Tags
10189
+ parameters:
10190
+ - $ref: '#/components/parameters/ProjectId'
10191
+ - name: entityType
10192
+ in: path
10193
+ required: true
10194
+ description: The kind of item — `ops`, `flag`, `config`, `killswitch`, `metric`, `error`, `alert_rule`, or `experiment`.
10195
+ schema:
10196
+ $ref: '#/components/schemas/TaggableEntityType'
10197
+ - name: entityId
10198
+ in: path
10199
+ required: true
10200
+ description: The item's handle — an ops item's number (e.g. `7`) or id; a flag/config/killswitch/metric/alert rule/experiment's name or id; a tracked error's id.
10201
+ schema:
10202
+ $ref: '#/components/schemas/ResourceId'
10203
+ requestBody:
10204
+ required: true
10205
+ content:
10206
+ application/json:
10207
+ schema:
10208
+ $ref: '#/components/schemas/SetEntityTagsRequest'
10209
+ examples:
10210
+ tag:
10211
+ summary: Set the item's tags
10212
+ value:
10213
+ tags:
10214
+ - checkout
10215
+ - ios
10216
+ clear:
10217
+ summary: Remove every tag
10218
+ value:
10219
+ tags: []
10220
+ responses:
10221
+ '200':
10222
+ description: Replace an item's tags
10223
+ content:
10224
+ application/json:
10225
+ schema:
10226
+ $ref: '#/components/schemas/EntityTagsResponse'
10227
+ example:
10228
+ entityType: ops
10229
+ entityId: fb_01j7w8a1b2c3d4e5f6g7h8i9j0
10230
+ tags:
10231
+ - checkout
10232
+ - ios
10233
+ '400':
10234
+ $ref: '#/components/responses/BadRequest'
10235
+ '401':
10236
+ $ref: '#/components/responses/Unauthorized'
10237
+ '403':
10238
+ $ref: '#/components/responses/Forbidden'
10239
+ '404':
10240
+ $ref: '#/components/responses/NotFound'
10241
+ '409':
10242
+ $ref: '#/components/responses/Conflict'
10243
+ '422':
10244
+ $ref: '#/components/responses/UnprocessableEntity'
10245
+ x-error-codes:
10246
+ - BAD_REQUEST
10247
+ - UNAUTHORIZED
10248
+ - FORBIDDEN
10249
+ - NOT_FOUND
10250
+ - VALIDATION
10251
+ x-cli:
10252
+ name: set
10253
+ positional:
10254
+ - entityType
10255
+ - entityId
10256
+ patch:
10257
+ operationId: updateEntityTags
10258
+ summary: Add or remove tags on an item
10259
+ description: |-
10260
+ Edits an item's tags incrementally: attaches every name in `add` (creating project tags as needed) and detaches every name in `remove`, leaving the item's other tags untouched. Matching is case-insensitive; a name in both lists ends up removed.
10261
+
10262
+ **Use cases**
10263
+
10264
+ - **Add a tag** — `{ "add": ["regression"] }`.
10265
+ - **Swap a tag** — `{ "add": ["p1"], "remove": ["p2"] }`.
10266
+ tags:
10267
+ - Tags
10268
+ parameters:
10269
+ - $ref: '#/components/parameters/ProjectId'
10270
+ - name: entityType
10271
+ in: path
10272
+ required: true
10273
+ description: The kind of item — `ops`, `flag`, `config`, `killswitch`, `metric`, `error`, `alert_rule`, or `experiment`.
10274
+ schema:
10275
+ $ref: '#/components/schemas/TaggableEntityType'
10276
+ - name: entityId
10277
+ in: path
10278
+ required: true
10279
+ description: The item's handle — an ops item's number (e.g. `7`) or id; a flag/config/killswitch/metric/alert rule/experiment's name or id; a tracked error's id.
10280
+ schema:
10281
+ $ref: '#/components/schemas/ResourceId'
10282
+ requestBody:
10283
+ required: true
10284
+ content:
10285
+ application/json:
10286
+ schema:
10287
+ $ref: '#/components/schemas/UpdateEntityTagsRequest'
10288
+ examples:
10289
+ add:
10290
+ summary: Attach one tag
10291
+ value:
10292
+ add:
10293
+ - regression
10294
+ swap:
10295
+ summary: Replace one tag with another
10296
+ value:
10297
+ add:
10298
+ - p1
10299
+ remove:
10300
+ - p2
10301
+ responses:
10302
+ '200':
10303
+ description: Add or remove tags on an item
10304
+ content:
10305
+ application/json:
10306
+ schema:
10307
+ $ref: '#/components/schemas/EntityTagsResponse'
10308
+ example:
10309
+ entityType: ops
10310
+ entityId: fb_01j7w8a1b2c3d4e5f6g7h8i9j0
10311
+ tags:
10312
+ - checkout
10313
+ - ios
10314
+ '400':
10315
+ $ref: '#/components/responses/BadRequest'
10316
+ '401':
10317
+ $ref: '#/components/responses/Unauthorized'
10318
+ '403':
10319
+ $ref: '#/components/responses/Forbidden'
10320
+ '404':
10321
+ $ref: '#/components/responses/NotFound'
10322
+ '409':
10323
+ $ref: '#/components/responses/Conflict'
10324
+ '422':
10325
+ $ref: '#/components/responses/UnprocessableEntity'
10326
+ x-error-codes:
10327
+ - BAD_REQUEST
10328
+ - UNAUTHORIZED
10329
+ - FORBIDDEN
10330
+ - NOT_FOUND
10331
+ - VALIDATION
10332
+ x-cli:
10333
+ name: update
10334
+ positional:
10335
+ - entityType
10336
+ - entityId
9620
10337
  components:
9621
10338
  securitySchemes:
9622
10339
  bearerSdkKey:
@@ -11134,13 +11851,10 @@ components:
11134
11851
  aggregation:
11135
11852
  type: string
11136
11853
  enum:
11137
- - count_users
11138
11854
  - count_events
11139
- - retention_7d
11140
- - retention_30d
11141
11855
  - sum
11142
11856
  - avg
11143
- description: Reducer for the `event` form. Defaults to `count_users`.
11857
+ description: 'Reducer for the `event` form. Defaults to `count_events`. `count_users` was removed: Analytics Engine samples rows and weights the survivors, and a distinct count cannot be reweighted, so it under-reports by the sample interval. `retention_7d` / `retention_30d` were removed on 2026-08-10: the day count reached no query and no reducer, so both produced the same number as a distinct user count and reported it as a returning-user rate.'
11144
11858
  value:
11145
11859
  type: string
11146
11860
  minLength: 1
@@ -13557,253 +14271,60 @@ components:
13557
14271
  - id
13558
14272
  additionalProperties: false
13559
14273
  description: Response for `PATCH /api/admin/attributes/{id}`.
14274
+ MetricDisplayConfig:
14275
+ description: How the metric's series is DRAWN, as opposed to what it measures. Both parts used to be DSL functions (`expected(q, seasonal)`, `forecast(q, …)`), which meant turning a band on minted a different metric; they are properties now, so every chart of the metric picks them up and nothing that JUDGES the metric — an alert rule, the experiment analyzer — reads them at all.
14276
+ type: object
14277
+ properties:
14278
+ band:
14279
+ description: Shade the seasonal baseline around the series. The fit is the median of this hour of the week over the four weeks before the window, and it is the SAME fit an `anomaly` alert rule fires on — so the shaded region is exactly where such a rule would stay quiet, whether or not one exists.
14280
+ type: object
14281
+ properties:
14282
+ method:
14283
+ type: string
14284
+ enum:
14285
+ - seasonal
14286
+ sigma:
14287
+ type: number
14288
+ exclusiveMinimum: 0
14289
+ description: Half-width of the band in sigma-equivalents. Omit for 3, the sigma an alert rule also defaults to.
14290
+ required:
14291
+ - method
14292
+ additionalProperties: false
14293
+ forecast:
14294
+ description: Project the series past the end of the window. Projected buckets are marked, so a chart can draw an estimate differently from a measurement.
14295
+ type: object
14296
+ properties:
14297
+ method:
14298
+ type: string
14299
+ enum:
14300
+ - linear
14301
+ - seasonal
14302
+ description: '`linear` continues a least-squares slope over the window it is already reading. `seasonal` de-seasonalises against the four weeks before the window, fits the trend on the remainder and adds the hour-of-week term back — ask for it when the metric has a shape.'
14303
+ horizon:
14304
+ type: integer
14305
+ minimum: 1
14306
+ maximum: 500
14307
+ description: Buckets to project. Omit for a quarter of the window, which is the same claim at every bucket width.
14308
+ required:
14309
+ - method
14310
+ additionalProperties: false
14311
+ additionalProperties: false
13560
14312
  QueryIr:
13561
- description: Typed query IR — the structured alternative to the `query` DSL string. Exactly one of `query` / `query_ir` is supplied per metric body.
14313
+ description: |-
14314
+ A metric definition as a typed expression tree — the structured alternative to the `query` DSL string, and what the API stores. Exactly one of `query` / `query_ir` is supplied per metric body; `query` is the same definition written as text, and is the spelling to prefer.
14315
+ Until 2026-08-10 this documented a single aggregation (`{agg, metric, filters}`, with `ratio` as a special case). That shape no longer exists — stored metrics were migrated and the code that read it was deleted — so a body sending it is rejected. Send the `query` string instead, or the tree below.
13562
14316
  type: object
13563
14317
  properties:
13564
- agg:
13565
- oneOf:
13566
- - type: object
13567
- properties:
13568
- kind:
13569
- type: string
13570
- const: count_users
13571
- required:
13572
- - kind
13573
- additionalProperties: false
13574
- - type: object
13575
- properties:
13576
- kind:
13577
- type: string
13578
- const: count_events
13579
- required:
13580
- - kind
13581
- additionalProperties: false
13582
- - type: object
13583
- properties:
13584
- kind:
13585
- type: string
13586
- const: sum
13587
- required:
13588
- - kind
13589
- additionalProperties: false
13590
- - type: object
13591
- properties:
13592
- kind:
13593
- type: string
13594
- const: avg
13595
- required:
13596
- - kind
13597
- additionalProperties: false
13598
- - type: object
13599
- properties:
13600
- kind:
13601
- type: string
13602
- const: min
13603
- required:
13604
- - kind
13605
- additionalProperties: false
13606
- - type: object
13607
- properties:
13608
- kind:
13609
- type: string
13610
- const: max
13611
- required:
13612
- - kind
13613
- additionalProperties: false
13614
- - type: object
13615
- properties:
13616
- kind:
13617
- type: string
13618
- const: unique
13619
- required:
13620
- - kind
13621
- additionalProperties: false
13622
- - type: object
13623
- properties:
13624
- kind:
13625
- type: string
13626
- const: quantile
13627
- p:
13628
- anyOf:
13629
- - type: number
13630
- const: 0.5
13631
- - type: number
13632
- const: 0.75
13633
- - type: number
13634
- const: 0.9
13635
- - type: number
13636
- const: 0.95
13637
- - type: number
13638
- const: 0.99
13639
- - type: number
13640
- const: 0.999
13641
- description: Quantile fraction (0.5 = p50 … 0.999 = p999).
13642
- required:
13643
- - kind
13644
- - p
13645
- additionalProperties: false
13646
- - type: object
13647
- properties:
13648
- kind:
13649
- type: string
13650
- const: retention_Nd
13651
- 'n':
13652
- type: integer
13653
- minimum: 1
13654
- maximum: 90
13655
- description: Retention window in days (1–90).
13656
- required:
13657
- - kind
13658
- - 'n'
13659
- additionalProperties: false
13660
- - type: object
13661
- properties:
13662
- kind:
13663
- type: string
13664
- const: ratio
13665
- numerator:
13666
- type: object
13667
- properties:
13668
- agg:
13669
- type: string
13670
- enum:
13671
- - count_users
13672
- - count_events
13673
- description: Counting mode for this ratio arm.
13674
- metric:
13675
- type: string
13676
- minLength: 1
13677
- maxLength: 128
13678
- description: Source event name for this ratio arm.
13679
- filters:
13680
- description: Optional label filters.
13681
- maxItems: 16
13682
- type: array
13683
- items:
13684
- type: object
13685
- properties:
13686
- label:
13687
- type: string
13688
- pattern: ^[a-z_][a-z0-9_]{0,63}$
13689
- description: Event property / label identifier.
13690
- op:
13691
- type: string
13692
- enum:
13693
- - '='
13694
- - '!='
13695
- - '=~'
13696
- - '!~'
13697
- description: Match operator (`=~`/`!~` are regex).
13698
- value:
13699
- type: string
13700
- maxLength: 512
13701
- description: Quoted filter value (coerced for numeric labels).
13702
- required:
13703
- - label
13704
- - op
13705
- - value
13706
- additionalProperties: false
13707
- description: One label filter on the source event.
13708
- required:
13709
- - agg
13710
- - metric
13711
- additionalProperties: false
13712
- description: Numerator or denominator arm of a `ratio` aggregation.
13713
- denominator:
13714
- type: object
13715
- properties:
13716
- agg:
13717
- type: string
13718
- enum:
13719
- - count_users
13720
- - count_events
13721
- description: Counting mode for this ratio arm.
13722
- metric:
13723
- type: string
13724
- minLength: 1
13725
- maxLength: 128
13726
- description: Source event name for this ratio arm.
13727
- filters:
13728
- description: Optional label filters.
13729
- maxItems: 16
13730
- type: array
13731
- items:
13732
- type: object
13733
- properties:
13734
- label:
13735
- type: string
13736
- pattern: ^[a-z_][a-z0-9_]{0,63}$
13737
- description: Event property / label identifier.
13738
- op:
13739
- type: string
13740
- enum:
13741
- - '='
13742
- - '!='
13743
- - '=~'
13744
- - '!~'
13745
- description: Match operator (`=~`/`!~` are regex).
13746
- value:
13747
- type: string
13748
- maxLength: 512
13749
- description: Quoted filter value (coerced for numeric labels).
13750
- required:
13751
- - label
13752
- - op
13753
- - value
13754
- additionalProperties: false
13755
- description: One label filter on the source event.
13756
- required:
13757
- - agg
13758
- - metric
13759
- additionalProperties: false
13760
- description: Numerator or denominator arm of a `ratio` aggregation.
13761
- required:
13762
- - kind
13763
- - numerator
13764
- - denominator
13765
- additionalProperties: false
13766
- description: Aggregation function applied to the source event.
13767
- metric:
13768
- type: string
13769
- minLength: 1
13770
- maxLength: 128
13771
- description: Source event name (must equal `event_name`).
13772
- valueLabel:
13773
- description: Numeric property summed/averaged for `sum`/`avg`/quantile aggregations.
13774
- type: string
13775
- minLength: 1
13776
- maxLength: 128
13777
- filters:
13778
- default: []
13779
- description: Label filters on the event.
13780
- maxItems: 16
13781
- type: array
13782
- items:
13783
- type: object
13784
- properties:
13785
- label:
13786
- type: string
13787
- pattern: ^[a-z_][a-z0-9_]{0,63}$
13788
- description: Event property / label identifier.
13789
- op:
13790
- type: string
13791
- enum:
13792
- - '='
13793
- - '!='
13794
- - '=~'
13795
- - '!~'
13796
- description: Match operator (`=~`/`!~` are regex).
13797
- value:
13798
- type: string
13799
- maxLength: 512
13800
- description: Quoted filter value (coerced for numeric labels).
13801
- required:
13802
- - label
13803
- - op
13804
- - value
13805
- additionalProperties: false
13806
- description: One label filter on the source event.
14318
+ v:
14319
+ type: integer
14320
+ const: 2
14321
+ description: Storage version of the definition. `2` is the only one.
14322
+ expr:
14323
+ type: object
14324
+ additionalProperties: true
14325
+ description: |-
14326
+ The expression tree: aggregate leaves (`{node: "agg", event, agg, filters, valueLabel?}`), arithmetic over them (`{node: "binary", op, left, right}`), scalars, and function calls (`{node: "call", fn, arg, params?}`). A ratio is ordinary division of two leaves.
14327
+ Deliberately opaque here rather than mirrored node by node: the grammar belongs to the query language, a second copy of it in this spec would drift from the one the server enforces, and the server validates every tree by planning it before the metric saves. `GET /api/admin/metrics` returns the grammar itself; the `query` string is the same tree in the form humans and the CLI write.
13807
14328
  groupBy:
13808
14329
  type: object
13809
14330
  properties:
@@ -13812,23 +14333,25 @@ components:
13812
14333
  enum:
13813
14334
  - by
13814
14335
  - without
13815
- description: '`by` keeps the listed labels; `without` drops them.'
14336
+ description: '`by` names the output dimensions; `without` names the ones to drop.'
13816
14337
  labels:
13817
- maxItems: 5
13818
14338
  type: array
14339
+ maxItems: 8
13819
14340
  items:
13820
14341
  type: string
13821
14342
  pattern: ^[a-z_][a-z0-9_]{0,63}$
13822
14343
  description: Event property / label identifier.
13823
- description: Labels to group by (max 5).
14344
+ description: Labels the grouping names.
13824
14345
  required:
13825
14346
  - op
13826
14347
  - labels
13827
14348
  additionalProperties: false
13828
- description: Optional group-by clause (ignored for experiment analysis).
14349
+ description: How the series is split. On the envelope rather than in `expr` because every leaf of one expression shares it.
14350
+ display:
14351
+ $ref: '#/components/schemas/MetricDisplayConfig'
13829
14352
  required:
13830
- - agg
13831
- - metric
14353
+ - v
14354
+ - expr
13832
14355
  additionalProperties: false
13833
14356
  ListMetricsResponse:
13834
14357
  type: array
@@ -13930,6 +14453,12 @@ components:
13930
14453
  additionalProperties: {}
13931
14454
  description: A metric definition.
13932
14455
  description: Every metric in the project (the list endpoint is not paginated).
14456
+ MetricDisplayName:
14457
+ description: 'What a human calls the metric — "Checkout revenue" beside the `checkout.revenue` that identifies it. Unlike `name` it is free text, it is editable, and nothing addresses the metric by it: the dashboard leads with it and falls back to `name` when it is absent, so a metric nobody named simply reads as its key. Send `null` to clear it.'
14458
+ maxLength: 200
14459
+ anyOf:
14460
+ - type: string
14461
+ - type: 'null'
13933
14462
  MetricEventName:
13934
14463
  type: string
13935
14464
  minLength: 1
@@ -13969,6 +14498,8 @@ components:
13969
14498
  properties:
13970
14499
  name:
13971
14500
  $ref: '#/components/schemas/MetricName'
14501
+ display_name:
14502
+ $ref: '#/components/schemas/MetricDisplayName'
13972
14503
  folder:
13973
14504
  $ref: '#/components/schemas/Folder'
13974
14505
  event_name:
@@ -13983,6 +14514,8 @@ components:
13983
14514
  $ref: '#/components/schemas/MetricDirection'
13984
14515
  unit:
13985
14516
  $ref: '#/components/schemas/MetricDisplayUnit'
14517
+ display:
14518
+ $ref: '#/components/schemas/MetricDisplayConfig'
13986
14519
  required:
13987
14520
  - name
13988
14521
  - event_name
@@ -13994,6 +14527,8 @@ components:
13994
14527
  properties:
13995
14528
  name:
13996
14529
  $ref: '#/components/schemas/MetricName'
14530
+ display_name:
14531
+ $ref: '#/components/schemas/MetricDisplayName'
13997
14532
  folder:
13998
14533
  $ref: '#/components/schemas/Folder'
13999
14534
  event_name:
@@ -14008,6 +14543,8 @@ components:
14008
14543
  $ref: '#/components/schemas/MetricDirection'
14009
14544
  unit:
14010
14545
  $ref: '#/components/schemas/MetricDisplayUnit'
14546
+ display:
14547
+ $ref: '#/components/schemas/MetricDisplayConfig'
14011
14548
  required:
14012
14549
  - name
14013
14550
  - event_name
@@ -14133,6 +14670,8 @@ components:
14133
14670
  UpdateMetricWithQuery:
14134
14671
  type: object
14135
14672
  properties:
14673
+ display_name:
14674
+ $ref: '#/components/schemas/MetricDisplayName'
14136
14675
  folder:
14137
14676
  $ref: '#/components/schemas/Folder'
14138
14677
  event_name:
@@ -14147,6 +14686,8 @@ components:
14147
14686
  $ref: '#/components/schemas/MetricDirection'
14148
14687
  unit:
14149
14688
  $ref: '#/components/schemas/MetricDisplayUnit'
14689
+ display:
14690
+ $ref: '#/components/schemas/MetricDisplayConfig'
14150
14691
  required:
14151
14692
  - query
14152
14693
  additionalProperties: false
@@ -14154,6 +14695,8 @@ components:
14154
14695
  UpdateMetricWithQueryIr:
14155
14696
  type: object
14156
14697
  properties:
14698
+ display_name:
14699
+ $ref: '#/components/schemas/MetricDisplayName'
14157
14700
  folder:
14158
14701
  $ref: '#/components/schemas/Folder'
14159
14702
  event_name:
@@ -14168,6 +14711,8 @@ components:
14168
14711
  $ref: '#/components/schemas/MetricDirection'
14169
14712
  unit:
14170
14713
  $ref: '#/components/schemas/MetricDisplayUnit'
14714
+ display:
14715
+ $ref: '#/components/schemas/MetricDisplayConfig'
14171
14716
  required:
14172
14717
  - query_ir
14173
14718
  additionalProperties: false
@@ -14175,6 +14720,8 @@ components:
14175
14720
  UpdateMetricFields:
14176
14721
  type: object
14177
14722
  properties:
14723
+ display_name:
14724
+ $ref: '#/components/schemas/MetricDisplayName'
14178
14725
  folder:
14179
14726
  $ref: '#/components/schemas/Folder'
14180
14727
  event_name:
@@ -14187,8 +14734,10 @@ components:
14187
14734
  $ref: '#/components/schemas/MetricDirection'
14188
14735
  unit:
14189
14736
  $ref: '#/components/schemas/MetricDisplayUnit'
14737
+ display:
14738
+ $ref: '#/components/schemas/MetricDisplayConfig'
14190
14739
  additionalProperties: false
14191
- description: Update-metric variant that leaves the query untouched (metadata-only edit — folder, event, winsorisation, default minimum effect of interest, direction, display unit).
14740
+ description: Update-metric variant that leaves the query untouched (metadata-only edit — folder, event, winsorisation, default minimum effect of interest, direction, display unit, band and projection).
14192
14741
  UpdateMetricRequest:
14193
14742
  oneOf:
14194
14743
  - $ref: '#/components/schemas/UpdateMetricWithQuery'
@@ -14287,6 +14836,14 @@ components:
14287
14836
  v:
14288
14837
  type: number
14289
14838
  description: Aggregated metric value in the bucket (sample-interval weighted).
14839
+ lo:
14840
+ type: number
14841
+ description: |
14842
+ Lower edge of the seasonal band — what this hour of the week normally holds, `sigma` below the median of the four weeks before the window. Present only for a metric whose `display.band` is on, and always alongside `hi`.
14843
+ hi:
14844
+ type: number
14845
+ description: |
14846
+ Upper edge of the seasonal band. Present only for a metric whose `display.band` is on, and always alongside `lo`. A point outside `[lo, hi]` is exactly a bucket an `anomaly` alert rule at the same sigma would breach on — the band a chart draws and the condition a rule fires on come from one fit.
14290
14847
  required:
14291
14848
  - t
14292
14849
  - v
@@ -14330,7 +14887,8 @@ components:
14330
14887
  type: string
14331
14888
  minLength: 1
14332
14889
  maxLength: 64
14333
- description: Property key, e.g. `amount` or `plan`.
14890
+ pattern: ^[a-zA-Z_][a-zA-Z_0-9]*$
14891
+ description: Property key, e.g. `amount` or `plan`. Must be a bare identifier — it is emitted as a SQL alias when the property is used as a `by()` dimension, and there is no way to quote one in the Analytics Engine dialect.
14334
14892
  type:
14335
14893
  type: string
14336
14894
  enum:
@@ -14346,11 +14904,14 @@ components:
14346
14904
  default: ''
14347
14905
  description: Human-readable description of the property.
14348
14906
  type: string
14907
+ pending:
14908
+ description: '`true` when this property was DISCOVERED in live traffic rather than declared. Nothing is recorded for it yet — a property is written to Analytics Engine only once it owns a column, and a column is allocated when an admin declares the property (include it in a `PATCH .../events/{id}` body). Approval is a gate rather than a formality: columns are finite and permanently reserved, so auto-allocating them from customer payload keys would exhaust an event''s schema irreversibly.'
14909
+ type: boolean
14349
14910
  required:
14350
14911
  - name
14351
14912
  - type
14352
14913
  additionalProperties: false
14353
- description: One typed property declared on a catalogued event.
14914
+ description: One typed property on a catalogued event — declared by an admin, or discovered in traffic and awaiting declaration.
14354
14915
  description: Typed properties declared on the event.
14355
14916
  pending:
14356
14917
  type: integer
@@ -14375,8 +14936,8 @@ components:
14375
14936
  properties:
14376
14937
  name:
14377
14938
  type: string
14378
- pattern: ^[a-zA-Z0-9_][a-zA-Z0-9_\-.]{0,127}$
14379
- description: Event name. Starts with a letter, digit, or `_`; letters, digits, `_`, `-`, `.`; max 128 chars. Immutable after create — this is the handle metric queries reference.
14939
+ pattern: ^[a-zA-Z0-9_][a-zA-Z0-9_\-.]{0,49}$
14940
+ description: Event name. Starts with a letter, digit, or `_`; letters, digits, `_`, `-`, `.`; max 50 characters. The cap is the Analytics Engine index budget — 96 bytes total, less a 36-byte project UUID and a separator — and the API has always enforced 50 while this spec advertised 128. The charset is ASCII, so characters and bytes are the same count. Immutable after create — this is the handle metric queries reference.
14380
14941
  folder:
14381
14942
  $ref: '#/components/schemas/Folder'
14382
14943
  description:
@@ -14393,7 +14954,8 @@ components:
14393
14954
  type: string
14394
14955
  minLength: 1
14395
14956
  maxLength: 64
14396
- description: Property key, e.g. `amount` or `plan`.
14957
+ pattern: ^[a-zA-Z_][a-zA-Z_0-9]*$
14958
+ description: Property key, e.g. `amount` or `plan`. Must be a bare identifier — it is emitted as a SQL alias when the property is used as a `by()` dimension, and there is no way to quote one in the Analytics Engine dialect.
14397
14959
  type:
14398
14960
  type: string
14399
14961
  enum:
@@ -14465,7 +15027,8 @@ components:
14465
15027
  type: string
14466
15028
  minLength: 1
14467
15029
  maxLength: 64
14468
- description: Property key, e.g. `amount` or `plan`.
15030
+ pattern: ^[a-zA-Z_][a-zA-Z_0-9]*$
15031
+ description: Property key, e.g. `amount` or `plan`. Must be a bare identifier — it is emitted as a SQL alias when the property is used as a `by()` dimension, and there is no way to quote one in the Analytics Engine dialect.
14469
15032
  type:
14470
15033
  type: string
14471
15034
  enum:
@@ -14481,11 +15044,14 @@ components:
14481
15044
  default: ''
14482
15045
  description: Human-readable description of the property.
14483
15046
  type: string
15047
+ pending:
15048
+ description: '`true` when this property was DISCOVERED in live traffic rather than declared. Nothing is recorded for it yet — a property is written to Analytics Engine only once it owns a column, and a column is allocated when an admin declares the property (include it in a `PATCH .../events/{id}` body). Approval is a gate rather than a formality: columns are finite and permanently reserved, so auto-allocating them from customer payload keys would exhaust an event''s schema irreversibly.'
15049
+ type: boolean
14484
15050
  required:
14485
15051
  - name
14486
15052
  - type
14487
15053
  additionalProperties: false
14488
- description: One typed property declared on a catalogued event.
15054
+ description: One typed property on a catalogued event — declared by an admin, or discovered in traffic and awaiting declaration.
14489
15055
  description: Typed properties declared on the event.
14490
15056
  pending:
14491
15057
  type: integer
@@ -14532,7 +15098,8 @@ components:
14532
15098
  type: string
14533
15099
  minLength: 1
14534
15100
  maxLength: 64
14535
- description: Property key, e.g. `amount` or `plan`.
15101
+ pattern: ^[a-zA-Z_][a-zA-Z_0-9]*$
15102
+ description: Property key, e.g. `amount` or `plan`. Must be a bare identifier — it is emitted as a SQL alias when the property is used as a `by()` dimension, and there is no way to quote one in the Analytics Engine dialect.
14536
15103
  type:
14537
15104
  type: string
14538
15105
  enum:
@@ -14583,7 +15150,8 @@ components:
14583
15150
  type: string
14584
15151
  minLength: 1
14585
15152
  maxLength: 64
14586
- description: Property key, e.g. `amount` or `plan`.
15153
+ pattern: ^[a-zA-Z_][a-zA-Z_0-9]*$
15154
+ description: Property key, e.g. `amount` or `plan`. Must be a bare identifier — it is emitted as a SQL alias when the property is used as a `by()` dimension, and there is no way to quote one in the Analytics Engine dialect.
14587
15155
  type:
14588
15156
  type: string
14589
15157
  enum:
@@ -14641,6 +15209,7 @@ components:
14641
15209
  enum:
14642
15210
  - pr_ready
14643
15211
  - pr_merged
15212
+ - pr_closed
14644
15213
  - investigated
14645
15214
  - detected
14646
15215
  - question
@@ -14648,7 +15217,7 @@ components:
14648
15217
  - pending_approval
14649
15218
  - working
14650
15219
  - backlog
14651
- description: 'Where automation has taken an item in the investigation lifecycle — the signal the ops cockpit renders as "what AI did" plus the one action expected of the user. It is **written as a side-effect of the actions that already happen**, never authored on its own: linking a PR sets `pr_ready`, moving to `in_progress` sets `working`, `ready_for_qa` sets `ready_for_qa`, a system/assistant reply sets `question`, resolving clears it, and creation seeds `backlog` or `investigated`/`detected` for auto-filed error/alert tickets. `backlog` is the single "nothing has happened yet" state — how urgent that is reads off `priority`, not off a separate state, and its one action is to hand the item to an agent. `pr_merged` is derived client-side by scanning the linked pull request on GitHub, so it is accepted on write but not currently emitted by the API. Absent on rows filed before the field existed — consumers should treat a missing value as "derive from the other fields".'
15220
+ description: 'Where automation has taken an item in the investigation lifecycle — the signal the ops cockpit renders as "what AI did" plus the one action expected of the user. It is **written as a side-effect of the actions that already happen**, never authored on its own: linking a PR sets `pr_ready`, moving to `in_progress` sets `working`, `ready_for_qa` sets `ready_for_qa`, a system/assistant reply sets `question`, resolving clears it, and creation seeds `backlog` for every untouched item (bugs, features, and auto-filed error/alert tickets alike — type is not a signal). `backlog` is the single "nothing has happened yet" state — how urgent that is reads off `priority`, not off a separate state, and its one action is to hand the item to an agent. `investigated`/`detected` are set when a completed agent run leaves findings to review, not at filing time. `pr_merged` and `pr_closed` are written by the GitHub `pull_request` webhook when the linked PR reaches a terminal outcome — merged (the fix shipped, resolve the item) or closed without merging (the fix was rejected, decide what happens next). Absent on rows filed before the field existed — consumers should treat a missing value as "derive from the other fields".'
14652
15221
  OpsBrowserContext:
14653
15222
  type: object
14654
15223
  properties:
@@ -14808,20 +15377,31 @@ components:
14808
15377
  type: string
14809
15378
  enum:
14810
15379
  - metric_rule
15380
+ - killswitch_armed
14811
15381
  - experiment_srm
14812
15382
  - experiment_peek
14813
15383
  - guardrail
14814
- description: What kind of alert transitioned to active.
15384
+ description: What kind of condition opened this alert instance.
14815
15385
  dedupeKey:
14816
15386
  type: string
14817
15387
  description: Stable per-condition key; with `source` forms the ticket's `sourceRef`.
15388
+ detail:
15389
+ anyOf:
15390
+ - type: string
15391
+ - type: 'null'
15392
+ description: The instance's one-line reading — the observed condition in words.
15393
+ ruleId:
15394
+ anyOf:
15395
+ - type: string
15396
+ - type: 'null'
15397
+ description: Id of the alert rule that opened this instance, or `null` for a built-in condition.
14818
15398
  severity:
14819
15399
  type: string
14820
15400
  enum:
14821
15401
  - danger
14822
15402
  - warn
14823
15403
  - info
14824
- description: Alert severity.
15404
+ description: How loud the alert is. Severity belongs to the RULE and is carried by this ticket as `priority`; it is DERIVED from that on the hydrated single-item read, and absent from the raw stored context list rows return.
14825
15405
  observedValue:
14826
15406
  anyOf:
14827
15407
  - type: number
@@ -14840,7 +15420,7 @@ components:
14840
15420
  - resolved
14841
15421
  - dismissed
14842
15422
  - type: 'null'
14843
- description: Current status of the underlying alert (`active` while the condition holds), or `null` if the alert row is gone.
15423
+ description: Firing state of this instance, projected from the ticket's own status (`active` while it is open, i.e. while the condition holds). Absent from the raw stored context list rows return.
14844
15424
  activeSince:
14845
15425
  anyOf:
14846
15426
  - type: string
@@ -14864,9 +15444,8 @@ components:
14864
15444
  required:
14865
15445
  - source
14866
15446
  - dedupeKey
14867
- - severity
14868
15447
  additionalProperties: {}
14869
- description: Hydrated detail for an auto-filed `alert` ticket, resolving the rule → metric → event chain at request time.
15448
+ description: Detail for an `alert` ticket — the fired-alert INSTANCE this ticket is. List rows carry the stored capture subset (`source`, `dedupeKey`, `ruleId`, `detail`, `href`, `observedValue`); the single-item read hydrates the rest, resolving `severity`/`status` off the ticket itself and the rule → metric → event chain at request time.
14870
15449
  MeasurePlanResource:
14871
15450
  type: object
14872
15451
  properties:
@@ -14970,7 +15549,7 @@ components:
14970
15549
  properties:
14971
15550
  t:
14972
15551
  type: number
14973
- description: Bucket start — unix epoch **milliseconds** (hourly buckets).
15552
+ description: Bucket start — unix epoch **milliseconds**. Bucket width scales with the window (hourly up to ~5 days, coarser beyond).
14974
15553
  v:
14975
15554
  type: number
14976
15555
  description: Occurrence count in the bucket (sample-rate weighted).
@@ -14978,11 +15557,11 @@ components:
14978
15557
  - t
14979
15558
  - v
14980
15559
  additionalProperties: {}
14981
- description: One hourly occurrence bucket.
14982
- description: Hourly occurrence buckets, oldest first. Hours with no occurrences are omitted.
15560
+ description: One occurrence bucket.
15561
+ description: Occurrence buckets, oldest first. Buckets with no occurrences are omitted.
14983
15562
  domainFrom:
14984
15563
  type: number
14985
- description: Chart domain start — unix epoch milliseconds (24h before the read).
15564
+ description: Chart domain start — unix epoch milliseconds (the window's start).
14986
15565
  domainTo:
14987
15566
  type: number
14988
15567
  description: Chart domain end — unix epoch milliseconds (the read time).
@@ -14991,7 +15570,7 @@ components:
14991
15570
  - domainFrom
14992
15571
  - domainTo
14993
15572
  additionalProperties: {}
14994
- description: Occurrence timeline for an `error` ticket — hourly buckets over the last 24h from the sampled occurrences, hydrated at read time by the single-item read only. ABSENT (not `null`) on non-error tickets, on list rows (raw context), and when no occurrence landed in the window.
15573
+ description: 'Occurrence timeline for an `error` ticket — buckets over the project''s error auto-close window (Settings → Errors: `errorAutocloseDays`, 7 days when auto-close is off, capped at 30) from the sampled occurrences, hydrated at read time by the single-item read only. Plotting exactly the quiet window that auto-resolves the error is what makes an empty right-hand edge meaningful. ABSENT (not `null`) on non-error tickets, on list rows (raw context), and when no occurrence landed in the window.'
14995
15574
  additionalProperties: {}
14996
15575
  description: Per-type capture context on a queue item. Exactly one of `browser` (bug/feature), `error`, `alert`, or `measurePlan` is populated depending on `type`; other keys (e.g. the AI dedupe block on bugs) may also be present.
14997
15576
  GithubPrLink:
@@ -15015,6 +15594,25 @@ components:
15015
15594
  - closes
15016
15595
  - comment
15017
15596
  description: How the PR was connected to the issue.
15597
+ owner:
15598
+ type: string
15599
+ description: Repository owner (org or user) the pull request lives in. Absent on links recorded before the field existed — `url` still carries it.
15600
+ repo:
15601
+ type: string
15602
+ description: Repository name the pull request lives in. Absent on links recorded before the field existed — `url` still carries it.
15603
+ state:
15604
+ type: string
15605
+ enum:
15606
+ - open
15607
+ - merged
15608
+ - closed
15609
+ description: Outcome of the pull request on GitHub, kept current by the `pull_request` webhook. `merged` and `closed` (closed WITHOUT merging, i.e. rejected) are terminal. Absent means the outcome has never been observed — treat it as `open`.
15610
+ mergedAt:
15611
+ type: string
15612
+ description: ISO-8601 timestamp the pull request merged, when `state` is `merged`.
15613
+ closedAt:
15614
+ type: string
15615
+ description: ISO-8601 timestamp the pull request closed unmerged, when `state` is `closed`.
15018
15616
  required:
15019
15617
  - number
15020
15618
  - url
@@ -15387,6 +15985,11 @@ components:
15387
15985
  additionalProperties: {}
15388
15986
  description: One queue item — any type — plus its resolved `owner` and run state.
15389
15987
  description: A page of queue items, newest first.
15988
+ TagName:
15989
+ type: string
15990
+ minLength: 1
15991
+ maxLength: 64
15992
+ description: A tag name. Trimmed; matched case-insensitively against the project's existing tags.
15390
15993
  CreateBugRequest:
15391
15994
  type: object
15392
15995
  properties:
@@ -15439,10 +16042,11 @@ components:
15439
16042
  description: Emails of teammates to subscribe to this item's Slack pings at creation.
15440
16043
  tags:
15441
16044
  type: array
16045
+ maxItems: 50
15442
16046
  items:
15443
- type: string
16047
+ $ref: '#/components/schemas/TagName'
15444
16048
  default: []
15445
- description: Tag names to attach at creation (get-or-created by name, deduped case-insensitively).
16049
+ description: Tag names to label the item with, e.g. `["ios", "checkout"]`. Each name is matched case-insensitively against the project's existing tags and reuses that tag (keeping its stored casing) when one matches; a name with no match creates a new project tag.
15446
16050
  reporterEmail:
15447
16051
  anyOf:
15448
16052
  - type: string
@@ -15530,10 +16134,11 @@ components:
15530
16134
  description: Emails of teammates to subscribe to this item's Slack pings at creation.
15531
16135
  tags:
15532
16136
  type: array
16137
+ maxItems: 50
15533
16138
  items:
15534
- type: string
16139
+ $ref: '#/components/schemas/TagName'
15535
16140
  default: []
15536
- description: Tag names to attach at creation (get-or-created by name, deduped case-insensitively).
16141
+ description: Tag names to label the item with, e.g. `["ios", "checkout"]`. Each name is matched case-insensitively against the project's existing tags and reuses that tag (keeping its stored casing) when one matches; a name with no match creates a new project tag.
15537
16142
  reporterEmail:
15538
16143
  anyOf:
15539
16144
  - type: string
@@ -15640,10 +16245,11 @@ components:
15640
16245
  description: Emails of teammates to subscribe to this item's Slack pings at creation.
15641
16246
  tags:
15642
16247
  type: array
16248
+ maxItems: 50
15643
16249
  items:
15644
- type: string
16250
+ $ref: '#/components/schemas/TagName'
15645
16251
  default: []
15646
- description: Tag names to attach at creation (get-or-created by name, deduped case-insensitively).
16252
+ description: 'Tag names to label the item with, e.g. `["ios", "checkout"]`. Each name is matched case-insensitively against the project''s existing tags and reuses that tag (keeping its stored casing) when one matches; a name with no match creates a new project tag. Here the tags only label the ticket: the project''s tag rules (owner/status/agent routing) are not applied to a public report while it awaits approval.'
15647
16253
  reporterEmail:
15648
16254
  anyOf:
15649
16255
  - type: string
@@ -15674,6 +16280,10 @@ components:
15674
16280
  additionalProperties: {}
15675
16281
  - type: 'null'
15676
16282
  description: Arbitrary capture context, or `null`.
16283
+ dedupKey:
16284
+ type: string
16285
+ maxLength: 200
16286
+ description: 'Caller-chosen dedupe identity for this report, stored on the ticket. A repeat submission carrying the same key does NOT file a second ticket: the open ticket already holding that key is refreshed from this payload (title and the report fields overwritten, a "re-triggered" comment appended) and returned with `deduped: true` and `updated: true`. Triage state a human owns — status, priority, assignee, tags — is left untouched, and a `resolved`/`wont_fix` ticket no longer holds the key, so a failure that comes back after being closed files a fresh ticket. Omit it to fall back to the derived (title + `context.step`) dedupe, whose repeats return the existing ticket unchanged.'
15677
16287
  notify:
15678
16288
  anyOf:
15679
16289
  - $ref: '#/components/schemas/NotificationTarget'
@@ -15682,7 +16292,7 @@ components:
15682
16292
  required:
15683
16293
  - title
15684
16294
  additionalProperties: false
15685
- description: Body for `POST /ops/bug`. The same bug fields as `CreateBugRequest`, minus the `type` discriminator — the path already says what is being filed.
16295
+ description: Body for `POST /ops/bug`. The same bug fields as `CreateBugRequest`, minus the `type` discriminator — the path already says what is being filed, plus the public intake's own `dedupKey`.
15686
16296
  CreatePublicTicketResponse:
15687
16297
  type: object
15688
16298
  properties:
@@ -15695,10 +16305,13 @@ components:
15695
16305
  deduped:
15696
16306
  type: boolean
15697
16307
  description: '`true` when an open ticket already tracked this report and nothing was filed (HTTP 200). Absent on a fresh file (HTTP 201).'
16308
+ updated:
16309
+ type: boolean
16310
+ description: '`true` when the deduped ticket was REFRESHED from this payload and got a "re-triggered" comment — only ever set alongside `deduped` on a submission that carried a `dedupKey`. Absent when the existing ticket was returned untouched.'
15698
16311
  required:
15699
16312
  - number
15700
16313
  additionalProperties: false
15701
- description: 'Response for the public ticket intake. A fresh file returns `201` with `id` + `number`; a repeat of a report already tracked by an open ticket returns `200` with that ticket''s `number` and `deduped: true`.'
16314
+ description: 'Response for the public ticket intake. A fresh file returns `201` with `id` + `number`; a repeat of a report already tracked by an open ticket returns `200` with that ticket''s `number` and `deduped: true` — plus `updated: true` when a `dedupKey` re-triggered it and its fields were refreshed.'
15702
16315
  CreatePublicFeatureRequestRequest:
15703
16316
  type: object
15704
16317
  properties:
@@ -15741,10 +16354,11 @@ components:
15741
16354
  description: Emails of teammates to subscribe to this item's Slack pings at creation.
15742
16355
  tags:
15743
16356
  type: array
16357
+ maxItems: 50
15744
16358
  items:
15745
- type: string
16359
+ $ref: '#/components/schemas/TagName'
15746
16360
  default: []
15747
- description: Tag names to attach at creation (get-or-created by name, deduped case-insensitively).
16361
+ description: 'Tag names to label the item with, e.g. `["ios", "checkout"]`. Each name is matched case-insensitively against the project''s existing tags and reuses that tag (keeping its stored casing) when one matches; a name with no match creates a new project tag. Here the tags only label the ticket: the project''s tag rules (owner/status/agent routing) are not applied to a public report while it awaits approval.'
15748
16362
  reporterEmail:
15749
16363
  anyOf:
15750
16364
  - type: string
@@ -15769,6 +16383,10 @@ components:
15769
16383
  additionalProperties: {}
15770
16384
  - type: 'null'
15771
16385
  description: Arbitrary capture context, or `null`.
16386
+ dedupKey:
16387
+ type: string
16388
+ maxLength: 200
16389
+ description: 'Caller-chosen dedupe identity for this request, stored on the ticket. Behaves exactly as on `POST /ops/bug`: a repeat carrying the same key refreshes the open ticket already holding it (fields overwritten, a "re-triggered" comment appended) and returns `deduped: true` with `updated: true`, instead of filing a second one.'
15772
16390
  notify:
15773
16391
  anyOf:
15774
16392
  - $ref: '#/components/schemas/NotificationTarget'
@@ -15777,7 +16395,7 @@ components:
15777
16395
  required:
15778
16396
  - title
15779
16397
  additionalProperties: false
15780
- description: Body for `POST /ops/feature-request`. The same feature-request fields as `CreateFeatureRequestRequest`, minus the `type` discriminator — the path already says what is being filed.
16398
+ description: Body for `POST /ops/feature-request`. The same feature-request fields as `CreateFeatureRequestRequest`, minus the `type` discriminator — the path already says what is being filed, plus the public intake's own `dedupKey`.
15781
16399
  OpsItemAttachment:
15782
16400
  type: object
15783
16401
  properties:
@@ -16121,6 +16739,10 @@ components:
16121
16739
  type: string
16122
16740
  maxLength: 300
16123
16741
  description: The agent-run session id (e.g. Claude's `session_01…`), so the dashboard can deep-link to the exact run page. Omit when the harness has no session id.
16742
+ launch:
16743
+ type: boolean
16744
+ default: true
16745
+ description: 'Whether an AI ack may FIRE the agent''s connected trigger. Default `true`: an ack naming a connected agent, with no `sessionId`, starts a new provider run of that agent. Pass `false` to only record the ack — claim the item and open the run record — without starting anything, e.g. when you ARE that agent working the item yourself and have no session id to pass. Ignored on a human ack.'
16124
16746
  additionalProperties: false
16125
16747
  description: Body for `POST /api/admin/ops/{handle}/ack`. With `agent` set this is an AI ack — the named agent type must have a connected trigger connector in the project, or the call fails with `AGENT_NOT_CONNECTED` and instructions to query the available agents (`ops agents list`). `gemini` is accepted as an alias for `jules`. Without `agent` it is a HUMAN ack by the authenticated caller.
16126
16748
  AckOpsItemResponse:
@@ -16542,91 +17164,338 @@ components:
16542
17164
  additionalProperties: false
16543
17165
  minProperties: 1
16544
17166
  description: Body for `PATCH /api/admin/ops/{handle}/investigation/{investigationId}` — a partial update of one existing investigation record (typically the `working` run record you were handed). Send only the fields you want to change; at least one is required.
16545
- OpsAgentProfile:
17167
+ OpsQuestionStatus:
17168
+ type: string
17169
+ enum:
17170
+ - open
17171
+ - completed
17172
+ - cancelled
17173
+ description: Where a question stands — `open` (waiting on a person), `completed` (answered), or `cancelled` (withdrawn by the asker).
17174
+ OpsQuestionAnswer:
16546
17175
  type: object
16547
17176
  properties:
16548
- connectorId:
16549
- type: string
16550
- description: The connector row id — what an agent assignment stores.
16551
- provider:
16552
- type: string
16553
- enum:
16554
- - claude_trigger
16555
- - cursor_trigger
16556
- - copilot_trigger
16557
- - jules_trigger
16558
- description: The trigger-connector provider backing this agent.
16559
- name:
16560
- type: string
16561
- description: Display name — the connector's `name`, or the provider default (e.g. "Claude").
16562
- handle:
16563
- type: string
16564
- description: Mention/search handle — the kebab-cased name (e.g. `claude`).
17177
+ selected:
17178
+ type: array
17179
+ items:
17180
+ type: string
17181
+ description: The option labels the person picked (empty when they only answered "Other").
17182
+ other:
17183
+ anyOf:
17184
+ - type: string
17185
+ - type: 'null'
17186
+ description: The free-text "Other" answer, or `null`.
16565
17187
  required:
16566
- - connectorId
16567
- - provider
16568
- - name
16569
- - handle
17188
+ - selected
17189
+ - other
16570
17190
  additionalProperties: false
16571
- description: One connected, assignable AI agent (an authenticated trigger connector).
16572
- ListOpsAgentsResponse:
16573
- type: array
16574
- items:
16575
- $ref: '#/components/schemas/OpsAgentProfile'
16576
- description: The project's connected AI agents — the agent types `POST /api/admin/ops/{handle}/ack` accepts. Empty when no trigger connector is connected yet.
16577
- OpsCommentAuthorType:
17191
+ description: A person's answer to a question.
17192
+ OpsQuestionDelivery:
16578
17193
  type: string
16579
17194
  enum:
16580
- - user
16581
- - system
16582
- description: Who wrote a comment. `user` is a teammate (their email is in `authorEmail`); `system` is Jarvis — the AI agent — which authors comments when an ops-notify escalation lands on the item and when it is `@shipeasy`-mentioned in a thread.
16583
- OpsComment:
17195
+ - resume
17196
+ - poll
17197
+ description: How the answer gets back to the asker — `resume` re-fires the asking run's trigger connector on the item once every question that run asked is answered; `poll` fires nothing and the asker reads `GET .../questions`.
17198
+ OpsQuestion:
16584
17199
  type: object
16585
17200
  properties:
16586
17201
  id:
16587
17202
  type: string
16588
- description: Comment id.
17203
+ description: Question id.
16589
17204
  feedbackId:
16590
17205
  type: string
16591
- description: The queue item this comment hangs off.
16592
- parentId:
17206
+ description: The queue item the question hangs off.
17207
+ header:
17208
+ type: string
17209
+ description: Short chip label for the question.
17210
+ question:
17211
+ type: string
17212
+ description: The full question (markdown).
17213
+ options:
17214
+ type: array
17215
+ items:
17216
+ type: object
17217
+ properties:
17218
+ label:
17219
+ type: string
17220
+ description: The option's display text.
17221
+ description:
17222
+ anyOf:
17223
+ - type: string
17224
+ - type: 'null'
17225
+ description: What picking the option means, or `null`.
17226
+ required:
17227
+ - label
17228
+ - description
17229
+ additionalProperties: false
17230
+ description: One offered choice.
17231
+ description: The offered choices, in order.
17232
+ multiSelect:
17233
+ type: boolean
17234
+ description: Whether several options may be picked.
17235
+ status:
17236
+ $ref: '#/components/schemas/OpsQuestionStatus'
17237
+ answer:
17238
+ anyOf:
17239
+ - $ref: '#/components/schemas/OpsQuestionAnswer'
17240
+ - type: 'null'
17241
+ description: The person's answer, or `null` while the question is open (or was withdrawn).
17242
+ answeredBy:
16593
17243
  anyOf:
16594
17244
  - type: string
16595
17245
  - type: 'null'
16596
- description: The top-level comment this is a reply to, or `null` for a top-level comment.
16597
- authorType:
16598
- $ref: '#/components/schemas/OpsCommentAuthorType'
16599
- authorEmail:
17246
+ description: Email of the person who answered, or `null`.
17247
+ answeredAt:
16600
17248
  anyOf:
16601
17249
  - type: string
16602
17250
  - type: 'null'
16603
- description: The teammate who wrote it, or `null` for a `system` (Jarvis) comment.
16604
- body:
16605
- type: string
16606
- description: Comment body as markdown (may contain sanitised inline image/video embeds).
17251
+ description: ISO-8601 timestamp of the answer, or `null`.
17252
+ agent:
17253
+ anyOf:
17254
+ - type: string
17255
+ enum:
17256
+ - jarvis
17257
+ - claude
17258
+ - cursor
17259
+ - copilot
17260
+ - jules
17261
+ - type: 'null'
17262
+ description: The agent that asked, or `null` (asked by a person or an unattributed key).
17263
+ askedBy:
17264
+ anyOf:
17265
+ - type: string
17266
+ - type: 'null'
17267
+ description: Email of the person who asked, when a human asked, or `null`.
17268
+ runId:
17269
+ anyOf:
17270
+ - type: string
17271
+ - type: 'null'
17272
+ description: The run (investigation record id) the question was asked under, or `null`.
17273
+ delivery:
17274
+ $ref: '#/components/schemas/OpsQuestionDelivery'
17275
+ deliveredAt:
17276
+ anyOf:
17277
+ - type: string
17278
+ - type: 'null'
17279
+ description: ISO-8601 timestamp the answer was re-delivered to the asking run (`resume` only), or `null`.
17280
+ deliveryError:
17281
+ anyOf:
17282
+ - type: string
17283
+ - type: 'null'
17284
+ description: Why a `resume` re-delivery failed, or `null`.
16607
17285
  createdAt:
16608
17286
  type: string
16609
17287
  description: ISO-8601 creation timestamp.
16610
17288
  updatedAt:
16611
17289
  type: string
16612
- description: ISO-8601 last-edit timestamp.
17290
+ description: ISO-8601 last-update timestamp.
16613
17291
  required:
16614
17292
  - id
16615
17293
  - feedbackId
16616
- - parentId
16617
- - authorType
16618
- - authorEmail
16619
- - body
17294
+ - header
17295
+ - question
17296
+ - options
17297
+ - multiSelect
17298
+ - status
17299
+ - answer
17300
+ - answeredBy
17301
+ - answeredAt
17302
+ - agent
17303
+ - askedBy
17304
+ - runId
17305
+ - delivery
17306
+ - deliveredAt
17307
+ - deliveryError
16620
17308
  - createdAt
16621
17309
  - updatedAt
16622
17310
  additionalProperties: false
16623
- description: A single comment on a queue item.
16624
- ListOpsCommentsResponse:
17311
+ description: One structured question on a queue item and, once answered, its answer.
17312
+ ListOpsQuestionsResponse:
17313
+ type: array
17314
+ items:
17315
+ $ref: '#/components/schemas/OpsQuestion'
17316
+ description: Response for `GET /api/admin/ops/{handle}/questions` — the item's questions, newest first (max 100).
17317
+ OpsQuestionOption:
16625
17318
  type: object
16626
17319
  properties:
16627
- comments:
16628
- type: array
16629
- items:
17320
+ label:
17321
+ type: string
17322
+ minLength: 1
17323
+ maxLength: 120
17324
+ description: The option's display text — short (1–5 words). Answers name options by this label.
17325
+ description:
17326
+ type: string
17327
+ maxLength: 500
17328
+ description: What picking this option means — its trade-off or consequence.
17329
+ required:
17330
+ - label
17331
+ additionalProperties: false
17332
+ description: One choice offered by a question — rendered as a radio, or a checkbox when the question is `multiSelect`.
17333
+ AskOpsQuestionInput:
17334
+ type: object
17335
+ properties:
17336
+ header:
17337
+ type: string
17338
+ minLength: 1
17339
+ maxLength: 24
17340
+ description: Very short chip label for the question (max 24 chars), e.g. `Auth method`.
17341
+ question:
17342
+ type: string
17343
+ minLength: 1
17344
+ maxLength: 2000
17345
+ description: The complete question, ending with a question mark (markdown).
17346
+ options:
17347
+ type: array
17348
+ minItems: 2
17349
+ maxItems: 4
17350
+ items:
17351
+ $ref: '#/components/schemas/OpsQuestionOption'
17352
+ description: 2–4 distinct choices. Do not add an "Other" option — a free-text answer is always offered.
17353
+ multiSelect:
17354
+ type: boolean
17355
+ default: false
17356
+ description: '`true` lets the person pick several options (checkboxes); `false` (the default) exactly one (radios).'
17357
+ required:
17358
+ - header
17359
+ - question
17360
+ - options
17361
+ additionalProperties: false
17362
+ description: One question to ask — the `AskUserQuestion` shape.
17363
+ AskOpsQuestionsRequest:
17364
+ type: object
17365
+ properties:
17366
+ questions:
17367
+ type: array
17368
+ minItems: 1
17369
+ maxItems: 4
17370
+ items:
17371
+ $ref: '#/components/schemas/AskOpsQuestionInput'
17372
+ description: 1–4 questions to ask at once.
17373
+ runId:
17374
+ type: string
17375
+ maxLength: 200
17376
+ description: The run (investigation record id) you are working the item under — the `runId` an ack or a launch handed you. Ties the questions to that run so a `resume` delivery continues it.
17377
+ agent:
17378
+ type: string
17379
+ enum:
17380
+ - jarvis
17381
+ - claude
17382
+ - cursor
17383
+ - copilot
17384
+ - jules
17385
+ description: The agent type asking — pass your own type when you are a coding agent. Defaults to the run's agent.
17386
+ delivery:
17387
+ $ref: '#/components/schemas/OpsQuestionDelivery'
17388
+ required:
17389
+ - questions
17390
+ additionalProperties: false
17391
+ description: Body for `POST /api/admin/ops/{handle}/questions`.
17392
+ AskOpsQuestionsResponse:
17393
+ type: array
17394
+ items:
17395
+ $ref: '#/components/schemas/OpsQuestion'
17396
+ description: Response for `POST /api/admin/ops/{handle}/questions` — the created questions, in the order asked.
17397
+ AnswerOpsQuestionRequest:
17398
+ type: object
17399
+ properties:
17400
+ selected:
17401
+ type: array
17402
+ maxItems: 4
17403
+ items:
17404
+ type: string
17405
+ maxLength: 120
17406
+ description: The option labels picked — exactly one for a single-select question (unless answering only with `other`), one or more for `multiSelect`.
17407
+ other:
17408
+ type: string
17409
+ maxLength: 10000
17410
+ description: A free-text "Other" answer.
17411
+ additionalProperties: false
17412
+ minProperties: 1
17413
+ description: Body for `POST /api/admin/ops/{handle}/questions/{questionId}/answer` — pick option labels, write a free-text answer, or both (multi-select only).
17414
+ OpsAgentProfile:
17415
+ type: object
17416
+ properties:
17417
+ connectorId:
17418
+ type: string
17419
+ description: The connector row id — what an agent assignment stores.
17420
+ provider:
17421
+ type: string
17422
+ enum:
17423
+ - claude_trigger
17424
+ - cursor_trigger
17425
+ - copilot_trigger
17426
+ - jules_trigger
17427
+ description: The trigger-connector provider backing this agent.
17428
+ name:
17429
+ type: string
17430
+ description: Display name — the connector's `name`, or the provider default (e.g. "Claude").
17431
+ handle:
17432
+ type: string
17433
+ description: Mention/search handle — the kebab-cased name (e.g. `claude`).
17434
+ required:
17435
+ - connectorId
17436
+ - provider
17437
+ - name
17438
+ - handle
17439
+ additionalProperties: false
17440
+ description: One connected, assignable AI agent (an authenticated trigger connector).
17441
+ ListOpsAgentsResponse:
17442
+ type: array
17443
+ items:
17444
+ $ref: '#/components/schemas/OpsAgentProfile'
17445
+ description: The project's connected AI agents — the agent types `POST /api/admin/ops/{handle}/ack` accepts. Empty when no trigger connector is connected yet.
17446
+ OpsCommentAuthorType:
17447
+ type: string
17448
+ enum:
17449
+ - user
17450
+ - system
17451
+ description: Who wrote a comment. `user` is a teammate (their email is in `authorEmail`); `system` is Jarvis — the AI agent — which authors comments when an ops-notify escalation lands on the item and when it is `@shipeasy`-mentioned in a thread.
17452
+ OpsComment:
17453
+ type: object
17454
+ properties:
17455
+ id:
17456
+ type: string
17457
+ description: Comment id.
17458
+ feedbackId:
17459
+ type: string
17460
+ description: The queue item this comment hangs off.
17461
+ parentId:
17462
+ anyOf:
17463
+ - type: string
17464
+ - type: 'null'
17465
+ description: The top-level comment this is a reply to, or `null` for a top-level comment.
17466
+ authorType:
17467
+ $ref: '#/components/schemas/OpsCommentAuthorType'
17468
+ authorEmail:
17469
+ anyOf:
17470
+ - type: string
17471
+ - type: 'null'
17472
+ description: The teammate who wrote it, or `null` for a `system` (Jarvis) comment.
17473
+ body:
17474
+ type: string
17475
+ description: Comment body as markdown (may contain sanitised inline image/video embeds).
17476
+ createdAt:
17477
+ type: string
17478
+ description: ISO-8601 creation timestamp.
17479
+ updatedAt:
17480
+ type: string
17481
+ description: ISO-8601 last-edit timestamp.
17482
+ required:
17483
+ - id
17484
+ - feedbackId
17485
+ - parentId
17486
+ - authorType
17487
+ - authorEmail
17488
+ - body
17489
+ - createdAt
17490
+ - updatedAt
17491
+ additionalProperties: false
17492
+ description: A single comment on a queue item.
17493
+ ListOpsCommentsResponse:
17494
+ type: object
17495
+ properties:
17496
+ comments:
17497
+ type: array
17498
+ items:
16630
17499
  $ref: '#/components/schemas/OpsComment'
16631
17500
  required:
16632
17501
  - comments
@@ -16735,6 +17604,27 @@ components:
16735
17604
  - channels
16736
17605
  additionalProperties: false
16737
17606
  description: Response for `GET /api/admin/slack/channels`.
17607
+ AlertCompositeSpec:
17608
+ type: object
17609
+ description: 'What a `composite` rule is a boolean over. `rules` are sibling alert-rule ids in the same project and `op` is how their states combine. A child''s state is what the current pass concluded about it, falling back to whether it has an open instance — so a composite still means something on a tick where a child could not be evaluated, which is exactly the tick a "two of these are broken at once" rule is for. One level deep: a child may not itself be composite.'
17610
+ properties:
17611
+ op:
17612
+ type: string
17613
+ enum:
17614
+ - and
17615
+ - or
17616
+ description: How the children's states combine.
17617
+ rules:
17618
+ type: array
17619
+ minItems: 2
17620
+ items:
17621
+ type: string
17622
+ minLength: 1
17623
+ description: Ids of the alert rules to combine. Stored sorted and de-duplicated.
17624
+ required:
17625
+ - op
17626
+ - rules
17627
+ additionalProperties: false
16738
17628
  ListAlertRulesResponse:
16739
17629
  type: array
16740
17630
  items:
@@ -16754,6 +17644,15 @@ components:
16754
17644
  - type: string
16755
17645
  - type: 'null'
16756
17646
  description: Display name of the bound metric, or `null` if the metric no longer exists.
17647
+ kind:
17648
+ type: string
17649
+ enum:
17650
+ - normal
17651
+ - anomaly
17652
+ - outliers
17653
+ - no_data
17654
+ - composite
17655
+ description: What the rule watches for. `normal` compares the metric's own value — against `threshold` via `comparator`, or against the [`rangeMin`, `rangeMax`] corridor it must stay inside. `anomaly` compares it against ITS OWN seasonal baseline in sigmas, which is the same fit a chart draws as the band. `outliers` compares each `by()` group against its peers in the same bucket, so it needs a grouped metric. `no_data` fires when the metric records nothing at all for `noDataMinutes` — a count of zero is not silence, and the two are distinguished by the reader's presence columns. `composite` is a boolean over OTHER rules' current state and watches no metric of its own.
16757
17656
  comparator:
16758
17657
  type: string
16759
17658
  enum:
@@ -16761,15 +17660,90 @@ components:
16761
17660
  - gte
16762
17661
  - lt
16763
17662
  - lte
16764
- description: How the metric value is compared to the threshold (gt/gte/lt/lte).
17663
+ description: How the metric value is compared to the threshold (gt/gte/lt/lte). Read only by a `normal` rule with no range set.
16765
17664
  threshold:
16766
17665
  type: number
16767
- description: Threshold the metric value is compared against.
17666
+ description: Threshold the metric value is compared against. Read only by a `normal` rule with no range set.
17667
+ rangeMin:
17668
+ anyOf:
17669
+ - type: number
17670
+ - type: 'null'
17671
+ description: Lower edge of the corridor a `normal` rule must stay inside; `null` if the rule is judged by comparator and threshold.
17672
+ rangeMax:
17673
+ anyOf:
17674
+ - type: number
17675
+ - type: 'null'
17676
+ description: Upper edge of the corridor a `normal` rule must stay inside; `null` if the rule is judged by comparator and threshold.
17677
+ sigma:
17678
+ anyOf:
17679
+ - type: number
17680
+ - type: 'null'
17681
+ description: How far from normal is too far, for `anomaly` and `outliers`, in sigma-equivalents. `null` takes the default of 3, which is also the width of the band a chart draws.
17682
+ direction:
17683
+ anyOf:
17684
+ - type: string
17685
+ enum:
17686
+ - above
17687
+ - below
17688
+ - either
17689
+ - type: 'null'
17690
+ description: Which side of the baseline an `anomaly` or `outliers` rule watches. `null` means either.
17691
+ sustained:
17692
+ type: boolean
17693
+ description: Whether the rule is judged by ACCUMULATED departure rather than bucket by bucket. `anomaly` and `outliers` only.
17694
+ warnThreshold:
17695
+ anyOf:
17696
+ - type: number
17697
+ - type: 'null'
17698
+ description: The milder bound, in the same unit the kind judges in. `null` means the rule has one level.
17699
+ recoveryThreshold:
17700
+ anyOf:
17701
+ - type: number
17702
+ - type: 'null'
17703
+ description: What the metric must get back to before a live alert closes. `null` means no hysteresis — the firing bound clears it.
17704
+ groupAlerts:
17705
+ type: boolean
17706
+ description: Whether the rule fires one alert per `by()` group rather than one for the whole metric.
17707
+ maxGroups:
17708
+ anyOf:
17709
+ - type: integer
17710
+ - type: 'null'
17711
+ description: How many groups this rule may alert on at once. `null` takes the shipped cap of 10.
17712
+ noDataMinutes:
17713
+ anyOf:
17714
+ - type: integer
17715
+ - type: 'null'
17716
+ description: How long the silence must last for a `no_data` rule. `null` on every other kind.
17717
+ delayMinutes:
17718
+ anyOf:
17719
+ - type: integer
17720
+ - type: 'null'
17721
+ description: How far behind live the evaluated window is held, on top of the reader's settle grace. `null` means no extra delay.
17722
+ autoResolveMinutes:
17723
+ anyOf:
17724
+ - type: integer
17725
+ - type: 'null'
17726
+ description: Close a live instance that has gone this long without a fresh verdict. `null` never auto-resolves.
17727
+ composite:
17728
+ anyOf:
17729
+ - $ref: '#/components/schemas/AlertCompositeSpec'
17730
+ - type: 'null'
17731
+ description: For a `composite` rule, the child rules and how their states combine. `null` on every other kind.
16768
17732
  windowHours:
16769
17733
  type: integer
16770
17734
  minimum: -9007199254740991
16771
17735
  maximum: 9007199254740991
16772
17736
  description: Lookback window (hours) the metric is aggregated over.
17737
+ bucketMinutes:
17738
+ anyOf:
17739
+ - type: integer
17740
+ - type: 'null'
17741
+ description: Width of the buckets the window is split into, in minutes. `null` divides the window into 12 equal buckets.
17742
+ requiredBuckets:
17743
+ anyOf:
17744
+ - type: integer
17745
+ - type: 'null'
17746
+ description: How many buckets must breach before the rule fires. `null` means every bucket that had data.
16773
17747
  severity:
16774
17748
  type: string
16775
17749
  enum:
@@ -16793,9 +17767,25 @@ components:
16793
17767
  - name
16794
17768
  - metricId
16795
17769
  - metricName
17770
+ - kind
16796
17771
  - comparator
16797
17772
  - threshold
17773
+ - rangeMin
17774
+ - rangeMax
17775
+ - sigma
17776
+ - direction
17777
+ - sustained
17778
+ - warnThreshold
17779
+ - recoveryThreshold
17780
+ - groupAlerts
17781
+ - maxGroups
17782
+ - noDataMinutes
17783
+ - delayMinutes
17784
+ - autoResolveMinutes
17785
+ - composite
16798
17786
  - windowHours
17787
+ - bucketMinutes
17788
+ - requiredBuckets
16799
17789
  - severity
16800
17790
  - enabled
16801
17791
  - notify
@@ -16815,26 +17805,122 @@ components:
16815
17805
  type: string
16816
17806
  minLength: 1
16817
17807
  description: Id of the metric to evaluate.
17808
+ kind:
17809
+ default: normal
17810
+ description: 'What the rule watches for. `normal` compares the metric''s own value — against `threshold` via `comparator`, or against the [`rangeMin`, `rangeMax`] corridor it must stay inside. `anomaly` compares it against its own seasonal baseline in sigmas: the same fit a chart draws as the band, so a point outside the drawn band is exactly a breaching bucket, and one metric can back rules at several sigmas. `outliers` compares each `by()` group against its peers in the same bucket and is refused on a metric with no grouping.'
17811
+ type: string
17812
+ enum:
17813
+ - normal
17814
+ - anomaly
17815
+ - outliers
17816
+ - no_data
17817
+ - composite
16818
17818
  comparator:
17819
+ default: gt
17820
+ description: How the metric value is compared to the threshold. Read only by a `normal` rule with no range set.
16819
17821
  type: string
16820
17822
  enum:
16821
17823
  - gt
16822
17824
  - gte
16823
17825
  - lt
16824
17826
  - lte
16825
- description: How the metric value is compared to the threshold (gt/gte/lt/lte).
16826
17827
  threshold:
16827
17828
  type: number
16828
- description: Threshold the metric value is compared against.
17829
+ description: Threshold the metric value is compared against. Required for a `normal` rule unless a range is given; ignored by every other kind.
17830
+ rangeMin:
17831
+ anyOf:
17832
+ - type: number
17833
+ - type: 'null'
17834
+ description: Lower edge of the corridor the metric must stay inside. Setting either bound switches a `normal` rule off `comparator`/`threshold` and onto the range, which breaches on LEAVING it in either direction. One bound alone is a one-sided range.
17835
+ rangeMax:
17836
+ anyOf:
17837
+ - type: number
17838
+ - type: 'null'
17839
+ description: Upper edge of the corridor the metric must stay inside.
17840
+ sigma:
17841
+ anyOf:
17842
+ - type: number
17843
+ exclusiveMinimum: 0
17844
+ - type: 'null'
17845
+ description: How far from normal is too far, for `anomaly` and `outliers`, in sigma-equivalents. Omit (or `null`) for 3, which is also the half-width of the band a chart draws — so an unconfigured rule fires exactly where the picture says it would.
17846
+ direction:
17847
+ anyOf:
17848
+ - type: string
17849
+ enum:
17850
+ - above
17851
+ - below
17852
+ - either
17853
+ - type: 'null'
17854
+ description: Which side of the baseline an `anomaly` or `outliers` rule watches. Omit (or `null`) for either, which is what "is this unusual" means; name a side for a metric that is only bad in one direction.
17855
+ sustained:
17856
+ default: false
17857
+ type: boolean
17858
+ description: 'Judge the window by ACCUMULATED departure instead of bucket by bucket. The ordinary check asks every bucket to be past `sigma`, which a slow regression never manages: a metric running 1.5σ worse than normal since Tuesday puts no single bucket past 3σ and pages nobody. A sustained rule adds up each bucket''s excess over half a sigma and fires once the total passes twice `sigma` — four buckets at 2σ do it, a lone 3σ spike does not. `anomaly` and `outliers` only, and refused alongside `requiredBuckets`: the accumulated bar IS the evidence bar, and a second one counted in buckets would silently override it.'
16829
17859
  windowHours:
16830
17860
  default: 24
16831
17861
  description: Lookback window (hours) the metric is aggregated over. 1–720.
16832
17862
  type: integer
16833
17863
  minimum: 1
16834
17864
  maximum: 720
17865
+ bucketMinutes:
17866
+ anyOf:
17867
+ - type: integer
17868
+ minimum: 1
17869
+ maximum: 43200
17870
+ - type: 'null'
17871
+ description: Width of the buckets the window is split into, in minutes. Omit (or `null`) to divide the window into 12 equal buckets. The rule is judged per bucket, so this is the resolution at which "sustained" is measured — a short bucket asks the condition to hold through finer detail.
17872
+ requiredBuckets:
17873
+ anyOf:
17874
+ - type: integer
17875
+ minimum: 1
17876
+ - type: 'null'
17877
+ description: How many buckets must breach before the rule fires. Omit (or `null`) to require every bucket that had data. Buckets with nothing in them are excluded before this is counted, so on a sparse metric `null` can mean a single bucket — set this to 2 or more where one lone sample must never page.
17878
+ warnThreshold:
17879
+ anyOf:
17880
+ - type: number
17881
+ - type: 'null'
17882
+ description: 'The milder bound — a second level, in the same unit the kind judges in, that raises a quieter alert before the firing level is reached. Must be strictly milder than the level the rule fires at, or it could never fire on its own. Refused on a range rule and on a `sustained` one: neither condition is a single number, so there is nothing to substitute.'
17883
+ recoveryThreshold:
17884
+ anyOf:
17885
+ - type: number
17886
+ - type: 'null'
17887
+ description: What the metric must get back to before a live alert closes. Without it a metric sitting on its threshold pages and clears once per tick. Must be on the safe side of the firing level, or equal to it. Omit (or `null`) for no hysteresis.
17888
+ groupAlerts:
17889
+ default: false
17890
+ type: boolean
17891
+ description: Fire one alert per `by()` group instead of one for the whole metric. The firing key becomes (rule, group), so each group raises, dedupes and recovers on its own. Refused on a metric with no `by()`, and on `no_data` — a group that went silent has no rows left to be missing from.
17892
+ maxGroups:
17893
+ anyOf:
17894
+ - type: integer
17895
+ minimum: 1
17896
+ maximum: 50
17897
+ - type: 'null'
17898
+ description: How many groups this rule may alert on at once. Omit (or `null`) for 10. Past the cap the worst groups fire and the count of the rest is stated on each ticket — a group set is customer data, and an uncapped rule on `by(user_id)` would file a ticket per user.
17899
+ noDataMinutes:
17900
+ anyOf:
17901
+ - type: integer
17902
+ minimum: 5
17903
+ maximum: 43200
17904
+ - type: 'null'
17905
+ description: 'For a `no_data` rule: how long the silence must last. Omit (or `null`) for 15 minutes. Five is the floor — below it, ordinary ingest lag empties the trailing bucket and reads as an outage.'
17906
+ delayMinutes:
17907
+ anyOf:
17908
+ - type: integer
17909
+ minimum: 1
17910
+ maximum: 1440
17911
+ - type: 'null'
17912
+ description: Hold the evaluated window back this far behind live, on top of the reader's own settle grace. For a metric assembled from a source that lands in batches, whose trailing buckets are legitimately incomplete for longer than the grace covers.
17913
+ autoResolveMinutes:
17914
+ anyOf:
17915
+ - type: integer
17916
+ minimum: 1
17917
+ - type: 'null'
17918
+ description: Close a live instance that has gone this long without a fresh verdict. A rule that cannot reach a verdict deliberately leaves its alert alone, which is right for a tick and wrong for a week. Never closes an instance that is currently breaching. Omit (or `null`) to never auto-resolve.
17919
+ composite:
17920
+ $ref: '#/components/schemas/AlertCompositeSpec'
16835
17921
  severity:
16836
17922
  default: warn
16837
- description: Severity of the raised alert.
17923
+ description: Severity of the raised alert. A `warnThreshold` breach opens one step quieter than this.
16838
17924
  type: string
16839
17925
  enum:
16840
17926
  - danger
@@ -16849,8 +17935,6 @@ components:
16849
17935
  required:
16850
17936
  - name
16851
17937
  - metricId
16852
- - comparator
16853
- - threshold
16854
17938
  additionalProperties: false
16855
17939
  CreateAlertRuleResponse:
16856
17940
  type: object
@@ -16877,6 +17961,14 @@ components:
16877
17961
  type: string
16878
17962
  minLength: 1
16879
17963
  maxLength: 120
17964
+ kind:
17965
+ type: string
17966
+ enum:
17967
+ - normal
17968
+ - anomaly
17969
+ - outliers
17970
+ - no_data
17971
+ - composite
16880
17972
  comparator:
16881
17973
  type: string
16882
17974
  enum:
@@ -16886,10 +17978,93 @@ components:
16886
17978
  - lte
16887
17979
  threshold:
16888
17980
  type: number
17981
+ rangeMin:
17982
+ anyOf:
17983
+ - type: number
17984
+ - type: 'null'
17985
+ description: Lower edge of the corridor; `null` drops it, returning the rule to comparator and threshold when both bounds are gone.
17986
+ rangeMax:
17987
+ anyOf:
17988
+ - type: number
17989
+ - type: 'null'
17990
+ description: Upper edge of the corridor; `null` drops it.
17991
+ sigma:
17992
+ anyOf:
17993
+ - type: number
17994
+ exclusiveMinimum: 0
17995
+ - type: 'null'
17996
+ description: Sigmas an `anomaly` or `outliers` rule fires at; `null` restores the default of 3.
17997
+ direction:
17998
+ anyOf:
17999
+ - type: string
18000
+ enum:
18001
+ - above
18002
+ - below
18003
+ - either
18004
+ - type: 'null'
18005
+ description: Which side an `anomaly` or `outliers` rule watches; `null` restores either.
18006
+ sustained:
18007
+ type: boolean
18008
+ description: Judge the window by accumulated departure rather than bucket by bucket. `anomaly` and `outliers` only; refused alongside `requiredBuckets`.
18009
+ warnThreshold:
18010
+ anyOf:
18011
+ - type: number
18012
+ - type: 'null'
18013
+ description: The milder bound; `null` drops the warning level, leaving the rule with one.
18014
+ recoveryThreshold:
18015
+ anyOf:
18016
+ - type: number
18017
+ - type: 'null'
18018
+ description: What the metric must get back to before a live alert closes; `null` drops the hysteresis.
18019
+ groupAlerts:
18020
+ type: boolean
18021
+ description: Fire one alert per `by()` group. Refused on a metric with no grouping.
18022
+ maxGroups:
18023
+ anyOf:
18024
+ - type: integer
18025
+ minimum: 1
18026
+ maximum: 50
18027
+ - type: 'null'
18028
+ description: How many groups may alert at once; `null` restores the default of 10.
18029
+ noDataMinutes:
18030
+ anyOf:
18031
+ - type: integer
18032
+ minimum: 5
18033
+ maximum: 43200
18034
+ - type: 'null'
18035
+ description: How long a `no_data` rule's silence must last; `null` restores 15 minutes.
18036
+ delayMinutes:
18037
+ anyOf:
18038
+ - type: integer
18039
+ minimum: 1
18040
+ maximum: 1440
18041
+ - type: 'null'
18042
+ description: How far behind live the window is held; `null` drops the delay.
18043
+ autoResolveMinutes:
18044
+ anyOf:
18045
+ - type: integer
18046
+ minimum: 1
18047
+ - type: 'null'
18048
+ description: Close a stale live instance after this long; `null` never auto-resolves.
18049
+ composite:
18050
+ $ref: '#/components/schemas/AlertCompositeSpec'
16889
18051
  windowHours:
16890
18052
  type: integer
16891
18053
  minimum: 1
16892
18054
  maximum: 720
18055
+ bucketMinutes:
18056
+ anyOf:
18057
+ - type: integer
18058
+ minimum: 1
18059
+ maximum: 43200
18060
+ - type: 'null'
18061
+ description: Bucket width in minutes; `null` restores the default 12 buckets per window.
18062
+ requiredBuckets:
18063
+ anyOf:
18064
+ - type: integer
18065
+ minimum: 1
18066
+ - type: 'null'
18067
+ description: Buckets that must breach to fire; `null` restores "every bucket that had data".
16893
18068
  severity:
16894
18069
  type: string
16895
18070
  enum:
@@ -16913,13 +18088,15 @@ components:
16913
18088
  AlertApiRow:
16914
18089
  type: object
16915
18090
  description: |-
16916
- One FIRED alert — a condition the platform observed and raised, not the rule that defines it (alert *rules* are the `/api/admin/alert-rules` resource). Alerts are never filed by hand: the UI (killswitch flips) and the worker (analysis consumer + alerts cron) raise them, keyed by `(source, dedupeKey)` so re-raising refreshes the active row and a clearing condition auto-resolves it.
18091
+ One FIRED alert — an INSTANCE of a condition the platform observed, not the rule that defines it (alert *rules* are the `/api/admin/alert-rules` resource). Instances are never filed by hand: the UI (killswitch flips) and the worker (analysis consumer + alerts cron) open them, keyed by `(source, dedupeKey)`, and at most one instance per condition is open at a time — re-raising refreshes the open one, and the condition clearing closes it.
18092
+
18093
+ An instance IS an ops queue item (`/api/admin/ops` with `type: "alert"`) seen through the alert lens: same `id`, `severity` is that item's `priority`, and `status` is its work status projected onto three states. Resolving the queue item and clearing the alert are therefore the same act.
16917
18094
 
16918
18095
  Every field is always present on the wire; nullable fields are `null`, never absent.
16919
18096
  properties:
16920
18097
  id:
16921
18098
  type: string
16922
- description: Stable opaque alert id.
18099
+ description: Stable opaque instance id — also the id of the ops queue item this instance is.
16923
18100
  source:
16924
18101
  type: string
16925
18102
  enum:
@@ -16940,10 +18117,10 @@ components:
16940
18117
  - danger
16941
18118
  - warn
16942
18119
  - info
16943
- description: How loud the alert is — `danger` pages, `warn` is the default, `info` is FYI-only.
18120
+ description: How loud the alert is — `danger` pages, `warn` is the default, `info` is FYI-only. Severity belongs to the RULE and seeds the instance's queue `priority` (`danger`→`critical`, `warn`→`high`, `info`→`medium`); it is read back from there, so re-prioritising the queue item changes what this reports.
16944
18121
  dedupeKey:
16945
18122
  type: string
16946
- description: Idempotency + reconcile key within `(project, source)` — re-raising the same condition refreshes the existing active row instead of stacking duplicates, and the condition clearing flips that row to `resolved`.
18123
+ description: Idempotency + reconcile key within `(project, source)` — re-raising the same condition refreshes the open instance instead of stacking duplicates, and the condition clearing closes it. A later firing of the same condition opens a NEW instance with its own `id`.
16947
18124
  title:
16948
18125
  type: string
16949
18126
  description: Short human-readable headline (what fired).
@@ -16966,10 +18143,10 @@ components:
16966
18143
  - active
16967
18144
  - resolved
16968
18145
  - dismissed
16969
- description: Lifecycle state. `active` means the condition currently holds (shown as "Firing" in the UI); `resolved` means it cleared (auto, when the raiser reconciles, or by hand); `dismissed` means a human waved it off.
18146
+ description: Lifecycle state, projected from the queue item's work status. `active` means the item is still open, i.e. the condition holds (shown as "Firing" in the UI); `resolved` means it cleared (auto, when the raiser reconciles, or by hand); `dismissed` means a human waved it off (the item is closed `wont_fix`).
16970
18147
  createdAt:
16971
18148
  type: string
16972
- description: ISO-8601 timestamp the alert was first raised.
18149
+ description: ISO-8601 timestamp this instance opened — when the condition started firing.
16973
18150
  resolvedAt:
16974
18151
  description: ISO-8601 timestamp the alert flipped to `resolved`, or `null` while it hasn't.
16975
18152
  anyOf:
@@ -17031,7 +18208,7 @@ components:
17031
18208
  - active
17032
18209
  - resolved
17033
18210
  - dismissed
17034
- description: New lifecycle state. `resolved` / `dismissed` stamp their timestamp; `active` re-opens and clears both.
18211
+ description: New lifecycle state, written through to the queue item (`resolved` / `wont_fix` / `open`). `resolved` / `dismissed` stamp their timestamp; `active` re-opens and clears both.
17035
18212
  assigneeId:
17036
18213
  description: PERSON owner — a `users.id`, or `null` to clear the assignment.
17037
18214
  anyOf:
@@ -17217,6 +18394,12 @@ components:
17217
18394
  - type: boolean
17218
18395
  - type: number
17219
18396
  description: Whether key-authenticated public ticket creation (`POST /cli/report`) is allowed into this project. Off by default.
18397
+ errorAutocloseDays:
18398
+ type: integer
18399
+ description: Days an `open` tracked error may go unseen before the nightly sweep auto-resolves it. `0` disables auto-close for this project. Ingest reopens a resolved error the moment it recurs, so this never hides a live issue.
18400
+ errorTicketMinOccurrences:
18401
+ type: integer
18402
+ description: Occurrence count a tracked error must cross before an `error` ticket is auto-filed into the ops queue. Customising it requires a paid plan; unentitled projects always run at the platform default of 10.
17220
18403
  stripeSubscriptionId:
17221
18404
  anyOf:
17222
18405
  - type: string
@@ -17296,6 +18479,8 @@ components:
17296
18479
  - moduleUser
17297
18480
  - moduleEvents
17298
18481
  - allowPublicTickets
18482
+ - errorAutocloseDays
18483
+ - errorTicketMinOccurrences
17299
18484
  - slug
17300
18485
  - logo
17301
18486
  - defaultEnv
@@ -17514,6 +18699,16 @@ components:
17514
18699
  minimum: 0.0001
17515
18700
  maximum: 0.05
17516
18701
  description: SRM chi-square p-value below which the run is called invalid.
18702
+ errorAutocloseDays:
18703
+ type: integer
18704
+ minimum: 0
18705
+ maximum: 365
18706
+ description: Days an `open` tracked error may go unseen before the nightly sweep auto-resolves it. `0` disables auto-close for this project.
18707
+ errorTicketMinOccurrences:
18708
+ type: integer
18709
+ minimum: 1
18710
+ maximum: 10000
18711
+ description: Occurrence count a tracked error must cross before an `error` ticket is auto-filed into the ops queue. Requires a paid plan — the request is rejected with 403 on a plan without the `ops_auto_error_issues` entitlement.
17517
18712
  additionalProperties: false
17518
18713
  description: Body for `PATCH /api/admin/projects/{id}`. Partial — every field is optional; only supplied fields change. The project id in the path must match the caller's own project.
17519
18714
  ListI18nProfilesResponse:
@@ -18582,21 +19777,50 @@ components:
18582
19777
  - config
18583
19778
  additionalProperties: false
18584
19779
  description: Create a Claude Code routine trigger in one shot. Idempotent by `config.routineId` — re-creating with the same routine id updates the existing row.
19780
+ CursorTriggerRepo:
19781
+ type: object
19782
+ properties:
19783
+ url:
19784
+ type: string
19785
+ format: uri
19786
+ description: Repo the cloud agent runs against, e.g. `https://github.com/owner/repo`.
19787
+ startingRef:
19788
+ type: string
19789
+ minLength: 1
19790
+ description: Optional branch this repo's runs start from. Omit for the repo's default branch.
19791
+ required:
19792
+ - url
19793
+ additionalProperties: false
19794
+ description: One repo a trigger runs against, with the branch its runs start from.
18585
19795
  CursorTriggerConfig:
18586
19796
  type: object
18587
19797
  properties:
18588
19798
  repoUrl:
18589
19799
  type: string
18590
19800
  format: uri
18591
- description: Repo the cloud agent runs against, e.g. `https://github.com/owner/repo`. Idempotency key for the connector.
19801
+ description: '**First** repo the cloud agent runs against, e.g. `https://github.com/owner/repo`. Idempotency key for the connector, and always a mirror of `repos[0].url` when `repos` is supplied.'
18592
19802
  startingRef:
18593
19803
  type: string
18594
19804
  minLength: 1
18595
- description: Optional git ref the run starts from.
19805
+ description: Optional git ref the run starts from. Mirrors `repos[0].startingRef`. Pinned onto the Cursor agent at creation, so changing it provisions a replacement agent.
19806
+ repos:
19807
+ type: array
19808
+ minItems: 1
19809
+ items:
19810
+ $ref: '#/components/schemas/CursorTriggerRepo'
19811
+ description: Every repo this trigger runs against, each with its own starting branch — Cursor launches ONE agent across the whole set. Omit for a single-repo trigger, which is then described by `repoUrl`/`startingRef` alone; when supplied, the first entry must match them. Changing the set re-provisions the agent (its repos are pinned at creation).
18596
19812
  projectId:
18597
19813
  type: string
18598
19814
  minLength: 1
18599
19815
  description: Shipeasy project id the run targets.
19816
+ agentId:
19817
+ type: string
19818
+ minLength: 1
19819
+ description: Server-managed. The reusable Cursor cloud agent (`bc-…`) Shipeasy provisions once at connect and then starts every run on. A supplied value is ignored on create; if Cursor stops recognising the id (404), the next fire provisions a replacement and stores it here.
19820
+ fireText:
19821
+ type: string
19822
+ minLength: 1
19823
+ description: Optional standing instruction prepended to every run's prompt — the trigger's base prompt. Lets two triggers on different repos steer their runs differently without changing the `/shipeasy-ops-work` task block.
18600
19824
  required:
18601
19825
  - repoUrl
18602
19826
  - projectId
@@ -18631,7 +19855,7 @@ components:
18631
19855
  opsKey:
18632
19856
  type: string
18633
19857
  minLength: 1
18634
- description: Restricted Shipeasy ops key, injected into the run as `SHIPEASY_CLI_TOKEN` via the launch envVars (secret). Encrypted; never returned.
19858
+ description: Restricted Shipeasy ops key (secret). Sent as the Bearer for the Shipeasy MCP server handed to each run inline — never in the prompt text or the run env. Encrypted; never returned.
18635
19859
  enabled:
18636
19860
  type: boolean
18637
19861
  default: true
@@ -18643,13 +19867,33 @@ components:
18643
19867
  - opsKey
18644
19868
  additionalProperties: false
18645
19869
  description: Create a Cursor cloud-agent trigger. Both keys are required up front (never tokenless). Idempotent by `config.repoUrl`.
19870
+ CopilotTriggerRepo:
19871
+ type: object
19872
+ properties:
19873
+ owner:
19874
+ type: string
19875
+ minLength: 1
19876
+ description: GitHub repo owner.
19877
+ repo:
19878
+ type: string
19879
+ minLength: 1
19880
+ description: GitHub repo name.
19881
+ baseRef:
19882
+ type: string
19883
+ minLength: 1
19884
+ description: Optional base ref this repo's agent task branches from. Omit for the repo's default branch.
19885
+ required:
19886
+ - owner
19887
+ - repo
19888
+ additionalProperties: false
19889
+ description: One repo a Copilot trigger runs against, with the base branch its tasks branch from.
18646
19890
  CopilotTriggerConfig:
18647
19891
  type: object
18648
19892
  properties:
18649
19893
  owner:
18650
19894
  type: string
18651
19895
  minLength: 1
18652
- description: GitHub repo owner. Together with `repo` forms the connector's idempotency key (`owner/repo`).
19896
+ description: '**First** GitHub repo owner. Together with `repo` forms the connector''s idempotency key (`owner/repo`), and both mirror `repos[0]` when `repos` is supplied.'
18653
19897
  repo:
18654
19898
  type: string
18655
19899
  minLength: 1
@@ -18657,11 +19901,21 @@ components:
18657
19901
  baseRef:
18658
19902
  type: string
18659
19903
  minLength: 1
18660
- description: Optional base ref the agent branches from.
19904
+ description: Optional base ref the agent branches from. Mirrors `repos[0].baseRef`.
19905
+ repos:
19906
+ type: array
19907
+ minItems: 1
19908
+ items:
19909
+ $ref: '#/components/schemas/CopilotTriggerRepo'
19910
+ description: Every repo this trigger runs against, each with its own base branch. GitHub's agent-tasks API is per-repo, so ONE fire starts one task per entry. Omit for a single-repo trigger, which is then described by `owner`/`repo`/`baseRef` alone; when supplied, the first entry must match them.
18661
19911
  projectId:
18662
19912
  type: string
18663
19913
  minLength: 1
18664
19914
  description: Shipeasy project id the run targets.
19915
+ fireText:
19916
+ type: string
19917
+ minLength: 1
19918
+ description: Optional standing instruction prepended to every run's prompt — the trigger's base prompt. Lets two triggers on different repos steer their runs differently without changing the `/shipeasy-ops-work` task block.
18665
19919
  required:
18666
19920
  - owner
18667
19921
  - repo
@@ -18719,6 +19973,10 @@ components:
18719
19973
  type: string
18720
19974
  minLength: 1
18721
19975
  description: Shipeasy project id the run targets.
19976
+ fireText:
19977
+ type: string
19978
+ minLength: 1
19979
+ description: Optional standing instruction prepended to every run's prompt — the trigger's base prompt. Lets two triggers on different repos steer their runs differently without changing the `/shipeasy-ops-work` task block.
18722
19980
  required:
18723
19981
  - source
18724
19982
  - projectId
@@ -18918,7 +20176,7 @@ components:
18918
20176
  - provider
18919
20177
  - config
18920
20178
  additionalProperties: false
18921
- description: Per-provider PATCH body for editing an existing `cursor_trigger` connector. `config` replaces the stored config wholesale; `name` and either credential are optional (a supplied credential rotates just that key).
20179
+ description: 'Per-provider PATCH body for editing an existing `cursor_trigger` connector. `config` replaces the stored config wholesale, except for the server-managed keys: the provisioned `agentId` is carried over (and dropped, so a replacement agent is provisioned, when `repoUrl`/`startingRef` change), as is the last-run pointer. `name` and either credential are optional (a supplied credential rotates just that key).'
18922
20180
  UpdateCopilotTriggerRequest:
18923
20181
  type: object
18924
20182
  properties:
@@ -19044,6 +20302,11 @@ components:
19044
20302
  - type: string
19045
20303
  - type: 'null'
19046
20304
  description: Human label for the key. `null` for unnamed dashboard mints; programmatic mints auto-name themselves.
20305
+ description:
20306
+ anyOf:
20307
+ - type: string
20308
+ - type: 'null'
20309
+ description: Free-text note recording what the key is for (the service that holds it, the rotation owner, the ticket it was minted under). Display only — `null` unless someone set it via `PATCH /api/admin/keys/{id}`.
19047
20310
  scopes:
19048
20311
  anyOf:
19049
20312
  - type: array
@@ -19065,6 +20328,7 @@ components:
19065
20328
  - expires_at
19066
20329
  - created_by_email
19067
20330
  - name
20331
+ - description
19068
20332
  - scopes
19069
20333
  - last4
19070
20334
  additionalProperties: false
@@ -19172,6 +20436,30 @@ components:
19172
20436
  - key
19173
20437
  - expires_at
19174
20438
  additionalProperties: false
20439
+ UpdateKeyRequest:
20440
+ type: object
20441
+ description: |-
20442
+ Body for `PATCH /api/admin/keys/{id}`. Only the key's **label** is editable — `name` and `description`.
20443
+
20444
+ Everything that gives a key its authority (`type`, `env`, `scopes`, `expires_at`) is fixed at mint: those are the isolation boundary the worker derives the read env and permissions from, so changing them would silently re-point a credential that is already deployed. Rotate instead — mint a replacement and revoke the old key.
20445
+
20446
+ Omitted fields are left untouched; send `null` to clear one.
20447
+ properties:
20448
+ name:
20449
+ anyOf:
20450
+ - type: string
20451
+ minLength: 1
20452
+ maxLength: 160
20453
+ - type: 'null'
20454
+ description: New human label, or `null` to clear it. Same 160-char budget as create — provenance labels are composed, not typed.
20455
+ description:
20456
+ anyOf:
20457
+ - type: string
20458
+ minLength: 1
20459
+ maxLength: 500
20460
+ - type: 'null'
20461
+ description: New free-text note, or `null` to clear it.
20462
+ additionalProperties: false
19175
20463
  RevokeKeyResponse:
19176
20464
  type: object
19177
20465
  description: Confirms the key is revoked. Idempotent — revoking an already-revoked key returns the same shape.
@@ -19232,6 +20520,85 @@ components:
19232
20520
  required:
19233
20521
  - hits
19234
20522
  additionalProperties: false
20523
+ Tag:
20524
+ type: object
20525
+ properties:
20526
+ id:
20527
+ type: string
20528
+ description: Stable opaque tag id.
20529
+ name:
20530
+ type: string
20531
+ description: Display name — also the tag's identity, unique case-insensitively within the project.
20532
+ required:
20533
+ - id
20534
+ - name
20535
+ additionalProperties: false
20536
+ ListTagsResponse:
20537
+ type: array
20538
+ items:
20539
+ $ref: '#/components/schemas/Tag'
20540
+ description: The project's tags, sorted by name.
20541
+ TaggableEntityType:
20542
+ type: string
20543
+ enum:
20544
+ - ops
20545
+ - flag
20546
+ - config
20547
+ - killswitch
20548
+ - metric
20549
+ - error
20550
+ - alert_rule
20551
+ - experiment
20552
+ description: 'The kind of item a tag set hangs off: `ops` (any queue item — bug, feature request, error/alert/measure-plan ticket), `flag` (feature gate), `config` (dynamic config), `killswitch`, `metric`, `error` (tracked error group), `alert_rule`, or `experiment`.'
20553
+ EntityTagsResponse:
20554
+ type: object
20555
+ properties:
20556
+ entityType:
20557
+ $ref: '#/components/schemas/TaggableEntityType'
20558
+ entityId:
20559
+ type: string
20560
+ description: The item's canonical id (the handle you passed, resolved).
20561
+ tags:
20562
+ type: array
20563
+ items:
20564
+ type: string
20565
+ description: The tag names now attached to the item, sorted by name.
20566
+ required:
20567
+ - entityType
20568
+ - entityId
20569
+ - tags
20570
+ additionalProperties: false
20571
+ SetEntityTagsRequest:
20572
+ type: object
20573
+ properties:
20574
+ tags:
20575
+ type: array
20576
+ maxItems: 50
20577
+ items:
20578
+ $ref: '#/components/schemas/TagName'
20579
+ description: The complete tag set the item should carry — tags not listed are detached. Names are get-or-created (a new name mints a project tag), deduped case-insensitively; `[]` clears every tag.
20580
+ required:
20581
+ - tags
20582
+ additionalProperties: false
20583
+ UpdateEntityTagsRequest:
20584
+ type: object
20585
+ properties:
20586
+ add:
20587
+ type: array
20588
+ maxItems: 50
20589
+ items:
20590
+ $ref: '#/components/schemas/TagName'
20591
+ default: []
20592
+ description: Tag names to attach (get-or-created by name, case-insensitive). Already-attached names are a no-op.
20593
+ remove:
20594
+ type: array
20595
+ maxItems: 50
20596
+ items:
20597
+ $ref: '#/components/schemas/TagName'
20598
+ default: []
20599
+ description: Tag names to detach (case-insensitive). Names the item doesn't carry are a no-op. A name in both lists is removed.
20600
+ additionalProperties: false
20601
+ description: Incremental tag edit — pass `add`, `remove`, or both. Every other tag on the item is left as is.
19235
20602
  responses:
19236
20603
  BadRequest:
19237
20604
  description: The request was malformed (bad JSON or missing project scope).