@useorgx/openclaw-plugin 0.4.6 → 0.4.9
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 +310 -24
- package/dashboard/dist/assets/B5NEElEI.css +1 -0
- package/dashboard/dist/assets/BhapSNAs.js +215 -0
- package/dashboard/dist/assets/iFdvE7lx.js +1 -0
- package/dashboard/dist/assets/jRJsmpYM.js +1 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/activity-actor-fields.d.ts +3 -0
- package/dist/activity-actor-fields.js +128 -0
- package/dist/activity-store.js +12 -19
- package/dist/agent-context-store.js +5 -25
- package/dist/agent-run-store.js +5 -25
- package/dist/agent-suite.js +1 -8
- package/dist/artifacts/register-artifact.d.ts +47 -0
- package/dist/artifacts/register-artifact.js +271 -0
- package/dist/auth/flows.d.ts +47 -0
- package/dist/auth/flows.js +169 -0
- package/dist/auth-store.js +14 -39
- package/dist/byok-store.js +5 -19
- package/dist/cli/orgx.d.ts +66 -0
- package/dist/cli/orgx.js +91 -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 +1 -0
- package/dist/contracts/client.js +7 -5
- package/dist/contracts/shared-types.d.ts +147 -0
- package/dist/contracts/shared-types.js +3 -0
- package/dist/contracts/types.d.ts +1 -130
- package/dist/contracts/types.js +5 -0
- package/dist/entities/auto-assignment.d.ts +36 -0
- package/dist/entities/auto-assignment.js +115 -0
- package/dist/entity-comment-store.js +5 -25
- package/dist/hash-utils.d.ts +2 -0
- package/dist/hash-utils.js +12 -0
- package/dist/http/helpers/activity-headline.d.ts +10 -0
- package/dist/http/helpers/activity-headline.js +192 -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 +298 -0
- package/dist/http/helpers/auto-continue-engine.js +1218 -0
- package/dist/http/helpers/autopilot-operations.d.ts +157 -0
- package/dist/http/helpers/autopilot-operations.js +403 -0
- package/dist/http/helpers/autopilot-runtime.d.ts +42 -0
- package/dist/http/helpers/autopilot-runtime.js +319 -0
- package/dist/http/helpers/autopilot-slice-utils.d.ts +38 -0
- package/dist/http/helpers/autopilot-slice-utils.js +476 -0
- package/dist/http/helpers/decision-mapper.d.ts +12 -0
- package/dist/http/helpers/decision-mapper.js +44 -0
- package/dist/http/helpers/dispatch-lifecycle.d.ts +102 -0
- package/dist/http/helpers/dispatch-lifecycle.js +604 -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 +154 -0
- package/dist/http/helpers/mission-control.d.ts +94 -0
- package/dist/http/helpers/mission-control.js +894 -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/value-utils.d.ts +6 -0
- package/dist/http/helpers/value-utils.js +67 -0
- package/dist/http/index.d.ts +88 -0
- package/dist/http/index.js +2353 -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 +29 -0
- package/dist/http/routes/agent-suite.js +198 -0
- package/dist/http/routes/agents-catalog.d.ts +40 -0
- package/dist/http/routes/agents-catalog.js +83 -0
- package/dist/http/routes/billing.d.ts +23 -0
- package/dist/http/routes/billing.js +55 -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 +13 -0
- package/dist/http/routes/decision-actions.js +66 -0
- package/dist/http/routes/delegation.d.ts +19 -0
- package/dist/http/routes/delegation.js +32 -0
- package/dist/http/routes/entities.d.ts +47 -0
- package/dist/http/routes/entities.js +152 -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 +110 -0
- package/dist/http/routes/live-legacy.js +598 -0
- package/dist/http/routes/live-misc.d.ts +69 -0
- package/dist/http/routes/live-misc.js +206 -0
- package/dist/http/routes/live-snapshot.d.ts +90 -0
- package/dist/http/routes/live-snapshot.js +297 -0
- package/dist/http/routes/mission-control-actions.d.ts +83 -0
- package/dist/http/routes/mission-control-actions.js +541 -0
- package/dist/http/routes/mission-control-read.d.ts +28 -0
- package/dist/http/routes/mission-control-read.js +67 -0
- package/dist/http/routes/onboarding.d.ts +34 -0
- package/dist/http/routes/onboarding.js +101 -0
- package/dist/http/routes/run-control.d.ts +24 -0
- package/dist/http/routes/run-control.js +86 -0
- package/dist/http/routes/runtime-hooks.d.ts +69 -0
- package/dist/http/routes/runtime-hooks.js +437 -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 +42 -0
- package/dist/http/routes/work-artifacts.d.ts +9 -0
- package/dist/http/routes/work-artifacts.js +36 -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 -9664
- package/dist/index.js +122 -2121
- package/dist/json-utils.d.ts +1 -0
- package/dist/json-utils.js +8 -0
- package/dist/local-openclaw.js +8 -0
- package/dist/mcp-client-setup.js +75 -90
- package/dist/next-up-queue-store.js +4 -18
- package/dist/runtime-instance-store.js +8 -34
- package/dist/services/background.d.ts +23 -0
- package/dist/services/background.js +23 -0
- package/dist/services/instrumentation.d.ts +29 -0
- package/dist/services/instrumentation.js +136 -0
- 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/outbox-replay.d.ts +55 -0
- package/dist/sync/outbox-replay.js +514 -0
- package/dist/tools/core-tools.d.ts +76 -0
- package/dist/tools/core-tools.js +1005 -0
- package/dist/worker-supervisor.js +15 -0
- package/package.json +6 -1
- package/dashboard/dist/assets/0tOC3wSN.js +0 -214
- package/dashboard/dist/assets/Bm8QnMJ_.js +0 -1
- package/dashboard/dist/assets/CyxZio4Y.js +0 -1
- package/dashboard/dist/assets/DaAIOik3.css +0 -1
|
@@ -0,0 +1,894 @@
|
|
|
1
|
+
import { pickNumber, pickString, toIsoString } from "./value-utils.js";
|
|
2
|
+
export const ORGX_SKILL_BY_DOMAIN = {
|
|
3
|
+
engineering: "orgx-engineering-agent",
|
|
4
|
+
product: "orgx-product-agent",
|
|
5
|
+
marketing: "orgx-marketing-agent",
|
|
6
|
+
sales: "orgx-sales-agent",
|
|
7
|
+
operations: "orgx-operations-agent",
|
|
8
|
+
design: "orgx-design-agent",
|
|
9
|
+
orchestration: "orgx-orchestrator-agent",
|
|
10
|
+
};
|
|
11
|
+
function safeErrorMessage(err) {
|
|
12
|
+
if (err instanceof Error)
|
|
13
|
+
return err.message;
|
|
14
|
+
if (typeof err === "string")
|
|
15
|
+
return err;
|
|
16
|
+
return "Unexpected error";
|
|
17
|
+
}
|
|
18
|
+
export function readBudgetEnvNumber(name, fallback, bounds = {}) {
|
|
19
|
+
const raw = process.env[name];
|
|
20
|
+
if (typeof raw !== "string" || raw.trim().length === 0)
|
|
21
|
+
return fallback;
|
|
22
|
+
const parsed = Number(raw);
|
|
23
|
+
if (!Number.isFinite(parsed))
|
|
24
|
+
return fallback;
|
|
25
|
+
if (typeof bounds.min === "number" && parsed < bounds.min)
|
|
26
|
+
return fallback;
|
|
27
|
+
if (typeof bounds.max === "number" && parsed > bounds.max)
|
|
28
|
+
return fallback;
|
|
29
|
+
return parsed;
|
|
30
|
+
}
|
|
31
|
+
const DEFAULT_TOKEN_MODEL_PRICING_USD_PER_1M = {
|
|
32
|
+
// GPT-5.3 Codex API pricing is not published yet; use GPT-5.2 Codex pricing as proxy.
|
|
33
|
+
gpt53CodexProxy: {
|
|
34
|
+
input: readBudgetEnvNumber("ORGX_BUDGET_GPT53_CODEX_INPUT_PER_1M", 1.75, { min: 0 }),
|
|
35
|
+
cachedInput: readBudgetEnvNumber("ORGX_BUDGET_GPT53_CODEX_CACHED_INPUT_PER_1M", 0.175, {
|
|
36
|
+
min: 0,
|
|
37
|
+
}),
|
|
38
|
+
output: readBudgetEnvNumber("ORGX_BUDGET_GPT53_CODEX_OUTPUT_PER_1M", 14, { min: 0 }),
|
|
39
|
+
},
|
|
40
|
+
opus46: {
|
|
41
|
+
input: readBudgetEnvNumber("ORGX_BUDGET_OPUS46_INPUT_PER_1M", 5, { min: 0 }),
|
|
42
|
+
// Anthropic does not publish a fixed cached-input rate on the model page.
|
|
43
|
+
cachedInput: readBudgetEnvNumber("ORGX_BUDGET_OPUS46_CACHED_INPUT_PER_1M", 5, { min: 0 }),
|
|
44
|
+
output: readBudgetEnvNumber("ORGX_BUDGET_OPUS46_OUTPUT_PER_1M", 25, { min: 0 }),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
export const DEFAULT_TOKEN_BUDGET_ASSUMPTIONS = {
|
|
48
|
+
tokensPerHour: readBudgetEnvNumber("ORGX_BUDGET_TOKENS_PER_HOUR", 1_200_000, { min: 1 }),
|
|
49
|
+
inputShare: readBudgetEnvNumber("ORGX_BUDGET_INPUT_TOKEN_SHARE", 0.86, { min: 0, max: 1 }),
|
|
50
|
+
cachedInputShare: readBudgetEnvNumber("ORGX_BUDGET_CACHED_INPUT_SHARE", 0.15, {
|
|
51
|
+
min: 0,
|
|
52
|
+
max: 1,
|
|
53
|
+
}),
|
|
54
|
+
contingencyMultiplier: readBudgetEnvNumber("ORGX_BUDGET_CONTINGENCY_MULTIPLIER", 1.3, {
|
|
55
|
+
min: 0.1,
|
|
56
|
+
}),
|
|
57
|
+
roundingStepUsd: readBudgetEnvNumber("ORGX_BUDGET_ROUNDING_STEP_USD", 5, { min: 0.01 }),
|
|
58
|
+
};
|
|
59
|
+
const DEFAULT_TOKEN_MODEL_MIX = {
|
|
60
|
+
gpt53CodexProxy: 0.7,
|
|
61
|
+
opus46: 0.3,
|
|
62
|
+
};
|
|
63
|
+
function modelCostPerMillionTokensUsd(pricing) {
|
|
64
|
+
const inputShare = DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.inputShare;
|
|
65
|
+
const outputShare = Math.max(0, 1 - inputShare);
|
|
66
|
+
const cachedShare = DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.cachedInputShare;
|
|
67
|
+
const uncachedShare = Math.max(0, 1 - cachedShare);
|
|
68
|
+
const effectiveInputRate = pricing.input * uncachedShare + pricing.cachedInput * cachedShare;
|
|
69
|
+
return inputShare * effectiveInputRate + outputShare * pricing.output;
|
|
70
|
+
}
|
|
71
|
+
function estimateBudgetUsdFromDurationHours(durationHours) {
|
|
72
|
+
if (!Number.isFinite(durationHours) || durationHours <= 0)
|
|
73
|
+
return 0;
|
|
74
|
+
const blendedPerMillionUsd = DEFAULT_TOKEN_MODEL_MIX.gpt53CodexProxy *
|
|
75
|
+
modelCostPerMillionTokensUsd(DEFAULT_TOKEN_MODEL_PRICING_USD_PER_1M.gpt53CodexProxy) +
|
|
76
|
+
DEFAULT_TOKEN_MODEL_MIX.opus46 *
|
|
77
|
+
modelCostPerMillionTokensUsd(DEFAULT_TOKEN_MODEL_PRICING_USD_PER_1M.opus46);
|
|
78
|
+
const tokenMillions = (durationHours * DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.tokensPerHour) / 1_000_000;
|
|
79
|
+
const rawBudgetUsd = tokenMillions *
|
|
80
|
+
blendedPerMillionUsd *
|
|
81
|
+
DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.contingencyMultiplier;
|
|
82
|
+
const roundedBudgetUsd = Math.round(rawBudgetUsd / DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.roundingStepUsd) *
|
|
83
|
+
DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.roundingStepUsd;
|
|
84
|
+
return Math.max(0, roundedBudgetUsd);
|
|
85
|
+
}
|
|
86
|
+
function isLegacyHourlyBudget(budgetUsd, durationHours) {
|
|
87
|
+
if (!Number.isFinite(budgetUsd) || !Number.isFinite(durationHours) || durationHours <= 0) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
const legacyHourlyBudget = durationHours * 40;
|
|
91
|
+
return Math.abs(budgetUsd - legacyHourlyBudget) <= 0.5;
|
|
92
|
+
}
|
|
93
|
+
const DEFAULT_DURATION_HOURS = {
|
|
94
|
+
initiative: 40,
|
|
95
|
+
workstream: 16,
|
|
96
|
+
milestone: 6,
|
|
97
|
+
task: 2,
|
|
98
|
+
};
|
|
99
|
+
const DEFAULT_BUDGET_USD = {
|
|
100
|
+
initiative: estimateBudgetUsdFromDurationHours(DEFAULT_DURATION_HOURS.initiative),
|
|
101
|
+
workstream: estimateBudgetUsdFromDurationHours(DEFAULT_DURATION_HOURS.workstream),
|
|
102
|
+
milestone: estimateBudgetUsdFromDurationHours(DEFAULT_DURATION_HOURS.milestone),
|
|
103
|
+
task: estimateBudgetUsdFromDurationHours(DEFAULT_DURATION_HOURS.task),
|
|
104
|
+
};
|
|
105
|
+
const PRIORITY_LABEL_TO_NUM = {
|
|
106
|
+
urgent: 10,
|
|
107
|
+
high: 25,
|
|
108
|
+
medium: 50,
|
|
109
|
+
low: 75,
|
|
110
|
+
};
|
|
111
|
+
function clampPriority(value) {
|
|
112
|
+
if (!Number.isFinite(value))
|
|
113
|
+
return 60;
|
|
114
|
+
return Math.max(1, Math.min(100, Math.round(value)));
|
|
115
|
+
}
|
|
116
|
+
function mapPriorityNumToLabel(priorityNum) {
|
|
117
|
+
if (priorityNum <= 12)
|
|
118
|
+
return "urgent";
|
|
119
|
+
if (priorityNum <= 30)
|
|
120
|
+
return "high";
|
|
121
|
+
if (priorityNum <= 60)
|
|
122
|
+
return "medium";
|
|
123
|
+
return "low";
|
|
124
|
+
}
|
|
125
|
+
function getRecordMetadata(record) {
|
|
126
|
+
const metadata = record.metadata;
|
|
127
|
+
if (metadata && typeof metadata === "object" && !Array.isArray(metadata)) {
|
|
128
|
+
return metadata;
|
|
129
|
+
}
|
|
130
|
+
return {};
|
|
131
|
+
}
|
|
132
|
+
function extractBudgetUsdFromText(...texts) {
|
|
133
|
+
for (const text of texts) {
|
|
134
|
+
if (typeof text !== "string" || text.trim().length === 0)
|
|
135
|
+
continue;
|
|
136
|
+
const moneyMatch = /(?:expected\s+budget|budget)[^0-9$]{0,24}\$?\s*([0-9][0-9,]*(?:\.[0-9]{1,2})?)/i.exec(text);
|
|
137
|
+
if (!moneyMatch)
|
|
138
|
+
continue;
|
|
139
|
+
const numeric = Number(moneyMatch[1].replace(/,/g, ""));
|
|
140
|
+
if (Number.isFinite(numeric) && numeric >= 0)
|
|
141
|
+
return numeric;
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
function extractDurationHoursFromText(...texts) {
|
|
146
|
+
for (const text of texts) {
|
|
147
|
+
if (typeof text !== "string" || text.trim().length === 0)
|
|
148
|
+
continue;
|
|
149
|
+
const durationMatch = /(?:expected\s+duration|duration)[^0-9]{0,24}([0-9]+(?:\.[0-9]+)?)\s*h/i.exec(text);
|
|
150
|
+
if (!durationMatch)
|
|
151
|
+
continue;
|
|
152
|
+
const numeric = Number(durationMatch[1]);
|
|
153
|
+
if (Number.isFinite(numeric) && numeric >= 0)
|
|
154
|
+
return numeric;
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
export function pickStringArray(record, keys) {
|
|
159
|
+
for (const key of keys) {
|
|
160
|
+
const value = record[key];
|
|
161
|
+
if (Array.isArray(value)) {
|
|
162
|
+
const items = value
|
|
163
|
+
.filter((entry) => typeof entry === "string")
|
|
164
|
+
.map((entry) => entry.trim())
|
|
165
|
+
.filter(Boolean);
|
|
166
|
+
if (items.length > 0)
|
|
167
|
+
return items;
|
|
168
|
+
}
|
|
169
|
+
if (typeof value === "string") {
|
|
170
|
+
const items = value
|
|
171
|
+
.split(",")
|
|
172
|
+
.map((entry) => entry.trim())
|
|
173
|
+
.filter(Boolean);
|
|
174
|
+
if (items.length > 0)
|
|
175
|
+
return items;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
export function dedupeStrings(items) {
|
|
181
|
+
const seen = new Set();
|
|
182
|
+
const out = [];
|
|
183
|
+
for (const item of items) {
|
|
184
|
+
if (!item || seen.has(item))
|
|
185
|
+
continue;
|
|
186
|
+
seen.add(item);
|
|
187
|
+
out.push(item);
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
function normalizePriorityForEntity(record) {
|
|
192
|
+
const explicitPriorityNum = pickNumber(record, [
|
|
193
|
+
"priority_num",
|
|
194
|
+
"priorityNum",
|
|
195
|
+
"priority_number",
|
|
196
|
+
]);
|
|
197
|
+
const priorityLabelRaw = pickString(record, ["priority", "priority_label"]);
|
|
198
|
+
if (explicitPriorityNum !== null) {
|
|
199
|
+
const clamped = clampPriority(explicitPriorityNum);
|
|
200
|
+
return {
|
|
201
|
+
priorityNum: clamped,
|
|
202
|
+
priorityLabel: priorityLabelRaw ?? mapPriorityNumToLabel(clamped),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (priorityLabelRaw) {
|
|
206
|
+
const mapped = PRIORITY_LABEL_TO_NUM[priorityLabelRaw.toLowerCase()] ?? 60;
|
|
207
|
+
return {
|
|
208
|
+
priorityNum: mapped,
|
|
209
|
+
priorityLabel: priorityLabelRaw.toLowerCase(),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
priorityNum: 60,
|
|
214
|
+
priorityLabel: null,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function normalizeDependencies(record) {
|
|
218
|
+
const metadata = getRecordMetadata(record);
|
|
219
|
+
const direct = pickStringArray(record, [
|
|
220
|
+
"depends_on",
|
|
221
|
+
"dependsOn",
|
|
222
|
+
"dependency_ids",
|
|
223
|
+
"dependencyIds",
|
|
224
|
+
"dependencies",
|
|
225
|
+
]);
|
|
226
|
+
const nested = pickStringArray(metadata, [
|
|
227
|
+
"depends_on",
|
|
228
|
+
"dependsOn",
|
|
229
|
+
"dependency_ids",
|
|
230
|
+
"dependencyIds",
|
|
231
|
+
"dependencies",
|
|
232
|
+
]);
|
|
233
|
+
return dedupeStrings([...direct, ...nested]);
|
|
234
|
+
}
|
|
235
|
+
function normalizeAssignedAgents(record) {
|
|
236
|
+
const metadata = getRecordMetadata(record);
|
|
237
|
+
const ids = dedupeStrings([
|
|
238
|
+
...pickStringArray(record, ["assigned_agent_ids", "assignedAgentIds"]),
|
|
239
|
+
...pickStringArray(metadata, ["assigned_agent_ids", "assignedAgentIds"]),
|
|
240
|
+
]);
|
|
241
|
+
const names = dedupeStrings([
|
|
242
|
+
...pickStringArray(record, ["assigned_agent_names", "assignedAgentNames"]),
|
|
243
|
+
...pickStringArray(metadata, ["assigned_agent_names", "assignedAgentNames"]),
|
|
244
|
+
]);
|
|
245
|
+
const objectCandidates = [
|
|
246
|
+
record.assigned_agents,
|
|
247
|
+
record.assignedAgents,
|
|
248
|
+
metadata.assigned_agents,
|
|
249
|
+
metadata.assignedAgents,
|
|
250
|
+
];
|
|
251
|
+
const fromObjects = [];
|
|
252
|
+
for (const candidate of objectCandidates) {
|
|
253
|
+
if (!Array.isArray(candidate))
|
|
254
|
+
continue;
|
|
255
|
+
for (const entry of candidate) {
|
|
256
|
+
if (!entry || typeof entry !== "object")
|
|
257
|
+
continue;
|
|
258
|
+
const item = entry;
|
|
259
|
+
const id = pickString(item, ["id", "agent_id", "agentId"]) ?? "";
|
|
260
|
+
const name = pickString(item, ["name", "agent_name", "agentName"]) ?? id;
|
|
261
|
+
if (!name)
|
|
262
|
+
continue;
|
|
263
|
+
fromObjects.push({
|
|
264
|
+
id: id || `name:${name}`,
|
|
265
|
+
name,
|
|
266
|
+
domain: pickString(item, ["domain", "role"]),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const merged = [...fromObjects];
|
|
271
|
+
if (merged.length === 0 && (names.length > 0 || ids.length > 0)) {
|
|
272
|
+
const maxLen = Math.max(names.length, ids.length);
|
|
273
|
+
for (let i = 0; i < maxLen; i += 1) {
|
|
274
|
+
const id = ids[i] ?? `name:${names[i] ?? `agent-${i + 1}`}`;
|
|
275
|
+
const name = names[i] ?? ids[i] ?? `Agent ${i + 1}`;
|
|
276
|
+
merged.push({ id, name, domain: null });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const seen = new Set();
|
|
280
|
+
const deduped = [];
|
|
281
|
+
for (const item of merged) {
|
|
282
|
+
const key = `${item.id}:${item.name}`.toLowerCase();
|
|
283
|
+
if (seen.has(key))
|
|
284
|
+
continue;
|
|
285
|
+
seen.add(key);
|
|
286
|
+
deduped.push(item);
|
|
287
|
+
}
|
|
288
|
+
return deduped;
|
|
289
|
+
}
|
|
290
|
+
function toMissionControlNode(type, entity, fallbackInitiativeId) {
|
|
291
|
+
const record = entity;
|
|
292
|
+
const metadata = getRecordMetadata(record);
|
|
293
|
+
const initiativeId = pickString(record, ["initiative_id", "initiativeId"]) ??
|
|
294
|
+
pickString(metadata, ["initiative_id", "initiativeId"]) ??
|
|
295
|
+
(type === "initiative" ? String(record.id ?? fallbackInitiativeId) : fallbackInitiativeId);
|
|
296
|
+
const workstreamId = type === "workstream"
|
|
297
|
+
? String(record.id ?? "")
|
|
298
|
+
: pickString(record, ["workstream_id", "workstreamId"]) ??
|
|
299
|
+
pickString(metadata, ["workstream_id", "workstreamId"]);
|
|
300
|
+
const milestoneId = type === "milestone"
|
|
301
|
+
? String(record.id ?? "")
|
|
302
|
+
: pickString(record, ["milestone_id", "milestoneId"]) ??
|
|
303
|
+
pickString(metadata, ["milestone_id", "milestoneId"]);
|
|
304
|
+
const parentIdRaw = pickString(record, ["parentId", "parent_id"]) ??
|
|
305
|
+
pickString(metadata, ["parentId", "parent_id"]);
|
|
306
|
+
const parentId = parentIdRaw ??
|
|
307
|
+
(type === "initiative"
|
|
308
|
+
? null
|
|
309
|
+
: type === "workstream"
|
|
310
|
+
? initiativeId
|
|
311
|
+
: type === "milestone"
|
|
312
|
+
? workstreamId ?? initiativeId
|
|
313
|
+
: milestoneId ?? workstreamId ?? initiativeId);
|
|
314
|
+
const status = pickString(record, ["status"]) ??
|
|
315
|
+
(type === "task" ? "todo" : "planned");
|
|
316
|
+
const dueDate = toIsoString(pickString(record, ["due_date", "dueDate", "target_date", "targetDate"]));
|
|
317
|
+
const etaEndAt = toIsoString(pickString(record, ["eta_end_at", "etaEndAt"]));
|
|
318
|
+
const expectedDuration = pickNumber(record, [
|
|
319
|
+
"expected_duration_hours",
|
|
320
|
+
"expectedDurationHours",
|
|
321
|
+
"duration_hours",
|
|
322
|
+
"durationHours",
|
|
323
|
+
]) ??
|
|
324
|
+
pickNumber(metadata, [
|
|
325
|
+
"expected_duration_hours",
|
|
326
|
+
"expectedDurationHours",
|
|
327
|
+
"duration_hours",
|
|
328
|
+
"durationHours",
|
|
329
|
+
]) ??
|
|
330
|
+
extractDurationHoursFromText(pickString(record, ["description", "summary", "context"]), pickString(metadata, ["description", "summary", "context"])) ??
|
|
331
|
+
DEFAULT_DURATION_HOURS[type];
|
|
332
|
+
const explicitBudget = pickNumber(record, [
|
|
333
|
+
"expected_budget_usd",
|
|
334
|
+
"expectedBudgetUsd",
|
|
335
|
+
"budget_usd",
|
|
336
|
+
"budgetUsd",
|
|
337
|
+
]) ??
|
|
338
|
+
pickNumber(metadata, [
|
|
339
|
+
"expected_budget_usd",
|
|
340
|
+
"expectedBudgetUsd",
|
|
341
|
+
"budget_usd",
|
|
342
|
+
"budgetUsd",
|
|
343
|
+
]);
|
|
344
|
+
const extractedBudget = extractBudgetUsdFromText(pickString(record, ["description", "summary", "context"]), pickString(metadata, ["description", "summary", "context"])) ?? null;
|
|
345
|
+
const tokenModeledBudget = estimateBudgetUsdFromDurationHours(expectedDuration > 0 ? expectedDuration : DEFAULT_DURATION_HOURS[type]) || DEFAULT_BUDGET_USD[type];
|
|
346
|
+
const expectedBudget = explicitBudget ??
|
|
347
|
+
(typeof extractedBudget === "number"
|
|
348
|
+
? isLegacyHourlyBudget(extractedBudget, expectedDuration)
|
|
349
|
+
? tokenModeledBudget
|
|
350
|
+
: extractedBudget
|
|
351
|
+
: DEFAULT_BUDGET_USD[type]);
|
|
352
|
+
const priority = normalizePriorityForEntity(record);
|
|
353
|
+
return {
|
|
354
|
+
id: String(record.id ?? ""),
|
|
355
|
+
type,
|
|
356
|
+
title: pickString(record, ["title", "name"]) ??
|
|
357
|
+
`${type[0].toUpperCase()}${type.slice(1)} ${String(record.id ?? "")}`,
|
|
358
|
+
status,
|
|
359
|
+
parentId: parentId ?? null,
|
|
360
|
+
initiativeId: initiativeId ?? null,
|
|
361
|
+
workstreamId: workstreamId ?? null,
|
|
362
|
+
milestoneId: milestoneId ?? null,
|
|
363
|
+
priorityNum: priority.priorityNum,
|
|
364
|
+
priorityLabel: priority.priorityLabel,
|
|
365
|
+
dependencyIds: normalizeDependencies(record),
|
|
366
|
+
dueDate,
|
|
367
|
+
etaEndAt,
|
|
368
|
+
expectedDurationHours: expectedDuration > 0 ? expectedDuration : DEFAULT_DURATION_HOURS[type],
|
|
369
|
+
expectedBudgetUsd: expectedBudget >= 0 ? expectedBudget : DEFAULT_BUDGET_USD[type],
|
|
370
|
+
assignedAgents: normalizeAssignedAgents(record),
|
|
371
|
+
updatedAt: toIsoString(pickString(record, [
|
|
372
|
+
"updated_at",
|
|
373
|
+
"updatedAt",
|
|
374
|
+
"created_at",
|
|
375
|
+
"createdAt",
|
|
376
|
+
])),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
export function isTodoStatus(status) {
|
|
380
|
+
const normalized = status.toLowerCase();
|
|
381
|
+
return (normalized === "todo" ||
|
|
382
|
+
normalized === "not_started" ||
|
|
383
|
+
normalized === "planned" ||
|
|
384
|
+
normalized === "backlog" ||
|
|
385
|
+
normalized === "pending");
|
|
386
|
+
}
|
|
387
|
+
export function isInProgressStatus(status) {
|
|
388
|
+
const normalized = status.toLowerCase();
|
|
389
|
+
return (normalized === "in_progress" ||
|
|
390
|
+
normalized === "active" ||
|
|
391
|
+
normalized === "running" ||
|
|
392
|
+
normalized === "queued");
|
|
393
|
+
}
|
|
394
|
+
export function isDispatchableWorkstreamStatus(status) {
|
|
395
|
+
const normalized = status.toLowerCase();
|
|
396
|
+
if (!normalized)
|
|
397
|
+
return true;
|
|
398
|
+
return !(normalized === "blocked" ||
|
|
399
|
+
normalized === "done" ||
|
|
400
|
+
normalized === "completed" ||
|
|
401
|
+
normalized === "cancelled" ||
|
|
402
|
+
normalized === "archived" ||
|
|
403
|
+
normalized === "deleted");
|
|
404
|
+
}
|
|
405
|
+
export function isDoneStatus(status) {
|
|
406
|
+
const normalized = status.toLowerCase();
|
|
407
|
+
return (normalized === "done" ||
|
|
408
|
+
normalized === "completed" ||
|
|
409
|
+
normalized === "cancelled" ||
|
|
410
|
+
normalized === "archived" ||
|
|
411
|
+
normalized === "deleted");
|
|
412
|
+
}
|
|
413
|
+
function detectCycleEdgeKeys(edges) {
|
|
414
|
+
const adjacency = new Map();
|
|
415
|
+
for (const edge of edges) {
|
|
416
|
+
const list = adjacency.get(edge.from) ?? [];
|
|
417
|
+
list.push(edge.to);
|
|
418
|
+
adjacency.set(edge.from, list);
|
|
419
|
+
}
|
|
420
|
+
const visiting = new Set();
|
|
421
|
+
const visited = new Set();
|
|
422
|
+
const cycleEdgeKeys = new Set();
|
|
423
|
+
function dfs(nodeId) {
|
|
424
|
+
if (visited.has(nodeId))
|
|
425
|
+
return;
|
|
426
|
+
visiting.add(nodeId);
|
|
427
|
+
const next = adjacency.get(nodeId) ?? [];
|
|
428
|
+
for (const childId of next) {
|
|
429
|
+
if (visiting.has(childId)) {
|
|
430
|
+
cycleEdgeKeys.add(`${nodeId}->${childId}`);
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
dfs(childId);
|
|
434
|
+
}
|
|
435
|
+
visiting.delete(nodeId);
|
|
436
|
+
visited.add(nodeId);
|
|
437
|
+
}
|
|
438
|
+
for (const key of adjacency.keys()) {
|
|
439
|
+
if (!visited.has(key))
|
|
440
|
+
dfs(key);
|
|
441
|
+
}
|
|
442
|
+
return cycleEdgeKeys;
|
|
443
|
+
}
|
|
444
|
+
export async function listEntitiesSafe(client, type, filters) {
|
|
445
|
+
try {
|
|
446
|
+
const response = await client.listEntities(type, filters);
|
|
447
|
+
const items = Array.isArray(response.data) ? response.data : [];
|
|
448
|
+
return { items, warning: null };
|
|
449
|
+
}
|
|
450
|
+
catch (err) {
|
|
451
|
+
return {
|
|
452
|
+
items: [],
|
|
453
|
+
warning: `${type} unavailable (${safeErrorMessage(err)})`,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
export async function buildMissionControlGraph(client, initiativeId, options) {
|
|
458
|
+
const degraded = [];
|
|
459
|
+
const preloadedInitiative = options?.initiativeEntity ?? null;
|
|
460
|
+
const [initiativeResult, workstreamResult, milestoneResult, taskResult] = await Promise.all([
|
|
461
|
+
preloadedInitiative
|
|
462
|
+
? Promise.resolve({
|
|
463
|
+
items: [preloadedInitiative],
|
|
464
|
+
warning: null,
|
|
465
|
+
})
|
|
466
|
+
: listEntitiesSafe(client, "initiative", { limit: 300 }),
|
|
467
|
+
listEntitiesSafe(client, "workstream", {
|
|
468
|
+
initiative_id: initiativeId,
|
|
469
|
+
limit: 500,
|
|
470
|
+
}),
|
|
471
|
+
listEntitiesSafe(client, "milestone", {
|
|
472
|
+
initiative_id: initiativeId,
|
|
473
|
+
limit: 700,
|
|
474
|
+
}),
|
|
475
|
+
listEntitiesSafe(client, "task", {
|
|
476
|
+
initiative_id: initiativeId,
|
|
477
|
+
limit: 1200,
|
|
478
|
+
}),
|
|
479
|
+
]);
|
|
480
|
+
for (const warning of [
|
|
481
|
+
initiativeResult.warning,
|
|
482
|
+
workstreamResult.warning,
|
|
483
|
+
milestoneResult.warning,
|
|
484
|
+
taskResult.warning,
|
|
485
|
+
]) {
|
|
486
|
+
if (warning)
|
|
487
|
+
degraded.push(warning);
|
|
488
|
+
}
|
|
489
|
+
const initiativeEntity = initiativeResult.items.find((item) => String(item.id ?? "") === initiativeId);
|
|
490
|
+
const initiativeNode = initiativeEntity
|
|
491
|
+
? toMissionControlNode("initiative", initiativeEntity, initiativeId)
|
|
492
|
+
: {
|
|
493
|
+
id: initiativeId,
|
|
494
|
+
type: "initiative",
|
|
495
|
+
title: `Initiative ${initiativeId.slice(0, 8)}`,
|
|
496
|
+
status: "active",
|
|
497
|
+
parentId: null,
|
|
498
|
+
initiativeId,
|
|
499
|
+
workstreamId: null,
|
|
500
|
+
milestoneId: null,
|
|
501
|
+
priorityNum: 60,
|
|
502
|
+
priorityLabel: null,
|
|
503
|
+
dependencyIds: [],
|
|
504
|
+
dueDate: null,
|
|
505
|
+
etaEndAt: null,
|
|
506
|
+
expectedDurationHours: DEFAULT_DURATION_HOURS.initiative,
|
|
507
|
+
expectedBudgetUsd: DEFAULT_BUDGET_USD.initiative,
|
|
508
|
+
assignedAgents: [],
|
|
509
|
+
updatedAt: null,
|
|
510
|
+
};
|
|
511
|
+
const workstreamNodes = workstreamResult.items.map((item) => toMissionControlNode("workstream", item, initiativeId));
|
|
512
|
+
const milestoneNodes = milestoneResult.items.map((item) => toMissionControlNode("milestone", item, initiativeId));
|
|
513
|
+
const taskNodes = taskResult.items.map((item) => toMissionControlNode("task", item, initiativeId));
|
|
514
|
+
const nodes = [
|
|
515
|
+
initiativeNode,
|
|
516
|
+
...workstreamNodes,
|
|
517
|
+
...milestoneNodes,
|
|
518
|
+
...taskNodes,
|
|
519
|
+
];
|
|
520
|
+
const nodeMap = new Map(nodes.map((node) => [node.id, node]));
|
|
521
|
+
for (const node of nodes) {
|
|
522
|
+
const validDependencies = dedupeStrings(node.dependencyIds.filter((depId) => depId !== node.id && nodeMap.has(depId)));
|
|
523
|
+
node.dependencyIds = validDependencies;
|
|
524
|
+
}
|
|
525
|
+
let edges = [];
|
|
526
|
+
for (const node of nodes) {
|
|
527
|
+
if (node.type === "initiative")
|
|
528
|
+
continue;
|
|
529
|
+
for (const depId of node.dependencyIds) {
|
|
530
|
+
edges.push({
|
|
531
|
+
from: depId,
|
|
532
|
+
to: node.id,
|
|
533
|
+
kind: "depends_on",
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
edges = edges.filter((edge, index, arr) => arr.findIndex((candidate) => candidate.from === edge.from &&
|
|
538
|
+
candidate.to === edge.to &&
|
|
539
|
+
candidate.kind === edge.kind) === index);
|
|
540
|
+
const cyclicEdgeKeys = detectCycleEdgeKeys(edges);
|
|
541
|
+
if (cyclicEdgeKeys.size > 0) {
|
|
542
|
+
degraded.push(`Detected ${cyclicEdgeKeys.size} cyclic dependency edge(s); excluded from ETA graph.`);
|
|
543
|
+
edges = edges.filter((edge) => !cyclicEdgeKeys.has(`${edge.from}->${edge.to}`));
|
|
544
|
+
for (const node of nodes) {
|
|
545
|
+
node.dependencyIds = node.dependencyIds.filter((depId) => !cyclicEdgeKeys.has(`${depId}->${node.id}`));
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
const etaMemo = new Map();
|
|
549
|
+
const etaVisiting = new Set();
|
|
550
|
+
const computeEtaEpoch = (nodeId) => {
|
|
551
|
+
const node = nodeMap.get(nodeId);
|
|
552
|
+
if (!node)
|
|
553
|
+
return Date.now();
|
|
554
|
+
const cached = etaMemo.get(nodeId);
|
|
555
|
+
if (cached !== undefined)
|
|
556
|
+
return cached;
|
|
557
|
+
const parsedEtaOverride = node.etaEndAt ? Date.parse(node.etaEndAt) : Number.NaN;
|
|
558
|
+
if (Number.isFinite(parsedEtaOverride)) {
|
|
559
|
+
etaMemo.set(nodeId, parsedEtaOverride);
|
|
560
|
+
return parsedEtaOverride;
|
|
561
|
+
}
|
|
562
|
+
const parsedDueDate = node.dueDate ? Date.parse(node.dueDate) : Number.NaN;
|
|
563
|
+
if (Number.isFinite(parsedDueDate)) {
|
|
564
|
+
etaMemo.set(nodeId, parsedDueDate);
|
|
565
|
+
return parsedDueDate;
|
|
566
|
+
}
|
|
567
|
+
if (etaVisiting.has(nodeId)) {
|
|
568
|
+
degraded.push(`ETA cycle fallback on node ${nodeId}.`);
|
|
569
|
+
const fallback = Date.now();
|
|
570
|
+
etaMemo.set(nodeId, fallback);
|
|
571
|
+
return fallback;
|
|
572
|
+
}
|
|
573
|
+
etaVisiting.add(nodeId);
|
|
574
|
+
let dependencyMax = 0;
|
|
575
|
+
for (const depId of node.dependencyIds) {
|
|
576
|
+
dependencyMax = Math.max(dependencyMax, computeEtaEpoch(depId));
|
|
577
|
+
}
|
|
578
|
+
etaVisiting.delete(nodeId);
|
|
579
|
+
const durationMs = (node.expectedDurationHours > 0
|
|
580
|
+
? node.expectedDurationHours
|
|
581
|
+
: DEFAULT_DURATION_HOURS[node.type]) * 60 * 60 * 1000;
|
|
582
|
+
const eta = Math.max(Date.now(), dependencyMax) + durationMs;
|
|
583
|
+
etaMemo.set(nodeId, eta);
|
|
584
|
+
return eta;
|
|
585
|
+
};
|
|
586
|
+
for (const node of nodes) {
|
|
587
|
+
const eta = computeEtaEpoch(node.id);
|
|
588
|
+
if (Number.isFinite(eta)) {
|
|
589
|
+
node.etaEndAt = new Date(eta).toISOString();
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
const taskNodesOnly = nodes.filter((node) => node.type === "task");
|
|
593
|
+
const hasActiveTasks = taskNodesOnly.some((node) => isInProgressStatus(node.status));
|
|
594
|
+
const hasTodoTasks = taskNodesOnly.some((node) => isTodoStatus(node.status));
|
|
595
|
+
if (initiativeNode.status.toLowerCase() === "active" &&
|
|
596
|
+
!hasActiveTasks &&
|
|
597
|
+
hasTodoTasks) {
|
|
598
|
+
initiativeNode.status = "paused";
|
|
599
|
+
}
|
|
600
|
+
const nodeById = new Map(nodes.map((node) => [node.id, node]));
|
|
601
|
+
const taskIsReady = (task) => task.dependencyIds.every((depId) => {
|
|
602
|
+
const dependency = nodeById.get(depId);
|
|
603
|
+
return dependency ? isDoneStatus(dependency.status) : true;
|
|
604
|
+
});
|
|
605
|
+
const taskHasBlockedParent = (task) => {
|
|
606
|
+
const milestone = task.milestoneId ? nodeById.get(task.milestoneId) ?? null : null;
|
|
607
|
+
const workstream = task.workstreamId ? nodeById.get(task.workstreamId) ?? null : null;
|
|
608
|
+
return (milestone?.status?.toLowerCase() === "blocked" ||
|
|
609
|
+
workstream?.status?.toLowerCase() === "blocked");
|
|
610
|
+
};
|
|
611
|
+
const recentTodos = nodes
|
|
612
|
+
.filter((node) => node.type === "task" && isTodoStatus(node.status))
|
|
613
|
+
.sort((a, b) => {
|
|
614
|
+
const aReady = taskIsReady(a);
|
|
615
|
+
const bReady = taskIsReady(b);
|
|
616
|
+
if (aReady !== bReady)
|
|
617
|
+
return aReady ? -1 : 1;
|
|
618
|
+
const aBlocked = taskHasBlockedParent(a);
|
|
619
|
+
const bBlocked = taskHasBlockedParent(b);
|
|
620
|
+
if (aBlocked !== bBlocked)
|
|
621
|
+
return aBlocked ? 1 : -1;
|
|
622
|
+
const priorityDelta = a.priorityNum - b.priorityNum;
|
|
623
|
+
if (priorityDelta !== 0)
|
|
624
|
+
return priorityDelta;
|
|
625
|
+
const aDue = a.dueDate ? Date.parse(a.dueDate) : Number.POSITIVE_INFINITY;
|
|
626
|
+
const bDue = b.dueDate ? Date.parse(b.dueDate) : Number.POSITIVE_INFINITY;
|
|
627
|
+
if (aDue !== bDue)
|
|
628
|
+
return aDue - bDue;
|
|
629
|
+
const aEta = a.etaEndAt ? Date.parse(a.etaEndAt) : Number.POSITIVE_INFINITY;
|
|
630
|
+
const bEta = b.etaEndAt ? Date.parse(b.etaEndAt) : Number.POSITIVE_INFINITY;
|
|
631
|
+
if (aEta !== bEta)
|
|
632
|
+
return aEta - bEta;
|
|
633
|
+
const aEpoch = a.updatedAt ? Date.parse(a.updatedAt) : 0;
|
|
634
|
+
const bEpoch = b.updatedAt ? Date.parse(b.updatedAt) : 0;
|
|
635
|
+
return aEpoch - bEpoch;
|
|
636
|
+
})
|
|
637
|
+
.map((node) => node.id);
|
|
638
|
+
return {
|
|
639
|
+
initiative: {
|
|
640
|
+
id: initiativeNode.id,
|
|
641
|
+
title: initiativeNode.title,
|
|
642
|
+
status: initiativeNode.status,
|
|
643
|
+
summary: initiativeEntity
|
|
644
|
+
? pickString(initiativeEntity, [
|
|
645
|
+
"summary",
|
|
646
|
+
"description",
|
|
647
|
+
"context",
|
|
648
|
+
])
|
|
649
|
+
: null,
|
|
650
|
+
assignedAgents: initiativeNode.assignedAgents,
|
|
651
|
+
},
|
|
652
|
+
nodes,
|
|
653
|
+
edges,
|
|
654
|
+
recentTodos,
|
|
655
|
+
degraded,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
export function normalizeEntityMutationPayload(payload) {
|
|
659
|
+
const next = { ...payload };
|
|
660
|
+
const priorityNumRaw = pickNumber(next, ["priority_num", "priorityNum"]);
|
|
661
|
+
const priorityLabelRaw = pickString(next, ["priority", "priority_label"]);
|
|
662
|
+
if (priorityNumRaw !== null) {
|
|
663
|
+
const clamped = clampPriority(priorityNumRaw);
|
|
664
|
+
next.priority_num = clamped;
|
|
665
|
+
if (!priorityLabelRaw) {
|
|
666
|
+
next.priority = mapPriorityNumToLabel(clamped);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
else if (priorityLabelRaw) {
|
|
670
|
+
next.priority_num = PRIORITY_LABEL_TO_NUM[priorityLabelRaw.toLowerCase()] ?? 60;
|
|
671
|
+
next.priority = priorityLabelRaw.toLowerCase();
|
|
672
|
+
}
|
|
673
|
+
const dependsOnArray = pickStringArray(next, ["depends_on", "dependsOn", "dependencies"]);
|
|
674
|
+
if (dependsOnArray.length > 0) {
|
|
675
|
+
next.depends_on = dedupeStrings(dependsOnArray);
|
|
676
|
+
}
|
|
677
|
+
else if ("depends_on" in next) {
|
|
678
|
+
next.depends_on = [];
|
|
679
|
+
}
|
|
680
|
+
const expectedDuration = pickNumber(next, [
|
|
681
|
+
"expected_duration_hours",
|
|
682
|
+
"expectedDurationHours",
|
|
683
|
+
]);
|
|
684
|
+
if (expectedDuration !== null) {
|
|
685
|
+
next.expected_duration_hours = Math.max(0, expectedDuration);
|
|
686
|
+
}
|
|
687
|
+
const expectedBudget = pickNumber(next, [
|
|
688
|
+
"expected_budget_usd",
|
|
689
|
+
"expectedBudgetUsd",
|
|
690
|
+
"budget_usd",
|
|
691
|
+
"budgetUsd",
|
|
692
|
+
]);
|
|
693
|
+
if (expectedBudget !== null) {
|
|
694
|
+
next.expected_budget_usd = Math.max(0, expectedBudget);
|
|
695
|
+
}
|
|
696
|
+
const etaEndAt = pickString(next, ["eta_end_at", "etaEndAt"]);
|
|
697
|
+
if (etaEndAt !== null) {
|
|
698
|
+
next.eta_end_at = toIsoString(etaEndAt) ?? null;
|
|
699
|
+
}
|
|
700
|
+
const assignedIds = pickStringArray(next, [
|
|
701
|
+
"assigned_agent_ids",
|
|
702
|
+
"assignedAgentIds",
|
|
703
|
+
]);
|
|
704
|
+
const assignedNames = pickStringArray(next, [
|
|
705
|
+
"assigned_agent_names",
|
|
706
|
+
"assignedAgentNames",
|
|
707
|
+
]);
|
|
708
|
+
if (assignedIds.length > 0) {
|
|
709
|
+
next.assigned_agent_ids = dedupeStrings(assignedIds);
|
|
710
|
+
}
|
|
711
|
+
if (assignedNames.length > 0) {
|
|
712
|
+
next.assigned_agent_names = dedupeStrings(assignedNames);
|
|
713
|
+
}
|
|
714
|
+
return next;
|
|
715
|
+
}
|
|
716
|
+
export async function resolveAutoAssignments(input) {
|
|
717
|
+
const warnings = [];
|
|
718
|
+
const assignedById = new Map();
|
|
719
|
+
const addAgent = (agent) => {
|
|
720
|
+
const key = agent.id || `name:${agent.name}`;
|
|
721
|
+
if (!assignedById.has(key))
|
|
722
|
+
assignedById.set(key, agent);
|
|
723
|
+
};
|
|
724
|
+
let liveAgents = [];
|
|
725
|
+
try {
|
|
726
|
+
const data = await input.client.getLiveAgents({
|
|
727
|
+
initiative: input.initiativeId,
|
|
728
|
+
includeIdle: true,
|
|
729
|
+
});
|
|
730
|
+
liveAgents = (Array.isArray(data.agents) ? data.agents : [])
|
|
731
|
+
.map((raw) => {
|
|
732
|
+
if (!raw || typeof raw !== "object")
|
|
733
|
+
return null;
|
|
734
|
+
const record = raw;
|
|
735
|
+
const id = pickString(record, ["id", "agentId"]) ?? "";
|
|
736
|
+
const name = pickString(record, ["name", "agentName"]) ?? (id ? `Agent ${id}` : "");
|
|
737
|
+
if (!name)
|
|
738
|
+
return null;
|
|
739
|
+
return {
|
|
740
|
+
id: id || `name:${name}`,
|
|
741
|
+
name,
|
|
742
|
+
domain: pickString(record, ["domain", "role"]),
|
|
743
|
+
status: pickString(record, ["status"]),
|
|
744
|
+
};
|
|
745
|
+
})
|
|
746
|
+
.filter((item) => item !== null);
|
|
747
|
+
}
|
|
748
|
+
catch (err) {
|
|
749
|
+
warnings.push(`live agent lookup failed (${safeErrorMessage(err)})`);
|
|
750
|
+
}
|
|
751
|
+
const orchestrator = liveAgents.find((agent) => /holt|orchestrator/i.test(agent.name) ||
|
|
752
|
+
/orchestrator/i.test(agent.domain ?? ""));
|
|
753
|
+
if (orchestrator)
|
|
754
|
+
addAgent(orchestrator);
|
|
755
|
+
let assignmentSource = "fallback";
|
|
756
|
+
try {
|
|
757
|
+
const preflight = await input.client.delegationPreflight({
|
|
758
|
+
intent: `${input.title}${input.summary ? `: ${input.summary}` : ""}`,
|
|
759
|
+
});
|
|
760
|
+
const recommendations = preflight.data?.recommended_split ?? [];
|
|
761
|
+
const recommendedDomains = dedupeStrings(recommendations
|
|
762
|
+
.map((entry) => String(entry.owner_domain ?? "").trim().toLowerCase())
|
|
763
|
+
.filter(Boolean));
|
|
764
|
+
for (const domain of recommendedDomains) {
|
|
765
|
+
const matched = liveAgents.find((agent) => (agent.domain ?? "").toLowerCase().includes(domain));
|
|
766
|
+
if (matched)
|
|
767
|
+
addAgent(matched);
|
|
768
|
+
}
|
|
769
|
+
if (recommendedDomains.length > 0) {
|
|
770
|
+
assignmentSource = "orchestrator";
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
catch (err) {
|
|
774
|
+
warnings.push(`delegation preflight failed (${safeErrorMessage(err)})`);
|
|
775
|
+
}
|
|
776
|
+
if (assignedById.size === 0) {
|
|
777
|
+
const text = `${input.title} ${input.summary ?? ""}`.toLowerCase();
|
|
778
|
+
const fallbackDomains = [];
|
|
779
|
+
if (/market|campaign|thread|article|tweet|copy/.test(text)) {
|
|
780
|
+
fallbackDomains.push("marketing");
|
|
781
|
+
}
|
|
782
|
+
else if (/design|ux|ui|a11y|accessibility/.test(text)) {
|
|
783
|
+
fallbackDomains.push("design");
|
|
784
|
+
}
|
|
785
|
+
else if (/ops|incident|runbook|reliability/.test(text)) {
|
|
786
|
+
fallbackDomains.push("operations");
|
|
787
|
+
}
|
|
788
|
+
else if (/sales|deal|pipeline|mrr/.test(text)) {
|
|
789
|
+
fallbackDomains.push("sales");
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
fallbackDomains.push("engineering", "product");
|
|
793
|
+
}
|
|
794
|
+
for (const domain of fallbackDomains) {
|
|
795
|
+
const matched = liveAgents.find((agent) => (agent.domain ?? "").toLowerCase().includes(domain));
|
|
796
|
+
if (matched)
|
|
797
|
+
addAgent(matched);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
if (assignedById.size === 0 && liveAgents.length > 0) {
|
|
801
|
+
addAgent(liveAgents[0]);
|
|
802
|
+
warnings.push("using first available live agent as fallback");
|
|
803
|
+
}
|
|
804
|
+
const assignedAgents = Array.from(assignedById.values());
|
|
805
|
+
const updatePayload = normalizeEntityMutationPayload({
|
|
806
|
+
assigned_agent_ids: assignedAgents.map((agent) => agent.id),
|
|
807
|
+
assigned_agent_names: assignedAgents.map((agent) => agent.name),
|
|
808
|
+
});
|
|
809
|
+
let updatedEntity;
|
|
810
|
+
try {
|
|
811
|
+
updatedEntity = await input.client.updateEntity(input.entityType, input.entityId, updatePayload);
|
|
812
|
+
}
|
|
813
|
+
catch (err) {
|
|
814
|
+
warnings.push(`assignment patch failed (${safeErrorMessage(err)})`);
|
|
815
|
+
}
|
|
816
|
+
return {
|
|
817
|
+
ok: true,
|
|
818
|
+
assignment_source: assignmentSource,
|
|
819
|
+
assigned_agents: assignedAgents,
|
|
820
|
+
warnings,
|
|
821
|
+
...(updatedEntity ? { updated_entity: updatedEntity } : {}),
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
export function normalizeExecutionDomain(value) {
|
|
825
|
+
const raw = (value ?? "").trim().toLowerCase();
|
|
826
|
+
if (!raw)
|
|
827
|
+
return null;
|
|
828
|
+
if (raw === "orchestrator")
|
|
829
|
+
return "orchestration";
|
|
830
|
+
if (raw === "ops")
|
|
831
|
+
return "operations";
|
|
832
|
+
return Object.prototype.hasOwnProperty.call(ORGX_SKILL_BY_DOMAIN, raw)
|
|
833
|
+
? raw
|
|
834
|
+
: null;
|
|
835
|
+
}
|
|
836
|
+
export function inferExecutionDomainFromText(...values) {
|
|
837
|
+
const text = values
|
|
838
|
+
.map((value) => (value ?? "").trim().toLowerCase())
|
|
839
|
+
.filter((value) => value.length > 0)
|
|
840
|
+
.join(" ");
|
|
841
|
+
if (!text)
|
|
842
|
+
return "engineering";
|
|
843
|
+
if (/\b(marketing|campaign|copy|ad|content)\b/.test(text))
|
|
844
|
+
return "marketing";
|
|
845
|
+
if (/\b(sales|meddic|pipeline|deal|outreach)\b/.test(text))
|
|
846
|
+
return "sales";
|
|
847
|
+
if (/\b(design|ui|ux|brand|wcag)\b/.test(text))
|
|
848
|
+
return "design";
|
|
849
|
+
if (/\b(product|prd|roadmap|prioritization)\b/.test(text))
|
|
850
|
+
return "product";
|
|
851
|
+
if (/\b(ops|operations|incident|reliability|oncall|slo)\b/.test(text))
|
|
852
|
+
return "operations";
|
|
853
|
+
if (/\b(orchestration|dispatch|handoff)\b/.test(text))
|
|
854
|
+
return "orchestration";
|
|
855
|
+
return "engineering";
|
|
856
|
+
}
|
|
857
|
+
export function deriveExecutionPolicy(taskNode, workstreamNode) {
|
|
858
|
+
const domainCandidate = taskNode.assignedAgents
|
|
859
|
+
.map((agent) => normalizeExecutionDomain(agent.domain))
|
|
860
|
+
.find((domain) => Boolean(domain)) ??
|
|
861
|
+
(workstreamNode
|
|
862
|
+
? workstreamNode.assignedAgents
|
|
863
|
+
.map((agent) => normalizeExecutionDomain(agent.domain))
|
|
864
|
+
.find((domain) => Boolean(domain))
|
|
865
|
+
: null) ??
|
|
866
|
+
inferExecutionDomainFromText(taskNode.title, workstreamNode?.title ?? null);
|
|
867
|
+
const domain = normalizeExecutionDomain(domainCandidate) ?? "engineering";
|
|
868
|
+
const requiredSkill = ORGX_SKILL_BY_DOMAIN[domain] ?? ORGX_SKILL_BY_DOMAIN.engineering;
|
|
869
|
+
return { domain, requiredSkills: [requiredSkill] };
|
|
870
|
+
}
|
|
871
|
+
export function spawnGuardIsRateLimited(result) {
|
|
872
|
+
if (!result || typeof result !== "object")
|
|
873
|
+
return false;
|
|
874
|
+
const record = result;
|
|
875
|
+
const checks = record.checks;
|
|
876
|
+
if (!checks || typeof checks !== "object")
|
|
877
|
+
return false;
|
|
878
|
+
const rateLimit = checks.rateLimit;
|
|
879
|
+
if (!rateLimit || typeof rateLimit !== "object")
|
|
880
|
+
return false;
|
|
881
|
+
return rateLimit.passed === false;
|
|
882
|
+
}
|
|
883
|
+
export function summarizeSpawnGuardBlockReason(result) {
|
|
884
|
+
if (!result || typeof result !== "object")
|
|
885
|
+
return "Spawn guard denied dispatch.";
|
|
886
|
+
const record = result;
|
|
887
|
+
const blockedReason = pickString(record, ["blockedReason", "blocked_reason"]);
|
|
888
|
+
if (blockedReason)
|
|
889
|
+
return blockedReason;
|
|
890
|
+
if (spawnGuardIsRateLimited(result)) {
|
|
891
|
+
return "Spawn guard rate limit reached.";
|
|
892
|
+
}
|
|
893
|
+
return "Spawn guard denied dispatch.";
|
|
894
|
+
}
|