devchain-cli 0.22.0 → 0.23.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 (241) hide show
  1. package/dist/drizzle/0077_familiar_morbius.sql +6 -0
  2. package/dist/drizzle/0078_preserve_legacy_connection_ownership.sql +180 -0
  3. package/dist/drizzle/0079_premium_forgotten_one.sql +18 -0
  4. package/dist/drizzle/0080_lethal_thunderbolt_ross.sql +49 -0
  5. package/dist/drizzle/0081_early_kronos.sql +58 -0
  6. package/dist/drizzle/0082_huge_black_queen.sql +14 -0
  7. package/dist/drizzle/meta/0077_snapshot.json +6957 -0
  8. package/dist/drizzle/meta/0078_snapshot.json +6957 -0
  9. package/dist/drizzle/meta/0079_snapshot.json +7093 -0
  10. package/dist/drizzle/meta/0080_snapshot.json +7241 -0
  11. package/dist/drizzle/meta/0081_snapshot.json +7482 -0
  12. package/dist/drizzle/meta/0082_snapshot.json +7571 -0
  13. package/dist/drizzle/meta/_journal.json +42 -0
  14. package/dist/node_modules/@devchain/shared/tsconfig.tsbuildinfo +1 -1
  15. package/dist/server/common/errors/error-types.d.ts +7 -0
  16. package/dist/server/common/errors/error-types.js +7 -1
  17. package/dist/server/common/errors/error-types.js.map +1 -1
  18. package/dist/server/modules/epic-time/controllers/agent-time-buffer.controller.d.ts +8 -0
  19. package/dist/server/modules/epic-time/controllers/agent-time-buffer.controller.js +67 -0
  20. package/dist/server/modules/epic-time/controllers/agent-time-buffer.controller.js.map +1 -0
  21. package/dist/server/modules/epic-time/controllers/external-estimate-log.controller.d.ts +12 -0
  22. package/dist/server/modules/epic-time/controllers/external-estimate-log.controller.js +322 -0
  23. package/dist/server/modules/epic-time/controllers/external-estimate-log.controller.js.map +1 -0
  24. package/dist/server/modules/epic-time/epic-time.module.js +14 -4
  25. package/dist/server/modules/epic-time/epic-time.module.js.map +1 -1
  26. package/dist/server/modules/epic-time/models/epic-time-daily-allocator.d.ts +30 -0
  27. package/dist/server/modules/epic-time/models/epic-time-daily-allocator.js +125 -0
  28. package/dist/server/modules/epic-time/models/epic-time-daily-allocator.js.map +1 -0
  29. package/dist/server/modules/epic-time/models/epic-time-local-day.d.ts +9 -0
  30. package/dist/server/modules/epic-time/models/epic-time-local-day.js +164 -0
  31. package/dist/server/modules/epic-time/models/epic-time-local-day.js.map +1 -0
  32. package/dist/server/modules/epic-time/models/epic-time.models.d.ts +138 -0
  33. package/dist/server/modules/epic-time/models/epic-time.models.js +2 -0
  34. package/dist/server/modules/epic-time/models/epic-time.models.js.map +1 -1
  35. package/dist/server/modules/epic-time/services/agent-time-accounting.service.d.ts +1 -0
  36. package/dist/server/modules/epic-time/services/agent-time-accounting.service.js +31 -8
  37. package/dist/server/modules/epic-time/services/agent-time-accounting.service.js.map +1 -1
  38. package/dist/server/modules/epic-time/services/epic-estimate-logging.service.d.ts +37 -0
  39. package/dist/server/modules/epic-time/services/epic-estimate-logging.service.js +716 -0
  40. package/dist/server/modules/epic-time/services/epic-estimate-logging.service.js.map +1 -0
  41. package/dist/server/modules/epic-time/services/epic-time.service.d.ts +12 -3
  42. package/dist/server/modules/epic-time/services/epic-time.service.js +155 -69
  43. package/dist/server/modules/epic-time/services/epic-time.service.js.map +1 -1
  44. package/dist/server/modules/epic-time/services/epic-time.store.d.ts +29 -3
  45. package/dist/server/modules/epic-time/services/epic-time.store.js +524 -40
  46. package/dist/server/modules/epic-time/services/epic-time.store.js.map +1 -1
  47. package/dist/server/modules/epics/controllers/epics.controller.d.ts +10 -1
  48. package/dist/server/modules/epics/controllers/epics.controller.js +108 -2
  49. package/dist/server/modules/epics/controllers/epics.controller.js.map +1 -1
  50. package/dist/server/modules/epics/epics.module.js +8 -2
  51. package/dist/server/modules/epics/epics.module.js.map +1 -1
  52. package/dist/server/modules/epics/models/epic-relations.models.d.ts +34 -0
  53. package/dist/server/modules/epics/models/epic-relations.models.js +16 -0
  54. package/dist/server/modules/epics/models/epic-relations.models.js.map +1 -0
  55. package/dist/server/modules/epics/services/epic-relation-target-resolver.d.ts +8 -0
  56. package/dist/server/modules/epics/services/epic-relation-target-resolver.js +37 -0
  57. package/dist/server/modules/epics/services/epic-relation-target-resolver.js.map +1 -0
  58. package/dist/server/modules/epics/services/epic-relations.service.d.ts +52 -0
  59. package/dist/server/modules/epics/services/epic-relations.service.js +135 -0
  60. package/dist/server/modules/epics/services/epic-relations.service.js.map +1 -0
  61. package/dist/server/modules/epics/services/epics.service.d.ts +9 -2
  62. package/dist/server/modules/epics/services/epics.service.js +115 -11
  63. package/dist/server/modules/epics/services/epics.service.js.map +1 -1
  64. package/dist/server/modules/events/catalog/broadcast-registry.js +16 -0
  65. package/dist/server/modules/events/catalog/broadcast-registry.js.map +1 -1
  66. package/dist/server/modules/events/catalog/epic.relations.invalidated.d.ts +12 -0
  67. package/dist/server/modules/events/catalog/epic.relations.invalidated.js +13 -0
  68. package/dist/server/modules/events/catalog/epic.relations.invalidated.js.map +1 -0
  69. package/dist/server/modules/events/catalog/epic.time.scope.invalidated.d.ts +12 -0
  70. package/dist/server/modules/events/catalog/epic.time.scope.invalidated.js +13 -0
  71. package/dist/server/modules/events/catalog/epic.time.scope.invalidated.js.map +1 -0
  72. package/dist/server/modules/events/catalog/index.d.ts +27 -4
  73. package/dist/server/modules/events/catalog/index.js +6 -0
  74. package/dist/server/modules/events/catalog/index.js.map +1 -1
  75. package/dist/server/modules/events/catalog/integration.connection.created.d.ts +4 -1
  76. package/dist/server/modules/events/catalog/integration.connection.created.js +5 -2
  77. package/dist/server/modules/events/catalog/integration.connection.created.js.map +1 -1
  78. package/dist/server/modules/events/catalog/integration.connection.deleted.d.ts +4 -1
  79. package/dist/server/modules/events/catalog/integration.connection.deleted.js +5 -2
  80. package/dist/server/modules/events/catalog/integration.connection.deleted.js.map +1 -1
  81. package/dist/server/modules/events/catalog/integration.connection.legacy.d.ts +85 -0
  82. package/dist/server/modules/events/catalog/integration.connection.legacy.js +41 -0
  83. package/dist/server/modules/events/catalog/integration.connection.legacy.js.map +1 -0
  84. package/dist/server/modules/events/catalog/integration.connection.updated.d.ts +4 -1
  85. package/dist/server/modules/events/catalog/integration.connection.updated.js +5 -2
  86. package/dist/server/modules/events/catalog/integration.connection.updated.js.map +1 -1
  87. package/dist/server/modules/events/services/committed-event-replay.d.ts +10 -0
  88. package/dist/server/modules/events/services/committed-event-replay.js +30 -0
  89. package/dist/server/modules/events/services/committed-event-replay.js.map +1 -0
  90. package/dist/server/modules/events/services/committed-event.store.d.ts +1 -0
  91. package/dist/server/modules/events/services/committed-event.store.js +12 -7
  92. package/dist/server/modules/events/services/committed-event.store.js.map +1 -1
  93. package/dist/server/modules/events/services/durable-event-registry.service.d.ts +9 -2
  94. package/dist/server/modules/events/services/durable-event-registry.service.js.map +1 -1
  95. package/dist/server/modules/external-integrations/adapters/clickup-external-task.provider.d.ts +7 -0
  96. package/dist/server/modules/external-integrations/adapters/clickup-external-task.provider.js +158 -50
  97. package/dist/server/modules/external-integrations/adapters/clickup-external-task.provider.js.map +1 -1
  98. package/dist/server/modules/external-integrations/adapters/jira-external-task.provider.d.ts +4 -0
  99. package/dist/server/modules/external-integrations/adapters/jira-external-task.provider.js +73 -33
  100. package/dist/server/modules/external-integrations/adapters/jira-external-task.provider.js.map +1 -1
  101. package/dist/server/modules/external-integrations/connections/integration-connections.controller.d.ts +6 -3
  102. package/dist/server/modules/external-integrations/connections/integration-connections.controller.js +67 -12
  103. package/dist/server/modules/external-integrations/connections/integration-connections.controller.js.map +1 -1
  104. package/dist/server/modules/external-integrations/connections/integration-connections.service.d.ts +35 -3
  105. package/dist/server/modules/external-integrations/connections/integration-connections.service.js +178 -17
  106. package/dist/server/modules/external-integrations/connections/integration-connections.service.js.map +1 -1
  107. package/dist/server/modules/external-integrations/connections/managed-subtask-sync.controller.d.ts +6 -3
  108. package/dist/server/modules/external-integrations/connections/managed-subtask-sync.controller.js +51 -11
  109. package/dist/server/modules/external-integrations/connections/managed-subtask-sync.controller.js.map +1 -1
  110. package/dist/server/modules/external-integrations/external-integrations.module.js +6 -1
  111. package/dist/server/modules/external-integrations/external-integrations.module.js.map +1 -1
  112. package/dist/server/modules/external-integrations/models/external-edit-session.models.d.ts +1 -0
  113. package/dist/server/modules/external-integrations/models/external-provider.models.d.ts +6 -2
  114. package/dist/server/modules/external-integrations/models/external-provider.models.js +2 -3
  115. package/dist/server/modules/external-integrations/models/external-provider.models.js.map +1 -1
  116. package/dist/server/modules/external-integrations/models/external-time-mutation.models.d.ts +33 -2
  117. package/dist/server/modules/external-integrations/models/external-time-mutation.models.js +14 -0
  118. package/dist/server/modules/external-integrations/models/external-time-mutation.models.js.map +1 -1
  119. package/dist/server/modules/external-integrations/my-work/external-edit-session.service.d.ts +10 -9
  120. package/dist/server/modules/external-integrations/my-work/external-edit-session.service.js +81 -46
  121. package/dist/server/modules/external-integrations/my-work/external-edit-session.service.js.map +1 -1
  122. package/dist/server/modules/external-integrations/my-work/external-my-work.controller.d.ts +22 -20
  123. package/dist/server/modules/external-integrations/my-work/external-my-work.controller.js +136 -64
  124. package/dist/server/modules/external-integrations/my-work/external-my-work.controller.js.map +1 -1
  125. package/dist/server/modules/external-integrations/my-work/external-my-work.openapi.d.ts +7 -0
  126. package/dist/server/modules/external-integrations/my-work/external-my-work.openapi.js +289 -4
  127. package/dist/server/modules/external-integrations/my-work/external-my-work.openapi.js.map +1 -1
  128. package/dist/server/modules/external-integrations/my-work/external-my-work.service.d.ts +12 -7
  129. package/dist/server/modules/external-integrations/my-work/external-my-work.service.js +114 -54
  130. package/dist/server/modules/external-integrations/my-work/external-my-work.service.js.map +1 -1
  131. package/dist/server/modules/external-integrations/my-work/external-time-mutation.service.d.ts +19 -6
  132. package/dist/server/modules/external-integrations/my-work/external-time-mutation.service.js +346 -46
  133. package/dist/server/modules/external-integrations/my-work/external-time-mutation.service.js.map +1 -1
  134. package/dist/server/modules/external-integrations/sessions/external-edit-session.store.d.ts +2 -1
  135. package/dist/server/modules/external-integrations/sessions/external-edit-session.store.js +3 -2
  136. package/dist/server/modules/external-integrations/sessions/external-edit-session.store.js.map +1 -1
  137. package/dist/server/modules/external-integrations/sessions/external-time-mutation.store.d.ts +6 -1
  138. package/dist/server/modules/external-integrations/sessions/external-time-mutation.store.js +19 -0
  139. package/dist/server/modules/external-integrations/sessions/external-time-mutation.store.js.map +1 -1
  140. package/dist/server/modules/external-integrations/sessions/provider-operation-gate.d.ts +15 -2
  141. package/dist/server/modules/external-integrations/sessions/provider-operation-gate.js +33 -6
  142. package/dist/server/modules/external-integrations/sessions/provider-operation-gate.js.map +1 -1
  143. package/dist/server/modules/external-integrations/subscribers/external-subtask-sync.subscriber.d.ts +13 -6
  144. package/dist/server/modules/external-integrations/subscribers/external-subtask-sync.subscriber.js +212 -75
  145. package/dist/server/modules/external-integrations/subscribers/external-subtask-sync.subscriber.js.map +1 -1
  146. package/dist/server/modules/external-integrations/subscribers/managed-subtask-content.d.ts +13 -4
  147. package/dist/server/modules/external-integrations/subscribers/managed-subtask-content.js +98 -0
  148. package/dist/server/modules/external-integrations/subscribers/managed-subtask-content.js.map +1 -1
  149. package/dist/server/modules/external-integrations/subscribers/managed-subtask-sync-health.service.d.ts +10 -4
  150. package/dist/server/modules/external-integrations/subscribers/managed-subtask-sync-health.service.js +57 -18
  151. package/dist/server/modules/external-integrations/subscribers/managed-subtask-sync-health.service.js.map +1 -1
  152. package/dist/server/modules/mcp/dtos/mcp.dto.d.ts +139 -0
  153. package/dist/server/modules/mcp/dtos/mcp.dto.js +41 -1
  154. package/dist/server/modules/mcp/dtos/mcp.dto.js.map +1 -1
  155. package/dist/server/modules/mcp/services/handlers/epic-context.d.ts +2 -0
  156. package/dist/server/modules/mcp/services/handlers/epic-tools.d.ts +4 -0
  157. package/dist/server/modules/mcp/services/handlers/epic-tools.js +275 -0
  158. package/dist/server/modules/mcp/services/handlers/epic-tools.js.map +1 -1
  159. package/dist/server/modules/mcp/services/mcp-tool-binding.registry.d.ts +2 -1
  160. package/dist/server/modules/mcp/services/mcp-tool-binding.registry.js +13 -8
  161. package/dist/server/modules/mcp/services/mcp-tool-binding.registry.js.map +1 -1
  162. package/dist/server/modules/mcp/services/testing/mcp-tool-binding-registry.fixture.d.ts +1 -1
  163. package/dist/server/modules/mcp/services/testing/mcp-tool-binding-registry.fixture.js +1 -1
  164. package/dist/server/modules/mcp/services/testing/mcp-tool-binding-registry.fixture.js.map +1 -1
  165. package/dist/server/modules/mcp/tool-descriptors/binding-types.d.ts +2 -0
  166. package/dist/server/modules/mcp/tool-descriptors/binding-types.js.map +1 -1
  167. package/dist/server/modules/mcp/tool-descriptors/epic.bindings.js +6 -0
  168. package/dist/server/modules/mcp/tool-descriptors/epic.bindings.js.map +1 -1
  169. package/dist/server/modules/mcp/tool-descriptors/epic.metadata.js +132 -0
  170. package/dist/server/modules/mcp/tool-descriptors/epic.metadata.js.map +1 -1
  171. package/dist/server/modules/projects/controllers/projects.controller.js +1 -4
  172. package/dist/server/modules/projects/controllers/projects.controller.js.map +1 -1
  173. package/dist/server/modules/projects/projects.module.js +2 -0
  174. package/dist/server/modules/projects/projects.module.js.map +1 -1
  175. package/dist/server/modules/projects/services/projects.service.d.ts +4 -1
  176. package/dist/server/modules/projects/services/projects.service.js +23 -2
  177. package/dist/server/modules/projects/services/projects.service.js.map +1 -1
  178. package/dist/server/modules/providers/adapters/claude.adapter.d.ts +4 -2
  179. package/dist/server/modules/providers/adapters/claude.adapter.js +31 -0
  180. package/dist/server/modules/providers/adapters/claude.adapter.js.map +1 -1
  181. package/dist/server/modules/providers/services/provider-mcp-ensure.service.d.ts +7 -0
  182. package/dist/server/modules/providers/services/provider-mcp-ensure.service.js +69 -4
  183. package/dist/server/modules/providers/services/provider-mcp-ensure.service.js.map +1 -1
  184. package/dist/server/modules/session-reader/data/pricing.json +452 -56
  185. package/dist/server/modules/session-reader/services/session-reader.service.d.ts +2 -0
  186. package/dist/server/modules/session-reader/services/session-reader.service.js +85 -9
  187. package/dist/server/modules/session-reader/services/session-reader.service.js.map +1 -1
  188. package/dist/server/modules/session-reader/services/transcript-path-validator.service.js +8 -2
  189. package/dist/server/modules/session-reader/services/transcript-path-validator.service.js.map +1 -1
  190. package/dist/server/modules/session-reader/services/transcript-watcher.service.js +44 -17
  191. package/dist/server/modules/session-reader/services/transcript-watcher.service.js.map +1 -1
  192. package/dist/server/modules/sessions/services/session-runtime/__test-utils__/pipeline-harness.d.ts +1 -0
  193. package/dist/server/modules/sessions/services/session-runtime/__test-utils__/pipeline-harness.js +1 -0
  194. package/dist/server/modules/sessions/services/session-runtime/__test-utils__/pipeline-harness.js.map +1 -1
  195. package/dist/server/modules/sessions/services/session-runtime/session-launch-pipeline.service.js +11 -1
  196. package/dist/server/modules/sessions/services/session-runtime/session-launch-pipeline.service.js.map +1 -1
  197. package/dist/server/modules/sessions/utils/claude-config.d.ts +4 -1
  198. package/dist/server/modules/sessions/utils/claude-config.js +265 -60
  199. package/dist/server/modules/sessions/utils/claude-config.js.map +1 -1
  200. package/dist/server/modules/storage/db/schema.d.ts +1109 -98
  201. package/dist/server/modules/storage/db/schema.js +141 -3
  202. package/dist/server/modules/storage/db/schema.js.map +1 -1
  203. package/dist/server/modules/storage/db/sqlite-json.d.ts +1 -0
  204. package/dist/server/modules/storage/db/sqlite-json.js +4 -0
  205. package/dist/server/modules/storage/db/sqlite-json.js.map +1 -1
  206. package/dist/server/modules/storage/interfaces/storage.interface.d.ts +58 -10
  207. package/dist/server/modules/storage/interfaces/storage.interface.js.map +1 -1
  208. package/dist/server/modules/storage/local/delegates/epic.delegate.d.ts +30 -5
  209. package/dist/server/modules/storage/local/delegates/epic.delegate.js +718 -14
  210. package/dist/server/modules/storage/local/delegates/epic.delegate.js.map +1 -1
  211. package/dist/server/modules/storage/local/delegates/external-estimate-log.delegate.d.ts +52 -0
  212. package/dist/server/modules/storage/local/delegates/external-estimate-log.delegate.js +659 -0
  213. package/dist/server/modules/storage/local/delegates/external-estimate-log.delegate.js.map +1 -0
  214. package/dist/server/modules/storage/local/delegates/external-managed-subtask.delegate.d.ts +2 -1
  215. package/dist/server/modules/storage/local/delegates/external-managed-subtask.delegate.js +17 -4
  216. package/dist/server/modules/storage/local/delegates/external-managed-subtask.delegate.js.map +1 -1
  217. package/dist/server/modules/storage/local/delegates/integration.delegate.d.ts +23 -8
  218. package/dist/server/modules/storage/local/delegates/integration.delegate.js +238 -52
  219. package/dist/server/modules/storage/local/delegates/integration.delegate.js.map +1 -1
  220. package/dist/server/modules/storage/local/delegates/project.delegate.d.ts +2 -0
  221. package/dist/server/modules/storage/local/delegates/project.delegate.js +180 -130
  222. package/dist/server/modules/storage/local/delegates/project.delegate.js.map +1 -1
  223. package/dist/server/modules/storage/local/local-storage.service.d.ts +45 -10
  224. package/dist/server/modules/storage/local/local-storage.service.js +99 -15
  225. package/dist/server/modules/storage/local/local-storage.service.js.map +1 -1
  226. package/dist/server/modules/storage/models/domain.models.d.ts +170 -0
  227. package/dist/server/modules/storage/models/domain.models.js +9 -0
  228. package/dist/server/modules/storage/models/domain.models.js.map +1 -1
  229. package/dist/server/templates/teams-dev.json +311 -301
  230. package/dist/server/tsconfig.tsbuildinfo +1 -1
  231. package/dist/server/ui/assets/{ExternalRichEditor-DMtnZIMZ.js → ExternalRichEditor-BG9SopK8.js} +1 -1
  232. package/dist/server/ui/assets/{ReviewDetailPage-BmyiM5IR.js → ReviewDetailPage-DNo7Qx3m.js} +1 -1
  233. package/dist/server/ui/assets/{ReviewsPage-CQ87BMwT.js → ReviewsPage-Cay-IObR.js} +1 -1
  234. package/dist/server/ui/assets/index-C1Fdqua-.js +1197 -0
  235. package/dist/server/ui/assets/index-CV8ydm7w.css +32 -0
  236. package/dist/server/ui/assets/{useReviewSubscription-DjbEUzt4.js → useReviewSubscription-sAAfh4k9.js} +1 -1
  237. package/dist/server/ui/index.html +2 -2
  238. package/dist/templates/teams-dev.json +311 -301
  239. package/package.json +19 -2
  240. package/dist/server/ui/assets/index-BX6Yze-A.js +0 -1177
  241. package/dist/server/ui/assets/index-qoQEv_u4.css +0 -32
