@a5c-ai/agent-comm-mux 5.0.1-staging.04ca6ab00d21

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 (263) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/dist/adapter-registry.d.ts +51 -0
  4. package/dist/adapter-registry.d.ts.map +1 -0
  5. package/dist/adapter-registry.js +208 -0
  6. package/dist/adapter-registry.js.map +1 -0
  7. package/dist/adapter-types.d.ts +164 -0
  8. package/dist/adapter-types.d.ts.map +1 -0
  9. package/dist/adapter-types.js +27 -0
  10. package/dist/adapter-types.js.map +1 -0
  11. package/dist/adapter.d.ts +248 -0
  12. package/dist/adapter.d.ts.map +1 -0
  13. package/dist/adapter.js +18 -0
  14. package/dist/adapter.js.map +1 -0
  15. package/dist/atomic-fs.d.ts +33 -0
  16. package/dist/atomic-fs.d.ts.map +1 -0
  17. package/dist/atomic-fs.js +190 -0
  18. package/dist/atomic-fs.js.map +1 -0
  19. package/dist/auth-manager.d.ts +37 -0
  20. package/dist/auth-manager.d.ts.map +1 -0
  21. package/dist/auth-manager.js +94 -0
  22. package/dist/auth-manager.js.map +1 -0
  23. package/dist/auth-types.d.ts +83 -0
  24. package/dist/auth-types.d.ts.map +1 -0
  25. package/dist/auth-types.js +10 -0
  26. package/dist/auth-types.js.map +1 -0
  27. package/dist/automation.d.ts +104 -0
  28. package/dist/automation.d.ts.map +1 -0
  29. package/dist/automation.js +2 -0
  30. package/dist/automation.js.map +1 -0
  31. package/dist/browser.d.ts +5 -0
  32. package/dist/browser.d.ts.map +1 -0
  33. package/dist/browser.js +2 -0
  34. package/dist/browser.js.map +1 -0
  35. package/dist/builtin-hooks.d.ts +24 -0
  36. package/dist/builtin-hooks.d.ts.map +1 -0
  37. package/dist/builtin-hooks.js +80 -0
  38. package/dist/builtin-hooks.js.map +1 -0
  39. package/dist/capabilities.d.ts +251 -0
  40. package/dist/capabilities.d.ts.map +1 -0
  41. package/dist/capabilities.js +7 -0
  42. package/dist/capabilities.js.map +1 -0
  43. package/dist/client.d.ts +118 -0
  44. package/dist/client.d.ts.map +1 -0
  45. package/dist/client.js +316 -0
  46. package/dist/client.js.map +1 -0
  47. package/dist/config-manager.d.ts +85 -0
  48. package/dist/config-manager.d.ts.map +1 -0
  49. package/dist/config-manager.js +202 -0
  50. package/dist/config-manager.js.map +1 -0
  51. package/dist/config-types.d.ts +134 -0
  52. package/dist/config-types.d.ts.map +1 -0
  53. package/dist/config-types.js +10 -0
  54. package/dist/config-types.js.map +1 -0
  55. package/dist/cost-utils.d.ts +31 -0
  56. package/dist/cost-utils.d.ts.map +1 -0
  57. package/dist/cost-utils.js +69 -0
  58. package/dist/cost-utils.js.map +1 -0
  59. package/dist/errors.d.ts +50 -0
  60. package/dist/errors.d.ts.map +1 -0
  61. package/dist/errors.js +85 -0
  62. package/dist/errors.js.map +1 -0
  63. package/dist/events-control.d.ts +91 -0
  64. package/dist/events-control.d.ts.map +1 -0
  65. package/dist/events-control.js +8 -0
  66. package/dist/events-control.js.map +1 -0
  67. package/dist/events.d.ts +285 -0
  68. package/dist/events.d.ts.map +1 -0
  69. package/dist/events.js +8 -0
  70. package/dist/events.js.map +1 -0
  71. package/dist/hook-catalog.d.ts +20 -0
  72. package/dist/hook-catalog.d.ts.map +1 -0
  73. package/dist/hook-catalog.js +63 -0
  74. package/dist/hook-catalog.js.map +1 -0
  75. package/dist/hook-dispatcher.d.ts +24 -0
  76. package/dist/hook-dispatcher.d.ts.map +1 -0
  77. package/dist/hook-dispatcher.js +91 -0
  78. package/dist/hook-dispatcher.js.map +1 -0
  79. package/dist/hook-payload.d.ts +24 -0
  80. package/dist/hook-payload.d.ts.map +1 -0
  81. package/dist/hook-payload.js +62 -0
  82. package/dist/hook-payload.js.map +1 -0
  83. package/dist/hooks.d.ts +59 -0
  84. package/dist/hooks.d.ts.map +1 -0
  85. package/dist/hooks.js +118 -0
  86. package/dist/hooks.js.map +1 -0
  87. package/dist/host-detection.d.ts +67 -0
  88. package/dist/host-detection.d.ts.map +1 -0
  89. package/dist/host-detection.js +147 -0
  90. package/dist/host-detection.js.map +1 -0
  91. package/dist/index.d.ts +88 -0
  92. package/dist/index.d.ts.map +1 -0
  93. package/dist/index.js +50 -0
  94. package/dist/index.js.map +1 -0
  95. package/dist/interaction-channel-impl.d.ts +56 -0
  96. package/dist/interaction-channel-impl.d.ts.map +1 -0
  97. package/dist/interaction-channel-impl.js +159 -0
  98. package/dist/interaction-channel-impl.js.map +1 -0
  99. package/dist/interaction.d.ts +93 -0
  100. package/dist/interaction.d.ts.map +1 -0
  101. package/dist/interaction.js +8 -0
  102. package/dist/interaction.js.map +1 -0
  103. package/dist/invocation.d.ts +100 -0
  104. package/dist/invocation.d.ts.map +1 -0
  105. package/dist/invocation.js +17 -0
  106. package/dist/invocation.js.map +1 -0
  107. package/dist/kanban.d.ts +894 -0
  108. package/dist/kanban.d.ts.map +1 -0
  109. package/dist/kanban.js +1155 -0
  110. package/dist/kanban.js.map +1 -0
  111. package/dist/merge.d.ts +37 -0
  112. package/dist/merge.d.ts.map +1 -0
  113. package/dist/merge.js +102 -0
  114. package/dist/merge.js.map +1 -0
  115. package/dist/model-registry.d.ts +61 -0
  116. package/dist/model-registry.d.ts.map +1 -0
  117. package/dist/model-registry.js +198 -0
  118. package/dist/model-registry.js.map +1 -0
  119. package/dist/plugin-manager-impl.d.ts +48 -0
  120. package/dist/plugin-manager-impl.d.ts.map +1 -0
  121. package/dist/plugin-manager-impl.js +343 -0
  122. package/dist/plugin-manager-impl.js.map +1 -0
  123. package/dist/plugin-manager.d.ts +81 -0
  124. package/dist/plugin-manager.d.ts.map +1 -0
  125. package/dist/plugin-manager.js +7 -0
  126. package/dist/plugin-manager.js.map +1 -0
  127. package/dist/plugin-types.d.ts +114 -0
  128. package/dist/plugin-types.d.ts.map +1 -0
  129. package/dist/plugin-types.js +7 -0
  130. package/dist/plugin-types.js.map +1 -0
  131. package/dist/process-tracker.d.ts +20 -0
  132. package/dist/process-tracker.d.ts.map +1 -0
  133. package/dist/process-tracker.js +96 -0
  134. package/dist/process-tracker.js.map +1 -0
  135. package/dist/profiles.d.ts +99 -0
  136. package/dist/profiles.d.ts.map +1 -0
  137. package/dist/profiles.js +231 -0
  138. package/dist/profiles.js.map +1 -0
  139. package/dist/programmatic-runner.d.ts +4 -0
  140. package/dist/programmatic-runner.d.ts.map +1 -0
  141. package/dist/programmatic-runner.js +110 -0
  142. package/dist/programmatic-runner.js.map +1 -0
  143. package/dist/provider-config.d.ts +40 -0
  144. package/dist/provider-config.d.ts.map +1 -0
  145. package/dist/provider-config.js +67 -0
  146. package/dist/provider-config.js.map +1 -0
  147. package/dist/provider-profiles.d.ts +47 -0
  148. package/dist/provider-profiles.d.ts.map +1 -0
  149. package/dist/provider-profiles.js +117 -0
  150. package/dist/provider-profiles.js.map +1 -0
  151. package/dist/provider-resolver.d.ts +15 -0
  152. package/dist/provider-resolver.d.ts.map +1 -0
  153. package/dist/provider-resolver.js +114 -0
  154. package/dist/provider-resolver.js.map +1 -0
  155. package/dist/provider-support-matrix.d.ts +12 -0
  156. package/dist/provider-support-matrix.d.ts.map +1 -0
  157. package/dist/provider-support-matrix.js +88 -0
  158. package/dist/provider-support-matrix.js.map +1 -0
  159. package/dist/remote-runner.d.ts +4 -0
  160. package/dist/remote-runner.d.ts.map +1 -0
  161. package/dist/remote-runner.js +114 -0
  162. package/dist/remote-runner.js.map +1 -0
  163. package/dist/retry.d.ts +13 -0
  164. package/dist/retry.d.ts.map +1 -0
  165. package/dist/retry.js +17 -0
  166. package/dist/retry.js.map +1 -0
  167. package/dist/run-handle-cost.d.ts +36 -0
  168. package/dist/run-handle-cost.d.ts.map +1 -0
  169. package/dist/run-handle-cost.js +62 -0
  170. package/dist/run-handle-cost.js.map +1 -0
  171. package/dist/run-handle-impl.d.ts +144 -0
  172. package/dist/run-handle-impl.d.ts.map +1 -0
  173. package/dist/run-handle-impl.js +695 -0
  174. package/dist/run-handle-impl.js.map +1 -0
  175. package/dist/run-handle.d.ts +222 -0
  176. package/dist/run-handle.d.ts.map +1 -0
  177. package/dist/run-handle.js +9 -0
  178. package/dist/run-handle.js.map +1 -0
  179. package/dist/run-options-validation.d.ts +23 -0
  180. package/dist/run-options-validation.d.ts.map +1 -0
  181. package/dist/run-options-validation.js +234 -0
  182. package/dist/run-options-validation.js.map +1 -0
  183. package/dist/run-options.d.ts +128 -0
  184. package/dist/run-options.d.ts.map +1 -0
  185. package/dist/run-options.js +329 -0
  186. package/dist/run-options.js.map +1 -0
  187. package/dist/runtime-hook-dispatcher.d.ts +19 -0
  188. package/dist/runtime-hook-dispatcher.d.ts.map +1 -0
  189. package/dist/runtime-hook-dispatcher.js +50 -0
  190. package/dist/runtime-hook-dispatcher.js.map +1 -0
  191. package/dist/runtime-hooks.d.ts +47 -0
  192. package/dist/runtime-hooks.d.ts.map +1 -0
  193. package/dist/runtime-hooks.js +2 -0
  194. package/dist/runtime-hooks.js.map +1 -0
  195. package/dist/schema/__tests__/event-schema.test.d.ts +1 -0
  196. package/dist/schema/__tests__/event-schema.test.d.ts.map +1 -0
  197. package/dist/schema/__tests__/event-schema.test.js +480 -0
  198. package/dist/schema/__tests__/event-schema.test.js.map +1 -0
  199. package/dist/schema/event-schema.d.ts +120 -0
  200. package/dist/schema/event-schema.d.ts.map +1 -0
  201. package/dist/schema/event-schema.js +546 -0
  202. package/dist/schema/event-schema.js.map +1 -0
  203. package/dist/schema/index.d.ts +7 -0
  204. package/dist/schema/index.d.ts.map +1 -0
  205. package/dist/schema/index.js +49 -0
  206. package/dist/schema/index.js.map +1 -0
  207. package/dist/session-manager-helpers.d.ts +33 -0
  208. package/dist/session-manager-helpers.d.ts.map +1 -0
  209. package/dist/session-manager-helpers.js +66 -0
  210. package/dist/session-manager-helpers.js.map +1 -0
  211. package/dist/session-manager.d.ts +70 -0
  212. package/dist/session-manager.d.ts.map +1 -0
  213. package/dist/session-manager.js +343 -0
  214. package/dist/session-manager.js.map +1 -0
  215. package/dist/session-types.d.ts +358 -0
  216. package/dist/session-types.d.ts.map +1 -0
  217. package/dist/session-types.js +10 -0
  218. package/dist/session-types.js.map +1 -0
  219. package/dist/spawn-invocation.d.ts +52 -0
  220. package/dist/spawn-invocation.d.ts.map +1 -0
  221. package/dist/spawn-invocation.js +218 -0
  222. package/dist/spawn-invocation.js.map +1 -0
  223. package/dist/spawn-runner-utils.d.ts +21 -0
  224. package/dist/spawn-runner-utils.d.ts.map +1 -0
  225. package/dist/spawn-runner-utils.js +102 -0
  226. package/dist/spawn-runner-utils.js.map +1 -0
  227. package/dist/spawn-runner.d.ts +21 -0
  228. package/dist/spawn-runner.d.ts.map +1 -0
  229. package/dist/spawn-runner.js +576 -0
  230. package/dist/spawn-runner.js.map +1 -0
  231. package/dist/spawn-runtime-hooks.d.ts +15 -0
  232. package/dist/spawn-runtime-hooks.d.ts.map +1 -0
  233. package/dist/spawn-runtime-hooks.js +136 -0
  234. package/dist/spawn-runtime-hooks.js.map +1 -0
  235. package/dist/state-machine.d.ts +33 -0
  236. package/dist/state-machine.d.ts.map +1 -0
  237. package/dist/state-machine.js +61 -0
  238. package/dist/state-machine.js.map +1 -0
  239. package/dist/storage.d.ts +47 -0
  240. package/dist/storage.d.ts.map +1 -0
  241. package/dist/storage.js +90 -0
  242. package/dist/storage.js.map +1 -0
  243. package/dist/stream-assembler.d.ts +51 -0
  244. package/dist/stream-assembler.d.ts.map +1 -0
  245. package/dist/stream-assembler.js +86 -0
  246. package/dist/stream-assembler.js.map +1 -0
  247. package/dist/tools/classify.d.ts +9 -0
  248. package/dist/tools/classify.d.ts.map +1 -0
  249. package/dist/tools/classify.js +77 -0
  250. package/dist/tools/classify.js.map +1 -0
  251. package/dist/tools/index.d.ts +2 -0
  252. package/dist/tools/index.d.ts.map +1 -0
  253. package/dist/tools/index.js +2 -0
  254. package/dist/tools/index.js.map +1 -0
  255. package/dist/types.d.ts +288 -0
  256. package/dist/types.d.ts.map +1 -0
  257. package/dist/types.js +8 -0
  258. package/dist/types.js.map +1 -0
  259. package/dist/workspaces.d.ts +142 -0
  260. package/dist/workspaces.d.ts.map +1 -0
  261. package/dist/workspaces.js +515 -0
  262. package/dist/workspaces.js.map +1 -0
  263. package/package.json +107 -0
