@maudecode/kanvil 0.6.1 → 0.8.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 (134) hide show
  1. package/apps/server/dist/{auth-CFjetlB4.mjs → auth-CVnX2XIg.mjs} +3828 -3463
  2. package/apps/server/dist/client.mjs +2 -2
  3. package/apps/server/dist/{config-BQw6mPzE.mjs → config-DUIdB3c2.mjs} +1 -1
  4. package/apps/server/dist/index.mjs +165 -43
  5. package/apps/server/dist/migrate.mjs +1 -1
  6. package/apps/server/dist/recover.mjs +3 -3
  7. package/apps/server/dist/{src-BaTNDJB7.mjs → src-CC6lPIU_.mjs} +190 -35
  8. package/apps/server/drizzle/0040_upload_urls.sql +20 -0
  9. package/apps/server/drizzle/0041_project_repositories.sql +2 -0
  10. package/apps/server/drizzle/0042_public_boards.sql +37 -0
  11. package/apps/server/drizzle/0043_epics.sql +18 -0
  12. package/apps/server/drizzle/meta/0040_snapshot.json +7170 -0
  13. package/apps/server/drizzle/meta/0041_snapshot.json +7183 -0
  14. package/apps/server/drizzle/meta/0042_snapshot.json +7315 -0
  15. package/apps/server/drizzle/meta/0043_snapshot.json +7466 -0
  16. package/apps/server/drizzle/meta/_journal.json +28 -0
  17. package/apps/web/dist/assets/{CompositeItem-Dotj8JZL.js → CompositeItem-GgAhkI4m.js} +1 -1
  18. package/apps/web/dist/assets/{Trans-CE4EAAY1.js → Trans-CT9xiBED.js} +1 -1
  19. package/apps/web/dist/assets/admin-settings-C7yxWm-w.js +1 -0
  20. package/apps/web/dist/assets/{archive-restore-BMtgjFUu.js → archive-restore-UgUTJyLs.js} +1 -1
  21. package/apps/web/dist/assets/az-AZ-IkRP1MKo.js +1 -0
  22. package/apps/web/dist/assets/{az-B-DxMc__.js → az-CZ46yOUl.js} +1 -1
  23. package/apps/web/dist/assets/configuration-CU0QOaRw.js +1 -0
  24. package/apps/web/dist/assets/{corner-left-up-O4OsTW-L.js → corner-left-up-BFY7AqdS.js} +1 -1
  25. package/apps/web/dist/assets/{de-Btbkq-aX.js → de-CseN-3gF.js} +1 -1
  26. package/apps/web/dist/assets/de-DE-DCGKtpLI.js +1 -0
  27. package/apps/web/dist/assets/default-CN9zcRIy.js +56 -0
  28. package/apps/web/dist/assets/delivery-settings-xx9Bb6Ux.js +1 -0
  29. package/apps/web/dist/assets/el-GR-C5w6p2-z.js +1 -0
  30. package/apps/web/dist/assets/{el-DBVEX16k.js → el-psH0HOR3.js} +1 -1
  31. package/apps/web/dist/assets/{ellipsis-CvEPDi9V.js → ellipsis-CcIJddV1.js} +1 -1
  32. package/apps/web/dist/assets/{es-CMJoxZBk.js → es-BVvGrD23.js} +1 -1
  33. package/apps/web/dist/assets/es-ES-PjJlJCgT.js +1 -0
  34. package/apps/web/dist/assets/{forms-DbfnnPg_.js → forms-BUNl5uNV.js} +1 -1
  35. package/apps/web/dist/assets/{fr-OyEmYoRn.js → fr-C43nghuI.js} +1 -1
  36. package/apps/web/dist/assets/fr-FR-CGWH-Juq.js +1 -0
  37. package/apps/web/dist/assets/{hi-CIgXSseB.js → hi-Ds3DGtT-.js} +1 -1
  38. package/apps/web/dist/assets/hi-IN-B3bq8hDJ.js +1 -0
  39. package/apps/web/dist/assets/{highlight-B4K3yEEh.js → highlight-BPZqoBaO.js} +1 -1
  40. package/apps/web/dist/assets/id-ID-98H240ET.js +1 -0
  41. package/apps/web/dist/assets/{id-WV2oSAPB.js → id-tg9awgd8.js} +1 -1
  42. package/apps/web/dist/assets/index-CvFGdLre.js +17 -0
  43. package/apps/web/dist/assets/index-DPT5LAs1.css +2 -0
  44. package/apps/web/dist/assets/{invitations-BTB8f2Xl.js → invitations-fH1M9hYm.js} +1 -1
  45. package/apps/web/dist/assets/{it-BWSrRqus.js → it-DRwaWQyE.js} +1 -1
  46. package/apps/web/dist/assets/it-IT-CNS9cL7I.js +1 -0
  47. package/apps/web/dist/assets/{item-HmFuRZet.js → item-BbaYy50i.js} +1 -1
  48. package/apps/web/dist/assets/ja-JP-BFTqtCdV.js +1 -0
  49. package/apps/web/dist/assets/{ja-BtOHWvUJ.js → ja-jj4Ubayf.js} +1 -1
  50. package/apps/web/dist/assets/{ko-DAryK0HN.js → ko-Dn2pRrar.js} +1 -1
  51. package/apps/web/dist/assets/ko-KR-C58VNccr.js +1 -0
  52. package/apps/web/dist/assets/{label-settings-ssfEX2uR.js → label-settings-CbBHSL0c.js} +1 -1
  53. package/apps/web/dist/assets/{markdown-blocks-B1kO6iku.js → markdown-blocks-B6mE_S6n.js} +1 -1
  54. package/apps/web/dist/assets/{member-directory-BA4ZT1qU.js → member-directory-CoGg7WkW.js} +1 -1
  55. package/apps/web/dist/assets/{mk-DBbiGUk5.js → mk-DQUnXaPE.js} +1 -1
  56. package/apps/web/dist/assets/mk-MK-Bf5RDo6j.js +1 -0
  57. package/apps/web/dist/assets/{nl-D4V-8WdF.js → nl-C_Ijir3O.js} +1 -1
  58. package/apps/web/dist/assets/nl-NL-D1WFJyOY.js +1 -0
  59. package/apps/web/dist/assets/{pl-C--QrxO0.js → pl-DgWXgYL-.js} +1 -1
  60. package/apps/web/dist/assets/pl-PL--jAcwyAK.js +1 -0
  61. package/apps/web/dist/assets/project-lifecycle-BHWCMq7o.js +1 -0
  62. package/apps/web/dist/assets/project-settings-CBG5CZX1.js +1 -0
  63. package/apps/web/dist/assets/pt-BR-CJQfuq9b.js +1 -0
  64. package/apps/web/dist/assets/{pt-BR-DvFVKpmO.js → pt-BR-N-Xvp9fC.js} +1 -1
  65. package/apps/web/dist/assets/public-project-DmTr9q4c.js +1 -0
  66. package/apps/web/dist/assets/{rotate-ccw-CB36Zu3L.js → rotate-ccw-BFYpBtuj.js} +1 -1
  67. package/apps/web/dist/assets/{ru-UmbPUOka.js → ru-Ctq-5FYl.js} +1 -1
  68. package/apps/web/dist/assets/ru-RU-BtvJ8Glt.js +1 -0
  69. package/apps/web/dist/assets/{search-Cvetr1OK.js → search-C2QaoLwI.js} +6 -6
  70. package/apps/web/dist/assets/{send-CJDct4Oj.js → send-qRPqbT2m.js} +1 -1
  71. package/apps/web/dist/assets/settings-BNPajW83.js +1 -0
  72. package/apps/web/dist/assets/{sliders-horizontal-dmqWngxy.js → sliders-horizontal-S16oyCiS.js} +1 -1
  73. package/apps/web/dist/assets/{switch-BtScTpP9.js → switch-C44Ouoqh.js} +1 -1
  74. package/apps/web/dist/assets/{table-byFokzo-.js → table-esnQpDtb.js} +1 -1
  75. package/apps/web/dist/assets/ticket-detail-BVyScTlg.js +2 -0
  76. package/apps/web/dist/assets/ticket-editor-BjuSeeiA.js +2 -0
  77. package/apps/web/dist/assets/ticket-list-BCvUvYju.js +6 -0
  78. package/apps/web/dist/assets/ticket-list-controls-BiC5CIwM.js +1 -0
  79. package/apps/web/dist/assets/{tr-CNpgQ4ka.js → tr-CYT8BT69.js} +1 -1
  80. package/apps/web/dist/assets/tr-TR-Bahy-EKO.js +1 -0
  81. package/apps/web/dist/assets/uk-UA-DDVgxWqX.js +1 -0
  82. package/apps/web/dist/assets/{uk-MQoTXdpk.js → uk-y7QuyfrN.js} +1 -1
  83. package/apps/web/dist/assets/{upload-D1x_ib3B.js → upload-e7CmLHhp.js} +1 -1
  84. package/apps/web/dist/assets/{useQueries-BnlNCoy4.js → useQueries-U3MOyy3t.js} +1 -1
  85. package/apps/web/dist/assets/{vi-CEw4gTWc.js → vi-Bz2g5X1k.js} +1 -1
  86. package/apps/web/dist/assets/vi-VN-CuHCYxNQ.js +1 -0
  87. package/apps/web/dist/assets/{visual-editor-CpdMRn3e.js → visual-editor-UmVaDXWu.js} +1 -1
  88. package/apps/web/dist/assets/workspace-settings-BiWJqW0m.js +1 -0
  89. package/apps/web/dist/assets/zh-CN-C94sYOI_.js +1 -0
  90. package/apps/web/dist/assets/{zh-CN-ChzoXkfr.js → zh-CN-y_sluDUm.js} +1 -1
  91. package/apps/web/dist/assets/zh-TW-BoRQfGQL.js +1 -0
  92. package/apps/web/dist/assets/{zh-TW-bWBKARrK.js → zh-TW-DIEfy239.js} +1 -1
  93. package/apps/web/dist/index.html +8 -8
  94. package/docs/api.md +63 -5
  95. package/docs/cli.md +1 -1
  96. package/docs/mobile.md +1 -1
  97. package/docs/plan.md +1 -0
  98. package/docs/releasing.md +1 -1
  99. package/package.json +1 -1
  100. package/apps/web/dist/assets/admin-settings-CO7nnD9T.js +0 -1
  101. package/apps/web/dist/assets/az-AZ-R8_HFWJr.js +0 -1
  102. package/apps/web/dist/assets/configuration-Bnt72HRu.js +0 -1
  103. package/apps/web/dist/assets/de-DE-BHgMj19Y.js +0 -1
  104. package/apps/web/dist/assets/default-DqsN66dq.js +0 -56
  105. package/apps/web/dist/assets/delivery-settings-Cji0N0m0.js +0 -1
  106. package/apps/web/dist/assets/el-GR-CQFGM5Sr.js +0 -1
  107. package/apps/web/dist/assets/es-ES-DZkcRVA4.js +0 -1
  108. package/apps/web/dist/assets/fr-FR-CRPAezXU.js +0 -1
  109. package/apps/web/dist/assets/hi-IN-2YFE4UQk.js +0 -1
  110. package/apps/web/dist/assets/id-ID-B-J0_QIW.js +0 -1
  111. package/apps/web/dist/assets/index-BAnwOrh1.css +0 -2
  112. package/apps/web/dist/assets/index-nltuyQtq.js +0 -17
  113. package/apps/web/dist/assets/it-IT-CJ0HYggO.js +0 -1
  114. package/apps/web/dist/assets/ja-JP-DDQS7D7b.js +0 -1
  115. package/apps/web/dist/assets/ko-KR-CkpoewEa.js +0 -1
  116. package/apps/web/dist/assets/mk-MK-C0RhS6XR.js +0 -1
  117. package/apps/web/dist/assets/nl-NL-BSKqlzcL.js +0 -1
  118. package/apps/web/dist/assets/pl-PL-CHz38SiU.js +0 -1
  119. package/apps/web/dist/assets/project-lifecycle-COZb9FqM.js +0 -1
  120. package/apps/web/dist/assets/project-settings-DwmTm9KA.js +0 -1
  121. package/apps/web/dist/assets/pt-BR-BotWHRiO.js +0 -1
  122. package/apps/web/dist/assets/public-project-B3hEkeCF.js +0 -1
  123. package/apps/web/dist/assets/ru-RU-DtOwNna6.js +0 -1
  124. package/apps/web/dist/assets/settings-3IXm9lJ-.js +0 -1
  125. package/apps/web/dist/assets/ticket-detail-C3YA3Hzc.js +0 -2
  126. package/apps/web/dist/assets/ticket-editor-CZnZQMG1.js +0 -2
  127. package/apps/web/dist/assets/ticket-list-BXUorfmj.js +0 -6
  128. package/apps/web/dist/assets/ticket-list-controls-BxGKCZMo.js +0 -1
  129. package/apps/web/dist/assets/tr-TR-CdG2alU2.js +0 -1
  130. package/apps/web/dist/assets/uk-UA-Ceu5gorw.js +0 -1
  131. package/apps/web/dist/assets/vi-VN-CxshgV4W.js +0 -1
  132. package/apps/web/dist/assets/workspace-settings-BickBtZx.js +0 -1
  133. package/apps/web/dist/assets/zh-CN-D7Yrjhd3.js +0 -1
  134. package/apps/web/dist/assets/zh-TW-DyF38kqx.js +0 -1
