@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,451 @@
1
+ /* ─────────────────────────────────────────────────────────────
2
+ * Kanban Board Component — Trello-style drag-and-drop task board
3
+ * ────────────────────────────────────────────────────────────── */
4
+ import { h } from "preact";
5
+ import { useState, useCallback, useRef, useEffect } from "preact/hooks";
6
+ import htm from "htm";
7
+ import { signal, computed } from "@preact/signals";
8
+ import { tasksData, tasksLoaded, showToast, runOptimistic, loadTasks } from "../modules/state.js";
9
+ import { apiFetch } from "../modules/api.js";
10
+ import { haptic } from "../modules/telegram.js";
11
+ import { formatRelative, truncate, cloneValue } from "../modules/utils.js";
12
+
13
+ const html = htm.bind(h);
14
+
15
+ /* ─── Column definitions ─── */
16
+ const COLUMN_MAP = {
17
+ draft: ["draft"],
18
+ backlog: ["backlog", "open", "new", "todo"],
19
+ inProgress: ["in-progress", "inprogress", "working", "active", "assigned"],
20
+ inReview: ["in-review", "inreview", "review", "pr-open", "pr-review"],
21
+ done: ["done", "completed", "closed", "merged", "cancelled"],
22
+ };
23
+
24
+ const COLUMNS = [
25
+ { id: "draft", title: "Drafts", icon: "\u{1F4DD}", color: "var(--color-warning, #f59e0b)" },
26
+ { id: "backlog", title: "Backlog", icon: "\u{1F4CB}", color: "var(--text-secondary)" },
27
+ { id: "inProgress", title: "In Progress", icon: "\u{1F528}", color: "var(--color-inprogress, #3b82f6)" },
28
+ { id: "inReview", title: "In Review", icon: "\u{1F440}", color: "var(--color-inreview, #f59e0b)" },
29
+ { id: "done", title: "Done", icon: "\u2705", color: "var(--color-done, #22c55e)" },
30
+ ];
31
+
32
+ const COLUMN_TO_STATUS = {
33
+ draft: "draft",
34
+ backlog: "todo",
35
+ inProgress: "inprogress",
36
+ inReview: "inreview",
37
+ done: "done",
38
+ };
39
+
40
+ const PRIORITY_COLORS = {
41
+ critical: "var(--color-critical, #dc2626)",
42
+ high: "var(--color-high, #f59e0b)",
43
+ medium: "var(--color-medium, #3b82f6)",
44
+ low: "var(--color-low, #8b95a2)",
45
+ };
46
+
47
+ const PRIORITY_LABELS = {
48
+ critical: "CRIT",
49
+ high: "HIGH",
50
+ medium: "MED",
51
+ low: "LOW",
52
+ };
53
+
54
+ function getColumnForStatus(status) {
55
+ const s = (status || "").toLowerCase();
56
+ for (const [col, statuses] of Object.entries(COLUMN_MAP)) {
57
+ if (statuses.includes(s)) return col;
58
+ }
59
+ return "backlog";
60
+ }
61
+
62
+ function getTaskTags(task) {
63
+ if (!task) return [];
64
+ const raw = Array.isArray(task.tags) && task.tags.length
65
+ ? task.tags
66
+ : Array.isArray(task?.meta?.tags)
67
+ ? task.meta.tags
68
+ : [];
69
+ return raw.filter((tag) => String(tag || "").trim().toLowerCase() !== "draft");
70
+ }
71
+
72
+ function getTaskBaseBranch(task) {
73
+ if (!task) return "";
74
+ return (
75
+ task.baseBranch ||
76
+ task.base_branch ||
77
+ task.meta?.baseBranch ||
78
+ task.meta?.base_branch ||
79
+ ""
80
+ );
81
+ }
82
+
83
+ /* ─── Derived column data ─── */
84
+ const columnData = computed(() => {
85
+ const tasks = tasksData.value || [];
86
+ const cols = {};
87
+ for (const col of COLUMNS) {
88
+ cols[col.id] = [];
89
+ }
90
+ for (const task of tasks) {
91
+ const col = getColumnForStatus(task.status);
92
+ if (cols[col]) cols[col].push(task);
93
+ }
94
+ return cols;
95
+ });
96
+
97
+ /* ─── Drag state (module-level signals) ─── */
98
+ const dragTaskId = signal(null);
99
+ const dragOverCol = signal(null);
100
+
101
+ /* ─── Touch drag state ─── */
102
+ const touchDragId = signal(null);
103
+ const touchOverCol = signal(null);
104
+ let _touchClone = null;
105
+ let _touchStartX = 0;
106
+ let _touchStartY = 0;
107
+ let _touchMoved = false;
108
+
109
+ /* ─── Touch drag helpers ─── */
110
+
111
+ function _createTouchClone(el) {
112
+ const rect = el.getBoundingClientRect();
113
+ const clone = el.cloneNode(true);
114
+ clone.className = "kanban-card touch-drag-clone";
115
+ clone.style.position = "fixed";
116
+ clone.style.width = rect.width + "px";
117
+ clone.style.left = rect.left + "px";
118
+ clone.style.top = rect.top + "px";
119
+ clone.style.zIndex = "9999";
120
+ clone.style.pointerEvents = "none";
121
+ document.body.appendChild(clone);
122
+ return clone;
123
+ }
124
+
125
+ function _moveTouchClone(clone, x, y) {
126
+ if (!clone) return;
127
+ const w = parseFloat(clone.style.width) || 0;
128
+ clone.style.left = (x - w / 2) + "px";
129
+ clone.style.top = (y - 40) + "px";
130
+ }
131
+
132
+ function _removeTouchClone() {
133
+ if (_touchClone && _touchClone.parentNode) {
134
+ _touchClone.parentNode.removeChild(_touchClone);
135
+ }
136
+ _touchClone = null;
137
+ }
138
+
139
+ function _columnFromPoint(x, y) {
140
+ const el = document.elementFromPoint(x, y);
141
+ if (!el) return null;
142
+ const colEl = el.closest(".kanban-column");
143
+ if (!colEl) return null;
144
+ for (const col of COLUMNS) {
145
+ if (colEl.getAttribute("data-col") === col.id) return col.id;
146
+ }
147
+ return null;
148
+ }
149
+
150
+ async function _handleTouchDrop(colId) {
151
+ const taskId = touchDragId.value;
152
+ touchDragId.value = null;
153
+ touchOverCol.value = null;
154
+ if (!taskId || !colId) return;
155
+
156
+ const currentTask = (tasksData.value || []).find((t) => t.id === taskId);
157
+ if (!currentTask) return;
158
+ const currentCol = getColumnForStatus(currentTask.status);
159
+ if (currentCol === colId) return;
160
+
161
+ const newStatus = COLUMN_TO_STATUS[colId] || "todo";
162
+ const col = COLUMNS.find((c) => c.id === colId);
163
+ haptic("medium");
164
+
165
+ const prev = cloneValue(tasksData.value);
166
+ try {
167
+ await runOptimistic(
168
+ () => {
169
+ tasksData.value = tasksData.value.map((t) =>
170
+ t.id === taskId ? { ...t, status: newStatus } : t,
171
+ );
172
+ },
173
+ async () => {
174
+ const res = await apiFetch("/api/tasks/update", {
175
+ method: "POST",
176
+ body: JSON.stringify({ taskId, status: newStatus }),
177
+ });
178
+ if (res?.data) {
179
+ tasksData.value = tasksData.value.map((t) =>
180
+ t.id === taskId ? { ...t, ...res.data } : t,
181
+ );
182
+ }
183
+ return res;
184
+ },
185
+ () => {
186
+ tasksData.value = prev;
187
+ },
188
+ );
189
+ showToast(`Moved to ${col ? col.title : colId}`, "success");
190
+ } catch {
191
+ /* toast via apiFetch */
192
+ }
193
+ }
194
+
195
+ /* ─── Inline create for a column ─── */
196
+ async function createTaskInColumn(columnStatus, title) {
197
+ haptic("medium");
198
+ try {
199
+ await apiFetch("/api/tasks/create", {
200
+ method: "POST",
201
+ body: JSON.stringify({
202
+ title,
203
+ status: columnStatus,
204
+ draft: columnStatus === "draft",
205
+ }),
206
+ });
207
+ showToast("Task created", "success");
208
+ await loadTasks();
209
+ } catch {
210
+ /* toast via apiFetch */
211
+ }
212
+ }
213
+
214
+ /* ─── KanbanCard ─── */
215
+ function KanbanCard({ task, onOpen }) {
216
+ const onDragStart = useCallback((e) => {
217
+ dragTaskId.value = task.id;
218
+ e.dataTransfer.effectAllowed = "move";
219
+ e.dataTransfer.setData("text/plain", task.id);
220
+ e.currentTarget.classList.add("dragging");
221
+ }, [task.id]);
222
+
223
+ const onDragEnd = useCallback((e) => {
224
+ dragTaskId.value = null;
225
+ e.currentTarget.classList.remove("dragging");
226
+ }, []);
227
+
228
+ /* ─ Touch drag handlers ─ */
229
+ const onTouchStart = useCallback((e) => {
230
+ const touch = e.touches[0];
231
+ _touchStartX = touch.clientX;
232
+ _touchStartY = touch.clientY;
233
+ _touchMoved = false;
234
+ touchDragId.value = task.id;
235
+ }, [task.id]);
236
+
237
+ const onTouchMove = useCallback((e) => {
238
+ const touch = e.touches[0];
239
+ const dx = touch.clientX - _touchStartX;
240
+ const dy = touch.clientY - _touchStartY;
241
+
242
+ // Only start drag after a small threshold to distinguish from scroll
243
+ if (!_touchMoved && Math.abs(dx) < 10 && Math.abs(dy) < 10) return;
244
+
245
+ if (!_touchMoved) {
246
+ _touchMoved = true;
247
+ _removeTouchClone();
248
+ _touchClone = _createTouchClone(e.currentTarget);
249
+ haptic("medium");
250
+ }
251
+
252
+ e.preventDefault(); // prevent scroll during drag
253
+ _moveTouchClone(_touchClone, touch.clientX, touch.clientY);
254
+
255
+ const colId = _columnFromPoint(touch.clientX, touch.clientY);
256
+ touchOverCol.value = colId;
257
+ }, []);
258
+
259
+ const onTouchEnd = useCallback(() => {
260
+ const colId = touchOverCol.value;
261
+ _removeTouchClone();
262
+ if (_touchMoved && colId) {
263
+ _handleTouchDrop(colId);
264
+ } else {
265
+ touchDragId.value = null;
266
+ touchOverCol.value = null;
267
+ }
268
+ _touchMoved = false;
269
+ }, []);
270
+
271
+ const onTouchCancel = useCallback(() => {
272
+ _removeTouchClone();
273
+ touchDragId.value = null;
274
+ touchOverCol.value = null;
275
+ _touchMoved = false;
276
+ }, []);
277
+
278
+ const priorityColor = PRIORITY_COLORS[task.priority] || null;
279
+ const priorityLabel = PRIORITY_LABELS[task.priority] || null;
280
+ const tags = getTaskTags(task);
281
+ const baseBranch = getTaskBaseBranch(task);
282
+
283
+ return html`
284
+ <div
285
+ class="kanban-card ${dragTaskId.value === task.id ? 'dragging' : ''} ${touchDragId.value === task.id && _touchMoved ? 'dragging' : ''}"
286
+ draggable="true"
287
+ onDragStart=${onDragStart}
288
+ onDragEnd=${onDragEnd}
289
+ onTouchStart=${onTouchStart}
290
+ onTouchMove=${onTouchMove}
291
+ onTouchEnd=${onTouchEnd}
292
+ onTouchCancel=${onTouchCancel}
293
+ onClick=${() => onOpen(task.id)}
294
+ >
295
+ ${priorityLabel && html`
296
+ <span class="kanban-card-badge" style="background:${priorityColor}">${priorityLabel}</span>
297
+ `}
298
+ <div class="kanban-card-title">${truncate(task.title || "(untitled)", 80)}</div>
299
+ ${task.description && html`
300
+ <div class="kanban-card-desc">${truncate(task.description, 72)}</div>
301
+ `}
302
+ ${baseBranch && html`
303
+ <div class="kanban-card-base">Base: <code>${truncate(baseBranch, 24)}</code></div>
304
+ `}
305
+ ${tags.length > 0 && html`
306
+ <div class="kanban-card-tags">
307
+ ${tags.map((tag) => html`<span class="tag-chip">#${tag}</span>`)}
308
+ </div>
309
+ `}
310
+ <div class="kanban-card-meta">
311
+ <span class="kanban-card-id">${typeof task.id === "string" ? truncate(task.id, 12) : task.id}</span>
312
+ ${task.created_at && html`<span>${formatRelative(task.created_at)}</span>`}
313
+ </div>
314
+ </div>
315
+ `;
316
+ }
317
+
318
+ /* ─── KanbanColumn ─── */
319
+ function KanbanColumn({ col, tasks, onOpen }) {
320
+ const [showCreate, setShowCreate] = useState(false);
321
+ const inputRef = useRef(null);
322
+
323
+ useEffect(() => {
324
+ if (showCreate && inputRef.current) inputRef.current.focus();
325
+ }, [showCreate]);
326
+
327
+ const onDragOver = useCallback((e) => {
328
+ e.preventDefault();
329
+ e.dataTransfer.dropEffect = "move";
330
+ dragOverCol.value = col.id;
331
+ }, [col.id]);
332
+
333
+ const onDragLeave = useCallback(() => {
334
+ if (dragOverCol.value === col.id) dragOverCol.value = null;
335
+ }, [col.id]);
336
+
337
+ const onDrop = useCallback(async (e) => {
338
+ e.preventDefault();
339
+ dragOverCol.value = null;
340
+ const taskId = e.dataTransfer.getData("text/plain") || dragTaskId.value;
341
+ dragTaskId.value = null;
342
+ if (!taskId) return;
343
+
344
+ const currentTask = (tasksData.value || []).find((t) => t.id === taskId);
345
+ if (!currentTask) return;
346
+ const currentCol = getColumnForStatus(currentTask.status);
347
+ if (currentCol === col.id) return;
348
+
349
+ const newStatus = COLUMN_TO_STATUS[col.id] || "todo";
350
+ haptic("medium");
351
+
352
+ const prev = cloneValue(tasksData.value);
353
+ try {
354
+ await runOptimistic(
355
+ () => {
356
+ tasksData.value = tasksData.value.map((t) =>
357
+ t.id === taskId ? { ...t, status: newStatus } : t,
358
+ );
359
+ },
360
+ async () => {
361
+ const res = await apiFetch("/api/tasks/update", {
362
+ method: "POST",
363
+ body: JSON.stringify({ taskId, status: newStatus }),
364
+ });
365
+ if (res?.data) {
366
+ tasksData.value = tasksData.value.map((t) =>
367
+ t.id === taskId ? { ...t, ...res.data } : t,
368
+ );
369
+ }
370
+ return res;
371
+ },
372
+ () => {
373
+ tasksData.value = prev;
374
+ },
375
+ );
376
+ showToast(`Moved to ${col.title}`, "success");
377
+ } catch {
378
+ /* toast via apiFetch */
379
+ }
380
+ }, [col.id, col.title]);
381
+
382
+ const handleInlineKeyDown = useCallback((e) => {
383
+ if (e.key === "Enter" && e.target.value.trim()) {
384
+ createTaskInColumn(COLUMN_TO_STATUS[col.id] || "todo", e.target.value.trim());
385
+ e.target.value = "";
386
+ setShowCreate(false);
387
+ }
388
+ if (e.key === "Escape") {
389
+ setShowCreate(false);
390
+ }
391
+ }, [col.id]);
392
+
393
+ const isOver = dragOverCol.value === col.id || touchOverCol.value === col.id;
394
+
395
+ return html`
396
+ <div
397
+ class="kanban-column ${isOver ? 'drag-over' : ''}"
398
+ data-col=${col.id}
399
+ onDragOver=${onDragOver}
400
+ onDragLeave=${onDragLeave}
401
+ onDrop=${onDrop}
402
+ >
403
+ <div class="kanban-column-header" style="border-bottom-color: ${col.color}">
404
+ <span>${col.icon}</span>
405
+ <span class="kanban-column-title">${col.title}</span>
406
+ <span class="kanban-count">${tasks.length}</span>
407
+ <button
408
+ class="kanban-add-btn"
409
+ onClick=${() => { setShowCreate(!showCreate); haptic(); }}
410
+ title="Add task to ${col.title}"
411
+ >+</button>
412
+ </div>
413
+ <div class="kanban-cards">
414
+ ${showCreate && html`
415
+ <input
416
+ ref=${inputRef}
417
+ class="kanban-inline-create"
418
+ placeholder="Task title…"
419
+ onKeyDown=${handleInlineKeyDown}
420
+ onBlur=${() => setShowCreate(false)}
421
+ />
422
+ `}
423
+ ${tasks.length
424
+ ? tasks.map((task) => html`
425
+ <${KanbanCard} key=${task.id} task=${task} onOpen=${onOpen} />
426
+ `)
427
+ : html`<div class="kanban-empty-col">Drop tasks here</div>`
428
+ }
429
+ </div>
430
+ <div class="kanban-scroll-fade"></div>
431
+ </div>
432
+ `;
433
+ }
434
+
435
+ /* ─── KanbanBoard (main export) ─── */
436
+ export function KanbanBoard({ onOpenTask }) {
437
+ const cols = columnData.value;
438
+
439
+ return html`
440
+ <div class="kanban-board">
441
+ ${COLUMNS.map((col) => html`
442
+ <${KanbanColumn}
443
+ key=${col.id}
444
+ col=${col}
445
+ tasks=${cols[col.id] || []}
446
+ onOpen=${onOpenTask}
447
+ />
448
+ `)}
449
+ </div>
450
+ `;
451
+ }