@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
|
@@ -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-orchestrator.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-orchestrator.sh] Native runtime unavailable (need node + ve-orchestrator.mjs)." >&2
|
|
18
|
+
exit 1
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* vibe-kanban-wrapper.mjs - Wrapper to expose bundled vibe-kanban CLI
|
|
4
|
+
*
|
|
5
|
+
* This wrapper ensures the bundled vibe-kanban CLI is available when
|
|
6
|
+
* @virtengine/openfleet is installed globally. npm doesn't expose
|
|
7
|
+
* bins from transitive dependencies, so we need this proxy.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { dirname, resolve } from "node:path";
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
|
|
17
|
+
// Path to the bundled vibe-kanban CLI
|
|
18
|
+
const vkBin = resolve(
|
|
19
|
+
__dirname,
|
|
20
|
+
"node_modules",
|
|
21
|
+
"vibe-kanban",
|
|
22
|
+
"bin",
|
|
23
|
+
"cli.js",
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// Forward all args to the bundled vibe-kanban
|
|
27
|
+
const args = process.argv.slice(2);
|
|
28
|
+
const child = spawn("node", [vkBin, ...args], {
|
|
29
|
+
stdio: "inherit",
|
|
30
|
+
shell: false,
|
|
31
|
+
env: {
|
|
32
|
+
...process.env,
|
|
33
|
+
// Prevent git from opening interactive editors (blocks agents if AFK)
|
|
34
|
+
GIT_EDITOR: "true",
|
|
35
|
+
GIT_SEQUENCE_EDITOR: "true",
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
child.on("exit", (code) => {
|
|
40
|
+
process.exit(code || 0);
|
|
41
|
+
});
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vk-error-resolver.mjs — Background agent for auto-resolving VK log errors
|
|
3
|
+
*
|
|
4
|
+
* Monitors VK log stream for error patterns and spawns resolution agents
|
|
5
|
+
* in isolated worktrees. Each resolution operates independently to avoid
|
|
6
|
+
* blocking the main orchestration loop.
|
|
7
|
+
*
|
|
8
|
+
* Supported error patterns:
|
|
9
|
+
* 1. Uncommitted changes: `has uncommitted changes: <files>`
|
|
10
|
+
* 2. Push failures: `Failed to push branch to remote`
|
|
11
|
+
* 3. CI re-trigger failures: `Failed to re-trigger CI on PR`
|
|
12
|
+
*
|
|
13
|
+
* Resolution strategy:
|
|
14
|
+
* - Each resolution runs in a fresh worktree
|
|
15
|
+
* - Maximum 3 attempts per error signature
|
|
16
|
+
* - 5-minute cooldown between attempts
|
|
17
|
+
* - Only resolves for successfully completed tasks
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { spawn, execSync } from "node:child_process";
|
|
21
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
|
|
22
|
+
import { resolve } from "node:path";
|
|
23
|
+
import { fileURLToPath } from "url";
|
|
24
|
+
import { getWorktreeManager } from "./worktree-manager.mjs";
|
|
25
|
+
|
|
26
|
+
const __dirname = resolve(fileURLToPath(new URL(".", import.meta.url)));
|
|
27
|
+
|
|
28
|
+
// ── Configuration ────────────────────────────────────────────────────────────
|
|
29
|
+
const CONFIG = {
|
|
30
|
+
maxAttempts: 3,
|
|
31
|
+
cooldownMinutes: 5,
|
|
32
|
+
worktreePrefix: "openfleet-resolver",
|
|
33
|
+
stateFile: resolve(__dirname, "logs", "vk-error-resolver-state.json"),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// ── Error Pattern Detection ─────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
const ERROR_PATTERNS = [
|
|
39
|
+
{
|
|
40
|
+
name: "uncommitted-changes",
|
|
41
|
+
pattern: /has uncommitted changes: (.+)$/,
|
|
42
|
+
extract: (match, logLine) => ({
|
|
43
|
+
branch: extractBranch(logLine),
|
|
44
|
+
files: match[1].split(",").map((f) => f.trim()),
|
|
45
|
+
}),
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "push-failure",
|
|
49
|
+
pattern: /Failed to push branch to remote: (\S+)/,
|
|
50
|
+
extract: (match, logLine) => ({
|
|
51
|
+
branch: match[1],
|
|
52
|
+
}),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "ci-retrigger-failure",
|
|
56
|
+
pattern: /Failed to re-trigger CI on PR #(\d+)/,
|
|
57
|
+
extract: (match, logLine) => ({
|
|
58
|
+
prNumber: match[1],
|
|
59
|
+
}),
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
function extractBranch(logLine) {
|
|
64
|
+
const branchMatch = logLine.match(/ve\/[\w-]+/);
|
|
65
|
+
return branchMatch ? branchMatch[0] : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── State Management ─────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
class ResolutionState {
|
|
71
|
+
constructor(stateFile) {
|
|
72
|
+
this.stateFile = stateFile;
|
|
73
|
+
this.state = this.load();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
load() {
|
|
77
|
+
if (!existsSync(this.stateFile)) {
|
|
78
|
+
return { attempts: {}, cooldowns: {} };
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(readFileSync(this.stateFile, "utf8"));
|
|
82
|
+
} catch {
|
|
83
|
+
return { attempts: {}, cooldowns: {} };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
save() {
|
|
88
|
+
const dir = resolve(this.stateFile, "..");
|
|
89
|
+
if (!existsSync(dir)) {
|
|
90
|
+
mkdirSync(dir, { recursive: true });
|
|
91
|
+
}
|
|
92
|
+
writeFileSync(this.stateFile, JSON.stringify(this.state, null, 2));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
getSignature(errorType, context) {
|
|
96
|
+
return `${errorType}:${context.branch || context.prNumber}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
canAttempt(signature) {
|
|
100
|
+
const attempts = this.state.attempts[signature] || 0;
|
|
101
|
+
if (attempts >= CONFIG.maxAttempts) {
|
|
102
|
+
return { allowed: false, reason: "max-attempts-reached" };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const cooldownUntil = this.state.cooldowns[signature];
|
|
106
|
+
if (cooldownUntil && new Date() < new Date(cooldownUntil)) {
|
|
107
|
+
return { allowed: false, reason: "cooldown-active" };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { allowed: true };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
recordAttempt(signature) {
|
|
114
|
+
this.state.attempts[signature] = (this.state.attempts[signature] || 0) + 1;
|
|
115
|
+
const cooldownUntil = new Date(
|
|
116
|
+
Date.now() + CONFIG.cooldownMinutes * 60 * 1000,
|
|
117
|
+
);
|
|
118
|
+
this.state.cooldowns[signature] = cooldownUntil.toISOString();
|
|
119
|
+
this.save();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
clearSignature(signature) {
|
|
123
|
+
delete this.state.attempts[signature];
|
|
124
|
+
delete this.state.cooldowns[signature];
|
|
125
|
+
this.save();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── Task Status Verification ─────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Check if a task attempt completed successfully
|
|
133
|
+
* Only auto-resolve errors for successful completions
|
|
134
|
+
*/
|
|
135
|
+
async function isTaskSuccessful(branch, vkBaseUrl) {
|
|
136
|
+
try {
|
|
137
|
+
// Extract task ID from branch name (ve/<id>-<slug>)
|
|
138
|
+
const taskIdMatch = branch.match(/ve\/([a-f0-9]+)-/);
|
|
139
|
+
if (!taskIdMatch) return false;
|
|
140
|
+
|
|
141
|
+
const taskId = taskIdMatch[1];
|
|
142
|
+
|
|
143
|
+
// Query VK API for task status
|
|
144
|
+
const response = await fetch(
|
|
145
|
+
`${vkBaseUrl}/api/task-attempts?task_id=${taskId}`,
|
|
146
|
+
{
|
|
147
|
+
headers: { Accept: "application/json" },
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
if (!response.ok) return false;
|
|
152
|
+
|
|
153
|
+
const attempts = await response.json();
|
|
154
|
+
const latest = attempts.sort(
|
|
155
|
+
(a, b) => new Date(b.created_at) - new Date(a.created_at),
|
|
156
|
+
)[0];
|
|
157
|
+
|
|
158
|
+
return latest?.status === "COMPLETED" && latest?.result === "SUCCESS";
|
|
159
|
+
} catch (err) {
|
|
160
|
+
console.error(
|
|
161
|
+
`[vk-error-resolver] Failed to check task status: ${err.message}`,
|
|
162
|
+
);
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Resolution Handlers ──────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
class UncommittedChangesResolver {
|
|
170
|
+
constructor(repoPath, stateManager) {
|
|
171
|
+
this.repoPath = repoPath;
|
|
172
|
+
this.stateManager = stateManager;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async resolve(context) {
|
|
176
|
+
const { branch, files } = context;
|
|
177
|
+
const signature = this.stateManager.getSignature(
|
|
178
|
+
"uncommitted-changes",
|
|
179
|
+
context,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
console.log(
|
|
183
|
+
`[vk-error-resolver] Resolving uncommitted changes on ${branch}`,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// Acquire worktree via centralized manager
|
|
187
|
+
const wm = getWorktreeManager(this.repoPath);
|
|
188
|
+
const acquired = await wm.acquireWorktree(
|
|
189
|
+
branch,
|
|
190
|
+
`err-uncommitted-${branch}`,
|
|
191
|
+
{ owner: "error-resolver" },
|
|
192
|
+
);
|
|
193
|
+
const worktreePath = acquired?.path || null;
|
|
194
|
+
if (!worktreePath) {
|
|
195
|
+
return { success: false, reason: "worktree-creation-failed" };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
// Add uncommitted files
|
|
200
|
+
execSync("git add .", {
|
|
201
|
+
cwd: worktreePath,
|
|
202
|
+
stdio: "pipe",
|
|
203
|
+
env: { ...process.env, GIT_EDITOR: ":", GIT_MERGE_AUTOEDIT: "no" },
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Commit changes
|
|
207
|
+
const commitMsg = "chore(openfleet): add uncommitted changes";
|
|
208
|
+
execSync(`git commit -m "${commitMsg}" --no-edit`, {
|
|
209
|
+
cwd: worktreePath,
|
|
210
|
+
stdio: "pipe",
|
|
211
|
+
env: { ...process.env, GIT_EDITOR: ":", GIT_MERGE_AUTOEDIT: "no" },
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Push to remote
|
|
215
|
+
execSync(`git push origin ${branch}`, {
|
|
216
|
+
cwd: worktreePath,
|
|
217
|
+
stdio: "pipe",
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
console.log(
|
|
221
|
+
`[vk-error-resolver] ✓ Resolved uncommitted changes on ${branch}`,
|
|
222
|
+
);
|
|
223
|
+
this.stateManager.clearSignature(signature);
|
|
224
|
+
return { success: true };
|
|
225
|
+
} catch (err) {
|
|
226
|
+
console.error(`[vk-error-resolver] Resolution failed: ${err.message}`);
|
|
227
|
+
return { success: false, reason: err.message };
|
|
228
|
+
} finally {
|
|
229
|
+
wm.releaseWorktreeByPath(worktreePath);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
class PushFailureResolver {
|
|
235
|
+
constructor(repoPath, stateManager) {
|
|
236
|
+
this.repoPath = repoPath;
|
|
237
|
+
this.stateManager = stateManager;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async resolve(context) {
|
|
241
|
+
const { branch } = context;
|
|
242
|
+
const signature = this.stateManager.getSignature("push-failure", context);
|
|
243
|
+
|
|
244
|
+
console.log(`[vk-error-resolver] Resolving push failure on ${branch}`);
|
|
245
|
+
|
|
246
|
+
// Acquire worktree via centralized manager
|
|
247
|
+
const wm = getWorktreeManager(this.repoPath);
|
|
248
|
+
const acquired = await wm.acquireWorktree(branch, `err-push-${branch}`, {
|
|
249
|
+
owner: "error-resolver",
|
|
250
|
+
});
|
|
251
|
+
const worktreePath = acquired?.path || null;
|
|
252
|
+
if (!worktreePath) {
|
|
253
|
+
return { success: false, reason: "worktree-creation-failed" };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
// Fetch latest from remote
|
|
258
|
+
execSync(`git fetch origin ${branch}`, {
|
|
259
|
+
cwd: worktreePath,
|
|
260
|
+
stdio: "pipe",
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Check if behind
|
|
264
|
+
const behind = execSync(`git rev-list --count HEAD..origin/${branch}`, {
|
|
265
|
+
cwd: worktreePath,
|
|
266
|
+
encoding: "utf8",
|
|
267
|
+
}).trim();
|
|
268
|
+
|
|
269
|
+
if (parseInt(behind) > 0) {
|
|
270
|
+
// Rebase and retry push
|
|
271
|
+
execSync(`git rebase origin/${branch}`, {
|
|
272
|
+
cwd: worktreePath,
|
|
273
|
+
stdio: "pipe",
|
|
274
|
+
env: { ...process.env, GIT_EDITOR: ":", GIT_MERGE_AUTOEDIT: "no" },
|
|
275
|
+
});
|
|
276
|
+
execSync(`git push origin ${branch} --force-with-lease`, {
|
|
277
|
+
cwd: worktreePath,
|
|
278
|
+
stdio: "pipe",
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
console.log(
|
|
282
|
+
`[vk-error-resolver] ✓ Resolved push failure on ${branch} (rebased)`,
|
|
283
|
+
);
|
|
284
|
+
this.stateManager.clearSignature(signature);
|
|
285
|
+
return { success: true };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Try force push as last resort
|
|
289
|
+
execSync(`git push origin ${branch} --force-with-lease`, {
|
|
290
|
+
cwd: worktreePath,
|
|
291
|
+
stdio: "pipe",
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
console.log(
|
|
295
|
+
`[vk-error-resolver] ✓ Resolved push failure on ${branch} (force-pushed)`,
|
|
296
|
+
);
|
|
297
|
+
this.stateManager.clearSignature(signature);
|
|
298
|
+
return { success: true };
|
|
299
|
+
} catch (err) {
|
|
300
|
+
console.error(`[vk-error-resolver] Resolution failed: ${err.message}`);
|
|
301
|
+
return { success: false, reason: err.message };
|
|
302
|
+
} finally {
|
|
303
|
+
wm.releaseWorktreeByPath(worktreePath);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
class CIRetriggerResolver {
|
|
309
|
+
constructor(repoPath, stateManager) {
|
|
310
|
+
this.repoPath = repoPath;
|
|
311
|
+
this.stateManager = stateManager;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async resolve(context) {
|
|
315
|
+
const { prNumber } = context;
|
|
316
|
+
const signature = this.stateManager.getSignature("ci-retrigger", context);
|
|
317
|
+
|
|
318
|
+
console.log(
|
|
319
|
+
`[vk-error-resolver] Resolving CI re-trigger for PR #${prNumber}`,
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
// Check PR status
|
|
324
|
+
const status = execSync(
|
|
325
|
+
`gh pr view ${prNumber} --json mergeable,mergeStateStatus`,
|
|
326
|
+
{ cwd: this.repoPath, encoding: "utf8" },
|
|
327
|
+
);
|
|
328
|
+
const prStatus = JSON.parse(status);
|
|
329
|
+
|
|
330
|
+
if (prStatus.mergeable === "MERGEABLE") {
|
|
331
|
+
// Create empty commit to trigger CI
|
|
332
|
+
const branch = execSync(
|
|
333
|
+
`gh pr view ${prNumber} --json headRefName --jq .headRefName`,
|
|
334
|
+
{ cwd: this.repoPath, encoding: "utf8" },
|
|
335
|
+
).trim();
|
|
336
|
+
|
|
337
|
+
// Acquire worktree via centralized manager
|
|
338
|
+
const wm = getWorktreeManager(this.repoPath);
|
|
339
|
+
const acquired = await wm.acquireWorktree(branch, `err-ci-${branch}`, {
|
|
340
|
+
owner: "error-resolver",
|
|
341
|
+
});
|
|
342
|
+
const worktreePath = acquired?.path || null;
|
|
343
|
+
if (!worktreePath) {
|
|
344
|
+
return { success: false, reason: "worktree-creation-failed" };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
execSync(
|
|
349
|
+
'git commit --allow-empty -m "chore: trigger CI" --no-edit',
|
|
350
|
+
{
|
|
351
|
+
cwd: worktreePath,
|
|
352
|
+
stdio: "pipe",
|
|
353
|
+
env: {
|
|
354
|
+
...process.env,
|
|
355
|
+
GIT_EDITOR: ":",
|
|
356
|
+
GIT_MERGE_AUTOEDIT: "no",
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
);
|
|
360
|
+
execSync(`git push origin ${branch}`, {
|
|
361
|
+
cwd: worktreePath,
|
|
362
|
+
stdio: "pipe",
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
console.log(`[vk-error-resolver] ✓ Triggered CI for PR #${prNumber}`);
|
|
366
|
+
this.stateManager.clearSignature(signature);
|
|
367
|
+
return { success: true };
|
|
368
|
+
} finally {
|
|
369
|
+
wm.releaseWorktreeByPath(worktreePath);
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
console.log(
|
|
373
|
+
`[vk-error-resolver] PR #${prNumber} not mergeable, escalating`,
|
|
374
|
+
);
|
|
375
|
+
return { success: false, reason: "pr-not-mergeable", escalate: true };
|
|
376
|
+
}
|
|
377
|
+
} catch (err) {
|
|
378
|
+
console.error(`[vk-error-resolver] Resolution failed: ${err.message}`);
|
|
379
|
+
return { success: false, reason: err.message };
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ── Main Error Resolver ──────────────────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
export class VKErrorResolver {
|
|
387
|
+
constructor(repoPath, vkBaseUrl, options = {}) {
|
|
388
|
+
this.repoPath = repoPath;
|
|
389
|
+
this.vkBaseUrl = vkBaseUrl;
|
|
390
|
+
this.stateManager = new ResolutionState(CONFIG.stateFile);
|
|
391
|
+
this.enabled = options.enabled ?? true;
|
|
392
|
+
this.onResolve = options.onResolve || null;
|
|
393
|
+
|
|
394
|
+
this.resolvers = {
|
|
395
|
+
"uncommitted-changes": new UncommittedChangesResolver(
|
|
396
|
+
repoPath,
|
|
397
|
+
this.stateManager,
|
|
398
|
+
),
|
|
399
|
+
"push-failure": new PushFailureResolver(repoPath, this.stateManager),
|
|
400
|
+
"ci-retrigger": new CIRetriggerResolver(repoPath, this.stateManager),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async handleLogLine(line) {
|
|
405
|
+
if (!this.enabled) return;
|
|
406
|
+
|
|
407
|
+
for (const pattern of ERROR_PATTERNS) {
|
|
408
|
+
const match = line.match(pattern.pattern);
|
|
409
|
+
if (!match) continue;
|
|
410
|
+
|
|
411
|
+
const context = pattern.extract(match, line);
|
|
412
|
+
const signature = this.stateManager.getSignature(pattern.name, context);
|
|
413
|
+
|
|
414
|
+
// Check if can attempt resolution
|
|
415
|
+
const canAttempt = this.stateManager.canAttempt(signature);
|
|
416
|
+
if (!canAttempt.allowed) {
|
|
417
|
+
console.log(
|
|
418
|
+
`[vk-error-resolver] Skipping ${pattern.name} on ${context.branch || context.prNumber}: ${canAttempt.reason}`,
|
|
419
|
+
);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Verify task completed successfully
|
|
424
|
+
if (
|
|
425
|
+
context.branch &&
|
|
426
|
+
!(await isTaskSuccessful(context.branch, this.vkBaseUrl))
|
|
427
|
+
) {
|
|
428
|
+
console.log(
|
|
429
|
+
`[vk-error-resolver] Skipping ${pattern.name}: task not successful`,
|
|
430
|
+
);
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Attempt resolution
|
|
435
|
+
console.log(
|
|
436
|
+
`[vk-error-resolver] Attempting resolution for ${pattern.name}`,
|
|
437
|
+
);
|
|
438
|
+
this.stateManager.recordAttempt(signature);
|
|
439
|
+
|
|
440
|
+
const resolver = this.resolvers[pattern.name];
|
|
441
|
+
const result = await resolver.resolve(context);
|
|
442
|
+
|
|
443
|
+
if (this.onResolve) {
|
|
444
|
+
this.onResolve({
|
|
445
|
+
errorType: pattern.name,
|
|
446
|
+
context,
|
|
447
|
+
result,
|
|
448
|
+
signature,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (result.escalate) {
|
|
453
|
+
console.warn(
|
|
454
|
+
`[vk-error-resolver] Escalation required for ${pattern.name}`,
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
getStats() {
|
|
461
|
+
return {
|
|
462
|
+
attempts: Object.keys(this.stateManager.state.attempts).length,
|
|
463
|
+
activeCooldowns: Object.keys(this.stateManager.state.cooldowns).filter(
|
|
464
|
+
(sig) => new Date() < new Date(this.stateManager.state.cooldowns[sig]),
|
|
465
|
+
).length,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export default VKErrorResolver;
|