@shipeasy/openapi 3.1.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 (45) hide show
  1. package/README.md +1 -1
  2. package/dist/{chunk-V4ZESAIF.js → chunk-75Z4SQOM.js} +141 -1
  3. package/dist/chunk-75Z4SQOM.js.map +1 -0
  4. package/dist/{chunk-UV333BIK.js → chunk-ZYK74ZIW.js} +569 -119
  5. package/dist/chunk-ZYK74ZIW.js.map +1 -0
  6. package/dist/{client-yqGYju-M.d.ts → client-DAGqRpF1.d.ts} +2209 -401
  7. package/dist/client.d.ts +1 -1
  8. package/dist/client.js +33 -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 +34 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/schemas.d.ts +2007 -891
  16. package/dist/schemas.js +179 -1
  17. package/openapi-sdk.json +1769 -0
  18. package/openapi.json +5861 -2597
  19. package/openapi.yaml +2739 -452
  20. package/package.json +6 -4
  21. package/spec/components/responses.yaml +39 -0
  22. package/spec/components/schemas/alert-rules.yaml +405 -7
  23. package/spec/components/schemas/alerts.yaml +9 -7
  24. package/spec/components/schemas/common.yaml +2 -2
  25. package/spec/components/schemas/connectors.yaml +69 -6
  26. package/spec/components/schemas/events.yaml +20 -9
  27. package/spec/components/schemas/gates.yaml +87 -0
  28. package/spec/components/schemas/keys.yaml +32 -2
  29. package/spec/components/schemas/killswitches.yaml +64 -0
  30. package/spec/components/schemas/metrics.yaml +131 -251
  31. package/spec/components/schemas/ops.yaml +555 -22
  32. package/spec/components/schemas/projects.yaml +18 -0
  33. package/spec/components/schemas/tags.yaml +91 -0
  34. package/spec/openapi-sdk.yaml +86 -0
  35. package/spec/openapi.yaml +61 -0
  36. package/spec/paths/alerts.yaml +5 -3
  37. package/spec/paths/gates.yaml +282 -0
  38. package/spec/paths/i18n.yaml +2 -1
  39. package/spec/paths/keys.yaml +79 -0
  40. package/spec/paths/killswitches.yaml +96 -0
  41. package/spec/paths/metrics.yaml +26 -17
  42. package/spec/paths/ops.yaml +566 -10
  43. package/spec/paths/tags.yaml +270 -0
  44. package/dist/chunk-UV333BIK.js.map +0 -1
  45. package/dist/chunk-V4ZESAIF.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
@@ -877,6 +908,288 @@ paths:
877
908
  name: activity
878
909
  positional:
879
910
  - id
911
+ /api/admin/gates/{id}/whitelist:
912
+ get:
913
+ operationId: getGateWhitelist
914
+ summary: Read a gate's whitelist
915
+ description: |-
916
+ Returns the gate's whitelist — the always-first allowlist that admits the listed identities before any targeting rule or percentage rollout is evaluated.
917
+
918
+ A gate with no whitelist returns `entries: []` (and the default `attr`), never a 404 — so a caller can read-then-write without special-casing the empty gate.
919
+
920
+ **Use case:** Check whether an account is already let through before adding it.
921
+ tags:
922
+ - Flags
923
+ parameters:
924
+ - $ref: '#/components/parameters/ProjectId'
925
+ - name: id
926
+ in: path
927
+ required: true
928
+ description: Stable opaque gate id (`gate_…`) or the gate's `name`.
929
+ schema:
930
+ $ref: '#/components/schemas/ResourceId'
931
+ responses:
932
+ '200':
933
+ description: Read a gate's whitelist
934
+ content:
935
+ application/json:
936
+ schema:
937
+ $ref: '#/components/schemas/GateWhitelist'
938
+ example:
939
+ id: gate_01j7w8a1b2c3d4e5f6g7h8i9j0
940
+ name: new_checkout
941
+ attr: email
942
+ entries:
943
+ - alice@acme.dev
944
+ - bob@acme.dev
945
+ '400':
946
+ $ref: '#/components/responses/BadRequest'
947
+ '401':
948
+ $ref: '#/components/responses/Unauthorized'
949
+ '403':
950
+ $ref: '#/components/responses/Forbidden'
951
+ '404':
952
+ $ref: '#/components/responses/NotFound'
953
+ '409':
954
+ $ref: '#/components/responses/Conflict'
955
+ '422':
956
+ $ref: '#/components/responses/UnprocessableEntity'
957
+ x-error-codes:
958
+ - BAD_REQUEST
959
+ - UNAUTHORIZED
960
+ - FORBIDDEN
961
+ - NOT_FOUND
962
+ x-cli:
963
+ name: whitelist
964
+ positional:
965
+ - id
966
+ put:
967
+ operationId: setGateWhitelist
968
+ summary: Replace a gate's whitelist
969
+ description: |-
970
+ Replaces the gate's whole whitelist with `entries`, creating the block if the gate didn't have one. Idempotent — the same call twice leaves the same list.
971
+
972
+ This is the only whitelist call that can switch `attr` (`email` ⇄ `user_id`) or clear the block: `entries: []` removes the whitelist from the gate entirely.
973
+
974
+ **Use cases**
975
+
976
+ - **Pin an exact list** — `{ "entries": ["alice@acme.dev", "bob@acme.dev"] }`.
977
+ - **Switch to user ids** — `{ "attr": "user_id", "entries": ["usr_123"] }`.
978
+ - **Drop the whitelist** — `{ "entries": [] }`.
979
+ tags:
980
+ - Flags
981
+ parameters:
982
+ - $ref: '#/components/parameters/ProjectId'
983
+ - name: id
984
+ in: path
985
+ required: true
986
+ description: Stable opaque gate id (`gate_…`) or the gate's `name`.
987
+ schema:
988
+ $ref: '#/components/schemas/ResourceId'
989
+ requestBody:
990
+ required: true
991
+ content:
992
+ application/json:
993
+ schema:
994
+ $ref: '#/components/schemas/SetGateWhitelistRequest'
995
+ examples:
996
+ pinList:
997
+ summary: Pin an exact list of emails
998
+ value:
999
+ entries:
1000
+ - alice@acme.dev
1001
+ - bob@acme.dev
1002
+ switchToUserIds:
1003
+ summary: Re-key the whitelist onto user ids
1004
+ description: Switching `attr` replaces the entries wholesale — the old emails are not translated.
1005
+ value:
1006
+ attr: user_id
1007
+ entries:
1008
+ - usr_01j7w8a1b2c3d4e5f6g7h8i9j0
1009
+ clear:
1010
+ summary: Remove the whitelist
1011
+ description: An empty list drops the pinned block from the gate's stack.
1012
+ value:
1013
+ entries: []
1014
+ responses:
1015
+ '200':
1016
+ description: Replace a gate's whitelist
1017
+ content:
1018
+ application/json:
1019
+ schema:
1020
+ $ref: '#/components/schemas/GateWhitelist'
1021
+ example:
1022
+ id: gate_01j7w8a1b2c3d4e5f6g7h8i9j0
1023
+ name: new_checkout
1024
+ attr: email
1025
+ entries:
1026
+ - alice@acme.dev
1027
+ - bob@acme.dev
1028
+ '400':
1029
+ $ref: '#/components/responses/BadRequest'
1030
+ '401':
1031
+ $ref: '#/components/responses/Unauthorized'
1032
+ '403':
1033
+ $ref: '#/components/responses/Forbidden'
1034
+ '404':
1035
+ $ref: '#/components/responses/NotFound'
1036
+ '409':
1037
+ $ref: '#/components/responses/Conflict'
1038
+ '422':
1039
+ $ref: '#/components/responses/UnprocessableEntity'
1040
+ x-error-codes:
1041
+ - BAD_REQUEST
1042
+ - UNAUTHORIZED
1043
+ - FORBIDDEN
1044
+ - NOT_FOUND
1045
+ - VALIDATION
1046
+ x-cli:
1047
+ name: whitelist-set
1048
+ positional:
1049
+ - id
1050
+ post:
1051
+ operationId: addToGateWhitelist
1052
+ summary: Add entries to a gate's whitelist
1053
+ description: |-
1054
+ Adds identities to the gate's whitelist, creating the block if the gate doesn't have one yet. Entries already on the list are skipped, so the call is idempotent and safe to retry.
1055
+
1056
+ Adding to a gate that already has a whitelist keyed on the other attribute is rejected (409) rather than silently re-keying the entries already there — use `PUT` to switch `attr` deliberately.
1057
+
1058
+ **Use case:** Let one more customer into a private beta without reading the current list first.
1059
+ tags:
1060
+ - Flags
1061
+ parameters:
1062
+ - $ref: '#/components/parameters/ProjectId'
1063
+ - name: id
1064
+ in: path
1065
+ required: true
1066
+ description: Stable opaque gate id (`gate_…`) or the gate's `name`.
1067
+ schema:
1068
+ $ref: '#/components/schemas/ResourceId'
1069
+ requestBody:
1070
+ required: true
1071
+ content:
1072
+ application/json:
1073
+ schema:
1074
+ $ref: '#/components/schemas/AddToGateWhitelistRequest'
1075
+ examples:
1076
+ addOne:
1077
+ summary: Admit one more email
1078
+ value:
1079
+ entries:
1080
+ - carol@acme.dev
1081
+ createOnUserIds:
1082
+ summary: Start a whitelist keyed on user ids
1083
+ description: '`attr` is honoured only because this gate has no whitelist yet.'
1084
+ value:
1085
+ attr: user_id
1086
+ entries:
1087
+ - usr_01j7w8a1b2c3d4e5f6g7h8i9j0
1088
+ responses:
1089
+ '200':
1090
+ description: Add entries to a gate's whitelist
1091
+ content:
1092
+ application/json:
1093
+ schema:
1094
+ $ref: '#/components/schemas/GateWhitelist'
1095
+ example:
1096
+ id: gate_01j7w8a1b2c3d4e5f6g7h8i9j0
1097
+ name: new_checkout
1098
+ attr: email
1099
+ entries:
1100
+ - alice@acme.dev
1101
+ - bob@acme.dev
1102
+ - carol@acme.dev
1103
+ '400':
1104
+ $ref: '#/components/responses/BadRequest'
1105
+ '401':
1106
+ $ref: '#/components/responses/Unauthorized'
1107
+ '403':
1108
+ $ref: '#/components/responses/Forbidden'
1109
+ '404':
1110
+ $ref: '#/components/responses/NotFound'
1111
+ '409':
1112
+ $ref: '#/components/responses/Conflict'
1113
+ '422':
1114
+ $ref: '#/components/responses/UnprocessableEntity'
1115
+ x-error-codes:
1116
+ - BAD_REQUEST
1117
+ - UNAUTHORIZED
1118
+ - FORBIDDEN
1119
+ - NOT_FOUND
1120
+ - IMMUTABLE_FIELD
1121
+ - VALIDATION
1122
+ x-cli:
1123
+ name: whitelist-add
1124
+ positional:
1125
+ - id
1126
+ delete:
1127
+ operationId: removeFromGateWhitelist
1128
+ summary: Remove entries from a gate's whitelist
1129
+ description: |-
1130
+ Removes identities from the gate's whitelist. Entries that aren't on the list are skipped, so the call is idempotent.
1131
+
1132
+ Removing the last entry leaves an empty whitelist block in place; to drop the block itself use `PUT` with `entries: []`.
1133
+
1134
+ **Use case:** Revoke one beta tester's access without touching anyone else's.
1135
+ tags:
1136
+ - Flags
1137
+ parameters:
1138
+ - $ref: '#/components/parameters/ProjectId'
1139
+ - name: id
1140
+ in: path
1141
+ required: true
1142
+ description: Stable opaque gate id (`gate_…`) or the gate's `name`.
1143
+ schema:
1144
+ $ref: '#/components/schemas/ResourceId'
1145
+ requestBody:
1146
+ required: true
1147
+ content:
1148
+ application/json:
1149
+ schema:
1150
+ $ref: '#/components/schemas/RemoveFromGateWhitelistRequest'
1151
+ examples:
1152
+ removeOne:
1153
+ summary: Revoke one email
1154
+ value:
1155
+ entries:
1156
+ - carol@acme.dev
1157
+ responses:
1158
+ '200':
1159
+ description: Remove entries from a gate's whitelist
1160
+ content:
1161
+ application/json:
1162
+ schema:
1163
+ $ref: '#/components/schemas/GateWhitelist'
1164
+ example:
1165
+ id: gate_01j7w8a1b2c3d4e5f6g7h8i9j0
1166
+ name: new_checkout
1167
+ attr: email
1168
+ entries:
1169
+ - alice@acme.dev
1170
+ - bob@acme.dev
1171
+ '400':
1172
+ $ref: '#/components/responses/BadRequest'
1173
+ '401':
1174
+ $ref: '#/components/responses/Unauthorized'
1175
+ '403':
1176
+ $ref: '#/components/responses/Forbidden'
1177
+ '404':
1178
+ $ref: '#/components/responses/NotFound'
1179
+ '409':
1180
+ $ref: '#/components/responses/Conflict'
1181
+ '422':
1182
+ $ref: '#/components/responses/UnprocessableEntity'
1183
+ x-error-codes:
1184
+ - BAD_REQUEST
1185
+ - UNAUTHORIZED
1186
+ - FORBIDDEN
1187
+ - NOT_FOUND
1188
+ - VALIDATION
1189
+ x-cli:
1190
+ name: whitelist-remove
1191
+ positional:
1192
+ - id
880
1193
  /api/admin/experiments:
