@useorgx/openclaw-plugin 0.4.9 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/README.md +77 -11
  2. package/dashboard/dist/assets/6mILZQ2a.js +1 -0
  3. package/dashboard/dist/assets/6mILZQ2a.js.br +0 -0
  4. package/dashboard/dist/assets/6mILZQ2a.js.gz +0 -0
  5. package/dashboard/dist/assets/8dksYiq4.js +2 -0
  6. package/dashboard/dist/assets/8dksYiq4.js.br +0 -0
  7. package/dashboard/dist/assets/8dksYiq4.js.gz +0 -0
  8. package/dashboard/dist/assets/B5zYRHc3.js +1 -0
  9. package/dashboard/dist/assets/B5zYRHc3.js.br +0 -0
  10. package/dashboard/dist/assets/B5zYRHc3.js.gz +0 -0
  11. package/dashboard/dist/assets/B6wPWJ35.js +1 -0
  12. package/dashboard/dist/assets/B6wPWJ35.js.br +0 -0
  13. package/dashboard/dist/assets/B6wPWJ35.js.gz +0 -0
  14. package/dashboard/dist/assets/BJgZIVUQ.js +53 -0
  15. package/dashboard/dist/assets/BJgZIVUQ.js.br +0 -0
  16. package/dashboard/dist/assets/BJgZIVUQ.js.gz +0 -0
  17. package/dashboard/dist/assets/BWEwjt1W.js +1 -0
  18. package/dashboard/dist/assets/BWEwjt1W.js.br +0 -0
  19. package/dashboard/dist/assets/BWEwjt1W.js.gz +0 -0
  20. package/dashboard/dist/assets/BgOYB78t.js +4 -0
  21. package/dashboard/dist/assets/BgOYB78t.js.br +0 -0
  22. package/dashboard/dist/assets/BgOYB78t.js.gz +0 -0
  23. package/dashboard/dist/assets/BzRbDCAD.css +1 -0
  24. package/dashboard/dist/assets/BzRbDCAD.css.br +0 -0
  25. package/dashboard/dist/assets/BzRbDCAD.css.gz +0 -0
  26. package/dashboard/dist/assets/C-KIc3Wc.js.br +0 -0
  27. package/dashboard/dist/assets/C-KIc3Wc.js.gz +0 -0
  28. package/dashboard/dist/assets/C8uM3AX8.js +1 -0
  29. package/dashboard/dist/assets/C8uM3AX8.js.br +0 -0
  30. package/dashboard/dist/assets/C8uM3AX8.js.gz +0 -0
  31. package/dashboard/dist/assets/C9jy61eu.js +212 -0
  32. package/dashboard/dist/assets/C9jy61eu.js.br +0 -0
  33. package/dashboard/dist/assets/C9jy61eu.js.gz +0 -0
  34. package/dashboard/dist/assets/CC63EwFD.js +1 -0
  35. package/dashboard/dist/assets/CC63EwFD.js.br +0 -0
  36. package/dashboard/dist/assets/CC63EwFD.js.gz +0 -0
  37. package/dashboard/dist/assets/CL_wXqR7.js +1 -0
  38. package/dashboard/dist/assets/CL_wXqR7.js.br +0 -0
  39. package/dashboard/dist/assets/CL_wXqR7.js.gz +0 -0
  40. package/dashboard/dist/assets/CZaT3ob_.js +1 -0
  41. package/dashboard/dist/assets/CZaT3ob_.js.br +0 -0
  42. package/dashboard/dist/assets/CZaT3ob_.js.gz +0 -0
  43. package/dashboard/dist/assets/CgaottFX.js +1 -0
  44. package/dashboard/dist/assets/CgaottFX.js.br +0 -0
  45. package/dashboard/dist/assets/CgaottFX.js.gz +0 -0
  46. package/dashboard/dist/assets/{CpJsfbXo.js → CxQ08qFN.js} +2 -2
  47. package/dashboard/dist/assets/CxQ08qFN.js.br +0 -0
  48. package/dashboard/dist/assets/CxQ08qFN.js.gz +0 -0
  49. package/dashboard/dist/assets/CzCxAZlW.js +1 -0
  50. package/dashboard/dist/assets/CzCxAZlW.js.br +0 -0
  51. package/dashboard/dist/assets/CzCxAZlW.js.gz +0 -0
  52. package/dashboard/dist/assets/D3iMTYEj.js +1 -0
  53. package/dashboard/dist/assets/D3iMTYEj.js.br +0 -0
  54. package/dashboard/dist/assets/D3iMTYEj.js.gz +0 -0
  55. package/dashboard/dist/assets/D8JNX8kq.js +2 -0
  56. package/dashboard/dist/assets/D8JNX8kq.js.br +0 -0
  57. package/dashboard/dist/assets/D8JNX8kq.js.gz +0 -0
  58. package/dashboard/dist/assets/DnA8dpj6.js +1 -0
  59. package/dashboard/dist/assets/DnA8dpj6.js.br +0 -0
  60. package/dashboard/dist/assets/DnA8dpj6.js.gz +0 -0
  61. package/dashboard/dist/assets/IUexzymk.js +1 -0
  62. package/dashboard/dist/assets/IUexzymk.js.br +0 -0
  63. package/dashboard/dist/assets/IUexzymk.js.gz +0 -0
  64. package/dashboard/dist/assets/cNrhgGc1.js +8 -0
  65. package/dashboard/dist/assets/cNrhgGc1.js.br +0 -0
  66. package/dashboard/dist/assets/cNrhgGc1.js.gz +0 -0
  67. package/dashboard/dist/assets/ic2FaMnh.js +1 -0
  68. package/dashboard/dist/assets/ic2FaMnh.js.br +0 -0
  69. package/dashboard/dist/assets/ic2FaMnh.js.gz +0 -0
  70. package/dashboard/dist/assets/qm8xLgv-.css +1 -0
  71. package/dashboard/dist/assets/qm8xLgv-.css.br +0 -0
  72. package/dashboard/dist/assets/qm8xLgv-.css.gz +0 -0
  73. package/dashboard/dist/assets/rttbDbEx.js +1 -0
  74. package/dashboard/dist/assets/rttbDbEx.js.br +0 -0
  75. package/dashboard/dist/assets/rttbDbEx.js.gz +0 -0
  76. package/dashboard/dist/brand/anthropic-mark.svg.br +0 -0
  77. package/dashboard/dist/brand/anthropic-mark.svg.gz +0 -0
  78. package/dashboard/dist/brand/openai-mark.svg.br +0 -0
  79. package/dashboard/dist/brand/openai-mark.svg.gz +0 -0
  80. package/dashboard/dist/brand/openclaw-mark.svg.br +0 -0
  81. package/dashboard/dist/brand/openclaw-mark.svg.gz +0 -0
  82. package/dashboard/dist/brand/xandy-orchestrator.png +0 -0
  83. package/dashboard/dist/index.html +7 -5
  84. package/dashboard/dist/index.html.br +0 -0
  85. package/dashboard/dist/index.html.gz +0 -0
  86. package/dist/activity-actor-fields.js +26 -4
  87. package/dist/activity-store.js +34 -8
  88. package/dist/agent-context-store.js +79 -17
  89. package/dist/agent-run-store.js +44 -3
  90. package/dist/agent-suite.d.ts +9 -0
  91. package/dist/agent-suite.js +149 -9
  92. package/dist/artifacts/artifact-domain-schemas.d.ts +66 -0
  93. package/dist/artifacts/artifact-domain-schemas.js +357 -0
  94. package/dist/artifacts/register-artifact.d.ts +4 -3
  95. package/dist/artifacts/register-artifact.js +170 -57
  96. package/dist/chat-store.d.ts +157 -0
  97. package/dist/chat-store.js +586 -0
  98. package/dist/cli/orgx.js +11 -0
  99. package/dist/contracts/client.d.ts +43 -3
  100. package/dist/contracts/client.js +159 -30
  101. package/dist/contracts/practice-exercise-schema.d.ts +216 -0
  102. package/dist/contracts/practice-exercise-schema.js +314 -0
  103. package/dist/contracts/retro-schema.d.ts +81 -0
  104. package/dist/contracts/retro-schema.js +80 -0
  105. package/dist/contracts/shared-types.d.ts +159 -0
  106. package/dist/contracts/shared-types.js +199 -1
  107. package/dist/contracts/skill-pack-schema.d.ts +192 -0
  108. package/dist/contracts/skill-pack-schema.js +180 -0
  109. package/dist/contracts/types.d.ts +247 -2
  110. package/dist/entities/auto-assignment.js +43 -17
  111. package/dist/event-sanitization.d.ts +11 -0
  112. package/dist/event-sanitization.js +113 -0
  113. package/dist/gateway-watchdog.d.ts +5 -0
  114. package/dist/gateway-watchdog.js +50 -0
  115. package/dist/hooks/post-reporting-event.mjs +1 -5
  116. package/dist/http/helpers/activity-headline.js +13 -132
  117. package/dist/http/helpers/auto-continue-engine.d.ts +198 -10
  118. package/dist/http/helpers/auto-continue-engine.js +3145 -186
  119. package/dist/http/helpers/autopilot-operations.d.ts +19 -0
  120. package/dist/http/helpers/autopilot-operations.js +182 -31
  121. package/dist/http/helpers/autopilot-runtime.d.ts +1 -0
  122. package/dist/http/helpers/autopilot-runtime.js +328 -25
  123. package/dist/http/helpers/autopilot-slice-utils.d.ts +18 -0
  124. package/dist/http/helpers/autopilot-slice-utils.js +514 -93
  125. package/dist/http/helpers/decision-mapper.d.ts +40 -0
  126. package/dist/http/helpers/decision-mapper.js +223 -7
  127. package/dist/http/helpers/dispatch-lifecycle.d.ts +19 -2
  128. package/dist/http/helpers/dispatch-lifecycle.js +242 -37
  129. package/dist/http/helpers/kickoff-context.js +104 -0
  130. package/dist/http/helpers/llm-client.d.ts +47 -0
  131. package/dist/http/helpers/llm-client.js +256 -0
  132. package/dist/http/helpers/mission-control.d.ts +102 -3
  133. package/dist/http/helpers/mission-control.js +498 -9
  134. package/dist/http/helpers/sentinel-catalog.d.ts +23 -0
  135. package/dist/http/helpers/sentinel-catalog.js +193 -0
  136. package/dist/http/helpers/session-classification.d.ts +9 -0
  137. package/dist/http/helpers/session-classification.js +564 -0
  138. package/dist/http/helpers/slice-experience-v2.d.ts +137 -0
  139. package/dist/http/helpers/slice-experience-v2.js +677 -0
  140. package/dist/http/helpers/slice-run-projections.d.ts +72 -0
  141. package/dist/http/helpers/slice-run-projections.js +877 -0
  142. package/dist/http/helpers/triage-mapper.d.ts +43 -0
  143. package/dist/http/helpers/triage-mapper.js +549 -0
  144. package/dist/http/helpers/value-utils.js +7 -2
  145. package/dist/http/helpers/workspace-scope.d.ts +15 -0
  146. package/dist/http/helpers/workspace-scope.js +170 -0
  147. package/dist/http/index.js +1420 -105
  148. package/dist/http/routes/agent-suite.d.ts +9 -0
  149. package/dist/http/routes/agent-suite.js +294 -8
  150. package/dist/http/routes/agents-catalog.js +64 -19
  151. package/dist/http/routes/chat.d.ts +19 -0
  152. package/dist/http/routes/chat.js +522 -0
  153. package/dist/http/routes/decision-actions.d.ts +8 -1
  154. package/dist/http/routes/decision-actions.js +42 -5
  155. package/dist/http/routes/dispatch-gateway-envelope.d.ts +25 -0
  156. package/dist/http/routes/dispatch-gateway-envelope.js +26 -0
  157. package/dist/http/routes/entities.d.ts +16 -0
  158. package/dist/http/routes/entities.js +232 -6
  159. package/dist/http/routes/live-legacy.d.ts +5 -0
  160. package/dist/http/routes/live-legacy.js +23 -509
  161. package/dist/http/routes/live-misc.d.ts +12 -0
  162. package/dist/http/routes/live-misc.js +251 -31
  163. package/dist/http/routes/live-snapshot.d.ts +49 -2
  164. package/dist/http/routes/live-snapshot.js +653 -23
  165. package/dist/http/routes/live-terminal.d.ts +11 -0
  166. package/dist/http/routes/live-terminal.js +154 -0
  167. package/dist/http/routes/live-triage.d.ts +61 -0
  168. package/dist/http/routes/live-triage.js +192 -0
  169. package/dist/http/routes/mission-control-actions.d.ts +49 -1
  170. package/dist/http/routes/mission-control-actions.js +1246 -84
  171. package/dist/http/routes/mission-control-read.d.ts +48 -3
  172. package/dist/http/routes/mission-control-read.js +1658 -20
  173. package/dist/http/routes/realtime-orchestrator.d.ts +10 -0
  174. package/dist/http/routes/realtime-orchestrator.js +74 -0
  175. package/dist/http/routes/run-control.d.ts +5 -2
  176. package/dist/http/routes/run-control.js +10 -0
  177. package/dist/http/routes/sentinels-catalog.d.ts +7 -0
  178. package/dist/http/routes/sentinels-catalog.js +24 -0
  179. package/dist/http/routes/summary.js +10 -3
  180. package/dist/http/routes/usage.d.ts +24 -0
  181. package/dist/http/routes/usage.js +362 -0
  182. package/dist/http/routes/work-artifacts.js +28 -9
  183. package/dist/index.js +165 -27
  184. package/dist/local-openclaw.js +29 -6
  185. package/dist/mcp-client-setup.js +3 -3
  186. package/dist/mcp-http-handler.d.ts +3 -0
  187. package/dist/mcp-http-handler.js +34 -60
  188. package/dist/next-up-queue-store.d.ts +16 -1
  189. package/dist/next-up-queue-store.js +89 -7
  190. package/dist/outbox.d.ts +5 -0
  191. package/dist/outbox.js +113 -9
  192. package/dist/paths.js +36 -5
  193. package/dist/reporting/rollups.d.ts +41 -0
  194. package/dist/reporting/rollups.js +113 -0
  195. package/dist/retro/domain-templates.d.ts +45 -0
  196. package/dist/retro/domain-templates.js +297 -0
  197. package/dist/retro/quality-rubric.d.ts +33 -0
  198. package/dist/retro/quality-rubric.js +213 -0
  199. package/dist/runtime-cleanup.d.ts +18 -0
  200. package/dist/runtime-cleanup.js +87 -0
  201. package/dist/services/background.d.ts +11 -0
  202. package/dist/services/background.js +22 -0
  203. package/dist/services/experiment-randomization.d.ts +21 -0
  204. package/dist/services/experiment-randomization.js +63 -0
  205. package/dist/skill-pack-state.d.ts +36 -5
  206. package/dist/skill-pack-state.js +273 -29
  207. package/dist/sync/local-agent-telemetry.d.ts +13 -0
  208. package/dist/sync/local-agent-telemetry.js +128 -0
  209. package/dist/sync/outbox-replay.js +131 -24
  210. package/dist/team-context-store.d.ts +23 -0
  211. package/dist/team-context-store.js +116 -0
  212. package/dist/telemetry/posthog.js +4 -2
  213. package/dist/tools/core-tools.d.ts +10 -14
  214. package/dist/tools/core-tools.js +1289 -24
  215. package/dist/types.d.ts +2 -0
  216. package/dist/types.js +2 -0
  217. package/dist/worker-supervisor.js +23 -0
  218. package/package.json +20 -6
  219. package/dashboard/dist/assets/B3ziCA02.js +0 -8
  220. package/dashboard/dist/assets/B5NEElEI.css +0 -1
  221. package/dashboard/dist/assets/BhapSNAs.js +0 -215
  222. package/dashboard/dist/assets/iFdvE7lx.js +0 -1
  223. package/dashboard/dist/assets/jRJsmpYM.js +0 -1
  224. package/dashboard/dist/assets/sAhvFnpk.js +0 -4
