@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.ps1
ADDED
|
@@ -0,0 +1,1365 @@
|
|
|
1
|
+
#!/usr/bin/env pwsh
|
|
2
|
+
<#
|
|
3
|
+
.SYNOPSIS
|
|
4
|
+
VirtEngine Kanban CLI — wraps the vibe-kanban HTTP API for task + attempt management.
|
|
5
|
+
|
|
6
|
+
.DESCRIPTION
|
|
7
|
+
Provides commands to list tasks, submit task attempts, check attempt status,
|
|
8
|
+
rebase attempts, merge PRs, and mark tasks as complete. Designed to be both
|
|
9
|
+
a standalone CLI and a dot-sourceable library for ve-orchestrator.ps1.
|
|
10
|
+
|
|
11
|
+
.EXAMPLE
|
|
12
|
+
# List todo tasks
|
|
13
|
+
./ve-kanban.ps1 list --status todo
|
|
14
|
+
|
|
15
|
+
# Submit the next N tasks as attempts
|
|
16
|
+
./ve-kanban.ps1 submit-next --count 2
|
|
17
|
+
|
|
18
|
+
# Show active attempts
|
|
19
|
+
./ve-kanban.ps1 status
|
|
20
|
+
|
|
21
|
+
# Merge a completed PR
|
|
22
|
+
./ve-kanban.ps1 merge --branch ve/abc1-feat-portal
|
|
23
|
+
|
|
24
|
+
# Run orchestration loop
|
|
25
|
+
./ve-kanban.ps1 orchestrate --parallel 2
|
|
26
|
+
#>
|
|
27
|
+
|
|
28
|
+
# ─── Configuration ────────────────────────────────────────────────────────────
|
|
29
|
+
$script:VK_BASE_URL = $env:VK_ENDPOINT_URL ?? $env:VK_BASE_URL ?? "http://127.0.0.1:54089"
|
|
30
|
+
$script:VK_PROJECT_NAME = $env:VK_PROJECT_NAME ?? "virtengine"
|
|
31
|
+
$script:VK_PROJECT_ID = $env:VK_PROJECT_ID ?? "" # Auto-detected if empty
|
|
32
|
+
$script:VK_REPO_ID = $env:VK_REPO_ID ?? "" # Auto-detected if empty
|
|
33
|
+
$script:GH_OWNER = $env:GH_OWNER ?? "virtengine"
|
|
34
|
+
$script:GH_REPO = $env:GH_REPO ?? "virtengine"
|
|
35
|
+
$script:VK_TARGET_BRANCH = $env:VK_TARGET_BRANCH ?? "origin/main"
|
|
36
|
+
$script:VK_INITIALIZED = $false
|
|
37
|
+
$script:VK_LAST_GH_ERROR = $null
|
|
38
|
+
$script:VK_LAST_GH_ERROR_AT = $null
|
|
39
|
+
$script:VK_CLI_RAW_LINE = $null
|
|
40
|
+
|
|
41
|
+
# Executor profiles (used for 50/50 cycling between Codex and Copilot)
|
|
42
|
+
$script:VK_EXECUTORS = @(
|
|
43
|
+
@{ executor = "CODEX"; variant = "DEFAULT" }
|
|
44
|
+
@{ executor = "COPILOT"; variant = "CLAUDE_OPUS_4_6" }
|
|
45
|
+
)
|
|
46
|
+
$script:VK_EXECUTOR_INDEX = (Get-Random -Minimum 0 -Maximum $script:VK_EXECUTORS.Count) # Random start for session diversity
|
|
47
|
+
|
|
48
|
+
# ─── HTTP Helpers ─────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
function Invoke-VKApi {
|
|
51
|
+
<#
|
|
52
|
+
.SYNOPSIS Invoke the vibe-kanban REST API.
|
|
53
|
+
#>
|
|
54
|
+
[CmdletBinding()]
|
|
55
|
+
param(
|
|
56
|
+
[Parameter(Mandatory)][string]$Path,
|
|
57
|
+
[string]$Method = "GET",
|
|
58
|
+
[object]$Body
|
|
59
|
+
)
|
|
60
|
+
$uri = "$script:VK_BASE_URL$Path"
|
|
61
|
+
$params = @{ Uri = $uri; Method = $Method; ContentType = "application/json"; UseBasicParsing = $true }
|
|
62
|
+
if ($Body) { $params.Body = ($Body | ConvertTo-Json -Depth 10 -Compress) }
|
|
63
|
+
try {
|
|
64
|
+
$raw = Invoke-WebRequest @params -ErrorAction Stop
|
|
65
|
+
$resp = $raw.Content | ConvertFrom-Json -Depth 20
|
|
66
|
+
if ($resp.success -eq $false) {
|
|
67
|
+
Write-Warning "API error on $Path : $($resp.message)"
|
|
68
|
+
return $null
|
|
69
|
+
}
|
|
70
|
+
return $resp.data ?? $resp
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
Write-Error "HTTP $Method $uri failed: $_"
|
|
74
|
+
return $null
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# ─── Copilot Cloud Guard ─────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
function Test-CopilotCloudDisabled {
|
|
81
|
+
$flag = $env:COPILOT_CLOUD_DISABLED
|
|
82
|
+
if ($flag -and $flag.ToString().ToLower() -in @("1", "true", "yes")) {
|
|
83
|
+
return $true
|
|
84
|
+
}
|
|
85
|
+
$untilRaw = $env:COPILOT_CLOUD_DISABLED_UNTIL
|
|
86
|
+
if (-not $untilRaw) { return $false }
|
|
87
|
+
try {
|
|
88
|
+
$until = [datetimeoffset]::Parse($untilRaw).ToLocalTime().DateTime
|
|
89
|
+
if ((Get-Date) -lt $until) { return $true }
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return $false
|
|
93
|
+
}
|
|
94
|
+
return $false
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# ─── Auto-Detection & Initialization ─────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
function Initialize-VKConfig {
|
|
100
|
+
<#
|
|
101
|
+
.SYNOPSIS Auto-detect project ID and repo ID from vibe-kanban by project name.
|
|
102
|
+
Works for any user with their own vibe-kanban setup.
|
|
103
|
+
#>
|
|
104
|
+
if ($script:VK_INITIALIZED) { return $true }
|
|
105
|
+
|
|
106
|
+
# If project ID already set (via env), skip auto-detection for project
|
|
107
|
+
if ($script:VK_PROJECT_ID) {
|
|
108
|
+
Write-Verbose "Using configured project ID: $script:VK_PROJECT_ID"
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
Write-Host " Auto-detecting project '$script:VK_PROJECT_NAME'..." -ForegroundColor DarkGray
|
|
112
|
+
$projects = Invoke-VKApi -Path "/api/projects"
|
|
113
|
+
if (-not $projects) {
|
|
114
|
+
Write-Error "Cannot connect to vibe-kanban at $script:VK_BASE_URL"
|
|
115
|
+
return $false
|
|
116
|
+
}
|
|
117
|
+
$projectList = if ($projects -is [System.Array]) { $projects } elseif ($projects.projects) { $projects.projects } else { @($projects) }
|
|
118
|
+
|
|
119
|
+
# Find project by name (case-insensitive)
|
|
120
|
+
$match = $projectList | Where-Object {
|
|
121
|
+
$_.name -ieq $script:VK_PROJECT_NAME -or
|
|
122
|
+
$_.display_name -ieq $script:VK_PROJECT_NAME -or
|
|
123
|
+
$_.title -ieq $script:VK_PROJECT_NAME
|
|
124
|
+
} | Select-Object -First 1
|
|
125
|
+
|
|
126
|
+
if (-not $match) {
|
|
127
|
+
Write-Error "No project named '$script:VK_PROJECT_NAME' found. Available: $($projectList | ForEach-Object { $_.name ?? $_.display_name ?? $_.title } | Join-String -Separator ', ')"
|
|
128
|
+
return $false
|
|
129
|
+
}
|
|
130
|
+
$script:VK_PROJECT_ID = $match.id
|
|
131
|
+
Write-Host " ✓ Project: $($match.name ?? $match.display_name ?? $match.title) ($($script:VK_PROJECT_ID.Substring(0,8))...)" -ForegroundColor Green
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# Auto-detect repo ID if not set
|
|
135
|
+
if (-not $script:VK_REPO_ID) {
|
|
136
|
+
Write-Host " Auto-detecting repository..." -ForegroundColor DarkGray
|
|
137
|
+
$repos = Invoke-VKApi -Path "/api/repos?project_id=$script:VK_PROJECT_ID"
|
|
138
|
+
if (-not $repos) {
|
|
139
|
+
# Try alternate endpoint
|
|
140
|
+
$repos = Invoke-VKApi -Path "/api/projects/$script:VK_PROJECT_ID/repos"
|
|
141
|
+
}
|
|
142
|
+
$repoList = if ($repos -is [System.Array]) { $repos } elseif ($repos.repos) { $repos.repos } else { @($repos) }
|
|
143
|
+
|
|
144
|
+
if ($repoList -and @($repoList).Count -gt 0) {
|
|
145
|
+
# Prefer repo matching GH_REPO name, otherwise take first
|
|
146
|
+
$repoMatch = $repoList | Where-Object { $_.name -ieq $script:GH_REPO } | Select-Object -First 1
|
|
147
|
+
if (-not $repoMatch) { $repoMatch = $repoList | Select-Object -First 1 }
|
|
148
|
+
$script:VK_REPO_ID = $repoMatch.id
|
|
149
|
+
Write-Host " ✓ Repo: $($repoMatch.name ?? 'default') ($($script:VK_REPO_ID.Substring(0,8))...)" -ForegroundColor Green
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
Write-Warning "No repos found for project. Repo ID must be set via VK_REPO_ID env var."
|
|
153
|
+
return $false
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
$script:VK_INITIALIZED = $true
|
|
158
|
+
return $true
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# ─── Executor Cycling ─────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
function Get-NextExecutorProfile {
|
|
164
|
+
<#
|
|
165
|
+
.SYNOPSIS Get the next executor profile in the 50/50 Codex/Copilot rotation.
|
|
166
|
+
#>
|
|
167
|
+
$max = $script:VK_EXECUTORS.Count
|
|
168
|
+
if ($max -le 1) {
|
|
169
|
+
return $script:VK_EXECUTORS[0]
|
|
170
|
+
}
|
|
171
|
+
for ($i = 0; $i -lt $max; $i++) {
|
|
172
|
+
$profile = $script:VK_EXECUTORS[$script:VK_EXECUTOR_INDEX]
|
|
173
|
+
$script:VK_EXECUTOR_INDEX = ($script:VK_EXECUTOR_INDEX + 1) % $script:VK_EXECUTORS.Count
|
|
174
|
+
if ((Test-CopilotCloudDisabled) -and $profile.executor -eq "COPILOT") {
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
return $profile
|
|
178
|
+
}
|
|
179
|
+
return $script:VK_EXECUTORS[0]
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function Get-CurrentExecutorProfile {
|
|
183
|
+
<#
|
|
184
|
+
.SYNOPSIS Peek at the next executor profile without advancing the cycle.
|
|
185
|
+
#>
|
|
186
|
+
return $script:VK_EXECUTORS[$script:VK_EXECUTOR_INDEX]
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# ─── Task Functions ───────────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
function Get-VKTasks {
|
|
192
|
+
<#
|
|
193
|
+
.SYNOPSIS List tasks with optional status filter (client-side filtering).
|
|
194
|
+
#>
|
|
195
|
+
[CmdletBinding()]
|
|
196
|
+
param(
|
|
197
|
+
[ValidateSet("todo", "inprogress", "inreview", "done", "cancelled")]
|
|
198
|
+
[string]$Status,
|
|
199
|
+
[int]$Limit = 500
|
|
200
|
+
)
|
|
201
|
+
if (-not (Initialize-VKConfig)) { return @() }
|
|
202
|
+
$result = Invoke-VKApi -Path "/api/tasks?project_id=$script:VK_PROJECT_ID"
|
|
203
|
+
if (-not $result) { return @() }
|
|
204
|
+
# Result is either the tasks array directly, or an object with .tasks
|
|
205
|
+
$tasks = if ($result -is [System.Array]) { $result } elseif ($result.tasks) { $result.tasks } else { @($result) }
|
|
206
|
+
# API doesn't filter server-side, so filter client-side
|
|
207
|
+
if ($Status -and $tasks) {
|
|
208
|
+
$tasks = @($tasks | Where-Object { $_.status -eq $Status })
|
|
209
|
+
}
|
|
210
|
+
return $tasks
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function Get-VKTask {
|
|
214
|
+
<#
|
|
215
|
+
.SYNOPSIS Get a single task by ID.
|
|
216
|
+
#>
|
|
217
|
+
[CmdletBinding()]
|
|
218
|
+
param([Parameter(Mandatory)][string]$TaskId)
|
|
219
|
+
return Invoke-VKApi -Path "/api/tasks/$TaskId"
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function Create-VKTask {
|
|
223
|
+
<#
|
|
224
|
+
.SYNOPSIS Create a new task in vibe-kanban.
|
|
225
|
+
#>
|
|
226
|
+
[CmdletBinding()]
|
|
227
|
+
param(
|
|
228
|
+
[Parameter(Mandatory)][string]$Title,
|
|
229
|
+
[Parameter(Mandatory)][string]$Description,
|
|
230
|
+
[ValidateSet("todo", "inprogress", "inreview", "done", "cancelled")]
|
|
231
|
+
[string]$Status = "todo"
|
|
232
|
+
)
|
|
233
|
+
if (-not (Initialize-VKConfig)) { return $null }
|
|
234
|
+
$body = @{
|
|
235
|
+
title = $Title
|
|
236
|
+
description = $Description
|
|
237
|
+
status = $Status
|
|
238
|
+
project_id = $script:VK_PROJECT_ID
|
|
239
|
+
}
|
|
240
|
+
return Invoke-VKApi -Path "/api/tasks" -Method "POST" -Body $body
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function Update-VKTaskStatus {
|
|
244
|
+
<#
|
|
245
|
+
.SYNOPSIS Update a task's status.
|
|
246
|
+
#>
|
|
247
|
+
[CmdletBinding()]
|
|
248
|
+
param(
|
|
249
|
+
[Parameter(Mandatory)][string]$TaskId,
|
|
250
|
+
[Parameter(Mandatory)]
|
|
251
|
+
[ValidateSet("todo", "inprogress", "inreview", "done", "cancelled")]
|
|
252
|
+
[string]$Status
|
|
253
|
+
)
|
|
254
|
+
return Invoke-VKApi -Path "/api/tasks/$TaskId" -Method "PUT" -Body @{ status = $Status }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function Get-VKNextTodoTasks {
|
|
258
|
+
<#
|
|
259
|
+
.SYNOPSIS Get the next N tasks in 'todo' status, ordered by creation date (earliest first = highest priority).
|
|
260
|
+
#>
|
|
261
|
+
[CmdletBinding()]
|
|
262
|
+
param([int]$Count = 1)
|
|
263
|
+
$tasks = Get-VKTasks -Status "todo" -Limit $Count
|
|
264
|
+
if (-not $tasks) { return @() }
|
|
265
|
+
# Return as array
|
|
266
|
+
$arr = @($tasks)
|
|
267
|
+
return $arr | Select-Object -First $Count
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
# ─── Attempt Functions ────────────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
function Get-VKAttempts {
|
|
273
|
+
<#
|
|
274
|
+
.SYNOPSIS List all task attempts.
|
|
275
|
+
#>
|
|
276
|
+
[CmdletBinding()]
|
|
277
|
+
param([switch]$ActiveOnly)
|
|
278
|
+
$result = Invoke-VKApi -Path "/api/task-attempts"
|
|
279
|
+
if (-not $result) { return @() }
|
|
280
|
+
$attempts = if ($result -is [System.Array]) { @($result) } else { @($result) }
|
|
281
|
+
if ($ActiveOnly) {
|
|
282
|
+
$attempts = @($attempts | Where-Object { -not $_.archived })
|
|
283
|
+
}
|
|
284
|
+
return $attempts
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function Get-VKAttemptSummaries {
|
|
288
|
+
<#
|
|
289
|
+
.SYNOPSIS Get attempt summaries for workspace status (idle/running/failed).
|
|
290
|
+
#>
|
|
291
|
+
[CmdletBinding()]
|
|
292
|
+
param([bool]$Archived = $false)
|
|
293
|
+
$body = @{ archived = $Archived }
|
|
294
|
+
$result = Invoke-VKApi -Path "/api/task-attempts/summary" -Method "POST" -Body $body
|
|
295
|
+
if (-not $result) { return @() }
|
|
296
|
+
$summaries = if ($result.summaries) { $result.summaries } else { $result }
|
|
297
|
+
return @($summaries)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function Get-VKArchivedAttempts {
|
|
301
|
+
<#
|
|
302
|
+
.SYNOPSIS List archived task attempts.
|
|
303
|
+
#>
|
|
304
|
+
[CmdletBinding()]
|
|
305
|
+
$attempts = Get-VKAttempts
|
|
306
|
+
if (-not $attempts) { return @() }
|
|
307
|
+
return @($attempts | Where-Object { $_.archived })
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function Submit-VKTaskAttempt {
|
|
311
|
+
<#
|
|
312
|
+
.SYNOPSIS Submit a task as a new attempt (creates worktree + starts agent).
|
|
313
|
+
Uses the next executor in the Codex/Copilot rotation cycle.
|
|
314
|
+
#>
|
|
315
|
+
[CmdletBinding()]
|
|
316
|
+
param(
|
|
317
|
+
[Parameter(Mandatory)][string]$TaskId,
|
|
318
|
+
[string]$TargetBranch = $script:VK_TARGET_BRANCH,
|
|
319
|
+
[hashtable]$ExecutorOverride
|
|
320
|
+
)
|
|
321
|
+
if (-not (Initialize-VKConfig)) { return $null }
|
|
322
|
+
|
|
323
|
+
# Use override if provided, otherwise cycle to next executor
|
|
324
|
+
$execProfile = if ($ExecutorOverride) { $ExecutorOverride } else { Get-NextExecutorProfile }
|
|
325
|
+
|
|
326
|
+
$body = @{
|
|
327
|
+
task_id = $TaskId
|
|
328
|
+
repos = @(
|
|
329
|
+
@{
|
|
330
|
+
repo_id = $script:VK_REPO_ID
|
|
331
|
+
target_branch = $TargetBranch
|
|
332
|
+
}
|
|
333
|
+
)
|
|
334
|
+
executor_profile_id = @{
|
|
335
|
+
executor = $execProfile.executor
|
|
336
|
+
variant = $execProfile.variant
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
Write-Host " Submitting attempt for task $TaskId ($($execProfile.executor)/$($execProfile.variant)) ..." -ForegroundColor Cyan
|
|
340
|
+
$result = Invoke-VKApi -Path "/api/task-attempts" -Method "POST" -Body $body
|
|
341
|
+
if ($result) {
|
|
342
|
+
Write-Host " ✓ Attempt created: $($result.id) → branch $($result.branch)" -ForegroundColor Green
|
|
343
|
+
}
|
|
344
|
+
return $result
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function Invoke-VKRebase {
|
|
348
|
+
<#
|
|
349
|
+
.SYNOPSIS Rebase an attempt's branch onto the latest target.
|
|
350
|
+
#>
|
|
351
|
+
[CmdletBinding()]
|
|
352
|
+
param(
|
|
353
|
+
[Parameter(Mandatory)][string]$AttemptId,
|
|
354
|
+
[string]$BaseBranch = $script:VK_TARGET_BRANCH
|
|
355
|
+
)
|
|
356
|
+
$body = @{
|
|
357
|
+
repo_id = $script:VK_REPO_ID
|
|
358
|
+
old_base_branch = $BaseBranch
|
|
359
|
+
new_base_branch = $BaseBranch
|
|
360
|
+
}
|
|
361
|
+
return Invoke-VKApi -Path "/api/task-attempts/$AttemptId/rebase" -Method "POST" -Body $body
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function Archive-VKAttempt {
|
|
365
|
+
<#
|
|
366
|
+
.SYNOPSIS Archive a task attempt so it no longer counts as active.
|
|
367
|
+
#>
|
|
368
|
+
[CmdletBinding()]
|
|
369
|
+
param([Parameter(Mandatory)][string]$AttemptId)
|
|
370
|
+
$body = @{ archived = $true }
|
|
371
|
+
return Invoke-VKApi -Path "/api/task-attempts/$AttemptId" -Method "PUT" -Body $body
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function Unarchive-VKAttempt {
|
|
375
|
+
<#
|
|
376
|
+
.SYNOPSIS Unarchive a task attempt so it counts as active again.
|
|
377
|
+
#>
|
|
378
|
+
[CmdletBinding()]
|
|
379
|
+
param([Parameter(Mandatory)][string]$AttemptId)
|
|
380
|
+
$body = @{ archived = $false }
|
|
381
|
+
return Invoke-VKApi -Path "/api/task-attempts/$AttemptId" -Method "PUT" -Body $body
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
# ─── GitHub PR Functions ─────────────────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
function Set-VKLastGithubError {
|
|
387
|
+
param(
|
|
388
|
+
[string]$Type,
|
|
389
|
+
[string]$Message
|
|
390
|
+
)
|
|
391
|
+
$script:VK_LAST_GH_ERROR = @{ type = $Type; message = $Message }
|
|
392
|
+
$script:VK_LAST_GH_ERROR_AT = Get-Date
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function Clear-VKLastGithubError {
|
|
396
|
+
$script:VK_LAST_GH_ERROR = $null
|
|
397
|
+
$script:VK_LAST_GH_ERROR_AT = $null
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function Get-VKLastGithubError {
|
|
401
|
+
return $script:VK_LAST_GH_ERROR
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function Invoke-VKGithub {
|
|
405
|
+
<#
|
|
406
|
+
.SYNOPSIS Invoke gh CLI with rate-limit detection.
|
|
407
|
+
#>
|
|
408
|
+
[CmdletBinding()]
|
|
409
|
+
param(
|
|
410
|
+
[Parameter(Mandatory)][string[]]$Args
|
|
411
|
+
)
|
|
412
|
+
$output = & gh @Args 2>&1
|
|
413
|
+
$exitCode = $LASTEXITCODE
|
|
414
|
+
if ($exitCode -ne 0) {
|
|
415
|
+
if ($output -match "rate limit|API rate limit exceeded|secondary rate limit|abuse detection") {
|
|
416
|
+
Set-VKLastGithubError -Type "rate_limit" -Message $output
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
Set-VKLastGithubError -Type "error" -Message $output
|
|
420
|
+
}
|
|
421
|
+
return $null
|
|
422
|
+
}
|
|
423
|
+
Clear-VKLastGithubError
|
|
424
|
+
return $output
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function Get-PRForBranch {
|
|
428
|
+
<#
|
|
429
|
+
.SYNOPSIS Find an open or merged PR for a given branch.
|
|
430
|
+
#>
|
|
431
|
+
[CmdletBinding()]
|
|
432
|
+
param([Parameter(Mandatory)][string]$Branch)
|
|
433
|
+
$prJson = Invoke-VKGithub -Args @(
|
|
434
|
+
"pr", "list", "--head", $Branch, "--repo", "$script:GH_OWNER/$script:GH_REPO",
|
|
435
|
+
"--json", "number,state,title,mergeable,statusCheckRollup,mergedAt,createdAt,url",
|
|
436
|
+
"--limit", "1"
|
|
437
|
+
)
|
|
438
|
+
if (-not $prJson -or $prJson -eq "[]") {
|
|
439
|
+
# Also check merged/closed
|
|
440
|
+
$prJson = Invoke-VKGithub -Args @(
|
|
441
|
+
"pr", "list", "--head", $Branch, "--repo", "$script:GH_OWNER/$script:GH_REPO",
|
|
442
|
+
"--state", "merged", "--json", "number,state,title,mergedAt,createdAt,url",
|
|
443
|
+
"--limit", "1"
|
|
444
|
+
)
|
|
445
|
+
}
|
|
446
|
+
if (-not $prJson -or $prJson -eq "[]") { return $null }
|
|
447
|
+
return ($prJson | ConvertFrom-Json) | Select-Object -First 1
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function Get-PRCheckStatus {
|
|
451
|
+
<#
|
|
452
|
+
.SYNOPSIS Get CI check status for a PR.
|
|
453
|
+
.OUTPUTS "passing", "failing", "pending", or "unknown"
|
|
454
|
+
#>
|
|
455
|
+
[CmdletBinding()]
|
|
456
|
+
param([Parameter(Mandatory)][int]$PRNumber)
|
|
457
|
+
$checksJson = Invoke-VKGithub -Args @(
|
|
458
|
+
"pr", "checks", $PRNumber.ToString(), "--repo", "$script:GH_OWNER/$script:GH_REPO",
|
|
459
|
+
"--json", "name,state"
|
|
460
|
+
)
|
|
461
|
+
if (-not $checksJson) { return "unknown" }
|
|
462
|
+
$checks = $checksJson | ConvertFrom-Json
|
|
463
|
+
if (-not $checks -or $checks.Count -eq 0) { return "unknown" }
|
|
464
|
+
|
|
465
|
+
$failing = $checks | Where-Object { $_.state -in @("FAILURE", "ERROR") }
|
|
466
|
+
$pending = $checks | Where-Object { $_.state -in @("PENDING", "IN_PROGRESS", "QUEUED") }
|
|
467
|
+
|
|
468
|
+
if ($failing.Count -gt 0) { return "failing" }
|
|
469
|
+
if ($pending.Count -gt 0) { return "pending" }
|
|
470
|
+
return "passing"
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function Get-PRLatestCheckTimestamp {
|
|
474
|
+
<#
|
|
475
|
+
.SYNOPSIS Get the most recent check started/completed time for a PR.
|
|
476
|
+
#>
|
|
477
|
+
[CmdletBinding()]
|
|
478
|
+
param([Parameter(Mandatory)][int]$PRNumber)
|
|
479
|
+
$checksJson = Invoke-VKGithub -Args @(
|
|
480
|
+
"pr", "checks", $PRNumber.ToString(), "--repo", "$script:GH_OWNER/$script:GH_REPO",
|
|
481
|
+
"--json", "name,state,startedAt,completedAt"
|
|
482
|
+
)
|
|
483
|
+
if (-not $checksJson) { return $null }
|
|
484
|
+
$checks = $checksJson | ConvertFrom-Json
|
|
485
|
+
if (-not $checks -or $checks.Count -eq 0) { return $null }
|
|
486
|
+
|
|
487
|
+
$timestamps = @()
|
|
488
|
+
foreach ($check in $checks) {
|
|
489
|
+
if ($check.startedAt) { $timestamps += [datetime]$check.startedAt }
|
|
490
|
+
if ($check.completedAt) { $timestamps += [datetime]$check.completedAt }
|
|
491
|
+
}
|
|
492
|
+
if ($timestamps.Count -eq 0) { return $null }
|
|
493
|
+
return ($timestamps | Sort-Object -Descending | Select-Object -First 1)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function Get-OpenPullRequests {
|
|
497
|
+
<#
|
|
498
|
+
.SYNOPSIS List open PRs with metadata.
|
|
499
|
+
#>
|
|
500
|
+
[CmdletBinding()]
|
|
501
|
+
param([int]$Limit = 100)
|
|
502
|
+
$prJson = Invoke-VKGithub -Args @(
|
|
503
|
+
"pr", "list", "--repo", "$script:GH_OWNER/$script:GH_REPO",
|
|
504
|
+
"--state", "open", "--limit", $Limit.ToString(),
|
|
505
|
+
"--json", "number,title,author,isDraft,createdAt,headRefName,baseRefName,mergeStateStatus,url,body"
|
|
506
|
+
)
|
|
507
|
+
if (-not $prJson -or $prJson -eq "[]") { return @() }
|
|
508
|
+
return $prJson | ConvertFrom-Json
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function Test-IsCopilotAuthor {
|
|
512
|
+
<#
|
|
513
|
+
.SYNOPSIS Determine whether a PR author is Copilot.
|
|
514
|
+
#>
|
|
515
|
+
[CmdletBinding()]
|
|
516
|
+
param([Parameter(Mandatory)][object]$Author)
|
|
517
|
+
$login = $Author.login
|
|
518
|
+
if (-not $login) { return $false }
|
|
519
|
+
return ($login -match "copilot")
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function Get-PRDetails {
|
|
523
|
+
<#
|
|
524
|
+
.SYNOPSIS Get mergeability details for a PR.
|
|
525
|
+
#>
|
|
526
|
+
[CmdletBinding()]
|
|
527
|
+
param([Parameter(Mandatory)][int]$PRNumber)
|
|
528
|
+
$prJson = Invoke-VKGithub -Args @(
|
|
529
|
+
"pr", "view", $PRNumber.ToString(), "--repo", "$script:GH_OWNER/$script:GH_REPO",
|
|
530
|
+
"--json", "number,state,mergeable,mergeStateStatus,isDraft,reviewDecision,url,headRefName,baseRefName,title,body"
|
|
531
|
+
)
|
|
532
|
+
if (-not $prJson) { return $null }
|
|
533
|
+
return $prJson | ConvertFrom-Json
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function Get-PRChecksDetail {
|
|
537
|
+
<#
|
|
538
|
+
.SYNOPSIS Get detailed check info for a PR.
|
|
539
|
+
#>
|
|
540
|
+
[CmdletBinding()]
|
|
541
|
+
param([Parameter(Mandatory)][int]$PRNumber)
|
|
542
|
+
$checksJson = Invoke-VKGithub -Args @(
|
|
543
|
+
"pr", "checks", $PRNumber.ToString(), "--repo", "$script:GH_OWNER/$script:GH_REPO",
|
|
544
|
+
"--json", "name,state,link,startedAt,completedAt"
|
|
545
|
+
)
|
|
546
|
+
if (-not $checksJson) { return @() }
|
|
547
|
+
return $checksJson | ConvertFrom-Json
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function Get-RequiredChecksForBranch {
|
|
551
|
+
<#
|
|
552
|
+
.SYNOPSIS Get required status check names for a branch.
|
|
553
|
+
#>
|
|
554
|
+
[CmdletBinding()]
|
|
555
|
+
param([Parameter(Mandatory)][string]$Branch)
|
|
556
|
+
$result = Invoke-VKGithub -Args @(
|
|
557
|
+
"api",
|
|
558
|
+
"repos/$script:GH_OWNER/$script:GH_REPO/branches/$Branch/protection/required_status_checks"
|
|
559
|
+
)
|
|
560
|
+
if (-not $result) { return @() }
|
|
561
|
+
$payload = $result | ConvertFrom-Json
|
|
562
|
+
if (-not $payload) { return @() }
|
|
563
|
+
|
|
564
|
+
$names = @()
|
|
565
|
+
if ($payload.contexts) {
|
|
566
|
+
$names += @($payload.contexts | Where-Object { $_ })
|
|
567
|
+
}
|
|
568
|
+
if ($payload.checks) {
|
|
569
|
+
$names += @($payload.checks | ForEach-Object {
|
|
570
|
+
if ($_.context) { $_.context } else { $_.name }
|
|
571
|
+
} | Where-Object { $_ })
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return @($names | Sort-Object -Unique)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function Get-PRRequiredCheckStatus {
|
|
578
|
+
<#
|
|
579
|
+
.SYNOPSIS Evaluate only required checks for a PR.
|
|
580
|
+
.OUTPUTS "passing", "failing", "pending", or "unknown"
|
|
581
|
+
#>
|
|
582
|
+
[CmdletBinding()]
|
|
583
|
+
param(
|
|
584
|
+
[Parameter(Mandatory)][int]$PRNumber,
|
|
585
|
+
[Parameter(Mandatory)][string]$BaseBranch
|
|
586
|
+
)
|
|
587
|
+
$required = Get-RequiredChecksForBranch -Branch $BaseBranch
|
|
588
|
+
if (-not $required -or @($required).Count -eq 0) {
|
|
589
|
+
return "passing"
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
$checks = Get-PRChecksDetail -PRNumber $PRNumber
|
|
593
|
+
if (-not $checks) { return "unknown" }
|
|
594
|
+
|
|
595
|
+
$requiredLower = $required | ForEach-Object { $_.ToLowerInvariant() }
|
|
596
|
+
$checksByName = @{}
|
|
597
|
+
foreach ($check in $checks) {
|
|
598
|
+
if (-not $check.name) { continue }
|
|
599
|
+
$checksByName[$check.name.ToLowerInvariant()] = $check.state
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
$hasPending = $false
|
|
603
|
+
foreach ($name in $requiredLower) {
|
|
604
|
+
if (-not $checksByName.ContainsKey($name)) {
|
|
605
|
+
$hasPending = $true
|
|
606
|
+
continue
|
|
607
|
+
}
|
|
608
|
+
$state = $checksByName[$name]
|
|
609
|
+
if ($state -in @("FAILURE", "ERROR")) { return "failing" }
|
|
610
|
+
if ($state -in @("PENDING", "IN_PROGRESS", "QUEUED")) { $hasPending = $true }
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if ($hasPending) { return "pending" }
|
|
614
|
+
return "passing"
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function Create-GithubIssue {
|
|
618
|
+
<#
|
|
619
|
+
.SYNOPSIS Create a GitHub issue.
|
|
620
|
+
#>
|
|
621
|
+
[CmdletBinding()]
|
|
622
|
+
param(
|
|
623
|
+
[Parameter(Mandatory)][string]$Title,
|
|
624
|
+
[Parameter(Mandatory)][string]$Body
|
|
625
|
+
)
|
|
626
|
+
$result = Invoke-VKGithub -Args @(
|
|
627
|
+
"api",
|
|
628
|
+
"repos/$script:GH_OWNER/$script:GH_REPO/issues",
|
|
629
|
+
"-f", "title=$Title",
|
|
630
|
+
"-f", "body=$Body"
|
|
631
|
+
)
|
|
632
|
+
if (-not $result) { return $null }
|
|
633
|
+
return $result | ConvertFrom-Json
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function Assign-IssueToCopilot {
|
|
637
|
+
<#
|
|
638
|
+
.SYNOPSIS Assign a GitHub issue to Copilot.
|
|
639
|
+
#>
|
|
640
|
+
[CmdletBinding()]
|
|
641
|
+
param([Parameter(Mandatory)][int]$IssueNumber)
|
|
642
|
+
$result = Invoke-VKGithub -Args @(
|
|
643
|
+
"api",
|
|
644
|
+
"repos/$script:GH_OWNER/$script:GH_REPO/issues/$IssueNumber/assignees",
|
|
645
|
+
"-f", "assignees[]=copilot"
|
|
646
|
+
)
|
|
647
|
+
return ($null -ne $result)
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function Get-RecentMergedPRs {
|
|
651
|
+
<#
|
|
652
|
+
.SYNOPSIS List recent merged PRs.
|
|
653
|
+
#>
|
|
654
|
+
[CmdletBinding()]
|
|
655
|
+
param([int]$Limit = 15)
|
|
656
|
+
$prJson = Invoke-VKGithub -Args @(
|
|
657
|
+
"pr", "list", "--repo", "$script:GH_OWNER/$script:GH_REPO",
|
|
658
|
+
"--state", "merged", "--limit", $Limit.ToString(),
|
|
659
|
+
"--json", "number,title,url,mergedAt"
|
|
660
|
+
)
|
|
661
|
+
if (-not $prJson -or $prJson -eq "[]") { return @() }
|
|
662
|
+
return $prJson | ConvertFrom-Json
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function Get-FailingWorkflowRuns {
|
|
666
|
+
<#
|
|
667
|
+
.SYNOPSIS Get recent failing workflow runs on main.
|
|
668
|
+
#>
|
|
669
|
+
[CmdletBinding()]
|
|
670
|
+
param([int]$Limit = 8)
|
|
671
|
+
$result = Invoke-VKGithub -Args @(
|
|
672
|
+
"api",
|
|
673
|
+
"repos/$script:GH_OWNER/$script:GH_REPO/actions/runs",
|
|
674
|
+
"-f", "branch=main",
|
|
675
|
+
"-f", "status=completed",
|
|
676
|
+
"-f", "per_page=$Limit"
|
|
677
|
+
)
|
|
678
|
+
if (-not $result) { return @() }
|
|
679
|
+
$payload = $result | ConvertFrom-Json
|
|
680
|
+
$runs = $payload.workflow_runs
|
|
681
|
+
if (-not $runs) { return @() }
|
|
682
|
+
return $runs | Where-Object {
|
|
683
|
+
$_.conclusion -in @("failure", "cancelled", "timed_out", "action_required")
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function Format-PRCheckFailures {
|
|
688
|
+
<#
|
|
689
|
+
.SYNOPSIS Format failing checks into a short markdown list.
|
|
690
|
+
#>
|
|
691
|
+
[CmdletBinding()]
|
|
692
|
+
param([object[]]$Checks = @())
|
|
693
|
+
if (-not $Checks -or $Checks.Count -eq 0) { return "- No failing checks found" }
|
|
694
|
+
$failed = $Checks | Where-Object { $_.state -eq "FAILURE" -or $_.state -eq "ERROR" }
|
|
695
|
+
if (-not $failed -or $failed.Count -eq 0) { return "- No failing checks found" }
|
|
696
|
+
$lines = $failed | ForEach-Object {
|
|
697
|
+
$link = if ($_.link) { " ($($_.link))" } else { "" }
|
|
698
|
+
"- $($_.name): $($_.state)$link"
|
|
699
|
+
}
|
|
700
|
+
return ($lines -join "`n")
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function Add-PRComment {
|
|
704
|
+
<#
|
|
705
|
+
.SYNOPSIS Add a comment to a PR.
|
|
706
|
+
#>
|
|
707
|
+
[CmdletBinding()]
|
|
708
|
+
param(
|
|
709
|
+
[Parameter(Mandatory)][int]$PRNumber,
|
|
710
|
+
[Parameter(Mandatory)][string]$Body
|
|
711
|
+
)
|
|
712
|
+
$result = Invoke-VKGithub -Args @(
|
|
713
|
+
"pr", "comment", $PRNumber.ToString(), "--repo", "$script:GH_OWNER/$script:GH_REPO",
|
|
714
|
+
"--body", $Body
|
|
715
|
+
)
|
|
716
|
+
return ($null -ne $result)
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function Get-PRComments {
|
|
720
|
+
<#
|
|
721
|
+
.SYNOPSIS Fetch recent PR comments (issue comments).
|
|
722
|
+
#>
|
|
723
|
+
[CmdletBinding()]
|
|
724
|
+
param(
|
|
725
|
+
[Parameter(Mandatory)][int]$PRNumber,
|
|
726
|
+
[int]$Limit = 30
|
|
727
|
+
)
|
|
728
|
+
$result = Invoke-VKGithub -Args @(
|
|
729
|
+
"api",
|
|
730
|
+
"repos/$script:GH_OWNER/$script:GH_REPO/issues/$PRNumber/comments",
|
|
731
|
+
"-f", "per_page=$Limit"
|
|
732
|
+
)
|
|
733
|
+
if (-not $result) { return @() }
|
|
734
|
+
try {
|
|
735
|
+
return $result | ConvertFrom-Json
|
|
736
|
+
}
|
|
737
|
+
catch {
|
|
738
|
+
return @()
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function Test-CopilotRateLimitComment {
|
|
743
|
+
<#
|
|
744
|
+
.SYNOPSIS Detect Copilot rate limit notices OR "stopped work due to error" in PR comments.
|
|
745
|
+
#>
|
|
746
|
+
[CmdletBinding()]
|
|
747
|
+
param([Parameter(Mandatory)][int]$PRNumber)
|
|
748
|
+
$comments = Get-PRComments -PRNumber $PRNumber -Limit 30
|
|
749
|
+
if (-not $comments) { return $null }
|
|
750
|
+
$pattern = "copilot stopped work|rate limit|rate-limited|secondary rate limit|due to an error"
|
|
751
|
+
foreach ($comment in $comments) {
|
|
752
|
+
if (-not $comment.body) { continue }
|
|
753
|
+
$body = $comment.body.ToString()
|
|
754
|
+
if ($body -match $pattern -and $body -match "copilot") {
|
|
755
|
+
$isError = $body -match "due to an error|stopped work"
|
|
756
|
+
return @{
|
|
757
|
+
hit = $true
|
|
758
|
+
is_error = [bool]$isError
|
|
759
|
+
body = $body
|
|
760
|
+
created_at = $comment.created_at
|
|
761
|
+
author = $comment.user?.login
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
return $null
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function Test-PRHasCopilotComment {
|
|
769
|
+
<#
|
|
770
|
+
.SYNOPSIS Check if @copilot has already been mentioned in PR comments.
|
|
771
|
+
Returns $true if any comment body contains '@copilot' — meaning Copilot
|
|
772
|
+
was already triggered and should NOT be triggered again.
|
|
773
|
+
#>
|
|
774
|
+
[CmdletBinding()]
|
|
775
|
+
param([Parameter(Mandatory)][int]$PRNumber)
|
|
776
|
+
$comments = Get-PRComments -PRNumber $PRNumber -Limit 50
|
|
777
|
+
if (-not $comments) { return $false }
|
|
778
|
+
foreach ($comment in $comments) {
|
|
779
|
+
if (-not $comment.body) { continue }
|
|
780
|
+
if ($comment.body.ToString() -match "@copilot") {
|
|
781
|
+
return $true
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return $false
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function Test-CopilotPRClosed {
|
|
788
|
+
<#
|
|
789
|
+
.SYNOPSIS Check if a Copilot-authored PR for this PR number was ever closed.
|
|
790
|
+
If so, we should never trigger @copilot for this PR again.
|
|
791
|
+
#>
|
|
792
|
+
[CmdletBinding()]
|
|
793
|
+
param([Parameter(Mandatory)][int]$PRNumber)
|
|
794
|
+
$search = "$PRNumber in:title,body"
|
|
795
|
+
$prJson = Invoke-VKGithub -Args @(
|
|
796
|
+
"pr", "list", "--repo", "$script:GH_OWNER/$script:GH_REPO",
|
|
797
|
+
"--state", "closed", "--search", $search,
|
|
798
|
+
"--json", "number,title,body,author",
|
|
799
|
+
"--limit", "10"
|
|
800
|
+
)
|
|
801
|
+
if (-not $prJson) { return $false }
|
|
802
|
+
$prs = $prJson | ConvertFrom-Json
|
|
803
|
+
if (-not $prs) { return $false }
|
|
804
|
+
$pattern = "(?i)(PR\s*#?$PRNumber|#$PRNumber)"
|
|
805
|
+
foreach ($pr in $prs) {
|
|
806
|
+
if (($pr.title -match $pattern) -or ($pr.body -match $pattern)) {
|
|
807
|
+
$authorLogin = if ($pr.author -and $pr.author.login) { $pr.author.login } else { "" }
|
|
808
|
+
if ($authorLogin -match "copilot|bot") {
|
|
809
|
+
return $true
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return $false
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function Mark-PRReady {
|
|
817
|
+
<#
|
|
818
|
+
.SYNOPSIS Mark a PR as ready for review (exit draft).
|
|
819
|
+
#>
|
|
820
|
+
[CmdletBinding()]
|
|
821
|
+
param([Parameter(Mandatory)][int]$PRNumber)
|
|
822
|
+
$result = Invoke-VKGithub -Args @(
|
|
823
|
+
"pr", "ready", $PRNumber.ToString(), "--repo", "$script:GH_OWNER/$script:GH_REPO"
|
|
824
|
+
)
|
|
825
|
+
return ($null -ne $result)
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function Find-CopilotFixPR {
|
|
829
|
+
<#
|
|
830
|
+
.SYNOPSIS Find a Copilot fix PR referencing an original PR number.
|
|
831
|
+
#>
|
|
832
|
+
[CmdletBinding()]
|
|
833
|
+
param([Parameter(Mandatory)][int]$OriginalPRNumber)
|
|
834
|
+
$search = "$OriginalPRNumber in:title,body"
|
|
835
|
+
$prJson = Invoke-VKGithub -Args @(
|
|
836
|
+
"pr", "list", "--repo", "$script:GH_OWNER/$script:GH_REPO",
|
|
837
|
+
"--state", "open", "--search", $search,
|
|
838
|
+
"--json", "number,title,body,headRefName,baseRefName,isDraft,url,createdAt",
|
|
839
|
+
"--limit", "20"
|
|
840
|
+
)
|
|
841
|
+
if (-not $prJson) { return $null }
|
|
842
|
+
$prs = $prJson | ConvertFrom-Json
|
|
843
|
+
if (-not $prs) { return $null }
|
|
844
|
+
$pattern = "(?i)(PR\s*#?$OriginalPRNumber|#$OriginalPRNumber)"
|
|
845
|
+
$match = $prs | Where-Object {
|
|
846
|
+
($_.title -match $pattern) -or ($_.body -match $pattern)
|
|
847
|
+
} | Sort-Object -Property createdAt -Descending | Select-Object -First 1
|
|
848
|
+
return $match
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function Test-CopilotPRComplete {
|
|
852
|
+
<#
|
|
853
|
+
.SYNOPSIS Determine whether a Copilot PR is complete (not WIP and not draft).
|
|
854
|
+
#>
|
|
855
|
+
[CmdletBinding()]
|
|
856
|
+
param([Parameter(Mandatory)][object]$PRDetails)
|
|
857
|
+
if (-not $PRDetails) { return $false }
|
|
858
|
+
if ($PRDetails.isDraft) { return $false }
|
|
859
|
+
if ($PRDetails.title -match "^\[WIP\]") { return $false }
|
|
860
|
+
return $true
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function Close-PRDeleteBranch {
|
|
864
|
+
<#
|
|
865
|
+
.SYNOPSIS Close a PR and delete its branch.
|
|
866
|
+
#>
|
|
867
|
+
[CmdletBinding()]
|
|
868
|
+
param([Parameter(Mandatory)][int]$PRNumber)
|
|
869
|
+
$result = Invoke-VKGithub -Args @(
|
|
870
|
+
"pr", "close", $PRNumber.ToString(), "--repo", "$script:GH_OWNER/$script:GH_REPO",
|
|
871
|
+
"--delete-branch"
|
|
872
|
+
)
|
|
873
|
+
return ($null -ne $result)
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function Merge-BranchFromPR {
|
|
877
|
+
<#
|
|
878
|
+
.SYNOPSIS Merge a PR head branch into a target base branch.
|
|
879
|
+
#>
|
|
880
|
+
[CmdletBinding()]
|
|
881
|
+
param(
|
|
882
|
+
[Parameter(Mandatory)][string]$BaseBranch,
|
|
883
|
+
[Parameter(Mandatory)][string]$HeadBranch
|
|
884
|
+
)
|
|
885
|
+
$result = Invoke-VKGithub -Args @(
|
|
886
|
+
"api", "repos/$script:GH_OWNER/$script:GH_REPO/merges",
|
|
887
|
+
"-f", "base=$BaseBranch",
|
|
888
|
+
"-f", "head=$HeadBranch"
|
|
889
|
+
)
|
|
890
|
+
return ($null -ne $result)
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function Create-PRForBranch {
|
|
894
|
+
<#
|
|
895
|
+
.SYNOPSIS Create a PR for a branch when the agent did not open one.
|
|
896
|
+
#>
|
|
897
|
+
[CmdletBinding()]
|
|
898
|
+
param(
|
|
899
|
+
[Parameter(Mandatory)][string]$Branch,
|
|
900
|
+
[Parameter(Mandatory)][string]$Title,
|
|
901
|
+
[string]$Body = "Automated PR created by ve-orchestrator"
|
|
902
|
+
)
|
|
903
|
+
$baseBranch = $script:VK_TARGET_BRANCH
|
|
904
|
+
if ($baseBranch -like "origin/*") { $baseBranch = $baseBranch.Substring(7) }
|
|
905
|
+
$result = Invoke-VKGithub -Args @(
|
|
906
|
+
"pr", "create", "--repo", "$script:GH_OWNER/$script:GH_REPO",
|
|
907
|
+
"--head", $Branch,
|
|
908
|
+
"--base", $baseBranch,
|
|
909
|
+
"--title", $Title,
|
|
910
|
+
"--body", $Body
|
|
911
|
+
)
|
|
912
|
+
return ($null -ne $result)
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function Test-RemoteBranchExists {
|
|
916
|
+
<#
|
|
917
|
+
.SYNOPSIS Check if a branch exists on the remote GitHub repo.
|
|
918
|
+
#>
|
|
919
|
+
[CmdletBinding()]
|
|
920
|
+
param([Parameter(Mandatory)][string]$Branch)
|
|
921
|
+
$result = Invoke-VKGithub -Args @(
|
|
922
|
+
"api",
|
|
923
|
+
"repos/$script:GH_OWNER/$script:GH_REPO/branches/$Branch"
|
|
924
|
+
)
|
|
925
|
+
return ($null -ne $result)
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function Get-VKSessions {
|
|
929
|
+
<#
|
|
930
|
+
.SYNOPSIS List sessions for a workspace.
|
|
931
|
+
#>
|
|
932
|
+
[CmdletBinding()]
|
|
933
|
+
param([Parameter(Mandatory)][string]$WorkspaceId)
|
|
934
|
+
$result = Invoke-VKApi -Path "/api/sessions?workspace_id=$WorkspaceId"
|
|
935
|
+
if (-not $result) { return @() }
|
|
936
|
+
return @($result)
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function New-VKSession {
|
|
940
|
+
<#
|
|
941
|
+
.SYNOPSIS Create a new session for a workspace.
|
|
942
|
+
#>
|
|
943
|
+
[CmdletBinding()]
|
|
944
|
+
param([Parameter(Mandatory)][string]$WorkspaceId)
|
|
945
|
+
$body = @{ workspace_id = $WorkspaceId }
|
|
946
|
+
return Invoke-VKApi -Path "/api/sessions" -Method "POST" -Body $body
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function Get-ExecutorProfileForSession {
|
|
950
|
+
<#
|
|
951
|
+
.SYNOPSIS Map a session executor to an executor profile.
|
|
952
|
+
#>
|
|
953
|
+
[CmdletBinding()]
|
|
954
|
+
param([Parameter(Mandatory)][string]$Executor)
|
|
955
|
+
switch ($Executor) {
|
|
956
|
+
"COPILOT" { return @{ executor = "COPILOT"; variant = "CLAUDE_OPUS_4_6" } }
|
|
957
|
+
"CODEX" { return @{ executor = "CODEX"; variant = "DEFAULT" } }
|
|
958
|
+
default { return @{ executor = "CODEX"; variant = "DEFAULT" } }
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function Send-VKWorkspaceFollowUp {
|
|
963
|
+
<#
|
|
964
|
+
.SYNOPSIS Send a direct follow-up message to the latest session for a workspace.
|
|
965
|
+
#>
|
|
966
|
+
[CmdletBinding()]
|
|
967
|
+
param(
|
|
968
|
+
[Parameter(Mandatory)][string]$WorkspaceId,
|
|
969
|
+
[Parameter(Mandatory)][string]$Message,
|
|
970
|
+
[hashtable]$ExecutorOverride
|
|
971
|
+
)
|
|
972
|
+
$sessions = Get-VKSessions -WorkspaceId $WorkspaceId
|
|
973
|
+
if (-not $sessions -or @($sessions).Count -eq 0) { return $false }
|
|
974
|
+
$session = $sessions | Sort-Object -Property updated_at -Descending | Select-Object -First 1
|
|
975
|
+
$profile = if ($ExecutorOverride) { $ExecutorOverride } else { Get-ExecutorProfileForSession -Executor $session.executor }
|
|
976
|
+
return Send-VKSessionFollowUp -SessionId $session.id -Message $Message -ExecutorProfile $profile
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function Send-VKSessionFollowUp {
|
|
980
|
+
<#
|
|
981
|
+
.SYNOPSIS Send a follow-up message to a specific session.
|
|
982
|
+
#>
|
|
983
|
+
[CmdletBinding()]
|
|
984
|
+
param(
|
|
985
|
+
[Parameter(Mandatory)][string]$SessionId,
|
|
986
|
+
[Parameter(Mandatory)][string]$Message,
|
|
987
|
+
[Parameter(Mandatory)][hashtable]$ExecutorProfile
|
|
988
|
+
)
|
|
989
|
+
$body = @{ prompt = $Message; executor_profile_id = $ExecutorProfile }
|
|
990
|
+
$result = Invoke-VKApi -Path "/api/sessions/$SessionId/follow-up" -Method "POST" -Body $body
|
|
991
|
+
return ($null -ne $result)
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function Queue-VKWorkspaceMessage {
|
|
995
|
+
<#
|
|
996
|
+
.SYNOPSIS Enqueue a message to the latest session for a workspace.
|
|
997
|
+
#>
|
|
998
|
+
[CmdletBinding()]
|
|
999
|
+
param(
|
|
1000
|
+
[Parameter(Mandatory)][string]$WorkspaceId,
|
|
1001
|
+
[Parameter(Mandatory)][string]$Message
|
|
1002
|
+
)
|
|
1003
|
+
$sessions = Get-VKSessions -WorkspaceId $WorkspaceId
|
|
1004
|
+
if (-not $sessions -or @($sessions).Count -eq 0) { return $false }
|
|
1005
|
+
$session = $sessions | Sort-Object -Property updated_at -Descending | Select-Object -First 1
|
|
1006
|
+
$profile = Get-ExecutorProfileForSession -Executor $session.executor
|
|
1007
|
+
$body = @{ executor_profile_id = $profile; message = $Message }
|
|
1008
|
+
$result = Invoke-VKApi -Path "/api/sessions/$($session.id)/queue" -Method "POST" -Body $body
|
|
1009
|
+
return ($null -ne $result)
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function Merge-PR {
|
|
1013
|
+
<#
|
|
1014
|
+
.SYNOPSIS Merge a PR after rebase onto latest main. Returns $true on success.
|
|
1015
|
+
#>
|
|
1016
|
+
[CmdletBinding()]
|
|
1017
|
+
param(
|
|
1018
|
+
[Parameter(Mandatory)][int]$PRNumber,
|
|
1019
|
+
[switch]$AutoMerge,
|
|
1020
|
+
[switch]$Admin
|
|
1021
|
+
)
|
|
1022
|
+
$args = @(
|
|
1023
|
+
"pr", "merge", $PRNumber.ToString(), "--repo", "$script:GH_OWNER/$script:GH_REPO",
|
|
1024
|
+
"--squash", "--delete-branch"
|
|
1025
|
+
)
|
|
1026
|
+
if ($AutoMerge) { $args += "--auto" }
|
|
1027
|
+
if ($Admin) { $args += "--admin" }
|
|
1028
|
+
if ($AutoMerge) {
|
|
1029
|
+
Write-Host " Enabling auto-merge for PR #$PRNumber ..." -ForegroundColor Yellow
|
|
1030
|
+
}
|
|
1031
|
+
elseif ($Admin) {
|
|
1032
|
+
Write-Host " Admin merging PR #$PRNumber ..." -ForegroundColor Yellow
|
|
1033
|
+
}
|
|
1034
|
+
else {
|
|
1035
|
+
Write-Host " Merging PR #$PRNumber ..." -ForegroundColor Yellow
|
|
1036
|
+
}
|
|
1037
|
+
$result = Invoke-VKGithub -Args $args
|
|
1038
|
+
return ($null -ne $result)
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function Enable-AutoMerge {
|
|
1042
|
+
<#
|
|
1043
|
+
.SYNOPSIS Enable auto-merge on a PR so it merges when CI passes.
|
|
1044
|
+
#>
|
|
1045
|
+
[CmdletBinding()]
|
|
1046
|
+
param([Parameter(Mandatory)][int]$PRNumber)
|
|
1047
|
+
$result = Invoke-VKGithub -Args @(
|
|
1048
|
+
"pr", "merge", $PRNumber.ToString(), "--repo", "$script:GH_OWNER/$script:GH_REPO",
|
|
1049
|
+
"--auto", "--squash", "--delete-branch"
|
|
1050
|
+
)
|
|
1051
|
+
return ($null -ne $result)
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
# ─── Orchestration Helpers ────────────────────────────────────────────────────
|
|
1055
|
+
|
|
1056
|
+
function Get-ActiveAttemptCount {
|
|
1057
|
+
<#
|
|
1058
|
+
.SYNOPSIS Count non-archived attempts that have running agents.
|
|
1059
|
+
#>
|
|
1060
|
+
$attempts = Get-VKAttempts -ActiveOnly
|
|
1061
|
+
return @($attempts).Count
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function Get-AttemptTaskMap {
|
|
1065
|
+
<#
|
|
1066
|
+
.SYNOPSIS Build a hashtable mapping task_id → latest attempt for active (non-archived) attempts.
|
|
1067
|
+
#>
|
|
1068
|
+
$attempts = Get-VKAttempts -ActiveOnly
|
|
1069
|
+
$map = @{}
|
|
1070
|
+
foreach ($a in $attempts) {
|
|
1071
|
+
$map[$a.task_id] = $a
|
|
1072
|
+
}
|
|
1073
|
+
return $map
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function Test-AttemptComplete {
|
|
1077
|
+
<#
|
|
1078
|
+
.SYNOPSIS Check if an attempt has completed (branch pushed + PR exists or is merged).
|
|
1079
|
+
#>
|
|
1080
|
+
[CmdletBinding()]
|
|
1081
|
+
param([Parameter(Mandatory)][object]$Attempt)
|
|
1082
|
+
if (-not $Attempt.branch) { return $false }
|
|
1083
|
+
$pr = Get-PRForBranch -Branch $Attempt.branch
|
|
1084
|
+
return ($null -ne $pr)
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
# ─── Display Helpers ──────────────────────────────────────────────────────────
|
|
1088
|
+
|
|
1089
|
+
function Show-Tasks {
|
|
1090
|
+
[CmdletBinding()]
|
|
1091
|
+
param(
|
|
1092
|
+
[ValidateSet("todo", "inprogress", "inreview", "done", "cancelled")]
|
|
1093
|
+
[string]$Status = "todo"
|
|
1094
|
+
)
|
|
1095
|
+
$tasks = Get-VKTasks -Status $Status
|
|
1096
|
+
if (-not $tasks) {
|
|
1097
|
+
Write-Host " No tasks with status '$Status'" -ForegroundColor Yellow
|
|
1098
|
+
return
|
|
1099
|
+
}
|
|
1100
|
+
Write-Host ""
|
|
1101
|
+
Write-Host " ┌─ $($Status.ToUpper()) Tasks ($(@($tasks).Count)) ─────────────────" -ForegroundColor Cyan
|
|
1102
|
+
foreach ($t in $tasks) {
|
|
1103
|
+
$age = if ($t.created_at) { [math]::Round(((Get-Date) - [datetime]$t.created_at).TotalDays, 0) } else { "?" }
|
|
1104
|
+
$inProgress = if ($t.has_in_progress_attempt) { " [RUNNING]" } else { "" }
|
|
1105
|
+
Write-Host " │ $($t.id.Substring(0,8)) $($t.title)$inProgress" -ForegroundColor $(if ($t.has_in_progress_attempt) { "Yellow" } else { "White" })
|
|
1106
|
+
}
|
|
1107
|
+
Write-Host " └───────────────────────────────────────────" -ForegroundColor Cyan
|
|
1108
|
+
Write-Host ""
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function Show-Status {
|
|
1112
|
+
[CmdletBinding()]
|
|
1113
|
+
param(
|
|
1114
|
+
[switch]$ShowIdle
|
|
1115
|
+
)
|
|
1116
|
+
if (-not (Initialize-VKConfig)) { return }
|
|
1117
|
+
$active = Get-VKAttempts -ActiveOnly
|
|
1118
|
+
$todoTasks = Get-VKTasks -Status "todo"
|
|
1119
|
+
$inProgressTasks = Get-VKTasks -Status "inprogress"
|
|
1120
|
+
|
|
1121
|
+
Write-Host ""
|
|
1122
|
+
Write-Host " ╔═══════════════════════════════════════════╗" -ForegroundColor Cyan
|
|
1123
|
+
Write-Host " ║ VirtEngine Kanban Status ║" -ForegroundColor Cyan
|
|
1124
|
+
Write-Host " ╚═══════════════════════════════════════════╝" -ForegroundColor Cyan
|
|
1125
|
+
Write-Host ""
|
|
1126
|
+
Write-Host " Todo: $(@($todoTasks).Count) tasks" -ForegroundColor White
|
|
1127
|
+
Write-Host " In-Progress: $(@($inProgressTasks).Count) tasks" -ForegroundColor Yellow
|
|
1128
|
+
Write-Host " Active Attempts: $(@($active).Count)" -ForegroundColor Green
|
|
1129
|
+
Write-Host ""
|
|
1130
|
+
|
|
1131
|
+
if ($active -and @($active).Count -gt 0) {
|
|
1132
|
+
Write-Host " ┌─ Active Attempts ──────────────────────────" -ForegroundColor Green
|
|
1133
|
+
foreach ($a in $active) {
|
|
1134
|
+
$name = if ($a.name) { $a.name.Substring(0, [Math]::Min(60, $a.name.Length)) } else { "(unnamed)" }
|
|
1135
|
+
$pr = Get-PRForBranch -Branch $a.branch 2>$null
|
|
1136
|
+
$prStatus = if ($pr) { "PR #$($pr.number) ($($pr.state))" } else { "No PR" }
|
|
1137
|
+
Write-Host " │ $($a.id.Substring(0,8)) $($a.branch)" -ForegroundColor White
|
|
1138
|
+
Write-Host " │ $name" -ForegroundColor DarkGray
|
|
1139
|
+
Write-Host " │ $prStatus" -ForegroundColor $(if ($pr -and $pr.state -eq "MERGED") { "Green" } elseif ($pr) { "Yellow" } else { "DarkGray" })
|
|
1140
|
+
if ($ShowIdle) {
|
|
1141
|
+
$lastUpdate = if ($a.updated_at) { [datetime]$a.updated_at } elseif ($a.created_at) { [datetime]$a.created_at } else { $null }
|
|
1142
|
+
$idleMinutes = if ($lastUpdate) { [math]::Round(((Get-Date) - $lastUpdate).TotalMinutes, 1) } else { "?" }
|
|
1143
|
+
$updatedAtText = if ($lastUpdate) { $lastUpdate.ToString("u") } else { "unknown" }
|
|
1144
|
+
Write-Host " │ Idle: ${idleMinutes} min (last update: $updatedAtText)" -ForegroundColor DarkGray
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
Write-Host " └────────────────────────────────────────────" -ForegroundColor Green
|
|
1148
|
+
}
|
|
1149
|
+
Write-Host ""
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
# ─── CLI Dispatch ─────────────────────────────────────────────────────────────
|
|
1153
|
+
|
|
1154
|
+
function Invoke-CLI {
|
|
1155
|
+
param([string[]]$Arguments)
|
|
1156
|
+
|
|
1157
|
+
if ($null -eq $Arguments) {
|
|
1158
|
+
$Arguments = @()
|
|
1159
|
+
}
|
|
1160
|
+
else {
|
|
1161
|
+
$Arguments = @($Arguments)
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
if ($Arguments.Count -eq 0) {
|
|
1165
|
+
Show-Usage
|
|
1166
|
+
return
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
$command = $Arguments[0]
|
|
1170
|
+
$rest = if ($Arguments.Count -gt 1) { $Arguments[1..($Arguments.Count - 1)] } else { @() }
|
|
1171
|
+
|
|
1172
|
+
switch ($command) {
|
|
1173
|
+
"create" {
|
|
1174
|
+
$title = $null
|
|
1175
|
+
$description = $null
|
|
1176
|
+
$descFile = $null
|
|
1177
|
+
$status = "todo"
|
|
1178
|
+
for ($i = 0; $i -lt $rest.Count; $i++) {
|
|
1179
|
+
if ($rest[$i] -in @("--title", "-t") -and ($i + 1) -lt $rest.Count) { $title = $rest[$i + 1] }
|
|
1180
|
+
if ($rest[$i] -in @("--description", "--desc", "-d") -and ($i + 1) -lt $rest.Count) { $description = $rest[$i + 1] }
|
|
1181
|
+
if ($rest[$i] -in @("--description-file", "--desc-file") -and ($i + 1) -lt $rest.Count) { $descFile = $rest[$i + 1] }
|
|
1182
|
+
if ($rest[$i] -in @("--status", "-s") -and ($i + 1) -lt $rest.Count) { $status = $rest[$i + 1] }
|
|
1183
|
+
}
|
|
1184
|
+
if (-not $description -and $descFile) {
|
|
1185
|
+
try { $description = Get-Content -Path $descFile -Raw }
|
|
1186
|
+
catch { Write-Error "Failed to read description file: $descFile"; return }
|
|
1187
|
+
}
|
|
1188
|
+
if (-not $title -or -not $description) {
|
|
1189
|
+
Write-Error "Usage: ve-kanban create --title <title> --description <markdown> [--status todo]"
|
|
1190
|
+
return
|
|
1191
|
+
}
|
|
1192
|
+
$result = Create-VKTask -Title $title -Description $description -Status $status
|
|
1193
|
+
if ($result -and $result.id) {
|
|
1194
|
+
Write-Host " ✓ Task created: $($result.id) — $title" -ForegroundColor Green
|
|
1195
|
+
}
|
|
1196
|
+
elseif ($result) {
|
|
1197
|
+
Write-Host " ✓ Task created: $title" -ForegroundColor Green
|
|
1198
|
+
}
|
|
1199
|
+
else {
|
|
1200
|
+
Write-Error "Failed to create task."
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
"list" {
|
|
1204
|
+
$status = "todo"
|
|
1205
|
+
for ($i = 0; $i -lt $rest.Count; $i++) {
|
|
1206
|
+
if ($rest[$i] -in @("--status", "-s") -and ($i + 1) -lt $rest.Count) { $status = $rest[$i + 1] }
|
|
1207
|
+
}
|
|
1208
|
+
Show-Tasks -Status $status
|
|
1209
|
+
}
|
|
1210
|
+
"status" {
|
|
1211
|
+
$showIdle = $false
|
|
1212
|
+
for ($i = 0; $i -lt $rest.Count; $i++) {
|
|
1213
|
+
if ($rest[$i] -in @("--verbose", "-v")) { $showIdle = $true }
|
|
1214
|
+
}
|
|
1215
|
+
if ($VerbosePreference -eq "Continue") { $showIdle = $true }
|
|
1216
|
+
if (Test-CLIFlagPresent -Flags @("--verbose", "-v") -Arguments $rest) { $showIdle = $true }
|
|
1217
|
+
Show-Status -ShowIdle:$showIdle
|
|
1218
|
+
}
|
|
1219
|
+
"archived" {
|
|
1220
|
+
$archived = Get-VKArchivedAttempts
|
|
1221
|
+
if (-not $archived -or @($archived).Count -eq 0) {
|
|
1222
|
+
Write-Host " No archived attempts." -ForegroundColor Yellow
|
|
1223
|
+
return
|
|
1224
|
+
}
|
|
1225
|
+
Write-Host "";
|
|
1226
|
+
Write-Host " ┌─ Archived Attempts ─────────────────────────" -ForegroundColor DarkGray
|
|
1227
|
+
foreach ($a in $archived) {
|
|
1228
|
+
$name = if ($a.name) { $a.name.Substring(0, [Math]::Min(60, $a.name.Length)) } else { "(unnamed)" }
|
|
1229
|
+
Write-Host " │ $($a.id.Substring(0,8)) $($a.branch)" -ForegroundColor White
|
|
1230
|
+
Write-Host " │ $name" -ForegroundColor DarkGray
|
|
1231
|
+
}
|
|
1232
|
+
Write-Host " └────────────────────────────────────────────" -ForegroundColor DarkGray
|
|
1233
|
+
Write-Host ""
|
|
1234
|
+
}
|
|
1235
|
+
"unarchive" {
|
|
1236
|
+
if ($rest.Count -eq 0) { Write-Error "Usage: ve-kanban unarchive <attempt-id>"; return }
|
|
1237
|
+
Unarchive-VKAttempt -AttemptId $rest[0] | Out-Null
|
|
1238
|
+
Write-Host " ✓ Attempt $($rest[0]) unarchived" -ForegroundColor Green
|
|
1239
|
+
}
|
|
1240
|
+
"submit" {
|
|
1241
|
+
if ($rest.Count -eq 0) { Write-Error "Usage: ve-kanban submit <task-id>"; return }
|
|
1242
|
+
$taskId = $rest[0]
|
|
1243
|
+
Submit-VKTaskAttempt -TaskId $taskId
|
|
1244
|
+
}
|
|
1245
|
+
"submit-next" {
|
|
1246
|
+
$count = 1
|
|
1247
|
+
for ($i = 0; $i -lt $rest.Count; $i++) {
|
|
1248
|
+
if ($rest[$i] -in @("--count", "-n") -and ($i + 1) -lt $rest.Count) { $count = [int]$rest[$i + 1] }
|
|
1249
|
+
}
|
|
1250
|
+
$tasks = Get-VKNextTodoTasks -Count $count
|
|
1251
|
+
if (-not $tasks -or @($tasks).Count -eq 0) {
|
|
1252
|
+
Write-Host " No todo tasks available." -ForegroundColor Yellow
|
|
1253
|
+
return
|
|
1254
|
+
}
|
|
1255
|
+
foreach ($t in $tasks) {
|
|
1256
|
+
Write-Host " → $($t.title)" -ForegroundColor White
|
|
1257
|
+
Submit-VKTaskAttempt -TaskId $t.id
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
"rebase" {
|
|
1261
|
+
if ($rest.Count -eq 0) { Write-Error "Usage: ve-kanban rebase <attempt-id>"; return }
|
|
1262
|
+
Invoke-VKRebase -AttemptId $rest[0]
|
|
1263
|
+
}
|
|
1264
|
+
"merge" {
|
|
1265
|
+
$branch = $null
|
|
1266
|
+
$auto = $false
|
|
1267
|
+
for ($i = 0; $i -lt $rest.Count; $i++) {
|
|
1268
|
+
if ($rest[$i] -in @("--branch", "-b") -and ($i + 1) -lt $rest.Count) { $branch = $rest[$i + 1] }
|
|
1269
|
+
if ($rest[$i] -eq "--auto") { $auto = $true }
|
|
1270
|
+
}
|
|
1271
|
+
if (-not $branch -and $rest.Count -gt 0 -and $rest[0] -notlike "--*") { $branch = $rest[0] }
|
|
1272
|
+
if (-not $branch) { Write-Error "Usage: ve-kanban merge <branch> [--auto]"; return }
|
|
1273
|
+
$pr = Get-PRForBranch -Branch $branch
|
|
1274
|
+
if (-not $pr) { Write-Error "No PR found for branch $branch"; return }
|
|
1275
|
+
if ($auto) {
|
|
1276
|
+
Enable-AutoMerge -PRNumber $pr.number
|
|
1277
|
+
}
|
|
1278
|
+
else {
|
|
1279
|
+
Merge-PR -PRNumber $pr.number
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
"complete" {
|
|
1283
|
+
if ($rest.Count -eq 0) { Write-Error "Usage: ve-kanban complete <task-id>"; return }
|
|
1284
|
+
Update-VKTaskStatus -TaskId $rest[0] -Status "done"
|
|
1285
|
+
Write-Host " ✓ Task $($rest[0]) marked as done" -ForegroundColor Green
|
|
1286
|
+
}
|
|
1287
|
+
"orchestrate" {
|
|
1288
|
+
$parallel = 2
|
|
1289
|
+
$interval = 60
|
|
1290
|
+
for ($i = 0; $i -lt $rest.Count; $i++) {
|
|
1291
|
+
if ($rest[$i] -in @("--parallel", "-p") -and ($i + 1) -lt $rest.Count) { $parallel = [int]$rest[$i + 1] }
|
|
1292
|
+
if ($rest[$i] -in @("--interval", "-i") -and ($i + 1) -lt $rest.Count) { $interval = [int]$rest[$i + 1] }
|
|
1293
|
+
}
|
|
1294
|
+
Write-Host " Delegating to ve-orchestrator.ps1 with parallel=$parallel, interval=$interval" -ForegroundColor Cyan
|
|
1295
|
+
& "$PSScriptRoot/ve-orchestrator.ps1" -MaxParallel $parallel -PollIntervalSec $interval
|
|
1296
|
+
}
|
|
1297
|
+
"help" { Show-Usage }
|
|
1298
|
+
default {
|
|
1299
|
+
Write-Error "Unknown command: $command"
|
|
1300
|
+
Show-Usage
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function Show-Usage {
|
|
1306
|
+
Write-Host @"
|
|
1307
|
+
|
|
1308
|
+
VirtEngine Kanban CLI (ve-kanban)
|
|
1309
|
+
═════════════════════════════════
|
|
1310
|
+
|
|
1311
|
+
COMMANDS:
|
|
1312
|
+
create --title <title> --description <md> [--status todo]
|
|
1313
|
+
Create a new task
|
|
1314
|
+
list [--status <status>] List tasks (default: todo)
|
|
1315
|
+
status Show dashboard (active attempts + queues)
|
|
1316
|
+
status --verbose | -v Show dashboard with idle minutes
|
|
1317
|
+
archived List archived attempts
|
|
1318
|
+
unarchive <attempt-id> Unarchive an attempt
|
|
1319
|
+
submit <task-id> Submit a single task as an attempt
|
|
1320
|
+
submit-next [--count N] Submit next N todo tasks as attempts
|
|
1321
|
+
rebase <attempt-id> Rebase an attempt branch onto latest main
|
|
1322
|
+
merge <branch> [--auto] Merge PR for a branch (--auto enables auto-merge)
|
|
1323
|
+
complete <task-id> Mark a task as done
|
|
1324
|
+
orchestrate [--parallel N] Run orchestration loop (default: 2 parallel)
|
|
1325
|
+
help Show this help
|
|
1326
|
+
|
|
1327
|
+
ENVIRONMENT:
|
|
1328
|
+
VK_BASE_URL Vibe-kanban API (default: http://127.0.0.1:54089)
|
|
1329
|
+
VK_PROJECT_NAME Project name to auto-detect (default: virtengine)
|
|
1330
|
+
VK_PROJECT_ID Project UUID (auto-detected if empty)
|
|
1331
|
+
VK_REPO_ID Repository UUID (auto-detected if empty)
|
|
1332
|
+
GH_OWNER GitHub owner (default: virtengine-gh)
|
|
1333
|
+
GH_REPO GitHub repo (default: virtengine)
|
|
1334
|
+
|
|
1335
|
+
EXECUTOR CYCLING:
|
|
1336
|
+
Alternates between CODEX/DEFAULT and COPILOT/CLAUDE_OPUS_4_6
|
|
1337
|
+
at 50/50 rate to avoid rate-limiting on either agent.
|
|
1338
|
+
"@ -ForegroundColor White
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
function Test-CLIFlagPresent {
|
|
1342
|
+
[CmdletBinding()]
|
|
1343
|
+
param(
|
|
1344
|
+
[Parameter(Mandatory)][string[]]$Flags,
|
|
1345
|
+
[string[]]$Arguments
|
|
1346
|
+
)
|
|
1347
|
+
if ($Arguments) {
|
|
1348
|
+
foreach ($flag in $Flags) {
|
|
1349
|
+
if ($Arguments -contains $flag) { return $true }
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
if ($script:VK_CLI_RAW_LINE) {
|
|
1353
|
+
foreach ($flag in $Flags) {
|
|
1354
|
+
$pattern = "(^|\s)" + [regex]::Escape($flag) + "(\s|$)"
|
|
1355
|
+
if ($script:VK_CLI_RAW_LINE -match $pattern) { return $true }
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
return $false
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
# Run CLI if invoked directly (not dot-sourced)
|
|
1362
|
+
if ($MyInvocation.InvocationName -ne ".") {
|
|
1363
|
+
$script:VK_CLI_RAW_LINE = $MyInvocation.Line
|
|
1364
|
+
Invoke-CLI -Arguments $args
|
|
1365
|
+
}
|