881
1194
  get:
882
1195
  operationId: listExperiments
@@ -3343,38 +3656,80 @@ paths:
3343
3656
  name: set-value
3344
3657
  positional:
3345
3658
  - id
3346
- /api/admin/universes:
3347
- get:
3348
- operationId: listUniverses
3349
- summary: List universes
3659
+ /api/admin/killswitches/{id}/toggle:
3660
+ post:
3661
+ operationId: toggleKillswitch
3662
+ summary: Toggle a killswitch or one of its switches
3350
3663
  description: |-
3351
- Returns a single page of universes ordered by `created_at desc, id desc`. The universes table has no `updated_at`, so this list is keyed on creation time.
3664
+ Flips a killswitch on one environment and publishes a new version there. This is the one-call incident verb: it reads the current value, flips it, and publishes, so you don't have to fetch the killswitch first.
3352
3665
 
3353
- **Use case:** Snapshot every universe in the project — for example to audit which `unit_type` and `holdout_range` are in use before launching a new experiment.
3666
+ Every body field is optional, which is what makes the call widen cleanly:
3667
+
3668
+ - **Flip the killswitch** — `{}`. Flips the flat `value` on `prod`.
3669
+ - **Flip one sub-switch** — `{ "switchKey": "eu_region" }`. Flips that entry on `prod`, creating it (from `false`) if it isn't in the map yet.
3670
+ - **Set it idempotently** — `{ "switchKey": "eu_region", "value": true }`. Publishes exactly that value, so a retried call can't undo the first one. A `null` `value` means "flip", not "set to null".
3671
+ - **Choose the environment** — add `"env": "staging"`. Omitted, `env` is `prod`.
3672
+
3673
+ The response reports both `previous` and `value`, so a caller that asked for a flip can see what it actually changed.
3674
+
3675
+ Prefer this over `PUT /{id}/value` and `PUT /{id}/switch` unless you specifically need those endpoints' unconditional set semantics.
3354
3676
  tags:
3355
- - Universes
3677
+ - Killswitch
3356
3678
  parameters:
3357
3679
  - $ref: '#/components/parameters/ProjectId'
3358
- - $ref: '#/components/parameters/PaginationLimit'
3359
- - $ref: '#/components/parameters/PaginationCursor'
3360
- - $ref: '#/components/parameters/Q'
3680
+ - name: id
3681
+ in: path
3682
+ required: true
3683
+ description: Stable opaque killswitch id (`ksw_…`) or the killswitch's `name`.
3684
+ schema:
3685
+ $ref: '#/components/schemas/ResourceId'
3686
+ requestBody:
3687
+ required: false
3688
+ content:
3689
+ application/json:
3690
+ schema:
3691
+ $ref: '#/components/schemas/ToggleKillswitchRequest'
3692
+ examples:
3693
+ flipOnProd:
3694
+ summary: Flip the killswitch on prod
3695
+ description: No body fields — reads the flat `value` on prod and publishes its opposite.
3696
+ value: {}
3697
+ flipOneSwitch:
3698
+ summary: Flip one sub-switch on prod
3699
+ description: Flips `switches.eu_region` on prod, leaving the flat `value` and every other key alone.
3700
+ value:
3701
+ switchKey: eu_region
3702
+ setOneSwitch:
3703
+ summary: Trip one sub-switch idempotently
3704
+ description: Publishes `switches.eu_region = true` on prod whatever it was before — safe to retry.
3705
+ value:
3706
+ switchKey: eu_region
3707
+ value: true
3708
+ setOneSwitchOnStaging:
3709
+ summary: Trip one sub-switch on a chosen env
3710
+ description: The same idempotent set, published on staging instead of prod.
3711
+ value:
3712
+ switchKey: eu_region
3713
+ value: true
3714
+ env: staging
3361
3715
  responses:
3362
3716
  '200':
3363
- description: List universes
3717
+ description: Toggle a killswitch or one of its switches
3364
3718
  content:
3365
3719
  application/json:
3366
3720
  schema:
3367
- $ref: '#/components/schemas/ListUniversesResponse'
3721
+ $ref: '#/components/schemas/ToggleKillswitchResponse'
3368
3722
  example:
3369
- data:
3370
- - id: uni_01j7w8a1b2c3d4e5f6g7h8i9j0
3371
- name: primary_users
3372
- unitType: user_id
3373
- holdoutRange:
3374
- - 9500
3375
- - 9999
3376
- createdAt: '2026-04-12T10:14:08.000Z'
3377
- next_cursor: null
3723
+ id: ksw_01j7w9d8h2k4m6n8p0q2r4s6t8
3724
+ env: prod
3725
+ switchKey: eu_region
3726
+ previous: false
3727
+ value: true
3728
+ version: 7
3729
+ published:
3730
+ value: false
3731
+ switches:
3732
+ eu_region: true
3378
3733
  '400':
3379
3734
  $ref: '#/components/responses/BadRequest'
3380
3735
  '401':
@@ -3391,14 +3746,68 @@ paths:
3391
3746
  - BAD_REQUEST
3392
3747
  - UNAUTHORIZED
3393
3748
  - FORBIDDEN
3749
+ - NOT_FOUND
3750
+ - VALIDATION
3394
3751
  x-cli:
3395
- name: list
3396
- post:
3397
- operationId: createUniverse
3398
- summary: Create a universe
3399
- description: |-
3400
- Creates a new universe. Only `name` is required — `unit_type` defaults to `user_id` and `holdout_range` defaults to `null` (no holdout).
3401
-
3752
+ name: toggle
3753
+ positional:
3754
+ - id
3755
+ /api/admin/universes:
3756
+ get:
3757
+ operationId: listUniverses
3758
+ summary: List universes
3759
+ description: |-
3760
+ Returns a single page of universes ordered by `created_at desc, id desc`. The universes table has no `updated_at`, so this list is keyed on creation time.
3761
+
3762
+ **Use case:** Snapshot every universe in the project — for example to audit which `unit_type` and `holdout_range` are in use before launching a new experiment.
3763
+ tags:
3764
+ - Universes
3765
+ parameters:
3766
+ - $ref: '#/components/parameters/ProjectId'
3767
+ - $ref: '#/components/parameters/PaginationLimit'
3768
+ - $ref: '#/components/parameters/PaginationCursor'
3769
+ - $ref: '#/components/parameters/Q'
3770
+ responses:
3771
+ '200':
3772
+ description: List universes
3773
+ content:
3774
+ application/json:
3775
+ schema:
3776
+ $ref: '#/components/schemas/ListUniversesResponse'
3777
+ example:
3778
+ data:
3779
+ - id: uni_01j7w8a1b2c3d4e5f6g7h8i9j0
3780
+ name: primary_users
3781
+ unitType: user_id
3782
+ holdoutRange:
3783
+ - 9500
3784
+ - 9999
3785
+ createdAt: '2026-04-12T10:14:08.000Z'
3786
+ next_cursor: null
3787
+ '400':
3788
+ $ref: '#/components/responses/BadRequest'
3789
+ '401':
3790
+ $ref: '#/components/responses/Unauthorized'
3791
+ '403':
3792
+ $ref: '#/components/responses/Forbidden'
3793
+ '404':
3794
+ $ref: '#/components/responses/NotFound'
3795
+ '409':
3796
+ $ref: '#/components/responses/Conflict'
3797
+ '422':
3798
+ $ref: '#/components/responses/UnprocessableEntity'
3799
+ x-error-codes:
3800
+ - BAD_REQUEST
3801
+ - UNAUTHORIZED
3802
+ - FORBIDDEN
3803
+ x-cli:
3804
+ name: list
3805
+ post:
3806
+ operationId: createUniverse
3807
+ summary: Create a universe
3808
+ description: |-
3809
+ Creates a new universe. Only `name` is required — `unit_type` defaults to `user_id` and `holdout_range` defaults to `null` (no holdout).
3810
+
3402
3811
  Returns `409` if `name` already exists in the project. Returns `403` if you supply `holdout_range` on a plan below Pro.
3403
3812
 
3404
3813
  **Use cases**
@@ -4183,12 +4592,15 @@ paths:
4183
4592
  name: checkouts
4184
4593
  folder: null
4185
4594
  eventName: checkout_completed
4186
- query: count_users(checkout_completed)
4595
+ query: count(checkout_completed)
4187
4596
  queryIr:
4188
- agg:
4189
- kind: count_users
4190
- metric: checkout_completed
4191
- filters: []
4597
+ v: 2
4598
+ expr:
4599
+ node: agg
4600
+ event: checkout_completed
4601
+ agg:
4602
+ kind: count
4603
+ filters: []
4192
4604
  direction: higher_better
4193
4605
  version: 1
4194
4606
  '400':
@@ -4222,7 +4634,7 @@ paths:
4222
4634
 
4223
4635
  **Use cases**
4224
4636
 
4225
- - **Track an event** — `count_users(<event>)` for unique-user counts.
4637
+ - **Track an event** — `count(<event>)` for event counts.
4226
4638
  - **Sum a value** — `sum(<event>, <label>)` for revenue / quantity metrics.
4227
4639
  - **Experiment success metric** — create the metric, then attach its id to an experiment.
4228
4640
  tags:
@@ -4236,13 +4648,13 @@ paths:
4236
4648
  schema:
4237
4649
  $ref: '#/components/schemas/CreateMetricRequest'
4238
4650
  examples:
4239
- countUsers:
4240
- summary: Count unique users (DSL)
4241
- 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.
4242
4654
  value:
4243
4655
  name: checkouts
4244
4656
  event_name: checkout_completed
4245
- query: count_users(checkout_completed)
4657
+ query: count(checkout_completed)
4246
4658
  sumRevenue:
4247
4659
  summary: Sum a value label (DSL)
4248
4660
  description: Total revenue, summing the `amount` property of the `purchase` event.
@@ -4259,10 +4671,13 @@ paths:
4259
4671
  name: checkouts
4260
4672
  event_name: checkout_completed
4261
4673
  query_ir:
4262
- agg:
4263
- kind: count_users
4264
- metric: checkout_completed
4265
- filters: []
4674
+ v: 2
4675
+ expr:
4676
+ node: agg
4677
+ event: checkout_completed
4678
+ agg:
4679
+ kind: count
4680
+ filters: []
4266
4681
  responses:
4267
4682
  '201':
4268
4683
  description: Create a metric
@@ -4343,11 +4758,14 @@ paths:
4343
4758
  eventName: purchase
4344
4759
  query: sum(purchase, amount)
4345
4760
  queryIr:
4346
- agg:
4347
- kind: sum
4348
- metric: purchase
4349
- valueLabel: amount
4350
- filters: []
4761
+ v: 2
4762
+ expr:
4763
+ node: agg
4764
+ event: purchase
4765
+ agg:
4766
+ kind: sum
4767
+ valueLabel: amount
4768
+ filters: []
4351
4769
  direction: higher_better
4352
4770
  winsorizePct: 99