@@ -1,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,12 +641,23 @@ 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");
261
663
  router.add("POST", "mission-control/next-up/pin", async ({ req, query, res }) => {
@@ -286,10 +688,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
286
688
  ]) ?? "")
287
689
  .trim() || null;
288
690
  if (!initiativeId || !workstreamId) {
289
- deps.sendJson(res, 400, {
290
- ok: false,
291
- error: "initiativeId and workstreamId are required",
292
- });
691
+ sendRouteError(res, 400, "mission-control.next-up.pin.validation", "initiativeId and workstreamId are required");
293
692
  return;
294
693
  }
295
694
  const next = deps.upsertNextUpQueuePin({
@@ -298,10 +697,11 @@ export function registerMissionControlActionsRoutes(router, deps) {
298
697
  preferredTaskId,
299
698
  preferredMilestoneId,
300
699
  });
700
+ deps.clearNextUpQueueCache(initiativeId);
301
701
  deps.sendJson(res, 200, { ok: true, pins: next.pins, updatedAt: next.updatedAt });
302
702
  }
303
703
  catch (err) {
304
- deps.sendJson(res, 500, { ok: false, error: deps.safeErrorMessage(err) });
704
+ sendRouteException(res, "mission-control.next-up.pin.handler", err);
305
705
  }