@@ -36,12 +36,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.EpicStorageDelegate = void 0;
37
37
  const drizzle_orm_1 = require("drizzle-orm");
38
38
  const node_crypto_1 = require("node:crypto");
39
+ const domain_models_1 = require("../../models/domain.models");
39
40
  const error_types_1 = require("../../../../common/errors/error-types");
40
41
  const logger_1 = require("../../../../common/logging/logger");
41
42
  const schema_1 = require("../../db/schema");
42
43
  const storage_helpers_1 = require("../helpers/storage-helpers");
43
44
  const base_storage_delegate_1 = require("./base-storage.delegate");
44
45
  const logger = (0, logger_1.createLogger)('EpicStorageDelegate');
46
+ const MAX_RELATION_BATCH_SIZE = 1000;
47
+ const MAX_RELATION_LIST_LIMIT = 100;
48
+ const MAX_RELATION_PREFIX_RESULTS = 100;
45
49
  class EpicStorageDelegate extends base_storage_delegate_1.BaseStorageDelegate {
46
50
  dependencies;
47
51
  constructor(context, dependencies) {
@@ -51,11 +55,14 @@ class EpicStorageDelegate extends base_storage_delegate_1.BaseStorageDelegate {
51
55
  async createEpic(data, eventFactory) {
52
56
  return this.txRunner.runImmediateQueued(() => this.insertEpic(data, eventFactory));
53
57
  }
54
- async createEpicInCurrentTransaction(data, eventFactory) {
58
+ async createEpicInCurrentTransaction(data, eventFactory, beforeEventAppend) {
55
59
  if (!this.rawClient.inTransaction) {
56
60
  throw new error_types_1.StorageError('Epic transaction insert requires an active storage transaction.');
57
61
  }
58
- return this.insertEpic(data, eventFactory);
62
+ const epic = this.insertEpic(data);
63
+ await beforeEventAppend?.(epic);
64
+ this.appendEpicEvent(epic, eventFactory);
65
+ return epic;
59
66
  }
60
67
  insertEpic(data, eventFactory) {
61
68
  const now = new Date().toISOString();
@@ -121,11 +128,14 @@ class EpicStorageDelegate extends base_storage_delegate_1.BaseStorageDelegate {
121
128
  .run();
122
129
  }
123
130
  }
131
+ this.appendEpicEvent(epic, eventFactory);
132
+ return epic;
133
+ }
134
+ appendEpicEvent(epic, eventFactory) {
124
135
  const event = eventFactory?.(epic);
125
136
  if (event) {
126
137
  this.dependencies.appendEvent(event);
127
138
  }
128
- return epic;
129
139
  }
130
140
  async getEpic(id) {
131
141
  return this.getEpicSync(id);
@@ -201,8 +211,9 @@ class EpicStorageDelegate extends base_storage_delegate_1.BaseStorageDelegate {
201
211
  };
202
212
  }
203
213
  async listProjectEpics(projectId, options = {}) {
204
- const { epics, statuses } = await Promise.resolve().then(() => __importStar(require('../../db/schema')));
214
+ const { epics, statuses, externalTaskLinks } = await Promise.resolve().then(() => __importStar(require('../../db/schema')));
205
215
  const { eq, and, sql, desc } = await Promise.resolve().then(() => __importStar(require('drizzle-orm')));
216
+ const { safeJsonText } = await Promise.resolve().then(() => __importStar(require('../../db/sqlite-json')));
206
217
  const limit = options.limit ?? 100;
207
218
  const offset = options.offset ?? 0;
208
219
  const conditions = [eq(epics.projectId, projectId)];
@@ -210,13 +221,19 @@ class EpicStorageDelegate extends base_storage_delegate_1.BaseStorageDelegate {
210
221
  const search = options.q.trim().toLowerCase();
211
222
  if (search.length) {
212
223
  const pattern = `%${search}%`;
224
+ const externalIdentifierMatch = sql `EXISTS (
225
+ SELECT 1 FROM ${externalTaskLinks}
226
+ WHERE ${externalTaskLinks.epicId} = ${epics.id}
227
+ AND (lower(${externalTaskLinks.remoteTaskId}) LIKE ${pattern}
228
+ OR lower(${safeJsonText(externalTaskLinks.sourceSnapshot, '$.remoteKey')}) LIKE ${pattern})
229
+ )`;
213
230
  const isUuidPrefix = search.length >= 8 && /^[a-f0-9-]+$/.test(search);
214
231
  if (isUuidPrefix) {
215
232
  const idPrefixPattern = `${search}%`;
216
- conditions.push(sql `(lower(${epics.title}) LIKE ${pattern} OR lower(ifnull(${epics.description}, '')) LIKE ${pattern} OR ${epics.id} LIKE ${idPrefixPattern})`);
233
+ conditions.push(sql `(lower(${epics.title}) LIKE ${pattern} OR lower(ifnull(${epics.description}, '')) LIKE ${pattern} OR ${epics.id} LIKE ${idPrefixPattern} OR ${externalIdentifierMatch})`);
217
234
  }
218
235
  else {
219
- conditions.push(sql `(lower(${epics.title}) LIKE ${pattern} OR lower(ifnull(${epics.description}, '')) LIKE ${pattern})`);
236
+ conditions.push(sql `(lower(${epics.title}) LIKE ${pattern} OR lower(ifnull(${epics.description}, '')) LIKE ${pattern} OR ${externalIdentifierMatch})`);
220
237
  }
221
238
  }
222
239
  }
@@ -372,6 +389,12 @@ class EpicStorageDelegate extends base_storage_delegate_1.BaseStorageDelegate {
372
389
  versionOf: (current) => current.version,
373
390
  prepare: ({ current }) => {
374
391
  if (data.parentId !== undefined) {
392
+ if (data.parentId !== current.parentId) {
393
+ this.ensureEpicIsNotRouteEndpointSync(id);
394
+ }
395
+ if (data.parentId === null && current.parentId !== null) {
396
+ this.assertPromotionKeepsEligibleRoutesSync(id, current.projectId);
397
+ }
375
398
  this.ensureValidEpicParentSync(current.projectId, data.parentId ?? null, id);
376
399
  }
377
400
  if (data.agentId !== undefined) {
@@ -436,8 +459,14 @@ class EpicStorageDelegate extends base_storage_delegate_1.BaseStorageDelegate {
436
459
  if (rows.length === 0) {
437
460
  throw new error_types_1.NotFoundError('Epic', id);
438
461
  }
462
+ const { workspace_id: workspaceId } = this.rawClient
463
+ .prepare(`SELECT project.workspace_id
464
+ FROM epics epic
465
+ INNER JOIN projects project ON project.id = epic.project_id
466
+ WHERE epic.id = ?`)
467
+ .get(id);
439
468
  for (const row of rows) {
440
- const event = eventFactory?.(this.getEpicSync(row.id));
469
+ const event = eventFactory?.(this.getEpicSync(row.id), workspaceId);
441
470
  if (event) {
442
471
  this.dependencies.appendEvent(event);
443
472
  }
@@ -651,27 +680,35 @@ class EpicStorageDelegate extends base_storage_delegate_1.BaseStorageDelegate {
651
680
  offset,
652
681
  };
653
682
  }
654
- async createEpicComment(data) {
655
- await this.getEpic(data.epicId);
656
- const { randomUUID } = await Promise.resolve().then(() => __importStar(require('crypto')));
657
- const { epicComments } = await Promise.resolve().then(() => __importStar(require('../../db/schema')));
683
+ async createEpicComment(data, eventFactory) {
684
+ return this.txRunner.runImmediateQueued(() => this.insertEpicComment(data, eventFactory));
685
+ }
686
+ insertEpicComment(data, eventFactory) {
687
+ const epic = this.getEpicSync(data.epicId);
658
688
  const now = new Date().toISOString();
659
689
  const comment = {
660
- id: randomUUID(),
690
+ id: (0, node_crypto_1.randomUUID)(),
661
691
  epicId: data.epicId,
662
692
  authorName: data.authorName,
663
693
  content: data.content,
664
694
  createdAt: now,
665
695
  updatedAt: now,
666
696
  };
667
- await this.db.insert(epicComments).values({
697
+ this.db
698
+ .insert(schema_1.epicComments)
699
+ .values({
668
700
  id: comment.id,
669
701
  epicId: comment.epicId,
670
702
  authorName: comment.authorName,
671
703
  content: comment.content,
672
704
  createdAt: comment.createdAt,
673
705
  updatedAt: comment.updatedAt,
674
- });
706
+ })
707
+ .run();
708
+ const event = eventFactory?.(comment, epic);
709
+ if (event) {
710
+ this.dependencies.appendEvent(event);
711
+ }
675
712
  return comment;
676
713
  }
677
714
  async deleteEpicComment(id) {
@@ -695,6 +732,661 @@ class EpicStorageDelegate extends base_storage_delegate_1.BaseStorageDelegate {
695
732
  .from(epics)
696
733
  .where(and(eq(epics.projectId, projectId), sql `substr(${epics.id}, 1, ${prefix.length}) = ${prefix}`));
697
734
  }
735
+ async setEpicRelation(data, context) {
736
+ return this.txRunner.runImmediateQueuedOrJoin(() => {
737
+ const [focal, related] = this.validateRelationPairSync(data.epicId, data.relatedEpicId);
738
+ this.authorizeRelationWriteSync(focal, related, context);
739
+ const { leftEpicId, rightEpicId } = this.canonicalRelationPair(focal.id, related.id);
740
+ const { type, direction } = this.toStoredRelation(focal.id, leftEpicId, data.type);
741
+ const createdBy = data.createdBy ?? null;
742
+ const createdByAgentId = data.createdByAgentId ?? null;
743
+ if (createdBy !== null && createdBy !== 'user' && createdBy !== 'agent') {
744
+ throw new error_types_1.ValidationError('Relation creator type must be user or agent.');
745
+ }
746
+ if (createdByAgentId !== null) {
747
+ if (createdBy !== 'agent') {
748
+ throw new error_types_1.ValidationError('An agent relation creator requires createdBy="agent".');
749
+ }
750
+ const creator = this.dependencies.getAgent(createdByAgentId);
751
+ if (creator.projectId !== focal.project_id) {
752
+ throw new error_types_1.ValidationError('Relation creator must belong to the focal Epic project.', {
753
+ createdByAgentId,
754
+ projectId: focal.project_id,
755
+ });
756
+ }
757
+ }
758
+ const existing = this.rawClient
759
+ .prepare(`SELECT id, left_epic_id, right_epic_id, type, direction,
760
+ created_by, created_by_agent_id, created_at, updated_at
761
+ FROM epic_relations
762
+ WHERE left_epic_id = ? AND right_epic_id = ?`)
763
+ .get(leftEpicId, rightEpicId);
764
+ if (existing?.type === type && existing.direction === direction) {
765
+ return {
766
+ ...this.mapEpicRelation(existing),
767
+ changed: false,
768
+ workspaceId: focal.workspace_id,
769
+ };
770
+ }
771
+ const pairEligible = focal.parent_id === null &&
772
+ related.parent_id === null &&
773
+ focal.project_id === related.project_id;
774
+ const directedEffect = type === 'related' ? (0, domain_models_1.resolveRelationEndpoints)(leftEpicId, rightEpicId, direction) : null;
775
+ const newEligibleRoute = pairEligible ? directedEffect : null;
776
+ const oldEffect = existing && existing.type === 'related' && existing.direction !== 'none' && pairEligible
777
+ ? (0, domain_models_1.resolveRelationEndpoints)(existing.left_epic_id, existing.right_epic_id, existing.direction)
778
+ : null;
779
+ const effectUnchanged = newEligibleRoute !== null &&
780
+ oldEffect !== null &&
781
+ newEligibleRoute.sourceEpicId === oldEffect.sourceEpicId &&
782
+ newEligibleRoute.targetEpicId === oldEffect.targetEpicId;
783
+ const displacedEffects = [];
784
+ if (oldEffect !== null && !effectUnchanged) {
785
+ displacedEffects.push(oldEffect);
786
+ }
787
+ if (newEligibleRoute) {
788
+ const otherRoute = this.findOutgoingEligibleRouteSync(newEligibleRoute.sourceEpicId, existing?.id);
789
+ if (otherRoute) {
790
+ displacedEffects.push(otherRoute);
791
+ }
792
+ }
793
+ if (displacedEffects.length > 1) {
794
+ throw new error_types_1.ValidationError('This change would displace two active time routes. Delete one Related pair explicitly first, then retry the change.', { displacedEffects });
795
+ }
796
+ const displacedEffect = displacedEffects[0] ?? null;
797
+ if (displacedEffect) {
798
+ const accepted = data.acceptedRouteEffect;
799
+ if (context.trustedLocalHuman !== true ||
800
+ !accepted ||
801
+ accepted.sourceEpicId !== displacedEffect.sourceEpicId ||
802
+ accepted.targetEpicId !== displacedEffect.targetEpicId) {
803
+ throw new error_types_1.RelationConfirmationRequiredError(displacedEffect);
804
+ }
805
+ }
806
+ if (newEligibleRoute) {
807
+ this.assertNoEligibleRouteCycleSync(newEligibleRoute, existing?.id);
808
+ }
809
+ const id = (0, node_crypto_1.randomUUID)();
810
+ const now = new Date().toISOString();
811
+ this.rawClient
812
+ .prepare(`INSERT INTO epic_relations (
813
+ id, left_epic_id, right_epic_id, type, direction,
814
+ created_by, created_by_agent_id, created_at, updated_at
815
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
816
+ ON CONFLICT(left_epic_id, right_epic_id) DO UPDATE SET
817
+ type = excluded.type,
818
+ direction = excluded.direction,
819
+ updated_at = excluded.updated_at`)
820
+ .run(id, leftEpicId, rightEpicId, type, direction, createdBy, createdByAgentId, now, now);
821
+ if (newEligibleRoute) {
822
+ this.deleteOutgoingEligibleRouteSync(newEligibleRoute.sourceEpicId, leftEpicId, rightEpicId);
823
+ }
824
+ const row = this.rawClient
825
+ .prepare(`SELECT id, left_epic_id, right_epic_id, type, direction,
826
+ created_by, created_by_agent_id, created_at, updated_at
827
+ FROM epic_relations
828
+ WHERE left_epic_id = ? AND right_epic_id = ?`)
829
+ .get(leftEpicId, rightEpicId);
830
+ return {
831
+ ...this.mapEpicRelation(row),
832
+ changed: true,
833
+ workspaceId: focal.workspace_id,
834
+ };
835
+ });
836
+ }
837
+ async deleteEpicRelation(epicId, relatedEpicId, context) {
838
+ return this.txRunner.runImmediateQueuedOrJoin(() => {
839
+ const [focal, related] = this.validateRelationPairSync(epicId, relatedEpicId);
840
+ this.authorizeRelationWriteSync(focal, related, context);
841
+ const pair = this.canonicalRelationPair(focal.id, related.id);
842
+ const deleted = this.rawClient
843
+ .prepare('DELETE FROM epic_relations WHERE left_epic_id = ? AND right_epic_id = ?')
844
+ .run(pair.leftEpicId, pair.rightEpicId).changes > 0;
845
+ return { deleted, workspaceId: focal.workspace_id };
846
+ });
847
+ }
848
+ async listEpicRelations(epicId, options = {}) {
849
+ const focal = this.getRelationContextSync(epicId);
850
+ this.assertRelationReadWorkspace(focal, options);
851
+ const limit = this.normalizeRelationListLimit(options.limit, 'Relation');
852
+ const offset = this.normalizeRelationListOffset(options.offset, 'Relation');
853
+ const relatedEpicId = options.relatedEpicId ?? null;
854
+ const queryArgs = [
855
+ epicId,
856
+ epicId,
857
+ epicId,
858
+ options.excludeMcpHidden ? 1 : 0,
859
+ relatedEpicId,
860
+ relatedEpicId,
861
+ ];
862
+ const count = this.rawClient
863
+ .prepare(`SELECT COUNT(*) AS total
864
+ FROM epic_relations relation
865
+ INNER JOIN epics target
866
+ ON target.id = CASE
867
+ WHEN relation.left_epic_id = ? THEN relation.right_epic_id
868
+ ELSE relation.left_epic_id
869
+ END
870
+ INNER JOIN statuses status ON status.id = target.status_id
871
+ WHERE (relation.left_epic_id = ? OR relation.right_epic_id = ?)
872
+ AND (? = 0 OR status.mcp_hidden = 0)
873
+ AND (? IS NULL OR target.id = ?)`)
874
+ .get(...queryArgs);
875
+ const rows = this.rawClient
876
+ .prepare(`SELECT
877
+ relation.id AS relation_id,
878
+ relation.left_epic_id,
879
+ relation.right_epic_id,
880
+ relation.type,
881
+ relation.direction,
882
+ relation.created_at,
883
+ relation.updated_at,
884
+ target.id AS epic_id,
885
+ target.project_id,
886
+ project.name AS project_name,
887
+ target.title,
888
+ target.status_id,
889
+ status.label AS status_label,
890
+ status.color AS status_color,
891
+ status.mcp_hidden AS status_mcp_hidden
892
+ FROM epic_relations relation
893
+ INNER JOIN epics target
894
+ ON target.id = CASE
895
+ WHEN relation.left_epic_id = ? THEN relation.right_epic_id
896
+ ELSE relation.left_epic_id
897
+ END
898
+ INNER JOIN projects project ON project.id = target.project_id
899
+ INNER JOIN statuses status ON status.id = target.status_id
900
+ WHERE (relation.left_epic_id = ? OR relation.right_epic_id = ?)
901
+ AND (? = 0 OR status.mcp_hidden = 0)
902
+ AND (? IS NULL OR target.id = ?)
903
+ ORDER BY lower(project.name), lower(target.title), target.id
904
+ LIMIT ? OFFSET ?`)
905
+ .all(...queryArgs, limit, offset);
906
+ return {
907
+ items: rows.map((row) => {
908
+ const endpoints = (0, domain_models_1.resolveRelationEndpoints)(row.left_epic_id, row.right_epic_id, row.direction);
909
+ return {
910
+ relationId: row.relation_id,
911
+ epicId: row.epic_id,
912
+ projectId: row.project_id,
913
+ projectName: row.project_name,
914
+ title: row.title,
915
+ statusId: row.status_id,
916
+ statusLabel: row.status_label,
917
+ statusColor: row.status_color,
918
+ statusMcpHidden: Boolean(row.status_mcp_hidden),
919
+ type: this.toFocalRelationType(epicId, row),
920
+ sourceEpicId: endpoints?.sourceEpicId ?? null,
921
+ targetEpicId: endpoints?.targetEpicId ?? null,
922
+ createdAt: row.created_at,
923
+ updatedAt: row.updated_at,
924
+ };
925
+ }),
926
+ total: count.total,
927
+ limit,
928
+ offset,
929
+ };
930
+ }
931
+ async summarizeEpicRelationsBatch(epicIds, options = {}) {
932
+ if (!Array.isArray(epicIds)) {
933
+ throw new error_types_1.ValidationError('epicIds must be an array.');
934
+ }
935
+ const uniqueIds = Array.from(new Set(epicIds.map((id) => (typeof id === 'string' ? id.trim() : ''))));
936
+ if (uniqueIds.some((id) => id.length === 0)) {
937
+ throw new error_types_1.ValidationError('epicIds must contain non-empty strings.');
938
+ }
939
+ if (uniqueIds.length > MAX_RELATION_BATCH_SIZE) {
940
+ throw new error_types_1.ValidationError(`epicIds cannot contain more than ${MAX_RELATION_BATCH_SIZE} IDs.`);
941
+ }
942
+ if (uniqueIds.length === 0)
943
+ return new Map();
944
+ const rows = this.rawClient
945
+ .prepare(`WITH requested(epic_id) AS (
946
+ SELECT value FROM json_each(?)
947
+ ), focal_relations AS (
948
+ SELECT relation.left_epic_id AS epic_id,
949
+ relation.right_epic_id AS target_epic_id,
950
+ relation.type, relation.direction, 1 AS focal_is_left
951
+ FROM epic_relations relation
952
+ UNION ALL
953
+ SELECT relation.right_epic_id AS epic_id,
954
+ relation.left_epic_id AS target_epic_id,
955
+ relation.type, relation.direction, 0 AS focal_is_left
956
+ FROM epic_relations relation
957
+ )
958
+ SELECT
959
+ focal.epic_id,
960
+ SUM(CASE WHEN focal.type = 'related' THEN 1 ELSE 0 END) AS related,
961
+ SUM(CASE WHEN focal.type = 'related' AND (
962
+ (focal.focal_is_left = 1 AND focal.direction = 'left_to_right') OR
963
+ (focal.focal_is_left = 0 AND focal.direction = 'right_to_left')
964
+ ) THEN 1 ELSE 0 END) AS related_targets,
965
+ SUM(CASE WHEN focal.type = 'related' AND (
966
+ (focal.focal_is_left = 1 AND focal.direction = 'right_to_left') OR
967
+ (focal.focal_is_left = 0 AND focal.direction = 'left_to_right')
968
+ ) THEN 1 ELSE 0 END) AS related_sources,
969
+ SUM(CASE WHEN focal.type = 'related' AND focal.direction = 'none' THEN 1 ELSE 0 END) AS related_neutral,
970
+ SUM(CASE WHEN focal.type = 'blocks' AND (
971
+ (focal.focal_is_left = 1 AND focal.direction = 'left_to_right') OR
972
+ (focal.focal_is_left = 0 AND focal.direction = 'right_to_left')
973
+ ) THEN 1 ELSE 0 END) AS blocks,
974
+ SUM(CASE WHEN focal.type = 'blocks' AND (
975
+ (focal.focal_is_left = 1 AND focal.direction = 'right_to_left') OR
976
+ (focal.focal_is_left = 0 AND focal.direction = 'left_to_right')
977
+ ) THEN 1 ELSE 0 END) AS blocked_by,
978
+ COUNT(*) AS total
979
+ FROM requested
980
+ INNER JOIN focal_relations focal ON focal.epic_id = requested.epic_id
981
+ INNER JOIN epics target ON target.id = focal.target_epic_id
982
+ INNER JOIN statuses target_status ON target_status.id = target.status_id
983
+ INNER JOIN epics focal_epic ON focal_epic.id = focal.epic_id
984
+ INNER JOIN projects focal_project ON focal_project.id = focal_epic.project_id
985
+ WHERE (? = 0 OR target_status.mcp_hidden = 0)
986
+ AND (? IS NULL OR focal_project.workspace_id = ?)
987
+ GROUP BY focal.epic_id`)
988
+ .all(JSON.stringify(uniqueIds), options.excludeMcpHidden ? 1 : 0, options.workspaceId ?? null, options.workspaceId ?? null);
989
+ return new Map(rows.map((row) => [
990
+ row.epic_id,
991
+ {
992
+ epicId: row.epic_id,
993
+ related: Number(row.related),
994
+ relatedTargets: Number(row.related_targets),
995
+ relatedSources: Number(row.related_sources),
996
+ relatedNeutral: Number(row.related_neutral),
997
+ blocks: Number(row.blocks),
998
+ blockedBy: Number(row.blocked_by),
999
+ total: Number(row.total),
1000
+ },
1001
+ ]));
1002
+ }
1003
+ async listEpicRelationCandidates(epicId, options = {}) {
1004
+ const focal = this.getRelationContextSync(epicId);
1005
+ this.assertRelationReadWorkspace(focal, options);
1006
+ const limit = this.normalizeRelationListLimit(options.limit, 'Relation candidate');
1007
+ const offset = this.normalizeRelationListOffset(options.offset, 'Relation candidate');
1008
+ const search = typeof options.q === 'string' ? options.q.trim().toLowerCase() : '';
1009
+ if (search.length > 200) {
1010
+ throw new error_types_1.ValidationError('Relation candidate search cannot exceed 200 characters.');
1011
+ }
1012
+ const conditions = [
1013
+ 'project.workspace_id = ?',
1014
+ 'project.is_template = 0',
1015
+ '(? = 0 OR status.mcp_hidden = 0)',
1016
+ 'candidate.id <> ?',
1017
+ '(candidate.parent_id IS NULL OR candidate.parent_id <> ?)',
1018
+ '(? IS NULL OR candidate.id <> ?)',
1019
+ `NOT EXISTS (
1020
+ SELECT 1 FROM epic_relations existing
1021
+ WHERE (existing.left_epic_id = ? AND existing.right_epic_id = candidate.id)
1022
+ OR (existing.right_epic_id = ? AND existing.left_epic_id = candidate.id)
1023
+ )`,
1024
+ ];
1025
+ const parameters = [
1026
+ focal.workspace_id,
1027
+ options.excludeMcpHidden ? 1 : 0,
1028
+ focal.id,
1029
+ focal.id,
1030
+ focal.parent_id,
1031
+ focal.parent_id,
1032
+ focal.id,
1033
+ focal.id,
1034
+ ];
1035
+ if (search) {
1036
+ const escapedSearch = search.replace(/[\\%_]/g, '\\$&');
1037
+ conditions.push(`(lower(candidate.title) LIKE ? ESCAPE '\\'
1038
+ OR lower(substr(candidate.id, 1, ?)) = ?)`);
1039
+ parameters.push(`%${escapedSearch}%`, search.length, search);
1040
+ }
1041
+ const where = conditions.join('\n AND ');
1042
+ const totalRow = this.rawClient
1043
+ .prepare(`SELECT COUNT(*) AS count
1044
+ FROM epics candidate
1045
+ INNER JOIN projects project ON project.id = candidate.project_id
1046
+ INNER JOIN statuses status ON status.id = candidate.status_id
1047
+ WHERE ${where}`)
1048
+ .get(...parameters);
1049
+ const rows = this.rawClient
1050
+ .prepare(`SELECT candidate.id, candidate.project_id, project.name AS project_name,
1051
+ candidate.title, candidate.status_id, status.label AS status_label,
1052
+ status.color AS status_color, status.mcp_hidden AS status_mcp_hidden,
1053
+ candidate.parent_id
1054
+ FROM epics candidate
1055
+ INNER JOIN projects project ON project.id = candidate.project_id
1056
+ INNER JOIN statuses status ON status.id = candidate.status_id
1057
+ WHERE ${where}
1058
+ ORDER BY lower(project.name), lower(candidate.title), candidate.id
1059
+ LIMIT ? OFFSET ?`)
1060
+ .all(...parameters, limit, offset);
1061
+ return {
1062
+ items: rows.map((row) => this.mapRelationCandidate(row)),
1063
+ total: Number(totalRow.count),
1064
+ limit,
1065
+ offset,
1066
+ };
1067
+ }
1068
+ async getWorkspaceEpicsByIdPrefix(epicId, prefix, options = {}) {
1069
+ const focal = this.getRelationContextSync(epicId);
1070
+ this.assertRelationReadWorkspace(focal, options);
1071
+ const normalizedPrefix = typeof prefix === 'string' ? prefix.trim().toLowerCase() : '';
1072
+ if (!/^[a-f0-9-]{8,36}$/.test(normalizedPrefix)) {
1073
+ return [];
1074
+ }
1075
+ const rows = this.rawClient
1076
+ .prepare(`SELECT candidate.id, candidate.project_id, project.name AS project_name,
1077
+ candidate.title, candidate.status_id, status.label AS status_label,
1078
+ status.color AS status_color, status.mcp_hidden AS status_mcp_hidden,
1079
+ candidate.parent_id
1080
+ FROM epics candidate
1081
+ INNER JOIN projects project ON project.id = candidate.project_id
1082
+ INNER JOIN statuses status ON status.id = candidate.status_id
1083
+ WHERE project.workspace_id = ?
1084
+ AND project.is_template = 0
1085
+ AND (? = 0 OR status.mcp_hidden = 0)
1086
+ AND candidate.id <> ?
1087
+ AND lower(substr(candidate.id, 1, ?)) = ?
1088
+ ORDER BY candidate.id
1089
+ LIMIT ?`)
1090
+ .all(focal.workspace_id, options.excludeMcpHidden ? 1 : 0, focal.id, normalizedPrefix.length, normalizedPrefix, MAX_RELATION_PREFIX_RESULTS);
1091
+ return rows.map((row) => this.mapRelationCandidate(row));
1092
+ }
1093
+ validateRelationPairSync(epicId, relatedEpicId) {
1094
+ if (typeof epicId !== 'string' || typeof relatedEpicId !== 'string') {
1095
+ throw new error_types_1.ValidationError('Relation Epic IDs must be strings.');
1096
+ }
1097
+ if (epicId === relatedEpicId) {
1098
+ throw new error_types_1.ValidationError('An Epic cannot have a relation to itself.', { epicId });
1099
+ }
1100
+ const focal = this.getRelationContextSync(epicId);
1101
+ let related;
1102
+ try {
1103
+ related = this.getRelationContextSync(relatedEpicId);
1104
+ }
1105
+ catch (error) {
1106
+ if (error instanceof error_types_1.NotFoundError) {
1107
+ throw new error_types_1.NotFoundError('Related Epic');
1108
+ }
1109
+ throw error;
1110
+ }
1111
+ if (focal.workspace_id !== related.workspace_id) {
1112
+ throw new error_types_1.NotFoundError('Related Epic');
1113
+ }
1114
+ if (focal.parent_id === related.id || related.parent_id === focal.id) {
1115
+ throw new error_types_1.ValidationError('A parent and direct sub-Epic cannot have a relation.', {
1116
+ epicId,
1117
+ relatedEpicId,
1118
+ });
1119
+ }
1120
+ return [focal, related];
1121
+ }
1122
+ authorizeRelationWriteSync(focal, related, context) {
1123
+ if (context?.actor?.type === 'guest') {
1124
+ throw new error_types_1.ForbiddenError('Guests cannot write Epic relations.');
1125
+ }
1126
+ if (context?.trustedLocalHuman === true && !context.actor) {
1127
+ return;
1128
+ }
1129
+ if (context?.actor?.type !== 'agent') {
1130
+ throw new error_types_1.ForbiddenError('An authorized relation writer is required.');
1131
+ }
1132
+ const currentAgent = this.rawClient
1133
+ .prepare('SELECT project_id, is_project_owner FROM agents WHERE id = ?')
1134
+ .get(context.actor.id);
1135
+ if (!currentAgent || currentAgent.project_id !== focal.project_id) {
1136
+ throw new error_types_1.ForbiddenError('The caller must currently belong to the focal Epic project.');
1137
+ }
1138
+ if (related.project_id !== focal.project_id && !Boolean(currentAgent.is_project_owner)) {
1139
+ throw new error_types_1.ForbiddenError('Cross-project Epic relations require the focal project current Project Owner.');
1140
+ }
1141
+ }
1142
+ assertRelationReadWorkspace(focal, options) {
1143
+ if (options.workspaceId && focal.workspace_id !== options.workspaceId) {
1144
+ throw new error_types_1.NotFoundError('Epic');
1145
+ }
1146
+ }
1147
+ getRelationContextSync(epicId) {
1148
+ const row = this.rawClient
1149
+ .prepare(`SELECT epic.id, epic.project_id, project.workspace_id, epic.parent_id
1150
+ FROM epics epic
1151
+ INNER JOIN projects project ON project.id = epic.project_id
1152
+ WHERE epic.id = ?`)
1153
+ .get(epicId);
1154
+ if (!row) {
1155
+ throw new error_types_1.NotFoundError('Epic', epicId);
1156
+ }
1157
+ return row;
1158
+ }
1159
+ canonicalRelationPair(epicId, relatedEpicId) {
1160
+ return epicId < relatedEpicId
1161
+ ? { leftEpicId: epicId, rightEpicId: relatedEpicId }
1162
+ : { leftEpicId: relatedEpicId, rightEpicId: epicId };
1163
+ }
1164
+ toStoredRelation(focalEpicId, leftEpicId, focalType) {
1165
+ const focalIsLeft = focalEpicId === leftEpicId;
1166
+ if (focalType === 'related') {
1167
+ return { type: 'related', direction: focalIsLeft ? 'left_to_right' : 'right_to_left' };
1168
+ }
1169
+ if (focalType !== 'blocks' && focalType !== 'blocked_by') {
1170
+ throw new error_types_1.ValidationError('Relation type must be related, blocks, or blocked_by.');
1171
+ }
1172
+ const leftBlocksRight = focalType === 'blocks' ? focalIsLeft : !focalIsLeft;
1173
+ return {
1174
+ type: 'blocks',
1175
+ direction: leftBlocksRight ? 'left_to_right' : 'right_to_left',
1176
+ };
1177
+ }
1178
+ static ELIGIBLE_ROUTE_SOURCE_PREDICATE = `
1179
+ relation.type = 'related'
1180
+ AND relation.direction != 'none'
1181
+ AND left_epic.parent_id IS NULL
1182
+ AND right_epic.parent_id IS NULL
1183
+ AND left_epic.project_id = right_epic.project_id
1184
+ AND (
1185
+ (relation.left_epic_id = ? AND relation.direction = 'left_to_right')
1186
+ OR (relation.right_epic_id = ? AND relation.direction = 'right_to_left')
1187
+ )`;
1188
+ assertNoEligibleRouteCycleSync(newEffect, replacedRowId) {
1189
+ const outgoing = this.rawClient.prepare(`SELECT relation.id, relation.left_epic_id, relation.right_epic_id, relation.direction
1190
+ FROM epic_relations relation
1191
+ INNER JOIN epics left_epic ON left_epic.id = relation.left_epic_id
1192
+ INNER JOIN epics right_epic ON right_epic.id = relation.right_epic_id
1193
+ WHERE${EpicStorageDelegate.ELIGIBLE_ROUTE_SOURCE_PREDICATE}`);
1194
+ const visited = new Set([newEffect.targetEpicId]);
1195
+ let current = newEffect.targetEpicId;
1196
+ for (let step = 0; step <= MAX_RELATION_BATCH_SIZE; step++) {
1197
+ if (current === newEffect.sourceEpicId) {
1198
+ throw new error_types_1.ValidationError('A time route cannot form a cycle.', {
1199
+ sourceEpicId: newEffect.sourceEpicId,
1200
+ targetEpicId: newEffect.targetEpicId,
1201
+ });
1202
+ }
1203
+ const edge = outgoing.get(current, current);
1204
+ if (!edge || edge.id === replacedRowId) {
1205
+ return;
1206
+ }
1207
+ const effect = (0, domain_models_1.resolveRelationEndpoints)(edge.left_epic_id, edge.right_epic_id, edge.direction);
1208
+ if (!effect || visited.has(effect.targetEpicId)) {
1209
+ return;
1210
+ }
1211
+ visited.add(effect.targetEpicId);
1212
+ current = effect.targetEpicId;
1213
+ }
1214
+ }
1215
+ findOutgoingEligibleRouteSync(sourceEpicId, excludedRowId) {
1216
+ const row = this.rawClient
1217
+ .prepare(`SELECT relation.id, relation.left_epic_id, relation.right_epic_id, relation.direction
1218
+ FROM epic_relations relation
1219
+ INNER JOIN epics left_epic ON left_epic.id = relation.left_epic_id
1220
+ INNER JOIN epics right_epic ON right_epic.id = relation.right_epic_id
1221
+ WHERE${EpicStorageDelegate.ELIGIBLE_ROUTE_SOURCE_PREDICATE}
1222
+ LIMIT 1`)
1223
+ .get(sourceEpicId, sourceEpicId);
1224
+ if (!row || row.id === excludedRowId) {
1225
+ return null;
1226
+ }
1227
+ return (0, domain_models_1.resolveRelationEndpoints)(row.left_epic_id, row.right_epic_id, row.direction);
1228
+ }
1229
+ deleteOutgoingEligibleRouteSync(sourceEpicId, keepLeftEpicId, keepRightEpicId) {
1230
+ this.rawClient
1231
+ .prepare(`DELETE FROM epic_relations
1232
+ WHERE id IN (
1233
+ SELECT relation.id
1234
+ FROM epic_relations relation
1235
+ INNER JOIN epics left_epic ON left_epic.id = relation.left_epic_id
1236
+ INNER JOIN epics right_epic ON right_epic.id = relation.right_epic_id
1237
+ WHERE${EpicStorageDelegate.ELIGIBLE_ROUTE_SOURCE_PREDICATE}
1238
+ )
1239
+ AND NOT (left_epic_id = ? AND right_epic_id = ?)`)
1240
+ .run(sourceEpicId, sourceEpicId, keepLeftEpicId, keepRightEpicId);
1241
+ }
1242
+ ensureEpicIsNotRouteEndpointSync(epicId) {
1243
+ const routed = this.rawClient
1244
+ .prepare(`SELECT relation.left_epic_id, relation.right_epic_id
1245
+ FROM epic_relations relation
1246
+ INNER JOIN epics left_epic ON left_epic.id = relation.left_epic_id
1247
+ INNER JOIN epics right_epic ON right_epic.id = relation.right_epic_id
1248
+ WHERE relation.type = 'related'
1249
+ AND relation.direction != 'none'
1250
+ AND left_epic.parent_id IS NULL
1251
+ AND right_epic.parent_id IS NULL
1252
+ AND left_epic.project_id = right_epic.project_id
1253
+ AND (relation.left_epic_id = ? OR relation.right_epic_id = ?)
1254
+ LIMIT 1`)
1255
+ .get(epicId, epicId);
1256
+ if (routed) {
1257
+ throw new error_types_1.ValidationError('Delete the eligible Related pair explicitly before reparenting one of its endpoints.', { epicId, leftEpicId: routed.left_epic_id, rightEpicId: routed.right_epic_id });
1258
+ }
1259
+ }
1260
+ assertPromotionKeepsEligibleRoutesSync(epicId, projectId) {
1261
+ const newlyEligible = this.rawClient
1262
+ .prepare(`SELECT CASE WHEN relation.direction = 'left_to_right'
1263
+ THEN relation.left_epic_id ELSE relation.right_epic_id END AS source_id,
1264
+ CASE WHEN relation.direction = 'left_to_right'
1265
+ THEN relation.right_epic_id ELSE relation.left_epic_id END AS target_id
1266
+ FROM epic_relations relation
1267
+ INNER JOIN epics other
1268
+ ON other.id = CASE
1269
+ WHEN relation.left_epic_id = ? THEN relation.right_epic_id
1270
+ ELSE relation.left_epic_id END
1271
+ WHERE relation.type = 'related'
1272
+ AND relation.direction != 'none'
1273
+ AND (relation.left_epic_id = ? OR relation.right_epic_id = ?)
1274
+ AND other.parent_id IS NULL
1275
+ AND other.project_id = ?`)
1276
+ .all(epicId, epicId, epicId, projectId);
1277
+ if (newlyEligible.length === 0) {
1278
+ return;
1279
+ }
1280
+ const duplicateSource = this.rawClient
1281
+ .prepare(`SELECT CASE WHEN relation.direction = 'left_to_right'
1282
+ THEN relation.left_epic_id ELSE relation.right_epic_id END AS source_id,
1283
+ COUNT(*) AS route_count
1284
+ FROM epic_relations relation
1285
+ INNER JOIN epics left_epic ON left_epic.id = relation.left_epic_id
1286
+ INNER JOIN epics right_epic ON right_epic.id = relation.right_epic_id
1287
+ WHERE relation.type = 'related'
1288
+ AND relation.direction != 'none'
1289
+ AND (left_epic.parent_id IS NULL OR left_epic.id = ?)
1290
+ AND (right_epic.parent_id IS NULL OR right_epic.id = ?)
1291
+ AND left_epic.project_id = right_epic.project_id
1292
+ GROUP BY source_id
1293
+ HAVING COUNT(*) > 1
1294
+ LIMIT 1`)
1295
+ .get(epicId, epicId);
1296
+ if (duplicateSource) {
1297
+ throw new error_types_1.ValidationError('Promoting this Epic would activate more than one eligible time route from one source. Delete the extra Related pair explicitly first.', {
1298
+ epicId,
1299
+ sourceEpicId: duplicateSource.source_id,
1300
+ routeCount: duplicateSource.route_count,
1301
+ });
1302
+ }
1303
+ const outgoing = this.rawClient.prepare(`SELECT relation.left_epic_id, relation.right_epic_id, relation.direction
1304
+ FROM epic_relations relation
1305
+ INNER JOIN epics left_epic ON left_epic.id = relation.left_epic_id
1306
+ INNER JOIN epics right_epic ON right_epic.id = relation.right_epic_id
1307
+ WHERE relation.type = 'related'
1308
+ AND relation.direction != 'none'
1309
+ AND (left_epic.parent_id IS NULL OR left_epic.id = ?)
1310
+ AND (right_epic.parent_id IS NULL OR right_epic.id = ?)
1311
+ AND left_epic.project_id = right_epic.project_id
1312
+ AND (
1313
+ (relation.left_epic_id = ? AND relation.direction = 'left_to_right')
1314
+ OR (relation.right_epic_id = ? AND relation.direction = 'right_to_left')
1315
+ )`);
1316
+ for (const edge of newlyEligible) {
1317
+ const visited = new Set([edge.target_id]);
1318
+ let current = edge.target_id;
1319
+ for (let step = 0; step <= MAX_RELATION_BATCH_SIZE; step++) {
1320
+ if (current === edge.source_id) {
1321
+ throw new error_types_1.ValidationError('Promoting this Epic would complete a time-route cycle. Delete one Related pair explicitly first.', { epicId, sourceEpicId: edge.source_id, targetEpicId: edge.target_id });
1322
+ }
1323
+ const next = outgoing.get(epicId, epicId, current, current);
1324
+ if (!next) {
1325
+ break;
1326
+ }
1327
+ const effect = (0, domain_models_1.resolveRelationEndpoints)(next.left_epic_id, next.right_epic_id, next.direction);
1328
+ if (!effect || visited.has(effect.targetEpicId)) {
1329
+ break;
1330
+ }
1331
+ visited.add(effect.targetEpicId);
1332
+ current = effect.targetEpicId;
1333
+ }
1334
+ }
1335
+ }
1336
+ toFocalRelationType(focalEpicId, relation) {
1337
+ if (relation.type === 'related') {
1338
+ return 'related';
1339
+ }
1340
+ const focalIsLeft = focalEpicId === relation.left_epic_id;
1341
+ const focalBlocks = (focalIsLeft && relation.direction === 'left_to_right') ||
1342
+ (!focalIsLeft && relation.direction === 'right_to_left');
1343
+ return focalBlocks ? 'blocks' : 'blocked_by';
1344
+ }
1345
+ mapEpicRelation(row) {
1346
+ const endpoints = (0, domain_models_1.resolveRelationEndpoints)(row.left_epic_id, row.right_epic_id, row.direction);
1347
+ return {
1348
+ id: row.id,
1349
+ leftEpicId: row.left_epic_id,
1350
+ rightEpicId: row.right_epic_id,
1351
+ type: row.type,
1352
+ direction: row.direction,
1353
+ sourceEpicId: endpoints?.sourceEpicId ?? null,
1354
+ targetEpicId: endpoints?.targetEpicId ?? null,
1355
+ createdBy: row.created_by,
1356
+ createdByAgentId: row.created_by_agent_id,
1357
+ createdAt: row.created_at,
1358
+ updatedAt: row.updated_at,
1359
+ };
1360
+ }
1361
+ mapRelationCandidate(row) {
1362
+ return {
1363
+ id: row.id,
1364
+ projectId: row.project_id,
1365
+ projectName: row.project_name,
1366
+ title: row.title,
1367
+ statusId: row.status_id,
1368
+ statusLabel: row.status_label,
1369
+ statusColor: row.status_color,
1370
+ statusMcpHidden: Boolean(row.status_mcp_hidden),
1371
+ parentId: row.parent_id,
1372
+ };
1373
+ }
1374
+ normalizeRelationListLimit(limit, label) {
1375
+ if (limit === undefined)
1376
+ return 50;
1377
+ if (!Number.isInteger(limit) || limit < 1 || limit > MAX_RELATION_LIST_LIMIT) {
1378
+ throw new error_types_1.ValidationError(`${label} limit must be between 1 and ${MAX_RELATION_LIST_LIMIT}.`);
1379
+ }
1380
+ return limit;
1381
+ }
1382
+ normalizeRelationListOffset(offset, label) {
1383
+ if (offset === undefined)
1384
+ return 0;
1385
+ if (!Number.isInteger(offset) || offset < 0) {
1386
+ throw new error_types_1.ValidationError(`${label} offset must be a non-negative integer.`);
1387
+ }
1388
+ return offset;
1389
+ }
698
1390
  ensureValidEpicParentSync(projectId, parentId, childId) {
699
1391
  if (!parentId) {
700
1392
  return;
@@ -719,6 +1411,18 @@ class EpicStorageDelegate extends base_storage_delegate_1.BaseStorageDelegate {
719
1411
  });
720
1412
  }
721
1413
  if (childId) {
1414
+ const existingRelation = this.rawClient
1415
+ .prepare(`SELECT 1 FROM epic_relations
1416
+ WHERE (left_epic_id = ? AND right_epic_id = ?)
1417
+ OR (left_epic_id = ? AND right_epic_id = ?)
1418
+ LIMIT 1`)
1419
+ .get(childId, parentId, parentId, childId);
1420
+ if (existingRelation) {
1421
+ throw new error_types_1.ValidationError('A parent and direct sub-Epic cannot have a relation.', {
1422
+ epicId: childId,
1423
+ parentId,
1424
+ });
1425
+ }
722
1426
  const descendants = this.db
723
1427
  .select({ id: schema_1.epics.id })
724
1428
  .from(schema_1.epics)