4353
4771
  version: 1
@@ -5053,7 +5471,11 @@ paths:
5053
5471
  operationId: listOpsItems
5054
5472
  summary: List the operational queue
5055
5473
  description: |-
5056
- 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.
5057
5479
 
5058
5480
  **Use case:** Pull the open queue to triage — e.g. every `bug` still `open` — before working items down one by one.
5059
5481
  tags:
@@ -5063,8 +5485,9 @@ paths:
5063
5485
  - name: type
5064
5486
  in: query
5065
5487
  required: false
5066
- 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.
5067
5489
  schema:
5490
+ default: all
5068
5491
  anyOf:
5069
5492
  - type: string
5070
5493
  enum:
@@ -5072,30 +5495,70 @@ paths:
5072
5495
  - feature_request
5073
5496
  - error
5074
5497
  - alert
5075
- 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.
5076
5500
  - type: string
5077
5501
  const: all
5078
5502
  - name: status
5079
5503
  in: query
5080
5504
  required: false
5081
- 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.
5082
5506
  schema:
5507
+ default: all
5083
5508
  anyOf:
5084
5509
  - $ref: '#/components/schemas/OpsItemStatus'
5510
+ - type: string
5511
+ const: unresolved
5512
+ - type: string
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'
5085
5526
  - type: string
5086
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
5087
5538
  - name: limit
5088
5539
  in: query
5089
5540
  required: false
5090
- 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.
5091
5542
  schema:
5092
5543
  type: integer
5093
5544
  minimum: 1
5094
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
5095
5558
  - name: owner
5096
5559
  in: query
5097
5560
  required: false
5098
- 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.
5099
5562
  schema:
5100
5563
  type: string
5101
5564
  responses:
@@ -5141,6 +5604,8 @@ paths:
5141
5604
  - FORBIDDEN
5142
5605
  x-cli:
5143
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.'
5144
5609
  post:
5145
5610
  operationId: createOpsItem
5146
5611
  summary: File a queue item
@@ -5222,6 +5687,173 @@ paths:
5222
5687
  summary: File a feature request.
5223
5688
  preset:
5224
5689
  type: feature_request
5690
+ /ops/bug:
5691
+ post:
5692
+ operationId: createPublicBug
5693
+ summary: File a bug
5694
+ description: |-
5695
+ Files one bug onto a project's queue, awaiting human approval. This is the **public** intake: it authenticates with a *client* SDK key rather than an admin key, so it can be called from a CLI, an installer script, a devtools overlay, or any shipped code — the same places a client key already lives.
5696
+
5697
+ Three gates decide whether a ticket is filed, and nothing else the caller sends can widen them:
5698
+
5699
+ 1. the key is a `client` key carrying the `tickets:public_create` scope,
5700
+ 2. the key's project has public ticket creation enabled, and
5701
+ 3. the item is filed as `pending_approval` — parked out of the work queue until a human promotes it in the dashboard.
5702
+
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.
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
+
5707
+ This endpoint is served by the Shipeasy **edge worker** (`api.shipeasy.ai`), not the admin API — see `servers` below.
5708
+
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": "…" }`.
5710
+ tags:
5711
+ - Ops
5712
+ servers:
5713
+ - url: https://api.shipeasy.ai
5714
+ description: Shipeasy edge worker (production)
5715
+ - url: http://localhost:8787
5716
+ description: Local `wrangler dev`
5717
+ security:
5718
+ - clientSdkKey: []
5719
+ requestBody:
5720
+ required: true
5721
+ content:
5722
+ application/json:
5723
+ schema:
5724
+ $ref: '#/components/schemas/CreatePublicBugRequest'
5725
+ examples:
5726
+ minimal:
5727
+ summary: Just a title
5728
+ description: Every other field is optional — the queue triages from `open` at default priority.
5729
+ value:
5730
+ title: Checkout button misaligned on mobile
5731
+ full:
5732
+ summary: A full report
5733
+ value:
5734
+ title: Checkout button misaligned on mobile
5735
+ stepsToReproduce: Open the cart on iOS Safari and tap the price row.
5736
+ actualResult: The primary CTA overlaps the price.
5737
+ expectedResult: The CTA sits below the price with clear spacing.
5738
+ priority: high
5739
+ reporterEmail: alice@acme.dev
5740
+ pageUrl: https://acme.dev/cart
5741
+ responses:
5742
+ '200':
5743
+ description: An open ticket already tracks this report; nothing was filed
5744
+ content:
5745
+ application/json:
5746
+ schema:
5747
+ $ref: '#/components/schemas/CreatePublicTicketResponse'
5748
+ example:
5749
+ number: 7
5750
+ deduped: true
5751
+ '201':
5752
+ description: File a bug
5753
+ content:
5754
+ application/json:
5755
+ schema:
5756
+ $ref: '#/components/schemas/CreatePublicTicketResponse'
5757
+ example:
5758
+ id: fb_01j7w8a1b2c3d4e5f6g7h8i9j0
5759
+ number: 7
5760
+ '400':
5761
+ $ref: '#/components/responses/BadRequest'
5762
+ '401':
5763
+ $ref: '#/components/responses/Unauthorized'
5764
+ '403':
5765
+ $ref: '#/components/responses/Forbidden'
5766
+ '413':
5767
+ $ref: '#/components/responses/PayloadTooLarge'
5768
+ '429':
5769
+ $ref: '#/components/responses/TooManyRequests'
5770
+ '503':
5771
+ $ref: '#/components/responses/ServiceUnavailable'
5772
+ x-error-codes:
5773
+ - BAD_REQUEST
5774
+ - UNAUTHORIZED
5775
+ - FORBIDDEN
5776
+ - VALIDATION
5777
+ x-cli:
5778
+ hidden: true
5779
+ /ops/feature-request:
5780
+ post:
5781
+ operationId: createPublicFeatureRequest
5782
+ summary: File a feature request
5783
+ description: |-
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.
5785
+
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.
5787
+
5788
+ This endpoint is served by the Shipeasy **edge worker** (`api.shipeasy.ai`), not the admin API — see `servers` below.
5789
+
5790
+ **Use case:** An in-product "request a feature" form posts what the user asked for — `{ "title": "Dark mode", "useCase": "Reduce eye strain at night" }`.
5791
+ tags:
5792
+ - Ops
5793
+ servers:
5794
+ - url: https://api.shipeasy.ai
5795
+ description: Shipeasy edge worker (production)
5796
+ - url: http://localhost:8787
5797
+ description: Local `wrangler dev`
5798
+ security:
5799
+ - clientSdkKey: []
5800
+ requestBody:
5801
+ required: true
5802
+ content:
5803
+ application/json:
5804
+ schema:
5805
+ $ref: '#/components/schemas/CreatePublicFeatureRequestRequest'
5806
+ examples:
5807
+ minimal:
5808
+ summary: Just a title
5809
+ value:
5810
+ title: Dark mode for the dashboard
5811
+ full:
5812
+ summary: A full request
5813
+ value:
5814
+ title: Dark mode for the dashboard
5815
+ description: Add a theme toggle that persists per user.
5816
+ useCase: Reduce eye strain for users working at night.
5817
+ priority: nice_to_have
5818
+ reporterEmail: alice@acme.dev
5819
+ responses:
5820
+ '200':
5821
+ description: An open ticket already tracks this report; nothing was filed
5822
+ content:
5823
+ application/json:
5824
+ schema:
5825
+ $ref: '#/components/schemas/CreatePublicTicketResponse'
5826
+ example:
5827
+ number: 7
5828
+ deduped: true
5829
+ '201':
5830
+ description: File a feature request
5831
+ content:
5832
+ application/json:
5833
+ schema:
5834
+ $ref: '#/components/schemas/CreatePublicTicketResponse'
5835
+ example:
5836
+ id: fb_01j7w8a1b2c3d4e5f6g7h8i9j0
5837
+ number: 8
5838
+ '400':
5839
+ $ref: '#/components/responses/BadRequest'
5840
+ '401':
5841
+ $ref: '#/components/responses/Unauthorized'
5842
+ '403':
5843
+ $ref: '#/components/responses/Forbidden'
5844
+ '413':
5845
+ $ref: '#/components/responses/PayloadTooLarge'
5846
+ '429':
5847
+ $ref: '#/components/responses/TooManyRequests'
5848
+ '503':
5849
+ $ref: '#/components/responses/ServiceUnavailable'
5850
+ x-error-codes:
5851
+ - BAD_REQUEST
5852
+ - UNAUTHORIZED
5853
+ - FORBIDDEN
5854
+ - VALIDATION
5855
+ x-cli:
5856
+ hidden: true
5225
5857
  /api/admin/ops/{handle}:
5226
5858
  get:
5227
5859
  operationId: getOpsItem
@@ -5470,6 +6102,11 @@ paths:
5470
6102
  list` and use one of those (or connect the agent under Settings →
5471
6103
  Triggers).
5472
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
+
5473
6110
  **Completion.** The run closes automatically on the loop's final actions —
5474
6111
  linking the fixing PR (`link-pr`), an ops-notify escalation, or a
5475
6112
  completion status change (`ready_for_qa`/`resolved`) — and the dashboard
@@ -5591,7 +6228,9 @@ paths:
5591
6228
  (`kind: investigated`), a blocking question for the team (`kind:
5592
6229
  question`), or how to verify the fix (`kind: ready_for_qa` with
5593
6230
  `qaNotes`). Append-only and create-only, so it is safe for restricted ops
5594
- 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.
5595
6234
 
5596
6235
  **Use case:** After working an item, leave a findings write-up (summary,
5597
6236
  markdown findings, sources inspected, confidence) so the team — and the
@@ -5734,53 +6373,88 @@ paths:
5734
6373
  positional:
5735
6374
  - handle
5736
6375
  - investigationId
5737
- /api/admin/agent-profiles:
6376
+ /api/admin/ops/{handle}/questions:
5738
6377
  get:
5739
- operationId: listOpsAgents
5740
- summary: List connected AI agents
6378
+ operationId: listOpsQuestions
6379
+ summary: List an item's questions
5741
6380
  description: |-
5742
- The project's connected AI agents — one per authenticated trigger
5743
- connector (Claude / Cursor / Copilot / Jules). These are the agent types
5744
- `ops ack` accepts and the agents a queue item can be assigned to.
5745
-
5746
- **Use case:** Discover which `agent` values an AI ack can use, e.g. after
5747
- an `AGENT_NOT_CONNECTED` error.
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.
5748
6389
  tags:
5749
- - Agents
6390
+ - Questions
5750
6391
  parameters:
5751
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
5752
6409
  responses:
5753
6410
  '200':
5754
- description: List connected AI agents
6411
+ description: List an item's questions
5755
6412
  content:
5756
6413
  application/json:
5757
6414
  schema:
5758
- $ref: '#/components/schemas/ListOpsAgentsResponse'
6415
+ $ref: '#/components/schemas/ListOpsQuestionsResponse'
5759
6416
  '400':
5760
6417
  $ref: '#/components/responses/BadRequest'
5761
6418
  '401':
5762
6419
  $ref: '#/components/responses/Unauthorized'
5763
6420
  '403':
5764
6421
  $ref: '#/components/responses/Forbidden'
6422
+ '404':
6423
+ $ref: '#/components/responses/NotFound'
6424
+ '422':
6425
+ $ref: '#/components/responses/UnprocessableEntity'
5765
6426
  x-error-codes:
5766
6427
  - BAD_REQUEST
5767
6428
  - UNAUTHORIZED
5768
6429
  - FORBIDDEN
6430
+ - NOT_FOUND
5769
6431
  x-cli:
5770
6432
  name: list
5771
- /api/admin/ops/{handle}/comments:
5772
- get:
5773
- operationId: listOpsComments
5774
- summary: List an item's comments
6433
+ positional:
6434
+ - handle
6435
+ post:
6436
+ operationId: askOpsQuestions
6437
+ summary: Ask the team questions about an item
5775
6438
  description: |-
5776
- List the comment thread on a queue item, oldest first. Each comment
5777
- carries its author (a teammate email, or `system` for a comment authored by
5778
- Jarvis — the AI agent), its markdown body, and `parentId` for the single
5779
- level of threaded replies. Removed comments are omitted.
5780
-
5781
- **Use case:** Read the discussion on an item before replying or acting on it.
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.
5782
6456
  tags:
5783
- - Comments
6457
+ - Questions
5784
6458
  parameters:
5785
6459
  - $ref: '#/components/parameters/ProjectId'
5786
6460
  - name: handle
@@ -5789,13 +6463,33 @@ paths:
5789
6463
  description: Per-project item number (e.g. `7`) or the full ops item id.
5790
6464
  schema:
5791
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
5792
6486
  responses:
5793
6487
  '200':
5794
- description: List an item's comments
6488
+ description: The created questions, in the order asked
5795
6489
  content:
5796
6490
  application/json:
5797
6491
  schema:
5798
- $ref: '#/components/schemas/ListOpsCommentsResponse'
6492
+ $ref: '#/components/schemas/AskOpsQuestionsResponse'
5799
6493
  '400':
5800
6494
  $ref: '#/components/responses/BadRequest'
5801
6495
  '401':
@@ -5804,23 +6498,237 @@ paths:
5804
6498
  $ref: '#/components/responses/Forbidden'
5805
6499
  '404':
5806
6500
  $ref: '#/components/responses/NotFound'
6501
+ '422':
6502
+ $ref: '#/components/responses/UnprocessableEntity'
5807
6503
  x-error-codes:
5808
6504
  - BAD_REQUEST
5809
6505
  - UNAUTHORIZED
5810
6506
  - FORBIDDEN
5811
6507
  - NOT_FOUND
6508
+ - VALIDATION
5812
6509
  x-cli:
5813
- name: list
6510
+ name: ask
5814
6511
  positional:
5815
6512
  - handle
6513
+ /api/admin/ops/{handle}/questions/{questionId}/answer:
5816
6514
  post:
5817
- operationId: createOpsComment
5818
- summary: Comment on an item
6515
+ operationId: answerOpsQuestion
6516
+ summary: Answer a question
5819
6517
  description: |-
5820
- Append a comment to a queue item's thread. The body is markdown (mentions
5821
- like `@teammate` notify that person; `@shipeasy` asks Jarvis, the AI agent,
5822
- to reply). Pass `parentId` to reply under an existing top-level comment
5823
- (one level of threading — a reply to a reply attaches to the same parent).
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
6645
+ /api/admin/agent-profiles:
6646
+ get:
6647
+ operationId: listOpsAgents
6648
+ summary: List connected AI agents
6649
+ description: |-
6650
+ The project's connected AI agents — one per authenticated trigger
6651
+ connector (Claude / Cursor / Copilot / Jules). These are the agent types
6652
+ `ops ack` accepts and the agents a queue item can be assigned to.
6653
+
6654
+ **Use case:** Discover which `agent` values an AI ack can use, e.g. after
6655
+ an `AGENT_NOT_CONNECTED` error.
6656
+ tags:
6657
+ - Agents
6658
+ parameters:
6659
+ - $ref: '#/components/parameters/ProjectId'
6660
+ responses:
6661
+ '200':
6662
+ description: List connected AI agents
6663
+ content:
6664
+ application/json:
6665
+ schema:
6666
+ $ref: '#/components/schemas/ListOpsAgentsResponse'
6667
+ '400':
6668
+ $ref: '#/components/responses/BadRequest'
6669
+ '401':
6670
+ $ref: '#/components/responses/Unauthorized'
6671
+ '403':
6672
+ $ref: '#/components/responses/Forbidden'
6673
+ x-error-codes:
6674
+ - BAD_REQUEST
6675
+ - UNAUTHORIZED
6676
+ - FORBIDDEN
6677
+ x-cli:
6678
+ name: list
6679
+ /api/admin/ops/{handle}/comments:
6680
+ get:
6681
+ operationId: listOpsComments
6682
+ summary: List an item's comments
6683
+ description: |-
6684
+ List the comment thread on a queue item, oldest first. Each comment
6685
+ carries its author (a teammate email, or `system` for a comment authored by
6686
+ Jarvis — the AI agent), its markdown body, and `parentId` for the single
6687
+ level of threaded replies. Removed comments are omitted.
6688
+
6689
+ **Use case:** Read the discussion on an item before replying or acting on it.
6690
+ tags:
6691
+ - Comments
6692
+ parameters:
6693
+ - $ref: '#/components/parameters/ProjectId'
6694
+ - name: handle
6695
+ in: path
6696
+ required: true
6697
+ description: Per-project item number (e.g. `7`) or the full ops item id.
6698
+ schema:
6699
+ $ref: '#/components/schemas/ResourceId'
6700
+ responses:
6701
+ '200':
6702
+ description: List an item's comments
6703
+ content:
6704
+ application/json:
6705
+ schema:
6706
+ $ref: '#/components/schemas/ListOpsCommentsResponse'
6707
+ '400':
6708
+ $ref: '#/components/responses/BadRequest'
6709
+ '401':
6710
+ $ref: '#/components/responses/Unauthorized'
6711
+ '403':
6712
+ $ref: '#/components/responses/Forbidden'
6713
+ '404':
6714
+ $ref: '#/components/responses/NotFound'
6715
+ x-error-codes:
6716
+ - BAD_REQUEST
6717
+ - UNAUTHORIZED
6718
+ - FORBIDDEN
6719
+ - NOT_FOUND
6720
+ x-cli:
6721
+ name: list
6722
+ positional:
6723
+ - handle
6724
+ post:
6725
+ operationId: createOpsComment
6726
+ summary: Comment on an item
6727
+ description: |-
6728
+ Append a comment to a queue item's thread. The body is markdown (mentions
6729
+ like `@teammate` notify that person; `@shipeasy` asks Jarvis, the AI agent,
6730
+ to reply). Pass `parentId` to reply under an existing top-level comment
6731
+ (one level of threading — a reply to a reply attaches to the same parent).
5824
6732
 
5825
6733
  Create-only and append-only, so it is safe for restricted ops keys — the
5826
6734
  same channel the ops loop and Jarvis use to leave a note on an item.
@@ -6247,7 +7155,9 @@ paths:
6247
7155
  description: |-
6248
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`.
6249
7157
 
6250
- 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`.
6251
7161
 
6252
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.
6253
7163
  tags:
@@ -6331,7 +7241,7 @@ paths:
6331
7241
  description: |-
6332
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.
6333
7243
 
6334
- - **`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.
6335
7245
  - **`assigneeId`** — the PERSON owner (a `users.id`), or `null` to unassign.
6336
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.
6337
7247
 
@@ -6348,7 +7258,7 @@ paths:
6348
7258
  - name: id
6349
7259
  in: path
6350
7260
  required: true
6351
- description: Stable opaque alert id (`al_…`).
7261
+ description: Stable opaque instance id — the same id as the `alert` ops item this instance is.
6352
7262
  schema:
6353
7263
  $ref: '#/components/schemas/ResourceId'
6354
7264
  requestBody:
@@ -6822,11 +7732,12 @@ paths:
6822
7732
  - name: limit
6823
7733
  in: query
6824
7734
  required: false
6825
- description: Max keys to return (1–500).
7735
+ description: Max keys to return (1–500). Defaults to 200.
6826
7736
  schema:
6827
7737
  type: integer
6828
7738
  minimum: 1
6829
7739
  maximum: 500
7740
+ default: 200
6830
7741
  - name: offset
6831
7742
  in: query
6832
7743
  required: false
@@ -8863,6 +9774,7 @@ paths:
8863
9774
  expires_at: null
8864
9775
  created_by_email: ana@example.com
8865
9776
  name: production back-end
9777
+ description: Held by the checkout service; rotate with the quarterly credential sweep
8866
9778
  scopes:
8867
9779
  - gates:evaluate
8868
9780
  - events:write
@@ -8962,6 +9874,84 @@ paths:
8962
9874
  - PLAN_LIMIT
8963
9875
  x-cli:
8964
9876
  name: create
9877
+ /api/admin/keys/{id}:
9878
+ patch:
9879
+ operationId: updateKey
9880
+ summary: Update an API key's label
9881
+ description: |-
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.
9883
+
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.
9885
+
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.
9887
+ tags:
9888
+ - API Keys
9889
+ parameters:
9890
+ - $ref: '#/components/parameters/ProjectId'
9891
+ - name: id
9892
+ in: path
9893
+ required: true
9894
+ description: Stable opaque key id (UUID) returned by `create` / `list`.
9895
+ schema:
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
9913
+ responses:
9914
+ '200':
9915
+ description: Update an API key's label
9916
+ content:
9917
+ application/json:
9918
+ schema:
9919
+ $ref: '#/components/schemas/KeyRecord'
9920
+ example:
9921
+ id: 3f2a9b1c-7d4e-4a8f-9c2b-1e5d6f7a8b90
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
9934
+ '400':
9935
+ $ref: '#/components/responses/BadRequest'
9936
+ '401':
9937
+ $ref: '#/components/responses/Unauthorized'
9938
+ '403':
9939
+ $ref: '#/components/responses/Forbidden'
9940
+ '404':
9941
+ $ref: '#/components/responses/NotFound'
9942
+ '409':
9943
+ $ref: '#/components/responses/Conflict'
9944
+ '422':
9945
+ $ref: '#/components/responses/UnprocessableEntity'
9946
+ x-error-codes:
9947
+ - UNAUTHORIZED
9948
+ - FORBIDDEN
9949
+ - NOT_FOUND
9950
+ - VALIDATION
9951
+ x-cli:
9952
+ name: update
9953
+ positional:
9954
+ - id
8965
9955
  /api/admin/keys/{id}/revoke:
8966
9956
  post:
8967
9957
  operationId: revokeKey
@@ -9074,30 +10064,310 @@ paths:
9074
10064
  - FORBIDDEN
9075
10065
  x-cli:
9076
10066
  name: search
9077
- components:
9078
- securitySchemes:
9079
- bearerSdkKey:
9080
- type: http
9081
- scheme: bearer
9082
- bearerFormat: sdk_admin_*
9083
- description: 'Pass an admin SDK key as `Authorization: Bearer sdk_admin_…`. Mint via `POST /api/admin/keys` with `type: "admin"`.'
9084
- parameters:
9085
- ProjectId:
9086
- name: X-Project-Id
9087
- in: header
9088
- required: false
9089
- description: Project the request operates on. Optional — defaults to the project the SDK key belongs to; pass it only to scope a multi-project key (the generated client sets it once from its configuration, so per-call callers never thread it).
9090
- schema:
9091
- type: string
9092
- PaginationLimit:
9093
- name: limit
9094
- in: query
9095
- required: false
9096
- description: Page size (1–500). Defaults to 100.
9097
- schema:
9098
- type: integer
9099
- minimum: 1
9100
- maximum: 500
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
10337
+ components:
10338
+ securitySchemes:
10339
+ bearerSdkKey:
10340
+ type: http
10341
+ scheme: bearer
10342
+ bearerFormat: sdk_admin_*
10343
+ description: 'Pass an admin SDK key as `Authorization: Bearer sdk_admin_…`. Mint via `POST /api/admin/keys` with `type: "admin"`.'
10344
+ clientSdkKey:
10345
+ type: apiKey
10346
+ in: header
10347
+ name: X-SDK-Key
10348
+ description: |-
10349
+ Pass a **client** SDK key as `X-SDK-Key: sdk_client_…`. Used only by the public ticket intake (`POST /ops/bug`, `POST /ops/feature-request`), which is served by the edge worker rather than the admin API.
10350
+
10351
+ Client keys are designed to be embedded in shipped code — a CLI, an installer, a browser bundle — so presenting one here is safe by construction: it can do nothing but file a rate-limited ticket into its own project, in a state a human must approve. It carries no read access and cannot reach any other operation in this contract.
10352
+
10353
+ The key must additionally carry the `tickets:public_create` scope, and its project must have public ticket creation enabled.
10354
+ parameters:
10355
+ ProjectId:
10356
+ name: X-Project-Id
10357
+ in: header
10358
+ required: false
10359
+ description: Project the request operates on. Optional — defaults to the project the SDK key belongs to; pass it only to scope a multi-project key (the generated client sets it once from its configuration, so per-call callers never thread it).
10360
+ schema:
10361
+ type: string
10362
+ PaginationLimit:
10363
+ name: limit
10364
+ in: query
10365
+ required: false
10366
+ description: Page size (1–500). Defaults to 100.
10367
+ schema:
10368
+ type: integer
10369
+ minimum: 1
10370
+ maximum: 500
9101
10371
  default: 100
