@useorgx/openclaw-plugin 0.4.8 → 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 (284) 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 +38 -26
  88. package/dist/agent-context-store.js +84 -42
  89. package/dist/agent-run-store.js +49 -28
  90. package/dist/agent-suite.d.ts +9 -0
  91. package/dist/agent-suite.js +150 -17
  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/auth/flows.d.ts +47 -0
  97. package/dist/auth/flows.js +169 -0
  98. package/dist/auth-store.js +6 -26
  99. package/dist/byok-store.js +5 -19
  100. package/dist/chat-store.d.ts +157 -0
  101. package/dist/chat-store.js +586 -0
  102. package/dist/cli/orgx.d.ts +66 -0
  103. package/dist/cli/orgx.js +102 -0
  104. package/dist/config/refresh.d.ts +32 -0
  105. package/dist/config/refresh.js +55 -0
  106. package/dist/config/resolution.d.ts +37 -0
  107. package/dist/config/resolution.js +178 -0
  108. package/dist/contracts/client.d.ts +43 -3
  109. package/dist/contracts/client.js +159 -30
  110. package/dist/contracts/retro-schema.d.ts +81 -0
  111. package/dist/contracts/retro-schema.js +80 -0
  112. package/dist/contracts/shared-types.d.ts +306 -0
  113. package/dist/contracts/shared-types.js +179 -0
  114. package/dist/contracts/skill-pack-schema.d.ts +192 -0
  115. package/dist/contracts/skill-pack-schema.js +180 -0
  116. package/dist/contracts/types.d.ts +224 -132
  117. package/dist/contracts/types.js +5 -0
  118. package/dist/entities/auto-assignment.d.ts +36 -0
  119. package/dist/entities/auto-assignment.js +141 -0
  120. package/dist/entity-comment-store.js +5 -25
  121. package/dist/event-sanitization.d.ts +11 -0
  122. package/dist/event-sanitization.js +113 -0
  123. package/dist/fs-utils.js +13 -1
  124. package/dist/gateway-watchdog.d.ts +5 -0
  125. package/dist/gateway-watchdog.js +50 -0
  126. package/dist/hash-utils.d.ts +2 -0
  127. package/dist/hash-utils.js +12 -0
  128. package/dist/hooks/post-reporting-event.mjs +1 -5
  129. package/dist/http/helpers/activity-headline.d.ts +10 -0
  130. package/dist/http/helpers/activity-headline.js +73 -0
  131. package/dist/http/helpers/artifact-fallback.d.ts +13 -0
  132. package/dist/http/helpers/artifact-fallback.js +148 -0
  133. package/dist/http/helpers/auto-continue-engine.d.ts +486 -0
  134. package/dist/http/helpers/auto-continue-engine.js +3563 -0
  135. package/dist/http/helpers/autopilot-operations.d.ts +176 -0
  136. package/dist/http/helpers/autopilot-operations.js +554 -0
  137. package/dist/http/helpers/autopilot-runtime.d.ts +43 -0
  138. package/dist/http/helpers/autopilot-runtime.js +607 -0
  139. package/dist/http/helpers/autopilot-slice-utils.d.ts +56 -0
  140. package/dist/http/helpers/autopilot-slice-utils.js +899 -0
  141. package/dist/http/helpers/decision-mapper.d.ts +52 -0
  142. package/dist/http/helpers/decision-mapper.js +260 -0
  143. package/dist/http/helpers/dispatch-lifecycle.d.ts +119 -0
  144. package/dist/http/helpers/dispatch-lifecycle.js +809 -0
  145. package/dist/http/helpers/hash-utils.d.ts +1 -0
  146. package/dist/http/helpers/hash-utils.js +1 -0
  147. package/dist/http/helpers/kickoff-context.d.ts +12 -0
  148. package/dist/http/helpers/kickoff-context.js +228 -0
  149. package/dist/http/helpers/llm-client.d.ts +47 -0
  150. package/dist/http/helpers/llm-client.js +256 -0
  151. package/dist/http/helpers/mission-control.d.ts +193 -0
  152. package/dist/http/helpers/mission-control.js +1383 -0
  153. package/dist/http/helpers/openclaw-cli.d.ts +37 -0
  154. package/dist/http/helpers/openclaw-cli.js +283 -0
  155. package/dist/http/helpers/runtime-sse.d.ts +20 -0
  156. package/dist/http/helpers/runtime-sse.js +110 -0
  157. package/dist/http/helpers/sentinel-catalog.d.ts +23 -0
  158. package/dist/http/helpers/sentinel-catalog.js +193 -0
  159. package/dist/http/helpers/session-classification.d.ts +9 -0
  160. package/dist/http/helpers/session-classification.js +564 -0
  161. package/dist/http/helpers/slice-experience-v2.d.ts +137 -0
  162. package/dist/http/helpers/slice-experience-v2.js +677 -0
  163. package/dist/http/helpers/slice-run-projections.d.ts +72 -0
  164. package/dist/http/helpers/slice-run-projections.js +860 -0
  165. package/dist/http/helpers/triage-mapper.d.ts +43 -0
  166. package/dist/http/helpers/triage-mapper.js +549 -0
  167. package/dist/http/helpers/value-utils.d.ts +6 -0
  168. package/dist/http/helpers/value-utils.js +72 -0
  169. package/dist/http/helpers/workspace-scope.d.ts +15 -0
  170. package/dist/http/helpers/workspace-scope.js +170 -0
  171. package/dist/http/index.d.ts +88 -0
  172. package/dist/http/index.js +3610 -0
  173. package/dist/http/router.d.ts +23 -0
  174. package/dist/http/router.js +23 -0
  175. package/dist/http/routes/agent-control.d.ts +79 -0
  176. package/dist/http/routes/agent-control.js +684 -0
  177. package/dist/http/routes/agent-suite.d.ts +38 -0
  178. package/dist/http/routes/agent-suite.js +397 -0
  179. package/dist/http/routes/agents-catalog.d.ts +40 -0
  180. package/dist/http/routes/agents-catalog.js +128 -0
  181. package/dist/http/routes/billing.d.ts +23 -0
  182. package/dist/http/routes/billing.js +55 -0
  183. package/dist/http/routes/chat.d.ts +19 -0
  184. package/dist/http/routes/chat.js +522 -0
  185. package/dist/http/routes/debug.d.ts +14 -0
  186. package/dist/http/routes/debug.js +21 -0
  187. package/dist/http/routes/decision-actions.d.ts +20 -0
  188. package/dist/http/routes/decision-actions.js +103 -0
  189. package/dist/http/routes/delegation.d.ts +19 -0
  190. package/dist/http/routes/delegation.js +32 -0
  191. package/dist/http/routes/dispatch-gateway-envelope.d.ts +25 -0
  192. package/dist/http/routes/dispatch-gateway-envelope.js +26 -0
  193. package/dist/http/routes/entities.d.ts +63 -0
  194. package/dist/http/routes/entities.js +440 -0
  195. package/dist/http/routes/entity-dynamic.d.ts +25 -0
  196. package/dist/http/routes/entity-dynamic.js +191 -0
  197. package/dist/http/routes/health.d.ts +22 -0
  198. package/dist/http/routes/health.js +49 -0
  199. package/dist/http/routes/live-legacy.d.ts +115 -0
  200. package/dist/http/routes/live-legacy.js +112 -0
  201. package/dist/http/routes/live-misc.d.ts +81 -0
  202. package/dist/http/routes/live-misc.js +426 -0
  203. package/dist/http/routes/live-snapshot.d.ts +136 -0
  204. package/dist/http/routes/live-snapshot.js +916 -0
  205. package/dist/http/routes/live-terminal.d.ts +11 -0
  206. package/dist/http/routes/live-terminal.js +261 -0
  207. package/dist/http/routes/live-triage.d.ts +61 -0
  208. package/dist/http/routes/live-triage.js +248 -0
  209. package/dist/http/routes/mission-control-actions.d.ts +131 -0
  210. package/dist/http/routes/mission-control-actions.js +1791 -0
  211. package/dist/http/routes/mission-control-read.d.ts +73 -0
  212. package/dist/http/routes/mission-control-read.js +1640 -0
  213. package/dist/http/routes/onboarding.d.ts +34 -0
  214. package/dist/http/routes/onboarding.js +101 -0
  215. package/dist/http/routes/realtime-orchestrator.d.ts +10 -0
  216. package/dist/http/routes/realtime-orchestrator.js +74 -0
  217. package/dist/http/routes/run-control.d.ts +27 -0
  218. package/dist/http/routes/run-control.js +96 -0
  219. package/dist/http/routes/runtime-hooks.d.ts +69 -0
  220. package/dist/http/routes/runtime-hooks.js +437 -0
  221. package/dist/http/routes/sentinels-catalog.d.ts +7 -0
  222. package/dist/http/routes/sentinels-catalog.js +24 -0
  223. package/dist/http/routes/settings-byok.d.ts +23 -0
  224. package/dist/http/routes/settings-byok.js +163 -0
  225. package/dist/http/routes/summary.d.ts +18 -0
  226. package/dist/http/routes/summary.js +49 -0
  227. package/dist/http/routes/usage.d.ts +24 -0
  228. package/dist/http/routes/usage.js +362 -0
  229. package/dist/http/routes/work-artifacts.d.ts +9 -0
  230. package/dist/http/routes/work-artifacts.js +55 -0
  231. package/dist/http/shared-state.d.ts +16 -0
  232. package/dist/http/shared-state.js +1 -0
  233. package/dist/http-handler.d.ts +1 -88
  234. package/dist/http-handler.js +1 -10605
  235. package/dist/index.js +287 -2284
  236. package/dist/json-utils.d.ts +1 -0
  237. package/dist/json-utils.js +8 -0
  238. package/dist/local-openclaw.js +29 -6
  239. package/dist/mcp-client-setup.js +3 -3
  240. package/dist/mcp-http-handler.js +33 -59
  241. package/dist/next-up-queue-store.d.ts +16 -1
  242. package/dist/next-up-queue-store.js +93 -25
  243. package/dist/outbox.d.ts +5 -0
  244. package/dist/outbox.js +113 -9
  245. package/dist/paths.js +24 -5
  246. package/dist/reporting/rollups.d.ts +53 -0
  247. package/dist/reporting/rollups.js +148 -0
  248. package/dist/retro/domain-templates.d.ts +45 -0
  249. package/dist/retro/domain-templates.js +297 -0
  250. package/dist/retro/quality-rubric.d.ts +33 -0
  251. package/dist/retro/quality-rubric.js +213 -0
  252. package/dist/runtime-cleanup.d.ts +18 -0
  253. package/dist/runtime-cleanup.js +87 -0
  254. package/dist/runtime-instance-store.js +5 -31
  255. package/dist/services/background.d.ts +34 -0
  256. package/dist/services/background.js +45 -0
  257. package/dist/services/experiment-randomization.d.ts +21 -0
  258. package/dist/services/experiment-randomization.js +63 -0
  259. package/dist/services/instrumentation.d.ts +29 -0
  260. package/dist/services/instrumentation.js +136 -0
  261. package/dist/skill-pack-state.d.ts +36 -5
  262. package/dist/skill-pack-state.js +273 -29
  263. package/dist/snapshot-store.js +5 -25
  264. package/dist/stores/json-store.d.ts +11 -0
  265. package/dist/stores/json-store.js +42 -0
  266. package/dist/sync/local-agent-telemetry.d.ts +13 -0
  267. package/dist/sync/local-agent-telemetry.js +128 -0
  268. package/dist/sync/outbox-replay.d.ts +55 -0
  269. package/dist/sync/outbox-replay.js +621 -0
  270. package/dist/team-context-store.d.ts +23 -0
  271. package/dist/team-context-store.js +116 -0
  272. package/dist/telemetry/posthog.js +4 -2
  273. package/dist/tools/core-tools.d.ts +72 -0
  274. package/dist/tools/core-tools.js +2270 -0
  275. package/dist/types.d.ts +2 -0
  276. package/dist/types.js +2 -0
  277. package/dist/worker-supervisor.js +23 -0
  278. package/package.json +14 -4
  279. package/dashboard/dist/assets/B3ziCA02.js +0 -8
  280. package/dashboard/dist/assets/BNeJ0kpF.js +0 -1
  281. package/dashboard/dist/assets/BzkiMPmM.js +0 -215
  282. package/dashboard/dist/assets/CUV9IHHi.js +0 -1
  283. package/dashboard/dist/assets/Ie7d9Iq2.css +0 -1
  284. package/dashboard/dist/assets/sAhvFnpk.js +0 -4
