@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.
- package/.env.example +914 -0
- package/LICENSE +190 -0
- package/README.md +500 -0
- package/agent-endpoint.mjs +918 -0
- package/agent-hook-bridge.mjs +230 -0
- package/agent-hooks.mjs +1188 -0
- package/agent-pool.mjs +2403 -0
- package/agent-prompts.mjs +689 -0
- package/agent-sdk.mjs +141 -0
- package/anomaly-detector.mjs +1195 -0
- package/autofix.mjs +1294 -0
- package/claude-shell.mjs +708 -0
- package/cli.mjs +906 -0
- package/codex-config.mjs +1274 -0
- package/codex-model-profiles.mjs +135 -0
- package/codex-shell.mjs +762 -0
- package/config-doctor.mjs +613 -0
- package/config.mjs +1720 -0
- package/conflict-resolver.mjs +248 -0
- package/container-runner.mjs +450 -0
- package/copilot-shell.mjs +827 -0
- package/daemon-restart-policy.mjs +56 -0
- package/diff-stats.mjs +282 -0
- package/error-detector.mjs +829 -0
- package/fetch-runtime.mjs +34 -0
- package/fleet-coordinator.mjs +838 -0
- package/get-telegram-chat-id.mjs +71 -0
- package/git-safety.mjs +170 -0
- package/github-reconciler.mjs +403 -0
- package/hook-profiles.mjs +651 -0
- package/kanban-adapter.mjs +4491 -0
- package/lib/logger.mjs +645 -0
- package/maintenance.mjs +828 -0
- package/merge-strategy.mjs +1171 -0
- package/monitor.mjs +12207 -0
- package/openfleet.config.example.json +115 -0
- package/openfleet.schema.json +465 -0
- package/package.json +203 -0
- package/postinstall.mjs +187 -0
- package/pr-cleanup-daemon.mjs +978 -0
- package/preflight.mjs +408 -0
- package/prepublish-check.mjs +90 -0
- package/presence.mjs +328 -0
- package/primary-agent.mjs +282 -0
- package/publish.mjs +151 -0
- package/repo-root.mjs +29 -0
- package/restart-controller.mjs +100 -0
- package/review-agent.mjs +557 -0
- package/rotate-agent-logs.sh +133 -0
- package/sdk-conflict-resolver.mjs +973 -0
- package/session-tracker.mjs +880 -0
- package/setup.mjs +3937 -0
- package/shared-knowledge.mjs +410 -0
- package/shared-state-manager.mjs +841 -0
- package/shared-workspace-cli.mjs +199 -0
- package/shared-workspace-registry.mjs +537 -0
- package/shared-workspaces.json +18 -0
- package/startup-service.mjs +1070 -0
- package/sync-engine.mjs +1063 -0
- package/task-archiver.mjs +801 -0
- package/task-assessment.mjs +550 -0
- package/task-claims.mjs +924 -0
- package/task-complexity.mjs +581 -0
- package/task-executor.mjs +5111 -0
- package/task-store.mjs +753 -0
- package/telegram-bot.mjs +9281 -0
- package/telegram-sentinel.mjs +2010 -0
- package/ui/app.js +867 -0
- package/ui/app.legacy.js +1464 -0
- package/ui/app.monolith.js +2488 -0
- package/ui/components/charts.js +226 -0
- package/ui/components/chat-view.js +567 -0
- package/ui/components/command-palette.js +587 -0
- package/ui/components/diff-viewer.js +190 -0
- package/ui/components/forms.js +327 -0
- package/ui/components/kanban-board.js +451 -0
- package/ui/components/session-list.js +305 -0
- package/ui/components/shared.js +473 -0
- package/ui/index.html +70 -0
- package/ui/modules/api.js +297 -0
- package/ui/modules/icons.js +461 -0
- package/ui/modules/router.js +81 -0
- package/ui/modules/settings-schema.js +261 -0
- package/ui/modules/state.js +679 -0
- package/ui/modules/telegram.js +331 -0
- package/ui/modules/utils.js +270 -0
- package/ui/styles/animations.css +140 -0
- package/ui/styles/base.css +98 -0
- package/ui/styles/components.css +1915 -0
- package/ui/styles/kanban.css +286 -0
- package/ui/styles/layout.css +809 -0
- package/ui/styles/sessions.css +827 -0
- package/ui/styles/variables.css +188 -0
- package/ui/styles.css +141 -0
- package/ui/styles.monolith.css +1046 -0
- package/ui/tabs/agents.js +1417 -0
- package/ui/tabs/chat.js +74 -0
- package/ui/tabs/control.js +887 -0
- package/ui/tabs/dashboard.js +515 -0
- package/ui/tabs/infra.js +537 -0
- package/ui/tabs/logs.js +783 -0
- package/ui/tabs/settings.js +1487 -0
- package/ui/tabs/tasks.js +1385 -0
- package/ui-server.mjs +4073 -0
- package/update-check.mjs +465 -0
- package/utils.mjs +172 -0
- package/ve-kanban.mjs +654 -0
- package/ve-kanban.ps1 +1365 -0
- package/ve-kanban.sh +18 -0
- package/ve-orchestrator.mjs +340 -0
- package/ve-orchestrator.ps1 +6546 -0
- package/ve-orchestrator.sh +18 -0
- package/vibe-kanban-wrapper.mjs +41 -0
- package/vk-error-resolver.mjs +470 -0
- package/vk-log-stream.mjs +914 -0
- package/whatsapp-channel.mjs +520 -0
- package/workspace-monitor.mjs +581 -0
- package/workspace-reaper.mjs +405 -0
- package/workspace-registry.mjs +238 -0
- package/worktree-manager.mjs +1266 -0
package/config.mjs
ADDED
|
@@ -0,0 +1,1720 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* openfleet — Configuration System
|
|
5
|
+
*
|
|
6
|
+
* Loads configuration from (in priority order):
|
|
7
|
+
* 1. CLI flags (--key value)
|
|
8
|
+
* 2. Environment variables
|
|
9
|
+
* 3. .env file
|
|
10
|
+
* 4. openfleet.config.json (project config)
|
|
11
|
+
* 5. Built-in defaults
|
|
12
|
+
*
|
|
13
|
+
* Executor configuration supports N executors with weights and failover.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { resolve, dirname, basename, relative, isAbsolute } from "node:path";
|
|
18
|
+
import { execSync } from "node:child_process";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { resolveAgentSdkConfig } from "./agent-sdk.mjs";
|
|
21
|
+
import {
|
|
22
|
+
ensureAgentPromptWorkspace,
|
|
23
|
+
getAgentPromptDefinitions,
|
|
24
|
+
resolveAgentPrompts,
|
|
25
|
+
} from "./agent-prompts.mjs";
|
|
26
|
+
|
|
27
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
|
|
29
|
+
const CONFIG_FILES = [
|
|
30
|
+
"openfleet.config.json",
|
|
31
|
+
".openfleet.json",
|
|
32
|
+
"openfleet.json",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
function hasSetupMarkers(dir) {
|
|
36
|
+
const markers = [".env", ...CONFIG_FILES];
|
|
37
|
+
return markers.some((name) => existsSync(resolve(dir, name)));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function hasConfigFiles(dir) {
|
|
41
|
+
return CONFIG_FILES.some((name) => existsSync(resolve(dir, name)));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isPathInside(parent, child) {
|
|
45
|
+
const rel = relative(parent, child);
|
|
46
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isWslInteropRuntime() {
|
|
50
|
+
return Boolean(
|
|
51
|
+
process.env.WSL_DISTRO_NAME ||
|
|
52
|
+
process.env.WSL_INTEROP ||
|
|
53
|
+
(process.platform === "win32" &&
|
|
54
|
+
String(process.env.HOME || "")
|
|
55
|
+
.trim()
|
|
56
|
+
.startsWith("/home/")),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resolveConfigDir(repoRoot) {
|
|
61
|
+
const repoPath = resolve(repoRoot || process.cwd());
|
|
62
|
+
const packageDir = resolve(__dirname);
|
|
63
|
+
if (isPathInside(repoPath, packageDir) || hasConfigFiles(packageDir)) {
|
|
64
|
+
return packageDir;
|
|
65
|
+
}
|
|
66
|
+
const preferWindowsDirs =
|
|
67
|
+
process.platform === "win32" && !isWslInteropRuntime();
|
|
68
|
+
const baseDir = preferWindowsDirs
|
|
69
|
+
? process.env.APPDATA ||
|
|
70
|
+
process.env.LOCALAPPDATA ||
|
|
71
|
+
process.env.USERPROFILE ||
|
|
72
|
+
process.env.HOME ||
|
|
73
|
+
process.cwd()
|
|
74
|
+
: process.env.HOME ||
|
|
75
|
+
process.env.XDG_CONFIG_HOME ||
|
|
76
|
+
process.env.USERPROFILE ||
|
|
77
|
+
process.env.APPDATA ||
|
|
78
|
+
process.env.LOCALAPPDATA ||
|
|
79
|
+
process.cwd();
|
|
80
|
+
return resolve(baseDir, "openfleet");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function ensurePromptWorkspaceGitIgnore(repoRoot) {
|
|
84
|
+
const gitignorePath = resolve(repoRoot, ".gitignore");
|
|
85
|
+
const entry = "/.openfleet/";
|
|
86
|
+
let existing = "";
|
|
87
|
+
try {
|
|
88
|
+
if (existsSync(gitignorePath)) {
|
|
89
|
+
existing = readFileSync(gitignorePath, "utf8");
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const hasEntry = existing
|
|
95
|
+
.split(/\r?\n/)
|
|
96
|
+
.map((line) => line.trim())
|
|
97
|
+
.includes(entry);
|
|
98
|
+
if (hasEntry) return;
|
|
99
|
+
const next =
|
|
100
|
+
existing.endsWith("\n") || !existing ? existing : `${existing}\n`;
|
|
101
|
+
try {
|
|
102
|
+
writeFileSync(gitignorePath, `${next}${entry}\n`, "utf8");
|
|
103
|
+
} catch {
|
|
104
|
+
/* best effort */
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── .env loader ──────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
function loadDotEnv(dir, options = {}) {
|
|
111
|
+
const { override = false } = options;
|
|
112
|
+
const envPath = resolve(dir, ".env");
|
|
113
|
+
if (!existsSync(envPath)) return;
|
|
114
|
+
const lines = readFileSync(envPath, "utf8").split("\n");
|
|
115
|
+
for (const line of lines) {
|
|
116
|
+
const trimmed = line.trim();
|
|
117
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
118
|
+
const eqIdx = trimmed.indexOf("=");
|
|
119
|
+
if (eqIdx === -1) continue;
|
|
120
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
121
|
+
let val = trimmed.slice(eqIdx + 1).trim();
|
|
122
|
+
// Strip surrounding quotes
|
|
123
|
+
if (
|
|
124
|
+
(val.startsWith('"') && val.endsWith('"')) ||
|
|
125
|
+
(val.startsWith("'") && val.endsWith("'"))
|
|
126
|
+
) {
|
|
127
|
+
val = val.slice(1, -1);
|
|
128
|
+
}
|
|
129
|
+
if (override || !(key in process.env)) {
|
|
130
|
+
process.env[key] = val;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function loadDotEnvFile(envPath, options = {}) {
|
|
136
|
+
const { override = false } = options;
|
|
137
|
+
const resolved = resolve(envPath);
|
|
138
|
+
if (!existsSync(resolved)) return;
|
|
139
|
+
const lines = readFileSync(resolved, "utf8").split("\n");
|
|
140
|
+
for (const line of lines) {
|
|
141
|
+
const trimmed = line.trim();
|
|
142
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
143
|
+
const eqIdx = trimmed.indexOf("=");
|
|
144
|
+
if (eqIdx === -1) continue;
|
|
145
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
146
|
+
let val = trimmed.slice(eqIdx + 1).trim();
|
|
147
|
+
if (
|
|
148
|
+
(val.startsWith('"') && val.endsWith('"')) ||
|
|
149
|
+
(val.startsWith("'") && val.endsWith("'"))
|
|
150
|
+
) {
|
|
151
|
+
val = val.slice(1, -1);
|
|
152
|
+
}
|
|
153
|
+
if (override || !(key in process.env)) {
|
|
154
|
+
process.env[key] = val;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function loadConfigFile(configDir) {
|
|
160
|
+
for (const name of CONFIG_FILES) {
|
|
161
|
+
const p = resolve(configDir, name);
|
|
162
|
+
if (!existsSync(p)) continue;
|
|
163
|
+
try {
|
|
164
|
+
const raw = JSON.parse(readFileSync(p, "utf8"));
|
|
165
|
+
return { path: p, data: raw };
|
|
166
|
+
} catch {
|
|
167
|
+
return { path: p, data: null, error: "invalid-json" };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Hint about the example template
|
|
171
|
+
const examplePath = resolve(configDir, "openfleet.config.example.json");
|
|
172
|
+
if (existsSync(examplePath)) {
|
|
173
|
+
console.log(
|
|
174
|
+
`[config] No openfleet.config.json found. Copy the example:\n` +
|
|
175
|
+
` cp ${examplePath} ${resolve(configDir, "openfleet.config.json")}`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return { path: null, data: null };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── CLI arg parser ───────────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
function parseArgs(argv) {
|
|
184
|
+
const args = argv.slice(2);
|
|
185
|
+
const result = { _positional: [], _flags: new Set() };
|
|
186
|
+
for (let i = 0; i < args.length; i++) {
|
|
187
|
+
if (args[i].startsWith("--")) {
|
|
188
|
+
const key = args[i].slice(2);
|
|
189
|
+
if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
|
|
190
|
+
result[key] = args[i + 1];
|
|
191
|
+
i++;
|
|
192
|
+
} else {
|
|
193
|
+
result._flags.add(key);
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
result._positional.push(args[i]);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── Config/profile helpers ───────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
function normalizeKey(value) {
|
|
205
|
+
return String(value || "")
|
|
206
|
+
.trim()
|
|
207
|
+
.toLowerCase();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function applyEnvProfile(profile, options = {}) {
|
|
211
|
+
if (!profile || typeof profile !== "object") return;
|
|
212
|
+
const env = profile.env;
|
|
213
|
+
if (!env || typeof env !== "object") return;
|
|
214
|
+
const override = profile.envOverride === true || options.override === true;
|
|
215
|
+
for (const [key, value] of Object.entries(env)) {
|
|
216
|
+
if (!override && key in process.env) continue;
|
|
217
|
+
process.env[key] = String(value);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function applyProfileOverrides(configData, profile) {
|
|
222
|
+
if (!configData || typeof configData !== "object") {
|
|
223
|
+
return configData || {};
|
|
224
|
+
}
|
|
225
|
+
if (!profile || typeof profile !== "object") {
|
|
226
|
+
return configData;
|
|
227
|
+
}
|
|
228
|
+
const overrides =
|
|
229
|
+
profile.overrides || profile.config || profile.settings || {};
|
|
230
|
+
if (!overrides || typeof overrides !== "object") {
|
|
231
|
+
return configData;
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
...configData,
|
|
235
|
+
...overrides,
|
|
236
|
+
repositories: overrides.repositories ?? configData.repositories,
|
|
237
|
+
executors: overrides.executors ?? configData.executors,
|
|
238
|
+
failover: overrides.failover ?? configData.failover,
|
|
239
|
+
distribution: overrides.distribution ?? configData.distribution,
|
|
240
|
+
agentPrompts: overrides.agentPrompts ?? configData.agentPrompts,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function resolveRepoPath(repoPath, baseDir) {
|
|
245
|
+
if (!repoPath) return "";
|
|
246
|
+
if (repoPath.startsWith("~")) {
|
|
247
|
+
return resolve(
|
|
248
|
+
process.env.HOME || process.env.USERPROFILE || "",
|
|
249
|
+
repoPath.slice(1),
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
return resolve(baseDir, repoPath);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function parseEnvBoolean(value, defaultValue) {
|
|
256
|
+
if (value === undefined || value === null || value === "") {
|
|
257
|
+
return defaultValue;
|
|
258
|
+
}
|
|
259
|
+
const raw = String(value).trim().toLowerCase();
|
|
260
|
+
if (["true", "1", "yes", "y", "on"].includes(raw)) return true;
|
|
261
|
+
if (["false", "0", "no", "n", "off"].includes(raw)) return false;
|
|
262
|
+
return defaultValue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function isEnvEnabled(value, defaultValue = false) {
|
|
266
|
+
return parseEnvBoolean(value, defaultValue);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── Git helpers ──────────────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
function detectRepoSlug() {
|
|
272
|
+
try {
|
|
273
|
+
const remote = execSync("git remote get-url origin", {
|
|
274
|
+
encoding: "utf8",
|
|
275
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
276
|
+
}).trim();
|
|
277
|
+
const match = remote.match(/github\.com[/:]([^/]+\/[^/.]+)/);
|
|
278
|
+
return match ? match[1] : null;
|
|
279
|
+
} catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function detectRepoRoot() {
|
|
285
|
+
try {
|
|
286
|
+
return execSync("git rev-parse --show-toplevel", {
|
|
287
|
+
encoding: "utf8",
|
|
288
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
289
|
+
}).trim();
|
|
290
|
+
} catch {
|
|
291
|
+
return process.cwd();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── Executor Configuration ───────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Executor config schema:
|
|
299
|
+
*
|
|
300
|
+
* {
|
|
301
|
+
* "executors": [
|
|
302
|
+
* {
|
|
303
|
+
* "name": "copilot-claude",
|
|
304
|
+
* "executor": "COPILOT",
|
|
305
|
+
* "variant": "CLAUDE_OPUS_4_6",
|
|
306
|
+
* "weight": 50,
|
|
307
|
+
* "role": "primary",
|
|
308
|
+
* "enabled": true
|
|
309
|
+
* },
|
|
310
|
+
* {
|
|
311
|
+
* "name": "codex-default",
|
|
312
|
+
* "executor": "CODEX",
|
|
313
|
+
* "variant": "DEFAULT",
|
|
314
|
+
* "weight": 50,
|
|
315
|
+
* "role": "backup",
|
|
316
|
+
* "enabled": true
|
|
317
|
+
* }
|
|
318
|
+
* ],
|
|
319
|
+
* "failover": {
|
|
320
|
+
* "strategy": "next-in-line", // "next-in-line" | "weighted-random" | "round-robin"
|
|
321
|
+
* "maxRetries": 3,
|
|
322
|
+
* "cooldownMinutes": 5,
|
|
323
|
+
* "disableOnConsecutiveFailures": 3
|
|
324
|
+
* },
|
|
325
|
+
* "distribution": "weighted" // "weighted" | "round-robin" | "primary-only"
|
|
326
|
+
* }
|
|
327
|
+
*/
|
|
328
|
+
|
|
329
|
+
const DEFAULT_EXECUTORS = {
|
|
330
|
+
executors: [
|
|
331
|
+
{
|
|
332
|
+
name: "codex-default",
|
|
333
|
+
executor: "CODEX",
|
|
334
|
+
variant: "DEFAULT",
|
|
335
|
+
weight: 100,
|
|
336
|
+
role: "primary",
|
|
337
|
+
enabled: true,
|
|
338
|
+
},
|
|
339
|
+
],
|
|
340
|
+
failover: {
|
|
341
|
+
strategy: "next-in-line",
|
|
342
|
+
maxRetries: 3,
|
|
343
|
+
cooldownMinutes: 5,
|
|
344
|
+
disableOnConsecutiveFailures: 3,
|
|
345
|
+
},
|
|
346
|
+
distribution: "primary-only",
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
function parseExecutorsFromEnv() {
|
|
350
|
+
// EXECUTORS=CODEX:DEFAULT:100
|
|
351
|
+
const raw = process.env.EXECUTORS;
|
|
352
|
+
if (!raw) return null;
|
|
353
|
+
const entries = raw.split(",").map((e) => e.trim());
|
|
354
|
+
const executors = [];
|
|
355
|
+
const roles = ["primary", "backup", "tertiary"];
|
|
356
|
+
for (let i = 0; i < entries.length; i++) {
|
|
357
|
+
const parts = entries[i].split(":");
|
|
358
|
+
if (parts.length < 2) continue;
|
|
359
|
+
executors.push({
|
|
360
|
+
name: `${parts[0].toLowerCase()}-${parts[1].toLowerCase()}`,
|
|
361
|
+
executor: parts[0].toUpperCase(),
|
|
362
|
+
variant: parts[1],
|
|
363
|
+
weight: parts[2] ? Number(parts[2]) : Math.floor(100 / entries.length),
|
|
364
|
+
role: roles[i] || `executor-${i + 1}`,
|
|
365
|
+
enabled: true,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
return executors.length ? executors : null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function normalizePrimaryAgent(value) {
|
|
372
|
+
const raw = String(value || "")
|
|
373
|
+
.trim()
|
|
374
|
+
.toLowerCase();
|
|
375
|
+
if (!raw) return "codex-sdk";
|
|
376
|
+
if (["codex", "codex-sdk"].includes(raw)) return "codex-sdk";
|
|
377
|
+
if (["copilot", "copilot-sdk", "github-copilot"].includes(raw))
|
|
378
|
+
return "copilot-sdk";
|
|
379
|
+
if (["claude", "claude-sdk", "claude_code", "claude-code"].includes(raw))
|
|
380
|
+
return "claude-sdk";
|
|
381
|
+
return raw;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function normalizeKanbanBackend(value) {
|
|
385
|
+
const backend = String(value || "")
|
|
386
|
+
.trim()
|
|
387
|
+
.toLowerCase();
|
|
388
|
+
if (
|
|
389
|
+
backend === "internal" ||
|
|
390
|
+
backend === "github" ||
|
|
391
|
+
backend === "jira" ||
|
|
392
|
+
backend === "vk"
|
|
393
|
+
) {
|
|
394
|
+
return backend;
|
|
395
|
+
}
|
|
396
|
+
return "internal";
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function normalizeKanbanSyncPolicy(value) {
|
|
400
|
+
const policy = String(value || "")
|
|
401
|
+
.trim()
|
|
402
|
+
.toLowerCase();
|
|
403
|
+
if (policy === "internal-primary" || policy === "bidirectional") {
|
|
404
|
+
return policy;
|
|
405
|
+
}
|
|
406
|
+
return "internal-primary";
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function normalizeProjectRequirementsProfile(value) {
|
|
410
|
+
const profile = String(value || "")
|
|
411
|
+
.trim()
|
|
412
|
+
.toLowerCase();
|
|
413
|
+
if (
|
|
414
|
+
[
|
|
415
|
+
"simple-feature",
|
|
416
|
+
"feature",
|
|
417
|
+
"large-feature",
|
|
418
|
+
"system",
|
|
419
|
+
"multi-system",
|
|
420
|
+
].includes(profile)
|
|
421
|
+
) {
|
|
422
|
+
return profile;
|
|
423
|
+
}
|
|
424
|
+
return "feature";
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function loadExecutorConfig(configDir, configData) {
|
|
428
|
+
// 1. Try env var
|
|
429
|
+
const fromEnv = parseExecutorsFromEnv();
|
|
430
|
+
|
|
431
|
+
// 2. Try config file
|
|
432
|
+
let fromFile = null;
|
|
433
|
+
if (configData && typeof configData === "object") {
|
|
434
|
+
fromFile = configData.executors ? configData : null;
|
|
435
|
+
}
|
|
436
|
+
if (!fromFile) {
|
|
437
|
+
for (const name of CONFIG_FILES) {
|
|
438
|
+
const p = resolve(configDir, name);
|
|
439
|
+
if (existsSync(p)) {
|
|
440
|
+
try {
|
|
441
|
+
const raw = JSON.parse(readFileSync(p, "utf8"));
|
|
442
|
+
fromFile = raw.executors ? raw : null;
|
|
443
|
+
break;
|
|
444
|
+
} catch {
|
|
445
|
+
/* invalid JSON — skip */
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const executors =
|
|
452
|
+
fromEnv || fromFile?.executors || DEFAULT_EXECUTORS.executors;
|
|
453
|
+
const failover = fromFile?.failover || {
|
|
454
|
+
strategy:
|
|
455
|
+
process.env.FAILOVER_STRATEGY || DEFAULT_EXECUTORS.failover.strategy,
|
|
456
|
+
maxRetries: Number(
|
|
457
|
+
process.env.FAILOVER_MAX_RETRIES || DEFAULT_EXECUTORS.failover.maxRetries,
|
|
458
|
+
),
|
|
459
|
+
cooldownMinutes: Number(
|
|
460
|
+
process.env.FAILOVER_COOLDOWN_MIN ||
|
|
461
|
+
DEFAULT_EXECUTORS.failover.cooldownMinutes,
|
|
462
|
+
),
|
|
463
|
+
disableOnConsecutiveFailures: Number(
|
|
464
|
+
process.env.FAILOVER_DISABLE_AFTER ||
|
|
465
|
+
DEFAULT_EXECUTORS.failover.disableOnConsecutiveFailures,
|
|
466
|
+
),
|
|
467
|
+
};
|
|
468
|
+
const distribution =
|
|
469
|
+
fromFile?.distribution ||
|
|
470
|
+
process.env.EXECUTOR_DISTRIBUTION ||
|
|
471
|
+
DEFAULT_EXECUTORS.distribution;
|
|
472
|
+
|
|
473
|
+
return { executors, failover, distribution };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ── Executor Scheduler ───────────────────────────────────────────────────────
|
|
477
|
+
|
|
478
|
+
class ExecutorScheduler {
|
|
479
|
+
constructor(config) {
|
|
480
|
+
this.executors = config.executors.filter((e) => e.enabled !== false);
|
|
481
|
+
this.failover = config.failover;
|
|
482
|
+
this.distribution = config.distribution;
|
|
483
|
+
this._roundRobinIndex = 0;
|
|
484
|
+
this._failureCounts = new Map(); // name → consecutive failures
|
|
485
|
+
this._disabledUntil = new Map(); // name → timestamp
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Get the next executor based on distribution strategy */
|
|
489
|
+
next() {
|
|
490
|
+
const available = this._getAvailable();
|
|
491
|
+
if (!available.length) {
|
|
492
|
+
// All disabled — reset and use primary
|
|
493
|
+
this._disabledUntil.clear();
|
|
494
|
+
this._failureCounts.clear();
|
|
495
|
+
return this.executors[0];
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
switch (this.distribution) {
|
|
499
|
+
case "round-robin":
|
|
500
|
+
return this._roundRobin(available);
|
|
501
|
+
case "primary-only":
|
|
502
|
+
return available[0];
|
|
503
|
+
case "weighted":
|
|
504
|
+
default:
|
|
505
|
+
return this._weightedSelect(available);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** Report a failure for an executor */
|
|
510
|
+
recordFailure(executorName) {
|
|
511
|
+
const count = (this._failureCounts.get(executorName) || 0) + 1;
|
|
512
|
+
this._failureCounts.set(executorName, count);
|
|
513
|
+
if (count >= this.failover.disableOnConsecutiveFailures) {
|
|
514
|
+
const until = Date.now() + this.failover.cooldownMinutes * 60 * 1000;
|
|
515
|
+
this._disabledUntil.set(executorName, until);
|
|
516
|
+
this._failureCounts.set(executorName, 0);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** Report a success for an executor */
|
|
521
|
+
recordSuccess(executorName) {
|
|
522
|
+
this._failureCounts.set(executorName, 0);
|
|
523
|
+
this._disabledUntil.delete(executorName);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** Get failover executor when current one fails */
|
|
527
|
+
getFailover(currentName) {
|
|
528
|
+
const available = this._getAvailable().filter(
|
|
529
|
+
(e) => e.name !== currentName,
|
|
530
|
+
);
|
|
531
|
+
if (!available.length) return null;
|
|
532
|
+
|
|
533
|
+
switch (this.failover.strategy) {
|
|
534
|
+
case "weighted-random":
|
|
535
|
+
return this._weightedSelect(available);
|
|
536
|
+
case "round-robin":
|
|
537
|
+
return available[0];
|
|
538
|
+
case "next-in-line":
|
|
539
|
+
default: {
|
|
540
|
+
// Find the next one by role priority
|
|
541
|
+
const roleOrder = [
|
|
542
|
+
"primary",
|
|
543
|
+
"backup",
|
|
544
|
+
"tertiary",
|
|
545
|
+
...Array.from({ length: 20 }, (_, i) => `executor-${i + 1}`),
|
|
546
|
+
];
|
|
547
|
+
available.sort(
|
|
548
|
+
(a, b) => roleOrder.indexOf(a.role) - roleOrder.indexOf(b.role),
|
|
549
|
+
);
|
|
550
|
+
return available[0];
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Get summary for display */
|
|
556
|
+
getSummary() {
|
|
557
|
+
const total = this.executors.reduce((s, e) => s + e.weight, 0);
|
|
558
|
+
return this.executors.map((e) => {
|
|
559
|
+
const pct = total > 0 ? Math.round((e.weight / total) * 100) : 0;
|
|
560
|
+
const disabled = this._isDisabled(e.name);
|
|
561
|
+
return {
|
|
562
|
+
...e,
|
|
563
|
+
percentage: pct,
|
|
564
|
+
status: disabled ? "cooldown" : e.enabled ? "active" : "disabled",
|
|
565
|
+
consecutiveFailures: this._failureCounts.get(e.name) || 0,
|
|
566
|
+
};
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** Format a display string like "COPILOT ⇄ CODEX (50/50)" */
|
|
571
|
+
toDisplayString() {
|
|
572
|
+
const summary = this.getSummary().filter((e) => e.status === "active");
|
|
573
|
+
if (!summary.length) return "No executors available";
|
|
574
|
+
return summary
|
|
575
|
+
.map((e) => `${e.executor}:${e.variant}(${e.percentage}%)`)
|
|
576
|
+
.join(" ⇄ ");
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
_getAvailable() {
|
|
580
|
+
return this.executors.filter(
|
|
581
|
+
(e) => e.enabled !== false && !this._isDisabled(e.name),
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
_isDisabled(name) {
|
|
586
|
+
const until = this._disabledUntil.get(name);
|
|
587
|
+
if (!until) return false;
|
|
588
|
+
if (Date.now() >= until) {
|
|
589
|
+
this._disabledUntil.delete(name);
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
_roundRobin(available) {
|
|
596
|
+
const idx = this._roundRobinIndex % available.length;
|
|
597
|
+
this._roundRobinIndex++;
|
|
598
|
+
return available[idx];
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
_weightedSelect(available) {
|
|
602
|
+
const totalWeight = available.reduce((s, e) => s + (e.weight || 1), 0);
|
|
603
|
+
let r = Math.random() * totalWeight;
|
|
604
|
+
for (const e of available) {
|
|
605
|
+
r -= e.weight || 1;
|
|
606
|
+
if (r <= 0) return e;
|
|
607
|
+
}
|
|
608
|
+
return available[available.length - 1];
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// ── Multi-Repo Support ───────────────────────────────────────────────────────
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Multi-repo config schema (supports defaults + selection):
|
|
616
|
+
*
|
|
617
|
+
* {
|
|
618
|
+
* "defaultRepository": "backend",
|
|
619
|
+
* "repositoryDefaults": {
|
|
620
|
+
* "orchestratorScript": "./orchestrator.ps1",
|
|
621
|
+
* "orchestratorArgs": "-MaxParallel 6",
|
|
622
|
+
* "profile": "local"
|
|
623
|
+
* },
|
|
624
|
+
* "repositories": [
|
|
625
|
+
* {
|
|
626
|
+
* "name": "backend",
|
|
627
|
+
* "path": "/path/to/backend",
|
|
628
|
+
* "slug": "org/backend",
|
|
629
|
+
* "primary": true
|
|
630
|
+
* },
|
|
631
|
+
* {
|
|
632
|
+
* "name": "frontend",
|
|
633
|
+
* "path": "/path/to/frontend",
|
|
634
|
+
* "slug": "org/frontend",
|
|
635
|
+
* "profile": "frontend"
|
|
636
|
+
* }
|
|
637
|
+
* ]
|
|
638
|
+
* }
|
|
639
|
+
*/
|
|
640
|
+
|
|
641
|
+
function normalizeRepoEntry(entry, defaults, baseDir) {
|
|
642
|
+
if (!entry || typeof entry !== "object") return null;
|
|
643
|
+
const name = String(entry.name || entry.id || "").trim();
|
|
644
|
+
if (!name) return null;
|
|
645
|
+
const repoPath =
|
|
646
|
+
entry.path || entry.repoRoot || defaults.path || defaults.repoRoot || "";
|
|
647
|
+
const resolvedPath = repoPath ? resolveRepoPath(repoPath, baseDir) : "";
|
|
648
|
+
const slug = entry.slug || entry.repo || defaults.slug || defaults.repo || "";
|
|
649
|
+
const aliases = Array.isArray(entry.aliases)
|
|
650
|
+
? entry.aliases.map(normalizeKey).filter(Boolean)
|
|
651
|
+
: [];
|
|
652
|
+
return {
|
|
653
|
+
...defaults,
|
|
654
|
+
...entry,
|
|
655
|
+
name,
|
|
656
|
+
id: normalizeKey(name),
|
|
657
|
+
path: resolvedPath,
|
|
658
|
+
slug,
|
|
659
|
+
aliases,
|
|
660
|
+
primary: entry.primary === true || defaults.primary === true,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function resolveRepoSelection(repositories, selection) {
|
|
665
|
+
if (!repositories || repositories.length === 0) return null;
|
|
666
|
+
const target = normalizeKey(selection);
|
|
667
|
+
if (!target) return null;
|
|
668
|
+
return (
|
|
669
|
+
repositories.find((repo) => repo.id === target) ||
|
|
670
|
+
repositories.find((repo) => normalizeKey(repo.name) === target) ||
|
|
671
|
+
repositories.find((repo) => normalizeKey(repo.slug) === target) ||
|
|
672
|
+
repositories.find((repo) => repo.aliases?.includes(target)) ||
|
|
673
|
+
null
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function loadRepoConfig(configDir, configData = {}, options = {}) {
|
|
678
|
+
const repoRootOverride = options.repoRootOverride || "";
|
|
679
|
+
const baseDir = configDir || process.cwd();
|
|
680
|
+
const repoDefaults =
|
|
681
|
+
configData.repositoryDefaults || configData.repositories?.defaults || {};
|
|
682
|
+
let repoEntries = null;
|
|
683
|
+
if (Array.isArray(configData.repositories)) {
|
|
684
|
+
repoEntries = configData.repositories;
|
|
685
|
+
} else if (Array.isArray(configData.repositories?.items)) {
|
|
686
|
+
repoEntries = configData.repositories.items;
|
|
687
|
+
} else if (Array.isArray(configData.repositories?.list)) {
|
|
688
|
+
repoEntries = configData.repositories.list;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (repoEntries && repoEntries.length) {
|
|
692
|
+
return repoEntries
|
|
693
|
+
.map((entry) => normalizeRepoEntry(entry, repoDefaults, baseDir))
|
|
694
|
+
.filter(Boolean);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const repoRoot = repoRootOverride || detectRepoRoot();
|
|
698
|
+
const slug = detectRepoSlug();
|
|
699
|
+
return [
|
|
700
|
+
{
|
|
701
|
+
name: basename(repoRoot),
|
|
702
|
+
id: normalizeKey(basename(repoRoot)),
|
|
703
|
+
path: repoRoot,
|
|
704
|
+
slug: process.env.GITHUB_REPO || slug || "unknown/unknown",
|
|
705
|
+
primary: true,
|
|
706
|
+
},
|
|
707
|
+
];
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function loadAgentPrompts(configDir, repoRoot, configData) {
|
|
711
|
+
const resolved = resolveAgentPrompts(configDir, repoRoot, configData);
|
|
712
|
+
return { ...resolved.prompts, _sources: resolved.sources };
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// ── Main Configuration Loader ────────────────────────────────────────────────
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Load the full openfleet configuration.
|
|
719
|
+
* Returns a frozen config object used by all modules.
|
|
720
|
+
*/
|
|
721
|
+
export function loadConfig(argv = process.argv, options = {}) {
|
|
722
|
+
const { reloadEnv = false } = options;
|
|
723
|
+
const cli = parseArgs(argv);
|
|
724
|
+
|
|
725
|
+
const repoRootForConfig = detectRepoRoot();
|
|
726
|
+
// Determine config directory (where openfleet stores its config)
|
|
727
|
+
const configDir =
|
|
728
|
+
cli["config-dir"] ||
|
|
729
|
+
process.env.CODEX_MONITOR_DIR ||
|
|
730
|
+
resolveConfigDir(repoRootForConfig);
|
|
731
|
+
|
|
732
|
+
const configFile = loadConfigFile(configDir);
|
|
733
|
+
let configData = configFile.data || {};
|
|
734
|
+
|
|
735
|
+
const repoRootOverride = cli["repo-root"] || process.env.REPO_ROOT || "";
|
|
736
|
+
let repositories = loadRepoConfig(configDir, configData, {
|
|
737
|
+
repoRootOverride,
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
const repoSelection =
|
|
741
|
+
cli["repo-name"] ||
|
|
742
|
+
cli.repository ||
|
|
743
|
+
process.env.CODEX_MONITOR_REPO ||
|
|
744
|
+
process.env.CODEX_MONITOR_REPO_NAME ||
|
|
745
|
+
process.env.REPO_NAME ||
|
|
746
|
+
configData.defaultRepository ||
|
|
747
|
+
configData.defaultRepo ||
|
|
748
|
+
configData.repositories?.default ||
|
|
749
|
+
"";
|
|
750
|
+
|
|
751
|
+
let selectedRepository =
|
|
752
|
+
resolveRepoSelection(repositories, repoSelection) ||
|
|
753
|
+
repositories.find((repo) => repo.primary) ||
|
|
754
|
+
repositories[0] ||
|
|
755
|
+
null;
|
|
756
|
+
|
|
757
|
+
let repoRoot =
|
|
758
|
+
repoRootOverride || selectedRepository?.path || detectRepoRoot();
|
|
759
|
+
|
|
760
|
+
// Load .env from config dir
|
|
761
|
+
loadDotEnv(configDir, { override: reloadEnv });
|
|
762
|
+
|
|
763
|
+
// Also load .env from repo root if different
|
|
764
|
+
if (resolve(repoRoot) !== resolve(configDir)) {
|
|
765
|
+
loadDotEnv(repoRoot, { override: reloadEnv });
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const initialRepoRoot = repoRoot;
|
|
769
|
+
|
|
770
|
+
const profiles = configData.profiles || configData.envProfiles || {};
|
|
771
|
+
const defaultProfile =
|
|
772
|
+
configData.defaultProfile ||
|
|
773
|
+
configData.defaultEnvProfile ||
|
|
774
|
+
(profiles.default ? "default" : "");
|
|
775
|
+
const profileName =
|
|
776
|
+
cli.profile ||
|
|
777
|
+
process.env.CODEX_MONITOR_PROFILE ||
|
|
778
|
+
process.env.CODEX_MONITOR_ENV_PROFILE ||
|
|
779
|
+
selectedRepository?.profile ||
|
|
780
|
+
selectedRepository?.envProfile ||
|
|
781
|
+
defaultProfile ||
|
|
782
|
+
"";
|
|
783
|
+
const profile = profileName ? profiles[profileName] : null;
|
|
784
|
+
|
|
785
|
+
if (profile?.envFile) {
|
|
786
|
+
const envFilePath = resolve(configDir, profile.envFile);
|
|
787
|
+
loadDotEnvFile(envFilePath, { override: profile.envOverride === true });
|
|
788
|
+
}
|
|
789
|
+
applyEnvProfile(profile, { override: reloadEnv });
|
|
790
|
+
|
|
791
|
+
// Apply profile overrides (executors, repos, etc.)
|
|
792
|
+
configData = applyProfileOverrides(configData, profile);
|
|
793
|
+
repositories = loadRepoConfig(configDir, configData, { repoRootOverride });
|
|
794
|
+
selectedRepository =
|
|
795
|
+
resolveRepoSelection(
|
|
796
|
+
repositories,
|
|
797
|
+
repoSelection ||
|
|
798
|
+
profile?.repository ||
|
|
799
|
+
profile?.repo ||
|
|
800
|
+
profile?.defaultRepository ||
|
|
801
|
+
"",
|
|
802
|
+
) ||
|
|
803
|
+
repositories.find((repo) => repo.primary) ||
|
|
804
|
+
repositories[0] ||
|
|
805
|
+
null;
|
|
806
|
+
repoRoot = repoRootOverride || selectedRepository?.path || detectRepoRoot();
|
|
807
|
+
|
|
808
|
+
if (resolve(repoRoot) !== resolve(initialRepoRoot)) {
|
|
809
|
+
loadDotEnv(repoRoot, { override: reloadEnv });
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const envPaths = [
|
|
813
|
+
resolve(configDir, ".env"),
|
|
814
|
+
resolve(repoRoot, ".env"),
|
|
815
|
+
].filter((p, i, arr) => arr.indexOf(p) === i);
|
|
816
|
+
|
|
817
|
+
// ── Project identity ─────────────────────────────────────
|
|
818
|
+
const projectName =
|
|
819
|
+
cli["project-name"] ||
|
|
820
|
+
process.env.PROJECT_NAME ||
|
|
821
|
+
process.env.VK_PROJECT_NAME ||
|
|
822
|
+
selectedRepository?.projectName ||
|
|
823
|
+
configData.projectName ||
|
|
824
|
+
detectProjectName(configDir, repoRoot);
|
|
825
|
+
|
|
826
|
+
const repoSlug =
|
|
827
|
+
cli["repo"] ||
|
|
828
|
+
process.env.GITHUB_REPO ||
|
|
829
|
+
selectedRepository?.slug ||
|
|
830
|
+
detectRepoSlug() ||
|
|
831
|
+
"unknown/unknown";
|
|
832
|
+
|
|
833
|
+
const repoUrlBase =
|
|
834
|
+
process.env.GITHUB_REPO_URL ||
|
|
835
|
+
selectedRepository?.repoUrlBase ||
|
|
836
|
+
`https://github.com/${repoSlug}`;
|
|
837
|
+
|
|
838
|
+
const mode =
|
|
839
|
+
(
|
|
840
|
+
cli.mode ||
|
|
841
|
+
process.env.CODEX_MONITOR_MODE ||
|
|
842
|
+
configData.mode ||
|
|
843
|
+
selectedRepository?.mode ||
|
|
844
|
+
""
|
|
845
|
+
)
|
|
846
|
+
.toString()
|
|
847
|
+
.toLowerCase() ||
|
|
848
|
+
(String(findOrchestratorScript(configDir, repoRoot)).includes(
|
|
849
|
+
"ve-orchestrator",
|
|
850
|
+
)
|
|
851
|
+
? "virtengine"
|
|
852
|
+
: "generic");
|
|
853
|
+
|
|
854
|
+
// ── Orchestrator ─────────────────────────────────────────
|
|
855
|
+
const defaultScript =
|
|
856
|
+
selectedRepository?.orchestratorScript ||
|
|
857
|
+
configData.orchestratorScript ||
|
|
858
|
+
findOrchestratorScript(configDir, repoRoot);
|
|
859
|
+
const defaultArgs =
|
|
860
|
+
mode === "virtengine" ? "-MaxParallel 6 -WaitForMutex" : "";
|
|
861
|
+
const rawScript =
|
|
862
|
+
cli.script || process.env.ORCHESTRATOR_SCRIPT || defaultScript;
|
|
863
|
+
// Resolve relative paths against configDir (not cwd) so that
|
|
864
|
+
// "../ve-orchestrator.ps1" always resolves to scripts/ve-orchestrator.ps1
|
|
865
|
+
// regardless of what directory the process was started from.
|
|
866
|
+
let scriptPath = resolve(configDir, rawScript);
|
|
867
|
+
// If the resolved path doesn't exist and rawScript is just a filename (no path separators),
|
|
868
|
+
// fall back to auto-detection to find it in common locations.
|
|
869
|
+
if (
|
|
870
|
+
!existsSync(scriptPath) &&
|
|
871
|
+
!rawScript.includes("/") &&
|
|
872
|
+
!rawScript.includes("\\")
|
|
873
|
+
) {
|
|
874
|
+
const autoDetected = findOrchestratorScript(configDir, repoRoot);
|
|
875
|
+
if (existsSync(autoDetected)) {
|
|
876
|
+
scriptPath = autoDetected;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
const scriptArgsRaw =
|
|
880
|
+
cli.args ||
|
|
881
|
+
process.env.ORCHESTRATOR_ARGS ||
|
|
882
|
+
selectedRepository?.orchestratorArgs ||
|
|
883
|
+
configData.orchestratorArgs ||
|
|
884
|
+
defaultArgs;
|
|
885
|
+
const scriptArgs = scriptArgsRaw.split(" ").filter(Boolean);
|
|
886
|
+
|
|
887
|
+
// ── Timing ───────────────────────────────────────────────
|
|
888
|
+
const restartDelayMs = Number(
|
|
889
|
+
cli["restart-delay"] || process.env.RESTART_DELAY_MS || "10000",
|
|
890
|
+
);
|
|
891
|
+
const maxRestarts = Number(
|
|
892
|
+
cli["max-restarts"] || process.env.MAX_RESTARTS || "0",
|
|
893
|
+
);
|
|
894
|
+
|
|
895
|
+
// ── Logging ──────────────────────────────────────────────
|
|
896
|
+
const logDir = resolve(
|
|
897
|
+
cli["log-dir"] ||
|
|
898
|
+
process.env.LOG_DIR ||
|
|
899
|
+
selectedRepository?.logDir ||
|
|
900
|
+
configData.logDir ||
|
|
901
|
+
resolve(configDir, "logs"),
|
|
902
|
+
);
|
|
903
|
+
// Max total size of the log directory in MB. 0 = unlimited.
|
|
904
|
+
const logMaxSizeMb = Number(
|
|
905
|
+
process.env.LOG_MAX_SIZE_MB ?? configData.logMaxSizeMb ?? 500,
|
|
906
|
+
);
|
|
907
|
+
// How often to check log folder size (minutes). 0 = only at startup.
|
|
908
|
+
const logCleanupIntervalMin = Number(
|
|
909
|
+
process.env.LOG_CLEANUP_INTERVAL_MIN ??
|
|
910
|
+
configData.logCleanupIntervalMin ??
|
|
911
|
+
30,
|
|
912
|
+
);
|
|
913
|
+
|
|
914
|
+
// ── Agent SDK Selection ───────────────────────────────────
|
|
915
|
+
const agentSdk = resolveAgentSdkConfig();
|
|
916
|
+
|
|
917
|
+
// ── Feature flags ────────────────────────────────────────
|
|
918
|
+
const flags = cli._flags;
|
|
919
|
+
const watchEnabled = flags.has("no-watch")
|
|
920
|
+
? false
|
|
921
|
+
: configData.watchEnabled !== undefined
|
|
922
|
+
? configData.watchEnabled
|
|
923
|
+
: true;
|
|
924
|
+
const watchPath = resolve(
|
|
925
|
+
cli["watch-path"] ||
|
|
926
|
+
process.env.WATCH_PATH ||
|
|
927
|
+
selectedRepository?.watchPath ||
|
|
928
|
+
configData.watchPath ||
|
|
929
|
+
scriptPath,
|
|
930
|
+
);
|
|
931
|
+
const echoLogs = flags.has("echo-logs")
|
|
932
|
+
? true
|
|
933
|
+
: flags.has("no-echo-logs")
|
|
934
|
+
? false
|
|
935
|
+
: configData.echoLogs !== undefined
|
|
936
|
+
? configData.echoLogs
|
|
937
|
+
: false;
|
|
938
|
+
const autoFixEnabled = flags.has("no-autofix")
|
|
939
|
+
? false
|
|
940
|
+
: configData.autoFixEnabled !== undefined
|
|
941
|
+
? configData.autoFixEnabled
|
|
942
|
+
: true;
|
|
943
|
+
const interactiveShellEnabled =
|
|
944
|
+
flags.has("shell") ||
|
|
945
|
+
flags.has("interactive") ||
|
|
946
|
+
isEnvEnabled(process.env.CODEX_MONITOR_SHELL, false) ||
|
|
947
|
+
isEnvEnabled(process.env.CODEX_MONITOR_INTERACTIVE, false) ||
|
|
948
|
+
configData.interactiveShellEnabled === true ||
|
|
949
|
+
configData.shellEnabled === true;
|
|
950
|
+
const preflightEnabled = flags.has("no-preflight")
|
|
951
|
+
? false
|
|
952
|
+
: configData.preflightEnabled !== undefined
|
|
953
|
+
? configData.preflightEnabled
|
|
954
|
+
: isEnvEnabled(process.env.CODEX_MONITOR_PREFLIGHT_DISABLED, false)
|
|
955
|
+
? false
|
|
956
|
+
: true;
|
|
957
|
+
const preflightRetryMs = Number(
|
|
958
|
+
cli["preflight-retry"] ||
|
|
959
|
+
process.env.CODEX_MONITOR_PREFLIGHT_RETRY_MS ||
|
|
960
|
+
configData.preflightRetryMs ||
|
|
961
|
+
"300000",
|
|
962
|
+
);
|
|
963
|
+
const codexEnabled =
|
|
964
|
+
!flags.has("no-codex") &&
|
|
965
|
+
(configData.codexEnabled !== undefined ? configData.codexEnabled : true) &&
|
|
966
|
+
!isEnvEnabled(process.env.CODEX_SDK_DISABLED, false) &&
|
|
967
|
+
agentSdk.primary === "codex";
|
|
968
|
+
const primaryAgent = normalizePrimaryAgent(
|
|
969
|
+
cli["primary-agent"] ||
|
|
970
|
+
cli.agent ||
|
|
971
|
+
process.env.PRIMARY_AGENT ||
|
|
972
|
+
process.env.PRIMARY_AGENT_SDK ||
|
|
973
|
+
configData.primaryAgent ||
|
|
974
|
+
"codex-sdk",
|
|
975
|
+
);
|
|
976
|
+
const primaryAgentEnabled = isEnvEnabled(
|
|
977
|
+
process.env.PRIMARY_AGENT_DISABLED,
|
|
978
|
+
false,
|
|
979
|
+
)
|
|
980
|
+
? false
|
|
981
|
+
: primaryAgent === "codex-sdk"
|
|
982
|
+
? codexEnabled
|
|
983
|
+
: primaryAgent === "copilot-sdk"
|
|
984
|
+
? !isEnvEnabled(process.env.COPILOT_SDK_DISABLED, false)
|
|
985
|
+
: !isEnvEnabled(process.env.CLAUDE_SDK_DISABLED, false);
|
|
986
|
+
|
|
987
|
+
// agentPoolEnabled: true when ANY agent SDK is available for pooled operations
|
|
988
|
+
// This decouples pooled prompt execution from specific SDK selection
|
|
989
|
+
const agentPoolEnabled =
|
|
990
|
+
!isEnvEnabled(process.env.CODEX_SDK_DISABLED, false) ||
|
|
991
|
+
!isEnvEnabled(process.env.COPILOT_SDK_DISABLED, false) ||
|
|
992
|
+
!isEnvEnabled(process.env.CLAUDE_SDK_DISABLED, false);
|
|
993
|
+
|
|
994
|
+
// ── Internal Executor ────────────────────────────────────
|
|
995
|
+
// Allows the monitor to run tasks via agent-pool directly instead of
|
|
996
|
+
// (or alongside) the VK executor. Modes: "internal" (default), "vk", "hybrid".
|
|
997
|
+
const kanbanBackend = normalizeKanbanBackend(
|
|
998
|
+
process.env.KANBAN_BACKEND || configData.kanban?.backend || "internal",
|
|
999
|
+
);
|
|
1000
|
+
const kanbanSyncPolicy = normalizeKanbanSyncPolicy(
|
|
1001
|
+
process.env.KANBAN_SYNC_POLICY || configData.kanban?.syncPolicy,
|
|
1002
|
+
);
|
|
1003
|
+
const kanban = Object.freeze({
|
|
1004
|
+
backend: kanbanBackend,
|
|
1005
|
+
projectId:
|
|
1006
|
+
process.env.KANBAN_PROJECT_ID || configData.kanban?.projectId || null,
|
|
1007
|
+
syncPolicy: kanbanSyncPolicy,
|
|
1008
|
+
});
|
|
1009
|
+
const githubProjectSync = Object.freeze({
|
|
1010
|
+
webhookPath:
|
|
1011
|
+
process.env.GITHUB_PROJECT_WEBHOOK_PATH ||
|
|
1012
|
+
configData.kanban?.github?.project?.webhook?.path ||
|
|
1013
|
+
"/api/webhooks/github/project-sync",
|
|
1014
|
+
webhookSecret:
|
|
1015
|
+
process.env.GITHUB_PROJECT_WEBHOOK_SECRET ||
|
|
1016
|
+
process.env.GITHUB_WEBHOOK_SECRET ||
|
|
1017
|
+
configData.kanban?.github?.project?.webhook?.secret ||
|
|
1018
|
+
"",
|
|
1019
|
+
webhookRequireSignature: isEnvEnabled(
|
|
1020
|
+
process.env.GITHUB_PROJECT_WEBHOOK_REQUIRE_SIGNATURE ??
|
|
1021
|
+
configData.kanban?.github?.project?.webhook?.requireSignature,
|
|
1022
|
+
Boolean(
|
|
1023
|
+
process.env.GITHUB_PROJECT_WEBHOOK_SECRET ||
|
|
1024
|
+
process.env.GITHUB_WEBHOOK_SECRET ||
|
|
1025
|
+
configData.kanban?.github?.project?.webhook?.secret,
|
|
1026
|
+
),
|
|
1027
|
+
),
|
|
1028
|
+
alertFailureThreshold: Math.max(
|
|
1029
|
+
1,
|
|
1030
|
+
Number(
|
|
1031
|
+
process.env.GITHUB_PROJECT_SYNC_ALERT_FAILURE_THRESHOLD ||
|
|
1032
|
+
configData.kanban?.github?.project?.syncMonitoring
|
|
1033
|
+
?.alertFailureThreshold ||
|
|
1034
|
+
3,
|
|
1035
|
+
),
|
|
1036
|
+
),
|
|
1037
|
+
rateLimitAlertThreshold: Math.max(
|
|
1038
|
+
1,
|
|
1039
|
+
Number(
|
|
1040
|
+
process.env.GITHUB_PROJECT_SYNC_RATE_LIMIT_ALERT_THRESHOLD ||
|
|
1041
|
+
configData.kanban?.github?.project?.syncMonitoring
|
|
1042
|
+
?.rateLimitAlertThreshold ||
|
|
1043
|
+
3,
|
|
1044
|
+
),
|
|
1045
|
+
),
|
|
1046
|
+
});
|
|
1047
|
+
const jira = Object.freeze({
|
|
1048
|
+
baseUrl:
|
|
1049
|
+
process.env.JIRA_BASE_URL || configData.kanban?.jira?.baseUrl || "",
|
|
1050
|
+
email: process.env.JIRA_EMAIL || configData.kanban?.jira?.email || "",
|
|
1051
|
+
apiToken:
|
|
1052
|
+
process.env.JIRA_API_TOKEN || configData.kanban?.jira?.apiToken || "",
|
|
1053
|
+
projectKey:
|
|
1054
|
+
process.env.JIRA_PROJECT_KEY || configData.kanban?.jira?.projectKey || "",
|
|
1055
|
+
issueType:
|
|
1056
|
+
process.env.JIRA_ISSUE_TYPE ||
|
|
1057
|
+
configData.kanban?.jira?.issueType ||
|
|
1058
|
+
"Task",
|
|
1059
|
+
baseBranchField:
|
|
1060
|
+
process.env.JIRA_CUSTOM_FIELD_BASE_BRANCH ||
|
|
1061
|
+
configData.kanban?.jira?.baseBranchField ||
|
|
1062
|
+
"",
|
|
1063
|
+
statusMapping: Object.freeze({
|
|
1064
|
+
todo:
|
|
1065
|
+
process.env.JIRA_STATUS_TODO ||
|
|
1066
|
+
configData.kanban?.jira?.statusMapping?.todo ||
|
|
1067
|
+
"To Do",
|
|
1068
|
+
inprogress:
|
|
1069
|
+
process.env.JIRA_STATUS_INPROGRESS ||
|
|
1070
|
+
configData.kanban?.jira?.statusMapping?.inprogress ||
|
|
1071
|
+
"In Progress",
|
|
1072
|
+
inreview:
|
|
1073
|
+
process.env.JIRA_STATUS_INREVIEW ||
|
|
1074
|
+
configData.kanban?.jira?.statusMapping?.inreview ||
|
|
1075
|
+
"In Review",
|
|
1076
|
+
done:
|
|
1077
|
+
process.env.JIRA_STATUS_DONE ||
|
|
1078
|
+
configData.kanban?.jira?.statusMapping?.done ||
|
|
1079
|
+
"Done",
|
|
1080
|
+
cancelled:
|
|
1081
|
+
process.env.JIRA_STATUS_CANCELLED ||
|
|
1082
|
+
configData.kanban?.jira?.statusMapping?.cancelled ||
|
|
1083
|
+
"Cancelled",
|
|
1084
|
+
}),
|
|
1085
|
+
labels: Object.freeze({
|
|
1086
|
+
claimed:
|
|
1087
|
+
process.env.JIRA_LABEL_CLAIMED ||
|
|
1088
|
+
configData.kanban?.jira?.labels?.claimed ||
|
|
1089
|
+
"codex:claimed",
|
|
1090
|
+
working:
|
|
1091
|
+
process.env.JIRA_LABEL_WORKING ||
|
|
1092
|
+
configData.kanban?.jira?.labels?.working ||
|
|
1093
|
+
"codex:working",
|
|
1094
|
+
stale:
|
|
1095
|
+
process.env.JIRA_LABEL_STALE ||
|
|
1096
|
+
configData.kanban?.jira?.labels?.stale ||
|
|
1097
|
+
"codex:stale",
|
|
1098
|
+
ignore:
|
|
1099
|
+
process.env.JIRA_LABEL_IGNORE ||
|
|
1100
|
+
configData.kanban?.jira?.labels?.ignore ||
|
|
1101
|
+
"codex:ignore",
|
|
1102
|
+
}),
|
|
1103
|
+
sharedStateFields: Object.freeze({
|
|
1104
|
+
ownerId:
|
|
1105
|
+
process.env.JIRA_CUSTOM_FIELD_OWNER_ID ||
|
|
1106
|
+
configData.kanban?.jira?.sharedStateFields?.ownerId ||
|
|
1107
|
+
"",
|
|
1108
|
+
attemptToken:
|
|
1109
|
+
process.env.JIRA_CUSTOM_FIELD_ATTEMPT_TOKEN ||
|
|
1110
|
+
configData.kanban?.jira?.sharedStateFields?.attemptToken ||
|
|
1111
|
+
"",
|
|
1112
|
+
attemptStarted:
|
|
1113
|
+
process.env.JIRA_CUSTOM_FIELD_ATTEMPT_STARTED ||
|
|
1114
|
+
configData.kanban?.jira?.sharedStateFields?.attemptStarted ||
|
|
1115
|
+
"",
|
|
1116
|
+
heartbeat:
|
|
1117
|
+
process.env.JIRA_CUSTOM_FIELD_HEARTBEAT ||
|
|
1118
|
+
configData.kanban?.jira?.sharedStateFields?.heartbeat ||
|
|
1119
|
+
"",
|
|
1120
|
+
retryCount:
|
|
1121
|
+
process.env.JIRA_CUSTOM_FIELD_RETRY_COUNT ||
|
|
1122
|
+
configData.kanban?.jira?.sharedStateFields?.retryCount ||
|
|
1123
|
+
"",
|
|
1124
|
+
ignoreReason:
|
|
1125
|
+
process.env.JIRA_CUSTOM_FIELD_IGNORE_REASON ||
|
|
1126
|
+
configData.kanban?.jira?.sharedStateFields?.ignoreReason ||
|
|
1127
|
+
"",
|
|
1128
|
+
}),
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
const internalExecutorConfig = configData.internalExecutor || {};
|
|
1132
|
+
const projectRequirements = {
|
|
1133
|
+
profile: normalizeProjectRequirementsProfile(
|
|
1134
|
+
process.env.PROJECT_REQUIREMENTS_PROFILE ||
|
|
1135
|
+
configData.projectRequirements?.profile ||
|
|
1136
|
+
internalExecutorConfig.projectRequirements?.profile ||
|
|
1137
|
+
"feature",
|
|
1138
|
+
),
|
|
1139
|
+
notes: String(
|
|
1140
|
+
process.env.PROJECT_REQUIREMENTS_NOTES ||
|
|
1141
|
+
configData.projectRequirements?.notes ||
|
|
1142
|
+
internalExecutorConfig.projectRequirements?.notes ||
|
|
1143
|
+
"",
|
|
1144
|
+
).trim(),
|
|
1145
|
+
};
|
|
1146
|
+
const replenishMin = Math.max(
|
|
1147
|
+
1,
|
|
1148
|
+
Math.min(
|
|
1149
|
+
2,
|
|
1150
|
+
Number(
|
|
1151
|
+
process.env.INTERNAL_EXECUTOR_REPLENISH_MIN_NEW_TASKS ||
|
|
1152
|
+
internalExecutorConfig.backlogReplenishment?.minNewTasks ||
|
|
1153
|
+
1,
|
|
1154
|
+
),
|
|
1155
|
+
),
|
|
1156
|
+
);
|
|
1157
|
+
const replenishMax = Math.max(
|
|
1158
|
+
replenishMin,
|
|
1159
|
+
Math.min(
|
|
1160
|
+
3,
|
|
1161
|
+
Number(
|
|
1162
|
+
process.env.INTERNAL_EXECUTOR_REPLENISH_MAX_NEW_TASKS ||
|
|
1163
|
+
internalExecutorConfig.backlogReplenishment?.maxNewTasks ||
|
|
1164
|
+
2,
|
|
1165
|
+
),
|
|
1166
|
+
),
|
|
1167
|
+
);
|
|
1168
|
+
const executorMode = (
|
|
1169
|
+
process.env.EXECUTOR_MODE ||
|
|
1170
|
+
internalExecutorConfig.mode ||
|
|
1171
|
+
"internal"
|
|
1172
|
+
).toLowerCase();
|
|
1173
|
+
const reviewAgentToggleRaw =
|
|
1174
|
+
process.env.INTERNAL_EXECUTOR_REVIEW_AGENT_ENABLED;
|
|
1175
|
+
const reviewAgentEnabled =
|
|
1176
|
+
reviewAgentToggleRaw !== undefined &&
|
|
1177
|
+
String(reviewAgentToggleRaw).trim() !== ""
|
|
1178
|
+
? isEnvEnabled(reviewAgentToggleRaw, true)
|
|
1179
|
+
: internalExecutorConfig.reviewAgentEnabled !== false;
|
|
1180
|
+
const internalExecutor = {
|
|
1181
|
+
mode: ["vk", "internal", "hybrid"].includes(executorMode)
|
|
1182
|
+
? executorMode
|
|
1183
|
+
: "internal",
|
|
1184
|
+
maxParallel: Number(
|
|
1185
|
+
process.env.INTERNAL_EXECUTOR_PARALLEL ||
|
|
1186
|
+
internalExecutorConfig.maxParallel ||
|
|
1187
|
+
3,
|
|
1188
|
+
),
|
|
1189
|
+
baseBranchParallelLimit: Number(
|
|
1190
|
+
process.env.INTERNAL_EXECUTOR_BASE_BRANCH_PARALLEL ||
|
|
1191
|
+
internalExecutorConfig.baseBranchParallelLimit ||
|
|
1192
|
+
0,
|
|
1193
|
+
),
|
|
1194
|
+
pollIntervalMs: Number(
|
|
1195
|
+
process.env.INTERNAL_EXECUTOR_POLL_MS ||
|
|
1196
|
+
internalExecutorConfig.pollIntervalMs ||
|
|
1197
|
+
30000,
|
|
1198
|
+
),
|
|
1199
|
+
sdk:
|
|
1200
|
+
process.env.INTERNAL_EXECUTOR_SDK || internalExecutorConfig.sdk || "auto",
|
|
1201
|
+
taskTimeoutMs: Number(
|
|
1202
|
+
process.env.INTERNAL_EXECUTOR_TIMEOUT_MS ||
|
|
1203
|
+
internalExecutorConfig.taskTimeoutMs ||
|
|
1204
|
+
90 * 60 * 1000,
|
|
1205
|
+
),
|
|
1206
|
+
maxRetries: Number(
|
|
1207
|
+
process.env.INTERNAL_EXECUTOR_MAX_RETRIES ||
|
|
1208
|
+
internalExecutorConfig.maxRetries ||
|
|
1209
|
+
2,
|
|
1210
|
+
),
|
|
1211
|
+
autoCreatePr: internalExecutorConfig.autoCreatePr !== false,
|
|
1212
|
+
projectId:
|
|
1213
|
+
process.env.INTERNAL_EXECUTOR_PROJECT_ID ||
|
|
1214
|
+
internalExecutorConfig.projectId ||
|
|
1215
|
+
null,
|
|
1216
|
+
reviewAgentEnabled,
|
|
1217
|
+
reviewMaxConcurrent: Number(
|
|
1218
|
+
process.env.INTERNAL_EXECUTOR_REVIEW_MAX_CONCURRENT ||
|
|
1219
|
+
internalExecutorConfig.reviewMaxConcurrent ||
|
|
1220
|
+
2,
|
|
1221
|
+
),
|
|
1222
|
+
reviewTimeoutMs: Number(
|
|
1223
|
+
process.env.INTERNAL_EXECUTOR_REVIEW_TIMEOUT_MS ||
|
|
1224
|
+
internalExecutorConfig.reviewTimeoutMs ||
|
|
1225
|
+
300000,
|
|
1226
|
+
),
|
|
1227
|
+
taskClaimOwnerStaleTtlMs: Number(
|
|
1228
|
+
process.env.TASK_CLAIM_OWNER_STALE_TTL_MS ||
|
|
1229
|
+
internalExecutorConfig.taskClaimOwnerStaleTtlMs ||
|
|
1230
|
+
10 * 60 * 1000,
|
|
1231
|
+
),
|
|
1232
|
+
taskClaimRenewIntervalMs: Number(
|
|
1233
|
+
process.env.TASK_CLAIM_RENEW_INTERVAL_MS ||
|
|
1234
|
+
internalExecutorConfig.taskClaimRenewIntervalMs ||
|
|
1235
|
+
5 * 60 * 1000,
|
|
1236
|
+
),
|
|
1237
|
+
backlogReplenishment: {
|
|
1238
|
+
enabled: isEnvEnabled(
|
|
1239
|
+
process.env.INTERNAL_EXECUTOR_REPLENISH_ENABLED,
|
|
1240
|
+
internalExecutorConfig.backlogReplenishment?.enabled === true,
|
|
1241
|
+
),
|
|
1242
|
+
minNewTasks: replenishMin,
|
|
1243
|
+
maxNewTasks: replenishMax,
|
|
1244
|
+
requirePriority: isEnvEnabled(
|
|
1245
|
+
process.env.INTERNAL_EXECUTOR_REPLENISH_REQUIRE_PRIORITY,
|
|
1246
|
+
internalExecutorConfig.backlogReplenishment?.requirePriority !== false,
|
|
1247
|
+
),
|
|
1248
|
+
},
|
|
1249
|
+
projectRequirements,
|
|
1250
|
+
};
|
|
1251
|
+
|
|
1252
|
+
// ── Vibe-Kanban ──────────────────────────────────────────
|
|
1253
|
+
const vkRecoveryPort = process.env.VK_RECOVERY_PORT || "54089";
|
|
1254
|
+
const vkRecoveryHost =
|
|
1255
|
+
process.env.VK_RECOVERY_HOST || process.env.VK_HOST || "0.0.0.0";
|
|
1256
|
+
const vkEndpointUrl =
|
|
1257
|
+
process.env.VK_ENDPOINT_URL ||
|
|
1258
|
+
process.env.VK_BASE_URL ||
|
|
1259
|
+
`http://127.0.0.1:${vkRecoveryPort}`;
|
|
1260
|
+
const vkPublicUrl = process.env.VK_PUBLIC_URL || process.env.VK_WEB_URL || "";
|
|
1261
|
+
const vkTaskUrlTemplate = process.env.VK_TASK_URL_TEMPLATE || "";
|
|
1262
|
+
const vkRecoveryCooldownMin = Number(
|
|
1263
|
+
process.env.VK_RECOVERY_COOLDOWN_MIN || "10",
|
|
1264
|
+
);
|
|
1265
|
+
const vkSpawnDefault =
|
|
1266
|
+
configData.vkSpawnEnabled !== undefined
|
|
1267
|
+
? configData.vkSpawnEnabled
|
|
1268
|
+
: mode !== "generic";
|
|
1269
|
+
const vkRequiredByExecutor =
|
|
1270
|
+
internalExecutor.mode === "vk" || internalExecutor.mode === "hybrid";
|
|
1271
|
+
const vkRequiredByBoard = kanban.backend === "vk";
|
|
1272
|
+
const vkRuntimeRequired = vkRequiredByExecutor || vkRequiredByBoard;
|
|
1273
|
+
const vkSpawnEnabled =
|
|
1274
|
+
vkRuntimeRequired &&
|
|
1275
|
+
!flags.has("no-vk-spawn") &&
|
|
1276
|
+
!isEnvEnabled(process.env.VK_NO_SPAWN, false) &&
|
|
1277
|
+
vkSpawnDefault;
|
|
1278
|
+
const vkEnsureIntervalMs = Number(
|
|
1279
|
+
cli["vk-ensure-interval"] || process.env.VK_ENSURE_INTERVAL || "60000",
|
|
1280
|
+
);
|
|
1281
|
+
|
|
1282
|
+
// ── Telegram ─────────────────────────────────────────────
|
|
1283
|
+
const telegramToken = process.env.TELEGRAM_BOT_TOKEN || "";
|
|
1284
|
+
const telegramChatId = process.env.TELEGRAM_CHAT_ID || "";
|
|
1285
|
+
const telegramIntervalMin = Number(process.env.TELEGRAM_INTERVAL_MIN || "10");
|
|
1286
|
+
const telegramCommandPollTimeoutSec = Math.max(
|
|
1287
|
+
5,
|
|
1288
|
+
Number(process.env.TELEGRAM_COMMAND_POLL_TIMEOUT_SEC || "20"),
|
|
1289
|
+
);
|
|
1290
|
+
const telegramCommandConcurrency = Math.max(
|
|
1291
|
+
1,
|
|
1292
|
+
Number(process.env.TELEGRAM_COMMAND_CONCURRENCY || "2"),
|
|
1293
|
+
);
|
|
1294
|
+
const telegramCommandMaxBatch = Math.max(
|
|
1295
|
+
1,
|
|
1296
|
+
Number(process.env.TELEGRAM_COMMAND_MAX_BATCH || "25"),
|
|
1297
|
+
);
|
|
1298
|
+
const telegramBotEnabled = !flags.has("no-telegram-bot") && !!telegramToken;
|
|
1299
|
+
const telegramCommandEnabled = flags.has("telegram-commands")
|
|
1300
|
+
? !telegramBotEnabled
|
|
1301
|
+
: false;
|
|
1302
|
+
// Verbosity: minimal (critical+error only), summary (default — up to warnings
|
|
1303
|
+
// + key info), detailed (everything including debug).
|
|
1304
|
+
const telegramVerbosity = (
|
|
1305
|
+
process.env.TELEGRAM_VERBOSITY ||
|
|
1306
|
+
configData.telegramVerbosity ||
|
|
1307
|
+
"summary"
|
|
1308
|
+
).toLowerCase();
|
|
1309
|
+
|
|
1310
|
+
// ── Task Planner ─────────────────────────────────────────
|
|
1311
|
+
// Mode: "codex-sdk" (default) runs Codex directly, "kanban" creates a VK
|
|
1312
|
+
// task for a real agent to plan, "disabled" turns off the planner entirely.
|
|
1313
|
+
const plannerMode = (
|
|
1314
|
+
process.env.TASK_PLANNER_MODE ||
|
|
1315
|
+
configData.plannerMode ||
|
|
1316
|
+
(mode === "generic" ? "disabled" : "codex-sdk")
|
|
1317
|
+
).toLowerCase();
|
|
1318
|
+
const plannerPerCapitaThreshold = Number(
|
|
1319
|
+
process.env.TASK_PLANNER_PER_CAPITA_THRESHOLD || "1",
|
|
1320
|
+
);
|
|
1321
|
+
const plannerIdleSlotThreshold = Number(
|
|
1322
|
+
process.env.TASK_PLANNER_IDLE_SLOT_THRESHOLD || "1",
|
|
1323
|
+
);
|
|
1324
|
+
const plannerDedupHours = Number(process.env.TASK_PLANNER_DEDUP_HOURS || "6");
|
|
1325
|
+
const plannerDedupMs = Number.isFinite(plannerDedupHours)
|
|
1326
|
+
? plannerDedupHours * 60 * 60 * 1000
|
|
1327
|
+
: 24 * 60 * 60 * 1000;
|
|
1328
|
+
|
|
1329
|
+
// ── GitHub Reconciler ───────────────────────────────────
|
|
1330
|
+
const ghReconcileEnabled = isEnvEnabled(
|
|
1331
|
+
process.env.GH_RECONCILE_ENABLED ?? configData.ghReconcileEnabled,
|
|
1332
|
+
process.env.VITEST ? false : true,
|
|
1333
|
+
);
|
|
1334
|
+
const ghReconcileIntervalMs = Number(
|
|
1335
|
+
process.env.GH_RECONCILE_INTERVAL_MS ||
|
|
1336
|
+
configData.ghReconcileIntervalMs ||
|
|
1337
|
+
5 * 60 * 1000,
|
|
1338
|
+
);
|
|
1339
|
+
const ghReconcileMergedLookbackHours = Number(
|
|
1340
|
+
process.env.GH_RECONCILE_MERGED_LOOKBACK_HOURS ||
|
|
1341
|
+
configData.ghReconcileMergedLookbackHours ||
|
|
1342
|
+
72,
|
|
1343
|
+
);
|
|
1344
|
+
const ghReconcileTrackingLabels = String(
|
|
1345
|
+
process.env.GH_RECONCILE_TRACKING_LABELS ||
|
|
1346
|
+
configData.ghReconcileTrackingLabels ||
|
|
1347
|
+
"tracking",
|
|
1348
|
+
)
|
|
1349
|
+
.split(",")
|
|
1350
|
+
.map((value) => value.trim().toLowerCase())
|
|
1351
|
+
.filter(Boolean);
|
|
1352
|
+
|
|
1353
|
+
// ── Branch Routing ────────────────────────────────────────
|
|
1354
|
+
// Maps scope patterns (from conventional commit scopes in task titles) to
|
|
1355
|
+
// upstream branches. Allows e.g. all "openfleet" tasks to route to
|
|
1356
|
+
// "origin/ve/openfleet-staging" instead of the default target branch.
|
|
1357
|
+
//
|
|
1358
|
+
// Config format (openfleet.config.json):
|
|
1359
|
+
// "branchRouting": {
|
|
1360
|
+
// "defaultBranch": "origin/staging",
|
|
1361
|
+
// "scopeMap": {
|
|
1362
|
+
// "openfleet": "origin/ve/openfleet-staging",
|
|
1363
|
+
// "veid": "origin/staging",
|
|
1364
|
+
// "provider": "origin/staging"
|
|
1365
|
+
// },
|
|
1366
|
+
// "autoRebaseOnMerge": true,
|
|
1367
|
+
// "assessWithSdk": true
|
|
1368
|
+
// }
|
|
1369
|
+
//
|
|
1370
|
+
// Env overrides:
|
|
1371
|
+
// VK_TARGET_BRANCH=origin/staging (default branch)
|
|
1372
|
+
// BRANCH_ROUTING_SCOPE_MAP=openfleet:origin/ve/openfleet-staging,veid:origin/staging
|
|
1373
|
+
// AUTO_REBASE_ON_MERGE=true
|
|
1374
|
+
// ASSESS_WITH_SDK=true
|
|
1375
|
+
const branchRoutingRaw = configData.branchRouting || {};
|
|
1376
|
+
const defaultTargetBranch =
|
|
1377
|
+
process.env.VK_TARGET_BRANCH ||
|
|
1378
|
+
branchRoutingRaw.defaultBranch ||
|
|
1379
|
+
"origin/main";
|
|
1380
|
+
const scopeMapEnv = process.env.BRANCH_ROUTING_SCOPE_MAP || "";
|
|
1381
|
+
const scopeMapFromEnv = {};
|
|
1382
|
+
if (scopeMapEnv) {
|
|
1383
|
+
for (const pair of scopeMapEnv.split(",")) {
|
|
1384
|
+
const [scope, branch] = pair.split(":").map((s) => s.trim());
|
|
1385
|
+
if (scope && branch) scopeMapFromEnv[scope.toLowerCase()] = branch;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
const scopeMap = {
|
|
1389
|
+
...(branchRoutingRaw.scopeMap || {}),
|
|
1390
|
+
...scopeMapFromEnv,
|
|
1391
|
+
};
|
|
1392
|
+
// Normalise keys to lowercase
|
|
1393
|
+
const normalizedScopeMap = {};
|
|
1394
|
+
for (const [key, val] of Object.entries(scopeMap)) {
|
|
1395
|
+
normalizedScopeMap[key.toLowerCase()] = val;
|
|
1396
|
+
}
|
|
1397
|
+
const autoRebaseOnMerge = isEnvEnabled(
|
|
1398
|
+
process.env.AUTO_REBASE_ON_MERGE ?? branchRoutingRaw.autoRebaseOnMerge,
|
|
1399
|
+
true,
|
|
1400
|
+
);
|
|
1401
|
+
const assessWithSdk = isEnvEnabled(
|
|
1402
|
+
process.env.ASSESS_WITH_SDK ?? branchRoutingRaw.assessWithSdk,
|
|
1403
|
+
true,
|
|
1404
|
+
);
|
|
1405
|
+
const branchRouting = Object.freeze({
|
|
1406
|
+
defaultBranch: defaultTargetBranch,
|
|
1407
|
+
scopeMap: Object.freeze(normalizedScopeMap),
|
|
1408
|
+
autoRebaseOnMerge,
|
|
1409
|
+
assessWithSdk,
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
// ── Fleet Coordination ─────────────────────────────────────
|
|
1413
|
+
// Multi-workstation collaboration: when 2+ openfleet instances share
|
|
1414
|
+
// the same repo, the fleet system coordinates task planning, dispatch,
|
|
1415
|
+
// and conflict-aware ordering.
|
|
1416
|
+
const fleetEnabled = isEnvEnabled(
|
|
1417
|
+
process.env.FLEET_ENABLED ?? configData.fleetEnabled,
|
|
1418
|
+
true,
|
|
1419
|
+
);
|
|
1420
|
+
const fleetBufferMultiplier = Number(
|
|
1421
|
+
process.env.FLEET_BUFFER_MULTIPLIER ||
|
|
1422
|
+
configData.fleetBufferMultiplier ||
|
|
1423
|
+
"3",
|
|
1424
|
+
);
|
|
1425
|
+
const fleetSyncIntervalMs = Number(
|
|
1426
|
+
process.env.FLEET_SYNC_INTERVAL_MS ||
|
|
1427
|
+
configData.fleetSyncIntervalMs ||
|
|
1428
|
+
String(2 * 60 * 1000), // 2 minutes
|
|
1429
|
+
);
|
|
1430
|
+
const fleetPresenceTtlMs = Number(
|
|
1431
|
+
process.env.FLEET_PRESENCE_TTL_MS ||
|
|
1432
|
+
configData.fleetPresenceTtlMs ||
|
|
1433
|
+
String(5 * 60 * 1000), // 5 minutes
|
|
1434
|
+
);
|
|
1435
|
+
const fleetKnowledgeEnabled = isEnvEnabled(
|
|
1436
|
+
process.env.FLEET_KNOWLEDGE_ENABLED ?? configData.fleetKnowledgeEnabled,
|
|
1437
|
+
true,
|
|
1438
|
+
);
|
|
1439
|
+
const fleetKnowledgeFile = String(
|
|
1440
|
+
process.env.FLEET_KNOWLEDGE_FILE ||
|
|
1441
|
+
configData.fleetKnowledgeFile ||
|
|
1442
|
+
"AGENTS.md",
|
|
1443
|
+
);
|
|
1444
|
+
const fleet = Object.freeze({
|
|
1445
|
+
enabled: fleetEnabled,
|
|
1446
|
+
bufferMultiplier: fleetBufferMultiplier,
|
|
1447
|
+
syncIntervalMs: fleetSyncIntervalMs,
|
|
1448
|
+
presenceTtlMs: fleetPresenceTtlMs,
|
|
1449
|
+
knowledgeEnabled: fleetKnowledgeEnabled,
|
|
1450
|
+
knowledgeFile: fleetKnowledgeFile,
|
|
1451
|
+
});
|
|
1452
|
+
|
|
1453
|
+
// ── Dependabot Auto-Merge ─────────────────────────────────
|
|
1454
|
+
const dependabotAutoMerge = isEnvEnabled(
|
|
1455
|
+
process.env.DEPENDABOT_AUTO_MERGE ?? configData.dependabotAutoMerge,
|
|
1456
|
+
true,
|
|
1457
|
+
);
|
|
1458
|
+
const dependabotAutoMergeIntervalMin = Number(
|
|
1459
|
+
process.env.DEPENDABOT_AUTO_MERGE_INTERVAL_MIN || "10",
|
|
1460
|
+
);
|
|
1461
|
+
// Merge method: squash (default), merge, rebase
|
|
1462
|
+
const dependabotMergeMethod = String(
|
|
1463
|
+
process.env.DEPENDABOT_MERGE_METHOD ||
|
|
1464
|
+
configData.dependabotMergeMethod ||
|
|
1465
|
+
"squash",
|
|
1466
|
+
).toLowerCase();
|
|
1467
|
+
// PR authors to auto-merge (comma-separated). Default: dependabot[bot]
|
|
1468
|
+
const dependabotAuthors = String(
|
|
1469
|
+
process.env.DEPENDABOT_AUTHORS ||
|
|
1470
|
+
configData.dependabotAuthors ||
|
|
1471
|
+
"dependabot[bot],app/dependabot",
|
|
1472
|
+
)
|
|
1473
|
+
.split(",")
|
|
1474
|
+
.map((a) => a.trim())
|
|
1475
|
+
.filter(Boolean);
|
|
1476
|
+
|
|
1477
|
+
// ── Status file ──────────────────────────────────────────
|
|
1478
|
+
const cacheDir = resolve(
|
|
1479
|
+
repoRoot,
|
|
1480
|
+
configData.cacheDir || selectedRepository?.cacheDir || ".cache",
|
|
1481
|
+
);
|
|
1482
|
+
// Default matches ve-orchestrator.ps1's $script:StatusStatePath
|
|
1483
|
+
const statusPath =
|
|
1484
|
+
process.env.STATUS_FILE ||
|
|
1485
|
+
configData.statusPath ||
|
|
1486
|
+
selectedRepository?.statusPath ||
|
|
1487
|
+
resolve(cacheDir, "ve-orchestrator-status.json");
|
|
1488
|
+
const lockBase =
|
|
1489
|
+
configData.telegramPollLockPath ||
|
|
1490
|
+
selectedRepository?.telegramPollLockPath ||
|
|
1491
|
+
resolve(cacheDir, "telegram-getupdates.lock");
|
|
1492
|
+
const telegramPollLockPath = lockBase.endsWith(".lock")
|
|
1493
|
+
? resolve(lockBase)
|
|
1494
|
+
: resolve(lockBase, "telegram-getupdates.lock");
|
|
1495
|
+
|
|
1496
|
+
// ── Executors ────────────────────────────────────────────
|
|
1497
|
+
const executorConfig = loadExecutorConfig(configDir, configData);
|
|
1498
|
+
const scheduler = new ExecutorScheduler(executorConfig);
|
|
1499
|
+
|
|
1500
|
+
// ── Agent prompts ────────────────────────────────────────
|
|
1501
|
+
ensurePromptWorkspaceGitIgnore(repoRoot);
|
|
1502
|
+
ensureAgentPromptWorkspace(repoRoot);
|
|
1503
|
+
const agentPrompts = loadAgentPrompts(configDir, repoRoot, configData);
|
|
1504
|
+
const agentPromptSources = agentPrompts._sources || {};
|
|
1505
|
+
delete agentPrompts._sources;
|
|
1506
|
+
const agentPromptCatalog = getAgentPromptDefinitions();
|
|
1507
|
+
|
|
1508
|
+
// ── First-run detection ──────────────────────────────────
|
|
1509
|
+
const isFirstRun = !hasSetupMarkers(configDir);
|
|
1510
|
+
|
|
1511
|
+
const config = {
|
|
1512
|
+
// Identity
|
|
1513
|
+
projectName,
|
|
1514
|
+
mode,
|
|
1515
|
+
repoSlug,
|
|
1516
|
+
repoUrlBase,
|
|
1517
|
+
repoRoot,
|
|
1518
|
+
configDir,
|
|
1519
|
+
envPaths,
|
|
1520
|
+
|
|
1521
|
+
// Orchestrator
|
|
1522
|
+
scriptPath,
|
|
1523
|
+
scriptArgs,
|
|
1524
|
+
restartDelayMs,
|
|
1525
|
+
maxRestarts,
|
|
1526
|
+
|
|
1527
|
+
// Logging
|
|
1528
|
+
logDir,
|
|
1529
|
+
logMaxSizeMb,
|
|
1530
|
+
logCleanupIntervalMin,
|
|
1531
|
+
|
|
1532
|
+
// Agent SDK
|
|
1533
|
+
agentSdk,
|
|
1534
|
+
|
|
1535
|
+
// Feature flags
|
|
1536
|
+
watchEnabled,
|
|
1537
|
+
watchPath,
|
|
1538
|
+
echoLogs,
|
|
1539
|
+
autoFixEnabled,
|
|
1540
|
+
interactiveShellEnabled,
|
|
1541
|
+
preflightEnabled,
|
|
1542
|
+
preflightRetryMs,
|
|
1543
|
+
codexEnabled,
|
|
1544
|
+
agentPoolEnabled,
|
|
1545
|
+
primaryAgent,
|
|
1546
|
+
primaryAgentEnabled,
|
|
1547
|
+
|
|
1548
|
+
// Internal Executor
|
|
1549
|
+
internalExecutor,
|
|
1550
|
+
executorMode: internalExecutor.mode,
|
|
1551
|
+
kanban,
|
|
1552
|
+
githubProjectSync,
|
|
1553
|
+
jira,
|
|
1554
|
+
projectRequirements,
|
|
1555
|
+
|
|
1556
|
+
// Merge Strategy
|
|
1557
|
+
codexAnalyzeMergeStrategy:
|
|
1558
|
+
codexEnabled &&
|
|
1559
|
+
(process.env.CODEX_ANALYZE_MERGE_STRATEGY || "").toLowerCase() !==
|
|
1560
|
+
"false",
|
|
1561
|
+
mergeStrategyTimeoutMs:
|
|
1562
|
+
parseInt(process.env.MERGE_STRATEGY_TIMEOUT_MS, 10) || 10 * 60 * 1000,
|
|
1563
|
+
|
|
1564
|
+
// Autofix mode hint (informational — actual detection uses isDevMode())
|
|
1565
|
+
autofixMode: process.env.AUTOFIX_MODE || "auto",
|
|
1566
|
+
|
|
1567
|
+
// Vibe-Kanban
|
|
1568
|
+
vkRecoveryPort,
|
|
1569
|
+
vkRecoveryHost,
|
|
1570
|
+
vkEndpointUrl,
|
|
1571
|
+
vkPublicUrl,
|
|
1572
|
+
vkTaskUrlTemplate,
|
|
1573
|
+
vkRecoveryCooldownMin,
|
|
1574
|
+
vkRuntimeRequired,
|
|
1575
|
+
vkSpawnEnabled,
|
|
1576
|
+
vkEnsureIntervalMs,
|
|
1577
|
+
|
|
1578
|
+
// Telegram
|
|
1579
|
+
telegramToken,
|
|
1580
|
+
telegramChatId,
|
|
1581
|
+
telegramIntervalMin,
|
|
1582
|
+
telegramCommandPollTimeoutSec,
|
|
1583
|
+
telegramCommandConcurrency,
|
|
1584
|
+
telegramCommandMaxBatch,
|
|
1585
|
+
telegramBotEnabled,
|
|
1586
|
+
telegramCommandEnabled,
|
|
1587
|
+
telegramVerbosity,
|
|
1588
|
+
|
|
1589
|
+
// Task Planner
|
|
1590
|
+
plannerMode,
|
|
1591
|
+
plannerPerCapitaThreshold,
|
|
1592
|
+
plannerIdleSlotThreshold,
|
|
1593
|
+
plannerDedupHours,
|
|
1594
|
+
plannerDedupMs,
|
|
1595
|
+
|
|
1596
|
+
// GitHub Reconciler
|
|
1597
|
+
githubReconcile: {
|
|
1598
|
+
enabled: ghReconcileEnabled,
|
|
1599
|
+
intervalMs: ghReconcileIntervalMs,
|
|
1600
|
+
mergedLookbackHours: ghReconcileMergedLookbackHours,
|
|
1601
|
+
trackingLabels: ghReconcileTrackingLabels,
|
|
1602
|
+
},
|
|
1603
|
+
|
|
1604
|
+
// Dependabot Auto-Merge
|
|
1605
|
+
dependabotAutoMerge,
|
|
1606
|
+
dependabotAutoMergeIntervalMin,
|
|
1607
|
+
dependabotMergeMethod,
|
|
1608
|
+
dependabotAuthors,
|
|
1609
|
+
|
|
1610
|
+
// Branch Routing
|
|
1611
|
+
branchRouting,
|
|
1612
|
+
|
|
1613
|
+
// Fleet Coordination
|
|
1614
|
+
fleet,
|
|
1615
|
+
|
|
1616
|
+
// Paths
|
|
1617
|
+
statusPath,
|
|
1618
|
+
telegramPollLockPath,
|
|
1619
|
+
cacheDir,
|
|
1620
|
+
|
|
1621
|
+
// Executors
|
|
1622
|
+
executorConfig,
|
|
1623
|
+
scheduler,
|
|
1624
|
+
|
|
1625
|
+
// Multi-repo
|
|
1626
|
+
repositories,
|
|
1627
|
+
selectedRepository,
|
|
1628
|
+
|
|
1629
|
+
// Agent prompts
|
|
1630
|
+
agentPrompts,
|
|
1631
|
+
agentPromptSources,
|
|
1632
|
+
agentPromptCatalog,
|
|
1633
|
+
|
|
1634
|
+
// First run
|
|
1635
|
+
isFirstRun,
|
|
1636
|
+
};
|
|
1637
|
+
|
|
1638
|
+
return Object.freeze(config);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
1642
|
+
|
|
1643
|
+
function detectProjectName(configDir, repoRoot) {
|
|
1644
|
+
// Try package.json in repo root
|
|
1645
|
+
const pkgPath = resolve(repoRoot, "package.json");
|
|
1646
|
+
if (existsSync(pkgPath)) {
|
|
1647
|
+
try {
|
|
1648
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
1649
|
+
if (pkg.name) return pkg.name.replace(/^@[^/]+\//, "");
|
|
1650
|
+
} catch {
|
|
1651
|
+
/* skip */
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
// Fallback to directory name
|
|
1655
|
+
return basename(repoRoot);
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
function findOrchestratorScript(configDir, repoRoot) {
|
|
1659
|
+
const shellModeEnv = String(process.env.CODEX_MONITOR_SHELL_MODE || "")
|
|
1660
|
+
.trim()
|
|
1661
|
+
.toLowerCase();
|
|
1662
|
+
const shellModeRequested = ["1", "true", "yes", "on"].includes(shellModeEnv);
|
|
1663
|
+
const orchestratorEnv = String(process.env.ORCHESTRATOR_SCRIPT || "")
|
|
1664
|
+
.trim()
|
|
1665
|
+
.toLowerCase();
|
|
1666
|
+
const preferShellScript =
|
|
1667
|
+
shellModeRequested ||
|
|
1668
|
+
orchestratorEnv.endsWith(".sh") ||
|
|
1669
|
+
(process.platform !== "win32" && !orchestratorEnv.endsWith(".ps1"));
|
|
1670
|
+
|
|
1671
|
+
const shCandidates = [
|
|
1672
|
+
resolve(configDir, "ve-orchestrator.sh"),
|
|
1673
|
+
resolve(configDir, "orchestrator.sh"),
|
|
1674
|
+
resolve(configDir, "..", "ve-orchestrator.sh"),
|
|
1675
|
+
resolve(configDir, "..", "orchestrator.sh"),
|
|
1676
|
+
resolve(repoRoot, "scripts", "ve-orchestrator.sh"),
|
|
1677
|
+
resolve(repoRoot, "scripts", "orchestrator.sh"),
|
|
1678
|
+
resolve(repoRoot, "ve-orchestrator.sh"),
|
|
1679
|
+
resolve(repoRoot, "orchestrator.sh"),
|
|
1680
|
+
resolve(process.cwd(), "ve-orchestrator.sh"),
|
|
1681
|
+
resolve(process.cwd(), "orchestrator.sh"),
|
|
1682
|
+
resolve(process.cwd(), "scripts", "ve-orchestrator.sh"),
|
|
1683
|
+
];
|
|
1684
|
+
|
|
1685
|
+
const psCandidates = [
|
|
1686
|
+
resolve(configDir, "ve-orchestrator.ps1"),
|
|
1687
|
+
resolve(configDir, "orchestrator.ps1"),
|
|
1688
|
+
resolve(configDir, "..", "ve-orchestrator.ps1"),
|
|
1689
|
+
resolve(configDir, "..", "orchestrator.ps1"),
|
|
1690
|
+
resolve(repoRoot, "scripts", "ve-orchestrator.ps1"),
|
|
1691
|
+
resolve(repoRoot, "scripts", "orchestrator.ps1"),
|
|
1692
|
+
resolve(repoRoot, "ve-orchestrator.ps1"),
|
|
1693
|
+
resolve(repoRoot, "orchestrator.ps1"),
|
|
1694
|
+
resolve(process.cwd(), "ve-orchestrator.ps1"),
|
|
1695
|
+
resolve(process.cwd(), "orchestrator.ps1"),
|
|
1696
|
+
resolve(process.cwd(), "scripts", "ve-orchestrator.ps1"),
|
|
1697
|
+
];
|
|
1698
|
+
|
|
1699
|
+
const candidates = preferShellScript
|
|
1700
|
+
? [...shCandidates, ...psCandidates]
|
|
1701
|
+
: [...psCandidates, ...shCandidates];
|
|
1702
|
+
for (const p of candidates) {
|
|
1703
|
+
if (existsSync(p)) return p;
|
|
1704
|
+
}
|
|
1705
|
+
return preferShellScript
|
|
1706
|
+
? resolve(configDir, "..", "ve-orchestrator.sh")
|
|
1707
|
+
: resolve(configDir, "..", "ve-orchestrator.ps1");
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
// ── Exports ──────────────────────────────────────────────────────────────────
|
|
1711
|
+
|
|
1712
|
+
export {
|
|
1713
|
+
ExecutorScheduler,
|
|
1714
|
+
loadExecutorConfig,
|
|
1715
|
+
loadRepoConfig,
|
|
1716
|
+
loadAgentPrompts,
|
|
1717
|
+
parseEnvBoolean,
|
|
1718
|
+
getAgentPromptDefinitions,
|
|
1719
|
+
};
|
|
1720
|
+
export default loadConfig;
|