@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.
Files changed (120) hide show
  1. package/.env.example +914 -0
  2. package/LICENSE +190 -0
  3. package/README.md +500 -0
  4. package/agent-endpoint.mjs +918 -0
  5. package/agent-hook-bridge.mjs +230 -0
  6. package/agent-hooks.mjs +1188 -0
  7. package/agent-pool.mjs +2403 -0
  8. package/agent-prompts.mjs +689 -0
  9. package/agent-sdk.mjs +141 -0
  10. package/anomaly-detector.mjs +1195 -0
  11. package/autofix.mjs +1294 -0
  12. package/claude-shell.mjs +708 -0
  13. package/cli.mjs +906 -0
  14. package/codex-config.mjs +1274 -0
  15. package/codex-model-profiles.mjs +135 -0
  16. package/codex-shell.mjs +762 -0
  17. package/config-doctor.mjs +613 -0
  18. package/config.mjs +1720 -0
  19. package/conflict-resolver.mjs +248 -0
  20. package/container-runner.mjs +450 -0
  21. package/copilot-shell.mjs +827 -0
  22. package/daemon-restart-policy.mjs +56 -0
  23. package/diff-stats.mjs +282 -0
  24. package/error-detector.mjs +829 -0
  25. package/fetch-runtime.mjs +34 -0
  26. package/fleet-coordinator.mjs +838 -0
  27. package/get-telegram-chat-id.mjs +71 -0
  28. package/git-safety.mjs +170 -0
  29. package/github-reconciler.mjs +403 -0
  30. package/hook-profiles.mjs +651 -0
  31. package/kanban-adapter.mjs +4491 -0
  32. package/lib/logger.mjs +645 -0
  33. package/maintenance.mjs +828 -0
  34. package/merge-strategy.mjs +1171 -0
  35. package/monitor.mjs +12207 -0
  36. package/openfleet.config.example.json +115 -0
  37. package/openfleet.schema.json +465 -0
  38. package/package.json +203 -0
  39. package/postinstall.mjs +187 -0
  40. package/pr-cleanup-daemon.mjs +978 -0
  41. package/preflight.mjs +408 -0
  42. package/prepublish-check.mjs +90 -0
  43. package/presence.mjs +328 -0
  44. package/primary-agent.mjs +282 -0
  45. package/publish.mjs +151 -0
  46. package/repo-root.mjs +29 -0
  47. package/restart-controller.mjs +100 -0
  48. package/review-agent.mjs +557 -0
  49. package/rotate-agent-logs.sh +133 -0
  50. package/sdk-conflict-resolver.mjs +973 -0
  51. package/session-tracker.mjs +880 -0
  52. package/setup.mjs +3937 -0
  53. package/shared-knowledge.mjs +410 -0
  54. package/shared-state-manager.mjs +841 -0
  55. package/shared-workspace-cli.mjs +199 -0
  56. package/shared-workspace-registry.mjs +537 -0
  57. package/shared-workspaces.json +18 -0
  58. package/startup-service.mjs +1070 -0
  59. package/sync-engine.mjs +1063 -0
  60. package/task-archiver.mjs +801 -0
  61. package/task-assessment.mjs +550 -0
  62. package/task-claims.mjs +924 -0
  63. package/task-complexity.mjs +581 -0
  64. package/task-executor.mjs +5111 -0
  65. package/task-store.mjs +753 -0
  66. package/telegram-bot.mjs +9281 -0
  67. package/telegram-sentinel.mjs +2010 -0
  68. package/ui/app.js +867 -0
  69. package/ui/app.legacy.js +1464 -0
  70. package/ui/app.monolith.js +2488 -0
  71. package/ui/components/charts.js +226 -0
  72. package/ui/components/chat-view.js +567 -0
  73. package/ui/components/command-palette.js +587 -0
  74. package/ui/components/diff-viewer.js +190 -0
  75. package/ui/components/forms.js +327 -0
  76. package/ui/components/kanban-board.js +451 -0
  77. package/ui/components/session-list.js +305 -0
  78. package/ui/components/shared.js +473 -0
  79. package/ui/index.html +70 -0
  80. package/ui/modules/api.js +297 -0
  81. package/ui/modules/icons.js +461 -0
  82. package/ui/modules/router.js +81 -0
  83. package/ui/modules/settings-schema.js +261 -0
  84. package/ui/modules/state.js +679 -0
  85. package/ui/modules/telegram.js +331 -0
  86. package/ui/modules/utils.js +270 -0
  87. package/ui/styles/animations.css +140 -0
  88. package/ui/styles/base.css +98 -0
  89. package/ui/styles/components.css +1915 -0
  90. package/ui/styles/kanban.css +286 -0
  91. package/ui/styles/layout.css +809 -0
  92. package/ui/styles/sessions.css +827 -0
  93. package/ui/styles/variables.css +188 -0
  94. package/ui/styles.css +141 -0
  95. package/ui/styles.monolith.css +1046 -0
  96. package/ui/tabs/agents.js +1417 -0
  97. package/ui/tabs/chat.js +74 -0
  98. package/ui/tabs/control.js +887 -0
  99. package/ui/tabs/dashboard.js +515 -0
  100. package/ui/tabs/infra.js +537 -0
  101. package/ui/tabs/logs.js +783 -0
  102. package/ui/tabs/settings.js +1487 -0
  103. package/ui/tabs/tasks.js +1385 -0
  104. package/ui-server.mjs +4073 -0
  105. package/update-check.mjs +465 -0
  106. package/utils.mjs +172 -0
  107. package/ve-kanban.mjs +654 -0
  108. package/ve-kanban.ps1 +1365 -0
  109. package/ve-kanban.sh +18 -0
  110. package/ve-orchestrator.mjs +340 -0
  111. package/ve-orchestrator.ps1 +6546 -0
  112. package/ve-orchestrator.sh +18 -0
  113. package/vibe-kanban-wrapper.mjs +41 -0
  114. package/vk-error-resolver.mjs +470 -0
  115. package/vk-log-stream.mjs +914 -0
  116. package/whatsapp-channel.mjs +520 -0
  117. package/workspace-monitor.mjs +581 -0
  118. package/workspace-reaper.mjs +405 -0
  119. package/workspace-registry.mjs +238 -0
  120. 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
+ }