306
706
  }, "Mission-control next-up pin");
307
707
  router.add("POST", "mission-control/next-up/unpin", async ({ req, query, res }) => {
@@ -318,50 +718,769 @@ export function registerMissionControlActionsRoutes(router, deps) {
318
718
  "")
319
719
  .trim();
320
720
  if (!initiativeId || !workstreamId) {
321
- deps.sendJson(res, 400, {
322
- ok: false,
323
- error: "initiativeId and workstreamId are required",
324
- });
721
+ sendRouteError(res, 400, "mission-control.next-up.unpin.validation", "initiativeId and workstreamId are required");
325
722
  return;
326
723
  }
327
724
  const next = deps.removeNextUpQueuePin({ initiativeId, workstreamId });
725
+ deps.clearNextUpQueueCache(initiativeId);
328
726
  deps.sendJson(res, 200, { ok: true, pins: next.pins, updatedAt: next.updatedAt });
329
727
  }
330
728
  catch (err) {
331
- deps.sendJson(res, 500, { ok: false, error: deps.safeErrorMessage(err) });
729
+ sendRouteException(res, "mission-control.next-up.unpin.handler", err);
332
730
  }
333
731
  }, "Mission-control next-up unpin");
334
732
  router.add("POST", "mission-control/next-up/reorder", async ({ req, res }) => {
335
733
  try {
336
734
  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)
735
+ const order = dedupeQueueOrder(parseQueueOrder(payload?.order, deps));
736
+ const next = deps.setNextUpQueuePinOrder({ order });
737
+ deps.clearNextUpQueueCache(null);
738
+ deps.sendJson(res, 200, { ok: true, pins: next.pins, updatedAt: next.updatedAt });
739
+ }
740
+ catch (err) {
741
+ sendRouteException(res, "mission-control.next-up.reorder.handler", err);
742
+ }
743
+ }, "Mission-control next-up reorder");
744
+ router.add("POST", "mission-control/slices/reorder", async ({ req, query, res }) => {
745
+ try {
746
+ const payload = await deps.parseJsonRequest(req);
747
+ const level = normalizeSliceLevel(deps.pickString(payload, ["level"]) ?? query.get("level"));
748
+ const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
749
+ query.get("initiativeId") ??
750
+ query.get("initiative_id") ??
751
+ "").trim() || null;
752
+ const scope = resolveWorkspaceScope(payload, query);
753
+ if (scope.error) {
754
+ sendRouteError(res, 400, "mission-control.slices.reorder.validation", scope.error);
755
+ return;
756
+ }
757
+ const workspaceId = scope.workspaceId;
758
+ const order = parseSliceOrderForMutation(payload?.order);
759
+ const canonicalOrder = order.map((sliceId) => ({ sliceId }));
760
+ const rawRequest = deps.rawRequest ??
761
+ (typeof deps.client?.rawRequest === "function"
762
+ ? deps.client.rawRequest.bind(deps.client)
763
+ : null);
764
+ if (!rawRequest) {
765
+ sendRouteError(res, 503, "mission-control.slices.reorder.unavailable", "Canonical mission-control slices API is unavailable");
766
+ return;
767
+ }
768
+ const response = await rawRequest("POST", "/api/client/mission-control/slices/reorder", {
769
+ ...(workspaceId
770
+ ? {
771
+ workspace_id: workspaceId,
772
+ command_center_id: workspaceId,
773
+ }
774
+ : {}),
775
+ level,
776
+ ...(initiativeId ? { initiative_id: initiativeId } : {}),
777
+ order: canonicalOrder,
778
+ });
779
+ deps.sendJson(res, 200, {
780
+ ...(response && typeof response === "object" ? response : { ok: true }),
781
+ source: "canonical",
782
+ });
783
+ }
784
+ catch (err) {
785
+ sendRouteError(res, 503, "mission-control.slices.reorder.canonical", "Canonical mission-control slices API unavailable for reorder", {
786
+ degraded: [`canonical unavailable (${deps.safeErrorMessage(err)})`],
787
+ canonical_only: true,
788
+ });
789
+ }
790
+ }, "Mission-control slices reorder (canonical)");
791
+ router.add("POST", "mission-control/slices/order-mode", async ({ req, query, res }) => {
792
+ try {
793
+ const payload = await deps.parseJsonRequest(req);
794
+ const level = normalizeSliceLevel(deps.pickString(payload, ["level"]) ?? query.get("level"));
795
+ const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
796
+ query.get("initiativeId") ??
797
+ query.get("initiative_id") ??
798
+ "").trim() || null;
799
+ const scope = resolveWorkspaceScope(payload, query);
800
+ if (scope.error) {
801
+ sendRouteError(res, 400, "mission-control.slices.order-mode.validation", scope.error);
802
+ return;
803
+ }
804
+ const workspaceId = scope.workspaceId;
805
+ const orderMode = normalizeSliceOrderMode(deps.pickString(payload, ["orderMode", "order_mode"]) ??
806
+ query.get("orderMode") ??
807
+ query.get("order_mode"));
808
+ if (!orderMode) {
809
+ sendRouteError(res, 400, "mission-control.slices.order-mode.validation", "order_mode must be either 'manual' or 'algorithmic'");
810
+ return;
811
+ }
812
+ const rawRequest = deps.rawRequest ??
813
+ (typeof deps.client?.rawRequest === "function"
814
+ ? deps.client.rawRequest.bind(deps.client)
815
+ : null);
816
+ if (!rawRequest) {
817
+ sendRouteError(res, 503, "mission-control.slices.order-mode.unavailable", "Canonical mission-control slices API is unavailable");
818
+ return;
819
+ }
820
+ const response = await rawRequest("POST", "/api/client/mission-control/slices/order-mode", {
821
+ ...(workspaceId
822
+ ? {
823
+ workspace_id: workspaceId,
824
+ command_center_id: workspaceId,
825
+ }
826
+ : {}),
827
+ level,
828
+ ...(initiativeId ? { initiative_id: initiativeId } : {}),
829
+ order_mode: orderMode,
830
+ });
831
+ deps.sendJson(res, 200, {
832
+ ...(response && typeof response === "object" ? response : { ok: true }),
833
+ source: "canonical",
834
+ });
835
+ }
836
+ catch (err) {
837
+ sendRouteError(res, 503, "mission-control.slices.order-mode.canonical", "Canonical mission-control slices API unavailable for mode changes", {
838
+ degraded: [`canonical unavailable (${deps.safeErrorMessage(err)})`],
839
+ canonical_only: true,
840
+ });
841
+ }
842
+ }, "Mission-control slices order mode (canonical)");
843
+ router.add("POST", "mission-control/next-up/move", async ({ req, query, res }) => {
844
+ try {
845
+ const payload = await deps.parseJsonRequest(req);
846
+ const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
847
+ query.get("initiativeId") ??
848
+ query.get("initiative_id") ??
849
+ "")
850
+ .trim();
851
+ const workstreamId = (deps.pickString(payload, ["workstreamId", "workstream_id"]) ??
852
+ query.get("workstreamId") ??
853
+ query.get("workstream_id") ??
854
+ "")
855
+ .trim();
856
+ const placement = normalizePlacement(deps.pickString(payload, ["placement", "queuePlacement", "queue_placement"]) ??
857
+ query.get("placement") ??
858
+ query.get("queuePlacement") ??
859
+ query.get("queue_placement"), "bottom");
860
+ if (!initiativeId || !workstreamId) {
861
+ sendRouteError(res, 400, "mission-control.next-up.move.validation", "initiativeId and workstreamId are required");
862
+ return;
863
+ }
864
+ const queue = await deps.buildNextUpQueue({ initiativeId });
865
+ const order = dedupeQueueOrder(queue.items.map((item) => ({
866
+ initiativeId: item.initiativeId,
867
+ workstreamId: item.workstreamId,
868
+ })));
869
+ const key = `${initiativeId}:${workstreamId}`;
870
+ const current = order.filter((entry) => `${entry.initiativeId}:${entry.workstreamId}` !== key);
871
+ const nextOrder = placement === "top"
872
+ ? [{ initiativeId, workstreamId }, ...current]
873
+ : [...current, { initiativeId, workstreamId }];
874
+ const next = deps.setNextUpQueuePinOrder({ order: nextOrder });
875
+ deps.clearNextUpQueueCache(initiativeId);
876
+ deps.sendJson(res, 200, {
877
+ ok: true,
878
+ placement,
879
+ orderApplied: nextOrder.length,
880
+ pins: next.pins,
881
+ updatedAt: next.updatedAt,
882
+ });
883
+ }
884
+ catch (err) {
885
+ sendRouteException(res, "mission-control.next-up.move.handler", err);
886
+ }
887
+ }, "Mission-control next-up move");
888
+ router.add("POST", "mission-control/next-up/triage/stop", async ({ req, query, res }) => {
889
+ try {
890
+ const payload = await deps.parseJsonRequest(req);
891
+ const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
892
+ query.get("initiativeId") ??
893
+ query.get("initiative_id") ??
894
+ "")
895
+ .trim();
896
+ const workstreamId = (deps.pickString(payload, ["workstreamId", "workstream_id"]) ??
897
+ query.get("workstreamId") ??
898
+ query.get("workstream_id") ??
899
+ "")
900
+ .trim();
901
+ const placement = normalizePlacement(deps.pickString(payload, ["placement", "queuePlacement", "queue_placement"]) ??
902
+ query.get("placement") ??
903
+ query.get("queuePlacement") ??
904
+ query.get("queue_placement"), "bottom");
905
+ const resetToTodoRaw = payload.resetToTodo ??
906
+ payload.reset_to_todo ??
907
+ query.get("resetToTodo") ??
908
+ query.get("reset_to_todo") ??
909
+ null;
910
+ const resetToTodo = typeof resetToTodoRaw === "boolean"
911
+ ? resetToTodoRaw
912
+ : deps.parseBooleanQuery(typeof resetToTodoRaw === "string" ? resetToTodoRaw : null) ?? false;
913
+ if (!initiativeId || !workstreamId) {
914
+ sendRouteError(res, 400, "mission-control.next-up.triage.stop.validation", "initiativeId and workstreamId are required");
915
+ return;
916
+ }
917
+ const run = deps.autoContinueRuns.get(initiativeId) ?? null;
918
+ let stoppedAutoContinue = false;
919
+ if (run) {
920
+ const now = new Date().toISOString();
921
+ const activeRunIds = Array.isArray(run.activeSliceRunIds)
922
+ ? run.activeSliceRunIds.filter((id) => typeof id === "string" && id.trim().length > 0)
923
+ : typeof run.activeRunId === "string" && run.activeRunId.trim().length > 0
924
+ ? [run.activeRunId]
925
+ : [];
926
+ run.stopRequested = true;
927
+ run.status = activeRunIds.length > 0 ? "stopping" : "stopped";
928
+ run.updatedAt = now;
929
+ if (activeRunIds.length === 0) {
930
+ await deps.stopAutoContinueRun({ run, reason: "stopped" });
931
+ }
932
+ else {
933
+ try {
934
+ await deps.updateInitiativeAutoContinueState({ initiativeId, run });
935
+ }
936
+ catch {
937
+ // best effort
938
+ }
939
+ }
940
+ stoppedAutoContinue = true;
941
+ }
942
+ let resetTaskCount = 0;
943
+ if (resetToTodo) {
944
+ const taskResult = await deps.client.listEntities("task", {
945
+ initiative_id: initiativeId,
946
+ workstream_id: workstreamId,
947
+ limit: 100,
948
+ });
949
+ const tasks = Array.isArray(taskResult?.data) ? taskResult.data : [];
950
+ const statesToReset = new Set(["running", "blocked"]);
951
+ for (const task of tasks) {
952
+ if (!task || typeof task !== "object")
953
+ continue;
954
+ const record = task;
955
+ const taskId = deps.pickString(record, ["id"]);
956
+ if (!taskId)
957
+ continue;
958
+ if (!shouldResetTaskStatus(record.status, statesToReset))
959
+ continue;
960
+ await deps.client.updateEntity("task", taskId, { status: "todo" });
961
+ resetTaskCount += 1;
962
+ }
963
+ }
964
+ const queue = await deps.buildNextUpQueue({ initiativeId });
965
+ const order = dedupeQueueOrder(queue.items.map((item) => ({
966
+ initiativeId: item.initiativeId,
967
+ workstreamId: item.workstreamId,
968
+ })));
969
+ const targetKey = `${initiativeId}:${workstreamId}`;
970
+ const nextOrder = buildPlacedOrder({
971
+ order,
972
+ targets: new Set([targetKey]),
973
+ placement,
974
+ });
975
+ const next = deps.setNextUpQueuePinOrder({
976
+ order: nextOrder.length > 0
977
+ ? nextOrder
978
+ : [{ initiativeId, workstreamId }],
979
+ });
980
+ deps.clearNextUpQueueCache(initiativeId);
981
+ deps.sendJson(res, 200, {
982
+ ok: true,
983
+ placement,
984
+ stoppedAutoContinue,
985
+ resetToTodo,
986
+ resetTaskCount,
987
+ run,
988
+ pins: next.pins,
989
+ updatedAt: next.updatedAt,
990
+ });
991
+ }
992
+ catch (err) {
993
+ sendRouteException(res, "mission-control.next-up.triage.stop.handler", err);
994
+ }
995
+ }, "Mission-control next-up triage stop");
996
+ router.add("POST", "mission-control/next-up/remove", async ({ req, query, res }) => {
997
+ try {
998
+ const payload = await deps.parseJsonRequest(req);
999
+ const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
1000
+ query.get("initiativeId") ??
1001
+ query.get("initiative_id") ??
1002
+ "")
1003
+ .trim();
1004
+ const workstreamId = (deps.pickString(payload, ["workstreamId", "workstream_id"]) ??
1005
+ query.get("workstreamId") ??
1006
+ query.get("workstream_id") ??
1007
+ "")
1008
+ .trim();
1009
+ if (!initiativeId || !workstreamId) {
1010
+ sendRouteError(res, 400, "mission-control.next-up.remove.validation", "initiativeId and workstreamId are required");
1011
+ return;
1012
+ }
1013
+ deps.removeNextUpQueuePin({ initiativeId, workstreamId });
1014
+ const next = deps.suppressNextUpQueueItem({ initiativeId, workstreamId });
1015
+ deps.clearNextUpQueueCache(initiativeId);
1016
+ deps.sendJson(res, 200, {
1017
+ ok: true,
1018
+ removed: { initiativeId, workstreamId },
1019
+ suppressions: next.suppressions,
1020
+ updatedAt: next.updatedAt,
1021
+ });
1022
+ }
1023
+ catch (err) {
1024
+ sendRouteException(res, "mission-control.next-up.remove.handler", err);
1025
+ }
1026
+ }, "Mission-control next-up remove");
1027
+ router.add("POST", "mission-control/next-up/bulk", async ({ req, query, res }) => {
1028
+ try {
1029
+ const payload = await deps.parseJsonRequest(req);
1030
+ const actionRaw = deps.pickString(payload, ["action"]) ??
1031
+ query.get("action") ??
1032
+ "";
1033
+ const action = actionRaw.trim().toLowerCase();
1034
+ const initiativeScopeRaw = deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
1035
+ query.get("initiativeId") ??
1036
+ query.get("initiative_id") ??
1037
+ "";
1038
+ const initiativeScope = initiativeScopeRaw.trim() || null;
1039
+ const items = dedupeQueueOrder(parseQueueOrder(payload.items, deps));
1040
+ if (!["move_top", "move_bottom", "remove"].includes(action)) {
1041
+ sendRouteError(res, 400, "mission-control.next-up.bulk.validation", "action must be one of: move_top, move_bottom, remove");
1042
+ return;
1043
+ }
1044
+ if (items.length === 0) {
1045
+ sendRouteError(res, 400, "mission-control.next-up.bulk.validation", "items must include at least one initiativeId/workstreamId pair");
1046
+ return;
1047
+ }
1048
+ const queue = await deps.buildNextUpQueue({ initiativeId: initiativeScope });
1049
+ const baseOrder = dedupeQueueOrder(queue.items.map((item) => ({
1050
+ initiativeId: item.initiativeId,
1051
+ workstreamId: item.workstreamId,
1052
+ })));
1053
+ const knownKeys = new Set(baseOrder.map((entry) => `${entry.initiativeId}:${entry.workstreamId}`));
1054
+ const results = items.map((entry) => {
1055
+ const key = `${entry.initiativeId}:${entry.workstreamId}`;
1056
+ if (knownKeys.has(key)) {
1057
+ return { ...entry, ok: true };
1058
+ }
1059
+ return {
1060
+ ...entry,
1061
+ ok: false,
1062
+ error: "Queue item is not currently available in this scope",
1063
+ };
1064
+ });
1065
+ const targetKeys = new Set(results
1066
+ .filter((entry) => entry.ok)
1067
+ .map((entry) => `${entry.initiativeId}:${entry.workstreamId}`));
1068
+ let nextOrder = baseOrder;
1069
+ if (targetKeys.size > 0) {
1070
+ if (action === "remove") {
1071
+ for (const entry of results) {
1072
+ if (!entry.ok)
1073
+ continue;
1074
+ deps.removeNextUpQueuePin({
1075
+ initiativeId: entry.initiativeId,
1076
+ workstreamId: entry.workstreamId,
1077
+ });
1078
+ deps.suppressNextUpQueueItem({
1079
+ initiativeId: entry.initiativeId,
1080
+ workstreamId: entry.workstreamId,
1081
+ });
1082
+ }
1083
+ nextOrder = baseOrder.filter((entry) => {
1084
+ const key = `${entry.initiativeId}:${entry.workstreamId}`;
1085
+ return !targetKeys.has(key);
1086
+ });
1087
+ }
1088
+ else {
1089
+ nextOrder = buildPlacedOrder({
1090
+ order: baseOrder,
1091
+ targets: targetKeys,
1092
+ placement: action === "move_top" ? "top" : "bottom",
1093
+ });
1094
+ deps.setNextUpQueuePinOrder({ order: nextOrder });
1095
+ }
1096
+ deps.clearNextUpQueueCache(initiativeScope);
1097
+ }
1098
+ const updated = results.filter((entry) => entry.ok).length;
1099
+ const failed = results.length - updated;
1100
+ deps.sendJson(res, 200, {
1101
+ ok: true,
1102
+ action,
1103
+ requested: results.length,
1104
+ updated,
1105
+ failed,
1106
+ results: results.map((entry) => ({
1107
+ initiativeId: entry.initiativeId,
1108
+ workstreamId: entry.workstreamId,
1109
+ ok: entry.ok,
1110
+ error: entry.ok ? null : entry.error,
1111
+ })),
1112
+ orderSize: nextOrder.length,
1113
+ updatedAt: new Date().toISOString(),
1114
+ });
1115
+ }
1116
+ catch (err) {
1117
+ sendRouteException(res, "mission-control.next-up.bulk.handler", err);
1118
+ }
1119
+ }, "Mission-control next-up bulk");
1120
+ router.add("POST", "mission-control/next-up/clear", async ({ req, query, res }) => {
1121
+ try {
1122
+ const payload = await deps.parseJsonRequest(req);
1123
+ const initiativeIdRaw = deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
1124
+ query.get("initiativeId") ??
1125
+ query.get("initiative_id") ??
1126
+ "";
1127
+ const initiativeId = initiativeIdRaw.trim() || null;
1128
+ const workstreamIdRaw = deps.pickString(payload, ["workstreamId", "workstream_id"]) ??
1129
+ query.get("workstreamId") ??
1130
+ query.get("workstream_id") ??
1131
+ "";
1132
+ const workstreamId = workstreamIdRaw.trim() || null;
1133
+ const placement = normalizePlacement(deps.pickString(payload, ["placement", "queuePlacement", "queue_placement"]) ??
1134
+ query.get("placement") ??
1135
+ query.get("queuePlacement") ??
1136
+ query.get("queue_placement"), "bottom");
1137
+ const requestedStates = deps.dedupeStrings([
1138
+ ...deps.pickStringArray(payload, ["states", "queueStates", "queue_states"]),
1139
+ ...(query.get("states") ?? query.get("queueStates") ?? query.get("queue_states") ?? "")
1140
+ .split(",")
1141
+ .map((entry) => entry.trim())
1142
+ .filter(Boolean),
1143
+ ])
1144
+ .map((entry) => entry.trim().toLowerCase())
1145
+ .filter((entry) => entry === "running" || entry === "blocked");
1146
+ const states = new Set(requestedStates.length > 0 ? requestedStates : ["running", "blocked"]);
1147
+ const queue = await deps.buildNextUpQueue({ initiativeId });
1148
+ const scopedItems = queue.items.filter((item) => {
1149
+ if (initiativeId && item.initiativeId !== initiativeId)
1150
+ return false;
1151
+ if (workstreamId && item.workstreamId !== workstreamId)
1152
+ return false;
1153
+ if (states.has(item.queueState))
1154
+ return true;
1155
+ if (states.has("running") &&
1156
+ item.autoContinue?.status === "running" &&
1157
+ !item.autoContinue?.stopReason) {
1158
+ return true;
1159
+ }
1160
+ return false;
1161
+ });
1162
+ const updatedTaskIds = new Set();
1163
+ let failedUpdates = 0;
1164
+ for (const item of scopedItems) {
1165
+ let taskRows = [];
1166
+ try {
1167
+ const response = await deps.client.listEntities("task", {
1168
+ initiative_id: item.initiativeId,
1169
+ workstream_id: item.workstreamId,
1170
+ limit: 100,
1171
+ });
1172
+ taskRows = Array.isArray(response?.data) ? response.data : [];
1173
+ }
1174
+ catch {
1175
+ // best effort: keep progressing through queue
343
1176
  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 });
1177
+ }
1178
+ for (const row of taskRows) {
1179
+ if (!row || typeof row !== "object")
1180
+ continue;
1181
+ const record = row;
1182
+ const taskId = deps.pickString(record, ["id"]);
1183
+ if (!taskId || updatedTaskIds.has(taskId))
1184
+ continue;
1185
+ if (!shouldResetTaskStatus(record.status, states))
1186
+ continue;
1187
+ try {
1188
+ await deps.client.updateEntity("task", taskId, { status: "todo" });
1189
+ updatedTaskIds.add(taskId);
1190
+ }
1191
+ catch {
1192
+ failedUpdates += 1;
1193
+ }
1194
+ }
1195
+ }
1196
+ const baseOrder = dedupeQueueOrder(queue.items.map((item) => ({
1197
+ initiativeId: item.initiativeId,
1198
+ workstreamId: item.workstreamId,
1199
+ })));
1200
+ const targetKeys = new Set(scopedItems.map((item) => `${item.initiativeId}:${item.workstreamId}`));
1201
+ const nextOrder = buildPlacedOrder({
1202
+ order: baseOrder,
1203
+ targets: targetKeys,
1204
+ placement,
1205
+ });
1206
+ const next = deps.setNextUpQueuePinOrder({ order: nextOrder });
1207
+ deps.clearNextUpQueueCache(initiativeId);
1208
+ deps.sendJson(res, 200, {
1209
+ ok: true,
1210
+ placement,
1211
+ states: Array.from(states),
1212
+ queueItemsCleared: scopedItems.length,
1213
+ tasksReset: updatedTaskIds.size,
1214
+ taskResetFailures: failedUpdates,
1215
+ pins: next.pins,
1216
+ updatedAt: next.updatedAt,
1217
+ });
1218
+ }
1219
+ catch (err) {
1220
+ sendRouteException(res, "mission-control.next-up.clear.handler", err);
1221
+ }
1222
+ }, "Mission-control next-up clear");
1223
+ router.add("POST", "mission-control/graph/cycles/auto-fix", async ({ req, query, res }) => {
1224
+ try {
1225
+ const payload = await deps.parseJsonRequest(req);
1226
+ const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
1227
+ query.get("initiativeId") ??
1228
+ query.get("initiative_id") ??
1229
+ "")
1230
+ .trim();
1231
+ const dryRunRaw = payload.dryRun ??
1232
+ payload.dry_run ??
1233
+ query.get("dryRun") ??
1234
+ query.get("dry_run") ??
1235
+ null;
1236
+ const dryRun = typeof dryRunRaw === "boolean"
1237
+ ? dryRunRaw
1238
+ : deps.parseBooleanQuery(typeof dryRunRaw === "string" ? dryRunRaw : null) ?? false;
1239
+ if (!initiativeId) {
1240
+ sendRouteError(res, 400, "mission-control.graph.cycles.auto-fix.validation", "initiativeId is required");
1241
+ return;
1242
+ }
1243
+ const graph = deps.applyLocalInitiativeOverrideToGraph(await deps.buildMissionControlGraph(initiativeId));
1244
+ const diagnosticsRemovedEdges = parseCycleDiagnosticsRemovedEdges(graph);
1245
+ const graphNodes = parseCycleGraphNodes(graph);
1246
+ const nodeById = new Map(graphNodes.map((node) => [node.id, node]));
1247
+ const workingDependencies = new Map(graphNodes.map((node) => [node.id, new Set(node.dependencyIds)]));
1248
+ const removedEdgeKeys = new Set();
1249
+ const maxPasses = 12;
1250
+ for (let pass = 0; pass < maxPasses; pass += 1) {
1251
+ const edges = [];
1252
+ for (const node of graphNodes) {
1253
+ const depsSet = workingDependencies.get(node.id) ?? new Set();
1254
+ for (const depId of depsSet.values()) {
1255
+ if (!nodeById.has(depId) || depId === node.id)
1256
+ continue;
1257
+ edges.push({ from: depId, to: node.id });
1258
+ }
1259
+ }
1260
+ const cycleEdgeKeys = detectCycleEdgeKeys(edges);
1261
+ if (cycleEdgeKeys.size === 0)
1262
+ break;
1263
+ let removedInPass = 0;
1264
+ for (const edgeKey of cycleEdgeKeys.values()) {
1265
+ const [from, to] = edgeKey.split("->", 2);
1266
+ if (!from || !to)
1267
+ continue;
1268
+ const nodeDeps = workingDependencies.get(to);
1269
+ if (!nodeDeps || !nodeDeps.has(from))
1270
+ continue;
1271
+ nodeDeps.delete(from);
1272
+ removedEdgeKeys.add(edgeKey);
1273
+ removedInPass += 1;
1274
+ }
1275
+ if (removedInPass === 0)
1276
+ break;
1277
+ }
1278
+ let removedEdges = Array.from(removedEdgeKeys.values())
1279
+ .map((edgeKey) => {
1280
+ const [from, to] = edgeKey.split("->", 2);
1281
+ if (!from || !to)
1282
+ return null;
1283
+ return { from, to };
1284
+ })
1285
+ .filter((entry) => Boolean(entry));
1286
+ if (removedEdges.length === 0 && diagnosticsRemovedEdges.length > 0) {
1287
+ removedEdges = diagnosticsRemovedEdges;
1288
+ }
1289
+ const affectedNodes = new Map();
1290
+ for (const edge of removedEdges) {
1291
+ const node = nodeById.get(edge.to);
1292
+ if (!node)
348
1293
  continue;
1294
+ const existing = affectedNodes.get(node.id) ?? {
1295
+ id: node.id,
1296
+ type: node.type,
1297
+ title: node.title,
1298
+ workstreamId: node.workstreamId,
1299
+ removedDependencyIds: [],
1300
+ dependencyIds: [],
1301
+ };
1302
+ if (!existing.removedDependencyIds.includes(edge.from)) {
1303
+ existing.removedDependencyIds.push(edge.from);
349
1304
  }
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 });
1305
+ existing.dependencyIds = Array.from((workingDependencies.get(node.id) ?? new Set()).values());
1306
+ affectedNodes.set(node.id, existing);
1307
+ }
1308
+ const affected = Array.from(affectedNodes.values()).sort((left, right) => left.title.localeCompare(right.title));
1309
+ if (dryRun) {
1310
+ deps.sendJson(res, 200, {
1311
+ ok: true,
1312
+ dryRun: true,
1313
+ initiativeId,
1314
+ cycleEdgesDetected: removedEdges.length,
1315
+ nodesToUpdate: affected.length,
1316
+ removedEdges,
1317
+ affected,
1318
+ });
1319
+ return;
1320
+ }
1321
+ const updateResults = [];
1322
+ for (const node of affected) {
1323
+ if (node.type !== "initiative" &&
1324
+ node.type !== "workstream" &&
1325
+ node.type !== "milestone" &&
1326
+ node.type !== "task") {
1327
+ updateResults.push({
1328
+ id: node.id,
1329
+ type: node.type,
1330
+ ok: false,
1331
+ error: "Unsupported entity type for dependency update",
1332
+ dependencyIds: node.dependencyIds,
1333
+ removedDependencyIds: node.removedDependencyIds,
1334
+ });
1335
+ continue;
1336
+ }
1337
+ try {
1338
+ await deps.client.updateEntity(node.type, node.id, {
1339
+ depends_on: node.dependencyIds,
1340
+ dependency_ids: node.dependencyIds,
1341
+ dependencyIds: node.dependencyIds,
1342
+ });
1343
+ updateResults.push({
1344
+ id: node.id,
1345
+ type: node.type,
1346
+ ok: true,
1347
+ dependencyIds: node.dependencyIds,
1348
+ removedDependencyIds: node.removedDependencyIds,
1349
+ });
1350
+ }
1351
+ catch (err) {
1352
+ updateResults.push({
1353
+ id: node.id,
1354
+ type: node.type,
1355
+ ok: false,
1356
+ error: deps.safeErrorMessage(err),
1357
+ dependencyIds: node.dependencyIds,
1358
+ removedDependencyIds: node.removedDependencyIds,
1359
+ });
356
1360
  }
357
1361
  }
