@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,3 +1,6 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { resolveWorkspaceScope as resolveCanonicalWorkspaceScope } from "../helpers/workspace-scope.js";
3
+ import { buildDispatchGatewayEnvelope } from "./dispatch-gateway-envelope.js";
1
4
  const PLAY_QUEUE_LOOKUP_TIMEOUT_MS = (() => {
2
5
  const raw = process.env.ORGX_PLAY_QUEUE_LOOKUP_TIMEOUT_MS;
3
6
  const parsed = Number(raw);
@@ -5,6 +8,17 @@ const PLAY_QUEUE_LOOKUP_TIMEOUT_MS = (() => {
5
8
  return 350;
6
9
  return Math.max(200, Math.floor(parsed));
7
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"]);
8
22
  async function withSoftTimeout(work, timeoutMs) {
9
23
  let timer = null;
10
24
  try {
@@ -22,7 +36,275 @@ async function withSoftTimeout(work, timeoutMs) {
22
36
  clearTimeout(timer);
23
37
  }
24
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
+ }
25
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
+ });
26
308
  router.add("POST", "mission-control/next-up/play", async ({ req, query, res }) => {
27
309
  try {
28
310
  const payload = await deps.parseJsonRequest(req);
@@ -37,10 +319,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
37
319
  "")
38
320
  .trim();
39
321
  if (!initiativeId || !workstreamId) {
40
- deps.sendJson(res, 400, {
41
- ok: false,
42
- error: "initiativeId and workstreamId are required",
43
- });
322
+ sendRouteError(res, 400, "mission-control.next-up.play.validation", "initiativeId and workstreamId are required");
44
323
  return;
45
324
  }
46
325
  let agentIdRaw = (deps.pickString(payload, ["agentId", "agent_id"]) ??
@@ -75,10 +354,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
75
354
  }
76
355
  const agentId = agentIdRaw || "main";
77
356
  if (!/^[a-zA-Z0-9_-]+$/.test(agentId)) {
78
- deps.sendJson(res, 400, {
79
- ok: false,
80
- error: "agentId must be a simple identifier (letters, numbers, _ or -).",
81
- });
357
+ sendRouteError(res, 400, "mission-control.next-up.play.validation", "agentId must be a simple identifier (letters, numbers, _ or -).");
82
358
  return;
83
359
  }
84
360
  const requestedAgentName = await deps.resolveAgentDisplayName(agentId, matchedQueueItem?.runnerAgentId === agentId
@@ -109,11 +385,48 @@ export function registerMissionControlActionsRoutes(router, deps) {
109
385
  : deps.parseBooleanQuery(typeof includeVerificationRaw === "string"
110
386
  ? includeVerificationRaw
111
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);
112
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
+ : [];
113
426
  if (existingRun &&
114
427
  (existingRun.status === "running" || existingRun.status === "stopping") &&
115
- existingRun.activeRunId) {
116
- const activeSlice = deps.autoContinueSliceRuns.get(existingRun.activeRunId) ?? null;
428
+ existingActiveRunIds.length > 0) {
429
+ const activeSlice = deps.autoContinueSliceRuns.get(existingActiveRunIds[0]) ?? null;
117
430
  const activeWorkstreamId = activeSlice?.workstreamId ?? null;
118
431
  const activeWorkstreamTitle = activeSlice?.workstreamTitle ?? null;
119
432
  deps.sendJson(res, 409, {
@@ -123,8 +436,10 @@ export function registerMissionControlActionsRoutes(router, deps) {
123
436
  ? `Auto-continue is already running for ${activeWorkstreamTitle ?? activeWorkstreamId}. Stop it before launching another Play run.`
124
437
  : "Auto-continue is already running for this initiative. Stop it before launching another Play run.",
125
438
  run: existingRun,
439
+ activeRunIds: existingActiveRunIds,
126
440
  activeWorkstreamId,
127
441
  activeWorkstreamTitle,
442
+ error_location: "mission-control.next-up.play.concurrent_run",
128
443
  });
129
444
  return;
130
445
  }
@@ -135,7 +450,24 @@ export function registerMissionControlActionsRoutes(router, deps) {
135
450
  tokenBudget,
136
451
  includeVerification,
137
452
  allowedWorkstreamIds: [workstreamId],
453
+ maxParallelSlices,
454
+ parallelMode,
138
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
+ : [],
139
471
  });
140
472
  let fallbackDispatch = null;
141
473
  const maybeDispatchFallback = async () => {
@@ -172,26 +504,50 @@ export function registerMissionControlActionsRoutes(router, deps) {
172
504
  new Promise((resolve) => setTimeout(() => resolve(false), 1100)),
173
505
  ]);
174
506
  if (!tickCompleted) {
175
- void tickPromise
176
- .then(async () => {
177
- await maybeDispatchFallback().catch(() => null);
178
- })
179
- .catch(() => {
180
- // best effort
181
- });
182
- deps.sendJson(res, 202, {
183
- ok: true,
184
- run,
185
- initiativeId,
186
- workstreamId,
187
- agentId,
188
- dispatchMode: "pending",
189
- sessionId: null,
190
- });
191
- return;
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();
192
550
  }
193
- await tickPromise;
194
- fallbackDispatch = await maybeDispatchFallback();
195
551
  }
196
552
  const fallbackStarted = Boolean(fallbackDispatch?.sessionId);
197
553
  const dispatchMode = run.activeRunId
@@ -215,8 +571,42 @@ export function registerMissionControlActionsRoutes(router, deps) {
215
571
  initiativeId,
216
572
  workstreamId,
217
573
  agentId,
218
- dispatchMode: finalizedDispatchMode,
574
+ ...playDispatchEnvelope(finalizedDispatchMode),
219
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,
220
610
  });
221
611
  return;
222
612
  }
@@ -241,6 +631,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
241
631
  workstreamId,
242
632
  agentId,
243
633
  fallbackDispatch,
634
+ error_location: "mission-control.next-up.play.dispatch",
244
635
  });
245
636
  return;
246
637
  }
@@ -250,14 +641,109 @@ export function registerMissionControlActionsRoutes(router, deps) {
250
641
  initiativeId,
251
642
  workstreamId,
252
643
  agentId,
253
- dispatchMode,
644
+ ...playDispatchEnvelope(dispatchMode),
254
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,
255
657
  });
