@virtengine/openfleet 0.26.2 → 0.26.3
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.
- package/.env.example +2 -2
- package/cli.mjs +48 -6
- package/compat.mjs +286 -0
- package/config.mjs +4 -0
- package/kanban-adapter.mjs +5 -5
- package/monitor.mjs +34 -4
- package/package.json +4 -2
- package/presence.mjs +1 -1
- package/publish.mjs +2 -2
- package/setup.mjs +9 -0
- package/shared-state-manager.mjs +1 -1
- package/startup-service.mjs +2 -2
- package/telegram-bot.mjs +136 -2
- package/ui/demo.html +640 -0
- package/ui/modules/settings-schema.js +1 -1
- package/ui/styles/variables.css +1 -1
- package/ui-server.mjs +3 -2
- package/update-check.mjs +9 -3
- package/ve-orchestrator.ps1 +6 -6
- package/whatsapp-channel.mjs +3 -3
package/.env.example
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# ───
|
|
1
|
+
# ─── OpenFleet — Environment Configuration ───────────────────────────────
|
|
2
2
|
# Copy this file to .env and fill in your values.
|
|
3
3
|
# Or run: openfleet --setup
|
|
4
4
|
# All variables are optional unless marked [REQUIRED].
|
|
@@ -788,7 +788,7 @@ COPILOT_CLOUD_DISABLED=true
|
|
|
788
788
|
|
|
789
789
|
# ─── Git Identity (optional) ─────────────────────────────────────────────────
|
|
790
790
|
# Override git author for automated commits
|
|
791
|
-
# VE_GIT_AUTHOR_NAME=
|
|
791
|
+
# VE_GIT_AUTHOR_NAME=OpenFleet
|
|
792
792
|
# VE_GIT_AUTHOR_EMAIL=bot@yoursite.com
|
|
793
793
|
|
|
794
794
|
# ─── Task Planner ─────────────────────────────────────────────────────────────
|
package/cli.mjs
CHANGED
|
@@ -27,6 +27,11 @@ import { fileURLToPath } from "node:url";
|
|
|
27
27
|
import { fork, spawn } from "node:child_process";
|
|
28
28
|
import os from "node:os";
|
|
29
29
|
import { createDaemonCrashTracker } from "./daemon-restart-policy.mjs";
|
|
30
|
+
import {
|
|
31
|
+
applyAllCompatibility,
|
|
32
|
+
detectLegacySetup,
|
|
33
|
+
migrateFromLegacy,
|
|
34
|
+
} from "./compat.mjs";
|
|
30
35
|
|
|
31
36
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
32
37
|
const args = process.argv.slice(2);
|
|
@@ -381,7 +386,8 @@ function startDaemon() {
|
|
|
381
386
|
stdio: "ignore",
|
|
382
387
|
windowsHide: process.platform === "win32",
|
|
383
388
|
env: { ...process.env, OPENFLEET_DAEMON: "1" },
|
|
384
|
-
|
|
389
|
+
// Use home dir so spawn never inherits a deleted CWD (e.g. old git worktree)
|
|
390
|
+
cwd: os.homedir(),
|
|
385
391
|
},
|
|
386
392
|
);
|
|
387
393
|
|
|
@@ -456,6 +462,9 @@ function daemonStatus() {
|
|
|
456
462
|
}
|
|
457
463
|
|
|
458
464
|
async function main() {
|
|
465
|
+
// Apply legacy CODEX_MONITOR_* → OPENFLEET_* env aliases before any config ops
|
|
466
|
+
applyAllCompatibility();
|
|
467
|
+
|
|
459
468
|
// Handle --help
|
|
460
469
|
if (args.includes("--help") || args.includes("-h")) {
|
|
461
470
|
showHelp();
|
|
@@ -689,6 +698,23 @@ async function main() {
|
|
|
689
698
|
console.log("\n Setup complete! Starting openfleet...\n");
|
|
690
699
|
}
|
|
691
700
|
|
|
701
|
+
// Legacy migration: if ~/codex-monitor exists with config, auto-migrate to ~/openfleet
|
|
702
|
+
const legacyInfo = detectLegacySetup();
|
|
703
|
+
if (legacyInfo.hasLegacy && !legacyInfo.alreadyMigrated) {
|
|
704
|
+
console.log(
|
|
705
|
+
`\n 📦 Detected legacy codex-monitor config at ${legacyInfo.legacyDir}`,
|
|
706
|
+
);
|
|
707
|
+
console.log(` Auto-migrating to ${legacyInfo.newDir}...\n`);
|
|
708
|
+
const result = migrateFromLegacy(legacyInfo.legacyDir, legacyInfo.newDir);
|
|
709
|
+
if (result.migrated.length > 0) {
|
|
710
|
+
console.log(` ✅ Migrated: ${result.migrated.join(", ")}`);
|
|
711
|
+
console.log(`\n Config is now at ${legacyInfo.newDir}\n`);
|
|
712
|
+
}
|
|
713
|
+
for (const err of result.errors) {
|
|
714
|
+
console.log(` ⚠️ Migration warning: ${err}`);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
692
718
|
// ── Handle --echo-logs: tail the active monitor's log instead of spawning a new instance ──
|
|
693
719
|
if (args.includes("--echo-logs")) {
|
|
694
720
|
// Search for the monitor PID file in common cache locations
|
|
@@ -731,24 +757,40 @@ async function main() {
|
|
|
731
757
|
`\n Tailing logs for active openfleet (PID ${monitorPid}):\n ${logFile}\n`,
|
|
732
758
|
);
|
|
733
759
|
await new Promise((res) => {
|
|
760
|
+
// Spawn tail in its own process group (detached) so that
|
|
761
|
+
// Ctrl+C in this terminal only kills the tailing session,
|
|
762
|
+
// never the running daemon.
|
|
734
763
|
const tail = spawn("tail", ["-f", "-n", "200", logFile], {
|
|
735
|
-
stdio: "inherit",
|
|
764
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
765
|
+
detached: true,
|
|
736
766
|
});
|
|
737
767
|
tail.on("exit", res);
|
|
738
768
|
process.on("SIGINT", () => {
|
|
739
|
-
tail.kill();
|
|
769
|
+
try { process.kill(-tail.pid, "SIGTERM"); } catch { tail.kill(); }
|
|
740
770
|
res();
|
|
741
771
|
});
|
|
742
772
|
});
|
|
743
773
|
process.exit(0);
|
|
774
|
+
} else {
|
|
775
|
+
console.error(
|
|
776
|
+
`\n No log file found for active openfleet (PID ${monitorPid}).\n Expected: ${logFile}\n`,
|
|
777
|
+
);
|
|
778
|
+
process.exit(1);
|
|
744
779
|
}
|
|
745
780
|
}
|
|
746
|
-
} catch {
|
|
747
|
-
|
|
781
|
+
} catch (e) {
|
|
782
|
+
console.error(`\n --echo-logs: failed to read PID file — ${e.message}\n`);
|
|
783
|
+
process.exit(1);
|
|
748
784
|
}
|
|
785
|
+
} else {
|
|
786
|
+
console.error(
|
|
787
|
+
"\n --echo-logs: no active openfleet found (PID file missing).\n Start openfleet first with: openfleet --daemon\n",
|
|
788
|
+
);
|
|
789
|
+
process.exit(1);
|
|
749
790
|
}
|
|
750
791
|
|
|
751
|
-
//
|
|
792
|
+
// Should not reach here — all paths above exit
|
|
793
|
+
process.exit(0);
|
|
752
794
|
}
|
|
753
795
|
|
|
754
796
|
// Fork monitor as a child process — enables self-restart on source changes.
|
package/compat.mjs
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* compat.mjs — Backward compatibility for users migrating from codex-monitor
|
|
4
|
+
*
|
|
5
|
+
* Handles:
|
|
6
|
+
* 1. Legacy env var aliasing: CODEX_MONITOR_X → OPENFLEET_X
|
|
7
|
+
* 2. Legacy config dir detection: ~/codex-monitor or $CODEX_MONITOR_DIR
|
|
8
|
+
* 3. Migration: copy old config dir to new openfleet dir
|
|
9
|
+
*
|
|
10
|
+
* This module is intentionally dependency-free (only Node built-ins) and
|
|
11
|
+
* should be imported as early as possible in the startup path.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from "node:fs";
|
|
15
|
+
import { resolve, join } from "node:path";
|
|
16
|
+
|
|
17
|
+
// ── Legacy config file names accepted from old codex-monitor installations ───
|
|
18
|
+
const LEGACY_CONFIG_NAMES = [
|
|
19
|
+
"codex-monitor.config.json",
|
|
20
|
+
"openfleet.config.json",
|
|
21
|
+
".openfleet.json",
|
|
22
|
+
"openfleet.json",
|
|
23
|
+
".env",
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Env vars the old codex-monitor package used that have been renamed.
|
|
28
|
+
* Format: [oldName, newName]
|
|
29
|
+
* Rule: any CODEX_MONITOR_X maps to OPENFLEET_X by default, but some had
|
|
30
|
+
* different names entirely — those are listed explicitly below.
|
|
31
|
+
*/
|
|
32
|
+
const EXPLICIT_ALIASES = [
|
|
33
|
+
// Dir / location
|
|
34
|
+
["CODEX_MONITOR_DIR", "OPENFLEET_DIR"],
|
|
35
|
+
["CODEX_MONITOR_HOME", "OPENFLEET_HOME"],
|
|
36
|
+
// Runtime flags
|
|
37
|
+
["CODEX_MONITOR_SKIP_AUTO_UPDATE", "OPENFLEET_SKIP_AUTO_UPDATE"],
|
|
38
|
+
["CODEX_MONITOR_SKIP_UPDATE_CHECK", "OPENFLEET_SKIP_UPDATE_CHECK"],
|
|
39
|
+
["CODEX_MONITOR_UPDATE_INTERVAL_MS", "OPENFLEET_UPDATE_INTERVAL_MS"],
|
|
40
|
+
["CODEX_MONITOR_SKIP_POSTINSTALL", "OPENFLEET_SKIP_POSTINSTALL"],
|
|
41
|
+
["CODEX_MONITOR_INTERACTIVE", "OPENFLEET_INTERACTIVE"],
|
|
42
|
+
["CODEX_MONITOR_PREFLIGHT_DISABLED", "OPENFLEET_PREFLIGHT_DISABLED"],
|
|
43
|
+
["CODEX_MONITOR_PREFLIGHT_RETRY_MS", "OPENFLEET_PREFLIGHT_RETRY_MS"],
|
|
44
|
+
["CODEX_MONITOR_MIN_FREE_GB", "OPENFLEET_MIN_FREE_GB"],
|
|
45
|
+
["CODEX_MONITOR_INSTANCE_ID", "OPENFLEET_INSTANCE_ID"],
|
|
46
|
+
["CODEX_MONITOR_MODE", "OPENFLEET_MODE"],
|
|
47
|
+
["CODEX_MONITOR_SHELL", "OPENFLEET_SHELL"],
|
|
48
|
+
["CODEX_MONITOR_SHELL_MODE", "OPENFLEET_SHELL_MODE"],
|
|
49
|
+
["CODEX_MONITOR_PROFILE", "OPENFLEET_PROFILE"],
|
|
50
|
+
["CODEX_MONITOR_ENV_PROFILE", "OPENFLEET_ENV_PROFILE"],
|
|
51
|
+
// Task
|
|
52
|
+
["CODEX_MONITOR_TASK_LABEL", "OPENFLEET_TASK_LABEL"],
|
|
53
|
+
["CODEX_MONITOR_TASK_LABELS", "OPENFLEET_TASK_LABELS"],
|
|
54
|
+
["CODEX_MONITOR_ENFORCE_TASK_LABEL", "OPENFLEET_ENFORCE_TASK_LABEL"],
|
|
55
|
+
["CODEX_MONITOR_TASK_UPSTREAM", "OPENFLEET_TASK_UPSTREAM"],
|
|
56
|
+
// Daemon
|
|
57
|
+
["CODEX_MONITOR_DAEMON", "OPENFLEET_DAEMON"],
|
|
58
|
+
["CODEX_MONITOR_DAEMON_RESTART_DELAY_MS", "OPENFLEET_DAEMON_RESTART_DELAY_MS"],
|
|
59
|
+
["CODEX_MONITOR_DAEMON_MAX_RESTARTS", "OPENFLEET_DAEMON_MAX_RESTARTS"],
|
|
60
|
+
["CODEX_MONITOR_DAEMON_INSTANT_CRASH_WINDOW_MS", "OPENFLEET_DAEMON_INSTANT_CRASH_WINDOW_MS"],
|
|
61
|
+
["CODEX_MONITOR_DAEMON_MAX_INSTANT_RESTARTS", "OPENFLEET_DAEMON_MAX_INSTANT_RESTARTS"],
|
|
62
|
+
// Hooks
|
|
63
|
+
["CODEX_MONITOR_HOOK_PROFILE", "OPENFLEET_HOOK_PROFILE"],
|
|
64
|
+
["CODEX_MONITOR_HOOK_TARGETS", "OPENFLEET_HOOK_TARGETS"],
|
|
65
|
+
["CODEX_MONITOR_HOOKS_ENABLED", "OPENFLEET_HOOKS_ENABLED"],
|
|
66
|
+
["CODEX_MONITOR_HOOKS_OVERWRITE", "OPENFLEET_HOOKS_OVERWRITE"],
|
|
67
|
+
["CODEX_MONITOR_HOOK_NODE_BIN", "OPENFLEET_HOOK_NODE_BIN"],
|
|
68
|
+
["CODEX_MONITOR_HOOK_BRIDGE_PATH", "OPENFLEET_HOOK_BRIDGE_PATH"],
|
|
69
|
+
["CODEX_MONITOR_HOOK_PREPUSH", "OPENFLEET_HOOK_PREPUSH"],
|
|
70
|
+
["CODEX_MONITOR_HOOK_PRECOMMIT", "OPENFLEET_HOOK_PRECOMMIT"],
|
|
71
|
+
["CODEX_MONITOR_HOOK_TASK_COMPLETE", "OPENFLEET_HOOK_TASK_COMPLETE"],
|
|
72
|
+
["CODEX_MONITOR_HOOKS_BUILTINS_MODE", "OPENFLEET_HOOKS_BUILTINS_MODE"],
|
|
73
|
+
["CODEX_MONITOR_HOOKS_FORCE", "OPENFLEET_HOOKS_FORCE"],
|
|
74
|
+
// Sentinel
|
|
75
|
+
["CODEX_MONITOR_SENTINEL_AUTO_START", "OPENFLEET_SENTINEL_AUTO_START"],
|
|
76
|
+
["CODEX_MONITOR_SENTINEL_STRICT", "OPENFLEET_SENTINEL_STRICT"],
|
|
77
|
+
// Prompts
|
|
78
|
+
["CODEX_MONITOR_PROMPT_WORKSPACE", "OPENFLEET_PROMPT_WORKSPACE"],
|
|
79
|
+
// Repo
|
|
80
|
+
["CODEX_MONITOR_REPO", "OPENFLEET_REPO"],
|
|
81
|
+
["CODEX_MONITOR_REPO_NAME", "OPENFLEET_REPO_NAME"],
|
|
82
|
+
["CODEX_MONITOR_REPO_ROOT", "OPENFLEET_REPO_ROOT"],
|
|
83
|
+
// Config path
|
|
84
|
+
["CODEX_MONITOR_CONFIG_PATH", "OPENFLEET_CONFIG_PATH"],
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Apply all legacy CODEX_MONITOR_* → OPENFLEET_* env var aliases.
|
|
89
|
+
* Also does a generic pass: any remaining CODEX_MONITOR_X that wasn't in the
|
|
90
|
+
* explicit list is aliased to OPENFLEET_X.
|
|
91
|
+
*
|
|
92
|
+
* Existing OPENFLEET_* values are never overwritten (OPENFLEET_ wins).
|
|
93
|
+
* Safe to call multiple times (idempotent).
|
|
94
|
+
*/
|
|
95
|
+
export function applyLegacyEnvAliases() {
|
|
96
|
+
// Explicit known renames
|
|
97
|
+
for (const [oldKey, newKey] of EXPLICIT_ALIASES) {
|
|
98
|
+
if (process.env[oldKey] !== undefined && process.env[newKey] === undefined) {
|
|
99
|
+
process.env[newKey] = process.env[oldKey];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Generic pass: CODEX_MONITOR_X → OPENFLEET_X for anything not already mapped
|
|
104
|
+
for (const key of Object.keys(process.env)) {
|
|
105
|
+
if (!key.startsWith("CODEX_MONITOR_")) continue;
|
|
106
|
+
const suffix = key.slice("CODEX_MONITOR_".length);
|
|
107
|
+
const newKey = `OPENFLEET_${suffix}`;
|
|
108
|
+
if (process.env[newKey] === undefined) {
|
|
109
|
+
process.env[newKey] = process.env[key];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Returns the legacy codex-monitor config directory if it exists and
|
|
116
|
+
* contains config files, null otherwise.
|
|
117
|
+
*
|
|
118
|
+
* Checks in priority order:
|
|
119
|
+
* 1. $CODEX_MONITOR_DIR env var
|
|
120
|
+
* 2. ~/codex-monitor
|
|
121
|
+
* 3. ~/.codex-monitor
|
|
122
|
+
* 4. ~/.config/codex-monitor
|
|
123
|
+
*/
|
|
124
|
+
export function getLegacyConfigDir() {
|
|
125
|
+
const home =
|
|
126
|
+
process.env.HOME ||
|
|
127
|
+
process.env.USERPROFILE ||
|
|
128
|
+
process.env.APPDATA ||
|
|
129
|
+
null;
|
|
130
|
+
|
|
131
|
+
const candidates = [
|
|
132
|
+
process.env.CODEX_MONITOR_DIR || null,
|
|
133
|
+
home ? join(home, "codex-monitor") : null,
|
|
134
|
+
home ? join(home, ".codex-monitor") : null,
|
|
135
|
+
home ? join(home, ".config", "codex-monitor") : null,
|
|
136
|
+
].filter(Boolean);
|
|
137
|
+
|
|
138
|
+
for (const dir of candidates) {
|
|
139
|
+
if (existsSync(dir) && hasLegacyMarkers(dir)) {
|
|
140
|
+
return dir;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function hasLegacyMarkers(dir) {
|
|
147
|
+
return LEGACY_CONFIG_NAMES.some((name) => existsSync(join(dir, name)));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Returns the target openfleet config directory (where we'd migrate to).
|
|
152
|
+
* Does NOT create the directory.
|
|
153
|
+
*/
|
|
154
|
+
export function getNewConfigDir() {
|
|
155
|
+
const home =
|
|
156
|
+
process.env.HOME ||
|
|
157
|
+
process.env.USERPROFILE ||
|
|
158
|
+
process.env.APPDATA ||
|
|
159
|
+
process.cwd();
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
process.env.OPENFLEET_DIR ||
|
|
163
|
+
join(home, "openfleet")
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Detect if the user has a legacy codex-monitor setup but no openfleet setup.
|
|
169
|
+
* Returns { hasLegacy, legacyDir, newDir, alreadyMigrated }
|
|
170
|
+
*/
|
|
171
|
+
export function detectLegacySetup() {
|
|
172
|
+
const legacyDir = getLegacyConfigDir();
|
|
173
|
+
const newDir = getNewConfigDir();
|
|
174
|
+
const newHasConfig = hasLegacyMarkers(newDir);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
hasLegacy: legacyDir !== null,
|
|
178
|
+
legacyDir,
|
|
179
|
+
newDir,
|
|
180
|
+
alreadyMigrated: newHasConfig,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Migrate config files from old codex-monitor dir to new openfleet dir.
|
|
186
|
+
* Only copies; never deletes the old directory.
|
|
187
|
+
*
|
|
188
|
+
* Files copied:
|
|
189
|
+
* .env → .env (with CODEX_MONITOR_ → OPENFLEET_ substitution)
|
|
190
|
+
* openfleet.config.json → openfleet.config.json
|
|
191
|
+
* codex-monitor.config.json → openfleet.config.json (rename)
|
|
192
|
+
* .openfleet.json / openfleet.json → as-is
|
|
193
|
+
*
|
|
194
|
+
* Returns { migrated: string[], skipped: string[], errors: string[] }
|
|
195
|
+
*/
|
|
196
|
+
export function migrateFromLegacy(legacyDir, newDir, { overwrite = false } = {}) {
|
|
197
|
+
const result = { migrated: [], skipped: [], errors: [] };
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
mkdirSync(newDir, { recursive: true });
|
|
201
|
+
} catch (e) {
|
|
202
|
+
result.errors.push(`Could not create ${newDir}: ${e.message}`);
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Files to migrate: [source name, dest name]
|
|
207
|
+
const filePairs = [
|
|
208
|
+
[".env", ".env"],
|
|
209
|
+
["openfleet.config.json", "openfleet.config.json"],
|
|
210
|
+
["codex-monitor.config.json","openfleet.config.json"],
|
|
211
|
+
[".openfleet.json", ".openfleet.json"],
|
|
212
|
+
["openfleet.json", "openfleet.json"],
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
for (const [srcName, destName] of filePairs) {
|
|
216
|
+
const src = join(legacyDir, srcName);
|
|
217
|
+
const dest = join(newDir, destName);
|
|
218
|
+
|
|
219
|
+
if (!existsSync(src)) continue;
|
|
220
|
+
if (existsSync(dest) && !overwrite) {
|
|
221
|
+
result.skipped.push(destName);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
if (srcName === ".env") {
|
|
227
|
+
// Rewrite CODEX_MONITOR_ prefixes to OPENFLEET_ in .env content
|
|
228
|
+
const content = readFileSync(src, "utf8");
|
|
229
|
+
const updated = rewriteEnvContent(content);
|
|
230
|
+
writeFileSync(dest, updated, "utf8");
|
|
231
|
+
} else {
|
|
232
|
+
copyFileSync(src, dest);
|
|
233
|
+
}
|
|
234
|
+
result.migrated.push(destName);
|
|
235
|
+
} catch (e) {
|
|
236
|
+
result.errors.push(`${destName}: ${e.message}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Rewrite .env file content: replace CODEX_MONITOR_X= with OPENFLEET_X=
|
|
245
|
+
* while preserving comments and blank lines.
|
|
246
|
+
*/
|
|
247
|
+
function rewriteEnvContent(content) {
|
|
248
|
+
return content
|
|
249
|
+
.split("\n")
|
|
250
|
+
.map((line) => {
|
|
251
|
+
// Match both active vars and commented-out vars
|
|
252
|
+
return line.replace(/^(#?\s*)CODEX_MONITOR_/, "$1OPENFLEET_");
|
|
253
|
+
})
|
|
254
|
+
.join("\n");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* If OPENFLEET_DIR is not set but CODEX_MONITOR_DIR is (or legacy dir exists),
|
|
259
|
+
* transparently set OPENFLEET_DIR to point at the legacy dir so openfleet reads
|
|
260
|
+
* from it without requiring a migration step.
|
|
261
|
+
*
|
|
262
|
+
* This is the "zero-friction" path: existing users just upgrade the package and
|
|
263
|
+
* it works. Migration is optional (improves going forward).
|
|
264
|
+
*/
|
|
265
|
+
export function autoApplyLegacyDir() {
|
|
266
|
+
// Already set — nothing to do
|
|
267
|
+
if (process.env.OPENFLEET_DIR) return false;
|
|
268
|
+
|
|
269
|
+
const legacyDir = getLegacyConfigDir();
|
|
270
|
+
if (!legacyDir) return false;
|
|
271
|
+
|
|
272
|
+
process.env.OPENFLEET_DIR = legacyDir;
|
|
273
|
+
console.log(
|
|
274
|
+
`[compat] Legacy codex-monitor config detected at ${legacyDir} — using it as OPENFLEET_DIR.`,
|
|
275
|
+
);
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Run full compatibility setup: alias env vars + auto-apply legacy dir.
|
|
281
|
+
* Call this before loadConfig().
|
|
282
|
+
*/
|
|
283
|
+
export function applyAllCompatibility() {
|
|
284
|
+
applyLegacyEnvAliases();
|
|
285
|
+
autoApplyLegacyDir();
|
|
286
|
+
}
|
package/config.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
getAgentPromptDefinitions,
|
|
24
24
|
resolveAgentPrompts,
|
|
25
25
|
} from "./agent-prompts.mjs";
|
|
26
|
+
import { applyAllCompatibility } from "./compat.mjs";
|
|
26
27
|
|
|
27
28
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
28
29
|
|
|
@@ -719,6 +720,9 @@ function loadAgentPrompts(configDir, repoRoot, configData) {
|
|
|
719
720
|
* Returns a frozen config object used by all modules.
|
|
720
721
|
*/
|
|
721
722
|
export function loadConfig(argv = process.argv, options = {}) {
|
|
723
|
+
// Apply legacy CODEX_MONITOR_* → OPENFLEET_* compatibility before anything else
|
|
724
|
+
applyAllCompatibility();
|
|
725
|
+
|
|
722
726
|
const { reloadEnv = false } = options;
|
|
723
727
|
const cli = parseArgs(argv);
|
|
724
728
|
|
package/kanban-adapter.mjs
CHANGED
|
@@ -2581,7 +2581,7 @@ class GitHubIssuesAdapter {
|
|
|
2581
2581
|
const commentBody = `<!-- openfleet-state
|
|
2582
2582
|
${stateJson}
|
|
2583
2583
|
-->
|
|
2584
|
-
**
|
|
2584
|
+
**OpenFleet Status**: Agent \`${agentId}\` on \`${workstationId}\` is ${normalizedState.status === "working" ? "working on" : normalizedState.status === "claimed" ? "claiming" : "stale for"} this task.
|
|
2585
2585
|
*Last heartbeat: ${normalizedState.heartbeat || normalizedState.ownerHeartbeat}*`;
|
|
2586
2586
|
|
|
2587
2587
|
if (stateCommentIndex >= 0) {
|
|
@@ -2709,7 +2709,7 @@ ${stateJson}
|
|
|
2709
2709
|
);
|
|
2710
2710
|
|
|
2711
2711
|
// Add comment explaining why
|
|
2712
|
-
const commentBody = `**
|
|
2712
|
+
const commentBody = `**OpenFleet**: This task has been marked as ignored.
|
|
2713
2713
|
|
|
2714
2714
|
**Reason**: ${reason}
|
|
2715
2715
|
|
|
@@ -3581,7 +3581,7 @@ class JiraAdapter {
|
|
|
3581
3581
|
const json = JSON.stringify(normalized, null, 2);
|
|
3582
3582
|
return (
|
|
3583
3583
|
`<!-- openfleet-state\n${json}\n-->\n` +
|
|
3584
|
-
`
|
|
3584
|
+
`OpenFleet Status: Agent ${agentId} on ${workstationId} is ${normalized?.status} this task.\n` +
|
|
3585
3585
|
`Last heartbeat: ${normalized?.heartbeat || normalized?.ownerHeartbeat || ""}`
|
|
3586
3586
|
);
|
|
3587
3587
|
}
|
|
@@ -4245,7 +4245,7 @@ class JiraAdapter {
|
|
|
4245
4245
|
* {
|
|
4246
4246
|
* "type": "paragraph",
|
|
4247
4247
|
* "content": [
|
|
4248
|
-
* {"type": "text", "text": "
|
|
4248
|
+
* {"type": "text", "text": "OpenFleet: Task marked as ignored."}
|
|
4249
4249
|
* ]
|
|
4250
4250
|
* }
|
|
4251
4251
|
* ]
|
|
@@ -4294,7 +4294,7 @@ class JiraAdapter {
|
|
|
4294
4294
|
});
|
|
4295
4295
|
}
|
|
4296
4296
|
const commentBody =
|
|
4297
|
-
`
|
|
4297
|
+
`OpenFleet: This task has been marked as ignored.\n\n` +
|
|
4298
4298
|
`Reason: ${ignoreReason}\n\n` +
|
|
4299
4299
|
`To re-enable openfleet for this task, remove the ${this._codexLabels.ignore} label.`;
|
|
4300
4300
|
await this.addComment(key, commentBody);
|
package/monitor.mjs
CHANGED
|
@@ -46,6 +46,8 @@ import {
|
|
|
46
46
|
getDigestSnapshot,
|
|
47
47
|
startStatusFileWriter,
|
|
48
48
|
stopStatusFileWriter,
|
|
49
|
+
initStatusBoard,
|
|
50
|
+
pushStatusBoardUpdate,
|
|
49
51
|
} from "./telegram-bot.mjs";
|
|
50
52
|
import { PRCleanupDaemon } from "./pr-cleanup-daemon.mjs";
|
|
51
53
|
import {
|
|
@@ -1739,6 +1741,11 @@ function notifyVkError(line) {
|
|
|
1739
1741
|
if (!isVkBackendActive()) {
|
|
1740
1742
|
return;
|
|
1741
1743
|
}
|
|
1744
|
+
// If the user explicitly disabled VK spawning they know VK isn't running —
|
|
1745
|
+
// spamming "unreachable" every 10 minutes is pure noise.
|
|
1746
|
+
if (!vkSpawnEnabled) {
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1742
1749
|
const key = "vibe-kanban-unavailable";
|
|
1743
1750
|
const now = Date.now();
|
|
1744
1751
|
const last = vkErrorNotified.get(key) || 0;
|
|
@@ -3401,6 +3408,14 @@ async function fetchTasksByStatus(status) {
|
|
|
3401
3408
|
const backend = getActiveKanbanBackend();
|
|
3402
3409
|
if (backend !== "vk") {
|
|
3403
3410
|
try {
|
|
3411
|
+
// Internal backend uses file-based storage — no project ID required.
|
|
3412
|
+
if (backend === "internal") {
|
|
3413
|
+
const tasks = status
|
|
3414
|
+
? getInternalTasksByStatus(status)
|
|
3415
|
+
: getAllInternalTasks();
|
|
3416
|
+
return Array.isArray(tasks) ? tasks : [];
|
|
3417
|
+
}
|
|
3418
|
+
|
|
3404
3419
|
const projectId = getConfiguredKanbanProjectId(backend);
|
|
3405
3420
|
if (!projectId) {
|
|
3406
3421
|
console.warn(
|
|
@@ -7796,6 +7811,10 @@ async function sendTelegramMessage(text, options = {}) {
|
|
|
7796
7811
|
}
|
|
7797
7812
|
}
|
|
7798
7813
|
|
|
7814
|
+
// Allow caller to explicitly override the computed priority (e.g. auto-update
|
|
7815
|
+
// restart notifications should always arrive as direct messages, not digest).
|
|
7816
|
+
if (options.priority !== undefined) priority = Number(options.priority);
|
|
7817
|
+
|
|
7799
7818
|
// Route through batching system — apply verbosity filter first.
|
|
7800
7819
|
// minimal: only priority 1-2 (critical + error)
|
|
7801
7820
|
// summary: priority 1-4 (everything except debug) — DEFAULT
|
|
@@ -8278,10 +8297,17 @@ async function startTelegramNotifier() {
|
|
|
8278
8297
|
const sendUpdate = async () => {
|
|
8279
8298
|
const summary = await readStatusSummary();
|
|
8280
8299
|
if (summary && summary.text) {
|
|
8281
|
-
|
|
8300
|
+
// Push to the pinned status board (edits in-place) when available.
|
|
8301
|
+
// Fall back to a regular new message only if the board hasn't been set up.
|
|
8302
|
+
const routed = pushStatusBoardUpdate(summary.text, {
|
|
8282
8303
|
parseMode: summary.parseMode,
|
|
8283
|
-
disablePreview: true,
|
|
8284
8304
|
});
|
|
8305
|
+
if (!routed) {
|
|
8306
|
+
await sendTelegramMessage(summary.text, {
|
|
8307
|
+
parseMode: summary.parseMode,
|
|
8308
|
+
disablePreview: true,
|
|
8309
|
+
});
|
|
8310
|
+
}
|
|
8285
8311
|
}
|
|
8286
8312
|
await flushMergeNotifications();
|
|
8287
8313
|
await checkStatusMilestones();
|
|
@@ -11466,7 +11492,9 @@ startAutoUpdateLoop({
|
|
|
11466
11492
|
onRestart: (reason) => restartSelf(reason),
|
|
11467
11493
|
onNotify: (msg) => {
|
|
11468
11494
|
try {
|
|
11469
|
-
|
|
11495
|
+
// Priority 1 (critical) bypasses the live digest so the user gets a
|
|
11496
|
+
// direct push notification for update-detected and restarting events.
|
|
11497
|
+
void sendTelegramMessage(msg, { priority: 1, skipDedup: true });
|
|
11470
11498
|
} catch {
|
|
11471
11499
|
/* best-effort */
|
|
11472
11500
|
}
|
|
@@ -12019,9 +12047,11 @@ if (telegramCommandEnabled) {
|
|
|
12019
12047
|
// Restore live digest state BEFORE any messages flow — so restarts continue the
|
|
12020
12048
|
// existing digest message instead of creating a new one.
|
|
12021
12049
|
// Chain notifier start after restore to prevent race conditions.
|
|
12050
|
+
// Also initialise the pinned status board (creates/restores the persistent message).
|
|
12022
12051
|
void restoreLiveDigest()
|
|
12023
12052
|
.catch(() => {})
|
|
12024
|
-
.then(() => startTelegramNotifier())
|
|
12053
|
+
.then(() => startTelegramNotifier())
|
|
12054
|
+
.then(() => initStatusBoard().catch(() => {}));
|
|
12025
12055
|
|
|
12026
12056
|
// ── Start long-running devmode monitor-monitor supervisor ───────────────────
|
|
12027
12057
|
startMonitorMonitorSupervisor();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@virtengine/openfleet",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.3",
|
|
4
4
|
"description": "AI-powered orchestrator supervisor — manages AI agent executors with failover, auto-restarts on failure, analyzes crashes with Codex SDK, creates PRs via Vibe-Kanban API, and sends Telegram notifications. Supports N executors with weighted distribution, multi-repo projects, and auto-setup.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache 2.0",
|
|
@@ -62,7 +62,8 @@
|
|
|
62
62
|
"./startup-service": "./startup-service.mjs",
|
|
63
63
|
"./telegram-sentinel": "./telegram-sentinel.mjs",
|
|
64
64
|
"./whatsapp-channel": "./whatsapp-channel.mjs",
|
|
65
|
-
"./container-runner": "./container-runner.mjs"
|
|
65
|
+
"./container-runner": "./container-runner.mjs",
|
|
66
|
+
"./compat": "./compat.mjs"
|
|
66
67
|
},
|
|
67
68
|
"bin": {
|
|
68
69
|
"openfleet": "cli.mjs",
|
|
@@ -110,6 +111,7 @@
|
|
|
110
111
|
"autofix.mjs",
|
|
111
112
|
"claude-shell.mjs",
|
|
112
113
|
"cli.mjs",
|
|
114
|
+
"compat.mjs",
|
|
113
115
|
"codex-config.mjs",
|
|
114
116
|
"openfleet.config.example.json",
|
|
115
117
|
"openfleet.schema.json",
|
package/presence.mjs
CHANGED
|
@@ -286,7 +286,7 @@ export function formatPresenceSummary({ nowMs, ttlMs } = {}) {
|
|
|
286
286
|
return "No active instances reported.";
|
|
287
287
|
}
|
|
288
288
|
const coordinator = selectCoordinator({ nowMs, ttlMs });
|
|
289
|
-
const lines = ["🛰️
|
|
289
|
+
const lines = ["🛰️ OpenFleet Presence"];
|
|
290
290
|
for (const entry of active) {
|
|
291
291
|
const name = entry.instance_label || entry.instance_id;
|
|
292
292
|
const role = entry.workspace_role || "workspace";
|
package/publish.mjs
CHANGED
|
@@ -225,9 +225,9 @@ function main() {
|
|
|
225
225
|
if (status === 0 && !dryRun) {
|
|
226
226
|
console.log(
|
|
227
227
|
"\n[publish] REMINDER: deprecate the legacy npm package to redirect users:\n" +
|
|
228
|
-
" npm deprecate
|
|
228
|
+
" npm deprecate openfleet@'*' \"Renamed to @virtengine/openfleet. Install: npm install -g @virtengine/openfleet\"\n" +
|
|
229
229
|
" # If a scoped legacy package exists:\n" +
|
|
230
|
-
" npm deprecate @virtengine/
|
|
230
|
+
" npm deprecate @virtengine/openfleet@'*' \"Renamed to @virtengine/openfleet. Install: npm install -g @virtengine/openfleet\"\n",
|
|
231
231
|
);
|
|
232
232
|
}
|
|
233
233
|
process.exit(status);
|
package/setup.mjs
CHANGED
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
normalizeHookTargets,
|
|
45
45
|
scaffoldAgentHookFiles,
|
|
46
46
|
} from "./hook-profiles.mjs";
|
|
47
|
+
import { detectLegacySetup, applyAllCompatibility } from "./compat.mjs";
|
|
47
48
|
|
|
48
49
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
49
50
|
|
|
@@ -3904,6 +3905,14 @@ async function writeConfigFiles({ env, configJson, repoRoot, configDir }) {
|
|
|
3904
3905
|
* Called from monitor.mjs before starting the main loop.
|
|
3905
3906
|
*/
|
|
3906
3907
|
export function shouldRunSetup() {
|
|
3908
|
+
// Apply legacy compat so OPENFLEET_DIR is set before resolveConfigDir is called
|
|
3909
|
+
applyAllCompatibility();
|
|
3910
|
+
|
|
3911
|
+
// If a legacy codex-monitor setup exists and the user hasn't migrated yet,
|
|
3912
|
+
// skip the setup wizard — they are already configured.
|
|
3913
|
+
const legacyInfo = detectLegacySetup();
|
|
3914
|
+
if (legacyInfo.hasLegacy) return false;
|
|
3915
|
+
|
|
3907
3916
|
const repoRoot = detectRepoRoot();
|
|
3908
3917
|
const configDir = resolveConfigDir(repoRoot);
|
|
3909
3918
|
return !hasSetupMarkers(configDir);
|
package/shared-state-manager.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Shared State Manager for
|
|
2
|
+
* @fileoverview Shared State Manager for OpenFleet Task Coordination
|
|
3
3
|
*
|
|
4
4
|
* Manages distributed task execution state across multiple agents and workstations.
|
|
5
5
|
* Provides atomic operations for claiming, updating, and releasing task ownership
|
package/startup-service.mjs
CHANGED
|
@@ -32,7 +32,7 @@ import { fileURLToPath } from "node:url";
|
|
|
32
32
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
33
33
|
|
|
34
34
|
const SERVICE_LABEL = "com.openfleet.service";
|
|
35
|
-
const TASK_NAME = "
|
|
35
|
+
const TASK_NAME = "OpenFleet";
|
|
36
36
|
const SYSTEMD_UNIT = "openfleet.service";
|
|
37
37
|
|
|
38
38
|
// ── Platform Detection ───────────────────────────────────────────────────────
|
|
@@ -121,7 +121,7 @@ function generateTaskSchedulerXml({ daemon = true } = {}) {
|
|
|
121
121
|
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
122
122
|
<RegistrationInfo>
|
|
123
123
|
<Description>Auto-start openfleet AI orchestrator on login</Description>
|
|
124
|
-
<Author>
|
|
124
|
+
<Author>OpenFleet</Author>
|
|
125
125
|
<URI>\\${TASK_NAME}</URI>
|
|
126
126
|
</RegistrationInfo>
|
|
127
127
|
<Triggers>
|