@virtengine/openfleet 0.25.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/.env.example +914 -0
- package/LICENSE +190 -0
- package/README.md +500 -0
- package/agent-endpoint.mjs +918 -0
- package/agent-hook-bridge.mjs +230 -0
- package/agent-hooks.mjs +1188 -0
- package/agent-pool.mjs +2403 -0
- package/agent-prompts.mjs +689 -0
- package/agent-sdk.mjs +141 -0
- package/anomaly-detector.mjs +1195 -0
- package/autofix.mjs +1294 -0
- package/claude-shell.mjs +708 -0
- package/cli.mjs +906 -0
- package/codex-config.mjs +1274 -0
- package/codex-model-profiles.mjs +135 -0
- package/codex-shell.mjs +762 -0
- package/config-doctor.mjs +613 -0
- package/config.mjs +1720 -0
- package/conflict-resolver.mjs +248 -0
- package/container-runner.mjs +450 -0
- package/copilot-shell.mjs +827 -0
- package/daemon-restart-policy.mjs +56 -0
- package/diff-stats.mjs +282 -0
- package/error-detector.mjs +829 -0
- package/fetch-runtime.mjs +34 -0
- package/fleet-coordinator.mjs +838 -0
- package/get-telegram-chat-id.mjs +71 -0
- package/git-safety.mjs +170 -0
- package/github-reconciler.mjs +403 -0
- package/hook-profiles.mjs +651 -0
- package/kanban-adapter.mjs +4491 -0
- package/lib/logger.mjs +645 -0
- package/maintenance.mjs +828 -0
- package/merge-strategy.mjs +1171 -0
- package/monitor.mjs +12207 -0
- package/openfleet.config.example.json +115 -0
- package/openfleet.schema.json +465 -0
- package/package.json +203 -0
- package/postinstall.mjs +187 -0
- package/pr-cleanup-daemon.mjs +978 -0
- package/preflight.mjs +408 -0
- package/prepublish-check.mjs +90 -0
- package/presence.mjs +328 -0
- package/primary-agent.mjs +282 -0
- package/publish.mjs +151 -0
- package/repo-root.mjs +29 -0
- package/restart-controller.mjs +100 -0
- package/review-agent.mjs +557 -0
- package/rotate-agent-logs.sh +133 -0
- package/sdk-conflict-resolver.mjs +973 -0
- package/session-tracker.mjs +880 -0
- package/setup.mjs +3937 -0
- package/shared-knowledge.mjs +410 -0
- package/shared-state-manager.mjs +841 -0
- package/shared-workspace-cli.mjs +199 -0
- package/shared-workspace-registry.mjs +537 -0
- package/shared-workspaces.json +18 -0
- package/startup-service.mjs +1070 -0
- package/sync-engine.mjs +1063 -0
- package/task-archiver.mjs +801 -0
- package/task-assessment.mjs +550 -0
- package/task-claims.mjs +924 -0
- package/task-complexity.mjs +581 -0
- package/task-executor.mjs +5111 -0
- package/task-store.mjs +753 -0
- package/telegram-bot.mjs +9281 -0
- package/telegram-sentinel.mjs +2010 -0
- package/ui/app.js +867 -0
- package/ui/app.legacy.js +1464 -0
- package/ui/app.monolith.js +2488 -0
- package/ui/components/charts.js +226 -0
- package/ui/components/chat-view.js +567 -0
- package/ui/components/command-palette.js +587 -0
- package/ui/components/diff-viewer.js +190 -0
- package/ui/components/forms.js +327 -0
- package/ui/components/kanban-board.js +451 -0
- package/ui/components/session-list.js +305 -0
- package/ui/components/shared.js +473 -0
- package/ui/index.html +70 -0
- package/ui/modules/api.js +297 -0
- package/ui/modules/icons.js +461 -0
- package/ui/modules/router.js +81 -0
- package/ui/modules/settings-schema.js +261 -0
- package/ui/modules/state.js +679 -0
- package/ui/modules/telegram.js +331 -0
- package/ui/modules/utils.js +270 -0
- package/ui/styles/animations.css +140 -0
- package/ui/styles/base.css +98 -0
- package/ui/styles/components.css +1915 -0
- package/ui/styles/kanban.css +286 -0
- package/ui/styles/layout.css +809 -0
- package/ui/styles/sessions.css +827 -0
- package/ui/styles/variables.css +188 -0
- package/ui/styles.css +141 -0
- package/ui/styles.monolith.css +1046 -0
- package/ui/tabs/agents.js +1417 -0
- package/ui/tabs/chat.js +74 -0
- package/ui/tabs/control.js +887 -0
- package/ui/tabs/dashboard.js +515 -0
- package/ui/tabs/infra.js +537 -0
- package/ui/tabs/logs.js +783 -0
- package/ui/tabs/settings.js +1487 -0
- package/ui/tabs/tasks.js +1385 -0
- package/ui-server.mjs +4073 -0
- package/update-check.mjs +465 -0
- package/utils.mjs +172 -0
- package/ve-kanban.mjs +654 -0
- package/ve-kanban.ps1 +1365 -0
- package/ve-kanban.sh +18 -0
- package/ve-orchestrator.mjs +340 -0
- package/ve-orchestrator.ps1 +6546 -0
- package/ve-orchestrator.sh +18 -0
- package/vibe-kanban-wrapper.mjs +41 -0
- package/vk-error-resolver.mjs +470 -0
- package/vk-log-stream.mjs +914 -0
- package/whatsapp-channel.mjs +520 -0
- package/workspace-monitor.mjs +581 -0
- package/workspace-reaper.mjs +405 -0
- package/workspace-registry.mjs +238 -0
- package/worktree-manager.mjs +1266 -0
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* task-complexity.mjs — Task complexity routing for openfleet.
|
|
3
|
+
*
|
|
4
|
+
* Maps task size/complexity to appropriate AI models and reasoning effort
|
|
5
|
+
* levels. Each executor type (CODEX, COPILOT/Claude) has its own model tier
|
|
6
|
+
* ladder, so small tasks use cheaper/faster models while complex tasks get
|
|
7
|
+
* the most capable models.
|
|
8
|
+
*
|
|
9
|
+
* Complexity Tiers:
|
|
10
|
+
* LOW — xs/s tasks: simple fixes, docs, config changes
|
|
11
|
+
* MEDIUM — m tasks: standard feature work, moderate refactors
|
|
12
|
+
* HIGH — l/xl/xxl tasks: complex architecture, multi-file changes
|
|
13
|
+
*
|
|
14
|
+
* Default Model Mapping:
|
|
15
|
+
* ┌──────────┬──────────────────────┬─────────────────────┐
|
|
16
|
+
* │ Tier │ CODEX │ COPILOT (Claude) │
|
|
17
|
+
* ├──────────┼──────────────────────┼─────────────────────┤
|
|
18
|
+
* │ LOW │ gpt-5.1-codex-mini │ haiku-4.5 │
|
|
19
|
+
* │ MEDIUM │ gpt-5.2-codex │ sonnet-4.5 │
|
|
20
|
+
* │ HIGH │ gpt-5.1-codex-max │ opus-4.6 │
|
|
21
|
+
* └──────────┴──────────────────────┴─────────────────────┘
|
|
22
|
+
*
|
|
23
|
+
* Reasoning Effort per tier:
|
|
24
|
+
* LOW → "low"
|
|
25
|
+
* MEDIUM → "medium"
|
|
26
|
+
* HIGH → "high"
|
|
27
|
+
*
|
|
28
|
+
* The orchestrator calls `resolveExecutorForTask(task, executorProfile, config)`
|
|
29
|
+
* to get the optimal model/variant/reasoning for that specific task.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
export const COMPLEXITY_TIERS = Object.freeze({
|
|
35
|
+
LOW: "low",
|
|
36
|
+
MEDIUM: "medium",
|
|
37
|
+
HIGH: "high",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Map task size labels (from ve-orchestrator.ps1 Get-TaskSizeInfo) to
|
|
42
|
+
* complexity tiers. The size→complexity mapping is intentionally simple.
|
|
43
|
+
*/
|
|
44
|
+
export const SIZE_TO_COMPLEXITY = Object.freeze({
|
|
45
|
+
xs: COMPLEXITY_TIERS.LOW,
|
|
46
|
+
s: COMPLEXITY_TIERS.LOW,
|
|
47
|
+
m: COMPLEXITY_TIERS.MEDIUM,
|
|
48
|
+
l: COMPLEXITY_TIERS.HIGH,
|
|
49
|
+
xl: COMPLEXITY_TIERS.HIGH,
|
|
50
|
+
xxl: COMPLEXITY_TIERS.HIGH,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Default model profiles per executor type and complexity tier.
|
|
55
|
+
* Users can override via config.complexityRouting.models
|
|
56
|
+
*/
|
|
57
|
+
export const DEFAULT_MODEL_PROFILES = Object.freeze({
|
|
58
|
+
CODEX: {
|
|
59
|
+
[COMPLEXITY_TIERS.LOW]: {
|
|
60
|
+
model: "gpt-5.1-codex-mini",
|
|
61
|
+
variant: "GPT51_CODEX_MINI",
|
|
62
|
+
reasoningEffort: "low",
|
|
63
|
+
},
|
|
64
|
+
[COMPLEXITY_TIERS.MEDIUM]: {
|
|
65
|
+
model: "gpt-5.2-codex",
|
|
66
|
+
variant: "DEFAULT",
|
|
67
|
+
reasoningEffort: "medium",
|
|
68
|
+
},
|
|
69
|
+
[COMPLEXITY_TIERS.HIGH]: {
|
|
70
|
+
model: "gpt-5.1-codex-max",
|
|
71
|
+
variant: "GPT51_CODEX_MAX",
|
|
72
|
+
reasoningEffort: "high",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
COPILOT: {
|
|
76
|
+
[COMPLEXITY_TIERS.LOW]: {
|
|
77
|
+
model: "haiku-4.5",
|
|
78
|
+
variant: "CLAUDE_HAIKU_4_5",
|
|
79
|
+
reasoningEffort: "low",
|
|
80
|
+
},
|
|
81
|
+
[COMPLEXITY_TIERS.MEDIUM]: {
|
|
82
|
+
model: "sonnet-4.5",
|
|
83
|
+
variant: "CLAUDE_SONNET_4_5",
|
|
84
|
+
reasoningEffort: "medium",
|
|
85
|
+
},
|
|
86
|
+
[COMPLEXITY_TIERS.HIGH]: {
|
|
87
|
+
model: "opus-4.6",
|
|
88
|
+
variant: "CLAUDE_OPUS_4_6",
|
|
89
|
+
reasoningEffort: "high",
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Additional model aliases for manual overrides and telegram /model command.
|
|
96
|
+
* These are not used in automatic routing but allow explicit model selection.
|
|
97
|
+
*/
|
|
98
|
+
export const MODEL_ALIASES = Object.freeze({
|
|
99
|
+
"gpt-5.1-codex-mini": { executor: "CODEX", variant: "GPT51_CODEX_MINI" },
|
|
100
|
+
"gpt-5.2-codex": { executor: "CODEX", variant: "DEFAULT" },
|
|
101
|
+
"gpt-5.1-codex-max": { executor: "CODEX", variant: "GPT51_CODEX_MAX" },
|
|
102
|
+
"claude-opus-4.6": { executor: "COPILOT", variant: "CLAUDE_OPUS_4_6" },
|
|
103
|
+
"opus-4.6": { executor: "COPILOT", variant: "CLAUDE_OPUS_4_6" },
|
|
104
|
+
"sonnet-4.5": { executor: "COPILOT", variant: "CLAUDE_SONNET_4_5" },
|
|
105
|
+
"haiku-4.5": { executor: "COPILOT", variant: "CLAUDE_HAIKU_4_5" },
|
|
106
|
+
"claude-code": { executor: "COPILOT", variant: "CLAUDE_CODE" },
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Keywords in task titles/descriptions that bump complexity up or down.
|
|
111
|
+
* Scanned case-insensitively against the combined task text blob.
|
|
112
|
+
*/
|
|
113
|
+
export const COMPLEXITY_SIGNALS = Object.freeze({
|
|
114
|
+
/** Signals that push complexity UP */
|
|
115
|
+
escalators: [
|
|
116
|
+
// Architecture / multi-system
|
|
117
|
+
/\b(architect|redesign|refactor.*entire|overhaul|migration)\b/i,
|
|
118
|
+
/\b(multi[- ]?module|cross[- ]?cutting|system[- ]?wide)\b/i,
|
|
119
|
+
/\b(breaking\s+change|backward.*compat|api.*redesign)\b/i,
|
|
120
|
+
// Security / crypto
|
|
121
|
+
/\b(security.*audit|vulnerability|encryption.*scheme|key.*rotation)\b/i,
|
|
122
|
+
// Consensus / blockchain specific
|
|
123
|
+
/\b(consensus|determinism|state.*machine|genesis|upgrade.*handler)\b/i,
|
|
124
|
+
// Testing complexity
|
|
125
|
+
/\b(e2e.*test.*suite|integration.*framework|test.*infrastructure)\b/i,
|
|
126
|
+
// Scale / performance
|
|
127
|
+
/\b(load\s+test|stress\s+test|1M|1,000,000|million\s+nodes?)\b/i,
|
|
128
|
+
/\b(service\s+mesh|api\s+gateway|mTLS|circuit\s+breaker)\b/i,
|
|
129
|
+
// LOC estimation (>3000 LOC signals high complexity)
|
|
130
|
+
/Est\.?\s*LOC\s*:\s*[3-9],?\d{3}/i,
|
|
131
|
+
/Est\.?\s*LOC\s*:\s*\d{2,},?\d{3}/i,
|
|
132
|
+
// Multi-file / broad scope
|
|
133
|
+
/\b(\d{2,}\s+(?:test|file|module)s?\s+fail)/i,
|
|
134
|
+
/\b(disaster\s+recovery|business\s+continuity|CRITICAL)\b/i,
|
|
135
|
+
],
|
|
136
|
+
/** Signals that push complexity DOWN */
|
|
137
|
+
simplifiers: [
|
|
138
|
+
/\b(typo|typos|spelling|grammar)\b/i,
|
|
139
|
+
/\b(bump|upgrade)\s+(version|dep|dependency)\b/i,
|
|
140
|
+
/\b(readme|changelog|docs?\s+only)\b/i,
|
|
141
|
+
/\b(lint|format|prettier|eslint)\s*(fix|cleanup|config)?\b/i,
|
|
142
|
+
/\b(rename|move\s+file|copy\s+file)\b/i,
|
|
143
|
+
/\b(add\s+comment|update\s+comment)\b/i,
|
|
144
|
+
/\b(config\s+change|env\s+var|\.env)\b/i,
|
|
145
|
+
// Plan-only tasks
|
|
146
|
+
/\bPlan\s+next\s+tasks\b/i,
|
|
147
|
+
/\b(manual[- ]telegram|triage)\b/i,
|
|
148
|
+
],
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ── Core Functions ───────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Classify a task's complexity tier based on its size label and text content.
|
|
155
|
+
*
|
|
156
|
+
* @param {object} params
|
|
157
|
+
* @param {string} [params.sizeLabel] - Task size: xs/s/m/l/xl/xxl
|
|
158
|
+
* @param {string} [params.title] - Task title
|
|
159
|
+
* @param {string} [params.description] - Task description
|
|
160
|
+
* @param {number} [params.points] - Story points (optional, used if sizeLabel missing)
|
|
161
|
+
* @returns {{ tier: string, reason: string, sizeLabel: string, adjusted: boolean }}
|
|
162
|
+
*/
|
|
163
|
+
export function classifyComplexity({
|
|
164
|
+
sizeLabel,
|
|
165
|
+
title = "",
|
|
166
|
+
description = "",
|
|
167
|
+
points,
|
|
168
|
+
} = {}) {
|
|
169
|
+
// Resolve size label from points if not provided
|
|
170
|
+
let resolvedSize = (sizeLabel || "m").toLowerCase();
|
|
171
|
+
if (!sizeLabel && typeof points === "number") {
|
|
172
|
+
if (points <= 1) resolvedSize = "xs";
|
|
173
|
+
else if (points <= 2) resolvedSize = "s";
|
|
174
|
+
else if (points <= 5) resolvedSize = "m";
|
|
175
|
+
else if (points <= 8) resolvedSize = "l";
|
|
176
|
+
else if (points <= 13) resolvedSize = "xl";
|
|
177
|
+
else resolvedSize = "xxl";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Base tier from size
|
|
181
|
+
let tier = SIZE_TO_COMPLEXITY[resolvedSize] || COMPLEXITY_TIERS.MEDIUM;
|
|
182
|
+
const baseTier = tier;
|
|
183
|
+
let adjusted = false;
|
|
184
|
+
let reason = `size=${resolvedSize}`;
|
|
185
|
+
|
|
186
|
+
// Scan text for complexity signals
|
|
187
|
+
const text = `${title} ${description}`.trim();
|
|
188
|
+
if (text) {
|
|
189
|
+
const escalatorHits = COMPLEXITY_SIGNALS.escalators.filter((rx) =>
|
|
190
|
+
rx.test(text),
|
|
191
|
+
);
|
|
192
|
+
const simplifierHits = COMPLEXITY_SIGNALS.simplifiers.filter((rx) =>
|
|
193
|
+
rx.test(text),
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
if (escalatorHits.length > 0 && simplifierHits.length === 0) {
|
|
197
|
+
// Escalate: LOW→MEDIUM, MEDIUM→HIGH (HIGH stays HIGH)
|
|
198
|
+
if (tier === COMPLEXITY_TIERS.LOW) {
|
|
199
|
+
tier = COMPLEXITY_TIERS.MEDIUM;
|
|
200
|
+
adjusted = true;
|
|
201
|
+
reason += " → escalated by keywords";
|
|
202
|
+
} else if (tier === COMPLEXITY_TIERS.MEDIUM) {
|
|
203
|
+
tier = COMPLEXITY_TIERS.HIGH;
|
|
204
|
+
adjusted = true;
|
|
205
|
+
reason += " → escalated by keywords";
|
|
206
|
+
}
|
|
207
|
+
} else if (simplifierHits.length > 0 && escalatorHits.length === 0) {
|
|
208
|
+
// Simplify: HIGH→MEDIUM, MEDIUM→LOW (LOW stays LOW)
|
|
209
|
+
if (tier === COMPLEXITY_TIERS.HIGH) {
|
|
210
|
+
tier = COMPLEXITY_TIERS.MEDIUM;
|
|
211
|
+
adjusted = true;
|
|
212
|
+
reason += " → simplified by keywords";
|
|
213
|
+
} else if (tier === COMPLEXITY_TIERS.MEDIUM) {
|
|
214
|
+
tier = COMPLEXITY_TIERS.LOW;
|
|
215
|
+
adjusted = true;
|
|
216
|
+
reason += " → simplified by keywords";
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// If both hit, they cancel out — keep the base tier
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { tier, reason, sizeLabel: resolvedSize, adjusted, baseTier };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Get the model profile for a given complexity tier and executor type.
|
|
227
|
+
*
|
|
228
|
+
* @param {string} tier - Complexity tier: "low" | "medium" | "high"
|
|
229
|
+
* @param {string} executorType - "CODEX" | "COPILOT"
|
|
230
|
+
* @param {object} [configOverrides] - User-provided model overrides from config
|
|
231
|
+
* @returns {{ model: string, variant: string, reasoningEffort: string }}
|
|
232
|
+
*/
|
|
233
|
+
export function getModelForComplexity(tier, executorType, configOverrides) {
|
|
234
|
+
const normalizedType = (executorType || "CODEX").toUpperCase();
|
|
235
|
+
const normalizedTier = (tier || "medium").toLowerCase();
|
|
236
|
+
|
|
237
|
+
// Check user overrides first
|
|
238
|
+
if (configOverrides?.models?.[normalizedType]?.[normalizedTier]) {
|
|
239
|
+
return { ...configOverrides.models[normalizedType][normalizedTier] };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Fall back to defaults
|
|
243
|
+
const profiles = DEFAULT_MODEL_PROFILES[normalizedType];
|
|
244
|
+
if (!profiles) {
|
|
245
|
+
// Unknown executor type — return a safe default
|
|
246
|
+
return {
|
|
247
|
+
model: null,
|
|
248
|
+
variant: null,
|
|
249
|
+
reasoningEffort: normalizedTier === "high" ? "high" : "medium",
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return { ...(profiles[normalizedTier] || profiles[COMPLEXITY_TIERS.MEDIUM]) };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Resolve the optimal executor profile for a specific task.
|
|
258
|
+
*
|
|
259
|
+
* This is the main entry point for the orchestrator. Given a task and the
|
|
260
|
+
* base executor profile (from round-robin/weighted selection), it returns
|
|
261
|
+
* an enhanced profile with the right model/variant/reasoning for the task's
|
|
262
|
+
* complexity.
|
|
263
|
+
*
|
|
264
|
+
* @param {object} task - Task object from VK (has .title, .description, fields, metadata)
|
|
265
|
+
* @param {object} baseProfile - Executor profile from ExecutorScheduler.next()
|
|
266
|
+
* { name, executor, variant, weight, role, enabled }
|
|
267
|
+
* @param {object} [complexityConfig] - Config from loadConfig().complexityRouting
|
|
268
|
+
* @returns {{
|
|
269
|
+
* executor: string,
|
|
270
|
+
* variant: string,
|
|
271
|
+
* model: string,
|
|
272
|
+
* reasoningEffort: string,
|
|
273
|
+
* complexity: { tier: string, reason: string, sizeLabel: string, adjusted: boolean },
|
|
274
|
+
* original: object
|
|
275
|
+
* }}
|
|
276
|
+
*/
|
|
277
|
+
export function resolveExecutorForTask(task, baseProfile, complexityConfig) {
|
|
278
|
+
const config = complexityConfig || {};
|
|
279
|
+
|
|
280
|
+
// If complexity routing is disabled, return base profile as-is
|
|
281
|
+
if (config.enabled === false) {
|
|
282
|
+
return {
|
|
283
|
+
...baseProfile,
|
|
284
|
+
model: null,
|
|
285
|
+
reasoningEffort: null,
|
|
286
|
+
complexity: null,
|
|
287
|
+
original: baseProfile,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Extract task info
|
|
292
|
+
const title = task?.title || "";
|
|
293
|
+
const description = task?.description || "";
|
|
294
|
+
const sizeLabel = extractSizeLabel(task);
|
|
295
|
+
const points = extractPoints(task);
|
|
296
|
+
|
|
297
|
+
// Classify complexity
|
|
298
|
+
const complexity = classifyComplexity({
|
|
299
|
+
sizeLabel,
|
|
300
|
+
title,
|
|
301
|
+
description,
|
|
302
|
+
points,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Get model profile for this tier + executor type
|
|
306
|
+
const executorType = (baseProfile?.executor || "CODEX").toUpperCase();
|
|
307
|
+
const modelProfile = getModelForComplexity(
|
|
308
|
+
complexity.tier,
|
|
309
|
+
executorType,
|
|
310
|
+
config,
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
name: baseProfile?.name || "auto",
|
|
315
|
+
executor: baseProfile?.executor || "CODEX",
|
|
316
|
+
variant: modelProfile.variant || baseProfile?.variant || "DEFAULT",
|
|
317
|
+
weight: baseProfile?.weight || 100,
|
|
318
|
+
role: baseProfile?.role || "primary",
|
|
319
|
+
enabled: baseProfile?.enabled !== false,
|
|
320
|
+
model: modelProfile.model,
|
|
321
|
+
reasoningEffort: modelProfile.reasoningEffort,
|
|
322
|
+
complexity,
|
|
323
|
+
original: baseProfile,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Produce a human-readable summary of the complexity routing decision.
|
|
329
|
+
* Used for log output and Telegram notifications.
|
|
330
|
+
*
|
|
331
|
+
* @param {object} resolved - Output from resolveExecutorForTask()
|
|
332
|
+
* @returns {string}
|
|
333
|
+
*/
|
|
334
|
+
export function formatComplexityDecision(resolved) {
|
|
335
|
+
if (!resolved?.complexity) return "complexity=disabled";
|
|
336
|
+
const { complexity, model, reasoningEffort, executor } = resolved;
|
|
337
|
+
const parts = [
|
|
338
|
+
`complexity=${complexity.tier}`,
|
|
339
|
+
`size=${complexity.sizeLabel}`,
|
|
340
|
+
`model=${model || "default"}`,
|
|
341
|
+
`reasoning=${reasoningEffort || "default"}`,
|
|
342
|
+
`executor=${executor}`,
|
|
343
|
+
];
|
|
344
|
+
if (complexity.adjusted) {
|
|
345
|
+
parts.push(`adjusted=true`);
|
|
346
|
+
}
|
|
347
|
+
return parts.join(" ");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Get all available complexity tiers with their model mappings.
|
|
352
|
+
* Useful for config validation and UI display.
|
|
353
|
+
*
|
|
354
|
+
* @param {object} [configOverrides] - User config overrides
|
|
355
|
+
* @returns {object} Map of executorType → tier → modelProfile
|
|
356
|
+
*/
|
|
357
|
+
export function getComplexityMatrix(configOverrides) {
|
|
358
|
+
const matrix = {};
|
|
359
|
+
for (const executorType of ["CODEX", "COPILOT"]) {
|
|
360
|
+
matrix[executorType] = {};
|
|
361
|
+
for (const tier of Object.values(COMPLEXITY_TIERS)) {
|
|
362
|
+
matrix[executorType][tier] = getModelForComplexity(
|
|
363
|
+
tier,
|
|
364
|
+
executorType,
|
|
365
|
+
configOverrides,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return matrix;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ── Task Completion Confidence ───────────────────────────────────────────────
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Confidence levels for task completion.
|
|
376
|
+
* Agents should mark tasks with one of these to signal review needs.
|
|
377
|
+
*/
|
|
378
|
+
export const COMPLETION_CONFIDENCE = Object.freeze({
|
|
379
|
+
/** Task fully completed, all tests pass, no concerns */
|
|
380
|
+
CONFIDENT: "confident",
|
|
381
|
+
/** Task completed but some edge cases may need review */
|
|
382
|
+
NEEDS_REVIEW: "needs-review",
|
|
383
|
+
/** Task partially completed, blocked or uncertain */
|
|
384
|
+
PARTIAL: "partial",
|
|
385
|
+
/** Task failed, needs replanning or different approach */
|
|
386
|
+
FAILED: "failed",
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Assess completion confidence based on task outcome signals.
|
|
391
|
+
*
|
|
392
|
+
* @param {object} params
|
|
393
|
+
* @param {boolean} params.testsPass - Did all tests pass?
|
|
394
|
+
* @param {boolean} params.buildClean - Is the build clean (0 warnings)?
|
|
395
|
+
* @param {boolean} params.lintClean - Did linting pass?
|
|
396
|
+
* @param {number} params.filesChanged - Number of files changed
|
|
397
|
+
* @param {number} params.attemptCount - How many attempts so far
|
|
398
|
+
* @param {string} params.complexityTier - The task's complexity tier
|
|
399
|
+
* @param {boolean} [params.hasTestCoverage] - Were new tests added for new code?
|
|
400
|
+
* @param {string[]} [params.warnings] - Any warning messages from the agent
|
|
401
|
+
* @returns {{ confidence: string, reason: string, shouldAutoMerge: boolean }}
|
|
402
|
+
*/
|
|
403
|
+
export function assessCompletionConfidence({
|
|
404
|
+
testsPass = false,
|
|
405
|
+
buildClean = false,
|
|
406
|
+
lintClean = false,
|
|
407
|
+
filesChanged = 0,
|
|
408
|
+
attemptCount = 1,
|
|
409
|
+
complexityTier = COMPLEXITY_TIERS.MEDIUM,
|
|
410
|
+
hasTestCoverage,
|
|
411
|
+
warnings = [],
|
|
412
|
+
}) {
|
|
413
|
+
// Failed basic checks → FAILED
|
|
414
|
+
if (!testsPass || !buildClean) {
|
|
415
|
+
return {
|
|
416
|
+
confidence: COMPLETION_CONFIDENCE.FAILED,
|
|
417
|
+
reason: testsPass ? "build has errors" : "tests failing",
|
|
418
|
+
shouldAutoMerge: false,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// High complexity + many files + no explicit test coverage → NEEDS_REVIEW
|
|
423
|
+
if (
|
|
424
|
+
complexityTier === COMPLEXITY_TIERS.HIGH &&
|
|
425
|
+
filesChanged > 10 &&
|
|
426
|
+
hasTestCoverage === false
|
|
427
|
+
) {
|
|
428
|
+
return {
|
|
429
|
+
confidence: COMPLETION_CONFIDENCE.NEEDS_REVIEW,
|
|
430
|
+
reason: "high complexity with many files and no new test coverage",
|
|
431
|
+
shouldAutoMerge: false,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Multiple attempts suggest difficulty → NEEDS_REVIEW
|
|
436
|
+
if (attemptCount >= 3) {
|
|
437
|
+
return {
|
|
438
|
+
confidence: COMPLETION_CONFIDENCE.NEEDS_REVIEW,
|
|
439
|
+
reason: `required ${attemptCount} attempts`,
|
|
440
|
+
shouldAutoMerge: false,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Warnings present → NEEDS_REVIEW
|
|
445
|
+
if (warnings.length > 0) {
|
|
446
|
+
return {
|
|
447
|
+
confidence: COMPLETION_CONFIDENCE.NEEDS_REVIEW,
|
|
448
|
+
reason: `${warnings.length} warning(s) reported`,
|
|
449
|
+
shouldAutoMerge: false,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Lint issues → NEEDS_REVIEW (non-blocking but review worthy)
|
|
454
|
+
if (!lintClean) {
|
|
455
|
+
return {
|
|
456
|
+
confidence: COMPLETION_CONFIDENCE.NEEDS_REVIEW,
|
|
457
|
+
reason: "lint warnings present",
|
|
458
|
+
shouldAutoMerge: false,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// All clean → CONFIDENT
|
|
463
|
+
return {
|
|
464
|
+
confidence: COMPLETION_CONFIDENCE.CONFIDENT,
|
|
465
|
+
reason: "all checks pass",
|
|
466
|
+
shouldAutoMerge: true,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Extract size label from a VK task object.
|
|
474
|
+
* Mirrors the logic in Get-TaskSizeInfo (ve-orchestrator.ps1).
|
|
475
|
+
*/
|
|
476
|
+
function extractSizeLabel(task) {
|
|
477
|
+
if (!task) return null;
|
|
478
|
+
|
|
479
|
+
// Check direct fields
|
|
480
|
+
for (const field of [
|
|
481
|
+
"size",
|
|
482
|
+
"estimate",
|
|
483
|
+
"story_points",
|
|
484
|
+
"points",
|
|
485
|
+
"effort",
|
|
486
|
+
"complexity",
|
|
487
|
+
]) {
|
|
488
|
+
const value = task[field];
|
|
489
|
+
if (value && typeof value === "string") return value.toLowerCase();
|
|
490
|
+
if (typeof value === "number") return pointsToSize(value);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Check metadata
|
|
494
|
+
if (task.metadata && typeof task.metadata === "object") {
|
|
495
|
+
for (const field of [
|
|
496
|
+
"size",
|
|
497
|
+
"estimate",
|
|
498
|
+
"story_points",
|
|
499
|
+
"points",
|
|
500
|
+
"effort",
|
|
501
|
+
]) {
|
|
502
|
+
const value = task.metadata[field];
|
|
503
|
+
if (value && typeof value === "string") return value.toLowerCase();
|
|
504
|
+
if (typeof value === "number") return pointsToSize(value);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Scan title for [size] pattern
|
|
509
|
+
const text = `${task.title || ""} ${task.description || ""}`;
|
|
510
|
+
const bracketMatch = text.match(/\[(xs|s|m|l|xl|xxl|2xl)\]/i);
|
|
511
|
+
if (bracketMatch) return bracketMatch[1].toLowerCase();
|
|
512
|
+
|
|
513
|
+
// Scan for size: value pattern
|
|
514
|
+
const colonMatch = text.match(
|
|
515
|
+
/\b(?:size|effort|estimate|points|story\s*points)\s*[:=]\s*(xs|s|small|m|medium|l|large|xl|x-large|xxl|2xl)\b/i,
|
|
516
|
+
);
|
|
517
|
+
if (colonMatch) {
|
|
518
|
+
const token = colonMatch[1].toLowerCase();
|
|
519
|
+
if (token === "small") return "s";
|
|
520
|
+
if (token === "medium" || token === "med") return "m";
|
|
521
|
+
if (token === "large" || token === "big") return "l";
|
|
522
|
+
if (token === "x-large") return "xl";
|
|
523
|
+
if (token === "2xl") return "xxl";
|
|
524
|
+
return token;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Scan for numeric points
|
|
528
|
+
const pointsMatch = text.match(
|
|
529
|
+
/\b(?:size|effort|estimate|points|story\s*points)\s*[:=]\s*(\d+(?:\.\d+)?)\b/i,
|
|
530
|
+
);
|
|
531
|
+
if (pointsMatch) return pointsToSize(Number(pointsMatch[1]));
|
|
532
|
+
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Extract numeric points from a VK task object.
|
|
538
|
+
*/
|
|
539
|
+
function extractPoints(task) {
|
|
540
|
+
if (!task) return null;
|
|
541
|
+
for (const field of [
|
|
542
|
+
"points",
|
|
543
|
+
"story_points",
|
|
544
|
+
"estimate",
|
|
545
|
+
"effort",
|
|
546
|
+
"size",
|
|
547
|
+
]) {
|
|
548
|
+
const value = task[field];
|
|
549
|
+
if (typeof value === "number") return value;
|
|
550
|
+
}
|
|
551
|
+
if (task.metadata && typeof task.metadata === "object") {
|
|
552
|
+
for (const field of ["points", "story_points", "estimate", "effort"]) {
|
|
553
|
+
const value = task.metadata[field];
|
|
554
|
+
if (typeof value === "number") return value;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Convert numeric story points to a size label.
|
|
562
|
+
* Mirrors Resolve-TaskSizeFromPoints in ve-orchestrator.ps1.
|
|
563
|
+
*/
|
|
564
|
+
function pointsToSize(points) {
|
|
565
|
+
if (points <= 1) return "xs";
|
|
566
|
+
if (points <= 2) return "s";
|
|
567
|
+
if (points <= 5) return "m";
|
|
568
|
+
if (points <= 8) return "l";
|
|
569
|
+
if (points <= 13) return "xl";
|
|
570
|
+
return "xxl";
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Map executor type string to SDK-compatible identifier.
|
|
575
|
+
*/
|
|
576
|
+
export function executorToSdk(executorType) {
|
|
577
|
+
const normalized = (executorType || "").toUpperCase();
|
|
578
|
+
if (normalized === "CLAUDE") return "claude";
|
|
579
|
+
if (normalized === "COPILOT") return "copilot";
|
|
580
|
+
return "codex";
|
|
581
|
+
}
|