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