package/dist/kanban.js ADDED
@@ -0,0 +1,1155 @@
1
+ const DEFAULT_PROJECT_STATUSES = [
2
+ { id: 'backlog', name: 'Backlog', kind: 'backlog' },
3
+ { id: 'ready', name: 'Ready', kind: 'backlog' },
4
+ { id: 'in-progress', name: 'In Progress', kind: 'active', wipLimit: 3 },
5
+ { id: 'review', name: 'Review', kind: 'active', wipLimit: 3 },
6
+ { id: 'done', name: 'Done', kind: 'done' },
7
+ ];
8
+ const DEFAULT_TEAM_SETTINGS = {
9
+ visibility: 'team',
10
+ defaultRole: 'contributor',
11
+ allowSelfAssign: true,
12
+ };
13
+ const DEFAULT_PROJECT_SETTINGS = {
14
+ reviewRequiredForDone: true,
15
+ activityScope: 'project-and-issues',
16
+ workspaceProvisioning: 'owners-maintainers',
17
+ };
18
+ const DEFAULT_PERMISSION_GRANTS = [
19
+ {
20
+ action: 'manage-project-settings',
21
+ roles: ['owner', 'maintainer'],
22
+ description: 'Only owners and maintainers can change shared project settings.',
23
+ },
24
+ {
25
+ action: 'manage-team-members',
26
+ roles: ['owner', 'maintainer'],
27
+ description: 'Team roster and role changes require elevated project roles.',
28
+ },
29
+ {
30
+ action: 'edit-board',
31
+ roles: ['owner', 'maintainer', 'contributor'],
32
+ description: 'Contributors can move cards and edit the shared board.',
33
+ },
34
+ {
35
+ action: 'assign-issues',
36
+ roles: ['owner', 'maintainer', 'contributor'],
37
+ description: 'Contributors can collaborate and self-assign when team settings allow it.',
38
+ },
39
+ {
40
+ action: 'review-work',
41
+ roles: ['owner', 'maintainer', 'contributor', 'viewer'],
42
+ description: 'Every collaborator can participate in review and activity visibility.',
43
+ },
44
+ {
45
+ action: 'manage-workspaces',
46
+ roles: ['owner', 'maintainer'],
47
+ description: 'Workspace lifecycle actions remain restricted to trusted roles.',
48
+ },
49
+ ];
50
+ const WORKFLOW_STATE_ORDER = [
51
+ 'todo',
52
+ 'in-progress',
53
+ 'review',
54
+ 'done',
55
+ ];
56
+ const DEFAULT_SWIMLANES = [
57
+ { id: 'expedite', name: 'Expedite' },
58
+ { id: 'standard', name: 'Standard' },
59
+ { id: 'blocked', name: 'Blocked' },
60
+ ];
61
+ const DEFAULT_BOARD_POLICY_HOOKS = [
62
+ {
63
+ id: 'dispatch-ready',
64
+ name: 'Dispatch readiness gate',
65
+ description: 'Only ready issues can move from todo into active work.',
66
+ scope: 'card',
67
+ blocking: true,
68
+ columnIds: ['in-progress'],
69
+ },
70
+ {
71
+ id: 'wip-limit',
72
+ name: 'WIP limit',
73
+ description: 'Active workflow columns enforce configured work-in-progress limits.',
74
+ scope: 'column',
75
+ blocking: true,
76
+ columnIds: ['in-progress', 'review'],
77
+ },
78
+ {
79
+ id: 'blocked-flow',
80
+ name: 'Blocked flow gate',
81
+ description: 'Blocked issues cannot advance into review or done.',
82
+ scope: 'card',
83
+ blocking: true,
84
+ columnIds: ['review', 'done'],
85
+ },
86
+ {
87
+ id: 'acceptance-complete',
88
+ name: 'Acceptance completion gate',
89
+ description: 'Issues can only move to done after every acceptance criterion is satisfied.',
90
+ scope: 'card',
91
+ blocking: true,
92
+ columnIds: ['done'],
93
+ },
94
+ {
95
+ id: 'repo-ci',
96
+ name: 'CI gate visibility',
97
+ description: 'Repository-scoped CI results should be visible on each work item.',
98
+ scope: 'card',
99
+ blocking: false,
100
+ },
101
+ {
102
+ id: 'merge-status',
103
+ name: 'Merge readiness',
104
+ description: 'PR review and merge readiness should be visible from the board.',
105
+ scope: 'card',
106
+ blocking: false,
107
+ },
108
+ {
109
+ id: 'publish-status',
110
+ name: 'Publish lifecycle',
111
+ description: 'Merged work should expose publish readiness and release state.',
112
+ scope: 'card',
113
+ blocking: false,
114
+ },
115
+ ];
116
+ function uniqueById(items) {
117
+ const seen = new Set();
118
+ const normalized = [];
119
+ for (const item of items) {
120
+ if (seen.has(item.id))
121
+ continue;
122
+ seen.add(item.id);
123
+ normalized.push(item);
124
+ }
125
+ return normalized;
126
+ }
127
+ function uniqueStrings(values) {
128
+ return Array.from(new Set(values));
129
+ }
130
+ export function normalizeKanbanTaskTagKey(value) {
131
+ return value
132
+ .trim()
133
+ .toLowerCase()
134
+ .replace(/[^a-z0-9]+/g, '_')
135
+ .replace(/^_+|_+$/g, '')
136
+ .replace(/_+/g, '_');
137
+ }
138
+ export function normalizeKanbanDispatchContextLabelKey(value) {
139
+ return normalizeKanbanTaskTagKey(value);
140
+ }
141
+ function compareKanbanDispatchContextLabels(left, right) {
142
+ return (left.order - right.order ||
143
+ left.label.localeCompare(right.label) ||
144
+ left.key.localeCompare(right.key) ||
145
+ left.id.localeCompare(right.id));
146
+ }
147
+ export function normalizeKanbanDispatchContextLabel(label) {
148
+ const normalizedLabel = label.label.trim();
149
+ const normalizedKey = normalizeKanbanDispatchContextLabelKey(label.key ?? normalizedLabel);
150
+ return {
151
+ ...label,
152
+ label: normalizedLabel,
153
+ key: normalizedKey || normalizeKanbanDispatchContextLabelKey(label.id),
154
+ instruction: label.instruction.replace(/\r\n?/g, '\n').trim(),
155
+ description: label.description?.trim() || undefined,
156
+ order: typeof label.order === 'number' && Number.isFinite(label.order)
157
+ ? Math.max(0, Math.floor(label.order))
158
+ : 0,
159
+ };
160
+ }
161
+ export function normalizeKanbanDispatchContextLabels(labels) {
162
+ return labels
163
+ .map((label) => normalizeKanbanDispatchContextLabel(label))
164
+ .sort(compareKanbanDispatchContextLabels);
165
+ }
166
+ export function normalizeKanbanDispatchContextLabelRefs(refs) {
167
+ return Array.from(new Set(refs
168
+ .map((ref) => ref.labelId?.trim() || '')
169
+ .filter(Boolean))).map((labelId) => ({ labelId }));
170
+ }
171
+ export function projectDispatchContextLabels(definitions, refs) {
172
+ const definitionMap = new Map(definitions.map((definition) => [definition.id, definition]));
173
+ return normalizeKanbanDispatchContextLabelRefs(refs)
174
+ .map((ref) => definitionMap.get(ref.labelId))
175
+ .filter((definition) => Boolean(definition))
176
+ .sort(compareKanbanDispatchContextLabels)
177
+ .map((definition) => ({
178
+ labelId: definition.id,
179
+ key: definition.key,
180
+ label: definition.label,
181
+ instruction: definition.instruction,
182
+ }));
183
+ }
184
+ export function renderDispatchContextLabels(definitions, refs) {
185
+ return projectDispatchContextLabels(definitions, refs)
186
+ .map((projection) => `- [${projection.key}] ${projection.instruction}`)
187
+ .join('\n');
188
+ }
189
+ export function renderKanbanExecutionContextBlock(input) {
190
+ const labelIds = Array.from(new Set(input.labelIds ?? []));
191
+ const labels = input.labels ?? [];
192
+ const projectSummary = input.projectKey?.trim()
193
+ ? input.projectName?.trim()
194
+ ? `${input.projectKey} (${input.projectName})`
195
+ : input.projectKey
196
+ : input.projectName?.trim() || input.projectId;
197
+ const labelSummary = labels.length > 0
198
+ ? labels.map((label) => `- ${label.key} (${label.labelId}): ${label.label}`).join('\n')
199
+ : labelIds.length > 0
200
+ ? labelIds.map((labelId) => `- ${labelId}`).join('\n')
201
+ : '- none';
202
+ const renderedContext = input.renderedContext?.trim() || '- none';
203
+ const runSummary = (input.runIds ?? []).length > 0 ? (input.runIds ?? []).join(', ') : 'none';
204
+ const sessionSummary = (input.sessionIds ?? []).length > 0 ? (input.sessionIds ?? []).join(', ') : 'none';
205
+ return [
206
+ 'Execution Context',
207
+ `Project: ${projectSummary}`,
208
+ `Issue: ${input.issueKey} (${input.issueId})`,
209
+ `Title: ${input.issueTitle}`,
210
+ `Applied Dispatch Context Labels (${labelIds.length}):`,
211
+ labelSummary,
212
+ `Run IDs: ${runSummary}`,
213
+ `Session IDs: ${sessionSummary}`,
214
+ `Last Dispatched At: ${input.lastDispatchedAt ?? 'none'}`,
215
+ 'Rendered Dispatch Context:',
216
+ renderedContext,
217
+ ].join('\n');
218
+ }
219
+ export function buildKanbanExecutionContextEnvelope(input) {
220
+ const labelIds = normalizeKanbanDispatchContextLabelRefs(input.issue.dispatch.contextLabels).map((ref) => ref.labelId);
221
+ const labels = input.issue.dispatch.contextLabelProjections ?? [];
222
+ const renderedContext = input.issue.dispatch.renderedContext?.trim() || undefined;
223
+ if (labelIds.length === 0 && labels.length === 0 && !renderedContext) {
224
+ return undefined;
225
+ }
226
+ return {
227
+ kind: 'dispatch-context-labels',
228
+ project: {
229
+ id: input.project?.id ?? input.issue.projectId,
230
+ key: input.project?.key,
231
+ name: input.project?.name,
232
+ },
233
+ issue: {
234
+ id: input.issue.id,
235
+ key: input.issue.key,
236
+ title: input.issue.title,
237
+ },
238
+ dispatch: {
239
+ runIds: Array.from(new Set(input.issue.dispatch.runIds ?? [])),
240
+ sessionIds: Array.from(new Set(input.issue.dispatch.sessionIds ?? [])),
241
+ labelIds,
242
+ labels,
243
+ renderedContext,
244
+ lastDispatchedAt: input.issue.dispatch.lastDispatchedAt,
245
+ },
246
+ block: renderKanbanExecutionContextBlock({
247
+ projectId: input.project?.id ?? input.issue.projectId,
248
+ projectKey: input.project?.key,
249
+ projectName: input.project?.name,
250
+ issueId: input.issue.id,
251
+ issueKey: input.issue.key,
252
+ issueTitle: input.issue.title,
253
+ runIds: input.issue.dispatch.runIds,
254
+ sessionIds: input.issue.dispatch.sessionIds,
255
+ labelIds,
256
+ labels,
257
+ renderedContext,
258
+ lastDispatchedAt: input.issue.dispatch.lastDispatchedAt,
259
+ }),
260
+ };
261
+ }
262
+ export function buildDispatchContextExecutionEnvelope(definitions, refs) {
263
+ const appliedLabels = projectDispatchContextLabels(definitions, refs);
264
+ if (appliedLabels.length === 0) {
265
+ return undefined;
266
+ }
267
+ return {
268
+ source: 'dispatch-context-labels',
269
+ appliedLabels,
270
+ renderedBlock: renderDispatchContextLabels(definitions, refs),
271
+ metadata: {
272
+ labelIds: appliedLabels.map((projection) => projection.labelId),
273
+ labelKeys: appliedLabels.map((projection) => projection.key),
274
+ labelCount: appliedLabels.length,
275
+ },
276
+ };
277
+ }
278
+ function findKanbanExecutionContextEnvelopes(snapshot, matcher) {
279
+ const projectsById = new Map(snapshot.projects.map((project) => [project.id, project]));
280
+ return snapshot.issues
281
+ .filter(matcher)
282
+ .map((issue) => buildKanbanExecutionContextEnvelope({ issue, project: projectsById.get(issue.projectId) }))
283
+ .filter((envelope) => Boolean(envelope))
284
+ .sort((left, right) => left.issue.key.localeCompare(right.issue.key) || left.issue.id.localeCompare(right.issue.id));
285
+ }
286
+ export function findKanbanExecutionContextEnvelopesForRun(snapshot, runId) {
287
+ const normalizedRunId = runId.trim();
288
+ if (!normalizedRunId) {
289
+ return [];
290
+ }
291
+ return findKanbanExecutionContextEnvelopes(snapshot, (issue) => issue.dispatch.runIds.includes(normalizedRunId));
292
+ }
293
+ export function findKanbanExecutionContextEnvelopesForSession(snapshot, sessionId) {
294
+ const normalizedSessionId = sessionId.trim();
295
+ if (!normalizedSessionId) {
296
+ return [];
297
+ }
298
+ return findKanbanExecutionContextEnvelopes(snapshot, (issue) => issue.dispatch.sessionIds.includes(normalizedSessionId));
299
+ }
300
+ function normalizeKanbanTaskTagScope(scope) {
301
+ if (!scope)
302
+ return undefined;
303
+ const kind = scope.kind === 'project' || scope.kind === 'workspace' ? scope.kind : 'global';
304
+ const refId = scope.refId?.trim() || undefined;
305
+ return refId ? { kind, refId } : { kind };
306
+ }
307
+ function normalizeKanbanTaskTagContent(content) {
308
+ return content.replace(/\r\n?/g, '\n').trim();
309
+ }
310
+ function compareKanbanTaskTags(left, right) {
311
+ return (left.order - right.order ||
312
+ left.label.localeCompare(right.label) ||
313
+ left.key.localeCompare(right.key) ||
314
+ left.id.localeCompare(right.id));
315
+ }
316
+ export function normalizeKanbanTaskTag(taskTag) {
317
+ const label = taskTag.label.trim();
318
+ const normalizedKey = normalizeKanbanTaskTagKey(taskTag.key ?? label);
319
+ return {
320
+ ...taskTag,
321
+ label,
322
+ key: normalizedKey || normalizeKanbanTaskTagKey(taskTag.id),
323
+ content: normalizeKanbanTaskTagContent(taskTag.content),
324
+ description: taskTag.description?.trim() || undefined,
325
+ order: typeof taskTag.order === 'number' && Number.isFinite(taskTag.order) ? Math.max(0, Math.floor(taskTag.order)) : 0,
326
+ scope: normalizeKanbanTaskTagScope(taskTag.scope),
327
+ };
328
+ }
329
+ export function normalizeKanbanTaskTags(taskTags) {
330
+ return taskTags.map((taskTag) => normalizeKanbanTaskTag(taskTag)).sort(compareKanbanTaskTags);
331
+ }
332
+ function normalizeCollaboratorRole(role) {
333
+ switch (role) {
334
+ case 'owner':
335
+ case 'maintainer':
336
+ case 'contributor':
337
+ case 'viewer':
338
+ return role;
339
+ default:
340
+ return 'contributor';
341
+ }
342
+ }
343
+ function normalizeCollaborator(collaborator) {
344
+ return {
345
+ ...collaborator,
346
+ displayName: collaborator.displayName.trim(),
347
+ email: collaborator.email?.trim() || undefined,
348
+ avatarUrl: collaborator.avatarUrl?.trim() || undefined,
349
+ role: normalizeCollaboratorRole(collaborator.role),
350
+ };
351
+ }
352
+ function normalizeTeamSettings(settings) {
353
+ return {
354
+ visibility: settings?.visibility === 'private' || settings?.visibility === 'workspace-shared'
355
+ ? settings.visibility
356
+ : DEFAULT_TEAM_SETTINGS.visibility,
357
+ defaultRole: normalizeCollaboratorRole(settings?.defaultRole ?? DEFAULT_TEAM_SETTINGS.defaultRole),
358
+ allowSelfAssign: settings?.allowSelfAssign ?? DEFAULT_TEAM_SETTINGS.allowSelfAssign,
359
+ };
360
+ }
361
+ function normalizeProjectSettings(settings) {
362
+ return {
363
+ reviewRequiredForDone: settings?.reviewRequiredForDone ?? DEFAULT_PROJECT_SETTINGS.reviewRequiredForDone,
364
+ activityScope: settings?.activityScope === 'all-board-entities'
365
+ ? 'all-board-entities'
366
+ : DEFAULT_PROJECT_SETTINGS.activityScope,
367
+ workspaceProvisioning: settings?.workspaceProvisioning === 'contributors-and-up'
368
+ ? 'contributors-and-up'
369
+ : DEFAULT_PROJECT_SETTINGS.workspaceProvisioning,
370
+ };
371
+ }
372
+ function normalizePermissionGrants(permissions) {
373
+ const seen = new Set();
374
+ const normalized = [];
375
+ for (const permission of permissions?.length ? permissions : DEFAULT_PERMISSION_GRANTS) {
376
+ if (seen.has(permission.action)) {
377
+ continue;
378
+ }
379
+ seen.add(permission.action);
380
+ normalized.push({
381
+ action: permission.action,
382
+ roles: uniqueStrings(permission.roles.map((role) => normalizeCollaboratorRole(role))),
383
+ description: permission.description?.trim() || undefined,
384
+ });
385
+ }
386
+ return normalized;
387
+ }
388
+ function normalizeActivityEntry(entry) {
389
+ return {
390
+ ...entry,
391
+ action: entry.action.trim(),
392
+ summary: entry.summary.trim(),
393
+ actor: {
394
+ ...entry.actor,
395
+ displayName: entry.actor.displayName.trim(),
396
+ role: entry.actor.role ? normalizeCollaboratorRole(entry.actor.role) : undefined,
397
+ },
398
+ };
399
+ }
400
+ function normalizeTeam(team, project) {
401
+ return {
402
+ id: team?.id?.trim() || `team-${project.id}`,
403
+ name: team?.name?.trim() || `${project.name} Team`,
404
+ members: uniqueById((team?.members ?? []).map(normalizeCollaborator)),
405
+ settings: normalizeTeamSettings(team?.settings),
406
+ };
407
+ }
408
+ function normalizeRepositorySettings(settings, defaultBranch) {
409
+ return {
410
+ baseBranch: settings?.baseBranch?.trim() || defaultBranch,
411
+ autoMerge: settings?.autoMerge ?? false,
412
+ requiredApprovals: typeof settings?.requiredApprovals === 'number' && settings.requiredApprovals >= 0
413
+ ? Math.floor(settings.requiredApprovals)
414
+ : 1,
415
+ ciProvider: settings?.ciProvider?.trim() || undefined,
416
+ publishTarget: settings?.publishTarget?.trim() || undefined,
417
+ };
418
+ }
419
+ function normalizeRepositoryContext(repository) {
420
+ const owner = repository.owner.trim();
421
+ const name = repository.name.trim();
422
+ const fullName = repository.fullName?.trim() || `${owner}/${name}`;
423
+ const defaultBranch = repository.defaultBranch?.trim() || 'main';
424
+ return {
425
+ ...repository,
426
+ owner,
427
+ name,
428
+ fullName,
429
+ defaultBranch,
430
+ url: repository.url?.trim() || undefined,
431
+ settings: normalizeRepositorySettings(repository.settings, defaultBranch),
432
+ };
433
+ }
434
+ function normalizeIntegrationPrerequisites(prerequisites) {
435
+ const seen = new Set();
436
+ const normalized = [];
437
+ for (const prerequisite of prerequisites ?? []) {
438
+ const key = prerequisite.key.trim();
439
+ if (!key || seen.has(key)) {
440
+ continue;
441
+ }
442
+ seen.add(key);
443
+ normalized.push({
444
+ ...prerequisite,
445
+ key,
446
+ label: prerequisite.label.trim(),
447
+ guidance: prerequisite.guidance?.trim() || undefined,
448
+ });
449
+ }
450
+ return normalized;
451
+ }
452
+ function normalizeIntegrationActionState(actions, status) {
453
+ const blockedByStatus = status === 'disconnected' ||
454
+ status === 'expired-auth' ||
455
+ status === 'missing-scopes' ||
456
+ status === 'failing';
457
+ const canCreatePullRequest = actions?.canCreatePullRequest ?? !blockedByStatus;
458
+ const canManagePullRequest = actions?.canManagePullRequest ?? !blockedByStatus;
459
+ const canApproveFromReview = actions?.canApproveFromReview ?? !blockedByStatus;
460
+ return {
461
+ canCreatePullRequest,
462
+ canManagePullRequest,
463
+ canApproveFromReview,
464
+ reason: actions?.reason?.trim() || undefined,
465
+ };
466
+ }
467
+ function normalizeIntegrationConnection(connection) {
468
+ return {
469
+ ...connection,
470
+ label: connection.label.trim(),
471
+ accountLabel: connection.accountLabel?.trim() || undefined,
472
+ connectedAt: connection.connectedAt,
473
+ failureMessage: connection.failureMessage?.trim() || undefined,
474
+ missingScopes: uniqueStrings((connection.missingScopes ?? []).map((scope) => scope.trim()).filter(Boolean)),
475
+ prerequisites: normalizeIntegrationPrerequisites(connection.prerequisites),
476
+ guidance: connection.guidance.trim(),
477
+ actions: normalizeIntegrationActionState(connection.actions, connection.status),
478
+ };
479
+ }
480
+ function normalizeRepositoryIntegrationState(integration, provider, pullRequest) {
481
+ if (!integration || (integration.provider !== 'github' && integration.provider !== 'azure-repos')) {
482
+ return undefined;
483
+ }
484
+ const normalizedLinkState = integration.linkState ??
485
+ (pullRequest ? pullRequest.linkState ?? 'linked' : 'unlinked');
486
+ return {
487
+ ...integration,
488
+ provider: integration.provider,
489
+ status: integration.status,
490
+ linkState: normalizedLinkState,
491
+ failureMessage: integration.failureMessage?.trim() || undefined,
492
+ guidance: integration.guidance.trim(),
493
+ missingScopes: uniqueStrings((integration.missingScopes ?? []).map((scope) => scope.trim()).filter(Boolean)),
494
+ prerequisites: normalizeIntegrationPrerequisites(integration.prerequisites),
495
+ actions: normalizeIntegrationActionState(integration.actions, integration.status),
496
+ };
497
+ }
498
+ function resolveReviewStatus(pullRequest) {
499
+ if (!pullRequest) {
500
+ return 'unlinked';
501
+ }
502
+ if (pullRequest.reviewLinks.some((review) => review.status === 'changes-requested')) {
503
+ return 'changes-requested';
504
+ }
505
+ if (pullRequest.reviewLinks.length > 0 && pullRequest.reviewLinks.every((review) => review.status === 'approved')) {
506
+ return 'approved';
507
+ }
508
+ if (pullRequest.reviewLinks.length > 0 || pullRequest.status === 'in-review') {
509
+ return 'pending';
510
+ }
511
+ if (pullRequest.status === 'approved') {
512
+ return 'approved';
513
+ }
514
+ if (pullRequest.status === 'changes-requested') {
515
+ return 'changes-requested';
516
+ }
517
+ return 'pending';
518
+ }
519
+ function resolveMergeStatus(lifecycle) {
520
+ if (!lifecycle.pullRequest) {
521
+ return 'not-ready';
522
+ }
523
+ if (lifecycle.pullRequest.status === 'merged') {
524
+ return 'merged';
525
+ }
526
+ const blockingGate = lifecycle.ciGates.some((gate) => gate.required && gate.status !== 'passing' && gate.status !== 'skipped');
527
+ if (blockingGate) {
528
+ return 'blocked';
529
+ }
530
+ return lifecycle.reviewStatus === 'approved' ? 'ready' : 'blocked';
531
+ }
532
+ function resolvePublishStatus(lifecycle) {
533
+ if (lifecycle.publishStatus === 'published' || lifecycle.lastPublishedAt) {
534
+ return 'published';
535
+ }
536
+ if (lifecycle.publishStatus === 'failed') {
537
+ return 'failed';
538
+ }
539
+ if (lifecycle.publishStatus === 'pending') {
540
+ return 'pending';
541
+ }
542
+ if (lifecycle.mergeStatus === 'merged') {
543
+ return 'ready';
544
+ }
545
+ return 'not-ready';
546
+ }
547
+ function normalizeIssueRepositoryLifecycle(lifecycle, repositoryMap) {
548
+ if (!lifecycle) {
549
+ return undefined;
550
+ }
551
+ const repository = repositoryMap.get(lifecycle.repositoryId);
552
+ const branchName = lifecycle.branchName?.trim() || repository?.defaultBranch || 'main';
553
+ const ciGates = uniqueById(lifecycle.ciGates.map((gate) => ({
554
+ ...gate,
555
+ name: gate.name.trim(),
556
+ provider: gate.provider?.trim() || repository?.settings.ciProvider,
557
+ summary: gate.summary?.trim() || undefined,
558
+ url: gate.url?.trim() || undefined,
559
+ })));
560
+ const pullRequest = lifecycle.pullRequest
561
+ ? {
562
+ ...lifecycle.pullRequest,
563
+ title: lifecycle.pullRequest.title.trim(),
564
+ branchName: lifecycle.pullRequest.branchName?.trim() || branchName,
565
+ baseBranch: lifecycle.pullRequest.baseBranch?.trim() ||
566
+ repository?.settings.baseBranch ||
567
+ repository?.defaultBranch ||
568
+ 'main',
569
+ reviewLinks: uniqueById(lifecycle.pullRequest.reviewLinks.map((reviewLink) => ({
570
+ ...reviewLink,
571
+ label: reviewLink.label.trim(),
572
+ reviewer: reviewLink.reviewer?.trim() || undefined,
573
+ url: reviewLink.url?.trim() || undefined,
574
+ }))),
575
+ url: lifecycle.pullRequest.url?.trim() || undefined,
576
+ linkState: lifecycle.pullRequest.linkState ?? 'linked',
577
+ }
578
+ : undefined;
579
+ const reviewStatus = resolveReviewStatus(pullRequest);
580
+ const mergeStatus = resolveMergeStatus({
581
+ ciGates,
582
+ pullRequest,
583
+ reviewStatus,
584
+ });
585
+ const publishStatus = resolvePublishStatus({
586
+ mergeStatus,
587
+ publishStatus: lifecycle.publishStatus,
588
+ lastPublishedAt: lifecycle.lastPublishedAt,
589
+ });
590
+ return {
591
+ ...lifecycle,
592
+ branchName,
593
+ ciGates,
594
+ integration: normalizeRepositoryIntegrationState(lifecycle.integration, repository?.provider, pullRequest),
595
+ pullRequest: pullRequest
596
+ ? {
597
+ ...pullRequest,
598
+ mergeStatus,
599
+ status: mergeStatus === 'merged'
600
+ ? 'merged'
601
+ : reviewStatus === 'approved'
602
+ ? 'approved'
603
+ : reviewStatus === 'changes-requested'
604
+ ? 'changes-requested'
605
+ : pullRequest.status,
606
+ }
607
+ : undefined,
608
+ reviewStatus,
609
+ mergeStatus,
610
+ publishStatus,
611
+ publishUrl: lifecycle.publishUrl?.trim() || undefined,
612
+ lastPublishedAt: lifecycle.lastPublishedAt,
613
+ };
614
+ }
615
+ function getColumnName(state) {
616
+ switch (state) {
617
+ case 'todo':
618
+ return 'Todo';
619
+ case 'in-progress':
620
+ return 'In Progress';
621
+ case 'review':
622
+ return 'Review';
623
+ case 'done':
624
+ return 'Done';
625
+ }
626
+ }
627
+ function getColumnWipLimit(project, state) {
628
+ if (state === 'todo' || state === 'done') {
629
+ return undefined;
630
+ }
631
+ return project.statuses.find((status) => status.id === state)?.wipLimit;
632
+ }
633
+ function getAllowedMoveStates(currentState) {
634
+ switch (currentState) {
635
+ case 'todo':
636
+ return ['in-progress'];
637
+ case 'in-progress':
638
+ return ['todo', 'review'];
639
+ case 'review':
640
+ return ['in-progress', 'done'];
641
+ case 'done':
642
+ return ['review'];
643
+ }
644
+ }
645
+ function acceptanceProgress(issue) {
646
+ return {
647
+ satisfied: issue.acceptanceCriteria.filter((criterion) => criterion.satisfied).length,
648
+ total: issue.acceptanceCriteria.length,
649
+ };
650
+ }
651
+ function isBlockedIssue(issue) {
652
+ return issue.status === 'blocked' || issue.dispatch.readiness === 'blocked';
653
+ }
654
+ function createPolicySignal(hookId, message, blocking = true) {
655
+ return {
656
+ hookId,
657
+ message,
658
+ blocking,
659
+ severity: blocking ? 'error' : 'warning',
660
+ };
661
+ }
662
+ export function summarizeKanbanReviewArtifact(artifact) {
663
+ return {
664
+ decision: artifact.decision,
665
+ queueState: artifact.queueState,
666
+ commentCount: artifact.comments.length,
667
+ openCommentCount: artifact.comments.filter((comment) => comment.status === 'open').length,
668
+ latestActivityAt: artifact.updatedAt,
669
+ };
670
+ }
671
+ function resolveReadiness(issue, issuesById) {
672
+ if (issue.status === 'done')
673
+ return 'completed';
674
+ if ((issue.dispatch?.runIds?.length ?? 0) > 0 || (issue.dispatch?.sessionIds?.length ?? 0) > 0) {
675
+ return 'dispatched';
676
+ }
677
+ if (issue.status === 'blocked')
678
+ return 'blocked';
679
+ const unresolvedDependencies = issue.dependencies.some((dependency) => {
680
+ if (dependency.type !== 'blocked-by')
681
+ return false;
682
+ return issuesById.get(dependency.issueId)?.status !== 'done';
683
+ });
684
+ if (unresolvedDependencies)
685
+ return 'blocked';
686
+ const unresolvedChildren = issue.childIssueIds.some((childIssueId) => {
687
+ return issuesById.get(childIssueId)?.status !== 'done';
688
+ });
689
+ if (unresolvedChildren)
690
+ return 'needs-decomposition';
691
+ const incompleteDecomposition = issue.decomposition.some((item) => item.status !== 'done' && !item.issueId);
692
+ if (incompleteDecomposition)
693
+ return 'needs-decomposition';
694
+ return 'ready';
695
+ }
696
+ function resolveBlockedReasons(issue, issuesById) {
697
+ const reasons = new Set(issue.dispatch?.blockedReasons ?? []);
698
+ for (const dependency of issue.dependencies) {
699
+ if (dependency.type !== 'blocked-by')
700
+ continue;
701
+ const dependencyIssue = issuesById.get(dependency.issueId);
702
+ if (!dependencyIssue || dependencyIssue.status !== 'done') {
703
+ reasons.add(`waiting on ${dependency.issueId}`);
704
+ }
705
+ }
706
+ if (issue.childIssueIds.some((childIssueId) => issuesById.get(childIssueId)?.status !== 'done')) {
707
+ reasons.add('child issues still open');
708
+ }
709
+ if (issue.decomposition.some((item) => item.status !== 'done' && !item.issueId)) {
710
+ reasons.add('decomposition incomplete');
711
+ }
712
+ return Array.from(reasons);
713
+ }
714
+ export function normalizeKanbanIssue(issue, issuesById, repositoryMap = new Map(), dispatchContextLabels = []) {
715
+ const labels = uniqueById(issue.labels);
716
+ const assignees = uniqueById(issue.assignees);
717
+ const collaborators = uniqueById((issue.collaborators ?? []).map(normalizeCollaborator));
718
+ const acceptanceCriteria = uniqueById(issue.acceptanceCriteria);
719
+ const decomposition = uniqueById(issue.decomposition);
720
+ const childIssueIds = Array.from(new Set(issue.childIssueIds));
721
+ const readiness = resolveReadiness({ ...issue, childIssueIds, decomposition }, issuesById);
722
+ const blockedReasons = resolveBlockedReasons({ ...issue, childIssueIds, decomposition }, issuesById);
723
+ const contextLabels = normalizeKanbanDispatchContextLabelRefs(issue.dispatch?.contextLabels ?? []);
724
+ const contextLabelProjections = projectDispatchContextLabels(dispatchContextLabels, contextLabels);
725
+ const executionContext = buildDispatchContextExecutionEnvelope(dispatchContextLabels, contextLabels);
726
+ const renderedContext = executionContext?.renderedBlock;
727
+ const workspaceLinks = Array.from(new Map((issue.workspaceLinks ?? [])
728
+ .filter((link) => typeof link.workspacePath === 'string' && link.workspacePath.trim().length > 0)
729
+ .map((link) => [
730
+ link.workspacePath.trim(),
731
+ {
732
+ workspacePath: link.workspacePath.trim(),
733
+ workspaceName: link.workspaceName.trim() || link.workspacePath.trim(),
734
+ branchName: link.branchName?.trim() || undefined,
735
+ linkedAt: link.linkedAt,
736
+ source: link.source,
737
+ },
738
+ ])).values());
739
+ return {
740
+ ...issue,
741
+ labels,
742
+ assignees,
743
+ collaborators,
744
+ acceptanceCriteria,
745
+ decomposition,
746
+ childIssueIds,
747
+ activity: uniqueById((issue.activity ?? []).map(normalizeActivityEntry)),
748
+ dispatch: {
749
+ readiness,
750
+ blockedReasons,
751
+ runIds: Array.from(new Set(issue.dispatch?.runIds ?? [])),
752
+ sessionIds: Array.from(new Set(issue.dispatch?.sessionIds ?? [])),
753
+ contextLabels,
754
+ contextLabelProjections,
755
+ executionContext,
756
+ renderedContext,
757
+ lastDispatchedAt: issue.dispatch?.lastDispatchedAt,
758
+ },
759
+ repositoryLifecycle: normalizeIssueRepositoryLifecycle(issue.repositoryLifecycle, repositoryMap),
760
+ workspaceLinks,
761
+ };
762
+ }
763
+ export function resolveKanbanWorkflowState(issue) {
764
+ switch (issue.status) {
765
+ case 'in-progress':
766
+ case 'blocked':
767
+ return 'in-progress';
768
+ case 'review':
769
+ return 'review';
770
+ case 'done':
771
+ return 'done';
772
+ case 'backlog':
773
+ case 'ready':
774
+ default:
775
+ return 'todo';
776
+ }
777
+ }
778
+ export function resolveKanbanSwimlane(issue) {
779
+ if (issue.status === 'blocked' || issue.dispatch.readiness === 'blocked') {
780
+ return 'blocked';
781
+ }
782
+ if (issue.priority === 'critical' || issue.priority === 'high') {
783
+ return 'expedite';
784
+ }
785
+ return 'standard';
786
+ }
787
+ export function resolveKanbanStatusForWorkflowState(issue, state) {
788
+ switch (state) {
789
+ case 'todo':
790
+ return issue.status === 'backlog' || issue.dispatch.readiness === 'needs-decomposition'
791
+ ? 'backlog'
792
+ : 'ready';
793
+ case 'in-progress':
794
+ return 'in-progress';
795
+ case 'review':
796
+ return 'review';
797
+ case 'done':
798
+ return 'done';
799
+ }
800
+ }
801
+ export function evaluateKanbanIssueMove(input) {
802
+ const issue = input.issues.find((candidate) => candidate.id === input.issueId);
803
+ if (!issue) {
804
+ return {
805
+ issueId: input.issueId,
806
+ fromState: 'todo',
807
+ toState: input.toState,
808
+ allowed: false,
809
+ signals: [createPolicySignal('dispatch-ready', `Issue ${input.issueId} does not exist.`)],
810
+ };
811
+ }
812
+ const fromState = resolveKanbanWorkflowState(issue);
813
+ const allowedStates = getAllowedMoveStates(fromState);
814
+ const signals = [];
815
+ if (!allowedStates.includes(input.toState)) {
816
+ signals.push(createPolicySignal('dispatch-ready', `Cannot move ${issue.key} from ${fromState} to ${input.toState}.`));
817
+ }
818
+ if (input.toState === 'in-progress') {
819
+ if (issue.dispatch.readiness !== 'ready' && issue.dispatch.readiness !== 'dispatched') {
820
+ signals.push(createPolicySignal('dispatch-ready', `${issue.key} is ${issue.dispatch.readiness} and cannot start active work yet.`));
821
+ }
822
+ }
823
+ if ((input.toState === 'review' || input.toState === 'done') && isBlockedIssue(issue)) {
824
+ signals.push(createPolicySignal('blocked-flow', `${issue.key} is blocked and cannot advance until the blocking reasons clear.`));
825
+ }
826
+ if (input.toState === 'done') {
827
+ const progress = acceptanceProgress(issue);
828
+ if (progress.total > 0 && progress.satisfied < progress.total) {
829
+ signals.push(createPolicySignal('acceptance-complete', `${issue.key} has ${progress.total - progress.satisfied} acceptance checks remaining.`));
830
+ }
831
+ }
832
+ const wipLimit = getColumnWipLimit(input.project, input.toState);
833
+ if (typeof wipLimit === 'number') {
834
+ const targetCount = input.issues.filter((candidate) => candidate.projectId === input.project.id &&
835
+ candidate.id !== issue.id &&
836
+ resolveKanbanWorkflowState(candidate) === input.toState).length;
837
+ if (targetCount + 1 > wipLimit) {
838
+ signals.push(createPolicySignal('wip-limit', `${getColumnName(input.toState)} would exceed its WIP limit of ${wipLimit}.`));
839
+ }
840
+ }
841
+ return {
842
+ issueId: issue.id,
843
+ fromState,
844
+ toState: input.toState,
845
+ allowed: signals.every((signal) => !signal.blocking),
846
+ nextStatus: signals.every((signal) => !signal.blocking)
847
+ ? resolveKanbanStatusForWorkflowState(issue, input.toState)
848
+ : undefined,
849
+ signals,
850
+ };
851
+ }
852
+ export function computeKanbanProjectMetrics(issues) {
853
+ let readyIssues = 0;
854
+ let blockedIssues = 0;
855
+ let dispatchedIssues = 0;
856
+ let completedIssues = 0;
857
+ let needsDecompositionIssues = 0;
858
+ let inProgressIssues = 0;
859
+ for (const issue of issues) {
860
+ if (issue.status === 'in-progress' || issue.status === 'review') {
861
+ inProgressIssues += 1;
862
+ }
863
+ switch (issue.dispatch.readiness) {
864
+ case 'ready':
865
+ readyIssues += 1;
866
+ break;
867
+ case 'blocked':
868
+ blockedIssues += 1;
869
+ break;
870
+ case 'dispatched':
871
+ dispatchedIssues += 1;
872
+ break;
873
+ case 'completed':
874
+ completedIssues += 1;
875
+ break;
876
+ case 'needs-decomposition':
877
+ needsDecompositionIssues += 1;
878
+ break;
879
+ }
880
+ }
881
+ return {
882
+ totalIssues: issues.length,
883
+ readyIssues,
884
+ blockedIssues,
885
+ dispatchedIssues,
886
+ completedIssues,
887
+ needsDecompositionIssues,
888
+ inProgressIssues,
889
+ };
890
+ }
891
+ export function buildKanbanBacklogSnapshot(input) {
892
+ const normalizedDispatchContextLabels = normalizeKanbanDispatchContextLabels(input.dispatchContextLabels ?? []);
893
+ const issueSeedMap = new Map();
894
+ for (const issue of input.issues) {
895
+ issueSeedMap.set(issue.id, {
896
+ ...issue,
897
+ collaborators: issue.collaborators ?? [],
898
+ activity: issue.activity ?? [],
899
+ dispatch: {
900
+ readiness: 'ready',
901
+ blockedReasons: [],
902
+ runIds: [],
903
+ sessionIds: [],
904
+ contextLabels: [],
905
+ contextLabelProjections: [],
906
+ },
907
+ });
908
+ }
909
+ const projects = input.projects.map((project) => ({
910
+ ...project,
911
+ labels: uniqueById(project.labels),
912
+ assignees: uniqueById(project.assignees),
913
+ team: normalizeTeam(project.team, project),
914
+ settings: normalizeProjectSettings(project.settings),
915
+ permissions: normalizePermissionGrants(project.permissions),
916
+ activity: uniqueById((project.activity ?? []).map(normalizeActivityEntry)),
917
+ statuses: project.statuses.length > 0 ? project.statuses : DEFAULT_PROJECT_STATUSES,
918
+ repositories: uniqueById((project.repositories ?? []).map(normalizeRepositoryContext)),
919
+ integrations: Array.from(new Map((project.integrations ?? [])
920
+ .map(normalizeIntegrationConnection)
921
+ .map((connection) => [connection.provider, connection])).values()),
922
+ }));
923
+ const repositoryMapByProject = new Map(projects.map((project) => [project.id, new Map(project.repositories.map((repository) => [repository.id, repository]))]));
924
+ const normalizedIssues = input.issues.map((issue) => normalizeKanbanIssue(issue, issueSeedMap, repositoryMapByProject.get(issue.projectId), normalizedDispatchContextLabels));
925
+ const normalizedIssueMap = new Map(normalizedIssues.map((issue) => [issue.id, issue]));
926
+ const projectsWithMetrics = projects.map((project) => {
927
+ const statuses = project.statuses.length > 0 ? project.statuses : DEFAULT_PROJECT_STATUSES;
928
+ const issueIds = Array.from(new Set([
929
+ ...project.issueIds,
930
+ ...normalizedIssues
931
+ .filter((issue) => issue.projectId === project.id)
932
+ .map((issue) => issue.id),
933
+ ]));
934
+ const projectIssues = issueIds
935
+ .map((issueId) => normalizedIssueMap.get(issueId))
936
+ .filter((issue) => Boolean(issue));
937
+ return {
938
+ ...project,
939
+ statuses,
940
+ issueIds,
941
+ metrics: computeKanbanProjectMetrics(projectIssues),
942
+ };
943
+ });
944
+ return {
945
+ generatedAt: input.generatedAt ?? new Date().toISOString(),
946
+ projects: projectsWithMetrics,
947
+ issues: normalizedIssues,
948
+ dispatchContextLabels: normalizedDispatchContextLabels,
949
+ };
950
+ }
951
+ export function buildKanbanProjectBoard(input) {
952
+ const projectIssues = input.issues.filter((issue) => issue.projectId === input.project.id);
953
+ const repositoryMap = new Map(input.project.repositories.map((repository) => [repository.id, repository]));
954
+ const cards = projectIssues.map((issue) => {
955
+ const workflowState = resolveKanbanWorkflowState(issue);
956
+ const swimlaneId = resolveKanbanSwimlane(issue);
957
+ const signals = [];
958
+ const progress = acceptanceProgress(issue);
959
+ const repository = issue.repositoryLifecycle
960
+ ? repositoryMap.get(issue.repositoryLifecycle.repositoryId)
961
+ : undefined;
962
+ if (isBlockedIssue(issue)) {
963
+ for (const reason of issue.dispatch.blockedReasons) {
964
+ signals.push(createPolicySignal('blocked-flow', reason, false));
965
+ }
966
+ }
967
+ if (issue.dispatch.readiness === 'needs-decomposition') {
968
+ signals.push(createPolicySignal('dispatch-ready', `${issue.key} still needs decomposition before it can be dispatched.`, false));
969
+ }
970
+ const currentLimit = getColumnWipLimit(input.project, workflowState);
971
+ if (typeof currentLimit === 'number') {
972
+ const currentCount = projectIssues.filter((candidate) => resolveKanbanWorkflowState(candidate) === workflowState).length;
973
+ if (currentCount > currentLimit) {
974
+ signals.push(createPolicySignal('wip-limit', `${getColumnName(workflowState)} is over its WIP limit (${currentCount}/${currentLimit}).`, false));
975
+ }
976
+ }
977
+ if (issue.repositoryLifecycle?.ciGates.some((gate) => gate.required && gate.status === 'failing')) {
978
+ signals.push(createPolicySignal('repo-ci', `${issue.key} has failing required CI gates.`, false));
979
+ }
980
+ if (issue.repositoryLifecycle?.pullRequest && issue.repositoryLifecycle.mergeStatus === 'ready') {
981
+ signals.push(createPolicySignal('merge-status', `${issue.key} is approved and ready to merge.`, false));
982
+ }
983
+ if (issue.repositoryLifecycle?.publishStatus === 'published') {
984
+ signals.push(createPolicySignal('publish-status', `${issue.key} has already been published.`, false));
985
+ }
986
+ return {
987
+ issueId: issue.id,
988
+ issueKey: issue.key,
989
+ projectId: issue.projectId,
990
+ title: issue.title,
991
+ summary: issue.summary,
992
+ workflowState,
993
+ swimlaneId,
994
+ priority: issue.priority,
995
+ readiness: issue.dispatch.readiness,
996
+ blocked: isBlockedIssue(issue),
997
+ blockedReasons: issue.dispatch.blockedReasons,
998
+ labelNames: uniqueStrings(issue.labels.map((label) => label.name)),
999
+ assigneeNames: uniqueStrings(issue.assignees.map((assignee) => assignee.displayName)),
1000
+ collaboratorNames: uniqueStrings(issue.collaborators.map((collaborator) => collaborator.displayName)),
1001
+ dependencyCount: issue.dependencies.length,
1002
+ childCount: issue.childIssueIds.length,
1003
+ activityCount: issue.activity.length,
1004
+ latestActivityAt: issue.activity[0]?.createdAt,
1005
+ acceptanceProgress: progress,
1006
+ repository,
1007
+ repositoryLifecycle: issue.repositoryLifecycle,
1008
+ moveTargets: getAllowedMoveStates(workflowState).map((state) => {
1009
+ const evaluation = evaluateKanbanIssueMove({
1010
+ project: input.project,
1011
+ issues: projectIssues,
1012
+ issueId: issue.id,
1013
+ toState: state,
1014
+ });
1015
+ return {
1016
+ state,
1017
+ allowed: evaluation.allowed,
1018
+ signals: evaluation.signals,
1019
+ };
1020
+ }),
1021
+ policySignals: signals,
1022
+ review: issue.review,
1023
+ };
1024
+ });
1025
+ const columns = WORKFLOW_STATE_ORDER.map((state) => {
1026
+ const issueIds = cards
1027
+ .filter((card) => card.workflowState === state)
1028
+ .map((card) => card.issueId);
1029
+ const wipLimit = getColumnWipLimit(input.project, state);
1030
+ return {
1031
+ id: state,
1032
+ name: getColumnName(state),
1033
+ issueIds,
1034
+ issueCount: issueIds.length,
1035
+ wipLimit,
1036
+ isOverLimit: typeof wipLimit === 'number' ? issueIds.length > wipLimit : false,
1037
+ };
1038
+ });
1039
+ const swimlanes = DEFAULT_SWIMLANES.map((swimlane) => ({
1040
+ ...swimlane,
1041
+ issueIds: cards
1042
+ .filter((card) => card.swimlaneId === swimlane.id)
1043
+ .map((card) => card.issueId),
1044
+ }));
1045
+ return {
1046
+ projectId: input.project.id,
1047
+ projectKey: input.project.key,
1048
+ projectName: input.project.name,
1049
+ generatedAt: input.generatedAt ?? new Date().toISOString(),
1050
+ columns,
1051
+ swimlanes,
1052
+ cards,
1053
+ policyHooks: DEFAULT_BOARD_POLICY_HOOKS,
1054
+ };
1055
+ }
1056
+ export function buildKanbanBoardSnapshot(snapshot) {
1057
+ return {
1058
+ generatedAt: snapshot.generatedAt,
1059
+ projects: snapshot.projects.map((project) => buildKanbanProjectBoard({
1060
+ generatedAt: snapshot.generatedAt,
1061
+ project,
1062
+ issues: snapshot.issues,
1063
+ })),
1064
+ };
1065
+ }
1066
+ export function upsertKanbanProjectRepository(project, repository) {
1067
+ const repositories = uniqueById([
1068
+ ...(project.repositories ?? []).filter((candidate) => candidate.id !== repository.id),
1069
+ normalizeRepositoryContext(repository),
1070
+ ]);
1071
+ return {
1072
+ ...project,
1073
+ repositories,
1074
+ };
1075
+ }
1076
+ export function updateKanbanProjectRepositorySettings(project, input) {
1077
+ return {
1078
+ ...project,
1079
+ repositories: (project.repositories ?? []).map((repository) => repository.id === input.repositoryId
1080
+ ? {
1081
+ ...repository,
1082
+ settings: normalizeRepositorySettings({
1083
+ ...repository.settings,
1084
+ ...input.settings,
1085
+ }, repository.defaultBranch),
1086
+ }
1087
+ : repository),
1088
+ };
1089
+ }
1090
+ export function linkKanbanIssueRepository(issue, input) {
1091
+ return {
1092
+ ...issue,
1093
+ repositoryLifecycle: {
1094
+ repositoryId: input.repositoryId,
1095
+ branchName: input.branchName.trim(),
1096
+ reviewStatus: issue.repositoryLifecycle?.reviewStatus ?? 'unlinked',
1097
+ mergeStatus: issue.repositoryLifecycle?.mergeStatus ?? 'not-ready',
1098
+ publishStatus: issue.repositoryLifecycle?.publishStatus ?? 'not-ready',
1099
+ ciGates: issue.repositoryLifecycle?.ciGates ?? [],
1100
+ pullRequest: issue.repositoryLifecycle?.pullRequest,
1101
+ integration: input.integration ?? issue.repositoryLifecycle?.integration,
1102
+ publishUrl: issue.repositoryLifecycle?.publishUrl,
1103
+ lastPublishedAt: issue.repositoryLifecycle?.lastPublishedAt,
1104
+ },
1105
+ };
1106
+ }
1107
+ export function createKanbanIssuePullRequest(issue, input) {
1108
+ if (!issue.repositoryLifecycle) {
1109
+ return issue;
1110
+ }
1111
+ const reviewLinks = (input.reviewLinks ?? []).map((reviewLink, index) => ({
1112
+ ...reviewLink,
1113
+ id: `${issue.repositoryLifecycle.repositoryId}-review-${index + 1}`,
1114
+ }));
1115
+ return {
1116
+ ...issue,
1117
+ repositoryLifecycle: {
1118
+ ...issue.repositoryLifecycle,
1119
+ branchName: input.branchName.trim(),
1120
+ reviewStatus: reviewLinks.length > 0 ? 'pending' : 'unlinked',
1121
+ mergeStatus: 'blocked',
1122
+ ciGates: issue.repositoryLifecycle.ciGates.length > 0
1123
+ ? issue.repositoryLifecycle.ciGates
1124
+ : [
1125
+ {
1126
+ id: `${issue.repositoryLifecycle.repositoryId}-ci-build`,
1127
+ name: 'Build',
1128
+ required: true,
1129
+ status: 'pending',
1130
+ },
1131
+ {
1132
+ id: `${issue.repositoryLifecycle.repositoryId}-ci-tests`,
1133
+ name: 'Tests',
1134
+ required: true,
1135
+ status: 'pending',
1136
+ },
1137
+ ],
1138
+ pullRequest: {
1139
+ id: `${issue.repositoryLifecycle.repositoryId}-pr-${input.number}`,
1140
+ number: input.number,
1141
+ title: input.title.trim(),
1142
+ status: reviewLinks.length > 0 ? 'in-review' : 'open',
1143
+ branchName: input.branchName.trim(),
1144
+ baseBranch: input.baseBranch.trim(),
1145
+ mergeStatus: 'blocked',
1146
+ reviewLinks,
1147
+ url: input.url?.trim() || undefined,
1148
+ linkState: input.linkState ?? 'linked',
1149
+ createdAt: input.now,
1150
+ updatedAt: input.now,
1151
+ },
1152
+ },
1153
+ };
1154
+ }
1155
+ //# sourceMappingURL=kanban.js.map