@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,557 @@
1
+ /**
2
+ * review-agent.mjs — Automated Code Review for Vibe-Kanban Tasks
3
+ *
4
+ * Reviews PRs when tasks move to "inreview" status.
5
+ * Only flags CRITICAL issues: security, bugs, broken functionality,
6
+ * missing implementations. Ignores style, naming, minor quality concerns.
7
+ *
8
+ * @module review-agent
9
+ */
10
+
11
+ import { spawnSync } from "node:child_process";
12
+ import { execWithRetry, getPoolSdkName } from "./agent-pool.mjs";
13
+ import { loadConfig } from "./config.mjs";
14
+ import { resolvePromptTemplate } from "./agent-prompts.mjs";
15
+
16
+ const TAG = "[review-agent]";
17
+
18
+ /** Maximum diff size before truncation (characters). */
19
+ const MAX_DIFF_CHARS = 50_000;
20
+
21
+ /** Default review timeout: 5 minutes. */
22
+ const DEFAULT_REVIEW_TIMEOUT_MS = 5 * 60 * 1000;
23
+
24
+ /** Default max concurrent reviews. */
25
+ const DEFAULT_MAX_CONCURRENT = 2;
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Review Prompt
29
+ // ---------------------------------------------------------------------------
30
+
31
+ /**
32
+ * Build the structured review prompt.
33
+ * @param {string} diff - PR diff content
34
+ * @param {string} taskDescription - Task description for context
35
+ * @param {string} [template]
36
+ * @returns {string}
37
+ */
38
+ function buildReviewPrompt(diff, taskDescription, template) {
39
+ const fallback = `You are a senior code reviewer for this software project.
40
+
41
+ Review the following PR diff for CRITICAL issues ONLY:
42
+
43
+ ## What to flag (ONLY these categories):
44
+ 1. **Security vulnerabilities** - injection, auth bypass, key exposure, unsafe crypto
45
+ 2. **Bugs** - logic errors, nil pointer dereferences, race conditions, data corruption
46
+ 3. **Missing implementations** - placeholder/stub code, TODO comments left in, empty function bodies
47
+ 4. **Broken functionality** - code that won't compile, tests that fail, broken imports
48
+
49
+ ## What to IGNORE (do NOT flag):
50
+ - Code style or formatting
51
+ - Variable naming conventions
52
+ - Minor code quality improvements
53
+ - Missing comments or documentation
54
+ - Performance optimizations (unless critical)
55
+ - Test coverage gaps (unless zero tests for critical code)
56
+
57
+ ## PR Diff:
58
+ \`\`\`diff
59
+ ${diff}
60
+ \`\`\`
61
+
62
+ ## Task Description:
63
+ ${taskDescription || "(no description provided)"}
64
+
65
+ ## Response Format:
66
+ Respond with ONLY a JSON object (no markdown, no explanation):
67
+ {
68
+ "verdict": "approved" | "changes_requested",
69
+ "issues": [
70
+ {
71
+ "severity": "critical" | "major",
72
+ "category": "security" | "bug" | "missing_impl" | "broken",
73
+ "file": "path/to/file",
74
+ "line": 123,
75
+ "description": "What's wrong and why it matters"
76
+ }
77
+ ],
78
+ "summary": "One sentence overall assessment"
79
+ }
80
+
81
+ If no critical issues found, return:
82
+ {"verdict": "approved", "issues": [], "summary": "No critical issues found"}`;
83
+ return resolvePromptTemplate(
84
+ template,
85
+ {
86
+ DIFF: diff,
87
+ TASK_DESCRIPTION: taskDescription || "(no description provided)",
88
+ },
89
+ fallback,
90
+ );
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Diff Retrieval
95
+ // ---------------------------------------------------------------------------
96
+
97
+ /**
98
+ * Extract PR number from a PR URL.
99
+ * @param {string} prUrl
100
+ * @returns {number|null}
101
+ */
102
+ function extractPrNumber(prUrl) {
103
+ if (!prUrl) return null;
104
+ const m = prUrl.match(/\/pull\/(\d+)/);
105
+ return m ? Number(m[1]) : null;
106
+ }
107
+
108
+ /**
109
+ * Extract repo slug (owner/repo) from a PR URL.
110
+ * @param {string} prUrl
111
+ * @returns {string|null}
112
+ */
113
+ function extractRepoSlug(prUrl) {
114
+ if (!prUrl) return null;
115
+ const m = prUrl.match(/github\.com\/([^/]+\/[^/]+)\/pull\//);
116
+ return m ? m[1] : null;
117
+ }
118
+
119
+ /**
120
+ * Get the PR diff using `gh pr diff` or `git diff`.
121
+ * @param {{ prUrl?: string, branchName?: string }} opts
122
+ * @returns {{ diff: string, truncated: boolean }}
123
+ */
124
+ function getPrDiff({ prUrl, branchName }) {
125
+ let diff = "";
126
+
127
+ // Strategy 1: gh pr diff
128
+ const prNumber = extractPrNumber(prUrl);
129
+ const repoSlug = extractRepoSlug(prUrl);
130
+ if (prNumber && repoSlug) {
131
+ try {
132
+ const result = spawnSync(
133
+ "gh",
134
+ ["pr", "diff", String(prNumber), "--repo", repoSlug],
135
+ { encoding: "utf8", timeout: 30_000, stdio: ["pipe", "pipe", "pipe"] },
136
+ );
137
+ if (result.status === 0 && result.stdout?.trim()) {
138
+ diff = result.stdout;
139
+ }
140
+ } catch {
141
+ /* fall through to git diff */
142
+ }
143
+ }
144
+
145
+ // Strategy 2: git diff main...<branch>
146
+ if (!diff && branchName) {
147
+ try {
148
+ const result = spawnSync("git", ["diff", `main...${branchName}`], {
149
+ encoding: "utf8",
150
+ timeout: 30_000,
151
+ stdio: ["pipe", "pipe", "pipe"],
152
+ });
153
+ if (result.status === 0 && result.stdout?.trim()) {
154
+ diff = result.stdout;
155
+ }
156
+ } catch {
157
+ /* ignore */
158
+ }
159
+ }
160
+
161
+ // Truncate if needed
162
+ let truncated = false;
163
+ if (diff.length > MAX_DIFF_CHARS) {
164
+ diff = diff.slice(0, MAX_DIFF_CHARS);
165
+ truncated = true;
166
+ }
167
+
168
+ return { diff, truncated };
169
+ }
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // Result Parsing
173
+ // ---------------------------------------------------------------------------
174
+
175
+ /**
176
+ * Parse the review JSON from agent output.
177
+ * Handles markdown fences, surrounding text, and invalid JSON gracefully.
178
+ * @param {string} raw
179
+ * @returns {{ approved: boolean, issues: Array, summary: string }}
180
+ */
181
+ function parseReviewResult(raw) {
182
+ if (!raw || !raw.trim()) {
183
+ return {
184
+ approved: true,
185
+ issues: [],
186
+ summary: "Empty agent output — auto-approved",
187
+ };
188
+ }
189
+
190
+ let text = raw.trim();
191
+
192
+ // Strip markdown code fences
193
+ const fenceMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
194
+ if (fenceMatch) {
195
+ text = fenceMatch[1].trim();
196
+ }
197
+
198
+ // Try direct JSON parse
199
+ try {
200
+ const parsed = JSON.parse(text);
201
+ return normalizeResult(parsed);
202
+ } catch {
203
+ /* continue to regex extraction */
204
+ }
205
+
206
+ // Extract first JSON object from text
207
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
208
+ if (jsonMatch) {
209
+ try {
210
+ const parsed = JSON.parse(jsonMatch[0]);
211
+ return normalizeResult(parsed);
212
+ } catch {
213
+ /* fall through */
214
+ }
215
+ }
216
+
217
+ // Couldn't parse — auto-approve with note
218
+ return {
219
+ approved: true,
220
+ issues: [],
221
+ summary: "Could not parse review output — auto-approved",
222
+ };
223
+ }
224
+
225
+ /**
226
+ * Normalize a parsed review object.
227
+ * @param {Object} obj
228
+ * @returns {{ approved: boolean, issues: Array, summary: string }}
229
+ */
230
+ function normalizeResult(obj) {
231
+ const approved = obj.verdict !== "changes_requested";
232
+ const issues = Array.isArray(obj.issues) ? obj.issues : [];
233
+ const summary = typeof obj.summary === "string" ? obj.summary : "";
234
+ return { approved, issues, summary };
235
+ }
236
+
237
+ // ---------------------------------------------------------------------------
238
+ // ReviewAgent Class
239
+ // ---------------------------------------------------------------------------
240
+
241
+ export class ReviewAgent {
242
+ /** @type {Map<string, Promise>} */
243
+ #activeReviews = new Map();
244
+
245
+ /** @type {Array<{ id: string, title: string, branchName: string, prUrl: string, description: string }>} */
246
+ #queue = [];
247
+
248
+ /** @type {Set<string>} - task IDs already reviewed or in-flight */
249
+ #seen = new Set();
250
+
251
+ #completedCount = 0;
252
+ #running = false;
253
+ #processing = false;
254
+
255
+ /** @type {string} */
256
+ #sdk;
257
+
258
+ /** @type {string|undefined} */
259
+ #model;
260
+
261
+ /** @type {number} */
262
+ #maxConcurrent;
263
+
264
+ /** @type {number} */
265
+ #reviewTimeoutMs;
266
+
267
+ /** @type {Function|undefined} */
268
+ #onReviewComplete;
269
+
270
+ /** @type {Function|undefined} */
271
+ #sendTelegram;
272
+
273
+ /** @type {string|undefined} */
274
+ #promptTemplate;
275
+
276
+ /**
277
+ * @param {Object} [options]
278
+ * @param {string} [options.sdk]
279
+ * @param {string} [options.model]
280
+ * @param {number} [options.maxConcurrentReviews]
281
+ * @param {number} [options.reviewTimeoutMs]
282
+ * @param {Function} [options.onReviewComplete]
283
+ * @param {Function} [options.sendTelegram]
284
+ * @param {string} [options.promptTemplate]
285
+ */
286
+ constructor(options = {}) {
287
+ this.#sdk = options.sdk || getPoolSdkName();
288
+ this.#model = options.model;
289
+ this.#maxConcurrent =
290
+ options.maxConcurrentReviews ?? DEFAULT_MAX_CONCURRENT;
291
+ this.#reviewTimeoutMs =
292
+ options.reviewTimeoutMs ?? DEFAULT_REVIEW_TIMEOUT_MS;
293
+ this.#onReviewComplete = options.onReviewComplete;
294
+ this.#sendTelegram = options.sendTelegram;
295
+ this.#promptTemplate = options.promptTemplate;
296
+ console.log(
297
+ `${TAG} initialized (sdk=${this.#sdk}, maxConcurrent=${this.#maxConcurrent}, timeout=${this.#reviewTimeoutMs}ms)`,
298
+ );
299
+ }
300
+
301
+ /**
302
+ * Queue a task for review.
303
+ * Deduplicates by task ID — same task won't be reviewed twice.
304
+ * @param {{ id: string, title: string, branchName: string, prUrl: string, description: string }} task
305
+ */
306
+ async queueReview(task) {
307
+ if (!task?.id) {
308
+ console.warn(`${TAG} queueReview called without task id — skipping`);
309
+ return;
310
+ }
311
+
312
+ if (this.#seen.has(task.id)) {
313
+ console.log(`${TAG} task ${task.id} already reviewed/queued — skipping`);
314
+ return;
315
+ }
316
+
317
+ this.#seen.add(task.id);
318
+ this.#queue.push(task);
319
+ console.log(
320
+ `${TAG} queued review for task "${task.title}" (${task.id}), queue depth: ${this.#queue.length}`,
321
+ );
322
+
323
+ // Kick processing if running
324
+ if (this.#running) {
325
+ this.#processQueue();
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Cancel a pending (not yet started) review.
331
+ * Active reviews cannot be cancelled.
332
+ * @param {string} taskId
333
+ */
334
+ cancelReview(taskId) {
335
+ const idx = this.#queue.findIndex((t) => t.id === taskId);
336
+ if (idx !== -1) {
337
+ this.#queue.splice(idx, 1);
338
+ this.#seen.delete(taskId);
339
+ console.log(`${TAG} cancelled queued review for task ${taskId}`);
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Get current review-agent status.
345
+ * @returns {{ running: boolean, sdk: string, activeReviews: number, queuedReviews: number, completedReviews: number }}
346
+ */
347
+ getStatus() {
348
+ return {
349
+ running: this.#running,
350
+ sdk: this.#sdk,
351
+ activeReviews: this.#activeReviews.size,
352
+ queuedReviews: this.#queue.length,
353
+ completedReviews: this.#completedCount,
354
+ };
355
+ }
356
+
357
+ /** Start processing the review queue. */
358
+ start() {
359
+ this.#running = true;
360
+ console.log(`${TAG} started`);
361
+ this.#processQueue();
362
+ }
363
+
364
+ /**
365
+ * Stop gracefully — waits for active reviews to finish.
366
+ * @returns {Promise<void>}
367
+ */
368
+ async stop() {
369
+ this.#running = false;
370
+ console.log(
371
+ `${TAG} stopping — waiting for ${this.#activeReviews.size} active review(s)`,
372
+ );
373
+ if (this.#activeReviews.size > 0) {
374
+ await Promise.allSettled([...this.#activeReviews.values()]);
375
+ }
376
+ console.log(`${TAG} stopped`);
377
+ }
378
+
379
+ // ── Private ──────────────────────────────────────────────────────────────
380
+
381
+ /** Process queued reviews up to concurrency limit. */
382
+ #processQueue() {
383
+ if (this.#processing || !this.#running) return;
384
+ this.#processing = true;
385
+
386
+ try {
387
+ while (
388
+ this.#queue.length > 0 &&
389
+ this.#activeReviews.size < this.#maxConcurrent
390
+ ) {
391
+ const task = this.#queue.shift();
392
+ const promise = this.#runReview(task)
393
+ .catch((err) => {
394
+ console.error(`${TAG} unhandled review error for ${task.id}:`, err);
395
+ })
396
+ .finally(() => {
397
+ this.#activeReviews.delete(task.id);
398
+ this.#completedCount++;
399
+ // Continue processing after slot freed
400
+ if (this.#running && this.#queue.length > 0) {
401
+ this.#processQueue();
402
+ }
403
+ });
404
+
405
+ this.#activeReviews.set(task.id, promise);
406
+ }
407
+ } finally {
408
+ this.#processing = false;
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Run a single review.
414
+ * @param {{ id: string, title: string, branchName: string, prUrl: string, description: string }} task
415
+ */
416
+ async #runReview(task) {
417
+ console.log(`${TAG} starting review for "${task.title}" (${task.id})`);
418
+
419
+ // 1. Get PR diff
420
+ const { diff, truncated } = getPrDiff({
421
+ prUrl: task.prUrl,
422
+ branchName: task.branchName,
423
+ });
424
+
425
+ if (!diff) {
426
+ console.log(
427
+ `${TAG} no diff available for task ${task.id} — auto-approving`,
428
+ );
429
+ const result = {
430
+ approved: true,
431
+ issues: [],
432
+ summary: "No diff available",
433
+ reviewedAt: new Date().toISOString(),
434
+ agentOutput: "",
435
+ };
436
+ this.#emitResult(task.id, result);
437
+ return;
438
+ }
439
+
440
+ if (truncated) {
441
+ console.warn(
442
+ `${TAG} diff for task ${task.id} truncated to ${MAX_DIFF_CHARS} chars`,
443
+ );
444
+ }
445
+
446
+ // 2. Build prompt
447
+ const prompt = buildReviewPrompt(
448
+ diff,
449
+ task.description,
450
+ this.#promptTemplate,
451
+ );
452
+
453
+ // 3. Run agent
454
+ let agentOutput = "";
455
+ try {
456
+ const sdkResult = await execWithRetry(prompt, {
457
+ taskKey: `review-${task.id}`,
458
+ timeoutMs: this.#reviewTimeoutMs,
459
+ maxRetries: 0, // Reviews don't retry — approve on failure
460
+ sdk: this.#sdk,
461
+ });
462
+
463
+ agentOutput = sdkResult.output || "";
464
+ } catch (err) {
465
+ console.error(`${TAG} SDK call failed for task ${task.id}:`, err.message);
466
+ const result = {
467
+ approved: true,
468
+ issues: [],
469
+ summary: `Review failed: ${err.message}`,
470
+ reviewedAt: new Date().toISOString(),
471
+ agentOutput: "",
472
+ };
473
+ this.#emitResult(task.id, result);
474
+ return;
475
+ }
476
+
477
+ // 4. Parse result
478
+ const parsed = parseReviewResult(agentOutput);
479
+
480
+ const result = {
481
+ approved: parsed.approved,
482
+ issues: parsed.issues,
483
+ summary: parsed.summary + (truncated ? " (diff was truncated)" : ""),
484
+ reviewedAt: new Date().toISOString(),
485
+ agentOutput: agentOutput.slice(0, 3000),
486
+ };
487
+
488
+ console.log(
489
+ `${TAG} review complete for "${task.title}": ${result.approved ? "APPROVED" : "CHANGES REQUESTED"} — ${result.issues.length} issue(s)`,
490
+ );
491
+
492
+ // 5. Report
493
+ this.#emitResult(task.id, result);
494
+ }
495
+
496
+ /**
497
+ * Emit review result via callback and optional Telegram notification.
498
+ * @param {string} taskId
499
+ * @param {Object} result
500
+ */
501
+ #emitResult(taskId, result) {
502
+ if (typeof this.#onReviewComplete === "function") {
503
+ try {
504
+ this.#onReviewComplete(taskId, result);
505
+ } catch (err) {
506
+ console.error(`${TAG} onReviewComplete callback error:`, err.message);
507
+ }
508
+ }
509
+
510
+ // Send Telegram for rejected reviews
511
+ if (!result.approved && typeof this.#sendTelegram === "function") {
512
+ const issueList = result.issues
513
+ .map(
514
+ (i) =>
515
+ `• [${i.severity}/${i.category}] ${i.file}${i.line ? `:${i.line}` : ""} — ${i.description}`,
516
+ )
517
+ .join("\n");
518
+
519
+ const message = [
520
+ `🔍 Review: changes requested`,
521
+ `Task: ${taskId}`,
522
+ `Summary: ${result.summary}`,
523
+ result.issues.length ? `\nIssues:\n${issueList}` : "",
524
+ ]
525
+ .filter(Boolean)
526
+ .join("\n");
527
+
528
+ try {
529
+ this.#sendTelegram(message);
530
+ } catch {
531
+ /* best effort */
532
+ }
533
+ }
534
+ }
535
+ }
536
+
537
+ // ---------------------------------------------------------------------------
538
+ // Factory
539
+ // ---------------------------------------------------------------------------
540
+
541
+ /**
542
+ * Create a ReviewAgent instance.
543
+ * @param {Object} [options] - Same options as ReviewAgent constructor
544
+ * @returns {ReviewAgent}
545
+ */
546
+ export function createReviewAgent(options) {
547
+ let promptTemplate = options?.promptTemplate;
548
+ if (!promptTemplate) {
549
+ try {
550
+ const config = loadConfig();
551
+ promptTemplate = config.agentPrompts?.reviewer;
552
+ } catch {
553
+ /* best effort */
554
+ }
555
+ }
556
+ return new ReviewAgent({ ...(options || {}), promptTemplate });
557
+ }
@@ -0,0 +1,133 @@
1
+ #!/bin/bash
2
+ #
3
+ # Agent Work Log Rotation Script
4
+ #
5
+ # Rotates and compresses agent work logs to prevent unbounded growth.
6
+ # Intended to run weekly via cron or manually.
7
+ #
8
+ # Usage: bash scripts/openfleet/rotate-agent-logs.sh
9
+
10
+ set -e
11
+
12
+ # ── Configuration ───────────────────────────────────────────────────────────
13
+
14
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
15
+ LOG_DIR="$REPO_ROOT/.cache/agent-work-logs"
16
+ ARCHIVE_DIR="$LOG_DIR/archive"
17
+
18
+ # Retention periods
19
+ STREAM_RETENTION_DAYS=30
20
+ ERROR_RETENTION_DAYS=90
21
+ SESSION_RETENTION_COUNT=100
22
+ ARCHIVE_RETENTION_DAYS=180
23
+
24
+ # ── Functions ───────────────────────────────────────────────────────────────
25
+
26
+ log() {
27
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
28
+ }
29
+
30
+ ensure_dir() {
31
+ if [ ! -d "$1" ]; then
32
+ mkdir -p "$1"
33
+ log "Created directory: $1"
34
+ fi
35
+ }
36
+
37
+ rotate_file() {
38
+ local file="$1"
39
+ local archive_name="$2"
40
+ local retention_days="$3"
41
+
42
+ if [ ! -f "$file" ]; then
43
+ log "Skipping $file (not found)"
44
+ return
45
+ fi
46
+
47
+ local size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null || echo 0)
48
+ if [ "$size" -eq 0 ]; then
49
+ log "Skipping $file (empty)"
50
+ return
51
+ fi
52
+
53
+ # Compress and archive
54
+ log "Archiving $file → $archive_name ($(numfmt --to=iec-i --suffix=B $size))"
55
+ gzip -c "$file" > "$ARCHIVE_DIR/$archive_name"
56
+
57
+ # Truncate original file
58
+ > "$file"
59
+ log "Truncated $file"
60
+
61
+ # Clean old archives
62
+ if [ -n "$retention_days" ]; then
63
+ find "$ARCHIVE_DIR" -name "$(basename "$archive_name" .gz)*" -type f -mtime +$retention_days -delete
64
+ log "Cleaned archives older than $retention_days days"
65
+ fi
66
+ }
67
+
68
+ clean_sessions() {
69
+ local session_dir="$LOG_DIR/agent-sessions"
70
+ local retention_count=$1
71
+
72
+ if [ ! -d "$session_dir" ]; then
73
+ return
74
+ fi
75
+
76
+ local session_count=$(find "$session_dir" -name "*.jsonl" -type f | wc -l)
77
+ if [ "$session_count" -le "$retention_count" ]; then
78
+ log "Session logs: $session_count/$retention_count (within limit)"
79
+ return
80
+ fi
81
+
82
+ # Delete oldest sessions beyond retention limit
83
+ log "Cleaning old session logs (keeping $retention_count newest)"
84
+ ls -t "$session_dir"/*.jsonl | tail -n +$((retention_count + 1)) | xargs rm -f
85
+ log "Deleted $((session_count - retention_count)) old session logs"
86
+ }
87
+
88
+ # ── Main ────────────────────────────────────────────────────────────────────
89
+
90
+ log "Starting agent work log rotation"
91
+
92
+ # Ensure directories exist
93
+ ensure_dir "$LOG_DIR"
94
+ ensure_dir "$ARCHIVE_DIR"
95
+
96
+ # Rotate main stream log
97
+ if [ -f "$LOG_DIR/agent-work-stream.jsonl" ]; then
98
+ STREAM_ARCHIVE="agent-work-stream-$(date +%Y%m%d).jsonl.gz"
99
+ rotate_file "$LOG_DIR/agent-work-stream.jsonl" "$STREAM_ARCHIVE" "$STREAM_RETENTION_DAYS"
100
+ fi
101
+
102
+ # Rotate error log
103
+ if [ -f "$LOG_DIR/agent-errors.jsonl" ]; then
104
+ ERROR_ARCHIVE="agent-errors-$(date +%Y%m%d).jsonl.gz"
105
+ rotate_file "$LOG_DIR/agent-errors.jsonl" "$ERROR_ARCHIVE" "$ERROR_RETENTION_DAYS"
106
+ fi
107
+
108
+ # Rotate alerts log
109
+ if [ -f "$LOG_DIR/agent-alerts.jsonl" ]; then
110
+ ALERTS_ARCHIVE="agent-alerts-$(date +%Y%m%d).jsonl.gz"
111
+ rotate_file "$LOG_DIR/agent-alerts.jsonl" "$ALERTS_ARCHIVE" "$STREAM_RETENTION_DAYS"
112
+ fi
113
+
114
+ # Metrics log is kept indefinitely (compressed monthly)
115
+ if [ -f "$LOG_DIR/agent-metrics.jsonl" ]; then
116
+ # Only rotate on first day of month
117
+ if [ "$(date +%d)" = "01" ]; then
118
+ METRICS_ARCHIVE="agent-metrics-$(date -d 'last month' +%Y%m).jsonl.gz"
119
+ rotate_file "$LOG_DIR/agent-metrics.jsonl" "$METRICS_ARCHIVE" ""
120
+ fi
121
+ fi
122
+
123
+ # Clean old session logs
124
+ clean_sessions "$SESSION_RETENTION_COUNT"
125
+
126
+ # Archive statistics
127
+ if [ -d "$ARCHIVE_DIR" ]; then
128
+ archive_count=$(find "$ARCHIVE_DIR" -name "*.gz" -type f | wc -l)
129
+ archive_size=$(du -sh "$ARCHIVE_DIR" 2>/dev/null | cut -f1 || echo "0")
130
+ log "Archive directory: $archive_count files, $archive_size total"
131
+ fi
132
+
133
+ log "Agent work log rotation completed"