@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,410 @@
1
+ /**
2
+ * shared-knowledge.mjs — Agent-to-agent knowledge sharing for openfleet.
3
+ *
4
+ * Allows agents across the fleet to contribute lessons learned, patterns,
5
+ * and critical findings to a shared knowledge base (AGENTS.md or a
6
+ * designated knowledge file).
7
+ *
8
+ * Features:
9
+ * - Append-only knowledge entries with dedup
10
+ * - Structured entry format with metadata (agent, timestamp, scope)
11
+ * - Git-conflict-safe appending (append to dedicated section)
12
+ * - Rate limiting to prevent spam
13
+ * - Entry validation before write
14
+ *
15
+ * Knowledge entries are appended to a `## Agent Learnings` section at the
16
+ * bottom of the target file (default: AGENTS.md).
17
+ */
18
+
19
+ import { readFile, writeFile } from "node:fs/promises";
20
+ import { existsSync } from "node:fs";
21
+ import { resolve, basename } from "node:path";
22
+ import crypto from "node:crypto";
23
+
24
+ // ── Constants ────────────────────────────────────────────────────────────────
25
+
26
+ const DEFAULT_SECTION_HEADER = "## Agent Learnings";
27
+ const DEFAULT_TARGET_FILE = "AGENTS.md";
28
+ const ENTRY_SEPARATOR = "\n---\n";
29
+ const MAX_ENTRY_LENGTH = 2000; // chars
30
+ const MIN_ENTRY_LENGTH = 20; // chars
31
+ const RATE_LIMIT_MS = 30_000; // 30s between entries from same agent
32
+
33
+ // ── State ────────────────────────────────────────────────────────────────────
34
+
35
+ const knowledgeState = {
36
+ repoRoot: null,
37
+ targetFile: DEFAULT_TARGET_FILE,
38
+ sectionHeader: DEFAULT_SECTION_HEADER,
39
+ entriesWritten: 0,
40
+ lastWriteAt: null,
41
+ entryHashes: new Set(), // dedup
42
+ };
43
+
44
+ // ── Initialization ───────────────────────────────────────────────────────────
45
+
46
+ /**
47
+ * Initialize the shared knowledge system.
48
+ *
49
+ * @param {object} opts
50
+ * @param {string} opts.repoRoot - Git repository root
51
+ * @param {string} [opts.targetFile] - File to append to (default: AGENTS.md)
52
+ * @param {string} [opts.sectionHeader] - Markdown section header for learnings
53
+ */
54
+ export function initSharedKnowledge(opts = {}) {
55
+ knowledgeState.repoRoot = opts.repoRoot || process.cwd();
56
+ knowledgeState.targetFile = opts.targetFile || DEFAULT_TARGET_FILE;
57
+ knowledgeState.sectionHeader = opts.sectionHeader || DEFAULT_SECTION_HEADER;
58
+ }
59
+
60
+ // ── Entry Format ─────────────────────────────────────────────────────────────
61
+
62
+ /**
63
+ * Build a knowledge entry object.
64
+ *
65
+ * @param {object} opts
66
+ * @param {string} opts.content - The actual learning / insight
67
+ * @param {string} [opts.scope] - Area of the codebase (e.g., "veid", "market")
68
+ * @param {string} [opts.agentId] - Instance/agent identifier
69
+ * @param {string} [opts.agentType] - "codex" | "copilot" | "human"
70
+ * @param {string} [opts.category] - "pattern" | "gotcha" | "perf" | "security" | "convention"
71
+ * @param {string} [opts.taskRef] - VK task ID or branch name reference
72
+ * @returns {object} entry
73
+ */
74
+ export function buildKnowledgeEntry(opts = {}) {
75
+ const {
76
+ content,
77
+ scope = null,
78
+ agentId = "unknown",
79
+ agentType = "codex",
80
+ category = "pattern",
81
+ taskRef = null,
82
+ } = opts;
83
+
84
+ return {
85
+ content: String(content || "").trim(),
86
+ scope,
87
+ agentId,
88
+ agentType,
89
+ category,
90
+ taskRef,
91
+ timestamp: new Date().toISOString(),
92
+ hash: hashEntry(content, scope),
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Format a knowledge entry as Markdown for appending to file.
98
+ */
99
+ export function formatEntryAsMarkdown(entry) {
100
+ const lines = [];
101
+ const datePart =
102
+ entry.timestamp?.split("T")[0] || new Date().toISOString().split("T")[0];
103
+ const scopePart = entry.scope ? ` (${entry.scope})` : "";
104
+ const catPart = entry.category ? `[${entry.category}]` : "";
105
+ const taskPart = entry.taskRef ? ` • ref: \`${entry.taskRef}\`` : "";
106
+
107
+ lines.push(`### ${catPart}${scopePart} — ${datePart}${taskPart}`);
108
+ lines.push("");
109
+ lines.push(`> **Agent:** ${entry.agentId} (${entry.agentType})`);
110
+ lines.push("");
111
+ lines.push(entry.content);
112
+ lines.push("");
113
+
114
+ return lines.join("\n");
115
+ }
116
+
117
+ // ── Validation ───────────────────────────────────────────────────────────────
118
+
119
+ /**
120
+ * Validate a knowledge entry before writing.
121
+ * Returns { valid: boolean, reason?: string }.
122
+ */
123
+ export function validateEntry(entry) {
124
+ if (!entry || typeof entry !== "object") {
125
+ return { valid: false, reason: "entry must be an object" };
126
+ }
127
+
128
+ const content = String(entry.content || "").trim();
129
+ if (content.length < MIN_ENTRY_LENGTH) {
130
+ return {
131
+ valid: false,
132
+ reason: `content too short (min ${MIN_ENTRY_LENGTH} chars)`,
133
+ };
134
+ }
135
+ if (content.length > MAX_ENTRY_LENGTH) {
136
+ return {
137
+ valid: false,
138
+ reason: `content too long (max ${MAX_ENTRY_LENGTH} chars)`,
139
+ };
140
+ }
141
+
142
+ // Check for obviously low-value entries
143
+ const lowValuePatterns = [
144
+ /^(ok|done|yes|no|maybe|test|todo|fixme|hack)$/i,
145
+ /^[^a-zA-Z]*$/, // no letters at all
146
+ /(.)\1{20,}/, // 20+ repeated chars
147
+ ];
148
+ for (const pat of lowValuePatterns) {
149
+ if (pat.test(content)) {
150
+ return { valid: false, reason: "entry appears to be low-value or noise" };
151
+ }
152
+ }
153
+
154
+ // Validate category
155
+ const validCategories = [
156
+ "pattern",
157
+ "gotcha",
158
+ "perf",
159
+ "security",
160
+ "convention",
161
+ "tip",
162
+ "bug",
163
+ ];
164
+ if (entry.category && !validCategories.includes(entry.category)) {
165
+ return {
166
+ valid: false,
167
+ reason: `invalid category — must be one of: ${validCategories.join(", ")}`,
168
+ };
169
+ }
170
+
171
+ return { valid: true };
172
+ }
173
+
174
+ // ── Deduplication ────────────────────────────────────────────────────────────
175
+
176
+ function hashEntry(content, scope) {
177
+ const data = `${scope || ""}|${String(content || "")
178
+ .trim()
179
+ .toLowerCase()}`;
180
+ return crypto.createHash("sha256").update(data).digest("hex").slice(0, 16);
181
+ }
182
+
183
+ /**
184
+ * Check if an entry with this content already exists (in-memory dedup).
185
+ */
186
+ export function isDuplicate(entry) {
187
+ return knowledgeState.entryHashes.has(entry.hash);
188
+ }
189
+
190
+ /**
191
+ * Load existing entry hashes from the target file for dedup.
192
+ */
193
+ async function loadExistingHashes() {
194
+ const filePath = resolve(knowledgeState.repoRoot, knowledgeState.targetFile);
195
+ if (!existsSync(filePath)) return;
196
+
197
+ try {
198
+ const content = await readFile(filePath, "utf8");
199
+ // Extract content blocks from Agent Learnings section
200
+ const sectionIdx = content.indexOf(knowledgeState.sectionHeader);
201
+ if (sectionIdx === -1) return;
202
+
203
+ const sectionContent = content.slice(sectionIdx);
204
+ // Parse each learning entry (between ### headers)
205
+ const entries = sectionContent.split(/^### /m).slice(1);
206
+ for (const block of entries) {
207
+ // Extract the content after the metadata lines
208
+ const lines = block.split("\n");
209
+ const contentLines = lines.filter(
210
+ (l) =>
211
+ !l.startsWith(">") &&
212
+ !l.startsWith("###") &&
213
+ !l.startsWith("---") &&
214
+ l.trim().length > 0,
215
+ );
216
+ const entryContent = contentLines.join(" ").trim();
217
+ if (entryContent) {
218
+ // Extract scope from header if present
219
+ const scopeMatch = lines[0]?.match(/\(([^)]+)\)/);
220
+ const scope = scopeMatch?.[1] || null;
221
+ const hash = hashEntry(entryContent, scope);
222
+ knowledgeState.entryHashes.add(hash);
223
+ }
224
+ }
225
+ } catch {
226
+ // file read error — skip
227
+ }
228
+ }
229
+
230
+ // ── Write ────────────────────────────────────────────────────────────────────
231
+
232
+ /**
233
+ * Append a knowledge entry to the target file.
234
+ *
235
+ * @param {object} entry - From buildKnowledgeEntry()
236
+ * @returns {object} { success: boolean, reason?: string }
237
+ */
238
+ export async function appendKnowledgeEntry(entry) {
239
+ // Validate
240
+ const validation = validateEntry(entry);
241
+ if (!validation.valid) {
242
+ return { success: false, reason: validation.reason };
243
+ }
244
+
245
+ // Rate limit
246
+ if (knowledgeState.lastWriteAt) {
247
+ const elapsed = Date.now() - knowledgeState.lastWriteAt;
248
+ if (elapsed < RATE_LIMIT_MS) {
249
+ return {
250
+ success: false,
251
+ reason: `rate limited — wait ${Math.ceil((RATE_LIMIT_MS - elapsed) / 1000)}s`,
252
+ };
253
+ }
254
+ }
255
+
256
+ // Dedup check
257
+ await loadExistingHashes();
258
+ if (isDuplicate(entry)) {
259
+ return { success: false, reason: "duplicate entry — already recorded" };
260
+ }
261
+
262
+ // Format
263
+ const markdown = formatEntryAsMarkdown(entry);
264
+
265
+ // Append to file
266
+ const filePath = resolve(knowledgeState.repoRoot, knowledgeState.targetFile);
267
+ try {
268
+ let content = "";
269
+ if (existsSync(filePath)) {
270
+ content = await readFile(filePath, "utf8");
271
+ }
272
+
273
+ // Find or create the section
274
+ const sectionIdx = content.indexOf(knowledgeState.sectionHeader);
275
+ if (sectionIdx === -1) {
276
+ // Append section at end of file
277
+ const newContent =
278
+ content.trimEnd() +
279
+ "\n\n" +
280
+ knowledgeState.sectionHeader +
281
+ "\n\n" +
282
+ markdown +
283
+ ENTRY_SEPARATOR;
284
+ await writeFile(filePath, newContent, "utf8");
285
+ } else {
286
+ // Append at end of existing section (before any next ## header or EOF)
287
+
288
+ const afterSection = content.slice(
289
+ sectionIdx + knowledgeState.sectionHeader.length,
290
+ );
291
+ // Find next top-level heading (## but not ###)
292
+ const nextSectionMatch = afterSection.match(/\n## [^#]/);
293
+ if (nextSectionMatch) {
294
+ const insertPos =
295
+
296
+ sectionIdx +
297
+ knowledgeState.sectionHeader.length +
298
+ nextSectionMatch.index;
299
+ const before = content.slice(0, insertPos);
300
+ const after = content.slice(insertPos);
301
+ await writeFile(
302
+ filePath,
303
+ before + "\n" + markdown + ENTRY_SEPARATOR + after,
304
+ "utf8",
305
+ );
306
+ } else {
307
+ // Append at end of file
308
+ await writeFile(
309
+ filePath,
310
+ content.trimEnd() + "\n\n" + markdown + ENTRY_SEPARATOR,
311
+ "utf8",
312
+ );
313
+ }
314
+ }
315
+
316
+ // Track
317
+ knowledgeState.entryHashes.add(entry.hash);
318
+ knowledgeState.entriesWritten++;
319
+ knowledgeState.lastWriteAt = Date.now();
320
+
321
+ return { success: true, hash: entry.hash };
322
+ } catch (err) {
323
+ return { success: false, reason: `write error: ${err.message}` };
324
+ }
325
+ }
326
+
327
+ // ── Read ─────────────────────────────────────────────────────────────────────
328
+
329
+ /**
330
+ * Read all knowledge entries from the target file.
331
+ * Returns structured entries, not raw markdown.
332
+ */
333
+ export async function readKnowledgeEntries() {
334
+ const filePath = resolve(knowledgeState.repoRoot, knowledgeState.targetFile);
335
+ if (!existsSync(filePath)) return [];
336
+
337
+ try {
338
+ const content = await readFile(filePath, "utf8");
339
+ const sectionIdx = content.indexOf(knowledgeState.sectionHeader);
340
+ if (sectionIdx === -1) return [];
341
+
342
+
343
+ const sectionContent = content.slice(
344
+ sectionIdx + knowledgeState.sectionHeader.length,
345
+ );
346
+ // Find next top-level heading
347
+ const nextSectionMatch = sectionContent.match(/\n## [^#]/);
348
+ const relevantContent = nextSectionMatch
349
+ ? sectionContent.slice(0, nextSectionMatch.index)
350
+ : sectionContent;
351
+
352
+ const blocks = relevantContent.split(/^### /m).slice(1);
353
+ const entries = [];
354
+
355
+ for (const block of blocks) {
356
+ const lines = block.split("\n");
357
+ const header = lines[0] || "";
358
+
359
+ // Parse header: [category](scope) — date • ref: `taskRef`
360
+ const catMatch = header.match(/^\[([^\]]+)\]/);
361
+ const scopeMatch = header.match(/\(([^)]+)\)/);
362
+ const dateMatch = header.match(/(\d{4}-\d{2}-\d{2})/);
363
+ const refMatch = header.match(/ref: `([^`]+)`/);
364
+
365
+ // Parse agent line
366
+ const agentLine = lines.find((l) => l.startsWith("> **Agent:**"));
367
+ const agentMatch = agentLine?.match(/\*\*Agent:\*\* ([^ ]+) \(([^)]+)\)/);
368
+
369
+ // Extract content
370
+
371
+ const contentLines = lines
372
+ .filter(
373
+ (l) =>
374
+ !l.startsWith(">") && l.trim().length > 0 && !l.startsWith("---"),
375
+ )
376
+ .slice(1); // skip header line
377
+
378
+ entries.push({
379
+ category: catMatch?.[1] || "unknown",
380
+ scope: scopeMatch?.[1] || null,
381
+ date: dateMatch?.[1] || null,
382
+ taskRef: refMatch?.[1] || null,
383
+ agentId: agentMatch?.[1] || "unknown",
384
+ agentType: agentMatch?.[2] || "unknown",
385
+ content: contentLines.join("\n").trim(),
386
+ });
387
+ }
388
+
389
+ return entries;
390
+ } catch {
391
+ return [];
392
+ }
393
+ }
394
+
395
+ // ── Getters ──────────────────────────────────────────────────────────────────
396
+
397
+ export function getKnowledgeState() {
398
+ return { ...knowledgeState, entryHashes: knowledgeState.entryHashes.size };
399
+ }
400
+
401
+ export function formatKnowledgeSummary() {
402
+ return [
403
+ `📚 Shared Knowledge: ${knowledgeState.entriesWritten} entries written this session`,
404
+ `Target: ${knowledgeState.targetFile}`,
405
+ `Dedup cache: ${knowledgeState.entryHashes.size} hashes`,
406
+ knowledgeState.lastWriteAt
407
+ ? `Last write: ${new Date(knowledgeState.lastWriteAt).toISOString()}`
408
+ : "No writes this session",
409
+ ].join("\n");
410
+ }