@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,1383 @@
|
|
|
1
|
+
import { pickNumber, pickString, toIsoString } from "./value-utils.js";
|
|
2
|
+
export const SLICE_SCOPE_MAX_TASKS = {
|
|
3
|
+
task: 6,
|
|
4
|
+
milestone: 15,
|
|
5
|
+
workstream: 30,
|
|
6
|
+
};
|
|
7
|
+
export const SLICE_SCOPE_TIMEOUT_MULTIPLIER = {
|
|
8
|
+
task: 1,
|
|
9
|
+
milestone: 2.5,
|
|
10
|
+
workstream: 4,
|
|
11
|
+
};
|
|
12
|
+
export const ORGX_SKILL_BY_DOMAIN = {
|
|
13
|
+
engineering: "orgx-engineering-agent",
|
|
14
|
+
product: "orgx-product-agent",
|
|
15
|
+
marketing: "orgx-marketing-agent",
|
|
16
|
+
sales: "orgx-sales-agent",
|
|
17
|
+
operations: "orgx-operations-agent",
|
|
18
|
+
design: "orgx-design-agent",
|
|
19
|
+
orchestration: "orgx-orchestrator-agent",
|
|
20
|
+
};
|
|
21
|
+
function safeErrorMessage(err) {
|
|
22
|
+
if (err instanceof Error)
|
|
23
|
+
return err.message;
|
|
24
|
+
if (typeof err === "string")
|
|
25
|
+
return err;
|
|
26
|
+
return "Unexpected error";
|
|
27
|
+
}
|
|
28
|
+
function toNullableBoolean(value) {
|
|
29
|
+
if (typeof value === "boolean")
|
|
30
|
+
return value;
|
|
31
|
+
if (typeof value === "number") {
|
|
32
|
+
if (!Number.isFinite(value))
|
|
33
|
+
return null;
|
|
34
|
+
if (value === 1)
|
|
35
|
+
return true;
|
|
36
|
+
if (value === 0)
|
|
37
|
+
return false;
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (typeof value !== "string")
|
|
41
|
+
return null;
|
|
42
|
+
const normalized = value.trim().toLowerCase();
|
|
43
|
+
if (!normalized)
|
|
44
|
+
return null;
|
|
45
|
+
if (["true", "1", "yes", "y", "on", "required"].includes(normalized))
|
|
46
|
+
return true;
|
|
47
|
+
if (["false", "0", "no", "n", "off"].includes(normalized))
|
|
48
|
+
return false;
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function normalizeBehaviorAutomationLevel(value) {
|
|
52
|
+
if (typeof value !== "string")
|
|
53
|
+
return null;
|
|
54
|
+
const normalized = value
|
|
55
|
+
.trim()
|
|
56
|
+
.toLowerCase()
|
|
57
|
+
.replace(/[\s-]+/g, "_");
|
|
58
|
+
if (!normalized)
|
|
59
|
+
return null;
|
|
60
|
+
if (normalized === "manual" ||
|
|
61
|
+
normalized === "manual_only" ||
|
|
62
|
+
normalized === "human" ||
|
|
63
|
+
normalized === "human_only") {
|
|
64
|
+
return "manual";
|
|
65
|
+
}
|
|
66
|
+
if (normalized === "supervised" ||
|
|
67
|
+
normalized === "constrained" ||
|
|
68
|
+
normalized === "reviewed" ||
|
|
69
|
+
normalized === "human_review") {
|
|
70
|
+
return "supervised";
|
|
71
|
+
}
|
|
72
|
+
if (normalized === "auto" ||
|
|
73
|
+
normalized === "autonomous" ||
|
|
74
|
+
normalized === "default") {
|
|
75
|
+
return "auto";
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
export function readBudgetEnvNumber(name, fallback, bounds = {}) {
|
|
80
|
+
const raw = process.env[name];
|
|
81
|
+
if (typeof raw !== "string" || raw.trim().length === 0)
|
|
82
|
+
return fallback;
|
|
83
|
+
const parsed = Number(raw);
|
|
84
|
+
if (!Number.isFinite(parsed))
|
|
85
|
+
return fallback;
|
|
86
|
+
if (typeof bounds.min === "number" && parsed < bounds.min)
|
|
87
|
+
return fallback;
|
|
88
|
+
if (typeof bounds.max === "number" && parsed > bounds.max)
|
|
89
|
+
return fallback;
|
|
90
|
+
return parsed;
|
|
91
|
+
}
|
|
92
|
+
const DEFAULT_TOKEN_MODEL_PRICING_USD_PER_1M = {
|
|
93
|
+
// GPT-5.3 Codex API pricing is not published yet; use GPT-5.2 Codex pricing as proxy.
|
|
94
|
+
gpt53CodexProxy: {
|
|
95
|
+
input: readBudgetEnvNumber("ORGX_BUDGET_GPT53_CODEX_INPUT_PER_1M", 1.75, { min: 0 }),
|
|
96
|
+
cachedInput: readBudgetEnvNumber("ORGX_BUDGET_GPT53_CODEX_CACHED_INPUT_PER_1M", 0.175, {
|
|
97
|
+
min: 0,
|
|
98
|
+
}),
|
|
99
|
+
output: readBudgetEnvNumber("ORGX_BUDGET_GPT53_CODEX_OUTPUT_PER_1M", 14, { min: 0 }),
|
|
100
|
+
},
|
|
101
|
+
opus46: {
|
|
102
|
+
input: readBudgetEnvNumber("ORGX_BUDGET_OPUS46_INPUT_PER_1M", 5, { min: 0 }),
|
|
103
|
+
// Anthropic does not publish a fixed cached-input rate on the model page.
|
|
104
|
+
cachedInput: readBudgetEnvNumber("ORGX_BUDGET_OPUS46_CACHED_INPUT_PER_1M", 5, { min: 0 }),
|
|
105
|
+
output: readBudgetEnvNumber("ORGX_BUDGET_OPUS46_OUTPUT_PER_1M", 25, { min: 0 }),
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
export const DEFAULT_TOKEN_BUDGET_ASSUMPTIONS = {
|
|
109
|
+
tokensPerHour: readBudgetEnvNumber("ORGX_BUDGET_TOKENS_PER_HOUR", 1_200_000, { min: 1 }),
|
|
110
|
+
inputShare: readBudgetEnvNumber("ORGX_BUDGET_INPUT_TOKEN_SHARE", 0.86, { min: 0, max: 1 }),
|
|
111
|
+
cachedInputShare: readBudgetEnvNumber("ORGX_BUDGET_CACHED_INPUT_SHARE", 0.15, {
|
|
112
|
+
min: 0,
|
|
113
|
+
max: 1,
|
|
114
|
+
}),
|
|
115
|
+
contingencyMultiplier: readBudgetEnvNumber("ORGX_BUDGET_CONTINGENCY_MULTIPLIER", 1.3, {
|
|
116
|
+
min: 0.1,
|
|
117
|
+
}),
|
|
118
|
+
roundingStepUsd: readBudgetEnvNumber("ORGX_BUDGET_ROUNDING_STEP_USD", 5, { min: 0.01 }),
|
|
119
|
+
};
|
|
120
|
+
const DEFAULT_TOKEN_MODEL_MIX = {
|
|
121
|
+
gpt53CodexProxy: 0.7,
|
|
122
|
+
opus46: 0.3,
|
|
123
|
+
};
|
|
124
|
+
function modelCostPerMillionTokensUsd(pricing) {
|
|
125
|
+
const inputShare = DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.inputShare;
|
|
126
|
+
const outputShare = Math.max(0, 1 - inputShare);
|
|
127
|
+
const cachedShare = DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.cachedInputShare;
|
|
128
|
+
const uncachedShare = Math.max(0, 1 - cachedShare);
|
|
129
|
+
const effectiveInputRate = pricing.input * uncachedShare + pricing.cachedInput * cachedShare;
|
|
130
|
+
return inputShare * effectiveInputRate + outputShare * pricing.output;
|
|
131
|
+
}
|
|
132
|
+
function estimateBudgetUsdFromDurationHours(durationHours) {
|
|
133
|
+
if (!Number.isFinite(durationHours) || durationHours <= 0)
|
|
134
|
+
return 0;
|
|
135
|
+
const blendedPerMillionUsd = DEFAULT_TOKEN_MODEL_MIX.gpt53CodexProxy *
|
|
136
|
+
modelCostPerMillionTokensUsd(DEFAULT_TOKEN_MODEL_PRICING_USD_PER_1M.gpt53CodexProxy) +
|
|
137
|
+
DEFAULT_TOKEN_MODEL_MIX.opus46 *
|
|
138
|
+
modelCostPerMillionTokensUsd(DEFAULT_TOKEN_MODEL_PRICING_USD_PER_1M.opus46);
|
|
139
|
+
const tokenMillions = (durationHours * DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.tokensPerHour) / 1_000_000;
|
|
140
|
+
const rawBudgetUsd = tokenMillions *
|
|
141
|
+
blendedPerMillionUsd *
|
|
142
|
+
DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.contingencyMultiplier;
|
|
143
|
+
const roundedBudgetUsd = Math.round(rawBudgetUsd / DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.roundingStepUsd) *
|
|
144
|
+
DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.roundingStepUsd;
|
|
145
|
+
return Math.max(0, roundedBudgetUsd);
|
|
146
|
+
}
|
|
147
|
+
function isLegacyHourlyBudget(budgetUsd, durationHours) {
|
|
148
|
+
if (!Number.isFinite(budgetUsd) || !Number.isFinite(durationHours) || durationHours <= 0) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
const legacyHourlyBudget = durationHours * 40;
|
|
152
|
+
return Math.abs(budgetUsd - legacyHourlyBudget) <= 0.5;
|
|
153
|
+
}
|
|
154
|
+
const DEFAULT_DURATION_HOURS = {
|
|
155
|
+
initiative: 40,
|
|
156
|
+
workstream: 16,
|
|
157
|
+
milestone: 6,
|
|
158
|
+
task: 2,
|
|
159
|
+
};
|
|
160
|
+
const DEFAULT_BUDGET_USD = {
|
|
161
|
+
initiative: estimateBudgetUsdFromDurationHours(DEFAULT_DURATION_HOURS.initiative),
|
|
162
|
+
workstream: estimateBudgetUsdFromDurationHours(DEFAULT_DURATION_HOURS.workstream),
|
|
163
|
+
milestone: estimateBudgetUsdFromDurationHours(DEFAULT_DURATION_HOURS.milestone),
|
|
164
|
+
task: estimateBudgetUsdFromDurationHours(DEFAULT_DURATION_HOURS.task),
|
|
165
|
+
};
|
|
166
|
+
const PRIORITY_LABEL_TO_NUM = {
|
|
167
|
+
urgent: 10,
|
|
168
|
+
high: 25,
|
|
169
|
+
medium: 50,
|
|
170
|
+
low: 75,
|
|
171
|
+
};
|
|
172
|
+
function clampPriority(value) {
|
|
173
|
+
if (!Number.isFinite(value))
|
|
174
|
+
return 60;
|
|
175
|
+
return Math.max(1, Math.min(100, Math.round(value)));
|
|
176
|
+
}
|
|
177
|
+
function mapPriorityNumToLabel(priorityNum) {
|
|
178
|
+
if (priorityNum <= 12)
|
|
179
|
+
return "urgent";
|
|
180
|
+
if (priorityNum <= 30)
|
|
181
|
+
return "high";
|
|
182
|
+
if (priorityNum <= 60)
|
|
183
|
+
return "medium";
|
|
184
|
+
return "low";
|
|
185
|
+
}
|
|
186
|
+
function getRecordMetadata(record) {
|
|
187
|
+
const metadata = record.metadata;
|
|
188
|
+
if (metadata && typeof metadata === "object" && !Array.isArray(metadata)) {
|
|
189
|
+
return metadata;
|
|
190
|
+
}
|
|
191
|
+
return {};
|
|
192
|
+
}
|
|
193
|
+
function extractBudgetUsdFromText(...texts) {
|
|
194
|
+
for (const text of texts) {
|
|
195
|
+
if (typeof text !== "string" || text.trim().length === 0)
|
|
196
|
+
continue;
|
|
197
|
+
const moneyMatch = /(?:expected\s+budget|budget)[^0-9$]{0,24}\$?\s*([0-9][0-9,]*(?:\.[0-9]{1,2})?)/i.exec(text);
|
|
198
|
+
if (!moneyMatch)
|
|
199
|
+
continue;
|
|
200
|
+
const numeric = Number(moneyMatch[1].replace(/,/g, ""));
|
|
201
|
+
if (Number.isFinite(numeric) && numeric >= 0)
|
|
202
|
+
return numeric;
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
function extractDurationHoursFromText(...texts) {
|
|
207
|
+
for (const text of texts) {
|
|
208
|
+
if (typeof text !== "string" || text.trim().length === 0)
|
|
209
|
+
continue;
|
|
210
|
+
const durationMatch = /(?:expected\s+duration|duration)[^0-9]{0,24}([0-9]+(?:\.[0-9]+)?)\s*h/i.exec(text);
|
|
211
|
+
if (!durationMatch)
|
|
212
|
+
continue;
|
|
213
|
+
const numeric = Number(durationMatch[1]);
|
|
214
|
+
if (Number.isFinite(numeric) && numeric >= 0)
|
|
215
|
+
return numeric;
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
export function pickStringArray(record, keys) {
|
|
220
|
+
for (const key of keys) {
|
|
221
|
+
const value = record[key];
|
|
222
|
+
if (Array.isArray(value)) {
|
|
223
|
+
const items = value
|
|
224
|
+
.filter((entry) => typeof entry === "string")
|
|
225
|
+
.map((entry) => entry.trim())
|
|
226
|
+
.filter(Boolean);
|
|
227
|
+
if (items.length > 0)
|
|
228
|
+
return items;
|
|
229
|
+
}
|
|
230
|
+
if (typeof value === "string") {
|
|
231
|
+
const items = value
|
|
232
|
+
.split(",")
|
|
233
|
+
.map((entry) => entry.trim())
|
|
234
|
+
.filter(Boolean);
|
|
235
|
+
if (items.length > 0)
|
|
236
|
+
return items;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
export function dedupeStrings(items) {
|
|
242
|
+
const seen = new Set();
|
|
243
|
+
const out = [];
|
|
244
|
+
for (const item of items) {
|
|
245
|
+
if (!item || seen.has(item))
|
|
246
|
+
continue;
|
|
247
|
+
seen.add(item);
|
|
248
|
+
out.push(item);
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
function normalizePriorityForEntity(record) {
|
|
253
|
+
const explicitPriorityNum = pickNumber(record, [
|
|
254
|
+
"priority_num",
|
|
255
|
+
"priorityNum",
|
|
256
|
+
"priority_number",
|
|
257
|
+
]);
|
|
258
|
+
const priorityLabelRaw = pickString(record, ["priority", "priority_label"]);
|
|
259
|
+
if (explicitPriorityNum !== null) {
|
|
260
|
+
const clamped = clampPriority(explicitPriorityNum);
|
|
261
|
+
return {
|
|
262
|
+
priorityNum: clamped,
|
|
263
|
+
priorityLabel: priorityLabelRaw ?? mapPriorityNumToLabel(clamped),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
if (priorityLabelRaw) {
|
|
267
|
+
const mapped = PRIORITY_LABEL_TO_NUM[priorityLabelRaw.toLowerCase()] ?? 60;
|
|
268
|
+
return {
|
|
269
|
+
priorityNum: mapped,
|
|
270
|
+
priorityLabel: priorityLabelRaw.toLowerCase(),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
priorityNum: 60,
|
|
275
|
+
priorityLabel: null,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function normalizeDependencies(record) {
|
|
279
|
+
const metadata = getRecordMetadata(record);
|
|
280
|
+
const direct = pickStringArray(record, [
|
|
281
|
+
"depends_on",
|
|
282
|
+
"dependsOn",
|
|
283
|
+
"dependency_ids",
|
|
284
|
+
"dependencyIds",
|
|
285
|
+
"dependencies",
|
|
286
|
+
]);
|
|
287
|
+
const nested = pickStringArray(metadata, [
|
|
288
|
+
"depends_on",
|
|
289
|
+
"dependsOn",
|
|
290
|
+
"dependency_ids",
|
|
291
|
+
"dependencyIds",
|
|
292
|
+
"dependencies",
|
|
293
|
+
]);
|
|
294
|
+
return dedupeStrings([...direct, ...nested]);
|
|
295
|
+
}
|
|
296
|
+
function toPositiveInteger(value) {
|
|
297
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
298
|
+
const normalized = Math.floor(value);
|
|
299
|
+
return normalized > 0 ? normalized : null;
|
|
300
|
+
}
|
|
301
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
302
|
+
const parsed = Number(value);
|
|
303
|
+
if (!Number.isFinite(parsed))
|
|
304
|
+
return null;
|
|
305
|
+
const normalized = Math.floor(parsed);
|
|
306
|
+
return normalized > 0 ? normalized : null;
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
function normalizeSliceScopePreference(value) {
|
|
311
|
+
if (typeof value !== "string")
|
|
312
|
+
return null;
|
|
313
|
+
const normalized = value.trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
314
|
+
if (!normalized)
|
|
315
|
+
return null;
|
|
316
|
+
if (normalized === "adaptive" || normalized === "task" || normalized === "milestone" || normalized === "workstream") {
|
|
317
|
+
return normalized;
|
|
318
|
+
}
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
function normalizeDependencyMode(value) {
|
|
322
|
+
if (typeof value !== "string")
|
|
323
|
+
return null;
|
|
324
|
+
const normalized = value.trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
325
|
+
if (!normalized)
|
|
326
|
+
return null;
|
|
327
|
+
if (normalized === "strict" || normalized === "relaxed") {
|
|
328
|
+
return normalized;
|
|
329
|
+
}
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
function normalizeAssignedAgents(record) {
|
|
333
|
+
const metadata = getRecordMetadata(record);
|
|
334
|
+
const ids = dedupeStrings([
|
|
335
|
+
...pickStringArray(record, ["assigned_agent_ids", "assignedAgentIds"]),
|
|
336
|
+
...pickStringArray(metadata, ["assigned_agent_ids", "assignedAgentIds"]),
|
|
337
|
+
]);
|
|
338
|
+
const names = dedupeStrings([
|
|
339
|
+
...pickStringArray(record, ["assigned_agent_names", "assignedAgentNames"]),
|
|
340
|
+
...pickStringArray(metadata, ["assigned_agent_names", "assignedAgentNames"]),
|
|
341
|
+
]);
|
|
342
|
+
const objectCandidates = [
|
|
343
|
+
record.assigned_agents,
|
|
344
|
+
record.assignedAgents,
|
|
345
|
+
metadata.assigned_agents,
|
|
346
|
+
metadata.assignedAgents,
|
|
347
|
+
];
|
|
348
|
+
const fromObjects = [];
|
|
349
|
+
for (const candidate of objectCandidates) {
|
|
350
|
+
if (!Array.isArray(candidate))
|
|
351
|
+
continue;
|
|
352
|
+
for (const entry of candidate) {
|
|
353
|
+
if (!entry || typeof entry !== "object")
|
|
354
|
+
continue;
|
|
355
|
+
const item = entry;
|
|
356
|
+
const id = pickString(item, ["id", "agent_id", "agentId"]) ?? "";
|
|
357
|
+
const name = pickString(item, ["name", "agent_name", "agentName"]) ?? id;
|
|
358
|
+
if (!name)
|
|
359
|
+
continue;
|
|
360
|
+
fromObjects.push({
|
|
361
|
+
id: id || `name:${name}`,
|
|
362
|
+
name,
|
|
363
|
+
domain: pickString(item, ["domain", "role"]),
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const merged = [...fromObjects];
|
|
368
|
+
if (merged.length === 0 && (names.length > 0 || ids.length > 0)) {
|
|
369
|
+
const maxLen = Math.max(names.length, ids.length);
|
|
370
|
+
for (let i = 0; i < maxLen; i += 1) {
|
|
371
|
+
const id = ids[i] ?? `name:${names[i] ?? `agent-${i + 1}`}`;
|
|
372
|
+
const name = names[i] ?? ids[i] ?? `Agent ${i + 1}`;
|
|
373
|
+
merged.push({ id, name, domain: null });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const seen = new Set();
|
|
377
|
+
const deduped = [];
|
|
378
|
+
for (const item of merged) {
|
|
379
|
+
const key = `${item.id}:${item.name}`.toLowerCase();
|
|
380
|
+
if (seen.has(key))
|
|
381
|
+
continue;
|
|
382
|
+
seen.add(key);
|
|
383
|
+
deduped.push(item);
|
|
384
|
+
}
|
|
385
|
+
return deduped;
|
|
386
|
+
}
|
|
387
|
+
function toMissionControlNode(type, entity, fallbackInitiativeId) {
|
|
388
|
+
const record = entity;
|
|
389
|
+
const metadata = getRecordMetadata(record);
|
|
390
|
+
const initiativeId = pickString(record, ["initiative_id", "initiativeId"]) ??
|
|
391
|
+
pickString(metadata, ["initiative_id", "initiativeId"]) ??
|
|
392
|
+
(type === "initiative" ? String(record.id ?? fallbackInitiativeId) : fallbackInitiativeId);
|
|
393
|
+
const workstreamId = type === "workstream"
|
|
394
|
+
? String(record.id ?? "")
|
|
395
|
+
: pickString(record, ["workstream_id", "workstreamId"]) ??
|
|
396
|
+
pickString(metadata, ["workstream_id", "workstreamId"]);
|
|
397
|
+
const milestoneId = type === "milestone"
|
|
398
|
+
? String(record.id ?? "")
|
|
399
|
+
: pickString(record, ["milestone_id", "milestoneId"]) ??
|
|
400
|
+
pickString(metadata, ["milestone_id", "milestoneId"]);
|
|
401
|
+
const parentIdRaw = pickString(record, ["parentId", "parent_id"]) ??
|
|
402
|
+
pickString(metadata, ["parentId", "parent_id"]);
|
|
403
|
+
const parentId = parentIdRaw ??
|
|
404
|
+
(type === "initiative"
|
|
405
|
+
? null
|
|
406
|
+
: type === "workstream"
|
|
407
|
+
? initiativeId
|
|
408
|
+
: type === "milestone"
|
|
409
|
+
? workstreamId ?? initiativeId
|
|
410
|
+
: milestoneId ?? workstreamId ?? initiativeId);
|
|
411
|
+
const status = pickString(record, ["status"]) ??
|
|
412
|
+
(type === "task" ? "todo" : "planned");
|
|
413
|
+
const dueDate = toIsoString(pickString(record, ["due_date", "dueDate", "target_date", "targetDate"]));
|
|
414
|
+
const etaEndAt = toIsoString(pickString(record, ["eta_end_at", "etaEndAt"]));
|
|
415
|
+
const expectedDuration = pickNumber(record, [
|
|
416
|
+
"expected_duration_hours",
|
|
417
|
+
"expectedDurationHours",
|
|
418
|
+
"duration_hours",
|
|
419
|
+
"durationHours",
|
|
420
|
+
]) ??
|
|
421
|
+
pickNumber(metadata, [
|
|
422
|
+
"expected_duration_hours",
|
|
423
|
+
"expectedDurationHours",
|
|
424
|
+
"duration_hours",
|
|
425
|
+
"durationHours",
|
|
426
|
+
]) ??
|
|
427
|
+
extractDurationHoursFromText(pickString(record, ["description", "summary", "context"]), pickString(metadata, ["description", "summary", "context"])) ??
|
|
428
|
+
DEFAULT_DURATION_HOURS[type];
|
|
429
|
+
const explicitBudget = pickNumber(record, [
|
|
430
|
+
"expected_budget_usd",
|
|
431
|
+
"expectedBudgetUsd",
|
|
432
|
+
"budget_usd",
|
|
433
|
+
"budgetUsd",
|
|
434
|
+
]) ??
|
|
435
|
+
pickNumber(metadata, [
|
|
436
|
+
"expected_budget_usd",
|
|
437
|
+
"expectedBudgetUsd",
|
|
438
|
+
"budget_usd",
|
|
439
|
+
"budgetUsd",
|
|
440
|
+
]);
|
|
441
|
+
const extractedBudget = extractBudgetUsdFromText(pickString(record, ["description", "summary", "context"]), pickString(metadata, ["description", "summary", "context"])) ?? null;
|
|
442
|
+
const tokenModeledBudget = estimateBudgetUsdFromDurationHours(expectedDuration > 0 ? expectedDuration : DEFAULT_DURATION_HOURS[type]) || DEFAULT_BUDGET_USD[type];
|
|
443
|
+
const expectedBudget = explicitBudget ??
|
|
444
|
+
(typeof extractedBudget === "number"
|
|
445
|
+
? isLegacyHourlyBudget(extractedBudget, expectedDuration)
|
|
446
|
+
? tokenModeledBudget
|
|
447
|
+
: extractedBudget
|
|
448
|
+
: DEFAULT_BUDGET_USD[type]);
|
|
449
|
+
const priority = normalizePriorityForEntity(record);
|
|
450
|
+
const behaviorConfigId = pickString(record, ["behavior_config_id", "behaviorConfigId"]) ??
|
|
451
|
+
pickString(metadata, ["behavior_config_id", "behaviorConfigId"]);
|
|
452
|
+
const behaviorConfigVersion = pickString(record, ["behavior_config_version", "behaviorConfigVersion"]) ??
|
|
453
|
+
pickString(metadata, ["behavior_config_version", "behaviorConfigVersion"]);
|
|
454
|
+
const behaviorConfigHash = pickString(record, ["behavior_config_hash", "behaviorConfigHash"]) ??
|
|
455
|
+
pickString(metadata, ["behavior_config_hash", "behaviorConfigHash"]);
|
|
456
|
+
const behaviorPolicySource = pickString(record, ["policy_source", "policySource"]) ??
|
|
457
|
+
pickString(metadata, ["policy_source", "policySource"]);
|
|
458
|
+
const behaviorContext = pickString(record, ["behavior_context", "behaviorContext", "behavior_prompt", "behaviorPrompt"]) ??
|
|
459
|
+
pickString(metadata, ["behavior_context", "behaviorContext", "behavior_prompt", "behaviorPrompt"]);
|
|
460
|
+
const behaviorRequiresApproval = toNullableBoolean(record.behavior_requires_approval ??
|
|
461
|
+
record.behaviorRequiresApproval ??
|
|
462
|
+
record.config_requires_approval ??
|
|
463
|
+
record.configRequiresApproval ??
|
|
464
|
+
record.requires_approval ??
|
|
465
|
+
record.requiresApproval) ??
|
|
466
|
+
toNullableBoolean(metadata.behavior_requires_approval ??
|
|
467
|
+
metadata.behaviorRequiresApproval ??
|
|
468
|
+
metadata.config_requires_approval ??
|
|
469
|
+
metadata.configRequiresApproval ??
|
|
470
|
+
metadata.requires_approval ??
|
|
471
|
+
metadata.requiresApproval);
|
|
472
|
+
const behaviorApprovalStatus = pickString(record, ["behavior_approval_status", "behaviorApprovalStatus", "approval_status", "approvalStatus"]) ??
|
|
473
|
+
pickString(metadata, [
|
|
474
|
+
"behavior_approval_status",
|
|
475
|
+
"behaviorApprovalStatus",
|
|
476
|
+
"approval_status",
|
|
477
|
+
"approvalStatus",
|
|
478
|
+
]);
|
|
479
|
+
const behaviorApprovalDecisionId = pickString(record, [
|
|
480
|
+
"behavior_approval_decision_id",
|
|
481
|
+
"behaviorApprovalDecisionId",
|
|
482
|
+
"approval_decision_id",
|
|
483
|
+
"approvalDecisionId",
|
|
484
|
+
]) ??
|
|
485
|
+
pickString(metadata, [
|
|
486
|
+
"behavior_approval_decision_id",
|
|
487
|
+
"behaviorApprovalDecisionId",
|
|
488
|
+
"approval_decision_id",
|
|
489
|
+
"approvalDecisionId",
|
|
490
|
+
]);
|
|
491
|
+
const behaviorAutomationLevel = normalizeBehaviorAutomationLevel(pickString(record, ["automation_level", "automationLevel"])) ??
|
|
492
|
+
normalizeBehaviorAutomationLevel(pickString(metadata, ["automation_level", "automationLevel"]));
|
|
493
|
+
const sliceScopePreference = normalizeSliceScopePreference(pickString(record, [
|
|
494
|
+
"slice_scope_preference",
|
|
495
|
+
"sliceScopePreference",
|
|
496
|
+
"scope_preference",
|
|
497
|
+
"scopePreference",
|
|
498
|
+
])) ??
|
|
499
|
+
normalizeSliceScopePreference(pickString(metadata, [
|
|
500
|
+
"slice_scope_preference",
|
|
501
|
+
"sliceScopePreference",
|
|
502
|
+
"scope_preference",
|
|
503
|
+
"scopePreference",
|
|
504
|
+
]));
|
|
505
|
+
const maxSliceTasks = toPositiveInteger(pickNumber(record, ["max_slice_tasks", "maxSliceTasks", "slice_max_tasks", "sliceMaxTasks"])) ??
|
|
506
|
+
toPositiveInteger(pickNumber(metadata, ["max_slice_tasks", "maxSliceTasks", "slice_max_tasks", "sliceMaxTasks"]));
|
|
507
|
+
const maxParallelAgents = toPositiveInteger(pickNumber(record, [
|
|
508
|
+
"max_parallel_agents",
|
|
509
|
+
"maxParallelAgents",
|
|
510
|
+
"parallel_agents_max",
|
|
511
|
+
"parallelAgentsMax",
|
|
512
|
+
])) ??
|
|
513
|
+
toPositiveInteger(pickNumber(metadata, [
|
|
514
|
+
"max_parallel_agents",
|
|
515
|
+
"maxParallelAgents",
|
|
516
|
+
"parallel_agents_max",
|
|
517
|
+
"parallelAgentsMax",
|
|
518
|
+
]));
|
|
519
|
+
const dependencyMode = normalizeDependencyMode(pickString(record, ["dependency_mode", "dependencyMode", "slice_dependency_mode", "sliceDependencyMode"])) ??
|
|
520
|
+
normalizeDependencyMode(pickString(metadata, [
|
|
521
|
+
"dependency_mode",
|
|
522
|
+
"dependencyMode",
|
|
523
|
+
"slice_dependency_mode",
|
|
524
|
+
"sliceDependencyMode",
|
|
525
|
+
]));
|
|
526
|
+
return {
|
|
527
|
+
id: String(record.id ?? ""),
|
|
528
|
+
type,
|
|
529
|
+
title: pickString(record, ["title", "name"]) ??
|
|
530
|
+
`${type[0].toUpperCase()}${type.slice(1)} ${String(record.id ?? "")}`,
|
|
531
|
+
status,
|
|
532
|
+
parentId: parentId ?? null,
|
|
533
|
+
initiativeId: initiativeId ?? null,
|
|
534
|
+
workstreamId: workstreamId ?? null,
|
|
535
|
+
milestoneId: milestoneId ?? null,
|
|
536
|
+
priorityNum: priority.priorityNum,
|
|
537
|
+
priorityLabel: priority.priorityLabel,
|
|
538
|
+
dependencyIds: normalizeDependencies(record),
|
|
539
|
+
dueDate,
|
|
540
|
+
etaEndAt,
|
|
541
|
+
expectedDurationHours: expectedDuration > 0 ? expectedDuration : DEFAULT_DURATION_HOURS[type],
|
|
542
|
+
expectedBudgetUsd: expectedBudget >= 0 ? expectedBudget : DEFAULT_BUDGET_USD[type],
|
|
543
|
+
assignedAgents: normalizeAssignedAgents(record),
|
|
544
|
+
behaviorConfigId: behaviorConfigId ?? null,
|
|
545
|
+
behaviorConfigVersion: behaviorConfigVersion ?? null,
|
|
546
|
+
behaviorConfigHash: behaviorConfigHash ?? null,
|
|
547
|
+
behaviorPolicySource: behaviorPolicySource ?? null,
|
|
548
|
+
behaviorContext: behaviorContext ?? null,
|
|
549
|
+
behaviorRequiresApproval,
|
|
550
|
+
behaviorApprovalStatus: behaviorApprovalStatus ?? null,
|
|
551
|
+
behaviorApprovalDecisionId: behaviorApprovalDecisionId ?? null,
|
|
552
|
+
behaviorAutomationLevel: behaviorAutomationLevel ?? null,
|
|
553
|
+
sliceScopePreference: sliceScopePreference ?? null,
|
|
554
|
+
maxSliceTasks: maxSliceTasks ?? null,
|
|
555
|
+
maxParallelAgents: maxParallelAgents ?? null,
|
|
556
|
+
dependencyMode: dependencyMode ?? null,
|
|
557
|
+
updatedAt: toIsoString(pickString(record, [
|
|
558
|
+
"updated_at",
|
|
559
|
+
"updatedAt",
|
|
560
|
+
"created_at",
|
|
561
|
+
"createdAt",
|
|
562
|
+
])),
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
export function isTodoStatus(status) {
|
|
566
|
+
const normalized = status.toLowerCase();
|
|
567
|
+
return (normalized === "todo" ||
|
|
568
|
+
normalized === "not_started" ||
|
|
569
|
+
normalized === "planned" ||
|
|
570
|
+
normalized === "backlog" ||
|
|
571
|
+
normalized === "pending");
|
|
572
|
+
}
|
|
573
|
+
export function isInProgressStatus(status) {
|
|
574
|
+
const normalized = status.toLowerCase();
|
|
575
|
+
return (normalized === "in_progress" ||
|
|
576
|
+
normalized === "active" ||
|
|
577
|
+
normalized === "running" ||
|
|
578
|
+
normalized === "queued");
|
|
579
|
+
}
|
|
580
|
+
export function isDispatchableWorkstreamStatus(status) {
|
|
581
|
+
const normalized = status.toLowerCase();
|
|
582
|
+
if (!normalized)
|
|
583
|
+
return true;
|
|
584
|
+
return !(normalized === "blocked" ||
|
|
585
|
+
normalized === "done" ||
|
|
586
|
+
normalized === "completed" ||
|
|
587
|
+
normalized === "cancelled" ||
|
|
588
|
+
normalized === "archived" ||
|
|
589
|
+
normalized === "deleted");
|
|
590
|
+
}
|
|
591
|
+
export function isDoneStatus(status) {
|
|
592
|
+
const normalized = status.toLowerCase();
|
|
593
|
+
return (normalized === "done" ||
|
|
594
|
+
normalized === "completed" ||
|
|
595
|
+
normalized === "cancelled" ||
|
|
596
|
+
normalized === "archived" ||
|
|
597
|
+
normalized === "deleted");
|
|
598
|
+
}
|
|
599
|
+
function detectCycleEdgeKeys(edges) {
|
|
600
|
+
const adjacency = new Map();
|
|
601
|
+
for (const edge of edges) {
|
|
602
|
+
const list = adjacency.get(edge.from) ?? [];
|
|
603
|
+
list.push(edge.to);
|
|
604
|
+
adjacency.set(edge.from, list);
|
|
605
|
+
}
|
|
606
|
+
const visiting = new Set();
|
|
607
|
+
const visited = new Set();
|
|
608
|
+
const cycleEdgeKeys = new Set();
|
|
609
|
+
function dfs(nodeId) {
|
|
610
|
+
if (visited.has(nodeId))
|
|
611
|
+
return;
|
|
612
|
+
visiting.add(nodeId);
|
|
613
|
+
const next = adjacency.get(nodeId) ?? [];
|
|
614
|
+
for (const childId of next) {
|
|
615
|
+
if (visiting.has(childId)) {
|
|
616
|
+
cycleEdgeKeys.add(`${nodeId}->${childId}`);
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
dfs(childId);
|
|
620
|
+
}
|
|
621
|
+
visiting.delete(nodeId);
|
|
622
|
+
visited.add(nodeId);
|
|
623
|
+
}
|
|
624
|
+
for (const key of adjacency.keys()) {
|
|
625
|
+
if (!visited.has(key))
|
|
626
|
+
dfs(key);
|
|
627
|
+
}
|
|
628
|
+
return cycleEdgeKeys;
|
|
629
|
+
}
|
|
630
|
+
export async function listEntitiesSafe(client, type, filters) {
|
|
631
|
+
try {
|
|
632
|
+
const response = await client.listEntities(type, filters);
|
|
633
|
+
const items = Array.isArray(response.data) ? response.data : [];
|
|
634
|
+
return { items, warning: null };
|
|
635
|
+
}
|
|
636
|
+
catch (err) {
|
|
637
|
+
return {
|
|
638
|
+
items: [],
|
|
639
|
+
warning: `${type} unavailable (${safeErrorMessage(err)})`,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
export async function buildMissionControlGraph(client, initiativeId, options) {
|
|
644
|
+
const degraded = [];
|
|
645
|
+
const preloadedInitiative = options?.initiativeEntity ?? null;
|
|
646
|
+
const [initiativeResult, workstreamResult, milestoneResult, taskResult] = await Promise.all([
|
|
647
|
+
preloadedInitiative
|
|
648
|
+
? Promise.resolve({
|
|
649
|
+
items: [preloadedInitiative],
|
|
650
|
+
warning: null,
|
|
651
|
+
})
|
|
652
|
+
: listEntitiesSafe(client, "initiative", { limit: 300 }),
|
|
653
|
+
listEntitiesSafe(client, "workstream", {
|
|
654
|
+
initiative_id: initiativeId,
|
|
655
|
+
limit: 500,
|
|
656
|
+
}),
|
|
657
|
+
listEntitiesSafe(client, "milestone", {
|
|
658
|
+
initiative_id: initiativeId,
|
|
659
|
+
limit: 700,
|
|
660
|
+
}),
|
|
661
|
+
listEntitiesSafe(client, "task", {
|
|
662
|
+
initiative_id: initiativeId,
|
|
663
|
+
limit: 1200,
|
|
664
|
+
}),
|
|
665
|
+
]);
|
|
666
|
+
for (const warning of [
|
|
667
|
+
initiativeResult.warning,
|
|
668
|
+
workstreamResult.warning,
|
|
669
|
+
milestoneResult.warning,
|
|
670
|
+
taskResult.warning,
|
|
671
|
+
]) {
|
|
672
|
+
if (warning)
|
|
673
|
+
degraded.push(warning);
|
|
674
|
+
}
|
|
675
|
+
const initiativeEntity = initiativeResult.items.find((item) => String(item.id ?? "") === initiativeId);
|
|
676
|
+
const initiativeNode = initiativeEntity
|
|
677
|
+
? toMissionControlNode("initiative", initiativeEntity, initiativeId)
|
|
678
|
+
: {
|
|
679
|
+
id: initiativeId,
|
|
680
|
+
type: "initiative",
|
|
681
|
+
title: `Initiative ${initiativeId.slice(0, 8)}`,
|
|
682
|
+
status: "active",
|
|
683
|
+
parentId: null,
|
|
684
|
+
initiativeId,
|
|
685
|
+
workstreamId: null,
|
|
686
|
+
milestoneId: null,
|
|
687
|
+
priorityNum: 60,
|
|
688
|
+
priorityLabel: null,
|
|
689
|
+
dependencyIds: [],
|
|
690
|
+
dueDate: null,
|
|
691
|
+
etaEndAt: null,
|
|
692
|
+
expectedDurationHours: DEFAULT_DURATION_HOURS.initiative,
|
|
693
|
+
expectedBudgetUsd: DEFAULT_BUDGET_USD.initiative,
|
|
694
|
+
assignedAgents: [],
|
|
695
|
+
updatedAt: null,
|
|
696
|
+
};
|
|
697
|
+
const workstreamNodes = workstreamResult.items.map((item) => toMissionControlNode("workstream", item, initiativeId));
|
|
698
|
+
const milestoneNodes = milestoneResult.items.map((item) => toMissionControlNode("milestone", item, initiativeId));
|
|
699
|
+
const taskNodes = taskResult.items.map((item) => toMissionControlNode("task", item, initiativeId));
|
|
700
|
+
const nodes = [
|
|
701
|
+
initiativeNode,
|
|
702
|
+
...workstreamNodes,
|
|
703
|
+
...milestoneNodes,
|
|
704
|
+
...taskNodes,
|
|
705
|
+
];
|
|
706
|
+
const nodeMap = new Map(nodes.map((node) => [node.id, node]));
|
|
707
|
+
for (const node of nodes) {
|
|
708
|
+
const validDependencies = dedupeStrings(node.dependencyIds.filter((depId) => depId !== node.id && nodeMap.has(depId)));
|
|
709
|
+
node.dependencyIds = validDependencies;
|
|
710
|
+
}
|
|
711
|
+
let edges = [];
|
|
712
|
+
for (const node of nodes) {
|
|
713
|
+
if (node.type === "initiative")
|
|
714
|
+
continue;
|
|
715
|
+
for (const depId of node.dependencyIds) {
|
|
716
|
+
edges.push({
|
|
717
|
+
from: depId,
|
|
718
|
+
to: node.id,
|
|
719
|
+
kind: "depends_on",
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
edges = edges.filter((edge, index, arr) => arr.findIndex((candidate) => candidate.from === edge.from &&
|
|
724
|
+
candidate.to === edge.to &&
|
|
725
|
+
candidate.kind === edge.kind) === index);
|
|
726
|
+
const cyclicEdgeKeys = detectCycleEdgeKeys(edges);
|
|
727
|
+
const cycleDiagnostics = cyclicEdgeKeys.size > 0
|
|
728
|
+
? {
|
|
729
|
+
detected: true,
|
|
730
|
+
cycleEdgeCount: cyclicEdgeKeys.size,
|
|
731
|
+
removedEdges: [],
|
|
732
|
+
affectedNodes: [],
|
|
733
|
+
}
|
|
734
|
+
: null;
|
|
735
|
+
if (cyclicEdgeKeys.size > 0) {
|
|
736
|
+
degraded.push(`Detected ${cyclicEdgeKeys.size} cyclic dependency edge(s); excluded from ETA graph.`);
|
|
737
|
+
if (cycleDiagnostics) {
|
|
738
|
+
const removedEdges = Array.from(cyclicEdgeKeys.values())
|
|
739
|
+
.map((key) => {
|
|
740
|
+
const [from, to] = key.split("->", 2);
|
|
741
|
+
if (!from || !to)
|
|
742
|
+
return null;
|
|
743
|
+
return { from, to };
|
|
744
|
+
})
|
|
745
|
+
.filter((entry) => Boolean(entry));
|
|
746
|
+
cycleDiagnostics.removedEdges = removedEdges;
|
|
747
|
+
}
|
|
748
|
+
edges = edges.filter((edge) => !cyclicEdgeKeys.has(`${edge.from}->${edge.to}`));
|
|
749
|
+
const affectedNodesById = new Map();
|
|
750
|
+
for (const node of nodes) {
|
|
751
|
+
const removed = node.dependencyIds.filter((depId) => cyclicEdgeKeys.has(`${depId}->${node.id}`));
|
|
752
|
+
if (removed.length > 0) {
|
|
753
|
+
const existing = affectedNodesById.get(node.id) ?? {
|
|
754
|
+
nodeId: node.id,
|
|
755
|
+
nodeType: node.type,
|
|
756
|
+
removedDependencyIds: new Set(),
|
|
757
|
+
};
|
|
758
|
+
for (const depId of removed) {
|
|
759
|
+
existing.removedDependencyIds.add(depId);
|
|
760
|
+
}
|
|
761
|
+
affectedNodesById.set(node.id, existing);
|
|
762
|
+
}
|
|
763
|
+
node.dependencyIds = node.dependencyIds.filter((depId) => !removed.includes(depId));
|
|
764
|
+
}
|
|
765
|
+
if (cycleDiagnostics) {
|
|
766
|
+
cycleDiagnostics.affectedNodes = Array.from(affectedNodesById.values()).map((entry) => {
|
|
767
|
+
const remaining = nodeMap.get(entry.nodeId)?.dependencyIds ?? [];
|
|
768
|
+
return {
|
|
769
|
+
nodeId: entry.nodeId,
|
|
770
|
+
nodeType: entry.nodeType,
|
|
771
|
+
removedDependencyIds: Array.from(entry.removedDependencyIds.values()),
|
|
772
|
+
remainingDependencyIds: [...remaining],
|
|
773
|
+
};
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
const etaMemo = new Map();
|
|
778
|
+
const etaVisiting = new Set();
|
|
779
|
+
const computeEtaEpoch = (nodeId) => {
|
|
780
|
+
const node = nodeMap.get(nodeId);
|
|
781
|
+
if (!node)
|
|
782
|
+
return Date.now();
|
|
783
|
+
const cached = etaMemo.get(nodeId);
|
|
784
|
+
if (cached !== undefined)
|
|
785
|
+
return cached;
|
|
786
|
+
const parsedEtaOverride = node.etaEndAt ? Date.parse(node.etaEndAt) : Number.NaN;
|
|
787
|
+
if (Number.isFinite(parsedEtaOverride)) {
|
|
788
|
+
etaMemo.set(nodeId, parsedEtaOverride);
|
|
789
|
+
return parsedEtaOverride;
|
|
790
|
+
}
|
|
791
|
+
const parsedDueDate = node.dueDate ? Date.parse(node.dueDate) : Number.NaN;
|
|
792
|
+
if (Number.isFinite(parsedDueDate)) {
|
|
793
|
+
etaMemo.set(nodeId, parsedDueDate);
|
|
794
|
+
return parsedDueDate;
|
|
795
|
+
}
|
|
796
|
+
if (etaVisiting.has(nodeId)) {
|
|
797
|
+
degraded.push(`ETA cycle fallback on node ${nodeId}.`);
|
|
798
|
+
const fallback = Date.now();
|
|
799
|
+
etaMemo.set(nodeId, fallback);
|
|
800
|
+
return fallback;
|
|
801
|
+
}
|
|
802
|
+
etaVisiting.add(nodeId);
|
|
803
|
+
let dependencyMax = 0;
|
|
804
|
+
for (const depId of node.dependencyIds) {
|
|
805
|
+
dependencyMax = Math.max(dependencyMax, computeEtaEpoch(depId));
|
|
806
|
+
}
|
|
807
|
+
etaVisiting.delete(nodeId);
|
|
808
|
+
const durationMs = (node.expectedDurationHours > 0
|
|
809
|
+
? node.expectedDurationHours
|
|
810
|
+
: DEFAULT_DURATION_HOURS[node.type]) * 60 * 60 * 1000;
|
|
811
|
+
const eta = Math.max(Date.now(), dependencyMax) + durationMs;
|
|
812
|
+
etaMemo.set(nodeId, eta);
|
|
813
|
+
return eta;
|
|
814
|
+
};
|
|
815
|
+
for (const node of nodes) {
|
|
816
|
+
const eta = computeEtaEpoch(node.id);
|
|
817
|
+
if (Number.isFinite(eta)) {
|
|
818
|
+
node.etaEndAt = new Date(eta).toISOString();
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
const taskNodesOnly = nodes.filter((node) => node.type === "task");
|
|
822
|
+
const hasActiveTasks = taskNodesOnly.some((node) => isInProgressStatus(node.status));
|
|
823
|
+
const hasTodoTasks = taskNodesOnly.some((node) => isTodoStatus(node.status));
|
|
824
|
+
if (initiativeNode.status.toLowerCase() === "active" &&
|
|
825
|
+
!hasActiveTasks &&
|
|
826
|
+
hasTodoTasks) {
|
|
827
|
+
initiativeNode.status = "paused";
|
|
828
|
+
}
|
|
829
|
+
const nodeById = new Map(nodes.map((node) => [node.id, node]));
|
|
830
|
+
const taskIsReady = (task) => task.dependencyIds.every((depId) => {
|
|
831
|
+
const dependency = nodeById.get(depId);
|
|
832
|
+
return dependency ? isDoneStatus(dependency.status) : true;
|
|
833
|
+
});
|
|
834
|
+
const taskHasBlockedParent = (task) => {
|
|
835
|
+
const milestone = task.milestoneId ? nodeById.get(task.milestoneId) ?? null : null;
|
|
836
|
+
const workstream = task.workstreamId ? nodeById.get(task.workstreamId) ?? null : null;
|
|
837
|
+
return (milestone?.status?.toLowerCase() === "blocked" ||
|
|
838
|
+
workstream?.status?.toLowerCase() === "blocked");
|
|
839
|
+
};
|
|
840
|
+
const recentTodos = nodes
|
|
841
|
+
.filter((node) => node.type === "task" && isTodoStatus(node.status))
|
|
842
|
+
.sort((a, b) => {
|
|
843
|
+
const aReady = taskIsReady(a);
|
|
844
|
+
const bReady = taskIsReady(b);
|
|
845
|
+
if (aReady !== bReady)
|
|
846
|
+
return aReady ? -1 : 1;
|
|
847
|
+
const aBlocked = taskHasBlockedParent(a);
|
|
848
|
+
const bBlocked = taskHasBlockedParent(b);
|
|
849
|
+
if (aBlocked !== bBlocked)
|
|
850
|
+
return aBlocked ? 1 : -1;
|
|
851
|
+
const priorityDelta = a.priorityNum - b.priorityNum;
|
|
852
|
+
if (priorityDelta !== 0)
|
|
853
|
+
return priorityDelta;
|
|
854
|
+
const aDue = a.dueDate ? Date.parse(a.dueDate) : Number.POSITIVE_INFINITY;
|
|
855
|
+
const bDue = b.dueDate ? Date.parse(b.dueDate) : Number.POSITIVE_INFINITY;
|
|
856
|
+
if (aDue !== bDue)
|
|
857
|
+
return aDue - bDue;
|
|
858
|
+
const aEta = a.etaEndAt ? Date.parse(a.etaEndAt) : Number.POSITIVE_INFINITY;
|
|
859
|
+
const bEta = b.etaEndAt ? Date.parse(b.etaEndAt) : Number.POSITIVE_INFINITY;
|
|
860
|
+
if (aEta !== bEta)
|
|
861
|
+
return aEta - bEta;
|
|
862
|
+
const aEpoch = a.updatedAt ? Date.parse(a.updatedAt) : 0;
|
|
863
|
+
const bEpoch = b.updatedAt ? Date.parse(b.updatedAt) : 0;
|
|
864
|
+
return aEpoch - bEpoch;
|
|
865
|
+
})
|
|
866
|
+
.map((node) => node.id);
|
|
867
|
+
return {
|
|
868
|
+
initiative: {
|
|
869
|
+
id: initiativeNode.id,
|
|
870
|
+
title: initiativeNode.title,
|
|
871
|
+
status: initiativeNode.status,
|
|
872
|
+
summary: initiativeEntity
|
|
873
|
+
? pickString(initiativeEntity, [
|
|
874
|
+
"summary",
|
|
875
|
+
"description",
|
|
876
|
+
"context",
|
|
877
|
+
])
|
|
878
|
+
: null,
|
|
879
|
+
assignedAgents: initiativeNode.assignedAgents,
|
|
880
|
+
},
|
|
881
|
+
nodes,
|
|
882
|
+
edges,
|
|
883
|
+
recentTodos,
|
|
884
|
+
degraded,
|
|
885
|
+
...(cycleDiagnostics ? { cycleDiagnostics } : {}),
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
export function normalizeEntityMutationPayload(payload) {
|
|
889
|
+
const next = { ...payload };
|
|
890
|
+
const priorityNumRaw = pickNumber(next, ["priority_num", "priorityNum"]);
|
|
891
|
+
const priorityLabelRaw = pickString(next, ["priority", "priority_label"]);
|
|
892
|
+
if (priorityNumRaw !== null) {
|
|
893
|
+
const clamped = clampPriority(priorityNumRaw);
|
|
894
|
+
next.priority_num = clamped;
|
|
895
|
+
if (!priorityLabelRaw) {
|
|
896
|
+
next.priority = mapPriorityNumToLabel(clamped);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
else if (priorityLabelRaw) {
|
|
900
|
+
next.priority_num = PRIORITY_LABEL_TO_NUM[priorityLabelRaw.toLowerCase()] ?? 60;
|
|
901
|
+
next.priority = priorityLabelRaw.toLowerCase();
|
|
902
|
+
}
|
|
903
|
+
const dependsOnArray = pickStringArray(next, ["depends_on", "dependsOn", "dependencies"]);
|
|
904
|
+
if (dependsOnArray.length > 0) {
|
|
905
|
+
next.depends_on = dedupeStrings(dependsOnArray);
|
|
906
|
+
}
|
|
907
|
+
else if ("depends_on" in next) {
|
|
908
|
+
next.depends_on = [];
|
|
909
|
+
}
|
|
910
|
+
const expectedDuration = pickNumber(next, [
|
|
911
|
+
"expected_duration_hours",
|
|
912
|
+
"expectedDurationHours",
|
|
913
|
+
]);
|
|
914
|
+
if (expectedDuration !== null) {
|
|
915
|
+
next.expected_duration_hours = Math.max(0, expectedDuration);
|
|
916
|
+
}
|
|
917
|
+
const expectedBudget = pickNumber(next, [
|
|
918
|
+
"expected_budget_usd",
|
|
919
|
+
"expectedBudgetUsd",
|
|
920
|
+
"budget_usd",
|
|
921
|
+
"budgetUsd",
|
|
922
|
+
]);
|
|
923
|
+
if (expectedBudget !== null) {
|
|
924
|
+
next.expected_budget_usd = Math.max(0, expectedBudget);
|
|
925
|
+
}
|
|
926
|
+
const etaEndAt = pickString(next, ["eta_end_at", "etaEndAt"]);
|
|
927
|
+
if (etaEndAt !== null) {
|
|
928
|
+
next.eta_end_at = toIsoString(etaEndAt) ?? null;
|
|
929
|
+
}
|
|
930
|
+
const assignedIds = pickStringArray(next, [
|
|
931
|
+
"assigned_agent_ids",
|
|
932
|
+
"assignedAgentIds",
|
|
933
|
+
]);
|
|
934
|
+
const assignedNames = pickStringArray(next, [
|
|
935
|
+
"assigned_agent_names",
|
|
936
|
+
"assignedAgentNames",
|
|
937
|
+
]);
|
|
938
|
+
if (assignedIds.length > 0) {
|
|
939
|
+
next.assigned_agent_ids = dedupeStrings(assignedIds);
|
|
940
|
+
}
|
|
941
|
+
if (assignedNames.length > 0) {
|
|
942
|
+
next.assigned_agent_names = dedupeStrings(assignedNames);
|
|
943
|
+
}
|
|
944
|
+
return next;
|
|
945
|
+
}
|
|
946
|
+
export async function resolveAutoAssignments(input) {
|
|
947
|
+
const warnings = [];
|
|
948
|
+
const assignedById = new Map();
|
|
949
|
+
const addAgent = (agent) => {
|
|
950
|
+
const key = agent.id || `name:${agent.name}`;
|
|
951
|
+
if (!assignedById.has(key))
|
|
952
|
+
assignedById.set(key, agent);
|
|
953
|
+
};
|
|
954
|
+
let liveAgents = [];
|
|
955
|
+
try {
|
|
956
|
+
const data = await input.client.getLiveAgents({
|
|
957
|
+
initiative: input.initiativeId,
|
|
958
|
+
includeIdle: true,
|
|
959
|
+
});
|
|
960
|
+
liveAgents = (Array.isArray(data.agents) ? data.agents : [])
|
|
961
|
+
.map((raw) => {
|
|
962
|
+
if (!raw || typeof raw !== "object")
|
|
963
|
+
return null;
|
|
964
|
+
const record = raw;
|
|
965
|
+
const id = pickString(record, ["id", "agentId"]) ?? "";
|
|
966
|
+
const name = pickString(record, ["name", "agentName"]) ?? (id ? `Agent ${id}` : "");
|
|
967
|
+
if (!name)
|
|
968
|
+
return null;
|
|
969
|
+
return {
|
|
970
|
+
id: id || `name:${name}`,
|
|
971
|
+
name,
|
|
972
|
+
domain: pickString(record, ["domain", "role"]),
|
|
973
|
+
status: pickString(record, ["status"]),
|
|
974
|
+
};
|
|
975
|
+
})
|
|
976
|
+
.filter((item) => item !== null);
|
|
977
|
+
}
|
|
978
|
+
catch (err) {
|
|
979
|
+
warnings.push(`live agent lookup failed (${safeErrorMessage(err)})`);
|
|
980
|
+
}
|
|
981
|
+
const orchestrator = liveAgents.find((agent) => /holt|orchestrator/i.test(agent.name) ||
|
|
982
|
+
/orchestrator/i.test(agent.domain ?? ""));
|
|
983
|
+
if (orchestrator)
|
|
984
|
+
addAgent(orchestrator);
|
|
985
|
+
let assignmentSource = "fallback";
|
|
986
|
+
try {
|
|
987
|
+
const preflight = await input.client.delegationPreflight({
|
|
988
|
+
intent: `${input.title}${input.summary ? `: ${input.summary}` : ""}`,
|
|
989
|
+
});
|
|
990
|
+
const recommendations = preflight.data?.recommended_split ?? [];
|
|
991
|
+
const recommendedDomains = dedupeStrings(recommendations
|
|
992
|
+
.map((entry) => String(entry.owner_domain ?? "").trim().toLowerCase())
|
|
993
|
+
.filter(Boolean));
|
|
994
|
+
for (const domain of recommendedDomains) {
|
|
995
|
+
const matched = liveAgents.find((agent) => (agent.domain ?? "").toLowerCase().includes(domain));
|
|
996
|
+
if (matched)
|
|
997
|
+
addAgent(matched);
|
|
998
|
+
}
|
|
999
|
+
if (recommendedDomains.length > 0) {
|
|
1000
|
+
assignmentSource = "orchestrator";
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
catch (err) {
|
|
1004
|
+
warnings.push(`delegation preflight failed (${safeErrorMessage(err)})`);
|
|
1005
|
+
}
|
|
1006
|
+
if (assignedById.size === 0) {
|
|
1007
|
+
const text = `${input.title} ${input.summary ?? ""}`.toLowerCase();
|
|
1008
|
+
const fallbackDomains = [];
|
|
1009
|
+
if (/market|campaign|thread|article|tweet|copy/.test(text)) {
|
|
1010
|
+
fallbackDomains.push("marketing");
|
|
1011
|
+
}
|
|
1012
|
+
else if (/design|ux|ui|a11y|accessibility/.test(text)) {
|
|
1013
|
+
fallbackDomains.push("design");
|
|
1014
|
+
}
|
|
1015
|
+
else if (/ops|incident|runbook|reliability/.test(text)) {
|
|
1016
|
+
fallbackDomains.push("operations");
|
|
1017
|
+
}
|
|
1018
|
+
else if (/sales|deal|pipeline|mrr/.test(text)) {
|
|
1019
|
+
fallbackDomains.push("sales");
|
|
1020
|
+
}
|
|
1021
|
+
else {
|
|
1022
|
+
fallbackDomains.push("engineering", "product");
|
|
1023
|
+
}
|
|
1024
|
+
for (const domain of fallbackDomains) {
|
|
1025
|
+
const matched = liveAgents.find((agent) => (agent.domain ?? "").toLowerCase().includes(domain));
|
|
1026
|
+
if (matched)
|
|
1027
|
+
addAgent(matched);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
if (assignedById.size === 0 && liveAgents.length > 0) {
|
|
1031
|
+
addAgent(liveAgents[0]);
|
|
1032
|
+
warnings.push("using first available live agent as fallback");
|
|
1033
|
+
}
|
|
1034
|
+
const assignedAgents = Array.from(assignedById.values());
|
|
1035
|
+
const updatePayload = normalizeEntityMutationPayload({
|
|
1036
|
+
assigned_agent_ids: assignedAgents.map((agent) => agent.id),
|
|
1037
|
+
assigned_agent_names: assignedAgents.map((agent) => agent.name),
|
|
1038
|
+
});
|
|
1039
|
+
let updatedEntity;
|
|
1040
|
+
try {
|
|
1041
|
+
updatedEntity = await input.client.updateEntity(input.entityType, input.entityId, updatePayload);
|
|
1042
|
+
}
|
|
1043
|
+
catch (err) {
|
|
1044
|
+
warnings.push(`assignment patch failed (${safeErrorMessage(err)})`);
|
|
1045
|
+
}
|
|
1046
|
+
return {
|
|
1047
|
+
ok: true,
|
|
1048
|
+
assignment_source: assignmentSource,
|
|
1049
|
+
assigned_agents: assignedAgents,
|
|
1050
|
+
warnings,
|
|
1051
|
+
...(updatedEntity ? { updated_entity: updatedEntity } : {}),
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
export function normalizeExecutionDomain(value) {
|
|
1055
|
+
const raw = (value ?? "").trim().toLowerCase();
|
|
1056
|
+
if (!raw)
|
|
1057
|
+
return null;
|
|
1058
|
+
if (raw === "orchestrator")
|
|
1059
|
+
return "orchestration";
|
|
1060
|
+
if (raw === "ops")
|
|
1061
|
+
return "operations";
|
|
1062
|
+
return Object.prototype.hasOwnProperty.call(ORGX_SKILL_BY_DOMAIN, raw)
|
|
1063
|
+
? raw
|
|
1064
|
+
: null;
|
|
1065
|
+
}
|
|
1066
|
+
export function inferExecutionDomainFromText(...values) {
|
|
1067
|
+
const text = values
|
|
1068
|
+
.map((value) => (value ?? "").trim().toLowerCase())
|
|
1069
|
+
.filter((value) => value.length > 0)
|
|
1070
|
+
.join(" ");
|
|
1071
|
+
if (!text)
|
|
1072
|
+
return "engineering";
|
|
1073
|
+
if (/\b(marketing|campaign|copy|ad|content)\b/.test(text))
|
|
1074
|
+
return "marketing";
|
|
1075
|
+
if (/\b(sales|meddic|pipeline|deal|outreach)\b/.test(text))
|
|
1076
|
+
return "sales";
|
|
1077
|
+
if (/\b(design|ui|ux|brand|wcag)\b/.test(text))
|
|
1078
|
+
return "design";
|
|
1079
|
+
if (/\b(product|prd|roadmap|prioritization)\b/.test(text))
|
|
1080
|
+
return "product";
|
|
1081
|
+
if (/\b(ops|operations|incident|reliability|oncall|slo)\b/.test(text))
|
|
1082
|
+
return "operations";
|
|
1083
|
+
if (/\b(orchestration|dispatch|handoff)\b/.test(text))
|
|
1084
|
+
return "orchestration";
|
|
1085
|
+
return "engineering";
|
|
1086
|
+
}
|
|
1087
|
+
// ---------------------------------------------------------------------------
|
|
1088
|
+
// Per-scope task selection for autopilot slices
|
|
1089
|
+
// ---------------------------------------------------------------------------
|
|
1090
|
+
export function selectSliceTasksByScope(input) {
|
|
1091
|
+
const { scope, workstreamId, nodeById, includeVerification } = input;
|
|
1092
|
+
const cap = SLICE_SCOPE_MAX_TASKS[scope];
|
|
1093
|
+
const taskIsReady = (node) => node.dependencyIds.every((depId) => {
|
|
1094
|
+
const dep = nodeById.get(depId);
|
|
1095
|
+
return dep ? isDoneStatus(dep.status) : true;
|
|
1096
|
+
});
|
|
1097
|
+
const taskHasBlockedParent = (node) => {
|
|
1098
|
+
const milestone = node.milestoneId ? nodeById.get(node.milestoneId) ?? null : null;
|
|
1099
|
+
const workstream = node.workstreamId ? nodeById.get(node.workstreamId) ?? null : null;
|
|
1100
|
+
return (milestone?.status?.toLowerCase() === "blocked" ||
|
|
1101
|
+
workstream?.status?.toLowerCase() === "blocked");
|
|
1102
|
+
};
|
|
1103
|
+
const isEligible = (node) => Boolean(node &&
|
|
1104
|
+
node.type === "task" &&
|
|
1105
|
+
isTodoStatus(node.status) &&
|
|
1106
|
+
taskIsReady(node) &&
|
|
1107
|
+
!taskHasBlockedParent(node) &&
|
|
1108
|
+
(includeVerification || !/^verification[ \t]+scenario/i.test(String(node.title ?? ""))));
|
|
1109
|
+
if (scope === "milestone") {
|
|
1110
|
+
// Pick tasks from a specific milestone (or the first milestone with ready tasks)
|
|
1111
|
+
const targetMilestoneId = input.milestoneId ?? null;
|
|
1112
|
+
if (targetMilestoneId) {
|
|
1113
|
+
const tasks = input.recentTodos
|
|
1114
|
+
.map((id) => nodeById.get(id))
|
|
1115
|
+
.filter((node) => Boolean(node) &&
|
|
1116
|
+
node.workstreamId === workstreamId &&
|
|
1117
|
+
node.milestoneId === targetMilestoneId &&
|
|
1118
|
+
isEligible(node))
|
|
1119
|
+
.slice(0, cap);
|
|
1120
|
+
return { tasks, milestoneIds: tasks.length > 0 ? [targetMilestoneId] : [] };
|
|
1121
|
+
}
|
|
1122
|
+
// Find first milestone with ready tasks
|
|
1123
|
+
for (const todoId of input.recentTodos) {
|
|
1124
|
+
const node = nodeById.get(todoId);
|
|
1125
|
+
if (!node || node.workstreamId !== workstreamId || !node.milestoneId || !isEligible(node))
|
|
1126
|
+
continue;
|
|
1127
|
+
const msId = node.milestoneId;
|
|
1128
|
+
const tasks = input.recentTodos
|
|
1129
|
+
.map((id) => nodeById.get(id))
|
|
1130
|
+
.filter((n) => Boolean(n) && n.workstreamId === workstreamId && n.milestoneId === msId && isEligible(n))
|
|
1131
|
+
.slice(0, cap);
|
|
1132
|
+
return { tasks, milestoneIds: [msId] };
|
|
1133
|
+
}
|
|
1134
|
+
return { tasks: [], milestoneIds: [] };
|
|
1135
|
+
}
|
|
1136
|
+
if (scope === "workstream") {
|
|
1137
|
+
const milestoneIdSet = new Set();
|
|
1138
|
+
const tasks = input.recentTodos
|
|
1139
|
+
.map((id) => nodeById.get(id))
|
|
1140
|
+
.filter((node) => Boolean(node) && node.workstreamId === workstreamId && isEligible(node))
|
|
1141
|
+
.slice(0, cap);
|
|
1142
|
+
for (const t of tasks) {
|
|
1143
|
+
if (t.milestoneId)
|
|
1144
|
+
milestoneIdSet.add(t.milestoneId);
|
|
1145
|
+
}
|
|
1146
|
+
return { tasks, milestoneIds: Array.from(milestoneIdSet) };
|
|
1147
|
+
}
|
|
1148
|
+
// Default: task scope — current behavior
|
|
1149
|
+
const tasks = input.recentTodos
|
|
1150
|
+
.map((id) => nodeById.get(id))
|
|
1151
|
+
.filter((node) => Boolean(node) && node.workstreamId === workstreamId && isEligible(node))
|
|
1152
|
+
.slice(0, cap);
|
|
1153
|
+
const milestoneIdSet = new Set();
|
|
1154
|
+
for (const t of tasks) {
|
|
1155
|
+
if (t.milestoneId)
|
|
1156
|
+
milestoneIdSet.add(t.milestoneId);
|
|
1157
|
+
}
|
|
1158
|
+
return { tasks, milestoneIds: Array.from(milestoneIdSet) };
|
|
1159
|
+
}
|
|
1160
|
+
// ---------------------------------------------------------------------------
|
|
1161
|
+
// Scope completion evaluation
|
|
1162
|
+
// ---------------------------------------------------------------------------
|
|
1163
|
+
export function evaluateScopeCompletion(input) {
|
|
1164
|
+
const { scope, milestoneIds, workstreamId, nodeById } = input;
|
|
1165
|
+
if (scope === "task") {
|
|
1166
|
+
return { scopeComplete: true, remainingTasks: 0 };
|
|
1167
|
+
}
|
|
1168
|
+
let remaining = 0;
|
|
1169
|
+
for (const [, node] of nodeById) {
|
|
1170
|
+
if (node.type !== "task")
|
|
1171
|
+
continue;
|
|
1172
|
+
if (isDoneStatus(node.status))
|
|
1173
|
+
continue;
|
|
1174
|
+
if (scope === "milestone") {
|
|
1175
|
+
if (node.milestoneId && milestoneIds.includes(node.milestoneId)) {
|
|
1176
|
+
remaining += 1;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
else if (scope === "workstream") {
|
|
1180
|
+
if (node.workstreamId === workstreamId) {
|
|
1181
|
+
remaining += 1;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
return { scopeComplete: remaining === 0, remainingTasks: remaining };
|
|
1186
|
+
}
|
|
1187
|
+
export function deriveExecutionPolicy(taskNode, workstreamNode) {
|
|
1188
|
+
const domainCandidate = taskNode.assignedAgents
|
|
1189
|
+
.map((agent) => normalizeExecutionDomain(agent.domain))
|
|
1190
|
+
.find((domain) => Boolean(domain)) ??
|
|
1191
|
+
(workstreamNode
|
|
1192
|
+
? workstreamNode.assignedAgents
|
|
1193
|
+
.map((agent) => normalizeExecutionDomain(agent.domain))
|
|
1194
|
+
.find((domain) => Boolean(domain))
|
|
1195
|
+
: null) ??
|
|
1196
|
+
inferExecutionDomainFromText(taskNode.title, workstreamNode?.title ?? null);
|
|
1197
|
+
const domain = normalizeExecutionDomain(domainCandidate) ?? "engineering";
|
|
1198
|
+
const requiredSkill = ORGX_SKILL_BY_DOMAIN[domain] ?? ORGX_SKILL_BY_DOMAIN.engineering;
|
|
1199
|
+
const profile = taskNode.behaviorConfigId ??
|
|
1200
|
+
workstreamNode?.behaviorConfigId ??
|
|
1201
|
+
null;
|
|
1202
|
+
const sliceScopePreference = taskNode.sliceScopePreference ??
|
|
1203
|
+
workstreamNode?.sliceScopePreference ??
|
|
1204
|
+
null;
|
|
1205
|
+
const maxSliceTasks = taskNode.maxSliceTasks ??
|
|
1206
|
+
workstreamNode?.maxSliceTasks ??
|
|
1207
|
+
null;
|
|
1208
|
+
const maxParallelAgents = taskNode.maxParallelAgents ??
|
|
1209
|
+
workstreamNode?.maxParallelAgents ??
|
|
1210
|
+
null;
|
|
1211
|
+
const dependencyMode = taskNode.dependencyMode ??
|
|
1212
|
+
workstreamNode?.dependencyMode ??
|
|
1213
|
+
null;
|
|
1214
|
+
return {
|
|
1215
|
+
domain,
|
|
1216
|
+
requiredSkills: [requiredSkill],
|
|
1217
|
+
profile,
|
|
1218
|
+
sliceScopePreference,
|
|
1219
|
+
maxSliceTasks,
|
|
1220
|
+
maxParallelAgents,
|
|
1221
|
+
dependencyMode,
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
export function deriveBehaviorConfigContext(taskNode, workstreamNode) {
|
|
1225
|
+
const approvalStatusRaw = taskNode.behaviorApprovalStatus ?? workstreamNode?.behaviorApprovalStatus ?? null;
|
|
1226
|
+
const approvalStatus = approvalStatusRaw
|
|
1227
|
+
? approvalStatusRaw
|
|
1228
|
+
.trim()
|
|
1229
|
+
.toLowerCase()
|
|
1230
|
+
.replace(/[\s-]+/g, "_")
|
|
1231
|
+
: null;
|
|
1232
|
+
const explicitlyRequiresApproval = taskNode.behaviorRequiresApproval ?? workstreamNode?.behaviorRequiresApproval ?? null;
|
|
1233
|
+
const pendingApprovalStatus = approvalStatus === "pending" ||
|
|
1234
|
+
approvalStatus === "requested" ||
|
|
1235
|
+
approvalStatus === "awaiting_approval" ||
|
|
1236
|
+
approvalStatus === "awaiting_review" ||
|
|
1237
|
+
approvalStatus === "in_review" ||
|
|
1238
|
+
approvalStatus === "review_pending" ||
|
|
1239
|
+
approvalStatus === "needs_approval" ||
|
|
1240
|
+
approvalStatus === "open" ||
|
|
1241
|
+
approvalStatus === "queued";
|
|
1242
|
+
const approvedStatus = approvalStatus === "approved" ||
|
|
1243
|
+
approvalStatus === "accepted" ||
|
|
1244
|
+
approvalStatus === "resolved" ||
|
|
1245
|
+
approvalStatus === "completed" ||
|
|
1246
|
+
approvalStatus === "complete";
|
|
1247
|
+
const requiresApproval = explicitlyRequiresApproval === true ? !approvedStatus : pendingApprovalStatus;
|
|
1248
|
+
return {
|
|
1249
|
+
configId: taskNode.behaviorConfigId ?? workstreamNode?.behaviorConfigId ?? null,
|
|
1250
|
+
version: taskNode.behaviorConfigVersion ?? workstreamNode?.behaviorConfigVersion ?? null,
|
|
1251
|
+
hash: taskNode.behaviorConfigHash ?? workstreamNode?.behaviorConfigHash ?? null,
|
|
1252
|
+
policySource: taskNode.behaviorPolicySource ?? workstreamNode?.behaviorPolicySource ?? null,
|
|
1253
|
+
context: taskNode.behaviorContext ?? workstreamNode?.behaviorContext ?? null,
|
|
1254
|
+
requiresApproval,
|
|
1255
|
+
approvalStatus: approvalStatus ?? null,
|
|
1256
|
+
approvalDecisionId: taskNode.behaviorApprovalDecisionId ?? workstreamNode?.behaviorApprovalDecisionId ?? null,
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
export function deriveBehaviorAutomationLevel(taskNode, workstreamNode) {
|
|
1260
|
+
return taskNode.behaviorAutomationLevel ?? workstreamNode?.behaviorAutomationLevel ?? "auto";
|
|
1261
|
+
}
|
|
1262
|
+
export function detectBehaviorConfigDrift(input) {
|
|
1263
|
+
const workstream = input.workstreamNode;
|
|
1264
|
+
if (!workstream)
|
|
1265
|
+
return null;
|
|
1266
|
+
const declared = {
|
|
1267
|
+
configId: workstream.behaviorConfigId ?? null,
|
|
1268
|
+
version: workstream.behaviorConfigVersion ?? null,
|
|
1269
|
+
hash: workstream.behaviorConfigHash ?? null,
|
|
1270
|
+
policySource: workstream.behaviorPolicySource ?? null,
|
|
1271
|
+
context: workstream.behaviorContext ?? null,
|
|
1272
|
+
automationLevel: workstream.behaviorAutomationLevel ?? null,
|
|
1273
|
+
};
|
|
1274
|
+
const runtime = {
|
|
1275
|
+
configId: input.behaviorConfig.configId ?? null,
|
|
1276
|
+
version: input.behaviorConfig.version ?? null,
|
|
1277
|
+
hash: input.behaviorConfig.hash ?? null,
|
|
1278
|
+
policySource: input.behaviorConfig.policySource ?? null,
|
|
1279
|
+
context: input.behaviorConfig.context ?? null,
|
|
1280
|
+
automationLevel: input.behaviorAutomationLevel,
|
|
1281
|
+
};
|
|
1282
|
+
const norm = (value) => {
|
|
1283
|
+
if (typeof value !== "string")
|
|
1284
|
+
return null;
|
|
1285
|
+
const trimmed = value.trim();
|
|
1286
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
1287
|
+
};
|
|
1288
|
+
const normContext = (value) => {
|
|
1289
|
+
const normalized = norm(value);
|
|
1290
|
+
if (!normalized)
|
|
1291
|
+
return null;
|
|
1292
|
+
return normalized.replace(/\s+/g, " ");
|
|
1293
|
+
};
|
|
1294
|
+
const normPolicySource = (value) => {
|
|
1295
|
+
const normalized = norm(value);
|
|
1296
|
+
if (!normalized)
|
|
1297
|
+
return null;
|
|
1298
|
+
return normalized.toLowerCase().replace(/[\s-]+/g, "_");
|
|
1299
|
+
};
|
|
1300
|
+
const hasDeclaredConfig = norm(declared.configId) !== null ||
|
|
1301
|
+
norm(declared.version) !== null ||
|
|
1302
|
+
norm(declared.hash) !== null ||
|
|
1303
|
+
norm(declared.policySource) !== null ||
|
|
1304
|
+
normContext(declared.context) !== null ||
|
|
1305
|
+
declared.automationLevel !== null;
|
|
1306
|
+
if (!hasDeclaredConfig)
|
|
1307
|
+
return null;
|
|
1308
|
+
const fields = [];
|
|
1309
|
+
if (norm(declared.configId) !== norm(runtime.configId))
|
|
1310
|
+
fields.push("config_id");
|
|
1311
|
+
if (norm(declared.version) !== norm(runtime.version))
|
|
1312
|
+
fields.push("version");
|
|
1313
|
+
if (norm(declared.hash) !== norm(runtime.hash))
|
|
1314
|
+
fields.push("hash");
|
|
1315
|
+
if (normPolicySource(declared.policySource) !== normPolicySource(runtime.policySource)) {
|
|
1316
|
+
fields.push("policy_source");
|
|
1317
|
+
}
|
|
1318
|
+
if (normContext(declared.context) !== normContext(runtime.context))
|
|
1319
|
+
fields.push("context");
|
|
1320
|
+
if ((declared.automationLevel ?? null) !== runtime.automationLevel)
|
|
1321
|
+
fields.push("automation_level");
|
|
1322
|
+
if (fields.length === 0)
|
|
1323
|
+
return null;
|
|
1324
|
+
return { fields, declared, runtime };
|
|
1325
|
+
}
|
|
1326
|
+
export function spawnGuardIsRateLimited(result) {
|
|
1327
|
+
if (!result || typeof result !== "object")
|
|
1328
|
+
return false;
|
|
1329
|
+
const record = result;
|
|
1330
|
+
const checks = record.checks && typeof record.checks === "object" && !Array.isArray(record.checks)
|
|
1331
|
+
? record.checks
|
|
1332
|
+
: {};
|
|
1333
|
+
const rateLimitCandidates = [
|
|
1334
|
+
checks.rateLimit,
|
|
1335
|
+
checks.rate_limit,
|
|
1336
|
+
record.rateLimit,
|
|
1337
|
+
record.rate_limit,
|
|
1338
|
+
].filter((entry) => {
|
|
1339
|
+
return Boolean(entry && typeof entry === "object" && !Array.isArray(entry));
|
|
1340
|
+
});
|
|
1341
|
+
for (const candidate of rateLimitCandidates) {
|
|
1342
|
+
if (candidate.passed === false)
|
|
1343
|
+
return true;
|
|
1344
|
+
if (candidate.rateLimited === true || candidate.rate_limited === true || candidate.limited === true) {
|
|
1345
|
+
return true;
|
|
1346
|
+
}
|
|
1347
|
+
const current = pickNumber(candidate, ["current", "count", "value", "used", "attempts"]) ?? null;
|
|
1348
|
+
const max = pickNumber(candidate, ["max", "limit", "threshold", "allowed"]) ?? null;
|
|
1349
|
+
if (typeof current === "number" &&
|
|
1350
|
+
Number.isFinite(current) &&
|
|
1351
|
+
typeof max === "number" &&
|
|
1352
|
+
Number.isFinite(max) &&
|
|
1353
|
+
max > 0 &&
|
|
1354
|
+
current >= max) {
|
|
1355
|
+
return true;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
if (record.rateLimited === true || record.rate_limited === true)
|
|
1359
|
+
return true;
|
|
1360
|
+
const blockedReason = pickString(record, [
|
|
1361
|
+
"blockedReason",
|
|
1362
|
+
"blocked_reason",
|
|
1363
|
+
"reason",
|
|
1364
|
+
"message",
|
|
1365
|
+
"error",
|
|
1366
|
+
]);
|
|
1367
|
+
if (blockedReason && /\brate[ -]?limit(?:ed)?\b|\btoo many requests\b/i.test(blockedReason)) {
|
|
1368
|
+
return true;
|
|
1369
|
+
}
|
|
1370
|
+
return false;
|
|
1371
|
+
}
|
|
1372
|
+
export function summarizeSpawnGuardBlockReason(result) {
|
|
1373
|
+
if (!result || typeof result !== "object")
|
|
1374
|
+
return "Spawn guard denied dispatch.";
|
|
1375
|
+
const record = result;
|
|
1376
|
+
const blockedReason = pickString(record, ["blockedReason", "blocked_reason"]);
|
|
1377
|
+
if (blockedReason)
|
|
1378
|
+
return blockedReason;
|
|
1379
|
+
if (spawnGuardIsRateLimited(result)) {
|
|
1380
|
+
return "Spawn guard rate limit reached.";
|
|
1381
|
+
}
|
|
1382
|
+
return "Spawn guard denied dispatch.";
|
|
1383
|
+
}
|