9102
10372
  PaginationCursor:
9103
10373
  name: cursor
@@ -10185,6 +11455,85 @@ components:
10185
11455
  - createdAt
10186
11456
  additionalProperties: false
10187
11457
  description: Recent audit rows for one gate, newest first.
11458
+ GateWhitelistAttr:
11459
+ type: string
11460
+ enum:
11461
+ - email
11462
+ - user_id
11463
+ description: Which identity attribute the whitelist matches on. `email` compares the caller's `email`; `user_id` compares the caller's `userID`. A whitelist matches on exactly one of the two at a time.
11464
+ GateWhitelist:
11465
+ type: object
11466
+ properties:
11467
+ id:
11468
+ type: string
11469
+ description: Resolved gate id.
11470
+ name:
11471
+ type: string
11472
+ description: The gate's stable `name`.
11473
+ attr:
11474
+ $ref: '#/components/schemas/GateWhitelistAttr'
11475
+ entries:
11476
+ type: array
11477
+ items:
11478
+ type: string
11479
+ minLength: 1
11480
+ description: The whitelisted identities, in the order they are stored. Deduplicated case-sensitively; an empty array means the gate has no whitelist.
11481
+ required:
11482
+ - id
11483
+ - name
11484
+ - attr
11485
+ - entries
11486
+ additionalProperties: false
11487
+ description: A gate's whitelist — the always-first allowlist that admits the listed identities before any targeting rule or rollout runs. Backed by the pinned `whitelist` entry at the head of the gate's `stack`, so it is the same list the dashboard's Whitelist block edits.
11488
+ SetGateWhitelistRequest:
11489
+ type: object
11490
+ properties:
11491
+ attr:
11492
+ anyOf:
11493
+ - $ref: '#/components/schemas/GateWhitelistAttr'
11494
+ description: Identity attribute to match on. Defaults to the whitelist's current attribute, or `email` when the gate has no whitelist yet.
11495
+ entries:
11496
+ type: array
11497
+ items:
11498
+ type: string
11499
+ minLength: 1
11500
+ description: The complete whitelist after the call. Pass `[]` to remove the whitelist from the gate entirely.
11501
+ required:
11502
+ - entries
11503
+ additionalProperties: false
11504
+ description: Body for `PUT /api/admin/gates/{id}/whitelist`. Replaces the whole list — idempotent, and the only call that can switch `attr` or clear the whitelist.
11505
+ AddToGateWhitelistRequest:
11506
+ type: object
11507
+ properties:
11508
+ attr:
11509
+ anyOf:
11510
+ - $ref: '#/components/schemas/GateWhitelistAttr'
11511
+ description: Identity attribute to match on. Only honoured when the gate has no whitelist yet (this call creates it); passing an attribute that disagrees with an existing whitelist is a 409 rather than a silent re-key of the entries already there.
11512
+ entries:
11513
+ type: array
11514
+ minItems: 1
11515
+ items:
11516
+ type: string
11517
+ minLength: 1
11518
+ description: Identities to admit. Already-listed entries are skipped, so the call is idempotent.
11519
+ required:
11520
+ - entries
11521
+ additionalProperties: false
11522
+ description: Body for `POST /api/admin/gates/{id}/whitelist`. Adds entries to the whitelist, creating it if the gate doesn't have one.
11523
+ RemoveFromGateWhitelistRequest:
11524
+ type: object
11525
+ properties:
11526
+ entries:
11527
+ type: array
11528
+ minItems: 1
11529
+ items:
11530
+ type: string
11531
+ minLength: 1
11532
+ description: Identities to stop admitting. Entries that aren't listed are skipped, so the call is idempotent.
11533
+ required:
11534
+ - entries
11535
+ additionalProperties: false
11536
+ description: 'Body for `DELETE /api/admin/gates/{id}/whitelist`. Removes entries from the whitelist. Removing the last entry leaves an empty whitelist in place — use `PUT` with `entries: []` to drop the block itself.'
10188
11537
  ExperimentApiRow:
10189
11538
  type: object
10190
11539
  properties:
@@ -10502,13 +11851,10 @@ components:
10502
11851
  aggregation:
10503
11852
  type: string
10504
11853
  enum:
10505
- - count_users
10506
11854
  - count_events
10507
- - retention_7d
10508
- - retention_30d
10509
11855
  - sum
10510
11856
  - avg
10511
- 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.'
10512
11858
  value:
10513
11859
  type: string
10514
11860
  minLength: 1
@@ -12214,12 +13560,76 @@ components:
12214
13560
  - version
12215
13561
  - published
12216
13562
  additionalProperties: false
12217
- UniverseParam:
13563
+ ToggleKillswitchRequest:
12218
13564
  type: object
12219
13565
  properties:
12220
- name:
12221
- type: string
12222
- minLength: 1
13566
+ switchKey:
13567
+ anyOf:
13568
+ - type: string
13569
+ maxLength: 64
13570
+ pattern: ^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$
13571
+ - type: 'null'
13572
+ description: Which target to flip. Omit (or `null`) to flip the killswitch's own flat `value`; name a switch key to flip that nested sub-switch instead, creating the entry if it doesn't exist yet.
13573
+ value:
13574
+ anyOf:
13575
+ - type: boolean
13576
+ - type: 'null'
13577
+ description: The value to publish. Omit (or `null`) to flip whatever is stored now — read-modify-write in one call. Pass an explicit `true`/`false` to make the call idempotent, so a retry can't undo the first attempt.
13578
+ env:
13579
+ anyOf:
13580
+ - $ref: '#/components/schemas/Env'
13581
+ description: Environment to publish on. Defaults to `prod` — the environment an incident response means when it says "kill it".
13582
+ additionalProperties: false
13583
+ description: |-
13584
+ Body for `POST /api/admin/killswitches/{id}/toggle`. Every field is optional, so the four useful calls read as one method with a widening argument list:
13585
+
13586
+ - `{}` — flip the flat value on prod.
13587
+ - `{ "switchKey": "eu_region" }` — flip that sub-switch on prod.
13588
+ - `{ "switchKey": "eu_region", "value": true }` — set that sub-switch on prod, idempotently.
13589
+ - `{ "switchKey": "eu_region", "value": true, "env": "staging" }` — the same, on a chosen env.
13590
+ ToggleKillswitchResponse:
13591
+ type: object
13592
+ properties:
13593
+ id:
13594
+ type: string
13595
+ description: Resolved killswitch id.
13596
+ env:
13597
+ $ref: '#/components/schemas/Env'
13598
+ switchKey:
13599
+ anyOf:
13600
+ - type: string
13601
+ maxLength: 64
13602
+ pattern: ^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$
13603
+ - type: 'null'
13604
+ description: The sub-switch that was flipped, or `null` when the flat `value` was.
13605
+ previous:
13606
+ type: boolean
13607
+ description: The value that was in effect before this call. Equal to `value` when an idempotent `value` matched what was already stored.
13608
+ value:
13609
+ type: boolean
13610
+ description: The value now in effect on `env`.
13611
+ version:
13612
+ type: integer
13613
+ minimum: -9007199254740991
13614
+ maximum: 9007199254740991
13615
+ description: Newly published version on `env`.
13616
+ published:
13617
+ $ref: '#/components/schemas/KillswitchValue'
13618
+ required:
13619
+ - id
13620
+ - env
13621
+ - switchKey
13622
+ - previous
13623
+ - value
13624
+ - version
13625
+ - published
13626
+ additionalProperties: false
13627
+ UniverseParam:
13628
+ type: object
13629
+ properties:
13630
+ name:
13631
+ type: string
13632
+ minLength: 1
12223
13633
  maxLength: 128
12224
13634
  pattern: ^[a-zA-Z_][a-zA-Z0-9_]*$
12225
13635
  description: Param key, referenced by `groups[].params` on every experiment in the universe.
@@ -12861,253 +14271,60 @@ components:
12861
14271
  - id
12862
14272
  additionalProperties: false
12863
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
12864
14312
  QueryIr:
12865
- 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.
12866
14316
  type: object
12867
14317
  properties:
12868
- agg:
12869
- oneOf:
12870
- - type: object
12871
- properties:
12872
- kind:
12873
- type: string
12874
- const: count_users
12875
- required:
12876
- - kind
12877
- additionalProperties: false
12878
- - type: object
12879
- properties:
12880
- kind:
12881
- type: string
12882
- const: count_events
12883
- required:
12884
- - kind
12885
- additionalProperties: false
12886
- - type: object
12887
- properties:
12888
- kind:
12889
- type: string
12890
- const: sum
12891
- required:
12892
- - kind
12893
- additionalProperties: false
12894
- - type: object
12895
- properties:
12896
- kind:
12897
- type: string
12898
- const: avg
12899
- required:
12900
- - kind
12901
- additionalProperties: false
12902
- - type: object
12903
- properties:
12904
- kind:
12905
- type: string
12906
- const: min
12907
- required:
12908
- - kind
12909
- additionalProperties: false
12910
- - type: object
12911
- properties:
12912
- kind:
12913
- type: string
12914
- const: max
12915
- required:
12916
- - kind
12917
- additionalProperties: false
12918
- - type: object
12919
- properties:
12920
- kind:
12921
- type: string
12922
- const: unique
12923
- required:
12924
- - kind
12925
- additionalProperties: false
12926
- - type: object
12927
- properties:
12928
- kind:
12929
- type: string
12930
- const: quantile
12931
- p:
12932
- anyOf:
12933
- - type: number
12934
- const: 0.5
12935
- - type: number
12936
- const: 0.75
12937
- - type: number
12938
- const: 0.9
12939
- - type: number
12940
- const: 0.95
12941
- - type: number
12942
- const: 0.99
12943
- - type: number
12944
- const: 0.999
12945
- description: Quantile fraction (0.5 = p50 … 0.999 = p999).
12946
- required:
12947
- - kind
12948
- - p
12949
- additionalProperties: false
12950
- - type: object
12951
- properties:
12952
- kind:
12953
- type: string
12954
- const: retention_Nd
12955
- 'n':
12956
- type: integer
12957
- minimum: 1
12958
- maximum: 90
12959
- description: Retention window in days (1–90).
12960
- required:
12961
- - kind
12962
- - 'n'
12963
- additionalProperties: false
12964
- - type: object
12965
- properties:
12966
- kind:
12967
- type: string
12968
- const: ratio
12969
- numerator:
12970
- type: object
12971
- properties:
12972
- agg:
12973
- type: string
12974
- enum:
12975
- - count_users
12976
- - count_events
12977
- description: Counting mode for this ratio arm.
12978
- metric:
12979
- type: string
12980
- minLength: 1
12981
- maxLength: 128
12982
- description: Source event name for this ratio arm.
12983
- filters:
12984
- description: Optional label filters.
12985
- maxItems: 16
12986
- type: array
12987
- items:
12988
- type: object
12989
- properties:
12990
- label:
12991
- type: string
12992
- pattern: ^[a-z_][a-z0-9_]{0,63}$
12993
- description: Event property / label identifier.
12994
- op:
12995
- type: string
12996
- enum:
12997
- - '='
12998
- - '!='
12999
- - '=~'
13000
- - '!~'
13001
- description: Match operator (`=~`/`!~` are regex).
13002
- value:
13003
- type: string
13004
- maxLength: 512
13005
- description: Quoted filter value (coerced for numeric labels).
13006
- required:
13007
- - label
13008
- - op
13009
- - value
13010
- additionalProperties: false
13011
- description: One label filter on the source event.
13012
- required:
13013
- - agg
13014
- - metric
13015
- additionalProperties: false
13016
- description: Numerator or denominator arm of a `ratio` aggregation.
13017
- denominator:
13018
- type: object
13019
- properties:
13020
- agg:
13021
- type: string
13022
- enum:
13023
- - count_users
13024
- - count_events
13025
- description: Counting mode for this ratio arm.
13026
- metric:
13027
- type: string
13028
- minLength: 1
13029
- maxLength: 128
13030
- description: Source event name for this ratio arm.
13031
- filters:
13032
- description: Optional label filters.
13033
- maxItems: 16
13034
- type: array
13035
- items:
13036
- type: object
13037
- properties:
13038
- label:
13039
- type: string
13040
- pattern: ^[a-z_][a-z0-9_]{0,63}$
13041
- description: Event property / label identifier.
13042
- op:
13043
- type: string
13044
- enum:
13045
- - '='
13046
- - '!='
13047
- - '=~'
13048
- - '!~'
13049
- description: Match operator (`=~`/`!~` are regex).
13050
- value:
13051
- type: string
13052
- maxLength: 512
13053
- description: Quoted filter value (coerced for numeric labels).
13054
- required:
13055
- - label
13056
- - op
13057
- - value
13058
- additionalProperties: false
13059
- description: One label filter on the source event.
13060
- required:
13061
- - agg
13062
- - metric
13063
- additionalProperties: false
13064
- description: Numerator or denominator arm of a `ratio` aggregation.
13065
- required:
13066
- - kind
13067
- - numerator
13068
- - denominator
13069
- additionalProperties: false
13070
- description: Aggregation function applied to the source event.
13071
- metric:
13072
- type: string
13073
- minLength: 1
13074
- maxLength: 128
13075
- description: Source event name (must equal `event_name`).
13076
- valueLabel:
13077
- description: Numeric property summed/averaged for `sum`/`avg`/quantile aggregations.
13078
- type: string
13079
- minLength: 1
13080
- maxLength: 128
13081
- filters:
13082
- default: []
13083
- description: Label filters on the event.
13084
- maxItems: 16
13085
- type: array
13086
- items:
13087
- type: object
13088
- properties:
13089
- label:
13090
- type: string
13091
- pattern: ^[a-z_][a-z0-9_]{0,63}$
13092
- description: Event property / label identifier.
13093
- op:
13094
- type: string
13095
- enum:
13096
- - '='
13097
- - '!='
13098
- - '=~'
13099
- - '!~'
13100
- description: Match operator (`=~`/`!~` are regex).
13101
- value:
13102
- type: string
13103
- maxLength: 512
13104
- description: Quoted filter value (coerced for numeric labels).
13105
- required:
13106
- - label
13107
- - op
13108
- - value
13109
- additionalProperties: false
13110
- 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.
13111
14328
  groupBy:
13112
14329
  type: object
13113
14330
  properties:
@@ -13116,23 +14333,25 @@ components:
13116
14333
  enum:
13117
14334
  - by
13118
14335
  - without
13119
- description: '`by` keeps the listed labels; `without` drops them.'
14336
+ description: '`by` names the output dimensions; `without` names the ones to drop.'
13120
14337
  labels:
13121
- maxItems: 5
13122
14338
  type: array
14339
+ maxItems: 8
13123
14340
  items:
13124
14341
  type: string
13125
14342
  pattern: ^[a-z_][a-z0-9_]{0,63}$
13126
14343
  description: Event property / label identifier.
13127
- description: Labels to group by (max 5).
14344
+ description: Labels the grouping names.
13128
14345
  required:
13129
14346
  - op
13130
14347
  - labels
13131
14348
  additionalProperties: false
13132
- 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'
13133
14352
  required:
13134
- - agg
13135
- - metric
14353
+ - v
14354
+ - expr
13136
14355
  additionalProperties: false
13137
14356
  ListMetricsResponse:
13138
14357
  type: array
@@ -13234,6 +14453,12 @@ components:
13234
14453
  additionalProperties: {}
13235
14454
  description: A metric definition.
13236
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'
13237
14462
  MetricEventName:
13238
14463
  type: string
13239
14464
  minLength: 1
@@ -13273,6 +14498,8 @@ components:
13273
14498
  properties:
13274
14499
  name:
13275
14500
  $ref: '#/components/schemas/MetricName'
14501
+ display_name:
14502
+ $ref: '#/components/schemas/MetricDisplayName'
13276
14503
  folder:
13277
14504
  $ref: '#/components/schemas/Folder'
13278
14505
  event_name:
@@ -13287,6 +14514,8 @@ components:
13287
14514
  $ref: '#/components/schemas/MetricDirection'
13288
14515
  unit:
13289
14516
  $ref: '#/components/schemas/MetricDisplayUnit'
14517
+ display:
14518
+ $ref: '#/components/schemas/MetricDisplayConfig'
13290
14519
  required:
13291
14520
  - name
13292
14521
  - event_name
@@ -13298,6 +14527,8 @@ components:
13298
14527
  properties:
13299
14528
  name:
13300
14529
  $ref: '#/components/schemas/MetricName'
14530
+ display_name:
14531
+ $ref: '#/components/schemas/MetricDisplayName'
13301
14532
  folder:
13302
14533
  $ref: '#/components/schemas/Folder'
13303
14534
  event_name:
@@ -13312,6 +14543,8 @@ components:
13312
14543
  $ref: '#/components/schemas/MetricDirection'
13313
14544
  unit:
13314
14545
  $ref: '#/components/schemas/MetricDisplayUnit'
14546
+ display:
14547
+ $ref: '#/components/schemas/MetricDisplayConfig'
13315
14548
  required:
13316
14549
  - name
13317
14550
  - event_name
@@ -13437,6 +14670,8 @@ components:
13437
14670
  UpdateMetricWithQuery:
13438
14671
  type: object
13439
14672
  properties:
14673
+ display_name:
14674
+ $ref: '#/components/schemas/MetricDisplayName'
13440
14675
  folder:
13441
14676
  $ref: '#/components/schemas/Folder'
13442
14677
  event_name:
@@ -13451,6 +14686,8 @@ components:
13451
14686
  $ref: '#/components/schemas/MetricDirection'
13452
14687
  unit:
13453
14688
  $ref: '#/components/schemas/MetricDisplayUnit'
14689
+ display:
14690
+ $ref: '#/components/schemas/MetricDisplayConfig'
13454
14691
  required:
13455
14692
  - query
13456
14693
  additionalProperties: false
@@ -13458,6 +14695,8 @@ components:
13458
14695
  UpdateMetricWithQueryIr:
13459
14696
  type: object
13460
14697
  properties:
14698
+ display_name:
14699
+ $ref: '#/components/schemas/MetricDisplayName'
13461
14700
  folder:
13462
14701
  $ref: '#/components/schemas/Folder'
13463
14702
  event_name:
@@ -13472,6 +14711,8 @@ components:
13472
14711
  $ref: '#/components/schemas/MetricDirection'
13473
14712
  unit:
13474
14713
  $ref: '#/components/schemas/MetricDisplayUnit'
14714
+ display:
14715
+ $ref: '#/components/schemas/MetricDisplayConfig'
13475
14716
  required:
13476
14717
  - query_ir
13477
14718
  additionalProperties: false
@@ -13479,6 +14720,8 @@ components:
13479
14720
  UpdateMetricFields:
13480
14721
  type: object
13481
14722
  properties:
14723
+ display_name:
14724
+ $ref: '#/components/schemas/MetricDisplayName'
13482
14725
  folder:
13483
14726
  $ref: '#/components/schemas/Folder'
13484
14727
  event_name:
@@ -13491,8 +14734,10 @@ components:
13491
14734
  $ref: '#/components/schemas/MetricDirection'
13492
14735
  unit:
13493
14736
  $ref: '#/components/schemas/MetricDisplayUnit'
14737
+ display:
14738
+ $ref: '#/components/schemas/MetricDisplayConfig'
13494
14739
  additionalProperties: false
13495
- 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).
13496
14741
  UpdateMetricRequest:
13497
14742
  oneOf:
13498
14743
  - $ref: '#/components/schemas/UpdateMetricWithQuery'
@@ -13591,6 +14836,14 @@ components:
13591
14836
  v:
13592
14837
  type: number
13593
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.
13594
14847
  required:
13595
14848
  - t
13596
14849
  - v
@@ -13634,7 +14887,8 @@ components:
13634
14887
  type: string
13635
14888
  minLength: 1
13636
14889
  maxLength: 64
13637
- 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.
13638
14892
  type:
13639
14893
  type: string
13640
14894
  enum:
@@ -13650,11 +14904,14 @@ components:
13650
14904
  default: ''
13651
14905
  description: Human-readable description of the property.
13652
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
13653
14910
  required:
13654
14911
  - name
13655
14912
  - type
13656
14913
  additionalProperties: false
13657
- 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.
13658
14915
  description: Typed properties declared on the event.
13659
14916
  pending:
13660
14917
  type: integer
@@ -13679,8 +14936,8 @@ components:
13679
14936
  properties:
13680
14937
  name:
13681
14938
  type: string
13682
- pattern: ^[a-zA-Z0-9_][a-zA-Z0-9_\-.]{0,127}$
13683
- 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.
13684
14941
  folder:
13685
14942
  $ref: '#/components/schemas/Folder'
13686
14943
  description:
@@ -13697,7 +14954,8 @@ components:
13697
14954
  type: string
13698
14955
  minLength: 1
13699
14956
  maxLength: 64
13700
- 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.
13701
14959
  type:
13702
14960
  type: string
13703
14961
  enum:
@@ -13769,7 +15027,8 @@ components:
13769
15027
  type: string
13770
15028
  minLength: 1
13771
15029
  maxLength: 64
13772
- 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.
13773
15032
  type:
13774
15033
  type: string
13775
15034
  enum:
@@ -13785,11 +15044,14 @@ components:
13785
15044
  default: ''
13786
15045
  description: Human-readable description of the property.
13787
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
13788
15050
  required:
13789
15051
  - name
13790
15052
  - type
13791
15053
  additionalProperties: false
13792
- 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.
13793
15055
  description: Typed properties declared on the event.
13794
15056
  pending:
13795
15057
  type: integer
@@ -13836,7 +15098,8 @@ components:
13836
15098
  type: string
13837
15099
  minLength: 1
13838
15100
  maxLength: 64
13839
- 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.
13840
15103
  type:
13841
15104
  type: string
13842
15105
  enum:
@@ -13887,7 +15150,8 @@ components:
13887
15150
  type: string
13888
15151
  minLength: 1
13889
15152
  maxLength: 64
13890
- 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.
13891
15155
  type:
13892
15156
  type: string
13893
15157
  enum:
@@ -13945,6 +15209,7 @@ components:
13945
15209
  enum:
13946
15210
  - pr_ready
13947
15211
  - pr_merged
15212
+ - pr_closed
13948
15213
  - investigated
13949
15214
  - detected
13950
15215
  - question
@@ -13952,7 +15217,7 @@ components:
13952
15217
  - pending_approval
13953
15218
  - working
13954
15219
  - backlog
13955
- 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".'
13956
15221
  OpsBrowserContext:
13957
15222
  type: object
13958
15223
  properties:
@@ -14112,20 +15377,31 @@ components:
14112
15377
  type: string
14113
15378
  enum:
14114
15379
  - metric_rule
15380
+ - killswitch_armed
14115
15381
  - experiment_srm
14116
15382
  - experiment_peek
14117
15383
  - guardrail
14118
- description: What kind of alert transitioned to active.
15384
+ description: What kind of condition opened this alert instance.
14119
15385
  dedupeKey:
14120
15386
  type: string
14121
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.
14122
15398
  severity:
14123
15399
  type: string
14124
15400
  enum:
14125
15401
  - danger
14126
15402
  - warn
14127
15403
  - info
14128
- 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.
14129
15405
  observedValue:
14130
15406
  anyOf:
14131
15407
  - type: number
@@ -14144,7 +15420,7 @@ components:
14144
15420
  - resolved
14145
15421
  - dismissed
14146
15422
  - type: 'null'
14147
- 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.
14148
15424
  activeSince:
14149
15425
  anyOf:
14150
15426
  - type: string
@@ -14168,9 +15444,8 @@ components:
14168
15444
  required:
14169
15445
  - source
14170
15446
  - dedupeKey
14171
- - severity
14172
15447
  additionalProperties: {}
14173
- 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.
14174
15449
  MeasurePlanResource:
14175
15450
  type: object
14176
15451
  properties:
@@ -14274,7 +15549,7 @@ components:
14274
15549
  properties:
14275
15550
  t:
14276
15551
  type: number
14277
- 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).
14278
15553
  v:
14279
15554
  type: number