358
- const next = deps.setNextUpQueuePinOrder({ order });
359
- deps.sendJson(res, 200, { ok: true, pins: next.pins, updatedAt: next.updatedAt });
1362
+ const scheduled = [];
1363
+ const failedSchedules = [];
1364
+ const workstreamIds = Array.from(new Set(affected
1365
+ .map((node) => {
1366
+ if (node.type === "workstream")
1367
+ return node.id;
1368
+ return node.workstreamId;
1369
+ })
1370
+ .filter((workstreamId) => typeof workstreamId === "string" &&
1371
+ workstreamId.trim().length > 0)));
1372
+ for (const workstreamId of workstreamIds) {
1373
+ try {
1374
+ const scheduledFix = await deps.scheduleAutoFixForWorkstream({
1375
+ initiativeId,
1376
+ workstreamId,
1377
+ runId: null,
1378
+ event: "dependency_cycle_auto_fix",
1379
+ requestedByAgentId: "orgx-orchestrator",
1380
+ requestedByAgentName: "OrgX Orchestrator",
1381
+ graceMs: 250,
1382
+ });
1383
+ scheduled.push({
1384
+ workstreamId,
1385
+ requestId: scheduledFix.requestId,
1386
+ });
1387
+ }
1388
+ catch (err) {
1389
+ failedSchedules.push({
1390
+ workstreamId,
1391
+ error: deps.safeErrorMessage(err),
1392
+ });
1393
+ }
1394
+ }
1395
+ if (removedEdges.length > 0 || affected.length > 0) {
1396
+ deps.clearNextUpQueueCache(initiativeId);
1397
+ }
1398
+ const updated = updateResults.filter((result) => result.ok).length;
1399
+ const failed = updateResults.length - updated;
1400
+ deps.sendJson(res, 200, {
1401
+ ok: true,
1402
+ initiativeId,
1403
+ cycleEdgesDetected: removedEdges.length,
1404
+ nodesUpdated: updated,
1405
+ nodesFailed: failed,
1406
+ removedEdges,
1407
+ updates: updateResults,
1408
+ scheduledAutofixes: scheduled,
1409
+ autofixScheduleFailures: failedSchedules,
1410
+ });
360
1411
  }
