@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/ve-kanban.sh
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
NODE_SCRIPT="${SCRIPT_DIR}/ve-kanban.mjs"
|
|
6
|
+
|
|
7
|
+
# Native Linux/macOS path only.
|
|
8
|
+
if [[ -f "${NODE_SCRIPT}" ]] && command -v node >/dev/null 2>&1; then
|
|
9
|
+
NODE_SCRIPT_PATH="${NODE_SCRIPT}"
|
|
10
|
+
NODE_PLATFORM="$(node -p 'process.platform' 2>/dev/null || true)"
|
|
11
|
+
if [[ "${NODE_PLATFORM}" == "win32" ]] && command -v wslpath >/dev/null 2>&1; then
|
|
12
|
+
NODE_SCRIPT_PATH="$(wslpath -w "${NODE_SCRIPT}")"
|
|
13
|
+
fi
|
|
14
|
+
exec node "${NODE_SCRIPT_PATH}" "$@"
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
echo "[ve-kanban.sh] Native runtime unavailable (need node + ve-kanban.mjs)." >&2
|
|
18
|
+
exit 1
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { VeKanbanRuntime } from "./ve-kanban.mjs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import {
|
|
7
|
+
shouldRetryTask,
|
|
8
|
+
sweepStaleSharedStates,
|
|
9
|
+
releaseSharedState,
|
|
10
|
+
} from "./shared-state-manager.mjs";
|
|
11
|
+
|
|
12
|
+
// Shared state configuration
|
|
13
|
+
const SHARED_STATE_ENABLED = process.env.SHARED_STATE_ENABLED !== "false"; // default true
|
|
14
|
+
const SHARED_STATE_STALE_THRESHOLD_MS =
|
|
15
|
+
Number(process.env.SHARED_STATE_STALE_THRESHOLD_MS) || 300_000;
|
|
16
|
+
const SHARED_STATE_MAX_RETRIES =
|
|
17
|
+
Number(process.env.SHARED_STATE_MAX_RETRIES) || 3;
|
|
18
|
+
|
|
19
|
+
function log(level, msg) {
|
|
20
|
+
const now = new Date().toISOString().replace("T", " ").replace("Z", "");
|
|
21
|
+
const tag = level.toUpperCase();
|
|
22
|
+
const prefix = `[${now}] [${tag}]`;
|
|
23
|
+
if (level === "error") console.error(`${prefix} ${msg}`);
|
|
24
|
+
else if (level === "warn") console.warn(`${prefix} ${msg}`);
|
|
25
|
+
else console.log(`${prefix} ${msg}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sleep(ms) {
|
|
29
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function toInt(value, fallback) {
|
|
33
|
+
const parsed = Number.parseInt(String(value), 10);
|
|
34
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeStatus(raw) {
|
|
38
|
+
return String(raw || "")
|
|
39
|
+
.toLowerCase()
|
|
40
|
+
.trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isTerminalAttempt(attempt) {
|
|
44
|
+
if (!attempt || attempt.archived) return true;
|
|
45
|
+
const status = normalizeStatus(attempt.status);
|
|
46
|
+
if (
|
|
47
|
+
[
|
|
48
|
+
"done",
|
|
49
|
+
"completed",
|
|
50
|
+
"complete",
|
|
51
|
+
"cancelled",
|
|
52
|
+
"canceled",
|
|
53
|
+
"failed",
|
|
54
|
+
"error",
|
|
55
|
+
"merged",
|
|
56
|
+
"closed",
|
|
57
|
+
].includes(status)
|
|
58
|
+
) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function sortByCreatedAsc(items) {
|
|
65
|
+
return [...items].sort((a, b) => {
|
|
66
|
+
const aTs = Date.parse(a?.created_at || a?.createdAt || 0) || 0;
|
|
67
|
+
const bTs = Date.parse(b?.created_at || b?.createdAt || 0) || 0;
|
|
68
|
+
return aTs - bTs;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function parseOrchestratorArgs(argv) {
|
|
73
|
+
const options = {
|
|
74
|
+
maxParallel: 2,
|
|
75
|
+
pollIntervalSec: 90,
|
|
76
|
+
oneShot: false,
|
|
77
|
+
dryRun: false,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
81
|
+
const token = String(argv[i] || "").toLowerCase();
|
|
82
|
+
if (
|
|
83
|
+
token === "-maxparallel" ||
|
|
84
|
+
token === "--max-parallel" ||
|
|
85
|
+
token === "-p"
|
|
86
|
+
) {
|
|
87
|
+
options.maxParallel = Math.max(
|
|
88
|
+
1,
|
|
89
|
+
toInt(argv[i + 1], options.maxParallel),
|
|
90
|
+
);
|
|
91
|
+
i += 1;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (
|
|
95
|
+
token === "-pollintervalsec" ||
|
|
96
|
+
token === "--poll-interval-sec" ||
|
|
97
|
+
token === "--interval" ||
|
|
98
|
+
token === "-i"
|
|
99
|
+
) {
|
|
100
|
+
options.pollIntervalSec = Math.max(
|
|
101
|
+
5,
|
|
102
|
+
toInt(argv[i + 1], options.pollIntervalSec),
|
|
103
|
+
);
|
|
104
|
+
i += 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (
|
|
108
|
+
token === "-oneshot" ||
|
|
109
|
+
token === "--one-shot" ||
|
|
110
|
+
token === "--oneshot"
|
|
111
|
+
) {
|
|
112
|
+
options.oneShot = true;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (token === "-dryrun" || token === "--dry-run") {
|
|
116
|
+
options.dryRun = true;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (token === "-waitformutex" || token === "--wait-for-mutex") {
|
|
120
|
+
// Kept for CLI compatibility. This native orchestrator is single-process.
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return options;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function reconcileMergedAttempts(runtime, attempts, dryRun) {
|
|
129
|
+
let merged = 0;
|
|
130
|
+
|
|
131
|
+
for (const attempt of attempts) {
|
|
132
|
+
if (isTerminalAttempt(attempt)) continue;
|
|
133
|
+
const attemptId = String(attempt.id || "").trim();
|
|
134
|
+
const branch = String(attempt.branch || attempt.branch_name || "").trim();
|
|
135
|
+
const taskId = String(attempt.task_id || attempt.taskId || "").trim();
|
|
136
|
+
if (!attemptId || !branch) continue;
|
|
137
|
+
|
|
138
|
+
const pr = runtime.findPullRequestForBranch(branch, "all");
|
|
139
|
+
if (!pr || String(pr.state).toUpperCase() !== "MERGED") continue;
|
|
140
|
+
|
|
141
|
+
if (dryRun) {
|
|
142
|
+
log(
|
|
143
|
+
"info",
|
|
144
|
+
`[dry-run] would mark task ${taskId || "(unknown)"} done and archive attempt ${attemptId}`,
|
|
145
|
+
);
|
|
146
|
+
merged += 1;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
if (taskId) {
|
|
152
|
+
await runtime.updateTaskStatus(taskId, "done");
|
|
153
|
+
|
|
154
|
+
// Mark task as complete in shared state
|
|
155
|
+
if (SHARED_STATE_ENABLED && attempt.claim_token) {
|
|
156
|
+
try {
|
|
157
|
+
await releaseSharedState(
|
|
158
|
+
taskId,
|
|
159
|
+
attempt.claim_token,
|
|
160
|
+
"complete",
|
|
161
|
+
undefined,
|
|
162
|
+
process.cwd(),
|
|
163
|
+
);
|
|
164
|
+
log("info", `marked ${taskId} complete in shared state`);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
log(
|
|
167
|
+
"warn",
|
|
168
|
+
`failed to update shared state for ${taskId}: ${err.message}`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
await runtime.archiveAttempt(attemptId, true);
|
|
174
|
+
merged += 1;
|
|
175
|
+
log("info", `archived merged attempt ${attemptId} (${branch})`);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
log(
|
|
178
|
+
"warn",
|
|
179
|
+
`failed to archive merged attempt ${attemptId}: ${err?.message || err}`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return merged;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function fillCapacity(runtime, maxParallel, dryRun) {
|
|
188
|
+
const attempts = await runtime.listAttempts();
|
|
189
|
+
const active = attempts.filter((attempt) => !isTerminalAttempt(attempt));
|
|
190
|
+
const availableSlots = Math.max(0, maxParallel - active.length);
|
|
191
|
+
if (availableSlots <= 0) {
|
|
192
|
+
return { submitted: 0, active: active.length };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const todo = sortByCreatedAsc(await runtime.listTasks("todo"));
|
|
196
|
+
const candidates = todo.slice(0, availableSlots);
|
|
197
|
+
if (candidates.length === 0) {
|
|
198
|
+
return { submitted: 0, active: active.length };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let submitted = 0;
|
|
202
|
+
for (const task of candidates) {
|
|
203
|
+
const taskId = String(task.id || "").trim();
|
|
204
|
+
if (!taskId) continue;
|
|
205
|
+
|
|
206
|
+
// Check shared state before claiming task
|
|
207
|
+
if (SHARED_STATE_ENABLED) {
|
|
208
|
+
try {
|
|
209
|
+
const retryCheck = await shouldRetryTask(
|
|
210
|
+
taskId,
|
|
211
|
+
SHARED_STATE_MAX_RETRIES,
|
|
212
|
+
);
|
|
213
|
+
if (!retryCheck.shouldRetry) {
|
|
214
|
+
log("info", `Skipping task ${taskId}: ${retryCheck.reason}`);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
} catch (err) {
|
|
218
|
+
log("warn", `Shared state check failed for ${taskId}: ${err.message}`);
|
|
219
|
+
// Continue with task on error (graceful degradation)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (dryRun) {
|
|
224
|
+
log(
|
|
225
|
+
"info",
|
|
226
|
+
`[dry-run] would submit task ${taskId} (${task.title || "untitled"})`,
|
|
227
|
+
);
|
|
228
|
+
submitted += 1;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const attempt = await runtime.submitTaskAttempt(taskId);
|
|
234
|
+
submitted += 1;
|
|
235
|
+
log(
|
|
236
|
+
"info",
|
|
237
|
+
`submitted task ${taskId} -> attempt ${attempt?.id || "(unknown)"} ${attempt?.branch || ""}`,
|
|
238
|
+
);
|
|
239
|
+
try {
|
|
240
|
+
await runtime.updateTaskStatus(taskId, "inprogress");
|
|
241
|
+
} catch {
|
|
242
|
+
// best-effort
|
|
243
|
+
}
|
|
244
|
+
} catch (err) {
|
|
245
|
+
log("warn", `submit failed for task ${taskId}: ${err?.message || err}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return { submitted, active: active.length };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export async function runOrchestrator(argv, runtime = new VeKanbanRuntime()) {
|
|
253
|
+
const opts = parseOrchestratorArgs(argv);
|
|
254
|
+
let stopRequested = false;
|
|
255
|
+
let cycle = 0;
|
|
256
|
+
|
|
257
|
+
process.on("SIGINT", () => {
|
|
258
|
+
stopRequested = true;
|
|
259
|
+
log("warn", "received SIGINT, stopping after current cycle");
|
|
260
|
+
});
|
|
261
|
+
process.on("SIGTERM", () => {
|
|
262
|
+
stopRequested = true;
|
|
263
|
+
log("warn", "received SIGTERM, stopping after current cycle");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
log(
|
|
267
|
+
"info",
|
|
268
|
+
`starting native orchestrator (maxParallel=${opts.maxParallel}, poll=${opts.pollIntervalSec}s, dryRun=${opts.dryRun}, oneShot=${opts.oneShot})`,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
while (!stopRequested) {
|
|
272
|
+
cycle += 1;
|
|
273
|
+
try {
|
|
274
|
+
await runtime.ensureConfig();
|
|
275
|
+
|
|
276
|
+
// Periodically sweep stale shared states (every cycle)
|
|
277
|
+
if (SHARED_STATE_ENABLED) {
|
|
278
|
+
try {
|
|
279
|
+
const sweepResult = await sweepStaleSharedStates(
|
|
280
|
+
SHARED_STATE_STALE_THRESHOLD_MS,
|
|
281
|
+
process.cwd(),
|
|
282
|
+
);
|
|
283
|
+
if (sweepResult.sweptCount > 0) {
|
|
284
|
+
log(
|
|
285
|
+
"info",
|
|
286
|
+
`swept ${sweepResult.sweptCount} stale shared state(s): ${sweepResult.abandonedTasks.join(", ")}`,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
} catch (err) {
|
|
290
|
+
log("warn", `shared state sweep failed: ${err.message}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const attemptsBefore = await runtime.listAttempts();
|
|
295
|
+
const merged = await reconcileMergedAttempts(
|
|
296
|
+
runtime,
|
|
297
|
+
attemptsBefore,
|
|
298
|
+
opts.dryRun,
|
|
299
|
+
);
|
|
300
|
+
const fill = await fillCapacity(runtime, opts.maxParallel, opts.dryRun);
|
|
301
|
+
|
|
302
|
+
log(
|
|
303
|
+
"info",
|
|
304
|
+
`cycle #${cycle}: active=${fill.active}, merged=${merged}, submitted=${fill.submitted}`,
|
|
305
|
+
);
|
|
306
|
+
} catch (err) {
|
|
307
|
+
log("error", `cycle #${cycle} failed: ${err?.message || err}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (opts.oneShot || stopRequested) {
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
await sleep(opts.pollIntervalSec * 1000);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
log("info", "orchestrator stopped");
|
|
317
|
+
return 0;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const isDirectRun = (() => {
|
|
321
|
+
if (!process.argv[1]) return false;
|
|
322
|
+
try {
|
|
323
|
+
return fileURLToPath(import.meta.url) === resolve(process.argv[1]);
|
|
324
|
+
} catch {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
})();
|
|
328
|
+
|
|
329
|
+
if (isDirectRun) {
|
|
330
|
+
runOrchestrator(process.argv.slice(2))
|
|
331
|
+
.then((code) => {
|
|
332
|
+
if (Number.isFinite(code) && code !== 0) {
|
|
333
|
+
process.exit(code);
|
|
334
|
+
}
|
|
335
|
+
})
|
|
336
|
+
.catch((err) => {
|
|
337
|
+
log("error", err?.message || String(err));
|
|
338
|
+
process.exit(1);
|
|
339
|
+
});
|
|
340
|
+
}
|