@@ -0,0 +1,1791 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { resolveWorkspaceScope as resolveCanonicalWorkspaceScope } from "../helpers/workspace-scope.js";
3
+ import { buildDispatchGatewayEnvelope } from "./dispatch-gateway-envelope.js";
4
+ const PLAY_QUEUE_LOOKUP_TIMEOUT_MS = (() => {
5
+ const raw = process.env.ORGX_PLAY_QUEUE_LOOKUP_TIMEOUT_MS;
6
+ const parsed = Number(raw);
7
+ if (!Number.isFinite(parsed))
8
+ return 350;
9
+ return Math.max(200, Math.floor(parsed));
10
+ })();
11
+ const IN_PROGRESS_TASK_STATUSES = new Set([
12
+ "in_progress",
13
+ "inprogress",
14
+ "active",
15
+ "running",
16
+ "working",
17
+ "planning",
18
+ "dispatching",
19
+ "pending",
20
+ ]);
21
+ const BLOCKED_TASK_STATUSES = new Set(["blocked", "stalled", "failed", "error"]);
22
+ async function withSoftTimeout(work, timeoutMs) {
23
+ let timer = null;
24
+ try {
25
+ return await Promise.race([
26
+ work,
27
+ new Promise((_, reject) => {
28
+ timer = setTimeout(() => {
29
+ reject(new Error(`timed out after ${timeoutMs}ms`));
30
+ }, timeoutMs);
31
+ }),
32
+ ]);
33
+ }
34
+ finally {
35
+ if (timer)
36
+ clearTimeout(timer);
37
+ }
38
+ }
39
+ function normalizeStatusValue(value) {
40
+ if (typeof value !== "string")
41
+ return "";
42
+ return value.trim().toLowerCase().replace(/[\s-]+/g, "_");
43
+ }
44
+ function normalizePlacement(value, fallback = "bottom") {
45
+ if (typeof value !== "string")
46
+ return fallback;
47
+ const normalized = value.trim().toLowerCase();
48
+ if (normalized === "top")
49
+ return "top";
50
+ if (normalized === "bottom")
51
+ return "bottom";
52
+ return fallback;
53
+ }
54
+ function normalizeScope(value) {
55
+ if (value === "task" || value === "milestone" || value === "workstream") {
56
+ return value;
57
+ }
58
+ if (typeof value === "string") {
59
+ const normalized = value.trim().toLowerCase();
60
+ if (normalized === "task" || normalized === "milestone" || normalized === "workstream") {
61
+ return normalized;
62
+ }
63
+ }
64
+ return null;
65
+ }
66
+ function normalizeParallelMode(value) {
67
+ if (typeof value === "string" && value.trim().toLowerCase() === "iwmt") {
68
+ return "iwmt";
69
+ }
70
+ return "iwmt";
71
+ }
72
+ function normalizeMaxParallelSlices(value, fallback) {
73
+ const normalizeValue = (input) => Math.max(1, Math.min(5, Math.floor(input)));
74
+ if (typeof value === "number" && Number.isFinite(value))
75
+ return normalizeValue(value);
76
+ if (typeof value === "string" && value.trim().length > 0) {
77
+ const parsed = Number(value);
78
+ if (Number.isFinite(parsed))
79
+ return normalizeValue(parsed);
80
+ }
81
+ return normalizeValue(fallback);
82
+ }
83
+ function parseQueueOrder(input, deps) {
84
+ const rawOrder = Array.isArray(input) ? input : [];
85
+ const order = [];
86
+ for (const entry of rawOrder) {
87
+ if (!entry)
88
+ continue;
89
+ if (typeof entry === "string") {
90
+ const [initiativeId, workstreamId] = entry.split(":", 2).map((s) => s.trim());
91
+ if (initiativeId && workstreamId)
92
+ order.push({ initiativeId, workstreamId });
93
+ continue;
94
+ }
95
+ if (typeof entry !== "object")
96
+ continue;
97
+ const record = entry;
98
+ const initiativeId = (deps.pickString(record, ["initiativeId", "initiative_id"]) ?? "").trim();
99
+ const workstreamId = (deps.pickString(record, ["workstreamId", "workstream_id"]) ?? "").trim();
100
+ if (initiativeId && workstreamId)
101
+ order.push({ initiativeId, workstreamId });
102
+ }
103
+ return order;
104
+ }
105
+ function dedupeQueueOrder(order) {
106
+ const next = [];
107
+ const seen = new Set();
108
+ for (const entry of order) {
109
+ const initiativeId = (entry.initiativeId ?? "").trim();
110
+ const workstreamId = (entry.workstreamId ?? "").trim();
111
+ if (!initiativeId || !workstreamId)
112
+ continue;
113
+ const key = `${initiativeId}:${workstreamId}`;
114
+ if (seen.has(key))
115
+ continue;
116
+ seen.add(key);
117
+ next.push({ initiativeId, workstreamId });
118
+ }
119
+ return next;
120
+ }
121
+ function normalizeSliceLevel(value) {
122
+ if (typeof value !== "string")
123
+ return "workstream";
124
+ const normalized = value.trim().toLowerCase();
125
+ if (normalized === "initiative")
126
+ return "initiative";
127
+ if (normalized === "milestone")
128
+ return "milestone";
129
+ if (normalized === "task")
130
+ return "task";
131
+ return "workstream";
132
+ }
133
+ function normalizeSliceOrderMode(value) {
134
+ if (typeof value !== "string")
135
+ return null;
136
+ const normalized = value.trim().toLowerCase();
137
+ if (normalized === "manual" || normalized === "algorithmic")
138
+ return normalized;
139
+ return null;
140
+ }
141
+ function parseSliceOrderForMutation(input) {
142
+ const values = Array.isArray(input) ? input : [];
143
+ const output = [];
144
+ const seen = new Set();
145
+ for (const entry of values) {
146
+ let raw = "";
147
+ if (typeof entry === "string")
148
+ raw = entry;
149
+ else if (entry && typeof entry === "object") {
150
+ const record = entry;
151
+ if (typeof record.sliceId === "string")
152
+ raw = record.sliceId;
153
+ else if (typeof record.id === "string")
154
+ raw = record.id;
155
+ }
156
+ const normalized = raw.trim();
157
+ if (!normalized || seen.has(normalized))
158
+ continue;
159
+ seen.add(normalized);
160
+ output.push(normalized);
161
+ }
162
+ return output;
163
+ }
164
+ function buildPlacedOrder(input) {
165
+ if (input.targets.size === 0)
166
+ return input.order;
167
+ const selected = [];
168
+ const remaining = [];
169
+ for (const entry of input.order) {
170
+ const key = `${entry.initiativeId}:${entry.workstreamId}`;
171
+ if (input.targets.has(key))
172
+ selected.push(entry);
173
+ else
174
+ remaining.push(entry);
175
+ }
176
+ if (selected.length === 0)
177
+ return input.order;
178
+ return input.placement === "top"
179
+ ? [...selected, ...remaining]
180
+ : [...remaining, ...selected];
181
+ }
182
+ function shouldResetTaskStatus(status, states) {
183
+ const normalized = normalizeStatusValue(status);
184
+ if (!normalized)
185
+ return false;
186
+ if (normalized === "todo" || normalized === "done" || normalized === "completed") {
187
+ return false;
188
+ }
189
+ if (states.has("running") && IN_PROGRESS_TASK_STATUSES.has(normalized)) {
190
+ return true;
191
+ }
192
+ if (states.has("blocked") && BLOCKED_TASK_STATUSES.has(normalized)) {
193
+ return true;
194
+ }
195
+ return false;
196
+ }
197
+ function asRecord(value) {
198
+ if (!value || typeof value !== "object" || Array.isArray(value))
199
+ return null;
200
+ return value;
201
+ }
202
+ function asString(value) {
203
+ if (typeof value !== "string")
204
+ return null;
205
+ const trimmed = value.trim();
206
+ return trimmed.length > 0 ? trimmed : null;
207
+ }
208
+ function asStringArray(value) {
209
+ if (!Array.isArray(value))
210
+ return [];
211
+ return value
212
+ .map((entry) => asString(entry))
213
+ .filter((entry) => Boolean(entry));
214
+ }
215
+ function parseCycleGraphNodes(graph) {
216
+ const root = asRecord(graph);
217
+ const rawNodes = Array.isArray(root?.nodes) ? root.nodes : [];
218
+ const nodes = [];
219
+ for (const entry of rawNodes) {
220
+ const record = asRecord(entry);
221
+ if (!record)
222
+ continue;
223
+ const id = asString(record.id);
224
+ const type = asString(record.type);
225
+ if (!id || !type)
226
+ continue;
227
+ if (type !== "initiative" &&
228
+ type !== "workstream" &&
229
+ type !== "milestone" &&
230
+ type !== "task") {
231
+ continue;
232
+ }
233
+ nodes.push({
234
+ id,
235
+ type,
236
+ title: asString(record.title) ?? id,
237
+ workstreamId: asString(record.workstreamId),
238
+ dependencyIds: Array.from(new Set(asStringArray(record.dependencyIds).filter((depId) => depId !== id))),
239
+ });
240
+ }
241
+ return nodes;
242
+ }
243
+ function parseCycleDiagnosticsRemovedEdges(graph) {
244
+ const root = asRecord(graph);
245
+ const diagnostics = asRecord(root?.cycleDiagnostics);
246
+ const rawRemoved = Array.isArray(diagnostics?.removedEdges)
247
+ ? diagnostics?.removedEdges
248
+ : [];
249
+ const removedEdges = [];
250
+ for (const entry of rawRemoved) {
251
+ const record = asRecord(entry);
252
+ if (!record)
253
+ continue;
254
+ const from = asString(record.from);
255
+ const to = asString(record.to);
256
+ if (!from || !to)
257
+ continue;
258
+ removedEdges.push({ from, to });
259
+ }
260
+ return removedEdges;
261
+ }
262
+ function detectCycleEdgeKeys(edges) {
263
+ const adjacency = new Map();
264
+ for (const edge of edges) {
265
+ const list = adjacency.get(edge.from) ?? [];
266
+ list.push(edge.to);
267
+ adjacency.set(edge.from, list);
268
+ }
269
+ const visiting = new Set();
270
+ const visited = new Set();
271
+ const cycleEdgeKeys = new Set();
272
+ const dfs = (nodeId) => {
273
+ if (visited.has(nodeId))
274
+ return;
275
+ visiting.add(nodeId);
276
+ const children = adjacency.get(nodeId) ?? [];
277
+ for (const childId of children) {
278
+ if (visiting.has(childId)) {
279
+ cycleEdgeKeys.add(`${nodeId}->${childId}`);
280
+ continue;
281
+ }
282
+ dfs(childId);
283
+ }
284
+ visiting.delete(nodeId);
285
+ visited.add(nodeId);
286
+ };
287
+ for (const nodeId of adjacency.keys()) {
288
+ if (!visited.has(nodeId))
289
+ dfs(nodeId);
290
+ }
291
+ return cycleEdgeKeys;
292
+ }
293
+ export function registerMissionControlActionsRoutes(router, deps) {
294
+ const sendRouteError = (res, status, location, error, extra = {}) => {
295
+ deps.sendJson(res, status, {
296
+ ok: false,
297
+ error,
298
+ error_location: location,
299
+ ...extra,
300
+ });
301
+ };
302
+ const sendRouteException = (res, location, err, extra = {}) => {
303
+ sendRouteError(res, 500, location, deps.safeErrorMessage(err), extra);
304
+ };
305
+ const resolveWorkspaceScope = (payload, query) => resolveCanonicalWorkspaceScope(query, payload, {
306
+ allowProjectScope: false,
307
+ });
308
+ router.add("POST", "mission-control/next-up/play", async ({ req, query, res }) => {
309
+ try {
310
+ const payload = await deps.parseJsonRequest(req);
311
+ const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
312
+ query.get("initiativeId") ??
313
+ query.get("initiative_id") ??
314
+ "")
315
+ .trim();
316
+ const workstreamId = (deps.pickString(payload, ["workstreamId", "workstream_id"]) ??
317
+ query.get("workstreamId") ??
318
+ query.get("workstream_id") ??
319
+ "")
320
+ .trim();
321
+ if (!initiativeId || !workstreamId) {
322
+ sendRouteError(res, 400, "mission-control.next-up.play.validation", "initiativeId and workstreamId are required");
323
+ return;
324
+ }
325
+ let agentIdRaw = (deps.pickString(payload, ["agentId", "agent_id"]) ??
326
+ query.get("agentId") ??
327
+ query.get("agent_id") ??
328
+ "")
329
+ .trim();
330
+ const fastAckRaw = payload.fastAck ??
331
+ payload.fast_ack ??
332
+ query.get("fastAck") ??
333
+ query.get("fast_ack") ??
334
+ null;
335
+ const fastAck = typeof fastAckRaw === "boolean"
336
+ ? fastAckRaw
337
+ : deps.parseBooleanQuery(typeof fastAckRaw === "string" ? fastAckRaw : null);
338
+ let matchedQueueItem = null;
339
+ const shouldLookupQueue = !fastAck || !agentIdRaw;
340
+ if (shouldLookupQueue) {
341
+ try {
342
+ const queue = fastAck
343
+ ? await withSoftTimeout(deps.buildNextUpQueue({ initiativeId }), PLAY_QUEUE_LOOKUP_TIMEOUT_MS)
344
+ : await deps.buildNextUpQueue({ initiativeId });
345
+ matchedQueueItem =
346
+ queue.items.find((item) => item.workstreamId === workstreamId) ?? null;
347
+ }
348
+ catch {
349
+ // Best effort: Play/Autopilot dispatch should still proceed even if queue refresh is slow.
350
+ }
351
+ }
352
+ if (!agentIdRaw && matchedQueueItem?.runnerAgentId) {
353
+ agentIdRaw = matchedQueueItem.runnerAgentId;
354
+ }
355
+ const agentId = agentIdRaw || "main";
356
+ if (!/^[a-zA-Z0-9_-]+$/.test(agentId)) {
357
+ sendRouteError(res, 400, "mission-control.next-up.play.validation", "agentId must be a simple identifier (letters, numbers, _ or -).");
358
+ return;
359
+ }
360
+ const requestedAgentName = await deps.resolveAgentDisplayName(agentId, matchedQueueItem?.runnerAgentId === agentId
361
+ ? matchedQueueItem.runnerAgentName ?? null
362
+ : null);
363
+ const tokenBudget = deps.pickNumber(payload, [
364
+ "tokenBudget",
365
+ "token_budget",
366
+ "tokenBudgetTokens",
367
+ "token_budget_tokens",
368
+ "maxTokens",
369
+ "max_tokens",
370
+ ]) ??
371
+ query.get("tokenBudget") ??
372
+ query.get("token_budget") ??
373
+ query.get("tokenBudgetTokens") ??
374
+ query.get("token_budget_tokens") ??
375
+ query.get("maxTokens") ??
376
+ query.get("max_tokens") ??
377
+ null;
378
+ const includeVerificationRaw = payload.includeVerification ??
379
+ payload.include_verification ??
380
+ query.get("includeVerification") ??
381
+ query.get("include_verification") ??
382
+ null;
383
+ const includeVerification = typeof includeVerificationRaw === "boolean"
384
+ ? includeVerificationRaw
385
+ : deps.parseBooleanQuery(typeof includeVerificationRaw === "string"
386
+ ? includeVerificationRaw
387
+ : null);
388
+ const ignoreSpawnGuardRateLimitRaw = payload.ignoreSpawnGuardRateLimit ??
389
+ payload.ignore_spawn_guard_rate_limit ??
390
+ query.get("ignoreSpawnGuardRateLimit") ??
391
+ query.get("ignore_spawn_guard_rate_limit") ??
392
+ null;
393
+ const ignoreSpawnGuardRateLimit = typeof ignoreSpawnGuardRateLimitRaw === "boolean"
394
+ ? ignoreSpawnGuardRateLimitRaw
395
+ : deps.parseBooleanQuery(typeof ignoreSpawnGuardRateLimitRaw === "string"
396
+ ? ignoreSpawnGuardRateLimitRaw
397
+ : null);
398
+ const requestedScopeRaw = deps.pickString(payload, ["scope", "sliceScope", "slice_scope"]) ??
399
+ query.get("scope") ??
400
+ query.get("sliceScope") ??
401
+ query.get("slice_scope") ??
402
+ null;
403
+ const queueScope = normalizeScope(matchedQueueItem?.sliceScope ?? null);
404
+ const scope = normalizeScope(requestedScopeRaw) ?? queueScope ?? "task";
405
+ const requestedParallelModeRaw = deps.pickString(payload, ["parallelMode", "parallel_mode"]) ??
406
+ query.get("parallelMode") ??
407
+ query.get("parallel_mode") ??
408
+ null;
409
+ const requestedMaxParallelSlicesRaw = deps.pickNumber(payload, ["maxParallelSlices", "max_parallel_slices"]) ??
410
+ query.get("maxParallelSlices") ??
411
+ query.get("max_parallel_slices") ??
412
+ null;
413
+ const queuePreferredParallel = typeof matchedQueueItem?.executionPolicy?.maxParallelAgents === "number"
414
+ ? matchedQueueItem.executionPolicy.maxParallelAgents
415
+ : null;
416
+ const maxParallelSlices = normalizeMaxParallelSlices(requestedMaxParallelSlicesRaw, queuePreferredParallel ?? 1);
417
+ const parallelMode = normalizeParallelMode(requestedParallelModeRaw);
418
+ const existingRun = deps.autoContinueRuns.get(initiativeId) ?? null;
419
+ const existingActiveRunIds = Array.isArray(existingRun?.activeSliceRunIds)
420
+ ? (existingRun?.activeSliceRunIds)
421
+ .filter((id) => typeof id === "string" && id.trim().length > 0)
422
+ .map((id) => id.trim())
423
+ : typeof existingRun?.activeRunId === "string" && existingRun.activeRunId.trim().length > 0
424
+ ? [existingRun.activeRunId.trim()]
425
+ : [];
426
+ if (existingRun &&
427
+ (existingRun.status === "running" || existingRun.status === "stopping") &&
428
+ existingActiveRunIds.length > 0) {
429
+ const activeSlice = deps.autoContinueSliceRuns.get(existingActiveRunIds[0]) ?? null;
430
+ const activeWorkstreamId = activeSlice?.workstreamId ?? null;
431
+ const activeWorkstreamTitle = activeSlice?.workstreamTitle ?? null;
432
+ deps.sendJson(res, 409, {
433
+ ok: false,
434
+ code: "auto_continue_already_running",
435
+ error: activeWorkstreamId || activeWorkstreamTitle
436
+ ? `Auto-continue is already running for ${activeWorkstreamTitle ?? activeWorkstreamId}. Stop it before launching another Play run.`
437
+ : "Auto-continue is already running for this initiative. Stop it before launching another Play run.",
438
+ run: existingRun,
439
+ activeRunIds: existingActiveRunIds,
440
+ activeWorkstreamId,
441
+ activeWorkstreamTitle,
442
+ error_location: "mission-control.next-up.play.concurrent_run",
443
+ });
444
+ return;
445
+ }
446
+ const run = await deps.startAutoContinueRun({
447
+ initiativeId,
448
+ agentId,
449
+ agentName: requestedAgentName,
450
+ tokenBudget,
451
+ includeVerification,
452
+ allowedWorkstreamIds: [workstreamId],
453
+ maxParallelSlices,
454
+ parallelMode,
455
+ stopAfterSlice: true,
456
+ ignoreSpawnGuardRateLimit: ignoreSpawnGuardRateLimit === true,
457
+ scope,
458
+ });
459
+ const dispatchId = randomUUID();
460
+ const playDispatchEnvelope = (dispatchMode) => buildDispatchGatewayEnvelope({
461
+ dispatchId,
462
+ dispatchMode,
463
+ route: "mission-control.next-up.play",
464
+ source: "manual_play",
465
+ initiativeId,
466
+ workstreamId,
467
+ workstreamIds: [workstreamId],
468
+ taskIds: Array.isArray(matchedQueueItem?.sliceTaskIds)
469
+ ? matchedQueueItem.sliceTaskIds
470
+ : [],
471
+ });
472
+ let fallbackDispatch = null;
473
+ const maybeDispatchFallback = async () => {
474
+ if (!run.activeRunId &&
475
+ matchedQueueItem &&
476
+ matchedQueueItem.runnerSource === "fallback") {
477
+ return await deps.dispatchFallbackWorkstreamTurn({
478
+ initiativeId,
479
+ initiativeTitle: matchedQueueItem.initiativeTitle,
480
+ workstreamId,
481
+ workstreamTitle: matchedQueueItem.workstreamTitle,
482
+ agentId,
483
+ agentName: requestedAgentName,
484
+ taskId: matchedQueueItem.nextTaskId ?? null,
485
+ taskTitle: matchedQueueItem.nextTaskTitle ?? null,
486
+ });
487
+ }
488
+ return null;
489
+ };
490
+ if (!fastAck) {
491
+ await deps.tickAutoContinueRun(run);
492
+ // Give short-lived workers a brief window to flush output so Play can resolve
493
+ // in one request/response cycle without requiring extra manual ticks.
494
+ if (run.activeRunId) {
495
+ await new Promise((resolve) => setTimeout(resolve, 140));
496
+ await deps.tickAutoContinueRun(run);
497
+ }
498
+ fallbackDispatch = await maybeDispatchFallback();
499
+ }
500
+ else {
501
+ const tickPromise = deps.tickAutoContinueRun(run);
502
+ const tickCompleted = await Promise.race([
503
+ tickPromise.then(() => true),
504
+ new Promise((resolve) => setTimeout(() => resolve(false), 1100)),
505
+ ]);
506
+ if (!tickCompleted) {
507
+ await new Promise((resolve) => setTimeout(resolve, 80));
508
+ const settledImmediately = Boolean(run.activeRunId) ||
509
+ Boolean(run.lastRunId) ||
510
+ Boolean(run.stopReason) ||
511
+ run.status !== "running";
512
+ if (settledImmediately) {
513
+ await tickPromise.catch(() => null);
514
+ fallbackDispatch = await maybeDispatchFallback();
515
+ }
516
+ else {
517
+ void tickPromise
518
+ .then(async () => {
519
+ await maybeDispatchFallback().catch(() => null);
520
+ })
521
+ .catch(() => {
522
+ // best effort
523
+ });
524
+ deps.sendJson(res, 202, {
525
+ ok: true,
526
+ run,
527
+ initiativeId,
528
+ workstreamId,
529
+ agentId,
530
+ ...playDispatchEnvelope("pending"),
531
+ sessionId: null,
532
+ slice: {
533
+ scope,
534
+ taskIds: matchedQueueItem?.sliceTaskIds ?? [],
535
+ taskCount: typeof matchedQueueItem?.sliceTaskCount === "number"
536
+ ? matchedQueueItem.sliceTaskCount
537
+ : Array.isArray(matchedQueueItem?.sliceTaskIds)
538
+ ? matchedQueueItem.sliceTaskIds.length
539
+ : 0,
540
+ primaryTaskId: matchedQueueItem?.nextTaskId ?? null,
541
+ },
542
+ executionPolicy: matchedQueueItem?.executionPolicy ?? null,
543
+ });
544
+ return;
545
+ }
546
+ }
547
+ else {
548
+ await tickPromise;
549
+ fallbackDispatch = await maybeDispatchFallback();
550
+ }
551
+ }
552
+ const fallbackStarted = Boolean(fallbackDispatch?.sessionId);
553
+ const dispatchMode = run.activeRunId
554
+ ? "slice"
555
+ : fallbackStarted
556
+ ? "fallback"
557
+ : "none";
558
+ if (dispatchMode === "none" &&
559
+ run.lastRunId &&
560
+ (run.stopReason === "completed" ||
561
+ run.stopReason === "blocked" ||
562
+ run.stopReason === "error")) {
563
+ const finalizedDispatchMode = run.stopReason === "completed"
564
+ ? "slice_completed"
565
+ : run.stopReason === "blocked"
566
+ ? "slice_blocked"
567
+ : "slice_error";
568
+ deps.sendJson(res, 200, {
569
+ ok: true,
570
+ run,
571
+ initiativeId,
572
+ workstreamId,
573
+ agentId,
574
+ ...playDispatchEnvelope(finalizedDispatchMode),
575
+ sessionId: run.lastRunId,
576
+ slice: {
577
+ scope,
578
+ taskIds: matchedQueueItem?.sliceTaskIds ?? [],
579
+ taskCount: typeof matchedQueueItem?.sliceTaskCount === "number"
580
+ ? matchedQueueItem.sliceTaskCount
581
+ : Array.isArray(matchedQueueItem?.sliceTaskIds)
582
+ ? matchedQueueItem.sliceTaskIds.length
583
+ : 0,
584
+ primaryTaskId: matchedQueueItem?.nextTaskId ?? null,
585
+ },
586
+ executionPolicy: matchedQueueItem?.executionPolicy ?? null,
587
+ });
588
+ return;
589
+ }
590
+ if (dispatchMode === "none" && run.status === "running" && !run.stopReason) {
591
+ deps.sendJson(res, 202, {
592
+ ok: true,
593
+ run,
594
+ initiativeId,
595
+ workstreamId,
596
+ agentId,
597
+ ...playDispatchEnvelope("pending"),
598
+ sessionId: null,
599
+ slice: {
600
+ scope,
601
+ taskIds: matchedQueueItem?.sliceTaskIds ?? [],
602
+ taskCount: typeof matchedQueueItem?.sliceTaskCount === "number"
603
+ ? matchedQueueItem.sliceTaskCount
604
+ : Array.isArray(matchedQueueItem?.sliceTaskIds)
605
+ ? matchedQueueItem.sliceTaskIds.length
606
+ : 0,
607
+ primaryTaskId: matchedQueueItem?.nextTaskId ?? null,
608
+ },
609
+ executionPolicy: matchedQueueItem?.executionPolicy ?? null,
610
+ });
611
+ return;
612
+ }
613
+ if (dispatchMode === "none") {
614
+ const fallbackBlockedReason = fallbackDispatch?.blockedReason ?? null;
615
+ const reason = fallbackBlockedReason ??
616
+ (run.stopReason === "blocked"
617
+ ? "No dispatchable task is ready for this workstream yet."
618
+ : run.stopReason === "completed"
619
+ ? "No queued task is available for this workstream."
620
+ : "Unable to dispatch this workstream right now.");
621
+ deps.sendJson(res, fallbackDispatch?.retryable ? 429 : 409, {
622
+ ok: false,
623
+ code: fallbackBlockedReason
624
+ ? fallbackDispatch?.retryable
625
+ ? "spawn_guard_rate_limited"
626
+ : "spawn_guard_blocked"
627
+ : undefined,
628
+ error: reason,
629
+ run,
630
+ initiativeId,
631
+ workstreamId,
632
+ agentId,
633
+ fallbackDispatch,
634
+ error_location: "mission-control.next-up.play.dispatch",
635
+ });
636
+ return;
637
+ }
638
+ deps.sendJson(res, 200, {
639
+ ok: true,
640
+ run,
641
+ initiativeId,
642
+ workstreamId,
643
+ agentId,
644
+ ...playDispatchEnvelope(dispatchMode),
645
+ sessionId: run.activeRunId ?? fallbackDispatch?.sessionId ?? null,
646
+ slice: {
647
+ scope,
648
+ taskIds: matchedQueueItem?.sliceTaskIds ?? [],
649
+ taskCount: typeof matchedQueueItem?.sliceTaskCount === "number"
650
+ ? matchedQueueItem.sliceTaskCount
651
+ : Array.isArray(matchedQueueItem?.sliceTaskIds)
652
+ ? matchedQueueItem.sliceTaskIds.length
653
+ : 0,
654
+ primaryTaskId: matchedQueueItem?.nextTaskId ?? null,
655
+ },
656
+ executionPolicy: matchedQueueItem?.executionPolicy ?? null,
657
+ });
658
+ }
659
+ catch (err) {
660
+ sendRouteException(res, "mission-control.next-up.play.handler", err);
661
+ }
662
+ }, "Mission-control next-up play");
663
+ router.add("POST", "mission-control/next-up/launch", async ({ req, query, res }) => {
664
+ try {
665
+ const payload = await deps.parseJsonRequest(req);
666
+ const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
667
+ query.get("initiativeId") ??
668
+ query.get("initiative_id") ??
669
+ "")
670
+ .trim();
671
+ if (!initiativeId) {
672
+ sendRouteError(res, 400, "mission-control.next-up.launch.validation", "initiativeId is required");
673
+ return;
674
+ }
675
+ const requestedWorkstreamIds = deps.dedupeStrings(deps.pickStringArray(payload, ["workstreamIds", "workstream_ids"]));
676
+ const requestedScopeRaw = deps.pickString(payload, ["scope", "sliceScope", "slice_scope"]) ??
677
+ query.get("scope") ??
678
+ null;
679
+ const scope = normalizeScope(requestedScopeRaw) ?? "task";
680
+ const ignoreSpawnGuardRateLimitRaw = payload.ignoreSpawnGuardRateLimit ??
681
+ payload.ignore_spawn_guard_rate_limit ??
682
+ null;
683
+ const ignoreSpawnGuardRateLimit = typeof ignoreSpawnGuardRateLimitRaw === "boolean"
684
+ ? ignoreSpawnGuardRateLimitRaw
685
+ : deps.parseBooleanQuery(typeof ignoreSpawnGuardRateLimitRaw === "string"
686
+ ? ignoreSpawnGuardRateLimitRaw
687
+ : null);
688
+ // Build the queue to discover workstreams to dispatch
689
+ let queue;
690
+ try {
691
+ queue = await deps.buildNextUpQueue({ initiativeId });
692
+ }
693
+ catch {
694
+ sendRouteError(res, 503, "mission-control.next-up.launch.queue", "Unable to load queue to determine dispatchable workstreams.");
695
+ return;
696
+ }
697
+ // Filter to requested workstreams if specified, otherwise take all
698
+ const candidateItems = requestedWorkstreamIds.length > 0
699
+ ? queue.items.filter((item) => requestedWorkstreamIds.includes(item.workstreamId))
700
+ : queue.items.filter((item) => item.queueState === "queued" || item.queueState === "idle");
701
+ if (candidateItems.length === 0) {
702
+ deps.sendJson(res, 200, {
703
+ ok: true,
704
+ dispatched: 0,
705
+ initiativeId,
706
+ message: "No dispatchable workstreams found in the queue.",
707
+ });
708
+ return;
709
+ }
710
+ // Dispatch each candidate as a one-shot (stopAfterSlice: true)
711
+ let dispatched = 0;
712
+ const errors = [];
713
+ for (const item of candidateItems) {
714
+ try {
715
+ const agentId = item.runnerAgentId || "main";
716
+ const agentName = await deps.resolveAgentDisplayName(agentId, item.runnerAgentName ?? null);
717
+ const run = await deps.startAutoContinueRun({
718
+ initiativeId,
719
+ agentId,
720
+ agentName,
721
+ allowedWorkstreamIds: [item.workstreamId],
722
+ stopAfterSlice: true,
723
+ ignoreSpawnGuardRateLimit: ignoreSpawnGuardRateLimit === true,
724
+ scope,
725
+ });
726
+ // Fire-and-forget tick to start the actual dispatch
727
+ void deps.tickAutoContinueRun(run).catch(() => null);
728
+ dispatched += 1;
729
+ }
730
+ catch (err) {
731
+ errors.push(`${item.workstreamId}: ${deps.safeErrorMessage(err)}`);
732
+ }
733
+ }
734
+ deps.clearNextUpQueueCache(initiativeId);
735
+ deps.sendJson(res, 200, {
736
+ ok: true,
737
+ dispatched,
738
+ initiativeId,
739
+ requested: candidateItems.length,
740
+ ...(errors.length > 0 ? { errors } : {}),
741
+ });
742
+ }
743
+ catch (err) {
744
+ sendRouteException(res, "mission-control.next-up.launch.handler", err);
745
+ }
746
+ }, "Mission-control next-up launch (dispatch without auto-continue loop)");
747
+ router.add("POST", "mission-control/next-up/pin", async ({ req, query, res }) => {
748
+ try {
749
+ const payload = await deps.parseJsonRequest(req);
750
+ const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
751
+ query.get("initiativeId") ??
752
+ query.get("initiative_id") ??
753
+ "")
754
+ .trim();
755
+ const workstreamId = (deps.pickString(payload, ["workstreamId", "workstream_id"]) ??
756
+ query.get("workstreamId") ??
757
+ query.get("workstream_id") ??
758
+ "")
759
+ .trim();
760
+ const preferredTaskId = (deps.pickString(payload, [
761
+ "taskId",
762
+ "task_id",
763
+ "preferredTaskId",
764
+ "preferred_task_id",
765
+ ]) ?? "")
766
+ .trim() || null;
767
+ const preferredMilestoneId = (deps.pickString(payload, [
768
+ "milestoneId",
769
+ "milestone_id",
770
+ "preferredMilestoneId",
771
+ "preferred_milestone_id",
772
+ ]) ?? "")
773
+ .trim() || null;
774
+ if (!initiativeId || !workstreamId) {
775
+ sendRouteError(res, 400, "mission-control.next-up.pin.validation", "initiativeId and workstreamId are required");
776
+ return;
777
+ }
778
+ const next = deps.upsertNextUpQueuePin({
779
+ initiativeId,
780
+ workstreamId,
781
+ preferredTaskId,
782
+ preferredMilestoneId,
783
+ });
784
+ deps.clearNextUpQueueCache(initiativeId);
785
+ deps.sendJson(res, 200, { ok: true, pins: next.pins, updatedAt: next.updatedAt });
786
+ }
787
+ catch (err) {
788
+ sendRouteException(res, "mission-control.next-up.pin.handler", err);
789
+ }
790
+ }, "Mission-control next-up pin");
791
+ router.add("POST", "mission-control/next-up/unpin", async ({ req, query, res }) => {
792
+ try {
793
+ const payload = await deps.parseJsonRequest(req);
794
+ const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
795
+ query.get("initiativeId") ??
796
+ query.get("initiative_id") ??
797
+ "")
798
+ .trim();
799
+ const workstreamId = (deps.pickString(payload, ["workstreamId", "workstream_id"]) ??
800
+ query.get("workstreamId") ??
801
+ query.get("workstream_id") ??
802
+ "")
803
+ .trim();
804
+ if (!initiativeId || !workstreamId) {
805
+ sendRouteError(res, 400, "mission-control.next-up.unpin.validation", "initiativeId and workstreamId are required");
806
+ return;
807
+ }
808
+ const next = deps.removeNextUpQueuePin({ initiativeId, workstreamId });
809
+ deps.clearNextUpQueueCache(initiativeId);
810
+ deps.sendJson(res, 200, { ok: true, pins: next.pins, updatedAt: next.updatedAt });
811
+ }
812
+ catch (err) {
813
+ sendRouteException(res, "mission-control.next-up.unpin.handler", err);
814
+ }
815
+ }, "Mission-control next-up unpin");
816
+ router.add("POST", "mission-control/next-up/reorder", async ({ req, res }) => {
817
+ try {
818
+ const payload = await deps.parseJsonRequest(req);
819
+ const order = dedupeQueueOrder(parseQueueOrder(payload?.order, deps));
820
+ const next = deps.setNextUpQueuePinOrder({ order });
821
+ deps.clearNextUpQueueCache(null);
822
+ deps.sendJson(res, 200, { ok: true, pins: next.pins, updatedAt: next.updatedAt });
823
+ }
824
+ catch (err) {
825
+ sendRouteException(res, "mission-control.next-up.reorder.handler", err);
826
+ }
827
+ }, "Mission-control next-up reorder");
828
+ router.add("POST", "mission-control/slices/reorder", async ({ req, query, res }) => {
829
+ try {
830
+ const payload = await deps.parseJsonRequest(req);
831
+ const level = normalizeSliceLevel(deps.pickString(payload, ["level"]) ?? query.get("level"));
832
+ const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
833
+ query.get("initiativeId") ??
834
+ query.get("initiative_id") ??
835
+ "").trim() || null;
836
+ const scope = resolveWorkspaceScope(payload, query);
837
+ if (scope.error) {
838
+ sendRouteError(res, 400, "mission-control.slices.reorder.validation", scope.error);
839
+ return;
840
+ }
841
+ const workspaceId = scope.workspaceId;
842
+ const order = parseSliceOrderForMutation(payload?.order);
843
+ const canonicalOrder = order.map((sliceId) => ({ sliceId }));
844
+ if (canonicalOrder.length === 0) {
845
+ sendRouteError(res, 400, "mission-control.slices.reorder.validation", "order must contain at least one slice id");
846
+ return;
847
+ }
848
+ const rawRequest = deps.rawRequest ??
849
+ (typeof deps.client?.rawRequest === "function"
850
+ ? deps.client.rawRequest.bind(deps.client)
851
+ : null);
852
+ if (!rawRequest) {
853
+ sendRouteError(res, 503, "mission-control.slices.reorder.unavailable", "Canonical mission-control slices API is unavailable");
854
+ return;
855
+ }
856
+ const response = await rawRequest("POST", "/api/client/mission-control/slices/reorder", {
857
+ ...(workspaceId
858
+ ? {
859
+ workspace_id: workspaceId,
860
+ command_center_id: workspaceId,
861
+ }
862
+ : {}),
863
+ level,
864
+ ...(initiativeId ? { initiative_id: initiativeId } : {}),
865
+ order: canonicalOrder,
866
+ });
867
+ deps.sendJson(res, 200, {
868
+ ...(response && typeof response === "object" ? response : { ok: true }),
869
+ source: "canonical",
870
+ });
871
+ }
872
+ catch (err) {
873
+ sendRouteError(res, 503, "mission-control.slices.reorder.canonical", "Canonical mission-control slices API unavailable for reorder", {
874
+ degraded: [`canonical unavailable (${deps.safeErrorMessage(err)})`],
875
+ canonical_only: true,
876
+ });
877
+ }
878
+ }, "Mission-control slices reorder (canonical)");
879
+ router.add("POST", "mission-control/slices/order-mode", async ({ req, query, res }) => {
880
+ try {
881
+ const payload = await deps.parseJsonRequest(req);
882
+ const level = normalizeSliceLevel(deps.pickString(payload, ["level"]) ?? query.get("level"));
883
+ const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
884
+ query.get("initiativeId") ??
885
+ query.get("initiative_id") ??
886
+ "").trim() || null;
887
+ const scope = resolveWorkspaceScope(payload, query);
888
+ if (scope.error) {
889
+ sendRouteError(res, 400, "mission-control.slices.order-mode.validation", scope.error);
890
+ return;
891
+ }
892
+ const workspaceId = scope.workspaceId;
893
+ const orderMode = normalizeSliceOrderMode(deps.pickString(payload, ["orderMode", "order_mode"]) ??
894
+ query.get("orderMode") ??
895
+ query.get("order_mode"));
896
+ if (!orderMode) {
897
+ sendRouteError(res, 400, "mission-control.slices.order-mode.validation", "order_mode must be either 'manual' or 'algorithmic'");
898
+ return;
899
+ }
900
+ const rawRequest = deps.rawRequest ??
901
+ (typeof deps.client?.rawRequest === "function"
902
+ ? deps.client.rawRequest.bind(deps.client)
903
+ : null);
904
+ if (!rawRequest) {
905
+ sendRouteError(res, 503, "mission-control.slices.order-mode.unavailable", "Canonical mission-control slices API is unavailable");
906
+ return;
907
+ }
908
+ const response = await rawRequest("POST", "/api/client/mission-control/slices/order-mode", {
909
+ ...(workspaceId
910
+ ? {
911
+ workspace_id: workspaceId,
912
+ command_center_id: workspaceId,
913
+ }
914
+ : {}),
915
+ level,
916
+ ...(initiativeId ? { initiative_id: initiativeId } : {}),
917
+ order_mode: orderMode,
918
+ });
919
+ deps.sendJson(res, 200, {
920
+ ...(response && typeof response === "object" ? response : { ok: true }),
921
+ source: "canonical",
922
+ });
923
+ }
924
+ catch (err) {
925
+ sendRouteError(res, 503, "mission-control.slices.order-mode.canonical", "Canonical mission-control slices API unavailable for mode changes", {
926
+ degraded: [`canonical unavailable (${deps.safeErrorMessage(err)})`],
927
+ canonical_only: true,
928
+ });
929
+ }
930
+ }, "Mission-control slices order mode (canonical)");
931
+ router.add("POST", "mission-control/next-up/move", async ({ req, query, res }) => {
932
+ try {
933
+ const payload = await deps.parseJsonRequest(req);
934
+ const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
935
+ query.get("initiativeId") ??
936
+ query.get("initiative_id") ??
937
+ "")
938
+ .trim();
939
+ const workstreamId = (deps.pickString(payload, ["workstreamId", "workstream_id"]) ??
940
+ query.get("workstreamId") ??
941
+ query.get("workstream_id") ??
942
+ "")
943
+ .trim();
944
+ const placement = normalizePlacement(deps.pickString(payload, ["placement", "queuePlacement", "queue_placement"]) ??
945
+ query.get("placement") ??
946
+ query.get("queuePlacement") ??
947
+ query.get("queue_placement"), "bottom");
948
+ if (!initiativeId || !workstreamId) {
949
+ sendRouteError(res, 400, "mission-control.next-up.move.validation", "initiativeId and workstreamId are required");
950
+ return;
951
+ }
952
+ const queue = await deps.buildNextUpQueue({ initiativeId });
953
+ const order = dedupeQueueOrder(queue.items.map((item) => ({
954
+ initiativeId: item.initiativeId,
955
+ workstreamId: item.workstreamId,
956
+ })));
957
+ const key = `${initiativeId}:${workstreamId}`;
958
+ const current = order.filter((entry) => `${entry.initiativeId}:${entry.workstreamId}` !== key);
959
+ const nextOrder = placement === "top"
960
+ ? [{ initiativeId, workstreamId }, ...current]
961
+ : [...current, { initiativeId, workstreamId }];
962
+ const next = deps.setNextUpQueuePinOrder({ order: nextOrder });
963
+ deps.clearNextUpQueueCache(initiativeId);
964
+ deps.sendJson(res, 200, {
965
+ ok: true,
966
+ placement,
967
+ orderApplied: nextOrder.length,
968
+ pins: next.pins,
969
+ updatedAt: next.updatedAt,
970
+ });
971
+ }
972
+ catch (err) {
973
+ sendRouteException(res, "mission-control.next-up.move.handler", err);
974
+ }
975
+ }, "Mission-control next-up move");
976
+ router.add("POST", "mission-control/next-up/triage/stop", async ({ req, query, res }) => {
977
+ try {
978
+ const payload = await deps.parseJsonRequest(req);
979
+ const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
980
+ query.get("initiativeId") ??
981
+ query.get("initiative_id") ??
982
+ "")
983
+ .trim();
984
+ const workstreamId = (deps.pickString(payload, ["workstreamId", "workstream_id"]) ??
985
+ query.get("workstreamId") ??
986
+ query.get("workstream_id") ??
987
+ "")
988
+ .trim();
989
+ const placement = normalizePlacement(deps.pickString(payload, ["placement", "queuePlacement", "queue_placement"]) ??
990
+ query.get("placement") ??
991
+ query.get("queuePlacement") ??
992
+ query.get("queue_placement"), "bottom");
993
+ const resetToTodoRaw = payload.resetToTodo ??
994
+ payload.reset_to_todo ??
995
+ query.get("resetToTodo") ??
996
+ query.get("reset_to_todo") ??
997
+ null;
998
+ const resetToTodo = typeof resetToTodoRaw === "boolean"
999
+ ? resetToTodoRaw
1000
+ : deps.parseBooleanQuery(typeof resetToTodoRaw === "string" ? resetToTodoRaw : null) ?? false;
1001
+ if (!initiativeId || !workstreamId) {
1002
+ sendRouteError(res, 400, "mission-control.next-up.triage.stop.validation", "initiativeId and workstreamId are required");
1003
+ return;
1004
+ }
1005
+ const run = deps.autoContinueRuns.get(initiativeId) ?? null;
1006
+ let stoppedAutoContinue = false;
1007
+ if (run) {
1008
+ const now = new Date().toISOString();
1009
+ const activeRunIds = Array.isArray(run.activeSliceRunIds)
1010
+ ? run.activeSliceRunIds.filter((id) => typeof id === "string" && id.trim().length > 0)
1011
+ : typeof run.activeRunId === "string" && run.activeRunId.trim().length > 0
1012
+ ? [run.activeRunId]
1013
+ : [];
1014
+ run.stopRequested = true;
1015
+ run.status = activeRunIds.length > 0 ? "stopping" : "stopped";
1016
+ run.updatedAt = now;
1017
+ if (activeRunIds.length === 0) {
1018
+ await deps.stopAutoContinueRun({ run, reason: "stopped" });
1019
+ }
1020
+ else {
1021
+ try {
1022
+ await deps.updateInitiativeAutoContinueState({ initiativeId, run });
1023
+ }
1024
+ catch {
1025
+ // best effort
1026
+ }
1027
+ }
1028
+ stoppedAutoContinue = true;
1029
+ }
1030
+ let resetTaskCount = 0;
1031
+ if (resetToTodo) {
1032
+ const taskResult = await deps.client.listEntities("task", {
1033
+ initiative_id: initiativeId,
1034
+ workstream_id: workstreamId,
1035
+ limit: 100,
1036
+ });
1037
+ const tasks = Array.isArray(taskResult?.data) ? taskResult.data : [];
1038
+ const statesToReset = new Set(["running", "blocked"]);
1039
+ for (const task of tasks) {
1040
+ if (!task || typeof task !== "object")
1041
+ continue;
1042
+ const record = task;
1043
+ const taskId = deps.pickString(record, ["id"]);
1044
+ if (!taskId)
1045
+ continue;
1046
+ if (!shouldResetTaskStatus(record.status, statesToReset))
1047
+ continue;
1048
+ await deps.client.updateEntity("task", taskId, { status: "todo" });
1049
+ resetTaskCount += 1;
1050
+ }
1051
+ }
1052
+ const queue = await deps.buildNextUpQueue({ initiativeId });
1053
+ const order = dedupeQueueOrder(queue.items.map((item) => ({
1054
+ initiativeId: item.initiativeId,
1055
+ workstreamId: item.workstreamId,
1056
+ })));
1057
+ const targetKey = `${initiativeId}:${workstreamId}`;
1058
+ const nextOrder = buildPlacedOrder({
1059
+ order,
1060
+ targets: new Set([targetKey]),
1061
+ placement,
1062
+ });
1063
+ const next = deps.setNextUpQueuePinOrder({
1064
+ order: nextOrder.length > 0
1065
+ ? nextOrder
1066
+ : [{ initiativeId, workstreamId }],
1067
+ });
1068
+ deps.clearNextUpQueueCache(initiativeId);
1069
+ deps.sendJson(res, 200, {
1070
+ ok: true,
1071
+ placement,
1072
+ stoppedAutoContinue,
1073
+ resetToTodo,
1074
+ resetTaskCount,
1075
+ run,
1076
+ pins: next.pins,
1077
+ updatedAt: next.updatedAt,
1078
+ });
1079
+ }
1080
+ catch (err) {
1081
+ sendRouteException(res, "mission-control.next-up.triage.stop.handler", err);
1082
+ }
1083
+ }, "Mission-control next-up triage stop");
1084
+ router.add("POST", "mission-control/next-up/remove", async ({ req, query, res }) => {
1085
+ try {
1086
+ const payload = await deps.parseJsonRequest(req);
1087
+ const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
1088
+ query.get("initiativeId") ??
1089
+ query.get("initiative_id") ??
1090
+ "")
1091
+ .trim();
1092
+ const workstreamId = (deps.pickString(payload, ["workstreamId", "workstream_id"]) ??
1093
+ query.get("workstreamId") ??
1094
+ query.get("workstream_id") ??
1095
+ "")
1096
+ .trim();
1097
+ if (!initiativeId || !workstreamId) {
1098
+ sendRouteError(res, 400, "mission-control.next-up.remove.validation", "initiativeId and workstreamId are required");
1099
+ return;
1100
+ }
1101
+ deps.removeNextUpQueuePin({ initiativeId, workstreamId });
1102
+ const next = deps.suppressNextUpQueueItem({ initiativeId, workstreamId });
1103
+ deps.clearNextUpQueueCache(initiativeId);
1104
+ deps.sendJson(res, 200, {
1105
+ ok: true,
1106
+ removed: { initiativeId, workstreamId },
1107
+ suppressions: next.suppressions,
1108
+ updatedAt: next.updatedAt,
1109
+ });
1110
+ }
1111
+ catch (err) {
1112
+ sendRouteException(res, "mission-control.next-up.remove.handler", err);
1113
+ }
1114
+ }, "Mission-control next-up remove");
1115
+ router.add("POST", "mission-control/next-up/bulk", async ({ req, query, res }) => {
1116
+ try {
1117
+ const payload = await deps.parseJsonRequest(req);
1118
+ const actionRaw = deps.pickString(payload, ["action"]) ??
1119
+ query.get("action") ??
1120
+ "";
1121
+ const action = actionRaw.trim().toLowerCase();
1122
+ const initiativeScopeRaw = deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
1123
+ query.get("initiativeId") ??
1124
+ query.get("initiative_id") ??
1125
+ "";
1126
+ const initiativeScope = initiativeScopeRaw.trim() || null;
1127
+ const items = dedupeQueueOrder(parseQueueOrder(payload.items, deps));
1128
+ if (!["move_top", "move_bottom", "remove"].includes(action)) {
1129
+ sendRouteError(res, 400, "mission-control.next-up.bulk.validation", "action must be one of: move_top, move_bottom, remove");
1130
+ return;
1131
+ }
1132
+ if (items.length === 0) {
1133
+ sendRouteError(res, 400, "mission-control.next-up.bulk.validation", "items must include at least one initiativeId/workstreamId pair");
1134
+ return;
1135
+ }
1136
+ const queue = await deps.buildNextUpQueue({ initiativeId: initiativeScope });
1137
+ const baseOrder = dedupeQueueOrder(queue.items.map((item) => ({
1138
+ initiativeId: item.initiativeId,
1139
+ workstreamId: item.workstreamId,
1140
+ })));
1141
+ const knownKeys = new Set(baseOrder.map((entry) => `${entry.initiativeId}:${entry.workstreamId}`));
1142
+ const results = items.map((entry) => {
1143
+ const key = `${entry.initiativeId}:${entry.workstreamId}`;
1144
+ if (knownKeys.has(key)) {
1145
+ return { ...entry, ok: true };
1146
+ }
1147
+ return {
1148
+ ...entry,
1149
+ ok: false,
1150
+ error: "Queue item is not currently available in this scope",
1151
+ };
1152
+ });
1153
+ const targetKeys = new Set(results
1154
+ .filter((entry) => entry.ok)
1155
+ .map((entry) => `${entry.initiativeId}:${entry.workstreamId}`));
1156
+ let nextOrder = baseOrder;
1157
+ if (targetKeys.size > 0) {
1158
+ if (action === "remove") {
1159
+ for (const entry of results) {
1160
+ if (!entry.ok)
1161
+ continue;
1162
+ deps.removeNextUpQueuePin({
1163
+ initiativeId: entry.initiativeId,
1164
+ workstreamId: entry.workstreamId,
1165
+ });
1166
+ deps.suppressNextUpQueueItem({
1167
+ initiativeId: entry.initiativeId,
1168
+ workstreamId: entry.workstreamId,
1169
+ });
1170
+ }
1171
+ nextOrder = baseOrder.filter((entry) => {
1172
+ const key = `${entry.initiativeId}:${entry.workstreamId}`;
1173
+ return !targetKeys.has(key);
1174
+ });
1175
+ }
1176
+ else {
1177
+ nextOrder = buildPlacedOrder({
1178
+ order: baseOrder,
1179
+ targets: targetKeys,
1180
+ placement: action === "move_top" ? "top" : "bottom",
1181
+ });
1182
+ deps.setNextUpQueuePinOrder({ order: nextOrder });
1183
+ }
1184
+ deps.clearNextUpQueueCache(initiativeScope);
1185
+ }
1186
+ const updated = results.filter((entry) => entry.ok).length;
1187
+ const failed = results.length - updated;
1188
+ deps.sendJson(res, 200, {
1189
+ ok: true,
1190
+ action,
1191
+ requested: results.length,
1192
+ updated,
1193
+ failed,
1194
+ results: results.map((entry) => ({
1195
+ initiativeId: entry.initiativeId,
1196
+ workstreamId: entry.workstreamId,
1197
+ ok: entry.ok,
1198
+ error: entry.ok ? null : entry.error,
1199
+ })),
1200
+ orderSize: nextOrder.length,
1201
+ updatedAt: new Date().toISOString(),
1202
+ });
1203
+ }
1204
+ catch (err) {
1205
+ sendRouteException(res, "mission-control.next-up.bulk.handler", err);
1206
+ }
1207
+ }, "Mission-control next-up bulk");
1208
+ router.add("POST", "mission-control/next-up/clear", async ({ req, query, res }) => {
1209
+ try {
1210
+ const payload = await deps.parseJsonRequest(req);
1211
+ const initiativeIdRaw = deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
1212
+ query.get("initiativeId") ??
1213
+ query.get("initiative_id") ??
1214
+ "";
1215
+ const initiativeId = initiativeIdRaw.trim() || null;
1216
+ const workstreamIdRaw = deps.pickString(payload, ["workstreamId", "workstream_id"]) ??
1217
+ query.get("workstreamId") ??
1218
+ query.get("workstream_id") ??
1219
+ "";
1220
+ const workstreamId = workstreamIdRaw.trim() || null;
1221
+ const placement = normalizePlacement(deps.pickString(payload, ["placement", "queuePlacement", "queue_placement"]) ??
1222
+ query.get("placement") ??
1223
+ query.get("queuePlacement") ??
1224
+ query.get("queue_placement"), "bottom");
1225
+ const requestedStates = deps.dedupeStrings([
1226
+ ...deps.pickStringArray(payload, ["states", "queueStates", "queue_states"]),
1227
+ ...(query.get("states") ?? query.get("queueStates") ?? query.get("queue_states") ?? "")
1228
+ .split(",")
1229
+ .map((entry) => entry.trim())
1230
+ .filter(Boolean),
1231
+ ])
1232
+ .map((entry) => entry.trim().toLowerCase())
1233
+ .filter((entry) => entry === "running" || entry === "blocked");
1234
+ const states = new Set(requestedStates.length > 0 ? requestedStates : ["running", "blocked"]);
1235
+ const queue = await deps.buildNextUpQueue({ initiativeId });
1236
+ const scopedItems = queue.items.filter((item) => {
1237
+ if (initiativeId && item.initiativeId !== initiativeId)
1238
+ return false;
1239
+ if (workstreamId && item.workstreamId !== workstreamId)
1240
+ return false;
1241
+ if (states.has(item.queueState))
1242
+ return true;
1243
+ if (states.has("running") &&
1244
+ item.autoContinue?.status === "running" &&
1245
+ !item.autoContinue?.stopReason) {
1246
+ return true;
1247
+ }
1248
+ return false;
1249
+ });
1250
+ const updatedTaskIds = new Set();
1251
+ let failedUpdates = 0;
1252
+ for (const item of scopedItems) {
1253
+ let taskRows = [];
1254
+ try {
1255
+ const response = await deps.client.listEntities("task", {
1256
+ initiative_id: item.initiativeId,
1257
+ workstream_id: item.workstreamId,
1258
+ limit: 100,
1259
+ });
1260
+ taskRows = Array.isArray(response?.data) ? response.data : [];
1261
+ }
1262
+ catch {
1263
+ // best effort: keep progressing through queue
1264
+ continue;
1265
+ }
1266
+ for (const row of taskRows) {
1267
+ if (!row || typeof row !== "object")
1268
+ continue;
1269
+ const record = row;
1270
+ const taskId = deps.pickString(record, ["id"]);
1271
+ if (!taskId || updatedTaskIds.has(taskId))
1272
+ continue;
1273
+ if (!shouldResetTaskStatus(record.status, states))
1274
+ continue;
1275
+ try {
1276
+ await deps.client.updateEntity("task", taskId, { status: "todo" });
1277
+ updatedTaskIds.add(taskId);
1278
+ }
1279
+ catch {
1280
+ failedUpdates += 1;
1281
+ }
1282
+ }
1283
+ }
1284
+ const baseOrder = dedupeQueueOrder(queue.items.map((item) => ({
1285
+ initiativeId: item.initiativeId,
1286
+ workstreamId: item.workstreamId,
1287
+ })));
1288
+ const targetKeys = new Set(scopedItems.map((item) => `${item.initiativeId}:${item.workstreamId}`));
1289
+ const nextOrder = buildPlacedOrder({
1290
+ order: baseOrder,
1291
+ targets: targetKeys,
1292
+ placement,
1293
+ });
1294
+ const next = deps.setNextUpQueuePinOrder({ order: nextOrder });
1295
+ deps.clearNextUpQueueCache(initiativeId);
1296
+ deps.sendJson(res, 200, {
1297
+ ok: true,
1298
+ placement,
1299
+ states: Array.from(states),
1300
+ queueItemsCleared: scopedItems.length,
1301
+ tasksReset: updatedTaskIds.size,
1302
+ taskResetFailures: failedUpdates,
1303
+ pins: next.pins,
1304
+ updatedAt: next.updatedAt,
1305
+ });
1306
+ }
1307
+ catch (err) {
1308
+ sendRouteException(res, "mission-control.next-up.clear.handler", err);
1309
+ }
1310
+ }, "Mission-control next-up clear");
1311
+ router.add("POST", "mission-control/graph/cycles/auto-fix", async ({ req, query, res }) => {
1312
+ try {
1313
+ const payload = await deps.parseJsonRequest(req);
1314
+ const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
1315
+ query.get("initiativeId") ??
1316
+ query.get("initiative_id") ??
1317
+ "")
1318
+ .trim();
1319
+ const dryRunRaw = payload.dryRun ??
1320
+ payload.dry_run ??
1321
+ query.get("dryRun") ??
1322
+ query.get("dry_run") ??
1323
+ null;
1324
+ const dryRun = typeof dryRunRaw === "boolean"
1325
+ ? dryRunRaw
1326
+ : deps.parseBooleanQuery(typeof dryRunRaw === "string" ? dryRunRaw : null) ?? false;
1327
+ if (!initiativeId) {
1328
+ sendRouteError(res, 400, "mission-control.graph.cycles.auto-fix.validation", "initiativeId is required");
1329
+ return;
1330
+ }
1331
+ const graph = deps.applyLocalInitiativeOverrideToGraph(await deps.buildMissionControlGraph(initiativeId));
1332
+ const diagnosticsRemovedEdges = parseCycleDiagnosticsRemovedEdges(graph);
1333
+ const graphNodes = parseCycleGraphNodes(graph);
1334
+ const nodeById = new Map(graphNodes.map((node) => [node.id, node]));
1335
+ const workingDependencies = new Map(graphNodes.map((node) => [node.id, new Set(node.dependencyIds)]));
1336
+ const removedEdgeKeys = new Set();
1337
+ const maxPasses = 12;
1338
+ for (let pass = 0; pass < maxPasses; pass += 1) {
1339
+ const edges = [];
1340
+ for (const node of graphNodes) {
1341
+ const depsSet = workingDependencies.get(node.id) ?? new Set();
1342
+ for (const depId of depsSet.values()) {
1343
+ if (!nodeById.has(depId) || depId === node.id)
1344
+ continue;
1345
+ edges.push({ from: depId, to: node.id });
1346
+ }
1347
+ }
1348
+ const cycleEdgeKeys = detectCycleEdgeKeys(edges);
1349
+ if (cycleEdgeKeys.size === 0)
1350
+ break;
1351
+ let removedInPass = 0;
1352
+ for (const edgeKey of cycleEdgeKeys.values()) {
1353
+ const [from, to] = edgeKey.split("->", 2);
1354
+ if (!from || !to)
1355
+ continue;
1356
+ const nodeDeps = workingDependencies.get(to);
1357
+ if (!nodeDeps || !nodeDeps.has(from))
1358
+ continue;
1359
+ nodeDeps.delete(from);
1360
+ removedEdgeKeys.add(edgeKey);
1361
+ removedInPass += 1;
1362
+ }
1363
+ if (removedInPass === 0)
1364
+ break;
1365
+ }
1366
+ let removedEdges = Array.from(removedEdgeKeys.values())
1367
+ .map((edgeKey) => {
1368
+ const [from, to] = edgeKey.split("->", 2);
1369
+ if (!from || !to)
1370
+ return null;
1371
+ return { from, to };
1372
+ })
1373
+ .filter((entry) => Boolean(entry));
1374
+ if (removedEdges.length === 0 && diagnosticsRemovedEdges.length > 0) {
1375
+ removedEdges = diagnosticsRemovedEdges;
1376
+ }
1377
+ const affectedNodes = new Map();
1378
+ for (const edge of removedEdges) {
1379
+ const node = nodeById.get(edge.to);
1380
+ if (!node)
1381
+ continue;
1382
+ const existing = affectedNodes.get(node.id) ?? {
1383
+ id: node.id,
1384
+ type: node.type,
1385
+ title: node.title,
1386
+ workstreamId: node.workstreamId,
1387
+ removedDependencyIds: [],
1388
+ dependencyIds: [],
1389
+ };
1390
+ if (!existing.removedDependencyIds.includes(edge.from)) {
1391
+ existing.removedDependencyIds.push(edge.from);
1392
+ }
1393
+ existing.dependencyIds = Array.from((workingDependencies.get(node.id) ?? new Set()).values());
1394
+ affectedNodes.set(node.id, existing);
1395
+ }
1396
+ const affected = Array.from(affectedNodes.values()).sort((left, right) => left.title.localeCompare(right.title));
1397
+ if (dryRun) {
1398
+ deps.sendJson(res, 200, {
1399
+ ok: true,
1400
+ dryRun: true,
1401
+ initiativeId,
1402
+ cycleEdgesDetected: removedEdges.length,
1403
+ nodesToUpdate: affected.length,
1404
+ removedEdges,
1405
+ affected,
1406
+ });
1407
+ return;
1408
+ }
1409
+ const updateResults = [];
1410
+ for (const node of affected) {
1411
+ if (node.type !== "initiative" &&
1412
+ node.type !== "workstream" &&
1413
+ node.type !== "milestone" &&
1414
+ node.type !== "task") {
1415
+ updateResults.push({
1416
+ id: node.id,
1417
+ type: node.type,
1418
+ ok: false,
1419
+ error: "Unsupported entity type for dependency update",
1420
+ dependencyIds: node.dependencyIds,
1421
+ removedDependencyIds: node.removedDependencyIds,
1422
+ });
1423
+ continue;
1424
+ }
1425
+ try {
1426
+ await deps.client.updateEntity(node.type, node.id, {
1427
+ depends_on: node.dependencyIds,
1428
+ dependency_ids: node.dependencyIds,
1429
+ dependencyIds: node.dependencyIds,
1430
+ });
1431
+ updateResults.push({
1432
+ id: node.id,
1433
+ type: node.type,
1434
+ ok: true,
1435
+ dependencyIds: node.dependencyIds,
1436
+ removedDependencyIds: node.removedDependencyIds,
1437
+ });
1438
+ }
1439
+ catch (err) {
1440
+ updateResults.push({
1441
+ id: node.id,
1442
+ type: node.type,
1443
+ ok: false,
1444
+ error: deps.safeErrorMessage(err),
1445
+ dependencyIds: node.dependencyIds,
1446
+ removedDependencyIds: node.removedDependencyIds,
1447
+ });
1448
+ }
1449
+ }
1450
+ const scheduled = [];
1451
+ const failedSchedules = [];
1452
+ const workstreamIds = Array.from(new Set(affected
1453
+ .map((node) => {
1454
+ if (node.type === "workstream")
1455
+ return node.id;
1456
+ return node.workstreamId;
1457
+ })
1458
+ .filter((workstreamId) => typeof workstreamId === "string" &&
1459
+ workstreamId.trim().length > 0)));
1460
+ for (const workstreamId of workstreamIds) {
1461
+ try {
1462
+ const scheduledFix = await deps.scheduleAutoFixForWorkstream({
1463
+ initiativeId,
1464
+ workstreamId,
1465
+ runId: null,
1466
+ event: "dependency_cycle_auto_fix",
1467
+ requestedByAgentId: "orgx-orchestrator",
1468
+ requestedByAgentName: "OrgX Orchestrator",
1469
+ graceMs: 250,
1470
+ });
1471
+ scheduled.push({
1472
+ workstreamId,
1473
+ requestId: scheduledFix.requestId,
1474
+ });
1475
+ }
1476
+ catch (err) {
1477
+ failedSchedules.push({
1478
+ workstreamId,
1479
+ error: deps.safeErrorMessage(err),
1480
+ });
1481
+ }
1482
+ }
1483
+ if (removedEdges.length > 0 || affected.length > 0) {
1484
+ deps.clearNextUpQueueCache(initiativeId);
1485
+ }
1486
+ const updated = updateResults.filter((result) => result.ok).length;
1487
+ const failed = updateResults.length - updated;
1488
+ deps.sendJson(res, 200, {
1489
+ ok: true,
1490
+ initiativeId,
1491
+ cycleEdgesDetected: removedEdges.length,
1492
+ nodesUpdated: updated,
1493
+ nodesFailed: failed,
1494
+ removedEdges,
1495
+ updates: updateResults,
1496
+ scheduledAutofixes: scheduled,
1497
+ autofixScheduleFailures: failedSchedules,
1498
+ });
1499
+ }
1500
+ catch (err) {
1501
+ sendRouteException(res, "mission-control.graph.cycles.auto-fix.handler", err);
1502
+ }
1503
+ }, "Mission-control dependency cycle auto-fix");
1504
+ router.add("POST", "mission-control/activity/auto-fix", async ({ req, query, res }) => {
1505
+ try {
1506
+ const payload = await deps.parseJsonRequest(req);
1507
+ const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
1508
+ query.get("initiativeId") ??
1509
+ query.get("initiative_id") ??
1510
+ "")
1511
+ .trim();
1512
+ const workstreamId = (deps.pickString(payload, ["workstreamId", "workstream_id"]) ??
1513
+ query.get("workstreamId") ??
1514
+ query.get("workstream_id") ??
1515
+ "")
1516
+ .trim();
1517
+ if (!initiativeId || !workstreamId) {
1518
+ sendRouteError(res, 400, "mission-control.activity.auto-fix.validation", "initiativeId and workstreamId are required");
1519
+ return;
1520
+ }
1521
+ const runId = (deps.pickString(payload, ["runId", "run_id", "sessionId", "session_id"]) ??
1522
+ query.get("runId") ??
1523
+ query.get("run_id") ??
1524
+ query.get("sessionId") ??
1525
+ query.get("session_id") ??
1526
+ "")
1527
+ .trim() || null;
1528
+ const event = (deps.pickString(payload, ["event", "eventName", "event_name"]) ??
1529
+ query.get("event") ??
1530
+ query.get("eventName") ??
1531
+ query.get("event_name") ??
1532
+ "")
1533
+ .trim() || null;
1534
+ const requestedByAgentId = (deps.pickString(payload, ["requestedByAgentId", "requested_by_agent_id"]) ??
1535
+ query.get("requestedByAgentId") ??
1536
+ query.get("requested_by_agent_id") ??
1537
+ "")
1538
+ .trim() || null;
1539
+ const requestedByAgentName = (deps.pickString(payload, ["requestedByAgentName", "requested_by_agent_name"]) ??
1540
+ query.get("requestedByAgentName") ??
1541
+ query.get("requested_by_agent_name") ??
1542
+ "")
1543
+ .trim() || null;
1544
+ const graceMsFromQueryRaw = query.get("graceMs") ??
1545
+ query.get("grace_ms") ??
1546
+ query.get("delayMs") ??
1547
+ query.get("delay_ms") ??
1548
+ null;
1549
+ const graceMsFromQuery = typeof graceMsFromQueryRaw === "string" && graceMsFromQueryRaw.trim().length > 0
1550
+ ? Number(graceMsFromQueryRaw)
1551
+ : null;
1552
+ const graceMs = deps.pickNumber(payload, ["graceMs", "grace_ms", "delayMs", "delay_ms"]) ??
1553
+ (Number.isFinite(graceMsFromQuery) ? graceMsFromQuery : null);
1554
+ const schedule = await deps.scheduleAutoFixForWorkstream({
1555
+ initiativeId,
1556
+ workstreamId,
1557
+ runId,
1558
+ event,
1559
+ requestedByAgentId,
1560
+ requestedByAgentName,
1561
+ graceMs,
1562
+ });
1563
+ deps.sendJson(res, 202, {
1564
+ ok: true,
1565
+ scheduled: schedule,
1566
+ });
1567
+ }
1568
+ catch (err) {
1569
+ sendRouteException(res, "mission-control.activity.auto-fix.handler", err);
1570
+ }
1571
+ }, "Mission-control activity auto-fix");
1572
+ router.add("POST", "mission-control/auto-continue/start", async ({ req, query, res }) => {
1573
+ try {
1574
+ const payload = await deps.parseJsonRequest(req);
1575
+ const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
1576
+ query.get("initiativeId") ??
1577
+ query.get("initiative_id") ??
1578
+ "")
1579
+ .trim();
1580
+ if (!initiativeId) {
1581
+ sendRouteError(res, 400, "mission-control.auto-continue.start.validation", "initiativeId is required");
1582
+ return;
1583
+ }
1584
+ const agentIdRaw = (deps.pickString(payload, ["agentId", "agent_id"]) ??
1585
+ query.get("agentId") ??
1586
+ query.get("agent_id") ??
1587
+ "main")
1588
+ .trim();
1589
+ const agentId = agentIdRaw || "main";
1590
+ if (!/^[a-zA-Z0-9_-]+$/.test(agentId)) {
1591
+ sendRouteError(res, 400, "mission-control.auto-continue.start.validation", "agentId must be a simple identifier (letters, numbers, _ or -).");
1592
+ return;
1593
+ }
1594
+ const tokenBudget = deps.pickNumber(payload, [
1595
+ "tokenBudget",
1596
+ "token_budget",
1597
+ "tokenBudgetTokens",
1598
+ "token_budget_tokens",
1599
+ "maxTokens",
1600
+ "max_tokens",
1601
+ ]) ??
1602
+ query.get("tokenBudget") ??
1603
+ query.get("token_budget") ??
1604
+ query.get("tokenBudgetTokens") ??
1605
+ query.get("token_budget_tokens") ??
1606
+ query.get("maxTokens") ??
1607
+ query.get("max_tokens") ??
1608
+ null;
1609
+ const includeVerificationRaw = payload.includeVerification ??
1610
+ payload.include_verification ??
1611
+ query.get("includeVerification") ??
1612
+ query.get("include_verification") ??
1613
+ null;
1614
+ const includeVerification = typeof includeVerificationRaw === "boolean"
1615
+ ? includeVerificationRaw
1616
+ : deps.parseBooleanQuery(typeof includeVerificationRaw === "string"
1617
+ ? includeVerificationRaw
1618
+ : null);
1619
+ const ignoreSpawnGuardRateLimitRaw = payload.ignoreSpawnGuardRateLimit ??
1620
+ payload.ignore_spawn_guard_rate_limit ??
1621
+ query.get("ignoreSpawnGuardRateLimit") ??
1622
+ query.get("ignore_spawn_guard_rate_limit") ??
1623
+ null;
1624
+ const ignoreSpawnGuardRateLimit = typeof ignoreSpawnGuardRateLimitRaw === "boolean"
1625
+ ? ignoreSpawnGuardRateLimitRaw
1626
+ : deps.parseBooleanQuery(typeof ignoreSpawnGuardRateLimitRaw === "string"
1627
+ ? ignoreSpawnGuardRateLimitRaw
1628
+ : null);
1629
+ const workstreamFilter = deps.dedupeStrings([
1630
+ ...deps.pickStringArray(payload, [
1631
+ "workstreamIds",
1632
+ "workstream_ids",
1633
+ "workstreamId",
1634
+ "workstream_id",
1635
+ ]),
1636
+ ...(query.get("workstreamIds") ??
1637
+ query.get("workstream_ids") ??
1638
+ query.get("workstreamId") ??
1639
+ query.get("workstream_id") ??
1640
+ "")
1641
+ .split(",")
1642
+ .map((entry) => entry.trim())
1643
+ .filter(Boolean),
1644
+ ]);
1645
+ const allowedWorkstreamIds = workstreamFilter.length > 0 ? workstreamFilter : null;
1646
+ const maxParallelRaw = deps.pickNumber(payload, [
1647
+ "maxParallelSlices",
1648
+ "max_parallel_slices",
1649
+ "maxParallel",
1650
+ "max_parallel",
1651
+ ]) ??
1652
+ query.get("maxParallelSlices") ??
1653
+ query.get("max_parallel_slices") ??
1654
+ query.get("maxParallel") ??
1655
+ query.get("max_parallel") ??
1656
+ null;
1657
+ const parallelModeRaw = (deps.pickString(payload, ["parallelMode", "parallel_mode"]) ??
1658
+ query.get("parallelMode") ??
1659
+ query.get("parallel_mode") ??
1660
+ "iwmt")
1661
+ .trim()
1662
+ .toLowerCase();
1663
+ const parallelMode = parallelModeRaw === "iwmt" ? "iwmt" : "iwmt";
1664
+ const startScopeRaw = deps.pickString(payload, ["scope", "sliceScope", "slice_scope"]) ??
1665
+ query.get("scope") ??
1666
+ query.get("sliceScope") ??
1667
+ query.get("slice_scope") ??
1668
+ null;
1669
+ const startScope = startScopeRaw === "milestone" || startScopeRaw === "workstream"
1670
+ ? startScopeRaw
1671
+ : "task";
1672
+ const run = await deps.startAutoContinueRun({
1673
+ initiativeId,
1674
+ agentId,
1675
+ agentName: await deps.resolveAgentDisplayName(agentId, null),
1676
+ tokenBudget,
1677
+ includeVerification,
1678
+ allowedWorkstreamIds,
1679
+ maxParallelSlices: maxParallelRaw,
1680
+ parallelMode,
1681
+ ignoreSpawnGuardRateLimit: ignoreSpawnGuardRateLimit === true,
1682
+ scope: startScope,
1683
+ });
1684
+ const dispatchEnvelope = buildDispatchGatewayEnvelope({
1685
+ dispatchMode: "server",
1686
+ route: "mission-control.auto-continue.start",
1687
+ source: "auto_continue_start",
1688
+ initiativeId,
1689
+ workstreamIds: allowedWorkstreamIds,
1690
+ });
1691
+ deps.sendJson(res, 200, { ok: true, ...dispatchEnvelope, run });
1692
+ }
1693
+ catch (err) {
1694
+ sendRouteException(res, "mission-control.auto-continue.start.handler", err);
1695
+ }
1696
+ }, "Mission-control auto-continue start");
1697
+ router.add("POST", "mission-control/auto-continue/stop", async ({ req, query, res }) => {
1698
+ try {
1699
+ const payload = await deps.parseJsonRequest(req);
1700
+ const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
1701
+ query.get("initiativeId") ??
1702
+ query.get("initiative_id") ??
1703
+ "")
1704
+ .trim();
1705
+ if (!initiativeId) {
1706
+ sendRouteError(res, 400, "mission-control.auto-continue.stop.validation", "initiativeId is required");
1707
+ return;
1708
+ }
1709
+ const run = deps.autoContinueRuns.get(initiativeId) ?? null;
1710
+ if (!run) {
1711
+ sendRouteError(res, 404, "mission-control.auto-continue.stop.lookup", "No auto-continue run found");
1712
+ return;
1713
+ }
1714
+ const now = new Date().toISOString();
1715
+ const activeRunIds = Array.isArray(run.activeSliceRunIds)
1716
+ ? run.activeSliceRunIds.filter((id) => typeof id === "string" && id.trim().length > 0)
1717
+ : typeof run.activeRunId === "string" && run.activeRunId.trim().length > 0
1718
+ ? [run.activeRunId]
1719
+ : [];
1720
+ run.stopRequested = true;
1721
+ run.status = activeRunIds.length > 0 ? "stopping" : "stopped";
1722
+ run.updatedAt = now;
1723
+ if (activeRunIds.length === 0) {
1724
+ await deps.stopAutoContinueRun({ run, reason: "stopped" });
1725
+ }
1726
+ else {
1727
+ try {
1728
+ await deps.updateInitiativeAutoContinueState({ initiativeId, run });
1729
+ }
1730
+ catch {
1731
+ // best effort
1732
+ }
1733
+ }
1734
+ deps.sendJson(res, 200, { ok: true, run });
1735
+ }
1736
+ catch (err) {
1737
+ sendRouteException(res, "mission-control.auto-continue.stop.handler", err);
1738
+ }
1739
+ }, "Mission-control auto-continue stop");
1740
+ router.add("POST", "mission-control/auto-continue/tick", async ({ req, query, res }) => {
1741
+ try {
1742
+ const payload = await deps.parseJsonRequest(req);
1743
+ const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
1744
+ query.get("initiativeId") ??
1745
+ query.get("initiative_id") ??
1746
+ "")
1747
+ .trim();
1748
+ if (initiativeId) {
1749
+ const run = deps.autoContinueRuns.get(initiativeId) ?? null;
1750
+ if (!run) {
1751
+ sendRouteError(res, 404, "mission-control.auto-continue.tick.lookup", "No auto-continue run found");
1752
+ return;
1753
+ }
1754
+ await deps.tickAutoContinueRun(run);
1755
+ deps.sendJson(res, 200, { ok: true, initiativeId, run });
1756
+ return;
1757
+ }
1758
+ await deps.tickAllAutoContinue();
1759
+ deps.sendJson(res, 200, { ok: true });
1760
+ }
1761
+ catch (err) {
1762
+ sendRouteException(res, "mission-control.auto-continue.tick.handler", err);
1763
+ }
1764
+ }, "Mission-control auto-continue tick");
1765
+ router.add("POST", "mission-control/assignments/auto", async ({ req, res }) => {
1766
+ try {
1767
+ const payload = await deps.parseJsonRequest(req);
1768
+ const entityId = deps.pickString(payload, ["entity_id", "entityId"]);
1769
+ const entityType = deps.pickString(payload, ["entity_type", "entityType"]);
1770
+ const initiativeId = deps.pickString(payload, ["initiative_id", "initiativeId"]) ?? null;
1771
+ const title = deps.pickString(payload, ["title", "name"]) ?? "Untitled";
1772
+ const summary = deps.pickString(payload, ["summary", "description", "context"]) ?? null;
1773
+ if (!entityId || !entityType) {
1774
+ sendRouteError(res, 400, "mission-control.assignments.auto.validation", "entity_id and entity_type are required.");
1775
+ return;
1776
+ }
1777
+ const assignment = await deps.resolveAutoAssignments({
1778
+ client: deps.client,
1779
+ entityId,
1780
+ entityType,
1781
+ initiativeId,
1782
+ title,
1783
+ summary,
1784
+ });
1785
+ deps.sendJson(res, 200, assignment);
1786
+ }
1787
+ catch (err) {
1788
+ sendRouteException(res, "mission-control.assignments.auto.handler", err);
1789
+ }
1790
+ }, "Mission-control auto assignment");
1791
+ }