361
1412
  catch (err) {
362
- deps.sendJson(res, 500, { ok: false, error: deps.safeErrorMessage(err) });
1413
+ sendRouteException(res, "mission-control.graph.cycles.auto-fix.handler", err);
363
1414
  }
364
- }, "Mission-control next-up reorder");
1415
+ }, "Mission-control dependency cycle auto-fix");
1416
+ router.add("POST", "mission-control/activity/auto-fix", async ({ req, query, res }) => {
1417
+ try {
1418
+ const payload = await deps.parseJsonRequest(req);
1419
+ const initiativeId = (deps.pickString(payload, ["initiativeId", "initiative_id"]) ??
1420
+ query.get("initiativeId") ??
1421
+ query.get("initiative_id") ??
1422
+ "")
1423
+ .trim();
1424
+ const workstreamId = (deps.pickString(payload, ["workstreamId", "workstream_id"]) ??
1425
+ query.get("workstreamId") ??
1426
+ query.get("workstream_id") ??
1427
+ "")
1428
+ .trim();
1429
+ if (!initiativeId || !workstreamId) {
1430
+ sendRouteError(res, 400, "mission-control.activity.auto-fix.validation", "initiativeId and workstreamId are required");
1431
+ return;
1432
+ }
1433
+ const runId = (deps.pickString(payload, ["runId", "run_id", "sessionId", "session_id"]) ??
1434
+ query.get("runId") ??
1435
+ query.get("run_id") ??
1436
+ query.get("sessionId") ??
1437
+ query.get("session_id") ??
1438
+ "")
1439
+ .trim() || null;
1440
+ const event = (deps.pickString(payload, ["event", "eventName", "event_name"]) ??
1441
+ query.get("event") ??
1442
+ query.get("eventName") ??
1443
+ query.get("event_name") ??
1444
+ "")
1445
+ .trim() || null;
1446
+ const requestedByAgentId = (deps.pickString(payload, ["requestedByAgentId", "requested_by_agent_id"]) ??
1447
+ query.get("requestedByAgentId") ??
1448
+ query.get("requested_by_agent_id") ??
1449
+ "")
1450
+ .trim() || null;
1451
+ const requestedByAgentName = (deps.pickString(payload, ["requestedByAgentName", "requested_by_agent_name"]) ??
1452
+ query.get("requestedByAgentName") ??
1453
+ query.get("requested_by_agent_name") ??
1454
+ "")
1455
+ .trim() || null;
1456
+ const graceMsFromQueryRaw = query.get("graceMs") ??
1457
+ query.get("grace_ms") ??
1458
+ query.get("delayMs") ??
1459
+ query.get("delay_ms") ??
1460
+ null;
1461
+ const graceMsFromQuery = typeof graceMsFromQueryRaw === "string" && graceMsFromQueryRaw.trim().length > 0
1462
+ ? Number(graceMsFromQueryRaw)
1463
+ : null;
1464
+ const graceMs = deps.pickNumber(payload, ["graceMs", "grace_ms", "delayMs", "delay_ms"]) ??
1465
+ (Number.isFinite(graceMsFromQuery) ? graceMsFromQuery : null);
1466
+ const schedule = await deps.scheduleAutoFixForWorkstream({
1467
+ initiativeId,
1468
+ workstreamId,
1469
+ runId,
1470
+ event,
1471
+ requestedByAgentId,
1472
+ requestedByAgentName,
1473
+ graceMs,
1474
+ });
1475
+ deps.sendJson(res, 202, {
1476
+ ok: true,
1477
+ scheduled: schedule,
1478
+ });
1479
+ }
1480
+ catch (err) {
1481
+ sendRouteException(res, "mission-control.activity.auto-fix.handler", err);
1482
+ }
1483
+ }, "Mission-control activity auto-fix");
365
1484
  router.add("POST", "mission-control/auto-continue/start", async ({ req, query, res }) => {
366
1485
  try {
367
1486
  const payload = await deps.parseJsonRequest(req);
@@ -371,7 +1490,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
371
1490
  "")
372
1491
  .trim();
373
1492
  if (!initiativeId) {
374
- deps.sendJson(res, 400, { ok: false, error: "initiativeId is required" });
1493
+ sendRouteError(res, 400, "mission-control.auto-continue.start.validation", "initiativeId is required");
375
1494
  return;
376
1495
  }
377
1496
  const agentIdRaw = (deps.pickString(payload, ["agentId", "agent_id"]) ??
@@ -381,10 +1500,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
381
1500
  .trim();
382
1501
  const agentId = agentIdRaw || "main";
383
1502
  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
- });
1503
+ sendRouteError(res, 400, "mission-control.auto-continue.start.validation", "agentId must be a simple identifier (letters, numbers, _ or -).");
388
1504
  return;
389
1505
  }
