@useorgx/openclaw-plugin 0.4.9 → 0.7.0

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