14280
15555
  description: Occurrence count in the bucket (sample-rate weighted).
@@ -14282,11 +15557,11 @@ components:
14282
15557
  - t
14283
15558
  - v
14284
15559
  additionalProperties: {}
14285
- description: One hourly occurrence bucket.
14286
- 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.
14287
15562
  domainFrom:
14288
15563
  type: number
14289
- description: Chart domain start — unix epoch milliseconds (24h before the read).
15564
+ description: Chart domain start — unix epoch milliseconds (the window's start).
14290
15565
  domainTo:
14291
15566
  type: number
14292
15567
  description: Chart domain end — unix epoch milliseconds (the read time).
@@ -14295,7 +15570,7 @@ components:
14295
15570
  - domainFrom
14296
15571
  - domainTo
14297
15572
  additionalProperties: {}
14298
- 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.'
14299
15574
  additionalProperties: {}
14300
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.
14301
15576
  GithubPrLink:
@@ -14319,6 +15594,25 @@ components:
14319
15594
  - closes
14320
15595
  - comment
14321
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`.
14322
15616
  required:
14323
15617
  - number
14324
15618
  - url
@@ -14691,6 +15985,11 @@ components:
14691
15985
  additionalProperties: {}
14692
15986
  description: One queue item — any type — plus its resolved `owner` and run state.
14693
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.
14694
15993
  CreateBugRequest:
14695
15994
  type: object
14696
15995
  properties:
@@ -14743,10 +16042,11 @@ components:
14743
16042
  description: Emails of teammates to subscribe to this item's Slack pings at creation.
14744
16043
  tags:
14745
16044
  type: array
16045
+ maxItems: 50
14746
16046
  items:
14747
- type: string
16047
+ $ref: '#/components/schemas/TagName'
14748
16048
  default: []
14749
- 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.
14750
16050
  reporterEmail:
14751
16051
  anyOf:
14752
16052
  - type: string
@@ -14834,10 +16134,11 @@ components:
14834
16134
  description: Emails of teammates to subscribe to this item's Slack pings at creation.
14835
16135
  tags:
14836
16136
  type: array
16137
+ maxItems: 50
14837
16138
  items:
14838
- type: string
16139
+ $ref: '#/components/schemas/TagName'
14839
16140
  default: []
14840
- 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.
14841
16142
  reporterEmail:
14842
16143
  anyOf:
14843
16144
  - type: string
@@ -14897,45 +16198,243 @@ components:
14897
16198
  - id
14898
16199
  additionalProperties: false
14899
16200
  description: Response for `POST /api/admin/ops`.
14900
- OpsItemAttachment:
16201
+ CreatePublicBugRequest:
14901
16202
  type: object
14902
16203
  properties:
14903
- id:
14904
- type: string
14905
- description: Attachment id.
14906
- kind:
14907
- type: string
14908
- description: '`screenshot` | `video` | `log`.'
14909
- filename:
14910
- type: string
14911
- description: Sanitized original filename.
14912
- mimeType:
16204
+ title:
14913
16205
  type: string
14914
- description: MIME type, e.g. `image/png`.
14915
- sizeBytes:
14916
- type: number
14917
- description: Upload size in bytes.
14918
- fetchUrl:
16206
+ minLength: 1
16207
+ maxLength: 200
16208
+ pattern: ^\S(.*\S)?$
16209
+ description: One-line bug title (no leading/trailing whitespace).
16210
+ stepsToReproduce:
14919
16211
  type: string
14920
- description: Admin-API path to download the file (`GET` with the admin/ops key — screenshots render inline; recordings can't be watched).
14921
- required:
14922
- - id
14923
- - kind
14924
- - filename
14925
- - mimeType
14926
- - sizeBytes
14927
- - fetchUrl
14928
- additionalProperties: {}
14929
- description: A file uploaded with a bug/feature report.
14930
- OpsItemRelated:
14931
- type: object
14932
- properties:
14933
- dashboard:
16212
+ maxLength: 8000
16213
+ default: ''
16214
+ description: How to reproduce the bug.
16215
+ actualResult:
14934
16216
  type: string
14935
- description: Dashboard-relative deep link to this item.
14936
- error:
16217
+ maxLength: 8000
16218
+ default: ''
16219
+ description: What actually happened.
16220
+ expectedResult:
14937
16221
  type: string
14938
- description: Admin-API path to the tracked error (error tickets only).
16222
+ maxLength: 8000
16223
+ default: ''
16224
+ description: What was expected instead.
16225
+ priority:
16226
+ anyOf:
16227
+ - $ref: '#/components/schemas/OpsItemPriority'
16228
+ - type: 'null'
16229
+ description: Initial triage priority, or `null`.
16230
+ status:
16231
+ anyOf:
16232
+ - $ref: '#/components/schemas/OpsItemStatus'
16233
+ description: Initial lifecycle status; defaults to `open` when omitted.
16234
+ assigneeId:
16235
+ anyOf:
16236
+ - type: string
16237
+ - type: 'null'
16238
+ description: The `users.id` of the person to assign as owner at creation, or `null`.
16239
+ subscribers:
16240
+ type: array
16241
+ items:
16242
+ type: string
16243
+ format: email
16244
+ default: []
16245
+ description: Emails of teammates to subscribe to this item's Slack pings at creation.
16246
+ tags:
16247
+ type: array
16248
+ maxItems: 50
16249
+ items:
16250
+ $ref: '#/components/schemas/TagName'
16251
+ default: []
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.'
16253
+ reporterEmail:
16254
+ anyOf:
16255
+ - type: string
16256
+ format: email
16257
+ - type: 'null'
16258
+ description: Email of the reporter, or `null`.
16259
+ pageUrl:
16260
+ anyOf:
16261
+ - type: string
16262
+ format: uri
16263
+ - type: 'null'
16264
+ description: URL of the page the bug relates to, or `null`.
16265
+ userAgent:
16266
+ anyOf:
16267
+ - type: string
16268
+ maxLength: 500
16269
+ - type: 'null'
16270
+ description: Reporter's user-agent string, or `null`.
16271
+ viewport:
16272
+ anyOf:
16273
+ - type: string
16274
+ maxLength: 40
16275
+ - type: 'null'
16276
+ description: Reporter's viewport (e.g. `1280x720`), or `null`.
16277
+ context:
16278
+ anyOf:
16279
+ - type: object
16280
+ additionalProperties: {}
16281
+ - type: 'null'
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.'
16287
+ notify:
16288
+ anyOf:
16289
+ - $ref: '#/components/schemas/NotificationTarget'
16290
+ - type: 'null'
16291
+ description: Where this bug's completion notification lands.
16292
+ required:
16293
+ - title
16294
+ additionalProperties: false
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`.
16296
+ CreatePublicTicketResponse:
16297
+ type: object
16298
+ properties:
16299
+ id:
16300
+ type: string
16301
+ description: The new item's id. Absent on a deduped response — nothing was created, so there is no new id to report.
16302
+ number:
16303
+ type: integer
16304
+ description: Per-project item number. On a deduped response this is the number of the OPEN ticket already tracking the report, which is what a caller should show the user either way.
16305
+ deduped:
16306
+ type: boolean
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.'
16311
+ required:
16312
+ - number
16313
+ additionalProperties: false
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.'
16315
+ CreatePublicFeatureRequestRequest:
16316
+ type: object
16317
+ properties:
16318
+ title:
16319
+ type: string
16320
+ minLength: 1
16321
+ maxLength: 200
16322
+ pattern: ^\S(.*\S)?$
16323
+ description: One-line feature-request title (no leading/trailing whitespace).
16324
+ description:
16325
+ type: string
16326
+ maxLength: 8000
16327
+ default: ''
16328
+ description: What the feature is.
16329
+ useCase:
16330
+ type: string
16331
+ maxLength: 8000
16332
+ default: ''
16333
+ description: Why it's needed / the use case.
16334
+ priority:
16335
+ anyOf:
16336
+ - $ref: '#/components/schemas/OpsItemPriority'
16337
+ - type: 'null'
16338
+ description: Initial triage priority, or `null`.
16339
+ status:
16340
+ anyOf:
16341
+ - $ref: '#/components/schemas/OpsItemStatus'
16342
+ description: Initial lifecycle status; defaults to `open` when omitted.
16343
+ assigneeId:
16344
+ anyOf:
16345
+ - type: string
16346
+ - type: 'null'
16347
+ description: The `users.id` of the person to assign as owner at creation, or `null`.
16348
+ subscribers:
16349
+ type: array
16350
+ items:
16351
+ type: string
16352
+ format: email
16353
+ default: []
16354
+ description: Emails of teammates to subscribe to this item's Slack pings at creation.
16355
+ tags:
16356
+ type: array
16357
+ maxItems: 50
16358
+ items:
16359
+ $ref: '#/components/schemas/TagName'
16360
+ default: []
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.'
16362
+ reporterEmail:
16363
+ anyOf:
16364
+ - type: string
16365
+ format: email
16366
+ - type: 'null'
16367
+ description: Email of the reporter, or `null`.
16368
+ pageUrl:
16369
+ anyOf:
16370
+ - type: string
16371
+ format: uri
16372
+ - type: 'null'
16373
+ description: URL of the page the request relates to, or `null`.
16374
+ userAgent:
16375
+ anyOf:
16376
+ - type: string
16377
+ maxLength: 500
16378
+ - type: 'null'
16379
+ description: Reporter's user-agent string, or `null`.
16380
+ context:
16381
+ anyOf:
16382
+ - type: object
16383
+ additionalProperties: {}
16384
+ - type: 'null'
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.'
16390
+ notify:
16391
+ anyOf:
16392
+ - $ref: '#/components/schemas/NotificationTarget'
16393
+ - type: 'null'
16394
+ description: Where this request's completion notification lands.
16395
+ required:
16396
+ - title
16397
+ additionalProperties: false
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`.
16399
+ OpsItemAttachment:
16400
+ type: object
16401
+ properties:
16402
+ id:
16403
+ type: string
16404
+ description: Attachment id.
16405
+ kind:
16406
+ type: string
16407
+ description: '`screenshot` | `video` | `log`.'
16408
+ filename:
16409
+ type: string
16410
+ description: Sanitized original filename.
16411
+ mimeType:
16412
+ type: string
16413
+ description: MIME type, e.g. `image/png`.
16414
+ sizeBytes:
16415
+ type: number
16416
+ description: Upload size in bytes.
16417
+ fetchUrl:
16418
+ type: string
16419
+ description: Admin-API path to download the file (`GET` with the admin/ops key — screenshots render inline; recordings can't be watched).
16420
+ required:
16421
+ - id
16422
+ - kind
16423
+ - filename
16424
+ - mimeType
16425
+ - sizeBytes
16426
+ - fetchUrl
16427
+ additionalProperties: {}
16428
+ description: A file uploaded with a bug/feature report.
16429
+ OpsItemRelated:
16430
+ type: object
16431
+ properties:
16432
+ dashboard:
16433
+ type: string
16434
+ description: Dashboard-relative deep link to this item.
16435
+ error:
16436
+ type: string
16437
+ description: Admin-API path to the tracked error (error tickets only).
14939
16438
  alertRule:
14940
16439
  type: string
14941
16440
  description: Admin-API path to the alert rule (metric-rule alerts only).
@@ -15240,6 +16739,10 @@ components:
15240
16739
  type: string
15241
16740
  maxLength: 300
15242
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.'
15243
16746
  additionalProperties: false
15244
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.
15245
16748
  AckOpsItemResponse:
@@ -15661,6 +17164,253 @@ components:
15661
17164
  additionalProperties: false
15662
17165
  minProperties: 1
15663
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.
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:
17175
+ type: object
17176
+ properties:
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`.
17187
+ required:
17188
+ - selected
17189
+ - other
17190
+ additionalProperties: false
17191
+ description: A person's answer to a question.
17192
+ OpsQuestionDelivery:
17193
+ type: string
17194
+ enum:
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:
17199
+ type: object
17200
+ properties:
17201
+ id:
17202
+ type: string
17203
+ description: Question id.
17204
+ feedbackId:
17205
+ type: string
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:
17243
+ anyOf:
17244
+ - type: string
17245
+ - type: 'null'
17246
+ description: Email of the person who answered, or `null`.
17247
+ answeredAt:
17248
+ anyOf:
17249
+ - type: string
17250
+ - type: 'null'
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`.
17285
+ createdAt:
17286
+ type: string
17287
+ description: ISO-8601 creation timestamp.
17288
+ updatedAt:
17289
+ type: string
17290
+ description: ISO-8601 last-update timestamp.
17291
+ required:
17292
+ - id
17293
+ - feedbackId
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
17308
+ - createdAt
17309
+ - updatedAt
17310
+ additionalProperties: false
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:
17318
+ type: object
17319
+ properties:
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).
15664
17414
  OpsAgentProfile:
15665
17415
  type: object
15666
17416
  properties:
@@ -15854,6 +17604,27 @@ components:
15854
17604
  - channels
15855
17605
  additionalProperties: false
15856
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
15857
17628
  ListAlertRulesResponse:
15858
17629
  type: array
15859
17630
  items:
@@ -15873,6 +17644,15 @@ components:
15873
17644
  - type: string
15874
17645
  - type: 'null'
15875
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.
15876
17656
  comparator:
15877
17657
  type: string
15878
17658
  enum:
@@ -15880,15 +17660,90 @@ components:
15880
17660
  - gte
15881
17661
  - lt
15882
17662
  - lte
15883
- 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.
15884
17664
  threshold:
15885
17665
  type: number
15886
- 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.
15887
17732
  windowHours:
15888
17733
  type: integer
15889
17734
  minimum: -9007199254740991
15890
17735
  maximum: 9007199254740991
15891
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.
15892
17747
  severity:
15893
17748
  type: string
15894
17749
  enum:
@@ -15912,9 +17767,25 @@ components:
15912
17767
  - name
15913
17768
  - metricId
15914
17769
  - metricName
17770
+ - kind
15915
17771
  - comparator
15916
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
15917
17786
  - windowHours
17787
+ - bucketMinutes
17788
+ - requiredBuckets
15918
17789
  - severity
15919
17790
  - enabled
15920
17791
  - notify
@@ -15934,26 +17805,122 @@ components:
15934
17805
  type: string
15935
17806
  minLength: 1
15936
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
15937
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.
15938
17821
  type: string
15939
17822
  enum:
15940
17823
  - gt
15941
17824
  - gte
15942
17825
  - lt
15943
17826
  - lte
15944
- description: How the metric value is compared to the threshold (gt/gte/lt/lte).
15945
17827
  threshold:
15946
17828
  type: number
15947
- 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.'
15948
17859
  windowHours:
15949
17860
  default: 24
15950
17861
  description: Lookback window (hours) the metric is aggregated over. 1–720.
15951
17862
  type: integer
15952
17863
  minimum: 1
15953
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'
15954
17921
  severity:
15955
17922
  default: warn
15956
- description: Severity of the raised alert.
17923
+ description: Severity of the raised alert. A `warnThreshold` breach opens one step quieter than this.
15957
17924
  type: string
15958
17925
  enum:
15959
17926
  - danger
@@ -15968,8 +17935,6 @@ components:
15968
17935
  required:
15969
17936
  - name
15970
17937
  - metricId
15971
- - comparator
15972
- - threshold
15973
17938
  additionalProperties: false
15974
17939
  CreateAlertRuleResponse:
15975
17940
  type: object
@@ -15996,6 +17961,14 @@ components:
15996
17961
  type: string
15997
17962
  minLength: 1
15998
17963
  maxLength: 120
17964
+ kind:
17965
+ type: string
17966
+ enum:
17967
+ - normal
17968
+ - anomaly
17969
+ - outliers
17970
+ - no_data
17971
+ - composite
15999
17972
  comparator:
16000
17973
  type: string
16001
17974
  enum:
@@ -16005,10 +17978,93 @@ components:
16005
17978
  - lte
16006
17979
  threshold:
16007
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'
16008
18051
  windowHours:
16009
18052
  type: integer
16010
18053
  minimum: 1
16011
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".
16012
18068
  severity:
16013
18069
  type: string
16014
18070
  enum:
@@ -16032,13 +18088,15 @@ components:
16032
18088
  AlertApiRow:
16033
18089
  type: object
16034
18090
  description: |-
16035
- 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.
16036
18094
 
16037
18095
  Every field is always present on the wire; nullable fields are `null`, never absent.
16038
18096
  properties:
16039
18097
  id:
16040
18098
  type: string
16041
- description: Stable opaque alert id.
18099
+ description: Stable opaque instance id — also the id of the ops queue item this instance is.
16042
18100
  source:
16043
18101
  type: string
16044
18102
  enum:
@@ -16059,10 +18117,10 @@ components:
16059
18117
  - danger
16060
18118
  - warn
16061
18119
  - info
16062
- 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.
16063
18121
  dedupeKey:
16064
18122
  type: string
16065
- 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`.
16066
18124
  title:
16067
18125
  type: string
16068
18126
  description: Short human-readable headline (what fired).
@@ -16085,10 +18143,10 @@ components:
16085
18143
  - active
16086
18144
  - resolved
16087
18145
  - dismissed
16088
- 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`).
16089
18147
  createdAt:
16090
18148
  type: string
16091
- description: ISO-8601 timestamp the alert was first raised.
18149
+ description: ISO-8601 timestamp this instance opened — when the condition started firing.
16092
18150
  resolvedAt:
16093
18151
  description: ISO-8601 timestamp the alert flipped to `resolved`, or `null` while it hasn't.
16094
18152
  anyOf:
@@ -16150,7 +18208,7 @@ components:
16150
18208
  - active
16151
18209
  - resolved
16152
18210
  - dismissed
16153
- 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.
16154
18212
  assigneeId:
16155
18213
  description: PERSON owner — a `users.id`, or `null` to clear the assignment.
16156
18214
  anyOf:
@@ -16336,6 +18394,12 @@ components:
16336
18394
  - type: boolean
16337
18395
  - type: number
16338
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.
16339
18403
  stripeSubscriptionId:
16340
18404
  anyOf:
16341
18405
  - type: string
@@ -16415,6 +18479,8 @@ components:
16415
18479
  - moduleUser
16416
18480
  - moduleEvents
16417
18481
  - allowPublicTickets
18482
+ - errorAutocloseDays
18483
+ - errorTicketMinOccurrences
16418
18484
  - slug
16419
18485
  - logo
16420
18486
  - defaultEnv
@@ -16633,6 +18699,16 @@ components:
16633
18699
  minimum: 0.0001
16634
18700
  maximum: 0.05
16635
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.
16636
18712
  additionalProperties: false
16637
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.
16638
18714
  ListI18nProfilesResponse:
@@ -17701,21 +19777,50 @@ components:
17701
19777
  - config
17702
19778
  additionalProperties: false
17703
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.
17704
19795
  CursorTriggerConfig:
17705
19796
  type: object
17706
19797
  properties:
17707
19798
  repoUrl:
17708
19799
  type: string
17709
19800
  format: uri
17710
- 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.'
17711
19802
  startingRef:
17712
19803
  type: string
17713
19804
  minLength: 1
17714
- 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).
17715
19812
  projectId:
17716
19813
  type: string
17717
19814
  minLength: 1
17718
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.
17719
19824
  required:
17720
19825
  - repoUrl
17721
19826
  - projectId
@@ -17750,7 +19855,7 @@ components:
17750
19855
  opsKey:
17751
19856
  type: string
17752
19857
  minLength: 1
17753
- 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.
17754
19859
  enabled:
17755
19860
  type: boolean
17756
19861
  default: true
@@ -17762,13 +19867,33 @@ components:
17762
19867
  - opsKey
17763
19868
  additionalProperties: false
17764
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.
17765
19890
  CopilotTriggerConfig:
17766
19891
  type: object
17767
19892
  properties:
17768
19893
  owner:
17769
19894
  type: string
17770
19895
  minLength: 1
17771
- 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.'
17772
19897
  repo:
17773
19898
  type: string
17774
19899
  minLength: 1
@@ -17776,11 +19901,21 @@ components:
17776
19901
  baseRef:
17777
19902
  type: string
17778
19903
  minLength: 1
17779
- 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.
17780
19911
  projectId:
17781
19912
  type: string
17782
19913
  minLength: 1
17783
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.
17784
19919
  required:
17785
19920
  - owner
17786
19921
  - repo
@@ -17838,6 +19973,10 @@ components:
17838
19973
  type: string
17839
19974
  minLength: 1
17840
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.
17841
19980
  required:
17842
19981
  - source
17843
19982
  - projectId
@@ -18037,7 +20176,7 @@ components:
18037
20176
  - provider
18038
20177
  - config
18039
20178
  additionalProperties: false
18040
- 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).'
18041
20180
  UpdateCopilotTriggerRequest:
18042
20181
  type: object
18043
20182
  properties:
@@ -18163,6 +20302,11 @@ components:
18163
20302
  - type: string
18164
20303
  - type: 'null'
18165
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}`.
18166
20310
  scopes:
18167
20311
  anyOf:
18168
20312
  - type: array
@@ -18184,6 +20328,7 @@ components:
18184
20328
  - expires_at
18185
20329
  - created_by_email
18186
20330
  - name
20331
+ - description
18187
20332
  - scopes
18188
20333
  - last4
18189
20334
  additionalProperties: false
@@ -18222,8 +20367,8 @@ components:
18222
20367
  name:
18223
20368
  type: string
18224
20369
  minLength: 1
18225
- maxLength: 80
18226
- description: Optional human label. Programmatic (API) mints that omit it get an auto-generated descriptive name; dashboard mints may leave it blank.
20370
+ maxLength: 160
20371
+ description: Optional human label. Programmatic (API) mints that omit it get an auto-generated descriptive name; dashboard mints may leave it blank. The cap is generous because provenance labels are composed, not typed — `shipeasy setup` mints keys named with the stack, package, date, and operator that produced them, which does not fit in a one-line-input budget.
18227
20372
  scopes:
18228
20373
  type: array
18229
20374
  items:
@@ -18291,6 +20436,30 @@ components:
18291
20436
  - key
18292
20437
  - expires_at
18293
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
18294
20463
  RevokeKeyResponse:
18295
20464
  type: object
18296
20465
  description: Confirms the key is revoked. Idempotent — revoking an already-revoked key returns the same shape.
@@ -18351,6 +20520,85 @@ components:
18351
20520
  required:
18352
20521
  - hits
18353
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.
18354
20602
  responses:
18355
20603
  BadRequest:
18356
20604
  description: The request was malformed (bad JSON or missing project scope).
@@ -18459,6 +20707,45 @@ components:
18459
20707
  value:
18460
20708
  error: The referenced event is still pending review and cannot back a metric yet.
18461
20709
  code: EVENT_PENDING
20710
+ PayloadTooLarge:
20711
+ description: The request body exceeded the intake's fixed size cap (16 KB).
20712
+ content:
20713
+ application/json:
20714
+ schema:
20715
+ $ref: '#/components/schemas/Error'
20716
+ examples:
20717
+ BAD_REQUEST:
20718
+ summary: Body too large
20719
+ value:
20720
+ error: Body too large
20721
+ code: BAD_REQUEST
20722
+ TooManyRequests:
20723
+ description: |-
20724
+ The public intake is rate-limited per (project, client IP). Back off and retry;
20725
+ the limit is a burst guard, not a quota, so a modest delay clears it.
20726
+ content:
20727
+ application/json:
20728
+ schema:
20729
+ $ref: '#/components/schemas/Error'
20730
+ examples:
20731
+ BAD_REQUEST:
20732
+ summary: Rate limited
20733
+ value:
20734
+ error: Too many requests
20735
+ code: BAD_REQUEST
20736
+ ServiceUnavailable:
20737
+ description: |-
20738
+ A backing store the endpoint needs is not bound in this environment. Transient
20739
+ and infrastructural — no stable `ErrorCode` is assigned, so `code` is omitted.
20740
+ content:
20741
+ application/json:
20742
+ schema:
20743
+ $ref: '#/components/schemas/Error'
20744
+ examples:
20745
+ UNAVAILABLE:
20746
+ summary: Storage unbound
20747
+ value:
20748
+ error: Unavailable
18462
20749
  BadGateway:
18463
20750
  description: |-
18464
20751
  The request was well-formed and the resource exists, but a downstream provider