256
658
  }
257
659
  catch (err) {
258
- deps.sendJson(res, 500, { ok: false, error: deps.safeErrorMessage(err) });
660
+ sendRouteException(res, "mission-control.next-up.play.handler", err);
259
661
  }
260
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)");
261
747
  router.add("POST", "mission-control/next-up/pin", async ({ req, query, res }) => {
262
748
  try {
263
749
  const payload = await deps.parseJsonRequest(req);
@@ -286,10 +772,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
286
772
  ]) ?? "")
287
773
  .trim() || null;
288
774
  if (!initiativeId || !workstreamId) {
289
- deps.sendJson(res, 400, {
290
- ok: false,
291
- error: "initiativeId and workstreamId are required",
292
- });
775
+ sendRouteError(res, 400, "mission-control.next-up.pin.validation", "initiativeId and workstreamId are required");
293
776
  return;
294
777
  }
295
778
  const next = deps.upsertNextUpQueuePin({
@@ -298,10 +781,11 @@ export function registerMissionControlActionsRoutes(router, deps) {
298
781
  preferredTaskId,
299
782
  preferredMilestoneId,
300
783
  });
784
+ deps.clearNextUpQueueCache(initiativeId);
301
785
  deps.sendJson(res, 200, { ok: true, pins: next.pins, updatedAt: next.updatedAt });
302
786
  }
303
787
  catch (err) {
304
- deps.sendJson(res, 500, { ok: false, error: deps.safeErrorMessage(err) });
788
+ sendRouteException(res, "mission-control.next-up.pin.handler", err);
305
789
  }
306
790
  }, "Mission-control next-up pin");
307
791
  router.add("POST", "mission-control/next-up/unpin", async ({ req, query, res }) => {
@@ -318,50 +802,773 @@ export function registerMissionControlActionsRoutes(router, deps) {
318
802
  "")
319
803
  .trim();
320
804
  if (!initiativeId || !workstreamId) {
321
- deps.sendJson(res, 400, {
322
- ok: false,
323
- error: "initiativeId and workstreamId are required",
324
- });
805
+ sendRouteError(res, 400, "mission-control.next-up.unpin.validation", "initiativeId and workstreamId are required");
325
806
  return;
326
807
  }
327
808
  const next = deps.removeNextUpQueuePin({ initiativeId, workstreamId });
809
+ deps.clearNextUpQueueCache(initiativeId);
328
810
  deps.sendJson(res, 200, { ok: true, pins: next.pins, updatedAt: next.updatedAt });
329
811
  }
330
812
  catch (err) {
331
- deps.sendJson(res, 500, { ok: false, error: deps.safeErrorMessage(err) });
813
+ sendRouteException(res, "mission-control.next-up.unpin.handler", err);
332
814
  }
333
815
  }, "Mission-control next-up unpin");
