@useorgx/openclaw-plugin 0.4.9 → 0.7.2

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 (224) hide show
  1. package/README.md +77 -11
  2. package/dashboard/dist/assets/6mILZQ2a.js +1 -0
  3. package/dashboard/dist/assets/6mILZQ2a.js.br +0 -0
  4. package/dashboard/dist/assets/6mILZQ2a.js.gz +0 -0
  5. package/dashboard/dist/assets/8dksYiq4.js +2 -0
  6. package/dashboard/dist/assets/8dksYiq4.js.br +0 -0
  7. package/dashboard/dist/assets/8dksYiq4.js.gz +0 -0
  8. package/dashboard/dist/assets/B5zYRHc3.js +1 -0
  9. package/dashboard/dist/assets/B5zYRHc3.js.br +0 -0
  10. package/dashboard/dist/assets/B5zYRHc3.js.gz +0 -0
  11. package/dashboard/dist/assets/B6wPWJ35.js +1 -0
  12. package/dashboard/dist/assets/B6wPWJ35.js.br +0 -0
  13. package/dashboard/dist/assets/B6wPWJ35.js.gz +0 -0
  14. package/dashboard/dist/assets/BJgZIVUQ.js +53 -0
  15. package/dashboard/dist/assets/BJgZIVUQ.js.br +0 -0
  16. package/dashboard/dist/assets/BJgZIVUQ.js.gz +0 -0
  17. package/dashboard/dist/assets/BWEwjt1W.js +1 -0
  18. package/dashboard/dist/assets/BWEwjt1W.js.br +0 -0
  19. package/dashboard/dist/assets/BWEwjt1W.js.gz +0 -0
  20. package/dashboard/dist/assets/BgOYB78t.js +4 -0
  21. package/dashboard/dist/assets/BgOYB78t.js.br +0 -0
  22. package/dashboard/dist/assets/BgOYB78t.js.gz +0 -0
  23. package/dashboard/dist/assets/BzRbDCAD.css +1 -0
  24. package/dashboard/dist/assets/BzRbDCAD.css.br +0 -0
  25. package/dashboard/dist/assets/BzRbDCAD.css.gz +0 -0
  26. package/dashboard/dist/assets/C-KIc3Wc.js.br +0 -0
  27. package/dashboard/dist/assets/C-KIc3Wc.js.gz +0 -0
  28. package/dashboard/dist/assets/C8uM3AX8.js +1 -0
  29. package/dashboard/dist/assets/C8uM3AX8.js.br +0 -0
  30. package/dashboard/dist/assets/C8uM3AX8.js.gz +0 -0
  31. package/dashboard/dist/assets/C9jy61eu.js +212 -0
  32. package/dashboard/dist/assets/C9jy61eu.js.br +0 -0
  33. package/dashboard/dist/assets/C9jy61eu.js.gz +0 -0
  34. package/dashboard/dist/assets/CC63EwFD.js +1 -0
  35. package/dashboard/dist/assets/CC63EwFD.js.br +0 -0
  36. package/dashboard/dist/assets/CC63EwFD.js.gz +0 -0
  37. package/dashboard/dist/assets/CL_wXqR7.js +1 -0
  38. package/dashboard/dist/assets/CL_wXqR7.js.br +0 -0
  39. package/dashboard/dist/assets/CL_wXqR7.js.gz +0 -0
  40. package/dashboard/dist/assets/CZaT3ob_.js +1 -0
  41. package/dashboard/dist/assets/CZaT3ob_.js.br +0 -0
  42. package/dashboard/dist/assets/CZaT3ob_.js.gz +0 -0
  43. package/dashboard/dist/assets/CgaottFX.js +1 -0
  44. package/dashboard/dist/assets/CgaottFX.js.br +0 -0
  45. package/dashboard/dist/assets/CgaottFX.js.gz +0 -0
  46. package/dashboard/dist/assets/{CpJsfbXo.js → CxQ08qFN.js} +2 -2
  47. package/dashboard/dist/assets/CxQ08qFN.js.br +0 -0
  48. package/dashboard/dist/assets/CxQ08qFN.js.gz +0 -0
  49. package/dashboard/dist/assets/CzCxAZlW.js +1 -0
  50. package/dashboard/dist/assets/CzCxAZlW.js.br +0 -0
  51. package/dashboard/dist/assets/CzCxAZlW.js.gz +0 -0
  52. package/dashboard/dist/assets/D3iMTYEj.js +1 -0
  53. package/dashboard/dist/assets/D3iMTYEj.js.br +0 -0
  54. package/dashboard/dist/assets/D3iMTYEj.js.gz +0 -0
  55. package/dashboard/dist/assets/D8JNX8kq.js +2 -0
  56. package/dashboard/dist/assets/D8JNX8kq.js.br +0 -0
  57. package/dashboard/dist/assets/D8JNX8kq.js.gz +0 -0
  58. package/dashboard/dist/assets/DnA8dpj6.js +1 -0
  59. package/dashboard/dist/assets/DnA8dpj6.js.br +0 -0
  60. package/dashboard/dist/assets/DnA8dpj6.js.gz +0 -0
  61. package/dashboard/dist/assets/IUexzymk.js +1 -0
  62. package/dashboard/dist/assets/IUexzymk.js.br +0 -0
  63. package/dashboard/dist/assets/IUexzymk.js.gz +0 -0
  64. package/dashboard/dist/assets/cNrhgGc1.js +8 -0
  65. package/dashboard/dist/assets/cNrhgGc1.js.br +0 -0
  66. package/dashboard/dist/assets/cNrhgGc1.js.gz +0 -0
  67. package/dashboard/dist/assets/ic2FaMnh.js +1 -0
  68. package/dashboard/dist/assets/ic2FaMnh.js.br +0 -0
  69. package/dashboard/dist/assets/ic2FaMnh.js.gz +0 -0
  70. package/dashboard/dist/assets/qm8xLgv-.css +1 -0
  71. package/dashboard/dist/assets/qm8xLgv-.css.br +0 -0
  72. package/dashboard/dist/assets/qm8xLgv-.css.gz +0 -0
  73. package/dashboard/dist/assets/rttbDbEx.js +1 -0
  74. package/dashboard/dist/assets/rttbDbEx.js.br +0 -0
  75. package/dashboard/dist/assets/rttbDbEx.js.gz +0 -0
  76. package/dashboard/dist/brand/anthropic-mark.svg.br +0 -0
  77. package/dashboard/dist/brand/anthropic-mark.svg.gz +0 -0
  78. package/dashboard/dist/brand/openai-mark.svg.br +0 -0
  79. package/dashboard/dist/brand/openai-mark.svg.gz +0 -0
  80. package/dashboard/dist/brand/openclaw-mark.svg.br +0 -0
  81. package/dashboard/dist/brand/openclaw-mark.svg.gz +0 -0
  82. package/dashboard/dist/brand/xandy-orchestrator.png +0 -0
  83. package/dashboard/dist/index.html +7 -5
  84. package/dashboard/dist/index.html.br +0 -0
  85. package/dashboard/dist/index.html.gz +0 -0
  86. package/dist/activity-actor-fields.js +26 -4
  87. package/dist/activity-store.js +34 -8
  88. package/dist/agent-context-store.js +79 -17
  89. package/dist/agent-run-store.js +44 -3
  90. package/dist/agent-suite.d.ts +9 -0
  91. package/dist/agent-suite.js +149 -9
  92. package/dist/artifacts/artifact-domain-schemas.d.ts +66 -0
  93. package/dist/artifacts/artifact-domain-schemas.js +357 -0
  94. package/dist/artifacts/register-artifact.d.ts +4 -3
  95. package/dist/artifacts/register-artifact.js +170 -57
  96. package/dist/chat-store.d.ts +157 -0
  97. package/dist/chat-store.js +586 -0
  98. package/dist/cli/orgx.js +11 -0
  99. package/dist/contracts/client.d.ts +43 -3
  100. package/dist/contracts/client.js +159 -30
  101. package/dist/contracts/practice-exercise-schema.d.ts +216 -0
  102. package/dist/contracts/practice-exercise-schema.js +314 -0
  103. package/dist/contracts/retro-schema.d.ts +81 -0
  104. package/dist/contracts/retro-schema.js +80 -0
  105. package/dist/contracts/shared-types.d.ts +159 -0
  106. package/dist/contracts/shared-types.js +199 -1
  107. package/dist/contracts/skill-pack-schema.d.ts +192 -0
  108. package/dist/contracts/skill-pack-schema.js +180 -0
  109. package/dist/contracts/types.d.ts +247 -2
  110. package/dist/entities/auto-assignment.js +43 -17
  111. package/dist/event-sanitization.d.ts +11 -0
  112. package/dist/event-sanitization.js +113 -0
  113. package/dist/gateway-watchdog.d.ts +5 -0
  114. package/dist/gateway-watchdog.js +50 -0
  115. package/dist/hooks/post-reporting-event.mjs +1 -5
  116. package/dist/http/helpers/activity-headline.js +13 -132
  117. package/dist/http/helpers/auto-continue-engine.d.ts +198 -10
  118. package/dist/http/helpers/auto-continue-engine.js +3145 -186
  119. package/dist/http/helpers/autopilot-operations.d.ts +19 -0
  120. package/dist/http/helpers/autopilot-operations.js +182 -31
  121. package/dist/http/helpers/autopilot-runtime.d.ts +1 -0
  122. package/dist/http/helpers/autopilot-runtime.js +328 -25
  123. package/dist/http/helpers/autopilot-slice-utils.d.ts +18 -0
  124. package/dist/http/helpers/autopilot-slice-utils.js +514 -93
  125. package/dist/http/helpers/decision-mapper.d.ts +40 -0
  126. package/dist/http/helpers/decision-mapper.js +223 -7
  127. package/dist/http/helpers/dispatch-lifecycle.d.ts +19 -2
  128. package/dist/http/helpers/dispatch-lifecycle.js +242 -37
  129. package/dist/http/helpers/kickoff-context.js +104 -0
  130. package/dist/http/helpers/llm-client.d.ts +47 -0
  131. package/dist/http/helpers/llm-client.js +256 -0
  132. package/dist/http/helpers/mission-control.d.ts +102 -3
  133. package/dist/http/helpers/mission-control.js +498 -9
  134. package/dist/http/helpers/sentinel-catalog.d.ts +23 -0
  135. package/dist/http/helpers/sentinel-catalog.js +193 -0
  136. package/dist/http/helpers/session-classification.d.ts +9 -0
  137. package/dist/http/helpers/session-classification.js +564 -0
  138. package/dist/http/helpers/slice-experience-v2.d.ts +137 -0
  139. package/dist/http/helpers/slice-experience-v2.js +677 -0
  140. package/dist/http/helpers/slice-run-projections.d.ts +72 -0
  141. package/dist/http/helpers/slice-run-projections.js +877 -0
  142. package/dist/http/helpers/triage-mapper.d.ts +43 -0
  143. package/dist/http/helpers/triage-mapper.js +549 -0
  144. package/dist/http/helpers/value-utils.js +7 -2
  145. package/dist/http/helpers/workspace-scope.d.ts +15 -0
  146. package/dist/http/helpers/workspace-scope.js +170 -0
  147. package/dist/http/index.js +1420 -105
  148. package/dist/http/routes/agent-suite.d.ts +9 -0
  149. package/dist/http/routes/agent-suite.js +294 -8
  150. package/dist/http/routes/agents-catalog.js +64 -19
  151. package/dist/http/routes/chat.d.ts +19 -0
  152. package/dist/http/routes/chat.js +522 -0
  153. package/dist/http/routes/decision-actions.d.ts +8 -1
  154. package/dist/http/routes/decision-actions.js +42 -5
  155. package/dist/http/routes/dispatch-gateway-envelope.d.ts +25 -0
  156. package/dist/http/routes/dispatch-gateway-envelope.js +26 -0
  157. package/dist/http/routes/entities.d.ts +16 -0
  158. package/dist/http/routes/entities.js +232 -6
  159. package/dist/http/routes/live-legacy.d.ts +5 -0
  160. package/dist/http/routes/live-legacy.js +23 -509
  161. package/dist/http/routes/live-misc.d.ts +12 -0
  162. package/dist/http/routes/live-misc.js +251 -31
  163. package/dist/http/routes/live-snapshot.d.ts +49 -2
  164. package/dist/http/routes/live-snapshot.js +653 -23
  165. package/dist/http/routes/live-terminal.d.ts +11 -0
  166. package/dist/http/routes/live-terminal.js +154 -0
  167. package/dist/http/routes/live-triage.d.ts +61 -0
  168. package/dist/http/routes/live-triage.js +192 -0
  169. package/dist/http/routes/mission-control-actions.d.ts +49 -1
  170. package/dist/http/routes/mission-control-actions.js +1246 -84
  171. package/dist/http/routes/mission-control-read.d.ts +48 -3
  172. package/dist/http/routes/mission-control-read.js +1658 -20
  173. package/dist/http/routes/realtime-orchestrator.d.ts +10 -0
  174. package/dist/http/routes/realtime-orchestrator.js +74 -0
  175. package/dist/http/routes/run-control.d.ts +5 -2
  176. package/dist/http/routes/run-control.js +10 -0
  177. package/dist/http/routes/sentinels-catalog.d.ts +7 -0
  178. package/dist/http/routes/sentinels-catalog.js +24 -0
  179. package/dist/http/routes/summary.js +10 -3
  180. package/dist/http/routes/usage.d.ts +24 -0
  181. package/dist/http/routes/usage.js +362 -0
  182. package/dist/http/routes/work-artifacts.js +28 -9
  183. package/dist/index.js +165 -27
  184. package/dist/local-openclaw.js +29 -6
  185. package/dist/mcp-client-setup.js +3 -3
  186. package/dist/mcp-http-handler.d.ts +3 -0
  187. package/dist/mcp-http-handler.js +34 -60
  188. package/dist/next-up-queue-store.d.ts +16 -1
  189. package/dist/next-up-queue-store.js +89 -7
  190. package/dist/outbox.d.ts +5 -0
  191. package/dist/outbox.js +113 -9
  192. package/dist/paths.js +36 -5
  193. package/dist/reporting/rollups.d.ts +41 -0
  194. package/dist/reporting/rollups.js +113 -0
  195. package/dist/retro/domain-templates.d.ts +45 -0
  196. package/dist/retro/domain-templates.js +297 -0
  197. package/dist/retro/quality-rubric.d.ts +33 -0
  198. package/dist/retro/quality-rubric.js +213 -0
  199. package/dist/runtime-cleanup.d.ts +18 -0
  200. package/dist/runtime-cleanup.js +87 -0
  201. package/dist/services/background.d.ts +11 -0
  202. package/dist/services/background.js +22 -0
  203. package/dist/services/experiment-randomization.d.ts +21 -0
  204. package/dist/services/experiment-randomization.js +63 -0
  205. package/dist/skill-pack-state.d.ts +36 -5
  206. package/dist/skill-pack-state.js +273 -29
  207. package/dist/sync/local-agent-telemetry.d.ts +13 -0
  208. package/dist/sync/local-agent-telemetry.js +128 -0
  209. package/dist/sync/outbox-replay.js +131 -24
  210. package/dist/team-context-store.d.ts +23 -0
  211. package/dist/team-context-store.js +116 -0
  212. package/dist/telemetry/posthog.js +4 -2
  213. package/dist/tools/core-tools.d.ts +10 -14
  214. package/dist/tools/core-tools.js +1289 -24
  215. package/dist/types.d.ts +2 -0
  216. package/dist/types.js +2 -0
  217. package/dist/worker-supervisor.js +23 -0
  218. package/package.json +20 -6
  219. package/dashboard/dist/assets/B3ziCA02.js +0 -8
  220. package/dashboard/dist/assets/B5NEElEI.css +0 -1
  221. package/dashboard/dist/assets/BhapSNAs.js +0 -215
  222. package/dashboard/dist/assets/iFdvE7lx.js +0 -1
  223. package/dashboard/dist/assets/jRJsmpYM.js +0 -1
  224. package/dashboard/dist/assets/sAhvFnpk.js +0 -4