@@ -290,10 +290,15 @@ var en_US_default = {
290
290
  priority: "Priority",
291
291
  kind: "Kind",
292
292
  difficulty: "Difficulty",
293
+ automation: "Agent readiness",
293
294
  label: "Label",
294
295
  assigneeId: "Assignee",
295
296
  customField: "Field"
296
297
  },
298
+ automationFilter: {
299
+ ready: "Ready for agents",
300
+ manual: "Needs a person"
301
+ },
297
302
  sort: {
298
303
  priority: "Priority",
299
304
  updated: "Recently updated",
@@ -436,6 +441,10 @@ var en_US_default = {
436
441
  updated: "Time entry updated",
437
442
  deleted: "Time entry deleted"
438
443
  },
444
+ epic: {
445
+ created: "Epic created",
446
+ updated: "Epic updated"
447
+ },
439
448
  label: {
440
449
  created: "Label created",
441
450
  updated: "Label updated",
@@ -489,6 +498,7 @@ var en_US_default = {
489
498
  created: "Calendar feed created",
490
499
  revoked: "Calendar feed revoked"
491
500
  },
501
+ public_board: { updated: "Public board updated" },
492
502
  project: {
493
503
  created: "Project created",
494
504
  updated: "Project updated",
@@ -535,6 +545,7 @@ var en_US_default = {
535
545
  time_entry_conflict: "This actor already has a running entry on the ticket.",
536
546
  claim_conflict: "Another worker holds this ticket.",
537
547
  idempotency_conflict: "This request was already used for another change.",
548
+ duplicate_repository: "Another project in this workspace links this repository.",
538
549
  project_archived: "Restore the archived project before changing it.",
539
550
  rate_limited: "Too many changes in a short time. Wait and retry.",
540
551
  snapshot_required: "The live feed fell behind. Reload to continue.",
@@ -1073,6 +1084,8 @@ var en_US_default = {
1073
1084
  kind: "Kind",
1074
1085
  difficulty: "Difficulty",
1075
1086
  assignee: "Assignee",
1087
+ epic: "Epic",
1088
+ noEpic: "No epic",
1076
1089
  you: "{{name}} (you)",
1077
1090
  assignTo: "Assign to…",
1078
1091
  set: "Set {{label, lowercase}}…",
@@ -1162,6 +1175,7 @@ var en_US_default = {
1162
1175
  priority: "All priorities",
1163
1176
  kind: "All kinds",
1164
1177
  difficulty: "All difficulties",
1178
+ automation: "Any readiness",
1165
1179
  label: "All labels",
1166
1180
  assigneeId: "Anyone",
1167
1181
  customField: "Any field value"
@@ -1171,6 +1185,7 @@ var en_US_default = {
1171
1185
  priority: "Search priorities",
1172
1186
  kind: "Search kinds",
1173
1187
  difficulty: "Search difficulties",
1188
+ automation: "Search readiness",
1174
1189
  label: "Search labels",
1175
1190
  assigneeId: "Search people",
1176
1191
  customField: "Search fields"
@@ -1703,6 +1718,9 @@ var en_US_default = {
1703
1718
  changePrefixDescription: "Every ticket in {{name}} takes the new prefix and keeps its number.",
1704
1719
  changing: "Changing…",
1705
1720
  prefixChanged: "Tickets now use {{key}}",
1721
+ repositories: "Repositories",
1722
+ repositoriesDescription: "Agents can pass one of these git remotes as the project ID.",
1723
+ repositoriesInvalid: "Enter up to 20 git remotes, such as git@github.com:owner/repo.git.",
1706
1724
  publicLink: "Public link",
1707
1725
  publicDisclosure: "Anyone with the link can read every unarchived ticket's title, description, acceptance criteria, status, priority, kind, difficulty, labels, scopes, dates, and links to other tickets in this project. People, comments, activity, attachments, evidence, and claims stay private.",
1708
1726
  publicOn: "Public link on",
@@ -1823,6 +1841,8 @@ var en_US_default = {
1823
1841
  kept: "Kept",
1824
1842
  projects: "Projects",
1825
1843
  members: "Members",
1844
+ epics: "Epics",
1845
+ epicTickets: "Tickets leaving an epic",
1826
1846
  sharedLabels: "Shared labels",
1827
1847
  signIn: "Sign-in",
1828
1848
  sessions: "Sessions",
@@ -2937,6 +2957,7 @@ const scopes = [
2937
2957
  ];
2938
2958
  const attachmentLimit = 1e7;
2939
2959
  const stagedUploadLifetime = 864e5;
2960
+ const uploadUrlLifetime = 9e5;
2940
2961
  const stateLabels = labelTable(states, (value) => t(`common.status.${value}`));
2941
2962
  const priorityLabels = labelTable(priorities, (value) => t(`common.priority.${value}`));
2942
2963
  ticketSymbols.status.backlog.tone, ticketSymbols.status.todo.tone, ticketSymbols.status.in_progress.tone, ticketSymbols.status.in_review.tone, ticketSymbols.status.done.tone, ticketSymbols.status.cancelled.tone;
@@ -3053,9 +3074,30 @@ const placement = {
3053
3074
  after: id.optional()
3054
3075
  };
3055
3076
  const oneNeighbor = (input) => input.before === void 0 || input.after === void 0;
3056
- const projectInput = z.object({ projectId: id });
3077
+ const projectInput = z.object({ projectId: z.string().min(1).max(300) });
3057
3078
  const ticketInput = projectInput.extend({ ticketId: id });
3058
3079
  const projectKey = z.string().trim().regex(/^[A-Z][A-Z0-9]{1,9}$/);
3080
+ const remoteHost = "(?<host>[a-z0-9][a-z0-9.-]*)";
3081
+ const remotePath = "(?<path>[^?#]+)";
3082
+ const remoteForms = [
3083
+ new RegExp(`^(?<scheme>https|ssh)://(?:[^/]*@)?${remoteHost}(?::(?<port>\\d+))?/${remotePath}$`),
3084
+ new RegExp(`^[^/:@]+@${remoteHost}:${remotePath}$`),
3085
+ new RegExp(`^${remoteHost}(?::(?<port>\\d+))?/${remotePath}$`)
3086
+ ];
3087
+ const defaultRemotePorts = {
3088
+ https: "443",
3089
+ ssh: "22"
3090
+ };
3091
+ function normalizeRemote(value) {
3092
+ const remote = value.trim().toLowerCase();
3093
+ if (/\s/.test(remote)) return null;
3094
+ const { scheme, host, port, path } = remoteForms.map((form) => form.exec(remote)).find(Boolean)?.groups ?? {};
3095
+ const segments = path?.replace(/\/+$/, "").replace(/\.git$/, "").split("/").filter(Boolean);
3096
+ if (!host || !segments?.length) return null;
3097
+ return `${host}${port && port !== defaultRemotePorts[scheme ?? ""] ? `:${port}` : ""}/${segments.join("/")}`;
3098
+ }
3099
+ const repositoryRemote = z.string().max(300).refine((value) => normalizeRemote(value) !== null, "Enter a git remote URL").overwrite((value) => normalizeRemote(value) ?? value);
3100
+ const projectRepositories = z.array(repositoryRemote).max(20).overwrite((remotes) => [...new Set(remotes)]);
3059
3101
  const reason = z.object({
3060
3102
  code: z.string(),
3061
3103
  message: z.string(),
@@ -3072,15 +3114,19 @@ const filterFieldNames = [
3072
3114
  "priority",
3073
3115
  "kind",
3074
3116
  "difficulty",
3117
+ "automation",
3075
3118
  "label",
3076
3119
  "assigneeId",
3077
3120
  "customField"
3078
3121
  ];
3122
+ const automationFilters = ["ready", "manual"];
3123
+ labelTable(automationFilters, (value) => t(`common.automationFilter.${value}`));
3079
3124
  const filterFields = {
3080
3125
  status: z.enum(states),
3081
3126
  priority: z.enum(priorities),
3082
3127
  kind: z.enum(kinds),
3083
3128
  difficulty: z.enum(difficulties),
3129
+ automation: z.enum(automationFilters),
3084
3130
  label: z.string().max(40),
3085
3131
  assigneeId: z.union([id, z.literal("none")]),
3086
3132
  customField: z.string().regex(/^[0-9a-f-]{36}:(?:none|true|[0-9a-f-]{36})$/)
@@ -3098,6 +3144,8 @@ const filterShape = {
3098
3144
  kindNot: filterValues(filterFields.kind),
3099
3145
  difficulty: filterValues(filterFields.difficulty),
3100
3146
  difficultyNot: filterValues(filterFields.difficulty),
3147
+ automation: filterValues(filterFields.automation),
3148
+ automationNot: filterValues(filterFields.automation),
3101
3149
  label: filterValues(filterFields.label),
3102
3150
  labelNot: filterValues(filterFields.label),
3103
3151
  assigneeId: filterValues(filterFields.assigneeId),
@@ -3167,6 +3215,7 @@ const ticketFilter = z.object({
3167
3215
  ]).default("priority"),
3168
3216
  order: z.enum(["asc", "desc"]).optional()
3169
3217
  }).strict().superRefine(rejectConflicts);
3218
+ const listSort = ticketFilter.shape.sort.unwrap().optional();
3170
3219
  labelTable(ticketFilter.shape.sort.unwrap().options, (sort) => t(`common.sort.${sort}`));
3171
3220
  const defaultOrder = {
3172
3221
  priority: "asc",
@@ -3197,6 +3246,7 @@ const ticketFields = z.object({
3197
3246
  scopes: tags,
3198
3247
  assigneeId: z.string().nullable(),
3199
3248
  parentId: z.uuid().nullable(),
3249
+ epicId: z.uuid().nullable(),
3200
3250
  automationReady: z.boolean(),
3201
3251
  holdReason: z.string().trim().max(2e3).nullable(),
3202
3252
  startAt: dueDate.nullable(),
@@ -3275,6 +3325,7 @@ const projectSchema = z.object({
3275
3325
  key: z.string(),
3276
3326
  name: z.string(),
3277
3327
  description: z.string(),
3328
+ repositories: z.array(z.string()).default([]),
3278
3329
  version,
3279
3330
  archivedAt: timestamp.nullable().default(null),
3280
3331
  createdAt: timestamp,
@@ -3304,7 +3355,8 @@ const projectImpactSchema = z.object({
3304
3355
  copiedLabels: z.array(z.string()),
3305
3356
  mergedLabels: z.array(z.string()),
3306
3357
  subscriptions: count,
3307
- invitations: count
3358
+ invitations: count,
3359
+ epicTickets: count.default(0)
3308
3360
  }).nullable()
3309
3361
  });
3310
3362
  const workspaceImpactSchema = projectImpactSchema.omit({ transfer: true }).extend({
@@ -3315,7 +3367,8 @@ const workspaceImpactSchema = projectImpactSchema.omit({ transfer: true }).exten
3315
3367
  })),
3316
3368
  members: count,
3317
3369
  invitations: count,
3318
- labels: count
3370
+ labels: count,
3371
+ epics: count.default(0)
3319
3372
  });
3320
3373
  const deletedAccountName = "Deleted account";
3321
3374
  const accountImpactSchema = z.object({
@@ -3337,6 +3390,7 @@ const accountImpactSchema = z.object({
3337
3390
  });
3338
3391
  const ticketSchema = z.object({
3339
3392
  ...ticketFields.shape,
3393
+ epicId: z.uuid().nullable().default(null),
3340
3394
  startAt: dueDate.nullable().default(null),
3341
3395
  stateId: z.uuid().nullable().default(null),
3342
3396
  customFields: z.record(z.string(), customFieldValue).default({}),
@@ -3365,11 +3419,27 @@ const ticketSummarySchema = ticketSchema.pick({
3365
3419
  scopes: true,
3366
3420
  archived: true,
3367
3421
  automationReady: true,
3422
+ epicId: true,
3368
3423
  updatedAt: true,
3369
3424
  version: true
3370
3425
  });
3371
3426
  const ticketView = { view: z.enum(["summary", "full"]).default("full") };
3372
3427
  const ticketRow = z.union([ticketSchema, ticketSummarySchema]);
3428
+ const epicSchema = z.object({
3429
+ id: z.uuid(),
3430
+ workspaceId: z.uuid(),
3431
+ title: text.max(200),
3432
+ description: z.string(),
3433
+ archived: z.boolean(),
3434
+ version,
3435
+ createdBy: z.string(),
3436
+ createdAt: timestamp,
3437
+ updatedAt: timestamp
3438
+ });
3439
+ const epicRollupSchema = epicSchema.extend({
3440
+ openTickets: count,
3441
+ closedTickets: count
3442
+ });
3373
3443
  const snapshotSchema = z.object({
3374
3444
  project: projectSchema,
3375
3445
  tickets: z.array(ticketSchema),
@@ -3445,6 +3515,11 @@ const attachmentSchema = z.object({
3445
3515
  createdAt: timestamp
3446
3516
  });
3447
3517
  const stagedUploadSchema = attachmentSchema.omit({ ticketId: true }).extend({ expiresAt: timestamp });
3518
+ const uploadUrlSchema = z.object({
3519
+ uploadId: z.uuid(),
3520
+ url: z.url(),
3521
+ expiresAt: timestamp
3522
+ });
3448
3523
  const httpUrl = z.url().regex(/^https?:\/\//i, "Use an HTTP or HTTPS URL");
3449
3524
  const evidenceFields = z.object({
3450
3525
  kind: z.enum([
@@ -3539,6 +3614,25 @@ const calendarFeedSchema = z.object({
3539
3614
  createdAt: timestamp,
3540
3615
  active: z.boolean()
3541
3616
  });
3617
+ const publicBoardSchema = z.object({
3618
+ projectId: z.uuid(),
3619
+ enabled: z.boolean(),
3620
+ title: text.max(80),
3621
+ labels: tags,
3622
+ excludedLabels: tags,
3623
+ version,
3624
+ createdAt: timestamp,
3625
+ updatedAt: timestamp
3626
+ });
3627
+ const publicBoardTicketSchema = z.object({
3628
+ projectId: z.uuid(),
3629
+ key: z.string(),
3630
+ title: z.string(),
3631
+ kind: z.enum(kinds),
3632
+ status: z.enum(states),
3633
+ createdAt: timestamp,
3634
+ updatedAt: timestamp
3635
+ });
3542
3636
  const eventSchema = z.object({
3543
3637
  id: z.string().regex(/^\d+$/),
3544
3638
  projectId: z.uuid(),
@@ -3922,6 +4016,8 @@ const eventLabels = labelTable([
3922
4016
  "label.created",
3923
4017
  "label.updated",
3924
4018
  "label.deleted",
4019
+ "epic.created",
4020
+ "epic.updated",
3925
4021
  "workflow.created",
3926
4022
  "workflow.updated",
3927
4023
  "workflow.deleted",
@@ -3955,6 +4051,7 @@ const eventLabels = labelTable([
3955
4051
  "resource.removed",
3956
4052
  "calendar_feed.created",
3957
4053
  "calendar_feed.revoked",
4054
+ "public_board.updated",
3958
4055
  "project.created",
3959
4056
  "project.updated",
3960
4057
  "project.archived",
@@ -4210,21 +4307,25 @@ const page = (item) => z.object({
4210
4307
  items: z.array(item),
4211
4308
  total: z.number().int().nonnegative()
4212
4309
  });
4213
- const fileFields = {
4310
+ const fileMetadata = {
4214
4311
  name: text.max(255).refine((value) => {
4215
4312
  for (const character of value) if (character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127) return false;
4216
4313
  return true;
4217
4314
  }, "Use a filename without control characters"),
4218
- contentType: z.string().max(120).regex(/^[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*$/i).default("application/octet-stream"),
4315
+ contentType: z.string().max(120).regex(/^[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*$/i).default("application/octet-stream")
4316
+ };
4317
+ const fileFields = {
4318
+ ...fileMetadata,
4219
4319
  data: z.string().max(Math.ceil(attachmentLimit / 3) * 4)
4220
4320
  };
4221
- const op = (input, output, method, path, scope, description) => ({
4321
+ const op = (input, output, method, path, scope, description, options = {}) => ({
4222
4322
  input,
4223
4323
  output,
4224
4324
  method,
4225
4325
  path,
4226
4326
  scope,
4227
- description
4327
+ description,
4328
+ ...options
4228
4329
  });
4229
4330
  const operations = {
4230
4331
  list_workspaces: op(z.object({}), z.array(workspaceSchema), "GET", "/workspaces", "read", "List accessible workspaces and the current membership role."),
@@ -4290,6 +4391,37 @@ const operations = {
4290
4391
  userId: z.string().min(1),
4291
4392
  confirmEmail: z.string().trim().max(320)
4292
4393
  }).strict(), okSchema, "DELETE", "/accounts/{userId}", "write", "Delete an account: remove its sign-in methods, sessions, API keys, OAuth grants, devices, notification settings, memberships, and photo, release its claims, cancel its imports, and replace its name and email with a `Deleted account` tombstone. Tickets, comments, evidence, and activity keep the account ID. Only the account itself or an instance administrator, signed in within the last 15 minutes, can delete; `confirmEmail` must repeat the account's email. Fails with `workspace_owner` while it owns a workspace and `last_administrator` for the last active instance or workspace administrator."),
4394
+ list_epics: op(z.object({
4395
+ workspaceId: z.uuid(),
4396
+ archived: ticketFilter.shape.archived,
4397
+ ...pagination
4398
+ }), page(epicRollupSchema), "GET", "/workspaces/{workspaceId}/epics", "read", "List the workspace's Epics by title, each with open and closed ticket counts from the projects you can read, leaving out archived tickets. Archived Epics are left out unless `archived` is `include` or `only`."),
4399
+ get_epic: op(z.object({
4400
+ workspaceId: z.uuid(),
4401
+ epicId: z.uuid()
4402
+ }), epicRollupSchema, "GET", "/workspaces/{workspaceId}/epics/{epicId}", "read", "Read one Epic with the same ticket counts as list_epics."),
4403
+ create_epic: op(z.object({
4404
+ ...mutation,
4405
+ workspaceId: z.uuid(),
4406
+ title: epicSchema.shape.title,
4407
+ description: z.string().max(1e5).default("")
4408
+ }).strict(), epicSchema, "POST", "/workspaces/{workspaceId}/epics", "write", "Create an Epic that groups tickets from any project in the workspace. Add a ticket by setting `epicId` through create_ticket, update_ticket, or bulk_update_tickets. Membership has no effect on readiness, eligibility, or claims. Reuse requestId when retrying this write."),
4409
+ update_epic: op(z.object({
4410
+ ...mutation,
4411
+ workspaceId: z.uuid(),
4412
+ epicId: z.uuid(),
4413
+ expectedVersion: version,
4414
+ title: epicSchema.shape.title.optional(),
4415
+ description: z.string().max(1e5).optional(),
4416
+ archived: z.boolean().optional()
4417
+ }).strict().refine(({ title, description, archived }) => title !== void 0 || description !== void 0 || archived !== void 0, "Provide at least one change"), epicSchema, "PATCH", "/workspaces/{workspaceId}/epics/{epicId}", "write", "Change an Epic's title or description, archive it, or restore it with `archived: false`. An archived Epic keeps its members and refuses new ones. There is no deletion. Reuse requestId when retrying this write."),
4418
+ list_epic_tickets: op(z.object({
4419
+ workspaceId: z.uuid(),
4420
+ epicId: z.uuid(),
4421
+ archived: ticketFilter.shape.archived,
4422
+ ...pagination,
4423
+ ...ticketView
4424
+ }), page(ticketRow).extend({ restrictedCount: count }), "GET", "/workspaces/{workspaceId}/epics/{epicId}/tickets", "read", "List an Epic's tickets across projects, oldest first. Only tickets of projects you can read appear; `restrictedCount` counts the rest without naming them. With `view: \"summary\"`, rows omit the description and acceptance criteria."),
4293
4425
  list_workspace_labels: op(z.object({ workspaceId: z.uuid() }), z.array(workspaceCatalogLabel), "GET", "/workspaces/{workspaceId}/labels", "read", "List the workspace's shared labels with ticket counts from the projects you can read, including archived tickets."),
4294
4426
  create_workspace_label: op(z.object({
4295
4427
  ...mutation,
@@ -4312,7 +4444,7 @@ const operations = {
4312
4444
  workspaceId: z.uuid(),
4313
4445
  labelId: z.uuid(),
4314
4446
  expectedVersion: version
4315
- }).strict(), okSchema, "DELETE", "/workspaces/{workspaceId}/labels/{labelId}", "admin", "Delete a workspace label and remove it from tickets and saved-view filters in every project of the workspace atomically."),
4447
+ }).strict(), okSchema, "DELETE", "/workspaces/{workspaceId}/labels/{labelId}", "admin", "Delete a workspace label and remove it from tickets and saved-view filters in every project of the workspace atomically. Fails with `label_in_use` while an enabled public board in the workspace excludes it."),
4316
4448
  list_invitations: op(z.object({ workspaceId: z.uuid() }), z.array(invitationSchema), "GET", "/workspaces/{workspaceId}/invitations", "admin", "List the workspace's pending and expired invitations."),
4317
4449
  create_invitation: op(z.object({
4318
4450
  workspaceId: z.uuid(),
@@ -4414,8 +4546,9 @@ const operations = {
4414
4546
  publicReadEnabled: z.boolean().optional(),
4415
4547
  name: text.max(120),
4416
4548
  description: z.string().max(5e3),
4417
- key: projectKey.optional()
4418
- }).strict(), projectSchema, "PATCH", "/projects/{projectId}", "admin", "Update project details. A new `key` re-keys every ticket of the project; the previous prefix stays reserved for the project, and the project and every key issued under it keep resolving. `publicReadEnabled` shares or stops sharing the project's read-only public view (see the public operations)."),
4549
+ key: projectKey.optional(),
4550
+ repositories: projectRepositories.optional()
4551
+ }).strict(), projectSchema, "PATCH", "/projects/{projectId}", "admin", "Update project details. A new `key` re-keys every ticket of the project; the previous prefix stays reserved for the project, and the project and every key issued under it keep resolving. `publicReadEnabled` shares or stops sharing the project's read-only public view (see the public operations). `repositories` replaces the project's linked git remotes (at most 20), and any `projectId` then accepts one of them; within a workspace only one unarchived project can hold a remote."),
4419
4552
  move_project: op(projectInput.extend({
4420
4553
  ...mutation,
4421
4554
  expectedVersion: version,
@@ -4426,18 +4559,18 @@ const operations = {
4426
4559
  expectedVersion: version,
4427
4560
  archived: z.boolean()
4428
4561
  }).strict(), projectSchema, "POST", "/projects/{projectId}/archive", "admin", "Archive a project, or restore it with `archived: false`. An archived project keeps its tickets and history but leaves default project lists, search, My work, calendar feeds, public links, reminders, and agent queues, and its tickets and settings are read only. Fails with `claim_conflict` while any ticket is claimed."),
4429
- get_project_impact: op(projectInput.extend({ workspaceId: z.uuid().optional() }), projectImpactSchema, "GET", "/projects/{projectId}/impact", "admin", "Preview what deleting the project removes, or with `workspaceId`, what moving it to that workspace changes: who loses or gains access, labels copied or merged, follows and invitations dropped, and blockers such as claimed tickets."),
4562
+ get_project_impact: op(projectInput.extend({ workspaceId: z.uuid().optional() }), projectImpactSchema, "GET", "/projects/{projectId}/impact", "admin", "Preview what deleting the project removes, or with `workspaceId`, what moving it to that workspace changes: who loses or gains access, labels copied or merged, follows and invitations dropped, tickets leaving their Epic, and blockers such as claimed tickets."),
4430
4563
  transfer_project: op(projectInput.extend({
4431
4564
  ...mutation,
4432
4565
  expectedVersion: version,
4433
4566
  workspaceId: z.uuid()
4434
- }).strict(), projectSchema, "POST", "/projects/{projectId}/transfer", "admin", "Move a project with its tickets, keys, and links to another workspace. Requires administration of both workspaces and applies the reconciliation `get_project_impact` previews in one transaction; blockers fail with their code."),
4567
+ }).strict(), projectSchema, "POST", "/projects/{projectId}/transfer", "admin", "Move a project with its tickets, keys, and links to another workspace. Requires administration of both workspaces and applies the reconciliation `get_project_impact` previews in one transaction, including clearing each ticket's `epicId`; blockers fail with their code."),
4435
4568
  delete_project: op(projectInput.extend({
4436
4569
  ...mutation,
4437
4570
  expectedVersion: version,
4438
4571
  confirmKey: z.string().max(20)
4439
- }).strict(), okSchema, "DELETE", "/projects/{projectId}", "admin", "Permanently delete a project's tickets, comments, attachments, labels, feeds, and destinations. `confirmKey` must repeat the project prefix. Fails with `claim_conflict` while any ticket is claimed. The prefix stays reserved and the activity history and deletion events are kept."),
4440
- export_project: op(projectInput, ticketExportSchema, "GET", "/projects/{projectId}/export", "read", "Export the project's tickets, archived ones included, as a versioned `kanvil.tickets` bundle: workflow states, custom fields, labels, relationships, comments, evidence, and links, with the people they name. Records reference each other by source ID. `completeness.omitted` counts what the bundle leaves out, such as attachments and activity history."),
4572
+ }).strict(), okSchema, "DELETE", "/projects/{projectId}", "admin", "Permanently delete a project's tickets, comments, attachments, labels, feeds, public board, and destinations. `confirmKey` must repeat the project prefix. Fails with `claim_conflict` while any ticket is claimed. The prefix stays reserved and the activity history and deletion events are kept."),
4573
+ export_project: op(projectInput, ticketExportSchema, "GET", "/projects/{projectId}/export", "read", "Export the project's tickets, archived ones included, as a versioned `kanvil.tickets` bundle: workflow states, custom fields, labels, relationships, comments, evidence, and links, with the people they name. Records reference each other by source ID. `completeness.omitted` counts what the bundle leaves out, such as attachments, Epics, and activity history."),
4441
4574
  preview_import: op(z.object({
4442
4575
  ...mutation,
4443
4576
  workspaceId: z.uuid(),
@@ -4479,13 +4612,15 @@ const operations = {
4479
4612
  }).strict(), okSchema, "POST", "/projects/{projectId}/members", "admin", "Add or update an existing user's membership with a seeded role or a custom role of the project's workspace, or remove it with a null role. Needs `member.manage`; the role, and any role the person holds now, must grant nothing the caller lacks."),
4480
4613
  list_tickets: op(projectInput.extend({
4481
4614
  ...ticketFilter.shape,
4615
+ sort: listSort,
4482
4616
  ...pagination,
4483
4617
  relatedTo: id.optional(),
4484
4618
  stateId: z.uuid().optional(),
4619
+ epicId: z.uuid().optional(),
4485
4620
  scheduledFrom: timestamp.optional(),
4486
4621
  scheduledTo: timestamp.optional(),
4487
4622
  ...ticketView
4488
- }).superRefine(rejectConflicts), page(ticketRow), "GET", "/projects/{projectId}/tickets", "read", "Search tickets with server-side filters, stable ordering, and pagination. Each property filter takes one value or a repeated list (any of them); its `Not` twin excludes values. Double-quoted segments of `q` match as exact phrases. Pass `view: \"summary\"` when scanning results: rows then omit the description and acceptance criteria."),
4623
+ }).superRefine(rejectConflicts), page(ticketRow), "GET", "/projects/{projectId}/tickets", "read", "Search tickets with server-side filters, stable ordering, and pagination. Each property filter takes one value or a repeated list (any of them); its `Not` twin excludes values. `q` keeps only tickets whose key, title, description, acceptance criteria, labels, or scopes contain its text as written, or whose key or ID it names exactly; double-quoted segments are exact phrases that must each appear on word boundaries, so `\"IME\"` does not match `time` while unquoted `IME` does. With `q` and no `sort`, results order by relevance as in search_tickets; otherwise by `sort` (default `priority`). With `view: \"summary\"`, rows omit the description and acceptance criteria."),
4489
4624
  list_ticket_labels: op(projectInput.extend({ archived: ticketFilter.shape.archived }), z.array(z.object({
4490
4625
  name: z.string(),
4491
4626
  count: z.number().int().nonnegative()
@@ -4507,7 +4642,7 @@ const operations = {
4507
4642
  ...mutation,
4508
4643
  labelId: z.uuid(),
4509
4644
  expectedVersion: version
4510
- }).strict(), okSchema, "DELETE", "/projects/{projectId}/labels/{labelId}", "admin", "Delete a project label and remove it from tickets and saved-view filters atomically."),
4645
+ }).strict(), okSchema, "DELETE", "/projects/{projectId}/labels/{labelId}", "admin", "Delete a project label and remove it from tickets and saved-view filters atomically. Fails with `label_in_use` while the project's enabled public board excludes it."),
4511
4646
  list_label_colors: op(z.object({}), z.array(z.object({
4512
4647
  value: hexColor,
4513
4648
  createdAt: timestamp
@@ -4579,7 +4714,7 @@ const operations = {
4579
4714
  claim: claimSchema.nullable(),
4580
4715
  subscribed: z.boolean(),
4581
4716
  canReleaseClaim: z.boolean()
4582
- }), "GET", "/projects/{projectId}/tickets/{ticketId}", "read", "Read a ticket by ID or key, with readiness and ownership."),
4717
+ }), "GET", "/projects/{projectId}/tickets/{ticketId}", "read", "Read a ticket by ID or key, with readiness and ownership.", { mcp: "core" }),
4583
4718
  create_ticket: op(projectInput.extend({
4584
4719
  ...mutation,
4585
4720
  title: ticketFields.shape.title,
@@ -4596,19 +4731,20 @@ const operations = {
4596
4731
  scopes: tags.default([]),
4597
4732
  assigneeId: z.string().nullable().default(null),
4598
4733
  parentId: z.uuid().nullable().default(null),
4734
+ epicId: z.uuid().nullable().default(null),
4599
4735
  automationReady: z.boolean().default(false),
4600
4736
  holdReason: z.string().trim().max(2e3).nullable().default(null),
4601
4737
  archived: z.boolean().default(false),
4602
4738
  uploads: z.array(z.uuid()).max(50).default([]),
4603
4739
  customFields: customFieldValues.default({})
4604
- }).strict(), ticketSchema, "POST", "/projects/{projectId}/tickets", "write", "Create a ticket. `customFields` maps field IDs from list_custom_fields to values; omitted fields take their default, and a required field must end with a value. `uploads` lists your staged inline uploads in this project; they become its attachments in the same write, or nothing is created. Reuse requestId when retrying this write."),
4605
- duplicate_ticket: op(ticketInput.extend(mutation).strict(), ticketSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/duplicate", "write", "Copy a ticket into a new To do ticket in the same project, linked to the original by a clones relationship. Copies requirements, properties, labels, scopes, and parent; resets agent readiness and never copies claims, comments, evidence, attachments, or children. Reuse requestId when retrying this write."),
4740
+ }).strict(), ticketSchema, "POST", "/projects/{projectId}/tickets", "write", "Create a ticket. `customFields` maps field IDs from list_custom_fields to values; omitted fields take their default, and a required field must end with a value. `uploads` lists your staged inline uploads in this project; they become its attachments in the same write, or nothing is created. Reuse requestId when retrying this write.", { mcp: "core" }),
4741
+ duplicate_ticket: op(ticketInput.extend(mutation).strict(), ticketSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/duplicate", "write", "Copy a ticket into a new To do ticket in the same project, linked to the original by a clones relationship. Copies requirements, properties, labels, scopes, parent, and an Epic the caller can still add to; resets agent readiness and never copies claims, comments, evidence, attachments, or children. Reuse requestId when retrying this write."),
4606
4742
  update_ticket: op(ticketInput.extend({
4607
4743
  ...mutation,
4608
4744
  expectedVersion: version,
4609
4745
  changes: ticketChanges,
4610
4746
  claimId: z.uuid().optional()
4611
- }).strict(), ticketSchema, "PATCH", "/projects/{projectId}/tickets/{ticketId}", "write", "Update a ticket with optimistic concurrency. Worker completion requires its active claim. `changes.customFields` sets only the named fields; null clears one."),
4747
+ }).strict(), ticketSchema, "PATCH", "/projects/{projectId}/tickets/{ticketId}", "write", "Update a ticket with optimistic concurrency. Worker completion requires its active claim. `changes.customFields` sets only the named fields; null clears one.", { mcp: "core" }),
4612
4748
  update_ticket_labels: op(ticketInput.extend({
4613
4749
  ...mutation,
4614
4750
  expectedVersion: version,
@@ -4638,7 +4774,7 @@ const operations = {
4638
4774
  resolve_ticket: op(z.object({ ticketId: id }).strict(), z.object({
4639
4775
  ticket: ticketSchema,
4640
4776
  project: projectRef
4641
- }), "GET", "/tickets/resolve", "read", "Find a ticket by ID, current key, or any earlier key from a prefix change or a move, across every accessible project. An unknown or inaccessible ticket reads as not found."),
4777
+ }), "GET", "/tickets/resolve", "read", "Find a ticket by ID, current key, or any earlier key from a prefix change or a move, across every accessible project. An unknown or inaccessible ticket reads as not found.", { mcp: "core" }),
4642
4778
  bulk_update_tickets: op(projectInput.extend({
4643
4779
  ...mutation,
4644
4780
  tickets: claimedTicketSelection,
@@ -4661,7 +4797,7 @@ const operations = {
4661
4797
  ticket: ticketRow,
4662
4798
  project: projectRef,
4663
4799
  snippet: z.string().optional()
4664
- })) }), "GET", "/tickets/search", "read", "Search unarchived tickets by key or text across every accessible project, optionally within one workspace. Results order by exact key match, then relevance, then recency. Double-quoted segments of `q` match as exact phrases. Pass `view: \"summary\"` when scanning results: each ticket is then a compact row and the result carries a `snippet` of the matching text."),
4800
+ })) }), "GET", "/tickets/search", "read", "Search unarchived tickets by key or text across every accessible project, optionally within one workspace. Results order by exact key match, then relevance, then recency. Double-quoted segments of `q` match as exact phrases on word boundaries, so `\"IME\"` does not match `time`. With `view: \"summary\"`, each ticket is a compact row and the result carries a `snippet` of the matching text.", { mcp: "core" }),
4665
4801
  search_workspace: op(z.object({
4666
4802
  q: z.string().trim().min(1).max(200),
4667
4803
  workspaceId: z.uuid().optional(),
@@ -4676,19 +4812,19 @@ const operations = {
4676
4812
  counts: z.record(z.enum(searchResultTypes), z.number().int().nonnegative()),
4677
4813
  engine: z.enum(["postgres", "index"]),
4678
4814
  indexing: z.boolean()
4679
- }), "GET", "/search", "read", "Search accessible tickets, projects, comments, and activity by text or ticket key, optionally within one workspace or one result type. Property filters restrict results to matching tickets. Results order by exact key match, then type, then ticket relevance, then recency. Double-quoted segments of `q` match ticket text as exact phrases. Pass `view: \"summary\"` when scanning results: ticket results then carry their summary row and each result a `snippet` of the matching text."),
4815
+ }), "GET", "/search", "read", "Search accessible tickets, projects, comments, and activity by text or ticket key, optionally within one workspace or one result type. Property filters restrict results to matching tickets. Results order by exact key match, then type, then ticket relevance, then recency. Double-quoted segments of `q` match ticket text as exact phrases on word boundaries. With `view: \"summary\"`, ticket results carry their summary row and each result a `snippet` of the matching text."),
4680
4816
  list_my_work: op(z.object({
4681
4817
  workspaceId: z.uuid().optional(),
4682
4818
  q: ticketFilter.shape.q,
4683
- sort: ticketFilter.shape.sort,
4819
+ sort: listSort,
4684
4820
  order: ticketFilter.shape.order,
4685
4821
  activity: z.coerce.number().int().min(0).max(50).default(0),
4686
4822
  ...pagination
4687
- }).strict(), page(ticketSchema).extend({ activity: z.array(eventSchema).default([]) }), "GET", "/my-work", "read", "List open, unarchived tickets assigned to the current identity across every accessible project, optionally within one workspace, with the most recent accessible activity when `activity` is set."),
4823
+ }).strict(), page(ticketSchema).extend({ activity: z.array(eventSchema).default([]) }), "GET", "/my-work", "read", "List open, unarchived tickets assigned to the current identity across every accessible project, optionally within one workspace, with the most recent accessible activity when `activity` is set. `q` and `sort` work as in list_tickets.", { mcp: "core" }),
4688
4824
  list_eligible_tickets: op(projectInput.extend(pagination), page(z.object({
4689
4825
  ticket: ticketSchema,
4690
4826
  readiness: readinessSchema
4691
- })), "GET", "/projects/{projectId}/eligibility", "read", "Inspect eligibility and explicit blocking reasons for open tickets."),
4827
+ })), "GET", "/projects/{projectId}/eligibility", "read", "Inspect eligibility and explicit blocking reasons for open tickets.", { mcp: "core" }),
4692
4828
  list_relations: op(ticketInput, z.array(relationSchema), "GET", "/projects/{projectId}/tickets/{ticketId}/relations", "read", "List incoming and outgoing ticket relationships with each side's key, title, and project. A ticket in a project the caller cannot read appears by ID and key alone, with `restricted: true`."),