334
816
  router.add("POST", "mission-control/next-up/reorder", async ({ req, res }) => {
335
817
  try {
336
818
  const payload = await deps.parseJsonRequest(req);
337
- const rawOrder = Array.isArray(payload?.order)
338
- ? payload.order
339
- : [];
340
- const order = [];
341
- for (const entry of rawOrder) {
342
- if (!entry)
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)
343
1381
  continue;
344
- if (typeof entry === "string") {
345
- const [initiativeId, workstreamId] = entry.split(":", 2).map((s) => s.trim());
346
- if (initiativeId && workstreamId)
347
- order.push({ initiativeId, workstreamId });
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
+ });
348
1423
  continue;
349
1424
  }
350
- if (typeof entry === "object") {
351
- const record = entry;
352
- const initiativeId = (deps.pickString(record, ["initiativeId", "initiative_id"]) ?? "").trim();
353
- const workstreamId = (deps.pickString(record, ["workstreamId", "workstream_id"]) ?? "").trim();
354
- if (initiativeId && workstreamId)
355
- order.push({ initiativeId, workstreamId });
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
+ });
356
1448
  }
357
1449
  }
358
- const next = deps.setNextUpQueuePinOrder({ order });
359
- deps.sendJson(res, 200, { ok: true, pins: next.pins, updatedAt: next.updatedAt });
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
+ });
360
1499
  }
361
1500
  catch (err) {
362
- deps.sendJson(res, 500, { ok: false, error: deps.safeErrorMessage(err) });
1501
+ sendRouteException(res, "mission-control.graph.cycles.auto-fix.handler", err);
363
1502
  }
