@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.mjs
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const CACHE_DIR = resolve(__dirname, ".cache");
|
|
10
|
+
const EXECUTOR_STATE_PATH = resolve(CACHE_DIR, "ve-kanban-executor-state.json");
|
|
11
|
+
|
|
12
|
+
const DEFAULT_EXECUTOR_PROFILES = [
|
|
13
|
+
{ executor: "CODEX", variant: "DEFAULT" },
|
|
14
|
+
{ executor: "COPILOT", variant: "CLAUDE_OPUS_4_6" },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function logInfo(msg) {
|
|
18
|
+
console.log(` ${msg}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function logWarn(msg) {
|
|
22
|
+
console.warn(` ⚠ ${msg}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function logError(msg) {
|
|
26
|
+
console.error(` ✗ ${msg}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function asArray(value) {
|
|
30
|
+
if (!value) return [];
|
|
31
|
+
if (Array.isArray(value)) return value;
|
|
32
|
+
return [value];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getPayloadData(payload) {
|
|
36
|
+
if (Array.isArray(payload)) return payload;
|
|
37
|
+
if (!payload || typeof payload !== "object") return payload;
|
|
38
|
+
if (payload.success === false) {
|
|
39
|
+
throw new Error(payload.message || "VK API returned success=false");
|
|
40
|
+
}
|
|
41
|
+
return payload.data ?? payload.tasks ?? payload.projects ?? payload;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readJsonFile(path, fallback) {
|
|
45
|
+
try {
|
|
46
|
+
if (!existsSync(path)) return fallback;
|
|
47
|
+
const raw = readFileSync(path, "utf8");
|
|
48
|
+
return JSON.parse(raw);
|
|
49
|
+
} catch {
|
|
50
|
+
return fallback;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function writeJsonFile(path, value) {
|
|
55
|
+
try {
|
|
56
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
57
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
58
|
+
} catch {
|
|
59
|
+
// best-effort cache write
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isTruthy(value) {
|
|
64
|
+
if (value == null) return false;
|
|
65
|
+
const normalized = String(value).trim().toLowerCase();
|
|
66
|
+
return ["1", "true", "yes", "on"].includes(normalized);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function stripAnsi(value) {
|
|
70
|
+
return String(value || "").replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseExecutorProfiles(rawValue) {
|
|
74
|
+
const raw = String(rawValue || "").trim();
|
|
75
|
+
if (!raw) return DEFAULT_EXECUTOR_PROFILES;
|
|
76
|
+
const parsed = raw
|
|
77
|
+
.split(",")
|
|
78
|
+
.map((item) => item.trim())
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.map((item) => {
|
|
81
|
+
const [executorRaw, variantRaw] = item.split(":").map((part) => (part || "").trim());
|
|
82
|
+
if (!executorRaw) return null;
|
|
83
|
+
return {
|
|
84
|
+
executor: executorRaw.toUpperCase(),
|
|
85
|
+
variant: variantRaw || "DEFAULT",
|
|
86
|
+
};
|
|
87
|
+
})
|
|
88
|
+
.filter(Boolean);
|
|
89
|
+
return parsed.length > 0 ? parsed : DEFAULT_EXECUTOR_PROFILES;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseRepoSlug(raw) {
|
|
93
|
+
const text = String(raw || "").trim().replace(/^https?:\/\/github\.com\//i, "");
|
|
94
|
+
if (!text) return null;
|
|
95
|
+
const cleaned = text.replace(/\.git$/i, "").replace(/^\/+|\/+$/g, "");
|
|
96
|
+
const [owner, repo] = cleaned.split("/", 2);
|
|
97
|
+
if (!owner || !repo) return null;
|
|
98
|
+
return { owner, repo };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeTaskStatus(raw) {
|
|
102
|
+
const key = String(raw || "todo").toLowerCase().trim();
|
|
103
|
+
const map = {
|
|
104
|
+
todo: "todo",
|
|
105
|
+
open: "todo",
|
|
106
|
+
inprogress: "inprogress",
|
|
107
|
+
"in-progress": "inprogress",
|
|
108
|
+
in_progress: "inprogress",
|
|
109
|
+
inreview: "inreview",
|
|
110
|
+
"in-review": "inreview",
|
|
111
|
+
in_review: "inreview",
|
|
112
|
+
done: "done",
|
|
113
|
+
closed: "done",
|
|
114
|
+
cancelled: "cancelled",
|
|
115
|
+
canceled: "cancelled",
|
|
116
|
+
};
|
|
117
|
+
return map[key] || "todo";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function parseValueFlag(args, flags, defaultValue = null) {
|
|
121
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
122
|
+
if (flags.includes(args[i]) && i + 1 < args.length) {
|
|
123
|
+
return args[i + 1];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return defaultValue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function hasFlag(args, flags) {
|
|
130
|
+
return args.some((arg) => flags.includes(arg));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function truncate(value, max = 72) {
|
|
134
|
+
const text = String(value || "");
|
|
135
|
+
if (text.length <= max) return text;
|
|
136
|
+
return `${text.slice(0, Math.max(0, max - 1))}…`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function sortByCreatedAsc(items) {
|
|
140
|
+
return [...items].sort((a, b) => {
|
|
141
|
+
const aTs = Date.parse(a?.created_at || a?.createdAt || 0) || 0;
|
|
142
|
+
const bTs = Date.parse(b?.created_at || b?.createdAt || 0) || 0;
|
|
143
|
+
return aTs - bTs;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function toInt(value, fallback) {
|
|
148
|
+
const parsed = Number.parseInt(String(value), 10);
|
|
149
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export class VeKanbanRuntime {
|
|
153
|
+
constructor(options = {}) {
|
|
154
|
+
const env = options.env || process.env;
|
|
155
|
+
const slugInfo =
|
|
156
|
+
parseRepoSlug(env.GITHUB_REPOSITORY) ||
|
|
157
|
+
parseRepoSlug(
|
|
158
|
+
env.GITHUB_REPO_OWNER && env.GITHUB_REPO_NAME
|
|
159
|
+
? `${env.GITHUB_REPO_OWNER}/${env.GITHUB_REPO_NAME}`
|
|
160
|
+
: "",
|
|
161
|
+
);
|
|
162
|
+
this.env = env;
|
|
163
|
+
this.baseUrl =
|
|
164
|
+
options.baseUrl ||
|
|
165
|
+
env.VK_ENDPOINT_URL ||
|
|
166
|
+
env.VK_BASE_URL ||
|
|
167
|
+
"http://127.0.0.1:54089";
|
|
168
|
+
this.projectName =
|
|
169
|
+
options.projectName || env.VK_PROJECT_NAME || slugInfo?.repo || "default-project";
|
|
170
|
+
this.projectId = options.projectId || env.VK_PROJECT_ID || "";
|
|
171
|
+
this.repoId = options.repoId || env.VK_REPO_ID || "";
|
|
172
|
+
this.targetBranch = options.targetBranch || env.VK_TARGET_BRANCH || "origin/main";
|
|
173
|
+
this.ghOwner =
|
|
174
|
+
options.ghOwner ||
|
|
175
|
+
env.GH_OWNER ||
|
|
176
|
+
env.GITHUB_REPO_OWNER ||
|
|
177
|
+
slugInfo?.owner ||
|
|
178
|
+
"unknown";
|
|
179
|
+
this.ghRepo =
|
|
180
|
+
options.ghRepo ||
|
|
181
|
+
env.GH_REPO ||
|
|
182
|
+
env.GITHUB_REPO_NAME ||
|
|
183
|
+
slugInfo?.repo ||
|
|
184
|
+
"unknown";
|
|
185
|
+
this.executorProfiles = parseExecutorProfiles(env.VK_EXECUTOR_PROFILES);
|
|
186
|
+
this.executorStatePath = options.executorStatePath || EXECUTOR_STATE_PATH;
|
|
187
|
+
this.fetchImpl = options.fetchImpl || globalThis.fetch;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async api(path, opts = {}) {
|
|
191
|
+
if (typeof this.fetchImpl !== "function") {
|
|
192
|
+
throw new Error("global fetch is unavailable in this Node runtime");
|
|
193
|
+
}
|
|
194
|
+
const method = (opts.method || "GET").toUpperCase();
|
|
195
|
+
const url = `${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
|
|
196
|
+
const headers = { "content-type": "application/json" };
|
|
197
|
+
const fetchOptions = {
|
|
198
|
+
method,
|
|
199
|
+
headers,
|
|
200
|
+
};
|
|
201
|
+
if (opts.body != null && method !== "GET") {
|
|
202
|
+
fetchOptions.body = JSON.stringify(opts.body);
|
|
203
|
+
}
|
|
204
|
+
const res = await this.fetchImpl(url, fetchOptions);
|
|
205
|
+
const rawText = await res.text();
|
|
206
|
+
let payload;
|
|
207
|
+
try {
|
|
208
|
+
payload = rawText ? JSON.parse(rawText) : {};
|
|
209
|
+
} catch {
|
|
210
|
+
throw new Error(`VK API ${method} ${path} returned non-JSON: ${truncate(rawText, 200)}`);
|
|
211
|
+
}
|
|
212
|
+
if (!res.ok) {
|
|
213
|
+
const detail = payload?.message || payload?.error || rawText || `HTTP ${res.status}`;
|
|
214
|
+
throw new Error(`VK API ${method} ${path} failed: ${detail}`);
|
|
215
|
+
}
|
|
216
|
+
return getPayloadData(payload);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async ensureConfig() {
|
|
220
|
+
if (!this.projectId) {
|
|
221
|
+
const projectsPayload = await this.api("/api/projects");
|
|
222
|
+
const projects = asArray(projectsPayload);
|
|
223
|
+
const target = projects.find((project) => {
|
|
224
|
+
const names = [project?.name, project?.display_name, project?.title]
|
|
225
|
+
.map((value) => String(value || "").toLowerCase())
|
|
226
|
+
.filter(Boolean);
|
|
227
|
+
return names.includes(String(this.projectName).toLowerCase());
|
|
228
|
+
});
|
|
229
|
+
if (!target) {
|
|
230
|
+
const available = projects
|
|
231
|
+
.map((project) => project?.name || project?.display_name || project?.title || project?.id)
|
|
232
|
+
.filter(Boolean)
|
|
233
|
+
.join(", ");
|
|
234
|
+
throw new Error(
|
|
235
|
+
`No project named \"${this.projectName}\" found at ${this.baseUrl}. Available: ${available || "(none)"}`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
this.projectId = target.id;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!this.repoId) {
|
|
242
|
+
let reposPayload;
|
|
243
|
+
try {
|
|
244
|
+
reposPayload = await this.api(`/api/repos?project_id=${encodeURIComponent(this.projectId)}`);
|
|
245
|
+
} catch {
|
|
246
|
+
reposPayload = await this.api(`/api/projects/${encodeURIComponent(this.projectId)}/repos`);
|
|
247
|
+
}
|
|
248
|
+
const repos = asArray(reposPayload);
|
|
249
|
+
if (repos.length === 0) {
|
|
250
|
+
throw new Error(`No repos returned for project ${this.projectId}. Set VK_REPO_ID manually.`);
|
|
251
|
+
}
|
|
252
|
+
const preferredRepoName = this.ghRepo.toLowerCase();
|
|
253
|
+
const repo =
|
|
254
|
+
repos.find((candidate) => String(candidate?.name || "").toLowerCase() === preferredRepoName) ||
|
|
255
|
+
repos[0];
|
|
256
|
+
this.repoId = repo.id;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return { projectId: this.projectId, repoId: this.repoId };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async listTasks(status = null) {
|
|
263
|
+
await this.ensureConfig();
|
|
264
|
+
const payload = await this.api(`/api/tasks?project_id=${encodeURIComponent(this.projectId)}`);
|
|
265
|
+
const tasks = asArray(payload);
|
|
266
|
+
if (!status) return tasks;
|
|
267
|
+
const targetStatus = normalizeTaskStatus(status);
|
|
268
|
+
return tasks.filter((task) => normalizeTaskStatus(task?.status) === targetStatus);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async getTask(taskId) {
|
|
272
|
+
return this.api(`/api/tasks/${encodeURIComponent(taskId)}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async createTask({ title, description, status = "todo" }) {
|
|
276
|
+
await this.ensureConfig();
|
|
277
|
+
return this.api(`/api/tasks`, {
|
|
278
|
+
method: "POST",
|
|
279
|
+
body: {
|
|
280
|
+
title,
|
|
281
|
+
description,
|
|
282
|
+
status: normalizeTaskStatus(status),
|
|
283
|
+
project_id: this.projectId,
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async updateTaskStatus(taskId, status) {
|
|
289
|
+
return this.api(`/api/tasks/${encodeURIComponent(taskId)}`, {
|
|
290
|
+
method: "PUT",
|
|
291
|
+
body: { status: normalizeTaskStatus(status) },
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async listAttempts() {
|
|
296
|
+
const payload = await this.api(`/api/task-attempts`);
|
|
297
|
+
return asArray(payload);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async listArchivedAttempts() {
|
|
301
|
+
const attempts = await this.listAttempts();
|
|
302
|
+
return attempts.filter((attempt) => Boolean(attempt?.archived));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async archiveAttempt(attemptId, archived = true) {
|
|
306
|
+
return this.api(`/api/task-attempts/${encodeURIComponent(attemptId)}`, {
|
|
307
|
+
method: "PUT",
|
|
308
|
+
body: { archived: Boolean(archived) },
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async rebaseAttempt(attemptId, baseBranch = this.targetBranch) {
|
|
313
|
+
await this.ensureConfig();
|
|
314
|
+
return this.api(`/api/task-attempts/${encodeURIComponent(attemptId)}/rebase`, {
|
|
315
|
+
method: "POST",
|
|
316
|
+
body: {
|
|
317
|
+
repo_id: this.repoId,
|
|
318
|
+
old_base_branch: baseBranch,
|
|
319
|
+
new_base_branch: baseBranch,
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
isCopilotCloudDisabled() {
|
|
325
|
+
if (isTruthy(this.env.COPILOT_CLOUD_DISABLED)) return true;
|
|
326
|
+
const until = this.env.COPILOT_CLOUD_DISABLED_UNTIL;
|
|
327
|
+
if (!until) return false;
|
|
328
|
+
const ts = Date.parse(until);
|
|
329
|
+
if (!Number.isFinite(ts)) return false;
|
|
330
|
+
return Date.now() < ts;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
getNextExecutorProfile() {
|
|
334
|
+
const profiles = this.executorProfiles.length > 0 ? this.executorProfiles : DEFAULT_EXECUTOR_PROFILES;
|
|
335
|
+
const state = readJsonFile(this.executorStatePath, {
|
|
336
|
+
index: Math.floor(Math.random() * profiles.length),
|
|
337
|
+
});
|
|
338
|
+
let index = Number.isFinite(state?.index) ? state.index : 0;
|
|
339
|
+
|
|
340
|
+
for (let i = 0; i < profiles.length; i += 1) {
|
|
341
|
+
const profile = profiles[((index % profiles.length) + profiles.length) % profiles.length];
|
|
342
|
+
index += 1;
|
|
343
|
+
if (this.isCopilotCloudDisabled() && profile.executor === "COPILOT") {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
writeJsonFile(this.executorStatePath, { index: index % profiles.length });
|
|
347
|
+
return profile;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const fallback = profiles[0];
|
|
351
|
+
writeJsonFile(this.executorStatePath, { index: 1 % profiles.length });
|
|
352
|
+
return fallback;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async submitTaskAttempt(taskId, options = {}) {
|
|
356
|
+
await this.ensureConfig();
|
|
357
|
+
const executorProfile = options.executorOverride || this.getNextExecutorProfile();
|
|
358
|
+
const targetBranch = options.targetBranch || this.targetBranch;
|
|
359
|
+
return this.api(`/api/task-attempts`, {
|
|
360
|
+
method: "POST",
|
|
361
|
+
body: {
|
|
362
|
+
task_id: taskId,
|
|
363
|
+
repos: [
|
|
364
|
+
{
|
|
365
|
+
repo_id: this.repoId,
|
|
366
|
+
target_branch: targetBranch,
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
executor_profile_id: {
|
|
370
|
+
executor: executorProfile.executor,
|
|
371
|
+
variant: executorProfile.variant,
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
runGh(args) {
|
|
378
|
+
try {
|
|
379
|
+
const output = execFileSync("gh", args, {
|
|
380
|
+
encoding: "utf8",
|
|
381
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
382
|
+
});
|
|
383
|
+
return { ok: true, output: stripAnsi(output).trim(), error: "" };
|
|
384
|
+
} catch (err) {
|
|
385
|
+
const stderr = stripAnsi(err?.stderr?.toString?.() || err?.message || "").trim();
|
|
386
|
+
return { ok: false, output: "", error: stderr };
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
findPullRequestForBranch(branch, state = "open") {
|
|
391
|
+
const result = this.runGh([
|
|
392
|
+
"pr",
|
|
393
|
+
"list",
|
|
394
|
+
"--repo",
|
|
395
|
+
`${this.ghOwner}/${this.ghRepo}`,
|
|
396
|
+
"--head",
|
|
397
|
+
branch,
|
|
398
|
+
"--state",
|
|
399
|
+
state,
|
|
400
|
+
"--json",
|
|
401
|
+
"number,state,url,title",
|
|
402
|
+
"--limit",
|
|
403
|
+
"1",
|
|
404
|
+
]);
|
|
405
|
+
if (!result.ok || !result.output) return null;
|
|
406
|
+
try {
|
|
407
|
+
const rows = JSON.parse(result.output);
|
|
408
|
+
return Array.isArray(rows) && rows.length > 0 ? rows[0] : null;
|
|
409
|
+
} catch {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
mergePullRequest(prNumber, { auto = false } = {}) {
|
|
415
|
+
const args = [
|
|
416
|
+
"pr",
|
|
417
|
+
"merge",
|
|
418
|
+
String(prNumber),
|
|
419
|
+
"--repo",
|
|
420
|
+
`${this.ghOwner}/${this.ghRepo}`,
|
|
421
|
+
"--squash",
|
|
422
|
+
"--delete-branch",
|
|
423
|
+
];
|
|
424
|
+
if (auto) args.push("--auto");
|
|
425
|
+
return this.runGh(args);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export function parseKanbanCommand(argv) {
|
|
430
|
+
const args = [...argv];
|
|
431
|
+
if (args.length === 0) return { command: "help", args: [] };
|
|
432
|
+
return { command: args[0], args: args.slice(1) };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function printTaskList(tasks, status) {
|
|
436
|
+
if (!tasks || tasks.length === 0) {
|
|
437
|
+
logInfo(`No tasks found for status=${status}.`);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
logInfo(`Tasks (${status}):`);
|
|
441
|
+
for (const task of tasks) {
|
|
442
|
+
const id = String(task?.id || "").slice(0, 8);
|
|
443
|
+
const title = truncate(task?.title || "(untitled)", 100);
|
|
444
|
+
logInfo(`- ${id} ${title}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function printStatusDashboard(tasks, attempts, showVerbose = false) {
|
|
449
|
+
const todo = tasks.filter((task) => normalizeTaskStatus(task?.status) === "todo");
|
|
450
|
+
const inProgress = tasks.filter((task) => normalizeTaskStatus(task?.status) === "inprogress");
|
|
451
|
+
const activeAttempts = attempts.filter((attempt) => !attempt?.archived);
|
|
452
|
+
|
|
453
|
+
logInfo(`Todo: ${todo.length}`);
|
|
454
|
+
logInfo(`In-Progress: ${inProgress.length}`);
|
|
455
|
+
logInfo(`Active Attempts: ${activeAttempts.length}`);
|
|
456
|
+
|
|
457
|
+
if (activeAttempts.length > 0) {
|
|
458
|
+
logInfo("Active attempts:");
|
|
459
|
+
for (const attempt of activeAttempts) {
|
|
460
|
+
const id = String(attempt?.id || "").slice(0, 8);
|
|
461
|
+
const branch = attempt?.branch || attempt?.branch_name || "(no branch)";
|
|
462
|
+
const name = truncate(attempt?.name || attempt?.task_title || "(unnamed)", 90);
|
|
463
|
+
logInfo(`- ${id} ${branch}`);
|
|
464
|
+
logInfo(` ${name}`);
|
|
465
|
+
if (showVerbose) {
|
|
466
|
+
const lastUpdate = attempt?.updated_at || attempt?.created_at || "unknown";
|
|
467
|
+
logInfo(` last update: ${lastUpdate}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export async function runKanbanCli(argv, runtime = new VeKanbanRuntime()) {
|
|
474
|
+
const { command, args } = parseKanbanCommand(argv);
|
|
475
|
+
|
|
476
|
+
switch (command) {
|
|
477
|
+
case "create": {
|
|
478
|
+
const title = parseValueFlag(args, ["--title", "-t"], null);
|
|
479
|
+
let description = parseValueFlag(args, ["--description", "--desc", "-d"], null);
|
|
480
|
+
const descFile = parseValueFlag(args, ["--description-file", "--desc-file"], null);
|
|
481
|
+
const status = parseValueFlag(args, ["--status", "-s"], "todo");
|
|
482
|
+
if (!description && descFile) {
|
|
483
|
+
description = readFileSync(resolve(process.cwd(), descFile), "utf8");
|
|
484
|
+
}
|
|
485
|
+
if (!title || !description) {
|
|
486
|
+
throw new Error("Usage: ve-kanban create --title <title> --description <markdown> [--status todo]");
|
|
487
|
+
}
|
|
488
|
+
const created = await runtime.createTask({ title, description, status });
|
|
489
|
+
logInfo(`✓ Task created: ${created?.id || "(id unavailable)"} — ${title}`);
|
|
490
|
+
return 0;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
case "list": {
|
|
494
|
+
const status = parseValueFlag(args, ["--status", "-s"], "todo");
|
|
495
|
+
const tasks = await runtime.listTasks(status);
|
|
496
|
+
printTaskList(tasks, status);
|
|
497
|
+
return 0;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
case "status": {
|
|
501
|
+
const showVerbose = hasFlag(args, ["--verbose", "-v"]);
|
|
502
|
+
const tasks = await runtime.listTasks();
|
|
503
|
+
const attempts = await runtime.listAttempts();
|
|
504
|
+
printStatusDashboard(tasks, attempts, showVerbose);
|
|
505
|
+
return 0;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
case "archived": {
|
|
509
|
+
const attempts = await runtime.listArchivedAttempts();
|
|
510
|
+
if (attempts.length === 0) {
|
|
511
|
+
logInfo("No archived attempts.");
|
|
512
|
+
return 0;
|
|
513
|
+
}
|
|
514
|
+
logInfo("Archived attempts:");
|
|
515
|
+
for (const attempt of attempts) {
|
|
516
|
+
const id = String(attempt?.id || "").slice(0, 8);
|
|
517
|
+
const branch = attempt?.branch || attempt?.branch_name || "(no branch)";
|
|
518
|
+
logInfo(`- ${id} ${branch}`);
|
|
519
|
+
}
|
|
520
|
+
return 0;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
case "unarchive": {
|
|
524
|
+
const attemptId = args[0];
|
|
525
|
+
if (!attemptId) throw new Error("Usage: ve-kanban unarchive <attempt-id>");
|
|
526
|
+
await runtime.archiveAttempt(attemptId, false);
|
|
527
|
+
logInfo(`✓ Attempt ${attemptId} unarchived`);
|
|
528
|
+
return 0;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
case "submit": {
|
|
532
|
+
const taskId = args[0];
|
|
533
|
+
if (!taskId) throw new Error("Usage: ve-kanban submit <task-id>");
|
|
534
|
+
const attempt = await runtime.submitTaskAttempt(taskId);
|
|
535
|
+
logInfo(`✓ Attempt created: ${attempt?.id || "(id unavailable)"} → branch ${attempt?.branch || "(unknown)"}`);
|
|
536
|
+
return 0;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
case "submit-next": {
|
|
540
|
+
const count = toInt(parseValueFlag(args, ["--count", "-n"], "1"), 1);
|
|
541
|
+
const todo = sortByCreatedAsc(await runtime.listTasks("todo"));
|
|
542
|
+
const selected = todo.slice(0, Math.max(0, count));
|
|
543
|
+
if (selected.length === 0) {
|
|
544
|
+
logInfo("No todo tasks available.");
|
|
545
|
+
return 0;
|
|
546
|
+
}
|
|
547
|
+
for (const task of selected) {
|
|
548
|
+
logInfo(`→ ${task?.title || task?.id}`);
|
|
549
|
+
const attempt = await runtime.submitTaskAttempt(task.id);
|
|
550
|
+
logInfo(` ✓ ${attempt?.id || "(id unavailable)"} ${attempt?.branch || ""}`);
|
|
551
|
+
}
|
|
552
|
+
return 0;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
case "rebase": {
|
|
556
|
+
const attemptId = args[0];
|
|
557
|
+
if (!attemptId) throw new Error("Usage: ve-kanban rebase <attempt-id>");
|
|
558
|
+
await runtime.rebaseAttempt(attemptId);
|
|
559
|
+
logInfo(`✓ Rebase requested for attempt ${attemptId}`);
|
|
560
|
+
return 0;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
case "merge": {
|
|
564
|
+
const branch = parseValueFlag(args, ["--branch", "-b"], args.find((value) => !value.startsWith("-")) || null);
|
|
565
|
+
const auto = hasFlag(args, ["--auto"]);
|
|
566
|
+
if (!branch) throw new Error("Usage: ve-kanban merge <branch> [--auto]");
|
|
567
|
+
const pr = runtime.findPullRequestForBranch(branch, "open") || runtime.findPullRequestForBranch(branch, "all");
|
|
568
|
+
if (!pr) throw new Error(`No PR found for branch ${branch}`);
|
|
569
|
+
const merged = runtime.mergePullRequest(pr.number, { auto });
|
|
570
|
+
if (!merged.ok) throw new Error(merged.error || `Failed to merge PR #${pr.number}`);
|
|
571
|
+
logInfo(`✓ PR #${pr.number} merge requested${auto ? " (auto-merge)" : ""}`);
|
|
572
|
+
return 0;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
case "complete": {
|
|
576
|
+
const taskId = args[0];
|
|
577
|
+
if (!taskId) throw new Error("Usage: ve-kanban complete <task-id>");
|
|
578
|
+
await runtime.updateTaskStatus(taskId, "done");
|
|
579
|
+
logInfo(`✓ Task ${taskId} marked as done`);
|
|
580
|
+
return 0;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
case "orchestrate": {
|
|
584
|
+
const parallel = toInt(parseValueFlag(args, ["--parallel", "-p"], "2"), 2);
|
|
585
|
+
const interval = toInt(parseValueFlag(args, ["--interval", "-i"], "60"), 60);
|
|
586
|
+
const orchestratorPath = resolve(__dirname, "ve-orchestrator.mjs");
|
|
587
|
+
logInfo(`Delegating to ve-orchestrator.mjs with parallel=${parallel}, interval=${interval}s`);
|
|
588
|
+
const child = spawnSync(process.execPath, [orchestratorPath, "-MaxParallel", String(parallel), "-PollIntervalSec", String(interval)], {
|
|
589
|
+
stdio: "inherit",
|
|
590
|
+
env: { ...process.env },
|
|
591
|
+
});
|
|
592
|
+
return child.status ?? 1;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
case "help":
|
|
596
|
+
case "--help":
|
|
597
|
+
case "-h":
|
|
598
|
+
printUsage();
|
|
599
|
+
return 0;
|
|
600
|
+
|
|
601
|
+
default:
|
|
602
|
+
printUsage();
|
|
603
|
+
throw new Error(`Unknown command: ${command}`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
export function printUsage() {
|
|
608
|
+
console.log(`
|
|
609
|
+
OpenFleet Kanban CLI (ve-kanban)
|
|
610
|
+
=================================
|
|
611
|
+
|
|
612
|
+
Commands:
|
|
613
|
+
create --title <title> --description <md> [--status todo]
|
|
614
|
+
list [--status <status>]
|
|
615
|
+
status [--verbose]
|
|
616
|
+
archived
|
|
617
|
+
unarchive <attempt-id>
|
|
618
|
+
submit <task-id>
|
|
619
|
+
submit-next [--count N]
|
|
620
|
+
rebase <attempt-id>
|
|
621
|
+
merge <branch> [--auto]
|
|
622
|
+
complete <task-id>
|
|
623
|
+
orchestrate [--parallel N] [--interval sec]
|
|
624
|
+
help
|
|
625
|
+
|
|
626
|
+
Environment:
|
|
627
|
+
VK_BASE_URL / VK_ENDPOINT_URL
|
|
628
|
+
VK_PROJECT_NAME / VK_PROJECT_ID / VK_REPO_ID
|
|
629
|
+
GH_OWNER / GH_REPO
|
|
630
|
+
VK_TARGET_BRANCH (default: origin/main)
|
|
631
|
+
`);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const isDirectRun = (() => {
|
|
635
|
+
if (!process.argv[1]) return false;
|
|
636
|
+
try {
|
|
637
|
+
return fileURLToPath(import.meta.url) === resolve(process.argv[1]);
|
|
638
|
+
} catch {
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
})();
|
|
642
|
+
|
|
643
|
+
if (isDirectRun) {
|
|
644
|
+
runKanbanCli(process.argv.slice(2))
|
|
645
|
+
.then((code) => {
|
|
646
|
+
if (Number.isFinite(code) && code !== 0) {
|
|
647
|
+
process.exit(code);
|
|
648
|
+
}
|
|
649
|
+
})
|
|
650
|
+
.catch((err) => {
|
|
651
|
+
logError(err?.message || String(err));
|
|
652
|
+
process.exit(1);
|
|
653
|
+
});
|
|
654
|
+
}
|