390
1506
  const tokenBudget = deps.pickNumber(payload, [
@@ -412,6 +1528,16 @@ export function registerMissionControlActionsRoutes(router, deps) {
412
1528
  : deps.parseBooleanQuery(typeof includeVerificationRaw === "string"
413
1529
  ? includeVerificationRaw
414
1530
  : null);
1531
+ const ignoreSpawnGuardRateLimitRaw = payload.ignoreSpawnGuardRateLimit ??
1532
+ payload.ignore_spawn_guard_rate_limit ??
1533
+ query.get("ignoreSpawnGuardRateLimit") ??
1534
+ query.get("ignore_spawn_guard_rate_limit") ??
1535
+ null;
1536
+ const ignoreSpawnGuardRateLimit = typeof ignoreSpawnGuardRateLimitRaw === "boolean"
1537
+ ? ignoreSpawnGuardRateLimitRaw
1538
+ : deps.parseBooleanQuery(typeof ignoreSpawnGuardRateLimitRaw === "string"
1539
+ ? ignoreSpawnGuardRateLimitRaw
1540
+ : null);
415
1541
  const workstreamFilter = deps.dedupeStrings([
416
1542
  ...deps.pickStringArray(payload, [
417
1543
  "workstreamIds",
@@ -429,6 +1555,32 @@ export function registerMissionControlActionsRoutes(router, deps) {
429
1555
  .filter(Boolean),
430
1556
  ]);
431
1557
  const allowedWorkstreamIds = workstreamFilter.length > 0 ? workstreamFilter : null;
1558
+ const maxParallelRaw = deps.pickNumber(payload, [
1559
+ "maxParallelSlices",
1560
+ "max_parallel_slices",
1561
+ "maxParallel",
1562
+ "max_parallel",
1563
+ ]) ??
1564
+ query.get("maxParallelSlices") ??
1565
+ query.get("max_parallel_slices") ??
1566
+ query.get("maxParallel") ??
1567
+ query.get("max_parallel") ??
1568
+ null;
1569
+ const parallelModeRaw = (deps.pickString(payload, ["parallelMode", "parallel_mode"]) ??
1570
+ query.get("parallelMode") ??
1571
+ query.get("parallel_mode") ??
1572
+ "iwmt")
1573
+ .trim()
1574
+ .toLowerCase();
1575
+ const parallelMode = parallelModeRaw === "iwmt" ? "iwmt" : "iwmt";
1576
+ const startScopeRaw = deps.pickString(payload, ["scope", "sliceScope", "slice_scope"]) ??
1577
+ query.get("scope") ??
1578
+ query.get("sliceScope") ??
1579
+ query.get("slice_scope") ??
1580
+ null;
1581
+ const startScope = startScopeRaw === "milestone" || startScopeRaw === "workstream"
1582
+ ? startScopeRaw
1583
+ : "task";
432
1584
  const run = await deps.startAutoContinueRun({
433
1585
  initiativeId,
434
1586
  agentId,
@@ -436,11 +1588,22 @@ export function registerMissionControlActionsRoutes(router, deps) {
436
1588
  tokenBudget,
437
1589
  includeVerification,
438
1590
  allowedWorkstreamIds,
1591
+ maxParallelSlices: maxParallelRaw,
1592
+ parallelMode,
1593
+ ignoreSpawnGuardRateLimit: ignoreSpawnGuardRateLimit === true,
1594
+ scope: startScope,
439
1595
  });
440
- deps.sendJson(res, 200, { ok: true, run });
1596
+ const dispatchEnvelope = buildDispatchGatewayEnvelope({
1597
+ dispatchMode: "server",
1598
+ route: "mission-control.auto-continue.start",
1599
+ source: "auto_continue_start",
1600
+ initiativeId,
1601
+ workstreamIds: allowedWorkstreamIds,
1602
+ });
1603
+ deps.sendJson(res, 200, { ok: true, ...dispatchEnvelope, run });
441
1604
  }
442
1605
  catch (err) {
443
- deps.sendJson(res, 500, { ok: false, error: deps.safeErrorMessage(err) });
1606
+ sendRouteException(res, "mission-control.auto-continue.start.handler", err);
444
1607
  }
445
1608
  }, "Mission-control auto-continue start");
446
1609
  router.add("POST", "mission-control/auto-continue/stop", async ({ req, query, res }) => {
@@ -452,19 +1615,24 @@ export function registerMissionControlActionsRoutes(router, deps) {
452
1615
  "")
453
1616
  .trim();
454
1617
  if (!initiativeId) {
455
- deps.sendJson(res, 400, { ok: false, error: "initiativeId is required" });
1618
+ sendRouteError(res, 400, "mission-control.auto-continue.stop.validation", "initiativeId is required");
456
1619
  return;
457
1620
  }
458
1621
  const run = deps.autoContinueRuns.get(initiativeId) ?? null;
459
1622
  if (!run) {
460
- deps.sendJson(res, 404, { ok: false, error: "No auto-continue run found" });
1623
+ sendRouteError(res, 404, "mission-control.auto-continue.stop.lookup", "No auto-continue run found");
461
1624
  return;
462
1625
  }
463
1626
  const now = new Date().toISOString();
1627
+ const activeRunIds = Array.isArray(run.activeSliceRunIds)
1628
+ ? run.activeSliceRunIds.filter((id) => typeof id === "string" && id.trim().length > 0)
1629
+ : typeof run.activeRunId === "string" && run.activeRunId.trim().length > 0
1630
+ ? [run.activeRunId]
1631
+ : [];
464
1632
  run.stopRequested = true;
465
- run.status = run.activeRunId ? "stopping" : "stopped";
1633
+ run.status = activeRunIds.length > 0 ? "stopping" : "stopped";
466
1634
  run.updatedAt = now;
467
- if (!run.activeRunId) {
1635
+ if (activeRunIds.length === 0) {
468
1636
  await deps.stopAutoContinueRun({ run, reason: "stopped" });
469
1637
  }
470
1638
  else {
@@ -478,7 +1646,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
478
1646
  deps.sendJson(res, 200, { ok: true, run });
479
1647
  }
480
1648
  catch (err) {
481
- deps.sendJson(res, 500, { ok: false, error: deps.safeErrorMessage(err) });
1649
+ sendRouteException(res, "mission-control.auto-continue.stop.handler", err);
482
1650
  }
483
1651
  }, "Mission-control auto-continue stop");
484
1652
  router.add("POST", "mission-control/auto-continue/tick", async ({ req, query, res }) => {
@@ -492,7 +1660,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
492
1660
  if (initiativeId) {
493
1661
  const run = deps.autoContinueRuns.get(initiativeId) ?? null;
494
1662
  if (!run) {
495
- deps.sendJson(res, 404, { ok: false, error: "No auto-continue run found" });
1663
+ sendRouteError(res, 404, "mission-control.auto-continue.tick.lookup", "No auto-continue run found");
496
1664
  return;
497
1665
  }
498
1666
  await deps.tickAutoContinueRun(run);
@@ -503,7 +1671,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
503
1671
  deps.sendJson(res, 200, { ok: true });
504
1672
  }
505
1673
  catch (err) {
506
- deps.sendJson(res, 500, { ok: false, error: deps.safeErrorMessage(err) });
1674
+ sendRouteException(res, "mission-control.auto-continue.tick.handler", err);
507
1675
  }
508
1676
  }, "Mission-control auto-continue tick");
509
1677
  router.add("POST", "mission-control/assignments/auto", async ({ req, res }) => {
@@ -515,10 +1683,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
515
1683
  const title = deps.pickString(payload, ["title", "name"]) ?? "Untitled";
516
1684
  const summary = deps.pickString(payload, ["summary", "description", "context"]) ?? null;
517
1685
  if (!entityId || !entityType) {
518
- deps.sendJson(res, 400, {
519
- ok: false,
520
- error: "entity_id and entity_type are required.",
521
- });
1686
+ sendRouteError(res, 400, "mission-control.assignments.auto.validation", "entity_id and entity_type are required.");
522
1687
  return;
523
1688
  }
524
1689
  const assignment = await deps.resolveAutoAssignments({
@@ -532,10 +1697,7 @@ export function registerMissionControlActionsRoutes(router, deps) {
532
1697
  deps.sendJson(res, 200, assignment);
533
1698
  }
534
1699
  catch (err) {
535
- deps.sendJson(res, 500, {
536
- ok: false,
537
- error: deps.safeErrorMessage(err),
538
- });
1700
+ sendRouteException(res, "mission-control.assignments.auto.handler", err);
539
1701
  }
540
1702
  }, "Mission-control auto assignment");
541
1703
  }