4693
4829
  add_relation: op(ticketInput.extend({
4694
4830
  ...mutation,
@@ -4704,7 +4840,7 @@ const operations = {
4704
4840
  add_comment: op(ticketInput.extend({
4705
4841
  ...mutation,
4706
4842
  body: text.max(5e4)
4707
- }).strict(), commentSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/comments", "write", "Add a comment with authenticated attribution. Mention a project member with `[@Name](mention:<user id>)` from list_members; members who can read the project are notified."),
4843
+ }).strict(), commentSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/comments", "write", "Add a comment with authenticated attribution. Mention a project member with `[@Name](mention:<user id>)` from list_members; members who can read the project are notified.", { mcp: "core" }),
4708
4844
  update_comment: op(projectInput.extend({
4709
4845
  ...mutation,
4710
4846
  commentId: z.uuid(),
@@ -4720,11 +4856,16 @@ const operations = {
4720
4856
  upload_attachment: op(ticketInput.extend({
4721
4857
  ...mutation,
4722
4858
  ...fileFields
4723
- }).strict(), attachmentSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/attachments", "write", "Upload up to 10 MB of base64-encoded bytes. The original filename is metadata, never a filesystem path. An intake credential can attach up to 10 files to a quarantined ticket it created, within 1 GB across its quarantined tickets."),
4859
+ }).strict(), attachmentSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/attachments", "write", "Upload up to 10 MB of base64-encoded bytes. The original filename is metadata, never a filesystem path. An intake credential can attach up to 10 files to a quarantined ticket it created, within 1 GB across its quarantined tickets. For a file on disk, use create_upload_url instead of inlining it."),
4724
4860
  stage_upload: op(projectInput.extend({
4725
4861
  ...mutation,
4726
4862
  ...fileFields
4727
- }).strict(), stagedUploadSchema, "POST", "/projects/{projectId}/uploads", "write", "Stage up to 10 MB of base64-encoded bytes for a ticket that does not exist yet. Only you can read it, at its attachment content address, until create_ticket claims it; unclaimed uploads expire after 24 hours."),
4863
+ }).strict(), stagedUploadSchema, "POST", "/projects/{projectId}/uploads", "write", "Stage up to 10 MB of base64-encoded bytes for a ticket that does not exist yet. Only you can read it, at its attachment content address, until create_ticket claims it; unclaimed uploads expire after 24 hours. For a file on disk, use create_upload_url instead of inlining it."),
4864
+ create_upload_url: op(projectInput.extend({
4865
+ ...fileMetadata,
4866
+ ticketId: id.optional(),
4867
+ size: z.number().int().nonnegative().max(attachmentLimit)
4868
+ }).strict(), uploadUrlSchema, "POST", "/projects/{projectId}/upload-urls", "write", "Get a single-use URL for one file of up to 10 MB, so its bytes never pass through the tool call. PUT exactly `size` bytes to `url` within 15 minutes, for example `curl --upload-file <path> <url>`; the URL needs no other credential and reads nothing. With `ticketId` the file becomes that ticket's attachment; without it, it is a staged upload that create_ticket `uploads` claims by `uploadId`."),
4728
4869
  discard_upload: op(projectInput.extend({
4729
4870
  ...mutation,
4730
4871
  uploadId: z.uuid()
@@ -4739,7 +4880,7 @@ const operations = {
4739
4880
  ...mutation,
4740
4881
  ...evidenceFields.shape,
4741
4882
  claimId: z.uuid().optional()
4742
- }).strict(), evidenceSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/evidence", "write", "Attach evidence with its commit and provenance."),
4883
+ }).strict(), evidenceSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/evidence", "write", "Attach evidence with its commit and provenance.", { mcp: "core" }),
4743
4884
  list_time_entries: op(ticketInput.extend(pagination), page(timeEntrySchema), "GET", "/projects/{projectId}/tickets/{ticketId}/time-entries", "read", "List observed time entries, newest first. Running entries have null endAt and durationMs."),
4744
4885
  get_time_entry: op(ticketInput.extend({ timeEntryId: z.uuid() }), timeEntrySchema, "GET", "/projects/{projectId}/tickets/{ticketId}/time-entries/{timeEntryId}", "read", "Read one attributed time entry within its current ticket and project."),
4745
4886
  create_time_entry: op(ticketInput.extend({
@@ -4785,18 +4926,18 @@ const operations = {
4785
4926
  claim_ticket: op(ticketInput.extend({
4786
4927
  ...mutation,
4787
4928
  workerId: text.max(200),
4788
- expectedVersion: version,
4929
+ expectedVersion: version.optional(),
4789
4930
  leaseSeconds: claimLeaseSecondsSchema.optional()
4790
- }).strict(), claimSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/claim", "claim", "Atomically claim ready work. Expired claims require explicit recovery."),
4931
+ }).strict(), claimSchema.extend({ ticket: ticketSchema }), "POST", "/projects/{projectId}/tickets/{ticketId}/claim", "claim", "Atomically claim ready work and return the claim with the claimed ticket. expectedVersion is optional; readiness always applies. Expired claims require explicit recovery.", { mcp: "core" }),
4791
4932
  renew_claim: op(ticketInput.extend({
4792
4933
  ...mutation,
4793
4934
  claimId: z.uuid(),
4794
4935
  leaseSeconds: claimLeaseSecondsSchema.optional()
4795
- }).strict(), claimSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/claim/renew", "claim", "Renew the caller's unexpired claim."),
4936
+ }).strict(), claimSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/claim/renew", "claim", "Renew the caller's unexpired claim.", { mcp: "core" }),
4796
4937
  release_claim: op(ticketInput.extend({
4797
4938
  ...mutation,
4798
4939
  claimId: z.uuid()
4799
- }).strict(), okSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/claim/release", "claim", "Release the caller's claim."),
4940
+ }).strict(), okSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/claim/release", "claim", "Release the caller's claim.", { mcp: "core" }),
4800
4941
  recover_claim: op(ticketInput.extend({
4801
4942
  ...mutation,
4802
4943
  claimId: z.uuid().optional(),
@@ -4829,6 +4970,18 @@ const operations = {
4829
4970
  ...mutation,
4830
4971
  feedId: z.uuid()
4831
4972
  }).strict(), okSchema, "DELETE", "/projects/{projectId}/calendar-feeds/{feedId}", "admin", "Revoke a calendar feed. Its link stops working immediately."),
4973
+ get_project_board: op(projectInput, z.object({
4974
+ board: publicBoardSchema.nullable(),
4975
+ tickets: z.array(publicBoardTicketSchema)
4976
+ }), "GET", "/projects/{projectId}/public-board", "admin", "Read the project's public board, or null, and the tickets it selects exactly as it publishes them, even while it is disabled."),
4977
+ update_project_board: op(projectInput.extend({
4978
+ ...mutation,
4979
+ expectedVersion: version.nullable(),
4980
+ enabled: z.boolean(),
4981
+ title: text.max(80),
4982
+ labels: tags,
4983
+ excludedLabels: tags
4984
+ }).strict(), publicBoardSchema, "PATCH", "/projects/{projectId}/public-board", "admin", "Create or replace the project's public board. It publishes the key, title, kind, status, and dates of unarchived, unheld tickets that carry any of `labels` and none of `excludedLabels`. Enabling requires at least one label."),
4832
4985
  list_views: op(projectInput, z.array(z.object({
4833
4986
  id: z.uuid(),
4834
4987
  name: z.string(),
@@ -5044,6 +5197,8 @@ const permissionOverrides = {
5044
5197
  list_calendar_feeds: "sharing.manage",
5045
5198
  create_calendar_feed: "sharing.manage",
5046
5199
  revoke_calendar_feed: "sharing.manage",
5200
+ get_project_board: "sharing.manage",
5201
+ update_project_board: "sharing.manage",
5047
5202
  update_project: null
5048
5203
  };
5049
5204
  function operationPermission(name) {
@@ -5084,4 +5239,4 @@ function parseOutput(name, value) {
5084
5239
  return operations[name].output.parse(value);
5085
5240
  }
5086
5241
  //#endregion
5087
- export { permissionLabels as $, hasAdminRole as A, workflowOrder as At, memberSchema as B, searchSettingsSchema as Bt, errorSchema as C, streamErrorSchema as Ct, filterKey as D, ticketPredicates as Dt, eventSchema as E, ticketImportSchema as Et, isHexColor as F, instanceUpdateSchema as Ft, notificationReadInput as G, notificationIdsInput as H, isOperation as I, photoInputSchema as It, operationPermission as J, notificationsSchema as K, isPresetColor as L, photoResultSchema as Lt, importProviderDefinitions as M, adminInstanceSchema as Mt, importSecretFields as N, authenticationPolicySchema as Nt, filterList as O, ticketSchema as Ot, invitationStatuses as P, brokerSettingsSchema as Pt, parsePublicOutput as Q, isPublicOperation as R, s3SettingsSchema as Rt, difficulties as S, states as St, eventLabels as T, ticketFilter as Tt, notificationPreferencesInput as U, mentionScheme as V, notificationPreferencesSchema as W, parseCustomFieldFilter as X, operations as Y, parseOutput as Z, deletedAccountName as _, seededPermissions as _t, checkpointSchema as a, publicOperations as at, destinationEventKeys as b, stagedUploadLifetime as bt, credentialInput as c, repositoryProviderDefinitions as ct, customFieldSchema as d, resourceProvider as dt, permissionScope as et, customFieldValue as f, roleCategory as ft, defaultWorkflow as g, searchResultTypes as gt, defaultOrder as h, scopes as ht, channelRouteAllows as i, projectSchema as it, importLimits as j, workspaceSchema as jt, formatEvent as k, ticketSummarySchema as kt, credentialProjectsSchema as l, repositoryProviders as lt, defaultClaimMaxLeaseSeconds as m, roles as mt, attachmentLimit as n, priorities as nt, claimLeaseSecondsSchema as o, pushDeviceInput as ot, customFieldValueSchema as p, roleLabels as pt, okSchema as q, channelKinds as r, priorityLabels as rt, closedStates as s, remoteKinds as st, ApiError as t, permissions as tt, customFieldFilterTokens as u, repositorySecretFields as ut, deliverySecretFields as v, signInMethodLabels as vt, eventCategories as w, ticketExportFormat as wt, destinationKinds as x, stateLabels as xt, deliverySettingsSchema as y, snapshotSchema as yt, isSeededRole as z, searchAuthentication as zt };
5242
+ export { parseCustomFieldFilter as $, filterList as A, ticketImportSchema as At, isPublicOperation as B, instanceUpdateSchema as Bt, destinationKinds as C, stagedUploadLifetime as Ct, eventLabels as D, streamErrorSchema as Dt, eventCategories as E, states as Et, importSecretFields as F, workflowOrder as Ft, notificationIdsInput as G, searchSettingsSchema as Gt, memberSchema as H, photoResultSchema as Ht, invitationStatuses as I, workspaceSchema as It, notificationReadInput as J, notificationPreferencesInput as K, isHexColor as L, adminInstanceSchema as Lt, hasAdminRole as M, ticketSchema as Mt, importLimits as N, ticketSummarySchema as Nt, eventSchema as O, ticketExportFormat as Ot, importProviderDefinitions as P, uploadUrlLifetime as Pt, operations as Q, isOperation as R, authenticationPolicySchema as Rt, destinationEventKeys as S, snapshotSchema as St, errorSchema as T, stateLabels as Tt, mentionScheme as U, s3SettingsSchema as Ut, isSeededRole as V, photoInputSchema as Vt, normalizeRemote as W, searchAuthentication as Wt, okSchema as X, notificationsSchema as Y, operationPermission as Z, defaultOrder as _, roles as _t, channelRouteAllows as a, priorities as at, deliverySecretFields as b, seededPermissions as bt, claimSchema as c, publicOperations as ct, credentialProjectsSchema as d, repositoryProviderDefinitions as dt, parseOutput as et, customFieldFilterTokens as f, repositoryProviders as ft, defaultClaimMaxLeaseSeconds as g, roleLabels as gt, customFieldValueSchema as h, roleCategory as ht, channelKinds as i, permissions as it, formatEvent as j, ticketPredicates as jt, filterKey as k, ticketFilter as kt, closedStates as l, pushDeviceInput as lt, customFieldValue as m, resourceProvider as mt, attachmentLimit as n, permissionLabels as nt, checkpointSchema as o, priorityLabels as ot, customFieldSchema as p, repositorySecretFields as pt, notificationPreferencesSchema as q, attachmentSchema as r, permissionScope as rt, claimLeaseSecondsSchema as s, projectSchema as st, ApiError as t, parsePublicOutput as tt, credentialInput as u, remoteKinds as ut, defaultWorkflow as v, scopes as vt, difficulties as w, stagedUploadSchema as wt, deliverySettingsSchema as x, signInMethodLabels as xt, deletedAccountName as y, searchResultTypes as yt, isPresetColor as z, brokerSettingsSchema as zt };
@@ -0,0 +1,20 @@
1
+ CREATE TABLE "upload_urls" (
2
+ "id" uuid PRIMARY KEY NOT NULL,
3
+ "project_id" uuid NOT NULL,
4
+ "ticket_id" uuid,
5
+ "actor_id" text NOT NULL,
6
+ "credential" jsonb,
7
+ "name" text NOT NULL,
8
+ "content_type" text NOT NULL,
9
+ "size" integer NOT NULL,
10
+ "token_hash" text NOT NULL,
11
+ "expires_at" timestamp with time zone NOT NULL,
12
+ "used_at" timestamp with time zone,
13
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
14
+ CONSTRAINT "upload_urls_token_hash_unique" UNIQUE("token_hash")
15
+ );
16
+ --> statement-breakpoint
17
+ ALTER TABLE "upload_urls" ADD CONSTRAINT "upload_urls_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
18
+ ALTER TABLE "upload_urls" ADD CONSTRAINT "upload_urls_ticket_id_tickets_id_fk" FOREIGN KEY ("ticket_id") REFERENCES "public"."tickets"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
19
+ ALTER TABLE "upload_urls" ADD CONSTRAINT "upload_urls_actor_id_user_id_fk" FOREIGN KEY ("actor_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
20
+ CREATE INDEX "upload_urls_expires" ON "upload_urls" USING btree ("expires_at");
@@ -0,0 +1,2 @@
1
+ ALTER TABLE "projects" ADD COLUMN "repositories" text[] DEFAULT '{}' NOT NULL;--> statement-breakpoint
2
+ ALTER TABLE "mutation_receipts" ADD COLUMN "project_id" uuid;