364
- }, "Mission-control next-up reorder");
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");
365
1572
  router.add("POST", "mission-control/auto-continue/start", async ({ req, query, res }) => {
366
1573
  try {
367
1574
  const payload = await deps.parseJsonRequest(req);
@@ -371,7 +1578,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
371
1578
  "")
372
1579
  .trim();
373
1580
  if (!initiativeId) {
374
- deps.sendJson(res, 400, { ok: false, error: "initiativeId is required" });
1581
+ sendRouteError(res, 400, "mission-control.auto-continue.start.validation", "initiativeId is required");
375
1582
  return;
376
1583
  }
377
1584
  const agentIdRaw = (deps.pickString(payload, ["agentId", "agent_id"]) ??
@@ -381,10 +1588,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
381
1588
  .trim();
382
1589
  const agentId = agentIdRaw || "main";
383
1590
  if (!/^[a-zA-Z0-9_-]+$/.test(agentId)) {
384
- deps.sendJson(res, 400, {
385
- ok: false,
386
- error: "agentId must be a simple identifier (letters, numbers, _ or -).",
387
- });
1591
+ sendRouteError(res, 400, "mission-control.auto-continue.start.validation", "agentId must be a simple identifier (letters, numbers, _ or -).");
388
1592
  return;
389
1593
  }
390
1594
  const tokenBudget = deps.pickNumber(payload, [
@@ -412,6 +1616,16 @@ export function registerMissionControlActionsRoutes(router, deps) {
412
1616
  : deps.parseBooleanQuery(typeof includeVerificationRaw === "string"
413
1617
  ? includeVerificationRaw
414
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);
415
1629
  const workstreamFilter = deps.dedupeStrings([
416
1630
  ...deps.pickStringArray(payload, [
417
1631
  "workstreamIds",
@@ -429,6 +1643,32 @@ export function registerMissionControlActionsRoutes(router, deps) {
429
1643
  .filter(Boolean),
430
1644
  ]);
431
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";
432
1672
  const run = await deps.startAutoContinueRun({
433
1673
  initiativeId,
434
1674
  agentId,
@@ -436,11 +1676,22 @@ export function registerMissionControlActionsRoutes(router, deps) {
436
1676
  tokenBudget,
437
1677
  includeVerification,
438
1678
  allowedWorkstreamIds,
1679
+ maxParallelSlices: maxParallelRaw,
1680
+ parallelMode,
1681
+ ignoreSpawnGuardRateLimit: ignoreSpawnGuardRateLimit === true,
1682
+ scope: startScope,
439
1683
  });
440
- deps.sendJson(res, 200, { ok: true, run });
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 });
441
1692
  }
442
1693
  catch (err) {
443
- deps.sendJson(res, 500, { ok: false, error: deps.safeErrorMessage(err) });
1694
+ sendRouteException(res, "mission-control.auto-continue.start.handler", err);
444
1695
  }
445
1696
  }, "Mission-control auto-continue start");
446
1697
  router.add("POST", "mission-control/auto-continue/stop", async ({ req, query, res }) => {
@@ -452,19 +1703,24 @@ export function registerMissionControlActionsRoutes(router, deps) {
452
1703
  "")
453
1704
  .trim();
454
1705
  if (!initiativeId) {
455
- deps.sendJson(res, 400, { ok: false, error: "initiativeId is required" });
1706
+ sendRouteError(res, 400, "mission-control.auto-continue.stop.validation", "initiativeId is required");
456
1707
  return;
457
1708
  }
458
1709
  const run = deps.autoContinueRuns.get(initiativeId) ?? null;
459
1710
  if (!run) {
460
- deps.sendJson(res, 404, { ok: false, error: "No auto-continue run found" });
1711
+ sendRouteError(res, 404, "mission-control.auto-continue.stop.lookup", "No auto-continue run found");
461
1712
  return;
462
1713
  }
463
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
+ : [];
464
1720
  run.stopRequested = true;
465
- run.status = run.activeRunId ? "stopping" : "stopped";
1721
+ run.status = activeRunIds.length > 0 ? "stopping" : "stopped";
466
1722
  run.updatedAt = now;
467
- if (!run.activeRunId) {
1723
+ if (activeRunIds.length === 0) {
468
1724
  await deps.stopAutoContinueRun({ run, reason: "stopped" });
469
1725
  }
470
1726
  else {
@@ -478,7 +1734,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
478
1734
  deps.sendJson(res, 200, { ok: true, run });
479
1735
  }
480
1736
  catch (err) {
481
- deps.sendJson(res, 500, { ok: false, error: deps.safeErrorMessage(err) });
1737
+ sendRouteException(res, "mission-control.auto-continue.stop.handler", err);
482
1738
  }
483
1739
  }, "Mission-control auto-continue stop");
484
1740
  router.add("POST", "mission-control/auto-continue/tick", async ({ req, query, res }) => {
@@ -492,7 +1748,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
492
1748
  if (initiativeId) {
493
1749
  const run = deps.autoContinueRuns.get(initiativeId) ?? null;
494
1750
  if (!run) {
495
- deps.sendJson(res, 404, { ok: false, error: "No auto-continue run found" });
1751
+ sendRouteError(res, 404, "mission-control.auto-continue.tick.lookup", "No auto-continue run found");
496
1752
  return;
497
1753
  }
498
1754
  await deps.tickAutoContinueRun(run);
@@ -503,7 +1759,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
503
1759
  deps.sendJson(res, 200, { ok: true });
504
1760
  }
505
1761
  catch (err) {
506
- deps.sendJson(res, 500, { ok: false, error: deps.safeErrorMessage(err) });
1762
+ sendRouteException(res, "mission-control.auto-continue.tick.handler", err);
507
1763
  }
508
1764
  }, "Mission-control auto-continue tick");
509
1765
  router.add("POST", "mission-control/assignments/auto", async ({ req, res }) => {
@@ -515,10 +1771,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
515
1771
  const title = deps.pickString(payload, ["title", "name"]) ?? "Untitled";
516
1772
  const summary = deps.pickString(payload, ["summary", "description", "context"]) ?? null;
517
1773
  if (!entityId || !entityType) {
518
- deps.sendJson(res, 400, {
519
- ok: false,
520
- error: "entity_id and entity_type are required.",
521
- });
1774
+ sendRouteError(res, 400, "mission-control.assignments.auto.validation", "entity_id and entity_type are required.");
522
1775
  return;
523
1776
  }
524
1777
  const assignment = await deps.resolveAutoAssignments({
@@ -532,10 +1785,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
532
1785
  deps.sendJson(res, 200, assignment);
533
1786
  }
534
1787
  catch (err) {
535
- deps.sendJson(res, 500, {
536
- ok: false,
537
- error: deps.safeErrorMessage(err),
538
- });
1788
+ sendRouteException(res, "mission-control.assignments.auto.handler", err);
539
1789
  }
540
1790
  }, "Mission-control auto assignment");
541
1791
  }