@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,613 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve, dirname, isAbsolute, relative, join } from "node:path";
3
+ import { execSync, spawnSync } from "node:child_process";
4
+ import { fileURLToPath } from "node:url";
5
+ import { homedir } from "node:os";
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const CONFIG_FILES = [
9
+ "openfleet.config.json",
10
+ ".openfleet.json",
11
+ "openfleet.json",
12
+ ];
13
+
14
+ function parseBool(value) {
15
+ return ["1", "true", "yes", "on"].includes(
16
+ String(value || "")
17
+ .trim()
18
+ .toLowerCase(),
19
+ );
20
+ }
21
+
22
+ function isPositiveInt(value) {
23
+ const n = Number(value);
24
+ return Number.isFinite(n) && n > 0 && Number.isInteger(n);
25
+ }
26
+
27
+ function isUrl(value) {
28
+ try {
29
+ if (!value) return false;
30
+ const parsed = new URL(String(value));
31
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ function detectRepoRoot() {
38
+ try {
39
+ return execSync("git rev-parse --show-toplevel", {
40
+ encoding: "utf8",
41
+ stdio: ["pipe", "pipe", "ignore"],
42
+ }).trim();
43
+ } catch {
44
+ return process.cwd();
45
+ }
46
+ }
47
+
48
+ function readProcValue(path) {
49
+ try {
50
+ return readFileSync(path, "utf8").trim();
51
+ } catch {
52
+ return "";
53
+ }
54
+ }
55
+
56
+ function isUserNamespaceDisabled() {
57
+ if (process.platform !== "linux") return false;
58
+ const unpriv = readProcValue("/proc/sys/kernel/unprivileged_userns_clone");
59
+ if (unpriv === "0") return true;
60
+ const maxUserNs = readProcValue("/proc/sys/user/max_user_namespaces");
61
+ if (maxUserNs && Number(maxUserNs) === 0) return true;
62
+ return false;
63
+ }
64
+
65
+ function isPathInside(parent, child) {
66
+ const rel = relative(parent, child);
67
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
68
+ }
69
+
70
+ function hasSetupMarkers(dir) {
71
+ const markers = [".env", ...CONFIG_FILES];
72
+ return markers.some((name) => existsSync(resolve(dir, name)));
73
+ }
74
+
75
+ function isWslInteropRuntime() {
76
+ return Boolean(
77
+ process.env.WSL_DISTRO_NAME ||
78
+ process.env.WSL_INTEROP ||
79
+ (process.platform === "win32" &&
80
+ String(process.env.HOME || "").trim().startsWith("/home/")),
81
+ );
82
+ }
83
+
84
+ function resolveConfigDir(repoRoot) {
85
+ const explicit = process.env.CODEX_MONITOR_DIR;
86
+ if (explicit) return resolve(explicit);
87
+
88
+ const repoPath = resolve(repoRoot || process.cwd());
89
+ const packageDir = resolve(__dirname);
90
+ if (isPathInside(repoPath, packageDir) || hasSetupMarkers(packageDir)) {
91
+ return packageDir;
92
+ }
93
+
94
+ const preferWindowsDirs =
95
+ process.platform === "win32" && !isWslInteropRuntime();
96
+ const baseDir =
97
+ preferWindowsDirs
98
+ ? process.env.APPDATA ||
99
+ process.env.LOCALAPPDATA ||
100
+ process.env.USERPROFILE ||
101
+ process.env.HOME ||
102
+ process.cwd()
103
+ : process.env.HOME ||
104
+ process.env.XDG_CONFIG_HOME ||
105
+ process.env.USERPROFILE ||
106
+ process.env.APPDATA ||
107
+ process.env.LOCALAPPDATA ||
108
+ process.cwd();
109
+ return resolve(baseDir, "openfleet");
110
+ }
111
+
112
+ function loadDotEnvToObject(envPath) {
113
+ if (!envPath || !existsSync(envPath)) return {};
114
+ const out = {};
115
+ const lines = readFileSync(envPath, "utf8").split(/\r?\n/);
116
+ for (const line of lines) {
117
+ const trimmed = line.trim();
118
+ if (!trimmed || trimmed.startsWith("#")) continue;
119
+ const idx = trimmed.indexOf("=");
120
+ if (idx === -1) continue;
121
+ const key = trimmed.slice(0, idx).trim();
122
+ let val = trimmed.slice(idx + 1).trim();
123
+ if (
124
+ (val.startsWith('"') && val.endsWith('"')) ||
125
+ (val.startsWith("'") && val.endsWith("'"))
126
+ ) {
127
+ val = val.slice(1, -1);
128
+ }
129
+ out[key] = val;
130
+ }
131
+ return out;
132
+ }
133
+
134
+ function mergeNoOverride(base, extra) {
135
+ const merged = { ...base };
136
+ for (const [key, value] of Object.entries(extra || {})) {
137
+ if (!(key in merged)) {
138
+ merged[key] = value;
139
+ }
140
+ }
141
+ return merged;
142
+ }
143
+
144
+ function commandExists(command) {
145
+ try {
146
+ const checker = process.platform === "win32" ? "where" : "which";
147
+ spawnSync(checker, [command], { stdio: "ignore" });
148
+ return true;
149
+ } catch {
150
+ return false;
151
+ }
152
+ }
153
+
154
+ function findConfigFile(configDir) {
155
+ for (const name of CONFIG_FILES) {
156
+ const p = resolve(configDir, name);
157
+ if (existsSync(p)) {
158
+ return p;
159
+ }
160
+ }
161
+ return null;
162
+ }
163
+
164
+ function validateExecutors(raw, issues) {
165
+ if (!raw) return;
166
+ const entries = String(raw)
167
+ .split(",")
168
+ .map((item) => item.trim())
169
+ .filter(Boolean);
170
+ if (entries.length === 0) {
171
+ issues.errors.push({
172
+ code: "EXECUTORS_EMPTY",
173
+ message: "EXECUTORS is set but empty.",
174
+ fix: "Use format EXECUTOR:VARIANT:WEIGHT, e.g. EXECUTORS=CODEX:DEFAULT:100",
175
+ });
176
+ return;
177
+ }
178
+ for (const entry of entries) {
179
+ const [executor, variant, weight] = entry.split(":");
180
+ if (!executor || !variant) {
181
+ issues.errors.push({
182
+ code: "EXECUTORS_FORMAT",
183
+ message: `Invalid EXECUTORS entry: ${entry}`,
184
+ fix: "Each entry must be EXECUTOR:VARIANT[:WEIGHT]",
185
+ });
186
+ continue;
187
+ }
188
+ if (weight && !isPositiveInt(weight)) {
189
+ issues.errors.push({
190
+ code: "EXECUTORS_WEIGHT",
191
+ message: `Invalid executor weight in entry: ${entry}`,
192
+ fix: "Use integer weights > 0",
193
+ });
194
+ }
195
+ }
196
+ }
197
+
198
+ export function runConfigDoctor(options = {}) {
199
+ const repoRoot = resolve(options.repoRoot || detectRepoRoot());
200
+ const configDir = resolve(options.configDir || resolveConfigDir(repoRoot));
201
+ const configEnvPath = resolve(configDir, ".env");
202
+ const repoEnvPath = resolve(repoRoot, ".env");
203
+ const configFilePath = findConfigFile(configDir);
204
+
205
+ const fromConfigEnv = loadDotEnvToObject(configEnvPath);
206
+ const fromRepoEnv =
207
+ resolve(repoEnvPath) === resolve(configEnvPath)
208
+ ? {}
209
+ : loadDotEnvToObject(repoEnvPath);
210
+
211
+ let effective = {};
212
+ effective = mergeNoOverride(effective, fromConfigEnv);
213
+ effective = mergeNoOverride(effective, fromRepoEnv);
214
+ effective = { ...effective, ...process.env };
215
+
216
+ const issues = {
217
+ errors: [],
218
+ warnings: [],
219
+ infos: [],
220
+ };
221
+
222
+ const telegramToken = effective.TELEGRAM_BOT_TOKEN || "";
223
+ const telegramChatId = effective.TELEGRAM_CHAT_ID || "";
224
+ if (
225
+ (telegramToken && !telegramChatId) ||
226
+ (!telegramToken && telegramChatId)
227
+ ) {
228
+ issues.errors.push({
229
+ code: "TELEGRAM_PARTIAL",
230
+ message:
231
+ "Telegram is partially configured (TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID mismatch).",
232
+ fix: "Set both TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID, or unset both.",
233
+ });
234
+ }
235
+
236
+ const telegramInterval = effective.TELEGRAM_INTERVAL_MIN;
237
+ if (telegramInterval && !isPositiveInt(telegramInterval)) {
238
+ issues.errors.push({
239
+ code: "TELEGRAM_INTERVAL_MIN",
240
+ message: `Invalid TELEGRAM_INTERVAL_MIN: ${telegramInterval}`,
241
+ fix: "Use a positive integer (minutes), e.g. TELEGRAM_INTERVAL_MIN=10",
242
+ });
243
+ }
244
+
245
+ const backend = String(effective.KANBAN_BACKEND || "internal").toLowerCase();
246
+ if (!["internal", "vk", "github", "jira"].includes(backend)) {
247
+ issues.errors.push({
248
+ code: "KANBAN_BACKEND",
249
+ message: `Invalid KANBAN_BACKEND: ${effective.KANBAN_BACKEND}`,
250
+ fix: "Use one of: internal, vk, github, jira",
251
+ });
252
+ }
253
+
254
+ const syncPolicy = String(
255
+ effective.KANBAN_SYNC_POLICY || "internal-primary",
256
+ ).toLowerCase();
257
+ if (!["internal-primary", "bidirectional"].includes(syncPolicy)) {
258
+ issues.errors.push({
259
+ code: "KANBAN_SYNC_POLICY",
260
+ message: `Invalid KANBAN_SYNC_POLICY: ${effective.KANBAN_SYNC_POLICY}`,
261
+ fix: "Use one of: internal-primary, bidirectional",
262
+ });
263
+ }
264
+
265
+ const requirementsProfile = String(
266
+ effective.PROJECT_REQUIREMENTS_PROFILE || "feature",
267
+ ).toLowerCase();
268
+ if (
269
+ ![
270
+ "simple-feature",
271
+ "feature",
272
+ "large-feature",
273
+ "system",
274
+ "multi-system",
275
+ ].includes(requirementsProfile)
276
+ ) {
277
+ issues.errors.push({
278
+ code: "PROJECT_REQUIREMENTS_PROFILE",
279
+ message: `Invalid PROJECT_REQUIREMENTS_PROFILE: ${effective.PROJECT_REQUIREMENTS_PROFILE}`,
280
+ fix: "Use one of: simple-feature, feature, large-feature, system, multi-system",
281
+ });
282
+ }
283
+
284
+ const replenishMin = Number(
285
+ effective.INTERNAL_EXECUTOR_REPLENISH_MIN_NEW_TASKS || "1",
286
+ );
287
+ const replenishMax = Number(
288
+ effective.INTERNAL_EXECUTOR_REPLENISH_MAX_NEW_TASKS || "2",
289
+ );
290
+ if (!Number.isFinite(replenishMin) || replenishMin < 1 || replenishMin > 2) {
291
+ issues.errors.push({
292
+ code: "INTERNAL_EXECUTOR_REPLENISH_MIN_NEW_TASKS",
293
+ message: `Invalid INTERNAL_EXECUTOR_REPLENISH_MIN_NEW_TASKS: ${effective.INTERNAL_EXECUTOR_REPLENISH_MIN_NEW_TASKS}`,
294
+ fix: "Use an integer between 1 and 2",
295
+ });
296
+ }
297
+ if (!Number.isFinite(replenishMax) || replenishMax < 1 || replenishMax > 3) {
298
+ issues.errors.push({
299
+ code: "INTERNAL_EXECUTOR_REPLENISH_MAX_NEW_TASKS",
300
+ message: `Invalid INTERNAL_EXECUTOR_REPLENISH_MAX_NEW_TASKS: ${effective.INTERNAL_EXECUTOR_REPLENISH_MAX_NEW_TASKS}`,
301
+ fix: "Use an integer between 1 and 3",
302
+ });
303
+ }
304
+ if (
305
+ Number.isFinite(replenishMin) &&
306
+ Number.isFinite(replenishMax) &&
307
+ replenishMax < replenishMin
308
+ ) {
309
+ issues.errors.push({
310
+ code: "INTERNAL_EXECUTOR_REPLENISH_RANGE",
311
+ message:
312
+ "INTERNAL_EXECUTOR_REPLENISH_MAX_NEW_TASKS cannot be lower than INTERNAL_EXECUTOR_REPLENISH_MIN_NEW_TASKS.",
313
+ fix: "Set max >= min",
314
+ });
315
+ }
316
+
317
+ const mode = String(effective.EXECUTOR_MODE || "internal").toLowerCase();
318
+ if (!["internal", "vk", "hybrid"].includes(mode)) {
319
+ issues.errors.push({
320
+ code: "EXECUTOR_MODE",
321
+ message: `Invalid EXECUTOR_MODE: ${effective.EXECUTOR_MODE}`,
322
+ fix: "Use one of: internal, vk, hybrid",
323
+ });
324
+ }
325
+
326
+ validateExecutors(effective.EXECUTORS, issues);
327
+
328
+ if (backend === "github") {
329
+ const hasSlug =
330
+ Boolean(effective.GITHUB_REPO) ||
331
+ Boolean(effective.GITHUB_REPOSITORY) ||
332
+ (Boolean(effective.GITHUB_REPO_OWNER) &&
333
+ Boolean(effective.GITHUB_REPO_NAME));
334
+ if (!hasSlug) {
335
+ issues.errors.push({
336
+ code: "GITHUB_BACKEND_REPO",
337
+ message: "KANBAN_BACKEND=github requires repository identification.",
338
+ fix: "Set GITHUB_REPOSITORY=owner/repo (or GITHUB_REPO, or owner + name).",
339
+ });
340
+ }
341
+ }
342
+
343
+ const vkNeeded = backend === "vk" || mode === "vk" || mode === "hybrid";
344
+ if (vkNeeded) {
345
+ const vkBaseUrl = effective.VK_BASE_URL || "";
346
+ const vkPort = effective.VK_RECOVERY_PORT || "";
347
+ if (vkBaseUrl && !isUrl(vkBaseUrl)) {
348
+ issues.errors.push({
349
+ code: "VK_BASE_URL",
350
+ message: `Invalid VK_BASE_URL: ${vkBaseUrl}`,
351
+ fix: "Use a full URL, e.g. http://127.0.0.1:54089",
352
+ });
353
+ }
354
+ if (vkPort && !isPositiveInt(vkPort)) {
355
+ issues.errors.push({
356
+ code: "VK_RECOVERY_PORT",
357
+ message: `Invalid VK_RECOVERY_PORT: ${vkPort}`,
358
+ fix: "Use a positive integer port, e.g. VK_RECOVERY_PORT=54089",
359
+ });
360
+ }
361
+ }
362
+
363
+ if (parseBool(effective.WHATSAPP_ENABLED)) {
364
+ if (!effective.WHATSAPP_CHAT_ID) {
365
+ issues.warnings.push({
366
+ code: "WHATSAPP_CHAT_ID",
367
+ message: "WHATSAPP_ENABLED is on but WHATSAPP_CHAT_ID is not set.",
368
+ fix: "Set WHATSAPP_CHAT_ID to restrict accepted chat(s).",
369
+ });
370
+ }
371
+ }
372
+
373
+ if (parseBool(effective.CONTAINER_ENABLED)) {
374
+ const runtime = String(effective.CONTAINER_RUNTIME || "auto").toLowerCase();
375
+ if (!["auto", "docker", "podman", "container"].includes(runtime)) {
376
+ issues.errors.push({
377
+ code: "CONTAINER_RUNTIME",
378
+ message: `Invalid CONTAINER_RUNTIME: ${effective.CONTAINER_RUNTIME}`,
379
+ fix: "Use one of: auto, docker, podman, container",
380
+ });
381
+ }
382
+ if (runtime !== "auto" && !commandExists(runtime)) {
383
+ issues.warnings.push({
384
+ code: "CONTAINER_RUNTIME_MISSING",
385
+ message: `Container runtime not found on PATH: ${runtime}`,
386
+ fix: "Install runtime or set CONTAINER_RUNTIME=auto",
387
+ });
388
+ }
389
+ }
390
+
391
+ if (effective.MAX_PARALLEL && !isPositiveInt(effective.MAX_PARALLEL)) {
392
+ issues.errors.push({
393
+ code: "MAX_PARALLEL",
394
+ message: `Invalid MAX_PARALLEL: ${effective.MAX_PARALLEL}`,
395
+ fix: "Use a positive integer, e.g. MAX_PARALLEL=6",
396
+ });
397
+ }
398
+
399
+ if (effective.ORCHESTRATOR_SCRIPT) {
400
+ const scriptPath = resolve(configDir, effective.ORCHESTRATOR_SCRIPT);
401
+ if (!existsSync(scriptPath)) {
402
+ issues.warnings.push({
403
+ code: "ORCHESTRATOR_SCRIPT",
404
+ message: `ORCHESTRATOR_SCRIPT does not exist: ${effective.ORCHESTRATOR_SCRIPT}`,
405
+ fix: "Set a valid absolute path or path relative to config directory",
406
+ });
407
+ }
408
+ }
409
+
410
+ if (configFilePath && existsSync(configFilePath)) {
411
+ try {
412
+ JSON.parse(readFileSync(configFilePath, "utf8"));
413
+ } catch (error) {
414
+ issues.errors.push({
415
+ code: "CONFIG_JSON",
416
+ message: `Invalid JSON in ${configFilePath}`,
417
+ fix: `Fix JSON syntax (${error.message})`,
418
+ });
419
+ }
420
+ } else {
421
+ issues.warnings.push({
422
+ code: "CONFIG_JSON_MISSING",
423
+ message: "No openfleet config JSON found.",
424
+ fix: "Run openfleet --setup to generate openfleet.config.json",
425
+ });
426
+ }
427
+
428
+ if (!existsSync(configEnvPath) && !existsSync(repoEnvPath)) {
429
+ issues.warnings.push({
430
+ code: "ENV_MISSING",
431
+ message: "No .env file found in config directory or repo root.",
432
+ fix: "Run openfleet --setup to generate .env",
433
+ });
434
+ }
435
+
436
+ const vscodeSettingsPath = resolve(repoRoot, ".vscode", "settings.json");
437
+ if (!existsSync(vscodeSettingsPath)) {
438
+ issues.warnings.push({
439
+ code: "VSCODE_SETTINGS_MISSING",
440
+ message:
441
+ "No .vscode/settings.json found — Copilot autonomous/subagent defaults may be missing.",
442
+ fix: "Run openfleet --setup to generate recommended workspace settings.",
443
+ });
444
+ } else {
445
+ try {
446
+ const settings = JSON.parse(readFileSync(vscodeSettingsPath, "utf8"));
447
+ const requiredKeys = [
448
+ "github.copilot.chat.searchSubagent.enabled",
449
+ "github.copilot.chat.switchAgent.enabled",
450
+ "github.copilot.chat.cli.customAgents.enabled",
451
+ "github.copilot.chat.cli.mcp.enabled",
452
+ ];
453
+ const missing = requiredKeys.filter((key) => settings[key] !== true);
454
+ if (missing.length > 0) {
455
+ issues.warnings.push({
456
+ code: "VSCODE_SETTINGS_PARTIAL",
457
+ message:
458
+ "Workspace Copilot settings are missing recommended autonomous/subagent flags.",
459
+ fix: "Run openfleet --setup to merge the recommended .vscode/settings.json defaults.",
460
+ });
461
+ }
462
+ } catch {
463
+ issues.warnings.push({
464
+ code: "VSCODE_SETTINGS_INVALID",
465
+ message: ".vscode/settings.json is not valid JSON.",
466
+ fix: "Fix JSON syntax or rerun openfleet --setup to regenerate it.",
467
+ });
468
+ }
469
+ }
470
+
471
+ // ── Codex config.toml feature flag / sub-agent checks ──────────────────────
472
+ const codexConfigToml = join(homedir(), ".codex", "config.toml");
473
+ if (existsSync(codexConfigToml)) {
474
+ const toml = readFileSync(codexConfigToml, "utf-8");
475
+ if (!/^\[features\]/m.test(toml)) {
476
+ issues.warnings.push({
477
+ code: "CODEX_NO_FEATURES",
478
+ message: "Codex config.toml has no [features] section — sub-agents and advanced features disabled.",
479
+ fix: "Run openfleet --setup to auto-configure features, or add [features] manually",
480
+ });
481
+ } else {
482
+ if (!/child_agents_md\s*=\s*true/i.test(toml)) {
483
+ issues.warnings.push({
484
+ code: "CODEX_NO_CHILD_AGENTS",
485
+ message: "child_agents_md not enabled — Codex cannot spawn sub-agents or discover CODEX.md.",
486
+ fix: 'Add child_agents_md = true under [features] in ~/.codex/config.toml',
487
+ });
488
+ }
489
+ if (!/memory_tool\s*=\s*true/i.test(toml)) {
490
+ issues.warnings.push({
491
+ code: "CODEX_NO_MEMORY",
492
+ message: "memory_tool not enabled — Codex has no persistent memory across sessions.",
493
+ fix: 'Add memory_tool = true under [features] in ~/.codex/config.toml',
494
+ });
495
+ }
496
+ }
497
+ if (
498
+ !/^\s*sandbox_permissions\s*=/m.test(toml) &&
499
+ !/^\[sandbox_permissions\]/m.test(toml)
500
+ ) {
501
+ issues.warnings.push({
502
+ code: "CODEX_NO_SANDBOX_PERMS",
503
+ message: "No sandbox_permissions in Codex config — may restrict agent file access.",
504
+ fix: "Run openfleet --setup to auto-configure sandbox permissions",
505
+ });
506
+ }
507
+ if (!/^\[sandbox_workspace_write\]/m.test(toml)) {
508
+ issues.warnings.push({
509
+ code: "CODEX_NO_SANDBOX_WORKSPACE",
510
+ message: "No [sandbox_workspace_write] section in Codex config — workspace-write roots may be missing.",
511
+ fix: "Run openfleet --setup to add workspace-write defaults (writable_roots, network_access).",
512
+ });
513
+ }
514
+ if (
515
+ isUserNamespaceDisabled() &&
516
+ /use_linux_sandbox_bwrap\s*=\s*true/i.test(toml)
517
+ ) {
518
+ issues.warnings.push({
519
+ code: "CODEX_BWRAP_DISABLED",
520
+ message: "Bubblewrap sandbox is enabled but unprivileged user namespaces appear disabled.",
521
+ fix: "Set CODEX_FEATURES_BWRAP=false and re-run openfleet --setup (or edit ~/.codex/config.toml [features]).",
522
+ });
523
+ }
524
+ } else {
525
+ issues.warnings.push({
526
+ code: "CODEX_CONFIG_MISSING",
527
+ message: "~/.codex/config.toml not found — Codex CLI may not be configured.",
528
+ fix: "Run openfleet --setup or 'codex --setup' to create initial config",
529
+ });
530
+ }
531
+
532
+ // ── CODEX.md repo-level check ──────────────────────────────────────────────
533
+ const codexMd = join(repoRoot, "CODEX.md");
534
+ if (!existsSync(codexMd)) {
535
+ issues.warnings.push({
536
+ code: "CODEX_MD_MISSING",
537
+ message: "No CODEX.md in repo root — Codex sub-agents cannot discover repo instructions.",
538
+ fix: "Create CODEX.md (copy from AGENTS.md) for Codex CLI sub-agent discovery",
539
+ });
540
+ }
541
+
542
+ issues.infos.push({
543
+ code: "PATHS",
544
+ message: `Config directory: ${configDir}`,
545
+ fix: null,
546
+ });
547
+ issues.infos.push({
548
+ code: "PATHS",
549
+ message: `Repo root: ${repoRoot}`,
550
+ fix: null,
551
+ });
552
+
553
+ return {
554
+ ok: issues.errors.length === 0,
555
+ ...issues,
556
+ details: {
557
+ configDir,
558
+ repoRoot,
559
+ configFilePath,
560
+ configEnvPath: existsSync(configEnvPath) ? configEnvPath : null,
561
+ repoEnvPath:
562
+ existsSync(repoEnvPath) &&
563
+ resolve(repoEnvPath) !== resolve(configEnvPath)
564
+ ? repoEnvPath
565
+ : null,
566
+ },
567
+ };
568
+ }
569
+
570
+ export function formatConfigDoctorReport(result) {
571
+ const lines = [];
572
+ lines.push("=== openfleet config doctor ===");
573
+ lines.push(
574
+ `Status: ${result.ok ? "OK" : "FAILED"} (${result.errors.length} error(s), ${result.warnings.length} warning(s))`,
575
+ );
576
+ lines.push("");
577
+
578
+ if (result.errors.length > 0) {
579
+ lines.push("Errors:");
580
+ for (const issue of result.errors) {
581
+ lines.push(` - ${issue.message}`);
582
+ if (issue.fix) lines.push(` fix: ${issue.fix}`);
583
+ }
584
+ lines.push("");
585
+ }
586
+
587
+ if (result.warnings.length > 0) {
588
+ lines.push("Warnings:");
589
+ for (const issue of result.warnings) {
590
+ lines.push(` - ${issue.message}`);
591
+ if (issue.fix) lines.push(` fix: ${issue.fix}`);
592
+ }
593
+ lines.push("");
594
+ }
595
+
596
+ if (result.infos.length > 0) {
597
+ lines.push("Info:");
598
+ for (const info of result.infos) {
599
+ lines.push(` - ${info.message}`);
600
+ }
601
+ lines.push("");
602
+ }
603
+
604
+ if (result.ok) {
605
+ lines.push("Doctor check passed — configuration looks consistent.");
606
+ } else {
607
+ lines.push(
608
+ "Doctor check failed — fix the errors above and run: openfleet --doctor",
609
+ );
610
+ }
611
+
612
+ return lines.join("\n");
613
+ }