@@ -1,12 +1,806 @@
1
+ import { listBuiltInSentinels } from "../helpers/sentinel-catalog.js";
2
+ import { resolveWorkspaceScope, workspaceScopeFromHeaders, } from "../helpers/workspace-scope.js";
3
+ function asRecord(value) {
4
+ if (!value || typeof value !== "object" || Array.isArray(value))
5
+ return null;
6
+ return value;
7
+ }
8
+ function asString(value) {
9
+ if (typeof value !== "string")
10
+ return null;
11
+ const trimmed = value.trim();
12
+ return trimmed.length > 0 ? trimmed : null;
13
+ }
14
+ function normalizeRunnerValue(value) {
15
+ const raw = asString(value);
16
+ if (!raw)
17
+ return null;
18
+ const normalized = raw.trim().toLowerCase();
19
+ if (!normalized || normalized === "undefined" || normalized === "null")
20
+ return null;
21
+ if (normalized === "main" || normalized === "unassigned")
22
+ return null;
23
+ if (normalized === "n/a" || normalized === "na" || normalized === "none")
24
+ return null;
25
+ if (normalized === "-" || normalized === "default")
26
+ return null;
27
+ return raw.trim();
28
+ }
29
+ function normalizeRunnerSource(value) {
30
+ const raw = asString(value);
31
+ if (!raw)
32
+ return null;
33
+ const normalized = raw.trim().toLowerCase();
34
+ if (normalized === "assigned")
35
+ return "assigned";
36
+ if (normalized === "inferred")
37
+ return "inferred";
38
+ if (normalized === "fallback")
39
+ return "fallback";
40
+ return null;
41
+ }
42
+ function normalizeRunnerAgents(value) {
43
+ if (!Array.isArray(value))
44
+ return [];
45
+ const output = [];
46
+ const seen = new Set();
47
+ for (const entry of value) {
48
+ const record = asRecord(entry);
49
+ if (!record)
50
+ continue;
51
+ const id = normalizeRunnerValue(record.id);
52
+ const name = normalizeRunnerValue(record.name);
53
+ if (!id && !name)
54
+ continue;
55
+ const resolvedId = id ?? name;
56
+ const key = resolvedId.toLowerCase();
57
+ if (seen.has(key))
58
+ continue;
59
+ seen.add(key);
60
+ output.push({
61
+ id: resolvedId,
62
+ name: name ?? resolvedId,
63
+ });
64
+ }
65
+ return output;
66
+ }
67
+ function mergeRunnerAgents(...groups) {
68
+ const output = [];
69
+ const seen = new Set();
70
+ for (const group of groups) {
71
+ for (const agent of group) {
72
+ const id = normalizeRunnerValue(agent.id);
73
+ const name = normalizeRunnerValue(agent.name);
74
+ if (!id && !name)
75
+ continue;
76
+ const resolvedId = id ?? name;
77
+ const key = resolvedId.toLowerCase();
78
+ if (seen.has(key))
79
+ continue;
80
+ seen.add(key);
81
+ output.push({
82
+ id: resolvedId,
83
+ name: name ?? resolvedId,
84
+ });
85
+ }
86
+ }
87
+ return output;
88
+ }
89
+ function asNumber(value) {
90
+ if (typeof value === "number" && Number.isFinite(value))
91
+ return value;
92
+ if (typeof value === "string" && value.trim().length > 0) {
93
+ const parsed = Number(value);
94
+ if (Number.isFinite(parsed))
95
+ return parsed;
96
+ }
97
+ return null;
98
+ }
99
+ function asStringArray(value) {
100
+ if (!Array.isArray(value))
101
+ return [];
102
+ const values = [];
103
+ for (const entry of value) {
104
+ const normalized = asString(entry);
105
+ if (!normalized)
106
+ continue;
107
+ values.push(normalized);
108
+ }
109
+ return dedupeStrings(values);
110
+ }
111
+ function isCanonicalAllScopeMismatch(canonicalRecord, useAllScope) {
112
+ if (!useAllScope)
113
+ return false;
114
+ const workspaceRaw = asString(canonicalRecord.workspaceId) ??
115
+ asString(canonicalRecord.workspace_id);
116
+ if (!workspaceRaw)
117
+ return false;
118
+ const normalized = workspaceRaw.trim().toLowerCase();
119
+ if (!normalized)
120
+ return false;
121
+ return normalized !== "all" && normalized !== "__all__" && normalized !== "*";
122
+ }
123
+ function dedupeStrings(values) {
124
+ const output = [];
125
+ const seen = new Set();
126
+ for (const value of values) {
127
+ const normalized = value.trim();
128
+ if (!normalized)
129
+ continue;
130
+ const key = normalized.toLowerCase();
131
+ if (seen.has(key))
132
+ continue;
133
+ seen.add(key);
134
+ output.push(normalized);
135
+ }
136
+ return output;
137
+ }
138
+ function parseBoolean(value) {
139
+ if (!value)
140
+ return false;
141
+ const normalized = value.trim().toLowerCase();
142
+ return normalized === "1" || normalized === "true" || normalized === "yes";
143
+ }
144
+ function parsePositiveInt(value, fallback, max = 300) {
145
+ if (!value || value.trim().length === 0)
146
+ return fallback;
147
+ const parsed = Number.parseInt(value, 10);
148
+ if (!Number.isFinite(parsed))
149
+ return fallback;
150
+ return Math.max(0, Math.min(max, parsed));
151
+ }
152
+ function normalizeSliceSearchTerm(value) {
153
+ return (value ?? "").trim().toLowerCase();
154
+ }
155
+ function extractSliceSearchText(item) {
156
+ const record = asRecord(item);
157
+ if (!record)
158
+ return "";
159
+ const candidates = [
160
+ asString(record.sliceId),
161
+ asString(record.id),
162
+ asString(record.title),
163
+ asString(record.initiativeTitle),
164
+ asString(record.workstreamTitle),
165
+ asString(record.milestoneTitle),
166
+ asString(record.taskTitle),
167
+ asString(record.initiativeId),
168
+ asString(record.workstreamId),
169
+ asString(record.milestoneId),
170
+ asString(record.taskId),
171
+ asString(record.scope),
172
+ asString(record.level),
173
+ ].filter((entry) => Boolean(entry));
174
+ return candidates.join(" ").toLowerCase();
175
+ }
176
+ function applySliceSearchAndPagination(input) {
177
+ const filtered = input.searchTerm.length === 0
178
+ ? input.items
179
+ : input.items.filter((item) => extractSliceSearchText(item).includes(input.searchTerm));
180
+ const offset = Math.max(0, input.offset);
181
+ const paged = filtered.slice(offset, offset + input.limit);
182
+ const nextOffset = offset + input.limit;
183
+ const hasMore = nextOffset < filtered.length;
184
+ return {
185
+ filtered,
186
+ paged,
187
+ pagination: {
188
+ offset,
189
+ limit: input.limit,
190
+ total: filtered.length,
191
+ nextCursor: hasMore ? String(nextOffset) : null,
192
+ hasMore,
193
+ },
194
+ };
195
+ }
196
+ function parsePaginationEnvelope(value, fallback) {
197
+ const record = asRecord(value);
198
+ const offset = Math.max(0, Math.min(100_000, Math.floor(asNumber(record?.offset) ?? fallback.offset)));
199
+ const limit = Math.max(1, Math.min(300, Math.floor(asNumber(record?.limit) ?? fallback.limit)));
200
+ const total = Math.max(0, Math.floor(asNumber(record?.total) ?? fallback.total));
201
+ const nextCursor = asString(record?.nextCursor);
202
+ const hasMore = typeof record?.hasMore === "boolean"
203
+ ? record.hasMore
204
+ : offset + limit < total;
205
+ return { offset, limit, total, nextCursor, hasMore };
206
+ }
207
+ const WARMUP_MIN_INTERVAL_MS = 12_000;
208
+ const NEXT_UP_DEFAULT_PAGE_SIZE = 24;
209
+ const SLICES_DEFAULT_PAGE_SIZE = 24;
210
+ const CANONICAL_NEXT_UP_TIMEOUT_MS = 20_000;
211
+ const CANONICAL_SLICES_TIMEOUT_MS = 20_000;
212
+ const CANONICAL_READ_CACHE_TTL_MS = 30_000;
213
+ const CANONICAL_READ_STALE_TTL_MS = 180_000;
214
+ const CANONICAL_AUTH_BYPASS_MS = 8_000;
215
+ const warmupByKey = new Map();
216
+ const canonicalReadCache = new Map();
217
+ const canonicalBypassState = new Map();
218
+ function shouldRunWarmup(key) {
219
+ const now = Date.now();
220
+ const previous = warmupByKey.get(key);
221
+ if (typeof previous === "number" && now - previous < WARMUP_MIN_INTERVAL_MS) {
222
+ return false;
223
+ }
224
+ warmupByKey.set(key, now);
225
+ return true;
226
+ }
227
+ function canonicalReadCacheKey(input) {
228
+ return [
229
+ input.route,
230
+ input.workspaceId ?? "__all__",
231
+ input.scopeMode ?? "implicit",
232
+ input.initiativeId ?? "__any__",
233
+ input.includeCompleted ? "include_completed" : "exclude_completed",
234
+ String(input.offset),
235
+ String(input.limit),
236
+ input.scope ?? "__none__",
237
+ input.order ?? "__none__",
238
+ input.mixPolicy ?? "__none__",
239
+ input.noiseThreshold ?? "__none__",
240
+ input.dedupWindowMs == null ? "__none__" : String(input.dedupWindowMs),
241
+ input.search ?? "__none__",
242
+ ].join("|");
243
+ }
244
+ function cloneCanonicalReadPayload(payload) {
245
+ const clone = { ...payload };
246
+ if (Array.isArray(payload.items))
247
+ clone.items = [...payload.items];
248
+ if (Array.isArray(payload.degraded))
249
+ clone.degraded = [...payload.degraded];
250
+ const pagination = asRecord(payload.pagination);
251
+ if (pagination)
252
+ clone.pagination = { ...pagination };
253
+ return clone;
254
+ }
255
+ function readCanonicalReadCache(key, opts) {
256
+ const cached = canonicalReadCache.get(key);
257
+ if (!cached)
258
+ return null;
259
+ const now = Date.now();
260
+ const allowStale = Boolean(opts?.allowStale);
261
+ const stillFresh = cached.expiresAt > now;
262
+ const stillStale = cached.staleUntil > now;
263
+ if (!stillFresh && !stillStale) {
264
+ canonicalReadCache.delete(key);
265
+ return null;
266
+ }
267
+ if (!stillFresh && !allowStale)
268
+ return null;
269
+ return cloneCanonicalReadPayload(cached.payload);
270
+ }
271
+ function writeCanonicalReadCache(key, payload) {
272
+ const now = Date.now();
273
+ canonicalReadCache.set(key, {
274
+ expiresAt: now + CANONICAL_READ_CACHE_TTL_MS,
275
+ staleUntil: now + CANONICAL_READ_STALE_TTL_MS,
276
+ payload: cloneCanonicalReadPayload(payload),
277
+ });
278
+ }
279
+ function readCanonicalBypass(route) {
280
+ const record = canonicalBypassState.get(route);
281
+ if (!record)
282
+ return null;
283
+ if (record.until <= Date.now()) {
284
+ canonicalBypassState.delete(route);
285
+ return null;
286
+ }
287
+ return { reason: record.reason };
288
+ }
289
+ function setCanonicalBypass(route, reason, durationMs) {
290
+ canonicalBypassState.set(route, {
291
+ until: Date.now() + Math.max(1_000, durationMs),
292
+ reason,
293
+ });
294
+ }
295
+ function isCanonicalAuthFailure(error) {
296
+ const message = String(error instanceof Error ? error.message : error ?? "").toLowerCase();
297
+ return (message.includes("401") ||
298
+ message.includes("403") ||
299
+ message.includes("unauthorized") ||
300
+ message.includes("forbidden") ||
301
+ message.includes("authentication required"));
302
+ }
303
+ function isCanonicalAllScopeMismatchError(error) {
304
+ const message = String(error instanceof Error ? error.message : error ?? "").toLowerCase();
305
+ return message.includes("all-workspaces scope mismatch");
306
+ }
307
+ async function withSoftTimeout(request, timeoutMs, label) {
308
+ let timer = null;
309
+ const timeout = new Promise((_, reject) => {
310
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
311
+ });
312
+ try {
313
+ return await Promise.race([request, timeout]);
314
+ }
315
+ finally {
316
+ if (timer)
317
+ clearTimeout(timer);
318
+ }
319
+ }
320
+ function shouldRetryLegacyCanonicalPath(error) {
321
+ const message = String(error instanceof Error ? error.message : error ?? "").toLowerCase();
322
+ return (message.includes("404") ||
323
+ message.includes("not found") ||
324
+ message.includes("unknown api endpoint") ||
325
+ message.includes("401") ||
326
+ message.includes("403") ||
327
+ message.includes("unauthorized") ||
328
+ message.includes("forbidden"));
329
+ }
330
+ async function requestCanonicalWithLegacyFallback(deps, input) {
331
+ try {
332
+ return await withSoftTimeout(deps.rawRequest("GET", input.modernPath), input.timeoutMs, input.label);
333
+ }
334
+ catch (error) {
335
+ if (!shouldRetryLegacyCanonicalPath(error))
336
+ throw error;
337
+ }
338
+ return await withSoftTimeout(deps.rawRequest("GET", input.legacyPath), Math.min(input.timeoutMs, 1_500), `${input.label} (legacy fallback)`);
339
+ }
340
+ function normalizeQueueState(value) {
341
+ const normalized = normalizeStatus(asString(value));
342
+ if (normalized === "running" || normalized === "in_progress" || normalized === "active") {
343
+ return "running";
344
+ }
345
+ if (normalized === "queued" || normalized === "pending" || normalized === "todo" || normalized === "ready") {
346
+ return "queued";
347
+ }
348
+ if (normalized === "blocked" ||
349
+ normalized === "waiting" ||
350
+ normalized === "needs_decision" ||
351
+ normalized === "waiting_on_decision") {
352
+ return "blocked";
353
+ }
354
+ if (normalized === "completed" || normalized === "done")
355
+ return "completed";
356
+ return "idle";
357
+ }
358
+ function normalizeStatus(value) {
359
+ return (value ?? "").trim().toLowerCase().replace(/[\s-]+/g, "_");
360
+ }
361
+ function isDoneStatus(value) {
362
+ const normalized = normalizeStatus(value);
363
+ return (normalized === "done" ||
364
+ normalized === "completed" ||
365
+ normalized === "resolved" ||
366
+ normalized === "cancelled" ||
367
+ normalized === "canceled" ||
368
+ normalized === "archived" ||
369
+ normalized === "closed");
370
+ }
371
+ function queueStateRank(state) {
372
+ if (state === "running")
373
+ return 0;
374
+ if (state === "queued")
375
+ return 1;
376
+ if (state === "blocked")
377
+ return 2;
378
+ if (state === "idle")
379
+ return 3;
380
+ return 4;
381
+ }
382
+ function combinedQueueState(states) {
383
+ if (states.some((state) => state === "running"))
384
+ return "running";
385
+ if (states.some((state) => state === "blocked"))
386
+ return "blocked";
387
+ if (states.some((state) => state === "queued"))
388
+ return "queued";
389
+ if (states.some((state) => state === "idle"))
390
+ return "idle";
391
+ return "completed";
392
+ }
393
+ function dueEpoch(value) {
394
+ if (!value)
395
+ return Number.MAX_SAFE_INTEGER;
396
+ const parsed = Date.parse(value);
397
+ return Number.isFinite(parsed) ? parsed : Number.MAX_SAFE_INTEGER;
398
+ }
399
+ function updatedEpoch(value) {
400
+ if (!value)
401
+ return 0;
402
+ const parsed = Date.parse(value);
403
+ return Number.isFinite(parsed) ? parsed : 0;
404
+ }
405
+ function parseSliceScope(value) {
406
+ const normalized = (value ?? "").trim().toLowerCase();
407
+ if (normalized === "initiative")
408
+ return "initiative";
409
+ if (normalized === "milestone")
410
+ return "milestone";
411
+ if (normalized === "task")
412
+ return "task";
413
+ return "workstream";
414
+ }
415
+ function parseSliceOrder(value) {
416
+ const normalized = (value ?? "").trim().toLowerCase();
417
+ if (normalized === "priority")
418
+ return "priority";
419
+ if (normalized === "due")
420
+ return "due";
421
+ if (normalized === "updated")
422
+ return "updated";
423
+ return "iwmt";
424
+ }
425
+ function sortSlices(items, order) {
426
+ return [...items].sort((left, right) => {
427
+ if (order === "priority") {
428
+ const leftPriority = left.nextTaskPriority ?? Number.MAX_SAFE_INTEGER;
429
+ const rightPriority = right.nextTaskPriority ?? Number.MAX_SAFE_INTEGER;
430
+ if (leftPriority !== rightPriority)
431
+ return leftPriority - rightPriority;
432
+ }
433
+ else if (order === "due") {
434
+ const leftDue = dueEpoch(left.nextTaskDueAt);
435
+ const rightDue = dueEpoch(right.nextTaskDueAt);
436
+ if (leftDue !== rightDue)
437
+ return leftDue - rightDue;
438
+ }
439
+ else if (order === "updated") {
440
+ const leftUpdated = updatedEpoch(left.updatedAt);
441
+ const rightUpdated = updatedEpoch(right.updatedAt);
442
+ if (leftUpdated !== rightUpdated)
443
+ return rightUpdated - leftUpdated;
444
+ }
445
+ const iwmtDelta = left.iwmtRank - right.iwmtRank;
446
+ if (iwmtDelta !== 0)
447
+ return iwmtDelta;
448
+ const queueDelta = queueStateRank(left.queueState) - queueStateRank(right.queueState);
449
+ if (queueDelta !== 0)
450
+ return queueDelta;
451
+ const initiativeDelta = left.initiativeTitle.localeCompare(right.initiativeTitle);
452
+ if (initiativeDelta !== 0)
453
+ return initiativeDelta;
454
+ const workstreamDelta = (left.workstreamTitle ?? "").localeCompare(right.workstreamTitle ?? "");
455
+ if (workstreamDelta !== 0)
456
+ return workstreamDelta;
457
+ return left.id.localeCompare(right.id);
458
+ });
459
+ }
460
+ function normalizeQueueItems(input) {
461
+ const output = [];
462
+ for (const entry of input) {
463
+ const record = asRecord(entry);
464
+ if (!record)
465
+ continue;
466
+ const initiativeId = asString(record.initiativeId) ?? asString(record.initiative_id);
467
+ const workstreamId = asString(record.workstreamId) ?? asString(record.workstream_id);
468
+ if (!initiativeId || !workstreamId)
469
+ continue;
470
+ const nextTaskId = asString(record.nextTaskId) ?? asString(record.next_task_id);
471
+ const sliceTaskIds = dedupeStrings([
472
+ ...asStringArray(record.sliceTaskIds),
473
+ ...asStringArray(record.slice_task_ids),
474
+ ...(nextTaskId ? [nextTaskId] : []),
475
+ ]);
476
+ const runnerAgentsRaw = mergeRunnerAgents(normalizeRunnerAgents(record.runnerAgents), normalizeRunnerAgents(record.runner_agents));
477
+ const runnerAgentIdRaw = normalizeRunnerValue(record.runnerAgentId) ?? normalizeRunnerValue(record.runner_agent_id);
478
+ const runnerAgentNameRaw = normalizeRunnerValue(record.runnerAgentName) ??
479
+ normalizeRunnerValue(record.runner_agent_name) ??
480
+ normalizeRunnerValue(record.agentName) ??
481
+ normalizeRunnerValue(record.runner);
482
+ const runnerAgents = mergeRunnerAgents(runnerAgentsRaw, runnerAgentIdRaw || runnerAgentNameRaw
483
+ ? [
484
+ {
485
+ id: runnerAgentIdRaw ?? runnerAgentNameRaw ?? "Unassigned",
486
+ name: runnerAgentNameRaw ?? runnerAgentIdRaw ?? "Unassigned",
487
+ },
488
+ ]
489
+ : []);
490
+ const runnerPrimary = runnerAgents[0] ?? null;
491
+ const runnerAgentId = runnerPrimary?.id ?? null;
492
+ const runnerAgentName = runnerPrimary?.name ?? "Unassigned";
493
+ const runnerSourceHint = normalizeRunnerSource(record.runnerSource) ?? normalizeRunnerSource(record.runner_source);
494
+ const runnerSource = runnerAgentId
495
+ ? runnerSourceHint ?? "inferred"
496
+ : "fallback";
497
+ const queueState = normalizeQueueState(record.queueState ?? record.queue_state);
498
+ const rawSliceScope = asString(record.sliceScope) ?? asString(record.slice_scope);
499
+ const sliceScope = rawSliceScope === "task" || rawSliceScope === "milestone" || rawSliceScope === "workstream"
500
+ ? rawSliceScope
501
+ : null;
502
+ const sliceTaskCountRaw = asNumber(record.sliceTaskCount ?? record.slice_task_count);
503
+ const blockReason = asString(record.blockReason) ??
504
+ asString(record.block_reason) ??
505
+ (queueState === "blocked"
506
+ ? `Waiting on dependency ${asString(record.nextTaskTitle) ?? asString(record.next_task_title) ?? asString(record.nextTaskId) ?? asString(record.next_task_id) ?? "task"}`
507
+ : null);
508
+ output.push({
509
+ initiativeId,
510
+ initiativeTitle: asString(record.initiativeTitle) ?? asString(record.initiative_title) ?? initiativeId,
511
+ initiativeStatus: asString(record.initiativeStatus) ?? asString(record.initiative_status) ?? "active",
512
+ initiativePriority: asString(record.initiativePriority) ?? asString(record.initiative_priority),
513
+ initiativePriorityNum: asNumber(record.initiativePriorityNum ?? record.initiative_priority_num),
514
+ workstreamId,
515
+ workstreamTitle: asString(record.workstreamTitle) ?? asString(record.workstream_title) ?? workstreamId,
516
+ workstreamStatus: asString(record.workstreamStatus) ?? asString(record.workstream_status) ?? "active",
517
+ nextTaskId,
518
+ nextTaskTitle: asString(record.nextTaskTitle) ?? asString(record.next_task_title),
519
+ nextTaskPriority: asNumber(record.nextTaskPriority ?? record.next_task_priority),
520
+ nextTaskDueAt: asString(record.nextTaskDueAt) ?? asString(record.next_task_due_at),
521
+ nextTaskMilestoneId: asString(record.nextTaskMilestoneId) ?? asString(record.next_task_milestone_id),
522
+ runnerAgentId,
523
+ runnerAgentName,
524
+ runnerAgents,
525
+ runnerSource,
526
+ queueState,
527
+ blockReason,
528
+ sliceScope,
529
+ sliceTaskIds,
530
+ sliceTaskCount: typeof sliceTaskCountRaw === "number"
531
+ ? Math.max(0, Math.floor(sliceTaskCountRaw))
532
+ : sliceTaskIds.length,
533
+ sliceMilestoneId: asString(record.sliceMilestoneId) ?? asString(record.slice_milestone_id),
534
+ isPinned: Boolean(record.isPinned ?? record.is_pinned),
535
+ pinnedRank: asNumber(record.pinnedRank ?? record.pinned_rank),
536
+ compositeScore: asNumber(record.compositeScore ?? record.composite_score) ?? undefined,
537
+ scoringTier: asString(record.scoringTier ?? record.scoring_tier) === "urgent" ||
538
+ asString(record.scoringTier ?? record.scoring_tier) === "ready" ||
539
+ asString(record.scoringTier ?? record.scoring_tier) === "waiting" ||
540
+ asString(record.scoringTier ?? record.scoring_tier) === "deferred"
541
+ ? asString(record.scoringTier ?? record.scoring_tier)
542
+ : undefined,
543
+ updatedAt: asString(record.updatedAt) ?? asString(record.updated_at) ?? null,
544
+ });
545
+ }
546
+ return output.sort((left, right) => {
547
+ const pinnedLeft = left.isPinned ? 0 : 1;
548
+ const pinnedRight = right.isPinned ? 0 : 1;
549
+ if (pinnedLeft !== pinnedRight)
550
+ return pinnedLeft - pinnedRight;
551
+ if (pinnedLeft === 0) {
552
+ const rankDelta = (left.pinnedRank ?? Number.MAX_SAFE_INTEGER) -
553
+ (right.pinnedRank ?? Number.MAX_SAFE_INTEGER);
554
+ if (rankDelta !== 0)
555
+ return rankDelta;
556
+ }
557
+ const queueDelta = queueStateRank(left.queueState) - queueStateRank(right.queueState);
558
+ if (queueDelta !== 0)
559
+ return queueDelta;
560
+ const priorityDelta = (left.nextTaskPriority ?? Number.MAX_SAFE_INTEGER) -
561
+ (right.nextTaskPriority ?? Number.MAX_SAFE_INTEGER);
562
+ if (priorityDelta !== 0)
563
+ return priorityDelta;
564
+ const initiativePriorityDelta = (left.initiativePriorityNum ?? Number.MAX_SAFE_INTEGER) -
565
+ (right.initiativePriorityNum ?? Number.MAX_SAFE_INTEGER);
566
+ if (initiativePriorityDelta !== 0)
567
+ return initiativePriorityDelta;
568
+ const dueDelta = dueEpoch(left.nextTaskDueAt) - dueEpoch(right.nextTaskDueAt);
569
+ if (dueDelta !== 0)
570
+ return dueDelta;
571
+ const titleDelta = left.initiativeTitle.localeCompare(right.initiativeTitle);
572
+ if (titleDelta !== 0)
573
+ return titleDelta;
574
+ return left.workstreamTitle.localeCompare(right.workstreamTitle);
575
+ });
576
+ }
577
+ function blockedItemSeverity(item) {
578
+ if (item.scoringTier === "urgent")
579
+ return "high";
580
+ if (item.scoringTier === "deferred")
581
+ return "low";
582
+ const reason = (item.blockReason ?? "").toLowerCase();
583
+ if (!reason)
584
+ return "medium";
585
+ if (/\b(critical|sev-?1|severity\s*1|outage|security|incident|prod(?:uction)?\s+down)\b/.test(reason)) {
586
+ return "high";
587
+ }
588
+ if (/\b(review|approval|capacity|queue|backlog|follow[\s-]?up)\b/.test(reason)) {
589
+ return "low";
590
+ }
591
+ return "medium";
592
+ }
593
+ function applyNextUpNoiseAndDedup(items, noiseThreshold, dedupWindowMs) {
594
+ const filtered = items.filter((item) => {
595
+ if (item.queueState !== "blocked")
596
+ return true;
597
+ const severity = blockedItemSeverity(item);
598
+ if (noiseThreshold === "low")
599
+ return true;
600
+ if (noiseThreshold === "high")
601
+ return severity === "high";
602
+ return severity !== "low";
603
+ });
604
+ if (dedupWindowMs <= 0)
605
+ return filtered;
606
+ const deduped = [];
607
+ const latestByKey = new Map();
608
+ for (const item of filtered) {
609
+ if (item.queueState !== "blocked") {
610
+ deduped.push(item);
611
+ continue;
612
+ }
613
+ const reason = (item.blockReason ?? "").trim().toLowerCase().replace(/\s+/g, " ");
614
+ if (!reason) {
615
+ deduped.push(item);
616
+ continue;
617
+ }
618
+ const updatedAt = Date.parse(item.updatedAt ?? "");
619
+ if (!Number.isFinite(updatedAt)) {
620
+ deduped.push(item);
621
+ continue;
622
+ }
623
+ const dedupKey = `${item.initiativeId}:${reason}`;
624
+ const lastSeen = latestByKey.get(dedupKey);
625
+ if (typeof lastSeen === "number" && Math.abs(updatedAt - lastSeen) <= dedupWindowMs) {
626
+ continue;
627
+ }
628
+ latestByKey.set(dedupKey, updatedAt);
629
+ deduped.push(item);
630
+ }
631
+ return deduped;
632
+ }
633
+ function mapCanonicalSlicesToQueueItems(input) {
634
+ const queueLike = [];
635
+ for (const entry of input) {
636
+ const record = asRecord(entry);
637
+ if (!record)
638
+ continue;
639
+ const initiativeId = asString(record.initiativeId) ?? asString(record.initiative_id);
640
+ const workstreamId = asString(record.workstreamId) ?? asString(record.workstream_id);
641
+ if (!initiativeId || !workstreamId)
642
+ continue;
643
+ const dispatch = asRecord(record.dispatch) ?? {};
644
+ const lineage = asRecord(record.lineage) ?? {};
645
+ const taskId = asString(record.taskId) ?? asString(record.task_id);
646
+ const sliceTaskIds = dedupeStrings([
647
+ ...asStringArray(record.sliceTaskIds),
648
+ ...asStringArray(record.slice_task_ids),
649
+ ...asStringArray(lineage.taskIds),
650
+ ...asStringArray(lineage.task_ids),
651
+ ...(taskId ? [taskId] : []),
652
+ ]);
653
+ const rawStatus = asString(record.status) ?? "active";
654
+ const normalizedStatus = normalizeStatus(rawStatus);
655
+ const runnable = Boolean(dispatch.runnable);
656
+ let queueState;
657
+ if (isDoneStatus(rawStatus)) {
658
+ queueState = "completed";
659
+ }
660
+ else if (normalizedStatus === "running" || normalizedStatus === "in_progress") {
661
+ queueState = "running";
662
+ }
663
+ else if (normalizedStatus === "blocked" ||
664
+ normalizedStatus === "waiting_dependency" ||
665
+ normalizedStatus === "needs_decision" ||
666
+ normalizedStatus === "waiting_on_decision" ||
667
+ normalizedStatus === "paused" ||
668
+ !runnable) {
669
+ queueState = "blocked";
670
+ }
671
+ else if (normalizedStatus === "idle" ||
672
+ normalizedStatus === "not_started" ||
673
+ normalizedStatus === "draft") {
674
+ queueState = "idle";
675
+ }
676
+ else {
677
+ queueState = "queued";
678
+ }
679
+ const runnerAgentIdRaw = normalizeRunnerValue(record.runnerAgentId) ?? normalizeRunnerValue(record.runner_agent_id);
680
+ const runnerAgentNameRaw = normalizeRunnerValue(record.runnerAgentName) ?? normalizeRunnerValue(record.runner_agent_name);
681
+ const runnerAgents = mergeRunnerAgents(normalizeRunnerAgents(record.runnerAgents), normalizeRunnerAgents(record.runner_agents), runnerAgentIdRaw || runnerAgentNameRaw
682
+ ? [
683
+ {
684
+ id: runnerAgentIdRaw ?? runnerAgentNameRaw ?? "Unassigned",
685
+ name: runnerAgentNameRaw ?? runnerAgentIdRaw ?? "Unassigned",
686
+ },
687
+ ]
688
+ : []);
689
+ const runnerSourceHint = normalizeRunnerSource(record.runnerSource) ?? normalizeRunnerSource(record.runner_source);
690
+ const runnerSource = runnerAgents.length > 0 ? runnerSourceHint ?? "inferred" : "fallback";
691
+ const suggestedScope = asString(dispatch.suggestedScope) ??
692
+ asString(dispatch.suggested_scope) ??
693
+ asString(record.level);
694
+ const sliceScope = suggestedScope === "task" ||
695
+ suggestedScope === "milestone" ||
696
+ suggestedScope === "workstream"
697
+ ? suggestedScope
698
+ : null;
699
+ const order = asRecord(record.order) ?? {};
700
+ const manualRank = asNumber(order.manualRank ?? order.manual_rank);
701
+ const iwmt = asRecord(record.iwmt);
702
+ const objective = asRecord(record.objective);
703
+ queueLike.push({
704
+ initiativeId,
705
+ initiativeTitle: asString(record.initiativeTitle) ??
706
+ asString(record.initiative_title) ??
707
+ initiativeId,
708
+ initiativeStatus: asString(record.initiativeStatus) ??
709
+ asString(record.initiative_status) ??
710
+ "active",
711
+ initiativePriority: asString(record.initiativePriority) ?? asString(record.initiative_priority),
712
+ initiativePriorityNum: asNumber(record.initiativePriorityNum ?? record.initiative_priority_num),
713
+ workstreamId,
714
+ workstreamTitle: asString(record.workstreamTitle) ??
715
+ asString(record.workstream_title) ??
716
+ asString(record.title) ??
717
+ workstreamId,
718
+ workstreamStatus: asString(record.workstreamStatus) ??
719
+ asString(record.workstream_status) ??
720
+ rawStatus,
721
+ nextTaskId: taskId ?? sliceTaskIds[0] ?? null,
722
+ nextTaskTitle: asString(record.nextTaskTitle) ?? asString(record.next_task_title),
723
+ nextTaskPriority: asNumber(record.priorityNum ?? record.nextTaskPriority),
724
+ nextTaskDueAt: asString(record.dueAt) ?? asString(record.nextTaskDueAt),
725
+ nextTaskMilestoneId: asString(record.milestoneId) ?? asString(record.milestone_id),
726
+ runnerAgentId: runnerAgentIdRaw,
727
+ runnerAgentName: runnerAgentNameRaw,
728
+ runnerAgents,
729
+ runnerSource,
730
+ queueState,
731
+ blockReason: asString(dispatch.blockReason) ??
732
+ asString(dispatch.block_reason) ??
733
+ null,
734
+ sliceScope,
735
+ sliceTaskIds,
736
+ sliceTaskCount: sliceTaskIds.length,
737
+ sliceMilestoneId: asString(record.milestoneId) ?? asString(record.milestone_id),
738
+ isPinned: typeof manualRank === "number",
739
+ pinnedRank: manualRank,
740
+ compositeScore: asNumber(iwmt?.mixScore ?? objective?.objectiveScore),
741
+ updatedAt: asString(record.updatedAt) ?? asString(record.updated_at) ?? null,
742
+ });
743
+ }
744
+ return normalizeQueueItems(queueLike);
745
+ }
746
+ async function loadInitiativeGraphIndex(deps, initiativeId) {
747
+ const graphRaw = deps.applyLocalInitiativeOverrideToGraph(await deps.buildMissionControlGraph(initiativeId));
748
+ const graph = asRecord(graphRaw);
749
+ const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
750
+ const tasksById = new Map();
751
+ const milestoneTitleById = new Map();
752
+ for (const nodeEntry of nodes) {
753
+ const node = asRecord(nodeEntry);
754
+ if (!node)
755
+ continue;
756
+ const id = asString(node.id);
757
+ const type = asString(node.type);
758
+ if (!id || !type)
759
+ continue;
760
+ if (type === "milestone") {
761
+ milestoneTitleById.set(id, asString(node.title) ?? id);
762
+ continue;
763
+ }
764
+ if (type !== "task")
765
+ continue;
766
+ tasksById.set(id, {
767
+ id,
768
+ title: asString(node.title) ?? id,
769
+ status: asString(node.status),
770
+ milestoneId: asString(node.milestoneId),
771
+ workstreamId: asString(node.workstreamId),
772
+ priorityNum: asNumber(node.priorityNum),
773
+ dueDate: asString(node.dueDate),
774
+ updatedAt: asString(node.updatedAt),
775
+ });
776
+ }
777
+ return {
778
+ tasksById,
779
+ milestoneTitleById,
780
+ };
781
+ }
1
782
  export function registerMissionControlReadRoutes(router, deps) {
783
+ // Handler registrations are process-local. Reset route caches so each newly
784
+ // constructed handler starts from a clean canonical cache/bypass state.
785
+ warmupByKey.clear();
786
+ canonicalReadCache.clear();
787
+ canonicalBypassState.clear();
788
+ const sendRouteError = (res, status, location, error, extra = {}) => {
789
+ deps.sendJson(res, status, {
790
+ ok: false,
791
+ error,
792
+ error_location: location,
793
+ ...extra,
794
+ });
795
+ };
796
+ const sendRouteException = (res, location, err) => {
797
+ sendRouteError(res, 500, location, deps.safeErrorMessage(err));
798
+ };
2
799
  async function renderAutoContinueStatus(query, res) {
3
800
  const initiativeId = query.get("initiative_id") ?? query.get("initiativeId") ?? "";
4
801
  const id = initiativeId.trim();
5
802
  if (!id) {
6
- deps.sendJson(res, 400, {
7
- ok: false,
8
- error: "Query parameter 'initiative_id' is required.",
9
- });
803
+ sendRouteError(res, 400, "mission-control.read.auto-continue.status.validation", "Query parameter 'initiative_id' is required.");
10
804
  return;
11
805
  }
12
806
  const run = deps.autoContinueRuns.get(id) ?? null;
@@ -16,6 +810,9 @@ export function registerMissionControlReadRoutes(router, deps) {
16
810
  run,
17
811
  defaults: {
18
812
  tokenBudget: deps.defaultAutoContinueTokenBudget(),
813
+ maxParallelSlices: typeof deps.defaultAutoContinueMaxParallelSlices === "function"
814
+ ? deps.defaultAutoContinueMaxParallelSlices()
815
+ : 1,
19
816
  tickMs: deps.autoContinueTickMs,
20
817
  },
21
818
  });
@@ -23,9 +820,7 @@ export function registerMissionControlReadRoutes(router, deps) {
23
820
  async function renderMissionControlGraph(query, res) {
24
821
  const initiativeId = query.get("initiative_id") ?? query.get("initiativeId");
25
822
  if (!initiativeId || initiativeId.trim().length === 0) {
26
- deps.sendJson(res, 400, {
27
- error: "Query parameter 'initiative_id' is required.",
28
- });
823
+ sendRouteError(res, 400, "mission-control.read.graph.validation", "Query parameter 'initiative_id' is required.");
29
824
  return;
30
825
  }
31
826
  try {
@@ -33,35 +828,878 @@ export function registerMissionControlReadRoutes(router, deps) {
33
828
  deps.sendJson(res, 200, graph);
34
829
  }
35
830
  catch (err) {
36
- deps.sendJson(res, 500, {
37
- error: deps.safeErrorMessage(err),
38
- });
831
+ sendRouteException(res, "mission-control.read.graph.handler", err);
39
832
  }
40
833
  }
41
- async function renderNextUpQueue(query, res) {
834
+ async function renderNextUpQueue(query, res, headerScope) {
42
835
  const initiativeIdRaw = query.get("initiative_id") ?? query.get("initiativeId") ?? "";
43
836
  const initiativeId = initiativeIdRaw.trim() || null;
837
+ const scope = resolveWorkspaceScope(query, headerScope, {
838
+ allowProjectScope: false,
839
+ });
840
+ if (scope.error) {
841
+ sendRouteError(res, 400, "mission-control.read.next-up.validation", scope.error);
842
+ return;
843
+ }
844
+ const projectId = scope.workspaceId;
845
+ const useAllScope = scope.isAll === true;
846
+ const includeCompleted = parseBoolean(query.get("include_completed"));
847
+ const offset = parsePositiveInt(query.get("cursor") ?? query.get("offset"), 0, 100_000);
848
+ const pageSize = parsePositiveInt(query.get("page_size") ?? query.get("pageSize") ?? query.get("limit"), NEXT_UP_DEFAULT_PAGE_SIZE, 300);
849
+ const requestedSliceLevelContext = query.get("slice_level_context") ?? query.get("sliceLevelContext");
850
+ const requestedMixPolicy = query.get("mix_policy") ?? query.get("mixPolicy");
851
+ const requestedOrderMode = query.get("order_mode") ?? query.get("orderMode");
852
+ const includeLineage = parseBoolean(query.get("include_lineage") ?? query.get("includeLineage"));
853
+ // Noise reduction params — suppress blocked/idle queue items by severity.
854
+ // noise_threshold: 'low' (show all) | 'medium' (default, hide low-severity blocked) | 'high' (only critical/high blocked)
855
+ const noiseThresholdRaw = query.get("noise_threshold") ?? query.get("noiseThreshold");
856
+ const noiseThreshold = noiseThresholdRaw === "low" || noiseThresholdRaw === "high"
857
+ ? noiseThresholdRaw
858
+ : "medium";
859
+ // dedup_window: time window in ms for grouping duplicate blocked items (default: 60000)
860
+ const dedupWindowRaw = query.get("dedup_window") ?? query.get("dedupWindow");
861
+ const dedupWindowMs = dedupWindowRaw != null
862
+ ? Math.max(0, parseInt(dedupWindowRaw, 10) || 60000)
863
+ : 60000;
864
+ const nextUpCanonicalCacheKey = canonicalReadCacheKey({
865
+ route: "next-up",
866
+ workspaceId: projectId,
867
+ scopeMode: useAllScope ? "all" : projectId ? "scoped" : "implicit",
868
+ initiativeId,
869
+ includeCompleted,
870
+ offset,
871
+ limit: pageSize,
872
+ scope: requestedSliceLevelContext,
873
+ order: requestedOrderMode,
874
+ mixPolicy: requestedMixPolicy,
875
+ noiseThreshold,
876
+ dedupWindowMs,
877
+ search: includeLineage ? "lineage:1" : null,
878
+ });
879
+ const cachedCanonicalNextUp = readCanonicalReadCache(nextUpCanonicalCacheKey, {
880
+ allowStale: false,
881
+ });
882
+ if (cachedCanonicalNextUp) {
883
+ deps.sendJson(res, 200, cachedCanonicalNextUp);
884
+ return;
885
+ }
886
+ const canonicalBypass = readCanonicalBypass("next-up");
887
+ const staleCanonicalForBypass = canonicalBypass
888
+ ? readCanonicalReadCache(nextUpCanonicalCacheKey, { allowStale: true })
889
+ : null;
890
+ const bypassAllScopeUnsupported = Boolean(useAllScope &&
891
+ canonicalBypass &&
892
+ canonicalBypass.reason.toLowerCase().includes("all-workspaces"));
893
+ const honorCanonicalBypass = Boolean(canonicalBypass && (staleCanonicalForBypass || bypassAllScopeUnsupported));
894
+ let canonicalFallbackReason = honorCanonicalBypass
895
+ ? `canonical next-up bypassed (${canonicalBypass?.reason ?? "unavailable"})`
896
+ : null;
897
+ if (honorCanonicalBypass && staleCanonicalForBypass) {
898
+ const staleDegraded = dedupeStrings([
899
+ ...asStringArray(staleCanonicalForBypass.degraded),
900
+ "Using cached canonical queue while sync recovers.",
901
+ ]);
902
+ deps.sendJson(res, 200, {
903
+ ...staleCanonicalForBypass,
904
+ degraded: staleDegraded,
905
+ source: "canonical_cache_stale",
906
+ });
907
+ return;
908
+ }
909
+ if (deps.rawRequest && !honorCanonicalBypass) {
910
+ try {
911
+ const params = new URLSearchParams();
912
+ if (initiativeId)
913
+ params.set("initiative_id", initiativeId);
914
+ if (projectId) {
915
+ params.set("workspace_id", projectId);
916
+ params.set("command_center_id", projectId);
917
+ }
918
+ else if (useAllScope) {
919
+ params.set("workspace_id", "all");
920
+ params.set("command_center_id", "all");
921
+ }
922
+ params.set("offset", String(offset));
923
+ params.set("limit", String(pageSize));
924
+ params.set("include_completed", includeCompleted ? "1" : "0");
925
+ if (requestedSliceLevelContext) {
926
+ params.set("slice_level_context", requestedSliceLevelContext);
927
+ }
928
+ if (requestedMixPolicy)
929
+ params.set("mix_policy", requestedMixPolicy);
930
+ if (requestedOrderMode)
931
+ params.set("order_mode", requestedOrderMode);
932
+ if (includeLineage)
933
+ params.set("include_lineage", "1");
934
+ params.set("noise_threshold", noiseThreshold);
935
+ params.set("dedup_window", String(dedupWindowMs));
936
+ const canonical = await requestCanonicalWithLegacyFallback(deps, {
937
+ timeoutMs: CANONICAL_NEXT_UP_TIMEOUT_MS,
938
+ label: "canonical next-up",
939
+ modernPath: `/api/client/mission-control/next-up?${params.toString()}`,
940
+ legacyPath: `/api/mission-control/next-up?${params.toString()}`,
941
+ });
942
+ const canonicalRecord = asRecord(canonical);
943
+ if (!canonicalRecord || !Array.isArray(canonicalRecord.items)) {
944
+ throw new Error("invalid canonical next-up payload");
945
+ }
946
+ if (isCanonicalAllScopeMismatch(canonicalRecord, useAllScope)) {
947
+ throw new Error("canonical next-up all-workspaces scope mismatch");
948
+ }
949
+ const canonicalItems = applyNextUpNoiseAndDedup(normalizeQueueItems(canonicalRecord.items).filter((item) => includeCompleted ? true : item.queueState !== "completed"), noiseThreshold, dedupWindowMs);
950
+ const canonicalTotal = Math.max(canonicalItems.length, Math.floor(asNumber(canonicalRecord.total) ?? canonicalItems.length)) ?? canonicalItems.length;
951
+ const canonicalPagination = parsePaginationEnvelope(canonicalRecord.pagination, {
952
+ offset,
953
+ limit: pageSize,
954
+ total: canonicalTotal,
955
+ });
956
+ const shouldRepaginateCanonically = canonicalItems.length > pageSize ||
957
+ canonicalPagination.offset !== offset ||
958
+ canonicalPagination.limit !== pageSize;
959
+ const paged = shouldRepaginateCanonically
960
+ ? applySliceSearchAndPagination({
961
+ items: canonicalItems,
962
+ searchTerm: "",
963
+ offset,
964
+ limit: pageSize,
965
+ })
966
+ : null;
967
+ const degraded = dedupeStrings(asStringArray(canonicalRecord.degraded));
968
+ const responsePayload = {
969
+ ok: true,
970
+ generatedAt: asString(canonicalRecord.generatedAt) ?? new Date().toISOString(),
971
+ total: paged ? paged.filtered.length : canonicalPagination.total,
972
+ items: paged ? paged.paged : canonicalItems,
973
+ pagination: paged ? paged.pagination : canonicalPagination,
974
+ source: "canonical",
975
+ degraded,
976
+ };
977
+ writeCanonicalReadCache(nextUpCanonicalCacheKey, responsePayload);
978
+ deps.sendJson(res, 200, responsePayload);
979
+ const paginationForWarmup = paged ? paged.pagination : canonicalPagination;
980
+ if (paginationForWarmup.hasMore && paginationForWarmup.nextCursor && shouldRunWarmup(`next-up:${projectId ?? "__all__"}:${initiativeId ?? "__all__"}:${paginationForWarmup.nextCursor}:${pageSize}`)) {
981
+ const nextOffset = parsePositiveInt(paginationForWarmup.nextCursor, offset + pageSize, 100_000);
982
+ const warmParams = new URLSearchParams(params);
983
+ warmParams.set("offset", String(nextOffset));
984
+ warmParams.set("limit", String(pageSize));
985
+ void requestCanonicalWithLegacyFallback(deps, {
986
+ timeoutMs: CANONICAL_NEXT_UP_TIMEOUT_MS,
987
+ label: "canonical next-up warmup",
988
+ modernPath: `/api/client/mission-control/next-up?${warmParams.toString()}`,
989
+ legacyPath: `/api/mission-control/next-up?${warmParams.toString()}`,
990
+ }).catch(() => undefined);
991
+ }
992
+ if (offset === 0 &&
993
+ shouldRunWarmup(`next-up->slices:${projectId ?? "__all__"}:${initiativeId ?? "__all__"}:${pageSize}`)) {
994
+ const warmSlicesParams = new URLSearchParams();
995
+ if (initiativeId)
996
+ warmSlicesParams.set("initiative_id", initiativeId);
997
+ if (projectId) {
998
+ warmSlicesParams.set("workspace_id", projectId);
999
+ warmSlicesParams.set("command_center_id", projectId);
1000
+ }
1001
+ else if (useAllScope) {
1002
+ warmSlicesParams.set("workspace_id", "all");
1003
+ warmSlicesParams.set("command_center_id", "all");
1004
+ }
1005
+ warmSlicesParams.set("level", "initiative");
1006
+ warmSlicesParams.set("include_completed", includeCompleted ? "1" : "0");
1007
+ warmSlicesParams.set("offset", "0");
1008
+ warmSlicesParams.set("limit", String(Math.max(SLICES_DEFAULT_PAGE_SIZE, Math.min(pageSize, 300))));
1009
+ void requestCanonicalWithLegacyFallback(deps, {
1010
+ timeoutMs: CANONICAL_SLICES_TIMEOUT_MS,
1011
+ label: "canonical slices warmup",
1012
+ modernPath: `/api/client/mission-control/slices?${warmSlicesParams.toString()}`,
1013
+ legacyPath: `/api/mission-control/slices?${warmSlicesParams.toString()}`,
1014
+ }).catch(() => undefined);
1015
+ }
1016
+ return;
1017
+ }
1018
+ catch (err) {
1019
+ if (isCanonicalAuthFailure(err)) {
1020
+ setCanonicalBypass("next-up", "authentication unavailable", CANONICAL_AUTH_BYPASS_MS);
1021
+ }
1022
+ else if (isCanonicalAllScopeMismatchError(err)) {
1023
+ setCanonicalBypass("next-up", "all-workspaces unsupported", Math.max(CANONICAL_AUTH_BYPASS_MS, 60_000));
1024
+ }
1025
+ const staleCanonical = readCanonicalReadCache(nextUpCanonicalCacheKey, {
1026
+ allowStale: true,
1027
+ });
1028
+ if (staleCanonical) {
1029
+ const staleDegraded = dedupeStrings([
1030
+ ...asStringArray(staleCanonical.degraded),
1031
+ "Using cached canonical queue while sync recovers.",
1032
+ ]);
1033
+ deps.sendJson(res, 200, {
1034
+ ...staleCanonical,
1035
+ degraded: staleDegraded,
1036
+ source: "canonical_cache_stale",
1037
+ });
1038
+ return;
1039
+ }
1040
+ canonicalFallbackReason = `canonical next-up unavailable (${deps.safeErrorMessage(err)})`;
1041
+ if (projectId || useAllScope) {
1042
+ try {
1043
+ const bridgeParams = new URLSearchParams();
1044
+ if (initiativeId)
1045
+ bridgeParams.set("initiative_id", initiativeId);
1046
+ if (projectId) {
1047
+ bridgeParams.set("workspace_id", projectId);
1048
+ bridgeParams.set("command_center_id", projectId);
1049
+ }
1050
+ else if (useAllScope) {
1051
+ bridgeParams.set("workspace_id", "all");
1052
+ bridgeParams.set("command_center_id", "all");
1053
+ }
1054
+ bridgeParams.set("level", "workstream");
1055
+ bridgeParams.set("offset", String(Math.max(0, offset)));
1056
+ bridgeParams.set("limit", String(Math.min(300, Math.max(pageSize, offset + pageSize))));
1057
+ bridgeParams.set("include_completed", includeCompleted ? "1" : "0");
1058
+ bridgeParams.set("mix_policy", requestedMixPolicy ?? "iwmt_v1");
1059
+ if (requestedOrderMode) {
1060
+ bridgeParams.set("order_mode", requestedOrderMode);
1061
+ }
1062
+ const canonicalSlices = await requestCanonicalWithLegacyFallback(deps, {
1063
+ timeoutMs: CANONICAL_SLICES_TIMEOUT_MS,
1064
+ label: "canonical slices bridge",
1065
+ modernPath: `/api/client/mission-control/slices?${bridgeParams.toString()}`,
1066
+ legacyPath: `/api/mission-control/slices?${bridgeParams.toString()}`,
1067
+ });
1068
+ const canonicalSlicesRecord = asRecord(canonicalSlices);
1069
+ if (!canonicalSlicesRecord || !Array.isArray(canonicalSlicesRecord.items)) {
1070
+ throw new Error("invalid canonical slices payload");
1071
+ }
1072
+ if (isCanonicalAllScopeMismatch(canonicalSlicesRecord, useAllScope)) {
1073
+ throw new Error("canonical slices all-workspaces scope mismatch");
1074
+ }
1075
+ const bridgedItems = applyNextUpNoiseAndDedup(mapCanonicalSlicesToQueueItems(canonicalSlicesRecord.items).filter((item) => includeCompleted ? true : item.queueState !== "completed"), noiseThreshold, dedupWindowMs);
1076
+ if (bridgedItems.length > 0) {
1077
+ const paged = applySliceSearchAndPagination({
1078
+ items: bridgedItems,
1079
+ searchTerm: "",
1080
+ offset,
1081
+ limit: pageSize,
1082
+ });
1083
+ const degraded = dedupeStrings([
1084
+ ...(Array.isArray(canonicalSlicesRecord.degraded)
1085
+ ? canonicalSlicesRecord.degraded
1086
+ : []),
1087
+ ...(canonicalFallbackReason ? [canonicalFallbackReason] : []),
1088
+ "Next Up derived from canonical slices.",
1089
+ ]);
1090
+ const responsePayload = {
1091
+ ok: true,
1092
+ generatedAt: asString(canonicalSlicesRecord.generatedAt) ??
1093
+ new Date().toISOString(),
1094
+ total: paged.filtered.length,
1095
+ items: paged.paged,
1096
+ pagination: paged.pagination,
1097
+ source: "canonical_slices_bridge",
1098
+ degraded,
1099
+ };
1100
+ writeCanonicalReadCache(nextUpCanonicalCacheKey, responsePayload);
1101
+ deps.sendJson(res, 200, responsePayload);
1102
+ return;
1103
+ }
1104
+ }
1105
+ catch (bridgeErr) {
1106
+ canonicalFallbackReason = dedupeStrings([
1107
+ canonicalFallbackReason ?? "",
1108
+ `canonical slices bridge unavailable (${deps.safeErrorMessage(bridgeErr)})`,
1109
+ ]).join(" | ");
1110
+ }
1111
+ }
1112
+ // Continue to local fallback.
1113
+ try {
1114
+ const queue = await deps.buildNextUpQueue({
1115
+ initiativeId,
1116
+ projectId,
1117
+ });
1118
+ const items = applyNextUpNoiseAndDedup(normalizeQueueItems(queue.items ?? []).filter((item) => includeCompleted ? true : item.queueState !== "completed"), noiseThreshold, dedupWindowMs);
1119
+ const paged = applySliceSearchAndPagination({
1120
+ items,
1121
+ searchTerm: "",
1122
+ offset,
1123
+ limit: pageSize,
1124
+ });
1125
+ const degraded = dedupeStrings([
1126
+ ...(Array.isArray(queue.degraded) ? queue.degraded : []),
1127
+ ...(canonicalFallbackReason ? [canonicalFallbackReason] : []),
1128
+ ]);
1129
+ deps.sendJson(res, 200, {
1130
+ ok: true,
1131
+ generatedAt: new Date().toISOString(),
1132
+ total: paged.filtered.length,
1133
+ items: paged.paged,
1134
+ pagination: paged.pagination,
1135
+ source: "local_fallback",
1136
+ degraded,
1137
+ });
1138
+ return;
1139
+ }
1140
+ catch (fallbackErr) {
1141
+ sendRouteException(res, "mission-control.read.next-up.handler", fallbackErr);
1142
+ return;
1143
+ }
1144
+ }
1145
+ }
44
1146
  try {
45
- const queue = await deps.buildNextUpQueue({ initiativeId });
1147
+ const queue = await deps.buildNextUpQueue({
1148
+ initiativeId,
1149
+ projectId,
1150
+ });
1151
+ const items = applyNextUpNoiseAndDedup(normalizeQueueItems(queue.items ?? []).filter((item) => includeCompleted ? true : item.queueState !== "completed"), noiseThreshold, dedupWindowMs);
1152
+ const paged = applySliceSearchAndPagination({
1153
+ items,
1154
+ searchTerm: "",
1155
+ offset,
1156
+ limit: pageSize,
1157
+ });
1158
+ const degraded = dedupeStrings([
1159
+ ...(Array.isArray(queue.degraded) ? queue.degraded : []),
1160
+ ...(canonicalFallbackReason ? [canonicalFallbackReason] : []),
1161
+ ]);
46
1162
  deps.sendJson(res, 200, {
47
1163
  ok: true,
48
1164
  generatedAt: new Date().toISOString(),
49
- total: queue.items.length,
50
- items: queue.items,
51
- degraded: queue.degraded,
1165
+ total: paged.filtered.length,
1166
+ items: paged.paged,
1167
+ pagination: paged.pagination,
1168
+ source: "local",
1169
+ degraded: dedupeStrings(degraded),
52
1170
  });
53
1171
  }
54
1172
  catch (err) {
55
- deps.sendJson(res, 500, {
56
- ok: false,
57
- error: deps.safeErrorMessage(err),
1173
+ sendRouteException(res, "mission-control.read.next-up.handler", err);
1174
+ }
1175
+ }
1176
+ async function renderSliceProjection(query, res, headerScope) {
1177
+ const initiativeIdRaw = query.get("initiative_id") ?? query.get("initiativeId") ?? "";
1178
+ const initiativeId = initiativeIdRaw.trim() || null;
1179
+ const workspaceScope = resolveWorkspaceScope(query, headerScope, {
1180
+ allowProjectScope: false,
1181
+ });
1182
+ if (workspaceScope.error) {
1183
+ sendRouteError(res, 400, "mission-control.read.slices.validation", workspaceScope.error);
1184
+ return;
1185
+ }
1186
+ const projectId = workspaceScope.workspaceId;
1187
+ const useAllScope = workspaceScope.isAll === true;
1188
+ const includeCompleted = parseBoolean(query.get("include_completed"));
1189
+ const sliceScope = parseSliceScope(query.get("scope") ?? query.get("level"));
1190
+ const order = parseSliceOrder(query.get("order"));
1191
+ const searchTerm = normalizeSliceSearchTerm(query.get("q") ?? query.get("search"));
1192
+ const offset = parsePositiveInt(query.get("cursor") ?? query.get("offset"), 0, 100_000);
1193
+ const pageSize = parsePositiveInt(query.get("page_size") ?? query.get("pageSize") ?? query.get("limit"), SLICES_DEFAULT_PAGE_SIZE, 300);
1194
+ const requestedMixPolicy = query.get("mix_policy") ?? query.get("mixPolicy") ?? "iwmt_v1";
1195
+ const requestedOrderMode = query.get("order_mode") ?? query.get("orderMode");
1196
+ const slicesCanonicalCacheKey = canonicalReadCacheKey({
1197
+ route: "slices",
1198
+ workspaceId: projectId,
1199
+ scopeMode: useAllScope ? "all" : projectId ? "scoped" : "implicit",
1200
+ initiativeId,
1201
+ includeCompleted,
1202
+ offset,
1203
+ limit: pageSize,
1204
+ scope: sliceScope,
1205
+ order: requestedOrderMode ?? order,
1206
+ mixPolicy: requestedMixPolicy,
1207
+ search: searchTerm || null,
1208
+ });
1209
+ const cachedCanonicalSlices = readCanonicalReadCache(slicesCanonicalCacheKey, {
1210
+ allowStale: false,
1211
+ });
1212
+ if (cachedCanonicalSlices) {
1213
+ deps.sendJson(res, 200, cachedCanonicalSlices);
1214
+ return;
1215
+ }
1216
+ const canonicalBypass = readCanonicalBypass("slices");
1217
+ const staleCanonicalForBypass = canonicalBypass
1218
+ ? readCanonicalReadCache(slicesCanonicalCacheKey, { allowStale: true })
1219
+ : null;
1220
+ const bypassAllScopeUnsupported = Boolean(useAllScope &&
1221
+ canonicalBypass &&
1222
+ canonicalBypass.reason.toLowerCase().includes("all-workspaces"));
1223
+ const honorCanonicalBypass = Boolean(canonicalBypass && (staleCanonicalForBypass || bypassAllScopeUnsupported));
1224
+ let canonicalFallbackReason = honorCanonicalBypass
1225
+ ? `canonical slices bypassed (${canonicalBypass?.reason ?? "unavailable"})`
1226
+ : null;
1227
+ if (honorCanonicalBypass && staleCanonicalForBypass) {
1228
+ const staleDegraded = dedupeStrings([
1229
+ ...asStringArray(staleCanonicalForBypass.degraded),
1230
+ "Using cached canonical slices while sync recovers.",
1231
+ ]);
1232
+ deps.sendJson(res, 200, {
1233
+ ...staleCanonicalForBypass,
1234
+ degraded: staleDegraded,
1235
+ source: "canonical_cache_stale",
58
1236
  });
1237
+ return;
1238
+ }
1239
+ if (deps.rawRequest && !honorCanonicalBypass) {
1240
+ try {
1241
+ const params = new URLSearchParams();
1242
+ if (initiativeId)
1243
+ params.set("initiative_id", initiativeId);
1244
+ if (projectId) {
1245
+ params.set("workspace_id", projectId);
1246
+ params.set("command_center_id", projectId);
1247
+ }
1248
+ else if (useAllScope) {
1249
+ params.set("workspace_id", "all");
1250
+ params.set("command_center_id", "all");
1251
+ }
1252
+ params.set("level", sliceScope);
1253
+ params.set("include_completed", includeCompleted ? "1" : "0");
1254
+ params.set("mix_policy", requestedMixPolicy);
1255
+ if (requestedOrderMode)
1256
+ params.set("order_mode", requestedOrderMode);
1257
+ const canonicalSupportsDirectPaging = searchTerm.length === 0;
1258
+ if (canonicalSupportsDirectPaging) {
1259
+ params.set("offset", String(offset));
1260
+ params.set("limit", String(pageSize));
1261
+ }
1262
+ else {
1263
+ params.set("offset", "0");
1264
+ params.set("limit", String(Math.min(300, Math.max(pageSize + offset, pageSize))));
1265
+ }
1266
+ const canonical = await requestCanonicalWithLegacyFallback(deps, {
1267
+ timeoutMs: CANONICAL_SLICES_TIMEOUT_MS,
1268
+ label: "canonical slices",
1269
+ modernPath: `/api/client/mission-control/slices?${params.toString()}`,
1270
+ legacyPath: `/api/mission-control/slices?${params.toString()}`,
1271
+ });
1272
+ const canonicalRecord = asRecord(canonical);
1273
+ if (!canonicalRecord || !Array.isArray(canonicalRecord.items)) {
1274
+ throw new Error("invalid canonical slices payload");
1275
+ }
1276
+ if (isCanonicalAllScopeMismatch(canonicalRecord, useAllScope)) {
1277
+ throw new Error("canonical slices all-workspaces scope mismatch");
1278
+ }
1279
+ const canonicalItems = canonicalRecord.items;
1280
+ const canonicalTotal = Math.max(canonicalItems.length, Math.floor(asNumber(canonicalRecord.total) ?? canonicalItems.length)) ?? canonicalItems.length;
1281
+ const canonicalPagination = parsePaginationEnvelope(canonicalRecord.pagination, {
1282
+ offset,
1283
+ limit: pageSize,
1284
+ total: canonicalTotal,
1285
+ });
1286
+ const shouldRepaginateCanonically = canonicalItems.length > pageSize ||
1287
+ canonicalPagination.offset !== offset ||
1288
+ canonicalPagination.limit !== pageSize;
1289
+ const canonicalPaged = shouldRepaginateCanonically
1290
+ ? applySliceSearchAndPagination({
1291
+ items: canonicalItems,
1292
+ searchTerm: "",
1293
+ offset,
1294
+ limit: pageSize,
1295
+ })
1296
+ : null;
1297
+ if (searchTerm.length === 0) {
1298
+ const responsePayload = {
1299
+ ...canonicalRecord,
1300
+ level: asString(canonicalRecord.level) ?? sliceScope,
1301
+ scope: asString(canonicalRecord.level) ?? sliceScope,
1302
+ order: asString(canonicalRecord.orderMode) ??
1303
+ asString(canonicalRecord.order) ??
1304
+ order,
1305
+ total: canonicalPaged ? canonicalPaged.filtered.length : canonicalPagination.total,
1306
+ items: canonicalPaged ? canonicalPaged.paged : canonicalItems,
1307
+ pagination: canonicalPaged ? canonicalPaged.pagination : canonicalPagination,
1308
+ source: "canonical",
1309
+ };
1310
+ writeCanonicalReadCache(slicesCanonicalCacheKey, responsePayload);
1311
+ deps.sendJson(res, 200, responsePayload);
1312
+ if (offset === 0 &&
1313
+ shouldRunWarmup(`slices->next-up:${projectId ?? "__all__"}:${initiativeId ?? "__all__"}:${pageSize}`)) {
1314
+ const warmNextUpParams = new URLSearchParams();
1315
+ if (initiativeId)
1316
+ warmNextUpParams.set("initiative_id", initiativeId);
1317
+ if (projectId) {
1318
+ warmNextUpParams.set("workspace_id", projectId);
1319
+ warmNextUpParams.set("command_center_id", projectId);
1320
+ }
1321
+ else if (useAllScope) {
1322
+ warmNextUpParams.set("workspace_id", "all");
1323
+ warmNextUpParams.set("command_center_id", "all");
1324
+ }
1325
+ warmNextUpParams.set("offset", "0");
1326
+ warmNextUpParams.set("limit", String(Math.max(NEXT_UP_DEFAULT_PAGE_SIZE, Math.min(pageSize, 300))));
1327
+ warmNextUpParams.set("include_completed", includeCompleted ? "1" : "0");
1328
+ void requestCanonicalWithLegacyFallback(deps, {
1329
+ timeoutMs: CANONICAL_NEXT_UP_TIMEOUT_MS,
1330
+ label: "canonical next-up warmup",
1331
+ modernPath: `/api/client/mission-control/next-up?${warmNextUpParams.toString()}`,
1332
+ legacyPath: `/api/mission-control/next-up?${warmNextUpParams.toString()}`,
1333
+ }).catch(() => undefined);
1334
+ }
1335
+ return;
1336
+ }
1337
+ const paged = applySliceSearchAndPagination({
1338
+ items: canonicalItems,
1339
+ searchTerm,
1340
+ offset,
1341
+ limit: pageSize,
1342
+ });
1343
+ const responsePayload = {
1344
+ ...canonicalRecord,
1345
+ level: asString(canonicalRecord.level) ?? sliceScope,
1346
+ scope: asString(canonicalRecord.level) ?? sliceScope,
1347
+ order: asString(canonicalRecord.orderMode) ??
1348
+ asString(canonicalRecord.order) ??
1349
+ order,
1350
+ total: paged.filtered.length,
1351
+ items: paged.paged,
1352
+ pagination: paged.pagination,
1353
+ source: "canonical",
1354
+ };
1355
+ writeCanonicalReadCache(slicesCanonicalCacheKey, responsePayload);
1356
+ deps.sendJson(res, 200, responsePayload);
1357
+ if (offset === 0 &&
1358
+ shouldRunWarmup(`slices->next-up:${projectId ?? "__all__"}:${initiativeId ?? "__all__"}:${pageSize}:search`)) {
1359
+ const warmNextUpParams = new URLSearchParams();
1360
+ if (initiativeId)
1361
+ warmNextUpParams.set("initiative_id", initiativeId);
1362
+ if (projectId) {
1363
+ warmNextUpParams.set("workspace_id", projectId);
1364
+ warmNextUpParams.set("command_center_id", projectId);
1365
+ }
1366
+ else if (useAllScope) {
1367
+ warmNextUpParams.set("workspace_id", "all");
1368
+ warmNextUpParams.set("command_center_id", "all");
1369
+ }
1370
+ warmNextUpParams.set("offset", "0");
1371
+ warmNextUpParams.set("limit", String(Math.max(NEXT_UP_DEFAULT_PAGE_SIZE, Math.min(pageSize, 300))));
1372
+ warmNextUpParams.set("include_completed", includeCompleted ? "1" : "0");
1373
+ void requestCanonicalWithLegacyFallback(deps, {
1374
+ timeoutMs: CANONICAL_NEXT_UP_TIMEOUT_MS,
1375
+ label: "canonical next-up warmup",
1376
+ modernPath: `/api/client/mission-control/next-up?${warmNextUpParams.toString()}`,
1377
+ legacyPath: `/api/mission-control/next-up?${warmNextUpParams.toString()}`,
1378
+ }).catch(() => undefined);
1379
+ }
1380
+ return;
1381
+ }
1382
+ catch (err) {
1383
+ if (isCanonicalAuthFailure(err)) {
1384
+ setCanonicalBypass("slices", "authentication unavailable", CANONICAL_AUTH_BYPASS_MS);
1385
+ }
1386
+ else if (isCanonicalAllScopeMismatchError(err)) {
1387
+ setCanonicalBypass("slices", "all-workspaces unsupported", Math.max(CANONICAL_AUTH_BYPASS_MS, 60_000));
1388
+ }
1389
+ const staleCanonical = readCanonicalReadCache(slicesCanonicalCacheKey, {
1390
+ allowStale: true,
1391
+ });
1392
+ if (staleCanonical) {
1393
+ const staleDegraded = dedupeStrings([
1394
+ ...asStringArray(staleCanonical.degraded),
1395
+ "Using cached canonical slices while sync recovers.",
1396
+ ]);
1397
+ deps.sendJson(res, 200, {
1398
+ ...staleCanonical,
1399
+ degraded: staleDegraded,
1400
+ source: "canonical_cache_stale",
1401
+ });
1402
+ return;
1403
+ }
1404
+ canonicalFallbackReason = `canonical slices unavailable (${deps.safeErrorMessage(err)})`;
1405
+ }
1406
+ }
1407
+ try {
1408
+ const queue = await deps.buildNextUpQueue({
1409
+ initiativeId,
1410
+ projectId,
1411
+ });
1412
+ const queueItems = normalizeQueueItems(queue.items ?? []).filter((item) => includeCompleted ? true : item.queueState !== "completed");
1413
+ const graphIndexByInitiative = new Map();
1414
+ const degraded = dedupeStrings([
1415
+ ...(Array.isArray(queue.degraded) ? queue.degraded : []),
1416
+ ...(canonicalFallbackReason ? [canonicalFallbackReason] : []),
1417
+ ]);
1418
+ if (sliceScope === "milestone" || sliceScope === "task") {
1419
+ const uniqueInitiatives = dedupeStrings(queueItems.map((item) => item.initiativeId));
1420
+ for (const id of uniqueInitiatives) {
1421
+ try {
1422
+ graphIndexByInitiative.set(id, await loadInitiativeGraphIndex(deps, id));
1423
+ }
1424
+ catch (err) {
1425
+ degraded.push(`graph unavailable for ${id} (${deps.safeErrorMessage(err)})`);
1426
+ }
1427
+ }
1428
+ }
1429
+ const slices = [];
1430
+ if (sliceScope === "initiative") {
1431
+ const grouped = new Map();
1432
+ queueItems.forEach((item, index) => {
1433
+ const bucket = grouped.get(item.initiativeId);
1434
+ if (!bucket) {
1435
+ grouped.set(item.initiativeId, {
1436
+ base: item,
1437
+ states: [item.queueState],
1438
+ taskIds: new Set(item.sliceTaskIds ?? []),
1439
+ workstreamIds: new Set([item.workstreamId]),
1440
+ runnerAgents: item.runnerAgents ?? [],
1441
+ iwmtRank: index,
1442
+ });
1443
+ return;
1444
+ }
1445
+ bucket.states.push(item.queueState);
1446
+ for (const taskId of item.sliceTaskIds ?? [])
1447
+ bucket.taskIds.add(taskId);
1448
+ bucket.workstreamIds.add(item.workstreamId);
1449
+ bucket.runnerAgents = mergeRunnerAgents(bucket.runnerAgents, item.runnerAgents ?? []);
1450
+ if (index < bucket.iwmtRank) {
1451
+ bucket.base = item;
1452
+ bucket.iwmtRank = index;
1453
+ }
1454
+ });
1455
+ for (const [initiativeKey, bucket] of grouped.entries()) {
1456
+ const runnerAgents = mergeRunnerAgents(bucket.runnerAgents, bucket.base.runnerAgents ?? []);
1457
+ const runnerPrimary = runnerAgents[0] ?? null;
1458
+ slices.push({
1459
+ id: initiativeKey,
1460
+ scope: sliceScope,
1461
+ initiativeId: bucket.base.initiativeId,
1462
+ initiativeTitle: bucket.base.initiativeTitle,
1463
+ workstreamId: null,
1464
+ workstreamTitle: null,
1465
+ milestoneId: null,
1466
+ milestoneTitle: null,
1467
+ taskId: null,
1468
+ taskTitle: null,
1469
+ queueState: combinedQueueState(bucket.states),
1470
+ sourceWorkstreamIds: Array.from(bucket.workstreamIds.values()),
1471
+ runnerAgentId: runnerPrimary?.id ?? null,
1472
+ runnerAgentName: runnerPrimary?.name ?? "Unassigned",
1473
+ runnerAgents,
1474
+ runnerSource: bucket.base.runnerSource ??
1475
+ (runnerPrimary ? "inferred" : "fallback"),
1476
+ nextTaskId: bucket.base.nextTaskId,
1477
+ nextTaskTitle: bucket.base.nextTaskTitle,
1478
+ nextTaskPriority: bucket.base.nextTaskPriority,
1479
+ nextTaskDueAt: bucket.base.nextTaskDueAt,
1480
+ updatedAt: bucket.base.updatedAt ?? null,
1481
+ sliceTaskIds: Array.from(bucket.taskIds.values()),
1482
+ sliceTaskCount: bucket.taskIds.size,
1483
+ compositeScore: bucket.base.compositeScore,
1484
+ scoringTier: bucket.base.scoringTier,
1485
+ iwmtRank: bucket.iwmtRank,
1486
+ });
1487
+ }
1488
+ }
1489
+ else if (sliceScope === "workstream") {
1490
+ queueItems.forEach((item, index) => {
1491
+ const runnerAgents = mergeRunnerAgents(item.runnerAgents ?? []);
1492
+ const runnerPrimary = runnerAgents[0] ?? null;
1493
+ slices.push({
1494
+ id: `${item.initiativeId}:${item.workstreamId}`,
1495
+ scope: sliceScope,
1496
+ initiativeId: item.initiativeId,
1497
+ initiativeTitle: item.initiativeTitle,
1498
+ workstreamId: item.workstreamId,
1499
+ workstreamTitle: item.workstreamTitle,
1500
+ milestoneId: item.sliceMilestoneId ?? item.nextTaskMilestoneId ?? null,
1501
+ milestoneTitle: null,
1502
+ taskId: null,
1503
+ taskTitle: null,
1504
+ queueState: item.queueState,
1505
+ sourceWorkstreamIds: [item.workstreamId],
1506
+ runnerAgentId: runnerPrimary?.id ?? null,
1507
+ runnerAgentName: runnerPrimary?.name ?? "Unassigned",
1508
+ runnerAgents,
1509
+ runnerSource: item.runnerSource ?? (runnerPrimary ? "inferred" : "fallback"),
1510
+ nextTaskId: item.nextTaskId,
1511
+ nextTaskTitle: item.nextTaskTitle,
1512
+ nextTaskPriority: item.nextTaskPriority,
1513
+ nextTaskDueAt: item.nextTaskDueAt,
1514
+ updatedAt: item.updatedAt ?? null,
1515
+ sliceTaskIds: dedupeStrings(item.sliceTaskIds ?? []),
1516
+ sliceTaskCount: typeof item.sliceTaskCount === "number"
1517
+ ? Math.max(0, Math.floor(item.sliceTaskCount))
1518
+ : (item.sliceTaskIds ?? []).length,
1519
+ compositeScore: item.compositeScore,
1520
+ scoringTier: item.scoringTier,
1521
+ iwmtRank: index,
1522
+ });
1523
+ });
1524
+ }
1525
+ else if (sliceScope === "milestone") {
1526
+ const grouped = new Map();
1527
+ queueItems.forEach((item, index) => {
1528
+ const graphIndex = graphIndexByInitiative.get(item.initiativeId) ?? null;
1529
+ const selectedTaskIds = dedupeStrings([
1530
+ ...(item.sliceTaskIds ?? []),
1531
+ ...(item.nextTaskId ? [item.nextTaskId] : []),
1532
+ ]);
1533
+ if (selectedTaskIds.length === 0)
1534
+ return;
1535
+ const taskBuckets = new Map();
1536
+ for (const taskId of selectedTaskIds) {
1537
+ const task = graphIndex?.tasksById.get(taskId) ?? null;
1538
+ if (!includeCompleted && isDoneStatus(task?.status ?? null))
1539
+ continue;
1540
+ const milestoneId = task?.milestoneId ??
1541
+ item.sliceMilestoneId ??
1542
+ item.nextTaskMilestoneId ??
1543
+ null;
1544
+ const bucketKey = milestoneId ?? "__none__";
1545
+ const bucket = taskBuckets.get(bucketKey) ?? {
1546
+ milestoneId,
1547
+ taskIds: [],
1548
+ };
1549
+ bucket.taskIds.push(taskId);
1550
+ taskBuckets.set(bucketKey, bucket);
1551
+ }
1552
+ for (const [bucketKey, bucket] of taskBuckets.entries()) {
1553
+ const scopedKey = `${item.initiativeId}:${item.workstreamId}:${bucketKey}`;
1554
+ const existing = grouped.get(scopedKey);
1555
+ if (!existing) {
1556
+ grouped.set(scopedKey, {
1557
+ base: item,
1558
+ milestoneId: bucket.milestoneId,
1559
+ milestoneTitle: (bucket.milestoneId
1560
+ ? graphIndex?.milestoneTitleById.get(bucket.milestoneId)
1561
+ : null) ?? null,
1562
+ taskIds: new Set(bucket.taskIds),
1563
+ iwmtRank: index,
1564
+ });
1565
+ continue;
1566
+ }
1567
+ for (const taskId of bucket.taskIds)
1568
+ existing.taskIds.add(taskId);
1569
+ if (index < existing.iwmtRank) {
1570
+ existing.base = item;
1571
+ existing.iwmtRank = index;
1572
+ }
1573
+ }
1574
+ });
1575
+ for (const [id, bucket] of grouped.entries()) {
1576
+ const runnerAgents = mergeRunnerAgents(bucket.base.runnerAgents ?? []);
1577
+ const runnerPrimary = runnerAgents[0] ?? null;
1578
+ slices.push({
1579
+ id,
1580
+ scope: sliceScope,
1581
+ initiativeId: bucket.base.initiativeId,
1582
+ initiativeTitle: bucket.base.initiativeTitle,
1583
+ workstreamId: bucket.base.workstreamId,
1584
+ workstreamTitle: bucket.base.workstreamTitle,
1585
+ milestoneId: bucket.milestoneId,
1586
+ milestoneTitle: bucket.milestoneTitle,
1587
+ taskId: null,
1588
+ taskTitle: null,
1589
+ queueState: bucket.base.queueState,
1590
+ sourceWorkstreamIds: [bucket.base.workstreamId],
1591
+ runnerAgentId: runnerPrimary?.id ?? null,
1592
+ runnerAgentName: runnerPrimary?.name ?? "Unassigned",
1593
+ runnerAgents,
1594
+ runnerSource: bucket.base.runnerSource ?? (runnerPrimary ? "inferred" : "fallback"),
1595
+ nextTaskId: bucket.base.nextTaskId,
1596
+ nextTaskTitle: bucket.base.nextTaskTitle,
1597
+ nextTaskPriority: bucket.base.nextTaskPriority,
1598
+ nextTaskDueAt: bucket.base.nextTaskDueAt,
1599
+ updatedAt: bucket.base.updatedAt ?? null,
1600
+ sliceTaskIds: Array.from(bucket.taskIds.values()),
1601
+ sliceTaskCount: bucket.taskIds.size,
1602
+ compositeScore: bucket.base.compositeScore,
1603
+ scoringTier: bucket.base.scoringTier,
1604
+ iwmtRank: bucket.iwmtRank,
1605
+ });
1606
+ }
1607
+ }
1608
+ else {
1609
+ queueItems.forEach((item, index) => {
1610
+ const graphIndex = graphIndexByInitiative.get(item.initiativeId) ?? null;
1611
+ const selectedTaskIds = dedupeStrings([
1612
+ ...(item.sliceTaskIds ?? []),
1613
+ ...(item.nextTaskId ? [item.nextTaskId] : []),
1614
+ ]);
1615
+ for (const taskId of selectedTaskIds) {
1616
+ const task = graphIndex?.tasksById.get(taskId) ?? null;
1617
+ if (!includeCompleted && isDoneStatus(task?.status ?? null))
1618
+ continue;
1619
+ const taskTitle = task?.title ??
1620
+ (taskId === item.nextTaskId ? item.nextTaskTitle : null) ??
1621
+ taskId;
1622
+ slices.push({
1623
+ id: `${item.initiativeId}:${item.workstreamId}:${taskId}`,
1624
+ scope: sliceScope,
1625
+ initiativeId: item.initiativeId,
1626
+ initiativeTitle: item.initiativeTitle,
1627
+ workstreamId: item.workstreamId,
1628
+ workstreamTitle: item.workstreamTitle,
1629
+ milestoneId: task?.milestoneId ??
1630
+ item.sliceMilestoneId ??
1631
+ item.nextTaskMilestoneId ??
1632
+ null,
1633
+ milestoneTitle: task?.milestoneId
1634
+ ? graphIndex?.milestoneTitleById.get(task.milestoneId) ?? null
1635
+ : null,
1636
+ taskId,
1637
+ taskTitle,
1638
+ queueState: isDoneStatus(task?.status ?? null) ? "completed" : item.queueState,
1639
+ sourceWorkstreamIds: [item.workstreamId],
1640
+ runnerAgentId: (item.runnerAgents ?? [])[0]?.id ?? item.runnerAgentId ?? null,
1641
+ runnerAgentName: (item.runnerAgents ?? [])[0]?.name ?? item.runnerAgentName ?? "Unassigned",
1642
+ runnerAgents: mergeRunnerAgents(item.runnerAgents ?? []),
1643
+ runnerSource: item.runnerSource ??
1644
+ ((item.runnerAgents ?? [])[0] ? "inferred" : "fallback"),
1645
+ nextTaskId: item.nextTaskId,
1646
+ nextTaskTitle: item.nextTaskTitle,
1647
+ nextTaskPriority: task?.priorityNum ?? item.nextTaskPriority,
1648
+ nextTaskDueAt: task?.dueDate ?? item.nextTaskDueAt,
1649
+ updatedAt: task?.updatedAt ?? item.updatedAt ?? null,
1650
+ sliceTaskIds: [taskId],
1651
+ sliceTaskCount: 1,
1652
+ compositeScore: item.compositeScore,
1653
+ scoringTier: item.scoringTier,
1654
+ iwmtRank: index,
1655
+ });
1656
+ }
1657
+ });
1658
+ }
1659
+ const sorted = sortSlices(slices, order);
1660
+ const paged = applySliceSearchAndPagination({
1661
+ items: sorted,
1662
+ searchTerm,
1663
+ offset,
1664
+ limit: pageSize,
1665
+ });
1666
+ deps.sendJson(res, 200, {
1667
+ ok: true,
1668
+ generatedAt: new Date().toISOString(),
1669
+ level: sliceScope,
1670
+ scope: sliceScope,
1671
+ order,
1672
+ includeCompleted,
1673
+ total: paged.filtered.length,
1674
+ items: paged.paged,
1675
+ pagination: paged.pagination,
1676
+ source: canonicalFallbackReason ? "local_fallback" : "local",
1677
+ degraded: degraded.length > 0 ? dedupeStrings(degraded) : undefined,
1678
+ });
1679
+ }
1680
+ catch (err) {
1681
+ sendRouteException(res, "mission-control.read.slices.handler", err);
59
1682
  }
60
1683
  }
1684
+ async function renderSentinelCatalog(query, res) {
1685
+ const domain = query.get("domain");
1686
+ const signal = query.get("signal");
1687
+ const items = listBuiltInSentinels({ domain, signal });
1688
+ deps.sendJson(res, 200, {
1689
+ ok: true,
1690
+ generatedAt: new Date().toISOString(),
1691
+ total: items.length,
1692
+ items,
1693
+ });
1694
+ }
61
1695
  router.add("GET", "mission-control/auto-continue/status", async ({ query, res }) => renderAutoContinueStatus(query, res), "Get auto-continue status for an initiative");
62
1696
  router.add("HEAD", "mission-control/auto-continue/status", async ({ query, res }) => renderAutoContinueStatus(query, res), "Get auto-continue status for an initiative (HEAD)");
63
1697
  router.add("GET", "mission-control/graph", async ({ query, res }) => renderMissionControlGraph(query, res), "Get mission-control dependency graph");
64
1698
  router.add("HEAD", "mission-control/graph", async ({ query, res }) => renderMissionControlGraph(query, res), "Get mission-control dependency graph (HEAD)");
65
- router.add("GET", "mission-control/next-up", async ({ query, res }) => renderNextUpQueue(query, res), "Get next-up queue");
66
- router.add("HEAD", "mission-control/next-up", async ({ query, res }) => renderNextUpQueue(query, res), "Get next-up queue (HEAD)");
1699
+ router.add("GET", "mission-control/next-up", async ({ query, res, req }) => renderNextUpQueue(query, res, workspaceScopeFromHeaders(req?.headers)), "Get next-up queue");
1700
+ router.add("HEAD", "mission-control/next-up", async ({ query, res, req }) => renderNextUpQueue(query, res, workspaceScopeFromHeaders(req?.headers)), "Get next-up queue (HEAD)");
1701
+ router.add("GET", "mission-control/slices", async ({ query, res, req }) => renderSliceProjection(query, res, workspaceScopeFromHeaders(req?.headers)), "Get mission-control slices at initiative/workstream/milestone/task scope");
1702
+ router.add("HEAD", "mission-control/slices", async ({ query, res, req }) => renderSliceProjection(query, res, workspaceScopeFromHeaders(req?.headers)), "Get mission-control slices at initiative/workstream/milestone/task scope (HEAD)");
1703
+ router.add("GET", "mission-control/sentinels", async ({ query, res }) => renderSentinelCatalog(query, res), "Get built-in sentinel catalog");
1704
+ router.add("HEAD", "mission-control/sentinels", async ({ query, res }) => renderSentinelCatalog(query, res), "Get built-in sentinel catalog (HEAD)");
67
1705
  }